/* SPDX-License-Identifier: GPL-3.0-only */ /* UART0 HTTPS lifecycle, legacy recovery credential, and certificate commands. */ #include "web_console.h" #include #include #include #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); }