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.
This commit is contained in:
2026-09-08 19:09:26 +02:00
parent 82f21d6116
commit ac80863d80
26 changed files with 1013 additions and 583 deletions
+109
View File
@@ -0,0 +1,109 @@
# Production web-security host regression
Run from the repository root:
```sh
python3 tests/web_security/run.py
```
Requires a C11 compiler, `nm`, Python 3, and installed mbedTLS 3.6 headers plus
`libmbedx509` / `libmbedcrypto`. Verified with host mbedTLS 3.6.7. No downloads,
firmware build, device access, asset generation or persistent build outputs.
The runner creates adapters and binaries in a temporary directory.
`security.c` includes the **entire unchanged production `src/web_security.c`**.
No crypto function, generator, validator, decoder or transaction is extracted or
replaced. Actual mbedTLS generates P-256 keys and signed certificates and parses,
hashes, checks the key pair and verifies the self-signature. The legacy fixture
is independently assembled at fixed little-endian byte offsets from a freshly
generated real identity, not a production legacy encoder. No real device secret
or fixed private key is checked in.
Host adapters provide a fixed device MAC, Linux `getrandom`, a lock-ownership
assertion, and fault-injectable NVS with staged writes/commit. At every write and
commit the adapter checks that production live state is still its predecessor.
Legacy wipe calls are counted and checked. Production restart is simulated by
clearing only module RAM while retaining the adapter's stored record. These
adapters do **not** prove ESP entropy initialization, allocation failure inside
mbedTLS, FreeRTOS concurrency, ESP NVS flash/power-loss semantics, TLS handshakes,
HTTPD restarts, target stack margins, or secure physical flash erasure. In
particular, modeled failed commits retain predecessor storage; real flash fault
and power-loss behavior needs target validation. No claim that NVS logical
replacement securely erases historical flash pages.
The suite reports 15 production security groups plus one API/console static
absence check. Coverage includes fresh and stored-v2 paths, exact v1 migration,
metadata/pair-copy bounds, NVS failures and retries, 21 legacy corruptions,
14 v2 corruptions, unknown sizes, real bad signatures with recomputed hashes,
mismatched private keys, wrong-device certificates, RNG/MAC/mutex failures,
rotation/reset commit-before-publication, unavailable explicit recovery,
generation exhaustion and invalid arguments. Console checks establish removal
of legacy command/secret/synchronization references, not runtime console
lifecycle execution. Read-only database status and login-failure counters are
intentionally retained.
## Integration/API contract
Five public functions remain:
- `web_security_init(web_security_load_result_t *)`
- `web_security_copy_tls_material(...)` (unchanged pair-copy API)
- `web_security_get_certificate_metadata(...)` (unchanged metadata)
- `web_security_rotate_certificate(void)`
- `web_security_reset_all(void)` (**TLS only**, changed signature)
Removed: two credential functions (`show_credentials`, `rotate_credentials`),
one credential struct type, three username/password capacity/length constants,
and two console operations (`web credentials show`, `web credentials rotate`).
There is no credential generation/display/synchronization path. Authentication
continues to belong to the user database; read-only status does not mutate it.
The integration owner must remove legacy startup callers in `main.c` and adapt
other console policy/completion/UI/test callers outside this ownership scope.
Load results retain `STORED=0`, `GENERATED_MISSING=1`, and add `MIGRATED_V1=2`.
Repeated successful init returns the remembered result without reloading.
Migration must validate and commit before publication; no fallback generation
or overwrite follows migration failure. Reset explicitly overwrites missing,
valid, or incompatible material, increments a live generation or uses one when
no live identity exists, and fails on live generation exhaustion. Rotation
requires live material and also fails at `UINT32_MAX`.
`web reset --force` retains the old lifecycle: commit first; when running,
stop then start, with no start after failed stop; otherwise attempt start.
Lifecycle failure does not roll back committed identity. Database accounts are
never synchronized, reset or otherwise mutated by these operations.
## Storage contract
The namespace/key remain **`web_sec/material`**, one blob, no new NVS keys.
V1 is exactly **1392 bytes**, read only through a private fixed-offset decoder.
Its credential layout must still match the shipped `admin`/24-character URL-safe
format, zero padding/reserved fields, and strict TLS validation.
V2 is exactly **1340 bytes** (52 bytes smaller), native little-endian ESP32
layout with compile-time offset/size assertions:
| Offset | Size | Field |
|---:|---:|---|
| 0 | 4 | schema version = 2 |
| 4 | 2 | blob size = 1340 |
| 6 | 2 | reserved, zero |
| 8 | 4 | nonzero generation |
| 12 | 2 | private key DER length |
| 14 | 2 | certificate DER length |
| 16 | 256 | private key DER, unused bytes zero |
| 272 | 1024 | certificate DER, unused bytes zero |
| 1296 | 32 | certificate SHA-256 fingerprint |
| 1328 | 12 | reserved, zero |
Migration preserves exact DER bytes, fingerprint and generation, including
`UINT32_MAX`; it never regenerates TLS identity. TLS validation includes exact
outer DER lengths, P-256 pair consistency, self-signature, fingerprint, device
CN/SAN, validity and existing certificate extension policy. Unknown sizes or
schema versions fail with `ESP_ERR_INVALID_VERSION`; malformed known records
fail with `ESP_ERR_INVALID_RESPONSE` (underlying operational failures propagate).
Every legacy input buffer is wiped on all post-read exits. The live blob has no
credential fields. No new task, queue, mutex, heap allocation or storage key is
introduced; the existing mutex remains. Migration adds a bounded 1392-byte
transient decoder buffer alongside the 1340-byte candidate; target call-stack
high-water usage is unmeasured.
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
"""Compile unchanged production security implementation with real host mbedTLS."""
from pathlib import Path
import subprocess
import re
import tempfile
ROOT = Path(__file__).resolve().parents[2]
HEADERS = {
"esp_err.h": """#pragma once
typedef int esp_err_t;
#define ESP_OK 0
#define ESP_FAIL -1
#define ESP_ERR_INVALID_ARG 1
#define ESP_ERR_INVALID_STATE 2
#define ESP_ERR_INVALID_SIZE 3
#define ESP_ERR_INVALID_VERSION 4
#define ESP_ERR_INVALID_RESPONSE 5
#define ESP_ERR_NO_MEM 6
""",
"esp_mac.h": """#pragma once
#include <stdint.h>
#include "esp_err.h"
#define ESP_MAC_WIFI_SOFTAP 1
esp_err_t esp_read_mac(uint8_t *, int);
""",
"freertos/FreeRTOS.h": """#pragma once
#define portMAX_DELAY 0xffffffffU
""",
"freertos/semphr.h": """#pragma once
typedef void *SemaphoreHandle_t;
SemaphoreHandle_t xSemaphoreCreateMutex(void);
int xSemaphoreTake(SemaphoreHandle_t, unsigned);
int xSemaphoreGive(SemaphoreHandle_t);
""",
"nvs.h": """#pragma once
#include <stddef.h>
#include "esp_err.h"
typedef int nvs_handle_t;
#define NVS_READONLY 0
#define NVS_READWRITE 1
#define ESP_ERR_NVS_NOT_FOUND 10
#define ESP_ERR_NVS_TYPE_MISMATCH 11
#define ESP_ERR_NVS_INVALID_LENGTH 12
esp_err_t nvs_open(const char *, int, nvs_handle_t *);
esp_err_t nvs_get_blob(nvs_handle_t, const char *, void *, size_t *);
esp_err_t nvs_set_blob(nvs_handle_t, const char *, const void *, size_t);
esp_err_t nvs_commit(nvs_handle_t);
void nvs_close(nvs_handle_t);
""",
}
with tempfile.TemporaryDirectory(prefix="web-security-") as directory:
out = Path(directory)
for name, text in HEADERS.items():
path = out / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text)
executable = out / "security"
subprocess.run([
"cc", "-std=c11", "-Wall", "-Wextra", "-Werror", "-O1", "-g",
"-I", str(out), "-I", str(ROOT / "src"),
str(ROOT / "tests/web_security/security.c"),
"-lmbedx509", "-lmbedcrypto", "-o", str(executable),
], check=True)
subprocess.run([str(executable)], check=True)
symbols = subprocess.check_output(["nm", "-g", str(executable)], text=True)
assert "web_security_show_credentials" not in symbols
assert "web_security_rotate_credentials" not in symbols
assert set(re.findall(r" T (web_security_\w+)$", symbols, re.MULTILINE)) == {
"web_security_init", "web_security_copy_tls_material",
"web_security_get_certificate_metadata", "web_security_rotate_certificate",
"web_security_reset_all",
}
header = (ROOT / "src/web_security.h").read_text()
assert "web_security_credentials_t" not in header
assert "WEB_SECURITY_PASSWORD" not in header
assert "WEB_SECURITY_USERNAME" not in header
console = (ROOT / "src/web_console.c").read_text()
for forbidden in ('"credentials"', "web credentials", "user_database_sync_legacy", "synchronize_migrated", "Password:"):
assert forbidden not in console, forbidden
assert "web_security_reset_all()" in console
assert set(re.findall(r"\b(user_database_\w+)\s*\(", console)) == {
"user_database_get_snapshot",
}
print("PASS exact five-function API and legacy credential/console DB-mutation absence")
+311
View File
@@ -0,0 +1,311 @@
/* SPDX-License-Identifier: GPL-3.0-only */
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/random.h>
/* Include, do not extract or replace: all production crypto/storage paths run. */
#include "../../src/web_security.c"
static uint8_t stored[1600], pending[1600];
static size_t stored_size, pending_size;
static int fault, writes, commits, rng_calls, legacy_wipes, groups;
static bool locked, fail_mutex, fail_rng, fail_mac, alternate_mac, watch_publication;
static web_security_blob_t expected_live;
enum { OPEN_RO = 20, OPEN_RW, QUERY, READ, SET, COMMIT, TYPE, SHORT_READ };
SemaphoreHandle_t xSemaphoreCreateMutex(void) { return fail_mutex ? NULL : (void *)1; }
int xSemaphoreTake(SemaphoreHandle_t m, unsigned delay)
{ (void)delay; assert(m && !locked); locked = true; return 1; }
int xSemaphoreGive(SemaphoreHandle_t m)
{ assert(m && locked); locked = false; return 1; }
esp_err_t esp_read_mac(uint8_t *mac, int type)
{
assert(type == ESP_MAC_WIFI_SOFTAP);
const uint8_t fixed[] = {2, 0, 0, 0x12, 0x34, 0x56};
memcpy(mac, fixed, sizeof(fixed));
if (alternate_mac) mac[5] ^= 1;
return fail_mac ? ESP_FAIL : ESP_OK;
}
esp_err_t secure_random_init(void) { return fail_rng ? ESP_FAIL : ESP_OK; }
esp_err_t secure_random_fill(void *out, size_t length)
{
++rng_calls;
if (fail_rng) return ESP_FAIL;
return getrandom(out, length, 0) == (ssize_t)length ? ESP_OK : ESP_FAIL;
}
int secure_random_mbedtls(void *ctx, unsigned char *out, size_t length)
{ (void)ctx; return secure_random_fill(out, length) == ESP_OK ? 0 : -1; }
void secure_wipe(void *data, size_t size)
{
volatile uint8_t *p = data;
for (size_t i = 0; i < size; ++i) p[i] = 0;
if (size == LEGACY_BLOB_SIZE) {
++legacy_wipes;
assert(bytes_are_zero(data, size));
}
}
esp_err_t nvs_open(const char *name, int mode, nvs_handle_t *handle)
{
assert(!strcmp(name, "web_sec"));
if (fault == (mode == NVS_READONLY ? OPEN_RO : OPEN_RW)) return ESP_FAIL;
*handle = mode;
return ESP_OK;
}
esp_err_t nvs_get_blob(nvs_handle_t handle, const char *key, void *data, size_t *size)
{
assert(handle == NVS_READONLY && !strcmp(key, "material"));
if (fault == TYPE) return ESP_ERR_NVS_TYPE_MISMATCH;
if (fault == (data ? READ : QUERY)) return ESP_FAIL;
if (!stored_size) return ESP_ERR_NVS_NOT_FOUND;
if (!data) { *size = stored_size; return ESP_OK; }
assert(*size >= stored_size);
memcpy(data, stored, stored_size);
*size = stored_size - (fault == SHORT_READ ? 1 : 0);
return ESP_OK;
}
esp_err_t nvs_set_blob(nvs_handle_t handle, const char *key, const void *data, size_t size)
{
assert(handle == NVS_READWRITE && !strcmp(key, "material"));
assert(size == 1340 && locked);
++writes;
if (watch_publication) assert(!memcmp(&s_material, &expected_live, sizeof(s_material)));
if (fault == SET) return ESP_FAIL;
memcpy(pending, data, size); pending_size = size;
return ESP_OK;
}
esp_err_t nvs_commit(nvs_handle_t handle)
{
assert(handle == NVS_READWRITE && pending_size);
++commits;
if (watch_publication) assert(!memcmp(&s_material, &expected_live, sizeof(s_material)));
if (fault == COMMIT) return ESP_FAIL;
memcpy(stored, pending, pending_size); stored_size = pending_size;
return ESP_OK;
}
void nvs_close(nvs_handle_t handle) { (void)handle; pending_size = 0; }
static void boot(void)
{
memset(&s_material, 0, sizeof(s_material));
s_material_ready = false; s_security_mutex = NULL;
s_load_result = WEB_SECURITY_LOAD_STORED;
fault = writes = commits = rng_calls = legacy_wipes = 0;
fail_mutex = fail_rng = fail_mac = alternate_mac = locked = false;
expected_live = s_material; watch_publication = true;
}
static void put16(uint8_t *p, unsigned v) { p[0] = v; p[1] = v >> 8; }
static void legacy(const web_security_blob_t *identity)
{
memset(stored, 0, sizeof(stored)); stored_size = 1392;
stored[0] = 1; put16(stored + 4, 1392);
put16(stored + 8, identity->generation);
put16(stored + 10, identity->generation >> 16);
stored[12] = 5; stored[13] = 24;
put16(stored + 14, identity->private_key_length);
put16(stored + 16, identity->certificate_length);
memcpy(stored + 20, "admin", 5);
memcpy(stored + 36, "Ab09-_Ab09-_Ab09-_Ab09-_", 24);
memcpy(stored + 68, identity->private_key_der, 256);
memcpy(stored + 324, identity->certificate_der, 1024);
memcpy(stored + 1348, identity->certificate_fingerprint, 32);
}
static void group(const char *name) { ++groups; printf("PASS %s\n", name); }
static void rejected(void)
{
uint8_t before[1600]; memcpy(before, stored, sizeof(before));
size_t size = stored_size;
web_security_load_result_t result = (web_security_load_result_t)99;
assert(web_security_init(&result) != ESP_OK);
assert(result == 99 && !s_material_ready);
assert(bytes_are_zero((uint8_t *)&s_material, sizeof(s_material)));
assert(size == stored_size && !memcmp(before, stored, sizeof(before)));
assert(writes == 0 || fault == SET || fault == COMMIT);
assert(web_security_rotate_certificate() == ESP_ERR_INVALID_STATE);
}
int main(void)
{
boot(); stored_size = 0;
assert(web_security_rotate_certificate() == ESP_ERR_INVALID_STATE);
web_security_load_result_t result;
assert(web_security_init(&result) == ESP_OK);
assert(result == WEB_SECURITY_LOAD_GENERATED_MISSING);
assert(stored_size == 1340 && writes == 1 && commits == 1);
assert(s_material.generation == 1 && validate_blob(&s_material) == ESP_OK);
web_security_blob_t identity = s_material;
group("fresh TLS-only generation and commit-before-publication");
boot(); assert(web_security_init(&result) == ESP_OK);
assert(result == WEB_SECURITY_LOAD_STORED && !writes && !commits);
assert(!memcmp(&identity, &s_material, sizeof(identity)));
group("stored v2 exact reload without write");
identity.generation = 0x12345678;
boot(); legacy(&identity);
assert(web_security_init(&result) == ESP_OK);
assert(result == WEB_SECURITY_LOAD_MIGRATED_V1 && legacy_wipes == 1);
assert(writes == 1 && commits == 1 && stored_size == 1340);
assert(!memcmp(&identity, &s_material, sizeof(identity)));
assert(!memcmp(stored, &identity, sizeof(identity)));
assert(web_security_init(&result) == ESP_OK && writes == 1);
assert(result == WEB_SECURITY_LOAD_MIGRATED_V1);
group("v1 migration exact key/certificate/fingerprint/generation and wipe");
uint8_t certificate[1024], key[256]; size_t cn, kn;
assert(web_security_copy_tls_material(NULL, 0, &cn, NULL, 0, &kn) == ESP_OK);
memset(certificate, 0xa5, sizeof(certificate));
assert(web_security_copy_tls_material(certificate, sizeof(certificate), &cn,
key, 1, &kn) == ESP_ERR_INVALID_SIZE);
assert(certificate[0] == 0xa5);
assert(web_security_copy_tls_material(certificate, sizeof(certificate), &cn,
key, sizeof(key), &kn) == ESP_OK);
assert(cn == identity.certificate_length && kn == identity.private_key_length);
assert(!memcmp(certificate, identity.certificate_der, cn));
assert(!memcmp(key, identity.private_key_der, kn));
web_security_certificate_metadata_t metadata;
assert(web_security_get_certificate_metadata(&metadata) == ESP_OK);
assert(metadata.material_generation == identity.generation);
assert(!memcmp(metadata.sha256_fingerprint, identity.certificate_fingerprint, 32));
group("public metadata and atomic pair-copy capacity contract");
const int faults[] = {OPEN_RO, QUERY, READ, TYPE, SHORT_READ, OPEN_RW, SET, COMMIT};
for (size_t i = 0; i < sizeof(faults)/sizeof(faults[0]); ++i) {
boot(); legacy(&identity); fault = faults[i]; rejected();
if (fault == READ || fault == SHORT_READ || fault == OPEN_RW ||
fault == SET || fault == COMMIT) assert(legacy_wipes == 1);
fault = 0;
assert(web_security_init(&result) == ESP_OK);
assert(!memcmp(&identity, &s_material, sizeof(identity)));
}
group("migration NVS open/query/read/type/short/set/commit failures and retry");
const size_t corrupt[] = {0, 4, 6, 8, 12, 13, 14, 15, 16, 17, 18,
20, 25, 36, 60, 68, 323, 324, 1347, 1348, 1380};
for (size_t i = 0; i < sizeof(corrupt)/sizeof(corrupt[0]); ++i) {
boot(); legacy(&identity);
if (corrupt[i] == 8) memset(stored + 8, 0, 4);
else stored[corrupt[i]] ^= 0x80;
rejected(); assert(legacy_wipes == 1 && writes == 0);
}
group("21 legacy schema/reserved/credential/length/DER/fingerprint corruptions");
const size_t bad_sizes[] = {1, 1339, 1341, 1391, 1393, 1600};
for (size_t i = 0; i < sizeof(bad_sizes)/sizeof(bad_sizes[0]); ++i) {
boot(); legacy(&identity); stored_size = bad_sizes[i]; rejected();
}
const size_t v2_corrupt[] = {0, 4, 6, 8, 12, 13, 14, 15, 16, 271, 272, 1295, 1296, 1328};
for (size_t i = 0; i < sizeof(v2_corrupt)/sizeof(v2_corrupt[0]); ++i) {
boot(); memcpy(stored, &identity, sizeof(identity)); stored_size = sizeof(identity);
if (v2_corrupt[i] == 8) memset(stored + 8, 0, 4);
else stored[v2_corrupt[i]] ^= 0x80;
rejected();
}
group("unknown sizes and 14 v2 structural/crypto corruptions fail untouched");
/* Recompute fingerprint so signature validation, not just hashing, rejects. */
web_security_blob_t invalid = identity;
invalid.certificate_der[invalid.certificate_length - 1] ^= 1;
assert(mbedtls_sha256(invalid.certificate_der, invalid.certificate_length,
invalid.certificate_fingerprint, 0) == 0);
boot(); legacy(&invalid); rejected();
web_security_blob_t other;
assert(generate_all(&other, 1) == ESP_OK);
invalid = identity;
memcpy(invalid.private_key_der, other.private_key_der, sizeof(invalid.private_key_der));
invalid.private_key_length = other.private_key_length;
boot(); legacy(&invalid); rejected();
boot(); legacy(&identity); fail_mac = true; rejected();
boot(); alternate_mac = true;
assert(generate_all(&invalid, 1) == ESP_OK);
alternate_mac = false; legacy(&invalid); rejected();
group("real signature, mismatched private key and device identity rejection");
for (size_t i = 0; i < 5; ++i) {
boot(); memcpy(stored, &identity, sizeof(identity)); stored_size = sizeof(identity);
fault = faults[i]; rejected(); assert(writes == 0);
}
boot(); memcpy(stored, &identity, sizeof(identity)); stored_size = sizeof(identity);
fault = SHORT_READ; rejected(); assert(writes == 0);
group("stored v2 read failures remain closed without writes");
for (int kind = 0; kind < 3; ++kind) {
boot(); stored_size = 0;
fail_rng = kind == 0; fail_mutex = kind == 1; fail_mac = kind == 2;
rejected(); assert(writes == 0);
}
for (size_t i = 5; i < sizeof(faults)/sizeof(faults[0]); ++i) {
boot(); stored_size = 0; fault = faults[i]; rejected();
}
group("fresh entropy/mutex/MAC/persistence failures do not publish");
boot(); legacy(&identity); assert(web_security_init(NULL) == ESP_OK);
expected_live = s_material;
for (int operation = 0; operation < 2; ++operation) {
for (int f = OPEN_RW; f <= COMMIT; ++f) {
if (f != OPEN_RW && f != SET && f != COMMIT) continue;
uint8_t before[1600]; memcpy(before, stored, sizeof(before));
fault = f;
assert((operation ? web_security_reset_all() : web_security_rotate_certificate()) != ESP_OK);
assert(!memcmp(&expected_live, &s_material, sizeof(s_material)));
assert(!memcmp(before, stored, sizeof(before)));
}
fault = 0; fail_rng = true;
assert((operation ? web_security_reset_all() : web_security_rotate_certificate()) != ESP_OK);
assert(!memcmp(&expected_live, &s_material, sizeof(s_material)));
fail_rng = false;
assert((operation ? web_security_reset_all() : web_security_rotate_certificate()) == ESP_OK);
assert(s_material.generation == expected_live.generation + 1);
assert(memcmp(s_material.certificate_fingerprint, expected_live.certificate_fingerprint, 32));
assert(validate_blob(&s_material) == ESP_OK);
expected_live = s_material;
}
group("rotation/reset transactional failures, identity change and generation increment");
for (int kind = 0; kind < 3; ++kind) {
boot(); legacy(&identity);
if (kind == 0) stored_size = 0;
if (kind == 1) stored[0] = 99;
assert(web_security_reset_all() == ESP_OK);
assert(s_material_ready && s_material.generation == 1 && stored_size == 1340);
assert(validate_blob(&s_material) == ESP_OK);
}
group("explicit reset replaces missing/unknown/valid storage without prior init");
for (int kind = 0; kind < 3; ++kind) {
for (size_t i = 5; i < sizeof(faults)/sizeof(faults[0]); ++i) {
boot(); legacy(&identity);
if (kind == 0) stored_size = 0;
if (kind == 1) stored[0] = 99;
uint8_t before[1600]; memcpy(before, stored, sizeof(before));
size_t size = stored_size;
fault = faults[i];
assert(web_security_reset_all() != ESP_OK);
assert(!s_material_ready && stored_size == size);
assert(!memcmp(stored, before, sizeof(before)));
assert(bytes_are_zero((uint8_t *)&s_material, sizeof(s_material)));
}
}
group("uninitialized reset persistence failures preserve missing/invalid/valid storage");
boot(); memcpy(stored, &identity, sizeof(identity)); stored_size = sizeof(identity);
assert(web_security_init(NULL) == ESP_OK);
s_material.generation = UINT32_MAX; expected_live = s_material;
int old_writes = writes;
assert(web_security_rotate_certificate() == ESP_ERR_INVALID_STATE);
assert(web_security_reset_all() == ESP_ERR_INVALID_STATE);
assert(!memcmp(&expected_live, &s_material, sizeof(s_material)) && writes == old_writes);
boot(); identity.generation = UINT32_MAX; legacy(&identity);
assert(web_security_init(&result) == ESP_OK && s_material.generation == UINT32_MAX);
group("generation exhaustion rejects mutations but preserves migration identity");
boot(); legacy(&identity);
assert(web_security_copy_tls_material(NULL, 0, NULL, NULL, 0, &kn) == ESP_ERR_INVALID_ARG);
assert(web_security_copy_tls_material(NULL, 1, &cn, NULL, 0, &kn) == ESP_ERR_INVALID_ARG);
assert(web_security_copy_tls_material(NULL, 0, &cn, NULL, 0, &kn) == ESP_ERR_INVALID_STATE);
assert(web_security_get_certificate_metadata(NULL) == ESP_ERR_INVALID_ARG);
assert(web_security_get_certificate_metadata(&metadata) == ESP_ERR_INVALID_STATE);
group("public invalid argument/unavailable contracts");
printf("PASS %d production security groups\n", groups);
return 0;
}