CDC Badge OS
Firmware for the CDC Badge v1.0 hardware security key
Loading...
Searching...
No Matches
Fido2Module.cpp
Go to the documentation of this file.
4#include "cdc_core/EventBus.h"
5#include "cdc_log.h"
6#include "mod_fido2/Fido2Ui.h"
7#include "mod_fido2/fido2.h"
10#include "mod_fido2/ctaphid.h"
11#include "mod_fido2/u2f.h"
12#include "usb_badge/usb_hid.h"
15#include "serial_cmd/Console.h"
16
17#include <freertos/FreeRTOS.h>
18#include <freertos/queue.h>
19#include <esp_attr.h>
20#include <string.h>
21#include <stdio.h>
22
23static const char* TAG = "FIDO2";
24
25namespace cdc::mod_fido2 {
26
28static const uint8_t s_fido_report_desc[] = {
29 0x06, 0xD0, 0xF1, // Usage Page (FIDO Alliance)
30 0x09, 0x01, // Usage (U2F HID Authenticator Device)
31 0xA1, 0x01, // Collection (Application)
32 0x09, 0x20, // Usage (Input Report Data)
33 0x15, 0x00, // Logical Minimum (0)
34 0x26, 0xFF, 0x00, // Logical Maximum (255)
35 0x75, 0x08, // Report Size (8)
36 0x95, CTAPHID_PACKET_SIZE, // Report Count (64)
37 0x81, 0x02, // Input (Data, Variable, Absolute)
38 0x09, 0x21, // Usage (Output Report Data)
39 0x15, 0x00, // Logical Minimum (0)
40 0x26, 0xFF, 0x00, // Logical Maximum (255)
41 0x75, 0x08, // Report Size (8)
42 0x95, CTAPHID_PACKET_SIZE, // Report Count (64)
43 0x91, 0x02, // Output (Data, Variable, Absolute)
44 0xC0 // End Collection
45};
46
48static constexpr size_t FIDO_QUEUE_SIZE = 8;
49static QueueHandle_t s_rx_queue = nullptr;
50
51struct FidoPacket {
53};
54
56static uint8_t s_hid_instance = 0;
57
67static uint16_t onFidoGetReport(uint8_t report_id, uint8_t report_type,
68 uint8_t* buffer, uint16_t reqlen) {
69 (void)report_id;
70 (void)report_type;
71 (void)buffer;
72 (void)reqlen;
73 return 0; // FIDO doesn't use GET_REPORT
74}
75
83static void onFidoSetReport(uint8_t report_id, uint8_t report_type,
84 uint8_t const* buffer, uint16_t bufsize) {
85 (void)report_id;
86 (void)report_type;
87
88 if (!s_rx_queue || !buffer || bufsize != CTAPHID_PACKET_SIZE) {
89 return;
90 }
91
92 FidoPacket pkt;
93 memcpy(pkt.data, buffer, CTAPHID_PACKET_SIZE);
94
95 // TinyUSB callbacks run in task context, not ISR
96 if (xQueueSend(s_rx_queue, &pkt, 0) != pdTRUE) {
97 LOG_W(TAG, "RX queue full, dropping packet");
98 }
99}
100
106static void onFidoReportComplete(uint8_t const* report, uint16_t len) {
107 (void)report;
108 (void)len;
109}
110
115Fido2Module& Fido2Module::instance() {
116 static Fido2Module inst;
117 return inst;
118}
119
120// ============================================================================
121// ATTEST serial commands: export the attestation public key for CA signing,
122// import the resulting CA-signed certificate, and revert to self-signed.
123// ============================================================================
124
125static constexpr const char* ATTEST_CMD_MODULE = "fido2";
126static bool s_attestCommandsRegistered = false;
127
128EXT_RAM_BSS_ATTR static char s_attestHex[U2F_MAX_ATT_CERT_SIZE * 2 + 8];
129static size_t s_attestHexPos = 0;
130static bool s_attestInputMode = false;
131
133static int attestHexNibble(char c) {
134 if (c >= '0' && c <= '9') return c - '0';
135 if (c >= 'a' && c <= 'f') return c - 'a' + 10;
136 if (c >= 'A' && c <= 'F') return c - 'A' + 10;
137 return -1;
138}
139
141static void attestFinishImport() {
143 s_attestInputMode = false;
145
146 EXT_RAM_BSS_ATTR static uint8_t der[U2F_MAX_ATT_CERT_SIZE];
147 size_t der_len = 0;
148 int hi = -1;
149 for (size_t i = 0; i < s_attestHexPos; ++i) {
150 int v = attestHexNibble(s_attestHex[i]);
151 if (v < 0) continue; // skip whitespace / newlines
152 if (hi < 0) {
153 hi = v;
154 } else {
155 if (der_len >= sizeof(der)) {
156 Console::printf("ERROR: certificate too large\r\n");
157 return;
158 }
159 der[der_len++] = static_cast<uint8_t>((hi << 4) | v);
160 hi = -1;
161 }
162 }
163 if (hi >= 0) {
164 Console::printf("ERROR: odd number of hex digits\r\n");
165 return;
166 }
167 if (der_len == 0) {
168 Console::printf("ERROR: no certificate data\r\n");
169 return;
170 }
171 Console::printf(u2f_import_attestation_cert(der, der_len)
172 ? "OK: attestation certificate imported\r\n"
173 : "ERROR: invalid certificate or key mismatch\r\n");
174}
175
177static bool attestLineInterceptor(const char* line) {
178 if (!s_attestInputMode) return false;
179 if (line && strcmp(line, "---") == 0) {
181 return true;
182 }
183 if (line && strcmp(line, "ABORT") == 0) {
184 s_attestInputMode = false;
185 s_attestHexPos = 0;
187 cdc::serial::Console::printf("Aborted\r\n");
188 return true;
189 }
190 if (line) {
191 for (const char* p = line; *p; ++p) {
193 }
194 }
195 return true;
196}
197
199static void cmd_attest_export(const char* args) {
200 (void)args;
201 uint8_t pub[65];
202 if (!u2f_get_attestation_pubkey(pub)) {
203 cdc::serial::Console::printf("ERROR: attestation key unavailable\r\n");
204 return;
205 }
206 char hex[131];
207 for (size_t i = 0; i < 65; ++i) snprintf(hex + i * 2, 3, "%02X", pub[i]);
208 cdc::serial::Console::printf("OK: attestation public key (P-256, uncompressed)\r\n");
209 cdc::serial::Console::printf("%s\r\n", hex);
210}
211
213static void cmd_attest_import(const char* args) {
214 (void)args;
215 s_attestHexPos = 0;
216 s_attestInputMode = true;
219 "Paste the CA-signed certificate as hex (DER), end with '---' on a new "
220 "line (or 'ABORT'):\r\n");
221}
222
224static void cmd_attest_clear(const char* args) {
225 (void)args;
227 ? "OK: reverted to self-signed attestation\r\n"
228 : "ERROR\r\n");
229}
230
232 {"EXPORT", "", "Print the attestation public key (hex) for CA signing", cmd_attest_export},
233 {"IMPORT", "", "Import a CA-signed attestation certificate (hex DER paste)", cmd_attest_import},
234 {"CLEAR", "", "Remove the imported certificate (revert to self-signed)", cmd_attest_clear},
235 {nullptr, nullptr, nullptr, nullptr},
236};
237
238static void cmd_attest(const char* args) {
240}
241
244 if (s_attestCommandsRegistered) return;
247 {"ATTEST", "Attestation cert: EXPORT/IMPORT/CLEAR", cmd_attest,
249}
250
256 LOG_I(TAG, "Initializing FIDO2 module");
257
258 // Create RX queue
259 if (!s_rx_queue) {
260 s_rx_queue = xQueueCreate(FIDO_QUEUE_SIZE, sizeof(FidoPacket));
261 if (!s_rx_queue) {
262 LOG_E(TAG, "Failed to create RX queue");
263 return false;
264 }
265 }
266
270
271 if (slotRange_.hasEcc && slotRange_.hasRmem) {
272 uint16_t eccCount = static_cast<uint16_t>(slotRange_.eccEnd - slotRange_.eccStart + 1);
273 uint16_t rmemCount = static_cast<uint16_t>(slotRange_.rmemEnd - slotRange_.rmemStart + 1);
274 if (rmemCount < eccCount) {
276 getName(), "FIDO2 R-MEM range smaller than ECC range");
278 return false;
279 }
280 fido2_storage_set_slot_range(slotRange_.eccStart, slotRange_.eccEnd,
281 slotRange_.rmemStart, slotRange_.rmemEnd);
283 } else {
284 core::ModuleRegistry::instance().reportModuleError(getName(), "FIDO2 slot range missing");
286 return false;
287 }
288
290 return true;
291}
292
298 if (state_ != core::ServiceState::INITIALIZED &&
299 state_ != core::ServiceState::STOPPED) {
300 return false;
301 }
302
305 spec.name = "FIDO2";
307 spec.reportDescLen = sizeof(s_fido_report_desc);
308 spec.protocol = 0; // HID_ITF_PROTOCOL_NONE
309 spec.hasOut = true;
315
316 if (!core::UsbManager::instance().registerInterface(core::UsbHidInterface::Fido, getName(), spec)) {
317 LOG_W(TAG, "Failed to register FIDO HID interface");
318 return false;
319 }
320
321 // FIDO is the first HID interface registered, so instance = 0
322 s_hid_instance = 0;
323
324 if (!fido2_is_initialized()) {
325 if (!fido2_init()) {
329 return false;
330 }
331 }
333
334 // Re-apply the persisted CTAP2 setMinPINLength floor to the badge PIN policy.
336
337 static bool sleepHandlerRegistered = false;
338 if (!sleepHandlerRegistered) {
339 auto& bus = core::EventBus::instance();
340 bus.subscribe([](const core::Event&) {
341 if (fido2_ui_abort_prompt()) {
342 LOG_I(TAG, "Aborted active FIDO2 prompt before sleep");
343 }
345 sleepHandlerRegistered = true;
346 }
347
349 return true;
350}
351
360
366 slotRange_ = range;
367}
368
375 req.mapName = getName();
376 req.minEccSlots = 1;
377 req.minRmemSlots = 1;
378 return req;
379}
380
387uint8_t Fido2Module::getMenuItems(core::ModuleMenuItem* items, uint8_t maxItems) {
388 if (!items || maxItems == 0) return 0;
389
390 items[0] = {fido2_ui_get_label(), 50, []() -> ui::IView* {
391 return fido2_ui_get_list_view();
392 }, nullptr, getName(), core::MenuLocation::MAIN_MENU, nullptr};
393
394 return 1;
395}
396
402 if (!s_rx_queue) return false;
403 return uxQueueMessagesWaiting(s_rx_queue) > 0;
404}
405
413
419uint16_t fido2_usb_read(uint8_t* buffer) {
420 if (!s_rx_queue || !buffer) return 0;
421
422 FidoPacket pkt;
423 if (xQueueReceive(s_rx_queue, &pkt, 0) == pdTRUE) {
424 memcpy(buffer, pkt.data, CTAPHID_PACKET_SIZE);
425 return CTAPHID_PACKET_SIZE;
426 }
427 return 0;
428}
429
435bool fido2_usb_write(const uint8_t* buffer) {
436 if (!buffer) return false;
438}
439
440} // namespace cdc::mod_fido2
441
445extern "C" void mod_fido2_register() {
447 auto& module = cdc::mod_fido2::Fido2Module::instance();
448 module.init();
449 });
450}
static const char * TAG
void mod_fido2_register()
Registers FIDO2 module initializer.
CDC Log: logging over TinyUSB CDC and UART.
#define LOG_W(tag, fmt,...)
Definition cdc_log.h:146
#define LOG_I(tag, fmt,...)
Definition cdc_log.h:147
#define LOG_E(tag, fmt,...)
Definition cdc_log.h:145
static EventBus & instance()
Returns singleton event-bus instance.
Definition EventBus.cpp:19
static constexpr uint32_t eventMask(EventType type)
Definition EventBus.h:139
void reportModuleError(const char *name, const char *message)
Records and publishes an operational module error by module name.
bool registerModule(IModule *module)
Registers a module instance in the runtime registry.
static ModuleRegistry & instance()
Returns the singleton module registry instance.
void registerInitializer(ModuleInitFunc initFunc)
Registers a deferred module initializer callback.
void clearModuleErrorByName(const char *name)
Clears stored module error by module name.
static UsbManager & instance()
Returns singleton USB manager instance.
void unregisterInterface(UsbHidInterface type, const char *moduleName)
Unregisters a previously registered HID interface.
uint8_t getMenuItems(core::ModuleMenuItem *items, uint8_t maxItems) override
Provides main-menu entry for FIDO2 credential list.
core::IModule::SlotRequest getSlotRequest() const override
Declares slot requirements for FIDO2 module.
static Fido2Module & instance()
Returns the singleton instance of the FIDO2 module.
const char * getName() const override
Definition Fido2Module.h:9
void setSlotRange(const core::IModule::SlotRange &range) override
Stores slot range assignment.
bool start() override
Starts FIDO2 module, USB HID interface, and core stack.
bool init() override
Initializes FIDO2 module resources and slot mapping.
void stop() override
Stops FIDO2 module and unregisters USB interface.
static void printf(const char *format,...) __attribute__((format(printf
Prints formatted text to console.
Definition Console.cpp:32
virtual void setLineInterceptor(LineInterceptor interceptor)
virtual bool registerCommand(const Command &cmd)=0
#define CTAPHID_PACKET_SIZE
Definition ctaphid.h:13
bool fido2_is_initialized(void)
Indicates whether FIDO2 subsystem is initialized.
Definition fido2.cpp:303
bool fido2_init(void)
Initializes storage, CTAP layers, and starts the processing task.
Definition fido2.cpp:127
void fido2_set_user_presence_callback(fido2_user_presence_cb_t cb)
Sets callback used to request user presence for CTAP operations.
Definition fido2.cpp:167
bool fido2_storage_counter_flush(void)
No-op flush retained for API stability; per-increment path commits.
uint8_t fido2_storage_get_min_pin_len(void)
void fido2_storage_set_slot_range(uint8_t ecc_start, uint8_t ecc_end, uint16_t rmem_start, uint16_t rmem_end)
Configures FIDO2 storage slot ranges.
cdc::ui::IView * fido2_ui_get_list_view()
Returns FIDO2 credential list view.
Definition Fido2Ui.cpp:439
static bool attestLineInterceptor(const char *line)
Line interceptor accumulating the hex DER paste for ATTEST IMPORT.
static constexpr const char * ATTEST_CMD_MODULE
static void cmd_attest_clear(const char *args)
Serial command: drop the imported cert, revert to self-signed.
bool fido2_usb_available()
Indicates whether at least one USB HID packet is queued for FIDO2.
static bool s_attestInputMode
static uint16_t onFidoGetReport(uint8_t report_id, uint8_t report_type, uint8_t *buffer, uint16_t reqlen)
USB HID callbacks for FIDO transport.
bool fido2_ui_abort_prompt()
Forcibly denies any in-flight user-presence prompt.
Definition Fido2Ui.cpp:680
static const cdc::serial::SubCommand kAttestSubs[]
static constexpr size_t FIDO_QUEUE_SIZE
Queue for incoming HID reports.
void fido2_ui_init()
Initializes FIDO2 UI resources and list views.
Definition Fido2Ui.cpp:421
static void onFidoSetReport(uint8_t report_id, uint8_t report_type, uint8_t const *buffer, uint16_t bufsize)
HID SET_REPORT callback queuing incoming CTAPHID packets.
static void onFidoReportComplete(uint8_t const *report, uint16_t len)
HID transfer-complete callback (currently unused).
bool fido2_usb_ready()
Reports whether USB HID endpoint is ready for transmission.
static void attestFinishImport()
Decodes the accumulated hex paste into DER and imports it.
static size_t s_attestHexPos
static char s_attestHex[U2F_MAX_ATT_CERT_SIZE *2+8]
static uint8_t s_hid_instance
HID interface instance index assigned at registration time.
static void cmd_attest_import(const char *args)
Serial command: begin a hex DER paste of a CA-signed certificate.
static bool s_attestCommandsRegistered
uint16_t fido2_usb_read(uint8_t *buffer)
Reads one queued CTAPHID packet from USB RX queue.
static const uint8_t s_fido_report_desc[]
FIDO U2F HID report descriptor (CTAPHID standard).
static QueueHandle_t s_rx_queue
static int attestHexNibble(char c)
Decodes one hex nibble, or -1 for non-hex characters.
static void cmd_attest_export(const char *args)
Serial command: print the attestation public key as hex.
fido2_user_presence_result_t fido2_ui_user_presence_callback(const char *rp_id, fido2_action_t action, const char *user_name)
User-presence callback used by FIDO2 core for approval prompts.
Definition Fido2Ui.cpp:462
static void registerAttestCommands()
Registers the ATTEST serial command group (idempotent).
static void cmd_attest(const char *args)
const char * fido2_ui_get_label()
Returns localized module label for menus.
Definition Fido2Ui.cpp:451
bool fido2_usb_write(const uint8_t *buffer)
Sends one CTAPHID packet over USB HID.
ICommandRegistry & getCommandRegistry()
Returns singleton command-registry interface.
void dispatchSubCommand(const char *parent, const char *args, const SubCommand *table)
Routes a sub-command line to its handler.
Definition SubCommand.h:73
void pin_storage_set_min_pin_floor(uint8_t min_len)
Menu item registered by a module.
Definition IModule.h:29
void(* onReportComplete)(uint8_t const *report, uint16_t len)
Definition UsbManager.h:24
void(* onSetReport)(uint8_t report_id, uint8_t report_type, uint8_t const *buffer, uint16_t bufsize)
Definition UsbManager.h:22
uint16_t(* onGetReport)(uint8_t report_id, uint8_t report_type, uint8_t *buffer, uint16_t reqlen)
Definition UsbManager.h:20
const uint8_t * reportDesc
Definition UsbManager.h:30
UsbInterfaceClass cls
Definition UsbManager.h:28
UsbHidCallbacks callbacks
Definition UsbManager.h:36
uint8_t data[CTAPHID_PACKET_SIZE]
#define U2F_MAX_ATT_CERT_SIZE
Definition u2f.h:44
bool u2f_import_attestation_cert(const uint8_t *der, size_t len)
Definition u2f.cpp:420
bool u2f_clear_attestation_cert(void)
Definition u2f.cpp:441
bool u2f_get_attestation_pubkey(uint8_t out[65])
Definition u2f.cpp:413
bool usb_hid_send_report(uint8_t instance, uint8_t report_id, const uint8_t *data, uint16_t len)
Sends one HID report on the selected interface instance.
Definition usb_hid.cpp:452
bool usb_hid_instance_ready(uint8_t instance)
Returns whether a specific HID instance endpoint is ready.
Definition usb_hid.cpp:440