Add Authenticated HTTPS Admin Foundation
This commit is contained in:
@@ -0,0 +1,979 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
/* Canonical NVS storage for HTTPS identity and administrative credentials. */
|
||||
|
||||
#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 1U
|
||||
#define WEB_SECURITY_BLOB_SIZE 1392U
|
||||
|
||||
static const uint8_t s_admin_username[] = "admin";
|
||||
static const char s_password_alphabet[] =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
||||
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;
|
||||
uint8_t username_length;
|
||||
uint8_t password_length;
|
||||
uint16_t private_key_length;
|
||||
uint16_t certificate_length;
|
||||
uint16_t reserved_lengths;
|
||||
uint8_t username[WEB_SECURITY_USERNAME_CAPACITY];
|
||||
uint8_t password[WEB_SECURITY_PASSWORD_CAPACITY];
|
||||
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, username) == 20U,
|
||||
"web security schema offsets changed");
|
||||
_Static_assert(offsetof(web_security_blob_t, private_key_der) == 68U,
|
||||
"web security key offset changed");
|
||||
_Static_assert(offsetof(web_security_blob_t, certificate_der) == 324U,
|
||||
"web security certificate 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 generate_credentials(web_security_blob_t *blob)
|
||||
{
|
||||
uint8_t random_bytes[WEB_SECURITY_PASSWORD_LENGTH] = {0};
|
||||
|
||||
memset(blob->username, 0, sizeof(blob->username));
|
||||
memset(blob->password, 0, sizeof(blob->password));
|
||||
memcpy(blob->username, s_admin_username, sizeof(s_admin_username) - 1U);
|
||||
blob->username_length = sizeof(s_admin_username) - 1U;
|
||||
blob->password_length = WEB_SECURITY_PASSWORD_LENGTH;
|
||||
|
||||
esp_err_t error = secure_random_fill(random_bytes, sizeof(random_bytes));
|
||||
if (error == ESP_OK) {
|
||||
/* Sixty-four symbols consume six random bits exactly, without modulo bias. */
|
||||
for (size_t i = 0U; i < sizeof(random_bytes); ++i) {
|
||||
blob->password[i] =
|
||||
(uint8_t)s_password_alphabet[random_bytes[i] & 0x3fU];
|
||||
}
|
||||
}
|
||||
secure_wipe(random_bytes, sizeof(random_bytes));
|
||||
return error;
|
||||
}
|
||||
|
||||
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 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 (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 ||
|
||||
blob->reserved_lengths != 0U ||
|
||||
!bytes_are_zero(blob->reserved, sizeof(blob->reserved)) ||
|
||||
blob->username_length != sizeof(s_admin_username) - 1U ||
|
||||
memcmp(blob->username, s_admin_username,
|
||||
sizeof(s_admin_username) - 1U) != 0 ||
|
||||
!unused_bytes_are_zero(blob->username, blob->username_length,
|
||||
sizeof(blob->username)) ||
|
||||
blob->password_length != WEB_SECURITY_PASSWORD_LENGTH ||
|
||||
!unused_bytes_are_zero(blob->password, blob->password_length,
|
||||
sizeof(blob->password)) ||
|
||||
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;
|
||||
}
|
||||
|
||||
for (size_t i = 0U; i < blob->password_length; ++i) {
|
||||
const uint8_t value = blob->password[i];
|
||||
bool valid = (value >= 'A' && value <= 'Z') ||
|
||||
(value >= 'a' && value <= 'z') ||
|
||||
(value >= '0' && value <= '9') ||
|
||||
value == '-' || value == '_';
|
||||
if (!valid) {
|
||||
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_credentials(blob);
|
||||
if (error == ESP_OK) {
|
||||
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;
|
||||
}
|
||||
|
||||
/* NVS append semantics retain the committed predecessor until commit succeeds. */
|
||||
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;
|
||||
}
|
||||
|
||||
static esp_err_t load_stored_blob(web_security_blob_t *blob, bool *missing)
|
||||
{
|
||||
*missing = 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 != 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 validate_blob(blob);
|
||||
}
|
||||
|
||||
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;
|
||||
error = load_stored_blob(&candidate, &missing);
|
||||
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
|
||||
: 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_copy_username(char *output, size_t capacity,
|
||||
size_t *output_length)
|
||||
{
|
||||
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) {
|
||||
if (output_length == NULL || (output == NULL && capacity != 0U)) {
|
||||
error = ESP_ERR_INVALID_ARG;
|
||||
} else {
|
||||
*output_length = s_material.username_length;
|
||||
if (output == NULL) {
|
||||
error = capacity == 0U ? ESP_OK : ESP_ERR_INVALID_ARG;
|
||||
} else if (capacity <= s_material.username_length) {
|
||||
error = ESP_ERR_INVALID_SIZE;
|
||||
} else {
|
||||
memcpy(output, s_material.username, s_material.username_length);
|
||||
output[s_material.username_length] = '\0';
|
||||
error = ESP_OK;
|
||||
}
|
||||
}
|
||||
}
|
||||
xSemaphoreGive(s_security_mutex);
|
||||
return error;
|
||||
}
|
||||
|
||||
static esp_err_t credential_digest(const uint8_t *username, size_t username_length,
|
||||
const uint8_t *password, size_t password_length,
|
||||
uint8_t digest[WEB_SECURITY_SHA256_LENGTH])
|
||||
{
|
||||
uint8_t canonical[4U + WEB_SECURITY_USERNAME_CAPACITY +
|
||||
WEB_SECURITY_PASSWORD_CAPACITY] = {0};
|
||||
size_t offset = 0U;
|
||||
|
||||
canonical[offset++] = (uint8_t)(username_length >> 8U);
|
||||
canonical[offset++] = (uint8_t)username_length;
|
||||
if (username_length > 0U) {
|
||||
memcpy(canonical + offset, username, username_length);
|
||||
offset += username_length;
|
||||
}
|
||||
canonical[offset++] = (uint8_t)(password_length >> 8U);
|
||||
canonical[offset++] = (uint8_t)password_length;
|
||||
if (password_length > 0U) {
|
||||
memcpy(canonical + offset, password, password_length);
|
||||
offset += password_length;
|
||||
}
|
||||
|
||||
int result = mbedtls_sha256(canonical, offset, digest, 0);
|
||||
secure_wipe(canonical, sizeof(canonical));
|
||||
return result == 0 ? ESP_OK : ESP_FAIL;
|
||||
}
|
||||
|
||||
esp_err_t web_security_authenticate_basic(const uint8_t *username,
|
||||
size_t username_length,
|
||||
const uint8_t *password,
|
||||
size_t password_length,
|
||||
bool *authenticated)
|
||||
{
|
||||
if (authenticated == NULL ||
|
||||
(username == NULL && username_length != 0U) ||
|
||||
(password == NULL && password_length != 0U)) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
*authenticated = false;
|
||||
if (s_security_mutex == NULL) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
if (username_length > WEB_SECURITY_USERNAME_CAPACITY ||
|
||||
password_length > WEB_SECURITY_PASSWORD_CAPACITY) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
uint8_t supplied_digest[WEB_SECURITY_SHA256_LENGTH] = {0};
|
||||
uint8_t expected_digest[WEB_SECURITY_SHA256_LENGTH] = {0};
|
||||
xSemaphoreTake(s_security_mutex, portMAX_DELAY);
|
||||
esp_err_t error = ESP_ERR_INVALID_STATE;
|
||||
if (s_material_ready) {
|
||||
error = credential_digest(username, username_length,
|
||||
password, password_length,
|
||||
supplied_digest);
|
||||
if (error == ESP_OK) {
|
||||
error = credential_digest(s_material.username,
|
||||
s_material.username_length,
|
||||
s_material.password,
|
||||
s_material.password_length,
|
||||
expected_digest);
|
||||
}
|
||||
if (error == ESP_OK) {
|
||||
*authenticated = constant_time_equal(supplied_digest,
|
||||
expected_digest,
|
||||
sizeof(expected_digest));
|
||||
}
|
||||
}
|
||||
xSemaphoreGive(s_security_mutex);
|
||||
secure_wipe(supplied_digest, sizeof(supplied_digest));
|
||||
secure_wipe(expected_digest, sizeof(expected_digest));
|
||||
return error;
|
||||
}
|
||||
|
||||
static void copy_credentials_locked(web_security_credentials_t *credentials,
|
||||
const web_security_blob_t *blob)
|
||||
{
|
||||
memset(credentials, 0, sizeof(*credentials));
|
||||
credentials->username_length = blob->username_length;
|
||||
credentials->password_length = blob->password_length;
|
||||
memcpy(credentials->username, blob->username, blob->username_length);
|
||||
memcpy(credentials->password, blob->password, blob->password_length);
|
||||
}
|
||||
|
||||
esp_err_t web_security_show_credentials(web_security_credentials_t *credentials)
|
||||
{
|
||||
if (credentials == 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) {
|
||||
copy_credentials_locked(credentials, &s_material);
|
||||
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_credentials(web_security_credentials_t *new_credentials)
|
||||
{
|
||||
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_credentials(&candidate);
|
||||
}
|
||||
if (error == ESP_OK) {
|
||||
error = save_blob(&candidate);
|
||||
}
|
||||
if (error == ESP_OK) {
|
||||
install_committed_blob(&candidate);
|
||||
if (new_credentials != NULL) {
|
||||
copy_credentials_locked(new_credentials, &s_material);
|
||||
}
|
||||
}
|
||||
}
|
||||
secure_wipe(&candidate, sizeof(candidate));
|
||||
xSemaphoreGive(s_security_mutex);
|
||||
return error;
|
||||
}
|
||||
|
||||
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(web_security_credentials_t *new_credentials)
|
||||
{
|
||||
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);
|
||||
if (new_credentials != NULL) {
|
||||
copy_credentials_locked(new_credentials, &s_material);
|
||||
}
|
||||
}
|
||||
secure_wipe(&candidate, sizeof(candidate));
|
||||
xSemaphoreGive(s_security_mutex);
|
||||
return error;
|
||||
}
|
||||
Reference in New Issue
Block a user