wks: Move a few server functions to wks-util.
[gnupg.git] / tools / gpg-wks-server.c
index 6fbcc05..eae93b3 100644 (file)
@@ -1,25 +1,26 @@
 /* gpg-wks-server.c - A server for the Web Key Service protocols.
- * Copyright (C) 2016 Werner Koch
+ * Copyright (C) 2016, 2018 Werner Koch
+ * Copyright (C) 2016 Bundesamt für Sicherheit in der Informationstechnik
  *
  * 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.
+ * This file is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
  *
- * GnuPG is distributed in the hope that it will be useful,
+ * This file is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
+ * GNU Lesser 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/>.
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, see <https://www.gnu.org/licenses/>.
  */
 
-/* The Web Key Service I-D defines an update protocol to stpre a
+/* The Web Key Service I-D defines an update protocol to store a
  * public key in the Web Key Directory.  The current specification is
- * draft-koch-openpgp-webkey-service-01.txt.
+ * draft-koch-openpgp-webkey-service-05.txt.
  */
 
 #include <config.h>
 #include <sys/stat.h>
 #include <dirent.h>
 
-#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 "../common/util.h"
+#include "../common/init.h"
+#include "../common/sysutils.h"
+#include "../common/userids.h"
+#include "../common/ccparray.h"
+#include "../common/exectool.h"
+#include "../common/zb32.h"
+#include "../common/mbox-util.h"
+#include "../common/name-value.h"
 #include "mime-maker.h"
 #include "send-mail.h"
 #include "gpg-wks.h"
@@ -56,16 +58,24 @@ enum cmd_and_opt_values
     oQuiet      = 'q',
     oVerbose   = 'v',
     oOutput     = 'o',
+    oDirectory  = 'C',
 
     oDebug      = 500,
 
     aReceive,
     aCron,
+    aListDomains,
+    aInstallKey,
+    aRevokeKey,
+    aRemoveKey,
+    aCheck,
 
     oGpgProgram,
     oSend,
     oFrom,
     oHeader,
+    oWithDir,
+    oWithFile,
 
     oDummy
   };
@@ -79,6 +89,17 @@ static ARGPARSE_OPTS opts[] = {
               ("receive a submission or confirmation")),
   ARGPARSE_c (aCron,      "cron",
               ("run regular jobs")),
+  ARGPARSE_c (aListDomains, "list-domains",
+              ("list configured domains")),
+  ARGPARSE_c (aCheck, "check",
+              ("check whether a key is installed")),
+  ARGPARSE_c (aCheck, "check-key", "@"),
+  ARGPARSE_c (aInstallKey, "install-key",
+              "install a key from FILE into the WKD"),
+  ARGPARSE_c (aRemoveKey, "remove-key",
+              "remove a key from the WKD"),
+  ARGPARSE_c (aRevokeKey, "revoke-key",
+              "mark a key as revoked"),
 
   ARGPARSE_group (301, ("@\nOptions:\n ")),
 
@@ -88,9 +109,12 @@ static ARGPARSE_OPTS opts[] = {
   ARGPARSE_s_s (oGpgProgram, "gpg", "@"),
   ARGPARSE_s_n (oSend, "send", "send the mail using sendmail"),
   ARGPARSE_s_s (oOutput, "output", "|FILE|write the mail to FILE"),
+  ARGPARSE_s_s (oDirectory, "directory", "|DIR|use DIR as top directory"),
   ARGPARSE_s_s (oFrom, "from", "|ADDR|use ADDR as the default sender"),
   ARGPARSE_s_s (oHeader, "header" ,
                 "|NAME=VALUE|add \"NAME: VALUE\" as header to all mails"),
+  ARGPARSE_s_n (oWithDir, "with-dir", "@"),
+  ARGPARSE_s_n (oWithFile, "with-file", "@"),
 
   ARGPARSE_end ()
 };
@@ -99,6 +123,8 @@ static ARGPARSE_OPTS opts[] = {
 /* The list of supported debug flags.  */
 static struct debug_flags_s debug_flags [] =
   {
+    { DBG_MIME_VALUE   , "mime"    },
+    { DBG_PARSER_VALUE , "parser"  },
     { DBG_CRYPTO_VALUE , "crypto"  },
     { DBG_MEMORY_VALUE , "memory"  },
     { DBG_MEMSTAT_VALUE, "memstat" },
@@ -112,19 +138,32 @@ static struct debug_flags_s debug_flags [] =
 struct server_ctx_s
 {
   char *fpr;
-  strlist_t mboxes;  /* List of addr-specs taken from the UIDs.  */
+  uidinfo_list_t mboxes;  /* List with addr-specs taken from the UIDs.  */
+  unsigned int draft_version_2:1; /* Client supports the draft 2.  */
 };
 typedef struct server_ctx_s *server_ctx_t;
 
 
+/* Flag for --with-dir.  */
+static int opt_with_dir;
+/* Flag for --with-file.  */
+static int opt_with_file;
+
+
+/* Prototypes.  */
+static gpg_error_t get_domain_list (strlist_t *r_list);
 
 static gpg_error_t command_receive_cb (void *opaque,
-                                       const char *mediatype, estream_t fp);
+                                       const char *mediatype, estream_t fp,
+                                       unsigned int flags);
+static gpg_error_t command_list_domains (void);
+static gpg_error_t command_revoke_key (const char *mailaddr);
+static gpg_error_t command_check_key (const char *mailaddr);
 static gpg_error_t command_cron (void);
 
 
 \f
-/* Print usage information and and provide strings for help. */
+/* Print usage information and provide strings for help. */
 static const char *
 my_strusage( int level )
 {
@@ -132,8 +171,8 @@ my_strusage( int level )
 
   switch (level)
     {
-    case 11: p = "gpg-wks-server (@GNUPG@)";
-      break;
+    case 11: p = "gpg-wks-server"; break;
+    case 12: p = "@GNUPG@"; break;
     case 13: p = VERSION; break;
     case 17: p = PRINTABLE_OS_NAME; break;
     case 19: p = ("Please report bugs to <@EMAIL@>.\n"); break;
@@ -186,6 +225,9 @@ parse_arguments (ARGPARSE_ARGS *pargs, ARGPARSE_OPTS *popts)
         case oGpgProgram:
           opt.gpg_program = pargs->r.ret_str;
           break;
+        case oDirectory:
+          opt.directory = pargs->r.ret_str;
+          break;
         case oFrom:
           opt.default_from = pargs->r.ret_str;
           break;
@@ -198,9 +240,20 @@ parse_arguments (ARGPARSE_ARGS *pargs, ARGPARSE_OPTS *popts)
         case oOutput:
           opt.output = pargs->r.ret_str;
           break;
+        case oWithDir:
+          opt_with_dir = 1;
+          break;
+        case oWithFile:
+          opt_with_file = 1;
+          break;
 
        case aReceive:
         case aCron:
+        case aListDomains:
+        case aCheck:
+        case aInstallKey:
+        case aRemoveKey:
+        case aRevokeKey:
           cmd = pargs->r_opt;
           break;
 
@@ -217,7 +270,7 @@ parse_arguments (ARGPARSE_ARGS *pargs, ARGPARSE_OPTS *popts)
 int
 main (int argc, char **argv)
 {
-  gpg_error_t err;
+  gpg_error_t err, firsterr;
   ARGPARSE_ARGS pargs;
   enum cmd_and_opt_values cmd;
 
@@ -296,10 +349,11 @@ main (int argc, char **argv)
         log_error ("directory '%s' not owned by user\n", opt.directory);
         exit (2);
       }
-    if ((sb.st_mode & S_IRWXO))
+    if ((sb.st_mode & (S_IROTH|S_IWOTH)))
       {
         log_error ("directory '%s' has too relaxed permissions\n",
                    opt.directory);
+        log_info ("Fix by running: chmod o-rw '%s'\n", opt.directory);
         exit (2);
       }
   }
@@ -314,67 +368,83 @@ main (int argc, char **argv)
       if (argc)
         wrong_args ("--receive");
       err = wks_receive (es_stdin, command_receive_cb, NULL);
-      if (err)
-        log_error ("processing mail failed: %s\n", gpg_strerror (err));
       break;
 
     case aCron:
       if (argc)
         wrong_args ("--cron");
       err = command_cron ();
-      if (err)
-        log_error ("running --cron failed: %s\n", gpg_strerror (err));
+      break;
+
+    case aListDomains:
+      err = command_list_domains ();
+      break;
+
+    case aInstallKey:
+      if (argc != 2)
+        wrong_args ("--install-key FILE USER-ID");
+      err = wks_cmd_install_key (*argv, argv[1]);
+      break;
+
+    case aRemoveKey:
+      if (argc != 1)
+        wrong_args ("--remove-key USER-ID");
+      err = wks_cmd_remove_key (*argv);
+      break;
+
+    case aRevokeKey:
+      if (argc != 1)
+        wrong_args ("--revoke-key USER-ID");
+      err = command_revoke_key (*argv);
+      break;
+
+    case aCheck:
+      if (!argc)
+        wrong_args ("--check USER-IDs");
+      firsterr = 0;
+      for (; argc; argc--, argv++)
+        {
+          err = command_check_key (*argv);
+          if (!firsterr)
+            firsterr = err;
+        }
+      err = firsterr;
       break;
 
     default:
       usage (1);
+      err = gpg_error (GPG_ERR_BUG);
       break;
     }
 
+  if (err)
+    log_error ("command failed: %s\n", gpg_strerror (err));
   return log_get_errorcount (0)? 1:0;
 }
 
 
-\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);
-}
-
-
+/* Take the key in KEYFILE and write it to OUTFILE in binary encoding.
+ * If ADDRSPEC is given only matching user IDs are included in the
+ * output.  */
 static gpg_error_t
-list_key (server_ctx_t ctx, estream_t key)
+copy_key_as_binary (const char *keyfile, const char *outfile,
+                    const char *addrspec)
 {
   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;
+  const char **argv = NULL;
+  char *filterexp = NULL;
 
-  /* Open a memory stream.  */
-  listing = es_fopenmem (0, "w+b");
-  if (!listing)
+  if (addrspec)
     {
-      err = gpg_error_from_syserror ();
-      log_error ("error allocating memory buffer: %s\n", gpg_strerror (err));
-      return err;
+      filterexp = es_bsprintf ("keep-uid=mbox = %s", addrspec);
+      if (!filterexp)
+        {
+          err = gpg_error_from_syserror ();
+          log_error ("error allocating memory buffer: %s\n",
+                     gpg_strerror (err));
+          goto leave;
+        }
     }
 
   ccparray_init (&ccp, 0);
@@ -385,12 +455,20 @@ list_key (server_ctx_t ctx, estream_t key)
   else if (opt.verbose > 1)
     ccparray_put (&ccp, "--verbose");
   ccparray_put (&ccp, "--batch");
-  ccparray_put (&ccp, "--status-fd=2");
+  ccparray_put (&ccp, "--yes");
   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, "--no-keyring");
+  ccparray_put (&ccp, "--output");
+  ccparray_put (&ccp, outfile);
+  ccparray_put (&ccp, "--import-options=import-export");
+  if (filterexp)
+    {
+      ccparray_put (&ccp, "--import-filter");
+      ccparray_put (&ccp, filterexp);
+    }
   ccparray_put (&ccp, "--import");
+  ccparray_put (&ccp, "--");
+  ccparray_put (&ccp, keyfile);
 
   ccparray_put (&ccp, NULL);
   argv = ccparray_get (&ccp, NULL);
@@ -399,100 +477,17 @@ list_key (server_ctx_t ctx, estream_t key)
       err = gpg_error_from_syserror ();
       goto leave;
     }
-  err = gnupg_exec_tool_stream (opt.gpg_program, argv, key,
-                                NULL, listing,
-                                list_key_status_cb, ctx);
+  err = gnupg_exec_tool_stream (opt.gpg_program, argv, NULL,
+                                NULL, NULL, NULL, NULL);
   if (err)
     {
-      log_error ("import failed: %s\n", gpg_strerror (err));
+      log_error ("%s failed: %s\n", __func__, 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 (filterexp);
   xfree (argv);
-  es_fclose (listing);
   return err;
 }
 
@@ -551,8 +546,8 @@ encrypt_stream_status_cb (void *opaque, const char *keyword, char *args)
 {
   (void)opaque;
 
-  if (opt.debug)
-    log_debug ("%s: %s\n", keyword, args);
+  if (DBG_CRYPTO)
+    log_debug ("gpg status: %s %s\n", keyword, args);
 }
 
 
@@ -620,6 +615,78 @@ encrypt_stream (estream_t *r_output, estream_t input, const char *keyfile)
 }
 
 
+static void
+sign_stream_status_cb (void *opaque, const char *keyword, char *args)
+{
+  (void)opaque;
+
+  if (DBG_CRYPTO)
+    log_debug ("gpg status: %s %s\n", keyword, args);
+}
+
+/* Sign the INPUT stream to a new stream which is stored at success at
+ * R_OUTPUT.  A detached signature is created using the key specified
+ * by USERID.  */
+static gpg_error_t
+sign_stream (estream_t *r_output, estream_t input, const char *userid)
+{
+  gpg_error_t err;
+  ccparray_t ccp;
+  const char **argv;
+  estream_t output;
+
+  *r_output = NULL;
+
+  output = es_fopenmem (0, "w+b");
+  if (!output)
+    {
+      err = gpg_error_from_syserror ();
+      log_error ("error allocating memory buffer: %s\n", gpg_strerror (err));
+      return err;
+    }
+
+  ccparray_init (&ccp, 0);
+
+  ccparray_put (&ccp, "--no-options");
+  if (!opt.verbose)
+    ccparray_put (&ccp, "--quiet");
+  else if (opt.verbose > 1)
+    ccparray_put (&ccp, "--verbose");
+  ccparray_put (&ccp, "--batch");
+  ccparray_put (&ccp, "--status-fd=2");
+  ccparray_put (&ccp, "--armor");
+  ccparray_put (&ccp, "--local-user");
+  ccparray_put (&ccp, userid);
+  ccparray_put (&ccp, "--detach-sign");
+  ccparray_put (&ccp, "--");
+
+  ccparray_put (&ccp, NULL);
+  argv = ccparray_get (&ccp, NULL);
+  if (!argv)
+    {
+      err = gpg_error_from_syserror ();
+      goto leave;
+    }
+  err = gnupg_exec_tool_stream (opt.gpg_program, argv, input,
+                                NULL, output,
+                                sign_stream_status_cb, NULL);
+  if (err)
+    {
+      log_error ("signing failed: %s\n", gpg_strerror (err));
+      goto leave;
+    }
+
+  es_rewind (output);
+  *r_output = output;
+  output = NULL;
+
+ leave:
+  es_fclose (output);
+  xfree (argv);
+  return err;
+}
+
+
 /* Get the submission address for address MBOX.  Caller must free the
  * value.  If no address can be found NULL is returned.  */
 static char *
@@ -688,6 +755,50 @@ get_submission_address (const char *mbox)
 }
 
 
+/* Get the policy flags for address MBOX and store them in POLICY.  */
+static gpg_error_t
+get_policy_flags (policy_flags_t policy, const char *mbox)
+{
+  gpg_error_t err;
+  const char *domain;
+  char *fname;
+  estream_t fp;
+
+  memset (policy, 0, sizeof *policy);
+
+  domain = strchr (mbox, '@');
+  if (!domain)
+    return gpg_error (GPG_ERR_INV_USER_ID);
+  domain++;
+
+  fname = make_filename_try (opt.directory, domain, "policy", NULL);
+  if (!fname)
+    {
+      err = gpg_error_from_syserror ();
+      log_error ("make_filename failed in %s: %s\n",
+                 __func__, gpg_strerror (err));
+      return err;
+    }
+
+  fp = es_fopen (fname, "r");
+  if (!fp)
+    {
+      err = gpg_error_from_syserror ();
+      if (gpg_err_code (err) == GPG_ERR_ENOENT)
+        err = 0;
+      else
+        log_error ("error reading '%s': %s\n", fname, gpg_strerror (err));
+      xfree (fname);
+      return err;
+    }
+
+  err = wks_parse_policy (policy, fp, 0);
+  es_fclose (fp);
+  xfree (fname);
+  return err;
+}
+
+
 /* We store the key under the name of the nonce we will then send to
  * the user.  On success the nonce is stored at R_NONCE and the file
  * name at R_FNAME.  */
@@ -713,9 +824,6 @@ store_key_as_pending (const char *dir, estream_t key,
       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
@@ -803,7 +911,7 @@ store_key_as_pending (const char *dir, estream_t key,
 }
 
 
-/* Send a confirmation rewqyest.  DIR is the directory used for the
+/* Send a confirmation request.  DIR is the directory used for the
  * address MBOX.  NONCE is the nonce we want to see in the response to
  * this mail.  FNAME the name of the file with the key.  */
 static gpg_error_t
@@ -814,6 +922,8 @@ send_confirmation_request (server_ctx_t ctx,
   gpg_error_t err;
   estream_t body = NULL;
   estream_t bodyenc = NULL;
+  estream_t signeddata = NULL;
+  estream_t signature = NULL;
   mime_maker_t mime = NULL;
   char *from_buffer = NULL;
   const char *from;
@@ -839,12 +949,16 @@ send_confirmation_request (server_ctx_t ctx,
       log_error ("error allocating memory buffer: %s\n", gpg_strerror (err));
       goto leave;
     }
-  /* It is fine to use 8 bit encoding because that is encrypted and
-   * only our client will see it.  */
-  es_fputs ("Content-Type: application/vnd.gnupg.wks\n"
-            "Content-Transfer-Encoding: 8bit\n"
-            "\n",
-            body);
+
+  if (!ctx->draft_version_2)
+    {
+      /* It is fine to use 8 bit encoding because that is encrypted and
+       * only our client will see it.  */
+      es_fputs ("Content-Type: application/vnd.gnupg.wks\n"
+                "Content-Transfer-Encoding: 8bit\n"
+                "\n",
+                body);
+    }
 
   es_fprintf (body, ("type: confirmation-request\n"
                      "sender: %s\n"
@@ -876,6 +990,18 @@ send_confirmation_request (server_ctx_t ctx,
   err = mime_maker_add_header (mime, "Subject", "Confirm your key publication");
   if (err)
     goto leave;
+
+  err = mime_maker_add_header (mime, "Wks-Draft-Version",
+                               STR2(WKS_DRAFT_VERSION));
+  if (err)
+    goto leave;
+
+  /* Help Enigmail to identify messages.  Note that this is in no way
+   * secured.  */
+  err = mime_maker_add_header (mime, "WKS-Phase", "confirm");
+  if (err)
+    goto leave;
+
   for (sl = opt.extra_headers; sl; sl = sl->next)
     {
       err = mime_maker_add_header (mime, sl->d, NULL);
@@ -883,75 +1009,160 @@ send_confirmation_request (server_ctx_t ctx,
         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;
+  if (!ctx->draft_version_2)
+    {
+      err = mime_maker_add_header (mime, "Content-Type",
+                                   "multipart/encrypted; "
+                                   "protocol=\"application/pgp-encrypted\"");
+      if (err)
+        goto leave;
+      err = mime_maker_add_container (mime);
+      if (err)
+        goto leave;
 
-  err = mime_maker_add_header (mime, "Content-Type",
-                               "application/pgp-encrypted");
-  if (err)
-    goto leave;
-  err = mime_maker_add_body (mime, "Version: 1\n");
-  if (err)
-    goto leave;
-  err = mime_maker_add_header (mime, "Content-Type",
-                               "application/octet-stream");
-  if (err)
-    goto leave;
+      err = mime_maker_add_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_add_stream (mime, &bodyenc);
+      if (err)
+        goto leave;
 
-  err = wks_send_mime (mime);
+    }
+  else
+    {
+      unsigned int partid;
 
- leave:
-  mime_maker_release (mime);
-  es_fclose (bodyenc);
-  es_fclose (body);
-  xfree (from_buffer);
-  return err;
-}
+      /* FIXME: Add micalg.  */
+      err = mime_maker_add_header (mime, "Content-Type",
+                                   "multipart/signed; "
+                                   "protocol=\"application/pgp-signature\"");
+      if (err)
+        goto leave;
+      err = mime_maker_add_container (mime);
+      if (err)
+        goto leave;
 
+      err = mime_maker_add_header (mime, "Content-Type", "multipart/mixed");
+      if (err)
+        goto leave;
 
-/* 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;
+      err = mime_maker_add_container (mime);
+      if (err)
+        goto leave;
+      partid = mime_maker_get_partid (mime);
+
+      err = mime_maker_add_header (mime, "Content-Type", "text/plain");
+      if (err)
+        goto leave;
+
+      err = mime_maker_add_body
+        (mime,
+         "This message has been send to confirm your request\n"
+         "to publish your key.  If you did not request a key\n"
+         "publication, simply ignore this message.\n"
+         "\n"
+         "Most mail software can handle this kind of message\n"
+         "automatically and thus you would not have seen this\n"
+         "message.  It seems that your client does not fully\n"
+         "support this service.  The web page\n"
+         "\n"
+         "       https://gnupg.org/faq/wkd.html\n"
+         "\n"
+         "explains how you can process this message anyway in\n"
+         "a few manual steps.\n");
+      if (err)
+        goto leave;
+
+      err = mime_maker_add_header (mime, "Content-Type",
+                                   "application/vnd.gnupg.wks");
+      if (err)
+        goto leave;
+
+      err = mime_maker_add_stream (mime, &bodyenc);
+      if (err)
+        goto leave;
+
+      err = mime_maker_end_container (mime);
+      if (err)
+        goto leave;
+
+      /* mime_maker_dump_tree (mime); */
+      err = mime_maker_get_part (mime, partid, &signeddata);
+      if (err)
+        goto leave;
+
+      err = sign_stream (&signature, signeddata, from);
+      if (err)
+        goto leave;
+
+      err = mime_maker_add_header (mime, "Content-Type",
+                                   "application/pgp-signature");
+      if (err)
+        goto leave;
+
+      err = mime_maker_add_stream (mime, &signature);
+      if (err)
+        goto leave;
+    }
+
+  err = wks_send_mime (mime);
+
+ leave:
+  mime_maker_release (mime);
+  es_fclose (signature);
+  es_fclose (signeddata);
+  es_fclose (bodyenc);
+  es_fclose (body);
+  xfree (from_buffer);
+  return err;
+}
+
+
+/* Store the key given by KEY into the pending directory and send a
+ * confirmation requests.  */
+static gpg_error_t
+process_new_key (server_ctx_t ctx, estream_t key)
+{
+  gpg_error_t err;
+  uidinfo_list_t sl;
   const char *s;
   char *dname = NULL;
   char *nonce = NULL;
   char *fname = NULL;
+  struct policy_flags_s policybuf;
+
+  memset (&policybuf, 0, sizeof policybuf);
 
   /* First figure out the user id from the key.  */
-  err = list_key (ctx, key);
+  xfree (ctx->fpr);
+  free_uidinfo_list (ctx->mboxes);
+  err = wks_list_key (key, &ctx->fpr, &ctx->mboxes);
   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_assert (ctx->fpr);
   log_info ("fingerprint: %s\n", ctx->fpr);
   for (sl = ctx->mboxes; sl; sl = sl->next)
     {
-      log_info ("  addr-spec: %s\n", sl->d);
+      if (sl->mbox)
+        log_info ("  addr-spec: %s\n", sl->mbox);
     }
 
   /* Walk over all user ids and send confirmation requests for those
    * we support.  */
   for (sl = ctx->mboxes; sl; sl = sl->next)
     {
-      s = strchr (sl->d, '@');
+      if (!sl->mbox)
+        continue;
+      s = strchr (sl->mbox, '@');
       log_assert (s && s[1]);
       xfree (dname);
       dname = make_filename_try (opt.directory, s+1, NULL);
@@ -960,23 +1171,40 @@ process_new_key (server_ctx_t ctx, estream_t key)
           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);
+          log_info ("skipping address '%s': Domain not configured\n", sl->mbox);
+          continue;
+        }
+      if (get_policy_flags (&policybuf, sl->mbox))
+        {
+          log_info ("skipping address '%s': Bad policy flags\n", sl->mbox);
           continue;
         }
-      log_info ("storing address '%s'\n", sl->d);
 
-      xfree (nonce);
-      xfree (fname);
-      err = store_key_as_pending (dname, key, &nonce, &fname);
-      if (err)
-        goto leave;
+      if (policybuf.auth_submit)
+        {
+          /* Bypass the confirmation stuff and publish the key as is.  */
+          log_info ("publishing address '%s'\n", sl->mbox);
+          /* FIXME: We need to make sure that we do this only for the
+           * address in the mail.  */
+          log_debug ("auth-submit not yet working!\n");
+        }
+      else
+        {
+          log_info ("storing address '%s'\n", sl->mbox);
 
-      err = send_confirmation_request (ctx, sl->d, nonce, fname);
-      if (err)
-        goto leave;
+          xfree (nonce);
+          xfree (fname);
+          err = store_key_as_pending (dname, key, &nonce, &fname);
+          if (err)
+            goto leave;
+
+          err = send_confirmation_request (ctx, sl->mbox, nonce, fname);
+          if (err)
+            goto leave;
+        }
     }
 
  leave:
@@ -985,11 +1213,137 @@ process_new_key (server_ctx_t ctx, estream_t key)
   xfree (nonce);
   xfree (fname);
   xfree (dname);
+  wks_free_policy (&policybuf);
   return err;
 }
 
 
 \f
+/* Send a message to tell the user at MBOX that their key has been
+ * published.  FNAME the name of the file with the key.  */
+static gpg_error_t
+send_congratulation_message (const char *mbox, const char *keyfile)
+{
+  gpg_error_t err;
+  estream_t body = NULL;
+  estream_t bodyenc = NULL;
+  mime_maker_t mime = NULL;
+  char *from_buffer = NULL;
+  const char *from;
+  strlist_t sl;
+
+  from = from_buffer = get_submission_address (mbox);
+  if (!from)
+    {
+      from = opt.default_from;
+      if (!from)
+        {
+          log_error ("no sender address found for '%s'\n", mbox);
+          err = gpg_error (GPG_ERR_CONFIGURATION);
+          goto leave;
+        }
+      log_info ("Note: using default sender address '%s'\n", from);
+    }
+
+  body = es_fopenmem (0, "w+b");
+  if (!body)
+    {
+      err = gpg_error_from_syserror ();
+      log_error ("error allocating memory buffer: %s\n", gpg_strerror (err));
+      goto leave;
+    }
+  /* It is fine to use 8 bit encoding because that is encrypted and
+   * only our client will see it.  */
+  es_fputs ("Content-Type: text/plain; charset=utf-8\n"
+            "Content-Transfer-Encoding: 8bit\n"
+            "\n",
+            body);
+
+  es_fprintf (body,
+              "Hello!\n\n"
+              "The key for your address '%s' has been published\n"
+              "and can now be retrieved from the Web Key Directory.\n"
+              "\n"
+              "For more information on this system see:\n"
+              "\n"
+              "  https://gnupg.org/faq/wkd.html\n"
+              "\n"
+              "Best regards\n"
+              "\n"
+              "  Gnu Key Publisher\n\n\n"
+              "-- \n"
+              "The GnuPG Project welcomes donations: %s\n",
+              mbox, "https://gnupg.org/donate");
+
+  es_rewind (body);
+  err = encrypt_stream (&bodyenc, body, keyfile);
+  if (err)
+    goto leave;
+  es_fclose (body);
+  body = NULL;
+
+  err = mime_maker_new (&mime, NULL);
+  if (err)
+    goto leave;
+  err = mime_maker_add_header (mime, "From", from);
+  if (err)
+    goto leave;
+  err = mime_maker_add_header (mime, "To", mbox);
+  if (err)
+    goto leave;
+  err = mime_maker_add_header (mime, "Subject", "Your key has been published");
+  if (err)
+    goto leave;
+  err = mime_maker_add_header (mime, "Wks-Draft-Version",
+                               STR2(WKS_DRAFT_VERSION));
+  if (err)
+    goto leave;
+  err = mime_maker_add_header (mime, "WKS-Phase", "done");
+  if (err)
+    goto leave;
+  for (sl = opt.extra_headers; sl; sl = sl->next)
+    {
+      err = mime_maker_add_header (mime, sl->d, NULL);
+      if (err)
+        goto leave;
+    }
+
+  err = mime_maker_add_header (mime, "Content-Type",
+                               "multipart/encrypted; "
+                               "protocol=\"application/pgp-encrypted\"");
+  if (err)
+    goto leave;
+  err = mime_maker_add_container (mime);
+  if (err)
+    goto leave;
+
+  err = mime_maker_add_header (mime, "Content-Type",
+                               "application/pgp-encrypted");
+  if (err)
+    goto leave;
+  err = mime_maker_add_body (mime, "Version: 1\n");
+  if (err)
+    goto leave;
+  err = mime_maker_add_header (mime, "Content-Type",
+                               "application/octet-stream");
+  if (err)
+    goto leave;
+
+  err = mime_maker_add_stream (mime, &bodyenc);
+  if (err)
+    goto leave;
+
+  err = wks_send_mime (mime);
+
+ leave:
+  mime_maker_release (mime);
+  es_fclose (bodyenc);
+  es_fclose (body);
+  xfree (from_buffer);
+  return err;
+}
+
+
 /* Check that we have send a request with NONCE and publish the key.  */
 static gpg_error_t
 check_and_publish (server_ctx_t ctx, const char *address, const char *nonce)
@@ -1001,7 +1355,7 @@ check_and_publish (server_ctx_t ctx, const char *address, const char *nonce)
   char *hash = NULL;
   const char *domain;
   const char *s;
-  strlist_t sl;
+  uidinfo_list_t sl;
   char shaxbuf[32]; /* Used for SHA-1 and SHA-256 */
 
   /* FIXME: There is a bug in name-value.c which adds white space for
@@ -1038,24 +1392,22 @@ check_and_publish (server_ctx_t ctx, const char *address, const char *nonce)
     }
 
   /* We need to get the fingerprint from the key.  */
-  err = list_key (ctx, key);
+  xfree (ctx->fpr);
+  free_uidinfo_list (ctx->mboxes);
+  err = wks_list_key (key, &ctx->fpr, &ctx->mboxes);
   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_assert (ctx->fpr);
   log_info ("fingerprint: %s\n", ctx->fpr);
   for (sl = ctx->mboxes; sl; sl = sl->next)
-    log_info ("  addr-spec: %s\n", sl->d);
+    if (sl->mbox)
+      log_info ("  addr-spec: %s\n", sl->mbox);
 
   /* Check that the key has 'address' as a user id.  We use
    * case-insensitive matching because the client is expected to
    * return the address verbatim.  */
   for (sl = ctx->mboxes; sl; sl = sl->next)
-    if (!strcmp (sl->d, address))
+    if (sl->mbox && !strcmp (sl->mbox, address))
       break;
   if (!sl)
     {
@@ -1065,49 +1417,28 @@ check_and_publish (server_ctx_t ctx, const char *address, const char *nonce)
       goto leave;
     }
 
-
   /* Hash user ID and create filename.  */
-  s = strchr (address, '@');
-  log_assert (s);
-  gcry_md_hash_buffer (GCRY_MD_SHA1, shaxbuf, address, s - address);
-  hash = zb32_encode (shaxbuf, 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", fnewname);
-    xfree (fnewname);
-  }
-  fnewname = make_filename_try (opt.directory, domain, "hu", hash, NULL);
-  if (!fnewname)
-    {
-      err = gpg_error_from_syserror ();
-      goto leave;
-    }
+  err = wks_compute_hu_fname (&fnewname, address);
+  if (err)
+    goto leave;
 
   /* Publish.  */
-  if (rename (fname, fnewname))
+  err = copy_key_as_binary (fname, fnewname, address);
+  if (err)
     {
       err = gpg_error_from_syserror ();
-      log_error ("renaming '%s' to '%s' failed: %s\n",
+      log_error ("copying '%s' to '%s' failed: %s\n",
                  fname, fnewname, gpg_strerror (err));
       goto leave;
     }
 
-  log_info ("key %s published for '%s'\n", ctx->fpr, address);
+  /* Make sure it is world readable.  */
+  if (gnupg_chmod (fnewname, "-rwxr--r--"))
+    log_error ("can't set permissions of '%s': %s\n",
+               fnewname, gpg_strerror (gpg_err_code_from_syserror()));
 
+  log_info ("key %s published for '%s'\n", ctx->fpr, address);
+  send_congratulation_message (address, fnewname);
 
   /* Try to publish as DANE record if the DANE directory exists.  */
   xfree (fname);
@@ -1144,7 +1475,6 @@ check_and_publish (server_ctx_t ctx, const char *address, const char *nonce)
       log_info ("key %s published for '%s' (DANE record)\n", ctx->fpr, address);
     }
 
-
  leave:
   es_fclose (key);
   xfree (hash);
@@ -1236,15 +1566,18 @@ process_confirmation_response (server_ctx_t ctx, estream_t msg)
 \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)
+command_receive_cb (void *opaque, const char *mediatype,
+                    estream_t msg, unsigned int flags)
 {
   gpg_error_t err;
   struct server_ctx_s ctx;
 
-  memset (&ctx, 0, sizeof ctx);
-
   (void)opaque;
 
+  memset (&ctx, 0, sizeof ctx);
+  if ((flags & WKS_RECEIVE_DRAFT2))
+    ctx.draft_version_2 = 1;
+
   if (!strcmp (mediatype, "application/pgp-keys"))
     err = process_new_key (&ctx, msg);
   else if (!strcmp (mediatype, "application/vnd.gnupg.wks"))
@@ -1256,8 +1589,78 @@ command_receive_cb (void *opaque, const char *mediatype, estream_t msg)
     }
 
   xfree (ctx.fpr);
-  free_strlist (ctx.mboxes);
+  free_uidinfo_list (ctx.mboxes);
+
+  return err;
+}
+
+
+\f
+/* Return a list of all configured domains.  Each list element is the
+ * top directory for the domain.  To figure out the actual domain
+ * name strrchr(name, '/') can be used.  */
+static gpg_error_t
+get_domain_list (strlist_t *r_list)
+{
+  gpg_error_t err;
+  DIR *dir = NULL;
+  char *fname = NULL;
+  struct dirent *dentry;
+  struct stat sb;
+  strlist_t list = NULL;
+
+  *r_list = NULL;
+
+  dir = opendir (opt.directory);
+  if (!dir)
+    {
+      err = gpg_error_from_syserror ();
+      goto leave;
+    }
+
+  while ((dentry = readdir (dir)))
+    {
+      if (*dentry->d_name == '.')
+        continue;
+      if (!strchr (dentry->d_name, '.'))
+        continue; /* No dot - can't be a domain subdir.  */
 
+      xfree (fname);
+      fname = make_filename_try (opt.directory, dentry->d_name, NULL);
+      if (!fname)
+        {
+          err = gpg_error_from_syserror ();
+          log_error ("make_filename failed in %s: %s\n",
+                     __func__, gpg_strerror (err));
+          goto leave;
+        }
+
+      if (stat (fname, &sb))
+        {
+          err = gpg_error_from_syserror ();
+          log_error ("error accessing '%s': %s\n", fname, gpg_strerror (err));
+          continue;
+        }
+      if (!S_ISDIR(sb.st_mode))
+        continue;
+
+      if (!add_to_strlist_try (&list, fname))
+        {
+          err = gpg_error_from_syserror ();
+          log_error ("add_to_strlist failed in %s: %s\n",
+                     __func__, gpg_strerror (err));
+          goto leave;
+        }
+    }
+  err = 0;
+  *r_list = list;
+  list = NULL;
+
+ leave:
+  free_strlist (list);
+  if (dir)
+    closedir (dir);
+  xfree (fname);
   return err;
 }
 
@@ -1352,55 +1755,153 @@ expire_one_domain (const char *top_dirname, const char *domain)
 
 /* Scan spool directories and expire too old pending keys.  */
 static gpg_error_t
-expire_pending_confirmations (void)
+expire_pending_confirmations (strlist_t domaindirs)
 {
+  gpg_error_t err = 0;
+  strlist_t sl;
+  const char *domain;
+
+  for (sl = domaindirs; sl; sl = sl->next)
+    {
+      domain = strrchr (sl->d, '/');
+      log_assert (domain);
+      domain++;
+
+      expire_one_domain (sl->d, domain);
+    }
+
+  return err;
+}
+
+
+/* List all configured domains.  */
+static gpg_error_t
+command_list_domains (void)
+{
+  static struct {
+    const char *name;
+    const char *perm;
+  } requireddirs[] = {
+    { "pending", "-rwx" },
+    { "hu",      "-rwxr-xr-x" }
+  };
+
   gpg_error_t err;
-  DIR *dir = NULL;
+  strlist_t domaindirs;
+  strlist_t sl;
+  const char *domain;
   char *fname = NULL;
-  struct dirent *dentry;
-  struct stat sb;
+  int i;
+  estream_t fp;
 
-  dir = opendir (opt.directory);
-  if (!dir)
+  err = get_domain_list (&domaindirs);
+  if (err)
     {
-      err = gpg_error_from_syserror ();
-      goto leave;
+      log_error ("error reading list of domains: %s\n", gpg_strerror (err));
+      return err;
     }
 
-  while ((dentry = readdir (dir)))
+  for (sl = domaindirs; sl; sl = sl->next)
     {
-      if (*dentry->d_name == '.')
-        continue;
-      if (!strchr (dentry->d_name, '.'))
-        continue; /* No dot - can't be a domain subdir.  */
+      domain = strrchr (sl->d, '/');
+      log_assert (domain);
+      domain++;
+      if (opt_with_dir)
+        es_printf ("%s %s\n", domain, sl->d);
+      else
+        es_printf ("%s\n", domain);
+
 
+      /* Check that the required directories are there.  */
+      for (i=0; i < DIM (requireddirs); i++)
+        {
+          xfree (fname);
+          fname = make_filename_try (sl->d, requireddirs[i].name, NULL);
+          if (!fname)
+            {
+              err = gpg_error_from_syserror ();
+              goto leave;
+            }
+          if (access (fname, W_OK))
+            {
+              err = gpg_error_from_syserror ();
+              if (gpg_err_code (err) == GPG_ERR_ENOENT)
+                {
+                  if (gnupg_mkdir (fname, requireddirs[i].perm))
+                    {
+                      err = gpg_error_from_syserror ();
+                      log_error ("domain %s: error creating subdir '%s': %s\n",
+                                 domain, requireddirs[i].name,
+                                 gpg_strerror (err));
+                    }
+                  else
+                    log_info ("domain %s: subdir '%s' created\n",
+                              domain, requireddirs[i].name);
+                }
+              else if (err)
+                log_error ("domain %s: problem with subdir '%s': %s\n",
+                           domain, requireddirs[i].name, gpg_strerror (err));
+            }
+        }
+
+      /* Print a warning if the submission address is not configured.  */
       xfree (fname);
-      fname = make_filename_try (opt.directory, dentry->d_name, NULL);
+      fname = make_filename_try (sl->d, "submission-address", NULL);
       if (!fname)
         {
           err = gpg_error_from_syserror ();
-          log_error ("make_filename failed in %s: %s\n",
-                     __func__, gpg_strerror (err));
           goto leave;
         }
-
-      if (stat (fname, &sb))
+      if (access (fname, F_OK))
         {
           err = gpg_error_from_syserror ();
-          log_error ("error accessing '%s': %s\n", fname, gpg_strerror (err));
-          continue;
+          if (gpg_err_code (err) == GPG_ERR_ENOENT)
+            log_error ("domain %s: submission address not configured\n",
+                       domain);
+          else
+            log_error ("domain %s: problem with '%s': %s\n",
+                       domain, fname, gpg_strerror (err));
         }
-      if (!S_ISDIR(sb.st_mode))
-        continue;
 
-      expire_one_domain (fname, dentry->d_name);
+      /* Check the syntax of the optional policy file.  */
+      xfree (fname);
+      fname = make_filename_try (sl->d, "policy", NULL);
+      if (!fname)
+        {
+          err = gpg_error_from_syserror ();
+          goto leave;
+        }
+      fp = es_fopen (fname, "r");
+      if (!fp)
+        {
+          err = gpg_error_from_syserror ();
+          if (gpg_err_code (err) == GPG_ERR_ENOENT)
+            {
+              fp = es_fopen (fname, "w");
+              if (!fp)
+                log_error ("domain %s: can't create policy file: %s\n",
+                           domain, gpg_strerror (err));
+              else
+                es_fclose (fp);
+              fp = NULL;
+            }
+          else
+            log_error ("domain %s: error in policy file: %s\n",
+                       domain, gpg_strerror (err));
+        }
+      else
+        {
+          struct policy_flags_s policy;
+          err = wks_parse_policy (&policy, fp, 0);
+          es_fclose (fp);
+          wks_free_policy (&policy);
+        }
     }
   err = 0;
 
  leave:
-  if (dir)
-    closedir (dir);
   xfree (fname);
+  free_strlist (domaindirs);
   return err;
 }
 
@@ -1409,5 +1910,71 @@ expire_pending_confirmations (void)
 static gpg_error_t
 command_cron (void)
 {
-  return expire_pending_confirmations ();
+  gpg_error_t err;
+  strlist_t domaindirs;
+
+  err = get_domain_list (&domaindirs);
+  if (err)
+    {
+      log_error ("error reading list of domains: %s\n", gpg_strerror (err));
+      return err;
+    }
+
+  err = expire_pending_confirmations (domaindirs);
+
+  free_strlist (domaindirs);
+  return err;
+}
+
+
+/* Check whether the key with USER_ID is installed.  */
+static gpg_error_t
+command_check_key (const char *userid)
+{
+  gpg_error_t err;
+  char *addrspec = NULL;
+  char *fname = NULL;
+
+  err = wks_fname_from_userid (userid, &fname, &addrspec);
+  if (err)
+    goto leave;
+
+  if (access (fname, R_OK))
+    {
+      err = gpg_error_from_syserror ();
+      if (opt_with_file)
+        es_printf ("%s n %s\n", addrspec, fname);
+      if (gpg_err_code (err) == GPG_ERR_ENOENT)
+        {
+          if (!opt.quiet)
+            log_info ("key for '%s' is NOT installed\n", addrspec);
+          log_inc_errorcount ();
+          err = 0;
+        }
+      else
+        log_error ("error stating '%s': %s\n", fname, gpg_strerror (err));
+      goto leave;
+    }
+
+  if (opt_with_file)
+    es_printf ("%s i %s\n", addrspec, fname);
+
+  if (opt.verbose)
+    log_info ("key for '%s' is installed\n", addrspec);
+  err = 0;
+
+ leave:
+  xfree (fname);
+  xfree (addrspec);
+  return err;
+}
+
+
+/* Revoke the key with mail address MAILADDR.  */
+static gpg_error_t
+command_revoke_key (const char *mailaddr)
+{
+  /* Remove should be different from removing but we have not yet
+   * defined a suitable way to do this.  */
+  return wks_cmd_remove_key (mailaddr);
 }