feat: added docs for this research
This commit is contained in:
@@ -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/<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 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/<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 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/<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-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.
|
||||
Reference in New Issue
Block a user