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>
130 lines
4.4 KiB
C
130 lines
4.4 KiB
C
// U2F / CTAP1 over CTAPHID_MSG (FIDO U2F Raw Message Formats v1.2).
|
|
#include <string.h>
|
|
|
|
#include "attestation.h"
|
|
#include "cred.h"
|
|
#include "crypto.h"
|
|
#include "ctap.h"
|
|
#include "presence.h"
|
|
#include "storage.h"
|
|
#include "util.h"
|
|
|
|
#define U2F_REGISTER 0x01
|
|
#define U2F_AUTHENTICATE 0x02
|
|
#define U2F_VERSION 0x03
|
|
|
|
#define AUTH_ENFORCE_PRESENCE 0x03
|
|
#define AUTH_CHECK_ONLY 0x07
|
|
#define AUTH_DONT_ENFORCE_PRESENCE 0x08
|
|
|
|
#define SW_NO_ERROR 0x9000
|
|
#define SW_WRONG_LENGTH 0x6700
|
|
#define SW_CONDITIONS_NOT_SATISFIED 0x6985
|
|
#define SW_WRONG_DATA 0x6a80
|
|
#define SW_INS_NOT_SUPPORTED 0x6d00
|
|
#define SW_CLA_NOT_SUPPORTED 0x6e00
|
|
#define SW_UNKNOWN 0x6f00
|
|
|
|
static size_t status(uint8_t *resp, size_t len, uint16_t sw) {
|
|
resp[len] = (uint8_t)(sw >> 8);
|
|
resp[len + 1] = (uint8_t)sw;
|
|
return len + 2;
|
|
}
|
|
|
|
static size_t u2f_register(const uint8_t *data, size_t lc, uint8_t *resp) {
|
|
const uint8_t *challenge = data, *app_id = data + 32;
|
|
uint8_t cred_id[CRED_ID_LEN], priv[32], pub[64];
|
|
static const uint8_t reserved = 0x00;
|
|
|
|
if (lc != 64) return status(resp, 0, SW_WRONG_LENGTH);
|
|
if (!presence_check()) return status(resp, 0, SW_CONDITIONS_NOT_SATISFIED);
|
|
if (!cred_create(app_id, cred_id, priv, pub)) return status(resp, 0, SW_UNKNOWN);
|
|
crypto_wipe(priv, sizeof(priv));
|
|
|
|
// 0x05 | public key | key handle length | key handle | certificate | signature
|
|
size_t len = 0;
|
|
resp[len++] = 0x05;
|
|
resp[len++] = 0x04; // uncompressed point
|
|
memcpy(resp + len, pub, 64);
|
|
len += 64;
|
|
resp[len++] = CRED_ID_LEN;
|
|
memcpy(resp + len, cred_id, CRED_ID_LEN);
|
|
len += CRED_ID_LEN;
|
|
memcpy(resp + len, attestation_cert, attestation_cert_len);
|
|
len += attestation_cert_len;
|
|
|
|
crypto_part_t signed_data[] = {
|
|
{&reserved, 1}, {app_id, 32}, {challenge, 32}, {cred_id, CRED_ID_LEN}, {resp + 1, 65},
|
|
};
|
|
size_t sig_len = crypto_p256_sign(attestation_key, signed_data, 5, resp + len);
|
|
if (!sig_len) return status(resp, 0, SW_UNKNOWN);
|
|
return status(resp, len + sig_len, SW_NO_ERROR);
|
|
}
|
|
|
|
static size_t u2f_authenticate(uint8_t mode, const uint8_t *data, size_t lc, uint8_t *resp) {
|
|
const uint8_t *challenge = data, *app_id = data + 32, *key_handle = data + 65;
|
|
uint8_t priv[32];
|
|
|
|
if (lc < 65 || lc != 65u + data[64]) return status(resp, 0, SW_WRONG_LENGTH);
|
|
if (mode != AUTH_ENFORCE_PRESENCE && mode != AUTH_CHECK_ONLY && mode != AUTH_DONT_ENFORCE_PRESENCE)
|
|
return status(resp, 0, SW_WRONG_DATA);
|
|
if (!cred_unwrap(app_id, key_handle, data[64], priv)) return status(resp, 0, SW_WRONG_DATA);
|
|
|
|
// Check-only answers "this key handle is mine" with a presence error.
|
|
uint16_t sw = SW_NO_ERROR;
|
|
uint8_t flags = 0;
|
|
if (mode == AUTH_CHECK_ONLY) {
|
|
sw = SW_CONDITIONS_NOT_SATISFIED;
|
|
} else if (mode == AUTH_ENFORCE_PRESENCE) {
|
|
if (presence_check())
|
|
flags = 0x01;
|
|
else
|
|
sw = SW_CONDITIONS_NOT_SATISFIED;
|
|
}
|
|
if (sw != SW_NO_ERROR) {
|
|
crypto_wipe(priv, sizeof(priv));
|
|
return status(resp, 0, sw);
|
|
}
|
|
|
|
// user presence | counter | signature
|
|
resp[0] = flags;
|
|
put_be32(resp + 1, storage_counter_next());
|
|
crypto_part_t signed_data[] = {{app_id, 32}, {resp, 5}, {challenge, 32}};
|
|
size_t sig_len = crypto_p256_sign(priv, signed_data, 3, resp + 5);
|
|
crypto_wipe(priv, sizeof(priv));
|
|
if (!sig_len) return status(resp, 0, SW_UNKNOWN);
|
|
return status(resp, 5 + sig_len, SW_NO_ERROR);
|
|
}
|
|
|
|
size_t ctap1_handle(const uint8_t *req, size_t len, uint8_t *resp) {
|
|
const uint8_t *data = req + 4;
|
|
size_t lc = 0;
|
|
|
|
if (len < 4) return status(resp, 0, SW_WRONG_LENGTH);
|
|
if (len > 4) {
|
|
if (req[4] == 0 && len >= 7) { // extended length: 00 Lc1 Lc2
|
|
lc = (size_t)req[5] << 8 | req[6];
|
|
data = req + 7;
|
|
} else {
|
|
lc = req[4];
|
|
data = req + 5;
|
|
}
|
|
// Anything past the data is Le, which we don't need.
|
|
if (lc > len - (size_t)(data - req)) return status(resp, 0, SW_WRONG_LENGTH);
|
|
}
|
|
if (req[0] != 0) return status(resp, 0, SW_CLA_NOT_SUPPORTED);
|
|
|
|
switch (req[1]) {
|
|
case U2F_REGISTER:
|
|
return u2f_register(data, lc, resp);
|
|
case U2F_AUTHENTICATE:
|
|
return u2f_authenticate(req[2], data, lc, resp);
|
|
case U2F_VERSION:
|
|
if (lc != 0) return status(resp, 0, SW_WRONG_LENGTH);
|
|
memcpy(resp, "U2F_V2", 6);
|
|
return status(resp, 6, SW_NO_ERROR);
|
|
default:
|
|
return status(resp, 0, SW_INS_NOT_SUPPORTED);
|
|
}
|
|
}
|