diff --git a/Cargo.lock b/Cargo.lock index 99562eb8..83caadd2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -306,9 +306,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.27" +version = "1.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" dependencies = [ "jobserver", "libc", @@ -1434,6 +1434,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "io-uring" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "libc", +] + [[package]] name = "ipconfig" version = "0.3.2" @@ -2397,9 +2408,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.21" +version = "0.12.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8cea6b35bcceb099f30173754403d2eba0a5dc18cea3630fccd88251909288" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" dependencies = [ "base64", "bytes", @@ -3103,15 +3114,17 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.45.1" +version = "1.46.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", "pin-project-lite", + "slab", "socket2", "tokio-macros", "windows-sys 0.52.0", diff --git a/Cargo.toml b/Cargo.toml index 3c1819cf..8ebe0af3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,10 +44,13 @@ sha1 = { version = "0.10", default-features = false } sha2 = "0.10" num-derive = "0.4" num-traits = { version = "0.2", default-features = false } -picky = { version = "7.0.0-rc.12", default-features = false } + +picky = { version = "7.0.0-rc.15", default-features = false } picky-asn1 = "0.10" picky-asn1-der = "0.5" picky-asn1-x509 = "0.14" +picky-krb = "0.11" + tokio = "1.45" ffi-types = { path = "crates/ffi-types" } winscard = { version = "0.2", path = "crates/winscard" } diff --git a/crates/dpapi/src/client.rs b/crates/dpapi/src/client.rs index 40905c99..8e5e54a3 100644 --- a/crates/dpapi/src/client.rs +++ b/crates/dpapi/src/client.rs @@ -213,7 +213,7 @@ async fn get_key( let mut rpc = RpcClient::::connect( &connection_options, AuthProvider::new( - SspiContext::Negotiate(Negotiate::new(negotiate_config.clone()).map_err(AuthError::from)?), + SspiContext::Negotiate(Negotiate::new_client(negotiate_config.clone()).map_err(AuthError::from)?), Credentials::AuthIdentity(AuthIdentity { username: username.clone(), password: password.clone(), @@ -246,7 +246,7 @@ async fn get_key( let mut rpc = RpcClient::::connect( &connection_options, AuthProvider::new( - SspiContext::Negotiate(Negotiate::new(negotiate_config).map_err(AuthError::from)?), + SspiContext::Negotiate(Negotiate::new_client(negotiate_config).map_err(AuthError::from)?), Credentials::AuthIdentity(AuthIdentity { username, password }), server, network_client, diff --git a/crates/dpapi/src/rpc/auth.rs b/crates/dpapi/src/rpc/auth.rs index c86eb168..599650e1 100644 --- a/crates/dpapi/src/rpc/auth.rs +++ b/crates/dpapi/src/rpc/auth.rs @@ -247,7 +247,7 @@ impl<'a> AuthProvider<'a> { .with_context_requirements( // Warning: do not change these flags if you don't know what you are doing. // The absence or presence of some flags can break the RPC auth. For example, - // if you enable the `ClientRequestFlags::USER_TO_USER`, then it will fail. + // if you enable the `ClientRequestFlags::USE_SESSION_KEY`, then it will fail. ClientRequestFlags::MUTUAL_AUTH | ClientRequestFlags::INTEGRITY | ClientRequestFlags::USE_DCE_STYLE diff --git a/examples/server.rs b/examples/server.rs index cb223301..60973130 100644 --- a/examples/server.rs +++ b/examples/server.rs @@ -9,7 +9,7 @@ use std::net::{TcpListener, TcpStream}; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use sspi::{ AuthIdentity, BufferType, CredentialUse, DataRepresentation, EncryptionFlags, Ntlm, SecurityBuffer, - SecurityBufferRef, SecurityStatus, ServerRequestFlags, Sspi, Username, + SecurityBufferRef, SecurityStatus, ServerRequestFlags, Sspi, SspiImpl, Username, }; const IP: &str = "127.0.0.1:8080"; @@ -84,14 +84,14 @@ fn do_authentication(ntlm: &mut Ntlm, identity: &AuthIdentity, mut stream: &mut loop { read_message(&mut stream, &mut input_buffer[0].buffer)?; - let result = ntlm + let builder = ntlm .accept_security_context() .with_credentials_handle(&mut acq_cred_result.credentials_handle) .with_context_requirements(ServerRequestFlags::ALLOCATE_MEMORY) .with_target_data_representation(DataRepresentation::Native) .with_input(&mut input_buffer) - .with_output(&mut output_buffer) - .execute(ntlm)?; + .with_output(&mut output_buffer); + let result = ntlm.accept_security_context_impl(builder)?.resolve_to_result()?; if [SecurityStatus::CompleteAndContinue, SecurityStatus::CompleteNeeded].contains(&result.status) { println!("Completing the token..."); diff --git a/ffi/src/dpapi/api.rs b/ffi/src/dpapi/api.rs index e0048869..bb250bcc 100644 --- a/ffi/src/dpapi/api.rs +++ b/ffi/src/dpapi/api.rs @@ -20,8 +20,7 @@ mod inner { use dpapi::{CryptProtectSecretArgs, CryptUnprotectSecretArgs, Result}; use dpapi_transport::{ProxyOptions, Transport}; use ffi_types::{Dword, LpByte, LpCStr, LpCUuid, LpDword}; - use sspi::network_client::AsyncNetworkClient; - use sspi::{KerberosConfig, Secret}; + use sspi::Secret; use url::Url; use uuid::Uuid; diff --git a/ffi/src/dpapi/mod.rs b/ffi/src/dpapi/mod.rs index c008168a..568f4720 100644 --- a/ffi/src/dpapi/mod.rs +++ b/ffi/src/dpapi/mod.rs @@ -182,7 +182,7 @@ pub unsafe extern "system" fn DpapiProtectSecret( return NTE_INTERNAL_ERROR; } - // SAFETY: Memory allocation should be safe. Moreover, we check for the null value below. + // SAFETY: Memory allocation is safe. Moreover, we check for the null value below. let blob_buf = unsafe { libc::malloc(blob_data.len()) as *mut u8 }; if blob_buf.is_null() { error!("Failed to allocate memory for the output DPAPI blob: blob buf pointer is NULL"); @@ -327,7 +327,7 @@ pub unsafe extern "system" fn DpapiUnprotectSecret( return NTE_INTERNAL_ERROR; } - // SAFETY: Memory allocation should be safe. Moreover, we check for the null value below. + // SAFETY: Memory allocation is safe. Moreover, we check for the null value below. let secret_buf = unsafe { libc::malloc(secret_data.as_ref().len()) as *mut u8 }; if secret_buf.is_null() { error!("Failed to allocate memory for the output DPAPI blob: blob buf pointer is NULL."); diff --git a/ffi/src/dpapi/network_client.rs b/ffi/src/dpapi/network_client.rs index 6801ad45..6e4349fb 100644 --- a/ffi/src/dpapi/network_client.rs +++ b/ffi/src/dpapi/network_client.rs @@ -9,7 +9,10 @@ use sspi::{Error, ErrorKind, NetworkRequest, Result}; pub struct SyncNetworkClient; impl AsyncNetworkClient for SyncNetworkClient { - fn send<'a>(&'a mut self, request: &'a NetworkRequest) -> Pin>> + 'a>> { + fn send<'a>( + &'a mut self, + request: &'a NetworkRequest, + ) -> Pin>> + Send + 'a>> { let request = request.clone(); Box::pin(async move { tokio::task::spawn_blocking(move || ReqwestNetworkClient.send(&request)) diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs index 6de7e445..32b7d8f9 100644 --- a/ffi/src/lib.rs +++ b/ffi/src/lib.rs @@ -1,6 +1,8 @@ #![allow(clippy::missing_safety_doc)] #![allow(clippy::print_stdout)] #![allow(non_snake_case)] +#![deny(unsafe_op_in_unsafe_fn)] +#![warn(clippy::undocumented_unsafe_blocks)] #[macro_use] extern crate tracing; @@ -13,6 +15,4 @@ pub mod logging; pub mod sspi; mod utils; #[cfg(feature = "scard")] -#[deny(unsafe_op_in_unsafe_fn)] -#[warn(clippy::undocumented_unsafe_blocks)] pub mod winscard; diff --git a/ffi/src/sspi/common.rs b/ffi/src/sspi/common.rs index 5a7df832..2ef5f804 100644 --- a/ffi/src/sspi/common.rs +++ b/ffi/src/sspi/common.rs @@ -3,8 +3,8 @@ use std::slice::{from_raw_parts, from_raw_parts_mut}; use libc::{c_ulonglong, c_void}; use num_traits::cast::{FromPrimitive, ToPrimitive}; use sspi::{ - BufferType, DataRepresentation, DecryptionFlags, EncryptionFlags, ErrorKind, SecurityBuffer, SecurityBufferRef, - SecurityBufferType, ServerRequestFlags, Sspi, + BufferType, DataRepresentation, DecryptionFlags, EncryptionFlags, Error, ErrorKind, SecurityBuffer, + SecurityBufferRef, SecurityBufferType, ServerRequestFlags, Sspi, SspiImpl, }; #[cfg(windows)] use symbol_rename_macro::rename_symbol; @@ -25,10 +25,15 @@ use crate::utils::into_raw_ptr; pub unsafe extern "system" fn FreeCredentialsHandle(ph_credential: PCredHandle) -> SecurityStatus { check_null!(ph_credential); - let cred_handle = (*ph_credential).dw_lower as *mut CredentialsHandle; + // SAFETY: `ph_credentials` is not null. We've checked for this above. + let cred_handle = unsafe { (*ph_credential).dw_lower as *mut CredentialsHandle }; check_null!(cred_handle); - let _cred_handle = Box::from_raw(cred_handle); + // SAFETY: `cred_handle` is not null. We've checked for this above. + // We create and allocate credentials handles using `Box::into_raw`. Thus, + // it is safe to deallocate them using `Box::from_raw`. + // The user have to ensure that the credentials handle was created by us. + let _cred_handle = unsafe { Box::from_raw(cred_handle) }; 0 } @@ -58,42 +63,64 @@ pub unsafe extern "system" fn AcceptSecurityContext( check_null!(p_output); check_null!(pf_context_attr); - let credentials_handle = (*ph_credential).dw_lower as *mut CredentialsHandle; + // SAFETY: `ph_credentials` is not null. We've checked for this above. + let credentials_handle = unsafe { (*ph_credential).dw_lower as *mut CredentialsHandle }; - let (auth_data, security_package_name, attributes) = match transform_credentials_handle(credentials_handle) { - Some(data) => data, - None => return ErrorKind::InvalidHandle.to_u32().unwrap(), + // SAFETY: It's safe to call the function, because it has internal null check and proper error handling. + let (auth_data, security_package_name, attributes) = unsafe { + match transform_credentials_handle(credentials_handle) { + Some(data) => data, + None => return ErrorKind::InvalidHandle.to_u32().unwrap(), + } }; - let sspi_context_ptr = try_execute!(p_ctxt_handle_to_sspi_context( + // SAFETY: It's safe to call the function, because: + // *`ph_context` can be null; + // * the value behind `ph_context` must be initialized by ourself: the user does not have to create the [CtxHandle] values ​​themselves. + // * other parameters are type checked. + let mut sspi_context_ptr = try_execute!(unsafe { p_ctxt_handle_to_sspi_context( &mut ph_context, Some(security_package_name), attributes, - )); - - let sspi_context = sspi_context_ptr - .as_mut() - .expect("security context pointer cannot be null"); - - let mut input_tokens = - p_sec_buffers_to_security_buffers(from_raw_parts((*p_input).p_buffers, (*p_input).c_buffers as usize)); + )}); + + // SAFETY: It's safe to call the `as_mut` function, because `sspi_context_ptr` is a local pointer, + // which is initialized by the `p_ctx_handle_to_sspi_context` function. Thus, the value behind this pointer is valid. + let sspi_context = unsafe { sspi_context_ptr.as_mut() }; + + // SAFETY: `p_input` is not null. We've checked for this above. Additionally, we check `p_buffers` for null. + // All other guarantees must be provided by user. + let mut input_tokens = try_execute!(unsafe { + if (*p_input).p_buffers.is_null() { + Err(Error::new(ErrorKind::InvalidParameter, "p_buffers cannot be null")) + } else { + Ok(p_sec_buffers_to_security_buffers(from_raw_parts((*p_input).p_buffers, (*p_input).c_buffers as usize))) + } + }); let mut output_tokens = vec![SecurityBuffer::new(Vec::with_capacity(1024), BufferType::Token)]; - let result_status = sspi_context.accept_security_context() - .with_credentials_handle(&mut Some(auth_data)) + let mut auth_data = Some(auth_data); + let builder = sspi_context.accept_security_context() + .with_credentials_handle(&mut auth_data) .with_context_requirements(ServerRequestFlags::from_bits(f_context_req.try_into().unwrap()).unwrap()) .with_target_data_representation(DataRepresentation::from_u32(target_data_rep.try_into().unwrap()).unwrap()) .with_input(&mut input_tokens) - .with_output(&mut output_tokens) - .execute(sspi_context); + .with_output(&mut output_tokens); + let result_status = try_execute!(sspi_context.accept_security_context_impl(builder)).resolve_with_default_network_client(); - copy_to_c_sec_buffer((*p_output).p_buffers, &output_tokens, false); + // SAFETY: `p_output` is not null. We've checked this above. + try_execute!(unsafe { copy_to_c_sec_buffer((*p_output).p_buffers, &output_tokens, false) }); - (*ph_new_context).dw_lower = sspi_context_ptr as c_ulonglong; - (*ph_new_context).dw_upper = into_raw_ptr(security_package_name.to_owned()) as c_ulonglong; + // SAFETY: `ph_new_context` is not null. We've checked this above. + let ph_new_context = unsafe { ph_new_context.as_mut() }.expect("ph_new_context should not be null"); - *pf_context_attr = f_context_req; + ph_new_context.dw_lower = sspi_context_ptr.as_ptr() as c_ulonglong; + ph_new_context.dw_upper = into_raw_ptr(security_package_name.to_owned()) as c_ulonglong; + // SAFETY: `pf_context_attr` is not null. We've checked this above. + unsafe { + *pf_context_attr = f_context_req; + } let result = try_execute!(result_status); result.status.to_u32().unwrap() @@ -123,16 +150,27 @@ pub unsafe extern "system" fn CompleteAuthToken( check_null!(ph_context); check_null!(p_token); - let sspi_context = try_execute!(p_ctxt_handle_to_sspi_context( + // SAFETY: `p_token` is not null. We've checked this above. + unsafe { check_null!((*p_token).p_buffers); } + + // SAFETY: It's safe to call the function, because: + // *`ph_context` can be null; + // * the value behind `ph_context` must be initialized by ourself: the user does not have to create the [CtxHandle] values ​​themselves. + // * other parameters are type checked. + let mut sspi_context_ptr = try_execute!(unsafe { p_ctxt_handle_to_sspi_context( &mut ph_context, None, &CredentialsAttributes::default() - )) - .as_mut() - .expect("security context pointer cannot be null"); + )}); - let raw_buffers = from_raw_parts((*p_token).p_buffers, (*p_token).c_buffers as usize); - let mut buffers = p_sec_buffers_to_security_buffers(raw_buffers); + // SAFETY: It's safe to call the `as_mut` function, because `sspi_context_ptr` is a local pointer, + // which is initialized by the `p_ctx_handle_to_sspi_context` function. Thus, the value behind this pointer is valid. + let sspi_context = unsafe { sspi_context_ptr.as_mut() }; + + // SAFETY: This function is safe to call because `p_buffers` is not null. We've checked this above. + let raw_buffers = unsafe { from_raw_parts((*p_token).p_buffers, (*p_token).c_buffers as usize) }; + // SAFETY: This function is safe to call because `raw_buffers` is type checked. All other guarantees must be provided by user. + let mut buffers = unsafe { p_sec_buffers_to_security_buffers(raw_buffers) }; sspi_context.complete_auth_token(&mut buffers).map_or_else( |err| err.error_type.to_u32().unwrap(), @@ -150,14 +188,29 @@ pub unsafe extern "system" fn DeleteSecurityContext(mut ph_context: PCtxtHandle) catch_panic!( check_null!(ph_context); - let _context: Box = Box::from_raw(try_execute!(p_ctxt_handle_to_sspi_context( + // SAFETY: It's safe to call the function, because: + // * the value behind `ph_context` must be initialized by ourself: the user does not have to create the [CtxHandle] values ​​themselves. + // * other parameters are type checked. + let mut sspi_context_ptr = try_execute!(unsafe { p_ctxt_handle_to_sspi_context( &mut ph_context, None, &CredentialsAttributes::default() - ))); + )}); + + // SAFETY: It's safe to constructs a box from a raw pointer because: + // * the `sspi_context_ptr` is not null; + // * the value behind `sspi_context_ptr` must be initialized by ourself: the user does not have to create the [CtxHandle] values ​​themselves. + let _context: Box = unsafe { + Box::from_raw(sspi_context_ptr.as_mut()) + }; - if (*ph_context).dw_upper != 0 { - let _name: Box = Box::from_raw((*ph_context).dw_upper as *mut String); + // SAFETY: `ph_context` is not null. We've checked for it above. + let dw_upper = unsafe { (*ph_context).dw_upper }; + if dw_upper != 0 { + // SAFETY: It's safe to constructs a box from a raw pointer because: + // * the `dw_upper` is not equal to zero; + // * the value behind `dw_upper` pointer must be initialized by ourself: the user does not have to create the [CtxHandle] values ​​themselves. + let _name: Box = unsafe { Box::from_raw(dw_upper as *mut String) }; } 0 @@ -226,7 +279,12 @@ pub type VerifySignatureFn = extern "system" fn(PCtxtHandle, PSecBufferDesc, u32 #[no_mangle] pub unsafe extern "system" fn FreeContextBuffer(pv_context_buffer: *mut c_void) -> SecurityStatus { // NOTE: see https://github.com/Devolutions/sspi-rs/pull/141 for rationale behind libc usage. - libc::free(pv_context_buffer); + // SAFETY: Memory deallocation is safe. + // The user must call this function to free buffers allocated by ourself. On our side, we always use `malloc` + // to allocate buffers in in FFI. + unsafe { + libc::free(pv_context_buffer); + } 0 } @@ -270,17 +328,33 @@ pub unsafe extern "system" fn EncryptMessage( check_null!(ph_context); check_null!(p_message); - let sspi_context = try_execute!(p_ctxt_handle_to_sspi_context( + // SAFETY: `p_message` is not null. We've checked this above. + unsafe { check_null!((*p_message).p_buffers); } + + // SAFETY: It's safe to call the function, because: + // *`ph_context` can be null; + // * the value behind `ph_context` must be initialized by ourself: the user does not have to create the [CtxHandle] values ​​themselves. + // * other parameters are type checked. + let mut sspi_context_ptr = try_execute!(unsafe { p_ctxt_handle_to_sspi_context( &mut ph_context, None, &CredentialsAttributes::default() - )) - .as_mut() - .expect("security context pointer cannot be null"); + )}); + + // SAFETY: It's safe to call the `as_mut` function, because `sspi_context_ptr` is a local pointer, + // which is initialized by the `p_ctx_handle_to_sspi_context` function. Thus, the value behind this pointer is valid. + let sspi_context = unsafe { sspi_context_ptr.as_mut() }; - let len = (*p_message).c_buffers as usize; - let raw_buffers = from_raw_parts((*p_message).p_buffers, len); - let mut message = try_execute!(p_sec_buffers_to_decrypt_buffers(raw_buffers)); + // SAFETY: `p_message` is not null. We've checked this above. + let len = unsafe { (*p_message).c_buffers as usize }; + + // SAFETY: `p_message` is not null. We've checked this above. Moreover, we've checked `p_buffers` for null above. + let raw_buffers = unsafe { + from_raw_parts((*p_message).p_buffers, len) + }; + + // SAFETY: The user must provide guarantees about the correctness of buffers in `raw_buffers'. + let mut message = try_execute!(unsafe { p_sec_buffers_to_decrypt_buffers(raw_buffers)}); let result_status = sspi_context.encrypt_message( EncryptionFlags::from_bits(f_qop.try_into().unwrap()).unwrap(), @@ -288,7 +362,9 @@ pub unsafe extern "system" fn EncryptMessage( message_seq_no.try_into().unwrap(), ); - try_execute!(copy_decrypted_buffers((*p_message).p_buffers, message)); + // SAFETY: `p_message` and `p_buffers` are not null. We've checked this above. + // All other guarantees must be provided by user. + try_execute!(unsafe { copy_decrypted_buffers((*p_message).p_buffers, message) }); let result = try_execute!(result_status); result.to_u32().unwrap() @@ -311,17 +387,29 @@ pub unsafe extern "system" fn DecryptMessage( check_null!(ph_context); check_null!(p_message); - let sspi_context = try_execute!(p_ctxt_handle_to_sspi_context( + // SAFETY: `p_message` is not null. We've checked this above. + unsafe { check_null!((*p_message).p_buffers); } + + // SAFETY: It's safe to call the function, because: + // *`ph_context` can be null; + // * the value behind `ph_context` must be initialized by ourself: the user does not have to create the [CtxHandle] values ​​themselves. + // * other parameters are type checked. + let mut sspi_context_ptr = try_execute!(unsafe { p_ctxt_handle_to_sspi_context( &mut ph_context, None, &CredentialsAttributes::default() - )) - .as_mut() - .expect("security context pointer cannot be null"); + )}); + + // SAFETY: It's safe to call the `as_mut` function, because `sspi_context_ptr` is a local pointer, + // which is initialized by the `p_ctx_handle_to_sspi_context` function. Thus, the value behind this pointer is valid. + let sspi_context = unsafe { sspi_context_ptr.as_mut() }; - let len = (*p_message).c_buffers as usize; - let raw_buffers = from_raw_parts((*p_message).p_buffers, len); - let mut message = try_execute!(p_sec_buffers_to_decrypt_buffers(raw_buffers)); + // SAFETY: `p_message` is not null. We've checked this above. + let len = unsafe { (*p_message).c_buffers as usize }; + // SAFETY: `p_message` and `p_buffers` is not null. We've checked this above. + let raw_buffers = unsafe { from_raw_parts((*p_message).p_buffers, len) }; + // SAFETY: The user must provide guarantees about the correctness of buffers in `raw_buffers'. + let mut message = try_execute!(unsafe { p_sec_buffers_to_decrypt_buffers(raw_buffers) }); let (decryption_flags, result_status) = match sspi_context.decrypt_message(&mut message, message_seq_no.try_into().unwrap()) { @@ -329,10 +417,14 @@ pub unsafe extern "system" fn DecryptMessage( Err(error) => (DecryptionFlags::empty(), Err(error)), }; - try_execute!(copy_decrypted_buffers((*p_message).p_buffers, message)); + // SAFETY: `p_message` and `p_buffers` is not null. We've checked this above. + // All other guarantees must be provided by user. + try_execute!(unsafe { copy_decrypted_buffers((*p_message).p_buffers, message) }); // `pf_qop` can be null if this library is used as a CredSsp security package if !pf_qop.is_null() { - *pf_qop = decryption_flags.bits().try_into().unwrap(); + let flags = try_execute!(decryption_flags.bits().try_into(), ErrorKind::InternalError); + // SAFETY: `pf_qop` is not null. We've checked this above. + unsafe { *pf_qop = flags }; } try_execute!(result_status); diff --git a/ffi/src/sspi/credentials_attributes.rs b/ffi/src/sspi/credentials_attributes.rs index b043ae96..0446cf29 100644 --- a/ffi/src/sspi/credentials_attributes.rs +++ b/ffi/src/sspi/credentials_attributes.rs @@ -1,8 +1,9 @@ use std::mem::size_of; +use std::ptr::NonNull; use std::slice::from_raw_parts; use libc::c_void; -use sspi::Result; +use sspi::{Error, ErrorKind, Result}; use super::sspi_data_types::{SecChar, SecWChar}; use super::utils::hostname; @@ -63,28 +64,65 @@ pub struct SecPkgCredentialsKdcProxySettingsW { pub client_tls_cred_length: u16, } -pub unsafe fn extract_kdc_proxy_settings(p_buffer: *mut c_void) -> KdcProxySettings { - let kdc_proxy_settings = p_buffer.cast::(); +/// Extracts [KdcProxySettings]. +/// +/// # Safety: +/// +/// * The pointer value must be [SecPkgCredentialsKdcProxySettingsW]. +/// * The proxy server and client TLS credentials (if any) values must be placed right after the [SecPkgCredentialsKdcProxySettingsW] value. +pub unsafe fn extract_kdc_proxy_settings(p_buffer: NonNull) -> Result { + let p_buffer = p_buffer.as_ptr(); - let proxy_server = String::from_utf16_lossy(from_raw_parts( - p_buffer.add((*kdc_proxy_settings).proxy_server_offset as usize) as *const u16, - (*kdc_proxy_settings).proxy_server_length as usize / size_of::(), - )); + // SAFETY: + // * `p_buffer` is not null: checked above; + // * the user must all other properties of the pointer and the value behind this pointer. + let kdc_proxy_settings = unsafe { + p_buffer + .cast::() + .as_ref() + .expect("p_buffer must not be null") + }; - let client_tls_cred = - if (*kdc_proxy_settings).client_tls_cred_offset != 0 && (*kdc_proxy_settings).client_tls_cred_length != 0 { - Some(String::from_utf16_lossy(from_raw_parts( - p_buffer.add((*kdc_proxy_settings).client_tls_cred_offset as usize) as *const u16, - (*kdc_proxy_settings).client_tls_cred_length as usize, - ))) - } else { - None - }; + let SecPkgCredentialsKdcProxySettingsW { + proxy_server_offset, + proxy_server_length, + client_tls_cred_offset, + client_tls_cred_length, + .. + } = kdc_proxy_settings; + + // SAFETY: `p_buffer` is not null (checked above). `kdc_proxy_settings` was cast from the `p_buffer', + // so it's not null either. + let proxy_server = String::from_utf16_lossy(unsafe { + from_raw_parts( + p_buffer.add(*proxy_server_offset as usize) as *const u16, + *proxy_server_length as usize / size_of::(), + ) + }); + + let client_tls_cred = if *client_tls_cred_offset != 0 && *client_tls_cred_length != 0 { + // SAFETY: `p_buffer` is not null (checked above). + // The client have to ensure that the `client_tls_cred_offset` is valid. + let client_tls_cred_ptr = unsafe { p_buffer.add(*client_tls_cred_offset as usize) } as *const u16; + if client_tls_cred_ptr.is_null() { + return Err(Error::new( + ErrorKind::InvalidParameter, + "client_tls_cred_ptr cannot be null", + )); + } + + // SAFETY: `client_tls_cred_ptr` is not null (checked above). + // The client have to ensure that the `client_tls_cred_length` is valid. + let client_tls_cred_data = unsafe { from_raw_parts(client_tls_cred_ptr, *client_tls_cred_length as usize) }; + Some(String::from_utf16_lossy(client_tls_cred_data)) + } else { + None + }; - KdcProxySettings { + Ok(KdcProxySettings { proxy_server, client_tls_cred, - } + }) } #[repr(C)] diff --git a/ffi/src/sspi/sec_buffer.rs b/ffi/src/sspi/sec_buffer.rs index 3d6c3a43..19056e7c 100644 --- a/ffi/src/sspi/sec_buffer.rs +++ b/ffi/src/sspi/sec_buffer.rs @@ -1,7 +1,7 @@ use std::slice::{from_raw_parts, from_raw_parts_mut}; use libc::c_char; -use sspi::{SecurityBuffer, SecurityBufferType}; +use sspi::{Error, ErrorKind, Result, SecurityBuffer, SecurityBufferType}; #[derive(Debug)] #[repr(C)] @@ -23,6 +23,33 @@ pub struct SecBufferDesc { pub type PSecBufferDesc = *mut SecBufferDesc; +/// # Safety: +/// +/// * The input pointer can be null. +/// * If the input pointer is not null, then it must point to the valid [SecBufferDesc] structure. Moreover, +/// the user have to ensure that the pointer is [convertible to a reference](https://doc.rust-lang.org/std/ptr/index.html#pointer-to-reference-conversion). +pub unsafe fn sec_buffer_desc_to_security_buffers(p_input: PSecBufferDesc) -> Vec { + // SAFETY: + // The user must upheld the [SecBufferDesc] validity and make sure that the pointer is convertible to a reference. + if let Some(input) = unsafe { p_input.as_ref() } { + let p_buffers = input.p_buffers; + let c_buffers = input.c_buffers; + + let sec_buffers = if p_buffers.is_null() { + &[] + } else { + // SAFETY: We checked above that the `p_buffers` is not null. + // The caller must ensure all other guarantees. + unsafe { from_raw_parts(p_buffers, c_buffers as usize) } + }; + + // SAFETY: This function is safe to call because the argument is type checked. + unsafe { p_sec_buffers_to_security_buffers(sec_buffers) } + } else { + Vec::new() + } +} + #[allow(clippy::useless_conversion)] pub(crate) unsafe fn p_sec_buffers_to_security_buffers(raw_buffers: &[SecBuffer]) -> Vec { raw_buffers @@ -31,7 +58,8 @@ pub(crate) unsafe fn p_sec_buffers_to_security_buffers(raw_buffers: &[SecBuffer] buffer: if raw_buffer.pv_buffer.is_null() { Vec::new() } else { - from_raw_parts(raw_buffer.pv_buffer, raw_buffer.cb_buffer as usize) + // SAFETY: `pv_buffer` is not null (checked above). All other guarantees must be provided by the user. + unsafe { from_raw_parts(raw_buffer.pv_buffer, raw_buffer.cb_buffer as usize) } .iter() .map(|v| *v as u8) .collect() @@ -41,17 +69,38 @@ pub(crate) unsafe fn p_sec_buffers_to_security_buffers(raw_buffers: &[SecBuffer] .collect() } -pub(crate) unsafe fn copy_to_c_sec_buffer(to_buffers: PSecBuffer, from_buffers: &[SecurityBuffer], allocate: bool) { - let to_buffers = from_raw_parts_mut(to_buffers, from_buffers.len()); +pub(crate) unsafe fn copy_to_c_sec_buffer( + to_buffers: PSecBuffer, + from_buffers: &[SecurityBuffer], + allocate: bool, +) -> Result<()> { + if to_buffers.is_null() { + return Err(Error::new(ErrorKind::InvalidParameter, "to_buffers cannot be null")); + } + + // SAFETY: `to_buffers` is not null. We've checked for this above. + let to_buffers = unsafe { from_raw_parts_mut(to_buffers, from_buffers.len()) }; for i in 0..from_buffers.len() { let buffer = &from_buffers[i]; let buffer_size = buffer.buffer.len(); to_buffers[i].cb_buffer = buffer_size.try_into().unwrap(); to_buffers[i].buffer_type = buffer.buffer_type.into(); if allocate || to_buffers[i].pv_buffer.is_null() { - to_buffers[i].pv_buffer = libc::malloc(buffer_size) as *mut c_char; + // SAFETY: Memory allocation is safe. Also, we check `pv_buffer` for the null below. + to_buffers[i].pv_buffer = unsafe { libc::malloc(buffer_size) } as *mut c_char; + + if to_buffers[i].pv_buffer.is_null() { + return Err(Error::new( + ErrorKind::InsufficientMemory, + format!("coudln't allocate {buffer_size} bytes"), + )); + } } - let to_buffer = from_raw_parts_mut(to_buffers[i].pv_buffer, buffer_size); - to_buffer.copy_from_slice(from_raw_parts(buffer.buffer.as_ptr() as *const c_char, buffer_size)); + + // SAFETY: `pv_buffer` is not null. We've checked for this above. + // The user must ensure that the `pv_buffer` size is big enough to accommodate the data. + unsafe { (buffer.buffer.as_ptr() as *const c_char).copy_to(to_buffers[i].pv_buffer, buffer_size) } } + + Ok(()) } diff --git a/ffi/src/sspi/sec_handle.rs b/ffi/src/sspi/sec_handle.rs index 5b4cb4f5..b0d919e5 100644 --- a/ffi/src/sspi/sec_handle.rs +++ b/ffi/src/sspi/sec_handle.rs @@ -1,4 +1,5 @@ use std::ffi::CStr; +use std::ptr::NonNull; use std::slice::from_raw_parts; use std::sync::Mutex; @@ -33,7 +34,10 @@ cfg_if::cfg_if! { use super::credentials_attributes::{ extract_kdc_proxy_settings, CredentialsAttributes, SecPkgCredentialsKdcUrlA, SecPkgCredentialsKdcUrlW, }; -use super::sec_buffer::{copy_to_c_sec_buffer, p_sec_buffers_to_security_buffers, PSecBuffer, PSecBufferDesc}; +use super::sec_buffer::{ + copy_to_c_sec_buffer, p_sec_buffers_to_security_buffers, sec_buffer_desc_to_security_buffers, PSecBuffer, + PSecBufferDesc, +}; use super::sec_pkg_info::{RawSecPkgInfoA, RawSecPkgInfoW, SecNegoInfoA, SecNegoInfoW, SecPkgInfoA, SecPkgInfoW}; use super::sec_winnt_auth_identity::auth_data_to_identity_buffers; use super::sspi_data_types::{ @@ -122,9 +126,10 @@ impl SspiImpl for SspiHandle { fn accept_security_context_impl( &mut self, - builder: sspi::builders::FilledAcceptSecurityContext<'_, Self::CredentialsHandle>, - ) -> Result { - self.sspi_context.lock()?.accept_security_context_impl(builder) + builder: sspi::builders::FilledAcceptSecurityContext, + ) -> Result { + let mut context = self.sspi_context.lock()?; + Ok(context.accept_security_context_sync(builder).into()) } } @@ -225,39 +230,47 @@ pub struct CredentialsHandle { fn create_negotiate_context(attributes: &CredentialsAttributes) -> Result { let client_computer_name = attributes.hostname()?; - if let Some(kdc_url) = attributes.kdc_url() { + let negotiate_config = if let Some(kdc_url) = attributes.kdc_url() { let kerberos_config = KerberosConfig::new(&kdc_url, client_computer_name.clone()); - let negotiate_config = NegotiateConfig::new( + + NegotiateConfig::new( Box::new(kerberos_config), attributes.package_list.clone(), client_computer_name, - ); - - Negotiate::new(negotiate_config) + ) } else { - let negotiate_config = NegotiateConfig { + NegotiateConfig { protocol_config: Box::new(NtlmConfig::new(client_computer_name.clone())), package_list: attributes.package_list.clone(), client_computer_name, - }; - Negotiate::new(negotiate_config) - } + } + }; + + Negotiate::new_client(negotiate_config) } +/// Transforms [&mut PCtxtHandle] to [*mut SspiHandle]. +/// +/// *Note*: `*context` **can** be NULL: the function will create a new security context if [PCtxHandle] is NULL. +/// +/// # Safety: +/// +/// The caller have to ensure that either `*(context)` is null or the pointer is [convertible to a reference](https://doc.rust-lang.org/std/ptr/index.html#pointer-to-reference-conversion). #[instrument(ret)] pub(crate) unsafe fn p_ctxt_handle_to_sspi_context( context: &mut PCtxtHandle, security_package_name: Option<&str>, attributes: &CredentialsAttributes, -) -> Result<*mut SspiHandle> { - if context.is_null() { +) -> Result> { + if (*context).is_null() { *context = into_raw_ptr(SecHandle { dw_lower: 0, dw_upper: 0, }); } - if (*(*context)).dw_lower == 0 { + // SAFETY: `*context` is not null. We've checked this above. + if unsafe { (*(*context)).dw_lower } == 0 { if security_package_name.is_none() { return Err(Error::new( ErrorKind::InvalidParameter, @@ -314,13 +327,19 @@ pub(crate) unsafe fn p_ctxt_handle_to_sspi_context( } }); - (*(*context)).dw_lower = into_raw_ptr(sspi_context) as c_ulonglong; - if (*(*context)).dw_upper == 0 { - (*(*context)).dw_upper = into_raw_ptr(name.to_owned()) as c_ulonglong; + // SAFETY: `context` and `*context` are not null. We've checked this above. + let context = unsafe { (*context).as_mut() }.expect("context should not be null"); + context.dw_lower = into_raw_ptr(sspi_context) as c_ulonglong; + if context.dw_upper == 0 { + context.dw_upper = into_raw_ptr(name.to_owned()) as c_ulonglong; } } - Ok((*(*context)).dw_lower as *mut _) + Ok(NonNull::new( + // SAFETY: `context` and `*context` are not null. We've checked this above. + unsafe { (*(*context)).dw_lower as *mut _ }, + ) + .expect("dw_lower must be initialized")) } fn verify_security_package(package_name: &str) -> Result<()> { @@ -359,20 +378,26 @@ pub unsafe extern "system" fn AcquireCredentialsHandleA( check_null!(ph_credential); let security_package_name = - try_execute!(CStr::from_ptr(psz_package).to_str(), ErrorKind::InvalidParameter).to_owned(); + // SAFETY: `psz_package` is not null. We've checked this above. + try_execute!(unsafe { CStr::from_ptr(psz_package) }.to_str(), ErrorKind::InvalidParameter).to_owned(); try_execute!(verify_security_package(&security_package_name)); debug!(?security_package_name); let mut package_list: Option = None; - let credentials = try_execute!(auth_data_to_identity_buffers(&security_package_name, p_auth_data, &mut package_list)); - - (*ph_credential).dw_lower = into_raw_ptr(CredentialsHandle { - credentials, - security_package_name, - attributes: CredentialsAttributes::new_with_package_list(package_list), - }) as c_ulonglong; + // SAFETY: This function is safe to call because `security_package_name` is checked, `p_auth_data` is not null + // and `package_list` is type checked. + let credentials = try_execute!(unsafe { auth_data_to_identity_buffers(&security_package_name, p_auth_data, &mut package_list) }); + + // SAFETY: `ph_credential` is not null. We've checked this above. + unsafe { + (*ph_credential).dw_lower = into_raw_ptr(CredentialsHandle { + credentials, + security_package_name, + attributes: CredentialsAttributes::new_with_package_list(package_list), + }) as c_ulonglong; + } 0 } @@ -413,20 +438,26 @@ pub unsafe extern "system" fn AcquireCredentialsHandleW( check_null!(p_auth_data); check_null!(ph_credential); - let security_package_name = c_w_str_to_string(psz_package); + // SAFETY: `psz_package` is not null. We've checked this above. + let security_package_name = unsafe { c_w_str_to_string(psz_package) }; try_execute!(verify_security_package(&security_package_name)); debug!(?security_package_name); let mut package_list: Option = None; - let credentials = try_execute!(auth_data_to_identity_buffers(&security_package_name, p_auth_data, &mut package_list)); - - (*ph_credential).dw_lower = into_raw_ptr(CredentialsHandle { - credentials, - security_package_name, - attributes: CredentialsAttributes::new_with_package_list(package_list), - }) as c_ulonglong; + // SAFETY: This function is safe to call because `security_package_name` is checked, `p_auth_date` is not null + // and `package_list` is type checked. + let credentials = try_execute!(unsafe { auth_data_to_identity_buffers(&security_package_name, p_auth_data, &mut package_list) }); + + // SAFETY: `ph_credentials` is not null. We've checked this above. + unsafe { + (*ph_credential).dw_lower = into_raw_ptr(CredentialsHandle { + credentials, + security_package_name, + attributes: CredentialsAttributes::new_with_package_list(package_list), + }) as c_ulonglong; + } 0 } @@ -496,42 +527,50 @@ pub unsafe extern "system" fn InitializeSecurityContextA( check_null!(p_output); check_null!(pf_context_attr); + // SAFETY: `p_output` is not null. We've checked this above. + unsafe { check_null!((*p_output).p_buffers); } + let service_principal = if p_target_name.is_null() { "" } else { - try_execute!(CStr::from_ptr(p_target_name).to_str(), ErrorKind::InvalidParameter) + // SAFETY: `p_target_name` is not null. We've checked this above. + try_execute!(unsafe { CStr::from_ptr(p_target_name) }.to_str(), ErrorKind::InvalidParameter) }; debug!(?service_principal, "Target name (SPN)"); - let credentials_handle = (*ph_credential).dw_lower as *mut CredentialsHandle; + // SAFETY: `ph_credentials` is not null. We've checked this above. + let credentials_handle = unsafe { (*ph_credential).dw_lower as *mut CredentialsHandle }; - let (auth_data, security_package_name, attributes) = match transform_credentials_handle(credentials_handle) { + // SAFETY: This function is safe to call because it can handle `credentials_handle` null pointer. + let (auth_data, security_package_name, attributes) = match unsafe { transform_credentials_handle(credentials_handle) } { Some(creds_handle) => creds_handle, None => return ErrorKind::InvalidHandle.to_u32().unwrap(), }; - let sspi_context_ptr = try_execute!(p_ctxt_handle_to_sspi_context( + // SAFETY: It's safe to call the function, because: + // *`ph_context` can be null; + // * the value behind `ph_context` must be initialized by ourself: the user does not have to create the [CtxHandle] values ​​themselves. + // * other parameters are type checked. + let mut sspi_context_ptr = try_execute!(unsafe { p_ctxt_handle_to_sspi_context( &mut ph_context, Some(security_package_name), attributes - )); - let sspi_context = sspi_context_ptr - .as_mut() - .expect("security context pointer cannot be null"); - - let mut input_tokens = if p_input.is_null() { - Vec::new() - } else { - p_sec_buffers_to_security_buffers(if (*p_input).p_buffers.is_null() { - &[] - } else { - from_raw_parts((*p_input).p_buffers, (*p_input).c_buffers as usize) - }) - }; - - let len = (*p_output).c_buffers as usize; - let raw_buffers = from_raw_parts((*p_output).p_buffers, len); - let mut output_tokens = p_sec_buffers_to_security_buffers(raw_buffers); + )}); + + // SAFETY: It's safe to call the `as_mut` function, because `sspi_context_ptr` is a local pointer, + // which is initialized by the `p_ctx_handle_to_sspi_context` function. Thus, the value behind this pointer is valid. + let sspi_context = unsafe { sspi_context_ptr.as_mut() }; + + // SAFETY: The pointer is allowed to be null and the user have to ensure the validity of the the pointer + // and the data behind it. + let mut input_tokens = unsafe { sec_buffer_desc_to_security_buffers(p_input) }; + + // SAFETY: `p_output` is not null. We've checked this above. + let len = unsafe { (*p_output).c_buffers as usize }; + // SAFETY: `p_output` and `p_buffers` are not null. We've checked this above. + let raw_buffers = unsafe { from_raw_parts((*p_output).p_buffers, len) }; + // SAFETY: This function if safe to call because `raw_buffers` is type checked. + let mut output_tokens = unsafe { p_sec_buffers_to_security_buffers(raw_buffers) }; output_tokens.iter_mut().for_each(|s| s.buffer.clear()); let mut auth_data = Some(auth_data); @@ -547,12 +586,18 @@ pub unsafe extern "system" fn InitializeSecurityContextA( let context_requirements = ClientRequestFlags::from_bits_retain(f_context_req); let allocate = context_requirements.contains(ClientRequestFlags::ALLOCATE_MEMORY); - copy_to_c_sec_buffer((*p_output).p_buffers, &output_tokens, allocate); + // SAFETY: This function is safe to call because `p_output` and `p_buffers` are not null, + // `output_tokens` is local variable initialized by the `p_sec_buffers_to_security_buffers` function + // and `allocate` is type checked. + try_execute!(unsafe { copy_to_c_sec_buffer((*p_output).p_buffers, &output_tokens, allocate) }); - (*ph_new_context).dw_lower = sspi_context_ptr as c_ulonglong; - (*ph_new_context).dw_upper = (*ph_context).dw_upper; + // SAFETY: `ph_new_context', `ph_context` and `pf_context_attr` are not null. We've checked this above. + unsafe { + (*ph_new_context).dw_lower = sspi_context_ptr.as_ptr() as c_ulonglong; + (*ph_new_context).dw_upper = (*ph_context).dw_upper; - *pf_context_attr = f_context_req; + *pf_context_attr = f_context_req; + } let result = try_execute!(result_status); result.status.to_u32().unwrap() @@ -600,41 +645,48 @@ pub unsafe extern "system" fn InitializeSecurityContextW( check_null!(p_output); check_null!(pf_context_attr); + // SAFETY: `p_output` is not null. We've checked this above. + unsafe { check_null!((*p_output).p_buffers); } + let service_principal = if p_target_name.is_null() { String::new() } else { - c_w_str_to_string(p_target_name) + // SAFETY: `p_target_name` is not null. We've checked this above. + unsafe { c_w_str_to_string(p_target_name) } }; debug!(?service_principal, "Target name (SPN)"); - let credentials_handle = (*ph_credential).dw_lower as *mut CredentialsHandle; + // SAFETY: `ph_credentials` is not null. We've checked this above. + let credentials_handle = unsafe { (*ph_credential).dw_lower as *mut CredentialsHandle }; - let (auth_data, security_package_name, attributes) = match transform_credentials_handle(credentials_handle) { + // SAFETY: This function is safe to call because it can handle `credentials_handle` when it's null pointer. + let (auth_data, security_package_name, attributes) = match unsafe { transform_credentials_handle(credentials_handle) } { Some(creds_handle) => creds_handle, None => return ErrorKind::InvalidHandle.to_u32().unwrap(), }; - let sspi_context_ptr = try_execute!(p_ctxt_handle_to_sspi_context( + // SAFETY: It's safe to call the function, because: + // *`ph_context` can be null; + // * the value behind `ph_context` must be initialized by ourself: the user does not have to create the [CtxHandle] values ​​themselves. + // * other parameters are type checked. + let mut sspi_context_ptr = try_execute!(unsafe { p_ctxt_handle_to_sspi_context( &mut ph_context, Some(security_package_name), attributes, - )); - let sspi_context = sspi_context_ptr - .as_mut() - .expect("security context pointer cannot be null"); + )}); - let mut input_tokens = if p_input.is_null() { - Vec::new() - } else { - p_sec_buffers_to_security_buffers(if (*p_input).p_buffers.is_null() { - &[] - } else { - from_raw_parts((*p_input).p_buffers, (*p_input).c_buffers as usize) - }) - }; + // SAFETY: It's safe to call the `as_mut` function, because `sspi_context_ptr` is a local pointer, + // which is initialized by the `p_ctx_handle_to_sspi_context` function. Thus, the value behind this pointer is valid. + let sspi_context = unsafe { sspi_context_ptr.as_mut() }; + + // SAFETY: The pointer is allowed to be null and the user have to ensure the validity of the the pointer + // and the data behind it. + let mut input_tokens = unsafe { sec_buffer_desc_to_security_buffers(p_input) }; - let raw_buffers = from_raw_parts((*p_output).p_buffers, (*p_output).c_buffers as usize); - let mut output_tokens = p_sec_buffers_to_security_buffers(raw_buffers); + // SAFETY: `p_output` and `p_buffers` are not null. We've checked this above. + let raw_buffers = unsafe { from_raw_parts((*p_output).p_buffers, (*p_output).c_buffers as usize) }; + // SAFETY: This function is safe to call because `raw_buffers` is type checked. + let mut output_tokens = unsafe { p_sec_buffers_to_security_buffers(raw_buffers) }; output_tokens.iter_mut().for_each(|s| s.buffer.clear()); let mut auth_data = Some(auth_data); @@ -650,12 +702,18 @@ pub unsafe extern "system" fn InitializeSecurityContextW( let context_requirements = ClientRequestFlags::from_bits_retain(f_context_req); let allocate = context_requirements.contains(ClientRequestFlags::ALLOCATE_MEMORY); - copy_to_c_sec_buffer((*p_output).p_buffers, &output_tokens, allocate); + // SAFETY: This function is safe to call because `p_output` and `p_buffers` are not null, + // `output_tokens` is local variable initialized by the `p_sec_buffers_to_security_buffers` function + // and `allocate` is type checked. + try_execute!(unsafe { copy_to_c_sec_buffer((*p_output).p_buffers, &output_tokens, allocate) }); - *pf_context_attr = f_context_req; + // SAFETY: `ph_new_context', `ph_context` and `pf_context_attr` are not null. We've checked this above. + unsafe { + *pf_context_attr = f_context_req; - (*ph_new_context).dw_lower = sspi_context_ptr as c_ulonglong; - (*ph_new_context).dw_upper = (*ph_context).dw_upper; + (*ph_new_context).dw_lower = sspi_context_ptr.as_ptr() as c_ulonglong; + (*ph_new_context).dw_upper = (*ph_context).dw_upper; + } let result = try_execute!(result_status); result.status.to_u32().unwrap() @@ -685,13 +743,19 @@ unsafe fn query_context_attributes_common( is_wide: bool, ) -> SecurityStatus { catch_panic! { - let sspi_context = try_execute!(p_ctxt_handle_to_sspi_context( + // SAFETY: It's safe to call the function, because: + // *`ph_context` can be null; + // * the value behind `ph_context` must be initialized by ourself: the user does not have to create the [CtxHandle] values ​​themselves. + // * other parameters are type checked. + let mut sspi_context_ptr = try_execute!(unsafe { p_ctxt_handle_to_sspi_context( &mut ph_context, None, &CredentialsAttributes::default() - )) - .as_mut() - .expect("security context pointer cannot be null"); + )}); + + // SAFETY: It's safe to call the `as_mut` function, because `sspi_context_ptr` is a local pointer, + // which is initialized by the `p_ctx_handle_to_sspi_context` function. Thus, the value behind this pointer is valid. + let sspi_context = unsafe { sspi_context_ptr.as_mut() }; check_null!(p_buffer); @@ -701,10 +765,13 @@ unsafe fn query_context_attributes_common( let pkg_sizes = try_execute!(sspi_context.query_context_sizes()); - (*sizes).cb_max_token = pkg_sizes.max_token; - (*sizes).cb_max_signature = pkg_sizes.max_signature; - (*sizes).cb_block_size = pkg_sizes.block; - (*sizes).cb_security_trailer = pkg_sizes.security_trailer; + // SAFETY: `sizes` was cast from `p_buffer` which is not null. We've checked this above. + let sizes = unsafe { sizes.as_mut() }.expect("sized pointer should not be null"); + + sizes.cb_max_token = pkg_sizes.max_token; + sizes.cb_max_signature = pkg_sizes.max_signature; + sizes.cb_block_size = pkg_sizes.block; + sizes.cb_security_trailer = pkg_sizes.security_trailer; return 0; } @@ -714,17 +781,23 @@ unsafe fn query_context_attributes_common( if is_wide { let nego_info = p_buffer.cast::(); - (*nego_info).nego_state = SECPKG_NEGOTIATION_COMPLETE.try_into().unwrap(); + // SAFETY: `nego_info` was cast from `p_buffer` which is not null. We've checked this above. + let nego_info = unsafe { nego_info.as_mut() }.expect("nego_info pointer should not be null"); + + nego_info.nego_state = SECPKG_NEGOTIATION_COMPLETE.try_into().unwrap(); let package_info: RawSecPkgInfoW = package_info.into(); - (*nego_info).package_info = package_info.0; + nego_info.package_info = package_info.0; } else { let nego_info = p_buffer.cast::(); - (*nego_info).nego_state = SECPKG_NEGOTIATION_COMPLETE.try_into().unwrap(); + // SAFETY: `nego_info` was cast from `p_buffer` which is not null. We've checked this above. + let nego_info = unsafe { nego_info.as_mut() }.expect("nego_info pointer should not be null"); + + nego_info.nego_state = SECPKG_NEGOTIATION_COMPLETE.try_into().unwrap(); let package_info: RawSecPkgInfoA = package_info.into(); - (*nego_info).package_info = package_info.0; + nego_info.package_info = package_info.0; } return 0; @@ -734,11 +807,14 @@ unsafe fn query_context_attributes_common( let stream_info = p_buffer.cast::(); - (*stream_info).cb_header = stream_sizes.header; - (*stream_info).cb_trailer = stream_sizes.trailer; - (*stream_info).cb_maximum_message = stream_sizes.max_message; - (*stream_info).c_buffers = stream_sizes.buffers; - (*stream_info).cb_block_size = stream_sizes.block_size; + // SAFETY: `stream_info` was cast from `p_buffer` which is not null. We've checked this above. + let stream_info = unsafe { stream_info.as_mut() }.expect("stream_info pointer should not be null"); + + stream_info.cb_header = stream_sizes.header; + stream_info.cb_trailer = stream_sizes.trailer; + stream_info.cb_maximum_message = stream_sizes.max_message; + stream_info.c_buffers = stream_sizes.buffers; + stream_info.cb_block_size = stream_sizes.block_size; return 0; } @@ -749,7 +825,8 @@ unsafe fn query_context_attributes_common( let cert_context = try_execute!(sspi_context.query_context_remote_cert()); - let store = CertOpenStore(CERT_STORE_PROV_MEMORY, 0, 0, CERT_STORE_CREATE_NEW_FLAG, null()); + // SAFETY: This function is safe to call because all arguments are type checked. + let store = unsafe { CertOpenStore(CERT_STORE_PROV_MEMORY, 0, 0, CERT_STORE_CREATE_NEW_FLAG, null()) }; if store.is_null() { return ErrorKind::InternalError.to_u32().unwrap(); @@ -757,20 +834,22 @@ unsafe fn query_context_attributes_common( let mut p_cert_context = null_mut(); - let result = CertAddEncodedCertificateToStore( + // SAFETY: This function is safe to call because all arguments are type checked. + let result = unsafe { CertAddEncodedCertificateToStore( store, cert_context.encoding_type.to_u32().unwrap(), cert_context.raw_cert.as_ptr(), cert_context.raw_cert.len() as u32, CERT_STORE_ADD_REPLACE_EXISTING, &mut p_cert_context - ); + )}; if result != 1 { return std::io::Error::last_os_error().raw_os_error().unwrap_or_else(|| ErrorKind::InternalError.to_i32().unwrap()) as u32; } let p_cert_buffer = p_buffer.cast::<*const CERT_CONTEXT>(); - *p_cert_buffer = p_cert_context; + // SAFETY: `p_cert_buffer` was cast from `p_buffer`, so it's not null either. + unsafe { *p_cert_buffer = p_cert_context; } return 0; } else { @@ -784,7 +863,8 @@ unsafe fn query_context_attributes_common( }; let sec_context_flags = p_buffer.cast::<*mut SecPkgContextFlags>(); - *sec_context_flags = into_raw_ptr(flags); + // SAFETY: `sec_context_flags` was cast from `p_buffer` which is not null. We've checked this above. + unsafe { *sec_context_flags = into_raw_ptr(flags); } return 0; } @@ -792,14 +872,15 @@ unsafe fn query_context_attributes_common( let connection_info = try_execute!(sspi_context.query_context_connection_info()); let sec_pkg_context_connection_info = p_buffer.cast::(); - - (*sec_pkg_context_connection_info).dw_protocol = connection_info.protocol.to_u32().unwrap(); - (*sec_pkg_context_connection_info).ai_cipher = connection_info.cipher.to_u32().unwrap(); - (*sec_pkg_context_connection_info).dw_cipher_strength = connection_info.cipher_strength; - (*sec_pkg_context_connection_info).ai_hash = connection_info.hash.to_u32().unwrap(); - (*sec_pkg_context_connection_info).dw_hash_strength = connection_info.hash_strength; - (*sec_pkg_context_connection_info).ai_exch = connection_info.key_exchange.to_u32().unwrap(); - (*sec_pkg_context_connection_info).dw_exch_strength = connection_info.exchange_strength; + // SAFETY: `sec_pgk_context_connection_info` was cast from `p_buffer` which is not null. + let sec_pkg_context_connection_info = unsafe { sec_pkg_context_connection_info.as_mut() }.expect("sec_pkg_context_connection_info should not be null"); + sec_pkg_context_connection_info.dw_protocol = connection_info.protocol.to_u32().unwrap(); + sec_pkg_context_connection_info.ai_cipher = connection_info.cipher.to_u32().unwrap(); + sec_pkg_context_connection_info.dw_cipher_strength = connection_info.cipher_strength; + sec_pkg_context_connection_info.ai_hash = connection_info.hash.to_u32().unwrap(); + sec_pkg_context_connection_info.dw_hash_strength = connection_info.hash_strength; + sec_pkg_context_connection_info.ai_exch = connection_info.key_exchange.to_u32().unwrap(); + sec_pkg_context_connection_info.dw_exch_strength = connection_info.exchange_strength; return 0; } @@ -807,8 +888,11 @@ unsafe fn query_context_attributes_common( let sspi_cert_trust_status = try_execute!(sspi_context.query_context_cert_trust_status()); let cert_trust_status = p_buffer.cast::(); - (*cert_trust_status).dw_error_status = sspi_cert_trust_status.error_status.bits(); - (*cert_trust_status).dw_info_status = sspi_cert_trust_status.info_status.bits(); + // SAFETY: `cert_trust_status` was cast from `p_buffer` which is not null. We've checked this above. + let cert_trust_status = unsafe { cert_trust_status.as_mut() }.expect("cert_trust_status pointer should not be null"); + + cert_trust_status.dw_error_status = sspi_cert_trust_status.error_status.bits(); + cert_trust_status.dw_info_status = sspi_cert_trust_status.info_status.bits(); return 0; } @@ -831,12 +915,14 @@ unsafe fn query_context_attributes_common( let nego_info = p_buffer.cast::<*mut SecPkgInfoW>(); let package_info: RawSecPkgInfoW = package_info.into(); - *nego_info = package_info.0; + // SAFETY: `nego_info` was cast from `p_buffer` which is not null. We've checked this above. + unsafe { *nego_info = package_info.0; } } else { let nego_info = p_buffer.cast::<*mut SecPkgInfoA>(); let package_info: RawSecPkgInfoA = package_info.into(); - *nego_info = package_info.0; + // SAFETY: `nego_info` was cast from `p_buffer` which is not null. We've checked this above. + unsafe { *nego_info = package_info.0; } } 0 @@ -851,7 +937,11 @@ pub unsafe extern "system" fn QueryContextAttributesA( ul_attribute: u32, p_buffer: *mut c_void, ) -> SecurityStatus { - query_context_attributes_common(ph_context, ul_attribute, p_buffer, false) + check_null!(p_buffer); + + // SAFETY: This function is safe to call because `p_buffer` is not null, we've checked this above + // and all other arguments are type checked. + unsafe { query_context_attributes_common(ph_context, ul_attribute, p_buffer, false) } } pub type QueryContextAttributesFnA = unsafe extern "system" fn(PCtxtHandle, u32, *mut c_void) -> SecurityStatus; @@ -864,7 +954,11 @@ pub unsafe extern "system" fn QueryContextAttributesW( ul_attribute: u32, p_buffer: *mut c_void, ) -> SecurityStatus { - query_context_attributes_common(ph_context, ul_attribute, p_buffer, true) + check_null!(p_buffer); + + // SAFETY: This function is safe to call because `p_buffer` is not null, we've checked this above + // and all other arguments are type checked. + unsafe { query_context_attributes_common(ph_context, ul_attribute, p_buffer, true) } } pub type QueryContextAttributesFnW = unsafe extern "system" fn(PCtxtHandle, u32, *mut c_void) -> SecurityStatus; @@ -993,22 +1087,32 @@ pub unsafe extern "system" fn SetCredentialsAttributesA( check_null!(ph_credential); check_null!(p_buffer); - let credentials_handle = ((*ph_credential).dw_lower as *mut CredentialsHandle).as_mut().unwrap(); + // SAFETY: `ph_credentials` is not null. We've checked this above. In order for the `as_mut` function + // to be safe to call, user must provide all guarantees regarding the correctness of the `dw_lower'. + let credentials_handle = if let Some(credentials_handle) = unsafe { ((*ph_credential).dw_lower as *mut CredentialsHandle).as_mut() } { + credentials_handle + } else { + return ErrorKind::InvalidParameter.to_u32().unwrap(); + }; if ul_attribute == SECPKG_CRED_ATTR_NAMES { let workstation = - try_execute!(CStr::from_ptr(p_buffer as *const _).to_str(), ErrorKind::InvalidParameter).to_owned(); + // SAFETY: `p_buffer` is not null. We've checked this above. + try_execute!(unsafe { CStr::from_ptr(p_buffer as *const _) }.to_str(), ErrorKind::InvalidParameter).to_owned(); credentials_handle.attributes.workstation = Some(workstation); 0 } else if ul_attribute == SECPKG_CRED_ATTR_KDC_PROXY_SETTINGS { - credentials_handle.attributes.kdc_proxy_settings = Some(extract_kdc_proxy_settings(p_buffer)); + credentials_handle.attributes.kdc_proxy_settings = + // SAFETY: This function is safe to call because `p_buffer` is not null. We've checked this above. + Some(try_execute!(unsafe { extract_kdc_proxy_settings(NonNull::new(p_buffer).expect("p_buffer should not be null")) })); 0 } else if ul_attribute == SECPKG_CRED_ATTR_KDC_URL { let cred_attr = p_buffer.cast::(); - let kdc_url = try_execute!(CStr::from_ptr((*cred_attr).kdc_url).to_str(), ErrorKind::InvalidParameter); + // SAFETY: `cred_attr` was cast from `p_buffer` which is not null. We've checked this above. + let kdc_url = try_execute!(unsafe { CStr::from_ptr((*cred_attr).kdc_url) }.to_str(), ErrorKind::InvalidParameter); credentials_handle.attributes.kdc_url = Some(kdc_url.to_string()); 0 } else { @@ -1032,21 +1136,31 @@ pub unsafe extern "system" fn SetCredentialsAttributesW( check_null!(ph_credential); check_null!(p_buffer); - let credentials_handle = ((*ph_credential).dw_lower as *mut CredentialsHandle).as_mut().unwrap(); + // SAFETY: `ph_credentials` is not null. We've checked this above. In order for the `as_mut` function + // to be safe to call, user must provide all guarantees regarding the correctness of the `dw_lower'. + let credentials_handle = if let Some(credentials_handle) = unsafe { ((*ph_credential).dw_lower as *mut CredentialsHandle).as_mut() } { + credentials_handle + } else { + return ErrorKind::InvalidParameter.to_u32().unwrap(); + }; if ul_attribute == SECPKG_CRED_ATTR_NAMES { - let workstation = c_w_str_to_string(p_buffer as *const _); + // SAFETY: `p_buffer` is not null. We've checked this above. + let workstation = unsafe { c_w_str_to_string(p_buffer as *const _) }; credentials_handle.attributes.workstation = Some(workstation); 0 } else if ul_attribute == SECPKG_CRED_ATTR_KDC_PROXY_SETTINGS { - credentials_handle.attributes.kdc_proxy_settings = Some(extract_kdc_proxy_settings(p_buffer)); + credentials_handle.attributes.kdc_proxy_settings = + // SAFETY: This function is safe to call because `p_buffer` is not null. We've checked this above. + Some(try_execute!(unsafe { extract_kdc_proxy_settings(NonNull::new(p_buffer).expect("p_buffer should not be null")) })); 0 } else if ul_attribute == SECPKG_CRED_ATTR_KDC_URL { let cred_attr = p_buffer.cast::(); - let kdc_url = c_w_str_to_string((*cred_attr).kdc_url as *const u16); + // SAFETY: `cred_attr` was cast from `p_buffer` which is not null. We've checked this above. + let kdc_url = unsafe { c_w_str_to_string((*cred_attr).kdc_url as *const u16) }; credentials_handle.attributes.kdc_url = Some(kdc_url); 0 @@ -1079,15 +1193,39 @@ pub unsafe extern "system" fn ChangeAccountPasswordA( check_null!(psz_new_password); check_null!(p_output); - let security_package_name = try_execute!(CStr::from_ptr(psz_package_name).to_str(), ErrorKind::InvalidParameter); - - let domain = try_execute!(CStr::from_ptr(psz_domain_name).to_str(), ErrorKind::InvalidParameter); - let username = try_execute!(CStr::from_ptr(psz_account_name).to_str(), ErrorKind::InvalidParameter); - let password = try_execute!(CStr::from_ptr(psz_old_password).to_str(), ErrorKind::InvalidParameter); - let new_password = try_execute!(CStr::from_ptr(psz_new_password).to_str(), ErrorKind::InvalidParameter); - - let len = (*p_output).c_buffers as usize; - let mut output_tokens = p_sec_buffers_to_security_buffers(from_raw_parts((*p_output).p_buffers, len)); + let security_package_name = + // SAFETY: `psz_package_name` is not null. We've checked this above. + // User must provide all other guarantees that the C string is valid. + try_execute!(unsafe { CStr::from_ptr(psz_package_name) }.to_str(), ErrorKind::InvalidParameter); + + let domain = + // SAFETY: `psz_domain_name` is not null. We've checked this above. + // User must provide all other guarantees that the C string is valid. + try_execute!(unsafe { CStr::from_ptr(psz_domain_name) }.to_str(), ErrorKind::InvalidParameter); + let username = + // SAFETY: `psz_account_name` is not null. We've checked this above. + // User must provide all other guarantees that the C string is valid. + try_execute!(unsafe { CStr::from_ptr(psz_account_name) }.to_str(), ErrorKind::InvalidParameter); + let password = + // SAFETY: `psz_old_password` is not null. We've checked this above. + // User must provide all other guarantees that the C string is valid. + try_execute!(unsafe { CStr::from_ptr(psz_old_password) }.to_str(), ErrorKind::InvalidParameter); + let new_password = + // SAFETY: `psz_new_password` is not null. We've checked this above. + // User must provide all other guarantees that the C string is valid. + try_execute!(unsafe { CStr::from_ptr(psz_new_password) }.to_str(), ErrorKind::InvalidParameter); + + // SAFETY: `p_output` is not null. We've checked this above. + let p_output = unsafe { p_output.as_mut() }.expect("p_output pointer should not be null"); + let len = try_execute!(usize::try_from(p_output.c_buffers), ErrorKind::InvalidParameter); + + check_null!(p_output.p_buffers); + + let mut output_tokens = + // SAFETY: `p_output` and `p_buffers` are not null. We've checked this above. + // The `p_sec_buffers_to_security_buffers` function is safe to call if user + // provides guarantees about the buffers. + unsafe { p_sec_buffers_to_security_buffers(from_raw_parts(p_output.p_buffers, len)) }; output_tokens.iter_mut().for_each(|s| s.buffer.clear()); let change_password = ChangePasswordBuilder::new() @@ -1106,7 +1244,7 @@ pub unsafe extern "system" fn ChangeAccountPasswordA( package_list: None, client_computer_name: try_execute!(hostname()), }; - SspiContext::Negotiate(try_execute!(Negotiate::new(negotiate_config))) + SspiContext::Negotiate(try_execute!(Negotiate::new_client(negotiate_config))) }, kerberos::PKG_NAME => { let krb_config = KerberosConfig{ @@ -1125,7 +1263,9 @@ pub unsafe extern "system" fn ChangeAccountPasswordA( let result_status = try_execute!(sspi_context.change_password(change_password)).resolve_with_default_network_client(); - copy_to_c_sec_buffer((*p_output).p_buffers, &output_tokens, false); + // SAFETY: This function is safe to call because `p_output` and `p_buffers` are not null. + // We've checked this above. And other arguments are type checked. + try_execute!(unsafe { copy_to_c_sec_buffer(p_output.p_buffers, &output_tokens, false) }); try_execute!(result_status); @@ -1165,23 +1305,39 @@ pub unsafe extern "system" fn ChangeAccountPasswordW( check_null!(psz_new_password); check_null!(p_output); - let mut security_package_name = c_w_str_to_string(psz_package_name); - - let mut domain = c_w_str_to_string(psz_domain_name); - let mut username = c_w_str_to_string(psz_account_name); - let mut password = Secret::new(c_w_str_to_string(psz_old_password)); - let mut new_password = Secret::new(c_w_str_to_string(psz_new_password)); - - ChangeAccountPasswordA( - security_package_name.as_mut_ptr() as *mut _, - domain.as_mut_ptr() as *mut _, - username.as_mut_ptr() as *mut _, - password.as_mut().as_mut_ptr() as *mut _, - new_password.as_mut().as_mut_ptr() as *mut _, - b_impersonating, - dw_reserved, - p_output, - ) + // SAFETY: This function is safe to call because `psw_pacakge_name` is not null. We've checked this above. + let mut security_package_name = unsafe { c_w_str_to_string(psz_package_name) }; + + // SAFETY: This function is safe to call because `psw_domain_name` is not null. We've checked this above. + let mut domain = unsafe { c_w_str_to_string(psz_domain_name) }; + // SAFETY: This function is safe to call because `psz_account_name` is not null. We've checked this above. + let mut username = unsafe { c_w_str_to_string(psz_account_name) }; + // SAFETY: This function is safe to call because `psz_old_password` is not null. We've checked this above. + let mut password = Secret::new(unsafe { c_w_str_to_string(psz_old_password) }); + // SAFETY: This function is safe to call because `psz_new_password` is not null. We've checked this above. + let mut new_password = Secret::new(unsafe { c_w_str_to_string(psz_new_password) }); + + // SAFETY: All arguments are type checked and/or validated: + // * `security_package_name': it's a String. So it's valid string. + // * `domain': it's a String. So it's valid string. + // * `username': it's a String. So it's valid string. + // * `password': it's a String. So it's valid string. + // * `new_password': it's a String. So it's valid string. + // * `b_impersonating`: it's a valid bool. + // * `dw_reserved': it's a valid u32. + // * `p_output': it's not null pointer. + unsafe { + ChangeAccountPasswordA( + security_package_name.as_mut_ptr() as *mut _, + domain.as_mut_ptr() as *mut _, + username.as_mut_ptr() as *mut _, + password.as_mut().as_mut_ptr() as *mut _, + new_password.as_mut().as_mut_ptr() as *mut _, + b_impersonating, + dw_reserved, + p_output, + ) + } } } diff --git a/ffi/src/sspi/sec_pkg_info.rs b/ffi/src/sspi/sec_pkg_info.rs index b068c21e..c051ce3c 100644 --- a/ffi/src/sspi/sec_pkg_info.rs +++ b/ffi/src/sspi/sec_pkg_info.rs @@ -38,8 +38,17 @@ impl From for RawSecPkgInfoW { let raw_pkg_info; let pkg_info_w; + // SAFETY: Memory allocation is safe. unsafe { raw_pkg_info = libc::malloc(size); + } + // SAFETY: + // FIXME(safety): it is illegal to construct a reference to uninitialized data + // Useful references: + // - https://doc.rust-lang.org/nomicon/unchecked-uninit.html + // - https://doc.rust-lang.org/core/mem/union.MaybeUninit.html#initializing-a-struct-field-by-field + // NOTE: this is not the only place that needs to be fixed. An audit is required. + unsafe { pkg_info_w = (raw_pkg_info as *mut SecPkgInfoW).as_mut().unwrap(); } @@ -49,15 +58,27 @@ impl From for RawSecPkgInfoW { pkg_info_w.cb_max_token = pkg_info.max_token_len.try_into().unwrap(); let name_ptr; + // SAFETY: Our allocated buffer is big enough to contain package name and comment. unsafe { name_ptr = raw_pkg_info.add(pkg_info_w_size); + } + // SAFETY: + // * pkg_name ptr is valid for read because it is Rust-allocated vector. + // * name_ptr is valid for write because we took into account its length during memory allocation. + unsafe { copy_nonoverlapping(pkg_name.as_ptr() as *const _, name_ptr, name_bytes_len); } pkg_info_w.name = name_ptr as *mut _; let comment_ptr; + // SAFETY: Our allocated buffer is big enough to contain package name and comment. unsafe { comment_ptr = name_ptr.add(name_bytes_len); + } + // SAFETY: + // * pkg_comment ptr is valid for read because it is Rust-allocated vector. + // * pkg_comment is valid for write because we took into account its length during memory allocation. + unsafe { copy_nonoverlapping(pkg_comment.as_ptr() as *const _, comment_ptr, comment_bytes_len); } pkg_info_w.comment = comment_ptr as *mut _; @@ -101,14 +122,17 @@ impl From for RawSecPkgInfoA { let raw_pkg_info; let pkg_info_a; + // SAFETY: Memory allocation is safe. unsafe { raw_pkg_info = libc::malloc(size); - - // FIXME(safety): it is illegal to construct a reference to uninitialized data - // Useful references: - // - https://doc.rust-lang.org/nomicon/unchecked-uninit.html - // - https://doc.rust-lang.org/core/mem/union.MaybeUninit.html#initializing-a-struct-field-by-field - // NOTE: this is not the only place that needs to be fixed. An audit is required. + } + // SAFETY: + // FIXME(safety): it is illegal to construct a reference to uninitialized data + // Useful references: + // - https://doc.rust-lang.org/nomicon/unchecked-uninit.html + // - https://doc.rust-lang.org/core/mem/union.MaybeUninit.html#initializing-a-struct-field-by-field + // NOTE: this is not the only place that needs to be fixed. An audit is required. + unsafe { pkg_info_a = (raw_pkg_info as *mut SecPkgInfoA).as_mut().unwrap(); } @@ -118,15 +142,27 @@ impl From for RawSecPkgInfoA { pkg_info_a.cb_max_token = pkg_info.max_token_len; let name_ptr; + // SAFETY: Our allocated buffer is big enough to contain package name and comment. unsafe { name_ptr = raw_pkg_info.add(pkg_info_a_size); + } + // SAFETY: + // * pkg_name ptr is valid for read because it is Rust-allocated vector. + // * name_ptr is valid for write because we took into account its length during memory allocation. + unsafe { copy_nonoverlapping(pkg_name.as_ptr() as *const _, name_ptr, name_bytes_len); } pkg_info_a.name = name_ptr as *mut _; let comment_ptr; + // SAFETY: Our allocated buffer is big enough to contain package name and comment. unsafe { comment_ptr = name_ptr.add(name_bytes_len); + } + // SAFETY: + // * pkg_comment ptr is valid for read because it is Rust-allocated vector. + // * pkg_comment is valid for write because we took into account its length during memory allocation. + unsafe { copy_nonoverlapping(pkg_comment.as_ptr() as *const _, comment_ptr, comment_bytes_len); } pkg_info_a.comment = comment_ptr as *mut _; @@ -162,7 +198,8 @@ pub unsafe extern "system" fn EnumerateSecurityPackagesA( let packages = try_execute!(enumerate_security_packages()); - *pc_packages = packages.len() as u32; + // SAFETY: `pc_packages` is not null. We've checked this above. + unsafe { *pc_packages = packages.len() as u32; } let mut size = size_of::() * packages.len(); @@ -170,12 +207,25 @@ pub unsafe extern "system" fn EnumerateSecurityPackagesA( size += package.name.as_ref().len() + 1 /* null byte */ + package.comment.len() + 1 /* null byte */; } - let raw_packages = libc::malloc(size); + // SAFETY: Memory allocation is safe. + let raw_packages = unsafe { libc::malloc(size) }; + + if raw_packages.is_null() { + return ErrorKind::InsufficientMemory.to_u32().unwrap(); + } let mut package_ptr = raw_packages as *mut SecPkgInfoA; - let mut data_ptr = raw_packages.add(size_of::() * packages.len()) as *mut SecChar; + + // SAFETY: It is safe to cast a pointer because we allocated enough memory to place package name and comment alongside SecPkgInfoA. + let mut data_ptr = unsafe { raw_packages.add(size_of::() * packages.len()) as *mut SecChar }; for pkg_info in packages { - let pkg_info_a = package_ptr.as_mut().unwrap(); + // FIXME(safety): it is illegal to construct a reference to uninitialized data + // Useful references: + // - https://doc.rust-lang.org/nomicon/unchecked-uninit.html + // - https://doc.rust-lang.org/core/mem/union.MaybeUninit.html#initializing-a-struct-field-by-field + // NOTE: this is not the only place that needs to be fixed. An audit is required. + // SAFETY: `package_ptr` is a local pointer and we've checked that it is not null above. + let pkg_info_a = unsafe { package_ptr.as_mut().unwrap() }; pkg_info_a.f_capabilities = pkg_info.capabilities.bits(); pkg_info_a.w_version = KERBEROS_VERSION as u16; @@ -185,21 +235,35 @@ pub unsafe extern "system" fn EnumerateSecurityPackagesA( let mut name = pkg_info.name.as_ref().as_bytes().to_vec(); // We need to add the null-terminator during the conversion from Rust to C string. name.push(0); - copy_nonoverlapping(name.as_ptr(), data_ptr as *mut _, name.len()); + // SAFETY: This function is safe to call because `name` is valid C string and + // `data_ptr` is a local pointer to allocated memory. + // We precalculated and allocated enough memory to accommodate all security packages + their names and comments. + unsafe { copy_nonoverlapping(name.as_ptr(), data_ptr as *mut _, name.len()); } pkg_info_a.name = data_ptr as *mut _; - data_ptr = data_ptr.add(name.len()); + // SAFETY: Our allocated buffer is big enough to contain package name and comment. + // We precalculated and allocated enough memory to accommodate all security packages + their names and comments. + data_ptr = unsafe { data_ptr.add(name.len()) }; let mut comment = pkg_info.comment.as_bytes().to_vec(); // We need to add the null-terminator during the conversion from Rust to C string. comment.push(0); - copy_nonoverlapping(comment.as_ptr(), data_ptr as *mut _, comment.len()); + + // SAFETY: This function is safe to call because `name` is valid C string and + // `data_ptr` is a local pointer to allocated memory. + // We precalculated and allocated enough memory to accommodate all security packages + their names and comments. + unsafe { copy_nonoverlapping(comment.as_ptr(), data_ptr as *mut _, comment.len()); } pkg_info_a.comment = data_ptr as *mut _; - data_ptr = data_ptr.add(comment.len()); + // SAFETY: Our allocated buffer is big enough to contain package name and comment. + // We precalculated and allocated enough memory to accommodate all security packages + their names and comments. + data_ptr = unsafe { data_ptr.add(comment.len()) }; - package_ptr = package_ptr.add(1); + // SAFETY: Next structure (if any) is placed right after this structure. + // We precalculated and allocated enough memory to accommodate all security packages + their names and comments. + package_ptr = unsafe { package_ptr.add(1) }; } - *pp_package_info = raw_packages as *mut _; + // SAFETY: `pp_package_into` is not null. We've checked this above. + unsafe { *pp_package_info = raw_packages as *mut _; } 0 } @@ -220,7 +284,8 @@ pub unsafe extern "system" fn EnumerateSecurityPackagesW( let packages = try_execute!(enumerate_security_packages()); - *pc_packages = packages.len() as u32; + // SAFETY: `pc_packages` is not null. We've checked this above. + unsafe { *pc_packages = packages.len() as u32; } let mut size = size_of::() * packages.len(); let mut names = Vec::with_capacity(packages.len()); @@ -236,30 +301,55 @@ pub unsafe extern "system" fn EnumerateSecurityPackagesW( comments.push(comment); } - let raw_packages = libc::malloc(size); + // SAFETY: Memory allocation is safe. + let raw_packages = unsafe { libc::malloc(size) }; + + if raw_packages.is_null() { + return ErrorKind::InsufficientMemory.to_u32().unwrap(); + } let mut package_ptr = raw_packages as *mut SecPkgInfoW; - let mut data_ptr = raw_packages.add(size_of::() * packages.len()) as *mut SecWChar; + // SAFETY: It is safe to cast a pointer because we allocated enough memory to place package name and comment alongside SecPkgInfoA. + let mut data_ptr = unsafe { raw_packages.add(size_of::() * packages.len()) as *mut SecWChar }; for (i, pkg_info) in packages.iter().enumerate() { - let pkg_info_w = package_ptr.as_mut().unwrap(); + // FIXME(safety): it is illegal to construct a reference to uninitialized data + // Useful references: + // - https://doc.rust-lang.org/nomicon/unchecked-uninit.html + // - https://doc.rust-lang.org/core/mem/union.MaybeUninit.html#initializing-a-struct-field-by-field + // NOTE: this is not the only place that needs to be fixed. An audit is required. + // SAFETY: `package_ptr` is a local pointer and we've checked that it is not null above. + let pkg_info_w = unsafe { package_ptr.as_mut().unwrap() }; pkg_info_w.f_capabilities = pkg_info.capabilities.bits(); pkg_info_w.w_version = KERBEROS_VERSION as u16; pkg_info_w.w_rpc_id = pkg_info.rpc_id; pkg_info_w.cb_max_token = pkg_info.max_token_len; - copy_nonoverlapping(names[i].as_ptr(), data_ptr, names[i].len()); + // SAFETY: This function is safe to call because `names[i]` is valid C string and + // `data_ptr` is a local pointer to allocated memory. + // We precalculated and allocated enough memory to accommodate all security packages + their names and comments. + unsafe { copy_nonoverlapping(names[i].as_ptr(), data_ptr, names[i].len()); } pkg_info_w.name = data_ptr as *mut _; - data_ptr = data_ptr.add(names[i].len()); - - copy_nonoverlapping(comments[i].as_ptr(), data_ptr, comments[i].len()); + // SAFETY: Our allocated buffer is big enough to contain package name and comment. + // We precalculated and allocated enough memory to accommodate all security packages + their names and comments. + data_ptr = unsafe { data_ptr.add(names[i].len()) }; + + // SAFETY: This function is safe to call because `name` is valid C string and + // `data_ptr` is a local pointer to allocated memory. + // We precalculated and allocated enough memory to accommodate all security packages + their names and comments. + unsafe { copy_nonoverlapping(comments[i].as_ptr(), data_ptr, comments[i].len()); } pkg_info_w.comment = data_ptr as *mut _; - data_ptr = data_ptr.add(comments[i].len()); + // SAFETY: Our allocated buffer is big enough to contain package name and comment. + // We precalculated and allocated enough memory to accommodate all security packages + their names and comments. + data_ptr = unsafe { data_ptr.add(comments[i].len()) }; - package_ptr = package_ptr.add(1); + // SAFETY: Next structure (if any) is placed right after this structure. + // We precalculated and allocated enough memory to accommodate all security packages + their names and comments. + package_ptr = unsafe { package_ptr.add(1) }; } - *pp_package_info = raw_packages as *mut _; + // SAFETY: `pp_package_into` is not null. We've checked this above. + unsafe { *pp_package_info = raw_packages as *mut _; } 0 } @@ -278,14 +368,17 @@ pub unsafe extern "system" fn QuerySecurityPackageInfoA( check_null!(p_package_name); check_null!(pp_package_info); - let pkg_name = try_execute!(CStr::from_ptr(p_package_name).to_str(), ErrorKind::InvalidParameter); + // SAFETY: This function is safe to call because `p_package_name` is not null, we've checked this above. + // All other guarantees about validity of C string must be provided by user. + let pkg_name = try_execute!(unsafe { CStr::from_ptr(p_package_name) }.to_str(), ErrorKind::InvalidParameter); let pkg_info: RawSecPkgInfoA = try_execute!(enumerate_security_packages()) .into_iter() .find(|pkg| pkg.name.as_ref() == pkg_name) .unwrap() .into(); - *pp_package_info = pkg_info.0; + // SAFETY: `pp_package_info` is not null. We've checked this above. + unsafe { *pp_package_info = pkg_info.0; } 0 } @@ -304,14 +397,17 @@ pub unsafe extern "system" fn QuerySecurityPackageInfoW( check_null!(p_package_name); check_null!(pp_package_info); - let pkg_name = c_w_str_to_string(p_package_name); + // SAFETY: This function is safe to call because `p_package_name` is not null, we've checked this above. + // All other guarantees about validity of C string must be provided by user. + let pkg_name = unsafe { c_w_str_to_string(p_package_name) }; let pkg_info: RawSecPkgInfoW = try_execute!(enumerate_security_packages()) .into_iter() .find(|pkg| pkg.name.to_string() == pkg_name) .unwrap() .into(); - *pp_package_info = pkg_info.0; + // SAFETY: `pp_package_info` is not null. We've checked this above. + unsafe { *pp_package_info = pkg_info.0; } 0 } diff --git a/ffi/src/sspi/sec_winnt_auth_identity.rs b/ffi/src/sspi/sec_winnt_auth_identity.rs index d9b2e03d..d6a70c12 100644 --- a/ffi/src/sspi/sec_winnt_auth_identity.rs +++ b/ffi/src/sspi/sec_winnt_auth_identity.rs @@ -154,28 +154,42 @@ pub struct CredSspCred { pub p_spnego_cred: *const c_void, } +/// Returns auth identity version and flags. +/// +/// # Safety: +/// +/// * The auth identity pointer must not be null. pub unsafe fn get_auth_data_identity_version_and_flags(p_auth_data: *const c_void) -> (u32, u32) { - let auth_version = *p_auth_data.cast::(); + // SAFETY: the safety contract [p_auth_data] must be upheld by the caller. + let auth_version = unsafe { *p_auth_data.cast::() }; if auth_version == SEC_WINNT_AUTH_IDENTITY_VERSION { let auth_data = p_auth_data.cast::(); - (auth_version, (*auth_data).flags) + // SAFETY: `auth_data` was cast from `p_auth_data`, so it's not null either. + (auth_version, unsafe { (*auth_data).flags }) } else if auth_version == SEC_WINNT_AUTH_IDENTITY_VERSION_2 { let auth_data = p_auth_data.cast::(); - (auth_version, (*auth_data).flags) + // SAFETY: `auth_data` was cast from `p_auth_data`, so it's not null either. + (auth_version, unsafe { (*auth_data).flags }) } else { // SEC_WINNT_AUTH_IDENTITY let auth_data = p_auth_data.cast::(); - (auth_version, (*auth_data).flags) + // SAFETY: `auth_data` was cast from `p_auth_data`, so it's not null either. + (auth_version, unsafe { (*auth_data).flags }) } } -// The only one purpose of this function is to handle CredSSP credentials passed into the AcquireCredentialsHandle function +/// The only one purpose of this function is to handle CredSSP credentials passed into the AcquireCredentialsHandle function. +/// +/// # Safety: +/// +/// * The user must ensure that `p_auth_data` must be not null and point to the valid [CredSspCred] structure. #[cfg(feature = "tsssp")] unsafe fn credssp_auth_data_to_identity_buffers(p_auth_data: *const c_void) -> Result { use sspi::string_to_utf16; use windows_sys::Win32::Foundation::ERROR_SUCCESS; - let credssp_cred = p_auth_data.cast::().as_ref().unwrap(); + // SAFETY: The `p_auth_data` pointer guarantees must be upheld by the user. + let credssp_cred = unsafe { p_auth_data.cast::().as_ref() }.unwrap(); if credssp_cred.submit_type == CredSspSubmitType::CredsspSubmitBufferBothOld { if credssp_cred.p_spnego_cred.is_null() { @@ -201,17 +215,22 @@ unsafe fn credssp_auth_data_to_identity_buffers(p_auth_data: *const c_void) -> R let mut out_buffer_size = 1024; let mut out_buffer = null_mut(); - let result = CredUIPromptForWindowsCredentialsW( - &cred_ui_info, - 0, - &mut auth_package_count, - null_mut(), - 0, - &mut out_buffer, - &mut out_buffer_size, - null_mut(), - 0, - ); + // SAFETY: + // * all non-null values are allocated by Rust inside the current function. Thus, they are valid. + // * all other (null and zero) values are allowed according to the function documentation. + let result = unsafe { + CredUIPromptForWindowsCredentialsW( + &cred_ui_info, + 0, + &mut auth_package_count, + null_mut(), + 0, + &mut out_buffer, + &mut out_buffer_size, + null_mut(), + 0, + ) + }; if result != ERROR_SUCCESS { return Err(Error::new( @@ -220,7 +239,10 @@ unsafe fn credssp_auth_data_to_identity_buffers(p_auth_data: *const c_void) -> R )); } - unpack_sec_winnt_auth_identity_ex2_w_sized(out_buffer, out_buffer_size) + // SAFETY: `out_buffer` and `out_buffer_size` are initialized and valid because + // the `CredUIPromptForWindowsCredentialsW` function returned successful status code and + // we've checked for errors above. + unsafe { unpack_sec_winnt_auth_identity_ex2_w_sized(out_buffer, out_buffer_size) } } else { // When we try to pass the plain password in the `ClearTextPassword` .rdp file property, // the CredSSP credentials will have the type `CredsspSubmitBufferBothOld` and @@ -228,34 +250,52 @@ unsafe fn credssp_auth_data_to_identity_buffers(p_auth_data: *const c_void) -> R // // Additional info: // * [ClearTextPassword](https://github.com/Devolutions/MsRdpEx/blob/a7978812cb31e363f4b536316bd59e1573e69384/README.md#extended-rdp-file-options) - auth_data_to_identity_buffers_w(credssp_cred.p_spnego_cred, &mut None) + // SAFETY: we've checked above that the `credssp_cred.p_spnego_cred` is not null. + // The data correctness behind `credssp_cred.p_spnego_cred` pointer must be guaranteed by the user. + unsafe { auth_data_to_identity_buffers_w(credssp_cred.p_spnego_cred, &mut None) } } } else { - unpack_sec_winnt_auth_identity_ex2_w(credssp_cred.p_spnego_cred) + // SAFETY: The data correctness behind `credssp_cred.p_spnego_cred` pointer must be guaranteed by the user. + unsafe { unpack_sec_winnt_auth_identity_ex2_w(credssp_cred.p_spnego_cred) } } } -// 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. +/// 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. +/// +/// # Safety: +/// +/// * The user must ensure that `p_auth_data` must be not null and point to the valid credentials structure +/// corresponding to the security package in use. pub unsafe fn auth_data_to_identity_buffers( _security_package_name: &str, p_auth_data: *const c_void, package_list: &mut Option, ) -> Result { + if p_auth_data.is_null() { + return Err(Error::new(ErrorKind::InvalidParameter, "p_auth_data cannot be null")); + } + #[cfg(feature = "tsssp")] if _security_package_name == sspi::credssp::sspi_cred_ssp::PKG_NAME { - return credssp_auth_data_to_identity_buffers(p_auth_data); + // SAFETY: The data correctness behind `p_auth_data` pointer must be guaranteed by the user. + return unsafe { credssp_auth_data_to_identity_buffers(p_auth_data) }; } - let (_, auth_flags) = get_auth_data_identity_version_and_flags(p_auth_data); + // SAFETY: This function is safe to call because `p_auth_data` is not null. We've checked this above. + let (_, auth_flags) = unsafe { get_auth_data_identity_version_and_flags(p_auth_data) }; if (auth_flags & SEC_WINNT_AUTH_IDENTITY_ANSI) != 0 { - auth_data_to_identity_buffers_a(p_auth_data, package_list) + // SAFETY: This function is safe to call because `p_auth_data` is not null, we've checked this above, + // and `package_list` is type checked. + unsafe { auth_data_to_identity_buffers_a(p_auth_data, package_list) } } else { - auth_data_to_identity_buffers_w(p_auth_data, package_list) + // SAFETY: This function is safe to call because `p_auth_data` is not null, we've checked this above, + // and `package_list` is type checked. + unsafe { auth_data_to_identity_buffers_w(p_auth_data, package_list) } } } @@ -263,31 +303,53 @@ pub unsafe fn auth_data_to_identity_buffers_a( p_auth_data: *const c_void, package_list: &mut Option, ) -> Result { - let (auth_version, _) = get_auth_data_identity_version_and_flags(p_auth_data); + if p_auth_data.is_null() { + return Err(Error::new(ErrorKind::InvalidParameter, "p_auth_data cannot be null")); + } + + // SAFETY: This function is safe to call because `p_auth_data` is not null. We've checked this above. + let (auth_version, _) = unsafe { get_auth_data_identity_version_and_flags(p_auth_data) }; if auth_version == SEC_WINNT_AUTH_IDENTITY_VERSION { let auth_data = p_auth_data.cast::(); - if !(*auth_data).package_list.is_null() && (*auth_data).package_list_length > 0 { + // SAFETY: `auth_data` is not null. We've checked this above. + let auth_data = unsafe { auth_data.as_ref() }.expect("auth_data pointer should not be null"); + + if !auth_data.package_list.is_null() && auth_data.package_list_length > 0 { *package_list = Some( - String::from_utf8_lossy(from_raw_parts( - (*auth_data).package_list as *const _, - (*auth_data).package_list_length as usize, - )) + // SAFETY: This function is safe to call because `package_list` is not null. We've checked this above. + String::from_utf8_lossy(unsafe { + from_raw_parts( + auth_data.package_list as *const _, + auth_data.package_list_length as usize, + ) + }) .to_string(), ); } - Ok(CredentialsBuffers::AuthIdentity(AuthIdentityBuffers { - user: credentials_str_into_bytes((*auth_data).user, (*auth_data).user_length as usize), - domain: credentials_str_into_bytes((*auth_data).domain, (*auth_data).domain_length as usize), - password: credentials_str_into_bytes((*auth_data).password, (*auth_data).password_length as usize).into(), - })) + + // SAFETY: This function is safe to call because credentials pointers can be null and the caller is responsible for the data validity. + unsafe { + Ok(CredentialsBuffers::AuthIdentity(AuthIdentityBuffers { + user: credentials_str_into_bytes(auth_data.user, auth_data.user_length as usize), + domain: credentials_str_into_bytes(auth_data.domain, auth_data.domain_length as usize), + password: credentials_str_into_bytes(auth_data.password, auth_data.password_length as usize).into(), + })) + } } else { let auth_data = p_auth_data.cast::(); - Ok(CredentialsBuffers::AuthIdentity(AuthIdentityBuffers { - user: credentials_str_into_bytes((*auth_data).user, (*auth_data).user_length as usize), - domain: credentials_str_into_bytes((*auth_data).domain, (*auth_data).domain_length as usize), - password: credentials_str_into_bytes((*auth_data).password, (*auth_data).password_length as usize).into(), - })) + + // SAFETY: `auth_data` is not null. We've checked this above. + let auth_data = unsafe { auth_data.as_ref() }.expect("auth_data pointer should not be null"); + + // SAFETY: This function is safe to call because credentials pointers can be null and the caller is responsible for the data validity. + unsafe { + Ok(CredentialsBuffers::AuthIdentity(AuthIdentityBuffers { + user: credentials_str_into_bytes(auth_data.user, auth_data.user_length as usize), + domain: credentials_str_into_bytes(auth_data.domain, auth_data.domain_length as usize), + password: credentials_str_into_bytes(auth_data.password, auth_data.password_length as usize).into(), + })) + } } } @@ -295,49 +357,70 @@ pub unsafe fn auth_data_to_identity_buffers_w( p_auth_data: *const c_void, package_list: &mut Option, ) -> Result { - let (auth_version, _) = get_auth_data_identity_version_and_flags(p_auth_data); + if p_auth_data.is_null() { + return Err(Error::new(ErrorKind::InvalidParameter, "p_auth_data cannot be null")); + } + + // SAFETY: This function is safe to call because `p_auth_data` is not null. We've checked this above. + let (auth_version, _) = unsafe { get_auth_data_identity_version_and_flags(p_auth_data) }; if auth_version == SEC_WINNT_AUTH_IDENTITY_VERSION { let auth_data = p_auth_data.cast::(); - if !(*auth_data).package_list.is_null() && (*auth_data).package_list_length > 0 { - *package_list = Some(String::from_utf16_lossy(from_raw_parts( - (*auth_data).package_list, - usize::try_from((*auth_data).package_list_length).unwrap(), - ))); + // SAFETY: `auth_data` is not null. We've checked this above. + let auth_data = unsafe { auth_data.as_ref() }.expect("auth_data pointer should not be null"); + + if !auth_data.package_list.is_null() && auth_data.package_list_length > 0 { + // SAFETY: This function is safe to call because `package_list` is not null. We've checked this above. + *package_list = Some(String::from_utf16_lossy(unsafe { + from_raw_parts( + auth_data.package_list, + usize::try_from(auth_data.package_list_length).unwrap(), + ) + })); + } + + // SAFETY: This function is safe to call because `user` can be null and the caller is responsible for the data validity. + let user = + unsafe { credentials_str_into_bytes(auth_data.user as *const _, auth_data.user_length as usize * 2) }; + // SAFETY: This function is safe to call because `password` can be null and the caller is responsible for the data validity. + let password = unsafe { + credentials_str_into_bytes(auth_data.password as *const _, auth_data.password_length as usize * 2) } - let user = credentials_str_into_bytes((*auth_data).user as *const _, (*auth_data).user_length as usize * 2); - let password = credentials_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(all(feature = "scard", target_os = "windows"))] - if !user.is_empty() && CredIsMarshaledCredentialW(user.as_ptr() as *const _) != 0 { + // SAFETY: This function is safe to call because argument is validated. + if !user.is_empty() && unsafe { CredIsMarshaledCredentialW(user.as_ptr() as *const _) } != 0 { return handle_smart_card_creds(user, password); } Ok(CredentialsBuffers::AuthIdentity(AuthIdentityBuffers { user, - domain: credentials_str_into_bytes( - (*auth_data).domain as *const _, - (*auth_data).domain_length as usize * 2, - ), + // SAFETY: This function is safe to call because `domain` can be null and the caller is responsible for the data validity. + domain: unsafe { + credentials_str_into_bytes(auth_data.domain as *const _, auth_data.domain_length as usize * 2) + }, password, })) } else { let auth_data = p_auth_data.cast::(); - let user = credentials_str_into_bytes((*auth_data).user as *const _, (*auth_data).user_length as usize * 2); - let password = credentials_str_into_bytes( - (*auth_data).password as *const _, - (*auth_data).password_length as usize * 2, - ) + // SAFETY: `auth_data` is not null. We've checked this above. + let auth_data = unsafe { auth_data.as_ref() }.expect("auth_data pointer should not be null"); + + // SAFETY: This function is safe to call because `user` can be null and the caller is responsible for the data validity. + let user = + unsafe { credentials_str_into_bytes(auth_data.user as *const _, auth_data.user_length as usize * 2) }; + // SAFETY: This function is safe to call because `password` can be null and the caller is responsible for the data validity. + let password = unsafe { + credentials_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(all(feature = "scard", target_os = "windows"))] - if !user.is_empty() && CredIsMarshaledCredentialW(user.as_ptr() as *const _) != 0 { + // SAFETY: This function is safe to call because argument is validated. + if !user.is_empty() && unsafe { CredIsMarshaledCredentialW(user.as_ptr() as *const _) } != 0 { return handle_smart_card_creds(user, password); } @@ -349,10 +432,10 @@ pub unsafe fn auth_data_to_identity_buffers_w( Ok(CredentialsBuffers::AuthIdentity(AuthIdentityBuffers { user, - domain: credentials_str_into_bytes( - (*auth_data).domain as *const _, - (*auth_data).domain_length as usize * 2, - ), + // SAFETY: This function is safe to call because `domain` can be null and the caller is responsible for the data validity. + domain: unsafe { + credentials_str_into_bytes(auth_data.domain as *const _, auth_data.domain_length as usize * 2) + }, password, })) } @@ -409,25 +492,56 @@ pub fn unpack_sec_winnt_auth_identity_ex2_a(_p_auth_data: *const c_void) -> Resu )) } +/// This function calculated the size of the credentials represented by the `SEC_WINNT_AUTH_IDENTITY_EX2` +/// structure. +/// +/// # Safety: +/// +/// * The `p_auth_data` pointer must be not null and point to the valid credentials represented +/// by the `SEC_WINNT_AUTH_IDENTITY_EX2` structure. #[cfg(target_os = "windows")] -unsafe fn get_sec_winnt_auth_identity_ex2_size(p_auth_data: *const c_void) -> u32 { +unsafe fn get_sec_winnt_auth_identity_ex2_size(p_auth_data: *const c_void) -> Result { // https://learn.microsoft.com/en-us/windows/win32/api/sspi/ns-sspi-sec_winnt_auth_identity_ex2 // https://github.com/FreeRDP/FreeRDP/blob/master/winpr/libwinpr/sspi/sspi_winpr.c#L473 // Username length is placed after the first 8 bytes. - let user_len_ptr = (p_auth_data as *const u16).add(4); - let user_buffer_len = *user_len_ptr as u32; + // SAFETY: According to the documentation, username length is placed after the first 8 bytes. + let user_len_ptr = unsafe { (p_auth_data as *const u16).add(4) }; + if user_len_ptr.is_null() { + return Err(Error::new( + ErrorKind::InvalidParameter, + "invalid credentials: username length pointer is null", + )); + } + // SAFETY: `user_len_ptr` is not null: checked above. + let user_buffer_len = unsafe { *user_len_ptr as u32 }; // Domain length is placed after 16 bytes from the username length. - let domain_len_ptr = user_len_ptr.add(8); - let domain_buffer_len = *domain_len_ptr as u32; + // SAFETY: According to the documentation, domain length is placed after the first 8 bytes. + let domain_len_ptr = unsafe { user_len_ptr.add(8) }; + if domain_len_ptr.is_null() { + return Err(Error::new( + ErrorKind::InvalidParameter, + "invalid credentials: domain length pointer is null", + )); + } + // SAFETY: `domain_len_ptr` is not null: checked above. + let domain_buffer_len = unsafe { *domain_len_ptr as u32 }; // Packet credentials length is placed after 16 bytes from the domain length. - let creds_len_ptr = domain_len_ptr.add(8); - let creds_buffer_len = *creds_len_ptr as u32; + // SAFETY: According to the documentation, packet credentials length is placed after the first 8 bytes. + let creds_len_ptr = unsafe { domain_len_ptr.add(8) }; + if creds_len_ptr.is_null() { + return Err(Error::new( + ErrorKind::InvalidParameter, + "invalid credentials: creds length pointer is null", + )); + } + // SAFETY: `creds_len_ptr` is not null: checked above. + let creds_buffer_len = unsafe { *creds_len_ptr as u32 }; // The resulting size is queal to header size + buffers size. - 64 /* size of the SEC_WINNT_AUTH_IDENTITY_EX2 */ + user_buffer_len + domain_buffer_len + creds_buffer_len + Ok(64 /* size of the SEC_WINNT_AUTH_IDENTITY_EX2 */ + user_buffer_len + domain_buffer_len + creds_buffer_len) } #[cfg(target_os = "windows")] @@ -441,41 +555,48 @@ pub unsafe fn unpack_sec_winnt_auth_identity_ex2_a(p_auth_data: *const c_void) - )); } - let auth_data_len = get_sec_winnt_auth_identity_ex2_size(p_auth_data); + // SAFETY: `p_auth_data` is not null. We've checked this above. + let auth_data_len = unsafe { get_sec_winnt_auth_identity_ex2_size(p_auth_data) }?; let mut username_len = 0; let mut domain_len = 0; let mut password_len = 0; // The first call is just to query the username, domain, and password lengths. - CredUnPackAuthenticationBufferA( - CRED_PACK_PROTECTED_CREDENTIALS, - p_auth_data, - auth_data_len, - null_mut() as *mut _, - &mut username_len, - null_mut() as *mut _, - &mut domain_len, - null_mut() as *mut _, - &mut password_len, - ); + // SAFETY: This function is safe to call because all arguments are type checked. + unsafe { + CredUnPackAuthenticationBufferA( + CRED_PACK_PROTECTED_CREDENTIALS, + p_auth_data, + auth_data_len, + null_mut() as *mut _, + &mut username_len, + null_mut() as *mut _, + &mut domain_len, + null_mut() as *mut _, + &mut password_len, + ) + }; let mut username = vec![0_u8; username_len as usize]; let mut domain = vec![0_u8; domain_len as usize]; let mut password = Secret::new(vec![0_u8; password_len as usize]); // Knowing the actual sizes, we can unpack credentials into prepared buffers. - let result = CredUnPackAuthenticationBufferA( - CRED_PACK_PROTECTED_CREDENTIALS, - p_auth_data, - auth_data_len, - username.as_mut_ptr() as *mut _, - &mut username_len, - domain.as_mut_ptr() as *mut _, - &mut domain_len, - password.as_mut().as_mut_ptr() as *mut _, - &mut password_len, - ); + // SAFETY: This function is safe to call because all arguments are type checked. + let result = unsafe { + CredUnPackAuthenticationBufferA( + CRED_PACK_PROTECTED_CREDENTIALS, + p_auth_data, + auth_data_len, + username.as_mut_ptr() as *mut _, + &mut username_len, + domain.as_mut_ptr() as *mut _, + &mut domain_len, + password.as_mut().as_mut_ptr() as *mut _, + &mut password_len, + ) + }; if result != 1 { return Err(Error::new( @@ -525,7 +646,7 @@ pub fn unpack_sec_winnt_auth_identity_ex2_w(_p_auth_data: *const c_void) -> Resu #[cfg(all(feature = "scard", target_os = "windows"))] #[instrument(level = "trace", ret)] -unsafe fn handle_smart_card_creds(mut username: Vec, password: Secret>) -> Result { +fn handle_smart_card_creds(mut username: Vec, password: Secret>) -> Result { use std::ptr::null_mut; use sspi::cert_utils::{finalize_smart_card_info, SmartCardInfo}; @@ -539,7 +660,8 @@ unsafe fn handle_smart_card_creds(mut username: Vec, password: Secret, password: Secret(); - let (raw_certificate, certificate) = - sspi::cert_utils::extract_certificate_by_thumbprint(&(*cert_credential).rgbHashOfCert)?; + // SAFETY: This function is safe to call because `cert_credential` is validated. + let (raw_certificate, certificate) = sspi::cert_utils::extract_certificate_by_thumbprint( + // SAFETY: We've checked the returned status code from `CredUnmarshalCredentialW` function and credentials type above. + // The `cert_credential` is a valid pointer to the `CERT_CREDENTIAL_INFO` structure at this point. + unsafe { (*cert_credential).rgbHashOfCert }.as_ref(), + )?; let username = string_to_utf16(sspi::cert_utils::extract_user_name_from_certificate(&certificate)?); + // SAFETY: This function is safe to call because argument is type-checked. let SmartCardInfo { key_container_name, reader_name, @@ -582,6 +709,12 @@ unsafe fn handle_smart_card_creds(mut username: Vec, password: Secret Result { @@ -592,11 +725,19 @@ pub unsafe fn unpack_sec_winnt_auth_identity_ex2_w(p_auth_data: *const c_void) - )); } - let auth_data_len = get_sec_winnt_auth_identity_ex2_size(p_auth_data); + // SAFETY: The `p_auth_data` is not null (checked above). All other requirements mu be upheld by the user. + let auth_data_len = unsafe { get_sec_winnt_auth_identity_ex2_size(p_auth_data) }?; - unpack_sec_winnt_auth_identity_ex2_w_sized(p_auth_data, auth_data_len) + // SAFETY: The `p_auth_data` is not null (checked above). All other requirements mu be upheld by the user. + unsafe { unpack_sec_winnt_auth_identity_ex2_w_sized(p_auth_data, auth_data_len) } } +/// Unpacks raw credentials when the `auth_data` length is known. +/// +/// # Safety: +/// +/// * The `p_auth_data` must not be null and point to the valid packed credentials. For more details, +/// see the `pAuthBuffer` pointer requirements: [CredUnPackAuthenticationBufferW](https://learn.microsoft.com/en-us/windows/win32/api/wincred/nf-wincred-credunpackauthenticationbufferw). #[cfg(feature = "tsssp")] #[instrument(level = "trace", ret)] pub unsafe fn unpack_sec_winnt_auth_identity_ex2_w_sized( @@ -621,33 +762,45 @@ pub unsafe fn unpack_sec_winnt_auth_identity_ex2_w_sized( let mut password_len = 0; // The first call is just to query the username, domain, and password lengths. - CredUnPackAuthenticationBufferW( - CRED_PACK_PROTECTED_CREDENTIALS, - p_auth_data, - auth_data_len, - null_mut() as *mut _, - &mut username_len, - null_mut() as *mut _, - &mut domain_len, - null_mut() as *mut _, - &mut password_len, - ); + // SAFETY: + // * `p_auth_data` pointer is not null (checked above). All other requirements mu be upheld by the user. + // * all null values are allowed by the documentation. + // * `username/domain/password_len` are safe to use because they are local variables. + unsafe { + CredUnPackAuthenticationBufferW( + CRED_PACK_PROTECTED_CREDENTIALS, + p_auth_data, + auth_data_len, + null_mut() as *mut _, + &mut username_len, + null_mut() as *mut _, + &mut domain_len, + null_mut() as *mut _, + &mut password_len, + ) + }; let mut username = vec![0_u8; username_len as usize * 2]; let mut domain = vec![0_u8; domain_len as usize * 2]; let mut password = Secret::new(vec![0_u8; password_len as usize * 2]); - let result = CredUnPackAuthenticationBufferW( - CRED_PACK_PROTECTED_CREDENTIALS, - p_auth_data, - auth_data_len, - username.as_mut_ptr() as *mut _, - &mut username_len, - domain.as_mut_ptr() as *mut _, - &mut domain_len, - password.as_mut().as_mut_ptr() as *mut _, - &mut password_len, - ); + // SAFETY: + // * `p_auth_data` pointer is not null (checked above). All other requirements mu be upheld by the user. + // * `username/domain/password` buffers are safe to use because they are buffers allocated by Rust. + // * `username/domain/password_len` are safe to use because they are local variables. + let result = unsafe { + CredUnPackAuthenticationBufferW( + CRED_PACK_PROTECTED_CREDENTIALS, + p_auth_data, + auth_data_len, + username.as_mut_ptr() as *mut _, + &mut username_len, + domain.as_mut_ptr() as *mut _, + &mut domain_len, + password.as_mut().as_mut_ptr() as *mut _, + &mut password_len, + ) + }; if result != 1 { return Err(Error::new( @@ -664,7 +817,9 @@ pub unsafe fn unpack_sec_winnt_auth_identity_ex2_w_sized( // Only marshaled smart card creds starts with '@' char. #[cfg(feature = "scard")] - if !username.is_empty() && CredIsMarshaledCredentialW(username.as_ptr() as *const _) != 0 { + // SAFETY: `username` is a Rust-allocated buffer which data has been written by the `CredUnPackAuthenticationBufferW` function. + // Thus, it is safe to pass it into the `CredIsMarshaledCredentialW` function. + if !username.is_empty() && unsafe { CredIsMarshaledCredentialW(username.as_ptr() as *const _) } != 0 { // The `handle_smart_card_creds` function expects credentials in a form of raw wide strings without NULL-terminator bytes. // The `CredUnPackAuthenticationBufferW` function always returns credentials as strings. // So, password data is a wide C string and we need to delete the NULL terminator. @@ -722,31 +877,41 @@ pub unsafe extern "system" fn SspiEncodeStringsAsAuthIdentity( check_null!(psz_domain_name); check_null!(psz_packed_credentials_string); - let user_length = w_str_len(psz_user_name); - let domain_length = w_str_len(psz_domain_name); - let password_length = w_str_len(psz_packed_credentials_string); + // SAFETY: This function is safe to call because `psz_user_name` is not null. We've checked this above. + let user_length = unsafe { w_str_len(psz_user_name) }; + // SAFETY: This function is safe to call because `psz_domain_name` is not null. We've checked this above. + let domain_length = unsafe { w_str_len(psz_domain_name) }; + // SAFETY: This function is safe to call because `psz_packed_credentials_string` is not null. We've checked this above. + let password_length = unsafe { w_str_len(psz_packed_credentials_string) }; if user_length == 0 || domain_length == 0 || password_length == 0 { return ErrorKind::InvalidParameter.to_u32().unwrap(); } + // SAFETY: Memory allocation is safe. let user = unsafe { libc::malloc(user_length * 2) as *mut SecWChar }; if user.is_null() { return ErrorKind::InternalError.to_u32().unwrap(); } - copy_nonoverlapping(psz_user_name, user, user_length); + // SAFETY: This function is safe to call because `psz_user_name` and `user` are not null. We've checked this above. + unsafe { copy_nonoverlapping(psz_user_name, user, user_length) }; + // SAFETY: Memory allocation is safe. let domain = unsafe { libc::malloc(domain_length * 2) as *mut SecWChar }; if domain.is_null() { return ErrorKind::InternalError.to_u32().unwrap(); } - copy_nonoverlapping(psz_domain_name, domain, domain_length); + // SAFETY: This function is safe to call because `psz_domain_name` and `domain` are not null. We've checked this above. + unsafe { copy_nonoverlapping(psz_domain_name, domain, domain_length) }; + // SAFETY: Memory allocation is safe. let password = unsafe { libc::malloc(password_length * 2) as *mut SecWChar }; if password.is_null() { return ErrorKind::InternalError.to_u32().unwrap(); } - copy_nonoverlapping(psz_packed_credentials_string, password, password_length); + + // SAFETY: This function is safe to call because `psz_packed_credentials_string` and `password` are not null. We've checked this above. + unsafe { copy_nonoverlapping(psz_packed_credentials_string, password, password_length) }; let auth_identity = SecWinntAuthIdentityW { user, @@ -758,7 +923,8 @@ pub unsafe extern "system" fn SspiEncodeStringsAsAuthIdentity( flags: 0, }; - *pp_auth_identity = into_raw_ptr(auth_identity) as *mut c_void; + // SAFETY: `pp_auth_identity` is not null. We've checked this above. + unsafe { *pp_auth_identity = into_raw_ptr(auth_identity) as *mut c_void; } 0 } @@ -774,13 +940,32 @@ pub unsafe extern "system" fn SspiFreeAuthIdentity(auth_data: *mut c_void) -> Se return 0; } - let auth_data = auth_data as *mut SecWinntAuthIdentityW; + let auth_data = auth_data.cast::(); + // SAFETY: The pointer is not null: checked above. + // The user have to ensure that the data behind this pointer is valid. + let auth_data = unsafe { auth_data.as_mut() }.expect("auth_data pointer should not be null"); - unsafe { libc::free((*auth_data).user as *mut _) }; - unsafe { libc::free((*auth_data).domain as *mut _) }; - unsafe { libc::free((*auth_data).password as *mut _) }; + if !auth_data.user.is_null() { + // SAFETY: We use malloc to allocated buffers for the user. + // The user have to ensure that the auth identity was allocated by us. + unsafe { libc::free(auth_data.user as *mut _); } + } + if !auth_data.domain.is_null() { + // SAFETY: We use malloc to allocated buffers for the user. + // The user have to ensure that the auth identity was allocated by us. + unsafe { libc::free(auth_data.domain as *mut _); } + } + if !auth_data.password.is_null() { + // SAFETY: We use malloc to allocated buffers for the user. + // The user have to ensure that the auth identity was allocated by us. + unsafe { libc::free(auth_data.password as *mut _); } + } - let _auth_data: Box = Box::from_raw(auth_data); + // SAFETY: `auth_data` is not null. We've checked this above. + // We create and allocate `SecWinntAuthIdentityW` using `Box::into_raw`. Thus, + // it is safe to deallocate them using `Box::from_raw`. + // The user have to ensure that the auth identity was allocated by us. + let _auth_data: Box = unsafe { Box::from_raw(auth_data) }; 0 } diff --git a/ffi/src/sspi/utils.rs b/ffi/src/sspi/utils.rs index 2f273893..1f4778c9 100644 --- a/ffi/src/sspi/utils.rs +++ b/ffi/src/sspi/utils.rs @@ -3,19 +3,23 @@ use sspi::{CredentialsBuffers, Result}; use super::credentials_attributes::CredentialsAttributes; use super::sec_handle::CredentialsHandle; +/// Transforms a passed pointer to the credentials handle into a triplet of [CredentialsBuffers], +/// security package name, and [CredentialsAttributes]. +/// +/// # Safety: +/// +/// The caller have to ensure that either the pointer is null or the pointer is [convertible to a reference](https://doc.rust-lang.org/std/ptr/index.html#pointer-to-reference-conversion). pub unsafe fn transform_credentials_handle<'a>( credentials_handle: *mut CredentialsHandle, ) -> Option<(CredentialsBuffers, &'a str, &'a CredentialsAttributes)> { - if credentials_handle.is_null() { - None - } else { - let cred_handle = credentials_handle.as_mut().unwrap(); - Some(( + // SAFETY: `credentials_handle` is not null. We've checked this above. + unsafe { credentials_handle.as_mut() }.map(|cred_handle| { + ( cred_handle.credentials.clone(), cred_handle.security_package_name.as_str(), &cred_handle.attributes, - )) - } + ) + }) } // When encoding a UTF-16 character using two code units, the 16-bit values are chosen from diff --git a/ffi/src/utils.rs b/ffi/src/utils.rs index 70007c69..7d72795f 100644 --- a/ffi/src/utils.rs +++ b/ffi/src/utils.rs @@ -6,19 +6,34 @@ pub fn into_raw_ptr(value: T) -> *mut T { Box::into_raw(Box::new(value)) } +/// # Safety +/// +/// *Note*: the resulting [String] will contain a null-terminator char at the end. +/// Behavior is undefined is any of the following conditions are violated: +/// +/// * `s` must be a [valid] C string. pub unsafe fn c_w_str_to_string(s: *const u16) -> String { let mut len = 0; + // SAFETY: The user must provide guarantees that `s` is a valid C string. while unsafe { *(s.add(len)) } != 0 { len += 1; } + // SAFETY: The user must provide guarantees that `s` is a valid C string. String::from_utf16_lossy(unsafe { from_raw_parts(s, len) }) } +/// # Safety +/// +/// The returned length includes the null terminator char. +/// Behavior is undefined is any of the following conditions are violated: +/// +/// * `s` must be a [valid] C string. pub unsafe fn w_str_len(s: *const u16) -> usize { let mut len = 0; + // SAFETY: The user must provide guarantees that `s` is a valid C string. while unsafe { *(s.add(len)) } != 0 { len += 1; } @@ -35,7 +50,8 @@ pub unsafe fn w_str_len(s: *const u16) -> usize { /// /// # Safety /// -/// * `raw_buffer` must be valid for reads for `len` many bytes, and it must be properly aligned. +/// * the `raw_buffer` pointer can be null. +/// * if `raw_buffer` is not null, then it must be valid for reads for `len` many bytes, and it must be properly aligned. /// * The total size `len` of the slice must be no larger than `isize::MAX`, and adding that size to `data` /// must not "wrap around" the address space. pub unsafe fn credentials_str_into_bytes(raw_buffer: *const c_char, len: usize) -> Vec { diff --git a/ffi/src/winscard/scard_handle.rs b/ffi/src/winscard/scard_handle.rs index cc2fc6fa..ea327e4e 100644 --- a/ffi/src/winscard/scard_handle.rs +++ b/ffi/src/winscard/scard_handle.rs @@ -74,7 +74,7 @@ impl WinScardContextHandle { /// Allocated a new buffer inside the scard context. #[instrument(level = "debug", ret)] pub fn allocate_buffer(&mut self, size: usize) -> WinScardResult<*mut u8> { - // SAFETY: Memory allocation should be safe. Moreover, we check for the null value below. + // SAFETY: Memory allocation is safe. Moreover, we check for the null value below. let buff = unsafe { libc::malloc(size) as *mut u8 }; if buff.is_null() { return Err(Error::new( diff --git a/src/auth_identity.rs b/src/auth_identity.rs index 8eee839c..71473cd9 100644 --- a/src/auth_identity.rs +++ b/src/auth_identity.rs @@ -170,6 +170,14 @@ impl AuthIdentityBuffers { pub fn is_empty(&self) -> bool { self.user.is_empty() } + + pub fn from_utf8(user: &str, domain: &str, password: &str) -> Self { + Self { + user: utils::string_to_utf16(user), + domain: utils::string_to_utf16(domain), + password: utils::string_to_utf16(password).into(), + } + } } impl fmt::Debug for AuthIdentityBuffers { diff --git a/src/builders/accept_sec_context.rs b/src/builders/accept_sec_context.rs index 39adb0d1..2c06c3ca 100644 --- a/src/builders/accept_sec_context.rs +++ b/src/builders/accept_sec_context.rs @@ -6,6 +6,7 @@ use super::{ ToAssign, WithContextRequirements, WithCredentialsHandle, WithOutput, WithTargetDataRepresentation, WithoutContextRequirements, WithoutCredentialsHandle, WithoutOutput, WithoutTargetDataRepresentation, }; +use crate::generator::GeneratorAcceptSecurityContext; use crate::{DataRepresentation, SecurityBuffer, SecurityStatus, ServerRequestFlags, ServerResponseFlags, SspiPackage}; pub type EmptyAcceptSecurityContext<'a, C> = AcceptSecurityContext< @@ -252,24 +253,8 @@ impl<'a, CredsHandle> FilledAcceptSecurityContext<'a, CredsHandle> { /// Executes the SSPI function that the builder represents. pub fn execute( self, - inner: SspiPackage<'_, CredsHandle, AuthData>, - ) -> crate::Result { + inner: SspiPackage<'a, CredsHandle, AuthData>, + ) -> crate::Result> { inner.accept_security_context_impl(self) } - - pub(crate) fn transform(self) -> FilledAcceptSecurityContext<'a, CredsHandle> { - AcceptSecurityContext { - phantom_creds_use_set: PhantomData, - phantom_context_req_set: PhantomData, - phantom_data_repr_set: PhantomData, - phantom_output_set: PhantomData, - - credentials_handle: self.credentials_handle, - context_requirements: self.context_requirements, - target_data_representation: self.target_data_representation, - - output: self.output, - input: self.input, - } - } } diff --git a/src/cert_utils.rs b/src/cert_utils.rs index 753cc951..b27e1dc2 100644 --- a/src/cert_utils.rs +++ b/src/cert_utils.rs @@ -57,17 +57,23 @@ unsafe fn find_raw_cert_by_thumbprint(thumbprint: &[u8], cert_store: *mut c_void )) } -unsafe fn open_user_cert_store() -> Result<*mut c_void> { +fn open_user_cert_store() -> Result<*mut c_void> { // "My\0" encoded as a wide string. // More info: https://docs.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certopenstore#remarks let my: [u16; 3] = [77, 121, 0]; - let cert_store = CertOpenStore( - CERT_STORE_PROV_SYSTEM_W, - 0, - 0, - CERT_SYSTEM_STORE_CURRENT_USER_ID << CERT_SYSTEM_STORE_LOCATION_SHIFT, - my.as_ptr() as *const _, - ); + // SAFETY: + // * constant parameters are taken from the `windows_sys` crate. Thus, they are valid; + // * `dwEncodingType` and `hCryptProv` are allowed to be zero by documentation; + // * `my` is as valid wide C string. + let cert_store = unsafe { + CertOpenStore( + CERT_STORE_PROV_SYSTEM_W, + 0, + 0, + CERT_SYSTEM_STORE_CURRENT_USER_ID << CERT_SYSTEM_STORE_LOCATION_SHIFT, + my.as_ptr() as *const _, + ) + }; if cert_store.is_null() { return Err(Error::new( @@ -80,17 +86,29 @@ unsafe fn open_user_cert_store() -> Result<*mut c_void> { } #[instrument(level = "trace", ret)] -pub unsafe fn extract_raw_certificate_by_thumbprint(thumbprint: &[u8]) -> Result> { +pub fn extract_raw_certificate_by_thumbprint(thumbprint: &[u8]) -> Result> { let cert_store = open_user_cert_store()?; - let cert = find_raw_cert_by_thumbprint(thumbprint, cert_store)?; + // SAFETY: `open_user_cert_store` returns valid store handle. + let cert = unsafe { find_raw_cert_by_thumbprint(thumbprint, cert_store) }.inspect_err(|_err| { + // SAFETY: `open_user_cert_store` returns valid store handle that needs to be closed. + if unsafe { CertCloseStore(cert_store, 0) } == 0 { + warn!("could not close the certificate store"); + } + })?; - CertCloseStore(cert_store, 0); + // SAFETY: `open_user_cert_store` returns valid store handle that needs to be closed. + if unsafe { CertCloseStore(cert_store, 0) } == 0 { + return Err(Error::new( + ErrorKind::InternalError, + "could not close the certificate store", + )); + } Ok(cert) } #[instrument(level = "trace", ret)] -pub unsafe fn extract_certificate_by_thumbprint(thumbprint: &[u8]) -> Result<(Vec, Certificate)> { +pub fn extract_certificate_by_thumbprint(thumbprint: &[u8]) -> Result<(Vec, Certificate)> { let raw_cert = extract_raw_certificate_by_thumbprint(thumbprint)?; Ok((raw_cert.to_vec(), picky_asn1_der::from_bytes(&raw_cert)?)) @@ -195,15 +213,21 @@ pub struct SmartCardInfo { // The similar approach is implemented in the FreeRDP for the smart card information gathering: // https://github.com/FreeRDP/FreeRDP/blob/56324906a2d5b2538675e2f10b9f1ffe4a27de79/libfreerdp/core/smartcardlogon.c#L616 #[instrument(level = "trace", ret)] -pub unsafe fn finalize_smart_card_info(cert_serial_number: &[u8]) -> Result { +pub fn finalize_smart_card_info(cert_serial_number: &[u8]) -> Result { let mut crypt_context_handle = HCRYPTPROV::default(); - if CryptAcquireContextW( - &mut crypt_context_handle, - null(), - CSP_NAME_W.as_ptr() as *const _, - PROV_RSA_FULL, - CRYPT_SILENT, - ) == 0 + // SAFETY: + // * `phProv` is constructed using Rust references. This it is valid. + // * `szContainer` can be null according to the documentation. + // * all other parameters are hardcoded constants that are valid. + if unsafe { + CryptAcquireContextW( + &mut crypt_context_handle, + null(), + CSP_NAME_W.as_ptr() as *const _, + PROV_RSA_FULL, + CRYPT_SILENT, + ) + } == 0 { return Err(Error::new( ErrorKind::InternalError, @@ -215,26 +239,40 @@ pub unsafe fn finalize_smart_card_info(cert_serial_number: &[u8]) -> Result Result reader_name, @@ -277,7 +322,14 @@ pub unsafe fn finalize_smart_card_info(cert_serial_number: &[u8]) -> Result>, pub error: crate::Error, } @@ -151,11 +152,10 @@ enum EndpointType { } #[derive(Debug, Clone)] -#[allow(clippy::large_enum_variant)] pub enum ClientMode { Negotiate(NegotiateConfig), Kerberos(KerberosConfig), - Pku2u(Pku2uConfig), + Pku2u(Box), Ntlm(NtlmConfig), } @@ -224,11 +224,11 @@ impl CredSspClient { } #[instrument(fields(state = ?self.state), skip_all)] - pub fn process<'a>( - &'a mut self, + pub fn process( + &mut self, ts_request: TsRequest, - ) -> Generator<'a, NetworkRequest, crate::Result>, crate::Result> { - Generator::<'a, NetworkRequest, crate::Result>, crate::Result>::new( + ) -> Generator>, crate::Result> { + Generator::>, crate::Result>::new( move |mut yield_point| async move { self.process_impl(&mut yield_point, ts_request).await }, ) } @@ -248,13 +248,13 @@ impl CredSspClient { .expect("CredSsp client mode should never be empty") { ClientMode::Negotiate(negotiate_config) => Some(CredSspContext::new(SspiContext::Negotiate( - Negotiate::new(negotiate_config)?, + Negotiate::new_client(negotiate_config)?, ))), ClientMode::Kerberos(kerberos_config) => Some(CredSspContext::new(SspiContext::Kerberos( Kerberos::new_client_from_config(kerberos_config)?, ))), ClientMode::Pku2u(pku2u) => Some(CredSspContext::new(SspiContext::Pku2u( - Pku2u::new_client_from_config(pku2u)?, + Pku2u::new_client_from_config(*pku2u)?, ))), ClientMode::Ntlm(ntlm) => Some(CredSspContext::new(SspiContext::Ntlm(Ntlm::with_config(ntlm)))), }; @@ -309,7 +309,7 @@ impl CredSspClient { ts_request.nego_tokens = Some(output_token.remove(0).buffer); if result.status == SecurityStatus::Ok { - debug!("CredSSP finished NLA stage"); + debug!("CredSSp finished NLA stage."); let peer_version = self.context.as_ref().unwrap().peer_version.expect( @@ -382,6 +382,14 @@ impl CredSspClient { } } +#[derive(Debug, Clone)] +pub enum ServerMode { + Negotiate(NegotiateConfig), + Kerberos(Box<(KerberosConfig, ServerProperties)>), + Pku2u(Box), + Ntlm(NtlmConfig), +} + /// Implements the CredSSP *server*. The client's credentials /// securely delegated to the server for authentication using TLS. /// @@ -396,11 +404,11 @@ pub struct CredSspServer> public_key: Vec, credentials_handle: Option, ts_request_version: u32, - context_config: Option, + context_config: Option, } -impl> CredSspServer { - pub fn new(public_key: Vec, credentials: C, client_mode: ClientMode) -> crate::Result { +impl + Send> CredSspServer { + pub fn new(public_key: Vec, credentials: C, client_mode: ServerMode) -> crate::Result { Ok(Self { state: CredSspState::NegoToken, context: None, @@ -416,7 +424,7 @@ impl> CredSspServer { public_key: Vec, credentials: C, ts_request_version: u32, - client_mode: ClientMode, + client_mode: ServerMode, ) -> crate::Result { Ok(Self { state: CredSspState::NegoToken, @@ -429,24 +437,40 @@ impl> CredSspServer { }) } - #[allow(clippy::result_large_err)] #[instrument(fields(state = ?self.state), skip_all)] - pub fn process(&mut self, mut ts_request: TsRequest) -> Result { + pub fn process( + &mut self, + ts_request: TsRequest, + ) -> Generator>, Result> { + Generator::>, Result>::new( + move |mut yield_point| async move { self.process_impl(&mut yield_point, ts_request).await }, + ) + } + + async fn process_impl( + &mut self, + yield_point: &mut YieldPointLocal, + mut ts_request: TsRequest, + ) -> Result { if self.context.is_none() { self.context = match self .context_config .take() .expect("CredSsp client mode should never be empty") { - ClientMode::Negotiate(neg_config) => Some(CredSspContext::new(SspiContext::Negotiate( - try_cred_ssp_server!(Negotiate::new(neg_config), ts_request), + ServerMode::Negotiate(neg_config) => Some(CredSspContext::new(SspiContext::Negotiate( + try_cred_ssp_server!(Negotiate::new_server(neg_config), ts_request), ))), - ClientMode::Kerberos(kerberos_config) => Some(CredSspContext::new(SspiContext::Kerberos( - try_cred_ssp_server!(Kerberos::new_server_from_config(kerberos_config), ts_request), - ))), - ClientMode::Ntlm(ntlm) => Some(CredSspContext::new(SspiContext::Ntlm(Ntlm::with_config(ntlm)))), - ClientMode::Pku2u(pku2u) => Some(CredSspContext::new(SspiContext::Pku2u(try_cred_ssp_server!( - Pku2u::new_server_from_config(pku2u), + ServerMode::Kerberos(kerberos_mode) => { + let (kerberos_config, server_properties) = *kerberos_mode; + Some(CredSspContext::new(SspiContext::Kerberos(try_cred_ssp_server!( + Kerberos::new_server_from_config(kerberos_config, server_properties), + ts_request + )))) + } + ServerMode::Ntlm(ntlm) => Some(CredSspContext::new(SspiContext::Ntlm(Ntlm::with_config(ntlm)))), + ServerMode::Pku2u(pku2u) => Some(CredSspContext::new(SspiContext::Pku2u(try_cred_ssp_server!( + Pku2u::new_server_from_config(*pku2u), ts_request )))), }; @@ -503,20 +527,21 @@ impl> CredSspServer { } CredSspState::NegoToken => { let input = ts_request.nego_tokens.take().unwrap_or_default(); - let input_token = SecurityBuffer::new(input, BufferType::Token); + let mut input_token = vec![SecurityBuffer::new(input, BufferType::Token)]; let mut output_token = vec![SecurityBuffer::new(Vec::with_capacity(1024), BufferType::Token)]; let mut credentials_handle = self.credentials_handle.take(); let sspi_context = &mut self.context.as_mut().unwrap().sspi_context; + + let builder = sspi_context + .accept_security_context() + .with_credentials_handle(&mut credentials_handle) + .with_context_requirements(ServerRequestFlags::empty()) + .with_target_data_representation(DataRepresentation::Native) + .with_input(&mut input_token) + .with_output(&mut output_token); match try_cred_ssp_server!( - sspi_context - .accept_security_context() - .with_credentials_handle(&mut credentials_handle) - .with_context_requirements(ServerRequestFlags::empty()) - .with_target_data_representation(DataRepresentation::Native) - .with_input(&mut [input_token]) - .with_output(&mut output_token) - .execute(sspi_context), + sspi_context.accept_security_context_impl(yield_point, builder).await, ts_request ) { AcceptSecurityContextResult { @@ -526,7 +551,7 @@ impl> CredSspServer { ts_request.nego_tokens = Some(output_token.remove(0).buffer); } AcceptSecurityContextResult { - status: SecurityStatus::CompleteNeeded, + status: SecurityStatus::CompleteNeeded | SecurityStatus::Ok, .. } => { let ContextNames { username } = try_cred_ssp_server!( @@ -589,14 +614,22 @@ impl> CredSspServer { self.state = CredSspState::AuthInfo; } - _ => unreachable!(), + result => { + try_cred_ssp_server!( + Err(Error::new( + ErrorKind::InternalError, + format!("SSPI returned unexpected status: {:?}", result.status) + )), + ts_request + ) + } }; self.credentials_handle = credentials_handle; Ok(ServerState::ReplyNeeded(ts_request)) } CredSspState::Final => Err(ServerError { - ts_request, + ts_request: Some(Box::new(ts_request)), error: Error::new( ErrorKind::UnsupportedFunction, "CredSSP server's 'process' method must not be fired after the 'Finished' state", @@ -682,47 +715,13 @@ impl SspiImpl for SspiContext { } #[instrument(ret, level = "debug", fields(security_package = self.package_name()), skip_all)] - fn accept_security_context_impl( - &mut self, - builder: FilledAcceptSecurityContext<'_, Self::CredentialsHandle>, - ) -> crate::Result { - match self { - SspiContext::Ntlm(ntlm) => { - let mut auth_identity = match builder.credentials_handle { - Some(Some(CredentialsBuffers::AuthIdentity(identity))) => Some(identity.clone()), - Some(Some(_)) => { - return Err(Error::new( - ErrorKind::UnknownCredentials, - "only password-based auth is supported in NTLM", - )) - } - Some(None) => None, - None => { - return Err(Error::new( - ErrorKind::NoCredentials, - "credentials handle is not provided for the NTLM", - )) - } - }; - builder.full_transform(Some(&mut auth_identity)).execute(ntlm) - } - SspiContext::Kerberos(kerberos) => builder.transform().execute(kerberos), - SspiContext::Negotiate(negotiate) => builder.transform().execute(negotiate), - SspiContext::Pku2u(pku2u) => { - let auth_identity = - if let Some(Some(CredentialsBuffers::AuthIdentity(identity))) = builder.credentials_handle { - identity.clone() - } else { - return Err(Error::new( - ErrorKind::NoCredentials, - "auth identity is not provided for the Pku2u", - )); - }; - builder.full_transform(Some(&mut Some(auth_identity))).execute(pku2u) - } - #[cfg(feature = "tsssp")] - SspiContext::CredSsp(credssp) => builder.transform().execute(credssp), - } + fn accept_security_context_impl<'a>( + &'a mut self, + builder: FilledAcceptSecurityContext<'a, Self::CredentialsHandle>, + ) -> crate::Result> { + Ok(GeneratorAcceptSecurityContext::new(move |mut yield_point| async move { + self.accept_security_context_impl(&mut yield_point, builder).await + })) } fn initialize_security_context_impl<'a>( @@ -763,12 +762,65 @@ impl<'a> SspiContext { .resolve_with_default_network_client() } + #[cfg(feature = "network_client")] + pub fn accept_security_context_sync( + &mut self, + builder: FilledAcceptSecurityContext<'_, ::CredentialsHandle>, + ) -> crate::Result { + Generator::new(move |mut yield_point| async move { + self.accept_security_context_impl(&mut yield_point, builder).await + }) + .resolve_with_default_network_client() + } + #[cfg(feature = "network_client")] pub fn change_password_sync(&mut self, builder: ChangePassword) -> crate::Result<()> { Generator::new(move |mut yield_point| async move { self.change_password_impl(&mut yield_point, builder).await }) .resolve_with_default_network_client() } + pub(crate) async fn accept_security_context_impl( + &mut self, + yield_point: &mut YieldPointLocal, + builder: FilledAcceptSecurityContext<'a, ::CredentialsHandle>, + ) -> crate::Result { + match self { + SspiContext::Ntlm(ntlm) => { + let mut auth_identity = match builder.credentials_handle { + Some(Some(CredentialsBuffers::AuthIdentity(identity))) => Some(identity.clone()), + Some(Some(_)) => { + return Err(Error::new( + ErrorKind::UnknownCredentials, + "only password-based auth is supported in NTLM", + )) + } + Some(None) => None, + None => { + return Err(Error::new( + ErrorKind::NoCredentials, + "credentials handle is not provided for the NTLM", + )) + } + }; + let new_builder = builder.full_transform(Some(&mut auth_identity)); + ntlm.accept_security_context_impl(new_builder) + } + SspiContext::Kerberos(kerberos) => kerberos.accept_security_context_impl(yield_point, builder).await, + SspiContext::Negotiate(negotiate) => negotiate.accept_security_context_impl(yield_point, builder).await, + SspiContext::Pku2u(pku2u) => { + let mut creds_handle = builder + .credentials_handle + .as_ref() + .and_then(|creds| (*creds).clone()) + .and_then(|creds_handle| creds_handle.auth_identity()); + let new_builder = builder.full_transform(Some(&mut creds_handle)); + pku2u.accept_security_context_impl(yield_point, new_builder).await + } + #[cfg(feature = "tsssp")] + SspiContext::CredSsp(credssp) => credssp.accept_security_context_impl(yield_point, builder).await, + } + } + #[instrument(ret, level = "debug", fields(security_package = self.package_name()), skip_all)] async fn initialize_security_context_impl( &'a mut self, diff --git a/src/credssp/sspi_cred_ssp/mod.rs b/src/credssp/sspi_cred_ssp/mod.rs index bfb05c37..1c362436 100644 --- a/src/credssp/sspi_cred_ssp/mod.rs +++ b/src/credssp/sspi_cred_ssp/mod.rs @@ -14,7 +14,9 @@ use self::tls_connection::{danger, TlsConnection}; use super::ts_request::NONCE_SIZE; use super::{CredSspContext, CredSspMode, EndpointType, SspiContext, TsRequest}; use crate::credssp::sspi_cred_ssp::tls_connection::{DecryptionResult, DecryptionResultBuffers}; -use crate::generator::{GeneratorChangePassword, GeneratorInitSecurityContext, YieldPointLocal}; +use crate::generator::{ + GeneratorAcceptSecurityContext, GeneratorChangePassword, GeneratorInitSecurityContext, YieldPointLocal, +}; use crate::{ builders, negotiate, AcquireCredentialsHandleResult, BufferType, CertContext, CertEncodingType, CertTrustErrorStatus, CertTrustInfoStatus, CertTrustStatus, ClientRequestFlags, ClientResponseFlags, @@ -352,19 +354,29 @@ impl SspiImpl for SspiCredSsp { })) } - #[instrument(level = "debug", ret, fields(state = ?self.state), skip(self, _builder))] - fn accept_security_context_impl( + #[instrument(level = "debug", ret, fields(state = ?self.state), skip(self, builder))] + fn accept_security_context_impl<'a>( + &'a mut self, + builder: builders::FilledAcceptSecurityContext<'a, Self::CredentialsHandle>, + ) -> Result> { + Ok(GeneratorAcceptSecurityContext::new(move |mut yield_point| async move { + self.accept_security_context_impl(&mut yield_point, builder).await + })) + } +} + +impl SspiCredSsp { + pub(crate) async fn accept_security_context_impl( &mut self, - _builder: builders::FilledAcceptSecurityContext<'_, Self::CredentialsHandle>, + _yield_point: &mut YieldPointLocal, + _builder: builders::FilledAcceptSecurityContext<'_, ::CredentialsHandle>, ) -> Result { Err(Error::new( ErrorKind::UnsupportedFunction, - "AcceptSecurityContext is not supported in SspiCredSsp context", + "accept_security_context_impl is not supported in SspiCredSsp", )) } -} -impl SspiCredSsp { #[instrument(ret, level = "debug", fields(state = ?self.state), skip_all)] #[async_recursion] pub(crate) async fn initialize_security_context_impl<'a>( diff --git a/src/generator.rs b/src/generator.rs index d2e40208..0c0d5666 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -5,8 +5,9 @@ use std::task::{Context, Poll, Wake, Waker}; use url::Url; +use crate::credssp::ServerError; use crate::network_client::{AsyncNetworkClient, NetworkClient, NetworkProtocol}; -use crate::{Error, InitializeSecurityContextResult}; +use crate::{AcceptSecurityContextResult, Error, InitializeSecurityContextResult}; pub struct Interrupt { value_to_yield: Option, @@ -139,11 +140,11 @@ fn execute_one_step(task: &mut PinnedFuture) -> Option { } /// Utility types and methods -impl<'a, YieldTy, ResumeTy, OutTy> Generator<'a, YieldTy, ResumeTy, Result> +impl<'a, YieldTy, ResumeTy, OutTy> Generator<'a, YieldTy, ResumeTy, Result> where OutTy: Send + 'a, { - pub fn resolve_to_result(&mut self) -> Result { + pub fn resolve_to_result(&mut self) -> Result { let state = self.start(); match state { GeneratorState::Suspended(_) => Err(Error::new( @@ -162,6 +163,23 @@ where self.resolve_to_result().expect(msg) } } + +impl<'a, YieldTy, ResumeTy, OutTy> Generator<'a, YieldTy, ResumeTy, Result> +where + OutTy: Send + 'a, +{ + pub fn resolve_to_result(&mut self) -> Result { + let state = self.start(); + match state { + GeneratorState::Suspended(_) => Err(ServerError { + ts_request: None, + error: Error::new(crate::ErrorKind::UnsupportedFunction, "cannot finish generator"), + }), + GeneratorState::Completed(res) => res, + } + } +} + #[derive(Debug, Clone)] pub struct NetworkRequest { pub protocol: NetworkProtocol, @@ -181,9 +199,13 @@ where .finish() } } + pub type GeneratorInitSecurityContext<'a> = Generator<'a, NetworkRequest, crate::Result>, crate::Result>; +pub type GeneratorAcceptSecurityContext<'a> = + Generator<'a, NetworkRequest, crate::Result>, crate::Result>; + pub type GeneratorChangePassword<'a> = Generator<'a, NetworkRequest, crate::Result>, crate::Result<()>>; pub(crate) type YieldPointLocal = YieldPoint>>; diff --git a/src/kerberos/client/as_exchange.rs b/src/kerberos/client/as_exchange.rs new file mode 100644 index 00000000..a80ee196 --- /dev/null +++ b/src/kerberos/client/as_exchange.rs @@ -0,0 +1,70 @@ +use picky_krb::data_types::{KrbResult, ResultExt}; +use picky_krb::messages::{AsRep, KdcReqBody}; + +use crate::generator::YieldPointLocal; +use crate::kerberos::client::extractors::extract_salt_from_krb_error; +use crate::kerberos::client::generators::generate_as_req; +use crate::kerberos::pa_datas::AsReqPaDataOptions; +use crate::kerberos::utils::serialize_message; +use crate::{Error, ErrorKind, Kerberos, Result}; + +/// Performs AS exchange as specified in [RFC 4210: The Authentication Service Exchange](https://www.rfc-editor.org/rfc/rfc4120#section-3.1). +pub async fn as_exchange( + client: &mut Kerberos, + yield_point: &mut YieldPointLocal, + kdc_req_body: &KdcReqBody, + mut pa_data_options: AsReqPaDataOptions<'_>, +) -> Result { + pa_data_options.with_pre_auth(false); + let pa_datas = pa_data_options.generate()?; + let as_req = generate_as_req(pa_datas, kdc_req_body.clone()); + + let response = client.send(yield_point, &serialize_message(&as_req)?).await?; + + // first 4 bytes are message len. skipping them + { + if response.len() < 4 { + return Err(Error::new( + ErrorKind::InternalError, + "the KDC reply message is too small: expected at least 4 bytes", + )); + } + + let mut d = picky_asn1_der::Deserializer::new_from_bytes(&response[4..]); + let as_rep: KrbResult = KrbResult::deserialize(&mut d)?; + + if as_rep.is_ok() { + error!("KDC replied with AS_REP to the AS_REQ without the encrypted timestamp. The KRB_ERROR expected."); + + return Err(Error::new( + ErrorKind::InvalidToken, + "KDC server should not process AS_REQ without the pa-pac data", + )); + } + + if let Some(correct_salt) = extract_salt_from_krb_error(&as_rep.unwrap_err())? { + debug!("salt extracted successfully from the KRB_ERROR"); + + pa_data_options.with_salt(correct_salt.as_bytes().to_vec()); + } + } + + pa_data_options.with_pre_auth(true); + let pa_datas = pa_data_options.generate()?; + + let as_req = generate_as_req(pa_datas, kdc_req_body.clone()); + + let response = client.send(yield_point, &serialize_message(&as_req)?).await?; + + if response.len() < 4 { + return Err(Error::new( + ErrorKind::InternalError, + "the KDC reply message is too small: expected at least 4 bytes", + )); + } + + // first 4 bytes are message len. skipping them + let mut d = picky_asn1_der::Deserializer::new_from_bytes(&response[4..]); + + Ok(KrbResult::::deserialize(&mut d)?.inspect_err(|err| error!(?err, "AS exchange error"))?) +} diff --git a/src/kerberos/client/change_password.rs b/src/kerberos/client/change_password.rs new file mode 100644 index 00000000..96342f89 --- /dev/null +++ b/src/kerberos/client/change_password.rs @@ -0,0 +1,146 @@ +use picky_krb::crypto::CipherSuite; +use picky_krb::messages::KrbPrivMessage; +use rand::rngs::OsRng; +use rand::Rng; + +use crate::builders::ChangePassword; +use crate::generator::YieldPointLocal; +use crate::kerberos::client::extractors::{ + extract_encryption_params_from_as_rep, extract_session_key_from_as_rep, extract_status_code_from_krb_priv_response, +}; +use crate::kerberos::client::generators::{ + generate_as_req_kdc_body, generate_authenticator, generate_krb_priv_request, get_client_principal_name_type, + get_client_principal_realm, EncKey, GenerateAsPaDataOptions, GenerateAsReqOptions, GenerateAuthenticatorOptions, +}; +use crate::kerberos::pa_datas::AsReqPaDataOptions; +use crate::kerberos::utils::{serialize_message, unwrap_hostname}; +use crate::kerberos::{client, CHANGE_PASSWORD_SERVICE_NAME, DEFAULT_ENCRYPTION_TYPE, KADMIN}; +use crate::utils::generate_random_symmetric_key; +use crate::{ClientRequestFlags, Error, ErrorKind, Kerberos, Result}; + +/// [Kerberos Change Password and Set Password Protocols](https://datatracker.ietf.org/doc/html/rfc3244#section-2) +/// "The service accepts requests on UDP port 464 and TCP port 464 as well." +const KPASSWD_PORT: u16 = 464; + +#[instrument(level = "debug", ret, fields(state = ?client.state), skip(client, change_password))] +pub async fn change_password<'a>( + client: &'a mut Kerberos, + yield_point: &mut YieldPointLocal, + change_password: ChangePassword<'a>, +) -> Result<()> { + let username = &change_password.account_name; + let domain = &change_password.domain_name; + let password = &change_password.old_password; + + let salt = format!("{}{}", domain, username); + + let cname_type = get_client_principal_name_type(username, domain); + let realm = &get_client_principal_realm(username, domain); + let hostname = unwrap_hostname(client.config.client_computer_name.as_deref())?; + + let options = GenerateAsReqOptions { + realm, + username, + cname_type, + snames: &[KADMIN, CHANGE_PASSWORD_SERVICE_NAME], + // 4 = size of u32 + nonce: &OsRng.gen::().to_ne_bytes(), + hostname: &hostname, + context_requirements: ClientRequestFlags::empty(), + }; + let kdc_req_body = generate_as_req_kdc_body(&options)?; + + let pa_data_options = AsReqPaDataOptions::AuthIdentity(GenerateAsPaDataOptions { + password: password.as_ref(), + salt: salt.as_bytes().to_vec(), + enc_params: client.encryption_params.clone(), + with_pre_auth: false, + }); + + let as_rep = client::as_exchange(client, yield_point, &kdc_req_body, pa_data_options).await?; + + debug!("AS exchange finished successfully."); + + client.realm = Some(as_rep.0.crealm.0.to_string()); + + let (encryption_type, salt) = extract_encryption_params_from_as_rep(&as_rep)?; + debug!(?encryption_type, "Negotiated encryption type"); + + client.encryption_params.encryption_type = Some(CipherSuite::try_from(usize::from(encryption_type))?); + + let session_key = extract_session_key_from_as_rep(&as_rep, &salt, password.as_ref(), &client.encryption_params)?; + + let seq_num = client.next_seq_number(); + + let enc_type = client + .encryption_params + .encryption_type + .as_ref() + .unwrap_or(&DEFAULT_ENCRYPTION_TYPE); + let authenticator_seb_key = generate_random_symmetric_key(enc_type, &mut OsRng); + + let authenticator = generate_authenticator(GenerateAuthenticatorOptions { + kdc_rep: &as_rep.0, + seq_num: Some(seq_num), + sub_key: Some(EncKey { + key_type: enc_type.clone(), + key_value: authenticator_seb_key, + }), + checksum: None, + channel_bindings: client.channel_bindings.as_ref(), + extensions: Vec::new(), + })?; + + let krb_priv = generate_krb_priv_request( + as_rep.0.ticket.0, + &session_key, + change_password.new_password.as_ref().as_bytes(), + &authenticator, + &client.encryption_params, + seq_num, + &hostname, + )?; + + if let Some((_realm, mut kdc_url)) = client.get_kdc() { + kdc_url + .set_port(Some(KPASSWD_PORT)) + .map_err(|_| Error::new(ErrorKind::InvalidParameter, "Cannot set port for KDC URL"))?; + + let response = client.send(yield_point, &serialize_message(&krb_priv)?).await?; + trace!(?response, "Change password raw response"); + + if response.len() < 4 { + return Err(Error::new( + ErrorKind::InternalError, + "the KDC reply message is too small: expected at least 4 bytes", + )); + } + + let krb_priv_response = KrbPrivMessage::deserialize(&response[4..]).map_err(|err| { + Error::new( + ErrorKind::InvalidToken, + format!("cannot deserialize krb_priv_response: {:?}", err), + ) + })?; + + let result_status = extract_status_code_from_krb_priv_response( + &krb_priv_response.krb_priv, + &authenticator.0.subkey.0.as_ref().unwrap().0.key_value.0 .0, + &client.encryption_params, + )?; + + if result_status != 0 { + return Err(Error::new( + ErrorKind::WrongCredentialHandle, + format!("unsuccessful krb result code: {}. expected 0", result_status), + )); + } + } else { + return Err(Error::new( + ErrorKind::NoAuthenticatingAuthority, + "no KDC server found".to_owned(), + )); + } + + Ok(()) +} diff --git a/src/kerberos/client/extractors.rs b/src/kerberos/client/extractors.rs index 9ce5a489..f2ea9938 100644 --- a/src/kerberos/client/extractors.rs +++ b/src/kerberos/client/extractors.rs @@ -1,13 +1,26 @@ -use picky_asn1::wrapper::Asn1SequenceOf; -use picky_krb::constants::key_usages::{AS_REP_ENC, KRB_PRIV_ENC_PART, TGS_REP_ENC_SESSION_KEY}; +use std::io::Read; + +use picky_asn1::wrapper::{Asn1SequenceOf, ObjectIdentifierAsn1}; +use picky_asn1_der::application_tag::ApplicationTag; +use picky_asn1_der::Asn1RawDer; +use picky_krb::constants::key_usages::{AP_REP_ENC, AS_REP_ENC, KRB_PRIV_ENC_PART, TGS_REP_ENC_SESSION_KEY}; use picky_krb::constants::types::PA_ETYPE_INFO2_TYPE; use picky_krb::crypto::CipherSuite; -use picky_krb::data_types::{EncKrbPrivPart, EtypeInfo2, PaData}; -use picky_krb::messages::{AsRep, EncAsRepPart, EncTgsRepPart, KrbError, KrbPriv, TgsRep}; +use picky_krb::data_types::{EncApRepPart, EncKrbPrivPart, EtypeInfo2, PaData, Ticket}; +use picky_krb::gss_api::NegTokenTarg1; +use picky_krb::messages::{ApRep, AsRep, EncAsRepPart, EncTgsRepPart, KrbError, KrbPriv, TgsRep, TgtRep}; use crate::kerberos::{EncryptionParams, DEFAULT_ENCRYPTION_TYPE}; use crate::{Error, ErrorKind, Result}; +/// Extracts password salt from the KRB error. +/// +/// We need a salt to derive the correct encryption key from user's password. Usually, the salt is domain+username, but the custom salt +/// value can be set in KDC database. So, we always extract the correct salt from the [KrbError] message. More info in [RFC 4120 PA-ETYPE-INFO2](https://www.rfc-editor.org/rfc/rfc4120#section-5.2.7.5): +/// +/// > The ETYPE-INFO2 pre-authentication type is sent by the KDC in a KRB-ERROR indicating a requirement for additional pre-authentication. +/// > It is usually used to notify a client of which key to use for the encryption of an encrypted timestamp for the purposes of sending a +/// > PA-ENC-TIMESTAMP pre-authentication value. pub fn extract_salt_from_krb_error(error: &KrbError) -> Result> { trace!(?error, "KRB_ERROR"); @@ -29,6 +42,7 @@ pub fn extract_salt_from_krb_error(error: &KrbError) -> Result> { Ok(None) } +/// Extracts a session from the [AsRep]. #[instrument(level = "trace", ret, skip(password))] pub fn extract_session_key_from_as_rep( as_rep: &AsRep, @@ -51,6 +65,7 @@ pub fn extract_session_key_from_as_rep( Ok(enc_as_rep_part.0.key.0.key_value.0.to_vec()) } +/// Extracts a session from the [TgsRep]. #[instrument(level = "trace", ret)] pub fn extract_session_key_from_tgs_rep( tgs_rep: &TgsRep, @@ -74,6 +89,11 @@ pub fn extract_session_key_from_tgs_rep( Ok(enc_as_rep_part.0.key.0.key_value.0.to_vec()) } +/// Extracts encryption type and salt from [AsRep]. +/// +/// More info in [RFC 4120 Receipt of KRB_AS_REP Message](https://www.rfc-editor.org/rfc/rfc4120#section-3.1.5): +/// +/// > If any padata fields are present, they may be used to derive the proper secret key to decrypt the message. #[instrument(level = "trace", ret)] pub fn extract_encryption_params_from_as_rep(as_rep: &AsRep) -> Result<(u8, String)> { match as_rep @@ -110,6 +130,7 @@ pub fn extract_encryption_params_from_as_rep(as_rep: &AsRep) -> Result<(u8, Stri } } +/// Extract a status code from the [KrbPriv] message. pub fn extract_status_code_from_krb_priv_response( krb_priv: &KrbPriv, auth_key: &[u8], @@ -148,3 +169,124 @@ pub fn extract_status_code_from_krb_priv_response( Ok(u16::from_be_bytes(user_data[0..2].try_into().unwrap())) } + +/// Extracts [ApRep] from the [NegTokenTarg1] . +#[instrument(ret, level = "trace")] +pub fn extract_ap_rep_from_neg_token_targ(token: &NegTokenTarg1) -> Result { + let resp_token = &token + .0 + .response_token + .0 + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::InvalidToken, "missing response token in NegTokenTarg"))? + .0 + .0; + + let mut data = resp_token.as_slice(); + let _oid: ApplicationTag = picky_asn1_der::from_reader(&mut data)?; + + let mut t = [0, 0]; + data.read_exact(&mut t)?; + + Ok(picky_asn1_der::from_reader(&mut data)?) +} + +/// Extracts the sequence number from the [ApRep]. +#[instrument(level = "trace", ret)] +pub fn extract_seq_number_from_ap_rep( + ap_rep: &ApRep, + session_key: &[u8], + enc_params: &EncryptionParams, +) -> Result> { + let cipher = enc_params + .encryption_type + .as_ref() + .unwrap_or(&DEFAULT_ENCRYPTION_TYPE) + .cipher(); + + let res = cipher + .decrypt(session_key, AP_REP_ENC, &ap_rep.0.enc_part.cipher.0 .0) + .map_err(|err| { + Error::new( + ErrorKind::DecryptFailure, + format!("cannot decrypt ap_rep.enc_part: {:?}", err), + ) + })?; + + let ap_rep_enc_part: EncApRepPart = picky_asn1_der::from_bytes(&res)?; + + Ok(ap_rep_enc_part + .0 + .seq_number + .0 + .ok_or_else(|| Error::new(ErrorKind::InvalidToken, "missing sequence number in ap_rep"))? + .0 + .0) +} + +/// Extracts a sub-session key from the [ApRep]. +#[instrument(level = "trace", ret)] +pub fn extract_sub_session_key_from_ap_rep( + ap_rep: &ApRep, + session_key: &[u8], + enc_params: &EncryptionParams, +) -> Result> { + let cipher = enc_params + .encryption_type + .as_ref() + .unwrap_or(&DEFAULT_ENCRYPTION_TYPE) + .cipher(); + + let res = cipher + .decrypt(session_key, AP_REP_ENC, &ap_rep.0.enc_part.cipher.0 .0) + .map_err(|err| { + Error::new( + ErrorKind::DecryptFailure, + format!("cannot decrypt ap_rep.enc_part: {:?}", err), + ) + })?; + + let ap_rep_enc_part: EncApRepPart = picky_asn1_der::from_bytes(&res)?; + + Ok(ap_rep_enc_part + .0 + .subkey + .0 + .ok_or_else(|| Error::new(ErrorKind::InvalidToken, "missing sub-key in ap_req"))? + .0 + .key_value + .0 + .0) +} + +/// Extracts TGT Ticket from encoded [NegTokenTarg1]. +/// +/// Returned OID means the selected authentication mechanism by the target server. More info: +/// * [3.2.1. Syntax](https://datatracker.ietf.org/doc/html/rfc2478#section-3.2.1): `responseToken` field; +/// +/// We use this oid to choose between the regular Kerberos 5 and Kerberos 5 User-to-User authentication. +#[instrument(level = "trace", ret)] +pub fn extract_tgt_ticket_with_oid(data: &[u8]) -> Result> { + if data.is_empty() { + return Ok(None); + } + + let neg_token_targ: NegTokenTarg1 = picky_asn1_der::from_bytes(data)?; + + if let Some(resp_token) = neg_token_targ.0.response_token.0.as_ref().map(|ticket| &ticket.0 .0) { + let mut c = resp_token.as_slice(); + + let oid: ApplicationTag = picky_asn1_der::from_reader(&mut c)?; + let oid: ObjectIdentifierAsn1 = picky_asn1_der::from_bytes(&oid.0 .0)?; + + let mut t = [0, 0]; + + c.read_exact(&mut t)?; + + let tgt_rep: TgtRep = picky_asn1_der::from_reader(&mut c)?; + + Ok(Some((tgt_rep.ticket.0, oid))) + } else { + Ok(None) + } +} diff --git a/src/kerberos/client/generators.rs b/src/kerberos/client/generators.rs index 30270cde..25aa5617 100644 --- a/src/kerberos/client/generators.rs +++ b/src/kerberos/client/generators.rs @@ -54,16 +54,22 @@ use crate::{ClientRequestFlags, Error, ErrorKind, Result}; const TGT_TICKET_LIFETIME_DAYS: i64 = 3; const NONCE_LEN: usize = 4; -pub const MAX_MICROSECONDS_IN_SECOND: u32 = 999_999; +/// [Microseconds](https://www.rfc-editor.org/rfc/rfc4120#section-5.2.4). +/// The maximum microseconds value. +/// +/// ```not_rust +/// Microseconds ::= INTEGER (0..999999) +/// ``` +pub const MAX_MICROSECONDS: u32 = 999_999; const MD5_CHECKSUM_TYPE: [u8; 1] = [0x07]; // Renewable, Canonicalize, and Renewable-ok are on by default // https://www.rfc-editor.org/rfc/rfc4120#section-5.4.1 pub const DEFAULT_AS_REQ_OPTIONS: [u8; 4] = [0x00, 0x81, 0x00, 0x10]; -// Renewable, Canonicalize, Enc-tkt-in-skey are on by default +// Renewable, Canonicalize. // https://www.rfc-editor.org/rfc/rfc4120#section-5.4.1 -const DEFAULT_TGS_REQ_OPTIONS: [u8; 4] = [0x00, 0x81, 0x00, 0x08]; +const DEFAULT_TGS_REQ_OPTIONS: [u8; 4] = [0x00, 0x81, 0x00, 0x00]; const DEFAULT_PA_PAC_OPTIONS: [u8; 4] = [0x40, 0x00, 0x00, 0x00]; @@ -76,8 +82,8 @@ pub const AUTHENTICATOR_DEFAULT_CHECKSUM: [u8; 24] = [ 0x00, 0x00, 0x00, 0x00, 0x00, ]; -// [MS-KILE] 3.3.5.6.1 Client Principal Lookup -// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-kile/6435d3fb-8cf6-4df5-a156-1277690ed59c +/// [MS-KILE] 3.3.5.6.1 Client Principal Lookup +/// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-kile/6435d3fb-8cf6-4df5-a156-1277690ed59c pub fn get_client_principal_name_type(username: &str, _domain: &str) -> u8 { if username.contains('@') { NT_ENTERPRISE @@ -136,11 +142,16 @@ fn matches_domain(domain: &str, mapping_domain: &str) -> bool { } } +/// Parameters for generating pa-datas for [AsReq] message. #[derive(Debug)] pub struct GenerateAsPaDataOptions<'a> { pub password: &'a str, + /// Salt for deriving the encryption key. + /// + /// The salt value should be extracted from the [KrbError] message. pub salt: Vec, pub enc_params: EncryptionParams, + /// Flag that indicates whether to generate pa-datas. pub with_pre_auth: bool, } @@ -155,7 +166,7 @@ pub fn generate_pa_datas_for_as_req(options: &GenerateAsPaDataOptions) -> Result let mut pa_datas = if *with_pre_auth { let current_date = OffsetDateTime::now_utc(); - let microseconds = current_date.microsecond().min(MAX_MICROSECONDS_IN_SECOND); + let microseconds = current_date.microsecond().min(MAX_MICROSECONDS); let timestamp = PaEncTsEnc { patimestamp: ExplicitContextTag0::from(KerberosTime::from(GeneralizedTime::from(current_date))), @@ -203,6 +214,7 @@ pub fn generate_pa_datas_for_as_req(options: &GenerateAsPaDataOptions) -> Result Ok(pa_datas) } +/// Parameters for generating [AsReq]. #[derive(Debug)] pub struct GenerateAsReqOptions<'a> { pub realm: &'a str, @@ -288,13 +300,18 @@ pub fn generate_as_req(pa_datas: Vec, kdc_req_body: KdcReqBody) -> AsReq }) } +/// Parameters for generating [TgsReq]. #[derive(Debug)] pub struct GenerateTgsReqOptions<'a> { pub realm: &'a str, pub service_principal: &'a str, pub session_key: &'a [u8], + /// [Ticket] extracted from the [AsRep] message. pub ticket: Ticket, + /// [Authenticator] to be included in [TgsReq] pa-data. pub authenticator: &'a mut Authenticator, + /// If the Kerberos U2U auth is negotiated, then this parameter must have one ticket: TGT ticket of the application service. + /// Otherwise, set it to `None`. pub additional_tickets: Option>, pub enc_params: &'a EncryptionParams, pub context_requirements: ClientRequestFlags, @@ -323,6 +340,9 @@ pub fn generate_tgs_req(options: GenerateTgsReqOptions) -> Result { if context_requirements.contains(ClientRequestFlags::DELEGATE) { tgs_req_options |= KdcOptions::FORWARDABLE; } + if context_requirements.contains(ClientRequestFlags::USE_SESSION_KEY) { + tgs_req_options |= KdcOptions::ENC_TKT_IN_SKEY; + } let req_body = KdcReqBody { kdc_options: ExplicitContextTag0::from(KerberosFlags::from(BitString::with_bytes( @@ -519,22 +539,33 @@ pub struct AuthenticatorChecksumExtension { pub extension_value: Vec, } +/// Encryption key. #[derive(Debug)] pub struct EncKey { + /// Encryption type. pub key_type: CipherSuite, + /// Encryption key value. pub key_value: Vec, } +/// Input parameters for generating ApReq Authenticator. #[derive(Debug)] pub struct GenerateAuthenticatorOptions<'a> { + /// [KdcRep] from previous interaction with KDC. pub kdc_rep: &'a KdcRep, + /// Sequence number. pub seq_num: Option, + /// Sub-session encryption key. pub sub_key: Option, + /// Authenticator checksum options. pub checksum: Option, + /// Channel bindings. pub channel_bindings: Option<&'a ChannelBindings>, + /// Possible authenticator extensions. pub extensions: Vec, } +/// Generated ApReq Authenticator. #[instrument(level = "trace", ret)] pub fn generate_authenticator(options: GenerateAuthenticatorOptions) -> Result { let GenerateAuthenticatorOptions { @@ -548,8 +579,8 @@ pub fn generate_authenticator(options: GenerateAuthenticatorOptions) -> Result MAX_MICROSECONDS_IN_SECOND { - microseconds = MAX_MICROSECONDS_IN_SECOND; + if microseconds > MAX_MICROSECONDS { + microseconds = MAX_MICROSECONDS; } let authorization_data = Optional::from(channel_bindings.as_ref().map(|_| { @@ -611,7 +642,7 @@ pub fn generate_authenticator(options: GenerateAuthenticatorOptions) -> Result, enc_params: &EncryptionParams) -> Result { let current_date = OffsetDateTime::now_utc(); - let microseconds = current_date.microsecond().min(MAX_MICROSECONDS_IN_SECOND); + let microseconds = current_date.microsecond().min(MAX_MICROSECONDS); let encryption_type = enc_params.encryption_type.as_ref().unwrap_or(&DEFAULT_ENCRYPTION_TYPE); @@ -716,36 +747,48 @@ pub fn generate_ap_req( })) } -// returns supported authentication types +/// Returns supported authentication types. pub fn get_mech_list() -> MechTypeList { MechTypeList::from(vec![MechType::from(oids::ms_krb5()), MechType::from(oids::krb5())]) } -pub fn generate_neg_token_init(username: &str, service_name: &str) -> Result> { - let krb5_neg_token_init = ApplicationTag0(KrbMessage { - krb5_oid: ObjectIdentifierAsn1::from(oids::krb5_user_to_user()), - krb5_token_id: TGT_REQ_TOKEN_ID, - krb_msg: TgtReq { - pvno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])), - msg_type: ExplicitContextTag1::from(IntegerAsn1::from(vec![TGT_REQ_MSG_TYPE])), - server_name: ExplicitContextTag2::from(PrincipalName { - name_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![NT_SRV_INST])), - name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(vec![ - KerberosStringAsn1::from(IA5String::from_string(service_name.into())?), - KerberosStringAsn1::from(IA5String::from_string(username.into())?), - ])), - }), - }, - }); +/// Generates the initial SPNEGO token. +/// +/// The `sname` parameter is optional. If it is present, then the Kerberos U2U is in use, and `TgtReq` will be generated +/// for the input `sname` and placed in the `mech_token` field. +pub fn generate_neg_token_init(sname: Option<&[&str]>) -> Result> { + let mech_token = if let Some(sname) = sname { + let sname = sname + .iter() + .map(|sname| Ok(KerberosStringAsn1::from(IA5String::from_string(sname.to_string())?))) + .collect::>>()?; + + let krb5_neg_token_init = ApplicationTag0(KrbMessage { + krb5_oid: ObjectIdentifierAsn1::from(oids::krb5_user_to_user()), + krb5_token_id: TGT_REQ_TOKEN_ID, + krb_msg: TgtReq { + pvno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])), + msg_type: ExplicitContextTag1::from(IntegerAsn1::from(vec![TGT_REQ_MSG_TYPE])), + server_name: ExplicitContextTag2::from(PrincipalName { + name_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![NT_SRV_INST])), + name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(sname)), + }), + }, + }); + + Some(ExplicitContextTag2::from(OctetStringAsn1::from( + picky_asn1_der::to_vec(&krb5_neg_token_init)?, + ))) + } else { + None + }; Ok(ApplicationTag0(GssApiNegInit { oid: ObjectIdentifierAsn1::from(oids::spnego()), neg_token_init: ExplicitContextTag0::from(NegTokenInit { mech_types: Optional::from(Some(ExplicitContextTag0::from(get_mech_list()))), req_flags: Optional::from(None), - mech_token: Optional::from(Some(ExplicitContextTag2::from(OctetStringAsn1::from( - picky_asn1_der::to_vec(&krb5_neg_token_init)?, - )))), + mech_token: Optional::from(mech_token), mech_list_mic: Optional::from(None), }), })) diff --git a/src/kerberos/client/mod.rs b/src/kerberos/client/mod.rs index 68b55bf6..1e369cf5 100644 --- a/src/kerberos/client/mod.rs +++ b/src/kerberos/client/mod.rs @@ -1,2 +1,437 @@ +mod as_exchange; +mod change_password; pub mod extractors; pub mod generators; + +use std::io::Write; + +pub use as_exchange::as_exchange; +pub use change_password::change_password; +use picky::key::PrivateKey; +use picky_asn1_x509::oids; +use picky_krb::constants::gss_api::AUTHENTICATOR_CHECKSUM_TYPE; +use picky_krb::constants::key_usages::ACCEPTOR_SIGN; +use picky_krb::crypto::CipherSuite; +use picky_krb::data_types::{KrbResult, ResultExt}; +use picky_krb::gss_api::NegTokenTarg1; +use picky_krb::messages::TgsRep; +use rand::rngs::OsRng; +use rand::Rng; +use rsa::{Pkcs1v15Sign, RsaPrivateKey}; +use sha1::{Digest, Sha1}; + +use self::extractors::{ + extract_ap_rep_from_neg_token_targ, extract_encryption_params_from_as_rep, extract_seq_number_from_ap_rep, + extract_session_key_from_tgs_rep, extract_sub_session_key_from_ap_rep, extract_tgt_ticket_with_oid, +}; +use self::generators::{ + generate_ap_rep, generate_ap_req, generate_as_req_kdc_body, generate_authenticator, generate_neg_ap_req, + generate_neg_token_init, generate_tgs_req, get_client_principal_name_type, get_client_principal_realm, + get_mech_list, ChecksumOptions, ChecksumValues, EncKey, GenerateAsPaDataOptions, GenerateAsReqOptions, + GenerateAuthenticatorOptions, GenerateTgsReqOptions, GssFlags, +}; +use crate::channel_bindings::ChannelBindings; +use crate::generator::YieldPointLocal; +use crate::kerberos::pa_datas::{AsRepSessionKeyExtractor, AsReqPaDataOptions}; +use crate::kerberos::utils::{serialize_message, unwrap_hostname, validate_mic_token}; +use crate::kerberos::{DEFAULT_ENCRYPTION_TYPE, EC, TGT_SERVICE_NAME}; +use crate::pku2u::generate_client_dh_parameters; +use crate::utils::{generate_random_symmetric_key, parse_target_name, utf16_bytes_to_utf8_string}; +use crate::{ + pk_init, BufferType, ClientRequestFlags, ClientResponseFlags, CredentialsBuffers, Error, ErrorKind, + InitializeSecurityContextResult, Kerberos, KerberosState, Result, SecurityBuffer, SecurityStatus, SspiImpl, +}; + +/// Indicated that the MIC token `SentByAcceptor` flag must be enabled in the incoming MIC token. +const SENT_BY_ACCEPTOR: u8 = 1; + +/// Performs one authentication step. +/// +/// The user should call this function until it returns `SecurityStatus::Ok`. +pub async fn initialize_security_context<'a>( + client: &'a mut Kerberos, + yield_point: &mut YieldPointLocal, + builder: &'a mut crate::builders::FilledInitializeSecurityContext<'_, ::CredentialsHandle>, +) -> Result { + trace!(?builder); + + let status = match client.state { + KerberosState::Negotiate => { + let sname = if builder + .context_requirements + .contains(ClientRequestFlags::USE_SESSION_KEY) + { + let (service_name, service_principal_name) = + parse_target_name(builder.target_name.ok_or_else(|| { + Error::new( + ErrorKind::NoCredentials, + "Service target name (service principal name) is not provided", + ) + })?)?; + + Some([service_name, service_principal_name]) + } else { + None + }; + + debug!(?sname); + + let encoded_neg_token_init = + picky_asn1_der::to_vec(&generate_neg_token_init(sname.as_ref().map(|sname| sname.as_slice()))?)?; + + let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?; + output_token.buffer.write_all(&encoded_neg_token_init)?; + + client.state = KerberosState::Preauthentication; + + SecurityStatus::ContinueNeeded + } + KerberosState::Preauthentication => { + let input = builder + .input + .as_ref() + .ok_or_else(|| crate::Error::new(ErrorKind::InvalidToken, "input buffers must be specified"))?; + + if let Ok(sec_buffer) = + SecurityBuffer::find_buffer(builder.input.as_ref().unwrap(), BufferType::ChannelBindings) + { + client.channel_bindings = Some(ChannelBindings::from_bytes(&sec_buffer.buffer)?); + } + + let input_token = SecurityBuffer::find_buffer(input, BufferType::Token)?; + + let (tgt_ticket, mech_id) = + if let Some((tbt_ticket, mech_oid)) = extract_tgt_ticket_with_oid(&input_token.buffer)? { + (Some(tbt_ticket), mech_oid.0) + } else { + (None, oids::krb5()) + }; + client.krb5_user_to_user = mech_id == oids::krb5_user_to_user(); + + let credentials = builder + .credentials_handle + .as_ref() + .unwrap() + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::WrongCredentialHandle, "No credentials provided"))?; + + let (username, password, realm, cname_type) = match credentials { + CredentialsBuffers::AuthIdentity(auth_identity) => { + let username = utf16_bytes_to_utf8_string(&auth_identity.user); + let domain = utf16_bytes_to_utf8_string(&auth_identity.domain); + let password = utf16_bytes_to_utf8_string(auth_identity.password.as_ref()); + + let realm = get_client_principal_realm(&username, &domain); + let cname_type = get_client_principal_name_type(&username, &domain); + + (username, password, realm, cname_type) + } + CredentialsBuffers::SmartCard(smart_card) => { + let username = utf16_bytes_to_utf8_string(&smart_card.username); + let password = utf16_bytes_to_utf8_string(smart_card.pin.as_ref()); + + let realm = get_client_principal_realm(&username, ""); + let cname_type = get_client_principal_name_type(&username, ""); + + (username, password, realm.to_uppercase(), cname_type) + } + }; + client.realm = Some(realm.clone()); + + let options = GenerateAsReqOptions { + realm: &realm, + username: &username, + cname_type, + snames: &[TGT_SERVICE_NAME, &realm], + // 4 = size of u32 + nonce: &OsRng.gen::<[u8; 4]>(), + hostname: &unwrap_hostname(client.config.client_computer_name.as_deref())?, + context_requirements: builder.context_requirements, + }; + let kdc_req_body = generate_as_req_kdc_body(&options)?; + + let pa_data_options = match credentials { + CredentialsBuffers::AuthIdentity(auth_identity) => { + let domain = utf16_bytes_to_utf8_string(&auth_identity.domain); + let salt = format!("{}{}", domain, username); + + AsReqPaDataOptions::AuthIdentity(GenerateAsPaDataOptions { + password: &password, + salt: salt.as_bytes().to_vec(), + enc_params: client.encryption_params.clone(), + with_pre_auth: false, + }) + } + CredentialsBuffers::SmartCard(smart_card) => { + let private_key_pem = utf16_bytes_to_utf8_string( + smart_card + .private_key_pem + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::InternalError, "scard private key is missing"))?, + ); + client.dh_parameters = Some(generate_client_dh_parameters(&mut OsRng)?); + + AsReqPaDataOptions::SmartCard(Box::new(pk_init::GenerateAsPaDataOptions { + p2p_cert: picky_asn1_der::from_bytes(&smart_card.certificate)?, + kdc_req_body: &kdc_req_body, + dh_parameters: client.dh_parameters.clone().unwrap(), + sign_data: Box::new(move |data_to_sign| { + let mut sha1 = Sha1::new(); + sha1.update(data_to_sign); + let hash = sha1.finalize().to_vec(); + let private_key = PrivateKey::from_pem_str(&private_key_pem)?; + let rsa_private_key = RsaPrivateKey::try_from(&private_key)?; + Ok(rsa_private_key.sign(Pkcs1v15Sign::new::(), &hash)?) + }), + with_pre_auth: false, + authenticator_nonce: OsRng.gen::<[u8; 4]>(), + })) + } + }; + + let as_rep = as_exchange(client, yield_point, &kdc_req_body, pa_data_options).await?; + + debug!("AS exchange finished successfully."); + + client.realm = Some(as_rep.0.crealm.0.to_string()); + + let (encryption_type, salt) = extract_encryption_params_from_as_rep(&as_rep)?; + + let encryption_type = CipherSuite::try_from(encryption_type as usize)?; + + client.encryption_params.encryption_type = Some(encryption_type); + + let mut authenticator = generate_authenticator(GenerateAuthenticatorOptions { + kdc_rep: &as_rep.0, + seq_num: Some(OsRng.gen::()), + sub_key: None, + checksum: None, + channel_bindings: client.channel_bindings.as_ref(), + extensions: Vec::new(), + })?; + + let mut session_key_extractor = match credentials { + CredentialsBuffers::AuthIdentity(_) => AsRepSessionKeyExtractor::AuthIdentity { + salt: &salt, + password: &password, + enc_params: &mut client.encryption_params, + }, + CredentialsBuffers::SmartCard(_) => AsRepSessionKeyExtractor::SmartCard { + dh_parameters: client.dh_parameters.as_mut().unwrap(), + enc_params: &mut client.encryption_params, + }, + }; + let session_key_1 = session_key_extractor.session_key(&as_rep)?; + + let service_principal = builder.target_name.ok_or_else(|| { + Error::new( + ErrorKind::NoCredentials, + "Service target name (service principal name) is not provided", + ) + })?; + + let mut context_requirements = builder.context_requirements; + + if client.krb5_user_to_user && !context_requirements.contains(ClientRequestFlags::USE_SESSION_KEY) { + warn!("KRB5 U2U has been negotiated (selected by the server) but the USE_SESSION_KEY flag is not set. Forcibly turning it on..."); + context_requirements.set(ClientRequestFlags::USE_SESSION_KEY, true); + } + + let tgs_req = generate_tgs_req(GenerateTgsReqOptions { + realm: &as_rep.0.crealm.0.to_string(), + service_principal, + session_key: &session_key_1, + ticket: as_rep.0.ticket.0, + authenticator: &mut authenticator, + additional_tickets: tgt_ticket.map(|ticket| vec![ticket]), + enc_params: &client.encryption_params, + context_requirements, + })?; + + let response = client.send(yield_point, &serialize_message(&tgs_req)?).await?; + + if response.len() < 4 { + return Err(Error::new( + ErrorKind::InternalError, + "the KDC reply message is too small: expected at least 4 bytes", + )); + } + + // first 4 bytes are message len. skipping them + let mut d = picky_asn1_der::Deserializer::new_from_bytes(&response[4..]); + let tgs_rep: KrbResult = KrbResult::deserialize(&mut d)?; + let tgs_rep = tgs_rep?; + + debug!("TGS exchange finished successfully"); + + let session_key_2 = extract_session_key_from_tgs_rep(&tgs_rep, &session_key_1, &client.encryption_params)?; + + client.encryption_params.session_key = Some(session_key_2); + + let enc_type = client + .encryption_params + .encryption_type + .as_ref() + .unwrap_or(&DEFAULT_ENCRYPTION_TYPE); + let authenticator_sub_key = generate_random_symmetric_key(enc_type, &mut OsRng); + + // the original flag is + // GSS_C_MUTUAL_FLAG | GSS_C_REPLAY_FLAG | GSS_C_SEQUENCE_FLAG | GSS_C_CONF_FLAG | GSS_C_INTEG_FLAG + // we want to be able to turn of sign and seal, so we leave confidentiality and integrity flags out + let mut flags: GssFlags = builder.context_requirements.into(); + if flags.contains(GssFlags::GSS_C_DELEG_FLAG) { + // Below are reasons why we turn off the GSS_C_DELEG_FLAG flag. + // + // RFC4121: The Kerberos Version 5 GSS-API. Section 4.1.1: Authenticator Checksum + // https://datatracker.ietf.org/doc/html/rfc4121#section-4.1.1.1 + // + // "The length of the checksum field MUST be at least 24 octets when GSS_C_DELEG_FLAG is not set, + // and at least 28 octets plus Dlgth octets when GSS_C_DELEG_FLAG is set." + // Out implementation _always_ uses the 24 octets checksum and do not support Kerberos credentials delegation. + // + // "When delegation is used, a ticket-granting ticket will be transferred in a KRB_CRED message." + // We do not support KRB_CRED messages. So, the GSS_C_DELEG_FLAG flags should be turned off. + warn!("Kerberos ApReq Authenticator checksum GSS_C_DELEG_FLAG is not supported. Turning it off..."); + flags.remove(GssFlags::GSS_C_DELEG_FLAG); + } + debug!(?flags, "ApReq Authenticator checksum flags"); + + let mut checksum_value = ChecksumValues::default(); + checksum_value.set_flags(flags); + + let authenticator_options = GenerateAuthenticatorOptions { + kdc_rep: &tgs_rep.0, + // The AP_REQ Authenticator sequence number should be the same as `seq_num` in the first Kerberos Wrap token generated + // by the `encrypt_message` method. So, we set the next sequence number but do not increment the counter, + // which will be incremented on each `encrypt_message` method call. + seq_num: Some(client.seq_number + 1), + sub_key: Some(EncKey { + key_type: enc_type.clone(), + key_value: authenticator_sub_key, + }), + + checksum: Some(ChecksumOptions { + checksum_type: AUTHENTICATOR_CHECKSUM_TYPE.to_vec(), + checksum_value, + }), + channel_bindings: client.channel_bindings.as_ref(), + extensions: Vec::new(), + }; + + let authenticator = generate_authenticator(authenticator_options)?; + let encoded_auth = picky_asn1_der::to_vec(&authenticator)?; + debug!(encoded_ap_req_authenticator = ?encoded_auth); + + let ap_req = generate_ap_req( + tgs_rep.0.ticket.0, + client + .encryption_params + .session_key + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::InternalError, "session key is not set"))?, + &authenticator, + &client.encryption_params, + context_requirements.into(), + )?; + + let encoded_neg_ap_req = if !builder.context_requirements.contains(ClientRequestFlags::USE_DCE_STYLE) { + // Wrap in a NegToken. + picky_asn1_der::to_vec(&generate_neg_ap_req(ap_req, mech_id)?)? + } else { + // Do not wrap if the `USE_DCE_STYLE` flag is set. + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-kile/190ab8de-dc42-49cf-bf1b-ea5705b7a087 + picky_asn1_der::to_vec(&ap_req)? + }; + + let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?; + output_token.buffer.write_all(&encoded_neg_ap_req)?; + + client.state = KerberosState::ApExchange; + + SecurityStatus::ContinueNeeded + } + KerberosState::ApExchange => { + let input = builder + .input + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::InvalidToken, "Input buffers must be specified"))?; + let input_token = SecurityBuffer::find_buffer(input, BufferType::Token)?; + + if builder.context_requirements.contains(ClientRequestFlags::USE_DCE_STYLE) { + // The `EC` field depends on the authentication type. For example, during RDP auth + // it is equal to 0, but during RPC auth it is equal to EC. + client.encryption_params.ec = EC; + + use picky_krb::messages::ApRep; + + let ap_rep: ApRep = picky_asn1_der::from_bytes(&input_token.buffer)?; + + let session_key = client + .encryption_params + .session_key + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::InternalError, "session key is not set"))?; + let sub_session_key = + extract_sub_session_key_from_ap_rep(&ap_rep, session_key, &client.encryption_params)?; + let seq_number = extract_seq_number_from_ap_rep(&ap_rep, session_key, &client.encryption_params)?; + + trace!(?sub_session_key, "DCE AP_REP sub-session key"); + + client.encryption_params.sub_session_key = Some(sub_session_key); + + let ap_rep = generate_ap_rep(session_key, seq_number, &client.encryption_params)?; + let ap_rep = picky_asn1_der::to_vec(&ap_rep)?; + + let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?; + output_token.buffer.write_all(&ap_rep)?; + } else { + let neg_token_targ = { + let mut d = picky_asn1_der::Deserializer::new_from_bytes(&input_token.buffer); + let neg_token_targ: NegTokenTarg1 = KrbResult::deserialize(&mut d)??; + neg_token_targ + }; + + let ap_rep = extract_ap_rep_from_neg_token_targ(&neg_token_targ)?; + + let session_key = client + .encryption_params + .session_key + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::InternalError, "session key is not set"))?; + let sub_session_key = + extract_sub_session_key_from_ap_rep(&ap_rep, session_key, &client.encryption_params)?; + + client.encryption_params.sub_session_key = Some(sub_session_key); + + if let Some(ref token) = neg_token_targ.0.mech_list_mic.0 { + validate_mic_token::( + &token.0 .0, + ACCEPTOR_SIGN, + &client.encryption_params, + &get_mech_list(), + )?; + } + + client.next_seq_number(); + client.prepare_final_neg_token(builder)?; + } + + client.state = KerberosState::PubKeyAuth; + SecurityStatus::Ok + } + _ => { + return Err(Error::new( + ErrorKind::OutOfSequence, + format!("got wrong Kerberos state: {:?}", client.state), + )) + } + }; + + trace!(output_buffers = ?builder.output); + + Ok(InitializeSecurityContextResult { + status, + flags: ClientResponseFlags::empty(), + expiry: None, + }) +} diff --git a/src/kerberos/config.rs b/src/kerberos/config.rs index 33c409a6..86fb743e 100644 --- a/src/kerberos/config.rs +++ b/src/kerberos/config.rs @@ -4,9 +4,11 @@ use std::str::FromStr; use url::Url; use crate::kdc::detect_kdc_url; +use crate::kerberos::ServerProperties; use crate::negotiate::{NegotiatedProtocol, ProtocolConfig}; use crate::{Kerberos, Result}; +/// Kerberos client configuration. #[derive(Clone, Debug)] pub struct KerberosConfig { /// KDC URL @@ -29,14 +31,14 @@ pub struct KerberosConfig { } impl ProtocolConfig for KerberosConfig { - fn new_client(&self) -> Result { + fn new_instance(&self) -> Result { Ok(NegotiatedProtocol::Kerberos(Kerberos::new_client_from_config( - Clone::clone(self), + self.clone(), )?)) } fn box_clone(&self) -> Box { - Box::new(Clone::clone(self)) + Box::new(self.clone()) } } @@ -75,3 +77,25 @@ impl KerberosConfig { } } } + +/// Kerberos server configuration. +#[derive(Clone, Debug)] +pub struct KerberosServerConfig { + /// General Kerberos configuration. + pub kerberos_config: KerberosConfig, + /// Kerberos server specific parameters. + pub server_properties: ServerProperties, +} + +impl ProtocolConfig for KerberosServerConfig { + fn new_instance(&self) -> Result { + Ok(NegotiatedProtocol::Kerberos(Kerberos::new_server_from_config( + self.kerberos_config.clone(), + self.server_properties.clone(), + )?)) + } + + fn box_clone(&self) -> Box { + Box::new(self.clone()) + } +} diff --git a/src/kerberos/mod.rs b/src/kerberos/mod.rs index 637c897b..dae14487 100644 --- a/src/kerberos/mod.rs +++ b/src/kerberos/mod.rs @@ -4,68 +4,44 @@ mod encryption_params; pub mod flags; mod pa_datas; pub mod server; +#[cfg(test)] +mod tests; mod utils; use std::fmt::Debug; use std::io::Write; use std::sync::LazyLock; -pub use encryption_params::EncryptionParams; -use picky::key::PrivateKey; use picky_asn1::restricted_string::IA5String; use picky_asn1::wrapper::{ExplicitContextTag0, ExplicitContextTag1, OctetStringAsn1, Optional}; -use picky_asn1_x509::oids; -use picky_krb::constants::gss_api::AUTHENTICATOR_CHECKSUM_TYPE; -use picky_krb::constants::key_usages::ACCEPTOR_SIGN; use picky_krb::crypto::{CipherSuite, DecryptWithoutChecksum, EncryptWithoutChecksum}; -use picky_krb::data_types::{KerberosStringAsn1, KrbResult, ResultExt}; -use picky_krb::gss_api::{NegTokenTarg1, WrapToken}; -use picky_krb::messages::{ApReq, AsRep, KdcProxyMessage, KdcReqBody, KrbPrivMessage, TgsRep}; +use picky_krb::data_types::KerberosStringAsn1; +use picky_krb::gss_api::WrapToken; +use picky_krb::messages::KdcProxyMessage; use rand::rngs::OsRng; use rand::Rng; -use rsa::{Pkcs1v15Sign, RsaPrivateKey}; -use sha1::{Digest, Sha1}; use url::Url; -use self::client::extractors::{ - extract_encryption_params_from_as_rep, extract_session_key_from_as_rep, extract_session_key_from_tgs_rep, -}; -use self::client::generators::{ - generate_ap_req, generate_as_req, generate_as_req_kdc_body, generate_krb_priv_request, generate_neg_ap_req, - generate_neg_token_init, generate_pa_datas_for_as_req, generate_tgs_req, get_client_principal_name_type, - get_client_principal_realm, ChecksumOptions, ChecksumValues, EncKey, GenerateAsPaDataOptions, GenerateAsReqOptions, - GenerateAuthenticatorOptions, -}; +pub use self::client::initialize_security_context; use self::config::KerberosConfig; -use self::pa_datas::AsReqPaDataOptions; -use self::server::extractors::extract_tgt_ticket_with_oid; -use self::utils::{serialize_message, unwrap_hostname}; +pub use self::encryption_params::EncryptionParams; +pub use self::server::{accept_security_context, ServerProperties}; use super::channel_bindings::ChannelBindings; use crate::builders::ChangePassword; -use crate::generator::{GeneratorChangePassword, GeneratorInitSecurityContext, NetworkRequest, YieldPointLocal}; -use crate::kerberos::client::extractors::{extract_salt_from_krb_error, extract_status_code_from_krb_priv_response}; -use crate::kerberos::client::generators::{ - generate_ap_rep, generate_authenticator, generate_final_neg_token_targ, get_mech_list, GenerateTgsReqOptions, - GssFlags, -}; -use crate::kerberos::pa_datas::AsRepSessionKeyExtractor; -use crate::kerberos::server::extractors::{ - extract_ap_rep_from_neg_token_targ, extract_seq_number_from_ap_rep, extract_sub_session_key_from_ap_rep, +use crate::generator::{ + GeneratorAcceptSecurityContext, GeneratorChangePassword, GeneratorInitSecurityContext, NetworkRequest, + YieldPointLocal, }; -use crate::kerberos::utils::{generate_initiator_raw, validate_mic_token}; +use crate::kerberos::client::generators::{generate_final_neg_token_targ, get_mech_list}; +use crate::kerberos::utils::generate_initiator_raw; use crate::network_client::NetworkProtocol; -use crate::pk_init::{self, DhParameters}; -use crate::pku2u::generate_client_dh_parameters; -use crate::utils::{ - extract_encrypted_data, generate_random_symmetric_key, get_encryption_key, parse_target_name, save_decrypted_data, - utf16_bytes_to_utf8_string, -}; +use crate::pk_init::DhParameters; +use crate::utils::{extract_encrypted_data, get_encryption_key, save_decrypted_data, utf16_bytes_to_utf8_string}; use crate::{ - check_if_empty, detect_kdc_url, AcceptSecurityContextResult, AcquireCredentialsHandleResult, AuthIdentity, - BufferType, ClientRequestFlags, ClientResponseFlags, ContextNames, ContextSizes, CredentialUse, Credentials, - CredentialsBuffers, DecryptionFlags, Error, ErrorKind, InitializeSecurityContextResult, PackageCapabilities, - PackageInfo, Result, SecurityBuffer, SecurityBufferFlags, SecurityBufferRef, SecurityPackageType, SecurityStatus, - ServerResponseFlags, SessionKeys, Sspi, SspiEx, SspiImpl, PACKAGE_ID_NONE, + detect_kdc_url, AcceptSecurityContextResult, AcquireCredentialsHandleResult, AuthIdentity, BufferType, + ContextNames, ContextSizes, CredentialUse, Credentials, CredentialsBuffers, DecryptionFlags, Error, ErrorKind, + PackageCapabilities, PackageInfo, Result, SecurityBuffer, SecurityBufferFlags, SecurityBufferRef, + SecurityPackageType, SecurityStatus, SessionKeys, Sspi, SspiEx, SspiImpl, PACKAGE_ID_NONE, }; pub const PKG_NAME: &str = "Kerberos"; @@ -87,14 +63,11 @@ pub const MAX_SIGNATURE: usize = 16; /// **Note**: Actual security trailer len is `SECURITY_TRAILER` + `EC`. The `EC` field is negotiated // during the authentication process. pub const SECURITY_TRAILER: usize = 60; -/// [Kerberos Change Password and Set Password Protocols](https://datatracker.ietf.org/doc/html/rfc3244#section-2) -/// "The service accepts requests on UDP port 464 and TCP port 464 as well." -const KPASSWD_PORT: u16 = 464; /// [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. -const EC: u16 = 16; +pub(crate) const EC: u16 = 16; pub static PACKAGE_INFO: LazyLock = LazyLock::new(|| PackageInfo { capabilities: PackageCapabilities::empty(), @@ -104,7 +77,7 @@ pub static PACKAGE_INFO: LazyLock = LazyLock::new(|| PackageInfo { comment: String::from("Kerberos Security Package"), }); -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub enum KerberosState { Negotiate, Preauthentication, @@ -116,16 +89,17 @@ pub enum KerberosState { #[derive(Debug, Clone)] pub struct Kerberos { - state: KerberosState, - config: KerberosConfig, - auth_identity: Option, - encryption_params: EncryptionParams, - seq_number: u32, - realm: Option, - kdc_url: Option, - channel_bindings: Option, - dh_parameters: Option, - krb5_user_to_user: bool, + pub(crate) state: KerberosState, + pub(crate) config: KerberosConfig, + pub(crate) auth_identity: Option, + pub(crate) encryption_params: EncryptionParams, + pub(crate) seq_number: u32, + pub(crate) realm: Option, + pub(crate) kdc_url: Option, + pub(crate) channel_bindings: Option, + pub(crate) dh_parameters: Option, + pub(crate) krb5_user_to_user: bool, + pub(crate) server: Option>, } impl Kerberos { @@ -143,10 +117,11 @@ impl Kerberos { channel_bindings: None, dh_parameters: None, krb5_user_to_user: false, + server: None, }) } - pub fn new_server_from_config(config: KerberosConfig) -> Result { + pub fn new_server_from_config(config: KerberosConfig, server_properties: ServerProperties) -> Result { let kdc_url = config.kdc_url.clone(); Ok(Self { @@ -160,6 +135,7 @@ impl Kerberos { channel_bindings: None, dh_parameters: None, krb5_user_to_user: false, + server: Some(Box::new(server_properties)), }) } @@ -264,58 +240,6 @@ impl Kerberos { output_token.buffer.write_all(&encoded_final_neg_token_targ)?; Ok(()) } - - pub async fn as_exchange( - &mut self, - yield_point: &mut YieldPointLocal, - kdc_req_body: &KdcReqBody, - mut pa_data_options: AsReqPaDataOptions<'_>, - ) -> Result { - pa_data_options.with_pre_auth(false); - let pa_datas = pa_data_options.generate()?; - let as_req = generate_as_req(pa_datas, kdc_req_body.clone()); - - let response = self.send(yield_point, &serialize_message(&as_req)?).await?; - - // first 4 bytes are message len. skipping them - { - let mut d = picky_asn1_der::Deserializer::new_from_bytes(&response[4..]); - let as_rep: KrbResult = KrbResult::deserialize(&mut d)?; - - if as_rep.is_ok() { - error!( - "KDC replied with AS_REP to the AS_REQ without the encrypted timestamp. The KRB_ERROR expected." - ); - - return Err(Error::new( - ErrorKind::InvalidToken, - "KDC server should not process AS_REQ without the pa-pac data", - )); - } - - if let Some(correct_salt) = extract_salt_from_krb_error(&as_rep.unwrap_err())? { - debug!("salt extracted successfully from the KRB_ERROR"); - - pa_data_options.with_salt(correct_salt.as_bytes().to_vec()); - } - } - - pa_data_options.with_pre_auth(true); - let pa_datas = pa_data_options.generate()?; - - let as_req = generate_as_req(pa_datas, kdc_req_body.clone()); - - let response = self.send(yield_point, &serialize_message(&as_req)?).await?; - - // first 4 bytes are message len. skipping them - let mut d = picky_asn1_der::Deserializer::new_from_bytes(&response[4..]); - let as_rep: KrbResult = KrbResult::deserialize(&mut d)?; - - as_rep.map_err(|err| { - error!(?err, "AS exchange error"); - err.into() - }) - } } impl Sspi for Kerberos { @@ -353,6 +277,19 @@ impl Sspi for Kerberos { 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| { @@ -426,10 +363,10 @@ impl Sspi for Kerberos { let token_buffer = SecurityBufferRef::find_buffer_mut(message, BufferType::Token)?; token_buffer.write_data(token)?; } - _ => { + KerberosState::Negotiate | KerberosState::Preauthentication | KerberosState::ApExchange => { return Err(Error::new( ErrorKind::OutOfSequence, - "Kerberos context is not established", + format!("Kerberos context is not established: current state: {:?}", self.state), )) } }; @@ -455,6 +392,33 @@ impl Sspi for Kerberos { 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. + if wrap_token.flags & 0b10 != 0b10 { + return Err(Error::new( + ErrorKind::InvalidToken, + "the Sealed flag has to be set in WRAP token", + )); + } 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): @@ -539,6 +503,12 @@ impl Sspi for Kerberos { #[instrument(level = "debug", ret, fields(state = ?self.state), skip(self))] fn query_context_names(&mut self) -> Result { + 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))?; @@ -547,14 +517,16 @@ impl Sspi for Kerberos { username: identity.username, }); } + if let Some(CredentialsBuffers::SmartCard(ref identity_buffers)) = self.auth_identity { let username = utf16_bytes_to_utf8_string(&identity_buffers.username); let username = crate::Username::parse(&username).map_err(|e| Error::new(ErrorKind::InvalidParameter, e))?; return Ok(ContextNames { username }); } - Err(crate::Error::new( - crate::ErrorKind::NoCredentials, - String::from("Requested Names, but no credentials were provided"), + + Err(Error::new( + ErrorKind::NoCredentials, + "requested names, but no credentials were provided", )) } @@ -567,7 +539,7 @@ impl Sspi for Kerberos { fn query_context_cert_trust_status(&mut self) -> Result { Err(Error::new( ErrorKind::UnsupportedFunction, - "Certificate trust status is not supported".to_owned(), + "certificate trust status is not supported".to_owned(), )) } @@ -580,7 +552,7 @@ impl Sspi for Kerberos { fn change_password<'a>(&'a mut self, change_password: ChangePassword<'a>) -> Result> { Ok(GeneratorChangePassword::new(move |mut yield_point| async move { - self.change_password(&mut yield_point, change_password).await + client::change_password(self, &mut yield_point, change_password).await })) } @@ -617,7 +589,7 @@ impl SspiImpl for Kerberos { if builder.credential_use == CredentialUse::Outbound && builder.auth_data.is_none() { return Err(Error::new( ErrorKind::NoCredentials, - String::from("The client must specify the auth data"), + "the client must specify the auth data", )); } @@ -634,38 +606,13 @@ impl SspiImpl for Kerberos { } #[instrument(level = "debug", ret, fields(state = ?self.state), skip(self, builder))] - fn accept_security_context_impl( - &mut self, - builder: crate::builders::FilledAcceptSecurityContext<'_, Self::CredentialsHandle>, - ) -> Result { - let input = builder - .input - .ok_or_else(|| crate::Error::new(ErrorKind::InvalidToken, "Input buffers must be specified"))?; - - let status = match &self.state { - KerberosState::ApExchange => { - let input_token = SecurityBuffer::find_buffer(input, BufferType::Token)?; - - let _ap_req: ApReq = picky_asn1_der::from_bytes(&input_token.buffer) - .map_err(|e| Error::new(ErrorKind::DecryptFailure, format!("{:?}", e)))?; - - self.state = KerberosState::Final; - - SecurityStatus::Ok - } - state => { - return Err(Error::new( - ErrorKind::OutOfSequence, - format!("Got wrong Kerberos state: {:?}", state), - )) - } - }; - - Ok(AcceptSecurityContextResult { - status, - flags: ServerResponseFlags::empty(), - expiry: None, - }) + fn accept_security_context_impl<'a>( + &'a mut self, + builder: crate::builders::FilledAcceptSecurityContext<'a, Self::CredentialsHandle>, + ) -> Result> { + Ok(GeneratorAcceptSecurityContext::new(move |mut yield_point| async move { + self.accept_security_context_impl(&mut yield_point, builder).await + })) } fn initialize_security_context_impl<'a>( @@ -685,114 +632,15 @@ impl<'a> Kerberos { yield_point: &mut YieldPointLocal, change_password: ChangePassword<'a>, ) -> Result<()> { - let username = &change_password.account_name; - let domain = &change_password.domain_name; - let password = &change_password.old_password; - - let salt = format!("{}{}", domain, username); - - let cname_type = get_client_principal_name_type(username, domain); - let realm = &get_client_principal_realm(username, domain); - let hostname = unwrap_hostname(self.config.client_computer_name.as_deref())?; - - let options = GenerateAsReqOptions { - realm, - username, - cname_type, - snames: &[KADMIN, CHANGE_PASSWORD_SERVICE_NAME], - // 4 = size of u32 - nonce: &OsRng.gen::().to_ne_bytes(), - hostname: &hostname, - context_requirements: ClientRequestFlags::empty(), - }; - let kdc_req_body = generate_as_req_kdc_body(&options)?; - - let pa_data_options = AsReqPaDataOptions::AuthIdentity(GenerateAsPaDataOptions { - password: password.as_ref(), - salt: salt.as_bytes().to_vec(), - enc_params: self.encryption_params.clone(), - with_pre_auth: false, - }); - - let as_rep = self.as_exchange(yield_point, &kdc_req_body, pa_data_options).await?; - - debug!("AS exchange finished successfully."); - - self.realm = Some(as_rep.0.crealm.0.to_string()); - - let (encryption_type, salt) = extract_encryption_params_from_as_rep(&as_rep)?; - debug!(?encryption_type, "Negotiated encryption type"); - - self.encryption_params.encryption_type = Some(CipherSuite::try_from(usize::from(encryption_type))?); - - let session_key = extract_session_key_from_as_rep(&as_rep, &salt, password.as_ref(), &self.encryption_params)?; - - let seq_num = self.next_seq_number(); - - let enc_type = self - .encryption_params - .encryption_type - .as_ref() - .unwrap_or(&DEFAULT_ENCRYPTION_TYPE); - let authenticator_seb_key = generate_random_symmetric_key(enc_type, &mut OsRng); - - let authenticator = generate_authenticator(GenerateAuthenticatorOptions { - kdc_rep: &as_rep.0, - seq_num: Some(seq_num), - sub_key: Some(EncKey { - key_type: enc_type.clone(), - key_value: authenticator_seb_key, - }), - checksum: None, - channel_bindings: self.channel_bindings.as_ref(), - extensions: Vec::new(), - })?; - - let krb_priv = generate_krb_priv_request( - as_rep.0.ticket.0, - &session_key, - change_password.new_password.as_ref().as_bytes(), - &authenticator, - &self.encryption_params, - seq_num, - &hostname, - )?; - - if let Some((_realm, mut kdc_url)) = self.get_kdc() { - kdc_url - .set_port(Some(KPASSWD_PORT)) - .map_err(|_| Error::new(ErrorKind::InvalidParameter, "Cannot set port for KDC URL"))?; - - let response = self.send(yield_point, &serialize_message(&krb_priv)?).await?; - trace!(?response, "Change password raw response"); - - let krb_priv_response = KrbPrivMessage::deserialize(&response[4..]).map_err(|err| { - Error::new( - ErrorKind::InvalidToken, - format!("Cannot deserialize krb_priv_response: {:?}", err), - ) - })?; - - let result_status = extract_status_code_from_krb_priv_response( - &krb_priv_response.krb_priv, - &authenticator.0.subkey.0.as_ref().unwrap().0.key_value.0 .0, - &self.encryption_params, - )?; - - if result_status != 0 { - return Err(Error::new( - ErrorKind::WrongCredentialHandle, - format!("unsuccessful krb result code: {}. expected 0", result_status), - )); - } - } else { - return Err(Error::new( - ErrorKind::NoAuthenticatingAuthority, - "No KDC server found!".to_owned(), - )); - } + client::change_password(self, yield_point, change_password).await + } - Ok(()) + pub(crate) async fn accept_security_context_impl( + &'a mut self, + yield_point: &mut YieldPointLocal, + builder: crate::builders::FilledAcceptSecurityContext<'a, ::CredentialsHandle>, + ) -> Result { + server::accept_security_context(self, yield_point, builder).await } pub(crate) async fn initialize_security_context_impl( @@ -800,381 +648,7 @@ impl<'a> Kerberos { yield_point: &mut YieldPointLocal, builder: &'a mut crate::builders::FilledInitializeSecurityContext<'_, ::CredentialsHandle>, ) -> Result { - trace!(?builder); - - let status = match self.state { - KerberosState::Negotiate => { - let (service_name, _service_principal_name) = - parse_target_name(builder.target_name.ok_or_else(|| { - Error::new( - ErrorKind::NoCredentials, - "Service target name (service principal name) is not provided", - ) - })?)?; - - let (username, service_name) = match check_if_empty!( - builder.credentials_handle.as_ref().unwrap().as_ref(), - "AuthIdentity is not provided" - ) { - CredentialsBuffers::AuthIdentity(auth_identity) => { - let username = utf16_bytes_to_utf8_string(&auth_identity.user); - let domain = utf16_bytes_to_utf8_string(&auth_identity.domain); - - (format!("{}.{}", username, domain.to_ascii_lowercase()), service_name) - } - CredentialsBuffers::SmartCard(_) => (_service_principal_name.into(), service_name), - }; - debug!(username, service_name); - - let encoded_neg_token_init = - picky_asn1_der::to_vec(&generate_neg_token_init(&username, service_name)?)?; - - let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?; - output_token.buffer.write_all(&encoded_neg_token_init)?; - - self.state = KerberosState::Preauthentication; - - SecurityStatus::ContinueNeeded - } - KerberosState::Preauthentication => { - let input = builder - .input - .as_ref() - .ok_or_else(|| crate::Error::new(ErrorKind::InvalidToken, "Input buffers must be specified"))?; - - if let Ok(sec_buffer) = - SecurityBuffer::find_buffer(builder.input.as_ref().unwrap(), BufferType::ChannelBindings) - { - self.channel_bindings = Some(ChannelBindings::from_bytes(&sec_buffer.buffer)?); - } - - let input_token = SecurityBuffer::find_buffer(input, BufferType::Token)?; - - let (tgt_ticket, mech_id) = - if let Some((tbt_ticket, mech_oid)) = extract_tgt_ticket_with_oid(&input_token.buffer)? { - (Some(tbt_ticket), mech_oid.0) - } else { - (None, oids::krb5()) - }; - self.krb5_user_to_user = mech_id == oids::krb5_user_to_user(); - - let credentials = builder - .credentials_handle - .as_ref() - .unwrap() - .as_ref() - .ok_or_else(|| Error::new(ErrorKind::WrongCredentialHandle, "No credentials provided"))?; - - let (username, password, realm, cname_type) = match credentials { - CredentialsBuffers::AuthIdentity(auth_identity) => { - let username = utf16_bytes_to_utf8_string(&auth_identity.user); - let domain = utf16_bytes_to_utf8_string(&auth_identity.domain); - let password = utf16_bytes_to_utf8_string(auth_identity.password.as_ref()); - - let realm = get_client_principal_realm(&username, &domain); - let cname_type = get_client_principal_name_type(&username, &domain); - - (username, password, realm, cname_type) - } - CredentialsBuffers::SmartCard(smart_card) => { - let username = utf16_bytes_to_utf8_string(&smart_card.username); - let password = utf16_bytes_to_utf8_string(smart_card.pin.as_ref()); - - let realm = get_client_principal_realm(&username, ""); - let cname_type = get_client_principal_name_type(&username, ""); - - (username, password, realm.to_uppercase(), cname_type) - } - }; - self.realm = Some(realm.clone()); - - let options = GenerateAsReqOptions { - realm: &realm, - username: &username, - cname_type, - snames: &[TGT_SERVICE_NAME, &realm], - // 4 = size of u32 - nonce: &OsRng.gen::<[u8; 4]>(), - hostname: &unwrap_hostname(self.config.client_computer_name.as_deref())?, - context_requirements: builder.context_requirements, - }; - let kdc_req_body = generate_as_req_kdc_body(&options)?; - - let pa_data_options = - match credentials { - CredentialsBuffers::AuthIdentity(auth_identity) => { - let domain = utf16_bytes_to_utf8_string(&auth_identity.domain); - let salt = format!("{}{}", domain, username); - - AsReqPaDataOptions::AuthIdentity(GenerateAsPaDataOptions { - password: &password, - salt: salt.as_bytes().to_vec(), - enc_params: self.encryption_params.clone(), - with_pre_auth: false, - }) - } - CredentialsBuffers::SmartCard(smart_card) => { - let private_key_pem = - utf16_bytes_to_utf8_string(smart_card.private_key_pem.as_ref().ok_or_else(|| { - Error::new(ErrorKind::InternalError, "scard private key is missing") - })?); - self.dh_parameters = Some(generate_client_dh_parameters(&mut OsRng)?); - - AsReqPaDataOptions::SmartCard(Box::new(pk_init::GenerateAsPaDataOptions { - p2p_cert: picky_asn1_der::from_bytes(&smart_card.certificate)?, - kdc_req_body: &kdc_req_body, - dh_parameters: self.dh_parameters.clone().unwrap(), - sign_data: Box::new(move |data_to_sign| { - let mut sha1 = Sha1::new(); - sha1.update(data_to_sign); - let hash = sha1.finalize().to_vec(); - let private_key = PrivateKey::from_pem_str(&private_key_pem)?; - let rsa_private_key = RsaPrivateKey::try_from(&private_key)?; - Ok(rsa_private_key.sign(Pkcs1v15Sign::new::(), &hash)?) - }), - with_pre_auth: false, - authenticator_nonce: OsRng.gen::<[u8; 4]>(), - })) - } - }; - - let as_rep = self.as_exchange(yield_point, &kdc_req_body, pa_data_options).await?; - - debug!("AS exchange finished successfully."); - - self.realm = Some(as_rep.0.crealm.0.to_string()); - - let (encryption_type, salt) = extract_encryption_params_from_as_rep(&as_rep)?; - - let encryption_type = CipherSuite::try_from(encryption_type as usize)?; - - self.encryption_params.encryption_type = Some(encryption_type); - - let mut authenticator = generate_authenticator(GenerateAuthenticatorOptions { - kdc_rep: &as_rep.0, - seq_num: Some(OsRng.gen::()), - sub_key: None, - checksum: None, - channel_bindings: self.channel_bindings.as_ref(), - extensions: Vec::new(), - })?; - - let mut session_key_extractor = match credentials { - CredentialsBuffers::AuthIdentity(_) => AsRepSessionKeyExtractor::AuthIdentity { - salt: &salt, - password: &password, - enc_params: &mut self.encryption_params, - }, - CredentialsBuffers::SmartCard(_) => AsRepSessionKeyExtractor::SmartCard { - dh_parameters: self.dh_parameters.as_mut().unwrap(), - enc_params: &mut self.encryption_params, - }, - }; - let session_key_1 = session_key_extractor.session_key(&as_rep)?; - - let service_principal = builder.target_name.ok_or_else(|| { - Error::new( - ErrorKind::NoCredentials, - "Service target name (service principal name) is not provided", - ) - })?; - - let tgs_req = generate_tgs_req(GenerateTgsReqOptions { - realm: &as_rep.0.crealm.0.to_string(), - service_principal, - session_key: &session_key_1, - ticket: as_rep.0.ticket.0, - authenticator: &mut authenticator, - additional_tickets: tgt_ticket.map(|ticket| vec![ticket]), - enc_params: &self.encryption_params, - context_requirements: builder.context_requirements, - })?; - - let response = self.send(yield_point, &serialize_message(&tgs_req)?).await?; - - // first 4 bytes are message len. skipping them - let mut d = picky_asn1_der::Deserializer::new_from_bytes(&response[4..]); - let tgs_rep: KrbResult = KrbResult::deserialize(&mut d)?; - let tgs_rep = tgs_rep?; - - debug!("TGS exchange finished successfully"); - - let session_key_2 = - extract_session_key_from_tgs_rep(&tgs_rep, &session_key_1, &self.encryption_params)?; - - self.encryption_params.session_key = Some(session_key_2); - - let enc_type = self - .encryption_params - .encryption_type - .as_ref() - .unwrap_or(&DEFAULT_ENCRYPTION_TYPE); - let authenticator_sub_key = generate_random_symmetric_key(enc_type, &mut OsRng); - - // the original flag is - // GSS_C_MUTUAL_FLAG | GSS_C_REPLAY_FLAG | GSS_C_SEQUENCE_FLAG | GSS_C_CONF_FLAG | GSS_C_INTEG_FLAG - // we want to be able to turn of sign and seal, so we leave confidentiality and integrity flags out - let mut flags: GssFlags = builder.context_requirements.into(); - if flags.contains(GssFlags::GSS_C_DELEG_FLAG) { - // Below are reasons why we turn off the GSS_C_DELEG_FLAG flag. - // - // RFC4121: The Kerberos Version 5 GSS-API. Section 4.1.1: Authenticator Checksum - // https://datatracker.ietf.org/doc/html/rfc4121#section-4.1.1.1 - // - // "The length of the checksum field MUST be at least 24 octets when GSS_C_DELEG_FLAG is not set, - // and at least 28 octets plus Dlgth octets when GSS_C_DELEG_FLAG is set." - // Out implementation _always_ uses the 24 octets checksum and do not support Kerberos credentials delegation. - // - // "When delegation is used, a ticket-granting ticket will be transferred in a KRB_CRED message." - // We do not support KRB_CRED messages. So, the GSS_C_DELEG_FLAG flags should be turned off. - warn!("Kerberos ApReq Authenticator checksum GSS_C_DELEG_FLAG is not supported. Turning it off..."); - flags.remove(GssFlags::GSS_C_DELEG_FLAG); - } - debug!(?flags, "ApReq Authenticator checksum flags"); - - let mut checksum_value = ChecksumValues::default(); - checksum_value.set_flags(flags); - - let authenticator_options = GenerateAuthenticatorOptions { - kdc_rep: &tgs_rep.0, - // The AP_REQ Authenticator sequence number should be the same as `seq_num` in the first Kerberos Wrap token generated - // by the `encrypt_message` method. So, we set the next sequence number but do not increment the counter, - // which will be incremented on each `encrypt_message` method call. - seq_num: Some(self.seq_number + 1), - sub_key: Some(EncKey { - key_type: enc_type.clone(), - key_value: authenticator_sub_key, - }), - - checksum: Some(ChecksumOptions { - checksum_type: AUTHENTICATOR_CHECKSUM_TYPE.to_vec(), - checksum_value, - }), - channel_bindings: self.channel_bindings.as_ref(), - extensions: Vec::new(), - }; - - let authenticator = generate_authenticator(authenticator_options)?; - let encoded_auth = picky_asn1_der::to_vec(&authenticator)?; - debug!(encoded_ap_req_authenticator = ?encoded_auth); - - let mut context_requirements = builder.context_requirements; - - if self.krb5_user_to_user && !context_requirements.contains(ClientRequestFlags::USE_SESSION_KEY) { - warn!("KRB5 U2U has been negotiated (selected by the server) but the USE_SESSION_KEY flag is not set. Forcibly turning it on..."); - context_requirements.set(ClientRequestFlags::USE_SESSION_KEY, true); - } - - let ap_req = generate_ap_req( - tgs_rep.0.ticket.0, - self.encryption_params - .session_key - .as_ref() - .ok_or_else(|| Error::new(ErrorKind::InternalError, "session key is not set"))?, - &authenticator, - &self.encryption_params, - context_requirements.into(), - )?; - - let encoded_neg_ap_req = if !builder.context_requirements.contains(ClientRequestFlags::USE_DCE_STYLE) { - // Wrap in a NegToken. - picky_asn1_der::to_vec(&generate_neg_ap_req(ap_req, mech_id)?)? - } else { - // Do not wrap if the `USE_DCE_STYLE` flag is set. - // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-kile/190ab8de-dc42-49cf-bf1b-ea5705b7a087 - picky_asn1_der::to_vec(&ap_req)? - }; - - let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?; - output_token.buffer.write_all(&encoded_neg_ap_req)?; - - self.state = KerberosState::ApExchange; - - SecurityStatus::ContinueNeeded - } - KerberosState::ApExchange => { - let input = builder - .input - .as_ref() - .ok_or_else(|| Error::new(ErrorKind::InvalidToken, "Input buffers must be specified"))?; - let input_token = SecurityBuffer::find_buffer(input, BufferType::Token)?; - - if builder.context_requirements.contains(ClientRequestFlags::USE_DCE_STYLE) { - // The `EC` field depends on the authentication type. For example, during RDP auth - // it is equal to 0, but during RPC auth it is equal to EC. - self.encryption_params.ec = EC; - - use picky_krb::messages::ApRep; - - let ap_rep: ApRep = picky_asn1_der::from_bytes(&input_token.buffer)?; - - let session_key = self - .encryption_params - .session_key - .as_ref() - .ok_or_else(|| Error::new(ErrorKind::InternalError, "session key is not set"))?; - let sub_session_key = - extract_sub_session_key_from_ap_rep(&ap_rep, session_key, &self.encryption_params)?; - let seq_number = extract_seq_number_from_ap_rep(&ap_rep, session_key, &self.encryption_params)?; - - trace!(?sub_session_key, "DCE AP_REP sub-session key"); - - self.encryption_params.sub_session_key = Some(sub_session_key); - - let ap_rep = generate_ap_rep(session_key, seq_number, &self.encryption_params)?; - let ap_rep = picky_asn1_der::to_vec(&ap_rep)?; - - let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?; - output_token.buffer.write_all(&ap_rep)?; - - self.state = KerberosState::PubKeyAuth; - - SecurityStatus::Ok - } else { - let neg_token_targ = { - let mut d = picky_asn1_der::Deserializer::new_from_bytes(&input_token.buffer); - let neg_token_targ: NegTokenTarg1 = KrbResult::deserialize(&mut d)??; - neg_token_targ - }; - - let ap_rep = extract_ap_rep_from_neg_token_targ(&neg_token_targ)?; - - let session_key = self - .encryption_params - .session_key - .as_ref() - .ok_or_else(|| Error::new(ErrorKind::InternalError, "session key is not set"))?; - let sub_session_key = - extract_sub_session_key_from_ap_rep(&ap_rep, session_key, &self.encryption_params)?; - - self.encryption_params.sub_session_key = Some(sub_session_key); - - if let Some(ref token) = neg_token_targ.0.mech_list_mic.0 { - validate_mic_token(&token.0 .0, ACCEPTOR_SIGN, &self.encryption_params)?; - } - - self.next_seq_number(); - self.prepare_final_neg_token(builder)?; - self.state = KerberosState::PubKeyAuth; - - SecurityStatus::Ok - } - } - _ => { - return Err(Error::new( - ErrorKind::OutOfSequence, - format!("Got wrong Kerberos state: {:?}", self.state), - )) - } - }; - - trace!(output_buffers = ?builder.output); - - Ok(InitializeSecurityContextResult { - status, - flags: ClientResponseFlags::empty(), - expiry: None, - }) + client::initialize_security_context(self, yield_point, builder).await } } @@ -1189,10 +663,18 @@ impl SspiEx for Kerberos { #[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::Kerberos; const SESSION_KEY: &[u8] = &[ @@ -1226,6 +708,25 @@ pub mod test_data { channel_bindings: None, 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()), + ])), + }, + user: None, + client: None, + authenticators_cache: Default::default(), } } @@ -1251,193 +752,7 @@ pub mod test_data { channel_bindings: None, dh_parameters: None, krb5_user_to_user: false, + server: Some(Box::new(fake_server_properties())), } } } - -#[cfg(test)] -mod tests { - use picky_krb::constants::key_usages::{ACCEPTOR_SEAL, INITIATOR_SEAL}; - use picky_krb::crypto::CipherSuite; - - use super::{EncryptionParams, KerberosConfig, KerberosState}; - use crate::{EncryptionFlags, Kerberos, SecurityBufferFlags, SecurityBufferRef, Sspi}; - - #[test] - fn stream_buffer_decryption() { - // https://learn.microsoft.com/en-us/windows/win32/secauthn/sspi-kerberos-interoperability-with-gssapi - - let mut kerberos_server = super::test_data::fake_server(); - let mut kerberos_client = super::test_data::fake_client(); - - let plain_message = b"some plain message"; - - 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()), - ]; - - kerberos_server - .encrypt_message(EncryptionFlags::empty(), &mut message, 0) - .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 []), - ]; - - kerberos_client.decrypt_message(&mut message, 0).unwrap(); - - assert_eq!(message[1].data(), plain_message); - } - - #[test] - fn secbuffer_readonly_with_checksum() { - // All values in this test (session keys, sequence number, encrypted and decrypted data) were extracted - // from the original Windows Kerberos implementation calls. - // We keep this test to guarantee full compatibility with the original Kerberos. - - let session_key = [ - 114, 67, 55, 26, 76, 210, 61, 0, 164, 44, 11, 133, 108, 220, 234, 145, 61, 144, 123, 45, 54, 175, 164, 168, - 99, 18, 99, 240, 242, 157, 95, 134, - ]; - let sub_session_key = [ - 91, 11, 188, 227, 10, 91, 180, 246, 64, 129, 251, 200, 118, 82, 109, 65, 241, 177, 109, 32, 124, 39, 127, - 171, 222, 132, 199, 199, 126, 110, 3, 166, - ]; - - let mut kerberos_server = Kerberos { - state: KerberosState::Final, - config: KerberosConfig { - kdc_url: None, - client_computer_name: None, - }, - auth_identity: None, - encryption_params: EncryptionParams { - encryption_type: Some(CipherSuite::Aes256CtsHmacSha196), - session_key: Some(session_key.to_vec()), - sub_session_key: Some(sub_session_key.to_vec()), - sspi_encrypt_key_usage: ACCEPTOR_SEAL, - sspi_decrypt_key_usage: INITIATOR_SEAL, - ec: 16, - }, - seq_number: 681238048, - realm: None, - kdc_url: None, - channel_bindings: None, - dh_parameters: None, - krb5_user_to_user: false, - }; - - // 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, - ]; - // RPC security trailer header - let trailer = [16, 6, 8, 0, 0, 0, 0, 0]; - // Encrypted data in RPC Request - let enc_data = [ - 41, 85, 192, 239, 104, 188, 180, 100, 229, 73, 83, 199, 77, 83, 79, 17, 163, 206, 241, 29, 90, 28, 89, 203, - 83, 176, 160, 252, 197, 221, 76, 113, 185, 141, 16, 200, 149, 55, 32, 96, 29, 49, 57, 124, 181, 147, 110, - 198, 125, 116, 150, 47, 35, 224, 117, 25, 10, 229, 201, 222, 153, 101, 131, 93, 204, 32, 9, 145, 186, 45, - 224, 160, 131, 23, 236, 111, 88, 48, 54, 4, 118, 114, 129, 119, 130, 164, 178, 4, 110, 74, 37, 1, 215, 177, - 16, 204, 238, 83, 255, 40, 240, 32, 209, 213, 90, 19, 126, 58, 34, 33, 72, 15, 206, 96, 67, 15, 169, 248, - 176, 9, 173, 196, 159, 239, 250, 120, 206, 52, 53, 229, 230, 66, 64, 109, 100, 21, 77, 193, 3, 40, 183, - 209, 177, 152, 165, 171, 108, 151, 112, 134, 53, 165, 128, 145, 147, 167, 5, 72, 35, 101, 42, 183, 67, 101, - 48, 255, 84, 208, 112, 199, 154, 62, 185, 87, 204, 228, 45, 30, 184, 47, 129, 145, 245, 168, 118, 174, 48, - 98, 174, 167, 208, 0, 113, 246, 219, 29, 192, 171, 97, 117, 115, 120, 115, 45, 44, 113, 62, 39, - ]; - // 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 Request security trailer data. Basically, it's a GSS API Wrap token - let security_trailer_data = [ - 5, 4, 6, 255, 0, 16, 0, 28, 0, 0, 0, 0, 40, 154, 222, 33, 170, 177, 218, 93, 176, 5, 210, 44, 38, 242, 179, - 168, 249, 202, 242, 199, 63, 162, 33, 40, 106, 186, 187, 28, 11, 229, 207, 219, 66, 86, 243, 16, 158, 100, - 133, 159, 87, 153, 196, 14, 251, 169, 164, 12, 18, 85, 182, 56, 72, 30, 137, 238, 50, 122, 73, 95, 109, - 194, 60, 120, - ]; - - let mut header_data = header.to_vec(); - let mut encrypted_data = enc_data.to_vec(); - let mut trailer_data = trailer.to_vec(); - let mut token_data = security_trailer_data.to_vec(); - let mut message = vec![ - SecurityBufferRef::data_buf(&mut header_data) - .with_flags(SecurityBufferFlags::SECBUFFER_READONLY_WITH_CHECKSUM), - SecurityBufferRef::data_buf(&mut encrypted_data), - SecurityBufferRef::data_buf(&mut trailer_data) - .with_flags(SecurityBufferFlags::SECBUFFER_READONLY_WITH_CHECKSUM), - SecurityBufferRef::token_buf(&mut token_data), - ]; - - kerberos_server.decrypt_message(&mut message, 0).unwrap(); - - assert_eq!(header[..], message[0].data()[..]); - assert_eq!(plaintext[..], message[1].data()[..]); - assert_eq!(trailer[..], message[2].data()[..]); - } - - #[test] - fn rpc_request_encryption() { - let mut kerberos_server = super::test_data::fake_server(); - let mut kerberos_client = super::test_data::fake_client(); - - // 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), - ]; - - kerberos_client - .encrypt_message(EncryptionFlags::empty(), &mut message, 0) - .unwrap(); - - assert_eq!(header[..], message[0].data()[..]); - assert_eq!(trailer[..], message[2].data()[..]); - - kerberos_server.decrypt_message(&mut message, 0).unwrap(); - - assert_eq!(header[..], message[0].data()[..]); - assert_eq!(message[1].data(), plaintext); - assert_eq!(trailer[..], message[2].data()[..]); - } -} diff --git a/src/kerberos/pa_datas.rs b/src/kerberos/pa_datas.rs index 6c80be51..0fec1f2e 100644 --- a/src/kerberos/pa_datas.rs +++ b/src/kerberos/pa_datas.rs @@ -5,11 +5,11 @@ use picky_krb::data_types::PaData; use picky_krb::messages::AsRep; use picky_krb::pkinit::PaPkAsRep; -use super::client::extractors::extract_session_key_from_as_rep; -use super::{ - generate_pa_datas_for_as_req as generate_password_based, EncryptionParams, - GenerateAsPaDataOptions as AuthIdentityPaDataOptions, +use crate::kerberos::client::extractors::extract_session_key_from_as_rep; +use crate::kerberos::client::generators::{ + generate_pa_datas_for_as_req as generate_password_based, GenerateAsPaDataOptions as AuthIdentityPaDataOptions, }; +use crate::kerberos::encryption_params::EncryptionParams; use crate::pk_init::{ extract_server_dh_public_key, generate_pa_datas_for_as_req as generate_private_key_based, DhParameters, GenerateAsPaDataOptions as SmartCardPaDataOptions, Wrapper, diff --git a/src/kerberos/server/as_exchange.rs b/src/kerberos/server/as_exchange.rs new file mode 100644 index 00000000..cdc5a18f --- /dev/null +++ b/src/kerberos/server/as_exchange.rs @@ -0,0 +1,126 @@ +use picky_krb::crypto::CipherSuite; +use picky_krb::data_types::Ticket; +use picky_krb::messages::TgtReq; +use rand::rngs::OsRng; +use rand::Rng; + +use crate::generator::YieldPointLocal; +use crate::kerberos::client::extractors::extract_encryption_params_from_as_rep; +use crate::kerberos::client::generators::{ + generate_as_req_kdc_body, get_client_principal_name_type, get_client_principal_realm, GenerateAsPaDataOptions, + GenerateAsReqOptions, +}; +use crate::kerberos::pa_datas::{AsRepSessionKeyExtractor, AsReqPaDataOptions}; +use crate::kerberos::utils::unwrap_hostname; +use crate::kerberos::{client, TGT_SERVICE_NAME}; +use crate::utils::utf16_bytes_to_utf8_string; +use crate::{ClientRequestFlags, CredentialsBuffers, Error, ErrorKind, Kerberos, Result}; + +/// Requests the TGT ticket from KDC. +/// +/// Basically, it performs the AS exchange, saves the session key, and returns the ticket. +pub async fn request_tgt( + server: &mut Kerberos, + credentials: &CredentialsBuffers, + tgt_req: &TgtReq, + yield_point: &mut YieldPointLocal, +) -> Result { + let service_name = &server + .server + .as_ref() + .ok_or_else(|| { + Error::new( + ErrorKind::InvalidHandle, + "Kerberos server properties are not initialized", + ) + })? + .service_name; + if tgt_req.server_name.0 != *service_name { + return Err(Error::new( + ErrorKind::InvalidToken, + format!( + "invalid ticket service name ({:?}): Kerberos server is configured for {:?}", + tgt_req.server_name.0, service_name + ), + )); + } + + let (username, password, realm, cname_type) = match credentials { + CredentialsBuffers::AuthIdentity(auth_identity) => { + let username = utf16_bytes_to_utf8_string(&auth_identity.user); + let domain = utf16_bytes_to_utf8_string(&auth_identity.domain); + let password = utf16_bytes_to_utf8_string(auth_identity.password.as_ref()); + + let realm = get_client_principal_realm(&username, &domain); + let cname_type = get_client_principal_name_type(&username, &domain); + + (username, password, realm, cname_type) + } + CredentialsBuffers::SmartCard(_) => { + return Err(Error::new( + ErrorKind::UnsupportedPreAuth, + "smart card credentials are not supported in Kerberos application server", + )); + } + }; + server.realm = Some(realm.clone()); + + let options = GenerateAsReqOptions { + realm: &realm, + username: &username, + cname_type, + snames: &[TGT_SERVICE_NAME, &realm], + // 4 = size of u32 + nonce: &OsRng.gen::<[u8; 4]>(), + hostname: &unwrap_hostname(server.config.client_computer_name.as_deref())?, + context_requirements: ClientRequestFlags::empty(), + }; + let kdc_req_body = generate_as_req_kdc_body(&options)?; + + let pa_data_options = match credentials { + CredentialsBuffers::AuthIdentity(auth_identity) => { + let domain = utf16_bytes_to_utf8_string(&auth_identity.domain); + let salt = format!("{}{}", domain, username); + + AsReqPaDataOptions::AuthIdentity(GenerateAsPaDataOptions { + password: &password, + salt: salt.as_bytes().to_vec(), + enc_params: server.encryption_params.clone(), + with_pre_auth: false, + }) + } + CredentialsBuffers::SmartCard(_) => { + return Err(Error::new( + ErrorKind::UnsupportedPreAuth, + "smart card credentials are not supported in Kerberos application server", + )); + } + }; + + let as_rep = client::as_exchange(server, yield_point, &kdc_req_body, pa_data_options).await?; + + debug!("AS exchange finished successfully."); + + server.realm = Some(as_rep.0.crealm.0.to_string()); + + let (encryption_type, salt) = extract_encryption_params_from_as_rep(&as_rep)?; + + let encryption_type = CipherSuite::try_from(encryption_type as usize)?; + server.encryption_params.encryption_type = Some(encryption_type); + + let mut session_key_extractor = AsRepSessionKeyExtractor::AuthIdentity { + salt: &salt, + password: &password, + enc_params: &mut server.encryption_params, + }; + + let server_props = server.server.as_mut().ok_or_else(|| { + Error::new( + ErrorKind::InvalidHandle, + "Kerberos server properties are not initialized", + ) + })?; + server_props.ticket_decryption_key = Some(session_key_extractor.session_key(&as_rep)?); + + Ok(as_rep.0.ticket.0) +} diff --git a/src/kerberos/server/cache.rs b/src/kerberos/server/cache.rs new file mode 100644 index 00000000..df104d1e --- /dev/null +++ b/src/kerberos/server/cache.rs @@ -0,0 +1,68 @@ +use std::collections::HashSet; +use std::hash::{Hash, Hasher}; + +use picky_asn1::wrapper::GeneralizedTimeAsn1; +use picky_krb::data_types::{Microseconds, PrincipalName}; + +#[derive(Debug, Clone, Eq)] +pub struct AuthenticatorCacheRecord { + pub cname: PrincipalName, + pub sname: PrincipalName, + pub ctime: GeneralizedTimeAsn1, + pub microseconds: Microseconds, +} + +// https://doc.rust-lang.org/std/hash/trait.Hash.html#hash-and-eq +// > When implementing both `Hash` and `Eq`, it is important that the following property holds: +// ``` +// k1 == k2 -> hash(k1) == hash(k2) +// ``` +// The `PrincipalName` implements the `PartialEq` trait but does not implement the `Hash` trait. +// We implement `PartialEq` manually to make sure we follow required properties. +impl PartialEq for AuthenticatorCacheRecord { + fn eq(&self, other: &Self) -> bool { + fn compare_principal_names(name_1: &PrincipalName, name_2: &PrincipalName) -> bool { + if name_1.name_type.0 != name_2.name_type.0 { + return false; + } + + let names_1 = &name_1.name_string.0 .0; + let names_2 = &name_2.name_string.0 .0; + + if names_1.len() != names_2.len() { + return false; + } + + for (name_1, name_2) in names_1.iter().zip(names_2.iter()) { + if name_1.0 != name_2.0 { + return false; + } + } + + true + } + + compare_principal_names(&self.cname, &other.cname) + && compare_principal_names(&self.sname, &other.sname) + && self.ctime == other.ctime + && self.microseconds == other.microseconds + } +} + +impl Hash for AuthenticatorCacheRecord { + fn hash(&self, state: &mut H) { + fn hash_principal_name(name: &PrincipalName, state: &mut H) { + name.name_type.0.hash(state); + for name_string in &name.name_string.0 .0 { + name_string.0.hash(state); + } + } + + hash_principal_name(&self.cname, state); + hash_principal_name(&self.sname, state); + self.ctime.hash(state); + self.microseconds.hash(state); + } +} + +pub type AuthenticatorsCache = HashSet; diff --git a/src/kerberos/server/extractors.rs b/src/kerberos/server/extractors.rs index 51b5780e..0423dfd1 100644 --- a/src/kerberos/server/extractors.rs +++ b/src/kerberos/server/extractors.rs @@ -1,129 +1,190 @@ -use std::io::Read; +use oid::ObjectIdentifier; +use picky::oids; +use picky_asn1::wrapper::ExplicitContextTag1; +use picky_krb::constants::gss_api::{ACCEPT_COMPLETE, AP_REQ_TOKEN_ID}; +use picky_krb::constants::key_usages::{AP_REQ_AUTHENTICATOR, TICKET_REP}; +use picky_krb::constants::types::NT_PRINCIPAL; +use picky_krb::crypto::CipherSuite; +use picky_krb::data_types::{Authenticator, EncTicketPart, PrincipalName}; +use picky_krb::gss_api::{ + ApplicationTag0, GssApiNegInit, KrbMessage, MechTypeList, NegTokenInit, NegTokenTarg, NegTokenTarg1, +}; +use picky_krb::messages::{ApReq, TgtReq}; -use picky_asn1::wrapper::ObjectIdentifierAsn1; -use picky_asn1_der::application_tag::ApplicationTag; -use picky_asn1_der::Asn1RawDer; -use picky_krb::constants::key_usages::AP_REP_ENC; -use picky_krb::data_types::{EncApRepPart, Ticket}; -use picky_krb::gss_api::NegTokenTarg1; -use picky_krb::messages::{ApRep, TgtRep}; - -use crate::kerberos::{EncryptionParams, DEFAULT_ENCRYPTION_TYPE}; use crate::{Error, ErrorKind, Result}; -pub fn extract_ap_rep_from_neg_token_targ(token: &NegTokenTarg1) -> Result { - let resp_token = &token - .0 - .response_token - .0 - .as_ref() - .ok_or_else(|| Error::new(ErrorKind::InvalidToken, "missing response token in NegTokenTarg"))? +/// Extract TGT request and mech types from the first token returned by the Kerberos client. +#[instrument(ret, level = "trace")] +pub fn decode_initial_neg_init(data: &[u8]) -> Result<(Option, MechTypeList)> { + let token: ApplicationTag0 = picky_asn1_der::from_bytes(data)?; + let NegTokenInit { + mech_types, + req_flags: _, + mech_token, + mech_list_mic: _, + } = token.0.neg_token_init.0; + + let mech_types = mech_types .0 - .0; + .ok_or_else(|| { + Error::new( + ErrorKind::InvalidToken, + "mech_types is missing in GssApiNegInit message", + ) + })? + .0; + + let tgt_req = if let Some(mech_token) = mech_token.0 { + let encoded_tgt_req = mech_token.0 .0; + let neg_token_init = KrbMessage::::decode_application_krb_message(&encoded_tgt_req)?; + + let token_oid = &neg_token_init.0.krb5_oid.0; + let krb5_u2u = oids::krb5_user_to_user(); + if *token_oid != krb5_u2u { + return Err(Error::new( + ErrorKind::InvalidToken, + format!( + "invalid oid inside mech_token: expected krb5 u2u ({:?}) but got {:?}", + krb5_u2u, token_oid + ), + )); + } + + Some(neg_token_init.0.krb_msg) + } else { + None + }; - let mut data = resp_token.as_slice(); - let _oid: ApplicationTag = picky_asn1_der::from_reader(&mut data)?; + Ok((tgt_req, mech_types)) +} - let mut t = [0, 0]; - data.read_exact(&mut t)?; +/// Decodes incoming SPNEGO message and extracts [ApReq] Kerberos message. +pub fn decode_neg_ap_req(data: &[u8]) -> Result { + let neg_token_targ: ExplicitContextTag1 = picky_asn1_der::from_bytes(data)?; + + let krb_message = KrbMessage::::decode_application_krb_message( + &neg_token_targ + .0 + .response_token + .0 + .ok_or_else(|| { + Error::new( + ErrorKind::InvalidToken, + "response_token is missing in NegTokenTarg message", + ) + })? + .0 + .0, + )? + .0; + + if krb_message.krb5_token_id != AP_REQ_TOKEN_ID { + return Err(Error::new( + ErrorKind::InvalidToken, + format!( + "invalid kerberos token id: expected {:?} but got {:?}", + AP_REQ_TOKEN_ID, krb_message.krb5_token_id + ), + )); + } - Ok(picky_asn1_der::from_reader(&mut data)?) + Ok(krb_message.krb_msg) } -#[instrument(level = "trace", ret)] -pub fn extract_seq_number_from_ap_rep( - ap_rep: &ApRep, - session_key: &[u8], - enc_params: &EncryptionParams, -) -> Result> { - let cipher = enc_params - .encryption_type - .as_ref() - .unwrap_or(&DEFAULT_ENCRYPTION_TYPE) - .cipher(); - - let res = cipher - .decrypt(session_key, AP_REP_ENC, &ap_rep.0.enc_part.cipher.0 .0) - .map_err(|err| { - Error::new( - ErrorKind::DecryptFailure, - format!("cannot decrypt ap_rep.enc_part: {:?}", err), - ) - })?; +/// Decrypts the [ApReq] ticket and returns decoded encrypted part of the ticket. +pub fn decrypt_ap_req_ticket(key: &[u8], ap_req: &ApReq) -> Result { + let ticket_enc_part = &ap_req.0.ticket.0 .0.enc_part.0; + let cipher = CipherSuite::try_from(ticket_enc_part.etype.0 .0.as_slice())?.cipher(); - let ap_rep_enc_part: EncApRepPart = picky_asn1_der::from_bytes(&res)?; + let encoded_enc_part = cipher.decrypt(key, TICKET_REP, &ticket_enc_part.cipher.0 .0)?; - Ok(ap_rep_enc_part - .0 - .seq_number - .0 - .ok_or_else(|| Error::new(ErrorKind::InvalidToken, "missing sequence number in ap_rep"))? - .0 - .0) + Ok(picky_asn1_der::from_bytes(&encoded_enc_part)?) } -#[instrument(level = "trace", ret)] -pub fn extract_sub_session_key_from_ap_rep( - ap_rep: &ApRep, - session_key: &[u8], - enc_params: &EncryptionParams, -) -> Result> { - let cipher = enc_params - .encryption_type - .as_ref() - .unwrap_or(&DEFAULT_ENCRYPTION_TYPE) - .cipher(); - - let res = cipher - .decrypt(session_key, AP_REP_ENC, &ap_rep.0.enc_part.cipher.0 .0) - .map_err(|err| { - Error::new( - ErrorKind::DecryptFailure, - format!("cannot decrypt ap_rep.enc_part: {:?}", err), - ) - })?; +/// Decrypts [ApReq] Authenticator and returns decoded authenticator. +pub fn decrypt_ap_req_authenticator(session_key: &[u8], ap_req: &ApReq) -> Result { + let encrypted_authenticator = &ap_req.0.authenticator.0; + let cipher = CipherSuite::try_from(encrypted_authenticator.etype.0 .0.as_slice())?.cipher(); - let ap_rep_enc_part: EncApRepPart = picky_asn1_der::from_bytes(&res)?; + let encoded_authenticator = + cipher.decrypt(session_key, AP_REQ_AUTHENTICATOR, &encrypted_authenticator.cipher.0 .0)?; + + Ok(picky_asn1_der::from_bytes(&encoded_authenticator)?) +} - Ok(ap_rep_enc_part +/// Validated client final [NegTokenTarg1] message and extract its MIC token. +/// +/// **Note**: the input client message should be last message in the _authentication_ sequence. +pub fn extract_client_mic_token(data: &[u8]) -> Result> { + let neg_token_targ: NegTokenTarg1 = picky_asn1_der::from_bytes(data)?; + let NegTokenTarg { + neg_result, + supported_mech: _, + response_token: _, + mech_list_mic, + } = neg_token_targ.0; + + let neg_result = neg_result .0 - .subkey + .ok_or_else(|| Error::new(ErrorKind::InvalidToken, "neg_result is missing in NegTokenTarg message"))? .0 - .ok_or_else(|| Error::new(ErrorKind::InvalidToken, "missing sub-key in ap_req"))? + .0; + if neg_result != ACCEPT_COMPLETE { + return Err(Error::new( + ErrorKind::InvalidToken, + "invalid neg result: expected accept_complete", + )); + } + + let mic_token = mech_list_mic .0 - .key_value + .ok_or_else(|| { + Error::new( + ErrorKind::InvalidToken, + "mech_list_mic is missing in NegTokenTarg message", + ) + })? .0 - .0) + .0; + + Ok(mic_token) } -/// Extracts TGT Ticket from encoded [NegTokenTarg1]. +/// Selects the preferred Kerberos oid. /// -/// Returned OID means the selected authentication mechanism by the target server. More info: -/// * [3.2.1. Syntax](https://datatracker.ietf.org/doc/html/rfc2478#section-3.2.1): `responseToken` field; -/// -/// We use this oid to choose between the regular Kerberos 5 and Kerberos 5 User-to-User authentication. -#[instrument(level = "trace", ret)] -pub fn extract_tgt_ticket_with_oid(data: &[u8]) -> Result> { - if data.is_empty() { - return Ok(None); +/// 1.2.840.48018.1.2.2 (MS KRB5 - Microsoft Kerberos 5) is preferred over 1.2.840.113554.1.2.2 (KRB5 - Kerberos 5). +pub fn select_mech_type(mech_list: &MechTypeList) -> Result { + let ms_krb5 = oids::ms_krb5(); + if mech_list.0.iter().any(|mech_type| mech_type.0 == ms_krb5) { + return Ok(ms_krb5); } - let neg_token_targ: NegTokenTarg1 = picky_asn1_der::from_bytes(data)?; - - if let Some(resp_token) = neg_token_targ.0.response_token.0.as_ref().map(|ticket| &ticket.0 .0) { - let mut c = resp_token.as_slice(); - - let oid: ApplicationTag = picky_asn1_der::from_reader(&mut c)?; - let oid: ObjectIdentifierAsn1 = picky_asn1_der::from_bytes(&oid.0 .0)?; - - let mut t = [0, 0]; - - c.read_exact(&mut t)?; + let krb5 = oids::krb5(); + if mech_list.0.iter().any(|mech_type| mech_type.0 == krb5) { + return Ok(krb5); + } - let tgt_rep: TgtRep = picky_asn1_der::from_reader(&mut c)?; + Err(Error::new( + ErrorKind::InvalidToken, + "invalid mech type list: Kerberos protocol is not present", + )) +} - Ok(Some((tgt_rep.ticket.0, oid))) +/// Extract username from the [PrincipalName]. +pub fn extract_username(cname: &PrincipalName) -> Result { + let name_type = &cname.name_type.0 .0; + if name_type == &[NT_PRINCIPAL] { + cname + .name_string + .0 + .0 + .first() + .map(|name| name.to_string()) + .ok_or_else(|| Error::new(ErrorKind::InvalidToken, "missing cname value in token")) } else { - Ok(None) + Err(Error::new( + ErrorKind::InvalidToken, + format!("unsupported principal name type: {:?}", name_type), + )) } } diff --git a/src/kerberos/server/generators.rs b/src/kerberos/server/generators.rs new file mode 100644 index 00000000..bad866a5 --- /dev/null +++ b/src/kerberos/server/generators.rs @@ -0,0 +1,117 @@ +use oid::ObjectIdentifier; +use picky::oids; +use picky_asn1::wrapper::{ + ExplicitContextTag0, ExplicitContextTag1, ExplicitContextTag2, ExplicitContextTag3, IntegerAsn1, + ObjectIdentifierAsn1, OctetStringAsn1, Optional, +}; +use picky_asn1_der::Asn1RawDer; +use picky_krb::constants::gss_api::{ACCEPT_INCOMPLETE, AP_REP_TOKEN_ID, TGT_REP_TOKEN_ID}; +use picky_krb::constants::key_usages::{ACCEPTOR_SIGN, AP_REP_ENC}; +use picky_krb::constants::types::{AP_REP_MSG_TYPE, TGT_REP_MSG_TYPE}; +use picky_krb::crypto::aes::{checksum_sha_aes, AesSize}; +use picky_krb::data_types::{ + EncApRepPart, EncApRepPartInner, EncryptedData, EncryptionKey, KerberosTime, Microseconds, Ticket, +}; +use picky_krb::gss_api::{ApplicationTag0, KrbMessage, MechType, MicToken, NegTokenTarg, NegTokenTarg1}; +use picky_krb::messages::{ApRep, ApRepInner, TgtRep}; + +use crate::kerberos::{EncryptionParams, DEFAULT_ENCRYPTION_TYPE}; +use crate::{Result, KERBEROS_VERSION}; + +pub fn generate_neg_token_targ(mech_type: ObjectIdentifier, tgt_rep: Option) -> Result { + let response_token = tgt_rep + .map(|tgt_rep| { + Result::Ok(ExplicitContextTag2::from(OctetStringAsn1::from( + picky_asn1_der::to_vec(&ApplicationTag0(KrbMessage { + krb5_oid: ObjectIdentifierAsn1::from(oids::krb5_user_to_user()), + krb5_token_id: TGT_REP_TOKEN_ID, + krb_msg: tgt_rep, + }))?, + ))) + }) + .transpose()?; + Ok(NegTokenTarg1::from(NegTokenTarg { + neg_result: Optional::from(Some(ExplicitContextTag0::from(Asn1RawDer(ACCEPT_INCOMPLETE.to_vec())))), + supported_mech: Optional::from(Some(ExplicitContextTag1::from(MechType::from(mech_type)))), + response_token: Optional::from(response_token), + mech_list_mic: Optional::from(None), + })) +} + +pub fn generate_ap_rep( + session_key: &[u8], + ctime: KerberosTime, + cusec: Microseconds, + seq_number: Vec, + enc_params: &EncryptionParams, +) -> Result { + let encryption_type = enc_params.encryption_type.as_ref().unwrap_or(&DEFAULT_ENCRYPTION_TYPE); + + let enc_part = EncApRepPart::from(EncApRepPartInner { + ctime: ExplicitContextTag0::from(ctime), + cusec: ExplicitContextTag1::from(cusec), + subkey: Optional::from(enc_params.sub_session_key.as_ref().map(|sub_key| { + ExplicitContextTag2::from(EncryptionKey { + key_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![encryption_type.into()])), + key_value: ExplicitContextTag1::from(OctetStringAsn1::from(sub_key.clone())), + }) + })), + seq_number: Optional::from(Some(ExplicitContextTag3::from(IntegerAsn1::from(seq_number)))), + }); + + let cipher = encryption_type.cipher(); + let enc_data = cipher.encrypt(session_key, AP_REP_ENC, &picky_asn1_der::to_vec(&enc_part)?)?; + + Ok(ApRep::from(ApRepInner { + pvno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])), + msg_type: ExplicitContextTag1::from(IntegerAsn1::from(vec![AP_REP_MSG_TYPE])), + enc_part: ExplicitContextTag2::from(EncryptedData { + etype: ExplicitContextTag0::from(IntegerAsn1::from(vec![encryption_type.into()])), + kvno: Optional::from(None), + cipher: ExplicitContextTag2::from(OctetStringAsn1::from(enc_data)), + }), + })) +} + +pub fn generate_final_neg_token_targ(mech_id: ObjectIdentifier, ap_rep: ApRep, mic: Vec) -> Result { + let krb_blob = ApplicationTag0(KrbMessage { + krb5_oid: ObjectIdentifierAsn1::from(mech_id), + krb5_token_id: AP_REP_TOKEN_ID, + krb_msg: ap_rep, + }); + + Ok(NegTokenTarg1::from(NegTokenTarg { + neg_result: Optional::from(Some(ExplicitContextTag0::from(Asn1RawDer(ACCEPT_INCOMPLETE.to_vec())))), + supported_mech: Optional::from(None), + response_token: Optional::from(Some(ExplicitContextTag2::from(OctetStringAsn1::from( + picky_asn1_der::to_vec(&krb_blob)?, + )))), + mech_list_mic: Optional::from(Some(ExplicitContextTag3::from(OctetStringAsn1::from(mic)))), + })) +} + +pub fn generate_mic_token(seq_number: u64, mut payload: Vec, session_key: &[u8]) -> Result> { + let mut mic_token = MicToken::with_acceptor_flags().with_seq_number(seq_number); + + payload.extend_from_slice(&mic_token.header()); + + mic_token.set_checksum(checksum_sha_aes( + session_key, + ACCEPTOR_SIGN, + &payload, + &AesSize::Aes256, + )?); + + let mut mic_token_raw = Vec::new(); + mic_token.encode(&mut mic_token_raw)?; + + Ok(mic_token_raw) +} + +pub fn generate_tgt_rep(ticket: Ticket) -> TgtRep { + TgtRep { + pvno: ExplicitContextTag0::from(IntegerAsn1::from(vec![KERBEROS_VERSION])), + msg_type: ExplicitContextTag1::from(IntegerAsn1::from(vec![TGT_REP_MSG_TYPE])), + ticket: ExplicitContextTag2::from(ticket), + } +} diff --git a/src/kerberos/server/mod.rs b/src/kerberos/server/mod.rs index a3e753ae..e8d5f049 100644 --- a/src/kerberos/server/mod.rs +++ b/src/kerberos/server/mod.rs @@ -1 +1,415 @@ -pub mod extractors; +mod as_exchange; +mod cache; +mod extractors; +mod generators; + +use std::io::Write; +use std::time::Duration; + +use cache::AuthenticatorCacheRecord; +use extractors::{extract_client_mic_token, extract_username, select_mech_type}; +use generators::{generate_mic_token, generate_tgt_rep}; +use picky::oids; +use picky_asn1::restricted_string::IA5String; +use picky_asn1::wrapper::{Asn1SequenceOf, ExplicitContextTag0, ExplicitContextTag1, IntegerAsn1}; +use picky_krb::constants::key_usages::INITIATOR_SIGN; +use picky_krb::constants::types::NT_SRV_INST; +use picky_krb::data_types::{AuthenticatorInner, KerberosStringAsn1, PrincipalName}; +use picky_krb::gss_api::MechTypeList; +use rand::rngs::OsRng; +use rand::RngCore; +use time::OffsetDateTime; + +use self::as_exchange::request_tgt; +use self::cache::AuthenticatorsCache; +use self::extractors::{ + decode_initial_neg_init, decode_neg_ap_req, decrypt_ap_req_authenticator, decrypt_ap_req_ticket, +}; +use self::generators::{generate_ap_rep, generate_final_neg_token_targ, generate_neg_token_targ}; +use super::utils::validate_mic_token; +use crate::builders::FilledAcceptSecurityContext; +use crate::generator::YieldPointLocal; +use crate::kerberos::flags::ApOptions; +use crate::kerberos::DEFAULT_ENCRYPTION_TYPE; +use crate::{ + AcceptSecurityContextResult, BufferType, CredentialsBuffers, Error, ErrorKind, Kerberos, KerberosState, Result, + SecurityBuffer, SecurityStatus, ServerRequestFlags, ServerResponseFlags, SspiImpl, Username, +}; + +/// Indicated that the MIC token `SentByAcceptor` flag must not be enabled in the incoming MIC token. +const SENT_BY_INITIATOR: u8 = 0; + +/// Additional properties that are needed only for server-side Kerberos. +#[derive(Debug, Clone)] +pub struct ServerProperties { + /// Supported mech types sent by the client in the first incoming message. + /// We user them for checksum calculation during MIC token generation. + pub mech_types: MechTypeList, + /// Maximum allowed time difference between client and server clocks. + /// It is recommended to set this value not greater then a few minutes. + pub max_time_skew: Duration, + /// Key that is used for TGS tickets decryption. + /// It should be provided by the user during regular Kerberos auth. Or + /// it will be established during AS exchange in the case of Kerberos U2U auth. + pub ticket_decryption_key: Option>, + /// Name of the Kerberos service. + pub service_name: PrincipalName, + /// User credentials on whose behalf the TGT ticket will be requested. + pub user: Option, + /// Username of the authenticated client. + /// + /// This field should be set by the Kerberos implementation after successful log on. + pub client: Option, + /// Authenticators cache. + /// + /// [Receipt of KRB_AP_REQ Message](https://www.rfc-editor.org/rfc/rfc4120#section-3.2.3): + /// + /// > The server MUST utilize a replay cache to remember any authenticator presented within the allowable clock skew. + /// > The replay cache will store at least the server name, along with the client name, time, + /// > and microsecond fields from the recently-seen authenticators, and if a matching tuple is found, + /// > the error is returned. + pub authenticators_cache: AuthenticatorsCache, +} + +impl ServerProperties { + /// Creates a new instance of [ServerProperties]. + pub fn new( + sname: &[&str], + user: Option, + max_time_skew: Duration, + ticket_decryption_key: Option>, + ) -> Result { + let service_names = sname + .iter() + .map(|sname| Ok(KerberosStringAsn1::from(IA5String::from_string((*sname).to_owned())?))) + .collect::>>()?; + + Ok(Self { + mech_types: MechTypeList::from(Vec::new()), + max_time_skew, + ticket_decryption_key, + service_name: PrincipalName { + name_type: ExplicitContextTag0::from(IntegerAsn1::from(vec![NT_SRV_INST])), + name_string: ExplicitContextTag1::from(Asn1SequenceOf::from(service_names)), + }, + user, + client: None, + authenticators_cache: AuthenticatorsCache::new(), + }) + } +} + +/// Performs one authentication step. +/// +/// The user should call this function until it returns `SecurityStatus::Ok`. +pub async fn accept_security_context( + server: &mut Kerberos, + yield_point: &mut YieldPointLocal, + builder: FilledAcceptSecurityContext<'_, ::CredentialsHandle>, +) -> Result { + let input = builder + .input + .as_ref() + .ok_or_else(|| crate::Error::new(ErrorKind::InvalidToken, "input buffers must be specified"))?; + let input_token = SecurityBuffer::find_buffer(input, BufferType::Token)?; + + let status = match server.state { + KerberosState::Negotiate => { + let (tgt_req, mech_types) = decode_initial_neg_init(&input_token.buffer)?; + let mech_type = select_mech_type(&mech_types)?; + + let server_props = server.server.as_mut().ok_or_else(|| { + Error::new( + ErrorKind::InvalidHandle, + "Kerberos server properties are not initialized", + ) + })?; + server_props.mech_types = mech_types; + + let tgt_rep = if let Some(tgt_req) = tgt_req { + // If user sent us TgtReq than they want Kerberos User-to-User auth. + // At this point, we need to request TGT token in KDC and send it back to the user. + + if !builder + .context_requirements + .contains(ServerRequestFlags::USE_SESSION_KEY) + { + warn!("KRB5 U2U has been negotiated (requested by the client) but the USE_SESSION_KEY flag is not set."); + } + + server.krb5_user_to_user = true; + + let credentials = builder + .credentials_handle + .and_then(|credentials_handle| (*credentials_handle).clone()) + .or_else(|| server_props.user.clone()); + let credentials = credentials.as_ref().ok_or_else(|| { + Error::new( + ErrorKind::WrongCredentialHandle, + "failed to request TGT ticket: no credentials provided", + ) + })?; + + Some(generate_tgt_rep( + request_tgt(server, credentials, &tgt_req, yield_point).await?, + )) + } else { + None + }; + + let encoded_neg_token_targ = picky_asn1_der::to_vec(&generate_neg_token_targ(mech_type, tgt_rep)?)?; + + let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?; + output_token.buffer.write_all(&encoded_neg_token_targ)?; + + server.state = KerberosState::Preauthentication; + + SecurityStatus::ContinueNeeded + } + KerberosState::Preauthentication => { + let ap_req = decode_neg_ap_req(&input_token.buffer)?; + + let server_data = server.server.as_ref().ok_or_else(|| { + Error::new( + ErrorKind::InvalidHandle, + "Kerberos server properties are not initialized", + ) + })?; + + let ticket_service_name = &ap_req.0.ticket.0 .0.sname.0; + if *ticket_service_name != server_data.service_name { + return Err(Error::new( + ErrorKind::InvalidToken, + format!( + "invalid ticket service name ({:?}): Kerberos server is configured for {:?}", + ticket_service_name, server_data.service_name + ), + )); + } + + let ticket_decryption_key = server_data + .ticket_decryption_key + .as_ref() + .ok_or_else(|| Error::new(ErrorKind::InternalError, "ticket decryption key is not set"))?; + + let ticket_enc_part = decrypt_ap_req_ticket(ticket_decryption_key, &ap_req)?; + let session_key = ticket_enc_part.0.key.0.key_value.0 .0.clone(); + + let AuthenticatorInner { + authenticator_vno: _, + crealm, + cname, + cksum: _, + cusec, + ctime, + subkey: _, + seq_number: _, + authorization_data: _, + } = decrypt_ap_req_authenticator(&session_key, &ap_req)?.0; + + // [3.2.3. Receipt of KRB_AP_REQ Message](https://www.rfc-editor.org/rfc/rfc4120#section-3.2.3) + // The name and realm of the client from the ticket are compared against the same fields in the authenticator. + if ticket_enc_part.0.crealm.0 != crealm.0 || ticket_enc_part.0.cname != cname.0 { + return Err(Error::new( + ErrorKind::InvalidToken, + "the name and realm of the client in ticket and authenticator do not match", + )); + } + + let now = OffsetDateTime::now_utc(); + let client_time = OffsetDateTime::try_from(ctime.0 .0.clone()) + .map_err(|err| Error::new(ErrorKind::InvalidToken, format!("clint time is not valid: {:?}", err)))?; + let max_time_skew = server_data.max_time_skew; + + if (now - client_time).abs() > max_time_skew { + return Err(Error::new( + ErrorKind::TimeSkew, + "invalid authenticator ctime: time skew is too big", + )); + } + + let ticket_start_time = ticket_enc_part + .0 + .starttime + .0 + .map(|start_time| start_time.0) + // [5.3. Tickets](https://www.rfc-editor.org/rfc/rfc4120#section-5.3) + // If the starttime field is absent from the ticket, then the authtime field SHOULD be used in its place to determine + // the life of the ticket. + .unwrap_or_else(|| ticket_enc_part.0.auth_time.0) + .0; + let ticket_start_time = OffsetDateTime::try_from(ticket_start_time).map_err(|err| { + Error::new( + ErrorKind::InvalidToken, + format!("ticket end time is not valid: {:?}", err), + ) + })?; + if ticket_start_time > now + max_time_skew { + return Err(Error::new( + ErrorKind::InvalidToken, + "ticket not yet valid: ticket start time is greater than current time + max time skew", + )); + } + + let ticket_end_time = OffsetDateTime::try_from(ticket_enc_part.0.endtime.0 .0).map_err(|err| { + Error::new( + ErrorKind::InvalidToken, + format!("ticket end time is not valid: {:?}", err), + ) + })?; + if now > ticket_end_time + max_time_skew { + return Err(Error::new( + ErrorKind::InvalidToken, + "ticket is expired: current time is greater than ticket end time + max time skew", + )); + } + + let server_data = server.server.as_mut().ok_or_else(|| { + Error::new( + ErrorKind::InvalidHandle, + "Kerberos server properties are not initialized", + ) + })?; + + let cache_record = AuthenticatorCacheRecord { + cname: cname.0.clone(), + sname: ticket_service_name.clone(), + ctime: ctime.0.clone(), + microseconds: cusec.0.clone(), + }; + if !server_data.authenticators_cache.contains(&cache_record) { + server_data.authenticators_cache.insert(cache_record); + } else { + return Err(Error::new( + ErrorKind::InvalidToken, + "ApReq Authenticator replay detected", + )); + } + + debug!("ApReq Ticket and Authenticator are valid!"); + + server_data.client = Some(Username::new_upn( + &extract_username(&cname.0)?, + &crealm.0 .0.to_string().to_ascii_lowercase(), + )?); + + let ap_options_bytes = ap_req.0.ap_options.0 .0.as_bytes(); + // [5.5.1. KRB_AP_REQ Definition](https://www.rfc-editor.org/rfc/rfc4120#section-5.5.1) + // The `ap-options` field has 32 bits or 4 bytes long. But it is encoded as BitStringAsn1, so the first byte + // indicates the number of bits used. Thus, the overall number of expected bytes is 1 + 4 = 5. + if ap_options_bytes.len() != 1 + 4 { + return Err(Error::new( + ErrorKind::InvalidToken, + format!( + "invalid ApReq ap-options: invalid data length: expected 5 bytes but got {}", + ap_options_bytes.len() + ), + )); + } + let ap_options = + u32::from_be_bytes(ap_options_bytes[1..].try_into().map_err(|err| { + Error::new(ErrorKind::InvalidToken, format!("invalid ApReq ap-options: {:?}", err)) + })?); + let ap_options = ApOptions::from_bits(ap_options) + .ok_or_else(|| Error::new(ErrorKind::InvalidToken, "invalid ApReq ap-options"))?; + + // [3.2.4. Generation of a KRB_AP_REP Message](https://www.rfc-editor.org/rfc/rfc4120#section-3.2.3) + // ...the server need not explicitly reply to the KRB_AP_REQ. However, if mutual authentication is being performed, + // the KRB_AP_REQ message will have MUTUAL-REQUIRED set in its ap-options field, and a KRB_AP_REP message + // is required in response. + if ap_options.contains(ApOptions::MUTUAL_REQUIRED) { + let key_size = server + .encryption_params + .encryption_type + .as_ref() + .unwrap_or(&DEFAULT_ENCRYPTION_TYPE) + .cipher() + .key_size(); + let mut sub_session_key = vec![0; key_size]; + OsRng.fill_bytes(&mut sub_session_key); + server.encryption_params.sub_session_key = Some(sub_session_key); + + // [3.2.4. Generation of a KRB_AP_REP Message](https://www.rfc-editor.org/rfc/rfc4120#section-3.2.3) + // A subkey MAY be included if the server desires to negotiate a different subkey. + // The KRB_AP_REP message is encrypted in the session key extracted from the ticket. + let ap_rep = generate_ap_rep( + &session_key, + ctime.0, + cusec.0, + (server.seq_number + 1).to_be_bytes().to_vec(), + &server.encryption_params, + )?; + + let mic_payload = picky_asn1_der::to_vec( + &server + .server + .as_ref() + .ok_or_else(|| { + Error::new( + ErrorKind::InvalidHandle, + "Kerberos server properties are not initialized", + ) + })? + .mech_types, + )?; + let mic = generate_mic_token( + u64::from(server.next_seq_number()), + mic_payload, + server + .encryption_params + .sub_session_key + .as_ref() + .expect("sub-session key should present"), + )?; + + let mech_id = if server.krb5_user_to_user { + oids::krb5_user_to_user() + } else { + oids::krb5() + }; + + let encoded_neg_ap_rep = picky_asn1_der::to_vec(&generate_final_neg_token_targ(mech_id, ap_rep, mic)?)?; + + let output_token = SecurityBuffer::find_buffer_mut(builder.output, BufferType::Token)?; + output_token.buffer.write_all(&encoded_neg_ap_rep)?; + } + + server.encryption_params.session_key = Some(session_key); + server.state = KerberosState::ApExchange; + + SecurityStatus::ContinueNeeded + } + KerberosState::ApExchange => { + let server_props = server.server.as_mut().ok_or_else(|| { + Error::new( + ErrorKind::InvalidHandle, + "Kerberos server properties are not initialized", + ) + })?; + + let client_mic = extract_client_mic_token(&input_token.buffer)?; + validate_mic_token::( + &client_mic, + INITIATOR_SIGN, + &server.encryption_params, + &server_props.mech_types, + )?; + + server.state = KerberosState::PubKeyAuth; + + SecurityStatus::Ok + } + KerberosState::PubKeyAuth | KerberosState::Credentials | KerberosState::Final => { + return Err(Error::new( + ErrorKind::OutOfSequence, + format!("got wrong Kerberos state: {:?}", server.state), + )) + } + }; + + Ok(AcceptSecurityContextResult { + status, + flags: ServerResponseFlags::empty(), + expiry: None, + }) +} diff --git a/src/kerberos/tests.rs b/src/kerberos/tests.rs new file mode 100644 index 00000000..4ffb3f59 --- /dev/null +++ b/src/kerberos/tests.rs @@ -0,0 +1,180 @@ +use picky_krb::constants::key_usages::{ACCEPTOR_SEAL, INITIATOR_SEAL}; +use picky_krb::crypto::CipherSuite; + +use crate::kerberos::{test_data, EncryptionParams, KerberosConfig, KerberosState}; +use crate::{EncryptionFlags, Kerberos, SecurityBufferFlags, SecurityBufferRef, Sspi}; + +#[test] +fn stream_buffer_decryption() { + // https://learn.microsoft.com/en-us/windows/win32/secauthn/sspi-kerberos-interoperability-with-gssapi + + let mut kerberos_server = test_data::fake_server(); + let mut kerberos_client = test_data::fake_client(); + + let plain_message = b"some plain message"; + + 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()), + ]; + + kerberos_server + .encrypt_message(EncryptionFlags::empty(), &mut message, 0) + .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 []), + ]; + + kerberos_client.decrypt_message(&mut message, 0).unwrap(); + + assert_eq!(message[1].data(), plain_message); +} + +#[test] +fn secbuffer_readonly_with_checksum() { + // All values in this test (session keys, sequence number, encrypted and decrypted data) were extracted + // from the original Windows Kerberos implementation calls. + // We keep this test to guarantee full compatibility with the original Kerberos. + + let session_key = [ + 114, 67, 55, 26, 76, 210, 61, 0, 164, 44, 11, 133, 108, 220, 234, 145, 61, 144, 123, 45, 54, 175, 164, 168, 99, + 18, 99, 240, 242, 157, 95, 134, + ]; + let sub_session_key = [ + 91, 11, 188, 227, 10, 91, 180, 246, 64, 129, 251, 200, 118, 82, 109, 65, 241, 177, 109, 32, 124, 39, 127, 171, + 222, 132, 199, 199, 126, 110, 3, 166, + ]; + + let mut kerberos_server = Kerberos { + state: KerberosState::Final, + config: KerberosConfig { + kdc_url: None, + client_computer_name: None, + }, + auth_identity: None, + encryption_params: EncryptionParams { + encryption_type: Some(CipherSuite::Aes256CtsHmacSha196), + session_key: Some(session_key.to_vec()), + sub_session_key: Some(sub_session_key.to_vec()), + sspi_encrypt_key_usage: ACCEPTOR_SEAL, + sspi_decrypt_key_usage: INITIATOR_SEAL, + ec: 16, + }, + seq_number: 681238048, + realm: None, + kdc_url: None, + channel_bindings: None, + dh_parameters: None, + krb5_user_to_user: false, + server: Some(Box::new(test_data::fake_server_properties())), + }; + + // 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, + ]; + // RPC security trailer header + let trailer = [16, 6, 8, 0, 0, 0, 0, 0]; + // Encrypted data in RPC Request + let enc_data = [ + 41, 85, 192, 239, 104, 188, 180, 100, 229, 73, 83, 199, 77, 83, 79, 17, 163, 206, 241, 29, 90, 28, 89, 203, 83, + 176, 160, 252, 197, 221, 76, 113, 185, 141, 16, 200, 149, 55, 32, 96, 29, 49, 57, 124, 181, 147, 110, 198, 125, + 116, 150, 47, 35, 224, 117, 25, 10, 229, 201, 222, 153, 101, 131, 93, 204, 32, 9, 145, 186, 45, 224, 160, 131, + 23, 236, 111, 88, 48, 54, 4, 118, 114, 129, 119, 130, 164, 178, 4, 110, 74, 37, 1, 215, 177, 16, 204, 238, 83, + 255, 40, 240, 32, 209, 213, 90, 19, 126, 58, 34, 33, 72, 15, 206, 96, 67, 15, 169, 248, 176, 9, 173, 196, 159, + 239, 250, 120, 206, 52, 53, 229, 230, 66, 64, 109, 100, 21, 77, 193, 3, 40, 183, 209, 177, 152, 165, 171, 108, + 151, 112, 134, 53, 165, 128, 145, 147, 167, 5, 72, 35, 101, 42, 183, 67, 101, 48, 255, 84, 208, 112, 199, 154, + 62, 185, 87, 204, 228, 45, 30, 184, 47, 129, 145, 245, 168, 118, 174, 48, 98, 174, 167, 208, 0, 113, 246, 219, + 29, 192, 171, 97, 117, 115, 120, 115, 45, 44, 113, 62, 39, + ]; + // 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 Request security trailer data. Basically, it's a GSS API Wrap token + let security_trailer_data = [ + 5, 4, 6, 255, 0, 16, 0, 28, 0, 0, 0, 0, 40, 154, 222, 33, 170, 177, 218, 93, 176, 5, 210, 44, 38, 242, 179, + 168, 249, 202, 242, 199, 63, 162, 33, 40, 106, 186, 187, 28, 11, 229, 207, 219, 66, 86, 243, 16, 158, 100, 133, + 159, 87, 153, 196, 14, 251, 169, 164, 12, 18, 85, 182, 56, 72, 30, 137, 238, 50, 122, 73, 95, 109, 194, 60, + 120, + ]; + + let mut header_data = header.to_vec(); + let mut encrypted_data = enc_data.to_vec(); + let mut trailer_data = trailer.to_vec(); + let mut token_data = security_trailer_data.to_vec(); + let mut message = vec![ + SecurityBufferRef::data_buf(&mut header_data).with_flags(SecurityBufferFlags::SECBUFFER_READONLY_WITH_CHECKSUM), + SecurityBufferRef::data_buf(&mut encrypted_data), + SecurityBufferRef::data_buf(&mut trailer_data) + .with_flags(SecurityBufferFlags::SECBUFFER_READONLY_WITH_CHECKSUM), + SecurityBufferRef::token_buf(&mut token_data), + ]; + + kerberos_server.decrypt_message(&mut message, 0).unwrap(); + + assert_eq!(header[..], message[0].data()[..]); + assert_eq!(plaintext[..], message[1].data()[..]); + assert_eq!(trailer[..], message[2].data()[..]); +} + +#[test] +fn rpc_request_encryption() { + let mut kerberos_server = test_data::fake_server(); + let mut kerberos_client = test_data::fake_client(); + + // 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), + ]; + + kerberos_client + .encrypt_message(EncryptionFlags::empty(), &mut message, 0) + .unwrap(); + + assert_eq!(header[..], message[0].data()[..]); + assert_eq!(trailer[..], message[2].data()[..]); + + kerberos_server.decrypt_message(&mut message, 0).unwrap(); + + assert_eq!(header[..], message[0].data()[..]); + assert_eq!(message[1].data(), plaintext); + assert_eq!(trailer[..], message[2].data()[..]); +} diff --git a/src/kerberos/utils.rs b/src/kerberos/utils.rs index 4a0d73a2..e8681ace 100644 --- a/src/kerberos/utils.rs +++ b/src/kerberos/utils.rs @@ -2,10 +2,9 @@ use std::io::Write; use picky_krb::constants::key_usages::INITIATOR_SIGN; use picky_krb::crypto::aes::{checksum_sha_aes, AesSize}; -use picky_krb::gss_api::MicToken; +use picky_krb::gss_api::{MechTypeList, MicToken}; use serde::Serialize; -use crate::kerberos::client::generators::get_mech_list; use crate::kerberos::encryption_params::EncryptionParams; use crate::{Error, ErrorKind, Result}; @@ -22,13 +21,45 @@ pub fn serialize_message(v: &T) -> Result> { Ok(data) } -pub fn validate_mic_token(raw_token: &[u8], key_usage: i32, params: &EncryptionParams) -> Result<()> { +pub fn validate_mic_token( + raw_token: &[u8], + key_usage: i32, + params: &EncryptionParams, + mech_types: &MechTypeList, +) -> Result<()> { let token = MicToken::decode(raw_token)?; + let token_flags = token.flags; + + // [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. + if token_flags & 0b01 != IS_SENT_BY_ACCEPTOR { + return Err(Error::new( + ErrorKind::InvalidToken, + "invalid MIC 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. + if token_flags & 0b10 == 0b10 { + return Err(Error::new( + ErrorKind::InvalidToken, + "the Sealed flag has not to be set in the MIC token", + )); + } - let mut payload = picky_asn1_der::to_vec(&get_mech_list())?; + let mut payload = picky_asn1_der::to_vec(mech_types)?; payload.extend_from_slice(&token.header()); - // the sub-session key is always preferred over the session key + // The sub-session key is always preferred over the session key. let key = if let Some(key) = params.sub_session_key.as_ref() { key } else if let Some(key) = params.session_key.as_ref() { diff --git a/src/lib.rs b/src/lib.rs index aa5c9c26..fbae2fa2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -87,7 +87,7 @@ use bitflags::bitflags; #[cfg(feature = "tsssp")] use credssp::sspi_cred_ssp; pub use generator::NetworkRequest; -use generator::{GeneratorChangePassword, GeneratorInitSecurityContext}; +use generator::{GeneratorAcceptSecurityContext, GeneratorChangePassword, GeneratorInitSecurityContext}; pub use network_client::NetworkProtocol; use num_derive::{FromPrimitive, ToPrimitive}; use picky_asn1::restricted_string::CharSetError; @@ -112,7 +112,7 @@ use self::builders::{ ChangePassword, FilledAcceptSecurityContext, FilledAcquireCredentialsHandle, FilledInitializeSecurityContext, }; pub use self::kdc::{detect_kdc_host, detect_kdc_url}; -pub use self::kerberos::config::KerberosConfig; +pub use self::kerberos::config::{KerberosConfig, KerberosServerConfig}; pub use self::kerberos::{Kerberos, KerberosState, KERBEROS_VERSION}; pub use self::negotiate::{Negotiate, NegotiateConfig, NegotiatedProtocol}; pub use self::ntlm::Ntlm; @@ -466,14 +466,16 @@ where /// .resolve_to_result() /// .unwrap(); /// - /// let server_result = ntlm + /// let builder = ntlm /// .accept_security_context() /// .with_credentials_handle(&mut server_acq_cred_result.credentials_handle) /// .with_context_requirements(sspi::ServerRequestFlags::ALLOCATE_MEMORY) /// .with_target_data_representation(sspi::DataRepresentation::Native) /// .with_input(&mut client_output_buffer) - /// .with_output(&mut output_buffer) - /// .execute(&mut ntlm) + /// .with_output(&mut output_buffer); + /// let server_result = ntlm.accept_security_context_impl(builder) + /// .unwrap() + /// .resolve_to_result() /// .unwrap(); /// /// if server_result.status == sspi::SecurityStatus::CompleteAndContinue @@ -555,14 +557,16 @@ where /// .resolve_to_result() /// .unwrap(); /// - /// let server_result = ntlm + /// let builder = ntlm /// .accept_security_context() /// .with_credentials_handle(&mut server_acq_cred_result.credentials_handle) /// .with_context_requirements(sspi::ServerRequestFlags::ALLOCATE_MEMORY) /// .with_target_data_representation(sspi::DataRepresentation::Native) /// .with_input(&mut client_output_buffer) - /// .with_output(&mut server_output_buffer) - /// .execute(&mut ntlm) + /// .with_output(&mut server_output_buffer); + /// let server_result = ntlm.accept_security_context_impl(builder) + /// .unwrap() + /// .resolve_to_result() /// .unwrap(); /// /// if server_result.status == sspi::SecurityStatus::CompleteAndContinue @@ -671,14 +675,16 @@ where /// .resolve_to_result() /// .unwrap(); /// - /// let server_result = ntlm + /// let builder = ntlm /// .accept_security_context() /// .with_credentials_handle(&mut server_acq_cred_result.credentials_handle) /// .with_context_requirements(sspi::ServerRequestFlags::ALLOCATE_MEMORY) /// .with_target_data_representation(sspi::DataRepresentation::Native) /// .with_input(&mut client_output_buffer) - /// .with_output(&mut server_output_buffer) - /// .execute(&mut ntlm) + /// .with_output(&mut server_output_buffer); + /// let server_result = ntlm.accept_security_context_impl(builder) + /// .unwrap() + /// .resolve_to_result() /// .unwrap(); /// /// if server_result.status == sspi::SecurityStatus::CompleteAndContinue @@ -779,14 +785,16 @@ where /// .resolve_to_result() /// .unwrap(); /// - /// let server_result = server_ntlm + /// let builder = server_ntlm /// .accept_security_context() /// .with_credentials_handle(&mut server_acq_cred_result.credentials_handle) /// .with_context_requirements(sspi::ServerRequestFlags::ALLOCATE_MEMORY) /// .with_target_data_representation(sspi::DataRepresentation::Native) /// .with_input(&mut client_output_buffer) - /// .with_output(&mut server_output_buffer) - /// .execute(&mut server_ntlm) + /// .with_output(&mut server_output_buffer); + /// let server_result = server_ntlm.accept_security_context_impl(builder) + /// .unwrap() + /// .resolve_to_result() /// .unwrap(); /// /// if server_result.status == sspi::SecurityStatus::CompleteAndContinue @@ -894,14 +902,16 @@ where /// .resolve_to_result() /// .unwrap(); /// - /// let server_result = server_ntlm + /// let builder = server_ntlm /// .accept_security_context() /// .with_credentials_handle(&mut server_acq_cred_result.credentials_handle) /// .with_context_requirements(sspi::ServerRequestFlags::ALLOCATE_MEMORY) /// .with_target_data_representation(sspi::DataRepresentation::Native) /// .with_input(&mut client_output_buffer) - /// .with_output(&mut server_output_buffer) - /// .execute(&mut server_ntlm) + /// .with_output(&mut server_output_buffer); + /// let server_result = server_ntlm.accept_security_context_impl(builder) + /// .unwrap() + /// .resolve_to_result() /// .unwrap(); /// /// if server_result.status == sspi::SecurityStatus::CompleteAndContinue @@ -1267,10 +1277,10 @@ pub trait SspiImpl { builder: &'a mut FilledInitializeSecurityContext<'a, Self::CredentialsHandle>, ) -> Result>; - fn accept_security_context_impl( - &mut self, - builder: FilledAcceptSecurityContext<'_, Self::CredentialsHandle>, - ) -> Result; + fn accept_security_context_impl<'a>( + &'a mut self, + builder: FilledAcceptSecurityContext<'a, Self::CredentialsHandle>, + ) -> Result>; } pub trait SspiEx diff --git a/src/negotiate.rs b/src/negotiate.rs index 2101754a..7ecda935 100644 --- a/src/negotiate.rs +++ b/src/negotiate.rs @@ -2,7 +2,9 @@ use std::fmt::Debug; use std::net::IpAddr; use std::sync::LazyLock; -use crate::generator::{GeneratorChangePassword, GeneratorInitSecurityContext, YieldPointLocal}; +use crate::generator::{ + GeneratorAcceptSecurityContext, GeneratorChangePassword, GeneratorInitSecurityContext, YieldPointLocal, +}; use crate::kdc::detect_kdc_url; use crate::kerberos::client::generators::get_client_principal_realm; use crate::ntlm::NtlmConfig; @@ -27,7 +29,7 @@ pub static PACKAGE_INFO: LazyLock = LazyLock::new(|| PackageInfo { }); pub trait ProtocolConfig: Debug + Send + Sync { - fn new_client(&self) -> Result; + fn new_instance(&self) -> Result; fn box_clone(&self) -> Box; } @@ -98,6 +100,7 @@ pub struct Negotiate { package_list: Option, auth_identity: Option, client_computer_name: String, + is_client: bool, } struct PackageListConfig { @@ -107,10 +110,29 @@ struct PackageListConfig { } impl Negotiate { - pub fn new(config: NegotiateConfig) -> Result { - let mut protocol = config.protocol_config.new_client()?; + pub fn new_client(config: NegotiateConfig) -> Result { + let is_client = true; + let mut protocol = config.protocol_config.new_instance()?; + if let Some(filtered_protocol) = + Self::filter_protocol(&protocol, &config.package_list, &config.client_computer_name, is_client)? + { + protocol = filtered_protocol; + } + + Ok(Negotiate { + protocol, + package_list: config.package_list, + auth_identity: None, + client_computer_name: config.client_computer_name, + is_client, + }) + } + + pub fn new_server(config: NegotiateConfig) -> Result { + let is_client = false; + let mut protocol = config.protocol_config.new_instance()?; if let Some(filtered_protocol) = - Self::filter_protocol(&protocol, &config.package_list, &config.client_computer_name)? + Self::filter_protocol(&protocol, &config.package_list, &config.client_computer_name, is_client)? { protocol = filtered_protocol; } @@ -120,6 +142,7 @@ impl Negotiate { package_list: config.package_list, auth_identity: None, client_computer_name: config.client_computer_name, + is_client, }) } @@ -159,9 +182,12 @@ impl Negotiate { } } - if let Some(filtered_protocol) = - Self::filter_protocol(&self.protocol, &self.package_list, &self.client_computer_name)? - { + if let Some(filtered_protocol) = Self::filter_protocol( + &self.protocol, + &self.package_list, + &self.client_computer_name, + self.is_client, + )? { self.protocol = filtered_protocol; } @@ -198,6 +224,7 @@ impl Negotiate { negotiated_protocol: &NegotiatedProtocol, package_list: &Option, client_computer_name: &str, + is_client: bool, ) -> Result> { let mut filtered_protocol = None; let PackageListConfig { @@ -231,8 +258,21 @@ impl Negotiate { kdc_url: None, }; - let kerberos_client = Kerberos::new_client_from_config(config)?; - filtered_protocol = Some(NegotiatedProtocol::Kerberos(kerberos_client)); + if is_client { + let kerberos_client = Kerberos::new_client_from_config(config)?; + filtered_protocol = Some(NegotiatedProtocol::Kerberos(kerberos_client)); + } else { + // Aborting because we need an additional data (ServerProperties object) to create the server-side Kerberos instance. + error!( + ?package_list, + "NTLM protocol has been negotiated but it is disabled in package_list." + ); + + return Err(Error::new( + ErrorKind::InternalError, + "NTLM protocol has been negotiated but it is disabled in package_list", + )); + } } } } @@ -483,31 +523,13 @@ impl SspiImpl for Negotiate { } #[instrument(ret, level = "debug", fields(protocol = self.protocol.protocol_name()), skip_all)] - fn accept_security_context_impl( - &mut self, - builder: builders::FilledAcceptSecurityContext<'_, Self::CredentialsHandle>, - ) -> Result { - match &mut self.protocol { - NegotiatedProtocol::Pku2u(pku2u) => { - let mut creds_handle = if let Some(creds_handle) = &builder.credentials_handle { - creds_handle.as_ref().and_then(|c| c.clone().auth_identity()) - } else { - None - }; - let new_builder = builder.full_transform(Some(&mut creds_handle)); - new_builder.execute(pku2u) - } - NegotiatedProtocol::Kerberos(kerberos) => kerberos.accept_security_context_impl(builder), - NegotiatedProtocol::Ntlm(ntlm) => { - let mut creds_handle = if let Some(creds_handle) = &builder.credentials_handle { - creds_handle.as_ref().and_then(|c| c.clone().auth_identity()) - } else { - None - }; - let new_builder = builder.full_transform(Some(&mut creds_handle)); - new_builder.execute(ntlm) - } - } + fn accept_security_context_impl<'a>( + &'a mut self, + builder: builders::FilledAcceptSecurityContext<'a, Self::CredentialsHandle>, + ) -> Result> { + Ok(GeneratorAcceptSecurityContext::new(move |mut yield_point| async move { + self.accept_security_context_impl(&mut yield_point, builder).await + })) } fn initialize_security_context_impl<'a>( @@ -538,7 +560,35 @@ impl<'a> Negotiate { } } - #[instrument(ret, level = "debug", fields(protocol = self.protocol.protocol_name()), skip_all)] + pub(crate) async fn accept_security_context_impl( + &mut self, + yield_point: &mut YieldPointLocal, + builder: builders::FilledAcceptSecurityContext<'a, ::CredentialsHandle>, + ) -> Result { + match &mut self.protocol { + NegotiatedProtocol::Pku2u(pku2u) => { + let mut creds_handle = builder + .credentials_handle + .as_ref() + .and_then(|creds| (*creds).clone()) + .and_then(|creds_handle| creds_handle.auth_identity()); + let new_builder = builder.full_transform(Some(&mut creds_handle)); + pku2u.accept_security_context_impl(yield_point, new_builder).await + } + NegotiatedProtocol::Kerberos(kerberos) => kerberos.accept_security_context_impl(yield_point, builder).await, + NegotiatedProtocol::Ntlm(ntlm) => { + let mut creds_handle = builder + .credentials_handle + .as_ref() + .and_then(|creds| (*creds).clone()) + .and_then(|creds_handle| creds_handle.auth_identity()); + let new_builder = builder.full_transform(Some(&mut creds_handle)); + ntlm.accept_security_context_impl(new_builder) + } + } + } + + #[instrument(ret, fields(protocol = self.protocol.protocol_name()), skip_all)] pub(crate) async fn initialize_security_context_impl( &'a mut self, yield_point: &mut YieldPointLocal, diff --git a/src/network_client.rs b/src/network_client.rs index c7753dfa..b2fdf2a0 100644 --- a/src/network_client.rs +++ b/src/network_client.rs @@ -40,7 +40,7 @@ pub trait AsyncNetworkClient: Send + Sync { fn send<'a>( &'a mut self, network_request: &'a NetworkRequest, - ) -> Pin>> + 'a>>; + ) -> Pin>> + Send + 'a>>; } pub trait NetworkClient: Send + Sync { diff --git a/src/ntlm/config.rs b/src/ntlm/config.rs index 306c84f8..f38a55e3 100644 --- a/src/ntlm/config.rs +++ b/src/ntlm/config.rs @@ -18,7 +18,7 @@ impl NtlmConfig { } impl ProtocolConfig for NtlmConfig { - fn new_client(&self) -> Result { + fn new_instance(&self) -> Result { Ok(NegotiatedProtocol::Ntlm(Ntlm::with_config(Clone::clone(self)))) } diff --git a/src/ntlm/mod.rs b/src/ntlm/mod.rs index 9d246c93..ef7bde93 100644 --- a/src/ntlm/mod.rs +++ b/src/ntlm/mod.rs @@ -14,7 +14,7 @@ use messages::{client, server}; pub use self::config::NtlmConfig; use super::channel_bindings::ChannelBindings; use crate::crypto::{compute_hmac_md5, Rc4, HASH_SIZE}; -use crate::generator::GeneratorInitSecurityContext; +use crate::generator::{GeneratorAcceptSecurityContext, GeneratorInitSecurityContext}; use crate::utils::{extract_encrypted_data, save_decrypted_data}; use crate::{ AcceptSecurityContextResult, AcquireCredentialsHandleResult, AuthIdentity, AuthIdentityBuffers, BufferType, @@ -243,9 +243,28 @@ impl SspiImpl for Ntlm { } #[instrument(level = "debug", ret, fields(state = ?self.state), skip(self, builder))] - fn accept_security_context_impl( + fn accept_security_context_impl<'a>( + &'a mut self, + builder: FilledAcceptSecurityContext<'a, Self::CredentialsHandle>, + ) -> crate::Result> { + Ok(GeneratorAcceptSecurityContext::new(move |_yield_point| async move { + self.accept_security_context_impl(builder) + })) + } + + #[instrument(level = "debug", ret, fields(state = ?self.state), skip_all)] + fn initialize_security_context_impl( &mut self, - builder: FilledAcceptSecurityContext<'_, Self::CredentialsHandle>, + builder: &mut FilledInitializeSecurityContext<'_, Self::CredentialsHandle>, + ) -> crate::Result { + Ok(self.initialize_security_context_impl(builder).into()) + } +} + +impl Ntlm { + pub(crate) fn accept_security_context_impl( + &mut self, + builder: FilledAcceptSecurityContext<'_, ::CredentialsHandle>, ) -> crate::Result { let input = builder .input @@ -286,16 +305,6 @@ impl SspiImpl for Ntlm { }) } - #[instrument(ret, level = "debug", fields(state = ?self.state), skip_all)] - fn initialize_security_context_impl( - &mut self, - builder: &mut FilledInitializeSecurityContext<'_, Self::CredentialsHandle>, - ) -> crate::Result { - Ok(self.initialize_security_context_impl(builder).into()) - } -} - -impl Ntlm { pub(crate) fn initialize_security_context_impl( &mut self, builder: &mut FilledInitializeSecurityContext<'_, ::CredentialsHandle>, diff --git a/src/ntlm/test.rs b/src/ntlm/test.rs index 4ecf44b7..dd45e638 100644 --- a/src/ntlm/test.rs +++ b/src/ntlm/test.rs @@ -466,15 +466,15 @@ fn accept_security_context_wrong_state_negotiate() { context.state = NtlmState::Negotiate; let mut output = vec![SecurityBuffer::new(Vec::new(), BufferType::Token)]; - - assert!(context + let mut credentials = Some(TEST_CREDENTIALS.clone()); + let builder = context .accept_security_context() - .with_credentials_handle(&mut Some(TEST_CREDENTIALS.clone())) + .with_credentials_handle(&mut credentials) .with_context_requirements(ServerRequestFlags::empty()) .with_target_data_representation(DataRepresentation::Native) - .with_output(&mut output) - .execute(&mut context) - .is_err()); + .with_output(&mut output); + + assert!(context.accept_security_context_impl(builder).is_err()); assert_eq!(context.state, NtlmState::Negotiate); } @@ -484,15 +484,15 @@ fn accept_security_context_wrong_state_challenge() { context.state = NtlmState::Challenge; let mut output = vec![SecurityBuffer::new(Vec::new(), BufferType::Token)]; - - assert!(context + let mut credentials = Some(TEST_CREDENTIALS.clone()); + let builder = context .accept_security_context() - .with_credentials_handle(&mut Some(TEST_CREDENTIALS.clone())) + .with_credentials_handle(&mut credentials) .with_context_requirements(ServerRequestFlags::empty()) .with_target_data_representation(DataRepresentation::Native) - .with_output(&mut output) - .execute(&mut context) - .is_err()); + .with_output(&mut output); + + assert!(context.accept_security_context_impl(builder).is_err()); assert_eq!(context.state, NtlmState::Challenge); } @@ -502,15 +502,15 @@ fn accept_security_context_wrong_state_completion() { context.state = NtlmState::Completion; let mut output = vec![SecurityBuffer::new(Vec::new(), BufferType::Token)]; - - assert!(context + let mut credentials = Some(TEST_CREDENTIALS.clone()); + let builder = context .accept_security_context() - .with_credentials_handle(&mut Some(TEST_CREDENTIALS.clone())) + .with_credentials_handle(&mut credentials) .with_context_requirements(ServerRequestFlags::empty()) .with_target_data_representation(DataRepresentation::Native) - .with_output(&mut output) - .execute(&mut context) - .is_err()); + .with_output(&mut output); + + assert!(context.accept_security_context_impl(builder).is_err()); assert_eq!(context.state, NtlmState::Completion); } @@ -520,15 +520,15 @@ fn accept_security_context_wrong_state_final() { context.state = NtlmState::Final; let mut output = vec![SecurityBuffer::new(Vec::new(), BufferType::Token)]; - - assert!(context + let mut credentials = Some(TEST_CREDENTIALS.clone()); + let builder = context .accept_security_context() - .with_credentials_handle(&mut Some(TEST_CREDENTIALS.clone())) + .with_credentials_handle(&mut credentials) .with_context_requirements(ServerRequestFlags::empty()) .with_target_data_representation(DataRepresentation::Native) - .with_output(&mut output) - .execute(&mut context) - .is_err()); + .with_output(&mut output); + + assert!(context.accept_security_context_impl(builder).is_err()); assert_eq!(context.state, NtlmState::Final); } @@ -537,24 +537,24 @@ fn accept_security_context_reads_negotiate_message() { let mut context = Ntlm::new(); context.state = NtlmState::Initial; - let input = SecurityBuffer::new( + let mut input = [SecurityBuffer::new( vec![ 0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00, 0x97, 0x82, 0x08, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, ], BufferType::Token, - ); + )]; let mut output = vec![SecurityBuffer::new(Vec::with_capacity(1024), BufferType::Token)]; - - let result = context + let mut credentials = Some(TEST_CREDENTIALS.clone()); + let builder = context .accept_security_context() - .with_credentials_handle(&mut Some(TEST_CREDENTIALS.clone())) + .with_credentials_handle(&mut credentials) .with_context_requirements(ServerRequestFlags::empty()) .with_target_data_representation(DataRepresentation::Native) .with_output(&mut output) - .with_input(&mut [input]) - .execute(&mut context) - .unwrap(); + .with_input(&mut input); + + let result = context.accept_security_context_impl(builder).unwrap(); assert_eq!(result.status, SecurityStatus::ContinueNeeded); assert_ne!(context.state, NtlmState::Challenge); } @@ -564,24 +564,24 @@ fn accept_security_context_writes_challenge_message() { let mut context = Ntlm::new(); context.state = NtlmState::Initial; - let input = SecurityBuffer::new( + let mut input = [SecurityBuffer::new( vec![ 0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 0x00, 0x00, 0x97, 0x82, 0x08, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, ], BufferType::Token, - ); + )]; let mut output = vec![SecurityBuffer::new(Vec::with_capacity(1024), BufferType::Token)]; - let result = context + let mut credentials = Some(TEST_CREDENTIALS.clone()); + let builder = context .accept_security_context() - .with_credentials_handle(&mut Some(TEST_CREDENTIALS.clone())) + .with_credentials_handle(&mut credentials) .with_context_requirements(ServerRequestFlags::empty()) .with_target_data_representation(DataRepresentation::Native) .with_output(&mut output) - .with_input(&mut [input]) - .execute(&mut context) - .unwrap(); + .with_input(&mut input); + let result = context.accept_security_context_impl(builder).unwrap(); assert_eq!(result.status, SecurityStatus::ContinueNeeded); let output = SecurityBuffer::find_buffer(&output, BufferType::Token).unwrap(); assert_eq!(context.state, NtlmState::Authenticate); @@ -600,7 +600,7 @@ fn accept_security_context_reads_authenticate() { 0, )); - let input = SecurityBuffer::new( + let mut input = [SecurityBuffer::new( vec![ 0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, // signature 0x03, 0x00, 0x00, 0x00, // message type @@ -623,19 +623,18 @@ fn accept_security_context_reads_authenticate() { 0x0f, // encrypted key ], BufferType::Token, - ); + )]; let mut output = vec![SecurityBuffer::new(Vec::with_capacity(1024), BufferType::Token)]; - - let result = context + let mut credentials = Some(TEST_CREDENTIALS.clone()); + let builder = context .accept_security_context() - .with_credentials_handle(&mut Some(TEST_CREDENTIALS.clone())) + .with_credentials_handle(&mut credentials) .with_context_requirements(ServerRequestFlags::empty()) .with_target_data_representation(DataRepresentation::Native) .with_output(&mut output) - .with_input(&mut [input]) - .execute(&mut context) - .unwrap(); + .with_input(&mut input); + let result = context.accept_security_context_impl(builder).unwrap(); assert_eq!(result.status, SecurityStatus::CompleteNeeded); assert_eq!(context.state, NtlmState::Completion); } @@ -647,15 +646,15 @@ fn accept_security_context_fails_on_empty_output_on_negotiate_state() { context.state = NtlmState::Initial; let mut output = vec![SecurityBuffer::new(Vec::new(), BufferType::Token)]; - - assert!(context + let mut credentials = Some(TEST_CREDENTIALS.clone()); + let builder = context .accept_security_context() - .with_credentials_handle(&mut Some(TEST_CREDENTIALS.clone())) + .with_credentials_handle(&mut credentials) .with_context_requirements(ServerRequestFlags::empty()) .with_target_data_representation(DataRepresentation::Native) - .with_output(&mut output) - .execute(&mut context) - .is_err()); + .with_output(&mut output); + + assert!(context.accept_security_context_impl(builder).is_err()); } #[test] diff --git a/src/pk_init.rs b/src/pk_init.rs index 0c58b80b..3d5188a3 100644 --- a/src/pk_init.rs +++ b/src/pk_init.rs @@ -28,7 +28,7 @@ use serde::{Deserialize, Serialize}; use sha1::{Digest, Sha1}; use time::OffsetDateTime; -use crate::kerberos::client::generators::MAX_MICROSECONDS_IN_SECOND; +use crate::kerberos::client::generators::MAX_MICROSECONDS; use crate::{Error, ErrorKind, Result}; /// [Generation of Client Request](https://www.rfc-editor.org/rfc/rfc4556.html#section-3.2.1) @@ -101,8 +101,8 @@ pub fn generate_pa_datas_for_as_req(options: &GenerateAsPaDataOptions<'_>) -> Re let current_date = OffsetDateTime::now_utc(); let mut microseconds = current_date.microsecond(); - if microseconds > MAX_MICROSECONDS_IN_SECOND { - microseconds = MAX_MICROSECONDS_IN_SECOND; + if microseconds > MAX_MICROSECONDS { + microseconds = MAX_MICROSECONDS; } // [Generation of Client Request](https://www.rfc-editor.org/rfc/rfc4556.html#section-3.2.1) diff --git a/src/pku2u/config.rs b/src/pku2u/config.rs index faaa3b37..cd53db82 100644 --- a/src/pku2u/config.rs +++ b/src/pku2u/config.rs @@ -36,7 +36,7 @@ impl Pku2uConfig { } impl ProtocolConfig for Pku2uConfig { - fn new_client(&self) -> Result { + fn new_instance(&self) -> Result { Ok(NegotiatedProtocol::Pku2u(Pku2u::new_client_from_config(Clone::clone( self, ))?)) diff --git a/src/pku2u/generators.rs b/src/pku2u/generators.rs index 5eae938f..2d9209a3 100644 --- a/src/pku2u/generators.rs +++ b/src/pku2u/generators.rs @@ -32,7 +32,7 @@ use time::OffsetDateTime; use super::Pku2uConfig; use crate::crypto::compute_md5_channel_bindings_hash; use crate::kerberos::client::generators::{ - AuthenticatorChecksumExtension, ChecksumOptions, EncKey, GenerateAuthenticatorOptions, MAX_MICROSECONDS_IN_SECOND, + AuthenticatorChecksumExtension, ChecksumOptions, EncKey, GenerateAuthenticatorOptions, MAX_MICROSECONDS, }; use crate::pk_init::DhParameters; use crate::{Error, ErrorKind, Result, KERBEROS_VERSION}; @@ -217,8 +217,8 @@ pub fn generate_authenticator(options: GenerateAuthenticatorOptions) -> Result MAX_MICROSECONDS_IN_SECOND { - microseconds = MAX_MICROSECONDS_IN_SECOND; + if microseconds > MAX_MICROSECONDS { + microseconds = MAX_MICROSECONDS; } let lsap_token = LsapTokenInfoIntegrity { diff --git a/src/pku2u/mod.rs b/src/pku2u/mod.rs index e7c9610a..e20ea224 100644 --- a/src/pku2u/mod.rs +++ b/src/pku2u/mod.rs @@ -36,13 +36,13 @@ use self::generators::{ generate_neg, generate_neg_token_init, generate_neg_token_targ, generate_pku2u_nego_req, generate_server_dh_parameters, WELLKNOWN_REALM, }; -use crate::builders::ChangePassword; -use crate::generator::GeneratorInitSecurityContext; +use crate::builders::{ChangePassword, FilledAcceptSecurityContext}; +use crate::generator::{GeneratorAcceptSecurityContext, GeneratorInitSecurityContext, YieldPointLocal}; +use crate::kerberos::client::extractors::extract_sub_session_key_from_ap_rep; use crate::kerberos::client::generators::{ generate_ap_req, generate_as_req, generate_as_req_kdc_body, ChecksumOptions, EncKey, GenerateAsReqOptions, GenerateAuthenticatorOptions, }; -use crate::kerberos::server::extractors::extract_sub_session_key_from_ap_rep; use crate::kerberos::{EncryptionParams, DEFAULT_ENCRYPTION_TYPE, MAX_SIGNATURE, RRC, SECURITY_TRAILER}; use crate::pk_init::{ extract_server_dh_public_key, generate_pa_datas_for_as_req, DhParameters, GenerateAsPaDataOptions, @@ -394,15 +394,14 @@ impl SspiImpl for Pku2u { }) } - #[instrument(level = "debug", ret, fields(state = ?self.state), skip(self, _builder))] - fn accept_security_context_impl( - &mut self, - _builder: crate::builders::FilledAcceptSecurityContext<'_, Self::CredentialsHandle>, - ) -> Result { - Err(Error::new( - ErrorKind::UnsupportedFunction, - "accept_security_context_impl is not implemented yet", - )) + #[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> { + Ok(GeneratorAcceptSecurityContext::new(move |mut yield_point| async move { + self.accept_security_context_impl(&mut yield_point, builder).await + })) } fn initialize_security_context_impl( @@ -414,6 +413,17 @@ impl SspiImpl for Pku2u { } impl Pku2u { + pub(crate) async fn accept_security_context_impl( + &mut self, + _yield_point: &mut YieldPointLocal, + _builder: FilledAcceptSecurityContext<'_, ::CredentialsHandle>, + ) -> crate::Result { + Err(Error::new( + ErrorKind::UnsupportedFunction, + "accept_security_context_impl is not implemented yet", + )) + } + #[instrument(ret, level = "debug", fields(state = ?self.state), skip_all)] pub(crate) fn initialize_security_context_impl( &mut self, diff --git a/tests/sspi/client_server/credssp.rs b/tests/sspi/client_server/credssp.rs index fafaa43a..c89562c1 100644 --- a/tests/sspi/client_server/credssp.rs +++ b/tests/sspi/client_server/credssp.rs @@ -1,46 +1,86 @@ +use std::collections::HashSet; use std::mem; -use sspi::credssp::{ClientMode, ClientState, CredSspClient, CredSspMode, CredSspServer, ServerState, TsRequest}; +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, Secret, Username}; +use sspi::{AuthIdentity, Credentials, CredentialsBuffers, KerberosConfig, Secret, Username}; +use url::Url; +use super::kerberos::kdc::{KdcMock, Validators, CLIENT_COMPUTER_NAME, KDC_URL, MAX_TIME_SKEW}; +use super::kerberos::network_client::NetworkClientMock; +use super::kerberos::{init_krb_environment, KrbEnvironment}; 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>, + 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 run_credssp() { +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 public_key = [ - 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, - ]; let mut client = CredSspClient::new( - public_key.to_vec(), + PUBLIC_KEY.to_vec(), credentials.clone(), CredSspMode::WithCredentials, ClientMode::Ntlm(NtlmConfig { @@ -49,35 +89,70 @@ fn run_credssp() { "TERMSRV/DESKTOP-8F33RFH.example.com".to_owned(), ) .unwrap(); + let mut server = CredSspServer::new( - public_key.to_vec(), + PUBLIC_KEY.to_vec(), CredentialsProxyImpl::new(&auth_identity), - ClientMode::Ntlm(NtlmConfig { + ServerMode::Ntlm(NtlmConfig { client_computer_name: Some("DESKTOP-3D83IAN.example.com".to_owned()), }), ) .unwrap(); - let mut ts_request = TsRequest::default(); + let mut network_client = NetworkClientMock { kdc: KdcMock::empty() }; - for _ in 0..3 { - ts_request = match client - .process(mem::take(&mut ts_request)) - .resolve_with_default_network_client() - .unwrap() - { - ClientState::ReplyNeeded(ts_request) => ts_request, - ClientState::FinalMessage(ts_request) => ts_request, - }; + run_credssp(&mut client, &mut server, &auth_identity, &mut network_client); +} - match server.process(ts_request).unwrap() { - ServerState::ReplyNeeded(server_ts_request) => ts_request = server_ts_request, - ServerState::Finished(received_auth_identity) => { - assert_eq!(auth_identity, received_auth_identity); - return; - } - }; - } +#[test] +fn credssp_kerberos() { + let KrbEnvironment { + realm, + credentials, + keys, + users, + target_name, + target_service_name, + } = init_krb_environment(); + let auth_identity = credentials.clone().auth_identity().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: Some(CLIENT_COMPUTER_NAME.into()), + }; + + let server_config = KerberosConfig { + kdc_url: Some(Url::parse(KDC_URL).unwrap()), + client_computer_name: Some(CLIENT_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, + user: Some(CredentialsBuffers::try_from(credentials.clone()).unwrap()), + client: None, + authenticators_cache: HashSet::new(), + }; + + let mut client = CredSspClient::new( + PUBLIC_KEY.to_vec(), + credentials, + CredSspMode::WithCredentials, + ClientMode::Kerberos(client_config), + target_name, + ) + .unwrap(); + + let mut server = CredSspServer::new( + PUBLIC_KEY.to_vec(), + CredentialsProxyImpl::new(&auth_identity), + ServerMode::Kerberos(Box::new((server_config, server_properties))), + ) + .unwrap(); - panic!("CredSSP authentication should not exceed 3 steps") + run_credssp(&mut client, &mut server, &auth_identity, &mut network_client); } diff --git a/tests/sspi/client_server/kerberos/kdc.rs b/tests/sspi/client_server/kerberos/kdc.rs new file mode 100644 index 00000000..fdb0aa03 --- /dev/null +++ b/tests/sspi/client_server/kerberos/kdc.rs @@ -0,0 +1,627 @@ +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, ExplicitContextTag10, ExplicitContextTag12, + ExplicitContextTag2, ExplicitContextTag3, ExplicitContextTag4, ExplicitContextTag5, ExplicitContextTag6, + ExplicitContextTag7, ExplicitContextTag9, 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::OsRng; +use rand::{Rng, RngCore}; +use sspi::kerberos::KERBEROS_VERSION; +use time::{Duration, OffsetDateTime}; + +pub const MAX_TIME_SKEW: StdDuration = StdDuration::from_secs(3); +pub const KDC_URL: &str = "tcp://192.168.1.103:88"; +pub const CLIENT_COMPUTER_NAME: &str = "DESKTOP-8F33RFH.example.com"; + +/// Represents user credentials in the internal KDC database. +pub struct PasswordCreds { + /// User's password. + pub password: Vec, + /// 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 struct UserName(pub PrincipalName); + +impl Hash for UserName { + fn hash(&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 struct Validators { + /// Closure that validates the incoming AsReq message. + pub as_req: Box, + /// Closure that validates the incoming TgsReq message. + pub tgs_req: Box, +} + +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 struct KdcMock { + /// Domain's Kerberos realm. + realm: String, + /// Represents Kerberos long-term keys. + keys: HashMap>, + /// Represents users credentials. + users: HashMap, + /// 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 fn empty() -> Self { + Self::default() + } + + /// Creates a new [KdcMock]. + pub fn new( + realm: String, + keys: HashMap>, + users: HashMap, + validators: Validators, + ) -> Self { + Self { + realm, + keys, + users, + validators, + } + } + + fn make_err(sname: PrincipalName, realm: Realm, salt: Option) -> 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, + ) -> Result, 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, + 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 fn as_exchange(&self, as_req: AsReq) -> Result { + (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 = OsRng; + 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 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(rng.gen::().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()), + caadr: 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, + ) -> Result<(Vec, 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 fn tgs_exchange(&self, tgs_req: TgsReq) -> Result { + (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 = OsRng; + let mut session_key = vec![0; cipher.key_size()]; + rng.fill_bytes(&mut session_key); + + 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(rng.gen::().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()), + caadr: 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)), + }), + })) + } +} diff --git a/tests/sspi/client_server/kerberos/mod.rs b/tests/sspi/client_server/kerberos/mod.rs new file mode 100644 index 00000000..e78fcb33 --- /dev/null +++ b/tests/sspi/client_server/kerberos/mod.rs @@ -0,0 +1,417 @@ +#![allow(clippy::result_large_err)] + +pub mod kdc; +pub 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, SecurityBuffer, SecurityStatus, ServerRequestFlags, Sspi, SspiImpl, Username, +}; +use url::Url; + +use crate::client_server::kerberos::kdc::{ + KdcMock, PasswordCreds, UserName, Validators, CLIENT_COMPUTER_NAME, KDC_URL, MAX_TIME_SKEW, +}; +use crate::client_server::kerberos::network_client::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 struct KrbEnvironment { + pub keys: HashMap>, + pub users: HashMap, + 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 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 fn initialize_security_context( + client: &mut SspiContext, + credentials_handle: &mut Option, + flags: ClientRequestFlags, + target_name: &str, + in_token: Vec, + network_client: &mut dyn NetworkClient, +) -> (SecurityStatus, Vec) { + 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 fn accept_security_context( + server: &mut SspiContext, + credentials_handle: &mut Option, + flags: ServerRequestFlags, + in_token: Vec, + network_client: &mut dyn NetworkClient, +) -> (SecurityStatus, Vec) { + 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) +} + +fn run_kerberos( + client: &mut SspiContext, + client_credentials_handle: &mut Option, + client_flags: ClientRequestFlags, + target_name: &str, + + server: &mut SspiContext, + server_credentials_handle: &mut Option, + server_flags: ServerRequestFlags, + + network_client: &mut dyn NetworkClient, +) { + let mut client_in_token = Vec::new(); + + for _ in 0..3 { + let (client_status, token) = initialize_security_context( + client, + client_credentials_handle, + client_flags, + target_name, + client_in_token, + network_client, + ); + + let (_, token) = + accept_security_context(server, server_credentials_handle, server_flags, token, network_client); + client_in_token = token; + + if client_status == SecurityStatus::Ok { + test_encryption(client, server); + test_stream_buffer_encryption(client, server); + test_rpc_request_encryption(client, server); + return; + } + } + + panic!("Kerberos authentication should not exceed 3 steps"); +} + +#[test] +fn kerberos_auth() { + let KrbEnvironment { + realm, + credentials, + keys, + users, + target_name, + target_service_name, + } = init_krb_environment(); + + let ticket_decryption_key = keys.get(&UserName(target_service_name.clone())).unwrap().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("tcp://192.168.1.103:88").unwrap()), + client_computer_name: Some("DESKTOP-I7E8EFA.example.com".into()), + }; + let kerberos_client = Kerberos::new_client_from_config(client_config).unwrap(); + + let server_config = KerberosConfig { + kdc_url: Some(Url::parse("tcp://192.168.1.103:88").unwrap()), + client_computer_name: Some("DESKTOP-8F33RFH.example.com".into()), + }; + let server_properties = ServerProperties { + mech_types: MechTypeList::from(Vec::new()), + max_time_skew: MAX_TIME_SKEW, + ticket_decryption_key: Some(ticket_decryption_key), + service_name: target_service_name, + 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, + ); +} + +#[test] +fn kerberos_u2u_auth() { + let KrbEnvironment { + realm, + credentials, + keys, + users, + target_name, + target_service_name, + } = init_krb_environment(); + + 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 successfully negotiated Kerberos U2U auth. + + let kdc_options = tgs_req.0.req_body.kdc_options.0 .0.as_bytes(); + // KDC options must have enc-tkt-in-skey enabled. + assert_eq!(kdc_options[4], 0x08, "the enc-tkt-in-skey KDC option is not enabled"); + + if let Some(tickets) = tgs_req.0.req_body.0.additional_tickets.0.as_ref() { + assert!( + !tickets.0 .0.is_empty(), + "TgsReq must have at least one additional ticket: TGT from the application service" + ); + } else { + panic!("TgsReq must have at least one additional ticket: TGT from the application service"); + } + }), + }, + ); + let mut network_client = NetworkClientMock { kdc }; + + let client_config = KerberosConfig { + kdc_url: Some(Url::parse(KDC_URL).unwrap()), + client_computer_name: Some(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: Some(CLIENT_COMPUTER_NAME.into()), + }; + let server_properties = ServerProperties { + mech_types: MechTypeList::default(), + max_time_skew: MAX_TIME_SKEW, + ticket_decryption_key: None, + service_name: target_service_name, + 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::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::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, + ); +} diff --git a/tests/sspi/client_server/kerberos/network_client.rs b/tests/sspi/client_server/kerberos/network_client.rs new file mode 100644 index 00000000..d17b6a0d --- /dev/null +++ b/tests/sspi/client_server/kerberos/network_client.rs @@ -0,0 +1,39 @@ +use sspi::generator::NetworkRequest; +use sspi::network_client::NetworkClient; +use sspi::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 struct NetworkClientMock { + pub kdc: KdcMock, +} + +impl NetworkClient for NetworkClientMock { + fn send(&self, request: &NetworkRequest) -> Result> { + 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) + } +} diff --git a/tests/sspi/client_server/mod.rs b/tests/sspi/client_server/mod.rs index 19148a02..1f40fbfb 100644 --- a/tests/sspi/client_server/mod.rs +++ b/tests/sspi/client_server/mod.rs @@ -1,6 +1,100 @@ #![cfg(feature = "network_client")] // The network_client feature is required for the client_server tests. mod credssp; +mod kerberos; mod ntlm; -// TODO(@TheBestTvarynka): add Kerberos test when the Kerberos server-side is implemented. +use sspi::credssp::SspiContext; +use sspi::{EncryptionFlags, SecurityBufferFlags, SecurityBufferRef, Sspi}; + +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, 0) + .unwrap(); + server.decrypt_message(&mut message, 0).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, 0) + .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, 0).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, 0) + .unwrap(); + + assert_eq!(header[..], message[0].data()[..]); + assert_eq!(trailer[..], message[2].data()[..]); + + server.decrypt_message(&mut message, 0).unwrap(); + + assert_eq!(header[..], message[0].data()[..]); + assert_eq!(message[1].data(), plaintext); + assert_eq!(trailer[..], message[2].data()[..]); +} diff --git a/tests/sspi/client_server/ntlm.rs b/tests/sspi/client_server/ntlm.rs index 11ddc665..285ad77f 100644 --- a/tests/sspi/client_server/ntlm.rs +++ b/tests/sspi/client_server/ntlm.rs @@ -3,28 +3,11 @@ use sspi::credssp::SspiContext; use sspi::ntlm::NtlmConfig; use sspi::{ AcquireCredentialsHandleResult, AuthIdentity, BufferType, ClientRequestFlags, CredentialUse, Credentials, - DataRepresentation, EncryptionFlags, InitializeSecurityContextResult, Ntlm, Secret, SecurityBuffer, - SecurityBufferRef, SecurityStatus, ServerRequestFlags, Sspi, Username, + DataRepresentation, InitializeSecurityContextResult, Ntlm, Secret, SecurityBuffer, SecurityStatus, + ServerRequestFlags, Sspi, Username, }; -fn test_ntlm_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, 0) - .unwrap(); - server.decrypt_message(&mut message, 0).unwrap(); - - assert_eq!(plain_message, message[1].data()); -} +use crate::client_server::{test_encryption, test_rpc_request_encryption, test_stream_buffer_encryption}; fn run_ntlm(config: NtlmConfig) { let credentials = Credentials::AuthIdentity(AuthIdentity { @@ -78,20 +61,21 @@ fn run_ntlm(config: NtlmConfig) { input_token[0].buffer.clear(); - server + 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) - .execute(&mut server) - .unwrap(); + .with_output(&mut input_token); + server.accept_security_context_sync(builder).unwrap(); output_token[0].buffer.clear(); if status == SecurityStatus::Ok { - test_ntlm_encryption(&mut client, &mut server); + test_encryption(&mut client, &mut server); + test_stream_buffer_encryption(&mut client, &mut server); + test_rpc_request_encryption(&mut client, &mut server); return; } } diff --git a/tests/sspi/common.rs b/tests/sspi/common.rs index 764cbb47..a7eb4aae 100644 --- a/tests/sspi/common.rs +++ b/tests/sspi/common.rs @@ -121,14 +121,14 @@ where server_output = vec![SecurityBuffer::new(Vec::new(), BufferType::Token)]; - let server_result = server + 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) - .execute(server)?; + .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 {