From 8351d62d138e5deab6feb8a48e5e9269d549924c Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Fri, 2 Aug 2024 17:08:02 +0200 Subject: [PATCH] Upgrade to rustls 0.23 --- .github/workflows/sqlx.yml | 2 +- Cargo.lock | 226 ++++++++++++++++++++++++---- Cargo.toml | 7 +- sqlx-core/Cargo.toml | 10 +- sqlx-core/src/net/tls/mod.rs | 8 +- sqlx-core/src/net/tls/tls_rustls.rs | 164 +++++++++++++------- sqlx-macros-core/Cargo.toml | 3 +- sqlx-macros/Cargo.toml | 3 +- 8 files changed, 330 insertions(+), 93 deletions(-) diff --git a/.github/workflows/sqlx.yml b/.github/workflows/sqlx.yml index e2967ec9ef..f588664085 100644 --- a/.github/workflows/sqlx.yml +++ b/.github/workflows/sqlx.yml @@ -147,7 +147,7 @@ jobs: matrix: postgres: [15, 11] runtime: [async-std, tokio] - tls: [native-tls, rustls, none] + tls: [native-tls, rustls-aws-lc-rs, rustls-ring, none] needs: check steps: - uses: actions/checkout@v4 diff --git a/Cargo.lock b/Cargo.lock index 9a0f789308..3f711abedd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -349,6 +349,33 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "aws-lc-rs" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a47f2fb521b70c11ce7369a6c5fa4bd6af7e5d62ec06303875bafe7c6ba245" +dependencies = [ + "aws-lc-sys", + "mirai-annotations", + "paste", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2927c7af777b460b7ccd95f8b67acd7b4c04ec8896bf0c8e80ba30523cffc057" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "libc", + "paste", +] + [[package]] name = "axum" version = "0.5.17" @@ -444,12 +471,6 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.0" @@ -484,6 +505,29 @@ dependencies = [ "num-traits", ] +[[package]] +name = "bindgen" +version = "0.69.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +dependencies = [ + "bitflags 2.4.2", + "cexpr", + "clang-sys", + "itertools 0.10.5", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.52", + "which", +] + [[package]] name = "bit-vec" version = "0.6.3" @@ -684,6 +728,19 @@ name = "cc" version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] [[package]] name = "cfg-if" @@ -737,6 +794,17 @@ dependencies = [ "half", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.5.2" @@ -797,6 +865,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "cmake" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +dependencies = [ + "cc", +] + [[package]] name = "colorchoice" version = "1.0.0" @@ -1110,6 +1187,12 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" +[[package]] +name = "dunce" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" + [[package]] name = "either" version = "1.10.0" @@ -1348,6 +1431,12 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "funty" version = "2.0.0" @@ -1834,6 +1923,15 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +[[package]] +name = "jobserver" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.69" @@ -1861,12 +1959,28 @@ dependencies = [ "spin 0.5.2", ] +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +[[package]] +name = "libloading" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" +dependencies = [ + "cfg-if", + "windows-targets 0.48.5", +] + [[package]] name = "libm" version = "0.2.8" @@ -2009,6 +2123,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mirai-annotations" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" + [[package]] name = "mockall" version = "0.11.4" @@ -2483,6 +2603,16 @@ dependencies = [ "termtree", ] +[[package]] +name = "prettyplease" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3928fb5db768cb86f891ff014f0144589297e3c6a1aba6ed7cecfdace270c7" +dependencies = [ + "proc-macro2", + "syn 2.0.52", +] + [[package]] name = "proc-macro-crate" version = "3.1.0" @@ -2803,6 +2933,12 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustix" version = "0.37.27" @@ -2832,31 +2968,44 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.11" +version = "0.23.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fecbfb7b1444f477b345853b1fce097a2c6fb637b2bfb87e6bc5db0f043fae4" +checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0" dependencies = [ + "aws-lc-rs", + "once_cell", "ring", + "rustls-pki-types", "rustls-webpki", - "sct", + "subtle", + "zeroize", ] [[package]] name = "rustls-pemfile" -version = "1.0.4" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" dependencies = [ - "base64 0.21.7", + "base64 0.22.0", + "rustls-pki-types", ] +[[package]] +name = "rustls-pki-types" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" + [[package]] name = "rustls-webpki" -version = "0.101.7" +version = "0.102.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +checksum = "f9a6fccd794a42c2c105b513a2f62bc3fd8f3ba57a4593677ceb0bd035164d78" dependencies = [ + "aws-lc-rs", "ring", + "rustls-pki-types", "untrusted", ] @@ -2920,16 +3069,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "seahash" version = "4.1.0" @@ -3061,6 +3200,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook" version = "0.3.17" @@ -4237,9 +4382,24 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.25.4" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "which" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.31", +] [[package]] name = "whoami" @@ -4466,3 +4626,17 @@ name = "zeroize" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] diff --git a/Cargo.toml b/Cargo.toml index b73630eac0..0ba0096a79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,7 +78,8 @@ runtime-tokio = ["_rt-tokio", "sqlx-core/_rt-tokio", "sqlx-macros?/_rt-tokio"] # TLS features tls-native-tls = ["sqlx-core/_tls-native-tls", "sqlx-macros?/_tls-native-tls"] -tls-rustls = ["sqlx-core/_tls-rustls", "sqlx-macros?/_tls-rustls"] +tls-rustls-aws-lc-rs = ["sqlx-core/_tls-rustls-aws-lc-rs", "sqlx-macros?/_tls-rustls-aws-lc-rs"] +tls-rustls-ring = ["sqlx-core/_tls-rustls-ring", "sqlx-macros?/_tls-rustls-ring"] # No-op feature used by the workflows to compile without TLS enabled. Not meant for general use. tls-none = [] @@ -86,10 +87,10 @@ tls-none = [] # Legacy Runtime + TLS features runtime-async-std-native-tls = ["runtime-async-std", "tls-native-tls"] -runtime-async-std-rustls = ["runtime-async-std", "tls-rustls"] +runtime-async-std-rustls = ["runtime-async-std", "tls-rustls-ring"] runtime-tokio-native-tls = ["runtime-tokio", "tls-native-tls"] -runtime-tokio-rustls = ["runtime-tokio", "tls-rustls"] +runtime-tokio-rustls = ["runtime-tokio", "tls-rustls-ring"] # for conditional compilation _rt-async-std = [] diff --git a/sqlx-core/Cargo.toml b/sqlx-core/Cargo.toml index f19c1a1e0d..f6a177ef0f 100644 --- a/sqlx-core/Cargo.toml +++ b/sqlx-core/Cargo.toml @@ -22,7 +22,9 @@ json = ["serde", "serde_json"] _rt-async-std = ["async-std", "async-io"] _rt-tokio = ["tokio", "tokio-stream"] _tls-native-tls = ["native-tls"] -_tls-rustls = ["rustls", "rustls-pemfile", "webpki-roots"] +_tls-rustls-aws-lc-rs = ["__tls-rustls", "rustls/aws-lc-rs"] +_tls-rustls-ring = ["__tls-rustls", "rustls/ring"] +__tls-rustls = ["rustls", "rustls-pemfile", "webpki-roots"] _tls-none = [] # support offline/decoupled building (enables serialization of `Describe`) @@ -36,9 +38,9 @@ tokio = { workspace = true, optional = true } # TLS native-tls = { version = "0.2.10", optional = true } -rustls = { version = "0.21.11", default-features = false, features = ["dangerous_configuration", "tls12"], optional = true } -rustls-pemfile = { version = "1.0", optional = true } -webpki-roots = { version = "0.25", optional = true } +rustls = { version = "0.23.11", default-features = false, features = ["std", "tls12"], optional = true } +rustls-pemfile = { version = "2", optional = true } +webpki-roots = { version = "0.26", optional = true } # Type Integrations bit-vec = { workspace = true, optional = true } diff --git a/sqlx-core/src/net/tls/mod.rs b/sqlx-core/src/net/tls/mod.rs index b49708b22e..f9c7e2670b 100644 --- a/sqlx-core/src/net/tls/mod.rs +++ b/sqlx-core/src/net/tls/mod.rs @@ -6,7 +6,7 @@ use crate::error::Error; use crate::net::socket::WithSocket; use crate::net::Socket; -#[cfg(feature = "_tls-rustls")] +#[cfg(feature = "__tls-rustls")] mod tls_rustls; #[cfg(feature = "_tls-native-tls")] @@ -77,10 +77,10 @@ where #[cfg(feature = "_tls-native-tls")] return Ok(with_socket.with_socket(tls_native_tls::handshake(socket, config).await?)); - #[cfg(all(feature = "_tls-rustls", not(feature = "_tls-native-tls")))] + #[cfg(all(feature = "__tls-rustls", not(feature = "_tls-native-tls")))] return Ok(with_socket.with_socket(tls_rustls::handshake(socket, config).await?)); - #[cfg(not(any(feature = "_tls-native-tls", feature = "_tls-rustls")))] + #[cfg(not(any(feature = "_tls-native-tls", feature = "__tls-rustls")))] { drop((socket, config, with_socket)); panic!("one of the `runtime-*-native-tls` or `runtime-*-rustls` features must be enabled") @@ -88,7 +88,7 @@ where } pub fn available() -> bool { - cfg!(any(feature = "_tls-native-tls", feature = "_tls-rustls")) + cfg!(any(feature = "_tls-native-tls", feature = "__tls-rustls")) } pub fn error_if_unavailable() -> crate::Result<()> { diff --git a/sqlx-core/src/net/tls/tls_rustls.rs b/sqlx-core/src/net/tls/tls_rustls.rs index e958fdef3d..938a05664c 100644 --- a/sqlx-core/src/net/tls/tls_rustls.rs +++ b/sqlx-core/src/net/tls/tls_rustls.rs @@ -2,12 +2,15 @@ use futures_util::future; use std::io::{self, BufReader, Cursor, Read, Write}; use std::sync::Arc; use std::task::{Context, Poll}; -use std::time::SystemTime; use rustls::{ - client::{ServerCertVerified, ServerCertVerifier, WebPkiVerifier}, - CertificateError, ClientConfig, ClientConnection, Error as TlsError, OwnedTrustAnchor, - RootCertStore, ServerName, + client::{ + danger::{ServerCertVerified, ServerCertVerifier}, + WebPkiServerVerifier, + }, + crypto::{verify_tls12_signature, verify_tls13_signature, CryptoProvider}, + pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime}, + CertificateError, ClientConfig, ClientConnection, Error as TlsError, RootCertStore, }; use crate::error::Error; @@ -85,7 +88,15 @@ pub async fn handshake(socket: S, tls_config: TlsConfig<'_>) -> Result) -> Result, Error> { +fn certs_from_pem(pem: Vec) -> Result>, Error> { let cur = Cursor::new(pem); let mut reader = BufReader::new(cur); - rustls_pemfile::certs(&mut reader)? - .into_iter() - .map(|v| Ok(rustls::Certificate(v))) + rustls_pemfile::certs(&mut reader) + .map(|result| result.map_err(|err| Error::Tls(err.into()))) .collect() } -fn private_key_from_pem(pem: Vec) -> Result { +fn private_key_from_pem(pem: Vec) -> Result, Error> { let cur = Cursor::new(pem); let mut reader = BufReader::new(cur); - - loop { - match rustls_pemfile::read_one(&mut reader)? { - Some( - rustls_pemfile::Item::RSAKey(key) - | rustls_pemfile::Item::PKCS8Key(key) - | rustls_pemfile::Item::ECKey(key), - ) => return Ok(rustls::PrivateKey(key)), - None => break, - _ => {} - } + match rustls_pemfile::private_key(&mut reader) { + Ok(Some(key)) => Ok(key), + Ok(None) => Err(Error::Configuration("no keys found pem file".into())), + Err(e) => Err(Error::Configuration(e.to_string().into())), } - - Err(Error::Configuration("no keys found pem file".into())) } -struct DummyTlsVerifier; +#[derive(Debug)] +struct DummyTlsVerifier { + provider: Arc, +} impl ServerCertVerifier for DummyTlsVerifier { fn verify_server_cert( &self, - _end_entity: &rustls::Certificate, - _intermediates: &[rustls::Certificate], - _server_name: &ServerName, - _scts: &mut dyn Iterator, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &ServerName<'_>, _ocsp_response: &[u8], - _now: SystemTime, + _now: UnixTime, ) -> Result { Ok(ServerCertVerified::assertion()) } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> Result { + verify_tls12_signature( + message, + cert, + dss, + &self.provider.signature_verification_algorithms, + ) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> Result { + verify_tls13_signature( + message, + cert, + dss, + &self.provider.signature_verification_algorithms, + ) + } + + fn supported_verify_schemes(&self) -> Vec { + self.provider + .signature_verification_algorithms + .supported_schemes() + } } +#[derive(Debug)] pub struct NoHostnameTlsVerifier { - verifier: WebPkiVerifier, + verifier: Arc, } impl ServerCertVerifier for NoHostnameTlsVerifier { fn verify_server_cert( &self, - end_entity: &rustls::Certificate, - intermediates: &[rustls::Certificate], - server_name: &ServerName, - scts: &mut dyn Iterator, + end_entity: &CertificateDer<'_>, + intermediates: &[CertificateDer<'_>], + server_name: &ServerName<'_>, ocsp_response: &[u8], - now: SystemTime, + now: UnixTime, ) -> Result { match self.verifier.verify_server_cert( end_entity, intermediates, server_name, - scts, ocsp_response, now, ) { @@ -247,4 +283,26 @@ impl ServerCertVerifier for NoHostnameTlsVerifier { res => res, } } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> Result { + self.verifier.verify_tls12_signature(message, cert, dss) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> Result { + self.verifier.verify_tls13_signature(message, cert, dss) + } + + fn supported_verify_schemes(&self) -> Vec { + self.verifier.supported_verify_schemes() + } } diff --git a/sqlx-macros-core/Cargo.toml b/sqlx-macros-core/Cargo.toml index aa9bb95044..7a7ba1a30e 100644 --- a/sqlx-macros-core/Cargo.toml +++ b/sqlx-macros-core/Cargo.toml @@ -15,7 +15,8 @@ _rt-async-std = ["async-std", "sqlx-core/_rt-async-std"] _rt-tokio = ["tokio", "sqlx-core/_rt-tokio"] _tls-native-tls = ["sqlx-core/_tls-native-tls"] -_tls-rustls = ["sqlx-core/_tls-rustls"] +_tls-rustls-aws-lc-rs = ["sqlx-core/_tls-rustls-aws-lc-rs"] +_tls-rustls-ring = ["sqlx-core/_tls-rustls-ring"] # SQLx features derive = [] diff --git a/sqlx-macros/Cargo.toml b/sqlx-macros/Cargo.toml index cb4d1b91c3..813a00b46d 100644 --- a/sqlx-macros/Cargo.toml +++ b/sqlx-macros/Cargo.toml @@ -18,7 +18,8 @@ _rt-async-std = ["sqlx-macros-core/_rt-async-std"] _rt-tokio = ["sqlx-macros-core/_rt-tokio"] _tls-native-tls = ["sqlx-macros-core/_tls-native-tls"] -_tls-rustls = ["sqlx-macros-core/_tls-rustls"] +_tls-rustls-aws-lc-rs = ["sqlx-macros-core/_tls-rustls-aws-lc-rs"] +_tls-rustls-ring = ["sqlx-macros-core/_tls-rustls-ring"] # SQLx features derive = ["sqlx-macros-core/derive"]