Add SSH host identity rotation controls
This commit is contained in:
+45
-16
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user