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]
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))
'';
# 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
# state, but we need the values at build time. original:
# https://github.com/gpg/gnupg/blob/c6702d77d936b3e9d91b34d8fdee9599ab94ee1b/common/homedir.c#L672-L681
@ -38,10 +38,14 @@ let
let
hash =
substring 0 24 (hexStringToBase32 (builtins.hashString "sha1" homedir));
in if homedir == options.programs.gpg.homedir.default then
"%t/gnupg/${dir}"
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/d.${hash}/${dir}";
"%t/gnupg/${subdir}";
# Act like `xxd -r -p | base32` but with z-base-32 alphabet and no trailing padding.
# Written in Nix for purity.
@ -77,6 +81,32 @@ let
};
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 ];
@ -272,90 +302,74 @@ in {
'') cfg.sshKeys;
})
# The systemd units below are direct translations of the
# descriptions in the
#
# ${gpgPkg}/share/doc/gnupg/examples/systemd-user
#
# directory.
{
assertions = [
(hm.assertions.assertPlatform "services.gpg-agent" pkgs platforms.linux)
];
(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;
};
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}" ];
};
};
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 = {
Unit = {
Description = "GnuPG cryptographic agent and passphrase cache";
Documentation = "man:gpg-agent(1)";
systemd.user.sockets.gpg-agent = mkSocket {
desc = "GnuPG cryptographic agent and passphrase cache";
docs = "man:gpg-agent(1)";
stream = "S.gpg-agent";
fdName = "std";
};
Socket = {
ListenStream = gpgconf "S.gpg-agent";
FileDescriptorName = "std";
SocketMode = "0600";
DirectoryMode = "0700";
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");
};
};
};
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/zplug
./modules/programs/zsh
./modules/services/gpg-agent
./modules/services/syncthing/common
./modules/xresources
] ++ lib.optionals isDarwin [
@ -242,7 +243,6 @@ in import nmtSrc {
./modules/services/fusuma
./modules/services/git-sync
./modules/services/glance
./modules/services/gpg-agent
./modules/services/gromit-mpx
./modules/services/home-manager-auto-upgrade
./modules/services/hypridle

View file

@ -2,7 +2,7 @@
with lib;
{
mkIf pkgs.stdenv.isLinux {
config = {
services.gpg-agent.enable = true;
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;
{
let inherit (pkgs.stdenv) isDarwin;
in {
config = {
services.gpg-agent.enable = true;
services.gpg-agent.pinentryPackage = null; # Don't build pinentry package.
programs.gpg = {
enable = true;
homedir = "/path/to/hash";
package = config.lib.test.mkStubPackage { outPath = "@gpg@"; };
};
test.stubs.gnupg = { };
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}"
if [[ $in != "%t/gnupg/d.wp4h7ks5zxy4dodqadgpbbpz/S.gpg-agent" ]]
then