1
0
Fork 0
mirror of https://github.com/nix-community/home-manager.git synced 2025-11-08 11:36:05 +01:00
home-manager/modules/services/gpg-agent.nix
2025-11-02 05:52:54 -06:00

468 lines
14 KiB
Nix

{
config,
options,
lib,
pkgs,
...
}:
let
inherit (lib)
mkIf
mkOption
optional
optionalString
types
;
cfg = config.services.gpg-agent;
gpgPkg = config.programs.gpg.package;
inherit (config.programs.gpg) homedir;
gpgSshSupportStr = "${gpgPkg}/bin/gpg-connect-agent --quiet updatestartuptty /bye";
gpgBashInitStr = ''
GPG_TTY="$(tty)"
export GPG_TTY
''
+ optionalString cfg.enableSshSupport ''
${gpgSshSupportStr} > /dev/null
'';
gpgZshInitStr = ''
export GPG_TTY=$TTY
''
+ optionalString cfg.enableSshSupport ''
${gpgSshSupportStr} > /dev/null
'';
gpgFishInitStr = ''
set -gx GPG_TTY (tty)
''
+ optionalString cfg.enableSshSupport ''
${gpgSshSupportStr} > /dev/null
'';
gpgNushellInitStr = ''
$env.GPG_TTY = (tty)
''
+ optionalString cfg.enableSshSupport ''
${gpgSshSupportStr} | 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 = lib.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 = lib.elemAt [
1
2
4
8
16
32
64
128
256
];
base32Alphabet = lib.stringToCharacters "ybndrfg8ejkmcpqxot1uwisza345h769";
hexToIntTable = lib.listToAttrs (
lib.genList (x: {
name = lib.toLower (lib.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 + lib.elemAt base32Alphabet (buf' / pow2 extraBits);
buf = mod buf' (pow2 extraBits);
bufBits = bufBits' - 5;
}
else
{
inherit ret;
buf = buf';
bufBits = bufBits';
};
in
hexString: (lib.foldl' go initState (lib.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 = [ lib.maintainers.rycee ];
imports = [
(lib.mkRemovedOptionModule [
"services"
"gpg-agent"
"pinentryFlavor"
] "Use services.gpg-agent.pinentryPackage instead")
(lib.mkRenamedOptionModule
[
"services"
"gpg-agent"
"pinentryPackage"
]
[
"services"
"gpg-agent"
"pinentry"
"package"
]
)
];
options = {
services.gpg-agent = {
enable = lib.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.
'';
};
noAllowExternalCache = mkOption {
type = types.bool;
default = false;
description = ''
Tell Pinentry not to enable features which use an external cache for
passphrases.
Some desktop environments prefer to unlock all credentials with one
master password and may have installed a Pinentry which employs an
additional external cache to implement such a policy. By using this
option the Pinentry is advised not to make use of such a cache and
instead always ask the user for the requested passphrase.
'';
};
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.
'';
};
pinentry = {
package = lib.mkPackageOption pkgs "pinentry-gnome3" {
nullable = true;
default = null;
extraDescription = ''
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 configuration:
```nix
home.packages = [ pkgs.gcr ];
```
'';
};
program = lib.mkOption {
type = types.nullOr types.str;
example = "pinentry-wayprompt";
description = ''
Which program to search for in the configured `pinentry.package`.
'';
};
};
enableBashIntegration = lib.hm.shell.mkBashIntegrationOption { inherit config; };
enableFishIntegration = lib.hm.shell.mkFishIntegrationOption { inherit config; };
enableNushellIntegration = lib.hm.shell.mkNushellIntegrationOption { inherit config; };
enableZshIntegration = lib.hm.shell.mkZshIntegrationOption { inherit config; };
};
};
config = mkIf cfg.enable (
lib.mkMerge [
{
# Grab the default binary name and fallback to expected value if `meta.mainProgram` not set
services.gpg-agent.pinentry.program = lib.mkOptionDefault (
cfg.pinentry.package.meta.mainProgram or "pinentry"
);
home.file."${homedir}/gpg-agent.conf".text = lib.concatStringsSep "\n" (
optional cfg.enableSshSupport "enable-ssh-support"
++ optional cfg.grabKeyboardAndMouse "grab"
++ optional (!cfg.enableScDaemon) "disable-scdaemon"
++ optional cfg.noAllowExternalCache "no-allow-external-cache"
++ 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.pinentry.package != null
) "pinentry-program ${lib.getExe' cfg.pinentry.package cfg.pinentry.program}"
++ [ cfg.extraConfig ]
);
home.sessionVariablesExtra = optionalString cfg.enableSshSupport ''
unset SSH_AGENT_PID
if [ -z "$SSH_CONNECTION" -o -z "$SSH_AUTH_SOCK" ] && [ "''${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 gpgBashInitStr;
zsh.initContent = mkIf cfg.enableZshIntegration gpgZshInitStr;
fish.interactiveShellInit = mkIf cfg.enableFishIntegration gpgFishInitStr;
nushell.extraConfig = mkIf cfg.enableNushellIntegration gpgNushellInitStr;
};
}
(mkIf (cfg.sshKeys != null) {
# Trailing newlines are important
home.file."${homedir}/sshcontrol".text = lib.concatMapStrings (s: ''
${s}
'') cfg.sshKeys;
})
(lib.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}" ];
};
};
sockets = {
gpg-agent = mkSocket {
desc = "GnuPG cryptographic agent and passphrase cache";
docs = "man:gpg-agent(1)";
stream = "S.gpg-agent";
fdName = "std";
};
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";
});
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");
};
};
};
})
])
]
);
}