use rcgen::{
CertificateParams, DistinguishedName, DnType, DnValue, ExtendedKeyUsagePurpose, IsCa,
KeyUsagePurpose, SanType, string::Ia5String,
};
use thiserror::Error;
use crate::keys::PrivateKey;
const CSR_ON: &str = "Datadog, Inc.";
const CSR_OU: &str = "RC Attestation Certificate";
#[derive(Debug, Error)]
pub enum CsrError {
#[error("invalid SAN provided: {0}")]
San(rcgen::Error),
#[error("failed to serialise CSR: {0}")]
Serialise(rcgen::Error),
#[error("an empty CN or SAN was provided")]
EmptyIdent,
}
#[derive(Debug, PartialEq)]
pub struct CertificateSigningRequest {
profile: CertificateParams,
serialised: rcgen::CertificateSigningRequest,
}
impl CertificateSigningRequest {
pub fn new_leaf(private_key: &PrivateKey, cn: &str, san: &str) -> Result<Self, CsrError> {
if cn.trim().is_empty() || san.trim().is_empty() {
return Err(CsrError::EmptyIdent);
}
let mut profile = CertificateParams::new([]).map_err(CsrError::San)?;
profile.is_ca = IsCa::NoCa;
profile.serial_number = None; profile.name_constraints = None; profile.crl_distribution_points = vec![]; profile.custom_extensions = vec![];
let mut distinguished_name = DistinguishedName::new();
distinguished_name.push(DnType::CommonName, cn);
distinguished_name.push(DnType::OrganizationName, CSR_ON);
distinguished_name.push(DnType::OrganizationalUnitName, CSR_OU);
profile.distinguished_name = distinguished_name;
profile.subject_alt_names = vec![SanType::DnsName(
Ia5String::try_from(san).map_err(CsrError::San)?,
)];
profile.key_usages = vec![KeyUsagePurpose::DigitalSignature];
profile.extended_key_usages = vec![ExtendedKeyUsagePurpose::CodeSigning];
profile.key_identifier_method =
rcgen::KeyIdMethod::PreSpecified(private_key.public_key().key_id().to_vec());
let serialised = profile
.serialize_request(private_key)
.map_err(CsrError::Serialise)?;
Ok(Self {
profile,
serialised,
})
}
pub fn new_intermediate(private_key: &PrivateKey, cn: &str) -> Result<Self, CsrError> {
if cn.trim().is_empty() {
return Err(CsrError::EmptyIdent);
}
let mut profile = CertificateParams::new([]).map_err(CsrError::San)?;
profile.name_constraints = None;
profile.serial_number = None; profile.name_constraints = None; profile.crl_distribution_points = vec![]; profile.custom_extensions = vec![];
let mut distinguished_name = DistinguishedName::new();
distinguished_name.push(DnType::CommonName, cn);
distinguished_name.push(DnType::OrganizationName, CSR_ON);
distinguished_name.push(DnType::OrganizationalUnitName, CSR_OU);
profile.distinguished_name = distinguished_name;
profile.is_ca = IsCa::NoCa;
profile.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign];
profile.extended_key_usages = vec![];
profile.key_identifier_method =
rcgen::KeyIdMethod::PreSpecified(private_key.public_key().key_id().to_vec());
let serialised = profile
.serialize_request(private_key)
.map_err(CsrError::Serialise)?;
Ok(Self {
profile,
serialised,
})
}
pub fn as_der_bytes(&self) -> &[u8] {
self.serialised.der()
}
pub fn as_pem_string(&self) -> String {
self.serialised.pem().expect("failed to generate CSR PEM")
}
pub fn common_name(&self) -> &str {
let dn_value = self
.profile
.distinguished_name
.get(&DnType::CommonName)
.expect("CSR should always have a CommonName");
match dn_value {
DnValue::Utf8String(s) => s.as_str(),
DnValue::PrintableString(s) => s.as_str(),
DnValue::Ia5String(s) => s.as_str(),
DnValue::TeletexString(s) => s.as_str(),
_ => panic!("Unexpected DnValue type for CommonName"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use assert_matches::assert_matches;
use proptest::{prelude::*, strategy::LazyJust};
use rcgen::{CertificateSigningRequestParams, PublicKeyData, SanType};
fn arb_string() -> impl Strategy<Value = String> {
prop_oneof![
10 => any::<String>(),
10 => prop::collection::vec(0_u8..=127, 1..1025).prop_map(|v| String::from_utf8(v).unwrap()),
1 => LazyJust::new(|| "127.0.0.42".to_string()),
]
}
fn is_bad_dn(s: &str) -> bool {
Ia5String::try_from(s).is_err()
}
proptest! {
#[test]
fn prop_leaf_csr_generation(
cn in arb_string(),
san in arb_string(),
) {
let key = PrivateKey::new();
let csr = match CertificateSigningRequest::new_leaf(&key, &cn, &san) {
Ok(v) => v,
Err(CsrError::EmptyIdent) => {
assert!(cn.trim().is_empty() || san.trim().is_empty());
return Ok(());
}
Err(e) => {
assert!(is_bad_dn(&cn) || is_bad_dn(&san), "{e}");
return Ok(());
}
};
match csr.profile.subject_alt_names.as_slice() {
[SanType::DnsName(v)] => {
assert_eq!(v.to_string(), san);
}
_ => panic!("invalid san config"),
}
let mut want_dn = DistinguishedName::new();
want_dn.push(DnType::CommonName, cn);
want_dn.push(DnType::OrganizationName, "Datadog, Inc.");
want_dn.push(DnType::OrganizationalUnitName, "RC Attestation Certificate");
assert_eq!(csr.profile.distinguished_name, want_dn);
assert_eq!(csr.profile.is_ca, rcgen::IsCa::NoCa);
assert_eq!(csr.profile.key_usages, vec![KeyUsagePurpose::DigitalSignature]);
assert_eq!(csr.profile.extended_key_usages, vec![ExtendedKeyUsagePurpose::CodeSigning]);
assert_eq!(csr.profile.serial_number, None);
assert_eq!(csr.profile.name_constraints, None);
assert_eq!(csr.profile.crl_distribution_points, vec![]);
assert_eq!(csr.profile.custom_extensions, vec![]);
let read = CertificateSigningRequestParams::from_pem(&csr.as_pem_string()).unwrap();
assert_eq!(read.params.serial_number, csr.profile.serial_number);
assert_eq!(read.params.subject_alt_names, csr.profile.subject_alt_names);
assert_eq!(read.params.distinguished_name, csr.profile.distinguished_name);
assert_eq!(read.params.is_ca, csr.profile.is_ca);
assert_eq!(read.params.key_usages, csr.profile.key_usages);
assert_eq!(read.params.extended_key_usages, csr.profile.extended_key_usages);
assert_eq!(read.params.name_constraints, csr.profile.name_constraints);
assert_eq!(read.params.crl_distribution_points, csr.profile.crl_distribution_points);
assert_eq!(read.params.custom_extensions, csr.profile.custom_extensions);
assert_eq!(read.params.use_authority_key_identifier_extension, csr.profile.use_authority_key_identifier_extension);
assert_eq!(read.public_key.subject_public_key_info(), key.public_key().subject_public_key_info());
assert_eq!(read.public_key.der_bytes(), key.public_key().der_bytes());
}
#[test]
fn prop_intermediate_csr_generation(
cn in arb_string(),
) {
let key = PrivateKey::new();
let csr = match CertificateSigningRequest::new_intermediate(&key, &cn) {
Ok(v) => v,
Err(CsrError::EmptyIdent) => {
assert!(cn.trim().is_empty());
return Ok(());
}
Err(e) => {
assert!(is_bad_dn(&cn), "{e}");
return Ok(());
}
};
assert!(csr.profile.subject_alt_names.is_empty());
let mut want_dn = DistinguishedName::new();
want_dn.push(DnType::CommonName, cn);
want_dn.push(DnType::OrganizationName, "Datadog, Inc.");
want_dn.push(DnType::OrganizationalUnitName, "RC Attestation Certificate");
assert_eq!(csr.profile.distinguished_name, want_dn);
assert_eq!(csr.profile.is_ca, IsCa::NoCa);
assert_matches!(csr.profile.name_constraints, None);
assert_eq!(csr.profile.key_usages, vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign]);
assert_eq!(csr.profile.extended_key_usages, vec![]);
assert_eq!(csr.profile.serial_number, None);
assert_eq!(csr.profile.crl_distribution_points, vec![]);
assert_eq!(csr.profile.custom_extensions, vec![]);
let read = CertificateSigningRequestParams::from_pem(&csr.as_pem_string()).unwrap();
assert_eq!(read.params.serial_number, csr.profile.serial_number);
assert_eq!(read.params.subject_alt_names, csr.profile.subject_alt_names);
assert_eq!(read.params.distinguished_name, csr.profile.distinguished_name);
assert_eq!(read.params.is_ca, csr.profile.is_ca);
assert_eq!(read.params.key_usages, csr.profile.key_usages);
assert_eq!(read.params.extended_key_usages, csr.profile.extended_key_usages);
assert_eq!(read.params.name_constraints, csr.profile.name_constraints);
assert_eq!(read.params.crl_distribution_points, csr.profile.crl_distribution_points);
assert_eq!(read.params.custom_extensions, csr.profile.custom_extensions);
assert_eq!(read.params.use_authority_key_identifier_extension, csr.profile.use_authority_key_identifier_extension);
assert_eq!(read.public_key.subject_public_key_info(), key.public_key().subject_public_key_info());
assert_eq!(read.public_key.der_bytes(), key.public_key().der_bytes());
}
}
}