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"
13 , usage ? [ "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 --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 lettersKeyUsage = usage:
140 (if builtins.elem "encrypt" usage then "e" else "") +
141 (if builtins.elem "sign" usage then "s" else "") +
142 (if builtins.elem "cert" usage then "c" else "") +
143 (if builtins.elem "auth" usage then "a" else "");
145 # Initialize the keyring according to gnupg.keys.
146 gpg-init = pkgs.writeShellScriptBin "gpg-init" (''
151 generateKeys gnupg.keys
154 # A wrapper around gpg to set GNUPGHOME.
155 gpg-with-home = pkgs.writeScriptBin "gpg-with-home" ''
156 GNUPGHOME=${gnupg.gnupgHome} \
157 exec ${pkgs.gnupg}/bin/gpg "$@"
160 # A wrapper around gpg to get fingerprints.
161 gpg-fingerprint = pkgs.writeScriptBin "gpg-fingerprint" ''
163 ${gpg-with-home}/bin/gpg-with-home \
164 --with-colons --with-fingerprint --with-subkey-fingerprint \
165 --list-public-keys "$@" |
166 while IFS=: read -r t x x x key x x x x uid x
169 while IFS=: read -r t x x x x x x x x fpr x
170 do case $t in (fpr) printf '%s\n' "$fpr"; break;;
176 # A wrapper around gpg to get keygrips.
177 gpg-keygrip = pkgs.writeScriptBin "gpg-keygrip" ''
179 ${gpg-with-home}/bin/gpg-with-home \
180 --with-colons --with-keygrip \
181 --list-public-keys "$@" |
182 while IFS=: read -r t x x x key x x x x uid x do case $t in (pub|sub|sec|ssb) while IFS=: read -r t x x x x x x x x grp x do case $t in (grp) printf '%s\n' "$grp"; break;; esac done ;; esac done
185 # A wrapper around gpg to get uids.
186 gpg-uid = pkgs.writeScriptBin "gpg-uid" ''
188 ${gpg-with-home}/bin/gpg-with-home \
190 --list-public-keys "$@" |
191 while IFS=: read -r t st x x x x x id x uid x
195 (u) printf '%s\n' "$uid";;
204 cat >/dev/null # NOTE: consuming all the input avoids useless triggering of pipefail
210 echo >&2 "gpg-init: $*"
216 enable = lib.mkEnableOption "GnuPG shell utilities";
217 gnupgHome = lib.mkOption {
219 default = "sec/gnupg";
223 keys = lib.mkOption {
227 "John Doe. <contact@example.coop>" = {
230 usage = [ "cert" "sign" ];
231 passPath = "example.coop/gpg/contact";
233 { algo = "rsa4096"; expire = "1y"; usage = [ "sign" ]; }
234 { algo = "rsa4096"; expire = "1y"; usage = [ "encrypt" ]; }
235 { algo = "rsa4096"; expire = "1y"; usage = [ "auth" ]; }
237 backupRecipients = [ "@john@doe.pro" ];
240 type = types.attrsOf (types.submodule ({ name, ... }: {
244 example = "John Doe <john.doe@example.coop>";
250 algo = lib.mkOption {
251 type = types.enum [ "rsa4096" ];
252 default = "future-default";
255 Cryptographic algorithm.
258 expire = lib.mkOption {
266 usage = lib.mkOption {
267 type = with types; listOf (enum [ "cert" "sign" "encrypt" "auth" "default" ]);
268 default = [ "default" ];
269 example = [ "cert" "sign" "encrypt" "auth" ];
274 passPath = lib.mkOption {
276 example = "gnupg/coop/example/contact@";
281 subKeys = lib.mkOption {
282 type = types.listOf (types.submodule {
284 algo = lib.mkOption {
285 type = types.enum [ "rsa4096" ];
289 Cryptographic algorithm.
292 expire = lib.mkOption {
300 usage = lib.mkOption {
301 type = with types; listOf (enum [ "sign" "encrypt" "auth" "default" ]);
302 default = [ "default" ];
303 example = [ "sign" "encrypt" "auth" ];
311 backupRecipients = lib.mkOption {
312 type = with types; listOf str;
314 example = [ "@john@doe.pro" ];
316 Backup keys used to encrypt the a backup copy of the secret keys.
319 postRun = lib.mkOption {
323 Shell code to run after the key has been generated or tested to exist.
329 dirmngrConf = lib.mkOption {
331 apply = s: pkgs.writeText "dirmngr.conf" s;
334 hkp-cacert ${gnupg.keyserverPEM}
335 keyserver hkps://keys.mayfirst.org
337 #log-file ${gnupg.gnupgHome}/dirmngr.log
341 GnuPG's dirmngr.conf content.
344 keyserverPEM = lib.mkOption {
346 apply = s: pkgs.writeText "keyserver.pem" s;
347 default = builtins.readFile gnupg/keyserver.pem;
349 dirmngr's hkp-cacert content.
352 gpgAgentConf = lib.mkOption {
354 apply = s: pkgs.writeText "gpg-agent.conf" (s + "\n" + gnupg.gpgAgentExtraConf);
357 pinentry = pkgs.writeShellScript "pinentry" ''
358 #!${pkgs.runtimeShell}
359 # choose pinentry depending on PINENTRY_USER_DATA
360 # this *only works* with gpg2
361 # see https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=802020
362 case "''${PINENTRY_USER_DATA:-curses}" in
363 curses) exec ${pkgs.pinentry.curses}/bin/pinentry-curses "$@";;
364 #emacs) exec ''${pkgs.pinentry.emacs}/bin/pinentry-emacs "$@";;
365 #gnome3) exec ''${pkgs.pinentry.gnome3}/bin/pinentry-gnome3 "$@";;
366 gtk-2) exec ''${pkgs.pinentry.gtk2}/bin/pinentry-gtk-2 "$@";;
367 none) exit 1;; # do not ask for passphrase
368 #qt) exec ''${pkgs.pinentry.qt}/bin/pinentry-qt "$@";;
369 tty) exec ${pkgs.pinentry.tty}/bin/pinentry-tty "$@";;
374 allow-loopback-pinentry
375 allow-preset-passphrase
376 default-cache-ttl 17200
377 default-cache-ttl-ssh 17200
380 max-cache-ttl-ssh 17200
381 no-allow-external-cache
382 pinentry-program ${pinentry}
385 GnuPG's gpg-agent.conf content.
388 gpgConf = lib.mkOption {
390 apply = s: pkgs.writeText "gpg.conf" (s + "\n" + gnupg.gpgExtraConf);
392 auto-key-locate keyserver
393 cert-digest-algo SHA512
395 default-preference-list SHA512 SHA384 SHA256 SHA224 AES256 AES192 AES CAST5 TWOFISH BZIP2 ZLIB ZIP Uncompressed
397 keyserver-options no-honor-keyserver-url
401 personal-cipher-preferences AES256 AES CAST5
402 personal-digest-preferences SHA512
404 s2k-cipher-algo AES256
406 s2k-digest-algo SHA512
408 tofu-default-policy unknown
414 GnuPG's gpg.conf content.
417 gpgExtraConf = lib.mkOption {
421 GnuPG's gpg.conf extra content.
424 gpgAgentExtraConf = lib.mkOption {
428 GnuPG's gpg-agent.conf extra content.
432 config = lib.mkIf gnupg.enable {
433 nix-shell.buildInputs = [
440 nix-shell.shellHook = ''
442 ${pkgs.coreutils}/bin/install -dm0700 -D ${gnupg.gnupgHome}
443 ${pkgs.coreutils}/bin/ln -snf ${gnupg.gpgConf} ${gnupg.gnupgHome}/gpg.conf
444 ${pkgs.coreutils}/bin/ln -snf ${gnupg.gpgAgentConf} ${gnupg.gnupgHome}/gpg-agent.conf
445 ${pkgs.coreutils}/bin/ln -snf ${gnupg.dirmngrConf} ${gnupg.gnupgHome}/dirmngr.conf
446 export GNUPGHOME=${gnupg.gnupgHome}
447 install -dm700 "$GNUPGHOME"
448 export GPG_TTY=$(${pkgs.coreutils}/bin/tty)
449 ${pkgs.gnupg}/bin/gpgconf --launch gpg-agent
450 export SSH_AUTH_SOCK=$(${pkgs.gnupg}/bin/gpgconf --list-dirs agent-ssh-socket)