]> Git — Sourcephile - sourcephile-nix.git/blob - shell/modules/tools/security/gnupg.nix
nix: improve shell.nix's modules system
[sourcephile-nix.git] / shell / modules / tools / security / gnupg.nix
1 { pkgs, lib, config, ... }:
2 let cfg = config.gnupg;
3 inherit (lib) types;
4 unlines = builtins.concatStringsSep "\n";
5 unwords = builtins.concatStringsSep " ";
6
7 generateKeys = keys: unlines (lib.mapAttrsToList generateKey keys);
8 generateKey =
9 uid:
10 { uid ? uid
11 , algo ? "future-default"
12 , usage ? ["default"]
13 , expire ? "-"
14 , passPath
15 , subKeys ? {}
16 , ...
17 }@primary:
18 ''
19 info " generateKey uid=\"${uid}\""
20 if ! ${gpg-with-home}/bin/gpg-with-home --list-secret-keys -- "=${uid}" >/dev/null 2>/dev/null
21 then
22 ${pkgs.pass}/bin/pass "${passPath}" |
23 ${gpg-with-home}/bin/gpg-with-home \
24 --batch --pinentry-mode loopback --passphrase-fd 0 \
25 --quick-generate-key "${uid}" "${algo}" "${unwords usage}" "${expire}"
26 fi
27 ${head1}
28 fpr=$(${gpg-fingerprint}/bin/gpg-fingerprint -- "=${uid}" | head1)
29 caps=$(${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)
34 ''
35 + unlines (map (generateSubKey primary) subKeys)
36 + generateBackupKey "$fpr" primary
37 ;
38 generateSubKey =
39 primary:
40 { expire ? primary.expire
41 , algo ? primary.algo
42 , usage
43 , ...
44 }:
45 ''
46 info " generateSubKey usage=[${unwords usage}]"
47 if ! printf '%s\n' "$caps" | ${pkgs.gnugrep}/bin/grep -Fqx "${lettersKeyUsage usage}"
48 then
49 ${pkgs.pass}/bin/pass "${primary.passPath}" |
50 ${gpg-with-home}/bin/gpg-with-home \
51 --batch --pinentry-mode loopback --passphrase-fd 0 \
52 --quick-add-key "$fpr" "${algo}" "${unwords usage}" "${expire}"
53 fi
54 '';
55 generateBackupKey =
56 fpr:
57 { passPath
58 , backupRecipients ? []
59 , uid
60 , ...
61 }:
62 lib.optionalString (backupRecipients != [])
63 ''
64 info " generateBackupKey backupRecipients=[${unwords (map (s: "\\\"${s}\\\"") backupRecipients)}]"
65 mkdir -p "${cfg.gnupgHome}/backup/${uid}/"
66 if ! test -s "${cfg.gnupgHome}/backup/${uid}/${fpr}.pubkey.asc"
67 then
68 ${gpg-with-home}/bin/gpg-with-home \
69 --batch \
70 --armor --yes --output "${cfg.gnupgHome}/backup/${uid}/${fpr}.pubkey.asc" \
71 --export-options export-backup \
72 --export "${fpr}"
73 fi
74 '' + (if backupRecipients == [""] then
75 ''
76 if ! test -s "${cfg.gnupgHome}/backup/${uid}/${fpr}.revoke.asc"
77 then
78 ${pkgs.pass}/bin/pass "${passPath}" |
79 ${gpg-with-home}/bin/gpg-with-home \
80 --pinentry-mode loopback --passphrase-fd 0 \
81 --armor --yes --output "${cfg.gnupgHome}/backup/${uid}/${fpr}.revoke.asc" \
82 --gen-revoke "${fpr}"
83 fi
84 if ! test -s "${cfg.gnupgHome}/backup/${uid}/${fpr}.privkey.sec"
85 then
86 ${pkgs.pass}/bin/pass "${passPath}" |
87 ${gpg-with-home}/bin/gpg-with-home \
88 --batch --pinentry-mode loopback --passphrase-fd 0 \
89 --armor --yes --output "${cfg.gnupgHome}/backup/${uid}/${fpr}.privkey.sec" \
90 --export-options export-backup \
91 --export-secret-key "${fpr}"
92 fi
93 if ! test -s "${cfg.gnupgHome}/backup/${uid}/${fpr}.subkeys.sec"
94 then
95 ${pkgs.pass}/bin/pass "${passPath}" |
96 ${gpg-with-home}/bin/gpg-with-home \
97 --batch --pinentry-mode loopback --passphrase-fd 0 \
98 --armor --yes --output "${cfg.gnupgHome}/backup/${uid}/${fpr}.subkeys.sec" \
99 --export-options export-backup \
100 --export-secret-subkeys "${fpr}"
101 fi
102 '' else ''
103 if ! test -s "${cfg.gnupgHome}/backup/${uid}/${fpr}.revoke.asc.gpg"
104 then
105 ${pkgs.pass}/bin/pass "${passPath}" |
106 ${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.gnupgHome}/backup/${uid}/${fpr}.revoke.asc.gpg"
111 fi
112 if ! test -s "${cfg.gnupgHome}/backup/${uid}/${fpr}.privkey.sec.gpg"
113 then
114 ${pkgs.pass}/bin/pass "${passPath}" |
115 ${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.gnupgHome}/backup/${uid}/${fpr}.privkey.sec.gpg"
121 fi
122 if ! test -s "${cfg.gnupgHome}/backup/${uid}/${fpr}.subkeys.sec.gpg"
123 then
124 ${pkgs.pass}/bin/pass "${passPath}" |
125 ${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.gnupgHome}/backup/${uid}/${fpr}.subkeys.sec.gpg"
131 fi
132 '');
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
136 then ""
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 "");
143
144 passOfFingerprint = key:
145 # Return shell code
146 # which fills a map from the fingerprints of the given key
147 # to its password file.
148 ''
149 # shell.gnupg.pass.passOfFingerprint
150 for fpr in $(${gpg-fingerprint}/bin/gpg-fingerprint -- "=${key.uid}")
151 do eval "pass_$fpr=\"${key.passPath}\""
152 done
153 '';
154 forgetPass =
155 # Return shell code
156 # which installs an exit and keyboard interruption (^C) trap
157 # removing any pass from gpg-agent
158 # whose keygrip is registered in $keygrips.
159 ''
160 # forgetPass
161 keygrips=
162 forgetPass () {
163 for keygrip in $keygrips
164 do
165 echo >&2 "gpg: forget: keygrip=$keygrip"
166 GNUPGHOME=${cfg.gnupgHome} \
167 ${pkgs.gnupg}/bin/gpg-connect-agent </dev/null >&2 "CLEAR_PASSPHRASE $keygrip" ||
168 true
169 done
170 keygrips=
171 }
172 trap 'forgetPass' EXIT INT
173 '';
174 presetPass = keys: uid:
175 # Return shell code
176 # which preset the pass of given uid into gpg-agent,
177 # using keys to find where the pass is stored.
178 ''
179 ${unlines (map passOfFingerprint keys)}
180 # presetPass
181 GNUPGHOME=${cfg.gnupgHome} \
182 ${pkgs.gnupg}/bin/gpgconf --launch gpg-agent
183 ${head1}
184 fpr="$(${gpg-fingerprint}/bin/fingerprint -- "${uid}" | head1)"
185 eval pass="\''${pass_$fpr}"
186 if test -n "$pass"
187 then
188 for keygrip in $(${cfg.gpg-keygrip}/bin/gpg-keygrip -- "$fpr")
189 do
190 keygrips="$keygrips $keygrip"
191 echo >&2 "gpg: preset: keygrip=$keygrip pass=$pass"
192 ${pkgs.pass}/bin/pass "$pass" |
193 GNUPGHOME=${cfg.gnupgHome} \
194 ${pkgs.gnupg}/libexec/gpg-preset-passphrase --preset ''${XTRACE:+--verbose} $keygrip
195 done
196 fi
197 '';
198
199 head1 = ''
200 head1(){
201 IFS= read -r line
202 cat >/dev/null # NOTE: consuming all the input avoids useless triggering of pipefail
203 printf %s "$line"
204 }
205 '';
206 info = ''
207 info(){
208 echo >&2 "INFO: $*"
209 }
210 '';
211
212 # A wrapper around gpg to set GNUPGHOME.
213 gpg-with-home = pkgs.writeScriptBin "gpg-with-home" ''
214 GNUPGHOME=${cfg.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 # Initialize the keyring according to cfg.keys.
267 gpg-init = pkgs.writeShellScriptBin "gpg-init" (''
268 set -eu
269 set -o pipefail
270 ${info}
271 info "Init GnuPG"
272 ${pkgs.coreutils}/bin/install -dm0700 -D ${cfg.gnupgHome}
273 ${pkgs.coreutils}/bin/ln -snf ${cfg.gpgConf} ${cfg.gnupgHome}/gpg.conf
274 ${pkgs.coreutils}/bin/ln -snf ${cfg.gpgAgentConf} ${cfg.gnupgHome}/gpg-agent.conf
275 ${pkgs.coreutils}/bin/ln -snf ${cfg.dirmngrConf} ${cfg.gnupgHome}/dirmngr.conf
276 '' +
277 generateKeys cfg.keys);
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 ({uid, ...}: {
305 #config.uid = lib.mkDefault uid;
306 options = {
307 uid = lib.mkOption {
308 type = types.str;
309 example = "John Doe <john.doe@example.coop>";
310 default = uid;
311 description = ''
312 User ID.
313 '';
314 };
315 algo = lib.mkOption {
316 type = types.enum [ "rsa4096" ];
317 default = "future-default";
318 example = "rsa4096";
319 description = ''
320 Cryptographic algorithm.
321 '';
322 };
323 expire = lib.mkOption {
324 type = types.str;
325 default = "1y";
326 example = "1y";
327 description = ''
328 Expiration timeout.
329 '';
330 };
331 usage = lib.mkOption {
332 type = with types; listOf (enum [ "cert" "sign" "encrypt" "auth" "default" ]);
333 default = ["default"];
334 example = ["cert" "sign" "encrypt" "auth"];
335 description = ''
336 Cryptographic usage.
337 '';
338 };
339 passPath = lib.mkOption {
340 type = types.str;
341 example = "gnupg/coop/example/contact@";
342 description = ''
343 Password path.
344 '';
345 };
346 subKeys = lib.mkOption {
347 type = types.listOf (types.submodule {
348 options = {
349 algo = lib.mkOption {
350 type = types.enum [ "rsa4096" ];
351 default = "default";
352 example = "rsa4096";
353 description = ''
354 Cryptographic algorithm.
355 '';
356 };
357 expire = lib.mkOption {
358 type = types.str;
359 default = "1y";
360 example = "1y";
361 description = ''
362 Expiration timeout.
363 '';
364 };
365 usage = lib.mkOption {
366 type = with types; listOf (enum [ "sign" "encrypt" "auth" "default" ]);
367 default = ["default"];
368 example = ["sign" "encrypt" "auth"];
369 description = ''
370 Cryptographic usage.
371 '';
372 };
373 };
374 });
375 };
376 backupRecipients = lib.mkOption {
377 type = with types; listOf str;
378 default = [];
379 example = ["@john@doe.pro"];
380 description = ''
381 Backup keys used to encrypt the a backup copy of the secret keys.
382 '';
383 };
384 };
385 }));
386 };
387 dirmngrConf = lib.mkOption {
388 type = types.lines;
389 apply = s: pkgs.writeText "dirmngr.conf" s;
390 default = ''
391 allow-ocsp
392 hkp-cacert ${cfg.keyserverPEM}
393 keyserver hkps://keys.mayfirst.org
394 use-tor
395 #log-file ${cfg.gnupgHome}/dirmngr.log
396 #standard-resolver
397 '';
398 description = ''
399 GnuPG's dirmngr.conf content.
400 '';
401 };
402 keyserverPEM = lib.mkOption {
403 type = types.lines;
404 apply = s: pkgs.writeText "keyserver.pem" s;
405 default = builtins.readFile gnupg/keyserver.pem;
406 description = ''
407 dirmngr's hkp-cacert content.
408 '';
409 };
410 gpgAgentConf = lib.mkOption {
411 type = types.lines;
412 apply = s: pkgs.writeText "gpg-agent.conf" s;
413 default = ''
414 allow-preset-passphrase
415 default-cache-ttl 17200
416 default-cache-ttl-ssh 17200
417 enable-ssh-support
418 max-cache-ttl 17200
419 max-cache-ttl-ssh 17200
420 '';
421 description = ''
422 GnuPG's gpg-agent.conf content.
423 '';
424 };
425 gpgConf = lib.mkOption {
426 type = types.lines;
427 apply = s: pkgs.writeText "gpg.conf" s;
428 default = ''
429 auto-key-locate keyserver
430 cert-digest-algo SHA512
431 charset utf-8
432 default-preference-list SHA512 SHA384 SHA256 SHA224 AES256 AES192 AES CAST5 TWOFISH BZIP2 ZLIB ZIP Uncompressed
433 fixed-list-mode
434 keyid-format 0xlong
435 keyserver-options no-honor-keyserver-url
436 no-auto-key-locate
437 no-default-keyring
438 no-emit-version
439 personal-cipher-preferences AES256 AES CAST5
440 personal-digest-preferences SHA512
441 quiet
442 s2k-cipher-algo AES256
443 s2k-count 65536
444 s2k-digest-algo SHA512
445 s2k-mode 3
446 tofu-default-policy unknown
447 trust-model tofu+pgp
448 use-agent
449 utf8-strings
450 '';
451 description = ''
452 GnuPG's gpg.conf content.
453 '';
454 };
455 };
456 config = lib.mkIf cfg.enable {
457 nix-shell.buildInputs = [
458 gpg-with-home
459 gpg-fingerprint
460 gpg-keygrip
461 gpg-uid
462 gpg-init
463 ];
464 nix-shell.shellHook = ''
465 # gnupg
466 export GNUPGHOME=${cfg.gnupgHome}
467 install -dm700 "$GNUPGHOME"
468 export GPG_TTY=$(${pkgs.coreutils}/bin/tty)
469 ${pkgs.gnupg}/bin/gpgconf --launch gpg-agent
470 export SSH_AUTH_SOCK=$(${pkgs.gnupg}/bin/gpgconf --list-dirs agent-ssh-socket)
471 '';
472 };
473 }