A Week of Boring Hardening

The last development note was on May 17. I had just come back from being sick for a few days, and the work then was the first practical bridge from the Python registry to the Rust registry: live deployment shape, operator-token parity, and migration tooling. The week since then has been the same kind of work, just deeper in the stack. Less visible, more important.

Oversight is a security protocol. That means the release story cannot only be about the happy path: seal a file, open a file, verify a bundle. The ugly cases matter more. What happens when a migrated database has an orphaned beacon row? What happens when an event points at a transparency log index that exists, but the leaf at that index is for different evidence? What happens when the local tlog file has a corrupted line? What happens if a mobile release quietly grows a network permission or a telemetry dependency?

This week's answer is that those cases now fail louder.

Registry burn-in got stricter

The Rust Axum registry has moved from "can serve the v1 surface" toward "can survive operator migration without hiding bad evidence state." The new oversight-registry --validate-db path started by checking the database relationships that matter after a Python-to-Rust import: manifests, beacons, watermarks, events, corpus rows, identity bindings, manifest signatures, and manifest/file ID agreement.

That validator now goes further. It rejects malformed event extra JSON and malformed corpus metadata JSON. It reports duplicate transparency-log indexes, negative indexes, missing event tlog indexes, and indexes outside the on-disk tlog size. Then it checks the part that is easy to miss: an event row's tlog index must point at a leaf whose payload actually matches that event row.

An in-range index is not enough. If row 24 says a DNS beacon fired for token A, but tlog leaf 24 is a different beacon for token B, the database is not clean. It may be accidental corruption, a failed migration, or a bad operator action, but it is not evidence an auditor should accept. The Rust validator now compares the event kind, token, file binding, recipient binding, source IP, user agent where applicable, timestamp, and DNS sidecar fields against the indexed tlog leaf.

That is boring. It is also exactly the kind of boring a registry needs before it becomes the default backend.

The tlog itself now fails closed on recovery

Yesterday's validator work caught database rows pointing at unrelated leaves. Today's follow-up tightened the local transparency log itself. The Rust oversight-tlog crate used to recover existing leaves permissively: malformed lines or bad hashes could be skipped during startup. That is the wrong posture for an append-only evidence log. If the log on disk is corrupted, the service should say so.

Recovery now rejects malformed leaf records, non-contiguous indexes, bad hash lengths, and leaf-hash mismatches. New leaf records also carry leaf_data_hex, which preserves the exact bytes used to compute SHA-256(0x00 || leaf_bytes). The older leaf_data field remains as the readable JSON/display value for registry events, so the public range endpoint stays usable for humans and existing consumers.

This is a small schema hardening, but it closes a real class of ambiguity. A verifier or monitor can recompute the RFC 6962 leaf hash from exact bytes instead of trusting a lossy UTF-8 display field. The registry v1 spec now says that explicitly for /tlog/range.

Writes fail closed too

The registry write paths were tightened from the other side as well. Register, HTTP beacon, DNS beacon, OCSP-style beacon, and license-style beacon writes now fail if the local transparency log cannot append. Before that change, a service could store a new database row while failing to write the audit-trail leaf. That is a split-brain evidence problem. It is better to return an internal error than to create a row that looks real but cannot be proven against the local log.

These three pieces line up: append must succeed before evidence is stored, database rows must point at real matching leaves, and recovered leaves must verify against their own hashes. That is the shape I want before the Rust registry is called stable.

Format parity kept moving

The registry was the main thread, but it was not the only one. The Rust format adapters also picked up two parity improvements after the last post.

PDF fingerprint text now uses lopdf page extraction plus a parsed content-stream fallback, instead of raw literal scanning. Tests cover Tj, TJ, quote operators, array spacing, and page-level extraction from a generated fixture. Image watermarking now ports the Python adapter's DCT mid-band spread-spectrum path using rustdct, while preserving the blind LSB recovery path for candidate extraction.

Both changes matter because Oversight's Rust path is not meant to be a second, approximate implementation. The goal is one protocol surface with Python as the original reference and Rust as the operational core. Format adapters cannot drift quietly if the registry and mobile app are going to rely on the Rust crates.

The mobile verifier got privacy guardrails

The mobile repo also changed in a way that is deliberately unglamorous. Recent verification history is session-only now, and the app clears the old persisted history key on boot and after verification. Issuer IDs, filenames, and content-hash summaries should not sit in mobile backups just because the user verified a bundle once.

Android and iOS CI now upload an oversight-mobile-build-manifest.json alongside artifacts. The manifest records commit, ref, lockfile hashes, toolchain settings, artifact paths, sizes, and SHA-256 hashes. That is not full reproducible builds yet, but it is the bridge artifact until byte-for- byte verification lands.

The newest guard is scripts/privacy_guard.py. Both mobile workflows run it. It fails if release manifests request network, location, microphone, or contacts access, or if telemetry and crash SDKs are added to pubspec.yaml. The mobile verifier's privacy claim is simple: no accounts, no telemetry, no server-side verification requirement. CI now protects that claim from accidental drift.

Where this leaves main

The current tagged release remains v0.4.11, the hardware-key completion line across Python, Rust, and the browser inspector. The work described here lives on main after that tag. It is pointed at the next gate: Rust registry burn-in, then a wire-format stability statement, then a v1.0 declaration when the implementation has earned it.

This week did not add a flashy feature. It made bad states harder to ignore. That is the progress I care about right now. A security protocol is not bulletproof because its diagram looks good. It gets closer by refusing to lie when storage, migration, logs, and release pipelines go sideways.

Relevant public references: docs/REGISTRY_DEPLOYMENT.md, docs/spec/registry-v1.md, docs/ROADMAP.md, and the mobile verifier repo at oversight-protocol/oversight-mobile.