use std::ffi::c_void;
use std::sync::OnceLock;
use super::{NegotiateError, ProviderFactory, StepResult, TokenProvider};
const LIB_ENV: &str = "OPENLATCH_GSSAPI_LIB";
#[cfg(target_os = "macos")]
const CANDIDATES: &[&str] = &["/System/Library/Frameworks/GSS.framework/GSS"];
#[cfg(not(target_os = "macos"))]
const CANDIDATES: &[&str] = &[
"libgssapi_krb5.so.2",
"libgssapi.so.4",
"libgssapi.so.3",
"libgssapi.so.2",
"libgssapi.so.1",
];
#[allow(non_camel_case_types)]
type OM_uint32 = u32;
#[repr(C)]
#[derive(Clone, Copy)]
struct GssBufferDesc {
length: usize,
value: *mut c_void,
}
impl GssBufferDesc {
fn empty() -> Self {
Self {
length: 0,
value: std::ptr::null_mut(),
}
}
fn borrowed(bytes: &[u8]) -> Self {
Self {
length: bytes.len(),
value: bytes.as_ptr() as *mut c_void,
}
}
unsafe fn to_vec(self) -> Vec<u8> {
if self.value.is_null() || self.length == 0 {
return Vec::new();
}
std::slice::from_raw_parts(self.value as *const u8, self.length).to_vec()
}
}
#[repr(C)]
#[derive(Clone, Copy)]
struct GssOidDesc {
length: OM_uint32,
elements: *const c_void,
}
const SPNEGO_OID: &[u8] = &[0x2b, 0x06, 0x01, 0x05, 0x05, 0x02];
const NT_HOSTBASED_SERVICE_OID: &[u8] =
&[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x12, 0x01, 0x02, 0x01, 0x04];
const GSS_C_MUTUAL_FLAG: OM_uint32 = 2;
const GSS_C_REPLAY_FLAG: OM_uint32 = 4;
const GSS_C_SEQUENCE_FLAG: OM_uint32 = 8;
const GSS_S_CONTINUE_NEEDED: OM_uint32 = 1;
const GSS_ERROR_MASK: OM_uint32 = 0xFFFF_0000;
const GSS_C_GSS_CODE: i32 = 1;
const GSS_C_MECH_CODE: i32 = 2;
type GssImportName = unsafe extern "C" fn(
*mut OM_uint32,
*const GssBufferDesc,
*const GssOidDesc,
*mut *mut c_void,
) -> OM_uint32;
#[allow(clippy::type_complexity)]
type GssInitSecContext = unsafe extern "C" fn(
*mut OM_uint32, *mut c_void, *mut *mut c_void, *mut c_void, *const GssOidDesc, OM_uint32, OM_uint32, *const c_void, *const GssBufferDesc, *mut *const GssOidDesc, *mut GssBufferDesc, *mut OM_uint32, *mut OM_uint32, ) -> OM_uint32;
type GssReleaseName = unsafe extern "C" fn(*mut OM_uint32, *mut *mut c_void) -> OM_uint32;
type GssReleaseBuffer = unsafe extern "C" fn(*mut OM_uint32, *mut GssBufferDesc) -> OM_uint32;
type GssDeleteSecContext =
unsafe extern "C" fn(*mut OM_uint32, *mut *mut c_void, *mut GssBufferDesc) -> OM_uint32;
type GssDisplayStatus = unsafe extern "C" fn(
*mut OM_uint32,
OM_uint32,
i32,
*const GssOidDesc,
*mut OM_uint32,
*mut GssBufferDesc,
) -> OM_uint32;
struct Gssapi {
_library: libloading::Library,
which: String,
import_name: GssImportName,
init_sec_context: GssInitSecContext,
release_name: GssReleaseName,
release_buffer: GssReleaseBuffer,
delete_sec_context: GssDeleteSecContext,
display_status: GssDisplayStatus,
}
unsafe impl Send for Gssapi {}
unsafe impl Sync for Gssapi {}
static LIBRARY: OnceLock<Result<&'static Gssapi, NegotiateError>> = OnceLock::new();
fn library() -> Result<&'static Gssapi, NegotiateError> {
LIBRARY.get_or_init(load).clone()
}
fn candidates() -> Vec<String> {
match std::env::var(LIB_ENV) {
Ok(path) if path.trim().is_empty() => Vec::new(),
Ok(path) => vec![path],
Err(_) => CANDIDATES.iter().map(|s| (*s).to_string()).collect(),
}
}
fn load() -> Result<&'static Gssapi, NegotiateError> {
let mut tried = Vec::new();
for candidate in candidates() {
let library = match unsafe { libloading::Library::new(&candidate) } {
Ok(library) => library,
Err(e) => {
tried.push(format!("{candidate}: {e}"));
continue;
}
};
match unsafe { bind(library, &candidate) } {
Ok(bound) => return Ok(Box::leak(Box::new(bound))),
Err(missing) => tried.push(format!("{candidate}: {missing}")),
}
}
Err(NegotiateError::LibraryUnavailable(if tried.is_empty() {
"no candidate libraries to try".to_string()
} else {
tried.join("; ")
}))
}
unsafe fn bind(library: libloading::Library, which: &str) -> Result<Gssapi, String> {
macro_rules! sym {
($name:literal, $ty:ty) => {{
let symbol: libloading::Symbol<$ty> = library
.get(concat!($name, "\0").as_bytes())
.map_err(|e| format!("{} is missing: {e}", $name))?;
*symbol
}};
}
Ok(Gssapi {
import_name: sym!("gss_import_name", GssImportName),
init_sec_context: sym!("gss_init_sec_context", GssInitSecContext),
release_name: sym!("gss_release_name", GssReleaseName),
release_buffer: sym!("gss_release_buffer", GssReleaseBuffer),
delete_sec_context: sym!("gss_delete_sec_context", GssDeleteSecContext),
display_status: sym!("gss_display_status", GssDisplayStatus),
which: which.to_string(),
_library: library,
})
}
fn describe(lib: &Gssapi, major: OM_uint32, minor: OM_uint32) -> String {
let mut parts = Vec::new();
for (code, kind) in [(major, GSS_C_GSS_CODE), (minor, GSS_C_MECH_CODE)] {
let mut context: OM_uint32 = 0;
let mut minor_out: OM_uint32 = 0;
let mut buffer = GssBufferDesc::empty();
let status = unsafe {
(lib.display_status)(
&mut minor_out,
code,
kind,
std::ptr::null(),
&mut context,
&mut buffer,
)
};
if status & GSS_ERROR_MASK == 0 {
let text = unsafe { buffer.to_vec() };
let mut release_minor: OM_uint32 = 0;
unsafe { (lib.release_buffer)(&mut release_minor, &mut buffer) };
if !text.is_empty() {
parts.push(String::from_utf8_lossy(&text).into_owned());
}
}
}
if parts.is_empty() {
format!("GSSAPI status major={major:#x} minor={minor:#x}")
} else {
parts.join(": ")
}
}
pub struct GssapiProvider;
impl GssapiProvider {
pub fn new() -> Self {
Self
}
}
impl Default for GssapiProvider {
fn default() -> Self {
Self::new()
}
}
impl ProviderFactory for GssapiProvider {
fn new_provider(&self, spn: &str) -> Result<Box<dyn TokenProvider>, NegotiateError> {
let lib = library()?;
Ok(Box::new(GssContext::new(lib, spn)?))
}
fn name(&self) -> &'static str {
"gssapi"
}
}
struct GssContext {
lib: &'static Gssapi,
target: *mut c_void,
context: *mut c_void,
done: bool,
}
unsafe impl Send for GssContext {}
impl GssContext {
fn new(lib: &'static Gssapi, spn: &str) -> Result<Self, NegotiateError> {
let hostbased = spn.replacen('/', "@", 1);
let name_buffer = GssBufferDesc::borrowed(hostbased.as_bytes());
let oid = GssOidDesc {
length: NT_HOSTBASED_SERVICE_OID.len() as OM_uint32,
elements: NT_HOSTBASED_SERVICE_OID.as_ptr() as *const c_void,
};
let mut minor: OM_uint32 = 0;
let mut target: *mut c_void = std::ptr::null_mut();
let major = unsafe { (lib.import_name)(&mut minor, &name_buffer, &oid, &mut target) };
if major & GSS_ERROR_MASK != 0 {
return Err(NegotiateError::Provider(format!(
"the SPN \"{spn}\" could not be imported ({}): {}",
lib.which,
describe(lib, major, minor)
)));
}
Ok(Self {
lib,
target,
context: std::ptr::null_mut(),
done: false,
})
}
}
impl TokenProvider for GssContext {
fn step(&mut self, peer: Option<&[u8]>) -> StepResult {
if self.done {
return StepResult::Done(None);
}
let lib = self.lib;
let mech = GssOidDesc {
length: SPNEGO_OID.len() as OM_uint32,
elements: SPNEGO_OID.as_ptr() as *const c_void,
};
let input = peer.map(GssBufferDesc::borrowed);
let input_ptr = input
.as_ref()
.map_or(std::ptr::null(), |b| b as *const GssBufferDesc);
let mut minor: OM_uint32 = 0;
let mut output = GssBufferDesc::empty();
let mut actual_mech: *const GssOidDesc = std::ptr::null();
let mut ret_flags: OM_uint32 = 0;
let major = unsafe {
(lib.init_sec_context)(
&mut minor,
std::ptr::null_mut(), &mut self.context,
self.target,
&mech,
GSS_C_MUTUAL_FLAG | GSS_C_REPLAY_FLAG | GSS_C_SEQUENCE_FLAG,
0,
std::ptr::null(),
input_ptr,
&mut actual_mech,
&mut output,
&mut ret_flags,
std::ptr::null_mut(),
)
};
let token = unsafe { output.to_vec() };
if !token.is_empty() {
let mut release_minor: OM_uint32 = 0;
unsafe { (lib.release_buffer)(&mut release_minor, &mut output) };
}
if major & GSS_ERROR_MASK != 0 {
self.done = true;
let detail = describe(lib, major, minor);
return if looks_like_no_credential(&detail) {
StepResult::Failed(NegotiateError::NoTicket(detail))
} else {
StepResult::Failed(NegotiateError::Provider(detail))
};
}
if major & GSS_S_CONTINUE_NEEDED != 0 {
return StepResult::Continue(token);
}
self.done = true;
StepResult::Done((!token.is_empty()).then_some(token))
}
}
fn looks_like_no_credential(detail: &str) -> bool {
let lower = detail.to_ascii_lowercase();
[
"no credentials",
"credentials cache",
"credential cache",
"no key table",
"can't find client principal",
"ticket expired",
"no ticket",
]
.iter()
.any(|needle| lower.contains(needle))
}
impl Drop for GssContext {
fn drop(&mut self) {
let mut minor: OM_uint32 = 0;
let mut output = GssBufferDesc::empty();
unsafe {
if !self.context.is_null() {
(self.lib.delete_sec_context)(&mut minor, &mut self.context, &mut output);
(self.lib.release_buffer)(&mut minor, &mut output);
}
if !self.target.is_null() {
(self.lib.release_name)(&mut minor, &mut self.target);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_ladder_names_mit_before_heimdal() {
#[cfg(not(target_os = "macos"))]
{
assert_eq!(CANDIDATES[0], "libgssapi_krb5.so.2");
assert!(CANDIDATES.contains(&"libgssapi.so.3"));
}
#[cfg(target_os = "macos")]
{
assert!(CANDIDATES[0].starts_with('/'));
}
}
#[test]
fn the_oids_are_the_der_bodies_the_rfcs_specify() {
assert_eq!(SPNEGO_OID, &[0x2b, 0x06, 0x01, 0x05, 0x05, 0x02]);
assert_eq!(NT_HOSTBASED_SERVICE_OID[0], 0x2a);
assert_eq!(NT_HOSTBASED_SERVICE_OID.len(), 10);
}
#[test]
fn a_missing_credential_is_told_apart_from_every_other_failure() {
assert!(looks_like_no_credential(
"No credentials cache found (filename: /tmp/krb5cc_1000)"
));
assert!(looks_like_no_credential("Ticket expired"));
assert!(!looks_like_no_credential(
"Server not found in Kerberos database"
));
}
#[test]
fn an_absent_library_is_a_named_degradation() {
let err = NegotiateError::LibraryUnavailable("nothing on the ladder".into()).into_ol();
assert!(err
.suggestion
.as_deref()
.is_some_and(|s| s.contains("krb5-libs")));
}
#[test]
fn an_empty_override_forces_the_no_library_branch() {
assert!(!CANDIDATES.is_empty());
assert_eq!(LIB_ENV, "OPENLATCH_GSSAPI_LIB");
}
}