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"
20 info "generateKey uid=\"${uid}\""
21 if ! ${gpg-with-home}/bin/gpg-with-home --list-secret-keys -- "=${uid}" >/dev/null 2>/dev/null
23 ${pkgs.pass}/bin/pass "${passPath}" |
24 ${gpg-with-home}/bin/gpg-with-home \
25 --batch --pinentry-mode loopback --passphrase-fd 0 \
26 --quick-generate-key "${uid}" "${algo}" "${unwords usage}" "${expire}"
29 fpr=$(${gpg-fingerprint}/bin/gpg-fingerprint -- "=${uid}" | head1)
30 caps=$(${gpg-with-home}/bin/gpg-with-home \
31 --with-colons --fixed-list-mode --with-fingerprint \
32 --list-secret-keys -- "=${uid}" |
33 ${pkgs.gnugrep}/bin/grep '^ssb:' |
34 ${pkgs.coreutils}/bin/cut -d : -f 12 || true)
36 + unlines (map (generateSubKey primary) subKeys)
37 + generateBackupKey "$fpr" primary
41 { expire ? primary.expire
47 info " generateSubKey usage=[${unwords usage}]"
48 if ! printf '%s\n' "$caps" | ${pkgs.gnugrep}/bin/grep -Fqx "${lettersKeyUsage usage}"
50 ${pkgs.pass}/bin/pass "${primary.passPath}" |
51 ${gpg-with-home}/bin/gpg-with-home \
52 --batch --pinentry-mode loopback --passphrase-fd 0 \
53 --quick-add-key "$fpr" "${algo}" "${unwords usage}" "${expire}"
59 , backupRecipients ? []
63 lib.optionalString (backupRecipients != [])
65 info " generateBackupKey backupRecipients=[${unwords (map (s: "\\\"${s}\\\"") backupRecipients)}]"
66 mkdir -p "${gnupg.gnupgHome}/backup/${uid}/"
67 if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.pubkey.asc"
69 ${gpg-with-home}/bin/gpg-with-home \
71 --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.pubkey.asc" \
72 --export-options export-backup \
75 '' + (if backupRecipients == [""] then
77 if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.revoke.asc"
79 ${pkgs.pass}/bin/pass "${passPath}" |
80 ${gpg-with-home}/bin/gpg-with-home \
81 --pinentry-mode loopback --passphrase-fd 0 \
82 --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.revoke.asc" \
85 if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.privkey.sec"
87 ${pkgs.pass}/bin/pass "${passPath}" |
88 ${gpg-with-home}/bin/gpg-with-home \
89 --batch --pinentry-mode loopback --passphrase-fd 0 \
90 --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.privkey.sec" \
91 --export-options export-backup \
92 --export-secret-key "${fpr}"
94 if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.subkeys.sec"
96 ${pkgs.pass}/bin/pass "${passPath}" |
97 ${gpg-with-home}/bin/gpg-with-home \
98 --batch --pinentry-mode loopback --passphrase-fd 0 \
99 --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.subkeys.sec" \
100 --export-options export-backup \
101 --export-secret-subkeys "${fpr}"
104 if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.revoke.asc.gpg"
106 ${pkgs.pass}/bin/pass "${passPath}" |
107 ${gpg-with-home}/bin/gpg-with-home \
108 --pinentry-mode loopback --passphrase-fd 0 \
109 --armor --gen-revoke "${fpr}" |
110 gpg --encrypt ${recipients backupRecipients} \
111 --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.revoke.asc.gpg"
113 if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.privkey.sec.gpg"
115 ${pkgs.pass}/bin/pass "${passPath}" |
116 ${gpg-with-home}/bin/gpg-with-home \
117 --batch --pinentry-mode loopback --passphrase-fd 0 \
118 --armor --export-options export-backup \
119 --export-secret-key "${fpr}" |
120 gpg --encrypt ${recipients backupRecipients} \
121 --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.privkey.sec.gpg"
123 if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.subkeys.sec.gpg"
125 ${pkgs.pass}/bin/pass "${passPath}" |
126 ${gpg-with-home}/bin/gpg-with-home \
127 --batch --pinentry-mode loopback --passphrase-fd 0 \
128 --armor --export-options export-backup \
129 --export-secret-subkeys "${fpr}" |
130 gpg --encrypt ${recipients backupRecipients} \
131 --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.subkeys.sec.gpg"
134 recipients = rs: unwords (map (r: ''--recipient "${refKey r}"'') rs);
135 refKey = key: if builtins.typeOf key == "string" then key else "=${key.uid}";
136 signer = s: if s == null
138 else ''--sign --default-key "${refKey s}"'';
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 passOfFingerprint = key:
147 # which fills a map from the fingerprints of the given key
148 # to its password file.
150 # shell.gnupg.pass.passOfFingerprint
151 for fpr in $(${gpg-fingerprint}/bin/gpg-fingerprint -- "=${key.uid}")
152 do eval "pass_$fpr=\"${key.passPath}\""
157 # which installs an exit and keyboard interruption (^C) trap
158 # removing any pass from gpg-agent
159 # whose keygrip is registered in $keygrips.
164 for keygrip in $keygrips
166 echo >&2 "gpg: forget: keygrip=$keygrip"
167 GNUPGHOME=${gnupg.gnupgHome} \
168 ${pkgs.gnupg}/bin/gpg-connect-agent </dev/null >&2 "CLEAR_PASSPHRASE $keygrip" ||
173 trap 'forgetPass' EXIT INT
175 presetPass = keys: uid:
177 # which preset the pass of given uid into gpg-agent,
178 # using keys to find where the pass is stored.
180 ${unlines (map passOfFingerprint keys)}
182 GNUPGHOME=${gnupg.gnupgHome} \
183 ${pkgs.gnupg}/bin/gpgconf --launch gpg-agent
185 fpr="$(${gpg-fingerprint}/bin/fingerprint -- "${uid}" | head1)"
186 eval pass="\''${pass_$fpr}"
189 for keygrip in $(${gnupg.gpg-keygrip}/bin/gpg-keygrip -- "$fpr")
191 keygrips="$keygrips $keygrip"
192 echo >&2 "gpg: preset: keygrip=$keygrip pass=$pass"
193 ${pkgs.pass}/bin/pass "$pass" |
194 GNUPGHOME=${gnupg.gnupgHome} \
195 ${pkgs.gnupg}/libexec/gpg-preset-passphrase --preset ''${XTRACE:+--verbose} $keygrip
200 # Initialize the keyring according to gnupg.keys.
201 gpg-init = pkgs.writeShellScriptBin "gpg-init" (''
205 ${pkgs.coreutils}/bin/install -dm0700 -D ${gnupg.gnupgHome}
206 ${pkgs.coreutils}/bin/ln -snf ${gnupg.gpgConf} ${gnupg.gnupgHome}/gpg.conf
207 ${pkgs.coreutils}/bin/ln -snf ${gnupg.gpgAgentConf} ${gnupg.gnupgHome}/gpg-agent.conf
208 ${pkgs.coreutils}/bin/ln -snf ${gnupg.dirmngrConf} ${gnupg.gnupgHome}/dirmngr.conf
210 generateKeys gnupg.keys
213 # A wrapper around gpg to set GNUPGHOME.
214 gpg-with-home = pkgs.writeScriptBin "gpg-with-home" ''
215 GNUPGHOME=${gnupg.gnupgHome} \
216 exec ${pkgs.gnupg}/bin/gpg "$@"
219 # A wrapper around gpg to get fingerprints.
220 gpg-fingerprint = pkgs.writeScriptBin "gpg-fingerprint" ''
222 ${gpg-with-home}/bin/gpg-with-home \
223 --with-colons --fixed-list-mode --with-fingerprint --with-subkey-fingerprint \
224 --list-public-keys "$@" |
225 while IFS=: read -r t x x x key x x x x uid x
228 while IFS=: read -r t x x x x x x x x fpr x
229 do case $t in (fpr) printf '%s\n' "$fpr"; break;;
235 # A wrapper around gpg to get keygrips.
236 gpg-keygrip = pkgs.writeScriptBin "gpg-keygrip" ''
238 ${gpg-with-home}/bin/gpg-with-home \
239 --with-colons --fixed-list-mode --with-keygrip \
240 --list-public-keys "$@" |
241 while IFS=: read -r t x x x key x x x x uid x
244 while IFS=: read -r t x x x x x x x x grp x
245 do case $t in (grp) printf '%s\n' "$grp"; break;;
251 # A wrapper around gpg to get uids.
252 gpg-uid = pkgs.writeScriptBin "gpg-uid" ''
254 ${gpg-with-home}/bin/gpg-with-home \
255 --with-colons --fixed-list-mode \
256 --list-public-keys "$@" |
257 while IFS=: read -r t st x x x x x id x uid x
261 (u) printf '%s\n' "$uid";;
270 cat >/dev/null # NOTE: consuming all the input avoids useless triggering of pipefail
276 echo >&2 "gpg-init: $*"
282 enable = lib.mkEnableOption "GnuPG shell utilities";
283 gnupgHome = lib.mkOption {
285 default = "sec/gnupg";
289 keys = lib.mkOption {
292 { "John Doe. <contact@example.coop>" = {
295 usage = ["cert" "sign"];
296 passPath = "example.coop/gpg/contact";
298 { algo = "rsa4096"; expire = "1y"; usage = ["sign"];}
299 { algo = "rsa4096"; expire = "1y"; usage = ["encrypt"];}
300 { algo = "rsa4096"; expire = "1y"; usage = ["auth"];}
302 backupRecipients = ["@john@doe.pro"];
305 type = types.attrsOf (types.submodule ({uid, ...}: {
306 #config.uid = lib.mkDefault uid;
310 example = "John Doe <john.doe@example.coop>";
316 algo = lib.mkOption {
317 type = types.enum [ "rsa4096" ];
318 default = "future-default";
321 Cryptographic algorithm.
324 expire = lib.mkOption {
332 usage = lib.mkOption {
333 type = with types; listOf (enum [ "cert" "sign" "encrypt" "auth" "default" ]);
334 default = ["default"];
335 example = ["cert" "sign" "encrypt" "auth"];
340 passPath = lib.mkOption {
342 example = "gnupg/coop/example/contact@";
347 subKeys = lib.mkOption {
348 type = types.listOf (types.submodule {
350 algo = lib.mkOption {
351 type = types.enum [ "rsa4096" ];
355 Cryptographic algorithm.
358 expire = lib.mkOption {
366 usage = lib.mkOption {
367 type = with types; listOf (enum [ "sign" "encrypt" "auth" "default" ]);
368 default = ["default"];
369 example = ["sign" "encrypt" "auth"];
377 backupRecipients = lib.mkOption {
378 type = with types; listOf str;
380 example = ["@john@doe.pro"];
382 Backup keys used to encrypt the a backup copy of the secret keys.
388 dirmngrConf = lib.mkOption {
390 apply = s: pkgs.writeText "dirmngr.conf" s;
393 hkp-cacert ${gnupg.keyserverPEM}
394 keyserver hkps://keys.mayfirst.org
396 #log-file ${gnupg.gnupgHome}/dirmngr.log
400 GnuPG's dirmngr.conf content.
403 keyserverPEM = lib.mkOption {
405 apply = s: pkgs.writeText "keyserver.pem" s;
406 default = builtins.readFile gnupg/keyserver.pem;
408 dirmngr's hkp-cacert content.
411 gpgAgentConf = lib.mkOption {
413 apply = s: pkgs.writeText "gpg-agent.conf" s;
415 allow-preset-passphrase
416 default-cache-ttl 17200
417 default-cache-ttl-ssh 17200
420 max-cache-ttl-ssh 17200
423 GnuPG's gpg-agent.conf content.
426 gpgConf = lib.mkOption {
428 apply = s: pkgs.writeText "gpg.conf" s;
430 auto-key-locate keyserver
431 cert-digest-algo SHA512
433 default-preference-list SHA512 SHA384 SHA256 SHA224 AES256 AES192 AES CAST5 TWOFISH BZIP2 ZLIB ZIP Uncompressed
436 keyserver-options no-honor-keyserver-url
440 personal-cipher-preferences AES256 AES CAST5
441 personal-digest-preferences SHA512
443 s2k-cipher-algo AES256
445 s2k-digest-algo SHA512
447 tofu-default-policy unknown
453 GnuPG's gpg.conf content.
457 config = lib.mkIf gnupg.enable {
458 nix-shell.buildInputs = [
465 nix-shell.shellHook = ''
467 export GNUPGHOME=${gnupg.gnupgHome}
468 install -dm700 "$GNUPGHOME"
469 export GPG_TTY=$(${pkgs.coreutils}/bin/tty)
470 ${pkgs.gnupg}/bin/gpgconf --launch gpg-agent
471 export SSH_AUTH_SOCK=$(${pkgs.gnupg}/bin/gpgconf --list-dirs agent-ssh-socket)