use anyhow::Context;
use rustls::pki_types::CertificateDer;
use std::sync::Arc;
use x509_parser::extensions::{GeneralName, ParsedExtension};
use x509_parser::prelude::FromDer;
#[derive(Debug, Clone)]
pub struct ClientCertIdentity {
pub cn: String,
pub subject: String,
pub sans: Vec<String>,
}
impl ClientCertIdentity {
pub fn from_chain(
chain: &[CertificateDer<'_>],
) -> anyhow::Result<Self> {
let leaf = chain
.first()
.ok_or_else(|| anyhow::anyhow!("empty peer cert chain"))?;
Self::from_der(leaf.as_ref())
}
pub fn from_der(der: &[u8]) -> anyhow::Result<Self> {
let (_, cert) = x509_parser::certificate::X509Certificate::from_der(
der,
)
.map_err(|e| anyhow::anyhow!("client cert parse: {e:?}"))?;
let subject_str = cert.subject().to_string();
let cn = cert
.subject()
.iter_common_name()
.next()
.and_then(|cn| cn.as_str().ok())
.map(str::to_owned)
.filter(|s| !s.is_empty())
.unwrap_or_else(|| subject_str.clone());
let mut sans = Vec::new();
for ext in cert.extensions() {
if let ParsedExtension::SubjectAlternativeName(san) =
ext.parsed_extension()
{
for name in &san.general_names {
match name {
GeneralName::DNSName(s) => sans.push((*s).to_owned()),
GeneralName::URI(s) => sans.push((*s).to_owned()),
GeneralName::RFC822Name(s) => {
sans.push((*s).to_owned())
}
_ => {}
}
}
}
}
Ok(ClientCertIdentity { cn, subject: subject_str, sans })
}
}
pub fn identity_from_connection(
conn: &rustls::ServerConnection,
) -> Option<Arc<ClientCertIdentity>> {
let chain = conn.peer_certificates()?;
match ClientCertIdentity::from_chain(chain).context(
"parsing verified client certificate",
) {
Ok(id) => Some(Arc::new(id)),
Err(e) => {
tracing::warn!(
"mtls: client cert parse failed after handshake: {e:#}"
);
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rcgen::{CertificateParams, DnType, KeyPair, SanType};
fn build_cert(cn: &str, sans: &[&str]) -> Vec<u8> {
let mut params =
CertificateParams::new(Vec::<String>::new()).unwrap();
params.distinguished_name.push(DnType::CommonName, cn);
for s in sans {
params
.subject_alt_names
.push(SanType::DnsName((*s).try_into().unwrap()));
}
let kp = KeyPair::generate().unwrap();
params.self_signed(&kp).unwrap().der().to_vec()
}
#[test]
fn parses_cn_and_sans() {
let der = build_cert("alice", &["alice.example.com", "alt.example"]);
let id = ClientCertIdentity::from_der(&der).unwrap();
assert_eq!(id.cn, "alice");
assert!(id.subject.contains("CN=alice"), "subject={}", id.subject);
assert_eq!(
id.sans,
vec![
"alice.example.com".to_string(),
"alt.example".to_string()
]
);
}
#[test]
fn empty_san_list_when_unset() {
let der = build_cert("alice", &[]);
let id = ClientCertIdentity::from_der(&der).unwrap();
assert!(id.sans.is_empty());
}
#[test]
fn empty_chain_is_error() {
let err = ClientCertIdentity::from_chain(&[]).unwrap_err();
assert!(
err.to_string().contains("empty peer cert chain"),
"got: {err}"
);
}
}