use sha2::{Digest, Sha256};
use tracing::{debug, warn};
use x509_parser::prelude::*;
use crate::error::{ClusterError, Result};
use crate::topology::NodeInfo;
pub const IDENTITY_MISMATCH_QUIC_ERROR: quinn::VarInt = quinn::VarInt::from_u32(0x02);
#[derive(Debug, PartialEq, Eq)]
pub enum VerifyOutcome {
Accepted { method: VerifyMethod },
BootstrapAccepted,
Rejected,
}
#[derive(Debug, PartialEq, Eq)]
pub enum VerifyMethod {
Spiffe,
SpkiPin,
}
pub fn spki_pin_from_cert_der(cert_der: &[u8]) -> Result<[u8; 32]> {
let (_, cert) = X509Certificate::from_der(cert_der).map_err(|e| ClusterError::Transport {
detail: format!("parse peer cert for SPKI pin: {e}"),
})?;
let spki_der = cert.public_key().raw;
let digest: [u8; 32] = Sha256::digest(spki_der).into();
Ok(digest)
}
pub fn spiffe_id_from_cert_der(cert_der: &[u8]) -> Option<String> {
let (_, cert) = X509Certificate::from_der(cert_der).ok()?;
for san in cert
.subject_alternative_name()
.ok()
.flatten()
.map(|ext| &ext.value.general_names)
.into_iter()
.flatten()
{
if let GeneralName::URI(uri) = san
&& uri.starts_with("spiffe://")
{
return Some(uri.to_string());
}
}
None
}
pub fn verify_peer_identity(info: &NodeInfo, peer_cert_der: &[u8]) -> VerifyOutcome {
let has_any_pin = info.spiffe_id.is_some() || info.spki_pin.is_some();
if !has_any_pin {
warn!(
node_id = info.node_id,
"accepting peer with no pinned identity (bootstrap window — pin will be recorded \
once the first JoinRequest is processed)"
);
return VerifyOutcome::BootstrapAccepted;
}
if let Some(pinned_spiffe) = &info.spiffe_id {
let peer_spiffe = spiffe_id_from_cert_der(peer_cert_der);
match peer_spiffe {
Some(ref s) if s == pinned_spiffe => {
debug!(
node_id = info.node_id,
spiffe = %s,
"peer identity verified via SPIFFE URI SAN"
);
return VerifyOutcome::Accepted {
method: VerifyMethod::Spiffe,
};
}
Some(ref s) => {
debug!(
node_id = info.node_id,
expected = %pinned_spiffe,
got = %s,
"SPIFFE URI SAN mismatch; trying SPKI pin"
);
}
None => {
debug!(
node_id = info.node_id,
"peer cert carries no SPIFFE URI SAN; trying SPKI pin"
);
}
}
}
if let Some(pinned_spki) = &info.spki_pin {
let peer_spki = match spki_pin_from_cert_der(peer_cert_der) {
Ok(p) => p,
Err(e) => {
warn!(
node_id = info.node_id,
error = %e,
"failed to extract SPKI pin from peer cert; rejecting"
);
return VerifyOutcome::Rejected;
}
};
if &peer_spki == pinned_spki {
debug!(
node_id = info.node_id,
"peer identity verified via SPKI fingerprint"
);
return VerifyOutcome::Accepted {
method: VerifyMethod::SpkiPin,
};
}
warn!(
node_id = info.node_id,
"SPKI pin mismatch — rejecting connection"
);
}
VerifyOutcome::Rejected
}
#[cfg(test)]
mod tests {
use super::*;
use crate::topology::{NodeInfo, NodeState};
fn make_node_no_pin(node_id: u64) -> NodeInfo {
NodeInfo::new(
node_id,
"127.0.0.1:9400".parse().unwrap(),
NodeState::Active,
)
}
fn make_node_spki(node_id: u64, pin: [u8; 32]) -> NodeInfo {
let mut n = make_node_no_pin(node_id);
n.spki_pin = Some(pin);
n
}
fn make_node_spiffe(node_id: u64, spiffe: &str) -> NodeInfo {
let mut n = make_node_no_pin(node_id);
n.spiffe_id = Some(spiffe.to_string());
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)
}
#[test]
fn no_pin_bootstrap_accepted() {
let node = make_node_no_pin(1);
let outcome = verify_peer_identity(&node, &[]);
assert_eq!(outcome, VerifyOutcome::BootstrapAccepted);
}
#[test]
fn spki_match_accepted() {
let (cert_der, pin) = gen_cert_and_pin();
let node = make_node_spki(2, pin);
let outcome = verify_peer_identity(&node, &cert_der);
assert_eq!(
outcome,
VerifyOutcome::Accepted {
method: VerifyMethod::SpkiPin
}
);
}
#[test]
fn spki_mismatch_rejected() {
let (cert_der, _pin) = gen_cert_and_pin();
let wrong_pin = [0xFFu8; 32];
let node = make_node_spki(3, wrong_pin);
let outcome = verify_peer_identity(&node, &cert_der);
assert_eq!(outcome, VerifyOutcome::Rejected);
}
#[test]
fn spiffe_match_accepted() {
use rcgen::{CertificateParams, Ia5String, KeyPair, SanType};
let key = KeyPair::generate().unwrap();
let mut params = CertificateParams::new(vec!["localhost".to_string()]).unwrap();
let spiffe_uri =
Ia5String::try_from("spiffe://cluster.local/ns/nodedb/node/42".to_string()).unwrap();
params.subject_alt_names.push(SanType::URI(spiffe_uri));
let cert = params.self_signed(&key).unwrap();
let cert_der = cert.der().to_vec();
let node = make_node_spiffe(42, "spiffe://cluster.local/ns/nodedb/node/42");
let outcome = verify_peer_identity(&node, &cert_der);
assert_eq!(
outcome,
VerifyOutcome::Accepted {
method: VerifyMethod::Spiffe
}
);
}
#[test]
fn spiffe_mismatch_falls_through_to_spki_rejection() {
use rcgen::{CertificateParams, Ia5String, KeyPair, SanType};
let key = KeyPair::generate().unwrap();
let mut params = CertificateParams::new(vec!["localhost".to_string()]).unwrap();
let spiffe_uri =
Ia5String::try_from("spiffe://cluster.local/ns/nodedb/node/99".to_string()).unwrap();
params.subject_alt_names.push(SanType::URI(spiffe_uri));
let cert = params.self_signed(&key).unwrap();
let cert_der = cert.der().to_vec();
let node = make_node_spiffe(42, "spiffe://cluster.local/ns/nodedb/node/42");
let outcome = verify_peer_identity(&node, &cert_der);
assert_eq!(outcome, VerifyOutcome::Rejected);
}
#[test]
fn envelope_node_id_cross_check_semantics() {
let (cert_der, pin) = gen_cert_and_pin();
let node_a = make_node_spki(10, pin);
let node_b = make_node_spki(20, pin);
assert_eq!(
verify_peer_identity(&node_a, &cert_der),
VerifyOutcome::Accepted {
method: VerifyMethod::SpkiPin
}
);
assert_eq!(
verify_peer_identity(&node_b, &cert_der),
VerifyOutcome::Accepted {
method: VerifyMethod::SpkiPin
}
);
}
}