use bytes::Bytes;
use sha2::{Digest, Sha256};
use x509_parser::prelude::{FromDer, X509Certificate};
pub fn ja4x_for_der(der: &[u8]) -> Option<String> {
let (_remaining, cert) = X509Certificate::from_der(der).ok()?;
Some(format!(
"{}_{}_{}",
sha256_prefix_12(&dn_oids_joined(&cert, DnSelector::Issuer)),
sha256_prefix_12(&dn_oids_joined(&cert, DnSelector::Subject)),
sha256_prefix_12(&extension_oids_joined(&cert)),
))
}
pub fn ja4x_for_chain(chain: &[Bytes]) -> Vec<Option<String>> {
chain.iter().map(|b| ja4x_for_der(b)).collect()
}
#[derive(Clone, Copy)]
enum DnSelector {
Issuer,
Subject,
}
fn dn_oids_joined(cert: &X509Certificate<'_>, which: DnSelector) -> String {
let name = match which {
DnSelector::Issuer => cert.issuer(),
DnSelector::Subject => cert.subject(),
};
let mut out = String::new();
let mut first = true;
for rdn in name.iter() {
for atv in rdn.iter() {
if !first {
out.push(',');
}
first = false;
out.push_str(&atv.attr_type().to_id_string());
}
}
out
}
fn extension_oids_joined(cert: &X509Certificate<'_>) -> String {
let mut out = String::new();
let mut first = true;
for ext in cert.extensions() {
if !first {
out.push(',');
}
first = false;
out.push_str(&ext.oid.to_id_string());
}
out
}
fn sha256_prefix_12(input: &str) -> String {
let mut h = Sha256::new();
h.update(input.as_bytes());
let digest = h.finalize();
let mut out = String::with_capacity(12);
for b in &digest[..6] {
use std::fmt::Write;
let _ = write!(out, "{:02x}", b);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE_DER: &[u8] = include_bytes!("../../tests/fixtures/x509/sample.der");
#[test]
fn ja4x_decodes_sample_cert() {
let fp = ja4x_for_der(SAMPLE_DER).expect("parse");
let parts: Vec<_> = fp.split('_').collect();
assert_eq!(parts.len(), 3);
for p in &parts {
assert_eq!(p.len(), 12);
assert!(p.chars().all(|c| c.is_ascii_hexdigit()));
}
}
#[test]
fn ja4x_for_chain_handles_empty() {
let chain: Vec<Bytes> = Vec::new();
let result = ja4x_for_chain(&chain);
assert!(result.is_empty());
}
#[test]
fn ja4x_for_chain_handles_invalid_der() {
let bogus: Vec<Bytes> = vec![Bytes::from_static(b"not-a-cert")];
let result = ja4x_for_chain(&bogus);
assert_eq!(result.len(), 1);
assert!(result[0].is_none());
}
#[test]
fn sha256_prefix_12_known_value() {
assert_eq!(sha256_prefix_12(""), "e3b0c44298fc");
}
}