use rcgen::{CertificateParams, DistinguishedName, DnType, KeyPair as RcgenKeyPair};
use rsa::pkcs8::{EncodePrivateKey, LineEnding};
use rsa::RsaPrivateKey;
use smbcloud_ascapi_core::{Error, Result};
use zeroize::Zeroizing;
const RSA_MODULUS_BITS: usize = 2048;
pub struct CertificateRequest {
csr_pem: String,
private_key_pem: Zeroizing<String>,
}
impl CertificateRequest {
pub fn csr_pem(&self) -> &str {
&self.csr_pem
}
pub fn private_key_pem(&self) -> &Zeroizing<String> {
&self.private_key_pem
}
}
impl std::fmt::Debug for CertificateRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CertificateRequest")
.field("csr_pem", &"<csr>")
.field("private_key_pem", &"<redacted>")
.finish()
}
}
pub fn generate_certificate_request(common_name: &str) -> Result<CertificateRequest> {
let mut rng = rand::thread_rng();
let private_key = RsaPrivateKey::new(&mut rng, RSA_MODULUS_BITS)
.map_err(|source| Error::KeyGeneration(source.to_string()))?;
let private_key_pem = private_key
.to_pkcs8_pem(LineEnding::LF)
.map_err(|source| Error::KeyGeneration(source.to_string()))?;
let rcgen_key = RcgenKeyPair::from_pem(&private_key_pem)
.map_err(|source| Error::CsrGeneration(source.to_string()))?;
let mut params = CertificateParams::default();
let mut dn = DistinguishedName::new();
dn.push(DnType::CommonName, common_name);
params.distinguished_name = dn;
let csr_pem = params
.serialize_request(&rcgen_key)
.map_err(|source| Error::CsrGeneration(source.to_string()))?
.pem()
.map_err(|source| Error::CsrGeneration(source.to_string()))?;
Ok(CertificateRequest {
csr_pem,
private_key_pem,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use std::process::{Command, Stdio};
fn openssl(args: &[&str], stdin_pem: &str) -> Option<String> {
let mut child = Command::new("openssl")
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.ok()?;
child
.stdin
.as_mut()
.expect("stdin piped")
.write_all(stdin_pem.as_bytes())
.expect("writes");
let out = child.wait_with_output().expect("waits");
assert!(
out.status.success(),
"openssl {args:?} failed: {}",
String::from_utf8_lossy(&out.stderr)
);
Some(format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
))
}
#[test]
fn csr_is_rsa_2048_and_self_signature_verifies() {
let request = generate_certificate_request("smbCloud signing test").expect("generates");
assert!(request
.csr_pem()
.starts_with("-----BEGIN CERTIFICATE REQUEST-----"));
assert!(request
.private_key_pem()
.starts_with("-----BEGIN PRIVATE KEY-----"));
let Some(text) = openssl(&["req", "-noout", "-text", "-verify"], request.csr_pem()) else {
eprintln!("openssl not available; skipped the cryptographic assertions");
return;
};
assert!(
text.contains("Public-Key: (2048 bit)"),
"expected an RSA 2048 key, got:\n{text}"
);
assert!(
text.contains("rsaEncryption"),
"expected an RSA key; Apple rejects ECDSA CSRs. Got:\n{text}"
);
assert!(
text.contains("Certificate request self-signature verify OK")
|| text.contains("verify OK"),
"CSR self-signature did not verify:\n{text}"
);
assert!(
text.contains("smbCloud signing test"),
"common name missing from subject:\n{text}"
);
}
#[test]
fn each_request_gets_a_fresh_key() {
let a = generate_certificate_request("a").expect("generates");
let b = generate_certificate_request("b").expect("generates");
assert_ne!(
a.private_key_pem().as_str(),
b.private_key_pem().as_str(),
"two requests shared a private key"
);
}
#[test]
fn debug_never_prints_key_material() {
let request = generate_certificate_request("redaction check").expect("generates");
let rendered = format!("{request:?}");
assert!(rendered.contains("<redacted>"));
assert!(
!rendered.contains("BEGIN PRIVATE KEY"),
"Debug leaked the private key"
);
}
}