Skip to main content

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, &current_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}