Use plain Mermaid diagrams again and drop the SVG images

The preview problems were caused by the diagram text, which is now fixed in
every copy, so the pre-rendered SVGs (white background, harder to keep in
sync) are no longer needed. Also updates docker/docs with the fixed text.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 11:31:29 +03:30
co-authored by Claude Sonnet 5.5
parent c1fcfb7b73
commit 20d69b93b7
10 changed files with 30 additions and 65 deletions
+2 -2
View File
@@ -12,8 +12,8 @@ X25519 key agreement so the broker password is never transmitted.
broker username, with the threat model. 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/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. - [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 diagrams are plain Mermaid. They avoid `;`, `|`, `+`, `{}` and `<...>` inside sequence-diagram messages, which
the sequence diagrams are also embedded as SVG images rendered with that version, with the source under each. 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 - `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. crypto libraries against the RFC test vectors and against each other.
+13 -13
View File
@@ -46,14 +46,14 @@ sequenceDiagram
participant G as Go backend participant G as Go backend
B->>B: random Client ID, X25519 keypair (b, B), nonce n1 B->>B: random Client ID, X25519 keypair (b, B), nonce n1
B->>M: CONNECT (TLS, anonymous, clientId=random) B->>M: CONNECT (TLS, anonymous, clientId=random)
B->>G: enroll/request/<cid> {B, n1, mac} B->>G: enroll/request/[cid] [B, n1, mac]
G->>G: create FN record, ephemeral (s, S) 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->>M: Dynamic Security: create client FN, pw, role board-bootstrap
G->>B: enroll/response/<cid> {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: verify sig with pinned public key
B->>B: shared = X25519(b, S); same pw B->>B: shared = X25519(b, S), same pw
B->>B: persist FN + pw to flash B->>B: persist FN and pw to flash
B->>M: reconnect as FN / pw B->>M: reconnect as FN / pw
``` ```
@@ -94,13 +94,13 @@ sequenceDiagram
participant G as Go backend participant G as Go backend
participant M as Mosquitto participant M as Mosquitto
participant B as Board (FN) participant B as Board (FN)
B->>G: board/<FN>/activation/request B->>G: board/[FN]/activation/request
O->>G: pick SN, confirm code and short string O->>G: pick SN, confirm code and short string
G->>M: ensure role machine-<SN> (literal ACLs device/<SN>/...) G->>M: ensure role machine-[SN] (literal ACLs device/[SN]/...)
G->>M: add roles machine-<SN> + capability roles to client FN G->>M: add roles machine-[SN] + capability roles to client FN
G->>B: board/<FN>/activation/response {SN, sig} G->>B: board/[FN]/activation/response [SN, sig]
B->>B: store SN, reconnect as FN B->>B: store SN, reconnect as FN
B->>M: use device/<SN>/... topics 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`). - **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`).
@@ -119,12 +119,12 @@ sequenceDiagram
participant M as Mosquitto participant M as Mosquitto
participant B as Board (FN) participant B as Board (FN)
B->>B: new ephemeral X25519 keypair (b2, B2) B->>B: new ephemeral X25519 keypair (b2, B2)
B->>G: board/<FN>/activation/request {B2, n2} B->>G: board/[FN]/activation/request [B2, n2]
O->>G: pick SN, confirm code and short string 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->>G: pw = HKDF(shared, SN, n2, generation)
G->>M: create or update client SN: pw, clientId bound to board, role vending-device G->>M: create or update client SN: pw, clientId bound to board, role vending-device
G->>B: board/<FN>/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->>B: verify sig, derive same pw, store SN + pw
B->>M: reconnect as SN / pw B->>M: reconnect as SN / pw
G->>M: disable FN client (or keep for rescue / re-enrollment) G->>M: disable FN client (or keep for rescue / re-enrollment)
+15 -15
View File
@@ -118,14 +118,14 @@ sequenceDiagram
participant G as Go backend participant G as Go backend
B->>B: random Client ID, X25519 pair (b, B), nonce n1 B->>B: random Client ID, X25519 pair (b, B), nonce n1
B->>M: CONNECT anonymous, clientId=random (TLS) B->>M: CONNECT anonymous, clientId=random (TLS)
B->>M: SUBSCRIBE enroll/response/<cid> (own topic only) B->>M: SUBSCRIBE enroll/response/[cid] (own topic only)
B->>G: enroll/request/<cid> {B, n1, mac} B->>G: enroll/request/[cid] [B, n1, mac]
G->>G: reserve <cid>, assign FN, ephemeral (s, S) G->>G: reserve [cid], assign FN, ephemeral (s, S)
G->>G: shared = X25519(s, B); pw = HKDF(shared, salt=n1, info=label|FN) 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->>M: dynsec createClient FN, pw, clientid=FN, role board-bootstrap
G->>B: enroll/response/<cid> {FN, S, sig} (not retained) G->>B: enroll/response/[cid] [FN, S, sig] (not retained)
B->>B: verify sig; shared = X25519(b, S); same pw B->>B: verify sig, shared = X25519(b, S), same pw
B->>B: persist FN + pw to flash, then reconnect as FN 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. - `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 G as Go backend
participant M as Mosquitto participant M as Mosquitto
participant B as Board (FN session) participant B as Board (FN session)
B->>B: fresh X25519 pair (b2, B2), nonce n2, SAS = digits(hash(B2 || n2 || FN)) B->>B: fresh X25519 pair (b2, B2), nonce n2, SAS = digits(hash(B2, n2, FN))
B->>G: board/<FN>/activation/request {B2, n2} B->>G: board/[FN]/activation/request [B2, n2]
G->>O: pending request FN, MAC label, SAS, time G->>O: pending request FN, MAC label, SAS, time
O->>G: choose SN, enter activation code, confirm SAS matches the board 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: DB: ACTIVATING (FN, SN, next generation), unique constraint
G->>G: ephemeral (s2, S2); shared2; pw_sn = HKDF(shared2, salt=n2, info=label|SN|g+1) 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->>M: dynsec create or update client SN: pw_sn, role vending-device, clientid = FN
G->>B: board/<FN>/activation/response {SN, g+1, S2, sig} G->>B: board/[FN]/activation/response [SN, next generation, S2, sig]
B->>B: verify sig, derive pw_sn, persist SN + pw_sn + generation, reconnect B->>B: verify sig, derive pw_sn, persist SN, pw_sn and generation, reconnect
B->>M: CONNECT username=SN, clientId=FN, password=pw_sn B->>M: CONNECT username=SN, clientId=FN, password=pw_sn
M-->>G: first message on device/<SN>/... (or presence) M-->>G: first message on device/[SN]/... (or presence)
G->>G: DB: ACTIVE; disable the FN client; audit G->>G: DB: ACTIVE, disable the FN client, audit
``` ```
### 9.3 Rules that make it safe ### 9.3 Rules that make it safe
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 27 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 25 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 25 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 25 KiB

-18
View File
@@ -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. Only the backend may subscribe to `enroll/request/+`. Devices cannot publish to response topics.
![Stage 0 enrollment sequence diagram](diagrams/stage0-enrollment.svg)
<details><summary>Mermaid source of this diagram</summary>
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
participant B as Board participant B as Board
@@ -61,8 +57,6 @@ sequenceDiagram
B->>M: reconnect as FN / pw B->>M: reconnect as FN / pw
``` ```
</details>
- `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. - `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. - 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. - 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. 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)
<details><summary>Mermaid source of this diagram</summary>
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
participant O as Operator participant O as Operator
@@ -113,8 +103,6 @@ sequenceDiagram
B->>M: use device/[SN]/... topics B->>M: use device/[SN]/... topics
``` ```
</details>
- **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`). - **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. - **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. - **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. 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)
<details><summary>Mermaid source of this diagram</summary>
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
participant O as Operator participant O as Operator
@@ -146,8 +130,6 @@ sequenceDiagram
G->>M: disable FN client (or keep for rescue / re-enrollment) G->>M: disable FN client (or keep for rescue / re-enrollment)
``` ```
</details>
- **One shared role** `vending-device` with `device/%u/...`, because the username equals the SN in the topic. - **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. - 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`. - **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`.
-12
View File
@@ -111,10 +111,6 @@ Role `enroll` (assigned to the anonymous group):
### 7.2 Protocol ### 7.2 Protocol
![Enrollment protocol sequence diagram](diagrams/enrollment-protocol.svg)
<details><summary>Mermaid source of this diagram</summary>
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
participant B as Board participant B as Board
@@ -132,8 +128,6 @@ sequenceDiagram
B->>B: persist FN and pw to flash, then reconnect as FN B->>B: persist FN and pw to flash, then reconnect as FN
``` ```
</details>
- `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. - `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 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. - 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 ### 9.2 Flow
![Activation flow sequence diagram](diagrams/activation-flow.svg)
<details><summary>Mermaid source of this diagram</summary>
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
participant O as Operator (UI) participant O as Operator (UI)
@@ -185,8 +175,6 @@ sequenceDiagram
G->>G: DB: ACTIVE, disable the FN client, audit G->>G: DB: ACTIVE, disable the FN client, audit
``` ```
</details>
### 9.3 Rules that make it safe ### 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. 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. 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.