270 lines
9.5 KiB
C++
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]));
|