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>
This commit is contained in:
ckochandClaude Opus 5.5 committed 2026-10-14 15:07:38 -04:00
commit 6c7e6ab5f5
41 files changed
+4218

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
build*/
deps/
# Attestation private key, created on first build.
attestation/
skey_flash.bin
__pycache__/
.pytest_cache/
+82
View File
@@ -0,0 +1,82 @@
cmake_minimum_required(VERSION 3.13)
# Two builds share the authenticator core in src/:
# default Pico firmware (pass -DPICO_BOARD=pico2 for a Pico 2)
# -DSKEY_HOST=ON skey_sim, a desktop simulator driven by the tests in tests/
option(SKEY_HOST "Build the host simulator instead of the Pico firmware" OFF)
if (SKEY_HOST)
project(skey C)
else()
if (NOT PICO_SDK_PATH)
if (DEFINED ENV{PICO_SDK_PATH})
set(PICO_SDK_PATH $ENV{PICO_SDK_PATH})
else()
set(PICO_SDK_PATH ${CMAKE_CURRENT_LIST_DIR}/deps/pico-sdk)
endif()
endif()
include(${PICO_SDK_PATH}/pico_sdk_init.cmake)
project(skey C CXX ASM)
pico_sdk_init()
endif()
set(CMAKE_C_STANDARD 11)
find_package(Python3 REQUIRED COMPONENTS Interpreter)
set(GENERATED_DIR ${CMAKE_CURRENT_BINARY_DIR}/generated)
add_custom_command(
OUTPUT ${GENERATED_DIR}/attestation_data.c
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/gen_attestation.py
--key-dir ${CMAKE_CURRENT_SOURCE_DIR}/attestation
--out ${GENERATED_DIR}/attestation_data.c
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/tools/gen_attestation.py
COMMENT "Generating attestation certificate")
set(CORE_SOURCES
src/cbor.c
src/cred.c
src/crypto.c
src/ctap1.c
src/ctap2.c
src/ctaphid.c
src/pin.c
src/presence.c
src/storage.c)
set(WARNINGS -Wall -Wextra)
if (SKEY_HOST)
option(SKEY_SANITIZE "Build the simulator with ASan and UBSan" ON)
find_path(MBEDTLS_INCLUDE_DIR mbedtls/ecdsa.h)
find_library(MBEDCRYPTO_LIBRARY mbedcrypto)
if (NOT MBEDTLS_INCLUDE_DIR OR NOT MBEDCRYPTO_LIBRARY)
message(FATAL_ERROR "mbedtls not found (apt install libmbedtls-dev)")
endif()
add_executable(skey_sim host/main.c host/platform_host.c ${CORE_SOURCES} ${GENERATED_DIR}/attestation_data.c)
target_include_directories(skey_sim PRIVATE src host ${MBEDTLS_INCLUDE_DIR})
target_link_libraries(skey_sim PRIVATE ${MBEDCRYPTO_LIBRARY})
target_compile_options(skey_sim PRIVATE ${WARNINGS})
if (SKEY_SANITIZE)
target_compile_options(skey_sim PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer)
target_link_options(skey_sim PRIVATE -fsanitize=address,undefined)
endif()
else()
set(PICO_SOURCES firmware/button.c firmware/main.c firmware/platform_pico.c firmware/usb.c)
add_executable(skey ${PICO_SOURCES} ${CORE_SOURCES} ${GENERATED_DIR}/attestation_data.c)
target_include_directories(skey PRIVATE src firmware)
target_link_libraries(skey PRIVATE
pico_stdlib
pico_rand
pico_unique_id
hardware_flash
tinyusb_device
pico_mbedtls)
# The deepest path (makeCredential -> mbedtls ECDSA) needs ~2.3 KB of stack,
# more than the SDK's 2 KB default. Reserve all of SCRATCH_Y instead.
target_compile_definitions(skey PRIVATE PICO_STACK_SIZE=0x1000)
# Only our own sources: the SDK and mbedtls compile into this target too.
set_source_files_properties(${PICO_SOURCES} ${CORE_SOURCES} PROPERTIES COMPILE_OPTIONS "${WARNINGS}")
pico_enable_stdio_usb(skey 0)
pico_enable_stdio_uart(skey 0)
pico_add_extra_outputs(skey)
endif()
+147
View File
@@ -0,0 +1,147 @@
# Skey
A FIDO2 / WebAuthn security key built on a Raspberry Pi Pico or Pico 2. It
plugs in over USB and works with browsers for passkeys and 2FA, and with
`ssh-keygen -t ecdsa-sk`. No drivers are needed.
## What works
- CTAP2: makeCredential, getAssertion, getNextAssertion, getInfo, reset, selection
- U2F (CTAP1) register/authenticate, so older sites work too
- ES256 (P-256) credentials with packed attestation
- Up to 64 discoverable credentials (passkeys)
- Touch to confirm, using the Pico's BOOTSEL button
- PIN (clientPIN, PIN/UV auth protocols 1 and 2), for sites that require user
verification
- Unlimited non-discoverable credentials. The credential ID carries everything
needed to re-derive the key, so nothing is stored for them.
Not yet supported: hmac-secret, credential management.
## Layout
```
src/ authenticator core: CTAPHID, CTAP1, CTAP2, CBOR, crypto, storage
firmware/ Pico specifics: USB descriptors, flash, RNG, LED, BOOTSEL button
host/ desktop simulator speaking CTAPHID over stdin/stdout
tests/ python-fido2 tests run against the simulator
tools/ attestation certificate generator
```
The core only talks to hardware through `src/platform.h`, so the simulator
runs exactly the same protocol and storage code as the firmware. The
simulator's flash file even follows NOR rules: erase whole sectors, and
program whole pages that can only clear bits.
## Setup
```bash
sudo apt install build-essential cmake ninja-build git gcc-arm-none-eabi \
libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib libmbedtls-dev \
python3-fido2 python3-pytest libusb-1.0-0-dev pkg-config
git clone --depth 1 https://github.com/raspberrypi/pico-sdk deps/pico-sdk
git -C deps/pico-sdk submodule update --init --depth 1 lib/tinyusb lib/mbedtls
```
Set `PICO_SDK_PATH` instead if you already have an SDK checkout.
## Simulator and tests
```bash
cmake -B build-host -G Ninja -DSKEY_HOST=ON
cmake --build build-host
python3 -m pytest tests
```
The simulator approves every request unless `SKEY_BUTTON_FILE` is set. When it
is set, creating that file counts as a button press. The tests in
`tests/test_presence.py` use this.
## Firmware
```bash
cmake -B build-pico -G Ninja -DPICO_BOARD=pico # or pico2
cmake --build build-pico
```
Hold BOOTSEL while plugging in the Pico, then copy `build-pico/skey.uf2` to the
`RPI-RP2` (or `RP2350`) drive. The board restarts as "Skey FIDO2 Key".
## Using the button
The BOOTSEL button is the touch button. Press it briefly; holding it while
plugging in still enters the bootloader as usual.
| LED | Meaning |
|---|---|
| off | idle |
| on | handling a request |
| fast blink | waiting for a touch: press BOOTSEL |
| slow blink | an older U2F site asked for a touch: press BOOTSEL |
- A press only counts if it happens after the request arrives, and each press
approves one request.
- An unanswered request times out after 30 seconds.
- Closing the browser prompt cancels the wait.
To try it out:
- https://webauthn.io: register, then authenticate
- `fido2-token -L` and `fido2-token -I <dev>` (package `fido2-tools`)
- `ssh-keygen -t ecdsa-sk` for an SSH key backed by the device
On Linux, systemd recognizes FIDO devices by their HID usage page, so no udev
rules are needed. The exception is snap-packaged browsers, such as Ubuntu's
Firefox. Snaps can only open keys on a fixed list of USB IDs, so without the
rule below the browser waits forever and never sees the key:
```bash
sudo cp udev/69-skey-snap.rules /etc/udev/rules.d/
sudo udevadm control --reload
```
Then unplug the key and plug it back in.
## PIN
The key works without a PIN. Once you set one:
- every new registration asks for it;
- logins ask for it when the site requires user verification;
- the key returns account names, so the browser can show which passkey you are
picking.
Set or change it in Chrome under Settings > Privacy and security > Security >
Manage security keys. From a terminal, use `fido2-token` (package
`fido2-tools`):
```bash
fido2-token -L # find the device, e.g. /dev/hidraw11
fido2-token -S /dev/hidraw11 # set a PIN
fido2-token -C /dev/hidraw11 # change it
```
A PIN must be at least 4 characters. 8 wrong guesses in a row lock the PIN
for good, and only a reset (which also deletes every credential) clears it.
After 3 wrong guesses, the key also needs unplugging before it accepts more.
## Security notes
- **Secrets live in flash.** The master secret sits in the last 64 KB of flash
in plain form. Anyone holding the key can read it via BOOTSEL. Pico 2 secure
boot plus OTP storage would fix that.
- **Attestation key.** The first build creates one in `attestation/`
(gitignored). Every key you flash from that checkout shares it, just like a
vendor's batch attestation.
- **Reset window.** `authenticatorReset` only works within 10 seconds of
plugging in.
- **Randomness.** The Pico 2 has a hardware TRNG. The RP2040 relies on ROSC
jitter via `pico_rand`, whitened with SHA-256.
## Roadmap
1. Credential management (list and delete passkeys), then advertise FIDO_2_1
2. hmac-secret / prf extension
3. Pico 2: keep the master secret in OTP and enable secure boot
4. Optional external button on a GPIO pin
+60
View File
@@ -0,0 +1,60 @@
// The BOOTSEL button doubles as the touch button.
#include "button.h"
#include "hardware/gpio.h"
#include "hardware/structs/ioqspi.h"
#include "hardware/structs/sio.h"
#include "hardware/sync.h"
#include "platform.h"
#define SAMPLE_INTERVAL_MS 10
#define DEBOUNCE_MS 30
#define QSPI_SS_INDEX 1
static bool pressed;
static uint32_t last_sample_ms;
static uint32_t last_change_ms;
static bool press_pending;
static uint32_t press_ms;
// BOOTSEL pulls the flash chip-select line low. Reading it means briefly
// releasing that line, so flash is unusable: run from RAM with interrupts off.
static bool __no_inline_not_in_flash_func(bootsel_pressed)(void) {
uint32_t irq = save_and_disable_interrupts();
hw_write_masked(&ioqspi_hw->io[QSPI_SS_INDEX].ctrl, GPIO_OVERRIDE_LOW << IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_LSB,
IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_BITS);
for (volatile int i = 0; i < 1000; i++) {
// let the line settle; sleep functions live in flash
}
#if PICO_RP2040
bool down = !(sio_hw->gpio_hi_in & (1u << QSPI_SS_INDEX));
#else
bool down = !(sio_hw->gpio_hi_in & SIO_GPIO_HI_IN_QSPI_CSN_BITS);
#endif
hw_write_masked(&ioqspi_hw->io[QSPI_SS_INDEX].ctrl, GPIO_OVERRIDE_NORMAL << IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_LSB,
IO_QSPI_GPIO_QSPI_SS_CTRL_OEOVER_BITS);
restore_interrupts(irq);
return down;
}
void button_poll(void) {
uint32_t now = platform_millis();
if (now - last_sample_ms < SAMPLE_INTERVAL_MS) return;
last_sample_ms = now;
bool down = bootsel_pressed();
if (down == pressed || now - last_change_ms < DEBOUNCE_MS) return;
pressed = down;
last_change_ms = now;
if (pressed) {
press_pending = true;
press_ms = now;
}
}
bool platform_button_take_press(uint32_t max_age_ms) {
button_poll();
if (!press_pending) return false;
press_pending = false;
return platform_millis() - press_ms <= max_age_ms;
}
+5
View File
@@ -0,0 +1,5 @@
#pragma once
// Samples the touch button. Call from the main loop so presses made between
// requests are seen.
void button_poll(void);
+24
View File
@@ -0,0 +1,24 @@
#include "button.h"
#include "ctaphid.h"
#include "pin.h"
#include "platform.h"
#include "presence.h"
#include "storage.h"
#include "tusb.h"
int main(void) {
uint8_t packet[HID_PACKET_SIZE];
platform_init();
storage_init();
pin_init();
ctaphid_init();
tud_init(0);
for (;;) {
while (platform_hid_receive(packet)) ctaphid_handle_packet(packet);
ctaphid_tick();
button_poll();
presence_tick();
}
}
+26
View File
@@ -0,0 +1,26 @@
// mbedtls build configuration for the firmware: SHA-256, HMAC, P-256 ECDSA/ECDH
// and AES-256-CBC (PIN protocols) only.
#ifndef SKEY_MBEDTLS_CONFIG_H
#define SKEY_MBEDTLS_CONFIG_H
#define MBEDTLS_NO_PLATFORM_ENTROPY
#define MBEDTLS_MD_C
#define MBEDTLS_SHA256_C
#define MBEDTLS_BIGNUM_C
#define MBEDTLS_ECP_C
#define MBEDTLS_ECP_DP_SECP256R1_ENABLED
#define MBEDTLS_ECP_NIST_OPTIM
#define MBEDTLS_ECDSA_C
#define MBEDTLS_ECDSA_DETERMINISTIC
#define MBEDTLS_HMAC_DRBG_C
#define MBEDTLS_ASN1_PARSE_C
#define MBEDTLS_ASN1_WRITE_C
#define MBEDTLS_AES_C
#define MBEDTLS_CIPHER_MODE_CBC
#define MBEDTLS_AES_ROM_TABLES // keep the lookup tables in flash, not RAM
#define MBEDTLS_AES_FEWER_TABLES
#endif
+86
View File
@@ -0,0 +1,86 @@
#include <string.h>
#include "crypto.h"
#include "hardware/flash.h"
#include "hardware/sync.h"
#include "pico/rand.h"
#include "pico/stdlib.h"
#include "platform.h"
// The storage region is the last 64 KB of flash, far past the firmware image.
#define STORAGE_FLASH_OFFSET (PICO_FLASH_SIZE_BYTES - STORAGE_SIZE)
_Static_assert(STORAGE_SECTOR_SIZE == FLASH_SECTOR_SIZE, "sector size mismatch");
_Static_assert(STORAGE_PAGE_SIZE == FLASH_PAGE_SIZE, "page size mismatch");
extern char __flash_binary_end;
void platform_led(bool on) {
#ifdef PICO_DEFAULT_LED_PIN
gpio_put(PICO_DEFAULT_LED_PIN, on);
#else
(void)on;
#endif
}
void platform_init(void) {
if ((uintptr_t)&__flash_binary_end > XIP_BASE + STORAGE_FLASH_OFFSET)
platform_panic("firmware overlaps the storage region");
#ifdef PICO_DEFAULT_LED_PIN
gpio_init(PICO_DEFAULT_LED_PIN);
gpio_set_dir(PICO_DEFAULT_LED_PIN, GPIO_OUT);
#endif
}
uint32_t platform_millis(void) {
return to_ms_since_boot(get_absolute_time());
}
// pico_rand mixes fresh hardware entropy (ROSC jitter, timers, and the TRNG
// on RP2350) into every call. Each output block hashes many of those calls.
void platform_random(uint8_t *buf, size_t len) {
uint64_t samples[32];
uint8_t block[32];
while (len > 0) {
for (size_t i = 0; i < 32; i++) samples[i] = get_rand_64();
crypto_part_t part = {samples, sizeof(samples)};
crypto_sha256(&part, 1, block);
size_t n = len < sizeof(block) ? len : sizeof(block);
memcpy(buf, block, n);
buf += n;
len -= n;
}
crypto_wipe(samples, sizeof(samples));
crypto_wipe(block, sizeof(block));
}
void platform_panic(const char *msg) {
panic("%s", msg);
}
void platform_wink(void) {
for (int i = 0; i < 3; i++) {
platform_led(true);
sleep_ms(100);
platform_led(false);
sleep_ms(100);
}
}
void platform_flash_read(uint32_t offset, void *buf, size_t len) {
memcpy(buf, (const void *)(XIP_BASE + STORAGE_FLASH_OFFSET + offset), len);
}
// Flash is unavailable for execution while it is being written, so interrupts
// (whose handlers live in flash) stay off for the duration.
void platform_flash_erase(uint32_t offset) {
uint32_t irq = save_and_disable_interrupts();
flash_range_erase(STORAGE_FLASH_OFFSET + offset, FLASH_SECTOR_SIZE);
restore_interrupts(irq);
}
void platform_flash_program(uint32_t offset, const void *buf, size_t len) {
uint32_t irq = save_and_disable_interrupts();
flash_range_program(STORAGE_FLASH_OFFSET + offset, buf, len);
restore_interrupts(irq);
}
+20
View File
@@ -0,0 +1,20 @@
#ifndef TUSB_CONFIG_H
#define TUSB_CONFIG_H
#define CFG_TUSB_RHPORT0_MODE (OPT_MODE_DEVICE | OPT_MODE_FULL_SPEED)
#ifndef CFG_TUSB_OS
#define CFG_TUSB_OS OPT_OS_NONE
#endif
#define CFG_TUD_ENDPOINT0_SIZE 64
// One HID interface: the FIDO CTAPHID transport.
#define CFG_TUD_HID 1
#define CFG_TUD_HID_EP_BUFSIZE 64
#define CFG_TUD_CDC 0
#define CFG_TUD_MSC 0
#define CFG_TUD_MIDI 0
#define CFG_TUD_VENDOR 0
#endif
+147
View File
@@ -0,0 +1,147 @@
// USB descriptors and the HID plumbing for the FIDO interface.
#include <string.h>
#include "pico/unique_id.h"
#include "platform.h"
#include "tusb.h"
// pid.codes test VID/PID: fine for personal use, not for distribution. Hosts
// recognize FIDO devices by the HID usage page, not the IDs.
#define USB_VID 0x1209
#define USB_PID 0x0001
#define SEND_TIMEOUT_MS 1000
#define RX_QUEUE_LEN 16 // power of two
enum { STR_LANGID, STR_MANUFACTURER, STR_PRODUCT, STR_SERIAL, STR_INTERFACE };
static const tusb_desc_device_t desc_device = {
.bLength = sizeof(tusb_desc_device_t),
.bDescriptorType = TUSB_DESC_DEVICE,
.bcdUSB = 0x0200,
.bDeviceClass = 0x00,
.bDeviceSubClass = 0x00,
.bDeviceProtocol = 0x00,
.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,
.idVendor = USB_VID,
.idProduct = USB_PID,
.bcdDevice = 0x0100,
.iManufacturer = STR_MANUFACTURER,
.iProduct = STR_PRODUCT,
.iSerialNumber = STR_SERIAL,
.bNumConfigurations = 1,
};
// FIDO HID report descriptor (CTAP 2.1, section 11.2.8.1): 64-byte reports both ways.
static const uint8_t desc_hid_report[] = {
0x06, 0xd0, 0xf1, // Usage Page (FIDO Alliance)
0x09, 0x01, // Usage (CTAPHID)
0xa1, 0x01, // Collection (Application)
0x09, 0x20, // Usage (Input Report Data)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xff, 0x00, // Logical Maximum (255)
0x75, 0x08, // Report Size (8)
0x95, 0x40, // Report Count (64)
0x81, 0x02, // Input (Data, Variable, Absolute)
0x09, 0x21, // Usage (Output Report Data)
0x15, 0x00, // Logical Minimum (0)
0x26, 0xff, 0x00, // Logical Maximum (255)
0x75, 0x08, // Report Size (8)
0x95, 0x40, // Report Count (64)
0x91, 0x02, // Output (Data, Variable, Absolute)
0xc0, // End Collection
};
#define EP_HID_OUT 0x01
#define EP_HID_IN 0x81
#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_HID_INOUT_DESC_LEN)
static const uint8_t desc_configuration[] = {
TUD_CONFIG_DESCRIPTOR(1, 1, 0, CONFIG_TOTAL_LEN, 0, 100),
TUD_HID_INOUT_DESCRIPTOR(0, STR_INTERFACE, HID_ITF_PROTOCOL_NONE, sizeof(desc_hid_report), EP_HID_OUT,
EP_HID_IN, CFG_TUD_HID_EP_BUFSIZE, 5),
};
static uint8_t rx_queue[RX_QUEUE_LEN][HID_PACKET_SIZE];
static unsigned rx_head, rx_tail;
const uint8_t *tud_descriptor_device_cb(void) {
return (const uint8_t *)&desc_device;
}
const uint8_t *tud_descriptor_configuration_cb(uint8_t index) {
(void)index;
return desc_configuration;
}
const uint8_t *tud_hid_descriptor_report_cb(uint8_t instance) {
(void)instance;
return desc_hid_report;
}
const uint16_t *tud_descriptor_string_cb(uint8_t index, uint16_t langid) {
static uint16_t desc[32];
char serial[2 * PICO_UNIQUE_BOARD_ID_SIZE_BYTES + 1];
const char *str;
size_t len;
(void)langid;
switch (index) {
case STR_LANGID:
desc[1] = 0x0409; // English (US)
desc[0] = (uint16_t)(TUSB_DESC_STRING << 8 | 4);
return desc;
case STR_MANUFACTURER: str = "Skey"; break;
case STR_PRODUCT: str = "Skey FIDO2 Key"; break;
case STR_SERIAL:
pico_get_unique_board_id_string(serial, sizeof(serial));
str = serial;
break;
case STR_INTERFACE: str = "FIDO"; break;
default: return NULL;
}
len = strlen(str);
if (len > 31) len = 31;
for (size_t i = 0; i < len; i++) desc[1 + i] = (uint8_t)str[i];
desc[0] = (uint16_t)(TUSB_DESC_STRING << 8 | (2 * len + 2));
return desc;
}
uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t *buffer,
uint16_t reqlen) {
(void)instance;
(void)report_id;
(void)report_type;
(void)buffer;
(void)reqlen;
return 0;
}
// Runs inside tud_task(). Packets are queued so they are handled from the main
// loop, never re-entrantly while a response is being sent.
void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type,
const uint8_t *buffer, uint16_t bufsize) {
(void)instance;
(void)report_id;
(void)report_type;
if (bufsize != HID_PACKET_SIZE || rx_head - rx_tail == RX_QUEUE_LEN) return;
memcpy(rx_queue[rx_head % RX_QUEUE_LEN], buffer, HID_PACKET_SIZE);
rx_head++;
}
bool platform_hid_receive(uint8_t packet[HID_PACKET_SIZE]) {
tud_task();
if (rx_head == rx_tail) return false;
memcpy(packet, rx_queue[rx_tail % RX_QUEUE_LEN], HID_PACKET_SIZE);
rx_tail++;
return true;
}
void platform_hid_send(const uint8_t packet[HID_PACKET_SIZE]) {
uint32_t start = platform_millis();
while (!tud_hid_ready()) {
if (!tud_mounted() || platform_millis() - start > SEND_TIMEOUT_MS) return; // host went away
tud_task();
}
tud_hid_report(0, packet, HID_PACKET_SIZE);
}
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include "platform.h"
// Reads one packet from stdin, waiting up to timeout_ms. Exits the process
// when stdin is closed.
bool host_receive(uint8_t packet[HID_PACKET_SIZE], int timeout_ms);
+23
View File
@@ -0,0 +1,23 @@
// Simulator: speaks CTAPHID over stdin/stdout, one 64-byte packet at a time.
#include "ctaphid.h"
#include "host.h"
#include "pin.h"
#include "platform.h"
#include "presence.h"
#include "storage.h"
#define TICK_MS 50
int main(void) {
uint8_t packet[HID_PACKET_SIZE];
platform_init();
storage_init();
pin_init();
ctaphid_init();
for (;;) {
if (host_receive(packet, TICK_MS)) ctaphid_handle_packet(packet);
ctaphid_tick();
presence_tick();
}
}
+121
View File
@@ -0,0 +1,121 @@
// Simulator backend: flash is emulated in a file (SKEY_FLASH_FILE), randomness
// comes from the OS, and HID packets travel over stdin/stdout.
//
// The button: without SKEY_BUTTON_FILE every request is approved. With it,
// creating that file is a press (its mtime is the press time), and the
// simulator deletes it when the press is used.
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/random.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#include "host.h"
#include "platform.h"
static uint8_t flash[STORAGE_SIZE];
static const char *flash_path;
static const char *button_path;
static struct timespec start;
void platform_init(void) {
flash_path = getenv("SKEY_FLASH_FILE");
if (!flash_path) flash_path = "skey_flash.bin";
button_path = getenv("SKEY_BUTTON_FILE");
memset(flash, 0xff, sizeof(flash));
FILE *f = fopen(flash_path, "rb");
if (f) {
if (fread(flash, 1, sizeof(flash), f) != sizeof(flash)) memset(flash, 0xff, sizeof(flash));
fclose(f);
}
clock_gettime(CLOCK_MONOTONIC, &start);
}
uint32_t platform_millis(void) {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
return (uint32_t)((now.tv_sec - start.tv_sec) * 1000 + (now.tv_nsec - start.tv_nsec) / 1000000);
}
void platform_random(uint8_t *buf, size_t len) {
while (len > 0) {
ssize_t n = getrandom(buf, len, 0);
if (n < 0) platform_panic("getrandom failed");
buf += n;
len -= (size_t)n;
}
}
void platform_panic(const char *msg) {
fprintf(stderr, "skey_sim: %s\n", msg);
abort();
}
bool platform_button_take_press(uint32_t max_age_ms) {
struct stat st;
struct timespec now;
if (!button_path) return true;
if (stat(button_path, &st) != 0) return false;
unlink(button_path);
clock_gettime(CLOCK_REALTIME, &now);
int64_t age_ms = (int64_t)(now.tv_sec - st.st_mtim.tv_sec) * 1000 + (now.tv_nsec - st.st_mtim.tv_nsec) / 1000000;
return age_ms <= (int64_t)max_age_ms;
}
void platform_led(bool on) {
(void)on;
}
void platform_wink(void) {
fprintf(stderr, "skey_sim: wink\n");
}
static void save_flash(void) {
FILE *f = fopen(flash_path, "wb");
if (!f || fwrite(flash, 1, sizeof(flash), f) != sizeof(flash)) platform_panic("cannot write flash file");
fclose(f);
}
void platform_flash_read(uint32_t offset, void *buf, size_t len) {
if (offset > STORAGE_SIZE || len > STORAGE_SIZE - offset) platform_panic("flash read out of range");
memcpy(buf, flash + offset, len);
}
void platform_flash_erase(uint32_t offset) {
if (offset % STORAGE_SECTOR_SIZE || offset >= STORAGE_SIZE) platform_panic("bad flash erase");
memset(flash + offset, 0xff, STORAGE_SECTOR_SIZE);
save_flash();
}
// Behaves like NOR flash: whole pages only, and programming only clears bits.
void platform_flash_program(uint32_t offset, const void *buf, size_t len) {
const uint8_t *src = buf;
if (offset % STORAGE_PAGE_SIZE || len % STORAGE_PAGE_SIZE || offset > STORAGE_SIZE ||
len > STORAGE_SIZE - offset)
platform_panic("bad flash program");
for (size_t i = 0; i < len; i++) flash[offset + i] &= src[i];
save_flash();
}
void platform_hid_send(const uint8_t packet[HID_PACKET_SIZE]) {
fwrite(packet, 1, HID_PACKET_SIZE, stdout);
fflush(stdout);
}
bool host_receive(uint8_t packet[HID_PACKET_SIZE], int timeout_ms) {
struct pollfd pfd = {.fd = STDIN_FILENO, .events = POLLIN};
if (poll(&pfd, 1, timeout_ms) <= 0) return false;
for (size_t got = 0; got < HID_PACKET_SIZE;) {
ssize_t n = read(STDIN_FILENO, packet + got, HID_PACKET_SIZE - got);
if (n <= 0) exit(0); // the host closed the pipe: unplugged
got += (size_t)n;
}
return true;
}
bool platform_hid_receive(uint8_t packet[HID_PACKET_SIZE]) {
return host_receive(packet, 0);
}
+10
View File
@@ -0,0 +1,10 @@
// Batch attestation identity, generated at build time by tools/gen_attestation.py.
#pragma once
#include <stddef.h>
#include <stdint.h>
extern const uint8_t skey_aaguid[16];
extern const uint8_t attestation_key[32];
extern const uint8_t attestation_cert[];
extern const size_t attestation_cert_len;
+215
View File
@@ -0,0 +1,215 @@
#include "cbor.h"
#include <string.h>
#include "ctap.h"
// CTAP2 messages nest at most four levels deep.
#define MAX_DEPTH 8
void cbor_reader_init(cbor_reader_t *r, const uint8_t *buf, size_t len) {
r->p = buf;
r->end = buf + len;
}
int cbor_peek_type(const cbor_reader_t *r) {
return r->p < r->end ? *r->p >> 5 : -1;
}
static size_t remaining(const cbor_reader_t *r) {
return (size_t)(r->end - r->p);
}
// Decodes an item head. Indefinite lengths are rejected: CTAP2 only accepts
// canonical CBOR.
static int read_head(cbor_reader_t *r, uint8_t *major, uint64_t *val) {
if (r->p >= r->end) return CTAP2_ERR_INVALID_CBOR;
uint8_t ib = *r->p++;
uint8_t ai = ib & 0x1f;
*major = ib >> 5;
if (ai < 24) {
*val = ai;
return 0;
}
if (ai > 27) return CTAP2_ERR_INVALID_CBOR;
size_t n = (size_t)1 << (ai - 24);
if (remaining(r) < n) return CTAP2_ERR_INVALID_CBOR;
uint64_t v = 0;
for (size_t i = 0; i < n; i++) v = v << 8 | *r->p++;
*val = v;
return 0;
}
static int read_typed(cbor_reader_t *r, int want, uint64_t *val) {
int type = cbor_peek_type(r);
if (type < 0) return CTAP2_ERR_INVALID_CBOR;
if (type != want) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE;
uint8_t major;
return read_head(r, &major, val);
}
int cbor_read_uint(cbor_reader_t *r, uint64_t *out) {
return read_typed(r, CBOR_UINT, out);
}
int cbor_read_int(cbor_reader_t *r, int64_t *out) {
int type = cbor_peek_type(r);
if (type < 0) return CTAP2_ERR_INVALID_CBOR;
if (type != CBOR_UINT && type != CBOR_NEGINT) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE;
uint8_t major;
uint64_t v;
int err = read_head(r, &major, &v);
if (err) return err;
if (v > INT64_MAX) return CTAP2_ERR_INVALID_CBOR;
*out = major == CBOR_UINT ? (int64_t)v : -1 - (int64_t)v;
return 0;
}
static int read_string(cbor_reader_t *r, int want, const uint8_t **data, size_t *len) {
uint64_t n;
int err = read_typed(r, want, &n);
if (err) return err;
if (n > remaining(r)) return CTAP2_ERR_INVALID_CBOR;
*data = r->p;
*len = (size_t)n;
r->p += n;
return 0;
}
int cbor_read_bytes(cbor_reader_t *r, const uint8_t **data, size_t *len) {
return read_string(r, CBOR_BYTES, data, len);
}
int cbor_read_text(cbor_reader_t *r, const char **text, size_t *len) {
const uint8_t *data;
int err = read_string(r, CBOR_TEXT, &data, len);
if (!err) *text = (const char *)data;
return err;
}
static int read_container(cbor_reader_t *r, int want, size_t *count, size_t bytes_per_entry) {
uint64_t n;
int err = read_typed(r, want, &n);
if (err) return err;
// Every entry takes at least one byte per item, which bounds the count.
if (n > remaining(r) / bytes_per_entry) return CTAP2_ERR_INVALID_CBOR;
*count = (size_t)n;
return 0;
}
int cbor_read_array(cbor_reader_t *r, size_t *count) {
return read_container(r, CBOR_ARRAY, count, 1);
}
int cbor_read_map(cbor_reader_t *r, size_t *count) {
return read_container(r, CBOR_MAP, count, 2);
}
int cbor_read_bool(cbor_reader_t *r, bool *out) {
if (r->p >= r->end) return CTAP2_ERR_INVALID_CBOR;
if (*r->p != 0xf4 && *r->p != 0xf5) return CTAP2_ERR_CBOR_UNEXPECTED_TYPE;
*out = *r->p++ == 0xf5;
return 0;
}
static int skip(cbor_reader_t *r, int depth) {
if (depth > MAX_DEPTH) return CTAP2_ERR_INVALID_CBOR;
uint8_t major;
uint64_t v;
int err = read_head(r, &major, &v);
if (err) return err;
switch (major) {
case CBOR_BYTES:
case CBOR_TEXT:
if (v > remaining(r)) return CTAP2_ERR_INVALID_CBOR;
r->p += v;
return 0;
case CBOR_ARRAY:
case CBOR_MAP:
if (v > remaining(r)) return CTAP2_ERR_INVALID_CBOR;
if (major == CBOR_MAP) v *= 2;
for (uint64_t i = 0; i < v; i++) {
err = skip(r, depth + 1);
if (err) return err;
}
return 0;
case CBOR_TAG:
return skip(r, depth + 1);
default:
// Integers and simple values are fully described by their head.
return 0;
}
}
int cbor_skip(cbor_reader_t *r) {
return skip(r, 0);
}
int cbor_read_uint_key(cbor_reader_t *r, uint64_t *key) {
if (cbor_peek_type(r) == CBOR_UINT) return cbor_read_uint(r, key);
*key = UINT64_MAX;
return cbor_skip(r);
}
void cbor_writer_init(cbor_writer_t *w, uint8_t *buf, size_t cap) {
w->buf = buf;
w->cap = cap;
w->len = 0;
w->overflow = false;
}
static void put(cbor_writer_t *w, const void *data, size_t len) {
if (w->overflow || len > w->cap - w->len) {
w->overflow = true;
return;
}
if (len) memcpy(w->buf + w->len, data, len);
w->len += len;
}
static void put_head(cbor_writer_t *w, uint8_t major, uint64_t v) {
uint8_t head[9];
size_t n = v < 24 ? 0 : v <= 0xff ? 1 : v <= 0xffff ? 2 : v <= 0xffffffff ? 4 : 8;
static const uint8_t ai_for_size[9] = {[1] = 24, [2] = 25, [4] = 26, [8] = 27};
head[0] = (uint8_t)(major << 5 | (n ? ai_for_size[n] : v));
for (size_t i = 0; i < n; i++) head[1 + i] = (uint8_t)(v >> (8 * (n - 1 - i)));
put(w, head, 1 + n);
}
void cbor_put_uint(cbor_writer_t *w, uint64_t v) {
put_head(w, CBOR_UINT, v);
}
void cbor_put_int(cbor_writer_t *w, int64_t v) {
if (v >= 0)
put_head(w, CBOR_UINT, (uint64_t)v);
else
put_head(w, CBOR_NEGINT, (uint64_t)(-1 - v));
}
void cbor_put_bytes(cbor_writer_t *w, const uint8_t *data, size_t len) {
put_head(w, CBOR_BYTES, len);
put(w, data, len);
}
void cbor_put_text(cbor_writer_t *w, const char *text, size_t len) {
put_head(w, CBOR_TEXT, len);
put(w, text, len);
}
void cbor_put_cstr(cbor_writer_t *w, const char *text) {
cbor_put_text(w, text, strlen(text));
}
void cbor_put_array(cbor_writer_t *w, size_t count) {
put_head(w, CBOR_ARRAY, count);
}
void cbor_put_map(cbor_writer_t *w, size_t count) {
put_head(w, CBOR_MAP, count);
}
void cbor_put_bool(cbor_writer_t *w, bool v) {
uint8_t b = v ? 0xf5 : 0xf4;
put(w, &b, 1);
}
+59
View File
@@ -0,0 +1,59 @@
// Minimal CBOR reader/writer covering what CTAP2 uses.
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
enum {
CBOR_UINT = 0,
CBOR_NEGINT = 1,
CBOR_BYTES = 2,
CBOR_TEXT = 3,
CBOR_ARRAY = 4,
CBOR_MAP = 5,
CBOR_TAG = 6,
CBOR_SIMPLE = 7,
};
// Reader functions return 0 on success or a CTAP2 error code. Strings are
// returned as pointers into the input buffer.
typedef struct {
const uint8_t *p;
const uint8_t *end;
} cbor_reader_t;
void cbor_reader_init(cbor_reader_t *r, const uint8_t *buf, size_t len);
int cbor_peek_type(const cbor_reader_t *r); // major type, or -1 at end of input
int cbor_read_uint(cbor_reader_t *r, uint64_t *out);
int cbor_read_int(cbor_reader_t *r, int64_t *out);
int cbor_read_bytes(cbor_reader_t *r, const uint8_t **data, size_t *len);
int cbor_read_text(cbor_reader_t *r, const char **text, size_t *len);
int cbor_read_array(cbor_reader_t *r, size_t *count);
int cbor_read_map(cbor_reader_t *r, size_t *count);
int cbor_read_bool(cbor_reader_t *r, bool *out);
int cbor_skip(cbor_reader_t *r);
// Reads a map key that should be an unsigned integer, as in CTAP requests.
// Any other key is skipped and reported as UINT64_MAX, so the caller treats
// it as unknown and skips its value.
int cbor_read_uint_key(cbor_reader_t *r, uint64_t *key);
// The writer emits minimal-length heads; callers write map keys in CTAP2
// canonical order. Running out of space sets `overflow`.
typedef struct {
uint8_t *buf;
size_t cap;
size_t len;
bool overflow;
} cbor_writer_t;
void cbor_writer_init(cbor_writer_t *w, uint8_t *buf, size_t cap);
void cbor_put_uint(cbor_writer_t *w, uint64_t v);
void cbor_put_int(cbor_writer_t *w, int64_t v);
void cbor_put_bytes(cbor_writer_t *w, const uint8_t *data, size_t len);
void cbor_put_text(cbor_writer_t *w, const char *text, size_t len);
void cbor_put_cstr(cbor_writer_t *w, const char *text);
void cbor_put_array(cbor_writer_t *w, size_t count);
void cbor_put_map(cbor_writer_t *w, size_t count);
void cbor_put_bool(cbor_writer_t *w, bool v);
+48
View File
@@ -0,0 +1,48 @@
#include "cred.h"
#include <string.h>
#include "crypto.h"
#include "platform.h"
#include "storage.h"
#define CRED_VERSION 1
#define NONCE_LEN 32
#define TAG_LEN 16
#define CREATE_ATTEMPTS 4
// `label` is always 8 bytes, which keeps the HMAC input unambiguous.
static void derive(const char label[8], const uint8_t *cred_id, const uint8_t rp_id_hash[32], uint8_t out[32]) {
crypto_part_t parts[] = {
{label, 8},
{cred_id, 1 + NONCE_LEN},
{rp_id_hash, 32},
};
crypto_hmac_sha256(storage_master_secret(), parts, 3, out);
}
bool cred_create(const uint8_t rp_id_hash[32], uint8_t cred_id[CRED_ID_LEN], uint8_t priv[32], uint8_t pub[64]) {
// A derived key is an invalid scalar with probability ~2^-32; pick a new nonce if so.
for (int attempt = 0; attempt < CREATE_ATTEMPTS; attempt++) {
cred_id[0] = CRED_VERSION;
platform_random(cred_id + 1, NONCE_LEN);
derive("skey-key", cred_id, rp_id_hash, priv);
if (crypto_p256_public(priv, pub)) {
uint8_t tag[32];
derive("skey-tag", cred_id, rp_id_hash, tag);
memcpy(cred_id + 1 + NONCE_LEN, tag, TAG_LEN);
return true;
}
}
crypto_wipe(priv, 32);
return false;
}
bool cred_unwrap(const uint8_t rp_id_hash[32], const uint8_t *cred_id, size_t len, uint8_t priv[32]) {
uint8_t tag[32];
if (len != CRED_ID_LEN || cred_id[0] != CRED_VERSION) return false;
derive("skey-tag", cred_id, rp_id_hash, tag);
if (!crypto_equal(tag, cred_id + 1 + NONCE_LEN, TAG_LEN)) return false;
derive("skey-key", cred_id, rp_id_hash, priv);
return true;
}
+19
View File
@@ -0,0 +1,19 @@
// Credential IDs are self-contained: the private key is re-derived from the
// device master secret, so non-resident credentials need no storage.
//
// credential ID = version (1) || nonce (32) || tag (16)
// private key = HMAC(master, "skey-key" || version || nonce || rpIdHash)
// tag = HMAC(master, "skey-tag" || version || nonce || rpIdHash)
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#define CRED_ID_LEN 49
// Makes a new credential bound to rp_id_hash.
bool cred_create(const uint8_t rp_id_hash[32], uint8_t cred_id[CRED_ID_LEN], uint8_t priv[32], uint8_t pub[64]);
// Recovers the private key if this device issued cred_id for rp_id_hash.
bool cred_unwrap(const uint8_t rp_id_hash[32], const uint8_t *cred_id, size_t len, uint8_t priv[32]);
+161
View File
@@ -0,0 +1,161 @@
#include "crypto.h"
#include <string.h>
#include "mbedtls/aes.h"
#include "mbedtls/bignum.h"
#include "mbedtls/ecdsa.h"
#include "mbedtls/ecp.h"
#include "mbedtls/md.h"
#include "mbedtls/platform_util.h"
#include "platform.h"
// Used by mbedtls for side-channel blinding only; signatures are deterministic.
static int rng(void *ctx, unsigned char *buf, size_t len) {
(void)ctx;
platform_random(buf, len);
return 0;
}
static void digest(const uint8_t *hmac_key, const crypto_part_t *parts, size_t count, uint8_t out[32]) {
mbedtls_md_context_t ctx;
int err;
mbedtls_md_init(&ctx);
err = mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), hmac_key != NULL);
if (!err) err = hmac_key ? mbedtls_md_hmac_starts(&ctx, hmac_key, 32) : mbedtls_md_starts(&ctx);
for (size_t i = 0; i < count && !err; i++) {
err = hmac_key ? mbedtls_md_hmac_update(&ctx, parts[i].data, parts[i].len)
: mbedtls_md_update(&ctx, parts[i].data, parts[i].len);
}
if (!err) err = hmac_key ? mbedtls_md_hmac_finish(&ctx, out) : mbedtls_md_finish(&ctx, out);
mbedtls_md_free(&ctx);
if (err) platform_panic("sha256 failed");
}
void crypto_sha256(const crypto_part_t *parts, size_t count, uint8_t out[32]) {
digest(NULL, parts, count, out);
}
void crypto_hmac_sha256(const uint8_t key[32], const crypto_part_t *parts, size_t count, uint8_t out[32]) {
digest(key, parts, count, out);
}
bool crypto_p256_public(const uint8_t priv[32], uint8_t pub[64]) {
mbedtls_ecp_group grp;
mbedtls_mpi d;
mbedtls_ecp_point q;
uint8_t point[65];
size_t len = 0;
mbedtls_ecp_group_init(&grp);
mbedtls_mpi_init(&d);
mbedtls_ecp_point_init(&q);
bool ok = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1) == 0 &&
mbedtls_mpi_read_binary(&d, priv, 32) == 0 && mbedtls_ecp_check_privkey(&grp, &d) == 0 &&
mbedtls_ecp_mul(&grp, &q, &d, &grp.G, rng, NULL) == 0 &&
mbedtls_ecp_point_write_binary(&grp, &q, MBEDTLS_ECP_PF_UNCOMPRESSED, &len, point,
sizeof(point)) == 0 &&
len == sizeof(point);
if (ok) memcpy(pub, point + 1, 64);
mbedtls_ecp_point_free(&q);
mbedtls_mpi_free(&d);
mbedtls_ecp_group_free(&grp);
return ok;
}
// Encodes a 32-byte unsigned big-endian integer as a minimal DER INTEGER.
static size_t der_integer(uint8_t *out, const uint8_t v[32]) {
size_t skip = 0;
while (skip < 31 && v[skip] == 0) skip++;
size_t len = 32 - skip;
size_t pad = v[skip] >> 7; // keep the value positive
out[0] = 0x02;
out[1] = (uint8_t)(pad + len);
out[2] = 0;
memcpy(out + 2 + pad, v + skip, len);
return 2 + pad + len;
}
size_t crypto_p256_sign(const uint8_t priv[32], const crypto_part_t *parts, size_t count,
uint8_t sig[P256_SIG_MAX_DER]) {
uint8_t hash[32], r_bytes[32], s_bytes[32];
mbedtls_ecp_group grp;
mbedtls_mpi d, r, s;
crypto_sha256(parts, count, hash);
mbedtls_ecp_group_init(&grp);
mbedtls_mpi_init(&d);
mbedtls_mpi_init(&r);
mbedtls_mpi_init(&s);
bool ok = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1) == 0 &&
mbedtls_mpi_read_binary(&d, priv, 32) == 0 &&
mbedtls_ecdsa_sign_det_ext(&grp, &r, &s, &d, hash, sizeof(hash), MBEDTLS_MD_SHA256, rng,
NULL) == 0 &&
mbedtls_mpi_write_binary(&r, r_bytes, 32) == 0 && mbedtls_mpi_write_binary(&s, s_bytes, 32) == 0;
mbedtls_mpi_free(&s);
mbedtls_mpi_free(&r);
mbedtls_mpi_free(&d);
mbedtls_ecp_group_free(&grp);
if (!ok) return 0;
size_t len = der_integer(sig + 2, r_bytes);
len += der_integer(sig + 2 + len, s_bytes);
sig[0] = 0x30;
sig[1] = (uint8_t)len;
return 2 + len;
}
bool crypto_p256_ecdh(const uint8_t priv[32], const uint8_t peer[64], uint8_t shared_x[32]) {
mbedtls_ecp_group grp;
mbedtls_mpi d;
mbedtls_ecp_point q, z;
uint8_t point[65];
size_t len = 0;
point[0] = 0x04; // uncompressed
memcpy(point + 1, peer, 64);
mbedtls_ecp_group_init(&grp);
mbedtls_mpi_init(&d);
mbedtls_ecp_point_init(&q);
mbedtls_ecp_point_init(&z);
bool ok = mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1) == 0 &&
mbedtls_mpi_read_binary(&d, priv, 32) == 0 &&
mbedtls_ecp_point_read_binary(&grp, &q, point, sizeof(point)) == 0 &&
mbedtls_ecp_check_pubkey(&grp, &q) == 0 && mbedtls_ecp_mul(&grp, &z, &d, &q, rng, NULL) == 0 &&
mbedtls_ecp_point_write_binary(&grp, &z, MBEDTLS_ECP_PF_UNCOMPRESSED, &len, point,
sizeof(point)) == 0 &&
len == sizeof(point);
if (ok) memcpy(shared_x, point + 1, 32);
crypto_wipe(point, sizeof(point));
mbedtls_ecp_point_free(&z);
mbedtls_ecp_point_free(&q);
mbedtls_mpi_free(&d);
mbedtls_ecp_group_free(&grp);
return ok;
}
bool crypto_aes256_cbc(bool encrypt, const uint8_t key[32], const uint8_t iv[16], const uint8_t *in, uint8_t *out,
size_t len) {
mbedtls_aes_context ctx;
uint8_t chain[16]; // mbedtls updates the IV in place
if (len % 16) return false;
memcpy(chain, iv, sizeof(chain));
mbedtls_aes_init(&ctx);
bool ok = (encrypt ? mbedtls_aes_setkey_enc(&ctx, key, 256) : mbedtls_aes_setkey_dec(&ctx, key, 256)) == 0 &&
mbedtls_aes_crypt_cbc(&ctx, encrypt ? MBEDTLS_AES_ENCRYPT : MBEDTLS_AES_DECRYPT, len, chain, in,
out) == 0;
mbedtls_aes_free(&ctx);
return ok;
}
bool crypto_equal(const void *a, const void *b, size_t len) {
const uint8_t *x = a, *y = b;
uint8_t diff = 0;
for (size_t i = 0; i < len; i++) diff |= x[i] ^ y[i];
return diff == 0;
}
void crypto_wipe(void *buf, size_t len) {
mbedtls_platform_zeroize(buf, len);
}
+36
View File
@@ -0,0 +1,36 @@
// SHA-256, HMAC-SHA256 and P-256 ECDSA, backed by mbedtls.
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#define P256_SIG_MAX_DER 72
// Hash inputs are given as a list of buffers that are processed back to back.
typedef struct {
const void *data;
size_t len;
} crypto_part_t;
void crypto_sha256(const crypto_part_t *parts, size_t count, uint8_t out[32]);
void crypto_hmac_sha256(const uint8_t key[32], const crypto_part_t *parts, size_t count, uint8_t out[32]);
// Computes the public key (x || y) for a private key; fails if priv is not a
// valid P-256 scalar.
bool crypto_p256_public(const uint8_t priv[32], uint8_t pub[64]);
// Signs SHA-256(parts) with deterministic ECDSA (RFC 6979). Writes a DER
// signature and returns its length, or 0 on failure.
size_t crypto_p256_sign(const uint8_t priv[32], const crypto_part_t *parts, size_t count,
uint8_t sig[P256_SIG_MAX_DER]);
// ECDH: the x coordinate of priv * peer. Fails if peer is not on the curve.
bool crypto_p256_ecdh(const uint8_t priv[32], const uint8_t peer[64], uint8_t shared_x[32]);
// AES-256-CBC without padding; len must be a multiple of 16. in may equal out.
bool crypto_aes256_cbc(bool encrypt, const uint8_t key[32], const uint8_t iv[16], const uint8_t *in, uint8_t *out,
size_t len);
bool crypto_equal(const void *a, const void *b, size_t len); // constant time
void crypto_wipe(void *buf, size_t len);
+49
View File
@@ -0,0 +1,49 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
// CTAP status codes (CTAP 2.1, section 8.2).
#define CTAP2_OK 0x00
#define CTAP1_ERR_INVALID_COMMAND 0x01
#define CTAP1_ERR_INVALID_PARAMETER 0x02
#define CTAP1_ERR_INVALID_LENGTH 0x03
#define CTAP2_ERR_CBOR_UNEXPECTED_TYPE 0x11
#define CTAP2_ERR_INVALID_CBOR 0x12
#define CTAP2_ERR_MISSING_PARAMETER 0x14
#define CTAP2_ERR_CREDENTIAL_EXCLUDED 0x19
#define CTAP2_ERR_PROCESSING 0x21
#define CTAP2_ERR_UNSUPPORTED_ALGORITHM 0x26
#define CTAP2_ERR_OPERATION_DENIED 0x27
#define CTAP2_ERR_KEY_STORE_FULL 0x28
#define CTAP2_ERR_UNSUPPORTED_OPTION 0x2B
#define CTAP2_ERR_INVALID_OPTION 0x2C
#define CTAP2_ERR_KEEPALIVE_CANCEL 0x2D
#define CTAP2_ERR_NO_CREDENTIALS 0x2E
#define CTAP2_ERR_USER_ACTION_TIMEOUT 0x2F
#define CTAP2_ERR_NOT_ALLOWED 0x30
#define CTAP2_ERR_PIN_INVALID 0x31
#define CTAP2_ERR_PIN_BLOCKED 0x32
#define CTAP2_ERR_PIN_AUTH_INVALID 0x33
#define CTAP2_ERR_PIN_AUTH_BLOCKED 0x34
#define CTAP2_ERR_PIN_NOT_SET 0x35
#define CTAP2_ERR_PUAT_REQUIRED 0x36 // PIN_REQUIRED in CTAP 2.0
#define CTAP2_ERR_PIN_POLICY_VIOLATION 0x37
#define CTAP2_ERR_INVALID_SUBCOMMAND 0x3E
#define CTAP2_ERR_UNAUTHORIZED_PERMISSION 0x40
#define CTAP1_ERR_OTHER 0x7F
// Returns from the calling function if `expr` yields a non-zero status.
#define CHECK(expr) \
do { \
int err_ = (expr); \
if (err_) return err_; \
} while (0)
// Handles a CTAPHID_CBOR request. Writes the status byte and CBOR body to
// resp and returns the response length.
size_t ctap2_handle(const uint8_t *req, size_t len, uint8_t *resp, size_t resp_cap);
// Handles a CTAPHID_MSG request (a U2F APDU). Writes the response data and
// status word to resp and returns the response length.
size_t ctap1_handle(const uint8_t *req, size_t len, uint8_t *resp);
+129
View File
@@ -0,0 +1,129 @@
// 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);
}
}
+614
View File
@@ -0,0 +1,614 @@
// 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;
}
+266
View File
@@ -0,0 +1,266 @@
#include "ctaphid.h"
#include <string.h>
#include "ctap.h"
#include "util.h"
#define CID_BROADCAST 0xffffffffu
#define CTAPHID_PING 0x81
#define CTAPHID_MSG 0x83
#define CTAPHID_INIT 0x86
#define CTAPHID_WINK 0x88
#define CTAPHID_CBOR 0x90
#define CTAPHID_CANCEL 0x91
#define CTAPHID_KEEPALIVE 0xbb
#define CTAPHID_ERROR 0xbf
#define ERR_INVALID_CMD 0x01
#define ERR_INVALID_LEN 0x03
#define ERR_INVALID_SEQ 0x04
#define ERR_MSG_TIMEOUT 0x05
#define ERR_CHANNEL_BUSY 0x06
#define ERR_INVALID_CHANNEL 0x0b
#define PROTOCOL_VERSION 2
#define DEVICE_VERSION_MAJOR 0
#define DEVICE_VERSION_MINOR 1
#define DEVICE_VERSION_BUILD 0
#define CAPABILITY_WINK 0x01
#define CAPABILITY_CBOR 0x04
#define INIT_DATA (HID_PACKET_SIZE - 7)
#define CONT_DATA (HID_PACKET_SIZE - 5)
#define MAX_MESSAGE (INIT_DATA + 128 * CONT_DATA)
#define MESSAGE_TIMEOUT_MS 500
#define KEEPALIVE_INTERVAL_MS 100
// The message being received. Only one channel can be mid-message at a time.
static struct {
bool active;
uint32_t cid;
uint8_t cmd;
uint16_t len;
uint16_t received;
uint8_t next_seq;
uint32_t last_ms;
uint8_t data[MAX_MESSAGE];
} rx;
// The request being handled. While a handler waits for the user, packets keep
// arriving: CANCEL or INIT on this channel abort it, other channels are busy.
static struct {
bool active;
uint32_t cid;
bool cancelled;
bool resynced; // the host reset the channel and expects no response
uint32_t last_keepalive_ms;
} busy;
static uint8_t tx[MAX_MESSAGE];
static uint32_t next_cid;
void ctaphid_init(void) {
rx.active = false;
next_cid = 1;
}
static void send_message(uint32_t cid, uint8_t cmd, const uint8_t *data, size_t len) {
uint8_t packet[HID_PACKET_SIZE] = {0};
size_t n = len < INIT_DATA ? len : INIT_DATA;
put_be32(packet, cid);
packet[4] = cmd;
packet[5] = (uint8_t)(len >> 8);
packet[6] = (uint8_t)len;
if (n) memcpy(packet + 7, data, n);
platform_hid_send(packet);
for (size_t off = n, seq = 0; off < len; off += n, seq++) {
n = len - off < CONT_DATA ? len - off : CONT_DATA;
memset(packet + 4, 0, sizeof(packet) - 4);
packet[4] = (uint8_t)seq;
memcpy(packet + 5, data + off, n);
platform_hid_send(packet);
}
}
static void send_error(uint32_t cid, uint8_t code) {
send_message(cid, CTAPHID_ERROR, &code, 1);
}
static void handle_init(uint32_t cid, const uint8_t *packet) {
uint8_t resp[17];
if ((packet[5] << 8 | packet[6]) != 8) {
send_error(cid, ERR_INVALID_LEN);
return;
}
// INIT on an existing channel resynchronizes it.
if (rx.active && rx.cid == cid) rx.active = false;
uint32_t new_cid = cid;
if (cid == CID_BROADCAST) {
new_cid = next_cid++;
if (next_cid == CID_BROADCAST) next_cid = 1;
}
memcpy(resp, packet + 7, 8); // nonce
put_be32(resp + 8, new_cid);
resp[12] = PROTOCOL_VERSION;
resp[13] = DEVICE_VERSION_MAJOR;
resp[14] = DEVICE_VERSION_MINOR;
resp[15] = DEVICE_VERSION_BUILD;
resp[16] = CAPABILITY_WINK | CAPABILITY_CBOR;
send_message(cid, CTAPHID_INIT, resp, sizeof(resp));
}
static void respond(uint8_t cmd, const uint8_t *data, size_t len) {
if (!busy.resynced) send_message(busy.cid, cmd, data, len);
}
static void dispatch(void) {
size_t len = rx.len;
uint8_t error;
busy.active = true;
busy.cid = rx.cid;
busy.cancelled = false;
busy.resynced = false;
busy.last_keepalive_ms = platform_millis();
platform_led(true);
switch (rx.cmd) {
case CTAPHID_PING:
respond(CTAPHID_PING, rx.data, len);
break;
case CTAPHID_MSG:
len = ctap1_handle(rx.data, len, tx);
respond(CTAPHID_MSG, tx, len);
break;
case CTAPHID_CBOR:
if (len == 0) {
error = ERR_INVALID_LEN;
respond(CTAPHID_ERROR, &error, 1);
break;
}
len = ctap2_handle(rx.data, len, tx, sizeof(tx));
respond(CTAPHID_CBOR, tx, len);
break;
case CTAPHID_WINK:
platform_wink();
respond(CTAPHID_WINK, NULL, 0);
break;
default:
error = ERR_INVALID_CMD;
respond(CTAPHID_ERROR, &error, 1);
break;
}
platform_led(false);
busy.active = false;
rx.active = false;
}
// Packets that arrive while a request is being handled.
static void handle_packet_while_busy(uint32_t cid, const uint8_t *packet) {
uint8_t cmd = packet[4];
if (!(cmd & 0x80)) return; // continuation packets have nothing to continue
if (cid == busy.cid && (cmd == CTAPHID_CANCEL || cmd == CTAPHID_INIT)) {
busy.cancelled = true;
if (cmd == CTAPHID_INIT) {
busy.resynced = true;
handle_init(cid, packet);
}
} else if (cmd == CTAPHID_INIT && cid == CID_BROADCAST) {
handle_init(cid, packet); // allocating a channel doesn't disturb anyone
} else if (cmd != CTAPHID_CANCEL) {
send_error(cid, ERR_CHANNEL_BUSY);
}
}
bool ctaphid_keepalive(uint8_t status) {
uint8_t packet[HID_PACKET_SIZE];
uint32_t now = platform_millis();
if (now - busy.last_keepalive_ms >= KEEPALIVE_INTERVAL_MS) {
respond(CTAPHID_KEEPALIVE, &status, 1);
busy.last_keepalive_ms = now;
}
while (!busy.cancelled && platform_hid_receive(packet)) ctaphid_handle_packet(packet);
return !busy.cancelled;
}
static void handle_init_packet(uint32_t cid, const uint8_t *packet) {
uint8_t cmd = packet[4];
uint16_t len = (uint16_t)(packet[5] << 8 | packet[6]);
if (cmd == CTAPHID_INIT) {
if (cid == 0)
send_error(cid, ERR_INVALID_CHANNEL);
else
handle_init(cid, packet);
return;
}
if (cid == 0 || cid == CID_BROADCAST) {
send_error(cid, ERR_INVALID_CHANNEL);
return;
}
if (cmd == CTAPHID_CANCEL) return; // nothing in progress to cancel
if (rx.active) {
if (rx.cid != cid) {
send_error(cid, ERR_CHANNEL_BUSY);
return;
}
// A new message before the previous one finished.
rx.active = false;
send_error(cid, ERR_INVALID_SEQ);
return;
}
if (len > MAX_MESSAGE) {
send_error(cid, ERR_INVALID_LEN);
return;
}
size_t n = len < INIT_DATA ? len : INIT_DATA;
memcpy(rx.data, packet + 7, n);
rx.active = true;
rx.cid = cid;
rx.cmd = cmd;
rx.len = len;
rx.received = (uint16_t)n;
rx.next_seq = 0;
rx.last_ms = platform_millis();
}
static void handle_cont_packet(uint32_t cid, const uint8_t *packet) {
// Continuation packets for messages we are not receiving are ignored.
if (!rx.active || rx.cid != cid) return;
if (packet[4] != rx.next_seq) {
rx.active = false;
send_error(cid, ERR_INVALID_SEQ);
return;
}
size_t n = rx.len - rx.received < CONT_DATA ? rx.len - rx.received : CONT_DATA;
memcpy(rx.data + rx.received, packet + 5, n);
rx.received = (uint16_t)(rx.received + n);
rx.next_seq++;
rx.last_ms = platform_millis();
}
void ctaphid_handle_packet(const uint8_t packet[HID_PACKET_SIZE]) {
uint32_t cid = get_be32(packet);
if (busy.active) {
handle_packet_while_busy(cid, packet);
return;
}
if (packet[4] & 0x80)
handle_init_packet(cid, packet);
else
handle_cont_packet(cid, packet);
if (rx.active && rx.received == rx.len) dispatch();
}
void ctaphid_tick(void) {
if (rx.active && platform_millis() - rx.last_ms > MESSAGE_TIMEOUT_MS) {
rx.active = false;
send_error(rx.cid, ERR_MSG_TIMEOUT);
}
}
+23
View File
@@ -0,0 +1,23 @@
// CTAPHID transport (CTAP 2.1, section 11.2): reassembles 64-byte HID reports
// into messages and dispatches them to the U2F and CTAP2 handlers.
#pragma once
#include <stdint.h>
#include "platform.h"
void ctaphid_init(void);
// Handles one output report from the host.
void ctaphid_handle_packet(const uint8_t packet[HID_PACKET_SIZE]);
// Call regularly; expires messages whose continuation packets stopped coming.
void ctaphid_tick(void);
#define CTAPHID_STATUS_PROCESSING 1
#define CTAPHID_STATUS_UPNEEDED 2
// For request handlers that block, e.g. waiting for a touch. Call it in the
// wait loop: it sends periodic keepalives and handles packets that arrive
// meanwhile. Returns false once the host has cancelled the request.
bool ctaphid_keepalive(uint8_t status);
+395
View File
@@ -0,0 +1,395 @@
#include "pin.h"
#include <string.h>
#include "crypto.h"
#include "ctap.h"
#include "platform.h"
#include "storage.h"
#define SUB_GET_RETRIES 0x01
#define SUB_GET_KEY_AGREEMENT 0x02
#define SUB_SET_PIN 0x03
#define SUB_CHANGE_PIN 0x04
#define SUB_GET_PIN_TOKEN 0x05
#define SUB_GET_TOKEN_WITH_PERMISSIONS 0x09
#define MAX_RETRIES 8
#define MAX_CONSECUTIVE_FAILURES 3 // then the key must be unplugged
#define MIN_PIN_CODE_POINTS 4
#define PADDED_PIN_LEN 64
#define PIN_HASH_LEN 16
#define TOKEN_LIFETIME_MS (10 * 60 * 1000)
#define COSE_ALG_ECDH_ES_HKDF_256 (-25)
// Protocol 1 shares a 32-byte AES/HMAC key; protocol 2 an HMAC key followed by an AES key.
#define SHARED_SECRET_MAX 64
#define IV_LEN 16
static pin_state_t state;
static uint8_t consecutive_failures; // since power-up
// Key agreement key pair, new at power-up and after each wrong PIN.
static uint8_t ka_priv[32];
static uint8_t ka_pub[64];
static struct {
bool valid;
uint8_t value[32];
uint8_t permissions;
bool rp_bound;
uint8_t rp_id_hash[32];
uint32_t issued_ms;
} token;
typedef struct {
uint64_t protocol;
uint64_t subcommand;
bool has_key_agreement;
uint8_t key_agreement[64];
const uint8_t *pin_uv_auth_param;
size_t pin_uv_auth_param_len;
const uint8_t *new_pin_enc;
size_t new_pin_enc_len;
const uint8_t *pin_hash_enc;
size_t pin_hash_enc_len;
bool has_permissions;
uint64_t permissions;
const char *rp_id;
size_t rp_id_len;
} request_t;
static void new_key_agreement(void) {
for (int attempt = 0; attempt < 4; attempt++) {
platform_random(ka_priv, sizeof(ka_priv));
if (crypto_p256_public(ka_priv, ka_pub)) return;
}
platform_panic("key agreement key generation failed");
}
static void reset_token(void) {
platform_random(token.value, sizeof(token.value));
token.valid = false;
token.permissions = 0;
token.rp_bound = false;
}
void pin_init(void) {
if (!storage_pin_load(&state)) {
memset(&state, 0, sizeof(state));
state.retries = MAX_RETRIES;
}
consecutive_failures = 0;
new_key_agreement();
reset_token();
}
bool pin_is_set(void) {
return state.set;
}
static bool protocol_supported(uint64_t protocol) {
return protocol == 1 || protocol == 2;
}
// HKDF-SHA-256 with a zero salt and a 32-byte output (RFC 5869).
static void hkdf(const uint8_t ikm[32], const char *info, uint8_t out[32]) {
static const uint8_t zero_salt[32] = {0};
static const uint8_t block_index = 0x01;
uint8_t prk[32];
crypto_part_t extract = {ikm, 32};
crypto_hmac_sha256(zero_salt, &extract, 1, prk);
crypto_part_t expand[] = {{info, strlen(info)}, {&block_index, 1}};
crypto_hmac_sha256(prk, expand, 2, out);
crypto_wipe(prk, sizeof(prk));
}
// The shared secret with the platform's key agreement key (CTAP 2.1, 6.5.6 and 6.5.7).
static int shared_secret(const request_t *req, uint8_t out[SHARED_SECRET_MAX]) {
uint8_t z[32];
if (!req->has_key_agreement) return CTAP2_ERR_MISSING_PARAMETER;
if (!crypto_p256_ecdh(ka_priv, req->key_agreement, z)) return CTAP1_ERR_INVALID_PARAMETER;
if (req->protocol == 1) {
crypto_part_t part = {z, sizeof(z)};
crypto_sha256(&part, 1, out);
} else {
hkdf(z, "CTAP2 HMAC key", out);
hkdf(z, "CTAP2 AES key", out + 32);
}
crypto_wipe(z, sizeof(z));
return 0;
}
// Protocol 1 uses a zero IV; protocol 2 prepends a random one.
static bool decrypt(uint64_t protocol, const uint8_t *shared, const uint8_t *in, size_t in_len, uint8_t *out,
size_t out_len) {
static const uint8_t zero_iv[IV_LEN] = {0};
if (protocol == 1) return in_len == out_len && crypto_aes256_cbc(false, shared, zero_iv, in, out, out_len);
return in_len == IV_LEN + out_len && crypto_aes256_cbc(false, shared + 32, in, in + IV_LEN, out, out_len);
}
static size_t encrypt(uint64_t protocol, const uint8_t *shared, const uint8_t *in, size_t len, uint8_t *out) {
static const uint8_t zero_iv[IV_LEN] = {0};
if (protocol == 1) return crypto_aes256_cbc(true, shared, zero_iv, in, out, len) ? len : 0;
platform_random(out, IV_LEN);
return crypto_aes256_cbc(true, shared + 32, out, in, out + IV_LEN, len) ? IV_LEN + len : 0;
}
// The MAC is HMAC-SHA-256, truncated to 16 bytes in protocol 1. Keys are
// 32 bytes: a pinUvAuthToken, or the first part of the shared secret.
static bool verify(uint64_t protocol, const uint8_t key[32], const crypto_part_t *parts, size_t count,
const uint8_t *mac, size_t mac_len) {
uint8_t expected[32];
crypto_hmac_sha256(key, parts, count, expected);
size_t want = protocol == 1 ? 16 : 32;
return mac && mac_len == want && crypto_equal(expected, mac, want);
}
// Validates a decrypted, zero-padded new PIN and stores it.
static int store_new_pin(const uint8_t padded[PADDED_PIN_LEN]) {
size_t len = 0, code_points = 0;
while (len < PADDED_PIN_LEN && padded[len]) {
if ((padded[len] & 0xc0) != 0x80) code_points++;
len++;
}
if (len == PADDED_PIN_LEN || code_points < MIN_PIN_CODE_POINTS) return CTAP2_ERR_PIN_POLICY_VIOLATION;
uint8_t hash[32];
crypto_part_t part = {padded, len};
crypto_sha256(&part, 1, hash);
state.set = true;
state.retries = MAX_RETRIES;
memcpy(state.hash, hash, PIN_HASH_LEN);
storage_pin_save(&state);
crypto_wipe(hash, sizeof(hash));
return 0;
}
// Checks pinHashEnc against the stored PIN. A retry is spent before the
// comparison, so cutting power mid-check doesn't give a free guess.
static int check_pin(const request_t *req, const uint8_t *shared) {
uint8_t pin_hash[PIN_HASH_LEN];
if (!req->pin_hash_enc) return CTAP2_ERR_MISSING_PARAMETER;
if (state.retries == 0) return CTAP2_ERR_PIN_BLOCKED;
if (consecutive_failures >= MAX_CONSECUTIVE_FAILURES) return CTAP2_ERR_PIN_AUTH_BLOCKED;
state.retries--;
storage_pin_save(&state);
bool match = decrypt(req->protocol, shared, req->pin_hash_enc, req->pin_hash_enc_len, pin_hash,
sizeof(pin_hash)) &&
crypto_equal(pin_hash, state.hash, PIN_HASH_LEN);
crypto_wipe(pin_hash, sizeof(pin_hash));
if (!match) {
new_key_agreement();
consecutive_failures++;
if (state.retries == 0) return CTAP2_ERR_PIN_BLOCKED;
if (consecutive_failures >= MAX_CONSECUTIVE_FAILURES) return CTAP2_ERR_PIN_AUTH_BLOCKED;
return CTAP2_ERR_PIN_INVALID;
}
consecutive_failures = 0;
state.retries = MAX_RETRIES;
storage_pin_save(&state);
return 0;
}
static int set_pin(const request_t *req, const uint8_t *shared) {
uint8_t padded[PADDED_PIN_LEN];
crypto_part_t signed_data = {req->new_pin_enc, req->new_pin_enc_len};
if (!req->new_pin_enc || !req->pin_uv_auth_param) return CTAP2_ERR_MISSING_PARAMETER;
if (state.set) return CTAP2_ERR_PIN_AUTH_INVALID;
if (!verify(req->protocol, shared, &signed_data, 1, req->pin_uv_auth_param, req->pin_uv_auth_param_len))
return CTAP2_ERR_PIN_AUTH_INVALID;
if (!decrypt(req->protocol, shared, req->new_pin_enc, req->new_pin_enc_len, padded, sizeof(padded)))
return CTAP1_ERR_INVALID_PARAMETER;
int err = store_new_pin(padded);
crypto_wipe(padded, sizeof(padded));
return err;
}
static int change_pin(const request_t *req, const uint8_t *shared) {
uint8_t padded[PADDED_PIN_LEN];
crypto_part_t signed_data[] = {
{req->new_pin_enc, req->new_pin_enc_len},
{req->pin_hash_enc, req->pin_hash_enc_len},
};
if (!req->new_pin_enc || !req->pin_hash_enc || !req->pin_uv_auth_param) return CTAP2_ERR_MISSING_PARAMETER;
if (!state.set) return CTAP2_ERR_PIN_NOT_SET;
if (state.retries == 0) return CTAP2_ERR_PIN_BLOCKED;
if (!verify(req->protocol, shared, signed_data, 2, req->pin_uv_auth_param, req->pin_uv_auth_param_len))
return CTAP2_ERR_PIN_AUTH_INVALID;
CHECK(check_pin(req, shared));
if (!decrypt(req->protocol, shared, req->new_pin_enc, req->new_pin_enc_len, padded, sizeof(padded)))
return CTAP1_ERR_INVALID_PARAMETER;
int err = store_new_pin(padded);
crypto_wipe(padded, sizeof(padded));
reset_token();
return err;
}
static int get_token(const request_t *req, const uint8_t *shared, cbor_writer_t *w) {
uint8_t permissions = PIN_PERMISSION_MC | PIN_PERMISSION_GA;
uint8_t encrypted[IV_LEN + sizeof(token.value)];
if (req->subcommand == SUB_GET_TOKEN_WITH_PERMISSIONS) {
if (!req->has_permissions) return CTAP2_ERR_MISSING_PARAMETER;
if (req->permissions == 0) return CTAP1_ERR_INVALID_PARAMETER;
if (req->permissions & ~(uint64_t)(PIN_PERMISSION_MC | PIN_PERMISSION_GA))
return CTAP2_ERR_UNAUTHORIZED_PERMISSION;
permissions = (uint8_t)req->permissions;
}
if (!state.set) return CTAP2_ERR_PIN_NOT_SET;
CHECK(check_pin(req, shared));
// Each new token replaces the previous one.
reset_token();
token.valid = true;
token.permissions = permissions;
token.issued_ms = platform_millis();
if (req->rp_id) {
crypto_part_t part = {req->rp_id, req->rp_id_len};
crypto_sha256(&part, 1, token.rp_id_hash);
token.rp_bound = true;
}
size_t len = encrypt(req->protocol, shared, token.value, sizeof(token.value), encrypted);
if (!len) return CTAP2_ERR_PROCESSING;
cbor_put_map(w, 1);
cbor_put_uint(w, 0x02); // pinUvAuthToken
cbor_put_bytes(w, encrypted, len);
return 0;
}
static void put_key_agreement(cbor_writer_t *w) {
cbor_put_map(w, 1);
cbor_put_uint(w, 0x01); // keyAgreement
cbor_put_map(w, 5);
cbor_put_int(w, 1); // kty: EC2
cbor_put_int(w, 2);
cbor_put_int(w, 3); // alg: ECDH-ES+HKDF-256
cbor_put_int(w, COSE_ALG_ECDH_ES_HKDF_256);
cbor_put_int(w, -1); // crv: P-256
cbor_put_int(w, 1);
cbor_put_int(w, -2);
cbor_put_bytes(w, ka_pub, 32);
cbor_put_int(w, -3);
cbor_put_bytes(w, ka_pub + 32, 32);
}
static int read_coordinate(cbor_reader_t *r, uint8_t out[32]) {
const uint8_t *data;
size_t len;
CHECK(cbor_read_bytes(r, &data, &len));
if (len != 32) return CTAP1_ERR_INVALID_PARAMETER;
memcpy(out, data, 32);
return 0;
}
// The platform's key agreement key: a P-256 COSE_Key.
static int read_key_agreement(cbor_reader_t *r, uint8_t pub[64]) {
size_t n;
int64_t kty = 0, crv = 0;
bool has_x = false, has_y = false;
CHECK(cbor_read_map(r, &n));
for (size_t i = 0; i < n; i++) {
int64_t key;
CHECK(cbor_read_int(r, &key));
switch (key) {
case 1: CHECK(cbor_read_int(r, &kty)); break;
case -1: CHECK(cbor_read_int(r, &crv)); break;
case -2: CHECK(read_coordinate(r, pub)); has_x = true; break;
case -3: CHECK(read_coordinate(r, pub + 32)); has_y = true; break;
default: CHECK(cbor_skip(r)); break; // alg varies between platforms
}
}
return kty == 2 && crv == 1 && has_x && has_y ? 0 : CTAP1_ERR_INVALID_PARAMETER;
}
static int parse_request(cbor_reader_t *r, request_t *req) {
size_t n;
bool has_subcommand = false;
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(cbor_read_uint(r, &req->protocol)); break;
case 0x02: CHECK(cbor_read_uint(r, &req->subcommand)); has_subcommand = true; break;
case 0x03:
CHECK(read_key_agreement(r, req->key_agreement));
req->has_key_agreement = true;
break;
case 0x04: CHECK(cbor_read_bytes(r, &req->pin_uv_auth_param, &req->pin_uv_auth_param_len)); break;
case 0x05: CHECK(cbor_read_bytes(r, &req->new_pin_enc, &req->new_pin_enc_len)); break;
case 0x06: CHECK(cbor_read_bytes(r, &req->pin_hash_enc, &req->pin_hash_enc_len)); break;
case 0x09:
CHECK(cbor_read_uint(r, &req->permissions));
req->has_permissions = true;
break;
case 0x0a: CHECK(cbor_read_text(r, &req->rp_id, &req->rp_id_len)); break;
default: CHECK(cbor_skip(r)); break;
}
}
return has_subcommand ? 0 : CTAP2_ERR_MISSING_PARAMETER;
}
int pin_handle(cbor_reader_t *r, cbor_writer_t *w) {
static request_t req;
uint8_t shared[SHARED_SECRET_MAX];
int err;
memset(&req, 0, sizeof(req));
CHECK(parse_request(r, &req));
if (req.subcommand == SUB_GET_RETRIES) {
cbor_put_map(w, 1);
cbor_put_uint(w, 0x03); // pinRetries
cbor_put_uint(w, state.retries);
return 0;
}
if (!req.protocol) return CTAP2_ERR_MISSING_PARAMETER;
if (!protocol_supported(req.protocol)) return CTAP1_ERR_INVALID_PARAMETER;
switch (req.subcommand) {
case SUB_GET_KEY_AGREEMENT:
put_key_agreement(w);
return 0;
case SUB_SET_PIN:
case SUB_CHANGE_PIN:
case SUB_GET_PIN_TOKEN:
case SUB_GET_TOKEN_WITH_PERMISSIONS:
break;
default:
return CTAP2_ERR_INVALID_SUBCOMMAND; // including the built-in UV ones
}
CHECK(shared_secret(&req, shared));
if (req.subcommand == SUB_SET_PIN)
err = set_pin(&req, shared);
else if (req.subcommand == SUB_CHANGE_PIN)
err = change_pin(&req, shared);
else
err = get_token(&req, shared, w);
crypto_wipe(shared, sizeof(shared));
return err;
}
int pin_check_token(uint64_t protocol, const uint8_t *param, size_t param_len, const uint8_t client_data_hash[32],
uint8_t permission, const uint8_t rp_id_hash[32]) {
crypto_part_t signed_data = {client_data_hash, 32};
if (!protocol_supported(protocol)) return CTAP1_ERR_INVALID_PARAMETER;
if (token.valid && platform_millis() - token.issued_ms > TOKEN_LIFETIME_MS) reset_token();
if (!token.valid || !verify(protocol, token.value, &signed_data, 1, param, param_len))
return CTAP2_ERR_PIN_AUTH_INVALID;
if (!(token.permissions & permission)) return CTAP2_ERR_PIN_AUTH_INVALID;
if (token.rp_bound && memcmp(token.rp_id_hash, rp_id_hash, 32) != 0) return CTAP2_ERR_PIN_AUTH_INVALID;
// A token requested without an RP belongs to the first RP it is used for.
if (!token.rp_bound) {
memcpy(token.rp_id_hash, rp_id_hash, 32);
token.rp_bound = true;
}
return 0;
}
+26
View File
@@ -0,0 +1,26 @@
// clientPIN (CTAP 2.1, section 6.5): the PIN and its retry counters, PIN/UV
// auth protocols 1 and 2, and the pinUvAuthToken.
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "cbor.h"
#define PIN_PERMISSION_MC 0x01 // makeCredential
#define PIN_PERMISSION_GA 0x02 // getAssertion
// Call at power-up, after storage_init(), and again after a reset.
void pin_init(void);
bool pin_is_set(void);
// authenticatorClientPIN.
int pin_handle(cbor_reader_t *r, cbor_writer_t *w);
// Checks a request's pinUvAuthParam: a MAC of clientDataHash under the current
// pinUvAuthToken, which must carry `permission` and belong to this RP.
// Returns a CTAP status code.
int pin_check_token(uint64_t protocol, const uint8_t *param, size_t param_len, const uint8_t client_data_hash[32],
uint8_t permission, const uint8_t rp_id_hash[32]);
+36
View File
@@ -0,0 +1,36 @@
// Hardware abstraction. Implemented by pico/ (firmware) and host/ (simulator).
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#define HID_PACKET_SIZE 64
// Persistent storage is a small region of NOR flash: erase sets a whole sector
// to 0xFF, and programming writes whole pages and can only clear bits.
#define STORAGE_SECTOR_SIZE 4096u
#define STORAGE_PAGE_SIZE 256u
#define STORAGE_SECTORS 16u
#define STORAGE_SIZE (STORAGE_SECTORS * STORAGE_SECTOR_SIZE)
void platform_init(void);
uint32_t platform_millis(void);
void platform_random(uint8_t *buf, size_t len);
void platform_panic(const char *msg);
// The touch button. Each press is reported once: returns true if there is an
// unused press made within the last max_age_ms, and uses it up.
bool platform_button_take_press(uint32_t max_age_ms);
void platform_led(bool on);
void platform_wink(void);
// Offsets are relative to the start of the storage region.
void platform_flash_read(uint32_t offset, void *buf, size_t len);
void platform_flash_erase(uint32_t offset);
void platform_flash_program(uint32_t offset, const void *buf, size_t len);
void platform_hid_send(const uint8_t packet[HID_PACKET_SIZE]);
// Takes the next output report from the host without blocking.
bool platform_hid_receive(uint8_t packet[HID_PACKET_SIZE]);
+60
View File
@@ -0,0 +1,60 @@
#include "presence.h"
#include "ctap.h"
#include "ctaphid.h"
#include "platform.h"
#define WAIT_TIMEOUT_MS 30000
#define WAIT_BLINK_MS 125
// A U2F request succeeds if the button was pressed within this window, which
// covers the gap between the host's retries.
#define U2F_PRESS_MAX_AGE_MS 3000
#define U2F_PROMPT_MS 1000
#define U2F_BLINK_MS 250
static bool prompting;
static uint32_t prompt_until_ms;
int presence_wait(void) {
uint32_t start = platform_millis();
int status = CTAP2_ERR_USER_ACTION_TIMEOUT;
for (;;) {
uint32_t elapsed = platform_millis() - start;
if (elapsed >= WAIT_TIMEOUT_MS) break;
platform_led(elapsed / WAIT_BLINK_MS % 2 == 0);
if (!ctaphid_keepalive(CTAPHID_STATUS_UPNEEDED)) {
status = CTAP2_ERR_KEEPALIVE_CANCEL;
break;
}
// Only a press made after the request arrived counts.
if (platform_button_take_press(elapsed)) {
status = CTAP2_OK;
break;
}
}
platform_led(true); // back to "busy"
return status;
}
bool presence_check(void) {
if (platform_button_take_press(U2F_PRESS_MAX_AGE_MS)) {
prompting = false;
return true;
}
prompting = true;
prompt_until_ms = platform_millis() + U2F_PROMPT_MS;
return false;
}
void presence_tick(void) {
if (!prompting) return;
uint32_t now = platform_millis();
if ((int32_t)(now - prompt_until_ms) >= 0) {
prompting = false;
platform_led(false);
return;
}
platform_led(now / U2F_BLINK_MS % 2 == 0);
}
+17
View File
@@ -0,0 +1,17 @@
// User presence: waiting for a touch of the button.
#pragma once
#include <stdbool.h>
// CTAP2: blocks until the button is pressed, keeping the host informed with
// keepalives. Returns CTAP2_OK, CTAP2_ERR_KEEPALIVE_CANCEL if the host
// cancelled, or CTAP2_ERR_USER_ACTION_TIMEOUT.
int presence_wait(void);
// U2F: hosts retry until a request succeeds, so this never blocks. Returns
// true if the button was pressed recently; otherwise starts blinking the LED
// to ask for a touch and returns false.
bool presence_check(void);
// Call regularly from the main loop to drive the LED between U2F retries.
void presence_tick(void);
+271
View File
@@ -0,0 +1,271 @@
#include "storage.h"
#include <string.h>
#include "ctap.h"
#include "platform.h"
// Layout of the storage region, in 4 KB sectors:
// 0 identity (master secret)
// 1-2 signature counter log, used alternately
// 3-10 resident credentials, 8 slots per sector
// 11-12 PIN state log, used alternately
// 13-15 unused
#define SECTOR(n) ((uint32_t)(n) * STORAGE_SECTOR_SIZE)
#define IDENTITY_OFFSET SECTOR(0)
#define COUNTER_OFFSET SECTOR(1)
#define COUNTER_END SECTOR(3)
#define RK_OFFSET SECTOR(3)
#define RK_PER_SECTOR ((int)(STORAGE_SECTOR_SIZE / sizeof(rk_record_t)))
#define RK_SECTORS (RK_SLOTS / RK_PER_SECTOR)
#define PIN_OFFSET SECTOR(11)
#define PIN_END SECTOR(13)
_Static_assert(RK_OFFSET + SECTOR(RK_SECTORS) <= PIN_OFFSET, "resident credentials overlap the PIN log");
_Static_assert(PIN_END <= STORAGE_SIZE, "storage layout does not fit the region");
#define IDENTITY_MAGIC 0x31494b53u // "SKI1"
#define RK_MAGIC 0x4b524b53u // "SKRK"
#define PIN_MAGIC 0x4e494b53u // "SKIN"
#define ERASED 0xffffffffu
typedef struct {
uint32_t magic;
uint8_t master_secret[32];
} identity_t;
// One entry of the PIN log; the entry with the highest sequence number is current.
typedef struct {
uint32_t magic; // written last
uint32_t seq;
uint8_t set;
uint8_t retries;
uint8_t hash[16];
uint8_t reserved[6];
} pin_record_t;
_Static_assert(STORAGE_PAGE_SIZE % sizeof(pin_record_t) == 0, "PIN records must not straddle pages");
static identity_t identity;
static uint32_t counter;
static uint32_t counter_next_offset;
static uint32_t pin_seq;
static uint32_t pin_next_offset;
static bool is_erased(const void *buf, size_t len) {
const uint8_t *p = buf;
for (size_t i = 0; i < len; i++) {
if (p[i] != 0xff) return false;
}
return true;
}
// Writes arbitrary bytes. Flash programs whole pages, so the data is padded
// with 0xFF, which leaves the rest of each page unchanged.
static void program(uint32_t offset, const void *data, size_t len) {
static uint8_t page[STORAGE_PAGE_SIZE];
const uint8_t *src = data;
while (len > 0) {
uint32_t page_start = offset & ~(STORAGE_PAGE_SIZE - 1);
size_t in_page = offset - page_start;
size_t n = STORAGE_PAGE_SIZE - in_page;
if (n > len) n = len;
memset(page, 0xff, sizeof(page));
memcpy(page + in_page, src, n);
platform_flash_program(page_start, page, sizeof(page));
offset += n;
src += n;
len -= n;
}
}
static void new_identity(void) {
identity.magic = IDENTITY_MAGIC;
platform_random(identity.master_secret, sizeof(identity.master_secret));
platform_flash_erase(IDENTITY_OFFSET);
program(IDENTITY_OFFSET, &identity, sizeof(identity));
}
// The counter log holds increasing values; the current counter is the largest.
static void load_counter(void) {
counter = 0;
counter_next_offset = COUNTER_OFFSET;
for (uint32_t off = COUNTER_OFFSET; off < COUNTER_END; off += sizeof(uint32_t)) {
uint32_t v;
platform_flash_read(off, &v, sizeof(v));
if (v != ERASED && v >= counter) {
counter = v;
counter_next_offset = off + sizeof(uint32_t);
}
}
}
void storage_init(void) {
platform_flash_read(IDENTITY_OFFSET, &identity, sizeof(identity));
if (identity.magic != IDENTITY_MAGIC) {
// First boot, or the region holds something else: start from scratch.
for (uint32_t off = 0; off < STORAGE_SIZE; off += STORAGE_SECTOR_SIZE) platform_flash_erase(off);
new_identity();
}
load_counter();
}
const uint8_t *storage_master_secret(void) {
return identity.master_secret;
}
uint32_t storage_counter_next(void) {
uint32_t off = counter_next_offset;
if (off % STORAGE_SECTOR_SIZE == 0) {
// Starting a fresh sector. The one being erased only holds older
// values; the current value stays in the other sector until the new
// one is written.
if (off == COUNTER_END) off = COUNTER_OFFSET;
platform_flash_erase(off);
}
counter++;
program(off, &counter, sizeof(counter));
counter_next_offset = off + sizeof(counter);
return counter;
}
static uint32_t rk_offset(int slot) {
return RK_OFFSET + (uint32_t)slot * sizeof(rk_record_t);
}
typedef enum { SLOT_FREE, SLOT_LIVE, SLOT_DEAD } slot_state_t;
// Dead slots hold deleted or half-written records and need an erase.
static slot_state_t slot_state(int slot, rk_record_t *rec) {
platform_flash_read(rk_offset(slot), rec, sizeof(*rec));
if (rec->magic == RK_MAGIC) return SLOT_LIVE;
return is_erased(rec, sizeof(*rec)) ? SLOT_FREE : SLOT_DEAD;
}
// The magic goes in last, so an interrupted write never looks valid.
static void write_record(int slot, const rk_record_t *rec) {
static rk_record_t body;
uint32_t magic = RK_MAGIC;
body = *rec;
body.magic = ERASED;
program(rk_offset(slot), &body, sizeof(body));
program(rk_offset(slot), &magic, sizeof(magic));
}
static int find_free_slot(void) {
static rk_record_t scratch;
for (int slot = 0; slot < RK_SLOTS; slot++) {
if (slot_state(slot, &scratch) == SLOT_FREE) return slot;
}
return -1;
}
static int find_same_user(const rk_record_t *rec) {
static rk_record_t cur;
for (int slot = 0; slot < RK_SLOTS; slot++) {
if (storage_rk_read(slot, &cur) && memcmp(cur.rp_id_hash, rec->rp_id_hash, 32) == 0 &&
cur.user_id_len == rec->user_id_len && memcmp(cur.user_id, rec->user_id, rec->user_id_len) == 0)
return slot;
}
return -1;
}
// Reclaims dead slots by rewriting each sector that has any. The sector's
// live records are held in RAM while it is erased.
static void compact(void) {
static rk_record_t live[RK_PER_SECTOR];
for (int sector = 0; sector < RK_SECTORS; sector++) {
int first = sector * RK_PER_SECTOR;
int n = 0;
bool dead = false;
for (int i = 0; i < RK_PER_SECTOR; i++) {
slot_state_t state = slot_state(first + i, &live[n]);
if (state == SLOT_LIVE) n++;
if (state == SLOT_DEAD) dead = true;
}
if (!dead) continue;
platform_flash_erase(RK_OFFSET + SECTOR(sector));
for (int i = 0; i < n; i++) write_record(first + i, &live[i]);
}
}
bool storage_rk_read(int slot, rk_record_t *out) {
platform_flash_read(rk_offset(slot), out, sizeof(*out));
return out->magic == RK_MAGIC && out->user_id_len <= RK_MAX_USER_ID && out->rp_id_len <= RK_MAX_RP_ID &&
out->user_name_len <= RK_MAX_NAME && out->display_name_len <= RK_MAX_NAME;
}
int storage_rk_save(const rk_record_t *rec) {
int old = find_same_user(rec);
int slot = find_free_slot();
if (slot < 0) {
// Make room: drop the credential being replaced, then reclaim dead slots.
if (old >= 0) {
storage_rk_delete(old);
old = -1;
}
compact();
slot = find_free_slot();
if (slot < 0) return CTAP2_ERR_KEY_STORE_FULL;
}
write_record(slot, rec);
if (old >= 0) storage_rk_delete(old);
return CTAP2_OK;
}
void storage_rk_delete(int slot) {
uint32_t zero = 0;
program(rk_offset(slot), &zero, sizeof(zero));
}
void storage_reset(void) {
for (int sector = 0; sector < RK_SECTORS; sector++) platform_flash_erase(RK_OFFSET + SECTOR(sector));
for (uint32_t off = PIN_OFFSET; off < PIN_END; off += STORAGE_SECTOR_SIZE) platform_flash_erase(off);
new_identity();
}
bool storage_pin_load(pin_state_t *out) {
pin_record_t rec;
bool found = false;
pin_seq = 0;
pin_next_offset = PIN_OFFSET;
for (uint32_t off = PIN_OFFSET; off < PIN_END; off += sizeof(rec)) {
platform_flash_read(off, &rec, sizeof(rec));
if (rec.magic != PIN_MAGIC || (found && rec.seq <= pin_seq)) continue;
found = true;
pin_seq = rec.seq;
pin_next_offset = off + sizeof(rec);
out->set = rec.set;
out->retries = rec.retries;
memcpy(out->hash, rec.hash, sizeof(out->hash));
}
return found;
}
// Appends an entry, the same way as the counter log: entries fill one sector,
// then the other sector (which only holds older entries) is erased and used.
void storage_pin_save(const pin_state_t *state) {
pin_record_t rec;
uint32_t off = pin_next_offset;
for (;; off += sizeof(rec)) {
if (off % STORAGE_SECTOR_SIZE == 0) {
if (off == PIN_END) off = PIN_OFFSET;
platform_flash_erase(off);
break;
}
// Skip anything a power cut left half-written.
platform_flash_read(off, &rec, sizeof(rec));
if (is_erased(&rec, sizeof(rec))) break;
}
// The magic goes in last, so an interrupted write never looks valid.
uint32_t magic = PIN_MAGIC;
memset(&rec, 0xff, sizeof(rec));
rec.seq = ++pin_seq;
rec.set = state->set;
rec.retries = state->retries;
memcpy(rec.hash, state->hash, sizeof(rec.hash));
program(off, &rec, sizeof(rec));
program(off, &magic, sizeof(magic));
pin_next_offset = off + sizeof(rec);
}
+60
View File
@@ -0,0 +1,60 @@
// Persistent state: master secret, signature counter and resident credentials.
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include "cred.h"
#define RK_SLOTS 64
#define RK_MAX_USER_ID 64
#define RK_MAX_RP_ID 128
#define RK_MAX_NAME 64
// One resident (discoverable) credential. Exactly one 512-byte flash slot.
typedef struct {
uint32_t magic; // managed by storage.c
uint32_t created; // signature counter at creation, orders credentials
uint8_t rp_id_hash[32];
uint8_t cred_id[CRED_ID_LEN];
uint8_t user_id_len;
uint8_t user_id[RK_MAX_USER_ID];
uint8_t rp_id_len;
char rp_id[RK_MAX_RP_ID];
uint8_t user_name_len;
char user_name[RK_MAX_NAME];
uint8_t display_name_len;
char display_name[RK_MAX_NAME];
uint8_t reserved[99];
} rk_record_t;
_Static_assert(sizeof(rk_record_t) == 512, "rk_record_t must fill one slot");
void storage_init(void);
const uint8_t *storage_master_secret(void);
// Increments the global signature counter and returns the new value.
uint32_t storage_counter_next(void);
// Generates a new master secret and deletes all resident credentials and the PIN.
void storage_reset(void);
// Returns false if the slot does not hold a credential.
bool storage_rk_read(int slot, rk_record_t *out);
// Stores a credential, replacing any with the same RP and user ID. Returns a
// CTAP status code.
int storage_rk_save(const rk_record_t *rec);
void storage_rk_delete(int slot);
// PIN state. The PIN itself is never stored, only LEFT(SHA-256(PIN), 16).
typedef struct {
bool set;
uint8_t retries;
uint8_t hash[16];
} pin_state_t;
// Returns false if no PIN state was saved since the last reset.
bool storage_pin_load(pin_state_t *out);
void storage_pin_save(const pin_state_t *state);
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include <stdint.h>
static inline void put_be32(uint8_t *p, uint32_t v) {
p[0] = (uint8_t)(v >> 24);
p[1] = (uint8_t)(v >> 16);
p[2] = (uint8_t)(v >> 8);
p[3] = (uint8_t)v;
}
static inline uint32_t get_be32(const uint8_t *p) {
return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 | p[3];
}
+106
View File
@@ -0,0 +1,106 @@
"""Runs the authenticator simulator and exposes it as a python-fido2 HID device."""
import os
import pathlib
import subprocess
import threading
import pytest
from fido2.hid import CtapHidDevice
from fido2.hid.base import CtapHidConnection, HidDescriptor
ROOT = pathlib.Path(__file__).resolve().parent.parent
SIMULATOR = pathlib.Path(os.environ.get("SKEY_SIM", ROOT / "build-host" / "skey_sim"))
PACKET_SIZE = 64
class PipeConnection(CtapHidConnection):
"""Carries HID reports over the simulator's stdin/stdout."""
def __init__(self, proc: subprocess.Popen):
self.proc = proc
def write_packet(self, data: bytes) -> None:
assert len(data) == PACKET_SIZE
self.proc.stdin.write(data)
self.proc.stdin.flush()
def read_packet(self) -> bytes:
data = self.proc.stdout.read(PACKET_SIZE)
if len(data) != PACKET_SIZE:
raise OSError(f"simulator exited with {self.proc.wait()}")
return data
def close(self) -> None:
self.proc.stdin.close()
assert self.proc.wait(timeout=10) == 0, "simulator crashed"
class Simulator:
"""One simulated key. Restarting keeps its flash, like unplugging a real one.
Without a button file every request is approved; with one, press() is a
touch of the button.
"""
def __init__(self, flash_file: pathlib.Path, button_file: pathlib.Path | None = None):
self.flash_file = flash_file
self.button_file = button_file
self.device = None
def start(self) -> CtapHidDevice:
if not SIMULATOR.exists():
pytest.fail(f"{SIMULATOR} not found; build it with: cmake -B build-host -DSKEY_HOST=ON && cmake --build build-host")
env = {**os.environ, "SKEY_FLASH_FILE": str(self.flash_file)}
if self.button_file:
env["SKEY_BUTTON_FILE"] = str(self.button_file)
proc = subprocess.Popen([SIMULATOR], stdin=subprocess.PIPE, stdout=subprocess.PIPE, env=env)
descriptor = HidDescriptor(
path="skey-sim",
vid=0x1209,
pid=0x0001,
report_size_in=PACKET_SIZE,
report_size_out=PACKET_SIZE,
product_name="Skey simulator",
serial_number="sim",
)
self.device = CtapHidDevice(descriptor, PipeConnection(proc))
return self.device
def stop(self) -> None:
if self.device:
self.device.close()
self.device = None
def restart(self) -> CtapHidDevice:
self.stop()
return self.start()
def press(self) -> None:
self.button_file.touch()
def press_after(self, seconds: float) -> threading.Timer:
timer = threading.Timer(seconds, self.press)
timer.start()
return timer
@pytest.fixture
def sim(tmp_path):
simulator = Simulator(tmp_path / "flash.bin")
simulator.start()
yield simulator
simulator.stop()
@pytest.fixture
def device(sim):
return sim.device
@pytest.fixture
def button_sim(tmp_path):
simulator = Simulator(tmp_path / "flash.bin", tmp_path / "button")
simulator.start()
yield simulator
simulator.stop()
+55
View File
@@ -0,0 +1,55 @@
import hashlib
import os
import pytest
from fido2.ctap1 import APDU, ApduError, Ctap1
from fido2.ctap2 import Ctap2
APP_ID = hashlib.sha256(b"example.com").digest()
def test_version(device):
assert Ctap1(device).get_version() == "U2F_V2"
def test_register_and_authenticate(device):
ctap1 = Ctap1(device)
challenge = os.urandom(32)
reg = ctap1.register(challenge, APP_ID)
reg.verify(APP_ID, challenge)
challenge = os.urandom(32)
sig = ctap1.authenticate(challenge, APP_ID, reg.key_handle)
sig.verify(APP_ID, challenge, reg.public_key)
assert sig.user_presence & 1
def test_check_only(device):
ctap1 = Ctap1(device)
reg = ctap1.register(os.urandom(32), APP_ID)
with pytest.raises(ApduError) as e:
ctap1.authenticate(os.urandom(32), APP_ID, reg.key_handle, check_only=True)
assert e.value.code == APDU.USE_NOT_SATISFIED
def test_wrong_app_id(device):
ctap1 = Ctap1(device)
reg = ctap1.register(os.urandom(32), APP_ID)
with pytest.raises(ApduError) as e:
ctap1.authenticate(os.urandom(32), hashlib.sha256(b"evil.com").digest(), reg.key_handle)
assert e.value.code == APDU.WRONG_DATA
def test_u2f_credential_works_with_ctap2(device):
reg = Ctap1(device).register(os.urandom(32), APP_ID)
client_data_hash = os.urandom(32)
assertion = Ctap2(device).get_assertion(
"example.com", client_data_hash, allow_list=[{"type": "public-key", "id": reg.key_handle}]
)
assert assertion.credential["id"] == reg.key_handle
def test_unknown_instruction(device):
with pytest.raises(ApduError) as e:
Ctap1(device).send_apdu(ins=0x55)
assert e.value.code == 0x6D00 # INS not supported
+266
View File
@@ -0,0 +1,266 @@
import hashlib
import os
import pytest
from fido2 import cbor
from fido2.attestation import PackedAttestation
from fido2.ctap import CtapError
from fido2.ctap2 import Ctap2
from fido2.hid import CTAPHID
from fido2.webauthn import AuthenticatorData
RP = {"id": "example.com", "name": "Example"}
ALICE = {"id": b"alice-id", "name": "alice", "displayName": "Alice"}
BOB = {"id": b"bob-id", "name": "bob", "displayName": "Bob"}
ES256 = [{"type": "public-key", "alg": -7}]
def descriptor(credential_id):
return {"type": "public-key", "id": credential_id}
def make(ctap, user=ALICE, rp=RP, rk=False, **kwargs):
options = {"rk": True} if rk else None
return ctap.make_credential(os.urandom(32), rp, user, ES256, options=options, **kwargs)
def ctap_error(call):
with pytest.raises(CtapError) as e:
call()
return e.value.code
def test_get_info(device):
info = Ctap2(device).get_info()
assert "FIDO_2_0" in info.versions
assert "U2F_V2" in info.versions
assert info.options["rk"] is True
assert info.options["up"] is True
assert info.options["clientPin"] is False # supported, no PIN set yet
assert info.pin_uv_protocols == [2, 1]
assert len(info.aaguid) == 16
def test_make_credential_attestation(device):
ctap = Ctap2(device)
client_data_hash = os.urandom(32)
att = ctap.make_credential(client_data_hash, RP, ALICE, ES256)
assert att.fmt == "packed"
PackedAttestation().verify(att.att_stmt, att.auth_data, client_data_hash)
assert att.auth_data.rp_id_hash == hashlib.sha256(b"example.com").digest()
assert att.auth_data.is_user_present()
assert not att.auth_data.is_user_verified()
assert att.auth_data.credential_data.aaguid == ctap.info.aaguid
def test_get_assertion(device):
ctap = Ctap2(device)
cred = make(ctap).auth_data.credential_data
client_data_hash = os.urandom(32)
assertion = ctap.get_assertion(RP["id"], client_data_hash, allow_list=[descriptor(cred.credential_id)])
assertion.verify(client_data_hash, cred.public_key)
assert assertion.credential["id"] == cred.credential_id
assert assertion.auth_data.is_user_present()
assert assertion.user is None # not a discoverable credential
def test_counter_increases(device):
ctap = Ctap2(device)
att = make(ctap)
allow = [descriptor(att.auth_data.credential_data.credential_id)]
counters = [att.auth_data.counter]
for _ in range(3):
counters.append(ctap.get_assertion(RP["id"], os.urandom(32), allow_list=allow).auth_data.counter)
assert counters == sorted(set(counters))
def test_picks_own_credential_from_allow_list(device):
ctap = Ctap2(device)
cred = make(ctap).auth_data.credential_data
allow = [descriptor(os.urandom(49)), descriptor(b"short"), descriptor(cred.credential_id)]
assertion = ctap.get_assertion(RP["id"], os.urandom(32), allow_list=allow)
assert assertion.credential["id"] == cred.credential_id
def test_credential_is_bound_to_rp(device):
ctap = Ctap2(device)
cred = make(ctap).auth_data.credential_data
code = ctap_error(lambda: ctap.get_assertion("evil.com", os.urandom(32), allow_list=[descriptor(cred.credential_id)]))
assert code == CtapError.ERR.NO_CREDENTIALS
def test_tampered_credential_rejected(device):
ctap = Ctap2(device)
cred_id = bytearray(make(ctap).auth_data.credential_data.credential_id)
cred_id[5] ^= 1
code = ctap_error(lambda: ctap.get_assertion(RP["id"], os.urandom(32), allow_list=[descriptor(bytes(cred_id))]))
assert code == CtapError.ERR.NO_CREDENTIALS
def test_exclude_list(device):
ctap = Ctap2(device)
cred = make(ctap).auth_data.credential_data
code = ctap_error(lambda: make(ctap, exclude_list=[descriptor(cred.credential_id)]))
assert code == CtapError.ERR.CREDENTIAL_EXCLUDED
# The same ID under another RP isn't ours, so it doesn't block.
make(ctap, rp={"id": "other.com"}, exclude_list=[descriptor(cred.credential_id)])
def test_unsupported_algorithm(device):
ctap = Ctap2(device)
code = ctap_error(lambda: ctap.make_credential(os.urandom(32), RP, ALICE, [{"type": "public-key", "alg": -257}]))
assert code == CtapError.ERR.UNSUPPORTED_ALGORITHM
def test_uv_not_supported(device):
ctap = Ctap2(device)
code = ctap_error(lambda: ctap.make_credential(os.urandom(32), RP, ALICE, ES256, options={"uv": True}))
assert code == CtapError.ERR.UNSUPPORTED_OPTION
def test_missing_parameter(device):
ctap = Ctap2(device)
code = ctap_error(lambda: ctap.send_cbor(Ctap2.CMD.MAKE_CREDENTIAL, {1: os.urandom(32), 2: RP, 4: ES256}))
assert code == CtapError.ERR.MISSING_PARAMETER
def test_empty_pin_auth_means_select(device):
ctap = Ctap2(device)
code = ctap_error(lambda: make(ctap, pin_uv_param=b"", pin_uv_protocol=1))
assert code == CtapError.ERR.PIN_NOT_SET
def test_silent_assertion(device):
ctap = Ctap2(device)
cred = make(ctap).auth_data.credential_data
assertion = ctap.get_assertion(
RP["id"], os.urandom(32), allow_list=[descriptor(cred.credential_id)], options={"up": False}
)
assert not assertion.auth_data.is_user_present()
def test_discoverable_credentials(device):
ctap = Ctap2(device)
alice = make(ctap, ALICE, rk=True).auth_data.credential_data
bob = make(ctap, BOB, rk=True).auth_data.credential_data
make(ctap, ALICE, rp={"id": "other.com"}, rk=True)
client_data_hash = os.urandom(32)
first = ctap.get_assertion(RP["id"], client_data_hash)
second = ctap.get_next_assertion()
assert first.number_of_credentials == 2
assert second.number_of_credentials is None
# Newest first; without user verification only the user ID is returned.
assert first.user == {"id": BOB["id"]}
assert second.user == {"id": ALICE["id"]}
first.verify(client_data_hash, bob.public_key)
second.verify(client_data_hash, alice.public_key)
assert ctap_error(ctap.get_next_assertion) == CtapError.ERR.NOT_ALLOWED
def test_discoverable_via_allow_list_includes_user(device):
ctap = Ctap2(device)
cred = make(ctap, ALICE, rk=True).auth_data.credential_data
assertion = ctap.get_assertion(RP["id"], os.urandom(32), allow_list=[descriptor(cred.credential_id)])
assert assertion.user == {"id": ALICE["id"]}
def test_no_discoverable_credentials(device):
ctap = Ctap2(device)
make(ctap) # not discoverable
assert ctap_error(lambda: ctap.get_assertion(RP["id"], os.urandom(32))) == CtapError.ERR.NO_CREDENTIALS
def test_same_user_replaces_credential(device):
ctap = Ctap2(device)
make(ctap, ALICE, rk=True)
newest = make(ctap, ALICE, rk=True).auth_data.credential_data
assertion = ctap.get_assertion(RP["id"], os.urandom(32))
assert assertion.number_of_credentials is None
assert assertion.credential["id"] == newest.credential_id
def test_replacing_many_times_reclaims_space(device):
# Each replacement leaves a dead slot; this needs several compactions.
ctap = Ctap2(device)
for _ in range(150):
cred = make(ctap, ALICE, rk=True).auth_data.credential_data
assertion = ctap.get_assertion(RP["id"], os.urandom(32))
assert assertion.credential["id"] == cred.credential_id
assert assertion.number_of_credentials is None
def test_key_store_full(device):
ctap = Ctap2(device)
for i in range(64):
make(ctap, {"id": b"user-%d" % i, "name": "u%d" % i}, rk=True)
code = ctap_error(lambda: make(ctap, {"id": b"one-too-many", "name": "x"}, rk=True))
assert code == CtapError.ERR.KEY_STORE_FULL
# Replacing an existing user still works when full.
make(ctap, {"id": b"user-0", "name": "u0"}, rk=True)
def test_state_survives_restart(sim):
ctap = Ctap2(sim.device)
resident = make(ctap, ALICE, rk=True)
plain = make(ctap, BOB).auth_data.credential_data
ctap = Ctap2(sim.restart())
assertion = ctap.get_assertion(RP["id"], os.urandom(32))
assert assertion.credential["id"] == resident.auth_data.credential_data.credential_id
assertion = ctap.get_assertion(RP["id"], os.urandom(32), allow_list=[descriptor(plain.credential_id)])
assert assertion.auth_data.counter > resident.auth_data.counter
def test_counter_survives_sector_rollover(sim):
# The counter log holds 1024 values per sector; cross into the second sector and back.
ctap = Ctap2(sim.device)
cred = make(ctap).auth_data.credential_data
allow = [descriptor(cred.credential_id)]
last = 0
for _ in range(2100):
counter = ctap.get_assertion(RP["id"], os.urandom(32), allow_list=allow, options={"up": False}).auth_data.counter
assert counter > last
last = counter
ctap = Ctap2(sim.restart())
assert ctap.get_assertion(RP["id"], os.urandom(32), allow_list=allow).auth_data.counter == last + 1
def test_reset(device):
ctap = Ctap2(device)
plain = make(ctap).auth_data.credential_data
make(ctap, ALICE, rk=True)
ctap.reset()
code = ctap_error(lambda: ctap.get_assertion(RP["id"], os.urandom(32), allow_list=[descriptor(plain.credential_id)]))
assert code == CtapError.ERR.NO_CREDENTIALS
assert ctap_error(lambda: ctap.get_assertion(RP["id"], os.urandom(32))) == CtapError.ERR.NO_CREDENTIALS
def test_responses_are_canonical_cbor(device):
# Browsers reject non-canonical CBOR (e.g. map keys out of order); python-fido2
# does not, so compare the raw bytes against a canonical re-encoding.
def raw_call(cmd, request=None):
payload = bytes([cmd]) + (cbor.encode(request) if request else b"")
response = device.call(CTAPHID.CBOR, payload)
assert response[0] == 0
assert cbor.encode(cbor.decode(response[1:])) == response[1:]
return cbor.decode(response[1:])
raw_call(Ctap2.CMD.GET_INFO)
raw_call(Ctap2.CMD.CLIENT_PIN, {1: 2, 2: 2}) # getKeyAgreement
raw_call(Ctap2.CMD.CLIENT_PIN, {2: 1}) # getPinRetries
client_data_hash = os.urandom(32)
att = raw_call(Ctap2.CMD.MAKE_CREDENTIAL, {1: client_data_hash, 2: RP, 3: ALICE, 4: ES256, 7: {"rk": True}})
make(Ctap2(device), BOB, rk=True)
raw_call(Ctap2.CMD.GET_ASSERTION, {1: RP["id"], 2: client_data_hash})
raw_call(Ctap2.CMD.GET_NEXT_ASSERTION)
cred_id = AuthenticatorData(att[2]).credential_data.credential_id
raw_call(Ctap2.CMD.GET_ASSERTION, {1: RP["id"], 2: client_data_hash, 3: [descriptor(cred_id)]})
+62
View File
@@ -0,0 +1,62 @@
import os
import struct
import pytest
from fido2.ctap import CtapError
from fido2.hid import CAPABILITY, CTAPHID
def test_init_assigns_channel(device):
assert device._channel_id not in (0, 0xFFFFFFFF)
assert device.capabilities & CAPABILITY.CBOR
assert device.capabilities & CAPABILITY.WINK
def test_ping_single_packet(device):
assert device.ping(b"hello") == b"hello"
def test_ping_multi_packet(device):
# 57 bytes in the first packet, then 59 per continuation packet.
payload = os.urandom(3000)
assert device.ping(payload) == payload
def test_ping_largest_message(device):
payload = os.urandom(57 + 128 * 59)
assert device.ping(payload) == payload
def test_wink(device):
device.wink()
def test_unknown_command(device):
with pytest.raises(CtapError) as e:
device.call(0x40)
assert e.value.code == CtapError.ERR.INVALID_COMMAND
def test_out_of_order_continuation(device):
conn = device._connection
cid = device._channel_id
conn.write_packet(struct.pack(">IBH", cid, 0x80 | CTAPHID.PING, 100).ljust(64, b"\0"))
conn.write_packet(struct.pack(">IB", cid, 1).ljust(64, b"\0")) # expected seq 0
response = conn.read_packet()
assert response[:7] == struct.pack(">IBH", cid, 0x80 | CTAPHID.ERROR, 1)
assert response[7] == CtapError.ERR.INVALID_SEQ
assert device.ping(b"still alive") == b"still alive"
def test_other_channel_busy_while_receiving(device):
conn = device._connection
cid = device._channel_id
conn.write_packet(struct.pack(">IBH", cid, 0x80 | CTAPHID.PING, 100).ljust(64, b"\0"))
conn.write_packet(struct.pack(">IBH", cid + 1, 0x80 | CTAPHID.PING, 1).ljust(64, b"\0"))
response = conn.read_packet()
assert response[:8] == struct.pack(">IBHB", cid + 1, 0x80 | CTAPHID.ERROR, 1, CtapError.ERR.CHANNEL_BUSY)
# Finish the original message.
conn.write_packet(struct.pack(">IB", cid, 0).ljust(64, b"\0"))
response = conn.read_packet()
assert response[:7] == struct.pack(">IBH", cid, 0x80 | CTAPHID.PING, 100)
conn.read_packet() # continuation packet with the rest of the echo
+212
View File
@@ -0,0 +1,212 @@
"""clientPIN: setting and changing the PIN, retries, and PIN tokens."""
import os
import pytest
from fido2.ctap import CtapError
from fido2.ctap2 import Ctap2
from fido2.ctap2.pin import ClientPin, PinProtocolV1, PinProtocolV2
RP = {"id": "example.com", "name": "Example"}
ALICE = {"id": b"alice-id", "name": "alice", "displayName": "Alice"}
ES256 = [{"type": "public-key", "alg": -7}]
PIN = "1234"
MC_GA = ClientPin.PERMISSION.MAKE_CREDENTIAL | ClientPin.PERMISSION.GET_ASSERTION
@pytest.fixture(params=[PinProtocolV1, PinProtocolV2], ids=["v1", "v2"])
def protocol(request):
return request.param()
def ctap_error(call):
with pytest.raises(CtapError) as e:
call()
return e.value.code
def with_pin(cp, token, client_data_hash):
return {"pin_uv_param": cp.protocol.authenticate(token, client_data_hash), "pin_uv_protocol": cp.protocol.VERSION}
def make(cp, token=None, rp=RP, rk=False):
client_data_hash = os.urandom(32)
pin = with_pin(cp, token, client_data_hash) if token else {}
options = {"rk": True} if rk else None
return cp.ctap.make_credential(client_data_hash, rp, ALICE, ES256, options=options, **pin)
def assert_with(cp, token, rp_id=RP["id"], allow_list=None):
client_data_hash = os.urandom(32)
pin = with_pin(cp, token, client_data_hash) if token else {}
return cp.ctap.get_assertion(rp_id, client_data_hash, allow_list=allow_list, **pin)
def setup_pin(device, protocol):
cp = ClientPin(Ctap2(device), protocol)
cp.set_pin(PIN)
return cp
def test_set_pin(device, protocol):
cp = setup_pin(device, protocol)
assert Ctap2(device).get_info().options["clientPin"] is True
assert cp.get_pin_retries()[0] == 8
def test_set_pin_only_once(device, protocol):
cp = setup_pin(device, protocol)
assert ctap_error(lambda: cp.set_pin("5678")) == CtapError.ERR.PIN_AUTH_INVALID
def test_pin_needs_four_characters(device, protocol):
cp = ClientPin(Ctap2(device), protocol)
def set_raw(pin: str):
key_agreement, shared = cp._get_shared_secret()
new_pin_enc = cp.protocol.encrypt(shared, pin.encode().ljust(64, b"\0"))
cp.ctap.client_pin(
cp.protocol.VERSION,
ClientPin.CMD.SET_PIN,
key_agreement=key_agreement,
new_pin_enc=new_pin_enc,
pin_uv_param=cp.protocol.authenticate(shared, new_pin_enc),
)
assert ctap_error(lambda: set_raw("123")) == CtapError.ERR.PIN_POLICY_VIOLATION
# Characters, not bytes: three two-byte characters are still too short.
assert ctap_error(lambda: set_raw("ééé")) == CtapError.ERR.PIN_POLICY_VIOLATION
set_raw("éééé")
cp.get_pin_token("éééé")
def test_registration_requires_pin_once_set(device, protocol):
cp = setup_pin(device, protocol)
assert ctap_error(lambda: make(cp)) == CtapError.ERR.PUAT_REQUIRED
def test_register_and_login_with_pin(device, protocol):
cp = setup_pin(device, protocol)
token = cp.get_pin_token(PIN, MC_GA, RP["id"])
att = make(cp, token, rk=True)
assertion = assert_with(cp, token)
assert att.auth_data.is_user_verified()
assert assertion.auth_data.is_user_verified()
# With the PIN verified, the account names are returned too.
assert assertion.user == {"id": ALICE["id"], "name": "alice", "displayName": "Alice"}
assert assertion.credential["id"] == att.auth_data.credential_data.credential_id
def test_login_without_pin_is_not_verified(device, protocol):
cp = setup_pin(device, protocol)
make(cp, cp.get_pin_token(PIN, MC_GA, RP["id"]), rk=True)
assertion = assert_with(cp, None)
assert not assertion.auth_data.is_user_verified()
assert assertion.user == {"id": ALICE["id"]}
def test_wrong_pin_and_power_cycle_lockout(sim, protocol):
cp = setup_pin(sim.device, protocol)
assert ctap_error(lambda: cp.get_pin_token("0000")) == CtapError.ERR.PIN_INVALID
assert cp.get_pin_retries()[0] == 7
assert ctap_error(lambda: cp.get_pin_token("0000")) == CtapError.ERR.PIN_INVALID
assert ctap_error(lambda: cp.get_pin_token("0000")) == CtapError.ERR.PIN_AUTH_BLOCKED
# Locked until unplugged, even with the right PIN.
assert ctap_error(lambda: cp.get_pin_token(PIN)) == CtapError.ERR.PIN_AUTH_BLOCKED
cp = ClientPin(Ctap2(sim.restart()), protocol)
assert cp.get_pin_retries()[0] == 5
cp.get_pin_token(PIN)
assert cp.get_pin_retries()[0] == 8
def test_pin_blocked_after_eight_failures_until_reset(sim, protocol):
cp = setup_pin(sim.device, protocol)
codes = []
for attempt in range(8):
if attempt in (3, 6):
cp = ClientPin(Ctap2(sim.restart()), protocol)
codes.append(ctap_error(lambda: cp.get_pin_token("0000")))
assert codes[-1] == CtapError.ERR.PIN_BLOCKED
assert ctap_error(lambda: cp.get_pin_token(PIN)) == CtapError.ERR.PIN_BLOCKED
ctap = Ctap2(sim.restart())
ctap.reset()
assert ctap.get_info().options["clientPin"] is False
def test_change_pin(device, protocol):
cp = setup_pin(device, protocol)
assert ctap_error(lambda: cp.change_pin("0000", "5678")) == CtapError.ERR.PIN_INVALID
assert cp.get_pin_retries()[0] == 7
cp.change_pin(PIN, "5678")
assert ctap_error(lambda: cp.get_pin_token(PIN)) == CtapError.ERR.PIN_INVALID
cp.get_pin_token("5678")
assert cp.get_pin_retries()[0] == 8
def test_pin_survives_restart(sim, protocol):
setup_pin(sim.device, protocol)
cp = ClientPin(Ctap2(sim.restart()), protocol)
assert cp.ctap.info.options["clientPin"] is True
make(cp, cp.get_pin_token(PIN, MC_GA, RP["id"]))
def test_token_permissions(device, protocol):
cp = setup_pin(device, protocol)
token = cp.get_pin_token(PIN, ClientPin.PERMISSION.GET_ASSERTION, RP["id"])
assert ctap_error(lambda: make(cp, token)) == CtapError.ERR.PIN_AUTH_INVALID
def test_token_bound_to_rp(device, protocol):
cp = setup_pin(device, protocol)
make(cp, cp.get_pin_token(PIN, MC_GA, "other.com"), rp={"id": "other.com"}, rk=True)
token = cp.get_pin_token(PIN, MC_GA, RP["id"])
assert ctap_error(lambda: assert_with(cp, token, rp_id="other.com")) == CtapError.ERR.PIN_AUTH_INVALID
def test_unsupported_permission_costs_no_retry(device, protocol):
cp = setup_pin(device, protocol)
code = ctap_error(lambda: cp.get_pin_token(PIN, ClientPin.PERMISSION.CREDENTIAL_MGMT))
assert code == CtapError.ERR.UNAUTHORIZED_PERMISSION
assert cp.get_pin_retries()[0] == 8
def test_new_token_replaces_old(device, protocol):
cp = setup_pin(device, protocol)
old = cp.get_pin_token(PIN, MC_GA, RP["id"])
cp.get_pin_token(PIN, MC_GA, RP["id"])
assert ctap_error(lambda: make(cp, old)) == CtapError.ERR.PIN_AUTH_INVALID
def test_legacy_get_pin_token(device, protocol, monkeypatch):
# CTAP 2.0 platforms use getPINToken (subcommand 5), which has no permissions.
monkeypatch.setattr(ClientPin, "is_token_supported", staticmethod(lambda info: False))
cp = setup_pin(device, protocol)
token = cp.get_pin_token(PIN)
assert make(cp, token).auth_data.is_user_verified()
def test_bad_pin_auth(device, protocol):
cp = setup_pin(device, protocol)
cp.get_pin_token(PIN, MC_GA, RP["id"])
bogus = {"pin_uv_param": os.urandom(32 if protocol.VERSION == 2 else 16), "pin_uv_protocol": protocol.VERSION}
code = ctap_error(lambda: cp.ctap.make_credential(os.urandom(32), RP, ALICE, ES256, **bogus))
assert code == CtapError.ERR.PIN_AUTH_INVALID
code = ctap_error(lambda: cp.ctap.make_credential(os.urandom(32), RP, ALICE, ES256, pin_uv_param=os.urandom(32)))
assert code == CtapError.ERR.MISSING_PARAMETER
def test_empty_pin_auth_with_pin_set(device, protocol):
# The "touch to select" probe answers PIN_INVALID once a PIN exists.
cp = setup_pin(device, protocol)
code = ctap_error(
lambda: cp.ctap.make_credential(os.urandom(32), RP, ALICE, ES256, pin_uv_param=b"", pin_uv_protocol=protocol.VERSION)
)
assert code == CtapError.ERR.PIN_INVALID
+141
View File
@@ -0,0 +1,141 @@
"""The touch button: requests wait for a press, with keepalives and cancel."""
import os
import struct
import threading
import time
import pytest
from fido2.ctap import STATUS, CtapError
from fido2.ctap1 import APDU, ApduError, Ctap1
from fido2.ctap2 import Ctap2
from fido2.hid import CTAPHID
from fido2 import cbor
RP = {"id": "example.com", "name": "Example"}
ALICE = {"id": b"alice-id", "name": "alice"}
ES256 = [{"type": "public-key", "alg": -7}]
TOUCH_DELAY = 0.3
def make(ctap, **kwargs):
return ctap.make_credential(os.urandom(32), RP, ALICE, ES256, **kwargs)
def test_waits_for_touch_with_keepalives(button_sim):
ctap = Ctap2(button_sim.device)
statuses = []
button_sim.press_after(TOUCH_DELAY)
start = time.monotonic()
att = make(ctap, on_keepalive=statuses.append)
assert time.monotonic() - start >= TOUCH_DELAY
assert STATUS.UPNEEDED in statuses
assert att.auth_data.is_user_present()
def test_assertion_waits_for_touch(button_sim):
ctap = Ctap2(button_sim.device)
button_sim.press_after(0.05) # presses only count once the request has arrived
cred = make(ctap).auth_data.credential_data
allow = [{"type": "public-key", "id": cred.credential_id}]
button_sim.press_after(TOUCH_DELAY)
start = time.monotonic()
assertion = ctap.get_assertion(RP["id"], os.urandom(32), allow_list=allow)
assert time.monotonic() - start >= TOUCH_DELAY
assert assertion.auth_data.is_user_present()
def test_press_before_request_does_not_count(button_sim):
ctap = Ctap2(button_sim.device)
button_sim.press()
time.sleep(0.1)
button_sim.press_after(TOUCH_DELAY)
start = time.monotonic()
make(ctap)
assert time.monotonic() - start >= TOUCH_DELAY
def test_silent_assertion_needs_no_touch(button_sim):
ctap = Ctap2(button_sim.device)
button_sim.press_after(0.05) # presses only count once the request has arrived
cred = make(ctap).auth_data.credential_data
allow = [{"type": "public-key", "id": cred.credential_id}]
assertion = ctap.get_assertion(RP["id"], os.urandom(32), allow_list=allow, options={"up": False})
assert not assertion.auth_data.is_user_present()
def test_cancel(button_sim):
ctap = Ctap2(button_sim.device)
cancel = threading.Event()
threading.Timer(TOUCH_DELAY, cancel.set).start()
with pytest.raises(CtapError) as e:
make(ctap, event=cancel)
assert e.value.code == CtapError.ERR.KEEPALIVE_CANCEL
assert ctap.get_info().versions # still responsive
def test_u2f_fails_until_touched(button_sim):
ctap1 = Ctap1(button_sim.device)
app_id, challenge = os.urandom(32), os.urandom(32)
with pytest.raises(ApduError) as e:
ctap1.register(challenge, app_id)
assert e.value.code == APDU.USE_NOT_SATISFIED
# Hosts retry; a press since the last attempt lets the next one through.
button_sim.press()
reg = ctap1.register(challenge, app_id)
reg.verify(app_id, challenge)
with pytest.raises(ApduError) as e:
ctap1.authenticate(challenge, app_id, reg.key_handle)
assert e.value.code == APDU.USE_NOT_SATISFIED
button_sim.press()
ctap1.authenticate(challenge, app_id, reg.key_handle).verify(app_id, challenge, reg.public_key)
def send_message(conn, cid, cmd, data):
conn.write_packet((struct.pack(">IBH", cid, 0x80 | cmd, len(data)) + data[:57]).ljust(64, b"\0"))
data, seq = data[57:], 0
while data:
conn.write_packet((struct.pack(">IB", cid, seq) + data[:59]).ljust(64, b"\0"))
data, seq = data[59:], seq + 1
def read_until(conn, cid, cmd):
"""Reads packets until an init packet for (cid, cmd); returns all seen."""
seen = []
while True:
packet = conn.read_packet()
r_cid, r_cmd = struct.unpack_from(">IB", packet)
seen.append((r_cid, r_cmd, packet))
if (r_cid, r_cmd) == (cid, 0x80 | cmd):
return seen
def test_other_channels_busy_while_waiting(button_sim):
conn = button_sim.device._connection
cid = button_sim.device._channel_id
request = bytes([Ctap2.CMD.MAKE_CREDENTIAL]) + cbor.encode({1: os.urandom(32), 2: RP, 3: ALICE, 4: ES256})
send_message(conn, cid, CTAPHID.CBOR, request)
read_until(conn, cid, CTAPHID.KEEPALIVE)
send_message(conn, cid + 1, CTAPHID.PING, b"hi")
seen = read_until(conn, cid + 1, CTAPHID.ERROR)
assert seen[-1][2][7] == CtapError.ERR.CHANNEL_BUSY
button_sim.press()
seen = read_until(conn, cid, CTAPHID.CBOR)
assert seen[-1][2][7] == 0 # CTAP2_OK
assert all(r_cid == cid for r_cid, _, _ in seen)
# Drain the rest of the response so the connection closes cleanly.
length = struct.unpack_from(">H", seen[-1][2], 5)[0]
for _ in range((max(0, length - 57) + 58) // 59):
conn.read_packet()
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""Creates the batch attestation key and certificate and emits them as C source.
The key and certificate are kept in --key-dir and reused, so rebuilding keeps
the same attestation identity. Delete that directory to start over.
"""
import argparse
import datetime
import pathlib
import uuid
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.x509.oid import NameOID
AAGUID = uuid.UUID("225e7977-54f1-4d15-8e35-295b11658d5c")
OID_FIDO_AAGUID = x509.ObjectIdentifier("1.3.6.1.4.1.45724.1.1.4")
def create(key_path: pathlib.Path, cert_path: pathlib.Path) -> None:
key = ec.generate_private_key(ec.SECP256R1())
# Subject fields required for packed attestation certificates (WebAuthn 8.2.1).
name = x509.Name(
[
x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Skey DIY"),
x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "Authenticator Attestation"),
x509.NameAttribute(NameOID.COMMON_NAME, "Skey Pico Attestation"),
]
)
now = datetime.datetime.now(datetime.timezone.utc)
cert = (
x509.CertificateBuilder()
.subject_name(name)
.issuer_name(name)
.public_key(key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(now - datetime.timedelta(days=1))
.not_valid_after(datetime.datetime(2049, 12, 31, tzinfo=datetime.timezone.utc))
.add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True)
.add_extension(x509.UnrecognizedExtension(OID_FIDO_AAGUID, b"\x04\x10" + AAGUID.bytes), critical=False)
.sign(key, hashes.SHA256())
)
key_path.parent.mkdir(parents=True, exist_ok=True)
key_path.write_bytes(
key.private_bytes(
serialization.Encoding.PEM,
serialization.PrivateFormat.PKCS8,
serialization.NoEncryption(),
)
)
key_path.chmod(0o600)
cert_path.write_bytes(cert.public_bytes(serialization.Encoding.DER))
def cert_aaguid(cert_der: bytes) -> bytes:
cert = x509.load_der_x509_certificate(cert_der)
try:
return cert.extensions.get_extension_for_oid(OID_FIDO_AAGUID).value.value[2:]
except x509.ExtensionNotFound:
return b""
def c_bytes(data: bytes) -> str:
rows = [", ".join(f"0x{b:02x}" for b in data[i : i + 12]) for i in range(0, len(data), 12)]
return "{\n " + ",\n ".join(rows) + ",\n}"
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--key-dir", type=pathlib.Path, required=True)
parser.add_argument("--out", type=pathlib.Path, required=True)
args = parser.parse_args()
key_path = args.key_dir / "attestation_key.pem"
cert_path = args.key_dir / "attestation_cert.der"
if not key_path.exists() or not cert_path.exists() or cert_aaguid(cert_path.read_bytes()) != AAGUID.bytes:
create(key_path, cert_path)
key = serialization.load_pem_private_key(key_path.read_bytes(), password=None)
private_value = key.private_numbers().private_value.to_bytes(32, "big")
cert_der = cert_path.read_bytes()
args.out.parent.mkdir(parents=True, exist_ok=True)
args.out.write_text(
f"""// Generated by tools/gen_attestation.py. Do not edit.
#include "attestation.h"
const uint8_t skey_aaguid[16] = {c_bytes(AAGUID.bytes)};
const uint8_t attestation_key[32] = {c_bytes(private_value)};
const uint8_t attestation_cert[] = {c_bytes(cert_der)};
const size_t attestation_cert_len = sizeof(attestation_cert);
"""
)
if __name__ == "__main__":
main()
+7
View File
@@ -0,0 +1,7 @@
# Lets snap-packaged Firefox use the Skey security key.
#
# snapd only lets snaps open FIDO keys whose USB IDs are on a fixed list, and
# Skey's pid.codes test ID isn't on it. This adds the tag snapd looks for. The
# file must sort before 70-snap.firefox.rules, so that snapd's own rule sees
# the tag and grants access as soon as the key is plugged in.
SUBSYSTEM=="hidraw", KERNEL=="hidraw*", ATTRS{idVendor}=="1209", ATTRS{idProduct}=="0001", TAG+="snap_firefox_firefox"