Remove Legacy Credential Bootstrap Paths

Decouple user provisioning from HTTPS identity storage while retaining
compatible v1 user records and migrating TLS material to the
credential-free
v2 format. Add focused security regression coverage and update operator
documentation.
This commit is contained in:
2026-09-08 19:09:26 +02:00
parent 82f21d6116
commit ac80863d80
26 changed files with 1013 additions and 583 deletions
+30 -7
View File
@@ -41,6 +41,20 @@ enum { ESP_OK, ESP_FAIL, ESP_ERR_INVALID_ARG, ESP_ERR_INVALID_STATE,
typedef void *SemaphoreHandle_t;
#define portMAX_DELAY 0
#define NVS_READWRITE 1
#define NVS_READONLY 0
#define ESP_ERR_NVS_NOT_FOUND 100
static uint8_t persisted[65536], staged[65536];
static size_t persisted_size, staged_size;
static bool storage_test;
static void *xSemaphoreCreateMutex(void) { return (void *)1; }
static void vSemaphoreDelete(void *m) { (void)m; }
static int nvs_flash_init(void) { return ESP_OK; }
static int nvs_get_blob(int h, const char *key, void *out, size_t *n) {
(void)h; (void)key;
if (!persisted_size) return ESP_ERR_NVS_NOT_FOUND;
if (out) { assert(*n>=persisted_size); memcpy(out,persisted,persisted_size); }
*n=persisted_size; return ESP_OK;
}
typedef int nvs_handle_t;
static unsigned locks, writes, commits, random_calls, prompts, checks, web_revokes, ssh_revokes;
static unsigned fail_stage, revoke_prompt, revoke_check, derivation_invalidations;
@@ -53,16 +67,21 @@ static int xSemaphoreTake(void *m, int t) { (void)m; last_wait=t; if (snapshot_b
static void xSemaphoreGive(void *m) { (void)m; assert(locks-- == 1); }
static const char *esp_err_to_name(int e) { (void)e; return "injected error"; }
static int nvs_open(const char *ns, int mode, int *h) {
(void)ns; (void)mode; assert(locks);
(void)ns; (void)mode; assert(locks || storage_test);
if (invalidate_during_derivation) {
assert(derivation_invalidations==1 && !owner_current);
}
*h=1; return fail_stage==1 ? ESP_FAIL : ESP_OK;
}
static int nvs_set_blob(int h, const char *key, const void *data, size_t n) {
(void)h; (void)key; (void)data; (void)n; ++writes; return fail_stage==2 ? ESP_FAIL : ESP_OK;
(void)h; (void)key; ++writes;
if (fail_stage==2) return ESP_FAIL;
assert(n<=sizeof(staged)); memcpy(staged,data,n); staged_size=n; return ESP_OK;
}
static int nvs_commit(int h) {
(void)h; ++commits; if (fail_stage==3) return ESP_FAIL;
memcpy(persisted,staged,staged_size); persisted_size=staged_size; return ESP_OK;
}
static int nvs_commit(int h) { (void)h; ++commits; return fail_stage==3 ? ESP_FAIL : ESP_OK; }
static void nvs_close(int h) { (void)h; }
static int secure_random_fill(void *p, size_t n) {
memset(p, ++random_calls, n); return fail_stage==4 ? ESP_FAIL : ESP_OK;
@@ -108,6 +127,8 @@ state = db[db.index("#define USER_DATABASE_SCHEMA_VERSION"):db.index("static esp
fakes = r'''
static stored_database_t candidate_storage;
static int allocate_candidate(void) { s_candidate=&candidate_storage; return ESP_OK; }
static void release_candidate(void) { secure_wipe(&candidate_storage,sizeof(candidate_storage)); s_candidate=NULL; }
static user_principal_t actor;
static bool admin_ssh_console_dispatch_is_remote(void) { return remote; }
static bool admin_ssh_console_dispatch_is_web(void) { return web; }
@@ -138,8 +159,7 @@ static int ssh_transport_revoke_user(const uint8_t *u, size_t n) {
}
/* Forbidden paths are traps rather than alternative implementations. */
static int show_users(const char *n) { (void)n; return 0; }
static int recover_database(void) { assert(!"recovery"); return 1; }
static int bootstrap(bool g) { (void)g; assert(!"bootstrap"); return 1; }
static int add_key(const char *n) { (void)n; assert(!"key mutation"); return 1; }
static int add_key_parts(const char *n,const uint8_t *t,size_t tl,const uint8_t *b,size_t bl) {
(void)n; (void)t; (void)tl; (void)b; (void)bl; assert(!"key mutation"); return 1;
@@ -167,10 +187,12 @@ db_names = ["constant_time_equal", "all_zero", "user_database_username_valid",
"user_database_get_account_keys", "add_ssh_key", "remove_ssh_key", "clear_ssh_keys",
"user_database_add_ssh_key", "user_database_remove_ssh_key", "user_database_clear_ssh_keys",
"key_target_valid", "user_database_add_ssh_key_current", "user_database_remove_ssh_key_current",
"user_database_clear_ssh_keys_current", "fill_principal", "user_database_authorize_ssh_public_key"]
"user_database_clear_ssh_keys_current", "fill_principal", "user_database_authorize_ssh_public_key",
"initialize_dummy_verifier", "user_database_init", "user_database_recover_empty",
"user_database_get_snapshot"]
console_names = ["print_usage", "revoke_user_network_sessions", "read_password",
"show_generated_password", "mutation_currentness", "add_user", "change_password",
"parse_key_index", "command_user_inner", "command_user"]
"parse_key_index", "recover_database", "command_user_inner", "command_user"]
unit = prelude + header + "\n" + state + fakes
unit += "\n".join(function(db, n) for n in db_names)
unit += function(admin, "admin_ssh_console_web_user_command_allowed")
@@ -190,6 +212,7 @@ with tempfile.TemporaryDirectory(prefix="admin-accounts-") as directory:
result = subprocess.run([str(path / "test")], check=True, timeout=10, capture_output=True, text=True)
assert "test-password" not in result.stdout
assert "Generated password for" not in result.stdout
print("PASS: empty initialization/recovery, unchanged v1 records, corrupt/unsupported fail-closed loads, first UART0 administrator and removed bootstrap commands")
print("PASS: canonical SSH keys: Ed25519/P256 parser and authorization, malformed/off-curve/truncated inputs, zero-wait fingerprints, stale ID/generation/recreation, duplicates/capacity, sparse indices, failed persistence and CLI parity (OpenSSL-backed curve/SHA adapters)")
print("PASS: operation-admission semantics: browser invalidated in derivation double before NVS; admitted add/password transactions still commit, only target is revoked, next command rejects; persistence failure still preserves live state (not precommit cancellation or real concurrency)")
print("PASS: canonical parsed accounts + production DB transactions: nonself isolation, prompt revocation/cancel/mismatch, currentness, persistence/RNG/derive failures, final-admin invariants, self/generated/key/recovery traps; no password output")