From e0bfb57d62fa22edef5caab1508e0d5c8c817e64 Mon Sep 17 00:00:00 2001 From: Miles Breslin Date: Tue, 20 Aug 2019 01:30:13 -0700 Subject: [PATCH] gpg: support declarative trust and public keys PR #810 (cherry picked from commit ea1794a798d60ac10b0354b811aea6fe652914a3) --- modules/programs/gpg.nix | 189 ++++++++++++++++++ tests/modules/programs/gpg/default.nix | 6 +- .../programs/gpg/immutable-keyfiles.nix | 52 +++++ .../modules/programs/gpg/mutable-keyfiles.nix | 30 +++ .../programs/gpg/override-defaults.nix | 2 + 5 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 tests/modules/programs/gpg/immutable-keyfiles.nix create mode 100644 tests/modules/programs/gpg/mutable-keyfiles.nix diff --git a/modules/programs/gpg.nix b/modules/programs/gpg.nix index d32a81924..31ddf2e8c 100644 --- a/modules/programs/gpg.nix +++ b/modules/programs/gpg.nix @@ -21,6 +21,110 @@ let } cfg.scdaemonSettings; primitiveType = types.oneOf [ types.str types.bool ]; + + publicKeyOpts = { config, ...}: { + options = { + text = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Text of an OpenPGP public key. + ''; + }; + + source = mkOption { + type = types.path; + description = '' + Path of an OpenPGP public key file. + ''; + }; + + trust = mkOption { + type = types.nullOr (types.enum [ 1 2 3 4 5 ]); + default = null; + description = '' + The amount of trust you have in the key ownership and the care the + owner puts into signing other keys. The available levels are + + + 1 + I don't know or won't say. + + + 2 + I do NOT trust. + + + 3 + I trust marginally. + + + 4 + I trust fully. + + + 5 + I trust ultimately. + + + + See + for more. + ''; + }; + }; + + config = { + source = mkIf (config.text != null) + (pkgs.writeText "gpg-pubkey" config.text); + }; + }; + + importTrustBashFunctions = + let gpg = "${cfg.package}/bin/gpg"; + in '' + function gpgKeyId() { + ${gpg} --show-key --with-colons "$1" \ + | grep ^pub: \ + | cut -d: -f5 + } + + function importTrust() { + local keyId trust + keyId="$(gpgKeyId "$1")" + trust="$2" + if [[ -n $keyId ]] ; then + echo -e "trust\n$trust\ny\nquit" \ + | ${gpg} --no-tty --command-fd 0 --edit-key "$keyId" + fi + } + ''; + + keyringFiles = + let + gpg = "${cfg.package}/bin/gpg"; + + importKey = { source, trust, ... }: '' + ${gpg} --import ${source} + ${optionalString (trust != null) '' + importTrust "${source}" ${toString trust}''} + ''; + + importKeys = concatMapStringsSep "\n" importKey cfg.publicKeys; + in pkgs.runCommand "gpg-pubring" { buildInputs = [ cfg.package ]; } '' + export GNUPGHOME + GNUPGHOME=$(mktemp -d) + + ${importTrustBashFunctions} + ${importKeys} + + mkdir $out + cp $GNUPGHOME/pubring.kbx $out/pubring.kbx + if [[ -e $GNUPGHOME/trustdb.gpg ]] ; then + cp $GNUPGHOME/trustdb.gpg $out/trustdb.gpg + fi + ''; + in { options.programs.gpg = { @@ -73,6 +177,48 @@ in defaultText = literalExpression "\"\${config.home.homeDirectory}/.gnupg\""; description = "Directory to store keychains and configuration."; }; + + mutableKeys = mkOption { + type = types.bool; + default = true; + description = '' + If set to true, you may manage your keyring as a user + using the gpg command. Upon activation, the keyring + will have managed keys added without overwriting unmanaged keys. + + If set to false, the path + $GNUPGHOME/pubring.kbx will become an immutable + link to the Nix store, denying modifications. + ''; + }; + + mutableTrust = mkOption { + type = types.bool; + default = true; + description = '' + If set to true, you may manage trust as a user using + the gpg command. Upon activation, trusted keys have + their trust set without overwriting unmanaged keys. + + If set to false, the path + $GNUPGHOME/trustdb.gpg will be + overwritten on each activation, removing trust for + any unmanaged keys. Be careful to make a backup of your old + trustdb.gpg before switching to immutable trust! + ''; + }; + + publicKeys = mkOption { + type = types.listOf (types.submodule publicKeyOpts); + example = literalExpression '' + [ { source = ./pubkeys.txt; } ] + ''; + default = [ ]; + description = '' + A list of public keys to be imported into GnuPG. Note, these key files + will be copied into the world-readable Nix store. + ''; + }; }; config = mkIf cfg.enable { @@ -109,5 +255,48 @@ in home.file."${cfg.homedir}/gpg.conf".text = cfgText; home.file."${cfg.homedir}/scdaemon.conf".text = scdaemonCfgText; + + # Link keyring if keys are not mutable + home.file."${cfg.homedir}/pubring.kbx" = + mkIf (!cfg.mutableKeys && cfg.publicKeys != []) { + source = "${keyringFiles}/pubring.kbx"; + }; + + home.activation = mkIf (cfg.publicKeys != []) { + importGpgKeys = + let + gpg = "${cfg.package}/bin/gpg"; + + importKey = { source, trust, ... }: + # Import mutable keys + optional cfg.mutableKeys '' + $DRY_RUN_CMD ${gpg} $QUIET_ARG --import ${source}'' + + # Import mutable trust + ++ optional (trust != null && cfg.mutableTrust) '' + $DRY_RUN_CMD importTrust "${source}" ${toString trust}''; + + anyTrust = any (k: k.trust != null) cfg.publicKeys; + + importKeys = concatStringsSep "\n" (concatMap importKey cfg.publicKeys); + + # If any key/trust should be imported then create the block. Otherwise + # leave it empty. + block = concatStringsSep "\n" ( + optional (importKeys != "") '' + export GNUPGHOME=${escapeShellArg cfg.homedir} + if [[ ! -v VERBOSE ]]; then + QUIET_ARG="--quiet" + else + QUIET_ARG="" + fi + ${importTrustBashFunctions} + ${importKeys} + unset GNUPGHOME QUIET_ARG keyId importTrust + '' ++ optional (!cfg.mutableTrust && anyTrust) '' + install -m 0700 ${keyringFiles}/trustdb.gpg "${cfg.homedir}/trustdb.gpg"'' + ); + in lib.hm.dag.entryAfter ["linkGeneration"] block; + }; }; } diff --git a/tests/modules/programs/gpg/default.nix b/tests/modules/programs/gpg/default.nix index 7fed2cdcc..a3949b186 100644 --- a/tests/modules/programs/gpg/default.nix +++ b/tests/modules/programs/gpg/default.nix @@ -1 +1,5 @@ -{ gpg-override-defaults = ./override-defaults.nix; } +{ + gpg-immutable-keyfiles = ./immutable-keyfiles.nix; + gpg-mutable-keyfiles = ./mutable-keyfiles.nix; + gpg-override-defaults = ./override-defaults.nix; +} diff --git a/tests/modules/programs/gpg/immutable-keyfiles.nix b/tests/modules/programs/gpg/immutable-keyfiles.nix new file mode 100644 index 000000000..d75ff5204 --- /dev/null +++ b/tests/modules/programs/gpg/immutable-keyfiles.nix @@ -0,0 +1,52 @@ +{ config, lib, pkgs, ... }: + +{ + programs.gpg = { + enable = true; + + mutableKeys = false; + mutableTrust = false; + + publicKeys = [ + { + source = pkgs.fetchurl { + url = + "https://keybase.io/rycee/pgp_keys.asc?fingerprint=36cacf52d098cc0e78fb0cb13573356c25c424d4"; + sha256 = "082mjy6llvrdry6i9r5gx97nw9d89blnam7bghza4ynsjk1mmx6c"; + }; + trust = 1; + } + { + source = pkgs.fetchurl { + url = "https://www.rsync.net/resources/pubkey.txt"; + sha256 = "16nzqfb1kvsxjkq919hxsawx6ydvip3md3qyhdmw54qx6drnxckl"; + }; + trust = 2; + } + ]; + }; + + nmt.script = '' + assertFileNotRegex activate "^export GNUPGHOME='/home/hm-user/.gnupg'$" + + assertFileRegex activate \ + '^install -m 0700 /nix/store/[0-9a-z]*-gpg-pubring/trustdb.gpg "/home/hm-user/.gnupg/trustdb.gpg"$' + + # Setup GPGHOME + export GNUPGHOME=$(mktemp -d) + cp -r $TESTED/home-files/.gnupg/* $GNUPGHOME + TRUSTDB=$(grep -o '/nix/store/[0-9a-z]*-gpg-pubring/trustdb.gpg' $TESTED/activate) + install -m 0700 $TRUSTDB $GNUPGHOME/trustdb.gpg + + # Export Trust + export WORKDIR=$(mktemp -d) + ${pkgs.gnupg}/bin/gpg -q --export-ownertrust > $WORKDIR/gpgtrust.txt + + # Check Trust + assertFileRegex $WORKDIR/gpgtrust.txt \ + '^36CACF52D098CC0E78FB0CB13573356C25C424D4:2:$' + + assertFileRegex $WORKDIR/gpgtrust.txt \ + '^BB847B5A69EF343CEF511B29073C282D7D6F806C:3:$' + ''; +} diff --git a/tests/modules/programs/gpg/mutable-keyfiles.nix b/tests/modules/programs/gpg/mutable-keyfiles.nix new file mode 100644 index 000000000..588c90704 --- /dev/null +++ b/tests/modules/programs/gpg/mutable-keyfiles.nix @@ -0,0 +1,30 @@ +{ config, lib, pkgs, ... }: + +{ + programs.gpg = { + enable = true; + + publicKeys = [ + { + source = builtins.toFile "key1" "key1"; + trust = 1; + } + { source = builtins.toFile "key2" "key2"; } + ]; + }; + + test.stubs.gnupg = { }; + + nmt.script = '' + assertFileContains activate "export GNUPGHOME='/home/hm-user/.gnupg'" + + assertFileContains activate "unset GNUPGHOME QUIET_ARG keyId importTrust" + + assertFileRegex activate \ + '^\$DRY_RUN_CMD @gnupg@/bin/gpg \$QUIET_ARG --import /nix/store/[0-9a-z]*-key1$' + assertFileRegex activate \ + '^\$DRY_RUN_CMD importTrust "/nix/store/[0-9a-z]*-key1" 1$' + assertFileRegex activate \ + '^\$DRY_RUN_CMD @gnupg@/bin/gpg \$QUIET_ARG --import /nix/store/[0-9a-z]*-key2$' + ''; +} diff --git a/tests/modules/programs/gpg/override-defaults.nix b/tests/modules/programs/gpg/override-defaults.nix index 3b00e451b..62fe50dc2 100644 --- a/tests/modules/programs/gpg/override-defaults.nix +++ b/tests/modules/programs/gpg/override-defaults.nix @@ -23,6 +23,8 @@ with lib; nmt.script = '' assertFileExists home-files/bar/foopg/gpg.conf assertFileContent home-files/bar/foopg/gpg.conf ${./override-defaults-expected.conf} + + assertFileNotRegex activate "^unset GNUPGHOME keyId importTrust$" ''; }; }