diff --git a/doc/wks.texi b/doc/wks.texi index 6d622828f..b8de29639 100644 --- a/doc/wks.texi +++ b/doc/wks.texi @@ -1,372 +1,379 @@ @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}. 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}. @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 --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 --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 return -success in this case; to also print a diagnostic, use option -@option{-v}. If the key is not installed a diagnostics is printed and +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 --output @var{file} -@itemx -o +@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 -Also print the directory name for each domain listed by command -@option{--list-domains}. +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 -With command @option{--check-key} print for each user-id, the address, -'i' for installed key or 'n' for not installed key, and the filename. +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 permission set +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 sent with an +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-server.c b/tools/gpg-wks-server.c index a5881557f..24b331262 100644 --- a/tools/gpg-wks-server.c +++ b/tools/gpg-wks-server.c @@ -1,2277 +1,2284 @@ /* 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_install_key (const char *fname, const char *userid); static gpg_error_t command_remove_key (const char *mailaddr); 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 != 2) wrong_args ("--install-key FILE USER-ID"); err = command_install_key (*argv, argv[1]); break; case aRemoveKey: if (argc != 1) wrong_args ("--remove-key USER-ID"); err = command_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, "--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; } /* Write the content of SRC to the new file FNAME. */ static gpg_error_t write_to_file (estream_t src, const char *fname) { gpg_error_t err; estream_t dst; char buffer[4096]; size_t nread, written; dst = es_fopen (fname, "wb"); if (!dst) return gpg_error_from_syserror (); do { nread = es_fread (buffer, 1, sizeof buffer, src); if (!nread) break; written = es_fwrite (buffer, 1, nread, dst); if (written != nread) break; } while (!es_feof (src) && !es_ferror (src) && !es_ferror (dst)); if (!es_feof (src) || es_ferror (src) || es_ferror (dst)) { err = gpg_error_from_syserror (); es_fclose (dst); gnupg_remove (fname); return err; } if (es_fclose (dst)) { err = gpg_error_from_syserror (); log_error ("error closing '%s': %s\n", fname, gpg_strerror (err)); return err; } return 0; } /* Compute the the full file name for the key with ADDRSPEC and return * it at R_FNAME. */ static gpg_error_t compute_hu_fname (char **r_fname, const char *addrspec) { gpg_error_t err; char *hash; const char *domain; char sha1buf[20]; *r_fname = NULL; domain = strchr (addrspec, '@'); if (!domain || !domain[1] || domain == addrspec) return gpg_error (GPG_ERR_INV_ARG); domain++; gcry_md_hash_buffer (GCRY_MD_SHA1, sha1buf, addrspec, domain - addrspec - 1); hash = zb32_encode (sha1buf, 8*20); if (!hash) return gpg_error_from_syserror (); *r_fname = make_filename_try (opt.directory, domain, "hu", hash, NULL); if (!*r_fname) err = gpg_error_from_syserror (); else err = 0; xfree (hash); 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 = 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. ECh list element is the +/* 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) + 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); - if (!err) - { - struct policy_flags_s empty_policy; - memset (&empty_policy, 0, sizeof empty_policy); - if (!memcmp (&empty_policy, &policy, sizeof policy)) - log_error ("domain %s: empty policy file\n", domain); - } 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; } /* Install a single key into the WKD by reading FNAME and extracting * USERID. */ static gpg_error_t command_install_key (const char *fname, const char *userid) { gpg_error_t err; KEYDB_SEARCH_DESC desc; estream_t fp = NULL; char *addrspec = NULL; char *fpr = NULL; uidinfo_list_t uidlist = NULL; uidinfo_list_t uid, thisuid; time_t thistime; char *huname = NULL; int any; addrspec = mailbox_from_userid (userid); if (!addrspec) { log_error ("\"%s\" is not a proper mail address\n", userid); err = gpg_error (GPG_ERR_INV_USER_ID); goto leave; } if (!classify_user_id (fname, &desc, 1) && (desc.mode == KEYDB_SEARCH_MODE_FPR || desc.mode == KEYDB_SEARCH_MODE_FPR20)) { /* FNAME looks like a fingerprint. Get the key from the * standard keyring. */ err = wks_get_key (&fp, fname, addrspec, 0); if (err) { log_error ("error getting key '%s' (uid='%s'): %s\n", fname, addrspec, gpg_strerror (err)); goto leave; } } else /* Take it from the file */ { fp = es_fopen (fname, "rb"); if (!fp) { err = gpg_error_from_syserror (); log_error ("error reading '%s': %s\n", fname, gpg_strerror (err)); goto leave; } } /* List the key so that we can figure out the newest UID with the * requested addrspec. */ err = wks_list_key (fp, &fpr, &uidlist); if (err) { log_error ("error parsing key: %s\n", gpg_strerror (err)); err = gpg_error (GPG_ERR_NO_PUBKEY); goto leave; } thistime = 0; thisuid = NULL; any = 0; for (uid = uidlist; uid; uid = uid->next) { if (!uid->mbox) continue; /* Should not happen anyway. */ if (ascii_strcasecmp (uid->mbox, addrspec)) continue; /* Not the requested addrspec. */ any = 1; if (uid->created > thistime) { thistime = uid->created; thisuid = uid; } } if (!thisuid) thisuid = uidlist; /* This is the case for a missing timestamp. */ if (!any) { log_error ("public key in '%s' has no mail address '%s'\n", fname, addrspec); err = gpg_error (GPG_ERR_INV_USER_ID); goto leave; } if (opt.verbose) log_info ("using key with user id '%s'\n", thisuid->uid); { estream_t fp2; es_rewind (fp); err = wks_filter_uid (&fp2, fp, thisuid->uid, 1); if (err) { log_error ("error filtering key: %s\n", gpg_strerror (err)); err = gpg_error (GPG_ERR_NO_PUBKEY); goto leave; } es_fclose (fp); fp = fp2; } /* Hash user ID and create filename. */ err = compute_hu_fname (&huname, addrspec); if (err) goto leave; /* Publish. */ err = write_to_file (fp, huname); if (err) { log_error ("copying key to '%s' failed: %s\n", huname,gpg_strerror (err)); goto leave; } /* Make sure it is world readable. */ if (gnupg_chmod (huname, "-rwxr--r--")) log_error ("can't set permissions of '%s': %s\n", huname, gpg_strerror (gpg_err_code_from_syserror())); if (!opt.quiet) log_info ("key %s published for '%s'\n", fpr, addrspec); leave: xfree (huname); free_uidinfo_list (uidlist); xfree (fpr); xfree (addrspec); es_fclose (fp); return err; } /* Return the filename and optionally the addrspec for USERID at * R_FNAME and R_ADDRSPEC. R_ADDRSPEC might also be set on error. */ static gpg_error_t fname_from_userid (const char *userid, char **r_fname, char **r_addrspec) { gpg_error_t err; char *addrspec = NULL; const char *domain; char *hash = NULL; const char *s; char shaxbuf[32]; /* Used for SHA-1 and SHA-256 */ *r_fname = NULL; if (r_addrspec) *r_addrspec = NULL; addrspec = mailbox_from_userid (userid); if (!addrspec) { if (opt.verbose) log_info ("\"%s\" is not a proper mail address\n", userid); err = gpg_error (GPG_ERR_INV_USER_ID); goto leave; } domain = strchr (addrspec, '@'); log_assert (domain); domain++; /* Hash user ID and create filename. */ s = strchr (addrspec, '@'); log_assert (s); gcry_md_hash_buffer (GCRY_MD_SHA1, shaxbuf, addrspec, s - addrspec); hash = zb32_encode (shaxbuf, 8*20); if (!hash) { err = gpg_error_from_syserror (); goto leave; } *r_fname = make_filename_try (opt.directory, domain, "hu", hash, NULL); if (!*r_fname) err = gpg_error_from_syserror (); else err = 0; leave: if (r_addrspec && addrspec) *r_addrspec = addrspec; else xfree (addrspec); xfree (hash); return err; } /* 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 = 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; } /* Remove the key with mail address in USERID. */ static gpg_error_t command_remove_key (const char *userid) { gpg_error_t err; char *addrspec = NULL; char *fname = NULL; err = fname_from_userid (userid, &fname, &addrspec); if (err) goto leave; if (gnupg_remove (fname)) { err = gpg_error_from_syserror (); if (gpg_err_code (err) == GPG_ERR_ENOENT) { if (!opt.quiet) log_info ("key for '%s' is not installed\n", addrspec); log_inc_errorcount (); err = 0; } else log_error ("error removing '%s': %s\n", fname, gpg_strerror (err)); goto leave; } if (opt.verbose) log_info ("key for '%s' removed\n", addrspec); err = 0; leave: xfree (fname); xfree (addrspec); return err; } /* 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 command_remove_key (mailaddr); }