CDC Badge OS
Firmware for the CDC Badge v1.0 hardware security key
Loading...
Searching...
No Matches
fido2_storage.cpp
Go to the documentation of this file.
1
5
10#include "cdc_log.h"
11#include "esp_attr.h"
12#include <mbedtls/sha256.h>
13#include <nvs_flash.h>
14#include <nvs.h>
15#include <string.h>
16
18
19static const char* TAG = "FIDO2";
20
22
23#define FIDO2_RMEM_MAGIC "FID2"
24#define FIDO2_RMEM_MAGIC_LEN 4
25#define NVS_NAMESPACE "fido2"
26#define NVS_KEY_COUNTER "auth_cnt"
27#define NVS_KEY_LARGEBLOB "lblob"
28#define NVS_KEY_ALWAYS_UV "always_uv"
29#define NVS_KEY_MIN_PIN "min_pin"
30
31#ifdef __DOXYGEN__
32namespace cdc::mod_fido2 {
33#endif
34
35#pragma pack(push, 1)
36typedef struct {
37 uint8_t magic[FIDO2_RMEM_MAGIC_LEN]; // "FID2"
38 uint8_t rp_id_hash[32]; // SHA-256 of RP ID
39 char rp_id[FIDO2_RP_ID_MAX_LEN]; // RP ID string (for display)
40 uint8_t user_id[FIDO2_USER_ID_MAX_LEN]; // User handle
41 uint8_t user_id_len; // Length of user ID
42 char user_name[FIDO2_USER_NAME_MAX_LEN];// Display name
43 uint32_t sign_count; // Per-credential counter
44 uint8_t cred_id_nonce[16]; // Random nonce for credential ID
45 uint8_t flags; // Flags (resident, cred_protect, etc.)
46 uint8_t cred_protect; // Credential protection level
47 uint8_t curve; // CDC_CURVE_P256 or CDC_CURVE_ED25519
48 uint8_t reserved[7]; // Reserved for future use
49} fido2_stored_cred_t; // Total: ~180 bytes
50#pragma pack(pop)
51
52#ifdef __DOXYGEN__
53} // namespace cdc::mod_fido2
54#endif
55
56#define FIDO2_STORED_SIZE sizeof(fido2_stored_cred_t)
57
59#define FIDO2_FLAG_RESIDENT 0x01
60
62
63EXT_RAM_BSS_ATTR static struct {
65 uint32_t auth_counter;
67
68 // Cached credential info
69 struct {
70 bool valid;
71 uint8_t rp_id_hash[32];
74 uint8_t user_id[FIDO2_USER_ID_MAX_LEN]; // User handle for replacement detection
75 uint8_t user_id_len;
76 uint32_t sign_count;
78 uint8_t cred_protect;
79 uint8_t curve; // CDC_CURVE_P256 or CDC_CURVE_ED25519
81
82 uint8_t cred_count;
83} g_storage = {};
84
85static uint8_t s_ecc_start = 0;
86static uint8_t s_ecc_end = 0;
87static uint16_t s_rmem_start = 0;
88static uint16_t s_rmem_end = 0;
89
97void fido2_storage_set_slot_range(uint8_t ecc_start, uint8_t ecc_end,
98 uint16_t rmem_start, uint16_t rmem_end) {
99 s_ecc_start = ecc_start;
100 s_ecc_end = ecc_end;
101 s_rmem_start = rmem_start;
102 s_rmem_end = rmem_end;
103}
104
109uint8_t fido2_storage_ecc_start(void) { return s_ecc_start; }
110
115uint8_t fido2_storage_ecc_end(void) { return s_ecc_end; }
116
121uint16_t fido2_storage_rmem_start(void) { return s_rmem_start; }
122
127uint16_t fido2_storage_rmem_end(void) { return s_rmem_end; }
128
133static bool slot_range_valid(void) {
135}
136
141static uint16_t ecc_count(void) {
142 if (!slot_range_valid()) return 0;
143 return static_cast<uint16_t>(s_ecc_end - s_ecc_start + 1);
144}
145
150static uint16_t rmem_count(void) {
151 if (!slot_range_valid()) return 0;
152 return static_cast<uint16_t>(s_rmem_end - s_rmem_start + 1);
153}
154
160static bool slot_logical_valid(uint8_t slot) {
161 uint16_t count = ecc_count();
162 return count > 0 && slot < count;
163}
164
170static uint8_t ecc_slot_for_logical(uint8_t slot) {
171 return static_cast<uint8_t>(s_ecc_start + slot);
172}
173
179static uint16_t rmem_slot_for_logical(uint8_t slot) {
180 if (!slot_range_valid()) return 0;
181 uint16_t offset = static_cast<uint16_t>(slot);
182 return static_cast<uint16_t>(s_rmem_start + offset);
183}
184
186
193static bool read_rmem_credential(uint8_t logical_slot, fido2_stored_cred_t* stored) {
194 if (!stored) return false;
195
197 if (!se) return false;
198
199 uint16_t rmem_slot = rmem_slot_for_logical(logical_slot);
200 uint8_t data[256];
201 uint16_t size = 0;
202
203 auto res = se->rmemRead(rmem_slot, data, sizeof(data), &size);
204 if (res != cdc::hal::SeResult::OK || size < FIDO2_STORED_SIZE) {
205 return false;
206 }
207
208 auto* tmp = reinterpret_cast<fido2_stored_cred_t*>(data);
209 if (memcmp(tmp->magic, FIDO2_RMEM_MAGIC, FIDO2_RMEM_MAGIC_LEN) != 0) {
210 return false;
211 }
212
213 memcpy(stored, tmp, sizeof(fido2_stored_cred_t));
214 return true;
215}
216
224static void update_cache_from_stored(uint8_t slot, const fido2_stored_cred_t* stored,
225 bool is_resident) {
226 g_storage.creds[slot].valid = true;
227 memcpy(g_storage.creds[slot].rp_id_hash, stored->rp_id_hash, 32);
228 strncpy(g_storage.creds[slot].rp_id, stored->rp_id, FIDO2_RP_ID_MAX_LEN - 1);
229 strncpy(g_storage.creds[slot].user_name, stored->user_name, FIDO2_USER_NAME_MAX_LEN - 1);
230 g_storage.creds[slot].user_id_len = stored->user_id_len;
231 if (stored->user_id_len > 0) {
232 memcpy(g_storage.creds[slot].user_id, stored->user_id, stored->user_id_len);
233 }
234 g_storage.creds[slot].sign_count = stored->sign_count;
235 g_storage.creds[slot].resident = is_resident;
236 g_storage.creds[slot].cred_protect = stored->cred_protect;
237 g_storage.creds[slot].curve = stored->curve;
238}
239
245static void erase_slot_data(uint8_t logical_slot) {
247 if (!se) return;
248
249 uint8_t phys_slot = ecc_slot_for_logical(logical_slot);
250 se->eccDelete(phys_slot);
251
252 uint16_t rmem_slot = rmem_slot_for_logical(logical_slot);
253 se->rmemErase(rmem_slot);
254}
255
257static constexpr uint8_t DER_TAG_SEQUENCE = 0x30;
258static constexpr uint8_t DER_TAG_INTEGER = 0x02;
260static constexpr uint8_t DER_INTEGER_MSB_MASK = 0x80;
261
271static uint8_t* encode_der_integer(uint8_t* p, const uint8_t* mpi) {
272 // Skip leading zeros, but keep at least one byte. We only strip a zero if
273 // the next byte's MSB is clear, otherwise the zero is required as padding.
274 size_t skip = 0;
275 while (skip + 1 < FIDO2_SIG_COMPONENT_SIZE && mpi[skip] == 0 &&
276 !(mpi[skip + 1] & DER_INTEGER_MSB_MASK)) {
277 skip++;
278 }
279 uint8_t pad = (mpi[skip] & DER_INTEGER_MSB_MASK) ? 1 : 0;
280 uint8_t actual_len = static_cast<uint8_t>(FIDO2_SIG_COMPONENT_SIZE - skip);
281
282 *p++ = DER_TAG_INTEGER;
283 *p++ = static_cast<uint8_t>(pad + actual_len);
284 if (pad) {
285 *p++ = 0x00;
286 }
287 memcpy(p, mpi + skip, actual_len);
288 return p + actual_len;
289}
290
298static uint8_t raw_sig_to_der(const uint8_t raw_sig[FIDO2_SIG_SIZE], uint8_t* der_sig) {
299 uint8_t* p = der_sig;
300 *p++ = DER_TAG_SEQUENCE;
301
302 // Reserve a placeholder for the SEQUENCE length, then encode R and S.
303 uint8_t* len_pos = p++;
304 uint8_t* r_end = encode_der_integer(p, raw_sig);
305 uint8_t* end = encode_der_integer(r_end, raw_sig + FIDO2_SIG_COMPONENT_SIZE);
306
307 *len_pos = static_cast<uint8_t>(end - len_pos - 1);
308 return static_cast<uint8_t>(end - der_sig);
309}
310
311
318static bool write_rmem_credential(uint8_t logical_slot, const fido2_stored_cred_t* stored) {
320 if (!se) return false;
321
322 uint16_t rmem_slot = rmem_slot_for_logical(logical_slot);
323
324 // Erase first (R-Memory requires empty slot)
325 se->rmemErase(rmem_slot);
326
327 if (se->rmemWrite(rmem_slot, reinterpret_cast<const uint8_t*>(stored),
329 LOG_E(TAG, "Failed to write credential metadata to slot %d", rmem_slot);
330 return false;
331 }
332 return true;
333}
334
336
341 nvs_handle_t nvs;
342 esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &nvs);
343 if (err != ESP_OK) {
344 if (err != ESP_ERR_NVS_NOT_FOUND) {
345 LOG_W(TAG, "Failed to open NVS for counter: %s", esp_err_to_name(err));
346 }
347 g_storage.auth_counter = 0;
348 g_storage.counter_loaded = true;
349 return;
350 }
351
352 err = nvs_get_u32(nvs, NVS_KEY_COUNTER, &g_storage.auth_counter);
353 if (err != ESP_OK) {
354 if (err != ESP_ERR_NVS_NOT_FOUND) {
355 LOG_W(TAG, "Failed to read counter from NVS: %s", esp_err_to_name(err));
356 }
357 g_storage.auth_counter = 0;
358 } else {
359 LOG_I(TAG, "Loaded auth counter: %lu", g_storage.auth_counter);
360 }
361
362 nvs_close(nvs);
363 g_storage.counter_loaded = true;
364}
365
371 if (!g_storage.counter_loaded) {
373 }
374 return g_storage.auth_counter;
375}
376
382 if (!g_storage.counter_loaded) {
384 }
385 uint32_t new_value = g_storage.auth_counter + 1;
386
387 nvs_handle_t nvs;
388 esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs);
389 if (err != ESP_OK) {
390 LOG_E(TAG, "Failed to open NVS for counter write: %s", esp_err_to_name(err));
391 return false;
392 }
393
394 err = nvs_set_u32(nvs, NVS_KEY_COUNTER, new_value);
395 if (err != ESP_OK) {
396 LOG_E(TAG, "Failed to set counter in NVS: %s", esp_err_to_name(err));
397 nvs_close(nvs);
398 return false;
399 }
400
401 err = nvs_commit(nvs);
402 if (err != ESP_OK) {
403 LOG_E(TAG, "NVS commit failed for counter: %s", esp_err_to_name(err));
404 nvs_close(nvs);
405 return false;
406 }
407
408 nvs_close(nvs);
409 g_storage.auth_counter = new_value;
410 return true;
411}
412
418 return true;
419}
420
422
424 nvs_handle_t nvs;
425 if (nvs_open(NVS_NAMESPACE, NVS_READONLY, &nvs) != ESP_OK) {
427 }
428 size_t sz = 0;
429 esp_err_t err = nvs_get_blob(nvs, NVS_KEY_LARGEBLOB, nullptr, &sz);
430 nvs_close(nvs);
431 if (err != ESP_OK || sz == 0) {
433 }
434 return static_cast<uint16_t>(sz);
435}
436
437bool fido2_storage_largeblob_get(uint8_t* out, uint16_t max_len, uint16_t* out_len) {
438 if (!out || !out_len) return false;
439
440 bool fall_back_to_empty = false;
441 nvs_handle_t nvs;
442 if (nvs_open(NVS_NAMESPACE, NVS_READONLY, &nvs) != ESP_OK) {
443 fall_back_to_empty = true;
444 } else {
445 size_t sz = max_len;
446 esp_err_t err = nvs_get_blob(nvs, NVS_KEY_LARGEBLOB, out, &sz);
447 nvs_close(nvs);
448 if (err == ESP_OK) {
449 *out_len = static_cast<uint16_t>(sz);
450 return true;
451 }
452 if (err != ESP_ERR_NVS_NOT_FOUND) {
453 LOG_W(TAG, "largeblob read failed: %s", esp_err_to_name(err));
454 return false;
455 }
456 fall_back_to_empty = true;
457 }
458
459 if (fall_back_to_empty) {
460 if (max_len < cdc::mod_fido2::kLargeBlobEmptyLen) return false;
463 return true;
464 }
465 return false;
466}
467
468bool fido2_storage_largeblob_set(const uint8_t* data, uint16_t len) {
469 if (!data || len == 0 || len > cdc::mod_fido2::kLargeBlobMaxArray) return false;
470 nvs_handle_t nvs;
471 if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs) != ESP_OK) return false;
472 esp_err_t err = nvs_set_blob(nvs, NVS_KEY_LARGEBLOB, data, len);
473 if (err == ESP_OK) err = nvs_commit(nvs);
474 nvs_close(nvs);
475 return err == ESP_OK;
476}
477
479 nvs_handle_t nvs;
480 if (nvs_open(NVS_NAMESPACE, NVS_READONLY, &nvs) != ESP_OK) return false;
481 uint8_t v = 0;
482 esp_err_t err = nvs_get_u8(nvs, NVS_KEY_ALWAYS_UV, &v);
483 nvs_close(nvs);
484 return err == ESP_OK && v != 0;
485}
486
487bool fido2_storage_set_always_uv(bool enabled) {
488 nvs_handle_t nvs;
489 if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs) != ESP_OK) return false;
490 esp_err_t err = nvs_set_u8(nvs, NVS_KEY_ALWAYS_UV, enabled ? 1 : 0);
491 if (err == ESP_OK) err = nvs_commit(nvs);
492 nvs_close(nvs);
493 return err == ESP_OK;
494}
495
497 nvs_handle_t nvs;
498 if (nvs_open(NVS_NAMESPACE, NVS_READONLY, &nvs) != ESP_OK) return 0;
499 uint8_t v = 0;
500 esp_err_t err = nvs_get_u8(nvs, NVS_KEY_MIN_PIN, &v);
501 nvs_close(nvs);
502 return (err == ESP_OK) ? v : 0;
503}
504
505bool fido2_storage_set_min_pin_len(uint8_t min_len) {
506 nvs_handle_t nvs;
507 if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs) != ESP_OK) return false;
508 esp_err_t err = nvs_set_u8(nvs, NVS_KEY_MIN_PIN, min_len);
509 if (err == ESP_OK) err = nvs_commit(nvs);
510 nvs_close(nvs);
511 return err == ESP_OK;
512}
513
515 nvs_handle_t nvs;
516 if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs) != ESP_OK) return;
517 nvs_erase_key(nvs, NVS_KEY_LARGEBLOB);
518 nvs_erase_key(nvs, NVS_KEY_ALWAYS_UV);
519 nvs_erase_key(nvs, NVS_KEY_MIN_PIN);
520 nvs_commit(nvs);
521 nvs_close(nvs);
522}
523
525
530uint8_t fido2_storage_init(void) {
531 LOG_I(TAG, "Initializing storage...");
532
533 memset(&g_storage, 0, sizeof(g_storage));
535
537 if (!se) {
538 LOG_E(TAG, "No secure element available");
539 return 0;
540 }
541
542 // Load credential metadata from R-Memory
543 if (!slot_range_valid()) {
544 LOG_E(TAG, "Slot range not configured");
545 return 0;
546 }
547
548 uint16_t count = ecc_count();
549 uint16_t rcount = rmem_count();
550 if (rcount < count) {
551 LOG_E(TAG, "R-Memory range smaller than ECC range");
552 return 0;
553 }
554
555 for (uint8_t i = 0; i < count && i < FIDO2_MAX_CREDENTIALS; i++) {
556 fido2_stored_cred_t stored;
557 if (read_rmem_credential(i, &stored)) {
558 bool is_resident = (stored.flags & FIDO2_FLAG_RESIDENT) != 0;
559 update_cache_from_stored(i, &stored, is_resident);
560 g_storage.cred_count++;
561 LOG_D(TAG, "Found credential %d: %s (curve=%d)", i, stored.rp_id, stored.curve);
562 }
563 }
564
565 g_storage.initialized = true;
566 LOG_I(TAG, "Found %d credentials", g_storage.cred_count);
567 return g_storage.cred_count;
568}
569
571
576uint8_t fido2_storage_count(void) {
577 return g_storage.cred_count;
578}
579
585bool fido2_storage_slot_used(uint8_t slot) {
586 if (!slot_logical_valid(slot)) return false;
587 return g_storage.creds[slot].valid;
588}
589
595 uint16_t count = ecc_count();
596 for (uint8_t i = 0; i < count && i < FIDO2_MAX_CREDENTIALS; i++) {
597 if (!g_storage.creds[i].valid) {
598 return i;
599 }
600 }
601 return -1;
602}
603
612 uint8_t *out_slots, uint8_t max_slots) {
613 uint8_t count = 0;
614
615 uint16_t total = ecc_count();
616 for (uint8_t i = 0; i < total && i < FIDO2_MAX_CREDENTIALS && count < max_slots; i++) {
617 if (g_storage.creds[i].valid &&
618 memcmp(g_storage.creds[i].rp_id_hash, rp_id_hash, 32) == 0) {
619 out_slots[count++] = i;
620 }
621 }
622
623 return count;
624}
625
634 uint8_t *out_slots, uint8_t max_slots) {
635 uint8_t count = 0;
636
637 LOG_D(TAG, "Searching for resident creds, total=%d", g_storage.cred_count);
638 uint16_t total = ecc_count();
639 for (uint8_t i = 0; i < total && i < FIDO2_MAX_CREDENTIALS && count < max_slots; i++) {
640 if (g_storage.creds[i].valid) {
641 bool rp_match = memcmp(g_storage.creds[i].rp_id_hash, rp_id_hash, 32) == 0;
642 LOG_D(TAG, "Slot %d: valid=%d resident=%d rp_match=%d rp=%s",
643 i, g_storage.creds[i].valid, g_storage.creds[i].resident,
644 rp_match, g_storage.creds[i].rp_id);
645 if (g_storage.creds[i].resident && rp_match) {
646 out_slots[count++] = i;
647 }
648 }
649 }
650
651 return count;
652}
653
659bool fido2_storage_is_resident(uint8_t slot) {
660 if (!slot_logical_valid(slot)) return false;
661 return g_storage.creds[slot].valid && g_storage.creds[slot].resident;
662}
663
672 const uint8_t *user_id,
673 uint8_t user_id_len) {
674 if (!rp_id_hash) return -1;
675
676 uint16_t total = ecc_count();
677 for (uint8_t i = 0; i < total && i < FIDO2_MAX_CREDENTIALS; i++) {
678 if (!g_storage.creds[i].valid) continue;
679
680 // Check RP ID hash match
681 if (memcmp(g_storage.creds[i].rp_id_hash, rp_id_hash, 32) != 0) continue;
682
683 // Check User ID match
684 if (g_storage.creds[i].user_id_len != user_id_len) continue;
685 if (user_id_len == 0) {
686 // Both have empty user_id - match!
687 LOG_D(TAG, "Found existing credential in slot %d (empty user_id)", i);
688 return i;
689 }
690 if (user_id && memcmp(g_storage.creds[i].user_id, user_id, user_id_len) == 0) {
691 LOG_D(TAG, "Found existing credential in slot %d for replacement", i);
692 return i;
693 }
694 }
695
696 return -1; // No existing credential found
697}
698
705int8_t fido2_storage_find_slot_by_cred_id(const uint8_t *cred_id, uint16_t cred_id_len) {
706 if (!cred_id || cred_id_len != FIDO2_CRED_ID_LEN) return -1;
707
708 uint8_t slot = cred_id[0];
709 if (!slot_logical_valid(slot) || !g_storage.creds[slot].valid) {
710 return -1;
711 }
712
713 uint8_t stored_id[FIDO2_CRED_ID_LEN];
714 if (!fido2_storage_get_cred_id(slot, stored_id)) {
715 return -1;
716 }
717
718 if (memcmp(stored_id, cred_id, FIDO2_CRED_ID_LEN) != 0) {
719 return -1;
720 }
721
722 return (int8_t)slot;
723}
724
734bool fido2_storage_get_user(uint8_t slot,
735 uint8_t *user_id,
736 uint8_t *user_id_len,
737 char *user_name,
738 size_t user_name_max) {
739 if (!slot_logical_valid(slot) || !g_storage.creds[slot].valid) {
740 return false;
741 }
742
743 fido2_stored_cred_t stored;
744 if (!read_rmem_credential(slot, &stored)) {
745 return false;
746 }
747
748 if (user_id && user_id_len) {
749 uint8_t len = stored.user_id_len;
751 memcpy(user_id, stored.user_id, len);
752 *user_id_len = len;
753 }
754
755 if (user_name && user_name_max > 0) {
756 size_t copy_len = strnlen(stored.user_name, FIDO2_USER_NAME_MAX_LEN);
757 if (copy_len >= user_name_max) copy_len = user_name_max - 1;
758 memcpy(user_name, stored.user_name, copy_len);
759 user_name[copy_len] = '\0';
760 }
761
762 return true;
763}
764
771bool fido2_storage_verify_cred_id(uint8_t slot, const uint8_t *cred_id) {
772 if (!cred_id) return false;
773 uint8_t stored_id[FIDO2_CRED_ID_LEN];
774 if (!fido2_storage_get_cred_id(slot, stored_id)) {
775 return false;
776 }
777 return memcmp(stored_id, cred_id, FIDO2_CRED_ID_LEN) == 0;
778}
779
786bool fido2_storage_get_cred_id(uint8_t slot, uint8_t *out_cred_id) {
787 if (!slot_logical_valid(slot) || !g_storage.creds[slot].valid || !out_cred_id) {
788 return false;
789 }
790
791 fido2_stored_cred_t stored;
792 if (!read_rmem_credential(slot, &stored)) {
793 LOG_E(TAG, "Failed to read credential %d", slot);
794 return false;
795 }
796
797 // Build credential ID: slot (1) + nonce (16) + padding (47) = 64 bytes
798 memset(out_cred_id, 0, FIDO2_CRED_ID_LEN);
799 out_cred_id[0] = slot;
800 memcpy(out_cred_id + 1, stored.cred_id_nonce, 16);
801
802 return true;
803}
804
806
813bool fido2_storage_get_credential(uint8_t slot, fido2_credential_info_t *info) {
814 if (!slot_logical_valid(slot) || !g_storage.creds[slot].valid || !info) {
815 return false;
816 }
817
818 memset(info, 0, sizeof(*info));
819 info->slot = slot;
820 memcpy(info->rp_id_hash, g_storage.creds[slot].rp_id_hash, 32);
821 strncpy(info->rp_id, g_storage.creds[slot].rp_id, FIDO2_RP_ID_MAX_LEN - 1);
822 strncpy(info->user_name, g_storage.creds[slot].user_name, FIDO2_USER_NAME_MAX_LEN - 1);
823 info->user_id_len = 0;
824 info->sign_count = g_storage.creds[slot].sign_count;
825 info->resident_key = g_storage.creds[slot].resident;
826 info->cred_protect = g_storage.creds[slot].cred_protect;
827 info->curve = g_storage.creds[slot].curve;
828
829 // Load user ID from R-Memory (not cached)
830 uint8_t user_id_len = 0;
831 if (fido2_storage_get_user(slot, info->user_id, &user_id_len,
832 info->user_name, FIDO2_USER_NAME_MAX_LEN)) {
833 info->user_id_len = user_id_len;
834 }
835
836 return true;
837}
838
844uint8_t fido2_storage_get_curve(uint8_t slot) {
845 if (!slot_logical_valid(slot) || !g_storage.creds[slot].valid) {
846 return 0xFF; // Invalid
847 }
848 return g_storage.creds[slot].curve;
849}
850
867 const char *rp_id,
868 const uint8_t *rp_id_hash,
869 const uint8_t *user_id,
870 uint8_t user_id_len,
871 const char *user_name,
872 bool resident_key,
873 uint8_t cred_protect,
874 uint8_t curve,
875 uint8_t *out_slot,
876 uint8_t *out_cred_id,
877 uint8_t *out_pubkey
878) {
880 LOG_E(TAG, "User ID too long: %u", user_id_len);
881 return false;
882 }
883
884 // FIDO2 spec: If credential with same RP ID + User ID exists, replace it
886 int8_t slot;
887
888 if (existing_slot >= 0) {
889 // Replace existing credential
890 LOG_I(TAG, "Replacing existing credential in slot %d", existing_slot);
891 slot = existing_slot;
892
893 // Erase existing key and metadata
894 erase_slot_data(static_cast<uint8_t>(slot));
895
896 // Update cache: mark as invalid temporarily, will be re-validated after creation
897 g_storage.creds[slot].valid = false;
898 g_storage.cred_count--;
899 } else {
900 // Find free slot for new credential
902 if (slot < 0) {
903 LOG_E(TAG, "No free slots");
904 return false;
905 }
906 }
907
908 const char *curve_name = (curve == CDC_CURVE_ED25519) ? "Ed25519" : "P-256";
909 LOG_I(TAG, "Creating %s credential in slot %d for %s", curve_name, slot, rp_id);
910
911 // Explicitly erase ECC slot first to ensure it's empty
912 // (handles cache/chip state mismatch)
913 LOG_D(TAG, "Erasing slot %d before key generation", slot);
914 uint8_t phys_slot = ecc_slot_for_logical(static_cast<uint8_t>(slot));
916 if (!se) return false;
917 se->eccDelete(phys_slot);
918
919 // Generate ECC key with requested curve
920 cdc::hal::EccCurve se_curve =
923 if (se->eccGenerate(phys_slot, se_curve) != cdc::hal::SeResult::OK) {
924 LOG_E(TAG, "Failed to generate %s key in slot %d", curve_name, slot);
925 return false;
926 }
927
928 // Read public key
929 // P-256: 64 bytes (X||Y without 0x04 prefix)
930 // Ed25519: 32 bytes
931 uint8_t pubkey[64];
932 uint8_t pubkey_size = (curve == CDC_CURVE_ED25519) ? 32 : 64;
934 if (se->eccGetPublicKey(phys_slot, pubkey, &se_read_curve) != cdc::hal::SeResult::OK) {
935 LOG_E(TAG, "Failed to read public key from slot %d", slot);
936 se->eccDelete(phys_slot);
937 return false;
938 }
939
940 // Generate random nonce for credential ID
941 uint8_t nonce[16];
942 if (!se->getRandom(nonce, 16)) {
943 LOG_E(TAG, "Failed to generate nonce");
944 se->eccDelete(phys_slot);
945 return false;
946 }
947
948 // Build credential ID (64 bytes)
949 // Format: slot (1) + nonce (16) + padding (47)
950 // In production, use HMAC for binding
951 memset(out_cred_id, 0, FIDO2_CRED_ID_LEN);
952 out_cred_id[0] = slot;
953 memcpy(out_cred_id + 1, nonce, 16);
954
955 // Prepare stored credential
956 fido2_stored_cred_t stored;
957 memset(&stored, 0, sizeof(stored));
958 memcpy(stored.magic, FIDO2_RMEM_MAGIC, FIDO2_RMEM_MAGIC_LEN);
959 memcpy(stored.rp_id_hash, rp_id_hash, 32);
960 if (rp_id) {
961 strncpy(stored.rp_id, rp_id, FIDO2_RP_ID_MAX_LEN - 1);
962 }
963 if (user_id && user_id_len > 0) {
964 memcpy(stored.user_id, user_id, user_id_len);
965 stored.user_id_len = user_id_len;
966 }
967 if (user_name) {
968 strncpy(stored.user_name, user_name, FIDO2_USER_NAME_MAX_LEN - 1);
969 }
970 stored.sign_count = 0;
971 memcpy(stored.cred_id_nonce, nonce, 16);
972 stored.flags = resident_key ? FIDO2_FLAG_RESIDENT : 0;
973 stored.cred_protect = cred_protect;
974 stored.curve = curve;
975
976 // Write to R-Memory
977 if (!write_rmem_credential(static_cast<uint8_t>(slot), &stored)) {
978 se->eccDelete(phys_slot);
979 return false;
980 }
981
982 // Update local cache
983 update_cache_from_stored(static_cast<uint8_t>(slot), &stored, resident_key);
984 g_storage.cred_count++;
985
986 // Copy public key output (32 bytes for Ed25519, 64 for P-256)
987 memcpy(out_pubkey, pubkey, pubkey_size);
988 *out_slot = slot;
989
990 LOG_I(TAG, "Created %s credential in slot %d", curve_name, slot);
991 return true;
992}
993
1000 if (!slot_logical_valid(slot) || !g_storage.creds[slot].valid) {
1001 return false;
1002 }
1003
1004 LOG_I(TAG, "Deleting credential in slot %d", slot);
1005
1006 // Erase ECC key and R-Memory
1007 erase_slot_data(slot);
1008
1009 // Update local cache
1010 g_storage.creds[slot].valid = false;
1011 g_storage.cred_count--;
1012
1013 LOG_I(TAG, "Deleted credential in slot %d", slot);
1014 return true;
1015}
1016
1023 if (!slot_logical_valid(slot) || !g_storage.creds[slot].valid) {
1024 return 0;
1025 }
1026
1027 // Increment local cache
1028 g_storage.creds[slot].sign_count++;
1029 uint32_t new_count = g_storage.creds[slot].sign_count;
1030
1031 // Read current stored data from TROPIC01
1032 fido2_stored_cred_t stored;
1033 if (read_rmem_credential(slot, &stored)) {
1034 stored.sign_count = new_count;
1035 if (!write_rmem_credential(slot, &stored)) {
1036 LOG_E(TAG, "CRITICAL: Failed to persist sign count for slot %d!", slot);
1037 }
1038 }
1039
1040 return new_count;
1041}
1042
1044
1054bool fido2_storage_sign(uint8_t slot, const uint8_t *msg, uint16_t msg_len,
1055 uint8_t *signature, uint8_t *sig_len) {
1056 if (!slot_logical_valid(slot) || !g_storage.creds[slot].valid) {
1057 return false;
1058 }
1059
1061 if (!se) return false;
1062
1063 uint8_t raw_sig[FIDO2_SIG_SIZE];
1064 size_t raw_len = sizeof(raw_sig);
1065 uint8_t phys_slot = ecc_slot_for_logical(slot);
1066 if (se->ecdsaSign(phys_slot, msg, msg_len, raw_sig, &raw_len) !=
1068 raw_len != FIDO2_SIG_SIZE) {
1069 LOG_E(TAG, "ECDSA sign failed for slot %d", slot);
1070 return false;
1071 }
1072
1073 // Convert to DER format
1074 *sig_len = raw_sig_to_der(raw_sig, signature);
1075
1076 LOG_D(TAG, "Signed with slot %d, sig_len=%d", slot, *sig_len);
1077 return true;
1078}
1079
1089bool fido2_storage_sign_raw(uint8_t slot, const uint8_t *msg, uint16_t msg_len,
1090 uint8_t *signature, uint8_t *sig_len) {
1091 if (!slot_logical_valid(slot) || !g_storage.creds[slot].valid) {
1092 return false;
1093 }
1094
1095 uint8_t curve = g_storage.creds[slot].curve;
1096
1097 if (curve == CDC_CURVE_ED25519) {
1098 // EdDSA sign: sign message directly
1100 if (!se) return false;
1101
1102 uint8_t phys_slot = ecc_slot_for_logical(slot);
1103 if (se->eddsaSign(phys_slot, msg, msg_len, signature) != cdc::hal::SeResult::OK) {
1104 LOG_E(TAG, "EdDSA sign failed for slot %d", slot);
1105 return false;
1106 }
1107 *sig_len = FIDO2_SIG_SIZE; // Ed25519 signature is always 64 bytes
1108 LOG_D(TAG, "EdDSA signed %d bytes with slot %d", msg_len, slot);
1109 } else {
1111 if (!se) return false;
1112 uint8_t phys_slot = ecc_slot_for_logical(slot);
1113 size_t raw_len = FIDO2_SIG_SIZE;
1114 if (se->ecdsaSign(phys_slot, msg, msg_len, signature, &raw_len) !=
1116 raw_len != FIDO2_SIG_SIZE) {
1117 LOG_E(TAG, "ECDSA sign failed for slot %d", slot);
1118 return false;
1119 }
1120 *sig_len = FIDO2_SIG_SIZE; // Raw P-256 signature (R||S) is always 64 bytes
1121 LOG_D(TAG, "ECDSA signed %d bytes with slot %d", msg_len, slot);
1122 }
1123
1124 return true;
1125}
1126
1137bool fido2_storage_sign_der(uint8_t slot, const uint8_t *msg, uint16_t msg_len,
1138 uint8_t *signature, uint8_t *sig_len) {
1139 if (!slot_logical_valid(slot) || !g_storage.creds[slot].valid) {
1140 return false;
1141 }
1142
1144 if (!se) return false;
1145
1146 uint8_t raw_sig[FIDO2_SIG_SIZE];
1147 size_t raw_len = sizeof(raw_sig);
1148 uint8_t phys_slot = ecc_slot_for_logical(slot);
1149 if (se->ecdsaSign(phys_slot, msg, msg_len, raw_sig, &raw_len) !=
1151 raw_len != FIDO2_SIG_SIZE) {
1152 LOG_E(TAG, "ECDSA sign failed for slot %d", slot);
1153 return false;
1154 }
1155
1156 // Convert to DER format
1157 *sig_len = raw_sig_to_der(raw_sig, signature);
1158
1159 LOG_D(TAG, "Signed DER %d bytes with slot %d, sig_len=%d", msg_len, slot, *sig_len);
1160 return true;
1161}
1162
1169bool fido2_storage_get_pubkey(uint8_t slot, uint8_t *pubkey) {
1170 if (!slot_logical_valid(slot)) {
1171 return false;
1172 }
1173
1175 if (!se) {
1176 return false;
1177 }
1178
1179 uint8_t phys_slot = ecc_slot_for_logical(slot);
1180 if (!se->eccSlotUsed(phys_slot)) {
1181 return false;
1182 }
1183
1185 return se->eccGetPublicKey(phys_slot, pubkey, &curve) == cdc::hal::SeResult::OK;
1186}
static const char * TAG
Portable authenticatorLargeBlobs write-session accumulator and the canonical empty large-blob array c...
CDC Log: logging over TinyUSB CDC and UART.
#define LOG_W(tag, fmt,...)
Definition cdc_log.h:146
#define LOG_D(tag, fmt,...)
Definition cdc_log.h:148
#define LOG_I(tag, fmt,...)
Definition cdc_log.h:147
#define LOG_E(tag, fmt,...)
Definition cdc_log.h:145
uint8_t cred_count
Definition ctap2.cpp:137
bool initialized
Definition ctap2.cpp:62
#define CDC_CURVE_ED25519
Definition fido2.h:23
#define FIDO2_MAX_CREDENTIALS
Definition fido2.h:16
#define FIDO2_CRED_ID_LEN
Definition fido2.h:20
#define FIDO2_RP_ID_MAX_LEN
Definition fido2.h:17
#define FIDO2_USER_NAME_MAX_LEN
Definition fido2.h:19
#define FIDO2_USER_ID_MAX_LEN
Definition fido2.h:18
#define FIDO2_SIG_COMPONENT_SIZE
#define FIDO2_SIG_SIZE
bool fido2_storage_largeblob_get(uint8_t *out, uint16_t max_len, uint16_t *out_len)
bool fido2_storage_get_always_uv(void)
uint8_t cred_protect
uint8_t fido2_storage_ecc_end(void)
Returns configured ECC end slot.
uint8_t curve
bool fido2_storage_set_always_uv(bool enabled)
uint8_t user_id_len
static uint8_t s_ecc_start
bool fido2_storage_largeblob_set(const uint8_t *data, uint16_t len)
bool valid
bool resident
bool fido2_storage_counter_flush(void)
No-op flush retained for API stability; per-increment path commits.
static bool slot_logical_valid(uint8_t slot)
Checks whether logical slot index is within range.
#define FIDO2_RMEM_MAGIC_LEN
uint16_t fido2_storage_rmem_start(void)
Returns configured RMEM start slot.
static void erase_slot_data(uint8_t logical_slot)
Erases ECC key material and R-Memory data for a logical slot.
static bool write_rmem_credential(uint8_t logical_slot, const fido2_stored_cred_t *stored)
Writes credential metadata to R-Memory after erasing the destination slot.
bool fido2_storage_set_min_pin_len(uint8_t min_len)
uint8_t fido2_storage_count(void)
Credential lookup operations using in-memory cache only.
static uint16_t rmem_slot_for_logical(uint8_t slot)
Maps logical slot to physical RMEM slot.
#define NVS_KEY_COUNTER
bool fido2_storage_sign(uint8_t slot, const uint8_t *msg, uint16_t msg_len, uint8_t *signature, uint8_t *sig_len)
Signing operations requiring secure-element access.
static uint8_t raw_sig_to_der(const uint8_t raw_sig[FIDO2_SIG_SIZE], uint8_t *der_sig)
Converts raw 64-byte ECDSA signature (R||S) to DER sequence format.
uint16_t fido2_storage_largeblob_length(void)
authenticatorLargeBlobs and authenticatorConfig persistence (NVS).
static uint16_t s_rmem_start
#define FIDO2_FLAG_RESIDENT
Stored-credential flag bits.
static uint8_t * encode_der_integer(uint8_t *p, const uint8_t *mpi)
Encodes a single ECDSA P-256 component (R or S) as a DER INTEGER.
bool fido2_storage_sign_raw(uint8_t slot, const uint8_t *msg, uint16_t msg_len, uint8_t *signature, uint8_t *sig_len)
Signs message and returns raw signature (EdDSA/ECDSA).
struct @262231322003320050276064353325174062307231151161::@131310070117174352112321004206244146355206237313 creds[FIDO2_MAX_CREDENTIALS]
bool fido2_storage_get_user(uint8_t slot, uint8_t *user_id, uint8_t *user_id_len, char *user_name, size_t user_name_max)
Loads user handle and optional user name for a credential slot.
bool fido2_storage_sign_der(uint8_t slot, const uint8_t *msg, uint16_t msg_len, uint8_t *signature, uint8_t *sig_len)
Signs data and returns DER-encoded signature for U2F compatibility.
uint32_t sign_count
static uint16_t rmem_count(void)
Returns number of configured logical RMEM slots.
uint32_t fido2_storage_counter_get(void)
Returns current global authentication counter.
#define NVS_KEY_LARGEBLOB
uint8_t fido2_storage_ecc_start(void)
Returns configured ECC start slot.
bool fido2_storage_get_credential(uint8_t slot, fido2_credential_info_t *info)
Credential create/read/delete operations.
uint8_t fido2_storage_get_min_pin_len(void)
#define NVS_KEY_ALWAYS_UV
static bool read_rmem_credential(uint8_t logical_slot, fido2_stored_cred_t *stored)
Internal helper functions for slot and cache management.
static uint16_t s_rmem_end
#define NVS_KEY_MIN_PIN
static constexpr uint8_t DER_TAG_INTEGER
bool fido2_storage_verify_cred_id(uint8_t slot, const uint8_t *cred_id)
Verifies credential-id for logical slot.
bool fido2_storage_delete_credential(uint8_t slot)
Deletes credential and associated slot data.
void fido2_storage_config_reset(void)
uint8_t user_id[FIDO2_USER_ID_MAX_LEN]
char rp_id[FIDO2_RP_ID_MAX_LEN]
static uint16_t ecc_count(void)
Returns number of configured logical ECC slots.
static struct @262231322003320050276064353325174062307231151161 g_storage
Runtime storage/cache state.
static bool slot_range_valid(void)
Validates slot-range configuration.
bool fido2_storage_counter_increment(void)
Increments and persists global authentication counter.
uint8_t fido2_storage_find_by_rp_resident(const uint8_t *rp_id_hash, uint8_t *out_slots, uint8_t max_slots)
Finds resident credentials matching RP hash.
bool fido2_storage_get_cred_id(uint8_t slot, uint8_t *out_cred_id)
Builds credential-id blob for logical slot.
int8_t fido2_storage_find_slot_by_cred_id(const uint8_t *cred_id, uint16_t cred_id_len)
Resolves and verifies logical slot from credential-id blob.
bool fido2_storage_get_pubkey(uint8_t slot, uint8_t *pubkey)
Reads public key from secure-element slot.
uint8_t rp_id_hash[32]
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.
#define FIDO2_RMEM_MAGIC
Persistent storage layout definitions.
uint8_t fido2_storage_get_curve(uint8_t slot)
Returns stored curve identifier for slot.
bool fido2_storage_create_credential(const char *rp_id, const uint8_t *rp_id_hash, const uint8_t *user_id, uint8_t user_id_len, const char *user_name, bool resident_key, uint8_t cred_protect, uint8_t curve, uint8_t *out_slot, uint8_t *out_cred_id, uint8_t *out_pubkey)
Creates or replaces credential in secure-element storage.
static constexpr uint8_t DER_INTEGER_MSB_MASK
MSB mask used to detect when DER INTEGER needs a 0x00 padding byte.
uint16_t fido2_storage_rmem_end(void)
Returns configured RMEM end slot.
static uint8_t ecc_slot_for_logical(uint8_t slot)
Maps logical slot to physical ECC slot.
uint8_t fido2_storage_find_by_rp(const uint8_t *rp_id_hash, uint8_t *out_slots, uint8_t max_slots)
Finds credentials matching RP hash.
uint32_t fido2_storage_increment_sign_count(uint8_t slot)
Increments per-credential sign counter and persists metadata.
uint32_t auth_counter
bool fido2_storage_slot_used(uint8_t slot)
Checks whether logical slot is occupied.
#define NVS_NAMESPACE
uint8_t fido2_storage_init(void)
Initialization and cache rebuild routines.
int8_t fido2_storage_find_free_slot(void)
Finds first unused logical slot.
static constexpr uint8_t DER_TAG_SEQUENCE
DER ASN.1 tags used for ECDSA signature encoding.
bool fido2_storage_is_resident(uint8_t slot)
Returns resident-key flag for slot.
static uint8_t s_ecc_end
int8_t fido2_storage_find_by_rp_user(const uint8_t *rp_id_hash, const uint8_t *user_id, uint8_t user_id_len)
Finds credential by RP hash and user handle for replacement logic.
#define FIDO2_STORED_SIZE
void fido2_storage_counter_load(void)
NVS-backed global authentication counter operations.
bool counter_loaded
static void update_cache_from_stored(uint8_t slot, const fido2_stored_cred_t *stored, bool is_resident)
Updates cache entry from stored credential payload.
char user_name[FIDO2_USER_NAME_MAX_LEN]
ISecureElement * getSecureElementInstance()
Returns singleton secure-element stub instance.
constexpr uint16_t kLargeBlobEmptyLen
void sha256(const uint8_t *data, size_t len, uint8_t out[32])
constexpr uint16_t kLargeBlobMaxArray
Largest serialized large-blob array, advertised as maxSerializedLargeBlobArray.
const uint8_t kLargeBlobEmpty[kLargeBlobEmptyLen]
Canonical empty large-blob array bytes.
char user_name[FIDO2_USER_NAME_MAX_LEN]
char rp_id[FIDO2_RP_ID_MAX_LEN]
uint8_t user_id[FIDO2_USER_ID_MAX_LEN]