203 lines
8.9 KiB
Rust
203 lines
8.9 KiB
Rust
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");
|
|
}
|