Firmware for the Pico / Pico 2 that acts as a USB FIDO2 authenticator: CTAP2 and U2F, passkeys, BOOTSEL as the touch button, and clientPIN with PIN/UV auth protocols 1 and 2. A host simulator runs the same core so the python-fido2 test suite exercises it without hardware. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
615 lines
20 KiB
C
615 lines
20 KiB
C
// CTAP2 authenticator API (CTAP 2.0, with the CTAP 2.1 behaviors browsers rely on).
|
|
#include <string.h>
|
|
|
|
#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;
|
|
}
|