feat(core): RDP client on IronRDP

clients::rdp connects over any caller-dialled TCP stream (direct, proxy
or SSH tunnel), upgrades to TLS and logs on with NLA (CredSSP/NTLM),
optionally falling back to TLS-only logon for servers without NLA such
as xrdp (credentials are passed so they log straight in). The server
certificate is pinned through a caller check on its SHA256 fingerprint
before any credentials are sent. The desktop decodes into the VNC
client's RGBA framebuffer; input (IronRDP's scancode/Unicode/mouse
operations), Display Control resizes and the deactivation-reactivation
sequence run on the session thread.

IronRDP's releases need three vendored patches (vendor/README.md):
picky and sspi with their RustCrypto release-candidate pins relaxed so
they share russh's stable versions (their own suites, 402 tests, pass
against them), and ironrdp-session with two unreleased upstream bitmap
fixes (#1486 stride, #1524 RDP6 orientation) without which xrdp desktops
render sheared.

Verified live against xrdp (scottyhardy/docker-remote-desktop): logon,
full desktop rendering, keyboard and mouse input.
This commit is contained in:
Andy Dixon
2026-10-12 10:50:24 +01:00
parent 6a4d7381d9
commit 50c0104d57
227 changed files with 66687 additions and 20 deletions
+222
View File
@@ -0,0 +1,222 @@
use std::collections::HashSet;
use std::mem;
use picky_krb::gss_api::MechTypeList;
use sspi::credssp::{
ClientMode, ClientState, CredSspClient, CredSspMode, CredSspServer, ServerMode, ServerState, TsRequest,
};
use sspi::kerberos::ServerProperties;
use sspi::network_client::NetworkClient;
use sspi::ntlm::NtlmConfig;
use sspi::{
AuthIdentity, Credentials, CredentialsBuffers, KerberosConfig, KerberosServerConfig, NegotiateConfig, Secret,
Username,
};
use url::Url;
use super::kerberos::kdc::{CLIENT_COMPUTER_NAME, KDC_URL, KdcMock, MAX_TIME_SKEW, Validators};
use super::kerberos::network_client::NetworkClientMock;
use super::kerberos::{KrbEnvironment, init_krb_environment};
use crate::client_server::TARGET_NAME;
use crate::client_server::kerberos::kdc::SERVER_COMPUTER_NAME;
use crate::common::CredentialsProxyImpl;
const PUBLIC_KEY: &[u8] = &[
48, 130, 2, 34, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 130, 2, 15, 0, 48, 130, 2, 10, 2, 130,
2, 1, 0, 153, 85, 210, 206, 231, 176, 16, 84, 146, 20, 255, 201, 74, 62, 122, 183, 157, 210, 202, 111, 17, 50, 30,
181, 14, 13, 193, 242, 152, 41, 178, 93, 237, 151, 133, 122, 29, 233, 73, 139, 182, 23, 93, 149, 119, 56, 5, 156,
180, 217, 84, 109, 88, 242, 117, 103, 167, 173, 81, 14, 171, 69, 18, 6, 149, 163, 35, 39, 128, 183, 73, 157, 200,
229, 17, 156, 115, 197, 187, 141, 211, 156, 148, 207, 94, 14, 119, 210, 166, 59, 242, 214, 224, 159, 51, 41, 55,
78, 250, 170, 175, 133, 213, 24, 173, 39, 234, 10, 216, 60, 238, 204, 157, 149, 186, 144, 203, 231, 241, 239, 41,
118, 35, 14, 245, 183, 29, 229, 209, 198, 182, 174, 34, 66, 146, 20, 214, 109, 119, 19, 8, 207, 231, 222, 119, 155,
192, 76, 15, 221, 210, 78, 132, 112, 33, 213, 87, 153, 25, 38, 190, 161, 178, 130, 108, 140, 75, 75, 22, 74, 28, 0,
164, 72, 103, 14, 57, 202, 58, 91, 94, 235, 177, 68, 209, 252, 254, 173, 97, 101, 156, 128, 139, 58, 140, 226, 73,
26, 232, 234, 178, 220, 193, 89, 196, 236, 89, 173, 235, 92, 39, 13, 1, 0, 93, 43, 252, 89, 236, 123, 140, 108,
144, 215, 171, 46, 211, 144, 236, 202, 59, 87, 177, 225, 162, 70, 144, 109, 113, 237, 2, 152, 115, 52, 166, 112,
249, 30, 53, 62, 239, 228, 226, 97, 56, 246, 27, 64, 43, 153, 195, 79, 176, 38, 178, 188, 192, 207, 0, 179, 255,
17, 173, 250, 152, 140, 8, 198, 9, 2, 50, 151, 16, 176, 125, 175, 161, 118, 185, 166, 34, 217, 189, 160, 27, 145,
91, 113, 71, 71, 220, 4, 195, 210, 242, 185, 14, 108, 61, 61, 5, 45, 27, 38, 56, 245, 49, 55, 196, 230, 22, 8, 155,
27, 3, 79, 252, 108, 199, 189, 29, 98, 220, 118, 212, 5, 0, 129, 59, 110, 131, 188, 159, 249, 56, 37, 69, 106, 185,
215, 38, 54, 36, 196, 28, 39, 81, 27, 255, 249, 155, 197, 237, 125, 92, 147, 108, 248, 238, 115, 101, 170, 27, 203,
193, 180, 33, 146, 208, 216, 113, 174, 158, 84, 100, 32, 200, 49, 30, 28, 31, 112, 247, 68, 190, 181, 247, 54, 117,
131, 215, 100, 13, 170, 52, 12, 137, 61, 253, 114, 120, 116, 124, 238, 3, 234, 95, 242, 208, 224, 96, 132, 150,
152, 186, 81, 85, 50, 179, 216, 191, 125, 25, 148, 232, 235, 234, 193, 150, 186, 41, 18, 38, 220, 144, 104, 97,
127, 215, 215, 49, 92, 81, 21, 232, 67, 145, 164, 179, 156, 220, 175, 154, 70, 144, 218, 31, 106, 84, 78, 218, 238,
15, 29, 207, 34, 33, 68, 121, 213, 114, 203, 80, 32, 42, 224, 115, 86, 161, 42, 78, 246, 183, 203, 213, 198, 110,
71, 22, 137, 164, 4, 163, 206, 239, 57, 197, 112, 179, 191, 160, 5, 2, 3, 1, 0, 1,
];
fn run_credssp(
client: &mut CredSspClient,
server: &mut CredSspServer<CredentialsProxyImpl<'_>>,
auth_identity: &AuthIdentity,
network_client: &mut dyn NetworkClient,
) {
let mut ts_request = TsRequest::default();
for _ in 0..4 {
ts_request = match client
.process(mem::take(&mut ts_request))
.resolve_with_client(network_client)
.unwrap()
{
ClientState::ReplyNeeded(ts_request) => ts_request,
ClientState::FinalMessage(ts_request) => ts_request,
};
match server.process(ts_request).resolve_with_client(network_client).unwrap() {
ServerState::ReplyNeeded(server_ts_request) => ts_request = server_ts_request,
ServerState::Finished(received_auth_identity) => {
assert_eq!(*auth_identity, received_auth_identity);
return;
}
};
}
panic!("CredSSP authentication should not exceed 4 steps")
}
#[test]
fn credssp_ntlm() {
let auth_identity = AuthIdentity {
username: Username::parse("test_user").unwrap(),
password: Secret::from("test_password".to_owned()),
};
let credentials = Credentials::AuthIdentity(auth_identity.clone());
let mut client = CredSspClient::new(
PUBLIC_KEY.to_vec(),
credentials.clone(),
CredSspMode::WithCredentials,
ClientMode::Ntlm(NtlmConfig {
client_computer_name: Some("DESKTOP-3D83IAN.example.com".to_owned()),
}),
TARGET_NAME.to_owned(),
)
.unwrap();
let mut server = CredSspServer::new(
PUBLIC_KEY.to_vec(),
CredentialsProxyImpl::new(&auth_identity),
ServerMode::Ntlm(NtlmConfig {
client_computer_name: Some("DESKTOP-3D83IAN.example.com".to_owned()),
}),
)
.unwrap();
let mut network_client = NetworkClientMock { kdc: KdcMock::empty() };
run_credssp(&mut client, &mut server, &auth_identity, &mut network_client);
}
#[test]
fn credssp_kerberos() {
// CredSSP with Kerberos inside requires SPNEGO. We cannot use Kerberos inside CredSSP without SPNEGO.
let KrbEnvironment {
realm,
credentials,
keys,
users,
target_name,
target_service_name,
} = init_krb_environment();
let identity_1 = credentials.to_auth_identity().unwrap();
let mut identity_2 = identity_1.clone();
identity_2.username = Username::new_upn(identity_1.username.account_name(), &realm.to_ascii_lowercase()).unwrap();
let kdc = KdcMock::new(realm, keys, users, Validators::default());
let mut network_client = NetworkClientMock { kdc };
let client_config = KerberosConfig {
kdc_url: Some(Url::parse(KDC_URL).unwrap()),
client_computer_name: CLIENT_COMPUTER_NAME.into(),
};
let server_config = KerberosConfig {
kdc_url: Some(Url::parse(KDC_URL).unwrap()),
client_computer_name: SERVER_COMPUTER_NAME.into(),
};
let server_properties = ServerProperties {
mech_types: MechTypeList::from(Vec::new()),
max_time_skew: MAX_TIME_SKEW,
ticket_decryption_key: None,
service_name: target_service_name,
additional_service_keys: Vec::new(),
user: Some(CredentialsBuffers::try_from(credentials.clone()).unwrap()),
client: None,
authenticators_cache: HashSet::new(),
};
let kerberos_server_config = KerberosServerConfig {
kerberos_config: server_config,
server_properties,
};
let mut client = CredSspClient::new(
PUBLIC_KEY.to_vec(),
credentials,
CredSspMode::WithCredentials,
ClientMode::Negotiate(NegotiateConfig::new(
Box::new(client_config.clone()),
Some(String::from("kerberos,!ntlm")),
CLIENT_COMPUTER_NAME.into(),
)),
target_name,
)
.unwrap();
let mut server = CredSspServer::new(
PUBLIC_KEY.to_vec(),
CredentialsProxyImpl::new(&identity_2),
ServerMode::Negotiate(NegotiateConfig::new(
Box::new(kerberos_server_config),
Some(String::from("kerberos,!ntlm")),
SERVER_COMPUTER_NAME.into(),
)),
)
.unwrap();
run_credssp(&mut client, &mut server, &identity_1, &mut network_client);
}
#[test]
fn credssp_negotiate_ntlm() {
let auth_identity = AuthIdentity {
username: Username::parse("test_user").unwrap(),
password: Secret::from("test_password".to_owned()),
};
let credentials = Credentials::AuthIdentity(auth_identity.clone());
let mut client = CredSspClient::new(
PUBLIC_KEY.to_vec(),
credentials.clone(),
CredSspMode::WithCredentials,
ClientMode::Negotiate(NegotiateConfig::new(
Box::new(NtlmConfig {
client_computer_name: Some("DESKTOP-3D83IAN.example.com".to_owned()),
}),
Some("ntlm,!kerberos,!pku2u".to_owned()),
"DESKTOP-3D83IAN.example.com".to_owned(),
)),
TARGET_NAME.to_owned(),
)
.unwrap();
let mut server = CredSspServer::new(
PUBLIC_KEY.to_vec(),
CredentialsProxyImpl::new(&auth_identity),
ServerMode::Negotiate(NegotiateConfig::new(
Box::new(NtlmConfig {
client_computer_name: Some("DESKTOP-3D83IAN.example.com".to_owned()),
}),
Some("ntlm,!kerberos,!pku2u".to_owned()),
"SERVER.example.com".to_owned(),
)),
)
.unwrap();
let mut network_client = NetworkClientMock { kdc: KdcMock::empty() };
run_credssp(&mut client, &mut server, &auth_identity, &mut network_client);
}
@@ -0,0 +1,70 @@
use sspi::NegotiatedProtocol;
use sspi::credssp::SspiContext;
/// Helper-trait to implement SSPI context validation in tests.
///
/// _Note_: this trait is not complete and may be extended in the future when needed.
pub(super) trait SspiContextValidator {
/// Validates the client SSPI context after the provided number of iterations.
fn validate_client(&mut self, step: usize, client: &SspiContext);
}
/// Empty validator that does not perform any validation.
pub(super) struct EmptySspiContextValidator;
impl SspiContextValidator for EmptySspiContextValidator {
fn validate_client(&mut self, _step: usize, _client: &SspiContext) {}
}
/// Performs additional SPNEGO context validation for Kerberos over SPNEGO tests.
pub(super) struct SpnegoKerberosContextValidator;
impl SspiContextValidator for SpnegoKerberosContextValidator {
fn validate_client(&mut self, _step: usize, client: &SspiContext) {
let SspiContext::Negotiate(negotiate) = client else {
panic!("Expected Negotiate context");
};
assert!(matches!(
negotiate.negotiated_protocol(),
NegotiatedProtocol::Kerberos(_)
));
}
}
/// Validates that the client correctly falls back to NTLM.
pub(super) struct SpnegoKerberosNtlmFallbackValidator;
impl SspiContextValidator for SpnegoKerberosNtlmFallbackValidator {
fn validate_client(&mut self, _step: usize, client: &SspiContext) {
let SspiContext::Negotiate(negotiate) = client else {
panic!("Expected Negotiate context");
};
assert!(matches!(negotiate.negotiated_protocol(), NegotiatedProtocol::Ntlm(_)));
}
}
/// Validates that the client correctly falls back to NTLM when the server selected NTLM in SPNEGO instead of Kerberos.
pub(super) struct SpnegoServerNtlmFallbackValidator;
impl SspiContextValidator for SpnegoServerNtlmFallbackValidator {
fn validate_client(&mut self, step: usize, client: &SspiContext) {
let SspiContext::Negotiate(negotiate) = client else {
panic!("Expected Negotiate context");
};
match step {
0 => {
assert!(matches!(
negotiate.negotiated_protocol(),
NegotiatedProtocol::Kerberos(_)
));
}
1 => {
assert!(matches!(negotiate.negotiated_protocol(), NegotiatedProtocol::Ntlm(_)));
}
_ => {}
}
}
}
@@ -0,0 +1,631 @@
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::time::Duration as StdDuration;
use picky_asn1::date::GeneralizedTime;
use picky_asn1::restricted_string::IA5String;
use picky_asn1::wrapper::{
Asn1SequenceOf, ExplicitContextTag0, ExplicitContextTag1, ExplicitContextTag2, ExplicitContextTag3,
ExplicitContextTag4, ExplicitContextTag5, ExplicitContextTag6, ExplicitContextTag7, ExplicitContextTag9,
ExplicitContextTag10, ExplicitContextTag12, IntegerAsn1, OctetStringAsn1, Optional,
};
use picky_krb::constants::error_codes::{KDC_ERR_PREAUTH_FAILED, KDC_ERR_PREAUTH_REQUIRED};
use picky_krb::constants::etypes::AES256_CTS_HMAC_SHA1_96;
use picky_krb::constants::key_usages::{
AS_REP_ENC, TGS_REP_ENC_SESSION_KEY, TGS_REP_ENC_SUB_KEY, TGS_REQ_PA_DATA_AP_REQ_AUTHENTICATOR, TICKET_REP,
};
use picky_krb::constants::types::{
AS_REP_MSG_TYPE, KRB_ERROR_MSG_TYPE, PA_ENC_TIMESTAMP, PA_ENC_TIMESTAMP_KEY_USAGE, PA_ETYPE_INFO2_TYPE,
PA_TGS_REQ_TYPE, TGS_REP_MSG_TYPE,
};
use picky_krb::crypto::CipherSuite;
use picky_krb::data_types::{
Authenticator, EncTicketPart, EncTicketPartInner, EncryptedData, EncryptionKey, EtypeInfo2Entry, KerberosFlags,
KerberosStringAsn1, KerberosTime, LastReq, LastReqInner, Microseconds, PaData, PaEncTsEnc, PrincipalName, Realm,
Ticket, TicketInner, TransitedEncoding,
};
use picky_krb::messages::{
ApReq, ApReqInner, AsRep, AsReq, EncAsRepPart, EncKdcRepPart, EncTgsRepPart, KdcRep, KdcReq, KdcReqBody, KrbError,
KrbErrorInner, TgsRep, TgsReq,
};
use rand::rngs::{StdRng, SysRng};
use rand_core::{Rng as _, SeedableRng as _, TryRng as _};
use sspi::kerberos::KERBEROS_VERSION;
use time::{Duration, OffsetDateTime};
pub(crate) const MAX_TIME_SKEW: StdDuration = StdDuration::from_secs(3);
pub(crate) const KDC_URL: &str = "tcp://192.168.1.103:88";
pub(crate) const CLIENT_COMPUTER_NAME: &str = "DESKTOP-8F33RFH.example.com";
pub(crate) const SERVER_COMPUTER_NAME: &str = "WIN-956CQOSSJTF.example.com";
/// Represents user credentials in the internal KDC database.
pub(crate) struct PasswordCreds {
/// User's password.
pub password: Vec<u8>,
/// Salt for deriving the encryption key.
pub salt: String,
}
/// Represents user name in the internal KDC database.
///
/// We created a wrapper type because [PrincipalName] does not
/// implement the [Hash] trait.
#[derive(Debug, PartialEq, Eq, Clone)]
pub(crate) struct UserName(pub PrincipalName);
impl Hash for UserName {
fn hash<H>(&self, state: &mut H)
where
H: Hasher,
{
self.0.name_type.0.0.hash(state);
self.0.name_string.0.0.iter().for_each(|s| s.0.hash(state))
}
}
/// Collection of validators of incoming Kerberos messages.
///
/// The user can provide customs validators to check messages generated by our Kerberos implementation.
pub(crate) struct Validators {
/// Closure that validates the incoming AsReq message.
pub as_req: Box<dyn Fn(&AsReq) + Send + Sync>,
/// Closure that validates the incoming TgsReq message.
pub tgs_req: Box<dyn Fn(&TgsReq) + Send + Sync>,
}
impl Default for Validators {
fn default() -> Self {
Self {
as_req: Box::new(|_| {}),
tgs_req: Box::new(|_| {}),
}
}
}
/// Simple mock of the KDC server.
///
/// We use it to test our Kerberos implementation.
/// This KDC implementation performs only small amount of all possible checks on
/// the incoming Kerberos messages: encryption keys + key usage number usage
/// and some mandatory fields like `pa-datas`.
/// All other validations like checking user/service names should be done separately. See [Validators] structure for more details.
#[derive(Default)]
pub(crate) struct KdcMock {
/// Domain's Kerberos realm.
realm: String,
/// Represents Kerberos long-term keys.
keys: HashMap<UserName, Vec<u8>>,
/// Represents users credentials.
users: HashMap<UserName, PasswordCreds>,
/// Incoming Kerberos messages validators.
validators: Validators,
}
impl KdcMock {
/// Returns empty [KdcMock].
///
/// Methods of the returned [KdcMock] should never be called. It can only be used for mocking in tests
/// where KDC is not needed but there is a necessary to provide [NetworkClient] because of the API.
pub(crate) fn empty() -> Self {
Self::default()
}
/// Creates a new [KdcMock].
pub(crate) fn new(
realm: String,
keys: HashMap<UserName, Vec<u8>>,
users: HashMap<UserName, PasswordCreds>,
validators: Validators,
) -> Self {
Self {
realm,
keys,
users,
validators,
}
}
fn make_err<const ERROR_CODE: u32>(sname: PrincipalName, realm: Realm, salt: Option<String>) -> KrbError {
let current_date = OffsetDateTime::now_utc();
// https://www.rfc-editor.org/rfc/rfc4120#section-5.2.4
// Microseconds ::= INTEGER (0..999999)
let microseconds = current_date.microsecond().min(999_999);
KrbError::from(KrbErrorInner {
pvno: ExplicitContextTag0::from(IntegerAsn1(vec![KERBEROS_VERSION])),
msg_type: ExplicitContextTag1::from(IntegerAsn1::from(vec![KRB_ERROR_MSG_TYPE])),
ctime: Optional::from(None),
cusec: Optional::from(None),
stime: ExplicitContextTag4::from(KerberosTime::from(GeneralizedTime::from(current_date))),
susec: ExplicitContextTag5::from(Microseconds::from(microseconds.to_be_bytes().to_vec())),
error_code: ExplicitContextTag6::from(ERROR_CODE),
crealm: Optional::from(None),
cname: Optional::from(None),
realm: ExplicitContextTag9::from(realm),
sname: ExplicitContextTag10::from(sname),
e_text: Optional::from(None),
e_data: Optional::from(Some(ExplicitContextTag12::from(OctetStringAsn1::from(
picky_asn1_der::to_vec(&Asn1SequenceOf::from(if let Some(salt) = salt {
vec![
PaData {
padata_type: ExplicitContextTag1::from(IntegerAsn1::from(PA_ETYPE_INFO2_TYPE.to_vec())),
padata_data: ExplicitContextTag2::from(OctetStringAsn1::from(
picky_asn1_der::to_vec(&Asn1SequenceOf::from(vec![EtypeInfo2Entry {
etype: ExplicitContextTag0::from(IntegerAsn1::from(vec![
AES256_CTS_HMAC_SHA1_96 as u8,
])),
salt: Optional::from(Some(ExplicitContextTag1::from(KerberosStringAsn1::from(
IA5String::from_string(salt).unwrap(),
)))),
s2kparams: Optional::from(None),
}]))
.unwrap(),
)),
},
PaData {
padata_type: ExplicitContextTag1::from(IntegerAsn1::from(PA_ENC_TIMESTAMP.to_vec())),
padata_data: ExplicitContextTag2::from(OctetStringAsn1::from(Vec::new())),
},
]
} else {
Vec::new()
}))
.unwrap(),
)))),
})
}
fn validate_timestamp(
creds: &PasswordCreds,
sname: PrincipalName,
realm: Realm,
pa_datas: &Asn1SequenceOf<PaData>,
) -> Result<Vec<u8>, KrbError> {
macro_rules! err_preauth {
(failed) => {
Self::make_err::<{ KDC_ERR_PREAUTH_FAILED }>(sname.clone(), realm.clone(), Some(creds.salt.clone()))
};
(required) => {
Self::make_err::<{ KDC_ERR_PREAUTH_REQUIRED }>(sname.clone(), realm.clone(), Some(creds.salt.clone()))
};
}
let enc_data: EncryptedData = picky_asn1_der::from_bytes(
&pa_datas
.0
.iter()
.find(|pa_data| pa_data.padata_type.0.0 == PA_ENC_TIMESTAMP)
.ok_or_else(|| err_preauth!(required))?
.padata_data
.0
.0,
)
.map_err(|_| err_preauth!(failed))?;
let cipher = CipherSuite::try_from(enc_data.etype.0.0.as_slice())
.map_err(|_| err_preauth!(failed))?
.cipher();
let key = cipher
.generate_key_from_password(&creds.password, creds.salt.as_bytes())
.unwrap();
let timestamp: PaEncTsEnc = picky_asn1_der::from_bytes(
&cipher
.decrypt(&key, PA_ENC_TIMESTAMP_KEY_USAGE, &enc_data.cipher.0.0)
.map_err(|_| err_preauth!(failed))?,
)
.map_err(|_| err_preauth!(failed))?;
let kdc_timestamp = OffsetDateTime::now_utc();
let client_timestamp = OffsetDateTime::try_from(timestamp.patimestamp.0.0)
.map_err(|_| err_preauth!(failed))
.map_err(|_| err_preauth!(failed))?;
if client_timestamp > kdc_timestamp || kdc_timestamp - client_timestamp > MAX_TIME_SKEW {
return Err(err_preauth!(failed));
}
Ok(key)
}
fn make_ticket(
realm: KerberosStringAsn1,
session_key: Vec<u8>,
service_key: &[u8],
kdc_options: KerberosFlags,
sname: PrincipalName,
cname: PrincipalName,
) -> Ticket {
let auth_time = OffsetDateTime::now_utc();
let end_time = auth_time + Duration::days(1);
let ticket_enc_part = EncTicketPart::from(EncTicketPartInner {
flags: ExplicitContextTag0::from(kdc_options.clone()),
key: ExplicitContextTag1::from(EncryptionKey {
key_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![18])),
key_value: ExplicitContextTag1::from(OctetStringAsn1::from(session_key)),
}),
crealm: ExplicitContextTag2::from(realm.clone()),
cname: ExplicitContextTag3::from(cname),
transited: ExplicitContextTag4::from(TransitedEncoding {
// the client is unable to check these fields, so we can put any values we want
tr_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![0])),
contents: ExplicitContextTag1::from(OctetStringAsn1::from(vec![1])),
}),
auth_time: ExplicitContextTag5::from(KerberosTime::from(GeneralizedTime::from(auth_time))),
starttime: Optional::from(None),
endtime: ExplicitContextTag7::from(KerberosTime::from(GeneralizedTime::from(end_time))),
renew_till: Optional::from(None),
caddr: Optional::from(None),
authorization_data: Optional::from(None),
});
let ticket_enc_data = CipherSuite::Aes256CtsHmacSha196
.cipher()
.encrypt(
service_key,
TICKET_REP,
&picky_asn1_der::to_vec(&ticket_enc_part).unwrap(),
)
.unwrap();
Ticket::from(TicketInner {
tkt_vno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])),
realm: ExplicitContextTag1::from(realm),
sname: ExplicitContextTag2::from(sname),
enc_part: ExplicitContextTag3::from(EncryptedData {
etype: ExplicitContextTag0::from(IntegerAsn1::from(vec![AES256_CTS_HMAC_SHA1_96 as u8])),
kvno: Optional::from(None),
cipher: ExplicitContextTag2::from(OctetStringAsn1::from(ticket_enc_data)),
}),
})
}
/// Performs AS exchange according to the RFC.
///
/// https://www.rfc-editor.org/rfc/rfc4120#section-3.1
pub(crate) fn as_exchange(&self, as_req: AsReq) -> Result<AsRep, KrbError> {
(self.validators.as_req)(&as_req);
let KdcReq {
pvno: _,
msg_type: _,
padata,
req_body,
} = as_req.0;
let KdcReqBody {
kdc_options,
cname,
realm,
sname,
from: _,
till: _,
rtime: _,
nonce: _,
etype: _,
addresses: _,
enc_authorization_data: _,
additional_tickets: _,
} = req_body.0;
let sname = sname.0.expect("sname must present in AsReq").0;
let service_key = self
.keys
.get(&UserName(sname.clone()))
.expect("service's key must present in KDC database");
let realm = realm.0;
let username = UserName(cname.0.expect("cname is missing in AsReq").0);
let creds = self
.users
.get(&username)
.expect("user's credentials is not found in KDC database");
Self::validate_timestamp(
creds,
sname.clone(),
realm.clone(),
&padata
.0
.ok_or_else(|| {
Self::make_err::<{ KDC_ERR_PREAUTH_REQUIRED }>(sname.clone(), realm, Some(creds.salt.clone()))
})?
.0,
)?;
let cipher = CipherSuite::Aes256CtsHmacSha196.cipher();
let mut rng = StdRng::try_from_rng(&mut SysRng).unwrap();
let mut session_key = vec![0; cipher.key_size()];
rng.fill_bytes(&mut session_key);
let initial_key = cipher
.generate_key_from_password(&creds.password, creds.salt.as_bytes())
.unwrap();
let auth_time = OffsetDateTime::now_utc();
let end_time = auth_time + Duration::days(1);
let realm = Realm::from(IA5String::from_string(self.realm.clone()).unwrap());
let nonce = rng.try_next_u32().unwrap();
let as_rep_enc_part = EncAsRepPart::from(EncKdcRepPart {
key: ExplicitContextTag0::from(EncryptionKey {
key_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![AES256_CTS_HMAC_SHA1_96 as u8])),
key_value: ExplicitContextTag1::from(OctetStringAsn1::from(session_key.to_vec())),
}),
last_req: ExplicitContextTag1::from(LastReq::from(vec![LastReqInner {
lr_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![0])),
lr_value: ExplicitContextTag1::from(KerberosTime::from(GeneralizedTime::from(
auth_time - Duration::hours(1),
))),
}])),
nonce: ExplicitContextTag2::from(IntegerAsn1::from(nonce.to_be_bytes().to_vec())),
key_expiration: Optional::from(None),
flags: ExplicitContextTag4::from(kdc_options.0.clone()),
auth_time: ExplicitContextTag5::from(KerberosTime::from(GeneralizedTime::from(auth_time))),
start_time: Optional::from(None),
end_time: ExplicitContextTag7::from(KerberosTime::from(GeneralizedTime::from(end_time))),
renew_till: Optional::from(None),
srealm: ExplicitContextTag9::from(realm.clone()),
sname: ExplicitContextTag10::from(sname.clone()),
caddr: Optional::from(None),
encrypted_pa_data: Optional::from(None),
});
let as_rep_enc_data = cipher
.encrypt(
&initial_key,
AS_REP_ENC,
&picky_asn1_der::to_vec(&as_rep_enc_part).unwrap(),
)
.unwrap();
Ok(AsRep::from(KdcRep {
pvno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])),
msg_type: ExplicitContextTag1::from(IntegerAsn1::from(vec![AS_REP_MSG_TYPE])),
padata: Optional::from(Some(ExplicitContextTag2::from(Asn1SequenceOf::from(vec![PaData {
padata_type: ExplicitContextTag1::from(IntegerAsn1::from(PA_ETYPE_INFO2_TYPE.to_vec())),
padata_data: ExplicitContextTag2::from(OctetStringAsn1::from(
picky_asn1_der::to_vec(&Asn1SequenceOf::from(vec![EtypeInfo2Entry {
etype: ExplicitContextTag0::from(IntegerAsn1::from(vec![AES256_CTS_HMAC_SHA1_96 as u8])),
salt: Optional::from(Some(ExplicitContextTag1::from(KerberosStringAsn1::from(
IA5String::from_string(creds.salt.clone()).unwrap(),
)))),
s2kparams: Optional::from(None),
}]))
.unwrap(),
)),
}])))),
crealm: ExplicitContextTag3::from(realm.clone()),
cname: ExplicitContextTag4::from(username.0.clone()),
ticket: ExplicitContextTag5::from(Self::make_ticket(
realm,
session_key,
service_key,
kdc_options.0,
sname,
username.0,
)),
enc_part: ExplicitContextTag6::from(EncryptedData {
etype: ExplicitContextTag0::from(IntegerAsn1::from(vec![AES256_CTS_HMAC_SHA1_96 as u8])),
kvno: Optional::from(None),
cipher: ExplicitContextTag2::from(OctetStringAsn1::from(as_rep_enc_data)),
}),
}))
}
fn tgs_preauth(
&self,
sname: PrincipalName,
realm: Realm,
pa_datas: &Asn1SequenceOf<PaData>,
) -> Result<(Vec<u8>, PrincipalName, i32), KrbError> {
macro_rules! err_preauth {
(failed) => {
Self::make_err::<{ KDC_ERR_PREAUTH_FAILED }>(sname.clone(), realm.clone(), None)
};
(required) => {
Self::make_err::<{ KDC_ERR_PREAUTH_REQUIRED }>(sname.clone(), realm.clone(), None)
};
}
let ap_req: ApReq = picky_asn1_der::from_bytes(
&pa_datas
.0
.iter()
.find(|pa_data| pa_data.padata_type.0.0 == PA_TGS_REQ_TYPE)
.ok_or_else(|| err_preauth!(required))?
.padata_data
.0
.0,
)
.map_err(|_| err_preauth!(failed))?;
let ApReqInner {
pvno: _,
msg_type: _,
ap_options: _,
ticket,
authenticator,
} = ap_req.0;
let TicketInner {
sname: ticket_sname,
enc_part: ticket_enc_data,
..
} = ticket.0.0;
let cipher = CipherSuite::try_from(ticket_enc_data.etype.0.0.as_slice())
.map_err(|_| err_preauth!(failed))?
.cipher();
let service_key = self
.keys
.get(&UserName(ticket_sname.0))
.expect("service's key must present in KDC database");
let ticket_enc_part: EncTicketPart = picky_asn1_der::from_bytes(
&cipher
.decrypt(service_key, TICKET_REP, &ticket_enc_data.cipher.0.0)
.expect("TGS REQ - TGT Ticket decryption should not fail"),
)
.expect("TGT Ticket enc part decoding should not fail");
let EncTicketPartInner { key, cname, .. } = ticket_enc_part.0;
let session_key = key.0.key_value.0.0;
let authenticator_enc_data = authenticator.0;
let cipher = CipherSuite::try_from(authenticator_enc_data.etype.0.0.as_slice())
.map_err(|_| err_preauth!(failed))?
.cipher();
let authenticator: Authenticator = picky_asn1_der::from_bytes(
&cipher
.decrypt(
&session_key,
TGS_REQ_PA_DATA_AP_REQ_AUTHENTICATOR,
&authenticator_enc_data.cipher.0.0,
)
.expect("TGS REQ - Authenticator decryption should no fail"),
)
.expect("Authenticator decoding should not fail");
Ok(if let Some(key) = authenticator.0.subkey.0 {
(key.0.key_value.0.0, cname.0, TGS_REP_ENC_SUB_KEY)
} else {
(session_key, cname.0, TGS_REP_ENC_SESSION_KEY)
})
}
/// Performs TGS exchange according to the RFC.
///
/// https://www.rfc-editor.org/rfc/rfc4120#section-3.3
pub(crate) fn tgs_exchange(&self, tgs_req: TgsReq) -> Result<TgsRep, KrbError> {
(self.validators.tgs_req)(&tgs_req);
let KdcReq {
pvno: _,
msg_type: _,
padata,
req_body,
} = tgs_req.0;
let KdcReqBody {
kdc_options,
cname: _,
realm: _,
sname,
from: _,
till: _,
rtime: _,
nonce: _,
etype: _,
addresses: _,
enc_authorization_data: _,
additional_tickets,
} = req_body.0;
let sname = sname.0.expect("sname must present in TgsReq").0;
let realm = Realm::from(IA5String::from_string(self.realm.clone()).unwrap());
let (initial_key, cname, tgs_rep_key_usage) = self.tgs_preauth(
sname.clone(),
realm.clone(),
&padata
.0
.ok_or_else(|| Self::make_err::<{ KDC_ERR_PREAUTH_REQUIRED }>(sname.clone(), realm.clone(), None))?
.0,
)?;
let ticket_enc_key = if let Some(tgt_ticket) = additional_tickets.0 {
let TicketInner { sname, enc_part, .. } = tgt_ticket
.0
.0
.into_iter()
.next()
.expect("array of additional tickets must not be empty")
.0;
let key = self
.keys
.get(&UserName(sname.0))
.expect("service's key must present in KDC database");
let EncryptedData {
etype,
cipher: ticket_enc_data,
kvno: _,
} = enc_part.0;
let cipher = CipherSuite::try_from(etype.0.0.as_slice())
.expect("ticket etype should be valid")
.cipher();
let ticket_enc_part: EncTicketPart =
picky_asn1_der::from_bytes(&cipher.decrypt(key, TICKET_REP, &ticket_enc_data.0.0).unwrap())
.expect("TGT Ticket enc part decoding should not fail");
ticket_enc_part.0.key.0.key_value.0.0
} else {
self.keys
.get(&UserName(sname.clone()))
.expect("service's key must present in KDC database")
.to_vec()
};
let cipher = CipherSuite::Aes256CtsHmacSha196.cipher();
let mut rng = StdRng::try_from_rng(&mut SysRng).unwrap();
let mut session_key = vec![0; cipher.key_size()];
rng.fill_bytes(&mut session_key);
let nonce = rng.next_u32();
let auth_time = OffsetDateTime::now_utc();
let end_time = auth_time + Duration::days(1);
let tgs_rep_enc_part = EncTgsRepPart::from(EncKdcRepPart {
key: ExplicitContextTag0::from(EncryptionKey {
key_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![AES256_CTS_HMAC_SHA1_96 as u8])),
key_value: ExplicitContextTag1::from(OctetStringAsn1::from(session_key.to_vec())),
}),
last_req: ExplicitContextTag1::from(LastReq::from(vec![LastReqInner {
lr_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![0])),
lr_value: ExplicitContextTag1::from(KerberosTime::from(GeneralizedTime::from(
auth_time - Duration::hours(1),
))),
}])),
nonce: ExplicitContextTag2::from(IntegerAsn1::from(nonce.to_be_bytes().to_vec())),
key_expiration: Optional::from(None),
flags: ExplicitContextTag4::from(kdc_options.0.clone()),
auth_time: ExplicitContextTag5::from(KerberosTime::from(GeneralizedTime::from(auth_time))),
start_time: Optional::from(None),
end_time: ExplicitContextTag7::from(KerberosTime::from(GeneralizedTime::from(end_time))),
renew_till: Optional::from(None),
srealm: ExplicitContextTag9::from(realm.clone()),
sname: ExplicitContextTag10::from(sname.clone()),
caddr: Optional::from(None),
encrypted_pa_data: Optional::from(None),
});
let tgs_rep_enc_data = cipher
.encrypt(
&initial_key,
tgs_rep_key_usage,
&picky_asn1_der::to_vec(&tgs_rep_enc_part).unwrap(),
)
.unwrap();
Ok(TgsRep::from(KdcRep {
pvno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])),
msg_type: ExplicitContextTag1::from(IntegerAsn1::from(vec![TGS_REP_MSG_TYPE])),
padata: Optional::from(None),
crealm: ExplicitContextTag3::from(realm.clone()),
cname: ExplicitContextTag4::from(cname.clone()),
ticket: ExplicitContextTag5::from(Self::make_ticket(
realm,
session_key,
&ticket_enc_key,
kdc_options.0,
sname,
cname,
)),
enc_part: ExplicitContextTag6::from(EncryptedData {
etype: ExplicitContextTag0::from(IntegerAsn1::from(vec![AES256_CTS_HMAC_SHA1_96 as u8])),
kvno: Optional::from(None),
cipher: ExplicitContextTag2::from(OctetStringAsn1::from(tgs_rep_enc_data)),
}),
}))
}
}
@@ -0,0 +1,796 @@
#![allow(clippy::result_large_err)]
mod context_validator;
pub(super) mod kdc;
pub(super) mod network_client;
use std::collections::{HashMap, HashSet};
use std::panic;
use picky_asn1::restricted_string::IA5String;
use picky_asn1::wrapper::{Asn1SequenceOf, ExplicitContextTag0, ExplicitContextTag1, IntegerAsn1};
use picky_krb::constants::types::{NT_PRINCIPAL, NT_SRV_INST};
use picky_krb::data_types::{KerberosStringAsn1, PrincipalName};
use picky_krb::gss_api::MechTypeList;
use sspi::credssp::SspiContext;
use sspi::kerberos::ServerProperties;
use sspi::network_client::NetworkClient;
use sspi::{
AuthIdentity, BufferType, ClientRequestFlags, Credentials, CredentialsBuffers, DataRepresentation, Kerberos,
KerberosConfig, KerberosServerConfig, Negotiate, NegotiateConfig, NegotiatedProtocol, SecurityBuffer,
SecurityStatus, ServerRequestFlags, Sspi, SspiImpl, Username,
};
use url::Url;
use crate::client_server::kerberos::context_validator::{
EmptySspiContextValidator, SpnegoKerberosContextValidator, SpnegoKerberosNtlmFallbackValidator,
SpnegoServerNtlmFallbackValidator, SspiContextValidator,
};
use crate::client_server::kerberos::kdc::{
CLIENT_COMPUTER_NAME, KDC_URL, KdcMock, MAX_TIME_SKEW, PasswordCreds, SERVER_COMPUTER_NAME, UserName, Validators,
};
use crate::client_server::kerberos::network_client::{FailedNetworkClientMock, NetworkClientMock};
use crate::client_server::{test_encryption, test_rpc_request_encryption, test_stream_buffer_encryption};
/// Represents a Kerberos environment:
/// * user and services keys;
/// * user logon credentials;
/// * realm and target application service name;
///
/// It is used for simplifying tests environment preparation.
pub(super) struct KrbEnvironment {
pub keys: HashMap<UserName, Vec<u8>>,
pub users: HashMap<UserName, PasswordCreds>,
pub credentials: Credentials,
pub realm: String,
pub target_name: String,
pub target_service_name: PrincipalName,
}
/// Initializes a Kerberos environment. It includes:
/// * User logon credentials (password-based).
/// * Kerberos services keys.
/// * Target machine name.
pub(super) fn init_krb_environment() -> KrbEnvironment {
let username = "pw13";
let user_password = "qweQWE123!@#";
let domain = "EXAMPLE";
let realm = "EXAMPLE.COM";
let mut salt = realm.to_string();
salt.push_str(username);
let krbtgt = "krbtgt";
let termsrv = "TERMSRV";
let target_machine_name = "DESKTOP-8F33RFH.example.com";
let mut target_name = termsrv.to_string();
target_name.push('/');
target_name.push_str(target_machine_name);
let tgt_service_key = vec![
199, 133, 201, 239, 57, 139, 61, 128, 71, 236, 217, 130, 250, 148, 117, 193, 197, 86, 155, 11, 92, 124, 232,
146, 3, 14, 158, 220, 113, 63, 110, 230,
];
let application_service_key = vec![
168, 29, 77, 196, 211, 88, 148, 180, 123, 188, 196, 182, 173, 30, 249, 191, 89, 35, 44, 56, 20, 217, 132, 131,
89, 144, 33, 79, 16, 91, 126, 72,
];
let keys = [
(
UserName(PrincipalName {
name_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![NT_SRV_INST])),
name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(vec![
KerberosStringAsn1::from(IA5String::from_string(krbtgt.into()).unwrap()),
KerberosStringAsn1::from(IA5String::from_string(domain.into()).unwrap()),
])),
}),
tgt_service_key.clone(),
),
(
UserName(PrincipalName {
name_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![NT_SRV_INST])),
name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(vec![
KerberosStringAsn1::from(IA5String::from_string(krbtgt.into()).unwrap()),
KerberosStringAsn1::from(IA5String::from_string(realm.to_string()).unwrap()),
])),
}),
tgt_service_key,
),
(
UserName(PrincipalName {
name_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![NT_SRV_INST])),
name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(vec![
KerberosStringAsn1::from(IA5String::from_string(termsrv.into()).unwrap()),
KerberosStringAsn1::from(IA5String::from_string(target_machine_name.into()).unwrap()),
])),
}),
application_service_key,
),
]
.into_iter()
.collect();
let users = [(
UserName(PrincipalName {
name_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![NT_PRINCIPAL])),
name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(vec![KerberosStringAsn1::from(
IA5String::from_string(username.into()).unwrap(),
)])),
}),
PasswordCreds {
password: user_password.as_bytes().to_vec(),
salt,
},
)]
.into_iter()
.collect();
let credentials = Credentials::AuthIdentity(AuthIdentity {
username: Username::new_down_level_logon_name(username, domain).unwrap(),
password: user_password.to_owned().into(),
});
KrbEnvironment {
keys,
users,
realm: realm.to_string(),
credentials,
target_name,
target_service_name: PrincipalName {
name_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![NT_SRV_INST])),
name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(vec![
KerberosStringAsn1::from(IA5String::from_string("TERMSRV".into()).unwrap()),
KerberosStringAsn1::from(IA5String::from_string("DESKTOP-8F33RFH.example.com".into()).unwrap()),
])),
},
}
}
/// Does all preparations and calls the [initialize_security_context_impl] function
/// on the provided Kerberos context.
pub(super) fn initialize_security_context(
client: &mut SspiContext,
credentials_handle: &mut Option<CredentialsBuffers>,
flags: ClientRequestFlags,
target_name: &str,
in_token: Vec<u8>,
network_client: &mut dyn NetworkClient,
) -> (SecurityStatus, Vec<u8>) {
let mut input_token = [SecurityBuffer::new(in_token, BufferType::Token)];
let mut output_token = vec![SecurityBuffer::new(Vec::with_capacity(1024), BufferType::Token)];
let mut builder = client
.initialize_security_context()
.with_credentials_handle(credentials_handle)
.with_context_requirements(flags)
.with_target_data_representation(DataRepresentation::Native)
.with_target_name(target_name)
.with_input(&mut input_token)
.with_output(&mut output_token);
let result = client
.initialize_security_context_impl(&mut builder)
.expect("Kerberos initialize_security_context should not fail")
.resolve_with_client(network_client)
.expect("Kerberos initialize_security_context should not fail");
(result.status, output_token.remove(0).buffer)
}
/// Does all preparations and calls the [accept_security_context] function
/// on the provided Kerberos context.
pub(super) fn accept_security_context(
server: &mut SspiContext,
credentials_handle: &mut Option<CredentialsBuffers>,
flags: ServerRequestFlags,
in_token: Vec<u8>,
network_client: &mut dyn NetworkClient,
) -> (SecurityStatus, Vec<u8>) {
let mut input_token = [SecurityBuffer::new(in_token, BufferType::Token)];
let mut output_token = vec![SecurityBuffer::new(Vec::with_capacity(1024), BufferType::Token)];
let builder = server
.accept_security_context()
.with_credentials_handle(credentials_handle)
.with_context_requirements(flags)
.with_target_data_representation(DataRepresentation::Native)
.with_input(&mut input_token)
.with_output(&mut output_token);
let result = server
.accept_security_context_impl(builder)
.expect("Kerberos accept_security_context should not fail")
.resolve_with_client(network_client)
.expect("Kerberos accept_security_context should not fail");
(result.status, output_token.remove(0).buffer)
}
#[expect(clippy::too_many_arguments, reason = "many arguments are acceptable in test helpers")]
fn run_kerberos(
client: &mut SspiContext,
client_credentials_handle: &mut Option<CredentialsBuffers>,
client_flags: ClientRequestFlags,
target_name: &str,
server: &mut SspiContext,
server_credentials_handle: &mut Option<CredentialsBuffers>,
server_flags: ServerRequestFlags,
network_client: &mut dyn NetworkClient,
steps: usize,
mut context_validator: impl SspiContextValidator,
) {
let mut client_in_token = Vec::new();
for step in 0..steps {
let (client_status, token) = initialize_security_context(
client,
client_credentials_handle,
client_flags,
target_name,
client_in_token,
network_client,
);
context_validator.validate_client(step, client);
if client_status == SecurityStatus::Ok {
if !token.is_empty() {
accept_security_context(server, server_credentials_handle, server_flags, token, network_client);
}
test_encryption(client, server);
test_stream_buffer_encryption(client, server);
test_rpc_request_encryption(client, server);
return;
}
let (_, token) =
accept_security_context(server, server_credentials_handle, server_flags, token, network_client);
client_in_token = token;
}
panic!("Kerberos authentication should not exceed {steps} steps");
}
#[test]
fn kerberos_auth() {
let KrbEnvironment {
realm,
credentials,
keys,
users,
target_name,
target_service_name,
} = init_krb_environment();
let ticket_decryption_key = keys[&UserName(target_service_name.clone())].clone();
let kdc = KdcMock::new(
realm,
keys,
users,
Validators {
as_req: Box::new(|_as_req| {
// Nothing to validate in AsReq.
}),
tgs_req: Box::new(|tgs_req| {
// Here, we should check that the Kerberos client does not negotiated Kerberos U2U auth and not enabled any unneeded flags.
let kdc_options = tgs_req.0.req_body.kdc_options.0.0.as_bytes();
// enc-tkt-in-skey must be disabled.
assert_eq!(kdc_options[4], 0x00, "some unneeded KDC options are enabled");
let additional_tickets = tgs_req
.0
.req_body
.0
.additional_tickets
.0
.as_ref()
.map(|additional_tickets| additional_tickets.0.0.as_slice());
assert!(
matches!(additional_tickets, None | Some(&[])),
"TgsReq should not contain any additional tickets"
);
}),
},
);
let mut network_client = NetworkClientMock { kdc };
let client_config = KerberosConfig {
kdc_url: Some(Url::parse(KDC_URL).unwrap()),
client_computer_name: CLIENT_COMPUTER_NAME.into(),
};
let kerberos_client = Kerberos::new_client_from_config(client_config).unwrap();
let server_config = KerberosConfig {
kdc_url: Some(Url::parse(KDC_URL).unwrap()),
client_computer_name: SERVER_COMPUTER_NAME.into(),
};
let server_properties = ServerProperties {
mech_types: MechTypeList::from(Vec::new()),
max_time_skew: MAX_TIME_SKEW,
ticket_decryption_key: Some(ticket_decryption_key.into()),
service_name: target_service_name,
additional_service_keys: Vec::new(),
user: None,
client: None,
authenticators_cache: HashSet::new(),
};
let kerberos_server = Kerberos::new_server_from_config(server_config, server_properties).unwrap();
let credentials = CredentialsBuffers::try_from(credentials).unwrap();
let mut client_credentials_handle = Some(credentials.clone());
let mut server_credentials_handle = Some(credentials);
let client_flags = ClientRequestFlags::MUTUAL_AUTH
| ClientRequestFlags::INTEGRITY
| ClientRequestFlags::SEQUENCE_DETECT
| ClientRequestFlags::REPLAY_DETECT
| ClientRequestFlags::CONFIDENTIALITY;
let server_flags = ServerRequestFlags::MUTUAL_AUTH
| ServerRequestFlags::INTEGRITY
| ServerRequestFlags::SEQUENCE_DETECT
| ServerRequestFlags::REPLAY_DETECT
| ServerRequestFlags::CONFIDENTIALITY;
run_kerberos(
&mut SspiContext::Kerberos(kerberos_client),
&mut client_credentials_handle,
client_flags,
&target_name,
&mut SspiContext::Kerberos(kerberos_server),
&mut server_credentials_handle,
server_flags,
&mut network_client,
2,
EmptySspiContextValidator,
);
}
#[test]
fn spnego_kerberos_u2u() {
let KrbEnvironment {
realm,
credentials,
keys,
users,
target_name,
target_service_name,
} = init_krb_environment();
let ticket_decryption_key = keys[&UserName(target_service_name.clone())].clone();
let identity_1 = credentials.to_auth_identity().unwrap();
let mut identity_2 = identity_1.clone();
identity_2.username = Username::new_upn(identity_1.username.account_name(), &realm.to_ascii_lowercase()).unwrap();
let kdc = KdcMock::new(
realm,
keys,
users,
Validators {
as_req: Box::new(|_as_req| {
// Nothing to validate in AsReq.
}),
tgs_req: Box::new(|_tgs_req| {
// Nothing to validate in TgsReq.
//
// Previously, we were able to validate the presence of the additional ticket and enc-tkt-in-skey flag.
// But since we use a preflight Kerberos exchange to check if the Kerberos is possible,
// we can no longer do that.
}),
},
);
let mut network_client = NetworkClientMock { kdc };
let client_config = KerberosConfig {
kdc_url: Some(Url::parse(KDC_URL).unwrap()),
client_computer_name: CLIENT_COMPUTER_NAME.into(),
};
let spnego_client = Negotiate::new_client(NegotiateConfig::new(
Box::new(client_config.clone()),
Some(String::from("kerberos,!ntlm")),
CLIENT_COMPUTER_NAME.into(),
))
.unwrap();
let credentials = CredentialsBuffers::try_from(credentials).unwrap();
let server_config = KerberosConfig {
kdc_url: Some(Url::parse(KDC_URL).unwrap()),
client_computer_name: SERVER_COMPUTER_NAME.into(),
};
let server_properties = ServerProperties {
mech_types: MechTypeList::from(Vec::new()),
max_time_skew: MAX_TIME_SKEW,
ticket_decryption_key: Some(ticket_decryption_key.into()),
service_name: target_service_name,
additional_service_keys: Vec::new(),
user: Some(credentials.clone()),
client: None,
authenticators_cache: HashSet::new(),
};
let kerberos_server_config = KerberosServerConfig {
kerberos_config: server_config,
server_properties,
};
let spnego_server = Negotiate::new_server(
NegotiateConfig::new(
Box::new(kerberos_server_config),
Some(String::from("kerberos,!ntlm")),
SERVER_COMPUTER_NAME.into(),
),
vec![identity_1, identity_2],
)
.unwrap();
let mut client_credentials_handle = Some(credentials.clone());
let mut server_credentials_handle = Some(credentials);
let client_flags = ClientRequestFlags::MUTUAL_AUTH
| ClientRequestFlags::INTEGRITY
| ClientRequestFlags::USE_SESSION_KEY // Kerberos U2U auth
| ClientRequestFlags::SEQUENCE_DETECT
| ClientRequestFlags::REPLAY_DETECT
| ClientRequestFlags::CONFIDENTIALITY;
let server_flags = ServerRequestFlags::MUTUAL_AUTH
| ServerRequestFlags::INTEGRITY
| ServerRequestFlags::USE_SESSION_KEY // Kerberos U2U auth
| ServerRequestFlags::SEQUENCE_DETECT
| ServerRequestFlags::REPLAY_DETECT
| ServerRequestFlags::CONFIDENTIALITY;
run_kerberos(
&mut SspiContext::Negotiate(spnego_client),
&mut client_credentials_handle,
client_flags,
&target_name,
&mut SspiContext::Negotiate(spnego_server),
&mut server_credentials_handle,
server_flags,
&mut network_client,
3,
SpnegoKerberosContextValidator,
);
}
fn run_spnego(
client_flags: ClientRequestFlags,
server_flags: ServerRequestFlags,
steps: usize,
get_network_client: impl Fn(KdcMock) -> Box<dyn NetworkClient>,
client_package_list: Option<String>,
server_package_list: Option<String>,
context_validator: impl SspiContextValidator,
) -> (SspiContext, SspiContext) {
let KrbEnvironment {
realm,
credentials,
keys,
users,
target_name,
target_service_name,
} = init_krb_environment();
let ticket_decryption_key = keys[&UserName(target_service_name.clone())].clone();
let identity_1 = credentials.to_auth_identity().unwrap();
let mut identity_2 = identity_1.clone();
identity_2.username = Username::new_upn(identity_1.username.account_name(), &realm.to_ascii_lowercase()).unwrap();
let kdc = KdcMock::new(
realm,
keys,
users,
Validators {
as_req: Box::new(|_as_req| {
// Nothing to validate in AsReq.
}),
tgs_req: Box::new(|_tgs_req| {
// Nothing to validate in TgsReq.
}),
},
);
let mut network_client = get_network_client(kdc);
let client_config = KerberosConfig {
kdc_url: Some(Url::parse(KDC_URL).unwrap()),
client_computer_name: CLIENT_COMPUTER_NAME.into(),
};
let mut spnego_client = SspiContext::Negotiate(
Negotiate::new_client(NegotiateConfig::new(
Box::new(client_config.clone()),
client_package_list.clone(),
CLIENT_COMPUTER_NAME.into(),
))
.unwrap(),
);
let server_config = KerberosConfig {
kdc_url: Some(Url::parse(KDC_URL).unwrap()),
client_computer_name: CLIENT_COMPUTER_NAME.into(),
};
let server_properties = ServerProperties {
mech_types: MechTypeList::from(Vec::new()),
max_time_skew: MAX_TIME_SKEW,
ticket_decryption_key: Some(ticket_decryption_key.into()),
service_name: target_service_name,
additional_service_keys: Vec::new(),
user: None,
client: None,
authenticators_cache: HashSet::new(),
};
let kerberos_server_config = KerberosServerConfig {
kerberos_config: server_config,
server_properties,
};
let mut spnego_server = SspiContext::Negotiate(
Negotiate::new_server(
NegotiateConfig::new(
Box::new(kerberos_server_config),
server_package_list.clone(),
SERVER_COMPUTER_NAME.into(),
),
vec![identity_1, identity_2],
)
.unwrap(),
);
let credentials = CredentialsBuffers::try_from(credentials).unwrap();
let mut client_credentials_handle = Some(credentials.clone());
let mut server_credentials_handle = Some(credentials);
run_kerberos(
&mut spnego_client,
&mut client_credentials_handle,
client_flags,
&target_name,
&mut spnego_server,
&mut server_credentials_handle,
server_flags,
&mut *network_client,
steps,
context_validator,
);
(spnego_client, spnego_server)
}
#[test]
fn spnego_kerberos_1() {
let client_flags = ClientRequestFlags::MUTUAL_AUTH
| ClientRequestFlags::INTEGRITY
| ClientRequestFlags::SEQUENCE_DETECT
| ClientRequestFlags::REPLAY_DETECT
| ClientRequestFlags::CONFIDENTIALITY;
let server_flags = ServerRequestFlags::MUTUAL_AUTH
| ServerRequestFlags::INTEGRITY
| ServerRequestFlags::SEQUENCE_DETECT
| ServerRequestFlags::REPLAY_DETECT
| ServerRequestFlags::CONFIDENTIALITY;
let package_list = Some(String::from("kerberos,ntlm"));
let (client, _server) = run_spnego(
client_flags,
server_flags,
3,
|kdc| Box::new(NetworkClientMock { kdc }),
package_list.clone(),
package_list,
SpnegoKerberosContextValidator,
);
let SspiContext::Negotiate(negotiate) = client else {
panic!("client must be a Negotiate context");
};
let negotiated_protocol = negotiate.negotiated_protocol();
assert!(matches!(negotiated_protocol, NegotiatedProtocol::Kerberos(_)),);
}
#[test]
fn spnego_kerberos_dce_style() {
let client_flags = ClientRequestFlags::MUTUAL_AUTH
| ClientRequestFlags::INTEGRITY
| ClientRequestFlags::USE_DCE_STYLE
| ClientRequestFlags::SEQUENCE_DETECT
| ClientRequestFlags::REPLAY_DETECT
| ClientRequestFlags::CONFIDENTIALITY;
let server_flags = ServerRequestFlags::MUTUAL_AUTH
| ServerRequestFlags::INTEGRITY
| ServerRequestFlags::USE_DCE_STYLE
| ServerRequestFlags::SEQUENCE_DETECT
| ServerRequestFlags::REPLAY_DETECT
| ServerRequestFlags::CONFIDENTIALITY;
let package_list = Some(String::from("kerberos,ntlm"));
let (client, _server) = run_spnego(
client_flags,
server_flags,
4,
|kdc| Box::new(NetworkClientMock { kdc }),
package_list.clone(),
package_list,
SpnegoKerberosContextValidator,
);
let SspiContext::Negotiate(negotiate) = client else {
panic!("client must be a Negotiate context");
};
let negotiated_protocol = negotiate.negotiated_protocol();
assert!(matches!(negotiated_protocol, NegotiatedProtocol::Kerberos(_)),);
}
#[test]
fn spnego_kerberos_ntlm_fallback() {
let client_flags = ClientRequestFlags::MUTUAL_AUTH
| ClientRequestFlags::INTEGRITY
| ClientRequestFlags::SEQUENCE_DETECT
| ClientRequestFlags::REPLAY_DETECT
| ClientRequestFlags::CONFIDENTIALITY;
let server_flags = ServerRequestFlags::MUTUAL_AUTH
| ServerRequestFlags::INTEGRITY
| ServerRequestFlags::SEQUENCE_DETECT
| ServerRequestFlags::REPLAY_DETECT
| ServerRequestFlags::CONFIDENTIALITY;
let package_list = Some(String::from("kerberos,ntlm"));
for kind in sspi::FALLBACK_ERROR_KINDS {
let (client, _server) = run_spnego(
client_flags,
server_flags,
4,
|_| Box::new(FailedNetworkClientMock { kind }),
package_list.clone(),
package_list.clone(),
SpnegoKerberosNtlmFallbackValidator,
);
let SspiContext::Negotiate(negotiate) = client else {
panic!("client must be a Negotiate context");
};
let negotiated_protocol = negotiate.negotiated_protocol();
assert!(
matches!(negotiated_protocol, NegotiatedProtocol::Ntlm(_)),
"Client should fallback to NTLM if Kerberos fails with {kind:?} error"
);
}
}
#[test]
fn spnego_kerberos_server_ntlm_fallback() {
let client_flags = ClientRequestFlags::MUTUAL_AUTH
| ClientRequestFlags::INTEGRITY
| ClientRequestFlags::SEQUENCE_DETECT
| ClientRequestFlags::REPLAY_DETECT
| ClientRequestFlags::CONFIDENTIALITY;
let server_flags = ServerRequestFlags::MUTUAL_AUTH
| ServerRequestFlags::INTEGRITY
| ServerRequestFlags::SEQUENCE_DETECT
| ServerRequestFlags::REPLAY_DETECT
| ServerRequestFlags::CONFIDENTIALITY;
let client_package_list = Some(String::from("kerberos,ntlm"));
let server_package_list = Some(String::from("!kerberos,ntlm"));
let (client, _server) = run_spnego(
client_flags,
server_flags,
4,
|kdc| Box::new(NetworkClientMock { kdc }),
client_package_list,
server_package_list,
SpnegoServerNtlmFallbackValidator,
);
let SspiContext::Negotiate(negotiate) = client else {
panic!("client must be a Negotiate context");
};
let negotiated_protocol = negotiate.negotiated_protocol();
assert!(matches!(negotiated_protocol, NegotiatedProtocol::Ntlm(_)),);
}
// This test ensures that the client falls back to NTLM when the SPN is an IP address, which is not supported by Kerberos.
#[test]
fn spnego_kerberos_ntlm_fallback_spn_ip_address() {
let KrbEnvironment {
realm,
credentials,
keys,
users,
target_name: _,
target_service_name,
} = init_krb_environment();
let ticket_decryption_key = keys[&UserName(target_service_name.clone())].clone();
let identity_1 = credentials.to_auth_identity().unwrap();
let mut identity_2 = identity_1.clone();
identity_2.username = Username::new_upn(identity_1.username.account_name(), &realm.to_ascii_lowercase()).unwrap();
let kdc = KdcMock::new(
realm,
keys,
users,
Validators {
as_req: Box::new(|_as_req| {
panic!("AS_REQ should not be sent when the SPN is an IP address");
}),
tgs_req: Box::new(|_tgs_req| {
panic!("TGS_REQ should not be sent when the SPN is an IP address");
}),
},
);
let mut network_client = NetworkClientMock { kdc };
let client_config = KerberosConfig {
kdc_url: Some(Url::parse(KDC_URL).unwrap()),
client_computer_name: CLIENT_COMPUTER_NAME.into(),
};
let spnego_client = Negotiate::new_client(NegotiateConfig::new(
Box::new(client_config.clone()),
Some(String::from("kerberos,ntlm")),
CLIENT_COMPUTER_NAME.into(),
))
.unwrap();
let credentials = CredentialsBuffers::try_from(credentials).unwrap();
let server_config = KerberosConfig {
kdc_url: Some(Url::parse(KDC_URL).unwrap()),
client_computer_name: SERVER_COMPUTER_NAME.into(),
};
let server_properties = ServerProperties {
mech_types: MechTypeList::from(Vec::new()),
max_time_skew: MAX_TIME_SKEW,
ticket_decryption_key: Some(ticket_decryption_key.into()),
service_name: target_service_name,
additional_service_keys: Vec::new(),
user: Some(credentials.clone()),
client: None,
authenticators_cache: HashSet::new(),
};
let kerberos_server_config = KerberosServerConfig {
kerberos_config: server_config,
server_properties,
};
let spnego_server = Negotiate::new_server(
NegotiateConfig::new(
Box::new(kerberos_server_config),
Some(String::from("kerberos,ntlm")),
SERVER_COMPUTER_NAME.into(),
),
vec![identity_1, identity_2],
)
.unwrap();
let mut client_credentials_handle = Some(credentials.clone());
let mut server_credentials_handle = Some(credentials);
let client_flags = ClientRequestFlags::MUTUAL_AUTH
| ClientRequestFlags::INTEGRITY
| ClientRequestFlags::USE_SESSION_KEY
| ClientRequestFlags::SEQUENCE_DETECT
| ClientRequestFlags::REPLAY_DETECT
| ClientRequestFlags::CONFIDENTIALITY;
let server_flags = ServerRequestFlags::MUTUAL_AUTH
| ServerRequestFlags::INTEGRITY
| ServerRequestFlags::USE_SESSION_KEY
| ServerRequestFlags::SEQUENCE_DETECT
| ServerRequestFlags::REPLAY_DETECT
| ServerRequestFlags::CONFIDENTIALITY;
run_kerberos(
&mut SspiContext::Negotiate(spnego_client),
&mut client_credentials_handle,
client_flags,
// The client must fallback to NTLM when the SPN is an IP address.
"TERMSRV/192.168.1.104",
&mut SspiContext::Negotiate(spnego_server),
&mut server_credentials_handle,
server_flags,
&mut network_client,
3,
SpnegoKerberosNtlmFallbackValidator,
);
}
@@ -0,0 +1,53 @@
use sspi::generator::NetworkRequest;
use sspi::network_client::NetworkClient;
use sspi::{ErrorKind, Result};
use crate::client_server::kerberos::kdc::KdcMock;
/// [NetworkClient] mock implementation.
///
/// Instead of sending Kerberos messages to the KDC service,
/// it redirects them to the KDC mock implementation.
pub(crate) struct NetworkClientMock {
pub kdc: KdcMock,
}
impl NetworkClient for NetworkClientMock {
fn send(&self, request: &NetworkRequest) -> Result<Vec<u8>> {
let data = &request.data[4..];
let response = if let Ok(as_req) = picky_asn1_der::from_bytes(data) {
match self.kdc.as_exchange(as_req) {
Ok(as_rep) => picky_asn1_der::to_vec(&as_rep)?,
Err(krb_err) => picky_asn1_der::to_vec(&krb_err)?,
}
} else if let Ok(tgs_req) = picky_asn1_der::from_bytes(data) {
match self.kdc.tgs_exchange(tgs_req) {
Ok(tgs_rep) => picky_asn1_der::to_vec(&tgs_rep)?,
Err(krb_err) => picky_asn1_der::to_vec(&krb_err)?,
}
} else {
panic!("Invalid Kerberos message: {:?}", request.data);
};
let mut data = vec![0; 4 + response.len()];
data[0..4].copy_from_slice(&u32::try_from(response.len()).unwrap().to_be_bytes());
data[4..].copy_from_slice(&response);
Ok(data)
}
}
/// [NetworkClient] that returns an error for every request.
///
/// The purpose of this specific mock is to test Kerberos to NTLM fallback in Negotiate (SPNEGO).
pub(crate) struct FailedNetworkClientMock {
/// Error kind to return for every request.
pub kind: ErrorKind,
}
impl NetworkClient for FailedNetworkClientMock {
fn send(&self, _request: &NetworkRequest) -> Result<Vec<u8>> {
Err(sspi::Error::new(self.kind, "error from mock network client"))
}
}
+98
View File
@@ -0,0 +1,98 @@
// The network_client and __test-data features are required for the client_server tests.
#![cfg(all(feature = "network_client", feature = "__test-data"))]
mod credssp;
mod kerberos;
mod negotiate;
mod ntlm;
use sspi::credssp::SspiContext;
use sspi::{EncryptionFlags, SecurityBufferFlags, SecurityBufferRef, Sspi};
const TARGET_NAME: &str = "TERMSRV/DESKTOP-8F33RFH.example.com";
fn test_encryption(client: &mut SspiContext, server: &mut SspiContext) {
let plain_message = b"Devolutions/sspi-rs";
let mut token = [0; 1024];
let mut data = plain_message.to_vec();
let mut message = vec![
SecurityBufferRef::token_buf(token.as_mut_slice()),
SecurityBufferRef::data_buf(data.as_mut_slice()),
];
client.encrypt_message(EncryptionFlags::empty(), &mut message).unwrap();
server.decrypt_message(&mut message).unwrap();
assert_eq!(plain_message, message[1].data());
}
fn test_stream_buffer_encryption(client: &mut SspiContext, server: &mut SspiContext) {
// https://learn.microsoft.com/en-us/windows/win32/secauthn/sspi-kerberos-interoperability-with-gssapi
let plain_message = b"Devolutions/sspi-rs";
let mut token = [0; 1024];
let mut data = plain_message.to_vec();
let mut message = [
SecurityBufferRef::token_buf(token.as_mut_slice()),
SecurityBufferRef::data_buf(data.as_mut_slice()),
];
client.encrypt_message(EncryptionFlags::empty(), &mut message).unwrap();
let mut buffer = message[0].data().to_vec();
buffer.extend_from_slice(message[1].data());
let mut message = [
SecurityBufferRef::stream_buf(&mut buffer),
SecurityBufferRef::data_buf(&mut []),
];
server.decrypt_message(&mut message).unwrap();
assert_eq!(message[1].data(), plain_message);
}
fn test_rpc_request_encryption(client: &mut SspiContext, server: &mut SspiContext) {
// RPC header
let header = [
5, 0, 0, 3, 16, 0, 0, 0, 60, 1, 76, 0, 1, 0, 0, 0, 208, 0, 0, 0, 0, 0, 0, 0,
];
// Unencrypted data in RPC Request
let plaintext = [
108, 0, 0, 0, 0, 0, 0, 0, 108, 0, 0, 0, 0, 0, 0, 0, 1, 0, 4, 128, 84, 0, 0, 0, 96, 0, 0, 0, 0, 0, 0, 0, 20, 0,
0, 0, 2, 0, 64, 0, 2, 0, 0, 0, 0, 0, 36, 0, 3, 0, 0, 0, 1, 5, 0, 0, 0, 0, 0, 5, 21, 0, 0, 0, 223, 243, 137, 88,
86, 131, 83, 53, 105, 218, 109, 33, 80, 4, 0, 0, 0, 0, 20, 0, 2, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0,
1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 138, 227, 19, 113, 2, 244, 54, 113, 2, 64, 40, 0,
96, 89, 120, 185, 79, 82, 223, 17, 139, 109, 131, 220, 222, 215, 32, 133, 1, 0, 0, 0, 51, 5, 113, 113, 186,
190, 55, 73, 131, 25, 181, 219, 239, 156, 204, 54, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];
// RPC security trailer header
let trailer = [16, 6, 8, 0, 0, 0, 0, 0];
let mut header_data = header.to_vec();
let mut data = plaintext.to_vec();
let mut trailer_data = trailer.to_vec();
let mut token_data = vec![0; 76];
let mut message = vec![
SecurityBufferRef::data_buf(&mut header_data).with_flags(SecurityBufferFlags::SECBUFFER_READONLY_WITH_CHECKSUM),
SecurityBufferRef::data_buf(&mut data),
SecurityBufferRef::data_buf(&mut trailer_data)
.with_flags(SecurityBufferFlags::SECBUFFER_READONLY_WITH_CHECKSUM),
SecurityBufferRef::token_buf(&mut token_data),
];
client.encrypt_message(EncryptionFlags::empty(), &mut message).unwrap();
assert_eq!(header[..], message[0].data()[..]);
assert_eq!(trailer[..], message[2].data()[..]);
server.decrypt_message(&mut message).unwrap();
assert_eq!(header[..], message[0].data()[..]);
assert_eq!(message[1].data(), plaintext);
assert_eq!(trailer[..], message[2].data()[..]);
}
+135
View File
@@ -0,0 +1,135 @@
use sspi::builders::{AcquireCredentialsHandle, WithoutCredentialUse};
use sspi::credssp::SspiContext;
use sspi::ntlm::NtlmConfig;
use sspi::{
AcquireCredentialsHandleResult, AuthIdentity, BufferType, ClientRequestFlags, CredentialUse, Credentials,
DataRepresentation, InitializeSecurityContextResult, Negotiate, NegotiateConfig, Secret, SecurityBuffer,
SecurityStatus, ServerRequestFlags, Sspi, Username,
};
use crate::client_server::{TARGET_NAME, test_encryption, test_rpc_request_encryption, test_stream_buffer_encryption};
const CLIENT_COMPUTER_NAME: &str = "DESKTOP-IHPPQ95.example.com";
fn run_spnego_ntlm(target_name: Option<&str>, username: &str, password: &str, mic_expected: bool) {
let ntlm_config = NtlmConfig {
client_computer_name: Some(CLIENT_COMPUTER_NAME.to_owned()),
};
let credentials = Credentials::AuthIdentity(AuthIdentity {
username: Username::parse(username).unwrap(),
password: Secret::from(password.to_owned()),
});
let mut client = SspiContext::Negotiate(
Negotiate::new_client(NegotiateConfig::new(
Box::new(ntlm_config.clone()),
Some(String::from("ntlm,!kerberos")),
CLIENT_COMPUTER_NAME.into(),
))
.unwrap(),
);
let mut server = SspiContext::Negotiate(
Negotiate::new_server(
NegotiateConfig::new(
Box::new(ntlm_config),
Some(String::from("ntlm,!kerberos")),
"WIN-956CQOSSJTF.example.com".into(),
),
vec![credentials.to_auth_identity().unwrap()],
)
.unwrap(),
);
let builder = AcquireCredentialsHandle::<'_, _, _, WithoutCredentialUse>::new();
let AcquireCredentialsHandleResult {
credentials_handle: mut client_credentials_handle,
..
} = builder
.with_auth_data(&credentials)
.with_credential_use(CredentialUse::Outbound)
.execute(&mut client)
.unwrap();
let builder = AcquireCredentialsHandle::<'_, _, _, WithoutCredentialUse>::new();
let AcquireCredentialsHandleResult {
credentials_handle: mut server_credentials_handle,
..
} = builder
.with_auth_data(&credentials)
.with_credential_use(CredentialUse::Inbound)
.execute(&mut server)
.unwrap();
let mut input_token = [SecurityBuffer::new(Vec::new(), BufferType::Token)];
let mut output_token = [SecurityBuffer::new(Vec::new(), BufferType::Token)];
for _ in 0..4 {
let mut builder = client
.initialize_security_context()
.with_credentials_handle(&mut client_credentials_handle)
.with_context_requirements(
ClientRequestFlags::MUTUAL_AUTH
| ClientRequestFlags::USE_SESSION_KEY
| ClientRequestFlags::INTEGRITY
| ClientRequestFlags::CONFIDENTIALITY,
)
.with_target_data_representation(DataRepresentation::Native)
.with_input(&mut input_token)
.with_output(&mut output_token);
builder.target_name = target_name;
let InitializeSecurityContextResult { status, .. } =
client.initialize_security_context_sync(&mut builder).unwrap();
input_token[0].buffer.clear();
if !output_token[0].buffer.is_empty() {
let builder = server
.accept_security_context()
.with_credentials_handle(&mut server_credentials_handle)
.with_context_requirements(ServerRequestFlags::empty())
.with_target_data_representation(DataRepresentation::Native)
.with_input(&mut output_token)
.with_output(&mut input_token);
server.accept_security_context_sync(builder).unwrap();
output_token[0].buffer.clear();
}
if status == SecurityStatus::Ok {
let negotiate_client = match &client {
SspiContext::Negotiate(negotiate) => negotiate,
_ => unreachable!(),
};
assert_eq!(negotiate_client.mic_needed(), mic_expected);
let negotiate_server = match &server {
SspiContext::Negotiate(negotiate) => negotiate,
_ => unreachable!(),
};
assert_eq!(negotiate_server.mic_needed(), mic_expected);
test_encryption(&mut client, &mut server);
test_stream_buffer_encryption(&mut client, &mut server);
test_rpc_request_encryption(&mut client, &mut server);
return;
}
}
panic!("SPNEGO authentication should not exceed 4 steps")
}
#[test]
fn spnego_ntlm_client_server() {
run_spnego_ntlm(Some(TARGET_NAME), "test_user@example.com", "test_password", true);
}
#[test]
fn spnego_ntlm_guest_logon() {
run_spnego_ntlm(Some(TARGET_NAME), "/GUEST", "", false);
}
#[test]
fn spnego_ntlm_without_target_name() {
run_spnego_ntlm(None, "test_user@example.com", "test_password", true);
}
+164
View File
@@ -0,0 +1,164 @@
use sspi::builders::{AcquireCredentialsHandle, WithoutCredentialUse};
use sspi::credssp::SspiContext;
use sspi::ntlm::NtlmConfig;
use sspi::{
AcquireCredentialsHandleResult, AuthIdentity, BufferType, ClientRequestFlags, CredentialUse, Credentials,
DataRepresentation, EncryptionFlags, InitializeSecurityContextResult, Ntlm, Secret, SecurityBuffer,
SecurityBufferFlags, SecurityBufferRef, SecurityStatus, ServerRequestFlags, Sspi, Username,
};
use crate::client_server::{TARGET_NAME, test_encryption, test_rpc_request_encryption, test_stream_buffer_encryption};
// Simulates readonly buffers encryption. In that case, the NTLM security package should not encrypt the data,
// but instead compute the checksum and write into the token buffer.
//
// More info: https://github.com/Devolutions/sspi-rs/pull/629.
//
// This is supported only by NTLM security package and do not implemented for Kerberos.
fn test_readonly_buffers_encryption(client: &mut SspiContext, server: &mut SspiContext) {
let plain_message = b"Devolutions/sspi-rs";
let mut token = [0; 1024];
let mut data_1 = plain_message.to_vec();
let mut data_2 = plain_message.to_vec();
let mut message = vec![
SecurityBufferRef::token_buf(token.as_mut_slice()),
SecurityBufferRef::data_buf(data_1.as_mut_slice()).with_flags(SecurityBufferFlags::SECBUFFER_READONLY),
SecurityBufferRef::data_buf(data_2.as_mut_slice())
.with_flags(SecurityBufferFlags::SECBUFFER_READONLY_WITH_CHECKSUM),
];
client.encrypt_message(EncryptionFlags::empty(), &mut message).unwrap();
// Make sure that readonly buffers was not encrypted.
assert_eq!(plain_message, message[1].data());
assert_eq!(plain_message, message[2].data());
server.decrypt_message(&mut message).unwrap();
assert_eq!(plain_message, message[1].data());
assert_eq!(plain_message, message[2].data());
}
fn run_ntlm(config: NtlmConfig, target_name: Option<&str>, username: &str, password: &str) {
let credentials = Credentials::AuthIdentity(AuthIdentity {
username: Username::parse(username).unwrap(),
password: Secret::from(password.to_owned()),
});
let mut client = SspiContext::Ntlm(Ntlm::with_config(config.clone()));
let mut server = SspiContext::Ntlm(Ntlm::with_config(config));
let builder = AcquireCredentialsHandle::<'_, _, _, WithoutCredentialUse>::new();
let AcquireCredentialsHandleResult {
credentials_handle: mut client_credentials_handle,
..
} = builder
.with_auth_data(&credentials)
.with_credential_use(CredentialUse::Outbound)
.execute(&mut client)
.unwrap();
let builder = AcquireCredentialsHandle::<'_, _, _, WithoutCredentialUse>::new();
let AcquireCredentialsHandleResult {
credentials_handle: mut server_credentials_handle,
..
} = builder
.with_auth_data(&credentials)
.with_credential_use(CredentialUse::Inbound)
.execute(&mut server)
.unwrap();
let mut input_token = [SecurityBuffer::new(Vec::new(), BufferType::Token)];
let mut output_token = [SecurityBuffer::new(Vec::new(), BufferType::Token)];
for _ in 0..3 {
let mut builder = client
.initialize_security_context()
.with_credentials_handle(&mut client_credentials_handle)
.with_context_requirements(
ClientRequestFlags::MUTUAL_AUTH
| ClientRequestFlags::USE_SESSION_KEY
| ClientRequestFlags::INTEGRITY
| ClientRequestFlags::CONFIDENTIALITY,
)
.with_target_data_representation(DataRepresentation::Native)
.with_input(&mut input_token)
.with_output(&mut output_token);
builder.target_name = target_name;
let InitializeSecurityContextResult { status, .. } =
client.initialize_security_context_sync(&mut builder).unwrap();
input_token[0].buffer.clear();
let builder = server
.accept_security_context()
.with_credentials_handle(&mut server_credentials_handle)
.with_context_requirements(ServerRequestFlags::empty())
.with_target_data_representation(DataRepresentation::Native)
.with_input(&mut output_token)
.with_output(&mut input_token);
server.accept_security_context_sync(builder).unwrap();
output_token[0].buffer.clear();
if status == SecurityStatus::Ok {
test_encryption(&mut client, &mut server);
test_stream_buffer_encryption(&mut client, &mut server);
test_rpc_request_encryption(&mut client, &mut server);
test_readonly_buffers_encryption(&mut client, &mut server);
return;
}
}
panic!("NTLM authentication should not exceed 3 steps")
}
#[test]
fn ntlm_with_computer_name() {
run_ntlm(
NtlmConfig {
client_computer_name: Some("DESKTOP-3D83IAN.example.com".to_owned()),
},
Some(TARGET_NAME),
"test_user",
"test_password",
);
}
#[test]
fn ntlm_without_computer_name() {
run_ntlm(
NtlmConfig {
client_computer_name: None,
},
Some(TARGET_NAME),
"test_user",
"test_password",
);
}
#[test]
fn ntlm_guest_logon() {
run_ntlm(
NtlmConfig {
client_computer_name: None,
},
Some("cifs/DESKTOP-8F33RFH.example.com"),
"/GUEST",
"",
);
}
#[test]
fn ntlm_without_target_name() {
run_ntlm(
NtlmConfig {
client_computer_name: None,
},
None,
"test_user",
"test_password",
);
}
+210
View File
@@ -0,0 +1,210 @@
use std::io;
use std::sync::LazyLock;
use sspi::{
AcquireCredentialsHandleResult, AuthIdentity, BufferType, ClientRequestFlags, ContextNames, CredentialUse,
DataRepresentation, EncryptionFlags, SecurityBuffer, SecurityBufferRef, SecurityStatus, ServerRequestFlags, Sspi,
SspiEx, Username, credssp,
};
use time::OffsetDateTime;
pub(crate) static CREDENTIALS: LazyLock<AuthIdentity> = LazyLock::new(|| AuthIdentity {
username: Username::new("Username", Some("Domain")).unwrap(),
password: String::from("Password").into(),
});
const MESSAGE_TO_CLIENT: &[u8] = b"Hello, client!";
pub(crate) struct CredentialsProxyImpl<'a> {
credentials: &'a AuthIdentity,
}
impl<'a> CredentialsProxyImpl<'a> {
pub(crate) fn new(credentials: &'a AuthIdentity) -> Self {
Self { credentials }
}
}
impl credssp::CredentialsProxy for CredentialsProxyImpl<'_> {
type AuthenticationData = AuthIdentity;
fn auth_data_by_user(&mut self, username: &Username) -> io::Result<Self::AuthenticationData> {
assert_eq!(username.account_name(), self.credentials.username.account_name());
Ok(self.credentials.clone())
}
fn auth_data(&mut self) -> io::Result<Vec<Self::AuthenticationData>> {
Ok(vec![self.credentials.clone()])
}
}
pub(crate) fn create_client_credentials_handle<T>(
client: &mut T,
auth_data: Option<&T::AuthenticationData>,
) -> sspi::Result<T::CredentialsHandle>
where
T: Sspi,
{
let AcquireCredentialsHandleResult {
credentials_handle,
expiry,
} = if let Some(auth_data) = auth_data {
client
.acquire_credentials_handle()
.with_credential_use(CredentialUse::Outbound)
.with_auth_data(auth_data)
.execute(client)?
} else {
client
.acquire_credentials_handle()
.with_credential_use(CredentialUse::Outbound)
.execute(client)?
};
if let Some(expiry) = expiry {
let now = OffsetDateTime::now_utc();
assert!(now < expiry);
}
Ok(credentials_handle)
}
pub(crate) fn create_server_credentials_handle<T>(server: &mut T) -> sspi::Result<T::CredentialsHandle>
where
T: Sspi,
{
let AcquireCredentialsHandleResult {
credentials_handle,
expiry,
} = server
.acquire_credentials_handle()
.with_credential_use(CredentialUse::Inbound)
.execute(server)?;
if let Some(expiry) = expiry {
let now = OffsetDateTime::now_utc();
assert!(now < expiry);
}
Ok(credentials_handle)
}
pub(crate) fn process_authentication_without_complete<ClientSspi, ServerSspi>(
client: &mut ClientSspi,
mut client_creds_handle: ClientSspi::CredentialsHandle,
server: &mut ServerSspi,
mut server_creds_handle: ServerSspi::CredentialsHandle,
) -> sspi::Result<(SecurityStatus, SecurityStatus)>
where
ClientSspi: Sspi,
ServerSspi: Sspi,
{
let mut server_output = Vec::new();
let mut client_status;
let mut server_status = SecurityStatus::ContinueNeeded;
loop {
let mut client_output = vec![SecurityBuffer::new(Vec::new(), BufferType::Token)];
let mut builder = client
.initialize_security_context()
.with_credentials_handle(&mut client_creds_handle)
.with_context_requirements(ClientRequestFlags::ALLOCATE_MEMORY | ClientRequestFlags::CONFIDENTIALITY)
.with_target_data_representation(DataRepresentation::Native)
.with_input(&mut server_output)
.with_output(&mut client_output);
let client_result = client
.initialize_security_context_impl(&mut builder)?
.resolve_to_result()?;
client_status = client_result.status;
if client_status != SecurityStatus::ContinueNeeded && server_status != SecurityStatus::ContinueNeeded {
return Ok((client_status, server_status));
}
server_output = vec![SecurityBuffer::new(Vec::new(), BufferType::Token)];
let builder = server
.accept_security_context()
.with_credentials_handle(&mut server_creds_handle)
.with_context_requirements(ServerRequestFlags::ALLOCATE_MEMORY)
.with_target_data_representation(DataRepresentation::Native)
.with_input(&mut client_output)
.with_output(&mut server_output);
let server_result = server.accept_security_context_impl(builder)?.resolve_to_result()?;
server_status = server_result.status;
if client_status != SecurityStatus::ContinueNeeded && server_status != SecurityStatus::ContinueNeeded {
return Ok((client_status, server_status));
}
}
}
pub(crate) fn try_complete_authentication<T>(server: &mut T, auth_server_status: SecurityStatus) -> sspi::Result<()>
where
T: Sspi,
{
if auth_server_status == SecurityStatus::CompleteNeeded || auth_server_status == SecurityStatus::CompleteAndContinue
{
let mut token = Vec::new();
server.complete_auth_token(&mut token)?;
}
Ok(())
}
pub(crate) fn set_identity_and_try_complete_authentication<T, C>(
server: &mut T,
auth_server_status: SecurityStatus,
credentials_proxy: &mut C,
) -> sspi::Result<()>
where
T: Sspi + SspiEx,
C: credssp::CredentialsProxy<AuthenticationData = T::AuthenticationData>,
{
if auth_server_status == SecurityStatus::CompleteNeeded || auth_server_status == SecurityStatus::CompleteAndContinue
{
let ContextNames { username } = server.query_context_names()?;
let auth_data = credentials_proxy.auth_data_by_user(&username)?;
server.custom_set_auth_identity(auth_data).unwrap();
let mut token = Vec::new();
server.complete_auth_token(&mut token)?;
}
Ok(())
}
pub(crate) fn check_messages_encryption(client: &mut impl Sspi, server: &mut impl Sspi) -> sspi::Result<()> {
let server_sizes = server.query_context_sizes()?;
let mut token = vec![0; server_sizes.security_trailer as usize];
let mut data = MESSAGE_TO_CLIENT.to_vec();
let mut messages = [
SecurityBufferRef::token_buf(token.as_mut_slice()),
SecurityBufferRef::data_buf(data.as_mut_slice()),
];
server.encrypt_message(EncryptionFlags::empty(), &mut messages)?;
assert_ne!(MESSAGE_TO_CLIENT, messages[1].data());
println!(
"Message to client: {:x?}, encrypted message: {:x?}, token: {:x?}",
MESSAGE_TO_CLIENT,
messages[0].data(),
messages[1].data()
);
let [mut token, mut data] = messages;
let mut messages = vec![
SecurityBufferRef::data_buf(data.take_data()),
SecurityBufferRef::token_buf(token.take_data()),
];
client.decrypt_message(&mut messages)?;
assert_eq!(MESSAGE_TO_CLIENT, messages[0].data());
Ok(())
}
+5
View File
@@ -0,0 +1,5 @@
#![allow(clippy::panic, reason = "panics are acceptable in test code")]
mod client_server;
mod common;
mod ntlm;
+82
View File
@@ -0,0 +1,82 @@
use sspi::Ntlm;
use crate::common::{
CREDENTIALS, CredentialsProxyImpl, check_messages_encryption, create_client_credentials_handle,
create_server_credentials_handle, process_authentication_without_complete,
set_identity_and_try_complete_authentication, try_complete_authentication,
};
#[test]
fn successful_ntlm_authentication_with_client_auth_data() {
let mut credentials_proxy = CredentialsProxyImpl::new(&CREDENTIALS);
let mut client = Ntlm::new();
let client_credentials_handle = create_client_credentials_handle(&mut client, Some(&*CREDENTIALS)).unwrap();
let mut server = Ntlm::new();
let server_credentials_handle = create_server_credentials_handle(&mut server).unwrap();
let (client_status, server_status) = process_authentication_without_complete(
&mut client,
client_credentials_handle,
&mut server,
server_credentials_handle,
)
.unwrap();
try_complete_authentication(&mut client, client_status).unwrap();
set_identity_and_try_complete_authentication(&mut server, server_status, &mut credentials_proxy).unwrap();
check_messages_encryption(&mut client, &mut server).unwrap();
}
mod nt_hash {
use md4::{Digest, Md4};
use sspi::ntlm::Ntlm;
use sspi::{AuthIdentityBuffers, NtlmHash, Sspi, SspiImpl};
/// Password: "Password123!" -> NT hash: 2B576ACBE6BCFDA7294D6BD18041B8FE
const TEST_NT_HASH: &str = "2B576ACBE6BCFDA7294D6BD18041B8FE";
const TEST_USERNAME: &str = "testuser";
const TEST_DOMAIN: &str = "TESTDOMAIN";
fn password_to_ntlm_hash(password: &str) -> [u8; 16] {
let utf16_password: Vec<u8> = password.encode_utf16().flat_map(|c| c.to_le_bytes()).collect();
let mut hasher = Md4::new();
hasher.update(&utf16_password);
let result = hasher.finalize();
let mut hash = [0u8; 16];
hash.copy_from_slice(&result);
hash
}
#[test]
fn test_ntlm_negotiate_with_hash() {
let nt_hash: NtlmHash = TEST_NT_HASH.try_into().expect("valid hash");
assert_eq!(nt_hash.as_bytes(), &password_to_ntlm_hash("Password123!"));
let credentials = AuthIdentityBuffers::from_utf8_with_hash(TEST_USERNAME, TEST_DOMAIN, &nt_hash);
let mut ntlm = Ntlm::with_auth_identity(Some(credentials.clone()), Default::default());
let mut output = vec![sspi::SecurityBuffer::new(Vec::new(), sspi::BufferType::Token)];
let mut binding = Some(credentials.clone());
let mut builder = ntlm
.initialize_security_context()
.with_credentials_handle(&mut binding)
.with_context_requirements(sspi::ClientRequestFlags::CONFIDENTIALITY)
.with_target_data_representation(sspi::DataRepresentation::Native)
.with_output(&mut output);
let result = ntlm.initialize_security_context_impl(&mut builder);
// Should succeed in creating NEGOTIATE message
assert!(result.is_ok(), "Failed to create NEGOTIATE message: {result:?}");
let result = result.unwrap().resolve_to_result().unwrap();
assert_eq!(result.status, sspi::SecurityStatus::ContinueNeeded);
assert!(!output[0].buffer.is_empty(), "NEGOTIATE token should not be empty");
}
}