Files

270 lines
9.5 KiB
C++

/* role_table.cpp — in-RAM role table implementation for the Teensy 4.1.
*
* Phase 2 of plans/teensy41_role_path_migration.md.
*
* Ports the path-template matching from src/role_table.c, slimmed for the
* Teensy (16 entries, range-only index validation, no allowed-indices set).
* The matching logic (role_path_matches_template, role_path_extract_index,
* role_path_matches_with_range) is a faithful port of the host's functions
* so the Teensy and the host accept the same paths for the same templates.
*/
#include "role_table.h"
#include <string.h>
#include <stdlib.h>
/* ====================================================================
* Table operations
* ==================================================================== */
__attribute__((section(".flashmem")))
void role_table_init(role_table_t *table) {
if (table != NULL) {
memset(table, 0, sizeof(*table));
}
}
__attribute__((section(".flashmem")))
int role_table_add(role_table_t *table, const role_entry_t *entry) {
int i;
if (table == NULL || entry == NULL) {
return -1;
}
if (table->count >= ROLE_TABLE_MAX_ENTRIES) {
return -1; /* full */
}
/* Duplicate name check */
for (i = 0; i < table->count; i++) {
if (strcmp(table->entries[i].name, entry->name) == 0) {
return -2;
}
}
table->entries[table->count] = *entry;
table->count++;
return 0;
}
__attribute__((section(".flashmem")))
role_entry_t *role_table_find_by_name(role_table_t *table, const char *name) {
int i;
if (table == NULL || name == NULL) {
return NULL;
}
for (i = 0; i < table->count; i++) {
if (strcmp(table->entries[i].name, name) == 0) {
return &table->entries[i];
}
}
return NULL;
}
__attribute__((section(".flashmem")))
role_entry_t *role_table_get_default(role_table_t *table) {
return role_table_find_by_name(table, "main");
}
/* ====================================================================
* Path-template matching (ported from src/role_table.c)
* ==================================================================== */
/* Check whether a concrete derivation path matches a role's path template.
* The template may contain a single "%d" placeholder (with an optional
* hardened marker after it, e.g. "m/44'/1237'/%d'/0/0").
* Returns 1 if the path matches the template, 0 if not.
* Ported from src/role_table.c:956. */
__attribute__((section(".flashmem")))
int role_path_matches_template(const char *path, const char *template_str) {
const char *p = path;
const char *t = template_str;
if (path == NULL || template_str == NULL) {
return 0;
}
while (*t != '\0' && *p != '\0') {
if (*t == '%' && *(t + 1) == 'd') {
/* %d placeholder — skip one path segment in the path */
t += 2; /* skip "%d" */
/* Skip optional hardened marker after %d in template */
if (*t == '\'' || *t == 'h' || *t == 'H') {
t++;
}
/* Skip the corresponding segment in the path (digits, possibly with ' or h) */
if (*p == '/') {
/* Path has a slash where we expect a segment — mismatch */
return 0;
}
while (*p != '\0' && *p != '/') {
p++;
}
/* If template has more after %d, it should start with '/' */
if (*t == '/' && *p == '/') {
t++;
p++;
} else if (*t == '\0' && *p == '\0') {
/* Both at end — exact match */
return 1;
} else if (*t == '\0' && *p == '/') {
/* Template ended but path has trailing slash — no match */
return 0;
} else if (*t == '/' && *p == '\0') {
/* Path ended but template has more — no match */
return 0;
}
/* If one has a separator and the other doesn't, let the loop continue */
} else if (*t == *p) {
t++;
p++;
} else {
return 0;
}
}
/* Both should be at the end */
return (*t == '\0' && *p == '\0') ? 1 : 0;
}
/* Extract the numeric index from a concrete derivation path that matches a
* role's path template (containing a single "%d" placeholder).
* Returns the extracted index on success, or -1 if no match / no %d.
* Ported from src/role_table.c:1007. */
__attribute__((section(".flashmem")))
int role_path_extract_index(const char *path, const char *template_str) {
const char *p = path;
const char *t = template_str;
const char *seg_start;
char seg_buf[32];
size_t seg_len;
long val;
char *endp;
if (path == NULL || template_str == NULL) {
return -1;
}
/* If template has no %d, there is no variable index to extract */
if (strstr(template_str, "%d") == NULL) {
return -1;
}
while (*t != '\0' && *p != '\0') {
if (*t == '%' && *(t + 1) == 'd') {
/* %d placeholder — extract the corresponding path segment */
t += 2; /* skip "%d" */
/* Skip optional hardened marker after %d in template */
if (*t == '\'' || *t == 'h' || *t == 'H') {
t++;
}
/* Extract the segment from the path (up to next '/' or end) */
if (*p == '/') {
return -1; /* path has a slash where a segment is expected */
}
seg_start = p;
while (*p != '\0' && *p != '/') {
p++;
}
seg_len = (size_t)(p - seg_start);
if (seg_len == 0 || seg_len >= sizeof(seg_buf)) {
return -1;
}
memcpy(seg_buf, seg_start, seg_len);
seg_buf[seg_len] = '\0';
/* Strip optional trailing hardened marker from the segment */
if (seg_len > 0 &&
(seg_buf[seg_len - 1] == '\'' || seg_buf[seg_len - 1] == 'h' ||
seg_buf[seg_len - 1] == 'H')) {
seg_buf[seg_len - 1] = '\0';
}
endp = NULL;
val = strtol(seg_buf, &endp, 10);
if (*endp != '\0' || val < 0) {
return -1;
}
return (int)val;
} else if (*t == *p) {
t++;
p++;
} else {
return -1;
}
}
return -1;
}
/* Check whether a concrete derivation path matches a role's path template
* AND the extracted index falls within the role's allowed range.
* Returns 1 if the path matches and the index is allowed, 0 otherwise.
* Ported from src/role_table.c:1070 (set-form omitted, range-only). */
__attribute__((section(".flashmem")))
int role_path_matches_with_range(const char *path, const role_entry_t *role) {
int index;
if (path == NULL || role == NULL) {
return 0;
}
/* Fixed path (no %d) — just check structural match */
if (strstr(role->role_path, "%d") == NULL) {
return role_path_matches_template(path, role->role_path);
}
/* Template path — check structural match first */
if (!role_path_matches_template(path, role->role_path)) {
return 0;
}
/* Extract the index and check it against the allowed range */
index = role_path_extract_index(path, role->role_path);
if (index < 0) {
return 0;
}
/* Range form: check lo..hi */
if (role->path_range_lo < 0 || role->path_range_hi < 0) {
/* No range configured — deny (fail-closed) */
return 0;
}
return (index >= role->path_range_lo && index <= role->path_range_hi) ? 1 : 0;
}
/* ====================================================================
* Presets (matching the host wizard, src/main.c:2068)
* ==================================================================== */
const role_preset_t role_presets[] = {
/* 1. Standard Nostr (secp256k1, m/44'/1237'/0'/0/0) */
{ "main", "m/44'/1237'/0'/0/0",
ROLE_PURPOSE_NOSTR, ROLE_CURVE_SECP256K1, -1, -1, -1 },
/* 2. Nostr range (secp256k1, m/44'/1237'/%d'/0/0, 0-100) */
{ "nostr_range", "m/44'/1237'/%d'/0/0",
ROLE_PURPOSE_NOSTR, ROLE_CURVE_SECP256K1, 0, 100, 0 },
/* 3. Nostr agent (secp256k1, m/44'/1237'/%d'/1'/0', 0-100) */
{ "nostr_agent", "m/44'/1237'/%d'/1'/0'",
ROLE_PURPOSE_NOSTR, ROLE_CURVE_SECP256K1, 0, 100, 0 },
/* 4. SSH (ed25519, m/44'/102001'/0'/0'/0') */
{ "ssh", "m/44'/102001'/0'/0'/0'",
ROLE_PURPOSE_SSH, ROLE_CURVE_ED25519, -1, -1, -1 },
/* 5. Age (x25519, m/44'/102002'/0'/0'/0') */
{ "age", "m/44'/102002'/0'/0'/0'",
ROLE_PURPOSE_AGE, ROLE_CURVE_X25519, -1, -1, -1 },
/* 6. ML-DSA-65 (m/44'/102003'/0'/0'/0') */
{ "ml_dsa_65", "m/44'/102003'/0'/0'/0'",
ROLE_PURPOSE_PQ_SIG, ROLE_CURVE_ML_DSA_65, -1, -1, -1 },
/* 7. SLH-DSA-128s (m/44'/102004'/0'/0'/0') */
{ "slh_dsa_128s", "m/44'/102004'/0'/0'/0'",
ROLE_PURPOSE_PQ_SIG, ROLE_CURVE_SLH_DSA_128S, -1, -1, -1 },
/* 8. ML-KEM-768 (m/44'/102005'/0'/0'/0') */
{ "ml_kem_768", "m/44'/102005'/0'/0'/0'",
ROLE_PURPOSE_PQ_KEM, ROLE_CURVE_ML_KEM_768, -1, -1, -1 },
/* 9. OTP (no derivation path — binds the SD pad instead) */
{ "otp", "",
ROLE_PURPOSE_OTP, ROLE_CURVE_OTP, -1, -1, -1 },
/* 10. Custom (user edits name + path) */
{ "custom", "m/44'/1237'/0'/0/0",
ROLE_PURPOSE_NOSTR, ROLE_CURVE_SECP256K1, -1, -1, -1 },
};
const int role_preset_count =
(int)(sizeof(role_presets) / sizeof(role_presets[0]));