338 lines
11 KiB
Rust
338 lines
11 KiB
Rust
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}");
|
|
}
|