1use 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#[derive(Debug, Clone)]
53pub struct ClientCertConfig {
54 pub trusted_ca_ders: Vec<Vec<u8>>,
64
65 pub subject_allowlist: Vec<String>,
69
70 pub issuer_allowlist: Vec<String>,
72
73 pub require_san: bool,
77
78 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 pub fn new() -> Self {
97 Self::default()
98 }
99
100 pub fn trust_ca(mut self, ca_der: Vec<u8>) -> Self {
102 self.trusted_ca_ders.push(ca_der);
103 self
104 }
105
106 pub fn allow_subject(mut self, pattern: impl Into<String>) -> Self {
108 self.subject_allowlist.push(pattern.into());
109 self
110 }
111
112 pub fn allow_issuer(mut self, pattern: impl Into<String>) -> Self {
114 self.issuer_allowlist.push(pattern.into());
115 self
116 }
117
118 pub fn with_require_san(mut self) -> Self {
120 self.require_san = true;
121 self
122 }
123}
124
125#[derive(Debug, Clone)]
129pub struct CertIdentity {
130 pub subject_dn: String,
132 pub common_name: Option<String>,
134 pub sans: Vec<String>,
138 pub issuer_dn: String,
140}
141
142pub struct ClientCertAuthMethod {
163 config: ClientCertConfig,
164}
165
166impl ClientCertAuthMethod {
167 pub fn new(config: ClientCertConfig) -> Self {
169 Self { config }
170 }
171
172 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 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 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 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 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 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
418fn 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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
439pub struct CertPin {
440 pub sha256_hex: String,
442}
443
444impl CertPin {
445 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 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#[derive(Debug, Clone, Default)]
467pub struct CertPinStore {
468 pins: Arc<std::sync::RwLock<HashSet<String>>>,
469}
470
471impl CertPinStore {
472 pub fn new() -> Self {
474 Self::default()
475 }
476
477 pub fn add(&self, pin: &CertPin) {
479 self.pins.write().unwrap().insert(pin.sha256_hex.clone());
480 }
481
482 pub fn remove(&self, pin: &CertPin) -> bool {
484 self.pins.write().unwrap().remove(&pin.sha256_hex)
485 }
486
487 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 pub fn count(&self) -> usize {
495 self.pins.read().unwrap().len()
496 }
497}
498
499#[derive(Debug, Clone, PartialEq, Eq)]
503pub enum RevocationStatus {
504 Good,
506 Revoked {
508 reason: Option<String>,
510 },
511 Unknown,
513}
514
515#[derive(Debug, Clone, Default)]
521pub struct CrlStore {
522 revoked: Arc<std::sync::RwLock<std::collections::HashMap<String, HashSet<String>>>>,
524}
525
526impl CrlStore {
527 pub fn new() -> Self {
529 Self::default()
530 }
531
532 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 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 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 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 pub fn clear_issuer(&self, issuer_dn: &str) {
576 self.revoked.write().unwrap().remove(issuer_dn);
577 }
578}
579
580pub 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
594pub 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#[cfg(test)]
614mod tests {
615 use super::*;
616
617 #[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 #[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 fn build_cert_der(
689 cn: &str,
690 not_before_utc: &[u8; 13], 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(); 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 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 v.push(0x81);
722 v.push(len as u8);
723 }
724 v.extend_from_slice(content);
725 v
726 };
727
728 let alg_id = tlv(0x30, &[0x06, 0x03, 0x2B, 0x65, 0x70]);
730
731 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 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 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 let version = [0xA0u8, 0x03, 0x02, 0x01, 0x02]; let serial = tlv(0x02, &[0x01]);
752 let tbs_body: Vec<u8> = [
753 version.as_slice(),
754 serial.as_slice(),
755 alg_id.as_slice(), name.as_slice(), validity.as_slice(),
758 name.as_slice(), spki.as_slice(),
760 ]
761 .concat();
762 let tbs = long_tlv(0x30, &tbs_body);
763
764 let sig = kp.sign(&tbs);
766 let mut sig_content = vec![0x00u8]; sig_content.extend_from_slice(sig.as_ref()); let sig_bit_str = tlv(0x03, &sig_content);
769
770 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 fn valid_cert(cn: &str) -> Vec<u8> {
779 build_cert_der(cn, b"250101000000Z", b"270101000000Z")
780 }
781 fn expired_cert(cn: &str) -> Vec<u8> {
783 build_cert_der(cn, b"200101000000Z", b"210101000000Z")
784 }
785 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 #[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 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 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 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 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 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 assert_eq!(id.issuer_dn, id.subject_dn);
948 }
949
950 #[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); }
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"); }
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 #[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 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 #[test]
1048 fn test_cert_thumbprint_s256() {
1049 let der = valid_cert("rfc8705");
1050 let thumbprint = cert_thumbprint_s256(&der);
1051 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}