OTA Firmware Updates

Signed binary uploads, staged rollouts, and Ed25519 verification — push firmware to your fleet from the dashboard or API.

Prerequisites A jettyd account with at least one provisioned device. Firmware binaries must be valid device images (e.g. ESP-IDF .bin files). The signing key is optional — SHA-256 integrity checking always applies.

Overview

jettyd OTA has three layers of safety:

  1. SHA-256 integrity — every upload stores the hash; the device verifies the downloaded binary matches before flashing.
  2. Ed25519 signature (optional) — when a signing key is configured, the platform signs the SHA-256 digest and includes the signature in the command params. The device verifies the signature against the tenant public key before accepting the update.
  3. Staged rollout — you control what percentage of the target fleet receives the update in the first batch. The platform's background worker dispatches remaining devices only when the current batch reaches a terminal state.

Step-by-step

Upload firmware

Upload your binary via the dashboard (Firmware → Upload) or the API:

curl -X POST https://api.jettyd.com/v1/firmware \
  -H "Authorization: Bearer $API_KEY" \
  -F "file=@build/firmware.bin" \
  -F "version=1.2.0" \
  -F "device_type=sensor-v2" \
  -F "notes=Adds BLE power-save mode"

The response includes the sha256 and, if a signing key is configured, the signature and signature_alg:

{
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "version": "1.2.0",
  "sha256": "e3b0c44298fc1c149afb4c8996fb92427ae41e4649b934ca495991b7852b855",
  "size": 524288,
  "signature": "a1b2c3d4…",
  "signature_alg": "ed25519"
}

Start a rollout

Pick a target (all devices, a device type, tag, individual device, or device group) and an initial batch percentage:

curl -X POST https://api.jettyd.com/v1/firmware/{firmware_id}/rollout \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "target_type": "group",
    "target_id": "your-device-group-uuid",
    "rollout_percentage": 20
  }'

Target types:

target_typetarget_idDescription
allomitAll devices in the tenant
device_typeslug stringAll devices of a given type
tagtag stringAll devices with a given tag
devicedevice UUIDA single device
groupgroup UUIDAll devices in a device group

When rollout_percentage is less than 100, the first batch size is ceil(total × pct / 100). The platform's background worker (runs every 30 seconds) advances to remaining devices once the current batch is fully terminal (confirmed, failed, or timed out).

Monitor progress

Check overall rollout status:

curl https://api.jettyd.com/v1/firmware/rollouts \
  -H "Authorization: Bearer $API_KEY"

Check per-device OTA state:

curl https://api.jettyd.com/v1/devices/{device_id}/firmware-status \
  -H "Authorization: Bearer $API_KEY"

The update_state field is derived from the most recent firmware_update command for the device:

update_stateMeaning
idleNo OTA command has been sent to this device
updatingCommand is pending or delivered, awaiting device response
up_to_dateDevice confirmed the update (command.status = confirmed)
failedCommand failed or timed out

Cancel if needed

Cancel a rolling rollout to stop dispatching to remaining devices. Commands already sent are not recalled.

curl -X POST https://api.jettyd.com/v1/firmware/rollouts/{rollout_id}/cancel \
  -H "Authorization: Bearer $API_KEY"

Device Groups

Group devices logically for targeted rollouts — for example, a staging-fleet group for canary testing before rolling out to all production devices.

Create a group

curl -X POST https://api.jettyd.com/v1/groups \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "staging-fleet"}'

Add devices

curl -X POST https://api.jettyd.com/v1/groups/{group_id}/members \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"device_id": "your-device-uuid"}'

You can also manage groups from the dashboard at Device Groups in the left nav.

Ed25519 Signature Verification

When a firmware signing key is configured in the platform, every upload generates an Ed25519 signature over the SHA-256 digest of the binary. The signature is included in the OTA command params as a hex-encoded 64-byte string.

Security note Without signature verification, a compromised CDN URL could deliver a malicious binary that passes SHA-256 (because SHA-256 is not a secret). Ed25519 verification proves the binary was signed by the tenant's private key. Enable verification on your devices for production fleets.

Get the public key

curl https://api.jettyd.com/v1/firmware/public-key \
  -H "Authorization: Bearer $API_KEY"
{
  "algorithm": "ed25519",
  "public_key": "abc123…"
}

Bake this 64-hex-character public key into your firmware at build time. It never changes for a given tenant.

Verification (ESP-IDF / C example)

On-device verification pseudocode using mbedTLS or a minimal Ed25519 library:

/* 1. Download binary and compute SHA-256 */
mbedtls_sha256(firmware_buf, firmware_len, digest, 0);  /* digest = 32 bytes */

/* 2. Decode hex fields from the OTA command params */
uint8_t pub_key[32];   hex_decode(pub_key_hex, pub_key, 32);
uint8_t sig[64];        hex_decode(sig_hex, sig, 64);

/* 3. Verify Ed25519 signature over the digest (NOT the full binary) */
if (ed25519_verify(sig, digest, 32, pub_key) != 0) {
    ESP_LOGE(TAG, "OTA signature verification FAILED — rejecting update");
    return ESP_FAIL;
}

/* 4. Flash the verified binary */
esp_ota_write(ota_handle, firmware_buf, firmware_len);
Tip The signature is over the SHA-256 digest (32 raw bytes), not the firmware binary itself. This keeps the signing contract simple and consistent — the device needs to compute SHA-256 anyway for integrity checking.

Verification (Python / CI example)

from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
import hashlib, requests

pub_key_hex = "abc123…"  # from /v1/firmware/public-key
sig_hex = "a1b2c3d4…"   # from OTA command params

firmware = open("firmware.bin", "rb").read()
digest = hashlib.sha256(firmware).digest()

pub_key = Ed25519PublicKey.from_public_bytes(bytes.fromhex(pub_key_hex))
sig = bytes.fromhex(sig_hex)

pub_key.verify(sig, digest)  # raises InvalidSignature on failure
print("Signature valid")

Configuring the Signing Key

Generate a random 32-byte seed and set it as the FIRMWARE_SIGNING_KEY environment variable on the platform. The platform derives the Ed25519 signing key and verifying key from this seed.

# Generate a secure random seed (32 bytes = 64 hex chars)
openssl rand -hex 32

Set FIRMWARE_SIGNING_KEY=<output> in your platform environment variables. Treat this like a private key — rotate it only if compromised, and never expose it outside the server. The corresponding public key is available via GET /v1/firmware/public-key at any time.

Optional Signing is optional. If FIRMWARE_SIGNING_KEY is not set, the OTA command params omits signature and signature_alg. SHA-256 integrity checking still applies. Your device firmware should check for the presence of the signature field before attempting verification.

API Quick Reference

EndpointMethodDescription
/v1/firmwarePOSTUpload a firmware binary (multipart)
/v1/firmwareGETList firmware images
/v1/firmware/{id}DELETEDelete a firmware image
/v1/firmware/public-keyGETGet the tenant's Ed25519 public key
/v1/firmware/{id}/rolloutPOSTStart a rollout
/v1/firmware/rolloutsGETList all rollouts
/v1/firmware/rollouts/{id}/cancelPOSTCancel a rolling rollout
/v1/devices/{id}/firmware-statusGETPer-device OTA state
/v1/groupsPOST / GETCreate / list device groups
/v1/groups/{id}GET / PATCH / DELETEGet, rename, or delete a group
/v1/groups/{id}/membersGET / POSTList / add group members
/v1/groups/{id}/members/{device_id}DELETERemove a device from a group