Files
ESP32_Serial_Swiss_Army_Knife/src/web_security.c
T
Commander1024 ac80863d80 Remove Legacy Credential Bootstrap Paths
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.
2026-09-08 19:09:26 +02:00

845 lines
30 KiB
C

/* SPDX-License-Identifier: GPL-3.0-only */
/* Canonical NVS storage for HTTPS identity with private v1 storage compatibility. */
#include "web_security.h"
#include <limits.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include "esp_mac.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "mbedtls/asn1.h"
#include "mbedtls/ecp.h"
#include "mbedtls/oid.h"
#include "mbedtls/pk.h"
#include "mbedtls/sha256.h"
#include "mbedtls/x509_crt.h"
#include "nvs.h"
#include "secure_random.h"
#define WEB_SECURITY_SCHEMA_VERSION 2U
#define WEB_SECURITY_BLOB_SIZE 1340U
#define LEGACY_BLOB_SIZE 1392U
static const uint8_t s_ap_ipv4_address[4] = {192U, 168U, 4U, 1U};
typedef struct {
uint32_t schema_version;
uint16_t blob_size;
uint16_t reserved_header;
uint32_t generation;
uint16_t private_key_length;
uint16_t certificate_length;
uint8_t private_key_der[WEB_SECURITY_PRIVATE_KEY_DER_CAPACITY];
uint8_t certificate_der[WEB_SECURITY_CERTIFICATE_DER_CAPACITY];
uint8_t certificate_fingerprint[WEB_SECURITY_SHA256_LENGTH];
uint8_t reserved[12];
} web_security_blob_t;
_Static_assert(offsetof(web_security_blob_t, private_key_der) == 16U,
"web security key offset changed");
_Static_assert(offsetof(web_security_blob_t, certificate_der) == 272U,
"web security certificate offset changed");
_Static_assert(offsetof(web_security_blob_t, certificate_fingerprint) == 1296U,
"web security fingerprint offset changed");
_Static_assert(sizeof(web_security_blob_t) == WEB_SECURITY_BLOB_SIZE,
"web security schema size changed");
static SemaphoreHandle_t s_security_mutex;
static web_security_blob_t s_material;
static bool s_material_ready;
static web_security_load_result_t s_load_result;
static bool bytes_are_zero(const uint8_t *data, size_t size)
{
for (size_t i = 0U; i < size; ++i) {
if (data[i] != 0U) {
return false;
}
}
return true;
}
static bool unused_bytes_are_zero(const uint8_t *data, size_t used, size_t capacity)
{
return used <= capacity && bytes_are_zero(data + used, capacity - used);
}
static bool constant_time_equal(const uint8_t *left, const uint8_t *right, size_t size)
{
uint8_t difference = 0U;
for (size_t i = 0U; i < size; ++i) {
difference |= left[i] ^ right[i];
}
return difference == 0U;
}
static esp_err_t ensure_security_mutex(void)
{
if (s_security_mutex != NULL) {
return ESP_OK;
}
s_security_mutex = xSemaphoreCreateMutex();
return s_security_mutex != NULL ? ESP_OK : ESP_ERR_NO_MEM;
}
static esp_err_t build_device_names(char *common_name, size_t common_name_size,
char *dns_name, size_t dns_name_size)
{
uint8_t mac[6] = {0};
esp_err_t error = esp_read_mac(mac, ESP_MAC_WIFI_SOFTAP);
if (error != ESP_OK) {
return error;
}
int common_length = snprintf(common_name, common_name_size,
"ESP32 SAK %02X%02X%02X",
mac[3], mac[4], mac[5]);
int dns_length = snprintf(dns_name, dns_name_size,
"esp32-sak-%02x%02x%02x.local",
mac[3], mac[4], mac[5]);
if (common_length < 0 || (size_t)common_length >= common_name_size ||
dns_length < 0 || (size_t)dns_length >= dns_name_size) {
return ESP_ERR_INVALID_SIZE;
}
return ESP_OK;
}
static esp_err_t normalize_der(unsigned char *buffer, size_t capacity,
int written, uint16_t *output_length)
{
if (written <= 0 || (size_t)written > capacity || written > UINT16_MAX) {
return ESP_FAIL;
}
/* Mbed TLS DER writers grow backward from the end of the output buffer. */
memmove(buffer, buffer + capacity - (size_t)written, (size_t)written);
memset(buffer + (size_t)written, 0, capacity - (size_t)written);
*output_length = (uint16_t)written;
return ESP_OK;
}
static esp_err_t generate_certificate(web_security_blob_t *blob)
{
char common_name[WEB_SECURITY_COMMON_NAME_CAPACITY] = {0};
char dns_name[WEB_SECURITY_DNS_NAME_CAPACITY] = {0};
char distinguished_name[WEB_SECURITY_COMMON_NAME_CAPACITY + 3U] = {0};
unsigned char serial[16] = {0};
mbedtls_pk_context key;
mbedtls_x509write_cert writer;
esp_err_t error = ESP_FAIL;
int result;
mbedtls_pk_init(&key);
mbedtls_x509write_crt_init(&writer);
memset(blob->private_key_der, 0, sizeof(blob->private_key_der));
memset(blob->certificate_der, 0, sizeof(blob->certificate_der));
memset(blob->certificate_fingerprint, 0, sizeof(blob->certificate_fingerprint));
blob->private_key_length = 0U;
blob->certificate_length = 0U;
error = build_device_names(common_name, sizeof(common_name),
dns_name, sizeof(dns_name));
if (error != ESP_OK) {
goto cleanup;
}
result = snprintf(distinguished_name, sizeof(distinguished_name),
"CN=%s", common_name);
if (result < 0 || (size_t)result >= sizeof(distinguished_name)) {
error = ESP_ERR_INVALID_SIZE;
goto cleanup;
}
result = mbedtls_pk_setup(&key, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY));
if (result != 0) {
error = ESP_ERR_NO_MEM;
goto cleanup;
}
result = mbedtls_ecp_gen_key(MBEDTLS_ECP_DP_SECP256R1,
mbedtls_pk_ec(key),
secure_random_mbedtls, NULL);
if (result != 0) {
error = ESP_FAIL;
goto cleanup;
}
result = mbedtls_pk_write_key_der(&key, blob->private_key_der,
sizeof(blob->private_key_der));
error = normalize_der(blob->private_key_der, sizeof(blob->private_key_der),
result, &blob->private_key_length);
if (error != ESP_OK) {
goto cleanup;
}
error = secure_random_fill(serial, sizeof(serial));
if (error != ESP_OK) {
goto cleanup;
}
/* RFC 5280 serials are positive and nonzero; retain 127 random bits. */
serial[0] &= 0x7fU;
serial[0] |= 0x01U;
mbedtls_x509_san_list dns_san = {0};
mbedtls_x509_san_list ip_san = {0};
dns_san.node.type = MBEDTLS_X509_SAN_DNS_NAME;
dns_san.node.san.unstructured_name.p = (unsigned char *)dns_name;
dns_san.node.san.unstructured_name.len = strlen(dns_name);
dns_san.next = &ip_san;
ip_san.node.type = MBEDTLS_X509_SAN_IP_ADDRESS;
ip_san.node.san.unstructured_name.p = (unsigned char *)s_ap_ipv4_address;
ip_san.node.san.unstructured_name.len = sizeof(s_ap_ipv4_address);
mbedtls_asn1_sequence server_auth = {0};
server_auth.buf.tag = MBEDTLS_ASN1_OID;
server_auth.buf.p = (unsigned char *)MBEDTLS_OID_SERVER_AUTH;
server_auth.buf.len = MBEDTLS_OID_SIZE(MBEDTLS_OID_SERVER_AUTH);
mbedtls_x509write_crt_set_version(&writer, MBEDTLS_X509_CRT_VERSION_3);
mbedtls_x509write_crt_set_md_alg(&writer, MBEDTLS_MD_SHA256);
mbedtls_x509write_crt_set_subject_key(&writer, &key);
mbedtls_x509write_crt_set_issuer_key(&writer, &key);
result = mbedtls_x509write_crt_set_serial_raw(&writer, serial, sizeof(serial));
if (result == 0) {
result = mbedtls_x509write_crt_set_validity(
&writer, WEB_SECURITY_CERT_NOT_BEFORE, WEB_SECURITY_CERT_NOT_AFTER);
}
if (result == 0) {
result = mbedtls_x509write_crt_set_subject_name(&writer, distinguished_name);
}
if (result == 0) {
result = mbedtls_x509write_crt_set_issuer_name(&writer, distinguished_name);
}
if (result == 0) {
result = mbedtls_x509write_crt_set_basic_constraints(&writer, 0, -1);
}
if (result == 0) {
result = mbedtls_x509write_crt_set_key_usage(
&writer, MBEDTLS_X509_KU_DIGITAL_SIGNATURE);
}
if (result == 0) {
result = mbedtls_x509write_crt_set_ext_key_usage(&writer, &server_auth);
}
if (result == 0) {
result = mbedtls_x509write_crt_set_subject_alternative_name(&writer,
&dns_san);
}
if (result != 0) {
error = ESP_FAIL;
goto cleanup;
}
result = mbedtls_x509write_crt_der(&writer, blob->certificate_der,
sizeof(blob->certificate_der),
secure_random_mbedtls, NULL);
error = normalize_der(blob->certificate_der, sizeof(blob->certificate_der),
result, &blob->certificate_length);
if (error != ESP_OK) {
goto cleanup;
}
if (mbedtls_sha256(blob->certificate_der, blob->certificate_length,
blob->certificate_fingerprint, 0) != 0) {
error = ESP_FAIL;
goto cleanup;
}
error = ESP_OK;
cleanup:
secure_wipe(serial, sizeof(serial));
mbedtls_x509write_crt_free(&writer);
mbedtls_pk_free(&key);
return error;
}
static bool x509_time_equals(const mbedtls_x509_time *time,
int year, int month, int day,
int hour, int minute, int second)
{
return time->year == year && time->mon == month && time->day == day &&
time->hour == hour && time->min == minute && time->sec == second;
}
static bool name_is_single_common_name(const mbedtls_x509_name *name,
const char *expected)
{
size_t expected_length = strlen(expected);
return name != NULL && name->next == NULL &&
name->oid.len == MBEDTLS_OID_SIZE(MBEDTLS_OID_AT_CN) &&
memcmp(name->oid.p, MBEDTLS_OID_AT_CN, name->oid.len) == 0 &&
name->val.len == expected_length &&
memcmp(name->val.p, expected, expected_length) == 0;
}
static bool extended_key_usage_is_server_auth_only(const mbedtls_x509_crt *certificate)
{
const mbedtls_x509_sequence *usage = &certificate->ext_key_usage;
return usage->next == NULL &&
usage->buf.p != NULL &&
usage->buf.len == MBEDTLS_OID_SIZE(MBEDTLS_OID_SERVER_AUTH) &&
memcmp(usage->buf.p, MBEDTLS_OID_SERVER_AUTH, usage->buf.len) == 0;
}
static bool subject_alt_names_are_expected(const mbedtls_x509_crt *certificate,
const char *dns_name)
{
bool found_dns = false;
bool found_ip = false;
for (const mbedtls_x509_sequence *item = &certificate->subject_alt_names;
item != NULL && item->buf.p != NULL; item = item->next) {
if (item->buf.tag == (MBEDTLS_ASN1_CONTEXT_SPECIFIC |
MBEDTLS_X509_SAN_DNS_NAME) &&
item->buf.len == strlen(dns_name) &&
memcmp(item->buf.p, dns_name, item->buf.len) == 0 && !found_dns) {
found_dns = true;
} else if (item->buf.tag == (MBEDTLS_ASN1_CONTEXT_SPECIFIC |
MBEDTLS_X509_SAN_IP_ADDRESS) &&
item->buf.len == sizeof(s_ap_ipv4_address) &&
memcmp(item->buf.p, s_ap_ipv4_address, item->buf.len) == 0 &&
!found_ip) {
found_ip = true;
} else {
return false;
}
}
return found_dns && found_ip;
}
static bool certificate_self_signature_is_valid(mbedtls_x509_crt *certificate)
{
unsigned char *cursor = certificate->raw.p;
const unsigned char *end = certificate->raw.p + certificate->raw.len;
const unsigned char *outer_end;
const unsigned char *signature;
size_t outer_length = 0U;
size_t field_length = 0U;
size_t signature_length = 0U;
uint8_t digest[WEB_SECURITY_SHA256_LENGTH] = {0};
bool valid = false;
/*
* Mbed TLS exposes the TBS bytes but not the signature bytes. Parse only
* the certificate's three outer fields instead of relying on private ABI.
*/
if (mbedtls_asn1_get_tag(&cursor, end, &outer_length,
MBEDTLS_ASN1_CONSTRUCTED |
MBEDTLS_ASN1_SEQUENCE) != 0 ||
outer_length != (size_t)(end - cursor)) {
goto cleanup;
}
outer_end = cursor + outer_length;
if (mbedtls_asn1_get_tag(&cursor, outer_end, &field_length,
MBEDTLS_ASN1_CONSTRUCTED |
MBEDTLS_ASN1_SEQUENCE) != 0 ||
field_length > (size_t)(outer_end - cursor)) {
goto cleanup;
}
cursor += field_length;
if (mbedtls_asn1_get_tag(&cursor, outer_end, &field_length,
MBEDTLS_ASN1_CONSTRUCTED |
MBEDTLS_ASN1_SEQUENCE) != 0 ||
field_length > (size_t)(outer_end - cursor)) {
goto cleanup;
}
cursor += field_length;
if (mbedtls_asn1_get_bitstring_null(&cursor, outer_end,
&signature_length) != 0 ||
signature_length == 0U ||
signature_length != (size_t)(outer_end - cursor)) {
goto cleanup;
}
signature = cursor;
if (mbedtls_sha256(certificate->tbs.p, certificate->tbs.len,
digest, 0) == 0 &&
mbedtls_pk_verify(&certificate->pk, MBEDTLS_MD_SHA256,
digest, sizeof(digest),
signature, signature_length) == 0) {
valid = true;
}
cleanup:
secure_wipe(digest, sizeof(digest));
return valid;
}
static bool der_is_exact_sequence(const uint8_t *der, size_t size)
{
unsigned char *cursor = (unsigned char *)der;
const unsigned char *end = der + size;
size_t length = 0U;
return mbedtls_asn1_get_tag(&cursor, end, &length,
MBEDTLS_ASN1_CONSTRUCTED | MBEDTLS_ASN1_SEQUENCE) == 0 &&
length == (size_t)(end - cursor);
}
static esp_err_t validate_certificate_and_key(const web_security_blob_t *blob)
{
char common_name[WEB_SECURITY_COMMON_NAME_CAPACITY] = {0};
char dns_name[WEB_SECURITY_DNS_NAME_CAPACITY] = {0};
uint8_t fingerprint[WEB_SECURITY_SHA256_LENGTH] = {0};
mbedtls_pk_context private_key;
mbedtls_x509_crt certificate;
esp_err_t error = ESP_ERR_INVALID_RESPONSE;
mbedtls_pk_init(&private_key);
mbedtls_x509_crt_init(&certificate);
if (build_device_names(common_name, sizeof(common_name),
dns_name, sizeof(dns_name)) != ESP_OK) {
error = ESP_FAIL;
goto cleanup;
}
if (mbedtls_sha256(blob->certificate_der, blob->certificate_length,
fingerprint, 0) != 0 ||
!constant_time_equal(fingerprint, blob->certificate_fingerprint,
sizeof(fingerprint))) {
goto cleanup;
}
if (!der_is_exact_sequence(blob->private_key_der, blob->private_key_length) ||
!der_is_exact_sequence(blob->certificate_der, blob->certificate_length)) {
goto cleanup;
}
if (mbedtls_pk_parse_key(&private_key,
blob->private_key_der, blob->private_key_length,
NULL, 0U, secure_random_mbedtls, NULL) != 0 ||
mbedtls_x509_crt_parse_der(&certificate,
blob->certificate_der,
blob->certificate_length) != 0) {
goto cleanup;
}
mbedtls_ecp_keypair *private_ec = mbedtls_pk_ec(private_key);
mbedtls_ecp_keypair *public_ec = mbedtls_pk_ec(certificate.pk);
if (private_ec == NULL || public_ec == NULL ||
mbedtls_ecp_keypair_get_group_id(private_ec) != MBEDTLS_ECP_DP_SECP256R1 ||
mbedtls_ecp_keypair_get_group_id(public_ec) != MBEDTLS_ECP_DP_SECP256R1 ||
mbedtls_pk_check_pair(&certificate.pk, &private_key,
secure_random_mbedtls, NULL) != 0) {
goto cleanup;
}
if (certificate.version != MBEDTLS_X509_CRT_VERSION_3 + 1 ||
certificate.sig_oid.len != MBEDTLS_OID_SIZE(MBEDTLS_OID_ECDSA_SHA256) ||
memcmp(certificate.sig_oid.p, MBEDTLS_OID_ECDSA_SHA256,
certificate.sig_oid.len) != 0 ||
certificate.issuer_raw.len != certificate.subject_raw.len ||
memcmp(certificate.issuer_raw.p, certificate.subject_raw.p,
certificate.subject_raw.len) != 0 ||
!name_is_single_common_name(&certificate.subject, common_name) ||
!name_is_single_common_name(&certificate.issuer, common_name) ||
!x509_time_equals(&certificate.valid_from, 2025, 1, 1, 0, 0, 0) ||
!x509_time_equals(&certificate.valid_to, 2049, 12, 31, 23, 59, 59)) {
goto cleanup;
}
if (!mbedtls_x509_crt_has_ext_type(&certificate,
MBEDTLS_X509_EXT_BASIC_CONSTRAINTS) ||
mbedtls_x509_crt_get_ca_istrue(&certificate) != 0 ||
!mbedtls_x509_crt_has_ext_type(&certificate, MBEDTLS_X509_EXT_KEY_USAGE) ||
mbedtls_x509_crt_check_key_usage(
&certificate, MBEDTLS_X509_KU_DIGITAL_SIGNATURE) != 0 ||
!mbedtls_x509_crt_has_ext_type(
&certificate, MBEDTLS_X509_EXT_EXTENDED_KEY_USAGE) ||
!extended_key_usage_is_server_auth_only(&certificate) ||
mbedtls_x509_crt_check_extended_key_usage(
&certificate, MBEDTLS_OID_SERVER_AUTH,
MBEDTLS_OID_SIZE(MBEDTLS_OID_SERVER_AUTH)) != 0 ||
!mbedtls_x509_crt_has_ext_type(&certificate,
MBEDTLS_X509_EXT_SUBJECT_ALT_NAME) ||
!subject_alt_names_are_expected(&certificate, dns_name)) {
goto cleanup;
}
if (!certificate_self_signature_is_valid(&certificate)) {
goto cleanup;
}
error = ESP_OK;
cleanup:
secure_wipe(fingerprint, sizeof(fingerprint));
mbedtls_x509_crt_free(&certificate);
mbedtls_pk_free(&private_key);
return error;
}
static esp_err_t validate_blob(const web_security_blob_t *blob)
{
if (blob == NULL) {
return ESP_ERR_INVALID_ARG;
}
if (blob->schema_version != WEB_SECURITY_SCHEMA_VERSION ||
blob->blob_size != WEB_SECURITY_BLOB_SIZE) {
return ESP_ERR_INVALID_VERSION;
}
if (blob->generation == 0U || blob->reserved_header != 0U ||
!bytes_are_zero(blob->reserved, sizeof(blob->reserved)) ||
blob->private_key_length == 0U ||
blob->private_key_length > sizeof(blob->private_key_der) ||
!unused_bytes_are_zero(blob->private_key_der, blob->private_key_length,
sizeof(blob->private_key_der)) ||
blob->certificate_length == 0U ||
blob->certificate_length > sizeof(blob->certificate_der) ||
!unused_bytes_are_zero(blob->certificate_der, blob->certificate_length,
sizeof(blob->certificate_der))) {
return ESP_ERR_INVALID_RESPONSE;
}
return validate_certificate_and_key(blob);
}
static esp_err_t generate_all(web_security_blob_t *blob, uint32_t generation)
{
memset(blob, 0, sizeof(*blob));
blob->schema_version = WEB_SECURITY_SCHEMA_VERSION;
blob->blob_size = WEB_SECURITY_BLOB_SIZE;
blob->generation = generation;
esp_err_t error = generate_certificate(blob);
if (error == ESP_OK) {
error = validate_blob(blob);
}
return error;
}
static esp_err_t save_blob(const web_security_blob_t *blob)
{
esp_err_t error = validate_blob(blob);
if (error != ESP_OK) {
return error;
}
nvs_handle_t handle;
error = nvs_open(WEB_SECURITY_NVS_NAMESPACE, NVS_READWRITE, &handle);
if (error != ESP_OK) {
return error;
}
/* Publish only after commit. NVS replacement is not secure flash erasure. */
error = nvs_set_blob(handle, WEB_SECURITY_NVS_BLOB_KEY,
blob, sizeof(*blob));
if (error == ESP_OK) {
error = nvs_commit(handle);
}
nvs_close(handle);
return error;
}
/* The shipped ESP32 v1 wire layout is little-endian, independent of host ABI.
* Credentials exist only in this transient decoder input, never live state. */
static uint16_t legacy_u16(const uint8_t *p)
{
return (uint16_t)p[0] | (uint16_t)((uint16_t)p[1] << 8);
}
static uint32_t legacy_u32(const uint8_t *p)
{
return (uint32_t)legacy_u16(p) | ((uint32_t)legacy_u16(p + 2) << 16);
}
static esp_err_t decode_legacy(const uint8_t raw[LEGACY_BLOB_SIZE],
web_security_blob_t *blob)
{
if (legacy_u32(raw) != 1U || legacy_u16(raw + 4) != LEGACY_BLOB_SIZE) {
return ESP_ERR_INVALID_VERSION;
}
if (legacy_u16(raw + 6) != 0U || legacy_u16(raw + 18) != 0U ||
raw[12] != 5U || raw[13] != 24U ||
memcmp(raw + 20, "admin", 5U) != 0 ||
!bytes_are_zero(raw + 25, 11U) ||
!bytes_are_zero(raw + 60, 8U) ||
!bytes_are_zero(raw + 1380, 12U)) {
return ESP_ERR_INVALID_RESPONSE;
}
for (size_t i = 36U; i < 60U; ++i) {
uint8_t c = raw[i];
if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') || c == '-' || c == '_')) {
return ESP_ERR_INVALID_RESPONSE;
}
}
memset(blob, 0, sizeof(*blob));
blob->schema_version = WEB_SECURITY_SCHEMA_VERSION;
blob->blob_size = WEB_SECURITY_BLOB_SIZE;
blob->generation = legacy_u32(raw + 8);
blob->private_key_length = legacy_u16(raw + 14);
blob->certificate_length = legacy_u16(raw + 16);
memcpy(blob->private_key_der, raw + 68, sizeof(blob->private_key_der));
memcpy(blob->certificate_der, raw + 324, sizeof(blob->certificate_der));
memcpy(blob->certificate_fingerprint, raw + 1348,
sizeof(blob->certificate_fingerprint));
return validate_blob(blob);
}
static esp_err_t load_stored_blob(web_security_blob_t *blob, bool *missing,
bool *migrated)
{
*missing = false;
*migrated = false;
nvs_handle_t handle;
esp_err_t error = nvs_open(WEB_SECURITY_NVS_NAMESPACE, NVS_READONLY, &handle);
if (error == ESP_ERR_NVS_NOT_FOUND) {
*missing = true;
return ESP_OK;
}
if (error != ESP_OK) {
return error;
}
size_t size = 0U;
error = nvs_get_blob(handle, WEB_SECURITY_NVS_BLOB_KEY, NULL, &size);
if (error == ESP_ERR_NVS_NOT_FOUND) {
*missing = true;
nvs_close(handle);
return ESP_OK;
}
if (error == ESP_ERR_NVS_TYPE_MISMATCH) {
nvs_close(handle);
return ESP_ERR_INVALID_RESPONSE;
}
if (error != ESP_OK) {
nvs_close(handle);
return error;
}
if (size == LEGACY_BLOB_SIZE) {
uint8_t legacy[LEGACY_BLOB_SIZE] = {0};
error = nvs_get_blob(handle, WEB_SECURITY_NVS_BLOB_KEY, legacy, &size);
nvs_close(handle);
if (error == ESP_OK) {
error = size == LEGACY_BLOB_SIZE ? decode_legacy(legacy, blob)
: ESP_ERR_INVALID_VERSION;
}
secure_wipe(legacy, sizeof(legacy));
if (error == ESP_OK) {
error = save_blob(blob);
*migrated = error == ESP_OK;
}
return error == ESP_ERR_NVS_INVALID_LENGTH ? ESP_ERR_INVALID_VERSION : error;
}
if (size != sizeof(*blob)) {
nvs_close(handle);
return ESP_ERR_INVALID_VERSION;
}
memset(blob, 0, sizeof(*blob));
error = nvs_get_blob(handle, WEB_SECURITY_NVS_BLOB_KEY, blob, &size);
nvs_close(handle);
if (error == ESP_ERR_NVS_INVALID_LENGTH) {
return ESP_ERR_INVALID_VERSION;
}
if (error != ESP_OK) {
return error;
}
return size == sizeof(*blob) ? validate_blob(blob) : ESP_ERR_INVALID_VERSION;
}
esp_err_t web_security_init(web_security_load_result_t *load_result)
{
esp_err_t error = secure_random_init();
if (error != ESP_OK) {
return error;
}
error = ensure_security_mutex();
if (error != ESP_OK) {
return error;
}
xSemaphoreTake(s_security_mutex, portMAX_DELAY);
if (s_material_ready) {
if (load_result != NULL) {
*load_result = s_load_result;
}
xSemaphoreGive(s_security_mutex);
return ESP_OK;
}
web_security_blob_t candidate;
bool missing = false;
bool migrated = false;
error = load_stored_blob(&candidate, &missing, &migrated);
if (error == ESP_OK && missing) {
error = generate_all(&candidate, 1U);
if (error == ESP_OK) {
error = save_blob(&candidate);
}
}
if (error == ESP_OK) {
s_material = candidate;
s_material_ready = true;
s_load_result = missing ? WEB_SECURITY_LOAD_GENERATED_MISSING
: migrated ? WEB_SECURITY_LOAD_MIGRATED_V1
: WEB_SECURITY_LOAD_STORED;
if (load_result != NULL) {
*load_result = s_load_result;
}
}
secure_wipe(&candidate, sizeof(candidate));
xSemaphoreGive(s_security_mutex);
return error;
}
esp_err_t web_security_copy_tls_material(
uint8_t *certificate, size_t certificate_capacity,
size_t *certificate_length,
uint8_t *private_key, size_t private_key_capacity,
size_t *private_key_length)
{
if (certificate_length == NULL || private_key_length == NULL ||
(certificate == NULL && certificate_capacity != 0U) ||
(private_key == NULL && private_key_capacity != 0U)) {
return ESP_ERR_INVALID_ARG;
}
if (s_security_mutex == NULL) {
return ESP_ERR_INVALID_STATE;
}
xSemaphoreTake(s_security_mutex, portMAX_DELAY);
esp_err_t error = ESP_ERR_INVALID_STATE;
if (s_material_ready) {
*certificate_length = s_material.certificate_length;
*private_key_length = s_material.private_key_length;
if ((certificate == NULL && certificate_capacity != 0U) ||
(private_key == NULL && private_key_capacity != 0U)) {
error = ESP_ERR_INVALID_ARG;
} else if ((certificate != NULL &&
certificate_capacity < s_material.certificate_length) ||
(private_key != NULL &&
private_key_capacity < s_material.private_key_length)) {
/* Check both capacities before copying either half of the pair. */
error = ESP_ERR_INVALID_SIZE;
} else {
if (certificate != NULL) {
memcpy(certificate, s_material.certificate_der,
s_material.certificate_length);
}
if (private_key != NULL) {
memcpy(private_key, s_material.private_key_der,
s_material.private_key_length);
}
error = ESP_OK;
}
}
xSemaphoreGive(s_security_mutex);
return error;
}
esp_err_t web_security_get_certificate_metadata(
web_security_certificate_metadata_t *metadata)
{
if (metadata == NULL) {
return ESP_ERR_INVALID_ARG;
}
if (s_security_mutex == NULL) {
return ESP_ERR_INVALID_STATE;
}
xSemaphoreTake(s_security_mutex, portMAX_DELAY);
esp_err_t error = ESP_ERR_INVALID_STATE;
if (s_material_ready) {
memset(metadata, 0, sizeof(*metadata));
error = build_device_names(metadata->common_name,
sizeof(metadata->common_name),
metadata->dns_name,
sizeof(metadata->dns_name));
if (error == ESP_OK) {
metadata->material_generation = s_material.generation;
memcpy(metadata->sha256_fingerprint,
s_material.certificate_fingerprint,
sizeof(metadata->sha256_fingerprint));
memcpy(metadata->ipv4_address, s_ap_ipv4_address,
sizeof(metadata->ipv4_address));
memcpy(metadata->not_before, WEB_SECURITY_CERT_NOT_BEFORE,
sizeof(WEB_SECURITY_CERT_NOT_BEFORE));
memcpy(metadata->not_after, WEB_SECURITY_CERT_NOT_AFTER,
sizeof(WEB_SECURITY_CERT_NOT_AFTER));
}
}
xSemaphoreGive(s_security_mutex);
return error;
}
static esp_err_t increment_generation(web_security_blob_t *blob)
{
if (blob->generation == UINT32_MAX) {
return ESP_ERR_INVALID_STATE;
}
++blob->generation;
return ESP_OK;
}
static void install_committed_blob(const web_security_blob_t *candidate)
{
/* Wipe the superseded private key before replacing the live snapshot. */
secure_wipe(&s_material, sizeof(s_material));
s_material = *candidate;
s_material_ready = true;
s_load_result = WEB_SECURITY_LOAD_STORED;
}
esp_err_t web_security_rotate_certificate(void)
{
if (s_security_mutex == NULL) {
return ESP_ERR_INVALID_STATE;
}
xSemaphoreTake(s_security_mutex, portMAX_DELAY);
esp_err_t error = ESP_ERR_INVALID_STATE;
web_security_blob_t candidate;
memset(&candidate, 0, sizeof(candidate));
if (s_material_ready) {
candidate = s_material;
error = increment_generation(&candidate);
if (error == ESP_OK) {
error = generate_certificate(&candidate);
}
if (error == ESP_OK) {
error = save_blob(&candidate);
}
if (error == ESP_OK) {
install_committed_blob(&candidate);
}
}
secure_wipe(&candidate, sizeof(candidate));
xSemaphoreGive(s_security_mutex);
return error;
}
esp_err_t web_security_reset_all(void)
{
esp_err_t error = secure_random_init();
if (error != ESP_OK) {
return error;
}
error = ensure_security_mutex();
if (error != ESP_OK) {
return error;
}
xSemaphoreTake(s_security_mutex, portMAX_DELAY);
web_security_blob_t candidate;
uint32_t generation = 1U;
if (s_material_ready) {
if (s_material.generation == UINT32_MAX) {
xSemaphoreGive(s_security_mutex);
return ESP_ERR_INVALID_STATE;
}
generation = s_material.generation + 1U;
}
error = generate_all(&candidate, generation);
if (error == ESP_OK) {
error = save_blob(&candidate);
}
if (error == ESP_OK) {
install_committed_blob(&candidate);
}
secure_wipe(&candidate, sizeof(candidate));
xSemaphoreGive(s_security_mutex);
return error;
}