pub const ENCTYPE_AES256_CTS_HMAC_SHA1_96: u16 = 18;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KeytabKey {
pub components: Vec<String>,
pub realm: String,
pub enctype: u16,
pub kvno: u32,
pub key: Vec<u8>,
}
#[derive(Debug, thiserror::Error)]
pub enum KeytabError {
#[error("unexpected keytab magic 0x{0:04x}, expected 0x0502")]
BadMagic(u16),
#[error("keytab truncated: needed {needed} more bytes at offset {offset}")]
Truncated {
offset: usize,
needed: usize,
},
#[error("no key for service '{service}' with enctype {enctype} found in keytab")]
NotFound {
service: String,
enctype: u16,
},
}
struct Reader<'a> {
bytes: &'a [u8],
pos: usize,
}
impl<'a> Reader<'a> {
fn new(bytes: &'a [u8]) -> Self {
Self { bytes, pos: 0 }
}
fn remaining(&self) -> usize {
self.bytes.len().saturating_sub(self.pos)
}
fn at_end(&self) -> bool {
self.pos >= self.bytes.len()
}
fn take(&mut self, n: usize) -> Result<&'a [u8], KeytabError> {
if self.remaining() < n {
return Err(KeytabError::Truncated {
offset: self.pos,
needed: n,
});
}
let slice = &self.bytes[self.pos..self.pos + n];
self.pos += n;
Ok(slice)
}
fn u8(&mut self) -> Result<u8, KeytabError> {
Ok(self.take(1)?[0])
}
fn u16(&mut self) -> Result<u16, KeytabError> {
let b = self.take(2)?;
Ok(u16::from_be_bytes([b[0], b[1]]))
}
fn i32(&mut self) -> Result<i32, KeytabError> {
let b = self.take(4)?;
Ok(i32::from_be_bytes([b[0], b[1], b[2], b[3]]))
}
fn u32(&mut self) -> Result<u32, KeytabError> {
let b = self.take(4)?;
Ok(u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
}
fn counted_str(&mut self) -> Result<String, KeytabError> {
let len = self.u16()? as usize;
let bytes = self.take(len)?;
Ok(String::from_utf8_lossy(bytes).into_owned())
}
}
pub fn parse_keytab(bytes: &[u8]) -> Result<Vec<KeytabKey>, KeytabError> {
let mut reader = Reader::new(bytes);
let magic = reader.u16()?;
if magic != 0x0502 {
return Err(KeytabError::BadMagic(magic));
}
let mut entries = Vec::new();
while !reader.at_end() {
let entry_size = match reader.i32() {
Ok(size) => size,
Err(KeytabError::Truncated { .. }) => break,
Err(e) => return Err(e),
};
if entry_size <= 0 {
let hole = entry_size.unsigned_abs() as usize;
reader.take(hole)?;
continue;
}
let entry_size = usize::try_from(entry_size).expect("entry_size is positive");
let entry_start = reader.pos;
let entry_end = entry_start + entry_size;
if entry_end > bytes.len() {
return Err(KeytabError::Truncated {
offset: entry_start,
needed: entry_size,
});
}
let num_components = reader.u16()? as usize;
let realm = reader.counted_str()?;
let mut components = Vec::with_capacity(num_components);
for _ in 0..num_components {
components.push(reader.counted_str()?);
}
let _name_type = reader.u32()?;
let _timestamp = reader.u32()?;
let kvno8 = u32::from(reader.u8()?);
let enctype = reader.u16()?;
let key_len = reader.u16()? as usize;
let key = reader.take(key_len)?.to_vec();
let kvno = if entry_end.saturating_sub(reader.pos) >= 4 {
reader.u32()?
} else {
kvno8
};
reader.pos = entry_end;
entries.push(KeytabKey {
components,
realm,
enctype,
kvno,
key,
});
}
Ok(entries)
}
pub fn load_service_key(
keytab_bytes: &[u8],
service_name: &str,
enctype: u16,
) -> Result<KeytabKey, KeytabError> {
let entries = parse_keytab(keytab_bytes)?;
entries
.into_iter()
.filter(|e| e.enctype == enctype && e.components.first().is_some_and(|c| c == service_name))
.max_by_key(|e| e.kvno)
.ok_or_else(|| KeytabError::NotFound {
service: service_name.to_owned(),
enctype,
})
}
pub fn load_service_keys(
keytab_bytes: &[u8],
service_name: &str,
enctype: u16,
) -> Result<Vec<KeytabKey>, KeytabError> {
let entries = parse_keytab(keytab_bytes)?;
let mut best: std::collections::BTreeMap<Vec<String>, KeytabKey> =
std::collections::BTreeMap::new();
for e in entries {
if e.enctype != enctype || e.components.first().is_none_or(|c| c != service_name) {
continue;
}
match best.get(&e.components) {
Some(existing) if existing.kvno >= e.kvno => {}
_ => {
best.insert(e.components.clone(), e);
}
}
}
Ok(best.into_values().collect())
}
#[cfg(test)]
mod tests {
use super::*;
use assert2::assert;
struct EntrySpec<'a> {
components: &'a [&'a str],
realm: &'a str,
kvno8: u8,
enctype: u16,
key: &'a [u8],
kvno32: Option<u32>,
}
fn entry_body(spec: &EntrySpec<'_>) -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(&(u16::try_from(spec.components.len()).unwrap()).to_be_bytes());
b.extend_from_slice(&(u16::try_from(spec.realm.len()).unwrap()).to_be_bytes());
b.extend_from_slice(spec.realm.as_bytes());
for c in spec.components {
b.extend_from_slice(&(u16::try_from(c.len()).unwrap()).to_be_bytes());
b.extend_from_slice(c.as_bytes());
}
b.extend_from_slice(&1u32.to_be_bytes()); b.extend_from_slice(&0x1234_5678u32.to_be_bytes()); b.push(spec.kvno8);
b.extend_from_slice(&spec.enctype.to_be_bytes());
b.extend_from_slice(&(u16::try_from(spec.key.len()).unwrap()).to_be_bytes());
b.extend_from_slice(spec.key);
if let Some(kvno32) = spec.kvno32 {
b.extend_from_slice(&kvno32.to_be_bytes());
}
b
}
fn keytab(entries: &[Vec<u8>]) -> Vec<u8> {
let mut out = vec![0x05, 0x02];
for e in entries {
out.extend_from_slice(&(i32::try_from(e.len()).unwrap()).to_be_bytes());
out.extend_from_slice(e);
}
out
}
#[test]
fn parses_single_aes256_entry() {
let key: Vec<u8> = (0u8..32).collect();
let body = entry_body(&EntrySpec {
components: &["kafka", "localhost"],
realm: "CRABKA.TEST",
kvno8: 1,
enctype: ENCTYPE_AES256_CTS_HMAC_SHA1_96,
key: &key,
kvno32: Some(1),
});
let kt = keytab(&[body]);
let entries = parse_keytab(&kt).expect("parse");
assert!(entries.len() == 1);
let e = &entries[0];
assert!(e.components == vec!["kafka", "localhost"]);
assert!(e.realm == "CRABKA.TEST");
assert!(e.enctype == ENCTYPE_AES256_CTS_HMAC_SHA1_96);
assert!(e.kvno == 1);
assert!(e.key == key);
let found = load_service_key(&kt, "kafka", ENCTYPE_AES256_CTS_HMAC_SHA1_96).expect("found");
assert!(found.key == key);
}
#[test]
fn highest_kvno_and_enctype_filter_win() {
let key_v1 = vec![0xAAu8; 32];
let key_v5 = vec![0xBBu8; 32];
let key_aes128 = vec![0xCCu8; 16];
let kt = keytab(&[
entry_body(&EntrySpec {
components: &["kafka", "host"],
realm: "R",
kvno8: 1,
enctype: ENCTYPE_AES256_CTS_HMAC_SHA1_96,
key: &key_v1,
kvno32: None,
}),
entry_body(&EntrySpec {
components: &["kafka", "host"],
realm: "R",
kvno8: 17,
enctype: 17,
key: &key_aes128,
kvno32: None,
}),
entry_body(&EntrySpec {
components: &["kafka", "host"],
realm: "R",
kvno8: 5,
enctype: ENCTYPE_AES256_CTS_HMAC_SHA1_96,
key: &key_v5,
kvno32: None,
}),
]);
let found = load_service_key(&kt, "kafka", ENCTYPE_AES256_CTS_HMAC_SHA1_96).expect("found");
assert!(found.kvno == 5);
assert!(found.key == key_v5);
let err = load_service_key(&kt, "http", ENCTYPE_AES256_CTS_HMAC_SHA1_96).unwrap_err();
assert!(matches!(err, KeytabError::NotFound { .. }));
}
#[test]
fn load_service_keys_dedups_per_spn_and_filters_enctype() {
let loc_v1 = vec![0x11u8; 32];
let loc_v3 = vec![0x33u8; 32];
let dock = vec![0x44u8; 32];
let loc_aes128 = vec![0x99u8; 16];
let http = vec![0x77u8; 32];
let kt = keytab(&[
entry_body(&EntrySpec {
components: &["kafka", "localhost"],
realm: "R",
kvno8: 1,
enctype: ENCTYPE_AES256_CTS_HMAC_SHA1_96,
key: &loc_v1,
kvno32: None,
}),
entry_body(&EntrySpec {
components: &["kafka", "localhost"],
realm: "R",
kvno8: 3,
enctype: ENCTYPE_AES256_CTS_HMAC_SHA1_96,
key: &loc_v3,
kvno32: None,
}),
entry_body(&EntrySpec {
components: &["kafka", "localhost"],
realm: "R",
kvno8: 9,
enctype: 17,
key: &loc_aes128,
kvno32: None,
}),
entry_body(&EntrySpec {
components: &["kafka", "host.docker.internal"],
realm: "R",
kvno8: 1,
enctype: ENCTYPE_AES256_CTS_HMAC_SHA1_96,
key: &dock,
kvno32: None,
}),
entry_body(&EntrySpec {
components: &["http", "localhost"],
realm: "R",
kvno8: 1,
enctype: ENCTYPE_AES256_CTS_HMAC_SHA1_96,
key: &http,
kvno32: None,
}),
]);
let keys = load_service_keys(&kt, "kafka", ENCTYPE_AES256_CTS_HMAC_SHA1_96).expect("load");
assert!(keys.len() == 2);
assert!(keys[0].components == vec!["kafka", "host.docker.internal"]);
assert!(keys[0].key == dock);
assert!(keys[1].components == vec!["kafka", "localhost"]);
assert!(keys[1].kvno == 3);
assert!(keys[1].key == loc_v3);
}
#[test]
fn load_service_keys_empty_when_no_match() {
let key = vec![0x42u8; 32];
let kt = keytab(&[entry_body(&EntrySpec {
components: &["kafka", "localhost"],
realm: "R",
kvno8: 1,
enctype: ENCTYPE_AES256_CTS_HMAC_SHA1_96,
key: &key,
kvno32: None,
})]);
let keys = load_service_keys(&kt, "http", ENCTYPE_AES256_CTS_HMAC_SHA1_96).expect("load");
assert!(keys.is_empty());
}
#[test]
fn holes_are_skipped() {
let key = vec![0x42u8; 32];
let good = entry_body(&EntrySpec {
components: &["kafka", "host"],
realm: "R",
kvno8: 1,
enctype: ENCTYPE_AES256_CTS_HMAC_SHA1_96,
key: &key,
kvno32: None,
});
let mut kt = vec![0x05, 0x02];
kt.extend_from_slice(&(-4i32).to_be_bytes()); kt.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]); kt.extend_from_slice(&(i32::try_from(good.len()).unwrap()).to_be_bytes());
kt.extend_from_slice(&good);
let entries = parse_keytab(&kt).expect("parse");
assert!(entries.len() == 1, "hole must not produce an entry");
assert!(entries[0].key == key);
}
#[test]
fn bad_magic_rejected() {
let err = parse_keytab(&[0x05, 0x01, 0x00, 0x00]).unwrap_err();
assert!(matches!(err, KeytabError::BadMagic(0x0501)));
}
#[test]
fn truncated_entry_rejected() {
let key = vec![0x00u8; 32];
let body = entry_body(&EntrySpec {
components: &["kafka", "host"],
realm: "R",
kvno8: 1,
enctype: ENCTYPE_AES256_CTS_HMAC_SHA1_96,
key: &key,
kvno32: None,
});
let mut kt = keytab(&[body]);
kt.truncate(kt.len() - 10); assert!(matches!(
parse_keytab(&kt),
Err(KeytabError::Truncated { .. })
));
}
}