use asn1_rs::{Any, Class, FromDer, Header, Tag};
use c2pa_status_tracker::{log_item, validation_codes::*, StatusTracker};
use chrono::{DateTime, Utc};
use thiserror::Error;
use web_time::SystemTime;
use x509_certificate::asn1time::GeneralizedTime;
use x509_parser::{
certificate::{BasicExtension, X509Certificate},
der_parser::{ber::parse_ber_sequence, oid},
extensions::ParsedExtension,
oid_registry::Oid,
x509::{AlgorithmIdentifier, X509Version},
};
use crate::{asn1::rfc3161::TstInfo, cose::CertificateTrustPolicy};
pub fn check_certificate_profile(
certificate_der: &[u8],
ctp: &CertificateTrustPolicy,
validation_log: &mut impl StatusTracker,
_tst_info_opt: Option<&TstInfo>,
) -> Result<(), CertificateProfileError> {
let (_rem, signcert) = X509Certificate::from_der(certificate_der).map_err(|_err| {
log_item!(
"Cose_Sign1",
"certificate could not be parsed",
"check_cert_alg"
)
.validation_status(SIGNING_CREDENTIAL_INVALID)
.failure_no_throw(validation_log, CertificateProfileError::InvalidCertificate);
CertificateProfileError::InvalidCertificate
})?;
if signcert.version() != X509Version::V3 {
log_item!(
"Cose_Sign1",
"certificate version incorrect",
"check_cert_alg"
)
.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 = generalized_time_to_datetime(tst_info.gen_time.clone());
if !signcert.validity().is_valid_at(
x509_parser::time::ASN1Time::from_timestamp(signing_time.timestamp())
.map_err(|_| CertificateProfileError::InvalidCertificate)?,
) {
log_item!("Cose_Sign1", "certificate expired", "check_cert_alg")
.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!("Cose_Sign1", "certificate expired", "check_cert_alg")
.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!(
"Cose_Sign1",
"certificate algorithm not supported",
"check_cert_alg"
)
.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!(
"Cose_Sign1",
"certificate algorithm error",
"check_cert_alg"
)
.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!(
"Cose_Sign1",
"certificate hash algorithm not supported",
"check_cert_alg"
)
.validation_status(SIGNING_CREDENTIAL_INVALID)
.failure_no_throw(validation_log, CertificateProfileError::InvalidCertificate);
return Err(CertificateProfileError::InvalidCertificate);
}
} else {
log_item!(
"Cose_Sign1",
"certificate missing algorithm parameters",
"check_cert_alg"
)
.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!(
"Cose_Sign1",
"certificate unsupported EC curve",
"check_cert_alg"
)
.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!(
"Cose_Sign1",
"certificate key length too short",
"check_cert_alg"
)
.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!(
"Cose_Sign1",
"certificate issuer and subject cannot be the same (self-signed disallowed)",
"check_cert_alg"
)
.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!(
"Cose_Sign1",
"certificate issuer/subject unique ids are not allowed",
"check_cert_alg"
)
.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 extended_key_usage_good = match tbscert
.extended_key_usage()
.map_err(|_| CertificateProfileError::InvalidCertificate)?
{
Some(BasicExtension { value: eku, .. }) => {
if eku.any {
log_item!(
"Cose_Sign1",
"certificate 'any' EKU not allowed",
"check_cert_alg"
)
.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!(
"Cose_Sign1",
"certificate missing required EKU",
"check_cert_alg"
)
.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!(
"Cose_Sign1",
"certificate invalid set of EKUs",
"check_cert_alg"
)
.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!(
"Cose_Sign1",
"certificate missing digitalSignature EKU",
"check_cert_alg"
)
.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(_) => (),
_ => (),
}
}
ski_good = if tbscert.is_ca() { ski_good } else { true };
if aki_good && ski_good && key_usage_good && extended_key_usage_good {
Ok(())
} else {
log_item!(
"Cose_Sign1",
"certificate params incorrect",
"check_cert_alg"
)
.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),
}
fn generalized_time_to_datetime(gt: GeneralizedTime) -> DateTime<Utc> {
gt.into()
}
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);