/* SPDX-License-Identifier: GPL-3.0-only */ /* Canonical NVS storage for HTTPS identity and legacy recovery credentials. */ #include "web_security.h" #include #include #include #include #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; } 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; }