purecrypto 0.6.29

A pure-Rust cryptography toolkit with no foreign-code dependencies, from constant-time primitives up to keys, X.509 and TLS.
Documentation
//! C ABI for X.509 CSR (PKCS#10) creation, parse, verify.

use alloc::boxed::Box;
use alloc::vec::Vec;

use super::common::{PcStatus, guard, out_write, slice};
use super::rsa::PcRsaKey;
use crate::x509::{CertSigner, CertificationRequest, DistinguishedName};

/// An opaque CSR.
pub struct PcCsr(CertificationRequest);

/// Creates a CSR signed by `rsa_key`, with `subject_cn` as the only subject
/// attribute (CN). `dns_names`/`dns_count` are optional DNS subjectAltName
/// extension requests. Returns NULL on failure.
///
/// # Safety
/// `rsa_key` valid; `subject_cn` a NUL-terminated UTF-8 string; `dns_names`
/// points to `dns_count` NUL-terminated UTF-8 C strings (or NULL when
/// `dns_count == 0`).
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_csr_create_rsa(
    rsa_key: *const PcRsaKey,
    subject_cn: *const core::ffi::c_char,
    dns_names: *const *const core::ffi::c_char,
    dns_count: usize,
) -> *mut PcCsr {
    crate::ffi::common::guard_ptr(|| {
        if rsa_key.is_null() || subject_cn.is_null() {
            return core::ptr::null_mut();
        }
        let cn = unsafe { core::ffi::CStr::from_ptr(subject_cn) };
        let Ok(cn) = cn.to_str() else {
            return core::ptr::null_mut();
        };
        // Cap the caller-controlled count BEFORE reserving so a garbage
        // `dns_count` can't drive `Vec::with_capacity` into
        // `handle_alloc_error` (an abort the panic guard can't catch).
        // 1024 SAN entries comfortably exceeds any realistic request.
        const PC_DNS_MAX: usize = 1024;
        if dns_count > PC_DNS_MAX {
            return core::ptr::null_mut();
        }
        if dns_count > 0 && dns_names.is_null() {
            return core::ptr::null_mut();
        }
        let mut dns: Vec<&str> = Vec::with_capacity(dns_count);
        for i in 0..dns_count {
            let p = unsafe { *dns_names.add(i) };
            if p.is_null() {
                return core::ptr::null_mut();
            }
            let s = unsafe { core::ffi::CStr::from_ptr(p) };
            let Ok(s) = s.to_str() else {
                return core::ptr::null_mut();
            };
            dns.push(s);
        }
        let subject = DistinguishedName::common_name(cn);
        // SAFETY: PcRsaKey is allocated by Box::into_raw, so we can borrow
        // the inner key through a shared reference. The internal field
        // `key` is private; expose it via a small accessor.
        let key_ref = unsafe { rsa_key_inner(&*rsa_key) };
        let signer = CertSigner::Rsa(key_ref);
        match CertificationRequest::create(&signer, &subject, &dns) {
            Ok(csr) => Box::into_raw(Box::new(PcCsr(csr))),
            Err(_) => core::ptr::null_mut(),
        }
    })
}

/// Helper that exposes the borrow of the inner BoxedRsaPrivateKey. Defined
/// here (rather than in `rsa.rs`) so the public field need not be widened.
fn rsa_key_inner(k: &PcRsaKey) -> &crate::rsa::BoxedRsaPrivateKey {
    k.key()
}

// Provide a small accessor on PcRsaKey. Done via an inherent impl below in
// rsa.rs is awkward — instead, declare the accessor in rsa.rs's
// `impl PcRsaKey`. We forward to it here. To avoid re-declaring it twice,
// gate on a method we add to PcRsaKey via an extension trait.
trait PcRsaKeyAccess {
    fn key(&self) -> &crate::rsa::BoxedRsaPrivateKey;
}

impl PcRsaKeyAccess for PcRsaKey {
    fn key(&self) -> &crate::rsa::BoxedRsaPrivateKey {
        super::rsa::pc_rsa_inner_key(self)
    }
}

/// Builds a CSR for common-name `cn` signed by `signer`, requesting each
/// newline-separated entry of `sans` as a DNS `subjectAltName`, and writes the
/// PEM to `out`. A JS/wasm-friendly path: strings are `ptr`+`len` (not C
/// strings) and the PEM is returned directly rather than via an opaque handle.
///
/// # Safety
/// `cn`/`sans` point to `cn_len`/`sans_len` valid bytes (or are NULL when the
/// length is 0); `out`/`out_len` follow the usual out-buffer convention.
unsafe fn create_csr_pem(
    signer: &CertSigner,
    cn: *const core::ffi::c_char,
    cn_len: usize,
    sans: *const core::ffi::c_char,
    sans_len: usize,
    out: *mut u8,
    out_len: *mut usize,
) -> PcStatus {
    let Some(cn_bytes) = (unsafe { slice(cn.cast(), cn_len) }) else {
        return PcStatus::NullPointer;
    };
    let Ok(cn_str) = core::str::from_utf8(cn_bytes) else {
        return PcStatus::BadEncoding;
    };
    let Some(sans_bytes) = (unsafe { slice(sans.cast(), sans_len) }) else {
        return PcStatus::NullPointer;
    };
    let Ok(sans_str) = core::str::from_utf8(sans_bytes) else {
        return PcStatus::BadEncoding;
    };
    let dns: Vec<&str> = sans_str
        .split('\n')
        .map(str::trim)
        .filter(|s| !s.is_empty())
        .collect();
    let subject = DistinguishedName::common_name(cn_str);
    match CertificationRequest::create(signer, &subject, &dns) {
        Ok(csr) => unsafe { out_write(csr.to_pem().as_bytes(), out, out_len) },
        Err(_) => PcStatus::Internal,
    }
}

/// Creates an RSA-signed CSR and writes its PEM to `out`. See [`create_csr_pem`].
///
/// # Safety
/// `rsa_key` valid; string/out pointers as in [`create_csr_pem`].
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_csr_create_rsa_pem(
    rsa_key: *const PcRsaKey,
    cn: *const core::ffi::c_char,
    cn_len: usize,
    sans: *const core::ffi::c_char,
    sans_len: usize,
    out: *mut u8,
    out_len: *mut usize,
) -> PcStatus {
    guard(|| {
        if rsa_key.is_null() {
            return PcStatus::NullPointer;
        }
        let signer = CertSigner::Rsa(super::rsa::pc_rsa_inner_key(unsafe { &*rsa_key }));
        unsafe { create_csr_pem(&signer, cn, cn_len, sans, sans_len, out, out_len) }
    })
}

/// Creates an ECDSA-signed CSR and writes its PEM to `out`. See [`create_csr_pem`].
///
/// # Safety
/// `ec_key` valid; string/out pointers as in [`create_csr_pem`].
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_csr_create_ec_pem(
    ec_key: *const super::ec::PcEcKey,
    cn: *const core::ffi::c_char,
    cn_len: usize,
    sans: *const core::ffi::c_char,
    sans_len: usize,
    out: *mut u8,
    out_len: *mut usize,
) -> PcStatus {
    guard(|| {
        if ec_key.is_null() {
            return PcStatus::NullPointer;
        }
        let signer = CertSigner::Ecdsa(super::ec::pc_ec_inner_key(unsafe { &*ec_key }));
        unsafe { create_csr_pem(&signer, cn, cn_len, sans, sans_len, out, out_len) }
    })
}

/// Creates an Ed25519-signed CSR and writes its PEM to `out`. See [`create_csr_pem`].
///
/// # Safety
/// `ed_key` valid; string/out pointers as in [`create_csr_pem`].
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_csr_create_ed25519_pem(
    ed_key: *const super::ec::PcEd25519Key,
    cn: *const core::ffi::c_char,
    cn_len: usize,
    sans: *const core::ffi::c_char,
    sans_len: usize,
    out: *mut u8,
    out_len: *mut usize,
) -> PcStatus {
    guard(|| {
        if ed_key.is_null() {
            return PcStatus::NullPointer;
        }
        let signer = CertSigner::Ed25519(super::ec::pc_ed25519_inner_key(unsafe { &*ed_key }));
        unsafe { create_csr_pem(&signer, cn, cn_len, sans, sans_len, out, out_len) }
    })
}

/// Parses a CSR from PEM.
///
/// # Safety
/// `pem` valid for `len` bytes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_csr_from_pem(pem: *const u8, len: usize) -> *mut PcCsr {
    crate::ffi::common::guard_ptr(|| {
        let Some(bytes) = (unsafe { slice(pem, len) }) else {
            return core::ptr::null_mut();
        };
        let Ok(s) = core::str::from_utf8(bytes) else {
            return core::ptr::null_mut();
        };
        match CertificationRequest::from_pem(s) {
            Ok(csr) => Box::into_raw(Box::new(PcCsr(csr))),
            Err(_) => core::ptr::null_mut(),
        }
    })
}

/// Writes the CSR as a `CERTIFICATE REQUEST` PEM to `out`.
///
/// # Safety
/// `csr` valid; buffer rules.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_csr_to_pem(
    csr: *const PcCsr,
    out: *mut u8,
    out_len: *mut usize,
) -> PcStatus {
    guard(|| {
        if csr.is_null() {
            return PcStatus::NullPointer;
        }
        let pem = unsafe { &*csr }.0.to_pem();
        unsafe { out_write(pem.as_bytes(), out, out_len) }
    })
}

/// Verifies the CSR's self-signature (the public key inside the request
/// signs its TBS contents). Returns [`PcStatus::Ok`] iff the signature is
/// valid.
///
/// # Safety
/// `csr` valid.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_csr_verify_self_signed(csr: *const PcCsr) -> PcStatus {
    guard(|| {
        if csr.is_null() {
            return PcStatus::NullPointer;
        }
        match unsafe { &*csr }.0.verify_self_signed() {
            Ok(()) => PcStatus::Ok,
            Err(_) => PcStatus::Verification,
        }
    })
}

/// Writes the CSR subject's CN attribute to `out` as a UTF-8 string (no NUL).
/// Returns [`PcStatus::BadEncoding`] if there is no CN.
///
/// # Safety
/// `csr` valid; buffer rules.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_csr_subject_cn(
    csr: *const PcCsr,
    out: *mut u8,
    out_len: *mut usize,
) -> PcStatus {
    guard(|| {
        if csr.is_null() {
            return PcStatus::NullPointer;
        }
        let subject = match unsafe { &*csr }.0.subject() {
            Ok(s) => s,
            Err(_) => return PcStatus::BadEncoding,
        };
        let Some(cn) = subject.common_name else {
            return PcStatus::BadEncoding;
        };
        unsafe { out_write(cn.as_bytes(), out, out_len) }
    })
}

/// Frees a CSR. NULL is ignored.
///
/// # Safety
/// `csr` from a constructor, not freed twice.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn pc_csr_free(csr: *mut PcCsr) {
    if !csr.is_null() {
        drop(unsafe { Box::from_raw(csr) });
    }
}