Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

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)
}