provenance/src/cert.rs
//! X.509 for keyless signing: who a Fulcio certificate names, whether an issuer
//! signed it, and the SCTs it carries.
use x509_parser::prelude::*;
use crate::crypto::{Hash, PublicKey};
use crate::der;
use crate::error::{ProvenanceError, Result};
/// Fulcio's OIDC issuer extension (v2, a DER UTF8String).
pub const OID_ISSUER_V2: &str = "1.3.6.1.4.1.57264.1.8";
/// Fulcio's OIDC issuer extension (v1, raw bytes).
pub const OID_ISSUER_V1: &str = "1.3.6.1.4.1.57264.1.1";
/// The embedded SCT list.
pub const OID_SCT_LIST: &str = "1.3.6.1.4.1.11129.2.4.2";
const ECDSA_SHA256: &str = "1.2.840.10045.4.3.2";
const ECDSA_SHA384: &str = "1.2.840.10045.4.3.3";
const ED25519: &str = "1.3.101.112";
const RSA_SHA256: &str = "1.2.840.113549.1.1.11";
const RSA_SHA384: &str = "1.2.840.113549.1.1.12";
fn parse(der: &[u8]) -> Result<X509Certificate<'_>> {
X509Certificate::from_der(der)
.map(|(_, c)| c)
.map_err(|e| ProvenanceError::Malformed(format!("certificate: {e}")))
}
/// The certificate's SubjectPublicKeyInfo DER.
pub fn spki_der(cert_der: &[u8]) -> Result<Vec<u8>> {
Ok(parse(cert_der)?.tbs_certificate.subject_pki.raw.to_vec())
}
/// The certificate's public key.
pub fn public_key(cert_der: &[u8]) -> Result<PublicKey> {
PublicKey::from_spki_der(&spki_der(cert_der)?)
}
/// `(not_before, not_after)`, Unix seconds.
pub fn validity(cert_der: &[u8]) -> Result<(i64, i64)> {
let c = parse(cert_der)?;
Ok((
c.validity().not_before.timestamp(),
c.validity().not_after.timestamp(),
))
}
/// Did `issuer_der`'s key sign `cert_der`?
pub fn issued_by(cert_der: &[u8], issuer_der: &[u8]) -> Result<bool> {
let parts = der::cert_parts(cert_der)?;
let key = public_key(issuer_der)?;
let is = |d: &str| parts.sig_alg_oid == der::oid_bytes(d).as_slice();
let hash = if is(ECDSA_SHA256) || is(RSA_SHA256) || is(ED25519) {
Hash::Sha256
} else if is(ECDSA_SHA384) || is(RSA_SHA384) {
Hash::Sha384
} else {
return Err(ProvenanceError::SignerUntrusted(
"the certificate is signed with an algorithm this verifier does not take".into(),
));
};
Ok(key.verify_with(hash, parts.tbs.raw, parts.signature))
}
/// The keyless identity a Fulcio certificate names.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CertIdentity {
/// The OIDC issuer, from the Fulcio extension.
pub issuer: Option<String>,
/// The SAN: a URI or an e-mail.
pub subject: Option<String>,
}
fn ext_string(value: &[u8]) -> String {
if let Ok((t, _)) = der::read(value) {
if matches!(t.tag, 0x0c | 0x16 | 0x13) {
return String::from_utf8_lossy(t.content).into_owned();
}
}
String::from_utf8_lossy(value).into_owned()
}
/// Read the issuer and subject.
pub fn identity(cert_der: &[u8]) -> Result<CertIdentity> {
let c = parse(cert_der)?;
let mut v2 = None;
let mut v1 = None;
for ext in c.iter_extensions() {
let id = ext.oid.to_id_string();
if id == OID_ISSUER_V2 {
v2 = Some(ext_string(ext.value));
} else if id == OID_ISSUER_V1 {
v1 = Some(String::from_utf8_lossy(ext.value).into_owned());
}
}
let mut subject = None;
if let Ok(Some(san)) = c.subject_alternative_name() {
for name in &san.value.general_names {
match name {
GeneralName::URI(u) => {
subject = Some(u.to_string());
break;
}
GeneralName::RFC822Name(e) => {
subject = Some(e.to_string());
break;
}
_ => {}
}
}
}
Ok(CertIdentity {
issuer: v2.or(v1),
subject,
})
}
/// Does the certificate allow code signing (EKU)?
pub fn allows_code_signing(cert_der: &[u8]) -> Result<bool> {
let c = parse(cert_der)?;
Ok(match c.extended_key_usage() {
Ok(Some(eku)) => eku.value.code_signing,
_ => false,
})
}
/// One embedded SCT.
#[derive(Debug, Clone)]
pub struct Sct {
pub log_id: [u8; 32],
pub timestamp_ms: u64,
pub extensions: Vec<u8>,
pub hash_alg: u8,
pub sig_alg: u8,
pub signature: Vec<u8>,
}
/// The SCTs embedded in the certificate (none when it has no SCT extension).
pub fn scts(cert_der: &[u8]) -> Result<Vec<Sct>> {
let c = parse(cert_der)?;
let bad = |w: &str| ProvenanceError::Malformed(format!("SCT list: {w}"));
let Some(ext) = c
.iter_extensions()
.find(|e| e.oid.to_id_string() == OID_SCT_LIST)
else {
return Ok(Vec::new());
};
// The extension value is an OCTET STRING holding the TLS-encoded list.
let (inner, _) = der::read(ext.value)?;
if inner.tag != 0x04 {
return Err(bad("not an OCTET STRING"));
}
let b = inner.content;
if b.len() < 2 {
return Err(bad("truncated"));
}
let total = u16::from_be_bytes([b[0], b[1]]) as usize;
let mut p = &b[2..];
if p.len() != total {
return Err(bad("length mismatch"));
}
let mut out = Vec::new();
while !p.is_empty() {
if p.len() < 2 {
return Err(bad("truncated entry"));
}
let n = u16::from_be_bytes([p[0], p[1]]) as usize;
let s = p.get(2..2 + n).ok_or_else(|| bad("truncated entry"))?;
p = &p[2 + n..];
// version(1) log_id(32) timestamp(8) ext_len(2) ext hash(1) sig(1) sig_len(2) sig
if s.len() < 47 || s[0] != 0 {
return Err(bad("not a v1 SCT"));
}
let mut log_id = [0u8; 32];
log_id.copy_from_slice(&s[1..33]);
let timestamp_ms = u64::from_be_bytes(s[33..41].try_into().unwrap());
let el = u16::from_be_bytes([s[41], s[42]]) as usize;
let rest = s
.get(43 + el..)
.ok_or_else(|| bad("truncated extensions"))?;
let extensions = s[43..43 + el].to_vec();
if rest.len() < 4 {
return Err(bad("truncated signature"));
}
let sl = u16::from_be_bytes([rest[2], rest[3]]) as usize;
let signature = rest
.get(4..4 + sl)
.ok_or_else(|| bad("truncated signature"))?
.to_vec();
out.push(Sct {
log_id,
timestamp_ms,
extensions,
hash_alg: rest[0],
sig_alg: rest[1],
signature,
});
}
Ok(out)
}
/// The bytes a CT log signed for an embedded SCT on a precertificate (RFC 6962 §3.2).
pub fn sct_signed_bytes(sct: &Sct, cert_der: &[u8], issuer_spki_der: &[u8]) -> Result<Vec<u8>> {
let tbs = der::tbs_without_extension(cert_der, OID_SCT_LIST)?;
let mut m = vec![0u8, 0u8];
m.extend_from_slice(&sct.timestamp_ms.to_be_bytes());
m.extend_from_slice(&1u16.to_be_bytes()); // precert_entry
m.extend_from_slice(&crate::crypto::sha256(issuer_spki_der));
let n = tbs.len();
m.extend_from_slice(&[(n >> 16) as u8, (n >> 8) as u8, n as u8]);
m.extend_from_slice(&tbs);
m.extend_from_slice(&(sct.extensions.len() as u16).to_be_bytes());
m.extend_from_slice(&sct.extensions);
Ok(m)
}