rc_crypto/certificate/csr.rs
1// Copyright 2026-Present Datadog, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Certificate signer request generation.
16//!
17//! A typical workflow for certifying a [`PrivateKey`] as part of a trust chain
18//! is shown below:
19//!
20//! ```text
21//! ┌──────────┐ ┌──────┐
22//! │Key Holder│ │Issuer│
23//! └─────┬────┘ └───┬──┘
24//! │────┐ │
25//! │ │ Generate key │
26//! │<───┘ │
27//! │ │
28//! │────┐ │
29//! │ │ Generate CSR │
30//! │<───┘ │
31//! │ │
32//! │ Send CSR │
33//! │───────────────────>│
34//! │ │
35//! │ │────┐
36//! │ │ │ Generate certificate from CSR
37//! │ │<───┘
38//! │ │
39//! │ Certificate │
40//! │<───────────────────│
41//! ┌─────┴────┐ ┌───┴──┐
42//! │Key Holder│ │Issuer│
43//! └──────────┘ └──────┘
44//! ```
45//!
46//! Where:
47//!
48//! * "Key Holder" is has a [`PrivateKey`].
49//! * "Issuer" has a CA certificate that chains to a trusted root.
50//!
51//! The certificate returned from the "Issuer" certifies the [`PrivateKey`] can
52//! be trusted as part of the trust chain from Issuer's root.
53
54use rcgen::{
55 CertificateParams, DistinguishedName, DnType, DnValue, ExtendedKeyUsagePurpose, IsCa,
56 KeyUsagePurpose, SanType, string::Ia5String,
57};
58use thiserror::Error;
59
60use crate::keys::PrivateKey;
61
62// CSR DN field values.
63const CSR_ON: &str = "Datadog, Inc.";
64const CSR_OU: &str = "RC Attestation Certificate";
65
66/// Failures when generating a new [`CertificateSigningRequest`].
67#[derive(Debug, Error)]
68pub enum CsrError {
69 /// The SAN provided is invalid.
70 #[error("invalid SAN provided: {0}")]
71 San(rcgen::Error),
72
73 /// The CSR was populated, but was invalid / could not be serialised.
74 #[error("failed to serialise CSR: {0}")]
75 Serialise(rcgen::Error),
76
77 /// The provided CA or SAN value was an empty string.
78 #[error("an empty CN or SAN was provided")]
79 EmptyIdent,
80}
81
82/// A [`CertificateSigningRequest`] ("CSR") contains requested certificate
83/// parameters that are provided to a Certificate Authority for issuance,
84/// notably:
85///
86/// * The public key to certify.
87/// * The key usage purposes (and EKUs) to be certified for.
88/// * The common name / SAN fields ("certificate name").
89///
90/// The resulting [`Certificate`] issued by a CA contains values provided in
91/// this [`CertificateSigningRequest`] in addition to values specified by the
92/// CA's issuance policy. The CA is free to override (or reject) any field
93/// provided in a CSR.
94///
95/// [`Certificate`]: crate::certificate::Certificate
96#[derive(Debug, PartialEq)]
97pub struct CertificateSigningRequest {
98 /// The certificate profile used when generating the "serialised" request.
99 ///
100 /// This is kept for informative purposes, allowing callers to inspect
101 /// (read-only) properties of the generated CSR (i.e. for logging).
102 profile: CertificateParams,
103
104 /// A pre-serialised form of "profile".
105 serialised: rcgen::CertificateSigningRequest,
106}
107
108impl CertificateSigningRequest {
109 /// Create a new certificate signing request for a leaf certificate profile.
110 ///
111 /// Certificates SHOULD use unique CN strings.
112 ///
113 /// Issuers MUST apply the following best practices when issuing the CA
114 /// certificate:
115 ///
116 /// * Set CA: FALSE as a basic constraint, and mark it as critical.
117 ///
118 pub fn new_leaf(private_key: &PrivateKey, cn: &str, san: &str) -> Result<Self, CsrError> {
119 if cn.trim().is_empty() || san.trim().is_empty() {
120 return Err(CsrError::EmptyIdent);
121 }
122
123 //
124 // The following code configures the certificate profile to be signed by
125 // an issuer, and the various fields and their consequences are defined
126 // in RFC5280:
127 //
128 // https://datatracker.ietf.org/doc/html/rfc5280
129 //
130
131 let mut profile = CertificateParams::new([]).map_err(CsrError::San)?;
132
133 // Explicitly mark this as not a CA (therefore end-entity / leaf)
134 // certificate.
135 profile.is_ca = IsCa::NoCa; // Issuer will insert CA: FALSE critical.
136
137 // Explicitly opt of of the following:
138 profile.serial_number = None; // Generated by issuer
139 profile.name_constraints = None; // Only applies to CAs.
140 profile.crl_distribution_points = vec![]; // CRLs are distributed via delivery protocol.
141 profile.custom_extensions = vec![]; // N/A
142
143 // Build the DN, which specifies the identity of the owner.
144 let mut distinguished_name = DistinguishedName::new();
145 distinguished_name.push(DnType::CommonName, cn);
146 distinguished_name.push(DnType::OrganizationName, CSR_ON);
147 distinguished_name.push(DnType::OrganizationalUnitName, CSR_OU);
148 profile.distinguished_name = distinguished_name;
149
150 // Specify the SAN DNS name.
151 profile.subject_alt_names = vec![SanType::DnsName(
152 Ia5String::try_from(san).map_err(CsrError::San)?,
153 )];
154
155 // Configure the key profile.
156 //
157 // Key Usage, § 4.2.1.3:
158 //
159 // The digitalSignature bit is asserted when the subject public key is
160 // used for verifying digital signatures, other than signatures on
161 // certificates (bit 5) and CRLs (bit 6), such as those used in an
162 // entity authentication service, a data origin authentication
163 // service, and/or an integrity service.
164 //
165 // Extended Key Usage, § 4.2.1.12 for "codeSigning":
166 //
167 // * Signing of downloadable executable code
168 // * Key usage bits that may be consistent: digitalSignature
169 //
170 profile.key_usages = vec![KeyUsagePurpose::DigitalSignature];
171 profile.extended_key_usages = vec![ExtendedKeyUsagePurpose::CodeSigning];
172
173 // Provide the Subject Key Identifier hash, which in this implementation
174 // is a SHA256 hash over the X509 SubjectPublicKeyInfo defined in § 4.1.
175 //
176 // § 4.2.1.2:
177 //
178 // To assist applications in identifying the appropriate end entity
179 // certificate, this extension SHOULD be included in all end entity
180 // certificates.
181 //
182 profile.key_identifier_method =
183 rcgen::KeyIdMethod::PreSpecified(private_key.public_key().key_id().to_vec());
184
185 let serialised = profile
186 .serialize_request(private_key)
187 .map_err(CsrError::Serialise)?;
188
189 Ok(Self {
190 profile,
191 serialised,
192 })
193 }
194
195 /// Create a new certificate signing request for an intermediate certificate
196 /// profile.
197 ///
198 /// Issuers SHOULD apply the following best practices when issuing the CA
199 /// certificate:
200 ///
201 /// * Apply name constraints such that the provisioned CA can issue
202 /// certificates only under the subdomain for which it is intended to be
203 /// used (DC isolation).
204 ///
205 /// * Constrain the pathLen of the CA to prevent further unintended CA
206 /// issuance.
207 ///
208 pub fn new_intermediate(private_key: &PrivateKey, cn: &str) -> Result<Self, CsrError> {
209 if cn.trim().is_empty() {
210 return Err(CsrError::EmptyIdent);
211 }
212
213 //
214 // The following code configures the certificate profile to be signed by
215 // an issuer, and the various fields and their consequences are defined
216 // in RFC5280:
217 //
218 // https://datatracker.ietf.org/doc/html/rfc5280
219 //
220
221 let mut profile = CertificateParams::new([]).map_err(CsrError::San)?;
222
223 // The issuer MUST mark this as not a CA (therefore end-entity / leaf)
224 // certificate with an appropriate pathLen.
225 //
226 // Basic Constraints, § 4.2.1.9:
227 //
228 // The basic constraints extension identifies whether the subject of
229 // the certificate is a CA and the maximum depth of valid
230 // certification paths that include this certificate.
231 //
232 // A pathLenConstraint of zero indicates that no non- self-issued
233 // intermediate CA certificates may follow in a valid certification
234 // path.
235 //
236 // This parameter is not supported in the CSR.
237 profile.name_constraints = None;
238
239 // Explicitly opt out of the following:
240 profile.serial_number = None; // Generated by issuer
241 profile.name_constraints = None; // Only applies to CAs.
242 profile.crl_distribution_points = vec![]; // CRLs are distributed via delivery protocol.
243 profile.custom_extensions = vec![]; // N/A
244
245 // Build the DN, which specifies the identity of the owner.
246 let mut distinguished_name = DistinguishedName::new();
247 distinguished_name.push(DnType::CommonName, cn);
248 distinguished_name.push(DnType::OrganizationName, CSR_ON);
249 distinguished_name.push(DnType::OrganizationalUnitName, CSR_OU);
250 profile.distinguished_name = distinguished_name;
251
252 // The issuer MUST constrain this intermediate using Name Constraints
253 // such that it MAY issue certificates only under the subdomain it is
254 // responsible for.
255 //
256 // This parameter is not supported in the CSR.
257 profile.is_ca = IsCa::NoCa;
258
259 // Configure the key profile.
260 //
261 // Key Usage, § 4.2.1.3:
262 //
263 // The keyCertSign bit is asserted when the subject public key is used
264 // for verifying signatures on public key certificates. If the
265 // keyCertSign bit is asserted, then the cA bit in the basic
266 // constraints extension (Section 4.2.1.9) MUST also be asserted.
267 //
268 // The cRLSign bit is asserted when the subject public key is used for
269 // verifying signatures on certificate revocation lists (e.g., CRLs,
270 // delta CRLs, or ARLs).
271 //
272 profile.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign];
273 profile.extended_key_usages = vec![];
274
275 // Provide the Subject Key Identifier hash, which in this implementation
276 // is a SHA256 hash over the X509 SubjectPublicKeyInfo defined in § 4.1.
277 //
278 // § 4.2.1.2:
279 //
280 // To assist applications in identifying the appropriate end entity
281 // certificate, this extension SHOULD be included in all end entity
282 // certificates.
283 //
284 profile.key_identifier_method =
285 rcgen::KeyIdMethod::PreSpecified(private_key.public_key().key_id().to_vec());
286
287 let serialised = profile
288 .serialize_request(private_key)
289 .map_err(CsrError::Serialise)?;
290
291 Ok(Self {
292 profile,
293 serialised,
294 })
295 }
296
297 /// Return the pre-serialised CSR as DER bytes.
298 ///
299 /// This call returns pre-cached content in O(1) time.
300 pub fn as_der_bytes(&self) -> &[u8] {
301 self.serialised.der()
302 }
303
304 /// Serialise the CSR into a PEM block.
305 pub fn as_pem_string(&self) -> String {
306 self.serialised.pem().expect("failed to generate CSR PEM")
307 }
308
309 /// Get the CommonName (CN) from the Distinguished Name in the CSR.
310 ///
311 /// Returns the CN value that was provided when creating the CSR in `new_leaf()`.
312 ///
313 /// Panics if the CSR doesn't have a CommonName (which should never happen for CSRs
314 /// created via `new_leaf()`), or if the CommonName is in an unexpected encoding.
315 pub fn common_name(&self) -> &str {
316 let dn_value = self
317 .profile
318 .distinguished_name
319 .get(&DnType::CommonName)
320 .expect("CSR should always have a CommonName");
321
322 // Extract the string from the DnValue enum
323 // Since new_leaf() pushes CN as &str, it becomes DnValue::Utf8String
324 match dn_value {
325 DnValue::Utf8String(s) => s.as_str(),
326 DnValue::PrintableString(s) => s.as_str(),
327 DnValue::Ia5String(s) => s.as_str(),
328 DnValue::TeletexString(s) => s.as_str(),
329 // BmpString and UniversalString don't implement as_str(),
330 // but they shouldn't be used for CN in our CSRs
331 _ => panic!("Unexpected DnValue type for CommonName"),
332 }
333 }
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339
340 use assert_matches::assert_matches;
341 use proptest::{prelude::*, strategy::LazyJust};
342 use rcgen::{CertificateSigningRequestParams, PublicKeyData, SanType};
343
344 fn arb_string() -> impl Strategy<Value = String> {
345 prop_oneof![
346 // Any random string, including incompatible characters.
347 10 => any::<String>(),
348 // ASCII only (DN compatible).
349 10 => prop::collection::vec(0_u8..=127, 1..1025).prop_map(|v| String::from_utf8(v).unwrap()),
350 // An IP address, which the rcgen crate would have helpfully
351 // inferred into a IPSan - no magic please.
352 1 => LazyJust::new(|| "127.0.0.42".to_string()),
353 ]
354 }
355
356 fn is_bad_dn(s: &str) -> bool {
357 Ia5String::try_from(s).is_err()
358 }
359
360 proptest! {
361 #[test]
362 fn prop_leaf_csr_generation(
363 cn in arb_string(),
364 san in arb_string(),
365 ) {
366 let key = PrivateKey::new();
367
368 let csr = match CertificateSigningRequest::new_leaf(&key, &cn, &san) {
369 Ok(v) => v,
370 Err(CsrError::EmptyIdent) => {
371 assert!(cn.trim().is_empty() || san.trim().is_empty());
372 return Ok(());
373 }
374 Err(e) => {
375 assert!(is_bad_dn(&cn) || is_bad_dn(&san), "{e}");
376 return Ok(());
377 }
378 };
379
380 // Invariant: the provided SAN is the only SAN present, and it is a
381 // DNS name.
382 match csr.profile.subject_alt_names.as_slice() {
383 [SanType::DnsName(v)] => {
384 assert_eq!(v.to_string(), san);
385 }
386 _ => panic!("invalid san config"),
387 }
388
389 // Invariant: the DN is composed of the hard-coded Datadog
390 // identifiers, plus the variable CN.
391 let mut want_dn = DistinguishedName::new();
392 want_dn.push(DnType::CommonName, cn);
393 want_dn.push(DnType::OrganizationName, "Datadog, Inc.");
394 want_dn.push(DnType::OrganizationalUnitName, "RC Attestation Certificate");
395 assert_eq!(csr.profile.distinguished_name, want_dn);
396
397 // Invariant: a leaf should never request to be a CA cert.
398 assert_eq!(csr.profile.is_ca, rcgen::IsCa::NoCa);
399
400 // Invariant: KU & EKU must be suitable for code signing.
401 assert_eq!(csr.profile.key_usages, vec![KeyUsagePurpose::DigitalSignature]);
402 assert_eq!(csr.profile.extended_key_usages, vec![ExtendedKeyUsagePurpose::CodeSigning]);
403
404 // Invariant: no serial, name constraints, CRL points, or
405 // extensions are set.
406 assert_eq!(csr.profile.serial_number, None);
407 assert_eq!(csr.profile.name_constraints, None);
408 assert_eq!(csr.profile.crl_distribution_points, vec![]);
409 assert_eq!(csr.profile.custom_extensions, vec![]);
410
411 // Invariant: the cert profile and the serialised bytes are
412 // consistent (meaning the serialised bytes accurately represent the
413 // contents of the profile kept for informative purposes) and
414 // deterministic.
415 let read = CertificateSigningRequestParams::from_pem(&csr.as_pem_string()).unwrap();
416 assert_eq!(read.params.serial_number, csr.profile.serial_number);
417 assert_eq!(read.params.subject_alt_names, csr.profile.subject_alt_names);
418 assert_eq!(read.params.distinguished_name, csr.profile.distinguished_name);
419 assert_eq!(read.params.is_ca, csr.profile.is_ca);
420 assert_eq!(read.params.key_usages, csr.profile.key_usages);
421 assert_eq!(read.params.extended_key_usages, csr.profile.extended_key_usages);
422 assert_eq!(read.params.name_constraints, csr.profile.name_constraints);
423 assert_eq!(read.params.crl_distribution_points, csr.profile.crl_distribution_points);
424 assert_eq!(read.params.custom_extensions, csr.profile.custom_extensions);
425 assert_eq!(read.params.use_authority_key_identifier_extension, csr.profile.use_authority_key_identifier_extension);
426 // assert_matches!(read.params.key_identifier_method, rcgen::KeyIdMethod::PreSpecified(_)); // No content
427
428 // Invariant: the SubjectPublicKeyInfo must have been propagated (it
429 // for whatever reason does not make it into the deserialised
430 // profile, but rather into the attached public key).
431 assert_eq!(read.public_key.subject_public_key_info(), key.public_key().subject_public_key_info());
432
433 // Invariant: the public key is correctly included in the serialised
434 // form.
435 assert_eq!(read.public_key.der_bytes(), key.public_key().der_bytes());
436 }
437
438 #[test]
439 fn prop_intermediate_csr_generation(
440 cn in arb_string(),
441 ) {
442 let key = PrivateKey::new();
443
444 let csr = match CertificateSigningRequest::new_intermediate(&key, &cn) {
445 Ok(v) => v,
446 Err(CsrError::EmptyIdent) => {
447 assert!(cn.trim().is_empty());
448 return Ok(());
449 }
450 Err(e) => {
451 assert!(is_bad_dn(&cn), "{e}");
452 return Ok(());
453 }
454 };
455
456 // Invariant: no SANs are provided.
457 assert!(csr.profile.subject_alt_names.is_empty());
458
459 // Invariant: the DN is composed of the hard-coded Datadog
460 // identifiers, plus the variable CN.
461 let mut want_dn = DistinguishedName::new();
462 want_dn.push(DnType::CommonName, cn);
463 want_dn.push(DnType::OrganizationName, "Datadog, Inc.");
464 want_dn.push(DnType::OrganizationalUnitName, "RC Attestation Certificate");
465 assert_eq!(csr.profile.distinguished_name, want_dn);
466
467 // Invariant: not set in CSR - provided by issuer.
468 assert_eq!(csr.profile.is_ca, IsCa::NoCa);
469
470 // Invariant: name constraints are not set in CSR - provided by
471 // issuer.
472 assert_matches!(csr.profile.name_constraints, None);
473
474 // Invariant: KU & EKU must be suitable for code signing.
475 assert_eq!(csr.profile.key_usages, vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign]);
476 assert_eq!(csr.profile.extended_key_usages, vec![]);
477
478 // Invariant: no serial, CRL points, or extensions are set.
479 assert_eq!(csr.profile.serial_number, None);
480 assert_eq!(csr.profile.crl_distribution_points, vec![]);
481 assert_eq!(csr.profile.custom_extensions, vec![]);
482
483 // Invariant: the cert profile and the serialised bytes are
484 // consistent (meaning the serialised bytes accurately represent the
485 // contents of the profile kept for informative purposes) and
486 // deterministic.
487 let read = CertificateSigningRequestParams::from_pem(&csr.as_pem_string()).unwrap();
488 assert_eq!(read.params.serial_number, csr.profile.serial_number);
489 assert_eq!(read.params.subject_alt_names, csr.profile.subject_alt_names);
490 assert_eq!(read.params.distinguished_name, csr.profile.distinguished_name);
491 assert_eq!(read.params.is_ca, csr.profile.is_ca);
492 assert_eq!(read.params.key_usages, csr.profile.key_usages);
493 assert_eq!(read.params.extended_key_usages, csr.profile.extended_key_usages);
494 assert_eq!(read.params.name_constraints, csr.profile.name_constraints);
495 assert_eq!(read.params.crl_distribution_points, csr.profile.crl_distribution_points);
496 assert_eq!(read.params.custom_extensions, csr.profile.custom_extensions);
497 assert_eq!(read.params.use_authority_key_identifier_extension, csr.profile.use_authority_key_identifier_extension);
498 // assert_matches!(read.params.key_identifier_method, rcgen::KeyIdMethod::PreSpecified(_)); // No content
499
500 // Invariant: the SubjectPublicKeyInfo must have been propagated (it
501 // for whatever reason does not make it into the deserialised
502 // profile, but rather into the attached public key).
503 assert_eq!(read.public_key.subject_public_key_info(), key.public_key().subject_public_key_info());
504
505 // Invariant: the public key is correctly included in the serialised
506 // form.
507 assert_eq!(read.public_key.der_bytes(), key.public_key().der_bytes());
508 }
509 }
510}