Implemented initial SSH support. Memory pressure too high for HTTPS and
SSH. Dirty commit.
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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. */
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user