Files
ESP32_Serial_Swiss_Army_Knife/tests/admin_console_boundary/accounts.c
T
Commander1024 ac80863d80 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.
2026-09-08 19:09:26 +02:00

375 lines
21 KiB
C

/* Included by accounts.py after extracted production functions. */
static void reset(void)
{
memset(&s_database, 0, sizeof(s_database));
memset(&candidate_storage, 0, sizeof(candidate_storage));
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;
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);
assert(initialize_user(&s_database.users[1], (const uint8_t *)"other", 5,
USER_ROLE_USER, (const uint8_t *)"test-password", 13)==ESP_OK);
assert(initialize_user(&s_database.users[2], (const uint8_t *)"observer", 8,
USER_ROLE_USER, (const uint8_t *)"test-password", 13)==ESP_OK);
recount(&s_database);
assert(validate_database(&s_database)==ESP_OK);
actor=(user_principal_t){.role=USER_ROLE_ADMIN, .username_length=5,
.username="admin", .user_id=s_database.users[0].user_id,
.auth_generation=s_database.users[0].auth_generation};
writes=commits=prompts=checks=web_revokes=ssh_revokes=0;
revoke_prompt=revoke_check=0; owner_current=true; remote=web=true;
mismatch=cancel_prompt=stale_prompt=false; notify_error=ESP_OK;
memset(revoked_name,0,sizeof(revoked_name));
}
static int run(const char *line)
{
char copy[257]; char *argv[10]={0};
assert(strlen(line)<sizeof(copy)); strcpy(copy,line);
size_t argc=esp_console_split_argv(copy,argv,10);
/* Direct canonical handler, deliberately bypassing dispatcher policy. */
return command_user((int)argc,argv);
}
static void unchanged(const stored_database_t *before)
{
assert(!memcmp(before,&s_database,sizeof(*before)));
assert(!web_revokes && !ssh_revokes);
assert(all_zero(s_candidate,sizeof(*s_candidate)));
assert(!locks);
}
static void typed_account_tests(void)
{
reset(); user_database_accounts_t list;
assert(user_database_get_accounts(&list)==ESP_OK && last_wait==0 && list.count==3);
assert(!strcmp(list.users[1].username,"other"));
user_database_account_t other=list.users[1], admin=list.users[0];
snapshot_busy=true; memset(&list,0xff,sizeof(list));
assert(user_database_get_accounts(&list)==ESP_ERR_TIMEOUT && all_zero(&list,sizeof(list)));
snapshot_busy=false;
assert(user_database_delete_current(&admin)==ESP_ERR_INVALID_STATE);
assert(user_database_set_role_current(&admin,USER_ROLE_USER)==ESP_ERR_INVALID_STATE);
assert(!writes && !commits);
assert(user_database_set_role_current(&other,USER_ROLE_ADMIN)==ESP_OK);
unsigned saved=commits;
assert(user_database_delete_current(&other)==ESP_ERR_NOT_FOUND && commits==saved);
assert(user_database_set_role_current(&other,USER_ROLE_USER)==ESP_ERR_NOT_FOUND);
assert(user_database_get_accounts(&list)==ESP_OK); other=list.users[1];
for (fail_stage=1;fail_stage<=3;++fail_stage) {
stored_database_t before=s_database;
assert(user_database_delete_current(&other)==ESP_FAIL); unchanged(&before);
assert(user_database_set_role_current(&other,USER_ROLE_USER)==ESP_FAIL); unchanged(&before);
}
fail_stage=0; assert(user_database_delete_current(&other)==ESP_OK);
assert(user_database_create((const uint8_t *)"other",5,USER_ROLE_USER,(const uint8_t *)"test-password",13)==ESP_OK);
assert(user_database_delete_current(&other)==ESP_ERR_NOT_FOUND);
assert(user_database_set_role_current(&other,USER_ROLE_ADMIN)==ESP_ERR_NOT_FOUND);
assert(user_database_get_accounts(&list)==ESP_OK); other=list.users[1];
assert(user_database_delete_current(&other)==ESP_OK);
assert(all_zero(s_candidate,sizeof(*s_candidate)) && !locks);
s_initialized=false; memset(&list,0xff,sizeof(list));
assert(user_database_get_accounts(&list)==ESP_ERR_INVALID_STATE && all_zero(&list,sizeof(list)));
assert(user_database_delete_current(NULL)==ESP_ERR_INVALID_ARG);
}
static void typed_password_tests(void)
{
reset(); user_database_accounts_t list;
assert(user_database_get_accounts(&list)==ESP_OK);
user_database_account_t other=list.users[1], admin=list.users[0];
const uint8_t password[]="quote\"slash\\ space";
for (unsigned stage=1;stage<=5;++stage) {
fail_stage=stage; stored_database_t before=s_database;
assert(user_database_set_password_current(&other,password,sizeof(password)-1)==ESP_FAIL);
unchanged(&before);
}
fail_stage=0; writes=commits=0;
assert(user_database_set_password_current(&other,password,sizeof(password)-1)==ESP_OK);
assert(writes==1 && commits==1 && s_database.users[1].auth_generation==other.auth_generation+1);
assert(all_zero(s_candidate,sizeof(*s_candidate)));
stored_database_t before=s_database; unsigned rng=random_calls;
assert(user_database_set_password_current(&other,password,sizeof(password)-1)==ESP_ERR_NOT_FOUND);
unchanged(&before); assert(writes==1 && commits==1 && random_calls==rng);
assert(user_database_get_accounts(&list)==ESP_OK); other=list.users[1];
assert(user_database_delete_current(&other)==ESP_OK);
assert(user_database_create((const uint8_t *)"other",5,USER_ROLE_USER,password,sizeof(password)-1)==ESP_OK);
before=s_database; rng=random_calls;
assert(user_database_set_password_current(&other,password,sizeof(password)-1)==ESP_ERR_NOT_FOUND);
unchanged(&before); assert(random_calls==rng);
assert(user_database_set_password_current(NULL,password,sizeof(password)-1)==ESP_ERR_INVALID_ARG);
other.user_id=0;
assert(user_database_set_password_current(&other,password,sizeof(password)-1)==ESP_ERR_NOT_FOUND);
memset(other.username,'x',sizeof(other.username));
assert(user_database_set_password_current(&other,password,sizeof(password)-1)==ESP_ERR_INVALID_ARG);
assert(user_database_set_password_current(&admin,(const uint8_t *)"short",5)==ESP_ERR_INVALID_ARG);
/* Own password is allowed even for the last administrator; old principal is stale. */
assert(user_database_set_password_current(&admin,password,sizeof(password)-1)==ESP_OK);
bool current=true; assert(user_database_principal_is_current(&actor,&current)==ESP_OK && !current);
assert(s_database.admin_count==1);
/* With a second admin, canonical self role/delete invariants allow both. */
assert(user_database_set_role((const uint8_t *)"other",5,USER_ROLE_ADMIN)==ESP_OK);
assert(user_database_get_accounts(&list)==ESP_OK); admin=list.users[0];
assert(user_database_set_role_current(&admin,USER_ROLE_USER)==ESP_OK);
assert(user_database_get_accounts(&list)==ESP_OK); admin=list.users[0];
assert(user_database_delete_current(&admin)==ESP_OK);
reset(); before=s_database; rng=random_calls;
assert(user_database_create((const uint8_t *)"other",5,USER_ROLE_ADMIN,password,sizeof(password)-1)==ESP_ERR_INVALID_STATE);
unchanged(&before); assert(!writes && !commits && rng==random_calls);
for (unsigned i=3;i<USER_DATABASE_MAX_USERS;++i) {
char name[17]; snprintf(name,sizeof(name),"account%u",i);
assert(user_database_create((const uint8_t *)name,strlen(name),USER_ROLE_USER,password,sizeof(password)-1)==ESP_OK);
}
before=s_database; rng=random_calls; unsigned saved=commits;
assert(user_database_create((const uint8_t *)"extra",5,USER_ROLE_USER,password,sizeof(password)-1)==ESP_ERR_NO_MEM);
unchanged(&before); assert(commits==saved && rng==random_calls);
/* RNG-only helper is independent of initialized storage and leaves all DB state alone. */
s_initialized=false; s_mutex=NULL;
for (unsigned mode=0;mode<2;++mode) {
user_database_generated_password_t generated; memset(&generated,0xa5,sizeof(generated));
fail_stage=mode ? 4 : 0;
assert(user_database_generate_password_value(&generated)==(mode ? ESP_FAIL : ESP_OK));
if (mode) assert(all_zero(&generated,sizeof(generated)));
else {
assert(generated.password_length==24 && strlen((const char *)generated.password)==24);
for (size_t i=0;i<24;++i) assert(strchr((const char *)s_generated_alphabet,generated.password[i]));
}
assert(!memcmp(&before,&s_database,sizeof(before)) && commits==saved && !locks);
secure_wipe(&generated,sizeof(generated));
}
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[]={
"user add fresh user", "user add fresh admin", "user password other",
"user delete other --force", "user role other admin --force",
"\"user\" \"password\" \"other\"", "user role other user --force",
};
for (size_t i=0;i<sizeof(supported)/sizeof(*supported);++i) {
reset(); stored_database_t before=s_database;
assert(run(supported[i])==0);
assert(web_revokes==1 && ssh_revokes==1);
assert(!strcmp(revoked_name,i<2 ? "fresh" : "other"));
assert(!memcmp(&before.users[0],&s_database.users[0],sizeof(stored_user_t)));
assert(!memcmp(&before.users[2],&s_database.users[2],sizeof(stored_user_t)));
assert(admin_ssh_console_dispatch_is_current());
assert(validate_database(&s_database)==ESP_OK);
if (i<2) {
assert(s_database.user_count==before.user_count+1);
int fresh=find_user(&s_database,(const uint8_t *)"fresh",5);
assert(fresh>=0 && s_database.users[fresh].role==(i==0 ? USER_ROLE_USER : USER_ROLE_ADMIN));
} else if (i==3) {
assert(find_user(&s_database,(const uint8_t *)"other",5)<0);
} else if (i!=6) {
assert(s_database.users[1].auth_generation==before.users[1].auth_generation+1);
} else {
assert(!writes && !commits); /* Existing no-op role still revokes target. */
}
assert(all_zero(s_candidate,sizeof(*s_candidate)));
assert(!locks);
}
const char *denied[]={
"user password admin", "\"user\" \"password\" \"admin\"",
"user delete admin --force", "user role admin user --force",
"user role admin admin --force", "user add admin admin",
"user password admin --generate", "user password other --generate",
"user add fresh user --generate", "user key add other",
"user key add other ssh-ed25519 AAAA", "user key delete other 0 --force",
"user key clear other --force", "user bootstrap", "user bootstrap --generate",
"user recover --force", "user password other extra",
"user role other admin --force extra", "user delete other --force extra",
"user add fresh invalid", "user add fresh user extra",
"user delete other", "user role other user",
"user role \"admin\" user --force", "user add \"admin\" user",
};
for (size_t i=0;i<sizeof(denied)/sizeof(*denied);++i) {
reset(); stored_database_t before=s_database; unsigned rng=random_calls;
assert(run(denied[i])!=0); unchanged(&before);
assert(!prompts && !writes && !commits && random_calls==rng);
}
reset();
actor.username_length=0;
assert(run("user password other")!=0 && !prompts && !writes);
reset();
actor.username_length=USER_DATABASE_USERNAME_CAPACITY+1;
assert(run("user password other")!=0 && !prompts && !writes);
reset();
/* Self defense is identity-based, not a hard-coded 'admin' name. */
memcpy(s_database.users[0].username,"chief",5);
memcpy(actor.username,"chief",5);
assert(run("user password \"chief\"")!=0 && !prompts && !writes);
assert(run("user role \"chief\" admin --force")!=0 && !writes);
const char *prompted[]={"user add fresh admin", "user password other"};
for (size_t i=0;i<2;++i) {
for (unsigned mode=0;mode<7;++mode) {
reset(); stored_database_t before=s_database; unsigned rng=random_calls;
if (mode<2) revoke_prompt=mode+1;
if (mode==2) revoke_check=2; /* After both successful prompts. */
if (mode==3) mismatch=true;
if (mode==4) cancel_prompt=true;
if (mode==5) { ++actor.auth_generation; }
if (mode==6) stale_prompt=true;
assert(run(prompted[i])!=0); unchanged(&before);
assert(!writes && !commits && random_calls==rng);
}
}
/* Operation admission is the final post-prompt check before the database
* API, not the later NVS commit. Once admitted, expiry/closure during
* derivation does not cancel the transaction. No browser receipt is proved. */
for (size_t i=0;i<2;++i) {
for (unsigned stage=0;stage<=3;++stage) {
reset(); stored_database_t before=s_database;
invalidate_during_derivation=true; fail_stage=stage;
int result=run(prompted[i]);
assert(derivation_invalidations==1 && !owner_current && checks==2);
assert(prompts==2 && !locks);
if (stage==0) {
assert(result==0 && writes==1 && commits==1);
assert(s_database.generation==before.generation+1);
if (i==0) {
int fresh=find_user(&s_database,(const uint8_t *)"fresh",5);
assert(fresh>=0 && s_database.users[fresh].role==USER_ROLE_ADMIN);
assert(s_database.user_count==before.user_count+1);
assert(!memcmp(&before.users[1],&s_database.users[1],sizeof(stored_user_t)));
} else {
assert(s_database.users[1].auth_generation==before.users[1].auth_generation+1);
assert(memcmp(before.users[1].password_salt,s_database.users[1].password_salt,
sizeof(before.users[1].password_salt))!=0);
}
assert(web_revokes==1 && ssh_revokes==1);
assert(!strcmp(revoked_name,i==0 ? "fresh" : "other"));
assert(!memcmp(&before.users[0],&s_database.users[0],sizeof(stored_user_t)));
assert(!memcmp(&before.users[2],&s_database.users[2],sizeof(stored_user_t)));
assert(validate_database(&s_database)==ESP_OK);
assert(all_zero(s_candidate,sizeof(*s_candidate)));
} else {
assert(result!=0); unchanged(&before);
assert(commits==(stage==3 ? 1U : 0U));
}
/* Loss of liveness cannot authorize a subsequent operation. */
stored_database_t after=s_database;
unsigned prior_writes=writes, prior_commits=commits;
unsigned prior_web=web_revokes, prior_ssh=ssh_revokes;
assert(run("user password observer")!=0);
assert(!memcmp(&after,&s_database,sizeof(after)));
assert(prompts==2 && writes==prior_writes && commits==prior_commits);
assert(web_revokes==prior_web && ssh_revokes==prior_ssh);
}
}
/* Every enabled mutation fails closed when the originating session or copied
* account is stale BEFORE operation admission, including forced mutations. */
for (size_t i=0;i<sizeof(supported)/sizeof(*supported);++i) {
reset(); stored_database_t before=s_database; owner_current=false;
assert(run(supported[i])!=0); unchanged(&before); assert(!prompts && !writes);
reset(); before=s_database; revoke_check=2;
assert(run(supported[i])!=0); unchanged(&before); assert(!writes && !commits);
reset(); before=s_database; actor.role=USER_ROLE_USER;
assert(run(supported[i])!=0); unchanged(&before); assert(!prompts && !writes);
}
for (size_t i=0;i<5;++i) {
for (unsigned stage=1;stage<=3;++stage) {
reset(); stored_database_t before=s_database; fail_stage=stage;
assert(run(supported[i])!=0); unchanged(&before);
assert(commits==(stage==3 ? 1U : 0U));
}
}
for (size_t i=0;i<3;++i) {
for (unsigned stage=4;stage<=5;++stage) {
reset(); stored_database_t before=s_database; fail_stage=stage;
assert(run(supported[i])!=0); unchanged(&before); assert(!writes && !commits);
}
}
reset(); notify_error=ESP_FAIL;
assert(run("user password other")==0);
assert(commits==1 && web_revokes==1 && ssh_revokes==1);
assert(s_database.users[1].auth_generation==2);
/* Real database invariants, independent of browser self-target policy. */
reset(); stored_database_t before=s_database;
assert(user_database_delete((const uint8_t *)"admin",5)!=ESP_OK); unchanged(&before);
assert(user_database_set_role((const uint8_t *)"admin",5,USER_ROLE_USER)!=ESP_OK);
unchanged(&before); assert(!writes && !commits);
/* Trusted UART0 bypasses browser admission, not database invariants. */
reset(); web=false; remote=false;
assert(run("user delete admin --force")!=0); assert(!writes && !web_revokes);
reset(); web=false; remote=false;
assert(run("user role admin user --force")!=0); assert(!writes && !web_revokes);
/* Normal UART0 and SSH prompted nonself commands still use the same handler. */
reset(); web=false; assert(run("user password other")==0);
reset(); web=false; remote=false; owner_current=false;
assert(run("user password other")==0);
return 0;
}