Files
nexus/rust/vendor/sspi/src/auth_identity.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

969 lines
37 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.
use std::fmt;
use std::ops::Not;
use picky_krb::crypto::CipherSuite;
use crate::utf16string::ZeroizedUtf16String;
use crate::{Error, Secret, Utf16String, Utf16StringExt};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum UsernameError {
MixedFormat,
}
impl std::error::Error for UsernameError {}
impl fmt::Display for UsernameError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
UsernameError::MixedFormat => write!(f, "mixed username format"),
}
}
}
/// Enumeration of the supported [User Name Formats].
///
/// [User Name Formats]: https://learn.microsoft.com/en-us/windows/win32/secauthn/user-name-formats
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum UserNameFormat {
/// [User principal name] (UPN) format is used to specify an Internet-style name, such as UserName@Example.Microsoft.com.
///
/// [User principal name]: https://learn.microsoft.com/en-us/windows/win32/secauthn/user-name-formats#user-principal-name
UserPrincipalName,
/// The [down-level logon name] format is used to specify a domain and a user account in that domain, for example, DOMAIN\UserName.
///
/// [down-level logon name]: https://learn.microsoft.com/en-us/windows/win32/secauthn/user-name-formats#down-level-logon-name
DownLevelLogonName,
}
/// A username formatted as either UPN or Down-Level Logon Name
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Username {
value: String,
format: UserNameFormat,
sep_idx: Option<usize>,
}
/// A format-tagged, borrowed view into the components of a [`Username`].
///
/// Unlike the (deprecated) [`Username::domain_name`] accessor, each variant only exposes the
/// components that are actually meaningful for its [`UserNameFormat`]. This makes it impossible to
/// mistake a UPN suffix for a NetBIOS domain name (or vice versa): the [`UserPrincipalNameParts`]
/// arm has no "domain" component at all, only a suffix.
///
/// Matching is exhaustive over the two [User Name Formats], so callers are forced to handle both.
///
/// A `UsernameParts` can only be obtained from [`Username::parts`]; the variant payloads are opaque
/// (accessor-only, not constructible outside this crate), so a view can never be forged into an
/// inconsistent state.
///
/// [User Name Formats]: https://learn.microsoft.com/en-us/windows/win32/secauthn/user-name-formats
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum UsernameParts<'a> {
/// [User principal name] components, e.g. `account_name@suffix`.
///
/// [User principal name]: https://learn.microsoft.com/en-us/windows/win32/secauthn/user-name-formats#user-principal-name
UserPrincipalName(UserPrincipalNameParts<'a>),
/// [Down-level logon name] components, e.g. `netbios_domain\account_name`.
///
/// [Down-level logon name]: https://learn.microsoft.com/en-us/windows/win32/secauthn/user-name-formats#down-level-logon-name
DownLevelLogonName(DownLevelLogonNameParts<'a>),
}
/// The components of a [user principal name](UsernameParts::UserPrincipalName).
///
/// Obtained via [`Username::parts`]; the components are opaque and exposed through accessors only,
/// so this type can never be constructed with inconsistent fields.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct UserPrincipalNameParts<'a> {
account_name: &'a str,
suffix: &'a str,
upn: &'a str,
}
impl<'a> UserPrincipalNameParts<'a> {
/// The account name, i.e. the part before the `@`.
pub fn account_name(&self) -> &'a str {
self.account_name
}
/// The UPN suffix, i.e. the part after the `@`. This is *not* a NetBIOS domain name.
pub fn suffix(&self) -> &'a str {
self.suffix
}
/// The complete user principal name (`account_name@suffix`), borrowed as-is.
///
/// For a UPN, this whole string is what identifies the user (e.g. it is used verbatim as the
/// NT-ENTERPRISE client name in Kerberos), unlike a down-level logon name where only
/// `account_name` does. Prefer this over reconstructing the string from `account_name`/`suffix`
/// or reaching for [`Username::inner`].
pub fn upn(&self) -> &'a str {
self.upn
}
}
/// The components of a [down-level logon name](UsernameParts::DownLevelLogonName).
///
/// Obtained via [`Username::parts`]; the components are opaque and exposed through accessors only,
/// so this type can never be constructed with inconsistent fields.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct DownLevelLogonNameParts<'a> {
account_name: &'a str,
netbios_domain: Option<&'a str>,
}
impl<'a> DownLevelLogonNameParts<'a> {
/// The account name, i.e. the part after the `\` (or the whole value when there is no separator).
pub fn account_name(&self) -> &'a str {
self.account_name
}
/// The NetBIOS domain name, i.e. the part before the `\`, or `None` when the value has no separator.
pub fn netbios_domain(&self) -> Option<&'a str> {
self.netbios_domain
}
}
impl UsernameParts<'_> {
/// Compares two username views for equality, ignoring ASCII case.
///
/// Equality is *format-aware*: a [`UsernameParts::UserPrincipalName`] and a
/// [`UsernameParts::DownLevelLogonName`] are never equal, even if their account names and their
/// UPN suffix / NetBIOS domain happen to match case-insensitively. Because the format is part of
/// the comparison, a UPN suffix is only ever compared against another UPN suffix, and a NetBIOS
/// domain only ever against another NetBIOS domain: the two can never be conflated.
pub fn eq_ignore_ascii_case(&self, other: &UsernameParts<'_>) -> bool {
match (self, other) {
(UsernameParts::UserPrincipalName(lhs), UsernameParts::UserPrincipalName(rhs)) => {
// `upn` is fully determined by `account_name` and `suffix`, so it needs no comparison.
lhs.account_name.eq_ignore_ascii_case(rhs.account_name) && lhs.suffix.eq_ignore_ascii_case(rhs.suffix)
}
(UsernameParts::DownLevelLogonName(lhs), UsernameParts::DownLevelLogonName(rhs)) => {
let domains_equal = match (lhs.netbios_domain, rhs.netbios_domain) {
(Some(lhs_domain), Some(rhs_domain)) => lhs_domain.eq_ignore_ascii_case(rhs_domain),
(None, None) => true,
_ => false,
};
lhs.account_name.eq_ignore_ascii_case(rhs.account_name) && domains_equal
}
// Different formats are distinct identities.
_ => false,
}
}
}
impl Username {
/// Builds a user principal name from an account name and an UPN suffix
pub fn new_upn(account_name: &str, upn_suffix: &str) -> Result<Self, UsernameError> {
// NOTE: AD usernames may contain `@`
if account_name.contains(['\\']) {
return Err(UsernameError::MixedFormat);
}
if upn_suffix.contains(['\\', '@']) {
return Err(UsernameError::MixedFormat);
}
Ok(Self {
value: format!("{account_name}@{upn_suffix}"),
format: UserNameFormat::UserPrincipalName,
sep_idx: Some(account_name.len()),
})
}
/// Builds a down-level logon name from an account name and a NetBIOS domain name
pub fn new_down_level_logon_name(account_name: &str, netbios_domain_name: &str) -> Result<Self, UsernameError> {
if account_name.contains(['\\', '@']) {
return Err(UsernameError::MixedFormat);
}
if netbios_domain_name.contains(['\\', '@']) {
return Err(UsernameError::MixedFormat);
}
// An empty NetBIOS domain means "no domain": represent it accurately as a separator-less
// down-level logon name (`netbios_domain == None`) rather than a distinct `Some("")`.
if netbios_domain_name.is_empty() {
return Ok(Self {
value: account_name.to_owned(),
format: UserNameFormat::DownLevelLogonName,
sep_idx: None,
});
}
Ok(Self {
value: format!("{netbios_domain_name}\\{account_name}"),
format: UserNameFormat::DownLevelLogonName,
sep_idx: Some(netbios_domain_name.len()),
})
}
/// Attempts to guess the right name format for the account name/domain combo
///
/// If no netbios domain name is provided, or if it is an empty string, the username will
/// be parsed as either a user principal name or a down-level logon name.
///
/// It falls back to a down-level logon name when the format can’t be guessed.
pub fn new(account_name: &str, netbios_domain_name: Option<&str>) -> Result<Self, UsernameError> {
match netbios_domain_name {
Some(netbios_domain_name) if !netbios_domain_name.is_empty() => {
Self::new_down_level_logon_name(account_name, netbios_domain_name)
}
_ => Self::parse(account_name),
}
}
/// Parses the value in order to find if the value is a user principal name or a down-level logon name
///
/// If there is no `\` or `@` separator, the value is considered to be a down-level logon name with
/// an empty NetBIOS domain.
pub fn parse(value: &str) -> Result<Self, UsernameError> {
match (value.split_once('\\'), value.rsplit_once('@')) {
(None, None) => Ok(Self {
value: value.to_owned(),
format: UserNameFormat::DownLevelLogonName,
sep_idx: None,
}),
(Some((netbios_domain_name, account_name)), _) => {
Self::new_down_level_logon_name(account_name, netbios_domain_name)
}
(_, Some((account_name, upn_suffix))) => Self::new_upn(account_name, upn_suffix),
}
}
/// Returns the internal representation, as-is
pub fn inner(&self) -> &str {
&self.value
}
/// Returns the [`UserNameFormat`] for this username
pub fn format(&self) -> UserNameFormat {
self.format
}
/// Returns a format-tagged, borrowed view into the components of the username.
///
/// Prefer this over [`Username::domain_name`]: matching on the returned [`UsernameParts`]
/// forces both user name formats to be handled explicitly and prevents confusing a UPN
/// suffix with a NetBIOS domain name.
pub fn parts(&self) -> UsernameParts<'_> {
match self.format {
UserNameFormat::UserPrincipalName => {
// A user principal name is always built with a separator (the `@`).
let idx = self.sep_idx.expect("a UPN always has an `@` separator");
UsernameParts::UserPrincipalName(UserPrincipalNameParts {
account_name: &self.value[..idx],
suffix: &self.value[idx + 1..],
upn: &self.value,
})
}
UserNameFormat::DownLevelLogonName => match self.sep_idx {
Some(idx) => UsernameParts::DownLevelLogonName(DownLevelLogonNameParts {
account_name: &self.value[idx + 1..],
netbios_domain: Some(&self.value[..idx]),
}),
None => UsernameParts::DownLevelLogonName(DownLevelLogonNameParts {
account_name: &self.value,
netbios_domain: None,
}),
},
}
}
/// May return an UPN suffix or NetBIOS domain name depending on the internal format
#[allow(
clippy::deprecated_semver,
reason = "`<next-version>` placeholder filled in at release time"
)]
#[deprecated(
since = "<next-version>",
note = "conflates UPN suffix with NetBIOS domain; match on `Username::parts()` instead — see https://github.com/Devolutions/sspi-rs/issues/708"
)]
pub fn domain_name(&self) -> Option<&str> {
self.sep_idx.map(|idx| match self.format {
UserNameFormat::UserPrincipalName => &self.value[idx + 1..],
UserNameFormat::DownLevelLogonName => &self.value[..idx],
})
}
/// Returns the account name
pub fn account_name(&self) -> &str {
if let Some(idx) = self.sep_idx {
match self.format {
UserNameFormat::UserPrincipalName => &self.value[..idx],
UserNameFormat::DownLevelLogonName => &self.value[idx + 1..],
}
} else {
&self.value
}
}
/// Compares two usernames for equality, ignoring ASCII case.
///
/// This is a case-insensitive counterpart to the (case-sensitive) [`PartialEq`] implementation,
/// matching Windows' case-insensitive treatment of account and domain names. Usernames of
/// different [formats](UserNameFormat) are never equal (see [`UsernameParts::eq_ignore_ascii_case`]).
pub fn eq_ignore_ascii_case(&self, other: &Username) -> bool {
self.parts().eq_ignore_ascii_case(&other.parts())
}
}
/// Allows you to pass a particular user name and password to the run-time library for the purpose of authentication
///
/// # MSDN
///
/// * [SEC_WINNT_AUTH_IDENTITY_W structure](https://docs.microsoft.com/en-us/windows/win32/api/sspi/ns-sspi-sec_winnt_auth_identity_w)
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct AuthIdentity {
pub username: Username,
pub password: Secret<String>,
}
/// Client credentials backed by a pre-derived Kerberos long-term key.
///
/// Unlike [`AuthIdentity`], no password is supplied: the raw long-term key
/// (as stored in a keytab) is used directly to encrypt the PA-ENC-TIMESTAMP
/// pre-authentication value and to decrypt the AS-REP, skipping the
/// string-to-key derivation. This is the credential a service uses when it
/// acts as a Kerberos *client* (e.g. inter-service authentication) without a
/// human password.
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct KeytabIdentity {
/// Client principal, e.g. `"svc@REALM"` or `"svc/host@REALM"`.
pub principal: Username,
/// Raw long-term key bytes for `key_enctype`.
pub key: Secret<Vec<u8>>,
/// Kerberos encryption type of `key` (e.g. aes256-cts-hmac-sha1-96).
pub key_enctype: CipherSuite,
}
/// Auth identity buffers for password-based logon.
#[derive(Clone, Eq, PartialEq, Default)]
pub struct AuthIdentityBuffers {
/// Username.
///
/// Must be UTF-16 encoded.
pub user: Utf16String,
/// Domain.
///
/// Must be UTF-16 encoded.
pub domain: Utf16String,
/// Password.
///
/// Must be UTF-16 encoded.
///
/// If the password is an NT hash, it should be prefixed with [`NTLM_HASH_PREFIX`](crate::NTLM_HASH_PREFIX) followed by the hash in hexadecimal format.
///
/// See [`NtlmHash`](crate::NtlmHash) for more details.
pub password: Secret<ZeroizedUtf16String>,
}
impl AuthIdentityBuffers {
/// Creates a new [AuthIdentityBuffers] object based on provided credentials.
///
/// Provided credentials must be UTF-16 encoded.
pub fn new(user: Utf16String, domain: Utf16String, password: Utf16String) -> Self {
Self {
user,
domain,
password: ZeroizedUtf16String(password).into(),
}
}
pub fn is_empty(&self) -> bool {
self.user.is_empty()
}
/// Creates a new [AuthIdentityBuffers] object based on UTF-8 credentials.
///
/// It converts the provided credentials to UTF-16 byte vectors automatically.
pub fn from_utf8(user: &str, domain: &str, password: &str) -> Self {
Self {
user: user.into(),
domain: domain.into(),
password: ZeroizedUtf16String(Utf16String::from(password)).into(),
}
}
/// Creates a new [AuthIdentityBuffers] object based on UTF-8 username and domain, and NT hash for the password.
pub fn from_utf8_with_hash(user: &str, domain: &str, nt_hash: &crate::NtlmHash) -> Self {
Self {
user: user.into(),
domain: domain.into(),
password: ZeroizedUtf16String(Utf16String::from(nt_hash.to_sspi_password())).into(),
}
}
}
impl fmt::Debug for AuthIdentityBuffers {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "AuthIdentityBuffers {{ user: 0x")?;
self.user
.as_bytes_le()
.iter()
.try_for_each(|byte| write!(f, "{byte:02X}"))?;
write!(f, ", domain: 0x")?;
self.domain
.as_bytes_le()
.iter()
.try_for_each(|byte| write!(f, "{byte:02X}"))?;
write!(f, ", password: {:?} }}", self.password)?;
Ok(())
}
}
impl From<AuthIdentity> for AuthIdentityBuffers {
fn from(credentials: AuthIdentity) -> Self {
let password: &str = credentials.password.as_ref().as_ref();
// Encode the username so that it round-trips back to the same format through
// `TryFrom<&AuthIdentityBuffers>` (which parses `user` and treats `domain` as a NetBIOS
// domain). A UPN is stored whole in `user` with an empty `domain` (parsing recovers the UPN
// via its `@`), while a down-level logon name keeps the `(account_name, netbios_domain)` split.
let (user, domain) = match credentials.username.parts() {
UsernameParts::UserPrincipalName(parts) => (parts.upn(), ""),
UsernameParts::DownLevelLogonName(parts) => {
(parts.account_name(), parts.netbios_domain().unwrap_or_default())
}
};
Self {
user: user.into(),
domain: domain.into(),
password: ZeroizedUtf16String(password.into()).into(),
}
}
}
impl TryFrom<&AuthIdentityBuffers> for AuthIdentity {
type Error = UsernameError;
fn try_from(credentials_buffers: &AuthIdentityBuffers) -> Result<Self, Self::Error> {
let account_name = credentials_buffers.user.to_string();
let domain_name = credentials_buffers
.domain
.is_empty()
.not()
.then(|| credentials_buffers.domain.to_string());
let username = Username::new(&account_name, domain_name.as_deref())?;
let password = credentials_buffers.password.as_ref().as_ref().to_string().into();
Ok(Self { username, password })
}
}
impl TryFrom<AuthIdentityBuffers> for AuthIdentity {
type Error = UsernameError;
fn try_from(credentials_buffers: AuthIdentityBuffers) -> Result<Self, Self::Error> {
AuthIdentity::try_from(&credentials_buffers)
}
}
#[cfg(feature = "scard")]
mod scard_credentials {
#[cfg(not(target_arch = "wasm32"))]
use std::path::PathBuf;
use picky::key::PrivateKey;
use picky_asn1_der::Asn1DerError;
use picky_asn1_x509::Certificate;
use crate::secret::SecretPrivateKey;
use crate::utf16string::ZeroizedUtf16String;
use crate::{Error, ErrorKind, NonEmpty, Secret, Utf16String};
/// DER-encoded x509 certificate.
#[derive(Clone, Eq, PartialEq, Debug)]
pub struct CertificateRaw(Vec<u8>);
impl AsRef<[u8]> for CertificateRaw {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
impl TryFrom<Vec<u8>> for CertificateRaw {
type Error = Asn1DerError;
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
let _: Certificate = picky_asn1_der::from_bytes(value.as_ref())?;
Ok(Self(value))
}
}
impl From<CertificateRaw> for Vec<u8> {
fn from(value: CertificateRaw) -> Self {
value.0
}
}
impl TryFrom<&Certificate> for CertificateRaw {
type Error = Asn1DerError;
fn try_from(value: &Certificate) -> Result<Self, Self::Error> {
picky_asn1_der::to_vec(value).map(Self)
}
}
impl TryFrom<Certificate> for CertificateRaw {
type Error = Asn1DerError;
fn try_from(value: Certificate) -> Result<Self, Self::Error> {
Self::try_from(&value)
}
}
impl From<&CertificateRaw> for Certificate {
fn from(value: &CertificateRaw) -> Self {
picky_asn1_der::from_bytes(&value.0).expect("value.0 is convertible to Certificate (checked on creation)")
}
}
impl From<CertificateRaw> for Certificate {
fn from(value: CertificateRaw) -> Self {
Self::from(&value)
}
}
/// Smart card type.
#[derive(Clone, Eq, PartialEq, Debug)]
pub enum SmartCardType {
/// Emulated smart card.
///
/// No real device is used. All smart card functionality is emulated using the [winscard] crate.
Emulated {
/// Emulated smart card PIN code.
///
/// This is smart card PIN code, not the PIN code provided by the user.
scard_pin: Secret<Vec<u8>>,
},
#[cfg(not(target_arch = "wasm32"))]
/// System-provided smart card.
///
/// Real smart card device in use.
SystemProvided {
/// Path to the PKCS11 module.
pkcs11_module_path: PathBuf,
},
/// System-provided smart card, but the Windows native API will be used for accessing smart card.
///
/// Available only on Windows.
#[cfg(target_os = "windows")]
WindowsNative,
}
/// Represents raw data needed for smart card authentication
#[derive(Clone, Eq, PartialEq, Debug)]
pub struct SmartCardIdentityBuffers {
/// UTF-16 encoded username
pub username: Utf16String,
/// DER-encoded X509 certificate
pub certificate: CertificateRaw,
/// UTF-16 encoded smart card name
pub card_name: Option<NonEmpty<Utf16String>>,
/// UTF-16 encoded smart card reader name
pub reader_name: Utf16String,
/// UTF-16 encoded smart card key container name
pub container_name: Option<NonEmpty<Utf16String>>,
/// UTF-16 encoded smart card CSP name
pub csp_name: Utf16String,
/// UTF-16 encoded smart card PIN code
pub pin: Secret<ZeroizedUtf16String>,
/// UTF-16 string with PEM-encoded RSA 2048-bit private key
pub private_key_pem: Option<NonEmpty<Utf16String>>,
/// Smart card type.
pub scard_type: SmartCardType,
}
/// Represents data needed for smart card authentication
#[derive(Debug, Clone, PartialEq)]
pub struct SmartCardIdentity {
/// Username
pub username: String,
/// X509 certificate
pub certificate: Certificate,
/// Smart card reader name
pub reader_name: String,
/// Smart card name
pub card_name: Option<String>,
/// Smart card key container name
pub container_name: Option<String>,
/// Smart card CSP name
pub csp_name: String,
/// ASCII encoded mart card PIN code
pub pin: Secret<Vec<u8>>,
/// RSA 2048-bit private key
pub private_key: Option<SecretPrivateKey>,
/// Smart card type.
pub scard_type: SmartCardType,
}
impl TryFrom<SmartCardIdentity> for SmartCardIdentityBuffers {
type Error = Error;
fn try_from(value: SmartCardIdentity) -> Result<Self, Self::Error> {
let private_key = if let Some(key) = value.private_key {
NonEmpty::new(Utf16String::from(key.as_ref().to_pem_str().map_err(|e| {
Error::new(
ErrorKind::InternalError,
format!("Unable to serialize a smart card private key: {e}"),
)
})?))
} else {
None
};
Ok(Self {
certificate: value.certificate.try_into()?,
reader_name: value.reader_name.into(),
pin: ZeroizedUtf16String(String::from_utf8_lossy(value.pin.as_ref()).as_ref().into()).into(),
username: value.username.into(),
card_name: value.card_name.and_then(|value| NonEmpty::new(value.into())),
container_name: value.container_name.and_then(|value| NonEmpty::new(value.into())),
csp_name: value.csp_name.into(),
private_key_pem: private_key,
scard_type: value.scard_type,
})
}
}
impl TryFrom<&SmartCardIdentityBuffers> for SmartCardIdentity {
type Error = Error;
fn try_from(value: &SmartCardIdentityBuffers) -> Result<Self, Self::Error> {
let private_key = if let Some(key) = &value.private_key_pem {
let pem_string = key.as_ref().to_string();
Some(SecretPrivateKey::new(PrivateKey::from_pem_str(&pem_string).map_err(
|e| {
Error::new(
ErrorKind::InternalError,
format!("Unable to create a PrivateKey from a PEM string: {e}"),
)
},
)?))
} else {
None
};
Ok(Self {
certificate: Certificate::from(&value.certificate),
reader_name: value.reader_name.to_string(),
pin: value.pin.as_ref().0.to_string().into_bytes().into(),
username: value.username.to_string(),
card_name: value.card_name.as_ref().map(NonEmpty::as_ref).map(ToString::to_string),
container_name: value
.container_name
.as_ref()
.map(NonEmpty::as_ref)
.map(ToString::to_string),
csp_name: value.csp_name.to_string(),
private_key,
scard_type: value.scard_type.clone(),
})
}
}
}
#[cfg(feature = "scard")]
pub use self::scard_credentials::{CertificateRaw, SmartCardIdentity, SmartCardIdentityBuffers, SmartCardType};
/// Generic enum that encapsulates raw credentials for any type of authentication
#[derive(Clone, Eq, PartialEq, Debug)]
pub enum CredentialsBuffers {
/// Raw auth identity buffers for the password based authentication
AuthIdentity(AuthIdentityBuffers),
#[cfg(feature = "scard")]
/// Raw smart card identity buffers for the smart card based authentication
SmartCard(SmartCardIdentityBuffers),
/// Pre-derived Kerberos long-term key for keytab-based client authentication
Keytab(KeytabIdentity),
}
impl CredentialsBuffers {
pub fn into_auth_identity(self) -> Option<AuthIdentityBuffers> {
match self {
CredentialsBuffers::AuthIdentity(identity) => Some(identity),
_ => None,
}
}
pub fn to_auth_identity(&self) -> Option<AuthIdentityBuffers> {
match self {
CredentialsBuffers::AuthIdentity(identity) => Some(identity.clone()),
_ => None,
}
}
pub fn as_auth_identity(&self) -> Option<&AuthIdentityBuffers> {
match self {
CredentialsBuffers::AuthIdentity(identity) => Some(identity),
_ => None,
}
}
pub fn as_mut_auth_identity(&mut self) -> Option<&mut AuthIdentityBuffers> {
match self {
CredentialsBuffers::AuthIdentity(identity) => Some(identity),
_ => None,
}
}
}
/// Generic enum that encapsulates credentials for any type of authentication
#[derive(Clone, PartialEq, Debug)]
pub enum Credentials {
/// Auth identity for the password based authentication
AuthIdentity(AuthIdentity),
/// Smart card identity for the smart card based authentication
#[cfg(feature = "scard")]
SmartCard(Box<SmartCardIdentity>),
/// Pre-derived Kerberos long-term key for keytab-based client authentication
Keytab(KeytabIdentity),
}
impl Credentials {
pub fn to_auth_identity(&self) -> Option<AuthIdentity> {
match self {
Credentials::AuthIdentity(identity) => Some(identity.clone()),
_ => None,
}
}
pub fn auth_identity(self) -> Option<AuthIdentity> {
match self {
Credentials::AuthIdentity(identity) => Some(identity),
_ => None,
}
}
}
#[cfg(feature = "scard")]
impl From<SmartCardIdentity> for Credentials {
fn from(value: SmartCardIdentity) -> Self {
Self::SmartCard(Box::new(value))
}
}
impl From<AuthIdentity> for Credentials {
fn from(value: AuthIdentity) -> Self {
Self::AuthIdentity(value)
}
}
impl From<KeytabIdentity> for Credentials {
fn from(value: KeytabIdentity) -> Self {
Self::Keytab(value)
}
}
impl TryFrom<Credentials> for CredentialsBuffers {
type Error = Error;
fn try_from(value: Credentials) -> Result<Self, Self::Error> {
Ok(match value {
Credentials::AuthIdentity(identity) => Self::AuthIdentity(identity.into()),
#[cfg(feature = "scard")]
Credentials::SmartCard(identity) => Self::SmartCard((*identity).try_into()?),
Credentials::Keytab(identity) => Self::Keytab(identity),
})
}
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::*;
#[test]
fn username_format_conversion() {
proptest!(|(value in "[a-zA-Z0-9.]{1,3}@?\\\\?[a-zA-Z0-9.]{1,3}@?\\\\?[a-zA-Z0-9.]{1,3}")| {
let res = Username::parse(&value);
prop_assume!(res.is_ok());
let initial_username = res.unwrap();
assert_eq!(initial_username.inner(), value);
// The "domain-ish" component, whatever its format-specific meaning is.
let domain = match initial_username.parts() {
UsernameParts::UserPrincipalName(upn) => Some(upn.suffix()),
UsernameParts::DownLevelLogonName(dlln) => dlln.netbios_domain(),
};
if let Some(domain) = domain {
let reconstructed_upn = Username::new_upn(initial_username.account_name(), domain).expect("UPN");
assert_eq!(reconstructed_upn.account_name(), initial_username.account_name());
assert_eq!(
reconstructed_upn.parts(),
UsernameParts::UserPrincipalName(UserPrincipalNameParts {
account_name: initial_username.account_name(),
suffix: domain,
upn: reconstructed_upn.inner(),
})
);
}
// A down-level user name can't contain a @ in the account name
if !initial_username.account_name().contains('@') {
let netbios_name = Username::new(initial_username.account_name(), domain).expect("NetBIOS");
assert_eq!(netbios_name.format(), UserNameFormat::DownLevelLogonName);
assert_eq!(netbios_name.account_name(), initial_username.account_name());
let netbios_domain = match netbios_name.parts() {
UsernameParts::DownLevelLogonName(dlln) => dlln.netbios_domain(),
UsernameParts::UserPrincipalName(_) => unreachable!("constructed as a down-level logon name"),
};
assert_eq!(netbios_domain, domain);
}
})
}
fn check_round_trip_property(username: &Username) {
let round_trip = Username::parse(username.inner()).expect("round-trip parse");
assert_eq!(*username, round_trip);
}
#[test]
fn upn_round_trip() {
proptest!(|(account_name in "[a-zA-Z0-9@.]{1,3}", domain_name in "[a-z0-9.]{1,3}")| {
let username = Username::new_upn(&account_name, &domain_name).expect("UPN");
assert_eq!(username.account_name(), account_name);
assert_eq!(username.format(), UserNameFormat::UserPrincipalName);
assert_eq!(
username.parts(),
UsernameParts::UserPrincipalName(UserPrincipalNameParts {
account_name: &account_name,
suffix: &domain_name,
upn: username.inner(),
})
);
check_round_trip_property(&username);
})
}
#[test]
fn down_level_logon_name_round_trip() {
proptest!(|(account_name in "[a-zA-Z0-9.]{1,3}", domain_name in "[A-Z0-9.]{1,3}")| {
let username = Username::new_down_level_logon_name(&account_name, &domain_name).expect("down-level logon name");
assert_eq!(username.account_name(), account_name);
assert_eq!(username.format(), UserNameFormat::DownLevelLogonName);
assert_eq!(
username.parts(),
UsernameParts::DownLevelLogonName(DownLevelLogonNameParts {
account_name: &account_name,
netbios_domain: Some(&domain_name),
})
);
check_round_trip_property(&username);
})
}
#[test]
fn down_level_logon_name_without_domain_parts() {
// When a bare name (no `\` and no `@`) is parsed, it is a down-level logon name with no
// NetBIOS domain: the `netbios_domain` field must be `None`.
proptest!(|(account_name in "[a-zA-Z0-9.]{1,3}")| {
let username = Username::parse(&account_name).expect("parse");
assert_eq!(username.account_name(), account_name);
assert_eq!(username.format(), UserNameFormat::DownLevelLogonName);
assert_eq!(
username.parts(),
UsernameParts::DownLevelLogonName(DownLevelLogonNameParts {
account_name: &account_name,
netbios_domain: None,
})
);
check_round_trip_property(&username);
})
}
#[test]
fn eq_ignore_ascii_case_matches_within_format() {
// UPNs that differ only by ASCII case are equal.
let upn = Username::new_upn("Alice", "Example.COM").expect("upn");
let upn_other_case = Username::new_upn("alice", "example.com").expect("upn");
assert!(upn.eq_ignore_ascii_case(&upn_other_case));
// Down-level logon names that differ only by ASCII case are equal.
let dlln = Username::new_down_level_logon_name("Bob", "EXAMPLE").expect("dlln");
let dlln_other_case = Username::new_down_level_logon_name("bob", "example").expect("dlln");
assert!(dlln.eq_ignore_ascii_case(&dlln_other_case));
// Bare down-level logon names (no NetBIOS domain) compare on the account name only.
let bare = Username::parse("Carol").expect("parse");
let bare_other_case = Username::parse("carol").expect("parse");
assert!(bare.eq_ignore_ascii_case(&bare_other_case));
}
#[test]
fn eq_ignore_ascii_case_distinguishes_components_and_formats() {
// Different account names are not equal.
let alice = Username::new_upn("alice", "example.com").expect("upn");
let bob = Username::new_upn("bob", "example.com").expect("upn");
assert!(!alice.eq_ignore_ascii_case(&bob));
// A present NetBIOS domain never equals an absent one.
let with_domain = Username::new_down_level_logon_name("alice", "EXAMPLE").expect("dlln");
let without_domain = Username::parse("alice").expect("parse");
assert!(!with_domain.eq_ignore_ascii_case(&without_domain));
// A UPN and a down-level logon name are distinct identities even with matching components:
// the UPN suffix must never be conflated with the NetBIOS domain across formats.
let upn = Username::new_upn("alice", "example").expect("upn");
let dlln = Username::new_down_level_logon_name("alice", "example").expect("dlln");
assert!(!upn.eq_ignore_ascii_case(&dlln));
}
#[test]
fn auth_identity_buffers_round_trip_preserves_format() {
// Converting to `AuthIdentityBuffers` and back must preserve the user name format: a UPN
// must not silently degrade into a down-level logon name.
for username in [
Username::new_upn("alice", "example.com").expect("upn"),
Username::new_upn("bob@dept", "example.com").expect("upn with @ in account name"),
Username::new_down_level_logon_name("carol", "EXAMPLE").expect("dlln"),
// An empty NetBIOS domain is normalized to a separator-less down-level logon name.
Username::new_down_level_logon_name("erin", "").expect("dlln without domain"),
Username::parse("dave").expect("bare name"),
] {
let identity = AuthIdentity {
username: username.clone(),
password: String::new().into(),
};
let buffers = AuthIdentityBuffers::from(identity);
let round_trip = AuthIdentity::try_from(&buffers).expect("round-trip");
assert_eq!(round_trip.username, username);
assert_eq!(round_trip.username.format(), username.format());
}
}
#[test]
fn empty_netbios_domain_is_normalized_to_no_domain() {
// An empty NetBIOS domain means "no domain": it must not produce a distinct `Some("")` view
// that would compare unequal to a separator-less down-level logon name.
let empty_domain = Username::new_down_level_logon_name("alice", "").expect("dlln without domain");
let bare = Username::parse("alice").expect("parse");
assert_eq!(empty_domain, bare);
assert_eq!(empty_domain.inner(), "alice");
assert!(matches!(
empty_domain.parts(),
UsernameParts::DownLevelLogonName(dlln) if dlln.netbios_domain().is_none()
));
assert!(empty_domain.eq_ignore_ascii_case(&bare));
}
}