Files
ESP32_Serial_Swiss_Army_Knife/src/web_console.c
T

426 lines
16 KiB
C

/* SPDX-License-Identifier: GPL-3.0-only */
/* UART0 HTTPS lifecycle, legacy recovery credential, and certificate commands. */
#include "web_console.h"
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include "esp_console.h"
#include "secure_random.h"
#include "ssh_transport.h"
#include "user_database.h"
#include "web_security.h"
#include "web_serial_transport.h"
#include "web_server.h"
static void print_usage(void)
{
printf("Usage:\n");
printf(" web status|start|stop\n");
printf(" web counters|clear-counters\n");
printf(" web credentials show\n");
printf(" web credentials rotate --force\n");
printf(" web certificate info\n");
printf(" web certificate rotate --force\n");
printf(" web reset --force\n");
}
static void print_fingerprint(const uint8_t fingerprint[WEB_SECURITY_SHA256_LENGTH])
{
for (size_t index = 0U; index < WEB_SECURITY_SHA256_LENGTH; ++index) {
printf(index == 0U ? "%02X" : ":%02X", (unsigned int)fingerprint[index]);
}
}
static int show_status(void)
{
web_server_snapshot_t snapshot;
esp_err_t error = web_server_get_snapshot(&snapshot);
if (error != ESP_OK) {
printf("HTTPS runtime unavailable: %s\n", esp_err_to_name(error));
return 1;
}
user_database_snapshot_t users;
esp_err_t users_error = user_database_get_snapshot(&users);
printf("HTTPS: 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 (users_error == ESP_OK) {
printf("Authentication: HTTP Basic over TLS via user database, users=%u admins=%u\n",
(unsigned int)users.user_count, (unsigned int)users.admin_count);
} else {
printf("Authentication database unavailable: %s; use 'user recover --force'.\n",
esp_err_to_name(users_error));
}
printf("Endpoints: GET /, GET /api/status, POST /api/ws-ticket, WSS /ws/serial\n");
web_serial_transport_snapshot_t transport;
esp_err_t transport_error = web_serial_transport_get_snapshot(&transport);
if (transport_error != ESP_OK) {
printf("WebSocket serial transport unavailable: %s\n",
esp_err_to_name(snapshot.serial_transport_error));
return 0;
}
printf("WebSocket serial: attached=%s sessions=%" PRIu32 "/%u tickets=%" PRIu32 "\n",
transport.server_attached ? "yes" : "no",
transport.active_sessions,
WEB_SERIAL_TRANSPORT_MAX_SESSIONS,
transport.active_tickets);
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_SESSIONS; ++index) {
const web_serial_transport_session_snapshot_t *session =
&transport.sessions[index];
if (!session->active) {
continue;
}
printf(" slot=%u fd=%d generation=%" PRIu32 " account=%s user-role=%s"
" method=%s broker=%" PRIu32
" broker-role=%s tx-pending=%s closing=%s\n",
(unsigned int)index,
session->socket_fd,
session->generation,
session->principal_valid ? session->username : "-",
session->principal_valid
? user_role_to_string(session->user_role)
: "-",
session->principal_valid &&
session->auth_method == USER_AUTH_METHOD_PASSWORD
? "password"
: "unknown",
session->broker_client_id,
session->writer ? "writer" : "observer",
session->tx_pending ? "yes" : "no",
session->close_requested ? "yes" : "no");
}
return 0;
}
static int show_counters(void)
{
web_server_snapshot_t snapshot;
esp_err_t error = web_server_get_snapshot(&snapshot);
if (error != ESP_OK) {
printf("Could not read HTTPS counters: %s\n", esp_err_to_name(error));
return 1;
}
const web_server_counters_t *counter = &snapshot.counters;
printf("Lifecycle: starts=%" PRIu64 " start-failures=%" PRIu64
" stops=%" PRIu64 "\n",
counter->starts, counter->start_failures, counter->stops);
printf("Requests: total=%" PRIu64 " authenticated=%" PRIu64
" auth-failures=%" PRIu64 " root=%" PRIu64
" status=%" PRIu64 " tickets=%" PRIu64 " assets=%" PRIu64
" response-errors=%" PRIu64 "\n",
counter->requests, counter->authenticated_requests,
counter->authentication_failures, counter->root_requests,
counter->status_requests, counter->ticket_requests,
counter->asset_requests, counter->response_errors);
web_serial_transport_snapshot_t transport;
error = web_serial_transport_get_snapshot(&transport);
if (error != ESP_OK) {
printf("WebSocket serial counters unavailable: %s\n", esp_err_to_name(error));
return 0;
}
const web_serial_transport_counters_t *websocket = &transport.counters;
printf("Tickets: issued=%" PRIu64 " consumed=%" PRIu64
" rejected=%" PRIu64 " expired=%" PRIu64 "\n",
websocket->tickets_issued, websocket->tickets_consumed,
websocket->tickets_rejected, websocket->tickets_expired);
printf("WebSocket sessions: connect=%" PRIu64 " failures=%" PRIu64
" disconnect=%" PRIu64 " service-start-failures=%" PRIu64
" broker-failures=%" PRIu64 "\n",
websocket->connections, websocket->connection_failures,
websocket->disconnections, websocket->service_start_failures,
websocket->broker_failures);
printf("WebSocket RX: frames-ok=%" PRIu64 " frames-rejected=%" PRIu64
" bytes-ok=%" PRIu64 " bytes-rejected=%" PRIu64 "\n",
websocket->rx_ws_frames_accepted,
websocket->rx_ws_frames_rejected,
websocket->rx_ws_bytes_accepted,
websocket->rx_ws_bytes_rejected);
printf("WebSocket TX: binary-frames=%" PRIu64 " binary-bytes=%" PRIu64
" control-frames=%" PRIu64 " control-bytes=%" PRIu64 "\n",
websocket->tx_binary_frames, websocket->tx_binary_bytes,
websocket->tx_control_frames, websocket->tx_control_bytes);
printf("WebSocket control: writer-requests=%" PRIu64
" grants=%" PRIu64 " denials=%" PRIu64
" releases=%" PRIu64 " revocations=%" PRIu64 "\n",
websocket->writer_requests, websocket->writer_grants,
websocket->writer_denials, websocket->writer_releases,
websocket->writer_revocations);
printf("WebSocket failures: send=%" PRIu64 " queue=%" PRIu64
" protocol=%" PRIu64 " closes=%" PRIu64 "\n",
websocket->send_failures, websocket->queue_failures,
websocket->protocol_errors, websocket->close_requests);
return 0;
}
static int show_credentials(void)
{
web_security_credentials_t credentials;
esp_err_t error = web_security_show_credentials(&credentials);
if (error != ESP_OK) {
printf("Could not read web credentials: %s\n", esp_err_to_name(error));
return 1;
}
printf("Username: %.*s\n", (int)credentials.username_length,
credentials.username);
printf("Password: %.*s\n", (int)credentials.password_length,
credentials.password);
printf("Phase 8B uses the user database for HTTPS and SSH authentication.\n");
printf("This legacy credential is retained only for migration and physical recovery.\n");
secure_wipe(&credentials, sizeof(credentials));
return 0;
}
static int show_certificate(void)
{
web_security_certificate_metadata_t metadata;
esp_err_t error = web_security_get_certificate_metadata(&metadata);
if (error != ESP_OK) {
printf("Could not read certificate information: %s\n", esp_err_to_name(error));
return 1;
}
printf("Security material generation=%" PRIu32 " type=ECDSA-P256 signature=SHA-256\n",
metadata.material_generation);
printf("Subject/issuer CN: %s\n", metadata.common_name);
printf("Validity: %sZ to %sZ\n", metadata.not_before, metadata.not_after);
printf("SAN: DNS:%s, IP:%u.%u.%u.%u\n",
metadata.dns_name,
(unsigned int)metadata.ipv4_address[0],
(unsigned int)metadata.ipv4_address[1],
(unsigned int)metadata.ipv4_address[2],
(unsigned int)metadata.ipv4_address[3]);
printf("SHA-256 fingerprint: ");
print_fingerprint(metadata.sha256_fingerprint);
putchar('\n');
return 0;
}
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 restart_if_running(bool was_running)
{
if (!was_running) {
return 0;
}
esp_err_t error = web_server_stop();
if (error != ESP_OK) {
printf("Material changed, but the old TLS server could not stop: %s\n",
esp_err_to_name(error));
return 1;
}
error = web_server_start();
if (error != ESP_OK) {
printf("Material changed, but HTTPS could not restart: %s\n",
esp_err_to_name(error));
return 1;
}
return 0;
}
static void synchronize_migrated_user(
const web_security_credentials_t *credentials)
{
const user_database_legacy_credentials_t legacy = {
.username = (const uint8_t *)credentials->username,
.username_length = credentials->username_length,
.password = (const uint8_t *)credentials->password,
.password_length = credentials->password_length,
};
bool synchronized = false;
esp_err_t error = user_database_sync_legacy_credentials(&legacy, &synchronized);
if (error != ESP_OK) {
printf("Warning: migrated user synchronization failed: %s. Boot will retry a valid stored database; otherwise use 'user recover --force'.\n",
esp_err_to_name(error));
return;
}
if (synchronized) {
(void)web_serial_transport_revoke_user(
(const uint8_t *)credentials->username,
credentials->username_length);
(void)ssh_transport_revoke_user(
(const uint8_t *)credentials->username,
credentials->username_length);
printf("The pre-bootstrap migrated user credential was synchronized.\n");
return;
}
user_database_snapshot_t snapshot;
if (user_database_get_snapshot(&snapshot) == ESP_OK &&
snapshot.admin_bootstrapped) {
printf("This legacy recovery credential is separate from role-based user passwords.\n");
} else {
printf("Warning: no matching pre-bootstrap migrated user was synchronized; establish an administrator with 'user bootstrap'.\n");
}
}
static int rotate_credentials(void)
{
web_security_credentials_t credentials;
esp_err_t error = web_security_rotate_credentials(&credentials);
if (error != ESP_OK) {
printf("Could not rotate web credentials: %s\n", esp_err_to_name(error));
return 1;
}
synchronize_migrated_user(&credentials);
printf("Legacy migration/recovery credential rotated and persisted.\n");
printf("Username: %.*s\nPassword: %.*s\n",
(int)credentials.username_length, credentials.username,
(int)credentials.password_length, credentials.password);
secure_wipe(&credentials, sizeof(credentials));
return 0;
}
static int rotate_certificate(void)
{
web_server_snapshot_t snapshot;
esp_err_t error = web_server_get_snapshot(&snapshot);
if (error != ESP_OK) {
printf("Could not inspect HTTPS runtime: %s\n", esp_err_to_name(error));
return 1;
}
error = web_security_rotate_certificate();
if (error != ESP_OK) {
printf("Could not rotate web certificate: %s\n", esp_err_to_name(error));
return 1;
}
printf("Web certificate and private key rotated and persisted.\n");
return restart_if_running(snapshot.running);
}
static int reset_material(void)
{
web_server_snapshot_t snapshot;
bool was_running = web_server_get_snapshot(&snapshot) == ESP_OK && snapshot.running;
web_security_credentials_t credentials;
esp_err_t error = web_security_reset_all(&credentials);
if (error != ESP_OK) {
printf("Could not reset web security material: %s\n", esp_err_to_name(error));
return 1;
}
synchronize_migrated_user(&credentials);
printf("Legacy recovery credential, HTTPS certificate, and HTTPS private key replaced and persisted.\n");
printf("Username: %.*s\nPassword: %.*s\n",
(int)credentials.username_length, credentials.username,
(int)credentials.password_length, credentials.password);
secure_wipe(&credentials, sizeof(credentials));
if (was_running) {
return restart_if_running(true);
}
error = web_server_start();
if (error != ESP_OK) {
printf("Security material recovered, but HTTPS could not start: %s\n",
esp_err_to_name(error));
return 1;
}
printf("HTTPS started with the recovered security material.\n");
return 0;
}
static int command_web(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();
}
if (argc == 2 && strcmp(argv[1], "start") == 0) {
esp_err_t error = web_server_start();
if (error != ESP_OK) {
printf("Could not start HTTPS: %s\n", esp_err_to_name(error));
return 1;
}
printf("HTTPS started on TCP port 443.\n");
return 0;
}
if (argc == 2 && strcmp(argv[1], "stop") == 0) {
esp_err_t error = web_server_stop();
if (error != ESP_OK) {
printf("Could not stop HTTPS: %s\n", esp_err_to_name(error));
return 1;
}
printf("HTTPS 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 = web_server_clear_counters();
if (error == ESP_OK) {
error = web_serial_transport_clear_counters();
}
if (error != ESP_OK) {
printf("Could not clear web counters: %s\n", esp_err_to_name(error));
return 1;
}
printf("HTTPS and WebSocket counters cleared.\n");
return 0;
}
if (argc == 3 && strcmp(argv[1], "credentials") == 0 &&
strcmp(argv[2], "show") == 0) {
return show_credentials();
}
if (strcmp(argv[1], "credentials") == 0 && argc >= 3 &&
strcmp(argv[2], "rotate") == 0) {
if (!force_is_present(argc, argv, 4)) {
printf("Credential rotation requires: web credentials rotate --force\n");
return 1;
}
return rotate_credentials();
}
if (argc == 3 && strcmp(argv[1], "certificate") == 0 &&
strcmp(argv[2], "info") == 0) {
return show_certificate();
}
if (strcmp(argv[1], "certificate") == 0 && argc >= 3 &&
strcmp(argv[2], "rotate") == 0) {
if (!force_is_present(argc, argv, 4)) {
printf("Certificate rotation requires: web certificate rotate --force\n");
return 1;
}
return rotate_certificate();
}
if (strcmp(argv[1], "reset") == 0) {
if (!force_is_present(argc, argv, 3)) {
printf("Full material replacement requires: web reset --force\n");
return 1;
}
return reset_material();
}
print_usage();
return 1;
}
esp_err_t web_console_register_commands(void)
{
const esp_console_cmd_t command = {
.command = "web",
.help = "Manage authenticated HTTPS and recover web credentials/certificate",
.hint = NULL,
.func = &command_web,
.argtable = NULL,
};
return esp_console_cmd_register(&command);
}