/* Full canonical storage/crypto; no hardware/power-loss/scheduler claims. */ #include #include #include #include "../../src/ssh_security.c" static unsigned depth, handles, wipes; static bool locked, busy, rng_fail, command_locked; static SemaphoreHandle_t s_command_mutex = (void *)2; static int s_lock; static bool s_initialized=true, s_running=true, s_transitioning, s_cleanup_pending, s_desired_running; static uint32_t s_management_generation=7, s_requested_sequence, s_completed_sequence; static esp_err_t s_command_result; static unsigned ticks, notifications; static bool stop_fail, start_fail; typedef unsigned TickType_t; #define pdMS_TO_TICKS(n) (n) #define SSH_TRANSPORT_COMMAND_TIMEOUT_MS 100 static unsigned xTaskGetTickCount(void) { return ticks; } static void notify_task(void) { assert(!depth && command_locked); ++notifications; } static int fault; static void *task = (void *)1; static void (*hook)(void); static ssh_security_blob_t stored, pending, before; static bool present, staged; void enter(void) { assert(!depth++); } void leave(void) { assert(!--depth); } TaskHandle_t xTaskGetCurrentTaskHandle(void) { return task; } void vTaskDelay(unsigned n) { assert(command_locked && !depth && !locked); ticks+=n; s_completed_sequence=s_requested_sequence; s_command_result=(s_desired_running ? start_fail : stop_fail) ? ESP_FAIL : ESP_OK; s_running=s_command_result==ESP_OK && s_desired_running; s_cleanup_pending=s_command_result!=ESP_OK; s_transitioning=false; } SemaphoreHandle_t xSemaphoreCreateMutex(void) { assert(!depth); return (void *)1; } int xSemaphoreTake(SemaphoreHandle_t m, unsigned wait) { if(m==s_command_mutex) { assert(!depth);if(command_locked){assert(!wait);return 0;}command_locked=true;return 1;} assert(m && !depth && !locked); if (busy) { assert(!wait); return 0; } locked = true; return 1; } int xSemaphoreGive(SemaphoreHandle_t m) { if(m==s_command_mutex){assert(!depth && command_locked);command_locked=false;return 1;} assert(m && locked && !depth); locked = false; return 1; } void secure_wipe(void *p, size_t n) { volatile unsigned char *b = p; for (size_t i=0;i=sizeof(stored)); memcpy(p,&stored,sizeof(stored)); } *n=sizeof(stored); return ESP_OK; } esp_err_t nvs_set_blob(nvs_handle_t h,const char *key,const void *p,size_t n) { assert(handles && h==NVS_READWRITE && !strcmp(key,"material") && n==312 && !depth); if (s_identity_token) assert(!locked && !memcmp(&s_material,&before,sizeof(before))); if (fault==2) return ESP_FAIL; memcpy(&pending,p,n); staged=true; return ESP_OK; } esp_err_t nvs_commit(nvs_handle_t h) { assert(handles && h==NVS_READWRITE && staged && !depth); if (fault==3) return ESP_FAIL; stored=pending; present=true; return ESP_OK; } void nvs_close(nvs_handle_t h) { (void)h; assert(handles--==1); staged=false; secure_wipe(&pending,sizeof(pending)); } #include "owner.inc" static void competitor(void) { assert(!depth && !locked); ssh_security_identity_snapshot_t v; assert(ssh_security_get_identity_snapshot(&v)==ESP_OK && v.busy); assert(v.metadata.generation==before.generation); assert(!memcmp(v.metadata.sha256_fingerprint,before.sha256_fingerprint,32)); assert(ssh_security_rotate()==ESP_ERR_INVALID_STATE); assert(ssh_security_reset()==ESP_ERR_INVALID_STATE); task=(void *)2; if(command_locked) { bool committed; assert(ssh_transport_replace_identity(s_management_generation,before.generation,false,&committed)==ESP_ERR_TIMEOUT); assert(ssh_transport_replace_host_key(true)==ESP_ERR_TIMEOUT); } assert(ssh_security_replace_reserved(s_identity_token)==ESP_ERR_INVALID_STATE); uint32_t token=s_identity_token; ssh_security_release_identity(token); assert(s_identity_token==token); task=(void *)1; } int main(void) { ssh_security_load_result_t result; assert(ssh_security_init(&result)==ESP_OK && result==SSH_SECURITY_LOAD_GENERATED_MISSING); assert(s_material.generation==1 && validate_blob(&s_material)==ESP_OK && !handles); before=s_material; uint8_t der[256]; size_t size=0; assert(ssh_security_copy_private_key(der,sizeof(der),&size)==ESP_OK && size==before.private_key_length); assert(!memcmp(der,before.private_key_der,size)); secure_wipe(der,sizeof(der)); s_material_ready=false; assert(ssh_security_init(&result)==ESP_OK && !memcmp(&s_material,&before,sizeof(before))); puts("PASS SSH real P256 generation/validation, bounded DER copy, exact persisted reload and handle closure"); for (fault=1;fault<=3;++fault) { before=s_material; hook=competitor; assert(ssh_security_rotate()!=ESP_OK && !s_identity_token && !handles); assert(!memcmp(&s_material,&before,sizeof(before)) && !memcmp(&stored,&before,sizeof(before))); } fault=0; rng_fail=true; before=s_material; assert(ssh_security_rotate()!=ESP_OK && !s_identity_token && !handles); assert(!memcmp(&s_material,&before,sizeof(before))); rng_fail=false; puts("PASS SSH real crypto RNG/NVS open-set-commit faults, unchanged live/stored bytes, reservation exclusion outside locks and wipes"); bool committed; before=s_material; unsigned old_notifications=notifications; assert(ssh_transport_replace_identity(6,1,false,&committed)==ESP_ERR_INVALID_STATE && notifications==old_notifications); assert(ssh_transport_replace_identity(7,2,false,&committed)==ESP_ERR_INVALID_STATE && notifications==old_notifications); stop_fail=true; assert(ssh_transport_replace_identity(7,1,false,&committed)==ESP_FAIL && !committed && notifications==old_notifications+1); assert(!memcmp(&s_material,&before,sizeof(before)) && !memcmp(&stored,&before,sizeof(before))); stop_fail=false;assert(ssh_transport_stop()==ESP_OK);assert(ssh_transport_start()==ESP_OK); for(fault=1;fault<=3;++fault) { before=s_material;hook=competitor; assert(ssh_transport_replace_identity(s_management_generation,1,false,&committed)==ESP_FAIL && !committed && s_running); assert(!memcmp(&s_material,&before,sizeof(before)) && !memcmp(&stored,&before,sizeof(before)) && !handles); } fault=0;start_fail=true;before=s_material;hook=competitor; assert(ssh_transport_replace_identity(s_management_generation,1,false,&committed)==ESP_FAIL && committed && !s_running); assert(s_material.generation==2 && !memcmp(&stored,&s_material,sizeof(stored))); start_fail=false;assert(ssh_transport_stop()==ESP_OK);assert(ssh_transport_start()==ESP_OK); puts("PASS integrated canonical SSH owner + real crypto/NVS: stale admission untouched, failed stop skips crypto/start, persistence failures restore old identity, committed restart failure never rolls back, competing CLI/direct owners excluded"); before=s_material; hook=competitor; assert(ssh_security_rotate()==ESP_OK); assert(s_material.generation==3 && memcmp(before.sha256_fingerprint,s_material.sha256_fingerprint,32)); assert(validate_blob(&s_material)==ESP_OK && !memcmp(&stored,&s_material,sizeof(stored))); uint32_t token=0, newer=0; assert(ssh_security_reserve_identity(1,false,&token)==ESP_ERR_INVALID_STATE && !token); assert(ssh_security_reserve_identity(3,false,&token)==ESP_OK); ssh_security_release_identity(token); assert(ssh_security_reserve_identity(3,false,&newer)==ESP_OK && newer!=token); ssh_security_release_identity(token); assert(s_identity_token==newer); assert(ssh_security_replace_reserved(token)==ESP_ERR_INVALID_STATE); before=s_material; assert(ssh_security_replace_reserved(newer)==ESP_OK); assert(ssh_security_replace_reserved(newer)==ESP_ERR_INVALID_STATE); ssh_security_release_identity(newer); puts("PASS SSH expected generation, owner-only nonreused token, stale release/replace and one-shot replacement"); ssh_security_identity_snapshot_t v; busy=true; assert(ssh_security_get_identity_snapshot(&v)==ESP_ERR_TIMEOUT && !v.metadata.generation); busy=false; s_next_identity_token=UINT32_MAX; assert(ssh_security_get_identity_snapshot(&v)==ESP_OK && v.busy); assert(ssh_security_rotate()==ESP_ERR_INVALID_STATE); s_next_identity_token=0; s_material.generation=UINT32_MAX; assert(ssh_security_reset()==ESP_ERR_INVALID_STATE); s_material_ready=false; stored.schema_version=99; assert(ssh_security_init(NULL)==ESP_ERR_INVALID_VERSION && stored.schema_version==99); before=s_material; assert(ssh_security_reset()==ESP_OK && s_material.generation==1 && validate_blob(&s_material)==ESP_OK); assert(!handles && !locked && !depth && wipes); puts("PASS SSH zero-wait public snapshot, saturation, corrupt-material fail-closed and canonical reset recovery"); }