docs: enrollment link encoding rules in the spec, session log in build-status
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
5291bdd045
commit
199807a8c9
@@ -660,3 +660,37 @@ One user-visible bug caught in the process: Go's JSON encoder HTML-escapes `<`,
|
||||
default, so the refusal reached the client as `needs \u003e= 0.2.0`. Disabled at the encoder (this
|
||||
is an API, not a page), and the client now *parses* the error field instead of pattern-matching it,
|
||||
so it survives whatever a future encoder decides to escape.
|
||||
|
||||
### Enrollment: the server mints the bootstrap link (server-v0.5.3 … v0.5.4, 2026-08-01)
|
||||
Until now a device was configured by hand-typing a control URL, a base64 SPKI pin and a
|
||||
credential. That is the step that goes wrong, and it goes wrong quietly: a pin off by one
|
||||
character does not fail loudly, it just never matches, and surfaces days later as an inscrutable
|
||||
TLS error.
|
||||
|
||||
`POST /admin/enroll-tokens` now returns the whole §2.1 bootstrap link alongside the token, because
|
||||
the server is the only party holding all three parts at once. The app takes it from a paste or an
|
||||
`echolot://enroll` deep link (so a QR scan configures a server in one action) and writes URL, pin
|
||||
and credential **together or not at all** — a half-applied server fails later, somewhere else,
|
||||
with an error pointing at the wrong thing.
|
||||
|
||||
The control URL comes from `ECHOLOT_PUBLIC_URL` (set on fmr to `https://fmr-1.echo-lot.app:8443`),
|
||||
falling back to the first control listen address; a wildcard bind warns rather than emitting a
|
||||
link to `0.0.0.0`.
|
||||
|
||||
**The encoding trap, which is the whole reason this is tested across both languages.** The pin is
|
||||
base64, so it contains `+`, `/` and `=` — each of which means something else in a query string. An
|
||||
unencoded `+` decodes to a space, leaving the pin wrong by exactly one character. Base64 has no
|
||||
spaces, so the parser restores them; that cannot damage a correctly-encoded pin and it rescues
|
||||
every hand-assembled link. `LiveEnrollmentTest` redeems a link the *server* produced, which is the
|
||||
only way to catch a disagreement between the Go assembler and the Kotlin parser — a unit test on
|
||||
either side alone cannot see it. It also asserts the token is refused the second time.
|
||||
|
||||
Also fixed a spec divergence found while reading §2.1: the spec names the field
|
||||
`device_credential`, the first implementation shipped `credential`. The server now sends both and
|
||||
the client prefers the spec's; the alias goes once nothing reads it.
|
||||
|
||||
Two process notes from this round:
|
||||
- An edit to the admin handler silently failed to apply and the endpoint kept returning just the
|
||||
token. Caught by deploying and *looking at the response*, not by trusting a green build.
|
||||
- The live suite is now six tests (`LiveServerTest`, `LiveMeasurement`, `LiveGranted`,
|
||||
`LiveUpload`, `LiveCompat`, `LiveEnrollment`), all green against fmr from the PC with no device.
|
||||
|
||||
+26
-3
@@ -24,11 +24,34 @@ echolot://enroll?v=1&u=<control-URL, urlencoded>&p=pin-sha256:<b64 SPKI hash>&t=
|
||||
|
||||
```
|
||||
POST /v1/enroll Authorization: Bearer <enrollment-token>
|
||||
→ 200 { "device_credential": "<random 256-bit, b64url>",
|
||||
"device_id": "uuid",
|
||||
"profile": { ... §2.2 ... } }
|
||||
→ 201 { "device_credential": "<random 256-bit, b64url>",
|
||||
"device_id": "uuid" }
|
||||
```
|
||||
|
||||
The **server assembles the bootstrap link**, because it is the only party holding all three parts
|
||||
at once, and the part an operator gets wrong by hand is the base64 pin — which does not fail
|
||||
loudly, it just never matches, and surfaces later as an inscrutable TLS error:
|
||||
|
||||
```
|
||||
POST /admin/enroll-tokens
|
||||
→ { "token": "…", "expires_in_s": 86400,
|
||||
"enroll_uri": "echolot://enroll?v=1&u=…&p=…&t=…" }
|
||||
```
|
||||
|
||||
The control URL in the link comes from `ECHOLOT_PUBLIC_URL`, falling back to the first control
|
||||
listen address. A wildcard bind has no single right answer, so it warns rather than guessing.
|
||||
|
||||
Encoding notes that matter in practice:
|
||||
- `u`, `p` and `t` are **percent-encoded**. The pin is base64, so it contains `+`, `/` and `=`,
|
||||
every one of which means something else in a query string.
|
||||
- A `+` that was *not* encoded decodes to a space. Base64 contains no spaces, so a parser SHOULD
|
||||
restore them — the alternative is a pin wrong by one character and a failure that points nowhere
|
||||
near the cause.
|
||||
- The control URL MUST be `https://`. The pin only protects a TLS connection; a cleartext URL
|
||||
would hand the token to anyone on the path.
|
||||
- **The link is a secret** while it is live: it carries a bearer token, so anyone who sees it
|
||||
before the device does can enroll instead.
|
||||
|
||||
Enrollment tokens are single-use with expiry, created in the admin UI, scoped `enroll`. The device credential is a long-lived bearer secret, scoped `run-tests`; it is also the HKDF input for session keys. Revocation = deleting the device in the admin UI.
|
||||
|
||||
### 2.2 Profile
|
||||
|
||||
Reference in New Issue
Block a user