add delta sync and fail fetch process
This commit is contained in:
@@ -7,6 +7,8 @@ use crate::data_model::rc::{
|
||||
};
|
||||
use x509_parser::prelude::{FromDer, X509Certificate};
|
||||
|
||||
use crate::validation::x509_name::x509_names_equivalent;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ValidatedSubordinateCa {
|
||||
pub child_ca: ResourceCertificate,
|
||||
@@ -135,7 +137,7 @@ pub fn validate_subordinate_ca_cert(
|
||||
return Err(CaPathError::IssuerNotCa);
|
||||
}
|
||||
|
||||
if child_ca.tbs.issuer_name != issuer_ca.tbs.subject_name {
|
||||
if !x509_names_equivalent(&child_ca.tbs.issuer_name, &issuer_ca.tbs.subject_name) {
|
||||
return Err(CaPathError::IssuerSubjectMismatch {
|
||||
child_issuer_dn: child_ca.tbs.issuer_name.to_string(),
|
||||
issuer_subject_dn: issuer_ca.tbs.subject_name.to_string(),
|
||||
@@ -697,6 +699,7 @@ mod tests {
|
||||
};
|
||||
use crate::data_model::common::X509NameDer;
|
||||
use der_parser::num_bigint::BigUint;
|
||||
use std::process::Command;
|
||||
fn dummy_cert(
|
||||
kind: ResourceCertKind,
|
||||
subject_dn: &str,
|
||||
@@ -736,6 +739,158 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn openssl_available() -> bool {
|
||||
Command::new("openssl")
|
||||
.arg("version")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn write_cert_der_with_addext(dir: &std::path::Path, addext: Option<&str>) -> Vec<u8> {
|
||||
assert!(openssl_available(), "openssl is required for this test");
|
||||
let key = dir.join("k.pem");
|
||||
let cert = dir.join("c.pem");
|
||||
let der = dir.join("c.der");
|
||||
|
||||
let mut cmd = Command::new("openssl");
|
||||
cmd.arg("req")
|
||||
.arg("-x509")
|
||||
.arg("-newkey")
|
||||
.arg("rsa:2048")
|
||||
.arg("-nodes")
|
||||
.arg("-keyout")
|
||||
.arg(&key)
|
||||
.arg("-subj")
|
||||
.arg("/CN=ku")
|
||||
.arg("-days")
|
||||
.arg("1")
|
||||
.arg("-out")
|
||||
.arg(&cert);
|
||||
if let Some(ext) = addext {
|
||||
cmd.arg("-addext").arg(ext);
|
||||
}
|
||||
let out = cmd.output().expect("openssl req");
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"openssl req failed: {}",
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
|
||||
let out = Command::new("openssl")
|
||||
.arg("x509")
|
||||
.arg("-in")
|
||||
.arg(&cert)
|
||||
.arg("-outform")
|
||||
.arg("DER")
|
||||
.arg("-out")
|
||||
.arg(&der)
|
||||
.output()
|
||||
.expect("openssl x509");
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"openssl x509 failed: {}",
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
std::fs::read(&der).expect("read der")
|
||||
}
|
||||
|
||||
fn gen_issuer_and_child_der(dir: &std::path::Path) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
|
||||
assert!(openssl_available(), "openssl is required for this test");
|
||||
let issuer_key = dir.join("issuer.key");
|
||||
let issuer_csr = dir.join("issuer.csr");
|
||||
let issuer_pem = dir.join("issuer.pem");
|
||||
let issuer_der = dir.join("issuer.der");
|
||||
|
||||
let child_key = dir.join("child.key");
|
||||
let child_csr = dir.join("child.csr");
|
||||
let child_pem = dir.join("child.pem");
|
||||
let child_der = dir.join("child.der");
|
||||
|
||||
let other_key = dir.join("other.key");
|
||||
let other_csr = dir.join("other.csr");
|
||||
let other_pem = dir.join("other.pem");
|
||||
let other_der = dir.join("other.der");
|
||||
|
||||
let run = |cmd: &mut Command| {
|
||||
let out = cmd.output().expect("run openssl");
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"command failed: {:?}\nstderr={}",
|
||||
cmd,
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
};
|
||||
|
||||
// Issuer self-signed.
|
||||
run(Command::new("openssl").args(["genrsa", "-out"]).arg(&issuer_key).arg("2048"));
|
||||
run(Command::new("openssl")
|
||||
.args(["req", "-new", "-key"])
|
||||
.arg(&issuer_key)
|
||||
.args(["-subj", "/CN=issuer", "-out"])
|
||||
.arg(&issuer_csr));
|
||||
run(Command::new("openssl")
|
||||
.args(["x509", "-req", "-in"])
|
||||
.arg(&issuer_csr)
|
||||
.args(["-signkey"])
|
||||
.arg(&issuer_key)
|
||||
.args(["-days", "1", "-out"])
|
||||
.arg(&issuer_pem));
|
||||
run(Command::new("openssl")
|
||||
.args(["x509", "-in"])
|
||||
.arg(&issuer_pem)
|
||||
.args(["-outform", "DER", "-out"])
|
||||
.arg(&issuer_der));
|
||||
|
||||
// Child signed by issuer.
|
||||
run(Command::new("openssl").args(["genrsa", "-out"]).arg(&child_key).arg("2048"));
|
||||
run(Command::new("openssl")
|
||||
.args(["req", "-new", "-key"])
|
||||
.arg(&child_key)
|
||||
.args(["-subj", "/CN=child", "-out"])
|
||||
.arg(&child_csr));
|
||||
run(Command::new("openssl")
|
||||
.args(["x509", "-req", "-in"])
|
||||
.arg(&child_csr)
|
||||
.args(["-CA"])
|
||||
.arg(&issuer_pem)
|
||||
.args(["-CAkey"])
|
||||
.arg(&issuer_key)
|
||||
.args(["-CAcreateserial", "-days", "1", "-out"])
|
||||
.arg(&child_pem));
|
||||
run(Command::new("openssl")
|
||||
.args(["x509", "-in"])
|
||||
.arg(&child_pem)
|
||||
.args(["-outform", "DER", "-out"])
|
||||
.arg(&child_der));
|
||||
|
||||
// Other self-signed issuer.
|
||||
run(Command::new("openssl").args(["genrsa", "-out"]).arg(&other_key).arg("2048"));
|
||||
run(Command::new("openssl")
|
||||
.args(["req", "-new", "-key"])
|
||||
.arg(&other_key)
|
||||
.args(["-subj", "/CN=other", "-out"])
|
||||
.arg(&other_csr));
|
||||
run(Command::new("openssl")
|
||||
.args(["x509", "-req", "-in"])
|
||||
.arg(&other_csr)
|
||||
.args(["-signkey"])
|
||||
.arg(&other_key)
|
||||
.args(["-days", "1", "-out"])
|
||||
.arg(&other_pem));
|
||||
run(Command::new("openssl")
|
||||
.args(["x509", "-in"])
|
||||
.arg(&other_pem)
|
||||
.args(["-outform", "DER", "-out"])
|
||||
.arg(&other_der));
|
||||
|
||||
(
|
||||
std::fs::read(&issuer_der).expect("read issuer der"),
|
||||
std::fs::read(&child_der).expect("read child der"),
|
||||
std::fs::read(&other_der).expect("read other der"),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_child_ip_resources_rejects_inherit_without_parent_effective_resources() {
|
||||
let child = IpResourceSet {
|
||||
@@ -953,6 +1108,142 @@ mod tests {
|
||||
validate_child_crldp_contains_issuer_crl_uri(&child, "rsync://example.test/issuer.crl")
|
||||
.expect("crldp ok");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_child_ca_key_usage_accepts_only_keycertsign_and_crlsign_critical() {
|
||||
let td = tempfile::tempdir().expect("tempdir");
|
||||
let der = write_cert_der_with_addext(
|
||||
td.path(),
|
||||
Some("keyUsage = critical, keyCertSign, cRLSign"),
|
||||
);
|
||||
validate_child_ca_key_usage(&der).expect("key usage ok");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_child_ca_key_usage_rejects_missing_noncritical_and_invalid_bits() {
|
||||
let td = tempfile::tempdir().expect("tempdir");
|
||||
let missing = write_cert_der_with_addext(td.path(), None);
|
||||
let err = validate_child_ca_key_usage(&missing).unwrap_err();
|
||||
assert!(matches!(err, CaPathError::KeyUsageMissing), "{err}");
|
||||
|
||||
let td = tempfile::tempdir().expect("tempdir");
|
||||
let noncritical =
|
||||
write_cert_der_with_addext(td.path(), Some("keyUsage = keyCertSign, cRLSign"));
|
||||
let err = validate_child_ca_key_usage(&noncritical).unwrap_err();
|
||||
assert!(matches!(err, CaPathError::KeyUsageNotCritical), "{err}");
|
||||
|
||||
let td = tempfile::tempdir().expect("tempdir");
|
||||
let invalid = write_cert_der_with_addext(
|
||||
td.path(),
|
||||
Some("keyUsage = critical, keyCertSign, cRLSign, digitalSignature"),
|
||||
);
|
||||
let err = validate_child_ca_key_usage(&invalid).unwrap_err();
|
||||
assert!(matches!(err, CaPathError::KeyUsageInvalidBits), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_cert_signature_with_issuer_accepts_valid_chain_and_rejects_wrong_issuer() {
|
||||
let td = tempfile::tempdir().expect("tempdir");
|
||||
let (issuer, child, other) = gen_issuer_and_child_der(td.path());
|
||||
verify_cert_signature_with_issuer(&child, &issuer).expect("signature ok");
|
||||
let err = verify_cert_signature_with_issuer(&child, &other).unwrap_err();
|
||||
assert!(matches!(err, CaPathError::ChildSignatureInvalid(_)), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_child_ip_and_as_resources_success_paths() {
|
||||
use crate::data_model::rc::{AsIdOrRange, IpAddressOrRange, IpPrefix};
|
||||
|
||||
let parent_ip = IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
|
||||
IpPrefix {
|
||||
afi: Afi::Ipv4,
|
||||
prefix_len: 8,
|
||||
addr: vec![10, 0, 0, 0],
|
||||
},
|
||||
)]),
|
||||
}],
|
||||
};
|
||||
|
||||
let child_ip_inherit = IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::Inherit,
|
||||
}],
|
||||
};
|
||||
let eff = resolve_child_ip_resources(Some(&child_ip_inherit), Some(&parent_ip))
|
||||
.expect("inherit resolves")
|
||||
.expect("some ip");
|
||||
assert_eq!(eff.families.len(), 1);
|
||||
assert!(matches!(
|
||||
eff.families[0].choice,
|
||||
IpAddressChoice::AddressesOrRanges(_)
|
||||
));
|
||||
|
||||
let child_ip_subset = IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
|
||||
IpPrefix {
|
||||
afi: Afi::Ipv4,
|
||||
prefix_len: 16,
|
||||
addr: vec![10, 1, 0, 0],
|
||||
},
|
||||
)]),
|
||||
}],
|
||||
};
|
||||
resolve_child_ip_resources(Some(&child_ip_subset), Some(&parent_ip))
|
||||
.expect("subset ok")
|
||||
.expect("some");
|
||||
|
||||
let child_ip_bad = IpResourceSet {
|
||||
families: vec![IpAddressFamily {
|
||||
afi: Afi::Ipv4,
|
||||
choice: IpAddressChoice::AddressesOrRanges(vec![IpAddressOrRange::Prefix(
|
||||
IpPrefix {
|
||||
afi: Afi::Ipv4,
|
||||
prefix_len: 16,
|
||||
addr: vec![11, 0, 0, 0],
|
||||
},
|
||||
)]),
|
||||
}],
|
||||
};
|
||||
let err = resolve_child_ip_resources(Some(&child_ip_bad), Some(&parent_ip)).unwrap_err();
|
||||
assert!(matches!(err, CaPathError::ResourcesNotSubset), "{err}");
|
||||
|
||||
let parent_as = AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Range {
|
||||
min: 1,
|
||||
max: 100,
|
||||
}])),
|
||||
rdi: None,
|
||||
};
|
||||
let child_as_inherit = AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::Inherit),
|
||||
rdi: None,
|
||||
};
|
||||
let eff_as = resolve_child_as_resources(Some(&child_as_inherit), Some(&parent_as))
|
||||
.expect("inherit as")
|
||||
.expect("some");
|
||||
assert_eq!(eff_as.asnum, parent_as.asnum);
|
||||
|
||||
let child_as_subset = AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(50)])),
|
||||
rdi: None,
|
||||
};
|
||||
resolve_child_as_resources(Some(&child_as_subset), Some(&parent_as))
|
||||
.expect("subset as")
|
||||
.expect("some");
|
||||
|
||||
let child_as_bad = AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(200)])),
|
||||
rdi: None,
|
||||
};
|
||||
let err = resolve_child_as_resources(Some(&child_as_bad), Some(&parent_as)).unwrap_err();
|
||||
assert!(matches!(err, CaPathError::ResourcesNotSubset), "{err}");
|
||||
}
|
||||
}
|
||||
|
||||
fn increment_bytes(v: &[u8]) -> Vec<u8> {
|
||||
|
||||
@@ -5,6 +5,8 @@ use crate::data_model::rc::{
|
||||
};
|
||||
use x509_parser::prelude::{FromDer, X509Certificate};
|
||||
|
||||
use crate::validation::x509_name::x509_names_equivalent;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ValidatedEeCertPath {
|
||||
pub ee: ResourceCertificate,
|
||||
@@ -112,7 +114,7 @@ pub fn validate_ee_cert_path(
|
||||
return Err(CertPathError::IssuerNotCa);
|
||||
}
|
||||
|
||||
if ee.tbs.issuer_name != issuer_ca.tbs.subject_name {
|
||||
if !x509_names_equivalent(&ee.tbs.issuer_name, &issuer_ca.tbs.subject_name) {
|
||||
return Err(CertPathError::IssuerSubjectMismatch {
|
||||
ee_issuer_dn: ee.tbs.issuer_name.to_string(),
|
||||
issuer_subject_dn: issuer_ca.tbs.subject_name.to_string(),
|
||||
|
||||
@@ -23,6 +23,9 @@ pub struct PublicationPointResult {
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ManifestFreshError {
|
||||
#[error("repo sync failed: {detail} (RFC 8182 §3.4.5; RFC 9286 §6.6)")]
|
||||
RepoSyncFailed { detail: String },
|
||||
|
||||
#[error(
|
||||
"manifest not found in raw_objects: {manifest_rsync_uri} (RFC 9286 §6.2; RFC 9286 §6.6)"
|
||||
)]
|
||||
@@ -148,14 +151,44 @@ pub fn process_manifest_publication_point(
|
||||
issuer_ca_rsync_uri: Option<&str>,
|
||||
validation_time: time::OffsetDateTime,
|
||||
) -> Result<PublicationPointResult, ManifestProcessError> {
|
||||
let fresh = try_build_fresh_pack(
|
||||
process_manifest_publication_point_after_repo_sync(
|
||||
store,
|
||||
policy,
|
||||
manifest_rsync_uri,
|
||||
publication_point_rsync_uri,
|
||||
issuer_ca_der,
|
||||
issuer_ca_rsync_uri,
|
||||
validation_time,
|
||||
);
|
||||
true,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn process_manifest_publication_point_after_repo_sync(
|
||||
store: &RocksStore,
|
||||
policy: &Policy,
|
||||
manifest_rsync_uri: &str,
|
||||
publication_point_rsync_uri: &str,
|
||||
issuer_ca_der: &[u8],
|
||||
issuer_ca_rsync_uri: Option<&str>,
|
||||
validation_time: time::OffsetDateTime,
|
||||
repo_sync_ok: bool,
|
||||
repo_sync_error: Option<&str>,
|
||||
) -> Result<PublicationPointResult, ManifestProcessError> {
|
||||
let fresh = if repo_sync_ok {
|
||||
try_build_fresh_pack(
|
||||
store,
|
||||
manifest_rsync_uri,
|
||||
publication_point_rsync_uri,
|
||||
issuer_ca_der,
|
||||
issuer_ca_rsync_uri,
|
||||
validation_time,
|
||||
)
|
||||
} else {
|
||||
Err(ManifestFreshError::RepoSyncFailed {
|
||||
detail: repo_sync_error.unwrap_or("repo sync failed").to_string(),
|
||||
})
|
||||
};
|
||||
|
||||
match fresh {
|
||||
Ok(pack) => {
|
||||
@@ -354,9 +387,16 @@ fn try_build_fresh_pack(
|
||||
|
||||
// RFC 9286 §4.2.1: replay/rollback detection for manifestNumber and thisUpdate.
|
||||
//
|
||||
// If a purported "new" manifest contains a manifestNumber equal to or lower than previously
|
||||
// validated manifests, or a thisUpdate less recent than previously validated manifests,
|
||||
// this is treated as a failed fetch and processing continues via the cached objects path (§6.6).
|
||||
// Important nuance for revalidation across runs:
|
||||
// - If the manifestNumber is equal to the previously validated manifestNumber *and* the
|
||||
// manifest bytes are identical, then this is the same manifest being revalidated and MUST
|
||||
// be accepted (otherwise, RPs would incorrectly treat stable repositories as "failed fetch"
|
||||
// and fall back to fetch_cache_pp).
|
||||
// - If manifestNumber is equal but the manifest bytes differ, treat this as invalid (a
|
||||
// repository is not allowed to change the manifest while keeping the manifestNumber).
|
||||
// - If manifestNumber is lower, treat as rollback and reject.
|
||||
// - If manifestNumber is higher, require thisUpdate to be more recent than the previously
|
||||
// validated thisUpdate.
|
||||
let key = FetchCachePpKey::from_manifest_rsync_uri(manifest_rsync_uri);
|
||||
if let Some(old_bytes) = store.get_fetch_cache_pp(&key).ok().flatten() {
|
||||
if let Ok(old_pack) = FetchCachePpPack::decode(&old_bytes) {
|
||||
@@ -365,28 +405,39 @@ fn try_build_fresh_pack(
|
||||
{
|
||||
let new_num = manifest.manifest.manifest_number.bytes_be.as_slice();
|
||||
let old_num = old_pack.manifest_number_be.as_slice();
|
||||
if cmp_minimal_be_unsigned(new_num, old_num) != Ordering::Greater {
|
||||
return Err(ManifestFreshError::ManifestNumberNotIncreasing {
|
||||
old_hex: hex::encode_upper(old_num),
|
||||
new_hex: hex::encode_upper(new_num),
|
||||
});
|
||||
}
|
||||
|
||||
let old_this_update = old_pack
|
||||
.this_update
|
||||
.parse()
|
||||
.expect("pack internal validation ensures this_update parses");
|
||||
if this_update <= old_this_update {
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
return Err(ManifestFreshError::ThisUpdateNotIncreasing {
|
||||
old_rfc3339_utc: old_this_update
|
||||
.to_offset(time::UtcOffset::UTC)
|
||||
.format(&Rfc3339)
|
||||
.expect("format old thisUpdate"),
|
||||
new_rfc3339_utc: this_update
|
||||
.format(&Rfc3339)
|
||||
.expect("format new thisUpdate"),
|
||||
});
|
||||
match cmp_minimal_be_unsigned(new_num, old_num) {
|
||||
Ordering::Greater => {
|
||||
let old_this_update = old_pack
|
||||
.this_update
|
||||
.parse()
|
||||
.expect("pack internal validation ensures this_update parses");
|
||||
if this_update <= old_this_update {
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
return Err(ManifestFreshError::ThisUpdateNotIncreasing {
|
||||
old_rfc3339_utc: old_this_update
|
||||
.to_offset(time::UtcOffset::UTC)
|
||||
.format(&Rfc3339)
|
||||
.expect("format old thisUpdate"),
|
||||
new_rfc3339_utc: this_update
|
||||
.format(&Rfc3339)
|
||||
.expect("format new thisUpdate"),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ordering::Equal => {
|
||||
if old_pack.manifest_bytes != manifest_bytes {
|
||||
return Err(ManifestFreshError::ManifestNumberNotIncreasing {
|
||||
old_hex: hex::encode_upper(old_num),
|
||||
new_hex: hex::encode_upper(new_num),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ordering::Less => {
|
||||
return Err(ManifestFreshError::ManifestNumberNotIncreasing {
|
||||
old_hex: hex::encode_upper(old_num),
|
||||
new_hex: hex::encode_upper(new_num),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,3 +8,4 @@ pub mod run;
|
||||
pub mod run_tree_from_tal;
|
||||
pub mod tree;
|
||||
pub mod tree_runner;
|
||||
pub mod x509_name;
|
||||
|
||||
@@ -10,7 +10,9 @@ use crate::sync::repo::sync_publication_point;
|
||||
use crate::sync::rrdp::Fetcher;
|
||||
use crate::validation::ca_instance::ca_instance_uris_from_ca_certificate;
|
||||
use crate::validation::ca_path::{CaPathError, validate_subordinate_ca_cert};
|
||||
use crate::validation::manifest::{PublicationPointSource, process_manifest_publication_point};
|
||||
use crate::validation::manifest::{
|
||||
PublicationPointSource, process_manifest_publication_point_after_repo_sync,
|
||||
};
|
||||
use crate::validation::objects::process_fetch_cache_pp_pack_for_issuer;
|
||||
use crate::validation::tree::{
|
||||
CaInstanceHandle, DiscoveredChildCaInstance, PublicationPointRunResult, PublicationPointRunner,
|
||||
@@ -31,7 +33,7 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> {
|
||||
) -> Result<PublicationPointRunResult, String> {
|
||||
let mut warnings: Vec<Warning> = Vec::new();
|
||||
|
||||
if let Err(e) = sync_publication_point(
|
||||
let (repo_sync_ok, repo_sync_err): (bool, Option<String>) = match sync_publication_point(
|
||||
self.store,
|
||||
self.policy,
|
||||
ca.rrdp_notification_uri.as_deref(),
|
||||
@@ -39,16 +41,23 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> {
|
||||
self.http_fetcher,
|
||||
self.rsync_fetcher,
|
||||
) {
|
||||
warnings.push(
|
||||
Warning::new(format!(
|
||||
"repo sync failed (continuing with cached/raw data): {e}"
|
||||
))
|
||||
.with_rfc_refs(&[RfcRef("RFC 8182 §3.4.5"), RfcRef("RFC 9286 §6.6")])
|
||||
.with_context(&ca.rsync_base_uri),
|
||||
);
|
||||
}
|
||||
Ok(res) => {
|
||||
warnings.extend(res.warnings);
|
||||
(true, None)
|
||||
}
|
||||
Err(e) => {
|
||||
warnings.push(
|
||||
Warning::new(format!(
|
||||
"repo sync failed (continuing with fetch_cache_pp only): {e}"
|
||||
))
|
||||
.with_rfc_refs(&[RfcRef("RFC 8182 §3.4.5"), RfcRef("RFC 9286 §6.6")])
|
||||
.with_context(&ca.rsync_base_uri),
|
||||
);
|
||||
(false, Some(e.to_string()))
|
||||
}
|
||||
};
|
||||
|
||||
let pp = match process_manifest_publication_point(
|
||||
let pp = match process_manifest_publication_point_after_repo_sync(
|
||||
self.store,
|
||||
self.policy,
|
||||
&ca.manifest_rsync_uri,
|
||||
@@ -56,6 +65,8 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> {
|
||||
&ca.ca_certificate_der,
|
||||
ca.ca_certificate_rsync_uri.as_deref(),
|
||||
self.validation_time,
|
||||
repo_sync_ok,
|
||||
repo_sync_err.as_deref(),
|
||||
) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return Err(format!("{e}")),
|
||||
@@ -376,6 +387,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::data_model::rc::ResourceCertificate;
|
||||
use crate::fetch::rsync::LocalDirRsyncFetcher;
|
||||
use crate::fetch::rsync::{RsyncFetchError, RsyncFetcher};
|
||||
use crate::storage::{FetchCachePpPack, PackFile, PackTime};
|
||||
use crate::sync::rrdp::Fetcher;
|
||||
use crate::validation::tree::PublicationPointRunner;
|
||||
@@ -389,6 +401,13 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
struct FailingRsyncFetcher;
|
||||
impl RsyncFetcher for FailingRsyncFetcher {
|
||||
fn fetch_objects(&self, _rsync_base_uri: &str) -> Result<Vec<(String, Vec<u8>)>, RsyncFetchError> {
|
||||
Err(RsyncFetchError::Fetch("rsync disabled in test".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
fn openssl_available() -> bool {
|
||||
Command::new("openssl")
|
||||
.arg("version")
|
||||
@@ -590,6 +609,34 @@ authorityKeyIdentifier = keyid:always
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn never_http_fetcher_returns_error() {
|
||||
let f = NeverHttpFetcher;
|
||||
let err = f.fetch("https://example.test/").unwrap_err();
|
||||
assert!(err.contains("disabled"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kind_from_rsync_uri_classifies_known_extensions() {
|
||||
assert_eq!(kind_from_rsync_uri("rsync://example.test/x.crl"), AuditObjectKind::Crl);
|
||||
assert_eq!(kind_from_rsync_uri("rsync://example.test/x.cer"), AuditObjectKind::Certificate);
|
||||
assert_eq!(kind_from_rsync_uri("rsync://example.test/x.roa"), AuditObjectKind::Roa);
|
||||
assert_eq!(kind_from_rsync_uri("rsync://example.test/x.asa"), AuditObjectKind::Aspa);
|
||||
assert_eq!(kind_from_rsync_uri("rsync://example.test/x.bin"), AuditObjectKind::Other);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_issuer_crl_from_pack_reports_missing_crldp_for_self_signed_cert() {
|
||||
let ta_der = std::fs::read(std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
|
||||
"tests/fixtures/ta/apnic-ta.cer",
|
||||
))
|
||||
.expect("read TA fixture");
|
||||
|
||||
let pack = dummy_pack_with_files(vec![]);
|
||||
let err = select_issuer_crl_from_pack(&ta_der, &pack).unwrap_err();
|
||||
assert!(err.contains("CRLDistributionPoints missing"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_issuer_crl_from_pack_finds_matching_crl() {
|
||||
// Use real fixtures to ensure child cert has CRLDP rsync URI and CRL exists.
|
||||
@@ -801,6 +848,127 @@ authorityKeyIdentifier = keyid:always
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runner_when_repo_sync_fails_uses_fetch_cache_pp_and_skips_child_discovery() {
|
||||
let fixture_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/fixtures/repository/rpki.cernet.net/repo/cernet/0");
|
||||
assert!(fixture_dir.is_dir(), "fixture directory must exist");
|
||||
|
||||
let rsync_base_uri = "rsync://rpki.cernet.net/repo/cernet/0/".to_string();
|
||||
let manifest_file = "05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft";
|
||||
let manifest_rsync_uri = format!("{rsync_base_uri}{manifest_file}");
|
||||
|
||||
let fixture_manifest_bytes =
|
||||
std::fs::read(fixture_dir.join(manifest_file)).expect("read manifest fixture");
|
||||
let fixture_manifest =
|
||||
crate::data_model::manifest::ManifestObject::decode_der(&fixture_manifest_bytes)
|
||||
.expect("decode manifest fixture");
|
||||
let validation_time = fixture_manifest.manifest.this_update + time::Duration::seconds(60);
|
||||
|
||||
let store_dir = tempfile::tempdir().expect("store dir");
|
||||
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
|
||||
let policy = Policy {
|
||||
sync_preference: crate::policy::SyncPreference::RsyncOnly,
|
||||
..Policy::default()
|
||||
};
|
||||
|
||||
let issuer_ca_der = std::fs::read(
|
||||
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(
|
||||
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
|
||||
),
|
||||
)
|
||||
.expect("read issuer ca fixture");
|
||||
let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca");
|
||||
|
||||
let handle = CaInstanceHandle {
|
||||
depth: 0,
|
||||
ca_certificate_der: issuer_ca_der,
|
||||
ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()),
|
||||
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
|
||||
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
|
||||
rsync_base_uri: rsync_base_uri.clone(),
|
||||
manifest_rsync_uri: manifest_rsync_uri.clone(),
|
||||
publication_point_rsync_uri: rsync_base_uri.clone(),
|
||||
rrdp_notification_uri: None,
|
||||
};
|
||||
|
||||
// First: successful repo sync to populate fetch_cache_pp.
|
||||
let ok_runner = Rpkiv1PublicationPointRunner {
|
||||
store: &store,
|
||||
policy: &policy,
|
||||
http_fetcher: &NeverHttpFetcher,
|
||||
rsync_fetcher: &LocalDirRsyncFetcher::new(&fixture_dir),
|
||||
validation_time,
|
||||
};
|
||||
let first = ok_runner
|
||||
.run_publication_point(&handle)
|
||||
.expect("first run ok");
|
||||
assert_eq!(first.source, PublicationPointSource::Fresh);
|
||||
assert!(first.discovered_children.is_empty(), "fixture has no child .cer");
|
||||
|
||||
// Second: repo sync fails, but we can still use fetch_cache_pp.
|
||||
let bad_runner = Rpkiv1PublicationPointRunner {
|
||||
store: &store,
|
||||
policy: &policy,
|
||||
http_fetcher: &NeverHttpFetcher,
|
||||
rsync_fetcher: &FailingRsyncFetcher,
|
||||
validation_time,
|
||||
};
|
||||
let second = bad_runner
|
||||
.run_publication_point(&handle)
|
||||
.expect("should fall back to fetch_cache_pp");
|
||||
assert_eq!(second.source, PublicationPointSource::FetchCachePp);
|
||||
assert!(second.discovered_children.is_empty());
|
||||
assert!(
|
||||
second
|
||||
.warnings
|
||||
.iter()
|
||||
.any(|w| w.message.contains("repo sync failed")),
|
||||
"expected warning about repo sync failure"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_publication_point_audit_emits_no_audit_entry_for_duplicate_pack_uri() {
|
||||
let pack = dummy_pack_with_files(vec![
|
||||
PackFile::from_bytes_compute_sha256("rsync://example.test/repo/dup.roa", vec![1u8]),
|
||||
PackFile::from_bytes_compute_sha256("rsync://example.test/repo/dup.roa", vec![2u8]),
|
||||
]);
|
||||
let pp = crate::validation::manifest::PublicationPointResult {
|
||||
source: crate::validation::manifest::PublicationPointSource::FetchCachePp,
|
||||
pack: pack.clone(),
|
||||
warnings: Vec::new(),
|
||||
};
|
||||
let ca = CaInstanceHandle {
|
||||
depth: 0,
|
||||
ca_certificate_der: vec![1],
|
||||
ca_certificate_rsync_uri: None,
|
||||
effective_ip_resources: None,
|
||||
effective_as_resources: None,
|
||||
rsync_base_uri: pack.publication_point_rsync_uri.clone(),
|
||||
manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
|
||||
publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(),
|
||||
rrdp_notification_uri: None,
|
||||
};
|
||||
let objects = crate::validation::objects::ObjectsOutput {
|
||||
vrps: Vec::new(),
|
||||
aspas: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
stats: crate::validation::objects::ObjectsStats::default(),
|
||||
audit: Vec::new(),
|
||||
};
|
||||
|
||||
let audit = build_publication_point_audit(&ca, &pp, &[], &objects, &[]);
|
||||
assert_eq!(audit.source, "fetch_cache_pp");
|
||||
assert!(
|
||||
audit
|
||||
.objects
|
||||
.iter()
|
||||
.any(|e| e.detail.as_deref() == Some("skipped: no audit entry")),
|
||||
"expected a duplicate key to produce a 'no audit entry' placeholder"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_publication_point_audit_marks_invalid_crl_as_error_and_overlays_roa_audit() {
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
|
||||
85
src/validation/x509_name.rs
Normal file
85
src/validation/x509_name.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use crate::data_model::common::X509NameDer;
|
||||
use x509_parser::prelude::FromDer;
|
||||
|
||||
fn canonicalize(name: &X509NameDer) -> Option<String> {
|
||||
let (rem, parsed) = x509_parser::x509::X509Name::from_der(name.as_raw()).ok()?;
|
||||
if !rem.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(parsed.to_string())
|
||||
}
|
||||
|
||||
/// Compare two X.509 distinguished names using a tolerant semantic comparison.
|
||||
///
|
||||
/// RPKI repositories in the wild sometimes encode the same name using different
|
||||
/// ASN.1 string types (e.g., PrintableString vs UTF8String) while remaining
|
||||
/// semantically equivalent. RFC 5280 path validation requires name matching, but
|
||||
/// DER byte equality is too strict for interoperability.
|
||||
pub fn x509_names_equivalent(a: &X509NameDer, b: &X509NameDer) -> bool {
|
||||
let Some(ca) = canonicalize(a) else {
|
||||
return a == b;
|
||||
};
|
||||
let Some(cb) = canonicalize(b) else {
|
||||
return a == b;
|
||||
};
|
||||
ca == cb
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn x509_names_equivalent_falls_back_to_der_equality_when_parse_fails() {
|
||||
// Invalid tag (not a SEQUENCE) makes x509-parser fail and forces DER equality fallback.
|
||||
let a = X509NameDer(vec![0x01, 0x00]);
|
||||
let b = X509NameDer(vec![0x01, 0x00]);
|
||||
assert!(x509_names_equivalent(&a, &b));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn x509_names_equivalent_compares_semantic_names_when_parse_succeeds() {
|
||||
let cert_der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
|
||||
)
|
||||
.expect("read certificate fixture");
|
||||
|
||||
let (_rem, cert) =
|
||||
x509_parser::parse_x509_certificate(&cert_der).expect("parse certificate fixture");
|
||||
|
||||
let subject = X509NameDer(cert.tbs_certificate.subject.as_raw().to_vec());
|
||||
let issuer = X509NameDer(cert.tbs_certificate.issuer.as_raw().to_vec());
|
||||
|
||||
assert!(x509_names_equivalent(&subject, &subject));
|
||||
assert!(!x509_names_equivalent(&subject, &issuer));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn x509_names_equivalent_falls_back_when_name_has_trailing_bytes() {
|
||||
// Use a real name DER and append a trailing byte so parsing yields leftover `rem`.
|
||||
let cert_der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
|
||||
)
|
||||
.expect("read certificate fixture");
|
||||
let (_rem, cert) =
|
||||
x509_parser::parse_x509_certificate(&cert_der).expect("parse certificate fixture");
|
||||
let mut name = cert.tbs_certificate.subject.as_raw().to_vec();
|
||||
name.push(0x00);
|
||||
let a = X509NameDer(name.clone());
|
||||
let b = X509NameDer(name);
|
||||
assert!(x509_names_equivalent(&a, &b));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn x509_names_equivalent_falls_back_when_one_side_fails_to_parse() {
|
||||
let cert_der = std::fs::read(
|
||||
"tests/fixtures/repository/rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer",
|
||||
)
|
||||
.expect("read certificate fixture");
|
||||
let (_rem, cert) =
|
||||
x509_parser::parse_x509_certificate(&cert_der).expect("parse certificate fixture");
|
||||
let good = X509NameDer(cert.tbs_certificate.subject.as_raw().to_vec());
|
||||
let bad = X509NameDer(vec![0x01, 0x00]);
|
||||
assert!(!x509_names_equivalent(&good, &bad));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user