From 4ebd905d69963385338a02051610673e6fc4878e Mon Sep 17 00:00:00 2001 From: Simone Baratta Date: Wed, 15 Jan 2020 14:13:48 +0100 Subject: [PATCH 01/15] u2f: Split U2F keyhandle generation/verification. Move the functions to generate/verify U2F keyhandles to a separate module. --- src/CMakeLists.txt | 1 + src/u2f.c | 64 +++++++------------------------------ src/u2f/u2f_keyhandle.c | 70 +++++++++++++++++++++++++++++++++++++++++ src/u2f/u2f_keyhandle.h | 50 +++++++++++++++++++++++++++++ 4 files changed, 132 insertions(+), 53 deletions(-) create mode 100644 src/u2f/u2f_keyhandle.c create mode 100644 src/u2f/u2f_keyhandle.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2640c9033..a1d9ecfef 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -213,6 +213,7 @@ set(FIRMWARE-DRIVER-SOURCES ${FIRMWARE-DRIVER-SOURCES} PARENT_SCOPE) set(FIRMWARE-U2F-SOURCES ${CMAKE_SOURCE_DIR}/src/u2f.c ${CMAKE_SOURCE_DIR}/src/u2f/u2f_app.c + ${CMAKE_SOURCE_DIR}/src/u2f/u2f_keyhandle.c ) set(FIRMWARE-U2F-SOURCES ${FIRMWARE-U2F-SOURCES} PARENT_SCOPE) diff --git a/src/u2f.c b/src/u2f.c index 1fd6b771e..6c3ceb79c 100644 --- a/src/u2f.c +++ b/src/u2f.c @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. #include "u2f.h" -#include "u2f/u2f_app.h" #include #include @@ -28,6 +27,8 @@ #include #include #include +#include +#include #include #include #include @@ -288,46 +289,6 @@ static void _version(const USB_APDU* a, Packet* out_packet) _fill_message(version_response, sizeof(version_response), out_packet); } -/** - * Generates a key for the given app id, salted with the passed nonce. - * @param[in] appId The app id of the RP which requests a registration or authentication process. - * @param[in] nonce A random nonce with which the seed for the private key is salted. - * @param[out] privkey The generated private key. Size must be HMAC_SHA256_LEN. - * @param[out] mac The message authentication code for the private key. Size must be - * HMAC_SHA256_LEN. - */ -USE_RESULT static bool _keyhandle_gen( - const uint8_t* appId, - uint8_t* nonce, - uint8_t* privkey, - uint8_t* mac) -{ - uint8_t hmac_in[U2F_APPID_SIZE + U2F_NONCE_LENGTH]; - uint8_t seed[32]; - UTIL_CLEANUP_32(seed); - if (!keystore_get_u2f_seed(seed)) { - return false; - } - - // Concatenate AppId and Nonce as input for the first HMAC round - memcpy(hmac_in, appId, U2F_APPID_SIZE); - memcpy(hmac_in + U2F_APPID_SIZE, nonce, U2F_NONCE_LENGTH); - int res = wally_hmac_sha256( - seed, KEYSTORE_U2F_SEED_LENGTH, hmac_in, sizeof(hmac_in), privkey, HMAC_SHA256_LEN); - if (res != WALLY_OK) { - return false; - } - - // Concatenate AppId and privkey for the second HMAC round - memcpy(hmac_in + U2F_APPID_SIZE, privkey, HMAC_SHA256_LEN); - res = wally_hmac_sha256( - seed, KEYSTORE_U2F_SEED_LENGTH, hmac_in, sizeof(hmac_in), mac, HMAC_SHA256_LEN); - if (res != WALLY_OK) { - return false; - } - return true; -} - static int _sig_to_der(const uint8_t* sig, uint8_t* der) { int i; @@ -498,7 +459,7 @@ static void _register_continue(const USB_APDU* apdu, Packet* out_packet) return; } random_32_bytes(nonce); - if (!_keyhandle_gen(reg_request->appId, nonce, privkey, mac)) { + if (!u2f_keyhandle_gen(reg_request->appId, nonce, privkey, mac)) { continue; } if (securechip_ecc_generate_public_key(privkey, (uint8_t*)&response->pubKey.x)) { @@ -565,12 +526,13 @@ static uint16_t _authenticate_sanity_check_req(const USB_APDU* apdu) static uint16_t _authenticate_verify_key_valid(const USB_APDU* apdu) { - uint8_t nonce[U2F_NONCE_LENGTH]; - uint8_t mac[HMAC_SHA256_LEN]; + uint8_t nonce[U2F_NONCE_LENGTH] = {0}; + uint8_t mac[HMAC_SHA256_LEN] = {0}; uint8_t privkey[U2F_EC_KEY_SIZE]; + UTIL_CLEANUP_32(privkey); const U2F_AUTHENTICATE_REQ* auth_request = (const U2F_AUTHENTICATE_REQ*)apdu->data; memcpy(nonce, auth_request->keyHandle + sizeof(mac), sizeof(nonce)); - if (!_keyhandle_gen(auth_request->appId, nonce, privkey, mac)) { + if (!u2f_keyhandle_gen(auth_request->appId, nonce, privkey, mac)) { return U2F_SW_WRONG_DATA; } if (!MEMEQ(auth_request->keyHandle, mac, SHA256_LEN)) { @@ -638,8 +600,6 @@ static void _authenticate_wait_refresh(const USB_APDU* apdu, Packet* out_packet) static void _authenticate_continue(const USB_APDU* apdu, Packet* out_packet) { uint8_t privkey[U2F_EC_KEY_SIZE]; - uint8_t nonce[U2F_NONCE_LENGTH]; - uint8_t mac[HMAC_SHA256_LEN]; uint8_t sig[64] = {0}; U2F_AUTHENTICATE_SIG_STR sig_base; uint16_t req_error = _authenticate_sanity_check_req(apdu); @@ -664,12 +624,10 @@ static void _authenticate_continue(const USB_APDU* apdu, Packet* out_packet) return; } - memcpy(nonce, auth_request->keyHandle + sizeof(mac), sizeof(nonce)); - if (!_keyhandle_gen(auth_request->appId, nonce, privkey, mac)) { - _error(U2F_SW_WRONG_DATA, out_packet); - return; - } - if (!MEMEQ(auth_request->keyHandle, mac, SHA256_LEN)) { + /* Decode the key handle into a private key. */ + bool key_is_valid = u2f_keyhandle_verify( + auth_request->appId, auth_request->keyHandle, auth_request->keyHandleLength, privkey); + if (!key_is_valid) { _error(U2F_SW_WRONG_DATA, out_packet); return; } diff --git a/src/u2f/u2f_keyhandle.c b/src/u2f/u2f_keyhandle.c new file mode 100644 index 000000000..551ce96fa --- /dev/null +++ b/src/u2f/u2f_keyhandle.c @@ -0,0 +1,70 @@ +// Copyright 2020 Shift Cryptosecurity AG +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "u2f_keyhandle.h" + +#include +#include +#include +#include + +USE_RESULT bool u2f_keyhandle_gen( + const uint8_t* appId, + const uint8_t* nonce, + uint8_t* privkey, + uint8_t* mac) +{ + uint8_t hmac_in[U2F_APPID_SIZE + U2F_NONCE_LENGTH]; + uint8_t seed[32]; + UTIL_CLEANUP_32(seed); + if (!keystore_get_u2f_seed(seed)) { + return false; + } + + // Concatenate AppId and Nonce as input for the first HMAC round + memcpy(hmac_in, appId, U2F_APPID_SIZE); + memcpy(hmac_in + U2F_APPID_SIZE, nonce, U2F_NONCE_LENGTH); + int res = wally_hmac_sha256( + seed, KEYSTORE_U2F_SEED_LENGTH, hmac_in, sizeof(hmac_in), privkey, HMAC_SHA256_LEN); + if (res != WALLY_OK) { + return false; + } + + // Concatenate AppId and privkey for the second HMAC round + memcpy(hmac_in + U2F_APPID_SIZE, privkey, HMAC_SHA256_LEN); + res = wally_hmac_sha256( + seed, KEYSTORE_U2F_SEED_LENGTH, hmac_in, sizeof(hmac_in), mac, HMAC_SHA256_LEN); + if (res != WALLY_OK) { + return false; + } + return true; +} + +bool u2f_keyhandle_verify(const uint8_t* appId, const uint8_t* key_handle_buf, size_t key_handle_len, uint8_t* privkey) { + if (key_handle_len < sizeof(u2f_keyhandle_t)) { + /* This U2F key handle can't represent a valid key handle. */ + return false; + } + + const u2f_keyhandle_t* key_handle = (const u2f_keyhandle_t*)key_handle_buf; + + /* Compute the MAC corresponding to this nonce. */ + uint8_t mac[HMAC_SHA256_LEN]; + if (!u2f_keyhandle_gen(appId, key_handle->nonce, privkey, mac)) { + return false; + } + + /* Verify the key handle's MAC against the actual one we compute. */ + return MEMEQ(key_handle->mac, mac, SHA256_LEN); +} diff --git a/src/u2f/u2f_keyhandle.h b/src/u2f/u2f_keyhandle.h new file mode 100644 index 000000000..a56f4e500 --- /dev/null +++ b/src/u2f/u2f_keyhandle.h @@ -0,0 +1,50 @@ +// Copyright 2020 Shift Cryptosecurity AG +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef _U2F_KEYHANDLE_H +#define _U2F_KEYHANDLE_H + +#include +#include +#include +#include +#include + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpacked" +typedef struct __attribute__((__packed__)) { + uint8_t mac[HMAC_SHA256_LEN]; + uint8_t nonce[U2F_NONCE_LENGTH]; +} u2f_keyhandle_t; +#pragma GCC diagnostic pop + +/** + * Generates a new private key for the given app id, salted with the passed nonce. + * @param[in] appId The app id of the RP which requests a registration or authentication process. + * @param[in] nonce A random nonce with which the seed for the private key is salted. + * @param[out] privkey Buffer in which to store the generated private key. Size must be HMAC_SHA256_LEN. + * @param[out] mac Buffer in which to store the message authentication code for the private key. + * Size must be HMAC_SHA256_LEN. + */ +USE_RESULT bool u2f_keyhandle_gen( + const uint8_t* appId, + const uint8_t* nonce, + uint8_t* privkey, + uint8_t* mac); + +USE_RESULT bool u2f_keyhandle_verify( + const uint8_t* appId, const uint8_t* key_handle_buf, size_t key_handle_len, uint8_t* privkey +); + +#endif // _U2F_KEYHANDLE_H From dd3dc4327dbe078f0369593faf5728301fcb7720 Mon Sep 17 00:00:00 2001 From: Simone Baratta Date: Thu, 16 Jan 2020 10:46:01 +0100 Subject: [PATCH 02/15] u2f: Improve #define names. Rename U2F_KEY_SIZE to U2F_COORD_SIZE for better clarity. The "32" refers to the size of one of the coordinates of the (x, y) point on the EC. The actual "key" is referred to as "point" in the same header and has size U2F_POINT_SIZE. --- src/u2f.c | 4 ++-- src/usb/u2f/u2f.h | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/u2f.c b/src/u2f.c index 6c3ceb79c..842300e03 100644 --- a/src/u2f.c +++ b/src/u2f.c @@ -52,7 +52,7 @@ typedef struct { #define APDU_LEN(A) (uint32_t)(((A).lc1 << 16) + ((A).lc2 << 8) + ((A).lc3)) #define U2F_KEYHANDLE_LEN (U2F_NONCE_LENGTH + SHA256_LEN) -#if (U2F_EC_KEY_SIZE != SHA256_LEN) || (U2F_EC_KEY_SIZE != U2F_NONCE_LENGTH) +#if (U2F_EC_COORD_SIZE != SHA256_LEN) || (U2F_EC_COORD_SIZE != U2F_NONCE_LENGTH) #error "Incorrect macro values for u2f" #endif @@ -528,7 +528,7 @@ static uint16_t _authenticate_verify_key_valid(const USB_APDU* apdu) { uint8_t nonce[U2F_NONCE_LENGTH] = {0}; uint8_t mac[HMAC_SHA256_LEN] = {0}; - uint8_t privkey[U2F_EC_KEY_SIZE]; + uint8_t privkey[U2F_EC_COORD_SIZE]; UTIL_CLEANUP_32(privkey); const U2F_AUTHENTICATE_REQ* auth_request = (const U2F_AUTHENTICATE_REQ*)apdu->data; memcpy(nonce, auth_request->keyHandle + sizeof(mac), sizeof(nonce)); diff --git a/src/usb/u2f/u2f.h b/src/usb/u2f/u2f.h index 2e28edb06..ee497c9cd 100644 --- a/src/usb/u2f/u2f.h +++ b/src/usb/u2f/u2f.h @@ -15,8 +15,9 @@ #pragma GCC diagnostic ignored "-Wattributes" // General constants -#define U2F_EC_KEY_SIZE 32 -#define U2F_EC_POINT_SIZE ((U2F_EC_KEY_SIZE * 2) + 1) +/* Size of one of the point coordinates on the EC */ +#define U2F_EC_COORD_SIZE 32 +#define U2F_EC_POINT_SIZE ((U2F_EC_COORD_SIZE * 2) + 1) #define U2F_MAX_KH_SIZE 128 // Max size of key handle #define U2F_MAX_ATT_CERT_SIZE 1024 // Max size of attestation certificate #define U2F_MAX_EC_SIG_SIZE 72 // Max size of ANS.1 DER encoded EC signature @@ -28,8 +29,8 @@ typedef struct { uint8_t format; - uint8_t x[U2F_EC_KEY_SIZE]; - uint8_t y[U2F_EC_KEY_SIZE]; + uint8_t x[U2F_EC_COORD_SIZE]; + uint8_t y[U2F_EC_COORD_SIZE]; } U2F_EC_POINT; // U2F native commands From d84e8faad0e98ae5e31c8f12294f6e52713fceca Mon Sep 17 00:00:00 2001 From: Simone Baratta Date: Tue, 31 Mar 2020 15:21:45 +0200 Subject: [PATCH 03/15] Squash me: Revert the crappy change to U2F_EC_KEY_SIZE. --- src/u2f.c | 4 ++-- src/usb/u2f/u2f.h | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/u2f.c b/src/u2f.c index 842300e03..6c3ceb79c 100644 --- a/src/u2f.c +++ b/src/u2f.c @@ -52,7 +52,7 @@ typedef struct { #define APDU_LEN(A) (uint32_t)(((A).lc1 << 16) + ((A).lc2 << 8) + ((A).lc3)) #define U2F_KEYHANDLE_LEN (U2F_NONCE_LENGTH + SHA256_LEN) -#if (U2F_EC_COORD_SIZE != SHA256_LEN) || (U2F_EC_COORD_SIZE != U2F_NONCE_LENGTH) +#if (U2F_EC_KEY_SIZE != SHA256_LEN) || (U2F_EC_KEY_SIZE != U2F_NONCE_LENGTH) #error "Incorrect macro values for u2f" #endif @@ -528,7 +528,7 @@ static uint16_t _authenticate_verify_key_valid(const USB_APDU* apdu) { uint8_t nonce[U2F_NONCE_LENGTH] = {0}; uint8_t mac[HMAC_SHA256_LEN] = {0}; - uint8_t privkey[U2F_EC_COORD_SIZE]; + uint8_t privkey[U2F_EC_KEY_SIZE]; UTIL_CLEANUP_32(privkey); const U2F_AUTHENTICATE_REQ* auth_request = (const U2F_AUTHENTICATE_REQ*)apdu->data; memcpy(nonce, auth_request->keyHandle + sizeof(mac), sizeof(nonce)); diff --git a/src/usb/u2f/u2f.h b/src/usb/u2f/u2f.h index ee497c9cd..ca57c94b6 100644 --- a/src/usb/u2f/u2f.h +++ b/src/usb/u2f/u2f.h @@ -16,8 +16,8 @@ // General constants /* Size of one of the point coordinates on the EC */ -#define U2F_EC_COORD_SIZE 32 -#define U2F_EC_POINT_SIZE ((U2F_EC_COORD_SIZE * 2) + 1) +#define U2F_EC_KEY_SIZE 32 +#define U2F_EC_POINT_SIZE ((U2F_EC_KEY_SIZE * 2) + 1) #define U2F_MAX_KH_SIZE 128 // Max size of key handle #define U2F_MAX_ATT_CERT_SIZE 1024 // Max size of attestation certificate #define U2F_MAX_EC_SIG_SIZE 72 // Max size of ANS.1 DER encoded EC signature @@ -29,8 +29,8 @@ typedef struct { uint8_t format; - uint8_t x[U2F_EC_COORD_SIZE]; - uint8_t y[U2F_EC_COORD_SIZE]; + uint8_t x[U2F_EC_KEY_SIZE]; + uint8_t y[U2F_EC_KEY_SIZE]; } U2F_EC_POINT; // U2F native commands From 2a9a2cd5bec0c4a72cfd12dfbcb8cbbdac000e48 Mon Sep 17 00:00:00 2001 From: Simone Baratta Date: Thu, 16 Jan 2020 16:16:26 +0100 Subject: [PATCH 04/15] u2f: Extract a function to create new keys. Create a new function in u2f_keyhandle that creates new keys. This makes the code cleaner as a side effect. --- src/u2f.c | 17 ++++------------- src/u2f/u2f_keyhandle.c | 16 ++++++++++++++++ src/u2f/u2f_keyhandle.h | 2 ++ 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/u2f.c b/src/u2f.c index 6c3ceb79c..cad37cceb 100644 --- a/src/u2f.c +++ b/src/u2f.c @@ -452,19 +452,10 @@ static void _register_continue(const USB_APDU* apdu, Packet* out_packet) } // Generate keys until a valid one is found - int i = 0; - for (;; ++i) { - if (i == 10) { - _error(U2F_SW_CONDITIONS_NOT_SATISFIED, out_packet); - return; - } - random_32_bytes(nonce); - if (!u2f_keyhandle_gen(reg_request->appId, nonce, privkey, mac)) { - continue; - } - if (securechip_ecc_generate_public_key(privkey, (uint8_t*)&response->pubKey.x)) { - break; - } + bool key_create_success = u2f_keyhandle_create_key(reg_request->appId, nonce, privkey, mac, (uint8_t*)&response->pubKey.x); + if (!key_create_success) { + _error(U2F_SW_CONDITIONS_NOT_SATISFIED, out_packet); + return; } response->pubKey.format = U2F_UNCOMPRESSED_POINT; diff --git a/src/u2f/u2f_keyhandle.c b/src/u2f/u2f_keyhandle.c index 551ce96fa..81e0100a2 100644 --- a/src/u2f/u2f_keyhandle.c +++ b/src/u2f/u2f_keyhandle.c @@ -15,6 +15,8 @@ #include "u2f_keyhandle.h" #include +#include +#include #include #include #include @@ -68,3 +70,17 @@ bool u2f_keyhandle_verify(const uint8_t* appId, const uint8_t* key_handle_buf, s /* Verify the key handle's MAC against the actual one we compute. */ return MEMEQ(key_handle->mac, mac, SHA256_LEN); } + +bool u2f_keyhandle_create_key(const uint8_t* app_id, uint8_t* nonce_out, uint8_t* privkey_out, uint8_t* mac_out, uint8_t* pubkey_out) +{ + for (int i = 0; i < 10; ++i) { + random_32_bytes(nonce_out); + if (!u2f_keyhandle_gen(app_id, nonce_out, privkey_out, mac_out)) { + continue; + } + if (securechip_ecc_generate_public_key(privkey_out, pubkey_out)) { + return true; + } + } + return false; +} diff --git a/src/u2f/u2f_keyhandle.h b/src/u2f/u2f_keyhandle.h index a56f4e500..22e0c016f 100644 --- a/src/u2f/u2f_keyhandle.h +++ b/src/u2f/u2f_keyhandle.h @@ -47,4 +47,6 @@ USE_RESULT bool u2f_keyhandle_verify( const uint8_t* appId, const uint8_t* key_handle_buf, size_t key_handle_len, uint8_t* privkey ); +USE_RESULT bool u2f_keyhandle_create_key(const uint8_t* app_id, uint8_t* nonce_out, uint8_t* privkey_out, uint8_t* mac_out, uint8_t* pubkey_out); + #endif // _U2F_KEYHANDLE_H From d9319b2c5c959e01c19d5048fd4d18c666c2b4c9 Mon Sep 17 00:00:00 2001 From: Simone Baratta Date: Tue, 11 Feb 2020 10:58:23 +0100 Subject: [PATCH 05/15] Bring the timer for U2F timeouts down to 50ms --- src/u2f/u2f_packet.c | 4 +++- src/usb/usb.c | 5 +++-- test/unit-test/framework/mock_hidapi.c | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/u2f/u2f_packet.c b/src/u2f/u2f_packet.c index 9aa9d92ca..e0479818e 100644 --- a/src/u2f/u2f_packet.c +++ b/src/u2f/u2f_packet.c @@ -103,11 +103,13 @@ void u2f_packet_timeout_enable(uint32_t cid) } } +#define U2F_TIMEOUT (500) // [msec] + bool u2f_packet_timeout_get(uint32_t* cid) { for (int i = 0; i < NUM_TIMEOUT_COUNTERS; ++i) { *cid = _timeout_counters[i].cid; - if (_timeout_counters[i].cid != 0 && _timeout_counters[i].counter >= 5) { + if (_timeout_counters[i].cid != 0 && _timeout_counters[i].counter >= (U2F_TIMEOUT / 50)) { return true; } } diff --git a/src/usb/usb.c b/src/usb/usb.c index 021875bc6..99a459a3f 100644 --- a/src/usb/usb.c +++ b/src/usb/usb.c @@ -25,13 +25,14 @@ #include "usb_processing.h" #ifndef TESTING +#include #include +#include #include #include -extern struct timer_descriptor TIMER_0; #endif -#define TIMEOUT_TICK_PERIOD_MS 100 +#define TIMEOUT_TICK_PERIOD_MS 50 #ifndef TESTING static uint8_t _ctrl_endpoint_buffer[USB_REPORT_SIZE]; diff --git a/test/unit-test/framework/mock_hidapi.c b/test/unit-test/framework/mock_hidapi.c index 2739dcf5d..72545a621 100644 --- a/test/unit-test/framework/mock_hidapi.c +++ b/test/unit-test/framework/mock_hidapi.c @@ -52,7 +52,7 @@ void* timer_task(void* args) for (;;) { // printf("tick\n"); u2f_packet_timeout_tick(); - _delay(90); + _delay(40); pthread_mutex_lock(&mutex); if (timer_thread_stop) { pthread_mutex_unlock(&mutex); From cdea7e5d2e003b476dd387402c2a2ba333b2c0b3 Mon Sep 17 00:00:00 2001 From: Simone Baratta Date: Thu, 23 Jan 2020 14:59:22 +0100 Subject: [PATCH 06/15] FIDO2: Add script to generate FIDO2 certificate. --- scripts/create_fido2_attestation_cert | 144 ++++++++++++++++++++++++++ src/fido2/fido2_keys.c | 84 +++++++++++++++ 2 files changed, 228 insertions(+) create mode 100755 scripts/create_fido2_attestation_cert create mode 100644 src/fido2/fido2_keys.c diff --git a/scripts/create_fido2_attestation_cert b/scripts/create_fido2_attestation_cert new file mode 100755 index 000000000..05d4eaad8 --- /dev/null +++ b/scripts/create_fido2_attestation_cert @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 + +import asn1 +import base64 +import os +import subprocess +import sys +import tempfile + +def get_root_cert(base_dir): + return os.path.join(base_dir, 'root_cert.pem') + +def get_final_cert(base_dir): + return os.path.join(base_dir, 'cert.der') + +def get_root_privkey(base_dir): + return os.path.join(base_dir, 'root_privkey.pem') + +def get_final_privkey(base_dir): + return os.path.join(base_dir, 'privkey.pem') + +def run_ssl_command(cmd): + print("Running: {}".format(cmd)) + p = subprocess.Popen(cmd, stdout=sys.stdout, stdin=sys.stdin, stderr=sys.stderr) + p.communicate() + +def generate_privkey(privkey_filename): + openssl_command = ['openssl', 'ecparam', '-name', 'secp256r1', '-genkey', '-out', privkey_filename] + run_ssl_command(openssl_command) + +def parse_privkey(privkey_filename): + p = subprocess.Popen(['openssl', 'ec', '-in', privkey_filename, '-text', '-noout'], stdin=None, stdout=subprocess.PIPE, stderr=sys.stderr) + privkey_out, _ = p.communicate() + privkey_out = privkey_out.decode().splitlines() + print("PVO:\n{}".format(privkey_out)) + privkey_lines = [] + in_privkey = False + for line in privkey_out: + if line[0].isspace(): + if in_privkey: + privkey_lines.append(line) + elif line.strip().startswith('priv:'): + in_privkey = True + else: + in_privkey = False + privkey_data = bytes([int(b, 16) for b in ''.join([l.strip() for l in privkey_lines]).split(':')]) + + assert len(privkey_data) == 32, "Wrong data generated for private key ({}).".format(len(privkey_data)) + return privkey_data + +def generate_root_cert(base_dir): + print("**** Creating root certificate ****") + cert_filename = get_root_cert(base_dir) + privkey_filename = get_root_privkey(base_dir) + openssl_command = ['openssl', 'req', '-new', '-x509', '-key', privkey_filename, '-outform', 'PEM', '-out', cert_filename, '-days', '10000', + '-subj', '/C=CH/ST=Zurich/L=Adliswil/O=Shift Cryptosecurity AG/CN=Shift Cryptosecurity/emailAddress=support@shiftcrypto.ch' + ] + run_ssl_command(openssl_command) + +def generate_cert_file(base_dir): + # We need an extension file in order to specify additional flags; this + # will result in a version 3 certificate as required by FIDO2. + ext_file = os.path.join(base_dir, 'v3.ext') + with open(ext_file, 'w') as f: + f.write("authorityKeyIdentifier=keyid,issuer\nbasicConstraints=CA:FALSE\nkeyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment") + + print("**** Creating final certificate ****") + cert_filename = get_final_cert(base_dir) + csr_filename = os.path.join(base_dir, 'final_csr.csr') + run_ssl_command(['openssl', 'req', '-new', '-key', get_final_privkey(base_dir), '-out', csr_filename, + '-subj', '/C=CH/ST=Zurich/L=Adliswil/O=Shift Cryptosecurity AG/OU=Authenticator Attestation/CN=Shift Cryptosecurity/emailAddress=support@shiftcrypto.ch' + ]) + print("**** Signing final certificate ****") + openssl_command = ['openssl', 'x509', '-req', '-in', csr_filename, '-extfile', ext_file, '-CA', get_root_cert(base_dir), '-CAkey', get_root_privkey(base_dir), '-CAcreateserial', '-outform', 'DER', '-out', cert_filename, '-days', '10000' + ] + run_ssl_command(openssl_command) + with open(cert_filename, 'rb') as cert: + cert_data = cert.read() + return cert_data + + +def dump_bytearray_to_file(f, name, data): + f.write('const uint8_t {}[] = {{'.format(name)) + items_per_line = 10 + line_separator = ',\n ' +# * f.write(', '.join(['0x{:02x}'.format(b) for b in test2cert])) + split_data = [data[x:x+10] for x in range(0, len(data), items_per_line)] + split_lines = [', '.join(['0x{:02x}'.format(b) for b in line]) for line in split_data] + f.write('\n ') + f.write(line_separator.join(split_lines)) + f.write('\n }};\n'.format(line_separator)) + + +def generate_fido2_cert_source(cert_data, privkey): + with open('fido2.c', 'w') as f: + f.write('#include "fido2_keys.h"\n\n') + f.write('/**\n') + f.write(' * Automatically generated with the following command:\n') + f.write(' * {}\n'.format(sys.argv)) + f.write(' */\n') + f.write('\n') + dump_bytearray_to_file(f, 'FIDO2_ATT_PRIV_KEY', privkey) + f.write('\n') + dump_bytearray_to_file(f, 'FIDO2_ATT_CERT', cert_data) + f.write('const size_t FIDO2_ATT_CERT_SIZE = sizeof(FIDO2_ATT_CERT);\n') + +def parse_private_key_from_pem(privkey_data): + privkey_lines = privkey_data.decode().splitlines() + started = False + base64_lines = [] + for line in privkey_lines: + if 'BEGIN EC PRIVATE KEY' in line: + started = True + elif 'END EC PRIVATE KEY' in line: + break + elif started: + base64_lines.append(line.strip()) + base64_data = ''.join(base64_lines) + print("Base64-encoded privkey data: {}".format(base64_data)) + privkey_binary = base64.b64decode(base64_data) + decoder = asn1.Decoder() + decoder.start(privkey_binary) + tag, value = decoder.read() + print('{}, {}'.format(tag, value)) + + +with tempfile.TemporaryDirectory() as tmpdirname: + print("Using temporary directory: {}".format(tmpdirname)) + print("**** Creating root privkey ****") + generate_privkey(get_root_privkey(tmpdirname)) + generate_root_cert(tmpdirname) + print("**** Creating final privkey ****") + generate_privkey(get_final_privkey(tmpdirname)) + privkey_data = parse_privkey(get_final_privkey(tmpdirname)) + cert_data = generate_cert_file(tmpdirname) + print("**** Dumping to fido2.c ****") + generate_fido2_cert_source(cert_data, privkey_data) + print("Done :)") +# +#/** +# * Generated with: +# * test2cert = open('/home/simone/provola/server.der', 'rb').read() +# * f.write(', '.join(['0x{:02x}'.format(b) for b in test2cert])) +# */ diff --git a/src/fido2/fido2_keys.c b/src/fido2/fido2_keys.c new file mode 100644 index 000000000..fa4cc2372 --- /dev/null +++ b/src/fido2/fido2_keys.c @@ -0,0 +1,84 @@ +#include "fido2_keys.h" + +/** + * Automatically generated with the following command: + * ['./scripts/create_fido2_attestation_cert'] + */ + +const uint8_t FIDO2_ATT_PRIV_KEY[] = { + 0xd9, 0x6e, 0x5e, 0x56, 0x34, 0x15, 0x98, 0xa6, 0x98, 0xd6, + 0xd4, 0x9e, 0x5d, 0x15, 0x4f, 0xef, 0x04, 0xe3, 0x45, 0x5c, + 0x19, 0x51, 0xd6, 0x31, 0x79, 0x00, 0x83, 0x20, 0x52, 0xae, + 0x03, 0x71 + }; + +const uint8_t FIDO2_ATT_CERT[] = { + 0x30, 0x82, 0x02, 0x96, 0x30, 0x82, 0x02, 0x3b, 0xa0, 0x03, + 0x02, 0x01, 0x02, 0x02, 0x14, 0x06, 0x80, 0x53, 0x0a, 0x2e, + 0x0e, 0x12, 0xd6, 0xff, 0x31, 0xe5, 0xcd, 0x23, 0xb8, 0x9a, + 0xaf, 0x05, 0xd5, 0x6b, 0xe6, 0x30, 0x0a, 0x06, 0x08, 0x2a, + 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02, 0x30, 0x81, 0x99, + 0x31, 0x0b, 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, + 0x02, 0x43, 0x48, 0x31, 0x0f, 0x30, 0x0d, 0x06, 0x03, 0x55, + 0x04, 0x08, 0x0c, 0x06, 0x5a, 0x75, 0x72, 0x69, 0x63, 0x68, + 0x31, 0x11, 0x30, 0x0f, 0x06, 0x03, 0x55, 0x04, 0x07, 0x0c, + 0x08, 0x41, 0x64, 0x6c, 0x69, 0x73, 0x77, 0x69, 0x6c, 0x31, + 0x20, 0x30, 0x1e, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x17, + 0x53, 0x68, 0x69, 0x66, 0x74, 0x20, 0x43, 0x72, 0x79, 0x70, + 0x74, 0x6f, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, + 0x20, 0x41, 0x47, 0x31, 0x1d, 0x30, 0x1b, 0x06, 0x03, 0x55, + 0x04, 0x03, 0x0c, 0x14, 0x53, 0x68, 0x69, 0x66, 0x74, 0x20, + 0x43, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x73, 0x65, 0x63, 0x75, + 0x72, 0x69, 0x74, 0x79, 0x31, 0x25, 0x30, 0x23, 0x06, 0x09, + 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x01, 0x16, + 0x16, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x40, 0x73, + 0x68, 0x69, 0x66, 0x74, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, + 0x2e, 0x63, 0x68, 0x30, 0x1e, 0x17, 0x0d, 0x32, 0x30, 0x30, + 0x31, 0x32, 0x33, 0x31, 0x30, 0x33, 0x35, 0x32, 0x30, 0x5a, + 0x17, 0x0d, 0x34, 0x37, 0x30, 0x36, 0x31, 0x30, 0x31, 0x30, + 0x33, 0x35, 0x32, 0x30, 0x5a, 0x30, 0x81, 0xbd, 0x31, 0x0b, + 0x30, 0x09, 0x06, 0x03, 0x55, 0x04, 0x06, 0x13, 0x02, 0x43, + 0x48, 0x31, 0x0f, 0x30, 0x0d, 0x06, 0x03, 0x55, 0x04, 0x08, + 0x0c, 0x06, 0x5a, 0x75, 0x72, 0x69, 0x63, 0x68, 0x31, 0x11, + 0x30, 0x0f, 0x06, 0x03, 0x55, 0x04, 0x07, 0x0c, 0x08, 0x41, + 0x64, 0x6c, 0x69, 0x73, 0x77, 0x69, 0x6c, 0x31, 0x20, 0x30, + 0x1e, 0x06, 0x03, 0x55, 0x04, 0x0a, 0x0c, 0x17, 0x53, 0x68, + 0x69, 0x66, 0x74, 0x20, 0x43, 0x72, 0x79, 0x70, 0x74, 0x6f, + 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x20, 0x41, + 0x47, 0x31, 0x22, 0x30, 0x20, 0x06, 0x03, 0x55, 0x04, 0x0b, + 0x0c, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, + 0x63, 0x61, 0x74, 0x6f, 0x72, 0x20, 0x41, 0x74, 0x74, 0x65, + 0x73, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x31, 0x1d, 0x30, + 0x1b, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x14, 0x53, 0x68, + 0x69, 0x66, 0x74, 0x20, 0x43, 0x72, 0x79, 0x70, 0x74, 0x6f, + 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x31, 0x25, + 0x30, 0x23, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, + 0x01, 0x09, 0x01, 0x16, 0x16, 0x73, 0x75, 0x70, 0x70, 0x6f, + 0x72, 0x74, 0x40, 0x73, 0x68, 0x69, 0x66, 0x74, 0x63, 0x72, + 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x63, 0x68, 0x30, 0x59, 0x30, + 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, + 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, + 0x03, 0x42, 0x00, 0x04, 0x1a, 0xc3, 0x9b, 0xb2, 0xf4, 0x56, + 0x0d, 0x16, 0x61, 0x8a, 0x45, 0x5d, 0x3e, 0xef, 0x8e, 0xfd, + 0xd3, 0x36, 0xab, 0x8e, 0x79, 0x23, 0x2a, 0x19, 0x72, 0xa3, + 0x7c, 0x66, 0xb9, 0x6a, 0x17, 0xf3, 0x0c, 0xe9, 0x44, 0x68, + 0x35, 0xfe, 0xc5, 0x72, 0x9a, 0x06, 0x86, 0x29, 0xed, 0x16, + 0x2f, 0xe5, 0xb9, 0xdf, 0xa4, 0x99, 0xd7, 0x10, 0xd2, 0x82, + 0xfe, 0x76, 0x8e, 0x92, 0x6e, 0x57, 0xb1, 0x5a, 0xa3, 0x3b, + 0x30, 0x39, 0x30, 0x1f, 0x06, 0x03, 0x55, 0x1d, 0x23, 0x04, + 0x18, 0x30, 0x16, 0x80, 0x14, 0x4f, 0x1b, 0x24, 0x4f, 0x6b, + 0x2b, 0x20, 0xfe, 0xf7, 0x64, 0xcc, 0x7d, 0x95, 0x84, 0x7e, + 0x57, 0x1a, 0x12, 0x94, 0xa3, 0x30, 0x09, 0x06, 0x03, 0x55, + 0x1d, 0x13, 0x04, 0x02, 0x30, 0x00, 0x30, 0x0b, 0x06, 0x03, + 0x55, 0x1d, 0x0f, 0x04, 0x04, 0x03, 0x02, 0x04, 0xf0, 0x30, + 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, + 0x02, 0x03, 0x49, 0x00, 0x30, 0x46, 0x02, 0x21, 0x00, 0xfd, + 0xde, 0x3c, 0x2d, 0x28, 0x67, 0x07, 0x45, 0x47, 0x1e, 0x85, + 0x58, 0x71, 0x89, 0x63, 0x77, 0x90, 0xd7, 0x34, 0x9b, 0x1f, + 0x27, 0x7d, 0xcd, 0xba, 0x73, 0x93, 0x67, 0xad, 0xac, 0x53, + 0x91, 0x02, 0x21, 0x00, 0xf4, 0x0d, 0xb4, 0x37, 0xbe, 0x62, + 0x63, 0x34, 0x86, 0xb5, 0xea, 0xbf, 0x86, 0x77, 0x6c, 0x31, + 0x38, 0x8b, 0x4b, 0xc4, 0x0d, 0x34, 0x9b, 0x75, 0x3b, 0x11, + 0x4c, 0x7f, 0x19, 0x5b, 0x43, 0x9e + }; +const size_t FIDO2_ATT_CERT_SIZE = sizeof(FIDO2_ATT_CERT); From 0236d2b0b17b4cebdda4ba034cb699a01db4d450 Mon Sep 17 00:00:00 2001 From: Simone Baratta Date: Mon, 2 Mar 2020 17:29:12 +0100 Subject: [PATCH 07/15] USB processing: Start decoupling USB receive and USB sends. USB sends can and will be sent only through a straight path starting when a USB packet is received. This is inappropriate as it forces processing the whole USB packet at once (hence blocking workflows etc). Start moving the responsibility for packet sending out of the main USB rx function - although the coupling is still there for now. --- src/bootloader/bootloader.c | 9 ++- src/hww.c | 11 +++- src/queue.c | 8 +++ src/queue.h | 10 ++++ src/u2f.c | 106 ++++++++++++++++++++++++------------ src/usart/usart_frame.c | 29 +++------- src/usart/usart_frame.h | 2 +- src/usb/usb_frame.c | 17 ++---- src/usb/usb_frame.h | 4 +- src/usb/usb_packet.h | 2 +- src/usb/usb_processing.c | 20 +++---- src/usb/usb_processing.h | 10 +++- 12 files changed, 139 insertions(+), 89 deletions(-) diff --git a/src/bootloader/bootloader.c b/src/bootloader/bootloader.c index f5f89a748..1f2900e0e 100644 --- a/src/bootloader/bootloader.c +++ b/src/bootloader/bootloader.c @@ -843,10 +843,13 @@ static size_t _api_command(const uint8_t* input, uint8_t* output, const size_t m return len; } -static void _api_msg(const Packet* in_packet, Packet* out_packet, const size_t max_out_len) +static void _api_msg(const Packet* in_packet) { - size_t len = _api_command(in_packet->data_addr, out_packet->data_addr, max_out_len); - out_packet->len = len; + Packet out_packet; + prepare_usb_packet(in_packet->cmd, in_packet->cid, &out_packet); + size_t len = _api_command(in_packet->data_addr, out_packet.data_addr, USB_DATA_MAX_LEN); + out_packet.len = len; + usb_processing_send_packet(usb_processing_hww(), &out_packet); } static void _api_setup(void) diff --git a/src/hww.c b/src/hww.c index c7cd30bb8..d3025ab4a 100644 --- a/src/hww.c +++ b/src/hww.c @@ -188,7 +188,7 @@ static void _process_packet(const in_buffer_t* in_req, buffer_t* out_rsp) } } -static void _msg(const Packet* in_packet, Packet* out_packet, const size_t max_out_len) +static void _process_msg(const Packet* in_packet, Packet* out_packet) { if (in_packet->len == 0) { out_packet->data_addr[0] = HWW_RSP_NACK; @@ -212,7 +212,7 @@ static void _msg(const Packet* in_packet, Packet* out_packet, const size_t max_o }; hww_packet_rsp_t response = { .status = HWW_RSP_NACK, - .buffer = {.data = out_packet->data_addr + 1, .len = 0, .max_len = max_out_len - 1}}; + .buffer = {.data = out_packet->data_addr + 1, .len = 0, .max_len = USB_DATA_MAX_LEN - 1}}; switch (cmd) { case HWW_REQ_NEW: _process_packet(&decoded_buffer, &response.buffer); @@ -237,6 +237,13 @@ static void _msg(const Packet* in_packet, Packet* out_packet, const size_t max_o } } +static void _msg(const Packet* in_packet) { + Packet out_packet; + prepare_usb_packet(in_packet->cmd, in_packet->cid, &out_packet); + _process_msg(in_packet, &out_packet); + usb_processing_send_packet(usb_processing_hww(), &out_packet); +} + bool hww_blocking_request_can_go_through(const Packet* in_packet) { if (in_packet->len != 1) { diff --git a/src/queue.c b/src/queue.c index 5fc3c7138..cf3cdbb62 100644 --- a/src/queue.c +++ b/src/queue.c @@ -127,6 +127,14 @@ queue_error_t queue_push(struct queue* ctx, const uint8_t* data) return result; } +void queue_push_retry(struct queue* ctx, const uint8_t* data) +{ + queue_error_t result = QUEUE_ERR_FULL; + while (result != QUEUE_ERR_NONE) { + result = queue_push(ctx, data); + } +} + /** * Thread-unsafe version of queue_peek. */ diff --git a/src/queue.h b/src/queue.h index 173c7ad78..eca7d72cb 100644 --- a/src/queue.h +++ b/src/queue.h @@ -32,6 +32,16 @@ struct queue; */ queue_error_t queue_push(struct queue* ctx, const uint8_t* data); +/** + * Append the given data to the queue. If the queue is full, retry until data is available again. + * + * Calling this function from interrupt context should never be done: it will block forever + * as nothing will ever be able to pop data from the queue! + * + * @param[in] data Frame to be sent. Must be USB_REPORT_SIZE large. + */ +void queue_push_retry(struct queue* ctx, const uint8_t* data); + /** * Return the first data that was added to the queue. * Returns NULL if empty diff --git a/src/u2f.c b/src/u2f.c index cad37cceb..b2539518d 100644 --- a/src/u2f.c +++ b/src/u2f.c @@ -77,6 +77,14 @@ typedef struct { * (blocking) on the U2F stack. */ uint8_t last_cmd; + /** + * last_cmd points to a valid byte, + * and incoming requests that match that + * command should be allowed. This is true + * for blocking U2F requests, but false for CTAP + * requests (as CTAP requests are not sent multiple times). + */ + bool allow_cmd_retries; /** * Keeps track of whether there is an outstanding * U2F operation going on in the background. @@ -174,7 +182,12 @@ static void _lock(const USB_APDU* apdu) { usb_processing_lock(usb_processing_u2f()); _state.locked = true; - _state.last_cmd = apdu->ins; + if (apdu) { + _state.last_cmd = apdu->ins; + _state.allow_cmd_retries = true; + } else { + _state.allow_cmd_retries = false; + } } static component_t* _nudge_label = NULL; @@ -671,43 +684,50 @@ static void _authenticate_continue(const USB_APDU* apdu, Packet* out_packet) _fill_message(buf, auth_packet_len + 2, out_packet); } -static void _cmd_ping(const Packet* in_packet, Packet* out_packet, const size_t max_out_len) +static void _cmd_ping(const Packet* in_packet) { - (void)max_out_len; + Packet out_packet; + prepare_usb_packet(in_packet->cmd, in_packet->cid, &out_packet); // 0 and broadcast are reserved if (in_packet->cid == U2FHID_CID_BROADCAST || in_packet->cid == 0) { - _error_hid(in_packet->cid, U2FHID_ERR_INVALID_CID, out_packet); + _error_hid(in_packet->cid, U2FHID_ERR_INVALID_CID, &out_packet); + usb_processing_send_packet(usb_processing_u2f(), &out_packet); return; } - util_zero(out_packet->data_addr, sizeof(out_packet->data_addr)); + util_zero(out_packet.data_addr, sizeof(out_packet.data_addr)); size_t max = MIN(in_packet->len, USB_DATA_MAX_LEN); - memcpy(out_packet->data_addr, in_packet->data_addr, max); - out_packet->len = in_packet->len; - out_packet->cmd = U2FHID_PING; - out_packet->cid = in_packet->cid; + memcpy(out_packet.data_addr, in_packet->data_addr, max); + out_packet.len = in_packet->len; + out_packet.cmd = U2FHID_PING; + out_packet.cid = in_packet->cid; + usb_processing_send_packet(usb_processing_u2f(), &out_packet); } -static void _cmd_wink(const Packet* in_packet, Packet* out_packet, const size_t max_out_len) +static void _cmd_wink(const Packet* in_packet) { - (void)max_out_len; + Packet out_packet; + prepare_usb_packet(in_packet->cmd, in_packet->cid, &out_packet); // 0 and broadcast are reserved if (in_packet->cid == U2FHID_CID_BROADCAST || in_packet->cid == 0) { - _error_hid(in_packet->cid, U2FHID_ERR_INVALID_CID, out_packet); + _error_hid(in_packet->cid, U2FHID_ERR_INVALID_CID, &out_packet); + usb_processing_send_packet(usb_processing_u2f(), &out_packet); return; } if (in_packet->len > 0) { - _error_hid(in_packet->cid, U2FHID_ERR_INVALID_LEN, out_packet); + _error_hid(in_packet->cid, U2FHID_ERR_INVALID_LEN, &out_packet); + usb_processing_send_packet(usb_processing_u2f(), &out_packet); return; } - util_zero(out_packet->data_addr, sizeof(out_packet->data_addr)); - out_packet->len = 0; - out_packet->cmd = U2FHID_WINK; - out_packet->cid = in_packet->cid; + util_zero(out_packet.data_addr, sizeof(out_packet.data_addr)); + out_packet.len = 0; + out_packet.cmd = U2FHID_WINK; + out_packet.cid = in_packet->cid; + usb_processing_send_packet(usb_processing_u2f(), &out_packet); } /** @@ -717,25 +737,30 @@ static void _cmd_wink(const Packet* in_packet, Packet* out_packet, const size_t * If the CID is the U2FHID_CID_BROADCAST then the application is requesting a CID from the device. * Otherwise the application has chosen the CID. */ -static void _cmd_init(const Packet* in_packet, Packet* out_packet, const size_t max_out_len) +static void _cmd_init(const Packet* in_packet) { - if (U2FHID_INIT_RESP_SIZE >= max_out_len) { - _error_hid(in_packet->cid, U2FHID_ERR_OTHER, out_packet); + Packet out_packet; + prepare_usb_packet(in_packet->cmd, in_packet->cid, &out_packet); + + if (U2FHID_INIT_RESP_SIZE >= USB_DATA_MAX_LEN) { + _error_hid(in_packet->cid, U2FHID_ERR_OTHER, &out_packet); + usb_processing_send_packet(usb_processing_u2f(), &out_packet); return; } // Channel 0 is reserved if (in_packet->cid == 0) { - _error_hid(in_packet->cid, U2FHID_ERR_INVALID_CID, out_packet); + _error_hid(in_packet->cid, U2FHID_ERR_INVALID_CID, &out_packet); + usb_processing_send_packet(usb_processing_u2f(), &out_packet); return; } const U2FHID_INIT_REQ* init_req = (const U2FHID_INIT_REQ*)&in_packet->data_addr; U2FHID_INIT_RESP response; - out_packet->cid = in_packet->cid; - out_packet->cmd = U2FHID_INIT; - out_packet->len = U2FHID_INIT_RESP_SIZE; + out_packet.cid = in_packet->cid; + out_packet.cmd = U2FHID_INIT; + out_packet.len = U2FHID_INIT_RESP_SIZE; util_zero(&response, sizeof(response)); memcpy(response.nonce, init_req->nonce, sizeof(init_req->nonce)); @@ -745,8 +770,9 @@ static void _cmd_init(const Packet* in_packet, Packet* out_packet, const size_t response.versionMinor = DIGITAL_BITBOX_VERSION_MINOR; response.versionBuild = DIGITAL_BITBOX_VERSION_PATCH; response.capFlags = U2FHID_CAPFLAG_WINK; - util_zero(out_packet->data_addr, sizeof(out_packet->data_addr)); - memcpy(out_packet->data_addr, &response, sizeof(response)); + util_zero(out_packet.data_addr, sizeof(out_packet.data_addr)); + memcpy(out_packet.data_addr, &response, sizeof(response)); + usb_processing_send_packet(usb_processing_u2f(), &out_packet); } /** @@ -860,9 +886,11 @@ static void _abort_authenticate(void) /** * Process a U2F message */ -static void _cmd_msg(const Packet* in_packet, Packet* out_packet, const size_t max_out_len) +static void _cmd_msg(const Packet* in_packet) { - (void)max_out_len; + Packet out_packet; + prepare_usb_packet(in_packet->cmd, in_packet->cid, &out_packet); + // By default always use the recieved cid _state.cid = in_packet->cid; @@ -873,23 +901,27 @@ static void _cmd_msg(const Packet* in_packet, Packet* out_packet, const size_t m } if (apdu->cla != 0) { - _error(U2F_SW_CLA_NOT_SUPPORTED, out_packet); + _error(U2F_SW_CLA_NOT_SUPPORTED, &out_packet); + usb_processing_send_packet(usb_processing_u2f(), &out_packet); return; } switch (apdu->ins) { case U2F_REGISTER: - _cmd_register(in_packet, out_packet); + _cmd_register(in_packet, &out_packet); + usb_processing_send_packet(usb_processing_u2f(), &out_packet); break; case U2F_AUTHENTICATE: - _cmd_authenticate(in_packet, out_packet); + _cmd_authenticate(in_packet, &out_packet); + usb_processing_send_packet(usb_processing_u2f(), &out_packet); break; case U2F_VERSION: - _version(apdu, out_packet); + _version(apdu, &out_packet); + usb_processing_send_packet(usb_processing_u2f(), &out_packet); break; default: - _error(U2F_SW_INS_NOT_SUPPORTED, out_packet); - return; + _error(U2F_SW_INS_NOT_SUPPORTED, &out_packet); + usb_processing_send_packet(usb_processing_u2f(), &out_packet); } } @@ -898,6 +930,11 @@ bool u2f_blocking_request_can_go_through(const Packet* in_packet) if (!_state.locked) { Abort("USB stack thinks we're busy, but we're not."); } + + if (!_state.allow_cmd_retries) { + /* We're blocking every command, including retries of the last command. */ + return false; + } /* * Check if this request is the same one we're currently operating on. * For now, this checks the request type and channel ID only. @@ -1002,6 +1039,7 @@ void u2f_process(void) } } + /** * Set up the U2F commands. */ diff --git a/src/usart/usart_frame.c b/src/usart/usart_frame.c index aed6147ff..276902b0b 100644 --- a/src/usart/usart_frame.c +++ b/src/usart/usart_frame.c @@ -268,14 +268,11 @@ static size_t n_pushed = 0; #define USART_FRAME_PUSH_BYTE(x) \ do { \ uint8_t to_push = x; \ - queue_error_t res = queue_push(queue, &to_push); \ - if (res != QUEUE_ERR_NONE) { \ - return res; \ - } \ + queue_push_retry(queue, &to_push); \ n_pushed++; \ } while (0) -static queue_error_t _usart_encode_push_byte(uint8_t b, struct queue* queue) +static void _usart_encode_push_byte(uint8_t b, struct queue* queue) { if (b == USART_FRAME_FLAG_BYTE || b == USART_FRAME_ESCAPE_BYTE) { // Escape special framing bytes. @@ -284,18 +281,9 @@ static queue_error_t _usart_encode_push_byte(uint8_t b, struct queue* queue) } else { USART_FRAME_PUSH_BYTE(b); } - return QUEUE_ERR_NONE; } -#define USART_FRAME_PUSH_ENCODED_BYTE(x) \ - do { \ - queue_error_t res = _usart_encode_push_byte(x, queue); \ - if (res != QUEUE_ERR_NONE) { \ - return res; \ - } \ - } while (0) - -queue_error_t usart_format_frame( +void usart_format_frame( uint8_t src_endpoint, const uint8_t* data, uint32_t len, @@ -305,15 +293,14 @@ queue_error_t usart_format_frame( (void)cid; USART_FRAME_PUSH_BYTE(USART_FRAME_FLAG_BYTE); // Version == 0x01 - USART_FRAME_PUSH_ENCODED_BYTE(0x01); - USART_FRAME_PUSH_ENCODED_BYTE(src_endpoint); + _usart_encode_push_byte(0x01, queue); + _usart_encode_push_byte(src_endpoint, queue); for (uint32_t i = 0; i < len; ++i) { - USART_FRAME_PUSH_ENCODED_BYTE(data[i]); + _usart_encode_push_byte(data[i], queue); } uint16_t cs = _compute_send_checksum(0x01, src_endpoint, data, len); uint8_t* cs_buf = (uint8_t*)&cs; - USART_FRAME_PUSH_ENCODED_BYTE(cs_buf[0]); - USART_FRAME_PUSH_ENCODED_BYTE(cs_buf[1]); + _usart_encode_push_byte(cs_buf[0], queue); + _usart_encode_push_byte(cs_buf[1], queue); USART_FRAME_PUSH_BYTE(USART_FRAME_FLAG_BYTE); - return QUEUE_ERR_NONE; } diff --git a/src/usart/usart_frame.h b/src/usart/usart_frame.h index b2410d360..0f267fd86 100644 --- a/src/usart/usart_frame.h +++ b/src/usart/usart_frame.h @@ -46,7 +46,7 @@ void usart_invalid_endpoint(struct queue* queue, uint32_t cid); /** * Creates a data frame for sending over USART. */ -queue_error_t usart_format_frame( +void usart_format_frame( uint8_t src_endpoint, const uint8_t* data, uint32_t len, diff --git a/src/usb/usb_frame.c b/src/usb/usb_frame.c index 64a036b0d..a1575e195 100644 --- a/src/usb/usb_frame.c +++ b/src/usb/usb_frame.c @@ -122,7 +122,7 @@ static int32_t _cmd_continue(const USB_FRAME* frame, State* state) * Prepares USB frames to be send to the host. * param[in] data The data is copied into one or more frames */ -queue_error_t usb_frame_reply( +void usb_frame_reply( uint8_t cmd, const uint8_t* data, uint32_t len, @@ -144,10 +144,7 @@ queue_error_t usb_frame_reply( // Init frame psz = MIN(sizeof(frame.init.data), l); memcpy(frame.init.data, data, psz); - queue_error_t err = queue_push(queue, (const uint8_t*)&frame); - if (err != QUEUE_ERR_NONE) { - return err; - } + queue_push_retry(queue, (const uint8_t*)&frame); l -= psz; cnt += psz; @@ -157,12 +154,8 @@ queue_error_t usb_frame_reply( frame.cont.seq = seq++; psz = MIN(sizeof(frame.cont.data), l); memcpy(frame.cont.data, data + cnt, psz); - err = queue_push(queue, (const uint8_t*)&frame); - if (err != QUEUE_ERR_NONE) { - return err; - } + queue_push_retry(queue, (const uint8_t*)&frame); } - return QUEUE_ERR_NONE; } /** @@ -172,7 +165,7 @@ queue_error_t usb_frame_reply( * @param[in] cid The channel id. * @param[in] add_frame_callback The callback to which we add the frame. */ -queue_error_t usb_frame_prepare_err(uint8_t err, uint32_t cid, struct queue* queue) +void usb_frame_prepare_err(uint8_t err, uint32_t cid, struct queue* queue) { USB_FRAME frame; @@ -181,7 +174,7 @@ queue_error_t usb_frame_prepare_err(uint8_t err, uint32_t cid, struct queue* que frame.init.cmd = FRAME_ERROR; frame.init.bcntl = 1; frame.init.data[0] = err; - return queue_push(queue, (const uint8_t*)&frame); + queue_push_retry(queue, (const uint8_t*)&frame); } /** diff --git a/src/usb/usb_frame.h b/src/usb/usb_frame.h index 7d16bd612..79944a93b 100644 --- a/src/usb/usb_frame.h +++ b/src/usb/usb_frame.h @@ -103,7 +103,7 @@ typedef struct { * @param[in] cid The channel ID. * @param[in] add_frame_callback The callback to which the prepared frames are passed to. */ -queue_error_t usb_frame_reply( +void usb_frame_reply( uint8_t cmd, const uint8_t* data, uint32_t len, @@ -129,7 +129,7 @@ void usb_frame_send_cmd(uint8_t cmd, const uint8_t* data, uint32_t len, uint8_t * @param[in] err The error send to the host. * @param[in] add_frame_callback The callback to which we add the frame. */ -queue_error_t usb_frame_prepare_err(uint8_t err, uint32_t cid, struct queue* queue); +void usb_frame_prepare_err(uint8_t err, uint32_t cid, struct queue* queue); /** * Processes usb frame requests. diff --git a/src/usb/usb_packet.h b/src/usb/usb_packet.h index a037f6994..8c165a98f 100644 --- a/src/usb/usb_packet.h +++ b/src/usb/usb_packet.h @@ -36,7 +36,7 @@ typedef struct { */ typedef struct { uint8_t cmd; - void (*process_cmd)(const Packet*, Packet*, const size_t); + void (*process_cmd)(const Packet*); } CMD_Callback; /** diff --git a/src/usb/usb_processing.c b/src/usb/usb_processing.c index cd3fadf91..adc760641 100644 --- a/src/usb/usb_processing.c +++ b/src/usb/usb_processing.c @@ -132,9 +132,9 @@ static usb_processing_state_t _usb_state = {0}; * Responds with data of a certain length. * @param[in] packet The packet to be sent. */ -static queue_error_t _enqueue_frames(struct usb_processing* ctx, const Packet* out_packet) +void usb_processing_send_packet(struct usb_processing* ctx, const Packet* out_packet) { - return ctx->format_frame( + ctx->format_frame( out_packet->cmd, out_packet->data_addr, out_packet->len, out_packet->cid, ctx->out_queue()); } @@ -153,12 +153,12 @@ static void _build_packet(const uint8_t* buf, size_t length, uint8_t cmd, uint32 /** * Prepares an outgoing packet. */ -static void _prepare_out_packet(const Packet* in_packet, Packet* out_packet) +void prepare_usb_packet(uint8_t cmd, uint32_t cid, Packet* out_packet) { memset(out_packet->data_addr, 0, sizeof(out_packet->data_addr)); out_packet->len = 0; - out_packet->cmd = in_packet->cmd; - out_packet->cid = in_packet->cid; + out_packet->cmd = cmd; + out_packet->cid = cid; } /** @@ -245,11 +245,7 @@ static void _usb_execute_packet(struct usb_processing* ctx, const Packet* in_pac if (in_packet->cmd == ctx->registered_cmds[i].cmd) { cmd_valid = true; // process_cmd calls commander(...) or U2F functions. - - Packet out_packet; - _prepare_out_packet(in_packet, &out_packet); - ctx->registered_cmds[i].process_cmd(in_packet, &out_packet, USB_DATA_MAX_LEN); - _enqueue_frames(ctx, (const Packet*)&out_packet); + ctx->registered_cmds[i].process_cmd(in_packet); break; } } @@ -287,9 +283,9 @@ static void _usb_arbitrate_packet(struct usb_processing* ctx, const Packet* in_p if (!can_go_through) { /* The receiving state should send back an error */ Packet out_packet; - _prepare_out_packet(in_packet, &out_packet); + prepare_usb_packet(in_packet->cmd, in_packet->cid, &out_packet); ctx->create_blocked_req_error(&out_packet, in_packet); - _enqueue_frames(ctx, &out_packet); + usb_processing_send_packet(ctx, &out_packet); } else { _usb_execute_packet(ctx, in_packet); /* New packet processed: reset the watchdog timeout. */ diff --git a/src/usb/usb_processing.h b/src/usb/usb_processing.h index 73a3c6bf1..6add00522 100644 --- a/src/usb/usb_processing.h +++ b/src/usb/usb_processing.h @@ -36,7 +36,7 @@ void usb_processing_register_cmds( * Prepares USB frames to be send to the host. * param[in] data The data is copied into one or more frames */ -typedef queue_error_t (*usb_frame_formatter_t)( +typedef void (*usb_frame_formatter_t)( const uint8_t cmd, const uint8_t* data, const uint32_t len, @@ -60,11 +60,19 @@ void usb_processing_process(struct usb_processing* ctx); void usb_processing_set_send(struct usb_processing* ctx, void (*send)(void)); +void usb_processing_send_packet(struct usb_processing* ctx, const Packet* out_packet); + struct usb_processing* usb_processing_u2f(void); struct usb_processing* usb_processing_hww(void); void usb_processing_init(void); +/** + * Initializes a packet as a response to the given incoming + * packet. + */ +void prepare_usb_packet(uint8_t cmd, uint32_t cid, Packet* out_packet); + #if !defined(BOOTLOADER) void usb_processing_lock(struct usb_processing* ctx); From 6ea40fc5cb653e6ef78449181461aedc8b44bd84 Mon Sep 17 00:00:00 2001 From: Simone Baratta Date: Mon, 30 Mar 2020 16:51:53 +0200 Subject: [PATCH 08/15] Squash me: allocate packet memory on the stack. --- src/bootloader/bootloader.c | 11 +++-- src/hww.c | 9 ++-- src/u2f.c | 99 ++++++++++++++++++++----------------- src/usb/usb_processing.c | 9 ++-- src/util.c | 9 ++++ src/util.h | 10 ++++ 6 files changed, 88 insertions(+), 59 deletions(-) diff --git a/src/bootloader/bootloader.c b/src/bootloader/bootloader.c index 1f2900e0e..2b6d051c1 100644 --- a/src/bootloader/bootloader.c +++ b/src/bootloader/bootloader.c @@ -845,11 +845,12 @@ static size_t _api_command(const uint8_t* input, uint8_t* output, const size_t m static void _api_msg(const Packet* in_packet) { - Packet out_packet; - prepare_usb_packet(in_packet->cmd, in_packet->cid, &out_packet); - size_t len = _api_command(in_packet->data_addr, out_packet.data_addr, USB_DATA_MAX_LEN); - out_packet.len = len; - usb_processing_send_packet(usb_processing_hww(), &out_packet); + Packet* out_packet = util_malloc(sizeof(*out_packet)); + prepare_usb_packet(in_packet->cmd, in_packet->cid, out_packet); + size_t len = _api_command(in_packet->data_addr, out_packet->data_addr, USB_DATA_MAX_LEN); + out_packet->len = len; + usb_processing_send_packet(usb_processing_hww(), out_packet); + free(out_packet); } static void _api_setup(void) diff --git a/src/hww.c b/src/hww.c index d3025ab4a..fa33f02c6 100644 --- a/src/hww.c +++ b/src/hww.c @@ -238,10 +238,11 @@ static void _process_msg(const Packet* in_packet, Packet* out_packet) } static void _msg(const Packet* in_packet) { - Packet out_packet; - prepare_usb_packet(in_packet->cmd, in_packet->cid, &out_packet); - _process_msg(in_packet, &out_packet); - usb_processing_send_packet(usb_processing_hww(), &out_packet); + Packet* out_packet = util_malloc(sizeof(*out_packet)); + prepare_usb_packet(in_packet->cmd, in_packet->cid, out_packet); + _process_msg(in_packet, out_packet); + usb_processing_send_packet(usb_processing_hww(), out_packet); + free(out_packet); } bool hww_blocking_request_can_go_through(const Packet* in_packet) diff --git a/src/u2f.c b/src/u2f.c index b2539518d..83d4ce8c2 100644 --- a/src/u2f.c +++ b/src/u2f.c @@ -686,48 +686,53 @@ static void _authenticate_continue(const USB_APDU* apdu, Packet* out_packet) static void _cmd_ping(const Packet* in_packet) { - Packet out_packet; - prepare_usb_packet(in_packet->cmd, in_packet->cid, &out_packet); + Packet* out_packet = util_malloc(sizeof(*out_packet)); + prepare_usb_packet(in_packet->cmd, in_packet->cid, out_packet); // 0 and broadcast are reserved if (in_packet->cid == U2FHID_CID_BROADCAST || in_packet->cid == 0) { - _error_hid(in_packet->cid, U2FHID_ERR_INVALID_CID, &out_packet); - usb_processing_send_packet(usb_processing_u2f(), &out_packet); + _error_hid(in_packet->cid, U2FHID_ERR_INVALID_CID, out_packet); + usb_processing_send_packet(usb_processing_u2f(), out_packet); + free(out_packet); return; } - util_zero(out_packet.data_addr, sizeof(out_packet.data_addr)); + util_zero(out_packet->data_addr, sizeof(out_packet->data_addr)); size_t max = MIN(in_packet->len, USB_DATA_MAX_LEN); - memcpy(out_packet.data_addr, in_packet->data_addr, max); - out_packet.len = in_packet->len; - out_packet.cmd = U2FHID_PING; - out_packet.cid = in_packet->cid; - usb_processing_send_packet(usb_processing_u2f(), &out_packet); + memcpy(out_packet->data_addr, in_packet->data_addr, max); + out_packet->len = in_packet->len; + out_packet->cmd = U2FHID_PING; + out_packet->cid = in_packet->cid; + usb_processing_send_packet(usb_processing_u2f(), out_packet); + free(out_packet); } static void _cmd_wink(const Packet* in_packet) { - Packet out_packet; - prepare_usb_packet(in_packet->cmd, in_packet->cid, &out_packet); + Packet* out_packet = util_malloc(sizeof(*out_packet)); + prepare_usb_packet(in_packet->cmd, in_packet->cid, out_packet); // 0 and broadcast are reserved if (in_packet->cid == U2FHID_CID_BROADCAST || in_packet->cid == 0) { - _error_hid(in_packet->cid, U2FHID_ERR_INVALID_CID, &out_packet); - usb_processing_send_packet(usb_processing_u2f(), &out_packet); + _error_hid(in_packet->cid, U2FHID_ERR_INVALID_CID, out_packet); + usb_processing_send_packet(usb_processing_u2f(), out_packet); + free(out_packet); return; } if (in_packet->len > 0) { - _error_hid(in_packet->cid, U2FHID_ERR_INVALID_LEN, &out_packet); - usb_processing_send_packet(usb_processing_u2f(), &out_packet); + _error_hid(in_packet->cid, U2FHID_ERR_INVALID_LEN, out_packet); + usb_processing_send_packet(usb_processing_u2f(), out_packet); + free(out_packet); return; } - util_zero(out_packet.data_addr, sizeof(out_packet.data_addr)); - out_packet.len = 0; - out_packet.cmd = U2FHID_WINK; - out_packet.cid = in_packet->cid; - usb_processing_send_packet(usb_processing_u2f(), &out_packet); + util_zero(out_packet->data_addr, sizeof(out_packet->data_addr)); + out_packet->len = 0; + out_packet->cmd = U2FHID_WINK; + out_packet->cid = in_packet->cid; + usb_processing_send_packet(usb_processing_u2f(), out_packet); + free(out_packet); } /** @@ -739,28 +744,30 @@ static void _cmd_wink(const Packet* in_packet) */ static void _cmd_init(const Packet* in_packet) { - Packet out_packet; - prepare_usb_packet(in_packet->cmd, in_packet->cid, &out_packet); + Packet* out_packet = util_malloc(sizeof(*out_packet)); + prepare_usb_packet(in_packet->cmd, in_packet->cid, out_packet); if (U2FHID_INIT_RESP_SIZE >= USB_DATA_MAX_LEN) { - _error_hid(in_packet->cid, U2FHID_ERR_OTHER, &out_packet); - usb_processing_send_packet(usb_processing_u2f(), &out_packet); + _error_hid(in_packet->cid, U2FHID_ERR_OTHER, out_packet); + usb_processing_send_packet(usb_processing_u2f(), out_packet); + free(out_packet); return; } // Channel 0 is reserved if (in_packet->cid == 0) { - _error_hid(in_packet->cid, U2FHID_ERR_INVALID_CID, &out_packet); - usb_processing_send_packet(usb_processing_u2f(), &out_packet); + _error_hid(in_packet->cid, U2FHID_ERR_INVALID_CID, out_packet); + usb_processing_send_packet(usb_processing_u2f(), out_packet); + free(out_packet); return; } const U2FHID_INIT_REQ* init_req = (const U2FHID_INIT_REQ*)&in_packet->data_addr; U2FHID_INIT_RESP response; - out_packet.cid = in_packet->cid; - out_packet.cmd = U2FHID_INIT; - out_packet.len = U2FHID_INIT_RESP_SIZE; + out_packet->cid = in_packet->cid; + out_packet->cmd = U2FHID_INIT; + out_packet->len = U2FHID_INIT_RESP_SIZE; util_zero(&response, sizeof(response)); memcpy(response.nonce, init_req->nonce, sizeof(init_req->nonce)); @@ -770,9 +777,10 @@ static void _cmd_init(const Packet* in_packet) response.versionMinor = DIGITAL_BITBOX_VERSION_MINOR; response.versionBuild = DIGITAL_BITBOX_VERSION_PATCH; response.capFlags = U2FHID_CAPFLAG_WINK; - util_zero(out_packet.data_addr, sizeof(out_packet.data_addr)); - memcpy(out_packet.data_addr, &response, sizeof(response)); - usb_processing_send_packet(usb_processing_u2f(), &out_packet); + util_zero(out_packet->data_addr, sizeof(out_packet->data_addr)); + memcpy(out_packet->data_addr, &response, sizeof(response)); + usb_processing_send_packet(usb_processing_u2f(), out_packet); + free(out_packet); } /** @@ -888,9 +896,6 @@ static void _abort_authenticate(void) */ static void _cmd_msg(const Packet* in_packet) { - Packet out_packet; - prepare_usb_packet(in_packet->cmd, in_packet->cid, &out_packet); - // By default always use the recieved cid _state.cid = in_packet->cid; @@ -900,29 +905,31 @@ static void _cmd_msg(const Packet* in_packet) return; } + Packet* out_packet = util_malloc(sizeof(*out_packet)); + prepare_usb_packet(in_packet->cmd, in_packet->cid, out_packet); + if (apdu->cla != 0) { - _error(U2F_SW_CLA_NOT_SUPPORTED, &out_packet); - usb_processing_send_packet(usb_processing_u2f(), &out_packet); + _error(U2F_SW_CLA_NOT_SUPPORTED, out_packet); + usb_processing_send_packet(usb_processing_u2f(), out_packet); + free(out_packet); return; } switch (apdu->ins) { case U2F_REGISTER: - _cmd_register(in_packet, &out_packet); - usb_processing_send_packet(usb_processing_u2f(), &out_packet); + _cmd_register(in_packet, out_packet); break; case U2F_AUTHENTICATE: - _cmd_authenticate(in_packet, &out_packet); - usb_processing_send_packet(usb_processing_u2f(), &out_packet); + _cmd_authenticate(in_packet, out_packet); break; case U2F_VERSION: - _version(apdu, &out_packet); - usb_processing_send_packet(usb_processing_u2f(), &out_packet); + _version(apdu, out_packet); break; default: - _error(U2F_SW_INS_NOT_SUPPORTED, &out_packet); - usb_processing_send_packet(usb_processing_u2f(), &out_packet); + _error(U2F_SW_INS_NOT_SUPPORTED, out_packet); } + usb_processing_send_packet(usb_processing_u2f(), out_packet); + free(out_packet); } bool u2f_blocking_request_can_go_through(const Packet* in_packet) diff --git a/src/usb/usb_processing.c b/src/usb/usb_processing.c index adc760641..a8e1d6304 100644 --- a/src/usb/usb_processing.c +++ b/src/usb/usb_processing.c @@ -282,10 +282,11 @@ static void _usb_arbitrate_packet(struct usb_processing* ctx, const Packet* in_p if (!can_go_through) { /* The receiving state should send back an error */ - Packet out_packet; - prepare_usb_packet(in_packet->cmd, in_packet->cid, &out_packet); - ctx->create_blocked_req_error(&out_packet, in_packet); - usb_processing_send_packet(ctx, &out_packet); + Packet* out_packet = util_malloc(sizeof(*out_packet)); + prepare_usb_packet(in_packet->cmd, in_packet->cid, out_packet); + ctx->create_blocked_req_error(out_packet, in_packet); + usb_processing_send_packet(ctx, out_packet); + free(out_packet); } else { _usb_execute_packet(ctx, in_packet); /* New packet processed: reset the watchdog timeout. */ diff --git a/src/util.c b/src/util.c index 7d54832ae..1c8362520 100644 --- a/src/util.c +++ b/src/util.c @@ -57,6 +57,15 @@ void util_cleanup_64(uint8_t** buf) util_zero(*buf, 64); } +void* util_malloc(size_t size) +{ + void* result = malloc(size); + if ((!result) && (size > 0)) { + Abort("util_malloc failed."); + } + return result; +} + char* util_strdup(const char* str) { char* result = strdup(str); diff --git a/src/util.h b/src/util.h index 7d70dd5a3..50ea38ac8 100644 --- a/src/util.h +++ b/src/util.h @@ -162,4 +162,14 @@ typedef struct { */ typedef enum { ASYNC_OP_TRUE, ASYNC_OP_FALSE, ASYNC_OP_NOT_READY } async_op_result_t; +/** + * Allocate memory. Abort + * if malloc fails. + * + * @param[in] size Number of bytes to allocate. Gets passed to malloc(2). + * @return Allocated pointer. Guaranteed to be non-NULL. + */ +__attribute__((malloc)) +void* util_malloc(size_t size); + #endif From ecfa287cc62c8d3b7d65ff3741f05624365fe87b Mon Sep 17 00:00:00 2001 From: Simone Baratta Date: Thu, 9 Jan 2020 10:50:46 +0100 Subject: [PATCH 09/15] Add FIDO2 support to the BB02-multi firmware. --- .gitmodules | 3 + external/CMakeLists.txt | 9 + external/tinycbor | 1 + src/CMakeLists.txt | 8 + src/common_main.c | 2 + src/fido2/cose_key.h | 22 + src/fido2/ctap.c | 1446 +++++++++++++++++++++++++ src/fido2/ctap.h | 353 ++++++ src/fido2/ctap_errors.h | 58 + src/fido2/ctap_parse.c | 1130 +++++++++++++++++++ src/fido2/ctap_parse.h | 25 + src/fido2/ctaphid.h | 111 ++ src/fido2/device.c | 35 + src/fido2/device.h | 224 ++++ src/fido2/extensions.c | 175 +++ src/fido2/extensions.h | 33 + src/fido2/fido2_keys.h | 11 + src/fido2/storage.h | 70 ++ src/fido2/wallet.h | 97 ++ src/keystore.c | 2 +- src/memory/memory.c | 25 + src/memory/memory.h | 18 + src/u2f.c | 65 +- src/usb/class/usb_desc.h | 9 + src/usb/u2f/u2f.h | 2 +- src/usb/u2f/u2f_hid.h | 20 +- src/usb/u2f/u2f_keys.c | 30 + src/usb/u2f/u2f_keys.h | 30 +- src/workflow/confirm.c | 6 + src/workflow/select_ctap_credential.c | 8 + src/workflow/select_ctap_credential.h | 24 + test/device-test/CMakeLists.txt | 3 + test/unit-test/CMakeLists.txt | 3 +- 33 files changed, 4017 insertions(+), 41 deletions(-) create mode 160000 external/tinycbor create mode 100644 src/fido2/cose_key.h create mode 100644 src/fido2/ctap.c create mode 100644 src/fido2/ctap.h create mode 100644 src/fido2/ctap_errors.h create mode 100644 src/fido2/ctap_parse.c create mode 100644 src/fido2/ctap_parse.h create mode 100644 src/fido2/ctaphid.h create mode 100644 src/fido2/device.c create mode 100644 src/fido2/device.h create mode 100644 src/fido2/extensions.c create mode 100644 src/fido2/extensions.h create mode 100644 src/fido2/fido2_keys.h create mode 100644 src/fido2/storage.h create mode 100644 src/fido2/wallet.h create mode 100644 src/usb/u2f/u2f_keys.c create mode 100644 src/workflow/select_ctap_credential.c create mode 100644 src/workflow/select_ctap_credential.h diff --git a/.gitmodules b/.gitmodules index 67a075a8f..2f63de26f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,3 +19,6 @@ [submodule "tools/ttf2ugui"] path = tools/ttf2ugui url = https://github.com/digitalbitbox/ttf2ugui +[submodule "external/tinycbor"] + path = external/tinycbor + url = https://github.com/intel/tinycbor diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 227cccdbb..517489e7f 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -143,6 +143,15 @@ set_property(TARGET secp256k1 set_target_properties(secp256k1 PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}/libwally-core/src/secp256k1/include) set_target_properties(secp256k1 PROPERTIES INTERFACE_SYSTEM_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}/libwally-core/src/secp256k1/include) +add_library(tinycbor STATIC + tinycbor/src/cborerrorstrings.c + tinycbor/src/cborencoder.c + tinycbor/src/cborencoder_close_container_checked.c + tinycbor/src/cborparser.c + tinycbor/src/cborpretty.c +) +set_target_properties(tinycbor PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}/tinycbor/src) +set_target_properties(tinycbor PROPERTIES INTERFACE_SYSTEM_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}/tinycbor/src) if(CMAKE_CROSSCOMPILING) # Cortex Microcontroller Software Interface Standard diff --git a/external/tinycbor b/external/tinycbor new file mode 160000 index 000000000..2b1105eb8 --- /dev/null +++ b/external/tinycbor @@ -0,0 +1 @@ +Subproject commit 2b1105eb8b19dca432d94961ea36d1725aa0dbae diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a1d9ecfef..79600641c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -54,6 +54,7 @@ set(DBB-FIRMWARE-SOURCES ${CMAKE_SOURCE_DIR}/src/workflow/restore.c ${CMAKE_SOURCE_DIR}/src/workflow/restore_from_mnemonic.c ${CMAKE_SOURCE_DIR}/src/workflow/sdcard.c + ${CMAKE_SOURCE_DIR}/src/workflow/select_ctap_credential.c ${CMAKE_SOURCE_DIR}/src/workflow/show_mnemonic.c ${CMAKE_SOURCE_DIR}/src/workflow/orientation_screen.c ${CMAKE_SOURCE_DIR}/src/workflow/status.c @@ -81,6 +82,7 @@ set(DBB-FIRMWARE-USB-SOURCES ${CMAKE_SOURCE_DIR}/src/usb/usb.c ${CMAKE_SOURCE_DIR}/src/usb/usb_frame.c ${CMAKE_SOURCE_DIR}/src/usb/usb_packet.c + ${CMAKE_SOURCE_DIR}/src/usb/u2f/u2f_keys.c ${CMAKE_SOURCE_DIR}/src/u2f/u2f_packet.c ) set(DBB-FIRMWARE-USB-SOURCES ${DBB-FIRMWARE-USB-SOURCES} PARENT_SCOPE) @@ -211,6 +213,11 @@ set(FIRMWARE-DRIVER-SOURCES set(FIRMWARE-DRIVER-SOURCES ${FIRMWARE-DRIVER-SOURCES} PARENT_SCOPE) set(FIRMWARE-U2F-SOURCES + ${CMAKE_SOURCE_DIR}/src/fido2/ctap.c + ${CMAKE_SOURCE_DIR}/src/fido2/ctap_parse.c + ${CMAKE_SOURCE_DIR}/src/fido2/device.c + ${CMAKE_SOURCE_DIR}/src/fido2/fido2_keys.c + ${CMAKE_SOURCE_DIR}/src/fido2/extensions.c ${CMAKE_SOURCE_DIR}/src/u2f.c ${CMAKE_SOURCE_DIR}/src/u2f/u2f_app.c ${CMAKE_SOURCE_DIR}/src/u2f/u2f_keyhandle.c @@ -522,6 +529,7 @@ if(CMAKE_CROSSCOMPILING) PRIVATE cryptoauthlib fatfs + tinycbor base32 bignum # TODO: only eth sha3 # TODO: Only eth diff --git a/src/common_main.c b/src/common_main.c index 9ad5c2904..0ff45313b 100644 --- a/src/common_main.c +++ b/src/common_main.c @@ -13,6 +13,7 @@ // limitations under the License. #include "common_main.h" +#include "fido2/ctap.h" #include "driver_init.h" #include "flags.h" #include "hardfault.h" @@ -84,4 +85,5 @@ void common_main(void) if (!securechip_setup(&_securechip_interface_functions)) { Abort("securechip_setup failed"); } + //ctap_init(); } diff --git a/src/fido2/cose_key.h b/src/fido2/cose_key.h new file mode 100644 index 000000000..6056f02e0 --- /dev/null +++ b/src/fido2/cose_key.h @@ -0,0 +1,22 @@ +// Copyright 2019 SoloKeys Developers +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. +#ifndef _COSE_KEY_H +#define _COSE_KEY_H + +#define COSE_KEY_LABEL_KTY 1 +#define COSE_KEY_LABEL_ALG 3 +#define COSE_KEY_LABEL_CRV -1 +#define COSE_KEY_LABEL_X -2 +#define COSE_KEY_LABEL_Y -3 + +#define COSE_KEY_KTY_EC2 2 +#define COSE_KEY_CRV_P256 1 + +#define COSE_ALG_ES256 -7 +#define COSE_ALG_ECDH_ES_HKDF_256 -25 + +#endif diff --git a/src/fido2/ctap.c b/src/fido2/ctap.c new file mode 100644 index 000000000..6a7158da2 --- /dev/null +++ b/src/fido2/ctap.c @@ -0,0 +1,1446 @@ +// Copyright 2019 SoloKeys Developers +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. +#include +#include +#include +#include + +#include "ctap.h" + +#include "cbor.h" +#include "cose_key.h" +#include "ctaphid.h" +#include "ctap_parse.h" +#include "device.h" +#include "extensions.h" +#include "fido2_keys.h" +#include "storage.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * CTAP request codes. + */ +#define CTAP_REQ_MAKE_CREDENTIAL (0x01) +#define CTAP_REQ_GET_ASSERTION (0x02) +#define CTAP_REQ_CANCEL (0x03) +#define CTAP_REQ_GET_INFO (0x04) +#define CTAP_REQ_CLIENT_PIN (0x06) +#define CTAP_REQ_RESET (0x07) +#define CTAP_REQ_GET_NEXT_ASSERTION (0x08) +#define CTAP_REQ_VENDOR_FIRST (0x40) +#define CTAP_REQ_VENDOR_LAST (0xBF) + +/** + * Auth data flags, defined in [WebAuthn] sec. 6.1. Authenticator Data. + */ +/** + * User is present/not present. + */ +#define CTAP_AUTH_DATA_FLAG_USER_PRESENT (1 << 0) +/** + * User is verified/not verified. + */ +#define CTAP_AUTH_DATA_FLAG_USER_VERIFIED (1 << 2) +/** + * Indicates whether the authenticator added attested credential data. + */ +#define CTAP_AUTH_DATA_FLAG_ATTESTED_CRED_DATA_INCLUDED (1 << 6) +/** + * Indicates if the authenticator data has extensions. + */ +#define CTAP_AUTH_DATA_FLAG_EXTENSION_DATA_INCLUDED (1 << 7) + +/** + * Response tags. + */ +#define CTAP_RESP_VERSIONS (0x1) +#define CTAP_RESP_EXTENSIONS (0x2) +#define CTAP_RESP_AAGUID (0x3) +#define CTAP_RESP_OPTIONS (0x4) +#define CTAP_RESP_MAX_MSG_SIZE (0x5) +#define CTAP_RESP_PIN_PROTOCOLS (0x6) + +#define CTAP_RESP_FMT (0x01) +#define CTAP_RESP_AUTH_DATA (0x02) +#define CTAP_RESP_ATT_STMT (0x03) + +#define CTAP_RESP_CREDENTIAL (0x01) +#define CTAP_RESP_SIGNATURE (0x03) +#define CTAP_RESP_PUBKEY_CREDENTIAL_USER_ENTITY (0x04) +#define CTAP_RESP_NUM_CREDENTIALS (0x05) + +#define CTAP_RESP_KEY_AGREEMENT (0x01) +#define CTAP_RESP_PIN_TOKEN (0x02) +#define CTAP_RESP_RETRIES (0x03) + + +typedef struct { + enum { + CTAP_MAKE_CREDENTIAL_STARTED, + CTAP_MAKE_CREDENTIAL_UNLOCKED, + CTAP_MAKE_CREDENTIAL_WAIT_CONFIRM, + CTAP_MAKE_CREDENTIAL_FINISHED, + CTAP_MAKE_CREDENTIAL_FAILED, + } state; + ctap_make_credential_req_t req; +} ctap_make_credential_state_t; + +typedef struct { + enum { + CTAP_GET_ASSERTION_STARTED, + CTAP_GET_ASSERTION_UNLOCKED, + CTAP_GET_ASSERTION_WAIT_CONFIRM, + CTAP_GET_ASSERTION_FAILED, + CTAP_GET_ASSERTION_FINISHED, + } state; + ctap_get_assertion_req_t req; +} ctap_get_assertion_state_t; + +static struct { + enum { + CTAP_BLOCKING_OP_NONE, + CTAP_BLOCKING_OP_MAKE_CRED, + CTAP_BLOCKING_OP_GET_ASSERTION + } blocking_op; + union { + ctap_make_credential_state_t make_cred; + ctap_get_assertion_state_t get_assertion; + } data; +} _state = {0}; + +static uint8_t ctap_get_info(CborEncoder * encoder) +{ + (void)_state; + int ret; + CborEncoder array; + CborEncoder map; + CborEncoder options; + CborEncoder pins; + uint8_t aaguid[16]; + device_read_aaguid(aaguid); + + ret = cbor_encoder_create_map(encoder, &map, 6); + check_ret(ret); + { + ret = cbor_encode_uint(&map, CTAP_RESP_VERSIONS); // versions key + check_ret(ret); + { + ret = cbor_encoder_create_array(&map, &array, 2); + check_ret(ret); + { + ret = cbor_encode_text_stringz(&array, "U2F_V2"); + check_ret(ret); + ret = cbor_encode_text_stringz(&array, "FIDO_2_0"); + check_ret(ret); + } + ret = cbor_encoder_close_container(&map, &array); + check_ret(ret); + } + + ret = cbor_encode_uint(&map, CTAP_RESP_EXTENSIONS); + check_ret(ret); + { + ret = cbor_encoder_create_array(&map, &array, 1); + check_ret(ret); + { + ret = cbor_encode_text_stringz(&array, "hmac-secret"); + check_ret(ret); + } + ret = cbor_encoder_close_container(&map, &array); + check_ret(ret); + } + + ret = cbor_encode_uint(&map, CTAP_RESP_AAGUID); + check_ret(ret); + { + ret = cbor_encode_byte_string(&map, aaguid, 16); + check_ret(ret); + } + + ret = cbor_encode_uint(&map, CTAP_RESP_OPTIONS); + check_ret(ret); + { + ret = cbor_encoder_create_map(&map, &options, 4); + check_ret(ret); + { + ret = cbor_encode_text_string(&options, "rk", 2); + check_ret(ret); + { + ret = cbor_encode_boolean(&options, 1); // Capable of storing keys locally + check_ret(ret); + } + + ret = cbor_encode_text_string(&options, "up", 2); + check_ret(ret); + { + ret = cbor_encode_boolean(&options, 1); // Capable of testing user presence + check_ret(ret); + } + + ret = cbor_encode_text_string(&options, "uv", 2); // Capable of verifying user + check_ret(ret); + { + /* + * The option should be true/false based on whether the UV function has already + * been initialized. + */ + ret = cbor_encode_boolean(&options, true); + check_ret(ret); + } + + ret = cbor_encode_text_string(&options, "plat", 4); + check_ret(ret); + { + ret = cbor_encode_boolean(&options, 0); // Not attached to platform + check_ret(ret); + } + /* + * We're not capable of PIN authentication, so the clientPin option + * should be unset. + */ + } + ret = cbor_encoder_close_container(&map, &options); + check_ret(ret); + } + + ret = cbor_encode_uint(&map, CTAP_RESP_MAX_MSG_SIZE); + check_ret(ret); + { + ret = cbor_encode_int(&map, CTAP_MAX_MESSAGE_SIZE); + check_ret(ret); + } + + ret = cbor_encode_uint(&map, CTAP_RESP_PIN_PROTOCOLS); + check_ret(ret); + { + ret = cbor_encoder_create_array(&map, &pins, 1); + check_ret(ret); + { + ret = cbor_encode_int(&pins, 1); + check_ret(ret); + } + ret = cbor_encoder_close_container(&map, &pins); + check_ret(ret); + } + } + ret = cbor_encoder_close_container(encoder, &map); + check_ret(ret); + + return CTAP1_ERR_SUCCESS; +} + + + +static int ctap_add_cose_key(CborEncoder* cose_key, uint8_t* x, uint8_t* y, int32_t algtype) +{ + int ret; + CborEncoder map; + + ret = cbor_encoder_create_map(cose_key, &map, 5); + check_ret(ret); + + + { + ret = cbor_encode_int(&map, COSE_KEY_LABEL_KTY); + check_ret(ret); + ret = cbor_encode_int(&map, COSE_KEY_KTY_EC2); + check_ret(ret); + } + + { + ret = cbor_encode_int(&map, COSE_KEY_LABEL_ALG); + check_ret(ret); + ret = cbor_encode_int(&map, algtype); + check_ret(ret); + } + + { + ret = cbor_encode_int(&map, COSE_KEY_LABEL_CRV); + check_ret(ret); + ret = cbor_encode_int(&map, COSE_KEY_CRV_P256); + check_ret(ret); + } + + + { + ret = cbor_encode_int(&map, COSE_KEY_LABEL_X); + check_ret(ret); + ret = cbor_encode_byte_string(&map, x, 32); + check_ret(ret); + } + + { + ret = cbor_encode_int(&map, COSE_KEY_LABEL_Y); + check_ret(ret); + ret = cbor_encode_byte_string(&map, y, 32); + check_ret(ret); + } + + ret = cbor_encoder_close_container(cose_key, &map); + check_ret(ret); + + return 0; +} + +/** + * Encode the 32bit U2F counter value as a big-endian + * sequence of bytes. + * @param counter Counter to encode. + * @param buf_out Buffer in which to encode the counter. Must be 4 bytes wide. + */ +static void _encode_u2f_counter(uint32_t counter, uint8_t* buf_out) +{ + *buf_out++ = (counter >> 24) & 0xff; + *buf_out++ = (counter >> 16) & 0xff; + *buf_out++ = (counter >> 8) & 0xff; + *buf_out++ = (counter >> 0) & 0xff; +} + +/** + * Copy the source string into the destination buffer. + * If the string is too long to fit the destination buffer, + * truncate the string with "...", so that the resulting + * string is always a valid NULL-terminated string. + */ +static void _copy_or_truncate(char* dst, size_t dst_size, const char* src) +{ + size_t src_size = strlen(src); + bool truncate = false; + + const char* padding = "..."; + size_t padding_size = strlen(padding); + + if (dst_size < src_size + 1) { + /* + * String is too long. + * Truncate the source string to the biggest possible size. + */ + truncate = true; + src_size = dst_size - 1 - padding_size; + } + strncpy(dst, src, src_size); + if (!truncate) { + dst[src_size] = '\0'; + } else { + strcpy(dst, padding); + dst[src_size + padding_size] = '\0'; + } +} + +static int _is_matching_rk(ctap_resident_key_t* rk, ctap_resident_key_t* rk2) +{ + return (memcmp(rk->rp_id_hash, rk2->rp_id_hash, 32) == 0) && + (memcmp(rk->rp_id, rk2->rp_id, CTAP_STORAGE_RP_ID_MAX_SIZE) == 0) && + (memcmp(rk->user_name, rk2->user_name, CTAP_STORAGE_USER_NAME_LIMIT) == 0); +} + +static void _get_assertion_unlock_cb(bool result, void* param) { + (void)param; + if (!result) { + /* + * User didn't authenticate. + * Let's count this as a "user denied" error. + */ + _state.data.get_assertion.state = CTAP_GET_ASSERTION_FAILED; + return; + } + _state.data.get_assertion.state = CTAP_GET_ASSERTION_UNLOCKED; +} + +static void _get_assertion_allow_cb(bool result, void* param) +{ + (void)param; + ctap_get_assertion_state_t* state = &_state.data.get_assertion; + if (result) { + state->state = CTAP_GET_ASSERTION_FINISHED; + } else { + state->state = CTAP_GET_ASSERTION_FAILED; + } +} + +static workflow_t* _get_assertion_confirm(ctap_rp_id_t* rp) +{ + char prompt_buf[100]; + size_t prompt_size; + if (rp->name && rp->name[0] != '\0') { + /* There is a human-readable name attached to this domain. */ + prompt_size = snprintf(prompt_buf, 100, "Authenticate on\n%s\n(%.*s)\n", + rp->name, (int)rp->size, rp->id); + } else { + prompt_size = snprintf(prompt_buf, 100, "Authenticate on\n%.*s\n", + (int)rp->size, rp->id); + } + if (prompt_size >= 100) { + prompt_buf[99] = '\0'; + } + + const confirm_params_t params = { + .title = "FIDO2", + .body = prompt_buf, + .scrollable = false, + }; + return workflow_confirm(¶ms, _get_assertion_allow_cb, NULL); +} + +/** + * @param sig[in] Location to deposit signature (must be 64 bytes) + * @param out_encoded_sig[in] Location to deposit der signature (must be 72 bytes) + * @return Length of DER encoded signature. + * // FIXME add tests for maximum and minimum length of the input and output + */ +static int _encode_der_sig(const uint8_t* sig, uint8_t* out_encoded_sig) +{ + // Need to caress into dumb der format .. + uint8_t lead_s = 0; // leading zeros + uint8_t lead_r = 0; + for (int i=0; i < 32; i++) { + if (sig[i] == 0) { + lead_r++; + } + else { + break; + } + } + + for (int i=0; i < 32; i++) { + if (sig[i+32] == 0) { + lead_s++; + } + else { + break; + } + } + + int8_t pad_s = ((sig[32 + lead_s] & 0x80) == 0x80); + int8_t pad_r = ((sig[0 + lead_r] & 0x80) == 0x80); + + memset(out_encoded_sig, 0, 72); + out_encoded_sig[0] = 0x30; + out_encoded_sig[1] = 0x44 + pad_s + pad_r - lead_s - lead_r; + + // R ingredient + out_encoded_sig[2] = 0x02; + out_encoded_sig[3 + pad_r] = 0; + out_encoded_sig[3] = 0x20 + pad_r - lead_r; + memmove(out_encoded_sig + 4 + pad_r, sig + lead_r, 32u - lead_r); + + // S ingredient + out_encoded_sig[4 + 32 + pad_r - lead_r] = 0x02; + out_encoded_sig[5 + 32 + pad_r + pad_s - lead_r] = 0; + out_encoded_sig[5 + 32 + pad_r - lead_r] = 0x20 + pad_s - lead_s; + memmove(out_encoded_sig + 6 + 32 + pad_r + pad_s - lead_r, sig + 32u + lead_s, 32u - lead_s); + + return 0x46 + pad_s + pad_r - lead_r - lead_s; +} + +/** + * Computes the EC256 + * See [WebAuthn], 8.2 "Signing procedure" + * require load_key prior to this + * @param[in] auth_data Authenticator data for the attestation. + * @param[in] auth_data_len Length of auth_data. + * @param[in] client_data_hash Hash of the serialized client data. + * @param[out] sigbuf_out Buffer in which to store the computed signature + */ +static bool _calculate_signature(const uint8_t* privkey, uint8_t* auth_data, size_t auth_data_len, uint8_t* client_data_hash, uint8_t* sigbuf_out) +{ + uint8_t hash_buf[SHA256_LEN]; + + sha256_context_t ctx; + sha256_reset(&ctx); + noise_sha256_update(&ctx, auth_data, auth_data_len); + noise_sha256_update(&ctx, client_data_hash, CLIENT_DATA_HASH_SIZE); + sha256_finish(&ctx, hash_buf); + if (!securechip_ecc_unsafe_sign(privkey, hash_buf, sigbuf_out)) { + return false; + } + return true; +} + +/** + * Adds the encoding of an attestation statement into a CBOR encoder. + * + * @param map[in] Encoder in which to append the attestation statement. + * @param signature[in] Signature to add to the statement. + * @param len[in] Length of signature. + * @return Error code (or 0 for success). + */ +static uint8_t _add_attest_statement(CborEncoder* map, const uint8_t* signature, int len) +{ + int ret; + /* TODO: simo: generate another cert? */ + const uint8_t *cert = FIDO2_ATT_CERT; + uint16_t cert_size = FIDO2_ATT_CERT_SIZE; + + CborEncoder stmtmap; + CborEncoder x5carr; + + ret = cbor_encode_int(map, CTAP_RESP_ATT_STMT); + check_ret(ret); + ret = cbor_encoder_create_map(map, &stmtmap, 3); + check_ret(ret); + { + ret = cbor_encode_text_stringz(&stmtmap,"alg"); + check_ret(ret); + ret = cbor_encode_int(&stmtmap, COSE_ALG_ES256); + check_ret(ret); + } + { + ret = cbor_encode_text_stringz(&stmtmap,"sig"); + check_ret(ret); + ret = cbor_encode_byte_string(&stmtmap, signature, len); + check_ret(ret); + } + { + ret = cbor_encode_text_stringz(&stmtmap,"x5c"); + check_ret(ret); + ret = cbor_encoder_create_array(&stmtmap, &x5carr, 1); + check_ret(ret); + { + ret = cbor_encode_byte_string(&x5carr, cert, cert_size); + check_ret(ret); + ret = cbor_encoder_close_container(&stmtmap, &x5carr); + check_ret(ret); + } + } + + ret = cbor_encoder_close_container(map, &stmtmap); + check_ret(ret); + return 0; +} + +/** + * Computes the sha256 hash of the given RP id. + * @param rp_hash_out Buffer in which to store the computed hash. + * Must be SHA256_LEN bytes wide. + */ +static void _compute_rpid_hash(ctap_rp_id_t* rp, uint8_t* rp_hash_out) { + if (wally_sha256(rp->id, rp->size, rp_hash_out, SHA256_LEN) != WALLY_OK) { + Abort("wally_sha256 failed"); + } +} + + +/** + * Asks the user for confirmation when + * a stored FIDO2 credential is about + * to be overwritten with a new one for + * the same user. + */ +static bool _confirm_overwrite_credential(void) { + /* TODO */ + return true; +} + +/** + * Check if any of the keys in a MakeCredential's + * exclude_list belong to our device. + * + * @param req MakeCredential request to analyze. + * @return Verification status: + * - 0 if no invalid key was found; + * - CTAP2_ERR_CREDENTIAL_EXCLUDED if an excluded key belongs to us; + * - other errors if we failed to parse the exclude list. + */ +static uint8_t _verify_exclude_list(ctap_make_credential_req_t* req) +{ + for (size_t i = 0; i < req->exclude_list_size; i++) { + u2f_keyhandle_t excl_cred; + bool cred_valid; + uint8_t ret = ctap_parse_credential_descriptor(&req->exclude_list, &excl_cred, &cred_valid); + if (!cred_valid || ret == CTAP2_ERR_CBOR_UNEXPECTED_TYPE) { + /* Skip credentials that fail to parse. */ + continue; + } + if (ret != CborNoError) { + return ret; + } + + uint8_t privkey[HMAC_SHA256_LEN]; + UTIL_CLEANUP_32(privkey); + bool key_is_ours = u2f_keyhandle_verify(req->rp.id, (uint8_t*)&excl_cred, sizeof(excl_cred), privkey); + if (key_is_ours) + { + return true; + } + + ret = cbor_value_advance(&req->exclude_list); + check_ret(ret); + } + return false; +} + +static bool _ask_generic_authorization(void) { + const confirm_params_t params = { + .title = "FIDO2", + .body = "Proceed?", + }; + return workflow_confirm_blocking(¶ms); +} + +/** + * Called after the user has confirmed (or declined) the + * creation of a new credential. + */ +static void _make_credential_allow_cb(bool result, void* param) { + (void)param; + if (result) { + _state.data.make_cred.state = CTAP_MAKE_CREDENTIAL_FINISHED; + } else { + _state.data.make_cred.state = CTAP_MAKE_CREDENTIAL_FAILED; + } +} + +/** + * Asks the user whether he wants to proceed + * with the creation of a new credential. + * @param req MakeCredential CTAP request. + * @return Confirmation workflow. + */ +static workflow_t* _make_credential_allow(ctap_make_credential_req_t* req) +{ + char prompt_buf[100]; + size_t prompt_size; + if (req->rp.name && req->rp.name[0] != '\0') { + /* There is a human-readable name attached to this domain. */ + prompt_size = snprintf(prompt_buf, 100, "Create credential for\n%s\n(%.*s)\n", + req->rp.name, (int)req->rp.size, req->rp.id); + } else { + prompt_size = snprintf(prompt_buf, 100, "Create credential for\n%.*s\n", + (int)req->rp.size, req->rp.id); + } + if (prompt_size >= 100) { + prompt_buf[99] = '\0'; + } + const confirm_params_t params = { + .title = "FIDO2", + .body = prompt_buf, + }; + return workflow_confirm(¶ms, _make_credential_allow_cb, NULL); +} + +static void _make_credential_unlock_cb(bool result, void* param) { + (void)param; + //screen_sprintf_debug(1000, "UNLOCK CB %d", result); + if (!result) { + /* + * User didn't authenticate. + * Let's count this as a "user denied" error. + */ + _state.data.make_cred.state = CTAP_MAKE_CREDENTIAL_FAILED; + return; + } + _state.data.make_cred.state = CTAP_MAKE_CREDENTIAL_UNLOCKED; +} + +static void _make_credential_init_state(ctap_make_credential_req_t* req) +{ + _state.data.make_cred.state = CTAP_MAKE_CREDENTIAL_STARTED; + memcpy(&_state.data.make_cred.req, req, sizeof(*req)); +} + +static void _make_credential_free_state(void) +{ +} + +static uint8_t ctap_make_credential(CborEncoder * encoder, const in_buffer_t* in_buffer) { + ctap_make_credential_req_t MC; + int ret; + + ret = ctap_parse_make_credential(&MC,encoder, in_buffer); + + if (ret != 0) { + return ret; + } + if (MC.pin_auth_empty) { + /* + * pin_auth was present and was an empty string. + * The client is asking us if we support pin + * (and asks to check for user presence before we move on). + */ + bool result = _ask_generic_authorization(); + if (!result) { + return CTAP2_ERR_OPERATION_DENIED; + } + /* We don't support PIN semantics. */ + return CTAP2_ERR_PIN_NOT_SET; + } + + if (MC.pin_auth_present) { + /* We don't support pin_auth. */ + return CTAP2_ERR_PIN_AUTH_INVALID; + } + + if (MC.up == 1 || MC.up == 0) { + /* + * The UP flag can't be set for authenticatorMakeCredential. + * It must always be unset (0xFF). + */ + return CTAP2_ERR_INVALID_OPTION; + } + + _make_credential_init_state(&MC); + workflow_stack_start_workflow(workflow_unlock(_make_credential_unlock_cb, NULL)); + return CTAP1_ERR_SUCCESS; +} + +/** + * Generates a new credential in response to a MakeCredential request. + * Only called when the user has already accepted and identified with the device. + * + * @return CTAP status code. + */ + +static int _make_credential_complete(buffer_t* out_buf) +{ + ctap_make_credential_state_t* state = &_state.data.make_cred; + /* + * The exclude list contains a list of credentials that we + * must check. If any credential was generated by our device, + * we must return with an error. This allows the server to avoid + * us creating more than one credential for the same user/device pair. + */ + int ret = _verify_exclude_list(&state->req); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + /* Update the U2F counter. */ + uint32_t u2f_counter; + if (!securechip_u2f_counter_inc(&u2f_counter)) { + workflow_status_blocking("Failed to create key.", false); + return CTAP2_ERR_OPERATION_DENIED; + } + + ctap_auth_data_t auth_data; + _compute_rpid_hash(&state->req.rp, auth_data.head.rp_id_hash); + + /* Generate the key. */ + memset((uint8_t*)&auth_data.attest.id, 0, sizeof(u2f_keyhandle_t)); + uint8_t* nonce = auth_data.attest.id.nonce; + uint8_t* mac = auth_data.attest.id.mac; + uint8_t pubkey[64]; + uint8_t privkey[HMAC_SHA256_LEN]; + UTIL_CLEANUP_32(privkey); + bool key_create_success = u2f_keyhandle_create_key(state->req.rp.id, nonce, privkey, mac, pubkey); + if (!key_create_success) { + /* TODO: simo: do something. */ + Abort("Failed to create new FIDO2 key."); + } + + /* + * Find where to store this key. + * If it's new, store it in the first + * available location. Otherwise, overwrite + * the existing key (after confirming with the user). + */ + if (state->req.cred_info.rk) { + ctap_resident_key_t rk_to_store; + memset(&rk_to_store, 0, sizeof(rk_to_store)); + memcpy(&rk_to_store.key_handle, &auth_data.attest.id, sizeof(rk_to_store.key_handle)); + memcpy(&rk_to_store.rp_id_hash, auth_data.head.rp_id_hash, sizeof(auth_data.head.rp_id_hash)); + _copy_or_truncate((char*)rk_to_store.rp_id, sizeof(rk_to_store.rp_id), (const char*)state->req.rp.id); + _copy_or_truncate((char*)rk_to_store.user_name, sizeof(rk_to_store.user_name), (const char*)state->req.cred_info.user.name); + _copy_or_truncate((char*)rk_to_store.display_name, sizeof(rk_to_store.display_name), (const char*)state->req.cred_info.user.display_name); + rk_to_store.valid = CTAP_RESIDENT_KEY_VALID; + rk_to_store.creation_time = u2f_counter; + if (state->req.cred_info.user.id_size > CTAP_USER_ID_MAX_SIZE) { + /* We can't store such a big user ID. + * But we can't even truncate it... So nothing we can do, alas. + */ + return CTAP2_ERR_REQUEST_TOO_LARGE; + } + //screen_sprintf_debug(2000, "UID (%u): %02x%02x", + //state->req.cred_info.user.id_size, + //state->req.cred_info.user.id[0], state->req.cred_info.user.id[state->req.cred_info.user.id_size - 1] + //); + rk_to_store.user_id_size = state->req.cred_info.user.id_size; + memcpy(rk_to_store.user_id, state->req.cred_info.user.id, state->req.cred_info.user.id_size); + + int store_location = 0; + bool must_overwrite = false; + bool free_spot_found = false; + + for (int i = 0; i < MEMORY_CTAP_RESIDENT_KEYS_MAX; i++) { + /* Check if we want to overwrite */ + ctap_resident_key_t this_key; + bool mem_result = memory_get_ctap_resident_key(i, &this_key); + if (!mem_result) { + /* Skip on error */ + continue; + } + if (this_key.valid != CTAP_RESIDENT_KEY_VALID) { + /* Skip invalid keys, mark spot as free */ + if (!free_spot_found) { + store_location = i; + free_spot_found = true; + } + continue; + } + if (_is_matching_rk(&rk_to_store, &this_key)) { + /* Found a matching key. Need to overwrite. */ + free_spot_found = true; + must_overwrite = true; + store_location = i; + break; + } + } + if (!free_spot_found) { + workflow_status_blocking("Out of memory for resident keys", false); + return CTAP2_ERR_KEY_STORE_FULL; + } + if (must_overwrite) { + if (!_confirm_overwrite_credential()) { + workflow_status_blocking("Operation cancelled", false); + return CTAP2_ERR_OPERATION_DENIED; + } + } + memory_store_ctap_resident_key(store_location, &rk_to_store); + //screen_sprintf_debug(500, "Stored key #%d\n", store_location); + //uint8_t* cred_raw = (uint8_t*)&rk_to_store.key_handle; + //screen_sprintf_debug(3000, "KH: %02x..%02x", + //cred_raw[0], cred_raw[15]); + } else { + //screen_print_debug("Not stored key\n", 500); + } + + /* + * Now create the response. + * This is an attestation object, as defined + * in [WebAuthn], 6.4 (Figure 5). + */ + CborEncoder encoder; + CborEncoder attest_obj; + memset(&encoder,0,sizeof(CborEncoder)); + cbor_encoder_init(&encoder, out_buf->data, out_buf->max_len, 0); + ret = cbor_encoder_create_map(&encoder, &attest_obj, 3); + check_ret(ret); + + /* + * First comes the Authenticator Data. + * (Note: the rpId has already been stored at the start of auth_data...) + */ + auth_data.head.flags = CTAP_AUTH_DATA_FLAG_ATTESTED_CRED_DATA_INCLUDED | + CTAP_AUTH_DATA_FLAG_USER_VERIFIED | CTAP_AUTH_DATA_FLAG_USER_PRESENT; + + _encode_u2f_counter(u2f_counter, (uint8_t*)&auth_data.head.signCount); + + device_read_aaguid(auth_data.attest.aaguid); + + /* Encode the length of the key handle in big endian. */ + uint16_t key_length = sizeof(u2f_keyhandle_t); + auth_data.attest.cred_len[0] = (key_length & 0xFF00) >> 8; + auth_data.attest.cred_len[1] = (key_length & 0x00FF); + + CborEncoder cose_key; + uint8_t* cose_key_buf = auth_data.other; + cbor_encoder_init(&cose_key, cose_key_buf, sizeof(auth_data.other), 0); + ret = ctap_add_cose_key(&cose_key, pubkey, pubkey + 32, COSE_ALG_ES256); + if (ret != CborNoError) { + return ret; + } + size_t cose_key_len = cbor_encoder_get_buffer_size(&cose_key, cose_key_buf); + size_t actual_auth_data_len = sizeof(auth_data) - sizeof(auth_data.other) + cose_key_len; + + /* FUTURE: manage extensions if we want to. */ + + /* + * 3 fields in an attestation object: + * - fmt + * - authData + * - attStmt + */ + { + ret = cbor_encode_int(&attest_obj, CTAP_RESP_FMT); + check_ret(ret); + ret = cbor_encode_text_stringz(&attest_obj, "packed"); + check_ret(ret); + } + + + { + ret = cbor_encode_int(&attest_obj, CTAP_RESP_AUTH_DATA); + check_ret(ret); + ret = cbor_encode_byte_string(&attest_obj, (uint8_t*)&auth_data, actual_auth_data_len); + check_ret(ret); + } + + /* Compute the attestation statement. */ + uint8_t sigbuf[32]; + bool sig_success = _calculate_signature(FIDO2_ATT_PRIV_KEY, (uint8_t*)&auth_data, actual_auth_data_len, state->req.client_data_hash, sigbuf); + if (!sig_success) { + return CTAP1_ERR_OTHER; + } + uint8_t attest_signature[72]; + int attest_sig_size = _encode_der_sig(sigbuf, attest_signature); + + ret = _add_attest_statement(&attest_obj, attest_signature, attest_sig_size); + if (ret != CborNoError) { + return ret; + } + + ret = cbor_encoder_close_container(&encoder, &attest_obj); + check_ret(ret); + //workflow_status_create("Registration\ncompleted.", true); + out_buf->len = cbor_encoder_get_buffer_size(&encoder, out_buf->data); + return CTAP1_ERR_SUCCESS; +} + +static ctap_request_result_t _make_credential_continue(buffer_t* out_buf) { + ctap_request_result_t result = {.status = 0, .request_completed = true}; + ctap_make_credential_state_t* state = &_state.data.make_cred; + + switch (state->state) { + case CTAP_MAKE_CREDENTIAL_UNLOCKED: + /* + * Request permission to the user. + * This must be done before checking for excluded credentials etc. + * so that we don't reveal the existance of credentials without + * the user's consent. + */ + workflow_stack_start_workflow(_make_credential_allow(&_state.data.make_cred.req)); + state->state = CTAP_MAKE_CREDENTIAL_WAIT_CONFIRM; + result.request_completed = false; + return result; + case CTAP_MAKE_CREDENTIAL_FINISHED: + result.status = _make_credential_complete(out_buf); + return result; + case CTAP_MAKE_CREDENTIAL_FAILED: + workflow_status_blocking("Operation cancelled", false); + result.status = CTAP2_ERR_OPERATION_DENIED; + return result; + case CTAP_MAKE_CREDENTIAL_STARTED: + case CTAP_MAKE_CREDENTIAL_WAIT_CONFIRM: + result.request_completed = false; + return result; + default: + Abort("Invalid make_credential state"); + } +} + +static uint8_t ctap_add_credential_descriptor(CborEncoder* map, u2f_keyhandle_t* key_handle) +{ + CborEncoder desc; + int ret = cbor_encode_int(map, CTAP_RESP_CREDENTIAL); + check_ret(ret); + + ret = cbor_encoder_create_map(map, &desc, 2); + check_ret(ret); + + { + ret = cbor_encode_text_string(&desc, "id", 2); + check_ret(ret); + + ret = cbor_encode_byte_string(&desc, (uint8_t*)key_handle, + sizeof(*key_handle)); + check_ret(ret); + } + + { + ret = cbor_encode_text_string(&desc, "type", 4); + check_ret(ret); + + ret = cbor_encode_text_string(&desc, "public-key", 10); + check_ret(ret); + } + + + ret = cbor_encoder_close_container(map, &desc); + check_ret(ret); + + return 0; +} + +/** + * Comparator function used to qsort() the credentials. + * @return >0 if b is more recent than a, 0 if they have the same age (should never happen!), + * <0 otherwise. + */ +static int _compare_display_credentials(const void * _a, const void * _b) +{ + const ctap_credential_display_t* a = (const ctap_credential_display_t* )_a; + const ctap_credential_display_t* b = (const ctap_credential_display_t* )_b; + return b->creation_time - a->creation_time; +} + +/** + * Adds the "publickKeyCredentialUserEntity" field to a CBOR + * object, containing the specified user id as its only field. + * + * @param user_id must be at least user_id_size wide. + * @param user_id_size Length of user_id. + */ +static uint8_t _encode_user_id(CborEncoder* map, const uint8_t* user_id, size_t user_id_size) +{ + CborEncoder entity; + int ret = cbor_encode_int(map, CTAP_RESP_PUBKEY_CREDENTIAL_USER_ENTITY); + check_ret(ret); + + ret = cbor_encoder_create_map(map, &entity, 1); + check_ret(ret); + + { + ret = cbor_encode_text_string(&entity, "id", 2); + check_ret(ret); + + ret = cbor_encode_byte_string(&entity, user_id, user_id_size); + check_ret(ret); + } + + ret = cbor_encoder_close_container(map, &entity); + check_ret(ret); + + return 0; +} + +/** + * Fills a getAssertion response, as defined in the FIDO2 specs, 5.2. + * + * The response map contains: + * - Credential descriptor + * - Auth data + * - Signature + * - User ID (if present) + * + * Note that we don't include any user data as there is no need for that + * (the user has already been selected on the device). + */ +static uint8_t ctap_end_get_assertion(CborEncoder* encoder, u2f_keyhandle_t* key_handle, uint8_t* auth_data_buf, unsigned int auth_data_buf_sz, uint8_t* privkey, uint8_t* client_data_hash, const uint8_t* user_id, size_t user_id_size) +{ + int ret; + uint8_t signature[64]; + uint8_t encoded_sig[72]; + int encoded_sig_size; + + CborEncoder map; + int map_size = 3; + if (user_id_size) { + map_size++; + } + + ret = cbor_encoder_create_map(encoder, &map, map_size); + check_ret(ret); + + ret = ctap_add_credential_descriptor(&map, key_handle); // 1 + if (ret != CborNoError) { + return ret; + } + + { + ret = cbor_encode_int(&map, CTAP_RESP_AUTH_DATA); // 2 + check_ret(ret); + ret = cbor_encode_byte_string(&map, auth_data_buf, auth_data_buf_sz); + check_ret(ret); + } + + bool sig_success = _calculate_signature(privkey, auth_data_buf, auth_data_buf_sz, client_data_hash, signature); + if (!sig_success) { + return CTAP1_ERR_OTHER; + } + encoded_sig_size = _encode_der_sig(signature, encoded_sig); + + { + ret = cbor_encode_int(&map, CTAP_RESP_SIGNATURE); // 3 + check_ret(ret); + ret = cbor_encode_byte_string(&map, encoded_sig, encoded_sig_size); + check_ret(ret); + } + if (user_id_size) + { + ret = _encode_user_id(&map, user_id, user_id_size); // 4 + if (ret != CborNoError) { + return ret; + } + } + ret = cbor_encoder_close_container(encoder, &map); + return 0; +} + +/** + * Selects one of the matching credentials in the given credential list. + * + * @param GA getAssertion request that must be examined. Must contain + * an allow list. + * @param chosen_credential_out Will be filled with a pointer to the chosen credential, + * or NULL if no key was found. + * @param chosen_privkey Will be filled with the the private key corresponding to chosen_credential. + * Must be at least HMAC_SHA256_LEN bytes wide. + */ +static void _authenticate_with_allow_list(ctap_get_assertion_req_t* GA, u2f_keyhandle_t** chosen_credential_out, uint8_t* chosen_privkey) +{ + /* + * We can just pick the first credential that we're able to authenticate with. + * No need to ask the user to select one if many credentials match. + * See Client to Authenticator Protocol, 5.2, point 9. + */ + for (int i = 0; i < GA->cred_len; i++) { + u2f_keyhandle_t* this_key = GA->creds + i; + bool key_valid = u2f_keyhandle_verify(GA->rp.id, (uint8_t*)this_key, sizeof(*this_key), chosen_privkey); + if (key_valid) { + /* Found an applicable credential. */ + *chosen_credential_out = this_key; + return; + } + } + /* No keys were found. */ + util_zero(chosen_privkey, HMAC_SHA256_LEN); + *chosen_credential_out = NULL; +} + +/** + * Selects one of the stored credentials for authentication. + * + * @param GA getAssertion request that must be examined. Must contain + * an allow list. + * @param chosen_credential_out Will be filled with the chosen credential. + * @param chosen_privkey Will be filled with the the private key corresponding to chosen_credential. + * Must be at least HMAC_SHA256_LEN bytes wide. + * @param user_id_out Will be filled with the stored User ID corresponding to the + * chosen credential. Must be CTAP_STORAGE_USER_NAME_LIMIT bytes long. + * @param user_id_size_out Will be filled with the size of user_id. + */ +static uint8_t _authenticate_with_rk(ctap_get_assertion_req_t* GA, u2f_keyhandle_t* chosen_credential_out, uint8_t* chosen_privkey, uint8_t* user_id_out, size_t* user_id_size_out) +{ + /* + * For each credential that we display, save which RK id it corresponds to. + */ + int cred_idx[CTAP_CREDENTIAL_LIST_MAX_SIZE]; + ctap_credential_display_list_t creds; + creds.n_elems = 0; + + /* + * Compute the hash of the RP id so that we + * can match it against the keys we have in memory. + */ + uint8_t rp_id_hash[SHA256_LEN]; + _compute_rpid_hash(&GA->rp, rp_id_hash); + /* Check all keys that match this RP. */ + for (int i = 0; i < MEMORY_CTAP_RESIDENT_KEYS_MAX; i++) { + ctap_resident_key_t this_key; + bool mem_result = memory_get_ctap_resident_key(i, &this_key); + if (!mem_result || this_key.valid != CTAP_RESIDENT_KEY_VALID) { + continue; + } + if (!memcmp(this_key.rp_id_hash, rp_id_hash, SHA256_LEN)) { + /* + * This key matches the RP! Add its user information to + * our list. + */ + cred_idx[creds.n_elems] = i; + ctap_credential_display_t* this_cred = creds.creds + creds.n_elems; + memcpy(this_cred->username, this_key.user_name, sizeof(this_key.user_name)); + memcpy(this_cred->display_name, this_key.display_name, sizeof(this_key.display_name)); + creds.n_elems++; + if (creds.n_elems == CTAP_CREDENTIAL_LIST_MAX_SIZE) { + /* No more space */ + break; + } + } + } + if (creds.n_elems == 0) { + workflow_status_blocking("No credentials found on this device.", false); + return CTAP2_ERR_NO_CREDENTIALS; + } + /* Sort credentials by creation time. */ + qsort(creds.creds, creds.n_elems, sizeof(*creds.creds), _compare_display_credentials); + int selected_cred = workflow_select_ctap_credential(&creds); + if (selected_cred < 0) { + /* User aborted. */ + workflow_status_blocking("Operation cancelled", false); + return CTAP2_ERR_OPERATION_DENIED; + } + + /* Now load the credential that was selected in the output buffer. */ + ctap_resident_key_t selected_key; + //screen_sprintf_debug(500, "Selected cred #%d", cred_idx[selected_cred]); + bool mem_result = memory_get_ctap_resident_key(cred_idx[selected_cred], &selected_key); + + if (!mem_result) { + /* Shouldn't happen, but if it does we effectively don't have any valid credential to provide. */ + workflow_status_blocking("Internal error. Operation cancelled", false); + return CTAP2_ERR_NO_CREDENTIALS; + } + /* Sanity check the stored credential. */ + if (selected_key.valid != CTAP_RESIDENT_KEY_VALID || + selected_key.user_id_size > CTAP_USER_ID_MAX_SIZE) { + //screen_sprintf_debug(1000, "BAD valid %d", selected_key.valid); + workflow_status_blocking("Internal error. Invalid key selected.", false); + return CTAP2_ERR_NO_CREDENTIALS; + } + memcpy(chosen_credential_out, &selected_key.key_handle, sizeof(selected_key.key_handle)); + *user_id_size_out = selected_key.user_id_size; + memcpy(user_id_out, selected_key.user_id, *user_id_size_out); + + /* Sanity check the key and extract the private key. */ + bool key_valid = u2f_keyhandle_verify(GA->rp.id, (uint8_t*)chosen_credential_out, sizeof(*chosen_credential_out), chosen_privkey); + if (!key_valid) { + workflow_status_blocking("Internal error. Keyhandle verification failed.", false); + return CTAP2_ERR_NO_CREDENTIALS; + } + return CTAP1_ERR_SUCCESS; +} + +/** + * @param auth_data_buf Must be at least sizeof(ctap_auth_data_t) bytes wide. + * @param data_buf_len_out Will be filled with the actual auth data size. + */ +static uint8_t _make_authentication_response(ctap_get_assertion_req_t* GA, uint8_t* auth_data_buf, size_t* data_buf_len_out) { + ctap_auth_data_header_t* auth_data_header = (ctap_auth_data_header_t*)auth_data_buf; + + auth_data_header->flags = 0; + if (GA->up) { + auth_data_header->flags |= CTAP_AUTH_DATA_FLAG_USER_PRESENT; // User presence + } + if (GA->uv) { + auth_data_header->flags |= CTAP_AUTH_DATA_FLAG_USER_VERIFIED; // User presence + } + + _compute_rpid_hash(&GA->rp, auth_data_header->rp_id_hash); + + /* Update the U2F counter. */ + uint32_t u2f_counter; + if (!securechip_u2f_counter_inc(&u2f_counter)) { + return CTAP2_ERR_OPERATION_DENIED; + } + _encode_u2f_counter(u2f_counter, (uint8_t*)&auth_data_header->signCount); + + uint32_t actual_auth_data_size = sizeof(ctap_auth_data_header_t); + + /* FUTURE: manage extensions if we want to. */ + *data_buf_len_out = actual_auth_data_size; + return CTAP1_ERR_SUCCESS; +} + +static void _get_assertion_init_state(ctap_get_assertion_req_t* req) +{ + _state.data.get_assertion.state = CTAP_GET_ASSERTION_STARTED; + memcpy(&_state.data.get_assertion.req, req, sizeof(*req)); +} + +static void _get_assertion_free_state(void) +{ +} + +static uint8_t ctap_get_assertion(const in_buffer_t* in_buffer) +{ + ctap_get_assertion_req_t req; + + int ret = ctap_parse_get_assertion(&req, in_buffer); + + if (ret != 0) { + return ret; + } + + if (req.pin_auth_empty) { + bool result = _ask_generic_authorization(); + if (!result) { + return CTAP2_ERR_OPERATION_DENIED; + } + return CTAP2_ERR_PIN_NOT_SET; + } + if (req.pin_auth_present) { + /* We don't support pin_auth. */ + return CTAP2_ERR_PIN_AUTH_INVALID; + } + + if (!req.rp.size || !req.client_data_hash_present) { + /* Both parameters are mandatory. */ + return CTAP2_ERR_MISSING_PARAMETER; + } + + /* + * Ask the user to confirm that he wants to authenticate. + * This must be done before we check for credentials so that + * we don't disclose the existance of credentials before the + * user has proven his identity (See 5.2, point 7). + */ + _get_assertion_init_state(&req); + workflow_stack_start_workflow(workflow_unlock(_get_assertion_unlock_cb, NULL)); + return CTAP1_ERR_SUCCESS; +} + +/** + * Generates a new assertion in response to a GetAssertion request. + * Only called when the user has already accepted and identified with the device. + */ +static int _get_assertion_complete(buffer_t* out_buf) +{ + ctap_get_assertion_state_t* state = &_state.data.get_assertion; + u2f_keyhandle_t auth_credential; + uint8_t auth_privkey[HMAC_SHA256_LEN]; + UTIL_CLEANUP_32(auth_privkey); + + /* + * When no allow list is present, it's mandatory that + * we add a user ID to the credential we return. + */ + uint8_t user_id[CTAP_USER_ID_MAX_SIZE] = {0}; + size_t user_id_size = 0; + + if (state->req.cred_len) { + // allowlist is present -> check all the credentials that were actually generated by us. + u2f_keyhandle_t* chosen_credential = NULL; + _authenticate_with_allow_list(&state->req, &chosen_credential, auth_privkey); + if (!chosen_credential) { + /* No credential selected (or no credential was known to the device). */ + return CTAP2_ERR_NO_CREDENTIALS; + } + memcpy(&auth_credential, chosen_credential, sizeof(auth_credential)); + } else { + // No allowList, so use all matching RK's matching rpId + uint8_t auth_status = _authenticate_with_rk(&state->req, &auth_credential, auth_privkey, user_id, &user_id_size); + if (auth_status != 0) { + return auth_status; + } + } + + size_t actual_auth_data_size; + uint8_t auth_data_buf[sizeof(ctap_auth_data_header_t) + 80]; + uint8_t ret = _make_authentication_response(&state->req, auth_data_buf, &actual_auth_data_size); + if (ret != CborNoError) { + return ret; + } + + /* Encode the resulting assertion in the output buffer. */ + CborEncoder encoder; + memset(&encoder, 0, sizeof(CborEncoder)); + cbor_encoder_init(&encoder, out_buf->data, out_buf->max_len, 0); + ret = ctap_end_get_assertion(&encoder, &auth_credential, auth_data_buf, actual_auth_data_size, auth_privkey, state->req.client_data_hash, user_id, user_id_size); + if (ret != CborNoError) { + return ret; + } + + out_buf->len = cbor_encoder_get_buffer_size(&encoder, out_buf->data); + + return CTAP1_ERR_SUCCESS; +} + +static ctap_request_result_t _get_assertion_continue(buffer_t* out_buf) +{ + ctap_request_result_t result = {.status = 0, .request_completed = true}; + ctap_get_assertion_state_t* state = &_state.data.get_assertion; + switch (state->state) { + case CTAP_GET_ASSERTION_FINISHED: + result.status = _get_assertion_complete(out_buf); + return result; + case CTAP_GET_ASSERTION_FAILED: + result.status = CTAP2_ERR_OPERATION_DENIED; + return result; + case CTAP_GET_ASSERTION_UNLOCKED: + /* + * Request permission to the user. + * This must be done before checking for excluded credentials etc. + * so that we don't reveal the existance of credentials without + * the user's consent. + */ + workflow_stack_start_workflow(_get_assertion_confirm(&_state.data.get_assertion.req.rp)); + state->state = CTAP_MAKE_CREDENTIAL_WAIT_CONFIRM; + result.request_completed = false; + return result; + case CTAP_GET_ASSERTION_STARTED: + case CTAP_GET_ASSERTION_WAIT_CONFIRM: + result.request_completed = false; + return result; + default: + Abort("Invalid get_assertion state."); + } +} + +void ctap_response_init(ctap_response_t* resp) +{ + memset(resp, 0, sizeof(*resp)); + resp->data_size = CTAP_RESPONSE_BUFFER_SIZE; +} + +ctap_request_result_t ctap_request(const in_buffer_t* in_buf, buffer_t* out_buf) +{ + CborEncoder encoder; + memset(&encoder,0,sizeof(CborEncoder)); + + uint8_t cmd = *in_buf->data; + in_buffer_t in_req_data = { + .data = in_buf->data + 1, + .len = in_buf->len - 1 + }; + + cbor_encoder_init(&encoder, out_buf->data, out_buf->max_len, 0); + ctap_request_result_t result = {.status = 0, .request_completed = true}; + + switch(cmd) + { + case CTAP_REQ_MAKE_CREDENTIAL: + result.status = ctap_make_credential(&encoder, &in_req_data); + if (result.status == CTAP1_ERR_SUCCESS) { + /* MakeCredential started successfully, don't reply yet. */ + _state.blocking_op = CTAP_BLOCKING_OP_MAKE_CRED; + result.request_completed = false; + } + break; + case CTAP_REQ_GET_ASSERTION: + result.status = ctap_get_assertion(&in_req_data); + if (result.status == CTAP1_ERR_SUCCESS) { + _state.blocking_op = CTAP_BLOCKING_OP_GET_ASSERTION; + result.request_completed = false; + } + break; + case CTAP_REQ_CANCEL: + break; + case CTAP_REQ_GET_INFO: + result.status = ctap_get_info(&encoder); + out_buf->len = cbor_encoder_get_buffer_size(&encoder, out_buf->data); + break; + case CTAP_REQ_CLIENT_PIN: + case CTAP_REQ_RESET: + case CTAP_REQ_GET_NEXT_ASSERTION: + result.status = CTAP2_ERR_NOT_ALLOWED; + break; + default: + result.status = CTAP1_ERR_INVALID_COMMAND; + } + + if (result.status != CTAP1_ERR_SUCCESS || !result.request_completed) { + out_buf->len = 0; + } + return result; +} + +ctap_request_result_t ctap_retry(buffer_t* out_buf) +{ + ctap_request_result_t result = {.status = 0, .request_completed = true}; + + switch (_state.blocking_op) { + case CTAP_BLOCKING_OP_MAKE_CRED: + result = _make_credential_continue(out_buf); + if (result.request_completed) { + _state.blocking_op = CTAP_BLOCKING_OP_NONE; + _make_credential_free_state(); + } + break; + case CTAP_BLOCKING_OP_GET_ASSERTION: + result = _get_assertion_continue(out_buf); + if (result.request_completed) { + _state.blocking_op = CTAP_BLOCKING_OP_NONE; + _get_assertion_free_state(); + } + break; + case CTAP_BLOCKING_OP_NONE: + default: + Abort("Invalid status in ctap_retry"); + } + return result; +} diff --git a/src/fido2/ctap.h b/src/fido2/ctap.h new file mode 100644 index 000000000..02f8113d6 --- /dev/null +++ b/src/fido2/ctap.h @@ -0,0 +1,353 @@ +// Copyright 2019 SoloKeys Developers +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. +#ifndef _CTAP_H +#define _CTAP_H + +#include +#include +#include +#include + +#ifdef assert +#undef assert +#endif + +/* + * TinyCBOR will use the default definition for assert. + * The USB driver will include its own definition though... + * + * Replace it with our custom definition (i.e. Abort if condition + * is false). + */ +#define assert(cond) \ + do { \ + if (!(cond)) { \ + Abort("Assertion failed:\n" #cond); \ + } \ + } while(0); + +#include + +#undef assert + +#include + +#define EXT_HMAC_SECRET_COSE_KEY 0x01 +#define EXT_HMAC_SECRET_SALT_ENC 0x02 +#define EXT_HMAC_SECRET_SALT_AUTH 0x03 + +#define EXT_HMAC_SECRET_REQUESTED 0x01 +#define EXT_HMAC_SECRET_PARSED 0x02 + +#define CLIENT_DATA_HASH_SIZE (SHA256_LEN) +#define DOMAIN_NAME_MAX_SIZE 253 +#define RP_NAME_LIMIT 32 // application limit, name parameter isn't needed. +#define CTAP_USER_ID_MAX_SIZE 64 + +/** + * Maximum length of the CTAP username getting stored + * in a resident credential. + * Can be longer than 32B, but we only store this + * data for displaying it when authenticating. + * So if the actual length is longer we can just display + * a truncated string. + */ +#define CTAP_STORAGE_USER_NAME_LIMIT (20) + +/** + * Maximum length of the CTAP username getting stored + * in a resident credential. + * Can be longer than 32B, but we only store this + * data for displaying it when authenticating. + * So if the actual length is longer we can just display + * a truncated string. + */ +#define CTAP_STORAGE_RP_ID_MAX_SIZE (20) + +/** + * Maximum length of the CTAP username getting stored + * in a resident credential. It could be truncated. + */ +#define CTAP_STORAGE_DISPLAY_NAME_LIMIT (20) + +/** Maximum length of the CTAP username. */ +#define CTAP_USER_NAME_LIMIT (64) +#define DISPLAY_NAME_LIMIT 32 // Must be minimum of 64 bytes but can be more. +#define ICON_LIMIT 128 // Must be minimum of 64 bytes but can be more. +#define CTAP_MAX_MESSAGE_SIZE 1200 + +#define CREDENTIAL_RK_FLASH_PAD 2 // size of RK should be 8-byte aligned to store in flash easily. + #define CREDENTIAL_TAG_SIZE 16 + #define CREDENTIAL_NONCE_SIZE (16 + CREDENTIAL_RK_FLASH_PAD) + #define CREDENTIAL_COUNTER_SIZE (4) + #define CREDENTIAL_ENC_SIZE 176 // pad to multiple of 16 bytes + + #define PUB_KEY_CRED_PUB_KEY 0x01 + #define PUB_KEY_CRED_CTAP1 0x41 + #define PUB_KEY_CRED_CUSTOM 0x42 + #define PUB_KEY_CRED_UNKNOWN 0x3F + + #define CREDENTIAL_IS_SUPPORTED 1 + #define CREDENTIAL_NOT_SUPPORTED 0 + + #define CTAP_CREDENTIAL_LIST_MAX_SIZE 20 + + #define NEW_PIN_ENC_MAX_SIZE 256 // includes NULL terminator + #define NEW_PIN_ENC_MIN_SIZE 64 + #define NEW_PIN_MAX_SIZE 64 + #define NEW_PIN_MIN_SIZE 4 + + #define CTAP_RESPONSE_BUFFER_SIZE 4096 + + #define PIN_LOCKOUT_ATTEMPTS 8 // Number of attempts total + #define PIN_BOOT_ATTEMPTS 3 // number of attempts per boot + + #define CTAP2_UP_DELAY_MS 29000 + + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wpacked" + #pragma GCC diagnostic ignored "-Wattributes" + +typedef struct { + uint8_t id[CTAP_USER_ID_MAX_SIZE]; + uint8_t id_size; + uint8_t name[CTAP_USER_NAME_LIMIT]; + uint8_t display_name[DISPLAY_NAME_LIMIT]; + uint8_t icon[ICON_LIMIT]; +} ctap_user_entity_t; + +#define CTAP_RESIDENT_KEY_VALID (0x01) + +/** + * Invalid keys have their "valid" field + * set to 0xFF so that erased flash is invalid. + */ +#define CTAP_RESIDENT_KEY_INVALID (0xff) + +typedef struct __attribute__((__packed__)) { + uint8_t valid; + + /** Key handle (credential ID) */ + u2f_keyhandle_t key_handle; + + /** + * Human-readable ID of the RP that created the credential. + * This is a NULL-terminated string. + */ + uint8_t rp_id[CTAP_STORAGE_RP_ID_MAX_SIZE]; + /** + * sha256 hash of the original RP id. + * This is necessary (together with the ID) so + * that we can make check RP ids for matching values + * even if the actual RP id is longer than 32 bytes. + */ + uint8_t rp_id_hash[32]; + /** + * User ID that the RP has assigned to our user. + * We need to store this and send it back together + * with our keyhandle when we're asked to authenticate. + */ + uint8_t user_id[CTAP_USER_ID_MAX_SIZE]; + /** + * Size of user_id. + */ + uint8_t user_id_size; + /** + * Username belonging to this credential. + * This is a NULL terminated string. + * Side effect: if a credential is created + * which matches the first CTAP_STORAGE_USER_NAME_LIMIT + * characters of the user and display name of an existing + * credential, the latter is going to be overwritten. + * Can this be used for evil purposes? (it shouldn't). + */ + uint8_t user_name[CTAP_STORAGE_USER_NAME_LIMIT]; + /** + * Display name of the user. Same considerations apply + * as for the user_name. + */ + uint8_t display_name[CTAP_STORAGE_DISPLAY_NAME_LIMIT]; + /** + * Creation "time" of the key. + * This is the value that the U2F counter had when the + * key got created. We must not store any real timestamp, + * but we must be able to sort keys by creation time. + */ + uint32_t creation_time; +} ctap_resident_key_t; + +/** + * Attested credential data, defined + * in [WebAuthn] 6.4.1. + */ +typedef struct __attribute__((packed)) { + /** The AAGUID of the authenticator. */ + uint8_t aaguid[16]; + /** Length of the credential ID (big-endian) */ + uint8_t cred_len[2]; + /** Credential ID */ + u2f_keyhandle_t id; +} ctap_attest_data_t; + +/** + * Authenticator data structure, to use + * for authentication operations. It is + * missing the attestedCredentialData + * field. Defined in The WebAuthn specs, 6.1. + */ +typedef struct __attribute__((packed)) { + uint8_t rp_id_hash[32]; + uint8_t flags; + uint32_t signCount; +} ctap_auth_data_header_t; + +/** + * Authenticator data structure, including + * the attestedCredentialData field. + * Defined in The WebAuthn specs, 6.1. + */ +typedef struct __attribute__((packed)) { + ctap_auth_data_header_t head; + ctap_attest_data_t attest; + /* COSE-encoded pubkey and extension data */ + uint8_t other[310 - sizeof(ctap_auth_data_header_t) - sizeof(ctap_attest_data_t)]; +} ctap_auth_data_t; + +#pragma GCC diagnostic pop + +typedef struct +{ + uint8_t data[CTAP_RESPONSE_BUFFER_SIZE]; + uint16_t data_size; + uint16_t length; +} ctap_response_t; + +typedef struct { + uint8_t id[DOMAIN_NAME_MAX_SIZE + 1]; // extra for NULL termination + /* TODO change to id_size */ + size_t size; + uint8_t name[RP_NAME_LIMIT]; +} ctap_rp_id_t; + +typedef struct +{ + struct{ + uint8_t x[32]; + uint8_t y[32]; + } pubkey; + + int kty; + int crv; +} COSE_key; + +typedef struct +{ + uint8_t saltLen; + uint8_t saltEnc[64]; + uint8_t saltAuth[32]; + COSE_key keyAgreement; + u2f_keyhandle_t* key_handle; +} CTAP_hmac_secret; + +typedef struct +{ + uint8_t hmac_secret_present; + CTAP_hmac_secret hmac_secret; +} ctap_extensions_t; + +typedef struct +{ + ctap_user_entity_t user; + uint8_t publicKeyCredentialType; + int32_t COSEAlgorithmIdentifier; + uint8_t rk; +} ctap_cred_info_t; + +typedef struct +{ + uint32_t param_parsed; + uint8_t client_data_hash[CLIENT_DATA_HASH_SIZE]; + ctap_rp_id_t rp; + + ctap_cred_info_t cred_info; + + CborValue exclude_list; + size_t exclude_list_size; + + uint8_t uv; + uint8_t up; + + uint8_t pin_auth[16]; + uint8_t pin_auth_present; + // pin_auth_empty is true iff an empty bytestring was provided as pin_auth. + // This is exclusive with |pin_auth_present|. It exists because an empty + // pin_auth is a special signal to block for touch. See + // https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#using-pinToken-in-authenticatorMakeCredential + uint8_t pin_auth_empty; + int pin_protocol; + ctap_extensions_t extensions; + +} ctap_make_credential_req_t; + + + +typedef struct +{ + uint32_t param_parsed; + uint8_t client_data_hash[CLIENT_DATA_HASH_SIZE]; + uint8_t client_data_hash_present; + + ctap_rp_id_t rp; + + uint8_t rk; + uint8_t uv; + uint8_t up; + + /* TODO remove pin_auth, we don't use it anyway. */ + uint8_t pin_auth[16]; + uint8_t pin_auth_present; + /** + * pin_auth_empty is true iff an empty bytestring was provided as pin_auth. + * This is exclusive with |pin_auth_present|. It exists because an empty + * pin_auth is a special signal to block for touch. See + * https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#using-pinToken-in-authenticatorGetAssertion + */ + uint8_t pin_auth_empty; + int pin_protocol; + + /** + * List of allowed credential descriptors for authentication. + * If this parameter is present, then the authenticator MUST + * use one of these credentials to authenticate. + */ + u2f_keyhandle_t creds[CTAP_CREDENTIAL_LIST_MAX_SIZE]; + /** Number of credential descriptors present in this request. */ + int cred_len; + + uint8_t allowListPresent; + + ctap_extensions_t extensions; +} ctap_get_assertion_req_t; + +typedef struct { + /** CTAP_* success/error code. */ + uint8_t status; + /** If true, a response for this request can be sent. */ + bool request_completed; +} ctap_request_result_t; + +void ctap_response_init(ctap_response_t* resp); + +ctap_request_result_t ctap_request(const in_buffer_t* in_buf, buffer_t* out_buf); +ctap_request_result_t ctap_retry(buffer_t* out_buf); + +// Run ctap related power-up procedures (init pinToken, generate shared secret) +void ctap_init(void); + +void make_auth_tag(uint8_t* rp_id_hash, uint8_t* nonce, uint32_t count, uint8_t* tag); + +#endif // _CTAP_H diff --git a/src/fido2/ctap_errors.h b/src/fido2/ctap_errors.h new file mode 100644 index 000000000..fc2f25778 --- /dev/null +++ b/src/fido2/ctap_errors.h @@ -0,0 +1,58 @@ +// Copyright 2019 SoloKeys Developers +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. +#define CTAP1_ERR_SUCCESS 0x00 +#define CTAP1_ERR_INVALID_COMMAND 0x01 +#define CTAP1_ERR_INVALID_PARAMETER 0x02 +#define CTAP1_ERR_INVALID_LENGTH 0x03 +#define CTAP1_ERR_INVALID_SEQ 0x04 +#define CTAP1_ERR_TIMEOUT 0x05 +#define CTAP1_ERR_CHANNEL_BUSY 0x06 +#define CTAP1_ERR_LOCK_REQUIRED 0x0A +#define CTAP1_ERR_INVALID_CHANNEL 0x0B +#define CTAP2_ERR_CBOR_PARSING 0x10 +#define CTAP2_ERR_CBOR_UNEXPECTED_TYPE 0x11 +#define CTAP2_ERR_INVALID_CBOR 0x12 +#define CTAP2_ERR_INVALID_CBOR_TYPE 0x13 +#define CTAP2_ERR_MISSING_PARAMETER 0x14 +#define CTAP2_ERR_LIMIT_EXCEEDED 0x15 +#define CTAP2_ERR_UNSUPPORTED_EXTENSION 0x16 +#define CTAP2_ERR_TOO_MANY_ELEMENTS 0x17 +#define CTAP2_ERR_EXTENSION_NOT_SUPPORTED 0x18 +#define CTAP2_ERR_CREDENTIAL_EXCLUDED 0x19 +#define CTAP2_ERR_CREDENTIAL_NOT_VALID 0x20 +#define CTAP2_ERR_PROCESSING 0x21 +#define CTAP2_ERR_INVALID_CREDENTIAL 0x22 +#define CTAP2_ERR_USER_ACTION_PENDING 0x23 +#define CTAP2_ERR_OPERATION_PENDING 0x24 +#define CTAP2_ERR_NO_OPERATIONS 0x25 +#define CTAP2_ERR_UNSUPPORTED_ALGORITHM 0x26 +#define CTAP2_ERR_OPERATION_DENIED 0x27 +#define CTAP2_ERR_KEY_STORE_FULL 0x28 +#define CTAP2_ERR_NOT_BUSY 0x29 +#define CTAP2_ERR_NO_OPERATION_PENDING 0x2A +#define CTAP2_ERR_UNSUPPORTED_OPTION 0x2B +#define CTAP2_ERR_INVALID_OPTION 0x2C +#define CTAP2_ERR_KEEPALIVE_CANCEL 0x2D +#define CTAP2_ERR_NO_CREDENTIALS 0x2E +#define CTAP2_ERR_USER_ACTION_TIMEOUT 0x2F +#define CTAP2_ERR_NOT_ALLOWED 0x30 +#define CTAP2_ERR_PIN_INVALID 0x31 +#define CTAP2_ERR_PIN_BLOCKED 0x32 +#define CTAP2_ERR_PIN_AUTH_INVALID 0x33 +#define CTAP2_ERR_PIN_AUTH_BLOCKED 0x34 +#define CTAP2_ERR_PIN_NOT_SET 0x35 +#define CTAP2_ERR_PIN_REQUIRED 0x36 +#define CTAP2_ERR_PIN_POLICY_VIOLATION 0x37 +#define CTAP2_ERR_PIN_TOKEN_EXPIRED 0x38 +#define CTAP2_ERR_REQUEST_TOO_LARGE 0x39 +#define CTAP2_ERR_ACTION_TIMEOUT 0x3A +#define CTAP1_ERR_OTHER 0x7F +#define CTAP2_ERR_SPEC_LAST 0xDF +#define CTAP2_ERR_EXTENSION_FIRST 0xE0 +#define CTAP2_ERR_EXTENSION_LAST 0xEF +#define CTAP2_ERR_VENDOR_FIRST 0xF0 +#define CTAP2_ERR_VENDOR_LAST 0xFF diff --git a/src/fido2/ctap_parse.c b/src/fido2/ctap_parse.c new file mode 100644 index 000000000..ad5279797 --- /dev/null +++ b/src/fido2/ctap_parse.c @@ -0,0 +1,1130 @@ +// Copyright 2019 SoloKeys Developers +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. +#include +#include + +#include "cbor.h" + +#include "ctap.h" +#include "ctap_parse.h" +#include "ctap_errors.h" +#include "cose_key.h" +#include "util.h" + +#include + +#define check_retr(r) \ + do { \ + if ((r) != CborNoError) { \ + return r; \ + } \ + } while(0); + +/** + * Field tags used in MakeCredential requests. + */ +#define MAKE_CREDENTIAL_TAG_CLIENT_DATA_HASH (0x01) +#define MAKE_CREDENTIAL_TAG_RELYING_PARTY (0x02) +#define MAKE_CREDENTIAL_TAG_USER (0x03) +#define MAKE_CREDENTIAL_TAG_PUB_KEY_CRED_PARAMS (0x04) +#define MAKE_CREDENTIAL_TAG_EXCLUDE_LIST (0x05) +#define MAKE_CREDENTIAL_TAG_EXTENSIONS (0x06) +#define MAKE_CREDENTIAL_TAG_OPTIONS (0x07) +#define MAKE_CREDENTIAL_TAG_PIN_AUTH (0x08) +#define MAKE_CREDENTIAL_TAG_PIN_PROTOCOL (0x09) + +/** + * Field tags used in MakeCredential requests. + */ +#define GET_ASSERTION_TAG_RPID (0x01) +#define GET_ASSERTION_TAG_CLIENT_DATA_HASH (0x02) +#define GET_ASSERTION_TAG_ALLOW_LIST (0x03) +#define GET_ASSERTION_TAG_EXTENSIONS (0x04) +#define GET_ASSERTION_TAG_OPTIONS (0x05) +#define GET_ASSERTION_TAG_PIN_AUTH (0x06) +#define GET_ASSERTION_TAG_PIN_PROTOCOL (0x07) + +/** + * Parameters contained in the requests. + */ +#define PARAM_CLIENT_DATA_HASH (1 << 0) +#define PARAM_RP (1 << 1) +#define PARAM_USER (1 << 2) +#define PARAM_PUB_KEY_CRED_PARAMS (1 << 3) +#define PARAM_EXCLUDE_LIST (1 << 4) +#define PARAM_EXTENSIONS (1 << 5) +#define PARAM_OPTIONS (1 << 6) +#define PARAM_PIN_AUTH (1 << 7) +#define PARAM_PIN_PROTOCOL (1 << 8) +#define PARAM_RP_ID (1 << 9) +#define PARAM_ALLOW_LIST (1 << 10) + +/** Required parameters for a MakeCredential request. */ +static const uint8_t MAKE_CREDENTIAL_REQUIRED_PARAM_MASK = + PARAM_CLIENT_DATA_HASH | PARAM_RP | PARAM_USER | PARAM_PUB_KEY_CRED_PARAMS; + +static uint8_t _parse_user(ctap_make_credential_req_t * MC, CborValue * val) +{ + size_t sz, map_length; + uint8_t key[24]; + int ret; + unsigned int i; + CborValue map; + + + if (cbor_value_get_type(val) != CborMapType) + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + ret = cbor_value_enter_container(val,&map); + check_ret(ret); + + ret = cbor_value_get_map_length(val, &map_length); + check_ret(ret); + + for (i = 0; i < map_length; i++) + { + if (cbor_value_get_type(&map) != CborTextStringType) + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + sz = sizeof(key); + ret = cbor_value_copy_text_string(&map, (char *)key, &sz, NULL); + + if (ret == CborErrorOutOfMemory) + { + return CTAP2_ERR_LIMIT_EXCEEDED; + } + + check_ret(ret); + key[sizeof(key) - 1] = 0; + + ret = cbor_value_advance(&map); + check_ret(ret); + + if (strcmp((const char*)key, "id") == 0) + { + + if (cbor_value_get_type(&map) != CborByteStringType) + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + sz = CTAP_USER_ID_MAX_SIZE; + ret = cbor_value_copy_byte_string(&map, MC->cred_info.user.id, &sz, NULL); + if (ret == CborErrorOutOfMemory) + { + return CTAP2_ERR_LIMIT_EXCEEDED; + } + MC->cred_info.user.id_size = sz; + check_ret(ret); + } + else if (strcmp((const char *)key, "name") == 0) + { + if (cbor_value_get_type(&map) != CborTextStringType) + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + sz = CTAP_USER_NAME_LIMIT; + ret = cbor_value_copy_text_string(&map, (char *)MC->cred_info.user.name, &sz, NULL); + if (ret != CborErrorOutOfMemory) + { // Just truncate the name it's okay + check_ret(ret); + } + MC->cred_info.user.name[CTAP_USER_NAME_LIMIT - 1] = 0; + } + else if (strcmp((const char *)key, "displayName") == 0) + { + if (cbor_value_get_type(&map) != CborTextStringType) + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + sz = DISPLAY_NAME_LIMIT; + ret = cbor_value_copy_text_string(&map, (char *)MC->cred_info.user.display_name, &sz, NULL); + if (ret != CborErrorOutOfMemory) + { // Just truncate the name it's okay + check_ret(ret); + } + MC->cred_info.user.display_name[DISPLAY_NAME_LIMIT - 1] = 0; + } + else if (strcmp((const char *)key, "icon") == 0) + { + if (cbor_value_get_type(&map) != CborTextStringType) + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + sz = ICON_LIMIT; + ret = cbor_value_copy_text_string(&map, (char *)MC->cred_info.user.icon, &sz, NULL); + if (ret != CborErrorOutOfMemory) + { // Just truncate the name it's okay + check_ret(ret); + } + MC->cred_info.user.icon[ICON_LIMIT - 1] = 0; + + } + + ret = cbor_value_advance(&map); + check_ret(ret); + + } + + MC->param_parsed |= PARAM_USER; + + return 0; +} + + +static uint8_t _parse_pub_key_cred_param(CborValue * val, uint8_t * cred_type, int32_t * alg_type) +{ + CborValue cred; + CborValue alg; + int ret; + uint8_t type_str[16]; + size_t sz = sizeof(type_str); + + if (cbor_value_get_type(val) != CborMapType) + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + ret = cbor_value_map_find_value(val, "type", &cred); + check_ret(ret); + ret = cbor_value_map_find_value(val, "alg", &alg); + check_ret(ret); + + if (cbor_value_get_type(&cred) != CborTextStringType) + { + return CTAP2_ERR_MISSING_PARAMETER; + } + if (cbor_value_get_type(&alg) != CborIntegerType) + { + return CTAP2_ERR_MISSING_PARAMETER; + } + + ret = cbor_value_copy_text_string(&cred, (char*)type_str, &sz, NULL); + check_ret(ret); + + type_str[sizeof(type_str) - 1] = 0; + + if (strcmp((const char*)type_str, "public-key") == 0) + { + *cred_type = PUB_KEY_CRED_PUB_KEY; + } + else + { + *cred_type = PUB_KEY_CRED_UNKNOWN; + } + + ret = cbor_value_get_int_checked(&alg, (int*)alg_type); + check_ret(ret); + + return 0; +} + +// Check if public key credential+algorithm type is supported +static int _pub_key_cred_param_supported(uint8_t cred, int32_t alg) +{ + if (cred == PUB_KEY_CRED_PUB_KEY) + { + if (alg == COSE_ALG_ES256) + { + return CREDENTIAL_IS_SUPPORTED; + } + } + + return CREDENTIAL_NOT_SUPPORTED; +} + +static uint8_t _parse_pub_key_cred_params(ctap_make_credential_req_t * MC, CborValue * val) +{ + size_t arr_length; + uint8_t cred_type; + int32_t alg_type; + int ret; + unsigned int i; + CborValue arr; + + + if (cbor_value_get_type(val) != CborArrayType) + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + ret = cbor_value_enter_container(val,&arr); + check_ret(ret); + + ret = cbor_value_get_array_length(val, &arr_length); + check_ret(ret); + + for (i = 0; i < arr_length; i++) + { + if ((ret = _parse_pub_key_cred_param(&arr, &cred_type, &alg_type)) != 0) + { + return ret; + } + ret = cbor_value_advance(&arr); + check_ret(ret); + } + + ret = cbor_value_enter_container(val,&arr); + check_ret(ret); + + for (i = 0; i < arr_length; i++) + { + if ((ret = _parse_pub_key_cred_param(&arr, &cred_type, &alg_type)) == 0) + { + if (_pub_key_cred_param_supported(cred_type, alg_type) == CREDENTIAL_IS_SUPPORTED) + { + MC->cred_info.publicKeyCredentialType = cred_type; + MC->cred_info.COSEAlgorithmIdentifier = alg_type; + MC->param_parsed |= PARAM_PUB_KEY_CRED_PARAMS; + return 0; + } + } + ret = cbor_value_advance(&arr); + check_ret(ret); + } + + return CTAP2_ERR_UNSUPPORTED_ALGORITHM; +} + +static uint8_t _parse_fixed_byte_string(CborValue * map, uint8_t * dst, unsigned int len) +{ + size_t sz; + int ret; + if (cbor_value_get_type(map) == CborByteStringType) + { + sz = len; + ret = cbor_value_copy_byte_string(map, dst, &sz, NULL); + check_ret(ret); + if (sz != len) + { + return CTAP1_ERR_INVALID_LENGTH; + } + } + else + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + return 0; +} + +static uint8_t parse_verify_exclude_list(CborValue * val) +{ + unsigned int i; + int ret; + CborValue arr; + size_t size; + u2f_keyhandle_t cred; + if (cbor_value_get_type(val) != CborArrayType) + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + ret = cbor_value_get_array_length(val, &size); + check_ret(ret); + ret = cbor_value_enter_container(val,&arr); + check_ret(ret); + for (i = 0; i < size; i++) + { + bool cred_valid; + ret = ctap_parse_credential_descriptor(&arr, &cred, &cred_valid); + if (!cred_valid) { + return CTAP2_ERR_INVALID_CBOR; + } + check_ret(ret); + ret = cbor_value_advance(&arr); + check_ret(ret); + + } + return 0; +} + +static uint8_t _parse_rp_id(ctap_rp_id_t * rp, CborValue * val) +{ + size_t sz = DOMAIN_NAME_MAX_SIZE; + if (cbor_value_get_type(val) != CborTextStringType) + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + int ret = cbor_value_copy_text_string(val, (char*)rp->id, &sz, NULL); + if (ret == CborErrorOutOfMemory) + { + return CTAP2_ERR_LIMIT_EXCEEDED; + } + check_ret(ret); + rp->id[DOMAIN_NAME_MAX_SIZE] = 0; // Extra byte defined in struct. + rp->size = sz; + return 0; +} + +static uint8_t _parse_rp(ctap_rp_id_t * rp, CborValue * val) +{ + size_t sz, map_length; + char key[8]; + int ret; + unsigned int i; + CborValue map; + + + if (cbor_value_get_type(val) != CborMapType) + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + ret = cbor_value_enter_container(val,&map); + check_ret(ret); + + ret = cbor_value_get_map_length(val, &map_length); + check_ret(ret); + + rp->size = 0; + + for (i = 0; i < map_length; i++) + { + if (cbor_value_get_type(&map) != CborTextStringType) + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + sz = sizeof(key); + ret = cbor_value_copy_text_string(&map, key, &sz, NULL); + + if (ret == CborErrorOutOfMemory) + { + return CTAP2_ERR_LIMIT_EXCEEDED; + } + check_ret(ret); + key[sizeof(key) - 1] = 0; + + ret = cbor_value_advance(&map); + check_ret(ret); + + if (cbor_value_get_type(&map) != CborTextStringType) + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + if (strcmp(key, "id") == 0) + { + ret = _parse_rp_id(rp, &map); + if (ret != 0) + { + return ret; + } + } + else if (strcmp(key, "name") == 0) + { + sz = RP_NAME_LIMIT; + ret = cbor_value_copy_text_string(&map, (char*)rp->name, &sz, NULL); + if (ret != CborErrorOutOfMemory) + { // Just truncate the name it's okay + check_ret(ret); + } + rp->name[RP_NAME_LIMIT - 1] = 0; + } + + ret = cbor_value_advance(&map); + check_ret(ret); + + } + if (rp->size == 0) + { + return CTAP2_ERR_MISSING_PARAMETER; + } + + + return 0; +} + +static uint8_t _parse_options(CborValue * val, uint8_t * rk, uint8_t * uv, uint8_t * up) +{ + size_t sz, map_length; + char key[8]; + int ret; + unsigned int i; + _Bool b; + CborValue map; + + if (cbor_value_get_type(val) != CborMapType) + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + ret = cbor_value_enter_container(val,&map); + check_ret(ret); + + ret = cbor_value_get_map_length(val, &map_length); + check_ret(ret); + + + for (i = 0; i < map_length; i++) + { + if (cbor_value_get_type(&map) != CborTextStringType) + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + sz = sizeof(key); + ret = cbor_value_copy_text_string(&map, key, &sz, NULL); + + if (ret == CborErrorOutOfMemory) + { + return CTAP2_ERR_LIMIT_EXCEEDED; + } + check_ret(ret); + key[sizeof(key) - 1] = 0; + + ret = cbor_value_advance(&map); + check_ret(ret); + + if (cbor_value_get_type(&map) != CborBooleanType) + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + if (strncmp(key, "rk",2) == 0) + { + ret = cbor_value_get_boolean(&map, &b); + check_ret(ret); + *rk = b; + } + else if (strncmp(key, "uv",2) == 0) + { + ret = cbor_value_get_boolean(&map, &b); + check_ret(ret); + *uv = b; + } + else if (strncmp(key, "up",2) == 0) + { + ret = cbor_value_get_boolean(&map, &b); + check_ret(ret); + *up = b; + } + ret = cbor_value_advance(&map); + check_ret(ret); + } + return 0; +} + +static uint8_t _parse_cose_key(CborValue * it, COSE_key * cose) +{ + CborValue map; + size_t map_length; + int ret,key; + unsigned int i; + int xkey = 0,ykey = 0; + cose->kty = 0; + cose->crv = 0; + + + CborType type = cbor_value_get_type(it); + if (type != CborMapType) + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + ret = cbor_value_enter_container(it,&map); + check_ret(ret); + + ret = cbor_value_get_map_length(it, &map_length); + check_ret(ret); + + + for (i = 0; i < map_length; i++) + { + if (cbor_value_get_type(&map) != CborIntegerType) + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + ret = cbor_value_get_int_checked(&map, &key); + check_ret(ret); + + ret = cbor_value_advance(&map); + check_ret(ret); + + switch(key) + { + case COSE_KEY_LABEL_KTY: + if (cbor_value_get_type(&map) == CborIntegerType) + { + ret = cbor_value_get_int_checked(&map, &cose->kty); + check_ret(ret); + } + else + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + break; + case COSE_KEY_LABEL_ALG: + break; + case COSE_KEY_LABEL_CRV: + if (cbor_value_get_type(&map) == CborIntegerType) + { + ret = cbor_value_get_int_checked(&map, &cose->crv); + check_ret(ret); + } + else + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + break; + case COSE_KEY_LABEL_X: + ret = _parse_fixed_byte_string(&map, cose->pubkey.x, 32); + check_retr(ret); + xkey = 1; + + break; + case COSE_KEY_LABEL_Y: + ret = _parse_fixed_byte_string(&map, cose->pubkey.y, 32); + check_retr(ret); + ykey = 1; + + break; + default: + break; + } + + ret = cbor_value_advance(&map); + check_ret(ret); + } + if (xkey == 0 || ykey == 0 || cose->kty == 0 || cose->crv == 0) + { + return CTAP2_ERR_MISSING_PARAMETER; + } + return 0; +} + +static uint8_t ctap_parse_hmac_secret(CborValue * val, CTAP_hmac_secret * hs) +{ + size_t map_length; + size_t salt_len; + uint8_t parsed_count = 0; + int key; + int ret; + unsigned int i; + CborValue map; + + if (cbor_value_get_type(val) != CborMapType) + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + ret = cbor_value_enter_container(val,&map); + check_ret(ret); + + ret = cbor_value_get_map_length(val, &map_length); + check_ret(ret); + + for (i = 0; i < map_length; i++) + { + if (cbor_value_get_type(&map) != CborIntegerType) + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + ret = cbor_value_get_int(&map, &key); + check_ret(ret); + + ret = cbor_value_advance(&map); + check_ret(ret); + + switch(key) + { + case EXT_HMAC_SECRET_COSE_KEY: + ret = _parse_cose_key(&map, &hs->keyAgreement); + check_retr(ret); + parsed_count++; + break; + case EXT_HMAC_SECRET_SALT_ENC: + salt_len = 64; + ret = cbor_value_copy_byte_string(&map, hs->saltEnc, &salt_len, NULL); + if ((salt_len != 32 && salt_len != 64) || ret == CborErrorOutOfMemory) + { + return CTAP1_ERR_INVALID_LENGTH; + } + check_ret(ret); + hs->saltLen = salt_len; + parsed_count++; + break; + case EXT_HMAC_SECRET_SALT_AUTH: + salt_len = 32; + ret = cbor_value_copy_byte_string(&map, hs->saltAuth, &salt_len, NULL); + check_ret(ret); + parsed_count++; + break; + default: + Abort("ctap_parse_hmac_secret: bad key"); + } + + ret = cbor_value_advance(&map); + check_ret(ret); + } + + if (parsed_count != 3) + { + return CTAP2_ERR_MISSING_PARAMETER; + } + + return 0; +} + + +static uint8_t ctap_parse_extensions(CborValue* val, ctap_extensions_t* ext) +{ + CborValue map; + size_t sz, map_length; + char key[16]; + int ret; + unsigned int i; + bool b; + + if (cbor_value_get_type(val) != CborMapType) + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + ret = cbor_value_enter_container(val, &map); + check_ret(ret); + + ret = cbor_value_get_map_length(val, &map_length); + check_ret(ret); + + for (i = 0; i < map_length; i++) + { + if (cbor_value_get_type(&map) != CborTextStringType) + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + sz = sizeof(key); + ret = cbor_value_copy_text_string(&map, key, &sz, NULL); + + if (ret == CborErrorOutOfMemory) + { + cbor_value_advance(&map); + cbor_value_advance(&map); + continue; + } + check_ret(ret); + key[sizeof(key) - 1] = 0; + + ret = cbor_value_advance(&map); + check_ret(ret); + + + if (strncmp(key, "hmac-secret",11) == 0) + { + if (cbor_value_get_type(&map) == CborBooleanType) + { + ret = cbor_value_get_boolean(&map, &b); + check_ret(ret); + if (b) ext->hmac_secret_present = EXT_HMAC_SECRET_REQUESTED; + } + else if (cbor_value_get_type(&map) == CborMapType) + { + ret = ctap_parse_hmac_secret(&map, &ext->hmac_secret); + check_retr(ret); + ext->hmac_secret_present = EXT_HMAC_SECRET_PARSED; + } + } + + ret = cbor_value_advance(&map); + check_ret(ret); + } + return 0; +} + +uint8_t ctap_parse_make_credential(ctap_make_credential_req_t * MC, CborEncoder * encoder, const in_buffer_t* in_buffer) +{ + (void)encoder; + int ret; + unsigned int i; + int key; + size_t map_length; + CborParser parser; + CborValue it,map; + + memset(MC, 0, sizeof(*MC)); + MC->up = 0xff; + ret = cbor_parser_init(in_buffer->data, in_buffer->len, CborValidateCanonicalFormat, &parser, &it); + check_retr(ret); + + CborType type = cbor_value_get_type(&it); + if (type != CborMapType) + { + return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + } + + ret = cbor_value_enter_container(&it,&map); + check_ret(ret); + + ret = cbor_value_get_map_length(&it, &map_length); + check_ret(ret); + + + for (i = 0; i < map_length; i++) + { + type = cbor_value_get_type(&map); + if (type != CborIntegerType) + { + return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + } + ret = cbor_value_get_int_checked(&map, &key); + check_ret(ret); + + ret = cbor_value_advance(&map); + check_ret(ret); + ret = 0; + + switch(key) + { + + case MAKE_CREDENTIAL_TAG_CLIENT_DATA_HASH: + + ret = _parse_fixed_byte_string(&map, MC->client_data_hash, CLIENT_DATA_HASH_SIZE); + if (ret == 0) + { + MC->param_parsed |= PARAM_CLIENT_DATA_HASH; + } + + break; + case MAKE_CREDENTIAL_TAG_RELYING_PARTY: + + ret = _parse_rp(&MC->rp, &map); + if (ret == 0) + { + MC->param_parsed |= PARAM_RP; + } + + + break; + case MAKE_CREDENTIAL_TAG_USER: + + ret = _parse_user(MC, &map); + + + break; + case MAKE_CREDENTIAL_TAG_PUB_KEY_CRED_PARAMS: + + ret = _parse_pub_key_cred_params(MC, &map); + + + break; + case MAKE_CREDENTIAL_TAG_EXCLUDE_LIST: + ret = parse_verify_exclude_list(&map); + check_ret(ret); + + ret = cbor_value_enter_container(&map, &MC->exclude_list); + check_ret(ret); + + ret = cbor_value_get_array_length(&map, &MC->exclude_list_size); + check_ret(ret); + + + break; + case MAKE_CREDENTIAL_TAG_EXTENSIONS: + type = cbor_value_get_type(&map); + if (type != CborMapType) + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + ret = ctap_parse_extensions(&map, &MC->extensions); + check_retr(ret); + break; + + case MAKE_CREDENTIAL_TAG_OPTIONS: + ret = _parse_options(&map, &MC->cred_info.rk, &MC->uv, &MC->up); + check_retr(ret); + break; + case MAKE_CREDENTIAL_TAG_PIN_AUTH: { + + size_t pinSize; + if (cbor_value_get_type(&map) == CborByteStringType && + cbor_value_get_string_length(&map, &pinSize) == CborNoError && + pinSize == 0) + { + MC->pin_auth_empty = 1; + break; + } + + ret = _parse_fixed_byte_string(&map, MC->pin_auth, 16); + if (CTAP1_ERR_INVALID_LENGTH != ret) // damn microsoft + { + check_retr(ret); + } + else + { + ret = 0; + } + MC->pin_auth_present = 1; + break; + } + case MAKE_CREDENTIAL_TAG_PIN_PROTOCOL: + if (cbor_value_get_type(&map) == CborIntegerType) + { + ret = cbor_value_get_int_checked(&map, &MC->pin_protocol); + check_ret(ret); + } + else + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + break; + + default: + break; + + } + if (ret != 0) + { + return ret; + } + cbor_value_advance(&map); + check_ret(ret); + } + + /* Check if all the mandatory parameters are present in the request. */ + if ((MC->param_parsed & MAKE_CREDENTIAL_REQUIRED_PARAM_MASK) != MAKE_CREDENTIAL_REQUIRED_PARAM_MASK) { + return CTAP2_ERR_MISSING_PARAMETER; + } + return 0; +} + +uint8_t ctap_parse_credential_descriptor(CborValue* arr, u2f_keyhandle_t* cred, bool* cred_valid_out) +{ + int ret; + size_t buflen; + char type[12]; + CborValue val; + + if (cbor_value_get_type(arr) != CborMapType) { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + /* Fetch the key handle. */ + ret = cbor_value_map_find_value(arr, "id", &val); + check_ret(ret); + + if (cbor_value_get_type(&val) != CborByteStringType) { + return CTAP2_ERR_MISSING_PARAMETER; + } + + buflen = sizeof(*cred); + ret = cbor_value_copy_byte_string(&val, (uint8_t*)cred, &buflen, NULL); + + if (buflen < sizeof(*cred)) { + /* Not enough bytes to be a credential that we've generated. Skip it. */ + *cred_valid_out = false; + return 0; + } + check_ret(ret); + + /* Now check the "type" field. */ + ret = cbor_value_map_find_value(arr, "type", &val); + check_ret(ret); + + if (cbor_value_get_type(&val) != CborTextStringType) { + *cred_valid_out = false; + return CTAP2_ERR_MISSING_PARAMETER; + } + + buflen = sizeof(type); + ret = cbor_value_copy_text_string(&val, type, &buflen, NULL); + if (ret == CborErrorOutOfMemory) { + /* + * The type string is too big, so type type of the key + * is not something we know about. + */ + *cred_valid_out = false; + return 0; + } else { + check_ret(ret); + } + + if (strncmp(type, "public-key", 11) != 0) { + /* Not a keytype we know. */ + *cred_valid_out = false; + return 0; + } + *cred_valid_out = true; + return 0; +} + +/** + * Parses the list of allowed credentials into GA->creds. + * Updates GA->creds and GA->cred_len. + * @return CTAP status code (0 is success). + */ +static uint8_t parse_allow_list(ctap_get_assertion_req_t* GA, CborValue * it) +{ + CborValue arr; + size_t len; + int ret; + unsigned int i; + + if (cbor_value_get_type(it) != CborArrayType) + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + ret = cbor_value_enter_container(it,&arr); + check_ret(ret); + + ret = cbor_value_get_array_length(it, &len); + check_ret(ret); + + GA->cred_len = 0; + + for (i = 0; i < len; i++) { + if (GA->cred_len >= CTAP_CREDENTIAL_LIST_MAX_SIZE) { + return CTAP2_ERR_TOO_MANY_ELEMENTS; + } + + /* Check if this is a credential we should consider. */ + bool cred_valid = false; + u2f_keyhandle_t* cred = &GA->creds[GA->cred_len]; + ret = ctap_parse_credential_descriptor(&arr, cred, &cred_valid); + + check_retr(ret); + if (cred_valid) { + GA->cred_len += 1; + } + + ret = cbor_value_advance(&arr); + check_ret(ret); + } + return 0; +} + +uint8_t ctap_parse_get_assertion(ctap_get_assertion_req_t * GA, const in_buffer_t* in_buffer) +{ + int ret; + int key; + size_t map_length; + CborParser parser; + CborValue it,map; + + memset(GA, 0, sizeof(ctap_get_assertion_req_t)); + GA->up = 0xff; + + ret = cbor_parser_init(in_buffer->data, in_buffer->len, CborValidateCanonicalFormat, &parser, &it); + check_ret(ret); + + CborType type = cbor_value_get_type(&it); + if (type != CborMapType) + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + ret = cbor_value_enter_container(&it,&map); + check_ret(ret); + + ret = cbor_value_get_map_length(&it, &map_length); + check_ret(ret); + + + for (size_t i = 0; i < map_length; i++) { + type = cbor_value_get_type(&map); + if (type != CborIntegerType) + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + ret = cbor_value_get_int_checked(&map, &key); + check_ret(ret); + + ret = cbor_value_advance(&map); + check_ret(ret); + ret = 0; + + switch(key) + { + + case GET_ASSERTION_TAG_CLIENT_DATA_HASH: + + ret = _parse_fixed_byte_string(&map, GA->client_data_hash, CLIENT_DATA_HASH_SIZE); + check_retr(ret); + GA->client_data_hash_present = 1; + + break; + case GET_ASSERTION_TAG_RPID: + + ret = _parse_rp_id(&GA->rp, &map); + + break; + case GET_ASSERTION_TAG_ALLOW_LIST: + ret = parse_allow_list(GA, &map); + check_ret(ret); + GA->allowListPresent = 1; + + break; + case GET_ASSERTION_TAG_EXTENSIONS: + ret = ctap_parse_extensions(&map, &GA->extensions); + check_retr(ret); + break; + + case GET_ASSERTION_TAG_OPTIONS: + ret = _parse_options(&map, &GA->rk, &GA->uv, &GA->up); + check_retr(ret); + break; + case GET_ASSERTION_TAG_PIN_AUTH: { + + size_t pinSize; + if (cbor_value_get_type(&map) == CborByteStringType && + cbor_value_get_string_length(&map, &pinSize) == CborNoError && + pinSize == 0) + { + GA->pin_auth_empty = 1; + break; + } + + ret = _parse_fixed_byte_string(&map, GA->pin_auth, 16); + if (CTAP1_ERR_INVALID_LENGTH != ret) // damn microsoft + { + check_retr(ret); + + } + else + { + ret = 0; + } + + check_retr(ret); + GA->pin_auth_present = 1; + + break; + } + case GET_ASSERTION_TAG_PIN_PROTOCOL: + if (cbor_value_get_type(&map) == CborIntegerType) + { + ret = cbor_value_get_int_checked(&map, &GA->pin_protocol); + check_ret(ret); + } + else + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + break; + + default: + Abort("ctap_parse_get_assertion: bad key."); + } + if (ret != 0) + { + return ret; + } + + cbor_value_advance(&map); + check_ret(ret); + } + + + return 0; +} + + diff --git a/src/fido2/ctap_parse.h b/src/fido2/ctap_parse.h new file mode 100644 index 000000000..707ead43e --- /dev/null +++ b/src/fido2/ctap_parse.h @@ -0,0 +1,25 @@ +// Copyright 2019 SoloKeys Developers +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. +#ifndef _CTAP_PARSE_H +#define _CTAP_PARSE_H + +#include "ctap_errors.h" + +#define check_ret(r) \ + do { \ + if ((r) != CborNoError) { \ + return CTAP2_ERR_CBOR_PARSING; \ + } \ + } while(0); + +const char* cbor_value_get_type_string(const CborValue *value); + +uint8_t ctap_parse_make_credential(ctap_make_credential_req_t* MC, CborEncoder * encoder, const in_buffer_t* in_buffer); +uint8_t ctap_parse_get_assertion(ctap_get_assertion_req_t* GA, const in_buffer_t* in_buffer); +uint8_t ctap_parse_credential_descriptor(CborValue * arr, u2f_keyhandle_t* cred, bool* cred_valid_out); + +#endif diff --git a/src/fido2/ctaphid.h b/src/fido2/ctaphid.h new file mode 100644 index 000000000..4c707c778 --- /dev/null +++ b/src/fido2/ctaphid.h @@ -0,0 +1,111 @@ +// Copyright 2019 SoloKeys Developers +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. +#ifndef _CTAPHID_H_H +#define _CTAPHID_H_H + +#include "device.h" +#include "ctap_errors.h" + +#define TYPE_INIT 0x80 +#define TYPE_CONT 0x00 + +#define CTAPHID_PING (TYPE_INIT | 0x01) +#define CTAPHID_MSG (TYPE_INIT | 0x03) +#define CTAPHID_LOCK (TYPE_INIT | 0x04) +#define CTAPHID_INIT (TYPE_INIT | 0x06) +#define CTAPHID_WINK (TYPE_INIT | 0x08) +#define CTAPHID_CBOR (TYPE_INIT | 0x10) +#define CTAPHID_CANCEL (TYPE_INIT | 0x11) +#define CTAPHID_ERROR (TYPE_INIT | 0x3f) +#define CTAPHID_KEEPALIVE (TYPE_INIT | 0x3b) + +// Custom commands between 0x40-0x7f +#define CTAPHID_BOOT (TYPE_INIT | 0x50) +#define CTAPHID_ENTERBOOT (TYPE_INIT | 0x51) +#define CTAPHID_ENTERSTBOOT (TYPE_INIT | 0x52) +#define CTAPHID_GETRNG (TYPE_INIT | 0x60) +#define CTAPHID_GETVERSION (TYPE_INIT | 0x61) +#define CTAPHID_LOADKEY (TYPE_INIT | 0x62) +// reserved for debug, not implemented except for HACKER and DEBUG_LEVEl > 0 +#define CTAPHID_PROBE (TYPE_INIT | 0x70) + + #define ERR_INVALID_CMD 0x01 + #define ERR_INVALID_PAR 0x02 + #define ERR_INVALID_SEQ 0x04 + #define ERR_MSG_TIMEOUT 0x05 + #define ERR_CHANNEL_BUSY 0x06 + +#define CTAPHID_PROTOCOL_VERSION 2 + +#define CTAPHID_STATUS_IDLE 0 +#define CTAPHID_STATUS_PROCESSING 1 +#define CTAPHID_STATUS_UPNEEDED 2 + +#define CTAPHID_INIT_PAYLOAD_SIZE (HID_MESSAGE_SIZE-7) +#define CTAPHID_CONT_PAYLOAD_SIZE (HID_MESSAGE_SIZE-5) + +#define CTAPHID_BROADCAST_CID 0xffffffff + +#define CTAPHID_BUFFER_SIZE 7609 + +#define CAPABILITY_WINK 0x01 +#define CAPABILITY_LOCK 0x02 +#define CAPABILITY_CBOR 0x04 +#define CAPABILITY_NMSG 0x08 + +#define CTAP_CAPABILITIES (CAPABILITY_WINK | CAPABILITY_CBOR) + +#define HID_MESSAGE_SIZE 64 + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpacked" +#pragma GCC diagnostic ignored "-Wattributes" + +typedef struct +{ + uint32_t cid; + union{ + struct{ + uint8_t cmd; + uint8_t bcnth; + uint8_t bcntl; + uint8_t payload[CTAPHID_INIT_PAYLOAD_SIZE]; + } init; + struct{ + uint8_t seq; + uint8_t payload[CTAPHID_CONT_PAYLOAD_SIZE]; + } cont; + } pkt; +} CTAPHID_PACKET; + + +typedef struct +{ + uint8_t nonce[8]; + uint32_t cid; + uint8_t protocol_version; + uint8_t version_major; + uint8_t version_minor; + uint8_t build_version; + uint8_t capabilities; +} __attribute__((packed)) CTAPHID_INIT_RESPONSE; + +#pragma GCC diagnostic pop + + +void ctaphid_init(void); + +uint8_t ctaphid_handle_packet(uint8_t * pkt_raw); + +void ctaphid_check_timeouts(void); + +void ctaphid_update_status(int8_t status); + + +#define ctaphid_packet_len(pkt) ((uint16_t)((pkt)->pkt.init.bcnth << 8) | ((pkt)->pkt.init.bcntl)) + +#endif diff --git a/src/fido2/device.c b/src/fido2/device.c new file mode 100644 index 000000000..aa335f1c7 --- /dev/null +++ b/src/fido2/device.c @@ -0,0 +1,35 @@ +#include "device.h" + +#include +#include +#include + +/* + * Get the AAGUID (identifier of the type of device authenticating). + */ +void device_read_aaguid(uint8_t * dst) { + /* + * Hack: + * For now, return the AAGUID of a YubiKey 5 (USB-A, No NFC) - ee882879-721c-4913-9775-3dfcce97072a + * See https://support.yubico.com/support/solutions/articles/15000028710-yubikey-hardware-fido2-aaguids + */ + const char yubikey_aaguid[16] = {0xee, 0x88, 0x28, 0x79, 0x72, 0x1c, 0x49, 0x13, 0x97, 0x75, 0x3d, 0xfc, 0xce, 0x97, 0x07, 0x2a}; + memcpy(dst, yubikey_aaguid, 16); +} + +int ctap_generate_rng(uint8_t* dst, size_t num) { + /* Generate bytes in chunks of 32 bytes into the destination buffer. */ + size_t n_32bytes_chunks = num / 32; + for (size_t i = 0; i < n_32bytes_chunks; ++i) { + random_32_bytes(dst + i * 32); + } + /* Generate the last N bytes as needed. */ + int bytes_missing = num % 32; + if (bytes_missing) { + int final_word_offset = num - bytes_missing; + uint8_t last_bytes[32]; + random_32_bytes(last_bytes); + memcpy(dst + final_word_offset, last_bytes, bytes_missing); + } + return 1; +} diff --git a/src/fido2/device.h b/src/fido2/device.h new file mode 100644 index 000000000..3197abd9a --- /dev/null +++ b/src/fido2/device.h @@ -0,0 +1,224 @@ +// Copyright 2019 SoloKeys Developers +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. +#ifndef _DEVICE_H +#define _DEVICE_H + +#include "storage.h" + +/** Return a millisecond timestamp. Does not need to be synchronized to anything. + * *Optional* to compile, but will not calculate delays correctly without a correct implementation. +*/ +uint32_t millis(void); + + +/** Called by HIDUSB layer to write bytes to the USB HID interface endpoint. + * Will write 64 bytes at a time. + * + * @param msg Pointer to a 64 byte buffer containing a payload to be sent via USB HID. + * + * **Required** to compile and work for FIDO application. +*/ +void usbhid_send(uint8_t * msg); + + +/** Reboot / power reset the device. + * **Optional** this is not used for FIDO2, and simply won't do anything if not implemented. +*/ +void device_reboot(void); + +/** Read AuthenticatorState from nonvolatile memory. + * @param s pointer to AuthenticatorState buffer to be overwritten with contents from NV memory. + * @return 0 - state stored is NOT initialized. + * 1 - state stored is initialized. + * + * *Optional* this is required to make persistant updates to FIDO2 State (PIN and device master secret). + * Without it, changes simply won't be persistant. +*/ +int authenticator_read_state(AuthenticatorState * s); + +/** Store changes in the authenticator state to nonvolatile memory. + * @param s pointer to valid Authenticator state to write to NV memory. + * + * *Optional* this is required to make persistant updates to FIDO2 State (PIN and device master secret). + * Without it, changes simply won't be persistant. + */ +void authenticator_write_state(AuthenticatorState * s); + +// sets status that's uses for sending status updates ~100ms. +// A timer should be set up to call `ctaphid_update_status` + +/** Updates status of the status of the FIDO2 layer application, which + * can be used for polling updates in the USBHID layer. + * + * @param status is one of the following, which can be used appropriately by USB HID layer. + #define CTAPHID_STATUS_IDLE 0 + #define CTAPHID_STATUS_PROCESSING 1 + #define CTAPHID_STATUS_UPNEEDED 2 + * + * *Optional* to compile and run, but will be required to be used for proper FIDO2 operation with some platforms. +*/ +void device_set_status(uint32_t status); + +/** Returns true if button is currently pressed. Debouncing does not need to be handled. Should not block. + * @return 1 if button is currently pressed. + * + * *Optional* to compile and run, but just returns one by default. +*/ +int device_is_button_pressed(void); + +// +// Return 2 for disabled, 1 for user is present, 0 user not present, -1 if cancel is requested. +/** Test for user presence. + * Perform test that user is present. Returns status on user presence. This is used by FIDO and U2F layer + * to check if an operation should continue, or if the UP flag should be set. + * + * @param delay number of milliseconds to delay waiting for user before timeout. + * + * @return 2 - User presence is disabled. Operation should continue, but UP flag not set. + * 1 - User presence confirmed. Operation should continue, and UP flag is set. + * 0 - User presence is not confirmed. Operation should be denied. + * -1 - Operation was canceled. Do not continue, reset transaction state. + * + * *Optional*, the default implementation will return 1, unless a FIDO2 operation calls for no UP, where this will then return 2. +*/ +int ctap_user_presence_test(const char* title, const char* prompt, uint32_t delay); + +/** Disable the next user presence test. This is called by FIDO2 layer when a transaction + * requests UP to be disabled. The next call to ctap_user_presence_test should return 2, + * and then UP should be enabled again. + * + * @param request_active indicate to activate (true) or disable (false) UP. + * + * *Optional*, the default implementation will provide expected behaviour with the default ctap_user_presence_test(...). +*/ +void device_disable_up(bool request_active); + +/** Generate random numbers. Random numbers should be good enough quality for + * cryptographic use. + * + * @param dst the buffer to write into. + * @param num the number of bytes to generate and write to dst. + * + * @return 1 if successful, or else the RNG failed. + * + * *Optional*, if not implemented, the random numbers will be from rand() and an error will be logged. +*/ +int ctap_generate_rng(uint8_t * dst, size_t num); + +/** Increment an atomic (non-volatile) counter and return the value. + * + * @param amount a non-zero amount to increment the counter by. + * + * *Optional*, if not implemented, the counter will not be persistant. +*/ +uint32_t ctap_atomic_count(uint32_t amount); + +/** Delete all resident keys. + * + * *Optional*, if not implemented, operates on non-persistant RK's. +*/ +void ctap_reset_rk(void); + +/** Return the maximum amount of resident keys that can be stored. + * @return max number of resident keys that can be stored, including already stored RK's. + * + * *Optional*, if not implemented, returns 50. +*/ +uint32_t ctap_rk_size(void); + +/** Store a resident key into an index between [ 0, ctap_rk_size() ). + * Storage should be in non-volatile memory. + * + * @param index between RK index range. + * @param rk pointer to valid rk structure that should be written to NV memory. + * + * *Optional*, if not implemented, operates on non-persistant RK's. +*/ +void ctap_store_rk(int index, ctap_resident_key_t* rk); + +/** Read a resident key from an index into memory + * @param index to read resident key from. + * @param rk pointer to resident key structure to write into with RK. + * + * *Optional*, if not implemented, operates on non-persistant RK's. +*/ +void ctap_load_rk(int index, ctap_resident_key_t* rk); + +/** Overwrite the RK located in index with a new RK. + * @param index to write resident key to. + * @param rk pointer to valid rk structure that should be written to NV memory, and replace existing RK there. + * + * *Optional*, if not implemented, operates on non-persistant RK's. +*/ +void ctap_overwrite_rk(int index, ctap_resident_key_t* rk); + + +/** Called by HID layer to indicate that a wink behavior should be performed. + * Should not block, and the wink behavior should occur in parallel to FIDO operations. + * + * *Optional*. +*/ +void device_wink(void); + +typedef enum { + DEVICE_LOW_POWER_IDLE = 0, + DEVICE_LOW_POWER_FAST = 1, + DEVICE_FAST = 2, +} DEVICE_CLOCK_RATE; + +/** + * Set the clock rate for the device. This gets called only when the device is running in NFC mode. + * Before Register and authenticate operations, the clock rate will be set to (1), and otherwise back to (0). + * @param param + 0: Lowest clock rate for NFC. + 1: fastest clock rate supported at a low power setting for NFC FIDO. + 2: fastest clock rate. Generally for USB interface. +* *Optional*, by default nothing happens. +*/ +void device_set_clock_rate(DEVICE_CLOCK_RATE param); + +// Returns NFC_IS_NA, NFC_IS_ACTIVE, or NFC_IS_AVAILABLE +#define NFC_IS_NA 0 +#define NFC_IS_ACTIVE 1 +#define NFC_IS_AVAILABLE 2 + +/** Returns NFC status of the device. + * @return 0 - NFC is not available. + * 1 - NFC is active, and is powering the chip for a transaction. + * 2 - NFC is available, but not currently being used. +*/ +int device_is_nfc(void); + + +/** Return pointer to attestation key. + * @return pointer to attestation private key, raw encoded. For P256, this is 32 bytes. +*/ +uint8_t * device_get_attestation_key(void); + +/** Read the device's attestation certificate into buffer @dst. + * @param dst the destination to write the certificate. + * + * The size of the certificate can be retrieved using `device_attestation_cert_der_get_size()`. +*/ +void device_attestation_read_cert_der(uint8_t * dst); + +/** Returns the size in bytes of attestation_cert_der. + * @return number of bytes in attestation_cert_der, not including any C string null byte. +*/ +uint16_t device_attestation_cert_der_get_size(void); + +/** Read the device's 16 byte AAGUID into a buffer. + * @param dst buffer to write 16 byte AAGUID into. + * */ +void device_read_aaguid(uint8_t * dst); + +/** + * @return whether the device has been configured + * properly and can verify the user. + */ +bool device_is_uv_initialized(void); +#endif diff --git a/src/fido2/extensions.c b/src/fido2/extensions.c new file mode 100644 index 000000000..894931eca --- /dev/null +++ b/src/fido2/extensions.c @@ -0,0 +1,175 @@ +// Copyright 2019 SoloKeys Developers +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +#include +#include +#include "extensions.h" +#if 0 +#include "u2f.h" +#include "ctap.h" +#endif +#include "wallet.h" +#if 0 +#include "solo.h" +#include "device.h" + +#include "log.h" + +#define htonl(x) (((x & 0xff) << 24) | ((x & 0xff00) << 8) \ + | ((x & 0xff0000) >> 8) | ((x & 0xff000000) >> 24) ) +#endif + +int is_extension_request(uint8_t * kh, int len) +{ + wallet_request * req = (wallet_request *) kh; + + if (len < WALLET_MIN_LENGTH) + return 0; + + return memcmp(req->tag, WALLET_TAG, sizeof(WALLET_TAG)-1) == 0; +} + +#if 0 + +int extension_needs_atomic_count(uint8_t klen, uint8_t * keyh) +{ + return ((wallet_request *) keyh)->operation == WalletRegister + || ((wallet_request *) keyh)->operation == WalletSign; +} + +static uint8_t * output_buffer_ptr; +uint8_t output_buffer_offset; +uint8_t output_buffer_size; + +void extension_writeback_init(uint8_t * buffer, uint8_t size) +{ + output_buffer_ptr = buffer; + output_buffer_offset = 0; + output_buffer_size = size; +} + +void extension_writeback(uint8_t * buf, uint8_t size) +{ + if ((output_buffer_offset + size) > output_buffer_size) + { + return; + } + memmove(output_buffer_ptr + output_buffer_offset, buf, size); + output_buffer_offset += size; +} + + +int16_t bridge_u2f_to_extensions(uint8_t * _chal, uint8_t * _appid, uint8_t klen, uint8_t * keyh) +{ + int8_t ret = 0; + uint32_t count; + uint8_t up = 1; + uint8_t sig[72]; + if (extension_needs_atomic_count(klen, keyh)) + { + count = htonl(ctap_atomic_count(0)); + } + else + { + count = htonl(10); + } + + u2f_response_writeback(&up,1); + u2f_response_writeback((uint8_t *)&count,4); + u2f_response_writeback((uint8_t *)&ret,1); +#ifdef IS_BOOTLOADER + ret = bootloader_bridge(klen, keyh); +#else + ret = bridge_u2f_to_solo(sig, keyh, klen); + u2f_response_writeback(sig,72); +#endif + + if (ret != 0) + { + u2f_reset_response(); + u2f_response_writeback(&up,1); + u2f_response_writeback((uint8_t *)&count,4); + + memset(sig,0,sizeof(sig)); + sig[0] = ret; + u2f_response_writeback(sig,72); + } + + return U2F_SW_NO_ERROR; +} + +// Returns 1 if this is a extension request. +// Else 0 if nothing is done. +int16_t extend_fido2(CredentialId * credid, uint8_t * output) +{ + if (is_extension_request((uint8_t*)credid, sizeof(CredentialId))) + { + printf1(TAG_EXT,"IS EXT REQ\r\n"); + output[0] = bridge_u2f_to_solo(output+1, (uint8_t*)credid, sizeof(CredentialId)); + return 1; + } + else + { + return 0; + } +} + +int16_t extend_u2f(APDU_HEADER * req, uint8_t * payload, uint32_t len) +{ + + struct u2f_authenticate_request * auth = (struct u2f_authenticate_request *) payload; + uint16_t rcode; + + if (req->ins == U2F_AUTHENTICATE) + { + if (req->p1 == U2F_AUTHENTICATE_CHECK) + { + + if (is_extension_request((uint8_t *) &auth->kh, auth->khl)) // Pin requests + { + rcode = U2F_SW_CONDITIONS_NOT_SATISFIED; + } + else + { + rcode = U2F_SW_WRONG_DATA; + } + printf1(TAG_EXT,"Ignoring U2F check request\n"); + dump_hex1(TAG_EXT, (uint8_t *) &auth->kh, auth->khl); + goto end; + } + else + { + if ( ! is_extension_request((uint8_t *) &auth->kh, auth->khl)) // Pin requests + { + rcode = U2F_SW_WRONG_DATA; + printf1(TAG_EXT, "Ignoring U2F auth request\n"); + dump_hex1(TAG_EXT, (uint8_t *) &auth->kh, auth->khl); + goto end; + } + rcode = bridge_u2f_to_extensions(auth->chal, auth->app, auth->khl, (uint8_t*)&auth->kh); + } + } + else if (req->ins == U2F_VERSION) + { + printf1(TAG_EXT, "U2F_VERSION\n"); + if (len) + { + rcode = U2F_SW_WRONG_LENGTH; + } + else + { + rcode = u2f_version(); + } + } + else + { + rcode = U2F_SW_INS_NOT_SUPPORTED; + } +end: + return rcode; +} +#endif diff --git a/src/fido2/extensions.h b/src/fido2/extensions.h new file mode 100644 index 000000000..66898c4a2 --- /dev/null +++ b/src/fido2/extensions.h @@ -0,0 +1,33 @@ +// Copyright 2019 SoloKeys Developers +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. +#ifndef EXTENSIONS_H_ +#define EXTENSIONS_H_ +#include "u2f.h" +//#include "apdu.h" + +#if 0 +int16_t bridge_u2f_to_extensions(uint8_t * chal, uint8_t * appid, uint8_t klen, uint8_t * keyh); +#endif + +// return 1 if request is a wallet request +int is_extension_request(uint8_t * req, int len); + +#if 0 +int16_t extend_u2f(APDU_HEADER * req, uint8_t * payload, uint32_t len); + +int16_t extend_fido2(CredentialId * credid, uint8_t * output); + +int bootloader_bridge(int klen, uint8_t * keyh); + +int is_extension_request(uint8_t * kh, int len); + + +void extension_writeback_init(uint8_t * buffer, uint8_t size); +void extension_writeback(uint8_t * buf, uint8_t size); +#endif + +#endif /* EXTENSIONS_H_ */ diff --git a/src/fido2/fido2_keys.h b/src/fido2/fido2_keys.h new file mode 100644 index 000000000..1e2bef02c --- /dev/null +++ b/src/fido2/fido2_keys.h @@ -0,0 +1,11 @@ +#ifndef __FIDO2_KEYS_H_INCLUDED__ +#define __FIDO2_KEYS_H_INCLUDED__ + +#include +#include + +extern const uint8_t FIDO2_ATT_PRIV_KEY[]; +extern const uint8_t FIDO2_ATT_CERT[]; +extern const size_t FIDO2_ATT_CERT_SIZE; + +#endif // __FIDO2_KEYS_H_INCLUDED__ diff --git a/src/fido2/storage.h b/src/fido2/storage.h new file mode 100644 index 000000000..271e23406 --- /dev/null +++ b/src/fido2/storage.h @@ -0,0 +1,70 @@ +// Copyright 2019 SoloKeys Developers +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. +#ifndef _STORAGE_H +#define _STORAGE_H + +#include "ctap.h" + +#define KEY_SPACE_BYTES 128 +#define MAX_KEYS (1) +#define PIN_SALT_LEN (32) +#define STATE_VERSION (1) + + +#define BACKUP_MARKER 0x5A +#define INITIALIZED_MARKER 0xA5 + +#define ERR_NO_KEY_SPACE (-1) +#define ERR_KEY_SPACE_TAKEN (-2) +#define ERR_KEY_SPACE_EMPTY (-2) + +typedef struct +{ + // Pin information + uint8_t is_initialized; + uint8_t is_pin_set; + uint8_t pin_code[NEW_PIN_ENC_MIN_SIZE]; + int pin_code_length; + int8_t remaining_tries; + + uint16_t rk_stored; + + uint16_t key_lens[MAX_KEYS]; + uint8_t key_space[KEY_SPACE_BYTES]; +} AuthenticatorState_0xFF; + + +typedef struct +{ + // Pin information + uint8_t is_initialized; + uint8_t is_pin_set; + uint8_t PIN_CODE_HASH[32]; + uint8_t PIN_SALT[PIN_SALT_LEN]; + int _reserved; + int8_t remaining_tries; + + uint16_t rk_stored; + + uint16_t key_lens[MAX_KEYS]; + uint8_t key_space[KEY_SPACE_BYTES]; + uint8_t data_version; +} AuthenticatorState_0x01; + +typedef AuthenticatorState_0x01 AuthenticatorState; + + +typedef struct +{ + uint32_t addr; + uint8_t * filename; + uint32_t count; +} AuthenticatorCounter; + +extern AuthenticatorState STATE; + +#endif diff --git a/src/fido2/wallet.h b/src/fido2/wallet.h new file mode 100644 index 000000000..830629e23 --- /dev/null +++ b/src/fido2/wallet.h @@ -0,0 +1,97 @@ +// Copyright 2019 SoloKeys Developers +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. +#ifndef WALLET_H_ +#define WALLET_H_ + +#include + +#define WALLET_MAX_BUFFER (32 + 255) + +// Sign request +// op: 0x10 +// authType: 0x00 //sign? +// reserved: 0x00 // mbedtls signature alg identifier +// pinAuth: data[16] +// challenge-length: 1-255 +// challenge: data[1-255] +// keyID-length: 1-255 +// keyID: data[1-255] + +// Resp: normal U2F auth response + +// Register request +// op: 0x11 +// formatType: 0x00 //sign? [0x00: WIF, 0x01: raw] +// keyType: 0x03 // mbedtls signature alg identifier +// key-length: 1-255 +// key: data[1-255] + + +// Resp: modded U2F auth response + +// PIN request +// op: 0x12 +// subcmd: 0x00 // Same as CTAP pin subcommands +// reserved: 0x03 // mbedtls signature alg identifier +// publickey: data[64] +// OR +// pinAuth data[64] +// OR +// pinHashEnc data[64] +// OR +// newPinEnc data[64] + +// key: data[1-255] +// keyID-length: 1-255 +// keyID: data[1-255] + +// Resp: modded U2F auth response +// Returns public key OR pinAuth + +// Only response to this challenge to prevent interference +#define WALLET_TAG "\x8C\x27\x90\xf6" + +#define WALLET_MIN_LENGTH (4 + 4 + 16) + +#define WALLET_VERSION "WALLET_V1.0" + +#define MAX_CHALLENGE_SIZE 229 +#define MAX_KEYID_SIZE 228 + +#define MAX_PAYLOAD_SIZE (255 - 16 - 4 - 4) + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpacked" +typedef struct +{ + uint8_t operation; + uint8_t p1; + uint8_t p2; + uint8_t numArgs; + uint8_t tag[4]; + uint8_t pinAuth[16]; + uint8_t payload[MAX_PAYLOAD_SIZE]; +}__attribute__((packed)) wallet_request; + +#pragma GCC diagnostic pop + +typedef enum +{ + WalletSign = 0x10, + WalletRegister = 0x11, + WalletPin = 0x12, + WalletReset= 0x13, + WalletVersion= 0x14, + WalletRng = 0x15, +} WalletOperation; + + +int16_t bridge_to_wallet(uint8_t * keyh, uint8_t klen); + +void wallet_init(void); + +#endif /* WALLET_H_ */ diff --git a/src/keystore.c b/src/keystore.c index 28ad12f3b..518737750 100644 --- a/src/keystore.c +++ b/src/keystore.c @@ -94,7 +94,7 @@ bool keystore_copy_seed(uint8_t* seed_out, uint32_t* length_out) } /** - * @return the pointer ot the static bip39 seed on success. returns NULL if the + * @return the pointer to the static bip39 seed on success. returns NULL if the * keystore is locked. */ static uint8_t* _get_bip39_seed(void) diff --git a/src/memory/memory.c b/src/memory/memory.c index 3c18a695f..d675ed172 100644 --- a/src/memory/memory.c +++ b/src/memory/memory.c @@ -810,3 +810,28 @@ bool memory_multisig_get_by_hash(const uint8_t* hash, char* name_out) } return false; } + +static ctap_resident_key_t rks[MEMORY_CTAP_RESIDENT_KEYS_MAX]; +static int rks_valid[MEMORY_CTAP_RESIDENT_KEYS_MAX] = {0}; +bool memory_get_ctap_resident_key(int key_idx, ctap_resident_key_t* key_out) +{ + /** TODO: simo: implement */ + memcpy(key_out, &rks[key_idx], sizeof(*rks)); + if (rks_valid[key_idx]) { + key_out->valid = CTAP_RESIDENT_KEY_VALID; + } else { + key_out->valid = CTAP_RESIDENT_KEY_INVALID; + } + return true; +} + +void memory_store_ctap_resident_key(int store_location, const ctap_resident_key_t* rk_to_store) +{ + /** TODO: simo: implement */ + memcpy(&rks[store_location], rk_to_store, sizeof(*rks)); + if (rk_to_store->valid == CTAP_RESIDENT_KEY_VALID) { + rks_valid[store_location] = true; + } else { + rks_valid[store_location] = false; + } +} diff --git a/src/memory/memory.h b/src/memory/memory.h index 94e809909..41babef09 100644 --- a/src/memory/memory.h +++ b/src/memory/memory.h @@ -21,6 +21,8 @@ #include "compiler_util.h" #include "util.h" +#include + // Including null terminator. #define MEMORY_MULTISIG_NAME_MAX_LEN (31) @@ -63,6 +65,15 @@ USE_RESULT bool memory_cleanup_smarteeprom(void); // memory.c) #define MEMORY_DEVICE_NAME_MAX_LEN (64) +/* Each memory chunk can contain up to 23 resident keys. */ +#define MEMORY_CTAP_RESIDENT_KEYS_PER_CHUNK (23) +#define MEMORY_CTAP_RESIDENT_KEYS_CHUNKS (2) +#if 0 +#define MEMORY_CTAP_RESIDENT_KEYS_MAX (MEMORY_CTAP_RESIDENT_KEYS_CHUNKS * MEMORY_CTAP_RESIDENT_KEYS_PER_CHUNK) +#else +#define MEMORY_CTAP_RESIDENT_KEYS_MAX (5) +#endif + // set device name. name is an utf8-encoded string, and null terminated. The max // size (including the null terminator) is MEMORY_DEVICE_NAME_MAX_LEN bytes. USE_RESULT bool memory_set_device_name(const char* name); @@ -248,4 +259,11 @@ USE_RESULT memory_result_t memory_multisig_set_by_hash(const uint8_t* hash, cons */ USE_RESULT bool memory_multisig_get_by_hash(const uint8_t* hash, char* name_out); +/* + * Loads the nth CTAP resident key from flash. + */ +USE_RESULT bool memory_get_ctap_resident_key(int key_idx, ctap_resident_key_t* key_out); + +void memory_store_ctap_resident_key(int store_location, const ctap_resident_key_t* rk_to_store); + #endif // _MEMORY_H_ diff --git a/src/u2f.c b/src/u2f.c index 83d4ce8c2..2d0229ad9 100644 --- a/src/u2f.c +++ b/src/u2f.c @@ -16,10 +16,13 @@ #include #include +#include +#include #include #include #include #include +#include #include #include #include @@ -478,7 +481,7 @@ static void _register_continue(const USB_APDU* apdu, Packet* out_packet) memcpy(response->keyHandleCertSig, mac, sizeof(mac)); memcpy(response->keyHandleCertSig + sizeof(mac), nonce, sizeof(nonce)); - memcpy(response->keyHandleCertSig + response->keyHandleLen, U2F_ATT_CERT, sizeof(U2F_ATT_CERT)); + memcpy(response->keyHandleCertSig + response->keyHandleLen, U2F_ATT_CERT, U2F_ATT_CERT_SIZE); // Add signature using attestation key sig_base.reserved = 0; @@ -495,9 +498,9 @@ static void _register_continue(const USB_APDU* apdu, Packet* out_packet) return; } - uint8_t* resp_sig = response->keyHandleCertSig + response->keyHandleLen + sizeof(U2F_ATT_CERT); + uint8_t* resp_sig = response->keyHandleCertSig + response->keyHandleLen + U2F_ATT_CERT_SIZE; int der_len = _sig_to_der(sig, resp_sig); - size_t kh_cert_sig_len = response->keyHandleLen + sizeof(U2F_ATT_CERT) + der_len; + size_t kh_cert_sig_len = response->keyHandleLen + U2F_ATT_CERT_SIZE + der_len; // Append success bytes memcpy(response->keyHandleCertSig + kh_cert_sig_len, "\x90\x00", 2); @@ -735,6 +738,16 @@ static void _cmd_wink(const Packet* in_packet) free(out_packet); } +static void _cmd_cancel(const Packet* in_packet) +{ + (void)in_packet; + //screen_print_debug("CANCEL", 500); + /* + * U2FHID_CANCEL messages should abort the current transaction, + * but no response should be given. + */ +} + /** * Synchronize a channel and optionally requests a unique 32-bit channel identifier (CID) * that can be used by the requesting application during its lifetime. @@ -776,7 +789,7 @@ static void _cmd_init(const Packet* in_packet) response.versionMajor = DIGITAL_BITBOX_VERSION_MAJOR; response.versionMinor = DIGITAL_BITBOX_VERSION_MINOR; response.versionBuild = DIGITAL_BITBOX_VERSION_PATCH; - response.capFlags = U2FHID_CAPFLAG_WINK; + response.capFlags = U2FHID_CAPFLAG_WINK | U2FHID_CAPFLAG_CBOR; util_zero(out_packet->data_addr, sizeof(out_packet->data_addr)); memcpy(out_packet->data_addr, &response, sizeof(response)); usb_processing_send_packet(usb_processing_u2f(), out_packet); @@ -932,12 +945,54 @@ static void _cmd_msg(const Packet* in_packet) free(out_packet); } +static void _cmd_cbor(const Packet* in_packet) { + Packet* out_packet = util_malloc(sizeof(*out_packet)); + prepare_usb_packet(in_packet->cmd, in_packet->cid, out_packet); + + if (in_packet->len == 0) + { + printf("Error,invalid 0 length field for cbor packet\n"); + _error_hid(in_packet->cid, U2FHID_ERR_INVALID_LEN, out_packet); + usb_processing_send_packet(usb_processing_u2f(), out_packet); + free(out_packet); + return; + } + in_buffer_t request_buf = { + .data = in_packet->data_addr, + .len = in_packet->len + }; + buffer_t response_buf = { + .data = out_packet->data_addr + 1, + .len = 0, + .max_len = USB_DATA_MAX_LEN - 1 + }; + ctap_request_result_t result = ctap_request(&request_buf, &response_buf); + if (!result.request_completed) { + /* Don't send a response yet. */ + free(out_packet); + return; + } + if (result.status != CTAP1_ERR_SUCCESS) { + _error_hid(in_packet->cid, result.status, out_packet); + } else { + out_packet->data_addr[0] = result.status; + } + out_packet->len = response_buf.len + 1; + usb_processing_send_packet(usb_processing_u2f(), out_packet); + free(out_packet); +} + bool u2f_blocking_request_can_go_through(const Packet* in_packet) { if (!_state.locked) { Abort("USB stack thinks we're busy, but we're not."); } + /* U2F keepalives should always be let through */ + if (in_packet->cmd == U2FHID_KEEPALIVE || in_packet->cmd == U2FHID_CANCEL) { + return true; + } + if (!_state.allow_cmd_retries) { /* We're blocking every command, including retries of the last command. */ return false; @@ -1057,6 +1112,8 @@ void u2f_device_setup(void) {U2FHID_WINK, _cmd_wink}, {U2FHID_INIT, _cmd_init}, {U2FHID_MSG, _cmd_msg}, + {U2FHID_CBOR, _cmd_cbor}, + {U2FHID_CANCEL, _cmd_cancel}, }; usb_processing_register_cmds( usb_processing_u2f(), u2f_cmd_callbacks, sizeof(u2f_cmd_callbacks) / sizeof(CMD_Callback)); diff --git a/src/usb/class/usb_desc.h b/src/usb/class/usb_desc.h index 6b1f1c93e..f10a4da08 100644 --- a/src/usb/class/usb_desc.h +++ b/src/usb/class/usb_desc.h @@ -18,6 +18,15 @@ #include #include #ifndef TESTING + +/* + * The USB stack will define its own version of assert... + * With the same name. + */ +#ifdef assert +#undef assert +#endif + #include "usb_protocol.h" #include "usb_protocol_hid.h" #endif diff --git a/src/usb/u2f/u2f.h b/src/usb/u2f/u2f.h index ca57c94b6..7e4f79e97 100644 --- a/src/usb/u2f/u2f.h +++ b/src/usb/u2f/u2f.h @@ -16,7 +16,7 @@ // General constants /* Size of one of the point coordinates on the EC */ -#define U2F_EC_KEY_SIZE 32 +#define U2F_EC_KEY_SIZE (32) #define U2F_EC_POINT_SIZE ((U2F_EC_KEY_SIZE * 2) + 1) #define U2F_MAX_KH_SIZE 128 // Max size of key handle #define U2F_MAX_ATT_CERT_SIZE 1024 // Max size of attestation certificate diff --git a/src/usb/u2f/u2f_hid.h b/src/usb/u2f/u2f_hid.h index 6cf70b9d5..b6c306138 100644 --- a/src/usb/u2f/u2f_hid.h +++ b/src/usb/u2f/u2f_hid.h @@ -27,15 +27,18 @@ #define U2FHID_TRANS_TIMEOUT 3000 // Default message timeout in ms // U2FHID native commands -#define U2FHID_PING (U2FHID_TYPE_INIT | 0x01) // Echo data -#define U2FHID_MSG (U2FHID_TYPE_INIT | 0x03) // Send U2F message frame -#define U2FHID_LOCK (U2FHID_TYPE_INIT | 0x04) // Send lock channel command -#define U2FHID_INIT (U2FHID_TYPE_INIT | 0x06) // Channel initialization -#define U2FHID_WINK (U2FHID_TYPE_INIT | 0x08) // Send device identification wink -#define U2FHID_SYNC (U2FHID_TYPE_INIT | 0x3c) // Send sync command -#define U2FHID_ERROR (U2FHID_TYPE_INIT | 0x3f) // Error response +#define U2FHID_PING (U2FHID_TYPE_INIT | 0x01) // Echo data +#define U2FHID_MSG (U2FHID_TYPE_INIT | 0x03) // Send U2F message frame +#define U2FHID_LOCK (U2FHID_TYPE_INIT | 0x04) // Send lock channel command +#define U2FHID_INIT (U2FHID_TYPE_INIT | 0x06) // Channel initialization +#define U2FHID_WINK (U2FHID_TYPE_INIT | 0x08) // Send device identification wink +#define U2FHID_CBOR (U2FHID_TYPE_INIT | 0x10) +#define U2FHID_CANCEL (U2FHID_TYPE_INIT | 0x11) +#define U2FHID_KEEPALIVE (U2FHID_TYPE_INIT | 0x3b) +#define U2FHID_SYNC (U2FHID_TYPE_INIT | 0x3c) // Send sync command +#define U2FHID_ERROR (U2FHID_TYPE_INIT | 0x3f) // Error response #define U2FHID_VENDOR_FIRST (U2FHID_TYPE_INIT | 0x40) // First vendor defined command -#define U2FHID_VENDOR_LAST (U2FHID_TYPE_INIT | 0x7f) // Last vendor defined command +#define U2FHID_VENDOR_LAST (U2FHID_TYPE_INIT | 0x7f) // Last vendor defined command // U2FHID vendor defined commands #define U2FHID_HWW (U2FHID_VENDOR_FIRST + 0x01) // Hardware wallet command @@ -44,6 +47,7 @@ #define U2FHID_INIT_NONCE_SIZE 8 #define U2FHID_CAPFLAG_WINK 0x01 // Device supports WINK command #define U2FHID_CAPFLAG_LOCK 0x02 // Device supports LOCK command +#define U2FHID_CAPFLAG_CBOR 0x04 // Device supports CBOR (FIDO2) commands // Error codes; return as negatives #define U2FHID_ERR_NONE 0x00 diff --git a/src/usb/u2f/u2f_keys.c b/src/usb/u2f/u2f_keys.c new file mode 100644 index 000000000..3d0312f32 --- /dev/null +++ b/src/usb/u2f/u2f_keys.c @@ -0,0 +1,30 @@ +#include "u2f_keys.h" + +const uint8_t U2F_ATT_PRIV_KEY[] = { + 0x74, 0xa4, 0x04, 0x25, 0xcc, 0x7b, 0x8c, 0xcd, 0x48, 0x3d, 0xe2, 0xff, 0xd8, 0xc6, 0xa5, 0xc7, + 0xdb, 0x4e, 0x6f, 0x56, 0xe1, 0xdb, 0x9a, 0xcb, 0x5a, 0x43, 0xda, 0x12, 0x42, 0xa8, 0xf4, 0xcf}; + +const uint8_t U2F_ATT_CERT[] = { + 0x30, 0x82, 0x01, 0x28, 0x30, 0x81, 0xcf, 0x02, 0x09, 0x00, 0xb8, 0x9e, 0xe4, 0x7e, 0x99, 0x53, + 0x83, 0x34, 0x30, 0x09, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x01, 0x30, 0x1d, 0x31, + 0x1b, 0x30, 0x19, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x12, 0x44, 0x69, 0x67, 0x69, 0x74, 0x61, + 0x6c, 0x20, 0x42, 0x69, 0x74, 0x62, 0x6f, 0x78, 0x20, 0x55, 0x32, 0x46, 0x30, 0x1e, 0x17, 0x0d, + 0x31, 0x37, 0x30, 0x32, 0x31, 0x38, 0x31, 0x35, 0x34, 0x30, 0x30, 0x35, 0x5a, 0x17, 0x0d, 0x33, + 0x37, 0x30, 0x32, 0x31, 0x33, 0x31, 0x35, 0x34, 0x30, 0x30, 0x35, 0x5a, 0x30, 0x1d, 0x31, 0x1b, + 0x30, 0x19, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x12, 0x44, 0x69, 0x67, 0x69, 0x74, 0x61, 0x6c, + 0x20, 0x42, 0x69, 0x74, 0x62, 0x6f, 0x78, 0x20, 0x55, 0x32, 0x46, 0x30, 0x59, 0x30, 0x13, 0x06, + 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, + 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0x37, 0x71, 0xa1, 0xa7, 0x23, 0x7e, 0x69, 0x0a, 0x72, 0x77, + 0x17, 0x7d, 0x71, 0x60, 0x41, 0x2a, 0x55, 0x90, 0xfc, 0xb9, 0xd6, 0xbf, 0x93, 0x8a, 0x6e, 0x8e, + 0xa9, 0x3f, 0xe6, 0x17, 0xb4, 0x1e, 0xc0, 0xc7, 0x6a, 0xad, 0x35, 0xe8, 0x10, 0x89, 0x1f, 0x48, + 0xdf, 0x19, 0x35, 0xe1, 0xef, 0x74, 0x4f, 0xb7, 0xba, 0xf6, 0x6f, 0x5e, 0xad, 0x93, 0x01, 0xaf, + 0xf4, 0x57, 0x0d, 0x6b, 0x11, 0x18, 0x30, 0x09, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, + 0x01, 0x03, 0x49, 0x00, 0x30, 0x46, 0x02, 0x21, 0x00, 0xc1, 0x4e, 0x00, 0x66, 0x7a, 0x0f, 0xbf, + 0xd1, 0x1e, 0x1e, 0xa3, 0xca, 0xb1, 0x9e, 0x31, 0x3f, 0xb1, 0x6a, 0x04, 0xc3, 0xd9, 0x96, 0x58, + 0x71, 0xda, 0x0b, 0x14, 0x84, 0x94, 0x05, 0xdd, 0x28, 0x02, 0x21, 0x00, 0xe6, 0xff, 0x72, 0x7c, + 0xcc, 0xf5, 0x21, 0x63, 0x71, 0x78, 0x51, 0xa7, 0x16, 0x46, 0x10, 0xda, 0x63, 0xaa, 0xb9, 0x30, + 0xe9, 0xc1, 0x1b, 0xff, 0x27, 0xc9, 0xbf, 0x2f, 0x4b, 0x6f, 0xc2, 0x0d, + +}; + +const size_t U2F_ATT_CERT_SIZE = sizeof(U2F_ATT_CERT); diff --git a/src/usb/u2f/u2f_keys.h b/src/usb/u2f/u2f_keys.h index 65369130e..16b2e1449 100644 --- a/src/usb/u2f/u2f_keys.h +++ b/src/usb/u2f/u2f_keys.h @@ -2,32 +2,10 @@ #define __U2F_KEYS_H_INCLUDED__ #include +#include -const uint8_t U2F_ATT_PRIV_KEY[] = { - 0x74, 0xa4, 0x04, 0x25, 0xcc, 0x7b, 0x8c, 0xcd, 0x48, 0x3d, 0xe2, 0xff, 0xd8, 0xc6, 0xa5, 0xc7, - 0xdb, 0x4e, 0x6f, 0x56, 0xe1, 0xdb, 0x9a, 0xcb, 0x5a, 0x43, 0xda, 0x12, 0x42, 0xa8, 0xf4, 0xcf}; - -const uint8_t U2F_ATT_CERT[] = { - 0x30, 0x82, 0x01, 0x28, 0x30, 0x81, 0xcf, 0x02, 0x09, 0x00, 0xb8, 0x9e, 0xe4, 0x7e, 0x99, 0x53, - 0x83, 0x34, 0x30, 0x09, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x01, 0x30, 0x1d, 0x31, - 0x1b, 0x30, 0x19, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x12, 0x44, 0x69, 0x67, 0x69, 0x74, 0x61, - 0x6c, 0x20, 0x42, 0x69, 0x74, 0x62, 0x6f, 0x78, 0x20, 0x55, 0x32, 0x46, 0x30, 0x1e, 0x17, 0x0d, - 0x31, 0x37, 0x30, 0x32, 0x31, 0x38, 0x31, 0x35, 0x34, 0x30, 0x30, 0x35, 0x5a, 0x17, 0x0d, 0x33, - 0x37, 0x30, 0x32, 0x31, 0x33, 0x31, 0x35, 0x34, 0x30, 0x30, 0x35, 0x5a, 0x30, 0x1d, 0x31, 0x1b, - 0x30, 0x19, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x12, 0x44, 0x69, 0x67, 0x69, 0x74, 0x61, 0x6c, - 0x20, 0x42, 0x69, 0x74, 0x62, 0x6f, 0x78, 0x20, 0x55, 0x32, 0x46, 0x30, 0x59, 0x30, 0x13, 0x06, - 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, - 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0x37, 0x71, 0xa1, 0xa7, 0x23, 0x7e, 0x69, 0x0a, 0x72, 0x77, - 0x17, 0x7d, 0x71, 0x60, 0x41, 0x2a, 0x55, 0x90, 0xfc, 0xb9, 0xd6, 0xbf, 0x93, 0x8a, 0x6e, 0x8e, - 0xa9, 0x3f, 0xe6, 0x17, 0xb4, 0x1e, 0xc0, 0xc7, 0x6a, 0xad, 0x35, 0xe8, 0x10, 0x89, 0x1f, 0x48, - 0xdf, 0x19, 0x35, 0xe1, 0xef, 0x74, 0x4f, 0xb7, 0xba, 0xf6, 0x6f, 0x5e, 0xad, 0x93, 0x01, 0xaf, - 0xf4, 0x57, 0x0d, 0x6b, 0x11, 0x18, 0x30, 0x09, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, - 0x01, 0x03, 0x49, 0x00, 0x30, 0x46, 0x02, 0x21, 0x00, 0xc1, 0x4e, 0x00, 0x66, 0x7a, 0x0f, 0xbf, - 0xd1, 0x1e, 0x1e, 0xa3, 0xca, 0xb1, 0x9e, 0x31, 0x3f, 0xb1, 0x6a, 0x04, 0xc3, 0xd9, 0x96, 0x58, - 0x71, 0xda, 0x0b, 0x14, 0x84, 0x94, 0x05, 0xdd, 0x28, 0x02, 0x21, 0x00, 0xe6, 0xff, 0x72, 0x7c, - 0xcc, 0xf5, 0x21, 0x63, 0x71, 0x78, 0x51, 0xa7, 0x16, 0x46, 0x10, 0xda, 0x63, 0xaa, 0xb9, 0x30, - 0xe9, 0xc1, 0x1b, 0xff, 0x27, 0xc9, 0xbf, 0x2f, 0x4b, 0x6f, 0xc2, 0x0d, - -}; +extern const uint8_t U2F_ATT_PRIV_KEY[]; +extern const uint8_t U2F_ATT_CERT[]; +extern const size_t U2F_ATT_CERT_SIZE; #endif // __U2F_KEYS_H_INCLUDED__ diff --git a/src/workflow/confirm.c b/src/workflow/confirm.c index ad1455a27..0563aff7b 100644 --- a/src/workflow/confirm.c +++ b/src/workflow/confirm.c @@ -88,6 +88,12 @@ static void _workflow_confirm_cleanup(workflow_t* self) ui_screen_stack_pop(); ui_screen_stack_cleanup(); data_t* data = (data_t*)self->data; + if (data->body) { + util_zero(data->body, strlen(data->body)); + } + if (data->title) { + util_zero(data->title, strlen(data->title)); + } free(data->title); free(data->body); } diff --git a/src/workflow/select_ctap_credential.c b/src/workflow/select_ctap_credential.c new file mode 100644 index 000000000..be17318f8 --- /dev/null +++ b/src/workflow/select_ctap_credential.c @@ -0,0 +1,8 @@ +#include "select_ctap_credential.h" + +int workflow_select_ctap_credential(ctap_credential_display_list_t* credentials) { + /* TODO */ + /* Just select the first credential in the list for now. */ + (void)credentials; + return 0; +} diff --git a/src/workflow/select_ctap_credential.h b/src/workflow/select_ctap_credential.h new file mode 100644 index 000000000..500ebe1f3 --- /dev/null +++ b/src/workflow/select_ctap_credential.h @@ -0,0 +1,24 @@ +#ifndef _WORKFLOW_SELECT_CTAP_CREDENTIAL_H +#define _WORKFLOW_SELECT_CTAP_CREDENTIAL_H + +#include + +typedef struct { + uint32_t creation_time; + char username[CTAP_STORAGE_USER_NAME_LIMIT]; + char display_name[CTAP_STORAGE_DISPLAY_NAME_LIMIT]; +} ctap_credential_display_t; + +typedef struct { + size_t n_elems; + ctap_credential_display_t creds[CTAP_CREDENTIAL_LIST_MAX_SIZE]; +} ctap_credential_display_list_t; + +/** + * Starts the select credential workflow. + * @param[in] credentials List of credentials to display. + * @return Index of the selected credential within the list, or -1 if aborted. + */ +int workflow_select_ctap_credential(ctap_credential_display_list_t* credentials); + +#endif // _WORKFLOW_SELECT_CTAP_CREDENTIAL_H diff --git a/test/device-test/CMakeLists.txt b/test/device-test/CMakeLists.txt index b52ac2e97..a166d126f 100644 --- a/test/device-test/CMakeLists.txt +++ b/test/device-test/CMakeLists.txt @@ -50,6 +50,7 @@ set(LIBRARIES ${QTOUCHLIB_A} ${QTOUCHLIB_B} ${QTOUCHLIB_T} + tinycbor wallycore secp256k1 noiseprotocol @@ -102,6 +103,7 @@ target_link_libraries(bitbox02-platform PUBLIC wallycore secp256k1 + tinycbor PRIVATE bitbox02_rust_c fatfs @@ -214,6 +216,7 @@ target_link_libraries(bitboxbase-platform PUBLIC wallycore secp256k1 + tinycbor PRIVATE asf4-drivers-min asf4-drivers diff --git a/test/unit-test/CMakeLists.txt b/test/unit-test/CMakeLists.txt index 22ce9c506..7a2890ab9 100644 --- a/test/unit-test/CMakeLists.txt +++ b/test/unit-test/CMakeLists.txt @@ -114,6 +114,7 @@ target_link_libraries(bitbox wallycore secp256k1 bignum + tinycbor sha3 PRIVATE base32 @@ -167,7 +168,7 @@ set(TEST_LIST keystore "-Wl,--wrap=secp256k1_ecdsa_sign_recoverable,--wrap=memory_is_initialized,--wrap=memory_is_seeded,--wrap=memory_get_failed_unlock_attempts,--wrap=memory_reset_failed_unlock_attempts,--wrap=memory_increment_failed_unlock_attempts,--wrap=memory_set_encrypted_seed_and_hmac,--wrap=memory_get_encrypted_seed_and_hmac,--wrap=reset_reset,--wrap=salt_hash_data,--wrap=cipher_aes_hmac_encrypt" keystore_functional - "-Wl,--wrap=memory_is_initialized,--wrap=memory_is_seeded,--wrap=memory_set_encrypted_seed_and_hmac,--wrap=memory_get_encrypted_seed_and_hmac,--wrap=memory_get_salt_root,--wrap=memory_get_failed_unlock_attempts,--wrap=memory_reset_failed_unlock_attempts,--wrap=memory_increment_failed_unlock_attempts,--wrap=securechip_kdf" + "-Wl,--wrap=memory_is_initialized,--wrap=memory_is_seeded,--wrap=memory_set_encrypted_seed_and_hmac,--wrap=memory_get_encrypted_seed_and_hmac,--wrap=memory_get_salt_root,--wrap=memory_set_salt_root,--wrap=memory_get_failed_unlock_attempts,--wrap=memory_reset_failed_unlock_attempts,--wrap=memory_increment_failed_unlock_attempts,--wrap=securechip_kdf" gestures "" random From b03bf6532abafc96ccd5f64389947b933f6498b5 Mon Sep 17 00:00:00 2001 From: Simone Baratta Date: Tue, 11 Feb 2020 11:03:07 +0100 Subject: [PATCH 10/15] Add timer on the U2F stack for CTAP keepalives. --- src/fido2/ctap.h | 15 ++++++++ src/u2f.c | 97 +++++++++++++++++++++++++++++++++++++++++++----- src/u2f.h | 1 + src/usb/usb.c | 1 + 4 files changed, 105 insertions(+), 9 deletions(-) diff --git a/src/fido2/ctap.h b/src/fido2/ctap.h index 02f8113d6..c3435b492 100644 --- a/src/fido2/ctap.h +++ b/src/fido2/ctap.h @@ -36,6 +36,13 @@ #include +/** + * Authenticator Status, transmitted through keepalive messages. + */ +#define CTAPHID_STATUS_IDLE 0 +#define CTAPHID_STATUS_PROCESSING 1 +#define CTAPHID_STATUS_UPNEEDED 2 + #define EXT_HMAC_SECRET_COSE_KEY 0x01 #define EXT_HMAC_SECRET_SALT_ENC 0x02 #define EXT_HMAC_SECRET_SALT_AUTH 0x03 @@ -343,6 +350,14 @@ typedef struct { void ctap_response_init(ctap_response_t* resp); ctap_request_result_t ctap_request(const in_buffer_t* in_buf, buffer_t* out_buf); + +/** + * Polls an outstanding operation for completion. + * + * @param out_data Buffer to fill with a response (if any is ready). + * @param out_len[out] Length of the response contained in out_data. + * @return Request status. + */ ctap_request_result_t ctap_retry(buffer_t* out_buf); // Run ctap related power-up procedures (init pinToken, generate shared secret) diff --git a/src/u2f.c b/src/u2f.c index 2d0229ad9..7a2ce5af7 100644 --- a/src/u2f.c +++ b/src/u2f.c @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -37,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -119,6 +121,21 @@ typedef struct { * Will drop the refresh_webpage() screen after a few ticks. */ uint16_t refresh_webpage_timeout; + /** + * If a CTAP operation is pending, it needs to be polled + * in the background. + */ + bool ctap_op_pending; + /** + * This value is incremented by a timer every 20ms. + * It is used to periodically send CTAP keepalives to the host. + */ + uint16_t ctap_keepalive_timer; + /** + * If a CTAP operation is pending, this pointer will point to + * a pre-allocated packet, prepared with the proper header. + */ + Packet* ctap_pending_response; } u2f_state_t; static u2f_state_t _state = {0}; @@ -748,6 +765,20 @@ static void _cmd_cancel(const Packet* in_packet) */ } +static void _cmd_keepalive(const Packet* in_packet) +{ + Packet out_packet; + prepare_usb_packet(in_packet->cmd, in_packet->cid, &out_packet); + + screen_print_debug("KEEPALIVE!!", 500); + util_zero(out_packet.data_addr, sizeof(out_packet.data_addr)); + out_packet.len = 1; + out_packet.cmd = U2FHID_KEEPALIVE; + out_packet.cid = in_packet->cid; + out_packet.data_addr[0] = CTAPHID_STATUS_UPNEEDED; + usb_processing_send_packet(usb_processing_u2f(), &out_packet); +} + /** * Synchronize a channel and optionally requests a unique 32-bit channel identifier (CID) * that can be used by the requesting application during its lifetime. @@ -969,6 +1000,19 @@ static void _cmd_cbor(const Packet* in_packet) { ctap_request_result_t result = ctap_request(&request_buf, &response_buf); if (!result.request_completed) { /* Don't send a response yet. */ + _state.ctap_keepalive_timer = 0; + _state.ctap_op_pending = true; + _state.ctap_pending_response = malloc(sizeof(*_state.ctap_pending_response)); + if (!_state.ctap_pending_response) { + /* Failed to allocate a big buffer - so we should be able to recover. */ + _state.ctap_op_pending = false; + _unlock(); + _error_hid(in_packet->cid, CTAP1_ERR_OTHER, out_packet); + usb_processing_send_packet(usb_processing_u2f(), out_packet); + return; + } + /* Save the output packet for later use. */ + memcpy(_state.ctap_pending_response, out_packet, sizeof(*out_packet)); free(out_packet); return; } @@ -1084,23 +1128,57 @@ static void _process_authenticate(void) } } +#define CTAP_KEEPALIVE_PERIOD (1) + void u2f_process(void) { if (!_state.locked) { return; } - switch (_state.last_cmd) { - case U2F_REGISTER: - _process_register(); - break; - case U2F_AUTHENTICATE: - _process_authenticate(); - break; - default: - Abort("Bad U2F process state."); + if (_state.ctap_op_pending) { + if (!_state.ctap_pending_response) { + Abort("NULL pending response buffer in u2f_process.\n"); + } + buffer_t out_buf = { + .data = _state.ctap_pending_response->data_addr + 1, + .len = 0, + .max_len = USB_DATA_MAX_LEN - 1 + }; + ctap_request_result_t result = ctap_retry(&out_buf); + if (!result.request_completed) { + if (_state.ctap_keepalive_timer >= CTAP_KEEPALIVE_PERIOD) { + _state.ctap_keepalive_timer = 0; + _state.ctap_pending_response->len = 1; + _state.ctap_pending_response->cmd = U2FHID_KEEPALIVE; + _state.ctap_pending_response->data_addr[0] = CTAPHID_STATUS_UPNEEDED; + usb_processing_send_packet(usb_processing_u2f(), _state.ctap_pending_response); + } + } else { + /* + * The CTAP operation has finished! + */ + _unlock(); + _state.ctap_op_pending = false; + } + } else { + switch (_state.last_cmd) { + case U2F_REGISTER: + _process_register(); + break; + case U2F_AUTHENTICATE: + _process_authenticate(); + break; + default: + Abort("Bad U2F process state."); + } } } +void u2f_timer(void) +{ + _state.ctap_keepalive_timer++; +} + /** * Set up the U2F commands. @@ -1114,6 +1192,7 @@ void u2f_device_setup(void) {U2FHID_MSG, _cmd_msg}, {U2FHID_CBOR, _cmd_cbor}, {U2FHID_CANCEL, _cmd_cancel}, + {U2FHID_KEEPALIVE, _cmd_keepalive}, }; usb_processing_register_cmds( usb_processing_u2f(), u2f_cmd_callbacks, sizeof(u2f_cmd_callbacks) / sizeof(CMD_Callback)); diff --git a/src/u2f.h b/src/u2f.h index 4b82ad525..9f0c0d2b1 100644 --- a/src/u2f.h +++ b/src/u2f.h @@ -42,6 +42,7 @@ void u2f_blocked_req_error(Packet* out_packet, const Packet* in_packet); * Called at every event loop to manage the U2F stack. */ void u2f_process(void); +void u2f_timer(void); /** * Called to abort any operation that blocked the U2F stack. diff --git a/src/usb/usb.c b/src/usb/usb.c index 99a459a3f..66ac18edd 100644 --- a/src/usb/usb.c +++ b/src/usb/usb.c @@ -76,6 +76,7 @@ static void _timeout_cb(const struct timer_task* const timer_task) { (void)timer_task; u2f_packet_timeout_tick(); + u2f_timer(); } #endif From 72fd7db1f9b8c0696e07cd863a15bddb2e1dc8e5 Mon Sep 17 00:00:00 2001 From: Simone Baratta Date: Wed, 12 Feb 2020 12:06:23 +0100 Subject: [PATCH 11/15] Add proper memory backing to FIDO2 --- src/memory/memory.c | 52 ++++++++++++++++++++++++++++++++++++++------- src/memory/memory.h | 4 ---- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/memory/memory.c b/src/memory/memory.c index d675ed172..537663f30 100644 --- a/src/memory/memory.c +++ b/src/memory/memory.c @@ -116,6 +116,28 @@ typedef union { uint8_t bytes[CHUNK_SIZE]; } chunk_2_t; +// Resident key stored in memory: +// the raw key object is stored, followed by a valid byte. +// The key is valid if valid == sectrue_u8. +typedef struct __attribute__((packed)) { + ctap_resident_key_t key; + uint8_t valid; +} memory_resident_key_t; + +// CHUNK_3-4: FIDO2 resident keys (first chunk). +typedef union { + struct __attribute__((__packed__)) { + memory_resident_key_t keys[MEMORY_CTAP_RESIDENT_KEYS_PER_CHUNK]; + } fields; + uint8_t bytes[CHUNK_SIZE]; +} fido2_resident_key_chunk_t; + +#define CHUNK_3 (3) +typedef fido2_resident_key_chunk_t chunk_3_t; + +#define CHUNK_4 (4) +typedef fido2_resident_key_chunk_t chunk_4_t; + // CHUNK_SHARED: Shared data between the bootloader and firmware. // auto_enter: if sectrue_u8, bootloader mode is entered on reboot // upside_down: passes screen orientation to the bootloader @@ -811,13 +833,20 @@ bool memory_multisig_get_by_hash(const uint8_t* hash, char* name_out) return false; } -static ctap_resident_key_t rks[MEMORY_CTAP_RESIDENT_KEYS_MAX]; -static int rks_valid[MEMORY_CTAP_RESIDENT_KEYS_MAX] = {0}; +static int _ctap_resident_key_chunk(int key_idx) +{ + return CHUNK_3 + key_idx / MEMORY_CTAP_RESIDENT_KEYS_PER_CHUNK; +} + bool memory_get_ctap_resident_key(int key_idx, ctap_resident_key_t* key_out) { + fido2_resident_key_chunk_t chunk = {0}; + CLEANUP_CHUNK(chunk); + _read_chunk(_ctap_resident_key_chunk(key_idx), chunk.bytes); /** TODO: simo: implement */ - memcpy(key_out, &rks[key_idx], sizeof(*rks)); - if (rks_valid[key_idx]) { + int key_idx_in_chunk = key_idx % MEMORY_CTAP_RESIDENT_KEYS_PER_CHUNK; + *key_out = chunk.fields.keys[key_idx_in_chunk].key; + if (chunk.fields.keys[key_idx_in_chunk].valid == sectrue_u8) { key_out->valid = CTAP_RESIDENT_KEY_VALID; } else { key_out->valid = CTAP_RESIDENT_KEY_INVALID; @@ -827,11 +856,18 @@ bool memory_get_ctap_resident_key(int key_idx, ctap_resident_key_t* key_out) void memory_store_ctap_resident_key(int store_location, const ctap_resident_key_t* rk_to_store) { - /** TODO: simo: implement */ - memcpy(&rks[store_location], rk_to_store, sizeof(*rks)); + fido2_resident_key_chunk_t chunk = {0}; + CLEANUP_CHUNK(chunk); + int chunk_idx = _ctap_resident_key_chunk(store_location); + int key_idx_in_chunk = store_location % MEMORY_CTAP_RESIDENT_KEYS_PER_CHUNK; + + _read_chunk(chunk_idx, chunk.bytes); + + chunk.fields.keys[key_idx_in_chunk].key = *rk_to_store; if (rk_to_store->valid == CTAP_RESIDENT_KEY_VALID) { - rks_valid[store_location] = true; + chunk.fields.keys[key_idx_in_chunk].valid = sectrue_u8; } else { - rks_valid[store_location] = false; + chunk.fields.keys[key_idx_in_chunk].valid = secfalse_u8; } + _write_chunk(chunk_idx, chunk.bytes); } diff --git a/src/memory/memory.h b/src/memory/memory.h index 41babef09..c12d46a7e 100644 --- a/src/memory/memory.h +++ b/src/memory/memory.h @@ -68,11 +68,7 @@ USE_RESULT bool memory_cleanup_smarteeprom(void); /* Each memory chunk can contain up to 23 resident keys. */ #define MEMORY_CTAP_RESIDENT_KEYS_PER_CHUNK (23) #define MEMORY_CTAP_RESIDENT_KEYS_CHUNKS (2) -#if 0 #define MEMORY_CTAP_RESIDENT_KEYS_MAX (MEMORY_CTAP_RESIDENT_KEYS_CHUNKS * MEMORY_CTAP_RESIDENT_KEYS_PER_CHUNK) -#else -#define MEMORY_CTAP_RESIDENT_KEYS_MAX (5) -#endif // set device name. name is an utf8-encoded string, and null terminated. The max // size (including the null terminator) is MEMORY_DEVICE_NAME_MAX_LEN bytes. From 5ab351f6326e3ac8b38df7a42b3e4e810633915c Mon Sep 17 00:00:00 2001 From: Simone Baratta Date: Wed, 12 Feb 2020 16:38:47 +0100 Subject: [PATCH 12/15] scroll_through_all_variants: Accept a custom callback parameter. --- src/ui/components/confirm_mnemonic.c | 16 ++++++++++--- src/ui/components/confirm_mnemonic.h | 6 +++-- .../components/scroll_through_all_variants.c | 18 ++++++++++----- .../components/scroll_through_all_variants.h | 6 +++-- src/workflow/show_mnemonic.c | 23 ++++++++++++++++--- test/device-test/src/test_all_variants_menu.c | 8 +++++-- test/device-test/src/test_confirm_bip39.c | 12 ++++++---- test/device-test/src/test_scroll_menu_2.c | 2 +- 8 files changed, 68 insertions(+), 23 deletions(-) diff --git a/src/ui/components/confirm_mnemonic.c b/src/ui/components/confirm_mnemonic.c index 1dac61ea6..792e9eb1d 100644 --- a/src/ui/components/confirm_mnemonic.c +++ b/src/ui/components/confirm_mnemonic.c @@ -39,8 +39,10 @@ component_t* confirm_mnemonic_create( const char** wordlist, uint8_t length, uint8_t index, - void (*check_word_cb)(uint8_t), - void (*cancel_cb)(void)) + void (*check_word_cb)(uint8_t, void*), + void* check_word_cb_param, + void (*cancel_cb)(void*), + void* cancel_cb_param) { component_t* confirm_mnemonic = malloc(sizeof(component_t)); if (!confirm_mnemonic) { @@ -60,7 +62,15 @@ component_t* confirm_mnemonic_create( ui_util_add_sub_component( confirm_mnemonic, scroll_through_all_variants_create( - wordlist, check_word_cb, length, "", NULL, cancel_cb, confirm_mnemonic)); + wordlist, + check_word_cb, + check_word_cb_param, + length, + "", + NULL, + cancel_cb, + cancel_cb_param, + confirm_mnemonic)); return confirm_mnemonic; } diff --git a/src/ui/components/confirm_mnemonic.h b/src/ui/components/confirm_mnemonic.h index 305127704..29246b4aa 100644 --- a/src/ui/components/confirm_mnemonic.h +++ b/src/ui/components/confirm_mnemonic.h @@ -23,7 +23,9 @@ component_t* confirm_mnemonic_create( const char** wordlist, uint8_t length, uint8_t index, - void (*check_word_cb)(uint8_t), - void (*cancel_cb)(void)); + void (*check_word_cb)(uint8_t, void*), + void* check_word_cb_param, + void (*cancel_cb)(void*), + void* cancel_cb_param); #endif diff --git a/src/ui/components/scroll_through_all_variants.c b/src/ui/components/scroll_through_all_variants.c index 53d58612f..81746eeb8 100644 --- a/src/ui/components/scroll_through_all_variants.c +++ b/src/ui/components/scroll_through_all_variants.c @@ -39,10 +39,12 @@ typedef struct { component_t* forward_arrow; bool show_index; int32_t diff_to_middle; - void (*select_word_cb)(uint8_t); + void (*select_word_cb)(uint8_t, void*); + void* select_word_cb_param; component_t* continue_on_last_button; void (*continue_on_last_cb)(void); - void (*cancel_cb)(void); + void (*cancel_cb)(void*); + void* cancel_cb_param; } scroll_through_all_variants_data_t; static const uint8_t part_width = 15; @@ -58,14 +60,14 @@ static void _select(component_t* button) { scroll_through_all_variants_data_t* data = (scroll_through_all_variants_data_t*)button->parent->data; - data->select_word_cb(data->index); + data->select_word_cb(data->index, data->select_word_cb_param); } static void _cancel(component_t* component) { scroll_through_all_variants_data_t* data = (scroll_through_all_variants_data_t*)component->parent->data; - data->cancel_cb(); + data->cancel_cb(data->cancel_cb_param); } static void _display_index(component_t* scroll_through_all_variants) @@ -232,11 +234,13 @@ static const component_functions_t _component_functions = { */ component_t* scroll_through_all_variants_create( const char* const* words, - void (*select_word_cb)(uint8_t), + void (*select_word_cb)(uint8_t, void*), + void* select_word_cb_param, const uint8_t length, const char* title, void (*continue_on_last_cb)(void), - void (*cancel_cb)(void), + void (*cancel_cb)(void*), + void* cancel_cb_param, component_t* parent) { component_t** labels = malloc(sizeof(component_t*) * length); @@ -264,12 +268,14 @@ component_t* scroll_through_all_variants_create( data->labels = labels; data->words = words; data->select_word_cb = select_word_cb; + data->select_word_cb_param = select_word_cb_param; data->length = length; data->index = 0; data->show_index = !title; data->continue_on_last_cb = continue_on_last_cb; data->continue_on_last_button = NULL; data->cancel_cb = cancel_cb; + data->cancel_cb_param = cancel_cb_param; scroll_through_all_variants->data = data; for (int i = 0; i < length; i++) { diff --git a/src/ui/components/scroll_through_all_variants.h b/src/ui/components/scroll_through_all_variants.h index 71c59400d..3449832b3 100644 --- a/src/ui/components/scroll_through_all_variants.h +++ b/src/ui/components/scroll_through_all_variants.h @@ -35,11 +35,13 @@ */ component_t* scroll_through_all_variants_create( const char* const* words, - void (*select_word_cb)(uint8_t), + void (*select_word_cb)(uint8_t, void*), + void* select_word_cb_param, uint8_t length, const char* title, void (*continue_on_last_cb)(void), - void (*cancel_cb)(void), + void (*cancel_cb)(void*), + void* cancel_cb_param, component_t* parent); #endif diff --git a/src/workflow/show_mnemonic.c b/src/workflow/show_mnemonic.c index edfefa26d..e48431fd0 100644 --- a/src/workflow/show_mnemonic.c +++ b/src/workflow/show_mnemonic.c @@ -108,18 +108,33 @@ static uint8_t _create_random_unique_words(const char** wordlist, uint8_t length } static uint8_t _selection_idx; -static void _select_word(uint8_t selection_idx) +static void _select_word(uint8_t selection_idx, void* param) { + (void)param; _selection_idx = selection_idx; workflow_blocking_unblock(); } +static void _workflow_cancel_cb(void* param) +{ + (void)param; + workflow_cancel(); +} + static bool _show_words(const char** words, uint8_t words_count) { return workflow_cancel_run( _cancel_confirm_title, scroll_through_all_variants_create( - words, NULL, words_count, NULL, workflow_blocking_unblock, workflow_cancel, NULL)); + words, + NULL, + NULL, + words_count, + NULL, + workflow_blocking_unblock, + _workflow_cancel_cb, + NULL, + NULL)); } typedef struct { @@ -175,7 +190,9 @@ bool workflow_show_mnemonic_create(void) NUM_RANDOM_WORDS + 1, word_idx, _select_word, - workflow_cancel))) { + NULL, + _workflow_cancel_cb, + NULL))) { return false; } if (_selection_idx == correct_idx) { diff --git a/test/device-test/src/test_all_variants_menu.c b/test/device-test/src/test_all_variants_menu.c index 8efc2ec4a..b7620c25a 100644 --- a/test/device-test/src/test_all_variants_menu.c +++ b/test/device-test/src/test_all_variants_menu.c @@ -29,7 +29,11 @@ uint32_t __stack_chk_guard = 0; -static void _cancel(void) {} +static void _cancel(void* param) +{ + (void)param; +} + int main(void) { @@ -39,7 +43,7 @@ int main(void) const char* words[] = {"one", "two", "three", "four", "five", "six", "seven"}; component_t* test_scroll_through_all_variants = - scroll_through_all_variants_create(words, NULL, 7, NULL, NULL, _cancel, NULL); + scroll_through_all_variants_create(words, NULL, NULL, 7, NULL, NULL, _cancel, NULL, NULL); ui_screen_stack_push(test_scroll_through_all_variants); firmware_main_loop(); diff --git a/test/device-test/src/test_confirm_bip39.c b/test/device-test/src/test_confirm_bip39.c index 5195744d8..6304c8cb5 100644 --- a/test/device-test/src/test_confirm_bip39.c +++ b/test/device-test/src/test_confirm_bip39.c @@ -51,8 +51,9 @@ static bool _mock_get_bip_39_mnemonic(char** mnemonic) static uint8_t _current_correct_idx; -static void _confirm_mnemonic(uint8_t index) +static void _confirm_mnemonic(uint8_t index, void* param) { + (void)param; if (index == 5) { screen_print_debug("back to seed phrase", 1000); } else if (index == _current_correct_idx) { @@ -106,7 +107,10 @@ static uint8_t _create_random_unique_words(const char** wordlist, uint8_t length return index_word; } -static void _cancel(void) {} +static void _cancel(void* param) +{ + (void)param; +} #define NUM_RANDOM_WORDS 5 #define NUM_CONFIRM_WORDS (NUM_RANDOM_WORDS + 1) @@ -123,8 +127,8 @@ int main(void) _current_correct_idx = _create_random_unique_words(wordlist, NUM_RANDOM_WORDS, "skirt"); wordlist[NUM_CONFIRM_WORDS - 1] = "Back to seed phrase"; - component_t* confirm_mnemonic = - confirm_mnemonic_create(wordlist, NUM_CONFIRM_WORDS, 0, _confirm_mnemonic, _cancel); + component_t* confirm_mnemonic = confirm_mnemonic_create( + wordlist, NUM_CONFIRM_WORDS, 0, _confirm_mnemonic, NULL, _cancel, NULL); ui_screen_stack_switch(confirm_mnemonic); firmware_main_loop(); diff --git a/test/device-test/src/test_scroll_menu_2.c b/test/device-test/src/test_scroll_menu_2.c index 619f3a9fa..469df6b6a 100644 --- a/test/device-test/src/test_scroll_menu_2.c +++ b/test/device-test/src/test_scroll_menu_2.c @@ -42,7 +42,7 @@ int main(void) const char* words[] = {"first", "second", "third", "forth"}; component_t* test_scroll_through_2 = - scroll_through_all_variants_create(words, NULL, 4, NULL, NULL, NULL, NULL); + scroll_through_all_variants_create(words, NULL, NULL, 4, NULL, NULL, NULL, NULL, NULL); ui_screen_stack_push(test_scroll_through_2); firmware_main_loop(); From 1aa154f1ecbac252f0cab51fea08ce42146415de Mon Sep 17 00:00:00 2001 From: Simone Baratta Date: Wed, 12 Feb 2020 16:39:46 +0100 Subject: [PATCH 13/15] FIDO2: Select credential to be returned when logging in. --- src/fido2/ctap.c | 202 ++++++++++++++++---------- src/util.c | 33 ++++- src/util.h | 9 ++ src/workflow/blocking.c | 2 +- src/workflow/select_ctap_credential.c | 110 +++++++++++++- src/workflow/select_ctap_credential.h | 12 +- 6 files changed, 281 insertions(+), 87 deletions(-) diff --git a/src/fido2/ctap.c b/src/fido2/ctap.c index 6a7158da2..80d113fc6 100644 --- a/src/fido2/ctap.c +++ b/src/fido2/ctap.c @@ -105,9 +105,36 @@ typedef struct { CTAP_GET_ASSERTION_STARTED, CTAP_GET_ASSERTION_UNLOCKED, CTAP_GET_ASSERTION_WAIT_CONFIRM, - CTAP_GET_ASSERTION_FAILED, + CTAP_GET_ASSERTION_CONFIRMED, + CTAP_GET_ASSERTION_SELECT_CREDENTIAL, + CTAP_GET_ASSERTION_SELECTED_CREDENTIAL, + /** User aborted the request */ + CTAP_GET_ASSERTION_DENIED, + /** No valid credentials were found. */ + CTAP_GET_ASSERTION_NO_CREDENTIALS, CTAP_GET_ASSERTION_FINISHED, } state; + /** Key handle that was selected for authentication. */ + u2f_keyhandle_t auth_credential; + /** Private key corresponding to auth_credential. */ + uint8_t auth_privkey[HMAC_SHA256_LEN]; + /** User ID corresponding to auth_credential. + * + * When no allow list is present, it's mandatory that + * we add a user ID to the credential we return. + */ + uint8_t user_id[CTAP_USER_ID_MAX_SIZE]; + /** Actual size of the user ID. */ + size_t user_id_size; + /** + * List of valid credentials for this GA request. + */ + ctap_credential_display_list_t cred_list; + /** + * For each credential in cred_list, save the index + * that that credential has in the RK memory. + */ + int cred_idx[CTAP_CREDENTIAL_LIST_MAX_SIZE]; ctap_get_assertion_req_t req; } ctap_get_assertion_state_t; @@ -337,7 +364,7 @@ static void _copy_or_truncate(char* dst, size_t dst_size, const char* src) if (!truncate) { dst[src_size] = '\0'; } else { - strcpy(dst, padding); + strcpy(dst + src_size, padding); dst[src_size + padding_size] = '\0'; } } @@ -356,7 +383,7 @@ static void _get_assertion_unlock_cb(bool result, void* param) { * User didn't authenticate. * Let's count this as a "user denied" error. */ - _state.data.get_assertion.state = CTAP_GET_ASSERTION_FAILED; + _state.data.get_assertion.state = CTAP_GET_ASSERTION_DENIED; return; } _state.data.get_assertion.state = CTAP_GET_ASSERTION_UNLOCKED; @@ -367,9 +394,9 @@ static void _get_assertion_allow_cb(bool result, void* param) (void)param; ctap_get_assertion_state_t* state = &_state.data.get_assertion; if (result) { - state->state = CTAP_GET_ASSERTION_FINISHED; + state->state = CTAP_GET_ASSERTION_CONFIRMED; } else { - state->state = CTAP_GET_ASSERTION_FAILED; + state->state = CTAP_GET_ASSERTION_DENIED; } } @@ -1102,6 +1129,55 @@ static void _authenticate_with_allow_list(ctap_get_assertion_req_t* GA, u2f_keyh *chosen_credential_out = NULL; } +/** + * Called when the user has selected one of the credentials from + * the available credential list for an authentication request. + * + * This function will decode the selected authentication key and + * move to the CTAP_GET_ASSERTION_SELECTED_CREDENTIAL state. + */ +static void _auth_credential_selected(int selected_cred, void* param) +{ + (void)param; + ctap_get_assertion_state_t* state = &_state.data.get_assertion; + + if (selected_cred < 0) { + /* User aborted. */ + state->state = CTAP_GET_ASSERTION_DENIED; + return; + } + + /* Now load the credential that was selected in the output buffer. */ + ctap_resident_key_t selected_key; + //screen_sprintf_debug(500, "Selected cred #%d", cred_idx[selected_cred]); + bool mem_result = memory_get_ctap_resident_key(state->cred_list.creds[selected_cred].mem_id, &selected_key); + + if (!mem_result) { + /* Shouldn't happen, but if it does we effectively don't have any valid credential to provide. */ + state->state = CTAP_GET_ASSERTION_NO_CREDENTIALS; + return; + } + /* Sanity check the stored credential. */ + if (selected_key.valid != CTAP_RESIDENT_KEY_VALID || + selected_key.user_id_size > CTAP_USER_ID_MAX_SIZE) { + state->state = CTAP_GET_ASSERTION_NO_CREDENTIALS; + return; + } + memcpy(&state->auth_credential, &selected_key.key_handle, sizeof(selected_key.key_handle)); + state->user_id_size = selected_key.user_id_size; + memcpy(state->user_id, selected_key.user_id, state->user_id_size); + + /* Sanity check the key and extract the private key. */ + bool key_valid = u2f_keyhandle_verify(state->req.rp.id, (const uint8_t*)&state->auth_credential, sizeof(state->auth_credential), state->auth_privkey); + if (!key_valid) { + workflow_status_blocking("Internal error. Keyhandle verification failed.", false); + state->state = CTAP_GET_ASSERTION_NO_CREDENTIALS; + return; + } + state->state = CTAP_GET_ASSERTION_SELECTED_CREDENTIAL; +} + + /** * Selects one of the stored credentials for authentication. * @@ -1114,14 +1190,10 @@ static void _authenticate_with_allow_list(ctap_get_assertion_req_t* GA, u2f_keyh * chosen credential. Must be CTAP_STORAGE_USER_NAME_LIMIT bytes long. * @param user_id_size_out Will be filled with the size of user_id. */ -static uint8_t _authenticate_with_rk(ctap_get_assertion_req_t* GA, u2f_keyhandle_t* chosen_credential_out, uint8_t* chosen_privkey, uint8_t* user_id_out, size_t* user_id_size_out) +static workflow_t* _authenticate_with_rk(ctap_get_assertion_req_t* GA) { - /* - * For each credential that we display, save which RK id it corresponds to. - */ - int cred_idx[CTAP_CREDENTIAL_LIST_MAX_SIZE]; - ctap_credential_display_list_t creds; - creds.n_elems = 0; + ctap_get_assertion_state_t* state = &_state.data.get_assertion; + state->cred_list.n_elems = 0; /* * Compute the hash of the RP id so that we @@ -1141,58 +1213,24 @@ static uint8_t _authenticate_with_rk(ctap_get_assertion_req_t* GA, u2f_keyhandle * This key matches the RP! Add its user information to * our list. */ - cred_idx[creds.n_elems] = i; - ctap_credential_display_t* this_cred = creds.creds + creds.n_elems; + ctap_credential_display_t* this_cred = state->cred_list.creds + state->cred_list.n_elems; + this_cred->mem_id = i; memcpy(this_cred->username, this_key.user_name, sizeof(this_key.user_name)); memcpy(this_cred->display_name, this_key.display_name, sizeof(this_key.display_name)); - creds.n_elems++; - if (creds.n_elems == CTAP_CREDENTIAL_LIST_MAX_SIZE) { + state->cred_list.n_elems++; + if (state->cred_list.n_elems == CTAP_CREDENTIAL_LIST_MAX_SIZE) { /* No more space */ break; } } } - if (creds.n_elems == 0) { - workflow_status_blocking("No credentials found on this device.", false); - return CTAP2_ERR_NO_CREDENTIALS; + if (state->cred_list.n_elems == 0) { + return NULL; } /* Sort credentials by creation time. */ - qsort(creds.creds, creds.n_elems, sizeof(*creds.creds), _compare_display_credentials); - int selected_cred = workflow_select_ctap_credential(&creds); - if (selected_cred < 0) { - /* User aborted. */ - workflow_status_blocking("Operation cancelled", false); - return CTAP2_ERR_OPERATION_DENIED; - } - - /* Now load the credential that was selected in the output buffer. */ - ctap_resident_key_t selected_key; - //screen_sprintf_debug(500, "Selected cred #%d", cred_idx[selected_cred]); - bool mem_result = memory_get_ctap_resident_key(cred_idx[selected_cred], &selected_key); - - if (!mem_result) { - /* Shouldn't happen, but if it does we effectively don't have any valid credential to provide. */ - workflow_status_blocking("Internal error. Operation cancelled", false); - return CTAP2_ERR_NO_CREDENTIALS; - } - /* Sanity check the stored credential. */ - if (selected_key.valid != CTAP_RESIDENT_KEY_VALID || - selected_key.user_id_size > CTAP_USER_ID_MAX_SIZE) { - //screen_sprintf_debug(1000, "BAD valid %d", selected_key.valid); - workflow_status_blocking("Internal error. Invalid key selected.", false); - return CTAP2_ERR_NO_CREDENTIALS; - } - memcpy(chosen_credential_out, &selected_key.key_handle, sizeof(selected_key.key_handle)); - *user_id_size_out = selected_key.user_id_size; - memcpy(user_id_out, selected_key.user_id, *user_id_size_out); - - /* Sanity check the key and extract the private key. */ - bool key_valid = u2f_keyhandle_verify(GA->rp.id, (uint8_t*)chosen_credential_out, sizeof(*chosen_credential_out), chosen_privkey); - if (!key_valid) { - workflow_status_blocking("Internal error. Keyhandle verification failed.", false); - return CTAP2_ERR_NO_CREDENTIALS; - } - return CTAP1_ERR_SUCCESS; + qsort(state->cred_list.creds, state->cred_list.n_elems, sizeof(*state->cred_list.creds), _compare_display_credentials); + workflow_t* wf = workflow_select_ctap_credential(&state->cred_list, _auth_credential_selected, NULL); + return wf; } /** @@ -1234,6 +1272,9 @@ static void _get_assertion_init_state(ctap_get_assertion_req_t* req) static void _get_assertion_free_state(void) { + util_zero(_state.data.get_assertion.auth_privkey, + sizeof(_state.data.get_assertion.auth_privkey) + ); } static uint8_t ctap_get_assertion(const in_buffer_t* in_buffer) @@ -1278,39 +1319,40 @@ static uint8_t ctap_get_assertion(const in_buffer_t* in_buffer) * Generates a new assertion in response to a GetAssertion request. * Only called when the user has already accepted and identified with the device. */ -static int _get_assertion_complete(buffer_t* out_buf) +static ctap_request_result_t _get_assertion_select_credential(void) { ctap_get_assertion_state_t* state = &_state.data.get_assertion; - u2f_keyhandle_t auth_credential; - uint8_t auth_privkey[HMAC_SHA256_LEN]; - UTIL_CLEANUP_32(auth_privkey); - - /* - * When no allow list is present, it's mandatory that - * we add a user ID to the credential we return. - */ - uint8_t user_id[CTAP_USER_ID_MAX_SIZE] = {0}; - size_t user_id_size = 0; if (state->req.cred_len) { // allowlist is present -> check all the credentials that were actually generated by us. u2f_keyhandle_t* chosen_credential = NULL; - _authenticate_with_allow_list(&state->req, &chosen_credential, auth_privkey); + _authenticate_with_allow_list(&state->req, &chosen_credential, state->auth_privkey); if (!chosen_credential) { /* No credential selected (or no credential was known to the device). */ - return CTAP2_ERR_NO_CREDENTIALS; + ctap_request_result_t result = {.status = CTAP2_ERR_NO_CREDENTIALS, .request_completed = true}; + return result; } - memcpy(&auth_credential, chosen_credential, sizeof(auth_credential)); + memcpy(&state->auth_credential, chosen_credential, sizeof(state->auth_credential)); + state->state = CTAP_GET_ASSERTION_SELECTED_CREDENTIAL; } else { // No allowList, so use all matching RK's matching rpId - uint8_t auth_status = _authenticate_with_rk(&state->req, &auth_credential, auth_privkey, user_id, &user_id_size); - if (auth_status != 0) { - return auth_status; + workflow_t* select_cred_wf = _authenticate_with_rk(&state->req); + if (!select_cred_wf) { + ctap_request_result_t result = {.status = CTAP2_ERR_NO_CREDENTIALS, .request_completed = true}; + return result; } + workflow_stack_start_workflow(select_cred_wf); + state->state = CTAP_GET_ASSERTION_SELECT_CREDENTIAL; } + ctap_request_result_t result = {.status = 0, .request_completed = false}; + return result; +} +static uint8_t _get_assertion_complete(buffer_t* out_buf) +{ size_t actual_auth_data_size; uint8_t auth_data_buf[sizeof(ctap_auth_data_header_t) + 80]; + ctap_get_assertion_state_t* state = &_state.data.get_assertion; uint8_t ret = _make_authentication_response(&state->req, auth_data_buf, &actual_auth_data_size); if (ret != CborNoError) { return ret; @@ -1320,7 +1362,7 @@ static int _get_assertion_complete(buffer_t* out_buf) CborEncoder encoder; memset(&encoder, 0, sizeof(CborEncoder)); cbor_encoder_init(&encoder, out_buf->data, out_buf->max_len, 0); - ret = ctap_end_get_assertion(&encoder, &auth_credential, auth_data_buf, actual_auth_data_size, auth_privkey, state->req.client_data_hash, user_id, user_id_size); + ret = ctap_end_get_assertion(&encoder, &state->auth_credential, auth_data_buf, actual_auth_data_size, state->auth_privkey, state->req.client_data_hash, state->user_id, state->user_id_size); if (ret != CborNoError) { return ret; } @@ -1335,12 +1377,16 @@ static ctap_request_result_t _get_assertion_continue(buffer_t* out_buf) ctap_request_result_t result = {.status = 0, .request_completed = true}; ctap_get_assertion_state_t* state = &_state.data.get_assertion; switch (state->state) { - case CTAP_GET_ASSERTION_FINISHED: - result.status = _get_assertion_complete(out_buf); + case CTAP_GET_ASSERTION_CONFIRMED: + result = _get_assertion_select_credential(); return result; - case CTAP_GET_ASSERTION_FAILED: + case CTAP_GET_ASSERTION_DENIED: result.status = CTAP2_ERR_OPERATION_DENIED; return result; + case CTAP_GET_ASSERTION_NO_CREDENTIALS: + workflow_status_blocking("No credentials found on this device.", false); + result.status = CTAP2_ERR_NO_CREDENTIALS; + return result; case CTAP_GET_ASSERTION_UNLOCKED: /* * Request permission to the user. @@ -1352,8 +1398,12 @@ static ctap_request_result_t _get_assertion_continue(buffer_t* out_buf) state->state = CTAP_MAKE_CREDENTIAL_WAIT_CONFIRM; result.request_completed = false; return result; + case CTAP_GET_ASSERTION_SELECTED_CREDENTIAL: + result.status = _get_assertion_complete(out_buf); + return result; case CTAP_GET_ASSERTION_STARTED: case CTAP_GET_ASSERTION_WAIT_CONFIRM: + case CTAP_GET_ASSERTION_SELECT_CREDENTIAL: result.request_completed = false; return result; default: diff --git a/src/util.c b/src/util.c index 1c8362520..425fe3b9e 100644 --- a/src/util.c +++ b/src/util.c @@ -12,14 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include -#include +#include "util.h" + +#include #include #include #include #include "rust/rust.h" -#include "util.h" + +#include +#include void util_zero(volatile void* dst, size_t len) { @@ -75,6 +78,30 @@ char* util_strdup(const char* str) return result; } +__attribute__ ((format (printf, 1, 2))) +char* util_asprintf(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + // There is a bug in clang-tidy + // See https://bugs.llvm.org/show_bug.cgi?id=41311 + /* Estimate the size of the resulting string. */ + int str_size = vsnprintf(NULL, 0, fmt, args); // NOLINT + if (str_size < 0) { + Abort("util_asprintf: vsnprintf count failed."); + } + char* result = malloc(str_size + 1); + if (!result) { + Abort("util_asprintf: malloc failed."); + } + int actual_size = vsnprintf(result, str_size + 1, fmt, args); // NOLINT + if (actual_size != str_size) { + Abort("util_asprintf: vsnprintf failed."); + } + va_end(args); + return result; +} + bool safe_uint64_add(uint64_t* a, uint64_t b) { if (a == NULL) { diff --git a/src/util.h b/src/util.h index 50ea38ac8..71877c5db 100644 --- a/src/util.h +++ b/src/util.h @@ -80,6 +80,15 @@ void util_cleanup_64(uint8_t** buf); */ char* util_strdup(const char* str); +/** + * Like sprintf, but creates the result buffer with the appropriate size. + * Guaranteed to return non-NULL (aborts if allocation fails). + * + * @param fmt Printf-like format string. + */ +__attribute__ ((format (printf, 1, 2))) +char* util_asprintf(const char* fmt, ...); + #define UTIL_CLEANUP_20(var) \ uint8_t* __attribute__((__cleanup__(util_cleanup_20))) var##_clean __attribute__((unused)) = \ var; diff --git a/src/workflow/blocking.c b/src/workflow/blocking.c index f17e33623..780f72f17 100644 --- a/src/workflow/blocking.c +++ b/src/workflow/blocking.c @@ -37,7 +37,7 @@ static void _run_blocking_ui(bool (*is_done)(void)) void workflow_blocking_block(void) { if (!_done) { - Abort("workflow_blocking_block"); + Abort("workflow_blocking_block invalid state"); } _done = false; _run_blocking_ui(_is_done); diff --git a/src/workflow/select_ctap_credential.c b/src/workflow/select_ctap_credential.c index be17318f8..8e579e06a 100644 --- a/src/workflow/select_ctap_credential.c +++ b/src/workflow/select_ctap_credential.c @@ -1,8 +1,108 @@ #include "select_ctap_credential.h" -int workflow_select_ctap_credential(ctap_credential_display_list_t* credentials) { - /* TODO */ - /* Just select the first credential in the list for now. */ - (void)credentials; - return 0; +#include +#include +#include +#include +#include + +typedef struct { + char* words[CTAP_CREDENTIAL_LIST_MAX_SIZE]; + size_t n_words; + void (*callback)(int, void*); + void *callback_param; + bool done; + int selected; +} data_t; + +/** + * Checks if the user has selected one of the credentials. + */ +static void _workflow_select_ctap_credential_spin(workflow_t* self) +{ + data_t* data = (data_t*)self->data; + if (data->done) { + /* Publish our result. */ + data->callback(data->selected, data->callback_param); + /* Time to go, goodbye. */ + workflow_stack_stop_workflow(); + } +} + +static void _credential_selected(uint8_t selected, void *param) { + data_t* data = (data_t*)param; + data->done = true; + data->selected = selected; +} + +static void _cancel_cb(void *param) { + data_t* data = (data_t*)param; + data->done = true; + data->selected = -1; +} + + +/** + * Starts this workflow. + */ +static void _workflow_select_ctap_credential_init(workflow_t* self) +{ + data_t* data = (data_t*)self->data; + component_t* comp; + comp = scroll_through_all_variants_create( + (const char * const*)data->words, + _credential_selected, + data, + data->n_words, + false, + NULL, + _cancel_cb, + data, + NULL); + ui_screen_stack_push(comp); +} + +/** + * Destroys this workflow. + */ +static void _workflow_select_ctap_credential_cleanup(workflow_t* self) +{ + ui_screen_stack_pop(); + ui_screen_stack_cleanup(); + data_t* data = self->data; + for (size_t i = 0; i < data->n_words; ++i) { + util_zero(data->words[i], strlen(data->words[i])); + free(data->words[i]); + } + util_zero(data, sizeof(*data)); + free(data); + util_zero(self, sizeof(*self)); + free(self); +} + +static char* _credential_to_string(ctap_credential_display_t* cred) +{ + if (strlen(cred->display_name) > 0) { + return util_asprintf("Login as:\n%s\n(%s)", cred->display_name, cred->username); + } + return util_asprintf("Login as:\n%s", cred->username); +} + +workflow_t* workflow_select_ctap_credential(ctap_credential_display_list_t* credentials, void (*callback)(int, void *), void *cb_param) +{ + workflow_t* result = workflow_allocate( + _workflow_select_ctap_credential_init, + _workflow_select_ctap_credential_cleanup, + _workflow_select_ctap_credential_spin, + sizeof(data_t) + ); + data_t* data = (data_t*)result->data; + data->callback = callback; + data->callback_param = cb_param; + /* Create the text representation of each credential. */ + for (size_t i = 0; i < credentials->n_elems; ++i) { + data->words[i] = _credential_to_string(&credentials->creds[i]); + } + data->n_words = credentials->n_elems; + return result; } diff --git a/src/workflow/select_ctap_credential.h b/src/workflow/select_ctap_credential.h index 500ebe1f3..da964f945 100644 --- a/src/workflow/select_ctap_credential.h +++ b/src/workflow/select_ctap_credential.h @@ -2,8 +2,12 @@ #define _WORKFLOW_SELECT_CTAP_CREDENTIAL_H #include +#include typedef struct { + /* ID of the key in the RK storage table. */ + int mem_id; + /* U2F counter at the time this credential was created. */ uint32_t creation_time; char username[CTAP_STORAGE_USER_NAME_LIMIT]; char display_name[CTAP_STORAGE_DISPLAY_NAME_LIMIT]; @@ -17,8 +21,12 @@ typedef struct { /** * Starts the select credential workflow. * @param[in] credentials List of credentials to display. - * @return Index of the selected credential within the list, or -1 if aborted. + * @param[in] callback Callback that will be invoked before the workflow terminates. + * It will be called with the index of the selected credential + * (or -1 if the operation was cancelled), and the user-defined parameter. + * @param[in] cb_param User-defined parameter to be passed to callback. + * @return Workflow object. */ -int workflow_select_ctap_credential(ctap_credential_display_list_t* credentials); +workflow_t* workflow_select_ctap_credential(ctap_credential_display_list_t* credentials, void (*callback)(int, void *), void *cb_param); #endif // _WORKFLOW_SELECT_CTAP_CREDENTIAL_H From 134903f2e5b1310e3b4a1d7a31c20bc7b56f4dd1 Mon Sep 17 00:00:00 2001 From: Simone Baratta Date: Thu, 13 Feb 2020 10:00:45 +0100 Subject: [PATCH 14/15] Fix user menu for fido2 --- src/workflow/select_ctap_credential.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/workflow/select_ctap_credential.c b/src/workflow/select_ctap_credential.c index 8e579e06a..99332b41b 100644 --- a/src/workflow/select_ctap_credential.c +++ b/src/workflow/select_ctap_credential.c @@ -54,7 +54,7 @@ static void _workflow_select_ctap_credential_init(workflow_t* self) _credential_selected, data, data->n_words, - false, + "Select user", NULL, _cancel_cb, data, @@ -83,9 +83,9 @@ static void _workflow_select_ctap_credential_cleanup(workflow_t* self) static char* _credential_to_string(ctap_credential_display_t* cred) { if (strlen(cred->display_name) > 0) { - return util_asprintf("Login as:\n%s\n(%s)", cred->display_name, cred->username); + return util_asprintf("%s\n(%s)", cred->display_name, cred->username); } - return util_asprintf("Login as:\n%s", cred->username); + return util_asprintf("%s", cred->username); } workflow_t* workflow_select_ctap_credential(ctap_credential_display_list_t* credentials, void (*callback)(int, void *), void *cb_param) From d41647d60e5405d7ad3c414efd7cb23d3ea78489 Mon Sep 17 00:00:00 2001 From: Simone Baratta Date: Thu, 13 Feb 2020 10:12:11 +0100 Subject: [PATCH 15/15] FIDO2: Don't select user if there's only one to choose from. --- src/fido2/ctap.c | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/fido2/ctap.c b/src/fido2/ctap.c index 80d113fc6..5eb8e72db 100644 --- a/src/fido2/ctap.c +++ b/src/fido2/ctap.c @@ -406,11 +406,11 @@ static workflow_t* _get_assertion_confirm(ctap_rp_id_t* rp) size_t prompt_size; if (rp->name && rp->name[0] != '\0') { /* There is a human-readable name attached to this domain. */ - prompt_size = snprintf(prompt_buf, 100, "Authenticate on\n%s\n(%.*s)\n", - rp->name, (int)rp->size, rp->id); + prompt_size = snprintf(prompt_buf, 100, "Authenticate on\n%s\n(%.*s)\nsize %u", + rp->name, (int)rp->size, rp->id, sizeof(_state)); } else { - prompt_size = snprintf(prompt_buf, 100, "Authenticate on\n%.*s\n", - (int)rp->size, rp->id); + prompt_size = snprintf(prompt_buf, 100, "Authenticate on\n%.*s\nsize %u", + (int)rp->size, rp->id, sizeof(_state)); } if (prompt_size >= 100) { prompt_buf[99] = '\0'; @@ -1189,8 +1189,10 @@ static void _auth_credential_selected(int selected_cred, void* param) * @param user_id_out Will be filled with the stored User ID corresponding to the * chosen credential. Must be CTAP_STORAGE_USER_NAME_LIMIT bytes long. * @param user_id_size_out Will be filled with the size of user_id. + * + * @return true if authentication was successful (operation should continue), false otherwise. */ -static workflow_t* _authenticate_with_rk(ctap_get_assertion_req_t* GA) +static bool _authenticate_with_rk(ctap_get_assertion_req_t* GA) { ctap_get_assertion_state_t* state = &_state.data.get_assertion; state->cred_list.n_elems = 0; @@ -1225,12 +1227,18 @@ static workflow_t* _authenticate_with_rk(ctap_get_assertion_req_t* GA) } } if (state->cred_list.n_elems == 0) { - return NULL; + return false; } /* Sort credentials by creation time. */ qsort(state->cred_list.creds, state->cred_list.n_elems, sizeof(*state->cred_list.creds), _compare_display_credentials); - workflow_t* wf = workflow_select_ctap_credential(&state->cred_list, _auth_credential_selected, NULL); - return wf; + if (state->cred_list.n_elems > 1) { + workflow_t* wf = workflow_select_ctap_credential(&state->cred_list, _auth_credential_selected, NULL); + workflow_stack_start_workflow(wf); + state->state = CTAP_GET_ASSERTION_SELECT_CREDENTIAL; + } else { + _auth_credential_selected(0, NULL); + } + return true; } /** @@ -1336,13 +1344,11 @@ static ctap_request_result_t _get_assertion_select_credential(void) state->state = CTAP_GET_ASSERTION_SELECTED_CREDENTIAL; } else { // No allowList, so use all matching RK's matching rpId - workflow_t* select_cred_wf = _authenticate_with_rk(&state->req); - if (!select_cred_wf) { + bool rk_result = _authenticate_with_rk(&state->req); + if (!rk_result) { ctap_request_result_t result = {.status = CTAP2_ERR_NO_CREDENTIALS, .request_completed = true}; return result; } - workflow_stack_start_workflow(select_cred_wf); - state->state = CTAP_GET_ASSERTION_SELECT_CREDENTIAL; } ctap_request_result_t result = {.status = 0, .request_completed = false}; return result;