synta-certificate 0.2.6

X.509 certificate structures for synta ASN.1 library
Documentation
//! Certificate signing backed by NSS (Network Security Services).
//!
//! Imports the PKCS#8 private key into the NSS internal in-memory slot via
//! `PK11_ImportDERPrivateKeyInfoAndReturnKey` and dispatches to the correct
//! NSS signing primitive:
//!
//! - **RSA PKCS#1 v1.5, ECDSA, ML-DSA**: `SEC_SignData` (hash-then-sign).
//!   Supports SHA-256, SHA-384, SHA-512.  ML-DSA requires NSS ≥ 3.103.
//! - **Ed25519**: `PK11_Sign` with the raw TBS message.  `CKM_EDDSA` on the
//!   NSS softokn performs the hash and sign atomically.  `SEC_SignData` cannot
//!   be used for EdDSA: NSS's `SGN_Update` does not support multi-part
//!   `CKM_EDDSA` operations and returns `SECFailure`.
//!
//! Ed448 is not supported (no `SEC_OID_ED448_SIGNATURE` in NSS ≤ 3.121).
//! [`NssSigner::new`] returns `None` for Ed448.

use std::ptr;

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

use super::ensure_nss_init;
use crate::crypto::utils::split_alg_id;
use crate::crypto::{ErasedCertificateSigner, PrivateKeyError};
use crate::oids;

// ── Shared NSS types and FFI (key types, signing primitives) ─────────────────

use super::ffi::{
    PK11_FreeSlot, PK11_GetInternalSlot, PK11_ImportDERPrivateKeyInfoAndReturnKey, PK11_Sign,
    PK11_SignatureLen, SECITEM_FreeItem, SECKEYPrivateKeyStr, SECKEY_DestroyPrivateKey, SECOidTag,
    SEC_SignData, KU_DIGITAL_SIGNATURE, SEC_OID_ANSIX962_ECDSA_SHA256_SIGNATURE,
    SEC_OID_ANSIX962_ECDSA_SHA384_SIGNATURE, SEC_OID_ANSIX962_ECDSA_SHA512_SIGNATURE,
    SEC_OID_ED25519_SIGNATURE, SEC_OID_ML_DSA_44, SEC_OID_ML_DSA_65, SEC_OID_ML_DSA_87,
    SEC_OID_PKCS1_SHA256_WITH_RSA_ENCRYPTION, SEC_OID_PKCS1_SHA384_WITH_RSA_ENCRYPTION,
    SEC_OID_PKCS1_SHA512_WITH_RSA_ENCRYPTION,
};

// ── NssSignerError ────────────────────────────────────────────────────────────

/// Error type for [`NssSigner`].
#[derive(Debug)]
pub struct NssSignerError(pub(crate) String);

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

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

// ── OID component → NSS tag mapping ──────────────────────────────────────────

/// Map a signing algorithm OID (as `&[u32]` component array, matching
/// `ObjectIdentifier::components()`) to the corresponding NSS `SECOidTag`.
///
/// Returns `None` for algorithms not supported by the NSS signing path
/// (e.g. Ed448, which has no `SEC_OID_ED448_SIGNATURE` in NSS ≤ 3.121).
///
/// Ed25519 maps to `SEC_OID_ED25519_SIGNATURE` and is signed via `PK11_Sign`
/// (not `SEC_SignData`) — the tag is used only to detect the EdDSA path and
/// to build the `AlgorithmIdentifier` DER, not as an argument to `SGN_Digest`.
fn oid_components_to_nss_tag(components: &[u32]) -> Option<SECOidTag> {
    match components {
        c if c == oids::ECDSA_WITH_SHA256 => Some(SEC_OID_ANSIX962_ECDSA_SHA256_SIGNATURE),
        c if c == oids::ECDSA_WITH_SHA384 => Some(SEC_OID_ANSIX962_ECDSA_SHA384_SIGNATURE),
        c if c == oids::ECDSA_WITH_SHA512 => Some(SEC_OID_ANSIX962_ECDSA_SHA512_SIGNATURE),
        c if c == oids::SHA256_WITH_RSA => Some(SEC_OID_PKCS1_SHA256_WITH_RSA_ENCRYPTION),
        c if c == oids::SHA384_WITH_RSA => Some(SEC_OID_PKCS1_SHA384_WITH_RSA_ENCRYPTION),
        c if c == oids::SHA512_WITH_RSA => Some(SEC_OID_PKCS1_SHA512_WITH_RSA_ENCRYPTION),
        c if c == oids::ED25519 => Some(SEC_OID_ED25519_SIGNATURE),
        c if c == oids::ML_DSA_44 => Some(SEC_OID_ML_DSA_44),
        c if c == oids::ML_DSA_65 => Some(SEC_OID_ML_DSA_65),
        c if c == oids::ML_DSA_87 => Some(SEC_OID_ML_DSA_87),
        _ => None, // Ed448 and unknown algorithms
    }
}

// ── Core signing function ─────────────────────────────────────────────────────

/// Sign `tbs_der` with the PKCS#8 private key `pkcs8_der`, using the NSS
/// algorithm OID tag `nss_alg_tag`.
///
/// The key is imported as a temporary session object into the NSS internal
/// in-memory slot (`PK11_GetInternalSlot`), signed, and destroyed immediately.
pub(super) fn do_nss_sign(
    pkcs8_der: &[u8],
    tbs_der: &[u8],
    nss_alg_tag: SECOidTag,
) -> Result<Vec<u8>, NssSignerError> {
    if !ensure_nss_init() {
        return Err(NssSignerError("NSS initialisation failed".to_string()));
    }

    // ── Import private key into the NSS internal in-memory slot ──────────────

    // SAFETY: PK11_GetInternalSlot is thread-safe after NSS_NoDB_Init returns.
    let slot = unsafe { PK11_GetInternalSlot() };
    if slot.is_null() {
        return Err(NssSignerError("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 valid; pkcs8_item.data points into pkcs8_der which lives
    // for the duration of this call; nickname and publicValue are NULL (not
    // needed for a temporary session key); privk receives the output pointer.
    let import_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_DIGITAL_SIGNATURE,
            &mut priv_key,
            ptr::null_mut(), // no PIN context
        )
    };

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

    if import_status != SECStatus::SECSuccess || priv_key.is_null() {
        return Err(NssSignerError(
            "PK11_ImportDERPrivateKeyInfoAndReturnKey failed: \
             cannot import private key into NSS internal slot"
                .to_string(),
        ));
    }

    // ── Sign the TBS DER ──────────────────────────────────────────────────────

    let sig = if nss_alg_tag == SEC_OID_ED25519_SIGNATURE {
        // Ed25519 path: `PK11_Sign` passes the raw TBS message to the PKCS#11
        // softokn (`CKM_EDDSA`), which hashes and signs atomically.
        //
        // `SEC_SignData` cannot be used for EdDSA: NSS's `SGN_Update` does not
        // support multi-part `CKM_EDDSA` operations, so it returns SECFailure.
        //
        // `PK11_Sign` requires a caller-allocated output buffer; query the size
        // with `PK11_SignatureLen` first, then truncate to the actual length
        // that NSS writes into `sig_item.len`.
        let sig_len = unsafe { PK11_SignatureLen(priv_key) };
        if sig_len <= 0 {
            unsafe { SECKEY_DestroyPrivateKey(priv_key) };
            return Err(NssSignerError(
                "PK11_SignatureLen returned non-positive value".to_string(),
            ));
        }
        let mut sig_buf = vec![0u8; sig_len as usize];

        let msg_item = SECItemStr {
            type_: SECItemType::siBuffer,
            data: tbs_der.as_ptr() as *mut _,
            len: tbs_der.len() as u32,
        };
        let mut sig_item = SECItemStr {
            type_: SECItemType::siBuffer,
            data: sig_buf.as_mut_ptr(),
            len: sig_len as u32,
        };

        // SAFETY: priv_key is non-null; msg_item points into tbs_der which
        // outlives this call; sig_buf is pre-allocated to PK11_SignatureLen bytes.
        let sign_status = unsafe { PK11_Sign(priv_key, &mut sig_item, &msg_item) };

        // SAFETY: priv_key is non-null and was returned by the import call.
        unsafe { SECKEY_DestroyPrivateKey(priv_key) };

        if sign_status != SECStatus::SECSuccess {
            return Err(NssSignerError("PK11_Sign (Ed25519) failed".to_string()));
        }

        // Truncate to the actual bytes written (NSS updates sig_item.len).
        sig_buf.truncate(sig_item.len as usize);
        sig_buf
    } else {
        // Hash-then-sign path: RSA PKCS#1 v1.5, ECDSA, ML-DSA.
        // SEC_SignData allocates sig_item.data; free with SECITEM_FreeItem.
        let mut sig_item = SECItemStr {
            type_: SECItemType::siBuffer,
            data: ptr::null_mut(),
            len: 0,
        };

        // SAFETY: tbs_der is a valid slice for the duration of this call;
        // priv_key is non-null; sig_item receives the NSS-allocated signature buffer.
        let sign_status = unsafe {
            SEC_SignData(
                &mut sig_item,
                tbs_der.as_ptr(),
                tbs_der.len() as std::ffi::c_int,
                priv_key,
                nss_alg_tag,
            )
        };

        // SAFETY: priv_key is non-null and was returned by the import call.
        unsafe { SECKEY_DestroyPrivateKey(priv_key) };

        if sign_status != SECStatus::SECSuccess {
            return Err(NssSignerError("SEC_SignData failed".to_string()));
        }

        // Copy the signature bytes out of the NSS-allocated buffer.
        // SAFETY: sig_item.data is non-null and sig_item.len bytes are valid after
        // a successful SEC_SignData call.
        let sig =
            unsafe { std::slice::from_raw_parts(sig_item.data, sig_item.len as usize).to_vec() };

        // SAFETY: PR_FALSE frees only .data, not the stack-allocated SECItemStr.
        unsafe { SECITEM_FreeItem(&mut sig_item, PR_FALSE) };
        sig
    };

    Ok(sig)
}

// ── NssSigner ─────────────────────────────────────────────────────────────────

/// NSS-backed certificate signer.
///
/// Holds the PKCS#8 DER private key, the pre-computed `AlgorithmIdentifier`
/// DER, and the NSS algorithm OID tag.  On each `sign_tbs_erased` call it
/// imports the key into the NSS internal in-memory slot, signs via the
/// appropriate NSS primitive (`PK11_Sign` for Ed25519, `SEC_SignData` for all
/// others), and destroys the temporary key object.
pub struct NssSigner {
    pkcs8_der: Vec<u8>,
    sig_alg_der: Vec<u8>,
    nss_alg_tag: SECOidTag,
}

impl NssSigner {
    /// Build an `NssSigner` from a PKCS#8 DER key and its pre-computed
    /// `AlgorithmIdentifier` DER.
    ///
    /// Returns `None` if the signing algorithm is not supported (e.g. Ed448,
    /// which has no `SEC_OID_ED448_SIGNATURE` in NSS ≤ 3.121).
    pub fn new(pkcs8_der: Vec<u8>, sig_alg_der: Vec<u8>) -> Option<Self> {
        // Parse the AlgorithmIdentifier to get the OID components for dispatch.
        let (oid, _, _) = split_alg_id(&sig_alg_der, |_| ()).ok()?;
        let nss_alg_tag = oid_components_to_nss_tag(oid.components())?;
        Some(Self {
            pkcs8_der,
            sig_alg_der,
            nss_alg_tag,
        })
    }
}

impl ErasedCertificateSigner for NssSigner {
    fn signature_algorithm_der_erased(&self) -> Result<Vec<u8>, PrivateKeyError> {
        Ok(self.sig_alg_der.clone())
    }

    fn sign_tbs_erased(&self, tbs_der: &[u8]) -> Result<Vec<u8>, PrivateKeyError> {
        do_nss_sign(&self.pkcs8_der, tbs_der, self.nss_alg_tag).map_err(PrivateKeyError::new)
    }
}

// ── NssUnsupportedSigner ──────────────────────────────────────────────────────

/// Fallback [`ErasedCertificateSigner`] returned by
/// [`crate::crypto::PrivateKey::as_signer`] when the NSS backend is active but
/// does not support the requested signing algorithm (e.g. Ed448, which has no
/// `SEC_OID_ED448_SIGNATURE` in NSS ≤ 3.121).
///
/// All methods return an error wrapping [`NssSignerError`].
pub(crate) struct NssUnsupportedSigner;

impl ErasedCertificateSigner for NssUnsupportedSigner {
    fn signature_algorithm_der_erased(&self) -> Result<Vec<u8>, PrivateKeyError> {
        Err(PrivateKeyError::new(NssSignerError(
            "signing algorithm not supported by the NSS backend".to_string(),
        )))
    }

    fn sign_tbs_erased(&self, _tbs_der: &[u8]) -> Result<Vec<u8>, PrivateKeyError> {
        Err(PrivateKeyError::new(NssSignerError(
            "signing algorithm not supported by the NSS backend".to_string(),
        )))
    }
}