One Verification Core: How the Desktop and Mobile Share Rust

A protocol whose desktop tool and mobile tool answer "is this seal valid" with two different code paths is a protocol that can lie to its user in two different ways. From the day I started Oversight, the rule was that verification has exactly one implementation, the Rust core, and every surface that calls it is a thin presentation layer over the same answer. The desktop CLI does that. The browser inspector at /viewer/ does that, with the Rust manifest parser ported to TypeScript and pinned by cross-language conformance. The mobile verifier (oversight-protocol/oversight-mobile) does it most directly: the same Rust crates, embedded into the app binary via flutter_rust_bridge.

Today I shipped two changes that together turn that rule into a reproducible artifact. Oversight v0.4.8's docs/EMBEDDING.md documents the integration contract for downstream consumers, and oversight-mobile v0.1.12 switches the Flutter app from sibling-directory path deps to a git-plus-tag pin against that same v0.4.8 release. A fresh clone of the mobile repo now builds without sibling-checkout choreography, and the desktop and mobile binaries are guaranteed to be running identical Rust source. I want to write down why path deps were the right call for development and the wrong call for shipping, what the seven-crate contract actually is, and what this unblocks downstream.

Path deps were right until they weren't

Until v0.1.12, the mobile repo's rust/Cargo.toml referenced the desktop crates this way:

oversight-crypto = { path = "../../oversight-gh/oversight-rust/oversight-crypto" }

That worked because my development machine has both repositories checked out as siblings. Cargo path deps are fast to iterate on; an edit to a crate is visible to the next mobile build with no fetch step. While I was bringing up the Flutter and Rust integration and chasing TestFlight gotchas, the path dep let me change the Rust core and the Flutter wrapper in the same edit cycle, run the iOS build on a GitHub-hosted macOS runner, and see the result without thinking about versions.

The day the mobile verifier landed on TestFlight as v0.1.11, the path dep started being a liability. Three problems showed up at once. A contributor cloning oversight-mobile alone could not build it; they had to also clone the desktop repo, rename the directory to oversight-gh, and place it as a sibling. The CI workflows worked around this by running git clone --depth 1 ... ../oversight-gh as a hidden step, which kept the build green but obscured the actual dependency graph. And the F-Droid story (reproducible mobile builds anyone can verify against the published binary) requires a stable reference to the Rust core. A path dep resolves to "whatever was sitting next to me at the time," which is the opposite of stable.

Seven crates, named exactly

The first part of the fix was deciding what "the verification core" actually means. The oversight-rust workspace has eleven crates. Not all of them belong in a verifier binary. The new embedding contract names the seven that do, and the four that do not, in docs/EMBEDDING.md:

Verifier-safe (embed these): oversight-crypto for the X25519, Ed25519, ChaCha20-Poly1305, HKDF-SHA256 primitives; oversight-manifest for manifest parsing, signature verification, and canonicalization; oversight-container for .sealed parsing and authenticated decryption; oversight-tlog for transparency-log proof verification; oversight-rekor for fetching tree heads and inclusion proofs from the Sigstore Rekor v2 public log; oversight-watermark for L1 zero-width and L2 whitespace mark detection (read-only); oversight-policy for unseal-time policy evaluation (allow, deny, warn).

Not for embedding: oversight-cli (binary, not a library); oversight-registry (Axum + SQLx server, sender-side and operator-side); oversight-formats (PDF, DOCX, image-LSB watermark application; sender-only and pulls in heavy format dependencies); oversight-semantic (L3 synonym rotation; sender-only; the verifier path uses oversight-watermark for detection only). A downstream embedder reaching for these is probably reaching for the wrong API. The pattern I want is "you embed the verifier and your platform layer; the sender stays on a full desktop install."

Git plus tag, not main, not path

The second part of the fix was picking a pin pattern. Cargo gives you three options for non-crates.io dependencies: path, git plus branch (or implicit main), and git plus a tag or revision. Path is what we just discussed. Git plus branch is worse than path for production purposes, because the pin moves silently every time the branch advances. Git plus tag is what release engineering should actually look like: every published release is a tag, every tag is a commit, and a Cargo.lock checked in next to the consumer captures the resolved commit so every build on every machine resolves identically.

The mobile rust/Cargo.toml at v0.1.12 reads:

oversight-crypto    = { git = "https://github.com/oversight-protocol/oversight.git", tag = "v0.4.8" }
oversight-manifest  = { git = "https://github.com/oversight-protocol/oversight.git", tag = "v0.4.8" }
oversight-container = { git = "https://github.com/oversight-protocol/oversight.git", tag = "v0.4.8" }
oversight-tlog      = { git = "https://github.com/oversight-protocol/oversight.git", tag = "v0.4.8" }
oversight-rekor     = { git = "https://github.com/oversight-protocol/oversight.git", tag = "v0.4.8" }
oversight-watermark = { git = "https://github.com/oversight-protocol/oversight.git", tag = "v0.4.8" }
oversight-policy    = { git = "https://github.com/oversight-protocol/oversight.git", tag = "v0.4.8" }

Cargo fetches the repository once, resolves all seven entries against the same checkout, and the lock file pins the resolved commit (af6f725c for v0.4.8) so subsequent builds are deterministic. v0.4.8 is the chosen pin because it is the minimum desktop tag that supports 32-bit Android cross-compile; the MAX_CIPHERTEXT_BYTES portability fix that landed in v0.4.8 is exactly what unblocks armv7-linux-androideabi and i686-linux-android. Older tags would compile for 64-bit Android and iOS but break the four-ABI Android matrix.

What this actually unblocks

The clone-and-build story now works without ceremony. A new contributor runs:

git clone https://github.com/oversight-protocol/oversight-mobile.git
cd oversight-mobile
flutter pub get
flutter run

and the build succeeds against an attached device or simulator. Cargo fetches the v0.4.8 Rust core during the first compile and caches it like any other git dep. There is no sibling repo, no rename step, no hidden CI workaround. The CI workflows lost a step in the process; both the iOS and Android jobs used to start with git clone --depth 1 https://github.com/oversight-protocol/oversight.git ../oversight-gh as a side channel into the dependency graph, and v0.1.12 deletes both.

The reproducibility story also gets concrete. A reproducible mobile build needs three things: deterministic source, deterministic toolchains, and deterministic dependency resolution. Source is a git checkout; toolchains are pinned in the workflow's RUST_VERSION and FLUTTER_VERSION environment variables; dependency resolution is now a tag pin plus a committed lock file. The remaining non-determinism is in the build profile (LTO, codegen-units, strip), which the mobile repo has set to deterministic values for some time. A downstream verifier who clones the tagged release, runs the documented build, and compares the resulting binary hash against what is in the GitHub Releases artifact attachment will get an answer that is meaningful. That is the gate F-Droid wants and the gate I want for any user who is skeptical of the App Store binary.

Bumping the upstream pin became cheap. When v0.4.9 ships, the entire upgrade procedure on the mobile side is a single-line edit to rust/Cargo.toml changing tag = "v0.4.8" to tag = "v0.4.9", plus a CHANGELOG entry describing what the new core gives the mobile build. There is no CI surgery, no profile review, no "pin to a different branch" coordination. The upgrade is the kind of change a CI bot could open.

The same contract works for whatever ships next

The blunt reason for documenting this contract upstream rather than treating it as a one-off mobile concern is that mobile is unlikely to be the last embedder. The verifier shape that fits in flutter_rust_bridge also fits in an Electron app over Tauri, in a browser via wasm-bindgen, in a server-side verifier deployed alongside an existing application, and in any third-party tool that wants to confirm a sealed bundle without taking a runtime dependency on the Python reference. The seven-crate split, the v0.4.8-and-up minimum, and the git-plus-tag pin are the same recommendations regardless of who the embedder is. The mobile companion is the worked example; the contract is the project's policy.

v0.4.8 ships the doc, v0.1.12 ships the proof, and the gap between "the desktop and mobile run the same code" being a slogan and being a verifiable artifact closed today. The integration contract lives at docs/EMBEDDING.md; the worked example is oversight-protocol/oversight-mobile; the mobile status page is at /mobile/. If you are thinking about building a fourth surface on top of the same Rust core, the doc is the place to start, and an issue with your embedder name plus the target triple is welcome.