.gnupg](https://search.nixos.org/options?channel=unstable&from=0&size=30&sort=relevance&query=security.pam.services.%3Cname%3E.gnupg) (currently only in the unstable channel)
27 |
28 | ### Manually
29 |
30 | The usual
31 |
32 | ./autogen.sh
33 | ./configure
34 | make
35 | make install
36 |
37 | should work. `configure` takes an option `--with-moduledir` to set the
38 | installation path of the PAM module. It defaults to `/lib/security`, but your
39 | distribution might use a different path.
40 |
41 | ## Usage
42 |
43 | ### Setup guide
44 |
45 | - For services that open a new session (gdm, sddm, login, ...), add the lines
46 |
47 | auth optional pam_gnupg.so store-only
48 | session optional pam_gnupg.so
49 |
50 | at the end (¹) of the corresponding file in `/etc/pam.d`, or in one of the
51 | files included from there, e.g. `system-local-login` on Arch.
52 |
53 | When opening the session, gpg-agent will be autostarted if necessary. If you
54 | want to start it by other means, e.g. as a systemd user service, make sure it
55 | is up before `pam_gnupg.so` is run, e.g. by putting the lines somewhere below
56 | `pam_systemd.so`, and (optionally) add `no-autostart` to the `session` line.
57 |
58 | (¹): The end is usually a good place, but details depend on your PAM setup.
59 | In particular, modules declared `sufficient` can terminate the PAM stack
60 | early. At least, `pam_gnupg.so` should come after `pam_unix.so`,
61 | `pam_systemd_home.so`, `pam_systemd.so` and `pam_env.so` in case you use
62 | those modules.
63 |
64 | - For services that only authenticate (i3lock, physlock, ...), use
65 |
66 | auth optional pam_gnupg.so
67 |
68 | For screen lockers, this only really makes sense if you arrange for the
69 | password cache to be cleared prior to locking the screen by calling
70 |
71 | gpg-connect-agent --no-autostart reloadagent /bye
72 |
73 | During authentication, the agent will never be autostarted.
74 |
75 | - Add
76 |
77 | allow-preset-passphrase
78 |
79 | to `~/.gnupg/gpg-agent.conf`. Optionally, customize the cache timeout via
80 | `max-cache-ttl`, e.g. set
81 |
82 | max-cache-ttl 86400
83 |
84 | to have it expire after a day.
85 |
86 | - Run
87 |
88 | gpg -K --with-keygrip
89 |
90 | The output should look something like this:
91 |
92 | sec rsa2048 2018-11-16 [SC]
93 | 9AB5DD43C5E5FD40475FA6DA0D776275F7F5B2E7
94 | Keygrip = 6F4ABB77A88E922406BCE6627AFEEE2363914B76
95 | uid [ultimate] Chris Ruegge <mail@cxcs.de>
96 | ssb rsa2048 2018-11-16 [E]
97 | Keygrip = FBDEAD7B0C484CDC85F1CF70352833EB0C921D58
98 |
99 |
100 | Write the keygrip for the encryption subkey marked `[E]` – shown in boldface
101 | in the output above – into `~/.pam-gnupg`. If you want to unlock multiple
102 | keys or subkeys, add all keygrips on separate lines.
103 |
104 | Keygrips are exactly 40 characters in length. Leading whitespace, lines
105 | starting with `#` and everything after the keygrip is ignored.
106 |
107 | If `~/.pam-gnupg` does not exists, `$XDG_CONFIG_HOME/pam-gnupg` will be
108 | tried, with `XDG_CONFIG_HOME` defaulting to `~/.config` as usual. If you want
109 | to customize this variable, read the section on environment variables below.
110 |
111 | - Set the same password for your gpg key and your user account. All pam-gnupg
112 | does is to send the password as entered to gpg-agent. It is therefore not
113 | compatible with auto-login of any kind; you actually have to type your
114 | password for things to work.
115 |
116 | ### `GNUPGHOME`
117 |
118 | If you change your gnupg directory from the default `~/.gnupg` by setting
119 | `GNUPGHOME`, this variable needs to be made available to pam-gnupg when
120 | presetting. Since PAM usually runs before your init scripts, it needs to obtain
121 | the variable in a different way.
122 |
123 | To set it, add the path to the config file on a separate line before any
124 | keygrips, either as absolute path or starting with `~/` for paths relative to
125 | the home directory. The connection to the agent will be opened when the first
126 | keygrip is read, so setting `GNUPGHOME` after that will have no effect.
127 |
128 | Note that the variable is only used for connecting to and optionally
129 | autostarting the agent. It is *not* passed down to your login shell or desktop
130 | session, so you also need to set it in your init scripts. Additionally, if you
131 | start the agent via systemd, you need to adjust the various service and socket
132 | units separately.
133 |
134 | #### Alternatives
135 |
136 | If you use `systemd-homed`, you can modify env vars via `homectl --setenv`, and
137 | they will be made available to PAM by `pam_systemd_home.so`.
138 |
139 | Another way is to run `pam_env.so` with `user_readenv=1` before `pam_gnupg.so`,
140 | so you can set env vars from `~/.pam_environment`, e.g.
141 |
142 | GNUPGHOME DEFAULT=@{HOME}/path/to/your/gnupg
143 |
144 | You can also modify `XDG_CONFIG_HOME` this way. Unfortunately, `user_readenv` is
145 | deprecated and will go away in some future version of `pam_env`.
146 |
147 | ### SSH Keys
148 |
149 | SSH key support is indirect via gpg-agent's built-in SSH support (there's no
150 | SSH specific code in pam-gnupg). The full details are in `gpg-agent(1)`, but
151 | here's a basic step-by-step guide:
152 |
153 | - Add
154 |
155 | enable-ssh-support
156 |
157 | to `~/.gnupg/gpg-agent.conf`. (This is not actually strictly necessary in all
158 | setups, but doesn't hurt either.)
159 |
160 | - Set the `SSH_AUTH_SOCK` variable to gpg-agent's SSH socket by putting
161 |
162 | export SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket)
163 |
164 | into your relevant init script.
165 |
166 | - Add your SSH keys to the agent using `ssh-add` as usual. This only needs to
167 | be done once. The agent will re-encrypt the keys and store them in
168 | `~/.gnupg`, independent of the ones in `~/.ssh`.
169 |
170 | - Get the SSH keygrips using
171 |
172 | gpg-connect-agent 'keyinfo --ssh-list' /bye
173 |
174 | The output should look like
175 |
176 | S KEYINFO DBB0B60CFE5F23716ABEE8787C6184C27E2486E1 D - - - P - - S
177 | OK
178 |
179 | with one keygrip per line. Alternatively, get them from `~/.gnupg/sshcontrol`.
180 |
181 | - Add the keygrips to `~/.pam-gnupg` the same way as for the gpg keys.
182 |
183 | ### Debug output
184 |
185 | Both the `auth` and the `session` module take a `debug` option to enable some
186 | basic debug logging to syslog / journal.
187 |
188 | ### Known issues
189 |
190 | - Using `pass` during startup of systemd user services has a racing condition
191 | even if the service declares `After=gpg-agent.socket`, because systemd does
192 | not know about pam-gnupg and will start the service right after the socket is
193 | up, but maybe before the key has been unlocked. Until I figure out a cleaner
194 | solution, you can circumvent this by adding a small startup delay to the
195 | service, e.g.
196 |
197 | ExecStartPre=/usr/bin/sleep 5
198 |
199 | - Screen lockers need to call `pam_setcred` after authentication to actually
200 | send the passphrase. Those who don't will not work with pam-gnupg.
201 | - Specifically for [suckless' slock](https://tools.suckless.org/slock/) with the
202 | [pam-auth
203 | patch](https://tools.suckless.org/slock/patches/pam_auth/slock-pam_auth-20190207-35633d4.diff),
204 | you have to set `user` and `group` to your user name and your primary group
205 | (as displayed by `id -gn`) in slock's `config.h`, which will therefore not work for multiple users. Alternatively, you can try the (untested) steps outlined in [this issue comment](https://github.com/cruegge/pam-gnupg/issues/34#issuecomment-857182214).
206 |
207 | ## Contact
208 |
209 | - Email: mail@cxcs.de, [gpg key](https://gist.githubusercontent.com/cruegge/273380ce582d8d6c38b00bfaac433711/raw/3b6d506bd650d2e1b92c138bc608c6c567f048cc/mail@cxcs.de.pub.asc). The `gpg -K` output above is real, so the second line is the actual fingerprint.
210 |
--------------------------------------------------------------------------------
/autogen.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | autoreconf --force --install
4 |
--------------------------------------------------------------------------------
/configure.ac:
--------------------------------------------------------------------------------
1 | AC_INIT([pam-gnupg], [0.1])
2 | AC_CONFIG_SRCDIR([src/pam_gnupg.c])
3 | AC_CONFIG_MACRO_DIRS([m4])
4 | AC_CONFIG_AUX_DIR([build-aux])
5 | AM_INIT_AUTOMAKE([foreign -Wall -Werror])
6 |
7 | AC_DISABLE_STATIC
8 |
9 | AC_PROG_CC
10 | AC_PROG_AWK
11 |
12 | AM_PROG_AR
13 | LT_INIT
14 |
15 | AC_ARG_WITH([moduledir], AS_HELP_STRING([--with-moduledir=PATH],
16 | [Path where PAM modules are to be installed [[/lib/security]]]),
17 | [moduledir="$withval"], [moduledir="/lib/security"])
18 | AC_SUBST(moduledir)
19 |
20 | AC_CHECK_HEADERS([security/pam_modules.h], [have_pamheader="yes"])
21 | if test x"$have_pamheader" != x"yes"; then
22 | AC_MSG_ERROR([You are missing PAM headers])
23 | fi
24 |
25 | AC_CHECK_PROGS([GPGCONF], [gpgconf], [:])
26 | if test x"$GPGCONF" = x:; then
27 | AC_MSG_ERROR([gpgconf not found])
28 | fi
29 |
30 | GPG_MIN_VERSION="2.1"
31 | AC_MSG_CHECKING([whether GnuPG version is >= ${GPG_MIN_VERSION}])
32 | GPG=$("$GPGCONF" --list-components | "$AWK" -F: '$1=="gpg" {print $3}')
33 | GPG_VERSION=$("$GPG" --version | "$AWK" 'NR==1 {print $NF}')
34 | AS_VERSION_COMPARE(${GPG_VERSION}, ${GPG_MIN_VERSION}, [gpg_too_old="yes"])
35 | if test x"$gpg_too_old" = xyes; then
36 | AC_MSG_RESULT([no])
37 | AC_MSG_ERROR([GnuPG is too old])
38 | else
39 | AC_MSG_RESULT([yes])
40 | fi
41 |
42 | AC_PATH_PROG(GPG_CONNECT_AGENT, [gpg-connect-agent], [:],
43 | [$("$GPGCONF" --list-dirs bindir)])
44 | if test x"$GPG_CONNECT_AGENT" = x:; then
45 | AC_MSG_ERROR([gpg-connect-agent not found])
46 | fi
47 | AC_DEFINE_UNQUOTED([GPG_CONNECT_AGENT], "$GPG_CONNECT_AGENT",
48 | [path to gpg-connect-agent])
49 |
50 | # From https://github.com/gpg/gnupg/blob/4c43fab/agent/agent.h#L54
51 | AC_DEFINE([MAX_PASSPHRASE_LEN], [255],
52 | [Maximum passphrase length accepted by gpg-agent])
53 |
54 | AC_CONFIG_HEADERS([config.h])
55 | AC_CONFIG_FILES([Makefile src/Makefile])
56 | AC_OUTPUT
57 |
--------------------------------------------------------------------------------
/src/Makefile.am:
--------------------------------------------------------------------------------
1 | module_LTLIBRARIES = pam_gnupg.la
2 | pam_gnupg_la_SOURCES = pam_gnupg.c
3 | pam_gnupg_la_LIBADD = -lpam
4 | pam_gnupg_la_LDFLAGS = -module -avoid-version
5 | pam_gnupg_la_CPPFLAGS = -DPAM_GNUPG_HELPER='"$(libexecdir)/pam_gnupg_helper"'
6 |
7 | libexec_PROGRAMS = pam_gnupg_helper
8 | pam_gnupg_helper_SOURCES = helper.c
9 |
10 | install-data-hook:
11 | rm -f $(DESTDIR)$(moduledir)/pam_gnupg.la
12 |
--------------------------------------------------------------------------------
/src/helper.c:
--------------------------------------------------------------------------------
1 | #define _GNU_SOURCE
2 |
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include
13 | #include
14 |
15 | #include "config.h"
16 |
17 | #define KEYGRIP_LEN 40
18 |
19 | #define xstr(x) str(x)
20 | #define str(x) #x
21 |
22 | #define die(...) do { syslog(LOG_ERR, __VA_ARGS__); exit(EXIT_FAILURE); } while (0)
23 |
24 | char tohex(char n) {
25 | if (n < 10) {
26 | return n + '0';
27 | } else {
28 | return n - 10 + 'A';
29 | }
30 | }
31 |
32 | void nextline(FILE *f) {
33 | for (;;) {
34 | switch (getc(f)) {
35 | case EOF:
36 | case '\n':
37 | return;
38 | }
39 | }
40 | }
41 |
42 | bool nextentry(FILE *f) {
43 | int c;
44 | for (;;) {
45 | do c = getc(f);
46 | while (c != EOF && strchr(" \t\n\r\f\v", c));
47 | if (c == EOF) {
48 | return false;
49 | }
50 | if (c != '#') {
51 | ungetc(c, f);
52 | return true;
53 | }
54 | nextline(f);
55 | }
56 | }
57 |
58 | void read_gnupghome(FILE *f, char *homedir) {
59 | char *gnupghome = NULL;
60 | char *line = NULL;
61 | size_t bufsize = 0;
62 | size_t len = getline(&line, &bufsize, f);
63 | if (len < 0) {
64 | die("failed to read GNUPGHOME from file: %m");
65 | } else if (len > 0) {
66 | if (line[len - 1] == '\n') {
67 | line[len - 1] = '\0';
68 | }
69 | if (line[0] == '~') {
70 | if (asprintf(&gnupghome, "%s%s", homedir, line + 1) < 0) {
71 | die("tilde expansion failed: %m");
72 | }
73 | free(line);
74 | } else {
75 | gnupghome = line;
76 | }
77 | if (setenv("GNUPGHOME", gnupghome, true) < 0) {
78 | die("failed to set GNUPGHOME: %m");
79 | }
80 | }
81 | free(gnupghome);
82 | }
83 |
84 | FILE *open_config(char *homedir) {
85 | if (chdir(homedir) < 0) {
86 | if (errno == ENOENT) {
87 | exit(EXIT_SUCCESS);
88 | }
89 | die("failed to open home directory: %m");
90 | }
91 |
92 | FILE *f = fopen(".pam-gnupg", "re");
93 | if (f != NULL) {
94 | return f;
95 | }
96 | if (errno != ENOENT) {
97 | die("failed to open config file: %m");
98 | }
99 |
100 | if (chdir(getenv("XDG_CONFIG_HOME") ?: ".config") < 0) {
101 | if (errno == ENOENT) {
102 | exit(EXIT_SUCCESS);
103 | }
104 | die("failed to open config directory: %m");
105 | }
106 |
107 | f = fopen("pam-gnupg", "re");
108 | if (f != NULL) {
109 | return f;
110 | }
111 | if (errno != ENOENT) {
112 | die("failed to open config file: %m");
113 | }
114 |
115 | exit(EXIT_SUCCESS);
116 | }
117 |
118 | pid_t connect_agent(bool autostart, int pipefd) {
119 | pid_t pid = fork();
120 | if (pid == -1) {
121 | die("fork failed: %m");
122 | } else if (pid == 0) {
123 | if (dup2(pipefd, STDIN_FILENO) < 0) {
124 | die("dup failed: %m");
125 | }
126 | // gpg-connect-agent has an option --no-autostart, which *should* return
127 | // non-zero when the agent is not running. Unfortunately, the exit code is
128 | // always 0 in version 2.1. Passing an invalid agent program here is a
129 | // workaround. See https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=797334
130 | char *cmd[] = {GPG_CONNECT_AGENT, "--agent-program", "/dev/null", NULL};
131 | if (autostart) {
132 | cmd[1] = NULL;
133 | }
134 | execv(cmd[0], cmd);
135 | die("exec failed: %m");
136 | }
137 | close(pipefd);
138 | return pid;
139 | }
140 |
141 | int main(int argc, char **argv) {
142 | bool autostart = false;
143 | for (int i = 1; i < argc; i++) {
144 | if (strcmp(argv[i], "--autostart") == 0) {
145 | autostart = true;
146 | }
147 | }
148 |
149 | openlog("pam_gnupg_helper", 0, LOG_AUTHPRIV);
150 |
151 | errno = 0;
152 | struct passwd *pwd = getpwuid(getuid());
153 | if (pwd == NULL) {
154 | if (errno == 0) {
155 | die("getpwuid failed: User not found");
156 | } else {
157 | die("getpwuid failed: %m");
158 | }
159 | }
160 |
161 | FILE *f = open_config(pwd->pw_dir);
162 |
163 | char tok[MAX_PASSPHRASE_LEN + 1];
164 | if (fgets(tok, MAX_PASSPHRASE_LEN + 1, stdin) == NULL) {
165 | die("failed to read passphrase: %m");
166 | }
167 |
168 | char hextok[2 * MAX_PASSPHRASE_LEN + 1];
169 | char *s = tok, *h = hextok;
170 | while (*s != '\0' && *s != '\n') {
171 | *h++ = tohex((*s >> 4) & 15);
172 | *h++ = tohex(*s & 15);
173 | s++;
174 | }
175 | *s = *h = '\0';
176 |
177 | int pipefd[2];
178 | if (pipe2(pipefd, O_CLOEXEC) < 0) {
179 | die("failed to open pipe: %m");
180 | }
181 |
182 | FILE *p = fdopen(pipefd[1], "w");
183 | if (p == NULL) {
184 | die("failed to fdopen pipe: %m");
185 | }
186 |
187 | signal(SIGCHLD, SIG_DFL);
188 | signal(SIGPIPE, SIG_IGN);
189 | pid_t pid = 0;
190 | for (; nextentry(f); nextline(f)) {
191 | int c = getc(f);
192 | ungetc(c, f);
193 | if (c == '/' || c == '~') {
194 | if (pid != 0) {
195 | syslog(LOG_WARNING, "Ignored GNUPHOME setting after keygrip.");
196 | } else {
197 | // TODO Should we send the environment during auth, or completely rely on session env?
198 | read_gnupghome(f, pwd->pw_dir);
199 | // Push back a newline, so nextline won't skip anything
200 | ungetc('\n', f);
201 | }
202 | continue;
203 | }
204 | char keygrip[KEYGRIP_LEN + 1];
205 | if (fscanf(f, "%" xstr(KEYGRIP_LEN) "[0-9A-Fa-f]", keygrip) < 1) {
206 | continue;
207 | }
208 | if (strlen(keygrip) < KEYGRIP_LEN) {
209 | continue;
210 | }
211 | for (s = keygrip; *s; s++) {
212 | *s = toupper(*s);
213 | }
214 | if (pid == 0) {
215 | // Connect when we see the first keygrip to allow setting GNUPGHOME first.
216 | pid = connect_agent(autostart, pipefd[0]);
217 | }
218 | if (fprintf(p, "preset_passphrase %s -1 %s\n", keygrip, hextok) < 0) {
219 | die("failed to write to pipe: %m");
220 | }
221 | }
222 |
223 | if (autostart && (pid == 0)) {
224 | // We're configured to autostart, but did not encounter any keygrips.
225 | pid = connect_agent(autostart, pipefd[0]);
226 | }
227 |
228 | fclose(p);
229 |
230 | if (pid == 0) {
231 | // We're not autostarting and did not encounter any keygrips.
232 | exit(EXIT_SUCCESS);
233 | }
234 |
235 | int status;
236 | waitpid(pid, &status, 0);
237 | if (WIFEXITED(status)) {
238 | status = WEXITSTATUS(status);
239 | if (status == EXIT_SUCCESS) {
240 | exit(EXIT_SUCCESS);
241 | }
242 | die("child terminated with exit code %d", status);
243 | } else if (WIFSIGNALED(status)) {
244 | die("child killed by signal %d", WTERMSIG(status));
245 | } else {
246 | die("child returned unknown status code %d", status);
247 | }
248 | }
249 |
--------------------------------------------------------------------------------
/src/pam_gnupg.c:
--------------------------------------------------------------------------------
1 | #define _GNU_SOURCE
2 |
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include
13 |
14 | #define PAM_SM_AUTH
15 | #define PAM_SM_SESSION
16 |
17 | #include
18 | #include
19 | #include
20 |
21 | #include "config.h"
22 |
23 | #define TOKEN_DATA_NAME "pam-gnupg-token"
24 |
25 | // Copied from gnome-keyring
26 | void wipestr(char *data) {
27 | volatile char *vp;
28 | size_t len;
29 | if (!data) {
30 | return;
31 | }
32 | /* Defeats some optimizations */
33 | len = strlen(data);
34 | memset(data, 0xAA, len);
35 | memset(data, 0xBB, len);
36 | /* Defeats others */
37 | vp = (volatile char*) data;
38 | while (*vp) {
39 | *(vp++) = 0xAA;
40 | }
41 | free((void *) data);
42 | }
43 |
44 | void cleanup_token(pam_handle_t *pamh, void *data, int error_status) {
45 | wipestr(data);
46 | }
47 |
48 | bool preset_passphrase(pam_handle_t *pamh, const char *tok, bool autostart, bool send_env) {
49 | const char *user = NULL;
50 | if (pam_get_user(pamh, &user, NULL) != PAM_SUCCESS || user == NULL) {
51 | pam_syslog(pamh, LOG_ERR, "failed to get username");
52 | return false;
53 | }
54 |
55 | struct passwd *pwd = pam_modutil_getpwnam(pamh, user);
56 | if (pwd == NULL) {
57 | pam_syslog(pamh, LOG_ERR, "failed to get user info");
58 | return false;
59 | }
60 | uid_t uid = pwd->pw_uid;
61 | gid_t gid = pwd->pw_gid;
62 |
63 | int pipefd[2];
64 | if (pipe2(pipefd, O_CLOEXEC) < 0) {
65 | pam_syslog(pamh, LOG_ERR, "failed to open pipe: %m");
66 | return false;
67 | }
68 |
69 | // pam_getenvlist() allocates, so we can't call it after fork().
70 | char **env = NULL;
71 | if (send_env) {
72 | env = pam_getenvlist(pamh);
73 | if (env == NULL) {
74 | pam_syslog(pamh, LOG_ERR, "failed to read pam environment");
75 | return false;
76 | }
77 | }
78 |
79 | // Reset SIGCHLD handler so we can use waitpid(). If the calling process
80 | // used a handler to manage its own child processes, and one of the
81 | // children exits while we're busy, things will probably break, but there
82 | // does not appear to be a sane way of avoiding this.
83 | //
84 | // TODO Add a noreap option like pam_unix to selectively disable this for
85 | // services that are able to handle it.
86 | struct sigaction sa, saved_sigchld;
87 | sigemptyset(&sa.sa_mask);
88 | sa.sa_handler = SIG_DFL;
89 | sa.sa_flags = 0;
90 | sigaction(SIGCHLD, &sa, &saved_sigchld);
91 |
92 | bool ret = true;
93 |
94 | pid_t pid = fork();
95 | if (pid < 0) {
96 | pam_syslog(pamh, LOG_ERR, "failed to fork: %m");
97 | close(pipefd[0]);
98 | close(pipefd[1]);
99 | ret = false;
100 | }
101 |
102 | else if (pid == 0) {
103 | // TODO what about supplementary groups?
104 | if (setregid(gid, gid) < 0 || setreuid(uid, uid) < 0) {
105 | exit(errno);
106 | }
107 |
108 | // Unblock all signals. fork() clears pending signals in the child, so
109 | // this is safe.
110 | sigset_t emptyset;
111 | sigemptyset(&emptyset);
112 | sigprocmask(SIG_SETMASK, &emptyset, NULL);
113 |
114 | if (dup2(pipefd[0], STDIN_FILENO) < 0) {
115 | exit(errno);
116 | }
117 | int dev_null = open("/dev/null", O_WRONLY | O_CLOEXEC);
118 | if (dev_null != -1) {
119 | dup2(dev_null, STDOUT_FILENO);
120 | dup2(dev_null, STDERR_FILENO);
121 | }
122 |
123 | int maxfd = getdtablesize();
124 | for (int n = 3; n < maxfd; n++) {
125 | close(n);
126 | }
127 |
128 | char * cmd[] = {PAM_GNUPG_HELPER, "--autostart", NULL};
129 | if (!autostart) {
130 | cmd[1] = NULL;
131 | }
132 | if (send_env) {
133 | execve(cmd[0], cmd, env);
134 | } else {
135 | execv(cmd[0], cmd);
136 | }
137 | exit(errno);
138 | }
139 |
140 | else {
141 | if (pam_modutil_write(pipefd[1], tok, strlen(tok)) < 0) {
142 | pam_syslog(pamh, LOG_ERR, "failed to write to pipe: %m");
143 | ret = false;
144 | }
145 | // We close the read fd after writing in order to avoid SIGPIPE. Since
146 | // we write at most MAX_PASSPHRASE_LEN bytes, the pipe buffer won't
147 | // fill up and block us even if the child process dies.
148 | close(pipefd[0]);
149 | close(pipefd[1]);
150 | int status;
151 | while (waitpid(pid, &status, 0) < 0 && errno == EINTR)
152 | ;
153 | if (WIFEXITED(status)) {
154 | status = WEXITSTATUS(status);
155 | if (status != EXIT_SUCCESS) {
156 | pam_syslog(pamh, LOG_ERR, "helper terminated with exit code %d", status);
157 | ret = false;
158 | }
159 | } else if (WIFSIGNALED(status)) {
160 | pam_syslog(pamh, LOG_ERR, "helper killed by signal %d", WTERMSIG(status));
161 | ret = false;
162 | } else {
163 | pam_syslog(pamh, LOG_ERR, "helper returned unknown status code %d", status);
164 | ret = false;
165 | }
166 | }
167 |
168 | free(env);
169 | sigaction(SIGCHLD, &saved_sigchld, NULL);
170 | return ret;
171 | }
172 |
173 | int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) {
174 | const char *tok = NULL;
175 | bool debug = false;
176 | for (int i = 0; i < argc; i++) {
177 | if (strcmp(argv[i], "debug") == 0) {
178 | debug = true;
179 | }
180 | else if (strcmp(argv[i], "store-only") == 0) {
181 | // unused here
182 | }
183 | else {
184 | pam_syslog(pamh, LOG_ERR, "invalid option: %s", argv[i]);
185 | return PAM_IGNORE;
186 | }
187 | }
188 | if (pam_get_item(pamh, PAM_AUTHTOK, (const void **) &tok) != PAM_SUCCESS
189 | || tok == NULL
190 | ) {
191 | if (debug) pam_syslog(pamh, LOG_DEBUG, "failed to obtain passphrase");
192 | return PAM_AUTHINFO_UNAVAIL;
193 | }
194 | // Don't copy more bytes than gpg-agent is able to handle.
195 | tok = strndup(tok, MAX_PASSPHRASE_LEN);
196 | if (tok == NULL) {
197 | pam_syslog(pamh, LOG_ERR, "failed to copy passphrase");
198 | return PAM_SYSTEM_ERR;
199 | }
200 | if (pam_set_data(pamh, TOKEN_DATA_NAME, (void *) tok, cleanup_token) != PAM_SUCCESS) {
201 | pam_syslog(pamh, LOG_ERR, "failed to store passphrase");
202 | return PAM_IGNORE;
203 | }
204 | if (debug) pam_syslog(pamh, LOG_DEBUG, "stored passphrase");
205 | return PAM_SUCCESS;
206 | }
207 |
208 | int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) {
209 | const char *tok = NULL;
210 | bool debug = false;
211 | bool store_only = false;
212 | for (int i = 0; i < argc; i++) {
213 | if (strcmp(argv[i], "debug") == 0) {
214 | debug = true;
215 | }
216 | else if (strcmp(argv[i], "store-only") == 0) {
217 | store_only = true;
218 | }
219 | else {
220 | pam_syslog(pamh, LOG_ERR, "invalid option: %s", argv[i]);
221 | return PAM_IGNORE;
222 | }
223 | }
224 | if (store_only) {
225 | if (debug) pam_syslog(pamh, LOG_DEBUG, "store-only set, skipping");
226 | return PAM_SUCCESS;
227 | }
228 | if (flags & PAM_DELETE_CRED) {
229 | if (debug) pam_syslog(pamh, LOG_DEBUG, "PAM_DELETE_CRED set, skipping");
230 | return PAM_SUCCESS;
231 | }
232 | if (pam_get_data(pamh, TOKEN_DATA_NAME, (const void **) &tok) != PAM_SUCCESS || tok == NULL) {
233 | if (debug) pam_syslog(pamh, LOG_DEBUG, "unable to obtain stored passphrase");
234 | return PAM_IGNORE;
235 | }
236 | if (preset_passphrase(pamh, tok, false, false)) {
237 | if (debug) pam_syslog(pamh, LOG_DEBUG, "presetting succeeded, cleaning up");
238 | pam_set_data(pamh, TOKEN_DATA_NAME, NULL, NULL);
239 | return PAM_SUCCESS;
240 | } else {
241 | if (debug) pam_syslog(pamh, LOG_DEBUG, "presetting failed, retaining passphrase");
242 | return PAM_IGNORE;
243 | }
244 | }
245 |
246 | int pam_sm_open_session(pam_handle_t *pamh, int flags, int argc, const char **argv) {
247 | const char *tok = NULL;
248 | bool debug = false;
249 | bool autostart = true;
250 | for (int i = 0; i < argc; i++) {
251 | if (strcmp(argv[i], "debug") == 0) {
252 | debug = true;
253 | }
254 | else if (strcmp(argv[i], "no-autostart") == 0) {
255 | autostart = false;
256 | }
257 | else {
258 | pam_syslog(pamh, LOG_ERR, "invalid option: %s", argv[i]);
259 | return PAM_IGNORE;
260 | }
261 | }
262 | if (pam_get_data(pamh, TOKEN_DATA_NAME, (const void **) &tok) != PAM_SUCCESS || tok == NULL) {
263 | if (debug) pam_syslog(pamh, LOG_DEBUG, "unable to obtain stored passphrase");
264 | return PAM_SUCCESS; // this is not necessarily an error, so return PAM_SUCCESS here
265 | }
266 | if (preset_passphrase(pamh, tok, autostart, true)) {
267 | if (debug) pam_syslog(pamh, LOG_DEBUG, "presetting passphrase succeeded, cleaning up");
268 | pam_set_data(pamh, TOKEN_DATA_NAME, NULL, NULL);
269 | return PAM_SUCCESS;
270 | } else {
271 | if (debug) pam_syslog(pamh, LOG_DEBUG, "presetting passphrase failed, cleaning up");
272 | pam_set_data(pamh, TOKEN_DATA_NAME, NULL, NULL);
273 | return PAM_SESSION_ERR;
274 | }
275 | }
276 |
277 | int pam_sm_close_session(pam_handle_t *pamh, int flags, int argc, const char **argv) {
278 | return PAM_SUCCESS;
279 | }
280 |
--------------------------------------------------------------------------------