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    Ok(certificate_signing_key(cert_der)?.public_key)
212}
213
214/// The subject key and the validity interval carried by a certificate.
215#[derive(Debug, Clone, Copy, PartialEq, Eq)]
216pub struct CertificateSigningKey {
217    /// The Ed25519 subject public key.
218    pub public_key: [u8; 32],
219    /// First valid second since the Unix epoch.
220    pub not_before: i64,
221    /// Last valid second since the Unix epoch.
222    pub not_after: i64,
223}
224
225/// Decode an Ed25519 subject key and its validity interval once.
226///
227/// # Errors
228/// Returns [`TrustError::CertificateParsing`] for malformed DER or a subject
229/// key that is not a parameter-free, 32-byte Ed25519 key.
230pub fn certificate_signing_key(cert_der: &[u8]) -> TrustResult<CertificateSigningKey> {
231    let (_, certificate) =
232        X509Certificate::from_der(cert_der).map_err(|e| TrustError::CertificateParsing {
233            reason: format!("failed to parse certificate DER: {e:?}"),
234        })?;
235
236    let spki = &certificate.tbs_certificate.subject_pki;
237    if spki.algorithm.algorithm != OID_SIG_ED25519 {
238        return Err(TrustError::CertificateParsing {
239            reason: "certificate subject key algorithm is not Ed25519".to_string(),
240        });
241    }
242    if spki.algorithm.parameters.is_some() {
243        return Err(TrustError::CertificateParsing {
244            reason: "Ed25519 subject key algorithm must carry no parameters (RFC 8410)".to_string(),
245        });
246    }
247    if spki.subject_public_key.unused_bits != 0 {
248        return Err(TrustError::CertificateParsing {
249            reason: "certificate subject key BIT STRING must have no unused bits".to_string(),
250        });
251    }
252
253    let data = spki.subject_public_key.data.as_ref();
254    let public_key = data
255        .try_into()
256        .map_err(|_err| TrustError::CertificateParsing {
257            reason: format!(
258                "certificate subject key must be 32 bytes for Ed25519, got {}",
259                data.len()
260            ),
261        })?;
262    Ok(CertificateSigningKey {
263        public_key,
264        not_before: certificate.validity().not_before.timestamp(),
265        not_after: certificate.validity().not_after.timestamp(),
266    })
267}
268
269/// Confirms that a freshly issued certificate's `subjectPublicKeyInfo` carries
270/// exactly `expected`.
271///
272/// This is a self-check on our own output, not a verification of a stranger's
273/// artifact: it exists so that "issuance succeeded" cannot be reported for a
274/// certificate that binds a different key than the one the caller asked to
275/// certify. The whole value of the presented-key path is that the certified
276/// key is the holder's, so that binding is the one thing worth re-reading from
277/// the bytes rather than assuming.
278///
279/// Failures are reported as [`TrustError::CertificateGeneration`] rather than
280/// the parsing error [`certificate_subject_public_key`] returns: at this point
281/// the malformed certificate is one *we* just produced, and that distinction
282/// matters to whoever reads the error.
283fn assert_der_binds_subject_key(der_bytes: &[u8], expected: &[u8; 32]) -> TrustResult<()> {
284    let subject_key = certificate_subject_public_key(der_bytes).map_err(|e| {
285        TrustError::CertificateGeneration {
286            reason: format!("freshly issued certificate could not be read back: {e}"),
287        }
288    })?;
289    if &subject_key != expected {
290        return Err(TrustError::CertificateGeneration {
291            reason: "freshly issued certificate binds a different subject key than requested"
292                .to_string(),
293        });
294    }
295    Ok(())
296}
297
298impl fmt::Debug for IssuedCertificate {
299    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300        f.debug_struct("IssuedCertificate")
301            .field("der_bytes_len", &self.der_bytes.len())
302            .field("subject_signing_key", &"[REDACTED]")
303            .field(
304                "subject_verifying_key",
305                &hex_lower(&self.subject_verifying_key.to_bytes()),
306            )
307            .field("fingerprint", &hex_lower(&self.fingerprint))
308            .field("expires_at", &self.expires_at)
309            .field("issuer_public_key", &hex_lower(&self.issuer_public_key))
310            .finish()
311    }
312}
313
314#[cfg(test)]
315#[path = "certificate_tests.rs"]
316mod tests;