20260324_2 增加ccr & router key support

This commit is contained in:
2026-03-26 11:52:06 +08:00
parent d6d44669b4
commit fe8b89d829
33 changed files with 5337 additions and 27 deletions

View File

@@ -891,6 +891,7 @@ mod tests {
summary: VcirSummary {
local_vrp_count: 0,
local_aspa_count: 0,
local_router_key_count: 0,
child_count: 0,
accepted_object_count: 2,
rejected_object_count: 0,

View File

@@ -65,10 +65,22 @@ pub struct AspaAttestation {
pub provider_as_ids: Vec<u32>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RouterKeyPayload {
pub as_id: u32,
pub ski: Vec<u8>,
pub spki_der: Vec<u8>,
pub source_object_uri: String,
pub source_object_hash: String,
pub source_ee_cert_hash: String,
pub item_effective_until: PackTime,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ObjectsOutput {
pub vrps: Vec<Vrp>,
pub aspas: Vec<AspaAttestation>,
pub router_keys: Vec<RouterKeyPayload>,
pub local_outputs_cache: Vec<VcirLocalOutput>,
pub warnings: Vec<Warning>,
pub stats: ObjectsStats,
@@ -151,6 +163,7 @@ pub fn process_publication_point_for_issuer<P: PublicationPointData>(
return ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
router_keys: Vec::new(),
local_outputs_cache: Vec::new(),
warnings,
stats,
@@ -175,6 +188,7 @@ pub fn process_publication_point_for_issuer<P: PublicationPointData>(
return ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
router_keys: Vec::new(),
local_outputs_cache: Vec::new(),
warnings,
stats,
@@ -193,6 +207,7 @@ pub fn process_publication_point_for_issuer<P: PublicationPointData>(
return ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
router_keys: Vec::new(),
local_outputs_cache: Vec::new(),
warnings,
stats,
@@ -252,6 +267,7 @@ pub fn process_publication_point_for_issuer<P: PublicationPointData>(
return ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
router_keys: Vec::new(),
local_outputs_cache: Vec::new(),
warnings,
stats,
@@ -356,6 +372,7 @@ pub fn process_publication_point_for_issuer<P: PublicationPointData>(
return ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
router_keys: Vec::new(),
local_outputs_cache: Vec::new(),
warnings,
stats,
@@ -456,6 +473,7 @@ pub fn process_publication_point_for_issuer<P: PublicationPointData>(
return ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
router_keys: Vec::new(),
local_outputs_cache: Vec::new(),
warnings,
stats,
@@ -470,6 +488,7 @@ pub fn process_publication_point_for_issuer<P: PublicationPointData>(
ObjectsOutput {
vrps,
aspas,
router_keys: Vec::new(),
local_outputs_cache,
warnings,
stats,

View File

@@ -3,7 +3,7 @@ use crate::audit::PublicationPointAudit;
use crate::data_model::rc::{AsResourceSet, IpResourceSet};
use crate::report::Warning;
use crate::validation::manifest::PublicationPointSource;
use crate::validation::objects::{AspaAttestation, ObjectsOutput, Vrp};
use crate::validation::objects::{AspaAttestation, ObjectsOutput, RouterKeyPayload, Vrp};
use crate::validation::publication_point::PublicationPointSnapshot;
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -81,6 +81,7 @@ pub struct TreeRunOutput {
pub warnings: Vec<Warning>,
pub vrps: Vec<Vrp>,
pub aspas: Vec<AspaAttestation>,
pub router_keys: Vec<RouterKeyPayload>,
}
#[derive(Debug, thiserror::Error)]
@@ -140,6 +141,7 @@ pub fn run_tree_serial_audit(
let mut warnings: Vec<Warning> = Vec::new();
let mut vrps: Vec<Vrp> = Vec::new();
let mut aspas: Vec<AspaAttestation> = Vec::new();
let mut router_keys: Vec<RouterKeyPayload> = Vec::new();
let mut publication_points: Vec<PublicationPointAudit> = Vec::new();
while let Some(node) = queue.pop_front() {
@@ -177,6 +179,7 @@ pub fn run_tree_serial_audit(
warnings.extend(res.objects.warnings.clone());
vrps.extend(res.objects.vrps.clone());
aspas.extend(res.objects.aspas.clone());
router_keys.extend(res.objects.router_keys.clone());
let mut audit = res.audit.clone();
audit.node_id = Some(node.id);
@@ -213,6 +216,7 @@ pub fn run_tree_serial_audit(
warnings,
vrps,
aspas,
router_keys,
},
publication_points,
})

View File

@@ -9,6 +9,10 @@ use crate::data_model::crl::RpkixCrl;
use crate::data_model::manifest::ManifestObject;
use crate::data_model::rc::ResourceCertificate;
use crate::data_model::roa::{RoaAfi, RoaObject};
use crate::data_model::router_cert::{
BgpsecRouterCertificate, BgpsecRouterCertificateDecodeError,
BgpsecRouterCertificatePathError, BgpsecRouterCertificateProfileError,
};
use crate::fetch::rsync::RsyncFetcher;
use crate::policy::Policy;
use crate::replay::archive::ReplayArchiveIndex;
@@ -33,7 +37,7 @@ use crate::validation::manifest::{
ManifestFreshError, PublicationPointData, PublicationPointSource,
process_manifest_publication_point_fresh_after_repo_sync,
};
use crate::validation::objects::{AspaAttestation, Vrp, process_publication_point_for_issuer};
use crate::validation::objects::{AspaAttestation, RouterKeyPayload, Vrp, process_publication_point_for_issuer};
use crate::validation::publication_point::PublicationPointSnapshot;
use crate::validation::tree::{
CaInstanceHandle, DiscoveredChildCaInstance, PublicationPointRunResult, PublicationPointRunner,
@@ -42,6 +46,7 @@ use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
use serde::Deserialize;
use base64::Engine as _;
use serde_json::json;
use x509_parser::prelude::FromDer;
use x509_parser::x509::SubjectPublicKeyInfo;
@@ -266,7 +271,7 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> {
match fresh_publication_point {
Ok(fresh_point) => {
let objects = {
let mut objects = {
let _objects_total = self
.timing
.as_ref()
@@ -295,18 +300,23 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> {
self.timing.as_ref(),
)
};
let (discovered_children, child_audits) = match out {
Ok(out) => (out.children, out.audits),
let (discovered_children, child_audits, discovered_router_keys) = match out {
Ok(out) => (out.children, out.audits, out.router_keys),
Err(e) => {
warnings.push(
Warning::new(format!("child CA discovery failed: {e}"))
.with_rfc_refs(&[RfcRef("RFC 6487 §7.2")])
.with_context(&ca.manifest_rsync_uri),
);
(Vec::new(), Vec::new())
(Vec::new(), Vec::new(), Vec::new())
}
};
objects.router_keys.extend(discovered_router_keys);
objects
.local_outputs_cache
.extend(build_router_key_local_outputs(ca, &objects.router_keys));
let pack = fresh_point.to_publication_point_snapshot();
persist_vcir_for_fresh_result(
@@ -384,6 +394,7 @@ fn normalize_rsync_base_uri(s: &str) -> String {
struct ChildDiscoveryOutput {
children: Vec<DiscoveredChildCaInstance>,
audits: Vec<ObjectAuditEntry>,
router_keys: Vec<RouterKeyPayload>,
}
#[derive(Clone, Debug)]
@@ -421,6 +432,13 @@ struct VcirAspaPayload {
provider_as_ids: Vec<u32>,
}
#[derive(Debug, Deserialize)]
struct VcirRouterKeyPayload {
as_id: u32,
ski_hex: String,
spki_der_base64: String,
}
fn discover_children_from_fresh_snapshot_with_audit<P: PublicationPointData>(
issuer: &CaInstanceHandle,
publication_point: &P,
@@ -484,6 +502,7 @@ fn discover_children_from_fresh_snapshot_with_audit<P: PublicationPointData>(
let mut out: Vec<DiscoveredChildCaInstance> = Vec::new();
let mut audits: Vec<ObjectAuditEntry> = Vec::new();
let mut router_keys: Vec<RouterKeyPayload> = Vec::new();
let issuer_resources_index = IssuerEffectiveResourcesIndex::from_effective_resources(
issuer.effective_ip_resources.as_ref(),
issuer.effective_as_resources.as_ref(),
@@ -494,12 +513,16 @@ fn discover_children_from_fresh_snapshot_with_audit<P: PublicationPointData>(
let mut ca_skipped_not_ca: u64 = 0;
let mut ca_ok: u64 = 0;
let mut ca_error: u64 = 0;
let mut router_ok: u64 = 0;
let mut router_error: u64 = 0;
let mut router_skipped_non_router: u64 = 0;
let mut crl_select_error: u64 = 0;
let mut uri_discovery_error: u64 = 0;
let mut select_crl_nanos: u64 = 0;
let mut child_decode_nanos: u64 = 0;
let mut validate_sub_ca_nanos: u64 = 0;
let mut validate_router_nanos: u64 = 0;
let mut uri_discovery_nanos: u64 = 0;
let mut enqueue_nanos: u64 = 0;
@@ -639,14 +662,100 @@ fn discover_children_from_fresh_snapshot_with_audit<P: PublicationPointData>(
) {
Ok(v) => v,
Err(CaPathError::ChildNotCa) => {
ca_skipped_not_ca = ca_skipped_not_ca.saturating_add(1);
audits.push(ObjectAuditEntry {
rsync_uri: f.rsync_uri.clone(),
sha256_hex: sha256_hex_from_32(&f.sha256),
kind: AuditObjectKind::Certificate,
result: AuditObjectResult::Skipped,
detail: Some("skipped: not a CA resource certificate".to_string()),
});
let tr = std::time::Instant::now();
let router_result = match ensure_issuer_crl_verified(
issuer_crl_uri.as_str(),
&mut crl_cache,
issuer_ca_der,
) {
Ok(verified_crl) => {
BgpsecRouterCertificate::validate_path_with_prevalidated_issuer(
child_der,
issuer_ca_ref,
issuer_spki_ref,
&verified_crl.crl,
&verified_crl.revoked_serials,
issuer.ca_certificate_rsync_uri.as_deref(),
Some(issuer_crl_uri.as_str()),
validation_time,
)
}
Err(err) => {
validate_router_nanos = validate_router_nanos.saturating_add(
tr.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64,
);
router_error = router_error.saturating_add(1);
audits.push(ObjectAuditEntry {
rsync_uri: f.rsync_uri.clone(),
sha256_hex: sha256_hex_from_32(&f.sha256),
kind: AuditObjectKind::RouterCertificate,
result: AuditObjectResult::Error,
detail: Some(format!(
"router certificate issuer CRL validation failed: {err}"
)),
});
continue;
}
};
validate_router_nanos = validate_router_nanos
.saturating_add(tr.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64);
match router_result {
Ok(router) => {
router_ok = router_ok.saturating_add(1);
let source_object_hash = sha256_hex_from_32(&f.sha256);
let item_effective_until = PackTime::from_utc_offset_datetime(
router.resource_cert.tbs.validity_not_after,
);
for as_id in &router.asns {
router_keys.push(RouterKeyPayload {
as_id: *as_id,
ski: router.subject_key_identifier.clone(),
spki_der: router.spki_der.clone(),
source_object_uri: f.rsync_uri.clone(),
source_object_hash: source_object_hash.clone(),
source_ee_cert_hash: source_object_hash.clone(),
item_effective_until: item_effective_until.clone(),
});
}
audits.push(ObjectAuditEntry {
rsync_uri: f.rsync_uri.clone(),
sha256_hex: sha256_hex_from_32(&f.sha256),
kind: AuditObjectKind::RouterCertificate,
result: AuditObjectResult::Ok,
detail: Some(
"validated BGPsec router certificate (RFC 8209); no child CA instance enqueued"
.to_string(),
),
});
}
Err(err) if is_non_router_certificate(&err) => {
ca_skipped_not_ca = ca_skipped_not_ca.saturating_add(1);
router_skipped_non_router = router_skipped_non_router.saturating_add(1);
audits.push(ObjectAuditEntry {
rsync_uri: f.rsync_uri.clone(),
sha256_hex: sha256_hex_from_32(&f.sha256),
kind: AuditObjectKind::Certificate,
result: AuditObjectResult::Skipped,
detail: Some(
"skipped: not a CA resource certificate or BGPsec router certificate"
.to_string(),
),
});
}
Err(err) => {
router_error = router_error.saturating_add(1);
audits.push(ObjectAuditEntry {
rsync_uri: f.rsync_uri.clone(),
sha256_hex: sha256_hex_from_32(&f.sha256),
kind: AuditObjectKind::RouterCertificate,
result: AuditObjectResult::Error,
detail: Some(format!(
"router certificate validation failed: {err}"
)),
});
}
}
continue;
}
Err(e) => {
@@ -734,6 +843,9 @@ fn discover_children_from_fresh_snapshot_with_audit<P: PublicationPointData>(
t.record_count("child_ca_ok", ca_ok);
t.record_count("child_ca_error", ca_error);
t.record_count("child_ca_skipped_not_ca", ca_skipped_not_ca);
t.record_count("child_router_ok", router_ok);
t.record_count("child_router_error", router_error);
t.record_count("child_router_skipped_non_router", router_skipped_non_router);
t.record_count("child_crl_select_error", crl_select_error);
t.record_count("child_uri_discovery_error", uri_discovery_error);
@@ -759,6 +871,7 @@ fn discover_children_from_fresh_snapshot_with_audit<P: PublicationPointData>(
t.record_phase_nanos("child_select_issuer_crl_total", select_crl_nanos);
t.record_phase_nanos("child_decode_certificate_total", child_decode_nanos);
t.record_phase_nanos("child_validate_subordinate_total", validate_sub_ca_nanos);
t.record_phase_nanos("child_validate_router_certificate_total", validate_router_nanos);
t.record_phase_nanos("child_ca_instance_uri_discovery_total", uri_discovery_nanos);
t.record_phase_nanos("child_enqueue_total", enqueue_nanos);
}
@@ -766,9 +879,21 @@ fn discover_children_from_fresh_snapshot_with_audit<P: PublicationPointData>(
Ok(ChildDiscoveryOutput {
children: out,
audits,
router_keys,
})
}
fn is_non_router_certificate(err: &BgpsecRouterCertificatePathError) -> bool {
matches!(
err,
BgpsecRouterCertificatePathError::Decode(BgpsecRouterCertificateDecodeError::Validate(
BgpsecRouterCertificateProfileError::NotEe
| BgpsecRouterCertificateProfileError::MissingExtendedKeyUsage
| BgpsecRouterCertificateProfileError::MissingBgpsecRouterEku
))
)
}
fn select_issuer_crl_uri_for_child<'a>(
child: &'a crate::data_model::rc::ResourceCertificate,
crl_cache: &std::collections::HashMap<String, CachedIssuerCrl>,
@@ -1181,6 +1306,7 @@ fn empty_objects_output() -> crate::validation::objects::ObjectsOutput {
crate::validation::objects::ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
router_keys: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: Vec::new(),
stats: crate::validation::objects::ObjectsStats::default(),
@@ -1367,6 +1493,14 @@ fn reconstruct_snapshot_from_vcir(
})
}
fn audit_kind_for_vcir_output_type(output_type: VcirOutputType) -> AuditObjectKind {
match output_type {
VcirOutputType::Vrp => AuditObjectKind::Roa,
VcirOutputType::Aspa => AuditObjectKind::Aspa,
VcirOutputType::RouterKey => AuditObjectKind::RouterCertificate,
}
}
fn build_objects_output_from_vcir(
vcir: &ValidatedCaInstanceResult,
validation_time: time::OffsetDateTime,
@@ -1411,11 +1545,7 @@ fn build_objects_output_from_vcir(
ObjectAuditEntry {
rsync_uri: local.source_object_uri.clone(),
sha256_hex: local.source_object_hash.clone(),
kind: if local.output_type == VcirOutputType::Vrp {
AuditObjectKind::Roa
} else {
AuditObjectKind::Aspa
},
kind: audit_kind_for_vcir_output_type(local.output_type),
result: AuditObjectResult::Error,
detail: Some(
"cached local output has invalid item_effective_until".to_string(),
@@ -1431,11 +1561,7 @@ fn build_objects_output_from_vcir(
.or_insert_with(|| ObjectAuditEntry {
rsync_uri: local.source_object_uri.clone(),
sha256_hex: local.source_object_hash.clone(),
kind: if local.output_type == VcirOutputType::Vrp {
AuditObjectKind::Roa
} else {
AuditObjectKind::Aspa
},
kind: audit_kind_for_vcir_output_type(local.output_type),
result: AuditObjectResult::Skipped,
detail: Some("skipped: cached local output expired".to_string()),
});
@@ -1507,6 +1633,37 @@ fn build_objects_output_from_vcir(
);
}
},
VcirOutputType::RouterKey => match parse_vcir_router_key_output(local) {
Ok(router_key) => {
output.router_keys.push(router_key);
audit_by_uri.insert(
local.source_object_uri.clone(),
ObjectAuditEntry {
rsync_uri: local.source_object_uri.clone(),
sha256_hex: local.source_object_hash.clone(),
kind: AuditObjectKind::RouterCertificate,
result: AuditObjectResult::Ok,
detail: Some("cached Router Key local output restored".to_string()),
},
);
}
Err(e) => {
warnings.push(
Warning::new(format!("cached Router Key local output parse failed: {e}"))
.with_context(&local.source_object_uri),
);
audit_by_uri.insert(
local.source_object_uri.clone(),
ObjectAuditEntry {
rsync_uri: local.source_object_uri.clone(),
sha256_hex: local.source_object_hash.clone(),
kind: AuditObjectKind::RouterCertificate,
result: AuditObjectResult::Error,
detail: Some(format!("cached Router Key local output parse failed: {e}")),
},
);
}
},
}
}
@@ -1539,6 +1696,24 @@ fn parse_vcir_aspa_output(local: &VcirLocalOutput) -> Result<AspaAttestation, St
})
}
fn parse_vcir_router_key_output(local: &VcirLocalOutput) -> Result<RouterKeyPayload, String> {
let payload: VcirRouterKeyPayload = serde_json::from_str(&local.payload_json)
.map_err(|e| format!("invalid Router Key payload JSON: {e}"))?;
let ski = hex::decode(&payload.ski_hex)
.map_err(|e| format!("invalid Router Key SKI hex: {e}"))?;
let spki_der = base64::engine::general_purpose::STANDARD.decode(&payload.spki_der_base64)
.map_err(|e| format!("invalid Router Key SPKI base64: {e}"))?;
Ok(RouterKeyPayload {
as_id: payload.as_id,
ski,
spki_der,
source_object_uri: local.source_object_uri.clone(),
source_object_hash: local.source_object_hash.clone(),
source_ee_cert_hash: local.source_ee_cert_hash.clone(),
item_effective_until: local.item_effective_until.clone(),
})
}
fn parse_vcir_prefix(prefix: &str) -> Result<crate::data_model::roa::IpPrefix, String> {
let (addr, len) = prefix
.split_once('/')
@@ -1775,6 +1950,10 @@ fn build_vcir_from_fresh_result(
.iter()
.filter(|output| output.output_type == VcirOutputType::Aspa)
.count() as u32,
local_router_key_count: local_outputs
.iter()
.filter(|output| output.output_type == VcirOutputType::RouterKey)
.count() as u32,
child_count: discovered_children.len() as u32,
accepted_object_count,
rejected_object_count,
@@ -1937,6 +2116,47 @@ fn build_vcir_local_outputs(
Ok(out)
}
fn build_router_key_local_outputs(
ca: &CaInstanceHandle,
router_keys: &[RouterKeyPayload],
) -> Vec<VcirLocalOutput> {
router_keys
.iter()
.map(|router_key| {
let ski_hex = hex::encode(&router_key.ski);
let spki_der_base64 = base64::engine::general_purpose::STANDARD.encode(&router_key.spki_der);
let rule_hash = sha256_hex(
format!(
"router-key-rule:{}:{}:{}:{}",
router_key.source_object_hash, router_key.as_id, ski_hex, spki_der_base64
)
.as_bytes(),
);
VcirLocalOutput {
output_id: rule_hash.clone(),
output_type: VcirOutputType::RouterKey,
item_effective_until: router_key.item_effective_until.clone(),
source_object_uri: router_key.source_object_uri.clone(),
source_object_type: "router_key".to_string(),
source_object_hash: router_key.source_object_hash.clone(),
source_ee_cert_hash: router_key.source_ee_cert_hash.clone(),
payload_json: json!({
"as_id": router_key.as_id,
"ski_hex": ski_hex,
"spki_der_base64": spki_der_base64,
})
.to_string(),
rule_hash,
validation_path_hint: vec![
ca.manifest_rsync_uri.clone(),
router_key.source_object_uri.clone(),
router_key.source_object_hash.clone(),
],
}
})
.collect()
}
fn build_vcir_child_entries(
discovered_children: &[DiscoveredChildCaInstance],
validation_time: time::OffsetDateTime,
@@ -2494,6 +2714,191 @@ authorityKeyIdentifier = keyid:always
}
}
struct GeneratedRouter {
issuer_ca_der: Vec<u8>,
router_der: Vec<u8>,
issuer_crl_der: Vec<u8>,
}
fn generate_router_cert_with_variant(key_spec: &str, include_eku: bool) -> GeneratedRouter {
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 eku_line = if include_eku {
"extendedKeyUsage = 1.3.6.1.5.5.7.3.30"
} else {
""
};
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
[ crl_ext ]
authorityKeyIdentifier = keyid:always
"#,
dir = dir.display(),
eku_line = eku_line,
);
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"))),
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")));
GeneratedRouter {
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"),
}
}
fn dummy_pack_with_files(files: Vec<PackFile>) -> PublicationPointSnapshot {
let now = time::OffsetDateTime::now_utc();
PublicationPointSnapshot {
@@ -2577,10 +2982,12 @@ authorityKeyIdentifier = keyid:always
let child_manifest_uri = "rsync://example.test/repo/child/child.mft".to_string();
let roa_uri = "rsync://example.test/repo/issuer/a.roa".to_string();
let aspa_uri = "rsync://example.test/repo/issuer/a.asa".to_string();
let router_uri = "rsync://example.test/repo/issuer/router.cer".to_string();
let manifest_hash = sha256_hex(b"manifest-bytes");
let current_crl_hash = sha256_hex(b"current-crl-bytes");
let roa_hash = sha256_hex(b"roa-bytes");
let aspa_hash = sha256_hex(b"aspa-bytes");
let router_hash = sha256_hex(b"router-bytes");
let ee_hash = sha256_hex(b"ee-cert-bytes");
let gate_until = PackTime::from_utc_offset_datetime(now + time::Duration::hours(1));
ValidatedCaInstanceResult {
@@ -2641,6 +3048,22 @@ authorityKeyIdentifier = keyid:always
rule_hash: sha256_hex(b"aspa-rule"),
validation_path_hint: vec![manifest_uri.clone(), aspa_uri.clone()],
},
VcirLocalOutput {
output_id: sha256_hex(b"router-key-out"),
output_type: VcirOutputType::RouterKey,
item_effective_until: PackTime::from_utc_offset_datetime(now + time::Duration::minutes(30)),
source_object_uri: router_uri.clone(),
source_object_type: "router_key".to_string(),
source_object_hash: router_hash.clone(),
source_ee_cert_hash: router_hash.clone(),
payload_json: serde_json::json!({
"as_id": 64496,
"ski_hex": "11".repeat(20),
"spki_der_base64": base64::engine::general_purpose::STANDARD.encode([0x30u8, 0x00]),
}).to_string(),
rule_hash: sha256_hex(b"router-key-rule"),
validation_path_hint: vec![manifest_uri.clone(), router_uri.clone()],
},
],
related_artifacts: vec![
VcirRelatedArtifact {
@@ -2687,6 +3110,7 @@ authorityKeyIdentifier = keyid:always
summary: VcirSummary {
local_vrp_count: 1,
local_aspa_count: 1,
local_router_key_count: 1,
child_count: 1,
accepted_object_count: 4,
rejected_object_count: 0,
@@ -2765,6 +3189,7 @@ authorityKeyIdentifier = keyid:always
&crate::validation::objects::ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
router_keys: Vec::new(),
local_outputs_cache: cached.clone(),
warnings: Vec::new(),
stats: crate::validation::objects::ObjectsStats::default(),
@@ -2831,6 +3256,39 @@ authorityKeyIdentifier = keyid:always
}
}
#[test]
fn build_router_key_local_outputs_encodes_router_key_payloads() {
let ca = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate_der: Vec::new(),
ca_certificate_rsync_uri: None,
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: None,
};
let outputs = build_router_key_local_outputs(
&ca,
&[RouterKeyPayload {
as_id: 64496,
ski: vec![0x11; 20],
spki_der: vec![0x30, 0x00],
source_object_uri: "rsync://example.test/repo/issuer/router.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() },
}],
);
assert_eq!(outputs.len(), 1);
assert_eq!(outputs[0].output_type, VcirOutputType::RouterKey);
assert_eq!(outputs[0].source_object_type, "router_key");
assert!(outputs[0].payload_json.contains("spki_der_base64"));
}
#[test]
fn build_vcir_local_outputs_falls_back_to_decoding_accepted_objects_when_cache_is_empty() {
let (pack, issuer_ca_der, validation_time) = cernet_publication_point_snapshot_for_vcir_tests();
@@ -2996,6 +3454,7 @@ authorityKeyIdentifier = keyid:always
let objects = crate::validation::objects::ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
router_keys: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: Vec::new(),
stats: crate::validation::objects::ObjectsStats::default(),
@@ -3566,6 +4025,7 @@ authorityKeyIdentifier = keyid:always
let objects = crate::validation::objects::ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
router_keys: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: Vec::new(),
stats: crate::validation::objects::ObjectsStats::default(),
@@ -3627,6 +4087,7 @@ authorityKeyIdentifier = keyid:always
let objects = crate::validation::objects::ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
router_keys: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: Vec::new(),
stats: crate::validation::objects::ObjectsStats::default(),
@@ -3670,6 +4131,145 @@ authorityKeyIdentifier = keyid:always
let _ = now;
}
#[test]
fn discover_children_with_router_certificate_records_ok_audit_and_no_child() {
let g = generate_router_cert_with_variant("ec-p256", true);
let pack = dummy_pack_with_files(vec![
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/issuer.crl",
g.issuer_crl_der.clone(),
),
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/router.cer",
g.router_der.clone(),
),
]);
let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer");
let issuer = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate_der: g.issuer_ca_der.clone(),
ca_certificate_rsync_uri: Some("rsync://example.test/repo/issuer/issuer.cer".to_string()),
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: None,
};
let out = discover_children_from_fresh_snapshot_with_audit(
&issuer,
&pack,
time::OffsetDateTime::now_utc(),
None,
)
.expect("discover router cert");
assert!(out.children.is_empty());
assert_eq!(out.audits.len(), 1);
assert!(matches!(out.audits[0].result, AuditObjectResult::Ok));
assert!(out.audits[0]
.detail
.as_deref()
.unwrap_or("")
.contains("validated BGPsec router certificate"));
}
#[test]
fn discover_children_with_non_router_ee_certificate_records_skipped_audit() {
let g = generate_router_cert_with_variant("ec-p256", false);
let pack = dummy_pack_with_files(vec![
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/issuer.crl",
g.issuer_crl_der.clone(),
),
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/router-no-eku.cer",
g.router_der.clone(),
),
]);
let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer");
let issuer = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate_der: g.issuer_ca_der.clone(),
ca_certificate_rsync_uri: Some("rsync://example.test/repo/issuer/issuer.cer".to_string()),
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: None,
};
let out = discover_children_from_fresh_snapshot_with_audit(
&issuer,
&pack,
time::OffsetDateTime::now_utc(),
None,
)
.expect("discover non-router cert");
assert!(out.children.is_empty());
assert_eq!(out.audits.len(), 1);
assert!(matches!(out.audits[0].result, AuditObjectResult::Skipped));
assert!(out.audits[0]
.detail
.as_deref()
.unwrap_or("")
.contains("not a CA resource certificate or BGPsec router certificate"));
}
#[test]
fn discover_children_with_invalid_router_certificate_records_error_audit() {
let g = generate_router_cert_with_variant("ec-p384", true);
let pack = dummy_pack_with_files(vec![
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/issuer.crl",
g.issuer_crl_der.clone(),
),
PackFile::from_bytes_compute_sha256(
"rsync://example.test/repo/issuer/router-invalid.cer",
g.router_der.clone(),
),
]);
let issuer_ca = ResourceCertificate::decode_der(&g.issuer_ca_der).expect("decode issuer");
let issuer = CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate_der: g.issuer_ca_der.clone(),
ca_certificate_rsync_uri: Some("rsync://example.test/repo/issuer/issuer.cer".to_string()),
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: "rsync://example.test/repo/issuer/issuer.mft".to_string(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: None,
};
let out = discover_children_from_fresh_snapshot_with_audit(
&issuer,
&pack,
time::OffsetDateTime::now_utc(),
None,
)
.expect("discover invalid router cert");
assert!(out.children.is_empty());
assert_eq!(out.audits.len(), 1);
assert!(matches!(out.audits[0].result, AuditObjectResult::Error));
assert!(out.audits[0]
.detail
.as_deref()
.unwrap_or("")
.contains("router certificate validation failed"));
}
#[test]
fn discover_children_with_audit_records_decode_error_for_corrupt_cer() {
let g = generate_chain_and_crl();
@@ -3889,6 +4489,7 @@ authorityKeyIdentifier = keyid:always
);
assert_eq!(projection.objects.vrps.len(), 1);
assert_eq!(projection.objects.aspas.len(), 1);
assert_eq!(projection.objects.router_keys.len(), 1);
assert_eq!(projection.discovered_children.len(), 1);
assert_eq!(
projection.discovered_children[0].handle.manifest_rsync_uri,
@@ -4233,6 +4834,7 @@ authorityKeyIdentifier = keyid:always
let objects = crate::validation::objects::ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
router_keys: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: vec![Warning::new("objects warning")],
stats: crate::validation::objects::ObjectsStats::default(),
@@ -4318,6 +4920,7 @@ authorityKeyIdentifier = keyid:always
&crate::validation::objects::ObjectsOutput {
vrps: Vec::new(),
aspas: Vec::new(),
router_keys: Vec::new(),
local_outputs_cache: Vec::new(),
warnings: vec![Warning::new("object warning")],
stats: crate::validation::objects::ObjectsStats::default(),