From 51e483d924bfac0b0ea17f99f26fdb54e8d02844 Mon Sep 17 00:00:00 2001 From: yuyr Date: Mon, 11 May 2026 11:32:17 +0800 Subject: [PATCH] =?UTF-8?q?20260510=20=E5=A2=9E=E5=8A=A0strict=E7=AD=96?= =?UTF-8?q?=E7=95=A5=E6=A8=A1=E5=BC=8F=E5=B9=B6=E9=9A=94=E7=A6=BB=E5=A4=9A?= =?UTF-8?q?TA=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/cli.rs | 191 +++++++++++++++++- src/data_model/aspa.rs | 23 ++- src/data_model/manifest.rs | 23 ++- src/data_model/rc.rs | 171 +++++++++++++++- src/data_model/roa.rs | 23 ++- src/data_model/signed_object.rs | 236 +++++++++++++++++++--- src/data_model/ta.rs | 17 ++ src/policy.rs | 103 ++++++++++ src/validation/from_tal.rs | 125 ++++++++++-- src/validation/objects.rs | 77 ++++++- src/validation/run_tree_from_tal.rs | 300 ++++++++++++++++++++++++---- src/validation/tree_parallel.rs | 1 + 12 files changed, 1194 insertions(+), 96 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 9e0a012..85e69fa 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -16,7 +16,7 @@ use crate::fetch::rsync::LocalDirRsyncFetcher; use crate::fetch::rsync_system::{SystemRsyncConfig, SystemRsyncFetcher}; use crate::parallel::config::{ParallelPhase1Config, ParallelPhase2Config}; use crate::parallel::types::TalInputSpec; -use crate::policy::Policy; +use crate::policy::{Policy, StrictPolicy}; use crate::storage::RocksStore; use crate::validation::run_tree_from_tal::{ RunTreeFromTalAuditOutput, run_tree_from_multiple_tals_parallel_phase2_audit, @@ -70,6 +70,7 @@ pub struct CliArgs { pub raw_store_db: Option, pub repo_bytes_db: Option, pub policy_path: Option, + pub strict_policy: Option, pub report_json_path: Option, pub report_json_compact: bool, pub skip_report_build: bool, @@ -120,6 +121,7 @@ Options: --raw-store-db External raw-by-hash store DB path (optional) --repo-bytes-db External repo object bytes DB path (optional) --policy Policy TOML path (optional) + --strict [policies] Enable strict policies (default all; comma list: name,cms-der,signed-attrs; none disables) --report-json Write full audit report as JSON (optional) --report-json-compact Write report JSON without pretty-printing (requires --report-json) --skip-report-build Skip full audit report construction when --report-json is not requested @@ -195,6 +197,7 @@ pub fn parse_args(argv: &[String]) -> Result { let mut raw_store_db: Option = None; let mut repo_bytes_db: Option = None; let mut policy_path: Option = None; + let mut strict_policy: Option = None; let mut report_json_path: Option = None; let mut report_json_compact: bool = false; let mut skip_report_build: bool = false; @@ -372,6 +375,18 @@ pub fn parse_args(argv: &[String]) -> Result { let v = argv.get(i).ok_or("--policy requires a value")?; policy_path = Some(PathBuf::from(v)); } + "--strict" => { + let next = argv.get(i + 1).map(String::as_str); + let spec = next.filter(|v| !v.starts_with("--")); + if spec.is_some() { + i += 1; + } + strict_policy = Some(StrictPolicy::parse_cli_spec(spec)?); + } + _ if arg.starts_with("--strict=") => { + let spec = arg.strip_prefix("--strict=").expect("prefix checked"); + strict_policy = Some(StrictPolicy::parse_cli_spec(Some(spec))?); + } "--report-json" => { i += 1; let v = argv.get(i).ok_or("--report-json requires a value")?; @@ -796,6 +811,7 @@ pub fn parse_args(argv: &[String]) -> Result { raw_store_db, repo_bytes_db, policy_path, + strict_policy, report_json_path, report_json_compact, skip_report_build, @@ -940,6 +956,7 @@ fn print_summary_from_shared(validation_time: time::OffsetDateTime, shared: &Pos struct PostValidationShared { discovery: crate::validation::from_tal::DiscoveredRootCaInstance, discoveries: Arc<[crate::validation::from_tal::DiscoveredRootCaInstance]>, + successful_tal_inputs: Arc<[TalInputSpec]>, instances_processed: usize, instances_failed: usize, tree_warnings: Arc<[crate::report::Warning]>, @@ -958,6 +975,7 @@ impl PostValidationShared { let RunTreeFromTalAuditOutput { discovery, discoveries, + successful_tal_inputs, tree, publication_points, downloads, @@ -977,6 +995,7 @@ impl PostValidationShared { Self { discovery, discoveries: discoveries.into(), + successful_tal_inputs: successful_tal_inputs.into(), instances_processed, instances_failed, tree_warnings: warnings.into(), @@ -1285,6 +1304,38 @@ fn resolve_cir_export_tal_uris(args: &CliArgs) -> Result, String> { Err("CIR export requires TAL URI source(s)".to_string()) } +fn effective_cir_tal_uris_for_discoveries( + args: &CliArgs, + shared: &PostValidationShared, + cir_tal_uris: Vec, +) -> Result, String> { + if shared.successful_tal_inputs.is_empty() { + return Ok(cir_tal_uris); + } + if cir_tal_uris.len() == shared.discoveries.len() { + return Ok(cir_tal_uris); + } + if cir_tal_uris.len() != args.tal_inputs.len() { + return Ok(cir_tal_uris); + } + + let mut mapped = Vec::with_capacity(shared.successful_tal_inputs.len()); + for successful in shared.successful_tal_inputs.iter() { + let input_index = args + .tal_inputs + .iter() + .position(|candidate| candidate == successful) + .ok_or_else(|| { + format!( + "successful TAL '{}' was not found in original TAL input list", + successful.tal_id + ) + })?; + mapped.push(cir_tal_uris[input_index].clone()); + } + Ok(mapped) +} + fn build_repo_sync_stats( publication_points: &[crate::audit::PublicationPointAudit], ) -> AuditRepoSyncStats { @@ -1420,6 +1471,9 @@ pub fn run(argv: &[String]) -> Result<(), String> { let args = parse_args(argv)?; let mut policy = read_policy(args.policy_path.as_deref())?; + if let Some(strict_policy) = args.strict_policy { + policy.strict = strict_policy; + } if args.disable_rrdp { policy.sync_preference = crate::policy::SyncPreference::RsyncOnly; } @@ -1796,7 +1850,11 @@ pub fn run(argv: &[String]) -> Result<(), String> { let mut cir_write_cir_ms = None; let mut cir_total_ms = None; if args.cir_enabled { - let cir_tal_uris = resolve_cir_export_tal_uris(&args)?; + let cir_tal_uris = effective_cir_tal_uris_for_discoveries( + &args, + &shared, + resolve_cir_export_tal_uris(&args)?, + )?; if cir_tal_uris.len() != shared.discoveries.len() { return Err(format!( "CIR export TAL URI count ({}) does not match discovery count ({})", @@ -2026,6 +2084,108 @@ mod tests { assert!(args.report_json_compact); } + #[test] + fn parse_accepts_strict_policy_list() { + let argv = vec![ + "rpki".to_string(), + "--db".to_string(), + "db".to_string(), + "--tal-url".to_string(), + "https://example.test/x.tal".to_string(), + "--strict".to_string(), + "name,cms-der".to_string(), + ]; + let args = parse_args(&argv).expect("parse args"); + assert_eq!( + args.strict_policy, + Some(StrictPolicy { + name: true, + cms_der: true, + signed_attrs: false, + }) + ); + } + + #[test] + fn parse_accepts_strict_without_value_as_all() { + let argv = vec![ + "rpki".to_string(), + "--db".to_string(), + "db".to_string(), + "--tal-url".to_string(), + "https://example.test/x.tal".to_string(), + "--strict".to_string(), + "--report-json".to_string(), + "out/report.json".to_string(), + ]; + let args = parse_args(&argv).expect("parse args"); + assert_eq!(args.strict_policy, Some(StrictPolicy::all())); + } + + #[test] + fn parse_rejects_unknown_strict_policy() { + let argv = vec![ + "rpki".to_string(), + "--db".to_string(), + "db".to_string(), + "--tal-url".to_string(), + "https://example.test/x.tal".to_string(), + "--strict=unknown".to_string(), + ]; + let err = parse_args(&argv).expect_err("unknown strict policy should fail"); + assert!(err.contains("unknown strict policy"), "{err}"); + } + + #[test] + fn effective_cir_tal_uris_filters_skipped_multi_tal_inputs() { + let argv = vec![ + "rpki".to_string(), + "--db".to_string(), + "db".to_string(), + "--tal-path".to_string(), + "afrinic.tal".to_string(), + "--ta-path".to_string(), + "afrinic.cer".to_string(), + "--tal-path".to_string(), + "apnic.tal".to_string(), + "--ta-path".to_string(), + "apnic.cer".to_string(), + "--tal-path".to_string(), + "arin.tal".to_string(), + "--ta-path".to_string(), + "arin.cer".to_string(), + "--cir-enable".to_string(), + "--cir-out".to_string(), + "out.cir".to_string(), + "--cir-tal-uri".to_string(), + "https://example.test/afrinic.cer".to_string(), + "--cir-tal-uri".to_string(), + "https://example.test/apnic.cer".to_string(), + "--cir-tal-uri".to_string(), + "https://example.test/arin.cer".to_string(), + ]; + let args = parse_args(&argv).expect("parse args"); + let mut shared = synthetic_post_validation_shared(); + shared.discoveries = vec![shared.discovery.clone(), shared.discovery.clone()].into(); + shared.successful_tal_inputs = + vec![args.tal_inputs[0].clone(), args.tal_inputs[2].clone()].into(); + + let effective = effective_cir_tal_uris_for_discoveries( + &args, + &shared, + resolve_cir_export_tal_uris(&args).expect("resolve cir tal uris"), + ) + .expect("map effective cir tal uris"); + + assert_eq!( + effective, + vec![ + "https://example.test/afrinic.cer".to_string(), + "https://example.test/arin.cer".to_string(), + ] + ); + } + #[test] fn parse_rejects_report_json_compact_without_report_json() { let argv = vec![ @@ -3024,6 +3184,32 @@ mod tests { policy.signed_object_failure_policy, crate::policy::SignedObjectFailurePolicy::DropPublicationPoint ); + assert_eq!(policy.strict, StrictPolicy::default()); + } + + #[test] + fn read_policy_accepts_strict_table() { + let dir = tempfile::tempdir().expect("tmpdir"); + let p = dir.path().join("policy.toml"); + std::fs::write( + &p, + r#" + [strict] + name = true + cms_der = true + "#, + ) + .expect("write policy"); + + let policy = read_policy(Some(&p)).expect("parse policy"); + assert_eq!( + policy.strict, + StrictPolicy { + name: true, + cms_der: true, + signed_attrs: false, + } + ); } #[test] @@ -3099,6 +3285,7 @@ mod tests { let out = crate::validation::run_tree_from_tal::RunTreeFromTalAuditOutput { discovery: discovery.clone(), discoveries: vec![discovery], + successful_tal_inputs: Vec::new(), tree, publication_points: vec![pp1, pp2, pp3], downloads: Vec::new(), diff --git a/src/data_model/aspa.rs b/src/data_model/aspa.rs index 09ee077..e2dbcab 100644 --- a/src/data_model/aspa.rs +++ b/src/data_model/aspa.rs @@ -2,7 +2,8 @@ use crate::data_model::common::{DerReader, der_take_tlv}; use crate::data_model::oid::OID_CT_ASPA; use crate::data_model::rc::ResourceCertificate; use crate::data_model::signed_object::{ - RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectParseError, SignedObjectValidateError, + RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectDecodeError, SignedObjectParseError, + SignedObjectValidateError, }; #[derive(Clone, Debug, PartialEq, Eq)] @@ -91,6 +92,15 @@ pub enum AspaProfileError { ProvidersContainCustomer(u32), } +impl From for AspaProfileError { + fn from(value: SignedObjectDecodeError) -> Self { + match value { + SignedObjectDecodeError::Parse(e) => AspaProfileError::ProfileDecode(e.to_string()), + SignedObjectDecodeError::Validate(e) => AspaProfileError::SignedObject(e), + } + } +} + #[derive(Debug, thiserror::Error)] pub enum AspaDecodeError { #[error("{0}")] @@ -173,6 +183,17 @@ impl AspaObject { Ok(Self::parse_der(der)?.validate_profile()?) } + pub fn decode_der_with_strict_options( + der: &[u8], + strict_cms_der: bool, + strict_name: bool, + ) -> Result { + let signed_object = + RpkiSignedObject::decode_der_with_strict_options(der, strict_cms_der, strict_name) + .map_err(AspaProfileError::from)?; + Self::from_signed_object(signed_object) + } + pub fn from_signed_object(signed_object: RpkiSignedObject) -> Result { let econtent_type = signed_object .signed_data diff --git a/src/data_model/manifest.rs b/src/data_model/manifest.rs index e0ee4ba..bbd02db 100644 --- a/src/data_model/manifest.rs +++ b/src/data_model/manifest.rs @@ -3,7 +3,8 @@ use crate::data_model::common::{BigUnsigned, UtcTime}; use crate::data_model::oid::{OID_CT_RPKI_MANIFEST, OID_SHA256}; use crate::data_model::rc::ResourceCertificate; use crate::data_model::signed_object::{ - RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectParseError, SignedObjectValidateError, + RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectDecodeError, SignedObjectParseError, + SignedObjectValidateError, }; #[derive(Clone, Debug, PartialEq, Eq)] @@ -119,6 +120,15 @@ pub enum ManifestProfileError { InvalidHashLength(usize), } +impl From for ManifestProfileError { + fn from(value: SignedObjectDecodeError) -> Self { + match value { + SignedObjectDecodeError::Parse(e) => ManifestProfileError::ProfileDecode(e.to_string()), + SignedObjectDecodeError::Validate(e) => ManifestProfileError::SignedObject(e), + } + } +} + #[derive(Debug, thiserror::Error)] pub enum ManifestDecodeError { #[error("{0}")] @@ -186,6 +196,17 @@ impl ManifestObject { Ok(Self::parse_der(der)?.validate_profile()?) } + pub fn decode_der_with_strict_options( + der: &[u8], + strict_cms_der: bool, + strict_name: bool, + ) -> Result { + let signed_object = + RpkiSignedObject::decode_der_with_strict_options(der, strict_cms_der, strict_name) + .map_err(ManifestProfileError::from)?; + Self::from_signed_object(signed_object) + } + pub fn from_signed_object( signed_object: RpkiSignedObject, ) -> Result { diff --git a/src/data_model/rc.rs b/src/data_model/rc.rs index bf7f609..222b7c9 100644 --- a/src/data_model/rc.rs +++ b/src/data_model/rc.rs @@ -8,7 +8,7 @@ use x509_parser::prelude::{FromDer, X509Certificate, X509Extension, X509Version} use serde::{Deserialize, Serialize}; use crate::data_model::common::{ - Asn1TimeUtc, InvalidTimeEncodingError, UtcTime, X509NameDer, asn1_time_to_model, + Asn1TimeUtc, DerReader, InvalidTimeEncodingError, UtcTime, X509NameDer, asn1_time_to_model, }; use crate::data_model::oid::{ OID_AD_CA_ISSUERS_RAW, OID_AD_CA_REPOSITORY, OID_AD_CA_REPOSITORY_RAW, OID_AD_RPKI_MANIFEST, @@ -389,6 +389,11 @@ pub enum ResourceCertificateProfileError { #[error("invalid signature algorithm parameters (RFC 5280 §4.1.1.2)")] InvalidSignatureAlgorithmParameters, + #[error( + "{role} Name strict validation failed: {detail} (RFC 6487 §4.4; RFC 5280 §4.1.2.4/§4.1.2.6)" + )] + StrictName { role: &'static str, detail: String }, + #[error("duplicate extension: {0} (RFC 5280 §4.2; RFC 6487 §4.8)")] DuplicateExtension(&'static str), @@ -562,17 +567,181 @@ impl ResourceCertificate { Ok(()) } + pub fn validate_strict_name_profile(&self) -> Result<(), ResourceCertificateProfileError> { + validate_strict_rpki_name(&self.tbs.issuer_name, "issuer")?; + validate_strict_rpki_name(&self.tbs.subject_name, "subject")?; + Ok(()) + } + /// Decode a resource certificate (`parse + validate`). pub fn decode_der(der: &[u8]) -> Result { Ok(Self::parse_der(der)?.validate_profile()?) } + pub fn decode_der_with_strict_name(der: &[u8]) -> Result { + let cert = Self::decode_der(der)?; + cert.validate_strict_name_profile()?; + Ok(cert) + } + /// Backwards-compatible helper (historical name). pub fn from_der(der: &[u8]) -> Result { Self::decode_der(der) } } +fn validate_strict_rpki_name( + name: &X509NameDer, + role: &'static str, +) -> Result<(), ResourceCertificateProfileError> { + let mut name_seq = DerReader::new(name.as_raw()) + .take_sequence() + .map_err(|e| ResourceCertificateProfileError::StrictName { role, detail: e })?; + + let mut common_name_count = 0usize; + let mut serial_number_count = 0usize; + + while !name_seq.is_empty() { + let set_bytes = name_seq + .take_tag(0x31) + .map_err(|e| ResourceCertificateProfileError::StrictName { role, detail: e })?; + let mut rdn_set = DerReader::new(set_bytes); + if rdn_set.is_empty() { + return Err(ResourceCertificateProfileError::StrictName { + role, + detail: "RelativeDistinguishedName SET is empty".to_string(), + }); + } + + while !rdn_set.is_empty() { + let mut attr = rdn_set + .take_sequence() + .map_err(|e| ResourceCertificateProfileError::StrictName { role, detail: e })?; + let oid = attr + .take_tag(0x06) + .map_err(|e| ResourceCertificateProfileError::StrictName { role, detail: e })?; + let (value_tag, _value) = attr + .take_any() + .map_err(|e| ResourceCertificateProfileError::StrictName { role, detail: e })?; + if !attr.is_empty() { + return Err(ResourceCertificateProfileError::StrictName { + role, + detail: "AttributeTypeAndValue must be SEQUENCE of 2".to_string(), + }); + } + + match oid { + // 2.5.4.3 commonName + &[0x55, 0x04, 0x03] => { + common_name_count += 1; + if value_tag != 0x13 { + return Err(ResourceCertificateProfileError::StrictName { + role, + detail: format!( + "commonName must be PrintableString, got tag 0x{value_tag:02X}" + ), + }); + } + } + // 2.5.4.5 serialNumber + &[0x55, 0x04, 0x05] => { + serial_number_count += 1; + if value_tag != 0x13 { + return Err(ResourceCertificateProfileError::StrictName { + role, + detail: format!( + "serialNumber must be PrintableString, got tag 0x{value_tag:02X}" + ), + }); + } + } + _ => {} + } + } + } + + if common_name_count != 1 { + return Err(ResourceCertificateProfileError::StrictName { + role, + detail: format!("commonName must appear exactly once, got {common_name_count}"), + }); + } + if serial_number_count > 1 { + return Err(ResourceCertificateProfileError::StrictName { + role, + detail: format!("serialNumber must appear at most once, got {serial_number_count}"), + }); + } + Ok(()) +} + +#[cfg(test)] +mod strict_name_tests { + use super::*; + + fn name_with_attrs(attrs: &[(&[u8], u8, &[u8])]) -> X509NameDer { + let mut rdns = Vec::new(); + for (oid, tag, value) in attrs { + let mut attr = Vec::new(); + attr.extend(der_tlv(0x06, oid)); + attr.extend(der_tlv(*tag, value)); + let attr = der_tlv(0x30, &attr); + let rdn = der_tlv(0x31, &attr); + rdns.extend(rdn); + } + X509NameDer(der_tlv(0x30, &rdns)) + } + + fn der_tlv(tag: u8, value: &[u8]) -> Vec { + let mut out = vec![tag]; + encode_len(value.len(), &mut out); + out.extend_from_slice(value); + out + } + + fn encode_len(len: usize, out: &mut Vec) { + if len < 0x80 { + out.push(len as u8); + return; + } + let mut bytes = Vec::new(); + let mut value = len; + while value > 0 { + bytes.push((value & 0xFF) as u8); + value >>= 8; + } + bytes.reverse(); + out.push(0x80 | bytes.len() as u8); + out.extend(bytes); + } + + #[test] + fn strict_name_accepts_printable_common_name_and_serial_number() { + let name = name_with_attrs(&[ + (&[0x55, 0x04, 0x03], 0x13, b"CN1"), + (&[0x55, 0x04, 0x05], 0x13, b"SN1"), + ]); + validate_strict_rpki_name(&name, "subject").expect("strict name"); + } + + #[test] + fn strict_name_rejects_utf8_common_name() { + let name = name_with_attrs(&[(&[0x55, 0x04, 0x03], 0x0C, b"CN1")]); + let err = validate_strict_rpki_name(&name, "subject").expect_err("strict name fails"); + assert!(err.to_string().contains("PrintableString"), "{err}"); + } + + #[test] + fn strict_name_rejects_duplicate_common_name() { + let name = name_with_attrs(&[ + (&[0x55, 0x04, 0x03], 0x13, b"CN1"), + (&[0x55, 0x04, 0x03], 0x13, b"CN2"), + ]); + let err = validate_strict_rpki_name(&name, "subject").expect_err("strict name fails"); + assert!(err.to_string().contains("exactly once"), "{err}"); + } +} + impl ResourceCertificateParsed { pub fn validate_profile(self) -> Result { let version = match self.version { diff --git a/src/data_model/roa.rs b/src/data_model/roa.rs index d6688f6..317484b 100644 --- a/src/data_model/roa.rs +++ b/src/data_model/roa.rs @@ -2,7 +2,8 @@ use crate::data_model::common::{DerReader, der_take_tlv}; use crate::data_model::oid::OID_CT_ROUTE_ORIGIN_AUTHZ; use crate::data_model::rc::{Afi as RcAfi, IpPrefix as RcIpPrefix, ResourceCertificate}; use crate::data_model::signed_object::{ - RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectParseError, SignedObjectValidateError, + RpkiSignedObject, RpkiSignedObjectParsed, SignedObjectDecodeError, SignedObjectParseError, + SignedObjectValidateError, }; #[derive(Clone, Debug, PartialEq, Eq)] @@ -106,6 +107,15 @@ pub enum RoaProfileError { }, } +impl From for RoaProfileError { + fn from(value: SignedObjectDecodeError) -> Self { + match value { + SignedObjectDecodeError::Parse(e) => RoaProfileError::ProfileDecode(e.to_string()), + SignedObjectDecodeError::Validate(e) => RoaProfileError::SignedObject(e), + } + } +} + #[derive(Debug, thiserror::Error)] pub enum RoaDecodeError { #[error("{0}")] @@ -171,6 +181,17 @@ impl RoaObject { Ok(Self::parse_der(der)?.validate_profile()?) } + pub fn decode_der_with_strict_options( + der: &[u8], + strict_cms_der: bool, + strict_name: bool, + ) -> Result { + let signed_object = + RpkiSignedObject::decode_der_with_strict_options(der, strict_cms_der, strict_name) + .map_err(RoaProfileError::from)?; + Self::from_signed_object(signed_object) + } + pub fn from_signed_object(signed_object: RpkiSignedObject) -> Result { let econtent_type = signed_object .signed_data diff --git a/src/data_model/signed_object.rs b/src/data_model/signed_object.rs index 282cebe..3c8a377 100644 --- a/src/data_model/signed_object.rs +++ b/src/data_model/signed_object.rs @@ -9,10 +9,10 @@ use crate::data_model::oid::{ OID_SIGNED_DATA_RAW, OID_SUBJECT_INFO_ACCESS, }; use crate::data_model::rc::{ResourceCertificate, SubjectInfoAccess}; -use asn1_rs::{Any, Class, FromBer, Header, Tag}; +use asn1_rs::{Any, Class, FromBer, FromDer as Asn1FromDer, Header, Tag}; use ring::digest; use x509_parser::extensions::ParsedExtension; -use x509_parser::prelude::{FromDer, X509Certificate}; +use x509_parser::prelude::X509Certificate; use x509_parser::public_key::PublicKey; use x509_parser::x509::SubjectPublicKeyInfo; @@ -321,7 +321,13 @@ impl RpkiSignedObject { /// This performs encoding/structure parsing only. Profile constraints are enforced by /// `RpkiSignedObjectParsed::validate_profile`. pub fn parse_der(der: &[u8]) -> Result { - parse_signed_object_content_info(der, der) + parse_signed_object_content_info(der, der, CmsParseMode::BerCompatible) + } + + pub fn parse_der_strict_cms( + der: &[u8], + ) -> Result { + parse_signed_object_content_info(der, der, CmsParseMode::DerStrict) } /// Decode a DER-encoded RPKI Signed Object (CMS ContentInfo wrapping SignedData) and enforce @@ -331,6 +337,19 @@ impl RpkiSignedObject { Ok(parsed.validate_profile()?) } + pub fn decode_der_with_strict_options( + der: &[u8], + strict_cms_der: bool, + strict_name: bool, + ) -> Result { + let parsed = if strict_cms_der { + Self::parse_der_strict_cms(der)? + } else { + Self::parse_der(der)? + }; + Ok(parsed.validate_profile_with_strict_name(strict_name)?) + } + /// Scheme-A naming for signature verification. pub fn verify(&self) -> Result<(), SignedObjectVerifyError> { self.verify_signature() @@ -399,13 +418,20 @@ impl RpkiSignedObject { } } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum CmsParseMode { + BerCompatible, + DerStrict, +} + struct CmsReader<'a> { buf: &'a [u8], + mode: CmsParseMode, } impl<'a> CmsReader<'a> { - fn new(buf: &'a [u8]) -> Self { - Self { buf } + fn new(buf: &'a [u8], mode: CmsParseMode) -> Self { + Self { buf, mode } } fn is_empty(&self) -> bool { @@ -417,19 +443,19 @@ impl<'a> CmsReader<'a> { } fn peek_tag(&self) -> Result { - let (_rem, any) = Any::from_ber(self.buf).map_err(|e| format!("BER parse error: {e}"))?; + let (_rem, any) = parse_any(self.buf, self.mode)?; header_to_single_byte_tag(&any.header) } fn take_any(&mut self) -> Result<(u8, &'a [u8]), String> { - let (rem, any) = Any::from_ber(self.buf).map_err(|e| format!("BER parse error: {e}"))?; + let (rem, any) = parse_any(self.buf, self.mode)?; let tag = header_to_single_byte_tag(&any.header)?; self.buf = rem; Ok((tag, any.data)) } fn take_any_full(&mut self) -> Result<(u8, &'a [u8], &'a [u8]), String> { - let (rem, any) = Any::from_ber(self.buf).map_err(|e| format!("BER parse error: {e}"))?; + let (rem, any) = parse_any(self.buf, self.mode)?; let consumed = self.buf.len() - rem.len(); let full = &self.buf[..consumed]; let tag = header_to_single_byte_tag(&any.header)?; @@ -454,16 +480,21 @@ impl<'a> CmsReader<'a> { fn take_sequence(&mut self) -> Result, String> { let value = self.take_tag(0x30)?; - Ok(CmsReader::new(value)) + Ok(CmsReader::new(value, self.mode)) } fn take_octet_string(&mut self) -> Result, String> { - let (rem, any) = Any::from_ber(self.buf).map_err(|e| format!("BER parse error: {e}"))?; + let (rem, any) = parse_any(self.buf, self.mode)?; let tag = header_to_single_byte_tag(&any.header)?; + if self.mode == CmsParseMode::DerStrict && tag != 0x04 { + return Err(format!( + "unexpected tag in DER strict mode: got 0x{tag:02X}, expected 0x04" + )); + } if tag != 0x04 && tag != 0x24 { return Err(format!("unexpected tag: got 0x{tag:02X}, expected 0x04")); } - let octets = flatten_octet_string(any)?; + let octets = flatten_octet_string(any, self.mode)?; self.buf = rem; Ok(octets) } @@ -475,7 +506,7 @@ impl<'a> CmsReader<'a> { fn take_explicit(&mut self, expected_outer_tag: u8) -> Result<(u8, &'a [u8]), String> { let inner_der = self.take_tag(expected_outer_tag)?; - let (tag, value, rem) = cms_take_tlv(inner_der)?; + let (tag, value, rem) = cms_take_tlv(inner_der, self.mode)?; if !rem.is_empty() { return Err("trailing bytes inside EXPLICIT value".into()); } @@ -484,7 +515,7 @@ impl<'a> CmsReader<'a> { fn take_explicit_der(&mut self, expected_outer_tag: u8) -> Result<&'a [u8], String> { let inner_der = self.take_tag(expected_outer_tag)?; - let (_tag, _value, rem) = cms_take_tlv(inner_der)?; + let (_tag, _value, rem) = cms_take_tlv(inner_der, self.mode)?; if !rem.is_empty() { return Err("trailing bytes inside EXPLICIT value".into()); } @@ -495,8 +526,9 @@ impl<'a> CmsReader<'a> { fn parse_signed_object_content_info( raw_der: &[u8], parse_der: &[u8], + mode: CmsParseMode, ) -> Result { - let mut r = CmsReader::new(parse_der); + let mut r = CmsReader::new(parse_der, mode); let mut content_info_seq = r .take_sequence() .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; @@ -519,6 +551,17 @@ fn parse_signed_object_content_info( }) } +fn parse_any<'a>(input: &'a [u8], mode: CmsParseMode) -> Result<(&'a [u8], Any<'a>), String> { + match mode { + CmsParseMode::BerCompatible => { + Any::from_ber(input).map_err(|e| format!("BER parse error: {e}")) + } + CmsParseMode::DerStrict => { + Any::from_der(input).map_err(|e| format!("DER parse error: {e}")) + } + } +} + fn header_to_single_byte_tag(header: &Header<'_>) -> Result { let tag_no = header.tag().0; if tag_no > 30 { @@ -529,24 +572,27 @@ fn header_to_single_byte_tag(header: &Header<'_>) -> Result { | tag_no as u8) } -fn cms_take_tlv(input: &[u8]) -> Result<(u8, &[u8], &[u8]), String> { - let (rem, any) = Any::from_ber(input).map_err(|e| format!("BER parse error: {e}"))?; +fn cms_take_tlv(input: &[u8], mode: CmsParseMode) -> Result<(u8, &[u8], &[u8]), String> { + let (rem, any) = parse_any(input, mode)?; let tag = header_to_single_byte_tag(&any.header)?; Ok((tag, any.data, rem)) } -fn flatten_octet_string(any: Any<'_>) -> Result, String> { +fn flatten_octet_string(any: Any<'_>, mode: CmsParseMode) -> Result, String> { if any.class() != Class::Universal || any.tag() != Tag::OctetString { return Err("expected OCTET STRING".into()); } if !any.header.constructed() { return Ok(any.data.to_vec()); } + if mode == CmsParseMode::DerStrict { + return Err("constructed OCTET STRING is not allowed in DER strict mode".into()); + } let mut out = Vec::new(); let mut input = any.data; while !input.is_empty() { let (rem, child) = Any::from_ber(input).map_err(|e| format!("BER parse error: {e}"))?; - out.extend(flatten_octet_string(child)?); + out.extend(flatten_octet_string(child, mode)?); input = rem; } Ok(out) @@ -554,13 +600,20 @@ fn flatten_octet_string(any: Any<'_>) -> Result, String> { impl RpkiSignedObjectParsed { pub fn validate_profile(self) -> Result { + self.validate_profile_with_strict_name(false) + } + + pub fn validate_profile_with_strict_name( + self, + strict_name: bool, + ) -> Result { if self.content_info_content_type != OID_SIGNED_DATA { return Err(SignedObjectValidateError::InvalidContentInfoContentType( self.content_info_content_type, )); } - let signed_data = validate_signed_data_profile(self.signed_data)?; + let signed_data = validate_signed_data_profile(self.signed_data, strict_name)?; Ok(RpkiSignedObject { raw_der: self.raw_der, @@ -576,7 +629,7 @@ fn parse_signed_data_from_contentinfo_cursor( let inner_der = seq.take_explicit_der(0xA0).map_err(|_e| { SignedObjectParseError::Parse("ContentInfo.content must be [0] EXPLICIT".into()) })?; - let mut r = CmsReader::new(inner_der); + let mut r = CmsReader::new(inner_der, seq.mode); let signed_data_seq = r .take_sequence() .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; @@ -598,7 +651,7 @@ fn parse_signed_data_cursor( let digest_set_bytes = seq .take_tag(0x31) .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; - let mut digest_set = CmsReader::new(digest_set_bytes); + let mut digest_set = CmsReader::new(digest_set_bytes, seq.mode); let mut digest_algorithms: Vec = Vec::new(); while !digest_set.is_empty() { let alg = digest_set @@ -631,7 +684,7 @@ fn parse_signed_data_cursor( let content = seq .take_tag(0xA0) .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; - certificates = Some(split_der_objects(content)?); + certificates = Some(split_der_objects(content, seq.mode)?); } 0xA1 => { crls_present = true; @@ -647,7 +700,7 @@ fn parse_signed_data_cursor( let set_bytes = seq .take_tag(0x31) .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; - signer_infos = Some(parse_signer_infos_set_cursor(set_bytes)?); + signer_infos = Some(parse_signer_infos_set_cursor(set_bytes, seq.mode)?); } _ => { return Err(SignedObjectParseError::Parse( @@ -689,7 +742,7 @@ fn parse_encapsulated_content_info_cursor( "EncapsulatedContentInfo.eContent must be [0] EXPLICIT".into(), ) })?; - let mut inner = CmsReader::new(inner_der); + let mut inner = CmsReader::new(inner_der, seq.mode); let octets = inner .take_octet_string() .map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; @@ -712,11 +765,14 @@ fn parse_encapsulated_content_info_cursor( }) } -fn split_der_objects(mut input: &[u8]) -> Result>, SignedObjectParseError> { +fn split_der_objects( + mut input: &[u8], + mode: CmsParseMode, +) -> Result>, SignedObjectParseError> { let mut out: Vec> = Vec::new(); while !input.is_empty() { let (_tag, _value, rem) = - cms_take_tlv(input).map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; + cms_take_tlv(input, mode).map_err(|e| SignedObjectParseError::Parse(e.to_string()))?; let consumed = input.len() - rem.len(); out.push(input[..consumed].to_vec()); input = rem; @@ -726,8 +782,9 @@ fn split_der_objects(mut input: &[u8]) -> Result>, SignedObjectParse fn parse_signer_infos_set_cursor( set_bytes: &[u8], + mode: CmsParseMode, ) -> Result, SignedObjectParseError> { - let mut set = CmsReader::new(set_bytes); + let mut set = CmsReader::new(set_bytes, mode); let mut out: Vec = Vec::new(); while !set.is_empty() { let si = set @@ -738,7 +795,10 @@ fn parse_signer_infos_set_cursor( Ok(out) } -fn validate_ee_certificate(der: &[u8]) -> Result { +fn validate_ee_certificate( + der: &[u8], + strict_name: bool, +) -> Result { let (rem, cert) = X509Certificate::from_der(der) .map_err(|e| SignedObjectValidateError::EeCertificateParse(e.to_string()))?; if !rem.is_empty() { @@ -762,6 +822,10 @@ fn validate_ee_certificate(der: &[u8]) -> Result Result { if signed_data.version != 3 { return Err(SignedObjectValidateError::InvalidSignedDataVersion( @@ -985,7 +1050,7 @@ fn validate_signed_data_profile( certs.len(), )); } - let ee = validate_ee_certificate(&certs[0])?; + let ee = validate_ee_certificate(&certs[0], strict_name)?; if signed_data.signer_infos.len() != 1 { return Err(SignedObjectValidateError::InvalidSignerInfosCount( @@ -1414,3 +1479,116 @@ fn strip_leading_zeros(bytes: &[u8]) -> &[u8] { &bytes[idx..] } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn strict_cms_der_rejects_constructed_octet_string_fixture() { + let der = std::fs::read(std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join( + "tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.mft", + )) + .expect("read manifest fixture"); + let parsed = RpkiSignedObject::parse_der(&der).expect("parse fixture"); + let econtent = parsed + .signed_data + .encap_content_info + .econtent + .clone() + .expect("fixture eContent"); + assert_eq!( + parsed.signed_data.encap_content_info.econtent.as_deref(), + Some(econtent.as_slice()) + ); + + let primitive_octets = der_tlv(0x04, &econtent); + let constructed_octets = same_size_constructed_octet_string(&primitive_octets, &econtent); + let mutated = replace_first_subslice(&der, &primitive_octets, &constructed_octets) + .expect("replace eContent OCTET STRING"); + let compatible = RpkiSignedObject::parse_der(&mutated).expect("BER-compatible parse"); + assert!( + econtent.starts_with( + compatible + .signed_data + .encap_content_info + .econtent + .as_deref() + .expect("compatible eContent") + ) + ); + let err = RpkiSignedObject::parse_der_strict_cms(&mutated) + .expect_err("DER strict rejects constructed OCTET STRING"); + assert!(err.to_string().contains("DER"), "{err}"); + } + + fn replace_first_subslice(input: &[u8], from: &[u8], to: &[u8]) -> Option> { + let pos = input + .windows(from.len()) + .position(|candidate| candidate == from)?; + let mut out = Vec::with_capacity(input.len() - from.len() + to.len()); + out.extend_from_slice(&input[..pos]); + out.extend_from_slice(to); + out.extend_from_slice(&input[pos + from.len()..]); + Some(out) + } + + fn same_size_constructed_octet_string(primitive: &[u8], content: &[u8]) -> Vec { + assert_eq!(primitive[0], 0x04); + let header_len = tlv_header_len(primitive); + let outer_value_len = primitive.len() - header_len; + let child_len = (0..=outer_value_len) + .rev() + .find(|candidate| 1 + len_len(*candidate) + *candidate == outer_value_len) + .expect("find child length"); + let mut out = primitive[..header_len].to_vec(); + out[0] = 0x24; + out.extend(der_tlv(0x04, &content[..child_len])); + assert_eq!(out.len(), primitive.len()); + out + } + + fn tlv_header_len(tlv: &[u8]) -> usize { + if tlv[1] & 0x80 == 0 { + 2 + } else { + 2 + (tlv[1] & 0x7F) as usize + } + } + + fn len_len(len: usize) -> usize { + if len < 0x80 { + return 1; + } + let mut value = len; + let mut n = 0usize; + while value > 0 { + n += 1; + value >>= 8; + } + 1 + n + } + + fn der_tlv(tag: u8, value: &[u8]) -> Vec { + let mut out = vec![tag]; + encode_len(value.len(), &mut out); + out.extend_from_slice(value); + out + } + + fn encode_len(len: usize, out: &mut Vec) { + if len < 0x80 { + out.push(len as u8); + return; + } + let mut bytes = Vec::new(); + let mut value = len; + while value > 0 { + bytes.push((value & 0xFF) as u8); + value >>= 8; + } + bytes.reverse(); + out.push(0x80 | bytes.len() as u8); + out.extend(bytes); + } +} diff --git a/src/data_model/ta.rs b/src/data_model/ta.rs index 1ee7868..197b47d 100644 --- a/src/data_model/ta.rs +++ b/src/data_model/ta.rs @@ -112,6 +112,14 @@ impl TaCertificate { Ok(Self::parse_der(der)?.validate_profile()?) } + pub fn decode_der_with_strict_name(der: &[u8]) -> Result { + let ta = Self::decode_der(der)?; + ta.rc_ca + .validate_strict_name_profile() + .map_err(TaCertificateProfileError::from)?; + Ok(ta) + } + /// Backwards-compatible helper (historical name). pub fn from_der(der: &[u8]) -> Result { Self::decode_der(der) @@ -264,6 +272,15 @@ impl TrustAnchor { Ok(Self::bind(tal, ta_certificate, resolved_uri)?) } + pub fn bind_der_with_strict_name( + tal: Tal, + ta_der: &[u8], + resolved_uri: Option<&Url>, + ) -> Result { + let ta_certificate = TaCertificate::decode_der_with_strict_name(ta_der)?; + Ok(Self::bind(tal, ta_certificate, resolved_uri)?) + } + pub fn bind( tal: Tal, ta_certificate: TaCertificate, diff --git a/src/policy.rs b/src/policy.rs index 54efa9a..ef8af5a 100644 --- a/src/policy.rs +++ b/src/policy.rs @@ -39,12 +39,67 @@ impl Default for SignedObjectFailurePolicy { } } +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(default)] +pub struct StrictPolicy { + pub name: bool, + pub cms_der: bool, + pub signed_attrs: bool, +} + +impl StrictPolicy { + pub fn none() -> Self { + Self::default() + } + + pub fn all() -> Self { + Self { + name: true, + cms_der: true, + signed_attrs: true, + } + } + + pub fn parse_cli_spec(spec: Option<&str>) -> Result { + let Some(spec) = spec else { + return Ok(Self::all()); + }; + let spec = spec.trim(); + if spec.is_empty() || spec == "all" { + return Ok(Self::all()); + } + if spec == "none" { + return Ok(Self::none()); + } + + let mut out = Self::none(); + for raw in spec.split(',') { + let item = raw.trim(); + match item { + "name" => out.name = true, + "cms-der" | "cms_der" => out.cms_der = true, + "signed-attrs" | "signed_attrs" => out.signed_attrs = true, + "all" => out = Self::all(), + "none" => out = Self::none(), + "" => return Err("empty strict policy name".to_string()), + _ => { + return Err(format!( + "unknown strict policy: {item}; supported: name,cms-der,signed-attrs,all,none" + )); + } + } + } + Ok(out) + } +} + #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(default)] pub struct Policy { pub sync_preference: SyncPreference, pub ca_failed_fetch_policy: CaFailedFetchPolicy, pub signed_object_failure_policy: SignedObjectFailurePolicy, + pub strict: StrictPolicy, } impl Default for Policy { @@ -53,6 +108,7 @@ impl Default for Policy { sync_preference: SyncPreference::default(), ca_failed_fetch_policy: CaFailedFetchPolicy::default(), signed_object_failure_policy: SignedObjectFailurePolicy::default(), + strict: StrictPolicy::default(), } } } @@ -68,3 +124,50 @@ impl Policy { toml::from_str(s).map_err(|e| PolicyParseError::Toml(e.to_string())) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn strict_policy_parses_cli_specs() { + assert_eq!( + StrictPolicy::parse_cli_spec(None).unwrap(), + StrictPolicy::all() + ); + assert_eq!( + StrictPolicy::parse_cli_spec(Some("name,signed-attrs")).unwrap(), + StrictPolicy { + name: true, + cms_der: false, + signed_attrs: true, + } + ); + assert_eq!( + StrictPolicy::parse_cli_spec(Some("none")).unwrap(), + StrictPolicy::none() + ); + assert!(StrictPolicy::parse_cli_spec(Some("bogus")).is_err()); + } + + #[test] + fn policy_toml_accepts_strict_table() { + let policy = Policy::from_toml_str( + r#" + [strict] + name = true + cms_der = true + signed_attrs = false + "#, + ) + .expect("parse policy"); + assert_eq!( + policy.strict, + StrictPolicy { + name: true, + cms_der: true, + signed_attrs: false, + } + ); + } +} diff --git a/src/validation/from_tal.rs b/src/validation/from_tal.rs index 687d302..a41ae3d 100644 --- a/src/validation/from_tal.rs +++ b/src/validation/from_tal.rs @@ -57,10 +57,42 @@ pub fn discover_root_ca_instance_from_tal_url( discover_root_ca_instance_from_tal(http_fetcher, tal, Some(tal_url.to_string())) } +pub fn discover_root_ca_instance_from_tal_url_with_strict_name( + http_fetcher: &dyn Fetcher, + tal_url: &str, +) -> Result { + let tal_bytes = http_fetcher + .fetch(tal_url) + .map_err(FromTalError::TalFetch)?; + let tal = Tal::decode_bytes(&tal_bytes)?; + discover_root_ca_instance_from_tal_with_strict_name( + http_fetcher, + tal, + Some(tal_url.to_string()), + ) +} + pub fn discover_root_ca_instance_from_tal( http_fetcher: &dyn Fetcher, tal: Tal, tal_url: Option, +) -> Result { + discover_root_ca_instance_from_tal_impl(http_fetcher, tal, tal_url, false) +} + +pub fn discover_root_ca_instance_from_tal_with_strict_name( + http_fetcher: &dyn Fetcher, + tal: Tal, + tal_url: Option, +) -> Result { + discover_root_ca_instance_from_tal_impl(http_fetcher, tal, tal_url, true) +} + +fn discover_root_ca_instance_from_tal_impl( + http_fetcher: &dyn Fetcher, + tal: Tal, + tal_url: Option, + strict_name: bool, ) -> Result { if tal.ta_uris.is_empty() { return Err(FromTalError::NoTaUris); @@ -76,13 +108,14 @@ pub fn discover_root_ca_instance_from_tal( } }; - let trust_anchor = match TrustAnchor::bind_der(tal.clone(), &ta_der, Some(ta_uri)) { - Ok(ta) => ta, - Err(e) => { - last_err = Some(format!("bind {ta_uri} failed: {e}")); - continue; - } - }; + let trust_anchor = + match bind_trust_anchor_der(tal.clone(), &ta_der, Some(ta_uri), strict_name) { + Ok(ta) => ta, + Err(e) => { + last_err = Some(format!("bind {ta_uri} failed: {e}")); + continue; + } + }; let ca_instance = match ca_instance_uris_from_ca_certificate(&trust_anchor.ta_certificate.rc_ca) { @@ -110,6 +143,37 @@ pub fn discover_root_ca_instance_from_tal_with_fetchers( rsync_fetcher: &dyn RsyncFetcher, tal: Tal, tal_url: Option, +) -> Result { + discover_root_ca_instance_from_tal_with_fetchers_impl( + http_fetcher, + rsync_fetcher, + tal, + tal_url, + false, + ) +} + +pub fn discover_root_ca_instance_from_tal_with_fetchers_strict_name( + http_fetcher: &dyn Fetcher, + rsync_fetcher: &dyn RsyncFetcher, + tal: Tal, + tal_url: Option, +) -> Result { + discover_root_ca_instance_from_tal_with_fetchers_impl( + http_fetcher, + rsync_fetcher, + tal, + tal_url, + true, + ) +} + +fn discover_root_ca_instance_from_tal_with_fetchers_impl( + http_fetcher: &dyn Fetcher, + rsync_fetcher: &dyn RsyncFetcher, + tal: Tal, + tal_url: Option, + strict_name: bool, ) -> Result { if tal.ta_uris.is_empty() { return Err(FromTalError::NoTaUris); @@ -127,13 +191,14 @@ pub fn discover_root_ca_instance_from_tal_with_fetchers( } }; - let trust_anchor = match TrustAnchor::bind_der(tal.clone(), &ta_der, Some(ta_uri)) { - Ok(ta) => ta, - Err(e) => { - last_err = Some(format!("bind {ta_uri} failed: {e}")); - continue; - } - }; + let trust_anchor = + match bind_trust_anchor_der(tal.clone(), &ta_der, Some(ta_uri), strict_name) { + Ok(ta) => ta, + Err(e) => { + last_err = Some(format!("bind {ta_uri} failed: {e}")); + continue; + } + }; let ca_instance = match ca_instance_uris_from_ca_certificate(&trust_anchor.ta_certificate.rc_ca) { @@ -168,6 +233,19 @@ fn fetch_ta_der( } } +fn bind_trust_anchor_der( + tal: Tal, + ta_der: &[u8], + resolved_uri: Option<&Url>, + strict_name: bool, +) -> Result { + if strict_name { + TrustAnchor::bind_der_with_strict_name(tal, ta_der, resolved_uri) + } else { + TrustAnchor::bind_der(tal, ta_der, resolved_uri) + } +} + fn fetch_ta_der_via_rsync( rsync_fetcher: &dyn RsyncFetcher, ta_rsync_uri: &str, @@ -213,9 +291,26 @@ pub fn discover_root_ca_instance_from_tal_and_ta_der( tal_bytes: &[u8], ta_der: &[u8], resolved_ta_uri: Option<&Url>, +) -> Result { + discover_root_ca_instance_from_tal_and_ta_der_impl(tal_bytes, ta_der, resolved_ta_uri, false) +} + +pub fn discover_root_ca_instance_from_tal_and_ta_der_with_strict_name( + tal_bytes: &[u8], + ta_der: &[u8], + resolved_ta_uri: Option<&Url>, +) -> Result { + discover_root_ca_instance_from_tal_and_ta_der_impl(tal_bytes, ta_der, resolved_ta_uri, true) +} + +fn discover_root_ca_instance_from_tal_and_ta_der_impl( + tal_bytes: &[u8], + ta_der: &[u8], + resolved_ta_uri: Option<&Url>, + strict_name: bool, ) -> Result { let tal = Tal::decode_bytes(tal_bytes)?; - let trust_anchor = TrustAnchor::bind_der(tal, ta_der, resolved_ta_uri)?; + let trust_anchor = bind_trust_anchor_der(tal, ta_der, resolved_ta_uri, strict_name)?; let ca_instance = ca_instance_uris_from_ca_certificate(&trust_anchor.ta_certificate.rc_ca)?; Ok(DiscoveredRootCaInstance { tal_url: None, diff --git a/src/validation/objects.rs b/src/validation/objects.rs index 3d9aaa1..0973c5a 100644 --- a/src/validation/objects.rs +++ b/src/validation/objects.rs @@ -28,6 +28,17 @@ const RFC_CRLDP: &[RfcRef] = &[RfcRef("RFC 6487 §4.8.6")]; const RFC_CRLDP_AND_LOCKED_PACK: &[RfcRef] = &[RfcRef("RFC 6487 §4.8.6"), RfcRef("RFC 9286 §4.2.1")]; +fn decode_resource_certificate_with_policy( + der: &[u8], + policy: &Policy, +) -> Result { + if policy.strict.name { + ResourceCertificate::decode_der_with_strict_name(der) + } else { + ResourceCertificate::decode_der(der) + } +} + #[derive(Clone, Debug)] pub(crate) struct VerifiedIssuerCrl { crl: crate::data_model::crl::RpkixCrl, @@ -178,11 +189,15 @@ pub fn process_publication_point_for_issuer_with_options = Vec::new(); // Enforce that `manifest_bytes` is actually a manifest object. - let _manifest = ManifestObject::decode_der(manifest_bytes) - .expect("publication point snapshot manifest decodes"); + let _manifest = ManifestObject::decode_der_with_strict_options( + manifest_bytes, + policy.strict.cms_der, + policy.strict.name, + ) + .expect("publication point snapshot manifest decodes"); // Decode issuer CA once; if it fails we cannot validate ROA/ASPA EE certificates. - let issuer_ca = match ResourceCertificate::decode_der(issuer_ca_der) { + let issuer_ca = match decode_resource_certificate_with_policy(issuer_ca_der, policy) { Ok(v) => v, Err(e) => { stats.publication_point_dropped = true; @@ -349,6 +364,8 @@ pub fn process_publication_point_for_issuer_with_options { @@ -457,6 +474,8 @@ pub fn process_publication_point_for_issuer_with_options { stats.aspa_ok += 1; @@ -739,6 +758,8 @@ pub(crate) struct OwnedRoaTask { issuer_effective_as: Option, validation_time: time::OffsetDateTime, collect_vcir_local_outputs: bool, + strict_cms_der: bool, + strict_name: bool, pub(crate) submitted_at: Option, } @@ -844,6 +865,8 @@ fn validate_owned_roa_task(worker_index: usize, task: OwnedRoaTask) -> RoaTaskRe task.validation_time, None, task.collect_vcir_local_outputs, + task.strict_cms_der, + task.strict_name, ) .map(|(vrps, local_outputs)| RoaTaskOk { vrps, @@ -881,6 +904,8 @@ pub(crate) struct ParallelObjectsStage { issuer_effective_as: Option, validation_time: time::OffsetDateTime, collect_vcir_local_outputs: bool, + strict_cms_der: bool, + strict_name: bool, warnings: Vec, stats: ObjectsStats, audit: Vec, @@ -907,6 +932,8 @@ impl ParallelObjectsStage { issuer_effective_as: self.issuer_effective_as.clone(), validation_time: self.validation_time, collect_vcir_local_outputs: self.collect_vcir_local_outputs, + strict_cms_der: self.strict_cms_der, + strict_name: self.strict_name, submitted_at: None, }) .collect() @@ -928,6 +955,7 @@ impl ParallelObjectsStage { pub(crate) fn prepare_publication_point_for_parallel_roa( publication_point_id: u64, publication_point: &P, + policy: &Policy, issuer_ca_der: &[u8], issuer_ca_rsync_uri: Option<&str>, issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>, @@ -950,10 +978,14 @@ pub(crate) fn prepare_publication_point_for_parallel_roa = Vec::new(); - let _manifest = ManifestObject::decode_der(manifest_bytes) - .expect("publication point snapshot manifest decodes"); + let _manifest = ManifestObject::decode_der_with_strict_options( + manifest_bytes, + policy.strict.cms_der, + policy.strict.name, + ) + .expect("publication point snapshot manifest decodes"); - let issuer_ca = match ResourceCertificate::decode_der(issuer_ca_der) { + let issuer_ca = match decode_resource_certificate_with_policy(issuer_ca_der, policy) { Ok(v) => v, Err(e) => { stats.publication_point_dropped = true; @@ -1109,6 +1141,8 @@ pub(crate) fn prepare_publication_point_for_parallel_roa { stats.aspa_ok += 1; @@ -1254,6 +1290,7 @@ fn process_publication_point_for_issuer_parallel_roa_inner, collect_vcir_local_outputs: bool, + strict_cms_der: bool, + strict_name: bool, ) -> RoaTaskResult { let sha256_hex = sha256_hex_from_32(&task.file.sha256); let outcome = process_roa_with_issuer( @@ -1427,6 +1466,8 @@ pub(crate) fn validate_roa_task_serial( validation_time, timing, collect_vcir_local_outputs, + strict_cms_der, + strict_name, ) .map(|(vrps, local_outputs)| RoaTaskOk { vrps, @@ -1459,11 +1500,17 @@ fn process_roa_with_issuer( validation_time: time::OffsetDateTime, timing: Option<&TimingHandle>, collect_vcir_local_outputs: bool, + strict_cms_der: bool, + strict_name: bool, ) -> Result<(Vec, Vec), ObjectValidateError> { let _decode = timing .as_ref() .map(|t| t.span_phase("objects_roa_decode_and_validate_total")); - let roa = RoaObject::decode_der(file.bytes().map_err(ObjectValidateError::BytesLoad)?)?; + let roa = RoaObject::decode_der_with_strict_options( + file.bytes().map_err(ObjectValidateError::BytesLoad)?, + strict_cms_der, + strict_name, + )?; drop(_decode); let _ee_profile = timing @@ -1575,11 +1622,17 @@ fn process_roa_with_issuer_parallel_cached( validation_time: time::OffsetDateTime, timing: Option<&TimingHandle>, collect_vcir_local_outputs: bool, + strict_cms_der: bool, + strict_name: bool, ) -> Result<(Vec, Vec), ObjectValidateError> { let _decode = timing .as_ref() .map(|t| t.span_phase("objects_roa_decode_and_validate_total")); - let roa = RoaObject::decode_der(file.bytes().map_err(ObjectValidateError::BytesLoad)?)?; + let roa = RoaObject::decode_der_with_strict_options( + file.bytes().map_err(ObjectValidateError::BytesLoad)?, + strict_cms_der, + strict_name, + )?; drop(_decode); let _ee_profile = timing @@ -1697,11 +1750,17 @@ fn process_aspa_with_issuer( validation_time: time::OffsetDateTime, timing: Option<&TimingHandle>, collect_vcir_local_outputs: bool, + strict_cms_der: bool, + strict_name: bool, ) -> Result<(AspaAttestation, Option), ObjectValidateError> { let _decode = timing .as_ref() .map(|t| t.span_phase("objects_aspa_decode_and_validate_total")); - let aspa = AspaObject::decode_der(file.bytes().map_err(ObjectValidateError::BytesLoad)?)?; + let aspa = AspaObject::decode_der_with_strict_options( + file.bytes().map_err(ObjectValidateError::BytesLoad)?, + strict_cms_der, + strict_name, + )?; drop(_decode); let _ee_profile = timing diff --git a/src/validation/run_tree_from_tal.rs b/src/validation/run_tree_from_tal.rs index 7a67a8e..54b9d5f 100644 --- a/src/validation/run_tree_from_tal.rs +++ b/src/validation/run_tree_from_tal.rs @@ -24,7 +24,11 @@ use crate::replay::fetch_rsync::PayloadReplayRsyncFetcher; use crate::sync::rrdp::Fetcher; use crate::validation::from_tal::{ DiscoveredRootCaInstance, FromTalError, discover_root_ca_instance_from_tal_and_ta_der, - discover_root_ca_instance_from_tal_url, discover_root_ca_instance_from_tal_with_fetchers, + discover_root_ca_instance_from_tal_and_ta_der_with_strict_name, + discover_root_ca_instance_from_tal_url, + discover_root_ca_instance_from_tal_url_with_strict_name, + discover_root_ca_instance_from_tal_with_fetchers, + discover_root_ca_instance_from_tal_with_fetchers_strict_name, }; use crate::validation::objects::ParallelRoaWorkerPool; use crate::validation::tree::{ @@ -86,6 +90,7 @@ pub struct RunTreeFromTalOutput { pub struct RunTreeFromTalAuditOutput { pub discovery: DiscoveredRootCaInstance, pub discoveries: Vec, + pub successful_tal_inputs: Vec, pub tree: TreeRunOutput, pub publication_points: Vec, pub downloads: Vec, @@ -196,19 +201,48 @@ fn root_discovery_from_tal_input( tal_input: &TalInputSpec, http_fetcher: &dyn Fetcher, rsync_fetcher: &dyn crate::fetch::rsync::RsyncFetcher, + strict_name: bool, ) -> Result { match &tal_input.source { - TalSource::Url(url) => discover_root_ca_instance_from_tal_url(http_fetcher, url), + TalSource::Url(url) => { + if strict_name { + discover_root_ca_instance_from_tal_url_with_strict_name(http_fetcher, url) + } else { + discover_root_ca_instance_from_tal_url(http_fetcher, url) + } + } TalSource::DerBytes { tal_bytes, ta_der, .. - } => discover_root_ca_instance_from_tal_and_ta_der(tal_bytes, ta_der, None), + } => { + if strict_name { + discover_root_ca_instance_from_tal_and_ta_der_with_strict_name( + tal_bytes, ta_der, None, + ) + } else { + discover_root_ca_instance_from_tal_and_ta_der(tal_bytes, ta_der, None) + } + } TalSource::FilePath(path) => { let tal_bytes = std::fs::read(path).map_err(|e| { FromTalError::TalFetch(format!("read TAL file failed: {}: {e}", path.display())) })?; let tal = crate::data_model::tal::Tal::decode_bytes(&tal_bytes) .map_err(FromTalError::from)?; - discover_root_ca_instance_from_tal_with_fetchers(http_fetcher, rsync_fetcher, tal, None) + if strict_name { + discover_root_ca_instance_from_tal_with_fetchers_strict_name( + http_fetcher, + rsync_fetcher, + tal, + None, + ) + } else { + discover_root_ca_instance_from_tal_with_fetchers( + http_fetcher, + rsync_fetcher, + tal, + None, + ) + } } TalSource::FilePathWithTa { tal_path, ta_path } => { let tal_bytes = std::fs::read(tal_path).map_err(|e| { @@ -217,19 +251,76 @@ fn root_discovery_from_tal_input( let ta_der = std::fs::read(ta_path).map_err(|e| { FromTalError::TaFetch(format!("read TA file failed: {}: {e}", ta_path.display())) })?; - discover_root_ca_instance_from_tal_and_ta_der(&tal_bytes, &ta_der, None) + if strict_name { + discover_root_ca_instance_from_tal_and_ta_der_with_strict_name( + &tal_bytes, &ta_der, None, + ) + } else { + discover_root_ca_instance_from_tal_and_ta_der(&tal_bytes, &ta_der, None) + } } } } +fn discover_root_ca_instance_from_tal_url_with_policy( + policy: &crate::policy::Policy, + http_fetcher: &dyn Fetcher, + tal_url: &str, +) -> Result { + if policy.strict.name { + discover_root_ca_instance_from_tal_url_with_strict_name(http_fetcher, tal_url) + } else { + discover_root_ca_instance_from_tal_url(http_fetcher, tal_url) + } +} + +fn discover_root_ca_instance_from_tal_and_ta_der_with_policy( + policy: &crate::policy::Policy, + tal_bytes: &[u8], + ta_der: &[u8], + resolved_ta_uri: Option<&Url>, +) -> Result { + if policy.strict.name { + discover_root_ca_instance_from_tal_and_ta_der_with_strict_name( + tal_bytes, + ta_der, + resolved_ta_uri, + ) + } else { + discover_root_ca_instance_from_tal_and_ta_der(tal_bytes, ta_der, resolved_ta_uri) + } +} + fn discover_multiple_roots_from_tal_inputs( tal_inputs: &[TalInputSpec], http_fetcher: &dyn Fetcher, rsync_fetcher: &dyn crate::fetch::rsync::RsyncFetcher, + strict_name: bool, ) -> Result, RunTreeFromTalError> { let mut roots = Vec::with_capacity(tal_inputs.len()); for tal_input in tal_inputs { - let discovery = root_discovery_from_tal_input(tal_input, http_fetcher, rsync_fetcher)?; + let discovery = match root_discovery_from_tal_input( + tal_input, + http_fetcher, + rsync_fetcher, + strict_name, + ) { + Ok(discovery) => discovery, + Err(error) + if should_isolate_multi_tal_strict_name_failure( + tal_inputs.len(), + strict_name, + &error, + ) => + { + eprintln!( + "warning: skipping TAL '{}' because strict name validation failed during trust anchor discovery: {error}", + tal_input.tal_id + ); + continue; + } + Err(error) => return Err(error.into()), + }; let root_handle = root_handle_from_trust_anchor( &discovery.trust_anchor, tal_input.tal_id.clone(), @@ -242,9 +333,25 @@ fn discover_multiple_roots_from_tal_inputs( root_handle, }); } + if roots.is_empty() { + return Err(RunTreeFromTalError::Replay( + "multi-TAL root discovery returned no usable roots after strict name filtering" + .to_string(), + )); + } Ok(roots) } +fn should_isolate_multi_tal_strict_name_failure( + tal_input_count: usize, + strict_name: bool, + error: &FromTalError, +) -> bool { + strict_name + && tal_input_count > 1 + && error.to_string().contains("Name strict validation failed") +} + #[derive(Debug, thiserror::Error)] pub enum RunTreeFromTalError { #[error("{0}")] @@ -288,7 +395,8 @@ pub fn run_tree_from_tal_url_serial( validation_time: time::OffsetDateTime, config: &TreeRunConfig, ) -> Result { - let discovery = discover_root_ca_instance_from_tal_url(http_fetcher, tal_url)?; + let discovery = + discover_root_ca_instance_from_tal_url_with_policy(policy, http_fetcher, tal_url)?; let runner = make_live_runner( store, @@ -325,7 +433,8 @@ pub fn run_tree_from_tal_url_serial_audit( validation_time: time::OffsetDateTime, config: &TreeRunConfig, ) -> Result { - let discovery = discover_root_ca_instance_from_tal_url(http_fetcher, tal_url)?; + let discovery = + discover_root_ca_instance_from_tal_url_with_policy(policy, http_fetcher, tal_url)?; let download_log = DownloadLogHandle::new(); let runner = make_live_runner( @@ -359,6 +468,7 @@ pub fn run_tree_from_tal_url_serial_audit( Ok(RunTreeFromTalAuditOutput { discovery: discovery.clone(), discoveries: vec![discovery], + successful_tal_inputs: Vec::new(), tree, publication_points, downloads, @@ -379,7 +489,8 @@ pub fn run_tree_from_tal_url_serial_audit_with_timing( timing: &TimingHandle, ) -> Result { let _tal = timing.span_phase("tal_bootstrap"); - let discovery = discover_root_ca_instance_from_tal_url(http_fetcher, tal_url)?; + let discovery = + discover_root_ca_instance_from_tal_url_with_policy(policy, http_fetcher, tal_url)?; drop(_tal); let download_log = DownloadLogHandle::new(); @@ -415,6 +526,7 @@ pub fn run_tree_from_tal_url_serial_audit_with_timing( Ok(RunTreeFromTalAuditOutput { discovery: discovery.clone(), discoveries: vec![discovery], + successful_tal_inputs: Vec::new(), tree, publication_points, downloads, @@ -489,6 +601,7 @@ where Ok(RunTreeFromTalAuditOutput { discovery: discovery.clone(), discoveries: vec![discovery], + successful_tal_inputs: Vec::new(), tree, publication_points, downloads, @@ -523,7 +636,12 @@ where "multi-TAL run requires at least one TAL input".to_string(), )); } - let roots = discover_multiple_roots_from_tal_inputs(&tal_inputs, http_fetcher, rsync_fetcher)?; + let roots = discover_multiple_roots_from_tal_inputs( + &tal_inputs, + http_fetcher, + rsync_fetcher, + policy.strict.name, + )?; let primary = roots.first().cloned().ok_or_else(|| { RunTreeFromTalError::Replay("multi-TAL root discovery returned no roots".to_string()) })?; @@ -531,9 +649,13 @@ where .iter() .map(|item| item.discovery.clone()) .collect::>(); + let successful_tal_inputs = roots + .iter() + .map(|item| item.tal_input.clone()) + .collect::>(); let root_handles = roots - .into_iter() - .map(|item| item.root_handle) + .iter() + .map(|item| item.root_handle.clone()) .collect::>(); let download_log = DownloadLogHandle::new(); @@ -545,7 +667,7 @@ where parallel_config, None, Some(download_log.clone()), - tal_inputs, + successful_tal_inputs.clone(), )?; let current_repo_index_for_output = current_repo_index.clone(); let runner = make_live_runner( @@ -583,6 +705,7 @@ where Ok(RunTreeFromTalAuditOutput { discovery: primary.discovery.clone(), discoveries, + successful_tal_inputs, tree, publication_points, downloads, @@ -610,7 +733,8 @@ where H: Fetcher + Clone + 'static, R: crate::fetch::rsync::RsyncFetcher + Clone + 'static, { - let discovery = discover_root_ca_instance_from_tal_url(http_fetcher, tal_url)?; + let discovery = + discover_root_ca_instance_from_tal_url_with_policy(policy, http_fetcher, tal_url)?; run_single_root_parallel_audit_inner( store, policy, @@ -643,8 +767,12 @@ where H: Fetcher + Clone + 'static, R: crate::fetch::rsync::RsyncFetcher + Clone + 'static, { - let discovery = - discover_root_ca_instance_from_tal_and_ta_der(tal_bytes, ta_der, resolved_ta_uri)?; + let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy( + policy, + tal_bytes, + ta_der, + resolved_ta_uri, + )?; let derived_tal_id = derive_tal_id(&discovery); let tal_inputs = vec![TalInputSpec { tal_id: derived_tal_id.clone(), @@ -718,7 +846,8 @@ where H: Fetcher + Clone + 'static, R: crate::fetch::rsync::RsyncFetcher + Clone + 'static, { - let discovery = discover_root_ca_instance_from_tal_url(http_fetcher, tal_url)?; + let discovery = + discover_root_ca_instance_from_tal_url_with_policy(policy, http_fetcher, tal_url)?; run_single_root_parallel_audit_inner( store, policy, @@ -752,8 +881,12 @@ where H: Fetcher + Clone + 'static, R: crate::fetch::rsync::RsyncFetcher + Clone + 'static, { - let discovery = - discover_root_ca_instance_from_tal_and_ta_der(tal_bytes, ta_der, resolved_ta_uri)?; + let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy( + policy, + tal_bytes, + ta_der, + resolved_ta_uri, + )?; let derived_tal_id = derive_tal_id(&discovery); let tal_inputs = vec![TalInputSpec { tal_id: derived_tal_id.clone(), @@ -823,8 +956,12 @@ pub fn run_tree_from_tal_and_ta_der_serial( validation_time: time::OffsetDateTime, config: &TreeRunConfig, ) -> Result { - let discovery = - discover_root_ca_instance_from_tal_and_ta_der(tal_bytes, ta_der, resolved_ta_uri)?; + let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy( + policy, + tal_bytes, + ta_der, + resolved_ta_uri, + )?; let runner = Rpkiv1PublicationPointRunner { store, @@ -916,6 +1053,7 @@ pub fn run_tree_from_tal_bytes_serial_audit( Ok(RunTreeFromTalAuditOutput { discovery: discovery.clone(), discoveries: vec![discovery], + successful_tal_inputs: Vec::new(), tree, publication_points, downloads, @@ -987,6 +1125,7 @@ pub fn run_tree_from_tal_bytes_serial_audit_with_timing( Ok(RunTreeFromTalAuditOutput { discovery: discovery.clone(), discoveries: vec![discovery], + successful_tal_inputs: Vec::new(), tree, publication_points, downloads, @@ -1007,8 +1146,12 @@ pub fn run_tree_from_tal_and_ta_der_serial_audit( validation_time: time::OffsetDateTime, config: &TreeRunConfig, ) -> Result { - let discovery = - discover_root_ca_instance_from_tal_and_ta_der(tal_bytes, ta_der, resolved_ta_uri)?; + let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy( + policy, + tal_bytes, + ta_der, + resolved_ta_uri, + )?; let download_log = DownloadLogHandle::new(); let runner = Rpkiv1PublicationPointRunner { @@ -1049,6 +1192,7 @@ pub fn run_tree_from_tal_and_ta_der_serial_audit( Ok(RunTreeFromTalAuditOutput { discovery: discovery.clone(), discoveries: vec![discovery], + successful_tal_inputs: Vec::new(), tree, publication_points, downloads, @@ -1071,8 +1215,12 @@ pub fn run_tree_from_tal_and_ta_der_serial_audit_with_timing( timing: &TimingHandle, ) -> Result { let _tal = timing.span_phase("tal_bootstrap"); - let discovery = - discover_root_ca_instance_from_tal_and_ta_der(tal_bytes, ta_der, resolved_ta_uri)?; + let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy( + policy, + tal_bytes, + ta_der, + resolved_ta_uri, + )?; drop(_tal); let download_log = DownloadLogHandle::new(); @@ -1115,6 +1263,7 @@ pub fn run_tree_from_tal_and_ta_der_serial_audit_with_timing( Ok(RunTreeFromTalAuditOutput { discovery: discovery.clone(), discoveries: vec![discovery], + successful_tal_inputs: Vec::new(), tree, publication_points, downloads, @@ -1135,8 +1284,12 @@ pub fn run_tree_from_tal_and_ta_der_payload_replay_serial( validation_time: time::OffsetDateTime, config: &TreeRunConfig, ) -> Result { - let discovery = - discover_root_ca_instance_from_tal_and_ta_der(tal_bytes, ta_der, resolved_ta_uri)?; + let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy( + policy, + tal_bytes, + ta_der, + resolved_ta_uri, + )?; let replay_index = Arc::new( ReplayArchiveIndex::load_allow_missing_rsync_modules( payload_archive_root, @@ -1192,8 +1345,12 @@ pub fn run_tree_from_tal_and_ta_der_payload_replay_serial_audit( validation_time: time::OffsetDateTime, config: &TreeRunConfig, ) -> Result { - let discovery = - discover_root_ca_instance_from_tal_and_ta_der(tal_bytes, ta_der, resolved_ta_uri)?; + let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy( + policy, + tal_bytes, + ta_der, + resolved_ta_uri, + )?; let replay_index = Arc::new( ReplayArchiveIndex::load_allow_missing_rsync_modules( payload_archive_root, @@ -1244,6 +1401,7 @@ pub fn run_tree_from_tal_and_ta_der_payload_replay_serial_audit( Ok(RunTreeFromTalAuditOutput { discovery: discovery.clone(), discoveries: vec![discovery], + successful_tal_inputs: Vec::new(), tree, publication_points, downloads, @@ -1266,8 +1424,12 @@ pub fn run_tree_from_tal_and_ta_der_payload_replay_serial_audit_with_timing( timing: &TimingHandle, ) -> Result { let _tal = timing.span_phase("tal_bootstrap"); - let discovery = - discover_root_ca_instance_from_tal_and_ta_der(tal_bytes, ta_der, resolved_ta_uri)?; + let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy( + policy, + tal_bytes, + ta_der, + resolved_ta_uri, + )?; drop(_tal); let replay_index = Arc::new( ReplayArchiveIndex::load_allow_missing_rsync_modules( @@ -1320,6 +1482,7 @@ pub fn run_tree_from_tal_and_ta_der_payload_replay_serial_audit_with_timing( Ok(RunTreeFromTalAuditOutput { discovery: discovery.clone(), discoveries: vec![discovery], + successful_tal_inputs: Vec::new(), tree, publication_points, downloads, @@ -1536,6 +1699,7 @@ fn run_payload_delta_replay_audit_inner( Ok(RunTreeFromTalAuditOutput { discovery: discovery.clone(), discoveries: vec![discovery], + successful_tal_inputs: Vec::new(), tree, publication_points, downloads, @@ -1559,8 +1723,12 @@ pub fn run_tree_from_tal_and_ta_der_payload_delta_replay_serial_audit( validation_time: time::OffsetDateTime, config: &TreeRunConfig, ) -> Result { - let discovery = - discover_root_ca_instance_from_tal_and_ta_der(tal_bytes, ta_der, resolved_ta_uri)?; + let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy( + policy, + tal_bytes, + ta_der, + resolved_ta_uri, + )?; run_payload_delta_replay_audit_inner( store, policy, @@ -1592,8 +1760,12 @@ pub fn run_tree_from_tal_and_ta_der_payload_delta_replay_serial_audit_with_timin timing: &TimingHandle, ) -> Result { let _tal = timing.span_phase("tal_bootstrap"); - let discovery = - discover_root_ca_instance_from_tal_and_ta_der(tal_bytes, ta_der, resolved_ta_uri)?; + let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy( + policy, + tal_bytes, + ta_der, + resolved_ta_uri, + )?; drop(_tal); run_payload_delta_replay_audit_inner( store, @@ -1681,6 +1853,7 @@ fn run_payload_delta_replay_step_audit_inner( Ok(RunTreeFromTalAuditOutput { discovery: discovery.clone(), discoveries: vec![discovery], + successful_tal_inputs: Vec::new(), tree, publication_points, downloads, @@ -1702,8 +1875,12 @@ pub fn run_tree_from_tal_and_ta_der_payload_delta_replay_step_serial_audit( validation_time: time::OffsetDateTime, config: &TreeRunConfig, ) -> Result { - let discovery = - discover_root_ca_instance_from_tal_and_ta_der(tal_bytes, ta_der, resolved_ta_uri)?; + let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy( + policy, + tal_bytes, + ta_der, + resolved_ta_uri, + )?; run_payload_delta_replay_step_audit_inner( store, policy, @@ -1790,6 +1967,7 @@ mod multi_tal_tests { &tal_inputs, &RejectingHttpFetcher, &RejectingRsyncFetcher, + false, ) .expect("discover roots"); @@ -1803,6 +1981,54 @@ mod multi_tal_tests { roots[1].root_handle.manifest_rsync_uri ); } + + #[test] + fn discover_multiple_roots_isolates_strict_name_failure() { + let apnic_tal = + std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal").expect("read apnic tal"); + let apnic_ta = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta"); + let arin_tal = std::fs::read("tests/fixtures/tal/arin.tal").expect("read arin tal"); + let arin_ta = std::fs::read("tests/fixtures/ta/arin-ta.cer").expect("read arin ta"); + + let tal_inputs = vec![ + TalInputSpec::from_ta_der("https://example.test/apnic.tal", apnic_tal, apnic_ta), + TalInputSpec::from_ta_der("https://example.test/arin.tal", arin_tal, arin_ta), + ]; + + let roots = discover_multiple_roots_from_tal_inputs( + &tal_inputs, + &RejectingHttpFetcher, + &RejectingRsyncFetcher, + true, + ) + .expect("strict discovery should keep usable roots"); + + assert_eq!(roots.len(), 1); + assert_eq!(roots[0].tal_input.tal_id, "arin"); + assert_eq!(roots[0].root_handle.tal_id, "arin"); + } + + #[test] + fn discover_single_root_keeps_strict_name_failure_fatal() { + let apnic_tal = + std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal").expect("read apnic tal"); + let apnic_ta = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta"); + let tal_inputs = vec![TalInputSpec::from_ta_der( + "https://example.test/apnic.tal", + apnic_tal, + apnic_ta, + )]; + + let error = discover_multiple_roots_from_tal_inputs( + &tal_inputs, + &RejectingHttpFetcher, + &RejectingRsyncFetcher, + true, + ) + .expect_err("single-TAL strict failure should remain fatal"); + + assert!(error.to_string().contains("Name strict validation failed")); + } } #[cfg(test)] diff --git a/src/validation/tree_parallel.rs b/src/validation/tree_parallel.rs index 0079f79..2ab488f 100644 --- a/src/validation/tree_parallel.rs +++ b/src/validation/tree_parallel.rs @@ -858,6 +858,7 @@ fn stage_ready_publication_point( match prepare_publication_point_for_parallel_roa( ready.node.id, &fresh_stage.fresh_point, + runner.policy, &ready.node.handle.ca_certificate_der, ready.node.handle.ca_certificate_rsync_uri.as_deref(), ready.node.handle.effective_ip_resources.as_ref(),