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.