Zion Boggan zionboggan.com ↗

Harden DNS evidence and Rust RNG dependencies

Co-authored-by: Codex (GPT-5.4) <noreply@openai.com>
04db88b   Zion Boggan committed on Apr 20, 2026 (2 months ago)
CHANGELOG.md +7 -0
@@ -26,6 +26,13 @@ confuse the hardened tree with the vulnerable `v0.4.3` baseline.
hash (`SHA-256("")`) instead of an all-zero placeholder.
- `oversight_core/__init__.py`, `pyproject.toml`, and the Rich CLI banner:
version metadata is now `0.4.4`, marking this post-`0.4.3` hardening train.
+- `oversight_dns/server.py` and `registry/server.py`: DNS beacon callbacks now
+ support a shared `OVERSIGHT_DNS_EVENT_SECRET`, and non-loopback callbacks
+ fail closed when no secret is configured.
+- `registry/server.py`: evidence bundles now include local transparency-log
+ inclusion proofs for recorded events, not just the signed tree head.
+- `oversight-rust`: removed the direct `rand` dependency in favor of
+ `rand_core::OsRng`, clearing the low-severity `rand` advisory path.
- Added focused regression coverage in `tests/test_policy_unit.py`,
`tests/test_registry_unit.py`, `tests/test_rekor_unit.py`,
`tests/test_text_format_unit.py`, and `tests/test_tlog_unit.py`.
oversight-rust/Cargo.lock +1 -32
@@ -696,7 +696,6 @@ dependencies = [
"ed25519-dalek",
"hex",
"hkdf",
- "rand",
"rand_core",
"serde_json",
"sha2",
@@ -774,7 +773,7 @@ dependencies = [
name = "oversight-watermark"
version = "0.5.0"
dependencies = [
- "rand",
+ "rand_core",
]
[[package]]
@@ -813,15 +812,6 @@ dependencies = [
"zerovec",
]
-[[package]]
-name = "ppv-lite86"
-version = "0.2.21"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
-dependencies = [
- "zerocopy",
-]
-
[[package]]
name = "prettyplease"
version = "0.2.37"
@@ -856,27 +846,6 @@ version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
-[[package]]
-name = "rand"
-version = "0.8.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
-dependencies = [
- "libc",
- "rand_chacha",
- "rand_core",
-]
-
-[[package]]
-name = "rand_chacha"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
-dependencies = [
- "ppv-lite86",
- "rand_core",
-]
-
[[package]]
name = "rand_core"
version = "0.6.4"
oversight-rust/Cargo.toml +0 -1
@@ -30,7 +30,6 @@ ed25519-dalek = { version = "2", features = ["rand_core"] }
chacha20poly1305 = { version = "0.10", features = ["alloc"] }
hkdf = "0.12"
sha2 = "0.10"
-rand = "0.8"
rand_core = "0.6"
# Serialization / encoding
oversight-rust/oversight-crypto/Cargo.toml +1 -2
@@ -12,8 +12,7 @@ ed25519-dalek.workspace = true
chacha20poly1305.workspace = true
hkdf.workspace = true
sha2.workspace = true
-rand.workspace = true
-rand_core.workspace = true
+rand_core = { workspace = true, features = ["getrandom"] }
hex.workspace = true
zeroize.workspace = true
thiserror.workspace = true
oversight-rust/oversight-crypto/src/lib.rs +1 -2
@@ -33,8 +33,7 @@ use ed25519_dalek::{
VerifyingKey as EdVerifyingKey,
};
use hkdf::Hkdf;
-use rand::rngs::OsRng;
-use rand_core::RngCore;
+use rand_core::{OsRng, RngCore};
use sha2::{Digest, Sha256};
use thiserror::Error;
use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret as X25519StaticSecret};
oversight-rust/oversight-watermark/Cargo.toml +1 -1
@@ -7,4 +7,4 @@ license.workspace = true
description = "Per-recipient text watermarking for Oversight (L1 zero-width, L2 whitespace)"
[dependencies]
-rand.workspace = true
+rand_core = { workspace = true, features = ["getrandom"] }
oversight-rust/oversight-watermark/src/lib.rs +1 -1
@@ -11,7 +11,7 @@
//! Higher-fidelity layers (semantic synonym rotation, DCT image watermarks,
//! PDF/DOCX metadata) live in separate crates so each can evolve independently.
-use rand::{rngs::OsRng, RngCore};
+use rand_core::{OsRng, RngCore};
pub const ZW_SPACE: char = '\u{200b}'; // bit 0
pub const ZW_NONJOIN: char = '\u{200c}'; // bit 1
oversight_dns/server.py +24 -2
@@ -32,6 +32,7 @@ from __future__ import annotations
import argparse
import logging
+import os
import sys
import time
from pathlib import Path
@@ -52,10 +53,17 @@ log = logging.getLogger("oversight_dns")
class OversightResolver(BaseResolver):
"""Resolves queries matching <token_id>.t.<beacon_domain> and logs them."""
- def __init__(self, beacon_domain: str, registry_url: str, answer_ip: str):
+ def __init__(
+ self,
+ beacon_domain: str,
+ registry_url: str,
+ answer_ip: str,
+ registry_secret: str = "",
+ ):
self.beacon_domain = beacon_domain.rstrip(".").lower()
self.registry_url = registry_url.rstrip("/")
self.answer_ip = answer_ip
+ self.registry_secret = registry_secret
self.token_suffix = f".t.{self.beacon_domain}"
def resolve(self, request, handler):
@@ -77,6 +85,9 @@ class OversightResolver(BaseResolver):
log.info(f"DNS beacon fired: token={token_id[:16]}... client={client_ip} qtype={qtype}")
# Report to registry asynchronously (best-effort - we still answer the query)
try:
+ headers = {}
+ if self.registry_secret:
+ headers["X-Oversight-DNS-Secret"] = self.registry_secret
httpx.post(
f"{self.registry_url}/dns_event",
json={
@@ -85,6 +96,7 @@ class OversightResolver(BaseResolver):
"qtype": qtype,
"qname": qname,
},
+ headers=headers,
timeout=2.0,
)
except Exception as e:
@@ -106,6 +118,8 @@ def main():
help="URL of the OVERSIGHT registry, e.g. http://localhost:8765")
p.add_argument("--answer-ip", required=True,
help="A-record answer IP (usually this server's public IP)")
+ p.add_argument("--registry-secret", default=os.environ.get("OVERSIGHT_DNS_EVENT_SECRET", ""),
+ help="shared secret sent to registry /dns_event")
p.add_argument("--port", type=int, default=53)
p.add_argument("--address", default="0.0.0.0")
p.add_argument("--log-level", default="INFO")
@@ -114,7 +128,15 @@ def main():
logging.basicConfig(level=args.log_level,
format="%(asctime)s %(levelname)s %(name)s %(message)s")
- resolver = OversightResolver(args.beacon_domain, args.registry_url, args.answer_ip)
+ if not args.registry_secret and "localhost" not in args.registry_url and "127.0.0.1" not in args.registry_url:
+ log.warning("no registry secret configured; public registry callbacks may be rejected")
+
+ resolver = OversightResolver(
+ args.beacon_domain,
+ args.registry_url,
+ args.answer_ip,
+ registry_secret=args.registry_secret,
+ )
server = DNSServer(resolver, port=args.port, address=args.address,
tcp=False)
tcp_server = DNSServer(resolver, port=args.port, address=args.address,
registry/server.py +64 -1
@@ -20,6 +20,8 @@ import sqlite3
import sys
import threading
import time
+import hmac
+import ipaddress
from collections import OrderedDict
from contextlib import asynccontextmanager, contextmanager
from pathlib import Path
@@ -45,6 +47,7 @@ TLOG_DIR = DATA_DIR / "tlog"
IDENTITY_PATH = DATA_DIR / "registry-identity.json"
TRUSTED_PROXY = bool(int(os.environ.get("TRUSTED_PROXY", "0")))
# When TRUSTED_PROXY=1, honor X-Forwarded-For for rate limiting.
+DNS_EVENT_SECRET = os.environ.get("OVERSIGHT_DNS_EVENT_SECRET", "")
# Rekor v2 wiring (v0.5 Session B). Off by default so existing tests do not
# generate live network traffic. Set OVERSIGHT_REKOR_ENABLED=1 to opt in.
@@ -262,6 +265,31 @@ def _append_tlog(event: dict) -> int:
return TLOG.append(event) if TLOG else -1
+def _tlog_proofs_for_events(events: list[dict]) -> list[dict]:
+ """Attach inclusion proofs for event rows that have local tlog indexes."""
+ if not TLOG:
+ return []
+ proofs = []
+ for i, event in enumerate(events):
+ idx = event.get("tlog_index")
+ if idx is None:
+ continue
+ try:
+ idx = int(idx)
+ except (TypeError, ValueError):
+ continue
+ if idx < 0:
+ continue
+ proof = TLOG.inclusion_proof(idx)
+ if proof is not None:
+ proofs.append({
+ "event_row": i,
+ "tlog_index": idx,
+ "proof": proof,
+ })
+ return proofs
+
+
def _attest_to_rekor(
file_id: str,
issuer_pub_hex: str,
@@ -341,6 +369,38 @@ def _rate_limit(request: Request):
raise HTTPException(429, "rate limit exceeded")
+def _is_loopback_host(host: Optional[str]) -> bool:
+ if not host:
+ return False
+ try:
+ return ipaddress.ip_address(host).is_loopback
+ except ValueError:
+ return host in {"localhost", "testclient"}
+
+
+def _verify_dns_event_auth(request: Request):
+ """Authenticate DNS bridge callbacks before trusting client_ip in the body."""
+ if DNS_EVENT_SECRET:
+ supplied = request.headers.get("x-oversight-dns-secret", "")
+ if not supplied:
+ auth = request.headers.get("authorization", "")
+ if auth.lower().startswith("bearer "):
+ supplied = auth[7:].strip()
+ if hmac.compare_digest(supplied, DNS_EVENT_SECRET):
+ return
+ raise HTTPException(401, "invalid DNS event secret")
+
+ # Local same-host deployments are acceptable without a shared secret; public
+ # deployments must set OVERSIGHT_DNS_EVENT_SECRET to prevent spoofed events.
+ host = request.client.host if request.client else None
+ if _is_loopback_host(host):
+ return
+ raise HTTPException(
+ 503,
+ "OVERSIGHT_DNS_EVENT_SECRET is required for non-loopback DNS event callbacks",
+ )
+
+
def _verify_manifest_signature(manifest_dict: dict) -> tuple[bool, str]:
"""
Parse and verify the manifest's embedded Ed25519 signature.
@@ -615,6 +675,7 @@ def evidence_bundle(file_id: str):
"SELECT * FROM watermarks WHERE file_id=?", (file_id,)
).fetchall()
+ event_dicts = [dict(e) for e in events]
bundle = {
"file_id": file_id,
"bundle_generated_at": timestamp_stub(),
@@ -622,8 +683,9 @@ def evidence_bundle(file_id: str):
"manifest": json.loads(m["manifest_json"]),
"beacons": [dict(b) for b in beacons],
"watermarks": [dict(w) for w in watermarks],
- "events": [dict(e) for e in events],
+ "events": event_dicts,
"tlog_head": TLOG.signed_head() if TLOG else None,
+ "tlog_proofs": _tlog_proofs_for_events(event_dicts),
"disclaimer": (
"This bundle is a provenance record, not a legal finding. For court use, "
"supplement with RFC 3161 qualified timestamps and ISO/IEC 27037 chain-of-custody."
@@ -686,6 +748,7 @@ class DnsEvent(BaseModel):
def dns_event(evt: DnsEvent, request: Request):
"""Called by the oversight_dns server when a beacon DNS query arrives."""
_rate_limit(request)
+ _verify_dns_event_auth(request)
with db() as con:
row = con.execute(
"SELECT file_id, recipient_id, issuer_id FROM beacons WHERE token_id=?",
tests/test_registry_unit.py +67 -1
@@ -9,7 +9,10 @@ from __future__ import annotations
import base64
import json
import os
+import shutil
import sys
+import uuid
+from types import SimpleNamespace
ROOT = os.path.join(os.path.dirname(__file__), "..")
sys.path.insert(0, ROOT)
@@ -19,6 +22,7 @@ from cryptography.hazmat.primitives import serialization
import registry.server as registry_server
from fastapi import HTTPException
+from oversight_core.tlog import TransparencyLog
def _new_identity() -> dict:
@@ -123,14 +127,76 @@ def t2_register_rejects_unsigned_sidecar_mismatch():
print(" [PASS] register rejects unsigned beacon/watermark sidecars")
+def _fake_request(host: str, headers: dict[str, str] | None = None):
+ return SimpleNamespace(
+ client=SimpleNamespace(host=host),
+ headers=headers or {},
+ )
+
+
+def t3_dns_event_requires_secret_for_non_loopback():
+ original_secret = registry_server.DNS_EVENT_SECRET
+ try:
+ registry_server.DNS_EVENT_SECRET = ""
+ registry_server._verify_dns_event_auth(_fake_request("127.0.0.1"))
+ try:
+ registry_server._verify_dns_event_auth(_fake_request("203.0.113.10"))
+ except HTTPException as exc:
+ assert exc.status_code == 503
+ assert "OVERSIGHT_DNS_EVENT_SECRET" in exc.detail
+ else:
+ raise AssertionError("public DNS callbacks should fail closed without a secret")
+
+ registry_server.DNS_EVENT_SECRET = "shared-secret"
+ registry_server._verify_dns_event_auth(
+ _fake_request("203.0.113.10", {"x-oversight-dns-secret": "shared-secret"})
+ )
+ try:
+ registry_server._verify_dns_event_auth(
+ _fake_request("203.0.113.10", {"x-oversight-dns-secret": "wrong"})
+ )
+ except HTTPException as exc:
+ assert exc.status_code == 401
+ else:
+ raise AssertionError("wrong DNS callback secret should be rejected")
+ finally:
+ registry_server.DNS_EVENT_SECRET = original_secret
+ print(" [PASS] dns_event rejects unauthenticated non-loopback callbacks")
+
+
+def t4_evidence_bundle_can_attach_tlog_proofs():
+ original_tlog = registry_server.TLOG
+ td = os.path.join(ROOT, ".tmp-tests", f"registry-tlog-{uuid.uuid4().hex}")
+ os.makedirs(td, exist_ok=False)
+ try:
+ registry_server.TLOG = TransparencyLog(td)
+ first = registry_server.TLOG.append({"event": "register", "file_id": "f"})
+ second = registry_server.TLOG.append({"event": "beacon", "file_id": "f"})
+ proofs = registry_server._tlog_proofs_for_events([
+ {"kind": "register", "tlog_index": first},
+ {"kind": "beacon", "tlog_index": second},
+ {"kind": "offline", "tlog_index": -1},
+ ])
+ finally:
+ registry_server.TLOG = original_tlog
+ shutil.rmtree(td, ignore_errors=True)
+
+ assert [p["event_row"] for p in proofs] == [0, 1]
+ assert [p["tlog_index"] for p in proofs] == [first, second]
+ assert all(p["proof"]["root"] for p in proofs)
+ print(" [PASS] evidence bundles attach tlog inclusion proofs for events")
+
+
def main():
print("=" * 60)
print(" registry.server - focused unit tests")
print("=" * 60)
t1_rekor_attestation_uses_real_mark_id_and_digest()
t2_register_rejects_unsigned_sidecar_mismatch()
+ t3_dns_event_requires_secret_for_non_loopback()
+ t4_evidence_bundle_can_attach_tlog_proofs()
print()
- print(" ALL TESTS PASSED - 2/2")
+ print(" ALL TESTS PASSED - 4/4")
if __name__ == "__main__":