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;