diff --git a/.evergreen/config.yml b/.evergreen/config.yml index c7a03538c..85dd6b1b4 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -274,6 +274,14 @@ buildvariants: tasks: - test-gssapi-auth + - name: gssapi-auth-windows + display_name: "GSSAPI Authentication - Windows" + patchable: true + run_on: + - windows-64-vs2017-small + tasks: + - test-gssapi-auth + - name: x509-auth display_name: "x509 Authentication" patchable: false @@ -1405,7 +1413,7 @@ functions: type: test params: binary: bash - working_dir: ${PROJECT_DIRECTORY} + working_dir: src args: - .evergreen/run-gssapi-tests.sh include_expansions_in_env: diff --git a/.evergreen/run-gssapi-tests.sh b/.evergreen/run-gssapi-tests.sh index 6957a705d..1ba01e7d7 100644 --- a/.evergreen/run-gssapi-tests.sh +++ b/.evergreen/run-gssapi-tests.sh @@ -16,50 +16,60 @@ FEATURE_FLAGS+=("gssapi-auth") set +o errexit -# Create a krb5 config file with relevant -touch krb5.conf -echo "[realms] - $SASL_REALM = { - kdc = $SASL_HOST - admin_server = $SASL_HOST - } - - $SASL_REALM_CROSS = { - kdc = $SASL_HOST - admin_server = $SASL_HOST - } - -[domain_realm] - .$SASL_DOMAIN = $SASL_REALM - $SASL_DOMAIN = $SASL_REALM -" > krb5.conf - -export KRB5_CONFIG=krb5.conf - -# Authenticate the user principal in the KDC before running the e2e test -echo "Authenticating $PRINCIPAL" -echo "$SASL_PASS" | kinit -p $PRINCIPAL -klist +# On Windows, `kinit`/`kdestroy` and other krb5 config settings are +# not available, nor are they required steps. Windows uses SSPI which +# is similar to but distinct from (KRB5) GSSAPI. Therefore, we only +# run the following steps if we are not on Windows. +if [[ "Windows_NT" != "$OSTYPE" ]]; then + # Create a krb5 config file with relevant + touch krb5.conf + echo "[realms] + $SASL_REALM = { + kdc = $SASL_HOST + admin_server = $SASL_HOST + } + + $SASL_REALM_CROSS = { + kdc = $SASL_HOST + admin_server = $SASL_HOST + } + + [domain_realm] + .$SASL_DOMAIN = $SASL_REALM + $SASL_DOMAIN = $SASL_REALM + " > krb5.conf + + export KRB5_CONFIG=krb5.conf + + # Authenticate the user principal in the KDC before running the e2e test + echo "Authenticating $PRINCIPAL" + echo "$SASL_PASS" | kinit -p $PRINCIPAL + klist +fi # Run end-to-end auth tests for "$PRINCIPAL" user TEST_OPTIONS+=("--skip with_service_realm_and_host_options") cargo_test test::auth::gssapi_skip_local -# Unauthenticate -echo "Unauthenticating $PRINCIPAL" -kdestroy +if [[ "Windows_NT" != "$OSTYPE" ]]; then + # Unauthenticate + echo "Unauthenticating $PRINCIPAL" + kdestroy -# Authenticate the alternative user principal in the KDC and run other e2e test -echo "Authenticating $PRINCIPAL_CROSS" -echo "$SASL_PASS_CROSS" | kinit -p $PRINCIPAL_CROSS -klist + # Authenticate the alternative user principal in the KDC and run other e2e test + echo "Authenticating $PRINCIPAL_CROSS" + echo "$SASL_PASS_CROSS" | kinit -p $PRINCIPAL_CROSS + klist +fi TEST_OPTIONS=() cargo_test test::auth::gssapi_skip_local::with_service_realm_and_host_options -# Unauthenticate -echo "Unuthenticating $PRINCIPAL_CROSS" -kdestroy +if [[ "Windows_NT" != "$OSTYPE" ]]; then + # Unauthenticate + echo "Unauthenticating $PRINCIPAL_CROSS" + kdestroy +fi # Run remaining tests cargo_test spec::auth diff --git a/Cargo.lock b/Cargo.lock index d9816bbec..424f7d339 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2129,6 +2129,7 @@ dependencies = [ "typed-builder", "uuid", "webpki-roots 0.26.11", + "windows-sys 0.60.2", "zstd", ] @@ -4096,6 +4097,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -4120,13 +4130,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + [[package]] name = "windows-threading" version = "0.1.0" @@ -4148,6 +4175,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -4160,6 +4193,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -4172,12 +4211,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -4190,6 +4241,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -4202,6 +4259,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -4214,6 +4277,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -4226,6 +4295,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winreg" version = "0.50.0" diff --git a/Cargo.toml b/Cargo.toml index 1c69c1af2..78fe3c9e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,7 +56,7 @@ gcp-oidc = ["dep:reqwest"] gcp-kms = ["dep:reqwest"] # Enable support for GSSAPI (Kerberos) authentication. -gssapi-auth = ["dep:cross-krb5", "dns-resolver"] +gssapi-auth = ["dep:cross-krb5", "dep:windows-sys", "dns-resolver"] zstd-compression = ["dep:zstd"] zlib-compression = ["dep:flate2"] @@ -80,7 +80,6 @@ chrono = { version = "0.4.7", default-features = false, features = [ "clock", "std", ] } -cross-krb5 = { version = "0.4.2", optional = true, default-features = false } derive_more = "0.99.17" derive-where = "1.2.7" flate2 = { version = "1.0", optional = true } @@ -235,6 +234,13 @@ features = ["serde", "serde_json-1"] rustdoc-args = ["--cfg", "docsrs"] all-features = true +# Target-specific dependencies for GSSAPI authentication +[target.'cfg(not(windows))'.dependencies] +cross-krb5 = { version = "0.4.2", optional = true, default-features = false } + +[target.'cfg(windows)'.dependencies] +windows-sys = { version = "0.60", optional = true, features = ["Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Foundation", "Win32_System", "Win32_System_Rpc"] } + [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(mongodb_internal_tracking_arc)', diff --git a/src/client/auth/gssapi.rs b/src/client/auth/gssapi/mod.rs similarity index 68% rename from src/client/auth/gssapi.rs rename to src/client/auth/gssapi/mod.rs index 835ceaec3..7dd8fcb51 100644 --- a/src/client/auth/gssapi.rs +++ b/src/client/auth/gssapi/mod.rs @@ -1,4 +1,8 @@ -use cross_krb5::{ClientCtx, InitiateFlags, K5Ctx, PendingClientCtx, Step}; +#[cfg(target_os = "windows")] +mod windows; + +#[cfg(not(target_os = "windows"))] +mod nix; use crate::{ bson::Bson, @@ -21,7 +25,7 @@ const SERVICE_REALM: &str = "SERVICE_REALM"; const SERVICE_HOST: &str = "SERVICE_HOST"; #[derive(Debug, Clone)] -pub(crate) struct GssapiProperties { +struct GssapiProperties { pub service_name: String, pub canonicalize_host_name: CanonicalizeHostName, pub service_realm: Option, @@ -29,7 +33,7 @@ pub(crate) struct GssapiProperties { } #[derive(Debug, Default, Clone, PartialEq)] -pub(crate) enum CanonicalizeHostName { +enum CanonicalizeHostName { #[default] None, Forward, @@ -54,11 +58,20 @@ pub(crate) async fn authenticate_stream( .await?; let user_principal = credential.username.clone(); - let (mut authenticator, initial_token) = - GssapiAuthenticator::init(user_principal, properties.clone(), &hostname).await?; - + let service_principal = properties.service_principal_name(&hostname, user_principal.as_ref()); let source = credential.source.as_deref().unwrap_or("$external"); + #[cfg(target_os = "windows")] + let (mut authenticator, initial_token) = windows::SspiAuthenticator::init( + user_principal, + credential.password.clone(), + service_principal, + )?; + + #[cfg(not(target_os = "windows"))] + let (mut authenticator, initial_token) = + nix::GssapiAuthenticator::init(user_principal, service_principal)?; + let command = SaslStart::new( source.to_string(), crate::client::auth::AuthMechanism::Gssapi, @@ -78,7 +91,7 @@ pub(crate) async fn authenticate_stream( // different configurations may require more). for _ in 0..10 { let challenge = payload.as_slice(); - let output_token = authenticator.step(challenge).await?; + let output_token = authenticator.step(challenge)?; // The step may return None, which is a valid final step. We still need to // send a saslContinue command, so we send an empty payload if there is no @@ -190,28 +203,14 @@ impl GssapiProperties { Ok(properties) } -} -struct GssapiAuthenticator { - pending_ctx: Option, - established_ctx: Option, - user_principal: Option, - is_complete: bool, -} - -impl GssapiAuthenticator { - // Initialize the GssapiAuthenticator by creating a PendingClientCtx and - // getting an initial token to send to the server. - async fn init( - user_principal: Option, - properties: GssapiProperties, - hostname: &str, - ) -> Result<(Self, Vec)> { - let service_name: &str = properties.service_name.as_ref(); + fn service_principal_name(self, hostname: &String, user_principal: Option<&String>) -> String { + // Set the service principal name in addition to the user provided properties + let service_name: &str = self.service_name.as_ref(); let mut service_principal = format!("{service_name}/{hostname}"); - if let Some(service_realm) = properties.service_realm.as_ref() { + if let Some(service_realm) = self.service_realm.as_ref() { service_principal = format!("{service_principal}@{service_realm}"); - } else if let Some(user_principal) = user_principal.as_ref() { + } else if let Some(user_principal) = user_principal { if let Some(idx) = user_principal.find('@') { // If no SERVICE_REALM was specified, use realm specified in the // username. Note that `realm` starts with '@'. @@ -220,94 +219,7 @@ impl GssapiAuthenticator { } } - let (pending_ctx, initial_token) = ClientCtx::new( - InitiateFlags::empty(), - user_principal.as_deref(), - &service_principal, - None, // No channel bindings - ) - .map_err(|e| { - Error::authentication_error( - GSSAPI_STR, - &format!("Failed to initialize GSSAPI context: {e}"), - ) - })?; - - Ok(( - Self { - pending_ctx: Some(pending_ctx), - established_ctx: None, - user_principal, - is_complete: false, - }, - initial_token.to_vec(), - )) - } - - // Issue the server provided token to the client context. If the ClientCtx - // is established, an optional final token that must be sent to the server - // may be returned; otherwise another token to pass to the server is - // returned and the client context remains in the pending state. - async fn step(&mut self, challenge: &[u8]) -> Result>> { - if challenge.is_empty() { - Err(Error::authentication_error( - GSSAPI_STR, - "Expected challenge data for GSSAPI continuation", - )) - } else if let Some(pending_ctx) = self.pending_ctx.take() { - match pending_ctx.step(challenge).map_err(|e| { - Error::authentication_error(GSSAPI_STR, &format!("GSSAPI step failed: {e}")) - })? { - Step::Finished((ctx, token)) => { - self.is_complete = true; - self.established_ctx = Some(ctx); - Ok(token.map(|t| t.to_vec())) - } - Step::Continue((ctx, token)) => { - self.pending_ctx = Some(ctx); - Ok(Some(token.to_vec())) - } - } - } else { - Err(Error::authentication_error( - GSSAPI_STR, - "Authentication context not initialized", - )) - } - } - - // Perform the final step of Kerberos authentication by gss_unwrap-ing the - // final server challenge, then wrapping the protocol bytes + user principal. - // The resulting token must be sent to the server. - fn do_unwrap_wrap(&mut self, payload: &[u8]) -> Result> { - if let Some(mut established_ctx) = self.established_ctx.take() { - let _ = established_ctx.unwrap(payload).map_err(|e| { - Error::authentication_error(GSSAPI_STR, &format!("GSSAPI unwrap failed: {e}")) - })?; - - if let Some(user_principal) = self.user_principal.take() { - let bytes: &[u8] = &[0x1, 0x0, 0x0, 0x0]; - let bytes = [bytes, user_principal.as_bytes()].concat(); - let output_token = established_ctx.wrap(false, bytes.as_slice()).map_err(|e| { - Error::authentication_error(GSSAPI_STR, &format!("GSSAPI wrap failed: {e}")) - })?; - Ok(output_token.to_vec()) - } else { - Err(Error::authentication_error( - GSSAPI_STR, - "User principal not specified", - )) - } - } else { - Err(Error::authentication_error( - GSSAPI_STR, - "Authentication context not established", - )) - } - } - - fn is_complete(&self) -> bool { - self.is_complete + service_principal } } diff --git a/src/client/auth/gssapi/nix.rs b/src/client/auth/gssapi/nix.rs new file mode 100644 index 000000000..d9999d169 --- /dev/null +++ b/src/client/auth/gssapi/nix.rs @@ -0,0 +1,111 @@ +use cross_krb5::{ClientCtx, InitiateFlags, K5Ctx, PendingClientCtx, Step}; + +use crate::{ + client::auth::GSSAPI_STR, + error::{Error, Result}, +}; + +#[derive(Default)] +enum CtxState { + #[default] + Empty, + Pending(PendingClientCtx), + Established(ClientCtx), +} + +pub(super) struct GssapiAuthenticator { + ctx: CtxState, + user_principal: String, +} + +impl GssapiAuthenticator { + // Initialize the GssapiAuthenticator by creating a PendingClientCtx and + // getting an initial token to send to the server. + pub(super) fn init( + user_principal: Option, + service_principal: String, + ) -> Result<(Self, Vec)> { + let (pending_ctx, initial_token) = ClientCtx::new( + InitiateFlags::empty(), + user_principal.as_deref(), + &service_principal, + None, // No channel bindings + ) + .map_err(|e| { + Error::authentication_error( + GSSAPI_STR, + &format!("Failed to initialize GSSAPI context: {e}"), + ) + })?; + + let user_principal = user_principal.ok_or_else(|| { + Error::authentication_error(GSSAPI_STR, "User principal not specified") + })?; + + Ok(( + Self { + ctx: CtxState::Pending(pending_ctx), + user_principal, + }, + initial_token.to_vec(), + )) + } + + // Issue the server provided token to the client context. If the ClientCtx + // is established, an optional final token that must be sent to the server + // may be returned; otherwise another token to pass to the server is + // returned and the client context remains in the pending state. + pub(super) fn step(&mut self, challenge: &[u8]) -> Result>> { + if challenge.is_empty() { + Err(Error::authentication_error( + GSSAPI_STR, + "Expected challenge data for GSSAPI continuation", + )) + } else if let CtxState::Pending(pending_ctx) = std::mem::take(&mut self.ctx) { + match pending_ctx.step(challenge).map_err(|e| { + Error::authentication_error(GSSAPI_STR, &format!("GSSAPI step failed: {e}")) + })? { + Step::Finished((ctx, token)) => { + self.ctx = CtxState::Established(ctx); + Ok(token.map(|t| t.to_vec())) + } + Step::Continue((ctx, token)) => { + self.ctx = CtxState::Pending(ctx); + Ok(Some(token.to_vec())) + } + } + } else { + Err(Error::authentication_error( + GSSAPI_STR, + "Authentication context not initialized", + )) + } + } + + // Perform the final step of Kerberos authentication by gss_unwrap-ing the + // final server challenge, then wrapping the protocol bytes + user principal. + // The resulting token must be sent to the server. + pub(super) fn do_unwrap_wrap(&mut self, payload: &[u8]) -> Result> { + if let CtxState::Established(ref mut established_ctx) = self.ctx { + let _ = established_ctx.unwrap(payload).map_err(|e| { + Error::authentication_error(GSSAPI_STR, &format!("GSSAPI unwrap failed: {e}")) + })?; + + let bytes: &[u8] = &[0x1, 0x0, 0x0, 0x0]; + let bytes = [bytes, self.user_principal.as_bytes()].concat(); + let output_token = established_ctx.wrap(false, bytes.as_slice()).map_err(|e| { + Error::authentication_error(GSSAPI_STR, &format!("GSSAPI wrap failed: {e}")) + })?; + Ok(output_token.to_vec()) + } else { + Err(Error::authentication_error( + GSSAPI_STR, + "Authentication context not established", + )) + } + } + + pub(super) fn is_complete(&self) -> bool { + matches!(self.ctx, CtxState::Established(_)) + } +} diff --git a/src/client/auth/gssapi/windows.rs b/src/client/auth/gssapi/windows.rs new file mode 100644 index 000000000..040b981d4 --- /dev/null +++ b/src/client/auth/gssapi/windows.rs @@ -0,0 +1,423 @@ +use std::ptr; +use windows_sys::Win32::{ + Foundation::{SEC_E_OK, SEC_I_CONTINUE_NEEDED}, + Security::{ + Authentication::Identity::{ + AcquireCredentialsHandleW, + DecryptMessage, + DeleteSecurityContext, + EncryptMessage, + FreeCredentialsHandle, + InitializeSecurityContextW, + QueryContextAttributesW, + SecBuffer, + SecBufferDesc, + SecPkgContext_Sizes, + ISC_REQ_ALLOCATE_MEMORY, + ISC_REQ_MUTUAL_AUTH, + SECBUFFER_DATA, + SECBUFFER_PADDING, + SECBUFFER_STREAM, + SECBUFFER_TOKEN, + SECBUFFER_VERSION, + SECPKG_ATTR_SIZES, + SECPKG_CRED_OUTBOUND, + SECQOP_WRAP_NO_ENCRYPT, + SECURITY_NETWORK_DREP, + }, + Credentials::SecHandle, + }, + System::Rpc::{SEC_WINNT_AUTH_IDENTITY_UNICODE, SEC_WINNT_AUTH_IDENTITY_W}, +}; + +use crate::{ + client::auth::GSSAPI_STR, + error::{Error, Result}, +}; + +pub(super) struct SspiAuthenticator { + cred_handle: Option, + ctx_handle: Option, + auth_complete: bool, + service_principal: Vec, + user_principal: String, +} + +impl SspiAuthenticator { + // Initialize the SspiAuthenticator by acquiring a credentials handle and + // making the first call to InitializeSecurityContext. + pub(super) fn init( + user_principal: Option, + password: Option, + service_principal: String, + ) -> Result<(Self, Vec)> { + let user_principal = user_principal.ok_or_else(|| { + Error::authentication_error(GSSAPI_STR, "User principal not specified") + })?; + + let service_principal: Vec = service_principal + .encode_utf16() + .chain(std::iter::once(0)) + .collect(); + + let mut authenticator = Self { + cred_handle: None, + ctx_handle: None, + auth_complete: false, + service_principal, + user_principal: user_principal.clone(), + }; + + let initial_token = authenticator.acquire_credentials_and_init(password)?; + Ok((authenticator, initial_token)) + } + + fn acquire_credentials_and_init(&mut self, password: Option) -> Result> { + let mut cred_handle = SecHandle::default(); + let mut expiry: i64 = 0; + + let mut auth_identity = SEC_WINNT_AUTH_IDENTITY_W::default(); + auth_identity.Flags = SEC_WINNT_AUTH_IDENTITY_UNICODE; + + // Note that SSPI uses the term "domain" instead of + // "realm" in this context. + let username_wide: Vec; + let domain_wide: Vec; + let password_wide: Vec; + + if let Some(at_pos) = self.user_principal.find('@') { + let username = &self.user_principal[..at_pos]; + let domain = &self.user_principal[at_pos + 1..]; + + username_wide = username.encode_utf16().chain(std::iter::once(0)).collect(); + domain_wide = domain.encode_utf16().chain(std::iter::once(0)).collect(); + + auth_identity.User = username_wide.as_ptr() as *mut u16; + auth_identity.UserLength = username.len() as u32; + auth_identity.Domain = domain_wide.as_ptr() as *mut u16; + auth_identity.DomainLength = domain.len() as u32; + + if let Some(password) = &password { + password_wide = password.encode_utf16().chain(std::iter::once(0)).collect(); + auth_identity.Password = password_wide.as_ptr() as *mut u16; + auth_identity.PasswordLength = password.len() as u32; + } + } + + // Security package name + let package_name: Vec = "kerberos\0".encode_utf16().collect(); + + // nosemgrep: rust.lang.security.unsafe-usage.unsafe-usage + unsafe { + let result = AcquireCredentialsHandleW( + std::ptr::null(), + package_name.as_ptr(), + SECPKG_CRED_OUTBOUND, + ptr::null_mut(), + if password.is_some() { + &auth_identity as *const _ as *const _ + } else { + ptr::null() + }, + None, + ptr::null_mut(), + &mut cred_handle, + &mut expiry, + ); + + if result != SEC_E_OK { + return Err(Error::authentication_error( + GSSAPI_STR, + &format!("Failed to acquire credentials handle: {:?}", result), + )); + } + } + + self.cred_handle = Some(cred_handle); + + let initial_token = self.initialize_security_context(&[])?; + Ok(initial_token) + } + + // Issue the server provided token to the context handle. If auth is complete, + // no token is returned; otherwise, return the next token to send to the server. + pub(super) fn step(&mut self, challenge: &[u8]) -> Result>> { + if self.auth_complete { + return Ok(None); + } + + let token = self.initialize_security_context(challenge)?; + Ok(Some(token)) + } + + fn initialize_security_context(&mut self, input_token: &[u8]) -> Result> { + let mut ctx_handle = self.ctx_handle.unwrap_or_default(); + + let mut input_buffer = SecBuffer { + cbBuffer: input_token.len() as u32, + BufferType: SECBUFFER_TOKEN, + pvBuffer: if input_token.is_empty() { + ptr::null_mut() + } else { + input_token.as_ptr() as *mut _ + }, + }; + + let input_buffer_desc = SecBufferDesc { + ulVersion: SECBUFFER_VERSION, + cBuffers: 1, + pBuffers: &mut input_buffer, + }; + + let mut output_buffer = SecBuffer { + cbBuffer: 0, + BufferType: SECBUFFER_TOKEN, + pvBuffer: ptr::null_mut(), + }; + + let mut output_buffer_desc = SecBufferDesc { + ulVersion: SECBUFFER_VERSION, + cBuffers: 1, + pBuffers: &mut output_buffer, + }; + + let mut context_attr = 0u32; + + // nosemgrep: rust.lang.security.unsafe-usage.unsafe-usage + unsafe { + let result = InitializeSecurityContextW( + &self.cred_handle.unwrap(), + if self.ctx_handle.is_some() { + &ctx_handle + } else { + ptr::null() + }, + self.service_principal.as_ptr(), + ISC_REQ_ALLOCATE_MEMORY | ISC_REQ_MUTUAL_AUTH, + 0, + SECURITY_NETWORK_DREP, + if self.ctx_handle.is_some() { + &input_buffer_desc + } else { + ptr::null() + }, + 0, + &mut ctx_handle, + &mut output_buffer_desc, + &mut context_attr, + ptr::null_mut(), + ); + + self.ctx_handle = Some(ctx_handle); + + match result { + SEC_E_OK => { + self.auth_complete = true; + } + SEC_I_CONTINUE_NEEDED => {} + _ => { + return Err(Error::authentication_error( + GSSAPI_STR, + &format!("InitializeSecurityContext failed: {:?}", result), + )); + } + } + + let token = if output_buffer.pvBuffer.is_null() || output_buffer.cbBuffer == 0 { + Vec::new() + } else { + let token_slice = std::slice::from_raw_parts( + output_buffer.pvBuffer as *const u8, + output_buffer.cbBuffer as usize, + ); + token_slice.to_vec() + }; + + Ok(token) + } + } + + // Perform the final step of Kerberos authentication by decrypting the + // final server challenge, then encrypting the protocol bytes + user + // principal. The resulting token must be sent to the server. + // For consistency with nix/GSSAPI, we use the terminology "unwrap" and + // "wrap" here, even though in the context of SSPI it is "decrypt" and + // "encrypt" (as seen in the implementation of this method). + pub(super) fn do_unwrap_wrap(&mut self, payload: &[u8]) -> Result> { + let mut message = payload.to_vec(); + + let mut wrap_bufs = [ + SecBuffer { + cbBuffer: message.len() as u32, + BufferType: SECBUFFER_STREAM, + pvBuffer: message.as_mut_ptr() as *mut _, + }, + SecBuffer { + cbBuffer: 0, + BufferType: SECBUFFER_DATA, + pvBuffer: ptr::null_mut(), + }, + ]; + + let mut wrap_buf_desc = SecBufferDesc { + ulVersion: SECBUFFER_VERSION, + cBuffers: 2, + pBuffers: wrap_bufs.as_mut_ptr(), + }; + + // nosemgrep: rust.lang.security.unsafe-usage.unsafe-usage + unsafe { + let result = DecryptMessage( + &self.ctx_handle.unwrap(), + &mut wrap_buf_desc, + 0, + ptr::null_mut(), + ); + if result != SEC_E_OK { + return Err(Error::authentication_error( + GSSAPI_STR, + &format!("DecryptMessage failed: {:?}", result), + )); + } + } + + if wrap_bufs[1].cbBuffer < 4 { + return Err(Error::authentication_error( + GSSAPI_STR, + "Server message is too short", + )); + } + + let data_ptr = wrap_bufs[1].pvBuffer as *const u8; + + // nosemgrep: rust.lang.security.unsafe-usage.unsafe-usage + unsafe { + let first_byte = *data_ptr; + if (first_byte & 1) == 0 { + return Err(Error::authentication_error( + GSSAPI_STR, + "Server does not support the required security layer", + )); + } + } + + let mut sizes = SecPkgContext_Sizes::default(); + + // nosemgrep: rust.lang.security.unsafe-usage.unsafe-usage + unsafe { + let result = QueryContextAttributesW( + &self.ctx_handle.unwrap(), + SECPKG_ATTR_SIZES, + &mut sizes as *mut _ as *mut _, + ); + if result != SEC_E_OK { + return Err(Error::authentication_error( + GSSAPI_STR, + &format!("QueryContextAttributes failed: {:?}", result), + )); + } + } + + let user_principal = &self.user_principal; + let plaintext_message_size = 4 + user_principal.len(); + let total_size = + sizes.cbSecurityTrailer as usize + plaintext_message_size + sizes.cbBlockSize as usize; + let mut message_buf = vec![0u8; total_size]; + + let plaintext_start = sizes.cbSecurityTrailer as usize; + message_buf[plaintext_start] = 1; + message_buf[plaintext_start + 1] = 0; + message_buf[plaintext_start + 2] = 0; + message_buf[plaintext_start + 3] = 0; + message_buf[plaintext_start + 4..plaintext_start + 4 + user_principal.len()] + .copy_from_slice(user_principal.as_bytes()); + + // nosemgrep: rust.lang.security.unsafe-usage.unsafe-usage + unsafe { + let mut encrypt_bufs = [ + SecBuffer { + cbBuffer: sizes.cbSecurityTrailer, + BufferType: SECBUFFER_TOKEN, + pvBuffer: message_buf.as_mut_ptr() as *mut _, + }, + SecBuffer { + cbBuffer: plaintext_message_size as u32, + BufferType: SECBUFFER_DATA, + pvBuffer: message_buf + .as_mut_ptr() + .add(sizes.cbSecurityTrailer as usize) + as *mut _, + }, + SecBuffer { + cbBuffer: sizes.cbBlockSize, + BufferType: SECBUFFER_PADDING, + pvBuffer: message_buf + .as_mut_ptr() + .add(plaintext_start + plaintext_message_size) + as *mut _, + }, + ]; + + let mut encrypt_buf_desc = SecBufferDesc { + ulVersion: SECBUFFER_VERSION, + cBuffers: 3, + pBuffers: encrypt_bufs.as_mut_ptr(), + }; + + let result = EncryptMessage( + &self.ctx_handle.unwrap(), + SECQOP_WRAP_NO_ENCRYPT, + &mut encrypt_buf_desc, + 0, + ); + if result != SEC_E_OK { + return Err(Error::authentication_error( + GSSAPI_STR, + &format!("EncryptMessage failed: {:?}", result), + )); + } + + let total_len = + encrypt_bufs[0].cbBuffer + encrypt_bufs[1].cbBuffer + encrypt_bufs[2].cbBuffer; + let mut result_buf = Vec::with_capacity(total_len as usize); + + let buf0_slice = std::slice::from_raw_parts( + encrypt_bufs[0].pvBuffer as *const u8, + encrypt_bufs[0].cbBuffer as usize, + ); + result_buf.extend_from_slice(buf0_slice); + + let buf1_slice = std::slice::from_raw_parts( + encrypt_bufs[1].pvBuffer as *const u8, + encrypt_bufs[1].cbBuffer as usize, + ); + result_buf.extend_from_slice(buf1_slice); + + let buf2_slice = std::slice::from_raw_parts( + encrypt_bufs[2].pvBuffer as *const u8, + encrypt_bufs[2].cbBuffer as usize, + ); + result_buf.extend_from_slice(buf2_slice); + + Ok(result_buf) + } + } + + pub(super) fn is_complete(&self) -> bool { + self.auth_complete + } +} + +impl Drop for SspiAuthenticator { + fn drop(&mut self) { + // nosemgrep: rust.lang.security.unsafe-usage.unsafe-usage + unsafe { + if let Some(ctx) = &self.ctx_handle { + let _ = DeleteSecurityContext(ctx); + } + if let Some(cred) = &self.cred_handle { + let _ = FreeCredentialsHandle(cred); + } + } + } +} diff --git a/src/test/auth/gssapi.rs b/src/test/auth/gssapi.rs index 45154d8b2..a00df0efc 100644 --- a/src/test/auth/gssapi.rs +++ b/src/test/auth/gssapi.rs @@ -9,6 +9,7 @@ use crate::{ /// - auth_mechanism_properties is an optional set of authMechanismProperties to append to the uri async fn run_gssapi_auth_test( user_principal_var: &str, + #[cfg(target_os = "windows")] password_var: &str, gssapi_db_var: &str, auth_mechanism_properties: Option<&str>, ) { @@ -28,9 +29,19 @@ async fn run_gssapi_auth_test( }; // Create client + #[cfg(not(target_os = "windows"))] let uri = format!( "mongodb://{user_principal}@{host}/?authSource=%24external&authMechanism=GSSAPI{props}" ); + #[cfg(target_os = "windows")] + let uri = { + let password = + std::env::var(password_var).unwrap_or_else(|_| panic!("{password_var} not set")); + format!( + "mongodb://{user_principal}:{password}@{host}/?authSource=%24external&\ + authMechanism=GSSAPI{props}" + ) + }; let client = Client::with_uri_str(uri) .await .expect("failed to create MongoDB Client"); @@ -57,13 +68,22 @@ async fn run_gssapi_auth_test( #[tokio::test] async fn no_options() { - run_gssapi_auth_test("PRINCIPAL", "GSSAPI_DB", None).await + run_gssapi_auth_test( + "PRINCIPAL", + #[cfg(target_os = "windows")] + "SASL_PASS", + "GSSAPI_DB", + None, + ) + .await } #[tokio::test] async fn explicit_canonicalize_host_name_false() { run_gssapi_auth_test( "PRINCIPAL", + #[cfg(target_os = "windows")] + "SASL_PASS", "GSSAPI_DB", Some("CANONICALIZE_HOST_NAME:false"), ) @@ -74,6 +94,8 @@ async fn explicit_canonicalize_host_name_false() { async fn canonicalize_host_name_forward() { run_gssapi_auth_test( "PRINCIPAL", + #[cfg(target_os = "windows")] + "SASL_PASS", "GSSAPI_DB", Some("CANONICALIZE_HOST_NAME:forward"), ) @@ -84,6 +106,8 @@ async fn canonicalize_host_name_forward() { async fn canonicalize_host_name_forward_and_reverse() { run_gssapi_auth_test( "PRINCIPAL", + #[cfg(target_os = "windows")] + "SASL_PASS", "GSSAPI_DB", Some("CANONICALIZE_HOST_NAME:forwardAndReverse"), ) @@ -100,6 +124,8 @@ async fn with_service_realm_and_host_options() { run_gssapi_auth_test( "PRINCIPAL_CROSS", + #[cfg(target_os = "windows")] + "SASL_PASS_CROSS", "GSSAPI_DB_CROSS", Some(format!("SERVICE_REALM:{service_realm},SERVICE_HOST:{service_host}").as_str()), )