{ pkgs, lib, config, ... }: let inherit (lib) types; inherit (config) gnupg; unlines = builtins.concatStringsSep "\n"; unwords = builtins.concatStringsSep " "; generateKeys = keys: unlines (lib.mapAttrsToList generateKey keys); generateKey = uid: { uid ? uid , algo ? "future-default" , usage ? ["default"] , expire ? "-" , passPath , subKeys ? {} , ... }@primary: '' info "generateKey uid=\"${uid}\"" if ! ${gpg-with-home}/bin/gpg-with-home --list-secret-keys -- "=${uid}" >/dev/null 2>/dev/null then ${pkgs.pass}/bin/pass "${passPath}" | ${gpg-with-home}/bin/gpg-with-home \ --batch --pinentry-mode loopback --passphrase-fd 0 \ --quick-generate-key "${uid}" "${algo}" "${unwords usage}" "${expire}" fi ${head1} fpr=$(${gpg-fingerprint}/bin/gpg-fingerprint -- "=${uid}" | head1) caps=$(${gpg-with-home}/bin/gpg-with-home \ --with-colons --fixed-list-mode --with-fingerprint \ --list-secret-keys -- "=${uid}" | ${pkgs.gnugrep}/bin/grep '^ssb:' | ${pkgs.coreutils}/bin/cut -d : -f 12 || true) '' + unlines (map (generateSubKey primary) subKeys) + generateBackupKey "$fpr" primary ; generateSubKey = primary: { expire ? primary.expire , algo ? primary.algo , usage , ... }: '' info " generateSubKey usage=[${unwords usage}]" if ! printf '%s\n' "$caps" | ${pkgs.gnugrep}/bin/grep -Fqx "${lettersKeyUsage usage}" then ${pkgs.pass}/bin/pass "${primary.passPath}" | ${gpg-with-home}/bin/gpg-with-home \ --batch --pinentry-mode loopback --passphrase-fd 0 \ --quick-add-key "$fpr" "${algo}" "${unwords usage}" "${expire}" fi ''; generateBackupKey = fpr: { passPath , backupRecipients ? [] , uid , ... }: lib.optionalString (backupRecipients != []) '' info " generateBackupKey backupRecipients=[${unwords (map (s: "\\\"${s}\\\"") backupRecipients)}]" mkdir -p "${gnupg.gnupgHome}/backup/${uid}/" if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.pubkey.asc" then ${gpg-with-home}/bin/gpg-with-home \ --batch \ --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.pubkey.asc" \ --export-options export-backup \ --export "${fpr}" fi '' + (if backupRecipients == [""] then '' if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.revoke.asc" then ${pkgs.pass}/bin/pass "${passPath}" | ${gpg-with-home}/bin/gpg-with-home \ --pinentry-mode loopback --passphrase-fd 0 \ --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.revoke.asc" \ --gen-revoke "${fpr}" fi if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.privkey.sec" then ${pkgs.pass}/bin/pass "${passPath}" | ${gpg-with-home}/bin/gpg-with-home \ --batch --pinentry-mode loopback --passphrase-fd 0 \ --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.privkey.sec" \ --export-options export-backup \ --export-secret-key "${fpr}" fi if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.subkeys.sec" then ${pkgs.pass}/bin/pass "${passPath}" | ${gpg-with-home}/bin/gpg-with-home \ --batch --pinentry-mode loopback --passphrase-fd 0 \ --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.subkeys.sec" \ --export-options export-backup \ --export-secret-subkeys "${fpr}" fi '' else '' if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.revoke.asc.gpg" then ${pkgs.pass}/bin/pass "${passPath}" | ${gpg-with-home}/bin/gpg-with-home \ --pinentry-mode loopback --passphrase-fd 0 \ --armor --gen-revoke "${fpr}" | gpg --encrypt ${recipients backupRecipients} \ --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.revoke.asc.gpg" fi if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.privkey.sec.gpg" then ${pkgs.pass}/bin/pass "${passPath}" | ${gpg-with-home}/bin/gpg-with-home \ --batch --pinentry-mode loopback --passphrase-fd 0 \ --armor --export-options export-backup \ --export-secret-key "${fpr}" | gpg --encrypt ${recipients backupRecipients} \ --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.privkey.sec.gpg" fi if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.subkeys.sec.gpg" then ${pkgs.pass}/bin/pass "${passPath}" | ${gpg-with-home}/bin/gpg-with-home \ --batch --pinentry-mode loopback --passphrase-fd 0 \ --armor --export-options export-backup \ --export-secret-subkeys "${fpr}" | gpg --encrypt ${recipients backupRecipients} \ --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.subkeys.sec.gpg" fi ''); recipients = rs: unwords (map (r: ''--recipient "${refKey r}"'') rs); refKey = key: if builtins.typeOf key == "string" then key else "=${key.uid}"; signer = s: if s == null then "" else ''--sign --default-key "${refKey s}"''; lettersKeyUsage = usage: (if builtins.elem "encrypt" usage then "e" else "") + (if builtins.elem "sign" usage then "s" else "") + (if builtins.elem "cert" usage then "c" else "") + (if builtins.elem "auth" usage then "a" else ""); passOfFingerprint = key: # Return shell code # which fills a map from the fingerprints of the given key # to its password file. '' # shell.gnupg.pass.passOfFingerprint for fpr in $(${gpg-fingerprint}/bin/gpg-fingerprint -- "=${key.uid}") do eval "pass_$fpr=\"${key.passPath}\"" done ''; forgetPass = # Return shell code # which installs an exit and keyboard interruption (^C) trap # removing any pass from gpg-agent # whose keygrip is registered in $keygrips. '' # forgetPass keygrips= forgetPass () { for keygrip in $keygrips do echo >&2 "gpg: forget: keygrip=$keygrip" GNUPGHOME=${gnupg.gnupgHome} \ ${pkgs.gnupg}/bin/gpg-connect-agent &2 "CLEAR_PASSPHRASE $keygrip" || true done keygrips= } trap 'forgetPass' EXIT INT ''; presetPass = keys: uid: # Return shell code # which preset the pass of given uid into gpg-agent, # using keys to find where the pass is stored. '' ${unlines (map passOfFingerprint keys)} # presetPass GNUPGHOME=${gnupg.gnupgHome} \ ${pkgs.gnupg}/bin/gpgconf --launch gpg-agent ${head1} fpr="$(${gpg-fingerprint}/bin/fingerprint -- "${uid}" | head1)" eval pass="\''${pass_$fpr}" if test -n "$pass" then for keygrip in $(${gnupg.gpg-keygrip}/bin/gpg-keygrip -- "$fpr") do keygrips="$keygrips $keygrip" echo >&2 "gpg: preset: keygrip=$keygrip pass=$pass" ${pkgs.pass}/bin/pass "$pass" | GNUPGHOME=${gnupg.gnupgHome} \ ${pkgs.gnupg}/libexec/gpg-preset-passphrase --preset ''${XTRACE:+--verbose} $keygrip done fi ''; # Initialize the keyring according to gnupg.keys. gpg-init = pkgs.writeShellScriptBin "gpg-init" ('' set -eu set -o pipefail ${info} '' + generateKeys gnupg.keys ); # A wrapper around gpg to set GNUPGHOME. gpg-with-home = pkgs.writeScriptBin "gpg-with-home" '' GNUPGHOME=${gnupg.gnupgHome} \ exec ${pkgs.gnupg}/bin/gpg "$@" ''; # A wrapper around gpg to get fingerprints. gpg-fingerprint = pkgs.writeScriptBin "gpg-fingerprint" '' set -eu ${gpg-with-home}/bin/gpg-with-home \ --with-colons --fixed-list-mode --with-fingerprint --with-subkey-fingerprint \ --list-public-keys "$@" | 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 fpr x do case $t in (fpr) printf '%s\n' "$fpr"; break;; esac done ;; esac done ''; # A wrapper around gpg to get keygrips. gpg-keygrip = pkgs.writeScriptBin "gpg-keygrip" '' set -eu ${gpg-with-home}/bin/gpg-with-home \ --with-colons --fixed-list-mode --with-keygrip \ --list-public-keys "$@" | 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 ''; # A wrapper around gpg to get uids. gpg-uid = pkgs.writeScriptBin "gpg-uid" '' set -eu ${gpg-with-home}/bin/gpg-with-home \ --with-colons --fixed-list-mode \ --list-public-keys "$@" | while IFS=: read -r t st x x x x x id x uid x do case $t in (uid) case $st in (u) printf '%s\n' "$uid";; esac ;; esac done ''; head1 = '' head1(){ IFS= read -r line cat >/dev/null # NOTE: consuming all the input avoids useless triggering of pipefail printf %s "$line" } ''; info = '' info(){ echo >&2 "gpg-init: $*" } ''; in { options.gnupg = { enable = lib.mkEnableOption "GnuPG shell utilities"; gnupgHome = lib.mkOption { type = types.path; default = "sec/gnupg"; description = '' ''; }; keys = lib.mkOption { default = {}; example = { "John Doe. " = { algo = "rsa4096"; expire = "1y"; usage = ["cert" "sign"]; passPath = "example.coop/gpg/contact"; subKeys = [ { algo = "rsa4096"; expire = "1y"; usage = ["sign"];} { algo = "rsa4096"; expire = "1y"; usage = ["encrypt"];} { algo = "rsa4096"; expire = "1y"; usage = ["auth"];} ]; backupRecipients = ["@john@doe.pro"]; }; }; type = types.attrsOf (types.submodule ({uid, ...}: { #config.uid = lib.mkDefault uid; options = { uid = lib.mkOption { type = types.str; example = "John Doe "; default = uid; description = '' User ID. ''; }; algo = lib.mkOption { type = types.enum [ "rsa4096" ]; default = "future-default"; example = "rsa4096"; description = '' Cryptographic algorithm. ''; }; expire = lib.mkOption { type = types.str; default = "1y"; example = "1y"; description = '' Expiration timeout. ''; }; usage = lib.mkOption { type = with types; listOf (enum [ "cert" "sign" "encrypt" "auth" "default" ]); default = ["default"]; example = ["cert" "sign" "encrypt" "auth"]; description = '' Cryptographic usage. ''; }; passPath = lib.mkOption { type = types.str; example = "gnupg/coop/example/contact@"; description = '' Password path. ''; }; subKeys = lib.mkOption { type = types.listOf (types.submodule { options = { algo = lib.mkOption { type = types.enum [ "rsa4096" ]; default = "default"; example = "rsa4096"; description = '' Cryptographic algorithm. ''; }; expire = lib.mkOption { type = types.str; default = "1y"; example = "1y"; description = '' Expiration timeout. ''; }; usage = lib.mkOption { type = with types; listOf (enum [ "sign" "encrypt" "auth" "default" ]); default = ["default"]; example = ["sign" "encrypt" "auth"]; description = '' Cryptographic usage. ''; }; }; }); }; backupRecipients = lib.mkOption { type = with types; listOf str; default = []; example = ["@john@doe.pro"]; description = '' Backup keys used to encrypt the a backup copy of the secret keys. ''; }; }; })); }; dirmngrConf = lib.mkOption { type = types.lines; apply = s: pkgs.writeText "dirmngr.conf" s; default = '' allow-ocsp hkp-cacert ${gnupg.keyserverPEM} keyserver hkps://keys.mayfirst.org use-tor #log-file ${gnupg.gnupgHome}/dirmngr.log #standard-resolver ''; description = '' GnuPG's dirmngr.conf content. ''; }; keyserverPEM = lib.mkOption { type = types.lines; apply = s: pkgs.writeText "keyserver.pem" s; default = builtins.readFile gnupg/keyserver.pem; description = '' dirmngr's hkp-cacert content. ''; }; gpgAgentConf = lib.mkOption { type = types.lines; apply = s: pkgs.writeText "gpg-agent.conf" s; default = let pinentry = pkgs.writeShellScript "pinentry" '' #!${pkgs.runtimeShell} # choose pinentry depending on PINENTRY_USER_DATA # this *only works* with gpg2 # see https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=802020 case "''${PINENTRY_USER_DATA:-tty}" in curses) exec ${pkgs.pinentry.curses}/bin/pinentry-curses "$@";; #emacs) exec ''${pkgs.pinentry.emacs}/bin/pinentry-emacs "$@";; #gnome3) exec ''${pkgs.pinentry.gnome3}/bin/pinentry-gnome3 "$@";; gtk-2) exec ${pkgs.pinentry.gtk2}/bin/pinentry-gtk-2 "$@";; none) exit 1;; # do not ask for passphrase #qt) exec ''${pkgs.pinentry.qt}/bin/pinentry-qt "$@";; tty) exec ${pkgs.pinentry.tty}/bin/pinentry-tty "$@";; esac ''; in '' allow-loopback-pinentry allow-preset-passphrase default-cache-ttl 17200 default-cache-ttl-ssh 17200 enable-ssh-support max-cache-ttl 17200 max-cache-ttl-ssh 17200 no-allow-external-cache pinentry-program ${pinentry} ''; description = '' GnuPG's gpg-agent.conf content. ''; }; gpgConf = lib.mkOption { type = types.lines; apply = s: pkgs.writeText "gpg.conf" (s+"\n"+gnupg.gpgExtraConf); default = '' auto-key-locate keyserver cert-digest-algo SHA512 charset utf-8 default-preference-list SHA512 SHA384 SHA256 SHA224 AES256 AES192 AES CAST5 TWOFISH BZIP2 ZLIB ZIP Uncompressed fixed-list-mode keyid-format 0xlong keyserver-options no-honor-keyserver-url no-auto-key-locate no-default-keyring no-emit-version personal-cipher-preferences AES256 AES CAST5 personal-digest-preferences SHA512 quiet s2k-cipher-algo AES256 s2k-count 65536 s2k-digest-algo SHA512 s2k-mode 3 tofu-default-policy unknown trust-model tofu+pgp use-agent utf8-strings ''; description = '' GnuPG's gpg.conf content. ''; }; gpgExtraConf = lib.mkOption { type = types.lines; default = ""; description = '' GnuPG's gpg.conf extra content. ''; }; }; config = lib.mkIf gnupg.enable { nix-shell.buildInputs = [ gpg-with-home gpg-fingerprint gpg-keygrip gpg-uid gpg-init ]; nix-shell.shellHook = '' # gnupg ${pkgs.coreutils}/bin/install -dm0700 -D ${gnupg.gnupgHome} ${pkgs.coreutils}/bin/ln -snf ${gnupg.gpgConf} ${gnupg.gnupgHome}/gpg.conf ${pkgs.coreutils}/bin/ln -snf ${gnupg.gpgAgentConf} ${gnupg.gnupgHome}/gpg-agent.conf ${pkgs.coreutils}/bin/ln -snf ${gnupg.dirmngrConf} ${gnupg.gnupgHome}/dirmngr.conf export GNUPGHOME=${gnupg.gnupgHome} install -dm700 "$GNUPGHOME" export GPG_TTY=$(${pkgs.coreutils}/bin/tty) ${pkgs.gnupg}/bin/gpgconf --launch gpg-agent export SSH_AUTH_SOCK=$(${pkgs.gnupg}/bin/gpgconf --list-dirs agent-ssh-socket) ''; }; }