From 79b43a90fdb13715ceb48109568edbca8f389f38 Mon Sep 17 00:00:00 2001 From: Radoslav Gerganov Date: Wed, 4 Mar 2020 18:20:35 +0200 Subject: [PATCH] Implement commands for management of resident keys Implement command 0x41 which is used by OpenSSH for reading RKs. It has the following subcommands: * CMD_CRED_METADATA - get number of saved/remaining RKs * CMD_RP_BEGIN/CMD_RP_NEXT - iterate over the saved RPs * CMD_RK_BEGIN/CMD_RK_NEXT - iterate over the RKs for a given RP Fixes issue #374 and issue #314 --- fido2/ctap.c | 312 ++++++++++++++++++++++++++++++++++++++++++++- fido2/ctap.h | 21 +++ fido2/ctap_parse.c | 136 ++++++++++++++++++++ fido2/ctap_parse.h | 1 + fido2/log.c | 1 + fido2/log.h | 1 + 6 files changed, 468 insertions(+), 4 deletions(-) diff --git a/fido2/ctap.c b/fido2/ctap.c index 358d294..5a359ec 100644 --- a/fido2/ctap.c +++ b/fido2/ctap.c @@ -38,12 +38,13 @@ static void ctap_reset_key_agreement(); struct _getAssertionState getAssertionState; -uint8_t verify_pin_auth(uint8_t * pinAuth, uint8_t * clientDataHash) + +static uint8_t verify_pin_auth_ex(uint8_t * pinAuth, uint8_t *buf, size_t len) { uint8_t hmac[32]; crypto_sha256_hmac_init(PIN_TOKEN, PIN_TOKEN_SIZE, hmac); - crypto_sha256_update(clientDataHash, CLIENT_DATA_HASH_SIZE); + crypto_sha256_update(buf, len); crypto_sha256_hmac_final(PIN_TOKEN, PIN_TOKEN_SIZE, hmac); if (memcmp(pinAuth, hmac, 16) == 0) @@ -57,10 +58,12 @@ uint8_t verify_pin_auth(uint8_t * pinAuth, uint8_t * clientDataHash) dump_hex1(TAG_ERR,hmac,16); return CTAP2_ERR_PIN_AUTH_INVALID; } - } - +uint8_t verify_pin_auth(uint8_t * pinAuth, uint8_t * clientDataHash) +{ + return verify_pin_auth_ex(pinAuth, clientDataHash, CLIENT_DATA_HASH_SIZE); +} uint8_t ctap_get_info(CborEncoder * encoder) { @@ -1154,6 +1157,298 @@ uint8_t ctap_get_next_assertion(CborEncoder * encoder) return 0; } +uint8_t ctap_cred_metadata(CborEncoder * encoder) +{ + CborEncoder map; + int ret = cbor_encoder_create_map(encoder, &map, 2); + check_ret(ret); + ret = cbor_encode_int(&map, 1); + check_ret(ret); + ret = cbor_encode_int(&map, STATE.rk_stored); + check_ret(ret); + ret = cbor_encode_int(&map, 2); + check_ret(ret); + int remaining_rks = ctap_rk_size() - STATE.rk_stored; + ret = cbor_encode_int(&map, remaining_rks); + check_ret(ret); + ret = cbor_encoder_close_container(encoder, &map); + check_ret(ret); + return 0; +} + +uint8_t ctap_cred_rp(CborEncoder * encoder, int rk_ind, int rp_count) +{ + CTAP_residentKey rk; + ctap_load_rk(rk_ind, &rk); + // SHA256 hash of "ssh:" + uint8_t ssh_hash[32] = {0xe3, 0x06, 0x10, 0xe8, 0xa1, 0x62, 0x11, 0x59, + 0x60, 0xfe, 0x1e, 0xc2, 0x23, 0xe6, 0x52, 0x9c, + 0x9f, 0x4b, 0x6e, 0x80, 0x20, 0x0d, 0xcb, 0x5e, + 0x5c, 0x32, 0x1c, 0x8a, 0xf1, 0xe2, 0xb1, 0xbf}; + + CborEncoder map; + size_t map_size = rp_count > 0 ? 3 : 2; + int ret = cbor_encoder_create_map(encoder, &map, map_size); + check_ret(ret); + ret = cbor_encode_int(&map, 3); + check_ret(ret); + { + CborEncoder rp; + ret = cbor_encoder_create_map(&map, &rp, 2); + check_ret(ret); + ret = cbor_encode_text_stringz(&rp, "id"); + check_ret(ret); + if (memcmp(ssh_hash, rk.id.rpIdHash, 32) == 0) { + // recover the rpId from the hash because Solo stores only the hash :( + ret = cbor_encode_text_stringz(&rp, "ssh:"); + } else { + ret = cbor_encode_text_stringz(&rp, ""); + } + check_ret(ret); + ret = cbor_encode_text_stringz(&rp, "name"); + check_ret(ret); + ret = cbor_encode_text_stringz(&rp, rk.user.name); + check_ret(ret); + ret = cbor_encoder_close_container(&map, &rp); + check_ret(ret); + } + ret = cbor_encode_int(&map, 4); + check_ret(ret); + cbor_encode_byte_string(&map, rk.id.rpIdHash, 32); + check_ret(ret); + if (rp_count > 0) + { + ret = cbor_encode_int(&map, 5); + check_ret(ret); + ret = cbor_encode_int(&map, rp_count); + check_ret(ret); + } + ret = cbor_encoder_close_container(encoder, &map); + check_ret(ret); + return 0; +} + +uint8_t ctap_cred_rk(CborEncoder * encoder, int rk_ind, int rk_count) +{ + CTAP_residentKey rk; + ctap_load_rk(rk_ind, &rk); + + CborEncoder map; + size_t map_size = rk_count > 0 ? 4 : 3; + int ret = cbor_encoder_create_map(encoder, &map, map_size); + check_ret(ret); + ret = cbor_encode_int(&map, 6); + check_ret(ret); + { + CborEncoder usr; + ret = cbor_encoder_create_map(&map, &usr, 4); + check_ret(ret); + ret = cbor_encode_text_stringz(&usr, "id"); + check_ret(ret); + ret = cbor_encode_byte_string(&usr, rk.user.id, rk.user.id_size); + check_ret(ret); + ret = cbor_encode_text_stringz(&usr, "icon"); + check_ret(ret); + ret = cbor_encode_text_stringz(&usr, rk.user.icon); + check_ret(ret); + ret = cbor_encode_text_stringz(&usr, "name"); + check_ret(ret); + ret = cbor_encode_text_stringz(&usr, rk.user.name); + check_ret(ret); + ret = cbor_encode_text_stringz(&usr, "displayName"); + check_ret(ret); + ret = cbor_encode_text_stringz(&usr, rk.user.displayName); + check_ret(ret); + ret = cbor_encoder_close_container(&map, &usr); + check_ret(ret); + } + + ret = cbor_encode_int(&map, 7); + check_ret(ret); + { + CborEncoder credId; + ret = cbor_encoder_create_map(&map, &credId, 1); + check_ret(ret); + ret = cbor_encode_text_stringz(&credId, "id"); + check_ret(ret); + ret = cbor_encode_byte_string(&credId, (uint8_t*)&rk.id, sizeof(CredentialId)); + check_ret(ret); + ret = cbor_encoder_close_container(&map, &credId); + check_ret(ret); + } + + ret = cbor_encode_int(&map, 8); + check_ret(ret); + ctap_generate_cose_key(&map, (uint8_t*)&rk.id, sizeof(CredentialId), PUB_KEY_CRED_PUB_KEY, COSE_ALG_ES256); + + if (rk_count > 0) + { + ret = cbor_encode_int(&map, 9); + check_ret(ret); + ret = cbor_encode_int(&map, rk_count); + check_ret(ret); + } + ret = cbor_encoder_close_container(encoder, &map); + check_ret(ret); + return 0; +} + +uint8_t ctap_cred_mgmt_pinauth(CTAP_credMgmt *CM) +{ + uint8_t ret = 0; + if (CM->cmd != CM_cmdMetadata && CM->cmd != CM_cmdRPBegin && CM->cmd != CM_cmdRKBegin) + { + // pinAuth is not required for other commands + return 0; + } + if (CM->pinProtocol != 1) + { + return CTAP1_ERR_OTHER; + } + if (CM->cmd == CM_cmdMetadata || CM->cmd == CM_cmdRPBegin) + { + uint8_t cmd = (uint8_t) CM->cmd; + ret = verify_pin_auth_ex(CM->pinAuth, &cmd, 1); + } + else if (CM->cmd == CM_cmdRKBegin) + { + uint8_t params[5 + sizeof(CM->rpIdHash)] = {CM->cmd, 0xa1, 0x01, 0x58, 0x20}; + if (CM->pinProtocol != 1) + { + return CTAP1_ERR_OTHER; + } + memcpy(¶ms[5], CM->rpIdHash, sizeof(CM->rpIdHash)); + ret = verify_pin_auth_ex(CM->pinAuth, params, sizeof(params)); + } + if (ret == CTAP2_ERR_PIN_AUTH_INVALID) + { + ctap_decrement_pin_attempts(); + if (ctap_device_boot_locked()) + { + return CTAP2_ERR_PIN_AUTH_BLOCKED; + } + return CTAP2_ERR_PIN_AUTH_INVALID; + } + else + { + ctap_reset_pin_attempts(); + } + return ret; +} + +uint8_t ctap_cred_mgmt(CborEncoder * encoder, uint8_t * request, int length) +{ + CTAP_credMgmt CM; + CTAP_residentKey rk; + int i = 0; + // use the same index for both RP and RK commands, it make things simpler + static int curr_rk_ind = 0; + // keep the rpIdHash specified in CM_cmdRKBegin cause it's not present in CM_cmdRKNext + static uint8_t rpIdHash[32]; + // flag that authenticated RPBegin was received + static bool rp_auth = false; + // flag that authenticated RKBegin was received + static bool rk_auth = false; + // number of stored RPs + int rp_count = 0; + // number of RKs with the specified rpIdHash + int rk_count = 0; + + int ret = ctap_parse_cred_mgmt(&CM, request, length); + if (ret != 0) + { + printf2(TAG_ERR,"error, ctap_parse_cred_mgmt failed\n"); + return ret; + } + ret = ctap_cred_mgmt_pinauth(&CM); + check_retr(ret); + if (STATE.rk_stored == 0 && CM.cmd != CM_cmdMetadata) + { + printf2(TAG_ERR,"No resident keys\n"); + return CTAP2_ERR_NO_CREDENTIALS; + } + if (CM.cmd == CM_cmdRPBegin) + { + curr_rk_ind = 0; + rp_count = STATE.rk_stored; + rp_auth = true; + rk_auth = false; + } + if (CM.cmd == CM_cmdRKBegin) + { + curr_rk_ind = 0; + rk_auth = true; + rp_auth = false; + // store the specified hash, we will need it for CM_cmdRKNext + memcpy(rpIdHash, CM.rpIdHash, 32); + // count how many RKs have this hash + for (i = 0; i < STATE.rk_stored; i++) + { + ctap_load_rk(i, &rk); + if (memcmp(rk.id.rpIdHash, rpIdHash, 32) == 0) + { + rk_count++; + } + } + } + if (curr_rk_ind >= STATE.rk_stored) + { + printf2(TAG_ERR,"No more resident keys\n"); + rk_auth = false; + rp_auth = false; + return CTAP2_ERR_NO_CREDENTIALS; + } + if (CM.cmd == CM_cmdRPNext && !rp_auth) + { + printf2(TAG_ERR, "RPNext without RPBegin\n"); + return CTAP2_ERR_NO_CREDENTIALS; + } + if (CM.cmd == CM_cmdRKNext && !rk_auth) + { + printf2(TAG_ERR, "RKNext without RKBegin\n"); + return CTAP2_ERR_NO_CREDENTIALS; + } + if (CM.cmd == CM_cmdRKBegin || CM.cmd == CM_cmdRKNext) + { + ctap_load_rk(curr_rk_ind, &rk); + // skip resident keys with different rpIdHash + while (memcmp(rk.id.rpIdHash, rpIdHash, 32) != 0 && curr_rk_ind < STATE.rk_stored) + { + curr_rk_ind++; + ctap_load_rk(curr_rk_ind, &rk); + } + if (curr_rk_ind == STATE.rk_stored) + { + printf2(TAG_ERR,"No more resident keys with this rpIdHash\n"); + rk_auth = false; + return CTAP2_ERR_NO_CREDENTIALS; + } + } + switch (CM.cmd) + { + case CM_cmdMetadata: + ret = ctap_cred_metadata(encoder); + check_ret(ret); + break; + case CM_cmdRPBegin: + case CM_cmdRPNext: + ret = ctap_cred_rp(encoder, curr_rk_ind, rp_count); + check_ret(ret); + curr_rk_ind++; + break; + case CM_cmdRKBegin: + case CM_cmdRKNext: + ret = ctap_cred_rk(encoder, curr_rk_ind, rk_count); + check_ret(ret); + curr_rk_ind++; + break; + default: + printf2(TAG_ERR, "error, invalid credMgmt cmd: 0x%02x\n", CM.cmd); + return CTAP1_ERR_INVALID_COMMAND; + } + return 0; +} + uint8_t ctap_get_assertion(CborEncoder * encoder, uint8_t * request, int length) { CTAP_getAssertion GA; @@ -1641,6 +1936,7 @@ uint8_t ctap_request(uint8_t * pkt_raw, int length, CTAP_RESPONSE * resp) { case CTAP_MAKE_CREDENTIAL: case CTAP_GET_ASSERTION: + case CTAP_CBOR_CRED_MGMT_PRE: if (ctap_device_locked()) { status = CTAP2_ERR_PIN_BLOCKED; @@ -1722,6 +2018,14 @@ uint8_t ctap_request(uint8_t * pkt_raw, int length, CTAP_RESPONSE * resp) status = CTAP2_ERR_NOT_ALLOWED; } break; + case CTAP_CBOR_CRED_MGMT_PRE: + printf1(TAG_CTAP,"CTAP_CBOR_CRED_MGMT_PRE\n"); + status = ctap_cred_mgmt(&encoder, pkt_raw, length); + + resp->length = cbor_encoder_get_buffer_size(&encoder, buf); + + dump_hex1(TAG_DUMP,buf, resp->length); + break; default: status = CTAP1_ERR_INVALID_COMMAND; printf2(TAG_ERR,"error, invalid cmd: 0x%02x\n", cmd); diff --git a/fido2/ctap.h b/fido2/ctap.h index 1872df4..90c05dc 100644 --- a/fido2/ctap.h +++ b/fido2/ctap.h @@ -17,6 +17,7 @@ #define CTAP_RESET 0x07 #define GET_NEXT_ASSERTION 0x08 #define CTAP_VENDOR_FIRST 0x40 +#define CTAP_CBOR_CRED_MGMT_PRE 0x41 #define CTAP_VENDOR_LAST 0xBF #define MC_clientDataHash 0x01 @@ -37,6 +38,16 @@ #define GA_pinAuth 0x06 #define GA_pinProtocol 0x07 +#define CM_cmd 0x01 + #define CM_cmdMetadata 0x01 + #define CM_cmdRPBegin 0x02 + #define CM_cmdRPNext 0x03 + #define CM_cmdRKBegin 0x04 + #define CM_cmdRKNext 0x05 +#define CM_rpIdHash 0x02 +#define CM_pinProtocol 0x03 +#define CM_pinAuth 0x04 + #define CP_pinProtocol 0x01 #define CP_subCommand 0x02 #define CP_cmdGetRetries 0x01 @@ -285,6 +296,16 @@ typedef struct } CTAP_getAssertion; +typedef struct +{ + int cmd; + uint8_t rpIdHash[32]; + uint8_t pinAuth[16]; + uint8_t pinAuthPresent; + int pinProtocol; +} CTAP_credMgmt; + + typedef struct { int pinProtocol; diff --git a/fido2/ctap_parse.c b/fido2/ctap_parse.c index 4a2f316..480d365 100644 --- a/fido2/ctap_parse.c +++ b/fido2/ctap_parse.c @@ -999,6 +999,142 @@ uint8_t parse_allow_list(CTAP_getAssertion * GA, CborValue * it) return 0; } +static uint8_t parse_rpid_hash(CborValue * val, CTAP_credMgmt * CM) +{ + size_t map_length; + int key; + int ret; + unsigned int i; + CborValue map; + size_t sz = 32; + + if (cbor_value_get_type(val) != CborMapType) + { + printf2(TAG_ERR,"error, wrong type\n"); + 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) + { + printf2(TAG_ERR,"Error, expecting integer type for map key, got %s\n", cbor_value_get_type_string(&map)); + 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 1: + ret = cbor_value_copy_byte_string(&map, CM->rpIdHash, &sz, NULL); + if (ret == CborErrorOutOfMemory) + { + printf2(TAG_ERR,"Error, map key is too large\n"); + return CTAP2_ERR_LIMIT_EXCEEDED; + } + check_ret(ret); + break; + } + ret = cbor_value_advance(&map); + check_ret(ret); + } + return 0; +} + +uint8_t ctap_parse_cred_mgmt(CTAP_credMgmt * CM, uint8_t * request, int length) +{ + int ret; + unsigned int i; + int key; + size_t map_length; + CborParser parser; + CborValue it,map; + + memset(CM, 0, sizeof(CTAP_credMgmt)); + ret = cbor_parser_init(request, length, CborValidateCanonicalFormat, &parser, &it); + check_ret(ret); + + CborType type = cbor_value_get_type(&it); + if (type != CborMapType) + { + printf2(TAG_ERR,"Error, expecting cbor map\n"); + 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); + + printf1(TAG_CM, "CM map has %d elements\n", map_length); + + for (i = 0; i < map_length; i++) + { + type = cbor_value_get_type(&map); + if (type != CborIntegerType) + { + printf2(TAG_ERR,"Error, expecting int for map key\n"); + 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 CM_cmd: + printf1(TAG_CM, "CM_cmd\n"); + if (cbor_value_get_type(&map) == CborIntegerType) + { + ret = cbor_value_get_int_checked(&map, &CM->cmd); + check_ret(ret); + } + else + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + break; + case CM_rpIdHash: + printf1(TAG_CM, "CM_rpIdHash\n"); + ret = parse_rpid_hash(&map, CM); + check_ret(ret); + dump_hex1(TAG_CM, CM->rpIdHash, 32); + break; + case CM_pinProtocol: + printf1(TAG_CM, "CM_pinProtocol\n"); + if (cbor_value_get_type(&map) == CborIntegerType) + { + ret = cbor_value_get_int_checked(&map, &CM->pinProtocol); + check_ret(ret); + } + else + { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + break; + case CM_pinAuth: + printf1(TAG_CM, "CM_pinAuth\n"); + ret = parse_fixed_byte_string(&map, CM->pinAuth, 16); + check_retr(ret); + CM->pinAuthPresent = 1; + break; + } + ret = cbor_value_advance(&map); + check_ret(ret); + } + + return 0; +} uint8_t ctap_parse_get_assertion(CTAP_getAssertion * GA, uint8_t * request, int length) { diff --git a/fido2/ctap_parse.h b/fido2/ctap_parse.h index 38aa398..e0be807 100644 --- a/fido2/ctap_parse.h +++ b/fido2/ctap_parse.h @@ -35,6 +35,7 @@ uint8_t parse_cose_key(CborValue * it, COSE_key * cose); uint8_t ctap_parse_make_credential(CTAP_makeCredential * MC, CborEncoder * encoder, uint8_t * request, int length); uint8_t ctap_parse_get_assertion(CTAP_getAssertion * GA, uint8_t * request, int length); +uint8_t ctap_parse_cred_mgmt(CTAP_credMgmt * CM, uint8_t * request, int length); uint8_t ctap_parse_client_pin(CTAP_clientPin * CP, uint8_t * request, int length); uint8_t parse_credential_descriptor(CborValue * arr, CTAP_credentialDescriptor * cred); diff --git a/fido2/log.c b/fido2/log.c index af2cd55..bb9a41a 100644 --- a/fido2/log.c +++ b/fido2/log.c @@ -51,6 +51,7 @@ struct logtag tagtable[] = { {TAG_NFC,"NFC"}, {TAG_NFC_APDU, "NAPDU"}, {TAG_CCID, "CCID"}, + {TAG_CM, "CRED_MGMT"}, }; diff --git a/fido2/log.h b/fido2/log.h index 8e33648..d415255 100644 --- a/fido2/log.h +++ b/fido2/log.h @@ -48,6 +48,7 @@ typedef enum TAG_NFC = (1 << 19), TAG_NFC_APDU = (1 << 20), TAG_CCID = (1 << 21), + TAG_CM = (1 << 22), TAG_NO_TAG = (1UL << 30), TAG_FILENO = (1UL << 31)