docs: the CA travels in the enrol link, and why not the two alternatives

DECISIONS.md gets the decision with both rejected options and what the
longer link measures (89 -> 652 bytes, a 45x23 QR -> 93x47), RUST.md ticks
the enrolment queue item and marks the log-upload route superseded rather
than editing it, and IRIS.md says what changed for anyone building the
Android app.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Fable 5.1 committed 2026-09-07 16:47:48 -04:00
1 parent d8562d96a3
commit 729098756d
3 files changed
+107 -18

No files matched your search

+52
View File
@@ -534,3 +534,55 @@ marked **DEFERRED** are ones the agent chose not to decide alone.
pass," and "The three remaining I5 verifications, closed 2026-09-05," pass," and "The three remaining I5 verifications, closed 2026-09-05,"
have the full account. The iris-vs-Masonry choice itself is still have the full account. The iris-vs-Masonry choice itself is still
Iris's to make. Iris's to make.
## 2026-09-07: the enrolment link carries the CA, so an APK need not be built where its server runs
**Problem.** Every phone build pinned the CA of the machine that compiled
it -- the Compose app from `GeneratePinnedCert`, the iris app from
`build.rs` reading `$XDG_CONFIG_HOME/ai-app/certs/ca.pem`. That is fine
while the two are the same machine and impossible when they are not, which
is exactly the iris client's situation: cross-compiled in this VM,
delivered to a phone, run against `ai-server` on the host. Baking the
host/port/token as well made it worse -- a token in a built artifact.
**Decided: the CA rides in the enrolment link**, as `&ca=<base64url of the
DER>` (`wg_app_link::enroll::ca_param`), optional and per mint. The app
that opens the link pins what the link said, and an APK built anywhere
works against whatever server it is pointed at.
Two alternatives were worked out and rejected.
- **A CA *fingerprint* in the link, pinned at the TLS handshake.** The
smallest link (43 more characters) and the strongest shape, but `ureq`
3.4 exposes no hook for a custom `rustls` `ServerCertVerifier`: its
`TlsConfig` builds the `ClientConfig` itself, so this needs a hand-written
`Connector` on the `unversioned` API and `rustls` as a direct dependency
of `client-core`. A lot of machinery in the one crate that must stay
light.
- **A fingerprint in the link plus an unauthenticated `GET /ca.pem`.**
Small code, but it needs a first connection with verification disabled,
and it breaks a documented, tested posture -- `auth.rs`'s "gates every
route with zero unauthenticated endpoints", which is a load-bearing
decision rather than an implementation detail. Not something to change
silently for this.
**What it costs**, measured rather than guessed: on this project's P-256
CA the link goes from 89 bytes to 652, and `print_enrollment`'s terminal
QR from 45x23 to 93x47 characters. That is why the parameter is the
minter's choice per call: `ai-server` passes it (its iris client needs it),
`dev-updater` passes `None` (its app is built on the machine it talks to,
and its QR stays scannable in an 80-column terminal). The URI printed under
the QR is the fallback either way, and is the path Dev Updater's Enroll
button already uses -- it opens the link with `ACTION_VIEW`, so Android
offers whichever apps registered the scheme, which needed no change here.
The CA is a public certificate, so putting it in the QR leaks nothing the
token did not already: photographing the terminal still costs exactly the
token, which is rotatable.
**The log upload's destination is moot**, so it is not wired to this. On
the same day Iris decided Dev Updater will read an APK's runtime log from
an on-device ContentProvider instead, which removes `log_upload`,
`POST /client-log` and the `AI_APP_LOG_*` baking altogether -- so the
enrolment landed without touching any of them, for that change to delete
whole.
+24
View File
@@ -1160,3 +1160,27 @@ and per-block-row work (RUST.md's "Verification pass over Tasks A and B").
measured. With the counter it is a test: one delta into a 100-paragraph measured. With the counter it is a test: one delta into a 100-paragraph
reply shapes exactly **1** text layout, the same as into a reply shapes exactly **1** text layout, the same as into a
one-paragraph one. one-paragraph one.
## 2026-09-07: the phone app is told which server to talk to, and pins from the link
Not an iris API change -- a client-facing one, in the crates around it,
worth knowing because it changes what a build of the Android app *is*.
- **An iris APK is no longer tied to the machine that compiled it.** It
used to have the server's host, port, token and CA compiled in, which
made a build good for exactly one emulator/server pair and put a token
in the artifact. Now it registers `aiapp://enroll` like the Compose app:
open the link (Dev Updater's Enroll button already offers it, and the
phone asks which app should take it) and the app stores where to go and
what to trust.
- **The CA rides in the link** as `&ca=<base64url DER>`, which is what
makes the above possible at all -- a pinned certificate cannot be baked
into an APK cross-compiled somewhere else. Optional, so the projects
that do build on their own machine keep the short link and the small QR.
docs/DECISIONS.md, 2026-09-07, has why not a fingerprint.
- **`client_core::config` now holds the storage as well as the parsing**:
`EnrolledServer` gained an optional `ca_pem`, and `EnrollmentStore` (the
0600 JSON file, moved out of `desktop-app`) is one implementation for
both the desktop and the phone -- only the directory differs.
`desktop-app --ca` is now the override for a link that carried no CA
rather than a required flag.
+31 -18
View File
@@ -131,6 +131,13 @@ What exists now:
how many lines are held and when the last arrived, and what the uploader how many lines are held and when the last arrived, and what the uploader
last did. last did.
**Superseded 2026-09-07, and left standing rather than edited**: Dev
Updater is growing an on-device runtime-log reader, so this whole route --
`log_upload`, `POST /client-log` and the `AI_APP_LOG_*` baking -- is being
removed rather than kept current. The app's *server* destination no longer
comes from any of it: that is the enrolment link (the queue item above).
What follows describes the route as built.
**How to use it.** Build the APK with the destination in the environment, **How to use it.** Build the APK with the destination in the environment,
on the machine `ai-server` runs on: on the machine `ai-server` runs on:
@@ -539,24 +546,30 @@ closes it.
handing its server token to the app it installed (leaks the token handing its server token to the app it installed (leaks the token
into every managed app); the app posting to ai-server (needs its own into every managed app); the app posting to ai-server (needs its own
enrolment first and puts the phone's logs in the wrong component). enrolment first and puts the phone's logs in the wrong component).
- [ ] Iris app enrolment (decided 2026-09-07): the bench APK's log - [x] Iris app enrolment (**done 2026-09-07**): the iris Android app is
destination is baked from `AI_APP_LOG_*` at build time, which cannot told which `ai-server` to talk to by an `aiapp://enroll` link, exactly
work for Iris's phone -- the APK is built in the VM, whose CA and as the Compose app and `desktop-app` are, instead of having it compiled
token are not the host's, and no secret may go in a repo or a in. `MainActivity` registers the VIEW intent and hands the URI and the
delivered artifact. Replace with the mechanism the Compose app and app's private files directory to Rust (`src/enrollment.rs` is the intent
`desktop-app` already use: the iris Android app registers the plumbing and nothing else); the parsing, the file and its 0600 mode are
`aiapp://enroll?host&port&token` VIEW intent, stores the enrolment `client_core::config`'s `EnrolledServer`/`EnrollmentStore`, shared with
the way `client_core::config` does, pins the CA the same way (the the desktop app. `build.rs`'s `AI_APP_TRANSCRIPT_HOST`/`_PORT`/`_TOKEN`
Compose app reads it at build time from the *building* machine, which are gone, and with them a token in a built artifact.
is the same problem -- so for the phone the CA has to arrive with the **The CA travels with the link** (`&ca=`, base64url of the DER) --
link or the build has to happen on the host; check how Dev Updater's docs/DECISIONS.md, 2026-09-07, has the decision, the two rejected
Enroll button and `ai-server --enroll-link` present the link and alternatives and what the longer link costs a QR code. That is what
whether the CA fingerprint can ride in it as a query parameter, which makes an APK cross-compiled here work against the server on the host.
`wg-app-link` may already support). `log_upload` then takes its Diagnostics says which of three things is true -- `enrolled: host:port`,
destination from the enrolment at runtime, `AI_APP_LOG_*` is removed, `not enrolled -- open the enrol link from Dev Updater`, or
and Dev Updater's enrol screen offers the link as a tappable link so `enrolment unreadable: ...` -- because "could not find out" wants a
Android hands it to the iris app. Rejected: building the iris APK on different action from "nothing there yet".
the host (needs cargo-ndk and an NDK there, and still bakes a token). Dev Updater needed no change: its Enroll button already opens the minted
link with `ACTION_VIEW`, and Android offers the chooser between this app
and the Compose one.
**The log upload was deliberately left out of it**: Dev Updater is
growing an on-device runtime-log reader instead (Iris, 2026-09-07), so
`log_upload`, `POST /client-log` and the `AI_APP_LOG_*` baking are on
their way out whole rather than being rewired first.
- [ ] `iris/android-app/build-apk.sh`: clear Gradle's merged-native-libs - [ ] `iris/android-app/build-apk.sh`: clear Gradle's merged-native-libs
cache when the ABI changes (the x86_64 trap), and make the debug bench cache when the ABI changes (the x86_64 trap), and make the debug bench
APK installable (648 MB) -- RUST.md's logging section names both. APK installable (648 MB) -- RUST.md's logging section names both.