diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 5e90588619..9def25a143 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -50,6 +50,7 @@ jobs: services: mysql: image: mysql:latest + command: --default-authentication-plugin=mysql_native_password env: MYSQL_ROOT_PASSWORD: password ports: diff --git a/sqlx-mysql/Cargo.toml b/sqlx-mysql/Cargo.toml index c7afc236c6..eda4ff8a2f 100644 --- a/sqlx-mysql/Cargo.toml +++ b/sqlx-mysql/Cargo.toml @@ -14,6 +14,7 @@ json = ["sqlx-core/json", "serde"] any = ["sqlx-core/any"] offline = ["sqlx-core/offline", "serde/derive", "bitflags/serde"] migrate = ["sqlx-core/migrate"] +rsa = ["dep:rsa"] # Type Integration features bigdecimal = ["dep:bigdecimal", "sqlx-core/bigdecimal"] @@ -38,7 +39,7 @@ hkdf = "0.12.0" hmac = { version = "0.12.0", default-features = false } md-5 = { version = "0.10.0", default-features = false } rand = { version = "0.8.4", default-features = false, features = ["std", "std_rng"] } -rsa = "0.9" +rsa = { version = "0.9", optional = true } sha1 = { version = "0.10.1", default-features = false } sha2 = { version = "0.10.0", default-features = false } diff --git a/sqlx-mysql/src/connection/auth.rs b/sqlx-mysql/src/connection/auth.rs index 613f8e702f..5353dea5f3 100644 --- a/sqlx-mysql/src/connection/auth.rs +++ b/sqlx-mysql/src/connection/auth.rs @@ -2,7 +2,9 @@ use bytes::buf::Chain; use bytes::Bytes; use digest::{Digest, OutputSizeUser}; use generic_array::GenericArray; +#[cfg(feature = "rsa")] use rand::thread_rng; +#[cfg(feature = "rsa")] use rsa::{pkcs8::DecodePublicKey, Oaep, RsaPublicKey}; use sha1::Sha1; use sha2::Sha256; @@ -131,9 +133,9 @@ fn scramble_sha256( async fn encrypt_rsa<'s>( stream: &'s mut MySqlStream, - public_key_request_id: u8, + _public_key_request_id: u8, password: &'s str, - nonce: &'s Chain, + _nonce: &'s Chain, ) -> Result, Error> { // https://mariadb.com/kb/en/caching_sha2_password-authentication-plugin/ @@ -142,29 +144,41 @@ async fn encrypt_rsa<'s>( return Ok(to_asciz(password)); } - // client sends a public key request - stream.write_packet(&[public_key_request_id][..])?; - stream.flush().await?; - - // server sends a public key response - let packet = stream.recv_packet().await?; - let rsa_pub_key = &packet[1..]; - - // xor the password with the given nonce - let mut pass = to_asciz(password); - - let (a, b) = (nonce.first_ref(), nonce.last_ref()); - let mut nonce = Vec::with_capacity(a.len() + b.len()); - nonce.extend_from_slice(a); - nonce.extend_from_slice(b); - - xor_eq(&mut pass, &nonce); - - // client sends an RSA encrypted password - let pkey = parse_rsa_pub_key(rsa_pub_key)?; - let padding = Oaep::new::(); - pkey.encrypt(&mut thread_rng(), padding, &pass[..]) - .map_err(Error::protocol) + // Non-TLS RSA password encryption requires the `rsa` feature. + // It is opt-in because RUSTSEC-2023-0071 (Marvin Attack timing side-channel) + // has no patched release; disable it when all connections use TLS. + #[cfg(not(feature = "rsa"))] + return Err(err_protocol!( + "sha256_password / caching_sha2_password over non-TLS requires the `rsa` feature \ + (disabled by default due to RUSTSEC-2023-0071)" + )); + + #[cfg(feature = "rsa")] + { + // client sends a public key request + stream.write_packet(&[_public_key_request_id][..])?; + stream.flush().await?; + + // server sends a public key response + let packet = stream.recv_packet().await?; + let rsa_pub_key = &packet[1..]; + + // xor the password with the given nonce + let mut pass = to_asciz(password); + + let (a, b) = (_nonce.first_ref(), _nonce.last_ref()); + let mut nonce = Vec::with_capacity(a.len() + b.len()); + nonce.extend_from_slice(a); + nonce.extend_from_slice(b); + + xor_eq(&mut pass, &nonce); + + // client sends an RSA encrypted password + let pkey = parse_rsa_pub_key(rsa_pub_key)?; + let padding = Oaep::new::(); + pkey.encrypt(&mut thread_rng(), padding, &pass[..]) + .map_err(Error::protocol) + } } // XOR(x, y) @@ -186,6 +200,7 @@ fn to_asciz(s: &str) -> Vec { } // https://docs.rs/rsa/0.3.0/rsa/struct.RSAPublicKey.html?search=#example-1 +#[cfg(feature = "rsa")] fn parse_rsa_pub_key(key: &[u8]) -> Result { let pem = std::str::from_utf8(key).map_err(Error::protocol)?; diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index c2ccdabef6..609a2f2271 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -14,6 +14,7 @@ services: MYSQL_ROOT_HOST: '%' MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: sqlx + command: --default-authentication-plugin=mysql_native_password mysql_8_client_ssl: build: