Skip to main content

auth_framework/methods/client_cert/
mod.rs

1//! Client certificate authentication (application-layer mTLS identity extraction).
2//!
3//! # Where this fits in the stack
4//!
5//! ```text
6//! TLS layer   ─── mTLS handshake ──► proves client holds the private key
7//! This module ─── cert inspection ──► extracts identity, applies policy
8//! ```
9//!
10//! The TLS handshake cryptographically proves that the client possesses the
11//! private key corresponding to the certificate it presented.  Once TLS is
12//! established, the server-side application receives the **verified** certificate
13//! as DER bytes.  This module's job is the *second* part: inspecting the certificate
14//! to answer "who is this client?" and "are they permitted?".
15//!
16//! # What this module covers
17//!
18//! Any PKI-based client authentication that delivers an X.509 certificate to
19//! the application layer:
20//! - Software client certificates (p12/pfx, PEM bundles)
21//! - Smart card client certificates (SC/PKCS#11, hardware-bound private key)
22//! - US government PIV (NIST SP 800-73) certificates
23//! - CAC (Common Access Card) certificates
24//!
25//! From this module's perspective a smart card certificate is just an X.509 DER
26//! byte string.  The PC/SC protocol that extracts it from the physical card is
27//! handled at the transport layer (PKCS#11 middleware, OS TLS stack, etc.).
28//!
29//! # What this module does NOT do
30//!
31//! - **Cryptographic signature verification of the cert chain**: that belongs in the
32//!   TLS library (rustls, native-tls).  `ClientCertConfig::trusted_ca_ders` is a
33//!   *defence-in-depth* post-TLS policy check (issuer DN matching), not a replacement
34//!   for TLS-level chain verification.
35//! - **OCSP / CRL revocation checking**: a future extension.  Rely on the TLS layer's
36//!   revocation configuration for now.
37//! - **PC/SC card reader access**: use a PKCS#11 middleware library at the transport
38//!   layer (e.g. OpenSC).
39
40use std::collections::HashSet;
41use std::sync::Arc;
42use std::time::SystemTime;
43
44use crate::{
45    authentication::credentials::Credential,
46    errors::{AuthError, Result},
47};
48
49// ─── Configuration ────────────────────────────────────────────────────────────
50
51/// Configuration for [`ClientCertAuthMethod`].
52#[derive(Debug, Clone)]
53pub struct ClientCertConfig {
54    /// DER-encoded trusted CA certificates.
55    ///
56    /// When non-empty, the presented certificate's issuer DN is matched against
57    /// the subjects of these CAs.  This provides a meaningful policy guard when
58    /// the list is kept to a small, curated set of CAs you actually trust.
59    ///
60    /// **Security note**: this is an *issuer DN equality check*, not a full
61    /// cryptographic path validation.  For cryptographic assurance configure your
62    /// TLS library's trusted CA store, then use this list as a second policy filter.
63    pub trusted_ca_ders: Vec<Vec<u8>>,
64
65    /// Subject DN substrings that are allowed.  An empty list accepts any subject
66    /// (given other checks pass).  Matching is case-sensitive substring search on
67    /// the full Distinguished Name string (e.g. `"CN=alice"` or just `"alice"`).
68    pub subject_allowlist: Vec<String>,
69
70    /// Issuer DN substrings that are allowed.  An empty list accepts any issuer.
71    pub issuer_allowlist: Vec<String>,
72
73    /// When `true`, the certificate must contain a Subject Alternative Name (SAN)
74    /// extension.  PIV and modern TLS certificates always carry one; older
75    /// enterprise CAs sometimes do not.
76    pub require_san: bool,
77
78    /// Lifetime of the session issued after successful authentication (seconds).
79    pub token_lifetime_secs: u64,
80}
81
82impl Default for ClientCertConfig {
83    fn default() -> Self {
84        Self {
85            trusted_ca_ders: Vec::new(),
86            subject_allowlist: Vec::new(),
87            issuer_allowlist: Vec::new(),
88            require_san: false,
89            token_lifetime_secs: 3600,
90        }
91    }
92}
93
94impl ClientCertConfig {
95    /// Create a permissive configuration with a 1-hour session lifetime.
96    pub fn new() -> Self {
97        Self::default()
98    }
99
100    /// Builder: add a trusted CA (DER-encoded).
101    pub fn trust_ca(mut self, ca_der: Vec<u8>) -> Self {
102        self.trusted_ca_ders.push(ca_der);
103        self
104    }
105
106    /// Builder: allow only subjects whose DN contains `pattern`.
107    pub fn allow_subject(mut self, pattern: impl Into<String>) -> Self {
108        self.subject_allowlist.push(pattern.into());
109        self
110    }
111
112    /// Builder: allow only issuers whose DN contains `pattern`.
113    pub fn allow_issuer(mut self, pattern: impl Into<String>) -> Self {
114        self.issuer_allowlist.push(pattern.into());
115        self
116    }
117
118    /// Builder: require a SAN extension.
119    pub fn with_require_san(mut self) -> Self {
120        self.require_san = true;
121        self
122    }
123}
124
125// ─── Identity type ────────────────────────────────────────────────────────────
126
127/// Identity extracted from a successfully validated client certificate.
128#[derive(Debug, Clone)]
129pub struct CertIdentity {
130    /// Full Distinguished Name of the subject (e.g. `"CN=alice, O=Example Corp"`).
131    pub subject_dn: String,
132    /// Common Name (CN) extracted from the subject, if present.
133    pub common_name: Option<String>,
134    /// Subject Alternative Names collected from the SAN extension, if present.
135    /// Each entry is prefixed with its type: `"dns:host.example.com"`,
136    /// `"email:user@example.com"`, `"ip:192.0.2.1"`.
137    pub sans: Vec<String>,
138    /// Full Distinguished Name of the issuer.
139    pub issuer_dn: String,
140}
141
142// ─── Method ───────────────────────────────────────────────────────────────────
143
144/// Application-layer client certificate authenticator.
145///
146/// Validates an X.509 client certificate presented after an mTLS handshake and
147/// extracts a [`CertIdentity`] that higher-level code can use to create a session.
148///
149/// ## Minimal usage
150///
151/// ```rust,no_run
152/// use auth_framework::methods::client_cert::{ClientCertAuthMethod, ClientCertConfig};
153/// use auth_framework::authentication::credentials::Credential;
154///
155/// # let cert_der: Vec<u8> = unimplemented!();
156/// let method = ClientCertAuthMethod::new(ClientCertConfig::new());
157///
158/// // `cert_der` comes from your HTTP framework's peer certificate extraction.
159/// let identity = method.authenticate(&Credential::client_cert_from_tls(cert_der))?;
160/// # Ok::<_, auth_framework::errors::AuthError>(())
161/// ```
162pub struct ClientCertAuthMethod {
163    config: ClientCertConfig,
164}
165
166impl ClientCertAuthMethod {
167    /// Create a new authenticator with the given configuration.
168    pub fn new(config: ClientCertConfig) -> Self {
169        Self { config }
170    }
171
172    /// Validate `credential` and return the caller's certificate identity.
173    ///
174    /// Accepts `Credential::Certificate { certificate, .. }`.  The `private_key`
175    /// field is **ignored** — key possession was already proved by the TLS
176    /// handshake.  Use [`Credential::client_cert_from_tls`] to construct the
177    /// credential without supplying a private key.
178    pub fn authenticate(&self, credential: &Credential) -> Result<CertIdentity> {
179        let cert_der = match credential {
180            Credential::Certificate {
181                certificate,
182                private_key,
183                ..
184            } => {
185                if !private_key.is_empty() {
186                    tracing::warn!(
187                        "ClientCertAuthMethod received a non-empty private_key — \
188                         it will be ignored.  For mTLS flows use \
189                         `Credential::client_cert_from_tls(der_bytes)`."
190                    );
191                }
192                certificate.as_slice()
193            }
194            other => {
195                return Err(AuthError::InvalidCredential {
196                    credential_type: other.credential_type().to_string(),
197                    message: "ClientCertAuthMethod requires a Credential::Certificate. \
198                              Use Credential::client_cert_from_tls(der_bytes) for mTLS flows."
199                        .to_string(),
200                });
201            }
202        };
203
204        self.validate_der(cert_der)
205    }
206
207    // ── Validation pipeline ───────────────────────────────────────────────────
208
209    fn validate_der(&self, cert_der: &[u8]) -> Result<CertIdentity> {
210        use x509_parser::prelude::*;
211
212        if cert_der.is_empty() {
213            return Err(AuthError::InvalidCredential {
214                credential_type: "certificate".to_string(),
215                message: "Certificate DER bytes are empty".to_string(),
216            });
217        }
218
219        let (_, cert) =
220            X509Certificate::from_der(cert_der).map_err(|_| AuthError::InvalidCredential {
221                credential_type: "certificate".to_string(),
222                message: "Failed to parse X.509 DER certificate — verify that the bytes \
223                          are DER-encoded (not PEM) and are not truncated."
224                    .to_string(),
225            })?;
226
227        self.check_validity(&cert)?;
228        self.check_subject_allowlist(&cert)?;
229        self.check_issuer_allowlist(&cert)?;
230        self.check_san_required(&cert)?;
231        self.check_trust_chain(cert_der)?;
232        self.extract_identity(&cert)
233    }
234
235    fn check_validity(&self, cert: &x509_parser::certificate::X509Certificate<'_>) -> Result<()> {
236        let now = SystemTime::now()
237            .duration_since(SystemTime::UNIX_EPOCH)
238            .unwrap_or_default()
239            .as_secs() as i64;
240
241        let not_before = cert.validity().not_before.timestamp();
242        let not_after = cert.validity().not_after.timestamp();
243
244        if now < not_before {
245            return Err(AuthError::InvalidCredential {
246                credential_type: "certificate".to_string(),
247                message: format!(
248                    "Certificate is not yet valid (valid from Unix timestamp {})",
249                    not_before
250                ),
251            });
252        }
253        if now > not_after {
254            return Err(AuthError::InvalidCredential {
255                credential_type: "certificate".to_string(),
256                message: "Certificate has expired".to_string(),
257            });
258        }
259        Ok(())
260    }
261
262    fn check_subject_allowlist(
263        &self,
264        cert: &x509_parser::certificate::X509Certificate<'_>,
265    ) -> Result<()> {
266        if self.config.subject_allowlist.is_empty() {
267            return Ok(());
268        }
269        let subject = cert.subject().to_string();
270        if !self
271            .config
272            .subject_allowlist
273            .iter()
274            .any(|p| subject.contains(p.as_str()))
275        {
276            return Err(AuthError::InvalidCredential {
277                credential_type: "certificate".to_string(),
278                message: format!("Subject DN '{}' is not in the subject allowlist", subject),
279            });
280        }
281        Ok(())
282    }
283
284    fn check_issuer_allowlist(
285        &self,
286        cert: &x509_parser::certificate::X509Certificate<'_>,
287    ) -> Result<()> {
288        if self.config.issuer_allowlist.is_empty() {
289            return Ok(());
290        }
291        let issuer = cert.issuer().to_string();
292        if !self
293            .config
294            .issuer_allowlist
295            .iter()
296            .any(|p| issuer.contains(p.as_str()))
297        {
298            return Err(AuthError::InvalidCredential {
299                credential_type: "certificate".to_string(),
300                message: format!("Issuer DN '{}' is not in the issuer allowlist", issuer),
301            });
302        }
303        Ok(())
304    }
305
306    fn check_san_required(
307        &self,
308        cert: &x509_parser::certificate::X509Certificate<'_>,
309    ) -> Result<()> {
310        if !self.config.require_san {
311            return Ok(());
312        }
313        // SAN extension OID: 2.5.29.17
314        let has_san = cert
315            .extensions()
316            .iter()
317            .any(|ext| ext.oid.to_id_string() == "2.5.29.17");
318        if !has_san {
319            return Err(AuthError::InvalidCredential {
320                credential_type: "certificate".to_string(),
321                message: "Certificate does not contain a Subject Alternative Name (SAN) \
322                          extension, but require_san is enabled in the configuration."
323                    .to_string(),
324            });
325        }
326        Ok(())
327    }
328
329    /// Issuer DN matching against trusted CAs.
330    ///
331    /// Finds a CA in `trusted_ca_ders` whose subject DN equals the end-entity
332    /// certificate's issuer DN.  This is effective as a policy filter when the CA
333    /// list is carefully maintained; it is NOT a cryptographic signature check.
334    fn check_trust_chain(&self, cert_der: &[u8]) -> Result<()> {
335        if self.config.trusted_ca_ders.is_empty() {
336            return Ok(());
337        }
338
339        use x509_parser::prelude::*;
340
341        let (_, cert) =
342            X509Certificate::from_der(cert_der).map_err(|_| AuthError::InvalidCredential {
343                credential_type: "certificate".to_string(),
344                message: "Failed to re-parse certificate for chain check".to_string(),
345            })?;
346        let issuer_dn = cert.issuer().to_string();
347
348        let found = self.config.trusted_ca_ders.iter().any(|ca_der| {
349            if let Ok((_, ca_cert)) = X509Certificate::from_der(ca_der) {
350                ca_cert.subject().to_string() == issuer_dn
351            } else {
352                false
353            }
354        });
355
356        if !found {
357            return Err(AuthError::InvalidCredential {
358                credential_type: "certificate".to_string(),
359                message: format!(
360                    "No trusted CA found for issuer '{}'. \
361                     Add the issuing CA's DER bytes to ClientCertConfig::trusted_ca_ders.",
362                    issuer_dn
363                ),
364            });
365        }
366        Ok(())
367    }
368
369    fn extract_identity(
370        &self,
371        cert: &x509_parser::certificate::X509Certificate<'_>,
372    ) -> Result<CertIdentity> {
373        let subject_dn = cert.subject().to_string();
374        let issuer_dn = cert.issuer().to_string();
375
376        // First CN attribute in the subject RDN sequence.
377        let common_name = cert
378            .subject()
379            .iter_common_name()
380            .next()
381            .and_then(|attr| attr.as_str().ok())
382            .map(str::to_string);
383
384        // Collect DNS, email, and IP SANs from the SAN extension (OID 2.5.29.17).
385        let mut sans: Vec<String> = Vec::new();
386        for ext in cert.extensions() {
387            if ext.oid.to_id_string() == "2.5.29.17"
388                && let x509_parser::extensions::ParsedExtension::SubjectAlternativeName(san) =
389                    ext.parsed_extension()
390            {
391                for gn in &san.general_names {
392                    let entry = match gn {
393                        x509_parser::extensions::GeneralName::DNSName(s) => {
394                            format!("dns:{s}")
395                        }
396                        x509_parser::extensions::GeneralName::RFC822Name(s) => {
397                            format!("email:{s}")
398                        }
399                        x509_parser::extensions::GeneralName::IPAddress(ip) => {
400                            format!("ip:{}", fmt_ip(ip))
401                        }
402                        _ => continue,
403                    };
404                    sans.push(entry);
405                }
406            }
407        }
408
409        Ok(CertIdentity {
410            subject_dn,
411            common_name,
412            sans,
413            issuer_dn,
414        })
415    }
416}
417
418/// Format raw IP bytes as dotted-decimal (IPv4) or colon-hex (IPv6).
419fn fmt_ip(bytes: &[u8]) -> String {
420    match bytes.len() {
421        4 => format!("{}.{}.{}.{}", bytes[0], bytes[1], bytes[2], bytes[3]),
422        16 => {
423            let parts: Vec<String> = bytes
424                .chunks(2)
425                .map(|c| format!("{:02x}{:02x}", c[0], c[1]))
426                .collect();
427            parts.join(":")
428        }
429        _ => format!("{:?}", bytes),
430    }
431}
432
433// ── Certificate Pinning ───────────────────────────────────────────────────────
434
435/// Certificate fingerprint for pinning.
436///
437/// Stores the SHA-256 hash of a DER-encoded certificate for pin comparison.
438#[derive(Debug, Clone, PartialEq, Eq, Hash)]
439pub struct CertPin {
440    /// SHA-256 fingerprint as a lowercase hex string.
441    pub sha256_hex: String,
442}
443
444impl CertPin {
445    /// Compute a pin from DER-encoded certificate bytes.
446    pub fn from_der(cert_der: &[u8]) -> Self {
447        use sha2::{Digest, Sha256};
448        let digest = Sha256::digest(cert_der);
449        Self {
450            sha256_hex: hex::encode(digest),
451        }
452    }
453
454    /// Create a pin from a known hex fingerprint.
455    pub fn from_hex(hex_fingerprint: impl Into<String>) -> Self {
456        Self {
457            sha256_hex: hex_fingerprint.into().to_lowercase(),
458        }
459    }
460}
461
462/// A certificate pin store for enforcing certificate pinning.
463///
464/// When pinning is enabled, only certificates whose SHA-256 fingerprint
465/// appears in this store are accepted.
466#[derive(Debug, Clone, Default)]
467pub struct CertPinStore {
468    pins: Arc<std::sync::RwLock<HashSet<String>>>,
469}
470
471impl CertPinStore {
472    /// Create a new empty pin store.
473    pub fn new() -> Self {
474        Self::default()
475    }
476
477    /// Add a pin by SHA-256 hex fingerprint.
478    pub fn add(&self, pin: &CertPin) {
479        self.pins.write().unwrap().insert(pin.sha256_hex.clone());
480    }
481
482    /// Remove a pin.
483    pub fn remove(&self, pin: &CertPin) -> bool {
484        self.pins.write().unwrap().remove(&pin.sha256_hex)
485    }
486
487    /// Check if a certificate (DER) matches any pinned fingerprint.
488    pub fn is_pinned(&self, cert_der: &[u8]) -> bool {
489        let pin = CertPin::from_der(cert_der);
490        self.pins.read().unwrap().contains(&pin.sha256_hex)
491    }
492
493    /// Number of stored pins.
494    pub fn count(&self) -> usize {
495        self.pins.read().unwrap().len()
496    }
497}
498
499// ── Revocation Checking ───────────────────────────────────────────────────────
500
501/// Revocation check result.
502#[derive(Debug, Clone, PartialEq, Eq)]
503pub enum RevocationStatus {
504    /// Certificate is known to be good (not revoked).
505    Good,
506    /// Certificate has been revoked.
507    Revoked {
508        /// RFC 5280 CRLReason (optional).
509        reason: Option<String>,
510    },
511    /// Revocation status is unknown (e.g., responder unavailable).
512    Unknown,
513}
514
515/// An in-memory CRL (Certificate Revocation List) store.
516///
517/// For production deployments, the TLS layer should handle CRL/OCSP. This
518/// provides an application-layer defence-in-depth check against known-revoked
519/// serial numbers.
520#[derive(Debug, Clone, Default)]
521pub struct CrlStore {
522    /// Revoked certificate serial numbers (hex-encoded), keyed by issuer DN.
523    revoked: Arc<std::sync::RwLock<std::collections::HashMap<String, HashSet<String>>>>,
524}
525
526impl CrlStore {
527    /// Create a new empty CRL store.
528    pub fn new() -> Self {
529        Self::default()
530    }
531
532    /// Mark a certificate serial number as revoked for a given issuer DN.
533    pub fn add_revoked(&self, issuer_dn: &str, serial_hex: &str) {
534        self.revoked
535            .write()
536            .unwrap()
537            .entry(issuer_dn.to_string())
538            .or_default()
539            .insert(serial_hex.to_lowercase());
540    }
541
542    /// Check if a certificate (by issuer DN and serial hex) is revoked.
543    pub fn check(&self, issuer_dn: &str, serial_hex: &str) -> RevocationStatus {
544        let store = self.revoked.read().unwrap();
545        if let Some(serials) = store.get(issuer_dn) {
546            if serials.contains(&serial_hex.to_lowercase()) {
547                return RevocationStatus::Revoked { reason: None };
548            }
549        }
550        RevocationStatus::Good
551    }
552
553    /// Check a DER-encoded certificate against the CRL store.
554    pub fn check_der(&self, cert_der: &[u8]) -> RevocationStatus {
555        use x509_parser::prelude::*;
556        let Ok((_, cert)) = X509Certificate::from_der(cert_der) else {
557            return RevocationStatus::Unknown;
558        };
559        let issuer = cert.issuer().to_string();
560        let serial = cert.raw_serial_as_string().to_lowercase();
561        self.check(&issuer, &serial)
562    }
563
564    /// Total count of revoked serial numbers across all issuers.
565    pub fn revoked_count(&self) -> usize {
566        self.revoked
567            .read()
568            .unwrap()
569            .values()
570            .map(|s| s.len())
571            .sum()
572    }
573
574    /// Remove all entries for an issuer.
575    pub fn clear_issuer(&self, issuer_dn: &str) {
576        self.revoked.write().unwrap().remove(issuer_dn);
577    }
578}
579
580// ── Certificate-Bound Access Tokens (RFC 8705) ───────────────────────────────
581
582/// Computes a certificate thumbprint for use in RFC 8705 certificate-bound
583/// access tokens (mTLS client certificate binding).
584///
585/// Returns the base64url-encoded SHA-256 hash of the DER-encoded certificate,
586/// suitable for the `x5t#S256` confirmation claim in JWT access tokens.
587pub fn cert_thumbprint_s256(cert_der: &[u8]) -> String {
588    use base64::Engine;
589    use sha2::{Digest, Sha256};
590    let digest = Sha256::digest(cert_der);
591    base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(digest)
592}
593
594/// Verify that a presented certificate matches the `x5t#S256` thumbprint
595/// bound to an access token (RFC 8705 §3).
596pub fn verify_cert_binding(cert_der: &[u8], expected_thumbprint: &str) -> Result<()> {
597    let actual = cert_thumbprint_s256(cert_der);
598    if actual == expected_thumbprint {
599        Ok(())
600    } else {
601        Err(AuthError::InvalidCredential {
602            credential_type: "certificate".to_string(),
603            message: format!(
604                "Certificate thumbprint mismatch: token bound to '{}', presented cert has '{}'",
605                expected_thumbprint, actual
606            ),
607        })
608    }
609}
610
611// ─── Tests ────────────────────────────────────────────────────────────────────
612
613#[cfg(test)]
614mod tests {
615    use super::*;
616
617    // ── Configuration ─────────────────────────────────────────────────────────
618
619    #[test]
620    fn test_config_default() {
621        let cfg = ClientCertConfig::default();
622        assert!(cfg.trusted_ca_ders.is_empty());
623        assert!(cfg.subject_allowlist.is_empty());
624        assert!(cfg.issuer_allowlist.is_empty());
625        assert!(!cfg.require_san);
626        assert_eq!(cfg.token_lifetime_secs, 3600);
627    }
628
629    #[test]
630    fn test_config_builder_chain() {
631        let cfg = ClientCertConfig::new()
632            .allow_subject("alice")
633            .allow_issuer("MyCA")
634            .with_require_san();
635        assert_eq!(cfg.subject_allowlist, ["alice"]);
636        assert_eq!(cfg.issuer_allowlist, ["MyCA"]);
637        assert!(cfg.require_san);
638    }
639
640    // ── Error paths (no real cert required) ───────────────────────────────────
641
642    #[test]
643    fn test_wrong_credential_type_rejected() {
644        let method = ClientCertAuthMethod::new(ClientCertConfig::new());
645        let cred = Credential::Password {
646            username: "u".into(),
647            password: "p".into(),
648        };
649        let err = method.authenticate(&cred).unwrap_err();
650        assert!(
651            format!("{err}").contains("Certificate"),
652            "unexpected: {err}"
653        );
654    }
655
656    #[test]
657    fn test_empty_der_rejected() {
658        let method = ClientCertAuthMethod::new(ClientCertConfig::new());
659        let cred = Credential::Certificate {
660            certificate: vec![],
661            private_key: vec![],
662            passphrase: None,
663        };
664        let err = method.authenticate(&cred).unwrap_err();
665        assert!(format!("{err}").contains("empty"), "unexpected: {err}");
666    }
667
668    #[test]
669    fn test_garbage_der_rejected() {
670        let method = ClientCertAuthMethod::new(ClientCertConfig::new());
671        let cred = Credential::Certificate {
672            certificate: vec![0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04],
673            private_key: vec![],
674            passphrase: None,
675        };
676        assert!(method.authenticate(&cred).is_err());
677    }
678
679    // ── Certificate DER builder for tests ─────────────────────────────────────
680    //
681    // Constructs a minimal self-signed Ed25519 X.509v3 certificate in DER.
682    // Validity dates are hardcoded as UTCTime raw bytes (ISO 8601 compact form).
683    //
684    // To regenerate equivalent certs with openssl:
685    //   openssl req -x509 -newkey ed25519 -keyout /tmp/k.pem -out /tmp/c.pem \
686    //     -days 730 -nodes -subj "/CN=<cn>"
687    //   openssl x509 -in /tmp/c.pem -outform DER | xxd -i
688    fn build_cert_der(
689        cn: &str,
690        not_before_utc: &[u8; 13], // raw UTCTime bytes e.g. b"250101000000Z"
691        not_after_utc: &[u8; 13],
692    ) -> Vec<u8> {
693        use ring::rand::SystemRandom;
694        use ring::signature::{Ed25519KeyPair, KeyPair};
695
696        let rng = SystemRandom::new();
697        let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
698        let kp = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
699        let pub_key = kp.public_key().as_ref(); // 32 bytes
700
701        // Short-form TLV (content < 128 bytes)
702        let tlv = |tag: u8, content: &[u8]| -> Vec<u8> {
703            assert!(
704                content.len() < 128,
705                "content too large for short-form TLV: {} bytes",
706                content.len()
707            );
708            let mut v = vec![tag, content.len() as u8];
709            v.extend_from_slice(content);
710            v
711        };
712
713        // TLV with definite long form when content >= 128 bytes
714        let long_tlv = |tag: u8, content: &[u8]| -> Vec<u8> {
715            let len = content.len();
716            let mut v = vec![tag];
717            if len < 128 {
718                v.push(len as u8);
719            } else {
720                // Two-byte length (content < 65536)
721                v.push(0x81);
722                v.push(len as u8);
723            }
724            v.extend_from_slice(content);
725            v
726        };
727
728        // OID 1.3.101.112 (id-EdDSA / Ed25519)
729        let alg_id = tlv(0x30, &[0x06, 0x03, 0x2B, 0x65, 0x70]);
730
731        // Name: SEQUENCE { SET { SEQUENCE { OID 2.5.4.3, UTF8String cn } } }
732        let cn_bytes = cn.as_bytes();
733        let utf8_cn = tlv(0x0C, cn_bytes);
734        let oid_cn = [0x06u8, 0x03, 0x55, 0x04, 0x03];
735        let seq_atv = [oid_cn.as_slice(), utf8_cn.as_slice()].concat();
736        let name = tlv(0x30, &tlv(0x31, &tlv(0x30, &seq_atv)));
737
738        // Validity: SEQUENCE { UTCTime nb, UTCTime na }
739        let nb_der = tlv(0x17, not_before_utc);
740        let na_der = tlv(0x17, not_after_utc);
741        let validity = tlv(0x30, &[nb_der.as_slice(), na_der.as_slice()].concat());
742
743        // SubjectPublicKeyInfo: SEQUENCE { alg_id, BIT STRING { 0x00 || pub_key } }
744        let mut bit_content = vec![0x00u8];
745        bit_content.extend_from_slice(pub_key);
746        let bit_str = tlv(0x03, &bit_content);
747        let spki = tlv(0x30, &[alg_id.as_slice(), bit_str.as_slice()].concat());
748
749        // TBSCertificate
750        let version = [0xA0u8, 0x03, 0x02, 0x01, 0x02]; // [0] EXPLICIT INTEGER 2 (v3)
751        let serial = tlv(0x02, &[0x01]);
752        let tbs_body: Vec<u8> = [
753            version.as_slice(),
754            serial.as_slice(),
755            alg_id.as_slice(), // signatureAlgorithm
756            name.as_slice(),   // issuer
757            validity.as_slice(),
758            name.as_slice(), // subject (same as issuer: self-signed)
759            spki.as_slice(),
760        ]
761        .concat();
762        let tbs = long_tlv(0x30, &tbs_body);
763
764        // Self-sign the TBSCertificate bytes
765        let sig = kp.sign(&tbs);
766        let mut sig_content = vec![0x00u8]; // BIT STRING: 0 unused bits
767        sig_content.extend_from_slice(sig.as_ref()); // 64 bytes
768        let sig_bit_str = tlv(0x03, &sig_content);
769
770        // Certificate: SEQUENCE { tbs, signatureAlgorithm, signatureValue }
771        let cert_body: Vec<u8> =
772            [tbs.as_slice(), alg_id.as_slice(), sig_bit_str.as_slice()].concat();
773        long_tlv(0x30, &cert_body)
774    }
775
776    // Validity windows (UTCTime raw bytes):
777    //   Valid now (2025-01-01 → 2027-01-01)
778    fn valid_cert(cn: &str) -> Vec<u8> {
779        build_cert_der(cn, b"250101000000Z", b"270101000000Z")
780    }
781    //   Expired (2020-01-01 → 2021-01-01)
782    fn expired_cert(cn: &str) -> Vec<u8> {
783        build_cert_der(cn, b"200101000000Z", b"210101000000Z")
784    }
785    //   Future (2028-01-01 → 2030-01-01)
786    fn future_cert(cn: &str) -> Vec<u8> {
787        build_cert_der(cn, b"280101000000Z", b"300101000000Z")
788    }
789
790    fn cert_cred(der: Vec<u8>) -> Credential {
791        Credential::Certificate {
792            certificate: der,
793            private_key: vec![],
794            passphrase: None,
795        }
796    }
797
798    // ── Certificate-based tests ────────────────────────────────────────────────
799
800    #[test]
801    fn test_valid_cert_accepted() {
802        let method = ClientCertAuthMethod::new(ClientCertConfig::new());
803        let id = method
804            .authenticate(&cert_cred(valid_cert("alice")))
805            .expect("valid cert should be accepted");
806        assert!(
807            id.subject_dn.contains("alice"),
808            "subject should contain CN: {}",
809            id.subject_dn
810        );
811        assert_eq!(id.common_name.as_deref(), Some("alice"));
812    }
813
814    #[test]
815    fn test_expired_cert_rejected() {
816        let method = ClientCertAuthMethod::new(ClientCertConfig::new());
817        let err = method
818            .authenticate(&cert_cred(expired_cert("bob")))
819            .unwrap_err();
820        let msg = format!("{err}");
821        assert!(msg.contains("expired"), "expected 'expired' in: {msg}");
822    }
823
824    #[test]
825    fn test_future_cert_rejected() {
826        let method = ClientCertAuthMethod::new(ClientCertConfig::new());
827        let err = method
828            .authenticate(&cert_cred(future_cert("carol")))
829            .unwrap_err();
830        let msg = format!("{err}");
831        assert!(msg.contains("valid"), "expected 'valid' in: {msg}");
832    }
833
834    #[test]
835    fn test_subject_allowlist_permits_matching_cn() {
836        let cfg = ClientCertConfig::new().allow_subject("alice");
837        assert!(
838            ClientCertAuthMethod::new(cfg)
839                .authenticate(&cert_cred(valid_cert("alice")))
840                .is_ok()
841        );
842    }
843
844    #[test]
845    fn test_subject_allowlist_blocks_non_matching_cn() {
846        let cfg = ClientCertConfig::new().allow_subject("alice");
847        let err = ClientCertAuthMethod::new(cfg)
848            .authenticate(&cert_cred(valid_cert("mallory")))
849            .unwrap_err();
850        let msg = format!("{err}");
851        assert!(msg.contains("allowlist"), "expected 'allowlist' in: {msg}");
852    }
853
854    #[test]
855    fn test_issuer_allowlist_permits_self_signed_when_matches() {
856        // Self-signed: issuer DN == subject DN == "CN=alice"
857        let cfg = ClientCertConfig::new().allow_issuer("alice");
858        assert!(
859            ClientCertAuthMethod::new(cfg)
860                .authenticate(&cert_cred(valid_cert("alice")))
861                .is_ok()
862        );
863    }
864
865    #[test]
866    fn test_issuer_allowlist_blocks_unmatched_issuer() {
867        let cfg = ClientCertConfig::new().allow_issuer("TrustedCorp");
868        let err = ClientCertAuthMethod::new(cfg)
869            .authenticate(&cert_cred(valid_cert("alice")))
870            .unwrap_err();
871        let msg = format!("{err}");
872        assert!(msg.contains("allowlist"), "expected 'allowlist' in: {msg}");
873    }
874
875    #[test]
876    fn test_require_san_rejects_cert_without_san() {
877        // Our test cert builder produces no SAN extension.
878        let cfg = ClientCertConfig::new().with_require_san();
879        let err = ClientCertAuthMethod::new(cfg)
880            .authenticate(&cert_cred(valid_cert("alice")))
881            .unwrap_err();
882        let msg = format!("{err}");
883        assert!(
884            msg.contains("Subject Alternative Name") || msg.contains("SAN"),
885            "expected SAN mention in: {msg}"
886        );
887    }
888
889    #[test]
890    fn test_trusted_ca_accepts_when_issuer_dn_matches() {
891        // A self-signed cert is its own issuer; adding it to trusted_ca_ders
892        // means its own subject DN matches the issuer DN look-up.
893        let der = valid_cert("alice");
894        let cfg = ClientCertConfig::new().trust_ca(der.clone());
895        assert!(
896            ClientCertAuthMethod::new(cfg)
897                .authenticate(&cert_cred(der))
898                .is_ok()
899        );
900    }
901
902    #[test]
903    fn test_trusted_ca_rejects_when_no_ca_matches() {
904        let untrusted_cert = valid_cert("alice");
905        // Different self-signed cert = different subject DN
906        let different_ca = valid_cert("OtherCA");
907        let cfg = ClientCertConfig::new().trust_ca(different_ca);
908        let err = ClientCertAuthMethod::new(cfg)
909            .authenticate(&cert_cred(untrusted_cert))
910            .unwrap_err();
911        let msg = format!("{err}");
912        assert!(
913            msg.contains("trusted CA") || msg.contains("issuer"),
914            "expected CA/issuer mention in: {msg}"
915        );
916    }
917
918    #[test]
919    fn test_client_cert_from_tls_constructor() {
920        let der = valid_cert("sys");
921        let cred = Credential::client_cert_from_tls(der.clone());
922        match &cred {
923            Credential::Certificate {
924                certificate,
925                private_key,
926                passphrase,
927            } => {
928                assert_eq!(certificate, &der);
929                assert!(private_key.is_empty(), "private_key should be empty");
930                assert!(passphrase.is_none());
931            }
932            _ => panic!("Expected Credential::Certificate"),
933        }
934
935        // Also verify it authenticates successfully
936        let method = ClientCertAuthMethod::new(ClientCertConfig::new());
937        assert!(method.authenticate(&cred).is_ok());
938    }
939
940    #[test]
941    fn test_issuer_dn_populated_in_identity() {
942        let method = ClientCertAuthMethod::new(ClientCertConfig::new());
943        let id = method
944            .authenticate(&cert_cred(valid_cert("charlie")))
945            .unwrap();
946        // Self-signed: issuer == subject
947        assert_eq!(id.issuer_dn, id.subject_dn);
948    }
949
950    // ── Certificate Pinning ─────────────────────────────────────
951
952    #[test]
953    fn test_cert_pin_from_der() {
954        let der = valid_cert("pin-test");
955        let pin = CertPin::from_der(&der);
956        assert_eq!(pin.sha256_hex.len(), 64); // SHA-256 hex = 64 chars
957    }
958
959    #[test]
960    fn test_cert_pin_deterministic() {
961        let der = vec![0x30, 0x82, 0x01, 0x00];
962        let p1 = CertPin::from_der(&der);
963        let p2 = CertPin::from_der(&der);
964        assert_eq!(p1, p2);
965    }
966
967    #[test]
968    fn test_cert_pin_from_hex() {
969        let pin = CertPin::from_hex("AABB");
970        assert_eq!(pin.sha256_hex, "aabb"); // lowercase
971    }
972
973    #[test]
974    fn test_cert_pin_store_add_and_check() {
975        let store = CertPinStore::new();
976        let der = valid_cert("pinned");
977        let pin = CertPin::from_der(&der);
978        store.add(&pin);
979        assert_eq!(store.count(), 1);
980        assert!(store.is_pinned(&der));
981        assert!(!store.is_pinned(&valid_cert("not-pinned")));
982    }
983
984    #[test]
985    fn test_cert_pin_store_remove() {
986        let store = CertPinStore::new();
987        let der = valid_cert("removable");
988        let pin = CertPin::from_der(&der);
989        store.add(&pin);
990        assert!(store.remove(&pin));
991        assert!(!store.is_pinned(&der));
992        assert_eq!(store.count(), 0);
993    }
994
995    // ── CRL Store ───────────────────────────────────────────────
996
997    #[test]
998    fn test_crl_store_add_and_check() {
999        let store = CrlStore::new();
1000        store.add_revoked("CN=TestCA", "0a1b2c");
1001        assert_eq!(
1002            store.check("CN=TestCA", "0a1b2c"),
1003            RevocationStatus::Revoked { reason: None }
1004        );
1005        assert_eq!(store.check("CN=TestCA", "ffffff"), RevocationStatus::Good);
1006        assert_eq!(store.check("CN=OtherCA", "0a1b2c"), RevocationStatus::Good);
1007    }
1008
1009    #[test]
1010    fn test_crl_store_case_insensitive_serial() {
1011        let store = CrlStore::new();
1012        store.add_revoked("CN=CA", "aAbBcC");
1013        assert_eq!(
1014            store.check("CN=CA", "AABBCC"),
1015            RevocationStatus::Revoked { reason: None }
1016        );
1017    }
1018
1019    #[test]
1020    fn test_crl_store_check_der() {
1021        let store = CrlStore::new();
1022        let der = valid_cert("crl-test");
1023        // Without any revocations → Good
1024        assert_eq!(store.check_der(&der), RevocationStatus::Good);
1025    }
1026
1027    #[test]
1028    fn test_crl_store_revoked_count() {
1029        let store = CrlStore::new();
1030        store.add_revoked("CN=CA1", "01");
1031        store.add_revoked("CN=CA1", "02");
1032        store.add_revoked("CN=CA2", "01");
1033        assert_eq!(store.revoked_count(), 3);
1034    }
1035
1036    #[test]
1037    fn test_crl_store_clear_issuer() {
1038        let store = CrlStore::new();
1039        store.add_revoked("CN=CA", "01");
1040        store.add_revoked("CN=CA", "02");
1041        store.clear_issuer("CN=CA");
1042        assert_eq!(store.revoked_count(), 0);
1043    }
1044
1045    // ── RFC 8705 Certificate-Bound Tokens ───────────────────────
1046
1047    #[test]
1048    fn test_cert_thumbprint_s256() {
1049        let der = valid_cert("rfc8705");
1050        let thumbprint = cert_thumbprint_s256(&der);
1051        // base64url-encoded SHA-256 = 43 chars
1052        assert_eq!(thumbprint.len(), 43);
1053    }
1054
1055    #[test]
1056    fn test_cert_thumbprint_deterministic() {
1057        let der = vec![0x30, 0x82, 0x00, 0x01];
1058        let t1 = cert_thumbprint_s256(&der);
1059        let t2 = cert_thumbprint_s256(&der);
1060        assert_eq!(t1, t2);
1061    }
1062
1063    #[test]
1064    fn test_verify_cert_binding_success() {
1065        let der = valid_cert("bound");
1066        let thumbprint = cert_thumbprint_s256(&der);
1067        assert!(verify_cert_binding(&der, &thumbprint).is_ok());
1068    }
1069
1070    #[test]
1071    fn test_verify_cert_binding_mismatch() {
1072        let der = valid_cert("bound");
1073        let err = verify_cert_binding(&der, "wrong-thumbprint").unwrap_err();
1074        let msg = format!("{err}");
1075        assert!(msg.contains("mismatch"), "expected 'mismatch' in: {msg}");
1076    }
1077}