synta-certificate 0.2.2

X.509 certificate structures for synta ASN.1 library
Documentation
/// Extract DER-encoded X.509 certificates from a PKCS#7 / CMS SignedData blob.
///
/// Accepts raw DER or BER input.  BER indefinite-length encodings (common in
/// PKCS#7 files generated by older tools and OpenSSL) are handled
/// transparently.  Returns one DER byte vector per certificate found in the
/// `certificates` field of the SignedData, in document order.
/// Non-certificate entries in the `CertificateSet` (attribute certificates,
/// other tagged alternatives) are silently skipped.
///
/// # Errors
///
/// Returns [`Pkcs7Error::NotSignedData`] if the outer `ContentInfo.contentType`
/// OID is not `id-signedData`.  Returns [`Pkcs7Error::Parse`] on any
/// structural ASN.1 error.
///
/// # Example
///
/// ```rust,ignore
/// let der = std::fs::read("bundle.p7b").unwrap();
/// let certs = synta_certificate::certs_from_pkcs7(&der).unwrap();
/// println!("found {} certificates", certs.len());
/// ```
use synta::{Decoder, Encoding, ObjectIdentifier, Tag, TagClass};

use crate::pkcs7_types::ID_SIGNED_DATA;

/// Error type for PKCS#7 certificate extraction.
#[derive(Debug)]
pub enum Pkcs7Error {
    /// ASN.1 structural parse error.
    Parse(synta::Error),
    /// The outer ContentInfo contentType is not id-signedData.
    NotSignedData,
}

impl std::fmt::Display for Pkcs7Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Pkcs7Error::Parse(e) => write!(f, "PKCS#7 ASN.1 parse error: {}", e),
            Pkcs7Error::NotSignedData => f.write_str("PKCS#7 ContentInfo is not id-signedData"),
        }
    }
}

impl std::error::Error for Pkcs7Error {}

impl From<synta::Error> for Pkcs7Error {
    fn from(e: synta::Error) -> Self {
        Pkcs7Error::Parse(e)
    }
}

/// Extract DER-encoded certificates from a PKCS#7 SignedData blob.
///
/// Accepts raw DER, BER, or CER input; the encoding is detected automatically.
/// BER indefinite-length wrappers (common in PKCS#7 files from older tools and
/// some OpenSSL versions) are handled transparently.
pub fn certs_from_pkcs7(data: &[u8]) -> Result<Vec<Vec<u8>>, Pkcs7Error> {
    // Use BER mode so that indefinite-length outer wrappers are accepted.
    let mut decoder = Decoder::new(data, Encoding::Ber);

    // Enter the outer ContentInfo SEQUENCE (may be BER indefinite-length).
    // We decode ContentInfo manually via enter_constructed rather than using the
    // generated ContentInfo::decode struct, to avoid storing RawDer<'a> slices
    // that point into assembled BER buffers (which would become dangling after
    // the child decoder is dropped).
    let seq_tag = Tag::universal_constructed(16); // SEQUENCE
    let mut outer = decoder
        .enter_constructed(seq_tag)
        .map_err(Pkcs7Error::Parse)?;

    // Decode the contentType OBJECT IDENTIFIER.
    let content_type: ObjectIdentifier = outer.decode().map_err(Pkcs7Error::Parse)?;
    if content_type.components() != ID_SIGNED_DATA {
        return Err(Pkcs7Error::NotSignedData);
    }

    // Enter the [0] EXPLICIT content wrapper (also possibly BER indefinite-length).
    let ctx0_tag = Tag::new(TagClass::ContextSpecific, true, 0);
    let mut content = outer
        .enter_constructed(ctx0_tag)
        .map_err(Pkcs7Error::Parse)?;

    // `content` is now positioned at the SignedData SEQUENCE TLV.
    parse_signed_data_certs(&mut content)
}

/// Traverse SignedData and collect certificate DER bytes.
///
/// `dec` must be positioned at the beginning of the SignedData SEQUENCE TLV.
///
/// SignedData (RFC 5652 §5.1):
///   SEQUENCE {
///     version          CMSVersion,
///     digestAlgorithms DigestAlgorithmIdentifiers,   -- SET
///     encapContentInfo EncapsulatedContentInfo,      -- SEQUENCE
///     certificates [0] IMPLICIT CertificateSet OPTIONAL,
///     crls         [1] IMPLICIT RevocationInfoChoices OPTIONAL,
///     signerInfos      SignerInfos                   -- SET
///   }
///
/// We skip version, digestAlgorithms and encapContentInfo, then check for the
/// optional [0] IMPLICIT certificates field (tag 0xA0).
fn parse_signed_data_certs(dec: &mut Decoder<'_>) -> Result<Vec<Vec<u8>>, Pkcs7Error> {
    let seq_tag = Tag::universal_constructed(16); // SEQUENCE
    let mut inner = dec.enter_constructed(seq_tag).map_err(Pkcs7Error::Parse)?;

    // version INTEGER
    skip_tlv(&mut inner)?;
    // digestAlgorithms SET OF AlgorithmIdentifier
    skip_tlv(&mut inner)?;
    // encapContentInfo SEQUENCE
    skip_tlv(&mut inner)?;

    // Check for optional certificates [0] IMPLICIT (context-specific constructed 0 = 0xA0)
    if inner.is_empty() {
        return Ok(Vec::new());
    }
    let next = inner.peek_tag().map_err(Pkcs7Error::Parse)?;
    if next.class() != TagClass::ContextSpecific || next.number() != 0 {
        return Ok(Vec::new());
    }

    // Consume the [0] IMPLICIT wrapper tag and enter it as constructed.
    let ctx0_tag = Tag::new(TagClass::ContextSpecific, true, 0);
    let mut cert_set = inner
        .enter_constructed(ctx0_tag)
        .map_err(Pkcs7Error::Parse)?;

    let mut certs = Vec::new();
    while !cert_set.is_empty() {
        // Each entry in CertificateSet is a CHOICE; we only take plain
        // SEQUENCE (0x30) entries as X.509 Certificate DER.  All other
        // alternatives (attribute certificate [1], v2AttrCert [2], …) are
        // skipped by consuming the TLV without inspecting the content.
        let tag = cert_set.peek_tag().map_err(Pkcs7Error::Parse)?;
        if tag.class() == TagClass::Universal && tag.number() == 16 && tag.is_constructed() {
            // Capture the entire Certificate TLV.  Certificates are normally
            // DER-encoded even inside BER wrappers; `to_vec()` copies the bytes
            // out before the decoder is dropped.
            let raw: synta::RawDer = cert_set.decode().map_err(Pkcs7Error::Parse)?;
            certs.push(raw.as_bytes().to_vec());
        } else {
            // Non-certificate alternative; skip the whole TLV.
            let _: synta::RawDer = cert_set.decode().map_err(Pkcs7Error::Parse)?;
        }
    }

    Ok(certs)
}

/// Skip one TLV in `dec` without inspecting its content.
fn skip_tlv(dec: &mut Decoder<'_>) -> Result<(), Pkcs7Error> {
    let _: synta::RawDer = dec.decode().map_err(Pkcs7Error::Parse)?;
    Ok(())
}