Files
Skey/src/ctap1.c
T
ckochandClaude Opus 5.5 6c7e6ab5f5 Skey: a FIDO2 security key on the Raspberry Pi Pico
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>
2026-10-14 15:07:38 -04:00

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);
}
}