Add SSH host identity rotation controls

This commit is contained in:
2026-09-13 19:58:05 +02:00
parent aa4bbc2c8c
commit 8df1d2218b
19 changed files with 626 additions and 107 deletions
+45 -16
View File
@@ -9,54 +9,58 @@
#include "freertos/FreeRTOS.h"
#include "secure_random.h"
#include "ssh_transport.h"
#include "ssh_security.h"
#include "mbedtls/base64.h"
#include "web_cookie_auth.h"
#include "web_httpd_adapter.h"
enum { IDLE, PENDING, OK, FAILED, CANCELLED, CONFLICT };
static const char *const s_states[] = {"idle", "pending", "ok", "failed", "cancelled", "conflict"};
static const char *const s_actions[] = {"start", "stop", "disconnect"};
enum { ROTATE = 3 };
static const char *const s_actions[] = {"start", "stop", "disconnect", "rotate"};
typedef struct {
uint32_t id;
web_session_id_t session;
user_principal_t principal;
int64_t deadline;
uint32_t generation, target;
ssh_transport_management_action_t action;
uint32_t generation, target, identity_generation;
unsigned action;
unsigned state;
} ssh_operation_t;
static portMUX_TYPE s_lock = portMUX_INITIALIZER_UNLOCKED;
static ssh_operation_t s_operation;
static uint32_t s_next_id;
/* Exact three-field flat JSON; no escapes, duplicates, extra fields or coercion. */
/* Three fields for service actions; rotation additionally requires identity_generation.
* No escapes, duplicates, extra fields or coercion. */
static bool parse(const char *body, size_t length, ssh_operation_t *operation)
{
const char *keys[] = {"action", "generation", "target"};
const char *keys[] = {"action", "generation", "target", "identity_generation"};
unsigned seen = 0;
size_t pos = 0;
#define SPACE() while (pos < length && (body[pos] == ' ' || body[pos] == '\t' || body[pos] == '\r' || body[pos] == '\n')) ++pos
#define TAKE(c) do { SPACE(); if (pos == length || body[pos++] != (c)) return false; } while (0)
TAKE('{');
for (unsigned field = 0; field < 3; ++field) {
for (unsigned field = 0; field < 4; ++field) {
if (field) { TAKE(','); }
TAKE('"');
size_t start = pos;
while (pos < length && body[pos] != '"') ++pos;
if (pos == length) return false;
unsigned key = 0;
for (; key < 3; ++key)
for (; key < 4; ++key)
if (strlen(keys[key]) == pos - start && !memcmp(body + start, keys[key], pos - start)) break;
if (key == 3 || (seen & (1U << key))) return false;
if (key == 4 || (seen & (1U << key))) return false;
++pos; TAKE(':'); SPACE();
if (key == 0) {
TAKE('"'); start = pos;
while (pos < length && body[pos] != '"') ++pos;
if (pos == length) return false;
unsigned action = 0;
for (; action < 3; ++action)
for (; action < 4; ++action)
if (strlen(s_actions[action]) == pos - start && !memcmp(body + start, s_actions[action], pos - start)) break;
if (action == 3) return false;
operation->action = (ssh_transport_management_action_t)action;
if (action == 4) return false;
operation->action = action;
++pos;
} else {
uint32_t number = 0;
@@ -68,14 +72,19 @@ static bool parse(const char *body, size_t length, ssh_operation_t *operation)
}
if (pos == start || (pos - start > 1 && body[start] == '0')) return false;
if (key == 1) operation->generation = number;
else operation->target = number;
else if (key == 2) operation->target = number;
else operation->identity_generation = number;
}
seen |= 1U << key;
SPACE();
if (pos < length && body[pos] == '}') break;
}
TAKE('}'); SPACE();
#undef TAKE
#undef SPACE
return pos == length && seen == 7 && operation->generation &&
return pos == length &&
(operation->action == ROTATE ? seen == 15 && operation->identity_generation &&
operation->identity_generation != UINT32_MAX : seen == 7) && operation->generation &&
operation->generation != UINT32_MAX &&
((operation->action == SSH_TRANSPORT_MANAGE_DISCONNECT) == (operation->target != 0U));
}
@@ -95,8 +104,12 @@ void web_ssh_settings_execute(uint32_t id)
unsigned state = CANCELLED;
if (error == ESP_OK && current && operation.principal.role == USER_ROLE_ADMIN &&
esp_timer_get_time() < operation.deadline) {
error = ssh_transport_manage_current(operation.action, operation.target, operation.generation);
bool committed = false;
error = operation.action == ROTATE
? ssh_transport_replace_identity(operation.generation, operation.identity_generation, false, &committed)
: ssh_transport_manage_current(operation.action, operation.target, operation.generation);
state = error == ESP_OK ? OK :
(operation.action == ROTATE) ? FAILED :
(error == ESP_ERR_INVALID_STATE || error == ESP_ERR_NOT_FOUND) ? CONFLICT : FAILED;
}
taskENTER_CRITICAL(&s_lock);
@@ -207,10 +220,26 @@ esp_err_t web_ssh_settings_handler(httpd_req_t *request)
error = respond(request, "503 Service Unavailable", "{\"error\":\"ssh_unavailable\"}");
goto done;
}
ssh_security_identity_snapshot_t identity = {0};
unsigned char fingerprint[48] = {0};
size_t fingerprint_length = 0;
bool have_identity = ssh_security_get_identity_snapshot(&identity) == ESP_OK;
if (have_identity && mbedtls_base64_encode(fingerprint, sizeof(fingerprint), &fingerprint_length,
identity.metadata.sha256_fingerprint, sizeof(identity.metadata.sha256_fingerprint)) != 0) {
error = ESP_FAIL;
goto done;
}
while (fingerprint_length && fingerprint[fingerprint_length - 1] == '=') --fingerprint_length;
fingerprint[fingerprint_length] = 0;
char response[768];
int written = snprintf(response, sizeof(response),
"{\"generation\":%" PRIu32 ",\"running\":%s,\"transitioning\":%s,\"sessions\":[",
snapshot.generation, snapshot.running ? "true" : "false", snapshot.transitioning ? "true" : "false");
"{\"generation\":%" PRIu32 ",\"running\":%s,\"transitioning\":%s,"
"\"identity_generation\":%" PRIu32 ",\"algorithm\":\"%s\",\"fingerprint\":\"%s%s\",\"rotatable\":%s,\"sessions\":[",
snapshot.generation, snapshot.running ? "true" : "false", snapshot.transitioning ? "true" : "false",
have_identity ? identity.metadata.generation : 0, SSH_SECURITY_KEY_TYPE,
have_identity ? "SHA256:" : "", fingerprint,
have_identity && !identity.busy && identity.metadata.generation != UINT32_MAX &&
!snapshot.transitioning && snapshot.generation != UINT32_MAX ? "true" : "false");
if (written < 0 || (size_t)written >= sizeof(response)) { error = ESP_FAIL; goto done; }
size_t used = (size_t)written;
unsigned count = 0;