use alloc::rc::Rc;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct OidEntry {
pub nid: i32,
pub short_name: &'static str,
pub long_name: &'static str,
oid: Option<Box<str>>,
}
impl OidEntry {
#[must_use]
pub fn oid_string(&self) -> Option<&str> {
self.oid.as_deref()
}
}
pub struct OidTable {
entries: Vec<OidEntry>,
nid_to_idx: HashMap<i32, usize>,
short_name_to_idx: HashMap<&'static str, usize>,
long_name_to_idx: HashMap<&'static str, usize>,
oid_str_to_idx: HashMap<String, usize>,
}
impl OidTable {
fn build() -> Self {
let entries = build_oid_entries();
let mut nid_to_idx = HashMap::with_capacity(entries.len());
let mut short_name_to_idx = HashMap::with_capacity(entries.len());
let mut long_name_to_idx = HashMap::with_capacity(entries.len());
let mut oid_str_to_idx = HashMap::with_capacity(entries.len());
for (idx, entry) in entries.iter().enumerate() {
nid_to_idx.entry(entry.nid).or_insert(idx);
short_name_to_idx.entry(entry.short_name).or_insert(idx);
long_name_to_idx.entry(entry.long_name).or_insert(idx);
if let Some(oid) = entry.oid_string() {
oid_str_to_idx.entry(oid.to_owned()).or_insert(idx);
}
}
Self {
entries,
nid_to_idx,
short_name_to_idx,
long_name_to_idx,
oid_str_to_idx,
}
}
#[must_use]
pub fn find_by_nid(&self, nid: i32) -> Option<&OidEntry> {
self.nid_to_idx.get(&nid).map(|&idx| &self.entries[idx])
}
#[must_use]
pub fn find_by_oid_string(&self, oid_str: &str) -> Option<&OidEntry> {
self.oid_str_to_idx
.get(canonical_oid(oid_str)?.as_str())
.map(|&idx| &self.entries[idx])
}
#[must_use]
pub fn find_by_name(&self, name: &str) -> Option<&OidEntry> {
self.short_name_to_idx
.get(name)
.or_else(|| self.long_name_to_idx.get(name))
.map(|&idx| &self.entries[idx])
}
}
fn canonical_oid(oid: &str) -> Option<String> {
let oid = oid.trim_end_matches(' ');
let oid = oid.strip_suffix('.').unwrap_or(oid);
let mut parts = oid.split('.');
let first = parts.next()?;
if !matches!(first, "0" | "1" | "2") {
return None;
}
let mut canonical = first.to_owned();
let mut count = 1;
for part in parts {
if part.is_empty() || !part.bytes().all(|byte| byte.is_ascii_digit()) {
return None;
}
let part = part.trim_start_matches('0');
canonical.push('.');
canonical.push_str(if part.is_empty() { "0" } else { part });
count += 1;
}
(count >= 2).then_some(canonical)
}
static OID_TABLE: std::sync::LazyLock<OidTable> = std::sync::LazyLock::new(OidTable::build);
static OBJ_MAC_NUM: &str = include_str!("data/obj_mac.num");
static OBJECTS_TXT: &str = include_str!("data/objects.txt");
fn qualify(name: &str, module: Option<&str>) -> Rc<str> {
let name = match module {
Some(module) => format!("{module}_{name}"),
None => name.to_owned(),
};
name.replace('-', "_").into()
}
fn resolve_oid<'a>(
parts: impl Iterator<Item = &'a str>,
aliases: &HashMap<Rc<str>, Rc<[u64]>>,
) -> Rc<[u64]> {
let mut oid = Vec::with_capacity(16);
for part in parts {
match aliases.get(part.replace('-', "_").as_str()) {
Some(prefix) => oid.extend_from_slice(prefix),
None => oid.push(part.parse().expect("OID arc is not a number")),
}
}
oid.into()
}
fn dotted(oid: &[u64]) -> Box<str> {
oid.iter()
.map(u64::to_string)
.collect::<Vec<_>>()
.join(".")
.into_boxed_str()
}
fn build_oid_entries() -> Vec<OidEntry> {
let nids: HashMap<&str, i32> = OBJ_MAC_NUM
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.filter_map(|line| {
let mut field = line.split_whitespace();
let name = field.next()?;
let nid = field.next()?.parse().ok()?;
Some((name, nid))
})
.collect();
let mut entries = Vec::with_capacity(nids.len());
let mut aliases: HashMap<Rc<str>, Rc<[u64]>> = HashMap::new();
let mut cname: Option<&str> = None;
let mut module: Option<&str> = None;
for line in OBJECTS_TXT
.lines()
.map(str::trim)
.filter(|line| !line.is_empty() && !line.starts_with('#'))
{
if let Some(directive) = line.strip_prefix('!') {
let mut field = directive.split_whitespace();
match field.next() {
Some("Alias") => {
let alias = field.next().expect("!Alias without a name");
let oid = resolve_oid(field, &aliases);
aliases.insert(qualify(alias, module), oid);
}
Some("Cname") => cname = Some(field.next().expect("!Cname without a name")),
Some("module") => module = Some(field.next().expect("!module without a name")),
Some("global") => module = None,
other => panic!("unknown objects.txt directive {other:?}"),
}
continue;
}
let mut field = line.split(':').map(str::trim);
let oid = resolve_oid(
field
.next()
.expect("object without an OID")
.split_whitespace(),
&aliases,
);
let (short_name, long_name) = match (field.next().unwrap_or(""), field.next().unwrap_or(""))
{
("", "") => continue,
("", name) | (name, "") => (name, name),
(short_name, long_name) => (short_name, long_name),
};
let mut identifiers: Vec<Rc<str>> = Vec::with_capacity(3);
for name in [cname.take(), Some(long_name), Some(short_name)]
.into_iter()
.flatten()
.filter(|name| !name.is_empty())
{
let name = qualify(name, module);
if !identifiers.contains(&name) {
aliases.entry(name.clone()).or_insert_with(|| oid.clone());
identifiers.push(name);
}
}
let Some(nid) = identifiers
.iter()
.find_map(|name| nids.get(&**name).copied())
else {
continue;
};
entries.push(OidEntry {
nid,
short_name,
long_name,
oid: (oid.len() >= 2).then(|| dotted(&oid)),
});
}
assert!(cname.is_none(), "!Cname with no object after it");
entries
}
#[must_use]
pub fn find_by_nid(nid: i32) -> Option<&'static OidEntry> {
OID_TABLE.find_by_nid(nid)
}
#[must_use]
pub fn find_by_oid_string(oid_str: &str) -> Option<&'static OidEntry> {
OID_TABLE.find_by_oid_string(oid_str)
}
#[must_use]
pub fn find_by_name(name: &str) -> Option<&'static OidEntry> {
OID_TABLE.find_by_name(name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn find_by_nid_ok() {
let entry = find_by_nid(13).unwrap();
assert_eq!(entry.short_name, "CN");
assert_eq!(entry.long_name, "commonName");
assert_eq!(entry.oid_string(), Some("2.5.4.3"));
}
#[test]
fn find_by_oid_string_ok() {
let entry = find_by_oid_string("2.5.4.3").unwrap();
assert_eq!(entry.nid, 13);
assert_eq!(entry.short_name, "CN");
}
#[test]
fn find_by_name_short() {
let entry = find_by_name("CN").unwrap();
assert_eq!(entry.nid, 13);
assert_eq!(entry.oid_string(), Some("2.5.4.3"));
}
#[test]
fn find_by_name_long() {
let entry = find_by_name("commonName").unwrap();
assert_eq!(entry.nid, 13);
assert_eq!(entry.short_name, "CN");
}
#[test]
fn find_by_name_is_case_sensitive() {
assert!(find_by_name("commonName").is_some());
assert!(find_by_name("COMMONNAME").is_none());
}
#[test]
fn find_by_oid_string_normalizes_arcs() {
let entry = find_by_oid_string("2.005.004.003.").unwrap();
assert_eq!(entry.nid, 13);
assert_eq!(entry.oid_string(), Some("2.5.4.3"));
}
#[test]
fn subject_alt_name() {
let entry = find_by_nid(85).unwrap();
assert_eq!(entry.short_name, "subjectAltName");
assert_eq!(entry.oid_string(), Some("2.5.29.17"));
}
#[test]
fn server_auth_eku() {
let entry = find_by_nid(129).unwrap();
assert_eq!(entry.short_name, "serverAuth");
assert_eq!(entry.oid_string(), Some("1.3.6.1.5.5.7.3.1"));
}
#[test]
fn no_duplicate_nids() {
let table = &*OID_TABLE;
assert_eq!(
table.entries.len(),
table.nid_to_idx.len(),
"Duplicate NIDs detected!"
);
}
#[test]
fn oid_count() {
let table = &*OID_TABLE;
assert!(
table.entries.len() >= 1000,
"Expected at least 1000 OIDs, got {}",
table.entries.len()
);
}
#[test]
fn nids_agree_with_openssl() {
for (nid, short_name, long_name, oid) in [
(13, "CN", "commonName", Some("2.5.4.3")),
(
48,
"emailAddress",
"emailAddress",
Some("1.2.840.113549.1.9.1"),
),
(
85,
"subjectAltName",
"X509v3 Subject Alternative Name",
Some("2.5.29.17"),
),
(105, "serialNumber", "serialNumber", Some("2.5.4.5")),
(
129,
"serverAuth",
"TLS Web Server Authentication",
Some("1.3.6.1.5.5.7.3.1"),
),
(660, "street", "streetAddress", Some("2.5.4.9")),
(11, "X500", "directory services (X.500)", Some("2.5")),
(181, "ISO", "iso", None),
(33, "DES-EDE3", "des-ede3", None),
(114, "MD5-SHA1", "md5-sha1", None),
(405, "ansi-X9-62", "ANSI X9.62", Some("1.2.840.10045")),
(
983,
"md_gost12_512",
"GOST R 34.11-2012 with 512 bit hash",
Some("1.2.643.7.1.1.2.3"),
),
] {
let entry = find_by_nid(nid).unwrap_or_else(|| panic!("no entry for NID {nid}"));
assert_eq!(entry.short_name, short_name, "NID {nid}");
assert_eq!(entry.long_name, long_name, "NID {nid}");
assert_eq!(entry.oid_string(), oid, "NID {nid}");
}
}
}