use super::CredentialCache;
use crate::ccache::{CredentialV4, PrincipalV4};
use crate::error::KrbError;
use crate::proto::{EncTicket, KdcReplyPart, KerberosCredentials, Name};
use binrw::{binread, binwrite};
use binrw::{BinReaderExt, BinWrite};
use errno::Errno;
use keyutils::keytypes::user::User;
use keyutils::SpecialKeyring;
use keyutils::{Key, Keyring};
use keyutils_raw::{keyctl_get_keyring_id, keyctl_get_persistent};
use rand::{distr::Alphanumeric, Rng};
use std::time::Duration;
use tracing::{debug, error};
impl From<errno::Errno> for KrbError {
fn from(value: errno::Errno) -> Self {
error!(errno = ?value, "kernel keyring error");
KrbError::KeyutilsError
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
struct Residual {
anchor: String,
collection: String,
subsidiary: Option<String>,
}
impl Residual {
fn parse(residual: &str) -> Result<Self, KrbError> {
if !residual.starts_with("KEYRING:") {
return Err(KrbError::UnsupportedCredentialCacheType);
}
let residual = residual
.strip_prefix("KEYRING:")
.ok_or(KrbError::UnsupportedCredentialCacheType)?;
let (anchor, suffix) = residual
.split_once(":")
.ok_or(KrbError::UnsupportedCredentialCacheType)?;
if anchor.is_empty() {
return Err(KrbError::UnsupportedCredentialCacheType);
}
let (collection, suffix) = suffix.split_once(":").unwrap_or((suffix, ""));
if collection.is_empty() {
return Err(KrbError::UnsupportedCredentialCacheType);
}
let subsidiary: Option<String> = match suffix.split_once(":") {
Some((subsidiary, _)) => Some(subsidiary.to_string()),
None => {
if !suffix.is_empty() {
Some(suffix.to_string())
} else {
None
}
}
};
Ok(Residual {
anchor: anchor.to_string(),
collection: collection.to_string(),
subsidiary,
})
}
}
#[binwrite]
#[bw(big, magic = 1u32)]
#[binread]
#[br(magic = 1u32)]
struct PrimaryName {
#[bw(calc=strval.len() as u32)]
#[br(temp)]
strlen: u32,
#[br(count=strlen)]
strval: Vec<u8>,
}
#[binwrite]
#[bw(big, magic = 1u8)]
#[binread]
#[br(magic = 1u8)]
#[derive(Debug)]
struct TimeOffsets {
secs: i32,
usecs: i32,
}
fn get_subsidiary_principal(keyring: &Keyring) -> Result<Option<Name>, KrbError> {
let key_name = "__krb5_princ__";
match keyring.search_for_key::<User, &str, Option<&mut Keyring>>(key_name, None) {
Ok(k) => {
let payload = k.read()?;
let mut reader = binrw::io::Cursor::new(payload);
let name: PrincipalV4 = reader.read_type(binrw::Endian::Big).map_err(|err| {
error!(error=?err);
KrbError::BinRWError
})?;
let name: Name = name.try_into()?;
Ok(Some(name))
}
Err(errno::Errno(libc::ENOKEY)) => Ok(None),
Err(e) => Err(KrbError::from(e)),
}
}
fn subsidiary_exists(collection: &Keyring, name: &str) -> Result<Option<Keyring>, KrbError> {
match collection.search_for_keyring(name, None) {
Ok(k) => Ok(Some(k)),
Err(errno::Errno(libc::ENOKEY)) => Ok(None),
Err(e) => Err(KrbError::from(e)),
}
}
fn get_random_subsidiary_name(collection: &mut Keyring) -> Result<String, KrbError> {
for _ in 1..10 {
let s: String = rand::rng()
.sample_iter(&Alphanumeric)
.take(7)
.map(char::from)
.collect();
let s = format!("_krb_{s}");
let k = subsidiary_exists(collection, s.as_str())?;
if k.is_none() {
return Ok(s);
}
}
error!(collection=?collection, "Failed to generate random cache name");
Err(KrbError::CredentialCacheError)
}
fn get_subsidiary_cache_name(residual: &Residual) -> String {
residual
.subsidiary
.clone()
.unwrap_or(residual.collection.clone())
}
fn get_subsidiary_cache(
name: &Name,
collection: &mut Keyring,
residual: &Residual,
) -> Result<Keyring, KrbError> {
let subsidiary_name = get_subsidiary_cache_name(residual);
match residual.subsidiary.as_deref() {
Some(_) => {
match subsidiary_exists(collection, &subsidiary_name)? {
Some(subsidiary) => {
let stored_name = get_subsidiary_principal(&subsidiary)?.ok_or_else(|| {
error!(
?collection,
?subsidiary,
?subsidiary_name,
?name,
"Subsidiary ccache has no principal"
);
KrbError::CredentialCacheError
})?;
if &stored_name == name {
Ok(subsidiary)
} else {
error!(
?collection,
?subsidiary_name,
?stored_name,
?name,
"Stored principal do not match"
);
Err(KrbError::CredentialCacheError)
}
}
None => {
collection
.add_keyring(subsidiary_name.as_str())
.map_err(|e| {
error!(?collection, ?e, "Failed to add keyring");
KrbError::CredentialCacheError
})
}
}
}
None => {
match subsidiary_exists(collection, subsidiary_name.as_str())? {
Some(subsidiary) => {
let stored_name = get_subsidiary_principal(&subsidiary)?.ok_or_else(|| {
error!(
?collection,
?subsidiary,
?subsidiary_name,
?name,
"Subsidiary ccache has no principal"
);
KrbError::CredentialCacheError
})?;
if &stored_name != name {
let subsidiary_name = get_random_subsidiary_name(collection)?;
let subsidiary = collection.add_keyring(subsidiary_name.as_str())?;
Ok(subsidiary)
} else {
Ok(subsidiary)
}
}
None => {
let subsidiary = collection.add_keyring(subsidiary_name.as_str())?;
Ok(subsidiary)
}
}
}
}
}
fn get_primary_subsidiary_name(collection: &mut Keyring) -> Result<Option<String>, KrbError> {
let primary_name: &str = "krb_ccache:primary";
match collection.search_for_key::<User, &str, Option<&mut Keyring>>(primary_name, None) {
Ok(k) => {
let payload = k.read()?;
let mut reader = binrw::io::Cursor::new(payload);
let pn: PrimaryName = reader.read_type(binrw::Endian::Big).map_err(|err| {
error!(collection=?collection, error=?err, "Failed to read primary name");
KrbError::BinRWError
})?;
let pn: String = String::from_utf8_lossy(pn.strval.as_slice()).to_string();
Ok(Some(pn))
}
Err(errno::Errno(libc::ENOKEY)) => Ok(None),
Err(e) => Err(KrbError::from(e)),
}
}
fn store_clock_skew(clock_skew: Duration, keyring: &mut Keyring) -> Result<Option<Key>, KrbError> {
let key_name = "__krb5_time_offsets__";
let offsets = TimeOffsets {
secs: clock_skew.as_secs() as i32,
usecs: clock_skew.subsec_micros() as i32,
};
let mut c = std::io::Cursor::new(Vec::new());
offsets.write(&mut c).map_err(|err| {
error!(keyring=?keyring, offsets=?offsets, error=?err, "Failed to store clock skew");
KrbError::BinRWError
})?;
let vec = c.into_inner();
let key = keyring.add_key::<User, &str, &[u8]>(key_name, vec.as_slice())?;
Ok(Some(key))
}
fn store_credential(
name: &Name,
ticket: &EncTicket,
kdc_reply_part: &KdcReplyPart,
subsidiary: &mut Keyring,
) -> Result<(), KrbError> {
let key_name: String = (&kdc_reply_part.server).into();
let creds: CredentialV4 = CredentialV4::new(name, ticket, kdc_reply_part)?;
let mut c = std::io::Cursor::new(Vec::new());
creds.write(&mut c).map_err(|err| {
error!(subsidiary=?subsidiary, name=?name,error=?err, "Failed to store credential");
KrbError::BinRWError
})?;
let vec = c.into_inner();
subsidiary
.add_key::<User, &str, &[u8]>(key_name.as_str(), vec.as_slice())
.map_err(KrbError::from)?;
Ok(())
}
fn store_principal(name: &Name, subsidiary: &mut Keyring) -> Result<(), KrbError> {
match get_subsidiary_principal(subsidiary)? {
Some(stored) => {
if &stored == name {
Ok(())
} else {
error!(subsidiary=?subsidiary, stored_name=?stored, name=?name, "Stored principal do not match");
Err(KrbError::CredentialCacheError)
}
}
None => {
let key_name = "__krb5_princ__";
let princ: PrincipalV4 = name.try_into()?;
let mut c = std::io::Cursor::new(Vec::new());
princ.write(&mut c).map_err(|err| {
error!(subsidiary=?subsidiary, name=?name, error=?err, "Failed to store principal");
KrbError::BinRWError
})?;
let vec = c.into_inner();
subsidiary
.add_key::<User, &str, &[u8]>(key_name, vec.as_slice())
.map_err(KrbError::from)?;
Ok(())
}
}
}
fn store_primary_subsidiary_name(
subsidiary_name: &str,
collection: &mut Keyring,
) -> Result<String, KrbError> {
let key_name: &str = "krb_ccache:primary";
let pn: PrimaryName = PrimaryName {
strval: subsidiary_name.as_bytes().to_vec(),
};
let mut c = std::io::Cursor::new(Vec::new());
pn.write(&mut c).map_err(|err| {
error!(subsidiary_name=?subsidiary_name, collection=?collection, error=?err, "Failed to store primary subsidiary name");
KrbError::BinRWError
})?;
let vec = c.into_inner();
collection
.add_key::<User, &str, &[u8]>(key_name, vec.as_slice())
.map_err(|e| {
error!(?e, "Failed to add key");
KrbError::from(e)
})?;
Ok(subsidiary_name.to_string())
}
pub(super) struct KeyringCredentialCacheContext {
residual: Residual,
collection: Keyring,
subsidiary: Option<Keyring>,
}
impl CredentialCache for KeyringCredentialCacheContext {
fn init(&mut self, name: &Name, clock_skew: Option<Duration>) -> Result<(), KrbError> {
let mut subsidiary = get_subsidiary_cache(name, &mut self.collection, &self.residual)?;
subsidiary.clear()?;
let desc = subsidiary.description().map_err(|e| {
error!(?e, "Failed to parse keyring description");
KrbError::CredentialCacheError
})?;
if self.residual.subsidiary.is_none() {
debug!(?desc.description, "Set as primary subsidiary");
store_primary_subsidiary_name(desc.description.as_str(), &mut self.collection)?;
}
debug!(
?name,
?subsidiary,
?desc,
"Storing principal name in subsidiary cache"
);
store_principal(name, &mut subsidiary)?;
if let Some(cs) = clock_skew {
debug!(?cs, ?subsidiary, "Storing clock skew in subsidiary cache");
store_clock_skew(cs, &mut subsidiary)?;
};
debug!(?subsidiary, "Subsidiary cache initialized");
self.subsidiary = Some(subsidiary);
Ok(())
}
fn destroy(&mut self) -> Result<(), KrbError> {
let subsidiary_name = match self.residual.subsidiary.as_deref() {
Some(name) => name.to_string(),
None => match get_primary_subsidiary_name(&mut self.collection)? {
Some(name) => name,
None => {
debug!(
concat!(
"No subsidiary cache was destroyed because the subsidiary name was not specified ",
"in the residual and the collection does not have a primary subsidiary defined"
)
);
return Ok(());
}
},
};
match self
.collection
.search_for_keyring(subsidiary_name.as_str(), None)
{
Ok(k) => self.collection.unlink_keyring(&k).map_err(|e| {
error!(?e, "Failed to unlink subsidiary from collection");
e.into()
}),
Err(errno::Errno(libc::ENOKEY)) => {
debug!(?subsidiary_name, "Subsidiary does not exist");
Ok(())
}
Err(e) => {
error!(?e, "Failed to search for keyring");
Err(e.into())
}
}
}
fn store(&mut self, credentials: &KerberosCredentials) -> Result<(), KrbError> {
let Some(subsidiary) = self.subsidiary.as_mut() else {
error!("Credential cache not initialized");
return Err(KrbError::CredentialCacheError);
};
let stored_name = get_subsidiary_principal(subsidiary)?.ok_or_else(|| {
error!(?subsidiary, "Subsidiary ccache has no principal");
KrbError::CredentialCacheError
})?;
if stored_name != credentials.name {
error!(
?stored_name,
?credentials.name,
"Stored principal do not match"
);
return Err(KrbError::CredentialCacheError);
}
let desc = subsidiary.description().map_err(|e| {
error!(?e, "Failed to parse keyring description");
KrbError::CredentialCacheError
})?;
debug!(?desc, "Storing credentials in subsidiary cache");
store_credential(
&credentials.name,
&credentials.ticket,
&credentials.kdc_reply,
subsidiary,
)?;
Ok(())
}
}
fn get_or_create_keyring(parent: &mut Keyring, name: &str) -> Result<Keyring, Errno> {
match parent.search_for_keyring(name, None) {
Ok(k) => Ok(k),
Err(errno::Errno(libc::ENOKEY)) => parent.add_keyring(name),
Err(e) => Err(e),
}
.inspect_err(|e| error!(?parent, ?name, ?e, "Failed to get or create keyring"))
}
fn get_collection(anchor: &str, collection: &str) -> Result<Keyring, KrbError> {
let mut parent = match anchor {
"process" => Keyring::attach_or_create(SpecialKeyring::Process)
.inspect_err(|e| error!(?e, "Failed to attach or create process keyring")),
"thread" => Keyring::attach_or_create(SpecialKeyring::Thread)
.inspect_err(|e| error!(?e, "Failed to attach or create thread keyring")),
"session" => Keyring::attach_or_create(SpecialKeyring::Session)
.inspect_err(|e| error!(?e, "Failed to attach or create session keyring")),
"user" => Keyring::attach_or_create(SpecialKeyring::User)
.inspect_err(|e| error!(?e, "Failed to attach or create user keyring")),
"persistent" => {
let uid = match collection.parse::<u32>() {
Ok(uid) => uid,
Err(e) => {
error!(?collection, ?e, "Failed to parse collection name into uid");
return Err(KrbError::CredentialCacheError);
}
};
let euid = uzers::get_effective_uid();
if uid != euid {
error!(
?uid,
?euid,
"The collection name (uid) does not match the effective uid (euid)"
);
return Err(KrbError::CredentialCacheError);
}
let parent = keyctl_get_keyring_id(SpecialKeyring::Process.serial(), true)
.inspect_err(|e| error!(?e, "Failed to attach or create process keyring"))?;
let parent = keyctl_get_persistent(uid, parent)
.inspect_err(|e| error!(?e, "Failed to attach to persistent keyring"))?;
let parent = unsafe { Keyring::new(parent) };
Ok(parent)
}
_ => Err(Errno(libc::ENOTSUP)),
}?;
let collection_name = match anchor {
"persistent" => "_krb".to_string(),
_ => format!("_krb_{collection}"),
};
get_or_create_keyring(&mut parent, &collection_name).map_err(|e| e.into())
}
pub(super) fn resolve(ccache_name: &str) -> Result<Box<dyn CredentialCache>, KrbError> {
debug!(?ccache_name, "Parsing ccache name");
let residual = Residual::parse(ccache_name)?;
debug!(?residual, "Parsed residual");
let collection = get_collection(residual.anchor.as_str(), residual.collection.as_str())?;
debug!(?collection, "Resolved collection");
let kcc = KeyringCredentialCacheContext {
residual,
collection,
subsidiary: None,
};
Ok(Box::new(kcc))
}
#[cfg(test)]
mod tests {
use super::*;
use std::process::Command;
#[cfg(feature = "keyring")]
use std::process::Stdio;
#[tokio::test]
async fn test_ccache_keyring_residual_parse() -> Result<(), KrbError> {
let residual = Residual::parse("KEYRING:session");
assert!(residual.is_err());
let residual = Residual::parse("KEYRING:session:");
assert!(residual.is_err());
let residual = Residual::parse("KEYRING:session:1000")?;
assert_eq!(
residual,
Residual {
anchor: "session".to_string(),
collection: "1000".to_string(),
subsidiary: None
}
);
let residual = Residual::parse("KEYRING:session:1000:")?;
assert_eq!(
residual,
Residual {
anchor: "session".to_string(),
collection: "1000".to_string(),
subsidiary: None
}
);
let residual = Residual::parse("KEYRING:session:1000:foo")?;
assert_eq!(
residual,
Residual {
anchor: "session".to_string(),
collection: "1000".to_string(),
subsidiary: Some("foo".to_string())
}
);
Ok(())
}
fn klist_all(ccache_name: &str) -> String {
let output = Command::new("klist")
.stderr(Stdio::null())
.arg("-c")
.arg(ccache_name)
.arg("-A")
.output()
.expect("Unable to execute command klist");
assert!(output.status.success());
String::from_utf8_lossy(output.stdout.as_slice()).to_string()
}
#[tokio::test]
async fn test_ccache_keyring_primary() -> Result<(), KrbError> {
let ccache_name = Some("KEYRING:session:c1");
let p1 = Name::Principal {
name: "p1".to_string(),
realm: "EXAMPLE.COM".to_string(),
};
let p2 = Name::Principal {
name: "p2".to_string(),
realm: "EXAMPLE.COM".to_string(),
};
let p3 = Name::Principal {
name: "p3".to_string(),
realm: "EXAMPLE.COM".to_string(),
};
let mut ccache = crate::ccache::resolve(ccache_name)?;
let mut col = get_collection("session", "c1")?;
ccache.init(&p1, None)?;
let primary = get_primary_subsidiary_name(&mut col)?.expect("No primary key");
assert!(primary == "c1");
ccache.init(&p2, None)?;
let random = get_primary_subsidiary_name(&mut col)?.expect("No primary key");
assert!(random != "c1");
let ccache_name = Some("KEYRING:session:c1:s1");
let mut ccache = crate::ccache::resolve(ccache_name)?;
ccache.init(&p3, None)?;
let primary = get_primary_subsidiary_name(&mut col)?.expect("No primary key");
assert!(primary == random);
let ccache_name = "KEYRING:session:c1";
let output = klist_all(ccache_name);
assert!(output.contains("p1@EXAMPLE.COM"));
assert!(output.contains("p2@EXAMPLE.COM"));
assert!(output.contains("p3@EXAMPLE.COM"));
let ccache_name = "KEYRING:session:c1:c1";
let mut ccache = crate::ccache::resolve(Some(ccache_name))?;
ccache.destroy()?;
let ccache_name = "KEYRING:session:c1";
let output = klist_all(ccache_name);
assert!(!output.contains("p1@EXAMPLE.COM"));
assert!(output.contains("p2@EXAMPLE.COM"));
assert!(output.contains("p3@EXAMPLE.COM"));
let ccache_name = "KEYRING:session:c1";
let mut ccache = crate::ccache::resolve(Some(ccache_name))?;
ccache.destroy()?;
let output = klist_all(ccache_name);
assert!(!output.contains("p1@EXAMPLE.COM"));
assert!(!output.contains("p2@EXAMPLE.COM"));
assert!(output.contains("p3@EXAMPLE.COM"));
let mut col = Keyring::attach_or_create(SpecialKeyring::Session)?;
if let Ok(k) = col.search_for_keyring("_krb_c1", None) {
col.unlink_keyring(&k).expect("Failed to unlink");
};
Ok(())
}
#[tokio::test]
async fn test_ccache_keyring() -> Result<(), KrbError> {
if std::env::var("CI").is_ok() {
tracing::warn!("Skipping test_ccache_keyring in CI");
return Ok(());
}
let ccache_name = Some("KEYRING:process:foo:bar");
let mut ccache = crate::ccache::resolve(ccache_name)?;
let credentials = crate::proto::get_tgt("testuser", "EXAMPLE.COM", "password").await?;
ccache.init(&credentials.name, None)?;
ccache.store(&credentials)?;
let credentials = crate::proto::get_tgt("testuser", "EXAMPLE.COM", "password").await?;
ccache.store(&credentials)?;
let credentials = crate::proto::get_tgt("testuser2", "EXAMPLE.COM", "password").await?;
let r = ccache.store(&credentials);
assert!(r.is_err());
let ccache_name_zap = Some("KEYRING:process:foo:zap");
let mut ccache_zap = crate::ccache::resolve(ccache_name_zap)?;
ccache_zap.init(&credentials.name, None)?;
ccache_zap.store(&credentials)?;
let ccache_name_no_sub = Some("KEYRING:process:abc");
let mut ccache_no_sub = crate::ccache::resolve(ccache_name_no_sub)?;
ccache_no_sub.init(&credentials.name, None)?;
ccache_no_sub.store(&credentials)?;
Ok(())
}
}