rpki/tests/test_ccr_m1.rs

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}");
}