synta-certificate 0.2.6

X.509 certificate structures for synta ASN.1 library
Documentation
//! PKCS#12 archive encryptor + PBES2/KDF helpers.

use crate::crypto::Pkcs12Encryptor;
use crate::pkcs12_types::{
    MacData, MacDigestInfo, Pbes2Params, Pbkdf2Params, ID_AES128_CBC, ID_AES192_CBC, ID_AES256_CBC,
    ID_HMAC_WITH_SHA256, ID_HMAC_WITH_SHA384, ID_HMAC_WITH_SHA512, ID_PBES2, ID_PBKDF2,
};

use super::cms::{build_alg_id_der, OpensslEncryptorError};

use native_ossl::cipher::CipherAlg;
use native_ossl::digest::DigestAlg;
use native_ossl::kdf::Pbkdf2Builder;
use native_ossl::mac::HmacCtx;
use native_ossl::rand::Rand;

/// Symmetric cipher for PKCS#12 private key encryption (PBES2, RFC 8018 §6.2).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Pkcs12Cipher {
    /// AES-128-CBC (128-bit key, 16-byte IV).
    Aes128Cbc,
    /// AES-192-CBC (192-bit key, 16-byte IV).
    Aes192Cbc,
    /// AES-256-CBC (256-bit key, 16-byte IV) — recommended.
    Aes256Cbc,
}

impl Pkcs12Cipher {
    fn alg_name(self) -> &'static std::ffi::CStr {
        match self {
            Self::Aes128Cbc => c"AES-128-CBC",
            Self::Aes192Cbc => c"AES-192-CBC",
            Self::Aes256Cbc => c"AES-256-CBC",
        }
    }

    fn key_len(self) -> usize {
        match self {
            Self::Aes128Cbc => 16,
            Self::Aes192Cbc => 24,
            Self::Aes256Cbc => 32,
        }
    }

    fn oid(self) -> &'static [u32] {
        match self {
            Self::Aes128Cbc => ID_AES128_CBC,
            Self::Aes192Cbc => ID_AES192_CBC,
            Self::Aes256Cbc => ID_AES256_CBC,
        }
    }
}

/// HMAC variant for PBKDF2 PRF and MAC computation.
///
/// Controls the hash used by PBKDF2 for key derivation and the hash used for
/// the HMAC-based MAC in `MacData.mac.digestAlgorithm`.
/// RFC 9879 PBMAC1 support (with its own AlgorithmIdentifier) will be added
/// as a future variant.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Pkcs12HmacAlgorithm {
    /// HMAC-SHA-256 — recommended; 32-byte key / output.
    Sha256,
    /// HMAC-SHA-384 — 48-byte key / output.
    Sha384,
    /// HMAC-SHA-512 — 64-byte key / output.
    Sha512,
}

impl Pkcs12HmacAlgorithm {
    fn alg_name(self) -> &'static std::ffi::CStr {
        match self {
            Self::Sha256 => c"SHA2-256",
            Self::Sha384 => c"SHA2-384",
            Self::Sha512 => c"SHA2-512",
        }
    }

    fn output_len(self) -> usize {
        match self {
            Self::Sha256 => 32,
            Self::Sha384 => 48,
            Self::Sha512 => 64,
        }
    }

    fn oid(self) -> &'static [u32] {
        match self {
            Self::Sha256 => ID_HMAC_WITH_SHA256,
            Self::Sha384 => ID_HMAC_WITH_SHA384,
            Self::Sha512 => ID_HMAC_WITH_SHA512,
        }
    }

    // RFC 7292 MacData.mac is a DigestInfo; digestAlgorithm must carry the
    // SHA digest OID, not the hmacWithSHAxxx OID.  OpenSSL's pkcs12_gen_mac
    // looks up the hash by this OID and fails on the HMAC OID.
    fn digest_oid(self) -> &'static [u32] {
        match self {
            Self::Sha256 => crate::ID_SHA256,
            Self::Sha384 => crate::ID_SHA384,
            Self::Sha512 => crate::ID_SHA512,
        }
    }
}

/// Configuration for [`OpensslPkcs12Encryptor`].
///
/// Controls the algorithms and iteration counts used during PKCS#12 archive
/// creation.  The default configuration uses PBKDF2-SHA256 with AES-256-CBC
/// and 600,000 iterations throughout (NIST SP 800-63B minimum for SHA-256).
#[derive(Debug, Clone)]
pub struct Pkcs12Config {
    /// PBKDF2 iteration count for the private-key PBES2 encryption KDF.
    pub encryption_iterations: u32,
    /// PBKDF2 iteration count for the MAC key derivation.
    pub mac_iterations: u32,
    /// Symmetric cipher for private-key encryption.
    pub cipher: Pkcs12Cipher,
    /// HMAC algorithm used as the PBKDF2 PRF for key encryption.
    pub encryption_prf: Pkcs12HmacAlgorithm,
    /// HMAC algorithm used for the `MacData` integrity check and its PBKDF2 PRF.
    pub mac_algorithm: Pkcs12HmacAlgorithm,
}

impl Default for Pkcs12Config {
    fn default() -> Self {
        Self {
            encryption_iterations: 600_000,
            mac_iterations: 600_000,
            cipher: Pkcs12Cipher::Aes256Cbc,
            encryption_prf: Pkcs12HmacAlgorithm::Sha256,
            mac_algorithm: Pkcs12HmacAlgorithm::Sha256,
        }
    }
}

/// OpenSSL-backed PKCS#12 archive encryptor and MAC generator.
///
/// Implements [`Pkcs12Encryptor`] using PBES2 (RFC 8018 §6.2) with
/// PBKDF2 key derivation for the private key, and HMAC (also PBKDF2-derived)
/// for the `MacData` integrity check (RFC 7292 §4).
///
/// Algorithms and iteration counts are controlled via [`Pkcs12Config`].
///
/// # 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())?;
/// ```
pub struct OpensslPkcs12Encryptor {
    config: Pkcs12Config,
}

impl OpensslPkcs12Encryptor {
    /// Create an encryptor with default settings (PBKDF2-SHA256, AES-256-CBC,
    /// 600,000 iterations).
    pub fn new() -> Self {
        Self {
            config: Pkcs12Config::default(),
        }
    }

    /// Create an encryptor with a custom [`Pkcs12Config`].
    pub fn with_config(config: Pkcs12Config) -> Self {
        Self { config }
    }
}

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

impl Pkcs12Encryptor for OpensslPkcs12Encryptor {
    type Error = OpensslEncryptorError;

    fn encrypt(
        &self,
        plaintext: &[u8],
        password: &[u8],
    ) -> Result<(Vec<u8>, Vec<u8>), OpensslEncryptorError> {
        let cipher = CipherAlg::fetch(self.config.cipher.alg_name(), None)?;
        let key_len = self.config.cipher.key_len();
        let iv_len = cipher.iv_len();

        // Generate random KDF salt and IV.
        let kdf_salt = Rand::bytes(16)?;
        let iv = Rand::bytes(iv_len)?;

        // Derive encryption key via PBKDF2.
        let prf_md = DigestAlg::fetch(self.config.encryption_prf.alg_name(), None)?;
        let key = Pbkdf2Builder::new(&prf_md, password, &kdf_salt)
            .iterations(self.config.encryption_iterations)
            .derive_to_vec(key_len)?;

        // Encrypt plaintext.
        let mut ctx = cipher.encrypt(&key, &iv, None)?;
        let block = cipher.block_size();
        let mut ciphertext = vec![0u8; plaintext.len() + block];
        let n = ctx.update(plaintext, &mut ciphertext)?;
        let m = ctx.finalize(&mut ciphertext[n..])?;
        ciphertext.truncate(n + m);

        // Build PBES2 AlgorithmIdentifier using schema-generated types.
        let alg_id_der = build_pbes2_alg_id_der(
            &kdf_salt,
            &iv,
            self.config.encryption_iterations,
            key_len as u32,
            self.config.cipher.oid(),
            self.config.encryption_prf.oid(),
        )?;
        Ok((alg_id_der, ciphertext))
    }

    fn compute_mac(
        &self,
        auth_safe_content: &[u8],
        password: &[u8],
    ) -> Result<Vec<u8>, OpensslEncryptorError> {
        use crate::AlgorithmIdentifier;
        use synta::{Element, Null, ObjectIdentifier, OctetStringRef};

        let mac_alg = self.config.mac_algorithm;
        let mac_key_len = mac_alg.output_len();

        // Generate random MAC salt (8 bytes).
        let mac_salt = Rand::bytes(8)?;

        // Derive MAC key via PBKDF2.
        let mac_md = DigestAlg::fetch(mac_alg.alg_name(), None)?;
        let mac_key = Pbkdf2Builder::new(&mac_md, password, &mac_salt)
            .iterations(self.config.mac_iterations)
            .derive_to_vec(mac_key_len)?;

        // Compute HMAC over the AuthenticatedSafe bytes.
        let mut hmac_ctx = HmacCtx::new(&mac_md, &mac_key)?;
        hmac_ctx.update(auth_safe_content)?;
        let hmac_bytes = hmac_ctx.finish_to_vec()?;

        // Build MacDigestInfo: { digestAlgorithm: { shaOID, NULL }, digest }.
        // RFC 7292: digestAlgorithm carries the SHA digest OID, not hmacWithSHAxxx.
        let hmac_oid = ObjectIdentifier::new(mac_alg.digest_oid()).map_err(|_| {
            OpensslEncryptorError::UnsupportedAlgorithm(
                "invalid SHA digest OID components in Pkcs12HmacAlgorithm".into(),
            )
        })?;
        let dig_alg = AlgorithmIdentifier {
            algorithm: hmac_oid,
            parameters: Some(Element::Null(Null)),
        };
        let mac_digest_info = MacDigestInfo {
            digest_algorithm: dig_alg,
            digest: OctetStringRef::new(&hmac_bytes),
        };

        // Build MacData: { mac, macSalt, iterations }.
        let mac_data = MacData {
            mac: mac_digest_info,
            mac_salt: OctetStringRef::new(&mac_salt),
            iterations: synta::Integer::from(self.config.mac_iterations),
        };

        // Encode MacData to DER.
        Ok(mac_data.to_der()?)
    }
}

/// Build a DER-encoded PBES2 `AlgorithmIdentifier` for `EncryptedPrivateKeyInfo`.
///
/// Encodes:
/// ```text
/// AlgorithmIdentifier { id-PBES2,
///     PBES2-params {
///         keyDerivationFunc: AlgorithmIdentifier { id-PBKDF2,
///             PBKDF2-params { salt, iterations, keyLength,
///                             prf: { prfOid, NULL } }
///         },
///         encryptionScheme: AlgorithmIdentifier { cipherOid, OCTET STRING(iv) }
///     }
/// }
/// ```
///
/// Uses the generated `Pbkdf2Params` and `Pbes2Params` schema types for the
/// inner SEQUENCE structures; outer AlgorithmIdentifier wrappers use the
/// crate-root `AlgorithmIdentifier<'_>` type.
fn build_pbes2_alg_id_der(
    kdf_salt: &[u8],
    iv: &[u8],
    iterations: u32,
    key_len: u32,
    cipher_oid: &[u32],
    prf_oid: &[u32],
) -> Result<Vec<u8>, OpensslEncryptorError> {
    use crate::AlgorithmIdentifier;
    use synta::traits::Decode;
    use synta::{Element, Null, ObjectIdentifier, OctetStringRef, RawDer};

    // ── Step 1: PRF AlgorithmIdentifier { prfOid, NULL } ─────────────────────
    let prf_alg_der = {
        let oid = ObjectIdentifier::new(prf_oid).map_err(|_| {
            OpensslEncryptorError::UnsupportedAlgorithm("invalid PRF OID components".into())
        })?;
        AlgorithmIdentifier {
            algorithm: oid,
            parameters: Some(Element::Null(Null)),
        }
        .to_der()?
    };

    // ── Step 2: PBKDF2-params SEQUENCE (via generated schema type) ────────────
    let pbkdf2_params_der = Pbkdf2Params {
        salt: OctetStringRef::new(kdf_salt),
        iteration_count: synta::Integer::from(iterations),
        key_length: Some(synta::Integer::from(key_len)),
        prf: Some(RawDer(&prf_alg_der)),
    }
    .to_der()?;

    // ── Step 3: PBKDF2 AlgorithmIdentifier { id-PBKDF2, PBKDF2-params } ──────
    let kdf_alg_der = {
        let oid = ObjectIdentifier::new(ID_PBKDF2).map_err(|_| {
            OpensslEncryptorError::UnsupportedAlgorithm("invalid id-PBKDF2 OID".into())
        })?;
        // Workaround: `AlgorithmIdentifier.parameters` is typed as `Element<'_>`, which
        // borrows from a decoder's lifetime.  To embed the already-encoded PBKDF2-params
        // bytes as the parameters value we round-trip them through a Decoder so we can
        // hold the `Element` with a lifetime tied to the encoded bytes.  A typed
        // `SEQUENCE { OID, params_type }` struct would avoid this but does not currently
        // exist in the generated schema.
        let mut dec = synta::Decoder::new(&pbkdf2_params_der, synta::Encoding::Der);
        let params_elem: Element<'_> = Element::decode(&mut dec)?;
        AlgorithmIdentifier {
            algorithm: oid,
            parameters: Some(params_elem),
        }
        .to_der()?
    };

    // ── Step 4: Cipher AlgorithmIdentifier { cipherOid, OCTET STRING(iv) } ───
    let enc_alg_der = build_alg_id_der(cipher_oid, iv)?;

    // ── Step 5: PBES2-params SEQUENCE (via generated schema type) ────────────
    let pbes2_params_der = Pbes2Params {
        key_derivation_func: RawDer(&kdf_alg_der),
        encryption_scheme: RawDer(&enc_alg_der),
    }
    .to_der()?;

    // ── Step 6: Outer PBES2 AlgorithmIdentifier { id-PBES2, PBES2-params } ───
    let oid = ObjectIdentifier::new(ID_PBES2)
        .map_err(|_| OpensslEncryptorError::UnsupportedAlgorithm("invalid id-PBES2 OID".into()))?;
    let mut dec = synta::Decoder::new(&pbes2_params_der, synta::Encoding::Der);
    let params_elem: Element<'_> = Element::decode(&mut dec)?;
    Ok(AlgorithmIdentifier {
        algorithm: oid,
        parameters: Some(params_elem),
    }
    .to_der()?)
}