synta-certificate 0.2.2

X.509 certificate structures for synta ASN.1 library
Documentation
//! CMS `EnvelopedData` (RFC 5652 §6) builder.
//!
//! Assembles a complete `EnvelopedData` SEQUENCE from pre-computed
//! cryptographic ingredients, delegating all crypto to the caller.
//!
//! # Design
//!
//! The builder is deliberately crypto-agnostic: it performs only ASN.1
//! assembly.  The caller is responsible for:
//!
//! 1. Generating a fresh content-encryption key (CEK).
//! 2. Encrypting the plaintext under the CEK and producing the
//!    `AlgorithmIdentifier` DER.
//! 3. For each recipient: building a `KeyTransRecipientInfo` DER (or another
//!    `RecipientInfo` variant) by encrypting the CEK under the recipient's
//!    public key and encoding the `IssuerAndSerialNumber`.
//!
//! The `synta_certificate` crate's `openssl` feature provides two
//! higher-level helpers that perform steps 1–3 using OpenSSL:
//!
//! * `create_enveloped_data` — the common case: generates the CEK,
//!   encrypts the plaintext, wraps the key for each recipient, assembles
//!   `EnvelopedData`, and returns the DER bytes in one call.
//! * `prepare_enveloped_data` — the two-step alternative: performs
//!   the same crypto but returns a pre-loaded [`EnvelopedDataBuilder`], letting
//!   the caller attach `OriginatorInfo` certificates/CRLs or
//!   `UnprotectedAttributes` before calling `.build()`.
//!
//! # Example
//!
//! ```rust,ignore
//! use synta_certificate::{create_enveloped_data, KeyWrapAlgorithm};
//!
//! // High-level convenience (requires "openssl" feature):
//! let der = create_enveloped_data(plaintext, &[(cert_der, KeyWrapAlgorithm::RsaOaepSha256)],
//!                                 synta_certificate::pkcs12_types::ID_AES256_CBC)?;
//!
//! // Low-level builder:
//! let der = EnvelopedDataBuilder::new(enc_alg_id_der, ciphertext)
//!     .add_recipient_info(ktri_der)
//!     .originator_cert(signer_cert_der)
//!     .build()?;
//! ```

use synta::{Encoding, Integer, ObjectIdentifier, OctetStringRef, RawDer, SetOf, ToDer};

use crate::cms_rfc5652_types::{EncryptedContentInfo, EnvelopedData, OriginatorInfo};

// ── Error ─────────────────────────────────────────────────────────────────────

/// Error returned by [`EnvelopedDataBuilder::build`].
#[derive(Debug)]
pub enum EnvelopedDataBuilderError {
    /// No recipient infos were added.
    NoRecipients,
    /// ASN.1 encoding failed.
    Encode(synta::Error),
    /// An OID constant was unexpectedly invalid (should never happen in
    /// practice — all OID constants are generated from the ASN.1 schema).
    InvalidOid(&'static str),
}

impl std::fmt::Display for EnvelopedDataBuilderError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::NoRecipients => f.write_str("EnvelopedData requires at least one RecipientInfo"),
            Self::Encode(e) => write!(f, "DER encoding error: {e:?}"),
            Self::InvalidOid(s) => write!(f, "invalid OID constant: {s}"),
        }
    }
}

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

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

// ── Builder ───────────────────────────────────────────────────────────────────

/// Builder for the `EnvelopedData` ASN.1 structure (RFC 5652 §6).
///
/// All cryptographic operations are the caller's responsibility.  Pass
/// pre-computed DER bytes for the content-encryption `AlgorithmIdentifier`,
/// the ciphertext, and each `RecipientInfo` variant.
///
/// # Encoding note for OriginatorInfo
///
/// The `certs` and `crls` fields of `OriginatorInfo` are encoded with
/// `[0] IMPLICIT` and `[1] IMPLICIT` context tags respectively, and stored
/// as `RawDer` value bytes (content without any outer SET tag or length).
/// When calling [`originator_cert`] / [`originator_crl`], pass the **full
/// DER SEQUENCE** of each certificate / CRL — the builder concatenates them
/// into the SET content and lets the encoder add the context tag.
///
/// [`originator_cert`]: EnvelopedDataBuilder::originator_cert
/// [`originator_crl`]: EnvelopedDataBuilder::originator_crl
pub struct EnvelopedDataBuilder {
    /// DER-encoded `AlgorithmIdentifier` for content encryption (including IV).
    enc_alg_id_der: Vec<u8>,
    /// Encrypted content bytes.
    ciphertext: Vec<u8>,
    /// DER-encoded `RecipientInfo` entries (each a complete TLV, e.g.
    /// `KeyTransRecipientInfo` SEQUENCE).
    recipient_info_ders: Vec<Vec<u8>>,
    /// DER-encoded certificates for `OriginatorInfo.certs`.
    originator_certs: Vec<Vec<u8>>,
    /// DER-encoded CRLs for `OriginatorInfo.crls`.
    originator_crls: Vec<Vec<u8>>,
    /// Raw content bytes for `UnprotectedAttributes` (without outer SET tag/len).
    unprotected_attrs: Option<Vec<u8>>,
}

impl EnvelopedDataBuilder {
    /// Create a new builder.
    ///
    /// - `enc_alg_id_der`: DER-encoded `AlgorithmIdentifier` SEQUENCE for the
    ///   content-encryption algorithm (e.g. AES-256-CBC with a random IV).
    /// - `ciphertext`: the encrypted content bytes.
    pub fn new(enc_alg_id_der: Vec<u8>, ciphertext: Vec<u8>) -> Self {
        Self {
            enc_alg_id_der,
            ciphertext,
            recipient_info_ders: Vec::new(),
            originator_certs: Vec::new(),
            originator_crls: Vec::new(),
            unprotected_attrs: None,
        }
    }

    /// Add a pre-computed `RecipientInfo` DER entry.
    ///
    /// `info_der` must be the complete DER TLV for one `RecipientInfo`
    /// variant, e.g. a `KeyTransRecipientInfo` SEQUENCE.  Call this once per
    /// recipient; order is preserved but the `RecipientInfos` SET is sorted
    /// lexicographically in DER encoding as required by X.690 §11.6.
    pub fn add_recipient_info(mut self, info_der: Vec<u8>) -> Self {
        self.recipient_info_ders.push(info_der);
        self
    }

    /// Add a certificate to `OriginatorInfo.certs`.
    ///
    /// `cert_der` must be the full DER-encoded `Certificate` SEQUENCE TLV.
    pub fn originator_cert(mut self, cert_der: &[u8]) -> Self {
        self.originator_certs.push(cert_der.to_vec());
        self
    }

    /// Add a CRL to `OriginatorInfo.crls`.
    ///
    /// `crl_der` must be the full DER-encoded `CertificateList` SEQUENCE TLV.
    pub fn originator_crl(mut self, crl_der: &[u8]) -> Self {
        self.originator_crls.push(crl_der.to_vec());
        self
    }

    /// Set the `unprotectedAttrs [1] IMPLICIT` field.
    ///
    /// `attrs` must be the raw **content** bytes of the `SET OF Attribute`
    /// (i.e. the value bytes without the outer SET tag or length prefix).
    /// The `[1] IMPLICIT` context tag is added by the encoder.
    pub fn unprotected_attrs(mut self, attrs: &[u8]) -> Self {
        self.unprotected_attrs = Some(attrs.to_vec());
        self
    }

    /// Assemble and return the DER-encoded `EnvelopedData` SEQUENCE.
    ///
    /// # Errors
    ///
    /// * [`EnvelopedDataBuilderError::NoRecipients`] — no recipient infos were added.
    /// * [`EnvelopedDataBuilderError::Encode`] — DER encoding of the assembled
    ///   structure failed.
    /// * [`EnvelopedDataBuilderError::InvalidOid`] — an internal OID constant was
    ///   invalid (should not occur in practice).
    pub fn build(self) -> Result<Vec<u8>, EnvelopedDataBuilderError> {
        if self.recipient_info_ders.is_empty() {
            return Err(EnvelopedDataBuilderError::NoRecipients);
        }

        // RecipientInfos SET: each entry is already a self-contained TLV.
        // SetOf<RawDer> sorts elements lexicographically in DER mode.
        let ri_raw: Vec<RawDer<'_>> = self
            .recipient_info_ders
            .iter()
            .map(|b| RawDer(b.as_slice()))
            .collect();
        let ri_set_der = SetOf::from_vec(ri_raw).to_der()?;

        // OriginatorInfo: certs/crls content bytes (without SET tag/len).
        // The [0]/[1] IMPLICIT context tags are added by the rawder encoder.
        let certs_content: Vec<u8> = self
            .originator_certs
            .iter()
            .flat_map(|c| c.iter().copied())
            .collect();
        let crls_content: Vec<u8> = self
            .originator_crls
            .iter()
            .flat_map(|c| c.iter().copied())
            .collect();
        let origin_info: Option<OriginatorInfo<'_>> =
            if certs_content.is_empty() && crls_content.is_empty() {
                None
            } else {
                Some(OriginatorInfo {
                    certs: if certs_content.is_empty() {
                        None
                    } else {
                        Some(RawDer(&certs_content))
                    },
                    crls: if crls_content.is_empty() {
                        None
                    } else {
                        Some(RawDer(&crls_content))
                    },
                })
            };

        // EncryptedContentInfo.
        let content_type = ObjectIdentifier::new(crate::pkcs7_types::ID_DATA)
            .map_err(|_| EnvelopedDataBuilderError::InvalidOid("id-data"))?;
        let content_encryption_algorithm: crate::AlgorithmIdentifier<'_> =
            synta::Decoder::new(&self.enc_alg_id_der, Encoding::Der).decode()?;
        let eci = EncryptedContentInfo {
            content_type,
            content_encryption_algorithm,
            encrypted_content: Some(OctetStringRef::new(&self.ciphertext)),
        };

        // EnvelopedData SEQUENCE.
        Ok(EnvelopedData {
            version: Integer::from_i64(0),
            originator_info: origin_info,
            recipient_infos: RawDer(&ri_set_der),
            encrypted_content_info: eci,
            unprotected_attrs: self.unprotected_attrs.as_deref().map(RawDer),
        }
        .to_der()?)
    }
}