diff --git a/Cargo.lock b/Cargo.lock index 61d2e7d7b6..0ba2a3bd78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -411,7 +411,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61b1d86e7705efe1be1b569bab41d4fa1e14e220b60a160f78de2db687add079" dependencies = [ - "bindgen", + "bindgen 0.69.5", "cc", "cmake", "dunce", @@ -555,12 +555,32 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 1.1.0", "shlex", "syn 2.0.104", "which", ] +[[package]] +name = "bindgen" +version = "0.71.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +dependencies = [ + "bitflags 2.9.1", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.1", + "shlex", + "syn 2.0.104", +] + [[package]] name = "bit-vec" version = "0.6.3" @@ -1058,6 +1078,19 @@ dependencies = [ "itertools 0.10.5", ] +[[package]] +name = "cross-krb5" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d4ddf7139e64dc916b11d434421031bcc5ba02e521a49a011652a0f68775188" +dependencies = [ + "anyhow", + "bitflags 2.9.1", + "bytes", + "libgssapi", + "windows", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -2104,6 +2137,28 @@ version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +[[package]] +name = "libgssapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "834339e86b2561169d45d3b01741967fee3e5716c7d0b6e33cd4e3b34c9558cd" +dependencies = [ + "bitflags 2.9.1", + "bytes", + "lazy_static", + "libgssapi-sys", +] + +[[package]] +name = "libgssapi-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7518e6902e94f92e7c7271232684b60988b4bd813529b4ef9d97aead96956ae8" +dependencies = [ + "bindgen 0.71.1", + "pkg-config", +] + [[package]] name = "libloading" version = "0.8.8" @@ -2137,7 +2192,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ - "bindgen", + "bindgen 0.69.5", "cc", "pkg-config", "vcpkg", @@ -3089,6 +3144,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustix" version = "0.38.44" @@ -3927,6 +3988,7 @@ dependencies = [ "byteorder", "chrono", "crc", + "cross-krb5", "dotenvy", "etcetera", "futures-channel", @@ -4856,6 +4918,28 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-link", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core", +] + [[package]] name = "windows-core" version = "0.61.2" @@ -4869,6 +4953,17 @@ dependencies = [ "windows-strings", ] +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.60.0" @@ -4897,6 +4992,16 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core", + "windows-link", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -4998,6 +5103,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" diff --git a/sqlx-core/src/error.rs b/sqlx-core/src/error.rs index c6652aef75..69036f5d37 100644 --- a/sqlx-core/src/error.rs +++ b/sqlx-core/src/error.rs @@ -52,6 +52,9 @@ pub enum Error { #[error("error occurred while attempting to establish a TLS connection: {0}")] Tls(#[source] BoxDynError), + #[error("error occured during gssapi negotiation: {0}")] + GssApi(#[from] BoxDynError), + /// Unexpected or invalid data encountered while communicating with the database. /// /// This should indicate there is a programming error in a SQLx driver or there diff --git a/sqlx-postgres/Cargo.toml b/sqlx-postgres/Cargo.toml index a70fb37d72..e3c22239cb 100644 --- a/sqlx-postgres/Cargo.toml +++ b/sqlx-postgres/Cargo.toml @@ -71,6 +71,7 @@ whoami = { version = "1.2.1", default-features = false } serde = { version = "1.0.144", features = ["derive"] } serde_json = { version = "1.0.85", features = ["raw_value"] } +cross-krb5 = { version = "0.4.2" } [dependencies.sqlx-core] workspace = true diff --git a/sqlx-postgres/src/connection/establish.rs b/sqlx-postgres/src/connection/establish.rs index 634b71de4b..cdacf3333e 100644 --- a/sqlx-postgres/src/connection/establish.rs +++ b/sqlx-postgres/src/connection/establish.rs @@ -1,3 +1,4 @@ +use crate::connection::gssapi; use crate::HashMap; use crate::common::StatementCache; @@ -97,6 +98,10 @@ impl PgConnection { .await?; } + Authentication::Gss => { + gssapi::authenticate(&mut stream, options).await?; + } + Authentication::Sasl(body) => { sasl::authenticate(&mut stream, options, body).await?; } diff --git a/sqlx-postgres/src/connection/gssapi.rs b/sqlx-postgres/src/connection/gssapi.rs new file mode 100644 index 0000000000..4f33dfc080 --- /dev/null +++ b/sqlx-postgres/src/connection/gssapi.rs @@ -0,0 +1,45 @@ +use std::borrow::Cow; + +use crate::error::Error; +use cross_krb5::InitiateFlags; + +use crate::{ + connection::PgStream, + message::{Authentication, AuthenticationGss, GssResponse}, + PgConnectOptions, +}; + +pub async fn authenticate(stream: &mut PgStream, options: &PgConnectOptions) -> Result<(), Error> { + let PgConnectOptions { + host, + gssapi_target_principal: gssapi_principal, + .. + } = options; + let principal = gssapi_principal + .as_ref() + .map(Cow::Borrowed) + .unwrap_or(Cow::Owned(format!("postgres/{host}"))); + let (mut ctx, token) = + cross_krb5::ClientCtx::new(InitiateFlags::empty(), None, &principal, None) + .map_err(|e| Error::GssApi(e.into()))?; + let msg = GssResponse { token: &token }; + stream.send(msg).await?; + loop { + let token = match stream.recv_expect().await? { + Authentication::GssContinue(AuthenticationGss { token }) => token, + other => return Err(err_protocol!("expected GssContinue but receiver {other:?}")), + }; + match ctx.step(&token).map_err(|e| Error::GssApi(e.into()))? { + cross_krb5::Step::Finished((_context, last_token)) => { + if let Some(last_token) = last_token { + stream.send(GssResponse { token: &last_token }).await?; + } + return Ok(()); + } + cross_krb5::Step::Continue((pending, token)) => { + ctx = pending; + stream.send(GssResponse { token: &token }).await?; + } + } + } +} diff --git a/sqlx-postgres/src/connection/mod.rs b/sqlx-postgres/src/connection/mod.rs index 4e05cd867b..8ef916467c 100644 --- a/sqlx-postgres/src/connection/mod.rs +++ b/sqlx-postgres/src/connection/mod.rs @@ -26,6 +26,7 @@ pub use self::stream::PgStream; pub(crate) mod describe; mod establish; mod executor; +mod gssapi; mod sasl; mod stream; mod tls; diff --git a/sqlx-postgres/src/message/authentication.rs b/sqlx-postgres/src/message/authentication.rs index 3a3cf7ff6e..6e071762fb 100644 --- a/sqlx-postgres/src/message/authentication.rs +++ b/sqlx-postgres/src/message/authentication.rs @@ -36,6 +36,12 @@ pub enum Authentication { /// again using the 4-byte random salt. Md5Password(AuthenticationMd5Password), + /// The frontend must initiate GSSAPI negotiation + Gss, + + /// GSSAPI token reponse for continuing the security context + GssContinue(AuthenticationGss), + /// The frontend must now initiate a SASL negotiation, /// using one of the SASL mechanisms listed in the message. /// @@ -75,6 +81,8 @@ impl BackendMessage for Authentication { Authentication::Md5Password(AuthenticationMd5Password { salt }) } + 7 => Authentication::Gss, + 8 => Authentication::GssContinue(AuthenticationGss { token: buf }), 10 => Authentication::Sasl(AuthenticationSasl(buf)), 11 => Authentication::SaslContinue(AuthenticationSaslContinue::decode(buf)?), @@ -191,3 +199,8 @@ impl ProtocolDecode<'_> for AuthenticationSaslFinal { Ok(Self { verifier }) } } + +#[derive(Debug)] +pub struct AuthenticationGss { + pub token: Bytes, +} diff --git a/sqlx-postgres/src/message/gssapi.rs b/sqlx-postgres/src/message/gssapi.rs new file mode 100644 index 0000000000..6076c6b928 --- /dev/null +++ b/sqlx-postgres/src/message/gssapi.rs @@ -0,0 +1,20 @@ +use crate::message::{FrontendMessage, FrontendMessageFormat}; + +pub struct GssResponse<'g> { + pub(crate) token: &'g [u8], +} +impl<'g> FrontendMessage for GssResponse<'g> { + const FORMAT: FrontendMessageFormat = FrontendMessageFormat::PasswordPolymorphic; + + fn body_size_hint(&self) -> std::num::Saturating { + let mut size = std::num::Saturating(0); + size += 4; + size += self.token.len(); + size + } + + fn encode_body(&self, buf: &mut Vec) -> Result<(), sqlx_core::Error> { + buf.extend_from_slice(&self.token); + Ok(()) + } +} diff --git a/sqlx-postgres/src/message/mod.rs b/sqlx-postgres/src/message/mod.rs index e62f9bebb3..5aaab1efa8 100644 --- a/sqlx-postgres/src/message/mod.rs +++ b/sqlx-postgres/src/message/mod.rs @@ -14,6 +14,7 @@ mod data_row; mod describe; mod execute; mod flush; +mod gssapi; mod notification; mod parameter_description; mod parameter_status; @@ -30,7 +31,7 @@ mod startup; mod sync; mod terminate; -pub use authentication::{Authentication, AuthenticationSasl}; +pub use authentication::{Authentication, AuthenticationGss, AuthenticationSasl}; pub use backend_key_data::BackendKeyData; pub use bind::Bind; pub use close::Close; @@ -41,6 +42,7 @@ pub use describe::Describe; pub use execute::Execute; #[allow(unused_imports)] pub use flush::Flush; +pub use gssapi::GssResponse; pub use notification::Notification; pub use parameter_description::ParameterDescription; pub use parameter_status::ParameterStatus; diff --git a/sqlx-postgres/src/options/mod.rs b/sqlx-postgres/src/options/mod.rs index efbc43989b..de616e1b4d 100644 --- a/sqlx-postgres/src/options/mod.rs +++ b/sqlx-postgres/src/options/mod.rs @@ -21,6 +21,7 @@ pub struct PgConnectOptions { pub(crate) username: String, pub(crate) password: Option, pub(crate) database: Option, + pub(crate) gssapi_target_principal: Option, pub(crate) ssl_mode: PgSslMode, pub(crate) ssl_root_cert: Option, pub(crate) ssl_client_cert: Option, @@ -75,6 +76,7 @@ impl PgConnectOptions { username, password: var("PGPASSWORD").ok(), database, + gssapi_target_principal: var("PGPRINCIPAL").ok(), ssl_root_cert: var("PGSSLROOTCERT").ok().map(CertificateInput::from), ssl_client_cert: var("PGSSLCERT").ok().map(CertificateInput::from), // As of writing, the implementation of `From` only looks for @@ -336,6 +338,12 @@ impl PgConnectOptions { self } + /// Sets the targeted principal in case of attempted Kerberos negotiation + /// If left out and Kerberos is challenged, uses 'postgres/' + pub fn gssapi_target_principal(mut self, target_principal: &str) -> Self { + self.gssapi_target_principal = Some(target_principal.to_owned()); + self + } /// Sets the capacity of the connection's statement cache in a number of stored /// distinct statements. Caching is handled using LRU, meaning when the /// amount of queries hits the defined limit, the oldest statement will get