synta-certificate 0.2.6

X.509 certificate structures for synta ASN.1 library
Documentation
//! PKCS#12 `PFX` archive builder.
//!
//! Assembles a complete `PFX` DER from X.509 certificates and an optional
//! private key, delegating all cryptography to a [`Pkcs12Encryptor`]
//! implementation.
//!
//! # Output structure
//!
//! ```text
//! PFX SEQUENCE {
//!   version INTEGER 3,
//!   authSafe ContentInfo {
//!     contentType id-data,
//!     content [0] EXPLICIT OCTET STRING {
//!       AuthenticatedSafe SEQUENCE OF ContentInfo {
//!         -- Certificates (unencrypted)
//!         ContentInfo { id-data, [0] EXPLICIT OCTET STRING {
//!           SafeContents SEQUENCE OF certBag SafeBag { ... }
//!         }}
//!         -- Private key (encrypted at bag level; only present when key supplied)
//!         ContentInfo { id-data, [0] EXPLICIT OCTET STRING {
//!           SafeContents SEQUENCE OF pkcs8ShroudedKeyBag SafeBag {
//!             bagValue [0] EXPLICIT EncryptedPrivateKeyInfo {
//!               encryptionAlgorithm AlgorithmIdentifier,
//!               encryptedData OCTET STRING
//!             }
//!           }
//!         }}
//!       }
//!     }
//!   },
//!   macData MacData { mac DigestInfo, macSalt OCTET STRING, iterations INTEGER }
//! }
//! ```
//!
//! # Example
//!
//! ```rust,ignore
//! use synta_certificate::{Pkcs12Builder, OpensslPkcs12Encryptor};
//!
//! let pfx_der = Pkcs12Builder::new()
//!     .certificate(&cert_der)
//!     .private_key(&key_der)
//!     .build(b"password", &OpensslPkcs12Encryptor::new())?;
//! ```

use synta::{ObjectIdentifier, ToDer};

use crate::crypto::Pkcs12Encryptor;
use crate::pkcs12_types::{ID_CERT_BAG, ID_DATA, ID_PKCS8_SHROUDED_KEY_BAG, ID_X509_CERTIFICATE};

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

/// Error returned by [`Pkcs12Builder::build`].
#[derive(Debug)]
pub enum Pkcs12BuilderError<E> {
    /// Neither certificates nor a private key were added to the builder.
    NothingToPackage,
    /// The [`Pkcs12Encryptor`] returned an error during encryption or MAC.
    EncryptError(E),
    /// An internal ASN.1 encoding step failed.
    EncodeError(String),
}

impl<E: std::fmt::Display> std::fmt::Display for Pkcs12BuilderError<E> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::NothingToPackage => {
                f.write_str("PKCS#12 builder: no certificates or key to package")
            }
            Self::EncryptError(e) => write!(f, "PKCS#12 encryption error: {e}"),
            Self::EncodeError(s) => write!(f, "PKCS#12 ASN.1 encoding error: {s}"),
        }
    }
}

impl<E: std::error::Error + 'static> std::error::Error for Pkcs12BuilderError<E> {}

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

/// Builder for PKCS#12 `PFX` archives.
///
/// Assembles a self-contained DER-encoded `PFX` from X.509 certificates and
/// an optional private key.  All cryptographic operations (key encryption and
/// MAC computation) are delegated to the [`Pkcs12Encryptor`] supplied to
/// [`build`].
///
/// [`build`]: Pkcs12Builder::build
///
/// # Example
///
/// ```rust,ignore
/// use synta_certificate::{Pkcs12Builder, OpensslPkcs12Encryptor};
///
/// let pfx_der = Pkcs12Builder::new()
///     .certificate(&cert_der)
///     .private_key(&key_der)
///     .build(b"s3cr3t", &OpensslPkcs12Encryptor::new())?;
///
/// std::fs::write("out.p12", &pfx_der)?;
/// ```
pub struct Pkcs12Builder {
    /// DER-encoded X.509 certificates for `certBag` entries.
    certs: Vec<Vec<u8>>,
    /// DER-encoded unencrypted PKCS#8 private key for `pkcs8ShroudedKeyBag`.
    key: Option<Vec<u8>>,
}

impl Default for Pkcs12Builder {
    fn default() -> Self {
        Self::new()
    }
}

impl Pkcs12Builder {
    /// Create a new, empty builder.
    pub fn new() -> Self {
        Self {
            certs: Vec::new(),
            key: None,
        }
    }

    /// Add a DER-encoded X.509 certificate.
    ///
    /// Multiple calls add multiple `certBag` entries in insertion order.
    pub fn certificate(mut self, cert_der: &[u8]) -> Self {
        self.certs.push(cert_der.to_vec());
        self
    }

    /// Set the DER-encoded PKCS#8 private key (`OneAsymmetricKey`, unencrypted).
    ///
    /// The key will be encrypted via [`Pkcs12Encryptor::encrypt`] and stored
    /// as a `pkcs8ShroudedKeyBag`.
    pub fn private_key(mut self, key_der: &[u8]) -> Self {
        self.key = Some(key_der.to_vec());
        self
    }

    /// Assemble and return a DER-encoded PKCS#12 `PFX`.
    ///
    /// Returns [`Pkcs12BuilderError::NothingToPackage`] if neither
    /// certificates nor a key were supplied.
    pub fn build<E: Pkcs12Encryptor>(
        self,
        password: &[u8],
        encryptor: &E,
    ) -> Result<Vec<u8>, Pkcs12BuilderError<E::Error>> {
        if self.certs.is_empty() && self.key.is_none() {
            return Err(Pkcs12BuilderError::NothingToPackage);
        }

        // ── Cert SafeContents ─────────────────────────────────────────────────
        let mut cert_bags_der = Vec::new();
        for cert_der in &self.certs {
            let bag = encode_cert_safe_bag(cert_der).map_err(Pkcs12BuilderError::EncodeError)?;
            cert_bags_der.extend_from_slice(&bag);
        }
        let cert_safe_contents = der_wrap_sequence(cert_bags_der);
        let cert_content_info = encode_id_data_content_info(&cert_safe_contents)
            .map_err(Pkcs12BuilderError::EncodeError)?;

        // ── Key SafeContents (encrypted at bag level) ─────────────────────────
        let key_content_info: Vec<u8> = if let Some(key_der) = &self.key {
            let (alg_id_der, ciphertext) = encryptor
                .encrypt(key_der, password)
                .map_err(Pkcs12BuilderError::EncryptError)?;
            let key_bag = encode_shrouded_key_safe_bag(&alg_id_der, &ciphertext)
                .map_err(Pkcs12BuilderError::EncodeError)?;
            let key_safe_contents = der_wrap_sequence(key_bag);
            encode_id_data_content_info(&key_safe_contents)
                .map_err(Pkcs12BuilderError::EncodeError)?
        } else {
            Vec::new()
        };

        // ── AuthenticatedSafe ─────────────────────────────────────────────────
        let mut auth_safe_body = cert_content_info;
        auth_safe_body.extend_from_slice(&key_content_info);
        let auth_safe = der_wrap_sequence(auth_safe_body);

        // ── Outer id-data ContentInfo wrapping AuthenticatedSafe ──────────────
        let auth_safe_content_info =
            encode_id_data_content_info(&auth_safe).map_err(Pkcs12BuilderError::EncodeError)?;

        // ── MacData ───────────────────────────────────────────────────────────
        let mac_data = encryptor
            .compute_mac(&auth_safe, password)
            .map_err(Pkcs12BuilderError::EncryptError)?;

        // ── PFX SEQUENCE: version=3, authSafe, macData ────────────────────────
        let version_der: &[u8] = &[0x02, 0x01, 0x03]; // INTEGER 3
        let mut pfx_body = Vec::new();
        pfx_body.extend_from_slice(version_der);
        pfx_body.extend_from_slice(&auth_safe_content_info);
        pfx_body.extend_from_slice(&mac_data);
        Ok(der_wrap_sequence(pfx_body))
    }
}

// ── PKCS#12 structure encoders ─────────────────────────────────────────────

/// Encode a `certBag` `SafeBag`.
///
/// ```text
/// SafeBag ::= SEQUENCE {
///     bagId    id-certBag OID,
///     bagValue [0] EXPLICIT CertBag SEQUENCE {
///         certId    id-x509Certificate OID,
///         certValue [0] EXPLICIT OCTET STRING (cert_der)
///     }
/// }
/// ```
fn encode_cert_safe_bag(cert_der: &[u8]) -> Result<Vec<u8>, String> {
    let x509_id = der_oid_tlv(ID_X509_CERTIFICATE)?;
    let cert_value = der_explicit_ctx(0, &der_octet_string(cert_der));
    let cert_bag = der_wrap_sequence([x509_id, cert_value].concat());

    let bag_id = der_oid_tlv(ID_CERT_BAG)?;
    let bag_value = der_explicit_ctx(0, &cert_bag);
    Ok(der_wrap_sequence([bag_id, bag_value].concat()))
}

/// Encode a `pkcs8ShroudedKeyBag` `SafeBag`.
///
/// ```text
/// SafeBag ::= SEQUENCE {
///     bagId    id-pkcs8ShroudedKeyBag OID,
///     bagValue [0] EXPLICIT EncryptedPrivateKeyInfo SEQUENCE {
///         encryptionAlgorithm AlgorithmIdentifier (= alg_id_der),
///         encryptedData       OCTET STRING (= ciphertext)
///     }
/// }
/// ```
fn encode_shrouded_key_safe_bag(alg_id_der: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, String> {
    let encrypted_data = der_octet_string(ciphertext);
    let epki_body = [alg_id_der, encrypted_data.as_slice()].concat();
    let epki = der_wrap_sequence(epki_body);

    let bag_id = der_oid_tlv(ID_PKCS8_SHROUDED_KEY_BAG)?;
    let bag_value = der_explicit_ctx(0, &epki);
    Ok(der_wrap_sequence([bag_id, bag_value].concat()))
}

/// Encode a `ContentInfo { id-data, [0] EXPLICIT OCTET STRING(content) }`.
fn encode_id_data_content_info(content: &[u8]) -> Result<Vec<u8>, String> {
    let oid_der = der_oid_tlv(ID_DATA)?;
    let octet_str = der_octet_string(content);
    let explicit_content = der_explicit_ctx(0, &octet_str);
    Ok(der_wrap_sequence([oid_der, explicit_content].concat()))
}

// ── Low-level DER helpers ─────────────────────────────────────────────────────

/// Append a DER length to `out` (short or multi-byte form).
pub(crate) fn der_push_len(out: &mut Vec<u8>, len: usize) {
    if len < 0x80 {
        out.push(len as u8);
    } else if len < 0x100 {
        out.extend_from_slice(&[0x81, len as u8]);
    } else if len < 0x10000 {
        out.extend_from_slice(&[0x82, (len >> 8) as u8, (len & 0xff) as u8]);
    } else {
        out.extend_from_slice(&[
            0x83,
            (len >> 16) as u8,
            (len >> 8) as u8,
            (len & 0xff) as u8,
        ]);
    }
}

/// Wrap `body` in a TLV with the given `tag` byte.
fn der_wrap(tag: u8, body: &[u8]) -> Vec<u8> {
    let mut out = Vec::with_capacity(1 + 4 + body.len());
    out.push(tag);
    der_push_len(&mut out, body.len());
    out.extend_from_slice(body);
    out
}

/// Wrap `body` in a DER SEQUENCE (tag 0x30).
pub(crate) fn der_wrap_sequence(body: Vec<u8>) -> Vec<u8> {
    der_wrap(0x30, &body)
}

/// Wrap `content` in a context-specific EXPLICIT constructed tag `[num]`.
pub(crate) fn der_explicit_ctx(num: u8, content: &[u8]) -> Vec<u8> {
    der_wrap(0xA0 | num, content)
}

/// Encode `content` as a DER OCTET STRING.
pub(crate) fn der_octet_string(content: &[u8]) -> Vec<u8> {
    der_wrap(0x04, content)
}

/// Encode an OID component slice as a DER OID TLV.
///
/// Returns an error string on invalid components (should not happen for
/// hard-coded known OIDs).
pub(crate) fn der_oid_tlv(components: &[u32]) -> Result<Vec<u8>, String> {
    let oid = ObjectIdentifier::new(components)
        .map_err(|e| format!("invalid OID {:?}: {e}", components))?;
    oid.to_der()
        .map_err(|e| format!("OID encode failed: {e:?}"))
}