diff --git a/README.md b/README.md index 19dbe71..dae42b6 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ X25519 key agreement so the broker password is never transmitted. broker username, with the threat model. - [docs/enrollment-and-activation.md](docs/enrollment-and-activation.md): the FN-as-username and SN-as-username models compared. - [docs/mqtt-dynamic-security-rnd.md](docs/mqtt-dynamic-security-rnd.md): the original R&D notes. - - The diagrams are written and checked against **Mermaid 11.13.0** (the version bundled with Typora and Obsidian); - the sequence diagrams are also embedded as SVG images rendered with that version, with the source under each. + - The diagrams are plain Mermaid. They avoid `;`, `|`, `+`, `{}` and `<...>` inside sequence-diagram messages, which + broke the GoLand and Typora previews, and parse on Mermaid 11.13.0 (the lowest version those tools bundle). - `go/`, `c/`, `c-libsodium/`, `cpp/`, `run.sh`, `run-libsodium.sh`: fixed-key programs that check the crypto libraries against the RFC test vectors and against each other. diff --git a/docker/docs/enrollment-and-activation.md b/docker/docs/enrollment-and-activation.md index 23279b1..d92dd2b 100644 --- a/docker/docs/enrollment-and-activation.md +++ b/docker/docs/enrollment-and-activation.md @@ -46,14 +46,14 @@ sequenceDiagram 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} + 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->>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) + 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->>B: shared = X25519(b, S), same pw + B->>B: persist FN and pw to flash B->>M: reconnect as FN / pw ``` @@ -94,13 +94,13 @@ sequenceDiagram participant G as Go backend participant M as Mosquitto participant B as Board (FN) - B->>G: board//activation/request + B->>G: board/[FN]/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} + 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//... topics + B->>M: use device/[SN]/... 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`). @@ -119,12 +119,12 @@ sequenceDiagram participant M as Mosquitto participant B as Board (FN) B->>B: new ephemeral X25519 keypair (b2, B2) - B->>G: board//activation/request {B2, n2} + 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: 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} + 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) diff --git a/docker/docs/sn-as-username-process.md b/docker/docs/sn-as-username-process.md index 8e9de66..e6c70ad 100644 --- a/docker/docs/sn-as-username-process.md +++ b/docker/docs/sn-as-username-process.md @@ -118,14 +118,14 @@ sequenceDiagram 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) + B->>M: SUBSCRIBE enroll/response/[cid] (own topic only) + B->>G: enroll/request/[cid] [B, n1, mac] + G->>G: reserve [cid], 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 + G->>B: enroll/response/[cid] [FN, S, sig] (not retained) + B->>B: verify sig, shared = X25519(b, S), same pw + B->>B: persist FN and 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. @@ -161,18 +161,18 @@ sequenceDiagram 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} + B->>B: fresh X25519 pair (b2, B2), nonce n2, SAS = digits(hash(B2, n2, FN)) + B->>G: board/[FN]/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->>G: DB: ACTIVATING (FN, SN, next generation), unique constraint + G->>G: ephemeral (s2, S2), shared2, pw_sn = HKDF(shared2, salt=n2, info = label, SN, generation) 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 + G->>B: board/[FN]/activation/response [SN, next generation, S2, sig] + B->>B: verify sig, derive pw_sn, persist SN, pw_sn and 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 + M-->>G: first message on device/[SN]/... (or presence) + G->>G: DB: ACTIVE, disable the FN client, audit ``` ### 9.3 Rules that make it safe diff --git a/docs/diagrams/activation-flow.svg b/docs/diagrams/activation-flow.svg deleted file mode 100644 index 967124f..0000000 --- a/docs/diagrams/activation-flow.svg +++ /dev/null @@ -1 +0,0 @@ -Board (FN session)MosquittoGo backendOperator (UI)Board (FN session)MosquittoGo backendOperator (UI)fresh X25519 pair (b2, B2), nonce n2, SAS = digits(hash(B2, n2, FN))board/[FN]/activation/request [B2, n2]pending request FN, MAC label, SAS, timechoose SN, enter activation code, confirm SAS matches the boardDB: ACTIVATING (FN, SN, next generation), unique constraintephemeral (s2, S2), shared2, pw_sn = HKDF(shared2, salt=n2, info = label, SN, generation)dynsec create or update client SN: pw_sn, role vending-device, clientid = FNboard/[FN]/activation/response [SN, next generation, S2, sig]verify sig, derive pw_sn, persist SN, pw_sn and generation, reconnectCONNECT username=SN, clientId=FN, password=pw_snfirst message on device/[SN]/... (or presence)DB: ACTIVE, disable the FN client, audit \ No newline at end of file diff --git a/docs/diagrams/enrollment-protocol.svg b/docs/diagrams/enrollment-protocol.svg deleted file mode 100644 index 7180e4e..0000000 --- a/docs/diagrams/enrollment-protocol.svg +++ /dev/null @@ -1 +0,0 @@ -Go backendMosquittoBoardGo backendMosquittoBoardrandom Client ID, X25519 pair (b, B), nonce n1CONNECT anonymous, clientId=random (TLS)SUBSCRIBE enroll/response/[cid] (own topic only)enroll/request/[cid] [B, n1, mac]reserve [cid], assign FN, ephemeral (s, S)shared = X25519(s, B), pw = HKDF(shared, salt=n1, info = label, FN)dynsec createClient FN, pw, clientid=FN, role board-bootstrapenroll/response/[cid] [FN, S, sig] (not retained)verify sig, shared = X25519(b, S), same pwpersist FN and pw to flash, then reconnect as FN \ No newline at end of file diff --git a/docs/diagrams/model-a-activation.svg b/docs/diagrams/model-a-activation.svg deleted file mode 100644 index cc62ef1..0000000 --- a/docs/diagrams/model-a-activation.svg +++ /dev/null @@ -1 +0,0 @@ -Board (FN)MosquittoGo backendOperatorBoard (FN)MosquittoGo backendOperatorboard/[FN]/activation/requestpick SN, confirm code and short stringensure role machine-[SN] (literal ACLs device/[SN]/...)add roles machine-[SN] + capability roles to client FNboard/[FN]/activation/response [SN, sig]store SN, reconnect as FNuse device/[SN]/... topics \ No newline at end of file diff --git a/docs/diagrams/model-b-activation.svg b/docs/diagrams/model-b-activation.svg deleted file mode 100644 index 8957be5..0000000 --- a/docs/diagrams/model-b-activation.svg +++ /dev/null @@ -1 +0,0 @@ -Board (FN)MosquittoGo backendOperatorBoard (FN)MosquittoGo backendOperatornew ephemeral X25519 keypair (b2, B2)board/[FN]/activation/request [B2, n2]pick SN, confirm code and short stringephemeral (s2, S2), shared = X25519(s2, B2)pw = HKDF(shared, SN, n2, generation)create or update client SN: pw, clientId bound to board, role vending-deviceboard/[FN]/activation/response [SN, S2, sig]verify sig, derive same pw, store SN + pwreconnect as SN / pwdisable FN client (or keep for rescue / re-enrollment) \ No newline at end of file diff --git a/docs/diagrams/stage0-enrollment.svg b/docs/diagrams/stage0-enrollment.svg deleted file mode 100644 index 9a07633..0000000 --- a/docs/diagrams/stage0-enrollment.svg +++ /dev/null @@ -1 +0,0 @@ -Go backendMosquittoBoardGo backendMosquittoBoardrandom Client ID, X25519 keypair (b, B), nonce n1CONNECT (TLS, anonymous, clientId=random)enroll/request/[cid] [B, n1, mac]create FN record, ephemeral (s, S)shared = X25519(s, B), pw = HKDF(shared, FN, n1)Dynamic Security: create client FN, pw, role board-bootstrapenroll/response/[cid] [FN, S, sig] (not retained)verify sig with pinned public keyshared = X25519(b, S), same pwpersist FN and pw to flashreconnect as FN / pw \ No newline at end of file diff --git a/docs/enrollment-and-activation.md b/docs/enrollment-and-activation.md index f877ce2..d92dd2b 100644 --- a/docs/enrollment-and-activation.md +++ b/docs/enrollment-and-activation.md @@ -39,10 +39,6 @@ Mosquitto does **not** require passwords: with `allow_anonymous true` on a dedic Only the backend may subscribe to `enroll/request/+`. Devices cannot publish to response topics. -![Stage 0 enrollment sequence diagram](diagrams/stage0-enrollment.svg) - -
Mermaid source of this diagram - ```mermaid sequenceDiagram participant B as Board @@ -61,8 +57,6 @@ sequenceDiagram 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. @@ -94,10 +88,6 @@ The database is authoritative for ownership. A successful broker command alone d The board's credential is created once at enrollment and never changes. Activation only changes which roles the FN client holds. -![Model A activation sequence diagram](diagrams/model-a-activation.svg) - -
Mermaid source of this diagram - ```mermaid sequenceDiagram participant O as Operator @@ -113,8 +103,6 @@ sequenceDiagram B->>M: use device/[SN]/... 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. @@ -124,10 +112,6 @@ sequenceDiagram At activation the board gets a second, machine-level broker identity. The same X25519 exchange is reused. -![Model B activation sequence diagram](diagrams/model-b-activation.svg) - -
Mermaid source of this diagram - ```mermaid sequenceDiagram participant O as Operator @@ -146,8 +130,6 @@ sequenceDiagram 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`. diff --git a/docs/sn-as-username-process.md b/docs/sn-as-username-process.md index 91e8922..e6c70ad 100644 --- a/docs/sn-as-username-process.md +++ b/docs/sn-as-username-process.md @@ -111,10 +111,6 @@ Role `enroll` (assigned to the anonymous group): ### 7.2 Protocol -![Enrollment protocol sequence diagram](diagrams/enrollment-protocol.svg) - -
Mermaid source of this diagram - ```mermaid sequenceDiagram participant B as Board @@ -132,8 +128,6 @@ sequenceDiagram B->>B: persist FN and 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. @@ -161,10 +155,6 @@ The board logs in as `FN`, Client ID `FN`, with the derived password, sends `boa ### 9.2 Flow -![Activation flow sequence diagram](diagrams/activation-flow.svg) - -
Mermaid source of this diagram - ```mermaid sequenceDiagram participant O as Operator (UI) @@ -185,8 +175,6 @@ sequenceDiagram 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.