Provisioning and Manufacturing
Provisioning is everything between "firmware builds" and "a customer unboxes a working, hardened device". For one hobby device it is five minutes with flash.sh; this chapter is the repeatable version, where per-device identity and irreversible eFuse burns demand an order of operations.
What makes a device an individual
| Item | Source | Where it lands |
|---|---|---|
| MAC addresses | factory-burned by Espressif | eFuse (already there) |
| API token | firmware generates on first boot | NVS |
| TLS certificate | optionally per-device at provisioning | storage partition |
| Pre-seeded settings (timezone, region defaults) | provisioning line | NVS image |
| Secure boot + flash encryption state | provisioning line | eFuse, irreversible |
The NVS pre-seeding pipeline
Shipping devices with sane defaults (and, for a batch, per-device values) uses ESP-IDF's NVS image tooling wrapped by tools/nvs_mass_provisioning.py: a CSV manifest (one row per device, columns for any NVS keys) becomes per-device NVS partition binaries, flashed at the nvs offset alongside the firmware. The firmware cannot tell pre-seeded values from ones it wrote itself, which is the point: no special "factory mode" code path exists to rot.
tools/provision_device.sh sequences one device end to end and is intentionally boring to read: flash images, seed NVS, run checks, print the serial-visible API token for the box label if your process wants it. The box label it prints carries the PoP together with the provisioning username safi (the firmware-wide PROV_USERNAME in main/wifi_manager.c), which the ESP SoftAP Provisioning app asks for before the PoP.
The provisioning PoP is derived, not stored
The PoP is never written to NVS. Production firmware derives it at boot from HMAC-SHA256(factory secret, base WiFi MAC) (main/pop_derive.c, PSA crypto), and the factory tooling derives the same value offline from the same MAC:
tools/nvs_mass_provisioning.pyreads the per-devicemacmanifest column and writes the derived PoP intolabels.csv(serial,cn,fingerprint,pop).tools/provision_device.shreads the base MAC withesptool.py read_macwhen--popis not passed and prints the derived PoP on the box label.
Both tools require SAFI_POP_FACTORY_SECRET (>=32 hex chars), which tools/production_preflight.sh also gates. Development builds have no secret and keep the old random-PoP fallback (generated on first boot, printed once on the serial console), so day-to-day flashing is unaffected.
Certificates
Development boards share the repo's self-signed certificate. For production, tools/generate_production_certs.sh mints a key and certificate per device, outside the git tree, written into that device's storage image. Per-device keys mean one extracted key compromises one device, and revocation is "that device", not "every device ever shipped". The browser-trust story is unchanged (self-signed either way, per the HTTPS chapter); what changes is blast radius.
The irreversible steps, in order
eFuses burn once. The sequence that cannot strand a batch:
- Everything reversible first: flash firmware + storage + NVS on the bench, full functional test (the boot report is the checklist: it must be empty).
tools/production_preflight.shverifies the build really is the production profile (gates on, keys present,SAFI_POP_FACTORY_SECRETset, insecure options structurally off; see security).- Secure boot eFuse on one sacrificial board from each new batch or firmware line first. Confirm it boots signed images and refuses unsigned ones.
- Flash encryption + NVS HMAC key (
tools/provision_device.sh --burn-efuses): the XTS flash-encryption key into BLOCK_KEY1, the random per-device NVS HMAC key (HMAC_UP) into BLOCK_KEY2, then the release-mode enable fuses. The first encrypted boot re-encrypts flash in place; power must not fail during it, so this happens on the bench, never at a customer. BLOCK_KEY0 already holds the Secure Boot digest, so the three purposes never share a key block (see the eFuse layout). - Only then, the rest of the batch.
The sacrificial-board step is not superstition: a wrong key digest in step 3 makes a brick, and the difference between one brick and a tray of them is this paragraph.
Reprovisioning in the field
- Factory reset (dashboard or long-press path) regenerates the API token and wipes settings; identity items in eFuse are untouched by design.
- An encrypted, secure-booted device accepts new firmware only via OTA with signed images; USB reflash requires the signing key and the encryption key material. That asymmetry (easy for you, hard for everyone else) is the whole product of this chapter.