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;