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:
@@ -84,6 +84,13 @@ static void typed_key_tests(void)
|
||||
ed[en-1]=2;
|
||||
assert(user_database_add_ssh_key_current(&target,s_ed25519_type,11,ed,en,&index)==ESP_OK && index==2);
|
||||
target=key_target(); saved=commits;
|
||||
/* Reload the persisted v1 record with all three keys and verifiers intact. */
|
||||
stored_database_t stored=s_database;
|
||||
storage_test=true; unload_database();
|
||||
user_database_load_result_t loaded;
|
||||
assert(user_database_init(&loaded)==ESP_OK && loaded==USER_DATABASE_LOAD_STORED);
|
||||
assert(!memcmp(&stored,&s_database,sizeof(stored)) && commits==saved);
|
||||
storage_test=false;
|
||||
assert(user_database_add_ssh_key_current(&target,s_ed25519_type,11,ed,en,&index)==USER_DATABASE_ERR_DUPLICATE_SSH_KEY);
|
||||
ed[en-1]=3;
|
||||
assert(user_database_add_ssh_key_current(&target,s_ed25519_type,11,ed,en,&index)==ESP_ERR_NO_MEM && commits==saved);
|
||||
@@ -98,6 +105,37 @@ static void typed_key_tests(void)
|
||||
stale_keys(&target,ed,en); target=key_target();
|
||||
assert(user_database_get_account_keys(&target,&snapshot)==ESP_OK && snapshot.public_key_count==2);
|
||||
assert(!snapshot.public_keys[1].active && snapshot.public_keys[2].index==2);
|
||||
/* Sparse v1 reload must preserve the entire record, including IDs,
|
||||
* generations, password verifiers, key blobs/types and fingerprints. */
|
||||
stored=s_database;
|
||||
user_database_user_snapshot_t sparse_snapshot=snapshot;
|
||||
unsigned sparse_writes=writes, sparse_commits=commits;
|
||||
assert(persisted_size==sizeof(stored) && !memcmp(persisted,&stored,sizeof(stored)));
|
||||
storage_test=true; unload_database();
|
||||
assert(user_database_init(&loaded)==ESP_OK && loaded==USER_DATABASE_LOAD_STORED);
|
||||
storage_test=false;
|
||||
assert(!memcmp(&stored,&s_database,sizeof(stored)));
|
||||
assert(persisted_size==sizeof(stored) && !memcmp(persisted,&stored,sizeof(stored)));
|
||||
assert(writes==sparse_writes && commits==sparse_commits);
|
||||
assert(user_database_get_account_keys(&target,&snapshot)==ESP_OK);
|
||||
assert(!memcmp(&sparse_snapshot,&snapshot,sizeof(snapshot)));
|
||||
for (size_t slot=0;slot<3;slot+=2) {
|
||||
const stored_key_t *key=&stored.users[1].keys[slot];
|
||||
assert(key->active && snapshot.public_keys[slot].active);
|
||||
assert(snapshot.public_keys[slot].index==slot);
|
||||
assert(user_database_key_valid(key->type,key->type_length,key->blob,key->blob_length));
|
||||
assert(user_database_authorize_ssh_public_key((const uint8_t *)"other",5,
|
||||
key->type,key->type_length,key->blob,key->blob_length,
|
||||
&authenticated,&authorized)==ESP_OK && authorized);
|
||||
assert(authenticated.user_id==target.user_id && authenticated.auth_generation==target.auth_generation);
|
||||
assert(authenticated.role==target.role && authenticated.method==USER_AUTH_METHOD_SSH_PUBLIC_KEY);
|
||||
bool current=false;
|
||||
assert(user_database_principal_is_current(&authenticated,¤t)==ESP_OK && current);
|
||||
}
|
||||
assert(!snapshot.public_keys[1].active);
|
||||
assert(user_database_authorize_ssh_public_key((const uint8_t *)"other",5,
|
||||
s_ecdsa_type,19,p256,pn,&authenticated,&authorized)==ESP_OK && !authorized);
|
||||
assert(writes==sparse_writes && commits==sparse_commits);
|
||||
assert(user_database_remove_ssh_key_current(&target,1)==ESP_ERR_NOT_FOUND);
|
||||
assert(user_database_remove_ssh_key_current(&target,3)==ESP_ERR_INVALID_ARG);
|
||||
assert(user_database_add_ssh_key_current(&target,s_ecdsa_type,19,p256,pn,&index)==ESP_OK && index==1);
|
||||
@@ -116,7 +154,7 @@ static void typed_key_tests(void)
|
||||
assert(user_database_remove_ssh_key_current(&target,0)==ESP_ERR_INVALID_ARG);
|
||||
assert(user_database_clear_ssh_keys_current(NULL)==ESP_ERR_INVALID_ARG);
|
||||
assert(user_database_get_account_keys(NULL,&snapshot)==ESP_ERR_INVALID_ARG && all_zero(&snapshot,sizeof(snapshot)));
|
||||
/* Legacy CLI APIs retain the exact transaction path and generation changes. */
|
||||
/* Ordinary CLI APIs retain the exact transaction path and generation changes. */
|
||||
target=key_target(); assert(user_database_add_ssh_key((const uint8_t *)"other",5,s_ed25519_type,11,ed,en,&index)==ESP_OK);
|
||||
stale_keys(&target,ed,en);
|
||||
assert(user_database_remove_ssh_key((const uint8_t *)"other",5,index)==ESP_OK);
|
||||
|
||||
@@ -6,7 +6,6 @@ static void reset(void)
|
||||
s_candidate=&candidate_storage; s_mutex=(void *)1; s_initialized=true;
|
||||
s_database.version=USER_DATABASE_SCHEMA_VERSION;
|
||||
s_database.size=sizeof(s_database); s_database.generation=1;
|
||||
s_database.admin_bootstrapped=1;
|
||||
fail_stage=0; invalidate_during_derivation=false; derivation_invalidations=0;
|
||||
assert(initialize_user(&s_database.users[0], (const uint8_t *)"admin", 5,
|
||||
USER_ROLE_ADMIN, (const uint8_t *)"test-password", 13)==ESP_OK);
|
||||
@@ -138,8 +137,80 @@ static void typed_password_tests(void)
|
||||
}
|
||||
assert(user_database_generate_password_value(NULL)==ESP_ERR_INVALID_ARG);
|
||||
}
|
||||
static void unload_database(void)
|
||||
{
|
||||
s_initialized=false; s_mutex=NULL; release_candidate();
|
||||
secure_wipe(&s_database,sizeof(s_database));
|
||||
}
|
||||
|
||||
static void storage_tests(void)
|
||||
{
|
||||
reset(); storage_test=true;
|
||||
/* Both historical v1 states load without any account/verifier/ID changes. */
|
||||
for (unsigned admins=0;admins<2;++admins) {
|
||||
reset();
|
||||
s_database.users[0].role=admins ? USER_ROLE_ADMIN : USER_ROLE_USER;
|
||||
recount(&s_database);
|
||||
stored_database_t before=s_database;
|
||||
memcpy(persisted,&before,sizeof(before)); persisted_size=sizeof(before);
|
||||
unload_database();
|
||||
user_database_load_result_t result;
|
||||
assert(user_database_init(&result)==ESP_OK && result==USER_DATABASE_LOAD_STORED);
|
||||
assert(!memcmp(&before,&s_database,sizeof(before)) && !writes && !commits);
|
||||
assert(user_database_recover_empty()==ESP_ERR_INVALID_STATE);
|
||||
if (!admins) {
|
||||
assert(user_database_delete((const uint8_t *)"admin",5)==ESP_OK);
|
||||
assert(s_database.admin_count==0 && s_database.user_count==2);
|
||||
}
|
||||
}
|
||||
for (unsigned kind=0;kind<4;++kind) {
|
||||
reset(); stored_database_t bad=s_database;
|
||||
if (kind==0) ++bad.version;
|
||||
if (kind==1) bad.v1_admin_marker=0;
|
||||
if (kind==2) bad.users[0].user_id=0;
|
||||
memcpy(persisted,&bad,sizeof(bad)); persisted_size=sizeof(bad)-(kind==3);
|
||||
size_t size=persisted_size;
|
||||
unload_database(); user_database_load_result_t result;
|
||||
assert(user_database_init(&result)!=ESP_OK && !s_initialized && !s_mutex);
|
||||
assert(!writes && !commits && persisted_size==size && !memcmp(persisted,&bad,size));
|
||||
web=false; remote=true;
|
||||
assert(run("user recover --force")!=0 && !writes);
|
||||
remote=false;
|
||||
assert(run("user recover")!=0 && !writes);
|
||||
assert(run("user recover --force")==0 && s_initialized);
|
||||
assert(!s_database.user_count && !s_database.admin_count);
|
||||
assert(validate_database(&s_database)==ESP_OK);
|
||||
}
|
||||
reset(); unload_database(); persisted_size=0;
|
||||
user_database_load_result_t result;
|
||||
assert(user_database_init(&result)==ESP_OK && result==USER_DATABASE_LOAD_EMPTY);
|
||||
assert(s_initialized && !s_database.user_count && writes==1 && commits==1);
|
||||
stored_database_t empty=s_database;
|
||||
assert(persisted_size==sizeof(empty) && !memcmp(persisted,&empty,sizeof(empty)));
|
||||
unload_database();
|
||||
assert(user_database_init(&result)==ESP_OK && result==USER_DATABASE_LOAD_STORED);
|
||||
assert(!memcmp(&empty,&s_database,sizeof(empty)) && writes==1 && commits==1);
|
||||
user_database_snapshot_t snapshot;
|
||||
assert(user_database_get_snapshot(&snapshot)==ESP_OK && snapshot.initialized);
|
||||
assert(!snapshot.user_count && !snapshot.admin_count);
|
||||
web=remote=false;
|
||||
assert(run("user bootstrap")!=0 && run("user bootstrap --generate")!=0);
|
||||
assert(run("user recover --force")!=0 && !memcmp(&empty,&s_database,sizeof(empty)));
|
||||
assert(run("user add chief admin")==0 && s_database.admin_count==1);
|
||||
assert(user_database_delete((const uint8_t *)"chief",5)==ESP_ERR_INVALID_STATE);
|
||||
assert(user_database_set_role((const uint8_t *)"chief",5,USER_ROLE_USER)==ESP_ERR_INVALID_STATE);
|
||||
for (unsigned stage=1;stage<=3;++stage) {
|
||||
reset(); unload_database(); persisted_size=0; fail_stage=stage;
|
||||
assert(user_database_init(&result)==ESP_FAIL && !s_initialized && !s_mutex);
|
||||
assert(!persisted_size);
|
||||
assert(user_database_recover_empty()==ESP_FAIL && !s_initialized && !s_mutex);
|
||||
}
|
||||
storage_test=false;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
storage_tests();
|
||||
typed_account_tests();
|
||||
typed_password_tests();
|
||||
const char *supported[]={
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -26,7 +26,7 @@ typedef struct { int unused; } user_principal_t;
|
||||
'''
|
||||
fakes = r'''
|
||||
static bool remote, web;
|
||||
static unsigned stops, reboots, scheduled, waits, rotations;
|
||||
static unsigned stops, reboots, scheduled, waits, rotations, usages;
|
||||
static esp_err_t schedule_result, stop_result;
|
||||
static admin_ssh_deferred_action_type_t last_action;
|
||||
bool admin_ssh_console_dispatch_is_remote(void) { return remote; }
|
||||
@@ -42,13 +42,11 @@ static esp_err_t web_serial_transport_clear_counters(void) { assert(false); retu
|
||||
static void esp_restart(void) { ++reboots; }
|
||||
static void vTaskDelay(unsigned delay) { assert(delay==100); ++waits; }
|
||||
#define pdMS_TO_TICKS(ms) (ms)
|
||||
static void print_usage(void) { assert(false); }
|
||||
static void print_usage(void) { ++usages; }
|
||||
static int web_diagnostics_command(const char *action) { assert(!strcmp(action, "show")); return 0; }
|
||||
static int show_status(void) { assert(false); return 1; }
|
||||
static int show_counters(void) { assert(false); return 1; }
|
||||
static int show_credentials(void) { assert(false); return 1; }
|
||||
static int show_certificate(void) { assert(false); return 1; }
|
||||
static int rotate_credentials(void) { assert(false); return 1; }
|
||||
static int rotate_certificate(void) { ++rotations; return 0; }
|
||||
static int reset_material(void) { assert(false); return 1; }
|
||||
static bool force_is_present(int argc, char **argv, int expected) {
|
||||
@@ -57,6 +55,18 @@ static bool force_is_present(int argc, char **argv, int expected) {
|
||||
'''
|
||||
tests = r'''
|
||||
int main(void) {
|
||||
char *removed[]={"web", "credentials", "show", "--force"};
|
||||
for (unsigned origin=0; origin<3; ++origin) {
|
||||
remote=origin!=0; web=origin==2;
|
||||
removed[2]="show";
|
||||
assert(command_web(2,removed)==1);
|
||||
assert(command_web(3,removed)==1);
|
||||
removed[2]="rotate";
|
||||
assert(command_web(3,removed)==1);
|
||||
assert(command_web(4,removed)==1);
|
||||
}
|
||||
assert(usages==12 && !stops && !scheduled && !rotations);
|
||||
remote=web=false;
|
||||
char *diagnostics[]={"web", "diagnostics", "show"};
|
||||
assert(command_web(3, diagnostics)==0 && !stops && !scheduled);
|
||||
char *stop[]={"web", "stop"};
|
||||
|
||||
Reference in New Issue
Block a user