Skip to content

Commit

Permalink
feat: Kerberos smart card credentials (#143)
Browse files Browse the repository at this point in the history
This adds Kerberos smart card credentials handling.

Note: it doesn't contain actual smart card logon implementation or code
for working with smart cards (follow-up patches incoming).

- Adds the `scard` feature to enable the smart card logon. This feature is only supported on Windows.
- Improves the `ffi` bindings. Now users can pass the smart card credentials using the
  `AcquireCredentialsHandleA/W` function. How it works: the smart card credentials are always marshaled
  into a string that always starts with the "@" character.
- Adds a `CredentialsBuffers`, `Credentials`, `SmartCardIdentityBuffers`, and `SmartCardIdentity` structure
  to be able to handle any kind of credentials.
- Improves `TsCredentials` encoding. For the `TsCredentials`, `TsSmartCardCreds`, and `TsCspDataDetail`
  structures I use the `picky-krb` crate.

References:

* [MS-CSSP: TSCredentials](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cssp/94a1ab00-5500-42fd-8d3d-7a84e6c2cf03).
* [CredUnmarshalCredentialW](https://learn.microsoft.com/en-us/windows/win32/api/wincred/nf-wincred-credunmarshalcredentialw).
  • Loading branch information
TheBestTvarynka authored Aug 25, 2023
1 parent ba3af51 commit 6250e8a
Show file tree
Hide file tree
Showing 25 changed files with 1,242 additions and 220 deletions.
18 changes: 12 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ network_client = ["dep:reqwest", "dep:portpicker"]
dns_resolver = ["dep:trust-dns-resolver", "dep:tokio"]
# TSSSP should be used only on Windows as a native CREDSSP replacement
tsssp = ["dep:rustls"]
# Turns on Kerberos smart card login (available only on Windows and users WinSCard API)
scard = ["dep:pcsc"]

[dependencies]
byteorder = "1.2.7"
Expand All @@ -46,20 +48,23 @@ serde = "1.0"
serde_derive = "1.0"
url = "2.2.2"
reqwest = { version = "0.11", features = ["blocking", "rustls-tls", "rustls-tls-native-roots"], optional = true, default-features = false }
picky = { version = "7.0.0-rc.4", default-features = false }
picky-krb = "0.6.0"
picky-asn1 = { version = "0.7.1", features = ["chrono_conversion"] }
picky-asn1-der = "0.4.0"
picky-asn1-x509 = { version = "0.9.0", features = ["pkcs7"] }

picky = { version = "7.0.0-rc.8", default-features = false }
picky-asn1 = { version = "0.8.0", features = ["chrono_conversion"] }
picky-asn1-der = "0.4.1"
picky-asn1-x509 = { version = "0.12.0", features = ["pkcs7"] }
picky-krb = "0.8.0"

oid = "0.2.1"
uuid = { version = "1.1", features = ["v4"] }
uuid = { version = "1.3", features = ["v4"] }
trust-dns-resolver = { version = "0.21.2", optional = true }
portpicker = { version = "0.1.1", optional = true }
num-bigint-dig = "0.8.1"
tracing = "0.1.37"
rustls = { version = "0.20.7", features = ["dangerous_configuration"], optional = true }
zeroize = { version = "1.5.7", features = ["zeroize_derive"] }
tokio = { version = "1.1", features = ["time", "rt"], optional = true }
pcsc = { version = "2.8.0", optional = true }

[target.'cfg(windows)'.dependencies]
winreg = "0.10"
Expand All @@ -75,3 +80,4 @@ tokio = { version = "1.1", features = ["time", "rt"] }
[dev-dependencies]
static_assertions = "1.1"
whoami = "0.5"
picky = { version = "7.0.0-rc.8", features = ["x509"] }
1 change: 1 addition & 0 deletions ffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ crate-type = ["cdylib"]
[features]
default = []
tsssp = ["sspi/tsssp"]
scard = ["sspi/scard"]

[dependencies]
cfg-if = "0.1"
Expand Down
4 changes: 2 additions & 2 deletions ffi/src/sec_handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use sspi::kerberos::config::KerberosConfig;
use sspi::network_client::reqwest_network_client::{RequestClientFactory, ReqwestNetworkClient};
use sspi::ntlm::NtlmConfig;
use sspi::{
kerberos, negotiate, ntlm, pku2u, AuthIdentityBuffers, ClientRequestFlags, DataRepresentation, Error, ErrorKind,
kerberos, negotiate, ntlm, pku2u, ClientRequestFlags, CredentialsBuffers, DataRepresentation, Error, ErrorKind,
Kerberos, Negotiate, NegotiateConfig, Ntlm, Result, Secret, Sspi, SspiImpl,
};
#[cfg(target_os = "windows")]
Expand Down Expand Up @@ -82,7 +82,7 @@ pub type PCredHandle = *mut SecHandle;
pub type PCtxtHandle = *mut SecHandle;

pub struct CredentialsHandle {
pub credentials: AuthIdentityBuffers,
pub credentials: CredentialsBuffers,
pub security_package_name: String,
pub attributes: CredentialsAttributes,
}
Expand Down
173 changes: 139 additions & 34 deletions ffi/src/sec_winnt_auth_identity.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
use std::slice::from_raw_parts;

use libc::{c_char, c_void};
use sspi::{AuthIdentityBuffers, Error, ErrorKind, Result};
#[cfg(windows)]
use sspi::Secret;
#[cfg(feature = "scard")]
use sspi::SmartCardIdentityBuffers;
use sspi::{AuthIdentityBuffers, CredentialsBuffers, Error, ErrorKind, Result};
#[cfg(windows)]
use symbol_rename_macro::rename_symbol;
#[cfg(feature = "scard")]
use winapi::um::wincred::CredIsMarshaledCredentialW;
#[cfg(feature = "tsssp")]
use windows_sys::Win32::Security::Authentication::Identity::SspiIsAuthIdentityEncrypted;

use crate::sspi_data_types::{SecWChar, SecurityStatus};
use crate::utils::{c_w_str_to_string, into_raw_ptr, raw_str_into_bytes};
Expand Down Expand Up @@ -158,25 +166,39 @@ pub unsafe fn get_auth_data_identity_version_and_flags(p_auth_data: *const c_voi
}
}

// This function determines what format credentials have: ASCII or UNICODE,
// and then calls an appropriate raw credentials handler function.
// Why do we need such a function:
// Actually, on Linux FreeRDP can pass UNICODE credentials into the AcquireCredentialsHandleA function.
// So, we need to be able to handle any credentials format in the AcquireCredentialsHandleA/W functions.
pub unsafe fn auth_data_to_identity_buffers(
security_package_name: &str,
p_auth_data: *const c_void,
package_list: &mut Option<String>,
) -> Result<AuthIdentityBuffers> {
) -> Result<CredentialsBuffers> {
let (_, auth_flags) = get_auth_data_identity_version_and_flags(p_auth_data);

if (auth_flags & SEC_WINNT_AUTH_IDENTITY_UNICODE) != 0 {
auth_data_to_identity_buffers_w(security_package_name, p_auth_data, package_list)
} else {
let rawcreds = std::slice::from_raw_parts(p_auth_data as *const u8, 128);
debug!(?rawcreds);

#[cfg(feature = "tsssp")]
if SspiIsAuthIdentityEncrypted(p_auth_data) != 0 {
let credssp_cred = p_auth_data.cast::<CredSspCred>().as_ref().unwrap();
return unpack_sec_winnt_auth_identity_ex2_w(credssp_cred.p_spnego_cred);
}

if (auth_flags & SEC_WINNT_AUTH_IDENTITY_ANSI) != 0 {
auth_data_to_identity_buffers_a(security_package_name, p_auth_data, package_list)
} else {
auth_data_to_identity_buffers_w(security_package_name, p_auth_data, package_list)
}
}

pub unsafe fn auth_data_to_identity_buffers_a(
_security_package_name: &str,
p_auth_data: *const c_void,
package_list: &mut Option<String>,
) -> Result<AuthIdentityBuffers> {
) -> Result<CredentialsBuffers> {
#[cfg(feature = "tsssp")]
if _security_package_name == sspi::credssp::sspi_cred_ssp::PKG_NAME {
let credssp_cred = p_auth_data.cast::<CredSspCred>().as_ref().unwrap();
Expand All @@ -197,26 +219,26 @@ pub unsafe fn auth_data_to_identity_buffers_a(
.to_string(),
);
}
Ok(AuthIdentityBuffers {
Ok(CredentialsBuffers::AuthIdentity(AuthIdentityBuffers {
user: raw_str_into_bytes((*auth_data).user, (*auth_data).user_length as usize),
domain: raw_str_into_bytes((*auth_data).domain, (*auth_data).domain_length as usize),
password: raw_str_into_bytes((*auth_data).password, (*auth_data).password_length as usize).into(),
})
}))
} else {
let auth_data = p_auth_data.cast::<SecWinntAuthIdentityA>();
Ok(AuthIdentityBuffers {
Ok(CredentialsBuffers::AuthIdentity(AuthIdentityBuffers {
user: raw_str_into_bytes((*auth_data).user, (*auth_data).user_length as usize),
domain: raw_str_into_bytes((*auth_data).domain, (*auth_data).domain_length as usize),
password: raw_str_into_bytes((*auth_data).password, (*auth_data).password_length as usize).into(),
})
}))
}
}

pub unsafe fn auth_data_to_identity_buffers_w(
_security_package_name: &str,
p_auth_data: *const c_void,
package_list: &mut Option<String>,
) -> Result<AuthIdentityBuffers> {
) -> Result<CredentialsBuffers> {
#[cfg(feature = "tsssp")]
if _security_package_name == sspi::credssp::sspi_cred_ssp::PKG_NAME {
let credssp_cred = p_auth_data.cast::<CredSspCred>().as_ref().unwrap();
Expand All @@ -234,26 +256,44 @@ pub unsafe fn auth_data_to_identity_buffers_w(
(*auth_data).package_list_length as usize,
)));
}
Ok(AuthIdentityBuffers {
user: raw_str_into_bytes((*auth_data).user as *const _, (*auth_data).user_length as usize * 2),
let user = raw_str_into_bytes((*auth_data).user as *const _, (*auth_data).user_length as usize * 2);
let password = raw_str_into_bytes(
(*auth_data).password as *const _,
(*auth_data).password_length as usize * 2,
)
.into();

// only marshaled smart card creds starts with '@' char
#[cfg(feature = "scard")]
if CredIsMarshaledCredentialW(user.as_ptr() as *const _) != 0 {
return handle_smart_card_creds(user, password);
}

Ok(CredentialsBuffers::AuthIdentity(AuthIdentityBuffers {
user,
domain: raw_str_into_bytes((*auth_data).domain as *const _, (*auth_data).domain_length as usize * 2),
password: raw_str_into_bytes(
(*auth_data).password as *const _,
(*auth_data).password_length as usize * 2,
)
.into(),
})
password,
}))
} else {
let auth_data = p_auth_data.cast::<SecWinntAuthIdentityW>();
Ok(AuthIdentityBuffers {
user: raw_str_into_bytes((*auth_data).user as *const _, (*auth_data).user_length as usize * 2),
let user = raw_str_into_bytes((*auth_data).user as *const _, (*auth_data).user_length as usize * 2);
let password = raw_str_into_bytes(
(*auth_data).password as *const _,
(*auth_data).password_length as usize * 2,
)
.into();

// only marshaled smart card creds starts with '@' char
#[cfg(feature = "scard")]
if CredIsMarshaledCredentialW(user.as_ptr() as *const _) != 0 {
return handle_smart_card_creds(user, password);
}

Ok(CredentialsBuffers::AuthIdentity(AuthIdentityBuffers {
user,
domain: raw_str_into_bytes((*auth_data).domain as *const _, (*auth_data).domain_length as usize * 2),
password: raw_str_into_bytes(
(*auth_data).password as *const _,
(*auth_data).password_length as usize * 2,
)
.into(),
})
password,
}))
}
}

Expand Down Expand Up @@ -287,10 +327,9 @@ unsafe fn get_sec_winnt_auth_identity_ex2_size(p_auth_data: *const c_void) -> u3
}

#[cfg(target_os = "windows")]
pub unsafe fn unpack_sec_winnt_auth_identity_ex2_a(p_auth_data: *const c_void) -> Result<AuthIdentityBuffers> {
pub unsafe fn unpack_sec_winnt_auth_identity_ex2_a(p_auth_data: *const c_void) -> Result<CredentialsBuffers> {
use std::ptr::null_mut;

use sspi::Secret;
use windows_sys::Win32::Security::Credentials::{CredUnPackAuthenticationBufferA, CRED_PACK_PROTECTED_CREDENTIALS};

if p_auth_data.is_null() {
Expand Down Expand Up @@ -364,22 +403,78 @@ pub unsafe fn unpack_sec_winnt_auth_identity_ex2_a(p_auth_data: *const c_void) -
password.as_mut().pop();
auth_identity_buffers.password = password;

Ok(auth_identity_buffers)
Ok(CredentialsBuffers::AuthIdentity(auth_identity_buffers))
}

#[cfg(not(target_os = "windows"))]
pub fn unpack_sec_winnt_auth_identity_ex2_w(_p_auth_data: *const c_void) -> Result<AuthIdentityBuffers> {
pub fn unpack_sec_winnt_auth_identity_ex2_w(_p_auth_data: *const c_void) -> Result<CredentialsBuffers> {
Err(Error::new(
ErrorKind::UnsupportedFunction,
"SecWinntIdentityEx2 is not supported on non Windows systems",
))
}

#[cfg(feature = "scard")]
#[instrument(level = "trace", ret)]
unsafe fn handle_smart_card_creds(mut username: Vec<u8>, password: Secret<Vec<u8>>) -> Result<CredentialsBuffers> {
use std::ptr::null_mut;

use sspi::cert_utils::{finalize_smart_card_info, SmartCardInfo};
use sspi::string_to_utf16;
use winapi::um::wincred::{CertCredential, CredUnmarshalCredentialW, CERT_CREDENTIAL_INFO};

let mut cred_type = 0;
let mut credential = null_mut();

// add wide null char
username.extend_from_slice(&[0, 0]);

if CredUnmarshalCredentialW(username.as_ptr() as *const _, &mut cred_type, &mut credential) == 0 {
return Err(Error::new(
ErrorKind::NoCredentials,
"Cannot unmarshal smart card credentials",
));
}

if cred_type != CertCredential {
return Err(Error::new(
ErrorKind::NoCredentials,
"Unmarshalled smart card credentials is not CRED_MARSHAL_TYPE::CertCredential",
));
}

let cert_credential = credential.cast::<CERT_CREDENTIAL_INFO>();

let (raw_certificate, certificate) =
sspi::cert_utils::extract_certificate_by_thumbprint(&(*cert_credential).rgbHashOfCert)?;

let username = string_to_utf16(sspi::cert_utils::extract_user_name_from_certificate(&certificate)?);
let SmartCardInfo {
key_container_name,
reader_name,
certificate: _,
csp_name,
private_key_file_index,
} = finalize_smart_card_info(&certificate.tbs_certificate.serial_number.0)?;

let creds = CredentialsBuffers::SmartCard(SmartCardIdentityBuffers {
certificate: raw_certificate,
reader_name: string_to_utf16(reader_name),
pin: password,
username,
card_name: None,
container_name: string_to_utf16(key_container_name),
csp_name: string_to_utf16(csp_name),
private_key_file_index: Some(private_key_file_index),
});

Ok(creds)
}

#[cfg(target_os = "windows")]
pub unsafe fn unpack_sec_winnt_auth_identity_ex2_w(p_auth_data: *const c_void) -> Result<AuthIdentityBuffers> {
pub unsafe fn unpack_sec_winnt_auth_identity_ex2_w(p_auth_data: *const c_void) -> Result<CredentialsBuffers> {
use std::ptr::null_mut;

use sspi::Secret;
use windows_sys::Win32::Security::Credentials::{CredUnPackAuthenticationBufferW, CRED_PACK_PROTECTED_CREDENTIALS};

if p_auth_data.is_null() {
Expand Down Expand Up @@ -431,6 +526,16 @@ pub unsafe fn unpack_sec_winnt_auth_identity_ex2_w(p_auth_data: *const c_void) -
));
}

// only marshaled smart card creds starts with '@' char
#[cfg(feature = "scard")]
if CredIsMarshaledCredentialW(username.as_ptr() as *const _) != 0 {
// remove null
let new_len = password.as_ref().len() - 2;
password.as_mut().truncate(new_len);

return handle_smart_card_creds(username, password);
}

let mut auth_identity_buffers = AuthIdentityBuffers::default();

// remove null
Expand All @@ -454,7 +559,7 @@ pub unsafe fn unpack_sec_winnt_auth_identity_ex2_w(p_auth_data: *const c_void) -
password.as_mut().truncate(new_len);
auth_identity_buffers.password = password;

Ok(auth_identity_buffers)
Ok(CredentialsBuffers::AuthIdentity(auth_identity_buffers))
}

#[allow(clippy::missing_safety_doc)]
Expand Down
4 changes: 2 additions & 2 deletions ffi/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::slice::from_raw_parts;

use libc::c_char;
use sspi::AuthIdentityBuffers;
use sspi::CredentialsBuffers;

use crate::credentials_attributes::CredentialsAttributes;
use crate::sec_handle::CredentialsHandle;
Expand Down Expand Up @@ -38,7 +38,7 @@ pub unsafe fn raw_str_into_bytes(raw_buffer: *const c_char, len: usize) -> Vec<u

pub unsafe fn transform_credentials_handle<'a>(
credentials_handle: *mut CredentialsHandle,
) -> Option<(AuthIdentityBuffers, &'a str, &'a CredentialsAttributes)> {
) -> Option<(CredentialsBuffers, &'a str, &'a CredentialsAttributes)> {
if credentials_handle.is_null() {
None
} else {
Expand Down
2 changes: 1 addition & 1 deletion rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[toolchain]
channel = "1.71.1"
components = [ "rustfmt", "clippy" ]
components = [ "rustfmt", "clippy" ]
Loading

0 comments on commit 6250e8a

Please sign in to comment.