1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8let
9 inherit (lib)
10 mkRemovedOptionModule
11 mkOption
12 mkPackageOption
13 types
14 mkIf
15 optionalString
16 ;
17
18 cfg = config.programs.gnupg;
19
20 agentSettingsFormat = pkgs.formats.keyValue {
21 mkKeyValue = lib.generators.mkKeyValueDefault { } " ";
22 };
23in
24{
25 imports = [
26 (mkRemovedOptionModule [
27 "programs"
28 "gnupg"
29 "agent"
30 "pinentryFlavor"
31 ] "Use programs.gnupg.agent.pinentryPackage instead")
32 ];
33
34 options.programs.gnupg = {
35 package = mkPackageOption pkgs "gnupg" { };
36
37 agent.enable = mkOption {
38 type = types.bool;
39 default = false;
40 description = ''
41 Enables GnuPG agent with socket-activation for every user session.
42 '';
43 };
44
45 agent.enableSSHSupport = mkOption {
46 type = types.bool;
47 default = false;
48 description = ''
49 Enable SSH agent support in GnuPG agent. Also sets SSH_AUTH_SOCK
50 environment variable correctly. This will disable socket-activation
51 and thus always start a GnuPG agent per user session.
52 '';
53 };
54
55 agent.enableExtraSocket = mkOption {
56 type = types.bool;
57 default = false;
58 description = ''
59 Enable extra socket for GnuPG agent.
60 '';
61 };
62
63 agent.enableBrowserSocket = mkOption {
64 type = types.bool;
65 default = false;
66 description = ''
67 Enable browser socket for GnuPG agent.
68 '';
69 };
70
71 agent.pinentryPackage = mkOption {
72 type = types.nullOr types.package;
73 example = lib.literalMD "pkgs.pinentry-gnome3";
74 default = pkgs.pinentry-curses;
75 defaultText = lib.literalMD "matching the configured desktop environment or `pkgs.pinentry-curses`";
76 description = ''
77 Which pinentry package to use. The path to the mainProgram as defined in
78 the package's meta attributes will be set in /etc/gnupg/gpg-agent.conf.
79 If not set by the user, it'll pick an appropriate flavor depending on the
80 system configuration (qt flavor for lxqt and plasma5, gtk2 for xfce,
81 gnome3 on all other systems with X enabled, curses otherwise).
82 '';
83 };
84
85 agent.settings = mkOption {
86 type = agentSettingsFormat.type;
87 default = { };
88 example = {
89 default-cache-ttl = 600;
90 };
91 description = ''
92 Configuration for /etc/gnupg/gpg-agent.conf.
93 See {manpage}`gpg-agent(1)` for supported options.
94 '';
95 };
96
97 dirmngr.enable = mkOption {
98 type = types.bool;
99 default = false;
100 description = ''
101 Enables GnuPG network certificate management daemon with socket-activation for every user session.
102 '';
103 };
104 };
105
106 config = mkIf cfg.agent.enable {
107 programs.gnupg.agent.settings = mkIf (cfg.agent.pinentryPackage != null) {
108 pinentry-program = lib.getExe cfg.agent.pinentryPackage;
109 };
110
111 environment.etc."gnupg/gpg-agent.conf".source =
112 agentSettingsFormat.generate "gpg-agent.conf" cfg.agent.settings;
113
114 # This overrides the systemd user unit shipped with the gnupg package
115 systemd.user.services.gpg-agent = {
116 unitConfig = {
117 Description = "GnuPG cryptographic agent and passphrase cache";
118 Documentation = "man:gpg-agent(1)";
119 Requires = [ "sockets.target" ];
120 };
121 serviceConfig = {
122 ExecStart = "${cfg.package}/bin/gpg-agent --supervised";
123 ExecReload = "${cfg.package}/bin/gpgconf --reload gpg-agent";
124 };
125 };
126
127 systemd.user.sockets.gpg-agent = {
128 unitConfig = {
129 Description = "GnuPG cryptographic agent and passphrase cache";
130 Documentation = "man:gpg-agent(1)";
131 };
132 socketConfig = {
133 ListenStream = "%t/gnupg/S.gpg-agent";
134 FileDescriptorName = "std";
135 SocketMode = "0600";
136 DirectoryMode = "0700";
137 };
138 wantedBy = [ "sockets.target" ];
139 };
140
141 systemd.user.sockets.gpg-agent-ssh = mkIf cfg.agent.enableSSHSupport {
142 unitConfig = {
143 Description = "GnuPG cryptographic agent (ssh-agent emulation)";
144 Documentation = "man:gpg-agent(1) man:ssh-add(1) man:ssh-agent(1) man:ssh(1)";
145 };
146 socketConfig = {
147 ListenStream = "%t/gnupg/S.gpg-agent.ssh";
148 FileDescriptorName = "ssh";
149 Service = "gpg-agent.service";
150 SocketMode = "0600";
151 DirectoryMode = "0700";
152 };
153 wantedBy = [ "sockets.target" ];
154 };
155
156 systemd.user.sockets.gpg-agent-extra = mkIf cfg.agent.enableExtraSocket {
157 unitConfig = {
158 Description = "GnuPG cryptographic agent and passphrase cache (restricted)";
159 Documentation = "man:gpg-agent(1)";
160 };
161 socketConfig = {
162 ListenStream = "%t/gnupg/S.gpg-agent.extra";
163 FileDescriptorName = "extra";
164 Service = "gpg-agent.service";
165 SocketMode = "0600";
166 DirectoryMode = "0700";
167 };
168 wantedBy = [ "sockets.target" ];
169 };
170
171 systemd.user.sockets.gpg-agent-browser = mkIf cfg.agent.enableBrowserSocket {
172 unitConfig = {
173 Description = "GnuPG cryptographic agent and passphrase cache (access for web browsers)";
174 Documentation = "man:gpg-agent(1)";
175 };
176 socketConfig = {
177 ListenStream = "%t/gnupg/S.gpg-agent.browser";
178 FileDescriptorName = "browser";
179 Service = "gpg-agent.service";
180 SocketMode = "0600";
181 DirectoryMode = "0700";
182 };
183 wantedBy = [ "sockets.target" ];
184 };
185
186 systemd.user.services.dirmngr = mkIf cfg.dirmngr.enable {
187 unitConfig = {
188 Description = "GnuPG network certificate management daemon";
189 Documentation = "man:dirmngr(8)";
190 Requires = "dirmngr.socket";
191 };
192 serviceConfig = {
193 ExecStart = "${cfg.package}/bin/dirmngr --supervised";
194 ExecReload = "${cfg.package}/bin/gpgconf --reload dirmngr";
195 };
196 };
197
198 systemd.user.sockets.dirmngr = mkIf cfg.dirmngr.enable {
199 unitConfig = {
200 Description = "GnuPG network certificate management daemon";
201 Documentation = "man:dirmngr(8)";
202 };
203 socketConfig = {
204 ListenStream = "%t/gnupg/S.dirmngr";
205 SocketMode = "0600";
206 DirectoryMode = "0700";
207 };
208 wantedBy = [ "sockets.target" ];
209 };
210
211 services.dbus.packages = mkIf (lib.elem "gnome3" (cfg.agent.pinentryPackage.flavors or [ ])) [
212 pkgs.gcr
213 ];
214
215 environment.systemPackages = [ cfg.package ];
216
217 environment.interactiveShellInit = ''
218 # Bind gpg-agent to this TTY if gpg commands are used.
219 export GPG_TTY=$(tty)
220 '';
221
222 programs.ssh.extraConfig = optionalString cfg.agent.enableSSHSupport ''
223 # The SSH agent protocol doesn't have support for changing TTYs; however we
224 # can simulate this with the `exec` feature of openssh (see ssh_config(5))
225 # that hooks a command to the shell currently running the ssh program.
226 Match host * exec "${pkgs.runtimeShell} -c '${cfg.package}/bin/gpg-connect-agent --quiet updatestartuptty /bye >/dev/null 2>&1'"
227 '';
228
229 environment.extraInit = mkIf cfg.agent.enableSSHSupport ''
230 if [ -z "$SSH_AUTH_SOCK" ]; then
231 export SSH_AUTH_SOCK=$(${cfg.package}/bin/gpgconf --list-dirs agent-ssh-socket)
232 fi
233 '';
234
235 assertions = [
236 {
237 assertion = cfg.agent.enableSSHSupport -> !config.programs.ssh.startAgent;
238 message = "You can't use ssh-agent and GnuPG agent with SSH support enabled at the same time!";
239 }
240 ];
241 };
242}