20260828 收口Feature151 TA constraints优化与验证

This commit is contained in:
yuyr 2026-08-28 17:42:04 +08:00
parent b59adc65a8
commit 74cbebbd33
18 changed files with 3854 additions and 40 deletions

View File

@ -121,6 +121,7 @@ done
cp -a "$REPO_ROOT/tests/fixtures/tal" "$STAGE_DIR/fixtures/" cp -a "$REPO_ROOT/tests/fixtures/tal" "$STAGE_DIR/fixtures/"
cp -a "$REPO_ROOT/tests/fixtures/ta" "$STAGE_DIR/fixtures/" cp -a "$REPO_ROOT/tests/fixtures/ta" "$STAGE_DIR/fixtures/"
cp -a "$REPO_ROOT/tests/fixtures/ta_constraints" "$STAGE_DIR/fixtures/"
cp -a "$REPO_ROOT/scripts/periodic" "$STAGE_DIR/scripts/" cp -a "$REPO_ROOT/scripts/periodic" "$STAGE_DIR/scripts/"
cp -a "$REPO_ROOT/scripts/cir" "$STAGE_DIR/scripts/" cp -a "$REPO_ROOT/scripts/cir" "$STAGE_DIR/scripts/"
cp -a "$REPO_ROOT/scripts/inter_rp" "$STAGE_DIR/scripts/" cp -a "$REPO_ROOT/scripts/inter_rp" "$STAGE_DIR/scripts/"

View File

@ -100,6 +100,15 @@ ENABLE_CHILD_CERTIFICATE_VALIDATION_CACHE=0
# 传给 rpki 子进程的额外参数。多个参数用空格分隔。 # 传给 rpki 子进程的额外参数。多个参数用空格分隔。
# 示例RPKI_EXTRA_ARGS="--enable-roa-validation-cache" # 示例RPKI_EXTRA_ARGS="--enable-roa-validation-cache"
# 实验性 transport 预热RPKI_EXTRA_ARGS="--enable-transport-request-prefetch --enable-roa-validation-cache" # 实验性 transport 预热RPKI_EXTRA_ARGS="--enable-transport-request-prefetch --enable-roa-validation-cache"
# TA constraints 的 RIPE 单 TA soak 示例policy 并非默认启用):
# RIRS=ripe
# RPKI_EXTRA_ARGS="--ta-constraints ripe-ncc=${PACKAGE_ROOT}/fixtures/ta_constraints/ripe-ncc-afrinic-deny.constraints"
# all5 性能 A/B 的完整 AFRINIC IPv4 deny 示例(仅四个非 AFRINIC TA
# RIRS=afrinic,apnic,arin,lacnic,ripe
# RPKI_EXTRA_ARGS="--ta-constraints apnic-rfc7730-https=${PACKAGE_ROOT}/fixtures/ta_constraints/afrinic-full-ipv4-deny.constraints --ta-constraints arin=${PACKAGE_ROOT}/fixtures/ta_constraints/afrinic-full-ipv4-deny.constraints --ta-constraints lacnic=${PACKAGE_ROOT}/fixtures/ta_constraints/afrinic-full-ipv4-deny.constraints --ta-constraints ripe-ncc=${PACKAGE_ROOT}/fixtures/ta_constraints/afrinic-full-ipv4-deny.constraints"
# M7 current-only AFRINIC holdingsdelegated status=allocated/assigned不含 legacy/ERX
# RIRS=afrinic,apnic,arin,lacnic,ripe
# RPKI_EXTRA_ARGS="--ta-constraints apnic-rfc7730-https=${PACKAGE_ROOT}/fixtures/ta_constraints/afrinic-current-ipv4-deny.constraints --ta-constraints arin=${PACKAGE_ROOT}/fixtures/ta_constraints/afrinic-current-ipv4-deny.constraints --ta-constraints lacnic=${PACKAGE_ROOT}/fixtures/ta_constraints/afrinic-current-ipv4-deny.constraints --ta-constraints ripe-ncc=${PACKAGE_ROOT}/fixtures/ta_constraints/afrinic-current-ipv4-deny.constraints"
RPKI_EXTRA_ARGS="" RPKI_EXTRA_ARGS=""
# 所有出站 HTTP 请求RRDP / TAL / TA / 死 repo 探针)的 User-Agent。 # 所有出站 HTTP 请求RRDP / TAL / TA / 死 repo 探针)的 User-Agent。

View File

@ -0,0 +1,178 @@
#!/usr/bin/env python3
"""Generate a current-only AFRINIC IPv4 holdings constraints fixture.
The AFRINIC delegated extended format contains one IPv4 allocation/assignment
per line. This tool keeps only active registry holdings (``allocated`` and
``assigned``), computes their exact union, and emits an allow-all policy with
the union as IPv4 deny entries. ``reserved`` and ``available`` records are
intentionally excluded.
"""
from __future__ import annotations
import argparse
import datetime as dt
import hashlib
import ipaddress
import pathlib
import sys
import urllib.error
import urllib.request
from collections import Counter
from typing import Iterable
DEFAULT_SOURCE_URL = (
"https://ftp.afrinic.net/pub/stats/afrinic/"
"delegated-afrinic-extended-latest"
)
ACTIVE_STATUSES = {"allocated", "assigned"}
def merge_intervals(
intervals: Iterable[tuple[int, int]],
) -> list[tuple[int, int]]:
merged: list[tuple[int, int]] = []
for start, end in sorted(intervals):
if merged and start <= merged[-1][1] + 1:
merged[-1] = (merged[-1][0], max(merged[-1][1], end))
else:
merged.append((start, end))
return merged
def parse_source(raw: bytes) -> tuple[list[tuple[int, int]], Counter[str], Counter[str]]:
status_counts: Counter[str] = Counter()
ipv4_status_counts: Counter[str] = Counter()
active: list[tuple[int, int]] = []
for line_number, raw_line in enumerate(raw.decode("utf-8", "replace").splitlines(), 1):
if not raw_line or raw_line.startswith("#"):
continue
fields = raw_line.split("|")
if len(fields) < 7 or fields[2] != "ipv4":
continue
status = fields[6]
status_counts[status] += 1
ipv4_status_counts[status] += 1
if status not in ACTIVE_STATUSES:
continue
try:
start = int(ipaddress.IPv4Address(fields[3]))
count = int(fields[4])
except (ValueError, TypeError) as error:
raise ValueError(f"line {line_number}: invalid IPv4 record: {raw_line}") from error
if count <= 0 or start + count > 1 << 32:
raise ValueError(f"line {line_number}: invalid IPv4 range: {raw_line}")
active.append((start, start + count - 1))
return active, status_counts, ipv4_status_counts
def render(
source_url: str,
retrieved: str,
source_sha256: str,
source_ipv4_count: int,
status_counts: Counter[str],
active_source: list[tuple[int, int]],
) -> tuple[str, dict[str, int]]:
merged = merge_intervals(active_source)
networks = [
network
for start, end in merged
for network in ipaddress.summarize_address_range(
ipaddress.IPv4Address(start), ipaddress.IPv4Address(end)
)
]
networks.sort(key=lambda network: int(network.network_address))
active_addresses = sum(end - start + 1 for start, end in merged)
counts = {
"source_ipv4_records": source_ipv4_count,
"allocated": status_counts["allocated"],
"assigned": status_counts["assigned"],
"reserved": status_counts["reserved"],
"available": status_counts["available"],
"selected_source_records": len(active_source),
"selected_addresses": active_addresses,
"merged_intervals": len(merged),
"cidr_rules": len(networks),
}
lines = [
"# Feature #151 M7 current AFRINIC IPv4 holdings deny fixture.",
"#",
"# Source: AFRINIC delegated extended latest.",
f"# URL: {source_url}",
f"# Retrieved: {retrieved}",
f"# Source SHA-256: {source_sha256}",
"#",
"# Selection: IPv4 records with status allocated or assigned only.",
"# Excluded statuses: reserved and available (not current registry holdings).",
(
"# Source IPv4 records: {source_ipv4_records}; allocated={allocated}; "
"assigned={assigned}; reserved={reserved}; available={available}."
).format(**counts),
"# Selected source records: {selected_source_records}; selected addresses: {selected_addresses}.".format(
**counts
),
"# Overlap/adjacency union intervals: {merged_intervals}; exact CIDR deny rules: {cidr_rules}.".format(
**counts
),
"# IPv6 and ASN remain allow-all to isolate the current AFRINIC IPv4 policy.",
"allow 0.0.0.0/0",
"allow ::/0",
"allow 0 - 4294967295",
"",
]
lines.extend(f"deny {network}" for network in networks)
return "\n".join(lines) + "\n", counts
def fetch_source(url: str, timeout: float) -> bytes:
request = urllib.request.Request(
url,
headers={"User-Agent": "rpki-dev-feature151-afrinic-holdings/1.0"},
)
with urllib.request.urlopen(request, timeout=timeout) as response:
return response.read()
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
source = parser.add_mutually_exclusive_group()
source.add_argument("--url", default=DEFAULT_SOURCE_URL, help="AFRINIC source URL")
source.add_argument("--input", type=pathlib.Path, help="local delegated file for replay")
parser.add_argument("--output", type=pathlib.Path, required=True)
parser.add_argument("--retrieved", help="UTC date recorded in the fixture header")
parser.add_argument("--timeout", type=float, default=60.0)
args = parser.parse_args()
source_url = args.url if args.input is None else f"file://{args.input.resolve()}"
raw = args.input.read_bytes() if args.input is not None else fetch_source(args.url, args.timeout)
retrieved = args.retrieved or dt.datetime.now(dt.timezone.utc).date().isoformat()
active, status_counts, ipv4_status_counts = parse_source(raw)
rendered, counts = render(
source_url,
retrieved,
hashlib.sha256(raw).hexdigest(),
sum(ipv4_status_counts.values()),
status_counts,
active,
)
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(rendered, encoding="utf-8")
print(
"generated {output}: {cidr_rules} CIDRs from {selected_source_records} active "
"records ({selected_addresses} addresses); source_sha256={sha256}".format(
output=args.output,
sha256=hashlib.sha256(raw).hexdigest(),
**counts,
)
)
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except (OSError, ValueError, urllib.error.URLError) as error:
print(f"error: {error}", file=sys.stderr)
raise SystemExit(2)

View File

@ -26,6 +26,7 @@ use crate::parallel::config::{ParallelPhase1Config, ParallelPhase2Config};
use crate::parallel::types::TalInputSpec; use crate::parallel::types::TalInputSpec;
use crate::policy::{Policy, ResourceValidationMode, StrictPolicy}; use crate::policy::{Policy, ResourceValidationMode, StrictPolicy};
use crate::storage::{RocksStore, VcirStorageSummary}; use crate::storage::{RocksStore, VcirStorageSummary};
use crate::ta_constraints::TaConstraintsByTal;
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,
run_tree_from_multiple_tals_parallel_phase2_audit_with_timing, run_tree_from_multiple_tals_parallel_phase2_audit_with_timing,
@ -131,6 +132,7 @@ pub struct CliArgs {
pub parallel_phase1_config: ParallelPhase1Config, pub parallel_phase1_config: ParallelPhase1Config,
pub parallel_phase2_config: ParallelPhase2Config, pub parallel_phase2_config: ParallelPhase2Config,
pub tal_inputs: Vec<TalInputSpec>, pub tal_inputs: Vec<TalInputSpec>,
pub ta_constraints: TaConstraintsByTal,
pub db_path: PathBuf, pub db_path: PathBuf,
pub raw_store_db: Option<PathBuf>, pub raw_store_db: Option<PathBuf>,
@ -210,6 +212,8 @@ Options:
--raw-store-db <path> External raw-by-hash store DB path (optional) --raw-store-db <path> External raw-by-hash store DB path (optional)
--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)
--ta-constraints <tal-id>=<path>
Apply local EE-resource constraints to one TAL (repeatable); adjacent <tal>.constraints files are auto-discovered
--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-validation-mode <validation-update-03|rfc6487>
Resource certificate validation mode (default: validation-update-03) Resource certificate validation mode (default: validation-update-03)
@ -318,6 +322,7 @@ pub fn parse_args(argv: &[String]) -> Result<CliArgs, String> {
let mut tal_urls: Vec<String> = Vec::new(); let mut tal_urls: Vec<String> = Vec::new();
let mut tal_paths: Vec<PathBuf> = Vec::new(); let mut tal_paths: Vec<PathBuf> = Vec::new();
let mut ta_paths: Vec<PathBuf> = Vec::new(); let mut ta_paths: Vec<PathBuf> = Vec::new();
let mut ta_constraint_specs: Vec<String> = Vec::new();
let mut parallel_phase1_cfg = ParallelPhase1Config::default(); let mut parallel_phase1_cfg = ParallelPhase1Config::default();
let mut parallel_phase2_cfg = ParallelPhase2Config::default(); let mut parallel_phase2_cfg = ParallelPhase2Config::default();
let mut dead_repo_blacklist_path: Option<PathBuf> = None; let mut dead_repo_blacklist_path: Option<PathBuf> = None;
@ -427,6 +432,13 @@ pub fn parse_args(argv: &[String]) -> Result<CliArgs, String> {
let v = argv.get(i).ok_or("--ta-path requires a value")?; let v = argv.get(i).ok_or("--ta-path requires a value")?;
ta_paths.push(PathBuf::from(v)); ta_paths.push(PathBuf::from(v));
} }
"--ta-constraints" => {
i += 1;
let v = argv
.get(i)
.ok_or("--ta-constraints requires <tal-id>=<path>")?;
ta_constraint_specs.push(v.clone());
}
"--parallel-max-repo-sync-workers-global" => { "--parallel-max-repo-sync-workers-global" => {
i += 1; i += 1;
let v = argv let v = argv
@ -989,6 +1001,12 @@ pub fn parse_args(argv: &[String]) -> Result<CliArgs, String> {
)); ));
} }
} }
if verification_only && !ta_constraint_specs.is_empty() {
return Err(format!(
"--ta-constraints is not supported with --verification-only\\n\\n{}",
usage()
));
}
let tal_url = tal_urls.first().cloned(); let tal_url = tal_urls.first().cloned();
let tal_path = tal_paths.first().cloned(); let tal_path = tal_paths.first().cloned();
let ta_path = ta_paths.first().cloned(); let ta_path = ta_paths.first().cloned();
@ -1147,6 +1165,7 @@ pub fn parse_args(argv: &[String]) -> Result<CliArgs, String> {
tal_inputs.extend(tal_paths.iter().cloned().map(TalInputSpec::from_file_path)); tal_inputs.extend(tal_paths.iter().cloned().map(TalInputSpec::from_file_path));
} }
} }
let ta_constraints = TaConstraintsByTal::load_for_tals(&tal_inputs, &ta_constraint_specs)?;
if dead_repo_blacklist_fail_threshold.is_some() && dead_repo_blacklist_path.is_none() { if dead_repo_blacklist_fail_threshold.is_some() && dead_repo_blacklist_path.is_none() {
return Err( return Err(
@ -1183,6 +1202,7 @@ pub fn parse_args(argv: &[String]) -> Result<CliArgs, String> {
parallel_phase1_config: parallel_phase1_cfg, parallel_phase1_config: parallel_phase1_cfg,
parallel_phase2_config: parallel_phase2_cfg, parallel_phase2_config: parallel_phase2_cfg,
tal_inputs, tal_inputs,
ta_constraints,
db_path, db_path,
raw_store_db, raw_store_db,
repo_bytes_db, repo_bytes_db,
@ -2090,7 +2110,11 @@ where
H: crate::sync::rrdp::Fetcher + Clone + 'static, H: crate::sync::rrdp::Fetcher + Clone + 'static,
R: crate::fetch::rsync::RsyncFetcher + Clone + 'static, R: crate::fetch::rsync::RsyncFetcher + Clone + 'static,
{ {
if args.verification_only || args.tal_inputs.len() > 1 { // The multi-TAL entry point preserves the TAL id supplied by the CLI.
// A single-file TAL may otherwise derive its id from the embedded TA URI,
// which is intentionally different from the local filename used for
// adjacent <tal>.constraints discovery.
if args.verification_only || args.tal_inputs.len() > 1 || !policy.ta_constraints.is_empty() {
return if let Some(t) = timing { return if let Some(t) = timing {
run_tree_from_multiple_tals_parallel_phase2_audit_with_timing( run_tree_from_multiple_tals_parallel_phase2_audit_with_timing(
store, store,
@ -2322,6 +2346,10 @@ pub fn run(argv: &[String]) -> Result<(), String> {
if args.disable_rrdp { if args.disable_rrdp {
policy.sync_preference = crate::policy::SyncPreference::RsyncOnly; policy.sync_preference = crate::policy::SyncPreference::RsyncOnly;
} }
policy.ta_constraints = args.ta_constraints.clone();
for warning in policy.ta_constraints.configuration_warnings() {
eprintln!("warning: {warning}");
}
} }
let validation_time = args let validation_time = args
.validation_time .validation_time
@ -3050,6 +3078,10 @@ pub fn run(argv: &[String]) -> Result<(), String> {
}, },
args.rsync_scope_policy, args.rsync_scope_policy,
)?; )?;
if !policy.ta_constraints.is_empty() {
contract.ta_constraints_fingerprint =
Some(policy.ta_constraints.fingerprint_sha256_hex());
}
contract.fallback_replay_selections = contract.fallback_replay_selections =
crate::verification_only::collect_fallback_replay_selections( crate::verification_only::collect_fallback_replay_selections(
store.as_ref(), store.as_ref(),

View File

@ -23,6 +23,7 @@ fn parse_help_returns_usage() {
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!(err.contains("--resource-validation-mode"), "{err}");
assert!(err.contains("--ta-constraints"), "{err}");
assert!(err.contains("--max-ca-depth"), "{err}"); assert!(err.contains("--max-ca-depth"), "{err}");
assert!(err.contains("default: 32"), "{err}"); assert!(err.contains("default: 32"), "{err}");
assert!( assert!(
@ -37,6 +38,26 @@ fn parse_help_returns_usage() {
assert!(!err.contains("--parallel-phase2 "), "{err}"); assert!(!err.contains("--parallel-phase2 "), "{err}");
} }
#[test]
fn parse_accepts_explicit_ta_constraints_for_known_tal() {
let dir = tempfile::tempdir().expect("tmpdir");
let constraints_path = dir.path().join("example.constraints");
std::fs::write(&constraints_path, "allow 192.0.2.0/24\n").expect("write constraints");
let argv = vec![
"rpki".to_string(),
"--db".to_string(),
"db".to_string(),
"--tal-path".to_string(),
"example.tal".to_string(),
"--ta-path".to_string(),
"example.cer".to_string(),
"--ta-constraints".to_string(),
format!("example={}", constraints_path.display()),
];
let args = parse_args(&argv).expect("parse args");
assert!(!args.ta_constraints.is_empty());
}
#[test] #[test]
fn parse_rejects_unknown_argument() { fn parse_rejects_unknown_argument() {
let argv = vec![ let argv = vec![
@ -119,6 +140,7 @@ fn verification_policy_preserves_contract_identity_and_selects_only_bound_fallba
sync_preference: crate::policy::SyncPreference::RrdpThenRsync, sync_preference: crate::policy::SyncPreference::RrdpThenRsync,
..Policy::default() ..Policy::default()
}, },
ta_constraints_fingerprint: None,
max_ca_depth: 32, max_ca_depth: 32,
max_instances: None, max_instances: None,
cache: crate::verification_only::ValidationCacheContract { cache: crate::verification_only::ValidationCacheContract {

View File

@ -40,6 +40,8 @@ pub mod storage;
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod sync; pub mod sync;
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod ta_constraints;
#[cfg(feature = "full")]
pub mod tools; pub mod tools;
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod validation; pub mod validation;

View File

@ -1,6 +1,8 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::BTreeSet; use std::collections::BTreeSet;
use crate::ta_constraints::TaConstraintsByTal;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum SyncPreference { pub enum SyncPreference {
@ -133,6 +135,10 @@ pub struct Policy {
/// the already contract-bound VCIR fallback branch while replaying a run. /// the already contract-bound VCIR fallback branch while replaying a run.
#[serde(skip)] #[serde(skip)]
pub verification_forced_vcir_reuse_manifest_uris: BTreeSet<String>, pub verification_forced_vcir_reuse_manifest_uris: BTreeSet<String>,
/// Locally configured, per-TAL EE certificate resource constraints. They
/// are intentionally CLI/runtime-only rather than policy-file input.
#[serde(skip)]
pub ta_constraints: TaConstraintsByTal,
} }
impl Default for Policy { impl Default for Policy {
@ -144,6 +150,7 @@ impl Default for Policy {
resource_validation_mode: ResourceValidationMode::default(), resource_validation_mode: ResourceValidationMode::default(),
strict: StrictPolicy::default(), strict: StrictPolicy::default(),
verification_forced_vcir_reuse_manifest_uris: BTreeSet::new(), verification_forced_vcir_reuse_manifest_uris: BTreeSet::new(),
ta_constraints: TaConstraintsByTal::default(),
} }
} }
} }

1139
src/ta_constraints.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -961,6 +961,38 @@ pub fn process_publication_point_for_issuer_with_cache_options<P: PublicationPoi
timing: Option<&TimingHandle>, timing: Option<&TimingHandle>,
collect_vcir_local_outputs: bool, collect_vcir_local_outputs: bool,
roa_cache: RoaValidationCacheInput<'_>, roa_cache: RoaValidationCacheInput<'_>,
) -> ObjectsOutput {
process_publication_point_for_issuer_with_cache_options_and_ta_constraints(
publication_point,
policy,
issuer_ca_der,
issuer_ca_rsync_uri,
issuer_effective_ip,
issuer_effective_as,
validation_time,
timing,
collect_vcir_local_outputs,
roa_cache,
None,
)
}
/// Serial signed-object processing with an optional, locally configured
/// constraint set for the TA that owns this publication-point tree.
pub fn process_publication_point_for_issuer_with_cache_options_and_ta_constraints<
P: PublicationPointData,
>(
publication_point: &P,
policy: &Policy,
issuer_ca_der: &[u8],
issuer_ca_rsync_uri: Option<&str>,
issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>,
issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
validation_time: time::OffsetDateTime,
timing: Option<&TimingHandle>,
collect_vcir_local_outputs: bool,
roa_cache: RoaValidationCacheInput<'_>,
ta_constraints: Option<&crate::ta_constraints::TaConstraints>,
) -> ObjectsOutput { ) -> ObjectsOutput {
let manifest_rsync_uri = publication_point.manifest_rsync_uri(); let manifest_rsync_uri = publication_point.manifest_rsync_uri();
let manifest_bytes = publication_point.manifest_bytes(); let manifest_bytes = publication_point.manifest_bytes();
@ -1271,6 +1303,7 @@ pub fn process_publication_point_for_issuer_with_cache_options<P: PublicationPoi
policy.strict.cms_der, policy.strict.cms_der,
policy.strict.name, policy.strict.name,
policy.resource_validation_mode, policy.resource_validation_mode,
ta_constraints,
) )
} }
blocked @ (RoaCacheLookupResult::HashBlocked blocked @ (RoaCacheLookupResult::HashBlocked
@ -1297,6 +1330,7 @@ pub fn process_publication_point_for_issuer_with_cache_options<P: PublicationPoi
policy.strict.cms_der, policy.strict.cms_der,
policy.strict.name, policy.strict.name,
policy.resource_validation_mode, policy.resource_validation_mode,
ta_constraints,
) )
} }
} }
@ -1320,6 +1354,7 @@ pub fn process_publication_point_for_issuer_with_cache_options<P: PublicationPoi
policy.strict.cms_der, policy.strict.cms_der,
policy.strict.name, policy.strict.name,
policy.resource_validation_mode, policy.resource_validation_mode,
ta_constraints,
) )
}; };
match result.outcome { match result.outcome {
@ -1437,6 +1472,7 @@ pub fn process_publication_point_for_issuer_with_cache_options<P: PublicationPoi
policy.strict.cms_der, policy.strict.cms_der,
policy.strict.name, policy.strict.name,
policy.resource_validation_mode, policy.resource_validation_mode,
ta_constraints,
) { ) {
Ok((att, local_output)) => { Ok((att, local_output)) => {
stats.aspa_ok += 1; stats.aspa_ok += 1;
@ -1790,6 +1826,10 @@ pub(crate) struct RoaTaskShared {
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, resource_validation_mode: ResourceValidationMode,
/// Immutable constraints snapshot selected by the owning TAL. Every
/// ROA task for a publication point shares this Arc, so workers do not
/// need a mutable/global policy lookup or a copy of the interval rules.
ta_constraints: Option<Arc<crate::ta_constraints::TaConstraints>>,
} }
#[derive(Clone)] #[derive(Clone)]
@ -1911,6 +1951,7 @@ fn validate_owned_roa_task(worker_index: usize, task: OwnedRoaTask) -> RoaTaskRe
task.strict_cms_der, task.strict_cms_der,
task.strict_name, task.strict_name,
task.resource_validation_mode, task.resource_validation_mode,
shared.ta_constraints.as_deref(),
) )
.map(|(vrps, local_outputs, cache_object_meta)| RoaTaskOk { .map(|(vrps, local_outputs, cache_object_meta)| RoaTaskOk {
vrps, vrps,
@ -2009,6 +2050,40 @@ pub(crate) fn prepare_publication_point_for_parallel_roa_with_cache<P: Publicati
validation_time: time::OffsetDateTime, validation_time: time::OffsetDateTime,
collect_vcir_local_outputs: bool, collect_vcir_local_outputs: bool,
roa_cache: RoaValidationCacheInput<'_>, roa_cache: RoaValidationCacheInput<'_>,
) -> ParallelObjectsPrepare {
prepare_publication_point_for_parallel_roa_with_cache_and_ta_constraints(
publication_point_id,
publication_point,
policy,
issuer_ca_der,
issuer_ca_rsync_uri,
issuer_effective_ip,
issuer_effective_as,
validation_time,
collect_vcir_local_outputs,
roa_cache,
None,
)
}
/// Prepare a publication point for parallel ROA validation with the
/// immutable constraints snapshot belonging to its TAL. The snapshot is
/// moved into the stage-owned shared payload and therefore remains available
/// to detached ROA workers after the scoped phase-2 stage worker returns.
pub(crate) fn prepare_publication_point_for_parallel_roa_with_cache_and_ta_constraints<
P: PublicationPointData,
>(
publication_point_id: u64,
publication_point: &P,
policy: &Policy,
issuer_ca_der: &[u8],
issuer_ca_rsync_uri: Option<&str>,
issuer_effective_ip: Option<&crate::data_model::rc::IpResourceSet>,
issuer_effective_as: Option<&crate::data_model::rc::AsResourceSet>,
validation_time: time::OffsetDateTime,
collect_vcir_local_outputs: bool,
roa_cache: RoaValidationCacheInput<'_>,
ta_constraints: Option<Arc<crate::ta_constraints::TaConstraints>>,
) -> ParallelObjectsPrepare { ) -> ParallelObjectsPrepare {
let manifest_rsync_uri = publication_point.manifest_rsync_uri(); let manifest_rsync_uri = publication_point.manifest_rsync_uri();
let manifest_bytes = publication_point.manifest_bytes(); let manifest_bytes = publication_point.manifest_bytes();
@ -2323,6 +2398,7 @@ 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, resource_validation_mode: policy.resource_validation_mode,
ta_constraints,
}), }),
validation_time, validation_time,
collect_vcir_local_outputs, collect_vcir_local_outputs,
@ -2441,6 +2517,7 @@ pub(crate) fn reduce_parallel_roa_stage(
strict_cms_der, strict_cms_der,
strict_name, strict_name,
shared.resource_validation_mode, shared.resource_validation_mode,
shared.ta_constraints.as_deref(),
) { ) {
Ok((att, local_output)) => { Ok((att, local_output)) => {
stats.aspa_ok += 1; stats.aspa_ok += 1;
@ -2660,6 +2737,9 @@ pub(crate) enum ObjectValidateError {
"EE certificate resources are not a subset of issuer effective resources (RFC 6487 §7.2; RFC 3779)" "EE certificate resources are not a subset of issuer effective resources (RFC 6487 §7.2; RFC 3779)"
)] )]
EeResourcesNotSubset, EeResourcesNotSubset,
#[error("EE certificate violates locally configured TA constraints: {0}")]
TaConstraints(#[from] crate::ta_constraints::TaConstraintsViolation),
} }
pub(crate) fn validate_roa_task_serial( pub(crate) fn validate_roa_task_serial(
@ -2679,6 +2759,7 @@ pub(crate) fn validate_roa_task_serial(
strict_cms_der: bool, strict_cms_der: bool,
strict_name: bool, strict_name: bool,
resource_validation_mode: ResourceValidationMode, resource_validation_mode: ResourceValidationMode,
ta_constraints: Option<&crate::ta_constraints::TaConstraints>,
) -> RoaTaskResult { ) -> RoaTaskResult {
let outcome = process_roa_with_issuer( let outcome = process_roa_with_issuer(
task.file, task.file,
@ -2697,6 +2778,7 @@ pub(crate) fn validate_roa_task_serial(
strict_cms_der, strict_cms_der,
strict_name, strict_name,
resource_validation_mode, resource_validation_mode,
ta_constraints,
) )
.map(|(vrps, local_outputs, cache_object_meta)| RoaTaskOk { .map(|(vrps, local_outputs, cache_object_meta)| RoaTaskOk {
vrps, vrps,
@ -2732,6 +2814,7 @@ fn process_roa_with_issuer(
strict_cms_der: bool, strict_cms_der: bool,
strict_name: bool, strict_name: bool,
resource_validation_mode: ResourceValidationMode, resource_validation_mode: ResourceValidationMode,
ta_constraints: Option<&crate::ta_constraints::TaConstraints>,
) -> 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()
@ -2792,6 +2875,10 @@ fn process_roa_with_issuer(
)?; )?;
drop(_subset); drop(_subset);
if let Some(ta_constraints) = ta_constraints {
ta_constraints.validate_ee_certificate(&ee.resource_cert)?;
}
let vrps = roa_to_vrps_with_vrs(&roa, ee_vrs.ip.as_ref())?; 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(),
@ -2861,6 +2948,7 @@ fn process_roa_with_issuer_parallel_cached(
strict_cms_der: bool, strict_cms_der: bool,
strict_name: bool, strict_name: bool,
resource_validation_mode: ResourceValidationMode, resource_validation_mode: ResourceValidationMode,
ta_constraints: Option<&crate::ta_constraints::TaConstraints>,
) -> 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()
@ -2927,6 +3015,10 @@ fn process_roa_with_issuer_parallel_cached(
)?; )?;
drop(_subset); drop(_subset);
if let Some(ta_constraints) = ta_constraints {
ta_constraints.validate_ee_certificate(&ee.resource_cert)?;
}
let vrps = roa_to_vrps_with_vrs(&roa, ee_vrs.ip.as_ref())?; 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(),
@ -2996,6 +3088,7 @@ fn process_aspa_with_issuer(
strict_cms_der: bool, strict_cms_der: bool,
strict_name: bool, strict_name: bool,
resource_validation_mode: ResourceValidationMode, resource_validation_mode: ResourceValidationMode,
ta_constraints: Option<&crate::ta_constraints::TaConstraints>,
) -> Result<(AspaAttestation, Option<VcirLocalOutput>), ObjectValidateError> { ) -> Result<(AspaAttestation, Option<VcirLocalOutput>), ObjectValidateError> {
let _decode = timing let _decode = timing
.as_ref() .as_ref()
@ -3056,6 +3149,10 @@ fn process_aspa_with_issuer(
)?; )?;
drop(_subset); drop(_subset);
if let Some(ta_constraints) = ta_constraints {
ta_constraints.validate_ee_certificate(&ee.resource_cert)?;
}
validate_aspa_customer_in_vrs(&aspa, ee_vrs.asn.as_ref())?; validate_aspa_customer_in_vrs(&aspa, ee_vrs.asn.as_ref())?;
let attestation = AspaAttestation { let attestation = AspaAttestation {
@ -5505,6 +5602,13 @@ mod tests {
#[test] #[test]
fn parallel_stage_roa_tasks_share_stage_owned_payloads() { fn parallel_stage_roa_tasks_share_stage_owned_payloads() {
let ta_constraints = Arc::new(
crate::ta_constraints::TaConstraints::from_file(
&std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/ta_constraints/afrinic-current-ipv4-deny.constraints"),
)
.expect("load constraints fixture"),
);
let stage = ParallelObjectsStage { let stage = ParallelObjectsStage {
publication_point_id: 7, publication_point_id: 7,
shared: Arc::new(RoaTaskShared { shared: Arc::new(RoaTaskShared {
@ -5535,6 +5639,7 @@ mod tests {
issuer_effective_ip: None, issuer_effective_ip: None,
issuer_effective_as: None, issuer_effective_as: None,
resource_validation_mode: ResourceValidationMode::default(), resource_validation_mode: ResourceValidationMode::default(),
ta_constraints: Some(Arc::clone(&ta_constraints)),
}), }),
validation_time: OffsetDateTime::now_utc(), validation_time: OffsetDateTime::now_utc(),
collect_vcir_local_outputs: false, collect_vcir_local_outputs: false,
@ -5554,6 +5659,14 @@ mod tests {
let tasks = stage.build_roa_tasks(); let tasks = stage.build_roa_tasks();
assert_eq!(tasks.len(), 2); assert_eq!(tasks.len(), 2);
assert!(Arc::ptr_eq(&tasks[0].shared, &tasks[1].shared)); assert!(Arc::ptr_eq(&tasks[0].shared, &tasks[1].shared));
assert!(Arc::ptr_eq(
tasks[0]
.shared
.ta_constraints
.as_ref()
.expect("task constraints snapshot"),
&ta_constraints
));
} }
#[test] #[test]

View File

@ -901,6 +901,10 @@ where
H: Fetcher + Clone + 'static, H: Fetcher + Clone + 'static,
R: crate::fetch::rsync::RsyncFetcher + Clone + 'static, R: crate::fetch::rsync::RsyncFetcher + Clone + 'static,
{ {
// Constraints are an immutable per-run policy snapshot. The phase-2
// ready-stage binds the snapshot to each CA's TAL and moves an Arc into
// ROA/ASPA worker state, so constrained multi-TAL runs retain the same
// parallel scheduler as unconstrained runs.
let phase2_enabled = phase2_config.is_some(); let phase2_enabled = phase2_config.is_some();
if tal_inputs.is_empty() { if tal_inputs.is_empty() {
return Err(RunTreeFromTalError::Replay( return Err(RunTreeFromTalError::Replay(

View File

@ -16,7 +16,8 @@ use crate::validation::manifest::PublicationPointData;
use crate::validation::manifest::PublicationPointSource; use crate::validation::manifest::PublicationPointSource;
use crate::validation::objects::{ use crate::validation::objects::{
ObjectsOutput, OwnedRoaTask, ParallelObjectsPrepare, ParallelObjectsStage, ObjectsOutput, OwnedRoaTask, ParallelObjectsPrepare, ParallelObjectsStage,
RoaValidationCacheInput, prepare_publication_point_for_parallel_roa_with_cache, RoaValidationCacheInput,
prepare_publication_point_for_parallel_roa_with_cache_and_ta_constraints,
reduce_parallel_roa_stage, reduce_parallel_roa_stage,
}; };
use crate::validation::tree::{ use crate::validation::tree::{
@ -1367,7 +1368,16 @@ fn compute_ready_publication_point_stage(
} else { } else {
RoaValidationCacheInput::disabled() RoaValidationCacheInput::disabled()
}; };
match prepare_publication_point_for_parallel_roa_with_cache( let ta_constraints = runner
.policy
.ta_constraints
.shared_for_tal(&ready.node.handle.tal_id);
if ta_constraints.is_some() {
if let Some(timing) = runner.timing.as_ref() {
timing.record_count("ta_constraints_parallel_publication_points", 1);
}
}
match prepare_publication_point_for_parallel_roa_with_cache_and_ta_constraints(
ready.node.id, ready.node.id,
&fresh_stage.fresh_point, &fresh_stage.fresh_point,
runner.policy, runner.policy,
@ -1378,6 +1388,7 @@ fn compute_ready_publication_point_stage(
runner.validation_time, runner.validation_time,
runner.persist_vcir, runner.persist_vcir,
roa_cache, roa_cache,
ta_constraints,
) { ) {
ParallelObjectsPrepare::Complete(mut objects) => { ParallelObjectsPrepare::Complete(mut objects) => {
metrics.prepare_ms = elapsed_ms(prepare_started); metrics.prepare_ms = elapsed_ms(prepare_started);

View File

@ -53,6 +53,7 @@ use crate::validation::objects::{
AspaAttestation, ParallelRoaWorkerPool, RoaValidationCacheInput, RoaValidationCacheView, AspaAttestation, ParallelRoaWorkerPool, RoaValidationCacheInput, RoaValidationCacheView,
RouterKeyPayload, Vrp, process_publication_point_for_issuer_parallel_roa_with_cache_options, RouterKeyPayload, Vrp, process_publication_point_for_issuer_parallel_roa_with_cache_options,
process_publication_point_for_issuer_parallel_roa_with_pool_cache_options, process_publication_point_for_issuer_parallel_roa_with_pool_cache_options,
process_publication_point_for_issuer_with_cache_options_and_ta_constraints,
}; };
use crate::validation::publication_point::PublicationPointSnapshot; use crate::validation::publication_point::PublicationPointSnapshot;
use crate::validation::tree::{ use crate::validation::tree::{
@ -1551,12 +1552,30 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> {
RoaValidationCacheInput::disabled() RoaValidationCacheInput::disabled()
}; };
let objects_processing_started = std::time::Instant::now(); let objects_processing_started = std::time::Instant::now();
let ta_constraints = self.policy.ta_constraints.for_tal(&ca.tal_id);
let mut objects = { let mut objects = {
let _objects_total = self let _objects_total = self
.timing .timing
.as_ref() .as_ref()
.map(|t| t.span_phase("objects_processing_total")); .map(|t| t.span_phase("objects_processing_total"));
if let Some(phase2_pool) = self.parallel_roa_worker_pool.as_ref() { if let Some(ta_constraints) = ta_constraints {
// This method is the serial/fallback publication-point path. The
// phase-2 scheduler uses the stage-specific parallel prepare path,
// which carries the same immutable per-TAL snapshot into workers.
process_publication_point_for_issuer_with_cache_options_and_ta_constraints(
&fresh_point,
self.policy,
issuer_ca_der.as_ref(),
ca.ca_certificate_rsync_uri.as_deref(),
ca.effective_ip_resources.as_ref(),
ca.effective_as_resources.as_ref(),
self.validation_time,
self.timing.as_ref(),
false,
roa_cache,
Some(ta_constraints),
)
} else if let Some(phase2_pool) = self.parallel_roa_worker_pool.as_ref() {
process_publication_point_for_issuer_parallel_roa_with_pool_cache_options( process_publication_point_for_issuer_parallel_roa_with_pool_cache_options(
&fresh_point, &fresh_point,
self.policy, self.policy,
@ -1971,13 +1990,13 @@ fn sha256_digest_32(bytes: impl AsRef<[u8]>) -> [u8; 32] {
out out
} }
fn hash_serialized_parts(parts: &[(&str, Vec<u8>)]) -> [u8; 32] { fn hash_serialized_parts(parts: &[(&str, &[u8])]) -> [u8; 32] {
let mut hasher = sha2::Sha256::new(); let mut hasher = sha2::Sha256::new();
for (label, value) in parts { for (label, value) in parts {
hasher.update((label.len() as u64).to_be_bytes()); hasher.update((label.len() as u64).to_be_bytes());
hasher.update(label.as_bytes()); hasher.update(label.as_bytes());
hasher.update((value.len() as u64).to_be_bytes()); hasher.update((value.len() as u64).to_be_bytes());
hasher.update(value); hasher.update(*value);
} }
let digest = hasher.finalize(); let digest = hasher.finalize();
let mut out = [0u8; 32]; let mut out = [0u8; 32];
@ -1991,41 +2010,34 @@ fn cbor_or_debug_bytes<T: serde::Serialize + std::fmt::Debug>(value: &T) -> Vec<
fn ta_context_digest_for_ca(ca: &CaInstanceHandle) -> [u8; 32] { fn ta_context_digest_for_ca(ca: &CaInstanceHandle) -> [u8; 32] {
hash_serialized_parts(&[ hash_serialized_parts(&[
("version", b"publication-point-cache-ta-v1".to_vec()), ("version", b"publication-point-cache-ta-v1"),
("tal_id", ca.tal_id.as_bytes().to_vec()), ("tal_id", ca.tal_id.as_bytes()),
]) ])
} }
pub(crate) fn ca_validation_context_digest_for_ca(ca: &CaInstanceHandle) -> [u8; 32] { pub(crate) fn ca_validation_context_digest_for_ca(ca: &CaInstanceHandle) -> [u8; 32] {
hash_serialized_parts(&[ let parent_manifest = ca
( .parent_manifest_rsync_uri
"version",
b"publication-point-cache-parent-context-v1".to_vec(),
),
("tal_id", ca.tal_id.as_bytes().to_vec()),
(
"parent_manifest",
ca.parent_manifest_rsync_uri
.as_deref() .as_deref()
.unwrap_or("") .unwrap_or("")
.as_bytes() .as_bytes();
.to_vec(), let effective_ip = cbor_or_debug_bytes(&ca.effective_ip_resources);
), let effective_as = cbor_or_debug_bytes(&ca.effective_as_resources);
( hash_serialized_parts(&[
"effective_ip", ("version", b"publication-point-cache-parent-context-v1"),
cbor_or_debug_bytes(&ca.effective_ip_resources), ("tal_id", ca.tal_id.as_bytes()),
), ("parent_manifest", parent_manifest),
( ("effective_ip", effective_ip.as_slice()),
"effective_as", ("effective_as", effective_as.as_slice()),
cbor_or_debug_bytes(&ca.effective_as_resources),
),
]) ])
} }
pub(crate) fn publication_point_cache_policy_fingerprint(policy: &Policy) -> [u8; 32] { pub(crate) fn publication_point_cache_policy_fingerprint(policy: &Policy) -> [u8; 32] {
let policy_bytes = cbor_or_debug_bytes(policy);
hash_serialized_parts(&[ hash_serialized_parts(&[
("version", b"publication-point-cache-policy-v1".to_vec()), ("version", b"publication-point-cache-policy-v3"),
("policy", cbor_or_debug_bytes(policy)), ("policy", policy_bytes.as_slice()),
("ta_constraints", policy.ta_constraints.fingerprint_bytes()),
]) ])
} }
@ -2084,16 +2096,13 @@ fn child_certificate_cache_key_sha256_hex(
policy_fingerprint: &[u8; 32], policy_fingerprint: &[u8; 32],
) -> String { ) -> String {
let digest = hash_serialized_parts(&[ let digest = hash_serialized_parts(&[
("version", b"child-certificate-cache-key-v1".to_vec()), ("version", b"child-certificate-cache-key-v1"),
("child_cert_uri", child_cert_uri.as_bytes().to_vec()), ("child_cert_uri", child_cert_uri.as_bytes()),
("child_cert_sha256", child_cert_sha256.to_vec()), ("child_cert_sha256", child_cert_sha256),
("issuer_ca_sha256", issuer_ca_sha256.to_vec()), ("issuer_ca_sha256", issuer_ca_sha256),
// Keep the persisted cache-key label stable; this change only renames Rust identifiers. // Keep the persisted cache-key label stable; this change only renames Rust identifiers.
( ("parent_context_digest", ca_validation_context_digest),
"parent_context_digest", ("policy_fingerprint", policy_fingerprint),
ca_validation_context_digest.to_vec(),
),
("policy_fingerprint", policy_fingerprint.to_vec()),
]); ]);
sha256_hex_from_32(&digest) sha256_hex_from_32(&digest)
} }
@ -2986,6 +2995,24 @@ fn discover_children_from_fresh_snapshot_with_audit_cached_with_issuer_der<
match router_result { match router_result {
Ok(router) => { Ok(router) => {
if let Some(ta_constraints) = policy.ta_constraints.for_tal(&issuer.tal_id)
{
if let Err(error) =
ta_constraints.validate_ee_certificate(&router.resource_cert)
{
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 violates TA constraints: {error}"
)),
});
continue;
}
}
let router_asns = match router_asns_for_resource_mode( let router_asns = match router_asns_for_resource_mode(
&router.asns, &router.asns,
issuer.effective_as_resources.as_ref(), issuer.effective_as_resources.as_ref(),

View File

@ -73,6 +73,11 @@ pub struct ValidationContract {
pub validation_time: String, pub validation_time: String,
pub binary_sha256: String, pub binary_sha256: String,
pub policy: Policy, pub policy: Policy,
/// Constraints are currently normal-run-only. Persist their semantic
/// fingerprint so verification-only can fail closed rather than replay a
/// constrained run with an empty runtime-only policy map.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ta_constraints_fingerprint: Option<String>,
pub max_ca_depth: usize, pub max_ca_depth: usize,
pub max_instances: Option<usize>, pub max_instances: Option<usize>,
pub cache: ValidationCacheContract, pub cache: ValidationCacheContract,
@ -140,6 +145,7 @@ impl ValidationContract {
validation_time: format_validation_time(validation_time)?, validation_time: format_validation_time(validation_time)?,
binary_sha256: current_binary_sha256()?, binary_sha256: current_binary_sha256()?,
policy, policy,
ta_constraints_fingerprint: None,
max_ca_depth, max_ca_depth,
max_instances, max_instances,
cache, cache,
@ -163,6 +169,9 @@ impl ValidationContract {
} }
self.validation_time()?; self.validation_time()?;
validate_sha256_hex("validation contract binarySha256", &self.binary_sha256)?; validate_sha256_hex("validation contract binarySha256", &self.binary_sha256)?;
if let Some(fingerprint) = self.ta_constraints_fingerprint.as_deref() {
validate_sha256_hex("validation contract taConstraintsFingerprint", fingerprint)?;
}
if self.max_ca_depth == 0 { if self.max_ca_depth == 0 {
return Err("validation contract maxCaDepth must be greater than zero".to_string()); return Err("validation contract maxCaDepth must be greater than zero".to_string());
} }
@ -600,6 +609,12 @@ pub fn prepare_verification(
let cir = read_cir(&artifacts.source_cir)?; let cir = read_cir(&artifacts.source_cir)?;
let contract = read_validation_contract(&artifacts.source_contract)?; let contract = read_validation_contract(&artifacts.source_contract)?;
if contract.ta_constraints_fingerprint.is_some() {
return Err(
"verification-only is not supported for a run with TA constraints; rerun normal validation with the same constraints"
.to_string(),
);
}
if cir.validation_time.to_offset(time::UtcOffset::UTC) != contract.validation_time()? { if cir.validation_time.to_offset(time::UtcOffset::UTC) != contract.validation_time()? {
return Err(format!( return Err(format!(
"source CIR validation time differs from validation contract: cir={}, contract={}", "source CIR validation time differs from validation contract: cir={}, contract={}",
@ -895,6 +910,7 @@ mod tests {
validation_time: "2026-07-16T00:00:00Z".to_string(), validation_time: "2026-07-16T00:00:00Z".to_string(),
binary_sha256: "ab".repeat(32), binary_sha256: "ab".repeat(32),
policy: Policy::default(), policy: Policy::default(),
ta_constraints_fingerprint: None,
max_ca_depth: 32, max_ca_depth: 32,
max_instances: None, max_instances: None,
cache: ValidationCacheContract { cache: ValidationCacheContract {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,394 @@
# Feature #151 remote all5 performance fixture.
#
# Source: official rpki-client 9.8 afrinic.constraints
# ($OpenBSD: afrinic.constraints,v 1.4 2026/03/11 21:46:36 job Exp $).
# Every IPv4 allow entry from that file (including recovered and transfer
# ranges) is converted to deny. IPv6 and ASN entries are intentionally not
# included because this fixture measures the full AFRINIC IPv4 policy.
allow 0.0.0.0/0
allow ::/0
allow 0 - 4294967295
deny 41.0.0.0/8
deny 102.0.0.0/8
deny 105.0.0.0/8
deny 154.0.0.0/16
deny 154.16.0.0/16
deny 154.65.0.0 - 154.255.255.255
deny 196.0.0.0 - 196.1.0.255
deny 196.1.4.0/24
deny 196.1.7.0 - 196.1.63.255
deny 196.1.71.0/24
deny 196.1.74.0 - 196.1.103.255
deny 196.1.115.0 - 196.1.133.255
deny 196.1.137.0/24
deny 196.1.143.0 - 196.1.159.255
deny 196.1.176.0 - 196.1.255.255
deny 196.2.2.0/23
deny 196.2.8.0 - 196.2.255.255
deny 196.3.14.0/23
deny 196.3.57.0 - 196.3.64.255
deny 196.3.90.0/24
deny 196.3.92.0 - 196.3.94.255
deny 196.3.96.0/21
deny 196.3.105.0/24
deny 196.3.107.0 - 196.3.131.255
deny 196.3.148.0/22
deny 196.3.154.0 - 196.3.183.255
deny 196.3.224.0 - 196.4.45.255
deny 196.4.71.0 - 196.11.171.255
deny 196.11.174.0 - 196.11.239.255
deny 196.11.248.0/21
deny 196.12.10.0 - 196.12.31.255
deny 196.12.128.0/19
deny 196.12.192.0 - 196.15.15.255
deny 196.15.64.0 - 196.26.255.255
deny 196.27.64.0 - 196.28.47.255
deny 196.28.64.0 - 196.29.63.255
deny 196.29.96.0 - 196.31.255.255
deny 196.32.8.0 - 196.32.31.255
deny 196.32.96.0/19
deny 196.32.160.0 - 196.39.255.255
deny 196.40.96.0 - 196.41.255.255
deny 196.42.64.0 - 196.216.0.255
deny 196.216.2.0 - 197.255.255.255
deny 45.96.0.0 - 45.111.255.255
deny 45.192.0.0 - 45.222.255.255
deny 45.240.0.0 - 45.247.255.255
deny 66.251.128.0 - 66.251.191.255
deny 139.26.0.0 - 139.26.255.255
deny 146.196.128.0 - 146.196.255.255
deny 160.19.36.0 - 160.19.39.255
deny 160.19.60.0 - 160.19.63.255
deny 160.19.96.0 - 160.19.103.255
deny 160.19.112.0 - 160.19.143.255
deny 160.19.152.0 - 160.19.155.255
deny 160.19.188.0 - 160.19.191.255
deny 160.19.192.0 - 160.19.199.255
deny 160.19.232.0 - 160.19.239.255
deny 160.20.24.0 - 160.20.31.255
deny 160.20.112.0 - 160.20.115.255
deny 160.20.213.0 - 160.20.213.255
deny 160.20.217.0 - 160.20.217.255
deny 160.20.221.0 - 160.20.221.255
deny 160.20.226.0 - 160.20.227.255
deny 160.20.252.0 - 160.20.255.255
deny 160.238.11.0 - 160.238.11.255
deny 160.238.48.0 - 160.238.49.255
deny 160.238.50.0 - 160.238.50.255
deny 160.238.57.0 - 160.238.57.255
deny 160.238.101.0 - 160.238.101.255
deny 161.123.0.0 - 161.123.255.255
deny 164.160.0.0 - 164.160.255.255
deny 192.12.110.0 - 192.12.111.255
deny 192.12.116.0 - 192.12.117.255
deny 192.47.36.0 - 192.47.36.255
deny 192.51.240.0 - 192.51.240.255
deny 192.70.200.0 - 192.70.201.255
deny 192.75.236.0 - 192.75.236.255
deny 192.83.208.0 - 192.83.215.255
deny 192.91.200.0 - 192.91.200.255
deny 192.142.0.0 - 192.143.255.255
deny 192.145.128.0 - 192.145.191.255
deny 192.145.230.0 - 192.145.230.255
deny 204.8.204.0 - 204.8.207.255
deny 208.85.156.0 - 208.85.159.255
deny 83.143.24.0 - 83.143.31.255
deny 84.205.96.0 - 84.205.127.255
deny 131.176.0.0 - 131.176.255.255
deny 163.121.0.0 - 163.121.255.255
deny 165.231.0.0 - 165.231.255.255
deny 192.52.232.0 - 192.52.232.255
deny 193.17.215.0 - 193.17.215.255
deny 193.19.232.0 - 193.19.235.255
deny 193.41.146.0 - 193.41.147.255
deny 193.108.23.0 - 193.108.23.255
deny 193.108.28.0 - 193.108.28.255
deny 193.109.66.0 - 193.109.67.255
deny 193.110.104.0 - 193.110.105.255
deny 193.194.128.0 - 193.194.128.255
deny 193.227.128.0 - 193.227.128.255
deny 194.9.64.0 - 194.9.65.255
deny 194.9.82.0 - 194.9.83.255
deny 195.24.80.0 - 195.24.87.255
deny 195.39.218.0 - 195.39.219.255
deny 195.234.120.0 - 195.234.123.255
deny 195.234.168.0 - 195.234.168.255
deny 195.234.185.0 - 195.234.185.255
deny 195.234.252.0 - 195.234.255.255
deny 193.188.7.0/24
deny 193.189.0.0/18
deny 193.189.128.0/24
deny 193.194.160.0/19
deny 193.221.218.0/24
deny 64.57.112.0 - 64.57.127.255
deny 66.8.0.0 - 66.8.127.255
deny 66.18.64.0 - 66.18.95.255
deny 69.63.64.0 - 69.63.79.255
deny 69.67.32.0 - 69.67.47.255
deny 137.158.0.0 - 137.158.255.255
deny 137.214.0.0 - 137.214.255.255
deny 137.215.0.0 - 137.215.255.255
deny 139.53.0.0 - 139.53.255.255
deny 143.128.0.0 - 143.128.255.255
deny 143.160.0.0 - 143.160.255.255
deny 146.64.0.0 - 146.64.255.255
deny 146.141.0.0 - 146.141.255.255
deny 146.182.0.0 - 146.182.255.255
deny 146.230.0.0 - 146.230.255.255
deny 146.231.0.0 - 146.231.255.255
deny 146.232.0.0 - 146.232.255.255
deny 147.110.0.0 - 147.110.255.255
deny 152.106.0.0 - 152.106.255.255
deny 152.107.0.0 - 152.107.255.255
deny 152.108.0.0 - 152.108.255.255
deny 152.109.0.0 - 152.109.255.255
deny 152.110.0.0 - 152.110.255.255
deny 152.111.0.0 - 152.111.255.255
deny 152.112.0.0 - 152.112.255.255
deny 155.159.0.0 - 155.159.255.255
deny 155.232.0.0 - 155.232.255.255
deny 155.233.0.0 - 155.233.255.255
deny 155.234.0.0 - 155.234.255.255
deny 155.235.0.0 - 155.235.255.255
deny 155.236.0.0 - 155.236.255.255
deny 155.237.0.0 - 155.237.255.255
deny 155.238.0.0 - 155.238.255.255
deny 155.239.0.0 - 155.239.255.255
deny 155.240.0.0 - 155.240.255.255
deny 156.8.0.0 - 156.8.255.255
deny 160.115.0.0 - 160.115.255.255
deny 160.116.0.0 - 160.116.255.255
deny 160.117.0.0 - 160.117.255.255
deny 160.118.0.0 - 160.118.255.255
deny 160.119.0.0 - 160.119.255.255
deny 160.120.0.0 - 160.120.255.255
deny 160.121.0.0 - 160.121.255.255
deny 160.122.0.0 - 160.122.255.255
deny 160.123.0.0 - 160.123.255.255
deny 160.124.0.0 - 160.124.255.255
deny 163.195.0.0 - 163.195.255.255
deny 163.196.0.0 - 163.196.255.255
deny 163.197.0.0 - 163.197.255.255
deny 163.198.0.0 - 163.198.255.255
deny 163.199.0.0 - 163.199.255.255
deny 163.200.0.0 - 163.200.255.255
deny 163.201.0.0 - 163.201.255.255
deny 163.202.0.0 - 163.202.255.255
deny 163.203.0.0 - 163.203.255.255
deny 164.88.0.0 - 164.88.255.255
deny 164.146.0.0 - 164.151.255.255
deny 164.155.0.0 - 164.155.255.255
deny 165.3.0.0 - 165.5.255.255
deny 165.8.0.0 - 165.11.255.255
deny 165.25.0.0 - 165.25.255.255
deny 165.143.0.0 - 165.149.255.255
deny 165.165.0.0 - 165.165.255.255
deny 165.180.0.0 - 165.180.255.255
deny 165.233.0.0 - 165.233.255.255
deny 166.85.0.0 - 166.85.255.255
deny 168.76.0.0 - 168.76.255.255
deny 168.80.0.0 - 168.81.255.255
deny 168.89.0.0 - 168.89.255.255
deny 168.128.0.0 - 168.128.255.255
deny 168.142.0.0 - 168.142.255.255
deny 168.155.0.0 - 168.155.255.255
deny 168.164.0.0 - 168.164.255.255
deny 168.167.0.0 - 168.167.255.255
deny 168.172.0.0 - 168.172.255.255
deny 168.206.0.0 - 168.206.255.255
deny 168.209.0.0 - 168.210.255.255
deny 169.129.0.0 - 169.129.255.255
deny 169.202.0.0 - 169.202.255.255
deny 192.33.10.0 - 192.33.10.255
deny 192.42.99.0 - 192.42.99.255
deny 192.48.253.0 - 192.48.253.255
deny 192.68.138.0 - 192.68.138.255
deny 192.70.237.0 - 192.70.237.255
deny 192.82.142.0 - 192.82.142.255
deny 192.84.244.0 - 192.84.244.255
deny 192.94.61.0 - 192.94.61.255
deny 192.94.210.0 - 192.94.210.255
deny 192.94.240.0 - 192.94.240.255
deny 192.94.241.0 - 192.94.241.255
deny 192.94.246.0 - 192.94.246.255
deny 192.96.0.0 - 192.96.255.255
deny 192.100.1.0 - 192.100.1.255
deny 192.101.142.0 - 192.101.142.255
deny 192.102.9.0 - 192.102.9.255
deny 192.133.250.0 - 192.133.250.255
deny 192.136.55.0 - 192.136.55.255
deny 192.136.56.0 - 192.136.56.255
deny 192.136.57.0 - 192.136.57.255
deny 192.157.190.0 - 192.157.190.255
deny 192.188.164.0 - 192.188.167.255
deny 192.189.75.0 - 192.189.75.255
deny 192.189.139.0 - 192.189.140.255
deny 192.231.237.0 - 192.231.237.255
deny 192.231.254.0 - 192.231.254.255
deny 192.245.148.0 - 192.245.148.255
deny 192.251.202.0 - 192.251.202.255
deny 198.54.0.0 - 198.54.255.255
deny 200.16.8.0 - 200.16.15.255
deny 204.12.128.0 - 204.12.143.255
deny 204.87.179.0 - 204.87.179.255
deny 204.152.14.0 - 204.152.15.255
deny 204.235.32.0 - 204.235.43.255
deny 205.159.79.0 - 205.159.79.255
deny 206.223.136.0 - 206.223.136.255
deny 209.203.0.0 - 209.203.63.255
deny 209.212.96.0 - 209.212.127.255
deny 216.236.176.0 - 216.236.191.255
deny 202.123.0.0/19
deny 62.8.64.0/19
deny 62.12.96.0/19
deny 62.24.96.0/19
deny 62.61.192.0/18
deny 62.68.32.0/19
deny 62.68.224.0/19
deny 62.114.0.0/16
deny 62.117.32.0/19
deny 62.135.0.0/17
deny 62.139.0.0/16
deny 62.140.64.0/18
deny 62.173.32.0/19
deny 62.193.64.0/18
deny 62.193.160.0/19
deny 62.240.32.0/19
deny 62.240.96.0/19
deny 62.241.128.0/19
deny 62.251.128.0/17
deny 77.220.0.0/19
deny 80.67.128.0/20
deny 80.72.96.0/20
deny 80.75.160.0/19
deny 80.87.64.0/19
deny 80.88.0.0/20
deny 80.95.0.0/20
deny 80.240.192.0/20
deny 80.246.0.0/20
deny 80.248.0.0/20
deny 80.248.64.0/20
deny 80.249.64.0/20
deny 80.250.32.0/20
deny 81.4.0.0/18
deny 81.10.0.0/17
deny 81.21.96.0/20
deny 81.22.64.0/19
deny 81.26.64.0/20
deny 81.29.96.0/20
deny 81.91.224.0/20
deny 81.192.0.0/16
deny 82.101.128.0/18
deny 82.128.0.0/17
deny 82.129.128.0/17
deny 82.151.64.0/19
deny 82.201.128.0/17
deny 84.36.0.0/16
deny 84.233.0.0/17
deny 87.255.96.0/19
deny 193.95.0.0/17
deny 193.108.214.0/24
deny 193.108.252.0/22
deny 193.189.64.0 - 193.189.65.255
deny 193.194.1.0 - 193.194.5.255
deny 193.194.32.0 - 193.194.95.255
deny 193.227.0.0/18
deny 194.6.224.0/24
deny 194.79.96.0/19
deny 194.204.192.0/18
deny 195.24.192.0/19
deny 195.43.0.0/19
deny 195.166.224.0/19
deny 195.202.64.0/19
deny 195.246.32.0/19
deny 212.0.128.0/19
deny 212.12.224.0/19
deny 212.22.160.0/19
deny 212.49.64.0/19
deny 212.52.128.0/19
deny 212.60.64.0/19
deny 212.85.192.0/19
deny 212.88.96.0/19
deny 212.96.0.0/19
deny 212.100.64.0/19
deny 212.103.160.0/19
deny 212.122.224.0/19
deny 212.217.0.0/17
deny 213.55.64.0/18
deny 213.131.64.0/19
deny 213.136.96.0/19
deny 213.147.64.0/19
deny 213.150.96.0/19
deny 213.150.160.0 - 213.150.223.255
deny 213.152.64.0/19
deny 213.154.32.0 - 213.154.95.255
deny 213.158.160.0/19
deny 213.172.128.0/19
deny 213.179.160.0/19
deny 213.181.224.0/19
deny 213.193.32.0/19
deny 213.212.192.0/18
deny 213.247.0.0/19
deny 213.255.128.0/19
deny 217.14.80.0/20
deny 217.20.224.0/20
deny 217.21.112.0/20
deny 217.29.128.0/20
deny 217.29.208.0/20
deny 217.52.0.0/14
deny 217.64.96.0/20
deny 217.77.64.0/20
deny 217.78.64.0/20
deny 217.117.0.0/20
deny 217.139.0.0/16
deny 217.170.144.0/20
deny 217.199.144.0/20
deny 129.0.0.0/16
deny 129.18.0.0/16
deny 129.45.0.0/16
deny 129.56.0.0/16
deny 129.122.0.0/16
deny 129.140.0.0/16
deny 129.205.0.0/16
deny 129.232.0.0/16
deny 137.63.0.0 - 137.64.255.255
deny 137.115.0.0/16
deny 137.171.0.0/16
deny 137.196.0.0/16
deny 137.255.0.0/16
deny 155.0.0.0/16
deny 155.11.0.0 - 155.12.255.255
deny 155.89.0.0/16
deny 155.93.0.0/16
deny 155.196.0.0/16
deny 155.251.0.0/16
deny 155.255.0.0 - 156.0.255.255
deny 156.38.0.0/16
deny 156.155.0.0 - 156.255.255.255
deny 160.0.0.0/16
deny 160.77.0.0/16
deny 160.89.0.0 - 160.90.255.255
deny 160.105.0.0/16
deny 160.113.0.0/16
deny 160.152.0.0/16
deny 160.154.0.0 - 160.179.255.255
deny 160.181.0.0 - 160.184.255.255
deny 160.224.0.0 - 160.226.255.255
deny 160.242.0.0/16
deny 160.255.0.0/16
deny 165.0.0.0/16
deny 165.16.0.0/16
deny 165.49.0.0 - 165.63.255.255
deny 165.73.0.0/16
deny 165.90.0.0/16
deny 165.169.0.0/16
deny 165.210.0.0/15
deny 165.255.0.0/16
deny 168.211.0.0 - 168.211.255.255
deny 168.253.0.0/16
deny 169.0.0.0/15
deny 169.159.0.0/16
deny 169.239.0.0/16
deny 169.255.0.0/16
deny 192.109.242.0/24

View File

@ -0,0 +1,2 @@
# local baseline-v1: the single valid ROA EE certificate has this IPv4 block
allow 203.0.113.0/24

View File

@ -0,0 +1,3 @@
# local baseline-v1 negative control: deny wins over a covering allow rule
allow 0.0.0.0/0
deny 203.0.113.0/24

View File

@ -0,0 +1,12 @@
# Remote RIPE TA soak policy for feature #151.
#
# Start permissive for every INR kind, then carve out several /8 blocks
# allocated to AFRINIC. These denies should not match normal RIPE NCC
# EE resources, while exercising deny-overrides-allow in a live run.
allow 0.0.0.0/0
allow ::/0
allow 0 - 4294967295
deny 41.0.0.0/8
deny 102.0.0.0/8
deny 105.0.0.0/8