|
|
|
@@ -0,0 +1,377 @@
|
|
|
|
|
/* Production auth/store/handler, deterministic dispatcher and DB/transport doubles.
|
|
|
|
|
* Actual conditional database transactions are tested by accounts.py. */
|
|
|
|
|
#define ESP_ERR_TIMEOUT 0x107
|
|
|
|
|
#define ESP_ERR_NOT_FOUND 0x105
|
|
|
|
|
static unsigned wiped_passwords, wiped_responses, wiped_generated, wiped_bodies;
|
|
|
|
|
static const uint8_t *executing_password;
|
|
|
|
|
static void account_wipe(void *p, size_t n) {
|
|
|
|
|
if (n==65) ++wiped_passwords;
|
|
|
|
|
if (n==96) ++wiped_responses;
|
|
|
|
|
if (n==sizeof(user_database_generated_password_t)) ++wiped_generated;
|
|
|
|
|
if (n==768) ++wiped_bodies;
|
|
|
|
|
secure_wipe(p,n); zero(p,n);
|
|
|
|
|
if (p==executing_password) executing_password=NULL;
|
|
|
|
|
}
|
|
|
|
|
#define secure_wipe account_wipe
|
|
|
|
|
#include "account_parse_production.h"
|
|
|
|
|
#include "../../src/web_account_settings.c"
|
|
|
|
|
#undef secure_wipe
|
|
|
|
|
|
|
|
|
|
static unsigned timer_creates, timer_starts;
|
|
|
|
|
static bool timer_create_fail, timer_start_fail;
|
|
|
|
|
static void (*timer_callback)(void *);
|
|
|
|
|
int esp_timer_create(const esp_timer_create_args_t *args, esp_timer_handle_t *out) {
|
|
|
|
|
assert(!host_lock_depth && !s_secret_timer); ++timer_creates;
|
|
|
|
|
if (timer_create_fail) return ESP_FAIL;
|
|
|
|
|
timer_callback=args->callback; *out=(void *)1; return ESP_OK;
|
|
|
|
|
}
|
|
|
|
|
int esp_timer_start_periodic(esp_timer_handle_t timer, uint64_t period) {
|
|
|
|
|
assert(!host_lock_depth && timer==s_secret_timer && period==1000000); ++timer_starts;
|
|
|
|
|
return timer_start_fail ? ESP_FAIL : ESP_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool dispatcher, queue_fail, list_fail;
|
|
|
|
|
static uint32_t queued;
|
|
|
|
|
static unsigned mutations, web_revokes, ssh_revokes, lists;
|
|
|
|
|
static esp_err_t mutation_error;
|
|
|
|
|
static void (*mutation_hook)(void);
|
|
|
|
|
static void (*queue_hook)(void);
|
|
|
|
|
static bool self_target;
|
|
|
|
|
static void check_slot_wiped(void) {
|
|
|
|
|
zero(s_operation.password,sizeof(s_operation.password)); assert(!s_operation.password_length);
|
|
|
|
|
zero(&s_operation.principal,sizeof(s_operation.principal));
|
|
|
|
|
zero(&s_operation.target,sizeof(s_operation.target));
|
|
|
|
|
}
|
|
|
|
|
esp_err_t admin_ssh_console_submit_account_settings(uint32_t id) {
|
|
|
|
|
assert(!host_lock_depth && !dispatcher && id);
|
|
|
|
|
if (queue_hook) { void (*hook)(void)=queue_hook; queue_hook=NULL; hook(); }
|
|
|
|
|
if (queue_fail) return ESP_ERR_TIMEOUT;
|
|
|
|
|
queued=id; return ESP_OK;
|
|
|
|
|
}
|
|
|
|
|
esp_err_t user_database_get_accounts(user_database_accounts_t *out) {
|
|
|
|
|
assert(!host_lock_depth && !dispatcher); ++lists; memset(out,0,sizeof(*out));
|
|
|
|
|
if (list_fail) return ESP_ERR_TIMEOUT;
|
|
|
|
|
out->count=8;
|
|
|
|
|
for (unsigned i=0;i<8;++i) {
|
|
|
|
|
snprintf(out->users[i].username,sizeof(out->users[i].username),"account%09u",i);
|
|
|
|
|
out->users[i].role=USER_ROLE_ADMIN; out->users[i].user_id=UINT32_MAX-i;
|
|
|
|
|
out->users[i].auth_generation=UINT32_MAX;
|
|
|
|
|
}
|
|
|
|
|
return ESP_OK;
|
|
|
|
|
}
|
|
|
|
|
esp_err_t user_database_delete_current(const user_database_account_t *target) {
|
|
|
|
|
assert(dispatcher && !host_lock_depth && !strcmp(target->username,self_target ? "alice" : "carol"));
|
|
|
|
|
assert(s_operation.executing); check_slot_wiped();
|
|
|
|
|
assert(target->user_id==7 && target->auth_generation==2); ++mutations;
|
|
|
|
|
if (mutation_hook) { void (*hook)(void)=mutation_hook; mutation_hook=NULL; hook(); }
|
|
|
|
|
return mutation_error;
|
|
|
|
|
}
|
|
|
|
|
esp_err_t user_database_set_role_current(const user_database_account_t *target,user_role_t role) {
|
|
|
|
|
assert(role==USER_ROLE_ADMIN); return user_database_delete_current(target);
|
|
|
|
|
}
|
|
|
|
|
esp_err_t user_database_create(const uint8_t *u,size_t n,user_role_t role,const uint8_t *p,size_t pn) {
|
|
|
|
|
assert(dispatcher && !host_lock_depth && n==5 && !memcmp(u,"carol",5) && role==USER_ROLE_ADMIN);
|
|
|
|
|
assert(user_database_password_valid(p,pn)); check_slot_wiped(); ++mutations;
|
|
|
|
|
if (mutation_hook) { void (*hook)(void)=mutation_hook; mutation_hook=NULL; hook(); }
|
|
|
|
|
return mutation_error;
|
|
|
|
|
}
|
|
|
|
|
esp_err_t user_database_set_password_current(const user_database_account_t *target,const uint8_t *p,size_t pn) {
|
|
|
|
|
assert(user_database_password_valid(p,pn)); executing_password=p;
|
|
|
|
|
return user_database_delete_current(target);
|
|
|
|
|
}
|
|
|
|
|
esp_err_t web_serial_transport_revoke_user(const uint8_t *u,size_t n) {
|
|
|
|
|
assert(dispatcher && !host_lock_depth && n==5 && !memcmp(u,self_target ? "alice" : "carol",5));
|
|
|
|
|
assert(!executing_password);
|
|
|
|
|
if (self_target) web_session_store_invalidate_username(u,n);
|
|
|
|
|
++web_revokes; return ESP_FAIL;
|
|
|
|
|
}
|
|
|
|
|
esp_err_t ssh_transport_revoke_user(const uint8_t *u,size_t n) {
|
|
|
|
|
assert(dispatcher && !host_lock_depth && n==5 && !memcmp(u,self_target ? "alice" : "carol",5)); ++ssh_revokes; return ESP_FAIL;
|
|
|
|
|
}
|
|
|
|
|
static const char deletion[]="{\"action\":\"delete\",\"username\":\"carol\",\"user_id\":7,\"auth_generation\":2}";
|
|
|
|
|
static const char role_body[]="{\"action\":\"role\",\"username\":\"carol\",\"user_id\":7,\"auth_generation\":2,\"role\":\"admin\"}";
|
|
|
|
|
static void account_begin(const issued_t *identity,const char *body) {
|
|
|
|
|
begin("/api/settings/account-operation",body?HTTP_POST:HTTP_GET,body); same_origin();
|
|
|
|
|
if (body) add("Content-Type","application/json");
|
|
|
|
|
if (identity) {
|
|
|
|
|
char cookie[100]; snprintf(cookie,sizeof(cookie),"__Host-sak-session=%s",identity->token); add("Cookie",cookie);
|
|
|
|
|
if (body) add("X-CSRF-Token",identity->view.csrf);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
static void account_expect(const char *status) {
|
|
|
|
|
unsigned before=mutations;
|
|
|
|
|
esp_err_t error=web_account_settings_handler(&req);
|
|
|
|
|
assert(error==(send_fail || aux.remaining_len ? ESP_FAIL : ESP_OK));
|
|
|
|
|
if (strcmp(status,response_status)) fprintf(stderr,"expected %s got %s\n",status,response_status);
|
|
|
|
|
assert(!strcmp(status,response_status) && mutations==before);
|
|
|
|
|
assert(strlen(output)<1024); zero(scratch,sizeof(scratch));
|
|
|
|
|
}
|
|
|
|
|
static void submit_account(const issued_t *identity,const char *body) {
|
|
|
|
|
account_begin(identity,body); account_expect("202 Accepted"); assert(s_operation.id==queued && s_operation.state==PENDING);
|
|
|
|
|
}
|
|
|
|
|
static void execute_account(void) { dispatcher=true; web_account_settings_execute(queued); dispatcher=false; }
|
|
|
|
|
static void invalidate_actor(void) { web_session_store_invalidate(s_operation.session); }
|
|
|
|
|
static const char create_body[]="{\"action\":\"create\",\"username\":\"carol\",\"role\":\"admin\",\"password\":\"password1234\"}";
|
|
|
|
|
static const char password_body[]="{\"action\":\"password\",\"username\":\"carol\",\"user_id\":7,\"auth_generation\":2,\"password\":\"password1234\"}";
|
|
|
|
|
static void generate_begin(const issued_t *identity) {
|
|
|
|
|
account_begin(identity,""); req.uri="/api/settings/accounts/generate-password";
|
|
|
|
|
}
|
|
|
|
|
static void generate_expect(const char *status) {
|
|
|
|
|
account_operation_t before=s_operation;
|
|
|
|
|
unsigned ids=s_next_id, calls=mutations, response_wipes=wiped_responses, generated_wipes=wiped_generated;
|
|
|
|
|
esp_err_t result=web_account_generate_password_handler(&req);
|
|
|
|
|
assert(result==((send_fail || fail_header || aux.remaining_len) ? ESP_FAIL : ESP_OK));
|
|
|
|
|
if (!fail_header) assert(!strcmp(status,response_status));
|
|
|
|
|
assert(!memcmp(&before,&s_operation,sizeof(before)) && ids==s_next_id && mutations==calls);
|
|
|
|
|
assert(wiped_responses==response_wipes+1 && wiped_generated>generated_wipes);
|
|
|
|
|
zero(scratch,sizeof(scratch));
|
|
|
|
|
}
|
|
|
|
|
static void generated_tests(void) {
|
|
|
|
|
auth_reset(); issued_t admin=mint(&alice), user=mint(&bob);
|
|
|
|
|
unsigned rng=rng_calls;
|
|
|
|
|
generate_begin(NULL); generate_expect("401 Unauthorized");
|
|
|
|
|
generate_begin(&user); generate_expect("403 Forbidden");
|
|
|
|
|
for (unsigned mode=0;mode<9;++mode) {
|
|
|
|
|
generate_begin(&admin);
|
|
|
|
|
if (mode==0) { req.content_len=aux.remaining_len=1; }
|
|
|
|
|
if (mode==1) req.method=HTTP_GET;
|
|
|
|
|
if (mode==2) req.uri="/api/settings/accounts/generate-password?x=1";
|
|
|
|
|
if (mode==3) add("Origin","https://evil.example");
|
|
|
|
|
if (mode==4) add("X-CSRF-Token","duplicate");
|
|
|
|
|
if (mode==5) add("Transfer-Encoding","chunked");
|
|
|
|
|
if (mode==6) add("Sec-Fetch-Site","cross-site");
|
|
|
|
|
if (mode==7) stale_user=alice.user_id;
|
|
|
|
|
if (mode==8) db_fail=true;
|
|
|
|
|
(void)web_account_generate_password_handler(&req);
|
|
|
|
|
assert(response_status[0]=='4' && rng_calls==rng);
|
|
|
|
|
stale_user=0; db_fail=false;
|
|
|
|
|
}
|
|
|
|
|
admin=mint(&alice); rng=rng_calls;
|
|
|
|
|
generate_begin(&admin); generate_expect("200 OK");
|
|
|
|
|
assert(rng_calls==rng+1 && strlen(output)==39 && !strncmp(output,"{\"password\":\"",13));
|
|
|
|
|
for (unsigned i=13;i<37;++i) assert(strchr((const char *)s_generated_alphabet,output[i]));
|
|
|
|
|
bool no_store=false;
|
|
|
|
|
for (unsigned i=0;i<aux.resp_hdrs_count;++i)
|
|
|
|
|
if (!strcmp(response_headers[i].field,"Cache-Control")) no_store=!strcmp(response_headers[i].value,"no-store");
|
|
|
|
|
assert(no_store);
|
|
|
|
|
rng_fail=true; generate_begin(&admin); generate_expect("503 Service Unavailable"); rng_fail=false;
|
|
|
|
|
assert(!strstr(output,"password"));
|
|
|
|
|
send_fail=true; generate_begin(&admin); generate_expect("200 OK"); send_fail=false;
|
|
|
|
|
fail_header=1; setter_calls=0; generate_begin(&admin); generate_expect(""); fail_header=0;
|
|
|
|
|
unsigned header_capacity=server.config.max_resp_headers;
|
|
|
|
|
for (unsigned capacity=0;capacity<3;++capacity) {
|
|
|
|
|
generate_begin(&admin); server.config.max_resp_headers=capacity;
|
|
|
|
|
unsigned wipes=wiped_responses, sent=sends;
|
|
|
|
|
assert(web_account_generate_password_handler(&req)==ESP_ERR_HTTPD_RESP_HDR);
|
|
|
|
|
assert(sends==sent && wiped_responses==wipes+1);
|
|
|
|
|
}
|
|
|
|
|
server.config.max_resp_headers=header_capacity;
|
|
|
|
|
hook_id=admin.view.id; rng_hook=invalidate_hook;
|
|
|
|
|
generate_begin(&admin); generate_expect("401 Unauthorized"); assert(!strstr(output,"password"));
|
|
|
|
|
admin=mint(&alice);
|
|
|
|
|
/* Generate even with the operation slot occupied; it must not touch it. */
|
|
|
|
|
submit_account(&admin,create_body); generate_begin(&admin); generate_expect("200 OK"); execute_account();
|
|
|
|
|
web_session_store_invalidate(admin.view.id);
|
|
|
|
|
rng=rng_calls; generate_begin(&admin); generate_expect("401 Unauthorized"); assert(rng_calls==rng);
|
|
|
|
|
puts("PASS Accounts generated value: real RNG helper/auth/store, bodyless/admin/currentness/Origin/CSRF, base64url/no-store, no slot/DB write, success/RNG/header/send cleanup");
|
|
|
|
|
}
|
|
|
|
|
static void expire_queued(void) { now=s_operation.deadline; timer_callback(NULL); }
|
|
|
|
|
static void executing_tick(void) {
|
|
|
|
|
assert(s_operation.executing); check_slot_wiped();
|
|
|
|
|
now=s_operation.deadline; timer_callback(NULL);
|
|
|
|
|
assert(s_operation.executing && s_operation.state==PENDING);
|
|
|
|
|
web_account_settings_execute(s_operation.id); /* Duplicate dispatcher delivery. */
|
|
|
|
|
}
|
|
|
|
|
static void validation_tick(void) {
|
|
|
|
|
assert(s_operation.executing); check_slot_wiped();
|
|
|
|
|
timer_callback(NULL); assert(s_operation.state==PENDING);
|
|
|
|
|
}
|
|
|
|
|
static issued_t busy_actor;
|
|
|
|
|
static void executing_busy_request(void) {
|
|
|
|
|
assert(dispatcher && s_operation.executing);
|
|
|
|
|
dispatcher=false; account_begin(&busy_actor,create_body); account_expect("503 Service Unavailable"); dispatcher=true;
|
|
|
|
|
assert(s_operation.executing); check_slot_wiped();
|
|
|
|
|
}
|
|
|
|
|
static void credential_tests(void) {
|
|
|
|
|
auth_reset(); issued_t admin=mint(&alice); receive_fragment=768;
|
|
|
|
|
unsigned before=mutations;
|
|
|
|
|
timer_create_fail=true; account_begin(&admin,create_body); account_expect("503 Service Unavailable"); timer_create_fail=false;
|
|
|
|
|
assert(!s_secret_timer && mutations==before);
|
|
|
|
|
timer_start_fail=true; account_begin(&admin,password_body); account_expect("503 Service Unavailable"); timer_start_fail=false;
|
|
|
|
|
assert(s_secret_timer && !s_secret_timer_started && mutations==before);
|
|
|
|
|
submit_account(&admin,password_body); assert(timer_creates==2 && timer_starts==2);
|
|
|
|
|
uint32_t old=queued; assert(s_operation.password_length==12);
|
|
|
|
|
now=s_operation.deadline-1; timer_callback(NULL); assert(s_operation.state==PENDING);
|
|
|
|
|
++now; timer_callback(NULL); assert(s_operation.state==CANCELLED); check_slot_wiped();
|
|
|
|
|
submit_account(&admin,create_body); timer_callback(NULL); assert(s_operation.state==PENDING);
|
|
|
|
|
dispatcher=true; web_account_settings_execute(old); dispatcher=false;
|
|
|
|
|
assert(s_operation.state==PENDING && mutations==before);
|
|
|
|
|
execute_account(); assert(mutations==before+1 && s_operation.state==OK); check_slot_wiped();
|
|
|
|
|
assert(timer_creates==2 && timer_starts==2);
|
|
|
|
|
/* Timeout between publication and queue submission leaves only a stale ID. */
|
|
|
|
|
queue_hook=expire_queued; account_begin(&admin,password_body); account_expect("202 Accepted");
|
|
|
|
|
assert(s_operation.state==CANCELLED); execute_account(); assert(mutations==before+1); check_slot_wiped();
|
|
|
|
|
queue_fail=true; account_begin(&admin,password_body); account_expect("503 Service Unavailable"); queue_fail=false;
|
|
|
|
|
zero(&s_operation,sizeof(s_operation));
|
|
|
|
|
submit_account(&admin,password_body); db_hook=validation_tick; execute_account();
|
|
|
|
|
assert(s_operation.state==OK && !executing_password); check_slot_wiped();
|
|
|
|
|
submit_account(&admin,password_body); mutation_hook=executing_tick; execute_account();
|
|
|
|
|
assert(s_operation.state==OK && !s_operation.executing && !executing_password);
|
|
|
|
|
/* Tick during external validation, followed by deadline check: cancelled. */
|
|
|
|
|
submit_account(&admin,password_body); db_hook=executing_tick; before=mutations; execute_account();
|
|
|
|
|
assert(s_operation.state==CANCELLED && mutations==before); check_slot_wiped();
|
|
|
|
|
submit_account(&admin,password_body); web_session_store_invalidate(admin.view.id); execute_account();
|
|
|
|
|
assert(s_operation.state==CANCELLED && mutations==before); check_slot_wiped();
|
|
|
|
|
admin=mint(&alice);
|
|
|
|
|
submit_account(&admin,password_body); stale_user=alice.user_id; execute_account(); stale_user=0;
|
|
|
|
|
assert(s_operation.state==CANCELLED && mutations==before); check_slot_wiped();
|
|
|
|
|
admin=mint(&alice);
|
|
|
|
|
submit_account(&admin,password_body); db_fail=true; execute_account(); db_fail=false;
|
|
|
|
|
assert(s_operation.state==CANCELLED && mutations==before); check_slot_wiped();
|
|
|
|
|
admin=mint(&alice);
|
|
|
|
|
const esp_err_t errors[]={ESP_ERR_NOT_FOUND,ESP_FAIL,ESP_ERR_INVALID_STATE,ESP_ERR_NO_MEM};
|
|
|
|
|
const unsigned states[]={STALE,FAILED,DUPLICATE,FULL};
|
|
|
|
|
unsigned revokes=web_revokes;
|
|
|
|
|
for (unsigned i=0;i<4;++i) {
|
|
|
|
|
mutation_error=errors[i]; submit_account(&admin,i<2?password_body:create_body); execute_account();
|
|
|
|
|
assert(s_operation.state==states[i] && web_revokes==revokes && !executing_password); check_slot_wiped();
|
|
|
|
|
}
|
|
|
|
|
mutation_error=ESP_OK;
|
|
|
|
|
send_fail=true; account_begin(&admin,password_body); account_expect("202 Accepted"); send_fail=false;
|
|
|
|
|
assert(s_operation.password_length==12); execute_account(); assert(s_operation.state==OK); check_slot_wiped();
|
|
|
|
|
/* Keep executing ownership even if another request arrives during validation. */
|
|
|
|
|
busy_actor=admin; submit_account(&admin,password_body); db_hook=executing_busy_request; execute_account();
|
|
|
|
|
assert(s_operation.state==OK); check_slot_wiped();
|
|
|
|
|
queue_fail=true; queue_hook=expire_queued;
|
|
|
|
|
account_begin(&admin,create_body); account_expect("503 Service Unavailable"); queue_fail=false;
|
|
|
|
|
zero(&s_operation,sizeof(s_operation));
|
|
|
|
|
assert(timer_creates==2 && timer_starts==2);
|
|
|
|
|
puts("PASS Accounts credential queue: timer create/start failure, explicit expiry, replacement/old IDs, queue failure, validation/execution ticks, stale/dead sessions, error and lost-ack cleanup");
|
|
|
|
|
}
|
|
|
|
|
static void password_parser_tests(void) {
|
|
|
|
|
auth_reset(); issued_t admin=mint(&alice); receive_fragment=768;
|
|
|
|
|
char body[800];
|
|
|
|
|
const char *valid[]={"password1234", "space space ", "quote\\\"slash\\\\", "\\u0020\\u0022\\u005c123456789", "slash\\/1234567"};
|
|
|
|
|
for (unsigned i=0;i<sizeof(valid)/sizeof(*valid);++i) {
|
|
|
|
|
snprintf(body,sizeof(body),"{\"action\":\"create\",\"username\":\"carol\",\"role\":\"admin\",\"password\":\"%s\"}",valid[i]);
|
|
|
|
|
account_operation_t parsed={0}; assert(parse(body,strlen(body),&parsed));
|
|
|
|
|
assert(user_database_password_valid(parsed.password,parsed.password_length));
|
|
|
|
|
if (i==2) assert(parsed.password_length==12 && !memcmp(parsed.password,"quote\"slash\\",12));
|
|
|
|
|
secure_wipe(&parsed,sizeof(parsed));
|
|
|
|
|
submit_account(&admin,body); execute_account(); assert(s_operation.state==OK);
|
|
|
|
|
}
|
|
|
|
|
const char *invalid[]={"", "12345678901", "12345678901\\n", "12345678901\\t", "12345678901\\u0000", "12345678901\\u007f", "12345678901\\u0080", "12345678901\\uD800", "12345678901\\x20"};
|
|
|
|
|
for (unsigned i=0;i<sizeof(invalid)/sizeof(*invalid);++i) {
|
|
|
|
|
snprintf(body,sizeof(body),"{\"action\":\"create\",\"username\":\"carol\",\"role\":\"admin\",\"password\":\"%s\"}",invalid[i]);
|
|
|
|
|
account_begin(&admin,body); account_expect("400 Bad Request");
|
|
|
|
|
}
|
|
|
|
|
for (unsigned n=11;n<=65;++n) {
|
|
|
|
|
char escaped[391];
|
|
|
|
|
for (unsigned i=0;i<n;++i) memcpy(escaped+i*6,"\\u0022",6);
|
|
|
|
|
escaped[n*6]=0;
|
|
|
|
|
snprintf(body,sizeof(body),"{\"action\":\"password\",\"username\":\"carol\",\"user_id\":7,\"auth_generation\":2,\"password\":\"%s\"}",escaped);
|
|
|
|
|
account_begin(&admin,body);
|
|
|
|
|
account_expect(n>=12 && n<=64 ? "202 Accepted" : "400 Bad Request");
|
|
|
|
|
if (n>=12 && n<=64) { assert(s_operation.password_length==n); execute_account(); }
|
|
|
|
|
}
|
|
|
|
|
const char *bad[]={
|
|
|
|
|
"{\"action\":\"create\",\"username\":\"carol\",\"role\":\"admin\",\"password\":\"password1234\",\"user_id\":7}",
|
|
|
|
|
"{\"action\":\"password\",\"username\":\"carol\",\"password\":\"password1234\"}",
|
|
|
|
|
"{\"action\":\"password\",\"username\":\"carol\",\"user_id\":7,\"auth_generation\":2,\"password\":\"password1234\",\"role\":\"admin\"}",
|
|
|
|
|
"{\"action\":\"create\",\"username\":\"carol\",\"role\":\"admin\",\"password\":\"password1234\",\"password\":\"password5678\"}"};
|
|
|
|
|
for (unsigned i=0;i<sizeof(bad)/sizeof(*bad);++i) { account_begin(&admin,bad[i]); account_expect("400 Bad Request"); }
|
|
|
|
|
for (size_t i=0;i<strlen(password_body);++i) {
|
|
|
|
|
account_begin(&admin,password_body); req.content_len=aux.remaining_len=i;
|
|
|
|
|
account_expect("400 Bad Request");
|
|
|
|
|
}
|
|
|
|
|
size_t n=strlen(create_body); memcpy(body,create_body,n); memset(body+n,' ',768-n); body[768]=0;
|
|
|
|
|
receive_fragment=192; submit_account(&admin,body); execute_account();
|
|
|
|
|
receive_fragment=191; account_begin(&admin,body); account_expect("400 Bad Request");
|
|
|
|
|
receive_fragment=768; recv_fail=true; account_begin(&admin,password_body); account_expect("400 Bad Request"); recv_fail=false;
|
|
|
|
|
puts("PASS Accounts passwords: decoded quote/backslash/space/slash/Unicode escapes, printable ASCII 12..64, exact schemas, truncations, 768-byte/four-receive bound");
|
|
|
|
|
}
|
|
|
|
|
static void self_tests(void) {
|
|
|
|
|
const char *bodies[]={
|
|
|
|
|
"{\"action\":\"role\",\"username\":\"alice\",\"user_id\":7,\"auth_generation\":2,\"role\":\"admin\"}",
|
|
|
|
|
"{\"action\":\"delete\",\"username\":\"alice\",\"user_id\":7,\"auth_generation\":2}",
|
|
|
|
|
"{\"action\":\"password\",\"username\":\"alice\",\"user_id\":7,\"auth_generation\":2,\"password\":\"password1234\"}"};
|
|
|
|
|
self_target=true;
|
|
|
|
|
for (unsigned i=0;i<3;++i) {
|
|
|
|
|
auth_reset(); issued_t admin=mint(&alice), sibling=mint(&alice), user=mint(&bob);
|
|
|
|
|
unsigned revokes=web_revokes;
|
|
|
|
|
mutation_error=ESP_ERR_INVALID_STATE; submit_account(&admin,bodies[i]); execute_account();
|
|
|
|
|
assert(s_operation.state==PROTECTED && web_revokes==revokes);
|
|
|
|
|
account_begin(&admin,NULL); account_expect("200 OK"); assert(strstr(output,"protected"));
|
|
|
|
|
mutation_error=ESP_OK; submit_account(&admin,bodies[i]); execute_account();
|
|
|
|
|
assert(s_operation.state==OK && web_revokes==revokes+1 && !executing_password);
|
|
|
|
|
account_begin(&admin,NULL); account_expect("401 Unauthorized"); assert(!strstr(output,"\"state\""));
|
|
|
|
|
account_begin(&sibling,NULL); account_expect("401 Unauthorized");
|
|
|
|
|
bool current=false; assert(web_session_store_check_principal(user.view.id,&user.view.principal,¤t)==ESP_OK && current);
|
|
|
|
|
}
|
|
|
|
|
self_target=false;
|
|
|
|
|
puts("PASS Accounts self role/delete/password: protected failures keep login, success immediately target-revokes all logins, stale result reads denied, unrelated user survives");
|
|
|
|
|
}
|
|
|
|
|
static void account_settings_tests(void) {
|
|
|
|
|
auth_reset(); issued_t admin=mint(&alice), user=mint(&bob), other=mint(&alice); receive_fragment=64;
|
|
|
|
|
account_begin(NULL,deletion); account_expect("401 Unauthorized");
|
|
|
|
|
account_begin(&user,deletion); account_expect("403 Forbidden");
|
|
|
|
|
for (unsigned mode=0;mode<8;++mode) {
|
|
|
|
|
account_begin(&admin,deletion);
|
|
|
|
|
if (mode==0) req.content_len=aux.remaining_len=769;
|
|
|
|
|
if (mode==1) req.uri="/api/settings/account-operation?x=1";
|
|
|
|
|
if (mode==2) req.method=HTTP_GET;
|
|
|
|
|
if (mode==3) add("X-CSRF-Token","duplicate");
|
|
|
|
|
if (mode==4) add("Origin","https://evil.example");
|
|
|
|
|
if (mode==5) add("Transfer-Encoding","chunked");
|
|
|
|
|
if (mode==6) add("Content-Type","text/plain");
|
|
|
|
|
if (mode==7) add("Sec-Fetch-Site","cross-site");
|
|
|
|
|
unsigned before=s_next_id; (void)web_account_settings_handler(&req);
|
|
|
|
|
assert(response_status[0]=='4' && s_next_id==before && !mutations);
|
|
|
|
|
}
|
|
|
|
|
puts("PASS Accounts security: current cookie/admin, body/query/framing/CSRF/Origin bounds");
|
|
|
|
|
const char *bad[]={"{}","[]","{\"action\":\"password\"}","{\"action\":\"delete\",\"action\":\"role\"}",
|
|
|
|
|
"{\"action\":\"delete\",\"username\":\"carol\",\"user_id\":0,\"auth_generation\":2}",
|
|
|
|
|
"{\"action\":\"delete\",\"username\":\"carol\",\"user_id\":4294967296,\"auth_generation\":2}",
|
|
|
|
|
"{\"action\":\"delete\",\"username\":\"carol\",\"user_id\":07,\"auth_generation\":2}",
|
|
|
|
|
"{\"action\":\"delete\",\"username\":\"carol\",\"user_id\":7e0,\"auth_generation\":2}",
|
|
|
|
|
"{\"action\":\"delete\",\"username\":\"c\\u0061rol\",\"user_id\":7,\"auth_generation\":2}",
|
|
|
|
|
"{\"action\":\"delete\",\"username\":\"carol\",\"user_id\":7,\"auth_generation\":2,\"role\":\"user\"}"};
|
|
|
|
|
for (unsigned i=0;i<sizeof(bad)/sizeof(*bad);++i) { account_begin(&admin,bad[i]); account_expect("400 Bad Request"); }
|
|
|
|
|
for (size_t n=0;n<strlen(role_body);++n) { account_operation_t out={0}; assert(!parse(role_body,n,&out)); }
|
|
|
|
|
account_operation_t parsed={0}; assert(parse(role_body,strlen(role_body),&parsed));
|
|
|
|
|
receive_fragment=1; account_begin(&admin,deletion); account_expect("400 Bad Request"); receive_fragment=64;
|
|
|
|
|
puts("PASS Accounts strict bounded schema/parser and fragmented-body rejection");
|
|
|
|
|
account_begin(&admin,NULL); req.uri="/api/settings/accounts"; account_expect("200 OK");
|
|
|
|
|
assert(strstr(output,"account000000007") && !strstr(output,"password") && !strstr(output,"key"));
|
|
|
|
|
list_fail=true; account_begin(&admin,NULL); req.uri="/api/settings/accounts"; account_expect("503 Service Unavailable"); list_fail=false;
|
|
|
|
|
unsigned reads=lists; account_begin(&user,NULL); req.uri="/api/settings/accounts"; account_expect("403 Forbidden"); assert(lists==reads);
|
|
|
|
|
puts("PASS Accounts list: eight bounded public projections, zero mutation and unavailable/role isolation");
|
|
|
|
|
queue_fail=true; account_begin(&admin,deletion); account_expect("503 Service Unavailable"); queue_fail=false;
|
|
|
|
|
submit_account(&admin,deletion); uint32_t old=queued;
|
|
|
|
|
account_begin(&other,role_body); account_expect("503 Service Unavailable");
|
|
|
|
|
account_begin(&other,NULL); account_expect("200 OK"); assert(strstr(output,"\"id\":0"));
|
|
|
|
|
account_begin(&admin,NULL); account_expect("200 OK"); assert(strstr(output,"pending"));
|
|
|
|
|
execute_account(); assert(s_operation.state==OK && mutations==1 && web_revokes==1 && ssh_revokes==1);
|
|
|
|
|
execute_account(); assert(mutations==1);
|
|
|
|
|
submit_account(&other,role_body); dispatcher=true; web_account_settings_execute(old); web_account_settings_execute(0); dispatcher=false;
|
|
|
|
|
assert(mutations==1 && s_operation.state==PENDING); execute_account(); assert(mutations==2 && web_revokes==2);
|
|
|
|
|
account_begin(&admin,NULL); account_expect("200 OK"); assert(strstr(output,"\"id\":0"));
|
|
|
|
|
puts("PASS Accounts single pending slot, session isolation, stale queued IDs and target-only best-effort notifications");
|
|
|
|
|
const esp_err_t errors[]={ESP_FAIL,ESP_ERR_NOT_FOUND,ESP_ERR_INVALID_STATE};
|
|
|
|
|
const unsigned states[]={FAILED,STALE,PROTECTED};
|
|
|
|
|
for (unsigned i=0;i<3;++i) { mutation_error=errors[i]; submit_account(&admin,deletion); execute_account(); assert(s_operation.state==states[i] && web_revokes==2 && ssh_revokes==2); }
|
|
|
|
|
mutation_error=ESP_OK;
|
|
|
|
|
unsigned before=mutations; submit_account(&admin,deletion); s_operation.deadline=0; execute_account(); assert(s_operation.state==CANCELLED && mutations==before);
|
|
|
|
|
submit_account(&admin,deletion); web_session_store_invalidate(admin.view.id); execute_account(); assert(s_operation.state==CANCELLED && mutations==before);
|
|
|
|
|
submit_account(&other,deletion); mutation_hook=invalidate_actor; execute_account(); assert(s_operation.state==OK && mutations==before+1 && web_revokes==3);
|
|
|
|
|
zero(&s_operation.principal,sizeof(s_operation.principal)); zero(&s_operation.target,sizeof(s_operation.target));
|
|
|
|
|
auth_reset(); admin=mint(&alice); before=mutations;
|
|
|
|
|
submit_account(&admin,deletion); db_fail=true; execute_account(); db_fail=false;
|
|
|
|
|
assert(s_operation.state==CANCELLED && mutations==before);
|
|
|
|
|
admin=mint(&alice);
|
|
|
|
|
submit_account(&admin,deletion); stale_user=alice.user_id; execute_account(); stale_user=0;
|
|
|
|
|
assert(s_operation.state==CANCELLED && mutations==before);
|
|
|
|
|
puts("PASS Accounts execution failure/stale/protected results, dequeue cancellation, missed account revocation/DB failure and admitted-work completion after expiry");
|
|
|
|
|
credential_tests(); password_parser_tests(); generated_tests(); self_tests();
|
|
|
|
|
assert(wiped_passwords && wiped_bodies && wiped_generated && wiped_responses);
|
|
|
|
|
}
|