diff --git a/tools/gpg-pair-tool.c b/tools/gpg-pair-tool.c index 9a781def1..06b258f90 100644 --- a/tools/gpg-pair-tool.c +++ b/tools/gpg-pair-tool.c @@ -1,2020 +1,1974 @@ /* 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 = 0; 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]; + unsigned char *p; + const unsigned char G[32] = { 0x9 }; /* We need a temporary buffer for the public key. Check the length * for the later memcpy. */ - if (dh_public_len < 32) + if (dh_public_len < 32 || dh_secret_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; + p = gcry_random_bytes (32, GCRY_VERY_STRONG_RANDOM); + if (!p) + return gpg_error_from_syserror (); - 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); + memcpy (dh_secret, p, 32); + xfree (p); + + err = gcry_ecc_mul_point (GCRY_ECC_CURVE25519, &p, dh_secret, G); 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); + memcpy (dh_public, p, 32); + xfree (p); if (DBG_CRYPTO) { - log_printhex (secret.data, secret.len, "DH secret:"); - log_printhex (public.data, public.len, "DH public:"); + log_printhex (dh_secret, 32, "DH secret:"); + log_printhex (dh_public, 32, "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; + unsigned char *s; log_assert (masterlen == 32); + log_assert (sk_a_len == 32); + log_assert (pk_b_len == 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); + err = gcry_ecc_mul_point (GCRY_ECC_CURVE25519, &s, sk_a, 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); + memcpy (master, s, 32); leave: - gcry_sexp_release (s_sk_a); - gcry_sexp_release (s_pk_b); - gcry_sexp_release (s_shared); + xfree (s); + 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 = 0; /* In case that read_message returns an error. */ int msgtype = 0; /* ditto. */ 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; }