use super::gssapi_ffi::{
self, GSS_C_DELEG_FLAG, GSS_C_MUTUAL_FLAG, GSS_C_NO_CHANNEL_BINDINGS, GSS_C_NO_CONTEXT,
GSS_C_NO_CREDENTIAL, GSS_C_NO_OID, GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED, GssBufferDesc,
GssCtxIdT, GssNameT, GssOmUint32, get_gss_nt_service_name, get_gssapi_error,
gss_delete_sec_context, gss_import_name, gss_init_sec_context, gss_release_buffer,
gss_release_name,
};
use crate::security::{
IntegratedAuthConfig, SecurityContext, SecurityError, SspiAuthToken,
spn::make_spn_canonicalized,
};
use std::ptr;
#[derive(Debug)]
struct GssCtxHandle(GssCtxIdT);
unsafe impl Send for GssCtxHandle {}
unsafe impl Sync for GssCtxHandle {}
impl Default for GssCtxHandle {
fn default() -> Self {
Self(GSS_C_NO_CONTEXT)
}
}
#[derive(Debug)]
struct GssNameHandle(GssNameT);
unsafe impl Send for GssNameHandle {}
unsafe impl Sync for GssNameHandle {}
impl Default for GssNameHandle {
fn default() -> Self {
Self(ptr::null_mut())
}
}
pub struct GssapiContext {
spn: String,
ctx_handle: GssCtxHandle,
target_name: GssNameHandle,
is_complete: bool,
#[allow(dead_code)]
channel_bindings: Option<Vec<u8>>,
}
impl GssapiContext {
pub fn new(
config: &IntegratedAuthConfig,
server: &str,
port: u16,
) -> Result<Self, SecurityError> {
Self::check_availability()?;
let (spn, is_user_provided) = match &config.server_spn {
Some(user_spn) => (user_spn.clone(), true),
None => (make_spn_canonicalized(server, None, port), false),
};
let target_name = import_name(&spn, is_user_provided)?;
Ok(Self {
spn,
ctx_handle: GssCtxHandle::default(),
target_name: GssNameHandle(target_name),
is_complete: false,
channel_bindings: config.channel_bindings.clone(),
})
}
pub fn check_availability() -> Result<(), SecurityError> {
if gssapi_ffi::is_gssapi_available() {
Ok(())
} else {
Err(SecurityError::LoadLibraryFailed(
"libgssapi_krb5.so: GSSAPI library not available".to_string(),
))
}
}
}
impl SecurityContext for GssapiContext {
fn package_name(&self) -> &str {
"Kerberos"
}
fn generate_token(
&mut self,
server_token: Option<&[u8]>,
) -> Result<SspiAuthToken, SecurityError> {
let mut minor_status: GssOmUint32 = 0;
let mut output_token = GssBufferDesc::default();
let mut ret_flags: GssOmUint32 = 0;
let mut time_rec: GssOmUint32 = 0;
let mut input_token_desc;
let input_token = match server_token {
Some(data) if !data.is_empty() => {
input_token_desc = GssBufferDesc::from_slice(data);
&mut input_token_desc as *mut GssBufferDesc
}
_ => ptr::null_mut(),
};
let req_flags = GSS_C_MUTUAL_FLAG | GSS_C_DELEG_FLAG;
let major_status = unsafe {
gss_init_sec_context(
&mut minor_status,
GSS_C_NO_CREDENTIAL, &mut self.ctx_handle.0,
self.target_name.0,
GSS_C_NO_OID, req_flags,
0, GSS_C_NO_CHANNEL_BINDINGS, input_token,
ptr::null_mut(), &mut output_token,
&mut ret_flags,
&mut time_rec,
)
};
let is_complete = major_status == GSS_S_COMPLETE;
let continue_needed = major_status == GSS_S_CONTINUE_NEEDED;
if !is_complete && !continue_needed {
let error_msg = get_gssapi_error(major_status, minor_status);
if major_status & 0xFFFF0000 == gssapi_ffi::GSS_S_NO_CRED {
return Err(SecurityError::GssapiError {
major: major_status,
minor: minor_status,
message: format!(
"No Kerberos credentials available. Run 'kinit' to obtain a ticket. {}",
error_msg
),
});
}
return Err(SecurityError::InitContextFailed {
message: error_msg,
code: major_status,
});
}
let token_data = unsafe { output_token.to_vec() };
if !output_token.value.is_null() {
unsafe {
gss_release_buffer(&mut minor_status, &mut output_token);
}
}
if is_complete {
self.is_complete = true;
}
Ok(SspiAuthToken {
data: token_data,
is_complete,
})
}
fn is_complete(&self) -> bool {
self.is_complete
}
fn spn(&self) -> &str {
&self.spn
}
}
impl Drop for GssapiContext {
fn drop(&mut self) {
if !gssapi_ffi::is_gssapi_available() {
return;
}
let mut minor_status: GssOmUint32 = 0;
if self.ctx_handle.0 != GSS_C_NO_CONTEXT {
unsafe {
gss_delete_sec_context(&mut minor_status, &mut self.ctx_handle.0, ptr::null_mut());
}
}
if !self.target_name.0.is_null() {
unsafe {
gss_release_name(&mut minor_status, &mut self.target_name.0);
}
}
}
}
fn import_name(spn: &str, user_provided: bool) -> Result<GssNameT, SecurityError> {
let mut minor_status: GssOmUint32 = 0;
let mut target_name: GssNameT = ptr::null_mut();
let gssapi_name = if user_provided {
spn.to_string()
} else {
let converted = convert_spn_to_gssapi_format(spn);
if converted != spn {
tracing::debug!(
"Converted auto-generated SPN '{}' to GSSAPI format '{}'",
spn,
converted
);
}
converted
};
let mut name_buffer = GssBufferDesc::from_str(&gssapi_name);
let name_type = get_gss_nt_service_name();
let major_status = unsafe {
gss_import_name(
&mut minor_status,
&mut name_buffer,
name_type,
&mut target_name,
)
};
if major_status != GSS_S_COMPLETE {
let error_msg = get_gssapi_error(major_status, minor_status);
return Err(SecurityError::InvalidSpnFormat(format!(
"Failed to import SPN '{}': {}",
spn, error_msg
)));
}
Ok(target_name)
}
fn convert_spn_to_gssapi_format(spn: &str) -> String {
if spn.contains('@') && !spn.contains('/') {
return spn.to_string();
}
if let Some(slash_pos) = spn.find('/') {
let service = &spn[..slash_pos];
let rest = &spn[slash_pos + 1..];
let host = if let Some(colon_pos) = rest.find(':') {
&rest[..colon_pos]
} else {
rest
};
format!("{}@{}", service, host)
} else {
spn.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gssapi_context_creation() {
let config = IntegratedAuthConfig::new();
let result = GssapiContext::new(&config, "server.contoso.com", 1433);
if let Ok(ctx) = result {
assert_eq!(ctx.spn(), "MSSQLSvc/server.contoso.com:1433");
assert!(!ctx.is_complete());
}
}
#[test]
fn test_gssapi_context_with_explicit_spn() {
let config = IntegratedAuthConfig::with_spn("MSSQLSvc/custom:5000".to_string());
if let Ok(ctx) = GssapiContext::new(&config, "server", 1433) {
assert_eq!(ctx.spn(), "MSSQLSvc/custom:5000");
}
}
#[test]
fn test_gssapi_package_name() {
let config = IntegratedAuthConfig::new();
if let Ok(ctx) = GssapiContext::new(&config, "server", 1433) {
assert_eq!(ctx.package_name(), "Kerberos");
}
}
#[test]
fn test_gssapi_check_availability() {
let result = GssapiContext::check_availability();
let _ = result;
}
#[test]
fn test_gssapi_generate_token_no_credentials() {
let config = IntegratedAuthConfig::new();
if let Ok(mut ctx) = GssapiContext::new(&config, "server.contoso.com", 1433) {
let result = ctx.generate_token(None);
match result {
Ok(token) => {
assert!(!token.data.is_empty());
}
Err(SecurityError::NoCredentials) => {
}
Err(e) => {
println!("Got error: {:?}", e);
}
}
}
}
#[test]
fn test_convert_spn_to_gssapi_format() {
assert_eq!(
convert_spn_to_gssapi_format("MSSQLSvc/sql.example.local:1433"),
"MSSQLSvc@sql.example.local"
);
assert_eq!(
convert_spn_to_gssapi_format("MSSQLSvc/server:INSTANCE1"),
"MSSQLSvc@server"
);
assert_eq!(
convert_spn_to_gssapi_format("MSSQLSvc@host"),
"MSSQLSvc@host"
);
assert_eq!(
convert_spn_to_gssapi_format("MSSQLSvc/server"),
"MSSQLSvc@server"
);
}
}