Implement SSH authorized key management

This commit is contained in:
2026-09-08 16:37:47 +02:00
parent 22a7c7b0a5
commit 42f6423d4e
19 changed files with 930 additions and 83 deletions
+36 -14
View File
@@ -80,16 +80,33 @@ static int derive_password(const uint8_t *p, size_t n, const uint8_t *s,
}
return fail_stage==5 ? ESP_FAIL : ESP_OK;
}
#include <openssl/sha.h>
#include <openssl/ec.h>
#include <openssl/obj_mac.h>
typedef EC_GROUP *mbedtls_ecp_group;
typedef struct { EC_POINT *point; } mbedtls_ecp_point;
#define MBEDTLS_ECP_DP_SECP256R1 1
static void mbedtls_ecp_group_init(mbedtls_ecp_group *g) { *g=NULL; }
static void mbedtls_ecp_point_init(mbedtls_ecp_point *p) { p->point=NULL; }
static int mbedtls_ecp_group_load(mbedtls_ecp_group *g, int id) {
assert(id==1); *g=EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1); return *g ? 0 : -1;
}
static int mbedtls_ecp_point_read_binary(mbedtls_ecp_group *g, mbedtls_ecp_point *p, const uint8_t *b, size_t n) {
p->point=EC_POINT_new(*g); return p->point && EC_POINT_oct2point(*g,p->point,b,n,NULL)==1 ? 0 : -1;
}
static int mbedtls_ecp_check_pubkey(mbedtls_ecp_group *g, mbedtls_ecp_point *p) {
return EC_POINT_is_at_infinity(*g,p->point)==0 && EC_POINT_is_on_curve(*g,p->point,NULL)==1 ? 0 : -1;
}
static void mbedtls_ecp_point_free(mbedtls_ecp_point *p) { EC_POINT_free(p->point); }
static void mbedtls_ecp_group_free(mbedtls_ecp_group *g) { EC_GROUP_free(*g); }
static int mbedtls_sha256(const uint8_t *p, size_t n, uint8_t *h, int mode) {
(void)p; (void)n; (void)h; (void)mode; assert(!"keys outside slice"); return -1;
assert(mode==0); return SHA256(p,n,h) ? 0 : -1;
}
'''
header = strip_includes((ROOT / "src/user_database.h").read_text())
state = db[db.index("#define USER_DATABASE_SCHEMA_VERSION"):db.index("static esp_err_t initialize_dummy_verifier(")]
fakes = r'''
bool user_database_key_valid(const uint8_t *t, size_t tn, const uint8_t *b, size_t bn) {
(void)t; (void)tn; (void)b; (void)bn; assert(!"keys outside slice"); return false;
}
static stored_database_t candidate_storage;
static user_principal_t actor;
static bool admin_ssh_console_dispatch_is_remote(void) { return remote; }
@@ -133,16 +150,11 @@ esp_err_t user_database_create_generated(const uint8_t *u,size_t n,user_role_t r
esp_err_t user_database_generate_password(const uint8_t *u,size_t n,user_database_generated_password_t *p) {
(void)u; (void)n; (void)p; assert(!"generated credential"); return ESP_FAIL;
}
esp_err_t user_database_remove_ssh_key(const uint8_t *u,size_t n,uint8_t i) {
(void)u; (void)n; (void)i; assert(!"key mutation"); return ESP_FAIL;
}
esp_err_t user_database_clear_ssh_keys(const uint8_t *u,size_t n) {
(void)u; (void)n; assert(!"key mutation"); return ESP_FAIL;
}
size_t esp_console_split_argv(char *, char **, size_t);
'''
db_names = ["constant_time_equal", "all_zero", "user_database_username_valid",
"user_database_password_valid",
"user_database_password_valid", "read_ssh_string", "user_database_key_valid",
"user_role_to_string", "user_role_parse", "set_record_password", "find_user",
"find_free_user", "stored_keys_equal", "validate_database", "recount",
"next_generation", "discard_candidate", "commit_candidate_locked", "initialize_user",
@@ -151,7 +163,11 @@ db_names = ["constant_time_equal", "all_zero", "user_database_username_valid",
"user_database_delete", "user_database_set_role", "user_database_get_accounts",
"user_database_delete_current", "user_database_set_role_current",
"set_password", "user_database_set_password", "user_database_set_password_current",
"user_database_generate_password_value"]
"user_database_generate_password_value",
"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"]
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"]
@@ -159,15 +175,21 @@ 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")
unit += "\n".join(function(console, n) for n in console_names)
unit += (ROOT / "tests/admin_console_boundary/accounts.c").read_text()
account_tests = (ROOT / "tests/admin_console_boundary/accounts.c").read_text()
key_tests = (ROOT / "tests/admin_console_boundary/account_keys.c").read_text()
account_tests = account_tests.replace('int main(void)', key_tests + '\nint main(void)')
account_tests = account_tests.replace(' typed_account_tests();', ' typed_key_tests();\n typed_account_tests();')
assert ' typed_key_tests();' in account_tests
unit += account_tests
with tempfile.TemporaryDirectory(prefix="admin-accounts-") as directory:
path = Path(directory)
(path / "test.c").write_text(unit)
subprocess.run(["cc", "-std=c11", "-Wall", "-Wextra", "-Werror", "-Wno-unused-variable",
str(path / "test.c"), str(IDF / "components/console/split_argv.c"),
"-o", str(path / "test")], check=True, timeout=30)
"-lcrypto", "-o", str(path / "test")], check=True, timeout=30)
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: 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")