Add role-based user database administration

This commit is contained in:
2026-08-30 00:56:18 +02:00
parent 0c7b763bec
commit cd235445c7
14 changed files with 2235 additions and 16 deletions
+4 -4
View File
@@ -14,7 +14,7 @@ ESP32-S3 firmware for a secure, multi-transport RS-232 adapter. It operates one
## Development status
Hardware characterization, the serial core, session broker, and native USB CDC-ACM are complete. The Wi-Fi foundation is implemented and substantially validated, with several smoke tests still pending. The project is currently in the **SSH-Phase**; later work includes interface hardware, security hardening, OTA, BLE, enterprise Wi-Fi, WireGuard evaluation, and optional storage features. See the [Roadmap](docs/roadmap.md) for validation details and roadmap.
Hardware characterization, the serial core, USB CDC-ACM, Wi-Fi, HTTPS/WebSocket, SSH serial transport, and the local display/control interface are implemented. Phase 8A adds the bounded role-based user database and physical UART0 user administration; target-hardware validation and the later HTTPS/SSH authentication cutover are still pending. See the [Roadmap](docs/roadmap.md) for phase status and validation details.
## Documentation
@@ -28,7 +28,7 @@ The N16R8 target has 16 MiB flash and 8 MiB octal PSRAM. PlatformIO uses the cus
| Partition | Offset | Size | Purpose |
|---|---:|---:|---|
| `nvs` | `0x009000` | 512 KiB | Serial, Wi-Fi, shared admin credential, HTTPS identity, and SSH host-key data |
| `nvs` | `0x009000` | 512 KiB | Serial, Wi-Fi, role-based users, legacy network credential, HTTPS identity, and SSH host-key data |
| `otadata` | `0x089000` | 8 KiB | Active OTA-slot selection metadata |
| `phy_init` | `0x08B000` | 4 KiB | Optional PHY initialization data |
| `nvs_key` | `0x08C000` | 4 KiB | Reserved for future encrypted-NVS keys |
@@ -72,11 +72,11 @@ The firmware provides an interactive UART0 console at `serial-tool>`. Run `help`
The console supports session history, line editing, cursor movement, and hierarchical Tab completion. After an unattended boot, attach an ANSI-capable terminal and press Enter once to enable enhanced editing; this avoids blocking while no terminal is attached.
Serial configuration and Wi-Fi edits remain in RAM until explicitly saved with `serial save` or `wifi save`. Retrieve generated web and SSH credentials only from the physical UART0 console with `web credentials show`.
Serial configuration and Wi-Fi edits remain in RAM until explicitly saved with `serial save` or `wifi save`. During Phase 8A, retrieve the legacy HTTPS/SSH network credential only from physical UART0 with `web credentials show`; manage the new role-based accounts with the `user` command group.
## Security notes
The HTTPS interface uses a device-specific self-signed certificate and HTTP Basic authentication over TLS; there is no plaintext HTTP or TCP serial listener. SSH supports password-authenticated interactive shell sessions only. The shared admin password, HTTPS private key, SSH private key, and Wi-Fi credentials are currently stored in plaintext application-owned NVS blobs. The reserved `nvs_key` partition does not enable encryption. Do not treat this firmware as resistant to physical flash or RAM extraction until the planned hardening work is complete.
The HTTPS interface uses a device-specific self-signed certificate and HTTP Basic authentication over TLS; there is no plaintext HTTP or TCP serial listener. During Phase 8A, HTTPS and SSH continue to use the legacy shared `admin` credential while role-aware password/public-key authentication remains scheduled for Phase 8B. New user passwords are stored as salted PBKDF2-HMAC-SHA256 verifiers, but the legacy network password, HTTPS private key, SSH private key, and Wi-Fi credentials remain recoverable from unencrypted application-owned NVS blobs. Offline password guessing and stale append-oriented flash copies also remain possible. The reserved `nvs_key` partition does not enable encryption. Do not treat this firmware as resistant to physical flash or RAM extraction until the planned hardening work is complete.
## License
+30 -5
View File
@@ -10,6 +10,31 @@ Use these commands from the UART0 `serial-tool>` administration console. Run `he
| `reboot` | Drain console output briefly and restart the ESP32. |
| `status` | Show quick MAX3243 signal state. |
## Role-based users (Phase 8A)
| Command | Description |
|---|---|
| `user status` / `user list` | Show database generation, capacity, administrator/bootstrap state, and all secret-free account summaries. |
| `user show <username>` | Show one account's role, ID, authentication generation, and SSH-key fingerprints. |
| `user bootstrap` | Set and confirm the `admin` password without echo, then promote the migrated account to `admin`. |
| `user bootstrap --generate` | Bootstrap `admin` with a generated 24-character password displayed once. |
| `user add <username> <user|admin>` | Create an account using a bounded no-echo password and confirmation prompt. |
| `user add <username> <user|admin> --generate` | Create an account with a generated password displayed once. |
| `user delete <username> --force` | Delete an account; the pre-bootstrap migrated `admin` and final administrator are protected. |
| `user role <username> <user|admin> --force` | Change a role; the final administrator cannot be demoted. |
| `user password <username>` | Set and confirm a new password without echo. |
| `user password <username> --generate` | Replace a password with a generated value displayed once. |
| `user key add <username>` | Prompt for one bounded OpenSSH public-key line. |
| `user key delete <username> <0..2> --force` | Delete one key by the index shown by `user show`. |
| `user key clear <username> --force` | Delete all public keys for an account. |
| `user recover --force` | When normal user-database initialization failed, explicitly replace its blob from the current legacy network credential. |
Usernames must match `[a-z][a-z0-9_-]{0,15}`. Passwords contain 1264 printable ASCII characters. The fixed database supports eight users and three SSH keys per user; initial key types are `ssh-ed25519` and `ecdsa-sha2-nistp256`. A key may belong to only one account. Password verifiers, salts, raw key blobs, and passwords are absent from ordinary status output. `Ctrl-C` cancels a password or key prompt, and generated passwords are shown once.
On the first Phase 8A boot, the existing shared `admin` HTTPS/SSH credential is imported as a role-`user` account, not silently granted administrator rights. Run `user bootstrap` from physical UART0 to establish the administrator. Before bootstrap, `web credentials rotate --force` and `web reset --force` synchronize the migrated verifier; reboot reconciliation retries an interrupted synchronization. After bootstrap, the legacy HTTPS/SSH credential and user-database passwords are intentionally separate until Phase 8B performs the authentication cutover. SSH public-key login is likewise enabled in Phase 8B, not Phase 8A.
`user recover --force` is a destructive physical recovery operation and succeeds only while the database is unavailable. It replaces the user blob with one role-`user` account derived from the current legacy credential; run `user bootstrap` afterward. It does not erase unrelated NVS data.
## Local display
| Command | Description |
@@ -90,13 +115,13 @@ Opening `/dev/ttyACM*` with DTR asserted creates the `usb-cdc` broker client, st
| `web status` | Show HTTPS and WebSocket state. |
| `web start` / `web stop` | Start or stop HTTPS service. |
| `web counters` / `web clear-counters` | Show or clear web counters. |
| `web credentials show` | Display the physical-console-only shared admin credentials. |
| `web credentials rotate --force` | Replace credentials and disconnect web and SSH sessions. |
| `web credentials show` | Display the physical-console-only legacy Phase 8A network credential. |
| `web credentials rotate --force` | Replace the legacy credential, apply pre-bootstrap migration synchronization, and disconnect web and SSH sessions. |
| `web certificate info` | Display certificate identity and fingerprint. |
| `web certificate rotate --force` | Replace the HTTPS certificate and private key. |
| `web reset --force` | Explicitly replace missing, incompatible, or damaged web material. |
| `web reset --force` | Explicitly replace missing, incompatible, or damaged legacy credentials and web material. |
HTTPS listens on port 443 only. Authenticate to `https://<device-address>/` as `admin` with the credential shown on UART0. The device serves its vendored xterm.js terminal without Internet access. Browser sessions use binary WebSocket frames and follow the broker's one-writer rule.
HTTPS listens on port 443 only. During Phase 8A, authenticate to `https://<device-address>/` as `admin` with the legacy credential shown on UART0; role-aware user-database authentication begins in Phase 8B. The device serves its vendored xterm.js terminal without Internet access. Browser sessions use binary WebSocket frames and follow the broker's one-writer rule.
## SSH serial transport
@@ -112,7 +137,7 @@ HTTPS listens on port 443 only. Authenticate to `https://<device-address>/` as `
| `ssh host-key rotate --force` | Replace the persistent SSH host key. |
| `ssh reset --force` | Explicitly replace invalid or missing SSH material. |
SSH listens on port 22 and uses the same `admin` credentials as HTTPS, but a separate host key. It accepts password-authenticated interactive shell/PTY sessions only; it does not provide public-key authentication, `exec`, SFTP, SCP, forwarding, or subsystems. Verify the fingerprint from `ssh host-key info` before accepting an SSH host key.
During Phase 8A, SSH listens on port 22 and uses the same legacy `admin` credential as HTTPS, but a separate host key. It accepts password-authenticated interactive shell/PTY serial sessions only. Stored per-user public keys and roles become active in Phase 8B/8C; Phase 8A does not yet provide public-key login or the administrative SSH shell. SSH does not provide `exec`, SFTP, SCP, forwarding, or subsystems. Verify the fingerprint from `ssh host-key info` before accepting an SSH host key.
## Hardware diagnostics
+83
View File
@@ -186,6 +186,89 @@ Exercise the following fault matrix while keeping UART0 available:
For the final concurrency regression, connect USB CDC, one WebSocket terminal, and one SSH terminal while UART1 carries sustained bidirectional traffic. Navigate the UI, run the persistence checks, exercise service stop/start and writer revocation, and inject the display/stuck-button faults above. Confirm UART0 remains responsive, the UI never appears as a broker client or injects serial bytes, writer ownership is always the expected client or none, and serial/broker drop counters do not increase unexpectedly. Record `memory`, transport counters, `wifi counters`, and `debug display status` before and after the run.
## Phase 8A role-based user database and UART0 administration
Keep UART0 physically attached throughout these tests. Phase 8A deliberately does not change HTTPS or SSH authentication: existing network sessions still use the legacy shared credential, while the new database and public keys are prepared for the Phase 8B cutover. Do not include generated or entered passwords in test logs.
### 1. First-boot migration and persistence
Upgrade a device that already has working HTTPS/SSH credentials and no `user_db` blob. After boot:
1. Run `user status` and `user show admin`.
2. Confirm exactly one `admin`-named account exists with role `user`, `bootstrapped=no`, no displayed password/salt/verifier, and no public keys.
3. Confirm the pre-upgrade credential still authenticates to HTTPS and SSH and that serial terminal behavior is unchanged.
4. Reboot and confirm `user status` reports the same account ID and authentication generation.
5. Confirm routine boot logs, `status`, `web status`, and `ssh status` contain no user password, verifier, salt, or raw public-key blob.
A transient failure reading the legacy web credential must not create or commit an empty user database. If a valid `user_db` blob already exists, it must still load without the legacy credential being readable.
### 2. Administrator bootstrap
Run `user bootstrap`, verify that typed password characters are not echoed, enter a 1264 character printable password twice, and confirm `user show admin` now reports role `admin` and a higher authentication generation. Reboot and confirm `bootstrapped=yes` and one administrator persist. On a separate resettable test image, exercise `user bootstrap --generate`; record the generated password securely for the test, verify it is displayed once, then clear the terminal scrollback after use.
During Phase 8A, verify the newly bootstrapped password does **not** replace the legacy HTTPS/SSH login yet and the existing network credential continues to work. This separation is intentional until Phase 8B. A second `user bootstrap` must be rejected without changing the account.
### 3. Account CRUD, capacity, and invariants
Use generated test passwords where practical:
```text
user add operator user --generate
user add maint admin --generate
user list
user show operator
user role operator admin --force
user role operator user --force
user password operator --generate
```
Confirm unique valid usernames are accepted; uppercase, empty, oversized, or punctuation-invalid usernames are rejected. Confirm duplicate users are rejected. Fill all eight slots and verify a ninth account is rejected without altering existing records; delete a non-final account and confirm its slot can be reused with a new account ID.
With two administrators, demote or delete one and confirm the operation persists. Then attempt both deletion and demotion of the final administrator and confirm both are rejected. Before bootstrap on a separate migration run, verify `user delete admin --force` is rejected so legacy synchronization remains recoverable. `Ctrl-C` during either hidden password prompt must cancel without changing the account or leaving stale input in the next console command.
### 4. SSH public-key import and fingerprints
Generate disposable Ed25519 and ECDSA P-256 OpenSSH keys on a trusted host. For each supported type, run `user key add <username>`, paste the single `.pub` line, and confirm `user show <username>` reports the type, stable slot index, and expected `SHA256:` fingerprint without displaying the Base64 blob or comment.
Verify all of the following fail without changing the account generation or key list:
- Unsupported RSA, certificate, or unknown key types.
- Truncated/invalid Base64, mismatched outer and embedded types, trailing blob data, malformed Ed25519 lengths, and an invalid/off-curve ECDSA point.
- Adding the same key again to the same account or assigning it to a different account.
- Adding a fourth key to an account that already has three.
- Deleting an empty/out-of-range slot.
Then exercise `user key delete <username> <index> --force` and `user key clear <username> --force`, reboot, and confirm the exact remaining fingerprints persist. Phase 8A stores these keys but does not yet accept SSH key login.
### 5. Legacy credential rotation boundary and reboot reconciliation
Before administrator bootstrap on a resettable test image:
1. Run `web credentials rotate --force`.
2. Confirm the console reports that the pre-bootstrap migrated verifier was synchronized.
3. Verify the new legacy credential works for HTTPS and SSH, reboot, and confirm `user show admin` retains the synchronized authentication generation.
4. Where NVS fault injection or controlled power interruption is available, interrupt after the web credential commits but before the user blob commits. On the next boot, confirm reconciliation updates the migrated verifier rather than committing an empty database or losing network recovery.
After bootstrap, rotate the legacy credential again. Confirm the console explicitly says it remains separate from user passwords, existing network sessions are revoked, the new legacy credential works, and the bootstrapped administrator generation/password are not silently replaced. Apply the same checks to `web reset --force`, additionally confirming HTTPS certificate recovery/restart behavior remains intact.
### 6. Explicit database recovery and storage failures
Using a disposable device or test NVS image, inject a wrong-size/version or malformed `user_db` blob and reboot. Confirm normal database initialization fails without erasing shared NVS, but the `user` command is still registered. `user status` must report the database unavailable. Run:
```text
user recover --force
user status
user bootstrap --generate
```
Confirm recovery explicitly replaces only `user_db`, imports the current legacy credential as role `user`, and leaves serial, Wi-Fi, HTTPS identity, SSH host key, and legacy network login intact. Confirm recovery is rejected while a valid database is initialized. Simulate NVS write/commit exhaustion or failure during create, password, role, key, and recovery operations; after reboot, each operation must expose either the complete old blob or complete new blob, never a partially live mutation.
Because NVS is not yet encrypted, this test validates logical verifier storage rather than resistance to physical extraction. PBKDF2 verifiers remain susceptible to offline guessing, and append-oriented NVS may retain historical plaintext legacy credentials until a full partition/flash erase.
### 7. Phase 8A regression
With the maximum supported mix of USB CDC, WebSocket, and SSH serial clients active, repeat user list/show and several UART0 mutations. Confirm password prompts remain physical-UART-only, no user operation acquires a broker client or writer lease, existing Phase 8A network authentication and revocation behavior remains functional, and serial/broker counters show no unexplained loss. Record `memory`, `web counters`, and `ssh counters` before and after the run.
## Configuration A: data and handshake pairs
Connect the following pairs:
+10 -7
View File
@@ -38,7 +38,7 @@ These constraints apply across all phases:
| 5B | Offline xterm.js WebSocket serial terminal | **Complete** |
| 6 | Authenticated SSH serial transport | **Complete** |
| 7 | Local display and button interface | **In progress (7E validation)** |
| 8 | Role-based users and administrative access | **Planned** |
| 8 | Role-based users and administrative access | **In progress (8A validation)** |
| 9 | Security and production hardening | **Planned** |
| 10 | Authenticated, rollback-capable OTA | **Planned** |
| 11 | BLE serial transport and provisioning evaluation | **Planned** |
@@ -179,7 +179,7 @@ The software-crypto build no longer reproduces the HTTPD watchdog stall. This va
## Current and planned phases
The order below is the current plan. Phase 7 is in progress; later phases remain planned or under evaluation. Detailed requirements should be finalized at the start of each phase, and optional features must not weaken the completed serial and recovery paths.
The order below is the current plan. Phase 7 is in progress, Phase 8A is implemented and awaiting target-hardware validation, and later work remains planned or under evaluation. Detailed requirements should be finalized at the start of each phase, and optional features must not weaken the completed serial and recovery paths.
### Phase 7 — Local display and buttons
@@ -244,11 +244,14 @@ Replace the single shared network credential with a bounded, persistent user sys
Implementation sequence:
1. **Phase 8A — User database and UART0 administration — Planned**
- Add a versioned, fixed-capacity user database with at most eight accounts, unique lowercase usernames, `user`/`admin` roles, per-account generations, and an invariant that prevents deletion or demotion of the final administrator.
- Store salted password verifiers rather than recoverable passwords. Support bounded hidden password entry and one-time generated passwords through UART0 commands for add/delete, role changes, password changes, and account inspection.
- Permit up to three SSH public keys per account. UART0 imports bounded OpenSSH public-key lines and lists fingerprints without exposing password verifiers or raw secret material.
- Migrate the existing shared credential transactionally so network recovery is preserved across power loss; document that append-oriented unencrypted NVS cannot guarantee physical erasure of historical plaintext entries.
1. **Phase 8A — User database and UART0 administration — Implemented; validation pending**
- A versioned `user_db` NVS blob holds at most eight accounts with unique lowercase usernames, `user`/`admin` roles, random account IDs, per-account authentication generations, and fixed storage bounds. The final administrator cannot be deleted or demoted.
- Passwords use random 16-byte salts and PBKDF2-HMAC-SHA256 verifiers rather than recoverable plaintext. UART0 provides bounded no-echo entry, confirmation, and one-time 24-character generated passwords for bootstrap, create, and password-change operations. Valid authentication attempts perform one real or dummy PBKDF2 outside the database mutex and revalidate the account generation before returning a principal.
- Each account accepts up to three bounded OpenSSH public keys. Initial import supports Ed25519 and ECDSA P-256, validates key structure and P-256 points, rejects a key already assigned to any account, and exposes only key type plus SHA-256 fingerprint in snapshots and status output.
- On first boot after upgrade, the existing shared `admin` network credential is transactionally imported as a role-`user` account; it is never silently elevated. `user bootstrap` sets a new password and promotes that account to `admin`. Until bootstrap, legacy web credential rotation/reset synchronizes the imported verifier and boot reconciliation repairs an interrupted second NVS commit. After bootstrap, Phase 8A deliberately leaves the legacy HTTPS/SSH credential separate until Phase 8B switches network authentication to the common database.
- `user recover --force` remains registered even when a malformed or incompatible user blob prevents normal initialization. From physical UART0 it explicitly replaces only the user-database blob with a role-`user` account derived from the current legacy network credential; it never automatically erases shared NVS.
- NVS remains unencrypted. Verifiers improve password-at-rest handling, but offline guessing remains possible and append-oriented flash may retain historical plaintext legacy credentials until the partition or flash is physically erased.
- Pending target-hardware validation covers migration and reboot persistence, CRUD/capacity/final-admin invariants, hidden and generated passwords, malformed/duplicate/capacity-limited key imports, legacy-rotation reconciliation, explicit recovery, NVS failures, and regression of existing HTTPS/SSH access.
2. **Phase 8B — Role-aware HTTPS and SSH authentication — Planned**
- Authenticate HTTPS and SSH passwords through the common user database and retain a secret-free principal containing account ID, role, authentication method, and credential generation.
- Add SSH public-key authentication while retaining password authentication. wolfSSH authorizes the stored username/key pair and remains responsible for cryptographic signature verification; unsigned key probes never count as completed authentication.
+3
View File
@@ -2,6 +2,7 @@ idf_component_register(
SRCS
"main.c"
"console_completion.c"
"console_input.c"
"network_console.c"
"system_console.c"
"secure_random.c"
@@ -24,6 +25,8 @@ idf_component_register(
"ssh_console.c"
"usb_cdc_transport.c"
"usb_console.c"
"user_database.c"
"user_console.c"
"web_security.c"
"web_serial_transport.c"
"web_assets_data.c"
+15
View File
@@ -116,6 +116,21 @@ static const char *const s_completion_candidates[] = {
"usb request-writer",
"usb release-writer",
/* Physical role-based user, password, and SSH-key administration. */
"user status",
"user list",
"user show",
"user bootstrap",
"user bootstrap --generate",
"user recover --force",
"user add",
"user delete",
"user role",
"user password",
"user key add",
"user key delete",
"user key clear",
/* Wi-Fi lifecycle, persistence, profiles, AP policy, and diagnostics. */
"wifi status",
"wifi profiles",
+108
View File
@@ -0,0 +1,108 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Bounded UART0 input helpers for physical-administration prompts. */
#include "console_input.h"
#include <stdio.h>
#include <string.h>
#include "driver/uart.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "secure_random.h"
#define CONSOLE_INPUT_UART UART_NUM_0
static esp_err_t prepare_prompt(const char *prompt)
{
vTaskDelay(1U);
esp_err_t error = uart_flush_input(CONSOLE_INPUT_UART);
if (error != ESP_OK) {
return error;
}
printf("%s", prompt);
fflush(stdout);
return ESP_OK;
}
static esp_err_t read_input(const char *prompt, uint8_t *output, size_t capacity,
bool hidden, size_t *output_length)
{
if (prompt == NULL || output == NULL || output_length == NULL || capacity == 0U) {
return ESP_ERR_INVALID_ARG;
}
*output_length = 0U;
memset(output, 0, capacity);
esp_err_t error = prepare_prompt(prompt);
if (error != ESP_OK) {
return error;
}
for (;;) {
uint8_t byte = 0U;
if (uart_read_bytes(CONSOLE_INPUT_UART, &byte, 1U, portMAX_DELAY) != 1) {
secure_wipe(output, capacity);
*output_length = 0U;
printf("\nInput failed.\n");
return ESP_FAIL;
}
if (byte == 0x03U) {
secure_wipe(output, capacity);
*output_length = 0U;
printf("\nCancelled.\n");
return ESP_ERR_INVALID_STATE;
}
if (byte == '\r' || byte == '\n') {
break;
}
if (byte == 0x08U || byte == 0x7fU) {
if (*output_length > 0U) {
output[--*output_length] = 0U;
if (!hidden) {
printf("\b \b");
fflush(stdout);
}
}
continue;
}
if (byte < 0x20U || byte > 0x7eU || *output_length >= capacity - 1U) {
putchar('\a');
fflush(stdout);
continue;
}
output[(*output_length)++] = byte;
if (!hidden) {
putchar((int)byte);
fflush(stdout);
}
}
putchar('\n');
return ESP_OK;
}
esp_err_t console_input_read_hidden(const char *prompt,
uint8_t *output, size_t capacity,
size_t minimum_length, size_t maximum_length,
size_t *output_length)
{
if (minimum_length > maximum_length || maximum_length >= capacity) {
return ESP_ERR_INVALID_ARG;
}
esp_err_t error = read_input(prompt, output, capacity, true, output_length);
if (error != ESP_OK) {
return error;
}
if (*output_length < minimum_length || *output_length > maximum_length) {
secure_wipe(output, capacity);
*output_length = 0U;
return ESP_ERR_INVALID_SIZE;
}
return ESP_OK;
}
esp_err_t console_input_read_line(const char *prompt,
uint8_t *output, size_t capacity,
size_t *output_length)
{
return read_input(prompt, output, capacity, false, output_length);
}
+17
View File
@@ -0,0 +1,17 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Bounded UART0 input helpers for physical-administration prompts. */
#pragma once
#include <stddef.h>
#include <stdint.h>
#include "esp_err.h"
esp_err_t console_input_read_hidden(const char *prompt,
uint8_t *output, size_t capacity,
size_t minimum_length, size_t maximum_length,
size_t *output_length);
esp_err_t console_input_read_line(const char *prompt,
uint8_t *output, size_t capacity,
size_t *output_length);
+36
View File
@@ -1,3 +1,5 @@
#include <string.h>
#include "driver/uart.h"
#include "console_completion.h"
#include "esp_console.h"
@@ -26,6 +28,8 @@
#include "system_console.h"
#include "usb_cdc_transport.h"
#include "usb_console.h"
#include "user_console.h"
#include "user_database.h"
#include "web_console.h"
#include "web_security.h"
#include "web_server.h"
@@ -127,6 +131,37 @@ void app_main(void)
web_security_source == WEB_SECURITY_LOAD_STORED ? "stored" : "newly generated");
}
user_database_load_result_t user_database_source = USER_DATABASE_LOAD_EMPTY;
web_security_credentials_t legacy_credentials;
memset(&legacy_credentials, 0, sizeof(legacy_credentials));
user_database_legacy_credentials_t legacy = {0};
const user_database_legacy_credentials_t *legacy_pointer = NULL;
if (web_security_error == ESP_OK &&
web_security_show_credentials(&legacy_credentials) == ESP_OK) {
legacy = (user_database_legacy_credentials_t){
.username = (const uint8_t *)legacy_credentials.username,
.username_length = legacy_credentials.username_length,
.password = (const uint8_t *)legacy_credentials.password,
.password_length = legacy_credentials.password_length,
};
legacy_pointer = &legacy;
}
esp_err_t user_database_error =
user_database_init(legacy_pointer, &user_database_source);
secure_wipe(&legacy_credentials, sizeof(legacy_credentials));
secure_wipe(&legacy, sizeof(legacy));
if (user_database_error != ESP_OK) {
ESP_LOGE(TAG, "User database unavailable: %s; current network authentication remains active",
esp_err_to_name(user_database_error));
} else {
ESP_LOGI(TAG, "Using %s user database",
user_database_source == USER_DATABASE_LOAD_STORED
? "stored"
: (user_database_source == USER_DATABASE_LOAD_MIGRATED_LEGACY
? "newly migrated user-level"
: "new empty"));
}
esp_err_t web_runtime_error = web_server_init();
if (web_runtime_error != ESP_OK) {
ESP_LOGE(TAG, "HTTPS runtime initialization failed: %s",
@@ -261,6 +296,7 @@ void app_main(void)
ESP_ERROR_CHECK(serial_console_register_commands());
ESP_ERROR_CHECK(session_console_register_commands());
ESP_ERROR_CHECK(usb_console_register_commands());
ESP_ERROR_CHECK(user_console_register_commands());
ESP_ERROR_CHECK(wifi_console_register_commands());
ESP_ERROR_CHECK(web_console_register_commands());
ESP_ERROR_CHECK(ssh_console_register_commands());
+457
View File
@@ -0,0 +1,457 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Physical UART0 role-based user administration. */
#include "user_console.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "console_input.h"
#include "esp_console.h"
#include "mbedtls/base64.h"
#include "secure_random.h"
#include "user_database.h"
#include "web_security.h"
#define USER_CONSOLE_KEY_LINE_CAPACITY 256U
static void print_usage(void)
{
printf("Usage:\n");
printf(" user status|list\n");
printf(" user show <username>\n");
printf(" user bootstrap [--generate]\n");
printf(" user recover --force\n");
printf(" user add <username> <user|admin> [--generate]\n");
printf(" user delete <username> --force\n");
printf(" user role <username> <user|admin> --force\n");
printf(" user password <username> [--generate]\n");
printf(" user key add <username>\n");
printf(" user key delete <username> <0..2> --force\n");
printf(" user key clear <username> --force\n");
}
static void print_fingerprint(const uint8_t fingerprint[USER_DATABASE_SHA256_LENGTH])
{
uint8_t encoded[48] = {0};
size_t length = 0U;
if (mbedtls_base64_encode(encoded, sizeof(encoded), &length,
fingerprint, USER_DATABASE_SHA256_LENGTH) != 0) {
printf("unavailable");
return;
}
while (length > 0U && encoded[length - 1U] == '=') {
--length;
}
printf("SHA256:%.*s", (int)length, (const char *)encoded);
secure_wipe(encoded, sizeof(encoded));
}
static void print_user(const user_database_user_snapshot_t *user)
{
printf("%.*s role=%s id=%lu generation=%lu keys=%u\n",
(int)user->username_length, user->username,
user_role_to_string(user->role),
(unsigned long)user->user_id,
(unsigned long)user->auth_generation,
(unsigned int)user->public_key_count);
for (size_t index = 0U; index < USER_DATABASE_MAX_SSH_KEYS_PER_USER; ++index) {
const user_database_key_snapshot_t *key = &user->public_keys[index];
if (!key->active) {
continue;
}
printf(" key %u %.*s ", (unsigned int)key->index,
(int)key->key_type_length, key->key_type);
print_fingerprint(key->sha256_fingerprint);
putchar('\n');
}
}
static int show_users(const char *selected)
{
user_database_snapshot_t snapshot;
esp_err_t error = user_database_get_snapshot(&snapshot);
if (error != ESP_OK) {
printf("User database unavailable: %s\n", esp_err_to_name(error));
return 1;
}
if (selected == NULL) {
printf("User database: generation=%lu users=%u/%u admins=%u bootstrapped=%s\n",
(unsigned long)snapshot.generation,
(unsigned int)snapshot.user_count,
USER_DATABASE_MAX_USERS,
(unsigned int)snapshot.admin_count,
snapshot.admin_bootstrapped ? "yes" : "no");
}
bool found = false;
for (size_t index = 0U; index < USER_DATABASE_MAX_USERS; ++index) {
const user_database_user_snapshot_t *user = &snapshot.users[index];
if (!user->active ||
(selected != NULL &&
(strlen(selected) != user->username_length ||
memcmp(selected, user->username, user->username_length) != 0))) {
continue;
}
print_user(user);
found = true;
}
if (selected != NULL && !found) {
printf("User '%s' not found.\n", selected);
return 1;
}
if (!snapshot.admin_bootstrapped) {
printf("Administrative network access is not bootstrapped; use 'user bootstrap'.\n");
}
return 0;
}
static esp_err_t read_password(uint8_t password[USER_DATABASE_PASSWORD_CAPACITY + 1U],
size_t *password_length)
{
uint8_t confirmation[USER_DATABASE_PASSWORD_CAPACITY + 1U] = {0};
size_t confirmation_length = 0U;
esp_err_t error = console_input_read_hidden(
"Password (12..64 printable characters, Ctrl-C cancels): ",
password, USER_DATABASE_PASSWORD_CAPACITY + 1U,
USER_DATABASE_PASSWORD_MIN_LENGTH, USER_DATABASE_PASSWORD_CAPACITY,
password_length);
if (error == ESP_OK) {
error = console_input_read_hidden(
"Repeat password: ", confirmation, sizeof(confirmation),
USER_DATABASE_PASSWORD_MIN_LENGTH, USER_DATABASE_PASSWORD_CAPACITY,
&confirmation_length);
}
if (error == ESP_OK &&
(*password_length != confirmation_length ||
memcmp(password, confirmation, *password_length) != 0)) {
printf("Passwords do not match.\n");
error = ESP_ERR_INVALID_RESPONSE;
}
secure_wipe(confirmation, sizeof(confirmation));
if (error != ESP_OK) {
secure_wipe(password, USER_DATABASE_PASSWORD_CAPACITY + 1U);
*password_length = 0U;
}
return error;
}
static void show_generated_password(const char *username,
user_database_generated_password_t *generated)
{
printf("Generated password for %s: %.*s\n",
username, (int)generated->password_length, generated->password);
printf("This password is shown once; store it securely.\n");
secure_wipe(generated, sizeof(*generated));
}
static int recover_database(void)
{
web_security_credentials_t credentials;
memset(&credentials, 0, sizeof(credentials));
esp_err_t error = web_security_show_credentials(&credentials);
if (error == ESP_OK) {
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,
};
error = user_database_recover_from_legacy(&legacy);
}
secure_wipe(&credentials, sizeof(credentials));
if (error != ESP_OK) {
printf("Could not recover user database: %s\n", esp_err_to_name(error));
return 1;
}
printf("User database replaced from the current legacy network credential.\n");
printf("The imported account has role user; run 'user bootstrap' to establish an administrator.\n");
return 0;
}
static int bootstrap(bool generated)
{
esp_err_t error;
if (generated) {
user_database_generated_password_t password;
error = user_database_bootstrap_admin_generated(&password);
if (error == ESP_OK) {
show_generated_password("admin", &password);
}
} else {
uint8_t password[USER_DATABASE_PASSWORD_CAPACITY + 1U] = {0};
size_t password_length = 0U;
error = read_password(password, &password_length);
if (error == ESP_OK) {
error = user_database_bootstrap_admin(password, password_length);
}
secure_wipe(password, sizeof(password));
}
if (error != ESP_OK) {
printf("Could not bootstrap administrator: %s\n", esp_err_to_name(error));
return 1;
}
printf("Administrator account bootstrapped. Authentication integration follows in Phase 8B.\n");
return 0;
}
static int add_user(const char *username, const char *role_text, bool generated)
{
user_role_t role;
if (!user_database_username_valid((const uint8_t *)username, strlen(username)) ||
!user_role_parse(role_text, &role)) {
printf("Username must match [a-z][a-z0-9_-]{0,15}; role is user or admin.\n");
return 1;
}
esp_err_t error;
if (generated) {
user_database_generated_password_t password;
error = user_database_create_generated((const uint8_t *)username,
strlen(username), role, &password);
if (error == ESP_OK) {
show_generated_password(username, &password);
}
} else {
uint8_t password[USER_DATABASE_PASSWORD_CAPACITY + 1U] = {0};
size_t password_length = 0U;
error = read_password(password, &password_length);
if (error == ESP_OK) {
error = user_database_create((const uint8_t *)username, strlen(username),
role, password, password_length);
}
secure_wipe(password, sizeof(password));
}
if (error != ESP_OK) {
printf("Could not add user: %s\n", esp_err_to_name(error));
return 1;
}
printf("User '%s' added with role %s.\n", username, user_role_to_string(role));
return 0;
}
static int change_password(const char *username, bool generated)
{
esp_err_t error;
if (generated) {
user_database_generated_password_t password;
error = user_database_generate_password((const uint8_t *)username,
strlen(username), &password);
if (error == ESP_OK) {
show_generated_password(username, &password);
}
} else {
uint8_t password[USER_DATABASE_PASSWORD_CAPACITY + 1U] = {0};
size_t password_length = 0U;
error = read_password(password, &password_length);
if (error == ESP_OK) {
error = user_database_set_password((const uint8_t *)username,
strlen(username),
password, password_length);
}
secure_wipe(password, sizeof(password));
}
if (error != ESP_OK) {
printf("Could not change password: %s\n", esp_err_to_name(error));
return 1;
}
printf("Password changed; affected network sessions will be revoked in Phase 8B.\n");
return 0;
}
static bool parse_key_index(const char *text, uint8_t *index)
{
if (text == NULL || text[0] < '0' || text[0] > '9' || text[1] != '\0') {
return false;
}
uint8_t parsed = (uint8_t)(text[0] - '0');
if (parsed >= USER_DATABASE_MAX_SSH_KEYS_PER_USER) {
return false;
}
*index = parsed;
return true;
}
static bool key_delimiter(uint8_t value)
{
return value == ' ' || value == '\t';
}
static int add_key(const char *username)
{
uint8_t line[USER_CONSOLE_KEY_LINE_CAPACITY] = {0};
size_t line_length = 0U;
esp_err_t error = console_input_read_line(
"OpenSSH public key (type base64 [comment], Ctrl-C cancels): ",
line, sizeof(line), &line_length);
if (error != ESP_OK) {
secure_wipe(line, sizeof(line));
return 1;
}
uint8_t *space = NULL;
for (size_t index = 0U; index < line_length; ++index) {
if (key_delimiter(line[index])) {
space = &line[index];
break;
}
}
if (space == NULL) {
printf("Public key must contain a key type and Base64 blob.\n");
secure_wipe(line, sizeof(line));
return 1;
}
size_t type_length = (size_t)(space - line);
uint8_t *encoded = space + 1U;
size_t remaining = line_length - type_length - 1U;
while (remaining > 0U && key_delimiter(*encoded)) {
++encoded;
--remaining;
}
uint8_t *encoded_end = NULL;
for (size_t index = 0U; index < remaining; ++index) {
if (key_delimiter(encoded[index])) {
encoded_end = &encoded[index];
break;
}
}
size_t encoded_length = encoded_end == NULL
? remaining
: (size_t)(encoded_end - encoded);
uint8_t blob[USER_DATABASE_SSH_KEY_BLOB_CAPACITY] = {0};
size_t blob_length = 0U;
int decoded = mbedtls_base64_decode(blob, sizeof(blob), &blob_length,
encoded, encoded_length);
if (decoded != 0 ||
!user_database_key_valid(line, type_length, blob, blob_length)) {
printf("Unsupported or malformed key; use ssh-ed25519 or ecdsa-sha2-nistp256.\n");
secure_wipe(blob, sizeof(blob));
secure_wipe(line, sizeof(line));
return 1;
}
uint8_t key_index = 0U;
error = user_database_add_ssh_key((const uint8_t *)username, strlen(username),
line, type_length, blob, blob_length, &key_index);
secure_wipe(blob, sizeof(blob));
secure_wipe(line, sizeof(line));
if (error != ESP_OK) {
printf("Could not add SSH key: %s\n", esp_err_to_name(error));
return 1;
}
printf("SSH public key added at index %u. Key login is enabled in Phase 8B.\n",
(unsigned int)key_index);
return 0;
}
static int command_user(int argc, char **argv)
{
if (argc == 1 || (argc == 2 && strcmp(argv[1], "status") == 0) ||
(argc == 2 && strcmp(argv[1], "list") == 0)) {
return show_users(NULL);
}
if (argc == 3 && strcmp(argv[1], "show") == 0) {
return show_users(argv[2]);
}
if (argc == 3 && strcmp(argv[1], "recover") == 0 &&
strcmp(argv[2], "--force") == 0) {
return recover_database();
}
if ((argc == 2 || argc == 3) && strcmp(argv[1], "bootstrap") == 0) {
bool generated = argc == 3 && strcmp(argv[2], "--generate") == 0;
if (argc == 3 && !generated) {
print_usage();
return 1;
}
return bootstrap(generated);
}
if ((argc == 4 || argc == 5) && strcmp(argv[1], "add") == 0) {
bool generated = argc == 5 && strcmp(argv[4], "--generate") == 0;
if (argc == 5 && !generated) {
print_usage();
return 1;
}
return add_user(argv[2], argv[3], generated);
}
if (argc == 4 && strcmp(argv[1], "delete") == 0 &&
strcmp(argv[3], "--force") == 0) {
esp_err_t error = user_database_delete((const uint8_t *)argv[2], strlen(argv[2]));
if (error != ESP_OK) {
printf("Could not delete user (the migrated or final admin is protected): %s\n",
esp_err_to_name(error));
return 1;
}
printf("User '%s' deleted.\n", argv[2]);
return 0;
}
if (argc == 5 && strcmp(argv[1], "role") == 0 &&
strcmp(argv[4], "--force") == 0) {
user_role_t role;
if (!user_role_parse(argv[3], &role)) {
printf("Role must be user or admin.\n");
return 1;
}
esp_err_t error = user_database_set_role((const uint8_t *)argv[2],
strlen(argv[2]), role);
if (error != ESP_OK) {
printf("Could not change role (the final admin is protected): %s\n",
esp_err_to_name(error));
return 1;
}
printf("User '%s' role changed to %s.\n", argv[2], user_role_to_string(role));
return 0;
}
if ((argc == 3 || argc == 4) && strcmp(argv[1], "password") == 0) {
bool generated = argc == 4 && strcmp(argv[3], "--generate") == 0;
if (argc == 4 && !generated) {
print_usage();
return 1;
}
return change_password(argv[2], generated);
}
if (argc == 4 && strcmp(argv[1], "key") == 0 &&
strcmp(argv[2], "add") == 0) {
return add_key(argv[3]);
}
if (argc == 6 && strcmp(argv[1], "key") == 0 &&
strcmp(argv[2], "delete") == 0 && strcmp(argv[5], "--force") == 0) {
uint8_t index;
if (!parse_key_index(argv[4], &index)) {
printf("Key index must be 0..2.\n");
return 1;
}
esp_err_t error = user_database_remove_ssh_key(
(const uint8_t *)argv[3], strlen(argv[3]), index);
if (error != ESP_OK) {
printf("Could not delete SSH key: %s\n", esp_err_to_name(error));
return 1;
}
printf("SSH key %u deleted for '%s'.\n", (unsigned int)index, argv[3]);
return 0;
}
if (argc == 5 && strcmp(argv[1], "key") == 0 &&
strcmp(argv[2], "clear") == 0 && strcmp(argv[4], "--force") == 0) {
esp_err_t error = user_database_clear_ssh_keys(
(const uint8_t *)argv[3], strlen(argv[3]));
if (error != ESP_OK) {
printf("Could not clear SSH keys: %s\n", esp_err_to_name(error));
return 1;
}
printf("SSH keys cleared for '%s'.\n", argv[3]);
return 0;
}
print_usage();
return 1;
}
esp_err_t user_console_register_commands(void)
{
const esp_console_cmd_t command = {
.command = "user",
.help = "Manage bounded role-based users, passwords, and SSH public keys",
.hint = NULL,
.func = &command_user,
.argtable = NULL,
};
return esp_console_cmd_register(&command);
}
+8
View File
@@ -0,0 +1,8 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Physical UART0 role-based user administration. */
#pragma once
#include "esp_err.h"
esp_err_t user_console_register_commands(void);
+1274
View File
File diff suppressed because it is too large Load Diff
+157
View File
@@ -0,0 +1,157 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Bounded persistent role-based user and SSH authorized-key database. */
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
#define USER_DATABASE_NVS_NAMESPACE "user_db"
#define USER_DATABASE_NVS_BLOB_KEY "database"
#define USER_DATABASE_MAX_USERS 8U
#define USER_DATABASE_MAX_SSH_KEYS_PER_USER 3U
#define USER_DATABASE_USERNAME_CAPACITY 16U
#define USER_DATABASE_PASSWORD_CAPACITY 64U
#define USER_DATABASE_PASSWORD_MIN_LENGTH 12U
#define USER_DATABASE_GENERATED_PASSWORD_LENGTH 24U
#define USER_DATABASE_SSH_KEY_TYPE_CAPACITY 32U
#define USER_DATABASE_SSH_KEY_BLOB_CAPACITY 128U
#define USER_DATABASE_SHA256_LENGTH 32U
typedef enum {
USER_ROLE_USER = 1,
USER_ROLE_ADMIN = 2,
} user_role_t;
typedef enum {
USER_AUTH_METHOD_PASSWORD = 1,
USER_AUTH_METHOD_SSH_PUBLIC_KEY = 2,
} user_auth_method_t;
typedef enum {
USER_DATABASE_LOAD_STORED = 0,
USER_DATABASE_LOAD_MIGRATED_LEGACY,
USER_DATABASE_LOAD_EMPTY,
} user_database_load_result_t;
typedef struct {
const uint8_t *username;
size_t username_length;
const uint8_t *password;
size_t password_length;
} user_database_legacy_credentials_t;
typedef struct {
uint32_t user_id;
uint32_t auth_generation;
user_role_t role;
user_auth_method_t method;
size_t username_length;
char username[USER_DATABASE_USERNAME_CAPACITY + 1U];
} user_principal_t;
typedef struct {
size_t password_length;
uint8_t password[USER_DATABASE_PASSWORD_CAPACITY + 1U];
} user_database_generated_password_t;
typedef struct {
bool active;
uint8_t index;
size_t key_type_length;
char key_type[USER_DATABASE_SSH_KEY_TYPE_CAPACITY + 1U];
uint8_t sha256_fingerprint[USER_DATABASE_SHA256_LENGTH];
} user_database_key_snapshot_t;
typedef struct {
bool active;
uint32_t user_id;
uint32_t auth_generation;
user_role_t role;
size_t username_length;
char username[USER_DATABASE_USERNAME_CAPACITY + 1U];
uint8_t public_key_count;
user_database_key_snapshot_t public_keys[USER_DATABASE_MAX_SSH_KEYS_PER_USER];
} user_database_user_snapshot_t;
typedef struct {
bool initialized;
bool admin_bootstrapped;
uint32_t generation;
uint8_t user_count;
uint8_t admin_count;
user_database_user_snapshot_t users[USER_DATABASE_MAX_USERS];
} user_database_snapshot_t;
esp_err_t user_database_init(const user_database_legacy_credentials_t *legacy,
user_database_load_result_t *load_result);
/*
* Before the first administrator is established, keep the migrated account in
* sync with Phase 8A's legacy network credential. Once bootstrapped, the two
* credentials deliberately remain independent until the Phase 8B cutover.
*/
esp_err_t user_database_sync_legacy_credentials(
const user_database_legacy_credentials_t *legacy, bool *synchronized);
/* Explicit UART0 recovery: replace unavailable user storage with one legacy user. */
esp_err_t user_database_recover_from_legacy(
const user_database_legacy_credentials_t *legacy);
esp_err_t user_database_get_snapshot(user_database_snapshot_t *snapshot);
esp_err_t user_database_authenticate_password(
const uint8_t *username, size_t username_length,
const uint8_t *password, size_t password_length,
user_principal_t *principal, bool *authenticated);
esp_err_t user_database_authorize_ssh_public_key(
const uint8_t *username, size_t username_length,
const uint8_t *key_type, size_t key_type_length,
const uint8_t *key_blob, size_t key_blob_length,
user_principal_t *principal, bool *authorized);
esp_err_t user_database_principal_is_current(const user_principal_t *principal,
bool *current);
esp_err_t user_database_bootstrap_admin(const uint8_t *password,
size_t password_length);
esp_err_t user_database_bootstrap_admin_generated(
user_database_generated_password_t *generated_password);
esp_err_t user_database_create(const uint8_t *username, size_t username_length,
user_role_t role,
const uint8_t *password, size_t password_length);
esp_err_t user_database_create_generated(
const uint8_t *username, size_t username_length, user_role_t role,
user_database_generated_password_t *generated_password);
esp_err_t user_database_delete(const uint8_t *username, size_t username_length);
esp_err_t user_database_set_role(const uint8_t *username, size_t username_length,
user_role_t role);
esp_err_t user_database_set_password(const uint8_t *username, size_t username_length,
const uint8_t *password, size_t password_length);
esp_err_t user_database_generate_password(
const uint8_t *username, size_t username_length,
user_database_generated_password_t *generated_password);
esp_err_t user_database_add_ssh_key(
const uint8_t *username, size_t username_length,
const uint8_t *key_type, size_t key_type_length,
const uint8_t *key_blob, size_t key_blob_length,
uint8_t *key_index);
esp_err_t user_database_remove_ssh_key(const uint8_t *username,
size_t username_length,
uint8_t key_index);
esp_err_t user_database_clear_ssh_keys(const uint8_t *username,
size_t username_length);
bool user_database_username_valid(const uint8_t *username, size_t length);
bool user_database_password_valid(const uint8_t *password, size_t length);
bool user_database_key_valid(const uint8_t *key_type, size_t key_type_length,
const uint8_t *key_blob, size_t key_blob_length);
const char *user_role_to_string(user_role_t role);
bool user_role_parse(const char *text, user_role_t *role);
#ifdef __cplusplus
}
#endif
+33
View File
@@ -10,6 +10,7 @@
#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"
@@ -223,6 +224,36 @@ static int restart_if_running(bool was_running)
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) {
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("Phase 8A note: this legacy HTTPS/SSH credential is separate from bootstrapped user passwords until Phase 8B.\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;
@@ -232,6 +263,7 @@ static int rotate_credentials(void)
return 1;
}
synchronize_migrated_user(&credentials);
esp_err_t web_revoke_error = web_serial_transport_revoke_sessions();
esp_err_t ssh_revoke_error = ssh_transport_revoke_sessions();
printf("Administrative credentials rotated and persisted. Existing HTTPS and SSH credentials are now invalid.\n");
@@ -278,6 +310,7 @@ static int reset_material(void)
return 1;
}
synchronize_migrated_user(&credentials);
esp_err_t web_revoke_error = web_serial_transport_revoke_sessions();
esp_err_t ssh_revoke_error = ssh_transport_revoke_sessions();
printf("Administrative credentials, HTTPS certificate, and HTTPS private key replaced and persisted.\n");