use std::sync::Arc;
use tracing::{debug, warn};
use crate::transport::peer_identity_verifier::{spiffe_id_from_cert_der, spki_pin_from_cert_der};
use crate::transport::server::PeerIdentityStore;
pub struct PinnedClientVerifier {
pub(crate) inner: Arc<dyn rustls::server::danger::ClientCertVerifier>,
pub(crate) identity_store: Arc<dyn PeerIdentityStore>,
}
impl std::fmt::Debug for PinnedClientVerifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PinnedClientVerifier")
.finish_non_exhaustive()
}
}
impl rustls::server::danger::ClientCertVerifier for PinnedClientVerifier {
fn root_hint_subjects(&self) -> &[rustls::DistinguishedName] {
self.inner.root_hint_subjects()
}
fn verify_client_cert(
&self,
end_entity: &rustls::pki_types::CertificateDer<'_>,
intermediates: &[rustls::pki_types::CertificateDer<'_>],
now: rustls::pki_types::UnixTime,
) -> std::result::Result<rustls::server::danger::ClientCertVerified, rustls::Error> {
self.inner
.verify_client_cert(end_entity, intermediates, now)?;
check_cert_pin(end_entity.as_ref(), &*self.identity_store)
}
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &rustls::pki_types::CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct,
) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
self.inner.verify_tls12_signature(message, cert, dss)
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &rustls::pki_types::CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct,
) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
self.inner.verify_tls13_signature(message, cert, dss)
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
self.inner.supported_verify_schemes()
}
}
pub struct PinnedServerVerifier {
pub(crate) inner: Arc<dyn rustls::client::danger::ServerCertVerifier>,
pub(crate) identity_store: Arc<dyn PeerIdentityStore>,
}
impl std::fmt::Debug for PinnedServerVerifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PinnedServerVerifier")
.finish_non_exhaustive()
}
}
impl rustls::client::danger::ServerCertVerifier for PinnedServerVerifier {
fn verify_server_cert(
&self,
end_entity: &rustls::pki_types::CertificateDer<'_>,
intermediates: &[rustls::pki_types::CertificateDer<'_>],
server_name: &rustls::pki_types::ServerName<'_>,
ocsp_response: &[u8],
now: rustls::pki_types::UnixTime,
) -> std::result::Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
self.inner.verify_server_cert(
end_entity,
intermediates,
server_name,
ocsp_response,
now,
)?;
check_cert_pin(end_entity.as_ref(), &*self.identity_store)?;
Ok(rustls::client::danger::ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &rustls::pki_types::CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct,
) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
self.inner.verify_tls12_signature(message, cert, dss)
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &rustls::pki_types::CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct,
) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
self.inner.verify_tls13_signature(message, cert, dss)
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
self.inner.supported_verify_schemes()
}
}
pub(crate) fn check_cert_pin(
cert_der: &[u8],
store: &dyn PeerIdentityStore,
) -> std::result::Result<rustls::server::danger::ClientCertVerified, rustls::Error> {
let spki = spki_pin_from_cert_der(cert_der).map_err(|_| {
rustls::Error::InvalidCertificate(rustls::CertificateError::Other(rustls::OtherError(
Arc::new(PinCheckError(
"failed to extract SPKI pin from peer cert".into(),
)),
)))
})?;
if let Some(peer_spiffe) = spiffe_id_from_cert_der(cert_der)
&& let Some(node) = store.find_by_spiffe(&peer_spiffe)
{
if node.spki_pin.is_none_or(|pinned| pinned == spki) {
debug!(spiffe = %peer_spiffe, "TLS pin check: accepted via SPIFFE");
return Ok(rustls::server::danger::ClientCertVerified::assertion());
}
warn!(
spiffe = %peer_spiffe,
"TLS pin check: SPIFFE matched topology entry but SPKI mismatch — rejecting"
);
return Err(rustls::Error::InvalidCertificate(
rustls::CertificateError::Other(rustls::OtherError(Arc::new(PinCheckError(
"SPIFFE/SPKI binding mismatch".into(),
)))),
));
}
if store.find_by_spki(&spki).is_some() {
debug!("TLS pin check: accepted via SPKI fingerprint");
return Ok(rustls::server::danger::ClientCertVerified::assertion());
}
warn!("TLS pin check: cert not in topology, accepting as bootstrap window");
Ok(rustls::server::danger::ClientCertVerified::assertion())
}
#[derive(Debug)]
struct PinCheckError(String);
impl std::fmt::Display for PinCheckError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for PinCheckError {}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::RwLock;
use super::*;
use crate::topology::{NodeInfo, NodeState};
use crate::transport::peer_identity_verifier::spki_pin_from_cert_der;
use crate::transport::server::PeerIdentityStore;
struct MapIdentityStore {
by_id: RwLock<HashMap<u64, NodeInfo>>,
}
impl MapIdentityStore {
fn new() -> Self {
Self {
by_id: RwLock::new(HashMap::new()),
}
}
fn insert(&self, info: NodeInfo) {
self.by_id.write().unwrap().insert(info.node_id, info);
}
}
impl PeerIdentityStore for MapIdentityStore {
fn get_node_info(&self, node_id: u64) -> Option<NodeInfo> {
self.by_id.read().unwrap().get(&node_id).cloned()
}
fn find_by_spki(&self, spki: &[u8; 32]) -> Option<NodeInfo> {
self.by_id
.read()
.unwrap()
.values()
.find(|n| n.spki_pin.as_ref() == Some(spki))
.cloned()
}
fn find_by_spiffe(&self, spiffe_id: &str) -> Option<NodeInfo> {
self.by_id
.read()
.unwrap()
.values()
.find(|n| n.spiffe_id.as_deref() == Some(spiffe_id))
.cloned()
}
}
fn addr() -> SocketAddr {
"127.0.0.1:9400".parse().unwrap()
}
fn node_with_spki(node_id: u64, pin: [u8; 32]) -> NodeInfo {
let mut n = NodeInfo::new(node_id, addr(), NodeState::Active);
n.spki_pin = Some(pin);
n
}
fn node_with_spiffe(node_id: u64, spiffe: &str, pin: Option<[u8; 32]>) -> NodeInfo {
let mut n = NodeInfo::new(node_id, addr(), NodeState::Active);
n.spiffe_id = Some(spiffe.to_string());
n.spki_pin = pin;
n
}
fn gen_cert_and_pin() -> (Vec<u8>, [u8; 32]) {
use rcgen::{CertificateParams, KeyPair};
let key = KeyPair::generate().unwrap();
let params = CertificateParams::new(vec!["localhost".to_string()]).unwrap();
let cert = params.self_signed(&key).unwrap();
let cert_der = cert.der().to_vec();
let pin = spki_pin_from_cert_der(&cert_der).unwrap();
(cert_der, pin)
}
fn gen_cert_with_spiffe(spiffe_uri: &str) -> (Vec<u8>, [u8; 32]) {
use rcgen::{CertificateParams, Ia5String, KeyPair, SanType};
let key = KeyPair::generate().unwrap();
let mut params = CertificateParams::new(vec!["localhost".to_string()]).unwrap();
let uri = Ia5String::try_from(spiffe_uri.to_string()).unwrap();
params.subject_alt_names.push(SanType::URI(uri));
let cert = params.self_signed(&key).unwrap();
let cert_der = cert.der().to_vec();
let pin = spki_pin_from_cert_der(&cert_der).unwrap();
(cert_der, pin)
}
#[test]
fn known_spki_accepted() {
let (cert_der, pin) = gen_cert_and_pin();
let store = MapIdentityStore::new();
store.insert(node_with_spki(1, pin));
let result = check_cert_pin(&cert_der, &store);
assert!(result.is_ok(), "expected accepted, got: {result:?}");
}
#[test]
fn wrong_spki_rejected() {
let wrong_pin = [0xFFu8; 32];
let spiffe_uri = "spiffe://cluster.local/ns/nodedb/node/1";
let (cert_with_spiffe, cert_spki) = gen_cert_with_spiffe(spiffe_uri);
let node = node_with_spiffe(1, spiffe_uri, Some(wrong_pin));
let store = MapIdentityStore::new();
store.insert(node);
let result = check_cert_pin(&cert_with_spiffe, &store);
assert!(
result.is_err(),
"expected rejection for SPIFFE/SPKI mismatch, got ok; cert_spki={cert_spki:?} wrong_pin={wrong_pin:?}"
);
}
#[test]
fn unknown_spki_bootstrap_window_accepted() {
let (cert_der, _pin) = gen_cert_and_pin();
let store = MapIdentityStore::new(); let result = check_cert_pin(&cert_der, &store);
assert!(
result.is_ok(),
"expected bootstrap window accept, got: {result:?}"
);
}
#[test]
fn spiffe_match_accepted() {
let spiffe_uri = "spiffe://cluster.local/ns/nodedb/node/42";
let (cert_der, pin) = gen_cert_with_spiffe(spiffe_uri);
let node = node_with_spiffe(42, spiffe_uri, Some(pin));
let store = MapIdentityStore::new();
store.insert(node);
let result = check_cert_pin(&cert_der, &store);
assert!(
result.is_ok(),
"expected accepted via SPIFFE, got: {result:?}"
);
}
#[test]
fn spiffe_match_no_spki_pin_accepted() {
let spiffe_uri = "spiffe://cluster.local/ns/nodedb/node/7";
let (cert_der, _pin) = gen_cert_with_spiffe(spiffe_uri);
let node = node_with_spiffe(7, spiffe_uri, None);
let store = MapIdentityStore::new();
store.insert(node);
let result = check_cert_pin(&cert_der, &store);
assert!(
result.is_ok(),
"expected accepted (SPIFFE, no pin), got: {result:?}"
);
}
#[test]
fn pinned_client_verifier_noop_store_bootstrap_window() {
use crate::transport::server::NoopIdentityStore;
let (cert_der, _pin) = gen_cert_and_pin();
let store = Arc::new(NoopIdentityStore) as Arc<dyn PeerIdentityStore>;
let result = check_cert_pin(&cert_der, &*store);
assert!(result.is_ok());
}
}