diff options
Diffstat (limited to 'tools')
-rw-r--r-- | tools/Makefile.am | 38 | ||||
-rw-r--r-- | tools/call-dirmngr.c | 205 | ||||
-rw-r--r-- | tools/call-dirmngr.h | 28 | ||||
-rw-r--r-- | tools/gpg-connect-agent.c | 10 | ||||
-rw-r--r-- | tools/gpg-wks-client.c | 758 | ||||
-rw-r--r-- | tools/gpg-wks-server.c | 1548 | ||||
-rw-r--r-- | tools/gpg-wks.h | 61 | ||||
-rw-r--r-- | tools/gpgconf.c | 96 | ||||
-rw-r--r-- | tools/gpgtar-extract.c | 2 | ||||
-rw-r--r-- | tools/gpgtar-list.c | 2 | ||||
-rw-r--r-- | tools/gpgtar.c | 25 | ||||
-rw-r--r-- | tools/mime-maker.c | 667 | ||||
-rw-r--r-- | tools/mime-maker.h | 43 | ||||
-rw-r--r-- | tools/mime-parser.c | 772 | ||||
-rw-r--r-- | tools/mime-parser.h | 52 | ||||
-rw-r--r-- | tools/rfc822parse.h | 2 | ||||
-rw-r--r-- | tools/send-mail.c | 129 | ||||
-rw-r--r-- | tools/send-mail.h | 27 | ||||
-rw-r--r-- | tools/wks-receive.c | 464 | ||||
-rw-r--r-- | tools/wks-util.c | 65 |
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; +} |