Add SSH host identity rotation controls
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
/* Full canonical storage/crypto; no hardware/power-loss/scheduler claims. */
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/random.h>
|
||||
#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<n;++i) b[i]=0; ++wipes; }
|
||||
esp_err_t secure_random_init(void) { assert(!depth); return ESP_OK; }
|
||||
esp_err_t secure_random_fill(void *p, size_t n) {
|
||||
assert(!depth && (!s_identity_token || !locked));
|
||||
if (hook) { void (*f)(void)=hook; hook=NULL; f(); }
|
||||
return !rng_fail && getrandom(p,n,0)==(ssize_t)n ? ESP_OK : ESP_FAIL;
|
||||
}
|
||||
int secure_random_mbedtls(void *ctx, unsigned char *p, size_t n) { (void)ctx; return secure_random_fill(p,n)==ESP_OK ? 0 : -1; }
|
||||
esp_err_t nvs_open(const char *name, int mode, nvs_handle_t *h) {
|
||||
assert(!depth && !strcmp(name,SSH_SECURITY_NVS_NAMESPACE));
|
||||
if (fault==1 && mode==NVS_READWRITE) return ESP_FAIL;
|
||||
assert(!handles++); *h=mode; return ESP_OK;
|
||||
}
|
||||
esp_err_t nvs_get_blob(nvs_handle_t h,const char *key,void *p,size_t *n) {
|
||||
assert(handles && h==NVS_READONLY && !strcmp(key,"material"));
|
||||
if (!present) return ESP_ERR_NVS_NOT_FOUND;
|
||||
if (p) { assert(*n>=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");
|
||||
}
|
||||
Reference in New Issue
Block a user