20260701 add validation-update-03 VRS mode
This commit is contained in:
parent
a501cfb709
commit
a5b194ce35
@ -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
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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` 复用流程。
|
||||||
|
|
||||||
## 架构检查
|
## 架构检查
|
||||||
|
|
||||||
关键脚本会读取:
|
关键脚本会读取:
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
23
src/cli.rs
23
src/cli.rs
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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");
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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(),
|
||||||
|
|||||||
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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> {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user