synta-certificate 0.2.6

X.509 certificate structures for synta ASN.1 library
Documentation
//! NSS-backed RSA-OAEP and PKCS#1 v1.5 key encryption/decryption.
//!
//! Implements `PK11_PubEncrypt` (RSA public-key encryption) and
//! `PK11_PrivDecrypt` (RSA private-key decryption) using the NSS PKCS#11
//! softokn.
//!
//! The public key is imported from SPKI DER via
//! `SECKEY_DecodeDERSubjectPublicKeyInfo` + `SECKEY_ExtractPublicKey`.
//! The private key is imported from PKCS#8 DER via
//! `PK11_ImportDERPrivateKeyInfoAndReturnKey` with `KU_KEY_ENCIPHERMENT`.

use std::ptr;

use nss_sys::nspr::{PR_FALSE, PR_TRUE};
use nss_sys::{SECItemStr, SECItemType, SECStatus};

use super::ensure_nss_init;
use super::ffi::{
    CKMechanismType, CKRsaPkcsMgfType, CkRsaPkcsOaepParams, PK11_FreeSlot, PK11_GetInternalSlot,
    PK11_ImportDERPrivateKeyInfoAndReturnKey, PK11_PrivDecrypt, PK11_PubEncrypt,
    SECKEYPrivateKeyStr, SECKEYPublicKeyStr, SECKEY_DecodeDERSubjectPublicKeyInfo,
    SECKEY_DestroyPrivateKey, SECKEY_DestroyPublicKey, SECKEY_DestroySubjectPublicKeyInfo,
    SECKEY_ExtractPublicKey, SECKEY_PublicKeyStrength, CKG_MGF1_SHA1, CKG_MGF1_SHA224,
    CKG_MGF1_SHA256, CKG_MGF1_SHA384, CKG_MGF1_SHA512, CKM_RSA_PKCS, CKM_RSA_PKCS_OAEP, CKM_SHA224,
    CKM_SHA256, CKM_SHA384, CKM_SHA512, CKM_SHA_1, CKZ_DATA_SPECIFIED, KU_KEY_ENCIPHERMENT,
};

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

/// Error returned by NSS RSA encryption/decryption functions.
#[derive(Debug)]
pub struct NssRsaError(pub(crate) String);

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

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

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

/// Map a hash algorithm name to the PKCS#11 mechanism type and MGF1 type.
fn hash_alg_to_ckm(alg: &str) -> Result<(CKMechanismType, CKRsaPkcsMgfType), NssRsaError> {
    match alg {
        "sha1" => Ok((CKM_SHA_1, CKG_MGF1_SHA1)),
        "sha224" => Ok((CKM_SHA224, CKG_MGF1_SHA224)),
        "sha256" => Ok((CKM_SHA256, CKG_MGF1_SHA256)),
        "sha384" => Ok((CKM_SHA384, CKG_MGF1_SHA384)),
        "sha512" => Ok((CKM_SHA512, CKG_MGF1_SHA512)),
        other => Err(NssRsaError(format!(
            "unsupported hash algorithm for RSA-OAEP: {other}; \
             expected sha1, sha224, sha256, sha384, or sha512"
        ))),
    }
}

/// Import a public key from SPKI DER.
///
/// Returns a non-null `SECKEYPublicKey *` on success.  The caller is
/// responsible for calling `SECKEY_DestroyPublicKey` when done.
fn import_public_key(spki_der: &[u8]) -> Result<*mut SECKEYPublicKeyStr, NssRsaError> {
    let spki_item = SECItemStr {
        type_: SECItemType::siBuffer,
        data: spki_der.as_ptr() as *mut _,
        len: spki_der.len() as u32,
    };

    // SAFETY: spki_item.data points into spki_der which lives for this call.
    // SECKEY_DecodeDERSubjectPublicKeyInfo makes its own internal copy.
    let spki_info = unsafe { SECKEY_DecodeDERSubjectPublicKeyInfo(&spki_item) };
    if spki_info.is_null() {
        return Err(NssRsaError(
            "SECKEY_DecodeDERSubjectPublicKeyInfo failed: cannot decode SPKI DER".to_string(),
        ));
    }

    // SAFETY: spki_info is non-null.
    let pub_key = unsafe { SECKEY_ExtractPublicKey(spki_info) };
    // SAFETY: spki_info is non-null.
    unsafe { SECKEY_DestroySubjectPublicKeyInfo(spki_info) };

    if pub_key.is_null() {
        return Err(NssRsaError(
            "SECKEY_ExtractPublicKey failed: cannot extract public key from SPKI".to_string(),
        ));
    }
    Ok(pub_key)
}

/// Import an RSA private key from PKCS#8 DER into the NSS internal slot.
///
/// Returns a non-null `SECKEYPrivateKey *` on success.  The caller is
/// responsible for calling `SECKEY_DestroyPrivateKey` when done.
fn import_private_key(pkcs8_der: &[u8]) -> Result<*mut SECKEYPrivateKeyStr, NssRsaError> {
    // SAFETY: PK11_GetInternalSlot is thread-safe after NSS_NoDB_Init.
    let slot = unsafe { PK11_GetInternalSlot() };
    if slot.is_null() {
        return Err(NssRsaError("PK11_GetInternalSlot failed".to_string()));
    }

    let pkcs8_item = SECItemStr {
        type_: SECItemType::siBuffer,
        data: pkcs8_der.as_ptr() as *mut _,
        len: pkcs8_der.len() as u32,
    };

    let mut priv_key: *mut SECKEYPrivateKeyStr = ptr::null_mut();

    // SAFETY: slot is non-null; pkcs8_item.data points into pkcs8_der which
    // lives for this call; nickname and publicValue are NULL (not needed for a
    // temporary session key); privk receives the result.
    let status = unsafe {
        PK11_ImportDERPrivateKeyInfoAndReturnKey(
            slot,
            &pkcs8_item,
            ptr::null(), // no nickname
            ptr::null(), // no pre-computed publicValue
            PR_FALSE,    // isPerm: session key, not permanent
            PR_TRUE,     // isPrivate: treat as private key
            KU_KEY_ENCIPHERMENT,
            &mut priv_key,
            ptr::null_mut(),
        )
    };

    // Release slot reference immediately — the key holds its own reference.
    // SAFETY: slot is non-null.
    unsafe { PK11_FreeSlot(slot) };

    if status != SECStatus::SECSuccess || priv_key.is_null() {
        return Err(NssRsaError(
            "PK11_ImportDERPrivateKeyInfoAndReturnKey failed: \
             cannot import private key for RSA decryption"
                .to_string(),
        ));
    }
    Ok(priv_key)
}

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

/// RSA-OAEP encryption using the NSS PKCS#11 softokn.
///
/// `spki_der` is the DER-encoded `SubjectPublicKeyInfo` for an RSA public key.
/// `hash_alg` is the OAEP hash algorithm name (`"sha1"`, `"sha256"`, etc.).
pub(crate) fn nss_rsa_oaep_encrypt(
    spki_der: &[u8],
    plaintext: &[u8],
    hash_alg: &str,
) -> Result<Vec<u8>, NssRsaError> {
    if !ensure_nss_init() {
        return Err(NssRsaError("NSS initialisation failed".to_string()));
    }

    let (hash_ckm, mgf_ckm) = hash_alg_to_ckm(hash_alg)?;
    let pub_key = import_public_key(spki_der)?;

    // Allocate output buffer = RSA key modulus size in bytes.
    // SAFETY: pub_key is non-null.
    let key_len = unsafe { SECKEY_PublicKeyStrength(pub_key) } as usize;
    let mut out_buf = vec![0u8; key_len];
    let mut out_len: std::ffi::c_uint = key_len as std::ffi::c_uint;

    let mut oaep_params = CkRsaPkcsOaepParams {
        hash_alg: hash_ckm,
        mgf: mgf_ckm,
        source: CKZ_DATA_SPECIFIED,
        p_source_data: ptr::null_mut(),
        ul_source_data_len: 0,
    };
    let mut param_item = SECItemStr {
        type_: SECItemType::siBuffer,
        data: &mut oaep_params as *mut _ as *mut u8,
        len: std::mem::size_of::<CkRsaPkcsOaepParams>() as u32,
    };

    // SAFETY: pub_key, param_item, and out_buf are valid for this call.
    let status = unsafe {
        PK11_PubEncrypt(
            pub_key,
            CKM_RSA_PKCS_OAEP,
            &mut param_item,
            out_buf.as_mut_ptr(),
            &mut out_len,
            out_buf.len() as std::ffi::c_uint,
            plaintext.as_ptr(),
            plaintext.len() as std::ffi::c_uint,
            ptr::null_mut(),
        )
    };

    // SAFETY: pub_key is non-null.
    unsafe { SECKEY_DestroyPublicKey(pub_key) };

    if status != SECStatus::SECSuccess {
        return Err(NssRsaError("PK11_PubEncrypt (RSA-OAEP) failed".to_string()));
    }

    out_buf.truncate(out_len as usize);
    Ok(out_buf)
}

/// RSA PKCS#1 v1.5 encryption using the NSS PKCS#11 softokn.
///
/// `spki_der` is the DER-encoded `SubjectPublicKeyInfo` for an RSA public key.
pub(crate) fn nss_rsa_pkcs1v15_encrypt(
    spki_der: &[u8],
    plaintext: &[u8],
) -> Result<Vec<u8>, NssRsaError> {
    if !ensure_nss_init() {
        return Err(NssRsaError("NSS initialisation failed".to_string()));
    }

    let pub_key = import_public_key(spki_der)?;

    // SAFETY: pub_key is non-null.
    let key_len = unsafe { SECKEY_PublicKeyStrength(pub_key) } as usize;
    let mut out_buf = vec![0u8; key_len];
    let mut out_len: std::ffi::c_uint = key_len as std::ffi::c_uint;

    // SAFETY: pub_key is non-null; out_buf is pre-allocated to key_len bytes.
    let status = unsafe {
        PK11_PubEncrypt(
            pub_key,
            CKM_RSA_PKCS,
            ptr::null_mut(), // no param for PKCS#1 v1.5
            out_buf.as_mut_ptr(),
            &mut out_len,
            out_buf.len() as std::ffi::c_uint,
            plaintext.as_ptr(),
            plaintext.len() as std::ffi::c_uint,
            ptr::null_mut(),
        )
    };

    // SAFETY: pub_key is non-null.
    unsafe { SECKEY_DestroyPublicKey(pub_key) };

    if status != SECStatus::SECSuccess {
        return Err(NssRsaError(
            "PK11_PubEncrypt (RSA PKCS#1 v1.5) failed".to_string(),
        ));
    }

    out_buf.truncate(out_len as usize);
    Ok(out_buf)
}

/// RSA-OAEP decryption using the NSS PKCS#11 softokn.
///
/// `pkcs8_der` is the DER-encoded PKCS#8 `PrivateKeyInfo` for an RSA private key.
/// `hash_alg` is the OAEP hash algorithm used during encryption.
pub(crate) fn nss_rsa_oaep_decrypt(
    pkcs8_der: &[u8],
    ciphertext: &[u8],
    hash_alg: &str,
) -> Result<Vec<u8>, NssRsaError> {
    if !ensure_nss_init() {
        return Err(NssRsaError("NSS initialisation failed".to_string()));
    }

    let (hash_ckm, mgf_ckm) = hash_alg_to_ckm(hash_alg)?;
    let priv_key = import_private_key(pkcs8_der)?;

    // Output buffer: ciphertext length is the RSA key size; plaintext is smaller.
    let mut out_buf = vec![0u8; ciphertext.len()];
    let mut out_len: std::ffi::c_uint = ciphertext.len() as std::ffi::c_uint;

    let mut oaep_params = CkRsaPkcsOaepParams {
        hash_alg: hash_ckm,
        mgf: mgf_ckm,
        source: CKZ_DATA_SPECIFIED,
        p_source_data: ptr::null_mut(),
        ul_source_data_len: 0,
    };
    let mut param_item = SECItemStr {
        type_: SECItemType::siBuffer,
        data: &mut oaep_params as *mut _ as *mut u8,
        len: std::mem::size_of::<CkRsaPkcsOaepParams>() as u32,
    };

    // SAFETY: priv_key and param_item are valid for this call.
    let status = unsafe {
        PK11_PrivDecrypt(
            priv_key,
            CKM_RSA_PKCS_OAEP,
            &mut param_item,
            out_buf.as_mut_ptr(),
            &mut out_len,
            out_buf.len() as std::ffi::c_uint,
            ciphertext.as_ptr(),
            ciphertext.len() as std::ffi::c_uint,
        )
    };

    // SAFETY: priv_key is non-null.
    unsafe { SECKEY_DestroyPrivateKey(priv_key) };

    if status != SECStatus::SECSuccess {
        return Err(NssRsaError(
            "PK11_PrivDecrypt (RSA-OAEP) failed".to_string(),
        ));
    }

    out_buf.truncate(out_len as usize);
    Ok(out_buf)
}

/// RSA PKCS#1 v1.5 decryption using the NSS PKCS#11 softokn.
///
/// `pkcs8_der` is the DER-encoded PKCS#8 `PrivateKeyInfo` for an RSA private key.
pub(crate) fn nss_rsa_pkcs1v15_decrypt(
    pkcs8_der: &[u8],
    ciphertext: &[u8],
) -> Result<Vec<u8>, NssRsaError> {
    if !ensure_nss_init() {
        return Err(NssRsaError("NSS initialisation failed".to_string()));
    }

    let priv_key = import_private_key(pkcs8_der)?;

    let mut out_buf = vec![0u8; ciphertext.len()];
    let mut out_len: std::ffi::c_uint = ciphertext.len() as std::ffi::c_uint;

    // SAFETY: priv_key is non-null; out_buf is sized to ciphertext.len().
    let status = unsafe {
        PK11_PrivDecrypt(
            priv_key,
            CKM_RSA_PKCS,
            ptr::null_mut(),
            out_buf.as_mut_ptr(),
            &mut out_len,
            out_buf.len() as std::ffi::c_uint,
            ciphertext.as_ptr(),
            ciphertext.len() as std::ffi::c_uint,
        )
    };

    // SAFETY: priv_key is non-null.
    unsafe { SECKEY_DestroyPrivateKey(priv_key) };

    if status != SECStatus::SECSuccess {
        return Err(NssRsaError(
            "PK11_PrivDecrypt (RSA PKCS#1 v1.5) failed".to_string(),
        ));
    }

    out_buf.truncate(out_len as usize);
    Ok(out_buf)
}