From 729098756dfa86b7771b37fad05583a34d50cc14 Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Mon, 7 Sep 2026 16:47:48 -0400 Subject: [PATCH] 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 --- docs/DECISIONS.md | 52 +++++++++++++++++++++++++++++++++++++++++++++++ docs/IRIS.md | 24 ++++++++++++++++++++++ docs/RUST.md | 49 ++++++++++++++++++++++++++++---------------- 3 files changed, 107 insertions(+), 18 deletions(-) diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 6cdc135..515854a 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -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," have the full account. The iris-vs-Masonry choice itself is still 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=` (`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. diff --git a/docs/IRIS.md b/docs/IRIS.md index 02d2247..ad2fd1a 100644 --- a/docs/IRIS.md +++ b/docs/IRIS.md @@ -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 reply shapes exactly **1** text layout, the same as into a 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=`, 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. diff --git a/docs/RUST.md b/docs/RUST.md index 2cb9452..5282dba 100644 --- a/docs/RUST.md +++ b/docs/RUST.md @@ -131,6 +131,13 @@ What exists now: how many lines are held and when the last arrived, and what the uploader 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, 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 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). -- [ ] Iris app enrolment (decided 2026-09-07): the bench APK's log - destination is baked from `AI_APP_LOG_*` at build time, which cannot - work for Iris's phone -- the APK is built in the VM, whose CA and - token are not the host's, and no secret may go in a repo or a - delivered artifact. Replace with the mechanism the Compose app and - `desktop-app` already use: the iris Android app registers the - `aiapp://enroll?host&port&token` VIEW intent, stores the enrolment - the way `client_core::config` does, pins the CA the same way (the - Compose app reads it at build time from the *building* machine, which - is the same problem -- so for the phone the CA has to arrive with the - link or the build has to happen on the host; check how Dev Updater's - Enroll button and `ai-server --enroll-link` present the link and - whether the CA fingerprint can ride in it as a query parameter, which - `wg-app-link` may already support). `log_upload` then takes its - destination from the enrolment at runtime, `AI_APP_LOG_*` is removed, - and Dev Updater's enrol screen offers the link as a tappable link so - Android hands it to the iris app. Rejected: building the iris APK on - the host (needs cargo-ndk and an NDK there, and still bakes a token). +- [x] Iris app enrolment (**done 2026-09-07**): the iris Android app is + told which `ai-server` to talk to by an `aiapp://enroll` link, exactly + as the Compose app and `desktop-app` are, instead of having it compiled + in. `MainActivity` registers the VIEW intent and hands the URI and the + app's private files directory to Rust (`src/enrollment.rs` is the intent + plumbing and nothing else); the parsing, the file and its 0600 mode are + `client_core::config`'s `EnrolledServer`/`EnrollmentStore`, shared with + the desktop app. `build.rs`'s `AI_APP_TRANSCRIPT_HOST`/`_PORT`/`_TOKEN` + are gone, and with them a token in a built artifact. + **The CA travels with the link** (`&ca=`, base64url of the DER) -- + docs/DECISIONS.md, 2026-09-07, has the decision, the two rejected + alternatives and what the longer link costs a QR code. That is what + makes an APK cross-compiled here work against the server on the host. + Diagnostics says which of three things is true -- `enrolled: host:port`, + `not enrolled -- open the enrol link from Dev Updater`, or + `enrolment unreadable: ...` -- because "could not find out" wants a + different action from "nothing there yet". + 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 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.