]> Git — Sourcephile - sourcephile-nix.git/blob - shell/modules/tools/security/gnupg.nix
mermet: radicle: fix publicKey old config name
[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 --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 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 # Initialize the keyring according to gnupg.keys.
146 gpg-init = pkgs.writeShellScriptBin "gpg-init" (''
147 set -eu
148 set -o pipefail
149 ${info}
150 '' +
151 generateKeys gnupg.keys
152 );
153
154 # A wrapper around gpg to set GNUPGHOME.
155 gpg-with-home = pkgs.writeScriptBin "gpg-with-home" ''
156 GNUPGHOME=${gnupg.gnupgHome} \
157 exec ${pkgs.gnupg}/bin/gpg "$@"
158 '';
159
160 # A wrapper around gpg to get fingerprints.
161 gpg-fingerprint = pkgs.writeScriptBin "gpg-fingerprint" ''
162 set -eu
163 ${gpg-with-home}/bin/gpg-with-home \
164 --with-colons --with-fingerprint --with-subkey-fingerprint \
165 --list-public-keys "$@" |
166 while IFS=: read -r t x x x key x x x x uid x
167 do case $t in
168 (pub|sub|sec|ssb)
169 while IFS=: read -r t x x x x x x x x fpr x
170 do case $t in (fpr) printf '%s\n' "$fpr"; break;;
171 esac done
172 ;;
173 esac done
174 '';
175
176 # A wrapper around gpg to get keygrips.
177 gpg-keygrip = pkgs.writeScriptBin "gpg-keygrip" ''
178 set -eu
179 ${gpg-with-home}/bin/gpg-with-home \
180 --with-colons --with-keygrip \
181 --list-public-keys "$@" |
182 while IFS=: read -r t x x x key x x x x uid x do case $t in (pub|sub|sec|ssb) while IFS=: read -r t x x x x x x x x grp x do case $t in (grp) printf '%s\n' "$grp"; break;; esac done ;; esac done
183 '';
184
185 # A wrapper around gpg to get uids.
186 gpg-uid = pkgs.writeScriptBin "gpg-uid" ''
187 set -eu
188 ${gpg-with-home}/bin/gpg-with-home \
189 --with-colons \
190 --list-public-keys "$@" |
191 while IFS=: read -r t st x x x x x id x uid x
192 do case $t in
193 (uid)
194 case $st in
195 (u) printf '%s\n' "$uid";;
196 esac
197 ;;
198 esac done
199 '';
200
201 head1 = ''
202 head1(){
203 IFS= read -r line
204 cat >/dev/null # NOTE: consuming all the input avoids useless triggering of pipefail
205 printf %s "$line"
206 }
207 '';
208 info = ''
209 info(){
210 echo >&2 "gpg-init: $*"
211 }
212 '';
213 in
214 {
215 options.gnupg = {
216 enable = lib.mkEnableOption "GnuPG shell utilities";
217 gnupgHome = lib.mkOption {
218 type = types.str;
219 default = "sec/gnupg";
220 description = ''
221 '';
222 };
223 keys = lib.mkOption {
224 default = { };
225 example =
226 {
227 "John Doe. <contact@example.coop>" = {
228 algo = "rsa4096";
229 expire = "1y";
230 usage = [ "cert" "sign" ];
231 passPath = "example.coop/gpg/contact";
232 subKeys = [
233 { algo = "rsa4096"; expire = "1y"; usage = [ "sign" ]; }
234 { algo = "rsa4096"; expire = "1y"; usage = [ "encrypt" ]; }
235 { algo = "rsa4096"; expire = "1y"; usage = [ "auth" ]; }
236 ];
237 backupRecipients = [ "@john@doe.pro" ];
238 };
239 };
240 type = types.attrsOf (types.submodule ({ name, ... }: {
241 options = {
242 uid = lib.mkOption {
243 type = types.str;
244 example = "John Doe <john.doe@example.coop>";
245 default = name;
246 description = ''
247 User ID.
248 '';
249 };
250 algo = lib.mkOption {
251 type = types.enum [ "rsa4096" ];
252 default = "future-default";
253 example = "rsa4096";
254 description = ''
255 Cryptographic algorithm.
256 '';
257 };
258 expire = lib.mkOption {
259 type = types.str;
260 default = "0";
261 example = "1y";
262 description = ''
263 Expiration timeout.
264 '';
265 };
266 usage = lib.mkOption {
267 type = with types; listOf (enum [ "cert" "sign" "encrypt" "auth" "default" ]);
268 default = [ "default" ];
269 example = [ "cert" "sign" "encrypt" "auth" ];
270 description = ''
271 Cryptographic usage.
272 '';
273 };
274 passPath = lib.mkOption {
275 type = types.str;
276 example = "gnupg/coop/example/contact@";
277 description = ''
278 Password path.
279 '';
280 };
281 subKeys = lib.mkOption {
282 type = types.listOf (types.submodule {
283 options = {
284 algo = lib.mkOption {
285 type = types.enum [ "rsa4096" ];
286 default = "default";
287 example = "rsa4096";
288 description = ''
289 Cryptographic algorithm.
290 '';
291 };
292 expire = lib.mkOption {
293 type = types.str;
294 default = "0";
295 example = "1y";
296 description = ''
297 Expiration timeout.
298 '';
299 };
300 usage = lib.mkOption {
301 type = with types; listOf (enum [ "sign" "encrypt" "auth" "default" ]);
302 default = [ "default" ];
303 example = [ "sign" "encrypt" "auth" ];
304 description = ''
305 Cryptographic usage.
306 '';
307 };
308 };
309 });
310 };
311 backupRecipients = lib.mkOption {
312 type = with types; listOf str;
313 default = [ ];
314 example = [ "@john@doe.pro" ];
315 description = ''
316 Backup keys used to encrypt the a backup copy of the secret keys.
317 '';
318 };
319 postRun = lib.mkOption {
320 type = types.lines;
321 default = "";
322 description = ''
323 Shell code to run after the key has been generated or tested to exist.
324 '';
325 };
326 };
327 }));
328 };
329 dirmngrConf = lib.mkOption {
330 type = types.lines;
331 apply = s: pkgs.writeText "dirmngr.conf" s;
332 default = ''
333 allow-ocsp
334 hkp-cacert ${gnupg.keyserverPEM}
335 keyserver hkps://keys.mayfirst.org
336 #use-tor
337 #log-file ${gnupg.gnupgHome}/dirmngr.log
338 #standard-resolver
339 '';
340 description = ''
341 GnuPG's dirmngr.conf content.
342 '';
343 };
344 keyserverPEM = lib.mkOption {
345 type = types.lines;
346 apply = s: pkgs.writeText "keyserver.pem" s;
347 default = builtins.readFile gnupg/keyserver.pem;
348 description = ''
349 dirmngr's hkp-cacert content.
350 '';
351 };
352 gpgAgentConf = lib.mkOption {
353 type = types.lines;
354 apply = s: pkgs.writeText "gpg-agent.conf" (s + "\n" + gnupg.gpgAgentExtraConf);
355 default =
356 let
357 pinentry = pkgs.writeShellScript "pinentry" ''
358 #!${pkgs.runtimeShell}
359 # choose pinentry depending on PINENTRY_USER_DATA
360 # this *only works* with gpg2
361 # see https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=802020
362 case "''${PINENTRY_USER_DATA:-curses}" in
363 curses) exec ${pkgs.pinentry.curses}/bin/pinentry-curses "$@";;
364 #emacs) exec ''${pkgs.pinentry.emacs}/bin/pinentry-emacs "$@";;
365 #gnome3) exec ''${pkgs.pinentry.gnome3}/bin/pinentry-gnome3 "$@";;
366 gtk-2) exec ''${pkgs.pinentry.gtk2}/bin/pinentry-gtk-2 "$@";;
367 none) exit 1;; # do not ask for passphrase
368 #qt) exec ''${pkgs.pinentry.qt}/bin/pinentry-qt "$@";;
369 tty) exec ${pkgs.pinentry.tty}/bin/pinentry-tty "$@";;
370 esac
371 '';
372 in
373 ''
374 allow-loopback-pinentry
375 allow-preset-passphrase
376 default-cache-ttl 17200
377 default-cache-ttl-ssh 17200
378 enable-ssh-support
379 max-cache-ttl 17200
380 max-cache-ttl-ssh 17200
381 no-allow-external-cache
382 pinentry-program ${pinentry}
383 '';
384 description = ''
385 GnuPG's gpg-agent.conf content.
386 '';
387 };
388 gpgConf = lib.mkOption {
389 type = types.lines;
390 apply = s: pkgs.writeText "gpg.conf" (s + "\n" + gnupg.gpgExtraConf);
391 default = ''
392 auto-key-locate keyserver
393 cert-digest-algo SHA512
394 charset utf-8
395 default-preference-list SHA512 SHA384 SHA256 SHA224 AES256 AES192 AES CAST5 TWOFISH BZIP2 ZLIB ZIP Uncompressed
396 keyid-format 0xlong
397 keyserver-options no-honor-keyserver-url
398 no-auto-key-locate
399 no-default-keyring
400 no-emit-version
401 personal-cipher-preferences AES256 AES CAST5
402 personal-digest-preferences SHA512
403 quiet
404 s2k-cipher-algo AES256
405 s2k-count 65536
406 s2k-digest-algo SHA512
407 s2k-mode 3
408 tofu-default-policy unknown
409 trust-model tofu+pgp
410 use-agent
411 utf8-strings
412 '';
413 description = ''
414 GnuPG's gpg.conf content.
415 '';
416 };
417 gpgExtraConf = lib.mkOption {
418 type = types.lines;
419 default = "";
420 description = ''
421 GnuPG's gpg.conf extra content.
422 '';
423 };
424 gpgAgentExtraConf = lib.mkOption {
425 type = types.lines;
426 default = "";
427 description = ''
428 GnuPG's gpg-agent.conf extra content.
429 '';
430 };
431 };
432 config = lib.mkIf gnupg.enable {
433 nix-shell.buildInputs = [
434 gpg-with-home
435 gpg-fingerprint
436 gpg-keygrip
437 gpg-uid
438 gpg-init
439 ];
440 nix-shell.shellHook = ''
441 # gnupg
442 ${pkgs.coreutils}/bin/install -dm0700 -D ${gnupg.gnupgHome}
443 ${pkgs.coreutils}/bin/ln -snf ${gnupg.gpgConf} ${gnupg.gnupgHome}/gpg.conf
444 ${pkgs.coreutils}/bin/ln -snf ${gnupg.gpgAgentConf} ${gnupg.gnupgHome}/gpg-agent.conf
445 ${pkgs.coreutils}/bin/ln -snf ${gnupg.dirmngrConf} ${gnupg.gnupgHome}/dirmngr.conf
446 export GNUPGHOME=${gnupg.gnupgHome}
447 install -dm700 "$GNUPGHOME"
448 export GPG_TTY=$(${pkgs.coreutils}/bin/tty)
449 ${pkgs.gnupg}/bin/gpgconf --launch gpg-agent
450 export SSH_AUTH_SOCK=$(${pkgs.gnupg}/bin/gpgconf --list-dirs agent-ssh-socket)
451 '';
452 };
453 }