From 3801ba54590060469c00aa36b8f97704ca539506 Mon Sep 17 00:00:00 2001 From: Fredrik Thulin Date: Sat, 5 Feb 2011 19:42:53 +0100 Subject: [PATCH] Add challenge-response tool (ykchalresp). This required implementing multi-feature-report reads in ykcore.c. It is kind of wrong to have these functions in libykpers, but that's where they are at the moment. We might split some part of libykpers out into a new library before releasing anything official. This is to be considered work in progress. --- Makefile.am | 5 +- NEWS | 1 + configure.ac | 6 +- libykpers-1.map | 8 ++ ykchalresp.c | 314 ++++++++++++++++++++++++++++++++++++++++++++++++ ykcore/ykcore.c | 216 +++++++++++++++++++++++++++++---- ykcore/ykcore.h | 13 +- 7 files changed, 532 insertions(+), 31 deletions(-) create mode 100644 ykchalresp.c diff --git a/Makefile.am b/Makefile.am index 7e0848b..748c453 100644 --- a/Makefile.am +++ b/Makefile.am @@ -61,11 +61,14 @@ endif # The command line tools. -bin_PROGRAMS = ykpersonalize +bin_PROGRAMS = ykpersonalize ykchalresp ykpersonalize_SOURCES = ykpersonalize.c ykpersonalize_LDADD = ./libykpers-1.la ./ykcore/libykcore.la +ykchalresp_SOURCES = ykchalresp.c +ykchalresp_LDADD = ./libykpers-1.la ./ykcore/libykcore.la + dist_man1_MANS = ykpersonalize.1 # Dist docs from wiki. diff --git a/NEWS b/NEWS index 9fe14b8..0925faf 100644 --- a/NEWS +++ b/NEWS @@ -4,6 +4,7 @@ Yubikey-personalize NEWS -- History of user-visible changes. -*- outline -*- ** When generating keys from passphrase, generate 160 bit keys for modes that support it (OATH-HOTP and HMAC challenge response). +** An additional program to use challenge-response was added. * Version 1.4.1 (released 2011-01-19) ** Restore OATH-HOTP symbols for LIBYKPERS_1.0. Accidentally moved to LIBYKPERS_1.4 in 1.4.0. diff --git a/configure.ac b/configure.ac index a231f57..5a5c4e2 100644 --- a/configure.ac +++ b/configure.ac @@ -36,9 +36,9 @@ AC_CONFIG_MACRO_DIR([m4]) # Interfaces changed/added/removed: CURRENT++ REVISION=0 # Interfaces added: AGE++ # Interfaces removed: AGE=0 -AC_SUBST(LT_CURRENT, 4) -AC_SUBST(LT_REVISION, 1) -AC_SUBST(LT_AGE, 3) +AC_SUBST(LT_CURRENT, 5) +AC_SUBST(LT_REVISION, 0) +AC_SUBST(LT_AGE, 4) AM_INIT_AUTOMAKE([-Wall -Werror]) AC_PROG_CC diff --git a/libykpers-1.map b/libykpers-1.map index 6a03c8a..f18d42e 100644 --- a/libykpers-1.map +++ b/libykpers-1.map @@ -109,3 +109,11 @@ LIBYKPERS_1.4 { ykp_set_extflag_SERIAL_API_VISIBLE; # Variables: } LIBYKPERS_1.0; + +LIBYKPERS_1.5 { + global: +# Functions: + yk_wait_for_key_status; + yk_read_response_from_key; +# Variables: +} LIBYKPERS_1.4; diff --git a/ykchalresp.c b/ykchalresp.c new file mode 100644 index 0000000..25772ad --- /dev/null +++ b/ykchalresp.c @@ -0,0 +1,314 @@ +/* -*- mode:C; c-file-style: "bsd" -*- */ +/* + * Copyright (c) 2011 Yubico AB. + * All rights reserved. + * + * Author : Fredrik Thulin + * + * Some basic code copied from ykpersonalize.c. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include + +#include +#include +#include + +const char *usage = + "Usage: ykchalresp [options] challenge\n" + "\n" + "Options :\n" + "\n" + "\t-1 Send challenge to slot 1. This is the default.\n" + "\t-2 Send challenge to slot 2.\n" + "\t-H Send a 64 byte HMAC challenge. This is the default.\n" + "\t-Y Send a 6 byte Yubico challenge.\n" + "\t-N Abort if Yubikey requires button press.\n" + "\t-x Challenge is hex encoded.\n" + "\n" + "\t-v verbose\n" + "\t-h help (this text)\n" + "\n" + "\n" + ; +const char *optstring = "12xvhHYN"; + +static void report_yk_error() +{ + if (ykp_errno) + fprintf(stderr, "Yubikey personalization error: %s\n", + ykp_strerror(ykp_errno)); + if (yk_errno) { + if (yk_errno == YK_EUSBERR) { + fprintf(stderr, "USB error: %s\n", + yk_usb_strerror()); + } else { + fprintf(stderr, "Yubikey core error: %s\n", + yk_strerror(yk_errno)); + } + } +} + +int parse_args(int argc, char **argv, + int *slot, bool *verbose, + unsigned char **challenge, unsigned int *challenge_len, + bool *hmac, bool *may_block, + int *exit_code) +{ + char c; + bool hex_encoded = false; + + while((c = getopt(argc, argv, optstring)) != -1) { + switch (c) { + case '1': + *slot = 1; + break; + case '2': + *slot = 2; + break; + case 'H': + *hmac = true; + break; + case 'N': + *may_block = false; + break; + case 'Y': + *hmac = false; + break; + case 'x': + hex_encoded = true; + break; + case 'v': + *verbose = true; + break; + case 'h': + default: + fputs(usage, stderr); + *exit_code = 0; + return 0; + } + } + + if (optind >= argc) { + /* No challenge */ + fputs(usage, stderr); + return 0; + } + + if (hex_encoded) { + static unsigned char decoded[64]; + int decoded_len; + + int strl = strlen(argv[optind]); + + if (strl >= sizeof(decoded) * 2) { + fprintf(stderr, "Hex-encoded challenge too long (max %i chars)\n", + sizeof(decoded) * 2); + return 0; + } + + if (strl % 2 != 0) { + fprintf(stderr, "Odd number of characters in hex-encoded challenge\n"); + return 0; + } + + memset(decoded, 0, sizeof(decoded)); + + if (yubikey_hex_p(argv[optind])) { + yubikey_hex_decode((char *)decoded, argv[optind], strl); + } else { + fprintf(stderr, "Bad hex-encoded string '%s'\n", argv[optind]); + return 0; + } + *challenge = (unsigned char *) &decoded; + *challenge_len = strl / 2; + } else { + *challenge = argv[optind]; + *challenge_len = strlen(*challenge); + } + + return 1; +} + +int check_firmware(YK_KEY *yk, bool verbose) +{ + YK_STATUS *st = ykds_alloc(); + + if (!yk_get_status(yk, st)) { + free(st); + return 0; + } + + if (verbose) { + printf("Firmware version %d.%d.%d\n", + ykds_version_major(st), + ykds_version_minor(st), + ykds_version_build(st)); + fflush(stdout); + } + + if (ykds_version_major(st) < 2 || + ykds_version_minor(st) < 2) { + fprintf(stderr, "Challenge-response not supported before YubiKey 2.2.\n"); + free(st); + return 0; + } + + free(st); + return 1; +} + +int challenge_response(YK_KEY *yk, int slot, + unsigned char *challenge, unsigned int len, + bool hmac, bool may_block, bool verbose) +{ + unsigned char response[64]; + unsigned char output_buf[sizeof(response) * 2]; + int yk_cmd; + unsigned int flags = 0; + unsigned int response_len = 0; + unsigned int expect_bytes = 0; + + memset(response, 0, sizeof(response)); + + if (may_block) + flags |= YK_FLAG_MAYBLOCK; + + if (verbose) { + fprintf(stderr, "Sending %i bytes %s challenge to slot %i\n", len, (hmac == true)?"HMAC":"Yubico", slot); + //_yk_hexdump(challenge, len); + } + + switch(slot) { + case 1: + yk_cmd = (hmac == true) ? SLOT_CHAL_HMAC1 : SLOT_CHAL_OTP1; + break; + case 2: + yk_cmd = (hmac == true) ? SLOT_CHAL_HMAC2 : SLOT_CHAL_OTP2; + break; + } + + if (!yk_write_to_key(yk, yk_cmd, challenge, len)) + return 0; + + if (verbose) { + fprintf(stderr, "Reading response...\n"); + } + + /* HMAC responses are 160 bits, Yubico 128 */ + expect_bytes = (hmac == true) ? 20 : 16; + + if (! yk_read_response_from_key(yk, slot, flags, + &response, sizeof(response), + expect_bytes, + &response_len)) + return 0; + + if (hmac && response_len > 20) + response_len = 20; + if (! hmac && response_len > 16) + response_len = 16; + + memset(output_buf, 0, sizeof(output_buf)); + if (hmac) { + yubikey_hex_encode(output_buf, (char *)response, response_len); + } else { + yubikey_modhex_encode(output_buf, (char *)response, response_len); + } + printf("%s\n", output_buf); + + return 1; +} + +int main(int argc, char **argv) +{ + YK_KEY *yk = 0; + bool error = true; + int exit_code = 0; + + /* Options */ + bool verbose = false; + bool hex_encoded = false; + bool hmac = true; + bool may_block = true; + unsigned char *challenge; + unsigned int challenge_len; + int slot = 1; + + ykp_errno = 0; + yk_errno = 0; + + if (! parse_args(argc, argv, + &slot, &verbose, + &challenge, &challenge_len, + &hmac, &may_block, + &exit_code)) + goto err; + + if (!yk_init()) { + exit_code = 1; + goto err; + } + + if (!(yk = yk_open_first_key())) { + exit_code = 1; + goto err; + } + + if (! check_firmware(yk, verbose)) { + exit_code = 1; + goto err; + } + + if (! challenge_response(yk, slot, + challenge, challenge_len, + hmac, may_block, verbose)) { + exit_code = 1; + goto err; + } + + exit_code = 0; + error = false; + +err: + if (error || exit_code != 0) { + report_yk_error(); + } + + if (yk && !yk_close_key(yk)) { + report_yk_error(); + exit_code = 2; + } + + if (!yk_release()) { + report_yk_error(); + exit_code = 2; + } + + exit(exit_code); +} diff --git a/ykcore/ykcore.c b/ykcore/ykcore.c index c7065a1..7447009 100644 --- a/ykcore/ykcore.c +++ b/ykcore/ykcore.c @@ -35,6 +35,7 @@ /* To get modhex and crc16 */ #include +#include #ifndef _WIN32 #include #define Sleep(x) usleep((x)*1000) @@ -202,7 +203,9 @@ static const char *errtext[] = { "unsupported firmware version", "out of memory", "no status structure given", - "not yet implemented" + "not yet implemented", + "checksum mismatch", + "operation would block" }; const char *yk_strerror(int errnum) { @@ -215,9 +218,15 @@ const char *yk_usb_strerror() return _ykusb_strerror(); } -/* Note: we currently have no idea whatsoever how to read things larger - than FEATURE_RPT_SIZE - 1. We also have no idea what to do with the - slot parameter, it currently is there for future purposes only. */ +/* This function would've been better named 'yk_read_status_from_key'. Because + * it disregards the first byte in each feature report, it can't be used to read + * generic feature reports from the Yubikey, and this behaviour can't be changed + * without breaking compatibility with existing programs. + * + * See yk_read_response_from_key() for a generic purpose data reading function. + * + * The slot parameter is here for future purposes only. + */ int yk_read_from_key(YK_KEY *yk, uint8_t slot, void *buf, unsigned int bufsize, unsigned int *bufcount) { @@ -235,12 +244,163 @@ int yk_read_from_key(YK_KEY *yk, uint8_t slot, /* This makes it apparent that there's some mysterious value in the first byte... I wonder what... /Richard Levitte */ - memcpy(buf, data + 1, bufsize); + memcpy(buf, data + 1, bufsize); *bufcount = bufsize; return 1; } +/* Wait for the Yubikey to either set or clear (controlled by the boolean logic_and) + * the bits in mask. + * + * The slot parameter is here for future purposes only. + */ +int yk_wait_for_key_status(YK_KEY *yk, uint8_t slot, unsigned int flags, + unsigned int max_time_ms, + bool logic_and, unsigned char mask, + unsigned char *last_data) +{ + unsigned char data[FEATURE_RPT_SIZE]; + unsigned int bytes_read; + + int sleepval = 10; + int slept_time = 0; + int blocking = 0; + + while (slept_time < max_time_ms) { + /* Read a status report from the key */ + memset(data, 0, sizeof(data)); + if (!_ykusb_read(yk, REPORT_TYPE_FEATURE, slot, (char *) &data, FEATURE_RPT_SIZE)) + return 0; + + if (last_data != NULL) + memcpy(last_data, data, sizeof(data)); + + /* The status byte from the key is now in last byte of data */ + if (logic_and) { + /* Check if Yubikey has SET the bit(s) in mask */ + if ((data[FEATURE_RPT_SIZE - 1] & mask) == mask) { + return 1; + } + } else { + /* Check if Yubikey has CLEARED the bit(s) in mask */ + if (! (data[FEATURE_RPT_SIZE - 1] & mask)) { + return 1; + } + } + + /* Check if Yubikey says it will wait for user interaction */ + if ((data[FEATURE_RPT_SIZE - 1] & RESP_TIMEOUT_WAIT_FLAG) == RESP_TIMEOUT_WAIT_FLAG) { + if ((flags & YK_FLAG_MAYBLOCK) == YK_FLAG_MAYBLOCK) { + if (! blocking) { + /* Extend timeout first time we see RESP_TIMEOUT_WAIT_FLAG. */ + blocking = 1; + max_time_ms += 15000; + } + } else { + /* Reset read mode of Yubikey before aborting. */ + yk_force_key_update(yk); + yk_errno = YK_EWOULDBLOCK; + return 0; + } + } else { + if (blocking) { + /* YubiKey timed out waiting for user interaction */ + break; + } + } + + Sleep(sleepval); + slept_time += sleepval; + /* exponential backoff, up to 500 ms */ + sleepval *= 2; + if (sleepval > 500) + sleepval = 500; + } + + yk_errno = YK_ETIMEOUT; + return 0; +} + +/* Read one or more feature reports from a Yubikey and put them together. + * + * Bufsize must be able to hold at least 2 more bytes than you are expecting + * (the CRC), but since all read requests return 7 bytes of data bufsize needs + * to be up to 7 bytes more than you expect. + * + * If the key returns more data than bufsize, we fail and set yk_errno to + * YK_EWRONGSIZ. If that happens there will be partial data in buf. + * + * If we read a response from a Yubikey that is configured to block and wait for + * a button press (in challenge response), this function will abort unless + * flags contain YK_FLAG_MAYBLOCK, in which case it might take up to 15 seconds + * for this function to return. + * + * The slot parameter is here for future purposes only. + */ +int yk_read_response_from_key(YK_KEY *yk, uint8_t slot, unsigned int flags, + void *buf, unsigned int bufsize, unsigned int expect_bytes, + unsigned int *bytes_read) +{ + unsigned char data[FEATURE_RPT_SIZE]; + memset(data, 0, sizeof(data)); + + memset(buf, 0, bufsize); + *bytes_read = 0; + + /* Wait for the key to turn on RESP_PENDING_FLAG */ + if (! yk_wait_for_key_status(yk, slot, flags, 1000, true, RESP_PENDING_FLAG, (char *) &data)) + return 0; + + /* The first part of the response was read by yk_wait_for_key_status(). We need + * to copy it to buf. + */ + memcpy(buf + *bytes_read, data, sizeof(data) - 1); + *bytes_read += sizeof(data) - 1; + + while (*bytes_read + FEATURE_RPT_SIZE <= bufsize) { + memset(data, 0, sizeof(data)); + + if (!_ykusb_read(yk, REPORT_TYPE_FEATURE, 0, (char *)data, FEATURE_RPT_SIZE)) + return 0; + + if (data[FEATURE_RPT_SIZE - 1] & RESP_PENDING_FLAG) { + /* The lower five bits of the status byte has the response sequence + * number. If that gets reset to zero we are done. + */ + if ((data[FEATURE_RPT_SIZE - 1] & 31) == 0) { + if (expect_bytes > 0) { + /* Size of response is known. Verify CRC. */ + int crc = yubikey_crc16(buf, expect_bytes + 2); + if (crc != YK_CRC_OK_RESIDUAL) { + yk_errno = YK_ECHECKSUM; + return 0; + } + } + + /* Reset read mode of Yubikey before returning. */ + yk_force_key_update(yk); + + return 1; + } + + memcpy(buf + *bytes_read, data, sizeof(data) - 1); + *bytes_read += sizeof(data) - 1; + } else { + /* Reset read mode of Yubikey before returning. */ + yk_force_key_update(yk); + + return 0; + } + } + + /* We're out of buffer space, abort reading */ + yk_force_key_update(yk); + + yk_errno = YK_EWRONGSIZ; + return 0; +} + int yk_write_to_key(YK_KEY *yk, uint8_t slot, const void *buf, int bufcount) { YK_FRAME frame; @@ -273,6 +433,12 @@ int yk_write_to_key(YK_KEY *yk, uint8_t slot, const void *buf, int bufcount) ptr = (unsigned char *) &frame; end = (unsigned char *) &frame + sizeof(frame); + /* Initial check that the YubiKey is in a state where it will accept + * a write. + * + if (! yk_wait_for_key_status(yk, slot, 0, 1000, false, SLOT_WRITE_FLAG, NULL)) + return 0; + */ for (seq = 0; ptr < end; seq++) { int all_zeros = 1; /* Ignore parts that are all zeroes except first and last @@ -287,29 +453,17 @@ int yk_write_to_key(YK_KEY *yk, uint8_t slot, const void *buf, int bufcount) /* sequence number goes into lower bits of last byte */ repbuf[i] = seq | SLOT_WRITE_FLAG; - if (!_ykusb_write(yk, REPORT_TYPE_FEATURE, 0, - (char *)repbuf, FEATURE_RPT_SIZE)) - return 0; - /* When the Yubikey clears the SLOT_WRITE_FLAG, the - next part can be sent */ - - for (i = 0; i < 50; i++) { - memset(repbuf, 0, sizeof(repbuf)); - if (!_ykusb_read(yk, REPORT_TYPE_FEATURE, 0, - (char *)repbuf, FEATURE_RPT_SIZE)) - return 0; - if (! (repbuf[FEATURE_RPT_SIZE - 1] & SLOT_WRITE_FLAG)) - break; - Sleep(10); - } - - /* If timeout, something has gone wrong */ + * next part can be sent. Yubikey low-level interface + * section 2.4 (Report arbitration polling) specifies + * a 600 ms timeout for this operation. + */ + if (! yk_wait_for_key_status(yk, slot, 0, 600, false, SLOT_WRITE_FLAG, NULL)) + return 0; - if (i >= 50) { - yk_errno = YK_ETIMEOUT; + if (!_ykusb_write(yk, REPORT_TYPE_FEATURE, 0, + (char *)repbuf, FEATURE_RPT_SIZE)) return 0; - } } return 1; @@ -346,3 +500,15 @@ uint16_t yk_endian_swap_16(uint16_t x) return x; } +/* Private little hexdump function for debugging */ +void _yk_hexdump(void *buffer, int size) +{ + unsigned char *p = buffer; + int i; + for (i = 0; i < size; i++) { + fprintf(stderr, "\\x%02x", *p); + p++; + } + fprintf(stderr, "\n"); + fflush(stderr); +} diff --git a/ykcore/ykcore.h b/ykcore/ykcore.h index 270dded..305a9ec 100644 --- a/ykcore/ykcore.h +++ b/ykcore/ykcore.h @@ -117,7 +117,16 @@ const char *yk_usb_strerror(); #define YK_ENOKEY 0x05 #define YK_EFIRMWARE 0x06 #define YK_ENOMEM 0x07 -#define YK_ENOSTATUS 0x07 -#define YK_ENOTYETIMPL 0x08 +#define YK_ENOSTATUS 0x08 +#define YK_ENOTYETIMPL 0x09 +#define YK_ECHECKSUM 0x0a /* checksum validation failed */ +#define YK_EWOULDBLOCK 0x0b /* operation would block */ + +/* Flags for response reading. Use high numbers to not exclude the possibility + * to combine these with for example SLOT commands from ykdef.h in the future. + */ +#define YK_FLAG_MAYBLOCK 0x01 << 16 + +#define YK_CRC_OK_RESIDUAL 0xf0b8 #endif /* __YKCORE_H_INCLUDED__ */ -- 2.39.5