quack-ruby 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/LICENSE-DuckDB +7 -0
  4. data/LICENSE-QuackProtocol +21 -0
  5. data/LICENSE-Rust-dependencies.json +2057 -0
  6. data/README.md +229 -0
  7. data/ext/quack/Cargo.lock +1803 -0
  8. data/ext/quack/Cargo.toml +17 -0
  9. data/ext/quack/extconf.rb +4 -0
  10. data/ext/quack/src/lib.rs +647 -0
  11. data/ext/quack/vendor/quack_protocol/Cargo.lock +1907 -0
  12. data/ext/quack/vendor/quack_protocol/Cargo.toml +41 -0
  13. data/ext/quack/vendor/quack_protocol/LICENSE +21 -0
  14. data/ext/quack/vendor/quack_protocol/PATCHES.md +16 -0
  15. data/ext/quack/vendor/quack_protocol/README.md +272 -0
  16. data/ext/quack/vendor/quack_protocol/src/arrow/columns.rs +399 -0
  17. data/ext/quack/vendor/quack_protocol/src/arrow/mod.rs +134 -0
  18. data/ext/quack/vendor/quack_protocol/src/arrow/schema.rs +103 -0
  19. data/ext/quack/vendor/quack_protocol/src/arrow/tests.rs +851 -0
  20. data/ext/quack/vendor/quack_protocol/src/arrow/values.rs +150 -0
  21. data/ext/quack/vendor/quack_protocol/src/binary.rs +504 -0
  22. data/ext/quack/vendor/quack_protocol/src/builders.rs +217 -0
  23. data/ext/quack/vendor/quack_protocol/src/client.rs +1486 -0
  24. data/ext/quack/vendor/quack_protocol/src/constants.rs +11 -0
  25. data/ext/quack/vendor/quack_protocol/src/errors.rs +103 -0
  26. data/ext/quack/vendor/quack_protocol/src/inspection.rs +250 -0
  27. data/ext/quack/vendor/quack_protocol/src/json.rs +225 -0
  28. data/ext/quack/vendor/quack_protocol/src/lib.rs +54 -0
  29. data/ext/quack/vendor/quack_protocol/src/logical_types.rs +977 -0
  30. data/ext/quack/vendor/quack_protocol/src/messages.rs +843 -0
  31. data/ext/quack/vendor/quack_protocol/src/pool.rs +741 -0
  32. data/ext/quack/vendor/quack_protocol/src/protocol_tests.rs +759 -0
  33. data/ext/quack/vendor/quack_protocol/src/proxy_error.rs +77 -0
  34. data/ext/quack/vendor/quack_protocol/src/sql.rs +359 -0
  35. data/ext/quack/vendor/quack_protocol/src/tls.rs +354 -0
  36. data/ext/quack/vendor/quack_protocol/src/values.rs +77 -0
  37. data/ext/quack/vendor/quack_protocol/src/vector.rs +1364 -0
  38. data/ext/quack/vendor/quack_protocol/testdata/tls/key.der +0 -0
  39. data/ext/quack/vendor/quack_protocol/testdata/tls/spki.der +0 -0
  40. data/ext/quack/vendor/quack_protocol/testdata/tls/v1-cert.der +0 -0
  41. data/ext/quack/vendor/quack_protocol/testdata/tls/v3-cert.der +0 -0
  42. data/lib/quack/codec.rb +263 -0
  43. data/lib/quack/connection.rb +131 -0
  44. data/lib/quack/logical_types.rb +72 -0
  45. data/lib/quack/result.rb +64 -0
  46. data/lib/quack/sql.rb +183 -0
  47. data/lib/quack/types.rb +40 -0
  48. data/lib/quack/variant.rb +298 -0
  49. data/lib/quack.rb +34 -0
  50. metadata +136 -0
@@ -0,0 +1,354 @@
1
+ use std::sync::Arc;
2
+
3
+ use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
4
+ use rustls::crypto::{CryptoProvider, verify_tls13_signature_with_raw_key};
5
+ use rustls::pki_types::{CertificateDer, ServerName, SubjectPublicKeyInfoDer, UnixTime};
6
+ use rustls::{ClientConfig, DigitallySignedStruct, SignatureScheme};
7
+ use x509_parser::prelude::{FromDer, X509Certificate};
8
+
9
+ use crate::errors::{QuackError, Result};
10
+
11
+ const SHA256_LEN: usize = 32;
12
+
13
+ /// SHA-256 fingerprint of the one server certificate a client trusts.
14
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
15
+ pub(crate) struct CertificatePin([u8; SHA256_LEN]);
16
+
17
+ impl CertificatePin {
18
+ /// Parses a fingerprint the way DuckDB's `ssl_fingerprint` does: hex
19
+ /// digits in any case, optionally separated by colons or spaces, with an
20
+ /// optional `sha256:` prefix.
21
+ pub(crate) fn parse(fingerprint: &str) -> Result<Self> {
22
+ let trimmed = fingerprint.trim();
23
+ let digits = match trimmed.get(..7) {
24
+ Some(prefix) if prefix.eq_ignore_ascii_case("sha256:") => &trimmed[7..],
25
+ _ => trimmed,
26
+ };
27
+ let digits: String = digits.chars().filter(|c| !matches!(c, ':' | ' ')).collect();
28
+ let mut bytes = [0; SHA256_LEN];
29
+ hex::decode_to_slice(&digits, &mut bytes).map_err(|err| {
30
+ QuackError::protocol(format!(
31
+ "invalid ssl_fingerprint {fingerprint:?}: expected the 64 hex digits of the \
32
+ server certificate's SHA-256 fingerprint, optionally separated by colons ({err})"
33
+ ))
34
+ })?;
35
+ Ok(Self(bytes))
36
+ }
37
+
38
+ fn matches(&self, certificate: &CertificateDer<'_>) -> bool {
39
+ ring::digest::digest(&ring::digest::SHA256, certificate.as_ref()).as_ref() == self.0
40
+ }
41
+ }
42
+
43
+ /// A rustls client configuration that trusts exactly the certificate with
44
+ /// `pin`'s fingerprint, which is how DuckDB clients authenticate a Quack
45
+ /// server's self-signed certificate. CA and hostname checks are skipped; the
46
+ /// handshake signature is still verified against the pinned certificate's key.
47
+ ///
48
+ /// Pinned connections use TLS 1.3 only: the signature is checked against the
49
+ /// certificate's raw public key, which rustls supports for TLS 1.3, so that
50
+ /// the X.509 v1 certificates `quack_generate_keys()` produces are accepted
51
+ /// (webpki parses v3 certificates only).
52
+ pub(crate) fn pinned_client_config(pin: CertificatePin) -> Result<ClientConfig> {
53
+ let provider = Arc::new(rustls::crypto::ring::default_provider());
54
+ let verifier = PinnedCertVerifier {
55
+ pin,
56
+ provider: Arc::clone(&provider),
57
+ };
58
+ let mut config = ClientConfig::builder_with_provider(provider)
59
+ .with_protocol_versions(&[&rustls::version::TLS13])
60
+ .map_err(|err| QuackError::protocol(format!("failed to configure TLS: {err}")))?
61
+ .dangerous()
62
+ .with_custom_certificate_verifier(Arc::new(verifier))
63
+ .with_no_client_auth();
64
+ // The ALPN list reqwest sets on the configurations it builds itself.
65
+ config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
66
+ Ok(config)
67
+ }
68
+
69
+ #[derive(Debug)]
70
+ struct PinnedCertVerifier {
71
+ pin: CertificatePin,
72
+ provider: Arc<CryptoProvider>,
73
+ }
74
+
75
+ impl ServerCertVerifier for PinnedCertVerifier {
76
+ fn verify_server_cert(
77
+ &self,
78
+ end_entity: &CertificateDer<'_>,
79
+ _intermediates: &[CertificateDer<'_>],
80
+ _server_name: &ServerName<'_>,
81
+ _ocsp_response: &[u8],
82
+ _now: UnixTime,
83
+ ) -> std::result::Result<ServerCertVerified, rustls::Error> {
84
+ if self.pin.matches(end_entity) {
85
+ Ok(ServerCertVerified::assertion())
86
+ } else {
87
+ Err(rustls::Error::General(
88
+ "server certificate does not match the pinned ssl_fingerprint".to_string(),
89
+ ))
90
+ }
91
+ }
92
+
93
+ fn verify_tls12_signature(
94
+ &self,
95
+ _message: &[u8],
96
+ _cert: &CertificateDer<'_>,
97
+ _dss: &DigitallySignedStruct,
98
+ ) -> std::result::Result<HandshakeSignatureValid, rustls::Error> {
99
+ Err(rustls::Error::General(
100
+ "pinned connections use TLS 1.3 only".to_string(),
101
+ ))
102
+ }
103
+
104
+ fn verify_tls13_signature(
105
+ &self,
106
+ message: &[u8],
107
+ cert: &CertificateDer<'_>,
108
+ dss: &DigitallySignedStruct,
109
+ ) -> std::result::Result<HandshakeSignatureValid, rustls::Error> {
110
+ let spki = subject_public_key_info(cert.as_ref()).ok_or_else(|| {
111
+ rustls::Error::General(
112
+ "server certificate has no readable subject public key".to_string(),
113
+ )
114
+ })?;
115
+ verify_tls13_signature_with_raw_key(
116
+ message,
117
+ &SubjectPublicKeyInfoDer::from(spki),
118
+ dss,
119
+ &self.provider.signature_verification_algorithms,
120
+ )
121
+ }
122
+
123
+ fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
124
+ self.provider
125
+ .signature_verification_algorithms
126
+ .supported_schemes()
127
+ }
128
+ }
129
+
130
+ /// The DER-encoded `subjectPublicKeyInfo` of an X.509 certificate of any
131
+ /// version, or `None` if `certificate` is not exactly one well-formed
132
+ /// certificate.
133
+ fn subject_public_key_info(certificate: &[u8]) -> Option<&[u8]> {
134
+ match X509Certificate::from_der(certificate) {
135
+ Ok(([], certificate)) => Some(certificate.tbs_certificate.subject_pki.raw),
136
+ _ => None,
137
+ }
138
+ }
139
+
140
+ /// A local HTTPS server for handshake tests, and certificates to serve.
141
+ #[cfg(test)]
142
+ pub(crate) mod test_server {
143
+ use std::sync::Arc;
144
+
145
+ use rustls::ServerConfig;
146
+ use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
147
+ use rustls::server::{ClientHello, ResolvesServerCert};
148
+ use rustls::sign::CertifiedKey;
149
+ use tokio::io::{AsyncReadExt, AsyncWriteExt};
150
+ use tokio::net::TcpListener;
151
+ use tokio_rustls::TlsAcceptor;
152
+
153
+ // Two self-signed certificates for one P-256 key: an X.509 v1 certificate,
154
+ // the version `quack_generate_keys()` produces, and a v3 one from
155
+ // `openssl req -x509`. The fingerprints are what
156
+ // `openssl x509 -noout -fingerprint -sha256` prints for them.
157
+ pub(crate) const V1_CERT: &[u8] = include_bytes!("../testdata/tls/v1-cert.der");
158
+ pub(crate) const V1_CERT_FINGERPRINT: &str = "01:50:96:0A:D8:17:B9:61:A1:A9:08:AB:4A:96:D0:A0:10:C3:02:87:B1:DB:AC:EE:33:92:4F:15:C1:2B:53:B1";
159
+ pub(crate) const V3_CERT: &[u8] = include_bytes!("../testdata/tls/v3-cert.der");
160
+ pub(crate) const V3_CERT_FINGERPRINT: &str = "A5:B7:18:96:27:21:76:41:25:8E:6E:68:0E:1E:DF:F5:31:23:F0:45:CA:44:69:82:9A:86:42:EE:C6:5E:05:33";
161
+ // The PKCS#8 private key both certificates are issued for.
162
+ const KEY: &[u8] = include_bytes!("../testdata/tls/key.der");
163
+
164
+ /// Serves `response`, a raw HTTP/1.1 response, over TLS 1.3 with
165
+ /// `certificate` to every connection on a local port, and returns the port.
166
+ pub(crate) async fn serve(certificate: &'static [u8], response: String) -> u16 {
167
+ let provider = Arc::new(rustls::crypto::ring::default_provider());
168
+ // Not `with_single_cert`: it checks the key against the certificate
169
+ // through webpki, which rejects v1 certificates.
170
+ let key = provider
171
+ .key_provider
172
+ .load_private_key(PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(KEY)))
173
+ .unwrap();
174
+ let certified = CertifiedKey::new(vec![CertificateDer::from(certificate)], key);
175
+ let config = ServerConfig::builder_with_provider(provider)
176
+ .with_protocol_versions(&[&rustls::version::TLS13])
177
+ .unwrap()
178
+ .with_no_client_auth()
179
+ .with_cert_resolver(Arc::new(SingleCert(Arc::new(certified))));
180
+ let acceptor = TlsAcceptor::from(Arc::new(config));
181
+ let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
182
+ let port = listener.local_addr().unwrap().port();
183
+ tokio::spawn(async move {
184
+ while let Ok((stream, _)) = listener.accept().await {
185
+ let acceptor = acceptor.clone();
186
+ let response = response.clone();
187
+ tokio::spawn(async move {
188
+ // Handshakes a test expects to fail end here.
189
+ let Ok(mut stream) = acceptor.accept(stream).await else {
190
+ return;
191
+ };
192
+ let mut request = Vec::new();
193
+ let mut buf = [0; 1024];
194
+ // Headers are enough: no test needs the body.
195
+ while !request.windows(4).any(|window| window == b"\r\n\r\n") {
196
+ match stream.read(&mut buf).await {
197
+ Ok(0) | Err(_) => return,
198
+ Ok(n) => request.extend_from_slice(&buf[..n]),
199
+ }
200
+ }
201
+ let _ = stream.write_all(response.as_bytes()).await;
202
+ let _ = stream.shutdown().await;
203
+ });
204
+ }
205
+ });
206
+ port
207
+ }
208
+
209
+ #[derive(Debug)]
210
+ struct SingleCert(Arc<CertifiedKey>);
211
+
212
+ impl ResolvesServerCert for SingleCert {
213
+ fn resolve(&self, _hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
214
+ Some(Arc::clone(&self.0))
215
+ }
216
+ }
217
+
218
+ pub(crate) fn ok_response() -> String {
219
+ "HTTP/1.1 200 OK\r\ncontent-length: 2\r\nconnection: close\r\n\r\nok".to_string()
220
+ }
221
+ }
222
+
223
+ #[cfg(test)]
224
+ mod tests {
225
+ use std::error::Error as _;
226
+
227
+ use super::test_server::{self, V1_CERT, V1_CERT_FINGERPRINT, V3_CERT, V3_CERT_FINGERPRINT};
228
+ use super::*;
229
+
230
+ const FINGERPRINT: &str = "2B:31:F0:0A:9C:44:D1:7E:65:B8:03:FE:12:34:56:78:9A:BC:DE:F0:01:23:45:67:89:AB:CD:EF:10:32:54:EA";
231
+ // `openssl pkey -pubout -outform der` for the key both test certificates share.
232
+ const SPKI: &[u8] = include_bytes!("../testdata/tls/spki.der");
233
+
234
+ async fn get_pinned(port: u16, fingerprint: &str) -> reqwest::Result<String> {
235
+ let config = pinned_client_config(CertificatePin::parse(fingerprint).unwrap()).unwrap();
236
+ reqwest::Client::builder()
237
+ .use_preconfigured_tls(config)
238
+ .build()?
239
+ .get(format!("https://127.0.0.1:{port}/"))
240
+ .send()
241
+ .await?
242
+ .text()
243
+ .await
244
+ }
245
+
246
+ // The error and its sources, which is where reqwest keeps the TLS error.
247
+ fn error_chain(err: &reqwest::Error) -> String {
248
+ let mut chain = err.to_string();
249
+ let mut source = err.source();
250
+ while let Some(err) = source {
251
+ chain.push_str(&format!(": {err}"));
252
+ source = err.source();
253
+ }
254
+ chain
255
+ }
256
+
257
+ #[test]
258
+ fn parses_duckdb_fingerprint_spellings() {
259
+ let expected = CertificatePin::parse(FINGERPRINT).unwrap();
260
+ let bare = FINGERPRINT.replace(':', "");
261
+ for spelling in [
262
+ bare.clone(),
263
+ bare.to_lowercase(),
264
+ format!("sha256:{FINGERPRINT}"),
265
+ format!("SHA256:{bare}"),
266
+ format!(" {} ", FINGERPRINT.replace(':', " ")),
267
+ ] {
268
+ assert_eq!(
269
+ CertificatePin::parse(&spelling).unwrap(),
270
+ expected,
271
+ "{spelling}"
272
+ );
273
+ }
274
+ assert_eq!(expected.0[0], 0x2B);
275
+ assert_eq!(expected.0[31], 0xEA);
276
+ }
277
+
278
+ #[test]
279
+ fn rejects_malformed_fingerprints() {
280
+ for bad in [
281
+ "",
282
+ "2B:31",
283
+ &FINGERPRINT.replace("EA", "EAFF"),
284
+ &FINGERPRINT.replace("2B", "ZZ"),
285
+ "sha1:2B31F00A9C44D17E65B803FE123456789ABCDEF00123456789ABCDEF103254EA",
286
+ ] {
287
+ let err = CertificatePin::parse(bad).unwrap_err();
288
+ assert!(
289
+ err.to_string().contains("invalid ssl_fingerprint"),
290
+ "{bad}: {err}"
291
+ );
292
+ }
293
+ }
294
+
295
+ #[test]
296
+ fn verifier_accepts_only_the_pinned_certificate() {
297
+ let cert = CertificateDer::from(V1_CERT);
298
+ let server_name = ServerName::try_from("quack.example").unwrap();
299
+ let verify = |fingerprint: &str| {
300
+ PinnedCertVerifier {
301
+ pin: CertificatePin::parse(fingerprint).unwrap(),
302
+ provider: Arc::new(rustls::crypto::ring::default_provider()),
303
+ }
304
+ .verify_server_cert(&cert, &[], &server_name, &[], UnixTime::now())
305
+ };
306
+
307
+ assert!(verify(V1_CERT_FINGERPRINT).is_ok());
308
+ let err = verify(FINGERPRINT).unwrap_err();
309
+ assert!(err.to_string().contains("ssl_fingerprint"), "{err}");
310
+ }
311
+
312
+ #[test]
313
+ fn reads_the_public_key_of_v1_and_v3_certificates() {
314
+ for cert in [V1_CERT, V3_CERT] {
315
+ assert_eq!(subject_public_key_info(cert), Some(SPKI));
316
+ }
317
+ }
318
+
319
+ #[test]
320
+ fn malformed_certificates_have_no_public_key() {
321
+ assert_eq!(subject_public_key_info(&[]), None);
322
+ assert_eq!(subject_public_key_info(&V1_CERT[..V1_CERT.len() / 2]), None);
323
+ assert_eq!(subject_public_key_info(&[V1_CERT, &[0]].concat()), None);
324
+ assert_eq!(subject_public_key_info(SPKI), None);
325
+ }
326
+
327
+ #[tokio::test]
328
+ async fn pinned_handshake_succeeds_with_v1_and_v3_certificates() {
329
+ for (cert, fingerprint) in [
330
+ (V1_CERT, V1_CERT_FINGERPRINT),
331
+ (V3_CERT, V3_CERT_FINGERPRINT),
332
+ ] {
333
+ let port = test_server::serve(cert, test_server::ok_response()).await;
334
+ assert_eq!(
335
+ get_pinned(port, fingerprint).await.unwrap(),
336
+ "ok",
337
+ "{fingerprint}"
338
+ );
339
+ }
340
+ }
341
+
342
+ #[tokio::test]
343
+ async fn pinned_handshake_fails_for_another_certificate() {
344
+ let port = test_server::serve(V1_CERT, test_server::ok_response()).await;
345
+ for fingerprint in [FINGERPRINT, V3_CERT_FINGERPRINT] {
346
+ let err = get_pinned(port, fingerprint).await.unwrap_err();
347
+ let chain = error_chain(&err);
348
+ assert!(
349
+ chain.contains("does not match the pinned ssl_fingerprint"),
350
+ "{chain}"
351
+ );
352
+ }
353
+ }
354
+ }
@@ -0,0 +1,77 @@
1
+ use chrono::NaiveDate;
2
+
3
+ use crate::errors::{QuackError, Result};
4
+ use crate::vector::{
5
+ DateValue, DecimalValue, IntervalValue, TimeTzValue, TimeUnit, TimeValue, TimestampUnit,
6
+ TimestampValue, Value,
7
+ };
8
+
9
+ pub fn decimal_value(value: impl ToString, width: u64, scale: u64) -> Result<Value> {
10
+ Ok(Value::Decimal(DecimalValue {
11
+ value: parse_decimal_value(&value.to_string(), scale)?,
12
+ width,
13
+ scale,
14
+ }))
15
+ }
16
+
17
+ pub fn date_value(days: i32) -> Value {
18
+ Value::Date(DateValue { days })
19
+ }
20
+
21
+ pub fn date_from_iso_date(value: &str) -> Result<Value> {
22
+ let date = NaiveDate::parse_from_str(value, "%Y-%m-%d")
23
+ .map_err(|_| QuackError::protocol(format!("invalid ISO date {value}")))?;
24
+ let epoch = NaiveDate::from_ymd_opt(1970, 1, 1).expect("valid epoch");
25
+ Ok(date_value((date - epoch).num_days() as i32))
26
+ }
27
+
28
+ pub fn time_value(value: i64, unit: TimeUnit) -> Value {
29
+ Value::Time(TimeValue { unit, value })
30
+ }
31
+
32
+ pub fn time_tz_value(bits: i64) -> Value {
33
+ Value::TimeTz(TimeTzValue { bits })
34
+ }
35
+
36
+ pub fn timestamp_value(value: i64, unit: TimestampUnit, timezone_utc: bool) -> Value {
37
+ Value::Timestamp(TimestampValue {
38
+ unit,
39
+ value,
40
+ timezone_utc,
41
+ })
42
+ }
43
+
44
+ pub fn interval_value(months: i32, days: i32, micros: i64) -> Value {
45
+ Value::Interval(IntervalValue {
46
+ months,
47
+ days,
48
+ micros,
49
+ })
50
+ }
51
+
52
+ fn parse_decimal_value(value: &str, scale: u64) -> Result<i128> {
53
+ let text = value.trim();
54
+ let negative = text.starts_with('-');
55
+ let unsigned = text.strip_prefix(['-', '+']).unwrap_or(text);
56
+ let (integer, fraction) = unsigned.split_once('.').unwrap_or((unsigned, ""));
57
+ if !integer.bytes().all(|byte| byte.is_ascii_digit())
58
+ || !fraction.bytes().all(|byte| byte.is_ascii_digit())
59
+ {
60
+ return Err(QuackError::protocol(format!(
61
+ "invalid decimal value {value}"
62
+ )));
63
+ }
64
+ let mut padded_fraction = fraction.to_string();
65
+ while padded_fraction.len() < scale as usize {
66
+ padded_fraction.push('0');
67
+ }
68
+ padded_fraction.truncate(scale as usize);
69
+ let unscaled = format!(
70
+ "{}{}",
71
+ if integer.is_empty() { "0" } else { integer },
72
+ padded_fraction
73
+ )
74
+ .parse::<i128>()
75
+ .map_err(|_| QuackError::protocol(format!("invalid decimal value {value}")))?;
76
+ Ok(if negative { -unscaled } else { unscaled })
77
+ }