"""Real CLI, HTTPS and persisted state for RFC 9697 and TA tiebreaker -06.""" from __future__ import annotations import argparse import functools import http.server import json from pathlib import Path import shutil import ssl import sys import tempfile import threading from test_public_config import ( assert_successful_output, free_port, make_rsync_wrapper, read_json, run_checked, write_config, ) def run(binary: str) -> dict: support = Path(__file__).resolve().parent with tempfile.TemporaryDirectory(prefix="panda-rpki-security-") as tmp: base = Path(tmp) fixture = base / "fixture" port = free_port() run_checked([ sys.executable, str(support / "generate_repository.py"), "--rrdp-host", "127.0.0.1", "--rrdp-port", str(port), "--rsync-host", "127.0.0.1", "--strict-names", "--output", str(fixture), ]) active = base / "active" shutil.copytree(fixture / "cases/baseline-v1/http", active) # Prefer real HTTPS for automatic TA retrieval. Retain the rsync URI: # the validation core uses it as the key for its offline TA projection. tal = fixture / "tal/custom.tal" lines = tal.read_text().splitlines() tal.write_text("\n".join( [line for line in lines if line.startswith("https://")] + [line for line in lines if line.startswith("rsync://")] + [line for line in lines if not line.startswith(("https://", "rsync://"))] ) + "\n", encoding="utf-8") served_ta = active / "ta/custom-ta.cer" served_ta.write_bytes((fixture / "ta-variants/newer.cer").read_bytes()) notification = active / "rrdp/notification.xml" original = notification.read_text() def advertise_hash(value: str) -> None: reference = ( f'' ) notification.write_text(original.replace("", reference + "")) advertise_hash("11") requests: list[str] = [] class Handler(http.server.SimpleHTTPRequestHandler): def do_GET(self) -> None: requests.append(self.path) super().do_GET() def log_message(self, format: str, *args) -> None: pass server = http.server.ThreadingHTTPServer( ("127.0.0.1", port), functools.partial(Handler, directory=str(active)) ) context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) context.load_cert_chain(fixture / "certs/rrdp-server.pem", fixture / "certs/rrdp-server.key") server.socket = context.wrap_socket(server.socket, server_side=True) thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() try: output, state = base / "output", base / "state" wrapper, wrapper_log = base / "rsync.py", base / "rsync.log" make_rsync_wrapper(wrapper, fixture, wrapper_log) config = base / "validate.toml" write_config( config, fixture=fixture, output=output, state=state, ccr=output / "result.ccr", wrapper=wrapper, mirror=base / "mirror", sync_preference="rrdp-then-rsync", rrdp_mode="auto", validation_time="2026-06-01T00:00:00Z", resource_mode="rfc6487", scope="publication-point", ) text = "\n".join(line for line in config.read_text().splitlines() if not line.startswith("ta = ")) text = text.replace("[strict]", 'ta-refresh-policy = "require-fresh"\n\n[strict]') config.write_text(text + "\n", encoding="utf-8") proof = {"steps": []} def validate(name: str) -> tuple[dict, str, list[str]]: start = len(requests) result = run_checked([binary, "validate", "--config", str(config)]) assert_successful_output(output, "2026-06-01T00:00:00Z") summary = read_json(output / "summary.json") seen = requests[start:] assert summary["taCacheFallbackCount"] == 0, summary assert not any("delta-" in uri for uri in seen), seen assert not wrapper_log.exists(), "unexpected rsync fallback" proof["steps"].append({ "name": name, "requests": seen, "vrps": summary["vrps"], "resultStatus": summary["resultStatus"], "taCacheFallbackCount": summary["taCacheFallbackCount"], "trustAnchors": summary["trustAnchors"], }) return summary, result.stderr, seen summary, _, seen = validate("initial_snapshot_newer_ta") assert "/rrdp/snapshot.xml" in seen, seen assert summary["trustAnchors"][0]["source"] == "download", summary cache = next(base.joinpath("state.ta-cache").glob("*.json")) cache_before = cache.read_bytes() # A new process sees a mutated historical delta hash at the same # session/serial, and a valid but older TA with the identical key. advertise_hash("33") served_ta.write_bytes((fixture / "ta/custom-ta.cer").read_bytes()) summary, stderr, seen = validate("restarted_hash_mutation_and_older_ta") assert "/rrdp/snapshot.xml" in seen, seen assert "rrdp_session_desynchronized" in stderr, stderr anchor = summary["trustAnchors"][0] assert anchor["source"] == "cache" and not anchor["refreshFailed"], anchor assert anchor["selectionReason"] == "cached_newer_not_before", anchor assert cache.read_bytes() == cache_before, "older TA replaced the preferred cache" _, stderr, seen = validate("restarted_noop_after_recovery") assert "/rrdp/snapshot.xml" not in seen, seen assert "rrdp_session_desynchronized" not in stderr, stderr assert cache.read_bytes() == cache_before return proof finally: server.shutdown() thread.join(timeout=5) server.server_close() if __name__ == "__main__": parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("binary") parser.add_argument("--proof", type=Path) args = parser.parse_args() proof = run(args.binary) if args.proof: args.proof.parent.mkdir(parents=True, exist_ok=True) args.proof.write_text(json.dumps(proof, indent=2) + "\n", encoding="utf-8") print(json.dumps(proof, indent=2))