Compare commits

...

9 Commits

23 changed files with 1102 additions and 155 deletions

View File

@ -29,6 +29,7 @@ pub const OID_AUTHORITY_INFO_ACCESS: &str = "1.3.6.1.5.5.7.1.1";
pub const OID_AUTHORITY_INFO_ACCESS_RAW: &[u8] = &asn1_rs::oid!(raw 1.3.6.1.5.5.7.1.1); pub const OID_AUTHORITY_INFO_ACCESS_RAW: &[u8] = &asn1_rs::oid!(raw 1.3.6.1.5.5.7.1.1);
pub const OID_CERTIFICATE_POLICIES: &str = "2.5.29.32"; pub const OID_CERTIFICATE_POLICIES: &str = "2.5.29.32";
pub const OID_CERTIFICATE_POLICIES_RAW: &[u8] = &asn1_rs::oid!(raw 2.5.29.32); pub const OID_CERTIFICATE_POLICIES_RAW: &[u8] = &asn1_rs::oid!(raw 2.5.29.32);
pub const OID_QT_CPS: &str = "1.3.6.1.5.5.7.2.1";
pub const OID_AUTHORITY_KEY_IDENTIFIER: &str = "2.5.29.35"; pub const OID_AUTHORITY_KEY_IDENTIFIER: &str = "2.5.29.35";
pub const OID_AUTHORITY_KEY_IDENTIFIER_RAW: &[u8] = &asn1_rs::oid!(raw 2.5.29.35); pub const OID_AUTHORITY_KEY_IDENTIFIER_RAW: &[u8] = &asn1_rs::oid!(raw 2.5.29.35);

View File

@ -13,11 +13,14 @@ use crate::data_model::common::{
use crate::data_model::oid::{ use crate::data_model::oid::{
OID_AD_CA_ISSUERS_RAW, OID_AD_CA_REPOSITORY, OID_AD_CA_REPOSITORY_RAW, OID_AD_RPKI_MANIFEST, OID_AD_CA_ISSUERS_RAW, OID_AD_CA_REPOSITORY, OID_AD_CA_REPOSITORY_RAW, OID_AD_RPKI_MANIFEST,
OID_AD_RPKI_MANIFEST_RAW, OID_AD_RPKI_NOTIFY, OID_AD_RPKI_NOTIFY_RAW, OID_AD_SIGNED_OBJECT, OID_AD_RPKI_MANIFEST_RAW, OID_AD_RPKI_NOTIFY, OID_AD_RPKI_NOTIFY_RAW, OID_AD_SIGNED_OBJECT,
OID_AD_SIGNED_OBJECT_RAW, OID_AUTHORITY_INFO_ACCESS_RAW, OID_AUTHORITY_KEY_IDENTIFIER_RAW, OID_AD_SIGNED_OBJECT_RAW, OID_AUTHORITY_INFO_ACCESS, OID_AUTHORITY_INFO_ACCESS_RAW,
OID_AUTONOMOUS_SYS_IDS_RAW, OID_BASIC_CONSTRAINTS_RAW, OID_CERTIFICATE_POLICIES_RAW, OID_AUTHORITY_KEY_IDENTIFIER, OID_AUTHORITY_KEY_IDENTIFIER_RAW, OID_AUTONOMOUS_SYS_IDS,
OID_CP_IPADDR_ASNUMBER, OID_CP_IPADDR_ASNUMBER_RAW, OID_CRL_DISTRIBUTION_POINTS_RAW, OID_AUTONOMOUS_SYS_IDS_RAW, OID_BASIC_CONSTRAINTS, OID_BASIC_CONSTRAINTS_RAW,
OID_IP_ADDR_BLOCKS_RAW, OID_SHA256_WITH_RSA_ENCRYPTION, OID_SHA256_WITH_RSA_ENCRYPTION_RAW, OID_CERTIFICATE_POLICIES, OID_CERTIFICATE_POLICIES_RAW, OID_CP_IPADDR_ASNUMBER,
OID_SUBJECT_INFO_ACCESS_RAW, OID_SUBJECT_KEY_IDENTIFIER_RAW, OID_CP_IPADDR_ASNUMBER_RAW, OID_CRL_DISTRIBUTION_POINTS, OID_CRL_DISTRIBUTION_POINTS_RAW,
OID_EXTENDED_KEY_USAGE, OID_IP_ADDR_BLOCKS, OID_IP_ADDR_BLOCKS_RAW, OID_KEY_USAGE, OID_QT_CPS,
OID_SHA256_WITH_RSA_ENCRYPTION, OID_SHA256_WITH_RSA_ENCRYPTION_RAW, OID_SUBJECT_INFO_ACCESS,
OID_SUBJECT_INFO_ACCESS_RAW, OID_SUBJECT_KEY_IDENTIFIER, OID_SUBJECT_KEY_IDENTIFIER_RAW,
}; };
/// Resource Certificate kind (semantic classification). /// Resource Certificate kind (semantic classification).
@ -27,6 +30,14 @@ pub enum ResourceCertKind {
Ee, Ee,
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ResourceCertificateRole {
TrustAnchor,
Ca,
SignedObjectEe,
RouterEe,
}
/// A parsed RPKI Resource Certificate (RFC 6487) data model. /// A parsed RPKI Resource Certificate (RFC 6487) data model.
/// ///
/// This module intentionally focuses on the semantics needed by Signed Object validation and /// This module intentionally focuses on the semantics needed by Signed Object validation and
@ -59,6 +70,7 @@ pub struct RpkixTbsCertificate {
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct RcExtensions { pub struct RcExtensions {
pub basic_constraints_ca: bool, pub basic_constraints_ca: bool,
pub basic_constraints: Option<BasicConstraintsProfile>,
pub subject_key_identifier: Option<Vec<u8>>, pub subject_key_identifier: Option<Vec<u8>>,
/// Authority Key Identifier (AKI) keyIdentifier value. /// Authority Key Identifier (AKI) keyIdentifier value.
pub authority_key_identifier: Option<Vec<u8>>, pub authority_key_identifier: Option<Vec<u8>>,
@ -68,11 +80,26 @@ pub struct RcExtensions {
pub ca_issuers_uris: Option<Vec<String>>, pub ca_issuers_uris: Option<Vec<String>>,
pub subject_info_access: Option<SubjectInfoAccess>, pub subject_info_access: Option<SubjectInfoAccess>,
pub certificate_policies_oid: Option<String>, pub certificate_policies_oid: Option<String>,
pub certificate_policies: Option<CertificatePoliciesProfile>,
pub extension_oids: Vec<String>,
pub ip_resources: Option<IpResourceSet>, pub ip_resources: Option<IpResourceSet>,
pub as_resources: Option<AsResourceSet>, pub as_resources: Option<AsResourceSet>,
} }
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BasicConstraintsProfile {
pub ca: bool,
pub critical: bool,
pub path_len_constraint: Option<u32>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CertificatePoliciesProfile {
pub policy_oid: String,
pub qualifier_oids: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResourceCertificateParsed { pub struct ResourceCertificateParsed {
pub raw_der: Vec<u8>, pub raw_der: Vec<u8>,
@ -114,13 +141,14 @@ impl AlgorithmIdentifierValue {
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct RcExtensionsParsed { pub struct RcExtensionsParsed {
pub basic_constraints_ca: Vec<bool>, pub basic_constraints: Vec<BasicConstraintsProfile>,
pub subject_key_identifier: Vec<(Vec<u8>, bool)>, pub subject_key_identifier: Vec<(Vec<u8>, bool)>,
pub authority_key_identifier: Vec<(AuthorityKeyIdentifierParsed, bool)>, pub authority_key_identifier: Vec<(AuthorityKeyIdentifierParsed, bool)>,
pub crl_distribution_points: Vec<(CrlDistributionPointsParsed, bool)>, pub crl_distribution_points: Vec<(CrlDistributionPointsParsed, bool)>,
pub authority_info_access: Vec<(AuthorityInfoAccessParsed, bool)>, pub authority_info_access: Vec<(AuthorityInfoAccessParsed, bool)>,
pub subject_info_access: Vec<(SubjectInfoAccessParsed, bool)>, pub subject_info_access: Vec<(SubjectInfoAccessParsed, bool)>,
pub certificate_policies: Vec<(Vec<String>, bool)>, pub certificate_policies: Vec<(Vec<CertificatePoliciesProfile>, bool)>,
pub extension_oids: Vec<String>,
pub ip_resources: Vec<(IpResourceSet, bool)>, pub ip_resources: Vec<(IpResourceSet, bool)>,
pub as_resources: Vec<(AsResourceSet, bool)>, pub as_resources: Vec<(AsResourceSet, bool)>,
} }
@ -406,11 +434,45 @@ pub enum ResourceCertificateProfileError {
#[error("certificatePolicies criticality must be critical (RFC 6487 §4.8.9)")] #[error("certificatePolicies criticality must be critical (RFC 6487 §4.8.9)")]
CertificatePoliciesCriticality, CertificatePoliciesCriticality,
#[error("certificatePolicies must be present (RFC 6487 §4.8.9)")]
CertificatePoliciesMissing,
#[error( #[error(
"certificatePolicies must contain RPKI policy OID {OID_CP_IPADDR_ASNUMBER}, got {0} (RFC 6487 §4.8.9)" "certificatePolicies must contain RPKI policy OID {OID_CP_IPADDR_ASNUMBER}, got {0} (RFC 6487 §4.8.9)"
)] )]
InvalidCertificatePolicy(String), InvalidCertificatePolicy(String),
#[error("certificatePolicies may contain at most one CPS qualifier (RFC 6487 §4.8.9)")]
CertificatePoliciesTooManyQualifiers,
#[error(
"certificatePolicies qualifier must be id-qt-cps ({OID_QT_CPS}), got {0} (RFC 6487 §4.8.9)"
)]
CertificatePoliciesInvalidQualifier(String),
#[error("basicConstraints must be present in CA certificates (RFC 6487 §4.8.1)")]
BasicConstraintsMissing,
#[error("basicConstraints criticality must be critical in CA certificates (RFC 6487 §4.8.1)")]
BasicConstraintsCriticality,
#[error("basicConstraints cA must be TRUE in CA certificates (RFC 6487 §4.8.1)")]
BasicConstraintsCaFalse,
#[error(
"basicConstraints pathLenConstraint must be absent in CA certificates (RFC 6487 §4.8.1)"
)]
BasicConstraintsPathLenPresent,
#[error("basicConstraints must be absent in EE certificates (RFC 6487 §4.8.1)")]
BasicConstraintsEeMustOmit,
#[error("extension {oid} is not permitted for {role} resource certificates (RFC 6487 §4.8)")]
DisallowedExtension { role: &'static str, oid: String },
#[error("autonomousSysIds RDI field must be absent (RFC 6487 §4.8.11; RFC 3779 §3.2.3)")]
AsResourcesRdiPresent,
#[error( #[error(
"SIA id-ad-signedObject accessLocation must be URI (RFC 6487 §4.8.8.2; RFC 5280 §4.2.2.2)" "SIA id-ad-signedObject accessLocation must be URI (RFC 6487 §4.8.8.2; RFC 5280 §4.2.2.2)"
)] )]
@ -567,6 +629,87 @@ impl ResourceCertificate {
Ok(()) Ok(())
} }
pub fn validate_rfc6487_profile(
&self,
role: ResourceCertificateRole,
) -> Result<(), ResourceCertificateProfileError> {
let role_name = match role {
ResourceCertificateRole::TrustAnchor => "trust anchor CA",
ResourceCertificateRole::Ca => "CA",
ResourceCertificateRole::SignedObjectEe => "signed-object EE",
ResourceCertificateRole::RouterEe => "router EE",
};
let ca_role = matches!(
role,
ResourceCertificateRole::TrustAnchor | ResourceCertificateRole::Ca
);
if ca_role {
let constraints = self
.tbs
.extensions
.basic_constraints
.as_ref()
.ok_or(ResourceCertificateProfileError::BasicConstraintsMissing)?;
if !constraints.critical {
return Err(ResourceCertificateProfileError::BasicConstraintsCriticality);
}
if !constraints.ca {
return Err(ResourceCertificateProfileError::BasicConstraintsCaFalse);
}
if constraints.path_len_constraint.is_some() {
return Err(ResourceCertificateProfileError::BasicConstraintsPathLenPresent);
}
} else if self.tbs.extensions.basic_constraints.is_some() {
return Err(ResourceCertificateProfileError::BasicConstraintsEeMustOmit);
}
for oid in &self.tbs.extensions.extension_oids {
if !is_permitted_extension(oid, role) {
return Err(ResourceCertificateProfileError::DisallowedExtension {
role: role_name,
oid: oid.clone(),
});
}
}
let policies = self
.tbs
.extensions
.certificate_policies
.as_ref()
.ok_or(ResourceCertificateProfileError::CertificatePoliciesMissing)?;
if policies.policy_oid != OID_CP_IPADDR_ASNUMBER {
return Err(ResourceCertificateProfileError::InvalidCertificatePolicy(
policies.policy_oid.clone(),
));
}
if policies.qualifier_oids.len() > 1 {
return Err(ResourceCertificateProfileError::CertificatePoliciesTooManyQualifiers);
}
if let Some(qualifier_oid) = policies.qualifier_oids.first() {
if qualifier_oid != OID_QT_CPS {
return Err(
ResourceCertificateProfileError::CertificatePoliciesInvalidQualifier(
qualifier_oid.clone(),
),
);
}
}
if self
.tbs
.extensions
.as_resources
.as_ref()
.is_some_and(|resources| resources.rdi.is_some())
{
return Err(ResourceCertificateProfileError::AsResourcesRdiPresent);
}
Ok(())
}
pub fn validate_strict_name_profile(&self) -> Result<(), ResourceCertificateProfileError> { pub fn validate_strict_name_profile(&self) -> Result<(), ResourceCertificateProfileError> {
validate_strict_rpki_name(&self.tbs.issuer_name, "issuer")?; validate_strict_rpki_name(&self.tbs.issuer_name, "issuer")?;
validate_strict_rpki_name(&self.tbs.subject_name, "subject")?; validate_strict_rpki_name(&self.tbs.subject_name, "subject")?;
@ -590,6 +733,22 @@ impl ResourceCertificate {
} }
} }
fn is_permitted_extension(oid: &str, role: ResourceCertificateRole) -> bool {
matches!(
oid,
OID_BASIC_CONSTRAINTS
| OID_KEY_USAGE
| OID_SUBJECT_KEY_IDENTIFIER
| OID_AUTHORITY_KEY_IDENTIFIER
| OID_CRL_DISTRIBUTION_POINTS
| OID_AUTHORITY_INFO_ACCESS
| OID_SUBJECT_INFO_ACCESS
| OID_CERTIFICATE_POLICIES
| OID_IP_ADDR_BLOCKS
| OID_AUTONOMOUS_SYS_IDS
) || (role == ResourceCertificateRole::RouterEe && oid == OID_EXTENDED_KEY_USAGE)
}
fn validate_strict_rpki_name( fn validate_strict_rpki_name(
name: &X509NameDer, name: &X509NameDer,
role: &'static str, role: &'static str,
@ -744,13 +903,24 @@ mod strict_name_tests {
#[test] #[test]
fn profile_rejects_rfc8360_v2_policy_oid() { fn profile_rejects_rfc8360_v2_policy_oid() {
let extensions = RcExtensionsParsed { let extensions = RcExtensionsParsed {
basic_constraints_ca: vec![true], basic_constraints: vec![BasicConstraintsProfile {
ca: true,
critical: true,
path_len_constraint: None,
}],
subject_key_identifier: Vec::new(), subject_key_identifier: Vec::new(),
authority_key_identifier: Vec::new(), authority_key_identifier: Vec::new(),
crl_distribution_points: Vec::new(), crl_distribution_points: Vec::new(),
authority_info_access: Vec::new(), authority_info_access: Vec::new(),
subject_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)], certificate_policies: vec![(
vec![CertificatePoliciesProfile {
policy_oid: "1.3.6.1.5.5.7.14.3".to_string(),
qualifier_oids: Vec::new(),
}],
true,
)],
extension_oids: Vec::new(),
ip_resources: Vec::new(), ip_resources: Vec::new(),
as_resources: Vec::new(), as_resources: Vec::new(),
}; };
@ -821,23 +991,25 @@ impl RcExtensionsParsed {
// NOTE(perf): `self` is consumed. Prefer moving decoded fields out rather than cloning, // NOTE(perf): `self` is consumed. Prefer moving decoded fields out rather than cloning,
// especially for large resource sets and URI lists. // especially for large resource sets and URI lists.
let RcExtensionsParsed { let RcExtensionsParsed {
basic_constraints_ca, basic_constraints,
subject_key_identifier, subject_key_identifier,
authority_key_identifier, authority_key_identifier,
crl_distribution_points, crl_distribution_points,
authority_info_access, authority_info_access,
subject_info_access, subject_info_access,
certificate_policies, certificate_policies,
extension_oids,
ip_resources, ip_resources,
as_resources, as_resources,
} = self; } = self;
if basic_constraints_ca.len() > 1 { if basic_constraints.len() > 1 {
return Err(ResourceCertificateProfileError::DuplicateExtension( return Err(ResourceCertificateProfileError::DuplicateExtension(
"basicConstraints", "basicConstraints",
)); ));
} }
let basic_constraints_ca = basic_constraints_ca.first().copied().unwrap_or(false); let basic_constraints = basic_constraints.into_iter().next();
let basic_constraints_ca = basic_constraints.as_ref().is_some_and(|bc| bc.ca);
let subject_key_identifier = match subject_key_identifier.len() { let subject_key_identifier = match subject_key_identifier.len() {
0 => None, 0 => None,
@ -1032,25 +1204,25 @@ impl RcExtensionsParsed {
} }
}; };
let certificate_policies_oid = match certificate_policies.len() { let certificate_policies = match certificate_policies.len() {
0 => None, 0 => None,
1 => { 1 => {
let (oids, critical) = certificate_policies.into_iter().next().expect("len==1"); let (policies, critical) = certificate_policies.into_iter().next().expect("len==1");
if !critical { if !critical {
return Err(ResourceCertificateProfileError::CertificatePoliciesCriticality); return Err(ResourceCertificateProfileError::CertificatePoliciesCriticality);
} }
if oids.len() != 1 { if policies.len() != 1 {
return Err(ResourceCertificateProfileError::InvalidCertificatePolicy( return Err(ResourceCertificateProfileError::InvalidCertificatePolicy(
"expected exactly one policy".into(), "expected exactly one policy".into(),
)); ));
} }
let policy_oid = oids.into_iter().next().expect("len==1"); let policy = policies.into_iter().next().expect("len==1");
if policy_oid != OID_CP_IPADDR_ASNUMBER { if policy.policy_oid != OID_CP_IPADDR_ASNUMBER {
return Err(ResourceCertificateProfileError::InvalidCertificatePolicy( return Err(ResourceCertificateProfileError::InvalidCertificatePolicy(
policy_oid, policy.policy_oid,
)); ));
} }
Some(OID_CP_IPADDR_ASNUMBER.to_string()) Some(policy)
} }
_ => { _ => {
return Err(ResourceCertificateProfileError::DuplicateExtension( return Err(ResourceCertificateProfileError::DuplicateExtension(
@ -1093,12 +1265,17 @@ impl RcExtensionsParsed {
Ok(RcExtensions { Ok(RcExtensions {
basic_constraints_ca, basic_constraints_ca,
basic_constraints,
subject_key_identifier, subject_key_identifier,
authority_key_identifier, authority_key_identifier,
crl_distribution_points_uris, crl_distribution_points_uris,
ca_issuers_uris, ca_issuers_uris,
subject_info_access, subject_info_access,
certificate_policies_oid, certificate_policies_oid: certificate_policies
.as_ref()
.map(|_| OID_CP_IPADDR_ASNUMBER.to_string()),
certificate_policies,
extension_oids,
ip_resources, ip_resources,
as_resources, as_resources,
}) })
@ -1127,26 +1304,32 @@ fn algorithm_identifier_value(
fn parse_extensions_parse( fn parse_extensions_parse(
exts: &[X509Extension<'_>], exts: &[X509Extension<'_>],
) -> Result<RcExtensionsParsed, ResourceCertificateParseError> { ) -> Result<RcExtensionsParsed, ResourceCertificateParseError> {
let mut basic_constraints_ca: Vec<bool> = Vec::new(); let mut basic_constraints: Vec<BasicConstraintsProfile> = Vec::new();
let mut ski: Vec<(Vec<u8>, bool)> = Vec::new(); let mut ski: Vec<(Vec<u8>, bool)> = Vec::new();
let mut aki: Vec<(AuthorityKeyIdentifierParsed, bool)> = Vec::new(); let mut aki: Vec<(AuthorityKeyIdentifierParsed, bool)> = Vec::new();
let mut crldp: Vec<(CrlDistributionPointsParsed, bool)> = Vec::new(); let mut crldp: Vec<(CrlDistributionPointsParsed, bool)> = Vec::new();
let mut aia: Vec<(AuthorityInfoAccessParsed, bool)> = Vec::new(); let mut aia: Vec<(AuthorityInfoAccessParsed, bool)> = Vec::new();
let mut sia: Vec<(SubjectInfoAccessParsed, bool)> = Vec::new(); let mut sia: Vec<(SubjectInfoAccessParsed, bool)> = Vec::new();
let mut cert_policies: Vec<(Vec<String>, bool)> = Vec::new(); let mut cert_policies: Vec<(Vec<CertificatePoliciesProfile>, bool)> = Vec::new();
let mut extension_oids: Vec<String> = Vec::with_capacity(exts.len());
let mut ip_resources: Vec<(IpResourceSet, bool)> = Vec::new(); let mut ip_resources: Vec<(IpResourceSet, bool)> = Vec::new();
let mut as_resources: Vec<(AsResourceSet, bool)> = Vec::new(); let mut as_resources: Vec<(AsResourceSet, bool)> = Vec::new();
for ext in exts { for ext in exts {
let oid = ext.oid.as_bytes(); let oid = ext.oid.as_bytes();
extension_oids.push(ext.oid.to_id_string());
if oid == OID_BASIC_CONSTRAINTS_RAW { if oid == OID_BASIC_CONSTRAINTS_RAW {
let ParsedExtension::BasicConstraints(bc) = ext.parsed_extension() else { let ParsedExtension::BasicConstraints(bc) = ext.parsed_extension() else {
return Err(ResourceCertificateParseError::Parse( return Err(ResourceCertificateParseError::Parse(
"basicConstraints parse failed".into(), "basicConstraints parse failed".into(),
)); ));
}; };
basic_constraints_ca.push(bc.ca); basic_constraints.push(BasicConstraintsProfile {
ca: bc.ca,
critical: ext.critical,
path_len_constraint: bc.path_len_constraint,
});
} else if oid == OID_SUBJECT_KEY_IDENTIFIER_RAW { } else if oid == OID_SUBJECT_KEY_IDENTIFIER_RAW {
let ParsedExtension::SubjectKeyIdentifier(s) = ext.parsed_extension() else { let ParsedExtension::SubjectKeyIdentifier(s) = ext.parsed_extension() else {
return Err(ResourceCertificateParseError::Parse( return Err(ResourceCertificateParseError::Parse(
@ -1195,16 +1378,30 @@ fn parse_extensions_parse(
"certificatePolicies parse failed".into(), "certificatePolicies parse failed".into(),
)); ));
}; };
let mut oids: Vec<String> = Vec::with_capacity(cp.len()); let mut policies: Vec<CertificatePoliciesProfile> = Vec::with_capacity(cp.len());
for p in cp.iter() { for p in cp.iter() {
let b = p.policy_id.as_bytes(); let b = p.policy_id.as_bytes();
if b == OID_CP_IPADDR_ASNUMBER_RAW { let policy_oid = if b == OID_CP_IPADDR_ASNUMBER_RAW {
oids.push(OID_CP_IPADDR_ASNUMBER.to_string()); OID_CP_IPADDR_ASNUMBER.to_string()
} else { } else {
oids.push(p.policy_id.to_id_string()); p.policy_id.to_id_string()
} };
let qualifier_oids = p
.policy_qualifiers
.as_ref()
.map(|qualifiers| {
qualifiers
.iter()
.map(|qualifier| qualifier.policy_qualifier_id.to_id_string())
.collect()
})
.unwrap_or_default();
policies.push(CertificatePoliciesProfile {
policy_oid,
qualifier_oids,
});
} }
cert_policies.push((oids, ext.critical)); cert_policies.push((policies, ext.critical));
} else if oid == OID_IP_ADDR_BLOCKS_RAW { } else if oid == OID_IP_ADDR_BLOCKS_RAW {
let parsed = IpResourceSet::decode_extn_value(ext.value) let parsed = IpResourceSet::decode_extn_value(ext.value)
.map_err(|_e| ResourceCertificateParseError::InvalidIpResourcesEncoding)?; .map_err(|_e| ResourceCertificateParseError::InvalidIpResourcesEncoding)?;
@ -1217,13 +1414,14 @@ fn parse_extensions_parse(
} }
Ok(RcExtensionsParsed { Ok(RcExtensionsParsed {
basic_constraints_ca, basic_constraints,
subject_key_identifier: ski, subject_key_identifier: ski,
authority_key_identifier: aki, authority_key_identifier: aki,
crl_distribution_points: crldp, crl_distribution_points: crldp,
authority_info_access: aia, authority_info_access: aia,
subject_info_access: sia, subject_info_access: sia,
certificate_policies: cert_policies, certificate_policies: cert_policies,
extension_oids,
ip_resources, ip_resources,
as_resources, as_resources,
}) })

View File

@ -4,6 +4,7 @@ use crate::data_model::oid::{
use crate::data_model::rc::{ use crate::data_model::rc::{
AsIdOrRange, AsIdentifierChoice, ResourceCertKind, ResourceCertificate, AsIdOrRange, AsIdentifierChoice, ResourceCertKind, ResourceCertificate,
ResourceCertificateParseError, ResourceCertificateParsed, ResourceCertificateProfileError, ResourceCertificateParseError, ResourceCertificateParsed, ResourceCertificateProfileError,
ResourceCertificateRole,
}; };
use crate::validation::cert_path::{CertPathError, validate_ee_cert_path_with_predecoded_ee}; use crate::validation::cert_path::{CertPathError, validate_ee_cert_path_with_predecoded_ee};
use x509_parser::extensions::ParsedExtension; use x509_parser::extensions::ParsedExtension;
@ -202,6 +203,7 @@ impl BgpsecRouterCertificateParsed {
self, self,
) -> Result<BgpsecRouterCertificate, BgpsecRouterCertificateProfileError> { ) -> Result<BgpsecRouterCertificate, BgpsecRouterCertificateProfileError> {
let rc = self.rc_parsed.validate_profile()?; let rc = self.rc_parsed.validate_profile()?;
rc.validate_rfc6487_profile(ResourceCertificateRole::RouterEe)?;
if rc.kind != ResourceCertKind::Ee { if rc.kind != ResourceCertKind::Ee {
return Err(BgpsecRouterCertificateProfileError::NotEe); return Err(BgpsecRouterCertificateProfileError::NotEe);
} }

View File

@ -8,7 +8,7 @@ use crate::data_model::oid::{
OID_SHA256_WITH_RSA_ENCRYPTION, OID_SHA256_WITH_RSA_ENCRYPTION_RAW, OID_SIGNED_DATA, OID_SHA256_WITH_RSA_ENCRYPTION, OID_SHA256_WITH_RSA_ENCRYPTION_RAW, OID_SIGNED_DATA,
OID_SIGNED_DATA_RAW, OID_SUBJECT_INFO_ACCESS, OID_SIGNED_DATA_RAW, OID_SUBJECT_INFO_ACCESS,
}; };
use crate::data_model::rc::{ResourceCertificate, SubjectInfoAccess}; use crate::data_model::rc::{ResourceCertificate, ResourceCertificateRole, SubjectInfoAccess};
use asn1_rs::{Any, Class, FromBer, FromDer as Asn1FromDer, Header, Tag}; use asn1_rs::{Any, Class, FromBer, FromDer as Asn1FromDer, Header, Tag};
use ring::digest; use ring::digest;
use x509_parser::extensions::ParsedExtension; use x509_parser::extensions::ParsedExtension;
@ -822,6 +822,8 @@ fn validate_ee_certificate(
}; };
} }
}; };
rc.validate_rfc6487_profile(ResourceCertificateRole::SignedObjectEe)
.map_err(|e| SignedObjectValidateError::EeCertificateParse(e.to_string()))?;
if strict_name { if strict_name {
rc.validate_strict_name_profile() rc.validate_strict_name_profile()
.map_err(|e| SignedObjectValidateError::EeCertificateParse(e.to_string()))?; .map_err(|e| SignedObjectValidateError::EeCertificateParse(e.to_string()))?;

View File

@ -5,6 +5,7 @@ use crate::data_model::oid::OID_CP_IPADDR_ASNUMBER;
use crate::data_model::rc::{ use crate::data_model::rc::{
AsIdentifierChoice, IpAddressChoice, ResourceCertKind, ResourceCertificate, AsIdentifierChoice, IpAddressChoice, ResourceCertKind, ResourceCertificate,
ResourceCertificateParseError, ResourceCertificateParsed, ResourceCertificateProfileError, ResourceCertificateParseError, ResourceCertificateParsed, ResourceCertificateProfileError,
ResourceCertificateRole,
}; };
use crate::data_model::tal::Tal; use crate::data_model::tal::Tal;
@ -218,6 +219,7 @@ impl TaCertificateParsed {
if rc_ca.kind != ResourceCertKind::Ca { if rc_ca.kind != ResourceCertKind::Ca {
return Err(TaCertificateProfileError::NotCa); return Err(TaCertificateProfileError::NotCa);
} }
rc_ca.validate_rfc6487_profile(ResourceCertificateRole::TrustAnchor)?;
if rc_ca.tbs.issuer_name != rc_ca.tbs.subject_name { if rc_ca.tbs.issuer_name != rc_ca.tbs.subject_name {
return Err(TaCertificateProfileError::NotSelfSignedIssuerSubject); return Err(TaCertificateProfileError::NotSelfSignedIssuerSubject);
@ -244,6 +246,9 @@ pub enum TrustAnchorError {
#[error("TA certificate error: {0} (RFC 8630 §2.3)")] #[error("TA certificate error: {0} (RFC 8630 §2.3)")]
TaCertificate(#[from] TaCertificateDecodeError), TaCertificate(#[from] TaCertificateDecodeError),
#[error("TA certificate self-signature error: {0} (RFC 8630 §2.3)")]
TaSelfSignature(#[from] TaCertificateVerifyError),
#[error("{0}")] #[error("{0}")]
Bind(#[from] TrustAnchorBindError), Bind(#[from] TrustAnchorBindError),
} }
@ -269,6 +274,7 @@ impl TrustAnchor {
resolved_uri: Option<&Url>, resolved_uri: Option<&Url>,
) -> Result<Self, TrustAnchorError> { ) -> Result<Self, TrustAnchorError> {
let ta_certificate = TaCertificate::decode_der(ta_der)?; let ta_certificate = TaCertificate::decode_der(ta_der)?;
ta_certificate.verify_self_signature()?;
Ok(Self::bind(tal, ta_certificate, resolved_uri)?) Ok(Self::bind(tal, ta_certificate, resolved_uri)?)
} }
@ -278,6 +284,7 @@ impl TrustAnchor {
resolved_uri: Option<&Url>, resolved_uri: Option<&Url>,
) -> Result<Self, TrustAnchorError> { ) -> Result<Self, TrustAnchorError> {
let ta_certificate = TaCertificate::decode_der_with_strict_name(ta_der)?; let ta_certificate = TaCertificate::decode_der_with_strict_name(ta_der)?;
ta_certificate.verify_self_signature()?;
Ok(Self::bind(tal, ta_certificate, resolved_uri)?) Ok(Self::bind(tal, ta_certificate, resolved_uri)?)
} }

View File

@ -19,7 +19,7 @@ pub use config::{
ALL_COLUMN_FAMILY_NAMES, CF_CHILD_CERTIFICATE_CACHE_PROJECTION, CF_MANIFEST_REPLAY_META, ALL_COLUMN_FAMILY_NAMES, CF_CHILD_CERTIFICATE_CACHE_PROJECTION, CF_MANIFEST_REPLAY_META,
CF_PUBLICATION_POINT_CACHE_PROJECTION, CF_RAW_BY_HASH, CF_REPOSITORY_VIEW, CF_PUBLICATION_POINT_CACHE_PROJECTION, CF_RAW_BY_HASH, CF_REPOSITORY_VIEW,
CF_ROA_CACHE_PROJECTION, CF_RRDP_SOURCE, CF_RRDP_SOURCE_MEMBER, CF_RRDP_URI_OWNER, CF_ROA_CACHE_PROJECTION, CF_RRDP_SOURCE, CF_RRDP_SOURCE_MEMBER, CF_RRDP_URI_OWNER,
CF_TRANSPORT_PREFETCH, CF_VCIR, column_family_descriptors, CF_TRANSPORT_PREFETCH, CF_VCIR, CF_VCIR_FAILED_FETCH_REUSE_IDENTITY, column_family_descriptors,
}; };
use keys::*; use keys::*;
use pack::compute_sha256_32; use pack::compute_sha256_32;
@ -585,6 +585,56 @@ impl VcirInstanceGate {
} }
} }
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct VcirFailedFetchReuseIdentity {
#[serde(rename = "c")]
#[serde(with = "serde_bytes_32")]
pub current_ca_sha256: [u8; 32],
#[serde(rename = "t")]
#[serde(with = "serde_bytes_32")]
pub ta_context_digest: [u8; 32],
#[serde(rename = "p")]
#[serde(with = "serde_bytes_32")]
pub parent_context_digest: [u8; 32],
#[serde(rename = "f")]
#[serde(with = "serde_bytes_32")]
pub policy_fingerprint: [u8; 32],
#[serde(rename = "nb")]
pub effective_not_before: PackTime,
#[serde(rename = "nu")]
pub effective_until: PackTime,
}
impl VcirFailedFetchReuseIdentity {
pub fn validate_internal(&self) -> StorageResult<()> {
let effective_not_before = parse_time(
"vcir_failed_fetch_reuse_identity.effective_not_before",
&self.effective_not_before,
)?;
let effective_until = parse_time(
"vcir_failed_fetch_reuse_identity.effective_until",
&self.effective_until,
)?;
if effective_not_before >= effective_until {
return Err(StorageError::InvalidData {
entity: "vcir_failed_fetch_reuse_identity.effective_window",
detail: "effective_not_before must be before effective_until".to_string(),
});
}
Ok(())
}
pub fn contains_validation_time(&self, validation_time: time::OffsetDateTime) -> bool {
let Ok(effective_not_before) = self.effective_not_before.parse() else {
return false;
};
let Ok(effective_until) = self.effective_until.parse() else {
return false;
};
validation_time >= effective_not_before && validation_time < effective_until
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct VcirChildEntry { pub struct VcirChildEntry {
pub child_manifest_rsync_uri: String, pub child_manifest_rsync_uri: String,
@ -1062,6 +1112,7 @@ pub struct RoaCacheObjectMeta {
pub source_object_hash: [u8; 32], pub source_object_hash: [u8; 32],
pub ee_serial: Vec<u8>, pub ee_serial: Vec<u8>,
pub crl_uri: String, pub crl_uri: String,
pub earliest_safe_reuse_time: PackTime,
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
@ -1127,6 +1178,8 @@ pub struct RoaCacheObjectProjection {
pub ee_serial: Option<Vec<u8>>, pub ee_serial: Option<Vec<u8>>,
#[serde(rename = "c", default, skip_serializing_if = "Option::is_none")] #[serde(rename = "c", default, skip_serializing_if = "Option::is_none")]
pub crl_uri: Option<String>, pub crl_uri: Option<String>,
#[serde(rename = "n", default, skip_serializing_if = "Option::is_none")]
pub earliest_safe_reuse_time_unix: Option<i64>,
#[serde(rename = "x")] #[serde(rename = "x")]
pub outputs_effective_until_unix: i64, pub outputs_effective_until_unix: i64,
#[serde(rename = "o")] #[serde(rename = "o")]
@ -1150,6 +1203,15 @@ impl RoaCacheObjectProjection {
if let Some(crl_uri) = &self.crl_uri { if let Some(crl_uri) = &self.crl_uri {
validate_non_empty("roa_cache_projection.entries[].crl_uri", crl_uri)?; validate_non_empty("roa_cache_projection.entries[].crl_uri", crl_uri)?;
} }
if let Some(earliest_safe_reuse_time_unix) = self.earliest_safe_reuse_time_unix
&& earliest_safe_reuse_time_unix >= self.outputs_effective_until_unix
{
return Err(StorageError::InvalidData {
entity: "roa_cache_projection.entries[].effective_window",
detail: "earliest_safe_reuse_time_unix must be before outputs_effective_until_unix"
.to_string(),
});
}
if self.outputs.is_empty() { if self.outputs.is_empty() {
return Err(StorageError::InvalidData { return Err(StorageError::InvalidData {
entity: "roa_cache_projection.entries[]", entity: "roa_cache_projection.entries[]",
@ -1867,6 +1929,9 @@ impl RoaCacheProjection {
vcir: &ValidatedCaInstanceResult, vcir: &ValidatedCaInstanceResult,
context: Option<&RoaCacheProjectionContext>, context: Option<&RoaCacheProjectionContext>,
) -> StorageResult<Option<Self>> { ) -> StorageResult<Option<Self>> {
let Some(context) = context else {
return Ok(None);
};
let mut issuer_ca_sha256_hex = None; let mut issuer_ca_sha256_hex = None;
let mut crl_sha256_by_uri = Vec::new(); let mut crl_sha256_by_uri = Vec::new();
for artifact in &vcir.related_artifacts { for artifact in &vcir.related_artifacts {
@ -1893,13 +1958,18 @@ impl RoaCacheProjection {
} }
crl_sha256_by_uri.sort_by(|left, right| left.uri.cmp(&right.uri)); crl_sha256_by_uri.sort_by(|left, right| left.uri.cmp(&right.uri));
let meta_by_uri = context.map(|context| { let meta_by_uri = context
context .object_meta
.object_meta .iter()
.iter() .map(|meta| (meta.source_object_uri.as_str(), meta))
.map(|meta| (meta.source_object_uri.as_str(), meta)) .collect::<HashMap<_, _>>();
.collect::<HashMap<_, _>>() let vcir_validation_time =
}); vcir.last_successful_validation_time
.parse()
.map_err(|detail| StorageError::InvalidData {
entity: "roa_cache_projection.entries[].earliest_safe_reuse_time_unix",
detail,
})?;
let mut entries: Vec<RoaCacheObjectProjection> = Vec::new(); let mut entries: Vec<RoaCacheObjectProjection> = Vec::new();
let mut entry_index_by_uri: HashMap<String, usize> = HashMap::new(); let mut entry_index_by_uri: HashMap<String, usize> = HashMap::new();
for output in &vcir.local_outputs { for output in &vcir.local_outputs {
@ -1915,23 +1985,26 @@ impl RoaCacheProjection {
entity: "roa_cache_projection.entries[].outputs_effective_until_unix", entity: "roa_cache_projection.entries[].outputs_effective_until_unix",
detail, detail,
})?; })?;
let meta = meta_by_uri let Some(meta) = meta_by_uri.get(output.source_object_uri.as_str()).copied() else {
.as_ref()
.and_then(|meta| meta.get(output.source_object_uri.as_str()).copied());
if context.is_some() && meta.is_none() {
continue; continue;
};
if meta.source_object_hash != output.source_object_hash {
return Err(StorageError::InvalidData {
entity: "roa_cache_projection.entries[]",
detail: format!(
"metadata source object hash mismatch for {}",
output.source_object_uri
),
});
} }
if let Some(meta) = meta { let earliest_safe_reuse_time_unix = meta
if meta.source_object_hash != output.source_object_hash { .earliest_safe_reuse_time
return Err(StorageError::InvalidData { .parse()
entity: "roa_cache_projection.entries[]", .map(|time| time.max(vcir_validation_time).unix_timestamp())
detail: format!( .map_err(|detail| StorageError::InvalidData {
"metadata source object hash mismatch for {}", entity: "roa_cache_projection.entries[].earliest_safe_reuse_time_unix",
output.source_object_uri detail,
), })?;
});
}
}
if let Some(entry_index) = entry_index_by_uri.get(output.source_object_uri.as_str()) { if let Some(entry_index) = entry_index_by_uri.get(output.source_object_uri.as_str()) {
let entry = &mut entries[*entry_index]; let entry = &mut entries[*entry_index];
if entry.source_object_hash != output.source_object_hash { if entry.source_object_hash != output.source_object_hash {
@ -1952,8 +2025,9 @@ impl RoaCacheProjection {
entries.push(RoaCacheObjectProjection { entries.push(RoaCacheObjectProjection {
source_object_uri: output.source_object_uri.clone(), source_object_uri: output.source_object_uri.clone(),
source_object_hash: output.source_object_hash, source_object_hash: output.source_object_hash,
ee_serial: meta.map(|meta| meta.ee_serial.clone()), ee_serial: Some(meta.ee_serial.clone()),
crl_uri: meta.map(|meta| meta.crl_uri.clone()), crl_uri: Some(meta.crl_uri.clone()),
earliest_safe_reuse_time_unix: Some(earliest_safe_reuse_time_unix),
outputs_effective_until_unix: projected_output_effective_until, outputs_effective_until_unix: projected_output_effective_until,
outputs: vec![projected_output], outputs: vec![projected_output],
}); });
@ -1968,8 +2042,8 @@ impl RoaCacheProjection {
manifest_rsync_uri: vcir.manifest_rsync_uri.clone(), manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
instance_effective_until: vcir.instance_gate.instance_effective_until.clone(), instance_effective_until: vcir.instance_gate.instance_effective_until.clone(),
issuer_ca_sha256_hex, issuer_ca_sha256_hex,
parent_context_digest: context.map(|context| context.parent_context_digest), parent_context_digest: Some(context.parent_context_digest),
policy_fingerprint: context.map(|context| context.policy_fingerprint), policy_fingerprint: Some(context.policy_fingerprint),
crl_sha256_by_uri, crl_sha256_by_uri,
entries, entries,
}; };
@ -2774,6 +2848,24 @@ fn write_roa_cache_projection_to_batch(
Ok(()) Ok(())
} }
fn write_vcir_failed_fetch_reuse_identity_to_batch(
failed_fetch_reuse_identity_cf: &ColumnFamily,
batch: &mut WriteBatch,
manifest_rsync_uri: &str,
failed_fetch_reuse_identity: Option<&VcirFailedFetchReuseIdentity>,
) -> StorageResult<()> {
let key = vcir_failed_fetch_reuse_identity_key(manifest_rsync_uri);
match failed_fetch_reuse_identity {
Some(identity) => {
identity.validate_internal()?;
let value = encode_cbor(identity, "vcir_failed_fetch_reuse_identity")?;
batch.put_cf(failed_fetch_reuse_identity_cf, key.as_bytes(), value);
}
None => batch.delete_cf(failed_fetch_reuse_identity_cf, key.as_bytes()),
}
Ok(())
}
fn write_publication_point_cache_projection_to_batch( fn write_publication_point_cache_projection_to_batch(
projection_cf: &ColumnFamily, projection_cf: &ColumnFamily,
batch: &mut WriteBatch, batch: &mut WriteBatch,
@ -3267,6 +3359,19 @@ impl RocksStore {
self.put_vcir_with_publication_point_cache_projection(vcir, None) self.put_vcir_with_publication_point_cache_projection(vcir, None)
} }
pub fn put_vcir_with_failed_fetch_reuse_identity(
&self,
vcir: &ValidatedCaInstanceResult,
failed_fetch_reuse_identity: &VcirFailedFetchReuseIdentity,
) -> StorageResult<()> {
self.put_vcir_with_projection_action(
vcir,
None,
PublicationPointCacheProjectionWriteAction::Keep,
Some(failed_fetch_reuse_identity),
)
}
pub fn put_vcir_with_publication_point_cache_projection( pub fn put_vcir_with_publication_point_cache_projection(
&self, &self,
vcir: &ValidatedCaInstanceResult, vcir: &ValidatedCaInstanceResult,
@ -3288,6 +3393,7 @@ impl RocksStore {
vcir, vcir,
roa_cache_context, roa_cache_context,
publication_point_projection_action, publication_point_projection_action,
None,
) )
} }
@ -3296,9 +3402,11 @@ impl RocksStore {
vcir: &ValidatedCaInstanceResult, vcir: &ValidatedCaInstanceResult,
roa_cache_context: Option<&RoaCacheProjectionContext>, roa_cache_context: Option<&RoaCacheProjectionContext>,
publication_point_projection_action: PublicationPointCacheProjectionWriteAction<'_>, publication_point_projection_action: PublicationPointCacheProjectionWriteAction<'_>,
failed_fetch_reuse_identity: Option<&VcirFailedFetchReuseIdentity>,
) -> StorageResult<()> { ) -> StorageResult<()> {
vcir.validate_internal()?; vcir.validate_internal()?;
let vcir_cf = self.cf(CF_VCIR)?; let vcir_cf = self.cf(CF_VCIR)?;
let failed_fetch_reuse_identity_cf = self.cf(CF_VCIR_FAILED_FETCH_REUSE_IDENTITY)?;
let replay_cf = self.cf(CF_MANIFEST_REPLAY_META)?; let replay_cf = self.cf(CF_MANIFEST_REPLAY_META)?;
let projection_cf = self.cf(CF_ROA_CACHE_PROJECTION)?; let projection_cf = self.cf(CF_ROA_CACHE_PROJECTION)?;
let pp_projection_cf = self.cf(CF_PUBLICATION_POINT_CACHE_PROJECTION)?; let pp_projection_cf = self.cf(CF_PUBLICATION_POINT_CACHE_PROJECTION)?;
@ -3308,6 +3416,12 @@ impl RocksStore {
let key = vcir_key(&vcir.manifest_rsync_uri); let key = vcir_key(&vcir.manifest_rsync_uri);
let value = encode_cbor(vcir, "vcir")?; let value = encode_cbor(vcir, "vcir")?;
batch.put_cf(vcir_cf, key.as_bytes(), value); batch.put_cf(vcir_cf, key.as_bytes(), value);
write_vcir_failed_fetch_reuse_identity_to_batch(
failed_fetch_reuse_identity_cf,
&mut batch,
&vcir.manifest_rsync_uri,
failed_fetch_reuse_identity,
)?;
let replay_key = manifest_replay_meta_key(&replay_meta.manifest_rsync_uri); let replay_key = manifest_replay_meta_key(&replay_meta.manifest_rsync_uri);
let replay_value = encode_cbor(&replay_meta, "manifest_replay_meta")?; let replay_value = encode_cbor(&replay_meta, "manifest_replay_meta")?;
batch.put_cf(replay_cf, replay_key.as_bytes(), replay_value); batch.put_cf(replay_cf, replay_key.as_bytes(), replay_value);
@ -3371,6 +3485,21 @@ impl RocksStore {
vcir: &ValidatedCaInstanceResult, vcir: &ValidatedCaInstanceResult,
roa_cache_context: Option<&RoaCacheProjectionContext>, roa_cache_context: Option<&RoaCacheProjectionContext>,
publication_point_projection_action: PublicationPointCacheProjectionWriteAction<'_>, publication_point_projection_action: PublicationPointCacheProjectionWriteAction<'_>,
) -> StorageResult<VcirReplaceTimingBreakdown> {
self.replace_vcir_manifest_replay_meta_and_projection_action_with_failed_fetch_reuse_identity(
vcir,
roa_cache_context,
publication_point_projection_action,
None,
)
}
pub(crate) fn replace_vcir_manifest_replay_meta_and_projection_action_with_failed_fetch_reuse_identity(
&self,
vcir: &ValidatedCaInstanceResult,
roa_cache_context: Option<&RoaCacheProjectionContext>,
publication_point_projection_action: PublicationPointCacheProjectionWriteAction<'_>,
failed_fetch_reuse_identity: Option<&VcirFailedFetchReuseIdentity>,
) -> StorageResult<VcirReplaceTimingBreakdown> { ) -> StorageResult<VcirReplaceTimingBreakdown> {
let mut timing = VcirReplaceTimingBreakdown { let mut timing = VcirReplaceTimingBreakdown {
rss_before_kb: process_vm_rss_kb(), rss_before_kb: process_vm_rss_kb(),
@ -3385,6 +3514,7 @@ impl RocksStore {
let batch_build_started = std::time::Instant::now(); let batch_build_started = std::time::Instant::now();
let vcir_cf = self.cf(CF_VCIR)?; let vcir_cf = self.cf(CF_VCIR)?;
let failed_fetch_reuse_identity_cf = self.cf(CF_VCIR_FAILED_FETCH_REUSE_IDENTITY)?;
let replay_cf = self.cf(CF_MANIFEST_REPLAY_META)?; let replay_cf = self.cf(CF_MANIFEST_REPLAY_META)?;
let projection_cf = self.cf(CF_ROA_CACHE_PROJECTION)?; let projection_cf = self.cf(CF_ROA_CACHE_PROJECTION)?;
let pp_projection_cf = self.cf(CF_PUBLICATION_POINT_CACHE_PROJECTION)?; let pp_projection_cf = self.cf(CF_PUBLICATION_POINT_CACHE_PROJECTION)?;
@ -3397,6 +3527,12 @@ impl RocksStore {
timing.vcir_value_bytes = vcir_value.len() as u64; timing.vcir_value_bytes = vcir_value.len() as u64;
batch.put_cf(vcir_cf, vcir_key.as_bytes(), vcir_value); batch.put_cf(vcir_cf, vcir_key.as_bytes(), vcir_value);
timing.rss_after_vcir_encode_kb = process_vm_rss_kb(); timing.rss_after_vcir_encode_kb = process_vm_rss_kb();
write_vcir_failed_fetch_reuse_identity_to_batch(
failed_fetch_reuse_identity_cf,
&mut batch,
&vcir.manifest_rsync_uri,
failed_fetch_reuse_identity,
)?;
let replay_meta_encode_started = std::time::Instant::now(); let replay_meta_encode_started = std::time::Instant::now();
let replay_meta = ManifestReplayMeta::from_vcir(vcir); let replay_meta = ManifestReplayMeta::from_vcir(vcir);
@ -3465,6 +3601,27 @@ impl RocksStore {
Ok(Some(vcir)) Ok(Some(vcir))
} }
pub fn get_vcir_failed_fetch_reuse_identity(
&self,
manifest_rsync_uri: &str,
) -> StorageResult<Option<VcirFailedFetchReuseIdentity>> {
let cf = self.cf(CF_VCIR_FAILED_FETCH_REUSE_IDENTITY)?;
let key = vcir_failed_fetch_reuse_identity_key(manifest_rsync_uri);
let Some(bytes) = self
.db
.get_cf(cf, key.as_bytes())
.map_err(|e| StorageError::RocksDb(e.to_string()))?
else {
return Ok(None);
};
let identity = decode_cbor::<VcirFailedFetchReuseIdentity>(
&bytes,
"vcir_failed_fetch_reuse_identity",
)?;
identity.validate_internal()?;
Ok(Some(identity))
}
pub fn get_manifest_replay_meta( pub fn get_manifest_replay_meta(
&self, &self,
manifest_rsync_uri: &str, manifest_rsync_uri: &str,
@ -4308,11 +4465,18 @@ impl RocksStore {
pub fn delete_vcir(&self, manifest_rsync_uri: &str) -> StorageResult<()> { pub fn delete_vcir(&self, manifest_rsync_uri: &str) -> StorageResult<()> {
let vcir_cf = self.cf(CF_VCIR)?; let vcir_cf = self.cf(CF_VCIR)?;
let failed_fetch_reuse_identity_cf = self.cf(CF_VCIR_FAILED_FETCH_REUSE_IDENTITY)?;
let replay_cf = self.cf(CF_MANIFEST_REPLAY_META)?; let replay_cf = self.cf(CF_MANIFEST_REPLAY_META)?;
let projection_cf = self.cf(CF_ROA_CACHE_PROJECTION)?; let projection_cf = self.cf(CF_ROA_CACHE_PROJECTION)?;
let mut batch = WriteBatch::default(); let mut batch = WriteBatch::default();
let key = vcir_key(manifest_rsync_uri); let key = vcir_key(manifest_rsync_uri);
batch.delete_cf(vcir_cf, key.as_bytes()); batch.delete_cf(vcir_cf, key.as_bytes());
let failed_fetch_reuse_identity_key =
vcir_failed_fetch_reuse_identity_key(manifest_rsync_uri);
batch.delete_cf(
failed_fetch_reuse_identity_cf,
failed_fetch_reuse_identity_key.as_bytes(),
);
let replay_key = manifest_replay_meta_key(manifest_rsync_uri); let replay_key = manifest_replay_meta_key(manifest_rsync_uri);
batch.delete_cf(replay_cf, replay_key.as_bytes()); batch.delete_cf(replay_cf, replay_key.as_bytes());
let projection_key = roa_cache_projection_key(manifest_rsync_uri); let projection_key = roa_cache_projection_key(manifest_rsync_uri);

View File

@ -4,6 +4,7 @@ pub const CF_REPOSITORY_VIEW: &str = "repository_view";
pub const CF_RAW_BY_HASH: &str = "raw_by_hash"; pub const CF_RAW_BY_HASH: &str = "raw_by_hash";
pub const CF_RAW_BLOB: &str = "raw_blob"; pub const CF_RAW_BLOB: &str = "raw_blob";
pub const CF_VCIR: &str = "vcir"; pub const CF_VCIR: &str = "vcir";
pub const CF_VCIR_FAILED_FETCH_REUSE_IDENTITY: &str = "vcir_failed_fetch_reuse_identity";
pub const CF_MANIFEST_REPLAY_META: &str = "manifest_replay_meta"; pub const CF_MANIFEST_REPLAY_META: &str = "manifest_replay_meta";
pub const CF_ROA_CACHE_PROJECTION: &str = "roa_cache_projection"; pub const CF_ROA_CACHE_PROJECTION: &str = "roa_cache_projection";
pub const CF_PUBLICATION_POINT_CACHE_PROJECTION: &str = "publication_point_cache_projection"; pub const CF_PUBLICATION_POINT_CACHE_PROJECTION: &str = "publication_point_cache_projection";
@ -18,6 +19,7 @@ pub const ALL_COLUMN_FAMILY_NAMES: &[&str] = &[
CF_RAW_BY_HASH, CF_RAW_BY_HASH,
CF_RAW_BLOB, CF_RAW_BLOB,
CF_VCIR, CF_VCIR,
CF_VCIR_FAILED_FETCH_REUSE_IDENTITY,
CF_MANIFEST_REPLAY_META, CF_MANIFEST_REPLAY_META,
CF_ROA_CACHE_PROJECTION, CF_ROA_CACHE_PROJECTION,
CF_PUBLICATION_POINT_CACHE_PROJECTION, CF_PUBLICATION_POINT_CACHE_PROJECTION,
@ -32,6 +34,8 @@ pub(super) const REPOSITORY_VIEW_KEY_PREFIX: &str = "repo_view:";
pub(super) const RAW_BY_HASH_KEY_PREFIX: &str = "rawbyhash:"; pub(super) const RAW_BY_HASH_KEY_PREFIX: &str = "rawbyhash:";
pub(super) const RAW_BLOB_KEY_PREFIX: &str = "rawblob:"; pub(super) const RAW_BLOB_KEY_PREFIX: &str = "rawblob:";
pub(super) const VCIR_KEY_PREFIX: &str = "vcir:"; pub(super) const VCIR_KEY_PREFIX: &str = "vcir:";
pub(super) const VCIR_FAILED_FETCH_REUSE_IDENTITY_KEY_PREFIX: &str =
"vcir_failed_fetch_reuse_identity:";
pub(super) const MANIFEST_REPLAY_META_KEY_PREFIX: &str = "manifest_replay_meta:"; pub(super) const MANIFEST_REPLAY_META_KEY_PREFIX: &str = "manifest_replay_meta:";
pub(super) const ROA_CACHE_PROJECTION_KEY_PREFIX: &str = "roa_cache_projection:"; pub(super) const ROA_CACHE_PROJECTION_KEY_PREFIX: &str = "roa_cache_projection:";
pub(super) const PUBLICATION_POINT_CACHE_PROJECTION_KEY_PREFIX: &str = pub(super) const PUBLICATION_POINT_CACHE_PROJECTION_KEY_PREFIX: &str =

View File

@ -26,6 +26,10 @@ pub(super) fn vcir_key(manifest_rsync_uri: &str) -> String {
format!("{VCIR_KEY_PREFIX}{manifest_rsync_uri}") format!("{VCIR_KEY_PREFIX}{manifest_rsync_uri}")
} }
pub(super) fn vcir_failed_fetch_reuse_identity_key(manifest_rsync_uri: &str) -> String {
format!("{VCIR_FAILED_FETCH_REUSE_IDENTITY_KEY_PREFIX}{manifest_rsync_uri}")
}
pub(super) fn manifest_replay_meta_key(manifest_rsync_uri: &str) -> String { pub(super) fn manifest_replay_meta_key(manifest_rsync_uri: &str) -> String {
format!("{MANIFEST_REPLAY_META_KEY_PREFIX}{manifest_rsync_uri}") format!("{MANIFEST_REPLAY_META_KEY_PREFIX}{manifest_rsync_uri}")
} }

View File

@ -284,6 +284,28 @@ fn sample_vcir(manifest_rsync_uri: &str) -> ValidatedCaInstanceResult {
} }
} }
fn roa_cache_projection_context(vcir: &ValidatedCaInstanceResult) -> RoaCacheProjectionContext {
let roa_output = vcir
.local_outputs
.iter()
.find(|output| {
output.output_type == VcirOutputType::Vrp
&& output.source_object_type == VcirSourceObjectType::Roa
})
.expect("sample VCIR has a ROA output");
RoaCacheProjectionContext {
parent_context_digest: [0x31; 32],
policy_fingerprint: [0x32; 32],
object_meta: vec![RoaCacheObjectMeta {
source_object_uri: roa_output.source_object_uri.clone(),
source_object_hash: roa_output.source_object_hash,
ee_serial: vec![0x01],
crl_uri: vcir.current_crl_rsync_uri.clone(),
earliest_safe_reuse_time: vcir.last_successful_validation_time.clone(),
}],
}
}
#[test] #[test]
fn vcir_ccr_manifest_projection_validate_accepts_valid_projection() { fn vcir_ccr_manifest_projection_validate_accepts_valid_projection() {
let projection = sample_ccr_manifest_projection( let projection = sample_ccr_manifest_projection(
@ -332,7 +354,8 @@ fn vcir_ccr_manifest_projection_validate_rejects_invalid_fields() {
#[test] #[test]
fn roa_cache_projection_from_vcir_keeps_only_roa_vrp_outputs() { fn roa_cache_projection_from_vcir_keeps_only_roa_vrp_outputs() {
let vcir = sample_vcir("rsync://example.test/repo/current.mft"); let vcir = sample_vcir("rsync://example.test/repo/current.mft");
let projection = RoaCacheProjection::from_vcir(&vcir) let context = roa_cache_projection_context(&vcir);
let projection = RoaCacheProjection::from_vcir_with_context(&vcir, Some(&context))
.expect("projection build") .expect("projection build")
.expect("projection exists"); .expect("projection exists");
@ -366,7 +389,8 @@ fn roa_cache_projection_groups_multiple_outputs_by_roa_uri() {
vcir.local_outputs.push(second); vcir.local_outputs.push(second);
vcir.summary.local_vrp_count = 2; vcir.summary.local_vrp_count = 2;
let projection = RoaCacheProjection::from_vcir(&vcir) let context = roa_cache_projection_context(&vcir);
let projection = RoaCacheProjection::from_vcir_with_context(&vcir, Some(&context))
.expect("projection build") .expect("projection build")
.expect("projection exists"); .expect("projection exists");
@ -748,7 +772,8 @@ fn roa_cache_projection_rejects_duplicate_uri_with_different_hash() {
vcir.local_outputs.push(duplicate); vcir.local_outputs.push(duplicate);
vcir.summary.local_vrp_count = 2; vcir.summary.local_vrp_count = 2;
let err = RoaCacheProjection::from_vcir(&vcir) let context = roa_cache_projection_context(&vcir);
let err = RoaCacheProjection::from_vcir_with_context(&vcir, Some(&context))
.expect_err("same ROA URI with different hash must fail"); .expect_err("same ROA URI with different hash must fail");
assert!(err.to_string().contains("source object hash mismatch")); assert!(err.to_string().contains("source object hash mismatch"));
} }
@ -1206,7 +1231,10 @@ fn vcir_roundtrip_and_validation_failures_are_reported() {
let store = RocksStore::open(td.path()).expect("open rocksdb"); let store = RocksStore::open(td.path()).expect("open rocksdb");
let vcir = sample_vcir("rsync://example.test/repo/current.mft"); let vcir = sample_vcir("rsync://example.test/repo/current.mft");
store.put_vcir(&vcir).expect("put vcir"); let roa_context = roa_cache_projection_context(&vcir);
store
.put_vcir_with_projections(&vcir, Some(&roa_context), None)
.expect("put vcir");
let got = store let got = store
.get_vcir(&vcir.manifest_rsync_uri) .get_vcir(&vcir.manifest_rsync_uri)
.expect("get vcir") .expect("get vcir")
@ -1446,8 +1474,13 @@ fn replace_vcir_and_manifest_replay_meta_replaces_current_entry() {
previous.summary.local_vrp_count = 1; previous.summary.local_vrp_count = 1;
previous.summary.local_aspa_count = 0; previous.summary.local_aspa_count = 0;
previous.summary.local_router_key_count = 0; previous.summary.local_router_key_count = 0;
let previous_roa_context = roa_cache_projection_context(&previous);
let previous_timing = store let previous_timing = store
.replace_vcir_and_manifest_replay_meta(&previous) .replace_vcir_manifest_replay_meta_and_projections(
&previous,
Some(&previous_roa_context),
None,
)
.expect("store previous vcir"); .expect("store previous vcir");
assert!(previous_timing.vcir_value_bytes > 0); assert!(previous_timing.vcir_value_bytes > 0);
assert!(previous_timing.replay_meta_value_bytes > 0); assert!(previous_timing.replay_meta_value_bytes > 0);

View File

@ -3,7 +3,8 @@ use crate::data_model::crl::{CrlDecodeError, CrlVerifyError, RpkixCrl};
use crate::data_model::oid::OID_KEY_USAGE_RAW; use crate::data_model::oid::OID_KEY_USAGE_RAW;
use crate::data_model::rc::{ use crate::data_model::rc::{
AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpResourceSet, ResourceCertKind, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpResourceSet, ResourceCertKind,
ResourceCertificate, ResourceCertificateDecodeError, ResourceCertificate, ResourceCertificateDecodeError, ResourceCertificateProfileError,
ResourceCertificateRole,
}; };
use crate::policy::ResourceValidationMode; use crate::policy::ResourceValidationMode;
use x509_parser::prelude::{FromDer, X509Certificate}; use x509_parser::prelude::{FromDer, X509Certificate};
@ -108,6 +109,12 @@ pub enum CaPathError {
#[error("issuer CA certificate decode failed: {0} (RFC 6487 §4; RFC 5280 §4.1)")] #[error("issuer CA certificate decode failed: {0} (RFC 6487 §4; RFC 5280 §4.1)")]
IssuerDecode(ResourceCertificateDecodeError), IssuerDecode(ResourceCertificateDecodeError),
#[error("child CA certificate profile validation failed: {0} (RFC 6487 §4.8)")]
ChildProfile(ResourceCertificateProfileError),
#[error("issuer CA certificate profile validation failed: {0} (RFC 6487 §4.8)")]
IssuerProfile(ResourceCertificateProfileError),
#[error("issuer CRL decode failed: {0} (RFC 6487 §5; RFC 9829 §3.1; RFC 5280 §5.1)")] #[error("issuer CRL decode failed: {0} (RFC 6487 §5; RFC 9829 §3.1; RFC 5280 §5.1)")]
CrlDecode(#[from] CrlDecodeError), CrlDecode(#[from] CrlDecodeError),
@ -244,12 +251,18 @@ pub fn validate_subordinate_ca_cert_with_resource_validation_mode(
if child_ca.kind != ResourceCertKind::Ca { if child_ca.kind != ResourceCertKind::Ca {
return Err(CaPathError::ChildNotCa); return Err(CaPathError::ChildNotCa);
} }
child_ca
.validate_rfc6487_profile(ResourceCertificateRole::Ca)
.map_err(CaPathError::ChildProfile)?;
let issuer_ca = let issuer_ca =
ResourceCertificate::decode_der(issuer_ca_der).map_err(CaPathError::IssuerDecode)?; ResourceCertificate::decode_der(issuer_ca_der).map_err(CaPathError::IssuerDecode)?;
if issuer_ca.kind != ResourceCertKind::Ca { if issuer_ca.kind != ResourceCertKind::Ca {
return Err(CaPathError::IssuerNotCa); return Err(CaPathError::IssuerNotCa);
} }
issuer_ca
.validate_rfc6487_profile(ResourceCertificateRole::Ca)
.map_err(CaPathError::IssuerProfile)?;
let issuer_spki = parse_subject_pki_from_der(&issuer_ca.tbs.subject_public_key_info)?; let issuer_spki = parse_subject_pki_from_der(&issuer_ca.tbs.subject_public_key_info)?;
if !x509_names_equivalent(&child_ca.tbs.issuer_name, &issuer_ca.tbs.subject_name) { if !x509_names_equivalent(&child_ca.tbs.issuer_name, &issuer_ca.tbs.subject_name) {
@ -379,6 +392,12 @@ pub fn validate_subordinate_ca_cert_with_prevalidated_issuer_and_resources(
if issuer_ca.kind != ResourceCertKind::Ca { if issuer_ca.kind != ResourceCertKind::Ca {
return Err(CaPathError::IssuerNotCa); return Err(CaPathError::IssuerNotCa);
} }
child_ca
.validate_rfc6487_profile(ResourceCertificateRole::Ca)
.map_err(CaPathError::ChildProfile)?;
issuer_ca
.validate_rfc6487_profile(ResourceCertificateRole::Ca)
.map_err(CaPathError::IssuerProfile)?;
if !x509_names_equivalent(&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 { return Err(CaPathError::IssuerSubjectMismatch {
@ -1375,12 +1394,14 @@ fn bytes_leq(a: &[u8], b: &[u8]) -> bool {
mod tests { mod tests {
use super::*; use super::*;
use crate::data_model::common::X509NameDer; use crate::data_model::common::X509NameDer;
use crate::data_model::oid::OID_CP_IPADDR_ASNUMBER;
use crate::data_model::rc::{ use crate::data_model::rc::{
Afi, AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily, Afi, AsIdOrRange, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily,
IpAddressOrRange, IpPrefix, IpResourceSet, IpAddressOrRange, IpPrefix, IpResourceSet,
}; };
use crate::data_model::rc::{ use crate::data_model::rc::{
RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate, BasicConstraintsProfile, CertificatePoliciesProfile, RcExtensions, ResourceCertKind,
ResourceCertificate, RpkixTbsCertificate,
}; };
use der_parser::num_bigint::BigUint; use der_parser::num_bigint::BigUint;
use std::process::Command; use std::process::Command;
@ -1410,12 +1431,27 @@ mod tests {
subject_public_key_info: Vec::new(), subject_public_key_info: Vec::new(),
extensions: RcExtensions { extensions: RcExtensions {
basic_constraints_ca: kind == ResourceCertKind::Ca, basic_constraints_ca: kind == ResourceCertKind::Ca,
basic_constraints: (kind == ResourceCertKind::Ca).then_some(
BasicConstraintsProfile {
ca: true,
critical: true,
path_len_constraint: None,
},
),
subject_key_identifier: ski, subject_key_identifier: ski,
authority_key_identifier: aki, authority_key_identifier: aki,
crl_distribution_points_uris: crldp, crl_distribution_points_uris: crldp,
ca_issuers_uris: aia, ca_issuers_uris: aia,
subject_info_access: None, subject_info_access: None,
certificate_policies_oid: None, certificate_policies_oid: (kind == ResourceCertKind::Ca)
.then_some(OID_CP_IPADDR_ASNUMBER.to_string()),
certificate_policies: (kind == ResourceCertKind::Ca).then_some(
CertificatePoliciesProfile {
policy_oid: OID_CP_IPADDR_ASNUMBER.to_string(),
qualifier_oids: Vec::new(),
},
),
extension_oids: Vec::new(),
ip_resources: None, ip_resources: None,
as_resources: None, as_resources: None,
}, },

View File

@ -2,6 +2,7 @@ use crate::data_model::common::BigUnsigned;
use crate::data_model::crl::{CrlDecodeError, CrlVerifyError, RpkixCrl}; use crate::data_model::crl::{CrlDecodeError, CrlVerifyError, RpkixCrl};
use crate::data_model::rc::{ use crate::data_model::rc::{
ResourceCertKind, ResourceCertificate, ResourceCertificateDecodeError, ResourceCertKind, ResourceCertificate, ResourceCertificateDecodeError,
ResourceCertificateProfileError, ResourceCertificateRole,
}; };
use crate::data_model::signed_object::{EeKeyUsageSummary, ResourceEeCertificate}; use crate::data_model::signed_object::{EeKeyUsageSummary, ResourceEeCertificate};
use ring::signature; use ring::signature;
@ -34,6 +35,9 @@ pub enum CertPathError {
)] )]
IssuerNotCa, IssuerNotCa,
#[error("issuer CA certificate profile validation failed: {0} (RFC 6487 §4.8)")]
IssuerProfile(ResourceCertificateProfileError),
#[error("EE certificate must be an EE resource certificate (RFC 6487 §4)")] #[error("EE certificate must be an EE resource certificate (RFC 6487 §4)")]
EeNotEe, EeNotEe,
@ -128,6 +132,9 @@ pub fn validate_ee_cert_path(
if issuer_ca.kind != ResourceCertKind::Ca { if issuer_ca.kind != ResourceCertKind::Ca {
return Err(CertPathError::IssuerNotCa); return Err(CertPathError::IssuerNotCa);
} }
issuer_ca
.validate_rfc6487_profile(ResourceCertificateRole::Ca)
.map_err(CertPathError::IssuerProfile)?;
let issuer_spki = parse_subject_pki_from_der(&issuer_ca.tbs.subject_public_key_info)?; let issuer_spki = parse_subject_pki_from_der(&issuer_ca.tbs.subject_public_key_info)?;
if !x509_names_equivalent(&ee.tbs.issuer_name, &issuer_ca.tbs.subject_name) { if !x509_names_equivalent(&ee.tbs.issuer_name, &issuer_ca.tbs.subject_name) {
@ -311,6 +318,9 @@ fn validate_ee_cert_path_bindings_and_status(
if issuer_ca.kind != ResourceCertKind::Ca { if issuer_ca.kind != ResourceCertKind::Ca {
return Err(CertPathError::IssuerNotCa); return Err(CertPathError::IssuerNotCa);
} }
issuer_ca
.validate_rfc6487_profile(ResourceCertificateRole::Ca)
.map_err(CertPathError::IssuerProfile)?;
if !x509_names_equivalent(&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 { return Err(CertPathError::IssuerSubjectMismatch {
@ -515,8 +525,10 @@ fn is_serial_revoked_by_crl(ee: &ResourceCertificate, crl: &RpkixCrl) -> bool {
mod tests { mod tests {
use super::*; use super::*;
use crate::data_model::common::X509NameDer; use crate::data_model::common::X509NameDer;
use crate::data_model::oid::OID_CP_IPADDR_ASNUMBER;
use crate::data_model::rc::{ use crate::data_model::rc::{
RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate, BasicConstraintsProfile, CertificatePoliciesProfile, RcExtensions, ResourceCertKind,
ResourceCertificate, RpkixTbsCertificate,
}; };
use der_parser::num_bigint::BigUint; use der_parser::num_bigint::BigUint;
@ -545,12 +557,27 @@ mod tests {
subject_public_key_info: Vec::new(), subject_public_key_info: Vec::new(),
extensions: RcExtensions { extensions: RcExtensions {
basic_constraints_ca: kind == ResourceCertKind::Ca, basic_constraints_ca: kind == ResourceCertKind::Ca,
basic_constraints: (kind == ResourceCertKind::Ca).then_some(
BasicConstraintsProfile {
ca: true,
critical: true,
path_len_constraint: None,
},
),
subject_key_identifier: ski, subject_key_identifier: ski,
authority_key_identifier: aki, authority_key_identifier: aki,
crl_distribution_points_uris: crldp, crl_distribution_points_uris: crldp,
ca_issuers_uris: aia, ca_issuers_uris: aia,
subject_info_access: None, subject_info_access: None,
certificate_policies_oid: None, certificate_policies_oid: (kind == ResourceCertKind::Ca)
.then_some(OID_CP_IPADDR_ASNUMBER.to_string()),
certificate_policies: (kind == ResourceCertKind::Ca).then_some(
CertificatePoliciesProfile {
policy_oid: OID_CP_IPADDR_ASNUMBER.to_string(),
qualifier_oids: Vec::new(),
},
),
extension_oids: Vec::new(),
ip_resources: None, ip_resources: None,
as_resources: None, as_resources: None,
}, },

View File

@ -506,6 +506,7 @@ pub struct CachedRoaValidationResult {
source_object_hash: [u8; 32], source_object_hash: [u8; 32],
ee_serial: Option<Vec<u8>>, ee_serial: Option<Vec<u8>>,
crl_uri: Option<String>, crl_uri: Option<String>,
earliest_safe_reuse_time_unix: i64,
outputs_effective_until_unix: i64, outputs_effective_until_unix: i64,
outputs: Vec<VcirLocalOutput>, outputs: Vec<VcirLocalOutput>,
} }
@ -543,6 +544,12 @@ impl RoaValidationCacheView {
} }
for entry in &projection.entries { for entry in &projection.entries {
let Some(earliest_safe_reuse_time_unix) = entry.earliest_safe_reuse_time_unix else {
continue;
};
if time::OffsetDateTime::from_unix_timestamp(earliest_safe_reuse_time_unix).is_err() {
continue;
}
let outputs = entry let outputs = entry
.outputs .outputs
.iter() .iter()
@ -563,6 +570,7 @@ impl RoaValidationCacheView {
source_object_hash: entry.source_object_hash, source_object_hash: entry.source_object_hash,
ee_serial: entry.ee_serial.clone(), ee_serial: entry.ee_serial.clone(),
crl_uri: entry.crl_uri.clone(), crl_uri: entry.crl_uri.clone(),
earliest_safe_reuse_time_unix,
outputs_effective_until_unix: entry.outputs_effective_until_unix, outputs_effective_until_unix: entry.outputs_effective_until_unix,
outputs, outputs,
}, },
@ -674,7 +682,9 @@ impl RoaValidationCacheView {
if cached.outputs.is_empty() { if cached.outputs.is_empty() {
return_entry_gate!(RoaCacheLookupResult::Miss); return_entry_gate!(RoaCacheLookupResult::Miss);
} }
if cached.outputs_effective_until_unix <= validation_time.unix_timestamp() { if validation_time.unix_timestamp() < cached.earliest_safe_reuse_time_unix
|| cached.outputs_effective_until_unix <= validation_time.unix_timestamp()
{
return_entry_gate!(RoaCacheLookupResult::ExpiredBlocked); return_entry_gate!(RoaCacheLookupResult::ExpiredBlocked);
} }
@ -760,6 +770,10 @@ impl RoaValidationCacheView {
source_object_hash: file.sha256, source_object_hash: file.sha256,
ee_serial: ee_serial.clone(), ee_serial: ee_serial.clone(),
crl_uri: crl_uri.to_string(), crl_uri: crl_uri.to_string(),
earliest_safe_reuse_time: PackTime::from_utc_offset_datetime(
time::OffsetDateTime::from_unix_timestamp(cached.earliest_safe_reuse_time_unix)
.expect("cached ROA safe reuse time must be valid"),
),
}), }),
}; };
metrics.materialize_nanos = metrics metrics.materialize_nanos = metrics
@ -821,6 +835,14 @@ fn crl_valid_at_time(
validation_time >= this_update && validation_time < next_update validation_time >= this_update && validation_time < next_update
} }
fn roa_cache_earliest_safe_reuse_time(
ee_not_before: time::OffsetDateTime,
crl_this_update: time::OffsetDateTime,
validation_time: time::OffsetDateTime,
) -> PackTime {
PackTime::from_utc_offset_datetime(ee_not_before.max(crl_this_update).max(validation_time))
}
fn record_roa_cache_block( fn record_roa_cache_block(
stats: &mut RoaValidationCacheStats, stats: &mut RoaValidationCacheStats,
lookup_result: &RoaCacheLookupResult, lookup_result: &RoaCacheLookupResult,
@ -2736,6 +2758,11 @@ fn process_roa_with_issuer(
source_object_hash: file.sha256, source_object_hash: file.sha256,
ee_serial: BigUnsigned::from_biguint(&ee.resource_cert.tbs.serial_number).bytes_be, ee_serial: BigUnsigned::from_biguint(&ee.resource_cert.tbs.serial_number).bytes_be,
crl_uri: issuer_crl_rsync_uri.to_string(), crl_uri: issuer_crl_rsync_uri.to_string(),
earliest_safe_reuse_time: roa_cache_earliest_safe_reuse_time(
ee.resource_cert.tbs.validity_not_before,
verified_crl.crl.this_update.utc,
validation_time,
),
}; };
if !collect_vcir_local_outputs { if !collect_vcir_local_outputs {
return Ok((vrps, Vec::new(), Some(cache_object_meta))); return Ok((vrps, Vec::new(), Some(cache_object_meta)));
@ -2866,6 +2893,11 @@ fn process_roa_with_issuer_parallel_cached(
source_object_hash: file.sha256, source_object_hash: file.sha256,
ee_serial: BigUnsigned::from_biguint(&ee.resource_cert.tbs.serial_number).bytes_be, ee_serial: BigUnsigned::from_biguint(&ee.resource_cert.tbs.serial_number).bytes_be,
crl_uri: issuer_crl_rsync_uri.clone(), crl_uri: issuer_crl_rsync_uri.clone(),
earliest_safe_reuse_time: roa_cache_earliest_safe_reuse_time(
ee.resource_cert.tbs.validity_not_before,
verified_crl.crl.this_update.utc,
validation_time,
),
}; };
if !collect_vcir_local_outputs { if !collect_vcir_local_outputs {
return Ok((vrps, Vec::new(), Some(cache_object_meta))); return Ok((vrps, Vec::new(), Some(cache_object_meta)));
@ -3908,6 +3940,7 @@ mod tests {
source_object_hash: roa_hash, source_object_hash: roa_hash,
ee_serial: vec![0x01], ee_serial: vec![0x01],
crl_uri: TEST_CRL_URI.to_string(), crl_uri: TEST_CRL_URI.to_string(),
earliest_safe_reuse_time: vcir.last_successful_validation_time.clone(),
}], }],
}), }),
) )
@ -4299,7 +4332,7 @@ mod tests {
issuer_der, issuer_der,
crl_hash, crl_hash,
roa_hash, roa_hash,
fixed_time("2026-06-04T00:00:00Z"), fixed_time("2026-06-04T01:00:00Z"),
fixed_time("2026-06-08T00:00:00Z"), fixed_time("2026-06-08T00:00:00Z"),
); );
let projection = sample_roa_cache_projection(&vcir, roa_hash); let projection = sample_roa_cache_projection(&vcir, roa_hash);
@ -4313,6 +4346,58 @@ mod tests {
)); ));
} }
#[test]
fn roa_validation_cache_view_blocks_before_safe_reuse_time() {
let validation_time = fixed_time("2026-06-05T00:00:00Z");
let issuer_der = b"issuer-ca";
let crl_bytes = b"current-crl".to_vec();
let crl_hash = sha256_32(&crl_bytes);
let roa_hash = [0x11; 32];
let vcir = sample_roa_cache_vcir(
issuer_der,
crl_hash,
roa_hash,
fixed_time("2026-06-07T00:00:00Z"),
fixed_time("2026-06-08T00:00:00Z"),
);
let mut projection = sample_roa_cache_projection(&vcir, roa_hash);
projection.entries[0].earliest_safe_reuse_time_unix =
Some(fixed_time("2026-06-06T00:00:00Z").unix_timestamp());
let view = RoaValidationCacheView::from_projection(&projection, validation_time);
let file = PackFile::from_bytes_with_sha256(TEST_ROA_URI, vec![0x01], roa_hash);
let mut crl_cache = sample_crl_cache(crl_bytes);
assert!(matches!(
view.lookup(&file, &mut crl_cache, issuer_der, validation_time),
RoaCacheLookupResult::ExpiredBlocked
));
}
#[test]
fn roa_validation_cache_view_treats_legacy_entry_without_lower_bound_as_miss() {
let validation_time = fixed_time("2026-06-05T00:00:00Z");
let issuer_der = b"issuer-ca";
let crl_hash = sha256_32(b"current-crl");
let roa_hash = [0x11; 32];
let vcir = sample_roa_cache_vcir(
issuer_der,
crl_hash,
roa_hash,
fixed_time("2026-06-07T00:00:00Z"),
fixed_time("2026-06-08T00:00:00Z"),
);
let mut projection = sample_roa_cache_projection(&vcir, roa_hash);
projection.entries[0].earliest_safe_reuse_time_unix = None;
let view = RoaValidationCacheView::from_projection(&projection, validation_time);
let file = PackFile::from_bytes_with_sha256(TEST_ROA_URI, vec![0x01], roa_hash);
let mut crl_cache = sample_crl_cache(b"current-crl".to_vec());
assert!(matches!(
view.lookup(&file, &mut crl_cache, issuer_der, validation_time),
RoaCacheLookupResult::Miss
));
}
#[test] #[test]
fn roa_validation_cache_stats_records_vcir_miss_to_timing() { fn roa_validation_cache_stats_records_vcir_miss_to_timing() {
let stats = RoaValidationCacheStats::for_input(RoaValidationCacheInput::enabled(None), 3); let stats = RoaValidationCacheStats::for_input(RoaValidationCacheInput::enabled(None), 3);
@ -4594,6 +4679,7 @@ mod tests {
source_object_hash: roa_hash, source_object_hash: roa_hash,
ee_serial: Some(vec![0x01]), ee_serial: Some(vec![0x01]),
crl_uri: None, crl_uri: None,
earliest_safe_reuse_time_unix: validation_time.unix_timestamp(),
outputs_effective_until_unix, outputs_effective_until_unix,
outputs: vec![output.clone()], outputs: vec![output.clone()],
}, },
@ -4610,6 +4696,7 @@ mod tests {
source_object_hash: roa_hash, source_object_hash: roa_hash,
ee_serial: None, ee_serial: None,
crl_uri: Some(TEST_CRL_URI.to_string()), crl_uri: Some(TEST_CRL_URI.to_string()),
earliest_safe_reuse_time_unix: validation_time.unix_timestamp(),
outputs_effective_until_unix, outputs_effective_until_unix,
outputs: vec![output.clone()], outputs: vec![output.clone()],
}, },
@ -4626,6 +4713,7 @@ mod tests {
source_object_hash: roa_hash, source_object_hash: roa_hash,
ee_serial: Some(vec![0x01]), ee_serial: Some(vec![0x01]),
crl_uri: Some(TEST_CRL_URI.to_string()), crl_uri: Some(TEST_CRL_URI.to_string()),
earliest_safe_reuse_time_unix: validation_time.unix_timestamp(),
outputs_effective_until_unix, outputs_effective_until_unix,
outputs: vec![output.clone()], outputs: vec![output.clone()],
}, },
@ -4652,6 +4740,7 @@ mod tests {
source_object_hash: [0xff; 32], source_object_hash: [0xff; 32],
ee_serial: Some(vec![0x01]), ee_serial: Some(vec![0x01]),
crl_uri: Some(TEST_CRL_URI.to_string()), crl_uri: Some(TEST_CRL_URI.to_string()),
earliest_safe_reuse_time_unix: validation_time.unix_timestamp(),
outputs_effective_until_unix, outputs_effective_until_unix,
outputs: vec![output.clone()], outputs: vec![output.clone()],
}, },
@ -4668,6 +4757,7 @@ mod tests {
source_object_hash: roa_hash, source_object_hash: roa_hash,
ee_serial: Some(vec![0x01]), ee_serial: Some(vec![0x01]),
crl_uri: Some(TEST_CRL_URI.to_string()), crl_uri: Some(TEST_CRL_URI.to_string()),
earliest_safe_reuse_time_unix: validation_time.unix_timestamp(),
outputs_effective_until_unix, outputs_effective_until_unix,
outputs: Vec::new(), outputs: Vec::new(),
}, },
@ -4688,6 +4778,7 @@ mod tests {
source_object_hash: roa_hash, source_object_hash: roa_hash,
ee_serial: Some(vec![0x01]), ee_serial: Some(vec![0x01]),
crl_uri: Some(TEST_CRL_URI.to_string()), crl_uri: Some(TEST_CRL_URI.to_string()),
earliest_safe_reuse_time_unix: validation_time.unix_timestamp(),
outputs_effective_until_unix, outputs_effective_until_unix,
outputs: vec![output], outputs: vec![output],
}, },

View File

@ -33,8 +33,9 @@ use crate::storage::{
PublicationPointCacheProjectionWriteAction, RawByHashEntry, RoaCacheProjectionContext, PublicationPointCacheProjectionWriteAction, RawByHashEntry, RoaCacheProjectionContext,
RocksStore, ValidatedCaInstanceResult, VcirArtifactKind, VcirArtifactRole, RocksStore, ValidatedCaInstanceResult, VcirArtifactKind, VcirArtifactRole,
VcirArtifactValidationStatus, VcirAuditSummary, VcirCcrManifestProjection, VcirChildEntry, VcirArtifactValidationStatus, VcirAuditSummary, VcirCcrManifestProjection, VcirChildEntry,
VcirInstanceGate, VcirLocalOutput, VcirLocalOutputPayload, VcirOutputType, VcirRelatedArtifact, VcirFailedFetchReuseIdentity, VcirInstanceGate, VcirLocalOutput, VcirLocalOutputPayload,
VcirReplaceTimingBreakdown, VcirSourceObjectType, VcirSummary, VcirOutputType, VcirRelatedArtifact, VcirReplaceTimingBreakdown, VcirSourceObjectType,
VcirSummary,
}; };
use crate::sync::repo::{ use crate::sync::repo::{
sync_publication_point, sync_publication_point_replay, sync_publication_point_replay_delta, sync_publication_point, sync_publication_point_replay, sync_publication_point_replay_delta,
@ -1751,6 +1752,7 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> {
self.store, self.store,
ca, ca,
&fresh_err, &fresh_err,
self.policy,
self.validation_time, self.validation_time,
) )
.map_err(|e| format!("failed fetch VCIR projection failed: {e}"))?; .map_err(|e| format!("failed fetch VCIR projection failed: {e}"))?;
@ -2314,8 +2316,18 @@ fn discover_children_from_fresh_snapshot_with_audit_cached_with_issuer_der<
let issuer_ca_decode_error: Option<String>; let issuer_ca_decode_error: Option<String>;
let issuer_ca = match crate::data_model::rc::ResourceCertificate::decode_der(issuer_ca_der) { let issuer_ca = match crate::data_model::rc::ResourceCertificate::decode_der(issuer_ca_der) {
Ok(v) => { Ok(v) => {
issuer_ca_decode_error = None; match v.validate_rfc6487_profile(crate::data_model::rc::ResourceCertificateRole::Ca) {
Some(v) Ok(()) => {
issuer_ca_decode_error = None;
Some(v)
}
Err(e) => {
issuer_ca_decode_error = Some(format!(
"issuer CA profile validation failed: {e} (RFC 6487 §4.8)"
));
None
}
}
} }
Err(e) => { Err(e) => {
issuer_ca_decode_error = Some(format!( issuer_ca_decode_error = Some(format!(
@ -4256,6 +4268,7 @@ fn project_current_instance_vcir_on_failed_fetch(
store: &RocksStore, store: &RocksStore,
ca: &CaInstanceHandle, ca: &CaInstanceHandle,
fresh_err: &ManifestFreshError, fresh_err: &ManifestFreshError,
policy: &Policy,
validation_time: time::OffsetDateTime, validation_time: time::OffsetDateTime,
) -> Result<VcirReuseProjection, String> { ) -> Result<VcirReuseProjection, String> {
let mut warnings = Vec::new(); let mut warnings = Vec::new();
@ -4264,80 +4277,57 @@ fn project_current_instance_vcir_on_failed_fetch(
.get_vcir(&ca.manifest_rsync_uri) .get_vcir(&ca.manifest_rsync_uri)
.map_err(|e| format!("load VCIR failed: {e}"))? .map_err(|e| format!("load VCIR failed: {e}"))?
else { else {
warnings.push( return Ok(failed_fetch_no_cache_projection(
Warning::new(format!("manifest failed fetch: {fresh_err}")) ca,
.with_rfc_refs(&[RfcRef("RFC 9286 §6.6")]) fresh_err,
.with_context(&ca.manifest_rsync_uri), None,
); "no latest validated result for current CA instance; no cached output reused",
warnings.push( ));
Warning::new(
"no latest validated result for current CA instance; no cached output reused",
)
.with_rfc_refs(&[RfcRef("RFC 9286 §6.6")])
.with_context(&ca.manifest_rsync_uri),
);
return Ok(VcirReuseProjection {
source: PublicationPointSource::FailedFetchNoCache,
vcir: None,
ccr_manifest_projection: None,
snapshot: None,
objects: empty_objects_output(),
child_audits: Vec::new(),
discovered_children: Vec::new(),
warnings,
});
}; };
if !vcir.audit_summary.failed_fetch_eligible { if !vcir.audit_summary.failed_fetch_eligible {
warnings.push( return Ok(failed_fetch_no_cache_projection(
Warning::new(format!("manifest failed fetch: {fresh_err}")) ca,
.with_rfc_refs(&[RfcRef("RFC 9286 §6.6")]) fresh_err,
.with_context(&ca.manifest_rsync_uri), Some(vcir),
); "latest VCIR is not marked failed-fetch eligible; no cached output reused",
warnings.push( ));
Warning::new(
"latest VCIR is not marked failed-fetch eligible; no cached output reused",
)
.with_rfc_refs(&[RfcRef("RFC 9286 §6.6")])
.with_context(&ca.manifest_rsync_uri),
);
return Ok(VcirReuseProjection {
source: PublicationPointSource::FailedFetchNoCache,
vcir: Some(vcir),
ccr_manifest_projection: None,
snapshot: None,
objects: empty_objects_output(),
child_audits: Vec::new(),
discovered_children: Vec::new(),
warnings,
});
} }
let instance_effective_until = let reuse_identity = match store.get_vcir_failed_fetch_reuse_identity(&ca.manifest_rsync_uri) {
parse_snapshot_time_value(&vcir.instance_gate.instance_effective_until)?; Ok(Some(identity)) => identity,
if validation_time > instance_effective_until { Ok(None) => {
warnings.push( return Ok(failed_fetch_no_cache_projection(
Warning::new(format!("manifest failed fetch: {fresh_err}")) ca,
.with_rfc_refs(&[RfcRef("RFC 9286 §6.6")]) fresh_err,
.with_context(&ca.manifest_rsync_uri), Some(vcir),
); "latest VCIR reuse identity is missing; no cached output or child reused",
warnings.push( ));
Warning::new( }
"latest VCIR instance_gate expired; current instance contributes no cached output", Err(error) => {
) return Ok(failed_fetch_no_cache_projection(
.with_rfc_refs(&[RfcRef("RFC 9286 §6.6")]) ca,
.with_context(&ca.manifest_rsync_uri), fresh_err,
); Some(vcir),
return Ok(VcirReuseProjection { &format!(
source: PublicationPointSource::FailedFetchNoCache, "latest VCIR reuse identity is invalid ({error}); no cached output or child reused"
vcir: Some(vcir), ),
ccr_manifest_projection: None, ));
snapshot: None, }
objects: empty_objects_output(), };
child_audits: Vec::new(), if !failed_fetch_reuse_identity_matches_current(
discovered_children: Vec::new(), &reuse_identity,
warnings, &vcir,
}); ca,
policy,
validation_time,
) {
return Ok(failed_fetch_no_cache_projection(
ca,
fresh_err,
Some(vcir),
"latest VCIR reuse identity does not match the current CA context or time window; no cached output or child reused",
));
} }
let ccr_manifest_projection = reuse_ccr_manifest_projection_from_vcir(ca, &vcir)?; let ccr_manifest_projection = reuse_ccr_manifest_projection_from_vcir(ca, &vcir)?;
@ -4367,6 +4357,73 @@ fn project_current_instance_vcir_on_failed_fetch(
}) })
} }
fn failed_fetch_no_cache_projection(
ca: &CaInstanceHandle,
fresh_err: &ManifestFreshError,
vcir: Option<ValidatedCaInstanceResult>,
reason: &str,
) -> VcirReuseProjection {
let warnings = vec![
Warning::new(format!("manifest failed fetch: {fresh_err}"))
.with_rfc_refs(&[RfcRef("RFC 9286 §6.6")])
.with_context(&ca.manifest_rsync_uri),
Warning::new(reason)
.with_rfc_refs(&[RfcRef("RFC 9286 §6.6")])
.with_context(&ca.manifest_rsync_uri),
];
VcirReuseProjection {
source: PublicationPointSource::FailedFetchNoCache,
vcir,
ccr_manifest_projection: None,
snapshot: None,
objects: empty_objects_output(),
child_audits: Vec::new(),
discovered_children: Vec::new(),
warnings,
}
}
fn failed_fetch_reuse_identity_for_fresh_result(
ca: &CaInstanceHandle,
policy: &Policy,
validation_time: time::OffsetDateTime,
effective_until: PackTime,
) -> Result<VcirFailedFetchReuseIdentity, String> {
let current_ca_sha256 = ca.ca_certificate_sha256_32().ok_or_else(|| {
"current CA certificate hash unavailable for VCIR reuse identity".to_string()
})?;
let identity = VcirFailedFetchReuseIdentity {
current_ca_sha256,
ta_context_digest: ta_context_digest_for_ca(ca),
parent_context_digest: parent_context_digest_for_ca(ca),
policy_fingerprint: publication_point_cache_policy_fingerprint(policy),
effective_not_before: PackTime::from_utc_offset_datetime(validation_time),
effective_until,
};
identity
.validate_internal()
.map_err(|error| error.to_string())?;
Ok(identity)
}
fn failed_fetch_reuse_identity_matches_current(
cached: &VcirFailedFetchReuseIdentity,
vcir: &ValidatedCaInstanceResult,
ca: &CaInstanceHandle,
policy: &Policy,
validation_time: time::OffsetDateTime,
) -> bool {
let Some(current_ca_sha256) = ca.ca_certificate_sha256_32() else {
return false;
};
cached.current_ca_sha256 == current_ca_sha256
&& cached.ta_context_digest == ta_context_digest_for_ca(ca)
&& cached.parent_context_digest == parent_context_digest_for_ca(ca)
&& cached.policy_fingerprint == publication_point_cache_policy_fingerprint(policy)
&& cached.effective_until == vcir.instance_gate.instance_effective_until
&& cached.contains_validation_time(validation_time)
}
#[cfg(test)] #[cfg(test)]
fn reconstruct_snapshot_from_vcir( fn reconstruct_snapshot_from_vcir(
store: &RocksStore, store: &RocksStore,
@ -5280,8 +5337,14 @@ fn persist_vcir_for_fresh_result_with_timing(
.expect("publication point projection must exist when guard is not active"), .expect("publication point projection must exist when guard is not active"),
) )
}; };
let failed_fetch_reuse_identity = failed_fetch_reuse_identity_for_fresh_result(
ca,
policy,
validation_time,
vcir.instance_gate.instance_effective_until.clone(),
)?;
let replace_timing = store let replace_timing = store
.replace_vcir_manifest_replay_meta_and_projection_action( .replace_vcir_manifest_replay_meta_and_projection_action_with_failed_fetch_reuse_identity(
&vcir, &vcir,
Some(&RoaCacheProjectionContext { Some(&RoaCacheProjectionContext {
parent_context_digest: parent_context_digest_for_ca(ca), parent_context_digest: parent_context_digest_for_ca(ca),
@ -5289,6 +5352,7 @@ fn persist_vcir_for_fresh_result_with_timing(
object_meta: objects.roa_cache_object_meta.clone(), object_meta: objects.roa_cache_object_meta.clone(),
}), }),
publication_point_cache_projection_action, publication_point_cache_projection_action,
Some(&failed_fetch_reuse_identity),
) )
.map_err(|e| format!("store VCIR and manifest replay meta failed: {e}"))?; .map_err(|e| format!("store VCIR and manifest replay meta failed: {e}"))?;
timing.replace_vcir_ms = replace_vcir_started.elapsed().as_millis() as u64; timing.replace_vcir_ms = replace_vcir_started.elapsed().as_millis() as u64;

View File

@ -753,6 +753,44 @@ fn sample_vcir_for_projection(
} }
} }
fn put_vcir_for_failed_fetch_reuse(
store: &RocksStore,
ca: &CaInstanceHandle,
policy: &Policy,
vcir: &ValidatedCaInstanceResult,
) {
let validation_time = vcir
.last_successful_validation_time
.parse()
.expect("parse VCIR validation time");
let identity = failed_fetch_reuse_identity_for_fresh_result(
ca,
policy,
validation_time,
vcir.instance_gate.instance_effective_until.clone(),
)
.expect("build VCIR failed-fetch reuse identity");
store
.put_vcir_with_failed_fetch_reuse_identity(vcir, &identity)
.expect("put reusable VCIR");
}
fn sample_ca_for_failed_fetch_reuse(vcir: &ValidatedCaInstanceResult) -> CaInstanceHandle {
CaInstanceHandle {
depth: 0,
tal_id: "test-tal".to_string(),
parent_manifest_rsync_uri: None,
ca_certificate: CaCertificateRef::inline_der(Vec::new()),
ca_certificate_rsync_uri: None,
effective_ip_resources: None,
effective_as_resources: None,
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: Some("https://example.test/notify.xml".to_string()),
}
}
#[test] #[test]
fn never_http_fetcher_returns_error() { fn never_http_fetcher_returns_error() {
let f = NeverHttpFetcher; let f = NeverHttpFetcher;
@ -3827,7 +3865,6 @@ fn project_current_instance_vcir_reuses_local_outputs_and_restores_children() {
let repo_bytes_db = store_dir.path().join("repo-bytes.db"); let repo_bytes_db = store_dir.path().join("repo-bytes.db");
let store = RocksStore::open_with_external_repo_bytes(&main_db, &repo_bytes_db) let store = RocksStore::open_with_external_repo_bytes(&main_db, &repo_bytes_db)
.expect("open rocksdb with external repo bytes"); .expect("open rocksdb with external repo bytes");
store.put_vcir(&vcir).expect("put vcir");
store store
.put_blob_bytes_batch(&[(child_cert_hash.clone(), g.child_ca_der.clone())]) .put_blob_bytes_batch(&[(child_cert_hash.clone(), g.child_ca_der.clone())])
.expect("put child cert repo bytes"); .expect("put child cert repo bytes");
@ -3852,6 +3889,8 @@ fn project_current_instance_vcir_reuses_local_outputs_and_restores_children() {
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: Some("https://example.test/notify.xml".to_string()), rrdp_notification_uri: Some("https://example.test/notify.xml".to_string()),
}; };
let policy = Policy::default();
put_vcir_for_failed_fetch_reuse(&store, &ca, &policy, &vcir);
let projection = project_current_instance_vcir_on_failed_fetch( let projection = project_current_instance_vcir_on_failed_fetch(
&store, &store,
@ -3859,6 +3898,7 @@ fn project_current_instance_vcir_reuses_local_outputs_and_restores_children() {
&ManifestFreshError::RepoSyncFailed { &ManifestFreshError::RepoSyncFailed {
detail: "synthetic".to_string(), detail: "synthetic".to_string(),
}, },
&policy,
now, now,
) )
.expect("project vcir"); .expect("project vcir");
@ -3913,6 +3953,79 @@ fn project_current_instance_vcir_reuses_local_outputs_and_restores_children() {
); );
} }
#[test]
fn project_current_instance_vcir_does_not_reuse_without_identity() {
let now = time::OffsetDateTime::now_utc();
let child_cert_hash = sha256_hex(b"child-cert");
let vcir = sample_vcir_for_projection(now, &child_cert_hash);
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
store.put_vcir(&vcir).expect("put legacy-style VCIR");
let ca = sample_ca_for_failed_fetch_reuse(&vcir);
let projection = project_current_instance_vcir_on_failed_fetch(
&store,
&ca,
&ManifestFreshError::RepoSyncFailed {
detail: "synthetic".to_string(),
},
&Policy::default(),
now,
)
.expect("project VCIR");
assert_eq!(
projection.source,
PublicationPointSource::FailedFetchNoCache
);
assert!(projection.objects.vrps.is_empty());
assert!(projection.discovered_children.is_empty());
assert!(
projection
.warnings
.iter()
.any(|warning| warning.message.contains("reuse identity is missing"))
);
}
#[test]
fn project_current_instance_vcir_does_not_reuse_when_ta_context_changes() {
let now = time::OffsetDateTime::now_utc();
let child_cert_hash = sha256_hex(b"child-cert");
let vcir = sample_vcir_for_projection(now, &child_cert_hash);
let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
let policy = Policy::default();
let original_ca = sample_ca_for_failed_fetch_reuse(&vcir);
put_vcir_for_failed_fetch_reuse(&store, &original_ca, &policy, &vcir);
let mut changed_ca = original_ca;
changed_ca.tal_id = "different-tal".to_string();
let projection = project_current_instance_vcir_on_failed_fetch(
&store,
&changed_ca,
&ManifestFreshError::RepoSyncFailed {
detail: "synthetic".to_string(),
},
&policy,
now,
)
.expect("project VCIR");
assert_eq!(
projection.source,
PublicationPointSource::FailedFetchNoCache
);
assert!(projection.objects.vrps.is_empty());
assert!(projection.discovered_children.is_empty());
assert!(
projection
.warnings
.iter()
.any(|warning| warning.message.contains("identity does not match"))
);
}
#[test] #[test]
fn project_current_instance_vcir_returns_no_output_when_instance_gate_expired() { fn project_current_instance_vcir_returns_no_output_when_instance_gate_expired() {
let now = time::OffsetDateTime::now_utc(); let now = time::OffsetDateTime::now_utc();
@ -3950,6 +4063,7 @@ fn project_current_instance_vcir_returns_no_output_when_instance_gate_expired()
&ManifestFreshError::RepoSyncFailed { &ManifestFreshError::RepoSyncFailed {
detail: "synthetic".to_string(), detail: "synthetic".to_string(),
}, },
&Policy::default(),
now, now,
) )
.expect("project vcir"); .expect("project vcir");
@ -3975,7 +4089,6 @@ fn project_current_instance_vcir_keeps_real_fresh_validation_warning() {
let repo_bytes_db = store_dir.path().join("repo-bytes.db"); let repo_bytes_db = store_dir.path().join("repo-bytes.db");
let store = RocksStore::open_with_external_repo_bytes(&main_db, &repo_bytes_db) let store = RocksStore::open_with_external_repo_bytes(&main_db, &repo_bytes_db)
.expect("open rocksdb with external repo bytes"); .expect("open rocksdb with external repo bytes");
store.put_vcir(&vcir).expect("put vcir");
store store
.put_blob_bytes_batch(&[(child_cert_hash, b"child-cert".to_vec())]) .put_blob_bytes_batch(&[(child_cert_hash, b"child-cert".to_vec())])
.expect("put child cert repo bytes"); .expect("put child cert repo bytes");
@ -3993,6 +4106,8 @@ fn project_current_instance_vcir_keeps_real_fresh_validation_warning() {
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: None, rrdp_notification_uri: None,
}; };
let policy = Policy::default();
put_vcir_for_failed_fetch_reuse(&store, &ca, &policy, &vcir);
let projection = project_current_instance_vcir_on_failed_fetch( let projection = project_current_instance_vcir_on_failed_fetch(
&store, &store,
@ -4000,6 +4115,7 @@ fn project_current_instance_vcir_keeps_real_fresh_validation_warning() {
&ManifestFreshError::HashMismatch { &ManifestFreshError::HashMismatch {
rsync_uri: "rsync://example.test/repo/issuer/a.roa".to_string(), rsync_uri: "rsync://example.test/repo/issuer/a.roa".to_string(),
}, },
&policy,
now, now,
) )
.expect("project vcir"); .expect("project vcir");
@ -4048,6 +4164,7 @@ fn project_current_instance_vcir_returns_no_output_when_latest_result_missing()
&ManifestFreshError::RepoSyncFailed { &ManifestFreshError::RepoSyncFailed {
detail: "synthetic".to_string(), detail: "synthetic".to_string(),
}, },
&Policy::default(),
now, now,
) )
.expect("project without cached vcir"); .expect("project without cached vcir");
@ -4099,6 +4216,7 @@ fn project_current_instance_vcir_returns_no_output_when_latest_result_is_ineligi
&ManifestFreshError::RepoSyncFailed { &ManifestFreshError::RepoSyncFailed {
detail: "synthetic".to_string(), detail: "synthetic".to_string(),
}, },
&Policy::default(),
now, now,
) )
.expect("project ineligible vcir"); .expect("project ineligible vcir");
@ -4128,7 +4246,6 @@ fn project_current_instance_vcir_rejects_mismatched_ccr_projection_uri() {
let store_dir = tempfile::tempdir().expect("store dir"); let store_dir = tempfile::tempdir().expect("store dir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb"); let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
store.put_vcir(&vcir).expect("put vcir");
let ca = CaInstanceHandle { let ca = CaInstanceHandle {
depth: 0, depth: 0,
@ -4143,6 +4260,8 @@ fn project_current_instance_vcir_rejects_mismatched_ccr_projection_uri() {
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(), publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
rrdp_notification_uri: None, rrdp_notification_uri: None,
}; };
let policy = Policy::default();
put_vcir_for_failed_fetch_reuse(&store, &ca, &policy, &vcir);
let err = project_current_instance_vcir_on_failed_fetch( let err = project_current_instance_vcir_on_failed_fetch(
&store, &store,
@ -4150,6 +4269,7 @@ fn project_current_instance_vcir_rejects_mismatched_ccr_projection_uri() {
&ManifestFreshError::RepoSyncFailed { &ManifestFreshError::RepoSyncFailed {
detail: "synthetic".to_string(), detail: "synthetic".to_string(),
}, },
&policy,
now, now,
) )
.unwrap_err(); .unwrap_err();

View File

@ -383,7 +383,7 @@ fn metrics_for(obj_type: ObjType, der: &[u8]) -> Result<Metrics, String> {
ObjType::Cer => { ObjType::Cer => {
let parsed = ResourceCertificate::parse_der(der).map_err(|e| e.to_string())?; let parsed = ResourceCertificate::parse_der(der).map_err(|e| e.to_string())?;
let spki_len = parsed.subject_public_key_info.len() as u32; let spki_len = parsed.subject_public_key_info.len() as u32;
let ext_count = (parsed.extensions.basic_constraints_ca.len() let ext_count = (parsed.extensions.basic_constraints.len()
+ parsed.extensions.subject_key_identifier.len() + parsed.extensions.subject_key_identifier.len()
+ parsed.extensions.authority_key_identifier.len() + parsed.extensions.authority_key_identifier.len()
+ parsed.extensions.crl_distribution_points.len() + parsed.extensions.crl_distribution_points.len()

View File

@ -25,6 +25,7 @@ fn dummy_ee(
subject_public_key_info: vec![], subject_public_key_info: vec![],
extensions: RcExtensions { extensions: RcExtensions {
basic_constraints_ca: false, basic_constraints_ca: false,
basic_constraints: None,
subject_key_identifier: Some(vec![0x01]), subject_key_identifier: Some(vec![0x01]),
authority_key_identifier: None, authority_key_identifier: None,
crl_distribution_points_uris: None, crl_distribution_points_uris: None,
@ -35,6 +36,8 @@ fn dummy_ee(
}, },
)), )),
certificate_policies_oid: None, certificate_policies_oid: None,
certificate_policies: None,
extension_oids: Vec::new(),
ip_resources, ip_resources,
as_resources, as_resources,
}, },

View File

@ -81,6 +81,7 @@ CN = Test Issuer CA
[ v3_issuer_ca ] [ v3_issuer_ca ]
basicConstraints = critical,CA:true basicConstraints = critical,CA:true
keyUsage = critical, keyCertSign, cRLSign keyUsage = critical, keyCertSign, cRLSign
certificatePolicies = critical,1.3.6.1.5.5.7.14.2
subjectKeyIdentifier = hash subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always authorityKeyIdentifier = keyid:always

View File

@ -88,6 +88,18 @@ fn offline_discovery_from_apnic_tal_and_ta_der_fixture_works() {
} }
} }
#[test]
fn offline_discovery_rejects_tampered_ta_self_signature() {
let tal_bytes = apnic_tal_bytes();
let mut ta_der = apnic_ta_der();
let last = ta_der.last_mut().expect("TA fixture must not be empty");
*last ^= 0x01;
let err = discover_root_ca_instance_from_tal_and_ta_der(&tal_bytes, &ta_der, None)
.expect_err("tampered TA signature must not form a root CA instance");
assert!(err.to_string().contains("self-signature"), "{err}");
}
#[test] #[test]
fn discover_root_from_tal_url_works_with_mock_fetcher() { fn discover_root_from_tal_url_works_with_mock_fetcher() {
let tal_bytes = apnic_tal_bytes(); let tal_bytes = apnic_tal_bytes();

View File

@ -0,0 +1,131 @@
use der_parser::num_bigint::BigUint;
use rpki::data_model::common::X509NameDer;
use rpki::data_model::oid::{
OID_AUTONOMOUS_SYS_IDS, OID_BASIC_CONSTRAINTS, OID_CERTIFICATE_POLICIES, OID_CP_IPADDR_ASNUMBER,
};
use rpki::data_model::rc::{
AsIdOrRange, AsIdentifierChoice, AsResourceSet, BasicConstraintsProfile,
CertificatePoliciesProfile, RcExtensions, ResourceCertKind, ResourceCertificate,
ResourceCertificateProfileError, ResourceCertificateRole, RpkixTbsCertificate,
};
use time::OffsetDateTime;
fn valid_ca_certificate() -> ResourceCertificate {
let validation_time = OffsetDateTime::from_unix_timestamp(0).expect("valid epoch");
ResourceCertificate {
raw_der: Vec::new(),
kind: ResourceCertKind::Ca,
tbs: RpkixTbsCertificate {
version: 2,
serial_number: BigUint::from(1u32),
signature_algorithm: "1.2.840.113549.1.1.11".to_string(),
issuer_name: X509NameDer(b"CN=issuer".to_vec()),
subject_name: X509NameDer(b"CN=subject".to_vec()),
validity_not_before: validation_time,
validity_not_after: validation_time,
subject_public_key_info: Vec::new(),
extensions: RcExtensions {
basic_constraints_ca: true,
basic_constraints: Some(BasicConstraintsProfile {
ca: true,
critical: true,
path_len_constraint: None,
}),
subject_key_identifier: None,
authority_key_identifier: None,
crl_distribution_points_uris: None,
ca_issuers_uris: None,
subject_info_access: None,
certificate_policies_oid: Some(OID_CP_IPADDR_ASNUMBER.to_string()),
certificate_policies: Some(CertificatePoliciesProfile {
policy_oid: OID_CP_IPADDR_ASNUMBER.to_string(),
qualifier_oids: Vec::new(),
}),
extension_oids: vec![
OID_BASIC_CONSTRAINTS.to_string(),
OID_CERTIFICATE_POLICIES.to_string(),
],
ip_resources: None,
as_resources: None,
},
},
}
}
#[test]
fn ca_profile_accepts_required_basic_constraints_and_policy() {
valid_ca_certificate()
.validate_rfc6487_profile(ResourceCertificateRole::Ca)
.expect("valid CA profile");
}
#[test]
fn ca_profile_rejects_noncritical_basic_constraints() {
let mut certificate = valid_ca_certificate();
certificate
.tbs
.extensions
.basic_constraints
.as_mut()
.expect("constraints")
.critical = false;
assert!(matches!(
certificate.validate_rfc6487_profile(ResourceCertificateRole::Ca),
Err(ResourceCertificateProfileError::BasicConstraintsCriticality)
));
}
#[test]
fn ca_profile_rejects_unknown_extension() {
let mut certificate = valid_ca_certificate();
certificate
.tbs
.extensions
.extension_oids
.push("1.2.3.4".to_string());
assert!(matches!(
certificate.validate_rfc6487_profile(ResourceCertificateRole::Ca),
Err(ResourceCertificateProfileError::DisallowedExtension { .. })
));
}
#[test]
fn ca_profile_rejects_non_cps_policy_qualifier() {
let mut certificate = valid_ca_certificate();
certificate
.tbs
.extensions
.certificate_policies
.as_mut()
.expect("certificate policies")
.qualifier_oids
.push("1.2.3.4".to_string());
assert!(matches!(
certificate.validate_rfc6487_profile(ResourceCertificateRole::Ca),
Err(ResourceCertificateProfileError::CertificatePoliciesInvalidQualifier(_))
));
}
#[test]
fn ca_profile_rejects_autonomous_system_rdi() {
let mut certificate = valid_ca_certificate();
certificate.tbs.extensions.as_resources = Some(AsResourceSet {
asnum: None,
rdi: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(
64512,
)])),
});
certificate
.tbs
.extensions
.extension_oids
.push(OID_AUTONOMOUS_SYS_IDS.to_string());
assert!(matches!(
certificate.validate_rfc6487_profile(ResourceCertificateRole::Ca),
Err(ResourceCertificateProfileError::AsResourcesRdiPresent)
));
}

View File

@ -28,6 +28,7 @@ fn dummy_ee(
subject_public_key_info: vec![], subject_public_key_info: vec![],
extensions: RcExtensions { extensions: RcExtensions {
basic_constraints_ca: false, basic_constraints_ca: false,
basic_constraints: None,
subject_key_identifier: Some(vec![0x01]), subject_key_identifier: Some(vec![0x01]),
authority_key_identifier: None, authority_key_identifier: None,
crl_distribution_points_uris: None, crl_distribution_points_uris: None,
@ -38,6 +39,8 @@ fn dummy_ee(
}, },
)), )),
certificate_policies_oid: None, certificate_policies_oid: None,
certificate_policies: None,
extension_oids: Vec::new(),
ip_resources, ip_resources,
as_resources, as_resources,
}, },

View File

@ -794,13 +794,22 @@ fn ee_certificate_missing_signed_object_sia_is_rejected() {
)); ));
let err = RpkiSignedObject::decode_der(&so).unwrap_err(); let err = RpkiSignedObject::decode_der(&so).unwrap_err();
assert!(matches!( let rejected_for_sia = matches!(
err, &err,
SignedObjectDecodeError::Validate(SignedObjectValidateError::EeCertificateMissingSia) SignedObjectDecodeError::Validate(SignedObjectValidateError::EeCertificateMissingSia)
| SignedObjectDecodeError::Validate( | SignedObjectDecodeError::Validate(
SignedObjectValidateError::EeCertificateMissingSignedObjectSia SignedObjectValidateError::EeCertificateMissingSignedObjectSia
) )
)); );
let rejected_for_ee_profile = matches!(
&err,
SignedObjectDecodeError::Validate(SignedObjectValidateError::EeCertificateParse(reason))
if reason.contains("basicConstraints must be absent")
);
assert!(
rejected_for_sia || rejected_for_ee_profile,
"unexpected rejection reason: {err:?}"
);
} }
#[test] #[test]
@ -834,12 +843,21 @@ fn ee_certificate_sia_without_signed_object_access_method_is_rejected() {
vec![si], vec![si],
)); ));
let err = RpkiSignedObject::decode_der(&so).unwrap_err(); let err = RpkiSignedObject::decode_der(&so).unwrap_err();
assert!(matches!( let rejected_for_sia = matches!(
err, &err,
SignedObjectDecodeError::Validate( SignedObjectDecodeError::Validate(
SignedObjectValidateError::EeCertificateMissingSignedObjectSia SignedObjectValidateError::EeCertificateMissingSignedObjectSia
) )
)); );
let rejected_for_ee_profile = matches!(
&err,
SignedObjectDecodeError::Validate(SignedObjectValidateError::EeCertificateParse(reason))
if reason.contains("basicConstraints must be absent")
);
assert!(
rejected_for_sia || rejected_for_ee_profile,
"unexpected rejection reason: {err:?}"
);
} }
#[test] #[test]

View File

@ -68,12 +68,15 @@ fn ta_certificate_rejects_non_self_signed_ca() {
fn ta_constraints_require_policies_and_ski() { fn ta_constraints_require_policies_and_ski() {
let rc = dummy_rc_ca(RcExtensions { let rc = dummy_rc_ca(RcExtensions {
basic_constraints_ca: true, basic_constraints_ca: true,
basic_constraints: None,
subject_key_identifier: None, subject_key_identifier: None,
authority_key_identifier: None, authority_key_identifier: None,
crl_distribution_points_uris: None, crl_distribution_points_uris: None,
ca_issuers_uris: None, ca_issuers_uris: None,
subject_info_access: None, subject_info_access: None,
certificate_policies_oid: None, certificate_policies_oid: None,
certificate_policies: None,
extension_oids: Vec::new(),
ip_resources: Some(rpki::data_model::rc::IpResourceSet { families: vec![] }), ip_resources: Some(rpki::data_model::rc::IpResourceSet { families: vec![] }),
as_resources: None, as_resources: None,
}); });
@ -97,12 +100,15 @@ fn ta_constraints_require_non_empty_resources_and_no_inherit() {
// Missing both IP and AS resources. // Missing both IP and AS resources.
let rc = dummy_rc_ca(RcExtensions { let rc = dummy_rc_ca(RcExtensions {
basic_constraints_ca: true, basic_constraints_ca: true,
basic_constraints: None,
subject_key_identifier: Some(vec![1]), subject_key_identifier: Some(vec![1]),
authority_key_identifier: None, authority_key_identifier: None,
crl_distribution_points_uris: None, crl_distribution_points_uris: None,
ca_issuers_uris: None, ca_issuers_uris: None,
subject_info_access: None, subject_info_access: None,
certificate_policies_oid: Some(OID_CP_IPADDR_ASNUMBER.to_string()), certificate_policies_oid: Some(OID_CP_IPADDR_ASNUMBER.to_string()),
certificate_policies: None,
extension_oids: Vec::new(),
ip_resources: None, ip_resources: None,
as_resources: None, as_resources: None,
}); });

View File

@ -1,4 +1,6 @@
use rpki::data_model::ta::{TrustAnchor, TrustAnchorBindError, TrustAnchorError}; use rpki::data_model::ta::{
TaCertificateVerifyError, TrustAnchor, TrustAnchorBindError, TrustAnchorError,
};
use rpki::data_model::tal::Tal; use rpki::data_model::tal::Tal;
use url::Url; use url::Url;
@ -76,3 +78,21 @@ fn bind_rejects_resolved_uri_not_listed_in_tal() {
)) ))
)); ));
} }
#[test]
fn bind_rejects_tampered_ta_self_signature() {
let tal_raw =
std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal").expect("read TAL fixture");
let tal = Tal::decode_bytes(&tal_raw).expect("decode TAL fixture");
let mut ta_der = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read TA fixture");
let last = ta_der.last_mut().expect("TA fixture must not be empty");
*last ^= 0x01;
assert!(matches!(
TrustAnchor::bind_der(tal, &ta_der, None),
Err(TrustAnchorError::TaSelfSignature(
TaCertificateVerifyError::InvalidSelfSignature(_)
))
));
}