diff options
Diffstat (limited to 'agent/findkey.c')
-rw-r--r-- | agent/findkey.c | 1827 |
1 files changed, 1827 insertions, 0 deletions
diff --git a/agent/findkey.c b/agent/findkey.c new file mode 100644 index 0000000..dadcc3c --- /dev/null +++ b/agent/findkey.c @@ -0,0 +1,1827 @@ +/* findkey.c - Locate the secret key + * Copyright (C) 2001, 2002, 2003, 2004, 2005, 2007, + * 2010, 2011 Free Software Foundation, Inc. + * Copyright (C) 2014 Werner Koch + * + * 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/>. + */ + +#include <config.h> +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <ctype.h> +#include <fcntl.h> +#include <assert.h> +#include <unistd.h> +#include <sys/stat.h> +#include <assert.h> +#include <npth.h> /* (we use pth_sleep) */ + +#include "agent.h" +#include "../common/i18n.h" +#include "../common/ssh-utils.h" +#include "../common/name-value.h" + +#ifndef O_BINARY +#define O_BINARY 0 +#endif + +/* Helper to pass data to the check callback of the unprotect function. */ +struct try_unprotect_arg_s +{ + ctrl_t ctrl; + const unsigned char *protected_key; + unsigned char *unprotected_key; + int change_required; /* Set by the callback to indicate that the + user should change the passphrase. */ +}; + + +/* Return the file name for the 20 byte keygrip GRIP. Return NULL on + * error. */ +static char * +fname_from_keygrip (const unsigned char *grip) +{ + char hexgrip[40+4+1]; + + bin2hex (grip, 20, hexgrip); + strcpy (hexgrip+40, ".key"); + + return make_filename_try (gnupg_homedir (), GNUPG_PRIVATE_KEYS_DIR, + hexgrip, NULL); +} + + +/* Note: Ownership of FNAME and FP are moved to this function. + * OLD_FORMAT is true if the file exists but is still in the + * non-extended mode format. If MAYBE_UPDATE is set the function + * assumes that the file exists but writes it only if it figures that + * an update is required. */ +static gpg_error_t +write_extended_private_key (int maybe_update, + char *fname, estream_t fp, + int old_format, int newkey, + const void *buf, size_t len, time_t timestamp, + const char *serialno, const char *keyref, + const char *dispserialno) +{ + gpg_error_t err; + nvc_t pk = NULL; + gcry_sexp_t key = NULL; + int remove = 0; + char *token0 = NULL; + char *token = NULL; + char *dispserialno_buffer = NULL; + char **tokenfields = NULL; + + if (old_format || newkey) + { + /* We must create a new NVC if the key is still in the old + * format and of course if it is a new key. */ + pk = nvc_new_private_key (); + if (!pk) + { + err = gpg_error_from_syserror (); + goto leave; + } + maybe_update = 0; /* Always write. */ + } + else + { /* Parse the existing NVC. */ + int lineno = 0; + + err = nvc_parse_private_key (&pk, &lineno, fp); + if (err) + { + log_error ("error parsing '%s' line %d: %s\n", + fname, lineno, gpg_strerror (err)); + goto leave; + } + } + es_clearerr (fp); + + err = gcry_sexp_sscan (&key, NULL, buf, len); + if (err) + goto leave; + + err = nvc_set_private_key (pk, key); + if (err) + goto leave; + + /* If a timestamp has been supplied and the key is new write a + * creation timestamp. Note that we can't add this item if we are + * still in the old format. We also add an extra check that there + * is no Created item yet. */ + if (timestamp && newkey && !nvc_lookup (pk, "Created:")) + { + gnupg_isotime_t timebuf; + + epoch2isotime (timebuf, timestamp); + err = nvc_add (pk, "Created:", timebuf); + if (err) + goto leave; + } + + /* If requested write a Token line. */ + if (serialno && keyref) + { + nve_t item; + const char *s; + size_t token0len; + + if (dispserialno) + { + /* Escape the DISPSERIALNO. */ + dispserialno_buffer = percent_plus_escape (dispserialno); + if (!dispserialno_buffer) + { + err = gpg_error_from_syserror (); + goto leave; + } + dispserialno = dispserialno_buffer; + } + + token0 = strconcat (serialno, " ", keyref, NULL); + if (token0) + token = strconcat (token0, " - ", dispserialno? dispserialno:"-", NULL); + if (!token0 || !token) + { + err = gpg_error_from_syserror (); + goto leave; + } + + token0len = strlen (token0); + for (item = nvc_lookup (pk, "Token:"); + item; + item = nve_next_value (item, "Token:")) + if ((s = nve_value (item)) && !strncmp (s, token0, token0len)) + break; + if (!item) + { + /* No token or no token with that value exists. Add a new + * one so that keys which have been stored on several cards + * are well supported. */ + err = nvc_add (pk, "Token:", token); + if (err) + goto leave; + maybe_update = 0; /* Force write. */ + } + else + { + /* Token exists: Update the display s/n. It may have + * changed due to changes in a newer software version. */ + if (maybe_update && s && (tokenfields = strtokenize (s, " \t\n")) + && tokenfields[0] && tokenfields[1] && tokenfields[2] + && tokenfields[3] + && !strcmp (tokenfields[3], dispserialno)) + ; /* No need to update Token entry. */ + else + { + err = nve_set (item, token); + if (err) + goto leave; + maybe_update = 0; /* Force write. */ + } + } + } + + err = es_fseek (fp, 0, SEEK_SET); + if (err) + goto leave; + + if (!maybe_update) + { + err = nvc_write (pk, fp); + if (!err) + err = es_fflush (fp); + if (err) + { + log_error ("error writing '%s': %s\n", fname, gpg_strerror (err)); + remove = 1; + goto leave; + } + + if (ftruncate (es_fileno (fp), es_ftello (fp))) + { + err = gpg_error_from_syserror (); + log_error ("error truncating '%s': %s\n", fname, gpg_strerror (err)); + remove = 1; + goto leave; + } + } + + if (es_fclose (fp)) + { + err = gpg_error_from_syserror (); + log_error ("error closing '%s': %s\n", fname, gpg_strerror (err)); + remove = 1; + goto leave; + } + else + fp = NULL; + + if (!maybe_update) + bump_key_eventcounter (); + + leave: + es_fclose (fp); + if (remove) + gnupg_remove (fname); + xfree (fname); + xfree (token); + xfree (token0); + xfree (dispserialno_buffer); + xfree (tokenfields); + gcry_sexp_release (key); + nvc_release (pk); + return err; +} + +/* Write an S-expression formatted key to our key storage. With FORCE + * passed as true an existing key with the given GRIP will get + * overwritten. If TIMESTAMP is not zero and the key does not yet + * exists it will be recorded as creation date. If SERIALNO, KEYREF, + * of DISPSERIALNO are not NULL they will be recorded as well. */ +int +agent_write_private_key (const unsigned char *grip, + const void *buffer, size_t length, + int force, time_t timestamp, + const char *serialno, const char *keyref, + const char *dispserialno) +{ + char *fname; + estream_t fp; + + fname = fname_from_keygrip (grip); + if (!fname) + return gpg_error_from_syserror (); + + /* FIXME: Write to a temp file first so that write failures during + key updates won't lead to a key loss. */ + + if (!force && !gnupg_access (fname, F_OK)) + { + log_error ("secret key file '%s' already exists\n", fname); + xfree (fname); + return gpg_error (GPG_ERR_EEXIST); + } + + fp = es_fopen (fname, force? "rb+,mode=-rw" : "wbx,mode=-rw"); + if (!fp) + { + gpg_error_t tmperr = gpg_error_from_syserror (); + + if (force && gpg_err_code (tmperr) == GPG_ERR_ENOENT) + { + fp = es_fopen (fname, "wbx,mode=-rw"); + if (!fp) + tmperr = gpg_error_from_syserror (); + } + if (!fp) + { + log_error ("can't create '%s': %s\n", fname, gpg_strerror (tmperr)); + xfree (fname); + return tmperr; + } + } + else if (force) + { + gpg_error_t rc; + char first; + + /* See if an existing key is in extended format. */ + if (es_fread (&first, 1, 1, fp) != 1) + { + rc = gpg_error_from_syserror (); + log_error ("error reading first byte from '%s': %s\n", + fname, strerror (errno)); + xfree (fname); + es_fclose (fp); + return rc; + } + + rc = es_fseek (fp, 0, SEEK_SET); + if (rc) + { + log_error ("error seeking in '%s': %s\n", fname, strerror (errno)); + xfree (fname); + es_fclose (fp); + return rc; + } + + if (first != '(') + { + /* Key is already in the extended format. */ + return write_extended_private_key (0, fname, fp, 0, 0, + buffer, length, + timestamp, serialno, keyref, + dispserialno); + } + if (first == '(' && opt.enable_extended_key_format) + { + /* Key is in the old format - but we want the extended format. */ + return write_extended_private_key (0, fname, fp, 1, 0, + buffer, length, + timestamp, serialno, keyref, + dispserialno); + } + } + + if (opt.enable_extended_key_format) + return write_extended_private_key (0, fname, fp, 0, 1, + buffer, length, + timestamp, serialno, keyref, + dispserialno); + + if (es_fwrite (buffer, length, 1, fp) != 1) + { + gpg_error_t tmperr = gpg_error_from_syserror (); + log_error ("error writing '%s': %s\n", fname, gpg_strerror (tmperr)); + es_fclose (fp); + gnupg_remove (fname); + xfree (fname); + return tmperr; + } + + /* When force is given, the file might have to be truncated. */ + if (force && ftruncate (es_fileno (fp), es_ftello (fp))) + { + gpg_error_t tmperr = gpg_error_from_syserror (); + log_error ("error truncating '%s': %s\n", fname, gpg_strerror (tmperr)); + es_fclose (fp); + gnupg_remove (fname); + xfree (fname); + return tmperr; + } + + if (es_fclose (fp)) + { + gpg_error_t tmperr = gpg_error_from_syserror (); + log_error ("error closing '%s': %s\n", fname, gpg_strerror (tmperr)); + gnupg_remove (fname); + xfree (fname); + return tmperr; + } + bump_key_eventcounter (); + xfree (fname); + return 0; +} + + +/* Callback function to try the unprotection from the passphrase query + code. */ +static gpg_error_t +try_unprotect_cb (struct pin_entry_info_s *pi) +{ + struct try_unprotect_arg_s *arg = pi->check_cb_arg; + ctrl_t ctrl = arg->ctrl; + size_t dummy; + gpg_error_t err; + gnupg_isotime_t now, protected_at, tmptime; + char *desc = NULL; + + assert (!arg->unprotected_key); + + arg->change_required = 0; + err = agent_unprotect (ctrl, arg->protected_key, pi->pin, protected_at, + &arg->unprotected_key, &dummy); + if (err) + return err; + if (!opt.max_passphrase_days || ctrl->in_passwd) + return 0; /* No regular passphrase change required. */ + + if (!*protected_at) + { + /* No protection date known - must force passphrase change. */ + desc = xtrystrdup (L_("Note: This passphrase has never been changed.%0A" + "Please change it now.")); + if (!desc) + return gpg_error_from_syserror (); + } + else + { + gnupg_get_isotime (now); + gnupg_copy_time (tmptime, protected_at); + err = add_days_to_isotime (tmptime, opt.max_passphrase_days); + if (err) + return err; + if (strcmp (now, tmptime) > 0 ) + { + /* Passphrase "expired". */ + desc = xtryasprintf + (L_("This passphrase has not been changed%%0A" + "since %.4s-%.2s-%.2s. Please change it now."), + protected_at, protected_at+4, protected_at+6); + if (!desc) + return gpg_error_from_syserror (); + } + } + + if (desc) + { + /* Change required. */ + if (opt.enforce_passphrase_constraints) + { + err = agent_get_confirmation (ctrl, desc, + L_("Change passphrase"), NULL, 0); + if (!err) + arg->change_required = 1; + } + else + { + err = agent_get_confirmation (ctrl, desc, + L_("Change passphrase"), + L_("I'll change it later"), 0); + if (!err) + arg->change_required = 1; + else if (gpg_err_code (err) == GPG_ERR_CANCELED + || gpg_err_code (err) == GPG_ERR_FULLY_CANCELED) + err = 0; + } + xfree (desc); + } + + return err; +} + + +/* Modify a Key description, replacing certain special format + characters. List of currently supported replacements: + + %% - Replaced by a single % + %c - Replaced by the content of COMMENT. + %C - Same as %c but put into parentheses. + %F - Replaced by an ssh style fingerprint computed from KEY. + + The functions returns 0 on success or an error code. On success a + newly allocated string is stored at the address of RESULT. + */ +gpg_error_t +agent_modify_description (const char *in, const char *comment, + const gcry_sexp_t key, char **result) +{ + size_t comment_length; + size_t in_len; + size_t out_len; + char *out; + size_t i; + int special, pass; + char *ssh_fpr = NULL; + char *p; + + *result = NULL; + + if (!comment) + comment = ""; + + comment_length = strlen (comment); + in_len = strlen (in); + + /* First pass calculates the length, second pass does the actual + copying. */ + /* FIXME: This can be simplified by using es_fopenmem. */ + out = NULL; + out_len = 0; + for (pass=0; pass < 2; pass++) + { + special = 0; + for (i = 0; i < in_len; i++) + { + if (special) + { + special = 0; + switch (in[i]) + { + case '%': + if (out) + *out++ = '%'; + else + out_len++; + break; + + case 'c': /* Comment. */ + if (out) + { + memcpy (out, comment, comment_length); + out += comment_length; + } + else + out_len += comment_length; + break; + + case 'C': /* Comment. */ + if (!comment_length) + ; + else if (out) + { + *out++ = '('; + memcpy (out, comment, comment_length); + out += comment_length; + *out++ = ')'; + } + else + out_len += comment_length + 2; + break; + + case 'F': /* SSH style fingerprint. */ + if (!ssh_fpr && key) + ssh_get_fingerprint_string (key, opt.ssh_fingerprint_digest, + &ssh_fpr); + if (ssh_fpr) + { + if (out) + out = stpcpy (out, ssh_fpr); + else + out_len += strlen (ssh_fpr); + } + break; + + default: /* Invalid special sequences are kept as they are. */ + if (out) + { + *out++ = '%'; + *out++ = in[i]; + } + else + out_len+=2; + break; + } + } + else if (in[i] == '%') + special = 1; + else + { + if (out) + *out++ = in[i]; + else + out_len++; + } + } + + if (!pass) + { + *result = out = xtrymalloc (out_len + 1); + if (!out) + { + xfree (ssh_fpr); + return gpg_error_from_syserror (); + } + } + } + + *out = 0; + log_assert (*result + out_len == out); + xfree (ssh_fpr); + + /* The ssh prompt may sometimes end in + * "...%0A ()" + * The empty parentheses doesn't look very good. We use this hack + * here to remove them as well as the indentation spaces. */ + p = *result; + i = strlen (p); + if (i > 2 && !strcmp (p + i - 2, "()")) + { + p += i - 2; + *p-- = 0; + while (p > *result && spacep (p)) + *p-- = 0; + } + + return 0; +} + + + +/* Unprotect the canconical encoded S-expression key in KEYBUF. GRIP + should be the hex encoded keygrip of that key to be used with the + caching mechanism. DESC_TEXT may be set to override the default + description used for the pinentry. If LOOKUP_TTL is given this + function is used to lookup the default ttl. If R_PASSPHRASE is not + NULL, the function succeeded and the key was protected the used + passphrase (entered or from the cache) is stored there; if not NULL + will be stored. The caller needs to free the returned + passphrase. */ +static gpg_error_t +unprotect (ctrl_t ctrl, const char *cache_nonce, const char *desc_text, + unsigned char **keybuf, const unsigned char *grip, + cache_mode_t cache_mode, lookup_ttl_t lookup_ttl, + char **r_passphrase) +{ + struct pin_entry_info_s *pi; + struct try_unprotect_arg_s arg; + int rc; + unsigned char *result; + size_t resultlen; + char hexgrip[40+1]; + + if (r_passphrase) + *r_passphrase = NULL; + + bin2hex (grip, 20, hexgrip); + + /* Initially try to get it using a cache nonce. */ + if (cache_nonce) + { + char *pw; + + pw = agent_get_cache (ctrl, cache_nonce, CACHE_MODE_NONCE); + if (pw) + { + rc = agent_unprotect (ctrl, *keybuf, pw, NULL, &result, &resultlen); + if (!rc) + { + if (r_passphrase) + *r_passphrase = pw; + else + xfree (pw); + xfree (*keybuf); + *keybuf = result; + return 0; + } + xfree (pw); + } + } + + /* First try to get it from the cache - if there is none or we can't + unprotect it, we fall back to ask the user */ + if (cache_mode != CACHE_MODE_IGNORE) + { + char *pw; + + retry: + pw = agent_get_cache (ctrl, hexgrip, cache_mode); + if (pw) + { + rc = agent_unprotect (ctrl, *keybuf, pw, NULL, &result, &resultlen); + if (!rc) + { + if (cache_mode == CACHE_MODE_NORMAL) + agent_store_cache_hit (hexgrip); + if (r_passphrase) + *r_passphrase = pw; + else + xfree (pw); + xfree (*keybuf); + *keybuf = result; + return 0; + } + xfree (pw); + } + else if (cache_mode == CACHE_MODE_NORMAL) + { + /* The standard use of GPG keys is to have a signing and an + encryption subkey. Commonly both use the same + passphrase. We try to help the user to enter the + passphrase only once by silently trying the last + correctly entered passphrase. Checking one additional + passphrase should be acceptable; despite the S2K + introduced delays. The assumed workflow is: + + 1. Read encrypted message in a MUA and thus enter a + passphrase for the encryption subkey. + + 2. Reply to that mail with an encrypted and signed + mail, thus entering the passphrase for the signing + subkey. + + We can often avoid the passphrase entry in the second + step. We do this only in normal mode, so not to + interfere with unrelated cache entries. */ + pw = agent_get_cache (ctrl, NULL, cache_mode); + if (pw) + { + rc = agent_unprotect (ctrl, *keybuf, pw, NULL, + &result, &resultlen); + if (!rc) + { + if (r_passphrase) + *r_passphrase = pw; + else + xfree (pw); + xfree (*keybuf); + *keybuf = result; + return 0; + } + xfree (pw); + } + } + + /* If the pinentry is currently in use, we wait up to 60 seconds + for it to close and check the cache again. This solves a common + situation where several requests for unprotecting a key have + been made but the user is still entering the passphrase for + the first request. Because all requests to agent_askpin are + serialized they would then pop up one after the other to + request the passphrase - despite that the user has already + entered it and is then available in the cache. This + implementation is not race free but in the worst case the + user has to enter the passphrase only once more. */ + if (pinentry_active_p (ctrl, 0)) + { + /* Active - wait */ + if (!pinentry_active_p (ctrl, 60)) + { + /* We need to give the other thread a chance to actually put + it into the cache. */ + npth_sleep (1); + goto retry; + } + /* Timeout - better call pinentry now the plain way. */ + } + } + + pi = gcry_calloc_secure (1, sizeof (*pi) + MAX_PASSPHRASE_LEN + 1); + if (!pi) + return gpg_error_from_syserror (); + pi->max_length = MAX_PASSPHRASE_LEN + 1; + pi->min_digits = 0; /* we want a real passphrase */ + pi->max_digits = 16; + pi->max_tries = 3; + pi->check_cb = try_unprotect_cb; + arg.ctrl = ctrl; + arg.protected_key = *keybuf; + arg.unprotected_key = NULL; + arg.change_required = 0; + pi->check_cb_arg = &arg; + + rc = agent_askpin (ctrl, desc_text, NULL, NULL, pi, hexgrip, cache_mode); + if (rc) + { + if ((pi->status & PINENTRY_STATUS_PASSWORD_FROM_CACHE)) + { + log_error ("Clearing pinentry cache which caused error %s\n", + gpg_strerror (rc)); + + agent_clear_passphrase (ctrl, hexgrip, cache_mode); + } + } + else + { + assert (arg.unprotected_key); + if (arg.change_required) + { + /* The callback told as that the user should change their + passphrase. Present the dialog to do. */ + size_t canlen, erroff; + gcry_sexp_t s_skey; + + assert (arg.unprotected_key); + canlen = gcry_sexp_canon_len (arg.unprotected_key, 0, NULL, NULL); + rc = gcry_sexp_sscan (&s_skey, &erroff, + (char*)arg.unprotected_key, canlen); + if (rc) + { + log_error ("failed to build S-Exp (off=%u): %s\n", + (unsigned int)erroff, gpg_strerror (rc)); + wipememory (arg.unprotected_key, canlen); + xfree (arg.unprotected_key); + xfree (pi); + return rc; + } + rc = agent_protect_and_store (ctrl, s_skey, NULL); + gcry_sexp_release (s_skey); + if (rc) + { + log_error ("changing the passphrase failed: %s\n", + gpg_strerror (rc)); + wipememory (arg.unprotected_key, canlen); + xfree (arg.unprotected_key); + xfree (pi); + return rc; + } + } + else + { + /* Passphrase is fine. */ + agent_put_cache (ctrl, hexgrip, cache_mode, pi->pin, + lookup_ttl? lookup_ttl (hexgrip) : 0); + agent_store_cache_hit (hexgrip); + if (r_passphrase && *pi->pin) + *r_passphrase = xtrystrdup (pi->pin); + } + xfree (*keybuf); + *keybuf = arg.unprotected_key; + } + xfree (pi); + return rc; +} + + +/* Read the key identified by GRIP from the private key directory and + * return it as an gcrypt S-expression object in RESULT. If R_KEYMETA + * is not NULl and the extended key format is used, the meta data + * items are stored there. However the "Key:" item is removed from + * it. On failure returns an error code and stores NULL at RESULT. */ +static gpg_error_t +read_key_file (const unsigned char *grip, gcry_sexp_t *result, nvc_t *r_keymeta) +{ + gpg_error_t err; + char *fname; + estream_t fp; + struct stat st; + unsigned char *buf; + size_t buflen, erroff; + gcry_sexp_t s_skey; + char hexgrip[40+4+1]; + char first; + + *result = NULL; + if (r_keymeta) + *r_keymeta = NULL; + + bin2hex (grip, 20, hexgrip); + strcpy (hexgrip+40, ".key"); + + fname = make_filename (gnupg_homedir (), GNUPG_PRIVATE_KEYS_DIR, + hexgrip, NULL); + fp = es_fopen (fname, "rb"); + if (!fp) + { + err = gpg_error_from_syserror (); + if (gpg_err_code (err) != GPG_ERR_ENOENT) + log_error ("can't open '%s': %s\n", fname, gpg_strerror (err)); + xfree (fname); + return err; + } + + if (es_fread (&first, 1, 1, fp) != 1) + { + err = gpg_error_from_syserror (); + log_error ("error reading first byte from '%s': %s\n", + fname, gpg_strerror (err)); + xfree (fname); + es_fclose (fp); + return err; + } + + if (es_fseek (fp, 0, SEEK_SET)) + { + err = gpg_error_from_syserror (); + log_error ("error seeking in '%s': %s\n", fname, gpg_strerror (err)); + xfree (fname); + es_fclose (fp); + return err; + } + + if (first != '(') + { + /* Key is in extended format. */ + nvc_t pk; + int line; + + err = nvc_parse_private_key (&pk, &line, fp); + es_fclose (fp); + + if (err) + log_error ("error parsing '%s' line %d: %s\n", + fname, line, gpg_strerror (err)); + else + { + err = nvc_get_private_key (pk, result); + if (err) + log_error ("error getting private key from '%s': %s\n", + fname, gpg_strerror (err)); + else + nvc_delete_named (pk, "Key:"); + } + + if (!err && r_keymeta) + *r_keymeta = pk; + else + nvc_release (pk); + xfree (fname); + return err; + } + + if (fstat (es_fileno (fp), &st)) + { + err = gpg_error_from_syserror (); + log_error ("can't stat '%s': %s\n", fname, gpg_strerror (err)); + xfree (fname); + es_fclose (fp); + return err; + } + + buflen = st.st_size; + buf = xtrymalloc (buflen+1); + if (!buf) + { + err = gpg_error_from_syserror (); + log_error ("error allocating %zu bytes for '%s': %s\n", + buflen, fname, gpg_strerror (err)); + xfree (fname); + es_fclose (fp); + xfree (buf); + return err; + + } + + if (es_fread (buf, buflen, 1, fp) != 1) + { + err = gpg_error_from_syserror (); + log_error ("error reading %zu bytes from '%s': %s\n", + buflen, fname, gpg_strerror (err)); + xfree (fname); + es_fclose (fp); + xfree (buf); + return err; + } + + /* Convert the file into a gcrypt S-expression object. */ + err = gcry_sexp_sscan (&s_skey, &erroff, (char*)buf, buflen); + xfree (fname); + es_fclose (fp); + xfree (buf); + if (err) + { + log_error ("failed to build S-Exp (off=%u): %s\n", + (unsigned int)erroff, gpg_strerror (err)); + return err; + } + *result = s_skey; + return 0; +} + + +/* Remove the key identified by GRIP from the private key directory. */ +static gpg_error_t +remove_key_file (const unsigned char *grip) +{ + gpg_error_t err = 0; + char *fname; + char hexgrip[40+4+1]; + + bin2hex (grip, 20, hexgrip); + strcpy (hexgrip+40, ".key"); + fname = make_filename (gnupg_homedir (), GNUPG_PRIVATE_KEYS_DIR, + hexgrip, NULL); + if (gnupg_remove (fname)) + err = gpg_error_from_syserror (); + xfree (fname); + return err; +} + + +/* Return the secret key as an S-Exp in RESULT after locating it using + the GRIP. If the operation shall be diverted to a token, an + allocated S-expression with the shadow_info part from the file is + stored at SHADOW_INFO; if not NULL will be stored at SHADOW_INFO. + CACHE_MODE defines now the cache shall be used. DESC_TEXT may be + set to present a custom description for the pinentry. LOOKUP_TTL + is an optional function to convey a TTL to the cache manager; we do + not simply pass the TTL value because the value is only needed if + an unprotect action was needed and looking up the TTL may have some + overhead (e.g. scanning the sshcontrol file). If a CACHE_NONCE is + given that cache item is first tried to get a passphrase. If + R_PASSPHRASE is not NULL, the function succeeded and the key was + protected the used passphrase (entered or from the cache) is stored + there; if not NULL will be stored. The caller needs to free the + returned passphrase. */ +gpg_error_t +agent_key_from_file (ctrl_t ctrl, const char *cache_nonce, + const char *desc_text, + const unsigned char *grip, unsigned char **shadow_info, + cache_mode_t cache_mode, lookup_ttl_t lookup_ttl, + gcry_sexp_t *result, char **r_passphrase) +{ + gpg_error_t err; + unsigned char *buf; + size_t len, buflen, erroff; + gcry_sexp_t s_skey; + + *result = NULL; + if (shadow_info) + *shadow_info = NULL; + if (r_passphrase) + *r_passphrase = NULL; + + err = read_key_file (grip, &s_skey, NULL); + if (err) + { + if (gpg_err_code (err) == GPG_ERR_ENOENT) + err = gpg_error (GPG_ERR_NO_SECKEY); + return err; + } + + /* For use with the protection functions we also need the key as an + canonical encoded S-expression in a buffer. Create this buffer + now. */ + err = make_canon_sexp (s_skey, &buf, &len); + if (err) + return err; + + switch (agent_private_key_type (buf)) + { + case PRIVATE_KEY_CLEAR: + break; /* no unprotection needed */ + case PRIVATE_KEY_OPENPGP_NONE: + { + unsigned char *buf_new; + size_t buf_newlen; + + err = agent_unprotect (ctrl, buf, "", NULL, &buf_new, &buf_newlen); + if (err) + log_error ("failed to convert unprotected openpgp key: %s\n", + gpg_strerror (err)); + else + { + xfree (buf); + buf = buf_new; + } + } + break; + case PRIVATE_KEY_PROTECTED: + { + char *desc_text_final; + char *comment = NULL; + + /* Note, that we will take the comment as a C string for + display purposes; i.e. all stuff beyond a Nul character is + ignored. */ + { + gcry_sexp_t comment_sexp; + + comment_sexp = gcry_sexp_find_token (s_skey, "comment", 0); + if (comment_sexp) + comment = gcry_sexp_nth_string (comment_sexp, 1); + gcry_sexp_release (comment_sexp); + } + + desc_text_final = NULL; + if (desc_text) + err = agent_modify_description (desc_text, comment, s_skey, + &desc_text_final); + gcry_free (comment); + + if (!err) + { + err = unprotect (ctrl, cache_nonce, desc_text_final, &buf, grip, + cache_mode, lookup_ttl, r_passphrase); + if (err) + log_error ("failed to unprotect the secret key: %s\n", + gpg_strerror (err)); + } + + xfree (desc_text_final); + } + break; + case PRIVATE_KEY_SHADOWED: + if (shadow_info) + { + const unsigned char *s; + size_t n; + + err = agent_get_shadow_info (buf, &s); + if (!err) + { + n = gcry_sexp_canon_len (s, 0, NULL,NULL); + log_assert (n); + *shadow_info = xtrymalloc (n); + if (!*shadow_info) + err = out_of_core (); + else + { + memcpy (*shadow_info, s, n); + err = 0; + } + } + if (err) + log_error ("get_shadow_info failed: %s\n", gpg_strerror (err)); + } + else + err = gpg_error (GPG_ERR_UNUSABLE_SECKEY); + break; + default: + log_error ("invalid private key format\n"); + err = gpg_error (GPG_ERR_BAD_SECKEY); + break; + } + gcry_sexp_release (s_skey); + s_skey = NULL; + if (err) + { + xfree (buf); + if (r_passphrase) + { + xfree (*r_passphrase); + *r_passphrase = NULL; + } + return err; + } + + buflen = gcry_sexp_canon_len (buf, 0, NULL, NULL); + err = gcry_sexp_sscan (&s_skey, &erroff, (char*)buf, buflen); + wipememory (buf, buflen); + xfree (buf); + if (err) + { + log_error ("failed to build S-Exp (off=%u): %s\n", + (unsigned int)erroff, gpg_strerror (err)); + if (r_passphrase) + { + xfree (*r_passphrase); + *r_passphrase = NULL; + } + return err; + } + + *result = s_skey; + return 0; +} + + +/* Return the string name from the S-expression S_KEY as well as a + string describing the names of the parameters. ALGONAMESIZE and + ELEMSSIZE give the allocated size of the provided buffers. The + buffers may be NULL if not required. If R_LIST is not NULL the top + level list will be stored there; the caller needs to release it in + this case. */ +static gpg_error_t +key_parms_from_sexp (gcry_sexp_t s_key, gcry_sexp_t *r_list, + char *r_algoname, size_t algonamesize, + char *r_elems, size_t elemssize) +{ + gcry_sexp_t list, l2; + const char *name, *algoname, *elems; + size_t n; + + if (r_list) + *r_list = NULL; + + list = gcry_sexp_find_token (s_key, "shadowed-private-key", 0 ); + if (!list) + list = gcry_sexp_find_token (s_key, "protected-private-key", 0 ); + if (!list) + list = gcry_sexp_find_token (s_key, "private-key", 0 ); + if (!list) + { + log_error ("invalid private key format\n"); + return gpg_error (GPG_ERR_BAD_SECKEY); + } + + l2 = gcry_sexp_cadr (list); + gcry_sexp_release (list); + list = l2; + name = gcry_sexp_nth_data (list, 0, &n); + if (n==3 && !memcmp (name, "rsa", 3)) + { + algoname = "rsa"; + elems = "ne"; + } + else if (n==3 && !memcmp (name, "dsa", 3)) + { + algoname = "dsa"; + elems = "pqgy"; + } + else if (n==3 && !memcmp (name, "ecc", 3)) + { + algoname = "ecc"; + elems = "pabgnq"; + } + else if (n==5 && !memcmp (name, "ecdsa", 5)) + { + algoname = "ecdsa"; + elems = "pabgnq"; + } + else if (n==4 && !memcmp (name, "ecdh", 4)) + { + algoname = "ecdh"; + elems = "pabgnq"; + } + else if (n==3 && !memcmp (name, "elg", 3)) + { + algoname = "elg"; + elems = "pgy"; + } + else + { + log_error ("unknown private key algorithm\n"); + gcry_sexp_release (list); + return gpg_error (GPG_ERR_BAD_SECKEY); + } + + if (r_algoname) + { + if (strlen (algoname) >= algonamesize) + return gpg_error (GPG_ERR_BUFFER_TOO_SHORT); + strcpy (r_algoname, algoname); + } + if (r_elems) + { + if (strlen (elems) >= elemssize) + return gpg_error (GPG_ERR_BUFFER_TOO_SHORT); + strcpy (r_elems, elems); + } + + if (r_list) + *r_list = list; + else + gcry_sexp_release (list); + + return 0; +} + + +/* Return true if KEYPARMS holds an EdDSA key. */ +static int +is_eddsa (gcry_sexp_t keyparms) +{ + int result = 0; + gcry_sexp_t list; + const char *s; + size_t n; + int i; + + list = gcry_sexp_find_token (keyparms, "flags", 0); + for (i = list ? gcry_sexp_length (list)-1 : 0; i > 0; i--) + { + s = gcry_sexp_nth_data (list, i, &n); + if (!s) + continue; /* Not a data element. */ + + if (n == 5 && !memcmp (s, "eddsa", 5)) + { + result = 1; + break; + } + } + gcry_sexp_release (list); + return result; +} + + +/* Return the public key algorithm number if S_KEY is a DSA style key. + If it is not a DSA style key, return 0. */ +int +agent_is_dsa_key (gcry_sexp_t s_key) +{ + int result; + gcry_sexp_t list; + char algoname[6]; + + if (!s_key) + return 0; + + if (key_parms_from_sexp (s_key, &list, algoname, sizeof algoname, NULL, 0)) + return 0; /* Error - assume it is not an DSA key. */ + + if (!strcmp (algoname, "dsa")) + result = GCRY_PK_DSA; + else if (!strcmp (algoname, "ecc")) + { + if (is_eddsa (list)) + result = 0; + else + result = GCRY_PK_ECDSA; + } + else if (!strcmp (algoname, "ecdsa")) + result = GCRY_PK_ECDSA; + else + result = 0; + + gcry_sexp_release (list); + return result; +} + + +/* Return true if S_KEY is an EdDSA key as used with curve Ed25519. */ +int +agent_is_eddsa_key (gcry_sexp_t s_key) +{ + int result; + gcry_sexp_t list; + char algoname[6]; + + if (!s_key) + return 0; + + if (key_parms_from_sexp (s_key, &list, algoname, sizeof algoname, NULL, 0)) + return 0; /* Error - assume it is not an EdDSA key. */ + + if (!strcmp (algoname, "ecc") && is_eddsa (list)) + result = 1; + else if (!strcmp (algoname, "eddsa")) /* backward compatibility. */ + result = 1; + else + result = 0; + + gcry_sexp_release (list); + return result; +} + + +/* Return the key for the keygrip GRIP. The result is stored at + RESULT. This function extracts the key from the private key + database and returns it as an S-expression object as it is. On + failure an error code is returned and NULL stored at RESULT. */ +gpg_error_t +agent_raw_key_from_file (ctrl_t ctrl, const unsigned char *grip, + gcry_sexp_t *result) +{ + gpg_error_t err; + gcry_sexp_t s_skey; + + (void)ctrl; + + *result = NULL; + + err = read_key_file (grip, &s_skey, NULL); + if (!err) + *result = s_skey; + return err; +} + + +gpg_error_t +agent_keymeta_from_file (ctrl_t ctrl, const unsigned char *grip, + nvc_t *r_keymeta) +{ + gpg_error_t err; + gcry_sexp_t s_skey; + + (void)ctrl; + + err = read_key_file (grip, &s_skey, r_keymeta); + gcry_sexp_release (s_skey); + return err; +} + + +/* Return the public key for the keygrip GRIP. The result is stored + at RESULT. This function extracts the public key from the private + key database. On failure an error code is returned and NULL stored + at RESULT. */ +gpg_error_t +agent_public_key_from_file (ctrl_t ctrl, + const unsigned char *grip, + gcry_sexp_t *result) +{ + gpg_error_t err; + int i, idx; + gcry_sexp_t s_skey; + const char *algoname, *elems; + int npkey; + gcry_mpi_t array[10]; + gcry_sexp_t curve = NULL; + gcry_sexp_t flags = NULL; + gcry_sexp_t uri_sexp, comment_sexp; + const char *uri, *comment; + size_t uri_length, comment_length; + int uri_intlen, comment_intlen; + char *format, *p; + void *args[2+7+2+2+1]; /* Size is 2 + max. # of elements + 2 for uri + 2 + for comment + end-of-list. */ + int argidx; + gcry_sexp_t list = NULL; + const char *s; + + (void)ctrl; + + *result = NULL; + + err = read_key_file (grip, &s_skey, NULL); + if (err) + return err; + + for (i=0; i < DIM (array); i++) + array[i] = NULL; + + err = extract_private_key (s_skey, 0, &algoname, &npkey, NULL, &elems, + array, DIM (array), &curve, &flags); + if (err) + { + gcry_sexp_release (s_skey); + return err; + } + + uri = NULL; + uri_length = 0; + uri_sexp = gcry_sexp_find_token (s_skey, "uri", 0); + if (uri_sexp) + uri = gcry_sexp_nth_data (uri_sexp, 1, &uri_length); + + comment = NULL; + comment_length = 0; + comment_sexp = gcry_sexp_find_token (s_skey, "comment", 0); + if (comment_sexp) + comment = gcry_sexp_nth_data (comment_sexp, 1, &comment_length); + + gcry_sexp_release (s_skey); + s_skey = NULL; + + + /* FIXME: The following thing is pretty ugly code; we should + investigate how to make it cleaner. Probably code to handle + canonical S-expressions in a memory buffer is better suited for + such a task. After all that is what we do in protect.c. Need + to find common patterns and write a straightformward API to use + them. */ + assert (sizeof (size_t) <= sizeof (void*)); + + format = xtrymalloc (15+4+7*npkey+10+15+1+1); + if (!format) + { + err = gpg_error_from_syserror (); + for (i=0; array[i]; i++) + gcry_mpi_release (array[i]); + gcry_sexp_release (curve); + gcry_sexp_release (flags); + gcry_sexp_release (uri_sexp); + gcry_sexp_release (comment_sexp); + return err; + } + + argidx = 0; + p = stpcpy (stpcpy (format, "(public-key("), algoname); + p = stpcpy (p, "%S%S"); /* curve name and flags. */ + args[argidx++] = &curve; + args[argidx++] = &flags; + for (idx=0, s=elems; idx < npkey; idx++) + { + *p++ = '('; + *p++ = *s++; + p = stpcpy (p, " %m)"); + assert (argidx < DIM (args)); + args[argidx++] = &array[idx]; + } + *p++ = ')'; + if (uri) + { + p = stpcpy (p, "(uri %b)"); + assert (argidx+1 < DIM (args)); + uri_intlen = (int)uri_length; + args[argidx++] = (void *)&uri_intlen; + args[argidx++] = (void *)&uri; + } + if (comment) + { + p = stpcpy (p, "(comment %b)"); + assert (argidx+1 < DIM (args)); + comment_intlen = (int)comment_length; + args[argidx++] = (void *)&comment_intlen; + args[argidx++] = (void*)&comment; + } + *p++ = ')'; + *p = 0; + assert (argidx < DIM (args)); + args[argidx] = NULL; + + err = gcry_sexp_build_array (&list, NULL, format, args); + xfree (format); + for (i=0; array[i]; i++) + gcry_mpi_release (array[i]); + gcry_sexp_release (curve); + gcry_sexp_release (flags); + gcry_sexp_release (uri_sexp); + gcry_sexp_release (comment_sexp); + + if (!err) + *result = list; + return err; +} + + + +/* Check whether the secret key identified by GRIP is available. + Returns 0 is the key is available. */ +int +agent_key_available (const unsigned char *grip) +{ + int result; + char *fname; + char hexgrip[40+4+1]; + + bin2hex (grip, 20, hexgrip); + strcpy (hexgrip+40, ".key"); + + fname = make_filename (gnupg_homedir (), GNUPG_PRIVATE_KEYS_DIR, + hexgrip, NULL); + result = !gnupg_access (fname, R_OK)? 0 : -1; + xfree (fname); + return result; +} + + + +/* Return the information about the secret key specified by the binary + keygrip GRIP. If the key is a shadowed one the shadow information + will be stored at the address R_SHADOW_INFO as an allocated + S-expression. */ +gpg_error_t +agent_key_info_from_file (ctrl_t ctrl, const unsigned char *grip, + int *r_keytype, unsigned char **r_shadow_info) +{ + gpg_error_t err; + unsigned char *buf; + size_t len; + int keytype; + + (void)ctrl; + + if (r_keytype) + *r_keytype = PRIVATE_KEY_UNKNOWN; + if (r_shadow_info) + *r_shadow_info = NULL; + + { + gcry_sexp_t sexp; + + err = read_key_file (grip, &sexp, NULL); + if (err) + { + if (gpg_err_code (err) == GPG_ERR_ENOENT) + return gpg_error (GPG_ERR_NOT_FOUND); + else + return err; + } + err = make_canon_sexp (sexp, &buf, &len); + gcry_sexp_release (sexp); + if (err) + return err; + } + + keytype = agent_private_key_type (buf); + switch (keytype) + { + case PRIVATE_KEY_CLEAR: + case PRIVATE_KEY_OPENPGP_NONE: + break; + case PRIVATE_KEY_PROTECTED: + /* If we ever require it we could retrieve the comment fields + from such a key. */ + break; + case PRIVATE_KEY_SHADOWED: + if (r_shadow_info) + { + const unsigned char *s; + size_t n; + + err = agent_get_shadow_info (buf, &s); + if (!err) + { + n = gcry_sexp_canon_len (s, 0, NULL, NULL); + assert (n); + *r_shadow_info = xtrymalloc (n); + if (!*r_shadow_info) + err = gpg_error_from_syserror (); + else + memcpy (*r_shadow_info, s, n); + } + } + break; + default: + err = gpg_error (GPG_ERR_BAD_SECKEY); + break; + } + + if (!err && r_keytype) + *r_keytype = keytype; + + xfree (buf); + return err; +} + + + +/* Delete the key with GRIP from the disk after having asked for + * confirmation using DESC_TEXT. If FORCE is set the function won't + * require a confirmation via Pinentry or warns if the key is also + * used by ssh. If ONLY_STUBS is set only stub keys (references to + * smartcards) will be affected. + * + * Common error codes are: + * GPG_ERR_NO_SECKEY + * GPG_ERR_KEY_ON_CARD + * GPG_ERR_NOT_CONFIRMED + * GPG_ERR_FORBIDDEN - Not a stub key and ONLY_STUBS requested. + */ +gpg_error_t +agent_delete_key (ctrl_t ctrl, const char *desc_text, + const unsigned char *grip, int force, int only_stubs) +{ + gpg_error_t err; + gcry_sexp_t s_skey = NULL; + unsigned char *buf = NULL; + size_t len; + char *desc_text_final = NULL; + char *comment = NULL; + ssh_control_file_t cf = NULL; + char hexgrip[40+4+1]; + char *default_desc = NULL; + int key_type; + + err = read_key_file (grip, &s_skey, NULL); + if (gpg_err_code (err) == GPG_ERR_ENOENT) + err = gpg_error (GPG_ERR_NO_SECKEY); + if (err) + goto leave; + + err = make_canon_sexp (s_skey, &buf, &len); + if (err) + goto leave; + + key_type = agent_private_key_type (buf); + if (only_stubs && key_type != PRIVATE_KEY_SHADOWED) + { + err = gpg_error (GPG_ERR_FORBIDDEN); + goto leave; + } + + switch (key_type) + { + case PRIVATE_KEY_CLEAR: + case PRIVATE_KEY_OPENPGP_NONE: + case PRIVATE_KEY_PROTECTED: + bin2hex (grip, 20, hexgrip); + if (!force) + { + if (!desc_text) + { + default_desc = xtryasprintf + (L_("Do you really want to delete the key identified by keygrip%%0A" + " %s%%0A %%C%%0A?"), hexgrip); + desc_text = default_desc; + } + + /* Note, that we will take the comment as a C string for + display purposes; i.e. all stuff beyond a Nul character is + ignored. */ + { + gcry_sexp_t comment_sexp; + + comment_sexp = gcry_sexp_find_token (s_skey, "comment", 0); + if (comment_sexp) + comment = gcry_sexp_nth_string (comment_sexp, 1); + gcry_sexp_release (comment_sexp); + } + + if (desc_text) + err = agent_modify_description (desc_text, comment, s_skey, + &desc_text_final); + if (err) + goto leave; + + err = agent_get_confirmation (ctrl, desc_text_final, + L_("Delete key"), L_("No"), 0); + if (err) + goto leave; + + cf = ssh_open_control_file (); + if (cf) + { + if (!ssh_search_control_file (cf, hexgrip, NULL, NULL, NULL)) + { + err = agent_get_confirmation + (ctrl, + L_("Warning: This key is also listed for use with SSH!\n" + "Deleting the key might remove your ability to " + "access remote machines."), + L_("Delete key"), L_("No"), 0); + if (err) + goto leave; + } + } + } + err = remove_key_file (grip); + break; + + case PRIVATE_KEY_SHADOWED: + err = remove_key_file (grip); + break; + + default: + log_error ("invalid private key format\n"); + err = gpg_error (GPG_ERR_BAD_SECKEY); + break; + } + + leave: + ssh_close_control_file (cf); + gcry_free (comment); + xfree (desc_text_final); + xfree (default_desc); + xfree (buf); + gcry_sexp_release (s_skey); + return err; +} + + +/* Write an S-expression formatted shadow key to our key storage. + Shadow key is created by an S-expression public key in PKBUF and + card's SERIALNO and the IDSTRING. With FORCE passed as true an + existing key with the given GRIP will get overwritten. If + DISPSERIALNO is not NULL the human readable s/n will also be + recorded in the key file. If MAYBE_UPDATE is set it is assumed that + the shadow key already exists and we test whether we should update + it (FORCE is ignored in this case). */ +gpg_error_t +agent_write_shadow_key (int maybe_update, const unsigned char *grip, + const char *serialno, const char *keyid, + const unsigned char *pkbuf, int force, + const char *dispserialno) +{ + gpg_error_t err; + unsigned char *shadow_info; + unsigned char *shdkey; + size_t len; + char *fname = NULL; + estream_t fp = NULL; + char first; + + if (maybe_update && !opt.enable_extended_key_format) + return 0; /* Silently ignore. */ + + /* Just in case some caller did not parse the stuff correctly, skip + * leading spaces. */ + while (spacep (serialno)) + serialno++; + while (spacep (keyid)) + keyid++; + + shadow_info = make_shadow_info (serialno, keyid); + if (!shadow_info) + return gpg_error_from_syserror (); + + err = agent_shadow_key (pkbuf, shadow_info, &shdkey); + xfree (shadow_info); + if (err) + { + log_error ("shadowing the key failed: %s\n", gpg_strerror (err)); + return err; + } + + len = gcry_sexp_canon_len (shdkey, 0, NULL, NULL); + + if (maybe_update) /* Update mode. */ + { + fname = fname_from_keygrip (grip); + if (!fname) + { + err = gpg_error_from_syserror (); + goto leave; + } + + fp = es_fopen (fname, "rb+,mode=-rw"); + if (!fp) + { + err = gpg_error_from_syserror (); + log_error ("shadow key file '%s' disappeared\n", fname); + goto leave; + } + + /* See if an existing key is in extended format. */ + if (es_fread (&first, 1, 1, fp) != 1) + { + err = gpg_error_from_syserror (); + log_error ("error reading first byte from '%s': %s\n", + fname, gpg_strerror (err)); + goto leave; + } + + if (es_fseek (fp, 0, SEEK_SET)) + { + err = gpg_error_from_syserror (); + log_error ("error seeking in '%s': %s\n", fname, gpg_strerror (err)); + goto leave; + } + + /* "(first == '(')" indicates that the key is in the old format. */ + err = write_extended_private_key (maybe_update, + fname, fp, (first == '('), 0, + shdkey, len, + 0, serialno, keyid, + dispserialno); + fname = NULL; /* Ownership was transferred. */ + fp = NULL; /* Ditto. */ + } + else /* Standard mode */ + { + err = agent_write_private_key (grip, shdkey, len, force, 0, + serialno, keyid, dispserialno); + } + + leave: + xfree (fname); + es_fclose (fp); + xfree (shdkey); + if (err) + log_error ("error %s key: %s\n", maybe_update? "updating":"writing", + gpg_strerror (err)); + + return err; +} |