1 { pkgs, lib, config, ... }:
4 inherit (config) gnupg;
5 unlines = builtins.concatStringsSep "\n";
6 unwords = builtins.concatStringsSep " ";
8 generateKeys = keys: unlines (lib.mapAttrsToList generateKey keys);
12 , algo ? "future-default"
21 info "generateKey uid=\"${uid}\""
22 if ! ${gpg-with-home}/bin/gpg-with-home --list-secret-keys -- "=${uid}" >/dev/null 2>/dev/null
24 ${if passPath != "" then "${pkgs.pass}/bin/pass '${passPath}'" else "cat /dev/null"} |
25 ${gpg-with-home}/bin/gpg-with-home \
26 --batch --pinentry-mode loopback --passphrase-fd 0 \
27 --quick-generate-key "${uid}" "${algo}" "${unwords usage}" "${expire}"
30 fpr=$(${gpg-fingerprint}/bin/gpg-fingerprint -- "=${uid}" | head1)
31 caps=$(${gpg-with-home}/bin/gpg-with-home \
32 --with-colons --fixed-list-mode --with-fingerprint \
33 --list-secret-keys -- "=${uid}" |
34 ${pkgs.gnugrep}/bin/grep '^ssb:' |
35 ${pkgs.coreutils}/bin/cut -d : -f 12 || true)
37 + unlines (map (generateSubKey primary) subKeys)
38 + generateBackupKey "$fpr" primary
43 { expire ? primary.expire
49 info " generateSubKey usage=[${unwords usage}]"
50 if ! printf '%s\n' "$caps" | ${pkgs.gnugrep}/bin/grep -Fqx "${lettersKeyUsage usage}"
52 ${if primary.passPath != "" then "${pkgs.pass}/bin/pass '${primary.passPath}'" else "cat /dev/null"} |
53 ${gpg-with-home}/bin/gpg-with-home \
54 --batch --pinentry-mode loopback --passphrase-fd 0 \
55 --quick-add-key "$fpr" "${algo}" "${unwords usage}" "${expire}"
61 , backupRecipients ? []
65 lib.optionalString (backupRecipients != [])
67 info " generateBackupKey backupRecipients=[${unwords (map (s: "\\\"${s}\\\"") backupRecipients)}]"
68 mkdir -p "${gnupg.gnupgHome}/backup/${uid}/"
69 if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.pubkey.asc"
71 ${gpg-with-home}/bin/gpg-with-home \
73 --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.pubkey.asc" \
74 --export-options export-backup \
77 '' + (if backupRecipients == [""] then
79 if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.revoke.asc" &&
80 ${gpg-with-home}/bin/gpg-with-home --list-secret-keys "${fpr}" | grep -q "sec "
82 ${if passPath != "" then "${pkgs.pass}/bin/pass '${passPath}'" else "cat /dev/null"} |
83 ${gpg-with-home}/bin/gpg-with-home \
84 --pinentry-mode loopback --passphrase-fd 0 \
85 --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.revoke.asc" \
88 if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.privkey.sec"
90 ${if passPath != "" then "${pkgs.pass}/bin/pass '${passPath}'" else "cat /dev/null"} |
91 ${gpg-with-home}/bin/gpg-with-home \
92 --batch --pinentry-mode loopback --passphrase-fd 0 \
93 --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.privkey.sec" \
94 --export-options export-backup \
95 --export-secret-key "${fpr}"
97 if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.subkeys.sec"
99 ${if passPath != "" then "${pkgs.pass}/bin/pass '${passPath}'" else "cat /dev/null"} |
100 ${gpg-with-home}/bin/gpg-with-home \
101 --batch --pinentry-mode loopback --passphrase-fd 0 \
102 --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.subkeys.sec" \
103 --export-options export-backup \
104 --export-secret-subkeys "${fpr}"
107 if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.revoke.asc.gpg"
109 ${if passPath != "" then "${pkgs.pass}/bin/pass '${passPath}'" else "cat /dev/null"} |
110 ${gpg-with-home}/bin/gpg-with-home \
111 --pinentry-mode loopback --passphrase-fd 0 \
112 --armor --gen-revoke "${fpr}" |
113 gpg --encrypt ${recipients backupRecipients} \
114 --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.revoke.asc.gpg"
116 if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.privkey.sec.gpg"
118 ${if passPath != "" then "${pkgs.pass}/bin/pass '${passPath}'" else "cat /dev/null"} |
119 ${gpg-with-home}/bin/gpg-with-home \
120 --batch --pinentry-mode loopback --passphrase-fd 0 \
121 --armor --export-options export-backup \
122 --export-secret-key "${fpr}" |
123 gpg --encrypt ${recipients backupRecipients} \
124 --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.privkey.sec.gpg"
126 if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.subkeys.sec.gpg"
128 ${if passPath != "" then "${pkgs.pass}/bin/pass '${passPath}'" else "cat /dev/null"} |
129 ${gpg-with-home}/bin/gpg-with-home \
130 --batch --pinentry-mode loopback --passphrase-fd 0 \
131 --armor --export-options export-backup \
132 --export-secret-subkeys "${fpr}" |
133 gpg --encrypt ${recipients backupRecipients} \
134 --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.subkeys.sec.gpg"
137 recipients = rs: unwords (map (r: ''--recipient "${refKey r}"'') rs);
138 refKey = key: if builtins.typeOf key == "string" then key else "=${key.uid}";
139 signer = s: if s == null
141 else ''--sign --default-key "${refKey s}"'';
142 lettersKeyUsage = usage:
143 (if builtins.elem "encrypt" usage then "e" else "") +
144 (if builtins.elem "sign" usage then "s" else "") +
145 (if builtins.elem "cert" usage then "c" else "") +
146 (if builtins.elem "auth" usage then "a" else "");
148 passOfFingerprint = key:
150 # which fills a map from the fingerprints of the given key
151 # to its password file.
153 # shell.gnupg.pass.passOfFingerprint
154 for fpr in $(${gpg-fingerprint}/bin/gpg-fingerprint -- "=${key.uid}")
155 do eval "pass_$fpr=\"${key.passPath}\""
160 # which installs an exit and keyboard interruption (^C) trap
161 # removing any pass from gpg-agent
162 # whose keygrip is registered in $keygrips.
167 for keygrip in $keygrips
169 echo >&2 "gpg: forget: keygrip=$keygrip"
170 GNUPGHOME=${gnupg.gnupgHome} \
171 ${pkgs.gnupg}/bin/gpg-connect-agent </dev/null >&2 "CLEAR_PASSPHRASE $keygrip" ||
176 trap 'forgetPass' EXIT INT
178 presetPass = keys: uid:
180 # which preset the pass of given uid into gpg-agent,
181 # using keys to find where the pass is stored.
183 ${unlines (map passOfFingerprint keys)}
185 GNUPGHOME=${gnupg.gnupgHome} \
186 ${pkgs.gnupg}/bin/gpgconf --launch gpg-agent
188 fpr="$(${gpg-fingerprint}/bin/fingerprint -- "${uid}" | head1)"
189 eval pass="\''${pass_$fpr}"
192 for keygrip in $(${gnupg.gpg-keygrip}/bin/gpg-keygrip -- "$fpr")
194 keygrips="$keygrips $keygrip"
195 echo >&2 "gpg: preset: keygrip=$keygrip pass=$pass"
196 ${pkgs.pass}/bin/pass "$pass" |
197 GNUPGHOME=${gnupg.gnupgHome} \
198 ${pkgs.gnupg}/libexec/gpg-preset-passphrase --preset ''${XTRACE:+--verbose} $keygrip
203 # Initialize the keyring according to gnupg.keys.
204 gpg-init = pkgs.writeShellScriptBin "gpg-init" (''
209 generateKeys gnupg.keys
212 # A wrapper around gpg to set GNUPGHOME.
213 gpg-with-home = pkgs.writeScriptBin "gpg-with-home" ''
214 GNUPGHOME=${gnupg.gnupgHome} \
215 exec ${pkgs.gnupg}/bin/gpg "$@"
218 # A wrapper around gpg to get fingerprints.
219 gpg-fingerprint = pkgs.writeScriptBin "gpg-fingerprint" ''
221 ${gpg-with-home}/bin/gpg-with-home \
222 --with-colons --fixed-list-mode --with-fingerprint --with-subkey-fingerprint \
223 --list-public-keys "$@" |
224 while IFS=: read -r t x x x key x x x x uid x
227 while IFS=: read -r t x x x x x x x x fpr x
228 do case $t in (fpr) printf '%s\n' "$fpr"; break;;
234 # A wrapper around gpg to get keygrips.
235 gpg-keygrip = pkgs.writeScriptBin "gpg-keygrip" ''
237 ${gpg-with-home}/bin/gpg-with-home \
238 --with-colons --fixed-list-mode --with-keygrip \
239 --list-public-keys "$@" |
240 while IFS=: read -r t x x x key x x x x uid x
243 while IFS=: read -r t x x x x x x x x grp x
244 do case $t in (grp) printf '%s\n' "$grp"; break;;
250 # A wrapper around gpg to get uids.
251 gpg-uid = pkgs.writeScriptBin "gpg-uid" ''
253 ${gpg-with-home}/bin/gpg-with-home \
254 --with-colons --fixed-list-mode \
255 --list-public-keys "$@" |
256 while IFS=: read -r t st x x x x x id x uid x
260 (u) printf '%s\n' "$uid";;
269 cat >/dev/null # NOTE: consuming all the input avoids useless triggering of pipefail
275 echo >&2 "gpg-init: $*"
281 enable = lib.mkEnableOption "GnuPG shell utilities";
282 gnupgHome = lib.mkOption {
284 default = "sec/gnupg";
288 keys = lib.mkOption {
291 { "John Doe. <contact@example.coop>" = {
294 usage = ["cert" "sign"];
295 passPath = "example.coop/gpg/contact";
297 { algo = "rsa4096"; expire = "1y"; usage = ["sign"];}
298 { algo = "rsa4096"; expire = "1y"; usage = ["encrypt"];}
299 { algo = "rsa4096"; expire = "1y"; usage = ["auth"];}
301 backupRecipients = ["@john@doe.pro"];
304 type = types.attrsOf (types.submodule ({name, ...}: {
308 example = "John Doe <john.doe@example.coop>";
314 algo = lib.mkOption {
315 type = types.enum [ "rsa4096" ];
316 default = "future-default";
319 Cryptographic algorithm.
322 expire = lib.mkOption {
330 usage = lib.mkOption {
331 type = with types; listOf (enum [ "cert" "sign" "encrypt" "auth" "default" ]);
332 default = ["default"];
333 example = ["cert" "sign" "encrypt" "auth"];
338 passPath = lib.mkOption {
340 example = "gnupg/coop/example/contact@";
345 subKeys = lib.mkOption {
346 type = types.listOf (types.submodule {
348 algo = lib.mkOption {
349 type = types.enum [ "rsa4096" ];
353 Cryptographic algorithm.
356 expire = lib.mkOption {
364 usage = lib.mkOption {
365 type = with types; listOf (enum [ "sign" "encrypt" "auth" "default" ]);
366 default = ["default"];
367 example = ["sign" "encrypt" "auth"];
375 backupRecipients = lib.mkOption {
376 type = with types; listOf str;
378 example = ["@john@doe.pro"];
380 Backup keys used to encrypt the a backup copy of the secret keys.
383 postRun = lib.mkOption {
387 Shell code to run after the key has been generated or tested to exist.
393 dirmngrConf = lib.mkOption {
395 apply = s: pkgs.writeText "dirmngr.conf" s;
398 hkp-cacert ${gnupg.keyserverPEM}
399 keyserver hkps://keys.mayfirst.org
401 #log-file ${gnupg.gnupgHome}/dirmngr.log
405 GnuPG's dirmngr.conf content.
408 keyserverPEM = lib.mkOption {
410 apply = s: pkgs.writeText "keyserver.pem" s;
411 default = builtins.readFile gnupg/keyserver.pem;
413 dirmngr's hkp-cacert content.
416 gpgAgentConf = lib.mkOption {
418 apply = s: pkgs.writeText "gpg-agent.conf" s;
420 let pinentry = pkgs.writeShellScript "pinentry" ''
421 #!${pkgs.runtimeShell}
422 # choose pinentry depending on PINENTRY_USER_DATA
423 # this *only works* with gpg2
424 # see https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=802020
425 case "''${PINENTRY_USER_DATA:-tty}" in
426 curses) exec ${pkgs.pinentry.curses}/bin/pinentry-curses "$@";;
427 #emacs) exec ''${pkgs.pinentry.emacs}/bin/pinentry-emacs "$@";;
428 #gnome3) exec ''${pkgs.pinentry.gnome3}/bin/pinentry-gnome3 "$@";;
429 gtk-2) exec ${pkgs.pinentry.gtk2}/bin/pinentry-gtk-2 "$@";;
430 none) exit 1;; # do not ask for passphrase
431 #qt) exec ''${pkgs.pinentry.qt}/bin/pinentry-qt "$@";;
432 tty) exec ${pkgs.pinentry.tty}/bin/pinentry-tty "$@";;
436 allow-loopback-pinentry
437 allow-preset-passphrase
438 default-cache-ttl 17200
439 default-cache-ttl-ssh 17200
442 max-cache-ttl-ssh 17200
443 no-allow-external-cache
444 pinentry-program ${pinentry}
447 GnuPG's gpg-agent.conf content.
450 gpgConf = lib.mkOption {
452 apply = s: pkgs.writeText "gpg.conf" (s+"\n"+gnupg.gpgExtraConf);
454 auto-key-locate keyserver
455 cert-digest-algo SHA512
457 default-preference-list SHA512 SHA384 SHA256 SHA224 AES256 AES192 AES CAST5 TWOFISH BZIP2 ZLIB ZIP Uncompressed
460 keyserver-options no-honor-keyserver-url
464 personal-cipher-preferences AES256 AES CAST5
465 personal-digest-preferences SHA512
467 s2k-cipher-algo AES256
469 s2k-digest-algo SHA512
471 tofu-default-policy unknown
477 GnuPG's gpg.conf content.
480 gpgExtraConf = lib.mkOption {
484 GnuPG's gpg.conf extra content.
488 config = lib.mkIf gnupg.enable {
489 nix-shell.buildInputs = [
496 nix-shell.shellHook = ''
498 ${pkgs.coreutils}/bin/install -dm0700 -D ${gnupg.gnupgHome}
499 ${pkgs.coreutils}/bin/ln -snf ${gnupg.gpgConf} ${gnupg.gnupgHome}/gpg.conf
500 ${pkgs.coreutils}/bin/ln -snf ${gnupg.gpgAgentConf} ${gnupg.gnupgHome}/gpg-agent.conf
501 ${pkgs.coreutils}/bin/ln -snf ${gnupg.dirmngrConf} ${gnupg.gnupgHome}/dirmngr.conf
502 export GNUPGHOME=${gnupg.gnupgHome}
503 install -dm700 "$GNUPGHOME"
504 export GPG_TTY=$(${pkgs.coreutils}/bin/tty)
505 ${pkgs.gnupg}/bin/gpgconf --launch gpg-agent
506 export SSH_AUTH_SOCK=$(${pkgs.gnupg}/bin/gpgconf --list-dirs agent-ssh-socket)