From 6c2fd0648ffd50a1079f50250e013a7563e32949 Mon Sep 17 00:00:00 2001 From: ryan Date: Sun, 25 Oct 2026 10:26:01 +0330 Subject: [PATCH] feat: added docs for this research --- docker/docs/enrollment-and-activation.md | 168 ++++++++++++ docker/docs/mqtt-dynamic-security-rnd.md | 183 +++++++++++++ docker/docs/sn-as-username-process.md | 313 +++++++++++++++++++++++ 3 files changed, 664 insertions(+) create mode 100644 docker/docs/enrollment-and-activation.md create mode 100644 docker/docs/mqtt-dynamic-security-rnd.md create mode 100644 docker/docs/sn-as-username-process.md diff --git a/docker/docs/enrollment-and-activation.md b/docker/docs/enrollment-and-activation.md new file mode 100644 index 0000000..23279b1 --- /dev/null +++ b/docker/docs/enrollment-and-activation.md @@ -0,0 +1,168 @@ +# 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`](./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. + +```mermaid +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/ {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/ {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 over `S`, `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 `hello` and `activation/request`. The trust gate is activation. + +## 4. Stage 1 to 2: activation gate (same in both models) + +1. SNs are pre-registered by an admin as `unassigned`. A board cannot invent one. +2. An operator with the right RBAC role selects an SN for a pending FN. +3. 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.** +4. The activation code is single-use, expiring, bound to one SN, and attempt-limited. +5. 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. +6. 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. + +```mermaid +sequenceDiagram + participant O as Operator + participant G as Go backend + participant M as Mosquitto + participant B as Board (FN) + B->>G: board//activation/request + O->>G: pick SN, confirm code and short string + G->>M: ensure role machine- (literal ACLs device//...) + G->>M: add roles machine- + capability roles to client FN + G->>B: board//activation/response {SN, sig} + B->>B: store SN, reconnect as FN + B->>M: use device//... topics +``` + +- **Roles:** one assignment role per machine, `machine-`, with **literal** topics such as `device/SN000123/sales/report`. `%u` cannot 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 attaches `machine-SN000123` to 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. + +```mermaid +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//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//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-device` with `device/%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 with `getClient`. +- **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. diff --git a/docker/docs/mqtt-dynamic-security-rnd.md b/docker/docs/mqtt-dynamic-security-rnd.md new file mode 100644 index 0000000..25112d6 --- /dev/null +++ b/docker/docs/mqtt-dynamic-security-rnd.md @@ -0,0 +1,183 @@ +# MQTT device authentication and authorization R&D + +**Status:** Design notes from the R&D discussion; no implementation authorized or performed. +**Scope:** Mosquitto Dynamic Security, Go backend, database identity, device provisioning, and ACL lifecycle. + +## Goal + +Give each board only the MQTT access it needs, move a vending machine's operational broker identity with that vending machine when hardware is replaced, and make broker-side changes traceable and reconcilable with the database. Keep authentication (who may connect) distinct from authorization (which MQTT actions and topics are allowed). + +## Domain terms + +| Term | Meaning in this design | +|---|---| +| **FN** | Backend-assigned identity for a physical board. It follows the board. | +| **SN** | Identity for a vending machine. The machine owns its broker account and database device record; a board is associated with it after installation/activation. | +| **DB device ID** | Database UUID for the vending-machine record. A candidate stable MQTT Client ID; it is not yet an adopted decision. | +| **Dynamic Security client** | Broker-side authentication record, normally identified by username and holding password, optional Client ID binding, and role/group memberships. | +| **Role** | Reusable collection of topic ACL rules. It does not itself authenticate a device or bind a Client ID. | + +The existing schema has separate `fn_plate` and `sn_plate` fields and a UUID primary key in `schema_v1.sql` (backend repo). Current repository flow documents activation messages using `device//...` and calls both `{serial}` and `{plate}` device identifiers in places (activation flow (backend repo), MQTT protocol contract (backend repo)). Therefore the design must explicitly decide which lifecycle event assigns SN and when the MQTT identity/ACL transition occurs. The examples below are proposals, not a claim about current runtime behavior. + +## Decisions and recommendations by step + +| Step | Decision / recommendation | Status | +|---|---|---| +| 1. Before a vending machine is assigned | Treat the board as an FN identity. Give each board its own bootstrap credential and a small bootstrap ACL set. Do not give it vending-operation topics yet. | Agreed direction; exact bootstrap topic contract remains open. | +| 2. Bootstrap Client ID binding | Dynamic Security allows the broker client record's Client ID binding to be unset. In that case, any Client ID can connect with that username/password. This is allowed for bootstrap if chosen, but it weakens device-instance binding. Prefer binding to a stable board identifier (ideally a backend-managed board UUID, or FN if that is stable and never reassigned) when available. | Optional behavior confirmed; project choice open. | +| 3. On MQTT CONNECT | MQTT CONNECT always includes a Client ID. Username and password are optional in MQTT generally, but this authenticated device design should send the board/vending username and its password. “No Client ID binding in Dynamic Security” means the broker does not require one specific ID; it does **not** mean the CONNECT packet omits Client ID. | Agreed protocol interpretation. | +| 4. Bootstrap password | Do not create a named Dynamic Security client with an unset password: that client cannot connect. Anonymous access is a separate mode, and all connections without usernames enter the shared `unauthenticated` group; this does not identify or isolate a board by FN. Use a unique per-board bootstrap secret. | Agreed recommendation. | +| 5. Assign SN / activate vending | Provision an operational broker client for the vending identity, give it the vending role, and scope its topics to that SN. Candidate mapping: username = SN, password = unique secret, Client ID = DB device UUID. | Recommended example; fields and exact cutover event need approval. | +| 6. Replace a board or move it to another vending machine | The operational account and ACL belong to the vending machine. Before handing the machine to replacement hardware, disable or rotate the existing secret so the old board cannot reconnect, then provision the replacement board. A board moved to another vending must not retain the previous vending's credentials. | Ownership principle agreed; detailed handover procedure open. | +| 7. Dynamic ACL setup | Define reusable roles once (e.g. `board-bootstrap`, `vending-device`); create/update per-device clients and attach the proper role. Avoid manually copying a different ACL list for every vending machine. Use username `%u` substitution only where the username deliberately equals the identity embedded in the topic. | Recommended model. | +| 8. Default authorization | Configure explicit deny defaults for publish-send, publish-receive, subscribe, and unsubscribe, then allow only reviewed topic/action pairs. | Security recommendation; current broker config not verified. | +| 9. Go backend and broker state | The Go backend can call the Dynamic Security MQTT control API using its MQTT client, but broker state is not automatically a database transaction. Plan idempotent provisioning, recorded desired status, retries/reconciliation, and audit records before implementation. | Design requirement; mechanism open. | +| 10. Human/operator access | Operators should use application endpoints with application RBAC and audit trails. Do not give routine operators the Dynamic Security administration credential. Separate the backend's broker-management principal from device identities and grant it only the required control API actions where feasible. | Recommendation; operator model open. | + +## MQTT connection fields + +For a provisioned device using the proposed named-client model: + +```text +CONNECT { + clientId: , + username: , + password: +} +``` + +If a Dynamic Security client has a configured Client ID, Mosquitto requires the username, password, and Client ID to match. If the Client ID binding is unset, the same username/password can connect using any Client ID. Avoid sharing credentials between boards or machines. + +Use MQTT over TLS so credentials are protected in transit. Keep broker-management credentials separate from device credentials and protect the Dynamic Security control API. + +## Reusable role model + +Dynamic Security separates client records from roles: + +1. Create a bootstrap role and an operational vending role with reviewed ACL rules. +2. For each board, create one unique broker client (e.g. username derived from FN), set a random secret, choose whether to bind a board Client ID, and attach `board-bootstrap`. +3. After the approved SN-assignment/activation transition, provision the vending client (e.g. username = SN, unique secret, Client ID = DB UUID) and attach `vending-device`. +4. Retire or disable the bootstrap client's access according to the chosen post-activation lifecycle. +5. During board replacement, revoke/rotate access before transferring the operational credential to replacement hardware. + +An example role could use `%u` to make one ACL pattern cover every member while keeping each member scoped to their username-derived identity. For example, `vending/%u/health` only has the intended per-SN meaning if the operational username is that SN. If the stable Client ID is a DB UUID and topics contain SN, `%c` would point at the wrong value; use username=SN, per-client literal ACLs, or another carefully designed identity/topic mapping instead. + +## ACL event checklist + +Review each topic independently in both directions. A device's ability to subscribe and its ability to receive broker-published messages are separate checks. + +| Direction | Dynamic Security ACL event to review | +|---|---| +| Device publishes to broker | `publishClientSend` | +| Device subscribes to a filter | `subscribeLiteral` or `subscribePattern` | +| Broker message is delivered to device | `publishClientReceive` | +| Device unsubscribes | `unsubscribeLiteral` or `unsubscribePattern` | + +Build the actual allowlist from the maintained MQTT protocol contract, and annotate each topic with lifecycle phase, publisher, subscriber, topic identity (FN/SN), request/response behavior, and whether it contains sensitive data. Do not use a broad `#` permission for devices. Ensure request-response topics include the device's required subscription/receive rules, not just its publish rule. + +## Example (illustrative, not ready to apply) + +This JSON illustrates a reusable vending role plus a single SN-specific client. The role shows only a small example subset; it is **not** the complete ACL for the repository's MQTT protocol. The exact Dynamic Security JSON/API schema should be checked against the deployed Mosquitto version before use. + +```json +{ + "defaultACLAccess": { + "publishClientSend": false, + "publishClientReceive": false, + "subscribe": false, + "unsubscribe": false + }, + "roles": [ + { + "rolename": "vending-device", + "acls": [ + { + "acltype": "publishClientSend", + "topic": "vending/%u/health", + "priority": 0, + "allow": true + }, + { + "acltype": "publishClientSend", + "topic": "vending/%u/sales/report", + "priority": 0, + "allow": true + }, + { + "acltype": "subscribePattern", + "topic": "device/%u/activation/push", + "priority": 0, + "allow": true + }, + { + "acltype": "publishClientReceive", + "topic": "device/%u/activation/push", + "priority": 0, + "allow": true + } + ] + } + ], + "clients": [ + { + "username": "SN000123", + "password": "", + "clientid": "", + "roles": [ + { + "rolename": "vending-device", + "priority": -1 + } + ] + } + ] +} +``` + +Do not commit real device passwords or broker-management credentials. In a real setup, secrets should be generated and provisioned through a protected secret-handling path, and plaintext should not be written to normal application logs. + +## Replacement and reassignment cases to design + +### Board replacement for the same vending machine + +1. Authorize the replacement in the backend/operator workflow. +2. Disable the old broker client or rotate its password so existing and future use is denied. +3. Associate the new FN with the existing vending-machine DB record. +4. Provision the current vending credential to the replacement board using a controlled channel. +5. Record old FN, new FN, vending DB ID/SN, actor, timestamps, and broker operation results. + +### Board moved to a different vending machine + +1. Revoke the old vending's credential from that board. +2. Confirm it cannot publish/subscribe under the previous vending identity. +3. Complete the new vending's ownership/activation checks. +4. Provision only the new vending's broker identity and ACLs. + +The device database is authoritative for ownership and lifecycle state. Broker state must be checked and reconciled with that state; a successful database write alone does not prove the broker accepted the ACL change, and a successful broker update alone does not prove the database transition committed. + +## Open decisions before implementation + +- What exact event completes SN assignment, and is it distinct from the current activation-code approval/ACK flow? +- Which identity is used in each pre-SN topic: FN, current plate, DB UUID, or another value? The current docs use a generic plate in activation topics. +- Should bootstrap clients be unbound to Client ID, bound to FN, or bound to a board UUID? How are FN/board identifiers protected against reuse? +- Does each vending machine keep one operational credential through board replacement, or is each replacement given a newly rotated credential? Recommended: rotate on replacement. +- How does the device securely receive credentials after SN assignment (factory channel, authenticated MQTT bootstrap response, physical/manual provisioning, or another mechanism)? +- Which exact topic/action allowlist applies before and after SN assignment? Review all request-response topics and any legacy `devices/...` paths. +- What broker-control operations must Go perform, and how are desired broker state, retries, timeouts, partial failures, and reconciliation persisted? +- Which application roles may trigger activation, credential rotation, disablement, or board transfer, and what audit fields are mandatory? +- How are credentials stored, delivered, rotated, and redacted in logs and support tooling? +- Which deployed Mosquitto version and plugin configuration are authoritative for validation? + +## Codebase touchpoints inspected + +- `device-onboarding-activation.md` (backend repo) — current repository activation steps and message flow. +- `device-server-mqtt-protocol-fa.md` (backend repo) — topic contract inventory and direction. +- `schema_v1.sql` (backend repo) — device UUID, FN/SN, current identity, and activation fields. +- `internal/mqtt` (backend repo) — current Go MQTT client package; its presence does not establish Dynamic Security integration. + +## Primary references + +- [Mosquitto Dynamic Security documentation](https://mosquitto.org/documentation/dynamic-security/) — clients, optional Client ID binding, roles/ACL events, anonymous group, pattern substitutions, and control API. +- [Mosquitto Dynamic Security plugin API README](https://github.com/eclipse-mosquitto/mosquitto/blob/master/plugins/dynamic-security/README.md) — JSON control commands. +- [OASIS MQTT 5.0 specification, CONNECT](https://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html) — CONNECT packet and Client ID / username / password fields. + diff --git a/docker/docs/sn-as-username-process.md b/docker/docs/sn-as-username-process.md new file mode 100644 index 0000000..8e9de66 --- /dev/null +++ b/docker/docs/sn-as-username-process.md @@ -0,0 +1,313 @@ +# Board identity over MQTT: the full process with SN as the broker username + +**Status:** Design proposal. The enrollment part (raw board to FN) has been prototyped and verified in a Docker spike; the activation part (FN to SN) is **designed but not built or tested**. Nothing here is implemented in the backend yet. +**Builds on:** [`mqtt-dynamic-security-rnd.md`](./mqtt-dynamic-security-rnd.md) and [`enrollment-and-activation.md`](./enrollment-and-activation.md) (which compares the FN-username and SN-username models). This document is the complete description of the chosen direction: **SN as the username**, including every caution and security risk found so far. + +## 1. Goal and scope + +Turn a raw board into a board that holds only the MQTT access of the vending machine it is installed in, safely and durably: + +``` +raw board -> FN board (low access) -> SN board (machine access via roles) +``` + +In scope: enrollment, activation, normal operation, board replacement and retirement, compromise handling, broker configuration, backend data model, threat model. +Out of scope: the MQTT application topics themselves (see the protocol contract), firmware update delivery, TLS certificate issuance. + +## 2. Terms + +| Term | Meaning | +|---|---| +| **Board** | The physical controller. Identified by its **FN**. | +| **FN** | Backend-assigned identity of one physical board. Never reused, never reassigned. Lives in its own board table. | +| **SN / plate** | Identity of one vending machine (the plate is the SN). Pre-registered by an admin. Printed on the machine, so **not a secret**. | +| **Enrollment** | A raw board obtains an FN and a broker account. Over MQTT, with the Go backend, no other channel. | +| **Activation** | An operator links an FN to an SN; the board receives the machine-level broker account. | +| **Nonce** | Random single-use value that binds a reply to exactly one request. | +| **Random Client ID** | 128-bit CSPRNG value the board picks for enrollment only. Acts as an unguessable mailbox address. | +| **SAS** | Short authentication string: a short code the board shows and the operator compares with the screen. | +| **Dynamic Security (dynsec)** | Mosquitto plugin holding users, roles and ACLs, managed over MQTT. | + +## 3. Design principles + +1. **Authentication is separate from authorization.** Who may connect (a client with a secret) is not the same as what it may do (roles and ACLs). +2. **Trust is granted once, at activation, by a human.** Enrollment is deliberately open; an FN grants almost nothing. +3. **No secret ever crosses the network.** Passwords are derived independently on both sides from an X25519 key agreement. +4. **The backend proves who it is with a signature** whose public key is pinned in the board. +5. **Deny by default.** Every ACL type starts denied; only reviewed topic and action pairs are allowed. +6. **The machine owns its broker account.** A board is the current holder of that account; replacement rotates it. +7. **The database is the authority; the broker is reconciled to it.** A broker command succeeding does not prove the database committed, and the reverse. +8. **One-way doors are logged.** Every identity-changing step is audited with actor, time, FN, SN and broker result. + +## 4. Components and namespaces + +```mermaid +flowchart LR + B[Board
firmware with pinned backend public keys] -- MQTT over TLS --> M[Mosquitto 2.1+
Dynamic Security] + G[Go backend
admin client + service client] -- MQTT --> M + G --> D[(Database
boards, devices, assignments, audit)] + O[Operator UI
RBAC] --> G +``` + +| Principal | Broker identity | May do | +|---|---|---| +| Raw board | anonymous (`enroll` role via the anonymous/unauthenticated group) | publish `enroll/request/%c`; subscribe and receive `enroll/response/%c` | +| FN board | username = FN, Client ID = FN, role `board-bootstrap` | publish `board/%u/hello` and `board/%u/activation/request`; subscribe and receive `board/%u/cmd` and `board/%u/activation/response` | +| SN board | username = SN, Client ID = current FN, role `vending-device` (+ capability roles) | topics under `device/%u/...` only, as listed in the protocol contract | +| Backend service | `backend-svc`, role `backend-service` | read enrollment requests, answer them, talk to FN and SN topics it must serve | +| Backend admin | `backend` (dynsec admin) | `$CONTROL/dynamic-security/#` only | + +**Two backend identities on purpose:** the admin client manages the broker; the service client carries application traffic. A client whose roles are changed while connected is disconnected by the broker, so an identity must never modify itself, and a compromise of the service client must not give broker administration. + +## 5. Lifecycle + +```mermaid +stateDiagram-v2 + [*] --> RAW + RAW --> ENROLLED: enrollment (FN created) + ENROLLED --> EXPIRED: never logged in within N hours + ENROLLED --> ACTIVATION_PENDING: board asks to activate + ACTIVATION_PENDING --> ACTIVATING: operator approves (SAS confirmed) + ACTIVATING --> ACTIVE: board logged in as SN (first SN login = ack) + ACTIVATING --> ENROLLED: timeout, rolled back + ACTIVE --> SUSPENDED: operator action + SUSPENDED --> ACTIVE: resumed (credentials rotated) + ACTIVE --> REPLACED: replacement approved + ACTIVE --> RETIRED: end of life + ENROLLED --> REVOKED: operator or security action + ACTIVE --> REVOKED: operator or security action +``` + +The SN has its own state: `UNASSIGNED -> ASSIGNED(FN, generation) -> UNASSIGNED` on replacement or retirement. Invariant, enforced by a database unique constraint: **one SN has at most one active FN, and one FN is active on at most one SN.** + +## 6. What the firmware contains + +Identical on every board, nothing secret: + +- Backend **primary and backup public signing keys** (Ed25519). +- Broker host and the CA certificate used to verify the broker's TLS certificate. +- Topic templates for enrollment, FN and SN topics. +- Crypto: X25519, HKDF-SHA256, Ed25519 verification, a hardware random number generator. +- Flash space for the stored identity (FN, SN, passwords, generation number). +- A way to show the SAS (display or service port). + +Rejected: any fleet-wide secret in firmware. One flash dump would expose every board and rotation would need a fleet-wide OTA update. + +## 7. Stage 0: enrollment (raw board becomes an FN board) + +### 7.1 Broker side +Anonymous clients are allowed on a dedicated TLS listener and fall into the anonymous group (`setAnonymousGroup`); on Mosquitto 2.1 and later there is also a default `unauthenticated` group. Mosquitto does **not** require passwords. A shared static password would add no security because it would be public. + +Role `enroll` (assigned to the anonymous group): + +| ACL type | Topic | +|---|---| +| `publishClientSend` | `enroll/request/%c` | +| `subscribePattern` | `enroll/response/%c` | +| `publishClientReceive` | `enroll/response/%c` | +| `unsubscribePattern` | `enroll/response/%c` | + +`%c` is the Client ID, so a board can only talk on its own mailbox. Only the backend service may read `enroll/request/+`. + +### 7.2 Protocol + +```mermaid +sequenceDiagram + participant B as Board + participant M as Mosquitto + participant G as Go backend + B->>B: random Client ID, X25519 pair (b, B), nonce n1 + B->>M: CONNECT anonymous, clientId=random (TLS) + B->>M: SUBSCRIBE enroll/response/ (own topic only) + B->>G: enroll/request/ {B, n1, mac} + G->>G: reserve , assign FN, ephemeral (s, S) + G->>G: shared = X25519(s, B); pw = HKDF(shared, salt=n1, info=label|FN) + G->>M: dynsec createClient FN, pw, clientid=FN, role board-bootstrap + G->>B: enroll/response/ {FN, S, sig} (not retained) + B->>B: verify sig; shared = X25519(b, S); same pw + B->>B: persist FN + pw to flash, then reconnect as FN +``` + +- `sig` = Ed25519 over `S || B || n1 || clientid || FN`. Binding the FN and the Client ID means a reply for another request, board or identity does not verify. +- The password is **hex of HKDF-SHA256(shared secret, salt = n1, info = "robot-market/mqtt-pw/v1|" + FN, 32 bytes)**. The label separates this use of the secret from any other. +- The first successful FN login is the enrollment acknowledgement. +- Dynamic Security stores only a salted hash of the password. The backend never stores the plaintext, so a re-issue means a new password, not a lookup. + +### 7.3 Idempotency and retries +- The board persists FN and password to flash **before** reconnecting; a power loss in between must not lose the only copy. +- A lost response is handled by the board re-sending the **same** request. The backend keeps a per-Client-ID record: `processing` (duplicates are ignored) or `answered` (the cached public reply is re-sent; no new FN, no new password). +- A failed enrollment releases its reservation so the board can retry. (The prototype originally created an orphan FN when a retry arrived while the first request was still running; the in-flight reservation fixes that.) +- Unclaimed FNs (never logged in within N hours) are disabled and then deleted. + +### 7.4 What an FN can do +Only `hello` and `activation/request` on its own FN topics. It cannot use enrollment topics, other FNs' topics, or any `device/...` topic. Because of this, a fake FN is harmless in itself, which is why enrollment can stay open. + +## 8. Stage 1: FN board waiting + +The board logs in as `FN`, Client ID `FN`, with the derived password, sends `board//hello`, and requests activation when a machine is ready. It receives only `board//cmd` and `board//activation/response`. + +## 9. Stage 2: activation (FN to SN) -- designed, not yet built + +### 9.1 Preconditions +- The SN exists in the database as `UNASSIGNED`, created by an admin. +- The board is `ENROLLED` and online as its FN. +- An operator with the right RBAC role starts the activation in the application. + +### 9.2 Flow + +```mermaid +sequenceDiagram + participant O as Operator (UI) + participant G as Go backend + participant M as Mosquitto + participant B as Board (FN session) + B->>B: fresh X25519 pair (b2, B2), nonce n2, SAS = digits(hash(B2 || n2 || FN)) + B->>G: board//activation/request {B2, n2} + G->>O: pending request FN, MAC label, SAS, time + O->>G: choose SN, enter activation code, confirm SAS matches the board + G->>G: DB: ACTIVATING (FN, SN, generation g+1), unique constraint + G->>G: ephemeral (s2, S2); shared2; pw_sn = HKDF(shared2, salt=n2, info=label|SN|g+1) + G->>M: dynsec create or update client SN: pw_sn, role vending-device, clientid = FN + G->>B: board//activation/response {SN, g+1, S2, sig} + B->>B: verify sig, derive pw_sn, persist SN + pw_sn + generation, reconnect + B->>M: CONNECT username=SN, clientId=FN, password=pw_sn + M-->>G: first message on device//... (or presence) + G->>G: DB: ACTIVE; disable the FN client; audit +``` + +### 9.3 Rules that make it safe +1. **SAS bound to the activation key.** The board shows a code derived from `B2` (and `n2`, FN). The operator compares it with what the backend received. An attacker who stole an FN password and sends a request with their own key produces a code that does not match what the real board displays. **This check is mandatory**, not optional; without it the operator could attach a stranger's FN to a real SN. +2. **Activation code** (from the existing activation flow): single use, expiring, bound to one SN, attempt-limited. It proves the operator is physically at the machine. +3. **SN is never trusted as proof.** It is printed on the machine; the operator chooses it, the board does not claim it. +4. **Fresh key pair per activation.** Reusing the enrollment key would link the two exchanges and weaken forward secrecy. +5. **Signed reply includes domain separation.** Sign `"robot-market/activate/v1" || S2 || B2 || n2 || FN || SN || generation`. (The enrollment prototype signs without a domain-separation prefix; add `"robot-market/enroll/v1"` there too so a signature from one flow can never be replayed in the other.) +6. **Generation counter.** Each activation of an SN increments a stored generation, mixed into the password derivation and delivered signed. It makes every password unique, gives auditors a monotonic value, and lets a board detect a stale reply. +7. **Client ID binding.** The SN client's Client ID is set to the current board's FN. A copied SN password does not work from a client with another Client ID. (A *clone* of the same flash image still has the right Client ID; see section 13.) +8. **Cutover is acknowledged, not assumed.** The FN client stays enabled until the first successful SN login. Disabling it earlier would strand a board that lost power before saving the new credentials. After the acknowledgement, the FN client is disabled (or deleted, per policy) and its live session dropped. +9. **Order of operations.** Database state `ACTIVATING` is committed first, then the broker command, then the reply, then `ACTIVE` after the acknowledgement. A reconciler repairs any crash in between (section 11). +10. **Activation requests expire** and are bounded in number per FN. + +### 9.4 Failure cases + +| Failure | Outcome | +|---|---| +| Response lost | Board times out and re-sends the request; backend resends the cached public reply (the password is the same). | +| Board loses power after the reply but before saving | FN still enabled, so it can request again; the backend re-derives with the same inputs or restarts the activation. | +| Broker command fails | State stays `ACTIVATING`; the reconciler retries idempotently or rolls back to `ENROLLED`. | +| Operator confirms the wrong SN | Detected before approval by the SAS and by the activation code binding; after approval, replacement flow reverts it (audited). | +| No SN login within the timeout | Roll back: remove or disable the SN credential, SN returns to `UNASSIGNED`, FN remains usable. | +| Two operators approve the same SN | The unique constraint rejects the second. | + +## 10. Stage 3: normal operation + +- The board connects as `SN` with Client ID `FN` and uses `device//...` topics. One shared role `vending-device` uses `device/%u/...`, so the broker enforces that a board can only touch its own machine's topics. +- **Capability roles** (`news`, `updates`, ...) are additional shared roles added per machine type or entitlement. Adding a role to a connected client **disconnects it**, so plan for a reconnect. +- The backend subscribes with wildcards (`device/+/...`) and takes the SN from the topic. MQTT does not tell a subscriber who published, so the backend must not blindly trust the topic: the broker's ACL and Client ID binding guarantee the topic matches the username, and critical messages should also carry the SN and be cross-checked against the database. +- Messages that carry secrets or credentials are never published retained. Device clients use clean sessions so queued messages do not outlive an identity. + +## 11. Backend data and reconciliation + +Suggested tables (names illustrative): + +| Table | Key fields | +|---|---| +| `boards` | FN (unique, never reused), enrolled_at, last_login_at, state, hardware_label (claim only), pubkey fingerprint | +| `enrollments` | client id, nonce, board FN, state (`processing`, `answered`, `confirmed`, `expired`), cached public reply | +| `devices` (existing) | UUID, SN (plate), state | +| `board_assignments` | SN, FN, generation, state, created/activated/ended timestamps, actor | +| `broker_outbox` | desired broker operation, idempotency key, attempts, result | +| `audit_log` | actor, action, FN, SN, generation, broker result, timestamp | + +Reconciler rules: +- Every broker mutation is a recorded desired state, applied idempotently: create client is "create or update", disable is safe to repeat. +- A periodic job lists clients, roles and the anonymous group from the broker and compares them with the database; drift raises an alert. +- Passwords cannot be read back from the broker, so drift in a password is detected by the generation counter and by login failures, and corrected by rotating. + +## 12. Replacement, transfer, retirement + +**Replacing the board in machine SN (same machine, new hardware):** +1. Authorize the replacement; mark the old assignment `REPLACED`. +2. **Revoke first:** rotate or disable the SN credential and drop the old board's session. The broker must refuse the old board before any new board gets access. +3. The new board enrolls as a **new FN** (a board never inherits an FN). +4. Normal activation runs for the same SN: new key exchange, **new password**, Client ID rebound to the new FN, generation incremented. +5. Audit: old FN, new FN, SN, generation, actor, time, broker results. + +A single "set a new password" action both revokes the old board and grants the new one, so the system cannot end up with two valid boards for one machine. Do not rely on a password change alone to drop an existing live session; verify the broker's behavior and explicitly disconnect or rebind (changing a client's roles or Client ID disconnects it, as observed). + +**Moving a board to another machine:** treat the board as revoked on the old machine, then run a deliberate **re-enrollment** (new FN, new password) before activating on the new one. Never re-enable an old credential. + +**Retirement / lost or stolen board:** disable the SN credential immediately (rotate), disable the FN, mark `REVOKED`, alert on any later connection attempt. + +## 13. Threat model + +| # | Threat | Mitigation | Residual risk | +|---|---|---|---| +| 1 | Eavesdropper on the network or broker reads the credential | The password is never transmitted; only public keys travel; TLS on top | Traffic metadata is visible. | +| 2 | Man in the middle swaps the backend's key | Ed25519 signature checked against the pinned public key | Depends on the pinned key staying secret on the server. | +| 3 | Replay of an old reply | Reply bound to the request nonce, Client ID, FN (and SN, generation); board rejects mismatches | None known if nonces come from a good RNG. | +| 4 | Another board reads my reply | `%c`-scoped ACL; random 128-bit Client ID; wildcard subscription denied | Weak RNG on the board would make Client IDs guessable. | +| 5 | Enrollment flood (anonymous) | Per-listener limits, backend rate limits and cap on pending enrollments, expiry of unclaimed FNs, payload size and schema checks | Some resource use under a determined flood; needs monitoring. | +| 6 | MAC spoofing to hijack or lock out a board | MAC is a label only; re-issue only while an FN has never logged in; later reset needs an operator | An attacker can still enroll fake FNs (harmless, section 7.4). | +| 7 | Attacker enrolls a fake FN and asks for activation | Operator must confirm the SAS derived from the activation key and enter the activation code | Relies on operators actually doing the check. | +| 8 | Stolen FN password | SAS makes activation hijack visible; FN can do nothing else | Attacker can disconnect the real board (nuisance). | +| 9 | Stolen or copied SN password | Client ID binding; rotation at replacement; revocation on report | A full flash clone has the correct Client ID too (see 11). | +| 10 | Firmware / flash dump | Flash encryption and secure boot; no fleet-wide secret; the pinned key is public | Without flash encryption, one board's credentials are readable. | +| 11 | Cloned board (same FN, SN, password) | Duplicate Client ID connections kick each other, which is detectable; alert on takeover events; rotate on suspicion | A silent clone can impersonate until detected. Secure element is the real fix. | +| 12 | Backend signing key leaked | Backup public key already in firmware; signing key in a secret store or HSM; separate key for firmware signing | Boards that have not yet seen a backup-signed message still trust the leaked key. | +| 13 | Dynamic Security admin credential stolen | Admin client separate from the service client; secret store; network restriction of the control topic; device roles never grant `$CONTROL` | Admin compromise is full broker compromise. | +| 14 | A device role accidentally allows `$CONTROL` or `#` | ACL review; automated test that connects as a device and attempts both | A mistake in a future role. | +| 15 | Broker compromise | Passwords are never on the wire (hashes only in the dynsec file); TLS terminates at the broker so traffic is visible there | All device data is exposed; credentials are not directly derivable but the file is sensitive. | +| 16 | Broker and database disagree | Outbox, idempotent commands, periodic reconciliation | Window between a crash and the next reconcile. | +| 17 | Two boards for one SN, or one board for two SNs | Database unique constraints plus broker-level credential rotation | None if constraints are enforced. | +| 18 | Race between operator approvals | Single transaction and constraint on the assignment | None. | +| 19 | Old board keeps access after replacement | Revoke first, rotate, explicit disconnect, Client ID rebinding | Depends on actually dropping the live session. | +| 20 | Low-order or all-zero X25519 results | Reject an all-zero shared secret (Go and libsodium do); verify the OpenSSL path | Verify per library. | +| 21 | Weak or unseeded RNG on the board | Use the hardware RNG; on ESP32 ensure the hardware generator has entropy (RF or bootloader entropy enabled) | Must be verified on the real chip. | +| 22 | Private keys left in memory | Zeroize secrets after use (the prototype does not yet) | Memory disclosure on the board. | +| 23 | TLS misconfiguration (no certificate check, expired certificate, no clock) | Pin the CA, verify the server name, get time before TLS (NTP or RTC) | Boards without a clock cannot validate certificates reliably. | +| 24 | Secrets in logs | Log only short fingerprints, never passwords, keys or nonces beyond what is public; redact in support tooling | Debug logging added later. | +| 25 | Retained messages or persistent sessions leaking across identities | No retained secrets; clean sessions; drop sessions on revoke | Misconfigured client. | +| 26 | Operator or insider abuse | RBAC, audit log, optional four-eyes for replacement | Authorized misuse remains possible but visible. | +| 27 | Protocol change in the field | Version field in every message; reject unknown versions | Needs a migration plan. | +| 28 | Domain confusion between enrollment and activation signatures | Distinct signed prefixes per flow | Prototype lacks the prefix; add it. | + +## 14. Broker facts verified in the prototype + +- **`%c` and `%u` substitution in dynsec ACLs works only in Mosquitto 2.1 or newer.** On 2.0.22, literal ACLs worked and substituted ones silently never matched. Use `eclipse-mosquitto:2.1-alpine` (2.1.2) or a pinned newer image. +- **Role changes disconnect the client**: `addClientRole` kicked the connected client. Use separate admin and service identities; expect a reconnect when roles change. +- `mosquitto_ctrl addRoleACL` requires an explicit `allow` or `deny`, otherwise it fails with "Invalid arguments". +- An ACL-denied subscription returns SUBACK 128; tests rely on that. A wildcard such as `enroll/response/#` is also denied. +- A pattern string must be the only item at its hierarchy level (`enroll/response/%c` is valid; `x/pre%c` is not). +- Client ID binding works: right password with another Client ID is refused. +- Not verified yet: whether a password change alone drops a live session; the `unauthenticated` group behavior on the deployed version; behavior of every device topic under the real protocol contract. + +## 15. Operations + +- Secrets: dynsec admin and service passwords and the signing keys live in a secret store, not in compose files or environment dumps (the prototype uses demo values). +- Keys: the backup signing key is stored offline; rotate the primary on a schedule and on suspicion; keep a separate key for firmware signing. +- Monitoring: alert on duplicate-Client-ID takeovers, refused logins, enrollment rate, pending-activation age, reconciler drift, and any connection by a revoked FN or SN. +- Testing: keep an automated test that connects as each role and asserts what is denied, including `$CONTROL/dynamic-security/#` and `#`. +- Retention: audit log is append-only and retained according to policy. + +## 16. Open decisions + +- Disable or delete the FN client after activation (recommended: disable, delete after a retention period). +- Whether activation also keeps the existing activation-code flow unchanged or merges it with the SAS step. +- Capability roles: which machine types or entitlements get `news`, `updates`, etc. +- Exact timeouts: unclaimed FN expiry, activation request expiry, SN-login acknowledgement window. +- Hardware: secure element or flash encryption availability, RNG behavior, clock source. +- Whether to add a four-eyes rule for replacement and retirement. +- Which Mosquitto 2.1.x version is deployed and pinned. + +## 17. Implementation checklist + +1. Pin Mosquitto 2.1 or newer; enable TLS on every listener; separate enrollment listener with limits. +2. Create the dynsec roles (`enroll`, `board-bootstrap`, `vending-device`, `backend-service`), anonymous group, default deny, and the admin and service clients. +3. Backend enrollment handler (in-flight reservation, caching, expiry), with the prototype as reference. +4. Database tables, constraints and outbox; reconciler. +5. Activation: operator UI, SAS, activation code, second exchange, SN client creation, acknowledgement, FN disable. +6. Replacement and retirement flows with revoke-first ordering. +7. Domain-separated signatures; generation counter; message versioning. +8. Board firmware: persistent identity, RNG, key zeroization, pinned primary and backup keys, TLS with CA pinning. +9. Automated role and ACL test suite; monitoring and alerts. +10. Extend the Docker prototype to cover activation and replacement before building the production version.