Docs menu

Device Makers

This page is for a team that builds the devices: what the factory writes into each unit, what the firmware does for the life of the unit, and how that changes as you move from a prototype, to a pilot run, to production and medical units. Each step says whether it works in OpenX today or is coming.

Two things change most makers' plans, so they come first.

Plan for these two first

1. The firmware must be updatable

The law requires a maker that submits a medical "cyber device" to FDA (in short: a device with software in it, able to connect to the internet, and open to cybersecurity threats) to make updates and patches available: on a reasonably justified regular cycle for known vulnerabilities, and as soon as possible for critical ones (FD&C Act section 524B(b)(2), 21 U.S.C. 360n-2). The law does not say how the update reaches the unit.

FDA recommends (its February 2026 cybersecurity guidance, which is nonbinding) that devices accept only cryptographically authenticated updates, refuse downgrades unless they are needed for safety and log the ones that happen, and plan for an update that is interrupted part way. A maker who chooses differently should be ready to explain why in the premarket submission.

So a sealed enclosure is fine, but firmware that can never change is not a plan for a connected medical device. For any connected product it is the safe plan anyway: a unit you cannot update cannot be fixed when a flaw is found. See Firmware updates.

2. Identical firmware should not carry a shared secret

FDA recommends avoiding master keys stored on the device and keys worked out only from a device's identifiers, and says serial numbers should be avoided in making keys, so that taking one unit apart never reveals the keys of the others. It also says keys used for authentication need to be kept confidential, and that hardware-based security should be used when possible. These are recommendations, not a ban; a medical maker that departs from them would have to justify it.

OpenX follows this today: each unit gets its own random key from the hub, never one worked out from its printed ID or serial number, and no two units share a key. Production and medical units should go further and keep their identity in hardware (below).

The stages at a glance

StageThe unit's identityHow a unit is addedStatus
PrototypeIts own key-based connection stringOne Connection per unit, then DeployAvailable today
PilotIts own random key, written once at flashing; it renews its own passOne Connection per unit, then DeployAvailable today
Production / medicalHeld in hardware: a SIM applet, a secure element, a TPM or a modem identityEnrolled in a device provisioning serviceComing, not available yet

Prototype

For a unit on your bench:

  1. Add a Connection for the unit and Deploy. Deploy registers the device in the hub, and the hub gives it its own random key.
  2. Open Inspector -> Connection Info and give the unit Device ConnStr (primary), the key-based connection string.
  3. If the unit runs an Azure IoT SDK, the SDK makes and renews its pass by itself. Without an SDK, the firmware makes its pass from the key (how).

A ready-made token from Connection Info is fine for a quick test from a laptop, never for a unit (testing tokens): a unit that stores a token stops connecting when it expires.

Pilot

A pilot run is the prototype recipe, once per unit, done with care. Pilot Devices is the full walk-through. In short:

  • One key per unit. Each unit is its own Connection, so each gets its own random key from the hub. Write it to the unit once, at flashing, in a step separate from the firmware image, so the image stays the same for every unit and holds no secret. If you keep a copy, keep it in your secret store, never in the firmware repository.
  • A unit that loses its key gets the same key back. If a storage wipe, a factory reset or a bad flash leaves a unit without its key, it sends nothing and waits (NO_KEY, below). The hub still holds the key: open the unit's Inspector -> Connection Info, copy Device ConnStr (primary) again and write it back to the unit. Nothing else changes.
  • The unit renews its own pass every day. It makes a pass that lasts 24 hours and makes the next one before the old one runs out. This is not a 24-hour connection: the unit keeps its key for years and is never re-flashed to keep connecting.
  • Keep the clock right. The hub checks the pass's expiry against its own clock, so the unit sets its clock from the network after every boot, before its first pass. If a site blocks NTP, use another source, such as the cellular network's time. If no source answers, the unit waits and tries again; it never makes a pass on a clock it has not set, because a unit without a battery-backed clock starts at a wrong date after a power loss, and its pass would be refused.
  • Turn a unit off by switching its Connection off in the Inspector and deploying. The unit keeps its keys but cannot send until it is switched back on and deployed again. While it is off, the hub refuses it, and over MQTT the unit cannot tell that from a changed key, so it falls back to retrying about once an hour. After you switch it back on and deploy, it reconnects at its next hourly try, or at once if you restart it.
  • Know what changes a key. Deleting a Connection and adding it back gives that unit new keys, and the delete confirm says so. A unit whose key changed needs its new key string, by hand or through your update channel.

Pilot grade. A key that firmware keeps in ordinary storage is fine for a pilot, but it falls short of FDA's recommendation to use hardware-based security when possible. Keep pilot units few and known, and plan the move to hardware identity before production.

Production and medical

Coming, not available yet. OpenX does not yet run a device provisioning service, so production units cannot be enrolled in one today; this page will cover it when it ships. What to plan for now:

  • Identity in hardware, chosen by you: a SIM applet (GSMA IoT SAFE, or a carrier service such as Soracom Krypton), a secure element that its vendor provisions before it ships (for example Microchip Trust&Go, Infineon OPTIGA Trust M or NXP SE050), a TPM, or a modem identity. The secret is made in that hardware and never leaves it.
  • What the factory writes stays the same in every unit except the printed ID: the provisioning service's address and ID (on Azure, the global endpoint and an ID scope, which OpenX will show once the service exists), the TLS trust roots (below), and the printed ID. Nothing secret.
  • Per-unit factory work is none beyond the printed ID. If the secure element ships with a vendor manifest, you upload that list and the units are enrolled before they ship.
  • The printed ID is a label, never a key. It is never used to make a key and never used as the unit's identity in the cloud.

What the firmware does for life

Today: the flow for prototype and pilot units

BOOT
  -> READ_KEY         read the key string written at flashing
       missing or unreadable -> NO_KEY
  -> SYNC_CLOCK       set the clock from the network (NTP or the cellular network)
       no source answers     -> BACKOFF, then SYNC_CLOCK (no pass on an unset clock)
  -> MAKE_PASS        a pass from the unit's key, lasting 24 hours, in memory only
  -> CONNECT          MQTT over TLS on port 8883 to the hub in the connection string
       accepted (CONNACK 0)  -> RUN
       refused (CONNACK 2, 4 or 5)
                             -> SYNC_CLOCK, MAKE_PASS, try once more
                                refused again -> WAIT_FOR_RECOVERY
       protocol version refused (CONNACK 1), a firmware fault that only an update fixes
                             -> WAIT_FOR_RECOVERY
       server unavailable (CONNACK 3), closed with no CONNACK, TLS failure or no network
                             -> BACKOFF, then CONNECT
RUN
  send telemetry; before the pass runs out (about 80% of its life) -> MAKE_PASS, reconnect
  connection dropped -> BACKOFF, then CONNECT
  update offered on your update channel -> UPDATE
BACKOFF
  wait a growing, jittered delay, then try again where it failed (SYNC_CLOCK or CONNECT)
  still on your update channel: stale trust roots fail every TLS handshake, and an update is the way out
  update offered on your update channel -> UPDATE
NO_KEY
  send nothing to the hub; your update channel runs on its own credentials, so it still works
  a key string is written (by hand, or through your update channel) -> SYNC_CLOCK
  update offered on your update channel -> UPDATE
WAIT_FOR_RECOVERY
  the hub keeps refusing: the key changed, the unit's Connection is off or deleted, or (CONNACK 1)
  the firmware speaks an MQTT version the hub does not accept
  about once an hour -> SYNC_CLOCK and try again (a unit switched back on heals here)
  a new key string is written -> SYNC_CLOCK
  update offered on your update channel -> UPDATE
UPDATE
  see Firmware updates
  the unit restarted (new image kept, or the old one restored) -> BOOT
  nothing installed (offer refused, download stopped), the unit has a key -> SYNC_CLOCK
  nothing installed, the unit has no key -> NO_KEY

The same loop as pseudo-code:

on boot:
  key = readKeyFromStorage()               # written once at flashing
  if key is missing or unreadable:
    key = waitForKey()                     # NO_KEY: sends nothing to the hub until a key string
                                           # is written; an update offered meanwhile runs runUpdate()
  refusedOnce = false
  loop forever:
    if not syncClockFromNetwork():         # never make a pass on an unset clock
      backoffWait()                        # BACKOFF
      continue
    pass = makePass(key, lifetime = 24 hours)    # never written to storage
    result = connectMqtt(host, deviceId, pass)
    if result is accepted:
      refusedOnce = false
      runUntil(passAge >= 0.8 * 24 hours or disconnected)  # an update offered meanwhile runs runUpdate()
      if the connection held 10 minutes: backoff = reset
      if disconnected: backoffWait()       # BACKOFF: never a tight reconnect loop
    else if result is refused (CONNACK 2, 4 or 5):
      if refusedOnce:
        key = waitForRecovery(key)         # returns after about an hour, or at once with a new key
      refusedOnce = true                   # the next try runs on a freshly set clock
    else if result is protocol refused (CONNACK 1):  # a firmware fault; only an update fixes it
      key = waitForRecovery(key)
    else:                                  # CONNACK 3, closed with no CONNACK, TLS, no network
      backoffWait()                        # BACKOFF

backoffWait():                             # BACKOFF
  sleep(backoff with jitter); backoff = min(backoff * 2, maximum)
  if an update is offered meanwhile: runUpdate()   # waitForRecovery() takes an update the same way

Over raw MQTT the hub answers a connect with an MQTT CONNACK code or by closing the connection, not with an HTTP status: the 429 and 5xx codes in Microsoft's error list appear on HTTPS calls, in twin replies and in SDK errors. Microsoft lists a changed key, a switched-off device and a missing device as separate errors (401003, 403012 DeviceDisabled, 404001 DeviceNotFound), but over MQTT the unit sees only that it was refused or disconnected, so it cannot tell them apart. That is why it keeps retrying about once an hour instead of giving up. A connection closed with no CONNACK is treated as passing trouble; with the backoff capped at an hour, it settles into the same hourly retry.

On MQTT a renewal is a disconnect and a reconnect with the new pass, about once a day; on AMQP the pass is refreshed without disconnecting. A unit that sleeps and wakes makes a fresh pass at each wake.

Every state, every transition, the timings and the update steps are in Firmware Reference Flow.

Why the recovery path matters: once a unit's key has changed, nothing sent through the hub can reach it, because it can no longer sign in to the hub. The way back must not need the hub: a re-flash by hand (enough for a side-loaded pilot), or an update channel of your own that works on its own credentials. Microsoft's Device Update runs through the hub connection, so it is not that way back (below).

Later: with a provisioning service

Coming, not available yet. When OpenX runs a device provisioning service, the flow gains these steps: register with the provisioning service on first boot using the hardware identity (sending the printed ID and firmware version), keep the hub it assigns instead of registering on every boot, register again when a connection keeps failing for a reason other than load (this is how a move to another hub heals), and accept a request to register again, both as a direct method and, for a unit that sleeps, as a setting it reads when it wakes.

Firmware updates

OpenX does not deliver firmware updates today. Use your board's update path or your own. Whichever it is, these are the steps FDA's recommendations point to, shown here for an ESP32-S3 built with ESP-IDF.

  1. Sign every image, and keep the signing key yourself. OpenX never holds it. In ESP-IDF, signed app verification checks each new image's signature before it is used, and Secure Boot v2 checks it at every boot.
  2. Install to the spare slot. Use two OTA app partitions, so the running image stays intact while the new one downloads.
  3. Prove the new image before keeping it. With app rollback on, a new image boots in a "pending verify" state. The self-test checks what the image itself is responsible for: it starts, sets its clock, reaches your update channel, and completes the TLS handshake with the hub (which proves its trust roots) and gets an answer to its connect. A refusal of its key (CONNACK 2, 4 or 5) is not the image's fault (the old image would be refused too), so it does not fail the test; a refusal of its MQTT version (CONNACK 1) is the image's fault, so it fails. If the hub cannot be reached at all, the test has no answer yet, and the image stays on trial and tries again. When the test passes, the firmware marks itself valid. When it fails, or the unit resets before deciding, the unit goes back to the previous image on the next reboot.
  4. Log every rollback. Going back to the previous image is a version rollback, and FDA recommends that rollbacks be logged and documented. Record both versions and the reason, report it on your update channel when the unit next reaches it, and do not install the same image again unless it is offered again.
  5. Refuse downgrades. With anti-rollback on, each image carries a security version, and the chip's eFuse records the floor: an image below it is erased at update time and never booted, and nothing overrides that, not even a signed image or a safety reason. Marking a new image valid raises the floor to its security version. The field is 16 bits, so on an ESP32-S3 the floor can be raised only 16 times in the chip's life: raise the security version only for a security fix, never for an ordinary release. Your firmware checks the ordinary version number itself: refuse an older one unless you sent it as a downgrade for safety, and log it. If safety ever calls for code from below the floor, release it again as a new signed image that carries the current security version, and log that too.
  6. Plan for interruptions. A download that stops part way must leave the unit on its old image, and the unit tries again later.
  7. Update the modem too. On a board whose MQTT runs on a cellular modem, the modem has firmware of its own. On a Walter board, the walter-modem driver has a modem_firmware_flash example that updates or recovers the modem's firmware, and a bluecherry example for the vendor's BlueCherry cloud, which supports OTA updates. Check how the channel you choose verifies an image.

Microsoft's Device Update for IoT Hub provides update agents for Linux, lists Eclipse ThreadX samples for microcontroller boards, and supports A/B updates with rollback. OpenX does not set it up today. Device Update sends its commands through the hub, as device twin properties, so it suits routine updates but cannot reach a unit that can no longer sign in to the hub. A unit that uses it still needs a way back that does not need the hub (why).

Trust roots and the root key warning

  • Trust both roots that Microsoft lists for IoT Hub: DigiCert Global Root G2 and Microsoft RSA Root CA 2017.
  • Never pin an intermediate or a server certificate. Microsoft changes those often, with little or no notice.
  • A root can change in an emergency. Microsoft says root changes are rare, but to prepare for an emergency change if a root is ever compromised. Keep the trust roots in a place your update channel can replace, and make sure that channel does not depend on the hub connection, which would be the thing that broke (Device Update does depend on it).
  • Device Update has root keys of its own. If you use Microsoft's Device Update, its agent carries Microsoft's root keys. Microsoft says a planned rotation is announced at least a year ahead, but a compromised root key would be revoked at once; an agent that can fetch new root keys (version 1.1.0 or later of Microsoft's reference agent) is the protection.

Sources

Read on 2026-10-20.

On this page