From a5b194ce3513244563b7241c88a97579c25d91aa Mon Sep 17 00:00:00 2001 From: yuyr Date: Wed, 1 Jul 2026 20:28:25 +0800 Subject: [PATCH] 20260701 add validation-update-03 VRS mode --- deploy/arm64-installer/.env.example | 4 + deploy/arm64-installer/docs/README.en.md | 17 + deploy/arm64-installer/docs/README.zh-CN.md | 17 + deploy/arm64-installer/scripts/common.sh | 2 + scripts/soak/portable-soak.env.example | 4 + scripts/soak/run_soak.sh | 13 + src/cli.rs | 23 +- src/cli/tests.rs | 52 ++ src/data_model/rc.rs | 23 + src/policy.rs | 55 ++ src/validation/ca_path.rs | 656 +++++++++++++++++++- src/validation/objects.rs | 435 ++++++++++++- src/validation/tree_runner.rs | 109 +++- src/validation/tree_runner/tests.rs | 44 +- 14 files changed, 1430 insertions(+), 24 deletions(-) diff --git a/deploy/arm64-installer/.env.example b/deploy/arm64-installer/.env.example index 9d2bbae..d552847 100644 --- a/deploy/arm64-installer/.env.example +++ b/deploy/arm64-installer/.env.example @@ -63,6 +63,10 @@ PERIODIC_SNAPSHOT_RESET=0 PERIODIC_SNAPSHOT_MAX_DELTAS=100 DB_STATS_EXACT_EVERY=0 +# Resource certificate validation mode passed to the rpki child process. +# Options: validation-update-03 | rfc6487 +RESOURCE_VALIDATION_MODE=validation-update-03 + # Validation and performance options aligned with current optimized soak defaults. ENABLE_CHILD_CERTIFICATE_VALIDATION_CACHE=1 RPKI_ANALYZE=1 diff --git a/deploy/arm64-installer/docs/README.en.md b/deploy/arm64-installer/docs/README.en.md index d07ea0b..0335788 100644 --- a/deploy/arm64-installer/docs/README.en.md +++ b/deploy/arm64-installer/docs/README.en.md @@ -33,6 +33,7 @@ Defaults: - `MAX_RUNS=-1` - `INTERVAL_SECS=600` - `TAL_INPUT_MODE=file-live-ta` +- `RESOURCE_VALIDATION_MODE=validation-update-03` - `LIVE_TA_REFRESH_BEFORE_SNAPSHOT=1` - `PERIODIC_SNAPSHOT_RESET=0` - `PERIODIC_SNAPSHOT_MAX_DELTAS=100` @@ -57,6 +58,22 @@ Container/image mapping: The first snapshot refreshes live TA certificates before starting the RP process. +## Resource Validation Mode + +New knob: + +```bash +RESOURCE_VALIDATION_MODE=validation-update-03 +``` + +Supported values: + +- `validation-update-03`: default behavior, using the current validation-update draft semantics; +- `rfc6487`: switch back to the original RFC 6487 resource containment behavior. + +The installer / soak runner passes `--resource-validation-mode` to every `rpki` child start. +When an upgraded deployment reuses an older `.env` that does not contain this key, the installer appends the new default `validation-update-03` automatically. + ## Architecture Guardrails The key scripts read: diff --git a/deploy/arm64-installer/docs/README.zh-CN.md b/deploy/arm64-installer/docs/README.zh-CN.md index 423346b..cc21c3c 100644 --- a/deploy/arm64-installer/docs/README.zh-CN.md +++ b/deploy/arm64-installer/docs/README.zh-CN.md @@ -33,6 +33,7 @@ vim .env - `MAX_RUNS=-1` - `INTERVAL_SECS=600` - `TAL_INPUT_MODE=file-live-ta` +- `RESOURCE_VALIDATION_MODE=validation-update-03` - `LIVE_TA_REFRESH_BEFORE_SNAPSHOT=1` - `PERIODIC_SNAPSHOT_RESET=0` - `PERIODIC_SNAPSHOT_MAX_DELTAS=100` @@ -57,6 +58,22 @@ vim .env 第一轮 snapshot 会先拉取 live TA,避免 clean state 使用旧 fixture TA。 +## 资源验证模式 + +新增配置: + +```bash +RESOURCE_VALIDATION_MODE=validation-update-03 +``` + +可选值: + +- `validation-update-03`:默认,按当前 validation update draft 语义运行; +- `rfc6487`:切换为 RFC 6487 原始资源包含判定语义。 + +installer / soak runner 每次启动 `rpki` 子进程时都会显式传入 `--resource-validation-mode`。 +如果升级时复用旧 `.env` 且缺少该变量,installer 会自动补入默认值 `validation-update-03`,不会破坏旧 `.env` 复用流程。 + ## 架构检查 关键脚本会读取: diff --git a/deploy/arm64-installer/scripts/common.sh b/deploy/arm64-installer/scripts/common.sh index ee6077a..1d6566a 100644 --- a/deploy/arm64-installer/scripts/common.sh +++ b/deploy/arm64-installer/scripts/common.sh @@ -203,12 +203,14 @@ load_env() { GRAFANA_IMAGE="${GRAFANA_IMAGE:-grafana/grafana:11.3.1}" METRICS_INSTANCE="${METRICS_INSTANCE:-${PACKAGE_ARCH}-installer}" ALLOW_CROSS_ARCH="${ALLOW_CROSS_ARCH:-0}" + RESOURCE_VALIDATION_MODE="${RESOURCE_VALIDATION_MODE:-validation-update-03}" FIRST_RUN_WAIT_TIMEOUT_SECS="${FIRST_RUN_WAIT_TIMEOUT_SECS:-7200}" persist_env_default "$ENV_FILE" "METRICS_IMAGE" "$METRICS_IMAGE" persist_env_default "$ENV_FILE" "METRICS_PLATFORM" "$METRICS_PLATFORM" persist_env_default "$ENV_FILE" "RTR_REPORT_DIR" "$RTR_REPORT_DIR" persist_env_default "$ENV_FILE" "RTR_REPORT_CONTAINER_DIR" "$RTR_REPORT_CONTAINER_DIR" + persist_env_default "$ENV_FILE" "RESOURCE_VALIDATION_MODE" "$RESOURCE_VALIDATION_MODE" } compose_cmd() { diff --git a/scripts/soak/portable-soak.env.example b/scripts/soak/portable-soak.env.example index 88bd14d..5ab7d44 100644 --- a/scripts/soak/portable-soak.env.example +++ b/scripts/soak/portable-soak.env.example @@ -81,6 +81,10 @@ RPKI_PROGRESS_PP_CONTROL_SLOW_MS=100 # 是否在运行前尝试禁用 rpki-client timer 并杀掉竞争 RP 进程。 DISABLE_COMPETING_RPS=1 +# 资源证书验证模式;会显式传给 rpki 子进程。 +# 合法值:validation-update-03、rfc6487。 +RESOURCE_VALIDATION_MODE=validation-update-03 + # 是否启用实验性 child CRT 验证缓存。独立于 ROA cache / PP cache;默认关闭。 # 开启后会额外传入 --enable-child-certificate-validation-cache。 ENABLE_CHILD_CERTIFICATE_VALIDATION_CACHE=0 diff --git a/scripts/soak/run_soak.sh b/scripts/soak/run_soak.sh index 6f7f66d..e839d32 100755 --- a/scripts/soak/run_soak.sh +++ b/scripts/soak/run_soak.sh @@ -15,6 +15,7 @@ INTERVAL_SECS="${INTERVAL_SECS:-0}" STOP_AFTER_SECS="${STOP_AFTER_SECS:-0}" RIRS="${RIRS:-afrinic,apnic,arin,lacnic,ripe}" TAL_INPUT_MODE="${TAL_INPUT_MODE:-file-with-ta}" +RESOURCE_VALIDATION_MODE="${RESOURCE_VALIDATION_MODE:-validation-update-03}" RUN_ROOT="${RUN_ROOT:-$PACKAGE_ROOT}" RETAIN_RUNS="${RETAIN_RUNS:-10}" CLEAN_TMP_AFTER_RUN="${CLEAN_TMP_AFTER_RUN:-0}" @@ -125,6 +126,16 @@ validate_tal_input_mode() { esac } +validate_resource_validation_mode() { + case "$RESOURCE_VALIDATION_MODE" in + validation-update-03|rfc6487) + ;; + *) + die "RESOURCE_VALIDATION_MODE must be validation-update-03 or rfc6487: $RESOURCE_VALIDATION_MODE" + ;; + esac +} + normalize_token() { local token="$1" token="${token#"${token%%[![:space:]]*}"}" @@ -1038,6 +1049,7 @@ build_child_args() { --db "$DB_DIR/work-db" --repo-bytes-db "$DB_DIR/repo-bytes.db" --rsync-scope "$RSYNC_SCOPE" + --resource-validation-mode "$RESOURCE_VALIDATION_MODE" ) if is_true "$ALLOW_RSYNC_MIRROR_REUSE"; then CHILD_ARGS+=(--rsync-mirror-root "$RSYNC_MIRROR_ROOT") @@ -1326,6 +1338,7 @@ main() { validate_positive_int "RETAIN_RUNS" "$RETAIN_RUNS" validate_rsync_scope validate_tal_input_mode + validate_resource_validation_mode validate_non_negative_int "PERIODIC_SNAPSHOT_MAX_DELTAS" "$PERIODIC_SNAPSHOT_MAX_DELTAS" if [[ -n "${DB_STATS_EXACT_EVERY:-}" && "$DB_STATS_EXACT_EVERY" != "0" ]]; then validate_positive_int "DB_STATS_EXACT_EVERY" "$DB_STATS_EXACT_EVERY" diff --git a/src/cli.rs b/src/cli.rs index e1685f0..d4cf8df 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -24,7 +24,7 @@ use crate::memory_telemetry::{ }; use crate::parallel::config::{ParallelPhase1Config, ParallelPhase2Config}; use crate::parallel::types::TalInputSpec; -use crate::policy::{Policy, StrictPolicy}; +use crate::policy::{Policy, ResourceValidationMode, StrictPolicy}; use crate::storage::{RocksStore, VcirStorageSummary}; use crate::validation::run_tree_from_tal::{ RunTreeFromTalAuditOutput, run_tree_from_multiple_tals_parallel_phase2_audit, @@ -130,6 +130,7 @@ pub struct CliArgs { pub repo_bytes_db: Option, pub policy_path: Option, pub strict_policy: Option, + pub resource_validation_mode: Option, pub report_json_path: Option, pub report_json_compact: bool, pub skip_report_build: bool, @@ -189,6 +190,8 @@ Options: --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) + --resource-validation-mode + Resource certificate validation mode (default: validation-update-03) --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 @@ -278,6 +281,7 @@ pub fn parse_args(argv: &[String]) -> Result { let mut repo_bytes_db: Option = None; let mut policy_path: Option = None; let mut strict_policy: Option = None; + let mut resource_validation_mode: Option = None; let mut report_json_path: Option = None; let mut report_json_compact: bool = false; let mut skip_report_build: bool = false; @@ -475,6 +479,19 @@ pub fn parse_args(argv: &[String]) -> Result { let spec = arg.strip_prefix("--strict=").expect("prefix checked"); strict_policy = Some(StrictPolicy::parse_cli_spec(Some(spec))?); } + "--resource-validation-mode" => { + i += 1; + let v = argv + .get(i) + .ok_or("--resource-validation-mode requires a value")?; + resource_validation_mode = Some(ResourceValidationMode::parse_cli_value(v)?); + } + _ if arg.starts_with("--resource-validation-mode=") => { + let spec = arg + .strip_prefix("--resource-validation-mode=") + .expect("prefix checked"); + resource_validation_mode = Some(ResourceValidationMode::parse_cli_value(spec)?); + } "--report-json" => { i += 1; let v = argv.get(i).ok_or("--report-json requires a value")?; @@ -929,6 +946,7 @@ pub fn parse_args(argv: &[String]) -> Result { repo_bytes_db, policy_path, strict_policy, + resource_validation_mode, report_json_path, report_json_compact, skip_report_build, @@ -1974,6 +1992,9 @@ pub fn run(argv: &[String]) -> Result<(), String> { if let Some(strict_policy) = args.strict_policy { policy.strict = strict_policy; } + if let Some(resource_validation_mode) = args.resource_validation_mode { + policy.resource_validation_mode = resource_validation_mode; + } if args.disable_rrdp { policy.sync_preference = crate::policy::SyncPreference::RsyncOnly; } diff --git a/src/cli/tests.rs b/src/cli/tests.rs index 6ec038b..86c9005 100644 --- a/src/cli/tests.rs +++ b/src/cli/tests.rs @@ -2,6 +2,7 @@ use super::*; use crate::memory_telemetry::{ MemoryTelemetryCheckpoint, MemoryTelemetrySummary, ProcessMemorySnapshot, }; +use crate::policy::ResourceValidationMode; use crate::storage::{ RocksDbMemorySnapshot, RocksDbMemoryTotals, VcirCcrProjectionSizeBreakdown, VcirChildResourceSizeBreakdown, VcirCoreFieldSizeBreakdown, VcirFieldSizeBreakdown, @@ -19,6 +20,7 @@ fn parse_help_returns_usage() { assert!(err.contains("--parallel-phase2-object-workers"), "{err}"); assert!(err.contains("--memory-trim-after-validation"), "{err}"); assert!(err.contains("--enable-roa-validation-cache"), "{err}"); + assert!(err.contains("--resource-validation-mode"), "{err}"); assert!( err.contains("--publication-point-cache-observe-only"), "{err}" @@ -340,6 +342,39 @@ fn parse_accepts_strict_without_value_as_all() { assert_eq!(args.strict_policy, Some(StrictPolicy::all())); } +#[test] +fn parse_accepts_resource_validation_mode() { + let argv = vec![ + "rpki".to_string(), + "--db".to_string(), + "db".to_string(), + "--tal-url".to_string(), + "https://example.test/x.tal".to_string(), + "--resource-validation-mode".to_string(), + "rfc6487".to_string(), + ]; + let args = parse_args(&argv).expect("parse args"); + assert_eq!( + args.resource_validation_mode, + Some(ResourceValidationMode::Rfc6487) + ); +} + +#[test] +fn parse_rejects_unknown_resource_validation_mode() { + let argv = vec![ + "rpki".to_string(), + "--db".to_string(), + "db".to_string(), + "--tal-url".to_string(), + "https://example.test/x.tal".to_string(), + "--resource-validation-mode".to_string(), + "bogus".to_string(), + ]; + let err = parse_args(&argv).expect_err("unknown mode should fail"); + assert!(err.contains("unknown resource validation mode"), "{err}"); +} + #[test] fn parse_rejects_unknown_strict_policy() { let argv = vec![ @@ -1407,6 +1442,10 @@ fn read_policy_accepts_valid_toml() { policy.signed_object_failure_policy, crate::policy::SignedObjectFailurePolicy::DropPublicationPoint ); + assert_eq!( + policy.resource_validation_mode, + ResourceValidationMode::ValidationUpdate03 + ); assert_eq!(policy.strict, StrictPolicy::default()); } @@ -1435,6 +1474,19 @@ fn read_policy_accepts_strict_table() { ); } +#[test] +fn read_policy_accepts_resource_validation_mode() { + let dir = tempfile::tempdir().expect("tmpdir"); + let p = dir.path().join("policy.toml"); + std::fs::write(&p, "resource_validation_mode = \"rfc6487\"\n").expect("write policy"); + + let policy = read_policy(Some(&p)).expect("parse policy"); + assert_eq!( + policy.resource_validation_mode, + ResourceValidationMode::Rfc6487 + ); +} + #[test] fn read_policy_reports_missing_file() { let dir = tempfile::tempdir().expect("tmpdir"); diff --git a/src/data_model/rc.rs b/src/data_model/rc.rs index 222b7c9..2a0329a 100644 --- a/src/data_model/rc.rs +++ b/src/data_model/rc.rs @@ -740,6 +740,29 @@ mod strict_name_tests { let err = validate_strict_rpki_name(&name, "subject").expect_err("strict name fails"); assert!(err.to_string().contains("exactly once"), "{err}"); } + + #[test] + fn profile_rejects_rfc8360_v2_policy_oid() { + let extensions = RcExtensionsParsed { + basic_constraints_ca: vec![true], + subject_key_identifier: Vec::new(), + authority_key_identifier: Vec::new(), + crl_distribution_points: Vec::new(), + authority_info_access: Vec::new(), + subject_info_access: Vec::new(), + certificate_policies: vec![(vec!["1.3.6.1.5.5.7.14.3".to_string()], true)], + ip_resources: Vec::new(), + as_resources: Vec::new(), + }; + + let err = extensions + .validate_profile(true) + .expect_err("v2 policy OID must remain invalid"); + assert!( + matches!(&err, ResourceCertificateProfileError::InvalidCertificatePolicy(oid) if oid == "1.3.6.1.5.5.7.14.3"), + "{err}" + ); + } } impl ResourceCertificateParsed { diff --git a/src/policy.rs b/src/policy.rs index ef8af5a..59a9c31 100644 --- a/src/policy.rs +++ b/src/policy.rs @@ -39,6 +39,32 @@ impl Default for SignedObjectFailurePolicy { } } +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum ResourceValidationMode { + #[serde(rename = "validation-update-03")] + ValidationUpdate03, + #[serde(rename = "rfc6487")] + Rfc6487, +} + +impl Default for ResourceValidationMode { + fn default() -> Self { + Self::ValidationUpdate03 + } +} + +impl ResourceValidationMode { + pub fn parse_cli_value(raw: &str) -> Result { + match raw.trim() { + "validation-update-03" => Ok(Self::ValidationUpdate03), + "rfc6487" => Ok(Self::Rfc6487), + value => Err(format!( + "unknown resource validation mode: {value}; supported: validation-update-03,rfc6487" + )), + } + } +} + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(default)] pub struct StrictPolicy { @@ -99,6 +125,7 @@ pub struct Policy { pub sync_preference: SyncPreference, pub ca_failed_fetch_policy: CaFailedFetchPolicy, pub signed_object_failure_policy: SignedObjectFailurePolicy, + pub resource_validation_mode: ResourceValidationMode, pub strict: StrictPolicy, } @@ -108,6 +135,7 @@ impl Default for Policy { sync_preference: SyncPreference::default(), ca_failed_fetch_policy: CaFailedFetchPolicy::default(), signed_object_failure_policy: SignedObjectFailurePolicy::default(), + resource_validation_mode: ResourceValidationMode::default(), strict: StrictPolicy::default(), } } @@ -150,6 +178,19 @@ mod tests { assert!(StrictPolicy::parse_cli_spec(Some("bogus")).is_err()); } + #[test] + fn resource_validation_mode_parses_cli_values() { + assert_eq!( + ResourceValidationMode::parse_cli_value("validation-update-03").unwrap(), + ResourceValidationMode::ValidationUpdate03 + ); + assert_eq!( + ResourceValidationMode::parse_cli_value("rfc6487").unwrap(), + ResourceValidationMode::Rfc6487 + ); + assert!(ResourceValidationMode::parse_cli_value("bogus").is_err()); + } + #[test] fn policy_toml_accepts_strict_table() { let policy = Policy::from_toml_str( @@ -170,4 +211,18 @@ mod tests { } ); } + + #[test] + fn policy_toml_accepts_resource_validation_mode() { + let policy = Policy::from_toml_str( + r#" + resource_validation_mode = "rfc6487" + "#, + ) + .expect("parse policy"); + assert_eq!( + policy.resource_validation_mode, + ResourceValidationMode::Rfc6487 + ); + } } diff --git a/src/validation/ca_path.rs b/src/validation/ca_path.rs index 179f374..2db4166 100644 --- a/src/validation/ca_path.rs +++ b/src/validation/ca_path.rs @@ -5,6 +5,7 @@ use crate::data_model::rc::{ AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpResourceSet, ResourceCertKind, ResourceCertificate, ResourceCertificateDecodeError, }; +use crate::policy::ResourceValidationMode; use x509_parser::prelude::{FromDer, X509Certificate}; use crate::validation::x509_name::x509_names_equivalent; @@ -18,6 +19,7 @@ pub struct ValidatedSubordinateCa { pub issuer_crl: RpkixCrl, pub effective_ip_resources: Option, pub effective_as_resources: Option, + pub resource_warnings: ResourceValidationWarnings, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -25,6 +27,41 @@ pub struct ValidatedSubordinateCaLite { pub child_ca: ResourceCertificate, pub effective_ip_resources: Option, pub effective_as_resources: Option, + pub resource_warnings: ResourceValidationWarnings, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct ResourceValidationWarnings { + pub ip_reduced_by_vrs: bool, + pub as_reduced_by_vrs: bool, + pub ip_vrs_empty: bool, + pub as_vrs_empty: bool, +} + +impl ResourceValidationWarnings { + pub fn is_empty(&self) -> bool { + !self.ip_reduced_by_vrs + && !self.as_reduced_by_vrs + && !self.ip_vrs_empty + && !self.as_vrs_empty + } + + pub fn summary(&self) -> String { + let mut parts = Vec::new(); + if self.ip_reduced_by_vrs { + parts.push("ip_reduced_by_vrs"); + } + if self.as_reduced_by_vrs { + parts.push("as_reduced_by_vrs"); + } + if self.ip_vrs_empty { + parts.push("ip_vrs_empty"); + } + if self.as_vrs_empty { + parts.push("as_vrs_empty"); + } + parts.join(",") + } } #[derive(Clone, Debug, Default)] @@ -178,6 +215,30 @@ pub fn validate_subordinate_ca_cert( issuer_effective_ip: Option<&IpResourceSet>, issuer_effective_as: Option<&AsResourceSet>, validation_time: time::OffsetDateTime, +) -> Result { + validate_subordinate_ca_cert_with_resource_validation_mode( + child_ca_der, + issuer_ca_der, + issuer_crl_der, + issuer_ca_rsync_uri, + issuer_crl_rsync_uri, + issuer_effective_ip, + issuer_effective_as, + validation_time, + ResourceValidationMode::Rfc6487, + ) +} + +pub fn validate_subordinate_ca_cert_with_resource_validation_mode( + child_ca_der: &[u8], + issuer_ca_der: &[u8], + issuer_crl_der: &[u8], + issuer_ca_rsync_uri: Option<&str>, + issuer_crl_rsync_uri: &str, + issuer_effective_ip: Option<&IpResourceSet>, + issuer_effective_as: Option<&AsResourceSet>, + validation_time: time::OffsetDateTime, + resource_validation_mode: ResourceValidationMode, ) -> Result { let child_ca = ResourceCertificate::decode_der(child_ca_der)?; if child_ca.kind != ResourceCertKind::Ca { @@ -230,13 +291,20 @@ pub fn validate_subordinate_ca_cert( return Err(CaPathError::ChildRevoked); } - let effective_ip_resources = resolve_child_ip_resources( + let ResourceResolution { + effective_ip_resources, + effective_as_resources, + warnings: resource_warnings, + } = resolve_child_resources( child_ca.tbs.extensions.ip_resources.as_ref(), issuer_effective_ip, - )?; - let effective_as_resources = resolve_child_as_resources( child_ca.tbs.extensions.as_resources.as_ref(), issuer_effective_as, + &IssuerEffectiveResourcesIndex::from_effective_resources( + issuer_effective_ip, + issuer_effective_as, + )?, + resource_validation_mode, )?; if effective_ip_resources.is_none() && effective_as_resources.is_none() { return Err(CaPathError::ResourcesMissing); @@ -248,6 +316,7 @@ pub fn validate_subordinate_ca_cert( issuer_crl, effective_ip_resources, effective_as_resources, + resource_warnings, }) } @@ -285,6 +354,7 @@ pub fn validate_subordinate_ca_cert_with_prevalidated_issuer( issuer_effective_as, &issuer_resources_index, validation_time, + ResourceValidationMode::Rfc6487, ) } @@ -301,6 +371,7 @@ pub fn validate_subordinate_ca_cert_with_prevalidated_issuer_and_resources( issuer_effective_as: Option<&AsResourceSet>, issuer_resources_index: &IssuerEffectiveResourcesIndex, validation_time: time::OffsetDateTime, + resource_validation_mode: ResourceValidationMode, ) -> Result { if child_ca.kind != ResourceCertKind::Ca { return Err(CaPathError::ChildNotCa); @@ -347,17 +418,17 @@ pub fn validate_subordinate_ca_cert_with_prevalidated_issuer_and_resources( return Err(CaPathError::ChildRevoked); } - let effective_ip_resources = resolve_child_ip_resources_indexed( + let ResourceResolution { + effective_ip_resources, + effective_as_resources, + warnings: resource_warnings, + } = resolve_child_resources( child_ca.tbs.extensions.ip_resources.as_ref(), issuer_effective_ip, - issuer_resources_index.parent_ip_by_afi_items.as_ref(), - &issuer_resources_index.parent_ip_merged_intervals, - )?; - let effective_as_resources = resolve_child_as_resources_indexed( child_ca.tbs.extensions.as_resources.as_ref(), issuer_effective_as, - issuer_resources_index.parent_asnum_intervals.as_deref(), - issuer_resources_index.parent_rdi_intervals.as_deref(), + issuer_resources_index, + resource_validation_mode, )?; if effective_ip_resources.is_none() && effective_as_resources.is_none() { return Err(CaPathError::ResourcesMissing); @@ -367,6 +438,7 @@ pub fn validate_subordinate_ca_cert_with_prevalidated_issuer_and_resources( child_ca, effective_ip_resources, effective_as_resources, + resource_warnings, }) } @@ -506,6 +578,66 @@ fn is_serial_revoked_by_crl(cert: &ResourceCertificate, crl: &RpkixCrl) -> bool .any(|rc| rc.serial_number == serial) } +#[derive(Clone, Debug)] +struct ResourceResolution { + effective_ip_resources: Option, + effective_as_resources: Option, + warnings: ResourceValidationWarnings, +} + +fn resolve_child_resources( + child_ip: Option<&IpResourceSet>, + issuer_effective_ip: Option<&IpResourceSet>, + child_as: Option<&AsResourceSet>, + issuer_effective_as: Option<&AsResourceSet>, + issuer_resources_index: &IssuerEffectiveResourcesIndex, + mode: ResourceValidationMode, +) -> Result { + match mode { + ResourceValidationMode::Rfc6487 => Ok(ResourceResolution { + effective_ip_resources: resolve_child_ip_resources_indexed( + child_ip, + issuer_effective_ip, + issuer_resources_index.parent_ip_by_afi_items.as_ref(), + &issuer_resources_index.parent_ip_merged_intervals, + )?, + effective_as_resources: resolve_child_as_resources_indexed( + child_as, + issuer_effective_as, + issuer_resources_index.parent_asnum_intervals.as_deref(), + issuer_resources_index.parent_rdi_intervals.as_deref(), + )?, + warnings: ResourceValidationWarnings::default(), + }), + ResourceValidationMode::ValidationUpdate03 => { + let (effective_ip_resources, ip_reduced_by_vrs, ip_vrs_empty) = + resolve_child_ip_resources_vrs( + child_ip, + issuer_effective_ip, + issuer_resources_index.parent_ip_by_afi_items.as_ref(), + &issuer_resources_index.parent_ip_merged_intervals, + )?; + let (effective_as_resources, as_reduced_by_vrs, as_vrs_empty) = + resolve_child_as_resources_vrs( + child_as, + issuer_effective_as, + issuer_resources_index.parent_asnum_intervals.as_deref(), + issuer_resources_index.parent_rdi_intervals.as_deref(), + )?; + Ok(ResourceResolution { + effective_ip_resources, + effective_as_resources, + warnings: ResourceValidationWarnings { + ip_reduced_by_vrs, + as_reduced_by_vrs, + ip_vrs_empty, + as_vrs_empty, + }, + }) + } + } +} + fn resolve_child_ip_resources( child_ip: Option<&IpResourceSet>, issuer_effective: Option<&IpResourceSet>, @@ -581,6 +713,77 @@ fn resolve_child_ip_resources_indexed( })) } +fn resolve_child_ip_resources_vrs( + child_ip: Option<&IpResourceSet>, + issuer_effective: Option<&IpResourceSet>, + parent_by_afi: Option< + &BTreeMap>, + >, + parent_intervals_by_afi: &HashMap, Vec)>>, +) -> Result<(Option, bool, bool), CaPathError> { + let Some(child_ip) = child_ip else { + return Ok((None, false, false)); + }; + if child_ip.has_any_inherit() && issuer_effective.is_none() { + return Err(CaPathError::InheritWithoutParentResources); + } + + let parent_by_afi = parent_by_afi.unwrap_or_else(|| { + static EMPTY: std::sync::OnceLock< + BTreeMap>, + > = std::sync::OnceLock::new(); + EMPTY.get_or_init(BTreeMap::new) + }); + let mut out_families = Vec::new(); + let mut reduced = false; + let mut saw_declared = false; + + for fam in &child_ip.families { + match &fam.choice { + IpAddressChoice::Inherit => { + let items = parent_by_afi + .get(&fam.afi) + .ok_or(CaPathError::InheritWithoutParentResources)?; + out_families.push(crate::data_model::rc::IpAddressFamily { + afi: fam.afi, + choice: IpAddressChoice::AddressesOrRanges(items.clone()), + }); + } + IpAddressChoice::AddressesOrRanges(items) => { + saw_declared = saw_declared || !items.is_empty(); + let parent_intervals = parent_intervals_by_afi + .get(&fam.afi) + .map(Vec::as_slice) + .unwrap_or(&[]); + let intersections = + intersect_ip_items_with_parent_intervals(items, parent_intervals); + let child_intervals = ip_items_to_merged_intervals(items); + if intervals_changed(&child_intervals, &intersections) { + reduced = true; + } + if !intersections.is_empty() { + out_families.push(crate::data_model::rc::IpAddressFamily { + afi: fam.afi, + choice: IpAddressChoice::AddressesOrRanges(ip_intervals_to_ranges( + fam.afi, + &intersections, + )), + }); + } + } + } + } + + let empty = saw_declared && out_families.is_empty(); + Ok(( + Some(IpResourceSet { + families: out_families, + }), + reduced, + empty, + )) +} + fn resolve_child_as_resources( child_as: Option<&AsResourceSet>, issuer_effective: Option<&AsResourceSet>, @@ -656,6 +859,80 @@ fn resolve_child_as_resources_indexed( Ok(Some(AsResourceSet { asnum, rdi })) } +fn resolve_child_as_resources_vrs( + child_as: Option<&AsResourceSet>, + issuer_effective: Option<&AsResourceSet>, + parent_asnum_intervals: Option<&[(u32, u32)]>, + parent_rdi_intervals: Option<&[(u32, u32)]>, +) -> Result<(Option, bool, bool), CaPathError> { + let Some(child_as) = child_as else { + return Ok((None, false, false)); + }; + if issuer_effective.is_none() + && (matches!(child_as.asnum, Some(AsIdentifierChoice::Inherit)) + || matches!(child_as.rdi, Some(AsIdentifierChoice::Inherit))) + { + return Err(CaPathError::InheritWithoutParentResources); + } + + let (asnum, asnum_reduced, asnum_empty) = resolve_as_choice_vrs( + child_as.asnum.as_ref(), + issuer_effective.and_then(|p| p.asnum.as_ref()), + parent_asnum_intervals, + )?; + let (rdi, rdi_reduced, rdi_empty) = resolve_as_choice_vrs( + child_as.rdi.as_ref(), + issuer_effective.and_then(|p| p.rdi.as_ref()), + parent_rdi_intervals, + )?; + Ok(( + Some(AsResourceSet { asnum, rdi }), + asnum_reduced || rdi_reduced, + asnum_empty || rdi_empty, + )) +} + +fn resolve_as_choice_vrs( + child: Option<&AsIdentifierChoice>, + parent: Option<&AsIdentifierChoice>, + parent_intervals_hint: Option<&[(u32, u32)]>, +) -> Result<(Option, bool, bool), CaPathError> { + let Some(child) = child else { + return Ok((None, false, false)); + }; + match child { + AsIdentifierChoice::Inherit => { + let parent = parent + .cloned() + .ok_or(CaPathError::InheritWithoutParentResources)?; + Ok((Some(parent), false, false)) + } + AsIdentifierChoice::AsIdsOrRanges(_) => { + let child_intervals = as_choice_to_merged_intervals(child); + let parent_intervals; + let parent_intervals = match parent_intervals_hint { + Some(v) => v, + None => { + parent_intervals = parent + .map(as_choice_to_merged_intervals) + .unwrap_or_default(); + parent_intervals.as_slice() + } + }; + let intersections = intersect_as_intervals(&child_intervals, parent_intervals); + let reduced = child_intervals != intersections; + let empty = !child_intervals.is_empty() && intersections.is_empty(); + Ok(( + Some(AsIdentifierChoice::AsIdsOrRanges(as_intervals_to_items( + &intersections, + ))), + reduced, + empty, + )) + } + } +} + fn as_choice_subset( child: Option<&AsIdentifierChoice>, parent: Option<&AsIdentifierChoice>, @@ -911,6 +1188,134 @@ fn ip_family_items_subset_with_parent_intervals( true } +fn ip_items_to_merged_intervals( + items: &[crate::data_model::rc::IpAddressOrRange], +) -> Vec<(Vec, Vec)> { + let mut intervals = Vec::new(); + for item in items { + match item { + crate::data_model::rc::IpAddressOrRange::Prefix(p) => { + intervals.push(prefix_to_range(p)) + } + crate::data_model::rc::IpAddressOrRange::Range(r) => { + intervals.push((r.min.clone(), r.max.clone())) + } + } + } + intervals.sort_by(|(a, _), (b, _)| a.cmp(b)); + merge_ip_intervals(&intervals) +} + +fn intersect_ip_items_with_parent_intervals( + items: &[crate::data_model::rc::IpAddressOrRange], + parent_intervals: &[(Vec, Vec)], +) -> Vec<(Vec, Vec)> { + let child_intervals = ip_items_to_merged_intervals(items); + intersect_ip_intervals(&child_intervals, parent_intervals) +} + +fn intersect_ip_intervals( + child_intervals: &[(Vec, Vec)], + parent_intervals: &[(Vec, Vec)], +) -> Vec<(Vec, Vec)> { + let mut out = Vec::new(); + let mut parent_index = 0usize; + for (child_min, child_max) in child_intervals { + while parent_index < parent_intervals.len() + && parent_intervals[parent_index].1.as_slice() < child_min.as_slice() + { + parent_index += 1; + } + let mut scan = parent_index; + while scan < parent_intervals.len() + && parent_intervals[scan].0.as_slice() <= child_max.as_slice() + { + let (parent_min, parent_max) = &parent_intervals[scan]; + let min = if bytes_leq(child_min, parent_min) { + parent_min.clone() + } else { + child_min.clone() + }; + let max = if bytes_leq(child_max, parent_max) { + child_max.clone() + } else { + parent_max.clone() + }; + if bytes_leq(&min, &max) { + out.push((min, max)); + } + scan += 1; + } + } + merge_ip_intervals(&out) +} + +fn ip_intervals_to_ranges( + afi: crate::data_model::rc::Afi, + intervals: &[(Vec, Vec)], +) -> Vec { + intervals + .iter() + .map(|(min, max)| { + crate::data_model::rc::IpAddressOrRange::Range(crate::data_model::rc::IpAddressRange { + min: normalize_ip_bytes(afi, min), + max: normalize_ip_bytes(afi, max), + }) + }) + .collect() +} + +fn normalize_ip_bytes(afi: crate::data_model::rc::Afi, bytes: &[u8]) -> Vec { + let target_len = afi.octets_len(); + if bytes.len() == target_len { + return bytes.to_vec(); + } + let mut out = vec![0u8; target_len]; + let copy_len = bytes.len().min(target_len); + out[..copy_len].copy_from_slice(&bytes[..copy_len]); + out +} + +fn intervals_changed(left: &[T], right: &[T]) -> bool { + left != right +} + +fn intersect_as_intervals(child: &[(u32, u32)], parent: &[(u32, u32)]) -> Vec<(u32, u32)> { + let mut out = Vec::new(); + let mut parent_index = 0usize; + for (child_min, child_max) in child { + while parent_index < parent.len() && parent[parent_index].1 < *child_min { + parent_index += 1; + } + let mut scan = parent_index; + while scan < parent.len() && parent[scan].0 <= *child_max { + let min = (*child_min).max(parent[scan].0); + let max = (*child_max).min(parent[scan].1); + if min <= max { + out.push((min, max)); + } + scan += 1; + } + } + merge_as_intervals(&out) +} + +fn as_intervals_to_items(intervals: &[(u32, u32)]) -> Vec { + intervals + .iter() + .map(|(min, max)| { + if min == max { + crate::data_model::rc::AsIdOrRange::Id(*min) + } else { + crate::data_model::rc::AsIdOrRange::Range { + min: *min, + max: *max, + } + } + }) + .collect() +} + fn merge_ip_intervals(v: &[(Vec, Vec)]) -> Vec<(Vec, Vec)> { let mut out: Vec<(Vec, Vec)> = Vec::new(); for (min, max) in v { @@ -971,8 +1376,8 @@ mod tests { use super::*; use crate::data_model::common::X509NameDer; use crate::data_model::rc::{ - Afi, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily, IpAddressOrRange, - IpResourceSet, + Afi, AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily, + IpAddressOrRange, IpPrefix, IpResourceSet, }; use crate::data_model::rc::{ RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate, @@ -1225,6 +1630,233 @@ mod tests { assert!(matches!(err, CaPathError::InheritWithoutParentResources)); } + #[test] + fn validation_update_03_intersects_overclaiming_child_ip_resources() { + let issuer = IpResourceSet { + families: vec![IpAddressFamily { + afi: Afi::Ipv4, + choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix( + IpPrefix { + afi: Afi::Ipv4, + prefix_len: 24, + addr: vec![10, 0, 0, 0], + }, + )]), + }], + }; + let child = IpResourceSet { + families: vec![IpAddressFamily { + afi: Afi::Ipv4, + choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix( + IpPrefix { + afi: Afi::Ipv4, + prefix_len: 23, + addr: vec![10, 0, 0, 0], + }, + )]), + }], + }; + let index = IssuerEffectiveResourcesIndex::from_effective_resources(Some(&issuer), None) + .expect("index"); + let resolved = resolve_child_resources( + Some(&child), + Some(&issuer), + None, + None, + &index, + ResourceValidationMode::ValidationUpdate03, + ) + .expect("vrs resolution"); + assert!(resolved.warnings.ip_reduced_by_vrs); + assert!(!resolved.warnings.ip_vrs_empty); + let effective = resolved.effective_ip_resources.expect("effective ip"); + assert!(effective.families[0].contains_prefix(&IpPrefix { + afi: Afi::Ipv4, + prefix_len: 24, + addr: vec![10, 0, 0, 0], + })); + assert!(!effective.families[0].contains_prefix(&IpPrefix { + afi: Afi::Ipv4, + prefix_len: 24, + addr: vec![10, 0, 1, 0], + })); + } + + #[test] + fn validation_update_03_keeps_empty_vrs_instead_of_rejecting_child_ca() { + let issuer = IpResourceSet { + families: vec![IpAddressFamily { + afi: Afi::Ipv4, + choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix( + IpPrefix { + afi: Afi::Ipv4, + prefix_len: 24, + addr: vec![10, 0, 0, 0], + }, + )]), + }], + }; + let child = IpResourceSet { + families: vec![IpAddressFamily { + afi: Afi::Ipv4, + choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix( + IpPrefix { + afi: Afi::Ipv4, + prefix_len: 24, + addr: vec![192, 0, 2, 0], + }, + )]), + }], + }; + let index = IssuerEffectiveResourcesIndex::from_effective_resources(Some(&issuer), None) + .expect("index"); + let strict_err = resolve_child_resources( + Some(&child), + Some(&issuer), + None, + None, + &index, + ResourceValidationMode::Rfc6487, + ) + .unwrap_err(); + assert!(matches!(strict_err, CaPathError::ResourcesNotSubset)); + + let resolved = resolve_child_resources( + Some(&child), + Some(&issuer), + None, + None, + &index, + ResourceValidationMode::ValidationUpdate03, + ) + .expect("vrs resolution"); + assert!(resolved.warnings.ip_reduced_by_vrs); + assert!(resolved.warnings.ip_vrs_empty); + assert!( + resolved + .effective_ip_resources + .expect("effective ip") + .families + .is_empty() + ); + } + + #[test] + fn validation_update_03_intersects_overclaiming_child_as_resources() { + let issuer = AsResourceSet { + asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![ + AsIdOrRange::Range { + min: 64500, + max: 64510, + }, + ])), + rdi: None, + }; + let child = AsResourceSet { + asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![ + AsIdOrRange::Range { + min: 64505, + max: 64520, + }, + ])), + rdi: None, + }; + let index = IssuerEffectiveResourcesIndex::from_effective_resources(None, Some(&issuer)) + .expect("index"); + let resolved = resolve_child_resources( + None, + None, + Some(&child), + Some(&issuer), + &index, + ResourceValidationMode::ValidationUpdate03, + ) + .expect("vrs resolution"); + assert!(resolved.warnings.as_reduced_by_vrs); + assert!(!resolved.warnings.as_vrs_empty); + let effective = resolved.effective_as_resources.expect("effective as"); + assert_eq!( + effective.asnum, + Some(AsIdentifierChoice::AsIdsOrRanges(vec![ + AsIdOrRange::Range { + min: 64505, + max: 64510, + }, + ])) + ); + } + + #[test] + fn validation_update_03_records_all_resource_warning_summary_parts() { + let issuer_ip = IpResourceSet { + families: vec![IpAddressFamily { + afi: Afi::Ipv4, + choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix( + IpPrefix { + afi: Afi::Ipv4, + prefix_len: 24, + addr: vec![10, 0, 0, 0], + }, + )]), + }], + }; + let child_ip = IpResourceSet { + families: vec![IpAddressFamily { + afi: Afi::Ipv4, + choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix( + IpPrefix { + afi: Afi::Ipv4, + prefix_len: 24, + addr: vec![192, 0, 2, 0], + }, + )]), + }], + }; + let issuer_as = AsResourceSet { + asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![ + AsIdOrRange::Range { + min: 64500, + max: 64510, + }, + ])), + rdi: None, + }; + let child_as = AsResourceSet { + asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![ + AsIdOrRange::Range { + min: 64520, + max: 64530, + }, + ])), + rdi: None, + }; + let index = IssuerEffectiveResourcesIndex::from_effective_resources( + Some(&issuer_ip), + Some(&issuer_as), + ) + .expect("index"); + + let resolved = resolve_child_resources( + Some(&child_ip), + Some(&issuer_ip), + Some(&child_as), + Some(&issuer_as), + &index, + ResourceValidationMode::ValidationUpdate03, + ) + .expect("vrs resolution"); + + assert!(resolved.warnings.ip_reduced_by_vrs); + assert!(resolved.warnings.ip_vrs_empty); + assert!(resolved.warnings.as_reduced_by_vrs); + assert!(resolved.warnings.as_vrs_empty); + assert_eq!( + resolved.warnings.summary(), + "ip_reduced_by_vrs,as_reduced_by_vrs,ip_vrs_empty,as_vrs_empty" + ); + assert!(!resolved.warnings.is_empty()); + } + #[test] fn child_aki_mismatch_is_rejected() { let issuer = dummy_cert( diff --git a/src/validation/objects.rs b/src/validation/objects.rs index e3e3b95..afce190 100644 --- a/src/validation/objects.rs +++ b/src/validation/objects.rs @@ -13,7 +13,7 @@ use crate::parallel::config::ParallelPhase2Config; use crate::parallel::object_worker::{ ObjectTaskExecutor, ObjectWorkerPool, ObjectWorkerSubmitError, }; -use crate::policy::{Policy, SignedObjectFailurePolicy}; +use crate::policy::{Policy, ResourceValidationMode, SignedObjectFailurePolicy}; use crate::report::{RfcRef, Warning}; use crate::storage::{ PackFile, PackTime, RoaCacheObjectMeta, RoaCacheProjection, VcirLocalOutput, @@ -1226,6 +1226,7 @@ pub fn process_publication_point_for_issuer_with_cache_options { stats.aspa_ok += 1; @@ -1735,6 +1739,7 @@ pub(crate) struct RoaTaskShared { issuer_resources_index: Arc, issuer_effective_ip: Option>, issuer_effective_as: Option>, + resource_validation_mode: ResourceValidationMode, } #[derive(Clone)] @@ -1746,6 +1751,7 @@ pub(crate) struct OwnedRoaTask { collect_vcir_local_outputs: bool, strict_cms_der: bool, strict_name: bool, + resource_validation_mode: ResourceValidationMode, pub(crate) submitted_at: Option, } @@ -1854,6 +1860,7 @@ fn validate_owned_roa_task(worker_index: usize, task: OwnedRoaTask) -> RoaTaskRe task.collect_vcir_local_outputs, task.strict_cms_der, task.strict_name, + task.resource_validation_mode, ) .map(|(vrps, local_outputs, cache_object_meta)| RoaTaskOk { vrps, @@ -1884,6 +1891,7 @@ pub(crate) struct ParallelObjectsStage { collect_vcir_local_outputs: bool, strict_cms_der: bool, strict_name: bool, + resource_validation_mode: ResourceValidationMode, roa_task_indices: Vec, cached_roa_results: Vec, roa_cache_stats: RoaValidationCacheStats, @@ -1921,6 +1929,7 @@ impl ParallelObjectsStage { collect_vcir_local_outputs: self.collect_vcir_local_outputs, strict_cms_der: self.strict_cms_der, strict_name: self.strict_name, + resource_validation_mode: self.resource_validation_mode, submitted_at: None, }); }); @@ -2257,11 +2266,13 @@ pub(crate) fn prepare_publication_point_for_parallel_roa_with_cache { stats.aspa_ok += 1; @@ -2604,6 +2616,7 @@ pub(crate) fn validate_roa_task_serial( collect_vcir_local_outputs: bool, strict_cms_der: bool, strict_name: bool, + resource_validation_mode: ResourceValidationMode, ) -> RoaTaskResult { let outcome = process_roa_with_issuer( task.file, @@ -2621,6 +2634,7 @@ pub(crate) fn validate_roa_task_serial( collect_vcir_local_outputs, strict_cms_der, strict_name, + resource_validation_mode, ) .map(|(vrps, local_outputs, cache_object_meta)| RoaTaskOk { vrps, @@ -2655,6 +2669,7 @@ fn process_roa_with_issuer( collect_vcir_local_outputs: bool, strict_cms_der: bool, strict_name: bool, + resource_validation_mode: ResourceValidationMode, ) -> Result<(Vec, Vec, Option), ObjectValidateError> { let _decode = timing .as_ref() @@ -2706,15 +2721,16 @@ fn process_roa_with_issuer( let _subset = timing .as_ref() .map(|t| t.span_phase("objects_roa_validate_ee_resources_subset_total")); - validate_ee_resources_subset( + let ee_vrs = validate_ee_resources_for_mode( &ee.resource_cert, issuer_effective_ip, issuer_effective_as, issuer_resources_index, + resource_validation_mode, )?; drop(_subset); - let vrps = roa_to_vrps(&roa); + let vrps = roa_to_vrps_with_vrs(&roa, ee_vrs.ip.as_ref())?; let cache_object_meta = RoaCacheObjectMeta { source_object_uri: file.rsync_uri.clone(), source_object_hash: file.sha256, @@ -2777,6 +2793,7 @@ fn process_roa_with_issuer_parallel_cached( collect_vcir_local_outputs: bool, strict_cms_der: bool, strict_name: bool, + resource_validation_mode: ResourceValidationMode, ) -> Result<(Vec, Vec, Option), ObjectValidateError> { let _decode = timing .as_ref() @@ -2834,15 +2851,16 @@ fn process_roa_with_issuer_parallel_cached( let _subset = timing .as_ref() .map(|t| t.span_phase("objects_roa_validate_ee_resources_subset_total")); - validate_ee_resources_subset( + let ee_vrs = validate_ee_resources_for_mode( &ee.resource_cert, issuer_effective_ip, issuer_effective_as, issuer_resources_index, + resource_validation_mode, )?; drop(_subset); - let vrps = roa_to_vrps(&roa); + let vrps = roa_to_vrps_with_vrs(&roa, ee_vrs.ip.as_ref())?; let cache_object_meta = RoaCacheObjectMeta { source_object_uri: file.rsync_uri.clone(), source_object_hash: file.sha256, @@ -2905,6 +2923,7 @@ fn process_aspa_with_issuer( collect_vcir_local_outputs: bool, strict_cms_der: bool, strict_name: bool, + resource_validation_mode: ResourceValidationMode, ) -> Result<(AspaAttestation, Option), ObjectValidateError> { let _decode = timing .as_ref() @@ -2956,14 +2975,17 @@ fn process_aspa_with_issuer( let _subset = timing .as_ref() .map(|t| t.span_phase("objects_aspa_validate_ee_resources_subset_total")); - validate_ee_resources_subset( + let ee_vrs = validate_ee_resources_for_mode( &ee.resource_cert, issuer_effective_ip, issuer_effective_as, issuer_resources_index, + resource_validation_mode, )?; drop(_subset); + validate_aspa_customer_in_vrs(&aspa, ee_vrs.asn.as_ref())?; + let attestation = AspaAttestation { customer_as_id: aspa.aspa.customer_as_id, provider_as_ids: aspa.aspa.provider_as_ids.clone(), @@ -3119,6 +3141,176 @@ fn validate_ee_resources_subset( Ok(()) } +#[derive(Debug)] +struct EeVerifiedResources { + ip: Option, + asn: Option, +} + +fn validate_ee_resources_for_mode( + ee: &ResourceCertificate, + issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>, + issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>, + issuer_resources_index: &IssuerResourcesIndex, + mode: ResourceValidationMode, +) -> Result { + match mode { + ResourceValidationMode::Rfc6487 => { + validate_ee_resources_subset( + ee, + issuer_effective_ip, + issuer_effective_as, + issuer_resources_index, + )?; + Ok(EeVerifiedResources { + ip: ee.tbs.extensions.ip_resources.clone(), + asn: ee.tbs.extensions.as_resources.clone(), + }) + } + ResourceValidationMode::ValidationUpdate03 => { + let ip = match ee.tbs.extensions.ip_resources.as_ref() { + Some(child_ip) => Some(intersect_ee_ip_resources_vrs( + child_ip, + issuer_effective_ip, + issuer_resources_index, + )?), + None => None, + }; + let asn = match ee.tbs.extensions.as_resources.as_ref() { + Some(child_as) => Some(intersect_ee_as_resources_vrs( + child_as, + issuer_effective_as, + issuer_resources_index, + )?), + None => None, + }; + Ok(EeVerifiedResources { ip, asn }) + } + } +} + +fn intersect_ee_ip_resources_vrs( + child_ip: &crate::data_model::rc::IpResourceSet, + issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>, + issuer_resources_index: &IssuerResourcesIndex, +) -> Result { + if child_ip.has_any_inherit() { + return Err(ObjectValidateError::EeResourcesNotSubset); + } + let _ = issuer_effective_ip; + let mut families = Vec::new(); + for fam in &child_ip.families { + let parent_intervals = match fam.afi { + crate::data_model::rc::Afi::Ipv4 => issuer_resources_index.ip_v4.as_deref(), + crate::data_model::rc::Afi::Ipv6 => issuer_resources_index.ip_v6.as_deref(), + } + .unwrap_or(&[]); + let items = match &fam.choice { + IpAddressChoice::Inherit => return Err(ObjectValidateError::EeResourcesNotSubset), + IpAddressChoice::AddressesOrRanges(items) => items, + }; + let intersections = intersect_ip_items_with_parent_intervals(items, parent_intervals); + if !intersections.is_empty() { + families.push(crate::data_model::rc::IpAddressFamily { + afi: fam.afi, + choice: IpAddressChoice::AddressesOrRanges(ip_intervals_to_ranges( + fam.afi, + &intersections, + )), + }); + } + } + Ok(crate::data_model::rc::IpResourceSet { families }) +} + +fn intersect_ee_as_resources_vrs( + child_as: &crate::data_model::rc::AsResourceSet, + issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>, + issuer_resources_index: &IssuerResourcesIndex, +) -> Result { + let _ = issuer_effective_as; + if matches!(child_as.asnum, Some(AsIdentifierChoice::Inherit)) + || matches!(child_as.rdi, Some(AsIdentifierChoice::Inherit)) + { + return Err(ObjectValidateError::EeResourcesNotSubset); + } + let asnum = child_as.asnum.as_ref().map(|choice| { + let child_intervals = as_choice_to_merged_intervals(choice); + AsIdentifierChoice::AsIdsOrRanges(as_intervals_to_items(&intersect_as_intervals( + &child_intervals, + issuer_resources_index.asnum.as_deref().unwrap_or(&[]), + ))) + }); + let rdi = child_as.rdi.as_ref().map(|choice| { + let child_intervals = as_choice_to_merged_intervals(choice); + AsIdentifierChoice::AsIdsOrRanges(as_intervals_to_items(&intersect_as_intervals( + &child_intervals, + issuer_resources_index.rdi.as_deref().unwrap_or(&[]), + ))) + }); + Ok(crate::data_model::rc::AsResourceSet { asnum, rdi }) +} + +fn roa_to_vrps_with_vrs( + roa: &RoaObject, + ee_vrs_ip: Option<&crate::data_model::rc::IpResourceSet>, +) -> Result, ObjectValidateError> { + let vrps = roa_to_vrps(roa); + let Some(ee_vrs_ip) = ee_vrs_ip else { + return Err(ObjectValidateError::EeResourcesNotSubset); + }; + for vrp in &vrps { + let rc_prefix = roa_prefix_to_rc_prefix(&vrp.prefix); + if !ee_vrs_ip.contains_prefix(&rc_prefix) { + return Err(ObjectValidateError::EeResourcesNotSubset); + } + } + Ok(vrps) +} + +fn validate_aspa_customer_in_vrs( + aspa: &AspaObject, + ee_vrs_as: Option<&crate::data_model::rc::AsResourceSet>, +) -> Result<(), ObjectValidateError> { + let Some(ee_vrs_as) = ee_vrs_as else { + return Err(ObjectValidateError::EeResourcesNotSubset); + }; + if !as_resource_set_contains_asn(ee_vrs_as, aspa.aspa.customer_as_id) { + return Err(ObjectValidateError::EeResourcesNotSubset); + } + Ok(()) +} + +fn as_resource_set_contains_asn( + resources: &crate::data_model::rc::AsResourceSet, + asn: u32, +) -> bool { + let Some(choice) = resources.asnum.as_ref() else { + return false; + }; + match choice { + AsIdentifierChoice::Inherit => false, + AsIdentifierChoice::AsIdsOrRanges(items) => items.iter().any(|item| match item { + crate::data_model::rc::AsIdOrRange::Id(id) => *id == asn, + crate::data_model::rc::AsIdOrRange::Range { min, max } => *min <= asn && asn <= *max, + }), + } +} + +fn roa_prefix_to_rc_prefix(prefix: &IpPrefix) -> RcIpPrefix { + let afi = match prefix.afi { + RoaAfi::Ipv4 => crate::data_model::rc::Afi::Ipv4, + RoaAfi::Ipv6 => crate::data_model::rc::Afi::Ipv6, + }; + let mut addr = prefix.addr.to_vec(); + addr.truncate(afi.octets_len()); + RcIpPrefix { + afi, + prefix_len: prefix.prefix_len, + addr, + } +} + fn as_resources_is_subset(child: &AsResourceSet, parent: &AsResourceSet) -> bool { as_choice_subset(child.asnum.as_ref(), parent.asnum.as_ref()) && as_choice_subset(child.rdi.as_ref(), parent.rdi.as_ref()) @@ -3292,6 +3484,121 @@ fn ip_resources_is_subset_indexed( true } +fn ip_items_to_merged_intervals( + items: &[crate::data_model::rc::IpAddressOrRange], +) -> Vec<(Vec, Vec)> { + let mut intervals = Vec::new(); + for item in items { + match item { + IpAddressOrRange::Prefix(p) => intervals.push(prefix_to_range(p)), + IpAddressOrRange::Range(r) => intervals.push((r.min.clone(), r.max.clone())), + } + } + intervals.sort_by(|(a, _), (b, _)| a.cmp(b)); + merge_ip_intervals_in_place(&mut intervals); + intervals +} + +fn intersect_ip_items_with_parent_intervals( + items: &[crate::data_model::rc::IpAddressOrRange], + parent_intervals: &[(Vec, Vec)], +) -> Vec<(Vec, Vec)> { + let child_intervals = ip_items_to_merged_intervals(items); + let mut out = Vec::new(); + let mut parent_index = 0usize; + for (child_min, child_max) in &child_intervals { + while parent_index < parent_intervals.len() + && parent_intervals[parent_index].1.as_slice() < child_min.as_slice() + { + parent_index += 1; + } + let mut scan = parent_index; + while scan < parent_intervals.len() + && parent_intervals[scan].0.as_slice() <= child_max.as_slice() + { + let (parent_min, parent_max) = &parent_intervals[scan]; + let min = if bytes_leq(child_min, parent_min) { + parent_min.clone() + } else { + child_min.clone() + }; + let max = if bytes_leq(child_max, parent_max) { + child_max.clone() + } else { + parent_max.clone() + }; + if bytes_leq(&min, &max) { + out.push((min, max)); + } + scan += 1; + } + } + merge_ip_intervals_in_place(&mut out); + out +} + +fn ip_intervals_to_ranges( + afi: crate::data_model::rc::Afi, + intervals: &[(Vec, Vec)], +) -> Vec { + intervals + .iter() + .map(|(min, max)| { + IpAddressOrRange::Range(crate::data_model::rc::IpAddressRange { + min: normalize_ip_bytes(afi, min), + max: normalize_ip_bytes(afi, max), + }) + }) + .collect() +} + +fn normalize_ip_bytes(afi: crate::data_model::rc::Afi, bytes: &[u8]) -> Vec { + let target_len = afi.octets_len(); + if bytes.len() == target_len { + return bytes.to_vec(); + } + let mut out = vec![0u8; target_len]; + let copy_len = bytes.len().min(target_len); + out[..copy_len].copy_from_slice(&bytes[..copy_len]); + out +} + +fn intersect_as_intervals(child: &[(u32, u32)], parent: &[(u32, u32)]) -> Vec<(u32, u32)> { + let mut out = Vec::new(); + let mut parent_index = 0usize; + for (child_min, child_max) in child { + while parent_index < parent.len() && parent[parent_index].1 < *child_min { + parent_index += 1; + } + let mut scan = parent_index; + while scan < parent.len() && parent[scan].0 <= *child_max { + let min = (*child_min).max(parent[scan].0); + let max = (*child_max).min(parent[scan].1); + if min <= max { + out.push((min, max)); + } + scan += 1; + } + } + merge_as_intervals(&out) +} + +fn as_intervals_to_items(intervals: &[(u32, u32)]) -> Vec { + intervals + .iter() + .map(|(min, max)| { + if min == max { + crate::data_model::rc::AsIdOrRange::Id(*min) + } else { + crate::data_model::rc::AsIdOrRange::Range { + min: *min, + max: *max, + } + } + }) + .collect() +} + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] enum AfiKey { V4, @@ -4724,6 +5031,120 @@ mod tests { assert!(matches!(err, ObjectValidateError::EeResourcesNotSubset)); } + #[test] + fn validation_update_03_ee_resources_reduce_to_vrs_for_roa_checks() { + let roa_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa"); + let roa_der = std::fs::read(roa_path).expect("read roa"); + let roa = RoaObject::decode_der(&roa_der).expect("decode roa"); + let ee = &roa.signed_object.signed_data.certificates[0].resource_cert; + let ee_ip = ee + .tbs + .extensions + .ip_resources + .as_ref() + .expect("fixture EE has IP resources"); + let first_family = ee_ip.families.first().expect("family"); + let first_item = match &first_family.choice { + IpAddressChoice::AddressesOrRanges(items) => items.first().expect("item").clone(), + IpAddressChoice::Inherit => panic!("fixture should not inherit"), + }; + let issuer_ip = IpResourceSet { + families: vec![IpAddressFamily { + afi: first_family.afi, + choice: IpAddressChoice::AddressesOrRanges(vec![first_item]), + }], + }; + let idx = build_issuer_resources_index(Some(&issuer_ip), None); + + let strict_err = validate_ee_resources_for_mode( + ee, + Some(&issuer_ip), + None, + &idx, + ResourceValidationMode::Rfc6487, + ) + .unwrap_err(); + assert!(matches!( + strict_err, + ObjectValidateError::EeResourcesNotSubset + )); + + let vrs = validate_ee_resources_for_mode( + ee, + Some(&issuer_ip), + None, + &idx, + ResourceValidationMode::ValidationUpdate03, + ) + .expect("vrs resource resolution"); + assert_eq!(vrs.ip.expect("vrs ip").families.len(), 1); + } + + #[test] + fn validation_update_03_ee_as_vrs_filters_aspa_customer_resources() { + let issuer_as = AsResourceSet { + asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![ + AsIdOrRange::Range { + min: 64500, + max: 64510, + }, + ])), + rdi: None, + }; + let child_as = AsResourceSet { + asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![ + AsIdOrRange::Range { + min: 64505, + max: 64520, + }, + ])), + rdi: None, + }; + let idx = build_issuer_resources_index(None, Some(&issuer_as)); + let vrs = intersect_ee_as_resources_vrs(&child_as, Some(&issuer_as), &idx) + .expect("intersect AS resources"); + assert!(as_resource_set_contains_asn(&vrs, 64505)); + assert!(as_resource_set_contains_asn(&vrs, 64510)); + assert!(!as_resource_set_contains_asn(&vrs, 64511)); + } + + #[test] + fn validation_update_03_empty_ee_vrs_rejects_roa_and_aspa_outputs() { + let roa_der = + fixture_bytes("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa"); + let roa = RoaObject::decode_der(&roa_der).expect("decode roa"); + let empty_ip_vrs = IpResourceSet { + families: Vec::new(), + }; + let roa_err = roa_to_vrps_with_vrs(&roa, Some(&empty_ip_vrs)).unwrap_err(); + assert!(matches!(roa_err, ObjectValidateError::EeResourcesNotSubset)); + let missing_ip_err = roa_to_vrps_with_vrs(&roa, None).unwrap_err(); + assert!(matches!( + missing_ip_err, + ObjectValidateError::EeResourcesNotSubset + )); + + let aspa_der = fixture_bytes( + "tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa", + ); + let aspa = AspaObject::decode_der(&aspa_der).expect("decode aspa"); + let empty_as_vrs = AsResourceSet { + asnum: Some(AsIdentifierChoice::AsIdsOrRanges(Vec::new())), + rdi: None, + }; + let aspa_err = validate_aspa_customer_in_vrs(&aspa, Some(&empty_as_vrs)).unwrap_err(); + assert!(matches!( + aspa_err, + ObjectValidateError::EeResourcesNotSubset + )); + let missing_as_err = validate_aspa_customer_in_vrs(&aspa, None).unwrap_err(); + assert!(matches!( + missing_as_err, + ObjectValidateError::EeResourcesNotSubset + )); + } + #[test] fn extra_rfc_refs_for_crl_selection_distinguishes_crl_errors() { assert_eq!( @@ -4976,11 +5397,13 @@ mod tests { issuer_resources_index: Arc::new(IssuerResourcesIndex::default()), issuer_effective_ip: None, issuer_effective_as: None, + resource_validation_mode: ResourceValidationMode::default(), }), validation_time: OffsetDateTime::now_utc(), collect_vcir_local_outputs: false, strict_cms_der: false, strict_name: false, + resource_validation_mode: ResourceValidationMode::default(), roa_task_indices: vec![0, 1], cached_roa_results: Vec::new(), roa_cache_stats: RoaValidationCacheStats::default(), diff --git a/src/validation/tree_runner.rs b/src/validation/tree_runner.rs index 997910e..c1ffd3e 100644 --- a/src/validation/tree_runner.rs +++ b/src/validation/tree_runner.rs @@ -21,7 +21,7 @@ use crate::data_model::router_cert::{ use crate::fetch::rsync::RsyncFetcher; use crate::parallel::config::ParallelPhase2Config; use crate::parallel::repo_runtime::{RepoSyncRuntime, RepoSyncRuntimeOutcome}; -use crate::policy::Policy; +use crate::policy::{Policy, ResourceValidationMode}; use crate::replay::archive::ReplayArchiveIndex; use crate::replay::delta_archive::ReplayDeltaArchiveIndex; use crate::report::{RfcRef, Warning}; @@ -932,6 +932,7 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> { &fresh_point, self.validation_time, self.timing.as_ref(), + self.policy, if self.enable_child_certificate_validation_cache { Some(ChildCertificateValidationCacheContext { store: self.store, @@ -2259,12 +2260,14 @@ fn discover_children_from_fresh_snapshot_with_audit( return Err("lazy CA certificate requires store-backed child discovery".to_string()); } }; + let default_policy = Policy::default(); discover_children_from_fresh_snapshot_with_audit_cached_with_issuer_der( issuer, issuer_ca_der, publication_point, validation_time, timing, + &default_policy, None, ) } @@ -2282,12 +2285,14 @@ fn discover_children_from_fresh_snapshot_with_audit_cached, + policy: &Policy, cache_context: Option>, ) -> Result { let locked_files = publication_point.files(); @@ -2906,6 +2912,7 @@ fn discover_children_from_fresh_snapshot_with_audit_cached_with_issuer_der< issuer.effective_as_resources.as_ref(), &issuer_resources_index, validation_time, + policy.resource_validation_mode, ) { Ok(v) => v, Err(CaPathError::ChildNotCa) => { @@ -2949,12 +2956,32 @@ fn discover_children_from_fresh_snapshot_with_audit_cached_with_issuer_der< match router_result { Ok(router) => { + let router_asns = match router_asns_for_resource_mode( + &router.asns, + issuer.effective_as_resources.as_ref(), + policy.resource_validation_mode, + ) { + Ok(v) => v, + Err(detail) => { + 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 AS resource validation failed: {detail}" + )), + }); + continue; + } + }; 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 { + for as_id in &router_asns { router_keys.push(RouterKeyPayload { as_id: *as_id, ski: router.subject_key_identifier.clone(), @@ -3006,8 +3033,7 @@ fn discover_children_from_fresh_snapshot_with_audit_cached_with_issuer_der< effective_not_before, effective_until, payload: ChildCertificateCachePayload::Router { - router_keys: router - .asns + router_keys: router_asns .iter() .map(|as_id| ChildCertificateCacheRouterKeyProjection { as_id: *as_id, @@ -3096,6 +3122,20 @@ fn discover_children_from_fresh_snapshot_with_audit_cached_with_issuer_der< validate_sub_ca_nanos = validate_sub_ca_nanos .saturating_add(t1.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64); + if !validated.resource_warnings.is_empty() { + audits.push(ObjectAuditEntry { + rsync_uri: f.rsync_uri.clone(), + sha256_hex: sha256_hex_from_32(&f.sha256), + kind: AuditObjectKind::Certificate, + result: AuditObjectResult::Ok, + detail: Some(format!( + "resource validation warning ({:?}): {}", + policy.resource_validation_mode, + validated.resource_warnings.summary() + )), + }); + } + let eff_ip_items = ip_item_count(validated.effective_ip_resources.as_ref()); match bucketize(eff_ip_items) { 0 => eff_ip_items_bucket_le_10 = eff_ip_items_bucket_le_10.saturating_add(1), @@ -3482,6 +3522,65 @@ fn is_non_router_certificate(err: &BgpsecRouterCertificatePathError) -> bool { ) } +fn router_asns_for_resource_mode( + router_asns: &[u32], + issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>, + mode: ResourceValidationMode, +) -> Result, String> { + let Some(issuer_effective_as) = issuer_effective_as else { + return Err("issuer has no effective AS resources".to_string()); + }; + + match mode { + ResourceValidationMode::Rfc6487 => { + let outside: Vec = router_asns + .iter() + .copied() + .filter(|asn| !as_resource_set_contains_asn(issuer_effective_as, *asn)) + .collect(); + if outside.is_empty() { + Ok(router_asns.to_vec()) + } else { + Err(format!( + "router AS resources are not a subset of issuer effective AS resources: {outside:?}" + )) + } + } + ResourceValidationMode::ValidationUpdate03 => { + let filtered: Vec = router_asns + .iter() + .copied() + .filter(|asn| as_resource_set_contains_asn(issuer_effective_as, *asn)) + .collect(); + if filtered.is_empty() { + Err("router AS resources have empty validated resource set".to_string()) + } else { + Ok(filtered) + } + } + } +} + +fn as_resource_set_contains_asn( + resources: &crate::data_model::rc::AsResourceSet, + asn: u32, +) -> bool { + let Some(choice) = resources.asnum.as_ref() else { + return false; + }; + match choice { + crate::data_model::rc::AsIdentifierChoice::Inherit => false, + crate::data_model::rc::AsIdentifierChoice::AsIdsOrRanges(items) => { + items.iter().any(|item| match item { + crate::data_model::rc::AsIdOrRange::Id(id) => *id == asn, + crate::data_model::rc::AsIdOrRange::Range { min, max } => { + *min <= asn && asn <= *max + } + }) + } + } +} + fn select_issuer_crl_uri_for_child<'a>( child: &'a crate::data_model::rc::ResourceCertificate, crl_cache: &std::collections::HashMap, @@ -3568,6 +3667,7 @@ fn validate_subordinate_ca_cert_with_cached_issuer( issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>, issuer_resources_index: &IssuerEffectiveResourcesIndex, validation_time: time::OffsetDateTime, + resource_validation_mode: crate::policy::ResourceValidationMode, ) -> Result { let verified_crl = ensure_issuer_crl_verified(issuer_crl_rsync_uri, crl_cache, issuer_ca_der)?; @@ -3584,6 +3684,7 @@ fn validate_subordinate_ca_cert_with_cached_issuer( issuer_effective_as, issuer_resources_index, validation_time, + resource_validation_mode, ) } diff --git a/src/validation/tree_runner/tests.rs b/src/validation/tree_runner/tests.rs index 5124531..db84613 100644 --- a/src/validation/tree_runner/tests.rs +++ b/src/validation/tree_runner/tests.rs @@ -1,5 +1,5 @@ use super::*; -use crate::data_model::rc::ResourceCertificate; +use crate::data_model::rc::{AsIdOrRange, AsIdentifierChoice, AsResourceSet, ResourceCertificate}; use crate::data_model::roa::RoaAfi; use crate::fetch::rsync::LocalDirRsyncFetcher; use crate::fetch::rsync::{RsyncFetchError, RsyncFetcher}; @@ -29,6 +29,48 @@ fn ipv4_addr(octets: [u8; 4]) -> [u8; 16] { addr } +#[test] +fn publication_point_cache_policy_fingerprint_includes_resource_validation_mode() { + let mut strict_policy = Policy::default(); + strict_policy.resource_validation_mode = ResourceValidationMode::Rfc6487; + let mut vrs_policy = Policy::default(); + vrs_policy.resource_validation_mode = ResourceValidationMode::ValidationUpdate03; + + assert_ne!( + publication_point_cache_policy_fingerprint(&strict_policy), + publication_point_cache_policy_fingerprint(&vrs_policy) + ); +} + +#[test] +fn router_asns_for_resource_mode_filters_vrs_and_rejects_strict_overclaim() { + let issuer_as = AsResourceSet { + asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![ + AsIdOrRange::Range { + min: 64500, + max: 64510, + }, + ])), + rdi: None, + }; + + let strict = router_asns_for_resource_mode( + &[64505, 64520], + Some(&issuer_as), + ResourceValidationMode::Rfc6487, + ) + .unwrap_err(); + assert!(strict.contains("not a subset"), "{strict}"); + + let vrs = router_asns_for_resource_mode( + &[64505, 64520], + Some(&issuer_as), + ResourceValidationMode::ValidationUpdate03, + ) + .expect("vrs filters router asns"); + assert_eq!(vrs, vec![64505]); +} + struct NeverHttpFetcher; impl Fetcher for NeverHttpFetcher { fn fetch(&self, _uri: &str) -> Result, String> {