use chrono::Utc;
use ed25519_dalek::{Signer, SigningKey, VerifyingKey};
use pkcs8::{EncodePrivateKey, LineEnding};
use rasn::types::{Any, BitString, Integer, ObjectIdentifier, OctetString, PrintableString, SetOf};
use rasn_pkix::{
AlgorithmIdentifier, AttributeTypeAndValue, AuthorityKeyIdentifier, BasicConstraints,
Certificate, Extension, Extensions, GeneralName, Name, RelativeDistinguishedName,
SubjectPublicKeyInfo, TbsCertificate, Time, Validity, Version,
};
use sha1::{Digest, Sha1};
use crate::{Error, Result};
const ED25519_OID: &[u64] = &[1, 3, 101, 112];
const CN_OID: &[u64] = &[2, 5, 4, 3];
const ORG_OID: &[u64] = &[2, 5, 4, 10];
const KEY_USAGE_OID: &[u64] = &[2, 5, 29, 15];
const BASIC_CONSTRAINTS_OID: &[u64] = &[2, 5, 29, 19];
const SUBJECT_KEY_ID_OID: &[u64] = &[2, 5, 29, 14];
const AUTH_KEY_ID_OID: &[u64] = &[2, 5, 29, 35];
const SUBJECT_ALT_NAME_OID: &[u64] = &[2, 5, 29, 17];
const EXT_KEY_USAGE_OID: &[u64] = &[2, 5, 29, 37];
const EKU_EMAIL_PROTECTION_OID: &[u64] = &[1, 3, 6, 1, 5, 5, 7, 3, 4];
const EKU_ANY_OID: &[u64] = &[1, 3, 6, 1, 5, 5, 7, 3, 0];
fn oid(components: &[u64]) -> Result<ObjectIdentifier> {
ObjectIdentifier::new(components.iter().map(|&c| c as u32).collect::<Vec<u32>>())
.ok_or_else(|| Error::OtherError(Box::new(std::io::Error::other("invalid OID"))))
}
fn fill_random(buf: &mut [u8]) -> Result<()> {
getrandom::fill(buf).map_err(|e| Error::OtherError(Box::new(e)))
}
fn generate_ed25519_keypair() -> Result<SigningKey> {
let mut seed = [0u8; 32];
fill_random(&mut seed)?;
Ok(SigningKey::from_bytes(&seed))
}
fn dn_attr(oid_components: &[u64], value: &str) -> Result<AttributeTypeAndValue> {
let ps =
PrintableString::try_from(value.to_string()).map_err(|e| Error::OtherError(Box::new(e)))?;
let value_der = rasn::der::encode(&ps).map_err(|e| Error::OtherError(Box::new(e)))?;
Ok(AttributeTypeAndValue {
r#type: oid(oid_components)?,
value: Any::new(value_der),
})
}
fn rdn_single(oid_components: &[u64], value: &str) -> Result<RelativeDistinguishedName> {
let mut set = SetOf::new();
set.insert(dn_attr(oid_components, value)?);
Ok(RelativeDistinguishedName::from(set))
}
fn build_name(cn: &str, org: Option<&str>) -> Result<Name> {
let mut rdns = vec![rdn_single(CN_OID, cn)?];
if let Some(o) = org {
rdns.push(rdn_single(ORG_OID, o)?);
}
Ok(Name::RdnSequence(rdns))
}
fn ed25519_algorithm_identifier() -> Result<AlgorithmIdentifier> {
Ok(AlgorithmIdentifier {
algorithm: oid(ED25519_OID)?,
parameters: None,
})
}
fn subject_public_key_info(verifying_key: &VerifyingKey) -> Result<SubjectPublicKeyInfo> {
Ok(SubjectPublicKeyInfo {
algorithm: ed25519_algorithm_identifier()?,
subject_public_key: BitString::from_slice(verifying_key.as_bytes()),
})
}
fn default_validity() -> Validity {
let now = Utc::now();
let not_before = now - chrono::Duration::days(1);
let not_after = now + chrono::Duration::days(365);
Validity {
not_before: Time::Utc(not_before),
not_after: Time::Utc(not_after),
}
}
fn subject_key_identifier_ext(public_key_der: &[u8]) -> Result<Extension> {
let hash = Sha1::digest(public_key_der);
Ok(Extension {
extn_id: oid(SUBJECT_KEY_ID_OID)?,
critical: false,
extn_value: OctetString::from(hash.to_vec()),
})
}
fn authority_key_identifier_ext(key_id: Option<Vec<u8>>) -> Result<Extension> {
let aki = AuthorityKeyIdentifier {
key_identifier: key_id.map(OctetString::from),
authority_cert_issuer: None,
authority_cert_serial_number: None,
};
Ok(Extension {
extn_id: oid(AUTH_KEY_ID_OID)?,
critical: false,
extn_value: rasn::der::encode(&aki)
.map_err(|e| Error::OtherError(Box::new(e)))?
.into(),
})
}
fn serial_number() -> Result<rasn_pkix::CertificateSerialNumber> {
let mut bytes = [0u8; 7];
fill_random(&mut bytes)?;
let n = u64::from_be_bytes([
0, bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6],
]);
Ok(Integer::from((n as i64).saturating_add(1)))
}
fn build_ca_cert(cn: &str, org: &str, keypair: &SigningKey) -> Result<Vec<u8>> {
let verifying_key = keypair.verifying_key();
let spki = subject_public_key_info(&verifying_key)?;
let spki_der = rasn::der::encode(&spki).map_err(|e| Error::OtherError(Box::new(e)))?;
let subject = build_name(cn, Some(org))?;
let exts = vec![
Extension {
extn_id: oid(BASIC_CONSTRAINTS_OID)?,
critical: true,
extn_value: rasn::der::encode(&BasicConstraints {
ca: true,
path_len_constraint: None,
})
.map_err(|e| Error::OtherError(Box::new(e)))?
.into(),
},
Extension {
extn_id: oid(KEY_USAGE_OID)?,
critical: true,
extn_value: rasn::der::encode(&BitString::from_slice(&[0x86]))
.map_err(|e| Error::OtherError(Box::new(e)))?
.into(),
},
subject_key_identifier_ext(&spki_der)?,
];
let tbs = TbsCertificate {
version: Version::V3,
serial_number: serial_number()?,
signature: ed25519_algorithm_identifier()?,
issuer: subject.clone(),
validity: default_validity(),
subject,
subject_public_key_info: spki,
issuer_unique_id: None,
subject_unique_id: None,
extensions: Some(Extensions::from(exts)),
};
let tbs_der = rasn::der::encode(&tbs).map_err(|e| Error::OtherError(Box::new(e)))?;
let sig = keypair.sign(&tbs_der);
let cert = Certificate {
tbs_certificate: tbs,
signature_algorithm: ed25519_algorithm_identifier()?,
signature_value: BitString::from_slice(sig.to_bytes().as_slice()),
};
rasn::der::encode(&cert).map_err(|e| Error::OtherError(Box::new(e)))
}
struct CaSigningContext<'a> {
subject: &'a Name,
key: &'a SigningKey,
ski: Option<Vec<u8>>,
}
const EXT_TAG_BASIC_CONSTRAINTS: &str = "basic_constraints";
const EXT_TAG_KEY_USAGE: &str = "key_usage";
const EXT_TAG_EXT_KEY_USAGE: &str = "ext_key_usage";
const EXT_TAG_SUBJECT_KEY_ID: &str = "subject_key_identifier";
const EXT_TAG_AUTHORITY_KEY_ID: &str = "authority_key_identifier";
const EXT_TAG_SUBJECT_ALT_NAME: &str = "subject_alt_name";
fn build_ee_cert(
ee_cn: &str,
ee_org: &str,
san_dns: &str,
ee_keypair: &SigningKey,
ca: &CaSigningContext<'_>,
skip_extensions: &[&str],
) -> Result<Vec<u8>> {
let verifying_key = ee_keypair.verifying_key();
let spki = subject_public_key_info(&verifying_key)?;
let spki_der = rasn::der::encode(&spki).map_err(|e| Error::OtherError(Box::new(e)))?;
let subject = build_name(ee_cn, Some(ee_org))?;
let eku_list: rasn_pkix::ExtKeyUsageSyntax =
vec![oid(EKU_EMAIL_PROTECTION_OID)?, oid(EKU_ANY_OID)?];
let eku_value = rasn::der::encode(&eku_list).map_err(|e| Error::OtherError(Box::new(e)))?;
let mut ext_pairs: Vec<(&str, Extension)> = vec![
(
EXT_TAG_BASIC_CONSTRAINTS,
Extension {
extn_id: oid(BASIC_CONSTRAINTS_OID)?,
critical: true,
extn_value: OctetString::from([0x30, 0x03, 0x01, 0x01, 0x00]),
},
),
(
EXT_TAG_KEY_USAGE,
Extension {
extn_id: oid(KEY_USAGE_OID)?,
critical: true,
extn_value: rasn::der::encode(&BitString::from_slice(&[0x80]))
.map_err(|e| Error::OtherError(Box::new(e)))?
.into(),
},
),
(
EXT_TAG_EXT_KEY_USAGE,
Extension {
extn_id: oid(EXT_KEY_USAGE_OID)?,
critical: false,
extn_value: eku_value.clone().into(),
},
),
(
EXT_TAG_SUBJECT_KEY_ID,
subject_key_identifier_ext(&spki_der)?,
),
(
EXT_TAG_AUTHORITY_KEY_ID,
authority_key_identifier_ext(ca.ski.clone())?,
),
(
EXT_TAG_SUBJECT_ALT_NAME,
Extension {
extn_id: oid(SUBJECT_ALT_NAME_OID)?,
critical: false,
extn_value: rasn::der::encode(&rasn_pkix::GeneralNames::from(vec![
GeneralName::DnsName(
rasn::types::Ia5String::try_from(san_dns.to_string())
.map_err(|e| Error::OtherError(Box::new(e)))?,
),
]))
.map_err(|e| Error::OtherError(Box::new(e)))?
.into(),
},
),
];
ext_pairs.retain(|(tag, _)| !skip_extensions.contains(tag));
let exts: Vec<Extension> = ext_pairs.into_iter().map(|(_, ext)| ext).collect();
let tbs = TbsCertificate {
version: Version::V3,
serial_number: serial_number()?,
signature: ed25519_algorithm_identifier()?,
issuer: ca.subject.clone(),
validity: default_validity(),
subject,
subject_public_key_info: spki,
issuer_unique_id: None,
subject_unique_id: None,
extensions: Some(Extensions::from(exts)),
};
let tbs_der = rasn::der::encode(&tbs).map_err(|e| Error::OtherError(Box::new(e)))?;
let sig = ca.key.sign(&tbs_der);
let cert = Certificate {
tbs_certificate: tbs,
signature_algorithm: ed25519_algorithm_identifier()?,
signature_value: BitString::from_slice(sig.to_bytes().as_slice()),
};
rasn::der::encode(&cert).map_err(|e| Error::OtherError(Box::new(e)))
}
pub struct EphemeralCertChain {
pub ee_der: Vec<u8>,
pub ca_der: Vec<u8>,
pub ee_private_key_pem: String,
}
pub fn generate_ephemeral_chain(ee_cert_name: &str) -> Result<EphemeralCertChain> {
let ca_keypair = generate_ed25519_keypair()?;
let ee_keypair = generate_ed25519_keypair()?;
let ca_der = build_ca_cert(
"c2pa-ephemeral-ca.local",
"Self-signed ephemeral CA (Content Authenticity SDK)",
&ca_keypair,
)?;
let ca_cert: Certificate =
rasn::der::decode(&ca_der).map_err(|e| Error::OtherError(Box::new(e)))?;
let ca_ski = Some(
Sha1::digest(
rasn::der::encode(&ca_cert.tbs_certificate.subject_public_key_info)
.map_err(|e| Error::OtherError(Box::new(e)))?
.as_slice(),
)
.to_vec(),
);
let ca_ctx = CaSigningContext {
subject: &ca_cert.tbs_certificate.subject,
key: &ca_keypair,
ski: ca_ski,
};
let ee_der = build_ee_cert(
ee_cert_name,
"Self-signed ephemeral certificate (Content Authenticity SDK) -- LOCAL USE ONLY",
ee_cert_name,
&ee_keypair,
&ca_ctx,
&[],
)?;
let ee_private_key_pem = ee_keypair
.to_pkcs8_pem(LineEnding::LF)
.map_err(|e| Error::OtherError(Box::new(e)))?
.to_string();
Ok(EphemeralCertChain {
ee_der,
ca_der,
ee_private_key_pem,
})
}
#[cfg(all(test, any(target_os = "linux", target_os = "macos")))]
pub(crate) fn generate_ephemeral_chain_with_ee_skip(
ee_cert_name: &str,
skip_extensions: &[&str],
) -> Result<EphemeralCertChain> {
let ca_keypair = generate_ed25519_keypair()?;
let ee_keypair = generate_ed25519_keypair()?;
let ca_der = build_ca_cert(
"c2pa-ephemeral-ca.local",
"Self-signed ephemeral CA (Content Authenticity SDK)",
&ca_keypair,
)?;
let ca_cert: Certificate =
rasn::der::decode(&ca_der).map_err(|e| Error::OtherError(Box::new(e)))?;
let ca_ski = Some(
Sha1::digest(
rasn::der::encode(&ca_cert.tbs_certificate.subject_public_key_info)
.map_err(|e| Error::OtherError(Box::new(e)))?
.as_slice(),
)
.to_vec(),
);
let ca_ctx = CaSigningContext {
subject: &ca_cert.tbs_certificate.subject,
key: &ca_keypair,
ski: ca_ski,
};
let ee_der = build_ee_cert(
ee_cert_name,
"Self-signed ephemeral certificate (Content Authenticity SDK) -- LOCAL USE ONLY",
ee_cert_name,
&ee_keypair,
&ca_ctx,
skip_extensions,
)?;
let ee_private_key_pem = ee_keypair
.to_pkcs8_pem(LineEnding::LF)
.map_err(|e| Error::OtherError(Box::new(e)))?
.to_string();
Ok(EphemeralCertChain {
ee_der,
ca_der,
ee_private_key_pem,
})
}
pub fn der_to_pem(der: &[u8]) -> String {
pem::Pem::new("CERTIFICATE", der.to_vec()).to_string()
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
#![allow(clippy::panic)]
#[cfg(any(target_os = "linux", target_os = "macos"))]
use std::process::Command;
use rasn_pkix::BasicConstraints;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use super::{der_to_pem, generate_ephemeral_chain};
#[test]
fn test_basic_constraints_encoding_compare() {
let bc = BasicConstraints {
ca: false,
path_len_constraint: None,
};
let rasn_der = rasn::der::encode(&bc).expect("rasn encode BasicConstraints");
let minimal_der: &[u8] = &[0x30, 0x03, 0x01, 0x01, 0x00];
assert_eq!(
rasn_der.as_slice(),
&[0x30, 0x00],
"rasn emits empty SEQUENCE for ca=false"
);
assert_eq!(
minimal_der,
&[0x30, 0x03, 0x01, 0x01, 0x00],
"minimal has explicit BOOLEAN FALSE"
);
let decoded_minimal: BasicConstraints =
rasn::der::decode(minimal_der).expect("decode minimal");
assert!(!decoded_minimal.ca);
assert!(decoded_minimal.path_len_constraint.is_none());
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn openssl_available() -> bool {
Command::new("openssl")
.arg("version")
.output()
.is_ok_and(|o| o.status.success())
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn is_openssl_verify_skip(stderr: &str) -> bool {
let s = stderr;
s.contains("invalid certificate")
|| s.contains("1100009E")
|| s.contains("wrong tag")
|| s.contains("nested asn1")
|| s.contains("header too long")
|| s.contains("bad object header")
|| s.contains("unsupported algorithm")
|| s.contains("unable to get certs public key")
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn test_ephemeral_cert_openssl_verify() {
if !openssl_available() {
eprintln!("openssl not available, skipping test_ephemeral_cert_openssl_verify");
return;
}
let chain = generate_ephemeral_chain("test-ephemeral.example.com")
.expect("generate_ephemeral_chain");
let temp_dir = tempfile::tempdir().expect("temp dir");
let ca_pem_path = temp_dir.path().join("ca.pem");
let ee_pem_path = temp_dir.path().join("ee.pem");
std::fs::write(&ca_pem_path, der_to_pem(&chain.ca_der)).expect("write ca.pem");
std::fs::write(&ee_pem_path, der_to_pem(&chain.ee_der)).expect("write ee.pem");
for (label, path) in [("CA", &ca_pem_path), ("EE", &ee_pem_path)] {
let out = Command::new("openssl")
.args(["x509", "-in"])
.arg(path)
.args(["-noout", "-subject"])
.output()
.expect("run openssl x509");
assert!(
out.status.success(),
"openssl must parse {} cert (exit {:?}). stderr: {}",
label,
out.status.code(),
String::from_utf8_lossy(&out.stderr)
);
}
let verify_out = Command::new("openssl")
.args(["verify", "-purpose", "any", "-CAfile"])
.arg(&ca_pem_path)
.arg(&ee_pem_path)
.output()
.expect("run openssl verify");
let stderr = String::from_utf8_lossy(&verify_out.stderr);
if !verify_out.status.success() {
if is_openssl_verify_skip(&stderr) {
eprintln!("openssl verify skipped (known platform quirk): {}", stderr);
} else {
panic!("openssl verify failed. stderr: {}", stderr);
}
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn test_openssl_which_extension_fails() {
if !openssl_available() {
eprintln!("openssl not available, skipping test_openssl_which_extension_fails");
return;
}
use super::{
generate_ephemeral_chain, generate_ephemeral_chain_with_ee_skip,
EXT_TAG_AUTHORITY_KEY_ID, EXT_TAG_BASIC_CONSTRAINTS, EXT_TAG_EXT_KEY_USAGE,
EXT_TAG_KEY_USAGE, EXT_TAG_SUBJECT_ALT_NAME, EXT_TAG_SUBJECT_KEY_ID,
};
let extension_tags = [
EXT_TAG_BASIC_CONSTRAINTS,
EXT_TAG_KEY_USAGE,
EXT_TAG_EXT_KEY_USAGE,
EXT_TAG_SUBJECT_KEY_ID,
EXT_TAG_AUTHORITY_KEY_ID,
EXT_TAG_SUBJECT_ALT_NAME,
];
let full_chain = generate_ephemeral_chain("test-diagnostic.example.com")
.expect("generate_ephemeral_chain");
let temp_dir = tempfile::tempdir().expect("temp dir");
let ca_pem = temp_dir.path().join("ca.pem");
let ee_pem = temp_dir.path().join("ee.pem");
std::fs::write(&ca_pem, der_to_pem(&full_chain.ca_der)).expect("write ca.pem");
std::fs::write(&ee_pem, der_to_pem(&full_chain.ee_der)).expect("write ee.pem");
let out = Command::new("openssl")
.args(["verify", "-purpose", "any", "-CAfile"])
.arg(&ca_pem)
.arg(&ee_pem)
.output()
.expect("openssl verify");
if out.status.success() {
return; }
let mut culprit: Option<&str> = None;
for &skip in &extension_tags {
let chain =
generate_ephemeral_chain_with_ee_skip("test-diagnostic.example.com", &[skip])
.expect("generate_ephemeral_chain_with_ee_skip");
let ca_pem = temp_dir.path().join("ca.pem");
let ee_pem = temp_dir.path().join("ee.pem");
std::fs::write(&ca_pem, der_to_pem(&chain.ca_der)).expect("write ca.pem");
std::fs::write(&ee_pem, der_to_pem(&chain.ee_der)).expect("write ee.pem");
let out = Command::new("openssl")
.args(["verify", "-purpose", "any", "-CAfile"])
.arg(&ca_pem)
.arg(&ee_pem)
.output()
.expect("openssl verify");
if out.status.success() {
culprit = Some(skip);
break;
}
}
if let Some(ext) = culprit {
panic!(
"OpenSSL 3.x rejects our EE cert when the '{}' extension is present. \
Verify passes when that extension is omitted. Fix the encoding or \
structure of this extension.",
ext
);
}
}
}