gnupg/tools/card-keys.c

653 lines
17 KiB
C
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* card-keys.c - OpenPGP and CMS related functions for gpg-card
* 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 <https://www.gnu.org/licenses/>.
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include <config.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../common/util.h"
#include "../common/i18n.h"
#include "../common/ccparray.h"
#include "../common/exectool.h"
#include "../common/openpgpdefs.h"
#include "gpg-card.h"
/* It is quite common that all keys of an OpenPGP card belong to the
* the same OpenPGP keyblock. To avoid running several queries
* despite that we already got the information with the previous
* keyblock, we keep a small cache of of previous done queries. */
static struct
{
unsigned int lru;
keyblock_t keyblock;
} keyblock_cache[5];
/* Helper for release_keyblock. */
static void
do_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;
}
}
/* Release a keyblock object. */
void
release_keyblock (keyblock_t keyblock)
{
static unsigned int lru_counter;
unsigned int lru;
int i, lru_idx;
if (!keyblock)
return;
lru = (unsigned int)(-1);
lru_idx = 0;
for (i=0; i < DIM (keyblock_cache); i++)
{
if (!keyblock_cache[i].keyblock)
{
keyblock_cache[i].keyblock = keyblock;
keyblock_cache[i].lru = ++lru_counter;
goto leave;
}
if (keyblock_cache[i].lru < lru)
{
lru = keyblock_cache[i].lru;
lru_idx = i;
}
}
/* No free slot. Replace one. */
do_release_keyblock (keyblock_cache[lru_idx].keyblock);
keyblock_cache[lru_idx].keyblock = keyblock;
keyblock_cache[lru_idx].lru = ++lru_counter;
leave:
if (!lru_counter)
{
/* Wrapped around. We simply clear the entire cache. */
flush_keyblock_cache ();
}
}
/* Flush the enire keyblock cache. */
void
flush_keyblock_cache (void)
{
int i;
for (i=0; i < DIM (keyblock_cache); i++)
{
do_release_keyblock (keyblock_cache[i].keyblock);
keyblock_cache[i].keyblock = NULL;
}
}
/* 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;
(void)fields; /* Not yet used. */
(void)nfields;
pubkey = xtrycalloc (1, sizeof *pubkey);
if (!pubkey)
return gpg_error_from_syserror ();
if (nfields > 5)
pubkey->created = parse_timestamp (fields[5], NULL);
*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;
int i;
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)
{
if (!err)
{
*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);
/* Try to get it from our cache. */
for (i=0; i < DIM (keyblock_cache); i++)
for (kb = keyblock_cache[i].keyblock; kb; kb = kb->next)
if (kb->protocol == protocol)
for (pk = kb->keys; pk; pk = pk->next)
if (pk->grip_valid && !memcmp (pk->grip, keygrip, KEYGRIP_LEN))
{
*r_keyblock = keyblock_cache[i].keyblock;
keyblock_cache[i].keyblock = NULL;
return 0;
}
/* 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;
}
struct export_key_status_parm_s
{
const char *fpr;
int found;
int count;
};
static void
export_key_status_cb (void *opaque, const char *keyword, char *args)
{
struct export_key_status_parm_s *parm = opaque;
if (!strcmp (keyword, "EXPORTED"))
{
parm->count++;
if (!ascii_strcasecmp (args, parm->fpr))
parm->found = 1;
}
}
/* Get a key by fingerprint from gpg's keyring. The binary key is
* returned as a new memory stream at R_KEY. */
gpg_error_t
get_minimal_openpgp_key (estream_t *r_key, const char *fingerprint)
{
gpg_error_t err;
ccparray_t ccp;
const char **argv = NULL;
estream_t key = NULL;
struct export_key_status_parm_s parm = { NULL };
*r_key = NULL;
key = es_fopenmem (0, "w+b");
if (!key)
{
err = gpg_error_from_syserror ();
log_error ("error allocating memory buffer: %s\n", gpg_strerror (err));
goto leave;
}
ccparray_init (&ccp, 0);
ccparray_put (&ccp, "--no-options");
if (!opt.verbose)
ccparray_put (&ccp, "--quiet");
else if (opt.verbose > 1)
ccparray_put (&ccp, "--verbose");
ccparray_put (&ccp, "--batch");
ccparray_put (&ccp, "--status-fd=2");
ccparray_put (&ccp, "--always-trust");
ccparray_put (&ccp, "--no-armor");
ccparray_put (&ccp, "--export-options=export-minimal");
ccparray_put (&ccp, "--export");
ccparray_put (&ccp, "--");
ccparray_put (&ccp, fingerprint);
ccparray_put (&ccp, NULL);
argv = ccparray_get (&ccp, NULL);
if (!argv)
{
err = gpg_error_from_syserror ();
goto leave;
}
parm.fpr = fingerprint;
err = gnupg_exec_tool_stream (opt.gpg_program, argv, NULL,
NULL, key,
export_key_status_cb, &parm);
if (!err && parm.count > 1)
err = gpg_error (GPG_ERR_TOO_MANY);
else if (!err && !parm.found)
err = gpg_error (GPG_ERR_NOT_FOUND);
if (err)
{
log_error ("export failed: %s\n", gpg_strerror (err));
goto leave;
}
es_rewind (key);
*r_key = key;
key = NULL;
leave:
es_fclose (key);
xfree (argv);
return err;
}