aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/Makefile.am38
-rw-r--r--tools/call-dirmngr.c205
-rw-r--r--tools/call-dirmngr.h28
-rw-r--r--tools/gpg-connect-agent.c10
-rw-r--r--tools/gpg-wks-client.c758
-rw-r--r--tools/gpg-wks-server.c1548
-rw-r--r--tools/gpg-wks.h61
-rw-r--r--tools/gpgconf.c96
-rw-r--r--tools/gpgtar-extract.c2
-rw-r--r--tools/gpgtar-list.c2
-rw-r--r--tools/gpgtar.c25
-rw-r--r--tools/mime-maker.c667
-rw-r--r--tools/mime-maker.h43
-rw-r--r--tools/mime-parser.c772
-rw-r--r--tools/mime-parser.h52
-rw-r--r--tools/rfc822parse.h2
-rw-r--r--tools/send-mail.c129
-rw-r--r--tools/send-mail.h27
-rw-r--r--tools/wks-receive.c464
-rw-r--r--tools/wks-util.c65
20 files changed, 4950 insertions, 44 deletions
diff --git a/tools/Makefile.am b/tools/Makefile.am
index d43ede8d1..7bc14568a 100644
--- a/tools/Makefile.am
+++ b/tools/Makefile.am
@@ -51,9 +51,17 @@ else
gpgtar =
endif
+if BUILD_WKS_TOOLS
+ gpg_wks_server = gpg-wks-server
+ gpg_wks_client = gpg-wks-client
+else
+ gpg_wks_server =
+ gpg_wks_client =
+endif
+
bin_PROGRAMS = gpgconf gpg-connect-agent ${symcryptrun}
if !HAVE_W32_SYSTEM
-bin_PROGRAMS += watchgnupg gpgparsemail
+bin_PROGRAMS += watchgnupg gpgparsemail ${gpg_wks_server} ${gpg_wks_client}
endif
if !HAVE_W32CE_SYSTEM
bin_PROGRAMS += ${gpgtar}
@@ -136,6 +144,34 @@ 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-util.c \
+ wks-receive.c \
+ rfc822parse.c rfc822parse.h \
+ mime-parser.c mime-parser.h \
+ mime-maker.c mime-maker.h \
+ send-mail.c send-mail.h
+
+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-util.c \
+ wks-receive.c \
+ rfc822parse.c rfc822parse.h \
+ mime-parser.c mime-parser.h \
+ mime-maker.h mime-maker.c \
+ send-mail.c send-mail.h \
+ call-dirmngr.c call-dirmngr.h
+
+gpg_wks_client_CFLAGS = $(LIBASSUAN_CFLAGS) $(GPG_ERROR_CFLAGS)
+gpg_wks_client_LDADD = $(libcommon) \
+ $(LIBASSUAN_LIBS) $(LIBGCRYPT_LIBS) $(GPG_ERROR_LIBS)
+
# Make sure that all libs are build before we use them. This is
# important for things like make -j2.
diff --git a/tools/call-dirmngr.c b/tools/call-dirmngr.c
new file mode 100644
index 000000000..0e591dd6d
--- /dev/null
+++ b/tools/call-dirmngr.c
@@ -0,0 +1,205 @@
+/* call-dirmngr.c - Interact with the Dirmngr.
+ * Copyright (C) 2016 g10 Code GmbH
+ *
+ * This file is part of GnuPG.
+ *
+ * GnuPG is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * GnuPG is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <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;
+}
+
+
+
+
+/* Parameter structure used with the WKD_GET command. */
+struct wkd_get_parm_s
+{
+ estream_t memfp;
+};
+
+
+/* Data callback for the WKD_GET command. */
+static gpg_error_t
+wkd_get_data_cb (void *opaque, const void *data, size_t datalen)
+{
+ struct wkd_get_parm_s *parm = opaque;
+ gpg_error_t err = 0;
+ size_t nwritten;
+
+ if (!data)
+ return 0; /* Ignore END commands. */
+ if (!parm->memfp)
+ return 0; /* Data is not required. */
+
+ if (es_write (parm->memfp, data, datalen, &nwritten))
+ err = gpg_error_from_syserror ();
+
+ return err;
+}
+
+
+/* Status callback for the WKD_GET command. */
+static gpg_error_t
+wkd_get_status_cb (void *opaque, const char *line)
+{
+ struct wkd_get_parm_s *parm = opaque;
+ gpg_error_t err = 0;
+
+ (void)line;
+ (void)parm;
+
+ return err;
+}
+
+
+/* Ask the dirmngr for the submission address of a WKD server for the
+ * mail address ADDRSPEC. On success the submission address is stored
+ * at R_ADDRSPEC. */
+gpg_error_t
+wkd_get_submission_address (const char *addrspec, char **r_addrspec)
+{
+ gpg_error_t err;
+ assuan_context_t ctx;
+ struct wkd_get_parm_s parm;
+ char *line = NULL;
+ void *vp;
+ char *buffer = NULL;
+ char *p;
+
+ memset (&parm, 0, sizeof parm);
+ *r_addrspec = NULL;
+
+ err = connect_dirmngr (&ctx);
+ if (err)
+ return err;
+
+ line = es_bsprintf ("WKD_GET --submission-address -- %s", addrspec);
+ if (!line)
+ {
+ err = gpg_error_from_syserror ();
+ goto leave;
+ }
+ if (strlen (line) + 2 >= ASSUAN_LINELENGTH)
+ {
+ err = gpg_error (GPG_ERR_TOO_LARGE);
+ goto leave;
+ }
+
+ parm.memfp = es_fopenmem (0, "rwb");
+ if (!parm.memfp)
+ {
+ err = gpg_error_from_syserror ();
+ goto leave;
+ }
+ err = assuan_transact (ctx, line, wkd_get_data_cb, &parm,
+ NULL, NULL, wkd_get_status_cb, &parm);
+ if (err)
+ goto leave;
+
+ es_fputc (0, parm.memfp);
+ if (es_fclose_snatch (parm.memfp, &vp, NULL))
+ {
+ err = gpg_error_from_syserror ();
+ goto leave;
+ }
+ buffer = vp;
+ parm.memfp = NULL;
+ p = strchr (buffer, '\n');
+ if (p)
+ *p = 0;
+ trim_spaces (buffer);
+ if (!is_valid_mailbox (buffer))
+ {
+ err = gpg_error (GPG_ERR_INV_USER_ID);
+ goto leave;
+ }
+ *r_addrspec = xtrystrdup (buffer);
+ if (!*r_addrspec)
+ err = gpg_error_from_syserror ();
+
+ leave:
+ es_free (buffer);
+ es_fclose (parm.memfp);
+ xfree (line);
+ assuan_release (ctx);
+ return err;
+}
diff --git a/tools/call-dirmngr.h b/tools/call-dirmngr.h
new file mode 100644
index 000000000..f1bc3686b
--- /dev/null
+++ b/tools/call-dirmngr.h
@@ -0,0 +1,28 @@
+/* call-dirmngr.h - Interact with the Dirmngr.
+ * Copyright (C) 2016 g10 Code GmbH
+ *
+ * This file is part of GnuPG.
+ *
+ * GnuPG is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * GnuPG is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <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-connect-agent.c b/tools/gpg-connect-agent.c
index 1cd554f1f..6b5f507ca 100644
--- a/tools/gpg-connect-agent.c
+++ b/tools/gpg-connect-agent.c
@@ -1879,6 +1879,16 @@ main (int argc, char **argv)
if (opt.verbose)
log_info ("closing connection to agent\n");
+ /* XXX: We would like to release the context here, but libassuan
+ nicely says good bye to the server, which results in a SIGPIPE if
+ the server died. Unfortunately, libassuan does not ignore
+ SIGPIPE when used with UNIX sockets, hence we simply leak the
+ context here. */
+ if (0)
+ assuan_release (ctx);
+ else
+ gpgrt_annotate_leaked_object (ctx);
+ xfree (line);
return 0;
}
diff --git a/tools/gpg-wks-client.c b/tools/gpg-wks-client.c
new file mode 100644
index 000000000..2ee23d7cb
--- /dev/null
+++ b/tools/gpg-wks-client.c
@@ -0,0 +1,758 @@
+/* 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 "send-mail.h"
+#include "gpg-wks.h"
+
+
+/* Constants to identify the commands and options. */
+enum cmd_and_opt_values
+ {
+ aNull = 0,
+
+ oQuiet = 'q',
+ oVerbose = 'v',
+ oOutput = 'o',
+
+ oDebug = 500,
+
+ aCreate,
+ aReceive,
+ aRead,
+
+ oGpgProgram,
+ oSend,
+
+ oDummy
+ };
+
+
+/* The list of commands and options. */
+static ARGPARSE_OPTS opts[] = {
+ ARGPARSE_group (300, ("@Commands:\n ")),
+
+ ARGPARSE_c (aCreate, "create",
+ ("create a publication request")),
+ ARGPARSE_c (aReceive, "receive",
+ ("receive a MIME confirmation request")),
+ ARGPARSE_c (aRead, "read",
+ ("receive a plain text confirmation request")),
+
+ ARGPARSE_group (301, ("@\nOptions:\n ")),
+
+ ARGPARSE_s_n (oVerbose, "verbose", ("verbose")),
+ ARGPARSE_s_n (oQuiet, "quiet", ("be somewhat more quiet")),
+ ARGPARSE_s_s (oDebug, "debug", "@"),
+ ARGPARSE_s_s (oGpgProgram, "gpg", "@"),
+ ARGPARSE_s_n (oSend, "send", "send the mail using sendmail"),
+ ARGPARSE_s_s (oOutput, "output", "|FILE|write the mail to FILE"),
+
+
+ ARGPARSE_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 process_confirmation_request (estream_t msg);
+static gpg_error_t command_receive_cb (void *opaque,
+ const char *mediatype, estream_t fp);
+
+
+
+/* Print usage information and and provide strings for help. */
+static const char *
+my_strusage( int level )
+{
+ const char *p;
+
+ switch (level)
+ {
+ case 11: p = "gpg-wks-client (@GNUPG@)";
+ break;
+ case 13: p = VERSION; break;
+ case 17: p = PRINTABLE_OS_NAME; break;
+ case 19: p = ("Please report bugs to <@EMAIL@>.\n"); break;
+
+ case 1:
+ case 40:
+ p = ("Usage: gpg-wks-client [command] [options] [args] (-h for help)");
+ break;
+ case 41:
+ p = ("Syntax: gpg-wks-client [command] [options] [args]\n"
+ "Client for the Web Key Service\n");
+ break;
+
+ default: p = NULL; break;
+ }
+ return p;
+}
+
+
+static void
+wrong_args (const char *text)
+{
+ es_fprintf (es_stderr, _("usage: %s [options] %s\n"), strusage (11), text);
+ exit (2);
+}
+
+
+
+/* Command line parsing. */
+static enum cmd_and_opt_values
+parse_arguments (ARGPARSE_ARGS *pargs, ARGPARSE_OPTS *popts)
+{
+ enum cmd_and_opt_values cmd = 0;
+ int no_more_options = 0;
+
+ while (!no_more_options && optfile_parse (NULL, NULL, NULL, pargs, popts))
+ {
+ switch (pargs->r_opt)
+ {
+ case oQuiet: opt.quiet = 1; break;
+ case oVerbose: opt.verbose++; break;
+ case oDebug:
+ if (parse_debug_flag (pargs->r.ret_str, &opt.debug, debug_flags))
+ {
+ pargs->r_opt = ARGPARSE_INVALID_ARG;
+ pargs->err = ARGPARSE_PRINT_ERROR;
+ }
+ break;
+
+ case oGpgProgram:
+ opt.gpg_program = pargs->r.ret_str;
+ break;
+ case oSend:
+ opt.use_sendmail = 1;
+ break;
+ case oOutput:
+ opt.output = pargs->r.ret_str;
+ break;
+
+ case aCreate:
+ case aReceive:
+ case aRead:
+ cmd = pargs->r_opt;
+ break;
+
+ default: pargs->err = 2; break;
+ }
+ }
+
+ return cmd;
+}
+
+
+
+/* gpg-wks-client main. */
+int
+main (int argc, char **argv)
+{
+ gpg_error_t err;
+ ARGPARSE_ARGS pargs;
+ enum cmd_and_opt_values cmd;
+
+ gnupg_reopen_std ("gpg-wks-client");
+ set_strusage (my_strusage);
+ log_set_prefix ("gpg-wks-client", GPGRT_LOG_WITH_PREFIX);
+
+ /* Make sure that our subsystems are ready. */
+ i18n_init();
+ init_common_subsystems (&argc, &argv);
+
+ assuan_set_gpg_err_source (GPG_ERR_SOURCE_DEFAULT);
+ setup_libassuan_logging (&opt.debug);
+
+ /* Parse the command line. */
+ pargs.argc = &argc;
+ pargs.argv = &argv;
+ pargs.flags = ARGPARSE_FLAG_KEEP;
+ cmd = parse_arguments (&pargs, opts);
+
+ if (log_get_errorcount (0))
+ exit (2);
+
+ /* Print a warning if an argument looks like an option. */
+ if (!opt.quiet && !(pargs.flags & ARGPARSE_FLAG_STOP_SEEN))
+ {
+ int i;
+
+ for (i=0; i < argc; i++)
+ if (argv[i][0] == '-' && argv[i][1] == '-')
+ log_info (("NOTE: '%s' is not considered an option\n"), argv[i]);
+ }
+
+ /* Set defaults for non given options. */
+ if (!opt.gpg_program)
+ opt.gpg_program = gnupg_module_name (GNUPG_MODULE_NAME_GPG);
+
+ /* Tell call-dirmngr what options we want. */
+ set_dirmngr_options (opt.verbose, (opt.debug & DBG_IPC_VALUE), 1);
+
+ /* Run the selected command. */
+ switch (cmd)
+ {
+ case aCreate:
+ if (argc != 2)
+ wrong_args ("--create FINGERPRINT USER-ID");
+ err = command_send (argv[0], argv[1]);
+ if (err)
+ log_error ("creating request failed: %s\n", gpg_strerror (err));
+ break;
+
+ case aReceive:
+ if (argc)
+ wrong_args ("--receive < MIME-DATA");
+ err = wks_receive (es_stdin, command_receive_cb, NULL);
+ if (err)
+ log_error ("processing mail failed: %s\n", gpg_strerror (err));
+ break;
+
+ case aRead:
+ if (argc)
+ wrong_args ("--read < WKS-DATA");
+ err = process_confirmation_request (es_stdin);
+ if (err)
+ log_error ("processing mail failed: %s\n", gpg_strerror (err));
+ break;
+
+ default:
+ usage (1);
+ break;
+ }
+
+ return log_get_errorcount (0)? 1:0;
+}
+
+
+
+struct get_key_status_parm_s
+{
+ const char *fpr;
+ int found;
+ int count;
+};
+
+static void
+get_key_status_cb (void *opaque, const char *keyword, char *args)
+{
+ struct get_key_status_parm_s *parm = opaque;
+
+ /*log_debug ("%s: %s\n", keyword, args);*/
+ if (!strcmp (keyword, "EXPORTED"))
+ {
+ parm->count++;
+ if (!ascii_strcasecmp (args, parm->fpr))
+ parm->found = 1;
+ }
+}
+
+
+/* Get a key by fingerprint from gpg's keyring and make sure that the
+ * mail address ADDRSPEC is included in the key. The key is returned
+ * as a new memory stream at R_KEY.
+ *
+ * Fixme: After we have implemented import and export filters for gpg
+ * this function shall only return a key with just this user id. */
+static gpg_error_t
+get_key (estream_t *r_key, const char *fingerprint, const char *addrspec)
+{
+ gpg_error_t err;
+ ccparray_t ccp;
+ const char **argv = NULL;
+ estream_t key = NULL;
+ struct get_key_status_parm_s parm;
+ char *filterexp = NULL;
+
+ 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));
+ goto leave;
+ }
+
+ filterexp = es_bsprintf ("keep-uid=mbox = %s", addrspec);
+ if (!filterexp)
+ {
+ err = gpg_error_from_syserror ();
+ log_error ("error allocating memory buffer: %s\n", gpg_strerror (err));
+ goto leave;
+ }
+
+ ccparray_init (&ccp, 0);
+
+ ccparray_put (&ccp, "--no-options");
+ if (!opt.verbose)
+ ccparray_put (&ccp, "--quiet");
+ else if (opt.verbose > 1)
+ ccparray_put (&ccp, "--verbose");
+ ccparray_put (&ccp, "--batch");
+ ccparray_put (&ccp, "--status-fd=2");
+ ccparray_put (&ccp, "--always-trust");
+ ccparray_put (&ccp, "--armor");
+ ccparray_put (&ccp, "--export-options=export-minimal");
+ ccparray_put (&ccp, "--export-filter");
+ ccparray_put (&ccp, filterexp);
+ 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);
+ xfree (filterexp);
+ return err;
+}
+
+
+
+/* Locate the key by fingerprint and userid and send a publication
+ * request. */
+static gpg_error_t
+command_send (const char *fingerprint, char *userid)
+{
+ gpg_error_t err;
+ KEYDB_SEARCH_DESC desc;
+ char *addrspec = NULL;
+ estream_t key = NULL;
+ char *submission_to = NULL;
+ mime_maker_t mime = NULL;
+
+ if (classify_user_id (fingerprint, &desc, 1)
+ || !(desc.mode == KEYDB_SEARCH_MODE_FPR
+ || desc.mode == KEYDB_SEARCH_MODE_FPR20))
+ {
+ log_error (_("\"%s\" is not a fingerprint\n"), fingerprint);
+ err = gpg_error (GPG_ERR_INV_NAME);
+ goto leave;
+ }
+ addrspec = mailbox_from_userid (userid);
+ if (!addrspec)
+ {
+ log_error (_("\"%s\" is not a proper mail address\n"), userid);
+ err = gpg_error (GPG_ERR_INV_USER_ID);
+ goto leave;
+ }
+ err = get_key (&key, fingerprint, addrspec);
+ if (err)
+ goto leave;
+
+ /* 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 = wks_send_mime (mime);
+
+ leave:
+ mime_maker_release (mime);
+ xfree (submission_to);
+ es_fclose (key);
+ xfree (addrspec);
+ return err;
+}
+
+
+
+static void
+encrypt_response_status_cb (void *opaque, const char *keyword, char *args)
+{
+ gpg_error_t *failure = opaque;
+ char *fields[2];
+
+ if (opt.debug)
+ log_debug ("%s: %s\n", keyword, args);
+
+ if (!strcmp (keyword, "FAILURE"))
+ {
+ if (split_fields (args, fields, DIM (fields)) >= 2
+ && !strcmp (fields[0], "encrypt"))
+ *failure = strtoul (fields[1], NULL, 10);
+ }
+
+}
+
+
+/* Encrypt the INPUT stream to a new stream which is stored at success
+ * at R_OUTPUT. Encryption is done for ADDRSPEC. We currently
+ * retrieve that key from the WKD, DANE, or from "local". "local" is
+ * last to prefer the latest key version but use a local copy in case
+ * we are working offline. It might be useful for the server to send
+ * the fingerprint of its encryption key - or even the entire key
+ * back. */
+static gpg_error_t
+encrypt_response (estream_t *r_output, estream_t input, const char *addrspec)
+{
+ gpg_error_t err;
+ ccparray_t ccp;
+ const char **argv;
+ estream_t output;
+ gpg_error_t gpg_err = 0;
+
+ *r_output = NULL;
+
+ output = es_fopenmem (0, "w+b");
+ if (!output)
+ {
+ err = gpg_error_from_syserror ();
+ log_error ("error allocating memory buffer: %s\n", gpg_strerror (err));
+ return err;
+ }
+
+ ccparray_init (&ccp, 0);
+
+ ccparray_put (&ccp, "--no-options");
+ if (!opt.verbose)
+ ccparray_put (&ccp, "--quiet");
+ else if (opt.verbose > 1)
+ ccparray_put (&ccp, "--verbose");
+ ccparray_put (&ccp, "--batch");
+ ccparray_put (&ccp, "--status-fd=2");
+ ccparray_put (&ccp, "--always-trust");
+ ccparray_put (&ccp, "--armor");
+ ccparray_put (&ccp, "--auto-key-locate=clear,wkd,dane,local");
+ ccparray_put (&ccp, "--recipient");
+ ccparray_put (&ccp, addrspec);
+ ccparray_put (&ccp, "--encrypt");
+ ccparray_put (&ccp, "--");
+
+ ccparray_put (&ccp, NULL);
+ argv = ccparray_get (&ccp, NULL);
+ if (!argv)
+ {
+ err = gpg_error_from_syserror ();
+ goto leave;
+ }
+ err = gnupg_exec_tool_stream (opt.gpg_program, argv, input,
+ NULL, output,
+ encrypt_response_status_cb, &gpg_err);
+ if (err)
+ {
+ if (gpg_err)
+ err = gpg_err;
+ log_error ("encryption failed: %s\n", gpg_strerror (err));
+ goto leave;
+ }
+
+ es_rewind (output);
+ *r_output = output;
+ output = NULL;
+
+ leave:
+ es_fclose (output);
+ xfree (argv);
+ return err;
+}
+
+
+static gpg_error_t
+send_confirmation_response (const char *sender, const char *address,
+ const char *nonce, int encrypt)
+{
+ gpg_error_t err;
+ estream_t body = NULL;
+ estream_t bodyenc = NULL;
+ mime_maker_t mime = NULL;
+
+ body = es_fopenmem (0, "w+b");
+ if (!body)
+ {
+ err = gpg_error_from_syserror ();
+ log_error ("error allocating memory buffer: %s\n", gpg_strerror (err));
+ return err;
+ }
+
+ /* It is fine to use 8 bit encoding because that is encrypted and
+ * only our client will see it. */
+ if (encrypt)
+ {
+ es_fputs ("Content-Type: application/vnd.gnupg.wks\n"
+ "Content-Transfer-Encoding: 8bit\n"
+ "\n",
+ body);
+ }
+
+ es_fprintf (body, ("type: confirmation-response\n"
+ "sender: %s\n"
+ "address: %s\n"
+ "nonce: %s\n"),
+ sender,
+ address,
+ nonce);
+
+ es_rewind (body);
+ if (encrypt)
+ {
+ err = encrypt_response (&bodyenc, body, sender);
+ 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;
+
+ if (encrypt)
+ {
+ err = mime_maker_add_header (mime, "Content-Type",
+ "multipart/encrypted; "
+ "protocol=\"application/pgp-encrypted\"");
+ if (err)
+ goto leave;
+ err = mime_maker_add_container (mime, "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;
+ }
+ else
+ {
+ err = mime_maker_add_header (mime, "Content-Type",
+ "application/vnd.gnupg.wks");
+ if (err)
+ goto leave;
+ err = mime_maker_add_stream (mime, &body);
+ if (err)
+ goto leave;
+ }
+
+ err = wks_send_mime (mime);
+
+ leave:
+ mime_maker_release (mime);
+ es_fclose (bodyenc);
+ es_fclose (body);
+ return err;
+}
+
+
+/* Reply to a confirmation request. The MSG has already been
+ * decrypted and we only need to send the nonce back. */
+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;
+
+ /* Send the confirmation. If no key was found, try again without
+ * encryption. */
+ err = send_confirmation_response (sender, address, nonce, 1);
+ if (gpg_err_code (err) == GPG_ERR_NO_PUBKEY)
+ {
+ log_info ("no encryption key found - sending response in the clear\n");
+ err = send_confirmation_response (sender, address, nonce, 0);
+ }
+
+ leave:
+ nvc_release (nvc);
+ return err;
+}
+
+
+/* Called from the MIME receiver to process the plain text data in MSG. */
+static gpg_error_t
+command_receive_cb (void *opaque, const char *mediatype, estream_t msg)
+{
+ gpg_error_t err;
+
+ (void)opaque;
+
+ if (!strcmp (mediatype, "application/vnd.gnupg.wks"))
+ err = process_confirmation_request (msg);
+ else
+ {
+ log_info ("ignoring unexpected message of type '%s'\n", mediatype);
+ err = gpg_error (GPG_ERR_UNEXPECTED_MSG);
+ }
+
+ return err;
+}
diff --git a/tools/gpg-wks-server.c b/tools/gpg-wks-server.c
new file mode 100644
index 000000000..f15085f7d
--- /dev/null
+++ b/tools/gpg-wks-server.c
@@ -0,0 +1,1548 @@
+/* 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>
+#include <sys/types.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 "mime-maker.h"
+#include "send-mail.h"
+#include "gpg-wks.h"
+
+
+/* The time we wait for a confirmation response. */
+#define PENDING_TTL (86400 * 3) /* 3 days. */
+
+
+/* Constants to identify the commands and options. */
+enum cmd_and_opt_values
+ {
+ aNull = 0,
+
+ oQuiet = 'q',
+ oVerbose = 'v',
+ oOutput = 'o',
+
+ oDebug = 500,
+
+ aReceive,
+ aCron,
+ aListDomains,
+
+ oGpgProgram,
+ oSend,
+ oFrom,
+ oHeader,
+
+ oDummy
+ };
+
+
+/* The list of commands and options. */
+static ARGPARSE_OPTS opts[] = {
+ ARGPARSE_group (300, ("@Commands:\n ")),
+
+ ARGPARSE_c (aReceive, "receive",
+ ("receive a submission or confirmation")),
+ ARGPARSE_c (aCron, "cron",
+ ("run regular jobs")),
+ ARGPARSE_c (aListDomains, "list-domains",
+ ("list configured domains")),
+
+ ARGPARSE_group (301, ("@\nOptions:\n ")),
+
+ ARGPARSE_s_n (oVerbose, "verbose", ("verbose")),
+ ARGPARSE_s_n (oQuiet, "quiet", ("be somewhat more quiet")),
+ ARGPARSE_s_s (oDebug, "debug", "@"),
+ ARGPARSE_s_s (oGpgProgram, "gpg", "@"),
+ ARGPARSE_s_n (oSend, "send", "send the mail using sendmail"),
+ ARGPARSE_s_s (oOutput, "output", "|FILE|write the mail to FILE"),
+ ARGPARSE_s_s (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_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;
+
+/* 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);
+static gpg_error_t command_list_domains (void);
+static gpg_error_t command_cron (void);
+
+
+
+/* Print usage information and and provide strings for help. */
+static const char *
+my_strusage( int level )
+{
+ const char *p;
+
+ switch (level)
+ {
+ case 11: p = "gpg-wks-server (@GNUPG@)";
+ break;
+ case 13: p = VERSION; break;
+ case 17: p = PRINTABLE_OS_NAME; break;
+ case 19: p = ("Please report bugs to <@EMAIL@>.\n"); break;
+
+ case 1:
+ case 40:
+ p = ("Usage: gpg-wks-server command [options] (-h for help)");
+ break;
+ case 41:
+ p = ("Syntax: gpg-wks-server command [options]\n"
+ "Server for the Web Key Service protocol\n");
+ break;
+
+ default: p = NULL; break;
+ }
+ return p;
+}
+
+
+static void
+wrong_args (const char *text)
+{
+ es_fprintf (es_stderr, "usage: %s [options] %s\n", strusage (11), text);
+ exit (2);
+}
+
+
+
+/* Command line parsing. */
+static enum cmd_and_opt_values
+parse_arguments (ARGPARSE_ARGS *pargs, ARGPARSE_OPTS *popts)
+{
+ enum cmd_and_opt_values cmd = 0;
+ int no_more_options = 0;
+
+ while (!no_more_options && optfile_parse (NULL, NULL, NULL, pargs, popts))
+ {
+ switch (pargs->r_opt)
+ {
+ case oQuiet: opt.quiet = 1; break;
+ case oVerbose: opt.verbose++; break;
+ case oDebug:
+ if (parse_debug_flag (pargs->r.ret_str, &opt.debug, debug_flags))
+ {
+ pargs->r_opt = ARGPARSE_INVALID_ARG;
+ pargs->err = ARGPARSE_PRINT_ERROR;
+ }
+ break;
+
+ case oGpgProgram:
+ opt.gpg_program = pargs->r.ret_str;
+ break;
+ case oFrom:
+ opt.default_from = pargs->r.ret_str;
+ break;
+ case oHeader:
+ append_to_strlist (&opt.extra_headers, pargs->r.ret_str);
+ break;
+ case oSend:
+ opt.use_sendmail = 1;
+ break;
+ case oOutput:
+ opt.output = pargs->r.ret_str;
+ break;
+
+ case aReceive:
+ case aCron:
+ case aListDomains:
+ cmd = pargs->r_opt;
+ break;
+
+ default: pargs->err = 2; break;
+ }
+ }
+
+ return cmd;
+}
+
+
+
+/* gpg-wks-server main. */
+int
+main (int argc, char **argv)
+{
+ gpg_error_t err;
+ ARGPARSE_ARGS pargs;
+ enum cmd_and_opt_values cmd;
+
+ gnupg_reopen_std ("gpg-wks-server");
+ set_strusage (my_strusage);
+ log_set_prefix ("gpg-wks-server", GPGRT_LOG_WITH_PREFIX);
+
+ /* Make sure that our subsystems are ready. */
+ init_common_subsystems (&argc, &argv);
+
+ /* Parse the command line. */
+ pargs.argc = &argc;
+ pargs.argv = &argv;
+ pargs.flags = ARGPARSE_FLAG_KEEP;
+ cmd = parse_arguments (&pargs, opts);
+
+ if (log_get_errorcount (0))
+ exit (2);
+
+ /* Print a warning if an argument looks like an option. */
+ if (!opt.quiet && !(pargs.flags & ARGPARSE_FLAG_STOP_SEEN))
+ {
+ int i;
+
+ for (i=0; i < argc; i++)
+ if (argv[i][0] == '-' && argv[i][1] == '-')
+ log_info (("NOTE: '%s' is not considered an option\n"), argv[i]);
+ }
+
+ /* Set defaults for non given options. */
+ if (!opt.gpg_program)
+ opt.gpg_program = gnupg_module_name (GNUPG_MODULE_NAME_GPG);
+
+ if (!opt.directory)
+ opt.directory = "/var/lib/gnupg/wks";
+
+ /* Check for syntax errors in the --header option to avoid later
+ * error messages with a not easy to find cause */
+ if (opt.extra_headers)
+ {
+ strlist_t sl;
+
+ for (sl = opt.extra_headers; sl; sl = sl->next)
+ {
+ err = mime_maker_add_header (NULL, sl->d, NULL);
+ if (err)
+ log_error ("syntax error in \"--header %s\": %s\n",
+ sl->d, gpg_strerror (err));
+ }
+ }
+
+ if (log_get_errorcount (0))
+ exit (2);
+
+
+ /* Check that we have a working directory. */
+#if defined(HAVE_STAT)
+ {
+ struct stat sb;
+
+ if (stat (opt.directory, &sb))
+ {
+ err = gpg_error_from_syserror ();
+ log_error ("error accessing directory '%s': %s\n",
+ opt.directory, gpg_strerror (err));
+ exit (2);
+ }
+ if (!S_ISDIR(sb.st_mode))
+ {
+ log_error ("error accessing directory '%s': %s\n",
+ opt.directory, "not a directory");
+ exit (2);
+ }
+ if (sb.st_uid != getuid())
+ {
+ log_error ("directory '%s' not owned by user\n", opt.directory);
+ exit (2);
+ }
+ if ((sb.st_mode & S_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);
+ break;
+
+ case aCron:
+ if (argc)
+ wrong_args ("--cron");
+ err = command_cron ();
+ break;
+
+ case aListDomains:
+ err = command_list_domains ();
+ 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;
+}
+
+
+
+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;
+}
+
+
+/* Take the key in KEYFILE and write it to DANEFILE using the DANE
+ * output format. */
+static gpg_error_t
+copy_key_as_dane (const char *keyfile, const char *danefile)
+{
+ gpg_error_t err;
+ ccparray_t ccp;
+ const char **argv;
+
+ ccparray_init (&ccp, 0);
+
+ ccparray_put (&ccp, "--no-options");
+ if (!opt.verbose)
+ ccparray_put (&ccp, "--quiet");
+ else if (opt.verbose > 1)
+ ccparray_put (&ccp, "--verbose");
+ ccparray_put (&ccp, "--batch");
+ ccparray_put (&ccp, "--yes");
+ ccparray_put (&ccp, "--always-trust");
+ ccparray_put (&ccp, "--no-keyring");
+ ccparray_put (&ccp, "--output");
+ ccparray_put (&ccp, danefile);
+ ccparray_put (&ccp, "--export-options=export-dane");
+ ccparray_put (&ccp, "--import-options=import-export");
+ ccparray_put (&ccp, "--import");
+ ccparray_put (&ccp, "--");
+ ccparray_put (&ccp, keyfile);
+
+ ccparray_put (&ccp, NULL);
+ argv = ccparray_get (&ccp, NULL);
+ if (!argv)
+ {
+ err = gpg_error_from_syserror ();
+ goto leave;
+ }
+ err = gnupg_exec_tool_stream (opt.gpg_program, argv, NULL,
+ NULL, NULL, NULL, NULL);
+ if (err)
+ {
+ log_error ("%s failed: %s\n", __func__, gpg_strerror (err));
+ goto leave;
+ }
+
+ leave:
+ xfree (argv);
+ return err;
+}
+
+
+static void
+encrypt_stream_status_cb (void *opaque, const char *keyword, char *args)
+{
+ (void)opaque;
+
+ if (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 in file KEYFIL. */
+static gpg_error_t
+encrypt_stream (estream_t *r_output, estream_t input, const char *keyfile)
+{
+ gpg_error_t err;
+ ccparray_t ccp;
+ const char **argv;
+ estream_t output;
+
+ *r_output = NULL;
+
+ output = es_fopenmem (0, "w+b");
+ if (!output)
+ {
+ err = gpg_error_from_syserror ();
+ log_error ("error allocating memory buffer: %s\n", gpg_strerror (err));
+ return err;
+ }
+
+ ccparray_init (&ccp, 0);
+
+ ccparray_put (&ccp, "--no-options");
+ if (!opt.verbose)
+ ccparray_put (&ccp, "--quiet");
+ else if (opt.verbose > 1)
+ ccparray_put (&ccp, "--verbose");
+ ccparray_put (&ccp, "--batch");
+ ccparray_put (&ccp, "--status-fd=2");
+ ccparray_put (&ccp, "--always-trust");
+ ccparray_put (&ccp, "--no-keyring");
+ ccparray_put (&ccp, "--armor");
+ ccparray_put (&ccp, "--recipient-file");
+ ccparray_put (&ccp, keyfile);
+ ccparray_put (&ccp, "--encrypt");
+ ccparray_put (&ccp, "--");
+
+ ccparray_put (&ccp, NULL);
+ argv = ccparray_get (&ccp, NULL);
+ if (!argv)
+ {
+ err = gpg_error_from_syserror ();
+ goto leave;
+ }
+ err = gnupg_exec_tool_stream (opt.gpg_program, argv, input,
+ NULL, output,
+ encrypt_stream_status_cb, NULL);
+ if (err)
+ {
+ log_error ("encryption failed: %s\n", gpg_strerror (err));
+ goto leave;
+ }
+
+ es_rewind (output);
+ *r_output = output;
+ output = NULL;
+
+ leave:
+ es_fclose (output);
+ xfree (argv);
+ return err;
+}
+
+
+/* Get the submission address for address MBOX. Caller must free the
+ * value. If no address can be found NULL is returned. */
+static char *
+get_submission_address (const char *mbox)
+{
+ gpg_error_t err;
+ const char *domain;
+ char *fname, *line, *p;
+ size_t n;
+ estream_t fp;
+
+ domain = strchr (mbox, '@');
+ if (!domain)
+ return NULL;
+ domain++;
+
+ fname = make_filename_try (opt.directory, domain, "submission-address", NULL);
+ if (!fname)
+ {
+ err = gpg_error_from_syserror ();
+ log_error ("make_filename failed in %s: %s\n",
+ __func__, gpg_strerror (err));
+ return NULL;
+ }
+
+ fp = es_fopen (fname, "r");
+ if (!fp)
+ {
+ err = gpg_error_from_syserror ();
+ if (gpg_err_code (err) == GPG_ERR_ENOENT)
+ log_info ("Note: no specific submission address configured"
+ " for domain '%s'\n", domain);
+ else
+ log_error ("error reading '%s': %s\n", fname, gpg_strerror (err));
+ xfree (fname);
+ return NULL;
+ }
+
+ line = NULL;
+ n = 0;
+ if (es_getline (&line, &n, fp) < 0)
+ {
+ err = gpg_error_from_syserror ();
+ log_error ("error reading '%s': %s\n", fname, gpg_strerror (err));
+ xfree (line);
+ es_fclose (fp);
+ xfree (fname);
+ return NULL;
+ }
+ es_fclose (fp);
+ xfree (fname);
+
+ p = strchr (line, '\n');
+ if (p)
+ *p = 0;
+ trim_spaces (line);
+ if (!is_valid_mailbox (line))
+ {
+ log_error ("invalid submission address for domain '%s' detected\n",
+ domain);
+ xfree (line);
+ return NULL;
+ }
+
+ return line;
+}
+
+
+/* We store the key under the name of the nonce we will then send to
+ * the user. On success the nonce is stored at R_NONCE and the file
+ * name at R_FNAME. */
+static gpg_error_t
+store_key_as_pending (const char *dir, estream_t key,
+ char **r_nonce, char **r_fname)
+{
+ gpg_error_t err;
+ char *dname = NULL;
+ char *fname = NULL;
+ char *nonce = NULL;
+ estream_t outfp = NULL;
+ char buffer[1024];
+ size_t nbytes, nwritten;
+
+ *r_nonce = NULL;
+ *r_fname = NULL;
+
+ dname = make_filename_try (dir, "pending", NULL);
+ if (!dname)
+ {
+ err = gpg_error_from_syserror ();
+ goto leave;
+ }
+
+ /* Create the nonce. We use 20 bytes so that we don't waste a
+ * character in our zBase-32 encoding. Using the gcrypt's nonce
+ * function is faster than using the strong random function; this is
+ * Good Enough for our purpose. */
+ log_assert (sizeof buffer > 20);
+ gcry_create_nonce (buffer, 20);
+ nonce = zb32_encode (buffer, 8 * 20);
+ memset (buffer, 0, 20); /* Not actually needed but it does not harm. */
+ if (!nonce)
+ {
+ err = gpg_error_from_syserror ();
+ goto leave;
+ }
+
+ fname = strconcat (dname, "/", nonce, NULL);
+ if (!fname)
+ {
+ err = gpg_error_from_syserror ();
+ goto leave;
+ }
+
+ /* With 128 bits of random we can expect that no other file exists
+ * under this name. We use "x" to detect internal errors. */
+ outfp = es_fopen (fname, "wbx,mode=-rw");
+ if (!outfp)
+ {
+ err = gpg_error_from_syserror ();
+ log_error ("error creating '%s': %s\n", fname, gpg_strerror (err));
+ goto leave;
+ }
+ es_rewind (key);
+ for (;;)
+ {
+ if (es_read (key, buffer, sizeof buffer, &nbytes))
+ {
+ err = gpg_error_from_syserror ();
+ log_error ("error reading '%s': %s\n",
+ es_fname_get (key), gpg_strerror (err));
+ break;
+ }
+
+ if (!nbytes)
+ {
+ err = 0;
+ goto leave; /* Ready. */
+ }
+ if (es_write (outfp, buffer, nbytes, &nwritten))
+ {
+ err = gpg_error_from_syserror ();
+ log_error ("error writing '%s': %s\n", fname, gpg_strerror (err));
+ goto leave;
+ }
+ else if (nwritten != nbytes)
+ {
+ err = gpg_error (GPG_ERR_EIO);
+ log_error ("error writing '%s': %s\n", fname, "short write");
+ goto leave;
+ }
+ }
+
+ leave:
+ if (err)
+ {
+ es_fclose (outfp);
+ gnupg_remove (fname);
+ }
+ else if (es_fclose (outfp))
+ {
+ err = gpg_error_from_syserror ();
+ log_error ("error closing '%s': %s\n", fname, gpg_strerror (err));
+ }
+
+ if (!err)
+ {
+ *r_nonce = nonce;
+ *r_fname = fname;
+ }
+ else
+ {
+ xfree (nonce);
+ xfree (fname);
+ }
+ xfree (dname);
+ return err;
+}
+
+
+/* Send a confirmation rewqyest. DIR is the directory used for the
+ * address MBOX. NONCE is the nonce we want to see in the response to
+ * this mail. FNAME the name of the file with the key. */
+static gpg_error_t
+send_confirmation_request (server_ctx_t ctx,
+ const char *mbox, const char *nonce,
+ const char *keyfile)
+{
+ gpg_error_t err;
+ estream_t body = NULL;
+ estream_t bodyenc = NULL;
+ 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: application/vnd.gnupg.wks\n"
+ "Content-Transfer-Encoding: 8bit\n"
+ "\n",
+ body);
+
+ es_fprintf (body, ("type: confirmation-request\n"
+ "sender: %s\n"
+ "address: %s\n"
+ "fingerprint: %s\n"
+ "nonce: %s\n"),
+ from,
+ mbox,
+ ctx->fpr,
+ nonce);
+
+ es_rewind (body);
+ err = encrypt_stream (&bodyenc, body, keyfile);
+ if (err)
+ goto leave;
+ es_fclose (body);
+ body = NULL;
+
+
+ err = mime_maker_new (&mime, NULL);
+ if (err)
+ goto leave;
+ err = mime_maker_add_header (mime, "From", from);
+ if (err)
+ goto leave;
+ err = mime_maker_add_header (mime, "To", mbox);
+ if (err)
+ goto leave;
+ err = mime_maker_add_header (mime, "Subject", "Confirm your key publication");
+ if (err)
+ goto leave;
+ 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, "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 = wks_send_mime (mime);
+
+ leave:
+ mime_maker_release (mime);
+ 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;
+ strlist_t sl;
+ const char *s;
+ char *dname = NULL;
+ char *nonce = NULL;
+ char *fname = 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);
+ xfree (fname);
+ err = store_key_as_pending (dname, key, &nonce, &fname);
+ if (err)
+ goto leave;
+
+ err = send_confirmation_request (ctx, sl->d, nonce, fname);
+ if (err)
+ goto leave;
+ }
+
+ leave:
+ if (nonce)
+ wipememory (nonce, strlen (nonce));
+ xfree (nonce);
+ xfree (fname);
+ xfree (dname);
+ return err;
+}
+
+
+
+/* Check that we have send a request with NONCE and publish the key. */
+static gpg_error_t
+check_and_publish (server_ctx_t ctx, const char *address, const char *nonce)
+{
+ gpg_error_t err;
+ char *fname = NULL;
+ char *fnewname = NULL;
+ estream_t key = NULL;
+ char *hash = NULL;
+ const char *domain;
+ const char *s;
+ strlist_t sl;
+ char shaxbuf[32]; /* Used for SHA-1 and SHA-256 */
+
+ /* FIXME: There is a bug in name-value.c which adds white space for
+ * the last pair and thus we strip the nonce here until this has
+ * been fixed. */
+ char *nonce2 = xstrdup (nonce);
+ trim_trailing_spaces (nonce2);
+ nonce = nonce2;
+
+
+ domain = strchr (address, '@');
+ log_assert (domain && domain[1]);
+ domain++;
+ fname = make_filename_try (opt.directory, domain, "pending", nonce, NULL);
+ if (!fname)
+ {
+ err = gpg_error_from_syserror ();
+ goto leave;
+ }
+
+ /* Try to open the file with the key. */
+ key = es_fopen (fname, "rb");
+ if (!key)
+ {
+ err = gpg_error_from_syserror ();
+ if (gpg_err_code (err) == GPG_ERR_ENOENT)
+ {
+ log_info ("no pending request for '%s'\n", address);
+ err = gpg_error (GPG_ERR_NOT_FOUND);
+ }
+ else
+ log_error ("error reading '%s': %s\n", fname, gpg_strerror (err));
+ goto leave;
+ }
+
+ /* We need to get the fingerprint from the key. */
+ 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);
+ 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;
+ }
+
+ 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);
+
+
+ /* Try to publish as DANE record if the DANE directory exists. */
+ xfree (fname);
+ fname = fnewname;
+ fnewname = make_filename_try (opt.directory, domain, "dane", NULL);
+ if (!fnewname)
+ {
+ err = gpg_error_from_syserror ();
+ goto leave;
+ }
+ if (!access (fnewname, W_OK))
+ {
+ /* Yes, we have a dane directory. */
+ s = strchr (address, '@');
+ log_assert (s);
+ gcry_md_hash_buffer (GCRY_MD_SHA256, shaxbuf, address, s - address);
+ xfree (hash);
+ hash = bin2hex (shaxbuf, 28, NULL);
+ if (!hash)
+ {
+ err = gpg_error_from_syserror ();
+ goto leave;
+ }
+ xfree (fnewname);
+ fnewname = make_filename_try (opt.directory, domain, "dane", hash, NULL);
+ if (!fnewname)
+ {
+ err = gpg_error_from_syserror ();
+ goto leave;
+ }
+ err = copy_key_as_dane (fname, fnewname);
+ if (err)
+ goto leave;
+ log_info ("key %s published for '%s' (DANE record)\n", ctx->fpr, address);
+ }
+
+
+ leave:
+ es_fclose (key);
+ xfree (hash);
+ xfree (fnewname);
+ xfree (fname);
+ xfree (nonce2);
+ return err;
+}
+
+
+/* Process a confirmation response in MSG. */
+static gpg_error_t
+process_confirmation_response (server_ctx_t ctx, estream_t msg)
+{
+ gpg_error_t err;
+ nvc_t nvc;
+ nve_t item;
+ const char *value, *sender, *address, *nonce;
+
+ err = nvc_parse (&nvc, NULL, msg);
+ if (err)
+ {
+ log_error ("parsing the WKS message failed: %s\n", gpg_strerror (err));
+ goto leave;
+ }
+
+ if (opt.debug)
+ {
+ log_debug ("response follows:\n");
+ nvc_write (nvc, log_get_stream ());
+ }
+
+ /* Check that this is a confirmation response. */
+ if (!((item = nvc_lookup (nvc, "type:")) && (value = nve_value (item))
+ && !strcmp (value, "confirmation-response")))
+ {
+ if (item && value)
+ log_error ("received unexpected wks message '%s'\n", value);
+ else
+ log_error ("received invalid wks message: %s\n", "'type' missing");
+ err = gpg_error (GPG_ERR_UNEXPECTED_MSG);
+ goto leave;
+ }
+
+ /* Get the sender. */
+ if (!((item = nvc_lookup (nvc, "sender:")) && (value = nve_value (item))
+ && is_valid_mailbox (value)))
+ {
+ log_error ("received invalid wks message: %s\n",
+ "'sender' missing or invalid");
+ err = gpg_error (GPG_ERR_INV_DATA);
+ goto leave;
+ }
+ sender = value;
+ (void)sender;
+ /* FIXME: Do we really need the sender?. */
+
+ /* Get the address. */
+ if (!((item = nvc_lookup (nvc, "address:")) && (value = nve_value (item))
+ && is_valid_mailbox (value)))
+ {
+ log_error ("received invalid wks message: %s\n",
+ "'address' missing or invalid");
+ err = gpg_error (GPG_ERR_INV_DATA);
+ goto leave;
+ }
+ address = value;
+
+ /* Get the nonce. */
+ if (!((item = nvc_lookup (nvc, "nonce:")) && (value = nve_value (item))
+ && strlen (value) > 16))
+ {
+ log_error ("received invalid wks message: %s\n",
+ "'nonce' missing or too short");
+ err = gpg_error (GPG_ERR_INV_DATA);
+ goto leave;
+ }
+ nonce = value;
+
+ err = check_and_publish (ctx, address, nonce);
+
+
+ leave:
+ nvc_release (nvc);
+ return err;
+}
+
+
+
+/* Called from the MIME receiver to process the plain text data in MSG . */
+static gpg_error_t
+command_receive_cb (void *opaque, const char *mediatype, estream_t msg)
+{
+ 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;
+}
+
+
+
+/* Return a list of all configured domains. ECh list element is the
+ * top directory for for the domain. To figure out the actual domain
+ * name strrchr(name, '/') can be used. */
+static gpg_error_t
+get_domain_list (strlist_t *r_list)
+{
+ gpg_error_t err;
+ DIR *dir = NULL;
+ char *fname = NULL;
+ struct dirent *dentry;
+ struct stat sb;
+ strlist_t list = NULL;
+
+ *r_list = NULL;
+
+ dir = opendir (opt.directory);
+ if (!dir)
+ {
+ err = gpg_error_from_syserror ();
+ goto leave;
+ }
+
+ while ((dentry = readdir (dir)))
+ {
+ if (*dentry->d_name == '.')
+ continue;
+ if (!strchr (dentry->d_name, '.'))
+ continue; /* No dot - can't be a domain subdir. */
+
+ xfree (fname);
+ fname = make_filename_try (opt.directory, dentry->d_name, NULL);
+ if (!fname)
+ {
+ err = gpg_error_from_syserror ();
+ log_error ("make_filename failed in %s: %s\n",
+ __func__, gpg_strerror (err));
+ goto leave;
+ }
+
+ if (stat (fname, &sb))
+ {
+ err = gpg_error_from_syserror ();
+ log_error ("error accessing '%s': %s\n", fname, gpg_strerror (err));
+ continue;
+ }
+ if (!S_ISDIR(sb.st_mode))
+ continue;
+
+ if (!add_to_strlist_try (&list, fname))
+ {
+ err = gpg_error_from_syserror ();
+ log_error ("add_to_strlist failed in %s: %s\n",
+ __func__, gpg_strerror (err));
+ goto leave;
+ }
+ }
+ err = 0;
+ *r_list = list;
+ list = NULL;
+
+ leave:
+ free_strlist (list);
+ if (dir)
+ closedir (dir);
+ xfree (fname);
+ return err;
+}
+
+
+
+static gpg_error_t
+expire_one_domain (const char *top_dirname, const char *domain)
+{
+ gpg_error_t err;
+ char *dirname;
+ char *fname = NULL;
+ DIR *dir = NULL;
+ struct dirent *dentry;
+ struct stat sb;
+ time_t now = gnupg_get_time ();
+
+ dirname = make_filename_try (top_dirname, "pending", NULL);
+ if (!dirname)
+ {
+ err = gpg_error_from_syserror ();
+ log_error ("make_filename failed in %s: %s\n",
+ __func__, gpg_strerror (err));
+ goto leave;
+ }
+
+ dir = opendir (dirname);
+ if (!dir)
+ {
+ err = gpg_error_from_syserror ();
+ log_error (("can't access directory '%s': %s\n"),
+ dirname, gpg_strerror (err));
+ goto leave;
+ }
+
+ while ((dentry = readdir (dir)))
+ {
+ if (*dentry->d_name == '.')
+ continue;
+ xfree (fname);
+ fname = make_filename_try (dirname, dentry->d_name, NULL);
+ if (!fname)
+ {
+ err = gpg_error_from_syserror ();
+ log_error ("make_filename failed in %s: %s\n",
+ __func__, gpg_strerror (err));
+ goto leave;
+ }
+ if (strlen (dentry->d_name) != 32)
+ {
+ log_info ("garbage file '%s' ignored\n", fname);
+ continue;
+ }
+ if (stat (fname, &sb))
+ {
+ err = gpg_error_from_syserror ();
+ log_error ("error accessing '%s': %s\n", fname, gpg_strerror (err));
+ continue;
+ }
+ if (S_ISDIR(sb.st_mode))
+ {
+ log_info ("garbage directory '%s' ignored\n", fname);
+ continue;
+ }
+ if (sb.st_mtime + PENDING_TTL < now)
+ {
+ if (opt.verbose)
+ log_info ("domain %s: removing pending key '%s'\n",
+ domain, dentry->d_name);
+ if (remove (fname))
+ {
+ err = gpg_error_from_syserror ();
+ /* In case the file has just been renamed or another
+ * processes is cleaning up, we don't print a diagnostic
+ * for ENOENT. */
+ if (gpg_err_code (err) != GPG_ERR_ENOENT)
+ log_error ("error removing '%s': %s\n",
+ fname, gpg_strerror (err));
+ }
+ }
+ }
+ err = 0;
+
+ leave:
+ if (dir)
+ closedir (dir);
+ xfree (dirname);
+ xfree (fname);
+ return err;
+
+}
+
+
+/* Scan spool directories and expire too old pending keys. */
+static gpg_error_t
+expire_pending_confirmations (strlist_t domaindirs)
+{
+ gpg_error_t err = 0;
+ strlist_t sl;
+ const char *domain;
+
+ for (sl = domaindirs; sl; sl = sl->next)
+ {
+ domain = strrchr (sl->d, '/');
+ log_assert (domain);
+ domain++;
+
+ expire_one_domain (sl->d, domain);
+ }
+
+ return err;
+}
+
+
+/* List all configured domains. */
+static gpg_error_t
+command_list_domains (void)
+{
+ static struct {
+ const char *name;
+ const char *perm;
+ } requireddirs[] = {
+ { "pending", "-rwx" },
+ { "hu", "-rwxr-xr-x" }
+ };
+
+ gpg_error_t err;
+ strlist_t domaindirs;
+ strlist_t sl;
+ const char *domain;
+ char *fname = NULL;
+ int i;
+
+ err = get_domain_list (&domaindirs);
+ if (err)
+ {
+ log_error ("error reading list of domains: %s\n", gpg_strerror (err));
+ return err;
+ }
+
+ for (sl = domaindirs; sl; sl = sl->next)
+ {
+ domain = strrchr (sl->d, '/');
+ log_assert (domain);
+ domain++;
+ 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 sumbission address is not configured. */
+ xfree (fname);
+ fname = make_filename_try (sl->d, "submission-address", NULL);
+ if (!fname)
+ {
+ err = gpg_error_from_syserror ();
+ goto leave;
+ }
+ if (access (fname, F_OK))
+ {
+ err = gpg_error_from_syserror ();
+ if (gpg_err_code (err) == GPG_ERR_ENOENT)
+ log_error ("domain %s: submission address not configured\n",
+ domain);
+ else
+ log_error ("domain %s: problem with '%s': %s\n",
+ domain, fname, gpg_strerror (err));
+ }
+ }
+ err = 0;
+
+ leave:
+ xfree (fname);
+ free_strlist (domaindirs);
+ return err;
+}
+
+
+/* Run regular maintenance jobs. */
+static gpg_error_t
+command_cron (void)
+{
+ gpg_error_t err;
+ strlist_t domaindirs;
+
+ err = get_domain_list (&domaindirs);
+ if (err)
+ {
+ log_error ("error reading list of domains: %s\n", gpg_strerror (err));
+ return err;
+ }
+
+ err = expire_pending_confirmations (domaindirs);
+
+ free_strlist (domaindirs);
+ return err;
+}
diff --git a/tools/gpg-wks.h b/tools/gpg-wks.h
new file mode 100644
index 000000000..be85eecfb
--- /dev/null
+++ b/tools/gpg-wks.h
@@ -0,0 +1,61 @@
+/* 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"
+#include "mime-maker.h"
+
+/* We keep all global options in the structure OPT. */
+struct
+{
+ int verbose;
+ unsigned int debug;
+ int quiet;
+ int use_sendmail;
+ const char *output;
+ const char *gpg_program;
+ const char *directory;
+ const char *default_from;
+ strlist_t extra_headers;
+} 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-util.c --*/
+gpg_error_t wks_send_mime (mime_maker_t mime);
+
+/*-- 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/gpgconf.c b/tools/gpgconf.c
index 2b177e233..ad61511d3 100644
--- a/tools/gpgconf.c
+++ b/tools/gpgconf.c
@@ -147,6 +147,64 @@ get_outfp (estream_t *fp)
}
+static void
+list_dirs (estream_t fp, char **names)
+{
+ static struct {
+ const char *name;
+ const char *(*fnc)(void);
+ const char *extra;
+ int special;
+ } list[] = {
+ { "sysconfdir", gnupg_sysconfdir, NULL },
+ { "bindir", gnupg_bindir, NULL },
+ { "libexecdir", gnupg_libexecdir, NULL },
+ { "libdir", gnupg_libdir, NULL },
+ { "datadir", gnupg_datadir, NULL },
+ { "localedir", gnupg_localedir, NULL },
+ { "dirmngr-socket", dirmngr_user_socket_name, NULL, 1 },
+ { "dirmngr-socket", dirmngr_sys_socket_name, NULL, 2 },
+ { "dirmngr-sys-socket", dirmngr_sys_socket_name, NULL, 1 },
+ { "agent-ssh-socket", gnupg_socketdir, GPG_AGENT_SSH_SOCK_NAME },
+ { "agent-socket", gnupg_socketdir, GPG_AGENT_SOCK_NAME },
+ { "homedir", gnupg_homedir, NULL }
+ };
+ int idx, j;
+ char *tmp;
+ const char *s;
+
+
+ for (idx = 0; idx < DIM (list); idx++)
+ {
+ if (list[idx].special == 1 && dirmngr_user_socket_name ())
+ ;
+ else if (list[idx].special == 2 && !dirmngr_user_socket_name ())
+ ;
+ else if (list[idx].special == 1 || list[idx].special == 2)
+ continue;
+
+ s = list[idx].fnc ();
+ if (list[idx].extra)
+ {
+ tmp = make_filename (s, list[idx].extra, NULL);
+ s = tmp;
+ }
+ else
+ tmp = NULL;
+ if (!names)
+ es_fprintf (fp, "%s:%s\n", list[idx].name, gc_percent_escape (s));
+ else
+ {
+ for (j=0; names[j]; j++)
+ if (!strcmp (names[j], list[idx].name))
+ es_fprintf (fp, "%s\n", s);
+ }
+
+ xfree (tmp);
+ }
+}
+
+
/* gpgconf main. */
int
main (int argc, char **argv)
@@ -357,43 +415,7 @@ main (int argc, char **argv)
case aListDirs:
/* Show the system configuration directories for gpgconf. */
get_outfp (&outfp);
- es_fprintf (outfp, "sysconfdir:%s\n",
- gc_percent_escape (gnupg_sysconfdir ()));
- es_fprintf (outfp, "bindir:%s\n",
- gc_percent_escape (gnupg_bindir ()));
- es_fprintf (outfp, "libexecdir:%s\n",
- gc_percent_escape (gnupg_libexecdir ()));
- es_fprintf (outfp, "libdir:%s\n",
- gc_percent_escape (gnupg_libdir ()));
- es_fprintf (outfp, "datadir:%s\n",
- gc_percent_escape (gnupg_datadir ()));
- es_fprintf (outfp, "localedir:%s\n",
- gc_percent_escape (gnupg_localedir ()));
-
- if (dirmngr_user_socket_name ())
- {
- es_fprintf (outfp, "dirmngr-socket:%s\n",
- gc_percent_escape (dirmngr_user_socket_name ()));
- es_fprintf (outfp, "dirmngr-sys-socket:%s\n",
- gc_percent_escape (dirmngr_sys_socket_name ()));
- }
- else
- {
- es_fprintf (outfp, "dirmngr-socket:%s\n",
- gc_percent_escape (dirmngr_sys_socket_name ()));
- }
-
- {
- char *tmp = make_filename (gnupg_socketdir (),
- GPG_AGENT_SOCK_NAME, NULL);
- es_fprintf (outfp, "agent-socket:%s\n", gc_percent_escape (tmp));
- xfree (tmp);
- }
- {
- char *tmp = xstrdup (gnupg_homedir ());
- es_fprintf (outfp, "homedir:%s\n", gc_percent_escape (tmp));
- xfree (tmp);
- }
+ list_dirs (outfp, argc? argv : NULL);
break;
case aCreateSocketDir:
diff --git a/tools/gpgtar-extract.c b/tools/gpgtar-extract.c
index 866215b2c..cee609c6a 100644
--- a/tools/gpgtar-extract.c
+++ b/tools/gpgtar-extract.c
@@ -282,7 +282,7 @@ gpgtar_extract (const char *filename, int decrypt)
if (filename)
{
if (!strcmp (filename, "-"))
- stream = es_stdout;
+ stream = es_stdin;
else
stream = es_fopen (filename, "rb");
if (!stream)
diff --git a/tools/gpgtar-list.c b/tools/gpgtar-list.c
index 1d59d9c65..cb2e70048 100644
--- a/tools/gpgtar-list.c
+++ b/tools/gpgtar-list.c
@@ -282,7 +282,7 @@ gpgtar_list (const char *filename, int decrypt)
if (filename)
{
if (!strcmp (filename, "-"))
- stream = es_stdout;
+ stream = es_stdin;
else
stream = es_fopen (filename, "rb");
if (!stream)
diff --git a/tools/gpgtar.c b/tools/gpgtar.c
index 416f51446..fcbee5086 100644
--- a/tools/gpgtar.c
+++ b/tools/gpgtar.c
@@ -48,6 +48,8 @@
enum cmd_and_opt_values
{
aNull = 0,
+ aCreate = 600,
+ aExtract,
aEncrypt = 'e',
aDecrypt = 'd',
aSign = 's',
@@ -84,8 +86,10 @@ enum cmd_and_opt_values
static ARGPARSE_OPTS opts[] = {
ARGPARSE_group (300, N_("@Commands:\n ")),
- ARGPARSE_c (aEncrypt, "encrypt", N_("create an archive")),
- ARGPARSE_c (aDecrypt, "decrypt", N_("extract an archive")),
+ ARGPARSE_c (aCreate, "create", N_("create an archive")),
+ ARGPARSE_c (aExtract, "extract", N_("extract an archive")),
+ ARGPARSE_c (aEncrypt, "encrypt", N_("create an encrypted archive")),
+ ARGPARSE_c (aDecrypt, "decrypt", N_("extract an encrypted archive")),
ARGPARSE_c (aSign, "sign", N_("create a signed archive")),
ARGPARSE_c (aList, "list-archive", N_("list an archive")),
@@ -275,7 +279,12 @@ shell_parse_argv (const char *s, int *r_argc, char ***r_argv)
return 1;
for (i = 0; list; i++)
- (*r_argv)[i] = list->d, list = list->next;
+ {
+ gpgrt_annotate_leaked_object (list);
+ (*r_argv)[i] = list->d;
+ list = list->next;
+ }
+ gpgrt_annotate_leaked_object (*r_argv);
return 0;
}
@@ -312,6 +321,16 @@ parse_arguments (ARGPARSE_ARGS *pargs, ARGPARSE_OPTS *popts)
set_cmd (&cmd, pargs->r_opt);
break;
+ case aCreate:
+ set_cmd (&cmd, aEncrypt);
+ skip_crypto = 1;
+ break;
+
+ case aExtract:
+ set_cmd (&cmd, aDecrypt);
+ skip_crypto = 1;
+ break;
+
case oRecipient:
add_to_strlist (&opt.recipients, pargs->r.ret_str);
break;
diff --git a/tools/mime-maker.c b/tools/mime-maker.c
new file mode 100644
index 000000000..fa4204328
--- /dev/null
+++ b/tools/mime-maker.c
@@ -0,0 +1,667 @@
+/* mime-maker.c - Create MIME structures
+ * 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 "zb32.h"
+#include "mime-maker.h"
+
+
+/* All valid charachters in a header name. */
+#define HEADER_NAME_CHARS ("abcdefghijklmnopqrstuvwxyz" \
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ" \
+ "-01234567890")
+
+/* An object to store an header. Also used for a list of headers. */
+struct header_s
+{
+ struct header_s *next;
+ char *value; /* Malloced value. */
+ char name[1]; /* Name. */
+};
+typedef struct header_s *header_t;
+
+
+/* An object to store a MIME part. A part is the header plus the
+ * content (body). */
+struct part_s
+{
+ struct part_s *next; /* Next part in the current container. */
+ struct part_s *child; /* Child container. */
+ char *mediatype; /* Mediatype of the container (malloced). */
+ char *boundary; /* Malloced boundary string. */
+ header_t headers; /* List of headers. */
+ header_t *headers_tail;/* Address of last header in chain. */
+ size_t bodylen; /* Length of BODY. */
+ char *body; /* Malloced buffer with the body. This is the
+ * non-encoded value. */
+};
+typedef struct part_s *part_t;
+
+
+
+/* Definition of the mime parser object. */
+struct mime_maker_context_s
+{
+ void *cookie; /* Cookie passed to all callbacks. */
+
+ unsigned int verbose:1; /* Enable verbose mode. */
+ unsigned int debug:1; /* Enable debug mode. */
+
+ part_t mail; /* The MIME tree. */
+ part_t current_part;
+
+ int boundary_counter; /* Used to create easy to read boundaries. */
+ char *boundary_suffix; /* Random string used in the boundaries. */
+
+ struct b64state *b64state; /* NULL or malloced Base64 decoder state. */
+
+ /* Helper to convey the output stream to recursive functions. */
+ estream_t outfp;
+};
+
+
+/* Create a new mime make object. COOKIE is a values woich will be
+ * used as first argument for all callbacks registered with this
+ * object. */
+gpg_error_t
+mime_maker_new (mime_maker_t *r_maker, void *cookie)
+{
+ mime_maker_t ctx;
+
+ *r_maker = NULL;
+
+ ctx = xtrycalloc (1, sizeof *ctx);
+ if (!ctx)
+ return gpg_error_from_syserror ();
+ ctx->cookie = cookie;
+
+ *r_maker = ctx;
+ return 0;
+}
+
+
+static void
+release_parts (part_t part)
+{
+ while (part)
+ {
+ part_t partnext = part->next;
+ while (part->headers)
+ {
+ header_t hdrnext = part->headers->next;
+ xfree (part->headers);
+ part->headers = hdrnext;
+ }
+ release_parts (part->child);
+ xfree (part->mediatype);
+ xfree (part->boundary);
+ xfree (part->body);
+ xfree (part);
+ part = partnext;
+ }
+}
+
+
+/* Release a mime maker object. */
+void
+mime_maker_release (mime_maker_t ctx)
+{
+ if (!ctx)
+ return;
+
+ release_parts (ctx->mail);
+ xfree (ctx->boundary_suffix);
+ xfree (ctx);
+}
+
+
+/* Set verbose and debug mode. */
+void
+mime_maker_set_verbose (mime_maker_t ctx, int level)
+{
+ if (!level)
+ {
+ ctx->verbose = 0;
+ ctx->debug = 0;
+ }
+ else
+ {
+ ctx->verbose = 1;
+ if (level > 10)
+ ctx->debug = 1;
+ }
+}
+
+
+static void
+dump_parts (part_t part, int level)
+{
+ header_t hdr;
+
+ for (; part; part = part->next)
+ {
+ log_debug ("%*s[part]\n", level*2, "");
+ for (hdr = part->headers; hdr; hdr = hdr->next)
+ {
+ log_debug ("%*s%s: %s\n", level*2, "", hdr->name, hdr->value);
+ }
+ log_debug ("%*s[body %zu bytes]\n", level*2, "", part->bodylen);
+ if (part->child)
+ {
+ log_debug ("%*s[container]\n", level*2, "");
+ dump_parts (part->child, level+1);
+ }
+ }
+}
+
+
+/* Dump the mime tree for debugging. */
+void
+mime_maker_dump_tree (mime_maker_t ctx)
+{
+ dump_parts (ctx->mail, 0);
+}
+
+
+/* Find the parent node for NEEDLE starting at ROOT. */
+static part_t
+find_parent (part_t root, part_t needle)
+{
+ part_t node, n;
+
+ for (node = root->child; node; node = node->next)
+ {
+ if (node == needle)
+ return root;
+ if ((n = find_parent (node, needle)))
+ return n;
+ }
+ return NULL;
+}
+
+
+/* Create a boundary string. Outr codes is aware of the general
+ * structure of that string (gebins with "=-=") so that
+ * it can protect against accidently used boundaries within the
+ * content. */
+static char *
+generate_boundary (mime_maker_t ctx)
+{
+ if (!ctx->boundary_suffix)
+ {
+ char buffer[12];
+
+ gcry_create_nonce (buffer, sizeof buffer);
+ ctx->boundary_suffix = zb32_encode (buffer, 8 * sizeof buffer);
+ if (!ctx->boundary_suffix)
+ return NULL;
+ }
+
+ ctx->boundary_counter++;
+ return es_bsprintf ("=-=%02d-%s=-=",
+ ctx->boundary_counter, ctx->boundary_suffix);
+}
+
+
+/* Ensure that the context has a MAIL and CURRENT_PART object and
+ * return the parent object if available */
+static gpg_error_t
+ensure_part (mime_maker_t ctx, part_t *r_parent)
+{
+ if (!ctx->mail)
+ {
+ ctx->mail = xtrycalloc (1, sizeof *ctx->mail);
+ if (!ctx->mail)
+ return gpg_error_from_syserror ();
+ log_assert (!ctx->current_part);
+ ctx->current_part = ctx->mail;
+ ctx->current_part->headers_tail = &ctx->current_part->headers;
+ }
+ log_assert (ctx->current_part);
+ if (r_parent)
+ *r_parent = find_parent (ctx->mail, ctx->current_part);
+
+ return 0;
+}
+
+
+/* Transform a header name into a standard capitalized format.
+ * "Content-Type". Conversion stops at the colon. */
+static void
+capitalize_header_name (char *name)
+{
+ unsigned char *p = name;
+ int first = 1;
+
+ /* Special cases first. */
+ if (!ascii_strcasecmp (name, "MIME-Version"))
+ {
+ strcpy (name, "MIME-Version");
+ return;
+ }
+
+ /* Regular cases. */
+ for (; *p && *p != ':'; p++)
+ {
+ if (*p == '-')
+ first = 1;
+ else if (first)
+ {
+ if (*p >= 'a' && *p <= 'z')
+ *p = *p - 'a' + 'A';
+ first = 0;
+ }
+ else if (*p >= 'A' && *p <= 'Z')
+ *p = *p - 'A' + 'a';
+ }
+}
+
+
+/* Check whether a header with NAME has already been set into PART.
+ * NAME must be in canonical capitalized format. Return true or
+ * false. */
+static int
+have_header (part_t part, const char *name)
+{
+ header_t hdr;
+
+ for (hdr = part->headers; hdr; hdr = hdr->next)
+ if (!strcmp (hdr->name, name))
+ return 1;
+ return 0;
+}
+
+
+/* Helper to add a header to a part. */
+static gpg_error_t
+add_header (part_t part, const char *name, const char *value)
+{
+ gpg_error_t err;
+ header_t hdr;
+ size_t namelen;
+ const char *s;
+
+ if (!value)
+ {
+ s = strchr (name, '=');
+ if (!s)
+ return gpg_error (GPG_ERR_INV_ARG);
+ namelen = s - name;
+ value = s+1;
+ }
+ else
+ namelen = strlen (name);
+
+ hdr = xtrymalloc (sizeof *hdr + namelen);
+ if (!hdr)
+ return gpg_error_from_syserror ();
+ hdr->next = NULL;
+ memcpy (hdr->name, name, namelen);
+ hdr->name[namelen] = 0;
+
+ /* Check that the header name is valid. We allow all lower and
+ * uppercase letters and, except for the first character, digits and
+ * the dash. */
+ if (strspn (hdr->name, HEADER_NAME_CHARS) != namelen
+ || strchr ("-0123456789", *hdr->name))
+ {
+ xfree (hdr);
+ return gpg_error (GPG_ERR_INV_NAME);
+ }
+
+ capitalize_header_name (hdr->name);
+ hdr->value = xtrystrdup (value);
+ if (!hdr->value)
+ {
+ err = gpg_error_from_syserror ();
+ xfree (hdr);
+ return err;
+ }
+
+ if (part)
+ {
+ *part->headers_tail = hdr;
+ part->headers_tail = &hdr->next;
+ }
+ else
+ xfree (hdr);
+
+ return 0;
+}
+
+
+/* Add a header with NAME and VALUE to the current mail. A LF in the
+ * VALUE will be handled automagically. If NULL is used for VALUE it
+ * is expected that the NAME has the format "NAME=VALUE" and VALUE is
+ * taken from there.
+ *
+ * If no container has been added, the header will be used for the
+ * regular mail headers and not for a MIME part. If the current part
+ * is in a container and a body has been added, we append a new part
+ * to the current container. Thus for a non-MIME mail the caller
+ * needs to call this function followed by a call to add a body. When
+ * adding a Content-Type the boundary parameter must not be included.
+ */
+gpg_error_t
+mime_maker_add_header (mime_maker_t ctx, const char *name, const char *value)
+{
+ gpg_error_t err;
+ part_t part, parent;
+
+ /* Hack to use this fucntion for a synacx check of NAME and VALUE. */
+ if (!ctx)
+ return add_header (NULL, name, value);
+
+ err = ensure_part (ctx, &parent);
+ if (err)
+ return err;
+ part = ctx->current_part;
+
+ if (part->body && !parent)
+ {
+ /* We already have a body but no parent. Adding another part is
+ * thus not possible. */
+ return gpg_error (GPG_ERR_CONFLICT);
+ }
+ if (part->body)
+ {
+ /* We already have a body and there is a parent. We now append
+ * a new part to the current container. */
+ part = xtrycalloc (1, sizeof *part);
+ if (!part)
+ return gpg_error_from_syserror ();
+ part->headers_tail = &part->headers;
+ log_assert (!ctx->current_part->next);
+ ctx->current_part->next = part;
+ ctx->current_part = part;
+ }
+
+ /* If no NAME and no VALUE has been given we do not add a header.
+ * This can be used to create a new part without any header. */
+ if (!name && !value)
+ return 0;
+
+ /* If we add Content-Type, make sure that we have a MIME-version
+ * header first; this simply looks better. */
+ if (!ascii_strcasecmp (name, "Content-Type")
+ && !have_header (ctx->mail, "MIME-Version"))
+ {
+ err = add_header (ctx->mail, "MIME-Version", "1.0");
+ if (err)
+ return err;
+ }
+ return add_header (part, name, value);
+}
+
+
+/* Helper for mime_maker_add_{body,stream}. */
+static gpg_error_t
+add_body (mime_maker_t ctx, const void *data, size_t datalen)
+{
+ gpg_error_t err;
+ part_t part, parent;
+
+ err = ensure_part (ctx, &parent);
+ if (err)
+ return err;
+ part = ctx->current_part;
+ if (part->body)
+ return gpg_error (GPG_ERR_CONFLICT);
+
+ part->body = xtrymalloc (datalen? datalen : 1);
+ if (!part->body)
+ return gpg_error_from_syserror ();
+ part->bodylen = datalen;
+ if (data)
+ memcpy (part->body, data, datalen);
+
+ return 0;
+}
+
+
+/* Add STRING as body to the mail or the current MIME container. A
+ * second call to this function is not allowed.
+ *
+ * FIXME: We may want to have an append_body to add more data to a body.
+ */
+gpg_error_t
+mime_maker_add_body (mime_maker_t ctx, const char *string)
+{
+ return add_body (ctx, string, strlen (string));
+}
+
+
+/* This is the same as mime_maker_add_body but takes a stream as
+ * argument. As of now the stream is copied to the MIME object but
+ * eventually we may delay that and read the stream only at the time
+ * it is needed. Note that the address of the stream object must be
+ * passed and that the ownership of the stream is transferred to this
+ * MIME object. To indicate the latter the function will store NULL
+ * at the ADDR_STREAM so that a caller can't use that object anymore
+ * except for es_fclose which accepts a NULL pointer. */
+gpg_error_t
+mime_maker_add_stream (mime_maker_t ctx, estream_t *stream_addr)
+{
+ void *data;
+ size_t datalen;
+
+ es_rewind (*stream_addr);
+ if (es_fclose_snatch (*stream_addr, &data, &datalen))
+ return gpg_error_from_syserror ();
+ *stream_addr = NULL;
+ return add_body (ctx, data, datalen);
+}
+
+
+/* Add a new MIME container. The caller needs to provide the media
+ * and media-subtype in MEDIATYPE. If MEDIATYPE is NULL
+ * "multipart/mixed" is assumed. This function will then add a
+ * Content-Type header with that media type and an approriate boundary
+ * string to the parent part. */
+gpg_error_t
+mime_maker_add_container (mime_maker_t ctx, const char *mediatype)
+{
+ gpg_error_t err;
+ part_t part;
+
+ if (!mediatype)
+ mediatype = "multipart/mixed";
+
+ err = ensure_part (ctx, NULL);
+ if (err)
+ return err;
+ part = ctx->current_part;
+ if (part->body)
+ return gpg_error (GPG_ERR_CONFLICT); /* There is already a body. */
+ if (part->child || part->mediatype || part->boundary)
+ return gpg_error (GPG_ERR_CONFLICT); /* There is already a container. */
+
+ /* If a content type has not yet been set, do it now. The boundary
+ * will be added while writing the headers. */
+ if (!have_header (ctx->mail, "Content-Type"))
+ {
+ err = add_header (ctx->mail, "Content-Type", mediatype);
+ if (err)
+ return err;
+ }
+
+ /* Create a child node. */
+ part->child = xtrycalloc (1, sizeof *part->child);
+ if (!part->child)
+ return gpg_error_from_syserror ();
+ part->child->headers_tail = &part->child->headers;
+
+ part->mediatype = xtrystrdup (mediatype);
+ if (!part->mediatype)
+ {
+ err = gpg_error_from_syserror ();
+ xfree (part->child);
+ part->child = NULL;
+ return err;
+ }
+
+ part->boundary = generate_boundary (ctx);
+ if (!part->boundary)
+ {
+ err = gpg_error_from_syserror ();
+ xfree (part->child);
+ part->child = NULL;
+ xfree (part->mediatype);
+ part->mediatype = NULL;
+ return err;
+ }
+
+ part = part->child;
+ ctx->current_part = part;
+
+ return 0;
+}
+
+
+/* Write the Content-Type header with the boundary value. */
+static gpg_error_t
+write_ct_with_boundary (mime_maker_t ctx,
+ const char *value, const char *boundary)
+{
+ const char *s;
+
+ if (!*value)
+ return gpg_error (GPG_ERR_INV_VALUE); /* Empty string. */
+
+ for (s=value + strlen (value) - 1;
+ (s >= value
+ && (*s == ' ' || *s == '\t' || *s == '\n'));
+ s--)
+ ;
+ if (!(s >= value))
+ return gpg_error (GPG_ERR_INV_VALUE); /* Only spaces. */
+
+ /* Fixme: We should use a dedicated header write functions which
+ * properly wraps the header. */
+ es_fprintf (ctx->outfp, "Content-Type: %s%s\n\tboundary=\"%s\"\n",
+ value,
+ (*s == ';')? "":";",
+ boundary);
+ return 0;
+}
+
+
+/* Recursive worker for mime_maker_make. */
+static gpg_error_t
+write_tree (mime_maker_t ctx, part_t parent, part_t part)
+{
+ gpg_error_t err;
+ header_t hdr;
+
+ for (; part; part = part->next)
+ {
+ for (hdr = part->headers; hdr; hdr = hdr->next)
+ {
+ if (part->child && !strcmp (hdr->name, "Content-Type"))
+ write_ct_with_boundary (ctx, hdr->value, part->boundary);
+ else
+ es_fprintf (ctx->outfp, "%s: %s\n", hdr->name, hdr->value);
+ }
+ es_fputc ('\n', ctx->outfp);
+ if (part->body)
+ {
+ if (es_write (ctx->outfp, part->body, part->bodylen, NULL))
+ return gpg_error_from_syserror ();
+ }
+ if (part->child)
+ {
+ log_assert (part->boundary);
+ if (es_fprintf (ctx->outfp, "\n--%s\n", part->boundary) < 0)
+ return gpg_error_from_syserror ();
+ err = write_tree (ctx, part, part->child);
+ if (err)
+ return err;
+ if (es_fprintf (ctx->outfp, "\n--%s--\n", part->boundary) < 0)
+ return gpg_error_from_syserror ();
+ }
+
+ if (part->next)
+ {
+ log_assert (parent && parent->boundary);
+ if (es_fprintf (ctx->outfp, "\n--%s\n", parent->boundary) < 0)
+ return gpg_error_from_syserror ();
+ }
+ }
+ return 0;
+}
+
+
+/* Add headers we always require. */
+static gpg_error_t
+add_missing_headers (mime_maker_t ctx)
+{
+ gpg_error_t err;
+
+ if (!ctx->mail)
+ return gpg_error (GPG_ERR_NO_DATA);
+ if (!have_header (ctx->mail, "MIME-Version"))
+ {
+ /* Even if a Content-Type has never been set, we want to
+ * announce that we do MIME. */
+ err = add_header (ctx->mail, "MIME-Version", "1.0");
+ if (err)
+ goto leave;
+ }
+
+ if (!have_header (ctx->mail, "Date"))
+ {
+ char *p = rfctimestamp (make_timestamp ());
+ if (!p)
+ err = gpg_error_from_syserror ();
+ else
+ err = add_header (ctx->mail, "Date", p);
+ xfree (p);
+ if (err)
+ goto leave;
+ }
+
+
+ leave:
+ return err;
+}
+
+
+/* Create message from the tree MIME and write it to FP. Noet that
+ * the output uses only a LF and a later called sendmail(1) is
+ * expected to convert them to network line endings. */
+gpg_error_t
+mime_maker_make (mime_maker_t ctx, estream_t fp)
+{
+ gpg_error_t err;
+
+ err = add_missing_headers (ctx);
+ if (err)
+ return err;
+
+ ctx->outfp = fp;
+ err = write_tree (ctx, NULL, ctx->mail);
+
+ ctx->outfp = NULL;
+ return err;
+}
diff --git a/tools/mime-maker.h b/tools/mime-maker.h
new file mode 100644
index 000000000..b21f7dd3d
--- /dev/null
+++ b/tools/mime-maker.h
@@ -0,0 +1,43 @@
+/* mime-maker.h - Create MIME structures
+ * 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_MIME_MAKER_H
+#define GNUPG_MIME_MAKER_H
+
+struct mime_maker_context_s;
+typedef struct mime_maker_context_s *mime_maker_t;
+
+gpg_error_t mime_maker_new (mime_maker_t *r_ctx, void *cookie);
+void mime_maker_release (mime_maker_t ctx);
+
+void mime_maker_set_verbose (mime_maker_t ctx, int level);
+
+void mime_maker_dump_tree (mime_maker_t ctx);
+
+gpg_error_t mime_maker_add_header (mime_maker_t ctx,
+ const char *name, const char *value);
+gpg_error_t mime_maker_add_body (mime_maker_t ctx, const char *string);
+gpg_error_t mime_maker_add_stream (mime_maker_t ctx, estream_t *stream_addr);
+gpg_error_t mime_maker_add_container (mime_maker_t ctx, const char *mediatype);
+
+gpg_error_t mime_maker_make (mime_maker_t ctx, estream_t fp);
+
+
+
+#endif /*GNUPG_MIME_MAKER_H*/
diff --git a/tools/mime-parser.c b/tools/mime-parser.c
new file mode 100644
index 000000000..5f3659ee5
--- /dev/null
+++ b/tools/mime-parser.c
@@ -0,0 +1,772 @@
+/* mime-parser.c - Parse MIME structures (high level rfc822 parser).
+ * 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 "rfc822parse.h"
+#include "mime-parser.h"
+
+
+enum pgpmime_states
+ {
+ PGPMIME_NONE = 0,
+ PGPMIME_WAIT_ENCVERSION,
+ PGPMIME_IN_ENCVERSION,
+ PGPMIME_WAIT_ENCDATA,
+ PGPMIME_IN_ENCDATA,
+ PGPMIME_GOT_ENCDATA,
+ PGPMIME_WAIT_SIGNEDDATA,
+ PGPMIME_IN_SIGNEDDATA,
+ PGPMIME_WAIT_SIGNATURE,
+ PGPMIME_IN_SIGNATURE,
+ PGPMIME_GOT_SIGNATURE,
+ PGPMIME_INVALID
+ };
+
+
+/* Definition of the mime parser object. */
+struct mime_parser_context_s
+{
+ void *cookie; /* Cookie passed to all callbacks. */
+
+ /* The callback to announce a new part. */
+ gpg_error_t (*new_part) (void *cookie,
+ const char *mediatype,
+ const char *mediasubtype);
+ /* The callback to return data of a part. */
+ gpg_error_t (*part_data) (void *cookie,
+ const void *data,
+ size_t datalen);
+ /* The callback to collect encrypted data. */
+ gpg_error_t (*collect_encrypted) (void *cookie, const char *data);
+ /* The callback to collect signed data. */
+ gpg_error_t (*collect_signeddata) (void *cookie, const char *data);
+ /* The callback to collect a signature. */
+ gpg_error_t (*collect_signature) (void *cookie, const char *data);
+
+ /* Helper to convey error codes from user callbacks. */
+ gpg_error_t err;
+
+ int nesting_level; /* The current nesting level. */
+ int hashing_at_level; /* The nesting level at which we are hashing. */
+ enum pgpmime_states pgpmime; /* Current PGP/MIME state. */
+ unsigned int delay_hashing:1;/* Helper for PGPMIME_IN_SIGNEDDATA. */
+ unsigned int want_part:1; /* Return the current part. */
+ unsigned int decode_part:2; /* Decode the part. 1 = QP, 2 = Base64. */
+
+ unsigned int verbose:1; /* Enable verbose mode. */
+ unsigned int debug:1; /* Enable debug mode. */
+
+ /* Flags to help with debug output. */
+ struct {
+ unsigned int n_skip; /* Skip showing these number of lines. */
+ unsigned int header:1; /* Show the header lines. */
+ unsigned int data:1; /* Show the data lines. */
+ unsigned int as_note:1; /* Show the next data line as a note. */
+ unsigned int boundary : 1;
+ } show;
+
+ struct b64state *b64state; /* NULL or malloced Base64 decoder state. */
+
+ /* A buffer for reading a mail line, */
+ char line[5000];
+};
+
+
+/* Print the event received by the parser for debugging. */
+static void
+show_message_parser_event (rfc822parse_event_t event)
+{
+ const char *s;
+
+ switch (event)
+ {
+ case RFC822PARSE_OPEN: s= "Open"; break;
+ case RFC822PARSE_CLOSE: s= "Close"; break;
+ case RFC822PARSE_CANCEL: s= "Cancel"; break;
+ case RFC822PARSE_T2BODY: s= "T2Body"; break;
+ case RFC822PARSE_FINISH: s= "Finish"; break;
+ case RFC822PARSE_RCVD_SEEN: s= "Rcvd_Seen"; break;
+ case RFC822PARSE_LEVEL_DOWN: s= "Level_Down"; break;
+ case RFC822PARSE_LEVEL_UP: s= "Level_Up"; break;
+ case RFC822PARSE_BOUNDARY: s= "Boundary"; break;
+ case RFC822PARSE_LAST_BOUNDARY: s= "Last_Boundary"; break;
+ case RFC822PARSE_BEGIN_HEADER: s= "Begin_Header"; break;
+ case RFC822PARSE_PREAMBLE: s= "Preamble"; break;
+ case RFC822PARSE_EPILOGUE: s= "Epilogue"; break;
+ default: s= "[unknown event]"; break;
+ }
+ log_debug ("*** RFC822 event %s\n", s);
+}
+
+
+/* Do in-place decoding of quoted-printable data of LENGTH in BUFFER.
+ Returns the new length of the buffer and stores true at R_SLBRK if
+ the line ended with a soft line break; false is stored if not.
+ This fucntion asssumes that a complete line is passed in
+ buffer. */
+static size_t
+qp_decode (char *buffer, size_t length, int *r_slbrk)
+{
+ char *d, *s;
+
+ if (r_slbrk)
+ *r_slbrk = 0;
+
+ /* Fixme: We should remove trailing white space first. */
+ for (s=d=buffer; length; length--)
+ {
+ if (*s == '=')
+ {
+ if (length > 2 && hexdigitp (s+1) && hexdigitp (s+2))
+ {
+ s++;
+ *(unsigned char*)d++ = xtoi_2 (s);
+ s += 2;
+ length -= 2;
+ }
+ else if (length > 2 && s[1] == '\r' && s[2] == '\n')
+ {
+ /* Soft line break. */
+ s += 3;
+ length -= 2;
+ if (r_slbrk && length == 1)
+ *r_slbrk = 1;
+ }
+ else if (length > 1 && s[1] == '\n')
+ {
+ /* Soft line break with only a Unix line terminator. */
+ s += 2;
+ length -= 1;
+ if (r_slbrk && length == 1)
+ *r_slbrk = 1;
+ }
+ else if (length == 1)
+ {
+ /* Soft line break at the end of the line. */
+ s += 1;
+ if (r_slbrk)
+ *r_slbrk = 1;
+ }
+ else
+ *d++ = *s++;
+ }
+ else
+ *d++ = *s++;
+ }
+
+ return d - buffer;
+}
+
+
+/* This function is called by parse_mail to communicate events. This
+ * callback communicates with the caller using a structure passed in
+ * OPAQUE. Should return 0 on success or set ERRNO and return -1. */
+static int
+parse_message_cb (void *opaque, rfc822parse_event_t event, rfc822parse_t msg)
+{
+ mime_parser_t ctx = opaque;
+ const char *s;
+ int rc = 0;
+
+ if (ctx->debug)
+ show_message_parser_event (event);
+
+ if (event == RFC822PARSE_BEGIN_HEADER || event == RFC822PARSE_T2BODY)
+ {
+ /* We need to check here whether to start collecting signed data
+ * because attachments might come without header lines and thus
+ * we won't see the BEGIN_HEADER event. */
+ if (ctx->pgpmime == PGPMIME_WAIT_SIGNEDDATA)
+ {
+ if (ctx->debug)
+ log_debug ("begin_hash\n");
+ ctx->hashing_at_level = ctx->nesting_level;
+ ctx->pgpmime = PGPMIME_IN_SIGNEDDATA;
+ ctx->delay_hashing = 0;
+ }
+ }
+
+ if (event == RFC822PARSE_OPEN)
+ {
+ /* Initialize for a new message. */
+ ctx->show.header = 1;
+ }
+ else if (event == RFC822PARSE_T2BODY)
+ {
+ rfc822parse_field_t field;
+
+ ctx->want_part = 0;
+ ctx->decode_part = 0;
+ field = rfc822parse_parse_field (msg, "Content-Type", -1);
+ if (field)
+ {
+ const char *s1, *s2;
+
+ s1 = rfc822parse_query_media_type (field, &s2);
+ if (s1)
+ {
+ if (ctx->verbose)
+ log_debug ("h media: %*s%s %s\n",
+ ctx->nesting_level*2, "", s1, s2);
+ if (ctx->pgpmime == PGPMIME_WAIT_ENCVERSION)
+ {
+ if (!strcmp (s1, "application")
+ && !strcmp (s2, "pgp-encrypted"))
+ {
+ if (ctx->debug)
+ log_debug ("c begin_encversion\n");
+ ctx->pgpmime = PGPMIME_IN_ENCVERSION;
+ }
+ else
+ {
+ log_error ("invalid PGP/MIME structure;"
+ " expected '%s', got '%s/%s'\n",
+ "application/pgp-encrypted", s1, s2);
+ ctx->pgpmime = PGPMIME_INVALID;
+ }
+ }
+ else if (ctx->pgpmime == PGPMIME_WAIT_ENCDATA)
+ {
+ if (!strcmp (s1, "application")
+ && !strcmp (s2, "octet-stream"))
+ {
+ if (ctx->debug)
+ log_debug ("c begin_encdata\n");
+ ctx->pgpmime = PGPMIME_IN_ENCDATA;
+ }
+ else
+ {
+ log_error ("invalid PGP/MIME structure;"
+ " expected '%s', got '%s/%s'\n",
+ "application/octet-stream", s1, s2);
+ ctx->pgpmime = PGPMIME_INVALID;
+ }
+ }
+ else if (ctx->pgpmime == PGPMIME_WAIT_SIGNATURE)
+ {
+ if (!strcmp (s1, "application")
+ && !strcmp (s2, "pgp-signature"))
+ {
+ if (ctx->debug)
+ log_debug ("c begin_signature\n");
+ ctx->pgpmime = PGPMIME_IN_SIGNATURE;
+ }
+ else
+ {
+ log_error ("invalid PGP/MIME structure;"
+ " expected '%s', got '%s/%s'\n",
+ "application/pgp-signature", s1, s2);
+ ctx->pgpmime = PGPMIME_INVALID;
+ }
+ }
+ else if (!strcmp (s1, "multipart")
+ && !strcmp (s2, "encrypted"))
+ {
+ s = rfc822parse_query_parameter (field, "protocol", 0);
+ if (s)
+ {
+ if (ctx->debug)
+ log_debug ("h encrypted.protocol: %s\n", s);
+ if (!strcmp (s, "application/pgp-encrypted"))
+ {
+ if (ctx->pgpmime)
+ log_error ("note: "
+ "ignoring nested PGP/MIME signature\n");
+ else
+ ctx->pgpmime = PGPMIME_WAIT_ENCVERSION;
+ }
+ else if (ctx->verbose)
+ log_debug ("# this protocol is not supported\n");
+ }
+ }
+ else if (!strcmp (s1, "multipart")
+ && !strcmp (s2, "signed"))
+ {
+ s = rfc822parse_query_parameter (field, "protocol", 1);
+ if (s)
+ {
+ if (ctx->debug)
+ log_debug ("h signed.protocol: %s\n", s);
+ if (!strcmp (s, "application/pgp-signature"))
+ {
+ if (ctx->pgpmime)
+ log_error ("note: "
+ "ignoring nested PGP/MIME signature\n");
+ else
+ ctx->pgpmime = PGPMIME_WAIT_SIGNEDDATA;
+ }
+ else if (ctx->verbose)
+ log_debug ("# this protocol is not supported\n");
+ }
+ }
+ else if (ctx->new_part)
+ {
+ ctx->err = ctx->new_part (ctx->cookie, s1, s2);
+ if (!ctx->err)
+ ctx->want_part = 1;
+ else if (gpg_err_code (ctx->err) == GPG_ERR_FALSE)
+ ctx->err = 0;
+ else if (gpg_err_code (ctx->err) == GPG_ERR_TRUE)
+ {
+ ctx->want_part = ctx->decode_part = 1;
+ ctx->err = 0;
+ }
+ }
+ }
+ else
+ {
+ if (ctx->debug)
+ log_debug ("h media: %*s none\n", ctx->nesting_level*2, "");
+ if (ctx->new_part)
+ {
+ ctx->err = ctx->new_part (ctx->cookie, "", "");
+ if (!ctx->err)
+ ctx->want_part = 1;
+ else if (gpg_err_code (ctx->err) == GPG_ERR_FALSE)
+ ctx->err = 0;
+ else if (gpg_err_code (ctx->err) == GPG_ERR_TRUE)
+ {
+ ctx->want_part = ctx->decode_part = 1;
+ ctx->err = 0;
+ }
+ }
+ }
+
+ rfc822parse_release_field (field);
+ }
+ else
+ {
+ if (ctx->verbose)
+ log_debug ("h media: %*stext plain [assumed]\n",
+ ctx->nesting_level*2, "");
+ if (ctx->new_part)
+ {
+ ctx->err = ctx->new_part (ctx->cookie, "text", "plain");
+ if (!ctx->err)
+ ctx->want_part = 1;
+ else if (gpg_err_code (ctx->err) == GPG_ERR_FALSE)
+ ctx->err = 0;
+ else if (gpg_err_code (ctx->err) == GPG_ERR_TRUE)
+ {
+ ctx->want_part = ctx->decode_part = 1;
+ ctx->err = 0;
+ }
+ }
+ }
+
+ /* Figure out the encoding if needed. */
+ if (ctx->decode_part)
+ {
+ char *value;
+ size_t valueoff;
+
+ ctx->decode_part = 0; /* Fallback for unknown encoding. */
+ value = rfc822parse_get_field (msg, "Content-Transfer-Encoding", -1,
+ &valueoff);
+ if (value)
+ {
+ if (!stricmp (value+valueoff, "quoted-printable"))
+ ctx->decode_part = 1;
+ else if (!stricmp (value+valueoff, "base64"))
+ {
+ ctx->decode_part = 2;
+ if (ctx->b64state)
+ b64dec_finish (ctx->b64state); /* Reuse state. */
+ else
+ {
+ ctx->b64state = xtrymalloc (sizeof *ctx->b64state);
+ if (!ctx->b64state)
+ rc = gpg_error_from_syserror ();
+ }
+ if (!rc)
+ rc = b64dec_start (ctx->b64state, NULL);
+ }
+ free (value); /* Right, we need a plain free. */
+ }
+ }
+
+ ctx->show.header = 0;
+ ctx->show.data = 1;
+ ctx->show.n_skip = 1;
+ }
+ else if (event == RFC822PARSE_PREAMBLE)
+ ctx->show.as_note = 1;
+ else if (event == RFC822PARSE_LEVEL_DOWN)
+ {
+ if (ctx->debug)
+ log_debug ("b down\n");
+ ctx->nesting_level++;
+ }
+ else if (event == RFC822PARSE_LEVEL_UP)
+ {
+ if (ctx->debug)
+ log_debug ("b up\n");
+ if (ctx->nesting_level)
+ ctx->nesting_level--;
+ else
+ log_error ("invalid structure (bad nesting level)\n");
+ }
+ else if (event == RFC822PARSE_BOUNDARY || event == RFC822PARSE_LAST_BOUNDARY)
+ {
+ ctx->show.data = 0;
+ ctx->show.boundary = 1;
+ if (event == RFC822PARSE_BOUNDARY)
+ {
+ ctx->show.header = 1;
+ ctx->show.n_skip = 1;
+ if (ctx->debug)
+ log_debug ("b part\n");
+ }
+ else if (ctx->debug)
+ log_debug ("b last\n");
+
+ if (ctx->pgpmime == PGPMIME_IN_ENCDATA)
+ {
+ if (ctx->debug)
+ log_debug ("c end_encdata\n");
+ ctx->pgpmime = PGPMIME_GOT_ENCDATA;
+ /* FIXME: We should assert (event == LAST_BOUNDARY). */
+ }
+ else if (ctx->pgpmime == PGPMIME_IN_SIGNEDDATA
+ && ctx->nesting_level == ctx->hashing_at_level)
+ {
+ if (ctx->debug)
+ log_debug ("c end_hash\n");
+ ctx->pgpmime = PGPMIME_WAIT_SIGNATURE;
+ if (ctx->collect_signeddata)
+ ctx->err = ctx->collect_signeddata (ctx->cookie, NULL);
+ }
+ else if (ctx->pgpmime == PGPMIME_IN_SIGNATURE)
+ {
+ if (ctx->debug)
+ log_debug ("c end_signature\n");
+ ctx->pgpmime = PGPMIME_GOT_SIGNATURE;
+ /* FIXME: We should assert (event == LAST_BOUNDARY). */
+ }
+ else if (ctx->want_part)
+ {
+ if (ctx->part_data)
+ {
+ /* FIXME: We may need to flush things. */
+ ctx->err = ctx->part_data (ctx->cookie, NULL, 0);
+ }
+ ctx->want_part = 0;
+ }
+ }
+
+ return rc;
+}
+
+
+/* Create a new mime parser object. COOKIE is a values which will be
+ * used as first argument for all callbacks registered with this
+ * parser object. */
+gpg_error_t
+mime_parser_new (mime_parser_t *r_parser, void *cookie)
+{
+ mime_parser_t ctx;
+
+ *r_parser = NULL;
+
+ ctx = xtrycalloc (1, sizeof *ctx);
+ if (!ctx)
+ return gpg_error_from_syserror ();
+ ctx->cookie = cookie;
+
+ *r_parser = ctx;
+ return 0;
+}
+
+
+/* Release a mime parser object. */
+void
+mime_parser_release (mime_parser_t ctx)
+{
+ if (!ctx)
+ return;
+
+ if (ctx->b64state)
+ {
+ b64dec_finish (ctx->b64state);
+ xfree (ctx->b64state);
+ }
+ xfree (ctx);
+}
+
+
+/* Set verbose and debug mode. */
+void
+mime_parser_set_verbose (mime_parser_t ctx, int level)
+{
+ if (!level)
+ {
+ ctx->verbose = 0;
+ ctx->debug = 0;
+ }
+ else
+ {
+ ctx->verbose = 1;
+ if (level > 10)
+ ctx->debug = 1;
+ }
+}
+
+
+/* Set the callback used to announce a new part. It will be called
+ * with the media type and media subtype of the part. If no
+ * Content-type header was given both values are the empty string.
+ * The callback should return 0 on success or an error code. The
+ * error code GPG_ERR_FALSE indicates that the caller is not
+ * interested in the part and data shall not be returned via a
+ * registered part_data callback. The error code GPG_ERR_TRUE
+ * indicates that the parts shall be redurned in decoded format
+ * (i.e. base64 or QP encoding is removed). */
+void
+mime_parser_set_new_part (mime_parser_t ctx,
+ gpg_error_t (*fnc) (void *cookie,
+ const char *mediatype,
+ const char *mediasubtype))
+{
+ ctx->new_part = fnc;
+}
+
+
+/* Set the callback used to return the data of a part to the caller.
+ * The end of the part is indicated by passing NUL for DATA. */
+void
+mime_parser_set_part_data (mime_parser_t ctx,
+ gpg_error_t (*fnc) (void *cookie,
+ const void *data,
+ size_t datalen))
+{
+ ctx->part_data = fnc;
+}
+
+
+/* Set the callback to collect encrypted data. A NULL passed to the
+ * callback indicates the end of the encrypted data; the callback may
+ * then decrypt the collected data. */
+void
+mime_parser_set_collect_encrypted (mime_parser_t ctx,
+ gpg_error_t (*fnc) (void *cookie,
+ const char *data))
+{
+ ctx->collect_encrypted = fnc;
+}
+
+
+/* Set the callback to collect signed data. A NULL passed to the
+ * callback indicates the end of the signed data. */
+void
+mime_parser_set_collect_signeddata (mime_parser_t ctx,
+ gpg_error_t (*fnc) (void *cookie,
+ const char *data))
+{
+ ctx->collect_signeddata = fnc;
+}
+
+
+/* Set the callback to collect the signature. A NULL passed to the
+ * callback indicates the end of the signature; the callback may the
+ * verify the signature. */
+void
+mime_parser_set_collect_signature (mime_parser_t ctx,
+ gpg_error_t (*fnc) (void *cookie,
+ const char *data))
+{
+ ctx->collect_signature = fnc;
+}
+
+
+/* Read and parse a message from FP and call the appropriate
+ * callbacks. */
+gpg_error_t
+mime_parser_parse (mime_parser_t ctx, estream_t fp)
+{
+ gpg_error_t err;
+ rfc822parse_t msg = NULL;
+ unsigned int lineno = 0;
+ size_t length, nbytes;
+ char *line;
+
+ line = ctx->line;
+
+ msg = rfc822parse_open (parse_message_cb, ctx);
+ if (!msg)
+ {
+ err = gpg_error_from_syserror ();
+ log_error ("can't open mail parser: %s", gpg_strerror (err));
+ goto leave;
+ }
+
+ /* Fixme: We should not use fgets because it can't cope with
+ embedded nul characters. */
+ while (es_fgets (ctx->line, sizeof (ctx->line), fp))
+ {
+ lineno++;
+ if (lineno == 1 && !strncmp (line, "From ", 5))
+ continue; /* We better ignore a leading From line. */
+
+ length = strlen (line);
+ if (length && line[length - 1] == '\n')
+ line[--length] = 0;
+ else
+ log_error ("mail parser detected too long or"
+ " non terminated last line (lnr=%u)\n", lineno);
+ if (length && line[length - 1] == '\r')
+ line[--length] = 0;
+
+ ctx->err = 0;
+ if (rfc822parse_insert (msg, line, length))
+ {
+ err = gpg_error_from_syserror ();
+ log_error ("mail parser failed: %s", gpg_strerror (err));
+ goto leave;
+ }
+ if (ctx->err)
+ {
+ /* Error from a callback detected. */
+ err = ctx->err;
+ goto leave;
+ }
+
+
+ /* Debug output. Note that the boundary is shown before n_skip
+ * is evaluated. */
+ if (ctx->show.boundary)
+ {
+ if (ctx->debug)
+ log_debug ("# Boundary: %s\n", line);
+ ctx->show.boundary = 0;
+ }
+ if (ctx->show.n_skip)
+ ctx->show.n_skip--;
+ else if (ctx->show.data)
+ {
+ if (ctx->show.as_note)
+ {
+ if (ctx->verbose)
+ log_debug ("# Note: %s\n", line);
+ ctx->show.as_note = 0;
+ }
+ else if (ctx->debug)
+ log_debug ("# Data: %s\n", line);
+ }
+ else if (ctx->show.header && ctx->verbose)
+ log_debug ("# Header: %s\n", line);
+
+ if (ctx->pgpmime == PGPMIME_IN_ENCVERSION)
+ {
+ trim_trailing_spaces (line);
+ if (!*line)
+ ; /* Skip empty lines. */
+ else if (!strcmp (line, "Version: 1"))
+ ctx->pgpmime = PGPMIME_WAIT_ENCDATA;
+ else
+ {
+ log_error ("invalid PGP/MIME structure;"
+ " garbage in pgp-encrypted part ('%s')\n", line);
+ ctx->pgpmime = PGPMIME_INVALID;
+ }
+ }
+ else if (ctx->pgpmime == PGPMIME_IN_ENCDATA)
+ {
+ if (ctx->collect_encrypted)
+ {
+ err = ctx->collect_encrypted (ctx->cookie, line);
+ if (!err)
+ err = ctx->collect_encrypted (ctx->cookie, "\r\n");
+ if (err)
+ goto leave;
+ }
+ }
+ else if (ctx->pgpmime == PGPMIME_GOT_ENCDATA)
+ {
+ ctx->pgpmime = PGPMIME_NONE;
+ if (ctx->collect_encrypted)
+ ctx->collect_encrypted (ctx->cookie, NULL);
+ }
+ else if (ctx->pgpmime == PGPMIME_IN_SIGNEDDATA)
+ {
+ /* If we are processing signed data, store the signed data.
+ * We need to delay the hashing of the CR/LF because the
+ * last line ending belongs to the next boundary. This is
+ * the reason why we can't use the PGPMIME state as a
+ * condition. */
+ if (ctx->debug)
+ log_debug ("# hashing %s'%s'\n",
+ ctx->delay_hashing? "CR,LF+":"", line);
+ if (ctx->collect_signeddata)
+ {
+ if (ctx->delay_hashing)
+ ctx->collect_signeddata (ctx->cookie, "\r\n");
+ ctx->collect_signeddata (ctx->cookie, line);
+ }
+ ctx->delay_hashing = 1;
+ }
+ else if (ctx->pgpmime == PGPMIME_IN_SIGNATURE)
+ {
+ if (ctx->collect_signeddata)
+ {
+ ctx->collect_signature (ctx->cookie, line);
+ ctx->collect_signature (ctx->cookie, "\r\n");
+ }
+ }
+ else if (ctx->pgpmime == PGPMIME_GOT_SIGNATURE)
+ {
+ ctx->pgpmime = PGPMIME_NONE;
+ if (ctx->collect_signeddata)
+ ctx->collect_signature (ctx->cookie, NULL);
+ }
+ else if (ctx->want_part)
+ {
+ if (ctx->part_data)
+ {
+ if (ctx->decode_part == 1)
+ {
+ length = qp_decode (line, length, NULL);
+ }
+ else if (ctx->decode_part == 2)
+ {
+ log_assert (ctx->b64state);
+ err = b64dec_proc (ctx->b64state, line, length, &nbytes);
+ if (err)
+ goto leave;
+ length = nbytes;
+ }
+ err = ctx->part_data (ctx->cookie, line, length);
+ if (err)
+ goto leave;
+ }
+ }
+ }
+
+ rfc822parse_close (msg);
+ msg = NULL;
+ err = 0;
+
+ leave:
+ rfc822parse_cancel (msg);
+ return err;
+}
diff --git a/tools/mime-parser.h b/tools/mime-parser.h
new file mode 100644
index 000000000..ab0d79288
--- /dev/null
+++ b/tools/mime-parser.h
@@ -0,0 +1,52 @@
+/* mime-parser.h - Parse MIME structures (high level rfc822 parser).
+ * 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_MIME_PARSER_H
+#define GNUPG_MIME_PARSER_H
+
+struct mime_parser_context_s;
+typedef struct mime_parser_context_s *mime_parser_t;
+
+gpg_error_t mime_parser_new (mime_parser_t *r_ctx, void *cookie);
+void mime_parser_release (mime_parser_t ctx);
+
+void mime_parser_set_verbose (mime_parser_t ctx, int level);
+void mime_parser_set_new_part (mime_parser_t ctx,
+ gpg_error_t (*fnc) (void *cookie,
+ const char *mediatype,
+ const char *mediasubtype));
+void mime_parser_set_part_data (mime_parser_t ctx,
+ gpg_error_t (*fnc) (void *cookie,
+ const void *data,
+ size_t datalen));
+void mime_parser_set_collect_encrypted (mime_parser_t ctx,
+ gpg_error_t (*fnc) (void *cookie,
+ const char *data));
+void mime_parser_set_collect_signeddata (mime_parser_t ctx,
+ gpg_error_t (*fnc) (void *cookie,
+ const char *data));
+void mime_parser_set_collect_signature (mime_parser_t ctx,
+ gpg_error_t (*fnc) (void *cookie,
+ const char *data));
+
+gpg_error_t mime_parser_parse (mime_parser_t ctx, estream_t fp);
+
+
+
+#endif /*GNUPG_MIME_PARSER_H*/
diff --git a/tools/rfc822parse.h b/tools/rfc822parse.h
index 8bb5536a1..c5579fe44 100644
--- a/tools/rfc822parse.h
+++ b/tools/rfc822parse.h
@@ -1,6 +1,6 @@
/* rfc822parse.h - Simple mail and MIME parser
* Copyright (C) 1999 Werner Koch, Duesseldorf
- * Copyright (C) 2003, g10 Code GmbH
+ * Copyright (C) 2003 g10 Code GmbH
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
diff --git a/tools/send-mail.c b/tools/send-mail.c
new file mode 100644
index 000000000..2266521a4
--- /dev/null
+++ b/tools/send-mail.c
@@ -0,0 +1,129 @@
+/* send-mail.c - Invoke sendmail or other delivery tool.
+ * 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 "exectool.h"
+#include "sysutils.h"
+#include "send-mail.h"
+
+
+static gpg_error_t
+run_sendmail (estream_t data)
+{
+ gpg_error_t err;
+ const char pgmname[] = "/usr/lib/sendmail";
+ const char *argv[3];
+
+ argv[0] = "-oi";
+ argv[1] = "-t";
+ argv[2] = NULL;
+
+ err = gnupg_exec_tool_stream (pgmname, argv, data, NULL, NULL, NULL, NULL);
+ if (err)
+ log_error ("running '%s' failed: %s\n", pgmname, gpg_strerror (err));
+ return err;
+}
+
+
+/* Send the data in FP as mail. */
+gpg_error_t
+send_mail (estream_t fp)
+{
+ return run_sendmail (fp);
+}
+
+
+/* Convenience function to write a mail to a named file. */
+gpg_error_t
+send_mail_to_file (estream_t fp, const char *fname)
+{
+ gpg_error_t err;
+ estream_t outfp = NULL;
+ char *buffer = NULL;
+ size_t buffersize = 32 * 1024;
+ size_t nbytes, nwritten;
+
+ if (!fname)
+ fname = "-";
+
+ buffer = xtrymalloc (buffersize);
+ if (!buffer)
+ return gpg_error_from_syserror ();
+
+ outfp = !strcmp (fname,"-")? es_stdout : es_fopen (fname, "wb");
+ if (!outfp)
+ {
+ err = gpg_error_from_syserror ();
+ log_error ("error creating '%s': %s\n", fname, gpg_strerror (err));
+ goto leave;
+ }
+ for (;;)
+ {
+ if (es_read (fp, buffer, sizeof buffer, &nbytes))
+ {
+ err = gpg_error_from_syserror ();
+ log_error ("error reading '%s': %s\n",
+ es_fname_get (fp), gpg_strerror (err));
+ goto leave;
+ }
+
+ if (!nbytes)
+ {
+ err = 0;
+ break; /* 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)
+ {
+ if (outfp && outfp != es_stdout)
+ {
+ es_fclose (outfp);
+ gnupg_remove (fname);
+ }
+ }
+ else if (outfp && outfp != es_stdout && es_fclose (outfp))
+ {
+ err = gpg_error_from_syserror ();
+ log_error ("error closing '%s': %s\n", fname, gpg_strerror (err));
+ }
+
+ xfree (buffer);
+ return err;
+}
diff --git a/tools/send-mail.h b/tools/send-mail.h
new file mode 100644
index 000000000..5f57854af
--- /dev/null
+++ b/tools/send-mail.h
@@ -0,0 +1,27 @@
+/* send-mail.h - Invoke sendmail or other delivery tool.
+ * 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_SEND_MAIL_H
+#define GNUPG_SEND_MAIL_H
+
+gpg_error_t send_mail (estream_t fp);
+gpg_error_t send_mail_to_file (estream_t fp, const char *fname);
+
+
+#endif /*GNUPG_SEND_MAIL_H*/
diff --git a/tools/wks-receive.c b/tools/wks-receive.c
new file mode 100644
index 000000000..59141fcdc
--- /dev/null
+++ b/tools/wks-receive.c
@@ -0,0 +1,464 @@
+/* wks-receive.c - Receive a WKS mail
+ * Copyright (C) 2016 g10 Code GmbH
+ *
+ * This file is part of GnuPG.
+ *
+ * GnuPG is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * GnuPG is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <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;
+}
diff --git a/tools/wks-util.c b/tools/wks-util.c
new file mode 100644
index 000000000..8d9f92bd3
--- /dev/null
+++ b/tools/wks-util.c
@@ -0,0 +1,65 @@
+/* wks-utils.c - Common helper fucntions for wks tools
+ * 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 "mime-maker.h"
+#include "send-mail.h"
+#include "gpg-wks.h"
+
+
+/* Helper to write mail to the output(s). */
+gpg_error_t
+wks_send_mime (mime_maker_t mime)
+{
+ gpg_error_t err;
+ estream_t mail;
+
+ /* Without any option we take a short path. */
+ if (!opt.use_sendmail && !opt.output)
+ return mime_maker_make (mime, es_stdout);
+
+ mail = es_fopenmem (0, "w+b");
+ if (!mail)
+ {
+ err = gpg_error_from_syserror ();
+ return err;
+ }
+
+ err = mime_maker_make (mime, mail);
+
+ if (!err && opt.output)
+ {
+ es_rewind (mail);
+ err = send_mail_to_file (mail, opt.output);
+ }
+
+ if (!err && opt.use_sendmail)
+ {
+ es_rewind (mail);
+ err = send_mail (mail);
+ }
+
+ es_fclose (mail);
+ return err;
+}