Implemented initial SSH support. Memory pressure too high for HTTPS and

SSH. Dirty commit.
This commit is contained in:
2026-08-24 22:30:34 +02:00
parent c018bfe361
commit 72d030bc7a
17 changed files with 2314 additions and 27 deletions
+12
View File
@@ -13,6 +13,9 @@ idf_component_register(
"serial_console.c"
"session_broker.c"
"session_console.c"
"ssh_security.c"
"ssh_transport.c"
"ssh_console.c"
"usb_cdc_transport.c"
"usb_console.c"
"web_security.c"
@@ -44,4 +47,13 @@ idf_component_register(
lwip
mbedtls
nvs_flash
wolfssl__wolfssh
wolfssl__wolfssl
)
# Public wolfSSH headers include wolfCrypt configuration from user_settings.h.
target_compile_definitions(${COMPONENT_LIB} PRIVATE
WOLFSSL_USER_SETTINGS
WOLFSSH_USER_SETTINGS
WC_RNG_SEED_CB
)
+16
View File
@@ -126,6 +126,22 @@ static const char *const s_completion_candidates[] = {
"web certificate rotate --force",
"web reset",
"web reset --force",
/* Authenticated SSH serial transport and independent host identity. */
"ssh help",
"ssh status",
"ssh start",
"ssh stop",
"ssh sessions",
"ssh disconnect",
"ssh counters",
"ssh clear-counters",
"ssh host-key",
"ssh host-key info",
"ssh host-key rotate",
"ssh host-key rotate --force",
"ssh reset",
"ssh reset --force",
};
static ssize_t console_read_with_late_terminal_upgrade(int file_descriptor,
+3
View File
@@ -4,3 +4,6 @@ dependencies:
idf: ">=5.3.0"
espressif/led_strip: "^3.0.3"
espressif/esp_tinyusb: "^2.2.1"
# Exact official registry versions form the reviewed Phase 6 integration baseline.
wolfssl/wolfssl: "5.8.2~1"
wolfssl/wolfssh: "1.4.20"
+38 -1
View File
@@ -13,6 +13,9 @@
#include "serial_service.h"
#include "session_broker.h"
#include "session_console.h"
#include "ssh_console.h"
#include "ssh_security.h"
#include "ssh_transport.h"
#include "status_led.h"
#include "system_console.h"
#include "usb_cdc_transport.h"
@@ -32,7 +35,7 @@ static const char *TAG = "firmware";
void app_main(void)
{
ESP_LOGI(TAG, "ESP32-S3 Serial Swiss Army Knife HTTPS foundation phase started");
ESP_LOGI(TAG, "ESP32-S3 Serial Swiss Army Knife SSH transport phase started");
if (esp_psram_is_initialized()) {
ESP_LOGI(TAG, "PSRAM initialized: %u bytes", (unsigned int)esp_psram_get_size());
@@ -88,6 +91,28 @@ void app_main(void)
esp_err_to_name(web_runtime_error));
}
ssh_security_load_result_t ssh_security_source = SSH_SECURITY_LOAD_STORED;
esp_err_t ssh_security_error = random_error;
if (ssh_security_error == ESP_OK) {
ssh_security_error = ssh_security_init(&ssh_security_source);
}
if (ssh_security_error != ESP_OK) {
ESP_LOGE(TAG,
"SSH host key unavailable (%s); use UART0 'ssh reset --force' to replace it",
esp_err_to_name(ssh_security_error));
} else {
ESP_LOGI(TAG, "Using %s SSH host key",
ssh_security_source == SSH_SECURITY_LOAD_STORED
? "stored"
: "newly generated");
}
esp_err_t ssh_runtime_error = ssh_transport_init();
if (ssh_runtime_error != ESP_OK) {
ESP_LOGE(TAG, "SSH runtime initialization failed: %s",
esp_err_to_name(ssh_runtime_error));
}
wifi_app_config_t wifi_config;
wifi_config_load_source_t wifi_config_source;
esp_err_t wifi_config_error = random_error;
@@ -143,6 +168,17 @@ void app_main(void)
ESP_LOGI(TAG, "Authenticated HTTPS listening on TCP port 443");
}
}
if (wifi_error == ESP_OK && web_security_error == ESP_OK &&
ssh_security_error == ESP_OK && ssh_runtime_error == ESP_OK) {
esp_err_t start_error = ssh_transport_start();
if (start_error != ESP_OK) {
ESP_LOGE(TAG, "SSH startup failed: %s; UART0 recovery remains available",
esp_err_to_name(start_error));
} else {
ESP_LOGI(TAG, "Authenticated SSH listening on TCP port %u",
SSH_TRANSPORT_PORT);
}
}
ESP_LOGI(
TAG,
@@ -174,6 +210,7 @@ void app_main(void)
ESP_ERROR_CHECK(usb_console_register_commands());
ESP_ERROR_CHECK(wifi_console_register_commands());
ESP_ERROR_CHECK(web_console_register_commands());
ESP_ERROR_CHECK(ssh_console_register_commands());
ESP_ERROR_CHECK(network_console_register_root_commands());
ESP_ERROR_CHECK(system_console_register_commands());
/* Upgrade late UART terminals safely and add nested completion. */
+304
View File
@@ -0,0 +1,304 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* UART0 SSH lifecycle, sessions, counters, and host-key recovery commands. */
#include "ssh_console.h"
#include <errno.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "esp_console.h"
#include "mbedtls/base64.h"
#include "secure_random.h"
#include "ssh_security.h"
#include "ssh_transport.h"
#include "web_security.h"
static void print_usage(void)
{
printf("Usage:\n");
printf(" ssh status|start|stop|sessions\n");
printf(" ssh disconnect <session-id>\n");
printf(" ssh counters|clear-counters\n");
printf(" ssh host-key info\n");
printf(" ssh host-key rotate --force\n");
printf(" ssh reset --force\n");
}
static const char *state_name(ssh_transport_session_state_t state)
{
switch (state) {
case SSH_TRANSPORT_SESSION_FREE:
return "free";
case SSH_TRANSPORT_SESSION_HANDSHAKE:
return "handshake";
case SSH_TRANSPORT_SESSION_ACTIVE:
return "active";
case SSH_TRANSPORT_SESSION_CLOSING:
return "closing";
default:
return "unknown";
}
}
static int print_sessions(const ssh_transport_snapshot_t *snapshot)
{
printf("SSH sessions: active=%" PRIu32 "/%u\n",
snapshot->active_sessions, SSH_TRANSPORT_MAX_SESSIONS);
for (size_t index = 0U; index < SSH_TRANSPORT_MAX_SESSIONS; ++index) {
const ssh_transport_session_snapshot_t *session = &snapshot->sessions[index];
if (!session->active) {
continue;
}
printf(" id=%" PRIu32 " slot=%u peer=%s state=%s auth=%s broker=%" PRIu32
" role=%s rx-pending=%s tx-pending=%s closing=%s\n",
session->session_id, (unsigned int)index, session->peer,
state_name(session->state), session->authenticated ? "yes" : "no",
session->broker_client_id,
session->broker_client_id == SESSION_BROKER_NO_CLIENT
? "unattached"
: (session->writer ? "writer" : "observer"),
session->rx_pending ? "yes" : "no",
session->tx_pending ? "yes" : "no",
session->close_requested ? "yes" : "no");
}
return 0;
}
static int show_status(bool sessions_only)
{
ssh_transport_snapshot_t snapshot;
esp_err_t error = ssh_transport_get_snapshot(&snapshot);
if (error != ESP_OK) {
printf("SSH runtime unavailable: %s\n", esp_err_to_name(error));
return 1;
}
if (!sessions_only) {
char username[WEB_SECURITY_USERNAME_CAPACITY + 1U] = {0};
size_t username_length = 0U;
esp_err_t username_error = web_security_copy_username(
username, sizeof(username), &username_length);
printf("SSH: initialized=%s running=%s transitioning=%s port=%u last-error=%s\n",
snapshot.initialized ? "yes" : "no",
snapshot.running ? "yes" : "no",
snapshot.transitioning ? "yes" : "no",
(unsigned int)snapshot.port,
esp_err_to_name(snapshot.last_error));
if (username_error == ESP_OK) {
printf("Authentication: SSH password, username=%.*s, shared with HTTPS\n",
(int)username_length, username);
} else {
printf("Administrative credentials unavailable: %s\n",
esp_err_to_name(username_error));
}
printf("Admission: shell/PTY only; exec, subsystem, forwarding, SCP, and SFTP disabled\n");
}
return print_sessions(&snapshot);
}
static int show_counters(void)
{
ssh_transport_snapshot_t snapshot;
esp_err_t error = ssh_transport_get_snapshot(&snapshot);
if (error != ESP_OK) {
printf("Could not read SSH counters: %s\n", esp_err_to_name(error));
return 1;
}
const ssh_transport_counters_t *counter = &snapshot.counters;
printf("Lifecycle: starts=%" PRIu64 " start-failures=%" PRIu64
" stops=%" PRIu64 " tcp-connect=%" PRIu64
" capacity-reject=%" PRIu64 "\n",
counter->starts, counter->start_failures, counter->stops,
counter->tcp_connections, counter->capacity_rejections);
printf("Handshake: success=%" PRIu64 " failures=%" PRIu64
" timeouts=%" PRIu64 " auth-attempts=%" PRIu64
" auth-failures=%" PRIu64 " request-rejects=%" PRIu64 "\n",
counter->handshake_successes, counter->handshake_failures,
counter->handshake_timeouts, counter->authentication_attempts,
counter->authentication_failures, counter->request_rejections);
printf("Broker: connect=%" PRIu64 " failures=%" PRIu64
" disconnect=%" PRIu64 " writer-requests=%" PRIu64
" grants=%" PRIu64 " denials=%" PRIu64
" revocations=%" PRIu64 "\n",
counter->broker_connections, counter->broker_failures,
counter->disconnections, counter->writer_requests,
counter->writer_grants, counter->writer_denials,
counter->writer_revocations);
printf("Stream: rx=%" PRIu64 " accepted=%" PRIu64
" rejected=%" PRIu64 " tx=%" PRIu64
" io-failures=%" PRIu64 " session-revocations=%" PRIu64 "\n",
counter->rx_bytes, counter->rx_accepted_bytes,
counter->rx_rejected_bytes, counter->tx_bytes,
counter->io_failures, counter->session_revocations);
return 0;
}
static int show_host_key(void)
{
ssh_security_metadata_t metadata;
esp_err_t error = ssh_security_get_metadata(&metadata);
if (error != ESP_OK) {
printf("Could not read SSH host-key information: %s\n", esp_err_to_name(error));
printf("Use 'ssh reset --force' to replace incompatible or corrupt material.\n");
return 1;
}
unsigned char encoded[48] = {0};
size_t encoded_length = 0U;
int result = mbedtls_base64_encode(encoded, sizeof(encoded), &encoded_length,
metadata.sha256_fingerprint,
sizeof(metadata.sha256_fingerprint));
if (result != 0 || encoded_length >= sizeof(encoded)) {
secure_wipe(encoded, sizeof(encoded));
printf("Could not encode SSH host-key fingerprint.\n");
return 1;
}
while (encoded_length > 0U && encoded[encoded_length - 1U] == '=') {
--encoded_length;
}
encoded[encoded_length] = '\0';
printf("SSH host key: generation=%" PRIu32 " type=%s curve=%s\n",
metadata.generation, SSH_SECURITY_KEY_TYPE, SSH_SECURITY_CURVE_NAME);
printf("OpenSSH SHA-256 fingerprint: SHA256:%s\n", encoded);
secure_wipe(encoded, sizeof(encoded));
return 0;
}
static bool parse_session_id(const char *text, uint32_t *session_id)
{
if (text == NULL || text[0] == '\0' || session_id == NULL) {
return false;
}
errno = 0;
char *end = NULL;
unsigned long value = strtoul(text, &end, 10);
if (errno != 0 || end == text || *end != '\0' || value == 0UL ||
value > UINT32_MAX) {
return false;
}
*session_id = (uint32_t)value;
return true;
}
static int replace_host_key(bool reset)
{
ssh_security_metadata_t before = {0};
bool had_before = ssh_security_get_metadata(&before) == ESP_OK;
esp_err_t error = ssh_transport_replace_host_key(reset);
ssh_security_metadata_t after = {0};
bool have_after = ssh_security_get_metadata(&after) == ESP_OK;
bool replaced = have_after && (!had_before || after.generation != before.generation);
if (error != ESP_OK) {
if (replaced) {
printf("SSH host key was persisted, but the transport could not complete its restart: %s\n",
esp_err_to_name(error));
} else {
printf("Could not %s SSH host key: %s\n",
reset ? "reset" : "rotate", esp_err_to_name(error));
}
return 1;
}
printf("SSH host key replaced and persisted; existing clients must verify the new fingerprint.\n");
return show_host_key();
}
static bool force_is_present(int argc, char **argv, int expected_argc)
{
return argc == expected_argc && strcmp(argv[expected_argc - 1], "--force") == 0;
}
static int command_ssh(int argc, char **argv)
{
if (argc == 1 || (argc == 2 && strcmp(argv[1], "help") == 0)) {
print_usage();
return 0;
}
if (argc == 2 && strcmp(argv[1], "status") == 0) {
return show_status(false);
}
if (argc == 2 && strcmp(argv[1], "sessions") == 0) {
return show_status(true);
}
if (argc == 2 && strcmp(argv[1], "start") == 0) {
esp_err_t error = ssh_transport_start();
if (error != ESP_OK) {
printf("Could not start SSH: %s\n", esp_err_to_name(error));
return 1;
}
printf("SSH started on TCP port %u.\n", SSH_TRANSPORT_PORT);
return 0;
}
if (argc == 2 && strcmp(argv[1], "stop") == 0) {
esp_err_t error = ssh_transport_stop();
if (error != ESP_OK) {
printf("Could not stop SSH: %s\n", esp_err_to_name(error));
return 1;
}
printf("SSH stopped.\n");
return 0;
}
if (argc == 2 && strcmp(argv[1], "counters") == 0) {
return show_counters();
}
if (argc == 2 && strcmp(argv[1], "clear-counters") == 0) {
esp_err_t error = ssh_transport_clear_counters();
if (error != ESP_OK) {
printf("Could not clear SSH counters: %s\n", esp_err_to_name(error));
return 1;
}
printf("SSH counters cleared.\n");
return 0;
}
if (argc == 3 && strcmp(argv[1], "disconnect") == 0) {
uint32_t session_id = 0U;
if (!parse_session_id(argv[2], &session_id)) {
printf("Session ID must be a nonzero decimal integer.\n");
return 1;
}
esp_err_t error = ssh_transport_disconnect(session_id);
if (error != ESP_OK) {
printf("Could not disconnect SSH session: %s\n", esp_err_to_name(error));
return 1;
}
printf("SSH session %" PRIu32 " scheduled for disconnect.\n", session_id);
return 0;
}
if (argc == 3 && strcmp(argv[1], "host-key") == 0 &&
strcmp(argv[2], "info") == 0) {
return show_host_key();
}
if (argc >= 3 && strcmp(argv[1], "host-key") == 0 &&
strcmp(argv[2], "rotate") == 0) {
if (!force_is_present(argc, argv, 4)) {
printf("Host-key rotation requires: ssh host-key rotate --force\n");
return 1;
}
return replace_host_key(false);
}
if (strcmp(argv[1], "reset") == 0) {
if (!force_is_present(argc, argv, 3)) {
printf("Host-key recovery requires: ssh reset --force\n");
return 1;
}
return replace_host_key(true);
}
print_usage();
return 1;
}
esp_err_t ssh_console_register_commands(void)
{
const esp_console_cmd_t command = {
.command = "ssh",
.help = "Manage authenticated SSH serial transport and host identity",
.hint = NULL,
.func = &command_ssh,
.argtable = NULL,
};
return esp_console_cmd_register(&command);
}
+16
View File
@@ -0,0 +1,16 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* UART0 administration commands for the SSH serial transport. */
#pragma once
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
esp_err_t ssh_console_register_commands(void);
#ifdef __cplusplus
}
#endif
+470
View File
@@ -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;
}
+47
View File
@@ -0,0 +1,47 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Persistent SSH host identity, separate from the HTTPS certificate key. */
#pragma once
#include <stddef.h>
#include <stdint.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
#define SSH_SECURITY_NVS_NAMESPACE "ssh_sec"
#define SSH_SECURITY_NVS_BLOB_KEY "material"
#define SSH_SECURITY_PRIVATE_KEY_DER_CAPACITY 256U
#define SSH_SECURITY_SHA256_LENGTH 32U
#define SSH_SECURITY_KEY_TYPE "ecdsa-sha2-nistp256"
#define SSH_SECURITY_CURVE_NAME "nistp256"
typedef enum {
SSH_SECURITY_LOAD_STORED = 0,
SSH_SECURITY_LOAD_GENERATED_MISSING = 1,
} ssh_security_load_result_t;
typedef struct {
uint32_t generation;
uint8_t sha256_fingerprint[SSH_SECURITY_SHA256_LENGTH];
} ssh_security_metadata_t;
/* NVS and secure_random must be ready. Existing malformed material is not replaced. */
esp_err_t ssh_security_init(ssh_security_load_result_t *load_result);
/* Query with output NULL/capacity zero; the required length is always returned. */
esp_err_t ssh_security_copy_private_key(uint8_t *output, size_t capacity,
size_t *output_length);
esp_err_t ssh_security_get_metadata(ssh_security_metadata_t *metadata);
/* Caller must stop SSH first. Rotation requires valid live material; reset replaces any stored state. */
esp_err_t ssh_security_rotate(void);
esp_err_t ssh_security_reset(void);
#ifdef __cplusplus
}
#endif
+1153
View File
File diff suppressed because it is too large Load Diff
+99
View File
@@ -0,0 +1,99 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Authenticated, bounded wolfSSH transport for the serial session broker. */
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "esp_err.h"
#include "session_broker.h"
#ifdef __cplusplus
extern "C" {
#endif
#define SSH_TRANSPORT_PORT 22U
#define SSH_TRANSPORT_MAX_SESSIONS 2U
#define SSH_TRANSPORT_IO_BUFFER_SIZE 512U
#define SSH_TRANSPORT_HANDSHAKE_TIMEOUT_SECONDS 15U
typedef enum {
SSH_TRANSPORT_SESSION_FREE = 0,
SSH_TRANSPORT_SESSION_HANDSHAKE,
SSH_TRANSPORT_SESSION_ACTIVE,
SSH_TRANSPORT_SESSION_CLOSING,
} ssh_transport_session_state_t;
typedef struct {
uint64_t starts;
uint64_t start_failures;
uint64_t stops;
uint64_t tcp_connections;
uint64_t capacity_rejections;
uint64_t handshake_successes;
uint64_t handshake_failures;
uint64_t handshake_timeouts;
uint64_t authentication_attempts;
uint64_t authentication_failures;
uint64_t request_rejections;
uint64_t broker_connections;
uint64_t broker_failures;
uint64_t disconnections;
uint64_t writer_requests;
uint64_t writer_grants;
uint64_t writer_denials;
uint64_t writer_revocations;
uint64_t rx_bytes;
uint64_t rx_accepted_bytes;
uint64_t rx_rejected_bytes;
uint64_t tx_bytes;
uint64_t io_failures;
uint64_t session_revocations;
} ssh_transport_counters_t;
typedef struct {
bool active;
bool authenticated;
bool writer;
bool close_requested;
bool rx_pending;
bool tx_pending;
uint32_t session_id;
uint32_t generation;
int socket_fd;
session_broker_client_id_t broker_client_id;
ssh_transport_session_state_t state;
char peer[48];
} ssh_transport_session_snapshot_t;
typedef struct {
bool initialized;
bool running;
bool transitioning;
uint16_t port;
esp_err_t last_error;
uint32_t active_sessions;
ssh_transport_session_snapshot_t sessions[SSH_TRANSPORT_MAX_SESSIONS];
ssh_transport_counters_t counters;
} ssh_transport_snapshot_t;
/* Installs wolfCrypt RNG/PSRAM hooks and starts the sole wolfSSH owner task. */
esp_err_t ssh_transport_init(void);
esp_err_t ssh_transport_start(void);
esp_err_t ssh_transport_stop(void);
/* Serialize stop, persistent host-key replacement, and conditional restart. */
esp_err_t ssh_transport_replace_host_key(bool reset);
esp_err_t ssh_transport_get_snapshot(ssh_transport_snapshot_t *snapshot);
esp_err_t ssh_transport_clear_counters(void);
/* Close one transport session or all authenticated/handshaking sessions. */
esp_err_t ssh_transport_disconnect(uint32_t session_id);
esp_err_t ssh_transport_revoke_sessions(void);
#ifdef __cplusplus
}
#endif
+24 -8
View File
@@ -1,5 +1,5 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* UART0 HTTPS lifecycle, credentials, certificate, and recovery commands. */
/* UART0 HTTPS lifecycle, shared credentials, certificate, and recovery commands. */
#include "web_console.h"
@@ -9,6 +9,7 @@
#include "esp_console.h"
#include "secure_random.h"
#include "ssh_transport.h"
#include "web_security.h"
#include "web_serial_transport.h"
#include "web_server.h"
@@ -52,7 +53,7 @@ static int show_status(void)
(unsigned int)snapshot.port,
esp_err_to_name(snapshot.last_error));
if (security_error == ESP_OK) {
printf("Authentication: HTTP Basic over TLS, username=%.*s, material=ready\n",
printf("Authentication: HTTP Basic over TLS, username=%.*s, shared with SSH\n",
(int)username_length, username);
} else {
printf("Authentication material unavailable: %s; use 'web reset --force' to replace it.\n",
@@ -167,7 +168,7 @@ static int show_credentials(void)
credentials.username);
printf("Password: %.*s\n", (int)credentials.password_length,
credentials.password);
printf("These credentials protect HTTPS only. Keep them private.\n");
printf("These credentials protect HTTPS and SSH. Keep them private.\n");
secure_wipe(&credentials, sizeof(credentials));
return 0;
}
@@ -231,11 +232,16 @@ static int rotate_credentials(void)
return 1;
}
esp_err_t revoke_error = web_serial_transport_revoke_sessions();
printf("Web credentials rotated and persisted. Existing Basic credentials are now invalid.\n");
if (revoke_error != ESP_OK && revoke_error != ESP_ERR_INVALID_STATE) {
esp_err_t web_revoke_error = web_serial_transport_revoke_sessions();
esp_err_t ssh_revoke_error = ssh_transport_revoke_sessions();
printf("Administrative credentials rotated and persisted. Existing HTTPS and SSH credentials are now invalid.\n");
if (web_revoke_error != ESP_OK && web_revoke_error != ESP_ERR_INVALID_STATE) {
printf("Warning: existing WebSocket sessions could not be revoked: %s\n",
esp_err_to_name(revoke_error));
esp_err_to_name(web_revoke_error));
}
if (ssh_revoke_error != ESP_OK && ssh_revoke_error != ESP_ERR_INVALID_STATE) {
printf("Warning: existing SSH sessions could not be revoked: %s\n",
esp_err_to_name(ssh_revoke_error));
}
printf("Username: %.*s\nPassword: %.*s\n",
(int)credentials.username_length, credentials.username,
@@ -272,7 +278,17 @@ static int reset_material(void)
return 1;
}
printf("Web credentials, certificate, and private key replaced and persisted.\n");
esp_err_t web_revoke_error = web_serial_transport_revoke_sessions();
esp_err_t ssh_revoke_error = ssh_transport_revoke_sessions();
printf("Administrative credentials, HTTPS certificate, and HTTPS private key replaced and persisted.\n");
if (web_revoke_error != ESP_OK && web_revoke_error != ESP_ERR_INVALID_STATE) {
printf("Warning: existing WebSocket sessions could not be revoked: %s\n",
esp_err_to_name(web_revoke_error));
}
if (ssh_revoke_error != ESP_OK && ssh_revoke_error != ESP_ERR_INVALID_STATE) {
printf("Warning: existing SSH sessions could not be revoked: %s\n",
esp_err_to_name(ssh_revoke_error));
}
printf("Username: %.*s\nPassword: %.*s\n",
(int)credentials.username_length, credentials.username,
(int)credentials.password_length, credentials.password);
+13 -2
View File
@@ -1,5 +1,5 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Canonical NVS storage for HTTPS identity and administrative credentials. */
/* Canonical NVS storage for HTTPS identity and shared admin credentials. */
#include "web_security.h"
@@ -753,7 +753,7 @@ static esp_err_t credential_digest(const uint8_t *username, size_t username_leng
return result == 0 ? ESP_OK : ESP_FAIL;
}
esp_err_t web_security_authenticate_basic(const uint8_t *username,
esp_err_t web_security_authenticate_admin(const uint8_t *username,
size_t username_length,
const uint8_t *password,
size_t password_length,
@@ -800,6 +800,17 @@ esp_err_t web_security_authenticate_basic(const uint8_t *username,
return error;
}
esp_err_t web_security_authenticate_basic(const uint8_t *username,
size_t username_length,
const uint8_t *password,
size_t password_length,
bool *authenticated)
{
return web_security_authenticate_admin(username, username_length,
password, password_length,
authenticated);
}
static void copy_credentials_locked(web_security_credentials_t *credentials,
const web_security_blob_t *blob)
{
+9 -2
View File
@@ -1,5 +1,5 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Persistent HTTPS identity and administrative Basic credentials. */
/* Persistent HTTPS identity and shared network-administration credentials. */
#pragma once
@@ -75,7 +75,14 @@ esp_err_t web_security_copy_tls_material(
esp_err_t web_security_copy_username(char *output, size_t capacity,
size_t *output_length);
/* Input fields are decoded HTTP Basic components, not the base64 header text. */
/* Protocol-neutral authentication for the shared HTTPS and SSH administrator. */
esp_err_t web_security_authenticate_admin(const uint8_t *username,
size_t username_length,
const uint8_t *password,
size_t password_length,
bool *authenticated);
/* Compatibility name for decoded HTTP Basic components. */
esp_err_t web_security_authenticate_basic(const uint8_t *username,
size_t username_length,
const uint8_t *password,