diff --git a/sqlx-core/src/error.rs b/sqlx-core/src/error.rs index 8c6f424cdf..67e3a13336 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 701cabff85..ff43295454 100644 --- a/sqlx-postgres/Cargo.toml +++ b/sqlx-postgres/Cargo.toml @@ -22,15 +22,27 @@ chrono = ["dep:chrono", "sqlx-core/chrono"] ipnet = ["dep:ipnet", "sqlx-core/ipnet"] ipnetwork = ["dep:ipnetwork", "sqlx-core/ipnetwork"] mac_address = ["dep:mac_address", "sqlx-core/mac_address"] -rust_decimal = ["dep:rust_decimal", "rust_decimal/maths", "sqlx-core/rust_decimal"] +rust_decimal = [ + "dep:rust_decimal", + "rust_decimal/maths", + "sqlx-core/rust_decimal", +] time = ["dep:time", "sqlx-core/time"] uuid = ["dep:uuid", "sqlx-core/uuid"] [dependencies] # Futures crates -futures-channel = { version = "0.3.32", default-features = false, features = ["sink", "alloc", "std"] } +futures-channel = { version = "0.3.32", default-features = false, features = [ + "sink", + "alloc", + "std", +] } futures-core = { version = "0.3.32", default-features = false } -futures-util = { version = "0.3.32", default-features = false, features = ["alloc", "sink", "io"] } +futures-util = { version = "0.3.32", default-features = false, features = [ + "alloc", + "sink", + "io", +] } # Cryptographic Primitives crc = { workspace = true, optional = true } @@ -71,6 +83,7 @@ thiserror.workspace = true serde = { version = "1.0.219", optional = true, features = ["derive"] } serde_json = { version = "1.0.142", optional = true, 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 3c2f516533..6960697050 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 d594585b6c..320e7d3b03 100644 --- a/sqlx-postgres/src/connection/mod.rs +++ b/sqlx-postgres/src/connection/mod.rs @@ -27,6 +27,7 @@ pub use self::stream::PgStream; mod describe; mod establish; mod executor; +mod gssapi; mod resolve; mod sasl; mod stream; 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 dedfe7c1bb..9109093e07 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 21e6628cae..bf254706bc 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, @@ -82,6 +83,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 @@ -343,6 +345,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