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:
commit
6c7e6ab5f5
41 files changed
+4218
No files matched your search
@@ -0,0 +1,7 @@
|
|||||||
|
build*/
|
||||||
|
deps/
|
||||||
|
# Attestation private key, created on first build.
|
||||||
|
attestation/
|
||||||
|
skey_flash.bin
|
||||||
|
__pycache__/
|
||||||
|
.pytest_cache/
|
||||||
@@ -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()
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||||
|
}
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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]);
|
||||||
@@ -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]);
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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
@@ -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);
|
||||||
|
}
|
||||||
@@ -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
@@ -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];
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
@@ -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
|
||||||
@@ -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)]})
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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"
|
||||||
Reference in new issue
Block a user