313 lines
10 KiB
Rust
313 lines
10 KiB
Rust
use url::Url;
|
|
use x509_parser::prelude::{FromDer, X509Certificate};
|
|
|
|
use crate::data_model::oid::OID_CP_IPADDR_ASNUMBER;
|
|
use crate::data_model::rc::{
|
|
AsIdentifierChoice, IpAddressChoice, ResourceCertKind, ResourceCertificate,
|
|
ResourceCertificateParseError, ResourceCertificateParsed, ResourceCertificateProfileError,
|
|
ResourceCertificateRole,
|
|
};
|
|
use crate::data_model::tal::Tal;
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct TaCertificate {
|
|
pub raw_der: Vec<u8>,
|
|
pub rc_ca: ResourceCertificate,
|
|
}
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum TaCertificateParseError {
|
|
#[error("TA certificate parse error: {0} (RFC 5280 §4.1; RFC 6487 §4; RFC 8630 §2.3)")]
|
|
ResourceCertificate(#[from] ResourceCertificateParseError),
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct TaCertificateParsed {
|
|
pub rc_parsed: ResourceCertificateParsed,
|
|
}
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum TaCertificateProfileError {
|
|
#[error("resource certificate profile error: {0} (RFC 5280 §4; RFC 6487 §4)")]
|
|
ResourceCertificate(#[from] ResourceCertificateProfileError),
|
|
|
|
#[error("TA certificate must be a CA certificate (RFC 8630 §2.3; RFC 6487 §4.8.1)")]
|
|
NotCa,
|
|
|
|
#[error(
|
|
"TA certificate must be self-signed (issuer DN must equal subject DN) (RFC 8630 §2.3; RFC 5280 §4.1.2.4)"
|
|
)]
|
|
NotSelfSignedIssuerSubject,
|
|
|
|
#[error(
|
|
"TA certificate must contain certificatePolicies ipAddr-asNumber ({OID_CP_IPADDR_ASNUMBER}) (RFC 6487 §4.8.9; RFC 8630 §2.3)"
|
|
)]
|
|
MissingOrInvalidCertificatePolicies,
|
|
|
|
#[error("TA certificate must contain SubjectKeyIdentifier (RFC 6487 §4.8.2; RFC 8630 §2.3)")]
|
|
MissingSubjectKeyIdentifier,
|
|
|
|
#[error(
|
|
"TA certificate must contain at least one RFC 3779 resource extension (IP or AS) (RFC 6487 §4.8.10-§4.8.11; RFC 8630 §2.3)"
|
|
)]
|
|
ResourcesMissing,
|
|
|
|
#[error("TA certificate resources must be non-empty (RFC 8630 §2.3)")]
|
|
ResourcesEmpty,
|
|
|
|
#[error(
|
|
"TA certificate MUST NOT use inherit in IP resources (RFC 8630 §2.3; RFC 3779 §2.2.3.5)"
|
|
)]
|
|
IpResourcesInherit,
|
|
|
|
#[error(
|
|
"TA certificate MUST NOT use inherit in AS resources (RFC 8630 §2.3; RFC 3779 §3.2.3.3)"
|
|
)]
|
|
AsResourcesInherit,
|
|
}
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum TaCertificateDecodeError {
|
|
#[error("{0}")]
|
|
Parse(#[from] TaCertificateParseError),
|
|
|
|
#[error("{0}")]
|
|
Validate(#[from] TaCertificateProfileError),
|
|
}
|
|
|
|
/// Backwards-compatible name: TA certificate errors from parse+validate.
|
|
pub type TaCertificateError = TaCertificateDecodeError;
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum TaCertificateVerifyError {
|
|
#[error("TA certificate parse error: {0} (RFC 5280 §4.1; RFC 8630 §2.3)")]
|
|
Parse(String),
|
|
|
|
#[error("trailing bytes after TA certificate DER: {0} bytes (DER; RFC 5280 §4.1)")]
|
|
TrailingBytes(usize),
|
|
|
|
#[error(
|
|
"TA certificate self-signature verification failed: {0} (RFC 8630 §2.3; RFC 5280 §6.1)"
|
|
)]
|
|
InvalidSelfSignature(String),
|
|
}
|
|
|
|
impl TaCertificate {
|
|
/// Parse step of scheme A (`parse → validate → verify`).
|
|
pub fn parse_der(der: &[u8]) -> Result<TaCertificateParsed, TaCertificateParseError> {
|
|
Ok(TaCertificateParsed {
|
|
rc_parsed: ResourceCertificate::parse_der(der)?,
|
|
})
|
|
}
|
|
|
|
/// Profile validate step of scheme A (`parse → validate → verify`).
|
|
///
|
|
/// `TaCertificate` is already profile-validated when constructed via `decode_der()` /
|
|
/// `TaCertificateParsed::validate_profile()`.
|
|
pub fn validate_profile(&self) -> Result<(), TaCertificateProfileError> {
|
|
Ok(())
|
|
}
|
|
|
|
/// Decode a TA certificate (`parse + validate`).
|
|
pub fn decode_der(der: &[u8]) -> Result<Self, TaCertificateDecodeError> {
|
|
Ok(Self::parse_der(der)?.validate_profile()?)
|
|
}
|
|
|
|
pub fn decode_der_with_strict_name(der: &[u8]) -> Result<Self, TaCertificateDecodeError> {
|
|
let ta = Self::decode_der(der)?;
|
|
ta.rc_ca
|
|
.validate_strict_name_profile()
|
|
.map_err(TaCertificateProfileError::from)?;
|
|
Ok(ta)
|
|
}
|
|
|
|
/// Backwards-compatible helper (historical name).
|
|
pub fn from_der(der: &[u8]) -> Result<Self, TaCertificateError> {
|
|
Self::decode_der(der)
|
|
}
|
|
|
|
pub fn spki_der(&self) -> &[u8] {
|
|
&self.rc_ca.tbs.subject_public_key_info
|
|
}
|
|
|
|
/// Verify step of scheme A (`parse → validate → verify`).
|
|
pub fn verify_self_signature(&self) -> Result<(), TaCertificateVerifyError> {
|
|
let (rem, cert) = X509Certificate::from_der(&self.raw_der)
|
|
.map_err(|e| TaCertificateVerifyError::Parse(e.to_string()))?;
|
|
if !rem.is_empty() {
|
|
return Err(TaCertificateVerifyError::TrailingBytes(rem.len()));
|
|
}
|
|
cert.verify_signature(None)
|
|
.map_err(|e| TaCertificateVerifyError::InvalidSelfSignature(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Validate TA-specific semantic constraints on a parsed Resource Certificate.
|
|
///
|
|
/// Note: this does not verify the X.509 signature; it is intended for higher-level logic and
|
|
/// for unit tests that exercise individual constraint branches.
|
|
pub fn validate_rc_constraints(
|
|
rc_ca: &ResourceCertificate,
|
|
) -> Result<(), TaCertificateProfileError> {
|
|
if rc_ca.kind != ResourceCertKind::Ca {
|
|
return Err(TaCertificateProfileError::NotCa);
|
|
}
|
|
|
|
if rc_ca.tbs.extensions.certificate_policies_oid.as_deref() != Some(OID_CP_IPADDR_ASNUMBER)
|
|
{
|
|
return Err(TaCertificateProfileError::MissingOrInvalidCertificatePolicies);
|
|
}
|
|
if rc_ca.tbs.extensions.subject_key_identifier.is_none() {
|
|
return Err(TaCertificateProfileError::MissingSubjectKeyIdentifier);
|
|
}
|
|
|
|
let ip = rc_ca.tbs.extensions.ip_resources.as_ref();
|
|
let asn = rc_ca.tbs.extensions.as_resources.as_ref();
|
|
if ip.is_none() && asn.is_none() {
|
|
return Err(TaCertificateProfileError::ResourcesMissing);
|
|
}
|
|
|
|
let mut has_any_resource = false;
|
|
|
|
if let Some(ip) = ip {
|
|
if ip.has_any_inherit() {
|
|
return Err(TaCertificateProfileError::IpResourcesInherit);
|
|
}
|
|
for fam in &ip.families {
|
|
match &fam.choice {
|
|
IpAddressChoice::Inherit => {
|
|
return Err(TaCertificateProfileError::IpResourcesInherit);
|
|
}
|
|
IpAddressChoice::AddressesOrRanges(items) => {
|
|
if !items.is_empty() {
|
|
has_any_resource = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(asn) = asn {
|
|
if matches!(asn.asnum, Some(AsIdentifierChoice::Inherit))
|
|
|| matches!(asn.rdi, Some(AsIdentifierChoice::Inherit))
|
|
{
|
|
return Err(TaCertificateProfileError::AsResourcesInherit);
|
|
}
|
|
if let Some(AsIdentifierChoice::AsIdsOrRanges(items)) = asn.asnum.as_ref() {
|
|
if !items.is_empty() {
|
|
has_any_resource = true;
|
|
}
|
|
}
|
|
if let Some(AsIdentifierChoice::AsIdsOrRanges(items)) = asn.rdi.as_ref() {
|
|
if !items.is_empty() {
|
|
has_any_resource = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if !has_any_resource {
|
|
return Err(TaCertificateProfileError::ResourcesEmpty);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl TaCertificateParsed {
|
|
pub fn validate_profile(self) -> Result<TaCertificate, TaCertificateProfileError> {
|
|
let rc_ca = self.rc_parsed.validate_profile()?;
|
|
if rc_ca.kind != ResourceCertKind::Ca {
|
|
return Err(TaCertificateProfileError::NotCa);
|
|
}
|
|
rc_ca.validate_rfc6487_profile(ResourceCertificateRole::TrustAnchor)?;
|
|
|
|
if rc_ca.tbs.issuer_name != rc_ca.tbs.subject_name {
|
|
return Err(TaCertificateProfileError::NotSelfSignedIssuerSubject);
|
|
}
|
|
|
|
TaCertificate::validate_rc_constraints(&rc_ca)?;
|
|
|
|
Ok(TaCertificate {
|
|
raw_der: rc_ca.raw_der.clone(),
|
|
rc_ca,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct TrustAnchor {
|
|
pub tal: Tal,
|
|
pub ta_certificate: TaCertificate,
|
|
pub resolved_ta_uri: Option<Url>,
|
|
}
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum TrustAnchorError {
|
|
#[error("TA certificate error: {0} (RFC 8630 §2.3)")]
|
|
TaCertificate(#[from] TaCertificateDecodeError),
|
|
|
|
#[error("TA certificate self-signature error: {0} (RFC 8630 §2.3)")]
|
|
TaSelfSignature(#[from] TaCertificateVerifyError),
|
|
|
|
#[error("{0}")]
|
|
Bind(#[from] TrustAnchorBindError),
|
|
}
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum TrustAnchorBindError {
|
|
#[error("resolved TA URI not listed in TAL: {0} (RFC 8630 §2.2-§2.3)")]
|
|
ResolvedUriNotInTal(String),
|
|
|
|
#[error(
|
|
"TAL SPKI does not match TA certificate SubjectPublicKeyInfo (RFC 8630 §2.3; RFC 5280 §4.1.2.7)"
|
|
)]
|
|
TalSpkiMismatch,
|
|
}
|
|
|
|
impl TrustAnchor {
|
|
/// Bind a TAL and a downloaded TA certificate.
|
|
///
|
|
/// This does not download anything; it only validates the binding rules from RFC 8630 §2.3.
|
|
pub fn bind_der(
|
|
tal: Tal,
|
|
ta_der: &[u8],
|
|
resolved_uri: Option<&Url>,
|
|
) -> Result<Self, TrustAnchorError> {
|
|
let ta_certificate = TaCertificate::decode_der(ta_der)?;
|
|
ta_certificate.verify_self_signature()?;
|
|
Ok(Self::bind(tal, ta_certificate, resolved_uri)?)
|
|
}
|
|
|
|
pub fn bind_der_with_strict_name(
|
|
tal: Tal,
|
|
ta_der: &[u8],
|
|
resolved_uri: Option<&Url>,
|
|
) -> Result<Self, TrustAnchorError> {
|
|
let ta_certificate = TaCertificate::decode_der_with_strict_name(ta_der)?;
|
|
ta_certificate.verify_self_signature()?;
|
|
Ok(Self::bind(tal, ta_certificate, resolved_uri)?)
|
|
}
|
|
|
|
pub fn bind(
|
|
tal: Tal,
|
|
ta_certificate: TaCertificate,
|
|
resolved_uri: Option<&Url>,
|
|
) -> Result<Self, TrustAnchorBindError> {
|
|
if let Some(u) = resolved_uri {
|
|
if !tal.ta_uris.iter().any(|x| x == u) {
|
|
return Err(TrustAnchorBindError::ResolvedUriNotInTal(u.to_string()));
|
|
}
|
|
}
|
|
|
|
if tal.subject_public_key_info_der != ta_certificate.spki_der() {
|
|
return Err(TrustAnchorBindError::TalSpkiMismatch);
|
|
}
|
|
|
|
Ok(Self {
|
|
tal,
|
|
ta_certificate,
|
|
resolved_ta_uri: resolved_uri.cloned(),
|
|
})
|
|
}
|
|
}
|