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
9#include "cdc_log.h"
10#include "esp_attr.h"
11#include <mbedtls/sha256.h>
12#include <nvs_flash.h>
13#include <nvs.h>
14#include <string.h>
15
17
18static const char* TAG = "FIDO2";
19
21
22#define FIDO2_RMEM_MAGIC "FID2"
23#define FIDO2_RMEM_MAGIC_LEN 4
24#define NVS_NAMESPACE "fido2"
25#define NVS_KEY_COUNTER "auth_cnt"
26
27#ifdef __DOXYGEN__
28namespace cdc::mod_fido2 {
29#endif
30
31#pragma pack(push, 1)
32typedef struct {
33 uint8_t magic[FIDO2_RMEM_MAGIC_LEN]; // "FID2"
34 uint8_t rp_id_hash[32]; // SHA-256 of RP ID
35 char rp_id[FIDO2_RP_ID_MAX_LEN]; // RP ID string (for display)
36 uint8_t user_id[FIDO2_USER_ID_MAX_LEN]; // User handle
37 uint8_t user_id_len; // Length of user ID
38 char user_name[FIDO2_USER_NAME_MAX_LEN];// Display name
39 uint32_t sign_count; // Per-credential counter
40 uint8_t cred_id_nonce[16]; // Random nonce for credential ID
41 uint8_t flags; // Flags (resident, cred_protect, etc.)
42 uint8_t cred_protect; // Credential protection level
43 uint8_t curve; // CDC_CURVE_P256 or CDC_CURVE_ED25519
44 uint8_t reserved[7]; // Reserved for future use
45} fido2_stored_cred_t; // Total: ~180 bytes
46#pragma pack(pop)
47
48#ifdef __DOXYGEN__
49} // namespace cdc::mod_fido2
50#endif
51
52#define FIDO2_STORED_SIZE sizeof(fido2_stored_cred_t)
53
55#define FIDO2_FLAG_RESIDENT 0x01
56
58
59EXT_RAM_BSS_ATTR static struct {
61 uint32_t auth_counter;
63
64 // Cached credential info
65 struct {
66 bool valid;
67 uint8_t rp_id_hash[32];
70 uint8_t user_id[FIDO2_USER_ID_MAX_LEN]; // User handle for replacement detection
71 uint8_t user_id_len;
72 uint32_t sign_count;
74 uint8_t cred_protect;
75 uint8_t curve; // CDC_CURVE_P256 or CDC_CURVE_ED25519
77
78 uint8_t cred_count;
79} g_storage = {};
80
81static uint8_t s_ecc_start = 0;
82static uint8_t s_ecc_end = 0;
83static uint16_t s_rmem_start = 0;
84static uint16_t s_rmem_end = 0;
85
93void fido2_storage_set_slot_range(uint8_t ecc_start, uint8_t ecc_end,
94 uint16_t rmem_start, uint16_t rmem_end) {
95 s_ecc_start = ecc_start;
96 s_ecc_end = ecc_end;
97 s_rmem_start = rmem_start;
98 s_rmem_end = rmem_end;
99}
100
105uint8_t fido2_storage_ecc_start(void) { return s_ecc_start; }
106
111uint8_t fido2_storage_ecc_end(void) { return s_ecc_end; }
112
117uint16_t fido2_storage_rmem_start(void) { return s_rmem_start; }
118
123uint16_t fido2_storage_rmem_end(void) { return s_rmem_end; }
124
129static bool slot_range_valid(void) {
131}
132
137static uint16_t ecc_count(void) {
138 if (!slot_range_valid()) return 0;
139 return static_cast<uint16_t>(s_ecc_end - s_ecc_start + 1);
140}
141
146static uint16_t rmem_count(void) {
147 if (!slot_range_valid()) return 0;
148 return static_cast<uint16_t>(s_rmem_end - s_rmem_start + 1);
149}
150
156static bool slot_logical_valid(uint8_t slot) {
157 uint16_t count = ecc_count();
158 return count > 0 && slot < count;
159}
160
166static uint8_t ecc_slot_for_logical(uint8_t slot) {
167 return static_cast<uint8_t>(s_ecc_start + slot);
168}
169
175static uint16_t rmem_slot_for_logical(uint8_t slot) {
176 if (!slot_range_valid()) return 0;
177 uint16_t offset = static_cast<uint16_t>(slot);
178 return static_cast<uint16_t>(s_rmem_start + offset);
179}
180
182
189static bool read_rmem_credential(uint8_t logical_slot, fido2_stored_cred_t* stored) {
190 if (!stored) return false;
191
193 if (!se) return false;
194
195 uint16_t rmem_slot = rmem_slot_for_logical(logical_slot);
196 uint8_t data[256];
197 uint16_t size = 0;
198
199 auto res = se->rmemRead(rmem_slot, data, sizeof(data), &size);
200 if (res != cdc::hal::SeResult::OK || size < FIDO2_STORED_SIZE) {
201 return false;
202 }
203
204 auto* tmp = reinterpret_cast<fido2_stored_cred_t*>(data);
205 if (memcmp(tmp->magic, FIDO2_RMEM_MAGIC, FIDO2_RMEM_MAGIC_LEN) != 0) {
206 return false;
207 }
208
209 memcpy(stored, tmp, sizeof(fido2_stored_cred_t));
210 return true;
211}
212
220static void update_cache_from_stored(uint8_t slot, const fido2_stored_cred_t* stored,
221 bool is_resident) {
222 g_storage.creds[slot].valid = true;
223 memcpy(g_storage.creds[slot].rp_id_hash, stored->rp_id_hash, 32);
224 strncpy(g_storage.creds[slot].rp_id, stored->rp_id, FIDO2_RP_ID_MAX_LEN - 1);
225 strncpy(g_storage.creds[slot].user_name, stored->user_name, FIDO2_USER_NAME_MAX_LEN - 1);
226 g_storage.creds[slot].user_id_len = stored->user_id_len;
227 if (stored->user_id_len > 0) {
228 memcpy(g_storage.creds[slot].user_id, stored->user_id, stored->user_id_len);
229 }
230 g_storage.creds[slot].sign_count = stored->sign_count;
231 g_storage.creds[slot].resident = is_resident;
232 g_storage.creds[slot].cred_protect = stored->cred_protect;
233 g_storage.creds[slot].curve = stored->curve;
234}
235
241static void erase_slot_data(uint8_t logical_slot) {
243 if (!se) return;
244
245 uint8_t phys_slot = ecc_slot_for_logical(logical_slot);
246 se->eccDelete(phys_slot);
247
248 uint16_t rmem_slot = rmem_slot_for_logical(logical_slot);
249 se->rmemErase(rmem_slot);
250}
251
253static constexpr uint8_t DER_TAG_SEQUENCE = 0x30;
254static constexpr uint8_t DER_TAG_INTEGER = 0x02;
256static constexpr uint8_t DER_INTEGER_MSB_MASK = 0x80;
257
267static uint8_t* encode_der_integer(uint8_t* p, const uint8_t* mpi) {
268 // Skip leading zeros, but keep at least one byte. We only strip a zero if
269 // the next byte's MSB is clear, otherwise the zero is required as padding.
270 size_t skip = 0;
271 while (skip + 1 < FIDO2_SIG_COMPONENT_SIZE && mpi[skip] == 0 &&
272 !(mpi[skip + 1] & DER_INTEGER_MSB_MASK)) {
273 skip++;
274 }
275 uint8_t pad = (mpi[skip] & DER_INTEGER_MSB_MASK) ? 1 : 0;
276 uint8_t actual_len = static_cast<uint8_t>(FIDO2_SIG_COMPONENT_SIZE - skip);
277
278 *p++ = DER_TAG_INTEGER;
279 *p++ = static_cast<uint8_t>(pad + actual_len);
280 if (pad) {
281 *p++ = 0x00;
282 }
283 memcpy(p, mpi + skip, actual_len);
284 return p + actual_len;
285}
286
294static uint8_t raw_sig_to_der(const uint8_t raw_sig[FIDO2_SIG_SIZE], uint8_t* der_sig) {
295 uint8_t* p = der_sig;
296 *p++ = DER_TAG_SEQUENCE;
297
298 // Reserve a placeholder for the SEQUENCE length, then encode R and S.
299 uint8_t* len_pos = p++;
300 uint8_t* r_end = encode_der_integer(p, raw_sig);
301 uint8_t* end = encode_der_integer(r_end, raw_sig + FIDO2_SIG_COMPONENT_SIZE);
302
303 *len_pos = static_cast<uint8_t>(end - len_pos - 1);
304 return static_cast<uint8_t>(end - der_sig);
305}
306
307
314static bool write_rmem_credential(uint8_t logical_slot, const fido2_stored_cred_t* stored) {
316 if (!se) return false;
317
318 uint16_t rmem_slot = rmem_slot_for_logical(logical_slot);
319
320 // Erase first (R-Memory requires empty slot)
321 se->rmemErase(rmem_slot);
322
323 if (se->rmemWrite(rmem_slot, reinterpret_cast<const uint8_t*>(stored),
325 LOG_E(TAG, "Failed to write credential metadata to slot %d", rmem_slot);
326 return false;
327 }
328 return true;
329}
330
332
337 nvs_handle_t nvs;
338 esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &nvs);
339 if (err != ESP_OK) {
340 if (err != ESP_ERR_NVS_NOT_FOUND) {
341 LOG_W(TAG, "Failed to open NVS for counter: %s", esp_err_to_name(err));
342 }
343 g_storage.auth_counter = 0;
344 g_storage.counter_loaded = true;
345 return;
346 }
347
348 err = nvs_get_u32(nvs, NVS_KEY_COUNTER, &g_storage.auth_counter);
349 if (err != ESP_OK) {
350 if (err != ESP_ERR_NVS_NOT_FOUND) {
351 LOG_W(TAG, "Failed to read counter from NVS: %s", esp_err_to_name(err));
352 }
353 g_storage.auth_counter = 0;
354 } else {
355 LOG_I(TAG, "Loaded auth counter: %lu", g_storage.auth_counter);
356 }
357
358 nvs_close(nvs);
359 g_storage.counter_loaded = true;
360}
361
367 if (!g_storage.counter_loaded) {
369 }
370 return g_storage.auth_counter;
371}
372
378 if (!g_storage.counter_loaded) {
380 }
381 uint32_t new_value = g_storage.auth_counter + 1;
382
383 nvs_handle_t nvs;
384 esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs);
385 if (err != ESP_OK) {
386 LOG_E(TAG, "Failed to open NVS for counter write: %s", esp_err_to_name(err));
387 return false;
388 }
389
390 err = nvs_set_u32(nvs, NVS_KEY_COUNTER, new_value);
391 if (err != ESP_OK) {
392 LOG_E(TAG, "Failed to set counter in NVS: %s", esp_err_to_name(err));
393 nvs_close(nvs);
394 return false;
395 }
396
397 err = nvs_commit(nvs);
398 if (err != ESP_OK) {
399 LOG_E(TAG, "NVS commit failed for counter: %s", esp_err_to_name(err));
400 nvs_close(nvs);
401 return false;
402 }
403
404 nvs_close(nvs);
405 g_storage.auth_counter = new_value;
406 return true;
407}
408
414 return true;
415}
416
418
423uint8_t fido2_storage_init(void) {
424 LOG_I(TAG, "Initializing storage...");
425
426 memset(&g_storage, 0, sizeof(g_storage));
428
430 if (!se) {
431 LOG_E(TAG, "No secure element available");
432 return 0;
433 }
434
435 // Load credential metadata from R-Memory
436 if (!slot_range_valid()) {
437 LOG_E(TAG, "Slot range not configured");
438 return 0;
439 }
440
441 uint16_t count = ecc_count();
442 uint16_t rcount = rmem_count();
443 if (rcount < count) {
444 LOG_E(TAG, "R-Memory range smaller than ECC range");
445 return 0;
446 }
447
448 for (uint8_t i = 0; i < count && i < FIDO2_MAX_CREDENTIALS; i++) {
449 fido2_stored_cred_t stored;
450 if (read_rmem_credential(i, &stored)) {
451 bool is_resident = (stored.flags & FIDO2_FLAG_RESIDENT) != 0;
452 update_cache_from_stored(i, &stored, is_resident);
453 g_storage.cred_count++;
454 LOG_D(TAG, "Found credential %d: %s (curve=%d)", i, stored.rp_id, stored.curve);
455 }
456 }
457
458 g_storage.initialized = true;
459 LOG_I(TAG, "Found %d credentials", g_storage.cred_count);
460 return g_storage.cred_count;
461}
462
464
469uint8_t fido2_storage_count(void) {
470 return g_storage.cred_count;
471}
472
478bool fido2_storage_slot_used(uint8_t slot) {
479 if (!slot_logical_valid(slot)) return false;
480 return g_storage.creds[slot].valid;
481}
482
488 uint16_t count = ecc_count();
489 for (uint8_t i = 0; i < count && i < FIDO2_MAX_CREDENTIALS; i++) {
490 if (!g_storage.creds[i].valid) {
491 return i;
492 }
493 }
494 return -1;
495}
496
505 uint8_t *out_slots, uint8_t max_slots) {
506 uint8_t count = 0;
507
508 uint16_t total = ecc_count();
509 for (uint8_t i = 0; i < total && i < FIDO2_MAX_CREDENTIALS && count < max_slots; i++) {
510 if (g_storage.creds[i].valid &&
511 memcmp(g_storage.creds[i].rp_id_hash, rp_id_hash, 32) == 0) {
512 out_slots[count++] = i;
513 }
514 }
515
516 return count;
517}
518
527 uint8_t *out_slots, uint8_t max_slots) {
528 uint8_t count = 0;
529
530 LOG_D(TAG, "Searching for resident creds, total=%d", g_storage.cred_count);
531 uint16_t total = ecc_count();
532 for (uint8_t i = 0; i < total && i < FIDO2_MAX_CREDENTIALS && count < max_slots; i++) {
533 if (g_storage.creds[i].valid) {
534 bool rp_match = memcmp(g_storage.creds[i].rp_id_hash, rp_id_hash, 32) == 0;
535 LOG_D(TAG, "Slot %d: valid=%d resident=%d rp_match=%d rp=%s",
536 i, g_storage.creds[i].valid, g_storage.creds[i].resident,
537 rp_match, g_storage.creds[i].rp_id);
538 if (g_storage.creds[i].resident && rp_match) {
539 out_slots[count++] = i;
540 }
541 }
542 }
543
544 return count;
545}
546
552bool fido2_storage_is_resident(uint8_t slot) {
553 if (!slot_logical_valid(slot)) return false;
554 return g_storage.creds[slot].valid && g_storage.creds[slot].resident;
555}
556
565 const uint8_t *user_id,
566 uint8_t user_id_len) {
567 if (!rp_id_hash) return -1;
568
569 uint16_t total = ecc_count();
570 for (uint8_t i = 0; i < total && i < FIDO2_MAX_CREDENTIALS; i++) {
571 if (!g_storage.creds[i].valid) continue;
572
573 // Check RP ID hash match
574 if (memcmp(g_storage.creds[i].rp_id_hash, rp_id_hash, 32) != 0) continue;
575
576 // Check User ID match
577 if (g_storage.creds[i].user_id_len != user_id_len) continue;
578 if (user_id_len == 0) {
579 // Both have empty user_id - match!
580 LOG_D(TAG, "Found existing credential in slot %d (empty user_id)", i);
581 return i;
582 }
583 if (user_id && memcmp(g_storage.creds[i].user_id, user_id, user_id_len) == 0) {
584 LOG_D(TAG, "Found existing credential in slot %d for replacement", i);
585 return i;
586 }
587 }
588
589 return -1; // No existing credential found
590}
591
598int8_t fido2_storage_find_slot_by_cred_id(const uint8_t *cred_id, uint16_t cred_id_len) {
599 if (!cred_id || cred_id_len != FIDO2_CRED_ID_LEN) return -1;
600
601 uint8_t slot = cred_id[0];
602 if (!slot_logical_valid(slot) || !g_storage.creds[slot].valid) {
603 return -1;
604 }
605
606 uint8_t stored_id[FIDO2_CRED_ID_LEN];
607 if (!fido2_storage_get_cred_id(slot, stored_id)) {
608 return -1;
609 }
610
611 if (memcmp(stored_id, cred_id, FIDO2_CRED_ID_LEN) != 0) {
612 return -1;
613 }
614
615 return (int8_t)slot;
616}
617
627bool fido2_storage_get_user(uint8_t slot,
628 uint8_t *user_id,
629 uint8_t *user_id_len,
630 char *user_name,
631 size_t user_name_max) {
632 if (!slot_logical_valid(slot) || !g_storage.creds[slot].valid) {
633 return false;
634 }
635
636 fido2_stored_cred_t stored;
637 if (!read_rmem_credential(slot, &stored)) {
638 return false;
639 }
640
641 if (user_id && user_id_len) {
642 uint8_t len = stored.user_id_len;
644 memcpy(user_id, stored.user_id, len);
645 *user_id_len = len;
646 }
647
648 if (user_name && user_name_max > 0) {
649 size_t copy_len = strnlen(stored.user_name, FIDO2_USER_NAME_MAX_LEN);
650 if (copy_len >= user_name_max) copy_len = user_name_max - 1;
651 memcpy(user_name, stored.user_name, copy_len);
652 user_name[copy_len] = '\0';
653 }
654
655 return true;
656}
657
664bool fido2_storage_verify_cred_id(uint8_t slot, const uint8_t *cred_id) {
665 if (!cred_id) return false;
666 uint8_t stored_id[FIDO2_CRED_ID_LEN];
667 if (!fido2_storage_get_cred_id(slot, stored_id)) {
668 return false;
669 }
670 return memcmp(stored_id, cred_id, FIDO2_CRED_ID_LEN) == 0;
671}
672
679bool fido2_storage_get_cred_id(uint8_t slot, uint8_t *out_cred_id) {
680 if (!slot_logical_valid(slot) || !g_storage.creds[slot].valid || !out_cred_id) {
681 return false;
682 }
683
684 fido2_stored_cred_t stored;
685 if (!read_rmem_credential(slot, &stored)) {
686 LOG_E(TAG, "Failed to read credential %d", slot);
687 return false;
688 }
689
690 // Build credential ID: slot (1) + nonce (16) + padding (47) = 64 bytes
691 memset(out_cred_id, 0, FIDO2_CRED_ID_LEN);
692 out_cred_id[0] = slot;
693 memcpy(out_cred_id + 1, stored.cred_id_nonce, 16);
694
695 return true;
696}
697
699
706bool fido2_storage_get_credential(uint8_t slot, fido2_credential_info_t *info) {
707 if (!slot_logical_valid(slot) || !g_storage.creds[slot].valid || !info) {
708 return false;
709 }
710
711 memset(info, 0, sizeof(*info));
712 info->slot = slot;
713 memcpy(info->rp_id_hash, g_storage.creds[slot].rp_id_hash, 32);
714 strncpy(info->rp_id, g_storage.creds[slot].rp_id, FIDO2_RP_ID_MAX_LEN - 1);
715 strncpy(info->user_name, g_storage.creds[slot].user_name, FIDO2_USER_NAME_MAX_LEN - 1);
716 info->user_id_len = 0;
717 info->sign_count = g_storage.creds[slot].sign_count;
718 info->resident_key = g_storage.creds[slot].resident;
719 info->cred_protect = g_storage.creds[slot].cred_protect;
720 info->curve = g_storage.creds[slot].curve;
721
722 // Load user ID from R-Memory (not cached)
723 uint8_t user_id_len = 0;
724 if (fido2_storage_get_user(slot, info->user_id, &user_id_len,
725 info->user_name, FIDO2_USER_NAME_MAX_LEN)) {
726 info->user_id_len = user_id_len;
727 }
728
729 return true;
730}
731
737uint8_t fido2_storage_get_curve(uint8_t slot) {
738 if (!slot_logical_valid(slot) || !g_storage.creds[slot].valid) {
739 return 0xFF; // Invalid
740 }
741 return g_storage.creds[slot].curve;
742}
743
760 const char *rp_id,
761 const uint8_t *rp_id_hash,
762 const uint8_t *user_id,
763 uint8_t user_id_len,
764 const char *user_name,
765 bool resident_key,
766 uint8_t cred_protect,
767 uint8_t curve,
768 uint8_t *out_slot,
769 uint8_t *out_cred_id,
770 uint8_t *out_pubkey
771) {
773 LOG_E(TAG, "User ID too long: %u", user_id_len);
774 return false;
775 }
776
777 // FIDO2 spec: If credential with same RP ID + User ID exists, replace it
779 int8_t slot;
780
781 if (existing_slot >= 0) {
782 // Replace existing credential
783 LOG_I(TAG, "Replacing existing credential in slot %d", existing_slot);
784 slot = existing_slot;
785
786 // Erase existing key and metadata
787 erase_slot_data(static_cast<uint8_t>(slot));
788
789 // Update cache: mark as invalid temporarily, will be re-validated after creation
790 g_storage.creds[slot].valid = false;
791 g_storage.cred_count--;
792 } else {
793 // Find free slot for new credential
795 if (slot < 0) {
796 LOG_E(TAG, "No free slots");
797 return false;
798 }
799 }
800
801 const char *curve_name = (curve == CDC_CURVE_ED25519) ? "Ed25519" : "P-256";
802 LOG_I(TAG, "Creating %s credential in slot %d for %s", curve_name, slot, rp_id);
803
804 // Explicitly erase ECC slot first to ensure it's empty
805 // (handles cache/chip state mismatch)
806 LOG_D(TAG, "Erasing slot %d before key generation", slot);
807 uint8_t phys_slot = ecc_slot_for_logical(static_cast<uint8_t>(slot));
809 if (!se) return false;
810 se->eccDelete(phys_slot);
811
812 // Generate ECC key with requested curve
813 cdc::hal::EccCurve se_curve =
816 if (se->eccGenerate(phys_slot, se_curve) != cdc::hal::SeResult::OK) {
817 LOG_E(TAG, "Failed to generate %s key in slot %d", curve_name, slot);
818 return false;
819 }
820
821 // Read public key
822 // P-256: 64 bytes (X||Y without 0x04 prefix)
823 // Ed25519: 32 bytes
824 uint8_t pubkey[64];
825 uint8_t pubkey_size = (curve == CDC_CURVE_ED25519) ? 32 : 64;
827 if (se->eccGetPublicKey(phys_slot, pubkey, &se_read_curve) != cdc::hal::SeResult::OK) {
828 LOG_E(TAG, "Failed to read public key from slot %d", slot);
829 se->eccDelete(phys_slot);
830 return false;
831 }
832
833 // Generate random nonce for credential ID
834 uint8_t nonce[16];
835 if (!se->getRandom(nonce, 16)) {
836 LOG_E(TAG, "Failed to generate nonce");
837 se->eccDelete(phys_slot);
838 return false;
839 }
840
841 // Build credential ID (64 bytes)
842 // Format: slot (1) + nonce (16) + padding (47)
843 // In production, use HMAC for binding
844 memset(out_cred_id, 0, FIDO2_CRED_ID_LEN);
845 out_cred_id[0] = slot;
846 memcpy(out_cred_id + 1, nonce, 16);
847
848 // Prepare stored credential
849 fido2_stored_cred_t stored;
850 memset(&stored, 0, sizeof(stored));
851 memcpy(stored.magic, FIDO2_RMEM_MAGIC, FIDO2_RMEM_MAGIC_LEN);
852 memcpy(stored.rp_id_hash, rp_id_hash, 32);
853 if (rp_id) {
854 strncpy(stored.rp_id, rp_id, FIDO2_RP_ID_MAX_LEN - 1);
855 }
856 if (user_id && user_id_len > 0) {
857 memcpy(stored.user_id, user_id, user_id_len);
858 stored.user_id_len = user_id_len;
859 }
860 if (user_name) {
861 strncpy(stored.user_name, user_name, FIDO2_USER_NAME_MAX_LEN - 1);
862 }
863 stored.sign_count = 0;
864 memcpy(stored.cred_id_nonce, nonce, 16);
865 stored.flags = resident_key ? FIDO2_FLAG_RESIDENT : 0;
866 stored.cred_protect = cred_protect;
867 stored.curve = curve;
868
869 // Write to R-Memory
870 if (!write_rmem_credential(static_cast<uint8_t>(slot), &stored)) {
871 se->eccDelete(phys_slot);
872 return false;
873 }
874
875 // Update local cache
876 update_cache_from_stored(static_cast<uint8_t>(slot), &stored, resident_key);
877 g_storage.cred_count++;
878
879 // Copy public key output (32 bytes for Ed25519, 64 for P-256)
880 memcpy(out_pubkey, pubkey, pubkey_size);
881 *out_slot = slot;
882
883 LOG_I(TAG, "Created %s credential in slot %d", curve_name, slot);
884 return true;
885}
886
893 if (!slot_logical_valid(slot) || !g_storage.creds[slot].valid) {
894 return false;
895 }
896
897 LOG_I(TAG, "Deleting credential in slot %d", slot);
898
899 // Erase ECC key and R-Memory
900 erase_slot_data(slot);
901
902 // Update local cache
903 g_storage.creds[slot].valid = false;
904 g_storage.cred_count--;
905
906 LOG_I(TAG, "Deleted credential in slot %d", slot);
907 return true;
908}
909
916 if (!slot_logical_valid(slot) || !g_storage.creds[slot].valid) {
917 return 0;
918 }
919
920 // Increment local cache
921 g_storage.creds[slot].sign_count++;
922 uint32_t new_count = g_storage.creds[slot].sign_count;
923
924 // Read current stored data from TROPIC01
925 fido2_stored_cred_t stored;
926 if (read_rmem_credential(slot, &stored)) {
927 stored.sign_count = new_count;
928 if (!write_rmem_credential(slot, &stored)) {
929 LOG_E(TAG, "CRITICAL: Failed to persist sign count for slot %d!", slot);
930 }
931 }
932
933 return new_count;
934}
935
937
947bool fido2_storage_sign(uint8_t slot, const uint8_t *msg, uint16_t msg_len,
948 uint8_t *signature, uint8_t *sig_len) {
949 if (!slot_logical_valid(slot) || !g_storage.creds[slot].valid) {
950 return false;
951 }
952
954 if (!se) return false;
955
956 uint8_t raw_sig[FIDO2_SIG_SIZE];
957 size_t raw_len = sizeof(raw_sig);
958 uint8_t phys_slot = ecc_slot_for_logical(slot);
959 if (se->ecdsaSign(phys_slot, msg, msg_len, raw_sig, &raw_len) !=
961 raw_len != FIDO2_SIG_SIZE) {
962 LOG_E(TAG, "ECDSA sign failed for slot %d", slot);
963 return false;
964 }
965
966 // Convert to DER format
967 *sig_len = raw_sig_to_der(raw_sig, signature);
968
969 LOG_D(TAG, "Signed with slot %d, sig_len=%d", slot, *sig_len);
970 return true;
971}
972
982bool fido2_storage_sign_raw(uint8_t slot, const uint8_t *msg, uint16_t msg_len,
983 uint8_t *signature, uint8_t *sig_len) {
984 if (!slot_logical_valid(slot) || !g_storage.creds[slot].valid) {
985 return false;
986 }
987
988 uint8_t curve = g_storage.creds[slot].curve;
989
990 if (curve == CDC_CURVE_ED25519) {
991 // EdDSA sign: sign message directly
993 if (!se) return false;
994
995 uint8_t phys_slot = ecc_slot_for_logical(slot);
996 if (se->eddsaSign(phys_slot, msg, msg_len, signature) != cdc::hal::SeResult::OK) {
997 LOG_E(TAG, "EdDSA sign failed for slot %d", slot);
998 return false;
999 }
1000 *sig_len = FIDO2_SIG_SIZE; // Ed25519 signature is always 64 bytes
1001 LOG_D(TAG, "EdDSA signed %d bytes with slot %d", msg_len, slot);
1002 } else {
1004 if (!se) return false;
1005 uint8_t phys_slot = ecc_slot_for_logical(slot);
1006 size_t raw_len = FIDO2_SIG_SIZE;
1007 if (se->ecdsaSign(phys_slot, msg, msg_len, signature, &raw_len) !=
1009 raw_len != FIDO2_SIG_SIZE) {
1010 LOG_E(TAG, "ECDSA sign failed for slot %d", slot);
1011 return false;
1012 }
1013 *sig_len = FIDO2_SIG_SIZE; // Raw P-256 signature (R||S) is always 64 bytes
1014 LOG_D(TAG, "ECDSA signed %d bytes with slot %d", msg_len, slot);
1015 }
1016
1017 return true;
1018}
1019
1030bool fido2_storage_sign_der(uint8_t slot, const uint8_t *msg, uint16_t msg_len,
1031 uint8_t *signature, uint8_t *sig_len) {
1032 if (!slot_logical_valid(slot) || !g_storage.creds[slot].valid) {
1033 return false;
1034 }
1035
1037 if (!se) return false;
1038
1039 uint8_t raw_sig[FIDO2_SIG_SIZE];
1040 size_t raw_len = sizeof(raw_sig);
1041 uint8_t phys_slot = ecc_slot_for_logical(slot);
1042 if (se->ecdsaSign(phys_slot, msg, msg_len, raw_sig, &raw_len) !=
1044 raw_len != FIDO2_SIG_SIZE) {
1045 LOG_E(TAG, "ECDSA sign failed for slot %d", slot);
1046 return false;
1047 }
1048
1049 // Convert to DER format
1050 *sig_len = raw_sig_to_der(raw_sig, signature);
1051
1052 LOG_D(TAG, "Signed DER %d bytes with slot %d, sig_len=%d", msg_len, slot, *sig_len);
1053 return true;
1054}
1055
1062bool fido2_storage_get_pubkey(uint8_t slot, uint8_t *pubkey) {
1063 if (!slot_logical_valid(slot)) {
1064 return false;
1065 }
1066
1068 if (!se) {
1069 return false;
1070 }
1071
1072 uint8_t phys_slot = ecc_slot_for_logical(slot);
1073 if (!se->eccSlotUsed(phys_slot)) {
1074 return false;
1075 }
1076
1078 return se->eccGetPublicKey(phys_slot, pubkey, &curve) == cdc::hal::SeResult::OK;
1079}
static const char * TAG
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:136
bool initialized
Definition ctap2.cpp:61
#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
uint8_t cred_protect
uint8_t fido2_storage_ecc_end(void)
Returns configured ECC end slot.
uint8_t curve
uint8_t user_id_len
static uint8_t s_ecc_start
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.
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.
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.
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.
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
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.
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.
void sha256(const uint8_t *data, size_t len, uint8_t out[32])
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]