]> Git — Sourcephile - sourcephile-nix.git/blob - shell/modules/tools/security/gnupg.nix
nix: use nixpkgs/patches/wip.diff instead of nixpkgs/overlays.nix and nixos/modules.nix
[sourcephile-nix.git] / shell / modules / tools / security / gnupg.nix
1 { pkgs, lib, config, ... }:
2 let
3 inherit (lib) types;
4 inherit (config) gnupg;
5 unlines = builtins.concatStringsSep "\n";
6 unwords = builtins.concatStringsSep " ";
7
8 generateKeys = keys: unlines (lib.mapAttrsToList generateKey keys);
9 generateKey =
10 uid:
11 { uid ? uid
12 , algo ? "future-default"
13 , usage ? ["default"]
14 , expire ? "-"
15 , passPath
16 , subKeys ? {}
17 , postRun ? ""
18 , ...
19 }@primary:
20 ''
21 info "generateKey uid=\"${uid}\""
22 if ! ${gpg-with-home}/bin/gpg-with-home --list-secret-keys -- "=${uid}" >/dev/null 2>/dev/null
23 then
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}"
28 fi
29 ${head1}
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)
36 ''
37 + unlines (map (generateSubKey primary) subKeys)
38 + generateBackupKey "$fpr" primary
39 + postRun
40 ;
41 generateSubKey =
42 primary:
43 { expire ? primary.expire
44 , algo ? primary.algo
45 , usage
46 , ...
47 }:
48 ''
49 info " generateSubKey usage=[${unwords usage}]"
50 if ! printf '%s\n' "$caps" | ${pkgs.gnugrep}/bin/grep -Fqx "${lettersKeyUsage usage}"
51 then
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}"
56 fi
57 '';
58 generateBackupKey =
59 fpr:
60 { passPath
61 , backupRecipients ? []
62 , uid
63 , ...
64 }:
65 lib.optionalString (backupRecipients != [])
66 ''
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"
70 then
71 ${gpg-with-home}/bin/gpg-with-home \
72 --batch \
73 --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.pubkey.asc" \
74 --export-options export-backup \
75 --export "${fpr}"
76 fi
77 '' + (if backupRecipients == [""] then
78 ''
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 "
81 then
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" \
86 --gen-revoke "${fpr}"
87 fi
88 if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.privkey.sec"
89 then
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}"
96 fi
97 if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.subkeys.sec"
98 then
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}"
105 fi
106 '' else ''
107 if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.revoke.asc.gpg"
108 then
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"
115 fi
116 if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.privkey.sec.gpg"
117 then
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"
125 fi
126 if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.subkeys.sec.gpg"
127 then
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"
135 fi
136 '');
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
140 then ""
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 "");
147
148 passOfFingerprint = key:
149 # Return shell code
150 # which fills a map from the fingerprints of the given key
151 # to its password file.
152 ''
153 # shell.gnupg.pass.passOfFingerprint
154 for fpr in $(${gpg-fingerprint}/bin/gpg-fingerprint -- "=${key.uid}")
155 do eval "pass_$fpr=\"${key.passPath}\""
156 done
157 '';
158 forgetPass =
159 # Return shell code
160 # which installs an exit and keyboard interruption (^C) trap
161 # removing any pass from gpg-agent
162 # whose keygrip is registered in $keygrips.
163 ''
164 # forgetPass
165 keygrips=
166 forgetPass () {
167 for keygrip in $keygrips
168 do
169 echo >&2 "gpg: forget: keygrip=$keygrip"
170 GNUPGHOME=${gnupg.gnupgHome} \
171 ${pkgs.gnupg}/bin/gpg-connect-agent </dev/null >&2 "CLEAR_PASSPHRASE $keygrip" ||
172 true
173 done
174 keygrips=
175 }
176 trap 'forgetPass' EXIT INT
177 '';
178 presetPass = keys: uid:
179 # Return shell code
180 # which preset the pass of given uid into gpg-agent,
181 # using keys to find where the pass is stored.
182 ''
183 ${unlines (map passOfFingerprint keys)}
184 # presetPass
185 GNUPGHOME=${gnupg.gnupgHome} \
186 ${pkgs.gnupg}/bin/gpgconf --launch gpg-agent
187 ${head1}
188 fpr="$(${gpg-fingerprint}/bin/fingerprint -- "${uid}" | head1)"
189 eval pass="\''${pass_$fpr}"
190 if test -n "$pass"
191 then
192 for keygrip in $(${gnupg.gpg-keygrip}/bin/gpg-keygrip -- "$fpr")
193 do
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
199 done
200 fi
201 '';
202
203 # Initialize the keyring according to gnupg.keys.
204 gpg-init = pkgs.writeShellScriptBin "gpg-init" (''
205 set -eu
206 set -o pipefail
207 ${info}
208 '' +
209 generateKeys gnupg.keys
210 );
211
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 "$@"
216 '';
217
218 # A wrapper around gpg to get fingerprints.
219 gpg-fingerprint = pkgs.writeScriptBin "gpg-fingerprint" ''
220 set -eu
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
225 do case $t in
226 (pub|sub|sec|ssb)
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;;
229 esac done
230 ;;
231 esac done
232 '';
233
234 # A wrapper around gpg to get keygrips.
235 gpg-keygrip = pkgs.writeScriptBin "gpg-keygrip" ''
236 set -eu
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
241 do case $t in
242 (pub|sub|sec|ssb)
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;;
245 esac done
246 ;;
247 esac done
248 '';
249
250 # A wrapper around gpg to get uids.
251 gpg-uid = pkgs.writeScriptBin "gpg-uid" ''
252 set -eu
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
257 do case $t in
258 (uid)
259 case $st in
260 (u) printf '%s\n' "$uid";;
261 esac
262 ;;
263 esac done
264 '';
265
266 head1 = ''
267 head1(){
268 IFS= read -r line
269 cat >/dev/null # NOTE: consuming all the input avoids useless triggering of pipefail
270 printf %s "$line"
271 }
272 '';
273 info = ''
274 info(){
275 echo >&2 "gpg-init: $*"
276 }
277 '';
278 in
279 {
280 options.gnupg = {
281 enable = lib.mkEnableOption "GnuPG shell utilities";
282 gnupgHome = lib.mkOption {
283 type = types.path;
284 default = "sec/gnupg";
285 description = ''
286 '';
287 };
288 keys = lib.mkOption {
289 default = {};
290 example =
291 { "John Doe. <contact@example.coop>" = {
292 algo = "rsa4096";
293 expire = "1y";
294 usage = ["cert" "sign"];
295 passPath = "example.coop/gpg/contact";
296 subKeys = [
297 { algo = "rsa4096"; expire = "1y"; usage = ["sign"];}
298 { algo = "rsa4096"; expire = "1y"; usage = ["encrypt"];}
299 { algo = "rsa4096"; expire = "1y"; usage = ["auth"];}
300 ];
301 backupRecipients = ["@john@doe.pro"];
302 };
303 };
304 type = types.attrsOf (types.submodule ({name, ...}: {
305 options = {
306 uid = lib.mkOption {
307 type = types.str;
308 example = "John Doe <john.doe@example.coop>";
309 default = name;
310 description = ''
311 User ID.
312 '';
313 };
314 algo = lib.mkOption {
315 type = types.enum [ "rsa4096" ];
316 default = "future-default";
317 example = "rsa4096";
318 description = ''
319 Cryptographic algorithm.
320 '';
321 };
322 expire = lib.mkOption {
323 type = types.str;
324 default = "0";
325 example = "1y";
326 description = ''
327 Expiration timeout.
328 '';
329 };
330 usage = lib.mkOption {
331 type = with types; listOf (enum [ "cert" "sign" "encrypt" "auth" "default" ]);
332 default = ["default"];
333 example = ["cert" "sign" "encrypt" "auth"];
334 description = ''
335 Cryptographic usage.
336 '';
337 };
338 passPath = lib.mkOption {
339 type = types.str;
340 example = "gnupg/coop/example/contact@";
341 description = ''
342 Password path.
343 '';
344 };
345 subKeys = lib.mkOption {
346 type = types.listOf (types.submodule {
347 options = {
348 algo = lib.mkOption {
349 type = types.enum [ "rsa4096" ];
350 default = "default";
351 example = "rsa4096";
352 description = ''
353 Cryptographic algorithm.
354 '';
355 };
356 expire = lib.mkOption {
357 type = types.str;
358 default = "0";
359 example = "1y";
360 description = ''
361 Expiration timeout.
362 '';
363 };
364 usage = lib.mkOption {
365 type = with types; listOf (enum [ "sign" "encrypt" "auth" "default" ]);
366 default = ["default"];
367 example = ["sign" "encrypt" "auth"];
368 description = ''
369 Cryptographic usage.
370 '';
371 };
372 };
373 });
374 };
375 backupRecipients = lib.mkOption {
376 type = with types; listOf str;
377 default = [];
378 example = ["@john@doe.pro"];
379 description = ''
380 Backup keys used to encrypt the a backup copy of the secret keys.
381 '';
382 };
383 postRun = lib.mkOption {
384 type = types.lines;
385 default = "";
386 description = ''
387 Shell code to run after the key has been generated or tested to exist.
388 '';
389 };
390 };
391 }));
392 };
393 dirmngrConf = lib.mkOption {
394 type = types.lines;
395 apply = s: pkgs.writeText "dirmngr.conf" s;
396 default = ''
397 allow-ocsp
398 hkp-cacert ${gnupg.keyserverPEM}
399 keyserver hkps://keys.mayfirst.org
400 #use-tor
401 #log-file ${gnupg.gnupgHome}/dirmngr.log
402 #standard-resolver
403 '';
404 description = ''
405 GnuPG's dirmngr.conf content.
406 '';
407 };
408 keyserverPEM = lib.mkOption {
409 type = types.lines;
410 apply = s: pkgs.writeText "keyserver.pem" s;
411 default = builtins.readFile gnupg/keyserver.pem;
412 description = ''
413 dirmngr's hkp-cacert content.
414 '';
415 };
416 gpgAgentConf = lib.mkOption {
417 type = types.lines;
418 apply = s: pkgs.writeText "gpg-agent.conf" s;
419 default =
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 "$@";;
433 esac
434 '';
435 in ''
436 allow-loopback-pinentry
437 allow-preset-passphrase
438 default-cache-ttl 17200
439 default-cache-ttl-ssh 17200
440 enable-ssh-support
441 max-cache-ttl 17200
442 max-cache-ttl-ssh 17200
443 no-allow-external-cache
444 pinentry-program ${pinentry}
445 '';
446 description = ''
447 GnuPG's gpg-agent.conf content.
448 '';
449 };
450 gpgConf = lib.mkOption {
451 type = types.lines;
452 apply = s: pkgs.writeText "gpg.conf" (s+"\n"+gnupg.gpgExtraConf);
453 default = ''
454 auto-key-locate keyserver
455 cert-digest-algo SHA512
456 charset utf-8
457 default-preference-list SHA512 SHA384 SHA256 SHA224 AES256 AES192 AES CAST5 TWOFISH BZIP2 ZLIB ZIP Uncompressed
458 fixed-list-mode
459 keyid-format 0xlong
460 keyserver-options no-honor-keyserver-url
461 no-auto-key-locate
462 no-default-keyring
463 no-emit-version
464 personal-cipher-preferences AES256 AES CAST5
465 personal-digest-preferences SHA512
466 quiet
467 s2k-cipher-algo AES256
468 s2k-count 65536
469 s2k-digest-algo SHA512
470 s2k-mode 3
471 tofu-default-policy unknown
472 trust-model tofu+pgp
473 use-agent
474 utf8-strings
475 '';
476 description = ''
477 GnuPG's gpg.conf content.
478 '';
479 };
480 gpgExtraConf = lib.mkOption {
481 type = types.lines;
482 default = "";
483 description = ''
484 GnuPG's gpg.conf extra content.
485 '';
486 };
487 };
488 config = lib.mkIf gnupg.enable {
489 nix-shell.buildInputs = [
490 gpg-with-home
491 gpg-fingerprint
492 gpg-keygrip
493 gpg-uid
494 gpg-init
495 ];
496 nix-shell.shellHook = ''
497 # gnupg
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)
507 '';
508 };
509 }