diff --git a/core/ctap.c b/core/ctap.c new file mode 100644 index 0000000..8862658 --- /dev/null +++ b/core/ctap.c @@ -0,0 +1,175 @@ +#include +#include +#include "ctap.h" +#include "utils.h" +#include "ctap_pin.h" + +int ctap_generate_rng(uint8_t *buffer, size_t length) { + for (size_t i = 0; i < length; i++) { + buffer[i] = (uint8_t) rand(); + } + return 1; +} + +static void ctap_state_init(ctap_persistent_state_t *state) { + + debug_log("ctap_state_init" nl); + + // set to 0xff instead of 0x00 to be easier on flash + memset(state, 0xff, sizeof(ctap_persistent_state_t)); + // fresh RNG for key + ctap_generate_rng(state->master_keys, KEY_SPACE_BYTES); + debug_log("generated master_keys: "); + dump_hex(state->master_keys, KEY_SPACE_BYTES); + + state->is_initialized = INITIALIZED_MARKER; + state->pin_total_remaining_attempts = PIN_TOTAL_ATTEMPTS; + state->is_pin_set = 0; + state->num_rk_stored = 0; + + // ctap_reset_rk(); + + if (ctap_generate_rng(state->PIN_SALT, sizeof(state->PIN_SALT)) != 1) { + error_log(red("ctap_generate_rng failed") nl); + // exit(1); // TODO + } + + debug_log("generated PIN SALT: "); + dump_hex(state->PIN_SALT, sizeof(state->PIN_SALT)); + +} + +void authenticator_write_state(ctap_state_t *state) { + +} + +bool ctap_is_pin_set(ctap_state_t *state) { + return state->persistent.is_pin_set == 1; +} + +bool ctap_device_locked(ctap_state_t *state) { + return state->persistent.pin_total_remaining_attempts <= 0; +} + +bool ctap_device_boot_locked(ctap_state_t *state) { + return state->pin_boot_remaining_attempts <= 0; +} + +int8_t ctap_leftover_pin_attempts(ctap_state_t *state) { + return state->persistent.pin_total_remaining_attempts; +} + +static void ctap_reset_key_agreement(ctap_state_t *state) { + static_assert(sizeof(state->KEY_AGREEMENT_PRIV) == 32, "unexpected sizeof(state->KEY_AGREEMENT_PRIV)"); + ctap_generate_rng(state->KEY_AGREEMENT_PRIV, sizeof(state->KEY_AGREEMENT_PRIV)); +} + +void ctap_init(ctap_state_t *state) { + + printf("ctap_init" nl); + + // crypto_ecc256_init(); + + // int is_init = authenticator_read_state(&state); + + // device_set_status(CTAPHID_STATUS_IDLE); + + // if (false) { + // printf( + // "auth state is initialized" nl + // " is_pin_set = %" wPRIu8 nl + // " remaining_tries = %" wPRId8 nl + // " num_rk_stored = %" PRIu16 nl + // " is_initialized = 0x%08" PRIx32 nl + // " is_invalid = 0x%08" PRIx32 nl, + // state.is_pin_set, + // state.remaining_tries, + // state.num_rk_stored, + // state.is_initialized, + // state.is_invalid + // ); + // debug_log(" master_keys = "); + // dump_hex(state->persistent.master_keys, KEY_SPACE_BYTES); + // debug_log(" PIN_SALT = "); + // dump_hex(state->persistent.PIN_SALT, sizeof(state->persistent.PIN_SALT)); + // } else { + ctap_state_init(&state->persistent); + authenticator_write_state(state); + // } + + // do_migration_if_required(&state); + + // crypto_load_master_secret(state->persistent->master_keys); + + if (ctap_is_pin_set(state)) { + info_log("pin remaining_tries=%" wPRId8 nl, state->persistent.pin_total_remaining_attempts); + } else { + info_log("pin not set" nl); + } + + if (ctap_device_locked(state)) { + error_log(red("DEVICE LOCKED!") nl); + } + + if (ctap_generate_rng(state->PIN_TOKEN, PIN_TOKEN_SIZE) != 1) { + error_log(red("ctap_generate_rng failed") nl); + // exit(1); // TODO + } + + ctap_reset_key_agreement(state); + +} + +uint8_t ctap_request( + ctap_state_t *state, + uint16_t request_data_length, + const uint8_t *request_data, + uint8_t *response_status_code, + uint16_t *response_data_length, + uint8_t **response_data +) { + + CborEncoder *encoder = &state->response.encoder; + + uint8_t status = 0; + uint8_t cmd = *request_data; + request_data++; + request_data_length--; + + cbor_encoder_init(encoder, state->response.data, sizeof(state->response.data), 0); + + debug_log("cbor input structure: %d bytes" nl, request_data_length); + debug_log("cbor req: "); + dump_hex(request_data, request_data_length); + + switch (cmd) { + case CTAP_CMD_CLIENT_PIN: + info_log(magenta("CTAP_CLIENT_PIN") nl); + status = ctap_client_pin(state, request_data, request_data_length); + break; + default: + status = CTAP1_ERR_INVALID_COMMAND; + error_log(red("error: invalid cmd: 0x%02" wPRIx8) nl, cmd); + } + + if (status == CTAP2_OK) { + state->response.length = cbor_encoder_get_buffer_size(encoder, state->response.data); + assert(state->response.length > 0); + }else { + state->response.length = 0; + } + + debug_log( + "cbor output structure length %zu bytes, status code 0x%02" wPRIx8 nl, + state->response.length, + status + ); + + *response_status_code = status; + *response_data = state->response.data; + *response_data_length = state->response.length; + + // TODO: return value is no longer used (status code is returned via the response_status_code reference) + return status; + +} diff --git a/core/ctap.h b/core/ctap.h new file mode 100644 index 0000000..538fa5f --- /dev/null +++ b/core/ctap.h @@ -0,0 +1,96 @@ +#ifndef POKUSEW_CTAP_H +#define POKUSEW_CTAP_H + +#include "ctap_parse.h" +#include + +#define CTAP_CMD_MAKE_CREDENTIAL 0x01 +#define CTAP_CMD_GET_ASSERTION 0x02 +#define CTAP_CMD_GET_NEXT_ASSERTION 0x08 +#define CTAP_CMD_GET_INFO 0x04 +#define CTAP_CMD_CLIENT_PIN 0x06 +#define CTAP_CMD_RESET 0x07 +#define CTAP_CMD_BIO_ENROLLMENT 0x09 +#define CTAP_CMD_CREDENTIAL_MANAGEMENT 0x0A +#define CTAP_CMD_SELECTION 0x0B +#define CTAP_CMD_LARGE_BLOBS 0x0C +#define CTAP_CMD_CONFIG 0x0D +// https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#commands +#define CTAP_VENDOR_FIRST 0x40 +#define CTAP_VENDOR_LAST 0xBF + + +#define KEY_SPACE_BYTES 128 +#define PIN_SALT_LEN (32) + +#define EMPTY_MARKER 0xFFFFFFFF +#define INITIALIZED_MARKER 0xA5A5A5A5 +#define INVALID_MARKER 0xDDDDDDDD + +#define PIN_TOTAL_ATTEMPTS 8 +#define PIN_PER_BOOT_ATTEMPTS 3 +static_assert( + PIN_PER_BOOT_ATTEMPTS < PIN_TOTAL_ATTEMPTS, + "PIN_TOTAL_ATTEMPTS must be greater than or equal to PIN_PER_BOOT_ATTEMPTS" +); + +typedef struct ctap_persistent_state { + + // PIN information + uint8_t is_pin_set; + uint8_t PIN_CODE_HASH[32]; + uint8_t PIN_SALT[PIN_SALT_LEN]; + int8_t pin_total_remaining_attempts; + + // number of stored client-side discoverable credentials + // aka resident credentials aka resident keys (RK) + uint16_t num_rk_stored; + + // master keys data + uint8_t master_keys[KEY_SPACE_BYTES]; + + uint8_t _alignment1; + + // this field must be WORD (32 bytes) aligned + uint32_t is_invalid; + // this field must be WORD (32 bytes) aligned + // note: in order for the data loss prevention logic to work, is_initialized must be the last field + uint32_t is_initialized; + +} ctap_persistent_state_t; + +#define CTAP_RESPONSE_BUFFER_SIZE 4096 + +typedef struct ctap_response { + CborEncoder encoder; + uint8_t data[CTAP_RESPONSE_BUFFER_SIZE]; + size_t length; +} ctap_response_t; + +#define PIN_TOKEN_SIZE 16 + +typedef struct ctap_state { + + ctap_persistent_state_t persistent; + + ctap_response_t response; + + uint8_t PIN_TOKEN[PIN_TOKEN_SIZE]; + uint8_t KEY_AGREEMENT_PUB[64]; + uint8_t KEY_AGREEMENT_PRIV[32]; + int8_t pin_boot_remaining_attempts; + +} ctap_state_t; + +uint8_t ctap_request( + ctap_state_t *state, + uint16_t request_data_length, + const uint8_t *request_data, + uint8_t *response_status_code, + uint16_t *response_data_length, + uint8_t **response_data +); + +void ctap_init(ctap_state_t *state); + +#endif // POKUSEW_CTAP_H diff --git a/core/ctap_pin.c b/core/ctap_pin.c new file mode 100644 index 0000000..893ea6b --- /dev/null +++ b/core/ctap_pin.c @@ -0,0 +1,91 @@ +#include "ctap_pin.h" + +// 6.5.4. PIN/UV Auth Protocol Abstract Definition + +// A specific PIN/UV auth protocol defines an implementation of two interfaces to cryptographic services: +// one for the authenticator, and one for the platform. +// +// The authenticator interface is: + +/// This process is run by the authenticator at power-on. +static void initialize(); + +/// Generates a fresh public key. +static void regenerate(); + +/// Generates a fresh pinUvAuthToken. +static void resetPinUvAuthToken(); + +/// Returns the authenticator’s public key as a COSE_Key structure. +static void getPublicKey(COSE_Key *cose_key); + +/// Processes the output of encapsulate from the peer and produces a shared secret, +/// known to both platform and authenticator. +static uint8_t decapsulate(const uint8_t *peer_cose_key, uint8_t *shared_secret); + +/// Decrypts a ciphertext, using sharedSecret as a key, and returns the plaintext. +static uint8_t decrypt(const uint8_t *shared_secret, const uint8_t *ciphertext, const uint8_t *plaintext); + +/// Verifies that the signature is a valid MAC for the given message. +/// If the key parameter value is the current pinUvAuthToken, +/// it also checks whether the pinUvAuthToken is in use or not. +static uint8_t verify(const uint8_t *key, const uint8_t *message, const uint8_t *signature); + + +// The platform interface is: +// +// initialize() +// This is run by the platform when starting a series of transactions with a specific authenticator. +// +// encapsulate(peerCoseKey) → (coseKey, sharedSecret) | error +// Generates an encapsulation for the authenticator’s public key and returns the message to transmit and the shared secret. +// +// encrypt(key, demPlaintext) → ciphertext +// Encrypts a plaintext to produce a ciphertext, which may be longer than the plaintext. The plaintext is restricted to being a multiple of the AES block size (16 bytes) in length. +// +// decrypt(key, ciphertext) → plaintext | error +// Decrypts a ciphertext and returns the plaintext. +// +// authenticate(key, message) → signature +// Computes a MAC of the given message. + +uint8_t ctap_client_pin(ctap_state_t *state, const uint8_t *request, size_t length) { + + CborError err; + + CTAP_clientPIN cp; + uint8_t status = ctap_parse_client_pin(request, length, &cp); + if (status != CTAP2_OK) { + return status; + } + + if (cp.pinUvAuthProtocol != 1 && cp.pinUvAuthProtocol != 2) { + return CTAP1_ERR_INVALID_PARAMETER; + } + + CborEncoder *encoder = &state->response.encoder; + CborEncoder map; + + switch (cp.subCommand) { + case CTAP_clientPIN_subCmd_getPINRetries: + // start response map + cbor_encoding_check(cbor_encoder_create_map(encoder, &map, 2)); + // 1. pinRetries + cbor_encoding_check(cbor_encode_int(&map, CTAP_clientPIN_res_pinRetries)); + cbor_encoding_check(cbor_encode_int(&map, state->persistent.pin_total_remaining_attempts)); + // 2. powerCycleState + cbor_encoding_check(cbor_encode_int(&map, CTAP_clientPIN_res_powerCycleState)); + cbor_encoding_check(cbor_encode_boolean(&map, state->pin_boot_remaining_attempts > 0)); + // close response map + cbor_encoding_check(cbor_encoder_close_container(encoder, &map)); + break; + case CTAP_clientPIN_subCmd_getKeyAgreement: + + break; + default: + return CTAP2_ERR_INVALID_SUBCOMMAND; + } + + return CTAP2_OK; + +} diff --git a/core/ctap_pin.h b/core/ctap_pin.h new file mode 100644 index 0000000..f818570 --- /dev/null +++ b/core/ctap_pin.h @@ -0,0 +1,32 @@ +#ifndef POKUSEW_CTAP_PIN_H +#define POKUSEW_CTAP_PIN_H + +#include "ctap.h" +#include "ctap_parse.h" + +typedef struct CTAP_pinUvAuthToken { + int rpId; + bool rpIdSet; + int permissions; + // usage timer + bool in_use; + // initial usage time limit + // user present time limit + // max usage time period + bool user_verified; + bool user_present; +} CTAP_pinUvAuthToken; + +typedef struct CTAP_pinState { + bool is_pin_set; + // uint8_t PIN_CODE_HASH[32]; + // uint8_t PIN_SALT[PIN_SALT_LEN]; + uint8_t remaining_tries; +} CTAP_pinState; + +// PIN information + +uint8_t ctap_client_pin(ctap_state_t *state, const uint8_t *request, size_t length); + + +#endif // POKUSEW_CTAP_PIN_H diff --git a/core/terminal.h b/core/terminal.h new file mode 100644 index 0000000..326b5d8 --- /dev/null +++ b/core/terminal.h @@ -0,0 +1,111 @@ +#ifndef POKUSEW_TERMINAL_H +#define POKUSEW_TERMINAL_H + +#define ESC "\033" +// CSI (Control Sequence Introducer) sequences +// see https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences +#define CSI ESC "[" +#define CSI_SHOW_CURSOR CSI "?25h" +#define CSI_HIDE_CURSOR CSI "?25l" +#define CSI_CURSOR_POSITION(line, col) CSI line ";" col "H" +#define csp(line, col) CSI_CURSOR_POSITION(line, col) +#define CSI_ERASE_IN_DISPLAY(n) CSI n "J" +#define CSI_ERASE_IN_DISPLAY_ENTIRE_SCREEN CSI_ERASE_IN_DISPLAY("2") +#define CSI_ERASE_IN_LINE(n) CSI n "K" + +#define RN "\r\n" +#define RNC CSI_ERASE_IN_LINE("0") "\r\n" + +#ifndef nl +#define nl "\r\n" +#endif + +// SGR (Select Graphic Rendition) parameters +// see https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters +// multiple arguments can be specified at once, must be separated by semicolon (;) +#define CSI_SGR(attrs) CSI attrs "m" +#define sgr(attrs) sgr(attrs) +#define rst CSI "0m" + +#define bold "1" +#define underline "4" + +// SGR colors +// see https://en.wikipedia.org/wiki/ANSI_escape_code#Colors +// +// SGR description notes +// 30–37 Set foreground color +// 38 Set foreground color Next arguments are 5;n or 2;r;g;b +// 39 Default foreground color Implementation defined (according to standard) +// 40–47 Set background color +// 48 Set background color Next arguments are 5;n or 2;r;g;b +// 49 Default background color Implementation defined (according to standard) +// 90–97 Set bright foreground color Not in standard +// 100–107 Set bright background color Not in standard + +// 0-7 3-bit colors +// 0 Black +// 1 Red +// 2 Green +// 3 Yellow +// 4 Blue +// 5 Magenta +// 6 Cyan +// 7 White + +#define fg_black "30" +#define fg_red "31" +#define fg_green "32" +#define fg_yellow "33" +#define fg_blue "34" +#define fg_magenta "35" +#define fg_cyan "36" +#define fg_white "37" + +#define bg_black "40" +#define bg_red "41" +#define bg_green "42" +#define bg_yellow "43" +#define bg_blue "44" +#define bg_magenta "45" +#define bg_cyan "46" +#define bg_white "47" + +#define fg_bright_black "90" +#define fg_bright_red "91" +#define fg_bright_green "92" +#define fg_bright_yellow "93" +#define fg_bright_blue "94" +#define fg_bright_magenta "95" +#define fg_bright_cyan "96" +#define fg_bright_white "97" + +#define bg_bright_black "100" +#define bg_bright_red "101" +#define bg_bright_green "102" +#define bg_bright_yellow "103" +#define bg_bright_blue "104" +#define bg_bright_magenta "105" +#define bg_bright_cyan "106" +#define bg_bright_white "107" + +// simplified usage +// TODO: recursive macros https://stackoverflow.com/questions/12447557/can-we-have-recursive-macros +#define red_s CSI fg_red ";" bold "m" +#define red(text) CSI fg_red ";" bold "m" text rst +#define green_s CSI fg_green ";" bold "m" +#define green(text) CSI fg_green ";" bold "m" text rst +#define yellow_s CSI fg_bright_yellow ";" bold "m" +#define yellow(text) CSI fg_bright_yellow ";" bold "m" text rst +#define blue_s CSI fg_bright_blue ";" bold "m" +#define blue(text) CSI fg_bright_blue ";" bold "m" text rst +#define magenta_s CSI fg_magenta ";" bold "m" +#define magenta(text) CSI fg_magenta ";" bold "m" text rst +#define cyan_s CSI fg_cyan ";" bold "m" +#define cyan(text) CSI fg_cyan ";" bold "m" text rst +#define gray_s CSI fg_bright_black "m" +#define gray(text) CSI fg_bright_black "m" text rst +#define gray_bold_s CSI fg_bright_black ";" bold "m" +#define gray_bold(text) CSI fg_bright_black ";" bold "m" text rst + +#endif // POKUSEW_TERMINAL_H diff --git a/core/utils.c b/core/utils.c new file mode 100644 index 0000000..81acf44 --- /dev/null +++ b/core/utils.c @@ -0,0 +1,9 @@ +#include "utils.h" + +void dump_hex(const uint8_t *buf, size_t size) { + printf("hex(%zu): ", size); + while (size--) { + printf("%02x ", *buf++); + } + printf("\n"); +} diff --git a/core/utils.h b/core/utils.h new file mode 100755 index 0000000..3fe663d --- /dev/null +++ b/core/utils.h @@ -0,0 +1,78 @@ +#ifndef POKUSEW_UTILS_H +#define POKUSEW_UTILS_H + +#include +#include + +#include "terminal.h" + +// PRI*8 does not work correctly with -specs=nano.specs (which is currently needed because of salty) +// see https://answers.launchpad.net/gcc-arm-embedded/+question/665299 +// see https://sourceware.org/legacy-ml/newlib/2016/msg00000.html +// for more info about -specs=nano.specs see https://metebalci.com/blog/demystifying-arm-gnu-toolchain-specs-nano-and-nosys/ +// we could probably change salty build to not depend on nano libc and the full libc, see https://github.com/rust-lang/compiler-builtins/issues/353#issuecomment-698038631 +#include +#define wPRId8 PRId16 +#define wPRIi8 PRIi16 +#define wPRIo8 PRIo16 +#define wPRIu8 PRIu16 +#define wPRIx8 PRIx16 +#define wPRIX8 PRIX16 + +#ifndef nl +#define nl "\r\n" +#endif + +#define min(a, b) ((a) < (b) ? (a) : (b)) +#define min_of_3(a, b, c) min(min(a, b), (c)) +#define max(a, b) ((a) > (b) ? (a) : (b)) +#define max_of_3(a, b, c) max(max(a, b), (c)) +#define is_in_range_incl(value, min_incl, max_incl) ((min_incl) <= (value) && (value) <= (max_incl)) +#define is_not_in_range_incl(value, min_incl, max_incl) ((value) < (min_incl) || (max_incl) < (value)) +#define pow2(y) (1u << (y)) + +// macro(...) fn(other_arg, __VA_ARGS__) +// see https://stackoverflow.com/questions/1644868/define-macro-for-debug-printing-in-c +// see https://gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html + +#define POKUSEW_DEBUG_LEVEL 3 + +#if POKUSEW_DEBUG_LEVEL > 2 +#define if_debug(code) (code) +#define debug_log(...) fprintf(stdout, __VA_ARGS__) +#define debug_log_str(line) write(STDERR_FILENO, (line), sizeof((line))) +#else +#define if_debug(code) ((void) 0) +#define debug_log(...) ((void) 0) +#define debug_log_str(line) ((void) 0) +// defining NDEBUG removes asserts +#define NDEBUG +#endif + +#if POKUSEW_DEBUG_LEVEL > 1 +#define info_log(...) fprintf(stdout, __VA_ARGS__) +#define info_log_str(line) write(STDERR_FILENO, (line), sizeof((line))) +#else +#define info_log(...) ((void) 0) +#define info_log_str(line) ((void) 0) +#endif + +#if POKUSEW_DEBUG_LEVEL > 0 +#define error_log(...) fprintf(stdout, __VA_ARGS__) +#define error_log_str(line) write(STDERR_FILENO, (line), sizeof((line))) +#else +#define error_log(...) ((void) 0) +#define error_log_str(line) ((void) 0) +#endif + +#include + +#define write_str(line) write(STDOUT_FILENO, (line), sizeof((line))); + +#include + +#define IS_BIG_ENDIAN (*(uint16_t *)"\0\xff" < 0x100) + +void dump_hex(const uint8_t *buf, size_t size); + +#endif // POKUSEW_UTILS_H diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 8063e04..7ae2bb6 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -65,14 +65,56 @@ target_include_directories( ) target_link_libraries( core_ctap_parse - tinycbor + PUBLIC tinycbor ) target_compile_definitions( core_ctap_parse - PRIVATE + PUBLIC LIONKEY_LOG=1 ) +add_library( + core_ctap_pin + STATIC + ../core/ctap_pin.c +) +target_include_directories( + core_ctap_pin + INTERFACE + ../core +) +target_link_libraries( + core_ctap_pin + PUBLIC tinycbor core_ctap_parse +) + +add_library( + core_ctap + STATIC + ../core/ctap.c +) +target_include_directories( + core_ctap + INTERFACE + ../core +) +target_link_libraries( + core_ctap + PUBLIC tinycbor core_ctap_parse core_ctap_pin core_utils +) + +add_library( + core_utils + STATIC + ../core/utils.c +) +target_include_directories( + core_utils + INTERFACE + ../core +) + + # TESTS @@ -141,3 +183,6 @@ target_link_libraries(test_cbor tinycbor) std_test(ctap_parse_client_pin src/ctap_parse_client_pin_test.cpp) target_link_libraries(test_ctap_parse_client_pin core_ctap_parse gtest_custom_assertions) + +std_test(ctap_client_pin src/ctap_client_pin_test.cpp) +target_link_libraries(test_ctap_client_pin core_ctap gtest_custom_assertions) diff --git a/test/src/ctap_client_pin_test.cpp b/test/src/ctap_client_pin_test.cpp new file mode 100644 index 0000000..f38124a --- /dev/null +++ b/test/src/ctap_client_pin_test.cpp @@ -0,0 +1,62 @@ +#include +#include +extern "C" { +#include +} + +namespace { + +class CtapClientPinTest : public testing::Test { +protected: + ctap_state_t ctap{}; + + CtapClientPinTest() { + ctap_init(&ctap); + } + +}; + +TEST_F(CtapClientPinTest, InvalidSubcommand) { + // 0x06 {1: 1, 2: 9} + const uint8_t request[] = "\x06\xa2\x01\x01\x02\x09"; + + uint8_t response_status_code; + uint16_t response_data_length; + uint8_t *response_data; + uint8_t status; + + status = ctap_request( + &ctap, + sizeof(request) - 1, request, + &response_status_code, &response_data_length, &response_data + ); + + EXPECT_EQ(status, CTAP2_ERR_INVALID_SUBCOMMAND); + EXPECT_EQ(response_status_code, status); + EXPECT_EQ(response_data_length, 0); +} + +TEST_F(CtapClientPinTest, GetPinRetries) { + // 0x06 {1: 1, 2: 1} + const uint8_t request[] = "\x06\xa2\x01\x01\x02\x01"; + + uint8_t response_status_code; + uint16_t response_data_length; + uint8_t *response_data; + uint8_t status; + + status = ctap_request( + &ctap, + sizeof(request) - 1, request, + &response_status_code, &response_data_length, &response_data + ); + + EXPECT_EQ(status, CTAP2_OK); + EXPECT_EQ(response_status_code, status); + // {3: 8, 4: false} + const uint8_t expected_response[] = "\xa2\x03\x08\x04\xF4"; + ASSERT_EQ(response_data_length, sizeof(expected_response) - 1); + EXPECT_SAME_BYTES_S(response_data_length, response_data, expected_response); +} + +} // namespace