diff --git a/tools/gpg-auth.c b/tools/gpg-auth.c index eafe125bf..cc8cc3943 100644 --- a/tools/gpg-auth.c +++ b/tools/gpg-auth.c @@ -1,944 +1,962 @@ /* gpg-auth.c - Authenticate using GnuPG * Copyright (C) 2022 g10 Code GmbH * * This file is part of GnuPG. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This file is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . * SPDX-License-Identifier: GPL-3.0-or-later */ #include #include #include #include #define INCLUDED_BY_MAIN_MODULE 1 #include "../common/util.h" #include "../common/status.h" #include "../common/i18n.h" #include "../common/init.h" #include "../common/sysutils.h" #include "../common/asshelp.h" #include "../common/session-env.h" #include "../common/membuf.h" #include "../common/exechelp.h" /* We keep all global options in the structure OPT. */ struct { int interactive; int verbose; unsigned int debug; int quiet; int with_colons; const char *agent_program; int autostart; /* Options passed to the gpg-agent: */ char *lc_ctype; char *lc_messages; } opt; /* Debug values and macros. */ #define DBG_IPC_VALUE 1024 /* Debug assuan communication. */ #define DBG_EXTPROG_VALUE 16384 /* Debug external program calls */ #define DBG_IPC (opt.debug & DBG_IPC_VALUE) #define DBG_EXTPROG (opt.debug & DBG_EXTPROG_VALUE) /* Constants to identify the commands and options. */ enum opt_values { aNull = 0, oQuiet = 'q', oVerbose = 'v', oDebug = 500, oGpgProgram, oGpgsmProgram, oAgentProgram, oStatusFD, oWithColons, oNoAutostart, oLCctype, oLCmessages, oChUid, oDummy }; /* The list of commands and options. */ static gpgrt_opt_t opts[] = { ARGPARSE_group (301, ("@\nOptions:\n ")), ARGPARSE_s_n (oVerbose, "verbose", ("verbose")), ARGPARSE_s_n (oQuiet, "quiet", ("be somewhat more quiet")), ARGPARSE_s_s (oDebug, "debug", "@"), ARGPARSE_s_i (oStatusFD, "status-fd", N_("|FD|write status info to this FD")), ARGPARSE_s_n (oWithColons, "with-colons", "@"), ARGPARSE_s_n (oNoAutostart, "no-autostart", "@"), ARGPARSE_s_s (oAgentProgram, "agent-program", "@"), ARGPARSE_s_s (oLCctype, "lc-ctype", "@"), ARGPARSE_s_s (oLCmessages, "lc-messages","@"), ARGPARSE_end () }; /* The list of supported debug flags. */ static struct debug_flags_s debug_flags [] = { { DBG_IPC_VALUE , "ipc" }, { DBG_EXTPROG_VALUE, "extprog" }, { 0, NULL } }; /* Print usage information and provide strings for help. */ static const char * my_strusage( int level ) { const char *p; switch (level) { case 9: p = "GPL-3.0-or-later"; break; case 11: p = "gpg-auth"; break; case 12: p = "@GNUPG@"; break; case 13: p = VERSION; break; case 14: p = GNUPG_DEF_COPYRIGHT_LINE; break; case 17: p = PRINTABLE_OS_NAME; break; case 19: p = ("Please report bugs to <@EMAIL@>.\n"); break; case 1: case 40: p = ("Usage: gpg-auth" " [options] (-h for help)"); break; case 41: p = ("Syntax: gpg-auth" " [options] \n\n" "Tool to authenticate a user using a smartcard.\n" "Use command \"help\" to list all commands."); break; default: p = NULL; break; } return p; } /* Command line parsing. */ static void parse_arguments (gpgrt_argparse_t *pargs, gpgrt_opt_t *popts) { while (gpgrt_argparse (NULL, pargs, popts)) { switch (pargs->r_opt) { case oQuiet: opt.quiet = 1; break; case oVerbose: opt.verbose++; break; case oDebug: if (parse_debug_flag (pargs->r.ret_str, &opt.debug, debug_flags)) { pargs->r_opt = ARGPARSE_INVALID_ARG; pargs->err = ARGPARSE_PRINT_ERROR; } break; case oAgentProgram: opt.agent_program = pargs->r.ret_str; break; case oStatusFD: gnupg_set_status_fd (translate_sys2libc_fd_int (pargs->r.ret_int, 1)); break; case oWithColons: opt.with_colons = 1; break; case oNoAutostart: opt.autostart = 0; break; case oLCctype: opt.lc_ctype = pargs->r.ret_str; break; case oLCmessages: opt.lc_messages = pargs->r.ret_str; break; default: pargs->err = ARGPARSE_PRINT_ERROR; break; } } } struct ga_key_list { struct ga_key_list *next; char keygrip[41]; /* Keygrip to identify a key. */ size_t pubkey_len; char *pubkey; /* Public key in SSH format. */ + char *comment; }; /* Local prototypes. */ static gpg_error_t scd_passwd_reset (assuan_context_t ctx, const char *keygrip); static gpg_error_t ga_scd_connect (assuan_context_t *r_scd_ctx, int use_agent); static gpg_error_t ga_scd_get_auth_keys (assuan_context_t ctx, struct ga_key_list **r_key_list); static gpg_error_t ga_filter_by_authorized_keys (struct ga_key_list **r_key_list); static void ga_release_auth_keys (struct ga_key_list *key_list); static gpg_error_t scd_pkauth (assuan_context_t ctx, const char *keygrip); static gpg_error_t authenticate (assuan_context_t ctx, struct ga_key_list *key_list); -static int getpin (const char *info, char *buf, size_t *r_len); +static int getpin (assuan_context_t ctx, const char *info, char *buf, size_t *r_len); /* gpg-auth main. */ int main (int argc, char **argv) { gpg_error_t err; gpgrt_argparse_t pargs; assuan_context_t scd_ctx = NULL; struct ga_key_list *key_list = NULL; gnupg_reopen_std ("gpg-auth"); gpgrt_set_strusage (my_strusage); log_set_prefix ("gpg-auth", GPGRT_LOG_WITH_PREFIX); /* Make sure that our subsystems are ready. */ i18n_init(); init_common_subsystems (&argc, &argv); assuan_set_gpg_err_source (GPG_ERR_SOURCE_DEFAULT); setup_libassuan_logging (&opt.debug, NULL); /* Setup default options. */ opt.autostart = 1; /* Parse the command line. */ pargs.argc = &argc; pargs.argv = &argv; pargs.flags = ARGPARSE_FLAG_KEEP; parse_arguments (&pargs, opts); gpgrt_argparse (NULL, &pargs, NULL); /* Release internal state. */ if (log_get_errorcount (0)) exit (2); if (argc != 0) gpgrt_usage (1); /* Never returns. */ err = ga_scd_connect (&scd_ctx, 1); if (!err) err = ga_scd_get_auth_keys (scd_ctx, &key_list); if (!err) err = ga_filter_by_authorized_keys (&key_list); if (!err) err = authenticate (scd_ctx, key_list); ga_release_auth_keys (key_list); if (scd_ctx) assuan_release (scd_ctx); if (err) exit (1); return 0; } static gpg_error_t authenticate (assuan_context_t ctx, struct ga_key_list *key_list) { gpg_error_t err; while (key_list) { err = scd_passwd_reset (ctx, key_list->keygrip); if (err) return err; + assuan_set_pointer (ctx, key_list->comment); err = scd_pkauth (ctx, key_list->keygrip); if (!err) /* Success! */ return 0; key_list = key_list->next; } return gpg_error (GPG_ERR_NOT_FOUND); } static gpg_error_t get_serialno_cb (void *opaque, const char *line) { char **serialno = opaque; const char *keyword = line; const char *s; int keywordlen, n; for (keywordlen=0; *line && !spacep (line); line++, keywordlen++) ; while (spacep (line)) line++; if (keywordlen == 8 && !memcmp (keyword, "SERIALNO", keywordlen)) { if (*serialno) return gpg_error (GPG_ERR_CONFLICT); /* Unexpected status line. */ for (n=0,s=line; hexdigitp (s); s++, n++) ; if (!n || (n&1)|| !(spacep (s) || !*s) ) return gpg_error (GPG_ERR_ASS_PARAMETER); *serialno = xtrymalloc (n+1); if (!*serialno) return gpg_error_from_syserror (); memcpy (*serialno, line, n); (*serialno)[n] = 0; } return 0; } /* Helper function, which is used by scd_connect. Try to retrieve the SCDaemon's socket name from the gpg-agent context CTX. On success, *SOCKET_NAME is filled with a copy of the socket name. Return proper error code or zero on success. */ static gpg_error_t agent_scd_getinfo_socket_name (assuan_context_t ctx, char **socket_name) { membuf_t data; gpg_error_t err = 0; unsigned char *databuf; size_t datalen; init_membuf (&data, 256); *socket_name = NULL; err = assuan_transact (ctx, "SCD GETINFO socket_name", put_membuf_cb, &data, NULL, NULL, NULL, NULL); databuf = get_membuf (&data, &datalen); if (!err) { if (databuf && datalen) { char *res = xtrymalloc (datalen + 1); if (!res) err = gpg_error_from_syserror (); else { memcpy (res, databuf, datalen); res[datalen] = 0; *socket_name = res; } } } xfree (databuf); return err; } /* Callback parameter for learn card */ struct learn_parm_s { void (*kpinfo_cb)(void*, const char *); void *kpinfo_cb_arg; void (*certinfo_cb)(void*, const char *); void *certinfo_cb_arg; void (*sinfo_cb)(void*, const char *, size_t, const char *); void *sinfo_cb_arg; }; /* Connect to the agent and send the standard options. */ static gpg_error_t start_agent (assuan_context_t *ctx_p) { gpg_error_t err; session_env_t session_env; session_env = session_env_new (); if (!session_env) log_fatal ("error allocating session environment block: %s\n", strerror (errno)); err = start_new_gpg_agent (ctx_p, GPG_ERR_SOURCE_DEFAULT, opt.agent_program, NULL, NULL, session_env, opt.autostart, !opt.quiet, 0, NULL, NULL); session_env_release (session_env); return err; } static gpg_error_t scd_serialno (assuan_context_t ctx) { char *serialno = NULL; gpg_error_t err; err = assuan_transact (ctx, "SERIALNO", NULL, NULL, NULL, NULL, get_serialno_cb, &serialno); xfree (serialno); return err; } static gpg_error_t scd_passwd_reset (assuan_context_t ctx, const char *keygrip) { char line[ASSUAN_LINELENGTH]; gpg_error_t err; snprintf (line, DIM(line), "PASSWD --clear OPENPGP.2 %s", keygrip); err = assuan_transact (ctx, line, NULL, NULL, NULL, NULL, NULL, NULL); return err; } /* Connect to scdaemon by pipe or socket. Execute initial "SEREIALNO" command to enable all connected token under scdaemon control. */ static gpg_error_t ga_scd_connect (assuan_context_t *r_scd_ctx, int use_agent) { assuan_context_t assuan_ctx; gpg_error_t err; err = assuan_new (&assuan_ctx); if (err) return err; if (use_agent) /* Use scdaemon under gpg-agent. */ { char *scd_socket_name = NULL; assuan_context_t ctx; err = start_agent (&ctx); if (err) return err; /* Note that if gpg-agent is there but no scdaemon yet, * gpg-agent automatically invokes scdaemon by this query * itself. */ err = agent_scd_getinfo_socket_name (ctx, &scd_socket_name); assuan_release (ctx); if (!err) err = assuan_socket_connect (assuan_ctx, scd_socket_name, 0, 0); if (!err && DBG_IPC) log_debug ("got scdaemon socket name from gpg-agent, " "connected to socket '%s'", scd_socket_name); xfree (scd_socket_name); } else { const char *scd_path; const char *pgmname; const char *argv[3]; int no_close_list[2]; scd_path = gnupg_module_name (GNUPG_MODULE_NAME_SCDAEMON); if (!(pgmname = strrchr (scd_path, '/'))) pgmname = scd_path; else pgmname++; /* Fill argument vector for scdaemon. */ argv[0] = pgmname; argv[1] = "--server"; argv[2] = NULL; no_close_list[0] = assuan_fd_from_posix_fd (fileno (stderr)); no_close_list[1] = ASSUAN_INVALID_FD; /* Connect to the scdaemon */ err = assuan_pipe_connect (assuan_ctx, scd_path, argv, no_close_list, NULL, NULL, 0); if (err) { log_error ("could not spawn scdaemon: %s", gpg_strerror (err)); return err; } if (DBG_IPC) log_debug ("spawned a new scdaemon (path: '%s')", scd_path); } if (err) assuan_release (assuan_ctx); else { scd_serialno (assuan_ctx); *r_scd_ctx = assuan_ctx; } return err; } /* Handle the NEEDPIN inquiry. */ static gpg_error_t inq_needpin (void *opaque, const char *line) { assuan_context_t ctx = opaque; const char *s; char *pin; size_t pinlen; int rc; rc = 0; if ((s = has_leading_keyword (line, "NEEDPIN"))) { line = s; pinlen = 90; pin = gcry_malloc_secure (pinlen); if (!pin) return out_of_core (); - rc = getpin (line, pin, &pinlen); + rc = getpin (ctx, line, pin, &pinlen); if (!rc) { assuan_begin_confidential (ctx); rc = assuan_send_data (ctx, pin, pinlen); assuan_end_confidential (ctx); } wipememory (pin, pinlen); xfree (pin); } else if ((s = has_leading_keyword (line, "POPUPPINPADPROMPT"))) { /* i 18 */; /* Please use PINPAD! */; } else if ((s = has_leading_keyword (line, "DISMISSPINPADPROMPT"))) { ; } else { log_error ("unsupported inquiry '%s'\n", line); rc = gpg_error (GPG_ERR_ASS_UNKNOWN_INQUIRE); } return gpg_error (rc); } struct card_keyinfo_parm_s { int error; struct ga_key_list *list; }; /* Callback function for scd_keyinfo_list. */ static gpg_error_t card_keyinfo_cb (void *opaque, const char *line) { gpg_error_t err = 0; struct card_keyinfo_parm_s *parm = opaque; const char *keyword = line; int keywordlen; struct ga_key_list *keyinfo = NULL; for (keywordlen=0; *line && !spacep (line); line++, keywordlen++) ; while (spacep (line)) line++; if (keywordlen == 7 && !memcmp (keyword, "KEYINFO", keywordlen)) { const char *s; int n; struct ga_key_list **l_p = &parm->list; /* It's going to append the information at the end. */ while ((*l_p)) l_p = &(*l_p)->next; keyinfo = xtrycalloc (1, sizeof *keyinfo); if (!keyinfo) goto alloc_error; for (n=0,s=line; hexdigitp (s); s++, n++) ; if (n != 40) goto parm_error; memcpy (keyinfo->keygrip, line, 40); keyinfo->keygrip[40] = 0; line = s; if (!*line) goto parm_error; while (spacep (line)) line++; if (*line++ != 'T') goto parm_error; if (!*line) goto parm_error; while (spacep (line)) line++; for (n=0,s=line; hexdigitp (s); s++, n++) ; if (!n) goto skip; skip: *l_p = keyinfo; } return err; alloc_error: xfree (keyinfo); if (!parm->error) parm->error = gpg_error_from_syserror (); return 0; parm_error: xfree (keyinfo); if (!parm->error) parm->error = gpg_error (GPG_ERR_ASS_PARAMETER); return 0; } /* Call the scdaemon to retrieve list of available keys on cards. On success, the allocated structure is stored at R_KEY_LIST. On error, an error code is returned and NULL is stored at R_KEY_LIST. */ static gpg_error_t scd_keyinfo_list (assuan_context_t ctx, struct ga_key_list **r_key_list) { int err; struct card_keyinfo_parm_s parm; memset (&parm, 0, sizeof parm); err = assuan_transact (ctx, "KEYINFO --list=auth", NULL, NULL, NULL, NULL, card_keyinfo_cb, &parm); if (!err && parm.error) err = parm.error; if (!err) *r_key_list = parm.list; else ga_release_auth_keys (parm.list); return err; } /* A variant of put_membuf_cb, which only put the second field. */ static gpg_error_t put_second_field_cb (void *opaque, const void *buf, size_t len) { char line[ASSUAN_LINELENGTH]; membuf_t *data = opaque; if (buf && len < ASSUAN_LINELENGTH) { const char *fields[3]; size_t field_len; memcpy (line, buf, len); if (split_fields (line, fields, DIM (fields)) < 2) return 0; field_len = strlen (fields[1]); put_membuf (data, fields[1], field_len); } return 0; } static gpg_error_t scd_get_pubkey (assuan_context_t ctx, struct ga_key_list *key) { char line[ASSUAN_LINELENGTH]; membuf_t data; unsigned char *databuf; size_t datalen; gpg_error_t err = 0; init_membuf (&data, 256); snprintf (line, DIM(line), "READKEY --format=ssh %s", key->keygrip); err = assuan_transact (ctx, line, put_second_field_cb, &data, NULL, NULL, NULL, NULL); databuf = get_membuf (&data, &datalen); if (!err) { key->pubkey_len = datalen; key->pubkey = databuf; } else xfree (databuf); return err; } static gpg_error_t ga_scd_get_auth_keys (assuan_context_t ctx, struct ga_key_list **r_key_list) { gpg_error_t err; struct ga_key_list *kl, *key_list = NULL; /* Get list of auth keys with their keygrips. */ err = scd_keyinfo_list (ctx, &key_list); /* And retrieve public key for each key. */ kl = key_list; while (kl) { err = scd_get_pubkey (ctx, kl); if (err) break; kl = kl->next; } if (err) ga_release_auth_keys (key_list); else *r_key_list = key_list; return err; } struct ssh_key_list { struct ssh_key_list *next; char *pubkey; /* Public key in SSH format. */ + char *comment; }; static void release_ssh_key_list (struct ssh_key_list *key_list) { struct ssh_key_list *key; while (key_list) { key = key_list; key_list = key_list->next; xfree (key->pubkey); + xfree (key->comment); xfree (key); } } static gpg_error_t ssh_authorized_keys (struct ssh_key_list **r_ssh_key_list) { gpg_error_t err = 0; char *fname = NULL; estream_t fp = NULL; char *line = NULL; size_t length_of_line = 0; size_t maxlen; ssize_t len; const char *fields[3]; struct ssh_key_list *ssh_key_list = NULL; struct ssh_key_list *ssh_key_prev = NULL; struct ssh_key_list *ssh_key = NULL; fname = make_absfilename_try ("~", ".ssh", "authorized_keys", NULL); if (fname == NULL) return gpg_error (GPG_ERR_INV_NAME); fp = es_fopen (fname, "r"); if (!fp) { err = gpg_error_from_syserror (); xfree (fname); return err; } xfree (fname); maxlen = 2048; /* Set limit. */ while ((len = es_read_line (fp, &line, &length_of_line, &maxlen)) > 0) { if (!maxlen) { err = gpg_error (GPG_ERR_LINE_TOO_LONG); log_error (_("error reading '%s': %s\n"), fname, gpg_strerror (err)); goto leave; } /* Strip newline and carriage return, if present. */ while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) line[--len] = '\0'; + fields[2] = NULL; if (split_fields (line, fields, DIM (fields)) < 2) continue; /* Skip empty lines or line with only a field. */ if (*fields[0] == '#') continue; /* Skip comments. */ ssh_key = xtrycalloc (1, sizeof *ssh_key); if (!ssh_key) { err = gpg_error_from_syserror (); release_ssh_key_list (ssh_key_list); goto leave; } ssh_key->pubkey = strdup (fields[1]); + ssh_key->comment = strdup (fields[2]); if (ssh_key_list) ssh_key_prev->next = ssh_key; else ssh_key_list = ssh_key; ssh_key_prev = ssh_key; } *r_ssh_key_list = ssh_key_list; leave: xfree (line); es_fclose (fp); return err; } static gpg_error_t ga_filter_by_authorized_keys (struct ga_key_list **r_key_list) { gpg_error_t err; struct ga_key_list *cur = *r_key_list; struct ga_key_list *key_list = NULL; struct ga_key_list *prev = NULL; struct ssh_key_list *ssh_key_list = NULL; err = ssh_authorized_keys (&ssh_key_list); if (err) return err; if (ssh_key_list == NULL) return gpg_error (GPG_ERR_NOT_FOUND); while (cur) { struct ssh_key_list *skl = ssh_key_list; while (skl) if (!strncmp (cur->pubkey, skl->pubkey, cur->pubkey_len)) break; else skl = skl->next; /* valid? */ if (skl) { if (key_list) prev->next = cur; else key_list = cur; + cur->comment = skl->comment; + skl->comment = NULL; prev = cur; cur = cur->next; } else { struct ga_key_list *k = cur; cur = cur->next; xfree (k->pubkey); xfree (k); } } if (prev && prev->next) prev->next = NULL; release_ssh_key_list (ssh_key_list); *r_key_list = key_list; return 0; } static void ga_release_auth_keys (struct ga_key_list *key_list) { struct ga_key_list *key; while (key_list) { key = key_list; key_list = key_list->next; xfree (key->pubkey); xfree (key); } } static int -getpin (const char *info, char *buf, size_t *r_len) +getpin (assuan_context_t ctx, const char *info, char *buf, size_t *r_len) { int rc = 0; char line[ASSUAN_LINELENGTH]; const char *fields[2]; + const char *comment; (void)info; + comment = assuan_get_pointer (ctx); + if (comment) + { + int msg_len = 5 + strlen (comment); + fprintf (stdout, "i %d\n", msg_len); + fprintf (stdout, "KEY: %s\n", comment); + fflush (stdout); + } + fputs ("P 18\n", stdout); fputs ("Please input PIN: \n", stdout); fflush (stdout); fgets (line, ASSUAN_LINELENGTH, stdin); if (split_fields (line, fields, DIM (fields)) < DIM (fields)) rc = GPG_ERR_PROTOCOL_VIOLATION; else if (strcmp (fields[0], "p") != 0) rc = GPG_ERR_CANCELED; if (!fgets (line, ASSUAN_LINELENGTH, stdin)) rc = GPG_ERR_PROTOCOL_VIOLATION; if (!rc) { size_t len = strlen (line); /* Strip newline and carriage return, if present. */ while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) line[--len] = '\0'; len++; /* Include last '\0' in the data. */ if (len > *r_len) rc = GPG_ERR_BUFFER_TOO_SHORT; else memcpy (buf, line, len); *r_len = len; } return rc; } static gpg_error_t scd_pkauth (assuan_context_t ctx, const char *keygrip) { char line[ASSUAN_LINELENGTH]; gpg_error_t err; snprintf (line, DIM(line), "PKAUTH --challenge-response %s", keygrip); err = assuan_transact (ctx, line, NULL, NULL, inq_needpin, ctx, NULL, NULL); return err; }