use std::{fmt, string::String, sync::Mutex};
use bytes::Bytes;
use libgssapi::{
context::{ClientCtx, CtxFlags, SecurityContext},
credential::{Cred, CredUsage},
name::Name,
oid::{GSS_MECH_KRB5, GSS_NT_HOSTBASED_SERVICE, OidSet},
util::Buf,
};
use super::{
SaslClientAction, SaslClientAuthenticator, SaslMechanism, WireError,
kerberos::KerberosLoginManager,
};
const SECURITY_LAYER_NONE: u8 = 0x01;
#[derive(Clone, Copy, PartialEq, Eq)]
enum GssapiPhase {
Establishing,
SecurityLayer,
Done,
}
struct GssapiSession {
context: ClientCtx,
phase: GssapiPhase,
}
pub struct GssapiAuthenticator {
service_name: String,
host: String,
target_name: String,
session: Mutex<Option<GssapiSession>>,
kerberos_login: Option<KerberosLoginManager>,
}
impl GssapiAuthenticator {
#[must_use]
pub fn new(service_name: String, host: String) -> Self {
let mut target_name = String::with_capacity(
service_name
.len()
.saturating_add(host.len())
.saturating_add(1),
);
target_name.push_str(&service_name);
target_name.push('@');
target_name.push_str(&host);
Self {
service_name,
host,
target_name,
session: Mutex::new(None),
kerberos_login: None,
}
}
#[must_use]
pub(crate) fn with_kerberos_login(mut self, kerberos_login: KerberosLoginManager) -> Self {
self.kerberos_login = Some(kerberos_login);
self
}
#[must_use]
pub fn target_name(&self) -> &str {
&self.target_name
}
fn new_context(&self) -> Result<ClientCtx, WireError> {
if let Some(kerberos_login) = &self.kerberos_login {
kerberos_login.login_blocking()?;
}
let desired_mechs = OidSet::singleton(GSS_MECH_KRB5).map_err(gssapi_error)?;
let name = Name::new(self.target_name.as_bytes(), Some(GSS_NT_HOSTBASED_SERVICE))
.map_err(gssapi_error)?;
let target = name
.canonicalize(Some(GSS_MECH_KRB5))
.map_err(gssapi_error)?;
let cred = Cred::acquire(None, None, CredUsage::Initiate, Some(&desired_mechs))
.map_err(gssapi_error)?;
if let Some(kerberos_login) = &self.kerberos_login {
let lifetime = cred.lifetime().map_err(gssapi_error)?;
kerberos_login.start_renewal(lifetime)?;
}
Ok(ClientCtx::new(
Some(cred),
target,
CtxFlags::GSS_C_MUTUAL_FLAG,
Some(GSS_MECH_KRB5),
))
}
}
impl fmt::Debug for GssapiAuthenticator {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("GssapiAuthenticator")
.field("service_name", &self.service_name)
.field("host", &self.host)
.field("target_name", &self.target_name)
.finish_non_exhaustive()
}
}
impl SaslClientAuthenticator for GssapiAuthenticator {
fn mechanism(&self) -> SaslMechanism {
SaslMechanism::Gssapi
}
fn start(&self) -> Result<SaslClientAction, WireError> {
let mut context = self.new_context()?;
let token = context.step(None, None).map_err(gssapi_error)?;
let action = token.as_ref().map(send_token).ok_or_else(|| {
WireError::SaslAuthentication("GSSAPI produced no initial token".to_owned())
})?;
let mut stored = self.session.lock().map_err(gssapi_lock_error)?;
*stored = Some(GssapiSession {
context,
phase: GssapiPhase::Establishing,
});
drop(stored);
Ok(action)
}
fn next(&self, challenge: &[u8]) -> Result<SaslClientAction, WireError> {
let mut stored = self.session.lock().map_err(gssapi_lock_error)?;
let session = stored.as_mut().ok_or_else(|| {
WireError::SaslAuthentication(
"GSSAPI challenge received before context start".to_owned(),
)
})?;
let action = match session.phase {
GssapiPhase::Establishing => {
let token = session
.context
.step(Some(challenge), None)
.map_err(gssapi_error)?;
if session.context.is_complete() {
session.phase = GssapiPhase::SecurityLayer;
send_token_or_empty(token.as_ref())
} else {
token.as_ref().map(send_token).ok_or_else(|| {
WireError::SaslAuthentication(
"GSSAPI context did not complete and produced no token".to_owned(),
)
})?
}
},
GssapiPhase::SecurityLayer => {
let offer = session.context.unwrap(challenge).map_err(gssapi_error)?;
if offer
.as_ref()
.first()
.is_some_and(|supported| supported & SECURITY_LAYER_NONE == 0)
{
return Err(WireError::SaslAuthentication(
"broker does not offer the SASL/GSSAPI no-security-layer option".to_owned(),
));
}
let reply = [SECURITY_LAYER_NONE, 0x00, 0x00, 0x00];
let wrapped = session.context.wrap(false, &reply).map_err(gssapi_error)?;
session.phase = GssapiPhase::Done;
SaslClientAction::Send(Bytes::copy_from_slice(wrapped.as_ref()))
},
GssapiPhase::Done => SaslClientAction::Complete,
};
drop(stored);
Ok(action)
}
}
fn send_token(token: &Buf) -> SaslClientAction {
SaslClientAction::Send(Bytes::copy_from_slice(token.as_ref()))
}
fn send_token_or_empty(token: Option<&Buf>) -> SaslClientAction {
token.map_or_else(|| SaslClientAction::Send(Bytes::new()), send_token)
}
fn gssapi_error(error: libgssapi::error::Error) -> WireError {
WireError::SaslAuthentication(format!("GSSAPI authentication failed: {error}"))
}
fn gssapi_lock_error<T>(_error: std::sync::PoisonError<T>) -> WireError {
WireError::SaslAuthentication("GSSAPI context lock is poisoned".to_owned())
}