From 5d6c83deaa11327366b0038928200b9f9f85b426 Mon Sep 17 00:00:00 2001 From: Werner Koch Date: Wed, 29 Jun 2016 12:00:22 +0200 Subject: [PATCH] tools: Add gpg-wks-client and gpg-wks-server. * configure.ac: Add option --enable-wks-tools * tools/gpg-wks-client.c: New. * tools/gpg-wks-server.c: New. * tools/gpg-wks.h: new. * tools/wks-receive.c: New. * tools/call-dirmngr.c, tools/call-dirmngr.h: New. -- Note that this is just a starting point and not a finished implementation. Here is how to test the system using foo@test.gnupg.org as example. Prepare: mkdir /var/lib/gnupg/wks chmod o-rwx /var/lib/gnupg/wks mkdir /var/lib/gnupg/wks/test.gnupg.org Run the protocol: ./gpg-wks-client -v --send FPR USERID >x ./gpg-wks-server -v --receive y ./gpg-wks-client --receive z ./gpg-wks-server -v --receive --- configure.ac | 3 + tools/Makefile.am | 34 +- tools/call-dirmngr.c | 205 ++++++++ tools/call-dirmngr.h | 28 ++ tools/gpg-wks-client.c | 615 ++++++++++++++++++++++++ tools/gpg-wks-server.c | 1012 ++++++++++++++++++++++++++++++++++++++++ tools/gpg-wks.h | 53 +++ tools/wks-receive.c | 464 ++++++++++++++++++ 8 files changed, 2413 insertions(+), 1 deletion(-) create mode 100644 tools/call-dirmngr.c create mode 100644 tools/call-dirmngr.h create mode 100644 tools/gpg-wks-client.c create mode 100644 tools/gpg-wks-server.c create mode 100644 tools/gpg-wks.h create mode 100644 tools/wks-receive.c diff --git a/configure.ac b/configure.ac index 215831b73..7f2ca332f 100644 --- a/configure.ac +++ b/configure.ac @@ -130,6 +130,7 @@ GNUPG_BUILD_PROGRAM(tools, yes) GNUPG_BUILD_PROGRAM(doc, yes) GNUPG_BUILD_PROGRAM(symcryptrun, no) GNUPG_BUILD_PROGRAM(gpgtar, yes) +GNUPG_BUILD_PROGRAM(wks-tools, no) AC_SUBST(PACKAGE) AC_SUBST(PACKAGE_GT) @@ -1670,6 +1671,7 @@ AM_CONDITIONAL(BUILD_TOOLS, test "$build_tools" = "yes") AM_CONDITIONAL(BUILD_DOC, test "$build_doc" = "yes") AM_CONDITIONAL(BUILD_SYMCRYPTRUN, test "$build_symcryptrun" = "yes") AM_CONDITIONAL(BUILD_GPGTAR, test "$build_gpgtar" = "yes") +AM_CONDITIONAL(BUILD_WKS_TOOLS, test "$build_wks_tools" = "yes") AM_CONDITIONAL(ENABLE_CARD_SUPPORT, test "$card_support" = yes) AM_CONDITIONAL(NO_TRUST_MODELS, test "$use_trust_models" = no) @@ -1927,6 +1929,7 @@ echo " G13: $build_g13 Dirmngr: $build_dirmngr Gpgtar: $build_gpgtar + WKS tools: $build_wks_tools Protect tool: $show_gnupg_protect_tool_pgm LDAP wrapper: $show_gnupg_dirmngr_ldap_pgm diff --git a/tools/Makefile.am b/tools/Makefile.am index d43ede8d1..362ee1faf 100644 --- a/tools/Makefile.am +++ b/tools/Makefile.am @@ -51,9 +51,17 @@ else gpgtar = endif +if BUILD_WKS_TOOLS + gpg_wks_server = gpg-wks-server + gpg_wks_client = gpg-wks-client +else + gpg_wks_server = + gpg_wks_client = +endif + bin_PROGRAMS = gpgconf gpg-connect-agent ${symcryptrun} if !HAVE_W32_SYSTEM -bin_PROGRAMS += watchgnupg gpgparsemail +bin_PROGRAMS += watchgnupg gpgparsemail ${gpg_wks_server} ${gpg_wks_client} endif if !HAVE_W32CE_SYSTEM bin_PROGRAMS += ${gpgtar} @@ -136,6 +144,30 @@ gpgtar_CFLAGS = $(GPG_ERROR_CFLAGS) gpgtar_LDADD = $(libcommon) $(LIBGCRYPT_LIBS) $(GPG_ERROR_LIBS) \ $(LIBINTL) $(NETLIBS) $(LIBICONV) $(W32SOCKLIBS) +gpg_wks_server_SOURCES = \ + gpg-wks-server.c \ + gpg-wks.h \ + wks-receive.c \ + rfc822parse.c rfc822parse.h \ + mime-parser.c mime-parser.h \ + mime-maker.h mime-maker.c + +gpg_wks_server_CFLAGS = $(GPG_ERROR_CFLAGS) +gpg_wks_server_LDADD = $(libcommon) $(LIBGCRYPT_LIBS) $(GPG_ERROR_LIBS) + +gpg_wks_client_SOURCES = \ + gpg-wks-client.c \ + gpg-wks.h \ + wks-receive.c \ + rfc822parse.c rfc822parse.h \ + mime-parser.c mime-parser.h \ + mime-maker.h mime-maker.c \ + call-dirmngr.c call-dirmngr.h + +gpg_wks_client_CFLAGS = $(LIBASSUAN_CFLAGS) $(GPG_ERROR_CFLAGS) +gpg_wks_client_LDADD = $(libcommon) \ + $(LIBASSUAN_LIBS) $(LIBGCRYPT_LIBS) $(GPG_ERROR_LIBS) + # Make sure that all libs are build before we use them. This is # important for things like make -j2. diff --git a/tools/call-dirmngr.c b/tools/call-dirmngr.c new file mode 100644 index 000000000..0e591dd6d --- /dev/null +++ b/tools/call-dirmngr.c @@ -0,0 +1,205 @@ +/* call-dirmngr.c - Interact with the Dirmngr. + * Copyright (C) 2016 g10 Code GmbH + * + * This file is part of GnuPG. + * + * GnuPG is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * GnuPG is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +#include +#include +#include +#include +#include +#include +#include +#ifdef HAVE_LOCALE_H +# include +#endif + +#include +#include "util.h" +#include "i18n.h" +#include "asshelp.h" +#include "mbox-util.h" +#include "./call-dirmngr.h" + +static struct +{ + int verbose; + int debug_ipc; + int autostart; +} opt; + + + +void +set_dirmngr_options (int verbose, int debug_ipc, int autostart) +{ + opt.verbose = verbose; + opt.debug_ipc = debug_ipc; + opt.autostart = autostart; +} + + +/* Connect to the Dirmngr and return an assuan context. */ +static gpg_error_t +connect_dirmngr (assuan_context_t *r_ctx) +{ + gpg_error_t err; + assuan_context_t ctx; + + *r_ctx = NULL; + err = start_new_dirmngr (&ctx, + GPG_ERR_SOURCE_DEFAULT, + NULL, + opt.autostart, opt.verbose, opt.debug_ipc, + NULL, NULL); + if (!opt.autostart && gpg_err_code (err) == GPG_ERR_NO_DIRMNGR) + { + static int shown; + + if (!shown) + { + shown = 1; + log_info (_("no dirmngr running in this session\n")); + } + } + + if (err) + assuan_release (ctx); + else + { + *r_ctx = ctx; + } + + return err; +} + + + + +/* Parameter structure used with the WKD_GET command. */ +struct wkd_get_parm_s +{ + estream_t memfp; +}; + + +/* Data callback for the WKD_GET command. */ +static gpg_error_t +wkd_get_data_cb (void *opaque, const void *data, size_t datalen) +{ + struct wkd_get_parm_s *parm = opaque; + gpg_error_t err = 0; + size_t nwritten; + + if (!data) + return 0; /* Ignore END commands. */ + if (!parm->memfp) + return 0; /* Data is not required. */ + + if (es_write (parm->memfp, data, datalen, &nwritten)) + err = gpg_error_from_syserror (); + + return err; +} + + +/* Status callback for the WKD_GET command. */ +static gpg_error_t +wkd_get_status_cb (void *opaque, const char *line) +{ + struct wkd_get_parm_s *parm = opaque; + gpg_error_t err = 0; + + (void)line; + (void)parm; + + return err; +} + + +/* Ask the dirmngr for the submission address of a WKD server for the + * mail address ADDRSPEC. On success the submission address is stored + * at R_ADDRSPEC. */ +gpg_error_t +wkd_get_submission_address (const char *addrspec, char **r_addrspec) +{ + gpg_error_t err; + assuan_context_t ctx; + struct wkd_get_parm_s parm; + char *line = NULL; + void *vp; + char *buffer = NULL; + char *p; + + memset (&parm, 0, sizeof parm); + *r_addrspec = NULL; + + err = connect_dirmngr (&ctx); + if (err) + return err; + + line = es_bsprintf ("WKD_GET --submission-address -- %s", addrspec); + if (!line) + { + err = gpg_error_from_syserror (); + goto leave; + } + if (strlen (line) + 2 >= ASSUAN_LINELENGTH) + { + err = gpg_error (GPG_ERR_TOO_LARGE); + goto leave; + } + + parm.memfp = es_fopenmem (0, "rwb"); + if (!parm.memfp) + { + err = gpg_error_from_syserror (); + goto leave; + } + err = assuan_transact (ctx, line, wkd_get_data_cb, &parm, + NULL, NULL, wkd_get_status_cb, &parm); + if (err) + goto leave; + + es_fputc (0, parm.memfp); + if (es_fclose_snatch (parm.memfp, &vp, NULL)) + { + err = gpg_error_from_syserror (); + goto leave; + } + buffer = vp; + parm.memfp = NULL; + p = strchr (buffer, '\n'); + if (p) + *p = 0; + trim_spaces (buffer); + if (!is_valid_mailbox (buffer)) + { + err = gpg_error (GPG_ERR_INV_USER_ID); + goto leave; + } + *r_addrspec = xtrystrdup (buffer); + if (!*r_addrspec) + err = gpg_error_from_syserror (); + + leave: + es_free (buffer); + es_fclose (parm.memfp); + xfree (line); + assuan_release (ctx); + return err; +} diff --git a/tools/call-dirmngr.h b/tools/call-dirmngr.h new file mode 100644 index 000000000..f1bc3686b --- /dev/null +++ b/tools/call-dirmngr.h @@ -0,0 +1,28 @@ +/* call-dirmngr.h - Interact with the Dirmngr. + * Copyright (C) 2016 g10 Code GmbH + * + * This file is part of GnuPG. + * + * GnuPG is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * GnuPG is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ +#ifndef GNUPG_TOOLS_CALL_DIRMNGR_H +#define GNUPG_TOOLS_CALL_DIRMNGR_H + +void set_dirmngr_options (int verbose, int debug_ipc, int autostart); + +gpg_error_t wkd_get_submission_address (const char *addrspec, + char **r_addrspec); + + +#endif /*GNUPG_TOOLS_CALL_DIRMNGR_H*/ diff --git a/tools/gpg-wks-client.c b/tools/gpg-wks-client.c new file mode 100644 index 000000000..c7cb8fb69 --- /dev/null +++ b/tools/gpg-wks-client.c @@ -0,0 +1,615 @@ +/* gpg-wks-client.c - A client for the Web Key Service protocols. + * Copyright (C) 2016 Werner Koch + * + * This file is part of GnuPG. + * + * GnuPG is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * GnuPG is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +#include +#include +#include +#include + +#include "util.h" +#include "i18n.h" +#include "sysutils.h" +#include "init.h" +#include "asshelp.h" +#include "userids.h" +#include "ccparray.h" +#include "exectool.h" +#include "mbox-util.h" +#include "name-value.h" +#include "call-dirmngr.h" +#include "mime-maker.h" +#include "gpg-wks.h" + + +/* Constants to identify the commands and options. */ +enum cmd_and_opt_values + { + aNull = 0, + + oQuiet = 'q', + oVerbose = 'v', + + oDebug = 500, + + aSend, + aReceive, + + oGpgProgram, + + oDummy + }; + + +/* The list of commands and options. */ +static ARGPARSE_OPTS opts[] = { + ARGPARSE_group (300, ("@Commands:\n ")), + + ARGPARSE_c (aSend, "send", + ("send a publication request")), + ARGPARSE_c (aReceive, "receive", + ("receive a confirmation request")), + + 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_end () +}; + + +/* The list of supported debug flags. */ +static struct debug_flags_s debug_flags [] = + { + { DBG_CRYPTO_VALUE , "crypto" }, + { DBG_MEMORY_VALUE , "memory" }, + { DBG_MEMSTAT_VALUE, "memstat" }, + { DBG_IPC_VALUE , "ipc" }, + { DBG_EXTPROG_VALUE, "extprog" }, + { 0, NULL } + }; + + +static void wrong_args (const char *text) GPGRT_ATTR_NORETURN; +static gpg_error_t command_send (const char *fingerprint, char *userid); +static gpg_error_t command_receive_cb (void *opaque, + const char *mediatype, estream_t fp); + + + +/* Print usage information and and provide strings for help. */ +static const char * +my_strusage( int level ) +{ + const char *p; + + switch (level) + { + case 11: p = "gpg-wks-client (@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 --send|--receive [args] (-h for help)"); + break; + case 41: + p = ("Syntax: gpg-wks-client --send|--receive [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 aSend: + case aReceive: + 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); + + /* 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); + + /* Tell call-dirmngr what options we want. */ + set_dirmngr_options (opt.verbose, (opt.debug & DBG_IPC_VALUE), 1); + + /* Run the selected command. */ + switch (cmd) + { + case aSend: + if (argc != 2) + wrong_args ("--send FINGERPRINT USER-ID"); + err = command_send (argv[0], argv[1]); + if (err) + log_error ("sending key failed: %s\n", gpg_strerror (err)); + break; + + case aReceive: + if (argc) + wrong_args ("--receive"); + err = wks_receive (es_stdin, command_receive_cb, NULL); + if (err) + log_error ("reading mail failed: %s\n", gpg_strerror (err)); + break; + + default: + usage (1); + break; + } + + return log_get_errorcount (0)? 1:0; +} + + + +struct get_key_status_parm_s +{ + const char *fpr; + int found; + int count; +}; + +static void +get_key_status_cb (void *opaque, const char *keyword, char *args) +{ + struct get_key_status_parm_s *parm = opaque; + + /*log_debug ("%s: %s\n", keyword, args);*/ + if (!strcmp (keyword, "EXPORTED")) + { + parm->count++; + if (!ascii_strcasecmp (args, parm->fpr)) + parm->found = 1; + } +} + + +/* Get a key by fingerprint from gpg's keyring and make sure that the + * mail address ADDRSPEC is included in the key. The key is returned + * as a new memory stream at R_KEY. + * + * Fixme: After we have implemented import and export filters for gpg + * this function shall only return a key with just this user id. */ +static gpg_error_t +get_key (estream_t *r_key, const char *fingerprint, const char *addrspec) +{ + gpg_error_t err; + ccparray_t ccp; + const char **argv; + estream_t key; + struct get_key_status_parm_s parm; + + (void)addrspec; /* FIXME - need to use it. */ + + memset (&parm, 0, sizeof parm); + + *r_key = NULL; + + key = es_fopenmem (0, "w+b"); + if (!key) + { + err = gpg_error_from_syserror (); + log_error ("error allocating memory buffer: %s\n", gpg_strerror (err)); + return err; + } + + ccparray_init (&ccp, 0); + + ccparray_put (&ccp, "--no-options"); + if (!opt.verbose) + ccparray_put (&ccp, "--quiet"); + else if (opt.verbose > 1) + ccparray_put (&ccp, "--verbose"); + ccparray_put (&ccp, "--batch"); + ccparray_put (&ccp, "--status-fd=2"); + ccparray_put (&ccp, "--always-trust"); + ccparray_put (&ccp, "--armor"); + ccparray_put (&ccp, "--export-options=export-minimal"); + ccparray_put (&ccp, "--export"); + ccparray_put (&ccp, "--"); + ccparray_put (&ccp, fingerprint); + + ccparray_put (&ccp, NULL); + argv = ccparray_get (&ccp, NULL); + if (!argv) + { + err = gpg_error_from_syserror (); + goto leave; + } + parm.fpr = fingerprint; + err = gnupg_exec_tool_stream (opt.gpg_program, argv, NULL, + NULL, key, + get_key_status_cb, &parm); + if (!err && parm.count > 1) + err = gpg_error (GPG_ERR_TOO_MANY); + else if (!err && !parm.found) + err = gpg_error (GPG_ERR_NOT_FOUND); + if (err) + { + log_error ("export failed: %s\n", gpg_strerror (err)); + goto leave; + } + + es_rewind (key); + *r_key = key; + key = NULL; + + leave: + es_fclose (key); + xfree (argv); + return err; +} + + + +/* Locate the key by fingerprint and userid and send a publication + * request. */ +static gpg_error_t +command_send (const char *fingerprint, char *userid) +{ + gpg_error_t err; + KEYDB_SEARCH_DESC desc; + char *addrspec = NULL; + estream_t key = NULL; + char *submission_to = NULL; + mime_maker_t mime = NULL; + + 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 = get_key (&key, fingerprint, addrspec); + if (err) + goto leave; + log_debug ("fixme: Check that the key has the requested user-id.\n"); + + /* Get the submission address. */ + err = wkd_get_submission_address (addrspec, &submission_to); + if (err) + goto leave; + log_info ("submitting request to '%s'\n", submission_to); + + /* 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; + + err = mime_maker_add_header (mime, "Content-type", "application/pgp-keys"); + if (err) + goto leave; + + err = mime_maker_add_stream (mime, &key); + if (err) + goto leave; + + err = mime_maker_make (mime, es_stdout); + + leave: + mime_maker_release (mime); + xfree (submission_to); + es_fclose (key); + xfree (addrspec); + return err; +} + + + +static gpg_error_t +send_confirmation_response (const char *sender, const char *address, + const char *nonce) +{ + gpg_error_t err; + estream_t body = NULL; + /* FIXME: Encrypt and sign the response. */ + /* 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 encosind 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-response\n" + "sender: %s\n" + "address: %s\n" + "nonce: %s\n"), + sender, + address, + nonce); + + es_rewind (body); + /* err = encrypt_stream (&bodyenc, body, ctx->fpr); */ + /* 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, "Content-Type", */ + /* "multipart/encrypted; " */ + /* "protocol=\"application/pgp-encrypted\""); */ + /* if (err) */ + /* goto leave; */ + /* err = mime_maker_add_container (mime, "multipart/encrypted"); */ + /* 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_header (mime, "Content-Type", + "application/vnd.gnupg.wks"); + if (err) + goto leave; + + err = mime_maker_add_stream (mime, &body); + if (err) + goto leave; + + err = mime_maker_make (mime, es_stdout); + + leave: + mime_maker_release (mime); + /* xfree (bodyenc); */ + xfree (body); + return err; +} + + +/* Reply to a confirmation request. The MSG has already been + * decrypted and we only need to send the nonce back. */ +static gpg_error_t +process_confirmation_request (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 ("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; + } + + /* FIXME: Check that the fingerprint matches the key used to decrypt the + * message. */ + + /* 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; + + err = send_confirmation_response (sender, 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) +{ + gpg_error_t err; + + (void)opaque; + + if (!strcmp (mediatype, "application/vnd.gnupg.wks")) + err = process_confirmation_request (msg); + else + { + log_info ("ignoring unexpected message of type '%s'\n", mediatype); + err = gpg_error (GPG_ERR_UNEXPECTED_MSG); + } + + return err; +} diff --git a/tools/gpg-wks-server.c b/tools/gpg-wks-server.c new file mode 100644 index 000000000..2ae84e251 --- /dev/null +++ b/tools/gpg-wks-server.c @@ -0,0 +1,1012 @@ +/* gpg-wks-server.c - A server for the Web Key Service protocols. + * Copyright (C) 2016 Werner Koch + * + * This file is part of GnuPG. + * + * GnuPG is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * GnuPG is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +/* The Web Key Service I-D defines an update protocol to stpre a + * public key in the Web Key Directory. The current specification is + * draft-koch-openpgp-webkey-service-01.txt. + */ + +#include +#include +#include +#include +#include +#ifdef HAVE_STAT +# include +#endif + +#include "util.h" +#include "init.h" +#include "sysutils.h" +#include "ccparray.h" +#include "exectool.h" +#include "zb32.h" +#include "mbox-util.h" +#include "name-value.h" +#include "mime-maker.h" +#include "gpg-wks.h" + + +/* Constants to identify the commands and options. */ +enum cmd_and_opt_values + { + aNull = 0, + + oQuiet = 'q', + oVerbose = 'v', + + oDebug = 500, + + aReceive, + aCron, + + oGpgProgram, + + 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_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_end () +}; + + +/* The list of supported debug flags. */ +static struct debug_flags_s debug_flags [] = + { + { 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; + strlist_t mboxes; /* List of addr-specs taken from the UIDs. */ +}; +typedef struct server_ctx_s *server_ctx_t; + + + +static gpg_error_t command_receive_cb (void *opaque, + const char *mediatype, estream_t fp); + + + +/* Print usage information and and provide strings for help. */ +static const char * +my_strusage( int level ) +{ + const char *p; + + switch (level) + { + case 11: p = "gpg-wks-server (@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 aReceive: + case aCron: + 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; + 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 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_IRWXO)) + { + log_error ("directory '%s' has too relaxed permissions\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); + if (err) + log_error ("reading mail failed: %s\n", gpg_strerror (err)); + break; + + case aCron: + if (argc) + wrong_args ("--cron"); + break; + + default: + usage (1); + break; + } + + return log_get_errorcount (0)? 1:0; +} + + + +static void +list_key_status_cb (void *opaque, const char *keyword, char *args) +{ + server_ctx_t ctx = opaque; + (void)ctx; + if (opt.debug) + log_debug ("%s: %s\n", keyword, args); +} + + +static gpg_error_t +list_key (server_ctx_t ctx, estream_t key) +{ + gpg_error_t err; + ccparray_t ccp; + const char **argv; + estream_t listing; + char *line = NULL; + size_t length_of_line = 0; + size_t maxlen; + ssize_t len; + char **fields = NULL; + int nfields; + int lnr; + char *mbox = NULL; + + /* We store our results in the context - clear it first. */ + xfree (ctx->fpr); + ctx->fpr = NULL; + free_strlist (ctx->mboxes); + ctx->mboxes = NULL; + + /* Open a memory stream. */ + listing = es_fopenmem (0, "w+b"); + if (!listing) + { + err = gpg_error_from_syserror (); + log_error ("error allocating memory buffer: %s\n", gpg_strerror (err)); + return err; + } + + ccparray_init (&ccp, 0); + + ccparray_put (&ccp, "--no-options"); + if (!opt.verbose) + 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, "--with-colons"); + ccparray_put (&ccp, "--dry-run"); + ccparray_put (&ccp, "--import-options=import-minimal,import-show"); + ccparray_put (&ccp, "--import"); + + ccparray_put (&ccp, NULL); + argv = ccparray_get (&ccp, NULL); + if (!argv) + { + err = gpg_error_from_syserror (); + goto leave; + } + err = gnupg_exec_tool_stream (opt.gpg_program, argv, key, + NULL, listing, + list_key_status_cb, ctx); + if (err) + { + log_error ("import failed: %s\n", gpg_strerror (err)); + goto leave; + } + + es_rewind (listing); + lnr = 0; + maxlen = 2048; /* Set limit. */ + while ((len = es_read_line (listing, &line, &length_of_line, &maxlen)) > 0) + { + lnr++; + if (!maxlen) + { + log_error ("received line too long\n"); + err = gpg_error (GPG_ERR_LINE_TOO_LONG); + goto leave; + } + /* Strip newline and carriage return, if present. */ + while (len > 0 + && (line[len - 1] == '\n' || line[len - 1] == '\r')) + line[--len] = '\0'; + /* log_debug ("line '%s'\n", line); */ + + xfree (fields); + fields = strtokenize (line, ":"); + if (!fields) + { + err = gpg_error_from_syserror (); + log_error ("strtokenize failed: %s\n", gpg_strerror (err)); + goto leave; + } + for (nfields = 0; fields[nfields]; nfields++) + ; + if (!nfields) + { + err = gpg_error (GPG_ERR_INV_ENGINE); + goto leave; + } + if (!strcmp (fields[0], "sec")) + { + /* gpg may return "sec" as the first record - but we do not + * accept secret keys. */ + err = gpg_error (GPG_ERR_NO_PUBKEY); + goto leave; + } + if (lnr == 1 && strcmp (fields[0], "pub")) + { + /* First record is not a public key. */ + err = gpg_error (GPG_ERR_INV_ENGINE); + goto leave; + } + if (lnr > 1 && !strcmp (fields[0], "pub")) + { + /* More than one public key. */ + err = gpg_error (GPG_ERR_TOO_MANY); + goto leave; + } + if (!strcmp (fields[0], "sub") || !strcmp (fields[0], "ssb")) + break; /* We can stop parsing here. */ + + if (!strcmp (fields[0], "fpr") && nfields > 9 && !ctx->fpr) + { + ctx->fpr = xtrystrdup (fields[9]); + if (!ctx->fpr) + { + err = gpg_error_from_syserror (); + goto leave; + } + } + else if (!strcmp (fields[0], "uid") && nfields > 9) + { + /* Fixme: Unescape fields[9] */ + xfree (mbox); + mbox = mailbox_from_userid (fields[9]); + if (mbox && !append_to_strlist_try (&ctx->mboxes, mbox)) + { + err = gpg_error_from_syserror (); + goto leave; + } + } + } + if (len < 0 || es_ferror (listing)) + log_error ("error reading memory stream\n"); + + leave: + xfree (mbox); + xfree (fields); + es_free (line); + xfree (argv); + es_fclose (listing); + return err; +} + + +static void +encrypt_stream_status_cb (void *opaque, const char *keyword, char *args) +{ + (void)opaque; + + if (opt.debug) + log_debug ("%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 with FINGERPRINT. */ +static gpg_error_t +encrypt_stream (estream_t *r_output, estream_t input, const char *fingerprint) +{ + 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, "--armor"); + 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_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; +} + + +/* 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. */ +static gpg_error_t +store_key_as_pending (const char *dir, estream_t key, char **r_nonce) +{ + 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; + + dname = make_filename_try (dir, "pending", NULL); + if (!dname) + { + err = gpg_error_from_syserror (); + goto leave; + } + + if (!gnupg_mkdir (dname, "-rwx")) + log_info ("directory '%s' created\n", dname); + + /* 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; + else + xfree (nonce); + + xfree (fname); + xfree (dname); + return err; +} + + +static gpg_error_t +send_confirmation_request (server_ctx_t ctx, const char *mbox, const char *nonce) +{ + 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 encosind 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"), + "sender@example.org", + mbox, + ctx->fpr, + nonce); + + es_rewind (body); + err = encrypt_stream (&bodyenc, body, ctx->fpr); + 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, "To", mbox); + if (err) + goto leave; + err = mime_maker_add_header (mime, "Subject", "confirm key publication"); + 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, "multipart/encrypted"); + 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 = mime_maker_make (mime, es_stdout); + + leave: + mime_maker_release (mime); + xfree (bodyenc); + xfree (body); + 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; + strlist_t sl; + const char *s; + char *dname = NULL; + char *nonce = NULL; + + /* First figure out the user id from the key. */ + err = list_key (ctx, key); + if (err) + goto leave; + if (!ctx->fpr) + { + log_error ("error parsing key (no fingerprint)\n"); + err = gpg_error (GPG_ERR_NO_PUBKEY); + goto leave; + } + log_info ("fingerprint: %s\n", ctx->fpr); + for (sl = ctx->mboxes; sl; sl = sl->next) + { + log_info (" addr-spec: %s\n", sl->d); + } + + /* Walk over all user ids and send confirmation requests for those + * we support. */ + for (sl = ctx->mboxes; sl; sl = sl->next) + { + s = strchr (sl->d, '@'); + 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; + } + /* Fixme: check for proper directory permissions. */ + if (access (dname, W_OK)) + { + log_info ("skipping address '%s': Domain not configured\n", sl->d); + continue; + } + log_info ("storing address '%s'\n", sl->d); + + xfree (nonce); + err = store_key_as_pending (dname, key, &nonce); + if (err) + goto leave; + + err = send_confirmation_request (ctx, sl->d, nonce); + if (err) + goto leave; + } + + leave: + if (nonce) + wipememory (nonce, strlen (nonce)); + xfree (nonce); + xfree (dname); + return 0; +} + + + +/* 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; + strlist_t sl; + + 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. */ + err = list_key (ctx, key); + if (err) + goto leave; + if (!ctx->fpr) + { + log_error ("error parsing key (no fingerprint)\n"); + err = gpg_error (GPG_ERR_NO_PUBKEY); + goto leave; + } + log_info ("fingerprint: %s\n", ctx->fpr); + for (sl = ctx->mboxes; sl; sl = sl->next) + log_info (" addr-spec: %s\n", sl->d); + + /* 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 (!strcmp (sl->d, 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. */ + s = strchr (address, '@'); + log_assert (s); + { + char sha1buf[20]; + gcry_md_hash_buffer (GCRY_MD_SHA1, sha1buf, address, s - address); + hash = zb32_encode (sha1buf, 8*20); + } + if (!hash) + { + err = gpg_error_from_syserror (); + goto leave; + } + + { + /*FIXME: This is a hack to make installation easier. It is better + * to let --cron create the required directories. */ + fnewname = make_filename_try (opt.directory, domain, "hu", NULL); + if (!fnewname) + { + err = gpg_error_from_syserror (); + goto leave; + } + if (!gnupg_mkdir (fnewname, "-rwxr-xr-x")) + log_info ("directory '%s' created\n", fname); + xfree (fnewname); + } + fnewname = make_filename_try (opt.directory, domain, "hu", hash, NULL); + if (!fnewname) + { + err = gpg_error_from_syserror (); + goto leave; + } + + /* Publish. */ + if (rename (fname, fnewname)) + { + err = gpg_error_from_syserror (); + log_error ("renaming '%s' to '%s' failed: %s\n", + fname, fnewname, gpg_strerror (err)); + goto leave; + } + + log_info ("key %s published for '%s'\n", ctx->fpr, address); + + leave: + es_fclose (key); + xfree (hash); + xfree (fnewname); + xfree (fname); + 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) +{ + gpg_error_t err; + struct server_ctx_s ctx; + + memset (&ctx, 0, sizeof ctx); + + (void)opaque; + + 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_strlist (ctx.mboxes); + + return err; +} diff --git a/tools/gpg-wks.h b/tools/gpg-wks.h new file mode 100644 index 000000000..249b10af5 --- /dev/null +++ b/tools/gpg-wks.h @@ -0,0 +1,53 @@ +/* gpg-wks.h - Common definitions for wks server and client. + * Copyright (C) 2016 g10 Code GmbH + * + * This file is part of GnuPG. + * + * GnuPG is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * GnuPG is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +#ifndef GNUPG_GPG_WKS_H +#define GNUPG_GPG_WKS_H + +#include "../common/util.h" +#include "../common/strlist.h" + +/* We keep all global options in the structure OPT. */ +struct +{ + int verbose; + unsigned int debug; + int quiet; + const char *gpg_program; + const char *directory; +} opt; + +/* Debug values and macros. */ +#define DBG_CRYPTO_VALUE 4 /* Debug low level crypto. */ +#define DBG_MEMORY_VALUE 32 /* Debug memory allocation stuff. */ +#define DBG_MEMSTAT_VALUE 128 /* Show memory statistics. */ +#define DBG_IPC_VALUE 1024 /* Debug assuan communication. */ +#define DBG_EXTPROG_VALUE 16384 /* debug external program calls */ + + +/*-- wks-receive.c --*/ +gpg_error_t wks_receive (estream_t fp, + gpg_error_t (*result_cb)(void *opaque, + const char *mediatype, + estream_t data), + void *cb_data); + + + +#endif /*GNUPG_GPG_WKS_H*/ diff --git a/tools/wks-receive.c b/tools/wks-receive.c new file mode 100644 index 000000000..59141fcdc --- /dev/null +++ b/tools/wks-receive.c @@ -0,0 +1,464 @@ +/* wks-receive.c - Receive a WKS mail + * Copyright (C) 2016 g10 Code GmbH + * + * This file is part of GnuPG. + * + * GnuPG is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * GnuPG is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +#include +#include +#include +#include + +#include "util.h" +#include "ccparray.h" +#include "exectool.h" +#include "gpg-wks.h" +#include "mime-parser.h" + + +/* Limit of acceptable signed data. */ +#define MAX_SIGNEDDATA 10000 + +/* Limit of acceptable signature. */ +#define MAX_SIGNATURE 10000 + +/* Limit of acceptable encrypted data. */ +#define MAX_ENCRYPTED 100000 + +/* Data for a received object. */ +struct receive_ctx_s +{ + estream_t encrypted; + estream_t plaintext; + estream_t signeddata; + estream_t signature; + estream_t key_data; + estream_t wkd_data; + unsigned int collect_key_data:1; + unsigned int collect_wkd_data:1; +}; +typedef struct receive_ctx_s *receive_ctx_t; + + + +static void +decrypt_data_status_cb (void *opaque, const char *keyword, char *args) +{ + receive_ctx_t ctx = opaque; + (void)ctx; + log_debug ("%s: %s\n", keyword, args); +} + + +/* Decrypt the collected data. */ +static void +decrypt_data (receive_ctx_t ctx) +{ + gpg_error_t err; + ccparray_t ccp; + const char **argv; + int c; + + es_rewind (ctx->encrypted); + + if (!ctx->plaintext) + ctx->plaintext = es_fopenmem (0, "w+b"); + if (!ctx->plaintext) + { + err = gpg_error_from_syserror (); + log_error ("error allocating space for plaintext: %s\n", + gpg_strerror (err)); + return; + } + + ccparray_init (&ccp, 0); + + /* 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=0xf0000"); /*FIXME: Change s/F/1/ */ + ccparray_put (&ccp, "--batch"); + if (opt.verbose) + ccparray_put (&ccp, "--verbose"); + ccparray_put (&ccp, "--always-trust"); + 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, ctx->encrypted, + NULL, ctx->plaintext, + decrypt_data_status_cb, ctx); + if (err) + { + log_error ("decryption failed: %s\n", gpg_strerror (err)); + goto leave; + } + + if (opt.debug) + { + es_rewind (ctx->plaintext); + log_debug ("plaintext: '"); + while ((c = es_getc (ctx->plaintext)) != EOF) + log_printf ("%c", c); + log_printf ("'\n"); + } + es_rewind (ctx->plaintext); + + leave: + xfree (argv); +} + + +static void +verify_signature_status_cb (void *opaque, const char *keyword, char *args) +{ + receive_ctx_t ctx = opaque; + (void)ctx; + log_debug ("%s: %s\n", keyword, args); +} + +/* Verify the signed data. */ +static void +verify_signature (receive_ctx_t ctx) +{ + gpg_error_t err; + ccparray_t ccp; + const char **argv; + + log_assert (ctx->signeddata); + log_assert (ctx->signature); + es_rewind (ctx->signeddata); + es_rewind (ctx->signature); + + ccparray_init (&ccp, 0); + + ccparray_put (&ccp, "--batch"); + if (opt.verbose) + ccparray_put (&ccp, "--verbose"); + ccparray_put (&ccp, "--enable-special-filenames"); + ccparray_put (&ccp, "--status-fd=2"); + ccparray_put (&ccp, "--verify"); + ccparray_put (&ccp, "--"); + ccparray_put (&ccp, "-&@INEXTRA@"); + 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, ctx->signeddata, + ctx->signature, NULL, + verify_signature_status_cb, ctx); + if (err) + { + log_error ("verification failed: %s\n", gpg_strerror (err)); + goto leave; + } + + leave: + xfree (argv); +} + + +static gpg_error_t +collect_encrypted (void *cookie, const char *data) +{ + receive_ctx_t ctx = cookie; + + if (!ctx->encrypted) + if (!(ctx->encrypted = es_fopenmem (MAX_ENCRYPTED, "w+b,samethread"))) + return gpg_error_from_syserror (); + if (data) + es_fputs (data, ctx->encrypted); + + if (es_ferror (ctx->encrypted)) + return gpg_error_from_syserror (); + + if (!data) + { + decrypt_data (ctx); + } + + return 0; +} + + +static gpg_error_t +collect_signeddata (void *cookie, const char *data) +{ + receive_ctx_t ctx = cookie; + + if (!ctx->signeddata) + if (!(ctx->signeddata = es_fopenmem (MAX_SIGNEDDATA, "w+b,samethread"))) + return gpg_error_from_syserror (); + if (data) + es_fputs (data, ctx->signeddata); + + if (es_ferror (ctx->signeddata)) + return gpg_error_from_syserror (); + return 0; +} + +static gpg_error_t +collect_signature (void *cookie, const char *data) +{ + receive_ctx_t ctx = cookie; + + if (!ctx->signature) + if (!(ctx->signature = es_fopenmem (MAX_SIGNATURE, "w+b,samethread"))) + return gpg_error_from_syserror (); + if (data) + es_fputs (data, ctx->signature); + + if (es_ferror (ctx->signature)) + return gpg_error_from_syserror (); + + if (!data) + { + verify_signature (ctx); + } + + return 0; +} + + +static gpg_error_t +new_part (void *cookie, const char *mediatype, const char *mediasubtype) +{ + receive_ctx_t ctx = cookie; + gpg_error_t err = 0; + + ctx->collect_key_data = 0; + ctx->collect_wkd_data = 0; + + if (!strcmp (mediatype, "application") + && !strcmp (mediasubtype, "pgp-keys")) + { + log_info ("new '%s/%s' message part\n", mediatype, mediasubtype); + if (ctx->key_data) + { + log_error ("we already got a key - ignoring this part\n"); + err = gpg_error (GPG_ERR_FALSE); + } + else + { + ctx->key_data = es_fopenmem (0, "w+b"); + if (!ctx->key_data) + { + err = gpg_error_from_syserror (); + log_error ("error allocating space for key: %s\n", + gpg_strerror (err)); + } + else + { + ctx->collect_key_data = 1; + err = gpg_error (GPG_ERR_TRUE); /* We want the part decoded. */ + } + } + } + else if (!strcmp (mediatype, "application") + && !strcmp (mediasubtype, "vnd.gnupg.wks")) + { + log_info ("new '%s/%s' message part\n", mediatype, mediasubtype); + if (ctx->wkd_data) + { + log_error ("we already got a wkd part - ignoring this part\n"); + err = gpg_error (GPG_ERR_FALSE); + } + else + { + ctx->wkd_data = es_fopenmem (0, "w+b"); + if (!ctx->wkd_data) + { + err = gpg_error_from_syserror (); + log_error ("error allocating space for key: %s\n", + gpg_strerror (err)); + } + else + { + ctx->collect_wkd_data = 1; + err = gpg_error (GPG_ERR_TRUE); /* We want the part decoded. */ + } + } + } + else + { + log_error ("unexpected '%s/%s' message part\n", mediatype, mediasubtype); + err = gpg_error (GPG_ERR_FALSE); /* We do not want the part. */ + } + + return err; +} + + +static gpg_error_t +part_data (void *cookie, const void *data, size_t datalen) +{ + receive_ctx_t ctx = cookie; + + if (data) + { + if (opt.debug) + log_debug ("part_data: '%.*s'\n", (int)datalen, (const char*)data); + if (ctx->collect_key_data) + { + if (es_write (ctx->key_data, data, datalen, NULL) + || es_fputs ("\n", ctx->key_data)) + return gpg_error_from_syserror (); + } + if (ctx->collect_wkd_data) + { + if (es_write (ctx->wkd_data, data, datalen, NULL) + || es_fputs ("\n", ctx->wkd_data)) + return gpg_error_from_syserror (); + } + } + else + { + if (opt.debug) + log_debug ("part_data: finished\n"); + ctx->collect_key_data = 0; + ctx->collect_wkd_data = 0; + } + return 0; +} + + +/* Receive a WKS mail from FP and process it accordingly. On success + * the RESULT_CB is called with the mediatype and a stream with the + * decrypted data. */ +gpg_error_t +wks_receive (estream_t fp, + gpg_error_t (*result_cb)(void *opaque, + const char *mediatype, + estream_t data), + void *cb_data) +{ + gpg_error_t err; + receive_ctx_t ctx; + mime_parser_t parser; + estream_t plaintext = NULL; + int c; + + ctx = xtrycalloc (1, sizeof *ctx); + if (!ctx) + return gpg_error_from_syserror (); + + err = mime_parser_new (&parser, ctx); + if (err) + goto leave; + if (opt.verbose > 1 || opt.debug) + mime_parser_set_verbose (parser, opt.debug? 10: 1); + mime_parser_set_new_part (parser, new_part); + mime_parser_set_part_data (parser, part_data); + mime_parser_set_collect_encrypted (parser, collect_encrypted); + mime_parser_set_collect_signeddata (parser, collect_signeddata); + mime_parser_set_collect_signature (parser, collect_signature); + + err = mime_parser_parse (parser, fp); + if (err) + goto leave; + + if (ctx->key_data) + log_info ("key data found\n"); + if (ctx->wkd_data) + log_info ("wkd data found\n"); + + if (ctx->plaintext) + { + if (opt.verbose) + log_info ("parsing decrypted message\n"); + plaintext = ctx->plaintext; + ctx->plaintext = NULL; + if (ctx->encrypted) + es_rewind (ctx->encrypted); + if (ctx->signeddata) + es_rewind (ctx->signeddata); + if (ctx->signature) + es_rewind (ctx->signature); + err = mime_parser_parse (parser, plaintext); + if (err) + return err; + } + + if (!ctx->key_data && !ctx->wkd_data) + { + log_error ("no suitable data found in the message\n"); + err = gpg_error (GPG_ERR_NO_DATA); + goto leave; + } + + if (ctx->key_data) + { + if (opt.debug) + { + es_rewind (ctx->key_data); + log_debug ("Key: '"); + log_printf ("\n"); + while ((c = es_getc (ctx->key_data)) != EOF) + log_printf ("%c", c); + log_printf ("'\n"); + } + if (result_cb) + { + es_rewind (ctx->key_data); + err = result_cb (cb_data, "application/pgp-keys", ctx->key_data); + if (err) + goto leave; + } + } + if (ctx->wkd_data) + { + if (opt.debug) + { + es_rewind (ctx->wkd_data); + log_debug ("WKD: '"); + log_printf ("\n"); + while ((c = es_getc (ctx->wkd_data)) != EOF) + log_printf ("%c", c); + log_printf ("'\n"); + } + if (result_cb) + { + es_rewind (ctx->wkd_data); + err = result_cb (cb_data, "application/vnd.gnupg.wks", ctx->wkd_data); + if (err) + goto leave; + } + } + + + leave: + es_fclose (plaintext); + mime_parser_release (parser); + es_fclose (ctx->encrypted); + es_fclose (ctx->plaintext); + es_fclose (ctx->signeddata); + es_fclose (ctx->signature); + es_fclose (ctx->key_data); + es_fclose (ctx->wkd_data); + xfree (ctx); + return err; +}