tools: Add gpg-wks-client and gpg-wks-server.
authorWerner Koch <wk@gnupg.org>
Wed, 29 Jun 2016 10:00:22 +0000 (12:00 +0200)
committerWerner Koch <wk@gnupg.org>
Wed, 29 Jun 2016 10:04:11 +0000 (12:04 +0200)
* 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  <x >y
  ./gpg-wks-client --receive <y >z
  ./gpg-wks-server -v --receive  <z

You should also setup a cron job to rsync
/var/lib/gnupg/wks/test.gnupg.org/hu/* to the webserver.

Signed-off-by: Werner Koch <wk@gnupg.org>
configure.ac
tools/Makefile.am
tools/call-dirmngr.c [new file with mode: 0644]
tools/call-dirmngr.h [new file with mode: 0644]
tools/gpg-wks-client.c [new file with mode: 0644]
tools/gpg-wks-server.c [new file with mode: 0644]
tools/gpg-wks.h [new file with mode: 0644]
tools/wks-receive.c [new file with mode: 0644]

index 215831b..7f2ca33 100644 (file)
@@ -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
index d43ede8..362ee1f 100644 (file)
@@ -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 (file)
index 0000000..0e591dd
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <unistd.h>
+#include <time.h>
+#ifdef HAVE_LOCALE_H
+# include <locale.h>
+#endif
+
+#include <assuan.h>
+#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;
+}
+
+
+
+\f
+/* 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 (file)
index 0000000..f1bc368
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.
+ */
+#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 (file)
index 0000000..c7cb8fb
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#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);
+
+
+\f
+/* 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);
+}
+
+
+\f
+/* 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;
+}
+
+
+\f
+/* 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;
+}
+
+
+\f
+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;
+}
+
+
+\f
+/* 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;
+}
+
+
+\f
+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 (file)
index 0000000..2ae84e2
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.
+ */
+
+/* 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 <config.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#ifdef HAVE_STAT
+# include <sys/stat.h>
+#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);
+
+
+\f
+/* 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);
+}
+
+
+\f
+/* 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;
+}
+
+
+\f
+/* 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;
+}
+
+
+\f
+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;
+}
+
+
+\f
+/* 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;
+}
+
+
+\f
+/* 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 (file)
index 0000000..249b10a
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.
+ */
+
+#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 (file)
index 0000000..59141fc
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#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;
+}