20261016 修复 Manifest failed-fetch 缓存与子树恢复
This commit is contained in:
22
tests/failed_fetch_e2e.rs
Normal file
22
tests/failed_fetch_e2e.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
#[test]
|
||||
fn valid_failed_fetch_results_and_subtree_survive_cli_restart() {
|
||||
let status = std::process::Command::new("python3")
|
||||
.arg("tests/support/test_failed_fetch.py")
|
||||
.arg(env!("CARGO_BIN_EXE_panda-rpki"))
|
||||
.status()
|
||||
.expect("start failed-fetch CLI driver");
|
||||
assert!(status.success(), "failed-fetch E2E failed: {status}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_failed_fetch_mutations_over_both_transports() {
|
||||
let status = std::process::Command::new("python3")
|
||||
.arg("tests/support/test_failed_fetch_extended.py")
|
||||
.arg(env!("CARGO_BIN_EXE_panda-rpki"))
|
||||
.status()
|
||||
.expect("start extended failed-fetch driver");
|
||||
assert!(
|
||||
status.success(),
|
||||
"extended failed-fetch E2E failed: {status}"
|
||||
);
|
||||
}
|
||||
72
tests/support/test_failed_fetch.py
Normal file
72
tests/support/test_failed_fetch.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""Real CLI restarts: failed-fetch must retain valid per-instance results."""
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from test_public_config import make_rsync_wrapper, read_json, run_checked, write_config
|
||||
|
||||
|
||||
def run(binary: str, evidence: Path | None = None, expect_no_cache: bool = False) -> dict:
|
||||
proof = {'runs': []}
|
||||
with tempfile.TemporaryDirectory(prefix='panda-failed-fetch-') as temp:
|
||||
base = Path(temp)
|
||||
fixture = base / 'fixture'
|
||||
run_checked([sys.executable, str(Path(__file__).with_name('generate_repository.py')),
|
||||
'--output', str(fixture), '--rsync-host', '127.0.0.1', '--strict-names'])
|
||||
repository = base / 'repository'
|
||||
shutil.copytree(fixture / 'cases/baseline-v1/repository', repository)
|
||||
wrapper = base / 'rsync.py'
|
||||
log = base / 'rsync.log'
|
||||
make_rsync_wrapper(wrapper, fixture, log)
|
||||
wrapper.write_text(wrapper.read_text().replace(str(fixture / 'cases/baseline-v1/repository'), str(repository)))
|
||||
wrapper.chmod(0o755)
|
||||
baseline = None
|
||||
for name in ('initial', 'missing-object', 'missing-parent', 'recovered'):
|
||||
if name == 'missing-object':
|
||||
(repository / 'child/valid.roa').write_bytes(b'corrupted downloaded ROA')
|
||||
if name == 'missing-parent':
|
||||
(repository / 'root/root.mft').write_bytes(b'corrupted downloaded manifest')
|
||||
if name == 'recovered':
|
||||
shutil.rmtree(repository)
|
||||
shutil.copytree(fixture / 'cases/baseline-v2/repository', repository)
|
||||
out = base / name
|
||||
config = base / 'validate.toml'
|
||||
write_config(config, fixture=fixture, output=out, state=base / 'state', ccr=out / 'result.ccr',
|
||||
wrapper=wrapper, mirror=base / 'mirror', sync_preference='rsync-only', rrdp_mode='auto',
|
||||
validation_time='2026-06-01T00:00:00Z', resource_mode='rfc6487', scope='publication-point')
|
||||
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')
|
||||
expected = 2 if name == 'recovered' else (0 if expect_no_cache and name.startswith('missing') else 1)
|
||||
assert summary['vrps'] == expected, (name, summary, result.stderr[-4000:])
|
||||
if name.startswith('missing'):
|
||||
assert summary['resultStatus'] == 'partial', summary
|
||||
assert result.returncode == 3, result
|
||||
else:
|
||||
assert summary['resultStatus'] == 'success' and result.returncode == 0, (summary, result.stderr[-3000:])
|
||||
csv = (out / 'vrps.csv').read_bytes()
|
||||
if name == 'initial':
|
||||
baseline = csv
|
||||
elif name.startswith('missing') and not expect_no_cache:
|
||||
assert baseline == csv, 'failed-fetch changed still-valid payloads'
|
||||
requests = log.read_text().splitlines()[start:]
|
||||
if name == 'missing-parent':
|
||||
assert len(requests) == 1, ('cached subtree must not fetch children', requests)
|
||||
proof['runs'].append({'name': name, 'status': summary['resultStatus'], 'vrps': summary['vrps'],
|
||||
'exit': result.returncode, 'rsync_requests': requests})
|
||||
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)
|
||||
parser.add_argument('--expect-no-cache', action='store_true')
|
||||
args = parser.parse_args()
|
||||
print(json.dumps(run(args.binary, args.evidence, args.expect_no_cache)))
|
||||
214
tests/support/test_failed_fetch_extended.py
Normal file
214
tests/support/test_failed_fetch_extended.py
Normal file
@@ -0,0 +1,214 @@
|
||||
"""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'<snapshot xmlns="http://www.ripe.net/rpki/rrdp" version="1" session_id="{session}" serial="1">'
|
||||
+ ''.join(g.publish('rsync://127.0.0.1:873/custom/'+str(p.relative_to(repository)), p) for p in objects)
|
||||
+ '</snapshot>').encode()
|
||||
(active/'rrdp/snapshot.xml').write_bytes(snapshot)
|
||||
(active/'rrdp/notification.xml').write_text(f'<notification xmlns="http://www.ripe.net/rpki/rrdp" version="1" session_id="{session}" serial="1"><snapshot uri="https://127.0.0.1:{port}/rrdp/snapshot.xml" hash="{hashlib.sha256(snapshot).hexdigest()}"/></notification>')
|
||||
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)))
|
||||
Reference in New Issue
Block a user