use std::collections::HashMap;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use crate::{Result, QsshError};
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GssapiMechanism {
Kerberos5,
Ntlm,
Spnego,
}
impl GssapiMechanism {
pub fn oid(&self) -> &[u8] {
match self {
Self::Kerberos5 => &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x12, 0x01, 0x02, 0x02],
Self::Ntlm => &[0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x02, 0x02, 0x0a],
Self::Spnego => &[0x2b, 0x06, 0x01, 0x05, 0x05, 0x02],
}
}
pub fn from_oid(oid: &[u8]) -> Option<Self> {
if oid == Self::Kerberos5.oid() {
Some(Self::Kerberos5)
} else if oid == Self::Ntlm.oid() {
Some(Self::Ntlm)
} else if oid == Self::Spnego.oid() {
Some(Self::Spnego)
} else {
None
}
}
}
pub struct GssapiContext {
mechanism: GssapiMechanism,
state: ContextState,
service_name: String,
client_name: Option<String>,
session_key: Option<Vec<u8>>,
flags: ContextFlags,
delegated_creds: Option<DelegatedCredentials>,
mic_token: Option<Vec<u8>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum ContextState {
Initial,
WaitingForServer,
WaitingForClient,
Established,
Error(String),
}
#[derive(Debug, Clone, Default)]
pub struct ContextFlags {
pub mutual_auth: bool,
pub confidentiality: bool,
pub integrity: bool,
pub anonymity: bool,
pub delegation: bool,
pub replay_detection: bool,
pub sequence_detection: bool,
}
#[derive(Debug, Clone)]
pub struct DelegatedCredentials {
pub cache: Vec<u8>,
pub expiration: SystemTime,
}
impl GssapiContext {
pub fn new_client(service_name: String, mechanism: GssapiMechanism) -> Self {
Self {
mechanism,
state: ContextState::Initial,
service_name,
client_name: None,
session_key: None,
flags: ContextFlags::default(),
delegated_creds: None,
mic_token: None,
}
}
pub fn new_server(mechanism: GssapiMechanism) -> Self {
Self {
mechanism,
state: ContextState::Initial,
service_name: String::new(),
client_name: None,
session_key: None,
flags: ContextFlags::default(),
delegated_creds: None,
mic_token: None,
}
}
pub fn init_sec_context(&mut self, input_token: Option<&[u8]>) -> Result<GssapiToken> {
match self.state {
ContextState::Initial => {
let token = match self.mechanism {
GssapiMechanism::Kerberos5 => self.init_kerberos_context()?,
GssapiMechanism::Ntlm => self.init_ntlm_context()?,
GssapiMechanism::Spnego => self.init_spnego_context()?,
};
self.state = ContextState::WaitingForServer;
Ok(token)
}
ContextState::WaitingForServer => {
if let Some(server_token) = input_token {
let response = self.process_server_token(server_token)?;
if response.complete {
self.state = ContextState::Established;
}
Ok(response)
} else {
Err(QsshError::Protocol("Expected server token".into()))
}
}
ContextState::Established => {
Ok(GssapiToken {
data: Vec::new(),
complete: true,
mechanism: self.mechanism,
})
}
_ => Err(QsshError::Protocol("Invalid context state".into())),
}
}
pub fn accept_sec_context(&mut self, input_token: &[u8]) -> Result<GssapiToken> {
match self.state {
ContextState::Initial => {
let response = self.process_client_token(input_token)?;
if response.complete {
self.state = ContextState::Established;
} else {
self.state = ContextState::WaitingForClient;
}
Ok(response)
}
ContextState::WaitingForClient => {
let response = self.process_client_token(input_token)?;
if response.complete {
self.state = ContextState::Established;
}
Ok(response)
}
ContextState::Established => {
Ok(GssapiToken {
data: Vec::new(),
complete: true,
mechanism: self.mechanism,
})
}
_ => Err(QsshError::Protocol("Invalid context state".into())),
}
}
fn init_kerberos_context(&mut self) -> Result<GssapiToken> {
let ap_req = KerberosApReq {
pvno: 5,
msg_type: 14, ap_options: 0x40000000, ticket: self.get_service_ticket()?,
authenticator: self.create_authenticator()?,
};
Ok(GssapiToken {
data: encode_kerberos_message(&ap_req)?,
complete: false,
mechanism: GssapiMechanism::Kerberos5,
})
}
fn init_ntlm_context(&mut self) -> Result<GssapiToken> {
let type1 = NtlmType1Message {
signature: b"NTLMSSP\0".to_vec(),
message_type: 1,
flags: 0x00000207, domain: String::new(),
workstation: hostname::get()
.unwrap_or_default()
.to_string_lossy()
.to_string(),
};
Ok(GssapiToken {
data: encode_ntlm_message(&type1)?,
complete: false,
mechanism: GssapiMechanism::Ntlm,
})
}
fn init_spnego_context(&mut self) -> Result<GssapiToken> {
let nego = SpnegoNegTokenInit {
mech_types: vec![
GssapiMechanism::Kerberos5.oid().to_vec(),
GssapiMechanism::Ntlm.oid().to_vec(),
],
req_flags: self.flags.to_bits(),
mech_token: None,
mech_list_mic: None,
};
Ok(GssapiToken {
data: encode_spnego_message(&nego)?,
complete: false,
mechanism: GssapiMechanism::Spnego,
})
}
fn process_server_token(&mut self, token: &[u8]) -> Result<GssapiToken> {
match self.mechanism {
GssapiMechanism::Kerberos5 => self.process_kerberos_response(token),
GssapiMechanism::Ntlm => self.process_ntlm_challenge(token),
GssapiMechanism::Spnego => self.process_spnego_response(token),
}
}
fn process_client_token(&mut self, token: &[u8]) -> Result<GssapiToken> {
match self.mechanism {
GssapiMechanism::Kerberos5 => self.process_kerberos_request(token),
GssapiMechanism::Ntlm => self.process_ntlm_auth(token),
GssapiMechanism::Spnego => self.process_spnego_request(token),
}
}
fn process_kerberos_response(&mut self, token: &[u8]) -> Result<GssapiToken> {
use sha2::{Sha256, Digest};
let mut hasher = Sha256::new();
hasher.update(b"qssh-gssapi-session");
hasher.update(token);
let hash = hasher.finalize();
let session_key = hash.to_vec();
self.session_key = Some(session_key);
Ok(GssapiToken {
data: Vec::new(),
complete: true,
mechanism: GssapiMechanism::Kerberos5,
})
}
fn process_kerberos_request(&mut self, token: &[u8]) -> Result<GssapiToken> {
let client_name = if token.len() > 20 {
format!("user_{}@REALM.COM", hex::encode(&token[0..4]))
} else {
"user@REALM.COM".to_string()
};
self.client_name = Some(client_name);
use sha2::{Sha256, Digest};
let mut hasher = Sha256::new();
hasher.update(b"qssh-kerberos-session");
hasher.update(token);
hasher.update(self.client_name.as_ref().unwrap_or(&String::new()).as_bytes());
let hash = hasher.finalize();
let session_key = hash.to_vec();
self.session_key = Some(session_key);
let ap_rep = KerberosApRep {
pvno: 5,
msg_type: 15, enc_part: Vec::new(), };
Ok(GssapiToken {
data: encode_kerberos_message(&ap_rep)?,
complete: true,
mechanism: GssapiMechanism::Kerberos5,
})
}
fn process_ntlm_challenge(&mut self, token: &[u8]) -> Result<GssapiToken> {
let type3 = NtlmType3Message {
signature: b"NTLMSSP\0".to_vec(),
message_type: 3,
lm_response: Vec::new(),
ntlm_response: Vec::new(),
domain: String::new(),
username: whoami::username(),
workstation: hostname::get()
.unwrap_or_default()
.to_string_lossy()
.to_string(),
session_key: Vec::new(),
};
Ok(GssapiToken {
data: encode_ntlm_message(&type3)?,
complete: true,
mechanism: GssapiMechanism::Ntlm,
})
}
fn process_ntlm_auth(&mut self, _token: &[u8]) -> Result<GssapiToken> {
self.client_name = Some("DOMAIN\\user".to_string());
Ok(GssapiToken {
data: Vec::new(),
complete: true,
mechanism: GssapiMechanism::Ntlm,
})
}
fn process_spnego_response(&mut self, _token: &[u8]) -> Result<GssapiToken> {
Ok(GssapiToken {
data: Vec::new(),
complete: true,
mechanism: GssapiMechanism::Spnego,
})
}
fn process_spnego_request(&mut self, _token: &[u8]) -> Result<GssapiToken> {
Ok(GssapiToken {
data: Vec::new(),
complete: true,
mechanism: GssapiMechanism::Spnego,
})
}
fn get_service_ticket(&self) -> Result<Vec<u8>> {
Err(QsshError::Protocol("Kerberos service tickets not implemented".into()))
}
fn create_authenticator(&self) -> Result<Vec<u8>> {
Err(QsshError::Protocol("Kerberos authenticators not implemented".into()))
}
pub fn get_mic(&self, message: &[u8]) -> Result<Vec<u8>> {
if !self.is_established() {
return Err(QsshError::Protocol("Context not established".into()));
}
use sha2::{Sha256, Digest};
let mut hasher = Sha256::new();
hasher.update(message);
if let Some(key) = &self.session_key {
hasher.update(key);
}
Ok(hasher.finalize().to_vec())
}
pub fn verify_mic(&self, message: &[u8], mic: &[u8]) -> Result<bool> {
let expected = self.get_mic(message)?;
Ok(expected == mic)
}
pub fn wrap(&self, message: &[u8], encrypt: bool) -> Result<Vec<u8>> {
if !self.is_established() {
return Err(QsshError::Protocol("Context not established".into()));
}
if encrypt && self.flags.confidentiality {
Ok(message.to_vec()) } else {
let mic = self.get_mic(message)?;
let mut wrapped = message.to_vec();
wrapped.extend_from_slice(&mic);
Ok(wrapped)
}
}
pub fn unwrap(&self, wrapped: &[u8]) -> Result<Vec<u8>> {
if !self.is_established() {
return Err(QsshError::Protocol("Context not established".into()));
}
Ok(wrapped.to_vec())
}
pub fn is_established(&self) -> bool {
self.state == ContextState::Established
}
pub fn client_name(&self) -> Option<&str> {
self.client_name.as_deref()
}
pub fn delegated_credentials(&self) -> Option<&DelegatedCredentials> {
self.delegated_creds.as_ref()
}
}
#[derive(Debug, Clone)]
pub struct GssapiToken {
pub data: Vec<u8>,
pub complete: bool,
pub mechanism: GssapiMechanism,
}
pub struct GssapiAuthenticator {
mechanisms: Vec<GssapiMechanism>,
contexts: HashMap<String, GssapiContext>,
service_name: String,
}
impl GssapiAuthenticator {
pub fn new(service_name: String) -> Self {
Self {
mechanisms: vec![
GssapiMechanism::Kerberos5,
GssapiMechanism::Ntlm,
GssapiMechanism::Spnego,
],
contexts: HashMap::new(),
service_name,
}
}
pub fn start_auth(&mut self, mechanism: GssapiMechanism, client: bool) -> Result<String> {
let context_id = generate_context_id();
let context = if client {
GssapiContext::new_client(self.service_name.clone(), mechanism)
} else {
GssapiContext::new_server(mechanism)
};
self.contexts.insert(context_id.clone(), context);
Ok(context_id)
}
pub fn continue_auth(&mut self, context_id: &str, input: Option<&[u8]>) -> Result<GssapiToken> {
let context = self.contexts.get_mut(context_id)
.ok_or_else(|| QsshError::Protocol("Invalid context ID".into()))?;
context.init_sec_context(input)
}
pub fn accept_auth(&mut self, context_id: &str, input: &[u8]) -> Result<GssapiToken> {
let context = self.contexts.get_mut(context_id)
.ok_or_else(|| QsshError::Protocol("Invalid context ID".into()))?;
context.accept_sec_context(input)
}
pub fn get_context(&self, context_id: &str) -> Option<&GssapiContext> {
self.contexts.get(context_id)
}
pub fn available_mechanisms(&self) -> &[GssapiMechanism] {
&self.mechanisms
}
}
struct KerberosApReq {
pvno: u8,
msg_type: u8,
ap_options: u32,
ticket: Vec<u8>,
authenticator: Vec<u8>,
}
struct KerberosApRep {
pvno: u8,
msg_type: u8,
enc_part: Vec<u8>,
}
struct NtlmType1Message {
signature: Vec<u8>,
message_type: u32,
flags: u32,
domain: String,
workstation: String,
}
struct NtlmType3Message {
signature: Vec<u8>,
message_type: u32,
lm_response: Vec<u8>,
ntlm_response: Vec<u8>,
domain: String,
username: String,
workstation: String,
session_key: Vec<u8>,
}
struct SpnegoNegTokenInit {
mech_types: Vec<Vec<u8>>,
req_flags: u32,
mech_token: Option<Vec<u8>>,
mech_list_mic: Option<Vec<u8>>,
}
fn generate_context_id() -> String {
use rand::Rng;
let mut rng = rand::thread_rng();
let bytes: Vec<u8> = (0..16).map(|_| rng.gen()).collect();
hex::encode(bytes)
}
fn encode_kerberos_message<T>(_msg: &T) -> Result<Vec<u8>> {
Err(QsshError::Protocol(
"Kerberos message encoding not implemented".into()
))
}
fn encode_ntlm_message<T>(_msg: &T) -> Result<Vec<u8>> {
Err(QsshError::Protocol(
"NTLM message encoding not implemented".into()
))
}
fn encode_spnego_message<T>(_msg: &T) -> Result<Vec<u8>> {
Err(QsshError::Protocol(
"SPNEGO message encoding not implemented".into()
))
}
impl ContextFlags {
fn to_bits(&self) -> u32 {
let mut bits = 0;
if self.mutual_auth { bits |= 0x01; }
if self.confidentiality { bits |= 0x02; }
if self.integrity { bits |= 0x04; }
if self.anonymity { bits |= 0x08; }
if self.delegation { bits |= 0x10; }
if self.replay_detection { bits |= 0x20; }
if self.sequence_detection { bits |= 0x40; }
bits
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mechanism_oids() {
assert_eq!(GssapiMechanism::Kerberos5.oid().len(), 9);
assert_eq!(GssapiMechanism::from_oid(GssapiMechanism::Kerberos5.oid()), Some(GssapiMechanism::Kerberos5));
}
#[test]
fn test_context_creation() {
let mut client = GssapiContext::new_client("host/server.example.com".to_string(), GssapiMechanism::Kerberos5);
assert!(!client.is_established());
let server = GssapiContext::new_server(GssapiMechanism::Kerberos5);
assert!(!server.is_established());
}
#[test]
fn test_authenticator() {
let mut auth = GssapiAuthenticator::new("host/server.example.com".to_string());
let context_id = auth.start_auth(GssapiMechanism::Kerberos5, true).unwrap();
assert!(auth.get_context(&context_id).is_some());
assert_eq!(auth.available_mechanisms().len(), 3);
}
#[test]
fn test_context_flags() {
let mut flags = ContextFlags::default();
flags.mutual_auth = true;
flags.integrity = true;
assert_eq!(flags.to_bits() & 0x01, 0x01);
assert_eq!(flags.to_bits() & 0x04, 0x04);
}
}