Decouple user provisioning from HTTPS identity storage while retaining compatible v1 user records and migrating TLS material to the credential-free v2 format. Add focused security regression coverage and update operator documentation.
173 lines
7.2 KiB
C
173 lines
7.2 KiB
C
/* 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
|
|
#define USER_DATABASE_ERR_DUPLICATE_SSH_KEY ESP_ERR_NOT_ALLOWED
|
|
|
|
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_EMPTY,
|
|
} user_database_load_result_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;
|
|
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;
|
|
|
|
/* Missing storage is persisted empty; valid v1 records load unchanged.
|
|
* Corrupt/unsupported storage fails closed and is never automatically replaced. */
|
|
esp_err_t user_database_init(user_database_load_result_t *load_result);
|
|
/* Explicit UART0 recovery only; refuses an initialized database. No credentials
|
|
* are imported or created. Caller enforces physical-console authorization. */
|
|
esp_err_t user_database_recover_empty(void);
|
|
esp_err_t user_database_get_snapshot(user_database_snapshot_t *snapshot);
|
|
|
|
/* Compact secret-free list, zero-wait mutex acquisition; no key material. */
|
|
typedef struct {
|
|
uint32_t user_id, auth_generation;
|
|
user_role_t role;
|
|
char username[USER_DATABASE_USERNAME_CAPACITY + 1U];
|
|
} user_database_account_t;
|
|
typedef struct {
|
|
size_t count;
|
|
user_database_account_t users[USER_DATABASE_MAX_USERS];
|
|
} user_database_accounts_t;
|
|
esp_err_t user_database_get_accounts(user_database_accounts_t *accounts);
|
|
/* Zero-wait, identity-conditional projection; fingerprints only, no key blobs.
|
|
* Output is cleared on failure; absent/stale identity returns NOT_FOUND. */
|
|
esp_err_t user_database_get_account_keys(const user_database_account_t *expected,
|
|
user_database_user_snapshot_t *snapshot);
|
|
esp_err_t user_database_add_ssh_key_current(const user_database_account_t *expected,
|
|
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_current(const user_database_account_t *expected,
|
|
uint8_t key_index);
|
|
esp_err_t user_database_clear_ssh_keys_current(const user_database_account_t *expected);
|
|
/* Compare target identity under the mutation lock, before candidate/commit.
|
|
* ESP_ERR_NOT_FOUND means absent or stale; existing account invariants apply. */
|
|
esp_err_t user_database_delete_current(const user_database_account_t *expected);
|
|
esp_err_t user_database_set_role_current(const user_database_account_t *expected,
|
|
user_role_t role);
|
|
esp_err_t user_database_set_password_current(const user_database_account_t *expected,
|
|
const uint8_t *password, size_t password_length);
|
|
/* RNG only: no database initialization, account mutation or persistence. Caller
|
|
* owns/wipes successful output; failures clear it. Same generator as CLI. */
|
|
esp_err_t user_database_generate_password_value(user_database_generated_password_t *generated);
|
|
|
|
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_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
|