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    pub fn subject_public_key(&self) -> &[u8; ED25519_KEY_LEN] {
135        &self.subject_public_key
136    }
137
138    /// The single common name the request asked to be certified under.
139    pub fn common_name(&self) -> &str {
140        &self.common_name
141    }
142}
143
144/// Builds a PKCS#10 certificate-signing request for `subject`, self-signed by
145/// `identity`.
146///
147/// The returned DER is the holder's side of the exchange: it carries
148/// `identity`'s public key and a signature over the request made with the
149/// matching private key, which is what
150/// [`verify_certificate_request`] checks. The private seed is reached only
151/// through `IdentitySigner`, so it is never serialised into an rcgen
152/// keypair.
153///
154/// The request is deliberately minimal — one common name, no subject
155/// alternative names, no requested extensions — because a request may not
156/// influence any certificate field other than the subject key, and a request
157/// carrying extensions would be rejected at verification.
158///
159/// # Errors
160///
161/// Returns [`TrustError::CertificateGeneration`] if `subject` is empty or
162/// only whitespace, or if rcgen cannot build or sign the request.
163pub fn create_certificate_request(
164    identity: &Arc<Ed25519Identity>,
165    subject: &str,
166) -> TrustResult<Vec<u8>> {
167    if subject.trim().is_empty() {
168        return Err(TrustError::CertificateGeneration {
169            reason: "certificate-signing request subject must not be empty".to_string(),
170        });
171    }
172
173    let mut params = CertificateParams::new(Vec::<String>::new()).map_err(|e| {
174        TrustError::CertificateGeneration {
175            reason: format!("failed to build certificate-signing request parameters: {e}"),
176        }
177    })?;
178    params.distinguished_name = distinguished_name(subject);
179
180    let signer = IdentitySigner::new(Arc::clone(identity));
181    let key_pair =
182        KeyPair::from_remote(Box::new(signer)).map_err(|e| TrustError::CertificateGeneration {
183            reason: format!("failed to construct request key from identity: {e}"),
184        })?;
185
186    let request =
187        params
188            .serialize_request(&key_pair)
189            .map_err(|e| TrustError::CertificateGeneration {
190                reason: format!("failed to sign certificate-signing request: {e}"),
191            })?;
192
193    Ok(request.der().to_vec())
194}
195
196/// Verifies a PKCS#10 request's proof of possession and returns the subject
197/// key it establishes.
198///
199/// Parses `request_der`, then requires all of the following. Every check is a
200/// rejection, never a repair:
201///
202/// - no trailing bytes after the request's outer `SEQUENCE`;
203/// - `CertificationRequestInfo` version v1, per RFC 2986;
204/// - the signature algorithm is Ed25519;
205/// - the subject public key algorithm is Ed25519, with the `parameters` field
206///   absent as RFC 8410 §3 requires;
207/// - the subject public key is exactly 32 bytes and the signature exactly 64,
208///   each in a BIT STRING with no unused bits;
209/// - exactly one common name, non-empty;
210/// - no requested extensions;
211/// - the signature verifies over the raw `CertificationRequestInfo` bytes
212///   under the subject key, using `ed25519-dalek` **strict** verification.
213///
214/// Strict verification is what makes this a proof of possession rather than a
215/// formality. Plain Ed25519 verification accepts small-order and torsion
216/// public keys, for which signatures can be produced without knowing any
217/// private key — so a requester could present a key nobody controls and still
218/// satisfy the check, which is precisely the guarantee this function exists to
219/// provide. `verify_strict` rejects those keys and signature malleability,
220/// matching [`verify_certificate_chain_at`](super::verify_certificate_chain_at).
221///
222/// x509-parser's own `verify_signature` is deliberately not used: it routes
223/// Ed25519 through ring's non-strict verification, which accepts exactly the
224/// keys named above.
225///
226/// # Errors
227///
228/// Returns [`TrustError::CertificateParsing`] if `request_der` is not a
229/// well-formed PKCS#10 request or carries structurally invalid fields, and
230/// [`TrustError::CertificateVerification`] if the request is well-formed but
231/// its self-signature does not verify. These reuse the certificate error
232/// variants rather than introducing request-specific ones, which would be a
233/// breaking change to a published error enum.
234pub fn verify_certificate_request(request_der: &[u8]) -> TrustResult<VerifiedRequest> {
235    let (remaining, request) = X509CertificationRequest::from_der(request_der).map_err(|e| {
236        TrustError::CertificateParsing {
237            reason: format!("failed to parse certificate-signing request DER: {e:?}"),
238        }
239    })?;
240    // Trailing bytes would let two distinct encodings carry the same request,
241    // so anything appended to the outer SEQUENCE is a rejection.
242    if !remaining.is_empty() {
243        return Err(TrustError::CertificateParsing {
244            reason: format!(
245                "certificate-signing request has {} trailing byte(s) after the outer SEQUENCE",
246                remaining.len()
247            ),
248        });
249    }
250
251    let info = &request.certification_request_info;
252    if info.version != X509Version::V1 {
253        return Err(TrustError::CertificateParsing {
254            reason: format!(
255                "certificate-signing request version must be v1 per RFC 2986, got {}",
256                info.version.0
257            ),
258        });
259    }
260
261    if request.signature_algorithm.algorithm != OID_SIG_ED25519 {
262        return Err(TrustError::CertificateParsing {
263            reason: "certificate-signing request signature algorithm is not Ed25519".to_string(),
264        });
265    }
266    if info.subject_pki.algorithm.algorithm != OID_SIG_ED25519 {
267        return Err(TrustError::CertificateParsing {
268            reason: "certificate-signing request subject key algorithm is not Ed25519".to_string(),
269        });
270    }
271    // RFC 8410 §3: the parameters field MUST be absent for Ed25519. Accepting
272    // a populated field would admit two encodings of the same key algorithm.
273    if info.subject_pki.algorithm.parameters.is_some() {
274        return Err(TrustError::CertificateParsing {
275            reason: "Ed25519 subject key algorithm must carry no parameters (RFC 8410)".to_string(),
276        });
277    }
278
279    let subject_public_key = ed25519_bit_string(
280        info.subject_pki.subject_public_key.unused_bits,
281        &info.subject_pki.subject_public_key.data,
282        ED25519_KEY_LEN,
283        "subject public key",
284    )?;
285    let subject_public_key: [u8; ED25519_KEY_LEN] =
286        subject_public_key
287            .try_into()
288            .map_err(|_err| TrustError::CertificateParsing {
289                reason: "subject public key length check did not yield 32 bytes".to_string(),
290            })?;
291
292    let signature_bytes = ed25519_bit_string(
293        request.signature_value.unused_bits,
294        &request.signature_value.data,
295        ED25519_SIGNATURE_LEN,
296        "signature",
297    )?;
298    let signature_bytes: [u8; ED25519_SIGNATURE_LEN] =
299        signature_bytes
300            .try_into()
301            .map_err(|_err| TrustError::CertificateParsing {
302                reason: "signature length check did not yield 64 bytes".to_string(),
303            })?;
304
305    let common_name = single_common_name(info)?;
306
307    // Requested extensions are refused, not stripped: a holder asking to be a
308    // CA, or to carry capability claims of their own choosing, must be told no
309    // rather than handed a certificate that silently omits the request.
310    if let Some(mut extensions) = request.requested_extensions() {
311        if extensions.next().is_some() {
312            return Err(TrustError::CertificateParsing {
313                reason: "certificate-signing request carries requested extensions, which are \
314                         never honoured — the authority chooses every certificate field other \
315                         than the subject key"
316                    .to_string(),
317            });
318        }
319    }
320
321    let verifying_key = VerifyingKey::from_bytes(&subject_public_key).map_err(|_err| {
322        TrustError::CertificateVerification {
323            reason: "certificate-signing request subject key is not a valid Ed25519 point"
324                .to_string(),
325        }
326    })?;
327    let signature = Signature::from_bytes(&signature_bytes);
328    verifying_key
329        .verify_strict(info.raw, &signature)
330        .map_err(|_err| TrustError::CertificateVerification {
331            reason: "certificate-signing request signature did not verify under the subject \
332                     key it presents — proof of possession failed"
333                .to_string(),
334        })?;
335
336    Ok(VerifiedRequest {
337        subject_public_key,
338        common_name,
339    })
340}
341
342/// Extracts a fixed-length Ed25519 value from a BIT STRING, rejecting unused
343/// bits and any other length.
344fn ed25519_bit_string<'a>(
345    unused_bits: u8,
346    data: &'a [u8],
347    expected_len: usize,
348    field: &str,
349) -> TrustResult<&'a [u8]> {
350    if unused_bits != 0 {
351        return Err(TrustError::CertificateParsing {
352            reason: format!(
353                "certificate-signing request {field} BIT STRING must have no unused bits, got \
354                 {unused_bits}"
355            ),
356        });
357    }
358    if data.len() != expected_len {
359        return Err(TrustError::CertificateParsing {
360            reason: format!(
361                "certificate-signing request {field} must be {expected_len} bytes for Ed25519, \
362                 got {}",
363                data.len()
364            ),
365        });
366    }
367    Ok(data)
368}
369
370/// Returns the request's single common name.
371///
372/// More than one common name is rejected rather than resolved: the authority
373/// compares one name against the subject it was asked to certify while a
374/// reader might display another, and that disagreement is a confusion surface
375/// with no legitimate use.
376fn single_common_name(
377    info: &x509_parser::certification_request::X509CertificationRequestInfo<'_>,
378) -> TrustResult<String> {
379    let mut names = info.subject.iter_common_name();
380    let first = names.next().ok_or_else(|| TrustError::CertificateParsing {
381        reason: "certificate-signing request subject carries no common name".to_string(),
382    })?;
383    if names.next().is_some() {
384        return Err(TrustError::CertificateParsing {
385            reason: "certificate-signing request subject carries more than one common name"
386                .to_string(),
387        });
388    }
389
390    let common_name = first
391        .as_str()
392        .map_err(|e| TrustError::CertificateParsing {
393            reason: format!("certificate-signing request common name is not printable text: {e}"),
394        })?
395        .to_string();
396    if common_name.trim().is_empty() {
397        return Err(TrustError::CertificateParsing {
398            reason: "certificate-signing request common name must not be empty".to_string(),
399        });
400    }
401    Ok(common_name)
402}
403
404#[cfg(test)]
405#[path = "request_tests.rs"]
406mod tests;