Device Cert Lifecycle

Every SCADABLE device carries a per-device X.509 certificate + private key minted at provision time. The library reads it from NVS at boot and uses it for two things in v0.1.0: identifying the device on the edge HTTPS call (via the X-Device-CN header) and validating the broker's TLS cert against the bundled CA chain.

Full mTLS — presenting the device cert at the MQTT TLS handshake — is a v0.2.0+ release coordinated with backend support.

Where it gets minted

When you provision a device via the SCADABLE dashboard:

  1. The dashboard mints a per-device cert against the SCADABLE intermediate CA.
  2. The cert's Common Name is SC-<device_id> (e.g. SC-a3f8b2c4e6d8).
  3. The cert + key + CA chain are bundled into an NVS partition image.
  4. The browser flasher writes that image to the chip at offset 0x9000.

Where it lives on the chip

NVS namespace scadable_cfg. Seven string keys:

KeyContent
device_idhex string, e.g. a3f8b2c4e6d8
common_namefull cert CN, format SC-<device_id>
cert_pemdevice certificate (PEM)
key_pemdevice private key (PEM)
ca_pemCA chain (PEM) for validating the broker
mqtt_hostfallback broker host (used only if /v1/route fails)
mqtt_portfallback broker port (as string)

The library opens this with nvs_open("scadable_cfg", NVS_READONLY, &h) on first boot. If anything is missing, the library logs an explicit error and idles — the device was flashed without provisioning.

On chips with flash encryption enabled (ESP32-S3 with eFuse keys), the NVS partition is encrypted at rest. Stock ESP32 without flash encryption → plaintext on flash. If physical attacker is in your threat model, enable flash encryption in sdkconfig before flashing.

How the library uses it

Three places at boot:

  1. Edge router call. The library reads common_name and sends it as X-Device-CN: SC-<device_id> on the HTTPS GET to edge.scadable.com/v1/route. The backend treats this header as the device identity for promotion + region lookup.
  2. Broker TLS validation. The MQTT client is configured with verification.certificate = ca_pem, so the broker's Let's Encrypt cert is validated against the SCADABLE CA chain.
  3. MQTT client ID. The library uses common_name as the MQTT client_id, giving the broker a stable identity per device for ACL configuration.

The device cert + key are loaded into memory but not presented at the MQTT TLS handshake in v0.1.0. They're carried for the v0.2.0+ mTLS-at-broker work.

Validity

v0.1.0 ships long-lived certs (the dashboard's provisioner currently mints with multi-year validity). Short-lived certs + on-device renewal-via-EST are roadmap items.

What to do if a cert is compromised

Re-provision the device:

  1. Dashboard → device → Revoke (planned UI; today the cert is rotated by re-issuing).
  2. Re-provision the device via the dashboard's flasher with a fresh bundle.
  3. The new cert overrides what's in NVS.

There's no current CRL distribution to brokers — the cert rotation IS the revocation. Coordinated CRL support arrives with the v0.2.0 mTLS-at-broker work.

Why one bundle, no separate Wi-Fi creds

v0.1.0 explicitly does NOT store Wi-Fi credentials in NVS. The library is network-agnostic — your code in scadable_user_main brings up whatever network you want (Wi-Fi STA from Kconfig, SoftAP+captive portal, Ethernet, cellular). If you store Wi-Fi creds in NVS yourself, use a different namespace (like app_wifi) to avoid colliding with scadable_cfg.

What the library reads at boot

If the load succeeds, you'll see this in idf.py monitor:

I (...) scd.identity: identity loaded: cn=SC-a3f8b2c4e6d8 device_id=a3f8b2c4e6d8 default_broker=mqtt-yyz.scadable.com:8883

If it fails:

E (...) scd.identity: nvs_open("scadable_cfg") failed: ESP_ERR_NVS_NOT_FOUND
E (...) scadable: identity load failed — device is unprovisioned ...

Fix is always the same: re-provision via the dashboard. See Troubleshooting for the full diagnostic flow.