20260701 add validation-update-03 VRS mode

This commit is contained in:
yuyr 2026-07-01 20:28:25 +08:00
parent a501cfb709
commit a5b194ce35
14 changed files with 1430 additions and 24 deletions

View File

@ -63,6 +63,10 @@ PERIODIC_SNAPSHOT_RESET=0
PERIODIC_SNAPSHOT_MAX_DELTAS=100 PERIODIC_SNAPSHOT_MAX_DELTAS=100
DB_STATS_EXACT_EVERY=0 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. # Validation and performance options aligned with current optimized soak defaults.
ENABLE_CHILD_CERTIFICATE_VALIDATION_CACHE=1 ENABLE_CHILD_CERTIFICATE_VALIDATION_CACHE=1
RPKI_ANALYZE=1 RPKI_ANALYZE=1

View File

@ -33,6 +33,7 @@ Defaults:
- `MAX_RUNS=-1` - `MAX_RUNS=-1`
- `INTERVAL_SECS=600` - `INTERVAL_SECS=600`
- `TAL_INPUT_MODE=file-live-ta` - `TAL_INPUT_MODE=file-live-ta`
- `RESOURCE_VALIDATION_MODE=validation-update-03`
- `LIVE_TA_REFRESH_BEFORE_SNAPSHOT=1` - `LIVE_TA_REFRESH_BEFORE_SNAPSHOT=1`
- `PERIODIC_SNAPSHOT_RESET=0` - `PERIODIC_SNAPSHOT_RESET=0`
- `PERIODIC_SNAPSHOT_MAX_DELTAS=100` - `PERIODIC_SNAPSHOT_MAX_DELTAS=100`
@ -57,6 +58,22 @@ Container/image mapping:
The first snapshot refreshes live TA certificates before starting the RP process. 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 ## Architecture Guardrails
The key scripts read: The key scripts read:

View File

@ -33,6 +33,7 @@ vim .env
- `MAX_RUNS=-1` - `MAX_RUNS=-1`
- `INTERVAL_SECS=600` - `INTERVAL_SECS=600`
- `TAL_INPUT_MODE=file-live-ta` - `TAL_INPUT_MODE=file-live-ta`
- `RESOURCE_VALIDATION_MODE=validation-update-03`
- `LIVE_TA_REFRESH_BEFORE_SNAPSHOT=1` - `LIVE_TA_REFRESH_BEFORE_SNAPSHOT=1`
- `PERIODIC_SNAPSHOT_RESET=0` - `PERIODIC_SNAPSHOT_RESET=0`
- `PERIODIC_SNAPSHOT_MAX_DELTAS=100` - `PERIODIC_SNAPSHOT_MAX_DELTAS=100`
@ -57,6 +58,22 @@ vim .env
第一轮 snapshot 会先拉取 live TA避免 clean state 使用旧 fixture TA。 第一轮 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` 复用流程。
## 架构检查 ## 架构检查
关键脚本会读取: 关键脚本会读取:

View File

@ -203,12 +203,14 @@ load_env() {
GRAFANA_IMAGE="${GRAFANA_IMAGE:-grafana/grafana:11.3.1}" GRAFANA_IMAGE="${GRAFANA_IMAGE:-grafana/grafana:11.3.1}"
METRICS_INSTANCE="${METRICS_INSTANCE:-${PACKAGE_ARCH}-installer}" METRICS_INSTANCE="${METRICS_INSTANCE:-${PACKAGE_ARCH}-installer}"
ALLOW_CROSS_ARCH="${ALLOW_CROSS_ARCH:-0}" 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}" 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_IMAGE" "$METRICS_IMAGE"
persist_env_default "$ENV_FILE" "METRICS_PLATFORM" "$METRICS_PLATFORM" 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_DIR" "$RTR_REPORT_DIR"
persist_env_default "$ENV_FILE" "RTR_REPORT_CONTAINER_DIR" "$RTR_REPORT_CONTAINER_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() { compose_cmd() {

View File

@ -81,6 +81,10 @@ RPKI_PROGRESS_PP_CONTROL_SLOW_MS=100
# 是否在运行前尝试禁用 rpki-client timer 并杀掉竞争 RP 进程。 # 是否在运行前尝试禁用 rpki-client timer 并杀掉竞争 RP 进程。
DISABLE_COMPETING_RPS=1 DISABLE_COMPETING_RPS=1
# 资源证书验证模式;会显式传给 rpki 子进程。
# 合法值validation-update-03、rfc6487。
RESOURCE_VALIDATION_MODE=validation-update-03
# 是否启用实验性 child CRT 验证缓存。独立于 ROA cache / PP cache默认关闭。 # 是否启用实验性 child CRT 验证缓存。独立于 ROA cache / PP cache默认关闭。
# 开启后会额外传入 --enable-child-certificate-validation-cache。 # 开启后会额外传入 --enable-child-certificate-validation-cache。
ENABLE_CHILD_CERTIFICATE_VALIDATION_CACHE=0 ENABLE_CHILD_CERTIFICATE_VALIDATION_CACHE=0

View File

@ -15,6 +15,7 @@ INTERVAL_SECS="${INTERVAL_SECS:-0}"
STOP_AFTER_SECS="${STOP_AFTER_SECS:-0}" STOP_AFTER_SECS="${STOP_AFTER_SECS:-0}"
RIRS="${RIRS:-afrinic,apnic,arin,lacnic,ripe}" RIRS="${RIRS:-afrinic,apnic,arin,lacnic,ripe}"
TAL_INPUT_MODE="${TAL_INPUT_MODE:-file-with-ta}" TAL_INPUT_MODE="${TAL_INPUT_MODE:-file-with-ta}"
RESOURCE_VALIDATION_MODE="${RESOURCE_VALIDATION_MODE:-validation-update-03}"
RUN_ROOT="${RUN_ROOT:-$PACKAGE_ROOT}" RUN_ROOT="${RUN_ROOT:-$PACKAGE_ROOT}"
RETAIN_RUNS="${RETAIN_RUNS:-10}" RETAIN_RUNS="${RETAIN_RUNS:-10}"
CLEAN_TMP_AFTER_RUN="${CLEAN_TMP_AFTER_RUN:-0}" CLEAN_TMP_AFTER_RUN="${CLEAN_TMP_AFTER_RUN:-0}"
@ -125,6 +126,16 @@ validate_tal_input_mode() {
esac 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() { normalize_token() {
local token="$1" local token="$1"
token="${token#"${token%%[![:space:]]*}"}" token="${token#"${token%%[![:space:]]*}"}"
@ -1038,6 +1049,7 @@ build_child_args() {
--db "$DB_DIR/work-db" --db "$DB_DIR/work-db"
--repo-bytes-db "$DB_DIR/repo-bytes.db" --repo-bytes-db "$DB_DIR/repo-bytes.db"
--rsync-scope "$RSYNC_SCOPE" --rsync-scope "$RSYNC_SCOPE"
--resource-validation-mode "$RESOURCE_VALIDATION_MODE"
) )
if is_true "$ALLOW_RSYNC_MIRROR_REUSE"; then if is_true "$ALLOW_RSYNC_MIRROR_REUSE"; then
CHILD_ARGS+=(--rsync-mirror-root "$RSYNC_MIRROR_ROOT") CHILD_ARGS+=(--rsync-mirror-root "$RSYNC_MIRROR_ROOT")
@ -1326,6 +1338,7 @@ main() {
validate_positive_int "RETAIN_RUNS" "$RETAIN_RUNS" validate_positive_int "RETAIN_RUNS" "$RETAIN_RUNS"
validate_rsync_scope validate_rsync_scope
validate_tal_input_mode validate_tal_input_mode
validate_resource_validation_mode
validate_non_negative_int "PERIODIC_SNAPSHOT_MAX_DELTAS" "$PERIODIC_SNAPSHOT_MAX_DELTAS" validate_non_negative_int "PERIODIC_SNAPSHOT_MAX_DELTAS" "$PERIODIC_SNAPSHOT_MAX_DELTAS"
if [[ -n "${DB_STATS_EXACT_EVERY:-}" && "$DB_STATS_EXACT_EVERY" != "0" ]]; then if [[ -n "${DB_STATS_EXACT_EVERY:-}" && "$DB_STATS_EXACT_EVERY" != "0" ]]; then
validate_positive_int "DB_STATS_EXACT_EVERY" "$DB_STATS_EXACT_EVERY" validate_positive_int "DB_STATS_EXACT_EVERY" "$DB_STATS_EXACT_EVERY"

View File

@ -24,7 +24,7 @@ use crate::memory_telemetry::{
}; };
use crate::parallel::config::{ParallelPhase1Config, ParallelPhase2Config}; use crate::parallel::config::{ParallelPhase1Config, ParallelPhase2Config};
use crate::parallel::types::TalInputSpec; use crate::parallel::types::TalInputSpec;
use crate::policy::{Policy, StrictPolicy}; use crate::policy::{Policy, ResourceValidationMode, StrictPolicy};
use crate::storage::{RocksStore, VcirStorageSummary}; use crate::storage::{RocksStore, VcirStorageSummary};
use crate::validation::run_tree_from_tal::{ use crate::validation::run_tree_from_tal::{
RunTreeFromTalAuditOutput, run_tree_from_multiple_tals_parallel_phase2_audit, RunTreeFromTalAuditOutput, run_tree_from_multiple_tals_parallel_phase2_audit,
@ -130,6 +130,7 @@ pub struct CliArgs {
pub repo_bytes_db: Option<PathBuf>, pub repo_bytes_db: Option<PathBuf>,
pub policy_path: Option<PathBuf>, pub policy_path: Option<PathBuf>,
pub strict_policy: Option<StrictPolicy>, pub strict_policy: Option<StrictPolicy>,
pub resource_validation_mode: Option<ResourceValidationMode>,
pub report_json_path: Option<PathBuf>, pub report_json_path: Option<PathBuf>,
pub report_json_compact: bool, pub report_json_compact: bool,
pub skip_report_build: bool, pub skip_report_build: bool,
@ -189,6 +190,8 @@ Options:
--repo-bytes-db <path> External repo object bytes DB path (optional) --repo-bytes-db <path> External repo object bytes DB path (optional)
--policy <path> Policy TOML path (optional) --policy <path> Policy TOML path (optional)
--strict [policies] Enable strict policies (default all; comma list: name,cms-der,signed-attrs; none disables) --strict [policies] Enable strict policies (default all; comma list: name,cms-der,signed-attrs; none disables)
--resource-validation-mode <validation-update-03|rfc6487>
Resource certificate validation mode (default: validation-update-03)
--report-json <path> Write full audit report as JSON (optional) --report-json <path> Write full audit report as JSON (optional)
--report-json-compact Write report JSON without pretty-printing (requires --report-json) --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 --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<CliArgs, String> {
let mut repo_bytes_db: Option<PathBuf> = None; let mut repo_bytes_db: Option<PathBuf> = None;
let mut policy_path: Option<PathBuf> = None; let mut policy_path: Option<PathBuf> = None;
let mut strict_policy: Option<StrictPolicy> = None; let mut strict_policy: Option<StrictPolicy> = None;
let mut resource_validation_mode: Option<ResourceValidationMode> = None;
let mut report_json_path: Option<PathBuf> = None; let mut report_json_path: Option<PathBuf> = None;
let mut report_json_compact: bool = false; let mut report_json_compact: bool = false;
let mut skip_report_build: bool = false; let mut skip_report_build: bool = false;
@ -475,6 +479,19 @@ pub fn parse_args(argv: &[String]) -> Result<CliArgs, String> {
let spec = arg.strip_prefix("--strict=").expect("prefix checked"); let spec = arg.strip_prefix("--strict=").expect("prefix checked");
strict_policy = Some(StrictPolicy::parse_cli_spec(Some(spec))?); 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" => { "--report-json" => {
i += 1; i += 1;
let v = argv.get(i).ok_or("--report-json requires a value")?; let v = argv.get(i).ok_or("--report-json requires a value")?;
@ -929,6 +946,7 @@ pub fn parse_args(argv: &[String]) -> Result<CliArgs, String> {
repo_bytes_db, repo_bytes_db,
policy_path, policy_path,
strict_policy, strict_policy,
resource_validation_mode,
report_json_path, report_json_path,
report_json_compact, report_json_compact,
skip_report_build, skip_report_build,
@ -1974,6 +1992,9 @@ pub fn run(argv: &[String]) -> Result<(), String> {
if let Some(strict_policy) = args.strict_policy { if let Some(strict_policy) = args.strict_policy {
policy.strict = 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 { if args.disable_rrdp {
policy.sync_preference = crate::policy::SyncPreference::RsyncOnly; policy.sync_preference = crate::policy::SyncPreference::RsyncOnly;
} }

View File

@ -2,6 +2,7 @@ use super::*;
use crate::memory_telemetry::{ use crate::memory_telemetry::{
MemoryTelemetryCheckpoint, MemoryTelemetrySummary, ProcessMemorySnapshot, MemoryTelemetryCheckpoint, MemoryTelemetrySummary, ProcessMemorySnapshot,
}; };
use crate::policy::ResourceValidationMode;
use crate::storage::{ use crate::storage::{
RocksDbMemorySnapshot, RocksDbMemoryTotals, VcirCcrProjectionSizeBreakdown, RocksDbMemorySnapshot, RocksDbMemoryTotals, VcirCcrProjectionSizeBreakdown,
VcirChildResourceSizeBreakdown, VcirCoreFieldSizeBreakdown, VcirFieldSizeBreakdown, VcirChildResourceSizeBreakdown, VcirCoreFieldSizeBreakdown, VcirFieldSizeBreakdown,
@ -19,6 +20,7 @@ fn parse_help_returns_usage() {
assert!(err.contains("--parallel-phase2-object-workers"), "{err}"); assert!(err.contains("--parallel-phase2-object-workers"), "{err}");
assert!(err.contains("--memory-trim-after-validation"), "{err}"); assert!(err.contains("--memory-trim-after-validation"), "{err}");
assert!(err.contains("--enable-roa-validation-cache"), "{err}"); assert!(err.contains("--enable-roa-validation-cache"), "{err}");
assert!(err.contains("--resource-validation-mode"), "{err}");
assert!( assert!(
err.contains("--publication-point-cache-observe-only"), err.contains("--publication-point-cache-observe-only"),
"{err}" "{err}"
@ -340,6 +342,39 @@ fn parse_accepts_strict_without_value_as_all() {
assert_eq!(args.strict_policy, Some(StrictPolicy::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] #[test]
fn parse_rejects_unknown_strict_policy() { fn parse_rejects_unknown_strict_policy() {
let argv = vec![ let argv = vec![
@ -1407,6 +1442,10 @@ fn read_policy_accepts_valid_toml() {
policy.signed_object_failure_policy, policy.signed_object_failure_policy,
crate::policy::SignedObjectFailurePolicy::DropPublicationPoint crate::policy::SignedObjectFailurePolicy::DropPublicationPoint
); );
assert_eq!(
policy.resource_validation_mode,
ResourceValidationMode::ValidationUpdate03
);
assert_eq!(policy.strict, StrictPolicy::default()); 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] #[test]
fn read_policy_reports_missing_file() { fn read_policy_reports_missing_file() {
let dir = tempfile::tempdir().expect("tmpdir"); let dir = tempfile::tempdir().expect("tmpdir");

View File

@ -740,6 +740,29 @@ mod strict_name_tests {
let err = validate_strict_rpki_name(&name, "subject").expect_err("strict name fails"); let err = validate_strict_rpki_name(&name, "subject").expect_err("strict name fails");
assert!(err.to_string().contains("exactly once"), "{err}"); 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 { impl ResourceCertificateParsed {

View File

@ -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<Self, String> {
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)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)] #[serde(default)]
pub struct StrictPolicy { pub struct StrictPolicy {
@ -99,6 +125,7 @@ pub struct Policy {
pub sync_preference: SyncPreference, pub sync_preference: SyncPreference,
pub ca_failed_fetch_policy: CaFailedFetchPolicy, pub ca_failed_fetch_policy: CaFailedFetchPolicy,
pub signed_object_failure_policy: SignedObjectFailurePolicy, pub signed_object_failure_policy: SignedObjectFailurePolicy,
pub resource_validation_mode: ResourceValidationMode,
pub strict: StrictPolicy, pub strict: StrictPolicy,
} }
@ -108,6 +135,7 @@ impl Default for Policy {
sync_preference: SyncPreference::default(), sync_preference: SyncPreference::default(),
ca_failed_fetch_policy: CaFailedFetchPolicy::default(), ca_failed_fetch_policy: CaFailedFetchPolicy::default(),
signed_object_failure_policy: SignedObjectFailurePolicy::default(), signed_object_failure_policy: SignedObjectFailurePolicy::default(),
resource_validation_mode: ResourceValidationMode::default(),
strict: StrictPolicy::default(), strict: StrictPolicy::default(),
} }
} }
@ -150,6 +178,19 @@ mod tests {
assert!(StrictPolicy::parse_cli_spec(Some("bogus")).is_err()); 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] #[test]
fn policy_toml_accepts_strict_table() { fn policy_toml_accepts_strict_table() {
let policy = Policy::from_toml_str( 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
);
}
} }

View File

@ -5,6 +5,7 @@ use crate::data_model::rc::{
AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpResourceSet, ResourceCertKind, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpResourceSet, ResourceCertKind,
ResourceCertificate, ResourceCertificateDecodeError, ResourceCertificate, ResourceCertificateDecodeError,
}; };
use crate::policy::ResourceValidationMode;
use x509_parser::prelude::{FromDer, X509Certificate}; use x509_parser::prelude::{FromDer, X509Certificate};
use crate::validation::x509_name::x509_names_equivalent; use crate::validation::x509_name::x509_names_equivalent;
@ -18,6 +19,7 @@ pub struct ValidatedSubordinateCa {
pub issuer_crl: RpkixCrl, pub issuer_crl: RpkixCrl,
pub effective_ip_resources: Option<IpResourceSet>, pub effective_ip_resources: Option<IpResourceSet>,
pub effective_as_resources: Option<AsResourceSet>, pub effective_as_resources: Option<AsResourceSet>,
pub resource_warnings: ResourceValidationWarnings,
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
@ -25,6 +27,41 @@ pub struct ValidatedSubordinateCaLite {
pub child_ca: ResourceCertificate, pub child_ca: ResourceCertificate,
pub effective_ip_resources: Option<IpResourceSet>, pub effective_ip_resources: Option<IpResourceSet>,
pub effective_as_resources: Option<AsResourceSet>, pub effective_as_resources: Option<AsResourceSet>,
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)] #[derive(Clone, Debug, Default)]
@ -178,6 +215,30 @@ pub fn validate_subordinate_ca_cert(
issuer_effective_ip: Option<&IpResourceSet>, issuer_effective_ip: Option<&IpResourceSet>,
issuer_effective_as: Option<&AsResourceSet>, issuer_effective_as: Option<&AsResourceSet>,
validation_time: time::OffsetDateTime, validation_time: time::OffsetDateTime,
) -> Result<ValidatedSubordinateCa, CaPathError> {
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<ValidatedSubordinateCa, CaPathError> { ) -> Result<ValidatedSubordinateCa, CaPathError> {
let child_ca = ResourceCertificate::decode_der(child_ca_der)?; let child_ca = ResourceCertificate::decode_der(child_ca_der)?;
if child_ca.kind != ResourceCertKind::Ca { if child_ca.kind != ResourceCertKind::Ca {
@ -230,13 +291,20 @@ pub fn validate_subordinate_ca_cert(
return Err(CaPathError::ChildRevoked); 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(), child_ca.tbs.extensions.ip_resources.as_ref(),
issuer_effective_ip, issuer_effective_ip,
)?;
let effective_as_resources = resolve_child_as_resources(
child_ca.tbs.extensions.as_resources.as_ref(), child_ca.tbs.extensions.as_resources.as_ref(),
issuer_effective_as, 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() { if effective_ip_resources.is_none() && effective_as_resources.is_none() {
return Err(CaPathError::ResourcesMissing); return Err(CaPathError::ResourcesMissing);
@ -248,6 +316,7 @@ pub fn validate_subordinate_ca_cert(
issuer_crl, issuer_crl,
effective_ip_resources, effective_ip_resources,
effective_as_resources, effective_as_resources,
resource_warnings,
}) })
} }
@ -285,6 +354,7 @@ pub fn validate_subordinate_ca_cert_with_prevalidated_issuer(
issuer_effective_as, issuer_effective_as,
&issuer_resources_index, &issuer_resources_index,
validation_time, 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_effective_as: Option<&AsResourceSet>,
issuer_resources_index: &IssuerEffectiveResourcesIndex, issuer_resources_index: &IssuerEffectiveResourcesIndex,
validation_time: time::OffsetDateTime, validation_time: time::OffsetDateTime,
resource_validation_mode: ResourceValidationMode,
) -> Result<ValidatedSubordinateCaLite, CaPathError> { ) -> Result<ValidatedSubordinateCaLite, CaPathError> {
if child_ca.kind != ResourceCertKind::Ca { if child_ca.kind != ResourceCertKind::Ca {
return Err(CaPathError::ChildNotCa); return Err(CaPathError::ChildNotCa);
@ -347,17 +418,17 @@ pub fn validate_subordinate_ca_cert_with_prevalidated_issuer_and_resources(
return Err(CaPathError::ChildRevoked); 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(), child_ca.tbs.extensions.ip_resources.as_ref(),
issuer_effective_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 = resolve_child_as_resources_indexed(
child_ca.tbs.extensions.as_resources.as_ref(), child_ca.tbs.extensions.as_resources.as_ref(),
issuer_effective_as, issuer_effective_as,
issuer_resources_index.parent_asnum_intervals.as_deref(), issuer_resources_index,
issuer_resources_index.parent_rdi_intervals.as_deref(), resource_validation_mode,
)?; )?;
if effective_ip_resources.is_none() && effective_as_resources.is_none() { if effective_ip_resources.is_none() && effective_as_resources.is_none() {
return Err(CaPathError::ResourcesMissing); return Err(CaPathError::ResourcesMissing);
@ -367,6 +438,7 @@ pub fn validate_subordinate_ca_cert_with_prevalidated_issuer_and_resources(
child_ca, child_ca,
effective_ip_resources, effective_ip_resources,
effective_as_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) .any(|rc| rc.serial_number == serial)
} }
#[derive(Clone, Debug)]
struct ResourceResolution {
effective_ip_resources: Option<IpResourceSet>,
effective_as_resources: Option<AsResourceSet>,
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<ResourceResolution, CaPathError> {
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( fn resolve_child_ip_resources(
child_ip: Option<&IpResourceSet>, child_ip: Option<&IpResourceSet>,
issuer_effective: 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<crate::data_model::rc::Afi, Vec<crate::data_model::rc::IpAddressOrRange>>,
>,
parent_intervals_by_afi: &HashMap<crate::data_model::rc::Afi, Vec<(Vec<u8>, Vec<u8>)>>,
) -> Result<(Option<IpResourceSet>, 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<crate::data_model::rc::Afi, Vec<crate::data_model::rc::IpAddressOrRange>>,
> = 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( fn resolve_child_as_resources(
child_as: Option<&AsResourceSet>, child_as: Option<&AsResourceSet>,
issuer_effective: Option<&AsResourceSet>, issuer_effective: Option<&AsResourceSet>,
@ -656,6 +859,80 @@ fn resolve_child_as_resources_indexed(
Ok(Some(AsResourceSet { asnum, rdi })) 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<AsResourceSet>, 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<AsIdentifierChoice>, 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( fn as_choice_subset(
child: Option<&AsIdentifierChoice>, child: Option<&AsIdentifierChoice>,
parent: Option<&AsIdentifierChoice>, parent: Option<&AsIdentifierChoice>,
@ -911,6 +1188,134 @@ fn ip_family_items_subset_with_parent_intervals(
true true
} }
fn ip_items_to_merged_intervals(
items: &[crate::data_model::rc::IpAddressOrRange],
) -> Vec<(Vec<u8>, Vec<u8>)> {
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<u8>, Vec<u8>)],
) -> Vec<(Vec<u8>, Vec<u8>)> {
let child_intervals = ip_items_to_merged_intervals(items);
intersect_ip_intervals(&child_intervals, parent_intervals)
}
fn intersect_ip_intervals(
child_intervals: &[(Vec<u8>, Vec<u8>)],
parent_intervals: &[(Vec<u8>, Vec<u8>)],
) -> Vec<(Vec<u8>, Vec<u8>)> {
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<u8>, Vec<u8>)],
) -> Vec<crate::data_model::rc::IpAddressOrRange> {
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<u8> {
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<T: PartialEq>(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<crate::data_model::rc::AsIdOrRange> {
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<u8>, Vec<u8>)]) -> Vec<(Vec<u8>, Vec<u8>)> { fn merge_ip_intervals(v: &[(Vec<u8>, Vec<u8>)]) -> Vec<(Vec<u8>, Vec<u8>)> {
let mut out: Vec<(Vec<u8>, Vec<u8>)> = Vec::new(); let mut out: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
for (min, max) in v { for (min, max) in v {
@ -971,8 +1376,8 @@ mod tests {
use super::*; use super::*;
use crate::data_model::common::X509NameDer; use crate::data_model::common::X509NameDer;
use crate::data_model::rc::{ use crate::data_model::rc::{
Afi, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily, IpAddressOrRange, Afi, AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily,
IpResourceSet, IpAddressOrRange, IpPrefix, IpResourceSet,
}; };
use crate::data_model::rc::{ use crate::data_model::rc::{
RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate, RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate,
@ -1225,6 +1630,233 @@ mod tests {
assert!(matches!(err, CaPathError::InheritWithoutParentResources)); 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] #[test]
fn child_aki_mismatch_is_rejected() { fn child_aki_mismatch_is_rejected() {
let issuer = dummy_cert( let issuer = dummy_cert(

View File

@ -13,7 +13,7 @@ use crate::parallel::config::ParallelPhase2Config;
use crate::parallel::object_worker::{ use crate::parallel::object_worker::{
ObjectTaskExecutor, ObjectWorkerPool, ObjectWorkerSubmitError, ObjectTaskExecutor, ObjectWorkerPool, ObjectWorkerSubmitError,
}; };
use crate::policy::{Policy, SignedObjectFailurePolicy}; use crate::policy::{Policy, ResourceValidationMode, SignedObjectFailurePolicy};
use crate::report::{RfcRef, Warning}; use crate::report::{RfcRef, Warning};
use crate::storage::{ use crate::storage::{
PackFile, PackTime, RoaCacheObjectMeta, RoaCacheProjection, VcirLocalOutput, PackFile, PackTime, RoaCacheObjectMeta, RoaCacheProjection, VcirLocalOutput,
@ -1226,6 +1226,7 @@ pub fn process_publication_point_for_issuer_with_cache_options<P: PublicationPoi
collect_vcir_local_outputs, collect_vcir_local_outputs,
policy.strict.cms_der, policy.strict.cms_der,
policy.strict.name, policy.strict.name,
policy.resource_validation_mode,
) )
} }
blocked @ (RoaCacheLookupResult::HashBlocked blocked @ (RoaCacheLookupResult::HashBlocked
@ -1251,6 +1252,7 @@ pub fn process_publication_point_for_issuer_with_cache_options<P: PublicationPoi
collect_vcir_local_outputs, collect_vcir_local_outputs,
policy.strict.cms_der, policy.strict.cms_der,
policy.strict.name, policy.strict.name,
policy.resource_validation_mode,
) )
} }
} }
@ -1273,6 +1275,7 @@ pub fn process_publication_point_for_issuer_with_cache_options<P: PublicationPoi
collect_vcir_local_outputs, collect_vcir_local_outputs,
policy.strict.cms_der, policy.strict.cms_der,
policy.strict.name, policy.strict.name,
policy.resource_validation_mode,
) )
}; };
match result.outcome { match result.outcome {
@ -1386,6 +1389,7 @@ pub fn process_publication_point_for_issuer_with_cache_options<P: PublicationPoi
collect_vcir_local_outputs, collect_vcir_local_outputs,
policy.strict.cms_der, policy.strict.cms_der,
policy.strict.name, policy.strict.name,
policy.resource_validation_mode,
) { ) {
Ok((att, local_output)) => { Ok((att, local_output)) => {
stats.aspa_ok += 1; stats.aspa_ok += 1;
@ -1735,6 +1739,7 @@ pub(crate) struct RoaTaskShared {
issuer_resources_index: Arc<IssuerResourcesIndex>, issuer_resources_index: Arc<IssuerResourcesIndex>,
issuer_effective_ip: Option<Arc<crate::data_model::rc::IpResourceSet>>, issuer_effective_ip: Option<Arc<crate::data_model::rc::IpResourceSet>>,
issuer_effective_as: Option<Arc<crate::data_model::rc::AsResourceSet>>, issuer_effective_as: Option<Arc<crate::data_model::rc::AsResourceSet>>,
resource_validation_mode: ResourceValidationMode,
} }
#[derive(Clone)] #[derive(Clone)]
@ -1746,6 +1751,7 @@ pub(crate) struct OwnedRoaTask {
collect_vcir_local_outputs: bool, collect_vcir_local_outputs: bool,
strict_cms_der: bool, strict_cms_der: bool,
strict_name: bool, strict_name: bool,
resource_validation_mode: ResourceValidationMode,
pub(crate) submitted_at: Option<Instant>, pub(crate) submitted_at: Option<Instant>,
} }
@ -1854,6 +1860,7 @@ fn validate_owned_roa_task(worker_index: usize, task: OwnedRoaTask) -> RoaTaskRe
task.collect_vcir_local_outputs, task.collect_vcir_local_outputs,
task.strict_cms_der, task.strict_cms_der,
task.strict_name, task.strict_name,
task.resource_validation_mode,
) )
.map(|(vrps, local_outputs, cache_object_meta)| RoaTaskOk { .map(|(vrps, local_outputs, cache_object_meta)| RoaTaskOk {
vrps, vrps,
@ -1884,6 +1891,7 @@ pub(crate) struct ParallelObjectsStage {
collect_vcir_local_outputs: bool, collect_vcir_local_outputs: bool,
strict_cms_der: bool, strict_cms_der: bool,
strict_name: bool, strict_name: bool,
resource_validation_mode: ResourceValidationMode,
roa_task_indices: Vec<usize>, roa_task_indices: Vec<usize>,
cached_roa_results: Vec<RoaTaskResult>, cached_roa_results: Vec<RoaTaskResult>,
roa_cache_stats: RoaValidationCacheStats, roa_cache_stats: RoaValidationCacheStats,
@ -1921,6 +1929,7 @@ impl ParallelObjectsStage {
collect_vcir_local_outputs: self.collect_vcir_local_outputs, collect_vcir_local_outputs: self.collect_vcir_local_outputs,
strict_cms_der: self.strict_cms_der, strict_cms_der: self.strict_cms_der,
strict_name: self.strict_name, strict_name: self.strict_name,
resource_validation_mode: self.resource_validation_mode,
submitted_at: None, submitted_at: None,
}); });
}); });
@ -2257,11 +2266,13 @@ pub(crate) fn prepare_publication_point_for_parallel_roa_with_cache<P: Publicati
)), )),
issuer_effective_ip: issuer_effective_ip.cloned().map(Arc::new), issuer_effective_ip: issuer_effective_ip.cloned().map(Arc::new),
issuer_effective_as: issuer_effective_as.cloned().map(Arc::new), issuer_effective_as: issuer_effective_as.cloned().map(Arc::new),
resource_validation_mode: policy.resource_validation_mode,
}), }),
validation_time, validation_time,
collect_vcir_local_outputs, collect_vcir_local_outputs,
strict_cms_der: policy.strict.cms_der, strict_cms_der: policy.strict.cms_der,
strict_name: policy.strict.name, strict_name: policy.strict.name,
resource_validation_mode: policy.resource_validation_mode,
roa_task_indices, roa_task_indices,
cached_roa_results, cached_roa_results,
roa_cache_stats, roa_cache_stats,
@ -2370,6 +2381,7 @@ pub(crate) fn reduce_parallel_roa_stage(
collect_vcir_local_outputs, collect_vcir_local_outputs,
strict_cms_der, strict_cms_der,
strict_name, strict_name,
shared.resource_validation_mode,
) { ) {
Ok((att, local_output)) => { Ok((att, local_output)) => {
stats.aspa_ok += 1; stats.aspa_ok += 1;
@ -2604,6 +2616,7 @@ pub(crate) fn validate_roa_task_serial(
collect_vcir_local_outputs: bool, collect_vcir_local_outputs: bool,
strict_cms_der: bool, strict_cms_der: bool,
strict_name: bool, strict_name: bool,
resource_validation_mode: ResourceValidationMode,
) -> RoaTaskResult { ) -> RoaTaskResult {
let outcome = process_roa_with_issuer( let outcome = process_roa_with_issuer(
task.file, task.file,
@ -2621,6 +2634,7 @@ pub(crate) fn validate_roa_task_serial(
collect_vcir_local_outputs, collect_vcir_local_outputs,
strict_cms_der, strict_cms_der,
strict_name, strict_name,
resource_validation_mode,
) )
.map(|(vrps, local_outputs, cache_object_meta)| RoaTaskOk { .map(|(vrps, local_outputs, cache_object_meta)| RoaTaskOk {
vrps, vrps,
@ -2655,6 +2669,7 @@ fn process_roa_with_issuer(
collect_vcir_local_outputs: bool, collect_vcir_local_outputs: bool,
strict_cms_der: bool, strict_cms_der: bool,
strict_name: bool, strict_name: bool,
resource_validation_mode: ResourceValidationMode,
) -> Result<(Vec<Vrp>, Vec<VcirLocalOutput>, Option<RoaCacheObjectMeta>), ObjectValidateError> { ) -> Result<(Vec<Vrp>, Vec<VcirLocalOutput>, Option<RoaCacheObjectMeta>), ObjectValidateError> {
let _decode = timing let _decode = timing
.as_ref() .as_ref()
@ -2706,15 +2721,16 @@ fn process_roa_with_issuer(
let _subset = timing let _subset = timing
.as_ref() .as_ref()
.map(|t| t.span_phase("objects_roa_validate_ee_resources_subset_total")); .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, &ee.resource_cert,
issuer_effective_ip, issuer_effective_ip,
issuer_effective_as, issuer_effective_as,
issuer_resources_index, issuer_resources_index,
resource_validation_mode,
)?; )?;
drop(_subset); 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 { let cache_object_meta = RoaCacheObjectMeta {
source_object_uri: file.rsync_uri.clone(), source_object_uri: file.rsync_uri.clone(),
source_object_hash: file.sha256, source_object_hash: file.sha256,
@ -2777,6 +2793,7 @@ fn process_roa_with_issuer_parallel_cached(
collect_vcir_local_outputs: bool, collect_vcir_local_outputs: bool,
strict_cms_der: bool, strict_cms_der: bool,
strict_name: bool, strict_name: bool,
resource_validation_mode: ResourceValidationMode,
) -> Result<(Vec<Vrp>, Vec<VcirLocalOutput>, Option<RoaCacheObjectMeta>), ObjectValidateError> { ) -> Result<(Vec<Vrp>, Vec<VcirLocalOutput>, Option<RoaCacheObjectMeta>), ObjectValidateError> {
let _decode = timing let _decode = timing
.as_ref() .as_ref()
@ -2834,15 +2851,16 @@ fn process_roa_with_issuer_parallel_cached(
let _subset = timing let _subset = timing
.as_ref() .as_ref()
.map(|t| t.span_phase("objects_roa_validate_ee_resources_subset_total")); .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, &ee.resource_cert,
issuer_effective_ip, issuer_effective_ip,
issuer_effective_as, issuer_effective_as,
issuer_resources_index, issuer_resources_index,
resource_validation_mode,
)?; )?;
drop(_subset); 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 { let cache_object_meta = RoaCacheObjectMeta {
source_object_uri: file.rsync_uri.clone(), source_object_uri: file.rsync_uri.clone(),
source_object_hash: file.sha256, source_object_hash: file.sha256,
@ -2905,6 +2923,7 @@ fn process_aspa_with_issuer(
collect_vcir_local_outputs: bool, collect_vcir_local_outputs: bool,
strict_cms_der: bool, strict_cms_der: bool,
strict_name: bool, strict_name: bool,
resource_validation_mode: ResourceValidationMode,
) -> Result<(AspaAttestation, Option<VcirLocalOutput>), ObjectValidateError> { ) -> Result<(AspaAttestation, Option<VcirLocalOutput>), ObjectValidateError> {
let _decode = timing let _decode = timing
.as_ref() .as_ref()
@ -2956,14 +2975,17 @@ fn process_aspa_with_issuer(
let _subset = timing let _subset = timing
.as_ref() .as_ref()
.map(|t| t.span_phase("objects_aspa_validate_ee_resources_subset_total")); .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, &ee.resource_cert,
issuer_effective_ip, issuer_effective_ip,
issuer_effective_as, issuer_effective_as,
issuer_resources_index, issuer_resources_index,
resource_validation_mode,
)?; )?;
drop(_subset); drop(_subset);
validate_aspa_customer_in_vrs(&aspa, ee_vrs.asn.as_ref())?;
let attestation = AspaAttestation { let attestation = AspaAttestation {
customer_as_id: aspa.aspa.customer_as_id, customer_as_id: aspa.aspa.customer_as_id,
provider_as_ids: aspa.aspa.provider_as_ids.clone(), provider_as_ids: aspa.aspa.provider_as_ids.clone(),
@ -3119,6 +3141,176 @@ fn validate_ee_resources_subset(
Ok(()) Ok(())
} }
#[derive(Debug)]
struct EeVerifiedResources {
ip: Option<crate::data_model::rc::IpResourceSet>,
asn: Option<crate::data_model::rc::AsResourceSet>,
}
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<EeVerifiedResources, ObjectValidateError> {
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<crate::data_model::rc::IpResourceSet, ObjectValidateError> {
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<crate::data_model::rc::AsResourceSet, ObjectValidateError> {
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<Vec<Vrp>, 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 { 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.asnum.as_ref(), parent.asnum.as_ref())
&& as_choice_subset(child.rdi.as_ref(), parent.rdi.as_ref()) && as_choice_subset(child.rdi.as_ref(), parent.rdi.as_ref())
@ -3292,6 +3484,121 @@ fn ip_resources_is_subset_indexed(
true true
} }
fn ip_items_to_merged_intervals(
items: &[crate::data_model::rc::IpAddressOrRange],
) -> Vec<(Vec<u8>, Vec<u8>)> {
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<u8>, Vec<u8>)],
) -> Vec<(Vec<u8>, Vec<u8>)> {
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<u8>, Vec<u8>)],
) -> Vec<crate::data_model::rc::IpAddressOrRange> {
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<u8> {
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<crate::data_model::rc::AsIdOrRange> {
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)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
enum AfiKey { enum AfiKey {
V4, V4,
@ -4724,6 +5031,120 @@ mod tests {
assert!(matches!(err, ObjectValidateError::EeResourcesNotSubset)); 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] #[test]
fn extra_rfc_refs_for_crl_selection_distinguishes_crl_errors() { fn extra_rfc_refs_for_crl_selection_distinguishes_crl_errors() {
assert_eq!( assert_eq!(
@ -4976,11 +5397,13 @@ mod tests {
issuer_resources_index: Arc::new(IssuerResourcesIndex::default()), issuer_resources_index: Arc::new(IssuerResourcesIndex::default()),
issuer_effective_ip: None, issuer_effective_ip: None,
issuer_effective_as: None, issuer_effective_as: None,
resource_validation_mode: ResourceValidationMode::default(),
}), }),
validation_time: OffsetDateTime::now_utc(), validation_time: OffsetDateTime::now_utc(),
collect_vcir_local_outputs: false, collect_vcir_local_outputs: false,
strict_cms_der: false, strict_cms_der: false,
strict_name: false, strict_name: false,
resource_validation_mode: ResourceValidationMode::default(),
roa_task_indices: vec![0, 1], roa_task_indices: vec![0, 1],
cached_roa_results: Vec::new(), cached_roa_results: Vec::new(),
roa_cache_stats: RoaValidationCacheStats::default(), roa_cache_stats: RoaValidationCacheStats::default(),

View File

@ -21,7 +21,7 @@ use crate::data_model::router_cert::{
use crate::fetch::rsync::RsyncFetcher; use crate::fetch::rsync::RsyncFetcher;
use crate::parallel::config::ParallelPhase2Config; use crate::parallel::config::ParallelPhase2Config;
use crate::parallel::repo_runtime::{RepoSyncRuntime, RepoSyncRuntimeOutcome}; use crate::parallel::repo_runtime::{RepoSyncRuntime, RepoSyncRuntimeOutcome};
use crate::policy::Policy; use crate::policy::{Policy, ResourceValidationMode};
use crate::replay::archive::ReplayArchiveIndex; use crate::replay::archive::ReplayArchiveIndex;
use crate::replay::delta_archive::ReplayDeltaArchiveIndex; use crate::replay::delta_archive::ReplayDeltaArchiveIndex;
use crate::report::{RfcRef, Warning}; use crate::report::{RfcRef, Warning};
@ -932,6 +932,7 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> {
&fresh_point, &fresh_point,
self.validation_time, self.validation_time,
self.timing.as_ref(), self.timing.as_ref(),
self.policy,
if self.enable_child_certificate_validation_cache { if self.enable_child_certificate_validation_cache {
Some(ChildCertificateValidationCacheContext { Some(ChildCertificateValidationCacheContext {
store: self.store, store: self.store,
@ -2259,12 +2260,14 @@ fn discover_children_from_fresh_snapshot_with_audit<P: PublicationPointData>(
return Err("lazy CA certificate requires store-backed child discovery".to_string()); 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( discover_children_from_fresh_snapshot_with_audit_cached_with_issuer_der(
issuer, issuer,
issuer_ca_der, issuer_ca_der,
publication_point, publication_point,
validation_time, validation_time,
timing, timing,
&default_policy,
None, None,
) )
} }
@ -2282,12 +2285,14 @@ fn discover_children_from_fresh_snapshot_with_audit_cached<P: PublicationPointDa
return Err("lazy CA certificate requires store-backed child discovery".to_string()); 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( discover_children_from_fresh_snapshot_with_audit_cached_with_issuer_der(
issuer, issuer,
issuer_ca_der, issuer_ca_der,
publication_point, publication_point,
validation_time, validation_time,
timing, timing,
&default_policy,
cache_context, cache_context,
) )
} }
@ -2300,6 +2305,7 @@ fn discover_children_from_fresh_snapshot_with_audit_cached_with_issuer_der<
publication_point: &P, publication_point: &P,
validation_time: time::OffsetDateTime, validation_time: time::OffsetDateTime,
timing: Option<&TimingHandle>, timing: Option<&TimingHandle>,
policy: &Policy,
cache_context: Option<ChildCertificateValidationCacheContext<'_>>, cache_context: Option<ChildCertificateValidationCacheContext<'_>>,
) -> Result<ChildDiscoveryOutput, String> { ) -> Result<ChildDiscoveryOutput, String> {
let locked_files = publication_point.files(); 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.effective_as_resources.as_ref(),
&issuer_resources_index, &issuer_resources_index,
validation_time, validation_time,
policy.resource_validation_mode,
) { ) {
Ok(v) => v, Ok(v) => v,
Err(CaPathError::ChildNotCa) => { Err(CaPathError::ChildNotCa) => {
@ -2949,12 +2956,32 @@ fn discover_children_from_fresh_snapshot_with_audit_cached_with_issuer_der<
match router_result { match router_result {
Ok(router) => { 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); router_ok = router_ok.saturating_add(1);
let source_object_hash = sha256_hex_from_32(&f.sha256); let source_object_hash = sha256_hex_from_32(&f.sha256);
let item_effective_until = PackTime::from_utc_offset_datetime( let item_effective_until = PackTime::from_utc_offset_datetime(
router.resource_cert.tbs.validity_not_after, router.resource_cert.tbs.validity_not_after,
); );
for as_id in &router.asns { for as_id in &router_asns {
router_keys.push(RouterKeyPayload { router_keys.push(RouterKeyPayload {
as_id: *as_id, as_id: *as_id,
ski: router.subject_key_identifier.clone(), 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_not_before,
effective_until, effective_until,
payload: ChildCertificateCachePayload::Router { payload: ChildCertificateCachePayload::Router {
router_keys: router router_keys: router_asns
.asns
.iter() .iter()
.map(|as_id| ChildCertificateCacheRouterKeyProjection { .map(|as_id| ChildCertificateCacheRouterKeyProjection {
as_id: *as_id, 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 validate_sub_ca_nanos = validate_sub_ca_nanos
.saturating_add(t1.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64); .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()); let eff_ip_items = ip_item_count(validated.effective_ip_resources.as_ref());
match bucketize(eff_ip_items) { match bucketize(eff_ip_items) {
0 => eff_ip_items_bucket_le_10 = eff_ip_items_bucket_le_10.saturating_add(1), 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<Vec<u32>, 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<u32> = 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<u32> = 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>( fn select_issuer_crl_uri_for_child<'a>(
child: &'a crate::data_model::rc::ResourceCertificate, child: &'a crate::data_model::rc::ResourceCertificate,
crl_cache: &std::collections::HashMap<String, CachedIssuerCrl>, crl_cache: &std::collections::HashMap<String, CachedIssuerCrl>,
@ -3568,6 +3667,7 @@ fn validate_subordinate_ca_cert_with_cached_issuer(
issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>, issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
issuer_resources_index: &IssuerEffectiveResourcesIndex, issuer_resources_index: &IssuerEffectiveResourcesIndex,
validation_time: time::OffsetDateTime, validation_time: time::OffsetDateTime,
resource_validation_mode: crate::policy::ResourceValidationMode,
) -> Result<ValidatedSubordinateCaLite, CaPathError> { ) -> Result<ValidatedSubordinateCaLite, CaPathError> {
let verified_crl = ensure_issuer_crl_verified(issuer_crl_rsync_uri, crl_cache, issuer_ca_der)?; 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_effective_as,
issuer_resources_index, issuer_resources_index,
validation_time, validation_time,
resource_validation_mode,
) )
} }

View File

@ -1,5 +1,5 @@
use super::*; 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::data_model::roa::RoaAfi;
use crate::fetch::rsync::LocalDirRsyncFetcher; use crate::fetch::rsync::LocalDirRsyncFetcher;
use crate::fetch::rsync::{RsyncFetchError, RsyncFetcher}; use crate::fetch::rsync::{RsyncFetchError, RsyncFetcher};
@ -29,6 +29,48 @@ fn ipv4_addr(octets: [u8; 4]) -> [u8; 16] {
addr 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; struct NeverHttpFetcher;
impl Fetcher for NeverHttpFetcher { impl Fetcher for NeverHttpFetcher {
fn fetch(&self, _uri: &str) -> Result<Vec<u8>, String> { fn fetch(&self, _uri: &str) -> Result<Vec<u8>, String> {