Files
nexus/rust/vendor/sspi/src/kerberos/mod.rs
T
Andy Dixon 50c0104d57 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.
2026-10-12 10:50:24 +01:00

877 lines
36 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
pub mod client;
pub mod config;
mod encryption_params;
pub mod flags;
mod messages;
mod pa_datas;
pub mod server;
#[cfg(test)]
mod tests;
mod utils;
use std::fmt::Debug;
use std::sync::LazyLock;
use picky_asn1::restricted_string::IA5String;
use picky_asn1::wrapper::{ExplicitContextTag0, ExplicitContextTag1, OctetStringAsn1, Optional};
use picky_krb::crypto::aes::{AesSize, checksum_sha_aes};
use picky_krb::crypto::{CipherSuite, DecryptWithoutChecksum, EncryptWithoutChecksum};
use picky_krb::data_types::KerberosStringAsn1;
use picky_krb::gss_api::WrapToken;
use picky_krb::messages::KdcProxyMessage;
use rand::rngs::{StdRng, SysRng};
use rand_core::{Rng as _, SeedableRng as _};
use url::Url;
pub use self::client::initialize_security_context;
use self::config::KerberosConfig;
pub use self::encryption_params::EncryptionParams;
pub use self::server::{ServerProperties, accept_security_context};
use super::channel_bindings::ChannelBindings;
use crate::builders::ChangePassword;
use crate::generator::{
GeneratorAcceptSecurityContext, GeneratorChangePassword, GeneratorInitSecurityContext, NetworkRequest,
YieldPointLocal,
};
use crate::network_client::NetworkProtocol;
#[cfg(feature = "scard")]
use crate::pk_init::DhParameters;
use crate::utils::{extract_encrypted_data, get_encryption_key, save_decrypted_data};
use crate::{
AcceptSecurityContextResult, AcquireCredentialsHandleResult, AuthIdentity, BufferType, ContextNames, ContextSizes,
CredentialUse, Credentials, CredentialsBuffers, DecryptionFlags, Error, ErrorKind, PACKAGE_ID_NONE,
PackageCapabilities, PackageInfo, Result, SecurityBuffer, SecurityBufferFlags, SecurityBufferRef,
SecurityPackageType, SecurityStatus, SessionKeys, Sspi, SspiEx, SspiImpl, detect_kdc_url,
};
pub const PKG_NAME: &str = "Kerberos";
pub const KERBEROS_VERSION: u8 = 0x05;
pub const TGT_SERVICE_NAME: &str = "krbtgt";
pub const KADMIN: &str = "kadmin";
pub const CHANGE_PASSWORD_SERVICE_NAME: &str = "changepw";
// pub const SSPI_KDC_URL_ENV: &str = "SSPI_KDC_URL";
pub const DEFAULT_ENCRYPTION_TYPE: CipherSuite = CipherSuite::Aes256CtsHmacSha196;
/// [3.4.5.4.1 Kerberos Binding of GSS_WrapEx()](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-kile/e94b3acd-8415-4d0d-9786-749d0c39d550)
/// The RRC field is 12 if no encryption is requested or 28 if encryption is requested
pub const RRC: u16 = 28;
// wrap token header len
pub const MAX_SIGNATURE: usize = 16;
/// Required `TOKEN` buffer length during data encryption (`encrypt_message` method call).
///
/// **Note**: Actual security trailer len is `SECURITY_TRAILER` + `EC`. The `EC` field is negotiated
// during the authentication process.
pub const SECURITY_TRAILER: usize = 60;
/// [3.4.5.4.1 Kerberos Binding of GSS_WrapEx()](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-kile/e94b3acd-8415-4d0d-9786-749d0c39d550)
///
/// The extra count (EC) must not be zero. The sender should set extra count (EC) to 1 block - 16 bytes.
pub(crate) const EC: u16 = 16;
pub static PACKAGE_INFO: LazyLock<PackageInfo> = LazyLock::new(|| PackageInfo {
capabilities: PackageCapabilities::empty(),
rpc_id: PACKAGE_ID_NONE,
max_token_len: 0xbb80, // 48 000 bytes: default maximum token len in Windows
name: SecurityPackageType::Kerberos,
comment: String::from("Kerberos Security Package"),
});
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum KerberosState {
TgtExchange,
Preauthentication,
ApExchange,
Final,
}
#[derive(Debug, Clone)]
pub struct Kerberos {
pub(crate) state: KerberosState,
pub(crate) config: KerberosConfig,
pub(crate) auth_identity: Option<CredentialsBuffers>,
pub(crate) encryption_params: EncryptionParams,
pub(crate) seq_number: u32,
pub(crate) realm: Option<String>,
pub(crate) kdc_url: Option<Url>,
pub(crate) channel_bindings: Option<ChannelBindings>,
#[cfg(feature = "scard")]
pub(crate) dh_parameters: Option<DhParameters>,
pub(crate) krb5_user_to_user: bool,
pub(crate) server: Option<Box<ServerProperties>>,
}
impl Kerberos {
pub fn new_client_from_config(config: KerberosConfig) -> Result<Self> {
let kdc_url = config.kdc_url.clone();
let mut rand = StdRng::try_from_rng(&mut SysRng)?;
Ok(Self {
state: KerberosState::TgtExchange,
config,
auth_identity: None,
encryption_params: EncryptionParams::default_for_client(),
seq_number: rand.next_u32(),
realm: None,
kdc_url,
channel_bindings: None,
#[cfg(feature = "scard")]
dh_parameters: None,
krb5_user_to_user: false,
server: None,
})
}
pub fn new_server_from_config(config: KerberosConfig, server_properties: ServerProperties) -> Result<Self> {
let kdc_url = config.kdc_url.clone();
let mut rand = StdRng::try_from_rng(&mut SysRng)?;
Ok(Self {
state: KerberosState::TgtExchange,
config,
auth_identity: None,
encryption_params: EncryptionParams::default_for_server(),
seq_number: rand.next_u32(),
realm: None,
kdc_url,
channel_bindings: None,
#[cfg(feature = "scard")]
dh_parameters: None,
krb5_user_to_user: false,
server: Some(Box::new(server_properties)),
})
}
pub fn is_client(&self) -> bool {
self.server.is_none()
}
pub fn config(&self) -> &KerberosConfig {
&self.config
}
pub fn next_seq_number(&mut self) -> u32 {
self.seq_number += 1;
self.seq_number
}
#[instrument(level = "debug", ret, skip(self))]
pub fn get_kdc(&self) -> Option<(String, Url)> {
let realm = self.realm.to_owned()?;
if let Some(kdc_url) = &self.kdc_url {
Some((realm, kdc_url.to_owned()))
} else {
let kdc_url = detect_kdc_url(&realm)?;
Some((realm, kdc_url))
}
}
async fn send(&self, yield_point: &mut YieldPointLocal, data: &[u8]) -> Result<Vec<u8>> {
let (realm, kdc_url) = self
.get_kdc()
.ok_or_else(|| Error::new(ErrorKind::NoAuthenticatingAuthority, "No KDC server found"))?;
self.send_to(yield_point, &realm, kdc_url, data).await
}
/// Sends a Kerberos message to the KDC responsible for `realm`.
///
/// The pinned `kdc_url` (e.g. from KDC proxy settings) is authoritative only for the client's
/// home realm. For cross-realm referrals, the target realm's KDC is resolved via
/// [`detect_kdc_url`], so that `SSPI_KDC_URL_<REALM>` environment overrides (and the system
/// krb5.conf / DNS SRV records) are honored for the referral hop. This lets a single auth
/// chase a referral into a child/trusted realm without changing the pinned home-realm KDC.
async fn send_for_realm(&self, yield_point: &mut YieldPointLocal, realm: &str, data: &[u8]) -> Result<Vec<u8>> {
let kdc_url = if self.realm.as_deref() == Some(realm) {
self.kdc_url.clone().or_else(|| detect_kdc_url(realm))
} else {
detect_kdc_url(realm)
}
.ok_or_else(|| {
Error::new(
ErrorKind::NoAuthenticatingAuthority,
format!("No KDC server found for realm `{realm}`"),
)
})?;
self.send_to(yield_point, realm, kdc_url, data).await
}
async fn send_to(
&self,
yield_point: &mut YieldPointLocal,
realm: &str,
kdc_url: Url,
data: &[u8],
) -> Result<Vec<u8>> {
let protocol = NetworkProtocol::from_url_scheme(kdc_url.scheme()).ok_or_else(|| {
Error::new(
ErrorKind::InvalidParameter,
format!("Invalid protocol `{}` for KDC server", kdc_url.scheme()),
)
})?;
match protocol {
NetworkProtocol::Tcp => {
let request = NetworkRequest {
protocol,
url: kdc_url,
data: data.to_vec(),
};
yield_point.suspend(request).await
}
NetworkProtocol::Udp => {
if data.len() < 4 {
return Err(Error::new(
ErrorKind::InternalError,
format!(
"kerberos message has invalid length. expected >= 4 but got {}",
data.len()
),
));
}
// First 4 bytes are message length and it’s not included when using UDP
let request = NetworkRequest {
protocol,
url: kdc_url,
data: data[4..].to_vec(),
};
yield_point.suspend(request).await
}
NetworkProtocol::Http | NetworkProtocol::Https => {
let data = OctetStringAsn1::from(data.to_vec());
let domain = KerberosStringAsn1::from(IA5String::from_string(realm.to_owned())?);
let kdc_proxy_message = KdcProxyMessage {
kerb_message: ExplicitContextTag0::from(data),
target_domain: Optional::from(Some(ExplicitContextTag1::from(domain))),
dclocator_hint: Optional::from(None),
};
let message_request = picky_asn1_der::to_vec(&kdc_proxy_message)?;
let request = NetworkRequest {
protocol,
url: kdc_url,
data: message_request,
};
let result_bytes = yield_point.suspend(request).await?;
let message_response: KdcProxyMessage = picky_asn1_der::from_bytes(&result_bytes)?;
Ok(message_response.kerb_message.0.0)
}
}
}
}
impl Sspi for Kerberos {
#[instrument(level = "debug", ret, fields(state = ?self.state), skip_all)]
fn complete_auth_token(&mut self, _token: &mut [SecurityBuffer]) -> Result<SecurityStatus> {
Ok(SecurityStatus::Ok)
}
#[instrument(level = "debug", ret, fields(state = ?self.state), skip(self, _flags))]
fn encrypt_message(
&mut self,
_flags: crate::EncryptionFlags,
message: &mut [SecurityBufferRef<'_>],
) -> Result<SecurityStatus> {
if self.state != KerberosState::Final {
return Err(Error::new(
ErrorKind::OutOfSequence,
format!("Kerberos context is not established: current state: {:?}", self.state),
));
}
trace!(encryption_params = ?self.encryption_params);
// checks if the Token buffer present
let _ = SecurityBufferRef::find_buffer(message, BufferType::Token)?;
// Find `Data` buffers but skip `Data` buffers with the `READONLY_WITH_CHECKSUM`/`READONLY` flag.
let data_to_encrypt =
SecurityBufferRef::buffers_of_type_and_flags(message, BufferType::Data, SecurityBufferFlags::NONE);
let cipher = self
.encryption_params
.encryption_type
.as_ref()
.unwrap_or(&DEFAULT_ENCRYPTION_TYPE)
.cipher();
let seq_number = self.next_seq_number();
let key = get_encryption_key(&self.encryption_params)?;
let key_usage = self.encryption_params.sspi_encrypt_key_usage;
let mut wrap_token = WrapToken::with_seq_number(seq_number as u64);
if self.server.is_some() {
// [Flags Field](https://datatracker.ietf.org/doc/html/rfc4121#section-4.2.2):
//
// The meanings of bits in this field (the least significant bit is bit 0) are as follows:
// Bit Name Description
// --------------------------------------------------------------
// 0 SentByAcceptor When set, this flag indicates the sender
// is the context acceptor. When not set,
// it indicates the sender is the context
// initiator.
// When the Kerberos is used as the Kerberos server we have to set the `SentByAcceptor` flag.
wrap_token.flags |= 0x01;
}
wrap_token.ec = self.encryption_params.ec;
let mut payload = data_to_encrypt.fold(Vec::new(), |mut acc, buffer| {
acc.extend_from_slice(buffer.data());
acc
});
// Add filler bytes to payload vector.
// More info:
// * [4.2.3. EC Field](https://datatracker.ietf.org/doc/html/rfc4121#section-4.2.3):
// In Wrap tokens with confidentiality, the EC field SHALL be used to encode the number of octets in the filler.
// * [4.2.4. Encryption and Checksum Operations](https://datatracker.ietf.org/doc/html/rfc4121#section-4.2.4):
// payload = plaintext-data | filler | "header"
payload.extend_from_slice(&vec![0; usize::from(self.encryption_params.ec)]);
payload.extend_from_slice(&wrap_token.header());
let EncryptWithoutChecksum {
mut encrypted,
confounder,
ki: _,
} = cipher.encrypt_no_checksum(key.as_ref(), key_usage, &payload)?;
// Find `Data` buffers (including `Data` buffers with the `READONLY_WITH_CHECKSUM` flag).
let mut data_to_sign =
SecurityBufferRef::buffers_of_type(message, BufferType::Data).fold(confounder, |mut acc, buffer| {
acc.extend_from_slice(buffer.data());
acc
});
// Add filler bytes to payload vector.
// More info:
// * [4.2.3. EC Field](https://datatracker.ietf.org/doc/html/rfc4121#section-4.2.3):
// In Wrap tokens with confidentiality, the EC field SHALL be used to encode the number of octets in the filler.
// * [4.2.4. Encryption and Checksum Operations](https://datatracker.ietf.org/doc/html/rfc4121#section-4.2.4):
// payload = plaintext-data | filler | "header"
data_to_sign.extend_from_slice(&vec![0; usize::from(self.encryption_params.ec)]);
data_to_sign.extend_from_slice(&wrap_token.header());
let checksum = cipher.encryption_checksum(key.as_ref(), key_usage, &data_to_sign)?;
encrypted.extend_from_slice(&checksum);
// [3.4.5.4.1 Kerberos Binding of GSS_WrapEx()](learn.microsoft.com/en-us/openspecs/windows_protocols/ms-kile/e94b3acd-8415-4d0d-9786-749d0c39d550):
// The trailing metadata H1 is rotated by RRC+EC bytes, which is different from RRC alone.
encrypted.rotate_right(usize::from(RRC + self.encryption_params.ec));
wrap_token.set_rrc(RRC);
wrap_token.set_checksum(encrypted);
let mut raw_wrap_token = Vec::with_capacity(wrap_token.checksum.len() + WrapToken::header_len());
wrap_token.encode(&mut raw_wrap_token)?;
let security_trailer_len = self.query_context_sizes()?.security_trailer.try_into()?;
let (token, data) = if raw_wrap_token.len() < security_trailer_len {
(raw_wrap_token.as_slice(), &[] as &[u8])
} else {
raw_wrap_token.split_at(security_trailer_len)
};
let data_buffer =
SecurityBufferRef::buffers_of_type_and_flags_mut(message, BufferType::Data, SecurityBufferFlags::NONE)
.next()
.ok_or_else(|| Error::new(ErrorKind::InvalidToken, "no buffer was provided with type Data"))?;
data_buffer.write_data(data)?;
let token_buffer = SecurityBufferRef::find_buffer_mut(message, BufferType::Token)?;
token_buffer.write_data(token)?;
Ok(SecurityStatus::Ok)
}
#[instrument(level = "debug", ret, fields(state = ?self.state), skip(self))]
fn decrypt_message(&mut self, message: &mut [SecurityBufferRef<'_>]) -> Result<DecryptionFlags> {
if self.state != KerberosState::Final {
return Err(Error::new(
ErrorKind::OutOfSequence,
format!("Kerberos context is not established: current state: {:?}", self.state),
));
}
trace!(encryption_params = ?self.encryption_params);
let encrypted = extract_encrypted_data(message)?;
let cipher = self
.encryption_params
.encryption_type
.as_ref()
.unwrap_or(&DEFAULT_ENCRYPTION_TYPE)
.cipher();
let key = get_encryption_key(&self.encryption_params)?;
let key_usage = self.encryption_params.sspi_decrypt_key_usage;
let wrap_token = WrapToken::decode(encrypted.as_slice())?;
// [Flags Field](https://datatracker.ietf.org/doc/html/rfc4121#section-4.2.2):
//
// The meanings of bits in this field (the least significant bit is bit 0) are as follows:
// Bit Name Description
// --------------------------------------------------------------
// 0 SentByAcceptor When set, this flag indicates the sender
// is the context acceptor. When not set,
// it indicates the sender is the context
// initiator.
let is_server = u8::from(self.server.is_some());
// If the Kerberos acts as the Kerberos application server, then the `SentByAcceptor` flag
// of the incoming WRAP token must be disabled (because it is sent by initiator).
if wrap_token.flags & 0x01 == is_server {
return Err(Error::new(
ErrorKind::InvalidToken,
"invalid WRAP token SentByAcceptor flag",
));
}
// 1 Sealed When set in Wrap tokens, this flag
// indicates confidentiality is provided
// for. It SHALL NOT be set in MIC tokens.
//
// When the Sealed flag is clear, this is an integrity-only Wrap token
// (GSS_Wrap with conf_req_flag == FALSE, RFC 4121 §4.2.4): the data is
// carried in cleartext followed by the checksum, rather than encrypted.
// Stock SASL/GSSAPI clients (e.g. Java/Kafka) use conf=false for the
// RFC 4752 security-layer negotiation reply, so the acceptor must
// accept it instead of demanding confidentiality.
if wrap_token.flags & 0b10 != 0b10 {
// The integrity-only checksum is the AES-SHA1 keyed HMAC
// (`checksum_sha_aes`), which is only defined for the AES cipher
// suites. Reject non-AES contexts (e.g. `Des3CbcSha1Kd`) explicitly
// rather than silently treating them as AES-256, which would produce
// incorrect checksum validation.
let aes_size = self.encryption_params.aes_size().ok_or_else(|| {
Error::new(
ErrorKind::UnsupportedFunction,
"integrity-only WRAP tokens are only supported for AES cipher suites",
)
})?;
return decrypt_integrity_only_wrap(&wrap_token, key.as_ref(), &aes_size, key_usage, message);
}
let mut checksum = wrap_token.checksum;
// [3.4.5.4.1 Kerberos Binding of GSS_WrapEx()](learn.microsoft.com/en-us/openspecs/windows_protocols/ms-kile/e94b3acd-8415-4d0d-9786-749d0c39d550):
// The trailing metadata H1 is rotated by RRC+EC bytes, which is different from RRC alone.
checksum.rotate_left((RRC + wrap_token.ec).into());
let DecryptWithoutChecksum {
plaintext: decrypted,
confounder,
checksum,
ki: _,
} = cipher.decrypt_no_checksum(key.as_ref(), key_usage, &checksum)?;
if decrypted.len() < usize::from(wrap_token.ec) + WrapToken::header_len() {
return Err(Error::new(ErrorKind::DecryptFailure, "decrypted data is too short"));
}
let plaintext_len = decrypted.len() - usize::from(wrap_token.ec) - WrapToken::header_len();
let plaintext = &decrypted[0..plaintext_len];
let wrap_token_header = &decrypted[plaintext_len..];
// Find `Data` buffers (including `Data` buffers with the `READONLY_WITH_CHECKSUM` flag).
let mut data_to_sign =
SecurityBufferRef::buffers_of_type(message, BufferType::Data).fold(confounder, |mut acc, buffer| {
if buffer
.buffer_flags()
.contains(SecurityBufferFlags::SECBUFFER_READONLY_WITH_CHECKSUM)
{
acc.extend_from_slice(buffer.data());
} else {
// The `Data` buffer contains encrypted data, but the checksum was calculated over the decrypted data.
// So, we replace encrypted data with decrypted one.
// Note: our implementation expect maximum one plain `DATA` buffer but multiple `DATA` buffers
// with `SECBUFFER_READONLY_WITH_CHECKSUM` flag are allowed.
acc.extend_from_slice(plaintext);
}
acc
});
data_to_sign.extend_from_slice(wrap_token_header);
let calculated_checksum = cipher.encryption_checksum(key.as_ref(), key_usage, &data_to_sign)?;
if calculated_checksum != checksum {
return Err(picky_krb::crypto::KerberosCryptoError::IntegrityCheck.into());
}
save_decrypted_data(plaintext, message)?;
Ok(DecryptionFlags::empty())
}
#[instrument(level = "debug", ret, fields(state = ?self.state), skip(self))]
fn query_context_sizes(&mut self) -> Result<ContextSizes> {
// We prevent users from calling `query_context_sizes` on a non-established security context
// because it can lead to invalid values being returned.
if self.state != KerberosState::Final {
return Err(Error::new(
ErrorKind::OutOfSequence,
format!("Kerberos context is not established: current state: {:?}", self.state),
));
}
Ok(ContextSizes {
max_token: PACKAGE_INFO.max_token_len,
max_signature: MAX_SIGNATURE as u32,
block: 0,
security_trailer: SECURITY_TRAILER as u32 + u32::from(self.encryption_params.ec),
})
}
#[instrument(level = "debug", ret, fields(state = ?self.state), skip(self))]
fn query_context_names(&mut self) -> Result<ContextNames> {
if let Some(client) = self.server.as_ref().and_then(|server| server.client.as_ref()) {
return Ok(ContextNames {
username: client.clone(),
});
}
if let Some(CredentialsBuffers::AuthIdentity(identity_buffers)) = &self.auth_identity {
let identity =
AuthIdentity::try_from(identity_buffers).map_err(|e| Error::new(ErrorKind::InvalidParameter, e))?;
return Ok(ContextNames {
username: identity.username,
});
}
#[cfg(feature = "scard")]
if let Some(CredentialsBuffers::SmartCard(ref identity_buffers)) = self.auth_identity {
let username = identity_buffers.username.to_string();
let username = crate::Username::parse(&username).map_err(|e| Error::new(ErrorKind::InvalidParameter, e))?;
return Ok(ContextNames { username });
}
Err(Error::new(
ErrorKind::NoCredentials,
"requested names, but no credentials were provided",
))
}
#[instrument(level = "debug", ret, fields(state = ?self.state), skip(self))]
fn query_context_package_info(&mut self) -> Result<PackageInfo> {
crate::query_security_package_info(SecurityPackageType::Kerberos)
}
#[instrument(level = "debug", ret, fields(state = ?self.state), skip(self))]
fn query_context_cert_trust_status(&mut self) -> Result<crate::CertTrustStatus> {
Err(Error::new(
ErrorKind::UnsupportedFunction,
"certificate trust status is not supported".to_owned(),
))
}
#[instrument(level = "debug", fields(state = ?self.state), skip(self))]
fn query_context_session_key(&self) -> Result<SessionKeys> {
Ok(SessionKeys {
session_key: get_encryption_key(&self.encryption_params)?.clone(),
})
}
fn change_password<'a>(&'a mut self, change_password: ChangePassword<'a>) -> Result<GeneratorChangePassword<'a>> {
Ok(GeneratorChangePassword::new(move |mut yield_point| async move {
client::change_password(self, &mut yield_point, change_password).await
}))
}
fn make_signature(
&mut self,
_flags: u32,
_message: &mut [SecurityBufferRef<'_>],
_sequence_number: u32,
) -> Result<()> {
Err(Error::new(
ErrorKind::UnsupportedFunction,
"make_signature is not supported. use encrypt_message to sign messages instead",
))
}
fn verify_signature(&mut self, _message: &mut [SecurityBufferRef<'_>], _sequence_number: u32) -> Result<u32> {
Err(Error::new(
ErrorKind::UnsupportedFunction,
"verify_signature is not supported. use decrypt_message to verify signatures instead",
))
}
}
/// Verifies an integrity-only (GSS_Wrap conf_req_flag == FALSE) Wrap token and
/// returns its cleartext payload in the message buffers.
///
/// Per RFC 4121 §4.2.4, an unsealed Wrap token carries the data in the clear
/// followed by the checksum: `{header | plaintext | checksum}`, right-rotated
/// by RRC. The checksum is computed over `plaintext | header`, where the EC and
/// RRC fields of the 16-octet header are zeroed for the computation. Wrap tokens
/// — sealed or not — use the SEAL key usage (the SIGN usage is reserved for MIC
/// tokens), so `key_usage` is the same value the sealed path uses. The checksum
/// itself is the keyed get_mic checksum (Kc-derived), not the Ki-derived
/// encryption integrity hash used for the confidential path.
fn decrypt_integrity_only_wrap(
wrap_token: &WrapToken,
key: &[u8],
aes_size: &AesSize,
key_usage: i32,
message: &mut [SecurityBufferRef<'_>],
) -> Result<DecryptionFlags> {
// `WrapToken::decode` stores everything after the 16-octet header in
// `checksum`; for an unsealed token that is `plaintext | trailing-checksum`.
let mut data = wrap_token.checksum.clone();
if data.is_empty() {
return Err(Error::new(ErrorKind::DecryptFailure, "empty integrity-only WRAP token"));
}
// Undo the sender's right rotation. For integrity-only tokens the rotation
// is by RRC alone (the EC octets are checksum, not filler).
let rotate = usize::from(wrap_token.rrc) % data.len();
data.rotate_left(rotate);
let ec = usize::from(wrap_token.ec);
if data.len() < ec {
return Err(Error::new(
ErrorKind::DecryptFailure,
"integrity-only WRAP token shorter than its checksum",
));
}
let plaintext_len = data.len() - ec;
let (plaintext, received_checksum) = data.split_at(plaintext_len);
// Checksum input: plaintext followed by the 16-octet header with the EC and
// RRC fields zeroed (RFC 4121 §4.2.4).
let mut header = wrap_token.header();
header[4..8].copy_from_slice(&[0, 0, 0, 0]);
let mut to_sign = plaintext.to_vec();
to_sign.extend_from_slice(&header);
let calculated = checksum_sha_aes(key, key_usage, &to_sign, aes_size)?;
if calculated.as_slice() != received_checksum {
return Err(picky_krb::crypto::KerberosCryptoError::IntegrityCheck.into());
}
save_decrypted_data(plaintext, message)?;
Ok(DecryptionFlags::empty())
}
impl SspiImpl for Kerberos {
type CredentialsHandle = Option<CredentialsBuffers>;
type AuthenticationData = Credentials;
#[instrument(level = "trace", ret, fields(state = ?self.state), skip(self))]
fn acquire_credentials_handle_impl(
&mut self,
builder: crate::builders::FilledAcquireCredentialsHandle<'_, Self::CredentialsHandle, Self::AuthenticationData>,
) -> Result<AcquireCredentialsHandleResult<Self::CredentialsHandle>> {
if builder.credential_use == CredentialUse::Outbound && builder.auth_data.is_none() {
return Err(Error::new(
ErrorKind::NoCredentials,
"the client must specify the auth data",
));
}
self.auth_identity = builder
.auth_data
.cloned()
.map(|auth_data| auth_data.try_into())
.transpose()?;
Ok(AcquireCredentialsHandleResult {
credentials_handle: self.auth_identity.clone(),
expiry: None,
})
}
#[instrument(level = "debug", ret, fields(state = ?self.state), skip(self, builder))]
fn accept_security_context_impl<'a>(
&'a mut self,
builder: crate::builders::FilledAcceptSecurityContext<'a, Self::CredentialsHandle>,
) -> Result<GeneratorAcceptSecurityContext<'a>> {
Ok(GeneratorAcceptSecurityContext::new(move |mut yield_point| async move {
self.accept_security_context_impl(&mut yield_point, builder).await
}))
}
fn initialize_security_context_impl<'ctx, 'b, 'g>(
&'ctx mut self,
builder: &'b mut crate::builders::FilledInitializeSecurityContext<'ctx, 'ctx, Self::CredentialsHandle>,
) -> Result<GeneratorInitSecurityContext<'g>>
where
'ctx: 'b,
'b: 'g,
{
Ok(GeneratorInitSecurityContext::new(move |mut yield_point| async move {
self.initialize_security_context_impl(&mut yield_point, builder).await
}))
}
}
impl<'a> Kerberos {
#[instrument(level = "debug", ret, fields(state = ?self.state), skip(self, change_password))]
pub async fn change_password(
&'a mut self,
yield_point: &mut YieldPointLocal,
change_password: ChangePassword<'a>,
) -> Result<()> {
client::change_password(self, yield_point, change_password).await
}
pub(crate) async fn accept_security_context_impl(
&'a mut self,
yield_point: &mut YieldPointLocal,
builder: crate::builders::FilledAcceptSecurityContext<'a, <Self as SspiImpl>::CredentialsHandle>,
) -> Result<AcceptSecurityContextResult> {
accept_security_context(self, yield_point, builder).await
}
pub(crate) async fn initialize_security_context_impl(
&'a mut self,
yield_point: &mut YieldPointLocal,
builder: &'a mut crate::builders::FilledInitializeSecurityContext<
'_,
'_,
<Self as SspiImpl>::CredentialsHandle,
>,
) -> Result<crate::InitializeSecurityContextResult> {
initialize_security_context(self, yield_point, builder).await
}
}
impl SspiEx for Kerberos {
#[instrument(level = "trace", ret, fields(state = ?self.state), skip(self))]
fn custom_set_auth_identity(&mut self, identity: Self::AuthenticationData) -> Result<()> {
self.auth_identity = Some(identity.try_into()?);
Ok(())
}
fn verify_mic_token(&mut self, token: &[u8], data: &[u8], _: crate::private::Sealed) -> Result<()> {
utils::validate_mic_token(self.is_client(), token, &self.encryption_params, data)
}
fn generate_mic_token(&mut self, data: &[u8], _: crate::private::Sealed) -> Result<Vec<u8>> {
// Do not increment sequence number for MIC token if security context is not established.
// We do not want to mess up the sequence number.
let seq_number = if self.encryption_params.sub_session_key.is_some() {
self.next_seq_number()
} else {
0
};
let session_key = self
.encryption_params
.sub_session_key
.as_ref()
.ok_or_else(|| Error::new(ErrorKind::InternalError, "kerberos sub-session key is not set"))?;
utils::generate_mic_token(self.is_client(), u64::from(seq_number), data.to_vec(), session_key)
}
}
#[cfg(any(feature = "__test-data", test))]
pub mod test_data {
use std::time::Duration;
use picky_asn1::restricted_string::IA5String;
use picky_asn1::wrapper::{Asn1SequenceOf, ExplicitContextTag0, ExplicitContextTag1, IntegerAsn1};
use picky_krb::constants::key_usages::{ACCEPTOR_SEAL, INITIATOR_SEAL};
use picky_krb::constants::types::NT_SRV_INST;
use picky_krb::crypto::CipherSuite;
use picky_krb::data_types::{KerberosStringAsn1, PrincipalName};
use picky_krb::gss_api::MechTypeList;
use super::{EncryptionParams, KerberosConfig, KerberosState};
use crate::kerberos::ServerProperties;
use crate::{AuthIdentityBuffers, CredentialsBuffers, Kerberos, Secret, Utf16String, ZeroizedUtf16String};
const SESSION_KEY: &[u8] = &[
21, 56, 207, 133, 152, 47, 177, 117, 223, 235, 169, 237, 173, 202, 11, 254, 142, 185, 237, 5, 97, 79, 112, 46,
73, 182, 117, 0, 35, 91, 24, 66,
];
const SUB_SESSION_KEY: &[u8] = &[
146, 61, 191, 46, 26, 68, 247, 94, 124, 95, 1, 190, 15, 185, 245, 64, 18, 203, 212, 49, 43, 222, 254, 217, 85,
222, 7, 92, 254, 153, 105, 144,
];
pub fn fake_client() -> Kerberos {
Kerberos {
state: KerberosState::Final,
config: KerberosConfig {
kdc_url: None,
client_computer_name: "hostname".into(),
},
auth_identity: Some(CredentialsBuffers::AuthIdentity(AuthIdentityBuffers {
user: Utf16String::from_str("fake_user"),
domain: Utf16String::from_str("FAKE_DOMAIN"),
password: Secret::new(ZeroizedUtf16String(Utf16String::from_str("fake_password"))),
})),
encryption_params: EncryptionParams {
encryption_type: Some(CipherSuite::Aes256CtsHmacSha196),
session_key: Some(SESSION_KEY.to_vec().into()),
sub_session_key: Some(SUB_SESSION_KEY.to_vec().into()),
sspi_encrypt_key_usage: INITIATOR_SEAL,
sspi_decrypt_key_usage: ACCEPTOR_SEAL,
ec: 0,
},
seq_number: 1234,
realm: None,
kdc_url: None,
channel_bindings: None,
#[cfg(feature = "scard")]
dh_parameters: None,
krb5_user_to_user: false,
server: None,
}
}
pub fn fake_server_properties() -> ServerProperties {
ServerProperties {
mech_types: MechTypeList::from(Vec::new()),
max_time_skew: Duration::from_secs(3 * 60),
ticket_decryption_key: None,
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".to_owned()).unwrap()),
KerberosStringAsn1::from(IA5String::from_string("VM1.example.com".to_owned()).unwrap()),
])),
},
additional_service_keys: Vec::new(),
user: None,
client: None,
authenticators_cache: Default::default(),
}
}
pub fn fake_server() -> Kerberos {
Kerberos {
state: KerberosState::Final,
config: KerberosConfig {
kdc_url: None,
client_computer_name: "hostname".into(),
},
auth_identity: None,
encryption_params: EncryptionParams {
encryption_type: Some(CipherSuite::Aes256CtsHmacSha196),
session_key: Some(SESSION_KEY.to_vec().into()),
sub_session_key: Some(SUB_SESSION_KEY.to_vec().into()),
sspi_encrypt_key_usage: ACCEPTOR_SEAL,
sspi_decrypt_key_usage: INITIATOR_SEAL,
ec: 0,
},
seq_number: 0,
realm: None,
kdc_url: None,
channel_bindings: None,
#[cfg(feature = "scard")]
dh_parameters: None,
krb5_user_to_user: false,
server: Some(Box::new(fake_server_properties())),
}
}
}