use sha2::{Digest, Sha256};
pub fn fingerprint_from_cert_der(cert_der: &[u8]) -> Option<String> {
if let Some(raw_key) = extract_ed25519_raw_key_from_spki(cert_der) {
return Some(format!("ed25519:{}", hex::encode(raw_key)));
}
let mut hasher = Sha256::new();
hasher.update(cert_der);
let digest = hasher.finalize();
Some(format!("SHA256:{}", hex::encode(digest)))
}
pub fn extract_ed25519_raw_key_from_spki(cert_der: &[u8]) -> Option<[u8; 32]> {
const ED25519_OID_BYTES: [u8; 3] = [0x2b, 0x65, 0x70];
let mut parser = DerParser::new(cert_der);
let spki_contents = parser.expect_sequence()?;
let mut spki_parser = DerParser::new(spki_contents);
let alg_id_contents = spki_parser.expect_sequence()?;
let mut alg_id_parser = DerParser::new(alg_id_contents);
let oid_bytes = alg_id_parser.expect_oid()?;
if oid_bytes != ED25519_OID_BYTES {
return None;
}
let bit_string_contents = spki_parser.expect_bit_string()?;
if bit_string_contents.len() != 33 || bit_string_contents[0] != 0x00 {
return None;
}
let mut raw_key = [0u8; 32];
raw_key.copy_from_slice(&bit_string_contents[1..33]);
Some(raw_key)
}
struct DerParser<'a> {
bytes: &'a [u8],
}
impl<'a> DerParser<'a> {
fn new(bytes: &'a [u8]) -> Self {
Self { bytes }
}
fn read_tlv(&mut self) -> Option<(u8, &'a [u8])> {
let (tag, len_size, header_len) = self.decode_header()?;
let total = header_len.checked_add(len_size)?;
if total > self.bytes.len() {
return None;
}
let content = &self.bytes[header_len..total];
self.bytes = &self.bytes[total..];
Some((tag, content))
}
fn decode_header(&self) -> Option<(u8, usize, usize)> {
if self.bytes.is_empty() {
return None;
}
let tag = self.bytes[0];
if self.bytes.len() < 2 {
return None;
}
let first_len = self.bytes[1];
if first_len < 0x80 {
return Some((tag, first_len as usize, 2));
}
let num_bytes = (first_len & 0x7f) as usize;
if num_bytes == 0 || num_bytes > 4 {
return None;
}
if self.bytes.len() < 2 + num_bytes {
return None;
}
let mut len: usize = 0;
for i in 0..num_bytes {
len = (len << 8) | (self.bytes[2 + i] as usize);
}
Some((tag, len, 2 + num_bytes))
}
fn expect_sequence(&mut self) -> Option<&'a [u8]> {
let (tag, content) = self.read_tlv()?;
if tag == 0x30 {
Some(content)
} else {
None
}
}
fn expect_oid(&mut self) -> Option<&'a [u8]> {
let (tag, content) = self.read_tlv()?;
if tag == 0x06 {
Some(content)
} else {
None
}
}
fn expect_bit_string(&mut self) -> Option<&'a [u8]> {
let (tag, content) = self.read_tlv()?;
if tag == 0x03 {
Some(content)
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const RAW_KEY_A: [u8; 32] = [
0x9d, 0x61, 0xb1, 0x9d, 0xef, 0xf5, 0x5a, 0x60, 0xba, 0x84, 0x4a, 0xf4, 0x52, 0x7a, 0xbd,
0xd7, 0x75, 0x62, 0x8d, 0x87, 0x46, 0x35, 0xc5, 0xac, 0x2d, 0xce, 0x9a, 0x0b, 0x5f, 0x06,
0x4b, 0x4b,
];
const RAW_KEY_B: [u8; 32] = [
0x4c, 0xcd, 0x08, 0x9b, 0x28, 0xff, 0x9d, 0xba, 0xe4, 0x62, 0x5b, 0x20, 0x4d, 0x14, 0x94,
0x9d, 0xa5, 0x91, 0x59, 0x6b, 0x10, 0x46, 0xd1, 0x55, 0x6d, 0x63, 0x81, 0x0b, 0xf7, 0xe9,
0x8e, 0x71,
];
fn build_ed25519_spki_der(raw_key: &[u8; 32]) -> Vec<u8> {
rustls::sign::public_key_to_spki(&rustls::pki_types::alg_id::ED25519, raw_key).to_vec()
}
#[test]
fn fingerprint_from_cert_der_produces_sha256_hex_format() {
let cert_der = b"fake-leaf-cert-der-bytes";
let fp = fingerprint_from_cert_der(cert_der).expect("non-empty cert produces fingerprint");
assert!(
fp.starts_with("SHA256:"),
"fingerprint must be SHA256-prefixed, got: {fp}"
);
let hex_part = &fp["SHA256:".len()..];
assert_eq!(
hex_part.len(),
64,
"hex digest must be 64 chars (32 bytes), got: {fp}"
);
assert!(
hex_part.chars().all(|c| c.is_ascii_hexdigit()),
"hex part must be lowercase hex, got: {fp}"
);
let mut hasher = Sha256::new();
hasher.update(cert_der);
let expected = format!("SHA256:{}", hex::encode(hasher.finalize()));
assert_eq!(fp, expected, "fingerprint must match SHA-256 of cert DER");
}
#[test]
fn fingerprint_from_cert_der_deterministic() {
let cert = b"some-cert";
let a = fingerprint_from_cert_der(cert).unwrap();
let b = fingerprint_from_cert_der(cert).unwrap();
assert_eq!(a, b, "same cert DER must produce same fingerprint");
}
#[test]
fn fingerprint_from_ed25519_spki_produces_ed25519_prefix() {
let raw_key = RAW_KEY_A;
let spki_der = build_ed25519_spki_der(&raw_key);
let fp = fingerprint_from_cert_der(&spki_der).expect("spki produces fingerprint");
assert!(
fp.starts_with("ed25519:"),
"Ed25519 raw key SPKI must produce ed25519: fingerprint, got: {fp}"
);
}
#[test]
fn fingerprint_from_ed25519_spki_is_lowercase_hex_of_32_byte_key() {
let raw_key = RAW_KEY_A;
let spki_der = build_ed25519_spki_der(&raw_key);
let fp = fingerprint_from_cert_der(&spki_der).expect("spki produces fingerprint");
let hex_part = &fp["ed25519:".len()..];
assert_eq!(
hex_part.len(),
64,
"ed25519 hex part must be 64 chars (32 bytes), got: {fp}"
);
assert!(
hex_part
.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()),
"ed25519 hex part must be lowercase hex, got: {fp}"
);
assert_eq!(
hex_part,
hex::encode(raw_key),
"ed25519 fingerprint must be hex of the raw 32-byte key, not the DER wrapper"
);
}
#[test]
fn fingerprint_from_ed25519_spki_matches_iroh_format() {
let raw_key = RAW_KEY_B;
let spki_der = build_ed25519_spki_der(&raw_key);
let quinn_fp = fingerprint_from_cert_der(&spki_der).expect("spki produces fingerprint");
let iroh_fp = format!("ed25519:{}", hex::encode(raw_key));
assert_eq!(
quinn_fp, iroh_fp,
"same Ed25519 key must produce the same fingerprint via quinn SPKI and iroh NodeId paths"
);
}
#[test]
fn extract_ed25519_raw_key_from_spki_returns_source_key_bytes() {
let raw_key = RAW_KEY_B;
let spki_der = build_ed25519_spki_der(&raw_key);
let extracted =
extract_ed25519_raw_key_from_spki(&spki_der).expect("valid Ed25519 SPKI extracts key");
assert_eq!(
extracted, raw_key,
"extracted raw key must equal the source 32-byte key"
);
}
#[test]
fn fingerprint_from_x509_cert_stays_sha256_of_der() {
let cert_der = b"fake-x509-cert-der-bytes-not-an-spki";
let fp = fingerprint_from_cert_der(cert_der).expect("x509 produces fingerprint");
assert!(
fp.starts_with("SHA256:"),
"X.509 cert must keep SHA256: format, got: {fp}"
);
let mut hasher = Sha256::new();
hasher.update(cert_der);
assert_eq!(
fp,
format!("SHA256:{}", hex::encode(hasher.finalize())),
"X.509 fingerprint must be SHA-256 of cert DER"
);
assert_eq!(
extract_ed25519_raw_key_from_spki(cert_der),
None,
"X.509 cert must not extract an Ed25519 raw key"
);
}
#[test]
fn fingerprint_from_non_ed25519_spki_falls_back_to_sha256() {
let raw_key = [0u8; 32];
let fake_non_ed25519_spki: Vec<u8> = vec![
0x30, 0x1c, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x06, 0x01, 0x03, 0x15, 0x00, 0x20,
]
.into_iter()
.chain(raw_key.iter().copied())
.collect();
let fp = fingerprint_from_cert_der(&fake_non_ed25519_spki).expect("fallback fingerprint");
assert!(
fp.starts_with("SHA256:"),
"non-Ed25519 SPKI must fall back to SHA256, got: {fp}"
);
assert_eq!(
extract_ed25519_raw_key_from_spki(&fake_non_ed25519_spki),
None,
"non-Ed25519 SPKI must not extract an Ed25519 raw key"
);
}
#[test]
fn empty_input_hashes_to_sha256_of_empty_and_extracts_nothing() {
assert_eq!(
extract_ed25519_raw_key_from_spki(&[]),
None,
"empty input must not extract a raw key"
);
let fp = fingerprint_from_cert_der(&[]).expect("empty input still hashes");
let mut hasher = Sha256::new();
hasher.update([]);
let expected = format!("SHA256:{}", hex::encode(hasher.finalize()));
assert_eq!(
fp, expected,
"empty input must fall back to the SHA-256 of the empty slice"
);
}
#[test]
fn malformed_der_truncated_headers_extract_nothing() {
assert_eq!(extract_ed25519_raw_key_from_spki(&[]), None);
assert_eq!(extract_ed25519_raw_key_from_spki(&[0x30]), None);
assert_eq!(extract_ed25519_raw_key_from_spki(&[0x30, 0x10]), None);
assert_eq!(extract_ed25519_raw_key_from_spki(&[0x30, 0x10, 0x01]), None);
assert_eq!(
extract_ed25519_raw_key_from_spki(&[0x30, 0x10, 0x30, 0x05, 0x06, 0x03]),
None,
"alg-id SEQUENCE content shorter than its declared length must fail"
);
}
#[test]
fn malformed_der_wrong_outer_tag_extracts_nothing() {
let inner = build_ed25519_spki_der(&RAW_KEY_A);
let mut not_a_sequence = vec![0x04u8];
not_a_sequence.extend_from_slice(&inner);
assert_eq!(
extract_ed25519_raw_key_from_spki(¬_a_sequence),
None,
"outer tag must be SEQUENCE (0x30)"
);
}
#[test]
fn malformed_der_long_form_lengths_extract_nothing() {
assert_eq!(
extract_ed25519_raw_key_from_spki(&[0x30, 0x80, 0x01, 0x02]),
None,
"0x80 (indefinite length, zero length octets) must be rejected"
);
assert_eq!(
extract_ed25519_raw_key_from_spki(&[0x30, 0x81, 0x05, 0x01]),
None,
"long-form length exceeding remaining bytes must be rejected"
);
assert_eq!(
extract_ed25519_raw_key_from_spki(&[0x30, 0x85, 0x01, 0x02, 0x03, 0x04, 0x05]),
None,
"more than 4 length bytes must be rejected"
);
assert_eq!(
extract_ed25519_raw_key_from_spki(&[0x30, 0x81]),
None,
"truncated long-form header must be rejected"
);
}
#[test]
fn well_formed_long_form_length_spki_extracts_the_key() {
let raw_key = RAW_KEY_A;
let inner = build_ed25519_spki_der(&raw_key);
let mut spki_der = vec![0x30u8, 0x81, 0x80];
spki_der.extend_from_slice(&inner[2..]);
spki_der.resize(3 + 128, 0x00);
assert_eq!(
extract_ed25519_raw_key_from_spki(&spki_der),
Some(raw_key),
"long-form outer length must parse and extract the key"
);
}
#[test]
fn wrong_oid_in_valid_spki_extracts_nothing() {
let raw_key = [0u8; 32];
let wrong_oid_spki: Vec<u8> = vec![
0x30, 0x2d, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x06, 0x01, 0x03, 0x21, 0x00,
]
.into_iter()
.chain(raw_key.iter().copied())
.collect();
assert_eq!(
extract_ed25519_raw_key_from_spki(&wrong_oid_spki),
None,
"non-Ed25519 OID (1.3.6.1) with well-formed BIT STRING must be rejected"
);
}
#[test]
fn bad_bit_string_lengths_extract_nothing() {
let alg_id: Vec<u8> = vec![0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70];
let bit_string_32: Vec<u8> = vec![0x03, 0x20, 0x00]
.into_iter()
.chain([0u8; 31].iter().copied())
.collect();
let mut spki = vec![0x30u8, 0x29];
spki.extend_from_slice(&alg_id);
spki.extend_from_slice(&bit_string_32);
assert_eq!(
extract_ed25519_raw_key_from_spki(&spki),
None,
"BIT STRING content of 32 bytes (missing unused-bits byte) must be rejected"
);
let bit_string_34: Vec<u8> = vec![0x03, 0x22, 0x00]
.into_iter()
.chain([0u8; 33].iter().copied())
.collect();
let mut spki = vec![0x30u8, 0x2b];
spki.extend_from_slice(&alg_id);
spki.extend_from_slice(&bit_string_34);
assert_eq!(
extract_ed25519_raw_key_from_spki(&spki),
None,
"BIT STRING content of 34 bytes must be rejected"
);
let bit_string_unused_bits: Vec<u8> = vec![0x03, 0x21, 0x01]
.into_iter()
.chain([0u8; 32].iter().copied())
.collect();
let mut spki = vec![0x30u8, 0x2a];
spki.extend_from_slice(&alg_id);
spki.extend_from_slice(&bit_string_unused_bits);
assert_eq!(
extract_ed25519_raw_key_from_spki(&spki),
None,
"non-zero unused-bits byte must be rejected"
);
}
#[test]
fn wrong_tag_before_oid_and_missing_bit_string_extract_nothing() {
let non_oid_alg_id: Vec<u8> = vec![0x30, 0x05, 0x04, 0x03, 0x2b, 0x65, 0x70];
let bit_string: Vec<u8> = vec![0x03, 0x21, 0x00]
.into_iter()
.chain([0u8; 32].iter().copied())
.collect();
let mut spki = vec![0x30u8, 0x2a];
spki.extend_from_slice(&non_oid_alg_id);
spki.extend_from_slice(&bit_string);
assert_eq!(
extract_ed25519_raw_key_from_spki(&spki),
None,
"a non-OID tag (0x04) inside AlgorithmIdentifier must be rejected"
);
let alg_id: Vec<u8> = vec![0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70];
let mut spki = vec![0x30u8, 0x07];
spki.extend_from_slice(&alg_id);
assert_eq!(
extract_ed25519_raw_key_from_spki(&spki),
None,
"a well-formed SPKI with no BIT STRING after the alg-id must be rejected"
);
}
#[test]
fn malformed_der_still_falls_back_to_sha256_fingerprint() {
let malformed = [0x30u8, 0x81, 0x05, 0x01];
let fp =
fingerprint_from_cert_der(&malformed).expect("malformed DER still hashes to SHA256");
let mut hasher = Sha256::new();
hasher.update(malformed);
assert_eq!(
fp,
format!("SHA256:{}", hex::encode(hasher.finalize())),
"malformed DER must fall back to SHA-256 of the full input"
);
}
}