Skip to main content

lys_core/ca/
request.rs

1//! PKCS#10 certificate-signing requests — proof of possession for a subject
2//! key the holder already controls.
3//!
4//! Without this module a certificate can only be issued over a keypair the
5//! authority itself generated (see
6//! [`CertificateAuthority::issue_certificate`](super::CertificateAuthority::issue_certificate)),
7//! which means the certificate binds a key the *authority* held, not one the
8//! subject proved control of. Anything built on top of such a certificate —
9//! transparently logging issuance, gating a write path on a recognised chain —
10//! authenticates the authority's willingness to issue rather than the
11//! identity of the holder. This module closes that gap: a holder signs a
12//! request with their own key, and that self-signature is the proof of
13//! possession.
14//!
15//! # What proof of possession actually prevents
16//!
17//! It is tempting to read proof of possession as "the verifier learns the
18//! holder controls the key", and then to conclude it is redundant — because a
19//! verifier who checks a signature against the certified key has just observed
20//! that control directly and far more recently. That reading is wrong, and
21//! getting it wrong is how an authority ends up dropping the check as
22//! ceremony.
23//!
24//! The attack it prevents is **misattribution by key binding**, and it targets
25//! the authority, not the verifier. Suppose Mallory may request certificates
26//! and presents Noor's public key under the subject name `agent-mallory`.
27//! Without proof of possession the authority issues it, and from then on every
28//! statement Noor legitimately signs verifies against a certificate naming
29//! Mallory. Noor's work is attributed to Mallory, by a certificate that is
30//! genuinely valid, with no forgery anywhere and nothing for a verifier to
31//! detect. The join in `lys verify --cert` would report success, correctly, on
32//! a false statement.
33//!
34//! Both paths this crate offers are safe against that: `issue_certificate`
35//! generates the key itself so no caller-supplied key is ever bound, and this
36//! module requires a signature over the request — which includes the subject
37//! name — so a presented key can only be certified by whoever holds it, and
38//! only under the name they asked for.
39//!
40//! # A certificate does not record how it was issued
41//!
42//! Nothing in X.509 marks a certificate as having been issued over a proven
43//! key, and this crate adds no such marker. A relying party therefore **cannot
44//! tell from the certificate alone** whether the certified key is one its
45//! holder controls or one an authority minted and discarded, and trust in that
46//! property rests on the issuer's policy rather than on the artifact.
47//!
48//! This is a limitation to state rather than paper over, but it is narrower
49//! than it first looks. A certificate over a discarded generated key binds a
50//! key nobody can sign with, so it can never satisfy a signature check — it
51//! fails closed, uselessly rather than dangerously. The property an issuer's
52//! policy must actually supply is the one above: that it never binds a key its
53//! requester did not prove control of. Making that policy publicly auditable is
54//! the argument for logging issuance transparently, rather than something a
55//! certificate field could carry.
56//!
57//! # Requests are replayable, deliberately
58//!
59//! A request's signature covers the subject key and the requested name, and
60//! nothing that ties it to one issuance, one authority, or one moment. Anyone
61//! holding a copy can submit it again, to this authority or another, and get a
62//! certificate over that key under that name.
63//!
64//! That is not a weakness, because proof of possession is a claim about key
65//! control and not an authorisation of a particular issuance. A replayed
66//! request yields a certificate naming the same holder over the same key they
67//! already proved they hold — a public fact anybody could have asked to have
68//! certified. An authority that needs issuance itself to be authorised needs an
69//! access-control decision about the requester, which is a separate concern
70//! from this one and must not be built on the request's signature.
71//!
72//! # Why PKCS#10 rather than a lys-native format
73//!
74//! RFC 2986 already specifies exactly this exchange, its self-signature is
75//! the canonical proof of possession, and third parties can produce and
76//! inspect requests with `openssl req` and equivalents in every language.
77//! Inventing a `lys/proof-of-possession/v1` tag would add a permanent wire
78//! contract nobody else speaks in order to restate a 1998 standard.
79//!
80//! # What a request is allowed to influence
81//!
82//! **Exactly one thing: the subject public key.** A verified request also
83//! carries the common name it asked for, which the authority compares against
84//! the subject it was told to issue for — agreement between the two is
85//! required, but the request never *supplies* the name. Validity window,
86//! extensions, basic constraints and every other certificate field are built
87//! by the authority from its own inputs. A request carrying requested
88//! extensions is rejected rather than silently stripped, because a holder
89//! whose asks are quietly dropped would believe they were honoured.
90//!
91//! # Verification is deliberately not non-oracle
92//!
93//! Artifact verification in this crate collapses every failure into one
94//! indistinguishable message so a verifier cannot be used as an oracle. That
95//! discipline does not apply here and precise diagnostics are correct: no
96//! secret of the authority's participates in checking a request, and the
97//! requester already knows their own key, so there is nothing an attacker
98//! could learn by distinguishing a malformed request from a bad signature.
99//! The authority operator, by contrast, needs to know which of the two it is.
100
101use std::sync::Arc;
102
103use ed25519_dalek::{Signature, VerifyingKey};
104use rcgen::{CertificateParams, KeyPair};
105use x509_parser::certification_request::X509CertificationRequest;
106use x509_parser::oid_registry::OID_SIG_ED25519;
107use x509_parser::prelude::FromDer;
108use x509_parser::x509::X509Version;
109
110use crate::ca::rcgen_bridge::{IdentitySigner, distinguished_name};
111use crate::error::{TrustError, TrustResult};
112use crate::keys::Ed25519Identity;
113
114/// The Ed25519 public-key and signature length, in bytes.
115const ED25519_KEY_LEN: usize = 32;
116/// The Ed25519 signature length, in bytes.
117const ED25519_SIGNATURE_LEN: usize = 64;
118
119/// A certificate-signing request whose proof of possession has been verified.
120///
121/// This type cannot be constructed except by
122/// [`verify_certificate_request`], and its fields are private. Holding one is
123/// therefore itself the evidence that the request's self-signature verified
124/// strictly under the key it carries — there is no path that produces a
125/// `VerifiedRequest` for a key the requester did not demonstrably control.
126#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct VerifiedRequest {
128    subject_public_key: [u8; ED25519_KEY_LEN],
129    common_name: String,
130}
131
132impl VerifiedRequest {
133    /// The 32-byte Ed25519 public key the requester proved possession of.
134    #[must_use]
135    pub fn subject_public_key(&self) -> &[u8; ED25519_KEY_LEN] {
136        &self.subject_public_key
137    }
138
139    /// The single common name the request asked to be certified under.
140    #[must_use]
141    pub fn common_name(&self) -> &str {
142        &self.common_name
143    }
144}
145
146/// Builds a PKCS#10 certificate-signing request for `subject`, self-signed by
147/// `identity`.
148///
149/// The returned DER is the holder's side of the exchange: it carries
150/// `identity`'s public key and a signature over the request made with the
151/// matching private key, which is what
152/// [`verify_certificate_request`] checks. The private seed is reached only
153/// through `IdentitySigner`, so it is never serialised into an rcgen
154/// keypair.
155///
156/// The request is deliberately minimal — one common name, no subject
157/// alternative names, no requested extensions — because a request may not
158/// influence any certificate field other than the subject key, and a request
159/// carrying extensions would be rejected at verification.
160///
161/// # Errors
162///
163/// Returns [`TrustError::CertificateGeneration`] if `subject` is empty or
164/// only whitespace, or if rcgen cannot build or sign the request.
165pub fn create_certificate_request(
166    identity: &Arc<Ed25519Identity>,
167    subject: &str,
168) -> TrustResult<Vec<u8>> {
169    if subject.trim().is_empty() {
170        return Err(TrustError::CertificateGeneration {
171            reason: "certificate-signing request subject must not be empty".to_string(),
172        });
173    }
174
175    let mut params = CertificateParams::new(Vec::<String>::new()).map_err(|e| {
176        TrustError::CertificateGeneration {
177            reason: format!("failed to build certificate-signing request parameters: {e}"),
178        }
179    })?;
180    params.distinguished_name = distinguished_name(subject);
181
182    let signer = IdentitySigner::new(Arc::clone(identity));
183    let key_pair =
184        KeyPair::from_remote(Box::new(signer)).map_err(|e| TrustError::CertificateGeneration {
185            reason: format!("failed to construct request key from identity: {e}"),
186        })?;
187
188    let request =
189        params
190            .serialize_request(&key_pair)
191            .map_err(|e| TrustError::CertificateGeneration {
192                reason: format!("failed to sign certificate-signing request: {e}"),
193            })?;
194
195    Ok(request.der().to_vec())
196}
197
198/// Verifies a PKCS#10 request's proof of possession and returns the subject
199/// key it establishes.
200///
201/// Parses `request_der`, then requires all of the following. Every check is a
202/// rejection, never a repair:
203///
204/// - no trailing bytes after the request's outer `SEQUENCE`;
205/// - `CertificationRequestInfo` version v1, per RFC 2986;
206/// - the signature algorithm is Ed25519;
207/// - the subject public key algorithm is Ed25519, with the `parameters` field
208///   absent as RFC 8410 §3 requires;
209/// - the subject public key is exactly 32 bytes and the signature exactly 64,
210///   each in a BIT STRING with no unused bits;
211/// - exactly one common name, non-empty;
212/// - no requested extensions;
213/// - the signature verifies over the raw `CertificationRequestInfo` bytes
214///   under the subject key, using `ed25519-dalek` **strict** verification.
215///
216/// Strict verification is what makes this a proof of possession rather than a
217/// formality. Plain Ed25519 verification accepts small-order and torsion
218/// public keys, for which signatures can be produced without knowing any
219/// private key — so a requester could present a key nobody controls and still
220/// satisfy the check, which is precisely the guarantee this function exists to
221/// provide. `verify_strict` rejects those keys and signature malleability,
222/// matching [`verify_certificate_chain_at`](super::verify_certificate_chain_at).
223///
224/// x509-parser's own `verify_signature` is deliberately not used: it routes
225/// Ed25519 through ring's non-strict verification, which accepts exactly the
226/// keys named above.
227///
228/// # Errors
229///
230/// Returns [`TrustError::CertificateParsing`] if `request_der` is not a
231/// well-formed PKCS#10 request or carries structurally invalid fields, and
232/// [`TrustError::CertificateVerification`] if the request is well-formed but
233/// its self-signature does not verify. These reuse the certificate error
234/// variants rather than introducing request-specific ones, which would be a
235/// breaking change to a published error enum.
236pub fn verify_certificate_request(request_der: &[u8]) -> TrustResult<VerifiedRequest> {
237    let (remaining, request) = X509CertificationRequest::from_der(request_der).map_err(|e| {
238        TrustError::CertificateParsing {
239            reason: format!("failed to parse certificate-signing request DER: {e:?}"),
240        }
241    })?;
242    // Trailing bytes would let two distinct encodings carry the same request,
243    // so anything appended to the outer SEQUENCE is a rejection.
244    if !remaining.is_empty() {
245        return Err(TrustError::CertificateParsing {
246            reason: format!(
247                "certificate-signing request has {} trailing byte(s) after the outer SEQUENCE",
248                remaining.len()
249            ),
250        });
251    }
252
253    let info = &request.certification_request_info;
254    if info.version != X509Version::V1 {
255        return Err(TrustError::CertificateParsing {
256            reason: format!(
257                "certificate-signing request version must be v1 per RFC 2986, got {}",
258                info.version.0
259            ),
260        });
261    }
262
263    if request.signature_algorithm.algorithm != OID_SIG_ED25519 {
264        return Err(TrustError::CertificateParsing {
265            reason: "certificate-signing request signature algorithm is not Ed25519".to_string(),
266        });
267    }
268    if info.subject_pki.algorithm.algorithm != OID_SIG_ED25519 {
269        return Err(TrustError::CertificateParsing {
270            reason: "certificate-signing request subject key algorithm is not Ed25519".to_string(),
271        });
272    }
273    // RFC 8410 §3: the parameters field MUST be absent for Ed25519. Accepting
274    // a populated field would admit two encodings of the same key algorithm.
275    if info.subject_pki.algorithm.parameters.is_some() {
276        return Err(TrustError::CertificateParsing {
277            reason: "Ed25519 subject key algorithm must carry no parameters (RFC 8410)".to_string(),
278        });
279    }
280
281    let subject_public_key = ed25519_bit_string(
282        info.subject_pki.subject_public_key.unused_bits,
283        &info.subject_pki.subject_public_key.data,
284        ED25519_KEY_LEN,
285        "subject public key",
286    )?;
287    let subject_public_key: [u8; ED25519_KEY_LEN] =
288        subject_public_key
289            .try_into()
290            .map_err(|_err| TrustError::CertificateParsing {
291                reason: "subject public key length check did not yield 32 bytes".to_string(),
292            })?;
293
294    let signature_bytes = ed25519_bit_string(
295        request.signature_value.unused_bits,
296        &request.signature_value.data,
297        ED25519_SIGNATURE_LEN,
298        "signature",
299    )?;
300    let signature_bytes: [u8; ED25519_SIGNATURE_LEN] =
301        signature_bytes
302            .try_into()
303            .map_err(|_err| TrustError::CertificateParsing {
304                reason: "signature length check did not yield 64 bytes".to_string(),
305            })?;
306
307    let common_name = single_common_name(info)?;
308
309    // Requested extensions are refused, not stripped: a holder asking to be a
310    // CA, or to carry capability claims of their own choosing, must be told no
311    // rather than handed a certificate that silently omits the request.
312    if let Some(mut extensions) = request.requested_extensions() {
313        if extensions.next().is_some() {
314            return Err(TrustError::CertificateParsing {
315                reason: "certificate-signing request carries requested extensions, which are \
316                         never honoured — the authority chooses every certificate field other \
317                         than the subject key"
318                    .to_string(),
319            });
320        }
321    }
322
323    let verifying_key = VerifyingKey::from_bytes(&subject_public_key).map_err(|_err| {
324        TrustError::CertificateVerification {
325            reason: "certificate-signing request subject key is not a valid Ed25519 point"
326                .to_string(),
327        }
328    })?;
329    let signature = Signature::from_bytes(&signature_bytes);
330    verifying_key
331        .verify_strict(info.raw, &signature)
332        .map_err(|_err| TrustError::CertificateVerification {
333            reason: "certificate-signing request signature did not verify under the subject \
334                     key it presents — proof of possession failed"
335                .to_string(),
336        })?;
337
338    Ok(VerifiedRequest {
339        subject_public_key,
340        common_name,
341    })
342}
343
344/// Extracts a fixed-length Ed25519 value from a BIT STRING, rejecting unused
345/// bits and any other length.
346fn ed25519_bit_string<'a>(
347    unused_bits: u8,
348    data: &'a [u8],
349    expected_len: usize,
350    field: &str,
351) -> TrustResult<&'a [u8]> {
352    if unused_bits != 0 {
353        return Err(TrustError::CertificateParsing {
354            reason: format!(
355                "certificate-signing request {field} BIT STRING must have no unused bits, got \
356                 {unused_bits}"
357            ),
358        });
359    }
360    if data.len() != expected_len {
361        return Err(TrustError::CertificateParsing {
362            reason: format!(
363                "certificate-signing request {field} must be {expected_len} bytes for Ed25519, \
364                 got {}",
365                data.len()
366            ),
367        });
368    }
369    Ok(data)
370}
371
372/// Returns the request's single common name.
373///
374/// More than one common name is rejected rather than resolved: the authority
375/// compares one name against the subject it was asked to certify while a
376/// reader might display another, and that disagreement is a confusion surface
377/// with no legitimate use.
378fn single_common_name(
379    info: &x509_parser::certification_request::X509CertificationRequestInfo<'_>,
380) -> TrustResult<String> {
381    let mut names = info.subject.iter_common_name();
382    let first = names.next().ok_or_else(|| TrustError::CertificateParsing {
383        reason: "certificate-signing request subject carries no common name".to_string(),
384    })?;
385    if names.next().is_some() {
386        return Err(TrustError::CertificateParsing {
387            reason: "certificate-signing request subject carries more than one common name"
388                .to_string(),
389        });
390    }
391
392    let common_name = first
393        .as_str()
394        .map_err(|e| TrustError::CertificateParsing {
395            reason: format!("certificate-signing request common name is not printable text: {e}"),
396        })?
397        .to_string();
398    if common_name.trim().is_empty() {
399        return Err(TrustError::CertificateParsing {
400            reason: "certificate-signing request common name must not be empty".to_string(),
401        });
402    }
403    Ok(common_name)
404}
405
406#[cfg(test)]
407#[path = "request_tests.rs"]
408mod tests;