Zion Boggan zionboggan.com ↗

v0.5 Session B: live Rekor v2 wiring + e2e + backcompat tests (#1)

oversight_core/rekor.py
  - upload_dsse: fix request shape against current rekor-tiles proto.
    'verifier' (singular) -> 'verifiers' (repeated array). keyDetails is
    a sibling of publicKey, not nested inside it. raw_bytes carries DER,
    not PEM. Verified live against log2025-1.rekor.sigstore.dev - got
    real log_index returned.

registry/server.py
  - Wire registry to attest registrations into a public Rekor v2 log.
  - Off by default; opt in with OVERSIGHT_REKOR_ENABLED=1. Override shard
    with OVERSIGHT_REKOR_URL. Failures are non-fatal: registry stays
    usable, /register response carries {error: ...} when Rekor is down.
  - Local SQLite event index is preserved as authoritative (only path to
    answer 'list marks for issuer X' queries).

tests/test_rekor_e2e.py
  - Live round-trip + privacy invariant. Gated behind OVERSIGHT_REKOR_E2E=1
    so default test runs do not append entries to the public log.

tests/test_rekor_backcompat.py
  - 5 offline checks that v0.4 contract is preserved: legacy
    TransparencyLog still verifies, missing tlog_kind defaults to
    legacy, JCS encoding unchanged, predicate URI pinned to v0.5 git tag.

Test counts: +5 backcompat, +2 e2e (gated). All offline tests green.
Pre-existing DCT-watermark flake in test_e2e_v2.py (2/3 pass) is
unrelated to this change.

Co-authored-by: Zion Boggan <zionboggan@gmail.com>
b528386   Z committed on Apr 19, 2026 (2 months ago)
oversight_core/rekor.py +22 -10
@@ -310,26 +310,38 @@ def upload_dsse(
) -> RekorUploadResult:
"""POST a DSSE envelope to Rekor v2.
- ``issuer_ed25519_pub_pem`` is the issuer's verification key in PEM.
- Rekor v2 self-managed-key submissions require a verifier key alongside
- the envelope so the log can sanity-check that the envelope is verifiable
- before accepting it.
+ ``issuer_ed25519_pub_pem`` is the issuer's verification key in PEM. The
+ upload payload converts it to the DER (SubjectPublicKeyInfo) bytes that
+ the Rekor v2 ``Verifier.PublicKey.raw_bytes`` field actually requires.
+
+ Wire shape per
+ https://github.com/sigstore/rekor-tiles/blob/main/api/proto/rekor/v2/dsse.proto
+ (verified 2026-04-19): ``verifiers`` is a repeated field; each verifier
+ carries ``publicKey.rawBytes`` (DER) and a sibling ``keyDetails`` enum
+ string (e.g. ``PKIX_ED25519``).
Network errors raise; callers decide whether to retry or fall back to
the local tlog (only acceptable for development, not production).
"""
+ # Rekor's PublicKey.raw_bytes wants DER (SubjectPublicKeyInfo), not PEM.
+ from cryptography.hazmat.primitives import serialization as _ser
+ pub_obj = _ser.load_pem_public_key(issuer_ed25519_pub_pem.encode("utf-8"))
+ pub_der = pub_obj.public_bytes(
+ encoding=_ser.Encoding.DER,
+ format=_ser.PublicFormat.SubjectPublicKeyInfo,
+ )
body = json.dumps(
{
"dsseRequestV002": {
"envelope": json.loads(envelope.to_json()),
- "verifier": {
- "publicKey": {
- "rawBytes": base64.b64encode(
- issuer_ed25519_pub_pem.encode("utf-8")
- ).decode("ascii"),
+ "verifiers": [
+ {
+ "publicKey": {
+ "rawBytes": base64.b64encode(pub_der).decode("ascii"),
+ },
"keyDetails": "PKIX_ED25519",
}
- },
+ ],
}
}
).encode("utf-8")
registry/server.py +87 -0
@@ -36,6 +36,7 @@ from pydantic import BaseModel
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from oversight_core.tlog import TransparencyLog
from oversight_core.manifest import Manifest
+from oversight_core import rekor as rekor_mod
DB_PATH = Path(os.environ.get("OVERSIGHT_DB", "/tmp/oversight-registry.sqlite"))
@@ -45,6 +46,13 @@ 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.
+# 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.
+# Failures are non-fatal: registry remains usable when Rekor is unreachable;
+# the local SQLite tlog continues to be the authoritative event index.
+REKOR_ENABLED = bool(int(os.environ.get("OVERSIGHT_REKOR_ENABLED", "0")))
+REKOR_URL = os.environ.get("OVERSIGHT_REKOR_URL", rekor_mod.DEFAULT_REKOR_URL)
+
SCHEMA = """
CREATE TABLE IF NOT EXISTS beacons (
@@ -254,6 +262,74 @@ def _append_tlog(event: dict) -> int:
return TLOG.append(event) if TLOG else -1
+def _attest_to_rekor(
+ file_id: str,
+ issuer_pub_hex: str,
+ recipient_id: str,
+ recipient_pubkey_hex: Optional[str],
+ suite: str,
+ content_hash_sha256_hex: str,
+ mark_id_hex: str,
+) -> Optional[dict]:
+ """Sign a registration predicate with the registry's identity key and
+ append it to a public Rekor v2 log.
+
+ Returns a small JSON-serializable summary on success (log_url, log_index,
+ log_id, integrated_time) so the response can carry it back to the client.
+ Returns ``None`` when REKOR_ENABLED is false. Returns a dict with an
+ ``error`` field (and no log_index) when the upload itself fails - the
+ caller treats this as non-fatal.
+ """
+ if not REKOR_ENABLED or IDENTITY is None:
+ return None
+ try:
+ recipient_hash = (
+ rekor_mod.hash_recipient_pubkey(recipient_pubkey_hex)
+ if recipient_pubkey_hex
+ else "0" * 64
+ )
+ predicate = rekor_mod.OversightRegistrationPredicate(
+ file_id=file_id,
+ issuer_pubkey_ed25519=issuer_pub_hex,
+ recipient_id=recipient_id,
+ recipient_pubkey_sha256=recipient_hash,
+ suite=suite,
+ registered_at=timestamp_stub(),
+ )
+ statement = rekor_mod.build_statement(
+ mark_id_hex=mark_id_hex,
+ content_hash_sha256_hex=content_hash_sha256_hex,
+ predicate=predicate,
+ )
+ envelope = rekor_mod.sign_dsse(
+ statement=statement,
+ issuer_ed25519_priv=bytes.fromhex(IDENTITY["ed25519_priv"]),
+ )
+ # Build a PEM for the registry's verifier key. Rekor v2 needs PEM.
+ registry_pub = Ed25519PublicKey.from_public_bytes(
+ bytes.fromhex(IDENTITY["ed25519_pub"])
+ )
+ pub_pem = registry_pub.public_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
+ ).decode("ascii")
+ result = rekor_mod.upload_dsse(
+ envelope=envelope,
+ issuer_ed25519_pub_pem=pub_pem,
+ log_url=REKOR_URL,
+ )
+ return {
+ "log_url": result.log_url,
+ "log_index": result.log_index,
+ "log_id": result.log_id,
+ "integrated_time": result.integrated_time,
+ "tlog_kind": rekor_mod.TLOG_KIND,
+ "bundle_schema": rekor_mod.BUNDLE_SCHEMA,
+ }
+ except Exception as e:
+ return {"error": f"{type(e).__name__}: {e}", "tlog_kind": rekor_mod.TLOG_KIND}
+
+
def _rate_limit(request: Request):
if not BUCKET.allow(_client_key(request)):
raise HTTPException(429, "rate limit exceeded")
@@ -348,11 +424,22 @@ def register(req: RegistrationRequest, request: Request):
"timestamp": timestamp_stub(),
})
+ rekor_result = _attest_to_rekor(
+ file_id=file_id,
+ issuer_pub_hex=issuer_pub,
+ recipient_id=recipient_id,
+ recipient_pubkey_hex=recipient.get("x25519_pub"),
+ suite=m.get("suite", "classic"),
+ content_hash_sha256_hex=(m.get("content") or {}).get("sha256", "0" * 64),
+ mark_id_hex=file_id,
+ )
+
return {
"ok": True,
"file_id": file_id,
"registered_beacons": len(req.beacons),
"tlog_index": tlog_idx,
+ "rekor": rekor_result,
}
tests/test_rekor_backcompat.py +146 -0
@@ -0,0 +1,146 @@
+"""
+test_rekor_backcompat
+=====================
+
+Confirms the v0.5 Rekor work has not broken any v0.4 behavior.
+
+Per the v0.5 plan §6 (Backward compatibility rules):
+ 1. Every v0.4.1 bundle/.sealed file must still parse, open, verify exactly.
+ 2. Bundles missing ``tlog_kind`` / ``bundle_schema`` are interpreted as the
+ v0.4 path (oversight-self-merkle-v1).
+ 3. JCS canonical ordering still applies; new fields are additions only.
+
+These checks run fully offline.
+"""
+from __future__ import annotations
+
+import json
+import os
+import sys
+import tempfile
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
+
+from oversight_core.tlog import TransparencyLog, verify_inclusion_proof
+from oversight_core import rekor as R
+
+
+def t1_legacy_tlog_still_works():
+ """A TransparencyLog built and verified the v0.4 way must still pass."""
+ with tempfile.TemporaryDirectory() as td:
+ tl = TransparencyLog(td)
+ for i in range(7):
+ tl.append({"event": "register", "i": i, "file_id": f"f{i}"})
+ size = tl.size()
+ root = tl.root()
+ assert size == 7, f"expected size 7, got {size}"
+ assert len(root) == 32, "root must be 32 bytes (sha256)"
+
+ # Inclusion proof for the middle leaf.
+ proof = tl.inclusion_proof(3)
+ assert proof is not None, "inclusion_proof returned None for valid index"
+ ok = verify_inclusion_proof(
+ leaf_hash=bytes.fromhex(proof["leaf_hash"]),
+ index=proof["index"],
+ proof=[bytes.fromhex(h) for h in proof["proof"]],
+ tree_size=proof["tree_size"],
+ expected_root=bytes.fromhex(proof["root"]),
+ )
+ assert ok, "RFC 6962 inclusion proof failed to verify"
+ print(" [PASS] 1. v0.4 TransparencyLog API still verifies end-to-end")
+
+
+def t2_legacy_bundle_shape_default_kind():
+ """A v0.4-shaped bundle (no tlog_kind, no bundle_schema) must be readable
+ and interpretable as ``oversight-self-merkle-v1``."""
+ legacy_bundle = {
+ "version": "0.4",
+ "file_id": "abcd" * 16,
+ "issuer_pubkey_ed25519": "11" * 32,
+ "tlog": {
+ "size": 7,
+ "root": "00" * 32,
+ "signature": "22" * 64,
+ },
+ "inclusion_proof": {
+ "index": 3,
+ "leaf_hash": "33" * 32,
+ "proof": ["44" * 32, "55" * 32],
+ "tree_size": 7,
+ "root": "00" * 32,
+ },
+ }
+ # Forward-looking interpretation: a v0.5+ verifier sees no tlog_kind →
+ # treats this as the legacy local-merkle path.
+ assert "rekor" not in legacy_bundle, "v0.4 bundle must not have a rekor field"
+ assert "bundle_schema" not in legacy_bundle, "v0.4 bundle must not advertise bundle_schema"
+ inferred_kind = legacy_bundle.get("tlog_kind", R.LEGACY_TLOG_KIND)
+ assert inferred_kind == R.LEGACY_TLOG_KIND, (
+ f"missing tlog_kind must default to {R.LEGACY_TLOG_KIND}, got {inferred_kind!r}"
+ )
+ inferred_schema = legacy_bundle.get("bundle_schema", 1)
+ assert inferred_schema == 1, "missing bundle_schema must default to 1 (v0.4 implicit)"
+ print(" [PASS] 2. v0.4 bundle defaults: tlog_kind=legacy, schema=1")
+
+
+def t3_v05_bundle_advertises_new_fields():
+ """The new bundle the v0.5 path emits MUST advertise both fields explicitly
+ so an old (v0.4) verifier fails fast with 'unknown schema' rather than
+ silently mis-routing."""
+ assert R.BUNDLE_SCHEMA == 2, f"BUNDLE_SCHEMA must be 2, got {R.BUNDLE_SCHEMA}"
+ assert R.TLOG_KIND == "rekor-v2-dsse", f"TLOG_KIND drift: {R.TLOG_KIND!r}"
+ assert R.LEGACY_TLOG_KIND == "oversight-self-merkle-v1"
+ print(" [PASS] 3. v0.5 constants: TLOG_KIND=rekor-v2-dsse, schema=2")
+
+
+def t4_canonical_jcs_unchanged_for_legacy_payload():
+ """The exact JCS encoding for a v0.4-shaped event must not have changed.
+ If this fails, downstream verifiers re-checking historical signatures
+ over canonical JSON will reject events they previously accepted."""
+ event = {
+ "event": "register",
+ "file_id": "f0",
+ "issuer_pub": "11" * 32,
+ "n_beacons": 3,
+ "n_watermarks": 1,
+ "recipient_id": "r0",
+ "timestamp": "2026-04-19T00:00:00Z",
+ }
+ expected = (
+ '{"event":"register","file_id":"f0","issuer_pub":'
+ + '"' + "11" * 32 + '",'
+ + '"n_beacons":3,"n_watermarks":1,"recipient_id":"r0","timestamp":"2026-04-19T00:00:00Z"}'
+ )
+ actual = json.dumps(event, sort_keys=True, separators=(",", ":"))
+ assert actual == expected, f"JCS drift!\n exp: {expected}\n got: {actual}"
+ print(" [PASS] 4. v0.4 event JCS encoding unchanged")
+
+
+def t5_predicate_uri_resolves_at_tagged_path():
+ """Sanity: the PREDICATE_TYPE URI references a git-tagged path. We don't
+ fetch (the e2e test does that); we just confirm the URI shape so a typo
+ like missing the tag won't make it through to a release."""
+ assert R.PREDICATE_TYPE.startswith(
+ "https://github.com/oversight-protocol/oversight/blob/v0.5"
+ ), f"PREDICATE_TYPE not pinned to a v0.5 git tag: {R.PREDICATE_TYPE}"
+ assert R.PREDICATE_TYPE.endswith("/docs/predicates/registration-v1.md")
+ assert R.PREDICATE_VERSION == 1
+ print(" [PASS] 5. PREDICATE_TYPE pinned to v0.5 git-tagged path")
+
+
+def main() -> int:
+ print("=" * 60)
+ print(" test_rekor_backcompat - v0.4 contract preservation (offline)")
+ print("=" * 60)
+ t1_legacy_tlog_still_works()
+ t2_legacy_bundle_shape_default_kind()
+ t3_v05_bundle_advertises_new_fields()
+ t4_canonical_jcs_unchanged_for_legacy_payload()
+ t5_predicate_uri_resolves_at_tagged_path()
+ print()
+ print(" ALL BACKCOMPAT TESTS PASSED - 5/5")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
tests/test_rekor_e2e.py +148 -0
@@ -0,0 +1,148 @@
+"""
+test_rekor_e2e
+==============
+
+Live end-to-end test against a public Sigstore Rekor v2 log.
+
+This test makes real network calls and writes a real (immutable) entry to
+the public log shard. It is therefore gated behind the OVERSIGHT_REKOR_E2E=1
+environment variable so routine test runs do not append to the public log.
+
+Run with:
+ OVERSIGHT_REKOR_E2E=1 python3 tests/test_rekor_e2e.py
+
+What is verified:
+ 1. A DSSE-wrapped Oversight registration predicate uploads successfully.
+ 2. The log returns a JSON response carrying a logIndex (or equivalent
+ under the v2 field naming).
+ 3. The DSSE envelope verifies under the issuer pubkey AFTER the upload -
+ i.e., the round-trip did not mutate signature-bearing bytes.
+ 4. The on-log predicate carries recipient_pubkey_sha256, never the raw
+ X25519 public key (privacy invariant).
+
+Skipped automatically when OVERSIGHT_REKOR_E2E is unset or 0.
+"""
+from __future__ import annotations
+
+import base64
+import json
+import os
+import sys
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
+
+from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
+from cryptography.hazmat.primitives import serialization
+
+from oversight_core import rekor as R
+
+
+GATE = os.environ.get("OVERSIGHT_REKOR_E2E", "0") == "1"
+LOG_URL = os.environ.get("OVERSIGHT_REKOR_URL", R.DEFAULT_REKOR_URL)
+
+
+def _new_keypair() -> tuple[bytes, bytes, str]:
+ sk = Ed25519PrivateKey.generate()
+ priv_raw = sk.private_bytes_raw()
+ pub_raw = sk.public_key().public_bytes_raw()
+ pub_pem = sk.public_key().public_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
+ ).decode("ascii")
+ return priv_raw, pub_raw, pub_pem
+
+
+def t1_live_upload_round_trip():
+ priv_raw, pub_raw, pub_pem = _new_keypair()
+
+ # Recipient X25519 pubkey is hashed before going on-log.
+ fake_x25519 = b"\x42" * 32
+ recipient_hash = R.hash_recipient_pubkey(fake_x25519.hex())
+
+ predicate = R.OversightRegistrationPredicate(
+ file_id="e2e-test-" + base64.b16encode(os.urandom(8)).decode().lower(),
+ issuer_pubkey_ed25519=pub_raw.hex(),
+ recipient_id="opaque-recipient-id-1",
+ recipient_pubkey_sha256=recipient_hash,
+ suite="classic",
+ registered_at="2026-04-19T00:00:00Z",
+ )
+ statement = R.build_statement(
+ mark_id_hex=predicate.file_id,
+ content_hash_sha256_hex="ab" * 32,
+ predicate=predicate,
+ )
+ envelope = R.sign_dsse(statement=statement, issuer_ed25519_priv=priv_raw)
+
+ # Round-trip verify BEFORE upload (sanity).
+ assert R.verify_dsse(envelope, pub_raw), "local DSSE verify failed before upload"
+
+ print(f" uploading to {LOG_URL} ...")
+ result = R.upload_dsse(envelope=envelope, issuer_ed25519_pub_pem=pub_pem, log_url=LOG_URL)
+
+ assert result.transparency_log_entry, "rekor returned empty body"
+ print(f" log_index={result.log_index} log_id={(result.log_id or '')[:24]}...")
+
+ # The envelope must still verify after the round trip.
+ assert R.verify_dsse(envelope, pub_raw), "DSSE verify failed AFTER upload (envelope mutated?)"
+
+ # Privacy invariant: raw X25519 must not appear in the on-log envelope.
+ on_log_payload = base64.b64decode(envelope.payload_b64)
+ assert fake_x25519.hex() not in on_log_payload.decode("utf-8", errors="ignore"), (
+ "raw recipient X25519 pubkey leaked into on-log payload"
+ )
+ print(" [PASS] live round trip + privacy invariant held")
+
+
+def t2_response_carries_inclusion_data():
+ """The bundled response must give a verifier enough to verify offline.
+
+ Per the v0.5 plan: the write response is the only place we get an
+ inclusion proof; there is no online proof-by-index API.
+ """
+ priv_raw, pub_raw, pub_pem = _new_keypair()
+ predicate = R.OversightRegistrationPredicate(
+ file_id="e2e-incl-" + base64.b16encode(os.urandom(8)).decode().lower(),
+ issuer_pubkey_ed25519=pub_raw.hex(),
+ recipient_id="opaque-recipient-id-2",
+ recipient_pubkey_sha256="0" * 64,
+ suite="classic",
+ registered_at="2026-04-19T00:00:00Z",
+ )
+ statement = R.build_statement(
+ mark_id_hex=predicate.file_id,
+ content_hash_sha256_hex="cd" * 32,
+ predicate=predicate,
+ )
+ envelope = R.sign_dsse(statement=statement, issuer_ed25519_priv=priv_raw)
+ result = R.upload_dsse(envelope=envelope, issuer_ed25519_pub_pem=pub_pem, log_url=LOG_URL)
+
+ body = result.transparency_log_entry
+ assert isinstance(body, dict) and body, "rekor body not a non-empty dict"
+ # Either logIndex appears, or inclusionProof / logEntry shape is present.
+ has_idx = result.log_index is not None
+ has_proof = any(k in body for k in ("inclusionProof", "inclusion_proof", "logEntry"))
+ assert has_idx or has_proof, f"response missing index AND proof shape: keys={list(body.keys())}"
+ print(f" [PASS] response carries inclusion data (idx={has_idx}, proof_shape={has_proof})")
+
+
+def main() -> int:
+ if not GATE:
+ print("=" * 60)
+ print(" test_rekor_e2e: SKIPPED")
+ print(" (set OVERSIGHT_REKOR_E2E=1 to run; this writes to the")
+ print(" public Sigstore log)")
+ print("=" * 60)
+ return 0
+ print("=" * 60)
+ print(f" test_rekor_e2e: LIVE against {LOG_URL}")
+ print("=" * 60)
+ t1_live_upload_round_trip()
+ t2_response_carries_inclusion_data()
+ print()
+ print(" ALL E2E TESTS PASSED - 2/2")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())