]> Git — Sourcephile - sourcephile-nix.git/blob - shell/modules/tools/security/gnupg.nix
nix: revamp directories to put nixpkgs-overlays in the store
[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 , ...
18 }@primary:
19 ''
20 info "generateKey uid=\"${uid}\""
21 if ! ${gpg-with-home}/bin/gpg-with-home --list-secret-keys -- "=${uid}" >/dev/null 2>/dev/null
22 then
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}"
27 fi
28 ${head1}
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)
35 ''
36 + unlines (map (generateSubKey primary) subKeys)
37 + generateBackupKey "$fpr" primary
38 ;
39 generateSubKey =
40 primary:
41 { expire ? primary.expire
42 , algo ? primary.algo
43 , usage
44 , ...
45 }:
46 ''
47 info " generateSubKey usage=[${unwords usage}]"
48 if ! printf '%s\n' "$caps" | ${pkgs.gnugrep}/bin/grep -Fqx "${lettersKeyUsage usage}"
49 then
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}"
54 fi
55 '';
56 generateBackupKey =
57 fpr:
58 { passPath
59 , backupRecipients ? []
60 , uid
61 , ...
62 }:
63 lib.optionalString (backupRecipients != [])
64 ''
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"
68 then
69 ${gpg-with-home}/bin/gpg-with-home \
70 --batch \
71 --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.pubkey.asc" \
72 --export-options export-backup \
73 --export "${fpr}"
74 fi
75 '' + (if backupRecipients == [""] then
76 ''
77 if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.revoke.asc"
78 then
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" \
83 --gen-revoke "${fpr}"
84 fi
85 if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.privkey.sec"
86 then
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}"
93 fi
94 if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.subkeys.sec"
95 then
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}"
102 fi
103 '' else ''
104 if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.revoke.asc.gpg"
105 then
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"
112 fi
113 if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.privkey.sec.gpg"
114 then
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"
122 fi
123 if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.subkeys.sec.gpg"
124 then
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"
132 fi
133 '');
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
137 then ""
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 "");
144
145 passOfFingerprint = key:
146 # Return shell code
147 # which fills a map from the fingerprints of the given key
148 # to its password file.
149 ''
150 # shell.gnupg.pass.passOfFingerprint
151 for fpr in $(${gpg-fingerprint}/bin/gpg-fingerprint -- "=${key.uid}")
152 do eval "pass_$fpr=\"${key.passPath}\""
153 done
154 '';
155 forgetPass =
156 # Return shell code
157 # which installs an exit and keyboard interruption (^C) trap
158 # removing any pass from gpg-agent
159 # whose keygrip is registered in $keygrips.
160 ''
161 # forgetPass
162 keygrips=
163 forgetPass () {
164 for keygrip in $keygrips
165 do
166 echo >&2 "gpg: forget: keygrip=$keygrip"
167 GNUPGHOME=${gnupg.gnupgHome} \
168 ${pkgs.gnupg}/bin/gpg-connect-agent </dev/null >&2 "CLEAR_PASSPHRASE $keygrip" ||
169 true
170 done
171 keygrips=
172 }
173 trap 'forgetPass' EXIT INT
174 '';
175 presetPass = keys: uid:
176 # Return shell code
177 # which preset the pass of given uid into gpg-agent,
178 # using keys to find where the pass is stored.
179 ''
180 ${unlines (map passOfFingerprint keys)}
181 # presetPass
182 GNUPGHOME=${gnupg.gnupgHome} \
183 ${pkgs.gnupg}/bin/gpgconf --launch gpg-agent
184 ${head1}
185 fpr="$(${gpg-fingerprint}/bin/fingerprint -- "${uid}" | head1)"
186 eval pass="\''${pass_$fpr}"
187 if test -n "$pass"
188 then
189 for keygrip in $(${gnupg.gpg-keygrip}/bin/gpg-keygrip -- "$fpr")
190 do
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
196 done
197 fi
198 '';
199
200 # Initialize the keyring according to gnupg.keys.
201 gpg-init = pkgs.writeShellScriptBin "gpg-init" (''
202 set -eu
203 set -o pipefail
204 ${info}
205 '' +
206 generateKeys gnupg.keys
207 );
208
209 # A wrapper around gpg to set GNUPGHOME.
210 gpg-with-home = pkgs.writeScriptBin "gpg-with-home" ''
211 GNUPGHOME=${gnupg.gnupgHome} \
212 exec ${pkgs.gnupg}/bin/gpg "$@"
213 '';
214
215 # A wrapper around gpg to get fingerprints.
216 gpg-fingerprint = pkgs.writeScriptBin "gpg-fingerprint" ''
217 set -eu
218 ${gpg-with-home}/bin/gpg-with-home \
219 --with-colons --fixed-list-mode --with-fingerprint --with-subkey-fingerprint \
220 --list-public-keys "$@" |
221 while IFS=: read -r t x x x key x x x x uid x
222 do case $t in
223 (pub|sub|sec|ssb)
224 while IFS=: read -r t x x x x x x x x fpr x
225 do case $t in (fpr) printf '%s\n' "$fpr"; break;;
226 esac done
227 ;;
228 esac done
229 '';
230
231 # A wrapper around gpg to get keygrips.
232 gpg-keygrip = pkgs.writeScriptBin "gpg-keygrip" ''
233 set -eu
234 ${gpg-with-home}/bin/gpg-with-home \
235 --with-colons --fixed-list-mode --with-keygrip \
236 --list-public-keys "$@" |
237 while IFS=: read -r t x x x key x x x x uid x
238 do case $t in
239 (pub|sub|sec|ssb)
240 while IFS=: read -r t x x x x x x x x grp x
241 do case $t in (grp) printf '%s\n' "$grp"; break;;
242 esac done
243 ;;
244 esac done
245 '';
246
247 # A wrapper around gpg to get uids.
248 gpg-uid = pkgs.writeScriptBin "gpg-uid" ''
249 set -eu
250 ${gpg-with-home}/bin/gpg-with-home \
251 --with-colons --fixed-list-mode \
252 --list-public-keys "$@" |
253 while IFS=: read -r t st x x x x x id x uid x
254 do case $t in
255 (uid)
256 case $st in
257 (u) printf '%s\n' "$uid";;
258 esac
259 ;;
260 esac done
261 '';
262
263 head1 = ''
264 head1(){
265 IFS= read -r line
266 cat >/dev/null # NOTE: consuming all the input avoids useless triggering of pipefail
267 printf %s "$line"
268 }
269 '';
270 info = ''
271 info(){
272 echo >&2 "gpg-init: $*"
273 }
274 '';
275 in
276 {
277 options.gnupg = {
278 enable = lib.mkEnableOption "GnuPG shell utilities";
279 gnupgHome = lib.mkOption {
280 type = types.path;
281 default = "sec/gnupg";
282 description = ''
283 '';
284 };
285 keys = lib.mkOption {
286 default = {};
287 example =
288 { "John Doe. <contact@example.coop>" = {
289 algo = "rsa4096";
290 expire = "1y";
291 usage = ["cert" "sign"];
292 passPath = "example.coop/gpg/contact";
293 subKeys = [
294 { algo = "rsa4096"; expire = "1y"; usage = ["sign"];}
295 { algo = "rsa4096"; expire = "1y"; usage = ["encrypt"];}
296 { algo = "rsa4096"; expire = "1y"; usage = ["auth"];}
297 ];
298 backupRecipients = ["@john@doe.pro"];
299 };
300 };
301 type = types.attrsOf (types.submodule ({uid, ...}: {
302 #config.uid = lib.mkDefault uid;
303 options = {
304 uid = lib.mkOption {
305 type = types.str;
306 example = "John Doe <john.doe@example.coop>";
307 default = uid;
308 description = ''
309 User ID.
310 '';
311 };
312 algo = lib.mkOption {
313 type = types.enum [ "rsa4096" ];
314 default = "future-default";
315 example = "rsa4096";
316 description = ''
317 Cryptographic algorithm.
318 '';
319 };
320 expire = lib.mkOption {
321 type = types.str;
322 default = "1y";
323 example = "1y";
324 description = ''
325 Expiration timeout.
326 '';
327 };
328 usage = lib.mkOption {
329 type = with types; listOf (enum [ "cert" "sign" "encrypt" "auth" "default" ]);
330 default = ["default"];
331 example = ["cert" "sign" "encrypt" "auth"];
332 description = ''
333 Cryptographic usage.
334 '';
335 };
336 passPath = lib.mkOption {
337 type = types.str;
338 example = "gnupg/coop/example/contact@";
339 description = ''
340 Password path.
341 '';
342 };
343 subKeys = lib.mkOption {
344 type = types.listOf (types.submodule {
345 options = {
346 algo = lib.mkOption {
347 type = types.enum [ "rsa4096" ];
348 default = "default";
349 example = "rsa4096";
350 description = ''
351 Cryptographic algorithm.
352 '';
353 };
354 expire = lib.mkOption {
355 type = types.str;
356 default = "1y";
357 example = "1y";
358 description = ''
359 Expiration timeout.
360 '';
361 };
362 usage = lib.mkOption {
363 type = with types; listOf (enum [ "sign" "encrypt" "auth" "default" ]);
364 default = ["default"];
365 example = ["sign" "encrypt" "auth"];
366 description = ''
367 Cryptographic usage.
368 '';
369 };
370 };
371 });
372 };
373 backupRecipients = lib.mkOption {
374 type = with types; listOf str;
375 default = [];
376 example = ["@john@doe.pro"];
377 description = ''
378 Backup keys used to encrypt the a backup copy of the secret keys.
379 '';
380 };
381 };
382 }));
383 };
384 dirmngrConf = lib.mkOption {
385 type = types.lines;
386 apply = s: pkgs.writeText "dirmngr.conf" s;
387 default = ''
388 allow-ocsp
389 hkp-cacert ${gnupg.keyserverPEM}
390 keyserver hkps://keys.mayfirst.org
391 #use-tor
392 #log-file ${gnupg.gnupgHome}/dirmngr.log
393 #standard-resolver
394 '';
395 description = ''
396 GnuPG's dirmngr.conf content.
397 '';
398 };
399 keyserverPEM = lib.mkOption {
400 type = types.lines;
401 apply = s: pkgs.writeText "keyserver.pem" s;
402 default = builtins.readFile gnupg/keyserver.pem;
403 description = ''
404 dirmngr's hkp-cacert content.
405 '';
406 };
407 gpgAgentConf = lib.mkOption {
408 type = types.lines;
409 apply = s: pkgs.writeText "gpg-agent.conf" s;
410 default =
411 let pinentry = pkgs.writeShellScript "pinentry" ''
412 #!${pkgs.runtimeShell}
413 # choose pinentry depending on PINENTRY_USER_DATA
414 # this *only works* with gpg2
415 # see https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=802020
416 case "''${PINENTRY_USER_DATA:-tty}" in
417 curses) exec ${pkgs.pinentry.curses}/bin/pinentry-curses "$@";;
418 #emacs) exec ''${pkgs.pinentry.emacs}/bin/pinentry-emacs "$@";;
419 #gnome3) exec ''${pkgs.pinentry.gnome3}/bin/pinentry-gnome3 "$@";;
420 gtk-2) exec ${pkgs.pinentry.gtk2}/bin/pinentry-gtk-2 "$@";;
421 none) exit 1;; # do not ask for passphrase
422 #qt) exec ''${pkgs.pinentry.qt}/bin/pinentry-qt "$@";;
423 tty) exec ${pkgs.pinentry.tty}/bin/pinentry-tty "$@";;
424 esac
425 '';
426 in ''
427 allow-loopback-pinentry
428 allow-preset-passphrase
429 default-cache-ttl 17200
430 default-cache-ttl-ssh 17200
431 enable-ssh-support
432 max-cache-ttl 17200
433 max-cache-ttl-ssh 17200
434 no-allow-external-cache
435 pinentry-program ${pinentry}
436 '';
437 description = ''
438 GnuPG's gpg-agent.conf content.
439 '';
440 };
441 gpgConf = lib.mkOption {
442 type = types.lines;
443 apply = s: pkgs.writeText "gpg.conf" (s+"\n"+gnupg.gpgExtraConf);
444 default = ''
445 auto-key-locate keyserver
446 cert-digest-algo SHA512
447 charset utf-8
448 default-preference-list SHA512 SHA384 SHA256 SHA224 AES256 AES192 AES CAST5 TWOFISH BZIP2 ZLIB ZIP Uncompressed
449 fixed-list-mode
450 keyid-format 0xlong
451 keyserver-options no-honor-keyserver-url
452 no-auto-key-locate
453 no-default-keyring
454 no-emit-version
455 personal-cipher-preferences AES256 AES CAST5
456 personal-digest-preferences SHA512
457 quiet
458 s2k-cipher-algo AES256
459 s2k-count 65536
460 s2k-digest-algo SHA512
461 s2k-mode 3
462 tofu-default-policy unknown
463 trust-model tofu+pgp
464 use-agent
465 utf8-strings
466 '';
467 description = ''
468 GnuPG's gpg.conf content.
469 '';
470 };
471 gpgExtraConf = lib.mkOption {
472 type = types.lines;
473 default = "";
474 description = ''
475 GnuPG's gpg.conf extra content.
476 '';
477 };
478 };
479 config = lib.mkIf gnupg.enable {
480 nix-shell.buildInputs = [
481 gpg-with-home
482 gpg-fingerprint
483 gpg-keygrip
484 gpg-uid
485 gpg-init
486 ];
487 nix-shell.shellHook = ''
488 # gnupg
489 ${pkgs.coreutils}/bin/install -dm0700 -D ${gnupg.gnupgHome}
490 ${pkgs.coreutils}/bin/ln -snf ${gnupg.gpgConf} ${gnupg.gnupgHome}/gpg.conf
491 ${pkgs.coreutils}/bin/ln -snf ${gnupg.gpgAgentConf} ${gnupg.gnupgHome}/gpg-agent.conf
492 ${pkgs.coreutils}/bin/ln -snf ${gnupg.dirmngrConf} ${gnupg.gnupgHome}/dirmngr.conf
493 export GNUPGHOME=${gnupg.gnupgHome}
494 install -dm700 "$GNUPGHOME"
495 export GPG_TTY=$(${pkgs.coreutils}/bin/tty)
496 ${pkgs.gnupg}/bin/gpgconf --launch gpg-agent
497 export SSH_AUTH_SOCK=$(${pkgs.gnupg}/bin/gpgconf --list-dirs agent-ssh-socket)
498 '';
499 };
500 }