diff --git a/doc/wks.texi b/doc/wks.texi index 89288d041..caae3fd62 100644 --- a/doc/wks.texi +++ b/doc/wks.texi @@ -1,418 +1,433 @@ @c wks.texi - man pages for the Web Key Service tools. @c Copyright (C) 2017 g10 Code GmbH @c Copyright (C) 2017 Bundesamt für Sicherheit in der Informationstechnik @c This is part of the GnuPG manual. @c For copying conditions, see the file GnuPG.texi. @include defs.inc @node Web Key Service @chapter Web Key Service GnuPG comes with tools used to maintain and access a Web Key Directory. @menu * gpg-wks-client:: Send requests via WKS * gpg-wks-server:: Server to provide the WKS. @end menu @c @c GPG-WKS-CLIENT @c @manpage gpg-wks-client.1 @node gpg-wks-client @section Send requests via WKS @ifset manverb .B gpg-wks-client \- Client for the Web Key Service @end ifset @mansect synopsis @ifset manverb .B gpg-wks-client .RI [ options ] .B \-\-supported .I user-id .br .B gpg-wks-client .RI [ options ] .B \-\-check .I user-id .br .B gpg-wks-client .RI [ options ] .B \-\-create .I fingerprint .I user-id .br .B gpg-wks-client .RI [ options ] .B \-\-receive .br .B gpg-wks-client .RI [ options ] .B \-\-read @end ifset @mansect description The @command{gpg-wks-client} is used to send requests to a Web Key Service provider. This is usuallay done to upload a key into a Web Key Directory. With the @option{--supported} command the caller can test whether a site supports the Web Key Service. The argument is an arbitray address in the to be tested domain. For example @file{foo@@example.net}. The command returns success if the Web Key Service is supported. The operation is silent; to get diagnostic output use the option @option{--verbose}. See option @option{--with-colons} for a variant of this command. With the @option{--check} command the caller can test whether a key exists for a supplied mail address. The command returns success if a key is available. The @option{--create} command is used to send a request for publication in the Web Key Directory. The arguments are the fingerprint of the key and the user id to publish. The output from the command is a properly formatted mail with all standard headers. This mail can be fed to @command{sendmail(8)} or any other tool to actually send that mail. If @command{sendmail(8)} is installed the option @option{--send} can be used to directly send the created request. If the provider request a 'mailbox-only' user id and no such user id is found, @command{gpg-wks-client} will try an additional user id. The @option{--receive} and @option{--read} commands are used to process confirmation mails as send from the service provider. The former expects an encrypted MIME messages, the latter an already decrypted MIME message. The result of these commands are another mail which can be send in the same way as the mail created with @option{--create}. +The command @option{--install-key} manually installs a key into a +local directory (see option @option{-C}) reflecting the structure of a +WKD. The arguments are a file with the keyblock and the user-id to +install. If the first argument resembles a fingerprint the key is +taken from the current keyring; to force the use of a file, prefix the +first argument with "./". The command @option{--remove-key} removes a +key from that directory, its only argument is a user-id. + @command{gpg-wks-client} is not commonly invoked directly and thus it is not installed in the bin directory. Here is an example how it can be invoked manually to check for a Web Key Directory entry for @file{foo@@example.org}: @example $(gpgconf --list-dirs libexecdir)/gpg-wks-client --check foo@@example.net @end example @mansect options @noindent @command{gpg-wks-client} understands these options: @table @gnupgtabopt @item --send @opindex send Directly send created mails using the @command{sendmail} command. Requires installation of that command. @item --with-colons @opindex with-colons This option has currently only an effect on the @option{--supported} command. If it is used all arguimenst on the command line are taken as domain names and tested for WKD support. The output format is one line per domain with colon delimited fields. The currently specified fields are (future versions may specify additional fields): @table @asis @item 1 - domain This is the domain name. Although quoting is not required for valid domain names this field is specified to be quoted in standard C manner. @item 2 - WKD If the value is true the domain supports the Web Key Directory. @item 3 - WKS If the value is true the domain supports the Web Key Service protocol to upload keys to the directory. @item 4 - error-code This may contain an gpg-error code to describe certain failures. Use @samp{gpg-error CODE} to explain the code. @item 5 - protocol-version The minimum protocol version supported by the server. @item 6 - auth-submit The auth-submit flag from the policy file of the server. @item 7 - mailbox-only The mailbox-only flag from the policy file of the server. @end table @item --output @var{file} @itemx -o @opindex output Write the created mail to @var{file} instead of stdout. Note that the value @code{-} for @var{file} is the same as writing to stdout. @item --status-fd @var{n} @opindex status-fd Write special status strings to the file descriptor @var{n}. This program returns only the status messages SUCCESS or FAILURE which are helpful when the caller uses a double fork approach and can't easily get the return code of the process. +@item -C @var{dir} +@itemx --directory @var{dir} +@opindex directory +Use @var{dir} as top level directory for the commands +@option{--install-key} and @option{--remove-key}. The default is +@file{openpgpkey}. + @item --verbose @opindex verbose Enable extra informational output. @item --quiet @opindex quiet Disable almost all informational output. @item --version @opindex version Print version of the program and exit. @item --help @opindex help Display a brief help page and exit. @end table @mansect see also @ifset isman @command{gpg-wks-server}(1) @end ifset @c @c GPG-WKS-SERVER @c @manpage gpg-wks-server.1 @node gpg-wks-server @section Provide the Web Key Service @ifset manverb .B gpg-wks-server \- Server providing the Web Key Service @end ifset @mansect synopsis @ifset manverb .B gpg-wks-server .RI [ options ] .B \-\-receive .br .B gpg-wks-server .RI [ options ] .B \-\-cron .br .B gpg-wks-server .RI [ options ] .B \-\-list-domains .br .B gpg-wks-server .RI [ options ] .B \-\-check-key .I user-id .br .B gpg-wks-server .RI [ options ] .B \-\-install-key .I file .I user-id .br .B gpg-wks-server .RI [ options ] .B \-\-remove-key .I user-id .br .B gpg-wks-server .RI [ options ] .B \-\-revoke-key .I user-id @end ifset @mansect description The @command{gpg-wks-server} is a server site implementation of the Web Key Service. It receives requests for publication, sends confirmation requests, receives confirmations, and published the key. It also has features to ease the setup and maintenance of a Web Key Directory. When used with the command @option{--receive} a single Web Key Service mail is processed. Commonly this command is used with the option @option{--send} to directly send the crerated mails back. See below for an installation example. The command @option{--cron} is used for regualr cleanup tasks. For example non-confirmed requested should be removed after their expire time. It is best to run this command once a day from a cronjob. The command @option{--list-domains} prints all configured domains. Further it creates missing directories for the configuration and prints warnings pertaining to problems in the configuration. The command @option{--check-key} (or just @option{--check}) checks whether a key with the given user-id is installed. The process returns success in this case; to also print a diagnostic use the option @option{-v}. If the key is not installed a diagnostic is printed and the process returns failure; to suppress the diagnostic, use option @option{-q}. More than one user-id can be given; see also option @option{with-file}. The command @option{--install-key} manually installs a key into the WKD. The arguments are a file with the keyblock and the user-id to install. If the first argument resembles a fingerprint the key is taken from the current keyring; to force the use of a file, prefix the first argument with "./". The command @option{--remove-key} uninstalls a key from the WKD. The process returns success in this case; to also print a diagnostic, use option @option{-v}. If the key is not installed a diagnostic is printed and the process returns failure; to suppress the diagnostic, use option @option{-q}. The command @option{--revoke-key} is not yet functional. @mansect options @noindent @command{gpg-wks-server} understands these options: @table @gnupgtabopt @item -C @var{dir} @itemx --directory @var{dir} @opindex directory Use @var{dir} as top level directory for domains. The default is @file{/var/lib/gnupg/wks}. @item --from @var{mailaddr} @opindex from Use @var{mailaddr} as the default sender address. @item --header @var{name}=@var{value} @opindex header Add the mail header "@var{name}: @var{value}" to all outgoing mails. @item --send @opindex send Directly send created mails using the @command{sendmail} command. Requires installation of that command. @item -o @var{file} @itemx --output @var{file} @opindex output Write the created mail also to @var{file}. Note that the value @code{-} for @var{file} would write it to stdout. @item --with-dir @opindex with-dir When used with the command @option{--list-domains} print for each installed domain the domain name and its directory name. @item --with-file @opindex with-file When used with the command @option{--check-key} print for each user-id, the address, 'i' for installed key or 'n' for not installed key, and the filename. @item --verbose @opindex verbose Enable extra informational output. @item --quiet @opindex quiet Disable almost all informational output. @item --version @opindex version Print version of the program and exit. @item --help @opindex help Display a brief help page and exit. @end table @noindent @mansect examples @chapheading Examples The Web Key Service requires a working directory to store keys pending for publication. As root create a working directory: @example # mkdir /var/lib/gnupg/wks # chown webkey:webkey /var/lib/gnupg/wks # chmod 2750 /var/lib/gnupg/wks @end example Then under your webkey account create directories for all your domains. Here we do it for "example.net": @example $ mkdir /var/lib/gnupg/wks/example.net @end example Finally run @example $ gpg-wks-server --list-domains @end example to create the required sub-directories with the permissions set correctly. For each domain a submission address needs to be configured. All service mails are directed to that address. It can be the same address for all configured domains, for example: @example $ cd /var/lib/gnupg/wks/example.net $ echo key-submission@@example.net >submission-address @end example The protocol requires that the key to be published is send with an encrypted mail to the service. Thus you need to create a key for the submission address: @example $ gpg --batch --passphrase '' --quick-gen-key key-submission@@example.net $ gpg -K key-submission@@example.net @end example The output of the last command looks similar to this: @example sec rsa2048 2016-08-30 [SC] C0FCF8642D830C53246211400346653590B3795B uid [ultimate] key-submission@@example.net ssb rsa2048 2016-08-30 [E] @end example Take the fingerprint from that output and manually publish the key: @example $ gpg-wks-server --install-key C0FCF8642D830C53246211400346653590B3795B \ > key-submission@@example.net @end example Finally that submission address needs to be redirected to a script running @command{gpg-wks-server}. The @command{procmail} command can be used for this: Redirect the submission address to the user "webkey" and put this into webkey's @file{.procmailrc}: @example :0 * !^From: webkey@@example.net * !^X-WKS-Loop: webkey.example.net |gpg-wks-server -v --receive \ --header X-WKS-Loop=webkey.example.net \ --from webkey@@example.net --send @end example @mansect see also @ifset isman @command{gpg-wks-client}(1) @end ifset diff --git a/tools/gpg-wks-client.c b/tools/gpg-wks-client.c index bf6b119e0..2adfcfad2 100644 --- a/tools/gpg-wks-client.c +++ b/tools/gpg-wks-client.c @@ -1,1437 +1,1490 @@ /* 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 != 2) + wrong_args ("--install-key FILE|FINGERPRINT USER-ID"); + err = wks_cmd_install_key (*argv, argv[1]); + 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"); 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; }