diff --git a/common/util.h b/common/util.h index a4b1cbd79..863f9e36f 100644 --- a/common/util.h +++ b/common/util.h @@ -262,6 +262,13 @@ void gnupg_module_name_flush_some (void); void gnupg_set_builddir (const char *newdir); +/* A list of constants to identify protocols. This is used by tools + * which need to distinguish between the different protocols + * implemented by GnuPG. May be used as bit flags. */ +#define GNUPG_PROTOCOL_OPENPGP 1 /* The one and only (gpg). */ +#define GNUPG_PROTOCOL_CMS 2 /* The core of S/MIME (gpgsm) */ +#define GNUPG_PROTOCOL_SSH_AGENT 4 /* Out ssh-agent implementation */ + /*-- gpgrlhelp.c --*/ void gnupg_rl_initialize (void); diff --git a/tools/Makefile.am b/tools/Makefile.am index f74221b2d..ad0f223b4 100644 --- a/tools/Makefile.am +++ b/tools/Makefile.am @@ -128,6 +128,7 @@ gpg_card_tool_SOURCES = \ gpg-card-tool.c \ card-tool.h \ card-call-scd.c \ + card-tool-keys.c \ card-tool-misc.c gpg_card_tool_LDADD = ../common/libgpgrl.a $(common_libs) \ diff --git a/tools/card-tool-keys.c b/tools/card-tool-keys.c new file mode 100644 index 000000000..af2425cbf --- /dev/null +++ b/tools/card-tool-keys.c @@ -0,0 +1,467 @@ +/* card-tool-keys.c - OpenPGP and CMS related functions for gpg-card-tool + * Copyright (C) 2019 g10 Code GmbH + * + * This file is part of GnuPG. + * + * GnuPG 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. + * + * GnuPG 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 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 + +#include "../common/util.h" +#include "../common/i18n.h" +#include "../common/ccparray.h" +#include "../common/exectool.h" +#include "../common/openpgpdefs.h" +#include "card-tool.h" + +/* Release a keyblocm object. */ +void +release_keyblock (keyblock_t keyblock) +{ + pubkey_t pubkey; + userid_t uid; + + while (keyblock) + { + keyblock_t keyblocknext = keyblock->next; + pubkey = keyblock->keys; + while (pubkey) + { + pubkey_t pubkeynext = pubkey->next; + xfree (pubkey); + pubkey = pubkeynext; + } + uid = keyblock->uids; + while (uid) + { + userid_t uidnext = uid->next; + xfree (uid->value); + xfree (uid); + uid = uidnext; + } + xfree (keyblock); + keyblock = keyblocknext; + } +} + + + +/* Object to communicate with the status_cb. */ +struct status_cb_s +{ + const char *pgm; /* Name of the program for debug purposes. */ + int no_pubkey; /* Result flag. */ +}; + + +/* Status callback helper for the exec functions. */ +static void +status_cb (void *opaque, const char *keyword, char *args) +{ + struct status_cb_s *c = opaque; + const char *s; + + if (DBG_EXTPROG) + log_debug ("%s: status: %s %s\n", c->pgm, keyword, args); + + if (!strcmp (keyword, "ERROR") + && (s=has_leading_keyword (args, "keylist.getkey")) + && gpg_err_code (atoi (s)) == GPG_ERR_NO_PUBKEY) + { + /* No public key was found. gpg terminates with an error in + * this case and we can't change that behaviour. Instead we + * detect this status and carry that error forward. */ + c->no_pubkey = 1; + } + +} + + +/* Helper for get_matching_keys to parse "pub" style records. */ +static gpg_error_t +parse_key_record (char **fields, int nfields, pubkey_t *r_pubkey) +{ + pubkey_t pubkey; + + pubkey = xtrycalloc (1, sizeof *pubkey); + if (!pubkey) + return gpg_error_from_syserror (); + *r_pubkey = pubkey; + return 0; +} + + +/* Run gpg or gpgsm to get a list of all keys matching the 20 byte + * KEYGRIP. PROTOCOL is one of or a combination of + * GNUPG_PROTOCOL_OPENPGP and GNUPG_PROTOCOL_CMS. On success a new + * keyblock is stored at R_KEYBLOCK; on error NULL is stored there. */ +gpg_error_t +get_matching_keys (const unsigned char *keygrip, int protocol, + keyblock_t *r_keyblock) +{ + gpg_error_t err; + ccparray_t ccp; + const char **argv; + estream_t listing; + char hexgrip[1 + (2*KEYGRIP_LEN) + 1]; + char *line = NULL; + size_t length_of_line = 0; + size_t maxlen; + ssize_t len; + char **fields = NULL; + int nfields; + int first_seen; + keyblock_t keyblock_head, *keyblock_tail, kb; + pubkey_t pubkey, pk; + size_t n; + struct status_cb_s status_cb_parm; + + *r_keyblock = NULL; + + keyblock_head = NULL; + keyblock_tail = &keyblock_head; + kb = NULL; + + /* Shortcut to run a listing on both protocols. */ + if ((protocol & GNUPG_PROTOCOL_OPENPGP) && (protocol & GNUPG_PROTOCOL_CMS)) + { + err = get_matching_keys (keygrip, GNUPG_PROTOCOL_OPENPGP, &kb); + if (!err || gpg_err_code (err) == GPG_ERR_NO_PUBKEY) + { + *keyblock_tail = kb; + keyblock_tail = &kb->next; + kb = NULL; + err = get_matching_keys (keygrip, GNUPG_PROTOCOL_CMS, &kb); + if (!err) + { + *keyblock_tail = kb; + keyblock_tail = &kb->next; + kb = NULL; + } + else if (gpg_err_code (err) == GPG_ERR_NO_PUBKEY) + err = 0; + } + if (err) + release_keyblock (keyblock_head); + else + *r_keyblock = keyblock_head; + return err; + } + + /* Check that we have only one protocol. */ + if (protocol != GNUPG_PROTOCOL_OPENPGP && protocol != GNUPG_PROTOCOL_CMS) + return gpg_error (GPG_ERR_UNSUPPORTED_PROTOCOL); + + /* Open a memory stream. */ + listing = es_fopenmem (0, "w+b"); + if (!listing) + { + err = gpg_error_from_syserror (); + log_error ("error allocating memory buffer: %s\n", gpg_strerror (err)); + return err; + } + + status_cb_parm.pgm = protocol == GNUPG_PROTOCOL_OPENPGP? "gpg":"gpgsm"; + status_cb_parm.no_pubkey = 0; + + hexgrip[0] = '&'; + bin2hex (keygrip, KEYGRIP_LEN, hexgrip+1); + + ccparray_init (&ccp, 0); + + if (opt.verbose > 1 || DBG_EXTPROG) + ccparray_put (&ccp, "--verbose"); + else + ccparray_put (&ccp, "--quiet"); + ccparray_put (&ccp, "--no-options"); + ccparray_put (&ccp, "--batch"); + ccparray_put (&ccp, "--status-fd=2"); + ccparray_put (&ccp, "--with-colons"); + ccparray_put (&ccp, "--with-keygrip"); + ccparray_put (&ccp, "--list-keys"); + ccparray_put (&ccp, hexgrip); + + ccparray_put (&ccp, NULL); + argv = ccparray_get (&ccp, NULL); + if (!argv) + { + err = gpg_error_from_syserror (); + goto leave; + } + err = gnupg_exec_tool_stream (protocol == GNUPG_PROTOCOL_OPENPGP? + opt.gpg_program : opt.gpgsm_program, + argv, NULL, NULL, listing, status_cb, + &status_cb_parm); + if (err) + { + if (status_cb_parm.no_pubkey) + err = gpg_error (GPG_ERR_NO_PUBKEY); + else if (gpg_err_code (err) != GPG_ERR_GENERAL) + log_error ("key listing failed: %s\n", gpg_strerror (err)); + goto leave; + } + + es_rewind (listing); + first_seen = 0; + maxlen = 8192; /* Set limit large enough for all escaped UIDs. */ + while ((len = es_read_line (listing, &line, &length_of_line, &maxlen)) > 0) + { + if (!maxlen) + { + log_error ("received line too long\n"); + err = gpg_error (GPG_ERR_LINE_TOO_LONG); + goto leave; + } + /* Strip newline and carriage return, if present. */ + while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) + line[--len] = '\0'; + + xfree (fields); + fields = strtokenize (line, ":"); + if (!fields) + { + err = gpg_error_from_syserror (); + log_error ("strtokenize failed: %s\n", gpg_strerror (err)); + goto leave; + } + for (nfields = 0; fields[nfields]; nfields++) + ; + if (!nfields) + { + err = gpg_error (GPG_ERR_INV_ENGINE); + goto leave; + } + + /* Skip over all records until we reach a pub or sec. */ + if (!first_seen + && (!strcmp (fields[0], "pub") || !strcmp (fields[0], "sec") + || !strcmp (fields[0], "crt") || !strcmp (fields[0], "crs"))) + first_seen = 1; + if (!first_seen) + continue; + + if (!strcmp (fields[0], "pub") || !strcmp (fields[0], "sec") + || !strcmp (fields[0], "crt") || !strcmp (fields[0], "crs")) + { + if (kb) /* Finish the current keyblock. */ + { + *keyblock_tail = kb; + keyblock_tail = &kb->next; + } + kb = xtrycalloc (1, sizeof *kb); + if (!kb) + { + err = gpg_error_from_syserror (); + goto leave; + } + kb->protocol = protocol; + err = parse_key_record (fields, nfields, &pubkey); + if (err) + goto leave; + kb->keys = pubkey; + pubkey = NULL; + } + else if (!strcmp (fields[0], "sub") || !strcmp (fields[0], "ssb")) + { + log_assert (kb && kb->keys); + err = parse_key_record (fields, nfields, &pubkey); + if (err) + goto leave; + for (pk = kb->keys; pk->next; pk = pk->next) + ; + pk->next = pubkey; + pubkey = NULL; + } + else if (!strcmp (fields[0], "fpr") && nfields > 9) + { + log_assert (kb && kb->keys); + n = strlen (fields[9]); + if (n != 64 && n != 40 && n != 32) + { + log_debug ("bad length (%zu) in fpr record\n", n); + err = gpg_error (GPG_ERR_INV_ENGINE); + goto leave; + } + n /= 2; + + for (pk = kb->keys; pk->next; pk = pk->next) + ; + if (pk->fprlen) + { + log_debug ("too many fpr records\n"); + err = gpg_error (GPG_ERR_INV_ENGINE); + goto leave; + } + log_assert (n <= sizeof pk->fpr); + pk->fprlen = n; + if (hex2bin (fields[9], pk->fpr, n) < 0) + { + log_debug ("bad chars in fpr record\n"); + err = gpg_error (GPG_ERR_INV_ENGINE); + goto leave; + } + } + else if (!strcmp (fields[0], "grp") && nfields > 9) + { + log_assert (kb && kb->keys); + n = strlen (fields[9]); + if (n != 2*KEYGRIP_LEN) + { + log_debug ("bad length (%zu) in grp record\n", n); + err = gpg_error (GPG_ERR_INV_ENGINE); + goto leave; + } + n /= 2; + + for (pk = kb->keys; pk->next; pk = pk->next) + ; + if (pk->grip_valid) + { + log_debug ("too many grp records\n"); + err = gpg_error (GPG_ERR_INV_ENGINE); + goto leave; + } + if (hex2bin (fields[9], pk->grip, KEYGRIP_LEN) < 0) + { + log_debug ("bad chars in fpr record\n"); + err = gpg_error (GPG_ERR_INV_ENGINE); + goto leave; + } + pk->grip_valid = 1; + if (!memcmp (pk->grip, keygrip, KEYGRIP_LEN)) + pk->requested = 1; + } + else if (!strcmp (fields[0], "uid") && nfields > 9) + { + userid_t uid, u; + + uid = xtrycalloc (1, sizeof *uid); + if (!uid) + { + err = gpg_error_from_syserror (); + goto leave; + } + uid->value = decode_c_string (fields[9]); + if (!uid->value) + { + err = gpg_error_from_syserror (); + xfree (uid); + goto leave; + } + if (!kb->uids) + kb->uids = uid; + else + { + for (u = kb->uids; u->next; u = u->next) + ; + u->next = uid; + } + } + } + if (len < 0 || es_ferror (listing)) + { + err = gpg_error_from_syserror (); + log_error ("error reading memory stream\n"); + goto leave; + } + + if (kb) /* Finish the current keyblock. */ + { + *keyblock_tail = kb; + keyblock_tail = &kb->next; + kb = NULL; + } + + if (!keyblock_head) + err = gpg_error (GPG_ERR_NO_PUBKEY); + + leave: + if (err) + release_keyblock (keyblock_head); + else + *r_keyblock = keyblock_head; + xfree (kb); + xfree (fields); + es_free (line); + xfree (argv); + es_fclose (listing); + return err; +} + + +void +dump_keyblock (keyblock_t keyblock) +{ + keyblock_t kb; + pubkey_t pubkey; + userid_t uid; + + for (kb = keyblock; kb; kb = kb->next) + { + log_info ("%s key:\n", + kb->protocol == GNUPG_PROTOCOL_OPENPGP? "OpenPGP":"X.509"); + for (pubkey = kb->keys; pubkey; pubkey = pubkey->next) + { + log_info (" grip: "); + if (pubkey->grip_valid) + log_printhex (pubkey->grip, KEYGRIP_LEN, NULL); + log_printf ("%s\n", pubkey->requested? " (*)":""); + + log_info (" fpr: "); + log_printhex (pubkey->fpr, pubkey->fprlen, ""); + } + for (uid = kb->uids; uid; uid = uid->next) + { + log_info (" uid: %s\n", uid->value); + } + } +} + + + +gpg_error_t +test_get_matching_keys (const char *hexgrip) +{ + gpg_error_t err; + unsigned char grip[KEYGRIP_LEN]; + keyblock_t keyblock; + + if (strlen (hexgrip) != 40) + { + log_error ("error: invalid keygrip\n"); + return 0; + } + if (hex2bin (hexgrip, grip, sizeof grip) < 0) + { + log_error ("error: bad kegrip\n"); + return 0; + } + err = get_matching_keys (grip, + (GNUPG_PROTOCOL_OPENPGP | GNUPG_PROTOCOL_CMS), + &keyblock); + if (err) + { + log_error ("get_matching_keys failed: %s\n", gpg_strerror (err)); + return err; + } + + dump_keyblock (keyblock); + release_keyblock (keyblock); + return 0; +} diff --git a/tools/card-tool.h b/tools/card-tool.h index b1d866228..d502ecb58 100644 --- a/tools/card-tool.h +++ b/tools/card-tool.h @@ -50,6 +50,41 @@ struct #define DBG_IPC (opt.debug & DBG_IPC_VALUE) #define DBG_EXTPROG (opt.debug & DBG_EXTPROG_VALUE) +/* The maximum length of a binary fingerprint. */ +#define MAX_FINGERPRINT_LEN 32 + + +/* + * Data structures to store keyblocks (aka certificates). + */ +struct pubkey_s +{ + struct pubkey_s *next; /* The next key. */ + unsigned char grip[KEYGRIP_LEN]; + unsigned char fpr[MAX_FINGERPRINT_LEN]; + unsigned char fprlen; /* The used length of a FPR. */ + unsigned int grip_valid:1;/* The grip is valid. */ + unsigned int requested: 1;/* This is the requested grip. */ +}; +typedef struct pubkey_s *pubkey_t; + +struct userid_s +{ + struct userid_s *next; + char *value; /* Malloced. */ +}; +typedef struct userid_s *userid_t; + +struct keyblock_s +{ + struct keyblock_s *next; /* Allow to link several keyblocks. */ + int protocol; /* GPGME_PROTOCOL_OPENPGP or _CMS. */ + pubkey_t keys; /* The key. For OpenPGP primary + list of subkeys. */ + userid_t uids; /* The list of user ids. */ +}; +typedef struct keyblock_s *keyblock_t; + + /* Enumeration of the known card application types. */ typedef enum @@ -76,9 +111,9 @@ struct key_attr }; }; -/* An object to store information pertaining to a key pair. This is - * commonly used as a linked list with all keys known for the current - * card. */ +/* An object to store information pertaining to a key pair as stored + * on a card. This is commonly used as a linked list with all keys + * known for the current card. */ struct key_info_s { struct key_info_s *next; @@ -144,6 +179,13 @@ struct card_info_s typedef struct card_info_s *card_info_t; +/*-- card-tool-keys.c --*/ +void release_keyblock (keyblock_t keyblock); +gpg_error_t get_matching_keys (const unsigned char *keygrip, int protocol, + keyblock_t *r_keyblock); +gpg_error_t test_get_matching_keys (const char *hexgrip); + + /*-- card-tool-misc.c --*/ key_info_t find_kinfo (card_info_t info, const char *keyref); diff --git a/tools/gpg-card-tool.c b/tools/gpg-card-tool.c index 4f7962060..321426bdd 100644 --- a/tools/gpg-card-tool.c +++ b/tools/gpg-card-tool.c @@ -69,6 +69,9 @@ enum cmd_and_opt_values oLCctype, oLCmessages, + aTest, + + oDummy }; @@ -76,6 +79,7 @@ enum cmd_and_opt_values /* The list of commands and options. */ static ARGPARSE_OPTS opts[] = { ARGPARSE_group (300, ("@Commands:\n ")), + ARGPARSE_c (aTest, "test", "test command"), ARGPARSE_group (301, ("@\nOptions:\n ")), @@ -227,6 +231,10 @@ parse_arguments (ARGPARSE_ARGS *pargs, ARGPARSE_OPTS *popts) case oLCctype: opt.lc_ctype = pargs->r.ret_str; break; case oLCmessages: opt.lc_messages = pargs->r.ret_str; break; + case aTest: + cmd = pargs->r_opt; + break; + default: pargs->err = 2; break; } } @@ -292,6 +300,12 @@ main (int argc, char **argv) /* Run the selected command. */ switch (cmd) { + case aTest: + if (!argc) + wrong_args ("--test KEYGRIP"); + err = test_get_matching_keys (*argv); + break; + default: interactive_loop (); err = 0; @@ -580,15 +594,25 @@ mem_is_ff (const char *mem, unsigned int memlen) /* Helper to list a single keyref. */ static void -list_one_kinfo (key_info_t kinfo, estream_t fp) +list_one_kinfo (key_info_t firstkinfo, key_info_t kinfo, estream_t fp) { - if (kinfo) + gpg_error_t err; + keyblock_t keyblock = NULL; + keyblock_t kb; + pubkey_t pubkey; + userid_t uid; + key_info_t ki; + const char *s; + + if (firstkinfo && kinfo) { tty_fprintf (fp, " "); if (mem_is_zero (kinfo->grip, sizeof kinfo->grip)) - tty_fprintf (fp, "[none]\n"); - else - print_keygrip (fp, kinfo->grip); + { + tty_fprintf (fp, "[none]\n"); + goto leave; + } + print_keygrip (fp, kinfo->grip); if (kinfo->fprlen && kinfo->created) { @@ -597,9 +621,63 @@ list_one_kinfo (key_info_t kinfo, estream_t fp) tty_fprintf (fp, " created ....: %s\n", isotimestamp (kinfo->created)); } + err = get_matching_keys (kinfo->grip, + (GNUPG_PROTOCOL_OPENPGP | GNUPG_PROTOCOL_CMS), + &keyblock); + if (err) + { + if (gpg_err_code (err) != GPG_ERR_NO_PUBKEY) + tty_fprintf (fp, " error ......: %s\n", gpg_strerror (err)); + goto leave; + } + for (kb = keyblock; kb; kb = kb->next) + { + tty_fprintf (fp, " used for ...: %s\n", + kb->protocol == GNUPG_PROTOCOL_OPENPGP? "OpenPGP" : + kb->protocol == GNUPG_PROTOCOL_CMS? "X.509" : "?"); + pubkey = kb->keys; + /* If this is not the primary key print the primary key's + * fingerprint or a reference to it. */ + if (kb->protocol == GNUPG_PROTOCOL_OPENPGP) + { + tty_fprintf (fp, " main key .:"); + for (ki=firstkinfo; ki; ki = ki->next) + if (pubkey->grip_valid + && !memcmp (ki->grip, pubkey->grip, KEYGRIP_LEN)) + break; + if (ki) + { + /* Fixme: Replace mapping by a table lookup. */ + if (!memcmp (kinfo->grip, pubkey->grip, KEYGRIP_LEN)) + s = "this"; + else if (!strcmp (ki->keyref, "OPENPGP.1")) + s = "Signature key"; + else if (!strcmp (ki->keyref, "OPENPGP.2")) + s = "Encryption key"; + else if (!strcmp (ki->keyref, "OPENPGP.3")) + s = "Authentication key"; + else + s = NULL; + if (s) + tty_fprintf (fp, " <%s>\n", s); + else + tty_fprintf (fp, " \n", ki->keyref); + } + else + print_shax_fpr (fp, pubkey->fpr, pubkey->fprlen); + } + for (uid = kb->uids; uid; uid = uid->next) + { + print_string (fp, " user id ..: ", uid->value); + } + + } } else tty_fprintf (fp, " [none]\n"); + + leave: + release_keyblock (keyblock); } @@ -620,7 +698,7 @@ list_all_kinfo (card_info_t info, keyinfolabel_t labels, estream_t fp) { tty_fprintf (fp, "%s", labels[idx].label); kinfo = find_kinfo (info, labels[idx].keyref); - list_one_kinfo (kinfo, fp); + list_one_kinfo (info->kinfo, kinfo, fp); if (kinfo) kinfo->xflag = 1; } @@ -633,7 +711,7 @@ list_all_kinfo (card_info_t info, keyinfolabel_t labels, estream_t fp) for (i=5+strlen (kinfo->keyref); i < 18; i++) tty_fprintf (fp, "."); tty_fprintf (fp, ":"); - list_one_kinfo (kinfo, fp); + list_one_kinfo (info->kinfo, kinfo, fp); } }