diff --git a/tools/gpg-wks-client.c b/tools/gpg-wks-client.c index c8ff16651..78e4fe420 100644 --- a/tools/gpg-wks-client.c +++ b/tools/gpg-wks-client.c @@ -1,1493 +1,1494 @@ /* gpg-wks-client.c - A client for the Web Key Service protocols. * Copyright (C) 2016 Werner Koch * 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 . */ #include #include #include #include #include #include #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, aInstallKey, aRemoveKey, oGpgProgram, oSend, oFakeSubmissionAddr, oStatusFD, oWithColons, 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 (aInstallKey, "install-key", "install a key into a directory"), ARGPARSE_c (aRemoveKey, "remove-key", "remove a key from a directory"), 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 (oWithColons, "with-colons", "@"), ARGPARSE_s_s (oDirectory, "directory", "@"), 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; static void wrong_args (const char *text) GPGRT_ATTR_NORETURN; 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); /* Print usage information and provide strings for help. */ static const char * my_strusage( int level ) { const char *p; switch (level) { case 11: p = "gpg-wks-client"; break; case 12: p = "@GNUPG@"; break; case 13: p = VERSION; 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 && optfile_parse (NULL, NULL, 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 aSupported: case aCreate: case aReceive: case aRead: case aCheck: case aInstallKey: case aRemoveKey: cmd = pargs->r_opt; break; default: pargs->err = 2; break; } } return cmd; } /* gpg-wks-client main. */ int main (int argc, char **argv) { gpg_error_t 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); 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), 1); /* Check that the top directory exists. */ if (cmd == aInstallKey || cmd == aRemoveKey) { struct stat sb; if (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 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; 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 log_get_errorcount (0)? 1:0; } /* 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) ccparray_put (&ccp, "--quiet"); else if (opt.verbose > 1) 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) ccparray_put (&ccp, "--quiet"); else if (opt.verbose > 1) ccparray_put (&ccp, "--verbose"); ccparray_put (&ccp, "--batch"); ccparray_put (&ccp, "--status-fd=2"); ccparray_put (&ccp, "--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 (!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; 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); 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; 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->created > thistime) { thistime = uid->created; thisuid = uid; } } if (!thisuid) thisuid = uidlist; /* This is the case for a missing timestamp. */ 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, 0); 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); if (err) goto leave; } /* 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, n; 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; /* We need to skip over the first line which has a content-type * header not needed here. */ for (n=0; n < datalen ; n++) if (((const char *)data)[n] == '\n') { n++; break; } err = mime_maker_add_body_data (mime, (char*)data + n, datalen - n); 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) ccparray_put (&ccp, "--quiet"); else if (opt.verbose > 1) ccparray_put (&ccp, "--verbose"); ccparray_put (&ccp, "--batch"); ccparray_put (&ccp, "--status-fd=2"); ccparray_put (&ccp, "--always-trust"); ccparray_put (&ccp, "--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; } diff --git a/tools/gpg-wks-server.c b/tools/gpg-wks-server.c index 1a0ba8f4f..f83ef6528 100644 --- a/tools/gpg-wks-server.c +++ b/tools/gpg-wks-server.c @@ -1,1983 +1,1984 @@ /* gpg-wks-server.c - A server for the Web Key Service protocols. * Copyright (C) 2016, 2018 Werner Koch * 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 . */ /* The Web Key Service I-D defines an update protocol to store a * public key in the Web Key Directory. The current specification is * draft-koch-openpgp-webkey-service-05.txt. */ #include #include #include #include #include #include #include #include #include "../common/util.h" #include "../common/init.h" #include "../common/sysutils.h" #include "../common/userids.h" #include "../common/ccparray.h" #include "../common/exectool.h" #include "../common/zb32.h" #include "../common/mbox-util.h" #include "../common/name-value.h" #include "mime-maker.h" #include "send-mail.h" #include "gpg-wks.h" /* The time we wait for a confirmation response. */ #define PENDING_TTL (86400 * 3) /* 3 days. */ /* Constants to identify the commands and options. */ enum cmd_and_opt_values { aNull = 0, oQuiet = 'q', oVerbose = 'v', oOutput = 'o', oDirectory = 'C', oDebug = 500, aReceive, aCron, aListDomains, aInstallKey, aRevokeKey, aRemoveKey, aCheck, oGpgProgram, oSend, oFrom, oHeader, oWithDir, oWithFile, oDummy }; /* The list of commands and options. */ static ARGPARSE_OPTS opts[] = { ARGPARSE_group (300, ("@Commands:\n ")), ARGPARSE_c (aReceive, "receive", ("receive a submission or confirmation")), ARGPARSE_c (aCron, "cron", ("run regular jobs")), ARGPARSE_c (aListDomains, "list-domains", ("list configured domains")), ARGPARSE_c (aCheck, "check", ("check whether a key is installed")), ARGPARSE_c (aCheck, "check-key", "@"), ARGPARSE_c (aInstallKey, "install-key", "install a key from FILE into the WKD"), ARGPARSE_c (aRemoveKey, "remove-key", "remove a key from the WKD"), ARGPARSE_c (aRevokeKey, "revoke-key", "mark a key as revoked"), 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_s (oDirectory, "directory", "|DIR|use DIR as top directory"), ARGPARSE_s_s (oFrom, "from", "|ADDR|use ADDR as the default sender"), ARGPARSE_s_s (oHeader, "header" , "|NAME=VALUE|add \"NAME: VALUE\" as header to all mails"), ARGPARSE_s_n (oWithDir, "with-dir", "@"), ARGPARSE_s_n (oWithFile, "with-file", "@"), 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 } }; /* State for processing a message. */ struct server_ctx_s { char *fpr; uidinfo_list_t mboxes; /* List with addr-specs taken from the UIDs. */ unsigned int draft_version_2:1; /* Client supports the draft 2. */ }; typedef struct server_ctx_s *server_ctx_t; /* Flag for --with-dir. */ static int opt_with_dir; /* Flag for --with-file. */ static int opt_with_file; /* Prototypes. */ static gpg_error_t get_domain_list (strlist_t *r_list); static gpg_error_t command_receive_cb (void *opaque, const char *mediatype, estream_t fp, unsigned int flags); static gpg_error_t command_list_domains (void); static gpg_error_t command_revoke_key (const char *mailaddr); static gpg_error_t command_check_key (const char *mailaddr); static gpg_error_t command_cron (void); /* Print usage information and provide strings for help. */ static const char * my_strusage( int level ) { const char *p; switch (level) { case 11: p = "gpg-wks-server"; break; case 12: p = "@GNUPG@"; break; case 13: p = VERSION; 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-server command [options] (-h for help)"); break; case 41: p = ("Syntax: gpg-wks-server command [options]\n" "Server for the Web Key Service protocol\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 && optfile_parse (NULL, NULL, 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 oFrom: opt.default_from = pargs->r.ret_str; break; case oHeader: append_to_strlist (&opt.extra_headers, pargs->r.ret_str); break; case oSend: opt.use_sendmail = 1; break; case oOutput: opt.output = pargs->r.ret_str; break; case oWithDir: opt_with_dir = 1; break; case oWithFile: opt_with_file = 1; break; case aReceive: case aCron: case aListDomains: case aCheck: case aInstallKey: case aRemoveKey: case aRevokeKey: cmd = pargs->r_opt; break; default: pargs->err = 2; break; } } return cmd; } /* gpg-wks-server main. */ int main (int argc, char **argv) { gpg_error_t err, firsterr; ARGPARSE_ARGS pargs; enum cmd_and_opt_values cmd; gnupg_reopen_std ("gpg-wks-server"); set_strusage (my_strusage); log_set_prefix ("gpg-wks-server", GPGRT_LOG_WITH_PREFIX); /* Make sure that our subsystems are ready. */ init_common_subsystems (&argc, &argv); /* Parse the command line. */ pargs.argc = &argc; pargs.argv = &argv; pargs.flags = ARGPARSE_FLAG_KEEP; cmd = parse_arguments (&pargs, opts); 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 = "/var/lib/gnupg/wks"; /* Check for syntax errors in the --header option to avoid later * error messages with a not easy to find cause */ if (opt.extra_headers) { strlist_t sl; for (sl = opt.extra_headers; sl; sl = sl->next) { err = mime_maker_add_header (NULL, sl->d, NULL); if (err) log_error ("syntax error in \"--header %s\": %s\n", sl->d, gpg_strerror (err)); } } if (log_get_errorcount (0)) exit (2); /* Check that we have a working directory. */ #if defined(HAVE_STAT) { struct stat sb; if (stat (opt.directory, &sb)) { err = gpg_error_from_syserror (); log_error ("error accessing directory '%s': %s\n", opt.directory, gpg_strerror (err)); exit (2); } if (!S_ISDIR(sb.st_mode)) { log_error ("error accessing directory '%s': %s\n", opt.directory, "not a directory"); exit (2); } if (sb.st_uid != getuid()) { log_error ("directory '%s' not owned by user\n", opt.directory); exit (2); } if ((sb.st_mode & (S_IROTH|S_IWOTH))) { log_error ("directory '%s' has too relaxed permissions\n", opt.directory); log_info ("Fix by running: chmod o-rw '%s'\n", opt.directory); exit (2); } } #else /*!HAVE_STAT*/ log_fatal ("program build w/o stat() call\n"); #endif /*!HAVE_STAT*/ /* Run the selected command. */ switch (cmd) { case aReceive: if (argc) wrong_args ("--receive"); err = wks_receive (es_stdin, command_receive_cb, NULL); break; case aCron: if (argc) wrong_args ("--cron"); err = command_cron (); break; case aListDomains: err = command_list_domains (); 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 aRevokeKey: if (argc != 1) wrong_args ("--revoke-key USER-ID"); err = command_revoke_key (*argv); break; case aCheck: if (!argc) wrong_args ("--check USER-IDs"); firsterr = 0; for (; argc; argc--, argv++) { err = command_check_key (*argv); if (!firsterr) firsterr = err; } err = firsterr; break; default: usage (1); err = gpg_error (GPG_ERR_BUG); break; } if (err) log_error ("command failed: %s\n", gpg_strerror (err)); return log_get_errorcount (0)? 1:0; } /* Take the key in KEYFILE and write it to OUTFILE in binary encoding. * If ADDRSPEC is given only matching user IDs are included in the * output. */ static gpg_error_t copy_key_as_binary (const char *keyfile, const char *outfile, const char *addrspec) { gpg_error_t err; ccparray_t ccp; const char **argv = NULL; char *filterexp = NULL; if (addrspec) { filterexp = es_bsprintf ("keep-uid=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) ccparray_put (&ccp, "--quiet"); else if (opt.verbose > 1) ccparray_put (&ccp, "--verbose"); ccparray_put (&ccp, "--batch"); ccparray_put (&ccp, "--yes"); ccparray_put (&ccp, "--always-trust"); ccparray_put (&ccp, "--no-keyring"); ccparray_put (&ccp, "--output"); ccparray_put (&ccp, outfile); ccparray_put (&ccp, "--import-options=import-export"); if (filterexp) { ccparray_put (&ccp, "--import-filter"); ccparray_put (&ccp, filterexp); } ccparray_put (&ccp, "--import"); ccparray_put (&ccp, "--"); ccparray_put (&ccp, keyfile); 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 ("%s failed: %s\n", __func__, gpg_strerror (err)); goto leave; } leave: xfree (filterexp); xfree (argv); return err; } /* Take the key in KEYFILE and write it to DANEFILE using the DANE * output format. */ static gpg_error_t copy_key_as_dane (const char *keyfile, const char *danefile) { gpg_error_t err; ccparray_t ccp; const char **argv; ccparray_init (&ccp, 0); ccparray_put (&ccp, "--no-options"); if (!opt.verbose) ccparray_put (&ccp, "--quiet"); else if (opt.verbose > 1) ccparray_put (&ccp, "--verbose"); ccparray_put (&ccp, "--batch"); ccparray_put (&ccp, "--yes"); ccparray_put (&ccp, "--always-trust"); ccparray_put (&ccp, "--no-keyring"); ccparray_put (&ccp, "--output"); ccparray_put (&ccp, danefile); ccparray_put (&ccp, "--export-options=export-dane"); ccparray_put (&ccp, "--import-options=import-export"); ccparray_put (&ccp, "--import"); ccparray_put (&ccp, "--"); ccparray_put (&ccp, keyfile); 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 ("%s failed: %s\n", __func__, gpg_strerror (err)); goto leave; } leave: xfree (argv); return err; } static void encrypt_stream_status_cb (void *opaque, const char *keyword, char *args) { (void)opaque; if (DBG_CRYPTO) log_debug ("gpg status: %s %s\n", keyword, args); } /* Encrypt the INPUT stream to a new stream which is stored at success * at R_OUTPUT. Encryption is done for the key in file KEYFIL. */ static gpg_error_t encrypt_stream (estream_t *r_output, estream_t input, const char *keyfile) { gpg_error_t err; ccparray_t ccp; const char **argv; estream_t output; *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) ccparray_put (&ccp, "--quiet"); else if (opt.verbose > 1) ccparray_put (&ccp, "--verbose"); ccparray_put (&ccp, "--batch"); ccparray_put (&ccp, "--status-fd=2"); ccparray_put (&ccp, "--always-trust"); ccparray_put (&ccp, "--no-keyring"); ccparray_put (&ccp, "--armor"); + ccparray_put (&ccp, "-z0"); /* No compression for improved robustness. */ ccparray_put (&ccp, "--recipient-file"); ccparray_put (&ccp, keyfile); 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_stream_status_cb, NULL); if (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 void sign_stream_status_cb (void *opaque, const char *keyword, char *args) { (void)opaque; if (DBG_CRYPTO) log_debug ("gpg status: %s %s\n", keyword, args); } /* Sign the INPUT stream to a new stream which is stored at success at * R_OUTPUT. A detached signature is created using the key specified * by USERID. */ static gpg_error_t sign_stream (estream_t *r_output, estream_t input, const char *userid) { gpg_error_t err; ccparray_t ccp; const char **argv; estream_t output; *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) ccparray_put (&ccp, "--quiet"); else if (opt.verbose > 1) ccparray_put (&ccp, "--verbose"); ccparray_put (&ccp, "--batch"); ccparray_put (&ccp, "--status-fd=2"); ccparray_put (&ccp, "--armor"); ccparray_put (&ccp, "--local-user"); ccparray_put (&ccp, userid); ccparray_put (&ccp, "--detach-sign"); 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, sign_stream_status_cb, NULL); if (err) { log_error ("signing failed: %s\n", gpg_strerror (err)); goto leave; } es_rewind (output); *r_output = output; output = NULL; leave: es_fclose (output); xfree (argv); return err; } /* Get the submission address for address MBOX. Caller must free the * value. If no address can be found NULL is returned. */ static char * get_submission_address (const char *mbox) { gpg_error_t err; const char *domain; char *fname, *line, *p; size_t n; estream_t fp; domain = strchr (mbox, '@'); if (!domain) return NULL; domain++; fname = make_filename_try (opt.directory, domain, "submission-address", NULL); if (!fname) { err = gpg_error_from_syserror (); log_error ("make_filename failed in %s: %s\n", __func__, gpg_strerror (err)); return NULL; } fp = es_fopen (fname, "r"); if (!fp) { err = gpg_error_from_syserror (); if (gpg_err_code (err) == GPG_ERR_ENOENT) log_info ("Note: no specific submission address configured" " for domain '%s'\n", domain); else log_error ("error reading '%s': %s\n", fname, gpg_strerror (err)); xfree (fname); return NULL; } line = NULL; n = 0; if (es_getline (&line, &n, fp) < 0) { err = gpg_error_from_syserror (); log_error ("error reading '%s': %s\n", fname, gpg_strerror (err)); xfree (line); es_fclose (fp); xfree (fname); return NULL; } es_fclose (fp); xfree (fname); p = strchr (line, '\n'); if (p) *p = 0; trim_spaces (line); if (!is_valid_mailbox (line)) { log_error ("invalid submission address for domain '%s' detected\n", domain); xfree (line); return NULL; } return line; } /* Get the policy flags for address MBOX and store them in POLICY. */ static gpg_error_t get_policy_flags (policy_flags_t policy, const char *mbox) { gpg_error_t err; const char *domain; char *fname; estream_t fp; memset (policy, 0, sizeof *policy); domain = strchr (mbox, '@'); if (!domain) return gpg_error (GPG_ERR_INV_USER_ID); domain++; fname = make_filename_try (opt.directory, domain, "policy", NULL); if (!fname) { err = gpg_error_from_syserror (); log_error ("make_filename failed in %s: %s\n", __func__, gpg_strerror (err)); return err; } fp = es_fopen (fname, "r"); if (!fp) { err = gpg_error_from_syserror (); if (gpg_err_code (err) == GPG_ERR_ENOENT) err = 0; else log_error ("error reading '%s': %s\n", fname, gpg_strerror (err)); xfree (fname); return err; } err = wks_parse_policy (policy, fp, 0); es_fclose (fp); xfree (fname); return err; } /* We store the key under the name of the nonce we will then send to * the user. On success the nonce is stored at R_NONCE and the file * name at R_FNAME. */ static gpg_error_t store_key_as_pending (const char *dir, estream_t key, char **r_nonce, char **r_fname) { gpg_error_t err; char *dname = NULL; char *fname = NULL; char *nonce = NULL; estream_t outfp = NULL; char buffer[1024]; size_t nbytes, nwritten; *r_nonce = NULL; *r_fname = NULL; dname = make_filename_try (dir, "pending", NULL); if (!dname) { err = gpg_error_from_syserror (); goto leave; } /* Create the nonce. We use 20 bytes so that we don't waste a * character in our zBase-32 encoding. Using the gcrypt's nonce * function is faster than using the strong random function; this is * Good Enough for our purpose. */ log_assert (sizeof buffer > 20); gcry_create_nonce (buffer, 20); nonce = zb32_encode (buffer, 8 * 20); memset (buffer, 0, 20); /* Not actually needed but it does not harm. */ if (!nonce) { err = gpg_error_from_syserror (); goto leave; } fname = strconcat (dname, "/", nonce, NULL); if (!fname) { err = gpg_error_from_syserror (); goto leave; } /* With 128 bits of random we can expect that no other file exists * under this name. We use "x" to detect internal errors. */ outfp = es_fopen (fname, "wbx,mode=-rw"); if (!outfp) { err = gpg_error_from_syserror (); log_error ("error creating '%s': %s\n", fname, gpg_strerror (err)); goto leave; } es_rewind (key); for (;;) { if (es_read (key, buffer, sizeof buffer, &nbytes)) { err = gpg_error_from_syserror (); log_error ("error reading '%s': %s\n", es_fname_get (key), gpg_strerror (err)); break; } if (!nbytes) { err = 0; goto leave; /* Ready. */ } if (es_write (outfp, buffer, nbytes, &nwritten)) { err = gpg_error_from_syserror (); log_error ("error writing '%s': %s\n", fname, gpg_strerror (err)); goto leave; } else if (nwritten != nbytes) { err = gpg_error (GPG_ERR_EIO); log_error ("error writing '%s': %s\n", fname, "short write"); goto leave; } } leave: if (err) { es_fclose (outfp); gnupg_remove (fname); } else if (es_fclose (outfp)) { err = gpg_error_from_syserror (); log_error ("error closing '%s': %s\n", fname, gpg_strerror (err)); } if (!err) { *r_nonce = nonce; *r_fname = fname; } else { xfree (nonce); xfree (fname); } xfree (dname); return err; } /* Send a confirmation request. DIR is the directory used for the * address MBOX. NONCE is the nonce we want to see in the response to * this mail. FNAME the name of the file with the key. */ static gpg_error_t send_confirmation_request (server_ctx_t ctx, const char *mbox, const char *nonce, const char *keyfile) { gpg_error_t err; estream_t body = NULL; estream_t bodyenc = NULL; estream_t signeddata = NULL; estream_t signature = NULL; mime_maker_t mime = NULL; char *from_buffer = NULL; const char *from; strlist_t sl; from = from_buffer = get_submission_address (mbox); if (!from) { from = opt.default_from; if (!from) { log_error ("no sender address found for '%s'\n", mbox); err = gpg_error (GPG_ERR_CONFIGURATION); goto leave; } log_info ("Note: using default sender address '%s'\n", from); } body = es_fopenmem (0, "w+b"); if (!body) { err = gpg_error_from_syserror (); log_error ("error allocating memory buffer: %s\n", gpg_strerror (err)); goto leave; } if (!ctx->draft_version_2) { /* It is fine to use 8 bit encoding because that is encrypted and * only our client will see it. */ es_fputs ("Content-Type: application/vnd.gnupg.wks\n" "Content-Transfer-Encoding: 8bit\n" "\n", body); } es_fprintf (body, ("type: confirmation-request\n" "sender: %s\n" "address: %s\n" "fingerprint: %s\n" "nonce: %s\n"), from, mbox, ctx->fpr, nonce); es_rewind (body); err = encrypt_stream (&bodyenc, body, keyfile); 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", from); if (err) goto leave; err = mime_maker_add_header (mime, "To", mbox); if (err) goto leave; err = mime_maker_add_header (mime, "Subject", "Confirm your key publication"); if (err) goto leave; err = mime_maker_add_header (mime, "Wks-Draft-Version", STR2(WKS_DRAFT_VERSION)); if (err) goto leave; /* Help Enigmail to identify messages. Note that this is in no way * secured. */ err = mime_maker_add_header (mime, "WKS-Phase", "confirm"); if (err) goto leave; for (sl = opt.extra_headers; sl; sl = sl->next) { err = mime_maker_add_header (mime, sl->d, NULL); if (err) goto leave; } if (!ctx->draft_version_2) { 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 { unsigned int partid; /* FIXME: Add micalg. */ err = mime_maker_add_header (mime, "Content-Type", "multipart/signed; " "protocol=\"application/pgp-signature\""); if (err) goto leave; err = mime_maker_add_container (mime); if (err) goto leave; err = mime_maker_add_header (mime, "Content-Type", "multipart/mixed"); if (err) goto leave; err = mime_maker_add_container (mime); if (err) goto leave; partid = mime_maker_get_partid (mime); err = mime_maker_add_header (mime, "Content-Type", "text/plain"); if (err) goto leave; err = mime_maker_add_body (mime, "This message has been send to confirm your request\n" "to publish your key. If you did not request a key\n" "publication, simply ignore this message.\n" "\n" "Most mail software can handle this kind of message\n" "automatically and thus you would not have seen this\n" "message. It seems that your client does not fully\n" "support this service. The web page\n" "\n" " https://gnupg.org/faq/wkd.html\n" "\n" "explains how you can process this message anyway in\n" "a few manual steps.\n"); if (err) goto leave; err = mime_maker_add_header (mime, "Content-Type", "application/vnd.gnupg.wks"); if (err) goto leave; err = mime_maker_add_stream (mime, &bodyenc); if (err) goto leave; err = mime_maker_end_container (mime); if (err) goto leave; /* mime_maker_dump_tree (mime); */ err = mime_maker_get_part (mime, partid, &signeddata); if (err) goto leave; err = sign_stream (&signature, signeddata, from); if (err) goto leave; err = mime_maker_add_header (mime, "Content-Type", "application/pgp-signature"); if (err) goto leave; err = mime_maker_add_stream (mime, &signature); if (err) goto leave; } err = wks_send_mime (mime); leave: mime_maker_release (mime); es_fclose (signature); es_fclose (signeddata); es_fclose (bodyenc); es_fclose (body); xfree (from_buffer); return err; } /* Store the key given by KEY into the pending directory and send a * confirmation requests. */ static gpg_error_t process_new_key (server_ctx_t ctx, estream_t key) { gpg_error_t err; uidinfo_list_t sl; const char *s; char *dname = NULL; char *nonce = NULL; char *fname = NULL; struct policy_flags_s policybuf; memset (&policybuf, 0, sizeof policybuf); /* First figure out the user id from the key. */ xfree (ctx->fpr); free_uidinfo_list (ctx->mboxes); err = wks_list_key (key, &ctx->fpr, &ctx->mboxes); if (err) goto leave; log_assert (ctx->fpr); log_info ("fingerprint: %s\n", ctx->fpr); for (sl = ctx->mboxes; sl; sl = sl->next) { if (sl->mbox) log_info (" addr-spec: %s\n", sl->mbox); } /* Walk over all user ids and send confirmation requests for those * we support. */ for (sl = ctx->mboxes; sl; sl = sl->next) { if (!sl->mbox) continue; s = strchr (sl->mbox, '@'); log_assert (s && s[1]); xfree (dname); dname = make_filename_try (opt.directory, s+1, NULL); if (!dname) { err = gpg_error_from_syserror (); goto leave; } if (access (dname, W_OK)) { log_info ("skipping address '%s': Domain not configured\n", sl->mbox); continue; } if (get_policy_flags (&policybuf, sl->mbox)) { log_info ("skipping address '%s': Bad policy flags\n", sl->mbox); continue; } if (policybuf.auth_submit) { /* Bypass the confirmation stuff and publish the key as is. */ log_info ("publishing address '%s'\n", sl->mbox); /* FIXME: We need to make sure that we do this only for the * address in the mail. */ log_debug ("auth-submit not yet working!\n"); } else { log_info ("storing address '%s'\n", sl->mbox); xfree (nonce); xfree (fname); err = store_key_as_pending (dname, key, &nonce, &fname); if (err) goto leave; err = send_confirmation_request (ctx, sl->mbox, nonce, fname); if (err) goto leave; } } leave: if (nonce) wipememory (nonce, strlen (nonce)); xfree (nonce); xfree (fname); xfree (dname); wks_free_policy (&policybuf); return err; } /* Send a message to tell the user at MBOX that their key has been * published. FNAME the name of the file with the key. */ static gpg_error_t send_congratulation_message (const char *mbox, const char *keyfile) { gpg_error_t err; estream_t body = NULL; estream_t bodyenc = NULL; mime_maker_t mime = NULL; char *from_buffer = NULL; const char *from; strlist_t sl; from = from_buffer = get_submission_address (mbox); if (!from) { from = opt.default_from; if (!from) { log_error ("no sender address found for '%s'\n", mbox); err = gpg_error (GPG_ERR_CONFIGURATION); goto leave; } log_info ("Note: using default sender address '%s'\n", from); } body = es_fopenmem (0, "w+b"); if (!body) { err = gpg_error_from_syserror (); log_error ("error allocating memory buffer: %s\n", gpg_strerror (err)); goto leave; } /* It is fine to use 8 bit encoding because that is encrypted and * only our client will see it. */ es_fputs ("Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "\n", body); es_fprintf (body, "Hello!\n\n" "The key for your address '%s' has been published\n" "and can now be retrieved from the Web Key Directory.\n" "\n" "For more information on this system see:\n" "\n" " https://gnupg.org/faq/wkd.html\n" "\n" "Best regards\n" "\n" " Gnu Key Publisher\n\n\n" "-- \n" "The GnuPG Project welcomes donations: %s\n", mbox, "https://gnupg.org/donate"); es_rewind (body); err = encrypt_stream (&bodyenc, body, keyfile); 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", from); if (err) goto leave; err = mime_maker_add_header (mime, "To", mbox); if (err) goto leave; err = mime_maker_add_header (mime, "Subject", "Your key has been published"); if (err) goto leave; err = mime_maker_add_header (mime, "Wks-Draft-Version", STR2(WKS_DRAFT_VERSION)); if (err) goto leave; err = mime_maker_add_header (mime, "WKS-Phase", "done"); if (err) goto leave; for (sl = opt.extra_headers; sl; sl = sl->next) { err = mime_maker_add_header (mime, sl->d, NULL); if (err) goto leave; } 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; err = wks_send_mime (mime); leave: mime_maker_release (mime); es_fclose (bodyenc); es_fclose (body); xfree (from_buffer); return err; } /* Check that we have send a request with NONCE and publish the key. */ static gpg_error_t check_and_publish (server_ctx_t ctx, const char *address, const char *nonce) { gpg_error_t err; char *fname = NULL; char *fnewname = NULL; estream_t key = NULL; char *hash = NULL; const char *domain; const char *s; uidinfo_list_t sl; char shaxbuf[32]; /* Used for SHA-1 and SHA-256 */ /* FIXME: There is a bug in name-value.c which adds white space for * the last pair and thus we strip the nonce here until this has * been fixed. */ char *nonce2 = xstrdup (nonce); trim_trailing_spaces (nonce2); nonce = nonce2; domain = strchr (address, '@'); log_assert (domain && domain[1]); domain++; fname = make_filename_try (opt.directory, domain, "pending", nonce, NULL); if (!fname) { err = gpg_error_from_syserror (); goto leave; } /* Try to open the file with the key. */ key = es_fopen (fname, "rb"); if (!key) { err = gpg_error_from_syserror (); if (gpg_err_code (err) == GPG_ERR_ENOENT) { log_info ("no pending request for '%s'\n", address); err = gpg_error (GPG_ERR_NOT_FOUND); } else log_error ("error reading '%s': %s\n", fname, gpg_strerror (err)); goto leave; } /* We need to get the fingerprint from the key. */ xfree (ctx->fpr); free_uidinfo_list (ctx->mboxes); err = wks_list_key (key, &ctx->fpr, &ctx->mboxes); if (err) goto leave; log_assert (ctx->fpr); log_info ("fingerprint: %s\n", ctx->fpr); for (sl = ctx->mboxes; sl; sl = sl->next) if (sl->mbox) log_info (" addr-spec: %s\n", sl->mbox); /* Check that the key has 'address' as a user id. We use * case-insensitive matching because the client is expected to * return the address verbatim. */ for (sl = ctx->mboxes; sl; sl = sl->next) if (sl->mbox && !strcmp (sl->mbox, address)) break; if (!sl) { log_error ("error publishing key: '%s' is not a user ID of %s\n", address, ctx->fpr); err = gpg_error (GPG_ERR_NO_PUBKEY); goto leave; } /* Hash user ID and create filename. */ err = wks_compute_hu_fname (&fnewname, address); if (err) goto leave; /* Publish. */ err = copy_key_as_binary (fname, fnewname, address); if (err) { err = gpg_error_from_syserror (); log_error ("copying '%s' to '%s' failed: %s\n", fname, fnewname, gpg_strerror (err)); goto leave; } /* Make sure it is world readable. */ if (gnupg_chmod (fnewname, "-rwxr--r--")) log_error ("can't set permissions of '%s': %s\n", fnewname, gpg_strerror (gpg_err_code_from_syserror())); log_info ("key %s published for '%s'\n", ctx->fpr, address); send_congratulation_message (address, fnewname); /* Try to publish as DANE record if the DANE directory exists. */ xfree (fname); fname = fnewname; fnewname = make_filename_try (opt.directory, domain, "dane", NULL); if (!fnewname) { err = gpg_error_from_syserror (); goto leave; } if (!access (fnewname, W_OK)) { /* Yes, we have a dane directory. */ s = strchr (address, '@'); log_assert (s); gcry_md_hash_buffer (GCRY_MD_SHA256, shaxbuf, address, s - address); xfree (hash); hash = bin2hex (shaxbuf, 28, NULL); if (!hash) { err = gpg_error_from_syserror (); goto leave; } xfree (fnewname); fnewname = make_filename_try (opt.directory, domain, "dane", hash, NULL); if (!fnewname) { err = gpg_error_from_syserror (); goto leave; } err = copy_key_as_dane (fname, fnewname); if (err) goto leave; log_info ("key %s published for '%s' (DANE record)\n", ctx->fpr, address); } leave: es_fclose (key); xfree (hash); xfree (fnewname); xfree (fname); xfree (nonce2); return err; } /* Process a confirmation response in MSG. */ static gpg_error_t process_confirmation_response (server_ctx_t ctx, estream_t msg) { gpg_error_t err; nvc_t nvc; nve_t item; const char *value, *sender, *address, *nonce; err = nvc_parse (&nvc, NULL, msg); if (err) { log_error ("parsing the WKS message failed: %s\n", gpg_strerror (err)); goto leave; } if (opt.debug) { log_debug ("response follows:\n"); nvc_write (nvc, log_get_stream ()); } /* Check that this is a confirmation response. */ if (!((item = nvc_lookup (nvc, "type:")) && (value = nve_value (item)) && !strcmp (value, "confirmation-response"))) { 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 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; (void)sender; /* FIXME: Do we really need the sender?. */ /* 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; /* 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; err = check_and_publish (ctx, address, nonce); leave: nvc_release (nvc); 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; struct server_ctx_s ctx; (void)opaque; memset (&ctx, 0, sizeof ctx); if ((flags & WKS_RECEIVE_DRAFT2)) ctx.draft_version_2 = 1; if (!strcmp (mediatype, "application/pgp-keys")) err = process_new_key (&ctx, msg); else if (!strcmp (mediatype, "application/vnd.gnupg.wks")) err = process_confirmation_response (&ctx, msg); else { log_info ("ignoring unexpected message of type '%s'\n", mediatype); err = gpg_error (GPG_ERR_UNEXPECTED_MSG); } xfree (ctx.fpr); free_uidinfo_list (ctx.mboxes); return err; } /* Return a list of all configured domains. Each list element is the * top directory for the domain. To figure out the actual domain * name strrchr(name, '/') can be used. */ static gpg_error_t get_domain_list (strlist_t *r_list) { gpg_error_t err; DIR *dir = NULL; char *fname = NULL; struct dirent *dentry; struct stat sb; strlist_t list = NULL; *r_list = NULL; dir = opendir (opt.directory); if (!dir) { err = gpg_error_from_syserror (); goto leave; } while ((dentry = readdir (dir))) { if (*dentry->d_name == '.') continue; if (!strchr (dentry->d_name, '.')) continue; /* No dot - can't be a domain subdir. */ xfree (fname); fname = make_filename_try (opt.directory, dentry->d_name, NULL); if (!fname) { err = gpg_error_from_syserror (); log_error ("make_filename failed in %s: %s\n", __func__, gpg_strerror (err)); goto leave; } if (stat (fname, &sb)) { err = gpg_error_from_syserror (); log_error ("error accessing '%s': %s\n", fname, gpg_strerror (err)); continue; } if (!S_ISDIR(sb.st_mode)) continue; if (!add_to_strlist_try (&list, fname)) { err = gpg_error_from_syserror (); log_error ("add_to_strlist failed in %s: %s\n", __func__, gpg_strerror (err)); goto leave; } } err = 0; *r_list = list; list = NULL; leave: free_strlist (list); if (dir) closedir (dir); xfree (fname); return err; } static gpg_error_t expire_one_domain (const char *top_dirname, const char *domain) { gpg_error_t err; char *dirname; char *fname = NULL; DIR *dir = NULL; struct dirent *dentry; struct stat sb; time_t now = gnupg_get_time (); dirname = make_filename_try (top_dirname, "pending", NULL); if (!dirname) { err = gpg_error_from_syserror (); log_error ("make_filename failed in %s: %s\n", __func__, gpg_strerror (err)); goto leave; } dir = opendir (dirname); if (!dir) { err = gpg_error_from_syserror (); log_error (("can't access directory '%s': %s\n"), dirname, gpg_strerror (err)); goto leave; } while ((dentry = readdir (dir))) { if (*dentry->d_name == '.') continue; xfree (fname); fname = make_filename_try (dirname, dentry->d_name, NULL); if (!fname) { err = gpg_error_from_syserror (); log_error ("make_filename failed in %s: %s\n", __func__, gpg_strerror (err)); goto leave; } if (strlen (dentry->d_name) != 32) { log_info ("garbage file '%s' ignored\n", fname); continue; } if (stat (fname, &sb)) { err = gpg_error_from_syserror (); log_error ("error accessing '%s': %s\n", fname, gpg_strerror (err)); continue; } if (S_ISDIR(sb.st_mode)) { log_info ("garbage directory '%s' ignored\n", fname); continue; } if (sb.st_mtime + PENDING_TTL < now) { if (opt.verbose) log_info ("domain %s: removing pending key '%s'\n", domain, dentry->d_name); if (remove (fname)) { err = gpg_error_from_syserror (); /* In case the file has just been renamed or another * processes is cleaning up, we don't print a diagnostic * for ENOENT. */ if (gpg_err_code (err) != GPG_ERR_ENOENT) log_error ("error removing '%s': %s\n", fname, gpg_strerror (err)); } } } err = 0; leave: if (dir) closedir (dir); xfree (dirname); xfree (fname); return err; } /* Scan spool directories and expire too old pending keys. */ static gpg_error_t expire_pending_confirmations (strlist_t domaindirs) { gpg_error_t err = 0; strlist_t sl; const char *domain; for (sl = domaindirs; sl; sl = sl->next) { domain = strrchr (sl->d, '/'); log_assert (domain); domain++; expire_one_domain (sl->d, domain); } return err; } /* List all configured domains. */ static gpg_error_t command_list_domains (void) { static struct { const char *name; const char *perm; } requireddirs[] = { { "pending", "-rwx" }, { "hu", "-rwxr-xr-x" } }; gpg_error_t err; strlist_t domaindirs; strlist_t sl; const char *domain; char *fname = NULL; int i; estream_t fp; err = get_domain_list (&domaindirs); if (err) { log_error ("error reading list of domains: %s\n", gpg_strerror (err)); return err; } for (sl = domaindirs; sl; sl = sl->next) { domain = strrchr (sl->d, '/'); log_assert (domain); domain++; if (opt_with_dir) es_printf ("%s %s\n", domain, sl->d); else es_printf ("%s\n", domain); /* Check that the required directories are there. */ for (i=0; i < DIM (requireddirs); i++) { xfree (fname); fname = make_filename_try (sl->d, requireddirs[i].name, NULL); if (!fname) { err = gpg_error_from_syserror (); goto leave; } if (access (fname, W_OK)) { err = gpg_error_from_syserror (); if (gpg_err_code (err) == GPG_ERR_ENOENT) { if (gnupg_mkdir (fname, requireddirs[i].perm)) { err = gpg_error_from_syserror (); log_error ("domain %s: error creating subdir '%s': %s\n", domain, requireddirs[i].name, gpg_strerror (err)); } else log_info ("domain %s: subdir '%s' created\n", domain, requireddirs[i].name); } else if (err) log_error ("domain %s: problem with subdir '%s': %s\n", domain, requireddirs[i].name, gpg_strerror (err)); } } /* Print a warning if the submission address is not configured. */ xfree (fname); fname = make_filename_try (sl->d, "submission-address", NULL); if (!fname) { err = gpg_error_from_syserror (); goto leave; } if (access (fname, F_OK)) { err = gpg_error_from_syserror (); if (gpg_err_code (err) == GPG_ERR_ENOENT) log_error ("domain %s: submission address not configured\n", domain); else log_error ("domain %s: problem with '%s': %s\n", domain, fname, gpg_strerror (err)); } /* Check the syntax of the optional policy file. */ xfree (fname); fname = make_filename_try (sl->d, "policy", NULL); if (!fname) { err = gpg_error_from_syserror (); goto leave; } fp = es_fopen (fname, "r"); if (!fp) { err = gpg_error_from_syserror (); if (gpg_err_code (err) == GPG_ERR_ENOENT) { fp = es_fopen (fname, "w"); if (!fp) log_error ("domain %s: can't create policy file: %s\n", domain, gpg_strerror (err)); else es_fclose (fp); fp = NULL; } else log_error ("domain %s: error in policy file: %s\n", domain, gpg_strerror (err)); } else { struct policy_flags_s policy; err = wks_parse_policy (&policy, fp, 0); es_fclose (fp); wks_free_policy (&policy); } } err = 0; leave: xfree (fname); free_strlist (domaindirs); return err; } /* Run regular maintenance jobs. */ static gpg_error_t command_cron (void) { gpg_error_t err; strlist_t domaindirs; err = get_domain_list (&domaindirs); if (err) { log_error ("error reading list of domains: %s\n", gpg_strerror (err)); return err; } err = expire_pending_confirmations (domaindirs); free_strlist (domaindirs); return err; } /* Check whether the key with USER_ID is installed. */ static gpg_error_t command_check_key (const char *userid) { gpg_error_t err; char *addrspec = NULL; char *fname = NULL; err = wks_fname_from_userid (userid, &fname, &addrspec); if (err) goto leave; if (access (fname, R_OK)) { err = gpg_error_from_syserror (); if (opt_with_file) es_printf ("%s n %s\n", addrspec, fname); 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 stating '%s': %s\n", fname, gpg_strerror (err)); goto leave; } if (opt_with_file) es_printf ("%s i %s\n", addrspec, fname); if (opt.verbose) log_info ("key for '%s' is installed\n", addrspec); err = 0; leave: xfree (fname); xfree (addrspec); return err; } /* Revoke the key with mail address MAILADDR. */ static gpg_error_t command_revoke_key (const char *mailaddr) { /* Remove should be different from removing but we have not yet * defined a suitable way to do this. */ return wks_cmd_remove_key (mailaddr); }