Implemented initial SSH support. Memory pressure too high for HTTPS and
SSH. Dirty commit.
This commit is contained in:
@@ -0,0 +1,470 @@
|
||||
/* 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 <limits.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
#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 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;
|
||||
}
|
||||
|
||||
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_rotate(void)
|
||||
{
|
||||
if (s_security_mutex == NULL) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
xSemaphoreTake(s_security_mutex, portMAX_DELAY);
|
||||
esp_err_t error = ESP_ERR_INVALID_STATE;
|
||||
ssh_security_blob_t candidate;
|
||||
memset(&candidate, 0, sizeof(candidate));
|
||||
if (s_material_ready && s_material.generation != UINT32_MAX) {
|
||||
error = generate_blob(&candidate, s_material.generation + 1U);
|
||||
if (error == ESP_OK) {
|
||||
error = save_blob(&candidate);
|
||||
}
|
||||
if (error == ESP_OK) {
|
||||
install_blob(&candidate);
|
||||
}
|
||||
}
|
||||
secure_wipe(&candidate, sizeof(candidate));
|
||||
xSemaphoreGive(s_security_mutex);
|
||||
return error;
|
||||
}
|
||||
|
||||
esp_err_t ssh_security_reset(void)
|
||||
{
|
||||
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);
|
||||
uint32_t generation = 1U;
|
||||
if (s_material_ready) {
|
||||
if (s_material.generation == UINT32_MAX) {
|
||||
xSemaphoreGive(s_security_mutex);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
generation = s_material.generation + 1U;
|
||||
}
|
||||
|
||||
ssh_security_blob_t candidate;
|
||||
error = generate_blob(&candidate, generation);
|
||||
if (error == ESP_OK) {
|
||||
error = save_blob(&candidate);
|
||||
}
|
||||
if (error == ESP_OK) {
|
||||
install_blob(&candidate);
|
||||
}
|
||||
secure_wipe(&candidate, sizeof(candidate));
|
||||
xSemaphoreGive(s_security_mutex);
|
||||
return error;
|
||||
}
|
||||
Reference in New Issue
Block a user