#[cfg(feature = "tls")]
use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer, PrivateSec1KeyDer};
use std::collections::BTreeSet;
use std::io::BufReader;
use std::path::Path;
use std::sync::Arc;
use super::error::TlsError;
#[derive(Clone, Debug)]
pub struct Certificate {
#[cfg(feature = "tls")]
inner: CertificateDer<'static>,
#[cfg(not(feature = "tls"))]
_data: Vec<u8>,
}
impl Certificate {
#[inline]
#[cfg(feature = "tls")]
pub fn from_der(der: impl Into<Vec<u8>>) -> Self {
Self {
inner: CertificateDer::from(der.into()),
}
}
#[inline]
#[cfg(not(feature = "tls"))]
pub fn from_der(der: impl Into<Vec<u8>>) -> Self {
Self { _data: der.into() }
}
#[cfg(feature = "tls")]
pub fn from_pem(pem: &[u8]) -> Result<Vec<Self>, TlsError> {
let mut reader = BufReader::new(pem);
let certs: Vec<_> = rustls_pemfile::certs(&mut reader)
.collect::<Result<Vec<_>, _>>()
.map_err(|e| TlsError::Certificate(e.to_string()))?;
if certs.is_empty() {
return Err(TlsError::Certificate("no certificates found in PEM".into()));
}
Ok(certs.into_iter().map(|c| Self { inner: c }).collect())
}
#[cfg(not(feature = "tls"))]
pub fn from_pem(_pem: &[u8]) -> Result<Vec<Self>, TlsError> {
Err(TlsError::Configuration("tls feature not enabled".into()))
}
pub fn from_pem_file(path: impl AsRef<Path>) -> Result<Vec<Self>, TlsError> {
let pem = std::fs::read(path.as_ref())
.map_err(|e| TlsError::Certificate(format!("reading file: {e}")))?;
Self::from_pem(&pem)
}
#[inline]
#[cfg(feature = "tls")]
pub fn as_der(&self) -> &[u8] {
self.inner.as_ref()
}
#[inline]
#[cfg(not(feature = "tls"))]
pub fn as_der(&self) -> &[u8] {
&self._data
}
#[inline]
#[cfg(feature = "tls")]
pub(crate) fn into_inner(self) -> CertificateDer<'static> {
self.inner
}
}
#[derive(Clone, Debug, Default)]
pub struct CertificateChain {
certs: Vec<Certificate>,
}
impl CertificateChain {
#[inline]
pub fn new() -> Self {
Self { certs: Vec::new() }
}
pub fn from_cert(cert: Certificate) -> Self {
Self { certs: vec![cert] }
}
pub fn push(&mut self, cert: Certificate) {
self.certs.push(cert);
}
pub fn len(&self) -> usize {
self.certs.len()
}
pub fn is_empty(&self) -> bool {
self.certs.is_empty()
}
pub fn from_pem_file(path: impl AsRef<Path>) -> Result<Self, TlsError> {
let certs = Certificate::from_pem_file(path)?;
Ok(Self::from(certs))
}
pub fn from_pem(pem: &[u8]) -> Result<Self, TlsError> {
let certs = Certificate::from_pem(pem)?;
Ok(Self::from(certs))
}
#[cfg(feature = "tls")]
pub(crate) fn into_inner(self) -> Vec<CertificateDer<'static>> {
self.certs
.into_iter()
.map(Certificate::into_inner)
.collect()
}
}
impl From<Vec<Certificate>> for CertificateChain {
fn from(certs: Vec<Certificate>) -> Self {
Self { certs }
}
}
impl IntoIterator for CertificateChain {
type Item = Certificate;
type IntoIter = std::vec::IntoIter<Certificate>;
fn into_iter(self) -> Self::IntoIter {
self.certs.into_iter()
}
}
#[derive(Clone)]
pub struct PrivateKey {
#[cfg(feature = "tls")]
inner: Arc<PrivateKeyDer<'static>>,
#[cfg(not(feature = "tls"))]
_data: Vec<u8>,
}
impl PrivateKey {
#[cfg(feature = "tls")]
pub fn from_pkcs8_der(der: impl Into<Vec<u8>>) -> Self {
Self {
inner: Arc::new(PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(der.into()))),
}
}
#[cfg(not(feature = "tls"))]
pub fn from_pkcs8_der(der: impl Into<Vec<u8>>) -> Self {
Self { _data: der.into() }
}
#[cfg(feature = "tls")]
pub fn from_pem(pem: &[u8]) -> Result<Self, TlsError> {
let mut reader = BufReader::new(pem);
let pkcs8_keys: Vec<_> = rustls_pemfile::pkcs8_private_keys(&mut reader)
.collect::<Result<Vec<_>, _>>()
.map_err(|e| TlsError::Certificate(e.to_string()))?;
if let Some(key) = pkcs8_keys.into_iter().next() {
return Ok(Self {
inner: Arc::new(PrivateKeyDer::Pkcs8(key)),
});
}
let mut reader = BufReader::new(pem);
let rsa_keys: Vec<_> = rustls_pemfile::rsa_private_keys(&mut reader)
.collect::<Result<Vec<_>, _>>()
.map_err(|e| TlsError::Certificate(e.to_string()))?;
if let Some(key) = rsa_keys.into_iter().next() {
return Ok(Self {
inner: Arc::new(PrivateKeyDer::Pkcs1(key)),
});
}
let mut reader = BufReader::new(pem);
let ec_keys: Vec<_> = rustls_pemfile::ec_private_keys(&mut reader)
.collect::<Result<Vec<_>, _>>()
.map_err(|e| TlsError::Certificate(e.to_string()))?;
if let Some(key) = ec_keys.into_iter().next() {
return Ok(Self {
inner: Arc::new(PrivateKeyDer::Sec1(key)),
});
}
Err(TlsError::Certificate("no private key found in PEM".into()))
}
#[cfg(not(feature = "tls"))]
pub fn from_pem(_pem: &[u8]) -> Result<Self, TlsError> {
Err(TlsError::Configuration("tls feature not enabled".into()))
}
pub fn from_pem_file(path: impl AsRef<Path>) -> Result<Self, TlsError> {
let pem = std::fs::read(path.as_ref())
.map_err(|e| TlsError::Certificate(format!("reading file: {e}")))?;
Self::from_pem(&pem)
}
#[cfg(feature = "tls")]
pub fn from_sec1_der(der: impl Into<Vec<u8>>) -> Self {
Self {
inner: Arc::new(PrivateKeyDer::Sec1(PrivateSec1KeyDer::from(der.into()))),
}
}
#[cfg(not(feature = "tls"))]
pub fn from_sec1_der(der: impl Into<Vec<u8>>) -> Self {
Self { _data: der.into() }
}
#[cfg(feature = "tls")]
pub(crate) fn clone_inner(&self) -> PrivateKeyDer<'static> {
(*self.inner).clone_key()
}
}
impl std::fmt::Debug for PrivateKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PrivateKey")
.field("type", &"[redacted]")
.finish()
}
}
#[derive(Clone, Debug)]
pub struct RootCertStore {
#[cfg(feature = "tls")]
inner: rustls::RootCertStore,
#[cfg(not(feature = "tls"))]
certs: Vec<Certificate>,
}
impl Default for RootCertStore {
fn default() -> Self {
Self::empty()
}
}
impl RootCertStore {
#[cfg(feature = "tls")]
pub fn empty() -> Self {
Self {
inner: rustls::RootCertStore::empty(),
}
}
#[cfg(not(feature = "tls"))]
pub fn empty() -> Self {
Self { certs: Vec::new() }
}
#[cfg(feature = "tls")]
pub fn add(&mut self, cert: &Certificate) -> Result<(), crate::tls::TlsError> {
self.inner
.add(cert.clone().into_inner())
.map_err(|e| crate::tls::TlsError::Certificate(e.to_string()))
}
#[cfg(not(feature = "tls"))]
pub fn add(&mut self, cert: &Certificate) -> Result<(), crate::tls::TlsError> {
self.certs.push(cert.clone());
Ok(())
}
#[cfg(feature = "tls")]
pub fn len(&self) -> usize {
self.inner.len()
}
#[cfg(not(feature = "tls"))]
pub fn len(&self) -> usize {
self.certs.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn add_pem_file(&mut self, path: impl AsRef<Path>) -> Result<usize, TlsError> {
let certs = Certificate::from_pem_file(path)?;
let mut count = 0;
for cert in &certs {
if self.add(cert).is_ok() {
count += 1;
}
}
Ok(count)
}
#[cfg(feature = "tls-webpki-roots")]
pub fn extend_from_webpki_roots(&mut self) {
self.inner
.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
}
#[cfg(not(feature = "tls-webpki-roots"))]
pub fn extend_from_webpki_roots(&mut self) {
}
#[cfg(feature = "tls-native-roots")]
pub fn extend_from_native_roots(&mut self) -> Result<usize, TlsError> {
let result = rustls_native_certs::load_native_certs();
let mut count = 0;
for cert in result.certs {
if self
.inner
.add(rustls_pki_types::CertificateDer::from(cert.to_vec()))
.is_ok()
{
count += 1;
}
}
Ok(count)
}
#[cfg(not(feature = "tls-native-roots"))]
pub fn extend_from_native_roots(&mut self) -> Result<usize, TlsError> {
Err(TlsError::Configuration(
"tls-native-roots feature not enabled".into(),
))
}
#[cfg(feature = "tls")]
pub(crate) fn into_inner(self) -> rustls::RootCertStore {
self.inner
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum CertificatePin {
SpkiSha256(Vec<u8>),
CertSha256(Vec<u8>),
}
impl CertificatePin {
pub fn spki_sha256_base64(base64_hash: &str) -> Result<Self, TlsError> {
use base64::Engine;
let bytes = base64::engine::general_purpose::STANDARD
.decode(base64_hash)
.map_err(|e| TlsError::Certificate(format!("invalid base64: {e}")))?;
if bytes.len() != 32 {
return Err(TlsError::Certificate(format!(
"SPKI SHA-256 hash must be 32 bytes, got {}",
bytes.len()
)));
}
Ok(Self::SpkiSha256(bytes))
}
pub fn cert_sha256_base64(base64_hash: &str) -> Result<Self, TlsError> {
use base64::Engine;
let bytes = base64::engine::general_purpose::STANDARD
.decode(base64_hash)
.map_err(|e| TlsError::Certificate(format!("invalid base64: {e}")))?;
if bytes.len() != 32 {
return Err(TlsError::Certificate(format!(
"certificate SHA-256 hash must be 32 bytes, got {}",
bytes.len()
)));
}
Ok(Self::CertSha256(bytes))
}
pub fn spki_sha256(hash: impl Into<Vec<u8>>) -> Result<Self, TlsError> {
let bytes = hash.into();
if bytes.len() != 32 {
return Err(TlsError::Certificate(format!(
"SPKI SHA-256 hash must be 32 bytes, got {}",
bytes.len()
)));
}
Ok(Self::SpkiSha256(bytes))
}
pub fn cert_sha256(hash: impl Into<Vec<u8>>) -> Result<Self, TlsError> {
let bytes = hash.into();
if bytes.len() != 32 {
return Err(TlsError::Certificate(format!(
"certificate SHA-256 hash must be 32 bytes, got {}",
bytes.len()
)));
}
Ok(Self::CertSha256(bytes))
}
#[cfg(feature = "tls")]
pub fn compute_spki_sha256(cert: &Certificate) -> Result<Self, TlsError> {
use ring::digest::{SHA256, digest};
let (_, parsed) = x509_parser::parse_x509_certificate(cert.as_der())
.map_err(|e| TlsError::Certificate(format!("failed to parse certificate DER: {e}")))?;
let hash = digest(&SHA256, parsed.public_key().raw);
Ok(Self::SpkiSha256(hash.as_ref().to_vec()))
}
#[cfg(not(feature = "tls"))]
pub fn compute_spki_sha256(_cert: &Certificate) -> Result<Self, TlsError> {
Err(TlsError::Configuration("tls feature not enabled".into()))
}
#[cfg(feature = "tls")]
pub fn compute_cert_sha256(cert: &Certificate) -> Result<Self, TlsError> {
use ring::digest::{SHA256, digest};
let hash = digest(&SHA256, cert.as_der());
Ok(Self::CertSha256(hash.as_ref().to_vec()))
}
#[cfg(not(feature = "tls"))]
pub fn compute_cert_sha256(_cert: &Certificate) -> Result<Self, TlsError> {
Err(TlsError::Configuration("tls feature not enabled".into()))
}
pub fn to_base64(&self) -> String {
use base64::Engine;
match self {
Self::SpkiSha256(bytes) | Self::CertSha256(bytes) => {
base64::engine::general_purpose::STANDARD.encode(bytes)
}
}
}
pub fn hash_bytes(&self) -> &[u8] {
match self {
Self::SpkiSha256(bytes) | Self::CertSha256(bytes) => bytes,
}
}
}
#[derive(Clone, Debug)]
pub struct CertificatePinSet {
pins: BTreeSet<CertificatePin>,
enforce: bool,
}
impl Default for CertificatePinSet {
fn default() -> Self {
Self::new()
}
}
impl CertificatePinSet {
pub fn new() -> Self {
Self {
pins: BTreeSet::new(),
enforce: true,
}
}
pub fn report_only() -> Self {
Self {
pins: BTreeSet::new(),
enforce: false,
}
}
pub fn add(&mut self, pin: CertificatePin) {
self.pins.insert(pin);
}
pub fn with_pin(mut self, pin: CertificatePin) -> Self {
self.add(pin);
self
}
pub fn add_spki_sha256_base64(&mut self, base64_hash: &str) -> Result<(), TlsError> {
self.add(CertificatePin::spki_sha256_base64(base64_hash)?);
Ok(())
}
pub fn add_cert_sha256_base64(&mut self, base64_hash: &str) -> Result<(), TlsError> {
self.add(CertificatePin::cert_sha256_base64(base64_hash)?);
Ok(())
}
pub fn is_empty(&self) -> bool {
self.pins.is_empty()
}
pub fn len(&self) -> usize {
self.pins.len()
}
pub fn is_enforcing(&self) -> bool {
self.enforce
}
pub fn set_enforce(&mut self, enforce: bool) {
self.enforce = enforce;
}
#[cfg(feature = "tls")]
pub fn validate(&self, cert: &Certificate) -> Result<bool, TlsError> {
if self.pins.is_empty() {
return Ok(true);
}
let spki_pin = CertificatePin::compute_spki_sha256(cert).ok();
let cert_pin = CertificatePin::compute_cert_sha256(cert).ok();
if spki_pin.as_ref().is_some_and(|p| self.pins.contains(p))
|| cert_pin.as_ref().is_some_and(|p| self.pins.contains(p))
{
return Ok(true);
}
if self.enforce {
let expected: Vec<String> = self.pins.iter().map(CertificatePin::to_base64).collect();
let actual = spki_pin
.as_ref()
.or(cert_pin.as_ref())
.map_or_else(|| "<unavailable>".to_string(), CertificatePin::to_base64);
Err(TlsError::PinMismatch { expected, actual })
} else {
#[cfg(feature = "tracing-integration")]
tracing::warn!(
expected = ?self.pins.iter().map(CertificatePin::to_base64).collect::<Vec<_>>(),
actual_spki = %spki_pin.as_ref().map_or_else(|| "<unavailable>".to_string(), CertificatePin::to_base64),
actual_cert = %cert_pin.as_ref().map_or_else(|| "<unavailable>".to_string(), CertificatePin::to_base64),
"Certificate pin mismatch (report-only mode)"
);
Ok(false)
}
}
#[cfg(not(feature = "tls"))]
pub fn validate(&self, _cert: &Certificate) -> Result<bool, TlsError> {
Err(TlsError::Configuration("tls feature not enabled".into()))
}
pub fn iter(&self) -> impl Iterator<Item = &CertificatePin> {
self.pins.iter()
}
}
impl FromIterator<CertificatePin> for CertificatePinSet {
fn from_iter<I: IntoIterator<Item = CertificatePin>>(iter: I) -> Self {
Self {
pins: iter.into_iter().collect(),
enforce: true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "tls")]
const TEST_CERT_PEM: &[u8] = br"-----BEGIN CERTIFICATE-----
MIIDGjCCAgKgAwIBAgIUEOa/xZnL2Xclme2QSueCrHSMLnEwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDIyNjIyMjk1MloXDTM2MDIy
NDIyMjk1MlowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEAx1JqCHpDIHPR4H1LDrb3gHVCzoKujANyHdOKw7CTLKdz
JbDybwJYqZ8vZpq0xwhYKpHdGO4yv7yLT7a2kThq3MrxohfXp9tv1Dop7siTQiWT
7uGYJzh1bOhw7ElLJc8bW/mBf7ksMyqkX8/8mRXRWqqDv3dKe5CrSt2Pqti9tYH0
DcT2fftUGT14VvL/Fq1kWPM16ebTRCFp/4ki/Th7SzFvTN99L45MAilHZFefRSzc
9xN1qQZNm7lT6oo0zD3wmOy70iiasqpLrmG51TRdbnBnGH6CIHvUIl3rCDteUuj1
pB9lh67qt5kipCn4+8zceXmUaO/nmRawC7Vz+6AsTwIDAQABo2QwYjALBgNVHQ8E
BAMCBLAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwFAYDVR0RBA0wC4IJbG9jYWxob3N0
MAkGA1UdEwQCMAAwHQYDVR0OBBYEFEGZkeJqxBWpc24NHkE8k5PM8gTyMA0GCSqG
SIb3DQEBCwUAA4IBAQAzfQ4na2v1VhK/dyhC89rMHPN/8OX7CGWwrpWlEOYtpMds
OyQKTZjdz8aFSFl9rvnyGRHrdo4J1RoMGNR5wt1XQ7+k3l/iEWRlSRw+JU6+jqsx
xfjik55Dji36pN7ARGW4ADBpc3yTOHFhaH41GpSZ6s/2KdGG2gifo7UGNdkdgL60
nxRt1tfapaNtzpi90TfDx2w6MQmkNMKVOowbYX/zUY7kklJLP8KWTwXO7eovtIpr
FPAy+SbPl3+sqPbes5IqAQO9jhjb0w0/5RlSTPtiKetb6gAA7Yqw+yZWkBN0WDye
Lru15URJw9pE1Uae8IuzyzHiF1fnn45swnvW3Szb
-----END CERTIFICATE-----";
#[cfg(feature = "tls")]
const TEST_CERT_SPKI_SHA256_BASE64: &str = "Wic7R2QEWx8m0gjc0UYQD4iTxorg2Q51QmvN8HuCprc=";
#[test]
fn certificate_from_der() {
let cert = Certificate::from_der(vec![0x30, 0x00]);
assert_eq!(cert.as_der().len(), 2);
}
#[test]
fn certificate_chain_operations() {
let chain = CertificateChain::new();
assert!(chain.is_empty());
assert_eq!(chain.len(), 0);
let mut chain = CertificateChain::new();
chain.push(Certificate::from_der(vec![1, 2, 3]));
assert!(!chain.is_empty());
assert_eq!(chain.len(), 1);
}
#[test]
fn certificate_chain_from_cert() {
let cert = Certificate::from_der(vec![1, 2, 3]);
let chain = CertificateChain::from_cert(cert);
assert_eq!(chain.len(), 1);
}
#[test]
fn root_cert_store_empty() {
let store = RootCertStore::empty();
assert!(store.is_empty());
assert_eq!(store.len(), 0);
}
#[test]
fn certificate_pin_spki_base64_valid() {
let hash = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
let pin = CertificatePin::spki_sha256_base64(hash).unwrap();
assert!(matches!(pin, CertificatePin::SpkiSha256(_)));
assert_eq!(pin.hash_bytes().len(), 32);
assert_eq!(pin.to_base64(), hash);
}
#[test]
fn certificate_pin_cert_base64_valid() {
let hash = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
let pin = CertificatePin::cert_sha256_base64(hash).unwrap();
assert!(matches!(pin, CertificatePin::CertSha256(_)));
assert_eq!(pin.hash_bytes().len(), 32);
}
#[test]
fn certificate_pin_invalid_base64() {
let result = CertificatePin::spki_sha256_base64("not valid base64!!!");
assert!(result.is_err());
}
#[test]
fn certificate_pin_wrong_length() {
let short_hash = "AAAAAAAAAAAAAAAAAAAAAA==";
let result = CertificatePin::spki_sha256_base64(short_hash);
assert!(result.is_err());
}
#[test]
fn certificate_pin_from_raw_bytes_valid() {
let bytes = vec![0u8; 32];
let pin = CertificatePin::spki_sha256(bytes).unwrap();
assert_eq!(pin.hash_bytes().len(), 32);
}
#[test]
fn certificate_pin_from_raw_bytes_wrong_length() {
let bytes = vec![0u8; 16];
let result = CertificatePin::spki_sha256(bytes);
assert!(result.is_err());
}
#[cfg(feature = "tls")]
#[test]
fn certificate_pin_compute_spki_sha256_known_answer() {
let cert = Certificate::from_pem(TEST_CERT_PEM).unwrap().remove(0);
let pin = CertificatePin::compute_spki_sha256(&cert).unwrap();
assert_eq!(pin.to_base64(), TEST_CERT_SPKI_SHA256_BASE64);
}
#[cfg(feature = "tls")]
#[test]
fn certificate_pin_compute_spki_sha256_rejects_invalid_der() {
let cert = Certificate::from_der(vec![0x30, 0x00]);
let err = CertificatePin::compute_spki_sha256(&cert).unwrap_err();
match err {
TlsError::Certificate(message) => {
assert!(message.contains("failed to parse certificate DER"));
}
other => panic!("expected certificate error, got {other:?}"),
}
}
#[cfg(feature = "tls")]
#[test]
fn pin_set_validate_accepts_matching_spki_pin() {
let cert = Certificate::from_pem(TEST_CERT_PEM).unwrap().remove(0);
let pin = CertificatePin::compute_spki_sha256(&cert).unwrap();
let mut set = CertificatePinSet::new();
set.add(pin);
assert!(set.validate(&cert).unwrap());
}
#[test]
fn pin_set_operations() {
let mut set = CertificatePinSet::new();
assert!(set.is_empty());
assert!(set.is_enforcing());
let pin = CertificatePin::spki_sha256(vec![0u8; 32]).unwrap();
set.add(pin);
assert!(!set.is_empty());
assert_eq!(set.len(), 1);
}
#[test]
fn pin_set_report_only_mode() {
let set = CertificatePinSet::report_only();
assert!(!set.is_enforcing());
}
#[test]
fn pin_set_builder_pattern() {
let pin = CertificatePin::spki_sha256(vec![0u8; 32]).unwrap();
let set = CertificatePinSet::new().with_pin(pin);
assert_eq!(set.len(), 1);
}
#[test]
fn pin_set_add_from_base64() {
let mut set = CertificatePinSet::new();
let hash = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
set.add_spki_sha256_base64(hash).unwrap();
set.add_cert_sha256_base64(hash).unwrap();
assert_eq!(set.len(), 2);
}
#[test]
fn pin_set_from_iterator() {
let set: CertificatePinSet = (0..3)
.map(|i| CertificatePin::spki_sha256(vec![i; 32]).unwrap())
.collect();
assert_eq!(set.len(), 3);
}
#[test]
fn pin_set_empty_validates_any() {
let set = CertificatePinSet::new();
#[cfg(feature = "tls")]
{
let _ = &set;
}
#[cfg(not(feature = "tls"))]
{
let _ = &set;
}
}
#[test]
fn pin_equality_and_hash() {
let pin1 = CertificatePin::spki_sha256(vec![1u8; 32]).unwrap();
let pin2 = CertificatePin::spki_sha256(vec![1u8; 32]).unwrap();
let pin3 = CertificatePin::spki_sha256(vec![2u8; 32]).unwrap();
assert_eq!(pin1, pin2);
assert_ne!(pin1, pin3);
let mut set = std::collections::BTreeSet::new();
set.insert(pin1);
assert!(set.contains(&pin2));
assert!(!set.contains(&pin3));
}
#[test]
fn private_key_debug_is_redacted() {
#[cfg(feature = "tls")]
{
let key = PrivateKey::from_pkcs8_der(vec![0u8; 32]);
let debug_str = format!("{key:?}");
assert!(debug_str.contains("redacted"));
assert!(!debug_str.contains('0'));
}
}
#[test]
fn error_variants_display() {
use super::super::error::TlsError;
let expired = TlsError::CertificateExpired {
expired_at: 1_000_000,
description: "test cert".to_string(),
};
let display = format!("{expired}");
assert!(display.contains("expired"));
assert!(display.contains("1000000"));
let not_yet = TlsError::CertificateNotYetValid {
valid_from: 2_000_000,
description: "test cert".to_string(),
};
let display = format!("{not_yet}");
assert!(display.contains("not valid"));
assert!(display.contains("2000000"));
let chain = TlsError::ChainValidation("chain error".to_string());
let display = format!("{chain}");
assert!(display.contains("chain"));
let pin_mismatch = TlsError::PinMismatch {
expected: vec!["pin1".to_string(), "pin2".to_string()],
actual: "actual_pin".to_string(),
};
let display = format!("{pin_mismatch}");
assert!(display.contains("mismatch"));
assert!(display.contains("actual_pin"));
}
}