synta-certificate 0.2.6

X.509 certificate structures for synta ASN.1 library
Documentation
//! Format-agnostic PKI data reader.
//!
//! [`read_pki_blocks`] inspects raw file bytes, detects the encoding
//! (PEM, PKCS#7/CMS, PKCS#12, or raw DER), and returns every PKI object
//! as a `(label, der)` pair — the same shape as [`pem_blocks`][crate::pem_blocks].

use synta::{Decoder, Encoding, TagClass};

use crate::crypto::Pkcs12Decryptor;
use crate::pkcs12::{pki_from_pkcs12, Pkcs12Error};
use crate::pkcs7::{certs_from_pkcs7, Pkcs7Error};

// ── Object-safe decryptor facade ─────────────────────────────────────────────

/// Object-safe decryptor used by [`read_pki_blocks`].
///
/// A blanket implementation is provided for every [`Pkcs12Decryptor`], so any
/// existing decryptor (e.g. `OpensslDecryptor`) can
/// be passed directly as `Some(&my_decryptor)` without additional boilerplate.
pub trait PkiDecryptor {
    /// Decrypt a PKCS#12 encrypted bag.
    ///
    /// Arguments mirror [`Pkcs12Decryptor::decrypt`]; the error is erased to
    /// `Box<dyn Error>` to keep this trait object-safe.
    fn decrypt_pkcs12(
        &self,
        algorithm_der: &[u8],
        ciphertext: &[u8],
        password: &[u8],
    ) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>>;
}

impl<T: Pkcs12Decryptor> PkiDecryptor for T {
    fn decrypt_pkcs12(
        &self,
        algorithm_der: &[u8],
        ciphertext: &[u8],
        password: &[u8],
    ) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
        Pkcs12Decryptor::decrypt(self, algorithm_der, ciphertext, password)
            .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
    }
}

// ── Error type ───────────────────────────────────────────────────────────────

/// Error returned by [`read_pki_blocks`] on structural or decryption failure.
#[derive(Debug)]
pub enum ReadAnyError {
    /// PKCS#7 parse or structure error.
    Pkcs7(Pkcs7Error),
    /// PKCS#12 ASN.1 structural parse error.
    Pkcs12Parse(synta::Error),
    /// PKCS#12 decryption failed (wrong password or unsupported algorithm).
    Pkcs12Crypto(Box<dyn std::error::Error + Send + Sync>),
    /// PKCS#12 unsupported format variant.
    Pkcs12Format(&'static str),
}

impl std::fmt::Display for ReadAnyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ReadAnyError::Pkcs7(e) => write!(f, "PKCS#7 error: {}", e),
            ReadAnyError::Pkcs12Parse(e) => write!(f, "PKCS#12 parse error: {}", e),
            ReadAnyError::Pkcs12Crypto(e) => write!(f, "PKCS#12 decryption error: {}", e),
            ReadAnyError::Pkcs12Format(s) => write!(f, "PKCS#12 unsupported format: {}", s),
        }
    }
}

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

// ── Public API ────────────────────────────────────────────────────────────────

/// Detect the encoding of `data` and return every PKI object as a
/// `(label, der)` pair, matching the output style of
/// [`pem_blocks`][crate::pem_blocks].
///
/// # Supported formats
///
/// | Format | How detected | Labels returned |
/// |--------|-------------|-----------------|
/// | PEM | `-----BEGIN` marker | `"CERTIFICATE"` per cert for PKCS#7 blocks; original label for all other block types (e.g. `"PRIVATE KEY"`) |
/// | PKCS#7 / CMS SignedData | first inner tag is OID | `"CERTIFICATE"` per cert |
/// | PKCS#12 | first inner tag is INTEGER | `"CERTIFICATE"` per cert, `"PRIVATE KEY"` per key |
/// | Raw DER | everything else | `"CERTIFICATE"` |
///
/// PEM blocks whose DER payload is a PKCS#7 `ContentInfo` (outer SEQUENCE
/// containing an OID as its first field) are transparently expanded: the
/// embedded certificates are extracted and returned with the `"CERTIFICATE"`
/// label, exactly as if the same data had been supplied in binary form.
/// Other PEM block types (e.g. `CERTIFICATE`, `PRIVATE KEY`) pass through
/// with their original label.  Malformed PEM blocks (bad base64, truncated
/// header) are silently skipped.
///
/// # Decryption
///
/// `password` and `decryptor` apply to PKCS#12 archives:
///
/// - `decryptor = Some(d)` — encrypted bags are decrypted with `d` and
///   `password`; a decryption failure returns `Err`.
/// - `decryptor = None` — encrypted bags are silently skipped; unencrypted
///   certificates in the same archive are still returned.
///
/// For PEM input the `password` and `decryptor` arguments are currently
/// ignored (PEM-encrypted private keys are not supported).
///
/// # Errors
///
/// | Variant | Trigger |
/// |---------|---------|
/// | [`ReadAnyError::Pkcs7`] | A PKCS#7 block (binary or PEM-wrapped) has malformed DER, or its `contentType` OID is not `id-signedData`. |
/// | [`ReadAnyError::Pkcs12Parse`] | The PKCS#12 input has a structural ASN.1 error (truncated, wrong tag, etc.). |
/// | [`ReadAnyError::Pkcs12Crypto`] | `decryptor` is `Some` and decryption of an encrypted SafeBag fails. Never returned when `decryptor` is `None`. |
/// | [`ReadAnyError::Pkcs12Format`] | The PKCS#12 authSafe uses an unsupported `ContentInfo` content type. |
///
/// # Example
///
/// ```rust,ignore
/// use synta_certificate::{read_pki_blocks, NoCrypto};
///
/// let data = std::fs::read("bundle.p7b").unwrap();
/// let blocks = read_pki_blocks(&data, b"", None::<&NoCrypto>).unwrap();
/// for (label, der) in &blocks {
///     println!("{}: {} bytes", label, der.len());
/// }
/// ```
pub fn read_pki_blocks(
    data: &[u8],
    password: &[u8],
    decryptor: Option<&dyn PkiDecryptor>,
) -> Result<Vec<(String, Vec<u8>)>, ReadAnyError> {
    // PEM: textual `-----BEGIN` marker somewhere in the data.
    if data.windows(11).any(|w| w == b"-----BEGIN ") {
        let pem = crate::pem::pem_blocks(data);
        let mut out = Vec::with_capacity(pem.len());
        for (label, der) in pem {
            // PEM-wrapped PKCS#7: the first element inside the outermost
            // SEQUENCE is an OID (ContentInfo.contentType).  Recurse once
            // into the binary path to extract the embedded certificates.
            if matches!(
                peek_inner_tag(&der),
                Some(tag) if tag.class() == TagClass::Universal && tag.number() == 6
            ) {
                let certs = certs_from_pkcs7(&der).map_err(ReadAnyError::Pkcs7)?;
                out.extend(label_as_certificates(certs));
            } else {
                out.push((label, der));
            }
        }
        return Ok(out);
    }

    // Binary: probe the tag of the first element inside the outermost SEQUENCE.
    match peek_inner_tag(data) {
        // INTEGER → PKCS#12 PFX (version INTEGER is the first field of PFX).
        Some(tag) if tag.class() == TagClass::Universal && tag.number() == 2 => {
            read_pkcs12_blocks(data, password, decryptor)
        }
        // OID → PKCS#7 / CMS ContentInfo (contentType OID is the first field).
        Some(tag) if tag.class() == TagClass::Universal && tag.number() == 6 => {
            certs_from_pkcs7(data)
                .map_err(ReadAnyError::Pkcs7)
                .map(label_as_certificates)
        }
        // Anything else — hand back the whole buffer as a single raw DER object.
        _ => Ok(vec![("CERTIFICATE".to_string(), data.to_vec())]),
    }
}

// ── Internal helpers ──────────────────────────────────────────────────────────

/// Peek at the tag of the first element inside the outermost SEQUENCE TLV.
///
/// Returns `None` if `data` is shorter than two bytes, does not start with a
/// SEQUENCE tag, or has a malformed length encoding.
fn peek_inner_tag(data: &[u8]) -> Option<synta::Tag> {
    let mut d = Decoder::new(data, Encoding::Ber);
    d.read_tag().ok()?; // outer SEQUENCE tag
    d.read_length().ok()?; // outer SEQUENCE length
    d.peek_tag().ok() // first inner tag
}

/// Wrap a list of DER blobs with the `"CERTIFICATE"` label.
fn label_as_certificates(certs: Vec<Vec<u8>>) -> Vec<(String, Vec<u8>)> {
    certs
        .into_iter()
        .map(|der| ("CERTIFICATE".to_string(), der))
        .collect()
}

// ── PKCS#12-specific logic ────────────────────────────────────────────────────

/// Concrete error type that wraps a type-erased box so it can satisfy the
/// `std::error::Error` bound required by [`Pkcs12Decryptor::Error`].
///
/// (`Box<dyn Error>` itself is `!Sized` and does not implement `Error` in
/// current Rust versions, so a newtype is needed.)
struct BoxedError(Box<dyn std::error::Error + Send + Sync + 'static>);

impl std::fmt::Debug for BoxedError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        self.0.fmt(f)
    }
}

impl std::fmt::Display for BoxedError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        self.0.fmt(f)
    }
}

impl std::error::Error for BoxedError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        self.0.source()
    }
}

/// Thin `Pkcs12Decryptor` wrapper around a `dyn PkiDecryptor`, so we can pass
/// a trait-object into the generic `certs_from_pkcs12` function.
struct DynWrap<'a>(&'a dyn PkiDecryptor);

impl Pkcs12Decryptor for DynWrap<'_> {
    type Error = BoxedError;

    fn decrypt(
        &self,
        algorithm_der: &[u8],
        ciphertext: &[u8],
        password: &[u8],
    ) -> Result<Vec<u8>, BoxedError> {
        self.0
            .decrypt_pkcs12(algorithm_der, ciphertext, password)
            .map_err(BoxedError)
    }
}

/// A decryptor that "succeeds" by returning an empty SafeContents SEQUENCE.
///
/// Used when `decryptor` is `None`: encrypted bags yield zero certificates
/// rather than an error, so unencrypted bags in the same archive are still
/// collected.
struct SkipEncrypted;

impl Pkcs12Decryptor for SkipEncrypted {
    type Error = std::convert::Infallible;

    fn decrypt(
        &self,
        _algorithm_der: &[u8],
        _ciphertext: &[u8],
        _password: &[u8],
    ) -> Result<Vec<u8>, std::convert::Infallible> {
        // `30 00` = SEQUENCE { } — an empty SafeContents with no SafeBags.
        Ok(vec![0x30, 0x00])
    }
}

fn read_pkcs12_blocks(
    data: &[u8],
    password: &[u8],
    decryptor: Option<&dyn PkiDecryptor>,
) -> Result<Vec<(String, Vec<u8>)>, ReadAnyError> {
    let pki = if let Some(d) = decryptor {
        pki_from_pkcs12(data, password, &DynWrap(d)).map_err(|e| match e {
            Pkcs12Error::Parse(e) => ReadAnyError::Pkcs12Parse(e),
            Pkcs12Error::Crypto(e) => ReadAnyError::Pkcs12Crypto(e.0),
            Pkcs12Error::UnsupportedFormat(s) => ReadAnyError::Pkcs12Format(s),
        })?
    } else {
        pki_from_pkcs12(data, password, &SkipEncrypted).map_err(|e| match e {
            Pkcs12Error::Parse(e) => ReadAnyError::Pkcs12Parse(e),
            // SkipEncrypted::Error = Infallible — this branch is unreachable.
            Pkcs12Error::Crypto(never) => match never {},
            Pkcs12Error::UnsupportedFormat(s) => ReadAnyError::Pkcs12Format(s),
        })?
    };

    let mut out = label_as_certificates(pki.certs);
    out.extend(
        pki.keys
            .into_iter()
            .map(|der| ("PRIVATE KEY".to_string(), der)),
    );
    Ok(out)
}