diff --git a/g10/export.c b/g10/export.c index 606f4eb22..ddd94cf05 100644 --- a/g10/export.c +++ b/g10/export.c @@ -1,2594 +1,2594 @@ /* export.c - Export keys in the OpenPGP defined format. * Copyright (C) 1998, 1999, 2000, 2001, 2002, 2003, 2004, * 2005, 2010 Free Software Foundation, Inc. * Copyright (C) 1998-2016 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 . */ #include #include #include #include #include #include "gpg.h" #include "options.h" #include "packet.h" #include "../common/status.h" #include "keydb.h" #include "../common/util.h" #include "main.h" #include "../common/i18n.h" #include "../common/membuf.h" #include "../common/host2net.h" #include "../common/zb32.h" #include "../common/recsel.h" #include "../common/mbox-util.h" #include "../common/init.h" #include "trustdb.h" #include "call-agent.h" #include "key-clean.h" /* An object to keep track of subkeys. */ struct subkey_list_s { struct subkey_list_s *next; u32 kid[2]; }; typedef struct subkey_list_s *subkey_list_t; /* An object to track statistics for export operations. */ struct export_stats_s { ulong count; /* Number of processed keys. */ ulong secret_count; /* Number of secret keys seen. */ ulong exported; /* Number of actual exported keys. */ }; /* Global variables to store the selectors created from * --export-filter keep-uid=EXPR. * --export-filter drop-subkey=EXPR. * --export-filter select=EXPR. * * FIXME: We should put this into the CTRL object but that requires a * lot more changes right now. */ static recsel_expr_t export_keep_uid; static recsel_expr_t export_drop_subkey; static recsel_expr_t export_select_filter; /* An object used for a linked list to implement the * push_export_filter/pop_export_filters functions. */ struct export_filter_attic_s { struct export_filter_attic_s *next; recsel_expr_t export_keep_uid; recsel_expr_t export_drop_subkey; recsel_expr_t export_select_filter; }; static struct export_filter_attic_s *export_filter_attic; /* Local prototypes. */ static int do_export (ctrl_t ctrl, strlist_t users, int secret, unsigned int options, export_stats_t stats); static int do_export_stream (ctrl_t ctrl, iobuf_t out, strlist_t users, int secret, kbnode_t *keyblock_out, unsigned int options, export_stats_t stats, int *any); static gpg_error_t print_pka_or_dane_records /**/ (iobuf_t out, kbnode_t keyblock, PKT_public_key *pk, const void *data, size_t datalen, int print_pka, int print_dane); static void cleanup_export_globals (void) { recsel_release (export_keep_uid); export_keep_uid = NULL; recsel_release (export_drop_subkey); export_drop_subkey = NULL; recsel_release (export_select_filter); export_select_filter = NULL; } /* Option parser for export options. See parse_options fro details. */ int parse_export_options(char *str,unsigned int *options,int noisy) { struct parse_options export_opts[]= { {"export-local-sigs",EXPORT_LOCAL_SIGS,NULL, N_("export signatures that are marked as local-only")}, {"export-attributes",EXPORT_ATTRIBUTES,NULL, N_("export attribute user IDs (generally photo IDs)")}, {"export-sensitive-revkeys",EXPORT_SENSITIVE_REVKEYS,NULL, N_("export revocation keys marked as \"sensitive\"")}, {"export-clean",EXPORT_CLEAN,NULL, N_("remove unusable parts from key during export")}, {"export-minimal",EXPORT_MINIMAL|EXPORT_CLEAN,NULL, N_("remove as much as possible from key during export")}, {"export-pka", EXPORT_PKA_FORMAT, NULL, NULL }, {"export-dane", EXPORT_DANE_FORMAT, NULL, NULL }, {"export-revocs", EXPORT_REVOCS, NULL, N_("export only revocation certificates") }, {"backup", EXPORT_BACKUP, NULL, N_("use the GnuPG key backup format")}, {"export-backup", EXPORT_BACKUP, NULL, NULL }, /* Aliases for backward compatibility */ {"include-local-sigs",EXPORT_LOCAL_SIGS,NULL,NULL}, {"include-attributes",EXPORT_ATTRIBUTES,NULL,NULL}, {"include-sensitive-revkeys",EXPORT_SENSITIVE_REVKEYS,NULL,NULL}, /* dummy */ {"export-unusable-sigs",0,NULL,NULL}, {"export-clean-sigs",0,NULL,NULL}, {"export-clean-uids",0,NULL,NULL}, {NULL,0,NULL,NULL} /* add tags for include revoked and disabled? */ }; int rc; rc = parse_options (str, options, export_opts, noisy); if (rc && (*options & EXPORT_BACKUP)) { /* Alter other options we want or don't want for restore. */ *options |= (EXPORT_LOCAL_SIGS | EXPORT_ATTRIBUTES | EXPORT_SENSITIVE_REVKEYS); *options &= ~(EXPORT_CLEAN | EXPORT_MINIMAL | EXPORT_PKA_FORMAT | EXPORT_DANE_FORMAT); } return rc; } /* Parse and set an export filter from string. STRING has the format * "NAME=EXPR" with NAME being the name of the filter. Spaces before * and after NAME are not allowed. If this function is called several * times all expressions for the same NAME are concatenated. * Supported filter names are: * * - keep-uid :: If the expression evaluates to true for a certain * user ID packet, that packet and all it dependencies * will be exported. The expression may use these * variables: * * - uid :: The entire user ID. * - mbox :: The mail box part of the user ID. * - primary :: Evaluate to true for the primary user ID. * * - drop-subkey :: If the expression evaluates to true for a subkey * packet that subkey and all it dependencies will be * remove from the keyblock. The expression may use these * variables: * * - secret :: 1 for a secret subkey, else 0. * - key_algo :: Public key algorithm id * * - select :: The key is only exported if the filter returns true. */ gpg_error_t parse_and_set_export_filter (const char *string) { gpg_error_t err; /* Auto register the cleanup function. */ register_mem_cleanup_func (cleanup_export_globals); if (!strncmp (string, "keep-uid=", 9)) err = recsel_parse_expr (&export_keep_uid, string+9); else if (!strncmp (string, "drop-subkey=", 12)) err = recsel_parse_expr (&export_drop_subkey, string+12); else if (!strncmp (string, "select=", 7)) err = recsel_parse_expr (&export_select_filter, string+7); else err = gpg_error (GPG_ERR_INV_NAME); return err; } /* Push the current export filters onto a stack so that new export * filters can be defined which will be active until the next * pop_export_filters or another push_export_filters. */ void push_export_filters (void) { struct export_filter_attic_s *item; item = xcalloc (1, sizeof *item); item->export_keep_uid = export_keep_uid; export_keep_uid = NULL; item->export_drop_subkey = export_drop_subkey; export_drop_subkey = NULL; item->export_select_filter = export_select_filter; export_select_filter = NULL; item->next = export_filter_attic; export_filter_attic = item; } /* Revert the last push_export_filters. */ void pop_export_filters (void) { struct export_filter_attic_s *item; item = export_filter_attic; if (!item) BUG (); /* No corresponding push. */ export_filter_attic = item->next; cleanup_export_globals (); export_keep_uid = item->export_keep_uid; export_drop_subkey = item->export_drop_subkey; export_select_filter = item->export_select_filter; } /* Create a new export stats object initialized to zero. On error returns NULL and sets ERRNO. */ export_stats_t export_new_stats (void) { export_stats_t stats; return xtrycalloc (1, sizeof *stats); } /* Release an export stats object. */ void export_release_stats (export_stats_t stats) { xfree (stats); } /* Print export statistics using the status interface. */ void export_print_stats (export_stats_t stats) { if (!stats) return; if (is_status_enabled ()) { char buf[15*20]; snprintf (buf, sizeof buf, "%lu %lu %lu", stats->count, stats->secret_count, stats->exported ); write_status_text (STATUS_EXPORT_RES, buf); } } /* * Export public keys (to stdout or to --output FILE). * * Depending on opt.armor the output is armored. OPTIONS are defined * in main.h. If USERS is NULL, all keys will be exported. STATS is * either an export stats object for update or NULL. * * This function is the core of "gpg --export". */ int export_pubkeys (ctrl_t ctrl, strlist_t users, unsigned int options, export_stats_t stats) { return do_export (ctrl, users, 0, options, stats); } /* * Export secret keys (to stdout or to --output FILE). * * Depending on opt.armor the output is armored. OPTIONS are defined * in main.h. If USERS is NULL, all secret keys will be exported. * STATS is either an export stats object for update or NULL. * * This function is the core of "gpg --export-secret-keys". */ int export_seckeys (ctrl_t ctrl, strlist_t users, unsigned int options, export_stats_t stats) { return do_export (ctrl, users, 1, options, stats); } /* * Export secret sub keys (to stdout or to --output FILE). * * This is the same as export_seckeys but replaces the primary key by * a stub key. Depending on opt.armor the output is armored. OPTIONS * are defined in main.h. If USERS is NULL, all secret subkeys will * be exported. STATS is either an export stats object for update or * NULL. * * This function is the core of "gpg --export-secret-subkeys". */ int export_secsubkeys (ctrl_t ctrl, strlist_t users, unsigned int options, export_stats_t stats) { return do_export (ctrl, users, 2, options, stats); } /* * Export a single key into a memory buffer. STATS is either an * export stats object for update or NULL. If PREFIX is not NULL * PREFIXLEN bytes from PREFIX are prepended to the R_DATA. */ gpg_error_t export_pubkey_buffer (ctrl_t ctrl, const char *keyspec, unsigned int options, const void *prefix, size_t prefixlen, export_stats_t stats, kbnode_t *r_keyblock, void **r_data, size_t *r_datalen) { gpg_error_t err; iobuf_t iobuf; int any; strlist_t helplist; *r_keyblock = NULL; *r_data = NULL; *r_datalen = 0; helplist = NULL; if (!add_to_strlist_try (&helplist, keyspec)) return gpg_error_from_syserror (); iobuf = iobuf_temp (); if (prefix && prefixlen) iobuf_write (iobuf, prefix, prefixlen); err = do_export_stream (ctrl, iobuf, helplist, 0, r_keyblock, options, stats, &any); if (!err && !any) err = gpg_error (GPG_ERR_NOT_FOUND); if (!err) { const void *src; size_t datalen; iobuf_flush_temp (iobuf); src = iobuf_get_temp_buffer (iobuf); datalen = iobuf_get_temp_length (iobuf); if (!datalen) err = gpg_error (GPG_ERR_NO_PUBKEY); else if (!(*r_data = xtrymalloc (datalen))) err = gpg_error_from_syserror (); else { memcpy (*r_data, src, datalen); *r_datalen = datalen; } } iobuf_close (iobuf); free_strlist (helplist); if (err && *r_keyblock) { release_kbnode (*r_keyblock); *r_keyblock = NULL; } return err; } /* Export the keys identified by the list of strings in USERS. If Secret is false public keys will be exported. With secret true secret keys will be exported; in this case 1 means the entire secret keyblock and 2 only the subkeys. OPTIONS are the export options to apply. */ static int do_export (ctrl_t ctrl, strlist_t users, int secret, unsigned int options, export_stats_t stats) { IOBUF out = NULL; int any, rc; armor_filter_context_t *afx = NULL; compress_filter_context_t zfx; memset( &zfx, 0, sizeof zfx); rc = open_outfile (-1, NULL, 0, !!secret, &out ); if (rc) return rc; if ( opt.armor && !(options & (EXPORT_PKA_FORMAT|EXPORT_DANE_FORMAT)) ) { afx = new_armor_context (); afx->what = secret? 5 : 1; push_armor_filter (afx, out); } rc = do_export_stream (ctrl, out, users, secret, NULL, options, stats, &any); if ( rc || !any ) iobuf_cancel (out); else iobuf_close (out); release_armor_context (afx); return rc; } /* Release an entire subkey list. */ static void release_subkey_list (subkey_list_t list) { while (list) { subkey_list_t tmp = list->next;; xfree (list); list = tmp; } } /* Returns true if NODE is a subkey and contained in LIST. */ static int subkey_in_list_p (subkey_list_t list, KBNODE node) { if (node->pkt->pkttype == PKT_PUBLIC_SUBKEY || node->pkt->pkttype == PKT_SECRET_SUBKEY ) { u32 kid[2]; keyid_from_pk (node->pkt->pkt.public_key, kid); for (; list; list = list->next) if (list->kid[0] == kid[0] && list->kid[1] == kid[1]) return 1; } return 0; } /* Allocate a new subkey list item from NODE. */ static subkey_list_t new_subkey_list_item (KBNODE node) { subkey_list_t list = xcalloc (1, sizeof *list); if (node->pkt->pkttype == PKT_PUBLIC_SUBKEY || node->pkt->pkttype == PKT_SECRET_SUBKEY) keyid_from_pk (node->pkt->pkt.public_key, list->kid); return list; } /* Helper function to check whether the subkey at NODE actually matches the description at DESC. The function returns true if the key under question has been specified by an exact specification (keyID or fingerprint) and does match the one at NODE. It is assumed that the packet at NODE is either a public or secret subkey. */ int exact_subkey_match_p (KEYDB_SEARCH_DESC *desc, kbnode_t node) { u32 kid[2]; byte fpr[MAX_FINGERPRINT_LEN]; size_t fprlen; int result = 0; switch(desc->mode) { case KEYDB_SEARCH_MODE_SHORT_KID: case KEYDB_SEARCH_MODE_LONG_KID: keyid_from_pk (node->pkt->pkt.public_key, kid); break; case KEYDB_SEARCH_MODE_FPR16: case KEYDB_SEARCH_MODE_FPR20: case KEYDB_SEARCH_MODE_FPR: fingerprint_from_pk (node->pkt->pkt.public_key, fpr,&fprlen); break; default: break; } switch(desc->mode) { case KEYDB_SEARCH_MODE_SHORT_KID: if (desc->u.kid[1] == kid[1]) result = 1; break; case KEYDB_SEARCH_MODE_LONG_KID: if (desc->u.kid[0] == kid[0] && desc->u.kid[1] == kid[1]) result = 1; break; case KEYDB_SEARCH_MODE_FPR16: if (!memcmp (desc->u.fpr, fpr, 16)) result = 1; break; case KEYDB_SEARCH_MODE_FPR20: case KEYDB_SEARCH_MODE_FPR: if (!memcmp (desc->u.fpr, fpr, 20)) result = 1; break; default: break; } return result; } /* Return an error if the key represented by the S-expression S_KEY * and the OpenPGP key represented by PK do not use the same curve. */ static gpg_error_t match_curve_skey_pk (gcry_sexp_t s_key, PKT_public_key *pk) { gcry_sexp_t curve = NULL; gcry_sexp_t flags = NULL; char *curve_str = NULL; char *flag; const char *oidstr = NULL; gcry_mpi_t curve_as_mpi = NULL; gpg_error_t err; int is_eddsa = 0; int idx = 0; if (!(pk->pubkey_algo==PUBKEY_ALGO_ECDH || pk->pubkey_algo==PUBKEY_ALGO_ECDSA || pk->pubkey_algo==PUBKEY_ALGO_EDDSA)) return gpg_error (GPG_ERR_PUBKEY_ALGO); curve = gcry_sexp_find_token (s_key, "curve", 0); if (!curve) { log_error ("no reported curve\n"); return gpg_error (GPG_ERR_UNKNOWN_CURVE); } curve_str = gcry_sexp_nth_string (curve, 1); gcry_sexp_release (curve); curve = NULL; if (!curve_str) { log_error ("no curve name\n"); return gpg_error (GPG_ERR_UNKNOWN_CURVE); } oidstr = openpgp_curve_to_oid (curve_str, NULL, NULL); if (!oidstr) { log_error ("no OID known for curve '%s'\n", curve_str); xfree (curve_str); return gpg_error (GPG_ERR_UNKNOWN_CURVE); } xfree (curve_str); err = openpgp_oid_from_str (oidstr, &curve_as_mpi); if (err) return err; if (gcry_mpi_cmp (pk->pkey[0], curve_as_mpi)) { log_error ("curves do not match\n"); gcry_mpi_release (curve_as_mpi); return gpg_error (GPG_ERR_INV_CURVE); } gcry_mpi_release (curve_as_mpi); flags = gcry_sexp_find_token (s_key, "flags", 0); if (flags) { for (idx = 1; idx < gcry_sexp_length (flags); idx++) { flag = gcry_sexp_nth_string (flags, idx); if (flag && (strcmp ("eddsa", flag) == 0)) is_eddsa = 1; gcry_free (flag); } } if (is_eddsa != (pk->pubkey_algo == PUBKEY_ALGO_EDDSA)) { log_error ("disagreement about EdDSA\n"); err = gpg_error (GPG_ERR_INV_CURVE); } return err; } /* Return a canonicalized public key algoithms. This is used to compare different flavors of algorithms (e.g. ELG and ELG_E are considered the same). */ static enum gcry_pk_algos canon_pk_algo (enum gcry_pk_algos algo) { switch (algo) { case GCRY_PK_RSA: case GCRY_PK_RSA_E: case GCRY_PK_RSA_S: return GCRY_PK_RSA; case GCRY_PK_ELG: case GCRY_PK_ELG_E: return GCRY_PK_ELG; case GCRY_PK_ECC: case GCRY_PK_ECDSA: case GCRY_PK_ECDH: return GCRY_PK_ECC; default: return algo; } } /* Take a cleartext dump of a secret key in PK and change the * parameter array in PK to include the secret parameters. */ static gpg_error_t cleartext_secret_key_to_openpgp (gcry_sexp_t s_key, PKT_public_key *pk) { gpg_error_t err; gcry_sexp_t top_list; gcry_sexp_t key = NULL; char *key_type = NULL; enum gcry_pk_algos pk_algo; struct seckey_info *ski; int idx, sec_start; gcry_mpi_t pub_params[10] = { NULL }; /* we look for a private-key, then the first element in it tells us the type */ top_list = gcry_sexp_find_token (s_key, "private-key", 0); if (!top_list) goto bad_seckey; /* ignore all S-expression after the first sublist -- we assume that they are comments or otherwise irrelevant to OpenPGP */ if (gcry_sexp_length(top_list) < 2) goto bad_seckey; key = gcry_sexp_nth (top_list, 1); if (!key) goto bad_seckey; key_type = gcry_sexp_nth_string(key, 0); pk_algo = gcry_pk_map_name (key_type); log_assert (!pk->seckey_info); pk->seckey_info = ski = xtrycalloc (1, sizeof *ski); if (!ski) { err = gpg_error_from_syserror (); goto leave; } switch (canon_pk_algo (pk_algo)) { case GCRY_PK_RSA: if (!is_RSA (pk->pubkey_algo)) goto bad_pubkey_algo; err = gcry_sexp_extract_param (key, NULL, "ne", &pub_params[0], &pub_params[1], NULL); for (idx=0; idx < 2 && !err; idx++) if (gcry_mpi_cmp(pk->pkey[idx], pub_params[idx])) err = gpg_error (GPG_ERR_BAD_PUBKEY); if (!err) { for (idx = 2; idx < 6 && !err; idx++) { gcry_mpi_release (pk->pkey[idx]); pk->pkey[idx] = NULL; } err = gcry_sexp_extract_param (key, NULL, "dpqu", &pk->pkey[2], &pk->pkey[3], &pk->pkey[4], &pk->pkey[5], NULL); } if (!err) { for (idx = 2; idx < 6; idx++) ski->csum += checksum_mpi (pk->pkey[idx]); } break; case GCRY_PK_DSA: if (!is_DSA (pk->pubkey_algo)) goto bad_pubkey_algo; err = gcry_sexp_extract_param (key, NULL, "pqgy", &pub_params[0], &pub_params[1], &pub_params[2], &pub_params[3], NULL); for (idx=0; idx < 4 && !err; idx++) if (gcry_mpi_cmp(pk->pkey[idx], pub_params[idx])) err = gpg_error (GPG_ERR_BAD_PUBKEY); if (!err) { gcry_mpi_release (pk->pkey[4]); pk->pkey[4] = NULL; err = gcry_sexp_extract_param (key, NULL, "x", &pk->pkey[4], NULL); } if (!err) ski->csum += checksum_mpi (pk->pkey[4]); break; case GCRY_PK_ELG: if (!is_ELGAMAL (pk->pubkey_algo)) goto bad_pubkey_algo; err = gcry_sexp_extract_param (key, NULL, "pgy", &pub_params[0], &pub_params[1], &pub_params[2], NULL); for (idx=0; idx < 3 && !err; idx++) if (gcry_mpi_cmp(pk->pkey[idx], pub_params[idx])) err = gpg_error (GPG_ERR_BAD_PUBKEY); if (!err) { gcry_mpi_release (pk->pkey[3]); pk->pkey[3] = NULL; err = gcry_sexp_extract_param (key, NULL, "x", &pk->pkey[3], NULL); } if (!err) ski->csum += checksum_mpi (pk->pkey[3]); break; case GCRY_PK_ECC: err = match_curve_skey_pk (key, pk); if (err) goto leave; if (!err) err = gcry_sexp_extract_param (key, NULL, "q", &pub_params[0], NULL); if (!err && (gcry_mpi_cmp(pk->pkey[1], pub_params[0]))) err = gpg_error (GPG_ERR_BAD_PUBKEY); sec_start = 2; if (pk->pubkey_algo == PUBKEY_ALGO_ECDH) sec_start += 1; if (!err) { gcry_mpi_release (pk->pkey[sec_start]); pk->pkey[sec_start] = NULL; err = gcry_sexp_extract_param (key, NULL, "d", &pk->pkey[sec_start], NULL); } if (!err) ski->csum += checksum_mpi (pk->pkey[sec_start]); break; default: pk->seckey_info = NULL; xfree (ski); err = gpg_error (GPG_ERR_NOT_IMPLEMENTED); break; } leave: gcry_sexp_release (top_list); gcry_sexp_release (key); gcry_free (key_type); for (idx=0; idx < DIM(pub_params); idx++) gcry_mpi_release (pub_params[idx]); return err; bad_pubkey_algo: err = gpg_error (GPG_ERR_PUBKEY_ALGO); goto leave; bad_seckey: err = gpg_error (GPG_ERR_BAD_SECKEY); goto leave; } /* Use the key transfer format given in S_PGP to create the secinfo structure in PK and change the parameter array in PK to include the secret parameters. */ static gpg_error_t transfer_format_to_openpgp (gcry_sexp_t s_pgp, PKT_public_key *pk) { gpg_error_t err; gcry_sexp_t top_list; gcry_sexp_t list = NULL; char *curve = NULL; const char *value; size_t valuelen; char *string; int idx; int is_v4, is_protected; enum gcry_pk_algos pk_algo; int protect_algo = 0; char iv[16]; int ivlen = 0; int s2k_mode = 0; int s2k_algo = 0; byte s2k_salt[8]; u32 s2k_count = 0; int is_ecdh = 0; size_t npkey, nskey; gcry_mpi_t skey[10]; /* We support up to 9 parameters. */ int skeyidx = 0; struct seckey_info *ski; /* gcry_log_debugsxp ("transferkey", s_pgp); */ top_list = gcry_sexp_find_token (s_pgp, "openpgp-private-key", 0); if (!top_list) goto bad_seckey; list = gcry_sexp_find_token (top_list, "version", 0); if (!list) goto bad_seckey; value = gcry_sexp_nth_data (list, 1, &valuelen); if (!value || valuelen != 1 || !(value[0] == '3' || value[0] == '4')) goto bad_seckey; is_v4 = (value[0] == '4'); gcry_sexp_release (list); list = gcry_sexp_find_token (top_list, "protection", 0); if (!list) goto bad_seckey; value = gcry_sexp_nth_data (list, 1, &valuelen); if (!value) goto bad_seckey; if (valuelen == 4 && !memcmp (value, "sha1", 4)) is_protected = 2; else if (valuelen == 3 && !memcmp (value, "sum", 3)) is_protected = 1; else if (valuelen == 4 && !memcmp (value, "none", 4)) is_protected = 0; else goto bad_seckey; if (is_protected) { string = gcry_sexp_nth_string (list, 2); if (!string) goto bad_seckey; protect_algo = gcry_cipher_map_name (string); xfree (string); value = gcry_sexp_nth_data (list, 3, &valuelen); if (!value || !valuelen || valuelen > sizeof iv) goto bad_seckey; memcpy (iv, value, valuelen); ivlen = valuelen; string = gcry_sexp_nth_string (list, 4); if (!string) goto bad_seckey; s2k_mode = strtol (string, NULL, 10); xfree (string); string = gcry_sexp_nth_string (list, 5); if (!string) goto bad_seckey; s2k_algo = gcry_md_map_name (string); xfree (string); value = gcry_sexp_nth_data (list, 6, &valuelen); if (!value || !valuelen || valuelen > sizeof s2k_salt) goto bad_seckey; memcpy (s2k_salt, value, valuelen); string = gcry_sexp_nth_string (list, 7); if (!string) goto bad_seckey; s2k_count = strtoul (string, NULL, 10); xfree (string); } /* Parse the gcrypt PK algo and check that it is okay. */ gcry_sexp_release (list); list = gcry_sexp_find_token (top_list, "algo", 0); if (!list) goto bad_seckey; string = gcry_sexp_nth_string (list, 1); if (!string) goto bad_seckey; pk_algo = gcry_pk_map_name (string); xfree (string); string = NULL; if (gcry_pk_algo_info (pk_algo, GCRYCTL_GET_ALGO_NPKEY, NULL, &npkey) || gcry_pk_algo_info (pk_algo, GCRYCTL_GET_ALGO_NSKEY, NULL, &nskey) || !npkey || npkey >= nskey) goto bad_seckey; /* Check that the pubkey algo matches the one from the public key. */ switch (canon_pk_algo (pk_algo)) { case GCRY_PK_RSA: if (!is_RSA (pk->pubkey_algo)) pk_algo = 0; /* Does not match. */ break; case GCRY_PK_DSA: if (!is_DSA (pk->pubkey_algo)) pk_algo = 0; /* Does not match. */ break; case GCRY_PK_ELG: if (!is_ELGAMAL (pk->pubkey_algo)) pk_algo = 0; /* Does not match. */ break; case GCRY_PK_ECC: if (pk->pubkey_algo == PUBKEY_ALGO_ECDSA) ; else if (pk->pubkey_algo == PUBKEY_ALGO_ECDH) is_ecdh = 1; else if (pk->pubkey_algo == PUBKEY_ALGO_EDDSA) ; else pk_algo = 0; /* Does not match. */ /* For ECC we do not have the domain parameters thus fix our info. */ npkey = 1; nskey = 2; break; default: pk_algo = 0; /* Oops. */ break; } if (!pk_algo) { err = gpg_error (GPG_ERR_PUBKEY_ALGO); goto leave; } /* This check has to go after the ecc adjustments. */ if (nskey > PUBKEY_MAX_NSKEY) goto bad_seckey; /* Parse the key parameters. */ gcry_sexp_release (list); list = gcry_sexp_find_token (top_list, "skey", 0); if (!list) goto bad_seckey; for (idx=0;;) { int is_enc; value = gcry_sexp_nth_data (list, ++idx, &valuelen); if (!value && skeyidx >= npkey) break; /* Ready. */ /* Check for too many parameters. Note that depending on the protection mode and version number we may see less than NSKEY (but at least NPKEY+1) parameters. */ if (idx >= 2*nskey) goto bad_seckey; if (skeyidx >= DIM (skey)-1) goto bad_seckey; if (!value || valuelen != 1 || !(value[0] == '_' || value[0] == 'e')) goto bad_seckey; is_enc = (value[0] == 'e'); value = gcry_sexp_nth_data (list, ++idx, &valuelen); if (!value || !valuelen) goto bad_seckey; if (is_enc) { void *p = xtrymalloc (valuelen); if (!p) goto outofmem; memcpy (p, value, valuelen); skey[skeyidx] = gcry_mpi_set_opaque (NULL, p, valuelen*8); if (!skey[skeyidx]) goto outofmem; } else { if (gcry_mpi_scan (skey + skeyidx, GCRYMPI_FMT_STD, value, valuelen, NULL)) goto bad_seckey; } skeyidx++; } skey[skeyidx++] = NULL; gcry_sexp_release (list); list = NULL; /* We have no need for the CSUM value thus we don't parse it. */ /* list = gcry_sexp_find_token (top_list, "csum", 0); */ /* if (list) */ /* { */ /* string = gcry_sexp_nth_string (list, 1); */ /* if (!string) */ /* goto bad_seckey; */ /* desired_csum = strtoul (string, NULL, 10); */ /* xfree (string); */ /* } */ /* else */ /* desired_csum = 0; */ /* gcry_sexp_release (list); list = NULL; */ /* Get the curve name if any, */ list = gcry_sexp_find_token (top_list, "curve", 0); if (list) { curve = gcry_sexp_nth_string (list, 1); gcry_sexp_release (list); list = NULL; } gcry_sexp_release (top_list); top_list = NULL; /* log_debug ("XXX is_v4=%d\n", is_v4); */ /* log_debug ("XXX pubkey_algo=%d\n", pubkey_algo); */ /* log_debug ("XXX is_protected=%d\n", is_protected); */ /* log_debug ("XXX protect_algo=%d\n", protect_algo); */ /* log_printhex (iv, ivlen, "XXX iv"); */ /* log_debug ("XXX ivlen=%d\n", ivlen); */ /* log_debug ("XXX s2k_mode=%d\n", s2k_mode); */ /* log_debug ("XXX s2k_algo=%d\n", s2k_algo); */ /* log_printhex (s2k_salt, sizeof s2k_salt, "XXX s2k_salt"); */ /* log_debug ("XXX s2k_count=%lu\n", (unsigned long)s2k_count); */ /* for (idx=0; skey[idx]; idx++) */ /* { */ /* int is_enc = gcry_mpi_get_flag (skey[idx], GCRYMPI_FLAG_OPAQUE); */ /* log_info ("XXX skey[%d]%s:", idx, is_enc? " (enc)":""); */ /* if (is_enc) */ /* { */ /* void *p; */ /* unsigned int nbits; */ /* p = gcry_mpi_get_opaque (skey[idx], &nbits); */ /* log_printhex ( p, (nbits+7)/8, NULL); */ /* } */ /* else */ /* gcry_mpi_dump (skey[idx]); */ /* log_printf ("\n"); */ /* } */ if (!is_v4 || is_protected != 2 ) { /* We only support the v4 format and a SHA-1 checksum. */ err = gpg_error (GPG_ERR_NOT_IMPLEMENTED); goto leave; } /* We need to change the received parameters for ECC algorithms. The transfer format has the curve name and the parameters separate. We put them all into the SKEY array. */ if (canon_pk_algo (pk_algo) == GCRY_PK_ECC) { const char *oidstr; /* Assert that all required parameters are available. We also check that the array does not contain more parameters than needed (this was used by some beta versions of 2.1. */ if (!curve || !skey[0] || !skey[1] || skey[2]) { err = gpg_error (GPG_ERR_INTERNAL); goto leave; } oidstr = openpgp_curve_to_oid (curve, NULL, NULL); if (!oidstr) { log_error ("no OID known for curve '%s'\n", curve); err = gpg_error (GPG_ERR_UNKNOWN_CURVE); goto leave; } /* Put the curve's OID into the MPI array. This requires that we shift Q and D. For ECDH also insert the KDF parms. */ if (is_ecdh) { skey[4] = NULL; skey[3] = skey[1]; skey[2] = gcry_mpi_copy (pk->pkey[2]); } else { skey[3] = NULL; skey[2] = skey[1]; } skey[1] = skey[0]; skey[0] = NULL; err = openpgp_oid_from_str (oidstr, skey + 0); if (err) goto leave; /* Fixup the NPKEY and NSKEY to match OpenPGP reality. */ npkey = 2 + is_ecdh; nskey = 3 + is_ecdh; /* for (idx=0; skey[idx]; idx++) */ /* { */ /* log_info ("YYY skey[%d]:", idx); */ /* if (gcry_mpi_get_flag (skey[idx], GCRYMPI_FLAG_OPAQUE)) */ /* { */ /* void *p; */ /* unsigned int nbits; */ /* p = gcry_mpi_get_opaque (skey[idx], &nbits); */ /* log_printhex (p, (nbits+7)/8, NULL); */ /* } */ /* else */ /* gcry_mpi_dump (skey[idx]); */ /* log_printf ("\n"); */ /* } */ } /* Do some sanity checks. */ if (s2k_count > 255) { /* We expect an already encoded S2K count. */ err = gpg_error (GPG_ERR_INV_DATA); goto leave; } err = openpgp_cipher_test_algo (protect_algo); if (err) goto leave; err = openpgp_md_test_algo (s2k_algo); if (err) goto leave; /* Check that the public key parameters match. Note that since Libgcrypt 1.5 gcry_mpi_cmp handles opaque MPI correctly. */ for (idx=0; idx < npkey; idx++) if (gcry_mpi_cmp (pk->pkey[idx], skey[idx])) { err = gpg_error (GPG_ERR_BAD_PUBKEY); goto leave; } /* Check that the first secret key parameter in SKEY is encrypted and that there are no more secret key parameters. The latter is guaranteed by the v4 packet format. */ if (!gcry_mpi_get_flag (skey[npkey], GCRYMPI_FLAG_OPAQUE)) goto bad_seckey; if (npkey+1 < DIM (skey) && skey[npkey+1]) goto bad_seckey; /* Check that the secret key parameters in PK are all set to NULL. */ for (idx=npkey; idx < nskey; idx++) if (pk->pkey[idx]) goto bad_seckey; /* Now build the protection info. */ pk->seckey_info = ski = xtrycalloc (1, sizeof *ski); if (!ski) { err = gpg_error_from_syserror (); goto leave; } ski->is_protected = 1; ski->sha1chk = 1; ski->algo = protect_algo; ski->s2k.mode = s2k_mode; ski->s2k.hash_algo = s2k_algo; log_assert (sizeof ski->s2k.salt == sizeof s2k_salt); memcpy (ski->s2k.salt, s2k_salt, sizeof s2k_salt); ski->s2k.count = s2k_count; log_assert (ivlen <= sizeof ski->iv); memcpy (ski->iv, iv, ivlen); ski->ivlen = ivlen; /* Store the protected secret key parameter. */ pk->pkey[npkey] = skey[npkey]; skey[npkey] = NULL; /* That's it. */ leave: gcry_free (curve); gcry_sexp_release (list); gcry_sexp_release (top_list); for (idx=0; idx < skeyidx; idx++) gcry_mpi_release (skey[idx]); return err; bad_seckey: err = gpg_error (GPG_ERR_BAD_SECKEY); goto leave; outofmem: err = gpg_error (GPG_ERR_ENOMEM); goto leave; } /* Print an "EXPORTED" status line. PK is the primary public key. */ static void print_status_exported (PKT_public_key *pk) { char *hexfpr; if (!is_status_enabled ()) return; hexfpr = hexfingerprint (pk, NULL, 0); write_status_text (STATUS_EXPORTED, hexfpr? hexfpr : "[?]"); xfree (hexfpr); } /* * Receive a secret key from agent specified by HEXGRIP. * * Since the key data from the agent is encrypted, decrypt it using * CIPHERHD context. Then, parse the decrypted key data into transfer * format, and put secret parameters into PK. * * If CLEARTEXT is 0, store the secret key material * passphrase-protected. Otherwise, store secret key material in the * clear. * * CACHE_NONCE_ADDR is used to share nonce for multple key retrievals. */ gpg_error_t receive_seckey_from_agent (ctrl_t ctrl, gcry_cipher_hd_t cipherhd, int cleartext, char **cache_nonce_addr, const char *hexgrip, PKT_public_key *pk) { gpg_error_t err = 0; unsigned char *wrappedkey = NULL; size_t wrappedkeylen; unsigned char *key = NULL; size_t keylen, realkeylen; gcry_sexp_t s_skey; char *prompt; if (opt.verbose) log_info ("key %s: asking agent for the secret parts\n", hexgrip); prompt = gpg_format_keydesc (ctrl, pk, FORMAT_KEYDESC_EXPORT,1); err = agent_export_key (ctrl, hexgrip, prompt, !cleartext, cache_nonce_addr, &wrappedkey, &wrappedkeylen, pk->keyid, pk->main_keyid, pk->pubkey_algo); xfree (prompt); if (err) goto unwraperror; if (wrappedkeylen < 24) { err = gpg_error (GPG_ERR_INV_LENGTH); goto unwraperror; } keylen = wrappedkeylen - 8; key = xtrymalloc_secure (keylen); if (!key) { err = gpg_error_from_syserror (); goto unwraperror; } err = gcry_cipher_decrypt (cipherhd, key, keylen, wrappedkey, wrappedkeylen); if (err) goto unwraperror; realkeylen = gcry_sexp_canon_len (key, keylen, NULL, &err); if (!realkeylen) goto unwraperror; /* Invalid csexp. */ err = gcry_sexp_sscan (&s_skey, NULL, key, realkeylen); if (!err) { if (cleartext) err = cleartext_secret_key_to_openpgp (s_skey, pk); else err = transfer_format_to_openpgp (s_skey, pk); gcry_sexp_release (s_skey); } unwraperror: xfree (key); xfree (wrappedkey); if (err) { log_error ("key %s: error receiving key from agent:" " %s%s\n", hexgrip, gpg_strerror (err), gpg_err_code (err) == GPG_ERR_FULLY_CANCELED? "":_(" - skipped")); } return err; } /* Write KEYBLOCK either to stdout or to the file set with the * --output option. This is a simplified version of do_export_stream * which supports only a few export options. */ gpg_error_t write_keyblock_to_output (kbnode_t keyblock, int with_armor, unsigned int options) { gpg_error_t err; const char *fname; iobuf_t out; kbnode_t node; armor_filter_context_t *afx = NULL; iobuf_t out_help = NULL; PKT_public_key *pk = NULL; fname = opt.outfile? opt.outfile : "-"; if (is_secured_filename (fname) ) return gpg_error (GPG_ERR_EPERM); out = iobuf_create (fname, 0); if (!out) { err = gpg_error_from_syserror (); log_error(_("can't create '%s': %s\n"), fname, gpg_strerror (err)); return err; } if (opt.verbose) log_info (_("writing to '%s'\n"), iobuf_get_fname_nonnull (out)); if ((options & (EXPORT_PKA_FORMAT|EXPORT_DANE_FORMAT))) { with_armor = 0; out_help = iobuf_temp (); } if (with_armor) { afx = new_armor_context (); afx->what = 1; push_armor_filter (afx, out); } for (node = keyblock; node; node = node->next) { if (is_deleted_kbnode (node)) continue; if (node->pkt->pkttype == PKT_RING_TRUST) continue; /* Skip - they should not be here anyway. */ if (!pk && (node->pkt->pkttype == PKT_PUBLIC_KEY || node->pkt->pkttype == PKT_SECRET_KEY)) pk = node->pkt->pkt.public_key; if ((options & EXPORT_BACKUP)) err = build_packet_and_meta (out_help? out_help : out, node->pkt); else err = build_packet (out_help? out_help : out, node->pkt); if (err) { log_error ("build_packet(%d) failed: %s\n", node->pkt->pkttype, gpg_strerror (err) ); goto leave; } } err = 0; if (out_help && pk) { const void *data; size_t datalen; iobuf_flush_temp (out_help); data = iobuf_get_temp_buffer (out_help); datalen = iobuf_get_temp_length (out_help); err = print_pka_or_dane_records (out, keyblock, pk, data, datalen, (options & EXPORT_PKA_FORMAT), (options & EXPORT_DANE_FORMAT)); } leave: if (err) iobuf_cancel (out); else iobuf_close (out); iobuf_cancel (out_help); release_armor_context (afx); return err; } /* * Apply the keep-uid filter to the keyblock. The deleted nodes are * marked and thus the caller should call commit_kbnode afterwards. * KEYBLOCK must not have any blocks marked as deleted. */ static void apply_keep_uid_filter (ctrl_t ctrl, kbnode_t keyblock, recsel_expr_t selector) { kbnode_t node; struct impex_filter_parm_s parm; parm.ctrl = ctrl; for (node = keyblock->next; node; node = node->next ) { if (node->pkt->pkttype == PKT_USER_ID) { parm.node = node; if (!recsel_select (selector, impex_filter_getval, &parm)) { /* log_debug ("keep-uid: deleting '%s'\n", */ /* node->pkt->pkt.user_id->name); */ /* The UID packet and all following packets up to the * next UID or a subkey. */ delete_kbnode (node); for (; node->next && node->next->pkt->pkttype != PKT_USER_ID && node->next->pkt->pkttype != PKT_PUBLIC_SUBKEY && node->next->pkt->pkttype != PKT_SECRET_SUBKEY ; node = node->next) delete_kbnode (node->next); } /* else */ /* log_debug ("keep-uid: keeping '%s'\n", */ /* node->pkt->pkt.user_id->name); */ } } } /* * Apply the drop-subkey filter to the keyblock. The deleted nodes are * marked and thus the caller should call commit_kbnode afterwards. * KEYBLOCK must not have any blocks marked as deleted. */ static void apply_drop_subkey_filter (ctrl_t ctrl, kbnode_t keyblock, recsel_expr_t selector) { kbnode_t node; struct impex_filter_parm_s parm; parm.ctrl = ctrl; for (node = keyblock->next; node; node = node->next ) { if (node->pkt->pkttype == PKT_PUBLIC_SUBKEY || node->pkt->pkttype == PKT_SECRET_SUBKEY) { parm.node = node; if (recsel_select (selector, impex_filter_getval, &parm)) { /*log_debug ("drop-subkey: deleting a key\n");*/ /* The subkey packet and all following packets up to the * next subkey. */ delete_kbnode (node); for (; node->next && node->next->pkt->pkttype != PKT_PUBLIC_SUBKEY && node->next->pkt->pkttype != PKT_SECRET_SUBKEY ; node = node->next) delete_kbnode (node->next); } } } } /* Print DANE or PKA records for all user IDs in KEYBLOCK to OUT. The * data for the record is taken from (DATA,DATELEN). PK is the public * key packet with the primary key. */ static gpg_error_t print_pka_or_dane_records (iobuf_t out, kbnode_t keyblock, PKT_public_key *pk, const void *data, size_t datalen, int print_pka, int print_dane) { gpg_error_t err = 0; kbnode_t kbctx, node; PKT_user_id *uid; char *mbox = NULL; char hashbuf[32]; char *hash = NULL; char *domain; const char *s; unsigned int len; estream_t fp = NULL; char *hexdata = NULL; char *hexfpr; hexfpr = hexfingerprint (pk, NULL, 0); if (!hexfpr) { err = gpg_error_from_syserror (); goto leave; } hexdata = bin2hex (data, datalen, NULL); if (!hexdata) { err = gpg_error_from_syserror (); goto leave; } ascii_strlwr (hexdata); fp = es_fopenmem (0, "rw,samethread"); if (!fp) { err = gpg_error_from_syserror (); goto leave; } for (kbctx = NULL; (node = walk_kbnode (keyblock, &kbctx, 0));) { if (node->pkt->pkttype != PKT_USER_ID) continue; uid = node->pkt->pkt.user_id; if (uid->flags.expired || uid->flags.revoked) continue; xfree (mbox); mbox = mailbox_from_userid (uid->name); if (!mbox) continue; domain = strchr (mbox, '@'); *domain++ = 0; if (print_pka) { es_fprintf (fp, "$ORIGIN _pka.%s.\n; %s\n; ", domain, hexfpr); print_utf8_buffer (fp, uid->name, uid->len); es_putc ('\n', fp); gcry_md_hash_buffer (GCRY_MD_SHA1, hashbuf, mbox, strlen (mbox)); xfree (hash); hash = zb32_encode (hashbuf, 8*20); if (!hash) { err = gpg_error_from_syserror (); goto leave; } len = strlen (hexfpr)/2; es_fprintf (fp, "%s TYPE37 \\# %u 0006 0000 00 %02X %s\n\n", hash, 6 + len, len, hexfpr); } if (print_dane && hexdata) { es_fprintf (fp, "$ORIGIN _openpgpkey.%s.\n; %s\n; ", domain, hexfpr); print_utf8_buffer (fp, uid->name, uid->len); es_putc ('\n', fp); gcry_md_hash_buffer (GCRY_MD_SHA256, hashbuf, mbox, strlen (mbox)); xfree (hash); hash = bin2hex (hashbuf, 28, NULL); if (!hash) { err = gpg_error_from_syserror (); goto leave; } ascii_strlwr (hash); len = strlen (hexdata)/2; es_fprintf (fp, "%s TYPE61 \\# %u (\n", hash, len); for (s = hexdata; ;) { es_fprintf (fp, "\t%.64s\n", s); if (strlen (s) < 64) break; s += 64; } es_fputs ("\t)\n\n", fp); } } /* Make sure it is a string and write it. */ es_fputc (0, fp); { void *vp; if (es_fclose_snatch (fp, &vp, NULL)) { err = gpg_error_from_syserror (); goto leave; } fp = NULL; iobuf_writestr (out, vp); es_free (vp); } err = 0; leave: xfree (hash); xfree (mbox); es_fclose (fp); xfree (hexdata); xfree (hexfpr); return err; } /* Helper for do_export_stream which writes one keyblock to OUT. */ static gpg_error_t do_export_one_keyblock (ctrl_t ctrl, kbnode_t keyblock, u32 *keyid, iobuf_t out, int secret, unsigned int options, export_stats_t stats, int *any, KEYDB_SEARCH_DESC *desc, size_t ndesc, size_t descindex, gcry_cipher_hd_t cipherhd) { gpg_error_t err = gpg_error (GPG_ERR_NOT_FOUND); char *cache_nonce = NULL; subkey_list_t subkey_list = NULL; /* Track already processed subkeys. */ int skip_until_subkey = 0; int cleartext = 0; char *hexgrip = NULL; char *serialno = NULL; PKT_public_key *pk; u32 subkidbuf[2], *subkid; kbnode_t kbctx, node; /* NB: walk_kbnode skips packets marked as deleted. */ for (kbctx=NULL; (node = walk_kbnode (keyblock, &kbctx, 0)); ) { if (skip_until_subkey) { if (node->pkt->pkttype == PKT_PUBLIC_SUBKEY) skip_until_subkey = 0; else continue; } /* We used to use comment packets, but not any longer. In * case we still have comments on a key, strip them here * before we call build_packet(). */ if (node->pkt->pkttype == PKT_COMMENT) continue; /* Skip ring trust packets - they should not ne here anyway. */ if (node->pkt->pkttype == PKT_RING_TRUST) continue; /* If exact is set, then we only export what was requested * (plus the primary key, if the user didn't specifically * request it). */ if (desc[descindex].exact && node->pkt->pkttype == PKT_PUBLIC_SUBKEY) { if (!exact_subkey_match_p (desc+descindex, node)) { /* Before skipping this subkey, check whether any * other description wants an exact match on a * subkey and include that subkey into the output * too. Need to add this subkey to a list so that * it won't get processed a second time. * * So the first step here is to check that list and * skip in any case if the key is in that list. * * We need this whole mess because the import * function of GnuPG < 2.1 is not able to merge * secret keys and thus it is useless to output them * as two separate keys and have import merge them. */ if (subkey_in_list_p (subkey_list, node)) skip_until_subkey = 1; /* Already processed this one. */ else { size_t j; for (j=0; j < ndesc; j++) if (j != descindex && desc[j].exact && exact_subkey_match_p (desc+j, node)) break; if (!(j < ndesc)) skip_until_subkey = 1; /* No other one matching. */ } } if (skip_until_subkey) continue; /* Mark this one as processed. */ { subkey_list_t tmp = new_subkey_list_item (node); tmp->next = subkey_list; subkey_list = tmp; } } if (node->pkt->pkttype == PKT_SIGNATURE) { /* Do not export packets which are marked as not * exportable. */ if (!(options & EXPORT_LOCAL_SIGS) && !node->pkt->pkt.signature->flags.exportable) continue; /* not exportable */ /* Do not export packets with a "sensitive" revocation key * unless the user wants us to. Note that we do export * these when issuing the actual revocation (see revoke.c). */ if (!(options & EXPORT_SENSITIVE_REVKEYS) && node->pkt->pkt.signature->revkey) { int i; for (i = 0; i < node->pkt->pkt.signature->numrevkeys; i++) if ((node->pkt->pkt.signature->revkey[i].class & 0x40)) break; if (i < node->pkt->pkt.signature->numrevkeys) continue; } } /* Don't export attribs? */ if (!(options & EXPORT_ATTRIBUTES) && node->pkt->pkttype == PKT_USER_ID && node->pkt->pkt.user_id->attrib_data) { /* Skip until we get to something that is not an attrib or a * signature on an attrib. */ while (kbctx->next && kbctx->next->pkt->pkttype == PKT_SIGNATURE) kbctx = kbctx->next; continue; } if (secret && (node->pkt->pkttype == PKT_PUBLIC_KEY || node->pkt->pkttype == PKT_PUBLIC_SUBKEY)) { pk = node->pkt->pkt.public_key; if (node->pkt->pkttype == PKT_PUBLIC_KEY) subkid = NULL; else { keyid_from_pk (pk, subkidbuf); subkid = subkidbuf; } if (pk->seckey_info) { log_error ("key %s: oops: seckey_info already set" " - skipped\n", keystr_with_sub (keyid, subkid)); skip_until_subkey = 1; continue; } xfree (hexgrip); err = hexkeygrip_from_pk (pk, &hexgrip); if (err) { log_error ("key %s: error computing keygrip: %s" " - skipped\n", keystr_with_sub (keyid, subkid), gpg_strerror (err)); skip_until_subkey = 1; err = 0; continue; } xfree (serialno); serialno = NULL; if (secret == 2 && node->pkt->pkttype == PKT_PUBLIC_KEY) { /* We are asked not to export the secret parts of the * primary key. Make up an error code to create the * stub. */ err = GPG_ERR_NOT_FOUND; } else err = agent_get_keyinfo (ctrl, hexgrip, &serialno, &cleartext); if ((!err && serialno) && secret == 2 && node->pkt->pkttype == PKT_PUBLIC_KEY) { /* It does not make sense to export a key with its * primary key on card using a non-key stub. Thus we * skip those keys when used with --export-secret-subkeys. */ log_info (_("key %s: key material on-card - skipped\n"), keystr_with_sub (keyid, subkid)); skip_until_subkey = 1; } else if (gpg_err_code (err) == GPG_ERR_NOT_FOUND || (!err && serialno)) { /* Create a key stub. */ struct seckey_info *ski; const char *s; pk->seckey_info = ski = xtrycalloc (1, sizeof *ski); if (!ski) { err = gpg_error_from_syserror (); goto leave; } ski->is_protected = 1; if (err) ski->s2k.mode = 1001; /* GNU dummy (no secret key). */ else { ski->s2k.mode = 1002; /* GNU-divert-to-card. */ for (s=serialno; sizeof (ski->ivlen) && *s && s[1]; ski->ivlen++, s += 2) ski->iv[ski->ivlen] = xtoi_2 (s); } if ((options & EXPORT_BACKUP)) err = build_packet_and_meta (out, node->pkt); else err = build_packet (out, node->pkt); if (!err && node->pkt->pkttype == PKT_PUBLIC_KEY) { stats->exported++; print_status_exported (node->pkt->pkt.public_key); } } else if (!err) { err = receive_seckey_from_agent (ctrl, cipherhd, cleartext, &cache_nonce, hexgrip, pk); if (err) { if (gpg_err_code (err) == GPG_ERR_FULLY_CANCELED) goto leave; write_status_error ("export_keys.secret", err); skip_until_subkey = 1; err = 0; } else { if ((options & EXPORT_BACKUP)) err = build_packet_and_meta (out, node->pkt); else err = build_packet (out, node->pkt); if (node->pkt->pkttype == PKT_PUBLIC_KEY) { stats->exported++; print_status_exported (node->pkt->pkt.public_key); } } } else { log_error ("key %s: error getting keyinfo from agent: %s" " - skipped\n", keystr_with_sub (keyid, subkid), gpg_strerror (err)); skip_until_subkey = 1; err = 0; } xfree (pk->seckey_info); pk->seckey_info = NULL; { int i; for (i = pubkey_get_npkey (pk->pubkey_algo); i < pubkey_get_nskey (pk->pubkey_algo); i++) { gcry_mpi_release (pk->pkey[i]); pk->pkey[i] = NULL; } } } else /* Not secret or common packets. */ { if ((options & EXPORT_BACKUP)) err = build_packet_and_meta (out, node->pkt); else err = build_packet (out, node->pkt); if (!err && node->pkt->pkttype == PKT_PUBLIC_KEY) { stats->exported++; print_status_exported (node->pkt->pkt.public_key); } } if (err) { log_error ("build_packet(%d) failed: %s\n", node->pkt->pkttype, gpg_strerror (err)); goto leave; } if (!skip_until_subkey) *any = 1; } leave: release_subkey_list (subkey_list); xfree (serialno); xfree (hexgrip); xfree (cache_nonce); return err; } /* Helper for do_export_stream which writes the own revocations * certificates (if any) from KEYBLOCK to OUT. */ static gpg_error_t do_export_revocs (ctrl_t ctrl, kbnode_t keyblock, u32 *keyid, iobuf_t out, unsigned int options, int *any) { gpg_error_t err = 0; kbnode_t kbctx, node; PKT_signature *sig; (void)ctrl; /* NB: walk_kbnode skips packets marked as deleted. */ for (kbctx=NULL; (node = walk_kbnode (keyblock, &kbctx, 0)); ) { if (node->pkt->pkttype != PKT_SIGNATURE) continue; sig = node->pkt->pkt.signature; /* We are only interested in revocation certifcates. */ if (!(IS_KEY_REV (sig) || IS_UID_REV (sig) || IS_SUBKEY_REV (sig))) continue; if (!(sig->keyid[0] == keyid[0] && sig->keyid[1] == keyid[1])) continue; /* Not a self-signature. */ /* Do not export signature packets which are marked as not * exportable. */ if (!(options & EXPORT_LOCAL_SIGS) && !sig->flags.exportable) continue; /* not exportable */ /* Do not export packets with a "sensitive" revocation key * unless the user wants us to. */ if (!(options & EXPORT_SENSITIVE_REVKEYS) && sig->revkey) { int i; for (i = 0; i < sig->numrevkeys; i++) if ((sig->revkey[i].class & 0x40)) break; if (i < sig->numrevkeys) continue; } if (!sig->flags.checked) { log_info ("signature not marked as checked - ignored\n"); continue; } if (!sig->flags.valid) { log_info ("signature not not valid - ignored\n"); continue; } err = build_packet (out, node->pkt); if (err) { log_error ("build_packet(%d) failed: %s\n", node->pkt->pkttype, gpg_strerror (err)); goto leave; } *any = 1; } leave: return err; } /* Export the keys identified by the list of strings in USERS to the stream OUT. If SECRET is false public keys will be exported. With secret true secret keys will be exported; in this case 1 means the entire secret keyblock and 2 only the subkeys. OPTIONS are the export options to apply. If KEYBLOCK_OUT is not NULL, AND the exit code is zero, a pointer to the first keyblock found and exported will be stored at this address; no other keyblocks are exported in this case. The caller must free the returned keyblock. If any key has been exported true is stored at ANY. */ static int do_export_stream (ctrl_t ctrl, iobuf_t out, strlist_t users, int secret, kbnode_t *keyblock_out, unsigned int options, export_stats_t stats, int *any) { gpg_error_t err = 0; PACKET pkt; kbnode_t keyblock = NULL; kbnode_t node; size_t ndesc, descindex; KEYDB_SEARCH_DESC *desc = NULL; KEYDB_HANDLE kdbhd; strlist_t sl; gcry_cipher_hd_t cipherhd = NULL; struct export_stats_s dummystats; iobuf_t out_help = NULL; if (!stats) stats = &dummystats; *any = 0; init_packet (&pkt); kdbhd = keydb_new (); if (!kdbhd) return gpg_error_from_syserror (); /* For the PKA and DANE format open a helper iobuf and for DANE * enforce some options. */ if ((options & (EXPORT_PKA_FORMAT | EXPORT_DANE_FORMAT))) { out_help = iobuf_temp (); if ((options & EXPORT_DANE_FORMAT)) options |= EXPORT_MINIMAL | EXPORT_CLEAN; } if (!users) { ndesc = 1; desc = xcalloc (ndesc, sizeof *desc); desc[0].mode = KEYDB_SEARCH_MODE_FIRST; } else { for (ndesc=0, sl=users; sl; sl = sl->next, ndesc++) ; desc = xmalloc ( ndesc * sizeof *desc); for (ndesc=0, sl=users; sl; sl = sl->next) { if (!(err=classify_user_id (sl->d, desc+ndesc, 1))) ndesc++; else log_error (_("key \"%s\" not found: %s\n"), sl->d, gpg_strerror (err)); } keydb_disable_caching (kdbhd); /* We are looping the search. */ /* It would be nice to see which of the given users did actually match one in the keyring. To implement this we need to have a found flag for each entry in desc. To set this flag we must check all those entries after a match to mark all matched one - currently we stop at the first match. To do this we need an extra flag to enable this feature. */ } #ifdef ENABLE_SELINUX_HACKS if (secret) { log_error (_("exporting secret keys not allowed\n")); err = gpg_error (GPG_ERR_NOT_SUPPORTED); goto leave; } #endif /* For secret key export we need to setup a decryption context. */ if (secret) { void *kek = NULL; size_t keklen; err = agent_keywrap_key (ctrl, 1, &kek, &keklen); if (err) { log_error ("error getting the KEK: %s\n", gpg_strerror (err)); goto leave; } /* Prepare a cipher context. */ err = gcry_cipher_open (&cipherhd, GCRY_CIPHER_AES128, GCRY_CIPHER_MODE_AESWRAP, 0); if (!err) err = gcry_cipher_setkey (cipherhd, kek, keklen); if (err) { log_error ("error setting up an encryption context: %s\n", gpg_strerror (err)); goto leave; } xfree (kek); kek = NULL; } for (;;) { u32 keyid[2]; PKT_public_key *pk; err = keydb_search (kdbhd, desc, ndesc, &descindex); if (!users) desc[0].mode = KEYDB_SEARCH_MODE_NEXT; if (err) break; /* Read the keyblock. */ release_kbnode (keyblock); keyblock = NULL; err = keydb_get_keyblock (kdbhd, &keyblock); if (err) { log_error (_("error reading keyblock: %s\n"), gpg_strerror (err)); goto leave; } node = find_kbnode (keyblock, PKT_PUBLIC_KEY); if (!node) { log_error ("public key packet not found in keyblock - skipped\n"); continue; } stats->count++; setup_main_keyids (keyblock); /* gpg_format_keydesc needs it. */ pk = node->pkt->pkt.public_key; keyid_from_pk (pk, keyid); /* If a secret key export is required we need to check whether we have a secret key at all and if so create the seckey_info structure. */ if (secret) { if (agent_probe_any_secret_key (ctrl, keyblock)) continue; /* No secret key (neither primary nor subkey). */ /* No v3 keys with GNU mode 1001. */ if (secret == 2 && pk->version == 3) { log_info (_("key %s: PGP 2.x style key - skipped\n"), keystr (keyid)); continue; } /* The agent does not yet allow export of v3 packets. It is actually questionable whether we should allow them at all. */ if (pk->version == 3) { log_info ("key %s: PGP 2.x style key (v3) export " "not yet supported - skipped\n", keystr (keyid)); continue; } stats->secret_count++; } /* Always do the cleaning on the public key part if requested. * A designated revocation is never stripped, even with * export-minimal set. */ if ((options & EXPORT_CLEAN)) { merge_keys_and_selfsig (ctrl, keyblock); clean_all_uids (ctrl, keyblock, opt.verbose, (options&EXPORT_MINIMAL), NULL, NULL); clean_all_subkeys (ctrl, keyblock, opt.verbose, (options&EXPORT_MINIMAL)? KEY_CLEAN_ALL /**/ : KEY_CLEAN_AUTHENCR, NULL, NULL); commit_kbnode (&keyblock); } else if (export_keep_uid || export_drop_subkey || export_select_filter) { /* Need to merge so that for example the "usage" property * has been setup. */ merge_keys_and_selfsig (ctrl, keyblock); } if (export_select_filter) { int selected = 0; struct impex_filter_parm_s parm; parm.ctrl = ctrl; for (parm.node = keyblock; parm.node; parm.node = parm.node->next) { if (recsel_select (export_select_filter, impex_filter_getval, &parm)) { selected = 1; break; } } if (!selected) continue; /* Skip this keyblock. */ } if (export_keep_uid) { commit_kbnode (&keyblock); apply_keep_uid_filter (ctrl, keyblock, export_keep_uid); commit_kbnode (&keyblock); } if (export_drop_subkey) { commit_kbnode (&keyblock); apply_drop_subkey_filter (ctrl, keyblock, export_drop_subkey); commit_kbnode (&keyblock); } /* And write it. */ if ((options & EXPORT_REVOCS)) err = do_export_revocs (ctrl, keyblock, keyid, out_help? out_help : out, options, any); else err = do_export_one_keyblock (ctrl, keyblock, keyid, out_help? out_help : out, secret, options, stats, any, desc, ndesc, descindex, cipherhd); if (err) break; if (keyblock_out) { *keyblock_out = keyblock; break; } if (out_help) { /* We want to write PKA or DANE records. OUT_HELP has the * keyblock and we print a record for each uid to OUT. */ const void *data; size_t datalen; iobuf_flush_temp (out_help); data = iobuf_get_temp_buffer (out_help); datalen = iobuf_get_temp_length (out_help); err = print_pka_or_dane_records (out, keyblock, pk, data, datalen, (options & EXPORT_PKA_FORMAT), (options & EXPORT_DANE_FORMAT)); if (err) goto leave; iobuf_close (out_help); out_help = iobuf_temp (); } } if (gpg_err_code (err) == GPG_ERR_NOT_FOUND) err = 0; leave: iobuf_cancel (out_help); gcry_cipher_close (cipherhd); xfree(desc); keydb_release (kdbhd); if (err || !keyblock_out) release_kbnode( keyblock ); - if( !*any ) - log_info(_("WARNING: nothing exported\n")); + if( !*any && !opt.quiet) + log_info (_("WARNING: nothing exported\n")); return err; } static gpg_error_t key_to_sshblob (membuf_t *mb, const char *identifier, ...) { va_list arg_ptr; gpg_error_t err = 0; unsigned char nbuf[4]; unsigned char *buf; size_t buflen; gcry_mpi_t a; ulongtobuf (nbuf, (ulong)strlen (identifier)); put_membuf (mb, nbuf, 4); put_membuf_str (mb, identifier); if (!strncmp (identifier, "ecdsa-sha2-", 11)) { ulongtobuf (nbuf, (ulong)strlen (identifier+11)); put_membuf (mb, nbuf, 4); put_membuf_str (mb, identifier+11); } va_start (arg_ptr, identifier); while ((a = va_arg (arg_ptr, gcry_mpi_t))) { err = gcry_mpi_aprint (GCRYMPI_FMT_SSH, &buf, &buflen, a); if (err) break; if (!strcmp (identifier, "ssh-ed25519") && buflen > 5 && buf[4] == 0x40) { /* We need to strip our 0x40 prefix. */ put_membuf (mb, "\x00\x00\x00\x20", 4); put_membuf (mb, buf+5, buflen-5); } else put_membuf (mb, buf, buflen); gcry_free (buf); } va_end (arg_ptr); return err; } /* Export the key identified by USERID in the SSH public key format. The function exports the latest subkey with Authentication capability unless the '!' suffix is used to export a specific key. */ gpg_error_t export_ssh_key (ctrl_t ctrl, const char *userid) { gpg_error_t err; kbnode_t keyblock = NULL; KEYDB_SEARCH_DESC desc; u32 latest_date; u32 curtime = make_timestamp (); kbnode_t latest_key, node; PKT_public_key *pk; const char *identifier = NULL; membuf_t mb; estream_t fp = NULL; struct b64state b64_state; const char *fname = "-"; init_membuf (&mb, 4096); /* We need to know whether the key has been specified using the exact syntax ('!' suffix). Thus we need to run a classify_user_id on our own. */ err = classify_user_id (userid, &desc, 1); /* Get the public key. */ if (!err) { getkey_ctx_t getkeyctx; err = get_pubkey_byname (ctrl, GET_PUBKEY_NO_AKL, &getkeyctx, NULL, userid, &keyblock, NULL, 0 /* Only usable keys or given exact. */); if (!err) { err = getkey_next (ctrl, getkeyctx, NULL, NULL); if (!err) err = gpg_error (GPG_ERR_AMBIGUOUS_NAME); else if (gpg_err_code (err) == GPG_ERR_NO_PUBKEY) err = 0; } getkey_end (ctrl, getkeyctx); } if (err) { log_error (_("key \"%s\" not found: %s\n"), userid, gpg_strerror (err)); return err; } /* The finish_lookup code in getkey.c does not handle auth keys, thus we have to duplicate the code here to find the latest subkey. However, if the key has been found using an exact match ('!' notation) we use that key without any further checks and even allow the use of the primary key. */ latest_date = 0; latest_key = NULL; for (node = keyblock; node; node = node->next) { if ((node->pkt->pkttype == PKT_PUBLIC_SUBKEY || node->pkt->pkttype == PKT_PUBLIC_KEY) && node->pkt->pkt.public_key->flags.exact) { latest_key = node; break; } } if (!latest_key) { for (node = keyblock; node; node = node->next) { if (node->pkt->pkttype != PKT_PUBLIC_SUBKEY) continue; pk = node->pkt->pkt.public_key; if (DBG_LOOKUP) log_debug ("\tchecking subkey %08lX\n", (ulong) keyid_from_pk (pk, NULL)); if (!(pk->pubkey_usage & PUBKEY_USAGE_AUTH)) { if (DBG_LOOKUP) log_debug ("\tsubkey not usable for authentication\n"); continue; } if (!pk->flags.valid) { if (DBG_LOOKUP) log_debug ("\tsubkey not valid\n"); continue; } if (pk->flags.revoked) { if (DBG_LOOKUP) log_debug ("\tsubkey has been revoked\n"); continue; } if (pk->has_expired) { if (DBG_LOOKUP) log_debug ("\tsubkey has expired\n"); continue; } if (pk->timestamp > curtime && !opt.ignore_valid_from) { if (DBG_LOOKUP) log_debug ("\tsubkey not yet valid\n"); continue; } if (DBG_LOOKUP) log_debug ("\tsubkey might be fine\n"); /* In case a key has a timestamp of 0 set, we make sure that it is used. A better change would be to compare ">=" but that might also change the selected keys and is as such a more intrusive change. */ if (pk->timestamp > latest_date || (!pk->timestamp && !latest_date)) { latest_date = pk->timestamp; latest_key = node; } } /* If no subkey was suitable check the primary key. */ if (!latest_key && (node = keyblock) && node->pkt->pkttype == PKT_PUBLIC_KEY) { pk = node->pkt->pkt.public_key; if (DBG_LOOKUP) log_debug ("\tchecking primary key %08lX\n", (ulong) keyid_from_pk (pk, NULL)); if (!(pk->pubkey_usage & PUBKEY_USAGE_AUTH)) { if (DBG_LOOKUP) log_debug ("\tprimary key not usable for authentication\n"); } else if (!pk->flags.valid) { if (DBG_LOOKUP) log_debug ("\tprimary key not valid\n"); } else if (pk->flags.revoked) { if (DBG_LOOKUP) log_debug ("\tprimary key has been revoked\n"); } else if (pk->has_expired) { if (DBG_LOOKUP) log_debug ("\tprimary key has expired\n"); } else if (pk->timestamp > curtime && !opt.ignore_valid_from) { if (DBG_LOOKUP) log_debug ("\tprimary key not yet valid\n"); } else { if (DBG_LOOKUP) log_debug ("\tprimary key is fine\n"); latest_date = pk->timestamp; latest_key = node; } } } if (!latest_key) { err = gpg_error (GPG_ERR_UNUSABLE_PUBKEY); log_error (_("key \"%s\" not found: %s\n"), userid, gpg_strerror (err)); goto leave; } pk = latest_key->pkt->pkt.public_key; if (DBG_LOOKUP) log_debug ("\tusing key %08lX\n", (ulong) keyid_from_pk (pk, NULL)); switch (pk->pubkey_algo) { case PUBKEY_ALGO_DSA: identifier = "ssh-dss"; err = key_to_sshblob (&mb, identifier, pk->pkey[0], pk->pkey[1], pk->pkey[2], pk->pkey[3], NULL); break; case PUBKEY_ALGO_RSA: case PUBKEY_ALGO_RSA_S: identifier = "ssh-rsa"; err = key_to_sshblob (&mb, identifier, pk->pkey[1], pk->pkey[0], NULL); break; case PUBKEY_ALGO_ECDSA: { char *curveoid; const char *curve; curveoid = openpgp_oid_to_str (pk->pkey[0]); if (!curveoid) err = gpg_error_from_syserror (); else if (!(curve = openpgp_oid_to_curve (curveoid, 0))) err = gpg_error (GPG_ERR_UNKNOWN_CURVE); else { if (!strcmp (curve, "nistp256")) identifier = "ecdsa-sha2-nistp256"; else if (!strcmp (curve, "nistp384")) identifier = "ecdsa-sha2-nistp384"; else if (!strcmp (curve, "nistp521")) identifier = "ecdsa-sha2-nistp521"; if (!identifier) err = gpg_error (GPG_ERR_UNKNOWN_CURVE); else err = key_to_sshblob (&mb, identifier, pk->pkey[1], NULL); } xfree (curveoid); } break; case PUBKEY_ALGO_EDDSA: if (!openpgp_oid_is_ed25519 (pk->pkey[0])) err = gpg_error (GPG_ERR_UNKNOWN_CURVE); else { identifier = "ssh-ed25519"; err = key_to_sshblob (&mb, identifier, pk->pkey[1], NULL); } break; case PUBKEY_ALGO_ELGAMAL_E: case PUBKEY_ALGO_ELGAMAL: err = gpg_error (GPG_ERR_UNUSABLE_PUBKEY); break; default: err = GPG_ERR_PUBKEY_ALGO; break; } if (!identifier) goto leave; if (opt.outfile && *opt.outfile && strcmp (opt.outfile, "-")) fp = es_fopen ((fname = opt.outfile), "w"); else fp = es_stdout; if (!fp) { err = gpg_error_from_syserror (); log_error (_("error creating '%s': %s\n"), fname, gpg_strerror (err)); goto leave; } es_fprintf (fp, "%s ", identifier); err = b64enc_start_es (&b64_state, fp, ""); if (!err) { void *blob; size_t bloblen; blob = get_membuf (&mb, &bloblen); if (blob) { err = b64enc_write (&b64_state, blob, bloblen); xfree (blob); if (err) goto leave; } err = b64enc_finish (&b64_state); } if (err) goto leave; es_fprintf (fp, " openpgp:0x%08lX\n", (ulong)keyid_from_pk (pk, NULL)); if (es_ferror (fp)) err = gpg_error_from_syserror (); else { if (fp != es_stdout && es_fclose (fp)) err = gpg_error_from_syserror (); fp = NULL; } if (err) log_error (_("error writing '%s': %s\n"), fname, gpg_strerror (err)); leave: if (fp != es_stdout) es_fclose (fp); xfree (get_membuf (&mb, NULL)); release_kbnode (keyblock); return err; } diff --git a/tools/gpg-wks-client.c b/tools/gpg-wks-client.c index 45c14bc55..2139011e0 100644 --- a/tools/gpg-wks-client.c +++ b/tools/gpg-wks-client.c @@ -1,2028 +1,2050 @@ /* gpg-wks-client.c - A client for the Web Key Service protocols. * Copyright (C) 2016, 2022 g10 Code GmbH * Copyright (C) 2016 Bundesamt für Sicherheit in der Informationstechnik * * 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 Lesser General Public License as * published by the Free Software Foundation; either version 2.1 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 Lesser General Public License * along with this program; if not, see . * SPDX-License-Identifier: LGPL-2.1-or-later */ #include #include #include #include #include #include #define INCLUDED_BY_MAIN_MODULE 1 #include "../common/util.h" #include "../common/status.h" #include "../common/i18n.h" #include "../common/sysutils.h" #include "../common/init.h" #include "../common/asshelp.h" #include "../common/userids.h" #include "../common/ccparray.h" #include "../common/exectool.h" #include "../common/mbox-util.h" #include "../common/name-value.h" #include "call-dirmngr.h" #include "mime-maker.h" #include "send-mail.h" #include "gpg-wks.h" /* Constants to identify the commands and options. */ enum cmd_and_opt_values { aNull = 0, oQuiet = 'q', oVerbose = 'v', oOutput = 'o', oDirectory = 'C', oDebug = 500, aSupported, aCheck, aCreate, aReceive, aRead, aMirror, aInstallKey, aRemoveKey, aPrintWKDHash, aPrintWKDURL, oGpgProgram, oSend, oFakeSubmissionAddr, oStatusFD, oWithColons, oBlacklist, oNoAutostart, oAddRevocs, oDummy }; /* The list of commands and options. */ static ARGPARSE_OPTS opts[] = { ARGPARSE_group (300, ("@Commands:\n ")), ARGPARSE_c (aSupported, "supported", ("check whether provider supports WKS")), ARGPARSE_c (aCheck, "check", ("check whether a key is available")), ARGPARSE_c (aCreate, "create", ("create a publication request")), ARGPARSE_c (aReceive, "receive", ("receive a MIME confirmation request")), ARGPARSE_c (aRead, "read", ("receive a plain text confirmation request")), ARGPARSE_c (aMirror, "mirror", "mirror an LDAP directory"), ARGPARSE_c (aInstallKey, "install-key", "install a key into a directory"), ARGPARSE_c (aRemoveKey, "remove-key", "remove a key from a directory"), ARGPARSE_c (aPrintWKDHash, "print-wkd-hash", "print the WKD identifier for the given user ids"), ARGPARSE_c (aPrintWKDURL, "print-wkd-url", "print the WKD URL for the given user id"), 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_s (oGpgProgram, "gpg", "@"), ARGPARSE_s_n (oSend, "send", "send the mail using sendmail"), ARGPARSE_s_s (oOutput, "output", "|FILE|write the mail to FILE"), ARGPARSE_s_i (oStatusFD, "status-fd", N_("|FD|write status info to this FD")), ARGPARSE_s_n (oNoAutostart, "no-autostart", "@"), ARGPARSE_s_n (oWithColons, "with-colons", "@"), ARGPARSE_s_s (oBlacklist, "blacklist", "@"), ARGPARSE_s_s (oDirectory, "directory", "@"), ARGPARSE_s_n (oAddRevocs, "add-revocs", "add revocation certificates"), ARGPARSE_s_s (oFakeSubmissionAddr, "fake-submission-addr", "@"), ARGPARSE_end () }; /* The list of supported debug flags. */ static struct debug_flags_s debug_flags [] = { { DBG_MIME_VALUE , "mime" }, { DBG_PARSER_VALUE , "parser" }, { DBG_CRYPTO_VALUE , "crypto" }, { DBG_MEMORY_VALUE , "memory" }, { DBG_MEMSTAT_VALUE, "memstat" }, { DBG_IPC_VALUE , "ipc" }, { DBG_EXTPROG_VALUE, "extprog" }, { 0, NULL } }; /* Value of the option --fake-submission-addr. */ const char *fake_submission_addr; /* An array with blacklisted addresses and its length. Use * is_in_blacklist to check. */ static char **blacklist_array; static size_t blacklist_array_len; static void wrong_args (const char *text) GPGRT_ATTR_NORETURN; static void add_blacklist (const char *fname); static gpg_error_t proc_userid_from_stdin (gpg_error_t (*func)(const char *), const char *text); static gpg_error_t command_supported (char *userid); static gpg_error_t command_check (char *userid); static gpg_error_t command_send (const char *fingerprint, const char *userid); static gpg_error_t encrypt_response (estream_t *r_output, estream_t input, const char *addrspec, const char *fingerprint); static gpg_error_t read_confirmation_request (estream_t msg); static gpg_error_t command_receive_cb (void *opaque, const char *mediatype, estream_t fp, unsigned int flags); static gpg_error_t command_mirror (char *domain[]); /* Print usage information and provide strings for help. */ static const char * my_strusage( int level ) { const char *p; switch (level) { case 9: p = "LGPL-2.1-or-later"; break; case 11: p = "gpg-wks-client"; 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-wks-client [command] [options] [args] (-h for help)"); break; case 41: p = ("Syntax: gpg-wks-client [command] [options] [args]\n" "Client for the Web Key Service\n"); break; default: p = NULL; break; } return p; } static void wrong_args (const char *text) { es_fprintf (es_stderr, _("usage: %s [options] %s\n"), strusage (11), text); exit (2); } /* Command line parsing. */ static enum cmd_and_opt_values parse_arguments (ARGPARSE_ARGS *pargs, ARGPARSE_OPTS *popts) { enum cmd_and_opt_values cmd = 0; int no_more_options = 0; while (!no_more_options && gnupg_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 oGpgProgram: opt.gpg_program = pargs->r.ret_str; break; case oDirectory: opt.directory = pargs->r.ret_str; break; case oSend: opt.use_sendmail = 1; break; case oOutput: opt.output = pargs->r.ret_str; break; case oFakeSubmissionAddr: fake_submission_addr = pargs->r.ret_str; break; case oStatusFD: wks_set_status_fd (translate_sys2libc_fd_int (pargs->r.ret_int, 1)); break; case oWithColons: opt.with_colons = 1; break; case oNoAutostart: opt.no_autostart = 1; break; case oBlacklist: add_blacklist (pargs->r.ret_str); break; case oAddRevocs: opt.add_revocs = 1; break; case aSupported: case aCreate: case aReceive: case aRead: case aCheck: case aMirror: case aInstallKey: case aRemoveKey: case aPrintWKDHash: case aPrintWKDURL: cmd = pargs->r_opt; break; default: pargs->err = ARGPARSE_PRINT_ERROR; break; } } return cmd; } /* gpg-wks-client main. */ int main (int argc, char **argv) { gpg_error_t err, delayed_err; ARGPARSE_ARGS pargs; enum cmd_and_opt_values cmd; gnupg_reopen_std ("gpg-wks-client"); set_strusage (my_strusage); log_set_prefix ("gpg-wks-client", 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); /* Parse the command line. */ pargs.argc = &argc; pargs.argv = &argv; pargs.flags = ARGPARSE_FLAG_KEEP; cmd = parse_arguments (&pargs, opts); gnupg_argparse (NULL, &pargs, NULL); if (log_get_errorcount (0)) exit (2); /* Print a warning if an argument looks like an option. */ if (!opt.quiet && !(pargs.flags & ARGPARSE_FLAG_STOP_SEEN)) { int i; for (i=0; i < argc; i++) if (argv[i][0] == '-' && argv[i][1] == '-') log_info (("NOTE: '%s' is not considered an option\n"), argv[i]); } /* Set defaults for non given options. */ if (!opt.gpg_program) opt.gpg_program = gnupg_module_name (GNUPG_MODULE_NAME_GPG); if (!opt.directory) opt.directory = "openpgpkey"; /* Tell call-dirmngr what options we want. */ set_dirmngr_options (opt.verbose, (opt.debug & DBG_IPC_VALUE), !opt.no_autostart); /* Check that the top directory exists. */ if (cmd == aInstallKey || cmd == aRemoveKey || cmd == aMirror) { struct stat sb; if (gnupg_stat (opt.directory, &sb)) { err = gpg_error_from_syserror (); log_error ("error accessing directory '%s': %s\n", opt.directory, gpg_strerror (err)); goto leave; } if (!S_ISDIR(sb.st_mode)) { log_error ("error accessing directory '%s': %s\n", opt.directory, "not a directory"); err = gpg_error (GPG_ERR_ENOENT); goto leave; } } /* Run the selected command. */ switch (cmd) { case aSupported: if (opt.with_colons) { for (; argc; argc--, argv++) command_supported (*argv); err = 0; } else { if (argc != 1) wrong_args ("--supported DOMAIN"); err = command_supported (argv[0]); if (err && gpg_err_code (err) != GPG_ERR_FALSE) log_error ("checking support failed: %s\n", gpg_strerror (err)); } break; case aCreate: if (argc != 2) wrong_args ("--create FINGERPRINT USER-ID"); err = command_send (argv[0], argv[1]); if (err) log_error ("creating request failed: %s\n", gpg_strerror (err)); break; case aReceive: if (argc) wrong_args ("--receive < MIME-DATA"); err = wks_receive (es_stdin, command_receive_cb, NULL); if (err) log_error ("processing mail failed: %s\n", gpg_strerror (err)); break; case aRead: if (argc) wrong_args ("--read < WKS-DATA"); err = read_confirmation_request (es_stdin); if (err) log_error ("processing mail failed: %s\n", gpg_strerror (err)); break; case aCheck: if (argc != 1) wrong_args ("--check USER-ID"); err = command_check (argv[0]); break; case aMirror: if (!argc) err = command_mirror (NULL); else err = command_mirror (argv); break; case aInstallKey: if (!argc) err = wks_cmd_install_key (NULL, NULL); else if (argc == 2) err = wks_cmd_install_key (*argv, argv[1]); else wrong_args ("--install-key [FILE|FINGERPRINT USER-ID]"); break; case aRemoveKey: if (argc != 1) wrong_args ("--remove-key USER-ID"); err = wks_cmd_remove_key (*argv); break; case aPrintWKDHash: case aPrintWKDURL: if (!argc) { if (cmd == aPrintWKDHash) err = proc_userid_from_stdin (wks_cmd_print_wkd_hash, "printing WKD hash"); else err = proc_userid_from_stdin (wks_cmd_print_wkd_url, "printing WKD URL"); } else { for (err = delayed_err = 0; !err && argc; argc--, argv++) { if (cmd == aPrintWKDHash) err = wks_cmd_print_wkd_hash (*argv); else err = wks_cmd_print_wkd_url (*argv); if (gpg_err_code (err) == GPG_ERR_INV_USER_ID) { /* Diagnostic already printed. */ delayed_err = err; err = 0; } else if (err) log_error ("printing hash failed: %s\n", gpg_strerror (err)); } if (!err) err = delayed_err; } break; default: usage (1); err = 0; break; } leave: if (err) wks_write_status (STATUS_FAILURE, "- %u", err); else if (log_get_errorcount (0)) wks_write_status (STATUS_FAILURE, "- %u", GPG_ERR_GENERAL); else wks_write_status (STATUS_SUCCESS, NULL); return (err || log_get_errorcount (0))? 1:0; } /* Read a file FNAME into a buffer and return that malloced buffer. * Caller must free the buffer. On error NULL is returned, on success * the valid length of the buffer is stored at R_LENGTH. The returned * buffer is guaranteed to be Nul terminated. */ static char * read_file (const char *fname, size_t *r_length) { estream_t fp; char *buf; size_t buflen; if (!strcmp (fname, "-")) { size_t nread, bufsize = 0; fp = es_stdin; es_set_binary (fp); buf = NULL; buflen = 0; #define NCHUNK 32767 do { bufsize += NCHUNK; if (!buf) buf = xmalloc (bufsize+1); else buf = xrealloc (buf, bufsize+1); nread = es_fread (buf+buflen, 1, NCHUNK, fp); if (nread < NCHUNK && es_ferror (fp)) { log_error ("error reading '[stdin]': %s\n", strerror (errno)); xfree (buf); return NULL; } buflen += nread; } while (nread == NCHUNK); #undef NCHUNK } else { struct stat st; fp = es_fopen (fname, "rb"); if (!fp) { log_error ("can't open '%s': %s\n", fname, strerror (errno)); return NULL; } if (fstat (es_fileno (fp), &st)) { log_error ("can't stat '%s': %s\n", fname, strerror (errno)); es_fclose (fp); return NULL; } buflen = st.st_size; buf = xmalloc (buflen+1); if (es_fread (buf, buflen, 1, fp) != 1) { log_error ("error reading '%s': %s\n", fname, strerror (errno)); es_fclose (fp); xfree (buf); return NULL; } es_fclose (fp); } buf[buflen] = 0; if (r_length) *r_length = buflen; return buf; } static int cmp_blacklist (const void *arg_a, const void *arg_b) { const char *a = *(const char **)arg_a; const char *b = *(const char **)arg_b; return strcmp (a, b); } /* Add a blacklist to our global table. This is called during option * parsing and thus any use of log_error will eventually stop further * processing. */ static void add_blacklist (const char *fname) { char *buffer; char *p, *pend; char **array; size_t arraysize, arrayidx; buffer = read_file (fname, NULL); if (!buffer) return; /* Estimate the number of entries by counting the non-comment lines. */ arraysize = 2; /* For the first and an extra NULL item. */ for (p=buffer; *p; p++) if (*p == '\n' && p[1] && p[1] != '#') arraysize++; array = xcalloc (arraysize, sizeof *array); arrayidx = 0; /* Loop over all lines. */ for (p = buffer; p && *p; p = pend) { pend = strchr (p, '\n'); if (pend) *pend++ = 0; trim_spaces (p); if (!*p || *p == '#' ) continue; ascii_strlwr (p); log_assert (arrayidx < arraysize); array[arrayidx] = p; arrayidx++; } log_assert (arrayidx < arraysize); qsort (array, arrayidx, sizeof *array, cmp_blacklist); blacklist_array = array; blacklist_array_len = arrayidx; gpgrt_annotate_leaked_object (buffer); gpgrt_annotate_leaked_object (blacklist_array); } /* Return true if NAME is in a blacklist. */ static int is_in_blacklist (const char *name) { if (!name || !blacklist_array) return 0; return !!bsearch (&name, blacklist_array, blacklist_array_len, sizeof *blacklist_array, cmp_blacklist); } /* Read user ids from stdin and call FUNC for each user id. TEXT is * used for error messages. */ static gpg_error_t proc_userid_from_stdin (gpg_error_t (*func)(const char *), const char *text) { gpg_error_t err = 0; gpg_error_t delayed_err = 0; char line[2048]; size_t n = 0; /* If we are on a terminal disable buffering to get direct response. */ if (gnupg_isatty (es_fileno (es_stdin)) && gnupg_isatty (es_fileno (es_stdout))) { es_setvbuf (es_stdin, NULL, _IONBF, 0); es_setvbuf (es_stdout, NULL, _IOLBF, 0); } while (es_fgets (line, sizeof line - 1, es_stdin)) { n = strlen (line); if (!n || line[n-1] != '\n') { err = gpg_error (*line? GPG_ERR_LINE_TOO_LONG : GPG_ERR_INCOMPLETE_LINE); log_error ("error reading stdin: %s\n", gpg_strerror (err)); break; } trim_spaces (line); err = func (line); if (gpg_err_code (err) == GPG_ERR_INV_USER_ID) { delayed_err = err; err = 0; } else if (err) log_error ("%s failed: %s\n", text, gpg_strerror (err)); } if (es_ferror (es_stdin)) { err = gpg_error_from_syserror (); log_error ("error reading stdin: %s\n", gpg_strerror (err)); goto leave; } leave: if (!err) err = delayed_err; return err; } /* Add the user id UID to the key identified by FINGERPRINT. */ static gpg_error_t add_user_id (const char *fingerprint, const char *uid) { gpg_error_t err; ccparray_t ccp; const char **argv = NULL; ccparray_init (&ccp, 0); ccparray_put (&ccp, "--no-options"); if (opt.verbose < 2) ccparray_put (&ccp, "--quiet"); else ccparray_put (&ccp, "--verbose"); ccparray_put (&ccp, "--batch"); ccparray_put (&ccp, "--always-trust"); ccparray_put (&ccp, "--quick-add-uid"); ccparray_put (&ccp, fingerprint); ccparray_put (&ccp, uid); ccparray_put (&ccp, NULL); argv = ccparray_get (&ccp, NULL); if (!argv) { err = gpg_error_from_syserror (); goto leave; } err = gnupg_exec_tool_stream (opt.gpg_program, argv, NULL, NULL, NULL, NULL, NULL); if (err) { log_error ("adding user id failed: %s\n", gpg_strerror (err)); goto leave; } leave: xfree (argv); return err; } struct decrypt_stream_parm_s { char *fpr; char *mainfpr; int otrust; }; static void decrypt_stream_status_cb (void *opaque, const char *keyword, char *args) { struct decrypt_stream_parm_s *decinfo = opaque; if (DBG_CRYPTO) log_debug ("gpg status: %s %s\n", keyword, args); if (!strcmp (keyword, "DECRYPTION_KEY") && !decinfo->fpr) { char *fields[3]; if (split_fields (args, fields, DIM (fields)) >= 3) { decinfo->fpr = xstrdup (fields[0]); decinfo->mainfpr = xstrdup (fields[1]); decinfo->otrust = *fields[2]; } } } /* Decrypt the INPUT stream to a new stream which is stored at success * at R_OUTPUT. */ static gpg_error_t decrypt_stream (estream_t *r_output, struct decrypt_stream_parm_s *decinfo, estream_t input) { gpg_error_t err; ccparray_t ccp; const char **argv; estream_t output; *r_output = NULL; memset (decinfo, 0, sizeof *decinfo); output = es_fopenmem (0, "w+b"); if (!output) { err = gpg_error_from_syserror (); log_error ("error allocating memory buffer: %s\n", gpg_strerror (err)); return err; } ccparray_init (&ccp, 0); ccparray_put (&ccp, "--no-options"); /* We limit the output to 64 KiB to avoid DoS using compression * tricks. A regular client will anyway only send a minimal key; * that is one w/o key signatures and attribute packets. */ ccparray_put (&ccp, "--max-output=0x10000"); if (opt.verbose < 2) ccparray_put (&ccp, "--quiet"); else ccparray_put (&ccp, "--verbose"); ccparray_put (&ccp, "--batch"); ccparray_put (&ccp, "--status-fd=2"); ccparray_put (&ccp, "--decrypt"); ccparray_put (&ccp, "--"); ccparray_put (&ccp, NULL); argv = ccparray_get (&ccp, NULL); if (!argv) { err = gpg_error_from_syserror (); goto leave; } err = gnupg_exec_tool_stream (opt.gpg_program, argv, input, NULL, output, decrypt_stream_status_cb, decinfo); if (!err && (!decinfo->fpr || !decinfo->mainfpr || !decinfo->otrust)) err = gpg_error (GPG_ERR_INV_ENGINE); if (err) { log_error ("decryption failed: %s\n", gpg_strerror (err)); goto leave; } else if (opt.verbose) log_info ("decryption succeeded\n"); es_rewind (output); *r_output = output; output = NULL; leave: if (err) { xfree (decinfo->fpr); xfree (decinfo->mainfpr); memset (decinfo, 0, sizeof *decinfo); } es_fclose (output); xfree (argv); return err; } /* Return the submission address for the address or just the domain in * ADDRSPEC. The submission address is stored as a malloced string at * R_SUBMISSION_ADDRESS. At R_POLICY the policy flags of the domain * are stored. The caller needs to free them with wks_free_policy. * The function returns an error code on failure to find a submission * address or policy file. Note: The function may store NULL at * R_SUBMISSION_ADDRESS but return success to indicate that the web * key directory is supported but not the web key service. As per WKD * specs a policy file is always required and will thus be return on * success. */ static gpg_error_t get_policy_and_sa (const char *addrspec, int silent, policy_flags_t *r_policy, char **r_submission_address) { gpg_error_t err; estream_t mbuf = NULL; const char *domain; const char *s; policy_flags_t policy = NULL; char *submission_to = NULL; *r_submission_address = NULL; *r_policy = NULL; domain = strchr (addrspec, '@'); if (domain) domain++; if (opt.with_colons) { s = domain? domain : addrspec; es_write_sanitized (es_stdout, s, strlen (s), ":", NULL); es_putc (':', es_stdout); } /* We first try to get the submission address from the policy file * (this is the new method). If both are available we check that * they match and print a warning if not. In the latter case we * keep on using the one from the submission-address file. */ err = wkd_get_policy_flags (addrspec, &mbuf); if (err && gpg_err_code (err) != GPG_ERR_NO_DATA && gpg_err_code (err) != GPG_ERR_NO_NAME) { if (!opt.with_colons) log_error ("error reading policy flags for '%s': %s\n", domain, gpg_strerror (err)); goto leave; } if (!mbuf) { if (!opt.with_colons) log_error ("provider for '%s' does NOT support the Web Key Directory\n", addrspec); err = gpg_error (GPG_ERR_FALSE); goto leave; } policy = xtrycalloc (1, sizeof *policy); if (!policy) err = gpg_error_from_syserror (); else err = wks_parse_policy (policy, mbuf, 1); es_fclose (mbuf); mbuf = NULL; if (err) goto leave; err = wkd_get_submission_address (addrspec, &submission_to); if (err && !policy->submission_address) { if (!silent && !opt.with_colons) log_error (_("error looking up submission address for domain '%s'" ": %s\n"), domain, gpg_strerror (err)); if (!silent && gpg_err_code (err) == GPG_ERR_NO_DATA && !opt.with_colons) log_error (_("this domain probably doesn't support WKS.\n")); goto leave; } if (submission_to && policy->submission_address && ascii_strcasecmp (submission_to, policy->submission_address)) log_info ("Warning: different submission addresses (sa=%s, po=%s)\n", submission_to, policy->submission_address); if (!submission_to && policy->submission_address) { submission_to = xtrystrdup (policy->submission_address); if (!submission_to) { err = gpg_error_from_syserror (); goto leave; } } leave: *r_submission_address = submission_to; submission_to = NULL; *r_policy = policy; policy = NULL; if (opt.with_colons) { if (*r_policy && !*r_submission_address) es_fprintf (es_stdout, "1:0::"); else if (*r_policy && *r_submission_address) es_fprintf (es_stdout, "1:1::"); else if (err && !(gpg_err_code (err) == GPG_ERR_FALSE || gpg_err_code (err) == GPG_ERR_NO_DATA || gpg_err_code (err) == GPG_ERR_UNKNOWN_HOST)) es_fprintf (es_stdout, "0:0:%d:", err); else es_fprintf (es_stdout, "0:0::"); if (*r_policy) { es_fprintf (es_stdout, "%u:%u:%u:", (*r_policy)->protocol_version, (*r_policy)->auth_submit, (*r_policy)->mailbox_only); } es_putc ('\n', es_stdout); } xfree (submission_to); wks_free_policy (policy); xfree (policy); es_fclose (mbuf); return err; } /* Check whether the provider supports the WKS protocol. */ static gpg_error_t command_supported (char *userid) { gpg_error_t err; char *addrspec = NULL; char *submission_to = NULL; policy_flags_t policy = NULL; if (!strchr (userid, '@')) { char *tmp = xstrconcat ("foo@", userid, NULL); addrspec = mailbox_from_userid (tmp); xfree (tmp); } else addrspec = mailbox_from_userid (userid); if (!addrspec) { log_error (_("\"%s\" is not a proper mail address\n"), userid); err = gpg_error (GPG_ERR_INV_USER_ID); goto leave; } /* Get the submission address. */ err = get_policy_and_sa (addrspec, 1, &policy, &submission_to); if (err || !submission_to) { if (!submission_to || gpg_err_code (err) == GPG_ERR_FALSE || gpg_err_code (err) == GPG_ERR_NO_DATA || gpg_err_code (err) == GPG_ERR_UNKNOWN_HOST ) { /* FALSE is returned if we already figured out that even the * Web Key Directory is not supported and thus printed an * error message. */ if (opt.verbose && gpg_err_code (err) != GPG_ERR_FALSE && !opt.with_colons) { if (gpg_err_code (err) == GPG_ERR_NO_DATA) log_info ("provider for '%s' does NOT support WKS\n", addrspec); else log_info ("provider for '%s' does NOT support WKS (%s)\n", addrspec, gpg_strerror (err)); } err = gpg_error (GPG_ERR_FALSE); if (!opt.with_colons) log_inc_errorcount (); } goto leave; } if (opt.verbose && !opt.with_colons) log_info ("provider for '%s' supports WKS\n", addrspec); leave: wks_free_policy (policy); xfree (policy); xfree (submission_to); xfree (addrspec); return err; } /* Check whether the key for USERID is available in the WKD. */ static gpg_error_t command_check (char *userid) { gpg_error_t err; char *addrspec = NULL; estream_t key = NULL; char *fpr = NULL; uidinfo_list_t mboxes = NULL; uidinfo_list_t sl; int found = 0; addrspec = mailbox_from_userid (userid); if (!addrspec) { log_error (_("\"%s\" is not a proper mail address\n"), userid); err = gpg_error (GPG_ERR_INV_USER_ID); goto leave; } /* Get the submission address. */ err = wkd_get_key (addrspec, &key); switch (gpg_err_code (err)) { case 0: if (opt.verbose) log_info ("public key for '%s' found via WKD\n", addrspec); /* Fixme: Check that the key contains the user id. */ break; case GPG_ERR_NO_DATA: /* No such key. */ if (opt.verbose) log_info ("public key for '%s' NOT found via WKD\n", addrspec); err = gpg_error (GPG_ERR_NO_PUBKEY); log_inc_errorcount (); break; case GPG_ERR_UNKNOWN_HOST: if (opt.verbose) log_info ("error looking up '%s' via WKD: %s\n", addrspec, gpg_strerror (err)); err = gpg_error (GPG_ERR_NOT_SUPPORTED); break; default: log_error ("error looking up '%s' via WKD: %s\n", addrspec, gpg_strerror (err)); break; } if (err) goto leave; /* Look closer at the key. */ err = wks_list_key (key, &fpr, &mboxes); if (err) { log_error ("error parsing key: %s\n", gpg_strerror (err)); err = gpg_error (GPG_ERR_NO_PUBKEY); goto leave; } if (opt.verbose) log_info ("fingerprint: %s\n", fpr); for (sl = mboxes; sl; sl = sl->next) { if (sl->mbox && !strcmp (sl->mbox, addrspec)) found = 1; if (opt.verbose) { log_info (" user-id: %s\n", sl->uid); log_info (" created: %s\n", asctimestamp (sl->created)); if (sl->mbox) log_info (" addr-spec: %s\n", sl->mbox); + if (sl->expired || sl->revoked) + log_info (" flags:%s%s\n", + sl->expired? " expired":"", sl->revoked?" revoked":""); } } if (!found) { log_error ("public key for '%s' has no user id with the mail address\n", addrspec); err = gpg_error (GPG_ERR_CERT_REVOKED); } leave: xfree (fpr); free_uidinfo_list (mboxes); es_fclose (key); xfree (addrspec); return err; } /* Locate the key by fingerprint and userid and send a publication * request. */ static gpg_error_t command_send (const char *fingerprint, const char *userid) { gpg_error_t err; KEYDB_SEARCH_DESC desc; char *addrspec = NULL; estream_t key = NULL; estream_t keyenc = NULL; char *submission_to = NULL; mime_maker_t mime = NULL; policy_flags_t policy = NULL; int no_encrypt = 0; int posteo_hack = 0; const char *domain; uidinfo_list_t uidlist = NULL; uidinfo_list_t uid, thisuid; time_t thistime; + int any; if (classify_user_id (fingerprint, &desc, 1) || !(desc.mode == KEYDB_SEARCH_MODE_FPR || desc.mode == KEYDB_SEARCH_MODE_FPR20)) { log_error (_("\"%s\" is not a fingerprint\n"), fingerprint); err = gpg_error (GPG_ERR_INV_NAME); goto leave; } addrspec = mailbox_from_userid (userid); if (!addrspec) { log_error (_("\"%s\" is not a proper mail address\n"), userid); err = gpg_error (GPG_ERR_INV_USER_ID); goto leave; } err = wks_get_key (&key, fingerprint, addrspec, 0, 1); if (err) goto leave; domain = strchr (addrspec, '@'); log_assert (domain); domain++; /* Get the submission address. */ if (fake_submission_addr) { policy = xcalloc (1, sizeof *policy); submission_to = xstrdup (fake_submission_addr); err = 0; } else { err = get_policy_and_sa (addrspec, 0, &policy, &submission_to); if (err) goto leave; if (!submission_to) { log_error (_("this domain probably doesn't support WKS.\n")); err = gpg_error (GPG_ERR_NO_DATA); goto leave; } } log_info ("submitting request to '%s'\n", submission_to); if (policy->auth_submit) log_info ("no confirmation required for '%s'\n", addrspec); /* In case the key has several uids with the same addr-spec we will * use the newest one. */ err = wks_list_key (key, NULL, &uidlist); if (err) { log_error ("error parsing key: %s\n",gpg_strerror (err)); err = gpg_error (GPG_ERR_NO_PUBKEY); goto leave; } thistime = 0; thisuid = NULL; + any = 0; for (uid = uidlist; uid; uid = uid->next) { if (!uid->mbox) continue; /* Should not happen anyway. */ if (policy->mailbox_only && ascii_strcasecmp (uid->uid, uid->mbox)) continue; /* UID has more than just the mailbox. */ + if (uid->expired) + { + if (opt.verbose) + log_info ("ignoring expired user id '%s'\n", uid->uid); + continue; + } + any = 1; if (uid->created > thistime) { thistime = uid->created; thisuid = uid; } } if (!thisuid) thisuid = uidlist; /* This is the case for a missing timestamp. */ + if (!any) + { + log_error ("public key %s has no mail address '%s'\n", + fingerprint, addrspec); + err = gpg_error (GPG_ERR_INV_USER_ID); + goto leave; + } + if (opt.verbose) log_info ("submitting key with user id '%s'\n", thisuid->uid); /* If we have more than one user id we need to filter the key to * include only THISUID. */ if (uidlist->next) { estream_t newkey; es_rewind (key); err = wks_filter_uid (&newkey, key, thisuid->uid, 1); if (err) { log_error ("error filtering key: %s\n", gpg_strerror (err)); err = gpg_error (GPG_ERR_NO_PUBKEY); goto leave; } es_fclose (key); key = newkey; } if (policy->mailbox_only && (!thisuid->mbox || ascii_strcasecmp (thisuid->uid, thisuid->mbox))) { log_info ("Warning: policy requires 'mailbox-only'" " - adding user id '%s'\n", addrspec); err = add_user_id (fingerprint, addrspec); if (err) goto leave; /* Need to get the key again. This time we request filtering * for the full user id, so that we do not need check and filter * the key again. */ es_fclose (key); key = NULL; err = wks_get_key (&key, fingerprint, addrspec, 1, 1); if (err) goto leave; } if (opt.add_revocs) { if (es_fseek (key, 0, SEEK_END)) { err = gpg_error_from_syserror (); log_error ("error seeking stream: %s\n", gpg_strerror (err)); goto leave; } err = wks_find_add_revocs (key, addrspec); if (err) { log_error ("error finding revocations for '%s': %s\n", addrspec, gpg_strerror (err)); goto leave; } } /* Now put the armor around the key. */ { estream_t newkey; es_rewind (key); err = wks_armor_key (&newkey, key, no_encrypt? NULL /* */ : ("Content-Type: application/pgp-keys\n" "\n")); if (err) { log_error ("error armoring key: %s\n", gpg_strerror (err)); goto leave; } es_fclose (key); key = newkey; } /* Hack to support posteo but let them disable this by setting the * new policy-version flag. */ if (policy->protocol_version < 3 && !ascii_strcasecmp (domain, "posteo.de")) { log_info ("Warning: Using draft-1 method for domain '%s'\n", domain); no_encrypt = 1; posteo_hack = 1; } /* Encrypt the key part. */ if (!no_encrypt) { es_rewind (key); err = encrypt_response (&keyenc, key, submission_to, fingerprint); if (err) goto leave; es_fclose (key); key = NULL; } /* Send the key. */ err = mime_maker_new (&mime, NULL); if (err) goto leave; err = mime_maker_add_header (mime, "From", addrspec); if (err) goto leave; err = mime_maker_add_header (mime, "To", submission_to); if (err) goto leave; err = mime_maker_add_header (mime, "Subject", "Key publishing request"); if (err) goto leave; /* Tell server which draft we support. */ err = mime_maker_add_header (mime, "Wks-Draft-Version", STR2(WKS_DRAFT_VERSION)); if (err) goto leave; if (no_encrypt) { void *data; size_t datalen; if (posteo_hack) { /* Needs a multipart/mixed with one(!) attachment. It does * not grok a non-multipart mail. */ err = mime_maker_add_header (mime, "Content-Type", "multipart/mixed"); if (err) goto leave; err = mime_maker_add_container (mime); if (err) goto leave; } err = mime_maker_add_header (mime, "Content-type", "application/pgp-keys"); if (err) goto leave; if (es_fclose_snatch (key, &data, &datalen)) { err = gpg_error_from_syserror (); goto leave; } key = NULL; err = mime_maker_add_body_data (mime, data, datalen); xfree (data); if (err) goto leave; } else { err = mime_maker_add_header (mime, "Content-Type", "multipart/encrypted; " "protocol=\"application/pgp-encrypted\""); if (err) goto leave; err = mime_maker_add_container (mime); if (err) goto leave; err = mime_maker_add_header (mime, "Content-Type", "application/pgp-encrypted"); if (err) goto leave; err = mime_maker_add_body (mime, "Version: 1\n"); if (err) goto leave; err = mime_maker_add_header (mime, "Content-Type", "application/octet-stream"); if (err) goto leave; err = mime_maker_add_stream (mime, &keyenc); if (err) goto leave; } err = wks_send_mime (mime); leave: mime_maker_release (mime); xfree (submission_to); free_uidinfo_list (uidlist); es_fclose (keyenc); es_fclose (key); wks_free_policy (policy); xfree (policy); xfree (addrspec); return err; } static void encrypt_response_status_cb (void *opaque, const char *keyword, char *args) { gpg_error_t *failure = opaque; char *fields[2]; if (DBG_CRYPTO) log_debug ("gpg status: %s %s\n", keyword, args); if (!strcmp (keyword, "FAILURE")) { if (split_fields (args, fields, DIM (fields)) >= 2 && !strcmp (fields[0], "encrypt")) *failure = strtoul (fields[1], NULL, 10); } } /* Encrypt the INPUT stream to a new stream which is stored at success * at R_OUTPUT. Encryption is done for ADDRSPEC and for FINGERPRINT * (so that the sent message may later be inspected by the user). We * currently retrieve that key from the WKD, DANE, or from "local". * "local" is last to prefer the latest key version but use a local * copy in case we are working offline. It might be useful for the * server to send the fingerprint of its encryption key - or even the * entire key back. */ static gpg_error_t encrypt_response (estream_t *r_output, estream_t input, const char *addrspec, const char *fingerprint) { gpg_error_t err; ccparray_t ccp; const char **argv; estream_t output; gpg_error_t gpg_err = 0; *r_output = NULL; output = es_fopenmem (0, "w+b"); if (!output) { err = gpg_error_from_syserror (); log_error ("error allocating memory buffer: %s\n", gpg_strerror (err)); return err; } ccparray_init (&ccp, 0); ccparray_put (&ccp, "--no-options"); if (opt.verbose < 2) ccparray_put (&ccp, "--quiet"); else ccparray_put (&ccp, "--verbose"); ccparray_put (&ccp, "--batch"); ccparray_put (&ccp, "--status-fd=2"); ccparray_put (&ccp, "--always-trust"); ccparray_put (&ccp, "--armor"); ccparray_put (&ccp, "-z0"); /* No compression for improved robustness. */ if (fake_submission_addr) ccparray_put (&ccp, "--auto-key-locate=clear,local"); else ccparray_put (&ccp, "--auto-key-locate=clear,wkd,dane,local"); ccparray_put (&ccp, "--recipient"); ccparray_put (&ccp, addrspec); ccparray_put (&ccp, "--recipient"); ccparray_put (&ccp, fingerprint); ccparray_put (&ccp, "--encrypt"); ccparray_put (&ccp, "--"); ccparray_put (&ccp, NULL); argv = ccparray_get (&ccp, NULL); if (!argv) { err = gpg_error_from_syserror (); goto leave; } err = gnupg_exec_tool_stream (opt.gpg_program, argv, input, NULL, output, encrypt_response_status_cb, &gpg_err); if (err) { if (gpg_err) err = gpg_err; log_error ("encryption failed: %s\n", gpg_strerror (err)); goto leave; } es_rewind (output); *r_output = output; output = NULL; leave: es_fclose (output); xfree (argv); return err; } static gpg_error_t send_confirmation_response (const char *sender, const char *address, const char *nonce, int encrypt, const char *fingerprint) { gpg_error_t err; estream_t body = NULL; estream_t bodyenc = NULL; mime_maker_t mime = NULL; body = es_fopenmem (0, "w+b"); if (!body) { err = gpg_error_from_syserror (); log_error ("error allocating memory buffer: %s\n", gpg_strerror (err)); return err; } /* It is fine to use 8 bit encoding because that is encrypted and * only our client will see it. */ if (encrypt) { es_fputs ("Content-Type: application/vnd.gnupg.wks\n" "Content-Transfer-Encoding: 8bit\n" "\n", body); } es_fprintf (body, ("type: confirmation-response\n" "sender: %s\n" "address: %s\n" "nonce: %s\n"), sender, address, nonce); es_rewind (body); if (encrypt) { err = encrypt_response (&bodyenc, body, sender, fingerprint); if (err) goto leave; es_fclose (body); body = NULL; } err = mime_maker_new (&mime, NULL); if (err) goto leave; err = mime_maker_add_header (mime, "From", address); if (err) goto leave; err = mime_maker_add_header (mime, "To", sender); if (err) goto leave; err = mime_maker_add_header (mime, "Subject", "Key publication confirmation"); if (err) goto leave; err = mime_maker_add_header (mime, "Wks-Draft-Version", STR2(WKS_DRAFT_VERSION)); if (err) goto leave; if (encrypt) { err = mime_maker_add_header (mime, "Content-Type", "multipart/encrypted; " "protocol=\"application/pgp-encrypted\""); if (err) goto leave; err = mime_maker_add_container (mime); if (err) goto leave; err = mime_maker_add_header (mime, "Content-Type", "application/pgp-encrypted"); if (err) goto leave; err = mime_maker_add_body (mime, "Version: 1\n"); if (err) goto leave; err = mime_maker_add_header (mime, "Content-Type", "application/octet-stream"); if (err) goto leave; err = mime_maker_add_stream (mime, &bodyenc); if (err) goto leave; } else { err = mime_maker_add_header (mime, "Content-Type", "application/vnd.gnupg.wks"); if (err) goto leave; err = mime_maker_add_stream (mime, &body); if (err) goto leave; } err = wks_send_mime (mime); leave: mime_maker_release (mime); es_fclose (bodyenc); es_fclose (body); return err; } /* Reply to a confirmation request. The MSG has already been * decrypted and we only need to send the nonce back. MAINFPR is * either NULL or the primary key fingerprint of the key used to * decrypt the request. */ static gpg_error_t process_confirmation_request (estream_t msg, const char *mainfpr) { gpg_error_t err; nvc_t nvc; nve_t item; const char *value, *sender, *address, *fingerprint, *nonce; err = nvc_parse (&nvc, NULL, msg); if (err) { log_error ("parsing the WKS message failed: %s\n", gpg_strerror (err)); goto leave; } if (DBG_MIME) { log_debug ("request follows:\n"); nvc_write (nvc, log_get_stream ()); } /* Check that this is a confirmation request. */ if (!((item = nvc_lookup (nvc, "type:")) && (value = nve_value (item)) && !strcmp (value, "confirmation-request"))) { if (item && value) log_error ("received unexpected wks message '%s'\n", value); else log_error ("received invalid wks message: %s\n", "'type' missing"); err = gpg_error (GPG_ERR_UNEXPECTED_MSG); goto leave; } /* Get the fingerprint. */ if (!((item = nvc_lookup (nvc, "fingerprint:")) && (value = nve_value (item)) && strlen (value) >= 40)) { log_error ("received invalid wks message: %s\n", "'fingerprint' missing or invalid"); err = gpg_error (GPG_ERR_INV_DATA); goto leave; } fingerprint = value; /* Check that the fingerprint matches the key used to decrypt the * message. In --read mode or with the old format we don't have the * decryption key; thus we can't bail out. */ if (!mainfpr || ascii_strcasecmp (mainfpr, fingerprint)) { log_info ("target fingerprint: %s\n", fingerprint); log_info ("but decrypted with: %s\n", mainfpr); log_error ("confirmation request not decrypted with target key\n"); if (mainfpr) { err = gpg_error (GPG_ERR_INV_DATA); goto leave; } } /* Get the address. */ if (!((item = nvc_lookup (nvc, "address:")) && (value = nve_value (item)) && is_valid_mailbox (value))) { log_error ("received invalid wks message: %s\n", "'address' missing or invalid"); err = gpg_error (GPG_ERR_INV_DATA); goto leave; } address = value; /* FIXME: Check that the "address" matches the User ID we want to * publish. */ /* Get the sender. */ if (!((item = nvc_lookup (nvc, "sender:")) && (value = nve_value (item)) && is_valid_mailbox (value))) { log_error ("received invalid wks message: %s\n", "'sender' missing or invalid"); err = gpg_error (GPG_ERR_INV_DATA); goto leave; } sender = value; /* FIXME: Check that the "sender" matches the From: address. */ /* Get the nonce. */ if (!((item = nvc_lookup (nvc, "nonce:")) && (value = nve_value (item)) && strlen (value) > 16)) { log_error ("received invalid wks message: %s\n", "'nonce' missing or too short"); err = gpg_error (GPG_ERR_INV_DATA); goto leave; } nonce = value; /* Send the confirmation. If no key was found, try again without * encryption. */ err = send_confirmation_response (sender, address, nonce, 1, fingerprint); if (gpg_err_code (err) == GPG_ERR_NO_PUBKEY) { log_info ("no encryption key found - sending response in the clear\n"); err = send_confirmation_response (sender, address, nonce, 0, NULL); } leave: nvc_release (nvc); return err; } /* Read a confirmation request and decrypt it if needed. This * function may not be used with a mail or MIME message but only with * the actual encrypted or plaintext WKS data. */ static gpg_error_t read_confirmation_request (estream_t msg) { gpg_error_t err; int c; estream_t plaintext = NULL; /* We take a really simple approach to check whether MSG is * encrypted: We know that an encrypted message is always armored * and thus starts with a few dashes. It is even sufficient to * check for a single dash, because that can never be a proper first * WKS data octet. We need to skip leading spaces, though. */ while ((c = es_fgetc (msg)) == ' ' || c == '\t' || c == '\r' || c == '\n') ; if (c == EOF) { log_error ("can't process an empty message\n"); return gpg_error (GPG_ERR_INV_DATA); } if (es_ungetc (c, msg) != c) { log_error ("error ungetting octet from message\n"); return gpg_error (GPG_ERR_INTERNAL); } if (c != '-') err = process_confirmation_request (msg, NULL); else { struct decrypt_stream_parm_s decinfo; err = decrypt_stream (&plaintext, &decinfo, msg); if (err) log_error ("decryption failed: %s\n", gpg_strerror (err)); else if (decinfo.otrust != 'u') { err = gpg_error (GPG_ERR_WRONG_SECKEY); log_error ("key used to decrypt the confirmation request" " was not generated by us\n"); } else err = process_confirmation_request (plaintext, decinfo.mainfpr); xfree (decinfo.fpr); xfree (decinfo.mainfpr); } es_fclose (plaintext); return err; } /* Called from the MIME receiver to process the plain text data in MSG. */ static gpg_error_t command_receive_cb (void *opaque, const char *mediatype, estream_t msg, unsigned int flags) { gpg_error_t err; (void)opaque; (void)flags; if (!strcmp (mediatype, "application/vnd.gnupg.wks")) err = read_confirmation_request (msg); else { log_info ("ignoring unexpected message of type '%s'\n", mediatype); err = gpg_error (GPG_ERR_UNEXPECTED_MSG); } return err; } /* An object used to communicate with the mirror_one_key callback. */ struct { const char *domain; int anyerror; unsigned int nkeys; /* Number of keys processed. */ unsigned int nuids; /* Number of published user ids. */ } mirror_one_key_parm; /* Return true if the Given a mail DOMAIN and the full addrspec MBOX * match. */ static int domain_matches_mbox (const char *domain, const char *mbox) { const char *s; if (!domain || !mbox) return 0; s = strchr (domain, '@'); if (s) domain = s+1; if (!*domain) return 0; /* Not a valid domain. */ s = strchr (mbox, '@'); if (!s || !s[1]) return 0; /* Not a valid mbox. */ mbox = s+1; return !ascii_strcasecmp (domain, mbox); } /* Core of mirror_one_key with the goal of mirroring just one uid. * UIDLIST is used to figure out whether the given MBOX occurs several * times in UIDLIST and then to single out the newest one. This is * so that for a key with * uid: Joe Someone * uid: Joe * only the news user id (and thus its self-signature) is used. * UIDLIST is nodified to set all MBOX fields to NULL for a processed * user id. FPR is the fingerprint of the key. */ static gpg_error_t mirror_one_keys_userid (estream_t key, const char *mbox, uidinfo_list_t uidlist, const char *fpr) { gpg_error_t err; uidinfo_list_t uid, thisuid, firstuid; time_t thistime; estream_t newkey = NULL; /* Find the UID we want to use. */ thistime = 0; thisuid = firstuid = NULL; for (uid = uidlist; uid; uid = uid->next) { if ((uid->flags & 1) || !uid->mbox || strcmp (uid->mbox, mbox)) continue; /* Already processed or no matching mbox. */ uid->flags |= 1; /* Set "processed" flag. */ if (!firstuid) firstuid = uid; if (uid->created > thistime) { thistime = uid->created; thisuid = uid; } } if (!thisuid) thisuid = firstuid; /* This is the case for a missing timestamp. */ if (!thisuid) { log_error ("error finding the user id for %s (%s)\n", fpr, mbox); err = gpg_error (GPG_ERR_NO_USER_ID); goto leave; } /* Always filter the key so that the result will be non-armored. */ es_rewind (key); err = wks_filter_uid (&newkey, key, thisuid->uid, 1); if (err) { log_error ("error filtering key %s: %s\n", fpr, gpg_strerror (err)); err = gpg_error (GPG_ERR_NO_PUBKEY); goto leave; } if (opt.add_revocs) { if (es_fseek (newkey, 0, SEEK_END)) { err = gpg_error_from_syserror (); log_error ("error seeking stream: %s\n", gpg_strerror (err)); goto leave; } err = wks_find_add_revocs (newkey, mbox); if (err) { log_error ("error finding revocations for '%s': %s\n", mbox, gpg_strerror (err)); goto leave; } es_rewind (newkey); } err = wks_install_key_core (newkey, mbox); if (opt.verbose) log_info ("key %s published for '%s'\n", fpr, mbox); mirror_one_key_parm.nuids++; if (!opt.quiet && !(mirror_one_key_parm.nuids % 25)) log_info ("%u user ids from %d keys so far\n", mirror_one_key_parm.nuids, mirror_one_key_parm.nkeys); leave: es_fclose (newkey); return err; } /* The callback used by command_mirror. It received an estream with * one key and should return success to process the next key. */ static gpg_error_t mirror_one_key (estream_t key) { gpg_error_t err = 0; char *fpr; uidinfo_list_t uidlist = NULL; uidinfo_list_t uid; const char *domain = mirror_one_key_parm.domain; /* List the key to get all user ids. */ err = wks_list_key (key, &fpr, &uidlist); if (err) { log_error ("error parsing a key: %s - skipped\n", gpg_strerror (err)); mirror_one_key_parm.anyerror = 1; err = 0; goto leave; } for (uid = uidlist; uid; uid = uid->next) { if (!uid->mbox || (uid->flags & 1)) continue; /* No mail box or already processed. */ + if (uid->expired) + continue; if (!domain_matches_mbox (domain, uid->mbox)) continue; /* We don't want this one. */ if (is_in_blacklist (uid->mbox)) continue; err = mirror_one_keys_userid (key, uid->mbox, uidlist, fpr); if (err) { log_error ("error processing key %s: %s - skipped\n", fpr, gpg_strerror (err)); mirror_one_key_parm.anyerror = 1; err = 0; goto leave; } } mirror_one_key_parm.nkeys++; leave: free_uidinfo_list (uidlist); xfree (fpr); return err; } /* Copy the keys from the configured LDAP server into a local WKD. * DOMAINLIST is an array of domain names to restrict the copy to only * the given domains; if it is NULL all keys are mirrored. */ static gpg_error_t command_mirror (char *domainlist[]) { gpg_error_t err; const char *domain; char *domainbuf = NULL; mirror_one_key_parm.anyerror = 0; mirror_one_key_parm.nkeys = 0; mirror_one_key_parm.nuids = 0; if (!domainlist) { mirror_one_key_parm.domain = ""; err = wkd_dirmngr_ks_get (NULL, mirror_one_key); } else { while ((domain = *domainlist++)) { if (*domain != '.' && domain[1] != '@') { /* This does not already specify a mail search by * domain. Change it. */ xfree (domainbuf); domainbuf = xstrconcat (".@", domain, NULL); domain = domainbuf; } mirror_one_key_parm.domain = domain; if (opt.verbose) log_info ("mirroring keys for domain '%s'\n", domain+2); err = wkd_dirmngr_ks_get (domain, mirror_one_key); if (err) break; } } if (!opt.quiet) log_info ("a total of %u user ids from %d keys published\n", mirror_one_key_parm.nuids, mirror_one_key_parm.nkeys); if (err) log_error ("error mirroring LDAP directory: %s <%s>\n", gpg_strerror (err), gpg_strsource (err)); else if (mirror_one_key_parm.anyerror) log_info ("warning: errors encountered - not all keys are mirrored\n"); xfree (domainbuf); return err; } diff --git a/tools/gpg-wks.h b/tools/gpg-wks.h index 59a0aca74..a7c17ca52 100644 --- a/tools/gpg-wks.h +++ b/tools/gpg-wks.h @@ -1,132 +1,134 @@ /* gpg-wks.h - Common definitions for wks server and client. * Copyright (C) 2016 g10 Code GmbH * Copyright (C) 2016 Bundesamt für Sicherheit in der Informationstechnik * * 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 Lesser General Public License as * published by the Free Software Foundation; either version 2.1 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 Lesser General Public License * along with this program; if not, see . */ #ifndef GNUPG_GPG_WKS_H #define GNUPG_GPG_WKS_H #include "../common/util.h" #include "../common/strlist.h" #include "mime-maker.h" /* The draft version we implement. */ #define WKS_DRAFT_VERSION 3 /* We keep all global options in the structure OPT. */ EXTERN_UNLESS_MAIN_MODULE struct { int verbose; unsigned int debug; int quiet; int use_sendmail; int with_colons; int no_autostart; int add_revocs; const char *output; const char *gpg_program; const char *directory; const char *default_from; strlist_t extra_headers; } opt; /* Debug values and macros. */ #define DBG_MIME_VALUE 1 /* Debug the MIME structure. */ #define DBG_PARSER_VALUE 2 /* Debug the Mail parser. */ #define DBG_CRYPTO_VALUE 4 /* Debug low level crypto. */ #define DBG_MEMORY_VALUE 32 /* Debug memory allocation stuff. */ #define DBG_MEMSTAT_VALUE 128 /* Show memory statistics. */ #define DBG_IPC_VALUE 1024 /* Debug assuan communication. */ #define DBG_EXTPROG_VALUE 16384 /* debug external program calls */ #define DBG_MIME (opt.debug & DBG_MIME_VALUE) #define DBG_PARSER (opt.debug & DBG_PARSER_VALUE) #define DBG_CRYPTO (opt.debug & DBG_CRYPTO_VALUE) /* The parsed policy flags. */ struct policy_flags_s { char *submission_address; unsigned int mailbox_only : 1; unsigned int dane_only : 1; unsigned int auth_submit : 1; unsigned int protocol_version; /* The supported WKS_DRAFT_VERION or 0 */ unsigned int max_pending; /* Seconds to wait for a confirmation. */ }; typedef struct policy_flags_s *policy_flags_t; /* An object to convey user ids of a key. */ struct uidinfo_list_s { struct uidinfo_list_s *next; time_t created; /* Time the userid was created. */ char *mbox; /* NULL or the malloced mailbox from UID. */ unsigned int flags; /* These flags are cleared on creation. */ + unsigned int expired:1; + unsigned int revoked:1; char uid[1]; }; typedef struct uidinfo_list_s *uidinfo_list_t; /*-- wks-util.c --*/ void wks_set_status_fd (int fd); void wks_write_status (int no, const char *format, ...) GPGRT_ATTR_PRINTF(2,3); void free_uidinfo_list (uidinfo_list_t list); gpg_error_t wks_get_key (estream_t *r_key, const char *fingerprint, const char *addrspec, int exact, int binary); gpg_error_t wks_list_key (estream_t key, char **r_fpr, uidinfo_list_t *r_mboxes); gpg_error_t wks_filter_uid (estream_t *r_newkey, estream_t key, const char *uid, int binary); gpg_error_t wks_armor_key (estream_t *r_newkey, estream_t key, const char *prefix); gpg_error_t wks_find_add_revocs (estream_t key, const char *addrspec); gpg_error_t wks_send_mime (mime_maker_t mime); gpg_error_t wks_parse_policy (policy_flags_t flags, estream_t stream, int ignore_unknown); void wks_free_policy (policy_flags_t policy); gpg_error_t wks_fname_from_userid (const char *userid, int hash_only, char **r_fname, char **r_addrspec); gpg_error_t wks_compute_hu_fname (char **r_fname, const char *addrspec); gpg_error_t wks_install_key_core (estream_t key, const char *addrspec); gpg_error_t wks_cmd_install_key (const char *fname, const char *userid); gpg_error_t wks_cmd_remove_key (const char *userid); gpg_error_t wks_cmd_print_wkd_hash (const char *userid); gpg_error_t wks_cmd_print_wkd_url (const char *userid); /*-- wks-receive.c --*/ /* Flag values for the receive callback. */ #define WKS_RECEIVE_DRAFT2 1 gpg_error_t wks_receive (estream_t fp, gpg_error_t (*result_cb)(void *opaque, const char *mediatype, estream_t data, unsigned int flags), void *cb_data); #endif /*GNUPG_GPG_WKS_H*/ diff --git a/tools/wks-util.c b/tools/wks-util.c index 91a4a7da8..2e8541491 100644 --- a/tools/wks-util.c +++ b/tools/wks-util.c @@ -1,1429 +1,1469 @@ /* wks-utils.c - Common helper functions for wks tools * Copyright (C) 2016 g10 Code GmbH * Copyright (C) 2016 Bundesamt für Sicherheit in der Informationstechnik * * 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 Lesser General Public License as * published by the Free Software Foundation; either version 2.1 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. */ #include #include #include #include #include #include #include #include "../common/util.h" #include "../common/status.h" #include "../common/ccparray.h" #include "../common/exectool.h" #include "../common/zb32.h" #include "../common/userids.h" #include "../common/mbox-util.h" #include "../common/sysutils.h" #include "mime-maker.h" #include "send-mail.h" #include "gpg-wks.h" /* The stream to output the status information. Output is disabled if this is NULL. */ static estream_t statusfp; /* Set the status FD. */ void wks_set_status_fd (int fd) { static int last_fd = -1; if (fd != -1 && last_fd == fd) return; if (statusfp && statusfp != es_stdout && statusfp != es_stderr) es_fclose (statusfp); statusfp = NULL; if (fd == -1) return; if (fd == 1) statusfp = es_stdout; else if (fd == 2) statusfp = es_stderr; else statusfp = es_fdopen (fd, "w"); if (!statusfp) { log_fatal ("can't open fd %d for status output: %s\n", fd, gpg_strerror (gpg_error_from_syserror ())); } last_fd = fd; } /* Write a status line with code NO followed by the outout of the * printf style FORMAT. The caller needs to make sure that LFs and * CRs are not printed. */ void wks_write_status (int no, const char *format, ...) { va_list arg_ptr; if (!statusfp) return; /* Not enabled. */ es_fputs ("[GNUPG:] ", statusfp); es_fputs (get_status_string (no), statusfp); if (format) { es_putc (' ', statusfp); va_start (arg_ptr, format); es_vfprintf (statusfp, format, arg_ptr); va_end (arg_ptr); } es_putc ('\n', statusfp); } /* Append UID to LIST and return the new item. On success LIST is * updated. C-style escaping is removed from UID. On error ERRNO is * set and NULL returned. */ static uidinfo_list_t -append_to_uidinfo_list (uidinfo_list_t *list, const char *uid, time_t created) +append_to_uidinfo_list (uidinfo_list_t *list, const char *uid, time_t created, + int expired, int revoked) { uidinfo_list_t r, sl; char *plainuid; plainuid = decode_c_string (uid); if (!plainuid) return NULL; sl = xtrymalloc (sizeof *sl + strlen (plainuid)); if (!sl) { xfree (plainuid); return NULL; } strcpy (sl->uid, plainuid); sl->created = created; sl->flags = 0; sl->mbox = mailbox_from_userid (plainuid); + sl->expired = !!expired; + sl->revoked = !!revoked; sl->next = NULL; if (!*list) *list = sl; else { for (r = *list; r->next; r = r->next ) ; r->next = sl; } xfree (plainuid); return sl; } /* Free the list of uid infos at LIST. */ void free_uidinfo_list (uidinfo_list_t list) { while (list) { uidinfo_list_t tmp = list->next; xfree (list->mbox); xfree (list); list = tmp; } } static void debug_gpg_invocation (const char *func, const char **argv) { int i; if (!(opt.debug & DBG_EXTPROG_VALUE)) return; log_debug ("%s: exec '%s' with", func, opt.gpg_program); for (i=0; argv[i]; i++) log_printf (" '%s'", argv[i]); log_printf ("\n"); } struct get_key_status_parm_s { const char *fpr; int found; int count; }; static void get_key_status_cb (void *opaque, const char *keyword, char *args) { struct get_key_status_parm_s *parm = opaque; if (DBG_CRYPTO) log_debug ("%s: %s\n", keyword, args); if (!strcmp (keyword, "EXPORTED")) { parm->count++; if (!ascii_strcasecmp (args, parm->fpr)) parm->found = 1; } } /* Get a key by fingerprint from gpg's keyring and make sure that the * mail address ADDRSPEC is included in the key. If EXACT is set the * returned user id must match Addrspec exactly and not just in the * addr-spec (mailbox) part. The key is returned as a new memory * stream at R_KEY. If BINARY is set the returned key is * non-armored. */ gpg_error_t wks_get_key (estream_t *r_key, const char *fingerprint, const char *addrspec, int exact, int binary) { gpg_error_t err; ccparray_t ccp; const char **argv = NULL; estream_t key = NULL; struct get_key_status_parm_s parm; char *filterexp = NULL; memset (&parm, 0, sizeof parm); *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; } /* Prefix the key with the MIME content type. */ if (!binary) es_fputs ("Content-Type: application/pgp-keys\n" "\n", key); filterexp = es_bsprintf ("keep-uid=%s= %s", exact? "uid":"mbox", addrspec); if (!filterexp) { 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 < 2) ccparray_put (&ccp, "--quiet"); else ccparray_put (&ccp, "--verbose"); ccparray_put (&ccp, "--batch"); ccparray_put (&ccp, "--status-fd=2"); ccparray_put (&ccp, "--always-trust"); if (!binary) ccparray_put (&ccp, "--armor"); ccparray_put (&ccp, "--export-options=export-minimal"); ccparray_put (&ccp, "--export-filter"); ccparray_put (&ccp, filterexp); 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; debug_gpg_invocation (__func__, argv); err = gnupg_exec_tool_stream (opt.gpg_program, argv, NULL, NULL, key, get_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); xfree (filterexp); return err; } /* Helper for wks_list_key and wks_filter_uid. */ static void key_status_cb (void *opaque, const char *keyword, char *args) { (void)opaque; if (DBG_CRYPTO) log_debug ("gpg status: %s %s\n", keyword, args); } +/* Parse field 1 and set revoked and expired on return. */ +static void +set_expired_revoked (const char *string, int *expired, int *revoked) +{ + *expired = *revoked = 0; + /* Look at letters and stop at the first digit. */ + for ( ;*string && !digitp (string); string++) + { + if (*string == 'e') + *expired = 1; + else if (*string == 'r') + *revoked = 1; + } +} + + /* Run gpg on KEY and store the primary fingerprint at R_FPR and the * list of mailboxes at R_MBOXES. Returns 0 on success; on error NULL * is stored at R_FPR and R_MBOXES and an error code is returned. * R_FPR may be NULL if the fingerprint is not needed. */ gpg_error_t wks_list_key (estream_t key, char **r_fpr, uidinfo_list_t *r_mboxes) { gpg_error_t err; ccparray_t ccp; const char **argv; estream_t listing; char *line = NULL; size_t length_of_line = 0; size_t maxlen; ssize_t len; char **fields = NULL; int nfields; int lnr; char *fpr = NULL; uidinfo_list_t mboxes = NULL; + int expired, revoked; if (r_fpr) *r_fpr = NULL; *r_mboxes = NULL; /* 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; } ccparray_init (&ccp, 0); ccparray_put (&ccp, "--no-options"); if (opt.verbose < 2) ccparray_put (&ccp, "--quiet"); else ccparray_put (&ccp, "--verbose"); ccparray_put (&ccp, "--batch"); ccparray_put (&ccp, "--status-fd=2"); ccparray_put (&ccp, "--always-trust"); ccparray_put (&ccp, "--with-colons"); ccparray_put (&ccp, "--dry-run"); ccparray_put (&ccp, "--import-options=import-minimal,import-show"); ccparray_put (&ccp, "--import"); ccparray_put (&ccp, NULL); argv = ccparray_get (&ccp, NULL); if (!argv) { err = gpg_error_from_syserror (); goto leave; } debug_gpg_invocation (__func__, argv); err = gnupg_exec_tool_stream (opt.gpg_program, argv, key, NULL, listing, key_status_cb, NULL); if (err) { log_error ("import failed: %s\n", gpg_strerror (err)); goto leave; } es_rewind (listing); lnr = 0; + expired = revoked = 0; maxlen = 2048; /* Set limit. */ while ((len = es_read_line (listing, &line, &length_of_line, &maxlen)) > 0) { lnr++; 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'; /* log_debug ("line '%s'\n", line); */ xfree (fields); fields = strtokenize_nt (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; } if (!strcmp (fields[0], "sec")) { /* gpg may return "sec" as the first record - but we do not * accept secret keys. */ err = gpg_error (GPG_ERR_NO_PUBKEY); goto leave; } if (lnr == 1 && strcmp (fields[0], "pub")) { /* First record is not a public key. */ err = gpg_error (GPG_ERR_INV_ENGINE); goto leave; } - if (lnr > 1 && !strcmp (fields[0], "pub")) + if (!strcmp (fields[0], "pub")) { - /* More than one public key. */ - err = gpg_error (GPG_ERR_TOO_MANY); - goto leave; + if (lnr > 1) + { + /* More than one public key. */ + err = gpg_error (GPG_ERR_TOO_MANY); + goto leave; + } + if (nfields > 1) + set_expired_revoked (fields[1], &expired, &revoked); + else + expired = revoked = 0; } + if (!strcmp (fields[0], "sub") || !strcmp (fields[0], "ssb")) break; /* We can stop parsing here. */ if (!strcmp (fields[0], "fpr") && nfields > 9 && !fpr) { fpr = xtrystrdup (fields[9]); if (!fpr) { err = gpg_error_from_syserror (); goto leave; } } else if (!strcmp (fields[0], "uid") && nfields > 9) { + int uidexpired, uidrevoked; + + set_expired_revoked (fields[1], &uidexpired, &uidrevoked); if (!append_to_uidinfo_list (&mboxes, fields[9], - parse_timestamp (fields[5], NULL))) + parse_timestamp (fields[5], NULL), + expired || uidexpired, + revoked || uidrevoked)) { err = gpg_error_from_syserror (); goto leave; } } } if (len < 0 || es_ferror (listing)) { err = gpg_error_from_syserror (); log_error ("error reading memory stream\n"); goto leave; } if (!fpr) { err = gpg_error (GPG_ERR_NO_PUBKEY); goto leave; } if (r_fpr) { *r_fpr = fpr; fpr = NULL; } *r_mboxes = mboxes; mboxes = NULL; leave: xfree (fpr); free_uidinfo_list (mboxes); xfree (fields); es_free (line); xfree (argv); es_fclose (listing); return err; } /* Run gpg as a filter on KEY and write the output to a new stream * stored at R_NEWKEY. The new key will contain only the user id UID. * Returns 0 on success. Only one key is expected in KEY. If BINARY * is set the resulting key is returned as a binary (non-armored) * keyblock. */ gpg_error_t wks_filter_uid (estream_t *r_newkey, estream_t key, const char *uid, int binary) { gpg_error_t err; ccparray_t ccp; const char **argv = NULL; estream_t newkey; char *filterexp = NULL; *r_newkey = NULL; /* Open a memory stream. */ newkey = es_fopenmem (0, "w+b"); if (!newkey) { err = gpg_error_from_syserror (); log_error ("error allocating memory buffer: %s\n", gpg_strerror (err)); return err; } /* Prefix the key with the MIME content type. */ if (!binary) es_fputs ("Content-Type: application/pgp-keys\n" "\n", newkey); filterexp = es_bsprintf ("keep-uid=-t uid= %s", uid); if (!filterexp) { 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 < 2) ccparray_put (&ccp, "--quiet"); else ccparray_put (&ccp, "--verbose"); ccparray_put (&ccp, "--batch"); ccparray_put (&ccp, "--status-fd=2"); ccparray_put (&ccp, "--always-trust"); if (!binary) ccparray_put (&ccp, "--armor"); ccparray_put (&ccp, "--import-options=import-export"); ccparray_put (&ccp, "--import-filter"); ccparray_put (&ccp, filterexp); ccparray_put (&ccp, "--import"); ccparray_put (&ccp, NULL); argv = ccparray_get (&ccp, NULL); if (!argv) { err = gpg_error_from_syserror (); goto leave; } debug_gpg_invocation (__func__, argv); err = gnupg_exec_tool_stream (opt.gpg_program, argv, key, NULL, newkey, key_status_cb, NULL); if (err) { log_error ("import/export failed: %s\n", gpg_strerror (err)); goto leave; } es_rewind (newkey); *r_newkey = newkey; newkey = NULL; leave: xfree (filterexp); xfree (argv); es_fclose (newkey); return err; } /* Put the ascii-armor around KEY and return that as a new estream * object at R_NEWKEY. Caller must make sure that KEY has been seeked * to the right position (usually by calling es_rewind). The * resulting NEWKEY has already been rewound. If PREFIX is not NULL, * its content is written to NEWKEY propr to the armor; this may be * used for MIME headers. */ gpg_error_t wks_armor_key (estream_t *r_newkey, estream_t key, const char *prefix) { gpg_error_t err; estream_t newkey; struct b64state b64state; char buffer[4096]; size_t nread; *r_newkey = NULL; newkey = es_fopenmem (0, "w+b"); if (!newkey) { err = gpg_error_from_syserror (); return err; } if (prefix) es_fputs (prefix, newkey); err = b64enc_start_es (&b64state, newkey, "PGP PUBLIC KEY BLOCK"); if (err) goto leave; do { nread = es_fread (buffer, 1, sizeof buffer, key); if (!nread) break; err = b64enc_write (&b64state, buffer, nread); if (err) goto leave; } while (!es_feof (key) && !es_ferror (key)); if (!es_feof (key) || es_ferror (key)) { err = gpg_error_from_syserror (); goto leave; } err = b64enc_finish (&b64state); if (err) goto leave; es_rewind (newkey); *r_newkey = newkey; newkey = NULL; leave: es_fclose (newkey); return err; } /* Run gpg to export the revocation certificates for ADDRSPEC. Add * them to KEY which is expected to be non-armored keyblock. */ gpg_error_t wks_find_add_revocs (estream_t key, const char *addrspec) { gpg_error_t err; ccparray_t ccp; const char **argv = NULL; char *filterexp = NULL; filterexp = es_bsprintf ("select=mbox= %s", addrspec); if (!filterexp) { 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 < 2) ccparray_put (&ccp, "--quiet"); else ccparray_put (&ccp, "--verbose"); ccparray_put (&ccp, "--batch"); ccparray_put (&ccp, "--status-fd=2"); ccparray_put (&ccp, "--export-options=export-revocs"); ccparray_put (&ccp, "--export-filter"); ccparray_put (&ccp, filterexp); ccparray_put (&ccp, "--export"); ccparray_put (&ccp, addrspec); ccparray_put (&ccp, NULL); argv = ccparray_get (&ccp, NULL); if (!argv) { err = gpg_error_from_syserror (); goto leave; } debug_gpg_invocation (__func__, argv); err = gnupg_exec_tool_stream (opt.gpg_program, argv, NULL, NULL, key, key_status_cb, NULL); if (err) { log_error ("exporting revocs failed: %s\n", gpg_strerror (err)); goto leave; } leave: xfree (filterexp); xfree (argv); return err; } /* Helper to write mail to the output(s). */ gpg_error_t wks_send_mime (mime_maker_t mime) { gpg_error_t err; estream_t mail; /* Without any option we take a short path. */ if (!opt.use_sendmail && !opt.output) { es_set_binary (es_stdout); return mime_maker_make (mime, es_stdout); } mail = es_fopenmem (0, "w+b"); if (!mail) { err = gpg_error_from_syserror (); return err; } err = mime_maker_make (mime, mail); if (!err && opt.output) { es_rewind (mail); err = send_mail_to_file (mail, opt.output); } if (!err && opt.use_sendmail) { es_rewind (mail); err = send_mail (mail); } es_fclose (mail); return err; } /* Parse the policy flags by reading them from STREAM and storing them * into FLAGS. If IGNORE_UNKNOWN is set unknown keywords are * ignored. */ gpg_error_t wks_parse_policy (policy_flags_t flags, estream_t stream, int ignore_unknown) { enum tokens { TOK_SUBMISSION_ADDRESS, TOK_MAILBOX_ONLY, TOK_DANE_ONLY, TOK_AUTH_SUBMIT, TOK_MAX_PENDING, TOK_PROTOCOL_VERSION }; static struct { const char *name; enum tokens token; } keywords[] = { { "submission-address", TOK_SUBMISSION_ADDRESS }, { "mailbox-only", TOK_MAILBOX_ONLY }, { "dane-only", TOK_DANE_ONLY }, { "auth-submit", TOK_AUTH_SUBMIT }, { "max-pending", TOK_MAX_PENDING }, { "protocol-version", TOK_PROTOCOL_VERSION } }; gpg_error_t err = 0; int lnr = 0; char line[1024]; char *p, *keyword, *value; int i, n; memset (flags, 0, sizeof *flags); while (es_fgets (line, DIM(line)-1, stream) ) { lnr++; n = strlen (line); if (!n || line[n-1] != '\n') { err = gpg_error (*line? GPG_ERR_LINE_TOO_LONG : GPG_ERR_INCOMPLETE_LINE); break; } trim_trailing_spaces (line); /* Skip empty and comment lines. */ for (p=line; spacep (p); p++) ; if (!*p || *p == '#') continue; if (*p == ':') { err = gpg_error (GPG_ERR_SYNTAX); break; } keyword = p; value = NULL; if ((p = strchr (p, ':'))) { /* Colon found: Keyword with value. */ *p++ = 0; for (; spacep (p); p++) ; if (!*p) { err = gpg_error (GPG_ERR_MISSING_VALUE); break; } value = p; } for (i=0; i < DIM (keywords); i++) if (!ascii_strcasecmp (keywords[i].name, keyword)) break; if (!(i < DIM (keywords))) { if (ignore_unknown) continue; err = gpg_error (GPG_ERR_INV_NAME); break; } switch (keywords[i].token) { case TOK_SUBMISSION_ADDRESS: if (!value || !*value) { err = gpg_error (GPG_ERR_SYNTAX); goto leave; } xfree (flags->submission_address); flags->submission_address = xtrystrdup (value); if (!flags->submission_address) { err = gpg_error_from_syserror (); goto leave; } break; case TOK_MAILBOX_ONLY: flags->mailbox_only = 1; break; case TOK_DANE_ONLY: flags->dane_only = 1; break; case TOK_AUTH_SUBMIT: flags->auth_submit = 1; break; case TOK_MAX_PENDING: if (!value) { err = gpg_error (GPG_ERR_SYNTAX); goto leave; } /* FIXME: Define whether these are seconds, hours, or days * and decide whether to allow other units. */ flags->max_pending = atoi (value); break; case TOK_PROTOCOL_VERSION: if (!value) { err = gpg_error (GPG_ERR_SYNTAX); goto leave; } flags->protocol_version = atoi (value); break; } } if (!err && !es_feof (stream)) err = gpg_error_from_syserror (); leave: if (err) log_error ("error reading '%s', line %d: %s\n", es_fname_get (stream), lnr, gpg_strerror (err)); return err; } void wks_free_policy (policy_flags_t policy) { if (policy) { xfree (policy->submission_address); memset (policy, 0, sizeof *policy); } } /* Write the content of SRC to the new file FNAME. */ static gpg_error_t write_to_file (estream_t src, const char *fname) { gpg_error_t err; estream_t dst; char buffer[4096]; size_t nread, written; dst = es_fopen (fname, "wb"); if (!dst) return gpg_error_from_syserror (); do { nread = es_fread (buffer, 1, sizeof buffer, src); if (!nread) break; written = es_fwrite (buffer, 1, nread, dst); if (written != nread) break; } while (!es_feof (src) && !es_ferror (src) && !es_ferror (dst)); if (!es_feof (src) || es_ferror (src) || es_ferror (dst)) { err = gpg_error_from_syserror (); es_fclose (dst); gnupg_remove (fname); return err; } if (es_fclose (dst)) { err = gpg_error_from_syserror (); log_error ("error closing '%s': %s\n", fname, gpg_strerror (err)); return err; } return 0; } /* Return the filename and optionally the addrspec for USERID at * R_FNAME and R_ADDRSPEC. R_ADDRSPEC might also be set on error. If * HASH_ONLY is set only the has is returned at R_FNAME and no file is * created. */ gpg_error_t wks_fname_from_userid (const char *userid, int hash_only, char **r_fname, char **r_addrspec) { gpg_error_t err; char *addrspec = NULL; const char *domain; char *hash = NULL; const char *s; char shaxbuf[32]; /* Used for SHA-1 and SHA-256 */ *r_fname = NULL; if (r_addrspec) *r_addrspec = NULL; addrspec = mailbox_from_userid (userid); if (!addrspec) { if (opt.verbose || hash_only) log_info ("\"%s\" is not a proper mail address\n", userid); err = gpg_error (GPG_ERR_INV_USER_ID); goto leave; } domain = strchr (addrspec, '@'); log_assert (domain); domain++; if (strchr (domain, '/') || strchr (domain, '\\')) { log_info ("invalid domain detected ('%s')\n", domain); err = gpg_error (GPG_ERR_NOT_FOUND); goto leave; } /* Hash user ID and create filename. */ s = strchr (addrspec, '@'); log_assert (s); gcry_md_hash_buffer (GCRY_MD_SHA1, shaxbuf, addrspec, s - addrspec); hash = zb32_encode (shaxbuf, 8*20); if (!hash) { err = gpg_error_from_syserror (); goto leave; } if (hash_only) { *r_fname = hash; hash = NULL; err = 0; } else { *r_fname = make_filename_try (opt.directory, domain, "hu", hash, NULL); if (!*r_fname) err = gpg_error_from_syserror (); else err = 0; } leave: if (r_addrspec && addrspec) *r_addrspec = addrspec; else xfree (addrspec); xfree (hash); return err; } /* Compute the the full file name for the key with ADDRSPEC and return * it at R_FNAME. */ gpg_error_t wks_compute_hu_fname (char **r_fname, const char *addrspec) { gpg_error_t err; char *hash; const char *domain; char sha1buf[20]; char *fname; struct stat sb; *r_fname = NULL; domain = strchr (addrspec, '@'); if (!domain || !domain[1] || domain == addrspec) return gpg_error (GPG_ERR_INV_ARG); domain++; if (strchr (domain, '/') || strchr (domain, '\\')) { log_info ("invalid domain detected ('%s')\n", domain); return gpg_error (GPG_ERR_NOT_FOUND); } gcry_md_hash_buffer (GCRY_MD_SHA1, sha1buf, addrspec, domain - addrspec - 1); hash = zb32_encode (sha1buf, 8*20); if (!hash) return gpg_error_from_syserror (); /* Try to create missing directories below opt.directory. */ fname = make_filename_try (opt.directory, domain, NULL); if (fname && gnupg_stat (fname, &sb) && gpg_err_code_from_syserror () == GPG_ERR_ENOENT) if (!gnupg_mkdir (fname, "-rwxr-xr-x") && opt.verbose) log_info ("directory '%s' created\n", fname); xfree (fname); fname = make_filename_try (opt.directory, domain, "hu", NULL); if (fname && gnupg_stat (fname, &sb) && gpg_err_code_from_syserror () == GPG_ERR_ENOENT) if (!gnupg_mkdir (fname, "-rwxr-xr-x") && opt.verbose) log_info ("directory '%s' created\n", fname); xfree (fname); /* Create the filename. */ fname = make_filename_try (opt.directory, domain, "hu", hash, NULL); err = fname? 0 : gpg_error_from_syserror (); if (err) xfree (fname); else *r_fname = fname; /* Okay. */ xfree (hash); return err; } /* Make sure that a policy file exists for addrspec. Directories must * already exist. */ static gpg_error_t ensure_policy_file (const char *addrspec) { gpg_err_code_t ec; gpg_error_t err; const char *domain; char *fname; estream_t fp; domain = strchr (addrspec, '@'); if (!domain || !domain[1] || domain == addrspec) return gpg_error (GPG_ERR_INV_ARG); domain++; if (strchr (domain, '/') || strchr (domain, '\\')) { log_info ("invalid domain detected ('%s')\n", domain); return gpg_error (GPG_ERR_NOT_FOUND); } /* Create the filename. */ fname = make_filename_try (opt.directory, domain, "policy", NULL); err = fname? 0 : gpg_error_from_syserror (); if (err) goto leave; /* First a quick check whether it already exists. */ if (!(ec = gnupg_access (fname, F_OK))) { err = 0; /* File already exists. */ goto leave; } err = gpg_error (ec); if (gpg_err_code (err) == GPG_ERR_ENOENT) err = 0; else { log_error ("domain %s: problem with '%s': %s\n", domain, fname, gpg_strerror (err)); goto leave; } /* Now create the file. */ fp = es_fopen (fname, "wxb"); if (!fp) { err = gpg_error_from_syserror (); if (gpg_err_code (err) == GPG_ERR_EEXIST) err = 0; /* Was created between the gnupg_access() and es_fopen(). */ else log_error ("domain %s: error creating '%s': %s\n", domain, fname, gpg_strerror (err)); goto leave; } es_fprintf (fp, "# Policy flags for domain %s\n", domain); if (es_ferror (fp) || es_fclose (fp)) { err = gpg_error_from_syserror (); log_error ("error writing '%s': %s\n", fname, gpg_strerror (err)); goto leave; } if (opt.verbose) log_info ("policy file '%s' created\n", fname); /* Make sure the policy file world readable. */ if (gnupg_chmod (fname, "-rw-r--r--")) { err = gpg_error_from_syserror (); log_error ("can't set permissions of '%s': %s\n", fname, gpg_strerror (err)); goto leave; } leave: xfree (fname); return err; } /* Helper form wks_cmd_install_key. */ static gpg_error_t install_key_from_spec_file (const char *fname) { gpg_error_t err; estream_t fp; char *line = NULL; size_t linelen = 0; size_t maxlen = 2048; char *fields[2]; unsigned int lnr = 0; if (!fname || !strcmp (fname, "")) fp = es_stdin; else fp = es_fopen (fname, "rb"); if (!fp) { err = gpg_error_from_syserror (); log_error ("error reading '%s': %s\n", fname, gpg_strerror (err)); goto leave; } while (es_read_line (fp, &line, &linelen, &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; } lnr++; trim_spaces (line); if (!*line || *line == '#') continue; if (split_fields (line, fields, DIM(fields)) < 2) { log_error ("error reading '%s': syntax error at line %u\n", fname, lnr); continue; } err = wks_cmd_install_key (fields[0], fields[1]); if (err) goto leave; } if (es_ferror (fp)) { err = gpg_error_from_syserror (); log_error ("error reading '%s': %s\n", fname, gpg_strerror (err)); goto leave; } leave: if (fp != es_stdin) es_fclose (fp); es_free (line); return err; } /* The core of the code to install a key as a file. */ gpg_error_t wks_install_key_core (estream_t key, const char *addrspec) { gpg_error_t err; char *huname = NULL; /* Hash user ID and create filename. */ err = wks_compute_hu_fname (&huname, addrspec); if (err) goto leave; /* Now that wks_compute_hu_fname has created missing directories we * can create a policy file if it does not exist. */ err = ensure_policy_file (addrspec); if (err) goto leave; /* Publish. */ err = write_to_file (key, huname); if (err) { log_error ("copying key to '%s' failed: %s\n", huname,gpg_strerror (err)); goto leave; } /* Make sure it is world readable. */ if (gnupg_chmod (huname, "-rw-r--r--")) log_error ("can't set permissions of '%s': %s\n", huname, gpg_strerror (gpg_err_code_from_syserror())); leave: xfree (huname); return err; } /* Install a single key into the WKD by reading FNAME and extracting * USERID. If USERID is NULL FNAME is expected to be a list of fpr * mbox lines and for each line the respective key will be * installed. */ gpg_error_t wks_cmd_install_key (const char *fname, const char *userid) { gpg_error_t err; KEYDB_SEARCH_DESC desc; estream_t fp = NULL; char *addrspec = NULL; char *fpr = NULL; uidinfo_list_t uidlist = NULL; uidinfo_list_t uid, thisuid; time_t thistime; int any; if (!userid) return install_key_from_spec_file (fname); addrspec = mailbox_from_userid (userid); if (!addrspec) { log_error ("\"%s\" is not a proper mail address\n", userid); err = gpg_error (GPG_ERR_INV_USER_ID); goto leave; } if (!classify_user_id (fname, &desc, 1) && (desc.mode == KEYDB_SEARCH_MODE_FPR || desc.mode == KEYDB_SEARCH_MODE_FPR20)) { /* FNAME looks like a fingerprint. Get the key from the * standard keyring. */ err = wks_get_key (&fp, fname, addrspec, 0, 1); if (err) { log_error ("error getting key '%s' (uid='%s'): %s\n", fname, addrspec, gpg_strerror (err)); goto leave; } } else /* Take it from the file */ { fp = es_fopen (fname, "rb"); if (!fp) { err = gpg_error_from_syserror (); log_error ("error reading '%s': %s\n", fname, gpg_strerror (err)); goto leave; } } /* List the key so that we can figure out the newest UID with the * requested addrspec. */ err = wks_list_key (fp, &fpr, &uidlist); if (err) { log_error ("error parsing key: %s\n", gpg_strerror (err)); err = gpg_error (GPG_ERR_NO_PUBKEY); goto leave; } thistime = 0; thisuid = NULL; any = 0; for (uid = uidlist; uid; uid = uid->next) { if (!uid->mbox) continue; /* Should not happen anyway. */ if (ascii_strcasecmp (uid->mbox, addrspec)) continue; /* Not the requested addrspec. */ + if (uid->expired) + { + if (opt.verbose) + log_info ("ignoring expired user id '%s'\n", uid->uid); + continue; + } any = 1; if (uid->created > thistime) { thistime = uid->created; thisuid = uid; } } if (!thisuid) thisuid = uidlist; /* This is the case for a missing timestamp. */ if (!any) { log_error ("public key in '%s' has no mail address '%s'\n", fname, addrspec); err = gpg_error (GPG_ERR_INV_USER_ID); goto leave; } if (opt.verbose) log_info ("using key with user id '%s'\n", thisuid->uid); { estream_t fp2; es_rewind (fp); err = wks_filter_uid (&fp2, fp, thisuid->uid, 1); if (err) { log_error ("error filtering key: %s\n", gpg_strerror (err)); err = gpg_error (GPG_ERR_NO_PUBKEY); goto leave; } es_fclose (fp); fp = fp2; } if (opt.add_revocs) { if (es_fseek (fp, 0, SEEK_END)) { err = gpg_error_from_syserror (); log_error ("error seeking stream: %s\n", gpg_strerror (err)); goto leave; } err = wks_find_add_revocs (fp, addrspec); if (err) { log_error ("error finding revocations for '%s': %s\n", addrspec, gpg_strerror (err)); goto leave; } es_rewind (fp); } err = wks_install_key_core (fp, addrspec); if (!opt.quiet) log_info ("key %s published for '%s'\n", fpr, addrspec); leave: free_uidinfo_list (uidlist); xfree (fpr); xfree (addrspec); es_fclose (fp); return err; } /* Remove the key with mail address in USERID. */ gpg_error_t wks_cmd_remove_key (const char *userid) { gpg_error_t err; char *addrspec = NULL; char *fname = NULL; err = wks_fname_from_userid (userid, 0, &fname, &addrspec); if (err) goto leave; if (gnupg_remove (fname)) { err = gpg_error_from_syserror (); if (gpg_err_code (err) == GPG_ERR_ENOENT) { if (!opt.quiet) log_info ("key for '%s' is not installed\n", addrspec); log_inc_errorcount (); err = 0; } else log_error ("error removing '%s': %s\n", fname, gpg_strerror (err)); goto leave; } if (opt.verbose) log_info ("key for '%s' removed\n", addrspec); err = 0; leave: xfree (fname); xfree (addrspec); return err; } /* Print the WKD hash for the user id to stdout. */ gpg_error_t wks_cmd_print_wkd_hash (const char *userid) { gpg_error_t err; char *addrspec, *fname; err = wks_fname_from_userid (userid, 1, &fname, &addrspec); if (err) return err; es_printf ("%s %s\n", fname, addrspec); xfree (fname); xfree (addrspec); return err; } /* Print the WKD URL for the user id to stdout. */ gpg_error_t wks_cmd_print_wkd_url (const char *userid) { gpg_error_t err; char *addrspec, *fname; char *domain; err = wks_fname_from_userid (userid, 1, &fname, &addrspec); if (err) return err; domain = strchr (addrspec, '@'); if (domain) *domain++ = 0; es_printf ("https://openpgpkey.%s/.well-known/openpgpkey/%s/hu/%s?l=%s\n", domain, domain, fname, addrspec); xfree (fname); xfree (addrspec); return err; }