20260324_2 增加ccr & router key support
This commit is contained in:
337
tests/test_ccr_m1.rs
Normal file
337
tests/test_ccr_m1.rs
Normal file
@@ -0,0 +1,337 @@
|
||||
use rpki::ccr::{
|
||||
AspaPayloadSet, AspaPayloadState, CcrContentInfo, CcrDigestAlgorithm, ManifestInstance,
|
||||
ManifestState, RoaPayloadSet, RoaPayloadState, RouterKey, RouterKeySet, RouterKeyState,
|
||||
RpkiCanonicalCacheRepresentation, TrustAnchorState, compute_state_hash,
|
||||
decode_content_info, encode::{
|
||||
encode_manifest_state_payload_der, encode_router_key_state_payload_der,
|
||||
encode_trust_anchor_state_payload_der,
|
||||
},
|
||||
encode_content_info, verify_state_hash,
|
||||
};
|
||||
use rpki::data_model::common::BigUnsigned;
|
||||
use rpki::data_model::oid::{OID_CT_RPKI_CCR, OID_CT_RPKI_CCR_RAW};
|
||||
|
||||
fn sample_time() -> time::OffsetDateTime {
|
||||
time::OffsetDateTime::parse(
|
||||
"2026-03-24T00:00:00Z",
|
||||
&time::format_description::well_known::Rfc3339,
|
||||
)
|
||||
.expect("valid rfc3339")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minimal_trust_anchor_ccr_roundtrips() {
|
||||
let skis = vec![vec![0x11; 20], vec![0x22; 20]];
|
||||
let skis_der = encode_trust_anchor_state_payload_der(&skis).expect("encode trust anchor payload");
|
||||
let state = TrustAnchorState {
|
||||
skis,
|
||||
hash: compute_state_hash(&skis_der),
|
||||
};
|
||||
let ccr = RpkiCanonicalCacheRepresentation {
|
||||
version: 0,
|
||||
hash_alg: CcrDigestAlgorithm::Sha256,
|
||||
produced_at: sample_time(),
|
||||
mfts: None,
|
||||
vrps: None,
|
||||
vaps: None,
|
||||
tas: Some(state),
|
||||
rks: None,
|
||||
};
|
||||
let content_info = CcrContentInfo::new(ccr.clone());
|
||||
let der = encode_content_info(&content_info).expect("encode ccr");
|
||||
let decoded = decode_content_info(&der).expect("decode ccr");
|
||||
assert_eq!(decoded, content_info);
|
||||
assert_eq!(decoded.content_type_oid, OID_CT_RPKI_CCR);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_rejects_wrong_content_type_oid() {
|
||||
let skis = vec![vec![0x11; 20]];
|
||||
let skis_der = encode_trust_anchor_state_payload_der(&skis).expect("encode trust anchor payload");
|
||||
let content_info = CcrContentInfo::new(RpkiCanonicalCacheRepresentation {
|
||||
version: 0,
|
||||
hash_alg: CcrDigestAlgorithm::Sha256,
|
||||
produced_at: sample_time(),
|
||||
mfts: None,
|
||||
vrps: None,
|
||||
vaps: None,
|
||||
tas: Some(TrustAnchorState {
|
||||
skis,
|
||||
hash: compute_state_hash(&skis_der),
|
||||
}),
|
||||
rks: None,
|
||||
});
|
||||
let mut der = encode_content_info(&content_info).expect("encode ccr");
|
||||
let needle = OID_CT_RPKI_CCR_RAW;
|
||||
let pos = der
|
||||
.windows(needle.len())
|
||||
.position(|w| w == needle)
|
||||
.expect("oid present");
|
||||
der[pos + needle.len() - 1] ^= 0x01;
|
||||
let err = decode_content_info(&der).expect_err("wrong content type must fail");
|
||||
assert!(err.to_string().contains("unexpected contentType OID"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ccr_requires_at_least_one_state_aspect() {
|
||||
let ccr = CcrContentInfo::new(RpkiCanonicalCacheRepresentation {
|
||||
version: 0,
|
||||
hash_alg: CcrDigestAlgorithm::Sha256,
|
||||
produced_at: sample_time(),
|
||||
mfts: None,
|
||||
vrps: None,
|
||||
vaps: None,
|
||||
tas: None,
|
||||
rks: None,
|
||||
});
|
||||
let err = encode_content_info(&ccr).expect_err("empty state aspects must fail");
|
||||
assert!(err.to_string().contains("at least one of mfts/vrps/vaps/tas/rks"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_hash_helpers_accept_matching_and_reject_tampered_payload() {
|
||||
let skis = vec![vec![0x11; 20]];
|
||||
let payload_der = encode_trust_anchor_state_payload_der(&skis).expect("encode trust anchor payload");
|
||||
let hash = compute_state_hash(&payload_der);
|
||||
assert!(verify_state_hash(&hash, &payload_der));
|
||||
let mut tampered = payload_der.clone();
|
||||
*tampered.last_mut().expect("non-empty der") ^= 0x01;
|
||||
assert!(!verify_state_hash(&hash, &tampered));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_and_router_key_skeletons_encode_payloads_and_validate_sorting() {
|
||||
let manifest_instances = vec![ManifestInstance {
|
||||
hash: vec![0x33; 32],
|
||||
size: 2048,
|
||||
aki: vec![0x44; 20],
|
||||
manifest_number: BigUnsigned { bytes_be: vec![0x01] },
|
||||
this_update: sample_time(),
|
||||
locations: vec![vec![0x30, 0x00]],
|
||||
subordinates: vec![vec![0x55; 20]],
|
||||
}];
|
||||
let mis_der = encode_manifest_state_payload_der(&manifest_instances).expect("encode manifest state payload");
|
||||
let manifest_state = ManifestState {
|
||||
mis: manifest_instances,
|
||||
most_recent_update: sample_time(),
|
||||
hash: compute_state_hash(&mis_der),
|
||||
};
|
||||
manifest_state.validate().expect("manifest state validate");
|
||||
|
||||
let rksets = vec![RouterKeySet {
|
||||
as_id: 64496,
|
||||
router_keys: vec![RouterKey {
|
||||
ski: vec![0x66; 20],
|
||||
spki_der: vec![0x30, 0x00],
|
||||
}],
|
||||
}];
|
||||
let rk_der = encode_router_key_state_payload_der(&rksets).expect("encode router key payload");
|
||||
let rks = RouterKeyState {
|
||||
rksets,
|
||||
hash: compute_state_hash(&rk_der),
|
||||
};
|
||||
rks.validate().expect("router key state validate");
|
||||
}
|
||||
|
||||
fn sample_manifest_state() -> ManifestState {
|
||||
let mis = vec![ManifestInstance {
|
||||
hash: vec![0x10; 32],
|
||||
size: 2048,
|
||||
aki: vec![0x20; 20],
|
||||
manifest_number: BigUnsigned { bytes_be: vec![1] },
|
||||
this_update: sample_time(),
|
||||
locations: vec![vec![0x30, 0x00]],
|
||||
subordinates: vec![vec![0x30; 20], vec![0x40; 20]],
|
||||
}];
|
||||
let mis_der = encode_manifest_state_payload_der(&mis).expect("encode mis");
|
||||
ManifestState {
|
||||
mis,
|
||||
most_recent_update: sample_time(),
|
||||
hash: compute_state_hash(&mis_der),
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_roa_state() -> RoaPayloadState {
|
||||
let rps = vec![RoaPayloadSet {
|
||||
as_id: 64496,
|
||||
ip_addr_blocks: vec![vec![0x30, 0x00]],
|
||||
}];
|
||||
let der = rpki::ccr::encode::encode_roa_payload_state_payload_der(&rps).expect("encode rps");
|
||||
RoaPayloadState {
|
||||
rps,
|
||||
hash: compute_state_hash(&der),
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_aspa_state() -> AspaPayloadState {
|
||||
let aps = vec![AspaPayloadSet {
|
||||
customer_as_id: 64496,
|
||||
providers: vec![64497, 64498],
|
||||
}];
|
||||
let der = rpki::ccr::encode::encode_aspa_payload_state_payload_der(&aps).expect("encode aps");
|
||||
AspaPayloadState {
|
||||
aps,
|
||||
hash: compute_state_hash(&der),
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_ta_state() -> TrustAnchorState {
|
||||
let skis = vec![vec![0x11; 20], vec![0x22; 20]];
|
||||
let der = encode_trust_anchor_state_payload_der(&skis).expect("encode skis");
|
||||
TrustAnchorState {
|
||||
skis,
|
||||
hash: compute_state_hash(&der),
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_router_key_state() -> RouterKeyState {
|
||||
let rksets = vec![
|
||||
RouterKeySet {
|
||||
as_id: 64496,
|
||||
router_keys: vec![RouterKey {
|
||||
ski: vec![0x41; 20],
|
||||
spki_der: vec![0x30, 0x00],
|
||||
}],
|
||||
},
|
||||
RouterKeySet {
|
||||
as_id: 64497,
|
||||
router_keys: vec![RouterKey {
|
||||
ski: vec![0x42; 20],
|
||||
spki_der: vec![0x30, 0x00],
|
||||
}],
|
||||
},
|
||||
];
|
||||
let der = encode_router_key_state_payload_der(&rksets).expect("encode rksets");
|
||||
RouterKeyState {
|
||||
rksets,
|
||||
hash: compute_state_hash(&der),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_ccr_roundtrips_all_supported_state_aspects() {
|
||||
let ccr = RpkiCanonicalCacheRepresentation {
|
||||
version: 0,
|
||||
hash_alg: CcrDigestAlgorithm::Sha256,
|
||||
produced_at: sample_time(),
|
||||
mfts: Some(sample_manifest_state()),
|
||||
vrps: Some(sample_roa_state()),
|
||||
vaps: Some(sample_aspa_state()),
|
||||
tas: Some(sample_ta_state()),
|
||||
rks: Some(sample_router_key_state()),
|
||||
};
|
||||
let encoded = encode_content_info(&CcrContentInfo::new(ccr.clone())).expect("encode full ccr");
|
||||
let decoded = decode_content_info(&encoded).expect("decode full ccr");
|
||||
assert_eq!(decoded.content, ccr);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_rejects_wrong_digest_algorithm_oid() {
|
||||
let ccr = CcrContentInfo::new(RpkiCanonicalCacheRepresentation {
|
||||
version: 0,
|
||||
hash_alg: CcrDigestAlgorithm::Sha256,
|
||||
produced_at: sample_time(),
|
||||
mfts: None,
|
||||
vrps: None,
|
||||
vaps: None,
|
||||
tas: Some(sample_ta_state()),
|
||||
rks: None,
|
||||
});
|
||||
let mut der = encode_content_info(&ccr).expect("encode ccr");
|
||||
let oid = rpki::data_model::oid::OID_SHA256_RAW;
|
||||
let pos = der.windows(oid.len()).position(|w| w == oid).expect("sha256 oid present");
|
||||
der[pos + oid.len() - 1] ^= 0x01;
|
||||
let err = decode_content_info(&der).expect_err("decode must reject wrong digest oid");
|
||||
assert!(err.to_string().contains("unexpected digest algorithm OID"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_rejects_bad_generalized_time() {
|
||||
let ccr = CcrContentInfo::new(RpkiCanonicalCacheRepresentation {
|
||||
version: 0,
|
||||
hash_alg: CcrDigestAlgorithm::Sha256,
|
||||
produced_at: sample_time(),
|
||||
mfts: None,
|
||||
vrps: None,
|
||||
vaps: None,
|
||||
tas: Some(sample_ta_state()),
|
||||
rks: None,
|
||||
});
|
||||
let mut der = encode_content_info(&ccr).expect("encode ccr");
|
||||
let pos = der.windows(15).position(|w| w == b"20260324000000Z").expect("time present");
|
||||
der[pos + 14] = b'X';
|
||||
let err = decode_content_info(&der).expect_err("bad time must fail");
|
||||
assert!(err.to_string().contains("GeneralizedTime"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_state_validate_rejects_unsorted_subordinates() {
|
||||
let mut state = sample_manifest_state();
|
||||
state.mis[0].subordinates = vec![vec![0x40; 20], vec![0x30; 20]];
|
||||
let err = state.validate().expect_err("unsorted subordinates must fail");
|
||||
assert!(err.to_string().contains("subordinates"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roa_payload_state_validate_rejects_duplicate_asn_sets() {
|
||||
let state = RoaPayloadState {
|
||||
rps: vec![
|
||||
RoaPayloadSet { as_id: 64496, ip_addr_blocks: vec![vec![0x30, 0x00]] },
|
||||
RoaPayloadSet { as_id: 64496, ip_addr_blocks: vec![vec![0x30, 0x00]] },
|
||||
],
|
||||
hash: vec![0u8; 32],
|
||||
};
|
||||
let err = state.validate().expect_err("duplicate roa sets must fail");
|
||||
assert!(err.to_string().contains("ROAPayloadState.rps"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspa_payload_state_validate_rejects_unsorted_providers() {
|
||||
let state = AspaPayloadState {
|
||||
aps: vec![AspaPayloadSet { customer_as_id: 64496, providers: vec![64498, 64497] }],
|
||||
hash: vec![0u8; 32],
|
||||
};
|
||||
let err = state.validate().expect_err("unsorted providers must fail");
|
||||
assert!(err.to_string().contains("providers"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trust_anchor_state_validate_rejects_invalid_ski_length() {
|
||||
let state = TrustAnchorState {
|
||||
skis: vec![vec![0x11; 19]],
|
||||
hash: vec![0u8; 32],
|
||||
};
|
||||
let err = state.validate().expect_err("bad ski len must fail");
|
||||
assert!(err.to_string().contains("TrustAnchorState.skis"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn router_key_state_validate_rejects_unsorted_router_keys() {
|
||||
let state = RouterKeyState {
|
||||
rksets: vec![RouterKeySet {
|
||||
as_id: 64496,
|
||||
router_keys: vec![
|
||||
RouterKey { ski: vec![0x42; 20], spki_der: vec![0x30, 0x00] },
|
||||
RouterKey { ski: vec![0x41; 20], spki_der: vec![0x30, 0x00] },
|
||||
],
|
||||
}],
|
||||
hash: vec![0u8; 32],
|
||||
};
|
||||
let err = state.validate().expect_err("unsorted router keys must fail");
|
||||
assert!(err.to_string().contains("router_keys"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_instance_validate_rejects_bad_location_tag() {
|
||||
let instance = ManifestInstance {
|
||||
hash: vec![0x10; 32],
|
||||
size: 2048,
|
||||
aki: vec![0x20; 20],
|
||||
manifest_number: BigUnsigned { bytes_be: vec![1] },
|
||||
this_update: sample_time(),
|
||||
locations: vec![vec![0x04, 0x00]],
|
||||
subordinates: vec![],
|
||||
};
|
||||
let err = instance.validate().expect_err("bad AccessDescription tag must fail");
|
||||
assert!(err.to_string().contains("unexpected tag"), "{err}");
|
||||
}
|
||||
202
tests/test_ccr_m7.rs
Normal file
202
tests/test_ccr_m7.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
use rpki::ccr::{
|
||||
CcrContentInfo, CcrDigestAlgorithm, ManifestInstance, ManifestState, RoaPayloadSet,
|
||||
RoaPayloadState, RouterKey, RouterKeySet, RouterKeyState, TrustAnchorState,
|
||||
compute_state_hash, decode_content_info, dump_content_info_json_value,
|
||||
encode::{
|
||||
encode_aspa_payload_state_payload_der, encode_content_info,
|
||||
encode_manifest_state_payload_der, encode_roa_payload_state_payload_der,
|
||||
encode_router_key_state_payload_der, encode_trust_anchor_state_payload_der,
|
||||
},
|
||||
verify::{verify_against_report_json_path, verify_against_vcir_store, verify_content_info_bytes},
|
||||
};
|
||||
use rpki::data_model::common::BigUnsigned;
|
||||
use rpki::storage::{
|
||||
PackTime, RocksStore, ValidatedCaInstanceResult, ValidatedManifestMeta,
|
||||
VcirArtifactKind, VcirArtifactRole, VcirArtifactValidationStatus, VcirAuditSummary,
|
||||
VcirChildEntry, VcirInstanceGate, VcirRelatedArtifact, VcirSummary,
|
||||
};
|
||||
|
||||
fn sample_time() -> time::OffsetDateTime {
|
||||
time::OffsetDateTime::parse(
|
||||
"2026-03-24T00:00:00Z",
|
||||
&time::format_description::well_known::Rfc3339,
|
||||
)
|
||||
.expect("valid rfc3339")
|
||||
}
|
||||
|
||||
fn sample_manifest_state() -> ManifestState {
|
||||
let mis = vec![ManifestInstance {
|
||||
hash: vec![0x10; 32],
|
||||
size: 2048,
|
||||
aki: vec![0x20; 20],
|
||||
manifest_number: BigUnsigned { bytes_be: vec![1] },
|
||||
this_update: sample_time(),
|
||||
locations: vec![vec![0x30, 0x00]],
|
||||
subordinates: vec![vec![0x30; 20]],
|
||||
}];
|
||||
let der = encode_manifest_state_payload_der(&mis).expect("encode mis");
|
||||
ManifestState {
|
||||
mis,
|
||||
most_recent_update: sample_time(),
|
||||
hash: compute_state_hash(&der),
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_roa_state() -> RoaPayloadState {
|
||||
let rps = vec![RoaPayloadSet {
|
||||
as_id: 64496,
|
||||
ip_addr_blocks: vec![vec![0x30, 0x08, 0x04, 0x02, 0x00, 0x01, 0x30, 0x02, 0x03, 0x00]],
|
||||
}];
|
||||
let der = encode_roa_payload_state_payload_der(&rps).expect("encode rps");
|
||||
RoaPayloadState { rps, hash: compute_state_hash(&der) }
|
||||
}
|
||||
|
||||
fn sample_aspa_state() -> rpki::ccr::AspaPayloadState {
|
||||
let aps = vec![rpki::ccr::AspaPayloadSet { customer_as_id: 64496, providers: vec![64497] }];
|
||||
let der = encode_aspa_payload_state_payload_der(&aps).expect("encode aps");
|
||||
rpki::ccr::AspaPayloadState { aps, hash: compute_state_hash(&der) }
|
||||
}
|
||||
|
||||
fn sample_ta_state() -> TrustAnchorState {
|
||||
let skis = vec![vec![0x11; 20]];
|
||||
let der = encode_trust_anchor_state_payload_der(&skis).expect("encode skis");
|
||||
TrustAnchorState { skis, hash: compute_state_hash(&der) }
|
||||
}
|
||||
|
||||
fn sample_rks() -> RouterKeyState {
|
||||
let rksets = vec![RouterKeySet { as_id: 64496, router_keys: vec![RouterKey { ski: vec![0x22; 20], spki_der: vec![0x30, 0x00] }] }];
|
||||
let der = encode_router_key_state_payload_der(&rksets).expect("encode rk");
|
||||
RouterKeyState { rksets, hash: compute_state_hash(&der) }
|
||||
}
|
||||
|
||||
fn sample_ccr() -> Vec<u8> {
|
||||
let ccr = rpki::ccr::RpkiCanonicalCacheRepresentation {
|
||||
version: 0,
|
||||
hash_alg: CcrDigestAlgorithm::Sha256,
|
||||
produced_at: sample_time(),
|
||||
mfts: Some(sample_manifest_state()),
|
||||
vrps: Some(sample_roa_state()),
|
||||
vaps: Some(sample_aspa_state()),
|
||||
tas: Some(sample_ta_state()),
|
||||
rks: Some(sample_rks()),
|
||||
};
|
||||
encode_content_info(&CcrContentInfo::new(ccr)).expect("encode ccr")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_content_info_bytes_succeeds_for_valid_ccr() {
|
||||
let der = sample_ccr();
|
||||
let summary = verify_content_info_bytes(&der).expect("verify ccr");
|
||||
assert!(summary.state_hashes_ok);
|
||||
assert_eq!(summary.manifest_instances, 1);
|
||||
assert_eq!(summary.roa_payload_sets, 1);
|
||||
assert_eq!(summary.aspa_payload_sets, 1);
|
||||
assert_eq!(summary.trust_anchor_ski_count, 1);
|
||||
assert_eq!(summary.router_key_sets, 1);
|
||||
assert_eq!(summary.router_key_count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_content_info_bytes_rejects_tampered_manifest_hash() {
|
||||
let mut content_info = decode_content_info(&sample_ccr()).expect("decode ccr");
|
||||
content_info.content.mfts.as_mut().unwrap().hash[0] ^= 0x01;
|
||||
let der = encode_content_info(&content_info).expect("encode tampered ccr");
|
||||
let err = verify_content_info_bytes(&der).expect_err("tampered hash must fail");
|
||||
assert!(err.to_string().contains("ManifestState hash mismatch"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dump_content_info_json_value_contains_expected_counts() {
|
||||
let json = dump_content_info_json_value(&sample_ccr()).expect("dump ccr");
|
||||
assert_eq!(json["version"], 0);
|
||||
assert_eq!(json["state_aspects"]["mfts"]["manifest_instances"], 1);
|
||||
assert_eq!(json["state_aspects"]["vrps"]["payload_sets"], 1);
|
||||
assert_eq!(json["state_aspects"]["vaps"]["payload_sets"], 1);
|
||||
assert_eq!(json["state_aspects"]["tas"]["ski_count"], 1);
|
||||
assert_eq!(json["state_aspects"]["rks"]["router_key_sets"], 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_against_report_json_path_rejects_mismatching_report() {
|
||||
let td = tempfile::tempdir().expect("tempdir");
|
||||
let report = serde_json::json!({
|
||||
"vrps": [{"asn": 64496, "prefix": "0.0.0.0/0", "max_length": 0}],
|
||||
"aspas": [{"customer_as_id": 64496, "provider_as_ids": [64497]}]
|
||||
});
|
||||
let report_path = td.path().join("report.json");
|
||||
std::fs::write(&report_path, serde_json::to_vec(&report).unwrap()).expect("write report");
|
||||
|
||||
let mut ci = decode_content_info(&sample_ccr()).expect("decode ccr");
|
||||
ci.content.vrps = Some(RoaPayloadState {
|
||||
rps: vec![RoaPayloadSet { as_id: 64496, ip_addr_blocks: vec![vec![0x30, 0x08, 0x04, 0x02, 0x00, 0x01, 0x30, 0x02, 0x03, 0x00]] }],
|
||||
hash: compute_state_hash(&encode_roa_payload_state_payload_der(&[RoaPayloadSet { as_id: 64496, ip_addr_blocks: vec![vec![0x30, 0x08, 0x04, 0x02, 0x00, 0x01, 0x30, 0x02, 0x03, 0x00]] }]).unwrap()),
|
||||
});
|
||||
verify_against_report_json_path(&ci, &report_path).expect_err("report mismatch expected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_against_vcir_store_matches_manifest_hashes() {
|
||||
let td = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(td.path()).expect("open rocksdb");
|
||||
let manifest_hash = hex::encode(vec![0x10; 32]);
|
||||
let vcir = ValidatedCaInstanceResult {
|
||||
manifest_rsync_uri: "rsync://example.test/current.mft".to_string(),
|
||||
parent_manifest_rsync_uri: None,
|
||||
tal_id: "apnic".to_string(),
|
||||
ca_subject_name: "CN=test".to_string(),
|
||||
ca_ski: "11".repeat(20),
|
||||
issuer_ski: "22".repeat(20),
|
||||
last_successful_validation_time: PackTime::from_utc_offset_datetime(sample_time()),
|
||||
current_manifest_rsync_uri: "rsync://example.test/current.mft".to_string(),
|
||||
current_crl_rsync_uri: "rsync://example.test/current.crl".to_string(),
|
||||
validated_manifest_meta: ValidatedManifestMeta {
|
||||
validated_manifest_number: vec![1],
|
||||
validated_manifest_this_update: PackTime::from_utc_offset_datetime(sample_time()),
|
||||
validated_manifest_next_update: PackTime::from_utc_offset_datetime(sample_time()),
|
||||
},
|
||||
instance_gate: VcirInstanceGate {
|
||||
manifest_next_update: PackTime::from_utc_offset_datetime(sample_time()),
|
||||
current_crl_next_update: PackTime::from_utc_offset_datetime(sample_time()),
|
||||
self_ca_not_after: PackTime::from_utc_offset_datetime(sample_time()),
|
||||
instance_effective_until: PackTime::from_utc_offset_datetime(sample_time()),
|
||||
},
|
||||
child_entries: vec![VcirChildEntry {
|
||||
child_manifest_rsync_uri: "rsync://example.test/child.mft".to_string(),
|
||||
child_cert_rsync_uri: "rsync://example.test/child.cer".to_string(),
|
||||
child_cert_hash: "aa".repeat(32),
|
||||
child_ski: "33".repeat(20),
|
||||
child_rsync_base_uri: "rsync://example.test/".to_string(),
|
||||
child_publication_point_rsync_uri: "rsync://example.test/".to_string(),
|
||||
child_rrdp_notification_uri: None,
|
||||
child_effective_ip_resources: None,
|
||||
child_effective_as_resources: None,
|
||||
accepted_at_validation_time: PackTime::from_utc_offset_datetime(sample_time()),
|
||||
}],
|
||||
local_outputs: Vec::new(),
|
||||
related_artifacts: vec![VcirRelatedArtifact {
|
||||
artifact_role: VcirArtifactRole::Manifest,
|
||||
artifact_kind: VcirArtifactKind::Mft,
|
||||
uri: Some("rsync://example.test/current.mft".to_string()),
|
||||
sha256: manifest_hash.clone(),
|
||||
object_type: Some("mft".to_string()),
|
||||
validation_status: VcirArtifactValidationStatus::Accepted,
|
||||
}],
|
||||
summary: VcirSummary {
|
||||
local_vrp_count: 0,
|
||||
local_aspa_count: 0,
|
||||
local_router_key_count: 0,
|
||||
child_count: 1,
|
||||
accepted_object_count: 1,
|
||||
rejected_object_count: 0,
|
||||
},
|
||||
audit_summary: VcirAuditSummary {
|
||||
failed_fetch_eligible: true,
|
||||
last_failed_fetch_reason: None,
|
||||
warning_count: 0,
|
||||
audit_flags: Vec::new(),
|
||||
},
|
||||
};
|
||||
store.put_vcir(&vcir).expect("put vcir");
|
||||
let ci = decode_content_info(&sample_ccr()).expect("decode ccr");
|
||||
verify_against_vcir_store(&ci, &store).expect("vcir cross-check");
|
||||
}
|
||||
114
tests/test_ccr_tools_m7.rs
Normal file
114
tests/test_ccr_tools_m7.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
|
||||
use rpki::ccr::{
|
||||
CcrContentInfo, CcrDigestAlgorithm, TrustAnchorState, compute_state_hash,
|
||||
encode::{encode_content_info, encode_trust_anchor_state_payload_der},
|
||||
};
|
||||
use std::process::Command;
|
||||
|
||||
fn sample_ccr_file() -> (tempfile::TempDir, std::path::PathBuf) {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let skis = vec![vec![0x11; 20]];
|
||||
let skis_der = encode_trust_anchor_state_payload_der(&skis).expect("encode skis");
|
||||
let ccr = CcrContentInfo::new(rpki::ccr::RpkiCanonicalCacheRepresentation {
|
||||
version: 0,
|
||||
hash_alg: CcrDigestAlgorithm::Sha256,
|
||||
produced_at: time::OffsetDateTime::parse(
|
||||
"2026-03-24T00:00:00Z",
|
||||
&time::format_description::well_known::Rfc3339,
|
||||
)
|
||||
.expect("time"),
|
||||
mfts: None,
|
||||
vrps: None,
|
||||
vaps: None,
|
||||
tas: Some(TrustAnchorState { skis, hash: compute_state_hash(&skis_der) }),
|
||||
rks: None,
|
||||
});
|
||||
let path = dir.path().join("sample.ccr");
|
||||
std::fs::write(&path, encode_content_info(&ccr).expect("encode ccr")).expect("write ccr");
|
||||
(dir, path)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ccr_dump_binary_prints_json_summary() {
|
||||
let (_dir, ccr_path) = sample_ccr_file();
|
||||
let bin = env!("CARGO_BIN_EXE_ccr_dump");
|
||||
let out = Command::new(bin)
|
||||
.args(["--ccr", ccr_path.to_string_lossy().as_ref()])
|
||||
.output()
|
||||
.expect("run ccr_dump");
|
||||
assert!(out.status.success(), "stderr={}", String::from_utf8_lossy(&out.stderr));
|
||||
let json: serde_json::Value = serde_json::from_slice(&out.stdout).expect("parse json");
|
||||
assert_eq!(json["version"], 0);
|
||||
assert_eq!(json["state_aspects"]["tas"]["ski_count"], 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ccr_verify_binary_prints_summary() {
|
||||
let (_dir, ccr_path) = sample_ccr_file();
|
||||
let bin = env!("CARGO_BIN_EXE_ccr_verify");
|
||||
let out = Command::new(bin)
|
||||
.args(["--ccr", ccr_path.to_string_lossy().as_ref()])
|
||||
.output()
|
||||
.expect("run ccr_verify");
|
||||
assert!(out.status.success(), "stderr={}", String::from_utf8_lossy(&out.stderr));
|
||||
let json: serde_json::Value = serde_json::from_slice(&out.stdout).expect("parse json");
|
||||
assert_eq!(json["version"], 0);
|
||||
assert_eq!(json["trust_anchor_ski_count"], 1);
|
||||
assert_eq!(json["state_hashes_ok"], true);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn ccr_to_routinator_csv_binary_writes_vrp_csv() {
|
||||
use rpki::ccr::{
|
||||
CcrContentInfo, CcrDigestAlgorithm, RpkiCanonicalCacheRepresentation,
|
||||
build_roa_payload_state, encode::encode_content_info,
|
||||
};
|
||||
use rpki::validation::objects::Vrp;
|
||||
use rpki::data_model::roa::{IpPrefix, RoaAfi};
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let ccr_path = dir.path().join("vrp.ccr");
|
||||
let csv_path = dir.path().join("out.csv");
|
||||
let vrps = vec![Vrp {
|
||||
asn: 64496,
|
||||
prefix: IpPrefix {
|
||||
afi: RoaAfi::Ipv4,
|
||||
prefix_len: 24,
|
||||
addr: [203, 0, 113, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
},
|
||||
max_length: 24,
|
||||
}];
|
||||
let roa_state = build_roa_payload_state(&vrps).expect("build roa state");
|
||||
let ccr = CcrContentInfo::new(RpkiCanonicalCacheRepresentation {
|
||||
version: 0,
|
||||
hash_alg: CcrDigestAlgorithm::Sha256,
|
||||
produced_at: time::OffsetDateTime::parse(
|
||||
"2026-03-25T00:00:00Z",
|
||||
&time::format_description::well_known::Rfc3339,
|
||||
)
|
||||
.expect("time"),
|
||||
mfts: None,
|
||||
vrps: Some(roa_state),
|
||||
vaps: None,
|
||||
tas: None,
|
||||
rks: None,
|
||||
});
|
||||
std::fs::write(&ccr_path, encode_content_info(&ccr).expect("encode ccr")).expect("write ccr");
|
||||
|
||||
let bin = env!("CARGO_BIN_EXE_ccr_to_routinator_csv");
|
||||
let out = Command::new(bin)
|
||||
.args([
|
||||
"--ccr",
|
||||
ccr_path.to_string_lossy().as_ref(),
|
||||
"--out",
|
||||
csv_path.to_string_lossy().as_ref(),
|
||||
"--trust-anchor",
|
||||
"apnic",
|
||||
])
|
||||
.output()
|
||||
.expect("run ccr_to_routinator_csv");
|
||||
assert!(out.status.success(), "stderr={}", String::from_utf8_lossy(&out.stderr));
|
||||
let csv = std::fs::read_to_string(csv_path).expect("read csv");
|
||||
assert!(csv.contains("ASN,IP Prefix,Max Length,Trust Anchor"));
|
||||
assert!(csv.contains("AS64496,203.0.113.0/24,24,apnic"));
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
#[test]
|
||||
fn cli_run_offline_mode_executes_and_writes_json() {
|
||||
fn cli_run_offline_mode_executes_and_writes_json_and_ccr() {
|
||||
let db_dir = tempfile::tempdir().expect("db tempdir");
|
||||
let repo_dir = tempfile::tempdir().expect("repo tempdir");
|
||||
let out_dir = tempfile::tempdir().expect("out tempdir");
|
||||
let report_path = out_dir.path().join("report.json");
|
||||
let ccr_path = out_dir.path().join("result.ccr");
|
||||
|
||||
let policy_path = out_dir.path().join("policy.toml");
|
||||
std::fs::write(&policy_path, "sync_preference = \"rsync_only\"\n").expect("write policy");
|
||||
@@ -31,6 +32,8 @@ fn cli_run_offline_mode_executes_and_writes_json() {
|
||||
"1".to_string(),
|
||||
"--report-json".to_string(),
|
||||
report_path.to_string_lossy().to_string(),
|
||||
"--ccr-out".to_string(),
|
||||
ccr_path.to_string_lossy().to_string(),
|
||||
];
|
||||
|
||||
rpki::cli::run(&argv).expect("cli run");
|
||||
@@ -39,3 +42,41 @@ fn cli_run_offline_mode_executes_and_writes_json() {
|
||||
let v: serde_json::Value = serde_json::from_slice(&bytes).expect("parse report json");
|
||||
assert_eq!(v["format_version"], 2);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn cli_run_offline_mode_writes_decodable_ccr() {
|
||||
let db_dir = tempfile::tempdir().expect("db tempdir");
|
||||
let repo_dir = tempfile::tempdir().expect("repo tempdir");
|
||||
let out_dir = tempfile::tempdir().expect("out tempdir");
|
||||
let ccr_path = out_dir.path().join("result.ccr");
|
||||
|
||||
let tal_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/fixtures/tal/apnic-rfc7730-https.tal");
|
||||
let ta_path =
|
||||
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/ta/apnic-ta.cer");
|
||||
|
||||
let argv = vec![
|
||||
"rpki".to_string(),
|
||||
"--db".to_string(),
|
||||
db_dir.path().to_string_lossy().to_string(),
|
||||
"--tal-path".to_string(),
|
||||
tal_path.to_string_lossy().to_string(),
|
||||
"--ta-path".to_string(),
|
||||
ta_path.to_string_lossy().to_string(),
|
||||
"--rsync-local-dir".to_string(),
|
||||
repo_dir.path().to_string_lossy().to_string(),
|
||||
"--max-depth".to_string(),
|
||||
"0".to_string(),
|
||||
"--max-instances".to_string(),
|
||||
"1".to_string(),
|
||||
"--ccr-out".to_string(),
|
||||
ccr_path.to_string_lossy().to_string(),
|
||||
];
|
||||
|
||||
rpki::cli::run(&argv).expect("cli run");
|
||||
|
||||
let bytes = std::fs::read(&ccr_path).expect("read ccr");
|
||||
let ccr = rpki::ccr::decode_content_info(&bytes).expect("decode ccr");
|
||||
assert!(ccr.content.tas.is_some());
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@ fn store_validated_manifest_baseline(
|
||||
summary: VcirSummary {
|
||||
local_vrp_count: 0,
|
||||
local_aspa_count: 0,
|
||||
local_router_key_count: 0,
|
||||
child_count: 0,
|
||||
accepted_object_count: 1,
|
||||
rejected_object_count: 0,
|
||||
|
||||
@@ -101,6 +101,7 @@ fn store_validated_manifest_baseline(
|
||||
summary: VcirSummary {
|
||||
local_vrp_count: 0,
|
||||
local_aspa_count: 0,
|
||||
local_router_key_count: 0,
|
||||
child_count: 0,
|
||||
accepted_object_count: 1,
|
||||
rejected_object_count: 0,
|
||||
|
||||
366
tests/test_router_cert_m4.rs
Normal file
366
tests/test_router_cert_m4.rs
Normal file
@@ -0,0 +1,366 @@
|
||||
use std::process::Command;
|
||||
|
||||
use rpki::data_model::rc::{ResourceCertificate, ResourceCertKind};
|
||||
use rpki::data_model::router_cert::{
|
||||
BgpsecRouterCertificate, BgpsecRouterCertificateDecodeError,
|
||||
BgpsecRouterCertificatePathError, BgpsecRouterCertificateProfileError,
|
||||
};
|
||||
|
||||
fn openssl_available() -> bool {
|
||||
Command::new("openssl")
|
||||
.arg("version")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn run(cmd: &mut Command) {
|
||||
let out = cmd.output().expect("run command");
|
||||
if !out.status.success() {
|
||||
panic!(
|
||||
"command failed: {:?}\nstdout={}\nstderr={}",
|
||||
cmd,
|
||||
String::from_utf8_lossy(&out.stdout),
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
struct Generated {
|
||||
issuer_ca_der: Vec<u8>,
|
||||
router_der: Vec<u8>,
|
||||
issuer_crl_der: Vec<u8>,
|
||||
wrong_issuer_der: Vec<u8>,
|
||||
}
|
||||
|
||||
fn generate_router_cert_with_variant(
|
||||
key_spec: &str,
|
||||
include_eku: bool,
|
||||
extra_ext: &str,
|
||||
) -> Generated {
|
||||
assert!(openssl_available(), "openssl is required for this test");
|
||||
let td = tempfile::tempdir().expect("tempdir");
|
||||
let dir = td.path();
|
||||
|
||||
std::fs::create_dir_all(dir.join("newcerts")).expect("newcerts");
|
||||
std::fs::write(dir.join("index.txt"), b"").expect("index");
|
||||
std::fs::write(dir.join("serial"), b"1000\n").expect("serial");
|
||||
std::fs::write(dir.join("crlnumber"), b"1000\n").expect("crlnumber");
|
||||
|
||||
let cnf = format!(
|
||||
r#"
|
||||
[ ca ]
|
||||
default_ca = CA_default
|
||||
|
||||
[ CA_default ]
|
||||
dir = {dir}
|
||||
database = $dir/index.txt
|
||||
new_certs_dir = $dir/newcerts
|
||||
certificate = $dir/issuer.pem
|
||||
private_key = $dir/issuer.key
|
||||
serial = $dir/serial
|
||||
crlnumber = $dir/crlnumber
|
||||
default_md = sha256
|
||||
default_days = 365
|
||||
default_crl_days = 1
|
||||
policy = policy_any
|
||||
x509_extensions = v3_issuer_ca
|
||||
crl_extensions = crl_ext
|
||||
unique_subject = no
|
||||
copy_extensions = none
|
||||
|
||||
[ policy_any ]
|
||||
commonName = supplied
|
||||
|
||||
[ req ]
|
||||
prompt = no
|
||||
distinguished_name = dn
|
||||
|
||||
[ dn ]
|
||||
CN = Test Issuer CA
|
||||
|
||||
[ v3_issuer_ca ]
|
||||
basicConstraints = critical,CA:true
|
||||
keyUsage = critical, keyCertSign, cRLSign
|
||||
subjectKeyIdentifier = hash
|
||||
authorityKeyIdentifier = keyid:always
|
||||
certificatePolicies = critical, 1.3.6.1.5.5.7.14.2
|
||||
subjectInfoAccess = caRepository;URI:rsync://example.test/repo/issuer/, rpkiManifest;URI:rsync://example.test/repo/issuer/issuer.mft, rpkiNotify;URI:https://example.test/notification.xml
|
||||
sbgp-ipAddrBlock = critical, IPv4:10.0.0.0/8
|
||||
sbgp-autonomousSysNum = critical, AS:64496-64511
|
||||
|
||||
[ v3_router ]
|
||||
keyUsage = critical, digitalSignature
|
||||
{eku_line}
|
||||
authorityKeyIdentifier = keyid:always
|
||||
crlDistributionPoints = URI:rsync://example.test/repo/issuer/issuer.crl
|
||||
authorityInfoAccess = caIssuers;URI:rsync://example.test/repo/issuer/issuer.cer
|
||||
certificatePolicies = critical, 1.3.6.1.5.5.7.14.2
|
||||
sbgp-autonomousSysNum = critical, AS:64496
|
||||
{eku_line}
|
||||
{extra_ext}
|
||||
|
||||
[ crl_ext ]
|
||||
authorityKeyIdentifier = keyid:always
|
||||
"#,
|
||||
dir = dir.display(),
|
||||
eku_line = if include_eku { "extendedKeyUsage = 1.3.6.1.5.5.7.3.30" } else { "" },
|
||||
extra_ext = extra_ext
|
||||
);
|
||||
std::fs::write(dir.join("openssl.cnf"), cnf.as_bytes()).expect("write cnf");
|
||||
|
||||
run(Command::new("openssl")
|
||||
.arg("genrsa")
|
||||
.arg("-out")
|
||||
.arg(dir.join("issuer.key"))
|
||||
.arg("2048"));
|
||||
run(Command::new("openssl")
|
||||
.arg("req")
|
||||
.arg("-new")
|
||||
.arg("-x509")
|
||||
.arg("-sha256")
|
||||
.arg("-days")
|
||||
.arg("365")
|
||||
.arg("-key")
|
||||
.arg(dir.join("issuer.key"))
|
||||
.arg("-config")
|
||||
.arg(dir.join("openssl.cnf"))
|
||||
.arg("-extensions")
|
||||
.arg("v3_issuer_ca")
|
||||
.arg("-out")
|
||||
.arg(dir.join("issuer.pem")));
|
||||
|
||||
match key_spec {
|
||||
"ec-p256" => {
|
||||
run(Command::new("openssl")
|
||||
.arg("ecparam")
|
||||
.arg("-name")
|
||||
.arg("prime256v1")
|
||||
.arg("-genkey")
|
||||
.arg("-noout")
|
||||
.arg("-out")
|
||||
.arg(dir.join("router.key")));
|
||||
}
|
||||
"ec-p384" => {
|
||||
run(Command::new("openssl")
|
||||
.arg("ecparam")
|
||||
.arg("-name")
|
||||
.arg("secp384r1")
|
||||
.arg("-genkey")
|
||||
.arg("-noout")
|
||||
.arg("-out")
|
||||
.arg(dir.join("router.key")));
|
||||
}
|
||||
"rsa" => {
|
||||
run(Command::new("openssl")
|
||||
.arg("genrsa")
|
||||
.arg("-out")
|
||||
.arg(dir.join("router.key"))
|
||||
.arg("2048"));
|
||||
}
|
||||
other => panic!("unsupported key_spec {other}"),
|
||||
}
|
||||
|
||||
run(Command::new("openssl")
|
||||
.arg("req")
|
||||
.arg("-new")
|
||||
.arg("-key")
|
||||
.arg(dir.join("router.key"))
|
||||
.arg("-subj")
|
||||
.arg("/CN=ROUTER-0000FC10/serialNumber=01020304")
|
||||
.arg("-out")
|
||||
.arg(dir.join("router.csr")));
|
||||
|
||||
run(Command::new("openssl")
|
||||
.arg("ca")
|
||||
.arg("-batch")
|
||||
.arg("-config")
|
||||
.arg(dir.join("openssl.cnf"))
|
||||
.arg("-in")
|
||||
.arg(dir.join("router.csr"))
|
||||
.arg("-extensions")
|
||||
.arg("v3_router")
|
||||
.arg("-out")
|
||||
.arg(dir.join("router.pem")));
|
||||
|
||||
run(Command::new("openssl")
|
||||
.arg("x509")
|
||||
.arg("-in")
|
||||
.arg(dir.join("issuer.pem"))
|
||||
.arg("-outform")
|
||||
.arg("DER")
|
||||
.arg("-out")
|
||||
.arg(dir.join("issuer.cer")));
|
||||
run(Command::new("openssl")
|
||||
.arg("x509")
|
||||
.arg("-in")
|
||||
.arg(dir.join("router.pem"))
|
||||
.arg("-outform")
|
||||
.arg("DER")
|
||||
.arg("-out")
|
||||
.arg(dir.join("router.cer")));
|
||||
|
||||
run(Command::new("openssl")
|
||||
.arg("ca")
|
||||
.arg("-gencrl")
|
||||
.arg("-config")
|
||||
.arg(dir.join("openssl.cnf"))
|
||||
.arg("-out")
|
||||
.arg(dir.join("issuer.crl.pem")));
|
||||
run(Command::new("openssl")
|
||||
.arg("crl")
|
||||
.arg("-in")
|
||||
.arg(dir.join("issuer.crl.pem"))
|
||||
.arg("-outform")
|
||||
.arg("DER")
|
||||
.arg("-out")
|
||||
.arg(dir.join("issuer.crl")));
|
||||
|
||||
// Wrong issuer for path failure.
|
||||
run(Command::new("openssl")
|
||||
.arg("genrsa")
|
||||
.arg("-out")
|
||||
.arg(dir.join("other.key"))
|
||||
.arg("2048"));
|
||||
run(Command::new("openssl")
|
||||
.arg("req")
|
||||
.arg("-new")
|
||||
.arg("-x509")
|
||||
.arg("-sha256")
|
||||
.arg("-days")
|
||||
.arg("365")
|
||||
.arg("-key")
|
||||
.arg(dir.join("other.key"))
|
||||
.arg("-subj")
|
||||
.arg("/CN=Other Issuer")
|
||||
.arg("-out")
|
||||
.arg(dir.join("other.pem")));
|
||||
run(Command::new("openssl")
|
||||
.arg("x509")
|
||||
.arg("-in")
|
||||
.arg(dir.join("other.pem"))
|
||||
.arg("-outform")
|
||||
.arg("DER")
|
||||
.arg("-out")
|
||||
.arg(dir.join("other.cer")));
|
||||
|
||||
Generated {
|
||||
issuer_ca_der: std::fs::read(dir.join("issuer.cer")).expect("read issuer der"),
|
||||
router_der: std::fs::read(dir.join("router.cer")).expect("read router der"),
|
||||
issuer_crl_der: std::fs::read(dir.join("issuer.crl")).expect("read crl der"),
|
||||
wrong_issuer_der: std::fs::read(dir.join("other.cer")).expect("read other issuer der"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_bgpsec_router_certificate_fixture_smoke() {
|
||||
let g = generate_router_cert_with_variant("ec-p256", true, "");
|
||||
let cert = BgpsecRouterCertificate::decode_der(&g.router_der).expect("decode router cert");
|
||||
assert_eq!(cert.resource_cert.kind, ResourceCertKind::Ee);
|
||||
assert_eq!(cert.asns, vec![64496]);
|
||||
assert_eq!(cert.subject_key_identifier.len(), 20);
|
||||
assert_eq!(cert.spki_der[0], 0x30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn router_certificate_profile_rejects_missing_eku() {
|
||||
let g = generate_router_cert_with_variant("ec-p256", false, "");
|
||||
let err = BgpsecRouterCertificate::decode_der(&g.router_der).unwrap_err();
|
||||
assert!(matches!(err, BgpsecRouterCertificateDecodeError::Validate(BgpsecRouterCertificateProfileError::MissingBgpsecRouterEku | BgpsecRouterCertificateProfileError::MissingExtendedKeyUsage)), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn router_certificate_profile_rejects_sia_and_ip_resources_and_ranges() {
|
||||
let g = generate_router_cert_with_variant(
|
||||
"ec-p256",
|
||||
true,
|
||||
"subjectInfoAccess = caRepository;URI:rsync://example.test/repo/router/\n",
|
||||
);
|
||||
let err = BgpsecRouterCertificate::decode_der(&g.router_der).unwrap_err();
|
||||
assert!(matches!(err, BgpsecRouterCertificateDecodeError::Validate(BgpsecRouterCertificateProfileError::SubjectInfoAccessPresent)), "{err}");
|
||||
|
||||
let g = generate_router_cert_with_variant(
|
||||
"ec-p256",
|
||||
true,
|
||||
"sbgp-ipAddrBlock = critical, IPv4:10.0.0.0/8\n",
|
||||
);
|
||||
let err = BgpsecRouterCertificate::decode_der(&g.router_der).unwrap_err();
|
||||
assert!(matches!(err, BgpsecRouterCertificateDecodeError::Validate(BgpsecRouterCertificateProfileError::IpResourcesPresent)), "{err}");
|
||||
|
||||
let g = generate_router_cert_with_variant(
|
||||
"ec-p256",
|
||||
true,
|
||||
"sbgp-autonomousSysNum = critical, AS:64496-64500\n",
|
||||
);
|
||||
let err = BgpsecRouterCertificate::decode_der(&g.router_der).unwrap_err();
|
||||
assert!(matches!(err, BgpsecRouterCertificateDecodeError::Validate(BgpsecRouterCertificateProfileError::AsResourcesRangeNotAllowed)), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn router_certificate_profile_rejects_wrong_spki_algorithm_or_curve() {
|
||||
let g = generate_router_cert_with_variant("rsa", true, "");
|
||||
let err = BgpsecRouterCertificate::decode_der(&g.router_der).unwrap_err();
|
||||
assert!(matches!(err, BgpsecRouterCertificateDecodeError::Validate(BgpsecRouterCertificateProfileError::SpkiAlgorithmNotEcPublicKey)), "{err}");
|
||||
|
||||
let g = generate_router_cert_with_variant("ec-p384", true, "");
|
||||
let err = BgpsecRouterCertificate::decode_der(&g.router_der).unwrap_err();
|
||||
assert!(matches!(err, BgpsecRouterCertificateDecodeError::Validate(BgpsecRouterCertificateProfileError::SpkiWrongCurve | BgpsecRouterCertificateProfileError::SpkiEcPointNotUncompressedP256)), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn router_certificate_path_validation_accepts_valid_and_rejects_wrong_issuer() {
|
||||
use rpki::data_model::common::BigUnsigned;
|
||||
use rpki::data_model::crl::RpkixCrl;
|
||||
use std::collections::HashSet;
|
||||
use x509_parser::prelude::FromDer;
|
||||
use x509_parser::x509::SubjectPublicKeyInfo;
|
||||
|
||||
let g = generate_router_cert_with_variant("ec-p256", true, "");
|
||||
let issuer = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer");
|
||||
let wrong_issuer = ResourceCertificate::decode_der(&g.wrong_issuer_der).expect("decode wrong issuer");
|
||||
let issuer_crl = RpkixCrl::decode_der(&g.issuer_crl_der).expect("decode crl");
|
||||
let (rem, issuer_spki) = SubjectPublicKeyInfo::from_der(&issuer.tbs.subject_public_key_info).expect("issuer spki");
|
||||
assert!(rem.is_empty());
|
||||
let (rem, wrong_spki) = SubjectPublicKeyInfo::from_der(&wrong_issuer.tbs.subject_public_key_info).expect("wrong issuer spki");
|
||||
assert!(rem.is_empty());
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
|
||||
let cert = BgpsecRouterCertificate::validate_path_with_prevalidated_issuer(
|
||||
&g.router_der,
|
||||
&issuer,
|
||||
&issuer_spki,
|
||||
&issuer_crl,
|
||||
&HashSet::new(),
|
||||
Some("rsync://example.test/repo/issuer/issuer.cer"),
|
||||
Some("rsync://example.test/repo/issuer/issuer.crl"),
|
||||
now,
|
||||
).expect("router path valid");
|
||||
assert_eq!(cert.asns, vec![64496]);
|
||||
|
||||
let err = BgpsecRouterCertificate::validate_path_with_prevalidated_issuer(
|
||||
&g.router_der,
|
||||
&wrong_issuer,
|
||||
&wrong_spki,
|
||||
&issuer_crl,
|
||||
&HashSet::new(),
|
||||
None,
|
||||
None,
|
||||
now,
|
||||
).unwrap_err();
|
||||
assert!(matches!(err, BgpsecRouterCertificatePathError::CertPath(_)), "{err}");
|
||||
|
||||
let rc = ResourceCertificate::decode_der(&g.router_der).expect("decode router rc");
|
||||
let mut revoked = HashSet::new();
|
||||
revoked.insert(BigUnsigned::from_biguint(&rc.tbs.serial_number).bytes_be);
|
||||
let err = BgpsecRouterCertificate::validate_path_with_prevalidated_issuer(
|
||||
&g.router_der,
|
||||
&issuer,
|
||||
&issuer_spki,
|
||||
&issuer_crl,
|
||||
&revoked,
|
||||
Some("rsync://example.test/repo/issuer/issuer.cer"),
|
||||
Some("rsync://example.test/repo/issuer/issuer.crl"),
|
||||
now,
|
||||
).unwrap_err();
|
||||
assert!(matches!(err, BgpsecRouterCertificatePathError::CertPath(_)), "{err}");
|
||||
}
|
||||
@@ -112,6 +112,7 @@ fn tree_continues_when_a_publication_point_fails() {
|
||||
objects: ObjectsOutput {
|
||||
vrps: Vec::new(),
|
||||
aspas: Vec::new(),
|
||||
router_keys: Vec::new(),
|
||||
local_outputs_cache: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
stats: ObjectsStats::default(),
|
||||
@@ -137,6 +138,7 @@ fn tree_continues_when_a_publication_point_fails() {
|
||||
objects: ObjectsOutput {
|
||||
vrps: Vec::new(),
|
||||
aspas: Vec::new(),
|
||||
router_keys: Vec::new(),
|
||||
local_outputs_cache: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
stats: ObjectsStats::default(),
|
||||
|
||||
@@ -4,7 +4,7 @@ use rpki::audit::{DiscoveredFrom, PublicationPointAudit};
|
||||
use rpki::report::Warning;
|
||||
use rpki::storage::{PackFile, PackTime};
|
||||
use rpki::validation::manifest::PublicationPointSource;
|
||||
use rpki::validation::objects::{ObjectsOutput, ObjectsStats};
|
||||
use rpki::validation::objects::{ObjectsOutput, ObjectsStats, RouterKeyPayload};
|
||||
use rpki::validation::publication_point::PublicationPointSnapshot;
|
||||
use rpki::validation::tree::{
|
||||
CaInstanceHandle, DiscoveredChildCaInstance, PublicationPointRunResult, PublicationPointRunner,
|
||||
@@ -122,6 +122,7 @@ fn tree_enqueues_children_for_fresh_and_current_instance_vcir_results() {
|
||||
objects: ObjectsOutput {
|
||||
vrps: Vec::new(),
|
||||
aspas: Vec::new(),
|
||||
router_keys: Vec::new(),
|
||||
local_outputs_cache: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
stats: ObjectsStats::default(),
|
||||
@@ -143,6 +144,7 @@ fn tree_enqueues_children_for_fresh_and_current_instance_vcir_results() {
|
||||
objects: ObjectsOutput {
|
||||
vrps: Vec::new(),
|
||||
aspas: Vec::new(),
|
||||
router_keys: Vec::new(),
|
||||
local_outputs_cache: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
stats: ObjectsStats::default(),
|
||||
@@ -164,6 +166,7 @@ fn tree_enqueues_children_for_fresh_and_current_instance_vcir_results() {
|
||||
objects: ObjectsOutput {
|
||||
vrps: Vec::new(),
|
||||
aspas: Vec::new(),
|
||||
router_keys: Vec::new(),
|
||||
local_outputs_cache: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
stats: ObjectsStats::default(),
|
||||
@@ -185,6 +188,7 @@ fn tree_enqueues_children_for_fresh_and_current_instance_vcir_results() {
|
||||
objects: ObjectsOutput {
|
||||
vrps: Vec::new(),
|
||||
aspas: Vec::new(),
|
||||
router_keys: Vec::new(),
|
||||
local_outputs_cache: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
stats: ObjectsStats::default(),
|
||||
@@ -243,6 +247,7 @@ fn tree_respects_max_depth_and_max_instances() {
|
||||
objects: ObjectsOutput {
|
||||
vrps: Vec::new(),
|
||||
aspas: Vec::new(),
|
||||
router_keys: Vec::new(),
|
||||
local_outputs_cache: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
stats: ObjectsStats::default(),
|
||||
@@ -264,6 +269,7 @@ fn tree_respects_max_depth_and_max_instances() {
|
||||
objects: ObjectsOutput {
|
||||
vrps: Vec::new(),
|
||||
aspas: Vec::new(),
|
||||
router_keys: Vec::new(),
|
||||
local_outputs_cache: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
stats: ObjectsStats::default(),
|
||||
@@ -314,6 +320,7 @@ fn tree_audit_includes_parent_and_discovered_from_for_non_root_nodes() {
|
||||
objects: ObjectsOutput {
|
||||
vrps: Vec::new(),
|
||||
aspas: Vec::new(),
|
||||
router_keys: Vec::new(),
|
||||
local_outputs_cache: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
stats: ObjectsStats::default(),
|
||||
@@ -335,6 +342,7 @@ fn tree_audit_includes_parent_and_discovered_from_for_non_root_nodes() {
|
||||
objects: ObjectsOutput {
|
||||
vrps: Vec::new(),
|
||||
aspas: Vec::new(),
|
||||
router_keys: Vec::new(),
|
||||
local_outputs_cache: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
stats: ObjectsStats::default(),
|
||||
@@ -366,6 +374,57 @@ fn tree_audit_includes_parent_and_discovered_from_for_non_root_nodes() {
|
||||
assert_eq!(df.parent_manifest_rsync_uri, root_manifest);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tree_aggregates_router_keys_from_publication_point_results() {
|
||||
let root_manifest = "rsync://example.test/repo/root.mft";
|
||||
|
||||
let runner = MockRunner::default().with(
|
||||
root_manifest,
|
||||
PublicationPointRunResult {
|
||||
source: PublicationPointSource::Fresh,
|
||||
snapshot: Some(empty_snapshot(root_manifest, "rsync://example.test/repo/")),
|
||||
warnings: Vec::new(),
|
||||
objects: ObjectsOutput {
|
||||
vrps: Vec::new(),
|
||||
aspas: Vec::new(),
|
||||
router_keys: vec![
|
||||
RouterKeyPayload {
|
||||
as_id: 64496,
|
||||
ski: vec![0x11; 20],
|
||||
spki_der: vec![0x30, 0x00],
|
||||
source_object_uri: "rsync://example.test/repo/router1.cer".to_string(),
|
||||
source_object_hash: "11".repeat(32),
|
||||
source_ee_cert_hash: "11".repeat(32),
|
||||
item_effective_until: PackTime { rfc3339_utc: "2026-12-31T00:00:00Z".to_string() },
|
||||
},
|
||||
RouterKeyPayload {
|
||||
as_id: 64497,
|
||||
ski: vec![0x22; 20],
|
||||
spki_der: vec![0x30, 0x01],
|
||||
source_object_uri: "rsync://example.test/repo/router2.cer".to_string(),
|
||||
source_object_hash: "22".repeat(32),
|
||||
source_ee_cert_hash: "22".repeat(32),
|
||||
item_effective_until: PackTime { rfc3339_utc: "2026-12-31T00:00:00Z".to_string() },
|
||||
},
|
||||
],
|
||||
local_outputs_cache: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
stats: ObjectsStats::default(),
|
||||
audit: Vec::new(),
|
||||
},
|
||||
audit: PublicationPointAudit::default(),
|
||||
discovered_children: Vec::new(),
|
||||
},
|
||||
);
|
||||
|
||||
let out = run_tree_serial(ca_handle(root_manifest), &runner, &TreeRunConfig::default())
|
||||
.expect("run tree");
|
||||
|
||||
assert_eq!(out.router_keys.len(), 2);
|
||||
assert_eq!(out.router_keys[0].as_id, 64496);
|
||||
assert_eq!(out.router_keys[1].as_id, 64497);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tree_prefers_lexicographically_first_discovery_when_duplicate_manifest_is_queued() {
|
||||
let root_manifest = "rsync://example.test/repo/root.mft";
|
||||
@@ -388,6 +447,7 @@ fn tree_prefers_lexicographically_first_discovery_when_duplicate_manifest_is_que
|
||||
objects: ObjectsOutput {
|
||||
vrps: Vec::new(),
|
||||
aspas: Vec::new(),
|
||||
router_keys: Vec::new(),
|
||||
local_outputs_cache: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
stats: ObjectsStats::default(),
|
||||
@@ -409,6 +469,7 @@ fn tree_prefers_lexicographically_first_discovery_when_duplicate_manifest_is_que
|
||||
objects: ObjectsOutput {
|
||||
vrps: Vec::new(),
|
||||
aspas: Vec::new(),
|
||||
router_keys: Vec::new(),
|
||||
local_outputs_cache: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
stats: ObjectsStats::default(),
|
||||
|
||||
Reference in New Issue
Block a user