Add HTTPS identity rotation support

This commit is contained in:
2026-09-13 18:21:37 +02:00
parent 36e80811e8
commit aa4bbc2c8c
24 changed files with 826 additions and 170 deletions
+35 -15
View File
@@ -11,6 +11,7 @@
#include "web_cookie_auth.h"
#include "web_httpd_adapter.h"
#include "web_server.h"
#include "web_security.h"
#if CONFIG_HTTPD_QUEUE_WORK_BLOCKING
#error "Lifecycle ACK handoff requires nonblocking HTTPD work submission"
@@ -18,9 +19,9 @@
enum { IDLE, PENDING, EXECUTING, OK, FAILED, CANCELLED };
static const char *const s_states[] = {"idle", "pending", "pending", "ok", "failed", "cancelled"};
static const char *const s_actions[] = {"stop", "restart", "reboot"};
static const char *const s_actions[] = {"stop", "restart", "reboot", "rotate"};
typedef struct {
uint32_t id, generation;
uint32_t id, generation, identity_generation;
web_session_id_t session;
user_principal_t principal;
int64_t ack_deadline, deadline;
@@ -46,33 +47,34 @@ static void expire_locked(int64_t now)
cancel_locked();
}
/* Exactly action + generation; no escapes, duplicates, coercions or extra fields. */
/* Exactly action + service generation, plus identity generation only for rotate.
* No escapes, duplicates, coercions or extra fields. */
static bool parse(const char *body, size_t length, lifecycle_operation_t *operation)
{
const char *keys[] = {"action", "generation"};
const char *keys[] = {"action", "generation", "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 < 2; ++field) {
for (unsigned field = 0; field < 3; ++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 < 2; ++key)
for (; key < 3; ++key)
if (strlen(keys[key]) == pos - start && !memcmp(body + start, keys[key], pos - start)) break;
if (key == 2 || (seen & (1U << key))) return false;
if (key == 3 || (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;
if (action == 4) return false;
operation->action = action; ++pos;
} else {
uint32_t number = 0; start = pos;
@@ -82,14 +84,19 @@ static bool parse(const char *body, size_t length, lifecycle_operation_t *operat
number = number * 10U + digit;
}
if (pos == start || (pos - start > 1 && body[start] == '0')) return false;
operation->generation = number;
if (key == 1) operation->generation = 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 == 3 && operation->generation && operation->generation != UINT32_MAX;
return pos == length && seen == (operation->action == 3 ? 7U : 3U) &&
operation->generation && operation->generation != UINT32_MAX &&
(operation->action != 3 || (operation->identity_generation && operation->identity_generation != UINT32_MAX));
}
/* Runs on HTTPD after its synchronous response handler returns. No socket IO,
@@ -143,9 +150,11 @@ void web_lifecycle_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) {
bool committed = false;
error = operation.action == 0 ? web_server_stop_current(operation.generation) :
operation.action == 1 ? web_server_restart_current(operation.generation) :
web_server_reboot_current(operation.generation);
operation.action == 2 ? web_server_reboot_current(operation.generation) :
web_server_replace_identity(operation.generation, operation.identity_generation, false, &committed);
/* Even INVALID_STATE can be a detach failure after stop admission. */
state = error == ESP_OK ? OK : FAILED;
}
@@ -250,11 +259,22 @@ esp_err_t web_lifecycle_settings_handler(httpd_req_t *request)
if (web_server_get_management_snapshot(&snapshot) != ESP_OK) {
error = respond(request, "503 Service Unavailable", "{\"error\":\"lifecycle_unavailable\"}"); goto done;
}
char response[128];
web_security_identity_snapshot_t identity = {0};
bool available = web_security_get_identity_snapshot(&identity) == ESP_OK;
char fingerprint[65] = {0};
if (available) {
for (size_t i = 0; i < sizeof(identity.fingerprint); ++i)
snprintf(fingerprint + i * 2, 3, "%02x", identity.fingerprint[i]);
}
char response[320];
int written = snprintf(response, sizeof(response),
"{\"generation\":%" PRIu32 ",\"running\":%s,\"transitioning\":%s,\"controllable\":%s}",
"{\"generation\":%" PRIu32 ",\"running\":%s,\"transitioning\":%s,\"controllable\":%s,"
"\"identity_generation\":%" PRIu32 ",\"fingerprint\":\"%s\",\"rotatable\":%s}",
snapshot.generation, snapshot.running ? "true" : "false",
snapshot.transitioning ? "true" : "false", snapshot.controllable ? "true" : "false");
snapshot.transitioning ? "true" : "false", snapshot.controllable ? "true" : "false",
available ? identity.generation : 0, fingerprint,
available && snapshot.controllable && !identity.busy && identity.generation &&
identity.generation != UINT32_MAX ? "true" : "false");
error = written < 0 || (size_t)written >= sizeof(response) ? ESP_FAIL : respond(request, "200 OK", response);
done:
secure_wipe(&view, sizeof(view));