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.
- [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.
+13 -13
View File
@@ -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/<cid> {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/<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: 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/<FN>/activation/request
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}
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
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`).
@@ -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/<FN>/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/<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->>M: reconnect as SN / pw
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
B->>B: random Client ID, X25519 pair (b, B), nonce n1
B->>M: CONNECT anonymous, clientId=random (TLS)
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)
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/<cid> {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/<FN>/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/<FN>/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/<SN>/... (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
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.
![Stage 0 enrollment sequence diagram](diagrams/stage0-enrollment.svg)
<details><summary>Mermaid source of this diagram</summary>
```mermaid
sequenceDiagram
participant B as Board
@@ -61,8 +57,6 @@ sequenceDiagram
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.
- 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)
<details><summary>Mermaid source of this diagram</summary>
```mermaid
sequenceDiagram
participant O as Operator
@@ -113,8 +103,6 @@ sequenceDiagram
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`).
- **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)
<details><summary>Mermaid source of this diagram</summary>
```mermaid
sequenceDiagram
participant O as Operator
@@ -146,8 +130,6 @@ sequenceDiagram
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.
- 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`.
-12
View File
@@ -111,10 +111,6 @@ Role `enroll` (assigned to the anonymous group):
### 7.2 Protocol
![Enrollment protocol sequence diagram](diagrams/enrollment-protocol.svg)
<details><summary>Mermaid source of this diagram</summary>
```mermaid
sequenceDiagram
participant B as Board
@@ -132,8 +128,6 @@ sequenceDiagram
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.
- 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)
<details><summary>Mermaid source of this diagram</summary>
```mermaid
sequenceDiagram
participant O as Operator (UI)
@@ -185,8 +175,6 @@ sequenceDiagram
G->>G: DB: ACTIVE, disable the FN client, audit
```
</details>
### 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.