20260922 对齐 OSS 验证配置并补充 public TOML E2E
This commit is contained in:
@@ -20,6 +20,7 @@ from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from cryptography.hazmat.primitives.serialization import Encoding, NoEncryption, PrivateFormat, PublicFormat
|
||||
from cryptography.x509 import AccessDescription, AuthorityInformationAccess, DistributionPoint, UniformResourceIdentifier
|
||||
from cryptography.x509.name import _ASN1Type
|
||||
from cryptography.x509.oid import AuthorityInformationAccessOID, NameOID, ObjectIdentifier
|
||||
|
||||
|
||||
@@ -55,6 +56,14 @@ SECOND_PREFIX = ipaddress.ip_network("203.0.113.128/25")
|
||||
OUTSIDE_PREFIX = ipaddress.ip_network("203.0.114.0/24")
|
||||
TEST_ASN = 64496
|
||||
SESSION_ID = "11111111-2222-4333-8444-555555555555"
|
||||
STRICT_NAMES = False
|
||||
|
||||
|
||||
def common_name(value: str) -> x509.NameAttribute:
|
||||
"""Build the fixture CN with the DER string type selected by the test."""
|
||||
if STRICT_NAMES:
|
||||
return x509.NameAttribute(NameOID.COMMON_NAME, value, _type=_ASN1Type.PrintableString)
|
||||
return x509.NameAttribute(NameOID.COMMON_NAME, value)
|
||||
|
||||
|
||||
def tlv(tag: int, value: bytes) -> bytes:
|
||||
@@ -317,7 +326,7 @@ def make_roa(
|
||||
) -> None:
|
||||
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||
cert = sign_cert(
|
||||
x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, f"Local Test EE {name}")]),
|
||||
x509.Name([common_name(f"Local Test EE {name}")]),
|
||||
child_name,
|
||||
key.public_key(),
|
||||
child_key,
|
||||
@@ -338,7 +347,7 @@ def make_roa(
|
||||
def make_aspa(name: str, child_name, child_key, common: Path, work: Path) -> None:
|
||||
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||
cert = sign_cert(
|
||||
x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, f"Local Test EE {name}")]),
|
||||
x509.Name([common_name(f"Local Test EE {name}")]),
|
||||
child_name,
|
||||
key.public_key(),
|
||||
child_key,
|
||||
@@ -359,7 +368,7 @@ def make_aspa(name: str, child_name, child_key, common: Path, work: Path) -> Non
|
||||
def make_manifest(name: str, number: int, files: list[tuple[str, bytes]], child_name, child_key, common: Path, work: Path) -> bytes:
|
||||
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||
cert = sign_cert(
|
||||
x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, f"Local Test Manifest EE {name}")]),
|
||||
x509.Name([common_name(f"Local Test Manifest EE {name}")]),
|
||||
child_name,
|
||||
key.public_key(),
|
||||
child_key,
|
||||
@@ -538,7 +547,7 @@ def make_https_certificates(output: Path) -> None:
|
||||
|
||||
def main() -> None:
|
||||
global RRDP_HOST, FOREIGN_RRDP_HOST, RRDP_PORT, RSYNC_HOST, RSYNC_PORT
|
||||
global TAL_URI, TAL_HTTPS_URI, RRDP_URI
|
||||
global TAL_URI, TAL_HTTPS_URI, RRDP_URI, STRICT_NAMES
|
||||
global ROOT_REPO_URI, ROOT_MFT_URI, CHILD_REPO_URI, CHILD_MFT_URI
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
@@ -571,12 +580,18 @@ def main() -> None:
|
||||
default=RSYNC_PORT,
|
||||
help="port embedded in rsync URIs (default: 873)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--strict-names",
|
||||
action="store_true",
|
||||
help="encode RPKI commonName values as PrintableString for strict-name tests",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
RRDP_HOST = args.rrdp_host
|
||||
FOREIGN_RRDP_HOST = args.foreign_rrdp_host
|
||||
RRDP_PORT = args.rrdp_port
|
||||
RSYNC_HOST = args.rsync_host
|
||||
RSYNC_PORT = args.rsync_port
|
||||
STRICT_NAMES = args.strict_names
|
||||
TAL_URI = f"rsync://{RSYNC_HOST}:{RSYNC_PORT}/custom/ta.cer"
|
||||
TAL_HTTPS_URI = f"https://{RRDP_HOST}:{RRDP_PORT}/ta/custom-ta.cer"
|
||||
RRDP_URI = f"https://{RRDP_HOST}:{RRDP_PORT}/rrdp/notification.xml"
|
||||
@@ -592,8 +607,8 @@ def main() -> None:
|
||||
|
||||
root_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||
child_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||
root_name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Local Test RPKI Root")])
|
||||
child_name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Local Test RPKI Child")])
|
||||
root_name = x509.Name([common_name("Local Test RPKI Root")])
|
||||
child_name = x509.Name([common_name("Local Test RPKI Child")])
|
||||
root_cert = sign_cert(
|
||||
root_name, root_name, root_key.public_key(), root_key, 0x14700001, True, root_key,
|
||||
ip_ext=ip_resources([CHILD_PREFIX]),
|
||||
@@ -606,7 +621,7 @@ def main() -> None:
|
||||
ca_repository=CHILD_REPO_URI, manifest_uri=CHILD_MFT_URI, notify_uri=RRDP_URI,
|
||||
crl_uri=f"{ROOT_REPO_URI}root.crl", ca_issuer_uri=f"{ROOT_REPO_URI}child.cer",
|
||||
)
|
||||
bad_child_name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Local Test RPKI Overclaiming Child")])
|
||||
bad_child_name = x509.Name([common_name("Local Test RPKI Overclaiming Child")])
|
||||
bad_child_repo_uri = f"rsync://{RSYNC_HOST}:{RSYNC_PORT}/custom/bad-child/"
|
||||
bad_child_cert = sign_cert(
|
||||
bad_child_name, root_name, child_key.public_key(), root_key, 0x14700003, True, child_key, root_key,
|
||||
@@ -706,6 +721,7 @@ def main() -> None:
|
||||
"fixture_schema_version=1\nfixture_kind=stack-local-test-rpki-repo\n"
|
||||
f"tal_uri={TAL_URI}\ntal_https_uri={TAL_HTTPS_URI}\nrrdp_uri={RRDP_URI}\nrsync_module=custom\n"
|
||||
f"rrdp_host={RRDP_HOST}\nforeign_rrdp_host={FOREIGN_RRDP_HOST}\nrrdp_port={RRDP_PORT}\nrsync_host={RSYNC_HOST}\nrsync_port={RSYNC_PORT}\n"
|
||||
f"strict_names={str(STRICT_NAMES).lower()}\n"
|
||||
"cases=baseline-v1,sync-hash-mismatch,rrdp-cross-origin-snapshot,baseline-v2,validation-expired,validation-max-length,validation-max-length-invalid,"
|
||||
"validation-roa-prefix-outside,validation-out-of-resource,validation-ca-over-resource,validation-nonstandard\n",
|
||||
encoding="utf-8",
|
||||
|
||||
276
tests/support/test_public_config.py
Normal file
276
tests/support/test_public_config.py
Normal file
@@ -0,0 +1,276 @@
|
||||
"""End-to-end coverage for the #170 public TOML validation settings."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
def free_port() -> int:
|
||||
with socket.socket() as sock:
|
||||
sock.bind(("127.0.0.1", 0))
|
||||
return int(sock.getsockname()[1])
|
||||
|
||||
|
||||
def toml_string(path: Path) -> str:
|
||||
return json.dumps(str(path))
|
||||
|
||||
|
||||
def run_checked(args: list[str], *, timeout: int = 120) -> subprocess.CompletedProcess[str]:
|
||||
result = subprocess.run(args, capture_output=True, text=True, timeout=timeout)
|
||||
if result.returncode != 0:
|
||||
raise AssertionError(
|
||||
f"command failed ({result.returncode}): {' '.join(args)}\n"
|
||||
f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}"
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def wait_for_server(server: subprocess.Popen[str]) -> None:
|
||||
assert server.stdout is not None
|
||||
line = server.stdout.readline().strip()
|
||||
if line != "ready":
|
||||
raise AssertionError(f"RRDP fixture server did not become ready: {line!r}")
|
||||
|
||||
|
||||
def stop_process(process: subprocess.Popen[str] | None) -> None:
|
||||
if process is None:
|
||||
return
|
||||
if process.poll() is None:
|
||||
process.terminate()
|
||||
try:
|
||||
process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
process.wait()
|
||||
|
||||
|
||||
def write_config(
|
||||
path: Path,
|
||||
*,
|
||||
fixture: Path,
|
||||
output: Path,
|
||||
state: Path,
|
||||
ccr: Path,
|
||||
wrapper: Path,
|
||||
mirror: Path,
|
||||
sync_preference: str,
|
||||
rrdp_mode: str,
|
||||
validation_time: str,
|
||||
resource_mode: str,
|
||||
scope: str,
|
||||
) -> None:
|
||||
path.write_text(
|
||||
f'''config-version = 1
|
||||
mode = "validate"
|
||||
out = {toml_string(output)}
|
||||
rrdp-state-dir = {toml_string(state)}
|
||||
rrdp-sync-mode = "{rrdp_mode}"
|
||||
ccr-out = {toml_string(ccr)}
|
||||
http-timeout-secs = 2
|
||||
http-root-cert = [{toml_string(fixture / "certs/rrdp-ca.pem")}]
|
||||
require-complete = true
|
||||
parallel-max-repo-sync-workers-global = 2
|
||||
parallel-phase2-object-workers = 2
|
||||
log-level = "info"
|
||||
log-format = "json"
|
||||
resource-validation-mode = "{resource_mode}"
|
||||
validation-time = "{validation_time}"
|
||||
sync-preference = "{sync_preference}"
|
||||
rsync-timeout-secs = 47
|
||||
rsync-command = {toml_string(wrapper)}
|
||||
rsync-mirror-root = {toml_string(mirror)}
|
||||
rsync-scope = "{scope}"
|
||||
|
||||
[strict]
|
||||
name = true
|
||||
cms-der = true
|
||||
signed-attrs = true
|
||||
|
||||
[[anchors]]
|
||||
id = "custom"
|
||||
tal = {toml_string(fixture / "tal/custom.tal")}
|
||||
ta = {toml_string(fixture / "ta/custom-ta.cer")}
|
||||
''',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def make_rsync_wrapper(path: Path, fixture: Path, log: Path) -> None:
|
||||
repository = fixture / "cases" / "baseline-v1" / "repository"
|
||||
path.write_text(
|
||||
f'''#!/usr/bin/env python3
|
||||
import json
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
repository = Path({str(repository)!r})
|
||||
log = Path({str(log)!r})
|
||||
args = sys.argv[1:]
|
||||
with log.open("a", encoding="utf-8") as stream:
|
||||
stream.write(json.dumps(args) + "\\n")
|
||||
|
||||
if len(args) < 2:
|
||||
raise SystemExit("rsync wrapper expected source and destination")
|
||||
source_uri, destination_text = args[-2:]
|
||||
destination = Path(destination_text)
|
||||
parts = [part for part in urlparse(source_uri).path.split("/") if part]
|
||||
if not parts or parts[0] != "custom":
|
||||
raise SystemExit(f"unexpected fixture rsync source: {{source_uri}}")
|
||||
relative = Path(*parts[1:]) if len(parts) > 1 else Path()
|
||||
source = repository / relative
|
||||
if not source.exists():
|
||||
raise SystemExit(f"fixture rsync source does not exist: {{source}}")
|
||||
destination.mkdir(parents=True, exist_ok=True)
|
||||
if source.is_file():
|
||||
shutil.copy2(source, destination / source.name)
|
||||
else:
|
||||
for item in source.iterdir():
|
||||
target = destination / item.name
|
||||
if item.is_dir():
|
||||
shutil.copytree(item, target, dirs_exist_ok=True)
|
||||
else:
|
||||
shutil.copy2(item, target)
|
||||
''',
|
||||
encoding="utf-8",
|
||||
)
|
||||
path.chmod(0o755)
|
||||
|
||||
|
||||
def read_json(path: Path) -> dict:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def assert_successful_output(output: Path, expected_validation_time: str) -> None:
|
||||
summary = read_json(output / "summary.json")
|
||||
assert summary["resultStatus"] == "success", summary
|
||||
assert summary["vrps"] == 1, summary
|
||||
assert (output / "result-complete.json").is_file()
|
||||
assert len((output / "vrps.csv").read_text(encoding="utf-8").splitlines()) == 2
|
||||
|
||||
timing = read_json(output / "analysis" / "timing.json")
|
||||
assert timing["meta"]["validation_time_utc_rfc3339"] == expected_validation_time, timing
|
||||
|
||||
|
||||
def run(binary: str) -> None:
|
||||
support = Path(__file__).resolve().parent
|
||||
with tempfile.TemporaryDirectory(prefix="panda-rpki-public-config-") 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 = fixture / "active"
|
||||
active.symlink_to("cases/baseline-v1/http")
|
||||
|
||||
wrapper_log = base / "rsync-wrapper.log"
|
||||
wrapper = base / "rsync-wrapper.py"
|
||||
make_rsync_wrapper(wrapper, fixture, wrapper_log)
|
||||
|
||||
server = subprocess.Popen(
|
||||
[
|
||||
sys.executable,
|
||||
str(support / "serve_repository.py"),
|
||||
str(active),
|
||||
str(port),
|
||||
str(fixture / "certs/rrdp-server.pem"),
|
||||
str(fixture / "certs/rrdp-server.key"),
|
||||
],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
try:
|
||||
wait_for_server(server)
|
||||
fixed_input = "2026-06-01T08:00:00.250+08:00"
|
||||
fixed_effective = "2026-06-01T00:00:00Z"
|
||||
rrdp_output = base / "rrdp-output"
|
||||
rrdp_state = base / "rrdp-state"
|
||||
rrdp_config = base / "rrdp.toml"
|
||||
write_config(
|
||||
rrdp_config,
|
||||
fixture=fixture,
|
||||
output=rrdp_output,
|
||||
state=rrdp_state,
|
||||
ccr=rrdp_output / "result.ccr",
|
||||
wrapper=wrapper,
|
||||
mirror=base / "rrdp-mirror",
|
||||
sync_preference="rrdp-then-rsync",
|
||||
rrdp_mode="snapshot",
|
||||
validation_time=fixed_input,
|
||||
resource_mode="validation-update-03",
|
||||
scope="publication-point",
|
||||
)
|
||||
run_checked([binary, "validate", "--config", str(rrdp_config)])
|
||||
assert_successful_output(rrdp_output, fixed_effective)
|
||||
assert not wrapper_log.exists() or not wrapper_log.read_text(encoding="utf-8").strip(), (
|
||||
"RRDP success path unexpectedly invoked the rsync command"
|
||||
)
|
||||
finally:
|
||||
stop_process(server)
|
||||
|
||||
rsync_output = base / "rsync-output"
|
||||
rsync_state = base / "rsync-state"
|
||||
rsync_mirror = base / "rsync-mirror"
|
||||
rsync_config = base / "rsync.toml"
|
||||
write_config(
|
||||
rsync_config,
|
||||
fixture=fixture,
|
||||
output=rsync_output,
|
||||
state=rsync_state,
|
||||
ccr=rsync_output / "result.ccr",
|
||||
wrapper=wrapper,
|
||||
mirror=rsync_mirror,
|
||||
sync_preference="rsync-only",
|
||||
rrdp_mode="auto",
|
||||
validation_time="2026-06-01T00:00:00Z",
|
||||
resource_mode="rfc6487",
|
||||
scope="publication-point",
|
||||
)
|
||||
# The RRDP server is stopped. A successful run therefore proves that
|
||||
# sync-preference came from public TOML and reached the scheduler.
|
||||
run_checked([binary, "validate", "--config", str(rsync_config)])
|
||||
assert_successful_output(rsync_output, "2026-06-01T00:00:00Z")
|
||||
|
||||
calls = [
|
||||
json.loads(line)
|
||||
for line in wrapper_log.read_text(encoding="utf-8").splitlines()
|
||||
if line.strip()
|
||||
]
|
||||
assert calls, "rsync-only validation did not invoke configured rsync command"
|
||||
assert all("--timeout" in call and call[call.index("--timeout") + 1] == "47" for call in calls), calls
|
||||
sources = [call[-2] for call in calls]
|
||||
assert any("/custom/root/" in source for source in sources), sources
|
||||
assert any("/custom/child/" in source for source in sources), sources
|
||||
assert all("/custom/" in source and source.rstrip("/") != "rsync://127.0.0.1:873/custom" for source in sources), sources
|
||||
mirror_files = [path for path in rsync_mirror.rglob("*") if path.is_file()]
|
||||
assert mirror_files, f"configured rsync mirror remained empty: {rsync_mirror}"
|
||||
|
||||
print(
|
||||
"Public TOML E2E passed: strict/resource/time + RRDP path + "
|
||||
"rsync-only command/timeout/mirror/scope"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run(str(Path(sys.argv[1]).resolve()))
|
||||
Reference in New Issue
Block a user