diff --git a/configure.ac b/configure.ac index 78a03c420..03f3af9eb 100644 --- a/configure.ac +++ b/configure.ac @@ -506,6 +506,7 @@ AH_BOTTOM([ #endif #define GNUPG_PRIVATE_KEYS_DIR "private-keys-v1.d" #define GNUPG_OPENPGP_REVOC_DIR "openpgp-revocs.d" +#define GNUPG_CACHE_DIR "cache.d" #define GNUPG_DEF_COPYRIGHT_LINE \ "Copyright (C) 2018 Free Software Foundation, Inc." diff --git a/tools/Makefile.am b/tools/Makefile.am index 0c828a7bd..a15427622 100644 --- a/tools/Makefile.am +++ b/tools/Makefile.am @@ -51,7 +51,7 @@ else gpg_wks_server = endif -libexec_PROGRAMS = gpg-wks-client +libexec_PROGRAMS = gpg-wks-client gpg-pair-tool bin_PROGRAMS = gpgconf gpg-connect-agent ${symcryptrun} if !HAVE_W32_SYSTEM @@ -173,6 +173,14 @@ gpg_wks_client_LDADD = $(libcommon) \ $(LIBASSUAN_LIBS) $(LIBGCRYPT_LIBS) $(GPG_ERROR_LIBS) \ $(LIBINTL) $(LIBICONV) +gpg_pair_tool_SOURCES = \ + gpg-pair-tool.c + +gpg_pair_tool_CFLAGS = $(GPG_ERROR_CFLAGS) $(INCICONV) +gpg_pair_tool_LDADD = $(libcommon) \ + $(LIBGCRYPT_LIBS) $(GPG_ERROR_LIBS) \ + $(LIBINTL) $(LIBICONV) + # Make sure that all libs are build before we use them. This is # important for things like make -j2. diff --git a/tools/gpg-pair-tool.c b/tools/gpg-pair-tool.c new file mode 100644 index 000000000..a86bd8e3c --- /dev/null +++ b/tools/gpg-pair-tool.c @@ -0,0 +1,2020 @@ +/* gpg-pair-tool.c - The tool to run the pairing protocol. + * Copyright (C) 2018 g10 Code GmbH + * + * 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 . + */ + +/* Protocol: + * + * Initiator Responder + * | | + * | COMMIT | + * |-------------------->| + * | | + * | DHPART1 | + * |<--------------------| + * | | + * | DHPART2 | + * |-------------------->| + * | | + * | CONFIRM | + * |<--------------------| + * | | + * + * The initiator creates a keypar (PKi,SKi) and sends this COMMIT + * message to the responder: + * + * 7 byte Magic, value: "GPG-pa1" + * 1 byte MessageType, value 1 (COMMIT) + * 8 byte SessionId, value: 8 random bytes + * 1 byte Realm, value 1 + * 2 byte reserved, value 0 + * 5 byte ExpireTime, value: seconds since Epoch as an unsigned int. + * 32 byte Hash(PKi) + * + * The initiator also needs to locally store the sessionid, the realm, + * the expiration time, the keypair and a hash of the entire message + * sent. + * + * The responder checks that the received message has not expired and + * stores sessionid, realm, expiretime and the Hash(PKi). The + * Responder then creates and locally stores its own keypair (PKr,SKr) + * and sends the DHPART1 message back: + * + * 7 byte Magic, value: "GPG-pa1" + * 1 byte MessageType, value 2 (DHPART1) + * 8 byte SessionId from COMMIT message + * 32 byte PKr + * 32 byte Hash(Hash(COMMIT) || DHPART1[0..47]) + * + * Note that Hash(COMMIT) is the hash over the entire received COMMIT + * message. DHPART1[0..47] are the first 48 bytes of the created + * DHPART1 message. + * + * The Initiator receives the DHPART1 message and checks that the hash + * matches. Although this hash is easily malleable it is later in the + * protocol used to assert the integrity of all messages. The + * Initiator then computes the shared master secret from its SKi and + * the received PKr. Using this master secret several keys are + * derived: + * + * - HMACi-key using the label "GPG-pa1-HMACi-key". + * - SYMx-key using the label "GPG-pa1-SYMx-key" + * + * For details on the KDF see the implementation of the function kdf. + * The master secret is stored securily in the local state. The + * DHPART2 message is then created and send to the Responder: + * + * 7 byte Magic, value: "GPG-pa1" + * 1 byte MessageType, value 3 (DHPART2) + * 8 byte SessionId from COMMIT message + * 32 byte PKi + * 32 byte MAC(HMACi-key, Hash(DHPART1) || DHPART2[0..47] || SYMx-key) + * + * The Responder receives the DHPART2 message and checks that the hash + * of the received PKi matches the Hash(PKi) value as received earlier + * with the COMMIT message. The Responder now also computes the + * shared master secret from its SKr and the recived PKi and derives + * the keys: + * + * - HMACi-key using the label "GPG-pa1-HMACi-key". + * - HMACr-key using the label "GPG-pa1-HMACr-key". + * - SYMx-key using the label "GPG-pa1-SYMx-key" + * - SAS using the label "GPG-pa1-SAS" + * + * With these keys the MAC from the received DHPART2 message is + * checked. On success a SAS is displayed to the user and a CONFIRM + * message send back: + * + * 7 byte Magic, value: "GPG-pa1" + * 1 byte MessageType, value 4 (CONFIRM) + * 8 byte SessionId from COMMIT message + * 32 byte MAC(HMACr-key, Hash(DHPART2) || CONFIRM[0..15] || SYMx-key) + * + * The Initiator receives this CONFIRM message, gets the master shared + * secrey from its local state and derives the keys. It checks the + * the MAC in the received CONFIRM message and ask the user to enter + * the SAS as displayed by the responder. Iff the SAS matches the + * master key is flagged as confirmed and the Initiator may now use a + * derived key to send encrypted data to the Responder. + * + * In case the Responder also needs to send encrypted data we need to + * introduce another final message to tell the responder that the + * Initiator validated the SAS. + * + * TODO: Encrypt the state files using a key stored in gpg-agent's cache. + * + */ + + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../common/util.h" +#include "../common/status.h" +#include "../common/i18n.h" +#include "../common/sysutils.h" +#include "../common/init.h" +#include "../common/name-value.h" + + +/* Constants to identify the commands and options. */ +enum cmd_and_opt_values + { + aNull = 0, + + oQuiet = 'q', + oVerbose = 'v', + oOutput = 'o', + oArmor = 'a', + + aInitiate = 400, + aRespond = 401, + aGet = 402, + aCleanup = 403, + + oDebug = 500, + oStatusFD, + oHomedir, + oSAS, + + oDummy + }; + + +/* The list of commands and options. */ +static gpgrt_opt_t opts[] = { + ARGPARSE_group (300, ("@Commands:\n ")), + + ARGPARSE_c (aInitiate, "initiate", N_("initiate a pairing request")), + ARGPARSE_c (aRespond, "respond", N_("respond to a pairing request")), + ARGPARSE_c (aGet, "get", N_("return the keys")), + ARGPARSE_c (aCleanup, "cleanup", N_("remove expired states etc.")), + + ARGPARSE_group (301, ("@\nOptions:\n ")), + + ARGPARSE_s_n (oVerbose, "verbose", N_("verbose")), + ARGPARSE_s_n (oQuiet, "quiet", N_("be somewhat more quiet")), + ARGPARSE_s_n (oArmor, "armor", N_("create ascii armored output")), + ARGPARSE_s_s (oSAS, "sas", N_("|SAS|the SAS as shown by the peer")), + ARGPARSE_s_s (oDebug, "debug", "@"), + ARGPARSE_s_s (oOutput, "output", N_("|FILE|write the request to FILE")), + ARGPARSE_s_i (oStatusFD, "status-fd", N_("|FD|write status info to this FD")), + + ARGPARSE_s_s (oHomedir, "homedir", "@"), + + ARGPARSE_end () +}; + + +/* We keep all global options in the structure OPT. */ +static struct +{ + int verbose; + unsigned int debug; + int quiet; + int armor; + const char *output; + estream_t statusfp; + unsigned int ttl; + const char *sas; +} opt; + + +/* Debug values and macros. */ +#define DBG_MESSAGE_VALUE 2 /* Debug the messages. */ +#define DBG_CRYPTO_VALUE 4 /* Debug low level crypto. */ +#define DBG_MEMORY_VALUE 32 /* Debug memory allocation stuff. */ + +#define DBG_MESSAGE (opt.debug & DBG_MESSAGE_VALUE) +#define DBG_CRYPTO (opt.debug & DBG_CRYPTO_VALUE) + + +/* The list of supported debug flags. */ +static struct debug_flags_s debug_flags [] = + { + { DBG_MESSAGE_VALUE, "message" }, + { DBG_CRYPTO_VALUE , "crypto" }, + { DBG_MEMORY_VALUE , "memory" }, + { 0, NULL } + }; + + +/* The directory name below the cache dir to store paring states. */ +#define PAIRING_STATE_DIR "state" + +/* Message types. */ +#define MSG_TYPE_COMMIT 1 +#define MSG_TYPE_DHPART1 2 +#define MSG_TYPE_DHPART2 3 +#define MSG_TYPE_CONFIRM 4 + + +/* Realm values. */ +#define REALM_STANDARD 1 + + + + +/* Local prototypes. */ +static void wrong_args (const char *text) GPGRT_ATTR_NORETURN; +static void xnvc_set_printf (nvc_t nvc, const char *name, const char *format, + ...) GPGRT_ATTR_PRINTF(3,4); +static void *hash_data (void *result, size_t resultsize, + ...) GPGRT_ATTR_SENTINEL(0); +static void *hmac_data (void *result, size_t resultsize, + const unsigned char *key, size_t keylen, + ...) GPGRT_ATTR_SENTINEL(0); + + +static gpg_error_t command_initiate (void); +static gpg_error_t command_respond (void); +static gpg_error_t command_cleanup (void); +static gpg_error_t command_get (const char *sessionidstr); + + + + +/* Print usage information and provide strings for help. */ +static const char * +my_strusage( int level ) +{ + const char *p; + + switch (level) + { + case 9: p = "LGPL-2.1-or-later"; break; + case 11: p = "gpg-pair-tool"; break; + case 12: p = "@GNUPG@"; break; + case 13: p = VERSION; break; + case 14: p = GNUPG_DEF_COPYRIGHT_LINE; 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-pair-tool [command] [options] [args] (-h for help)"); + break; + case 41: + p = ("Syntax: gpg-pair-tool [command] [options] [args]\n" + "Client to run the pairing 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); +} + + +/* Set the status FD. */ +static void +set_status_fd (int fd) +{ + static int last_fd = -1; + + if (fd != -1 && last_fd == fd) + return; + + if (opt.statusfp && opt.statusfp != es_stdout && opt.statusfp != es_stderr) + es_fclose (opt.statusfp); + opt.statusfp = NULL; + if (fd == -1) + return; + + if (fd == 1) + opt.statusfp = es_stdout; + else if (fd == 2) + opt.statusfp = es_stderr; + else + opt.statusfp = es_fdopen (fd, "w"); + if (!opt.statusfp) + { + log_fatal ("can't open fd %d for status output: %s\n", + fd, gpg_strerror (gpg_error_from_syserror ())); + } + last_fd = fd; +} + + +/* Write a status line with code NO followed by the outout of the + * printf style FORMAT. The caller needs to make sure that LFs and + * CRs are not printed. */ +static void +write_status (int no, const char *format, ...) +{ + va_list arg_ptr; + + if (!opt.statusfp) + return; /* Not enabled. */ + + es_fputs ("[GNUPG:] ", opt.statusfp); + es_fputs (get_status_string (no), opt.statusfp); + if (format) + { + es_putc (' ', opt.statusfp); + va_start (arg_ptr, format); + es_vfprintf (opt.statusfp, format, arg_ptr); + va_end (arg_ptr); + } + es_putc ('\n', opt.statusfp); +} + + + +/* gpg-pair-tool main. */ +int +main (int argc, char **argv) +{ + gpg_error_t err; + gpgrt_argparse_t pargs = { &argc, &argv }; + enum cmd_and_opt_values cmd = 0; + + opt.ttl = 8*3600; /* Default to 8 hours. */ + + gnupg_reopen_std ("gpg-pair-tool"); + gpgrt_set_strusage (my_strusage); + log_set_prefix ("gpg-pair-tool", GPGRT_LOG_WITH_PREFIX); + + /* Make sure that our subsystems are ready. */ + i18n_init(); + init_common_subsystems (&argc, &argv); + + /* Parse the command line. */ + while (gpgrt_argparse (NULL, &pargs, opts)) + { + switch (pargs.r_opt) + { + case oQuiet: opt.quiet = 1; break; + case oVerbose: opt.verbose++; break; + case oArmor: opt.armor = 1; 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 oOutput: + opt.output = pargs.r.ret_str; + break; + + case oStatusFD: + set_status_fd (translate_sys2libc_fd_int (pargs.r.ret_int, 1)); + break; + + case oHomedir: + gnupg_set_homedir (pargs.r.ret_str); + break; + + case oSAS: + opt.sas = pargs.r.ret_str; + break; + + case aInitiate: + case aRespond: + case aGet: + case aCleanup: + if (cmd && cmd != pargs.r_opt) + log_error (_("conflicting commands\n")); + else + cmd = pargs.r_opt; + break; + + default: pargs.err = ARGPARSE_PRINT_WARNING; break; + } + } + + /* 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]); + } + gpgrt_argparse (NULL, &pargs, NULL); /* Free internal memory. */ + + if (opt.sas) + { + if (strlen (opt.sas) != 11 + || !digitp (opt.sas+0) || !digitp (opt.sas+1) || !digitp (opt.sas+2) + || opt.sas[3] != '-' + || !digitp (opt.sas+4) || !digitp (opt.sas+5) || !digitp (opt.sas+6) + || opt.sas[7] != '-' + || !digitp (opt.sas+8) || !digitp (opt.sas+9) || !digitp (opt.sas+10)) + log_error ("invalid formatted SAS\n"); + } + + /* Stop if any error, inclduing ARGPARSE_PRINT_WARNING, occurred. */ + if (log_get_errorcount (0)) + exit (2); + + if (DBG_CRYPTO) + gcry_control (GCRYCTL_SET_DEBUG_FLAGS, 1|2); + + + /* Now run the requested command. */ + switch (cmd) + { + case aInitiate: + if (argc) + wrong_args ("--initiate"); + err = command_initiate (); + break; + + case aRespond: + if (argc) + wrong_args ("--respond"); + err = command_respond (); + break; + + case aGet: + if (argc > 1) + wrong_args ("--respond [sessionid]"); + err = command_get (argc? *argv:NULL); + break; + + case aCleanup: + if (argc) + wrong_args ("--cleanup"); + err = command_cleanup (); + break; + + default: + gpgrt_usage (1); + err = 0; + break; + } + + if (err) + write_status (STATUS_FAILURE, "- %u", err); + else if (log_get_errorcount (0)) + write_status (STATUS_FAILURE, "- %u", GPG_ERR_GENERAL); + else + write_status (STATUS_SUCCESS, NULL); + return log_get_errorcount (0)? 1:0; +} + + + +/* Wrapper around nvc_new which terminates in the error case. */ +static nvc_t +xnvc_new (void) +{ + nvc_t c = nvc_new (); + if (!c) + log_fatal ("error creating NVC object: %s\n", + gpg_strerror (gpg_error_from_syserror ())); + return c; +} + +/* Wrapper around nvc_set which terminates in the error case. */ +static void +xnvc_set (nvc_t nvc, const char *name, const char *value) +{ + gpg_error_t err = nvc_set (nvc, name, value); + if (err) + log_fatal ("error updating NVC object: %s\n", gpg_strerror (err)); +} + +/* Call vnc_set with (BUFFER, BUFLEN) converted to a hex string as + * value. Terminates in the error case. */ +static void +xnvc_set_hex (nvc_t nvc, const char *name, const void *buffer, size_t buflen) +{ + char *hex; + + hex = bin2hex (buffer, buflen, NULL); + if (!hex) + xoutofcore (); + strlwr (hex); + xnvc_set (nvc, name, hex); + xfree (hex); +} + +/* Call nvc_set with a value created from the string generated using + * the printf style FORMAT. Terminates in the error case. */ +static void +xnvc_set_printf (nvc_t nvc, const char *name, const char *format, ...) +{ + va_list arg_ptr; + char *buffer; + + va_start (arg_ptr, format); + if (gpgrt_vasprintf (&buffer, format, arg_ptr) < 0) + log_fatal ("estream_asprintf failed: %s\n", + gpg_strerror (gpg_error_from_syserror ())); + va_end (arg_ptr); + xnvc_set (nvc, name, buffer); + xfree (buffer); +} + + +/* Return the string for the first entry in NVC with NAME. If NAME is + * missing, an empty string is returned. The returned string is a + * pointer into NVC. */ +static const char * +xnvc_get_string (nvc_t nvc, const char *name) +{ + nve_t item; + + if (!nvc) + return ""; + item = nvc_lookup (nvc, name); + if (!item) + return ""; + return nve_value (item); +} + + + +/* Return a string for MSGTYPE. */ +const char * +msgtypestr (int msgtype) +{ + switch (msgtype) + { + case MSG_TYPE_COMMIT: return "Commit"; + case MSG_TYPE_DHPART1: return "DHPart1"; + case MSG_TYPE_DHPART2: return "DHPart2"; + case MSG_TYPE_CONFIRM: return "Confirm"; + } + return "?"; +} + + +/* Private to {get,set}_session_id(). */ +static struct { + int initialized; + unsigned char sessid[8]; +} session_id; + + +/* Return the 8 octet session. */ +static unsigned char * +get_session_id (void) +{ + if (!session_id.initialized) + { + session_id.initialized = 1; + gcry_create_nonce (session_id.sessid, sizeof session_id.sessid); + } + + return session_id.sessid; +} + +static void +set_session_id (const void *sessid, size_t len) +{ + log_assert (!session_id.initialized); + if (len > sizeof session_id.sessid) + len = sizeof session_id.sessid; + memcpy (session_id.sessid, sessid, len); + if (len < sizeof session_id.sessid) + memset (session_id.sessid+len, 0, sizeof session_id.sessid - len); + session_id.initialized = 1; +} + +/* Return a string with the hexified session id. */ +static const char * +get_session_id_hex (void) +{ + static char hexstr[16+1]; + + bin2hex (get_session_id (), 8, hexstr); + strlwr (hexstr); + return hexstr; +} + + +/* Return a fixed string with the directory used to store the state of + * pairings. On error a diagnostic is printed but the file name is + * returned anyway. It is expected that the expected failure of the + * following open is responsible for error handling. */ +static const char * +get_pairing_statedir (void) +{ + static char *fname; + gpg_error_t err = 0; + char *tmpstr; + struct stat statbuf; + + if (fname) + return fname; + + fname = make_filename (gnupg_homedir (), GNUPG_CACHE_DIR, NULL); + if (stat (fname, &statbuf) && errno == ENOENT) + { + if (gnupg_mkdir (fname, "-rwx")) + { + err = gpg_error_from_syserror (); + log_error (_("can't create directory '%s': %s\n"), + fname, gpg_strerror (err) ); + } + else if (!opt.quiet) + log_info (_("directory '%s' created\n"), fname); + } + + tmpstr = make_filename (fname, PAIRING_STATE_DIR, NULL); + xfree (fname); + fname = tmpstr; + if (stat (fname, &statbuf) && errno == ENOENT) + { + if (gnupg_mkdir (fname, "-rwx")) + { + if (!err) + { + err = gpg_error_from_syserror (); + log_error (_("can't create directory '%s': %s\n"), + fname, gpg_strerror (err) ); + } + } + else if (!opt.quiet) + log_info (_("directory '%s' created\n"), fname); + } + + return fname; +} + + +/* Open the pairing state file. SESSIONID is a 8 byte buffer with the + * session-id. If CREATE_FLAG is set the file is created and will + * always return a valid stream. If CREATE_FLAG is not set the file + * is opened for reading and writing. If the file does not exist NULL + * is return; in all other error cases the process is terminated. If + * R_FNAME is not NULL the name of the file is stored there and the + * caller needs to free it. */ +static estream_t +open_pairing_state (const unsigned char *sessionid, int create_flag, + char **r_fname) +{ + gpg_error_t err; + char *fname, *tmpstr; + estream_t fp; + + /* The filename is the session id with a "pa1" suffix. Note that + * the state dir may eventually be used for other purposes as well + * and thus the suffix identifies that the file belongs to this + * tool. We use lowercase file names for no real reason. */ + tmpstr = bin2hex (sessionid, 8, NULL); + if (!tmpstr) + xoutofcore (); + strlwr (tmpstr); + fname = xstrconcat (tmpstr, ".pa1", NULL); + xfree (tmpstr); + tmpstr = make_filename (get_pairing_statedir (), fname, NULL); + xfree (fname); + fname = tmpstr; + + fp = es_fopen (fname, create_flag? "wbx,mode=-rw": "rb+,mode=-rw"); + if (!fp) + { + err = gpg_error_from_syserror (); + if (create_flag) + { + /* We should always be able to create a file. Also we use a + * 64 bit session id, it is theoretically possible that such + * a session already exists. However, that is rare enough + * and thus the fatal error message should still be okay. */ + log_fatal ("can't create '%s': %s\n", fname, gpg_strerror (err)); + } + else if (gpg_err_code (err) == GPG_ERR_ENOENT) + { + /* That is an expected error; return NULL. */ + } + else + { + log_fatal ("can't open '%s': %s\n", fname, gpg_strerror (err)); + } + } + + if (r_fname) + *r_fname = fname; + else + xfree (fname); + + return fp; +} + + +/* Write the state to a possible new state file. */ +static void +write_state (nvc_t state, int create_flag) +{ + gpg_error_t err; + char *fname = NULL; + estream_t fp; + + fp = open_pairing_state (get_session_id (), create_flag, &fname); + log_assert (fp); + + err = nvc_write (state, fp); + if (err) + { + es_fclose (fp); + gnupg_remove (fname); + log_fatal ("error writing '%s': %s\n", fname, gpg_strerror (err)); + } + + /* If we did not create the file, we need to truncate the file. */ + if (!create_flag && ftruncate (es_fileno (fp), es_ftello (fp))) + { + err = gpg_error_from_syserror (); + log_fatal ("error truncating '%s': %s\n", fname, gpg_strerror (err)); + } + if (es_ferror (fp) || es_fclose (fp)) + { + err = gpg_error_from_syserror (); + es_fclose (fp); + gnupg_remove (fname); + log_fatal ("error writing '%s': %s\n", fname, gpg_strerror (err)); + } +} + + +/* Read the state into a newly allocated state object and store that + * at R_STATE. If no state is available GPG_ERR_NOT_FOUND is returned + * and as with all errors NULL is tored at R_STATE. SESSIONID is an + * input with the 8 session id. */ +static gpg_error_t +read_state (nvc_t *r_state) +{ + gpg_error_t err; + char *fname = NULL; + estream_t fp; + nvc_t state = NULL; + nve_t item; + const char *value; + unsigned long expire; + + *r_state = NULL; + + fp = open_pairing_state (get_session_id (), 0, &fname); + if (!fp) + return gpg_error (GPG_ERR_NOT_FOUND); + + err = nvc_parse (&state, NULL, fp); + if (err) + { + log_info ("failed to parse state file '%s': %s\n", + fname, gpg_strerror (err)); + goto leave; + } + + /* Check whether the state already expired. */ + item = nvc_lookup (state, "Expires:"); + if (!item) + { + log_info ("invalid state file '%s': %s\n", + fname, "field 'expire' not found"); + goto leave; + } + value = nve_value (item); + if (!value || !(expire = strtoul (value, NULL, 10))) + { + log_info ("invalid state file '%s': %s\n", + fname, "field 'expire' has an invalid value"); + goto leave; + } + if (expire <= gnupg_get_time ()) + { + es_fclose (fp); + fp = NULL; + if (gnupg_remove (fname)) + { + err = gpg_error_from_syserror (); + log_info ("failed to delete state file '%s': %s\n", + fname, gpg_strerror (err)); + } + else if (opt.verbose) + log_info ("state file '%s' deleted\n", fname); + err = gpg_error (GPG_ERR_NOT_FOUND); + goto leave; + } + + *r_state = state; + state = NULL; + + leave: + nvc_release (state); + es_fclose (fp); + return err; +} + + +/* Send (MSG,MSGLEN) to the output device. */ +static void +send_message (const unsigned char *msg, size_t msglen) +{ + gpg_error_t err; + + if (opt.verbose) + log_info ("session %s: sending %s message\n", + get_session_id_hex (), msgtypestr (msg[7])); + + if (DBG_MESSAGE) + log_printhex (msg, msglen, "send msg(%s):", msgtypestr (msg[7])); + + /* FIXME: For now only stdout. */ + if (opt.armor) + { + gpgrt_b64state_t state; + + state = gpgrt_b64enc_start (es_stdout, ""); + if (!state) + log_fatal ("error setting up base64 encoder: %s\n", + gpg_strerror (gpg_error_from_syserror ())); + err = gpgrt_b64enc_write (state, msg, msglen); + if (!err) + err = gpgrt_b64enc_finish (state); + if (err) + log_fatal ("error writing base64 to stdout: %s\n", gpg_strerror (err)); + } + else + { + if (es_fwrite (msg, msglen, 1, es_stdout) != 1) + log_fatal ("error writing to stdout: %s\n", + gpg_strerror (gpg_error_from_syserror ())); + } + es_fputc ('\n', es_stdout); +} + + +/* Read a message from stdin and store it at the address (R_MSG, + * R_MSGLEN). This function detects armoring and removes it. On + * error NULL is stored at R_MSG, a diagnostic printed and an error + * code returned. The returned message has a proper message type and + * an appropriate length. The message type is stored at R_MSGTYPE and + * if a state is availabale it is stored at R_STATE. */ +static gpg_error_t +read_message (unsigned char **r_msg, size_t *r_msglen, int *r_msgtype, + nvc_t *r_state) +{ + gpg_error_t err; + unsigned char msg[128]; /* max msg size is 80 but 107 with base64. */ + size_t msglen; + size_t reqlen; + + *r_msg = NULL; + *r_state = NULL; + + es_setvbuf (es_stdin, NULL, _IONBF, 0); + es_set_binary (es_stdin); + + if (es_read (es_stdin, msg, sizeof msg, &msglen)) + { + err = gpg_error_from_syserror (); + log_error ("error reading from message: %s\n", gpg_strerror (err)); + return err; + } + + if (msglen > 4 && !memcmp (msg, "R1BH", 4)) + { + /* This is base64 of the first 3 bytes. */ + gpgrt_b64state_t state = gpgrt_b64dec_start (NULL); + if (!state) + log_fatal ("error setting up base64 decoder: %s\n", + gpg_strerror (gpg_error_from_syserror ())); + err = gpgrt_b64dec_proc (state, msg, msglen, &msglen); + gpgrt_b64dec_finish (state); + if (err) + { + log_error ("error decoding message: %s\n", gpg_strerror (err)); + return err; + } + } + + if (msglen < 16 || memcmp (msg, "GPG-pa1", 7)) + { + log_error ("error parsing message: %s\n", + msglen? "invalid header":"empty message"); + return gpg_error (GPG_ERR_INV_RESPONSE); + } + switch (msg[7]) + { + case MSG_TYPE_COMMIT: reqlen = 56; break; + case MSG_TYPE_DHPART1: reqlen = 80; break; + case MSG_TYPE_DHPART2: reqlen = 80; break; + case MSG_TYPE_CONFIRM: reqlen = 48; break; + + default: + log_error ("error parsing message: %s\n", "invalid message type"); + return gpg_error (GPG_ERR_INV_RESPONSE); + } + if (msglen < reqlen) + { + log_error ("error parsing message: %s\n", "message too short"); + return gpg_error (GPG_ERR_INV_RESPONSE); + } + + if (DBG_MESSAGE) + log_printhex (msg, msglen, "recv msg(%s):", msgtypestr (msg[7])); + + /* Note that we ignore any garbage at the end of a message. */ + msglen = reqlen; + + set_session_id (msg+8, 8); + + if (opt.verbose) + log_info ("session %s: received %s message\n", + get_session_id_hex (), msgtypestr (msg[7])); + + /* Read the state. */ + err = read_state (r_state); + if (err && gpg_err_code (err) != GPG_ERR_NOT_FOUND) + return err; + + *r_msg = xmalloc (msglen); + memcpy (*r_msg, msg, msglen); + *r_msglen = msglen; + *r_msgtype = msg[7]; + return err; +} + + +/* Display the Short Authentication String (SAS). If WAIT is true the + * function waits until the user has entered the SAS as seen at the + * peer. + * + * To construct the SAS we take the 4 most significant octets of HASH, + * interpret them as a 32 bit big endian unsigned integer, divide that + * integer by 10^9 and take the remainder. The remainder is displayed + * as 3 groups of 3 decimal digits delimited by a hyphens. This gives + * a search space of close to 2^30 and is still easy to compare. + */ +static gpg_error_t +display_sas (const unsigned char *hash, size_t hashlen, int wait) +{ + gpg_error_t err; + unsigned long sas = 0; + char sasbuf[12]; + + log_assert (hashlen >= 4); + + sas |= (unsigned long)hash[20] << 24; + sas |= (unsigned long)hash[21] << 16; + sas |= (unsigned long)hash[22] << 8; + sas |= (unsigned long)hash[23]; + sas %= 1000000000ul; + snprintf (sasbuf, sizeof sasbuf, "%09lu", sas); + memmove (sasbuf+8, sasbuf+6, 3); + memmove (sasbuf+4, sasbuf+3, 3); + sasbuf[3] = sasbuf[7] = '-'; + sasbuf[11] = 0; + + if (wait) + log_info ("Please check the SAS:\n"); + else + log_info ("Please note the SAS:\n"); + log_info ("\n"); + log_info (" %s\n", sasbuf); + log_info ("\n"); + + if (wait) + { + if (!opt.sas || strcmp (sasbuf, opt.sas)) + err = gpg_error (GPG_ERR_NOT_CONFIRMED); + else + log_info ("SAS confirmed\n"); + } + + if (err) + log_info ("checking SAS failed: %s\n", gpg_strerror (err)); + return err; +} + + + +static gpg_error_t +create_dh_keypair (unsigned char *dh_secret, size_t dh_secret_len, + unsigned char *dh_public, size_t dh_public_len) +{ + gpg_error_t err; + gcry_sexp_t sexp; + gcry_sexp_t s_keypair; + gcry_buffer_t secret; + gcry_buffer_t public; + unsigned char publicbuf[33]; + + /* We need a temporary buffer for the public key. Check the length + * for the later memcpy. */ + if (dh_public_len < 32) + return gpg_error (GPG_ERR_BUFFER_TOO_SHORT); + + secret.size = dh_secret_len; + secret.data = dh_secret; + secret.off = 0; + public.size = sizeof publicbuf; + public.data = publicbuf; + public.off = 0; + + err = gcry_sexp_build (&sexp, NULL, + "(genkey(ecc(curve Curve25519)(flags djb-tweak)))"); + if (err) + return err; + err = gcry_pk_genkey (&s_keypair, sexp); + gcry_sexp_release (sexp); + if (err) + return err; + err = gcry_sexp_extract_param (s_keypair, "key-data!private-key", + "&dq", &secret, &public, NULL); + gcry_sexp_release (s_keypair); + if (err) + return err; + + /* Gcrypt prepends a 0x40 indicator - remove that. */ + if (public.len == 33) + { + public.len = 32; + memmove (public.data, publicbuf+1, 32); + } + memcpy (dh_public, public.data, public.len); + + if (DBG_CRYPTO) + { + log_printhex (secret.data, secret.len, "DH secret:"); + log_printhex (public.data, public.len, "DH public:"); + } + + return 0; +} + + +/* SHA256 the data given as varargs tuples of (const void*, size_t) + * and store the result in RESULT. The end of the list is indicated + * by a NULL element in a tuple. RESULTLEN gives the length of the + * RESULT buffer which must be at least 32. Note that the second item + * of the tuple is the length and it is a size_t. */ +static void * +hash_data (void *result, size_t resultsize, ...) +{ + va_list arg_ptr; + gpg_error_t err; + gcry_md_hd_t hd; + const void *data; + size_t datalen; + + log_assert (resultsize >= 32); + + err = gcry_md_open (&hd, GCRY_MD_SHA256, 0); + if (err) + log_fatal ("error creating a Hash handle: %s\n", gpg_strerror (err)); + /* log_printhex ("", 0, "Hash-256:"); */ + + va_start (arg_ptr, resultsize); + while ((data = va_arg (arg_ptr, const void *))) + { + datalen = va_arg (arg_ptr, size_t); + /* log_printhex (data, datalen, " data:"); */ + gcry_md_write (hd, data, datalen); + } + va_end (arg_ptr); + + memcpy (result, gcry_md_read (hd, 0), 32); + /* log_printhex (result, 32, " result:"); */ + + gcry_md_close (hd); + + return result; +} + + +/* HMAC-SHA256 the data given as varargs tuples of (const void*, + * size_t) using (KEYLEN,KEY) and store the result in RESULT. The end + * of the list is indicated by a NULL element in a tuple. RESULTLEN + * gives the length of the RESULT buffer which must be at least 32. + * Note that the second item of the tuple is the length and it is a + * size_t. */ +static void * +hmac_data (void *result, size_t resultsize, + const unsigned char *key, size_t keylen, ...) +{ + va_list arg_ptr; + gpg_error_t err; + gcry_mac_hd_t hd; + const void *data; + size_t datalen; + + log_assert (resultsize >= 32); + + err = gcry_mac_open (&hd, GCRY_MAC_HMAC_SHA256, 0, NULL); + if (err) + log_fatal ("error creating a MAC handle: %s\n", gpg_strerror (err)); + err = gcry_mac_setkey (hd, key, keylen); + if (err) + log_fatal ("error setting the MAC key: %s\n", gpg_strerror (err)); + /* log_printhex (key, keylen, "HMAC-key:"); */ + + va_start (arg_ptr, keylen); + while ((data = va_arg (arg_ptr, const void *))) + { + datalen = va_arg (arg_ptr, size_t); + /* log_printhex (data, datalen, " data:"); */ + err = gcry_mac_write (hd, data, datalen); + if (err) + log_fatal ("error writing to the MAC handle: %s\n", gpg_strerror (err)); + } + va_end (arg_ptr); + + err = gcry_mac_read (hd, result, &resultsize); + if (err || resultsize != 32) + log_fatal ("error reading MAC value: %s\n", gpg_strerror (err)); + /* log_printhex (result, resultsize, " result:"); */ + + gcry_mac_close (hd); + + return result; +} + + +/* Key derivation function: + * + * FIXME(doc) + */ +static void +kdf (unsigned char *result, size_t resultlen, + const unsigned char *master, size_t masterlen, + const unsigned char *sessionid, size_t sessionidlen, + const unsigned char *expire, size_t expirelen, + const char *label) +{ + log_assert (masterlen == 32 && sessionidlen == 8 && expirelen == 5); + log_assert (*label); + log_assert (resultlen == 32); + + hmac_data (result, resultlen, master, masterlen, + "\x00\x00\x00\x01", (size_t)4, /* Counter=1*/ + label, strlen (label) + 1, /* Label, 0x00 */ + sessionid, sessionidlen, /* Context */ + expire, expirelen, /* Context */ + "\x00\x00\x01\x00", (size_t)4, /* L=256 */ + NULL); +} + + +static gpg_error_t +compute_master_secret (unsigned char *master, size_t masterlen, + const unsigned char *sk_a, size_t sk_a_len, + const unsigned char *pk_b, size_t pk_b_len) +{ + gpg_error_t err; + gcry_sexp_t s_sk_a = NULL; + gcry_sexp_t s_pk_b = NULL; + gcry_sexp_t s_shared = NULL; + gcry_sexp_t s_tmp; + const char *s; + size_t n; + + log_assert (masterlen == 32); + + err = gcry_sexp_build (&s_sk_a, NULL, "%b", (int)sk_a_len, sk_a); + if (!err) + err = gcry_sexp_build (&s_pk_b, NULL, + "(public-key(ecdh(curve Curve25519)" + " (flags djb-tweak)(q%b)))", + (int)pk_b_len, pk_b); + if (err) + { + log_error ("error building S-expression: %s\n", gpg_strerror (err)); + goto leave; + } + + err = gcry_pk_encrypt (&s_shared, s_sk_a, s_pk_b); + if (err) + { + log_error ("error computing DH: %s\n", gpg_strerror (err)); + goto leave; + } + /* gcry_log_debugsxp ("sk_a", s_sk_a); */ + /* gcry_log_debugsxp ("pk_b", s_pk_b); */ + /* gcry_log_debugsxp ("shared", s_shared); */ + + s_tmp = gcry_sexp_find_token (s_shared, "s", 0); + if (!s_tmp || !(s = gcry_sexp_nth_data (s_tmp, 1, &n)) + || n != 33 || s[0] != 0x40) + { + err = gpg_error (GPG_ERR_INTERNAL); + log_error ("error computing DH: %s\n", gpg_strerror (err)); + goto leave; + } + memcpy (master, s+1, 32); + + + leave: + gcry_sexp_release (s_sk_a); + gcry_sexp_release (s_pk_b); + gcry_sexp_release (s_shared); + return err; +} + + +/* We are the Initiator: Create the commit message. This function + * sends the COMMIT message and writes STATE. */ +static gpg_error_t +make_msg_commit (nvc_t state) +{ + gpg_error_t err; + uint64_t now, expire; + unsigned char secret[32]; + unsigned char public[32]; + unsigned char *newmsg; + size_t newmsglen; + unsigned char tmphash[32]; + + err = create_dh_keypair (secret, sizeof secret, public, sizeof public ); + if (err) + log_error ("creating DH keypair failed: %s\n", gpg_strerror (err)); + + now = gnupg_get_time (); + expire = now + opt.ttl; + + newmsglen = 7+1+8+1+2+5+32; + newmsg = xmalloc (newmsglen); + memcpy (newmsg+0, "GPG-pa1", 7); + newmsg[7] = MSG_TYPE_COMMIT; + memcpy (newmsg+8, get_session_id (), 8); + newmsg[16] = REALM_STANDARD; + newmsg[17] = 0; + newmsg[18] = 0; + newmsg[19] = expire >> 32; + newmsg[20] = expire >> 24; + newmsg[21] = expire >> 16; + newmsg[22] = expire >> 8; + newmsg[23] = expire; + gcry_md_hash_buffer (GCRY_MD_SHA256, newmsg+24, public, 32); + + /* Create the state file. */ + xnvc_set (state, "State:", "Commit-sent"); + xnvc_set_printf (state, "Created:", "%llu", (unsigned long long)now); + xnvc_set_printf (state, "Expires:", "%llu", (unsigned long long)expire); + xnvc_set_hex (state, "DH-PKi:", public, 32); + xnvc_set_hex (state, "DH-SKi:", secret, 32); + gcry_md_hash_buffer (GCRY_MD_SHA256, tmphash, newmsg, newmsglen); + xnvc_set_hex (state, "Hash-Commit:", tmphash, 32); + + /* Write the state. Note that we need to create it. The state + * updating should in theory be done atomically with send_message. + * However, we can't assure that the message will actually be + * delivered and thus it doesn't matter whether we have an already + * update state when we later fail in send_message. */ + write_state (state, 1); + + /* Write the message. */ + send_message (newmsg, newmsglen); + + xfree (newmsg); + return err; +} + + +/* We are the Responder: Process a commit message in (MSG,MSGLEN) + * which has already been validated to have a correct header and + * message type. Sends the DHPart1 message and writes STATE. */ +static gpg_error_t +proc_msg_commit (nvc_t state, const unsigned char *msg, size_t msglen) +{ + gpg_error_t err; + uint64_t now, expire; + unsigned char tmphash[32]; + unsigned char secret[32]; + unsigned char public[32]; + unsigned char *newmsg = NULL; + size_t newmsglen; + + log_assert (msglen >= 56); + now = gnupg_get_time (); + + /* Check that the message has not expired. */ + expire = (uint64_t)msg[19] << 32; + expire |= (uint64_t)msg[20] << 24; + expire |= (uint64_t)msg[21] << 16; + expire |= (uint64_t)msg[22] << 8; + expire |= (uint64_t)msg[23]; + if (expire < now) + { + log_error ("received %s message is too old\n", + msgtypestr (MSG_TYPE_COMMIT)); + err = gpg_error (GPG_ERR_TOO_OLD); + goto leave; + } + + /* Create the response. */ + err = create_dh_keypair (secret, sizeof secret, public, sizeof public ); + if (err) + { + log_error ("creating DH keypair failed: %s\n", gpg_strerror (err)); + goto leave; + } + + newmsglen = 7+1+8+32+32; + newmsg = xmalloc (newmsglen); + memcpy (newmsg+0, "GPG-pa1", 7); + newmsg[7] = MSG_TYPE_DHPART1; + memcpy (newmsg+8, msg + 8, 8); /* SessionID. */ + memcpy (newmsg+16, public, 32); /* PKr */ + /* Hash(Hash(Commit) || DHPart1[0..47]) */ + gcry_md_hash_buffer (GCRY_MD_SHA256, tmphash, msg, msglen); + hash_data (newmsg+48, 32, + tmphash, sizeof tmphash, + newmsg, (size_t)48, + NULL); + + /* Update the state. */ + xnvc_set (state, "State:", "DHPart1-sent"); + xnvc_set_printf (state, "Created:", "%llu", (unsigned long long)now); + xnvc_set_printf (state, "Expires:", "%llu", (unsigned long long)expire); + xnvc_set_hex (state, "Hash-PKi:", msg+24, 32); + xnvc_set_hex (state, "DH-PKr:", public, 32); + xnvc_set_hex (state, "DH-SKr:", secret, 32); + gcry_md_hash_buffer (GCRY_MD_SHA256, tmphash, newmsg, newmsglen); + xnvc_set_hex (state, "Hash-DHPart1:", tmphash, 32); + + /* Write the state. Note that we need to create it. */ + write_state (state, 1); + + /* Write the message. */ + send_message (newmsg, newmsglen); + + leave: + xfree (newmsg); + return err; +} + + +/* We are the Initiator: Process a DHPART1 message in (MSG,MSGLEN) + * which has already been validated to have a correct header and + * message type. Sends the DHPart2 message and writes STATE. */ +static gpg_error_t +proc_msg_dhpart1 (nvc_t state, const unsigned char *msg, size_t msglen) +{ + gpg_error_t err; + unsigned char hash[32]; + unsigned char tmphash[32]; + unsigned char pki[32]; + unsigned char pkr[32]; + unsigned char ski[32]; + unsigned char master[32]; + uint64_t expire; + unsigned char expirebuf[5]; + unsigned char hmacikey[32]; + unsigned char symxkey[32]; + unsigned char *newmsg = NULL; + size_t newmsglen; + + log_assert (msglen >= 80); + + /* Check that the message includes the Hash(Commit). */ + if (hex2bin (xnvc_get_string (state, "Hash-Commit:"), hash, sizeof hash) < 0) + { + err = gpg_error (GPG_ERR_INV_VALUE); + log_error ("no or garbled 'Hash-Commit' in our state file\n"); + goto leave; + } + hash_data (tmphash, 32, + hash, sizeof hash, + msg, (size_t)48, + NULL); + if (memcmp (msg+48, tmphash, 32)) + { + err = gpg_error (GPG_ERR_BAD_DATA); + log_error ("manipulation of received %s message detected: %s\n", + msgtypestr (MSG_TYPE_DHPART1), "Bad Hash"); + goto leave; + } + /* Check that the received PKr is different from our PKi and copy + * PKr into PKR. */ + if (hex2bin (xnvc_get_string (state, "DH-PKi:"), pki, sizeof pki) < 0) + { + err = gpg_error (GPG_ERR_INV_VALUE); + log_error ("no or garbled 'DH-PKi' in our state file\n"); + goto leave; + } + if (!memcmp (msg+16, pki, 32)) + { + /* This can only happen if the state file leaked to the + * responder. */ + err = gpg_error (GPG_ERR_BAD_DATA); + log_error ("received our own public key PKi instead of PKr\n"); + goto leave; + } + memcpy (pkr, msg+16, 32); + + /* Put the expire value into a buffer. */ + expire = string_to_u64 (xnvc_get_string (state, "Expires:")); + if (!expire) + { + err = gpg_error (GPG_ERR_INV_VALUE); + log_error ("no 'Expire' in our state file\n"); + goto leave; + } + expirebuf[0] = expire >> 32; + expirebuf[1] = expire >> 24; + expirebuf[2] = expire >> 16; + expirebuf[3] = expire >> 8; + expirebuf[4] = expire; + + /* Get our secret from the state. */ + if (hex2bin (xnvc_get_string (state, "DH-SKi:"), ski, sizeof ski) < 0) + { + err = gpg_error (GPG_ERR_INV_VALUE); + log_error ("no or garbled 'DH-SKi' in our state file\n"); + goto leave; + } + + /* Compute the shared secrets. */ + err = compute_master_secret (master, sizeof master, + ski, sizeof ski, pkr, sizeof pkr); + if (err) + { + log_error ("creating DH keypair failed: %s\n", gpg_strerror (err)); + goto leave; + } + + kdf (hmacikey, sizeof hmacikey, + master, sizeof master, msg+8, 8, expirebuf, sizeof expirebuf, + "GPG-pa1-HMACi-key"); + kdf (symxkey, sizeof symxkey, + master, sizeof master, msg+8, 8, expirebuf, sizeof expirebuf, + "GPG-pa1-SYMx-key"); + + + /* Create the response. */ + newmsglen = 7+1+8+32+32; + newmsg = xmalloc (newmsglen); + memcpy (newmsg+0, "GPG-pa1", 7); + newmsg[7] = MSG_TYPE_DHPART2; + memcpy (newmsg+8, msg + 8, 8); /* SessionID. */ + memcpy (newmsg+16, pki, 32); /* PKi */ + /* MAC(HMACi-key, Hash(DHPART1) || DHPART2[0..47] || SYMx-key) */ + gcry_md_hash_buffer (GCRY_MD_SHA256, tmphash, msg, msglen); + hmac_data (newmsg+48, 32, hmacikey, sizeof hmacikey, + tmphash, sizeof tmphash, + newmsg, (size_t)48, + symxkey, sizeof symxkey, + NULL); + + /* Update the state. */ + xnvc_set (state, "State:", "DHPart2-sent"); + xnvc_set_hex (state, "DH-Master:", master, sizeof master); + gcry_md_hash_buffer (GCRY_MD_SHA256, tmphash, newmsg, newmsglen); + xnvc_set_hex (state, "Hash-DHPart2:", tmphash, 32); + + /* Write the state. */ + write_state (state, 0); + + /* Write the message. */ + send_message (newmsg, newmsglen); + + leave: + xfree (newmsg); + return err; +} + + +/* We are the Responder: Process a DHPART2 message in (MSG,MSGLEN) + * which has already been validated to have a correct header and + * message type. Sends the CONFIRM message and writes STATE. */ +static gpg_error_t +proc_msg_dhpart2 (nvc_t state, const unsigned char *msg, size_t msglen) +{ + gpg_error_t err; + unsigned char hash[32]; + unsigned char tmphash[32]; + uint64_t expire; + unsigned char expirebuf[5]; + unsigned char pki[32]; + unsigned char pkr[32]; + unsigned char skr[32]; + unsigned char master[32]; + unsigned char hmacikey[32]; + unsigned char hmacrkey[32]; + unsigned char symxkey[32]; + unsigned char sas[32]; + unsigned char *newmsg = NULL; + size_t newmsglen; + + log_assert (msglen >= 80); + + /* Check that the PKi in the message matches the Hash(Pki) received + * with the Commit message. */ + memcpy (pki, msg + 16, 32); + gcry_md_hash_buffer (GCRY_MD_SHA256, hash, pki, 32); + if (hex2bin (xnvc_get_string (state, "Hash-PKi:"), + tmphash, sizeof tmphash) < 0) + { + err = gpg_error (GPG_ERR_INV_VALUE); + log_error ("no or garbled 'Hash-PKi' in our state file\n"); + goto leave; + } + if (memcmp (hash, tmphash, 32)) + { + err = gpg_error (GPG_ERR_BAD_DATA); + log_error ("Initiator sent a different key in %s than announced in %s\n", + msgtypestr (MSG_TYPE_DHPART2), + msgtypestr (MSG_TYPE_COMMIT)); + goto leave; + } + /* Check that the received PKi is different from our PKr. */ + if (hex2bin (xnvc_get_string (state, "DH-PKr:"), pkr, sizeof pkr) < 0) + { + err = gpg_error (GPG_ERR_INV_VALUE); + log_error ("no or garbled 'DH-PKr' in our state file\n"); + goto leave; + } + if (!memcmp (pkr, pki, 32)) + { + err = gpg_error (GPG_ERR_BAD_DATA); + log_error ("Initiator sent our own PKr back\n"); + goto leave; + } + + /* Put the expire value into a buffer. */ + expire = string_to_u64 (xnvc_get_string (state, "Expires:")); + if (!expire) + { + err = gpg_error (GPG_ERR_INV_VALUE); + log_error ("no 'Expire' in our state file\n"); + goto leave; + } + expirebuf[0] = expire >> 32; + expirebuf[1] = expire >> 24; + expirebuf[2] = expire >> 16; + expirebuf[3] = expire >> 8; + expirebuf[4] = expire; + + /* Get our secret from the state. */ + if (hex2bin (xnvc_get_string (state, "DH-SKr:"), skr, sizeof skr) < 0) + { + err = gpg_error (GPG_ERR_INV_VALUE); + log_error ("no or garbled 'DH-SKr' in our state file\n"); + goto leave; + } + + /* Compute the shared secrets. */ + err = compute_master_secret (master, sizeof master, + skr, sizeof skr, pki, sizeof pki); + if (err) + { + log_error ("creating DH keypair failed: %s\n", gpg_strerror (err)); + goto leave; + } + + kdf (hmacikey, sizeof hmacikey, + master, sizeof master, msg+8, 8, expirebuf, sizeof expirebuf, + "GPG-pa1-HMACi-key"); + kdf (hmacrkey, sizeof hmacrkey, + master, sizeof master, msg+8, 8, expirebuf, sizeof expirebuf, + "GPG-pa1-HMACr-key"); + kdf (symxkey, sizeof symxkey, + master, sizeof master, msg+8, 8, expirebuf, sizeof expirebuf, + "GPG-pa1-SYMx-key"); + kdf (sas, sizeof sas, + master, sizeof master, msg+8, 8, expirebuf, sizeof expirebuf, + "GPG-pa1-SAS"); + + /* Check the MAC from the message which is + * MAC(HMACi-key, Hash(DHPART1) || DHPART2[0..47] || SYMx-key). + * For that we need to fetch the stored hash from the state. */ + if (hex2bin (xnvc_get_string (state, "Hash-DHPart1:"), + tmphash, sizeof tmphash) < 0) + { + err = gpg_error (GPG_ERR_INV_VALUE); + log_error ("no or garbled 'Hash-DHPart1' in our state file\n"); + goto leave; + } + hmac_data (hash, 32, hmacikey, sizeof hmacikey, + tmphash, sizeof tmphash, + msg, 48, + symxkey, sizeof symxkey, + NULL); + if (memcmp (msg+48, hash, 32)) + { + err = gpg_error (GPG_ERR_BAD_DATA); + log_error ("manipulation of received %s message detected: %s\n", + msgtypestr (MSG_TYPE_DHPART2), "Bad MAC"); + goto leave; + } + + /* Create the response. */ + newmsglen = 7+1+8+32; + newmsg = xmalloc (newmsglen); + memcpy (newmsg+0, "GPG-pa1", 7); + newmsg[7] = MSG_TYPE_CONFIRM; + memcpy (newmsg+8, msg + 8, 8); /* SessionID. */ + /* MAC(HMACr-key, Hash(DHPART2) || CONFIRM[0..15] || SYMx-key) */ + gcry_md_hash_buffer (GCRY_MD_SHA256, tmphash, msg, msglen); + hmac_data (newmsg+16, 32, hmacrkey, sizeof hmacrkey, + tmphash, sizeof tmphash, + newmsg, (size_t)16, + symxkey, sizeof symxkey, + NULL); + + /* Update the state. */ + xnvc_set (state, "State:", "Confirm-sent"); + xnvc_set_hex (state, "DH-Master:", master, sizeof master); + + /* Write the state. */ + write_state (state, 0); + + /* Write the message. */ + send_message (newmsg, newmsglen); + + display_sas (sas, sizeof sas, 0); + + + leave: + xfree (newmsg); + return err; +} + + +/* We are the Initiator: Process a CONFIRM message in (MSG,MSGLEN) + * which has already been validated to have a correct header and + * message type. Does not send anything back. */ +static gpg_error_t +proc_msg_confirm (nvc_t state, const unsigned char *msg, size_t msglen) +{ + gpg_error_t err; + unsigned char hash[32]; + unsigned char tmphash[32]; + unsigned char master[32]; + uint64_t expire; + unsigned char expirebuf[5]; + unsigned char hmacrkey[32]; + unsigned char symxkey[32]; + unsigned char sas[32]; + + log_assert (msglen >= 48); + + /* Put the expire value into a buffer. */ + expire = string_to_u64 (xnvc_get_string (state, "Expires:")); + if (!expire) + { + err = gpg_error (GPG_ERR_INV_VALUE); + log_error ("no 'Expire' in our state file\n"); + goto leave; + } + expirebuf[0] = expire >> 32; + expirebuf[1] = expire >> 24; + expirebuf[2] = expire >> 16; + expirebuf[3] = expire >> 8; + expirebuf[4] = expire; + + /* Get the master secret. */ + if (hex2bin (xnvc_get_string (state, "DH-Master:"),master,sizeof master) < 0) + { + err = gpg_error (GPG_ERR_INV_VALUE); + log_error ("no or garbled 'DH-Master' in our state file\n"); + goto leave; + } + + kdf (hmacrkey, sizeof hmacrkey, + master, sizeof master, msg+8, 8, expirebuf, sizeof expirebuf, + "GPG-pa1-HMACr-key"); + kdf (symxkey, sizeof symxkey, + master, sizeof master, msg+8, 8, expirebuf, sizeof expirebuf, + "GPG-pa1-SYMx-key"); + kdf (sas, sizeof sas, + master, sizeof master, msg+8, 8, expirebuf, sizeof expirebuf, + "GPG-pa1-SAS"); + + /* Check the MAC from the message which is */ + /* MAC(HMACr-key, Hash(DHPART2) || CONFIRM[0..15] || SYMx-key). */ + if (hex2bin (xnvc_get_string (state, "Hash-DHPart2:"), + tmphash, sizeof tmphash) < 0) + { + err = gpg_error (GPG_ERR_INV_VALUE); + log_error ("no or garbled 'Hash-DHPart2' in our state file\n"); + goto leave; + } + hmac_data (hash, 32, hmacrkey, sizeof hmacrkey, + tmphash, sizeof tmphash, + msg, (size_t)16, + symxkey, sizeof symxkey, + NULL); + if (!memcmp (msg+48, hash, 32)) + { + err = gpg_error (GPG_ERR_BAD_DATA); + log_error ("manipulation of received %s message detected: %s\n", + msgtypestr (MSG_TYPE_CONFIRM), "Bad MAC"); + goto leave; + } + + + err = display_sas (sas, sizeof sas, 1); + if (err) + goto leave; + + /* Update the state. */ + xnvc_set (state, "State:", "Confirmed"); + + /* Write the state. */ + write_state (state, 0); + + leave: + return err; +} + + + +/* Expire old state files. This loops over all state files and remove + * those which are expired. */ +static void +expire_old_states (void) +{ + gpg_error_t err = 0; + const char *dirname; + DIR *dir = NULL; + struct dirent *dir_entry; + char *fname = NULL; + estream_t fp = NULL; + nvc_t nvc = NULL; + nve_t item; + const char *value; + unsigned long expire; + unsigned long now = gnupg_get_time (); + + dirname = get_pairing_statedir (); + dir = opendir (dirname); + if (!dir) + { + err = gpg_error_from_syserror (); + goto leave; + } + + while ((dir_entry = readdir (dir))) + { + if (strlen (dir_entry->d_name) != 16+4 + || strcmp (dir_entry->d_name + 16, ".pa1")) + continue; + + xfree (fname); + fname = make_filename (dirname, dir_entry->d_name, NULL); + es_fclose (fp); + fp = es_fopen (fname, "rb"); + if (!fp) + { + err = gpg_error_from_syserror (); + if (gpg_err_code (err) != GPG_ERR_ENOENT) + log_info ("failed to open state file '%s': %s\n", + fname, gpg_strerror (err)); + continue; + } + nvc_release (nvc); + + /* NB.: The following is similar to code in read_state. */ + err = nvc_parse (&nvc, NULL, fp); + if (err) + { + log_info ("failed to parse state file '%s': %s\n", + fname, gpg_strerror (err)); + continue; /* Skip */ + } + item = nvc_lookup (nvc, "Expires:"); + if (!item) + { + log_info ("invalid state file '%s': %s\n", + fname, "field 'expire' not found"); + continue; /* Skip */ + } + value = nve_value (item); + if (!value || !(expire = strtoul (value, NULL, 10))) + { + log_info ("invalid state file '%s': %s\n", + fname, "field 'expire' has an invalid value"); + continue; /* Skip */ + } + + if (expire <= now) + { + es_fclose (fp); + fp = NULL; + if (gnupg_remove (fname)) + { + err = gpg_error_from_syserror (); + log_info ("failed to delete state file '%s': %s\n", + fname, gpg_strerror (err)); + } + else if (opt.verbose) + log_info ("state file '%s' deleted\n", fname); + } + } + + leave: + if (err) + log_error ("expiring old states in '%s' failed: %s\n", + dirname, gpg_strerror (err)); + if (dir) + closedir (dir); + es_fclose (fp); + xfree (fname); +} + + + +/* Initiate a pairing. The output needs to be conveyed to the + * peer */ +static gpg_error_t +command_initiate (void) +{ + gpg_error_t err; + nvc_t state; + + state = xnvc_new (); + xnvc_set (state, "Version:", "GPG-pa1"); + xnvc_set_hex (state, "Session:", get_session_id (), 8); + xnvc_set (state, "Role:", "Initiator"); + + err = make_msg_commit (state); + + nvc_release (state); + return err; +} + + + +/* Helper for command_respond(). */ +static gpg_error_t +expect_state (int msgtype, const char *statestr, const char *expected) +{ + if (strcmp (statestr, expected)) + { + log_error ("received %s message in %s state (should be %s)\n", + msgtypestr (msgtype), statestr, expected); + return gpg_error (GPG_ERR_INV_RESPONSE); + } + return 0; +} + +/* Respond to a pairing intiation. This is used by the peer and later + * by the original responder. Depending on the state the output needs + * to be conveyed to the peer. */ +static gpg_error_t +command_respond (void) +{ + gpg_error_t err; + unsigned char *msg; + size_t msglen; + int msgtype; + nvc_t state; + const char *rolestr; + const char *statestr; + + err = read_message (&msg, &msglen, &msgtype, &state); + if (err && gpg_err_code (err) != GPG_ERR_NOT_FOUND) + goto leave; + rolestr = xnvc_get_string (state, "Role:"); + statestr = xnvc_get_string (state, "State:"); + if (DBG_MESSAGE) + { + if (!state) + log_debug ("no state available\n"); + else + log_debug ("we are %s, our current state is %s\n", rolestr, statestr); + log_debug ("got message of type %s (%d)\n", + msgtypestr (msgtype), msgtype); + } + + if (!state) + { + if (msgtype == MSG_TYPE_COMMIT) + { + state = xnvc_new (); + xnvc_set (state, "Version:", "GPG-pa1"); + xnvc_set_hex (state, "Session:", get_session_id (), 8); + xnvc_set (state, "Role:", "Responder"); + err = proc_msg_commit (state, msg, msglen); + } + else + { + log_error ("%s message expected but got %s\n", + msgtypestr (MSG_TYPE_COMMIT), msgtypestr (msgtype)); + if (msgtype == MSG_TYPE_DHPART1) + log_info ("the pairing probably took too long and timed out\n"); + err = gpg_error (GPG_ERR_INV_RESPONSE); + goto leave; + } + } + else if (!strcmp (rolestr, "Initiator")) + { + if (msgtype == MSG_TYPE_DHPART1) + { + if (!(err = expect_state (msgtype, statestr, "Commit-sent"))) + err = proc_msg_dhpart1 (state, msg, msglen); + } + else if (msgtype == MSG_TYPE_CONFIRM) + { + if (!(err = expect_state (msgtype, statestr, "DHPart2-sent"))) + err = proc_msg_confirm (state, msg, msglen); + } + else + { + log_error ("%s message not expected by Initiator\n", + msgtypestr (msgtype)); + err = gpg_error (GPG_ERR_INV_RESPONSE); + goto leave; + } + } + else if (!strcmp (rolestr, "Responder")) + { + if (msgtype == MSG_TYPE_DHPART2) + { + if (!(err = expect_state (msgtype, statestr, "DHPart1-sent"))) + err = proc_msg_dhpart2 (state, msg, msglen); + } + else + { + log_error ("%s message not expected by Responder\n", + msgtypestr (msgtype)); + err = gpg_error (GPG_ERR_INV_RESPONSE); + goto leave; + } + } + else + log_fatal ("invalid role '%s' in state file\n", rolestr); + + + leave: + xfree (msg); + nvc_release (state); + return err; +} + + + +/* Return the keys for SESSIONIDSTR or the last one if it is NULL. + * Two keys are returned: The first is the one for sending encrypted + * data and the second one for decrypting received data. The keys are + * always returned hex encoded and both are terminated by a LF. */ +static gpg_error_t +command_get (const char *sessionidstr) +{ + gpg_error_t err; + unsigned char sessid[8]; + nvc_t state; + + if (!sessionidstr) + { + log_error ("calling without session-id is not yet implemented\n"); + err = gpg_error (GPG_ERR_NOT_IMPLEMENTED); + goto leave; + } + if (hex2bin (sessionidstr, sessid, sizeof sessid) < 0) + { + err = gpg_error (GPG_ERR_INV_VALUE); + log_error ("invalid session id given\n"); + goto leave; + } + set_session_id (sessid, sizeof sessid); + err = read_state (&state); + if (err) + { + log_error ("reading state of session %s failed: %s\n", + sessionidstr, gpg_strerror (err)); + goto leave; + } + + leave: + return err; +} + + + +/* Cleanup command. */ +static gpg_error_t +command_cleanup (void) +{ + expire_old_states (); + return 0; +}