diff --git a/doc/wks.texi b/doc/wks.texi
index 6d622828f..b8de29639 100644
--- a/doc/wks.texi
+++ b/doc/wks.texi
@@ -1,372 +1,379 @@
@c wks.texi - man pages for the Web Key Service tools.
@c Copyright (C) 2017 g10 Code GmbH
@c Copyright (C) 2017 Bundesamt für Sicherheit in der Informationstechnik
@c This is part of the GnuPG manual.
@c For copying conditions, see the file GnuPG.texi.
@include defs.inc
@node Web Key Service
@chapter Web Key Service
GnuPG comes with tools used to maintain and access a Web Key
Directory.
@menu
* gpg-wks-client:: Send requests via WKS
* gpg-wks-server:: Server to provide the WKS.
@end menu
@c
@c GPG-WKS-CLIENT
@c
@manpage gpg-wks-client.1
@node gpg-wks-client
@section Send requests via WKS
@ifset manverb
.B gpg-wks-client
\- Client for the Web Key Service
@end ifset
@mansect synopsis
@ifset manverb
.B gpg-wks-client
.RI [ options ]
.B \-\-supported
.I user-id
.br
.B gpg-wks-client
.RI [ options ]
.B \-\-check
.I user-id
.br
.B gpg-wks-client
.RI [ options ]
.B \-\-create
.I fingerprint
.I user-id
.br
.B gpg-wks-client
.RI [ options ]
.B \-\-receive
.br
.B gpg-wks-client
.RI [ options ]
.B \-\-read
@end ifset
@mansect description
The @command{gpg-wks-client} is used to send requests to a Web Key
Service provider. This is usuallay done to upload a key into a Web
Key Directory.
With the @option{--supported} command the caller can test whether a
site supports the Web Key Service. The argument is an arbitray
address in the to be tested domain. For example
@file{foo@@example.net}. The command returns success if the Web Key
Service is supported. The operation is silent; to get diagnostic
output use the option @option{--verbose}.
With the @option{--check} command the caller can test whether a key
exists for a supplied mail address. The command returns success if a
key is available.
The @option{--create} command is used to send a request for
publication in the Web Key Directory. The arguments are the
fingerprint of the key and the user id to publish. The output from
the command is a properly formatted mail with all standard headers.
This mail can be fed to @command{sendmail(8)} or any other tool to
actually send that mail. If @command{sendmail(8)} is installed the
option @option{--send} can be used to directly send the created
request. If the provider request a 'mailbox-only' user id and no such
user id is found, @command{gpg-wks-client} will try an additional user
id.
The @option{--receive} and @option{--read} commands are used to
process confirmation mails as send from the service provider. The
former expects an encrypted MIME messages, the latter an already
decrypted MIME message. The result of these commands are another mail
which can be send in the same way as the mail created with
@option{--create}.
@command{gpg-wks-client} is not commonly invoked directly and thus it
is not installed in the bin directory. Here is an example how it can
be invoked manually to check for a Web Key Directory entry for
@file{foo@@example.org}:
@example
$(gpgconf --list-dirs libexecdir)/gpg-wks-client --check foo@@example.net
@end example
@mansect options
@noindent
@command{gpg-wks-client} understands these options:
@table @gnupgtabopt
@item --send
@opindex send
Directly send created mails using the @command{sendmail} command.
Requires installation of that command.
@item --output @var{file}
@itemx -o
@opindex output
Write the created mail to @var{file} instead of stdout. Note that the
value @code{-} for @var{file} is the same as writing to stdout.
@item --status-fd @var{n}
@opindex status-fd
Write special status strings to the file descriptor @var{n}.
This program returns only the status messages SUCCESS or FAILURE which
are helpful when the caller uses a double fork approach and can't
easily get the return code of the process.
@item --verbose
@opindex verbose
Enable extra informational output.
@item --quiet
@opindex quiet
Disable almost all informational output.
@item --version
@opindex version
Print version of the program and exit.
@item --help
@opindex help
Display a brief help page and exit.
@end table
@mansect see also
@ifset isman
@command{gpg-wks-server}(1)
@end ifset
@c
@c GPG-WKS-SERVER
@c
@manpage gpg-wks-server.1
@node gpg-wks-server
@section Provide the Web Key Service
@ifset manverb
.B gpg-wks-server
\- Server providing the Web Key Service
@end ifset
@mansect synopsis
@ifset manverb
.B gpg-wks-server
.RI [ options ]
.B \-\-receive
.br
.B gpg-wks-server
.RI [ options ]
.B \-\-cron
.br
.B gpg-wks-server
.RI [ options ]
.B \-\-list-domains
.br
.B gpg-wks-server
.RI [ options ]
.B \-\-check-key
.I user-id
.br
.B gpg-wks-server
.RI [ options ]
.B \-\-install-key
.I file
.I user-id
.br
.B gpg-wks-server
.RI [ options ]
.B \-\-remove-key
.I user-id
.br
.B gpg-wks-server
.RI [ options ]
.B \-\-revoke-key
.I user-id
@end ifset
@mansect description
The @command{gpg-wks-server} is a server site implementation of the
Web Key Service. It receives requests for publication, sends
confirmation requests, receives confirmations, and published the key.
It also has features to ease the setup and maintenance of a Web Key
Directory.
When used with the command @option{--receive} a single Web Key Service
mail is processed. Commonly this command is used with the option
@option{--send} to directly send the crerated mails back. See below
for an installation example.
The command @option{--cron} is used for regualr cleanup tasks. For
example non-confirmed requested should be removed after their expire
time. It is best to run this command once a day from a cronjob.
The command @option{--list-domains} prints all configured domains.
Further it creates missing directories for the configuration and
prints warnings pertaining to problems in the configuration.
The command @option{--check-key} (or just @option{--check}) checks
-whether a key with the given user-id is installed. The process return
-success in this case; to also print a diagnostic, use option
-@option{-v}. If the key is not installed a diagnostics is printed and
+whether a key with the given user-id is installed. The process returns
+success in this case; to also print a diagnostic use the option
+@option{-v}. If the key is not installed a diagnostic is printed and
the process returns failure; to suppress the diagnostic, use option
@option{-q}. More than one user-id can be given; see also option
@option{with-file}.
The command @option{--install-key} manually installs a key into the
WKD. The arguments are a file with the keyblock and the user-id to
install. If the first argument resembles a fingerprint the key is
taken from the current keyring; to force the use of a file, prefix the
first argument with "./".
The command @option{--remove-key} uninstalls a key from the WKD. The
process returns success in this case; to also print a diagnostic, use
option @option{-v}. If the key is not installed a diagnostic is
printed and the process returns failure; to suppress the diagnostic,
use option @option{-q}.
The command @option{--revoke-key} is not yet functional.
@mansect options
@noindent
@command{gpg-wks-server} understands these options:
@table @gnupgtabopt
+@item -C @var{dir}
+@itemx --directory @var{dir}
+@opindex directory
+Use @var{dir} as top level directory for domains. The default is
+@file{/var/lib/gnupg/wks}.
+
@item --from @var{mailaddr}
@opindex from
Use @var{mailaddr} as the default sender address.
@item --header @var{name}=@var{value}
@opindex header
Add the mail header "@var{name}: @var{value}" to all outgoing mails.
@item --send
@opindex send
Directly send created mails using the @command{sendmail} command.
Requires installation of that command.
-@item --output @var{file}
-@itemx -o
+@item -o @var{file}
+@itemx --output @var{file}
@opindex output
Write the created mail also to @var{file}. Note that the value
@code{-} for @var{file} would write it to stdout.
@item --with-dir
@opindex with-dir
-Also print the directory name for each domain listed by command
-@option{--list-domains}.
+When used with the command @option{--list-domains} print for each
+installed domain the domain name and its directory name.
@item --with-file
@opindex with-file
-With command @option{--check-key} print for each user-id, the address,
-'i' for installed key or 'n' for not installed key, and the filename.
+When used with the command @option{--check-key} print for each user-id,
+the address, 'i' for installed key or 'n' for not installed key, and
+the filename.
@item --verbose
@opindex verbose
Enable extra informational output.
@item --quiet
@opindex quiet
Disable almost all informational output.
@item --version
@opindex version
Print version of the program and exit.
@item --help
@opindex help
Display a brief help page and exit.
@end table
@noindent
@mansect examples
@chapheading Examples
The Web Key Service requires a working directory to store keys
pending for publication. As root create a working directory:
@example
# mkdir /var/lib/gnupg/wks
# chown webkey:webkey /var/lib/gnupg/wks
# chmod 2750 /var/lib/gnupg/wks
@end example
Then under your webkey account create directories for all your
domains. Here we do it for "example.net":
@example
$ mkdir /var/lib/gnupg/wks/example.net
@end example
Finally run
@example
$ gpg-wks-server --list-domains
@end example
-to create the required sub-directories with the permission set
+to create the required sub-directories with the permissions set
correctly. For each domain a submission address needs to be
configured. All service mails are directed to that address. It can
be the same address for all configured domains, for example:
@example
$ cd /var/lib/gnupg/wks/example.net
$ echo key-submission@@example.net >submission-address
@end example
-The protocol requires that the key to be published is sent with an
+The protocol requires that the key to be published is send with an
encrypted mail to the service. Thus you need to create a key for
the submission address:
@example
$ gpg --batch --passphrase '' --quick-gen-key key-submission@@example.net
$ gpg -K key-submission@@example.net
@end example
The output of the last command looks similar to this:
@example
sec rsa2048 2016-08-30 [SC]
C0FCF8642D830C53246211400346653590B3795B
uid [ultimate] key-submission@@example.net
ssb rsa2048 2016-08-30 [E]
@end example
Take the fingerprint from that output and manually publish the key:
@example
$ gpg-wks-server --install-key C0FCF8642D830C53246211400346653590B3795B \
> key-submission@@example.net
@end example
Finally that submission address needs to be redirected to a script
running @command{gpg-wks-server}. The @command{procmail} command can
be used for this: Redirect the submission address to the user "webkey"
and put this into webkey's @file{.procmailrc}:
@example
:0
* !^From: webkey@@example.net
* !^X-WKS-Loop: webkey.example.net
|gpg-wks-server -v --receive \
--header X-WKS-Loop=webkey.example.net \
--from webkey@@example.net --send
@end example
@mansect see also
@ifset isman
@command{gpg-wks-client}(1)
@end ifset
diff --git a/tools/gpg-wks-server.c b/tools/gpg-wks-server.c
index a5881557f..24b331262 100644
--- a/tools/gpg-wks-server.c
+++ b/tools/gpg-wks-server.c
@@ -1,2277 +1,2284 @@
/* gpg-wks-server.c - A server for the Web Key Service protocols.
* Copyright (C) 2016, 2018 Werner Koch
* Copyright (C) 2016 Bundesamt für Sicherheit in der Informationstechnik
*
* This file is part of GnuPG.
*
* This file is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This file is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, see .
*/
/* The Web Key Service I-D defines an update protocol to store a
* public key in the Web Key Directory. The current specification is
* draft-koch-openpgp-webkey-service-05.txt.
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include "../common/util.h"
#include "../common/init.h"
#include "../common/sysutils.h"
#include "../common/userids.h"
#include "../common/ccparray.h"
#include "../common/exectool.h"
#include "../common/zb32.h"
#include "../common/mbox-util.h"
#include "../common/name-value.h"
#include "mime-maker.h"
#include "send-mail.h"
#include "gpg-wks.h"
/* 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',
+ oDirectory = 'C',
oDebug = 500,
aReceive,
aCron,
aListDomains,
aInstallKey,
aRevokeKey,
aRemoveKey,
aCheck,
oGpgProgram,
oSend,
oFrom,
oHeader,
oWithDir,
oWithFile,
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_c (aCheck, "check",
("check whether a key is installed")),
ARGPARSE_c (aCheck, "check-key", "@"),
ARGPARSE_c (aInstallKey, "install-key",
"install a key from FILE into the WKD"),
ARGPARSE_c (aRemoveKey, "remove-key",
"remove a key from the WKD"),
ARGPARSE_c (aRevokeKey, "revoke-key",
"mark a key as revoked"),
ARGPARSE_group (301, ("@\nOptions:\n ")),
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 (oDirectory, "directory", "|DIR|use DIR as top directory"),
ARGPARSE_s_s (oFrom, "from", "|ADDR|use ADDR as the default sender"),
ARGPARSE_s_s (oHeader, "header" ,
"|NAME=VALUE|add \"NAME: VALUE\" as header to all mails"),
ARGPARSE_s_n (oWithDir, "with-dir", "@"),
ARGPARSE_s_n (oWithFile, "with-file", "@"),
ARGPARSE_end ()
};
/* The list of supported debug flags. */
static struct debug_flags_s debug_flags [] =
{
{ DBG_MIME_VALUE , "mime" },
{ DBG_PARSER_VALUE , "parser" },
{ DBG_CRYPTO_VALUE , "crypto" },
{ DBG_MEMORY_VALUE , "memory" },
{ DBG_MEMSTAT_VALUE, "memstat" },
{ DBG_IPC_VALUE , "ipc" },
{ DBG_EXTPROG_VALUE, "extprog" },
{ 0, NULL }
};
/* State for processing a message. */
struct server_ctx_s
{
char *fpr;
uidinfo_list_t mboxes; /* List with addr-specs taken from the UIDs. */
unsigned int draft_version_2:1; /* Client supports the draft 2. */
};
typedef struct server_ctx_s *server_ctx_t;
/* Flag for --with-dir. */
static int opt_with_dir;
/* Flag for --with-file. */
static int opt_with_file;
/* Prototypes. */
static gpg_error_t get_domain_list (strlist_t *r_list);
static gpg_error_t command_receive_cb (void *opaque,
const char *mediatype, estream_t fp,
unsigned int flags);
static gpg_error_t command_list_domains (void);
static gpg_error_t command_install_key (const char *fname, const char *userid);
static gpg_error_t command_remove_key (const char *mailaddr);
static gpg_error_t command_revoke_key (const char *mailaddr);
static gpg_error_t command_check_key (const char *mailaddr);
static gpg_error_t command_cron (void);
/* Print usage information and provide strings for help. */
static const char *
my_strusage( int level )
{
const char *p;
switch (level)
{
case 11: p = "gpg-wks-server"; break;
case 12: p = "@GNUPG@"; break;
case 13: p = VERSION; break;
case 17: p = PRINTABLE_OS_NAME; break;
case 19: p = ("Please report bugs to <@EMAIL@>.\n"); break;
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 oDirectory:
+ opt.directory = 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 oWithDir:
opt_with_dir = 1;
break;
case oWithFile:
opt_with_file = 1;
break;
case aReceive:
case aCron:
case aListDomains:
case aCheck:
case aInstallKey:
case aRemoveKey:
case aRevokeKey:
cmd = pargs->r_opt;
break;
default: pargs->err = 2; break;
}
}
return cmd;
}
/* gpg-wks-server main. */
int
main (int argc, char **argv)
{
gpg_error_t err, firsterr;
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_IROTH|S_IWOTH)))
{
log_error ("directory '%s' has too relaxed permissions\n",
opt.directory);
+ log_info ("Fix by running: chmod o-rw '%s'\n", opt.directory);
exit (2);
}
}
#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;
case aInstallKey:
if (argc != 2)
wrong_args ("--install-key FILE USER-ID");
err = command_install_key (*argv, argv[1]);
break;
case aRemoveKey:
if (argc != 1)
wrong_args ("--remove-key USER-ID");
err = command_remove_key (*argv);
break;
case aRevokeKey:
if (argc != 1)
wrong_args ("--revoke-key USER-ID");
err = command_revoke_key (*argv);
break;
case aCheck:
if (!argc)
wrong_args ("--check USER-IDs");
firsterr = 0;
for (; argc; argc--, argv++)
{
err = command_check_key (*argv);
if (!firsterr)
firsterr = err;
}
err = firsterr;
break;
default:
usage (1);
err = gpg_error (GPG_ERR_BUG);
break;
}
if (err)
log_error ("command failed: %s\n", gpg_strerror (err));
return log_get_errorcount (0)? 1:0;
}
/* Take the key in KEYFILE and write it to OUTFILE in binary encoding.
* If ADDRSPEC is given only matching user IDs are included in the
* output. */
static gpg_error_t
copy_key_as_binary (const char *keyfile, const char *outfile,
const char *addrspec)
{
gpg_error_t err;
ccparray_t ccp;
const char **argv = NULL;
char *filterexp = NULL;
if (addrspec)
{
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, "--yes");
ccparray_put (&ccp, "--always-trust");
ccparray_put (&ccp, "--no-keyring");
ccparray_put (&ccp, "--output");
ccparray_put (&ccp, outfile);
ccparray_put (&ccp, "--import-options=import-export");
if (filterexp)
{
ccparray_put (&ccp, "--import-filter");
ccparray_put (&ccp, filterexp);
}
ccparray_put (&ccp, "--import");
ccparray_put (&ccp, "--");
ccparray_put (&ccp, keyfile);
ccparray_put (&ccp, NULL);
argv = ccparray_get (&ccp, NULL);
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 (filterexp);
xfree (argv);
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 (DBG_CRYPTO)
log_debug ("gpg status: %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;
}
static void
sign_stream_status_cb (void *opaque, const char *keyword, char *args)
{
(void)opaque;
if (DBG_CRYPTO)
log_debug ("gpg status: %s %s\n", keyword, args);
}
/* Sign the INPUT stream to a new stream which is stored at success at
* R_OUTPUT. A detached signature is created using the key specified
* by USERID. */
static gpg_error_t
sign_stream (estream_t *r_output, estream_t input, const char *userid)
{
gpg_error_t err;
ccparray_t ccp;
const char **argv;
estream_t output;
*r_output = NULL;
output = es_fopenmem (0, "w+b");
if (!output)
{
err = gpg_error_from_syserror ();
log_error ("error allocating memory buffer: %s\n", gpg_strerror (err));
return err;
}
ccparray_init (&ccp, 0);
ccparray_put (&ccp, "--no-options");
if (!opt.verbose)
ccparray_put (&ccp, "--quiet");
else if (opt.verbose > 1)
ccparray_put (&ccp, "--verbose");
ccparray_put (&ccp, "--batch");
ccparray_put (&ccp, "--status-fd=2");
ccparray_put (&ccp, "--armor");
ccparray_put (&ccp, "--local-user");
ccparray_put (&ccp, userid);
ccparray_put (&ccp, "--detach-sign");
ccparray_put (&ccp, "--");
ccparray_put (&ccp, NULL);
argv = ccparray_get (&ccp, NULL);
if (!argv)
{
err = gpg_error_from_syserror ();
goto leave;
}
err = gnupg_exec_tool_stream (opt.gpg_program, argv, input,
NULL, output,
sign_stream_status_cb, NULL);
if (err)
{
log_error ("signing failed: %s\n", gpg_strerror (err));
goto leave;
}
es_rewind (output);
*r_output = output;
output = NULL;
leave:
es_fclose (output);
xfree (argv);
return err;
}
/* Get the submission address for address MBOX. Caller must free the
* value. If no address can be found NULL is returned. */
static char *
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;
}
/* Get the policy flags for address MBOX and store them in POLICY. */
static gpg_error_t
get_policy_flags (policy_flags_t policy, const char *mbox)
{
gpg_error_t err;
const char *domain;
char *fname;
estream_t fp;
memset (policy, 0, sizeof *policy);
domain = strchr (mbox, '@');
if (!domain)
return gpg_error (GPG_ERR_INV_USER_ID);
domain++;
fname = make_filename_try (opt.directory, domain, "policy", NULL);
if (!fname)
{
err = gpg_error_from_syserror ();
log_error ("make_filename failed in %s: %s\n",
__func__, gpg_strerror (err));
return err;
}
fp = es_fopen (fname, "r");
if (!fp)
{
err = gpg_error_from_syserror ();
if (gpg_err_code (err) == GPG_ERR_ENOENT)
err = 0;
else
log_error ("error reading '%s': %s\n", fname, gpg_strerror (err));
xfree (fname);
return err;
}
err = wks_parse_policy (policy, fp, 0);
es_fclose (fp);
xfree (fname);
return err;
}
/* We store the key under the name of the nonce we will then send to
* the user. On success the nonce is stored at R_NONCE and the file
* name at R_FNAME. */
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 request. DIR is the directory used for the
* address MBOX. NONCE is the nonce we want to see in the response to
* this mail. FNAME the name of the file with the key. */
static gpg_error_t
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;
estream_t signeddata = NULL;
estream_t signature = 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;
}
if (!ctx->draft_version_2)
{
/* It is fine to use 8 bit encoding because that is encrypted and
* only our client will see it. */
es_fputs ("Content-Type: application/vnd.gnupg.wks\n"
"Content-Transfer-Encoding: 8bit\n"
"\n",
body);
}
es_fprintf (body, ("type: confirmation-request\n"
"sender: %s\n"
"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;
err = mime_maker_add_header (mime, "Wks-Draft-Version",
STR2(WKS_DRAFT_VERSION));
if (err)
goto leave;
/* Help Enigmail to identify messages. Note that this is in no way
* secured. */
err = mime_maker_add_header (mime, "WKS-Phase", "confirm");
if (err)
goto leave;
for (sl = opt.extra_headers; sl; sl = sl->next)
{
err = mime_maker_add_header (mime, sl->d, NULL);
if (err)
goto leave;
}
if (!ctx->draft_version_2)
{
err = mime_maker_add_header (mime, "Content-Type",
"multipart/encrypted; "
"protocol=\"application/pgp-encrypted\"");
if (err)
goto leave;
err = mime_maker_add_container (mime);
if (err)
goto leave;
err = mime_maker_add_header (mime, "Content-Type",
"application/pgp-encrypted");
if (err)
goto leave;
err = mime_maker_add_body (mime, "Version: 1\n");
if (err)
goto leave;
err = mime_maker_add_header (mime, "Content-Type",
"application/octet-stream");
if (err)
goto leave;
err = mime_maker_add_stream (mime, &bodyenc);
if (err)
goto leave;
}
else
{
unsigned int partid;
/* FIXME: Add micalg. */
err = mime_maker_add_header (mime, "Content-Type",
"multipart/signed; "
"protocol=\"application/pgp-signature\"");
if (err)
goto leave;
err = mime_maker_add_container (mime);
if (err)
goto leave;
err = mime_maker_add_header (mime, "Content-Type", "multipart/mixed");
if (err)
goto leave;
err = mime_maker_add_container (mime);
if (err)
goto leave;
partid = mime_maker_get_partid (mime);
err = mime_maker_add_header (mime, "Content-Type", "text/plain");
if (err)
goto leave;
err = mime_maker_add_body
(mime,
"This message has been send to confirm your request\n"
"to publish your key. If you did not request a key\n"
"publication, simply ignore this message.\n"
"\n"
"Most mail software can handle this kind of message\n"
"automatically and thus you would not have seen this\n"
"message. It seems that your client does not fully\n"
"support this service. The web page\n"
"\n"
" https://gnupg.org/faq/wkd.html\n"
"\n"
"explains how you can process this message anyway in\n"
"a few manual steps.\n");
if (err)
goto leave;
err = mime_maker_add_header (mime, "Content-Type",
"application/vnd.gnupg.wks");
if (err)
goto leave;
err = mime_maker_add_stream (mime, &bodyenc);
if (err)
goto leave;
err = mime_maker_end_container (mime);
if (err)
goto leave;
/* mime_maker_dump_tree (mime); */
err = mime_maker_get_part (mime, partid, &signeddata);
if (err)
goto leave;
err = sign_stream (&signature, signeddata, from);
if (err)
goto leave;
err = mime_maker_add_header (mime, "Content-Type",
"application/pgp-signature");
if (err)
goto leave;
err = mime_maker_add_stream (mime, &signature);
if (err)
goto leave;
}
err = wks_send_mime (mime);
leave:
mime_maker_release (mime);
es_fclose (signature);
es_fclose (signeddata);
es_fclose (bodyenc);
es_fclose (body);
xfree (from_buffer);
return err;
}
/* Store the key given by KEY into the pending directory and send a
* confirmation requests. */
static gpg_error_t
process_new_key (server_ctx_t ctx, estream_t key)
{
gpg_error_t err;
uidinfo_list_t sl;
const char *s;
char *dname = NULL;
char *nonce = NULL;
char *fname = NULL;
struct policy_flags_s policybuf;
memset (&policybuf, 0, sizeof policybuf);
/* First figure out the user id from the key. */
xfree (ctx->fpr);
free_uidinfo_list (ctx->mboxes);
err = wks_list_key (key, &ctx->fpr, &ctx->mboxes);
if (err)
goto leave;
log_assert (ctx->fpr);
log_info ("fingerprint: %s\n", ctx->fpr);
for (sl = ctx->mboxes; sl; sl = sl->next)
{
if (sl->mbox)
log_info (" addr-spec: %s\n", sl->mbox);
}
/* Walk over all user ids and send confirmation requests for those
* we support. */
for (sl = ctx->mboxes; sl; sl = sl->next)
{
if (!sl->mbox)
continue;
s = strchr (sl->mbox, '@');
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;
}
if (access (dname, W_OK))
{
log_info ("skipping address '%s': Domain not configured\n", sl->mbox);
continue;
}
if (get_policy_flags (&policybuf, sl->mbox))
{
log_info ("skipping address '%s': Bad policy flags\n", sl->mbox);
continue;
}
if (policybuf.auth_submit)
{
/* Bypass the confirmation stuff and publish the key as is. */
log_info ("publishing address '%s'\n", sl->mbox);
/* FIXME: We need to make sure that we do this only for the
* address in the mail. */
log_debug ("auth-submit not yet working!\n");
}
else
{
log_info ("storing address '%s'\n", sl->mbox);
xfree (nonce);
xfree (fname);
err = store_key_as_pending (dname, key, &nonce, &fname);
if (err)
goto leave;
err = send_confirmation_request (ctx, sl->mbox, nonce, fname);
if (err)
goto leave;
}
}
leave:
if (nonce)
wipememory (nonce, strlen (nonce));
xfree (nonce);
xfree (fname);
xfree (dname);
wks_free_policy (&policybuf);
return err;
}
/* Send a message to tell the user at MBOX that their key has been
* published. FNAME the name of the file with the key. */
static gpg_error_t
send_congratulation_message (const char *mbox, const char *keyfile)
{
gpg_error_t err;
estream_t body = NULL;
estream_t bodyenc = NULL;
mime_maker_t mime = NULL;
char *from_buffer = NULL;
const char *from;
strlist_t sl;
from = from_buffer = get_submission_address (mbox);
if (!from)
{
from = opt.default_from;
if (!from)
{
log_error ("no sender address found for '%s'\n", mbox);
err = gpg_error (GPG_ERR_CONFIGURATION);
goto leave;
}
log_info ("Note: using default sender address '%s'\n", from);
}
body = es_fopenmem (0, "w+b");
if (!body)
{
err = gpg_error_from_syserror ();
log_error ("error allocating memory buffer: %s\n", gpg_strerror (err));
goto leave;
}
/* It is fine to use 8 bit encoding because that is encrypted and
* only our client will see it. */
es_fputs ("Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"\n",
body);
es_fprintf (body,
"Hello!\n\n"
"The key for your address '%s' has been published\n"
"and can now be retrieved from the Web Key Directory.\n"
"\n"
"For more information on this system see:\n"
"\n"
" https://gnupg.org/faq/wkd.html\n"
"\n"
"Best regards\n"
"\n"
" Gnu Key Publisher\n\n\n"
"-- \n"
"The GnuPG Project welcomes donations: %s\n",
mbox, "https://gnupg.org/donate");
es_rewind (body);
err = encrypt_stream (&bodyenc, body, keyfile);
if (err)
goto leave;
es_fclose (body);
body = NULL;
err = mime_maker_new (&mime, NULL);
if (err)
goto leave;
err = mime_maker_add_header (mime, "From", from);
if (err)
goto leave;
err = mime_maker_add_header (mime, "To", mbox);
if (err)
goto leave;
err = mime_maker_add_header (mime, "Subject", "Your key has been published");
if (err)
goto leave;
err = mime_maker_add_header (mime, "Wks-Draft-Version",
STR2(WKS_DRAFT_VERSION));
if (err)
goto leave;
err = mime_maker_add_header (mime, "WKS-Phase", "done");
if (err)
goto leave;
for (sl = opt.extra_headers; sl; sl = sl->next)
{
err = mime_maker_add_header (mime, sl->d, NULL);
if (err)
goto leave;
}
err = mime_maker_add_header (mime, "Content-Type",
"multipart/encrypted; "
"protocol=\"application/pgp-encrypted\"");
if (err)
goto leave;
err = mime_maker_add_container (mime);
if (err)
goto leave;
err = mime_maker_add_header (mime, "Content-Type",
"application/pgp-encrypted");
if (err)
goto leave;
err = mime_maker_add_body (mime, "Version: 1\n");
if (err)
goto leave;
err = mime_maker_add_header (mime, "Content-Type",
"application/octet-stream");
if (err)
goto leave;
err = mime_maker_add_stream (mime, &bodyenc);
if (err)
goto leave;
err = wks_send_mime (mime);
leave:
mime_maker_release (mime);
es_fclose (bodyenc);
es_fclose (body);
xfree (from_buffer);
return err;
}
/* Write the content of SRC to the new file FNAME. */
static gpg_error_t
write_to_file (estream_t src, const char *fname)
{
gpg_error_t err;
estream_t dst;
char buffer[4096];
size_t nread, written;
dst = es_fopen (fname, "wb");
if (!dst)
return gpg_error_from_syserror ();
do
{
nread = es_fread (buffer, 1, sizeof buffer, src);
if (!nread)
break;
written = es_fwrite (buffer, 1, nread, dst);
if (written != nread)
break;
}
while (!es_feof (src) && !es_ferror (src) && !es_ferror (dst));
if (!es_feof (src) || es_ferror (src) || es_ferror (dst))
{
err = gpg_error_from_syserror ();
es_fclose (dst);
gnupg_remove (fname);
return err;
}
if (es_fclose (dst))
{
err = gpg_error_from_syserror ();
log_error ("error closing '%s': %s\n", fname, gpg_strerror (err));
return err;
}
return 0;
}
/* Compute the the full file name for the key with ADDRSPEC and return
* it at R_FNAME. */
static gpg_error_t
compute_hu_fname (char **r_fname, const char *addrspec)
{
gpg_error_t err;
char *hash;
const char *domain;
char sha1buf[20];
*r_fname = NULL;
domain = strchr (addrspec, '@');
if (!domain || !domain[1] || domain == addrspec)
return gpg_error (GPG_ERR_INV_ARG);
domain++;
gcry_md_hash_buffer (GCRY_MD_SHA1, sha1buf, addrspec, domain - addrspec - 1);
hash = zb32_encode (sha1buf, 8*20);
if (!hash)
return gpg_error_from_syserror ();
*r_fname = make_filename_try (opt.directory, domain, "hu", hash, NULL);
if (!*r_fname)
err = gpg_error_from_syserror ();
else
err = 0;
xfree (hash);
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;
uidinfo_list_t sl;
char shaxbuf[32]; /* Used for SHA-1 and SHA-256 */
/* FIXME: There is a bug in name-value.c which adds white space for
* 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. */
xfree (ctx->fpr);
free_uidinfo_list (ctx->mboxes);
err = wks_list_key (key, &ctx->fpr, &ctx->mboxes);
if (err)
goto leave;
log_assert (ctx->fpr);
log_info ("fingerprint: %s\n", ctx->fpr);
for (sl = ctx->mboxes; sl; sl = sl->next)
if (sl->mbox)
log_info (" addr-spec: %s\n", sl->mbox);
/* Check that the key has 'address' as a user id. We use
* case-insensitive matching because the client is expected to
* return the address verbatim. */
for (sl = ctx->mboxes; sl; sl = sl->next)
if (sl->mbox && !strcmp (sl->mbox, 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. */
err = compute_hu_fname (&fnewname, address);
if (err)
goto leave;
/* Publish. */
err = copy_key_as_binary (fname, fnewname, address);
if (err)
{
err = gpg_error_from_syserror ();
log_error ("copying '%s' to '%s' failed: %s\n",
fname, fnewname, gpg_strerror (err));
goto leave;
}
/* Make sure it is world readable. */
if (gnupg_chmod (fnewname, "-rwxr--r--"))
log_error ("can't set permissions of '%s': %s\n",
fnewname, gpg_strerror (gpg_err_code_from_syserror()));
log_info ("key %s published for '%s'\n", ctx->fpr, address);
send_congratulation_message (address, fnewname);
/* Try to publish as DANE record if the DANE directory exists. */
xfree (fname);
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, unsigned int flags)
{
gpg_error_t err;
struct server_ctx_s ctx;
(void)opaque;
memset (&ctx, 0, sizeof ctx);
if ((flags & WKS_RECEIVE_DRAFT2))
ctx.draft_version_2 = 1;
if (!strcmp (mediatype, "application/pgp-keys"))
err = process_new_key (&ctx, msg);
else if (!strcmp (mediatype, "application/vnd.gnupg.wks"))
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_uidinfo_list (ctx.mboxes);
return err;
}
-/* Return a list of all configured domains. ECh list element is the
+/* Return a list of all configured domains. Each list element is the
* top directory for the domain. To figure out the actual domain
* name strrchr(name, '/') can be used. */
static gpg_error_t
get_domain_list (strlist_t *r_list)
{
gpg_error_t err;
DIR *dir = NULL;
char *fname = NULL;
struct dirent *dentry;
struct stat sb;
strlist_t list = NULL;
*r_list = NULL;
dir = opendir (opt.directory);
if (!dir)
{
err = gpg_error_from_syserror ();
goto leave;
}
while ((dentry = readdir (dir)))
{
if (*dentry->d_name == '.')
continue;
if (!strchr (dentry->d_name, '.'))
continue; /* No dot - can't be a domain subdir. */
xfree (fname);
fname = make_filename_try (opt.directory, dentry->d_name, NULL);
if (!fname)
{
err = gpg_error_from_syserror ();
log_error ("make_filename failed in %s: %s\n",
__func__, gpg_strerror (err));
goto leave;
}
if (stat (fname, &sb))
{
err = gpg_error_from_syserror ();
log_error ("error accessing '%s': %s\n", fname, gpg_strerror (err));
continue;
}
if (!S_ISDIR(sb.st_mode))
continue;
if (!add_to_strlist_try (&list, fname))
{
err = gpg_error_from_syserror ();
log_error ("add_to_strlist failed in %s: %s\n",
__func__, gpg_strerror (err));
goto leave;
}
}
err = 0;
*r_list = list;
list = NULL;
leave:
free_strlist (list);
if (dir)
closedir (dir);
xfree (fname);
return err;
}
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;
estream_t fp;
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++;
if (opt_with_dir)
es_printf ("%s %s\n", domain, sl->d);
else
es_printf ("%s\n", domain);
/* Check that the required directories are there. */
for (i=0; i < DIM (requireddirs); i++)
{
xfree (fname);
fname = make_filename_try (sl->d, requireddirs[i].name, NULL);
if (!fname)
{
err = gpg_error_from_syserror ();
goto leave;
}
if (access (fname, W_OK))
{
err = gpg_error_from_syserror ();
if (gpg_err_code (err) == GPG_ERR_ENOENT)
{
if (gnupg_mkdir (fname, requireddirs[i].perm))
{
err = gpg_error_from_syserror ();
log_error ("domain %s: error creating subdir '%s': %s\n",
domain, requireddirs[i].name,
gpg_strerror (err));
}
else
log_info ("domain %s: subdir '%s' created\n",
domain, requireddirs[i].name);
}
else if (err)
log_error ("domain %s: problem with subdir '%s': %s\n",
domain, requireddirs[i].name, gpg_strerror (err));
}
}
/* Print a warning if the submission address is not configured. */
xfree (fname);
fname = make_filename_try (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));
}
/* Check the syntax of the optional policy file. */
xfree (fname);
fname = make_filename_try (sl->d, "policy", NULL);
if (!fname)
{
err = gpg_error_from_syserror ();
goto leave;
}
fp = es_fopen (fname, "r");
if (!fp)
{
err = gpg_error_from_syserror ();
- if (gpg_err_code (err) != GPG_ERR_ENOENT)
+ if (gpg_err_code (err) == GPG_ERR_ENOENT)
+ {
+ fp = es_fopen (fname, "w");
+ if (!fp)
+ log_error ("domain %s: can't create policy file: %s\n",
+ domain, gpg_strerror (err));
+ else
+ es_fclose (fp);
+ fp = NULL;
+ }
+ else
log_error ("domain %s: error in policy file: %s\n",
domain, gpg_strerror (err));
}
else
{
struct policy_flags_s policy;
err = wks_parse_policy (&policy, fp, 0);
es_fclose (fp);
- if (!err)
- {
- struct policy_flags_s empty_policy;
- memset (&empty_policy, 0, sizeof empty_policy);
- if (!memcmp (&empty_policy, &policy, sizeof policy))
- log_error ("domain %s: empty policy file\n", domain);
- }
wks_free_policy (&policy);
}
-
-
}
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;
}
/* Install a single key into the WKD by reading FNAME and extracting
* USERID. */
static gpg_error_t
command_install_key (const char *fname, const char *userid)
{
gpg_error_t err;
KEYDB_SEARCH_DESC desc;
estream_t fp = NULL;
char *addrspec = NULL;
char *fpr = NULL;
uidinfo_list_t uidlist = NULL;
uidinfo_list_t uid, thisuid;
time_t thistime;
char *huname = NULL;
int any;
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;
}
if (!classify_user_id (fname, &desc, 1)
&& (desc.mode == KEYDB_SEARCH_MODE_FPR
|| desc.mode == KEYDB_SEARCH_MODE_FPR20))
{
/* FNAME looks like a fingerprint. Get the key from the
* standard keyring. */
err = wks_get_key (&fp, fname, addrspec, 0);
if (err)
{
log_error ("error getting key '%s' (uid='%s'): %s\n",
fname, addrspec, gpg_strerror (err));
goto leave;
}
}
else /* Take it from the file */
{
fp = es_fopen (fname, "rb");
if (!fp)
{
err = gpg_error_from_syserror ();
log_error ("error reading '%s': %s\n", fname, gpg_strerror (err));
goto leave;
}
}
/* List the key so that we can figure out the newest UID with the
* requested addrspec. */
err = wks_list_key (fp, &fpr, &uidlist);
if (err)
{
log_error ("error parsing key: %s\n", gpg_strerror (err));
err = gpg_error (GPG_ERR_NO_PUBKEY);
goto leave;
}
thistime = 0;
thisuid = NULL;
any = 0;
for (uid = uidlist; uid; uid = uid->next)
{
if (!uid->mbox)
continue; /* Should not happen anyway. */
if (ascii_strcasecmp (uid->mbox, addrspec))
continue; /* Not the requested addrspec. */
any = 1;
if (uid->created > thistime)
{
thistime = uid->created;
thisuid = uid;
}
}
if (!thisuid)
thisuid = uidlist; /* This is the case for a missing timestamp. */
if (!any)
{
log_error ("public key in '%s' has no mail address '%s'\n",
fname, addrspec);
err = gpg_error (GPG_ERR_INV_USER_ID);
goto leave;
}
if (opt.verbose)
log_info ("using key with user id '%s'\n", thisuid->uid);
{
estream_t fp2;
es_rewind (fp);
err = wks_filter_uid (&fp2, fp, thisuid->uid, 1);
if (err)
{
log_error ("error filtering key: %s\n", gpg_strerror (err));
err = gpg_error (GPG_ERR_NO_PUBKEY);
goto leave;
}
es_fclose (fp);
fp = fp2;
}
/* Hash user ID and create filename. */
err = compute_hu_fname (&huname, addrspec);
if (err)
goto leave;
/* Publish. */
err = write_to_file (fp, huname);
if (err)
{
log_error ("copying key to '%s' failed: %s\n", huname,gpg_strerror (err));
goto leave;
}
/* Make sure it is world readable. */
if (gnupg_chmod (huname, "-rwxr--r--"))
log_error ("can't set permissions of '%s': %s\n",
huname, gpg_strerror (gpg_err_code_from_syserror()));
if (!opt.quiet)
log_info ("key %s published for '%s'\n", fpr, addrspec);
leave:
xfree (huname);
free_uidinfo_list (uidlist);
xfree (fpr);
xfree (addrspec);
es_fclose (fp);
return err;
}
/* Return the filename and optionally the addrspec for USERID at
* R_FNAME and R_ADDRSPEC. R_ADDRSPEC might also be set on error. */
static gpg_error_t
fname_from_userid (const char *userid, char **r_fname, char **r_addrspec)
{
gpg_error_t err;
char *addrspec = NULL;
const char *domain;
char *hash = NULL;
const char *s;
char shaxbuf[32]; /* Used for SHA-1 and SHA-256 */
*r_fname = NULL;
if (r_addrspec)
*r_addrspec = NULL;
addrspec = mailbox_from_userid (userid);
if (!addrspec)
{
if (opt.verbose)
log_info ("\"%s\" is not a proper mail address\n", userid);
err = gpg_error (GPG_ERR_INV_USER_ID);
goto leave;
}
domain = strchr (addrspec, '@');
log_assert (domain);
domain++;
/* Hash user ID and create filename. */
s = strchr (addrspec, '@');
log_assert (s);
gcry_md_hash_buffer (GCRY_MD_SHA1, shaxbuf, addrspec, s - addrspec);
hash = zb32_encode (shaxbuf, 8*20);
if (!hash)
{
err = gpg_error_from_syserror ();
goto leave;
}
*r_fname = make_filename_try (opt.directory, domain, "hu", hash, NULL);
if (!*r_fname)
err = gpg_error_from_syserror ();
else
err = 0;
leave:
if (r_addrspec && addrspec)
*r_addrspec = addrspec;
else
xfree (addrspec);
xfree (hash);
return err;
}
/* Check whether the key with USER_ID is installed. */
static gpg_error_t
command_check_key (const char *userid)
{
gpg_error_t err;
char *addrspec = NULL;
char *fname = NULL;
err = fname_from_userid (userid, &fname, &addrspec);
if (err)
goto leave;
if (access (fname, R_OK))
{
err = gpg_error_from_syserror ();
if (opt_with_file)
es_printf ("%s n %s\n", addrspec, fname);
if (gpg_err_code (err) == GPG_ERR_ENOENT)
{
if (!opt.quiet)
log_info ("key for '%s' is NOT installed\n", addrspec);
log_inc_errorcount ();
err = 0;
}
else
log_error ("error stating '%s': %s\n", fname, gpg_strerror (err));
goto leave;
}
if (opt_with_file)
es_printf ("%s i %s\n", addrspec, fname);
if (opt.verbose)
log_info ("key for '%s' is installed\n", addrspec);
err = 0;
leave:
xfree (fname);
xfree (addrspec);
return err;
}
/* Remove the key with mail address in USERID. */
static gpg_error_t
command_remove_key (const char *userid)
{
gpg_error_t err;
char *addrspec = NULL;
char *fname = NULL;
err = fname_from_userid (userid, &fname, &addrspec);
if (err)
goto leave;
if (gnupg_remove (fname))
{
err = gpg_error_from_syserror ();
if (gpg_err_code (err) == GPG_ERR_ENOENT)
{
if (!opt.quiet)
log_info ("key for '%s' is not installed\n", addrspec);
log_inc_errorcount ();
err = 0;
}
else
log_error ("error removing '%s': %s\n", fname, gpg_strerror (err));
goto leave;
}
if (opt.verbose)
log_info ("key for '%s' removed\n", addrspec);
err = 0;
leave:
xfree (fname);
xfree (addrspec);
return err;
}
/* Revoke the key with mail address MAILADDR. */
static gpg_error_t
command_revoke_key (const char *mailaddr)
{
/* Remove should be different from removing but we have not yet
* defined a suitable way to do this. */
return command_remove_key (mailaddr);
}