v0.4.4: Failing Closed

Most of the releases on this blog add a capability. This one removes a class of quiet lie. v0.4.4 is a dedicated security hardening pass, started from the v0.4.3 Python package baseline and folding in a Codex review that read the code adversarially rather than charitably. The theme across all fourteen fixes is the same: when Oversight cannot prove the safe thing, it must fail closed — refuse, raise, reject — instead of silently doing the convenient thing and reporting success.

Why the version moves off v0.4.3

The honest reason the package metadata jumps from 0.4.3 to 0.4.4 is so nobody confuses the hardened tree with the baseline that preceded it. The historical v0.5.0 Rekor/Rust exploration still lives in git history and the Rust workspace, but the download line that users install from is this one. If you are on 0.4.3, the version string is the signal: upgrade. A security release that is hard to distinguish from the thing it fixes is a security release that does not get installed.

The container counts opens honestly now

A sealed container enforces a max_opens limit. The bug: the counter incremented on every attempt, including attempts that never produced plaintext. A recipient who failed to decrypt — wrong key, tampered bytes — still burned an open. Worse, the accounting was generous in the wrong direction in some paths. v0.4.4 increments max_opens only after a successful decrypt, so the limit means what it says: number of times this content was actually revealed.

In the same pass, seal_multi() — sealing one document to multiple recipients — is disabled outright. Not deprecated, disabled. The manifest format cannot yet honestly represent multiple recipients, and shipping a multi-recipient seal whose manifest cannot describe what it did is exactly the kind of silent dishonesty this release is about. It comes back when the manifest can tell the truth about it.

Policy modes fail closed

Oversight's policy layer governs whether open-counting is authoritative locally (LOCAL_ONLY), enforced by a registry (REGISTRY), or both (HYBRID). The dangerous behavior was that REGISTRY and HYBRID would, when the registry was unreachable, quietly fall back to local state and keep going as if nothing were wrong. That turns a network outage into a silent downgrade of the security model the issuer chose. v0.4.4 makes both modes fail closed: if the registry that is supposed to be authoritative cannot be reached, the open is refused, not approximated. LOCAL_ONLY counter locking also now actually works on Windows, where the previous lock was a no-op.

Rekor verification rejects digest mismatches

Offline verification of a Rekor DSSE envelope is supposed to confirm that the logged attestation is about this content. The prior code verified the signature but did not insist that the envelope's subject digest match the expected content hash — so a validly signed attestation about a different document could be accepted against the wrong file. v0.4.4 rejects any DSSE envelope whose subject digest does not match the expected content hash. A signature over the wrong subject is not evidence about the right one.

The registry stops trusting unsigned material

Several registry fixes, all in the same spirit:

  • Rekor attestations now use real watermark mark IDs and the manifest's actual content_hash, instead of a placeholder like mark:<file_id> that recorded the right shape but the wrong fact.
  • /register rejects unsigned beacon and watermark sidecars that do not match the signed manifest. A sidecar that is not bound to the signed record is not part of the record.
  • Evidence bundles now include local transparency-log inclusion proofs for the recorded events, not just the signed tree head — so an auditor can verify that a specific event is actually in the log, not merely that a log exists.
  • Registrations with no watermark are skipped for attestation rather than logged under a synthetic mark, because attesting to a watermark that was never applied is a false statement.

DNS beacons require a secret to be trusted

The DNS beacon path lets a recipient's environment phone home a passive lookup so an issuer can see that a sealed file was opened. The hardening: beacon callbacks now support a shared OVERSIGHT_DNS_EVENT_SECRET, and any non-loopback callback fails closed when no secret is configured. An unauthenticated callback from an arbitrary host is not an event worth recording, and pretending it is would let anyone forge open-evidence.

Format adapters stop lying about success

Three format fixes, each closing a "reported success but did nothing safe" gap:

  • The text adapter now applies L3 before L2/L1, matching the canonical core pipeline order instead of a divergent local order.
  • DOCX metadata insertion no longer reports success when the <cp:keywords> element is missing — it failed to write and said it succeeded.
  • PDF processing now rejects indirect Launch, JavaScript, and unsafe URI actions before rewriting the file, so the watermarker does not faithfully preserve an active exploit primitive inside a document it just vouched for.

The empty tree gets its real root

The transparency log used an all-zero placeholder for the empty-tree root. RFC 6962 specifies the empty-tree Merkle root as SHA-256(""). v0.4.4 uses the real value. This is the kind of fix that matters for exactly one reason: conformance with auditors who verify against the spec rather than against my implementation.

Rust at parity

None of this is real until the Rust port does it too, because the conformance test between Python and Rust is the ground truth this project is built on. So the Rust workspace lands the same posture: oversight-container and oversight-policy enforce max_opens only after a successful recipient decrypt, REGISTRY/HYBRID fail closed instead of falling back to local counters, and Rust seal_multi() fails closed until recipient-honest manifests exist. oversight-rekor mirrors the Python DSSE subject-digest rejection. oversight-registry requires the DNS event secret for non-loopback callbacks and fails registration on malformed signed artifacts instead of dropping them silently. oversight-formats gets the same DOCX keywords and PDF action-rejection fixes. And the direct rand dependency is gone in favor of rand_core::OsRng, clearing the low-severity advisory path.

What v0.4.4 is not

v0.4.4 adds no features. It does not solve multi-recipient sealing — it disables it until the manifest can be honest. It does not make L3 collusion-resistant. It does not ship a GUI; that is the next release. What it does is take a tree that mostly worked and remove the places where it would rather succeed quietly than fail honestly. A provenance protocol earns trust by being conservative about what it claims, and the fastest way to lose that trust is a security model that downgrades itself the moment the network blinks. v0.4.4 is the release where Oversight stops doing that.