// CTAP2 authenticator API (CTAP 2.0, with the CTAP 2.1 behaviors browsers rely on). #include #include "attestation.h" #include "cbor.h" #include "cred.h" #include "crypto.h" #include "ctap.h" #include "pin.h" #include "platform.h" #include "presence.h" #include "storage.h" #include "util.h" #define CMD_MAKE_CREDENTIAL 0x01 #define CMD_GET_ASSERTION 0x02 #define CMD_GET_INFO 0x04 #define CMD_CLIENT_PIN 0x06 #define CMD_RESET 0x07 #define CMD_GET_NEXT_ASSERTION 0x08 #define CMD_SELECTION 0x0b #define COSE_ALG_ES256 (-7) #define MAX_MSG_SIZE 2048 #define FLAG_UP 0x01 #define FLAG_UV 0x04 #define FLAG_AT 0x40 #define RESET_WINDOW_MS 10000 #define NEXT_ASSERTION_TIMEOUT_MS 30000 // rpIdHash | flags | counter | aaguid | credIdLen | credId | COSE key #define AUTH_DATA_HEADER 37 #define AUTH_DATA_MAX 256 // Strings point into the request buffer. typedef struct { const char *data; size_t len; } text_t; typedef struct { const uint8_t *data; size_t len; } bytes_t; typedef struct { bytes_t id; text_t name; text_t display_name; } user_t; typedef struct { bool rk, uv, up; bool rk_present, up_present; } options_t; // A PublicKeyCredentialDescriptor list, validated during parsing and walked again later. typedef struct { bool present; size_t count; cbor_reader_t items; } cred_list_t; // Discoverable credentials still to be returned by authenticatorGetNextAssertion. static struct { bool active; uint8_t rp_id_hash[32]; uint8_t client_data_hash[32]; uint8_t flags; int16_t slots[RK_SLOTS]; size_t count; size_t next; uint32_t last_ms; } pending; static bool text_eq(text_t t, const char *s) { size_t n = strlen(s); return t.len == n && memcmp(t.data, s, n) == 0; } static int read_text(cbor_reader_t *r, text_t *t) { return cbor_read_text(r, &t->data, &t->len); } static int read_bytes(cbor_reader_t *r, bytes_t *b) { return cbor_read_bytes(r, &b->data, &b->len); } static int read_hash(cbor_reader_t *r, const uint8_t **hash) { bytes_t b; CHECK(read_bytes(r, &b)); if (b.len != 32) return CTAP1_ERR_INVALID_LENGTH; *hash = b.data; return 0; } static int parse_rp(cbor_reader_t *r, text_t *rp_id) { size_t n; CHECK(cbor_read_map(r, &n)); for (size_t i = 0; i < n; i++) { text_t key; CHECK(read_text(r, &key)); if (text_eq(key, "id")) CHECK(read_text(r, rp_id)); else CHECK(cbor_skip(r)); } return rp_id->data ? 0 : CTAP2_ERR_MISSING_PARAMETER; } static int parse_user(cbor_reader_t *r, user_t *user) { size_t n; CHECK(cbor_read_map(r, &n)); for (size_t i = 0; i < n; i++) { text_t key; CHECK(read_text(r, &key)); if (text_eq(key, "id")) CHECK(read_bytes(r, &user->id)); else if (text_eq(key, "name")) CHECK(read_text(r, &user->name)); else if (text_eq(key, "displayName")) CHECK(read_text(r, &user->display_name)); else CHECK(cbor_skip(r)); } return user->id.data ? 0 : CTAP2_ERR_MISSING_PARAMETER; } static int parse_cred_params(cbor_reader_t *r, bool *es256) { size_t n; CHECK(cbor_read_array(r, &n)); for (size_t i = 0; i < n; i++) { size_t m; int64_t alg = 0; bool has_alg = false; text_t type = {0}; CHECK(cbor_read_map(r, &m)); for (size_t j = 0; j < m; j++) { text_t key; CHECK(read_text(r, &key)); if (text_eq(key, "alg")) { CHECK(cbor_read_int(r, &alg)); has_alg = true; } else if (text_eq(key, "type")) { CHECK(read_text(r, &type)); } else { CHECK(cbor_skip(r)); } } if (!has_alg || !type.data) return CTAP2_ERR_MISSING_PARAMETER; if (text_eq(type, "public-key") && alg == COSE_ALG_ES256) *es256 = true; } return 0; } static int read_descriptor(cbor_reader_t *r, bytes_t *id, bool *public_key) { size_t n; text_t type = {0}; id->data = NULL; CHECK(cbor_read_map(r, &n)); for (size_t i = 0; i < n; i++) { text_t key; CHECK(read_text(r, &key)); if (text_eq(key, "id")) CHECK(read_bytes(r, id)); else if (text_eq(key, "type")) CHECK(read_text(r, &type)); else CHECK(cbor_skip(r)); } if (!id->data || !type.data) return CTAP2_ERR_MISSING_PARAMETER; *public_key = text_eq(type, "public-key"); return 0; } static int parse_cred_list(cbor_reader_t *r, cred_list_t *list) { CHECK(cbor_read_array(r, &list->count)); list->present = true; list->items = *r; for (size_t i = 0; i < list->count; i++) { bytes_t id; bool public_key; CHECK(read_descriptor(r, &id, &public_key)); } return 0; } // Finds the first credential in the list that this device issued for the RP. static bool find_in_list(const cred_list_t *list, const uint8_t rp_id_hash[32], bytes_t *id, uint8_t priv[32]) { cbor_reader_t r = list->items; for (size_t i = 0; i < list->count; i++) { bool public_key; read_descriptor(&r, id, &public_key); // already validated if (public_key && cred_unwrap(rp_id_hash, id->data, id->len, priv)) return true; } return false; } static int parse_options(cbor_reader_t *r, options_t *opt) { size_t n; CHECK(cbor_read_map(r, &n)); for (size_t i = 0; i < n; i++) { text_t key; CHECK(read_text(r, &key)); if (text_eq(key, "rk")) { CHECK(cbor_read_bool(r, &opt->rk)); opt->rk_present = true; } else if (text_eq(key, "uv")) { CHECK(cbor_read_bool(r, &opt->uv)); } else if (text_eq(key, "up")) { CHECK(cbor_read_bool(r, &opt->up)); opt->up_present = true; } else { CHECK(cbor_skip(r)); } } return 0; } // Platforms send an empty pinUvAuthParam to ask "touch to pick this // authenticator"; after the touch, the answer says whether a PIN is set. static int empty_pin_auth(void) { CHECK(presence_wait()); return pin_is_set() ? CTAP2_ERR_PIN_INVALID : CTAP2_ERR_PIN_NOT_SET; } static int check_pin_auth(const bytes_t *pin_auth, uint64_t protocol, const uint8_t client_data_hash[32], uint8_t permission, const uint8_t rp_id_hash[32]) { if (!protocol) return CTAP2_ERR_MISSING_PARAMETER; return pin_check_token(protocol, pin_auth->data, pin_auth->len, client_data_hash, permission, rp_id_hash); } static void hash_rp_id(text_t rp_id, uint8_t out[32]) { crypto_part_t part = {rp_id.data, rp_id.len}; crypto_sha256(&part, 1, out); } // Copies at most `cap` bytes of UTF-8 without splitting a character. static uint8_t copy_text(char *dst, size_t cap, text_t src) { size_t n = src.len; if (!src.data) return 0; if (n > cap) { n = cap; while (n > 0 && ((uint8_t)src.data[n] & 0xc0) == 0x80) n--; } memcpy(dst, src.data, n); return (uint8_t)n; } static int store_resident(const uint8_t rp_id_hash[32], text_t rp_id, const user_t *user, const uint8_t cred_id[CRED_ID_LEN], uint32_t created) { static rk_record_t rec; memset(&rec, 0, sizeof(rec)); rec.created = created; memcpy(rec.rp_id_hash, rp_id_hash, 32); memcpy(rec.cred_id, cred_id, CRED_ID_LEN); rec.user_id_len = (uint8_t)user->id.len; memcpy(rec.user_id, user->id.data, user->id.len); rec.rp_id_len = copy_text(rec.rp_id, sizeof(rec.rp_id), rp_id); rec.user_name_len = copy_text(rec.user_name, sizeof(rec.user_name), user->name); rec.display_name_len = copy_text(rec.display_name, sizeof(rec.display_name), user->display_name); return storage_rk_save(&rec); } static void put_cose_key(cbor_writer_t *w, const uint8_t pub[64]) { cbor_put_map(w, 5); cbor_put_int(w, 1); // kty: EC2 cbor_put_int(w, 2); cbor_put_int(w, 3); // alg: ES256 cbor_put_int(w, COSE_ALG_ES256); cbor_put_int(w, -1); // crv: P-256 cbor_put_int(w, 1); cbor_put_int(w, -2); // x cbor_put_bytes(w, pub, 32); cbor_put_int(w, -3); // y cbor_put_bytes(w, pub + 32, 32); } static int make_credential(cbor_reader_t *r, cbor_writer_t *w) { const uint8_t *client_data_hash = NULL; text_t rp_id = {0}; user_t user = {0}; bool has_params = false, es256 = false; cred_list_t exclude = {0}; options_t opt = {.up = true}; bytes_t pin_auth = {0}; uint64_t pin_protocol = 0; size_t n; CHECK(cbor_read_map(r, &n)); for (size_t i = 0; i < n; i++) { uint64_t key; CHECK(cbor_read_uint_key(r, &key)); switch (key) { case 0x01: CHECK(read_hash(r, &client_data_hash)); break; case 0x02: CHECK(parse_rp(r, &rp_id)); break; case 0x03: CHECK(parse_user(r, &user)); break; case 0x04: CHECK(parse_cred_params(r, &es256)); has_params = true; break; case 0x05: CHECK(parse_cred_list(r, &exclude)); break; case 0x07: CHECK(parse_options(r, &opt)); break; case 0x08: CHECK(read_bytes(r, &pin_auth)); break; case 0x09: CHECK(cbor_read_uint(r, &pin_protocol)); break; default: CHECK(cbor_skip(r)); break; // extensions, ... } } if (!client_data_hash || !rp_id.data || !user.id.data || !has_params) return CTAP2_ERR_MISSING_PARAMETER; if (pin_auth.data && pin_auth.len == 0) return empty_pin_auth(); if (!es256) return CTAP2_ERR_UNSUPPORTED_ALGORITHM; if (opt.uv) return CTAP2_ERR_UNSUPPORTED_OPTION; if (opt.up_present && !opt.up) return CTAP2_ERR_INVALID_OPTION; if (user.id.len > RK_MAX_USER_ID) return CTAP1_ERR_INVALID_LENGTH; uint8_t rp_id_hash[32], priv[32], pub[64], cred_id[CRED_ID_LEN]; hash_rp_id(rp_id, rp_id_hash); // Once a PIN is set, every registration needs it (CTAP 2.0 behavior). bool uv = pin_auth.data != NULL; if (uv) CHECK(check_pin_auth(&pin_auth, pin_protocol, client_data_hash, PIN_PERMISSION_MC, rp_id_hash)); else if (pin_is_set()) return CTAP2_ERR_PUAT_REQUIRED; bytes_t excluded; if (exclude.present && find_in_list(&exclude, rp_id_hash, &excluded, priv)) { crypto_wipe(priv, sizeof(priv)); CHECK(presence_wait()); // the spec requires a touch before saying so return CTAP2_ERR_CREDENTIAL_EXCLUDED; } CHECK(presence_wait()); if (!cred_create(rp_id_hash, cred_id, priv, pub)) return CTAP2_ERR_PROCESSING; crypto_wipe(priv, sizeof(priv)); // only the attestation key signs here uint32_t counter = storage_counter_next(); if (opt.rk) CHECK(store_resident(rp_id_hash, rp_id, &user, cred_id, counter)); uint8_t auth_data[AUTH_DATA_MAX]; size_t len = 0; memcpy(auth_data, rp_id_hash, 32); len += 32; auth_data[len++] = FLAG_UP | FLAG_AT | (uv ? FLAG_UV : 0); put_be32(auth_data + len, counter); len += 4; memcpy(auth_data + len, skey_aaguid, 16); len += 16; auth_data[len++] = 0; auth_data[len++] = CRED_ID_LEN; memcpy(auth_data + len, cred_id, CRED_ID_LEN); len += CRED_ID_LEN; cbor_writer_t cose; cbor_writer_init(&cose, auth_data + len, sizeof(auth_data) - len); put_cose_key(&cose, pub); len += cose.len; // Packed attestation signed by the batch attestation key. uint8_t sig[P256_SIG_MAX_DER]; crypto_part_t signed_data[] = {{auth_data, len}, {client_data_hash, 32}}; size_t sig_len = crypto_p256_sign(attestation_key, signed_data, 2, sig); if (!sig_len) return CTAP2_ERR_PROCESSING; cbor_put_map(w, 3); cbor_put_uint(w, 0x01); cbor_put_cstr(w, "packed"); cbor_put_uint(w, 0x02); cbor_put_bytes(w, auth_data, len); cbor_put_uint(w, 0x03); cbor_put_map(w, 3); cbor_put_cstr(w, "alg"); cbor_put_int(w, COSE_ALG_ES256); cbor_put_cstr(w, "sig"); cbor_put_bytes(w, sig, sig_len); cbor_put_cstr(w, "x5c"); cbor_put_array(w, 1); cbor_put_bytes(w, attestation_cert, attestation_cert_len); return 0; } // Lists the RP's discoverable credentials, newest first. static size_t find_resident(const uint8_t rp_id_hash[32], int16_t slots[RK_SLOTS]) { static rk_record_t rec; uint32_t created[RK_SLOTS]; size_t n = 0; for (int slot = 0; slot < RK_SLOTS; slot++) { if (!storage_rk_read(slot, &rec) || memcmp(rec.rp_id_hash, rp_id_hash, 32) != 0) continue; size_t i = n++; for (; i > 0 && created[i - 1] < rec.created; i--) { slots[i] = slots[i - 1]; created[i] = created[i - 1]; } slots[i] = (int16_t)slot; created[i] = rec.created; } return n; } // Looks up the resident record for a credential ID, if there is one. static bool find_resident_by_id(const uint8_t rp_id_hash[32], bytes_t id, rk_record_t *rec) { for (int slot = 0; slot < RK_SLOTS; slot++) { if (storage_rk_read(slot, rec) && memcmp(rec->rp_id_hash, rp_id_hash, 32) == 0 && id.len == CRED_ID_LEN && memcmp(rec->cred_id, id.data, CRED_ID_LEN) == 0) return true; } return false; } static void put_user(cbor_writer_t *w, const rk_record_t *user, bool verified) { // Without user verification, only the user ID may be revealed. bool name = verified && user->user_name_len > 0; bool display_name = verified && user->display_name_len > 0; cbor_put_map(w, 1 + name + display_name); cbor_put_cstr(w, "id"); cbor_put_bytes(w, user->user_id, user->user_id_len); if (name) { cbor_put_cstr(w, "name"); cbor_put_text(w, user->user_name, user->user_name_len); } if (display_name) { cbor_put_cstr(w, "displayName"); cbor_put_text(w, user->display_name, user->display_name_len); } } static int write_assertion(cbor_writer_t *w, const uint8_t rp_id_hash[32], const uint8_t client_data_hash[32], uint8_t flags, const uint8_t *cred_id, size_t cred_id_len, const uint8_t priv[32], const rk_record_t *user, size_t number_of_credentials) { uint8_t auth_data[AUTH_DATA_HEADER]; uint8_t sig[P256_SIG_MAX_DER]; memcpy(auth_data, rp_id_hash, 32); auth_data[32] = flags; put_be32(auth_data + 33, storage_counter_next()); crypto_part_t signed_data[] = {{auth_data, sizeof(auth_data)}, {client_data_hash, 32}}; size_t sig_len = crypto_p256_sign(priv, signed_data, 2, sig); if (!sig_len) return CTAP2_ERR_PROCESSING; cbor_put_map(w, 3 + (user != NULL) + (number_of_credentials > 0)); cbor_put_uint(w, 0x01); cbor_put_map(w, 2); cbor_put_cstr(w, "id"); cbor_put_bytes(w, cred_id, cred_id_len); cbor_put_cstr(w, "type"); cbor_put_cstr(w, "public-key"); cbor_put_uint(w, 0x02); cbor_put_bytes(w, auth_data, sizeof(auth_data)); cbor_put_uint(w, 0x03); cbor_put_bytes(w, sig, sig_len); if (user) { cbor_put_uint(w, 0x04); put_user(w, user, flags & FLAG_UV); } if (number_of_credentials > 0) { cbor_put_uint(w, 0x05); cbor_put_uint(w, number_of_credentials); } return 0; } static int get_assertion(cbor_reader_t *r, cbor_writer_t *w) { static rk_record_t rec; text_t rp_id = {0}; const uint8_t *client_data_hash = NULL; cred_list_t allow = {0}; options_t opt = {.up = true}; bytes_t pin_auth = {0}; uint64_t pin_protocol = 0; size_t n; CHECK(cbor_read_map(r, &n)); for (size_t i = 0; i < n; i++) { uint64_t key; CHECK(cbor_read_uint_key(r, &key)); switch (key) { case 0x01: CHECK(read_text(r, &rp_id)); break; case 0x02: CHECK(read_hash(r, &client_data_hash)); break; case 0x03: CHECK(parse_cred_list(r, &allow)); break; case 0x05: CHECK(parse_options(r, &opt)); break; case 0x06: CHECK(read_bytes(r, &pin_auth)); break; case 0x07: CHECK(cbor_read_uint(r, &pin_protocol)); break; default: CHECK(cbor_skip(r)); break; } } if (!rp_id.data || !client_data_hash) return CTAP2_ERR_MISSING_PARAMETER; if (pin_auth.data && pin_auth.len == 0) return empty_pin_auth(); if (opt.uv || opt.rk_present) return CTAP2_ERR_UNSUPPORTED_OPTION; uint8_t rp_id_hash[32], priv[32]; bytes_t id; const rk_record_t *user = NULL; size_t count = 0; hash_rp_id(rp_id, rp_id_hash); uint8_t flags = opt.up ? FLAG_UP : 0; if (pin_auth.data) { CHECK(check_pin_auth(&pin_auth, pin_protocol, client_data_hash, PIN_PERMISSION_GA, rp_id_hash)); flags |= FLAG_UV; } if (allow.count > 0) { if (!find_in_list(&allow, rp_id_hash, &id, priv)) return CTAP2_ERR_NO_CREDENTIALS; if (find_resident_by_id(rp_id_hash, id, &rec)) user = &rec; } else { count = find_resident(rp_id_hash, pending.slots); if (count == 0 || !storage_rk_read(pending.slots[0], &rec)) return CTAP2_ERR_NO_CREDENTIALS; if (!cred_unwrap(rp_id_hash, rec.cred_id, CRED_ID_LEN, priv)) return CTAP2_ERR_NO_CREDENTIALS; id.data = rec.cred_id; id.len = CRED_ID_LEN; user = &rec; } int err = opt.up ? presence_wait() : CTAP2_OK; if (err) { crypto_wipe(priv, sizeof(priv)); return err; } err = write_assertion(w, rp_id_hash, client_data_hash, flags, id.data, id.len, priv, user, count > 1 ? count : 0); crypto_wipe(priv, sizeof(priv)); if (err) return err; if (count > 1) { pending.active = true; memcpy(pending.rp_id_hash, rp_id_hash, 32); memcpy(pending.client_data_hash, client_data_hash, 32); pending.flags = flags; pending.count = count; pending.next = 1; pending.last_ms = platform_millis(); } return 0; } static int get_next_assertion(cbor_writer_t *w) { static rk_record_t rec; uint8_t priv[32]; if (!pending.active || pending.next >= pending.count || platform_millis() - pending.last_ms > NEXT_ASSERTION_TIMEOUT_MS) { pending.active = false; return CTAP2_ERR_NOT_ALLOWED; } int slot = pending.slots[pending.next++]; pending.last_ms = platform_millis(); if (!storage_rk_read(slot, &rec) || !cred_unwrap(pending.rp_id_hash, rec.cred_id, CRED_ID_LEN, priv)) return CTAP2_ERR_NOT_ALLOWED; int err = write_assertion(w, pending.rp_id_hash, pending.client_data_hash, pending.flags, rec.cred_id, CRED_ID_LEN, priv, &rec, 0); crypto_wipe(priv, sizeof(priv)); return err; } static int get_info(cbor_writer_t *w) { cbor_put_map(w, 5); cbor_put_uint(w, 0x01); // versions cbor_put_array(w, 2); cbor_put_cstr(w, "U2F_V2"); cbor_put_cstr(w, "FIDO_2_0"); cbor_put_uint(w, 0x03); // aaguid cbor_put_bytes(w, skey_aaguid, 16); cbor_put_uint(w, 0x04); // options cbor_put_map(w, 5); cbor_put_cstr(w, "rk"); cbor_put_bool(w, true); cbor_put_cstr(w, "up"); cbor_put_bool(w, true); cbor_put_cstr(w, "plat"); cbor_put_bool(w, false); cbor_put_cstr(w, "clientPin"); cbor_put_bool(w, pin_is_set()); cbor_put_cstr(w, "pinUvAuthToken"); cbor_put_bool(w, true); cbor_put_uint(w, 0x05); // maxMsgSize cbor_put_uint(w, MAX_MSG_SIZE); cbor_put_uint(w, 0x06); // pinUvAuthProtocols, preferred first cbor_put_array(w, 2); cbor_put_uint(w, 2); cbor_put_uint(w, 1); return 0; } static int reset(void) { // Only allowed shortly after power-up, so malware can't wipe a plugged-in key. if (platform_millis() > RESET_WINDOW_MS) return CTAP2_ERR_NOT_ALLOWED; CHECK(presence_wait()); storage_reset(); pin_init(); return 0; } size_t ctap2_handle(const uint8_t *req, size_t len, uint8_t *resp, size_t resp_cap) { cbor_reader_t r; cbor_writer_t w; int status; cbor_reader_init(&r, req + 1, len - 1); cbor_writer_init(&w, resp + 1, resp_cap - 1); if (req[0] != CMD_GET_NEXT_ASSERTION) pending.active = false; switch (req[0]) { case CMD_MAKE_CREDENTIAL: status = make_credential(&r, &w); break; case CMD_GET_ASSERTION: status = get_assertion(&r, &w); break; case CMD_GET_INFO: status = get_info(&w); break; case CMD_CLIENT_PIN: status = pin_handle(&r, &w); break; case CMD_RESET: status = reset(); break; case CMD_GET_NEXT_ASSERTION: status = get_next_assertion(&w); break; case CMD_SELECTION: status = presence_wait(); break; default: status = CTAP1_ERR_INVALID_COMMAND; break; } if (status == CTAP2_OK && w.overflow) status = CTAP1_ERR_OTHER; resp[0] = (uint8_t)status; return status == CTAP2_OK ? 1 + w.len : 1; }