/* SPDX-License-Identifier: GPL-3.0-only */ /* Versioned NVS storage and validation for the SSH ECDSA P-256 host key. */ #include "ssh_security.h" #include #include #include #include #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" #include "freertos/task.h" #include "mbedtls/ecp.h" #include "mbedtls/pk.h" #include "mbedtls/sha256.h" #include "nvs.h" #include "secure_random.h" #define SSH_SECURITY_SCHEMA_VERSION 1U #define SSH_SECURITY_BLOB_SIZE 312U #define SSH_PUBLIC_POINT_LENGTH 65U #define SSH_PUBLIC_BLOB_LENGTH \ (4U + (sizeof(SSH_SECURITY_KEY_TYPE) - 1U) + \ 4U + (sizeof(SSH_SECURITY_CURVE_NAME) - 1U) + \ 4U + SSH_PUBLIC_POINT_LENGTH) typedef struct { uint32_t schema_version; uint16_t blob_size; uint16_t private_key_length; uint32_t generation; uint8_t private_key_der[SSH_SECURITY_PRIVATE_KEY_DER_CAPACITY]; uint8_t sha256_fingerprint[SSH_SECURITY_SHA256_LENGTH]; uint8_t reserved[12]; } ssh_security_blob_t; _Static_assert(offsetof(ssh_security_blob_t, private_key_der) == 12U, "SSH security schema offsets changed"); _Static_assert(offsetof(ssh_security_blob_t, sha256_fingerprint) == 268U, "SSH fingerprint offset changed"); _Static_assert(sizeof(ssh_security_blob_t) == SSH_SECURITY_BLOB_SIZE, "SSH security schema size changed"); static SemaphoreHandle_t s_security_mutex; static portMUX_TYPE s_mutex_init_lock = portMUX_INITIALIZER_UNLOCKED; static bool s_mutex_creating; static ssh_security_blob_t s_material; static bool s_material_ready; static ssh_security_load_result_t s_load_result; static uint32_t s_identity_token, s_next_identity_token; static TaskHandle_t s_identity_owner; static bool s_identity_used; static bool bytes_are_zero(const uint8_t *data, size_t size) { for (size_t index = 0U; index < size; ++index) { if (data[index] != 0U) { return false; } } return true; } static bool constant_time_equal(const uint8_t *left, const uint8_t *right, size_t size) { uint8_t difference = 0U; for (size_t index = 0U; index < size; ++index) { difference |= left[index] ^ right[index]; } return difference == 0U; } static esp_err_t ensure_mutex(void) { for (;;) { bool create = false; taskENTER_CRITICAL(&s_mutex_init_lock); if (s_security_mutex != NULL) { taskEXIT_CRITICAL(&s_mutex_init_lock); return ESP_OK; } if (!s_mutex_creating) { s_mutex_creating = true; create = true; } taskEXIT_CRITICAL(&s_mutex_init_lock); if (create) { SemaphoreHandle_t mutex = xSemaphoreCreateMutex(); taskENTER_CRITICAL(&s_mutex_init_lock); s_security_mutex = mutex; s_mutex_creating = false; taskEXIT_CRITICAL(&s_mutex_init_lock); return mutex != NULL ? ESP_OK : ESP_ERR_NO_MEM; } vTaskDelay(1U); } } static void write_u32_be(uint8_t output[4], uint32_t value) { output[0] = (uint8_t)(value >> 24U); output[1] = (uint8_t)(value >> 16U); output[2] = (uint8_t)(value >> 8U); output[3] = (uint8_t)value; } static size_t append_ssh_string(uint8_t *output, size_t offset, const uint8_t *value, size_t value_length) { write_u32_be(output + offset, (uint32_t)value_length); offset += 4U; memcpy(output + offset, value, value_length); return offset + value_length; } static esp_err_t fingerprint_key(const mbedtls_pk_context *key, uint8_t fingerprint[SSH_SECURITY_SHA256_LENGTH]) { const mbedtls_ecp_keypair *ec = mbedtls_pk_ec(*key); if (ec == NULL || mbedtls_ecp_keypair_get_group_id(ec) != MBEDTLS_ECP_DP_SECP256R1) { return ESP_ERR_INVALID_RESPONSE; } uint8_t point[SSH_PUBLIC_POINT_LENGTH] = {0}; size_t point_length = 0U; int result = mbedtls_ecp_point_write_binary( &ec->MBEDTLS_PRIVATE(grp), &ec->MBEDTLS_PRIVATE(Q), MBEDTLS_ECP_PF_UNCOMPRESSED, &point_length, point, sizeof(point)); if (result != 0 || point_length != sizeof(point)) { secure_wipe(point, sizeof(point)); return ESP_ERR_INVALID_RESPONSE; } uint8_t public_blob[SSH_PUBLIC_BLOB_LENGTH] = {0}; size_t offset = append_ssh_string( public_blob, 0U, (const uint8_t *)SSH_SECURITY_KEY_TYPE, sizeof(SSH_SECURITY_KEY_TYPE) - 1U); offset = append_ssh_string( public_blob, offset, (const uint8_t *)SSH_SECURITY_CURVE_NAME, sizeof(SSH_SECURITY_CURVE_NAME) - 1U); offset = append_ssh_string(public_blob, offset, point, point_length); result = offset == sizeof(public_blob) ? mbedtls_sha256(public_blob, offset, fingerprint, 0) : -1; secure_wipe(public_blob, sizeof(public_blob)); secure_wipe(point, sizeof(point)); return result == 0 ? ESP_OK : ESP_FAIL; } static esp_err_t parse_and_fingerprint(const ssh_security_blob_t *blob, uint8_t fingerprint[SSH_SECURITY_SHA256_LENGTH]) { mbedtls_pk_context key; mbedtls_pk_init(&key); int result = mbedtls_pk_parse_key(&key, blob->private_key_der, blob->private_key_length, NULL, 0U, secure_random_mbedtls, NULL); esp_err_t error = ESP_ERR_INVALID_RESPONSE; if (result == 0 && mbedtls_pk_get_type(&key) == MBEDTLS_PK_ECKEY) { const mbedtls_ecp_keypair *ec = mbedtls_pk_ec(key); if (ec != NULL && mbedtls_ecp_keypair_get_group_id(ec) == MBEDTLS_ECP_DP_SECP256R1 && mbedtls_ecp_check_privkey(&ec->MBEDTLS_PRIVATE(grp), &ec->MBEDTLS_PRIVATE(d)) == 0 && mbedtls_ecp_check_pubkey(&ec->MBEDTLS_PRIVATE(grp), &ec->MBEDTLS_PRIVATE(Q)) == 0 && mbedtls_pk_check_pair(&key, &key, secure_random_mbedtls, NULL) == 0) { error = fingerprint_key(&key, fingerprint); } } mbedtls_pk_free(&key); return error; } static esp_err_t validate_blob(const ssh_security_blob_t *blob) { if (blob == NULL) { return ESP_ERR_INVALID_ARG; } if (blob->schema_version != SSH_SECURITY_SCHEMA_VERSION || blob->blob_size != SSH_SECURITY_BLOB_SIZE) { return ESP_ERR_INVALID_VERSION; } if (blob->generation == 0U || blob->private_key_length == 0U || blob->private_key_length > sizeof(blob->private_key_der) || !bytes_are_zero(blob->private_key_der + blob->private_key_length, sizeof(blob->private_key_der) - blob->private_key_length) || !bytes_are_zero(blob->reserved, sizeof(blob->reserved))) { return ESP_ERR_INVALID_RESPONSE; } uint8_t fingerprint[SSH_SECURITY_SHA256_LENGTH] = {0}; esp_err_t error = parse_and_fingerprint(blob, fingerprint); if (error == ESP_OK && !constant_time_equal(fingerprint, blob->sha256_fingerprint, sizeof(fingerprint))) { error = ESP_ERR_INVALID_RESPONSE; } secure_wipe(fingerprint, sizeof(fingerprint)); return error; } static esp_err_t generate_blob(ssh_security_blob_t *blob, uint32_t generation) { memset(blob, 0, sizeof(*blob)); blob->schema_version = SSH_SECURITY_SCHEMA_VERSION; blob->blob_size = SSH_SECURITY_BLOB_SIZE; blob->generation = generation; mbedtls_pk_context key; mbedtls_pk_init(&key); int result = mbedtls_pk_setup(&key, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY)); esp_err_t error = result == 0 ? ESP_OK : ESP_ERR_NO_MEM; if (error == ESP_OK) { result = mbedtls_ecp_gen_key(MBEDTLS_ECP_DP_SECP256R1, mbedtls_pk_ec(key), secure_random_mbedtls, NULL); error = result == 0 ? ESP_OK : ESP_FAIL; } if (error == ESP_OK) { result = mbedtls_pk_write_key_der(&key, blob->private_key_der, sizeof(blob->private_key_der)); if (result <= 0 || (size_t)result > sizeof(blob->private_key_der)) { error = ESP_FAIL; } else { size_t length = (size_t)result; memmove(blob->private_key_der, blob->private_key_der + sizeof(blob->private_key_der) - length, length); memset(blob->private_key_der + length, 0, sizeof(blob->private_key_der) - length); blob->private_key_length = (uint16_t)length; } } if (error == ESP_OK) { error = fingerprint_key(&key, blob->sha256_fingerprint); } mbedtls_pk_free(&key); if (error == ESP_OK) { error = validate_blob(blob); } return error; } static esp_err_t save_blob(const ssh_security_blob_t *blob) { esp_err_t error = validate_blob(blob); if (error != ESP_OK) { return error; } nvs_handle_t handle; error = nvs_open(SSH_SECURITY_NVS_NAMESPACE, NVS_READWRITE, &handle); if (error != ESP_OK) { return error; } error = nvs_set_blob(handle, SSH_SECURITY_NVS_BLOB_KEY, blob, sizeof(*blob)); if (error == ESP_OK) { error = nvs_commit(handle); } nvs_close(handle); return error; } static esp_err_t load_blob(ssh_security_blob_t *blob, bool *missing) { *missing = false; nvs_handle_t handle; esp_err_t error = nvs_open(SSH_SECURITY_NVS_NAMESPACE, NVS_READONLY, &handle); if (error == ESP_ERR_NVS_NOT_FOUND) { *missing = true; return ESP_OK; } if (error != ESP_OK) { return error; } size_t size = 0U; error = nvs_get_blob(handle, SSH_SECURITY_NVS_BLOB_KEY, NULL, &size); if (error == ESP_ERR_NVS_NOT_FOUND) { *missing = true; nvs_close(handle); return ESP_OK; } if (error == ESP_ERR_NVS_TYPE_MISMATCH) { nvs_close(handle); return ESP_ERR_INVALID_RESPONSE; } if (error != ESP_OK) { nvs_close(handle); return error; } if (size != sizeof(*blob)) { nvs_close(handle); return ESP_ERR_INVALID_VERSION; } memset(blob, 0, sizeof(*blob)); error = nvs_get_blob(handle, SSH_SECURITY_NVS_BLOB_KEY, blob, &size); nvs_close(handle); if (error == ESP_ERR_NVS_INVALID_LENGTH) { return ESP_ERR_INVALID_VERSION; } return error == ESP_OK ? validate_blob(blob) : error; } static void install_blob(const ssh_security_blob_t *candidate) { secure_wipe(&s_material, sizeof(s_material)); s_material = *candidate; s_material_ready = true; s_load_result = SSH_SECURITY_LOAD_STORED; } esp_err_t ssh_security_init(ssh_security_load_result_t *load_result) { esp_err_t error = secure_random_init(); if (error != ESP_OK) { return error; } error = ensure_mutex(); if (error != ESP_OK) { return error; } xSemaphoreTake(s_security_mutex, portMAX_DELAY); if (s_material_ready) { if (load_result != NULL) { *load_result = s_load_result; } xSemaphoreGive(s_security_mutex); return ESP_OK; } if (s_identity_token) { xSemaphoreGive(s_security_mutex); return ESP_ERR_INVALID_STATE; } ssh_security_blob_t candidate; bool missing = false; error = load_blob(&candidate, &missing); if (error == ESP_OK && missing) { error = generate_blob(&candidate, 1U); if (error == ESP_OK) { error = save_blob(&candidate); } } if (error == ESP_OK) { s_material = candidate; s_material_ready = true; s_load_result = missing ? SSH_SECURITY_LOAD_GENERATED_MISSING : SSH_SECURITY_LOAD_STORED; if (load_result != NULL) { *load_result = s_load_result; } } secure_wipe(&candidate, sizeof(candidate)); xSemaphoreGive(s_security_mutex); return error; } esp_err_t ssh_security_copy_private_key(uint8_t *output, size_t capacity, size_t *output_length) { if (output_length == NULL || (output == NULL && capacity != 0U)) { return ESP_ERR_INVALID_ARG; } if (s_security_mutex == NULL) { return ESP_ERR_INVALID_STATE; } xSemaphoreTake(s_security_mutex, portMAX_DELAY); esp_err_t error = ESP_ERR_INVALID_STATE; if (s_material_ready) { *output_length = s_material.private_key_length; if (output == NULL) { error = capacity == 0U ? ESP_OK : ESP_ERR_INVALID_ARG; } else if (capacity < s_material.private_key_length) { error = ESP_ERR_INVALID_SIZE; } else { memcpy(output, s_material.private_key_der, s_material.private_key_length); error = ESP_OK; } } xSemaphoreGive(s_security_mutex); return error; } esp_err_t ssh_security_get_metadata(ssh_security_metadata_t *metadata) { if (metadata == NULL) { return ESP_ERR_INVALID_ARG; } if (s_security_mutex == NULL) { return ESP_ERR_INVALID_STATE; } xSemaphoreTake(s_security_mutex, portMAX_DELAY); esp_err_t error = ESP_ERR_INVALID_STATE; if (s_material_ready) { memset(metadata, 0, sizeof(*metadata)); metadata->generation = s_material.generation; memcpy(metadata->sha256_fingerprint, s_material.sha256_fingerprint, sizeof(metadata->sha256_fingerprint)); error = ESP_OK; } xSemaphoreGive(s_security_mutex); return error; } esp_err_t ssh_security_get_identity_snapshot(ssh_security_identity_snapshot_t *snapshot) { if (!snapshot) return ESP_ERR_INVALID_ARG; memset(snapshot, 0, sizeof(*snapshot)); if (!s_security_mutex) return ESP_ERR_INVALID_STATE; if (xSemaphoreTake(s_security_mutex, 0U) != pdTRUE) return ESP_ERR_TIMEOUT; esp_err_t error = s_material_ready ? ESP_OK : ESP_ERR_INVALID_STATE; if (error == ESP_OK) { snapshot->metadata.generation = s_material.generation; memcpy(snapshot->metadata.sha256_fingerprint, s_material.sha256_fingerprint, sizeof(snapshot->metadata.sha256_fingerprint)); snapshot->busy = s_identity_token != 0 || s_next_identity_token == UINT32_MAX; } xSemaphoreGive(s_security_mutex); return error; } esp_err_t ssh_security_reserve_identity(uint32_t generation, bool reset, uint32_t *token) { if (!token || (reset && generation)) return ESP_ERR_INVALID_ARG; *token = 0; if (reset) { esp_err_t error = secure_random_init(); if (error == ESP_OK) error = ensure_mutex(); if (error != ESP_OK) return error; } if (!s_security_mutex) return ESP_ERR_INVALID_STATE; if (xSemaphoreTake(s_security_mutex, 0U) != pdTRUE) return ESP_ERR_TIMEOUT; if (s_identity_token || s_next_identity_token == UINT32_MAX || (!s_material_ready && !reset) || (s_material_ready && s_material.generation == UINT32_MAX) || (generation && generation != s_material.generation)) { xSemaphoreGive(s_security_mutex); return ESP_ERR_INVALID_STATE; } *token = s_identity_token = ++s_next_identity_token; s_identity_owner = xTaskGetCurrentTaskHandle(); s_identity_used = false; xSemaphoreGive(s_security_mutex); return ESP_OK; } esp_err_t ssh_security_replace_reserved(uint32_t token) { if (!s_security_mutex || !token) return ESP_ERR_INVALID_STATE; xSemaphoreTake(s_security_mutex, portMAX_DELAY); if (s_identity_token != token || s_identity_used || s_identity_owner != xTaskGetCurrentTaskHandle()) { xSemaphoreGive(s_security_mutex); return ESP_ERR_INVALID_STATE; } s_identity_used = true; uint32_t generation = s_material_ready ? s_material.generation + 1U : 1U; xSemaphoreGive(s_security_mutex); /* Reservation excludes writers while crypto and flash run outside locks. */ ssh_security_blob_t candidate = {0}; esp_err_t error = generate_blob(&candidate, generation); if (error == ESP_OK) error = save_blob(&candidate); xSemaphoreTake(s_security_mutex, portMAX_DELAY); if (error == ESP_OK) install_blob(&candidate); xSemaphoreGive(s_security_mutex); secure_wipe(&candidate, sizeof(candidate)); return error; } void ssh_security_release_identity(uint32_t token) { if (!s_security_mutex || !token) return; xSemaphoreTake(s_security_mutex, portMAX_DELAY); if (s_identity_token == token && s_identity_owner == xTaskGetCurrentTaskHandle()) { s_identity_token = 0; s_identity_owner = NULL; } xSemaphoreGive(s_security_mutex); } static esp_err_t replace_identity(bool reset) { uint32_t token = 0; esp_err_t error = ssh_security_reserve_identity(0, reset, &token); if (error == ESP_OK) error = ssh_security_replace_reserved(token); ssh_security_release_identity(token); return error; } esp_err_t ssh_security_rotate(void) { return replace_identity(false); } esp_err_t ssh_security_reset(void) { return replace_identity(true); }