Mobile beta is live — and the Apple-side gotcha that nearly hid it

The Oversight mobile verifier is on TestFlight as of today. The repo is at oversight-protocol/oversight-mobile, the build that just landed for the internal beta is v0.1.11, and the same Rust crates that power the desktop CLI are linked into the iOS and Android binaries. There is exactly one verification implementation; a manifest that opens on a laptop opens the same way on a phone, with the same answer.

The path to that build was not clean. I want to write down what shipped, why the architecture looks the way it does, and the single Apple-side detail that cost me six TestFlight builds I could see in App Store Connect but my testers could not. If you have ever uploaded to TestFlight, succeeded, and then watched the build never appear on a tester's phone, this post is the answer.

Why a phone-shaped surface

A provenance protocol whose only verifier is a Python CLI is a protocol nobody verifies. The recipient of a sealed document is rarely the same person who installed the issuer's tooling. They are a journalist who got handed a leaked memo, an analyst checking whether the dataset they pulled is the dataset they were told they pulled, an auditor confirming that a contract bundle has not been tampered with since signing. They have a phone. They do not have pip.

The mobile app does one thing: take an Oversight artifact (a hash, a QR code, or a .oversight bundle), check it against the public Sigstore Rekor log, and tell you whether the signature is valid, whether the inclusion proof checks out, and what the manifest says about who issued it and to whom. No accounts. No telemetry. No server in the middle. The cryptography happens on the device, against a public append-only log that anybody can audit.

One core, two surfaces

The verifier is Flutter on top of Rust. The UI layer is Dart, single codebase, both platforms. The verification core is the existing oversight-rust workspace, embedded via flutter_rust_bridge and called from Dart through a generated FFI shim. There is no second implementation of Rekor proof verification, no second implementation of manifest signature checks, no second copy of the synonym dictionary. Whatever passes the desktop conformance suite passes the mobile build, because they compile from the same source tree.

That choice has a cost. Cross-compiling Rust for iOS arm64, Android arm64, and Android x86_64 is a real pipeline. The repo carries cargo configurations for each target, a CI job that primes the toolchains on the runner, and a small wrapper crate that hides the platform-specific async runtime selection. The payoff is that I do not have to maintain two cryptography stacks. The most likely way a mobile verifier would lie to its user is by drifting out of sync with the canonical implementation; that class of bug is structurally impossible here.

iOS without a Mac

I do not own a Mac. The entire iOS pipeline runs on GitHub-hosted macOS runners. flutter build ios --release --no-codesign assembles the .app, xcodebuild archive signs it with a Distribution certificate imported from a base64-encoded .p12 in Actions secrets, and xcrun altool --upload-app ships the IPA to App Store Connect with a key from the App Store Connect API. Nothing on my machine ever touches an Xcode project file.

The interesting constraint is that the runner is headless. There is no keychain, no Xcode project history, no developer account session. Every secret has to arrive through the workflow's encrypted environment, and every signing operation has to declare every input explicitly. The apple-actions/import-codesign-certs and apple-actions/download-provisioning-profiles actions handle the keychain and profile setup; everything else is a shell script in the workflow file. It is reproducible, it is auditable, and it does not require me to own $2,500 of Apple hardware to ship.

The Info.plist key that hides every build

Here is the part that cost me a day. After v0.1.4 made it onto TestFlight and a tester actually installed it, I shipped six more builds (v0.1.5 through v0.1.10) over the next several hours. Each one passed CI. Each one returned UPLOAD SUCCEEDED with no errors from altool. Each one received an "uploaded build had one or more issues" email from App Store Connect that, on close reading, said delivery was successful and the warning was advisory. None of them showed up on the tester's phone.

The cause is a key called ITSAppUsesNonExemptEncryption. If your Info.plist does not declare it, App Store Connect parks every new build in a state called "Missing Compliance," which is invisible to internal-testing groups by default. The build is uploaded. The build is processed. The build sits there forever, gated behind a manual question that asks whether your app uses non-exempt encryption.

Oversight's mobile verifier uses standard system cryptography (HTTPS, the platform's TLS stack, and the curated primitives in RustCrypto) and nothing in the regulated-export category. The honest answer is "no." Set the key once in the manifest:

<key>ITSAppUsesNonExemptEncryption</key>
<false/>

and every future build sails through compliance the moment it finishes processing, with no manual click and no human in the loop. v0.1.11 was the first build to ship with the key in place. App Store Connect processed it in about two minutes. It auto-attached itself to the internal testing group. It was on the tester's TestFlight app a few minutes after that, with a green check and no warning badge.

I am writing this down because the Apple documentation for the key is accurate but easy to miss, and the failure mode is silent. There is no error. There is no rejection email. The build simply does not appear, which makes the obvious next move ("upload another build") cost you another submission slot for nothing.

The April 28 SDK cutoff

The other thing the same v0.1.11 release fixed is the iOS 26 SDK migration. Apple announced months ago that starting 2026-04-28, every new upload has to be built with Xcode 26 or later, against the iOS 26 SDK. That is two days from now. Builds against the iOS 18 SDK get a warning email today and a hard rejection from Wednesday on. The fix on the GitHub Actions side is one line: bump the runner image label from macos-15 to macos-26, which has Xcode 26 preinstalled. The runtime image was already in the GitHub-hosted runner pool; I had not realized I was still pinned to the previous one until the ITMS-90725 warnings made it explicit.

What is in the build, and what is not

v0.1.11 is a pure verifier. It scans QR codes, accepts pasted hashes, and opens .oversight bundles via the iOS share sheet. It pulls live from Sigstore Rekor and checks inclusion proofs against the verified log root. It shows the manifest fields: issuer, recipient, content hash, L3 mode, and the timestamp from RFC 3161. It does not store any of that anywhere; the local history is encrypted at rest with a per-install key in the platform keystore.

It does not sign. Hardware-backed signing — Secure Enclave on iOS, StrongBox on Android — is the v2 work, and it is gated on a real threat model for what kind of issuer a phone-bound key should be allowed to be. A device that can be confiscated at a border crossing is not the same trust anchor as an air-gapped laptop in a SCIF, and the protocol's policy primitives need to reflect that before a mobile signer ships.

What recruiters and operators care about

There is a slightly different audience for this post than for the L3 safety or SIEM export posts: people who evaluate the project as a whole rather than any one feature. For them, the short version is that Oversight is a cryptographic protocol with an end-to-end reference implementation, a conformance test suite, a federation spec, three SIEM-native exporters, a desktop GUI, and a mobile verifier. The mobile build is not a marketing skin. It is the same Rust verification core, on a different surface, with the platform-store distribution cleared end-to-end on a CI pipeline that does not require me to own a Mac. Everything is Apache 2.0, everything is in public repos, every release is a tag with a matching CI run.

Reproducible mobile builds are next. When that lands, you will be able to clone the repo, run one command, and confirm that the binary distributed by the App Store and Play Store is byte-identical to what you built locally. That is the point at which "trust the protocol" stops requiring "trust the store." Until then, the source is public and every artifact is traceable to a commit.