20261015 实现RRDP失步检测与TA证书择优
This commit is contained in:
151
tests/support/test_security_recovery.py
Normal file
151
tests/support/test_security_recovery.py
Normal file
@@ -0,0 +1,151 @@
|
||||
"""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'<delta serial="1" uri="https://127.0.0.1:{port}/rrdp/delta-1.xml" '
|
||||
f'hash="{value * 32}"/>'
|
||||
)
|
||||
notification.write_text(original.replace("</notification>", reference + "</notification>"))
|
||||
|
||||
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))
|
||||
Reference in New Issue
Block a user