1 { pkgs, lib, config, ... }:
2 let cfg = config.gnupg;
4 unlines = builtins.concatStringsSep "\n";
5 unwords = builtins.concatStringsSep " ";
7 generateKeys = keys: unlines (lib.mapAttrsToList generateKey keys);
11 , algo ? "future-default"
19 info " generateKey uid=\"${uid}\""
20 if ! ${cfg.gpg-with-home}/bin/gpg-with-home --list-secret-keys -- "=${uid}" >/dev/null 2>/dev/null
22 ${pkgs.pass}/bin/pass "${passPath}" |
23 ${cfg.gpg-with-home}/bin/gpg-with-home \
24 --batch --pinentry-mode loopback --passphrase-fd 0 \
25 --quick-generate-key "${uid}" "${algo}" "${unwords usage}" "${expire}"
28 fpr=$(${cfg.gpg-fingerprint}/bin/gpg-fingerprint -- "=${uid}" | head1)
29 caps=$(${cfg.gpg-with-home}/bin/gpg-with-home \
30 --with-colons --fixed-list-mode --with-fingerprint \
31 --list-secret-keys -- "=${uid}" |
32 ${pkgs.gnugrep}/bin/grep '^ssb:' |
33 ${pkgs.coreutils}/bin/cut -d : -f 12 || true)
35 + unlines (map (generateSubKey primary) subKeys)
36 + generateBackupKey "$fpr" primary
40 { expire ? primary.expire
46 info " generateSubKey usage=[${unwords usage}]"
47 if ! printf '%s\n' "$caps" | ${pkgs.gnugrep}/bin/grep -Fqx "${lettersKeyUsage usage}"
49 ${pkgs.pass}/bin/pass "${primary.passPath}" |
50 ${cfg.gpg-with-home}/bin/gpg-with-home \
51 --batch --pinentry-mode loopback --passphrase-fd 0 \
52 --quick-add-key "$fpr" "${algo}" "${unwords usage}" "${expire}"
58 , backupRecipients ? []
62 lib.optionalString (backupRecipients != [])
64 info " generateBackupKey backupRecipients=[${unwords (map (s: "\\\"${s}\\\"") backupRecipients)}]"
65 mkdir -p "${cfg.dir.var}/backup/${uid}/"
66 if ! test -s "${cfg.dir.var}/backup/${uid}/${fpr}.pubkey.asc"
68 ${cfg.gpg-with-home}/bin/gpg-with-home \
70 --armor --yes --output "${cfg.dir.var}/backup/${uid}/${fpr}.pubkey.asc" \
71 --export-options export-backup \
74 '' + (if backupRecipients == [""] then
76 if ! test -s "${cfg.dir.var}/backup/${uid}/${fpr}.revoke.asc"
78 ${pkgs.pass}/bin/pass "${passPath}" |
79 ${cfg.gpg-with-home}/bin/gpg-with-home \
80 --pinentry-mode loopback --passphrase-fd 0 \
81 --armor --yes --output "${cfg.dir.var}/backup/${uid}/${fpr}.revoke.asc" \
84 if ! test -s "${cfg.dir.var}/backup/${uid}/${fpr}.privkey.sec"
86 ${pkgs.pass}/bin/pass "${passPath}" |
87 ${cfg.gpg-with-home}/bin/gpg-with-home \
88 --batch --pinentry-mode loopback --passphrase-fd 0 \
89 --armor --yes --output "${cfg.dir.var}/backup/${uid}/${fpr}.privkey.sec" \
90 --export-options export-backup \
91 --export-secret-key "${fpr}"
93 if ! test -s "${cfg.dir.var}/backup/${uid}/${fpr}.subkeys.sec"
95 ${pkgs.pass}/bin/pass "${passPath}" |
96 ${cfg.gpg-with-home}/bin/gpg-with-home \
97 --batch --pinentry-mode loopback --passphrase-fd 0 \
98 --armor --yes --output "${cfg.dir.var}/backup/${uid}/${fpr}.subkeys.sec" \
99 --export-options export-backup \
100 --export-secret-subkeys "${fpr}"
103 if ! test -s "${cfg.dir.var}/backup/${uid}/${fpr}.revoke.asc.gpg"
105 ${pkgs.pass}/bin/pass "${passPath}" |
106 ${cfg.gpg-with-home}/bin/gpg-with-home \
107 --pinentry-mode loopback --passphrase-fd 0 \
108 --armor --gen-revoke "${fpr}" |
109 gpg --encrypt ${recipients backupRecipients} \
110 --armor --yes --output "${cfg.dir.var}/backup/${uid}/${fpr}.revoke.asc.gpg"
112 if ! test -s "${cfg.dir.var}/backup/${uid}/${fpr}.privkey.sec.gpg"
114 ${pkgs.pass}/bin/pass "${passPath}" |
115 ${cfg.gpg-with-home}/bin/gpg-with-home \
116 --batch --pinentry-mode loopback --passphrase-fd 0 \
117 --armor --export-options export-backup \
118 --export-secret-key "${fpr}" |
119 gpg --encrypt ${recipients backupRecipients} \
120 --armor --yes --output "${cfg.dir.var}/backup/${uid}/${fpr}.privkey.sec.gpg"
122 if ! test -s "${cfg.dir.var}/backup/${uid}/${fpr}.subkeys.sec.gpg"
124 ${pkgs.pass}/bin/pass "${passPath}" |
125 ${cfg.gpg-with-home}/bin/gpg-with-home \
126 --batch --pinentry-mode loopback --passphrase-fd 0 \
127 --armor --export-options export-backup \
128 --export-secret-subkeys "${fpr}" |
129 gpg --encrypt ${recipients backupRecipients} \
130 --armor --yes --output "${cfg.dir.var}/backup/${uid}/${fpr}.subkeys.sec.gpg"
133 recipients = rs: unwords (map (r: ''--recipient "${refKey r}"'') rs);
134 refKey = key: if builtins.typeOf key == "string" then key else "=${key.uid}";
135 signer = s: if s == null
137 else ''--sign --default-key "${refKey s}"'';
138 lettersKeyUsage = usage:
139 (if builtins.elem "encrypt" usage then "e" else "") +
140 (if builtins.elem "sign" usage then "s" else "") +
141 (if builtins.elem "cert" usage then "c" else "") +
142 (if builtins.elem "auth" usage then "a" else "");
144 passOfFingerprint = key:
146 # which fills a map from the fingerprints of the given key
147 # to its password file.
149 # shell.gnupg.pass.passOfFingerprint
150 for fpr in $(${cfg.gpg-fingerprint}/bin/gpg-fingerprint -- "=${key.uid}")
151 do eval "pass_$fpr=\"${key.passPath}\""
156 # which installs an exit and keyboard interruption (^C) trap
157 # removing any pass from gpg-agent
158 # whose keygrip is registered in $keygrips.
163 for keygrip in $keygrips
165 echo >&2 "gpg: forget: keygrip=$keygrip"
166 GNUPGHOME=${cfg.dir.var} \
167 ${pkgs.gnupg}/bin/gpg-connect-agent </dev/null >&2 "CLEAR_PASSPHRASE $keygrip" ||
172 trap 'forgetPass' EXIT INT
174 presetPass = keys: uid:
176 # which preset the pass of given uid into gpg-agent,
177 # using keys to find where the pass is stored.
179 ${unlines (map passOfFingerprint keys)}
181 GNUPGHOME=${cfg.dir.var} \
182 ${pkgs.gnupg}/bin/gpgconf --launch gpg-agent
184 fpr="$(${cfg.gpg-fingerprint}/bin/fingerprint -- "${uid}" | head1)"
185 eval pass="\''${pass_$fpr}"
188 for keygrip in $(${cfg.gpg-keygrip}/bin/gpg-keygrip -- "$fpr")
190 keygrips="$keygrips $keygrip"
191 echo >&2 "gpg: preset: keygrip=$keygrip pass=$pass"
192 ${pkgs.pass}/bin/pass "$pass" |
193 GNUPGHOME=${cfg.dir.var} \
194 ${pkgs.gnupg}/libexec/gpg-preset-passphrase --preset ''${XTRACE:+--verbose} $keygrip
202 cat >/dev/null # NOTE: consuming all the input avoids useless triggering of pipefail
214 enable = lib.mkEnableOption "GnuPG admin utilities";
215 dir.var = lib.mkOption {
217 default = "sec/gnupg";
221 gpg-with-home = lib.mkOption {
223 apply = pkgs.writeScriptBin "gpg-with-home";
225 GNUPGHOME=${cfg.dir.var} \
226 exec ${pkgs.gnupg}/bin/gpg "$@"
229 A wrapper around gpg to set GNUPGHOME.
232 gpg-fingerprint = lib.mkOption {
234 apply = pkgs.writeScriptBin "gpg-fingerprint";
237 ${cfg.gpg-with-home}/bin/gpg-with-home \
238 --with-colons --fixed-list-mode --with-fingerprint --with-subkey-fingerprint \
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 fpr x
244 do case $t in (fpr) printf '%s\n' "$fpr"; break;;
250 A wrapper around gpg to get fingerprints.
253 gpg-keygrip = lib.mkOption {
255 apply = pkgs.writeScriptBin "gpg-keygrip";
258 ${cfg.gpg-with-home}/bin/gpg-with-home \
259 --with-colons --fixed-list-mode --with-keygrip \
260 --list-public-keys "$@" |
261 while IFS=: read -r t x x x key x x x x uid x
264 while IFS=: read -r t x x x x x x x x grp x
265 do case $t in (grp) printf '%s\n' "$grp"; break;;
271 A wrapper around gpg to get keygrips.
274 gpg-uid = lib.mkOption {
276 apply = pkgs.writeScriptBin "gpg-uid";
279 ${cfg.gpg-with-home}/bin/gpg-with-home \
280 --with-colons --fixed-list-mode \
281 --list-public-keys "$@" |
282 while IFS=: read -r t st x x x x x id x uid x
286 (u) printf '%s\n' "$uid";;
292 A wrapper around gpg to get uids.
295 init = lib.mkOption {
297 apply = pkgs.writeShellScriptBin "init-gpg";
303 ${pkgs.coreutils}/bin/install -dm0700 -D ${cfg.dir.var}
304 ${pkgs.coreutils}/bin/ln -snf ${cfg.gpgConf} ${cfg.dir.var}/gpg.conf
305 ${pkgs.coreutils}/bin/ln -snf ${cfg.gpgAgentConf} ${cfg.dir.var}/gpg-agent.conf
306 ${pkgs.coreutils}/bin/ln -snf ${cfg.dirmngrConf} ${cfg.dir.var}/dirmngr.conf
308 generateKeys cfg.keys;
313 keys = lib.mkOption {
316 { "John Doe. <contact@example.coop>" = {
319 usage = ["cert" "sign"];
320 passPath = "example.coop/gpg/contact";
322 { algo = "rsa4096"; expire = "1y"; usage = ["sign"];}
323 { algo = "rsa4096"; expire = "1y"; usage = ["encrypt"];}
324 { algo = "rsa4096"; expire = "1y"; usage = ["auth"];}
326 backupRecipients = ["@john@doe.pro"];
329 type = types.attrsOf (types.submodule ({uid, ...}: {
330 #config.uid = lib.mkDefault uid;
334 example = "John Doe <john.doe@example.coop>";
340 algo = lib.mkOption {
341 type = types.enum [ "rsa4096" ];
342 default = "future-default";
345 Cryptographic algorithm.
348 expire = lib.mkOption {
356 usage = lib.mkOption {
357 type = with types; listOf (enum [ "cert" "sign" "encrypt" "auth" "default" ]);
358 default = ["default"];
359 example = ["cert" "sign" "encrypt" "auth"];
364 passPath = lib.mkOption {
366 example = "gnupg/coop/example/contact@";
371 subKeys = lib.mkOption {
372 type = types.listOf (types.submodule {
374 algo = lib.mkOption {
375 type = types.enum [ "rsa4096" ];
379 Cryptographic algorithm.
382 expire = lib.mkOption {
390 usage = lib.mkOption {
391 type = with types; listOf (enum [ "sign" "encrypt" "auth" "default" ]);
392 default = ["default"];
393 example = ["sign" "encrypt" "auth"];
401 backupRecipients = lib.mkOption {
402 type = with types; listOf str;
404 example = ["@john@doe.pro"];
406 Backup keys used to encrypt the a backup copy of the secret keys.
412 dirmngrConf = lib.mkOption {
414 apply = s: pkgs.writeText "dirmngr.conf" s;
417 hkp-cacert ${cfg.keyserverPEM}
418 keyserver hkps://keys.mayfirst.org
420 #log-file ${cfg.dir.var}/dirmngr.log
424 GnuPG's dirmngr.conf content.
427 keyserverPEM = lib.mkOption {
429 apply = s: pkgs.writeText "keyserver.pem" s;
430 default = builtins.readFile gnupg/keyserver.pem;
432 dirmngr's hkp-cacert content.
435 gpgAgentConf = lib.mkOption {
437 apply = s: pkgs.writeText "gpg-agent.conf" s;
439 allow-preset-passphrase
440 default-cache-ttl 17200
441 default-cache-ttl-ssh 17200
444 max-cache-ttl-ssh 17200
447 GnuPG's gpg-agent.conf content.
450 gpgConf = lib.mkOption {
452 apply = s: pkgs.writeText "gpg.conf" s;
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.