Skip to main content

lys_core/ca/
certificate.rs

1//! The two results a certificate-authority issuance can produce.
2//!
3//! [`IssuedCertificate`] comes from the path where the authority generated the
4//! subject keypair, so it carries both halves. The subject signing key is
5//! private material: the manual [`fmt::Debug`] impl redacts it unconditionally
6//! and the type never derives `Debug` (which would expose key internals).
7//!
8//! [`CertifiedKey`] comes from the path where the holder presented a key and
9//! proved possession of it. There is no private material to hold or redact —
10//! its absence is the point, and it is why the two outcomes are separate types
11//! rather than one type with an optional signing key. A caller cannot
12//! accidentally treat a presented-key issuance as though it handed back a
13//! usable private key.
14//!
15//! [`CertifiedKey::from_der_and_public_key`] re-parses the DER it is handed and
16//! confirms it binds the presented key. Nothing else in the crate checks that,
17//! so without it a certificate-generation bug could return a
18//! successfully-signed certificate over the wrong key — a success report for
19//! work not done, which is the failure class this crate exists to make
20//! impossible. The binding is the entire value of the presented-key path, so it
21//! is read back from the bytes rather than assumed.
22//!
23//! [`IssuedCertificate::from_der_and_keypair`] deliberately does **not** do the
24//! same. It is published API whose accepted inputs cannot be narrowed without
25//! breaking callers that hand it DER for their own purposes, and the property
26//! matters less there: the subject key on that path came from this process, so
27//! a certificate over the wrong key misbinds a key nobody controls rather than
28//! misattributing one somebody does. The equivalent guarantee is asserted for
29//! that path in the authority's tests instead.
30
31use std::fmt;
32
33use chrono::{DateTime, Utc};
34use ed25519_dalek::pkcs8::DecodePrivateKey;
35use ed25519_dalek::{SigningKey, VerifyingKey};
36use sha2::{Digest, Sha256};
37use x509_parser::oid_registry::OID_SIG_ED25519;
38use x509_parser::prelude::{FromDer, X509Certificate};
39
40use crate::error::{TrustError, TrustResult};
41use crate::hex_lower;
42
43/// A certificate issued by a [`crate::ca::CertificateAuthority`].
44///
45/// The subject keypair is generated during issuance and travels with the
46/// certificate so the holder can prove possession of the subject identity.
47/// The signing half is private — guard it accordingly; it is redacted from
48/// `Debug` output.
49pub struct IssuedCertificate {
50    /// The signed certificate in DER encoding.
51    pub der_bytes: Vec<u8>,
52    /// The subject's Ed25519 signing (private) key. **Sensitive material** —
53    /// redacted from `Debug`.
54    pub subject_signing_key: SigningKey,
55    /// The subject's Ed25519 verifying (public) key, matching
56    /// [`Self::subject_signing_key`].
57    pub subject_verifying_key: VerifyingKey,
58    /// SHA-256 fingerprint of [`Self::der_bytes`].
59    pub fingerprint: [u8; 32],
60    /// Instant after which the certificate is no longer valid.
61    pub expires_at: DateTime<Utc>,
62    /// The 32-byte Ed25519 public key of the issuing authority.
63    pub issuer_public_key: [u8; 32],
64}
65
66impl IssuedCertificate {
67    /// Builds an [`IssuedCertificate`] from signed DER and the subject keypair.
68    ///
69    /// Computes the SHA-256 fingerprint over `der_bytes`, converts the rcgen
70    /// Ed25519 subject keypair into dalek signing/verifying keys via its
71    /// PKCS#8 serialization, and validates that the raw public key is exactly
72    /// 32 bytes and consistent with the recovered signing key.
73    ///
74    /// # Errors
75    ///
76    /// Returns [`TrustError::CertificateGeneration`] if the subject public key
77    /// is not 32 bytes, the PKCS#8 private key cannot be decoded into a dalek
78    /// signing key, or the recovered public key does not match the keypair.
79    pub fn from_der_and_keypair(
80        der_bytes: Vec<u8>,
81        subject_keypair: &rcgen::KeyPair,
82        expires_at: DateTime<Utc>,
83        issuer_public_key: [u8; 32],
84    ) -> TrustResult<Self> {
85        let raw_public = subject_keypair.public_key_raw();
86        if raw_public.len() != 32 {
87            return Err(TrustError::CertificateGeneration {
88                reason: format!(
89                    "subject public key must be 32 bytes for Ed25519, got {}",
90                    raw_public.len()
91                ),
92            });
93        }
94
95        let pkcs8 = subject_keypair.serialize_der();
96        let subject_signing_key =
97            SigningKey::from_pkcs8_der(&pkcs8).map_err(|e| TrustError::CertificateGeneration {
98                reason: format!("failed to load subject signing key from PKCS#8: {e}"),
99            })?;
100        let subject_verifying_key = subject_signing_key.verifying_key();
101
102        if subject_verifying_key.to_bytes().as_slice() != raw_public {
103            return Err(TrustError::CertificateGeneration {
104                reason: "subject keypair public and private halves do not match".to_string(),
105            });
106        }
107
108        let fingerprint: [u8; 32] = Sha256::digest(&der_bytes).into();
109
110        Ok(Self {
111            der_bytes,
112            subject_signing_key,
113            subject_verifying_key,
114            fingerprint,
115            expires_at,
116            issuer_public_key,
117        })
118    }
119}
120
121/// A certificate issued over a subject key its holder presented and proved
122/// possession of.
123///
124/// Produced by
125/// [`CertificateAuthority::issue_certificate_for_request`](crate::ca::CertificateAuthority::issue_certificate_for_request).
126/// Carries no private material: the subject's signing key stays with the
127/// holder and was never present in this process. That is what makes this
128/// certificate meaningful to a third party — it binds a key someone
129/// demonstrably controls, rather than one the authority minted.
130#[derive(Clone, PartialEq, Eq)]
131pub struct CertifiedKey {
132    /// The signed certificate in DER encoding.
133    pub der_bytes: Vec<u8>,
134    /// The subject's 32-byte Ed25519 public key, as carried in the
135    /// certificate's `subjectPublicKeyInfo` and proved possessed at issuance.
136    pub subject_public_key: [u8; 32],
137    /// SHA-256 fingerprint of [`Self::der_bytes`].
138    pub fingerprint: [u8; 32],
139    /// Instant after which the certificate is no longer valid.
140    pub expires_at: DateTime<Utc>,
141    /// The 32-byte Ed25519 public key of the issuing authority.
142    pub issuer_public_key: [u8; 32],
143}
144
145impl CertifiedKey {
146    /// Builds a [`CertifiedKey`] from signed DER and the presented subject key.
147    ///
148    /// Computes the SHA-256 fingerprint over `der_bytes` and confirms that the
149    /// certificate actually binds `subject_public_key` — see
150    /// `assert_der_binds_subject_key`.
151    ///
152    /// # Errors
153    ///
154    /// Returns [`TrustError::CertificateGeneration`] if the certificate cannot
155    /// be parsed back, does not carry an Ed25519 subject key, or binds a key
156    /// other than `subject_public_key`.
157    pub fn from_der_and_public_key(
158        der_bytes: Vec<u8>,
159        subject_public_key: &[u8; 32],
160        expires_at: DateTime<Utc>,
161        issuer_public_key: [u8; 32],
162    ) -> TrustResult<Self> {
163        assert_der_binds_subject_key(&der_bytes, subject_public_key)?;
164        let fingerprint: [u8; 32] = Sha256::digest(&der_bytes).into();
165
166        Ok(Self {
167            der_bytes,
168            subject_public_key: *subject_public_key,
169            fingerprint,
170            expires_at,
171            issuer_public_key,
172        })
173    }
174}
175
176impl fmt::Debug for CertifiedKey {
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        f.debug_struct("CertifiedKey")
179            .field("der_bytes_len", &self.der_bytes.len())
180            .field("subject_public_key", &hex_lower(&self.subject_public_key))
181            .field("fingerprint", &hex_lower(&self.fingerprint))
182            .field("expires_at", &self.expires_at)
183            .field("issuer_public_key", &hex_lower(&self.issuer_public_key))
184            .finish()
185    }
186}
187
188/// Reads the 32-byte Ed25519 subject public key out of a certificate's
189/// `subjectPublicKeyInfo`.
190///
191/// This is the key a certificate *vouches for*. Recovering it is what lets a
192/// verifier close the join between a certificate and something the subject
193/// signed: verifying a certificate proves the authority issued it, and
194/// verifying an attestation proves some key signed a payload, but neither says
195/// the two concern the same identity. Comparing this against an attestation's
196/// signer key is the check that connects them.
197///
198/// Parsing only — this says nothing about whether the certificate verifies, and
199/// callers must not treat a successful read as any kind of endorsement. Verify
200/// the chain with [`verify_certificate_chain_at`](crate::ca::verify_certificate_chain_at)
201/// first; a key recovered from an unverified certificate is an attacker-chosen
202/// value.
203///
204/// # Errors
205///
206/// Returns [`TrustError::CertificateParsing`] if `cert_der` cannot be parsed,
207/// the subject key algorithm is not Ed25519, the algorithm carries parameters
208/// (RFC 8410 §3 requires their absence), or the key is not exactly 32 bytes in
209/// a BIT STRING with no unused bits.
210pub fn certificate_subject_public_key(cert_der: &[u8]) -> TrustResult<[u8; 32]> {
211    let (_, certificate) =
212        X509Certificate::from_der(cert_der).map_err(|e| TrustError::CertificateParsing {
213            reason: format!("failed to parse certificate DER: {e:?}"),
214        })?;
215
216    let spki = &certificate.tbs_certificate.subject_pki;
217    if spki.algorithm.algorithm != OID_SIG_ED25519 {
218        return Err(TrustError::CertificateParsing {
219            reason: "certificate subject key algorithm is not Ed25519".to_string(),
220        });
221    }
222    if spki.algorithm.parameters.is_some() {
223        return Err(TrustError::CertificateParsing {
224            reason: "Ed25519 subject key algorithm must carry no parameters (RFC 8410)".to_string(),
225        });
226    }
227    if spki.subject_public_key.unused_bits != 0 {
228        return Err(TrustError::CertificateParsing {
229            reason: "certificate subject key BIT STRING must have no unused bits".to_string(),
230        });
231    }
232
233    let data = spki.subject_public_key.data.as_ref();
234    data.try_into()
235        .map_err(|_err| TrustError::CertificateParsing {
236            reason: format!(
237                "certificate subject key must be 32 bytes for Ed25519, got {}",
238                data.len()
239            ),
240        })
241}
242
243/// Confirms that a freshly issued certificate's `subjectPublicKeyInfo` carries
244/// exactly `expected`.
245///
246/// This is a self-check on our own output, not a verification of a stranger's
247/// artifact: it exists so that "issuance succeeded" cannot be reported for a
248/// certificate that binds a different key than the one the caller asked to
249/// certify. The whole value of the presented-key path is that the certified
250/// key is the holder's, so that binding is the one thing worth re-reading from
251/// the bytes rather than assuming.
252///
253/// Failures are reported as [`TrustError::CertificateGeneration`] rather than
254/// the parsing error [`certificate_subject_public_key`] returns: at this point
255/// the malformed certificate is one *we* just produced, and that distinction
256/// matters to whoever reads the error.
257fn assert_der_binds_subject_key(der_bytes: &[u8], expected: &[u8; 32]) -> TrustResult<()> {
258    let subject_key = certificate_subject_public_key(der_bytes).map_err(|e| {
259        TrustError::CertificateGeneration {
260            reason: format!("freshly issued certificate could not be read back: {e}"),
261        }
262    })?;
263    if &subject_key != expected {
264        return Err(TrustError::CertificateGeneration {
265            reason: "freshly issued certificate binds a different subject key than requested"
266                .to_string(),
267        });
268    }
269    Ok(())
270}
271
272impl fmt::Debug for IssuedCertificate {
273    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274        f.debug_struct("IssuedCertificate")
275            .field("der_bytes_len", &self.der_bytes.len())
276            .field("subject_signing_key", &"[REDACTED]")
277            .field(
278                "subject_verifying_key",
279                &hex_lower(&self.subject_verifying_key.to_bytes()),
280            )
281            .field("fingerprint", &hex_lower(&self.fingerprint))
282            .field("expires_at", &self.expires_at)
283            .field("issuer_public_key", &hex_lower(&self.issuer_public_key))
284            .finish()
285    }
286}
287
288#[cfg(test)]
289#[path = "certificate_tests.rs"]
290mod tests;