use rustls::pki_types::CertificateDer;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct NodeIdentity(String);
pub type ReplicationPeerIdentity = NodeIdentity;
pub type ClusterVoterIdentity = NodeIdentity;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NodeIdentityError {
EmptySubject,
CertificateParse(String),
}
impl std::fmt::Display for NodeIdentityError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::EmptySubject => write!(f, "certificate subject is empty"),
Self::CertificateParse(err) => write!(f, "certificate parse error: {err}"),
}
}
}
impl std::error::Error for NodeIdentityError {}
impl NodeIdentity {
pub fn from_certificate_subject(subject: impl AsRef<str>) -> Result<Self, NodeIdentityError> {
let subject = subject.as_ref().trim();
if subject.is_empty() {
return Err(NodeIdentityError::EmptySubject);
}
Ok(Self(subject.to_string()))
}
pub fn from_peer_certificate_der(cert: &CertificateDer<'_>) -> Result<Self, NodeIdentityError> {
let (_, parsed) = x509_parser::parse_x509_certificate(cert.as_ref())
.map_err(|err| NodeIdentityError::CertificateParse(format!("{err:?}")))?;
Self::from_certificate_subject(parsed.subject().to_string())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for NodeIdentity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn node_identity_rejects_empty_certificate_subjects() {
assert_eq!(
NodeIdentity::from_certificate_subject(" ").unwrap_err(),
NodeIdentityError::EmptySubject
);
}
#[test]
fn cluster_voter_and_replication_peer_share_node_identity() {
let voter = ClusterVoterIdentity::from_certificate_subject("CN=node-a").unwrap();
let replica = ReplicationPeerIdentity::from_certificate_subject("CN=node-a").unwrap();
assert_eq!(voter, replica);
assert_eq!(voter.as_str(), "CN=node-a");
}
}