"""Signed, multi-level repository mutations and real CLI restarts over both transports.""" import argparse, functools, hashlib, http.server, json, shutil, ssl, subprocess, sys, tempfile, threading from datetime import timedelta from pathlib import Path from cryptography import x509 from cryptography.hazmat.primitives import serialization, hashes from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.serialization import Encoding import generate_repository as g from test_public_config import free_port, make_rsync_wrapper, read_json, run_checked, write_config def canonical_ccr(blob): # producedAt describes artifact generation, not the validated state. Compare # every other DER byte while normalizing only that top-level CCR timestamp. def tlv(offset): tag = blob[offset]; length = blob[offset+1]; start = offset+2 if length & 128: count = length & 127; length = int.from_bytes(blob[start:start+count], 'big'); start += count return tag, start, start+length tag, outer, end = tlv(0); assert tag == 0x30 and end == len(blob) _, _, oid_end = tlv(outer) tag, explicit, _ = tlv(oid_end); assert tag == 0xa0 tag, start, end = tlv(explicit); assert tag == 0x30 while start < end: tag, value, next_field = tlv(start) if tag == 0x18: return blob[:value] + b'0'*(next_field-value) + blob[next_field:] start = next_field raise AssertionError('CCR producedAt missing') def run(binary, evidence=None): proof = {'transports': [], 'binary_sha256': hashlib.sha256(Path(binary).read_bytes()).hexdigest(), 'ccr_comparison': 'all DER bytes except producedAt; CSV byte identical'} with tempfile.TemporaryDirectory(prefix='panda-failed-fetch-extended-') as temp: base = Path(temp); fixture = base/'fixture'; port = free_port() run_checked([sys.executable, str(Path(__file__).with_name('generate_repository.py')), '--output', str(fixture), '--rrdp-host', '127.0.0.1', '--rrdp-port', str(port), '--rsync-host', '127.0.0.1', '--strict-names']) g.STRICT_NAMES = True root_uri = 'rsync://127.0.0.1:873/custom/root/' child_uri = 'rsync://127.0.0.1:873/custom/child/' grand_uri = 'rsync://127.0.0.1:873/custom/grand/' sibling_uri = 'rsync://127.0.0.1:873/custom/sibling/' notify = f'https://127.0.0.1:{port}/rrdp/notification.xml' active = base/'active'; active.mkdir(); (active/'rrdp').mkdir() repository = base/'repository' requests = [] class Handler(http.server.SimpleHTTPRequestHandler): def do_GET(self): requests.append(self.path); super().do_GET() def log_message(self, *_): pass server = http.server.ThreadingHTTPServer(('127.0.0.1', port), functools.partial(Handler, directory=str(active))) tls = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) tls.load_cert_chain(fixture/'certs/rrdp-server.pem', fixture/'certs/rrdp-server.key') server.socket = tls.wrap_socket(server.socket, server_side=True) thread = threading.Thread(target=server.serve_forever, daemon=True); thread.start() original = fixture/'cases/baseline-v1/repository' root = x509.load_der_x509_certificate((original/'ta.cer').read_bytes()) child = x509.load_der_x509_certificate((original/'root/child.cer').read_bytes()) root_key = serialization.load_pem_private_key((fixture/'keys/root.key').read_bytes(), None) child_key = serialization.load_pem_private_key((fixture/'keys/child.key').read_bytes(), None) work = base/'signing'; work.mkdir() def manifest(folder, number, names, issuer, key, ca_uri): uri = {'root': root_uri, 'child': child_uri, 'grand': grand_uri, 'sibling': sibling_uri}[folder] ee_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) ee = g.sign_cert(x509.Name([g.common_name('Failed Fetch Manifest')]), issuer, ee_key.public_key(), key, x509.random_serial_number(), False, ee_key, key, ip_ext=g.ip_inherit(), as_ext=g.as_resources(None), signed_object_uri=uri+folder+'.mft', crl_uri=uri+folder+'.crl', ca_issuer_uri=ca_uri) content = g.manifest_content(number, [(n, (repository/folder/n).read_bytes()) for n in names]) g.write_cms(content, g.OID_MANIFEST_ECONTENT, ee, ee_key, repository/folder/(folder+'.mft'), work) def publish(index): session = f'{index:08d}-1111-4111-8111-222222222222' objects = sorted(p for p in repository.rglob('*') if p.is_file() and p.suffix in {'.mft', '.crl', '.cer', '.roa', '.asa'}) snapshot = (f'' + ''.join(g.publish('rsync://127.0.0.1:873/custom/'+str(p.relative_to(repository)), p) for p in objects) + '').encode() (active/'rrdp/snapshot.xml').write_bytes(snapshot) (active/'rrdp/notification.xml').write_text(f'') def reset(case): if repository.exists(): shutil.rmtree(repository) shutil.copytree(fixture/'cases'/case/'repository', repository) try: for transport in ('rsync-only', 'rrdp-then-rsync'): target = base/transport; target.mkdir(); log = target/'rsync.log'; wrapper = target/'rsync.py' make_rsync_wrapper(wrapper, fixture, log) wrapper.write_text(wrapper.read_text().replace(str(original), str(repository)).replace( 'destination.mkdir(parents=True, exist_ok=True)', 'if destination.exists(): shutil.rmtree(destination)\ndestination.mkdir(parents=True, exist_ok=True)')) wrapper.chmod(0o755) reset('baseline-v1'); runs = []; initial_vrps = initial_aspas = initial_ccr = None def execute(name, expected, partial=False, *, stamp='2026-06-01T00:00:00Z', config_change=None, no_child_fetch=False): nonlocal initial_vrps, initial_aspas, initial_ccr publish(len(runs)+1) out = target/name; config = target/'validate.toml' write_config(config, fixture=fixture, output=out, state=target/'state', ccr=out/'result.ccr', wrapper=wrapper, mirror=target/'mirror', sync_preference=transport, rrdp_mode='auto', validation_time=stamp, resource_mode='rfc6487', scope='publication-point') if config_change: config.write_text(config_change(config.read_text())) start = len(requests); rsync_start = len(log.read_text().splitlines()) if log.exists() else 0 result = subprocess.run([binary, 'validate', '--config', str(config)], capture_output=True, text=True, timeout=120) summary = read_json(out/'summary.json') assert summary['vrps'] == expected, (transport, name, summary, result.stderr[-5000:]) assert summary['resultStatus'] == ('partial' if partial else 'success'), (name, summary, result.stderr[-5000:]) assert result.returncode == (3 if partial else 0), (name, result.returncode, result.stderr[-5000:]) events = [] for line in result.stderr.splitlines(): try: event = json.loads(line) except ValueError: continue if event.get('event') == 'failed_fetch_cache_reused': events.append(event['fields']) cached = summary['failed_fetch_cached'] # Structured cache events also expose the unchanged validity bounds. assert cached == len(events), (name, cached, events) rsync = log.read_text().splitlines()[rsync_start:] if log.exists() else [] if no_child_fetch: if transport == 'rsync-only': assert len(rsync) == 1, (name, rsync) assert sum(e['repo_sync_phase'] == 'cache_only' for e in events) == max(0, cached-1), events if name == 'initial': initial_vrps = (out/'vrps.csv').read_bytes(); initial_aspas = (out/'vaps.csv').read_bytes(); initial_ccr = canonical_ccr((out/'result.ccr').read_bytes()) if name in {'corrupt-object', 'repeat-failure', 'missing-object', 'bad-crl', 'corrupt-parent'}: assert (out/'vrps.csv').read_bytes() == initial_vrps assert (out/'vaps.csv').read_bytes() == initial_aspas assert canonical_ccr((out/'result.ccr').read_bytes()) == initial_ccr if transport == 'rrdp-then-rsync': assert not rsync, ('unexpected rsync fallback', name, rsync) runs.append({'name': name, 'vrps': expected, 'status': summary['resultStatus'], 'cached': cached, 'publication_points': summary['publication_points'], 'http_requests': requests[start:], 'rsync_requests': len(rsync), 'verified_at': [p['verified_at'] for p in sorted(events, key=lambda e:e['manifest_rsync_uri'])]}) execute('initial', 1) good_roa = (repository/'child/valid.roa').read_bytes() (repository/'child/valid.roa').write_bytes(b'corrupt downloaded object') execute('corrupt-object', 1, True) execute('repeat-failure', 1, True, config_change=lambda s:s.replace('parallel-phase2-object-workers = 2', 'parallel-phase2-object-workers = 1')) assert runs[-1]['verified_at'] == runs[-2]['verified_at'], 'failure refreshed validation time' execute('policy-changed', 0, True, config_change=lambda s:s.replace('name = true', 'name = false')) execute('clock-rollback', 0, True, stamp='2026-05-31T00:00:00Z') execute('expired', 0, True, stamp='2027-12-01T01:00:00Z') (repository/'child/valid.roa').unlink(); execute('missing-object', 1, True) (repository/'child/valid.roa').write_bytes(good_roa) good_crl = (repository/'child/child.crl').read_bytes(); (repository/'child/child.crl').write_bytes(b'bad crl') execute('bad-crl', 1, True); (repository/'child/child.crl').write_bytes(good_crl) (repository/'root/root.mft').write_bytes(b'bad parent'); execute('corrupt-parent', 1, True, no_child_fetch=True) reset('baseline-v2'); execute('recovered', 2) manifest('child', 3, ['child.crl'], child.subject, child_key, child_uri+'child.cer') execute('fresh-empty-child', 0) (repository/'child/child.mft').write_bytes(b'bad empty child'); execute('cached-empty-child', 0, True) manifest('root', 4, ['root.crl'], root.subject, root_key, root_uri+'ta.cer') execute('fresh-deleted-child', 0) (repository/'root/root.mft').write_bytes(b'bad deleted parent'); execute('cached-deleted-child', 0, True, no_child_fetch=True) # Restore a valid child, then revoke its certificate in a signed root CRL. shutil.copy2(fixture/'cases/baseline-v2/repository/child/child.mft', repository/'child/child.mft') manifest('child', 4, ['child.crl', 'valid.roa', 'valid2.roa', 'valid.asa'], child.subject, child_key, child_uri+'child.cer') manifest('root', 5, ['root.crl', 'child.cer'], root.subject, root_key, root_uri+'ta.cer') execute('restored-child', 2) revoked = x509.RevokedCertificateBuilder().serial_number(child.serial_number).revocation_date(g.VALID_FROM+timedelta(days=32)).build() crl = (x509.CertificateRevocationListBuilder().issuer_name(root.subject).last_update(g.VALID_FROM+timedelta(days=33)) .next_update(g.VALID_TO-timedelta(days=30)).add_extension(x509.AuthorityKeyIdentifier.from_issuer_public_key(root_key.public_key()), False) .add_extension(x509.CRLNumber(2), False).add_revoked_certificate(revoked).sign(root_key, hashes.SHA256())) (repository/'root/root.crl').write_bytes(crl.public_bytes(Encoding.DER)) manifest('root', 6, ['root.crl', 'child.cer'], root.subject, root_key, root_uri+'ta.cer'); execute('fresh-revoked-child', 0) (repository/'root/root.mft').write_bytes(b'bad revoked parent'); execute('cached-revoked-child', 0, True, no_child_fetch=True) # A genuine root -> child -> grandchild chain, with payload only at depth 2. (repository/'root/root.crl').write_bytes((original/'root/root.crl').read_bytes()) grand_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) grand_name = x509.Name([g.common_name('Grandchild CA')]) grand = g.sign_cert(grand_name, child.subject, grand_key.public_key(), child_key, 909, True, grand_key, child_key, ip_ext=g.ip_resources([g.CHILD_PREFIX]), as_ext=g.as_resources([g.TEST_ASN]), ca_repository=grand_uri, manifest_uri=grand_uri+'grand.mft', notify_uri=notify, crl_uri=child_uri+'child.crl', ca_issuer_uri=root_uri+'child.cer') (repository/'child/grand.cer').write_bytes(grand.public_bytes(Encoding.DER)); (repository/'grand').mkdir() (repository/'grand/grand.crl').write_bytes(g.make_crl(grand_name, grand_key, 1).public_bytes(Encoding.DER)) roa_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) roa_ee = g.sign_cert(x509.Name([g.common_name('Grandchild ROA')]), grand_name, roa_key.public_key(), grand_key, x509.random_serial_number(), False, roa_key, grand_key, ip_ext=g.ip_resources([g.CHILD_PREFIX]), signed_object_uri=grand_uri+'valid.roa', crl_uri=grand_uri+'grand.crl', ca_issuer_uri=child_uri+'grand.cer', not_before=g.VALID_FROM+timedelta(days=31), not_after=g.VALID_TO-timedelta(days=31)) g.write_cms(g.roa_content(g.CHILD_PREFIX, 24), g.OID_ROA_ECONTENT, roa_ee, roa_key, repository/'grand/valid.roa', work) manifest('grand', 2, ['grand.crl', 'valid.roa'], grand_name, grand_key, child_uri+'grand.cer') manifest('child', 5, ['child.crl', 'grand.cer'], child.subject, child_key, child_uri+'child.cer') manifest('root', 7, ['root.crl', 'child.cer'], root.subject, root_key, root_uri+'ta.cer') execute('three-level-fresh', 1) (repository/'root/root.mft').write_bytes(b'bad three-level parent') execute('three-level-cache', 1, True, no_child_fetch=True) execute('three-level-cache-ccr-off', 1, True, config_change=lambda s:'\n'.join(l for l in s.splitlines() if not l.startswith('ccr-out ='))+'\n', no_child_fetch=True) execute('three-level-depth-limit', 0, True, config_change=lambda s:s.replace('require-complete = true', 'require-complete = true\nmax-ca-depth = 1'), no_child_fetch=True) # A later-expiring sibling stays usable when the first leaf expires. sibling_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) sibling_name = x509.Name([g.common_name('Sibling CA')]) sibling = g.sign_cert(sibling_name, child.subject, sibling_key.public_key(), child_key, 910, True, sibling_key, child_key, ip_ext=g.ip_resources([g.SECOND_PREFIX]), as_ext=g.as_resources([g.TEST_ASN]), ca_repository=sibling_uri, manifest_uri=sibling_uri+'sibling.mft', notify_uri=notify, crl_uri=child_uri+'child.crl', ca_issuer_uri=root_uri+'child.cer') (repository/'child/sibling.cer').write_bytes(sibling.public_bytes(Encoding.DER)); (repository/'sibling').mkdir() (repository/'sibling/sibling.crl').write_bytes(g.make_crl(sibling_name, sibling_key, 1).public_bytes(Encoding.DER)) ee_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) ee = g.sign_cert(x509.Name([g.common_name('Sibling ROA')]), sibling_name, ee_key.public_key(), sibling_key, x509.random_serial_number(), False, ee_key, sibling_key, ip_ext=g.ip_resources([g.SECOND_PREFIX]), signed_object_uri=sibling_uri+'valid.roa', crl_uri=sibling_uri+'sibling.crl', ca_issuer_uri=child_uri+'sibling.cer', not_before=g.VALID_FROM+timedelta(days=31), not_after=g.VALID_TO-timedelta(days=10)) g.write_cms(g.roa_content(g.SECOND_PREFIX, 25), g.OID_ROA_ECONTENT, ee, ee_key, repository/'sibling/valid.roa', work) manifest('sibling', 1, ['sibling.crl', 'valid.roa'], sibling_name, sibling_key, child_uri+'sibling.cer') manifest('child', 6, ['child.crl', 'grand.cer', 'sibling.cer'], child.subject, child_key, child_uri+'child.cer') manifest('root', 8, ['root.crl', 'child.cer'], root.subject, root_key, root_uri+'ta.cer') execute('siblings-fresh', 2) (repository/'root/root.mft').write_bytes(b'bad sibling parent') execute('siblings-cache', 2, True, no_child_fetch=True) execute('one-cached-leaf-expired', 1, True, stamp='2027-12-01T00:00:00Z', no_child_fetch=True) proof['transports'].append({'transport': transport, 'runs': runs}) finally: server.shutdown(); server.server_close(); thread.join(timeout=5) if evidence: evidence.write_text(json.dumps(proof, indent=2)+'\n') return proof if __name__ == '__main__': parser = argparse.ArgumentParser(); parser.add_argument('binary'); parser.add_argument('--evidence', type=Path); args = parser.parse_args() print(json.dumps(run(args.binary, args.evidence)))