gpg-agent: add launchd service agent and sockets

This adds a Darwin Launchd agent along with its sockets to make
gpg-agent starts at load or whenever the sockets are needed.

Fixes: https://github.com/nix-community/home-manager/issues/3864
This commit is contained in:
Ayman Bagabas 2024-08-31 21:12:16 -04:00 committed by Robert Helgesson
parent c82fc8cf3f
commit ef50612457
No known key found for this signature in database
GPG key ID: 96E745BD17AA17ED
6 changed files with 151 additions and 87 deletions

View file

@ -19,3 +19,6 @@ indent_style = tab
[*.md] [*.md]
trim_trailing_whitespace = false trim_trailing_whitespace = false
[*.plist]
insert_final_newline = false

View file

@ -30,7 +30,7 @@ let
$env.SSH_AUTH_SOCK = ($env.SSH_AUTH_SOCK? | default (${gpgPkg}/bin/gpgconf --list-dirs agent-ssh-socket)) $env.SSH_AUTH_SOCK = ($env.SSH_AUTH_SOCK? | default (${gpgPkg}/bin/gpgconf --list-dirs agent-ssh-socket))
''; '';
# mimic `gpgconf` output for use in `systemd` unit definitions. # mimic `gpgconf` output for use in the service definitions.
# we cannot use `gpgconf` directly because it heavily depends on system # we cannot use `gpgconf` directly because it heavily depends on system
# state, but we need the values at build time. original: # state, but we need the values at build time. original:
# https://github.com/gpg/gnupg/blob/c6702d77d936b3e9d91b34d8fdee9599ab94ee1b/common/homedir.c#L672-L681 # https://github.com/gpg/gnupg/blob/c6702d77d936b3e9d91b34d8fdee9599ab94ee1b/common/homedir.c#L672-L681
@ -38,10 +38,14 @@ let
let let
hash = hash =
substring 0 24 (hexStringToBase32 (builtins.hashString "sha1" homedir)); substring 0 24 (hexStringToBase32 (builtins.hashString "sha1" homedir));
in if homedir == options.programs.gpg.homedir.default then subdir = if homedir == options.programs.gpg.homedir.default then
"%t/gnupg/${dir}" "${dir}"
else
"d.${hash}/${dir}";
in if pkgs.stdenv.isDarwin then
"/private/var/run/org.nix-community.home.gpg-agent/${subdir}"
else else
"%t/gnupg/d.${hash}/${dir}"; "%t/gnupg/${subdir}";
# Act like `xxd -r -p | base32` but with z-base-32 alphabet and no trailing padding. # Act like `xxd -r -p | base32` but with z-base-32 alphabet and no trailing padding.
# Written in Nix for purity. # Written in Nix for purity.
@ -77,6 +81,32 @@ let
}; };
in hexString: (foldl' go initState (stringToCharacters hexString)).ret; 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 { in {
meta.maintainers = [ maintainers.rycee ]; meta.maintainers = [ maintainers.rycee ];
@ -272,90 +302,74 @@ in {
'') cfg.sshKeys; '') cfg.sshKeys;
}) })
# The systemd units below are direct translations of the (mkMerge [
# descriptions in the (mkIf pkgs.stdenv.isLinux {
# systemd.user.services.gpg-agent = {
# ${gpgPkg}/share/doc/gnupg/examples/systemd-user Unit = {
# Description = "GnuPG cryptographic agent and passphrase cache";
# directory. Documentation = "man:gpg-agent(1)";
{ Requires = "gpg-agent.socket";
assertions = [ After = "gpg-agent.socket";
(hm.assertions.assertPlatform "services.gpg-agent" pkgs platforms.linux) # This is a socket-activated service:
]; RefuseManualStart = true;
};
systemd.user.services.gpg-agent = { Service = {
Unit = { ExecStart = "${gpgPkg}/bin/gpg-agent --supervised"
Description = "GnuPG cryptographic agent and passphrase cache"; + optionalString cfg.verbose " --verbose";
Documentation = "man:gpg-agent(1)"; ExecReload = "${gpgPkg}/bin/gpgconf --reload gpg-agent";
Requires = "gpg-agent.socket"; Environment = [ "GNUPGHOME=${homedir}" ];
After = "gpg-agent.socket"; };
# This is a socket-activated service:
RefuseManualStart = true;
}; };
Service = { systemd.user.sockets.gpg-agent = mkSocket {
ExecStart = "${gpgPkg}/bin/gpg-agent --supervised" desc = "GnuPG cryptographic agent and passphrase cache";
+ optionalString cfg.verbose " --verbose"; docs = "man:gpg-agent(1)";
ExecReload = "${gpgPkg}/bin/gpgconf --reload gpg-agent"; stream = "S.gpg-agent";
Environment = [ "GNUPGHOME=${homedir}" ]; fdName = "std";
};
};
systemd.user.sockets.gpg-agent = {
Unit = {
Description = "GnuPG cryptographic agent and passphrase cache";
Documentation = "man:gpg-agent(1)";
}; };
Socket = { systemd.user.sockets.gpg-agent-ssh = mkIf cfg.enableSshSupport
ListenStream = gpgconf "S.gpg-agent"; (mkSocket ({
FileDescriptorName = "std"; desc = "GnuPG cryptographic agent (ssh-agent emulation)";
SocketMode = "0600"; docs =
DirectoryMode = "0700"; "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");
};
};
}; };
})
Install = { WantedBy = [ "sockets.target" ]; }; ])
};
}
(mkIf cfg.enableSshSupport {
systemd.user.sockets.gpg-agent-ssh = {
Unit = {
Description = "GnuPG cryptographic agent (ssh-agent emulation)";
Documentation =
"man:gpg-agent(1) man:ssh-add(1) man:ssh-agent(1) man:ssh(1)";
};
Socket = {
ListenStream = gpgconf "S.gpg-agent.ssh";
FileDescriptorName = "ssh";
Service = "gpg-agent.service";
SocketMode = "0600";
DirectoryMode = "0700";
};
Install = { WantedBy = [ "sockets.target" ]; };
};
})
(mkIf cfg.enableExtraSocket {
systemd.user.sockets.gpg-agent-extra = {
Unit = {
Description =
"GnuPG cryptographic agent and passphrase cache (restricted)";
Documentation = "man:gpg-agent(1) man:ssh(1)";
};
Socket = {
ListenStream = gpgconf "S.gpg-agent.extra";
FileDescriptorName = "extra";
Service = "gpg-agent.service";
SocketMode = "0600";
DirectoryMode = "0700";
};
Install = { WantedBy = [ "sockets.target" ]; };
};
})
]); ]);
} }

View file

@ -162,6 +162,7 @@ in import nmtSrc {
./modules/programs/zk ./modules/programs/zk
./modules/programs/zplug ./modules/programs/zplug
./modules/programs/zsh ./modules/programs/zsh
./modules/services/gpg-agent
./modules/services/syncthing/common ./modules/services/syncthing/common
./modules/xresources ./modules/xresources
] ++ lib.optionals isDarwin [ ] ++ lib.optionals isDarwin [
@ -242,7 +243,6 @@ in import nmtSrc {
./modules/services/fusuma ./modules/services/fusuma
./modules/services/git-sync ./modules/services/git-sync
./modules/services/glance ./modules/services/glance
./modules/services/gpg-agent
./modules/services/gromit-mpx ./modules/services/gromit-mpx
./modules/services/home-manager-auto-upgrade ./modules/services/home-manager-auto-upgrade
./modules/services/hypridle ./modules/services/hypridle

View file

@ -2,7 +2,7 @@
with lib; with lib;
{ mkIf pkgs.stdenv.isLinux {
config = { config = {
services.gpg-agent.enable = true; services.gpg-agent.enable = true;
services.gpg-agent.pinentryPackage = pkgs.pinentry-gnome3; services.gpg-agent.pinentryPackage = pkgs.pinentry-gnome3;

View file

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>EnvironmentVariables</key>
<dict>
<key>GNUPGHOME</key>
<string>/path/to/hash</string>
</dict>
<key>KeepAlive</key>
<dict>
<key>Crashed</key>
<true/>
<key>SuccessfulExit</key>
<false/>
</dict>
<key>Label</key>
<string>org.nix-community.home.gpg-agent</string>
<key>ProcessType</key>
<string>Background</string>
<key>ProgramArguments</key>
<array>
<string>@gpg@/bin/gpg-agent</string>
<string>--supervised</string>
</array>
<key>RunAtLoad</key>
<false/>
<key>Sockets</key>
<dict>
<key>Agent</key>
<dict>
<key>SockPathMode</key>
<integer>384</integer>
<key>SockPathName</key>
<string>/private/var/run/org.nix-community.home.gpg-agent/d.wp4h7ks5zxy4dodqadgpbbpz/S.gpg-agent</string>
<key>SockType</key>
<string>stream</string>
</dict>
</dict>
</dict>
</plist>

View file

@ -2,19 +2,25 @@
with lib; with lib;
{ let inherit (pkgs.stdenv) isDarwin;
in {
config = { config = {
services.gpg-agent.enable = true; services.gpg-agent.enable = true;
services.gpg-agent.pinentryPackage = null; # Don't build pinentry package. services.gpg-agent.pinentryPackage = null; # Don't build pinentry package.
programs.gpg = { programs.gpg = {
enable = true; enable = true;
homedir = "/path/to/hash"; homedir = "/path/to/hash";
package = config.lib.test.mkStubPackage { outPath = "@gpg@"; };
}; };
test.stubs.gnupg = { }; test.stubs.gnupg = { };
test.stubs.systemd = { }; # depends on gnupg.override test.stubs.systemd = { }; # depends on gnupg.override
nmt.script = '' nmt.script = if isDarwin then ''
serviceFile=LaunchAgents/org.nix-community.home.gpg-agent.plist
assertFileExists "$serviceFile"
assertFileContent "$serviceFile" ${./expected-agent.plist}
'' else ''
in="${config.systemd.user.sockets.gpg-agent.Socket.ListenStream}" in="${config.systemd.user.sockets.gpg-agent.Socket.ListenStream}"
if [[ $in != "%t/gnupg/d.wp4h7ks5zxy4dodqadgpbbpz/S.gpg-agent" ]] if [[ $in != "%t/gnupg/d.wp4h7ks5zxy4dodqadgpbbpz/S.gpg-agent" ]]
then then