869 lines
32 KiB
C
869 lines
32 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 uint32_t s_identity_token, s_next_identity_token;
|
|
static bool s_identity_used;
|
|
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_identity_token) {
|
|
xSemaphoreGive(s_security_mutex);
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
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 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_get_identity_snapshot(web_security_identity_snapshot_t *snapshot)
|
|
{
|
|
if (!snapshot) return ESP_ERR_INVALID_ARG;
|
|
memset(snapshot, 0, sizeof(*snapshot));
|
|
if (!s_security_mutex) return ESP_ERR_INVALID_STATE;
|
|
if (xSemaphoreTake(s_security_mutex, 0U) != pdTRUE) return ESP_ERR_TIMEOUT;
|
|
esp_err_t error = s_material_ready ? ESP_OK : ESP_ERR_INVALID_STATE;
|
|
if (error == ESP_OK) {
|
|
snapshot->generation = s_material.generation;
|
|
memcpy(snapshot->fingerprint, s_material.certificate_fingerprint, sizeof(snapshot->fingerprint));
|
|
snapshot->busy = s_identity_token != 0 || s_next_identity_token == UINT32_MAX;
|
|
}
|
|
xSemaphoreGive(s_security_mutex);
|
|
return error;
|
|
}
|
|
|
|
esp_err_t web_security_reserve_identity(uint32_t expected_generation, bool reset, uint32_t *token)
|
|
{
|
|
if (!token || (reset && expected_generation)) return ESP_ERR_INVALID_ARG;
|
|
*token = 0;
|
|
if (!reset) {
|
|
if (!s_security_mutex) return ESP_ERR_INVALID_STATE;
|
|
if (xSemaphoreTake(s_security_mutex, 0U) != pdTRUE) return ESP_ERR_TIMEOUT;
|
|
bool ready = s_material_ready;
|
|
xSemaphoreGive(s_security_mutex);
|
|
if (!ready) return ESP_ERR_INVALID_STATE;
|
|
}
|
|
esp_err_t error = secure_random_init();
|
|
if (error == ESP_OK) error = ensure_security_mutex();
|
|
if (error != ESP_OK) return error;
|
|
if (xSemaphoreTake(s_security_mutex, 0U) != pdTRUE) return ESP_ERR_TIMEOUT;
|
|
if (s_identity_token || s_next_identity_token == UINT32_MAX ||
|
|
(!s_material_ready && !reset) ||
|
|
(s_material_ready && s_material.generation == UINT32_MAX) ||
|
|
(expected_generation && expected_generation != s_material.generation)) {
|
|
xSemaphoreGive(s_security_mutex);
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
*token = s_identity_token = ++s_next_identity_token;
|
|
|
|
s_identity_used = false;
|
|
xSemaphoreGive(s_security_mutex);
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t web_security_replace_reserved(uint32_t token)
|
|
{
|
|
if (!s_security_mutex || !token) return ESP_ERR_INVALID_STATE;
|
|
xSemaphoreTake(s_security_mutex, portMAX_DELAY);
|
|
if (s_identity_token != token || s_identity_used) {
|
|
xSemaphoreGive(s_security_mutex);
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
s_identity_used = true;
|
|
uint32_t generation = s_material_ready ? s_material.generation + 1U : 1U;
|
|
xSemaphoreGive(s_security_mutex);
|
|
|
|
/* The reservation, not a held mutex/spinlock, excludes all identity writers. */
|
|
web_security_blob_t candidate = {0};
|
|
esp_err_t error = generate_all(&candidate, generation);
|
|
if (error == ESP_OK) error = save_blob(&candidate);
|
|
xSemaphoreTake(s_security_mutex, portMAX_DELAY);
|
|
if (error == ESP_OK) install_committed_blob(&candidate);
|
|
xSemaphoreGive(s_security_mutex);
|
|
secure_wipe(&candidate, sizeof(candidate));
|
|
return error;
|
|
}
|
|
|
|
void web_security_release_identity(uint32_t token)
|
|
{
|
|
if (!s_security_mutex || !token) return;
|
|
xSemaphoreTake(s_security_mutex, portMAX_DELAY);
|
|
if (s_identity_token == token) s_identity_token = 0;
|
|
xSemaphoreGive(s_security_mutex);
|
|
}
|
|
|
|
static esp_err_t replace_identity(bool reset)
|
|
{
|
|
uint32_t token = 0;
|
|
esp_err_t error = web_security_reserve_identity(0, reset, &token);
|
|
if (error == ESP_OK) error = web_security_replace_reserved(token);
|
|
web_security_release_identity(token);
|
|
return error;
|
|
}
|
|
|
|
esp_err_t web_security_rotate_certificate(void) { return replace_identity(false); }
|
|
esp_err_t web_security_reset_all(void) { return replace_identity(true); }
|