{ config, options, lib, pkgs, ... }: with lib; let cfg = config.services.gpg-agent; gpgPkg = config.programs.gpg.package; homedir = config.programs.gpg.homedir; gpgSshSupportStr = '' ${gpgPkg}/bin/gpg-connect-agent updatestartuptty /bye > /dev/null ''; gpgInitStr = '' GPG_TTY="$(tty)" export GPG_TTY '' + optionalString cfg.enableSshSupport gpgSshSupportStr; gpgFishInitStr = '' set -gx GPG_TTY (tty) '' + optionalString cfg.enableSshSupport gpgSshSupportStr; gpgNushellInitStr = '' $env.GPG_TTY = (tty) '' + optionalString cfg.enableSshSupport '' ${gpgPkg}/bin/gpg-connect-agent updatestartuptty /bye | ignore $env.SSH_AUTH_SOCK = ($env.SSH_AUTH_SOCK? | default (${gpgPkg}/bin/gpgconf --list-dirs agent-ssh-socket)) ''; # mimic `gpgconf` output for use in the service definitions. # we cannot use `gpgconf` directly because it heavily depends on system # state, but we need the values at build time. original: # https://github.com/gpg/gnupg/blob/c6702d77d936b3e9d91b34d8fdee9599ab94ee1b/common/homedir.c#L672-L681 gpgconf = dir: let hash = substring 0 24 (hexStringToBase32 (builtins.hashString "sha1" homedir)); subdir = if homedir == options.programs.gpg.homedir.default then "${dir}" else "d.${hash}/${dir}"; in if pkgs.stdenv.isDarwin then "/private/var/run/org.nix-community.home.gpg-agent/${subdir}" else "%t/gnupg/${subdir}"; # Act like `xxd -r -p | base32` but with z-base-32 alphabet and no trailing padding. # Written in Nix for purity. hexStringToBase32 = let mod = a: b: a - a / b * b; pow2 = elemAt [ 1 2 4 8 16 32 64 128 256 ]; base32Alphabet = stringToCharacters "ybndrfg8ejkmcpqxot1uwisza345h769"; hexToIntTable = listToAttrs (genList (x: { name = toLower (toHexString x); value = x; }) 16); initState = { ret = ""; buf = 0; bufBits = 0; }; go = { ret, buf, bufBits }: hex: let buf' = buf * pow2 4 + hexToIntTable.${hex}; bufBits' = bufBits + 4; extraBits = bufBits' - 5; in if bufBits >= 5 then { ret = ret + elemAt base32Alphabet (buf' / pow2 extraBits); buf = mod buf' (pow2 extraBits); bufBits = bufBits' - 5; } else { ret = ret; buf = buf'; bufBits = bufBits'; }; in hexString: (foldl' go initState (stringToCharacters hexString)).ret; # Systemd socket unit generator. mkSocket = { desc, docs, stream, fdName }: { Unit = { Description = desc; Documentation = docs; }; Socket = { ListenStream = gpgconf "${stream}"; FileDescriptorName = "${fdName}"; Service = "gpg-agent.service"; SocketMode = "0600"; DirectoryMode = "0700"; }; Install = { WantedBy = [ "sockets.target" ]; }; }; # Launchd agent socket generator. mkAgentSock = name: { SockType = "stream"; SockPathName = gpgconf name; SockPathMode = 384; # Property lists don't support octal literals (0600 = 384). }; in { meta.maintainers = [ maintainers.rycee ]; imports = [ (mkRemovedOptionModule [ "services" "gpg-agent" "pinentryFlavor" ] "Use services.gpg-agent.pinentryPackage instead") ]; options = { services.gpg-agent = { enable = mkEnableOption "GnuPG private key agent"; defaultCacheTtl = mkOption { type = types.nullOr types.int; default = null; description = '' Set the time a cache entry is valid to the given number of seconds. ''; }; defaultCacheTtlSsh = mkOption { type = types.nullOr types.int; default = null; description = '' Set the time a cache entry used for SSH keys is valid to the given number of seconds. ''; }; maxCacheTtl = mkOption { type = types.nullOr types.int; default = null; description = '' Set the maximum time a cache entry is valid to n seconds. After this time a cache entry will be expired even if it has been accessed recently or has been set using gpg-preset-passphrase. The default is 2 hours (7200 seconds). ''; }; maxCacheTtlSsh = mkOption { type = types.nullOr types.int; default = null; description = '' Set the maximum time a cache entry used for SSH keys is valid to n seconds. After this time a cache entry will be expired even if it has been accessed recently or has been set using gpg-preset-passphrase. The default is 2 hours (7200 seconds). ''; }; enableSshSupport = mkOption { type = types.bool; default = false; description = '' Whether to use the GnuPG key agent for SSH keys. ''; }; sshKeys = mkOption { type = types.nullOr (types.listOf types.str); default = null; description = '' Which GPG keys (by keygrip) to expose as SSH keys. ''; }; enableExtraSocket = mkOption { type = types.bool; default = false; description = '' Whether to enable extra socket of the GnuPG key agent (useful for GPG Agent forwarding). ''; }; verbose = mkOption { type = types.bool; default = false; description = '' Whether to produce verbose output. ''; }; grabKeyboardAndMouse = mkOption { type = types.bool; default = true; description = '' Tell the pinentry to grab the keyboard and mouse. This option should in general be used to avoid X-sniffing attacks. When disabled, this option passes {option}`no-grab` setting to gpg-agent. ''; }; enableScDaemon = mkOption { type = types.bool; default = true; description = '' Make use of the scdaemon tool. This option has the effect of enabling the ability to do smartcard operations. When disabled, this option passes {option}`disable-scdaemon` setting to gpg-agent. ''; }; extraConfig = mkOption { type = types.lines; default = ""; example = '' allow-emacs-pinentry allow-loopback-pinentry ''; description = '' Extra configuration lines to append to the gpg-agent configuration file. ''; }; pinentryPackage = mkOption { type = types.nullOr types.package; example = literalExpression "pkgs.pinentry-gnome3"; default = null; description = '' Which pinentry interface to use. If not `null`, it sets {option}`pinentry-program` in {file}`gpg-agent.conf`. Beware that `pinentry-gnome3` may not work on non-Gnome systems. You can fix it by adding the following to your system configuration: ```nix services.dbus.packages = [ pkgs.gcr ]; ``` ''; }; enableBashIntegration = mkEnableOption "Bash integration" // { default = true; }; enableZshIntegration = mkEnableOption "Zsh integration" // { default = true; }; enableFishIntegration = mkEnableOption "Fish integration" // { default = true; }; enableNushellIntegration = mkEnableOption "Nushell integration" // { default = true; }; }; }; config = mkIf cfg.enable (mkMerge [ { home.file."${homedir}/gpg-agent.conf".text = concatStringsSep "\n" (optional (cfg.enableSshSupport) "enable-ssh-support" ++ optional cfg.grabKeyboardAndMouse "grab" ++ optional (!cfg.enableScDaemon) "disable-scdaemon" ++ optional (cfg.defaultCacheTtl != null) "default-cache-ttl ${toString cfg.defaultCacheTtl}" ++ optional (cfg.defaultCacheTtlSsh != null) "default-cache-ttl-ssh ${toString cfg.defaultCacheTtlSsh}" ++ optional (cfg.maxCacheTtl != null) "max-cache-ttl ${toString cfg.maxCacheTtl}" ++ optional (cfg.maxCacheTtlSsh != null) "max-cache-ttl-ssh ${toString cfg.maxCacheTtlSsh}" ++ optional (cfg.pinentryPackage != null) "pinentry-program ${lib.getExe cfg.pinentryPackage}" ++ [ cfg.extraConfig ]); # Make sure we export GnuPG agent socket for SSH # https://www.gnupg.org/documentation/manuals/gnupg/Agent-Examples.html#Agent-Examples home.sessionVariablesExtra = optionalString cfg.enableSshSupport '' unset SSH_AGENT_PID if [ "''${gnupg_SSH_AUTH_SOCK_by:-0}" -ne $$ ]; then export SSH_AUTH_SOCK="$(${gpgPkg}/bin/gpgconf --list-dirs agent-ssh-socket)" fi ''; programs.bash.initExtra = mkIf cfg.enableBashIntegration gpgInitStr; programs.zsh.initExtra = mkIf cfg.enableZshIntegration gpgInitStr; programs.fish.interactiveShellInit = mkIf cfg.enableFishIntegration gpgFishInitStr; programs.nushell.extraEnv = mkIf cfg.enableNushellIntegration gpgNushellInitStr; } (mkIf (cfg.sshKeys != null) { # Trailing newlines are important home.file."${homedir}/sshcontrol".text = concatMapStrings (s: '' ${s} '') cfg.sshKeys; }) (mkMerge [ (mkIf pkgs.stdenv.isLinux { systemd.user.services.gpg-agent = { Unit = { Description = "GnuPG cryptographic agent and passphrase cache"; Documentation = "man:gpg-agent(1)"; Requires = "gpg-agent.socket"; After = "gpg-agent.socket"; # This is a socket-activated service: RefuseManualStart = true; }; Service = { ExecStart = "${gpgPkg}/bin/gpg-agent --supervised" + optionalString cfg.verbose " --verbose"; ExecReload = "${gpgPkg}/bin/gpgconf --reload gpg-agent"; Environment = [ "GNUPGHOME=${homedir}" ]; }; }; systemd.user.sockets.gpg-agent = mkSocket { desc = "GnuPG cryptographic agent and passphrase cache"; docs = "man:gpg-agent(1)"; stream = "S.gpg-agent"; fdName = "std"; }; systemd.user.sockets.gpg-agent-ssh = mkIf cfg.enableSshSupport (mkSocket ({ desc = "GnuPG cryptographic agent (ssh-agent emulation)"; docs = "man:gpg-agent(1) man:ssh-add(1) man:ssh-agent(1) man:ssh(1)"; stream = "S.gpg-agent.ssh"; fdName = "ssh"; })); systemd.user.sockets.gpg-agent-extra = mkIf cfg.enableExtraSocket (mkSocket { desc = "GnuPG cryptographic agent and passphrase cache (restricted)"; docs = "man:gpg-agent(1) man:ssh(1)"; stream = "S.gpg-agent.extra"; fdName = "extra"; }); }) (mkIf pkgs.stdenv.isDarwin { launchd.agents.gpg-agent = { enable = true; config = { ProgramArguments = [ "${gpgPkg}/bin/gpg-agent" "--supervised" ] ++ optional cfg.verbose "--verbose"; EnvironmentVariables = { GNUPGHOME = homedir; }; KeepAlive = { Crashed = true; SuccessfulExit = false; }; ProcessType = "Background"; RunAtLoad = cfg.enableSshSupport; Sockets = { Agent = mkAgentSock "S.gpg-agent"; Ssh = mkIf cfg.enableSshSupport (mkAgentSock "S.gpg-agent.ssh"); Extra = mkIf cfg.enableExtraSocket (mkAgentSock "S.gpg-agent.extra"); }; }; }; }) ]) ]); }