Skip to main content

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}