carotte: try to upgrade
[sourcephile-nix.git] / shell / modules / tools / security / gnupg.nix
index 083e7049bd92cc42ec000255e13daf908e7641ab..a34d1d980f1624525b5af0c4184a47c0d8639e5d 100644 (file)
 { pkgs, lib, config, ... }:
-let cfg = config.gnupg;
-    inherit (lib) types;
-    unlines = builtins.concatStringsSep "\n";
-    unwords = builtins.concatStringsSep " ";
+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 ! ${cfg.gpg-with-home}/bin/gpg-with-home --list-secret-keys -- "=${uid}" >/dev/null 2>/dev/null
+  generateKeys = keys: unlines (lib.mapAttrsToList generateKey keys);
+  generateKey =
+    _uid:
+    { uid ? uid
+    , algo ? "future-default"
+    , usage ? [ "default" ]
+    , expire ? "-"
+    , passPath
+    , subKeys ? { }
+    , postRun ? ""
+    , ...
+    }@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}" |
-        ${cfg.gpg-with-home}/bin/gpg-with-home \
+        ${if passPath != "" then "${pkgs.pass}/bin/pass '${passPath}'" else "cat /dev/null"} |
+        ${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=$(${cfg.gpg-fingerprint}/bin/gpg-fingerprint -- "=${uid}" | head1)
-      caps=$(${cfg.gpg-with-home}/bin/gpg-with-home \
-              --with-colons --fixed-list-mode --with-fingerprint \
+      fpr=$(${gpg-fingerprint}/bin/gpg-fingerprint -- "=${uid}" | head1)
+      caps=$(${gpg-with-home}/bin/gpg-with-home \
+              --with-colons --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}]"
+    ''
+    + unlines (map (generateSubKey primary) subKeys)
+    + generateBackupKey "$fpr" primary
+    + postRun
+  ;
+  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}" |
-        ${cfg.gpg-with-home}/bin/gpg-with-home \
+        ${if primary.passPath != "" then "${pkgs.pass}/bin/pass '${primary.passPath}'" else "cat /dev/null"} |
+        ${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 != [])
+    '';
+  generateBackupKey =
+    fpr:
+    { passPath
+    , backupRecipients ? [ ]
+    , uid
+    , ...
+    }:
+    lib.optionalString (backupRecipients != [ ])
       ''
-      info "    generateBackupKey backupRecipients=[${unwords (map (s: "\\\"${s}\\\"") backupRecipients)}]"
-      mkdir -p "${cfg.dir.var}/backup/${uid}/"
-      if ! test -s "${cfg.dir.var}/backup/${uid}/${fpr}.pubkey.asc"
-       then
-        ${cfg.gpg-with-home}/bin/gpg-with-home \
-          --batch \
-          --armor --yes --output "${cfg.dir.var}/backup/${uid}/${fpr}.pubkey.asc" \
-          --export-options export-backup \
-          --export "${fpr}"
-       fi
-      '' + (if backupRecipients == [""] then
+        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 "${cfg.dir.var}/backup/${uid}/${fpr}.revoke.asc"
-       then
-        ${pkgs.pass}/bin/pass "${passPath}" |
-        ${cfg.gpg-with-home}/bin/gpg-with-home \
-          --pinentry-mode loopback --passphrase-fd 0 \
-          --armor --yes --output "${cfg.dir.var}/backup/${uid}/${fpr}.revoke.asc" \
-          --gen-revoke "${fpr}"
-       fi
-      if ! test -s "${cfg.dir.var}/backup/${uid}/${fpr}.privkey.sec"
-       then
-        ${pkgs.pass}/bin/pass "${passPath}" |
-        ${cfg.gpg-with-home}/bin/gpg-with-home \
-          --batch --pinentry-mode loopback --passphrase-fd 0 \
-          --armor --yes --output "${cfg.dir.var}/backup/${uid}/${fpr}.privkey.sec" \
-          --export-options export-backup \
-          --export-secret-key "${fpr}"
-       fi
-      if ! test -s "${cfg.dir.var}/backup/${uid}/${fpr}.subkeys.sec"
-       then
-        ${pkgs.pass}/bin/pass "${passPath}" |
-        ${cfg.gpg-with-home}/bin/gpg-with-home \
-          --batch --pinentry-mode loopback --passphrase-fd 0 \
-          --armor --yes --output "${cfg.dir.var}/backup/${uid}/${fpr}.subkeys.sec" \
-          --export-options export-backup \
-          --export-secret-subkeys "${fpr}"
-       fi
+        if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.revoke.asc" &&
+         ${gpg-with-home}/bin/gpg-with-home --list-secret-keys "${fpr}" | grep -q "sec "
+         then
+          ${if passPath != "" then "${pkgs.pass}/bin/pass '${passPath}'" else "cat /dev/null"} |
+          ${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
+          ${if passPath != "" then "${pkgs.pass}/bin/pass '${passPath}'" else "cat /dev/null"} |
+          ${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
+          ${if passPath != "" then "${pkgs.pass}/bin/pass '${passPath}'" else "cat /dev/null"} |
+          ${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 "${cfg.dir.var}/backup/${uid}/${fpr}.revoke.asc.gpg"
+      if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.revoke.asc.gpg"
        then
-        ${pkgs.pass}/bin/pass "${passPath}" |
-        ${cfg.gpg-with-home}/bin/gpg-with-home \
+        ${if passPath != "" then "${pkgs.pass}/bin/pass '${passPath}'" else "cat /dev/null"} |
+        ${gpg-with-home}/bin/gpg-with-home \
           --pinentry-mode loopback --passphrase-fd 0 \
           --armor --gen-revoke "${fpr}" |
         gpg --encrypt ${recipients backupRecipients} \
-            --armor --yes --output "${cfg.dir.var}/backup/${uid}/${fpr}.revoke.asc.gpg"
+            --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.revoke.asc.gpg"
        fi
-      if ! test -s "${cfg.dir.var}/backup/${uid}/${fpr}.privkey.sec.gpg"
+      if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.privkey.sec.gpg"
        then
-        ${pkgs.pass}/bin/pass "${passPath}" |
-        ${cfg.gpg-with-home}/bin/gpg-with-home \
+        ${if passPath != "" then "${pkgs.pass}/bin/pass '${passPath}'" else "cat /dev/null"} |
+        ${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 "${cfg.dir.var}/backup/${uid}/${fpr}.privkey.sec.gpg"
+            --armor --yes --output "${gnupg.gnupgHome}/backup/${uid}/${fpr}.privkey.sec.gpg"
        fi
-      if ! test -s "${cfg.dir.var}/backup/${uid}/${fpr}.subkeys.sec.gpg"
+      if ! test -s "${gnupg.gnupgHome}/backup/${uid}/${fpr}.subkeys.sec.gpg"
        then
-        ${pkgs.pass}/bin/pass "${passPath}" |
-        ${cfg.gpg-with-home}/bin/gpg-with-home \
+        ${if passPath != "" then "${pkgs.pass}/bin/pass '${passPath}'" else "cat /dev/null"} |
+        ${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 "${cfg.dir.var}/backup/${uid}/${fpr}.subkeys.sec.gpg"
+            --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 "");
+    '');
+  recipients = rs: unwords (map (r: ''--recipient "${refKey r}"'') rs);
+  refKey = key: if builtins.typeOf key == "string" then key else "=${key.uid}";
+  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 $(${cfg.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=${cfg.dir.var} \
-          ${pkgs.gnupg}/bin/gpg-connect-agent </dev/null >&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=${cfg.dir.var} \
-      ${pkgs.gnupg}/bin/gpgconf --launch gpg-agent
-      ${head1}
-      fpr="$(${cfg.gpg-fingerprint}/bin/fingerprint -- "${uid}" | head1)"
-      eval pass="\''${pass_$fpr}"
-      if test -n "$pass"
-       then
-        for keygrip in $(${cfg.gpg-keygrip}/bin/gpg-keygrip -- "$fpr")
-         do
-          keygrips="$keygrips $keygrip"
-          echo >&2 "gpg: preset: keygrip=$keygrip pass=$pass"
-          ${pkgs.pass}/bin/pass "$pass" |
-          GNUPGHOME=${cfg.dir.var} \
-          ${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
+  );
 
-    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 "INFO: $*"
-      }
-    '';
+  # 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 --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 --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 \
+     --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 admin utilities";
-    dir.var = lib.mkOption {
-      type = types.path;
-      default = "sec/gnupg";
-      description = ''
-      '';
-    };
-    gpg-with-home = lib.mkOption {
+    enable = lib.mkEnableOption "GnuPG shell utilities";
+    gnupgHome = lib.mkOption {
       type = types.str;
-      apply = pkgs.writeScriptBin "gpg-with-home";
-      default = ''
-        GNUPGHOME=${cfg.dir.var} \
-        exec ${pkgs.gnupg}/bin/gpg "$@"
-        '';
-      description = ''
-        A wrapper around gpg to set GNUPGHOME.
-      '';
-    };
-    gpg-fingerprint = lib.mkOption {
-      type = types.str;
-      apply = pkgs.writeScriptBin "gpg-fingerprint";
-      default = ''
-        set -eu
-        ${cfg.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
-        '';
-      description = ''
-        A wrapper around gpg to get fingerprints.
-      '';
-    };
-    gpg-keygrip = lib.mkOption {
-      type = types.str;
-      apply = pkgs.writeScriptBin "gpg-keygrip";
-      default = ''
-        set -eu
-        ${cfg.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
-        '';
-      description = ''
-        A wrapper around gpg to get keygrips.
-      '';
-    };
-    gpg-uid = lib.mkOption {
-      type = types.str;
-      apply = pkgs.writeScriptBin "gpg-uid";
-      default = ''
-        set -eu
-        ${cfg.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
-        '';
-      description = ''
-        A wrapper around gpg to get uids.
-      '';
-    };
-    init = lib.mkOption {
-      type = types.str;
-      apply = pkgs.writeShellScriptBin "init-gpg";
-      default = ''
-        set -eu
-        set -o pipefail
-        ${info}
-        info "Init GnuPG"
-        ${pkgs.coreutils}/bin/install -dm0700 -D ${cfg.dir.var}
-        ${pkgs.coreutils}/bin/ln -snf ${cfg.gpgConf}      ${cfg.dir.var}/gpg.conf
-        ${pkgs.coreutils}/bin/ln -snf ${cfg.gpgAgentConf} ${cfg.dir.var}/gpg-agent.conf
-        ${pkgs.coreutils}/bin/ln -snf ${cfg.dirmngrConf}  ${cfg.dir.var}/dirmngr.conf
-        '' +
-        generateKeys cfg.keys;
+      default = "sec/gnupg";
       description = ''
-        Setup gpg.
-      '';
+    '';
     };
     keys = lib.mkOption {
-      default = {};
+      default = { };
       example =
-        { "John Doe. <contact@example.coop>" = {
-            algo   = "rsa4096";
+        {
+          "John Doe. <contact@example.coop>" = {
+            algo = "rsa4096";
             expire = "1y";
-            usage  = ["cert" "sign"];
+            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"];
+              { 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;
+      type = types.attrsOf (types.submodule ({ name, ... }: {
         options = {
           uid = lib.mkOption {
-            type        = types.str;
-            example     = "John Doe <john.doe@example.coop>";
-            default     = uid;
+            type = types.str;
+            example = "John Doe <john.doe@example.coop>";
+            default = name;
             description = ''
               User ID.
             '';
           };
           algo = lib.mkOption {
-            type        = types.enum [ "rsa4096" ];
-            default     = "future-default";
-            example     = "rsa4096";
+            type = types.enum [ "rsa4096" ];
+            default = "future-default";
+            example = "rsa4096";
             description = ''
               Cryptographic algorithm.
             '';
           };
           expire = lib.mkOption {
-            type        = types.str;
-            default     = "1y";
-            example     = "1y";
+            type = types.str;
+            default = "0";
+            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"];
+            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@";
+            type = types.str;
+            example = "gnupg/coop/example/contact@";
             description = ''
               Password path.
             '';
@@ -372,25 +282,25 @@ in
             type = types.listOf (types.submodule {
               options = {
                 algo = lib.mkOption {
-                  type        = types.enum [ "rsa4096" ];
-                  default     = "default";
-                  example     = "rsa4096";
+                  type = types.enum [ "rsa4096" ];
+                  default = "default";
+                  example = "rsa4096";
                   description = ''
                     Cryptographic algorithm.
                   '';
                 };
                 expire = lib.mkOption {
-                  type        = types.str;
-                  default     = "1y";
-                  example     = "1y";
+                  type = types.str;
+                  default = "0";
+                  example = "1y";
                   description = ''
                     Expiration timeout.
                   '';
                 };
                 usage = lib.mkOption {
-                  type        = with types; listOf (enum [ "sign" "encrypt" "auth" "default" ]);
-                  default     = ["default"];
-                  example     = ["sign" "encrypt" "auth"];
+                  type = with types; listOf (enum [ "sign" "encrypt" "auth" "default" ]);
+                  default = [ "default" ];
+                  example = [ "sign" "encrypt" "auth" ];
                   description = ''
                     Cryptographic usage.
                   '';
@@ -399,25 +309,32 @@ in
             });
           };
           backupRecipients = lib.mkOption {
-            type        = with types; listOf str;
-            default     = [];
-            example     = ["@john@doe.pro"];
+            type = with types; listOf str;
+            default = [ ];
+            example = [ "@john@doe.pro" ];
             description = ''
               Backup keys used to encrypt the a backup copy of the secret keys.
             '';
           };
+          postRun = lib.mkOption {
+            type = types.lines;
+            default = "";
+            description = ''
+              Shell code to run after the key has been generated or tested to exist.
+            '';
+          };
         };
       }));
     };
     dirmngrConf = lib.mkOption {
-      type = types.str;
+      type = types.lines;
       apply = s: pkgs.writeText "dirmngr.conf" s;
       default = ''
         allow-ocsp
-        hkp-cacert ${cfg.keyserverPEM}
+        hkp-cacert ${gnupg.keyserverPEM}
         keyserver hkps://keys.mayfirst.org
-        use-tor
-        #log-file ${cfg.dir.var}/dirmngr.log
+        #use-tor
+        #log-file ${gnupg.gnupgHome}/dirmngr.log
         #standard-resolver
       '';
       description = ''
@@ -425,7 +342,7 @@ in
       '';
     };
     keyserverPEM = lib.mkOption {
-      type = types.str;
+      type = types.lines;
       apply = s: pkgs.writeText "keyserver.pem" s;
       default = builtins.readFile gnupg/keyserver.pem;
       description = ''
@@ -433,29 +350,49 @@ in
       '';
     };
     gpgAgentConf = lib.mkOption {
-      type = types.str;
-      apply = s: pkgs.writeText "gpg-agent.conf" s;
-      default = ''
-        allow-preset-passphrase
-        default-cache-ttl 17200
-        default-cache-ttl-ssh 17200
-        enable-ssh-support
-        max-cache-ttl 17200
-        max-cache-ttl-ssh 17200
-      '';
+      type = types.lines;
+      apply = s: pkgs.writeText "gpg-agent.conf" (s + "\n" + gnupg.gpgAgentExtraConf);
+      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:-curses}" 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.str;
-      apply = s: pkgs.writeText "gpg.conf" s;
+      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
@@ -477,5 +414,40 @@ in
         GnuPG's gpg.conf content.
       '';
     };
+    gpgExtraConf = lib.mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        GnuPG's gpg.conf extra content.
+      '';
+    };
+    gpgAgentExtraConf = lib.mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        GnuPG's gpg-agent.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)
+    '';
   };
 }