Skip to main content

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​

ItemSourceWhere it lands
MAC addressesfactory-burned by EspressifeFuse (already there)
API tokenfirmware generates on first bootNVS
TLS certificateoptionally per-device at provisioningstorage partition
Pre-seeded settings (timezone, region defaults)provisioning lineNVS image
Secure boot + flash encryption stateprovisioning lineeFuse, 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.py reads the per-device mac manifest column and writes the derived PoP into labels.csv (serial,cn,fingerprint,pop).
  • tools/provision_device.sh reads the base MAC with esptool.py read_mac when --pop is 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:

  1. Everything reversible first: flash firmware + storage + NVS on the bench, full functional test (the boot report is the checklist: it must be empty).
  2. tools/production_preflight.sh verifies the build really is the production profile (gates on, keys present, SAFI_POP_FACTORY_SECRET set, insecure options structurally off; see security).
  3. Secure boot eFuse on one sacrificial board from each new batch or firmware line first. Confirm it boots signed images and refuses unsigned ones.
  4. 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).
  5. 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.