Semicolons end a statement in Mermaid sequence diagrams and angle brackets
are treated as HTML, so messages like "shared = X25519(s, B); pw = ..." and
"enroll/request/<cid>" broke rendering. Use commas and {cid} style
placeholders instead.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
12 KiB
Board enrollment and activation over MQTT: FN-as-username vs SN-as-username
Status: Design proposal from the R&D discussion; nothing is implemented. Builds on mqtt-dynamic-security-rnd.md.
Scope: How a raw board becomes an FN board (low access) and then an SN board (role-limited access), with Mosquitto Dynamic Security and the Go backend. Both candidate username models are described so the choice can be made.
1. Facts and terms
| Term | Meaning |
|---|---|
| FN | Backend-assigned identity of a physical board. Stored in a separate board table. Never reused. |
| SN | Vending machine identity (the "plate"). Pre-registered by an admin; printed on the machine, so not a secret. |
| Enrollment | Raw board obtains an FN and a broker credential. Done over MQTT with the Go backend only (no HTTPS, no factory step). |
| Activation | An operator links an FN to an SN; the board gets machine-scoped access. |
| Nonce | Random single-use value that ties a reply to one request (stops replay). |
| Random Client ID | 128-bit CSPRNG value the board picks for enrollment only. It acts as an unguessable mailbox address. |
| OTA | Over-the-air firmware update. |
Three stages: raw board → FN board (low access, FN topics only) → SN board (roles: vending, news, updates).
2. What ships in the firmware (identical on every board)
- Backend primary and backup public signing keys (Ed25519). Public values; extracting them gives an attacker nothing.
- Broker host and the CA certificate used to verify the broker's TLS certificate.
- Enrollment topic names; templates for the FN and SN topics.
- X25519 key agreement, Ed25519 verification, HKDF, and a hardware random number generator.
- Flash space for the board's stored identity (FN, password, later SN).
- A way to show a short authentication code to the operator (display or service port).
No passwords and no secrets are built in. Fleet-wide shared secrets are rejected: one firmware dump would expose every board and rotation would need an OTA update.
3. Stage 0: enrollment (same in both models)
Mosquitto does not require passwords: with allow_anonymous true on a dedicated TLS listener, anonymous clients fall into Dynamic Security's anonymous group (setAnonymousGroup). A shared static password would add no real security. (Verify exact behavior on the deployed version.)
Enrollment role (anonymous group), nothing else allowed:
- publish
enroll/request/%c - subscribe and receive
enroll/response/%c
Only the backend may subscribe to enroll/request/+. Devices cannot publish to response topics.
sequenceDiagram
participant B as Board
participant M as Mosquitto
participant G as Go backend
B->>B: random Client ID, X25519 keypair (b, B), nonce n1
B->>M: CONNECT (TLS, anonymous, clientId=random)
B->>G: enroll/request/{cid} {B, n1, mac}
G->>G: create FN record, ephemeral (s, S)
G->>G: shared = X25519(s, B), pw = HKDF(shared, FN, n1)
G->>M: Dynamic Security: create client FN, pw, role board-bootstrap
G->>B: enroll/response/{cid} {FN, S, sig} (not retained)
B->>B: verify sig with pinned public key
B->>B: shared = X25519(b, S), same pw
B->>B: persist FN + pw to flash
B->>M: reconnect as FN / pw
sig= Ed25519 signature by the backend overS,B,n1, and the Client ID. It stops a man in the middle from substituting their own key.- The password is never transmitted. First successful FN login is the enrollment acknowledgement.
- Plaintext passwords are never stored; Dynamic Security keeps a hash, so re-issue means rotate.
Role board-bootstrap (FN board): publish board/%u/hello and board/%u/activation/request; receive on board/%u/activation/response and board/%u/cmd. Bind the broker client's Client ID to the FN (or a board UUID). Deny $CONTROL/dynamic-security/# to every device role and test it.
Stage 0 cautions
- The MAC / hardware label is a claim, not proof. Re-issue for the same MAC only while that FN has never logged in; afterwards only an operator action may reset it.
- FNs that never log in within N hours are disabled and deleted.
- Rate-limit enrollment, cap pending enrollments, validate payload size and schema, set per-listener connection and packet-size limits where supported.
- The enrollment handler may call only a narrow "create bootstrap client" function, never the general Dynamic Security admin client.
- The board must pick its Client ID with a CSPRNG, never derive it from the MAC or serial.
- Keep TLS with server-certificate verification regardless of the key agreement.
- A fake FN is harmless: it can only do
helloandactivation/request. The trust gate is activation.
4. Stage 1 to 2: activation gate (same in both models)
- SNs are pre-registered by an admin as
unassigned. A board cannot invent one. - An operator with the right RBAC role selects an SN for a pending FN.
- The board shows a short authentication string (for example, the last digits of
hash(B)or its FN). The operator confirms it matches the pending request, binding the physical board in hand to the FN on screen. Treat this as required. - The activation code is single-use, expiring, bound to one SN, and attempt-limited.
- A unique constraint guarantees one active FN per SN. Replacement is an explicit operator action that revokes the old board first and drops its live session.
- Every step is audited (actor, time, FN, SN, broker operation result).
The database is authoritative for ownership. A successful broker command alone does not prove the database committed, and the reverse: use desired-state records, idempotent commands, retries and a reconciler that diffs broker state against the database.
5. Model A: FN stays the username
The board's credential is created once at enrollment and never changes. Activation only changes which roles the FN client holds.
sequenceDiagram
participant O as Operator
participant G as Go backend
participant M as Mosquitto
participant B as Board (FN)
B->>G: board/{FN}/activation/request
O->>G: pick SN, confirm code and short string
G->>M: ensure role machine-{SN} (literal ACLs device/{SN}/...)
G->>M: add roles machine-{SN} + capability roles to client FN
G->>B: board/{FN}/activation/response {SN, sig}
B->>B: store SN, reconnect as FN
B->>M: use device/{SN}/... topics
- Roles: one assignment role per machine,
machine-<SN>, with literal topics such asdevice/SN000123/sales/report.%ucannot express the SN here because the username is the FN. Plus shared capability roles (vending,news,updates). - Backend subscribes with wildcards (
device/+/...), takes the SN from the topic, and checks in the database that this SN is assigned to the publishing board. - Replacing a board for SN000123: disable and kick
FN-old, remove its roles; the new board enrolls as a new FN; activation attachesmachine-SN000123to the new FN. The role's ACL text is unchanged; only which client holds it changes. - Reusing a retired board on another machine: treat it as revoked and require re-enrollment (new FN, new password), not re-enabling the old credential.
6. Model B: SN becomes the username
At activation the board gets a second, machine-level broker identity. The same X25519 exchange is reused.
sequenceDiagram
participant O as Operator
participant G as Go backend
participant M as Mosquitto
participant B as Board (FN)
B->>B: new ephemeral X25519 keypair (b2, B2)
B->>G: board/{FN}/activation/request {B2, n2}
O->>G: pick SN, confirm code and short string
G->>G: ephemeral (s2, S2), shared = X25519(s2, B2)
G->>G: pw = HKDF(shared, SN, n2, generation)
G->>M: create or update client SN: pw, clientId bound to board, role vending-device
G->>B: board/{FN}/activation/response {SN, S2, sig}
B->>B: verify sig, derive same pw, store SN + pw
B->>M: reconnect as SN / pw
G->>M: disable FN client (or keep for rescue / re-enrollment)
- One shared role
vending-devicewithdevice/%u/..., because the username equals the SN in the topic. - Use a fresh keypair on the board at each activation, not the enrollment one.
- Bind the SN client's Client ID to the current board (for example its FN) with
setClientId, if the deployed version supports it. A copied SN password is then useless from any other device, and the binding is readable withgetClient. - Replacing a board: the SN client persists. Rotate its password immediately and kick the old session; the new board enrolls as a new FN and activation runs again, which sets a new password (rotation is intrinsic to the exchange) and rebinds the Client ID. ACLs never change.
- Store a credential generation counter per SN in the database for audit. Passwords cannot be read back from the broker, so rotation can be recorded but not directly verified; the readable Client ID binding gives a checkable signal.
7. Comparison
| A: FN username | B: SN username | |
|---|---|---|
| Credential deliveries | once (enrollment) | enrollment, then each activation and replacement |
| SN topic ACLs | one literal role per machine | one shared role with %u |
| Broker logs show | the board | the machine |
| Replacement | move roles from old FN to new FN | rotate the SN password and rebind Client ID |
| Grant and revoke | two separate commands; a partial failure can leave two live boards (revoke first, then grant) | one atomic action: a new password both grants and revokes |
| Verifiable by query | roles and enabled state are readable | password is not readable; Client ID binding is |
| Secret lifetime | long-lived board credential across assignments | machine credential rotates every activation |
| Extra objects | many roles | an FN client to retire per board |
Both models are close in safety once the backend automates revocation. Neither leaves "nothing to forget": in A the role removal must run, in B the new exchange must overwrite the password. The decision is operational: machine-readable logs and one shared ACL (B) versus fewer protocol steps and a stable board identity (A). Current leaning: B with Client ID binding and a generation counter, pending the user's decision.
8. Firmware signing keys
Ship two public keys: a primary and a backup. Keep the backup private key offline and separate. If the primary leaks, the backend signs with the backup and boards that see a valid backup-signed message permanently drop the primary. This limits damage; offline boards still trust the leaked key until they connect. Use a different key for firmware-update (OTA) signing than for enrollment signing.
9. Open decisions
- Final username model (A or B).
- Whether the FN client is disabled, deleted or kept for rescue after activation (model B).
- Exact activation event and whether it replaces or extends the current activation-code approval/ACK flow in
device-onboarding-activation.md(backend repo). - Board MCU and secure-element capabilities (X25519 and Ed25519 support, hardware RNG, flash encryption and secure boot).
- Deployed Mosquitto version: anonymous group,
setClientId, and per-listener limits must be validated. - Capability roles and which machine types or entitlements get them.
- Audit field list, retry and reconciliation persistence, and RBAC for activation, rotation and disablement.
- Credential handling in logs and support tooling (never log derived passwords or key material).
- Whether a retired board can ever be re-enrolled, and by whom.