auths_keri/tls_cert.rs
1//! KEL-rooted X.509 leaf certificates — composing a KERI identity with TLS.
2//!
3//! TLS already authenticates endpoints through the WebPKI/CA system. This module
4//! lets a KERI AID compose *with* that pipe instead of replacing it: an X.509
5//! leaf certificate whose **trust roots in the AID's key event log**, not in a
6//! certificate authority. A stock TLS stack (rustls, OpenSSL, BoringSSL, Go
7//! `crypto/tls`) completes a handshake with the cert exactly as it would with any
8//! self-signed leaf; an *AID-aware* verifier additionally re-derives the trust by
9//! replaying the KEL — so deployment rides every load balancer, mesh, and client
10//! that already speaks TLS, while the identity stays self-certifying.
11//!
12//! ## How the cert chains to the KEL
13//!
14//! The leaf is *bound* to the AID, not signed by a CA. Two things tie them:
15//!
16//! 1. **A `did:keri:<aid>` URI in `subjectAltName`** — the SPIFFE X.509-SVID
17//! pattern (identity-in-SAN). It parses cleanly in any stock X.509 verifier
18//! (graceful degradation: a legacy verifier sees an ordinary URI SAN), and an
19//! AID-aware verifier reads the AID out of it.
20//! 2. **An `AuthsKeriBinding` certificate extension** carrying the AID's resolved
21//! key-state — the AID prefix, every current signing key (CESR), and the KEL
22//! tip SAID. This is the projection of a KEL replay into the cert, so the
23//! verifier checks the cert against the *log*, never against a CA.
24//!
25//! The verifier (`verify_binds_to_key_state`, available with the `tls-cert`
26//! feature) replays the supplied KEL into a [`KeyState`] and asserts the cert's
27//! embedded binding equals that state. Trust is rooted in the log: change a
28//! current key in the cert and replay no longer agrees, so the cert is rejected.
29//!
30//! ## Unforgeability: the AID authorizes the TLS key
31//!
32//! The leaf carries its own ephemeral TLS keypair (so the long-term AID signing
33//! key never goes on the wire). The binding to the key-state alone is *not*
34//! unforgeable: a stock self-signed leaf only proves possession of the *TLS* key,
35//! and anyone replaying a public KEL could project the same key-state into a leaf
36//! minted over *their own* TLS key. So a KEL-rooted leaf additionally carries a
37//! [`TlsKeyAuthorization`] — a KERI signature, by one of the AID's *current*
38//! signing keys, over the leaf's `SubjectPublicKeyInfo` DER. That signature is the
39//! proof the AID authorized *this* TLS key: an attacker who never held the AID's
40//! signing key cannot produce it, even with the full public KEL in hand.
41//!
42//! The adversarial verifier ([`verify_authorized_against_key_state`]) re-roots
43//! trust in the log and rejects every forgery class:
44//!
45//! * **forged binding** — a leaf whose embedded key-state matches the replay but
46//! whose TLS key the AID never signed → [`TlsCertError::Unauthorized`];
47//! * **stripped binding / authorization** — a plain leaf, or one missing the
48//! authorization → [`TlsCertError::MissingBinding`] / [`TlsCertError::MissingAuthorization`];
49//! * **revoked / rotated AID** — a leaf whose embedded key-state diverges from a
50//! fresh replay of the *current* KEL → [`TlsCertError::BindingMismatch`];
51//! * **SAN spoof** — a `did:keri` SAN that disagrees with the binding →
52//! [`TlsCertError::SanMismatch`].
53//!
54//! Relay/MITM (a proof lifted off one TLS channel and replayed on another) is
55//! rejected one layer up, by the session's channel binding to the TLS exporter;
56//! the cert proves *who*, the channel binding proves *which connection*.
57//!
58//! ## Parse, don't validate
59//!
60//! [`AuthsKeriBinding`] is a parsed type: [`AuthsKeriBinding::from_key_state`]
61//! builds it only from a resolved key-state, and
62//! [`AuthsKeriBinding::from_canonical_json`] is total over its serialized form —
63//! an ill-formed extension cannot be represented as a binding, it is an error at
64//! the boundary.
65
66use serde::{Deserialize, Serialize};
67
68use crate::keys::{KeriDecodeError, KeriPublicKey};
69use crate::state::KeyState;
70
71/// OID of the `AuthsKeriBinding` certificate extension, under the
72/// Private Enterprise arc `1.3.6.1.4.1.59999` (`auths`), extension `.1.1`.
73///
74/// The content is the DER encoding of an OCTET STRING wrapping the canonical
75/// JSON of an [`AuthsKeriBinding`]. The extension is **non-critical**: a legacy
76/// X.509 verifier that does not understand it ignores it (graceful degradation),
77/// while an AID-aware verifier reads it to re-root trust in the KEL.
78pub const AUTHS_KERI_BINDING_OID: &[u64] = &[1, 3, 6, 1, 4, 1, 59999, 1, 1];
79
80/// The `did:keri` DID method scheme prefix used in the certificate SAN URI.
81pub const DID_KERI_SCHEME: &str = "did:keri:";
82
83/// Errors building or verifying a KEL-rooted certificate.
84#[derive(Debug, thiserror::Error)]
85#[non_exhaustive]
86pub enum TlsCertError {
87 /// A current signing key in the key-state could not be decoded.
88 #[error("decode AID key-state key: {0}")]
89 Key(#[from] KeriDecodeError),
90
91 /// X.509 certificate generation failed in the backend.
92 #[error("generate certificate: {0}")]
93 Generate(String),
94
95 /// The supplied TLS key material could not be loaded as a keypair.
96 #[error("load TLS keypair: {0}")]
97 KeyPair(String),
98
99 /// The certificate PEM/DER could not be parsed.
100 #[error("parse certificate: {0}")]
101 ParseCert(String),
102
103 /// The certificate carries no `AuthsKeriBinding` extension — it is not a
104 /// KEL-rooted auths certificate.
105 #[error("certificate carries no auths KEL binding extension")]
106 MissingBinding,
107
108 /// The `AuthsKeriBinding` extension content was not well-formed.
109 #[error("malformed auths KEL binding extension: {0}")]
110 MalformedBinding(String),
111
112 /// The certificate's binding does not match the replayed KEL key-state.
113 #[error("certificate binding does not match the replayed KEL: {0}")]
114 BindingMismatch(String),
115
116 /// The certificate's `did:keri` SAN is absent or does not match the binding.
117 #[error("certificate did:keri SAN mismatch: {0}")]
118 SanMismatch(String),
119
120 /// The certificate carries no `did:keri` URI in its `subjectAltName` — there
121 /// is no auths identity to read out of it (the X.509-SVID identity surface).
122 #[error("certificate carries no did:keri subjectAltName")]
123 NoSanIdentity,
124
125 /// The `did:keri` SAN was present but its AID is not a valid KERI prefix.
126 #[error("did:keri SAN carries an invalid AID: {0}")]
127 InvalidSanAid(#[from] crate::types::KeriTypeError),
128
129 /// The certificate binds to a key-state but carries no [`TlsKeyAuthorization`]
130 /// — there is no proof the AID authorized this TLS key, so it is unforgeable
131 /// only if rejected (the adversarial verifier requires the authorization).
132 #[error("certificate carries no AID authorization over its TLS key")]
133 MissingAuthorization,
134
135 /// The authorization names a current-key index outside the replayed key-state.
136 #[error("authorization key index {index} is out of range (key-state has {len} current keys)")]
137 AuthorizationIndexOutOfRange {
138 /// The out-of-range index the authorization claimed.
139 index: usize,
140 /// The number of current keys the replayed key-state actually has.
141 len: usize,
142 },
143
144 /// A current signing key named by the key-state could not be decoded when
145 /// checking the authorization (a malformed verkey reached the verifier).
146 #[error("decode authorizing key: {0}")]
147 AuthorizationKey(String),
148
149 /// The authorization signature does not verify: the AID's current key did not
150 /// sign this leaf's TLS public key, so the AID never authorized it (a forged
151 /// binding, or a relayed/substituted leaf).
152 #[error("AID did not authorize this TLS key: {0}")]
153 Unauthorized(String),
154}
155
156/// A KERI signature, by one of the AID's *current* signing keys, over the leaf's
157/// `SubjectPublicKeyInfo` DER — the proof the AID **authorized** this TLS key.
158///
159/// Without it, a KEL-rooted leaf only proves possession of the *TLS* key; with
160/// it, the leaf proves the AID's controller (the holder of a current signing key)
161/// bound *that specific* TLS key to the AID. Parse, don't validate: the signature
162/// is held as raw bytes (hex on the wire) and `key_index` is the position in the
163/// key-state's `current_keys` whose signature this is; an out-of-range index is an
164/// error at verification, never a silent skip.
165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
166pub struct TlsKeyAuthorization {
167 /// The index, in the key-state's `current_keys`, of the signing key that
168 /// produced `signature`. (Single-sig today; the index makes multi-sig
169 /// authorization a forward-compatible extension, not a reshape.)
170 pub key_index: usize,
171 /// The detached signature over the leaf's `SubjectPublicKeyInfo` DER, raw
172 /// bytes (serialized as hex inside the canonical JSON).
173 #[serde(with = "hex::serde")]
174 pub signature: Vec<u8>,
175}
176
177/// The AID key-state a KEL-rooted certificate embeds — the projection of a KEL
178/// replay into the cert, so a verifier checks the leaf against the *log*.
179///
180/// Field order and labels are stable (`serde_json` with `preserve_order`), so the
181/// JSON inside the extension is canonical across producers and the bytes a
182/// verifier re-derives from a fresh replay equal the bytes in the cert.
183#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
184pub struct AuthsKeriBinding {
185 /// The AID this certificate is bound to (the KEL prefix). Also the subject
186 /// of the `did:keri:<aid>` SAN.
187 pub aid: String,
188 /// Every current signing key of the AID, CESR-qualified, in KEL order.
189 pub current_keys: Vec<String>,
190 /// The SAID of the KEL tip the binding was projected from — the exact log
191 /// position whose replay must reproduce `current_keys`.
192 pub kel_tip: String,
193 /// The AID's authorization over the leaf's TLS key, when present. `None` for a
194 /// binding that names only the key-state (the discovery / identity surface);
195 /// the adversarial verifier ([`verify_authorized_against_key_state`]) requires
196 /// it, so an unauthorized leaf cannot pass the security check.
197 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub tls_key_authorization: Option<TlsKeyAuthorization>,
199}
200
201impl AuthsKeriBinding {
202 /// Project a resolved [`KeyState`] into a certificate binding (no
203 /// authorization yet — the key-state projection only).
204 ///
205 /// Every current key is decoded first (parse, don't validate), so a binding
206 /// is only ever built from keys that are valid for their curve — an
207 /// undecodable key is [`TlsCertError::Key`] at the boundary, never serialized
208 /// into a cert.
209 pub fn from_key_state(state: &KeyState) -> Result<Self, TlsCertError> {
210 let mut current_keys = Vec::with_capacity(state.current_keys.len());
211 for key in &state.current_keys {
212 // Decode to reject a malformed key before it reaches the cert.
213 KeriPublicKey::parse(key.as_str())?;
214 current_keys.push(key.as_str().to_string());
215 }
216 Ok(Self {
217 aid: state.prefix.as_str().to_string(),
218 current_keys,
219 kel_tip: state.last_event_said.as_str().to_string(),
220 tls_key_authorization: None,
221 })
222 }
223
224 /// The same projection, carrying the AID's authorization over the leaf's TLS
225 /// key. This is the binding a KEL-rooted leaf embeds once the AID has signed
226 /// its `SubjectPublicKeyInfo` DER.
227 pub fn with_authorization(mut self, authorization: TlsKeyAuthorization) -> Self {
228 self.tls_key_authorization = Some(authorization);
229 self
230 }
231
232 /// The `did:keri:<aid>` URI this binding's AID resolves to.
233 pub fn did_keri(&self) -> String {
234 format!("{DID_KERI_SCHEME}{}", self.aid)
235 }
236
237 /// Serialize to the canonical JSON bytes carried (DER-wrapped) in the cert.
238 pub fn to_canonical_json(&self) -> Vec<u8> {
239 // `serde_json` here is configured (workspace-wide) with preserve_order,
240 // and the struct field order is fixed, so this is deterministic.
241 serde_json::to_vec(self).unwrap_or_default()
242 }
243
244 /// Parse a binding from the canonical JSON bytes. Total over its input:
245 /// malformed JSON is [`TlsCertError::MalformedBinding`], not a panic.
246 pub fn from_canonical_json(bytes: &[u8]) -> Result<Self, TlsCertError> {
247 serde_json::from_slice(bytes).map_err(|e| TlsCertError::MalformedBinding(e.to_string()))
248 }
249}
250
251/// A port: signs the leaf's `SubjectPublicKeyInfo` DER with one of the AID's
252/// *current* signing keys, producing the authorization that proves the AID bound
253/// this TLS key. The core never imports a concrete key store — an adapter (a
254/// keychain-backed signer, a held seed) supplies the signature.
255///
256/// The contract: [`sign_tls_key`](TlsKeyAuthorizer::sign_tls_key) returns the
257/// detached signature over `spki_der`, and [`current_key_index`] is the position
258/// in the key-state's `current_keys` of the public key that signature verifies
259/// against. The verifier checks the signature against exactly that key, so an
260/// adapter that lies about either is caught at [`verify_authorized_against_key_state`].
261pub trait TlsKeyAuthorizer {
262 /// The index, in the key-state's `current_keys`, of the signing key used.
263 fn current_key_index(&self) -> usize;
264 /// Sign the leaf's `SubjectPublicKeyInfo` DER, returning the raw signature.
265 fn sign_tls_key(&self, spki_der: &[u8]) -> Result<Vec<u8>, TlsCertError>;
266}
267
268/// Verify a [`TlsKeyAuthorization`] against a binding's `current_keys` and the
269/// leaf's `SubjectPublicKeyInfo` DER — the unforgeability check.
270///
271/// The authorization must (1) name an in-range current-key index, (2) reference a
272/// decodable verkey, and (3) carry a signature that verifies, under that key, over
273/// the leaf's SPKI DER. Any failure is a rejection: the AID did not authorize this
274/// TLS key. Shared by every verify direction so there is one source of truth for
275/// "the AID signed this leaf." Only the cert backend (the `tls-cert` feature)
276/// extracts an SPKI to check, so it is gated alongside it.
277#[cfg(feature = "tls-cert")]
278fn check_tls_key_authorization(
279 authorization: &TlsKeyAuthorization,
280 current_keys: &[String],
281 spki_der: &[u8],
282) -> Result<(), TlsCertError> {
283 let key_str = current_keys.get(authorization.key_index).ok_or(
284 TlsCertError::AuthorizationIndexOutOfRange {
285 index: authorization.key_index,
286 len: current_keys.len(),
287 },
288 )?;
289 let key =
290 KeriPublicKey::parse(key_str).map_err(|e| TlsCertError::AuthorizationKey(e.to_string()))?;
291 key.verify_signature(spki_der, &authorization.signature)
292 .map_err(TlsCertError::Unauthorized)
293}
294
295#[cfg(feature = "tls-cert")]
296mod backend {
297 use super::*;
298
299 use rcgen::string::Ia5String;
300 use rcgen::{
301 CertificateParams, CustomExtension, DnType, ExtendedKeyUsagePurpose, KeyPair,
302 KeyUsagePurpose, PublicKeyData, SanType,
303 };
304
305 /// A freshly issued KEL-rooted leaf certificate plus its private key.
306 ///
307 /// The cert's subject public key is a fresh ephemeral TLS keypair (the AID's
308 /// long-term key is never put on the wire); the AID binding rides in the SAN
309 /// and the [`AUTHS_KERI_BINDING_OID`] extension.
310 pub struct IssuedCert {
311 /// The certificate, PEM-encoded.
312 pub cert_pem: String,
313 /// The ephemeral TLS private key, PKCS#8 PEM. Hand to the TLS acceptor.
314 pub key_pem: zeroize::Zeroizing<String>,
315 /// The binding the cert embeds (for the caller to echo / log).
316 pub binding: AuthsKeriBinding,
317 }
318
319 /// Issue a KEL-rooted leaf certificate for a resolved AID key-state.
320 ///
321 /// Generates a fresh P-256 TLS keypair, sets the subject CN and a
322 /// `did:keri:<aid>` URI SAN, embeds the [`AuthsKeriBinding`] extension, and
323 /// self-signs with the ephemeral key so a stock TLS stack completes a
324 /// handshake. `extra_sans` carries the transport host names/IPs the cert must
325 /// also be valid for (e.g. `localhost`, `127.0.0.1`) — without them a
326 /// hostname-checking client would reject the leaf even though the binding is
327 /// sound.
328 pub fn issue_kel_rooted_cert(
329 state: &KeyState,
330 extra_sans: &[String],
331 ) -> Result<IssuedCert, TlsCertError> {
332 let binding = AuthsKeriBinding::from_key_state(state)?;
333 let key_pair =
334 KeyPair::generate().map_err(|e| TlsCertError::KeyPair(format!("generate: {e}")))?;
335 issue_with_keypair(&binding, &key_pair, extra_sans)
336 }
337
338 /// Issue a KEL-rooted leaf from an existing PKCS#8-PEM TLS keypair.
339 ///
340 /// The deterministic path used by tests and by callers that already hold a
341 /// TLS key. The key is the cert's subject key and self-signs the leaf.
342 pub fn issue_kel_rooted_cert_with_key(
343 state: &KeyState,
344 tls_key_pkcs8_pem: &str,
345 extra_sans: &[String],
346 ) -> Result<IssuedCert, TlsCertError> {
347 let binding = AuthsKeriBinding::from_key_state(state)?;
348 let key_pair = KeyPair::from_pem(tls_key_pkcs8_pem)
349 .map_err(|e| TlsCertError::KeyPair(format!("from pem: {e}")))?;
350 issue_with_keypair(&binding, &key_pair, extra_sans)
351 }
352
353 /// Issue a KEL-rooted leaf whose TLS key the AID has **authorized**.
354 ///
355 /// Generates a fresh ephemeral TLS keypair, has `authorizer` sign that key's
356 /// `SubjectPublicKeyInfo` DER with one of the AID's current signing keys, and
357 /// embeds the resulting [`TlsKeyAuthorization`] in the binding. The leaf is
358 /// then unforgeable: only the AID's controller can produce the authorization,
359 /// so an attacker replaying the public KEL cannot mint a leaf over a TLS key of
360 /// their own choosing.
361 pub fn issue_authorized_kel_rooted_cert(
362 state: &KeyState,
363 authorizer: &dyn TlsKeyAuthorizer,
364 extra_sans: &[String],
365 ) -> Result<IssuedCert, TlsCertError> {
366 let key_pair =
367 KeyPair::generate().map_err(|e| TlsCertError::KeyPair(format!("generate: {e}")))?;
368 issue_authorized_with_keypair(state, authorizer, &key_pair, extra_sans)
369 }
370
371 /// The deterministic-key counterpart of [`issue_authorized_kel_rooted_cert`]:
372 /// authorize and issue over an existing PKCS#8-PEM TLS keypair.
373 pub fn issue_authorized_kel_rooted_cert_with_key(
374 state: &KeyState,
375 authorizer: &dyn TlsKeyAuthorizer,
376 tls_key_pkcs8_pem: &str,
377 extra_sans: &[String],
378 ) -> Result<IssuedCert, TlsCertError> {
379 let key_pair = KeyPair::from_pem(tls_key_pkcs8_pem)
380 .map_err(|e| TlsCertError::KeyPair(format!("from pem: {e}")))?;
381 issue_authorized_with_keypair(state, authorizer, &key_pair, extra_sans)
382 }
383
384 fn issue_authorized_with_keypair(
385 state: &KeyState,
386 authorizer: &dyn TlsKeyAuthorizer,
387 key_pair: &KeyPair,
388 extra_sans: &[String],
389 ) -> Result<IssuedCert, TlsCertError> {
390 let base = AuthsKeriBinding::from_key_state(state)?;
391
392 // The leaf's SubjectPublicKeyInfo DER — the exact bytes the verifier reads
393 // back out of the parsed certificate. Signing these binds the AID to *this*
394 // TLS key (one source of truth for "what the AID signed").
395 let spki_der = key_pair.subject_public_key_info();
396 let key_index = authorizer.current_key_index();
397 if base.current_keys.get(key_index).is_none() {
398 return Err(TlsCertError::AuthorizationIndexOutOfRange {
399 index: key_index,
400 len: base.current_keys.len(),
401 });
402 }
403 let signature = authorizer.sign_tls_key(&spki_der)?;
404 let authorization = TlsKeyAuthorization {
405 key_index,
406 signature,
407 };
408 // Reject an authorizer that signed with a key that doesn't match its
409 // claimed current key before the leaf ever leaves the issuer.
410 check_tls_key_authorization(&authorization, &base.current_keys, &spki_der)?;
411
412 let binding = base.with_authorization(authorization);
413 issue_with_keypair(&binding, key_pair, extra_sans)
414 }
415
416 /// Mint a leaf embedding `binding` over `key_pair`. `pub(crate)` so the
417 /// crate's adversarial tests can craft a leaf with a hand-built (e.g. forged)
418 /// binding; production callers go through the `issue_*` entry points which
419 /// build the binding from a replayed key-state.
420 pub(crate) fn issue_with_keypair(
421 binding: &AuthsKeriBinding,
422 key_pair: &KeyPair,
423 extra_sans: &[String],
424 ) -> Result<IssuedCert, TlsCertError> {
425 let mut params = CertificateParams::new(Vec::new())
426 .map_err(|e| TlsCertError::Generate(format!("params: {e}")))?;
427
428 // Subject CN = the did:keri DID, so the identity is visible even in tools
429 // that only print the subject.
430 params
431 .distinguished_name
432 .push(DnType::CommonName, binding.did_keri());
433
434 // SAN: the did:keri URI (the SPIFFE X.509-SVID identity-in-SAN pattern)
435 // plus any transport hostnames/IPs the leaf must serve.
436 let did_uri = Ia5String::try_from(binding.did_keri())
437 .map_err(|e| TlsCertError::Generate(format!("did:keri SAN: {e}")))?;
438 params.subject_alt_names.push(SanType::URI(did_uri));
439 for san in extra_sans {
440 params.subject_alt_names.push(san_for(san)?);
441 }
442
443 params.key_usages = vec![
444 KeyUsagePurpose::DigitalSignature,
445 KeyUsagePurpose::KeyEncipherment,
446 ];
447 params.extended_key_usages = vec![
448 ExtendedKeyUsagePurpose::ServerAuth,
449 ExtendedKeyUsagePurpose::ClientAuth,
450 ];
451
452 // The KEL binding: a non-critical extension carrying the replayed
453 // key-state, DER-wrapped as an OCTET STRING (the standard envelope for an
454 // opaque extension value).
455 let content = yasna_octet_string(&binding.to_canonical_json());
456 let mut ext = CustomExtension::from_oid_content(AUTHS_KERI_BINDING_OID, content);
457 ext.set_criticality(false);
458 params.custom_extensions.push(ext);
459
460 let cert = params
461 .self_signed(key_pair)
462 .map_err(|e| TlsCertError::Generate(format!("self-sign: {e}")))?;
463
464 Ok(IssuedCert {
465 cert_pem: cert.pem(),
466 key_pem: zeroize::Zeroizing::new(key_pair.serialize_pem()),
467 binding: binding.clone(),
468 })
469 }
470
471 /// Build a SAN entry from a host string: an IP literal becomes an IP SAN,
472 /// anything else a DNS SAN (matching how `rcgen`/stock stacks treat hosts).
473 fn san_for(host: &str) -> Result<SanType, TlsCertError> {
474 if let Ok(ip) = host.parse::<std::net::IpAddr>() {
475 Ok(SanType::IpAddress(ip))
476 } else {
477 let dns = Ia5String::try_from(host.to_string())
478 .map_err(|e| TlsCertError::Generate(format!("DNS SAN {host:?}: {e}")))?;
479 Ok(SanType::DnsName(dns))
480 }
481 }
482
483 /// DER-encode `bytes` as an OCTET STRING (the extension value envelope).
484 fn yasna_octet_string(bytes: &[u8]) -> Vec<u8> {
485 yasna::construct_der(|w| w.write_bytes(bytes))
486 }
487
488 /// Extract the [`AuthsKeriBinding`] embedded in a PEM certificate.
489 ///
490 /// Reads the `AUTHS_KERI_BINDING_OID` extension, unwraps the OCTET STRING, and
491 /// parses the canonical JSON. Errors classify the failure precisely:
492 /// [`TlsCertError::MissingBinding`] when there is no such extension (a plain
493 /// cert), [`TlsCertError::MalformedBinding`] when its content is not the
494 /// expected envelope.
495 pub fn extract_binding(cert_pem: &str) -> Result<AuthsKeriBinding, TlsCertError> {
496 use x509_parser::prelude::*;
497
498 let (_, pem) = x509_parser::pem::parse_x509_pem(cert_pem.as_bytes())
499 .map_err(|e| TlsCertError::ParseCert(format!("PEM: {e}")))?;
500 let (_, cert) = X509Certificate::from_der(&pem.contents)
501 .map_err(|e| TlsCertError::ParseCert(format!("DER: {e}")))?;
502
503 let oid_str = oid_string(AUTHS_KERI_BINDING_OID);
504 for ext in cert.extensions() {
505 if ext.oid.to_id_string() == oid_str {
506 let inner = unwrap_octet_string(ext.value)?;
507 return AuthsKeriBinding::from_canonical_json(&inner);
508 }
509 }
510 Err(TlsCertError::MissingBinding)
511 }
512
513 /// Read the `did:keri` URI SAN out of a PEM certificate, if present.
514 pub fn extract_did_keri_san(cert_pem: &str) -> Result<Option<String>, TlsCertError> {
515 use x509_parser::prelude::*;
516
517 let (_, pem) = x509_parser::pem::parse_x509_pem(cert_pem.as_bytes())
518 .map_err(|e| TlsCertError::ParseCert(format!("PEM: {e}")))?;
519 let (_, cert) = X509Certificate::from_der(&pem.contents)
520 .map_err(|e| TlsCertError::ParseCert(format!("DER: {e}")))?;
521
522 if let Ok(Some(san)) = cert.subject_alternative_name() {
523 for name in &san.value.general_names {
524 if let GeneralName::URI(uri) = name
525 && uri.starts_with(DID_KERI_SCHEME)
526 {
527 return Ok(Some((*uri).to_string()));
528 }
529 }
530 }
531 Ok(None)
532 }
533
534 /// Read the AID a certificate claims out of its `did:keri` SAN — the
535 /// X.509-SVID identity surface.
536 ///
537 /// This is the identity-discovery direction (the SPIFFE X.509-SVID precedent):
538 /// the AID rides in the `subjectAltName` URI every stock X.509 parser already
539 /// exposes, so a verifier learns *which* auths identity a peer claims directly
540 /// from the cert — **before** it holds that AID's KEL. The returned [`Prefix`]
541 /// is then the lookup key to fetch and replay the KEL (via an OOBI / a held
542 /// log), at which point [`verify_binds_to_key_state`] re-roots trust in the
543 /// log. Parse, don't validate: the scheme is stripped and the AID is parsed
544 /// into a validated [`Prefix`], so a present-but-malformed identifier is
545 /// [`TlsCertError::InvalidSanAid`] at the boundary, never a raw string the
546 /// caller has to re-check. A cert with no `did:keri` URI SAN is
547 /// [`TlsCertError::NoSanIdentity`] (a plain leaf carries no auths identity).
548 pub fn extract_aid_from_san(cert_pem: &str) -> Result<crate::types::Prefix, TlsCertError> {
549 match extract_did_keri_san(cert_pem)? {
550 Some(uri) => {
551 let aid = uri.strip_prefix(DID_KERI_SCHEME).ok_or_else(|| {
552 TlsCertError::SanMismatch(format!("SAN {uri} is not a {DID_KERI_SCHEME} URI"))
553 })?;
554 Ok(crate::types::Prefix::new(aid.to_string())?)
555 }
556 None => Err(TlsCertError::NoSanIdentity),
557 }
558 }
559
560 /// Read the leaf's `SubjectPublicKeyInfo` DER — the exact bytes the AID's
561 /// authorization signature covers. Re-derived from the parsed certificate, so
562 /// the verifier signs/checks over the canonical encoding (not a re-serialized
563 /// approximation).
564 pub fn extract_spki_der(cert_pem: &str) -> Result<Vec<u8>, TlsCertError> {
565 use x509_parser::prelude::*;
566
567 let (_, pem) = x509_parser::pem::parse_x509_pem(cert_pem.as_bytes())
568 .map_err(|e| TlsCertError::ParseCert(format!("PEM: {e}")))?;
569 let (_, cert) = X509Certificate::from_der(&pem.contents)
570 .map_err(|e| TlsCertError::ParseCert(format!("DER: {e}")))?;
571 Ok(cert.public_key().raw.to_vec())
572 }
573
574 /// Assert a parsed binding and the cert's SAN both agree with the replayed
575 /// key-state. The shared "the leaf chains to the log" check, with no
576 /// authorization — used directly by [`verify_binds_to_key_state`] and as the
577 /// first half of [`verify_authorized_against_key_state`].
578 fn check_binds_to_key_state(
579 cert_pem: &str,
580 embedded: &AuthsKeriBinding,
581 expected: &AuthsKeriBinding,
582 ) -> Result<(), TlsCertError> {
583 if embedded.aid != expected.aid {
584 return Err(TlsCertError::BindingMismatch(format!(
585 "AID {} in cert != {} from KEL replay",
586 embedded.aid, expected.aid
587 )));
588 }
589 if embedded.current_keys != expected.current_keys {
590 return Err(TlsCertError::BindingMismatch(
591 "current signing keys in cert do not match the KEL replay".to_string(),
592 ));
593 }
594 if embedded.kel_tip != expected.kel_tip {
595 return Err(TlsCertError::BindingMismatch(format!(
596 "KEL tip {} in cert != {} from replay",
597 embedded.kel_tip, expected.kel_tip
598 )));
599 }
600
601 // The SAN must carry the same AID — the legacy-compat identity surface
602 // must agree with the binding, or a tool reading only the SAN would trust
603 // a different AID than the one the binding (and the KEL) attest.
604 match extract_did_keri_san(cert_pem)? {
605 Some(san) if san == expected.did_keri() => Ok(()),
606 Some(san) => Err(TlsCertError::SanMismatch(format!(
607 "SAN {san} != {}",
608 expected.did_keri()
609 ))),
610 None => Err(TlsCertError::SanMismatch(
611 "certificate carries no did:keri SAN".to_string(),
612 )),
613 }
614 }
615
616 /// Verify a KEL-rooted certificate against an AID's KEL key-state.
617 ///
618 /// The peer→auths direction: parse the cert, read its embedded binding *and*
619 /// its `did:keri` SAN, then assert both agree with `state` — the freshly
620 /// replayed key-state of the KEL the verifier holds. Trust is rooted in the
621 /// log: a cert whose embedded key-state diverges from a real replay is
622 /// rejected ([`TlsCertError::BindingMismatch`]).
623 ///
624 /// This checks the leaf *chains to* the log; it does **not** check the AID
625 /// authorized the leaf's TLS key. For the adversarial guarantee (rejecting a
626 /// forged binding minted over an attacker's TLS key) use
627 /// [`verify_authorized_against_key_state`].
628 pub fn verify_binds_to_key_state(
629 cert_pem: &str,
630 state: &KeyState,
631 ) -> Result<AuthsKeriBinding, TlsCertError> {
632 let expected = AuthsKeriBinding::from_key_state(state)?;
633 let embedded = extract_binding(cert_pem)?;
634 check_binds_to_key_state(cert_pem, &embedded, &expected)?;
635 Ok(embedded)
636 }
637
638 /// The adversarial verifier (T3): a leaf passes only if it chains to the log
639 /// **and** the AID authorized its TLS key.
640 ///
641 /// On top of [`verify_binds_to_key_state`], this requires the embedded
642 /// [`TlsKeyAuthorization`] and checks it against the leaf's
643 /// `SubjectPublicKeyInfo` DER and the *replayed* current keys. The rejection
644 /// classes, each a distinct error:
645 ///
646 /// * a plain leaf (no extension) → [`TlsCertError::MissingBinding`];
647 /// * a leaf whose key-state diverges from the replay (revoked / rotated AID) →
648 /// [`TlsCertError::BindingMismatch`];
649 /// * a leaf whose SAN disagrees with the binding → [`TlsCertError::SanMismatch`];
650 /// * a leaf with no authorization (stripped) → [`TlsCertError::MissingAuthorization`];
651 /// * a leaf whose authorization does not verify under a current key — a forged
652 /// binding minted over a TLS key the AID never signed →
653 /// [`TlsCertError::Unauthorized`].
654 pub fn verify_authorized_against_key_state(
655 cert_pem: &str,
656 state: &KeyState,
657 ) -> Result<AuthsKeriBinding, TlsCertError> {
658 let expected = AuthsKeriBinding::from_key_state(state)?;
659 let embedded = extract_binding(cert_pem)?;
660 check_binds_to_key_state(cert_pem, &embedded, &expected)?;
661
662 let authorization = embedded
663 .tls_key_authorization
664 .as_ref()
665 .ok_or(TlsCertError::MissingAuthorization)?;
666 let spki_der = extract_spki_der(cert_pem)?;
667 // Check against the *replayed* current keys, not the embedded ones: the
668 // binding's keys were already asserted equal to the replay, but rooting the
669 // authorization check in the replay keeps the log the single source of truth.
670 check_tls_key_authorization(authorization, &expected.current_keys, &spki_der)?;
671 Ok(embedded)
672 }
673
674 /// Render an OID arc as the dotted string `x509-parser` exposes.
675 fn oid_string(arc: &[u64]) -> String {
676 arc.iter()
677 .map(|n| n.to_string())
678 .collect::<Vec<_>>()
679 .join(".")
680 }
681
682 /// Unwrap a DER OCTET STRING to its content bytes.
683 fn unwrap_octet_string(der: &[u8]) -> Result<Vec<u8>, TlsCertError> {
684 yasna::parse_der(der, |r| r.read_bytes())
685 .map_err(|e| TlsCertError::MalformedBinding(format!("OCTET STRING: {e}")))
686 }
687}
688
689#[cfg(feature = "tls-cert")]
690pub use backend::{
691 IssuedCert, extract_aid_from_san, extract_binding, extract_did_keri_san, extract_spki_der,
692 issue_authorized_kel_rooted_cert, issue_authorized_kel_rooted_cert_with_key,
693 issue_kel_rooted_cert, issue_kel_rooted_cert_with_key, verify_authorized_against_key_state,
694 verify_binds_to_key_state,
695};
696
697#[cfg(test)]
698#[allow(clippy::unwrap_used, clippy::expect_used)]
699mod tests {
700 use super::*;
701 use crate::types::{CesrKey, Prefix, Said, Threshold};
702
703 /// A single-key Ed25519 key-state at the given AID/key/tip.
704 fn state(aid: &str, keys: &[&str], tip: &str) -> KeyState {
705 KeyState::from_inception(
706 Prefix::new_unchecked(aid.to_string()),
707 keys.iter()
708 .map(|k| CesrKey::new_unchecked(k.to_string()))
709 .collect(),
710 vec![Said::new_unchecked("ENext0".to_string())],
711 Threshold::Simple(1),
712 Threshold::Simple(1),
713 Said::new_unchecked(tip.to_string()),
714 vec![],
715 Threshold::Simple(0),
716 vec![],
717 )
718 }
719
720 fn ed25519_key(raw: &[u8; 32]) -> String {
721 KeriPublicKey::ed25519(raw).unwrap().to_qb64().unwrap()
722 }
723
724 #[test]
725 fn binding_projects_key_state() {
726 let k = ed25519_key(&[7u8; 32]);
727 let st = state("EAidAAA", &[&k], "ETip000");
728 let b = AuthsKeriBinding::from_key_state(&st).unwrap();
729 assert_eq!(b.aid, "EAidAAA");
730 assert_eq!(b.current_keys, vec![k]);
731 assert_eq!(b.kel_tip, "ETip000");
732 assert_eq!(b.did_keri(), "did:keri:EAidAAA");
733 }
734
735 #[test]
736 fn binding_rejects_undecodable_key_at_boundary() {
737 let st = state("EAidAAA", &["Xnot-a-verkey"], "ETip000");
738 assert!(matches!(
739 AuthsKeriBinding::from_key_state(&st),
740 Err(TlsCertError::Key(_))
741 ));
742 }
743
744 #[test]
745 fn binding_canonical_json_round_trips() {
746 let k = ed25519_key(&[3u8; 32]);
747 let st = state("EAidAAA", &[&k], "ETip000");
748 let b = AuthsKeriBinding::from_key_state(&st).unwrap();
749 let json = b.to_canonical_json();
750 let back = AuthsKeriBinding::from_canonical_json(&json).unwrap();
751 assert_eq!(back, b);
752 }
753
754 #[test]
755 fn binding_json_field_order_is_canonical() {
756 let k = ed25519_key(&[1u8; 32]);
757 let st = state("EAidAAA", &[&k], "ETip000");
758 let b = AuthsKeriBinding::from_key_state(&st).unwrap();
759 let s = String::from_utf8(b.to_canonical_json()).unwrap();
760 // aid, current_keys, kel_tip — struct order, preserve_order serde.
761 let i_aid = s.find("\"aid\"").unwrap();
762 let i_keys = s.find("\"current_keys\"").unwrap();
763 let i_tip = s.find("\"kel_tip\"").unwrap();
764 assert!(i_aid < i_keys && i_keys < i_tip, "field order: {s}");
765 }
766
767 #[test]
768 fn malformed_binding_json_is_an_error_not_a_panic() {
769 assert!(matches!(
770 AuthsKeriBinding::from_canonical_json(b"not json"),
771 Err(TlsCertError::MalformedBinding(_))
772 ));
773 }
774
775 #[cfg(feature = "tls-cert")]
776 mod backend_tests {
777 use super::*;
778 // The crate-internal mint-with-binding entry point, for crafting leaves
779 // with hand-built (forged / out-of-range) bindings in the adversarial tests.
780 use crate::tls_cert::backend::issue_with_keypair;
781 use rcgen::PublicKeyData;
782
783 fn multi_state() -> (KeyState, Vec<String>) {
784 let k1 = ed25519_key(&[1u8; 32]);
785 let k2 = ed25519_key(&[2u8; 32]);
786 let st = state(
787 "EAidMultiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
788 &[&k1, &k2],
789 "ETipMultiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
790 );
791 (st, vec![k1, k2])
792 }
793
794 /// A test [`TlsKeyAuthorizer`] backed by a ring Ed25519 keypair. Signs the
795 /// SPKI directly (no `native` feature needed for the crate's own tests).
796 struct Ed25519Authorizer {
797 keypair: ring::signature::Ed25519KeyPair,
798 key_index: usize,
799 }
800
801 impl Ed25519Authorizer {
802 fn from_seed(seed: &[u8; 32], key_index: usize) -> Self {
803 let keypair = ring::signature::Ed25519KeyPair::from_seed_unchecked(seed).unwrap();
804 Self { keypair, key_index }
805 }
806 /// The CESR-qualified current key string this authorizer's key occupies.
807 fn cesr_key(&self) -> String {
808 use ring::signature::KeyPair;
809 let raw: [u8; 32] = self.keypair.public_key().as_ref().try_into().unwrap();
810 ed25519_key(&raw)
811 }
812 }
813
814 impl TlsKeyAuthorizer for Ed25519Authorizer {
815 fn current_key_index(&self) -> usize {
816 self.key_index
817 }
818 fn sign_tls_key(&self, spki_der: &[u8]) -> Result<Vec<u8>, TlsCertError> {
819 Ok(self.keypair.sign(spki_der).as_ref().to_vec())
820 }
821 }
822
823 /// A single-key key-state whose current key is `auth`'s public key, plus a
824 /// matching authorizer at index 0. The standard authorized-cert fixture.
825 fn authorized_state(seed: &[u8; 32]) -> (KeyState, Ed25519Authorizer) {
826 let auth = Ed25519Authorizer::from_seed(seed, 0);
827 let key = auth.cesr_key();
828 let st = state(
829 "EAidAuthAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
830 &[&key],
831 "ETipAuthAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
832 );
833 (st, auth)
834 }
835
836 #[test]
837 fn issued_cert_carries_binding_and_san() {
838 let (st, keys) = multi_state();
839 let issued =
840 issue_kel_rooted_cert(&st, &["localhost".to_string(), "127.0.0.1".to_string()])
841 .unwrap();
842 assert!(issued.cert_pem.contains("BEGIN CERTIFICATE"));
843 assert!(!issued.key_pem.is_empty());
844
845 let binding = extract_binding(&issued.cert_pem).unwrap();
846 assert_eq!(binding.aid, st.prefix.as_str());
847 assert_eq!(binding.current_keys, keys);
848
849 let san = extract_did_keri_san(&issued.cert_pem).unwrap();
850 assert_eq!(san, Some(format!("did:keri:{}", st.prefix.as_str())));
851 }
852
853 #[test]
854 fn aid_reads_out_of_the_san_without_the_kel() {
855 // The X.509-SVID identity surface: a verifier learns *which* AID a
856 // cert claims from the SAN alone, before it holds the KEL.
857 let (st, _) = multi_state();
858 let issued = issue_kel_rooted_cert(&st, &["localhost".to_string()]).unwrap();
859 let aid = extract_aid_from_san(&issued.cert_pem).unwrap();
860 assert_eq!(aid.as_str(), st.prefix.as_str());
861 }
862
863 #[test]
864 fn plain_cert_has_no_san_identity() {
865 // A stock self-signed leaf carries no did:keri SAN, so there is no
866 // auths identity to read out of it — NoSanIdentity, not a panic.
867 let kp = rcgen::KeyPair::generate().unwrap();
868 let params = rcgen::CertificateParams::new(vec!["localhost".to_string()]).unwrap();
869 let cert = params.self_signed(&kp).unwrap();
870 assert!(matches!(
871 extract_aid_from_san(&cert.pem()),
872 Err(TlsCertError::NoSanIdentity)
873 ));
874 }
875
876 #[test]
877 fn malformed_san_aid_is_rejected_at_the_boundary() {
878 // A did:keri SAN whose AID is not a valid KERI prefix is an error at
879 // the parse boundary, never returned as a trusted identity.
880 let kp = rcgen::KeyPair::generate().unwrap();
881 let mut params = rcgen::CertificateParams::new(Vec::new()).unwrap();
882 let bad = rcgen::string::Ia5String::try_from("did:keri:".to_string()).unwrap();
883 params.subject_alt_names.push(rcgen::SanType::URI(bad));
884 let cert = params.self_signed(&kp).unwrap();
885 assert!(matches!(
886 extract_aid_from_san(&cert.pem()),
887 Err(TlsCertError::InvalidSanAid(_))
888 ));
889 }
890
891 #[test]
892 fn issued_cert_verifies_against_the_same_key_state() {
893 let (st, _) = multi_state();
894 let issued = issue_kel_rooted_cert(&st, &["localhost".to_string()]).unwrap();
895 let binding = verify_binds_to_key_state(&issued.cert_pem, &st).unwrap();
896 assert_eq!(binding.aid, st.prefix.as_str());
897 }
898
899 #[test]
900 fn cert_is_rejected_against_a_different_key_state() {
901 let (st, _) = multi_state();
902 let issued = issue_kel_rooted_cert(&st, &["localhost".to_string()]).unwrap();
903
904 // A KEL replay that yields a different current key must not verify.
905 let other_key = ed25519_key(&[9u8; 32]);
906 let other = state(
907 st.prefix.as_str(),
908 &[&other_key],
909 st.last_event_said.as_str(),
910 );
911 assert!(matches!(
912 verify_binds_to_key_state(&issued.cert_pem, &other),
913 Err(TlsCertError::BindingMismatch(_))
914 ));
915 }
916
917 #[test]
918 fn cert_is_rejected_against_a_different_aid() {
919 let (st, _) = multi_state();
920 let issued = issue_kel_rooted_cert(&st, &["localhost".to_string()]).unwrap();
921 let k = ed25519_key(&[1u8; 32]);
922 let other = state(
923 "EAidOTHERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
924 &[&k],
925 "ETip",
926 );
927 assert!(matches!(
928 verify_binds_to_key_state(&issued.cert_pem, &other),
929 Err(TlsCertError::BindingMismatch(_))
930 ));
931 }
932
933 #[test]
934 fn plain_cert_has_no_binding() {
935 // A cert minted without the extension reports MissingBinding, not a
936 // false match — so a stock self-signed cert can't masquerade as
937 // KEL-rooted.
938 let kp = rcgen::KeyPair::generate().unwrap();
939 let params = rcgen::CertificateParams::new(vec!["localhost".to_string()]).unwrap();
940 let cert = params.self_signed(&kp).unwrap();
941 assert!(matches!(
942 extract_binding(&cert.pem()),
943 Err(TlsCertError::MissingBinding)
944 ));
945 }
946
947 #[test]
948 fn issue_with_supplied_key_is_deterministic_in_binding() {
949 // Same KEL + same TLS key → identical embedded binding (the cert
950 // serial/validity may differ, but the KEL projection is stable).
951 let (st, _) = multi_state();
952 let kp = rcgen::KeyPair::generate().unwrap();
953 let pem = kp.serialize_pem();
954 let a = issue_kel_rooted_cert_with_key(&st, &pem, &["localhost".to_string()]).unwrap();
955 let b = issue_kel_rooted_cert_with_key(&st, &pem, &["localhost".to_string()]).unwrap();
956 assert_eq!(
957 extract_binding(&a.cert_pem).unwrap(),
958 extract_binding(&b.cert_pem).unwrap()
959 );
960 }
961
962 // --- T3 adversarial verifier: the AID authorizes the TLS key ---
963
964 #[test]
965 fn authorized_cert_carries_authorization_and_verifies() {
966 // The happy path: an AID-authorized leaf carries the authorization and
967 // passes the adversarial verifier against its own replayed key-state.
968 let (st, auth) = authorized_state(&[7u8; 32]);
969 let issued =
970 issue_authorized_kel_rooted_cert(&st, &auth, &["localhost".to_string()]).unwrap();
971
972 let embedded = extract_binding(&issued.cert_pem).unwrap();
973 assert!(
974 embedded.tls_key_authorization.is_some(),
975 "an authorized leaf must embed the authorization"
976 );
977
978 let binding = verify_authorized_against_key_state(&issued.cert_pem, &st).unwrap();
979 assert_eq!(binding.aid, st.prefix.as_str());
980 }
981
982 #[test]
983 fn forged_binding_over_attackers_tls_key_is_rejected() {
984 // The core forgery: an attacker replays the victim's *public* KEL, so
985 // the binding's key-state matches a real replay — but mints the leaf
986 // over their own TLS key with no valid authorization. The adversarial
987 // verifier rejects it (the AID never signed this TLS key), even though
988 // the key-state binding alone would "match".
989 let (st, _auth) = authorized_state(&[7u8; 32]);
990
991 // Attacker forges a binding that names the correct key-state but signs
992 // the SPKI with a key they DO hold — which is not the AID's key.
993 let attacker = Ed25519Authorizer::from_seed(&[99u8; 32], 0);
994 let forged_kp = rcgen::KeyPair::generate().unwrap();
995 let spki = forged_kp.subject_public_key_info();
996 let forged_sig = attacker.sign_tls_key(&spki).unwrap();
997 // Build the cert by hand: correct key-state binding (matches replay),
998 // but the embedded authorization is the attacker's signature.
999 let binding = AuthsKeriBinding::from_key_state(&st)
1000 .unwrap()
1001 .with_authorization(TlsKeyAuthorization {
1002 key_index: 0,
1003 signature: forged_sig,
1004 });
1005 let issued =
1006 issue_with_keypair(&binding, &forged_kp, &["localhost".to_string()]).unwrap();
1007
1008 // The key-state binding "matches" the replay (forged from the public KEL)...
1009 assert!(verify_binds_to_key_state(&issued.cert_pem, &st).is_ok());
1010 // ...but the AID's current key did not sign this TLS SPKI → Unauthorized.
1011 assert!(matches!(
1012 verify_authorized_against_key_state(&issued.cert_pem, &st),
1013 Err(TlsCertError::Unauthorized(_))
1014 ));
1015 }
1016
1017 #[test]
1018 fn stripped_authorization_is_rejected() {
1019 // A leaf that chains to the key-state but carries NO authorization (the
1020 // discovery-only binding) is rejected by the adversarial verifier — a
1021 // KEL-rooted leaf must prove the AID authorized its TLS key.
1022 let (st, _auth) = authorized_state(&[7u8; 32]);
1023 let unauthorized = issue_kel_rooted_cert(&st, &["localhost".to_string()]).unwrap();
1024 assert!(matches!(
1025 verify_authorized_against_key_state(&unauthorized.cert_pem, &st),
1026 Err(TlsCertError::MissingAuthorization)
1027 ));
1028 }
1029
1030 #[test]
1031 fn stripped_binding_plain_cert_is_rejected() {
1032 // A plain self-signed leaf (no binding extension at all) is rejected —
1033 // MissingBinding, before any authorization check.
1034 let (st, _auth) = authorized_state(&[7u8; 32]);
1035 let kp = rcgen::KeyPair::generate().unwrap();
1036 let params = rcgen::CertificateParams::new(vec!["localhost".to_string()]).unwrap();
1037 let plain = params.self_signed(&kp).unwrap();
1038 assert!(matches!(
1039 verify_authorized_against_key_state(&plain.pem(), &st),
1040 Err(TlsCertError::MissingBinding)
1041 ));
1042 }
1043
1044 #[test]
1045 fn revoked_or_rotated_aid_is_rejected() {
1046 // A leaf authorized under the AID's *old* key-state is rejected once the
1047 // verifier replays the *current* KEL (rotated/revoked): the binding's
1048 // key-state no longer matches the replay → BindingMismatch, before the
1049 // authorization is even checked.
1050 let (old_state, auth) = authorized_state(&[7u8; 32]);
1051 let issued =
1052 issue_authorized_kel_rooted_cert(&old_state, &auth, &["localhost".to_string()])
1053 .unwrap();
1054
1055 // The current key-state after a rotation: a different current key.
1056 let rotated_key = ed25519_key(&[8u8; 32]);
1057 let current_state = state(
1058 old_state.prefix.as_str(),
1059 &[&rotated_key],
1060 "ETipRotatedAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
1061 );
1062 assert!(matches!(
1063 verify_authorized_against_key_state(&issued.cert_pem, ¤t_state),
1064 Err(TlsCertError::BindingMismatch(_))
1065 ));
1066 }
1067
1068 #[test]
1069 fn authorization_with_out_of_range_index_is_rejected() {
1070 // An authorization that names a current-key index the key-state does
1071 // not have is rejected, not silently skipped.
1072 let (st, auth) = authorized_state(&[7u8; 32]);
1073 let kp = rcgen::KeyPair::generate().unwrap();
1074 let spki = kp.subject_public_key_info();
1075 let sig = auth.sign_tls_key(&spki).unwrap();
1076 let binding = AuthsKeriBinding::from_key_state(&st)
1077 .unwrap()
1078 .with_authorization(TlsKeyAuthorization {
1079 key_index: 5, // out of range: single-key state
1080 signature: sig,
1081 });
1082 let issued = issue_with_keypair(&binding, &kp, &["localhost".to_string()]).unwrap();
1083 assert!(matches!(
1084 verify_authorized_against_key_state(&issued.cert_pem, &st),
1085 Err(TlsCertError::AuthorizationIndexOutOfRange { index: 5, len: 1 })
1086 ));
1087 }
1088
1089 #[test]
1090 fn issuer_rejects_authorizer_signing_with_wrong_key() {
1091 // Defense at issuance: an authorizer whose signing key does not match
1092 // the current key it claims is caught before the leaf is emitted, so a
1093 // miswired adapter can't mint a leaf that will only fail at the verifier.
1094 let (st, _auth) = authorized_state(&[7u8; 32]);
1095 // An authorizer at index 0 but holding a key that is NOT the state's key.
1096 let wrong = Ed25519Authorizer::from_seed(&[42u8; 32], 0);
1097 assert!(matches!(
1098 issue_authorized_kel_rooted_cert(&st, &wrong, &["localhost".to_string()]),
1099 Err(TlsCertError::Unauthorized(_))
1100 ));
1101 }
1102
1103 #[test]
1104 fn issuer_rejects_out_of_range_authorizer_index() {
1105 // An authorizer claiming a key index the state lacks is rejected at
1106 // issuance, before signing.
1107 let (st, _auth) = authorized_state(&[7u8; 32]);
1108 let bad_index = Ed25519Authorizer::from_seed(&[7u8; 32], 9);
1109 assert!(matches!(
1110 issue_authorized_kel_rooted_cert(&st, &bad_index, &["localhost".to_string()]),
1111 Err(TlsCertError::AuthorizationIndexOutOfRange { index: 9, len: 1 })
1112 ));
1113 }
1114
1115 #[test]
1116 fn authorization_round_trips_through_binding_json() {
1117 // The authorization survives canonical-JSON round-trip (the wire form
1118 // inside the cert extension), so a verifier reads back exactly what the
1119 // issuer embedded.
1120 let (st, auth) = authorized_state(&[7u8; 32]);
1121 let issued =
1122 issue_authorized_kel_rooted_cert(&st, &auth, &["localhost".to_string()]).unwrap();
1123 let embedded = extract_binding(&issued.cert_pem).unwrap();
1124 let json = embedded.to_canonical_json();
1125 let back = AuthsKeriBinding::from_canonical_json(&json).unwrap();
1126 assert_eq!(back, embedded);
1127 assert!(back.tls_key_authorization.is_some());
1128 }
1129 }
1130}