#[derive(Debug, Clone, Default, PartialEq)]
pub struct PeerIdentity {
pub subject_cn: Option<String>,
pub sans: Vec<String>,
}
const MAX_SANS: usize = 64;
const OID_CN: &[u8] = &[0x55, 0x04, 0x03];
const OID_SAN: &[u8] = &[0x55, 0x1D, 0x11];
pub fn parse(cert_der: &[u8]) -> PeerIdentity {
let mut out = PeerIdentity::default();
let Some(tbs) = Der::new(cert_der)
.seq()
.and_then(|body| Der::new(body).seq())
else {
return out;
};
let mut fields: Vec<(u8, &[u8])> = Vec::new();
let mut r = Der::new(tbs);
while let Some(f) = r.tlv() {
fields.push(f);
if fields.len() > 24 {
break; }
}
let base = usize::from(fields.first().is_some_and(|(t, _)| *t == 0xA0));
if let Some((0x30, subject)) = fields.get(base + 4).copied() {
out.subject_cn = subject_cn(subject);
}
if let Some((_, ext3)) = fields.iter().skip(base + 5).find(|(t, _)| *t == 0xA3) {
out.sans = sans(ext3);
}
out
}
fn subject_cn(name_der: &[u8]) -> Option<String> {
let mut rdns = Der::new(name_der);
while let Some((t, set)) = rdns.tlv() {
if t != 0x31 {
continue; }
let mut atvs = Der::new(set);
while let Some((t2, atv)) = atvs.tlv() {
if t2 != 0x30 {
continue;
}
let mut ir = Der::new(atv);
let Some((0x06, oid)) = ir.tlv() else {
continue;
};
if oid == OID_CN
&& let Some((_, val)) = ir.tlv()
{
return Some(String::from_utf8_lossy(val).into_owned());
}
}
}
None
}
fn sans(ext3_der: &[u8]) -> Vec<String> {
let mut out = Vec::new();
let Some(exts_seq) = Der::new(ext3_der).seq() else {
return out;
};
let mut exts = Der::new(exts_seq);
while let Some((t, ext)) = exts.tlv() {
if t != 0x30 {
continue; }
let mut er = Der::new(ext);
let Some((0x06, oid)) = er.tlv() else {
continue;
};
if oid != OID_SAN {
continue;
}
let mut value = None;
while let Some((vt, v)) = er.tlv() {
if vt == 0x04 {
value = Some(v);
}
}
let Some(octets) = value else { continue };
let Some(gnames) = Der::new(octets).seq() else {
continue;
};
let mut gr = Der::new(gnames);
while let Some((gt, gv)) = gr.tlv() {
let entry = match gt {
0x81 | 0x82 | 0x86 => Some(String::from_utf8_lossy(gv).into_owned()),
0x87 => fmt_ip(gv), _ => None,
};
if let Some(e) = entry {
out.push(e);
if out.len() >= MAX_SANS {
return out;
}
}
}
}
out
}
fn fmt_ip(bytes: &[u8]) -> Option<String> {
match bytes.len() {
4 => Some(std::net::Ipv4Addr::new(bytes[0], bytes[1], bytes[2], bytes[3]).to_string()),
16 => {
let mut o = [0u8; 16];
o.copy_from_slice(bytes);
Some(std::net::Ipv6Addr::from(o).to_string())
}
_ => None,
}
}
struct Der<'a> {
b: &'a [u8],
}
impl<'a> Der<'a> {
fn new(b: &'a [u8]) -> Der<'a> {
Der { b }
}
fn tlv(&mut self) -> Option<(u8, &'a [u8])> {
let (&tag, rest) = self.b.split_first()?;
let (&l0, rest) = rest.split_first()?;
let (len, rest) = if l0 & 0x80 == 0 {
(l0 as usize, rest)
} else {
let n = (l0 & 0x7f) as usize;
if n == 0 || n > 4 || rest.len() < n {
return None; }
let mut len = 0usize;
for &byte in &rest[..n] {
len = (len << 8) | byte as usize;
}
(len, &rest[n..])
};
if rest.len() < len {
return None;
}
let (val, after) = rest.split_at(len);
self.b = after;
Some((tag, val))
}
fn seq(&mut self) -> Option<&'a [u8]> {
match self.tlv() {
Some((0x30, v)) => Some(v),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn pem_to_der(pem: &str) -> Vec<u8> {
let b64: String = pem
.lines()
.skip_while(|l| !l.contains("BEGIN CERTIFICATE"))
.skip(1)
.take_while(|l| !l.contains("END CERTIFICATE"))
.collect();
b64_decode(&b64)
}
fn b64_decode(s: &str) -> Vec<u8> {
const T: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let val = |c: u8| T.iter().position(|&t| t == c);
let mut out = Vec::new();
let mut buf = 0u32;
let mut bits = 0u32;
for &c in s.as_bytes() {
if c == b'=' || c.is_ascii_whitespace() {
continue;
}
let Some(v) = val(c) else { continue };
buf = (buf << 6) | v as u32;
bits += 6;
if bits >= 8 {
bits -= 8;
out.push((buf >> bits) as u8);
}
}
out
}
#[test]
fn extracts_subject_cn_from_a_real_cert() {
let der = pem_to_der(include_str!("../tests/fixtures/client.pem"));
let id = parse(&der);
assert_eq!(id.subject_cn.as_deref(), Some("agentctl-test"));
assert!(
id.sans.is_empty(),
"client fixture has no SAN: {:?}",
id.sans
);
}
#[test]
fn extracts_dns_and_ip_sans_from_a_real_cert() {
let der = pem_to_der(include_str!("../tests/fixtures/server.pem"));
let id = parse(&der);
assert_eq!(id.subject_cn.as_deref(), Some("localhost"));
assert!(id.sans.contains(&"localhost".to_string()), "{:?}", id.sans);
assert!(id.sans.contains(&"127.0.0.1".to_string()), "{:?}", id.sans);
assert!(id.sans.iter().any(|s| s == "::1"), "{:?}", id.sans);
}
#[test]
fn malformed_input_never_panics() {
assert_eq!(parse(&[]), PeerIdentity::default());
assert_eq!(parse(&[0x30]), PeerIdentity::default()); assert_eq!(parse(&[0x30, 0x80]), PeerIdentity::default()); assert_eq!(
parse(&[0x30, 0x84, 0xff, 0xff, 0xff, 0xff]),
PeerIdentity::default()
);
}
#[test]
fn parses_a_synthetic_spiffe_uri_san() {
let uri = b"spiffe://example.org/workload/api";
let mut gname = vec![0x86, uri.len() as u8];
gname.extend_from_slice(uri);
let mut gseq = vec![0x30, gname.len() as u8];
gseq.extend_from_slice(&gname);
let mut octet = vec![0x04, gseq.len() as u8];
octet.extend_from_slice(&gseq);
let mut ext = vec![0x06, 0x03, 0x55, 0x1D, 0x11];
ext.extend_from_slice(&octet);
let mut extseq = vec![0x30, ext.len() as u8];
extseq.extend_from_slice(&ext);
let mut exts_seq = vec![0x30, extseq.len() as u8];
exts_seq.extend_from_slice(&extseq);
assert_eq!(
sans(&exts_seq),
vec!["spiffe://example.org/workload/api".to_string()]
);
}
}