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:
- The dashboard mints a per-device cert against the SCADABLE intermediate CA.
- The cert's Common Name is
SC-<device_id>(e.g.SC-a3f8b2c4e6d8). - The cert + key + CA chain are bundled into an NVS partition image.
- 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:
| Key | Content |
|---|---|
device_id | hex string, e.g. a3f8b2c4e6d8 |
common_name | full cert CN, format SC-<device_id> |
cert_pem | device certificate (PEM) |
key_pem | device private key (PEM) |
ca_pem | CA chain (PEM) for validating the broker |
mqtt_host | fallback broker host (used only if /v1/route fails) |
mqtt_port | fallback 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:
- Edge router call. The library reads
common_nameand sends it asX-Device-CN: SC-<device_id>on the HTTPS GET toedge.scadable.com/v1/route. The backend treats this header as the device identity for promotion + region lookup. - 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. - MQTT client ID. The library uses
common_nameas the MQTTclient_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:
- Dashboard → device → Revoke (planned UI; today the cert is rotated by re-issuing).
- Re-provision the device via the dashboard's flasher with a fresh bundle.
- 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.
