use asn1_rs::{Any, Class, FromDer, Header, Tag};
use chrono::{DateTime, Utc};
use thiserror::Error;
use web_time::SystemTime;
use x509_parser::{
certificate::{BasicExtension, X509Certificate},
der_parser::{ber::parse_ber_sequence, oid},
extensions::ParsedExtension,
oid_registry::Oid,
x509::{AlgorithmIdentifier, X509Version},
};
use crate::{
crypto::{asn1::rfc3161::TstInfo, cose::CertificateTrustPolicy},
log_item,
status_tracker::StatusTracker,
validation_results::validation_codes::*,
};
pub fn check_end_entity_certificate_profile(
certificate_der: &[u8],
ctp: &CertificateTrustPolicy,
validation_log: &mut StatusTracker,
tst_info_opt: Option<&TstInfo>,
) -> Result<(), CertificateProfileError> {
check_certificate_profile(certificate_der, ctp, validation_log, tst_info_opt)?;
let (_rem, signcert) = X509Certificate::from_der(certificate_der).map_err(|_err| {
log_item!(
"",
"certificate could not be parsed",
"check_certificate_profile"
)
.validation_status(SIGNING_CREDENTIAL_INVALID)
.failure_no_throw(validation_log, CertificateProfileError::InvalidCertificate);
CertificateProfileError::InvalidCertificate
})?;
let tbscert = &signcert.tbs_certificate;
if tbscert.is_ca() {
log_item!(
"",
"expected end-entity certificate",
"check_certificate_profile"
)
.validation_status(SIGNING_CREDENTIAL_INVALID)
.failure_no_throw(validation_log, CertificateProfileError::InvalidCertificate);
return Err(CertificateProfileError::InvalidCertificate);
}
Ok(())
}
pub fn check_certificate_profile(
certificate_der: &[u8],
ctp: &CertificateTrustPolicy,
validation_log: &mut StatusTracker,
_tst_info_opt: Option<&TstInfo>,
) -> Result<(), CertificateProfileError> {
let (_rem, signcert) = X509Certificate::from_der(certificate_der).map_err(|_err| {
log_item!(
"",
"certificate could not be parsed",
"check_certificate_profile"
)
.validation_status(SIGNING_CREDENTIAL_INVALID)
.failure_no_throw(validation_log, CertificateProfileError::InvalidCertificate);
CertificateProfileError::InvalidCertificate
})?;
if signcert.version() != X509Version::V3 {
log_item!(
"",
"certificate version incorrect",
"check_certificate_profile"
)
.validation_status(SIGNING_CREDENTIAL_INVALID)
.failure_no_throw(
validation_log,
CertificateProfileError::InvalidCertificateVersion,
);
return Err(CertificateProfileError::InvalidCertificateVersion);
}
if let Some(tst_info) = _tst_info_opt {
let signing_time: DateTime<Utc> = tst_info.gen_time.clone().into();
if !signcert.validity().is_valid_at(
x509_parser::time::ASN1Time::from_timestamp(signing_time.timestamp())
.map_err(|_| CertificateProfileError::InvalidCertificate)?,
) {
log_item!("", "certificate expired", "check_certificate_profile")
.validation_status(SIGNING_CREDENTIAL_EXPIRED)
.failure_no_throw(
validation_log,
CertificateProfileError::CertificateNotValidAtTime,
);
return Err(CertificateProfileError::CertificateNotValidAtTime);
}
} else {
let Ok(now) = SystemTime::now().duration_since(web_time::UNIX_EPOCH) else {
return Err(CertificateProfileError::InternalError(
"system time invalid".to_string(),
));
};
if !signcert.validity().is_valid_at(
x509_parser::time::ASN1Time::from_timestamp(now.as_secs() as i64)
.map_err(|_| CertificateProfileError::InvalidCertificate)?,
) {
log_item!("", "certificate expired", "check_certificate_profile")
.validation_status(SIGNING_CREDENTIAL_EXPIRED)
.failure_no_throw(
validation_log,
CertificateProfileError::CertificateNotValidAtTime,
);
return Err(CertificateProfileError::CertificateNotValidAtTime);
}
}
let cert_alg = &signcert.signature_algorithm.algorithm;
if !(*cert_alg == SHA256_WITH_RSAENCRYPTION_OID
|| *cert_alg == SHA384_WITH_RSAENCRYPTION_OID
|| *cert_alg == SHA512_WITH_RSAENCRYPTION_OID
|| *cert_alg == ECDSA_WITH_SHA256_OID
|| *cert_alg == ECDSA_WITH_SHA384_OID
|| *cert_alg == ECDSA_WITH_SHA512_OID
|| *cert_alg == RSASSA_PSS_OID
|| *cert_alg == ED25519_OID)
{
log_item!(
"",
"certificate algorithm not supported",
"check_certificate_profile"
)
.validation_status(SIGNING_CREDENTIAL_INVALID)
.failure_no_throw(
validation_log,
CertificateProfileError::UnsupportedAlgorithm,
);
return Err(CertificateProfileError::UnsupportedAlgorithm);
}
if *cert_alg == RSASSA_PSS_OID {
if let Some(parameters) = &signcert.signature_algorithm.parameters {
let seq = parameters
.as_sequence()
.map_err(|_err| CertificateProfileError::InvalidCertificate)?;
let (_i, (ha_alg, mgf_ai)) = seq
.parse(|i| {
let (i, h) = <Header as asn1_rs::FromDer>::from_der(i)?;
if h.class() != Class::ContextSpecific || h.tag() != Tag(0) {
return Err(nom::Err::Error(asn1_rs::Error::BerValueError));
}
let (i, ha_alg) = AlgorithmIdentifier::from_der(i)
.map_err(|_| nom::Err::Error(asn1_rs::Error::BerValueError))?;
let (i, h) = <Header as asn1_rs::FromDer>::from_der(i)?;
if h.class() != Class::ContextSpecific || h.tag() != Tag(1) {
return Err(nom::Err::Error(asn1_rs::Error::BerValueError));
}
let (i, mgf_ai) = AlgorithmIdentifier::from_der(i)
.map_err(|_| nom::Err::Error(asn1_rs::Error::BerValueError))?;
Ok((i, (ha_alg, mgf_ai)))
})
.map_err(|_| CertificateProfileError::InvalidCertificate)?;
let mgf_ai_parameters = mgf_ai
.parameters
.ok_or(CertificateProfileError::InvalidCertificate)?;
let mgf_ai_parameters = mgf_ai_parameters
.as_sequence()
.map_err(|_| CertificateProfileError::InvalidCertificate)?;
let (_i, mgf_ai_params_algorithm) =
<Any as asn1_rs::FromDer>::from_der(&mgf_ai_parameters.content)
.map_err(|_| CertificateProfileError::InvalidCertificate)?;
let mgf_ai_params_algorithm = mgf_ai_params_algorithm
.as_oid()
.map_err(|_| CertificateProfileError::InvalidCertificate)?;
if ha_alg.algorithm.to_id_string() != mgf_ai_params_algorithm.to_id_string() {
log_item!(
"",
"certificate algorithm error",
"check_certificate_profile"
)
.validation_status(SIGNING_CREDENTIAL_INVALID)
.failure_no_throw(validation_log, CertificateProfileError::InvalidCertificate);
return Err(CertificateProfileError::InvalidCertificate);
}
if !(ha_alg.algorithm == SHA256_OID
|| ha_alg.algorithm == SHA384_OID
|| ha_alg.algorithm == SHA512_OID)
{
log_item!(
"",
"certificate hash algorithm not supported",
"check_certificate_profile"
)
.validation_status(SIGNING_CREDENTIAL_INVALID)
.failure_no_throw(validation_log, CertificateProfileError::InvalidCertificate);
return Err(CertificateProfileError::InvalidCertificate);
}
} else {
log_item!(
"",
"certificate missing algorithm parameters",
"check_certificate_profile"
)
.validation_status(SIGNING_CREDENTIAL_INVALID)
.failure_no_throw(validation_log, CertificateProfileError::InvalidCertificate);
return Err(CertificateProfileError::InvalidCertificate);
}
}
let pk = signcert.public_key();
let skpi_alg = &pk.algorithm;
if skpi_alg.algorithm == EC_PUBLICKEY_OID {
if let Some(parameters) = &skpi_alg.parameters {
let named_curve_oid = parameters
.as_oid()
.map_err(|_err| CertificateProfileError::InvalidCertificate)?;
if !(named_curve_oid == PRIME256V1_OID
|| named_curve_oid == SECP384R1_OID
|| named_curve_oid == SECP521R1_OID)
{
log_item!(
"",
"certificate unsupported EC curve",
"check_certificate_profile"
)
.validation_status(SIGNING_CREDENTIAL_INVALID)
.failure_no_throw(validation_log, CertificateProfileError::InvalidCertificate);
return Err(CertificateProfileError::InvalidCertificate);
}
} else {
return Err(CertificateProfileError::InvalidCertificate);
}
}
if skpi_alg.algorithm == RSA_OID || skpi_alg.algorithm == RSASSA_PSS_OID {
let (_, skpi_ber) = parse_ber_sequence(&pk.subject_public_key.data)
.map_err(|_err| CertificateProfileError::InvalidCertificate)?;
let seq = skpi_ber
.as_sequence()
.map_err(|_err| CertificateProfileError::InvalidCertificate)?;
if seq.len() < 2 {
return Err(CertificateProfileError::InvalidCertificate);
}
let modulus = seq[0]
.as_bigint()
.map_err(|_| CertificateProfileError::InvalidCertificate)?;
if modulus.bits() < 2048 {
log_item!(
"",
"certificate key length too short",
"check_certificate_profile"
)
.validation_status(SIGNING_CREDENTIAL_INVALID)
.failure_no_throw(validation_log, CertificateProfileError::InvalidCertificate);
return Err(CertificateProfileError::InvalidCertificate);
}
}
let tbscert = &signcert.tbs_certificate;
if tbscert.is_ca() && tbscert.issuer() == tbscert.subject() {
log_item!(
"",
"certificate issuer and subject cannot be the same (self-signed disallowed)",
"check_certificate_profile"
)
.validation_status(SIGNING_CREDENTIAL_INVALID)
.failure_no_throw(
validation_log,
CertificateProfileError::SelfSignedCertificate,
);
return Err(CertificateProfileError::SelfSignedCertificate);
}
if signcert.issuer_uid.is_some() || signcert.subject_uid.is_some() {
log_item!(
"",
"certificate issuer/subject unique ids are not allowed",
"check_certificate_profile"
)
.validation_status(SIGNING_CREDENTIAL_INVALID)
.failure_no_throw(validation_log, CertificateProfileError::InvalidCertificate);
return Err(CertificateProfileError::InvalidCertificate);
}
let mut aki_good = false;
let mut ski_good = false;
let mut key_usage_good = false;
let mut handled_all_critical = true;
let extended_key_usage_good = match tbscert
.extended_key_usage()
.map_err(|_| CertificateProfileError::InvalidCertificate)?
{
Some(BasicExtension { value: eku, .. }) => {
if eku.any {
log_item!(
"",
"certificate 'any' EKU not allowed",
"check_certificate_profile"
)
.validation_status(SIGNING_CREDENTIAL_INVALID)
.failure_no_throw(validation_log, CertificateProfileError::InvalidCertificate);
return Err(CertificateProfileError::InvalidCertificate);
}
if ctp.has_allowed_eku(eku).is_none() {
log_item!(
"",
"certificate missing required EKU",
"check_certificate_profile"
)
.validation_status(SIGNING_CREDENTIAL_INVALID)
.failure_no_throw(validation_log, CertificateProfileError::InvalidCertificate);
return Err(CertificateProfileError::InvalidCertificate);
}
if (eku.ocsp_signing && eku.time_stamping)
|| ((eku.ocsp_signing ^ eku.time_stamping)
&& (eku.client_auth
| eku.code_signing
| eku.email_protection
| eku.server_auth
| !eku.other.is_empty()))
{
log_item!(
"",
"certificate invalid set of EKUs",
"check_certificate_profile"
)
.validation_status(SIGNING_CREDENTIAL_INVALID)
.failure_no_throw(validation_log, CertificateProfileError::InvalidCertificate);
return Err(CertificateProfileError::InvalidCertificate);
}
true
}
None => tbscert.is_ca(), };
for e in signcert.extensions() {
match e.parsed_extension() {
ParsedExtension::AuthorityKeyIdentifier(_aki) => {
aki_good = true;
}
ParsedExtension::SubjectKeyIdentifier(_spki) => {
ski_good = true;
}
ParsedExtension::KeyUsage(ku) => {
if ku.digital_signature() {
if ku.key_cert_sign() && !tbscert.is_ca() {
log_item!(
"",
"certificate missing digitalSignature EKU",
"check_certificate_profile"
)
.validation_status(SIGNING_CREDENTIAL_INVALID)
.failure_no_throw(
validation_log,
CertificateProfileError::InvalidCertificate,
);
return Err(CertificateProfileError::InvalidCertificate);
}
key_usage_good = true;
}
if ku.key_cert_sign() || ku.non_repudiation() {
key_usage_good = true;
}
}
ParsedExtension::CertificatePolicies(_) => (),
ParsedExtension::PolicyMappings(_) => (),
ParsedExtension::SubjectAlternativeName(_) => (),
ParsedExtension::BasicConstraints(_) => (),
ParsedExtension::NameConstraints(_) => (),
ParsedExtension::PolicyConstraints(_) => (),
ParsedExtension::ExtendedKeyUsage(_) => (),
ParsedExtension::CRLDistributionPoints(_) => (),
ParsedExtension::InhibitAnyPolicy(_) => (),
ParsedExtension::AuthorityInfoAccess(_) => (),
ParsedExtension::NSCertType(_) => (),
ParsedExtension::CRLNumber(_) => (),
ParsedExtension::ReasonCode(_) => (),
ParsedExtension::InvalidityDate(_) => (),
ParsedExtension::Unparsed => {
if e.critical {
handled_all_critical = false;
}
}
_ => {
if e.critical {
handled_all_critical = false;
}
}
}
}
ski_good = if tbscert.is_ca() { ski_good } else { true };
if aki_good && ski_good && key_usage_good && extended_key_usage_good && handled_all_critical {
Ok(())
} else {
log_item!(
"",
"certificate params incorrect",
"check_certificate_profile"
)
.validation_status(SIGNING_CREDENTIAL_INVALID)
.failure_no_throw(validation_log, CertificateProfileError::InvalidCertificate);
Err(CertificateProfileError::InvalidCertificate)
}
}
#[derive(Debug, Eq, Error, PartialEq)]
#[non_exhaustive]
pub enum CertificateProfileError {
#[error("the certificate is invalid")]
InvalidCertificate,
#[error("the certificate must be a `v3` certificate")]
InvalidCertificateVersion,
#[error("the certificate was not valid at time of signing")]
CertificateNotValidAtTime,
#[error("the certificate was signed with an unsupported algorithm")]
UnsupportedAlgorithm,
#[error("the certificate contains an invalid extended key usage (EKU) value")]
InvalidEku,
#[error("the certificate was self-signed")]
SelfSignedCertificate,
#[error("internal error ({0})")]
InternalError(String),
}
const RSA_OID: Oid<'static> = oid!(1.2.840 .113549 .1 .1 .1);
const EC_PUBLICKEY_OID: Oid<'static> = oid!(1.2.840 .10045 .2 .1);
const RSASSA_PSS_OID: Oid<'static> = oid!(1.2.840 .113549 .1 .1 .10);
const ECDSA_WITH_SHA256_OID: Oid<'static> = oid!(1.2.840 .10045 .4 .3 .2);
const ECDSA_WITH_SHA384_OID: Oid<'static> = oid!(1.2.840 .10045 .4 .3 .3);
const ECDSA_WITH_SHA512_OID: Oid<'static> = oid!(1.2.840 .10045 .4 .3 .4);
const SHA256_WITH_RSAENCRYPTION_OID: Oid<'static> = oid!(1.2.840 .113549 .1 .1 .11);
const SHA384_WITH_RSAENCRYPTION_OID: Oid<'static> = oid!(1.2.840 .113549 .1 .1 .12);
const SHA512_WITH_RSAENCRYPTION_OID: Oid<'static> = oid!(1.2.840 .113549 .1 .1 .13);
const ED25519_OID: Oid<'static> = oid!(1.3.101 .112);
const SHA256_OID: Oid<'static> = oid!(2.16.840 .1 .101 .3 .4 .2 .1);
const SHA384_OID: Oid<'static> = oid!(2.16.840 .1 .101 .3 .4 .2 .2);
const SHA512_OID: Oid<'static> = oid!(2.16.840 .1 .101 .3 .4 .2 .3);
const SECP521R1_OID: Oid<'static> = oid!(1.3.132 .0 .35);
const SECP384R1_OID: Oid<'static> = oid!(1.3.132 .0 .34);
const PRIME256V1_OID: Oid<'static> = oid!(1.2.840 .10045 .3 .1 .7);
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
#![allow(clippy::panic)]
#![allow(clippy::unwrap_used)]
#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
use wasm_bindgen_test::wasm_bindgen_test;
use x509_parser::pem::Pem;
use crate::{
crypto::cose::{check_end_entity_certificate_profile, CertificateTrustPolicy},
status_tracker::StatusTracker,
validation_results::validation_codes::SIGNING_CREDENTIAL_EXPIRED,
};
#[test]
#[cfg_attr(
all(target_arch = "wasm32", not(target_os = "wasi")),
wasm_bindgen_test
)]
fn expired_cert() {
let ctp = CertificateTrustPolicy::default();
let mut validation_log = StatusTracker::default();
let cert_der = x509_der_from_pem(include_bytes!(
"../../../tests/fixtures/crypto/cose/rsa-pss256_key-expired.pub"
));
assert!(
check_end_entity_certificate_profile(&cert_der, &ctp, &mut validation_log, None)
.is_err()
);
assert!(!validation_log.logged_items().is_empty());
assert_eq!(
validation_log.logged_items()[0].validation_status,
Some(SIGNING_CREDENTIAL_EXPIRED.into())
);
}
#[test]
#[cfg_attr(
all(target_arch = "wasm32", not(target_os = "wasi")),
wasm_bindgen_test
)]
fn cert_algorithms() {
let ctp = CertificateTrustPolicy::default();
let mut validation_log = StatusTracker::default();
let es256_cert = x509_der_from_pem(include_bytes!(
"../../../tests/fixtures/crypto/raw_signature/es256.pub"
));
let es384_cert = x509_der_from_pem(include_bytes!(
"../../../tests/fixtures/crypto/raw_signature/es384.pub"
));
let es512_cert = x509_der_from_pem(include_bytes!(
"../../../tests/fixtures/crypto/raw_signature/es512.pub"
));
let ps256_cert = x509_der_from_pem(include_bytes!(
"../../../tests/fixtures/crypto/raw_signature/ps256.pub"
));
check_end_entity_certificate_profile(&es256_cert, &ctp, &mut validation_log, None).unwrap();
check_end_entity_certificate_profile(&es384_cert, &ctp, &mut validation_log, None).unwrap();
check_end_entity_certificate_profile(&es512_cert, &ctp, &mut validation_log, None).unwrap();
check_end_entity_certificate_profile(&ps256_cert, &ctp, &mut validation_log, None).unwrap();
}
fn x509_der_from_pem(cert_pem: &[u8]) -> Vec<u8> {
let mut pems = Pem::iter_from_buffer(cert_pem);
let pem = pems.next().unwrap().unwrap();
pem.contents
}
}