fraiseql-wire 2.2.0

Streaming JSON query engine for Postgres 17
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
//! TLS configuration and support for secure connections to Postgres.
//!
//! This module provides TLS configuration for connecting to remote Postgres servers.
//! TLS is recommended for all non-local connections to prevent credential interception.

use crate::{Result, WireError};
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
use rustls::RootCertStore;
use rustls::{ClientConfig, DigitallySignedStruct, SignatureScheme};
use rustls_pemfile::Item;
use std::fmt::Debug;
use std::fs;
use std::sync::Arc;

/// TLS configuration for secure Postgres connections.
///
/// Provides a builder for creating TLS configurations with various certificate handling options.
/// By default, server certificates are validated against system root certificates.
///
/// # Examples
///
/// ```no_run
/// // Requires: system root certificates or a CA certificate file on disk.
/// use fraiseql_wire::connection::TlsConfig;
///
/// // With system root certificates (production)
/// let tls = TlsConfig::builder()
///     .verify_hostname(true)
///     .build()?;
///
/// // With custom CA certificate
/// let tls = TlsConfig::builder()
///     .ca_cert_path("/path/to/ca.pem")
///     .verify_hostname(true)
///     .build()?;
///
/// // For development (danger: disables verification)
/// let tls = TlsConfig::builder()
///     .danger_accept_invalid_certs(true)
///     .danger_accept_invalid_hostnames(true)
///     .build()?;
/// # fraiseql_wire::Result::Ok(())
/// ```
#[derive(Clone)]
pub struct TlsConfig {
    /// Path to CA certificate file (None = use system roots)
    ca_cert_path: Option<String>,
    /// Whether to verify hostname matches certificate
    verify_hostname: bool,
    /// Whether to accept invalid certificates (development only)
    danger_accept_invalid_certs: bool,
    /// Whether to accept invalid hostnames (development only)
    danger_accept_invalid_hostnames: bool,
    /// Compiled rustls `ClientConfig`
    client_config: Arc<ClientConfig>,
}

impl TlsConfig {
    /// Create a new TLS configuration builder.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// // Requires: system root certificates.
    /// use fraiseql_wire::connection::TlsConfig;
    /// let tls = TlsConfig::builder()
    ///     .verify_hostname(true)
    ///     .build()?;
    /// # fraiseql_wire::Result::Ok(())
    /// ```
    pub fn builder() -> TlsConfigBuilder {
        TlsConfigBuilder::default()
    }

    /// Get the rustls `ClientConfig` for this TLS configuration.
    pub fn client_config(&self) -> Arc<ClientConfig> {
        self.client_config.clone()
    }

    /// Check if hostname verification is enabled.
    pub const fn verify_hostname(&self) -> bool {
        self.verify_hostname
    }

    /// Check if invalid certificates are accepted (development only).
    pub const fn danger_accept_invalid_certs(&self) -> bool {
        self.danger_accept_invalid_certs
    }

    /// Check if invalid hostnames are accepted (development only).
    pub const fn danger_accept_invalid_hostnames(&self) -> bool {
        self.danger_accept_invalid_hostnames
    }
}

impl std::fmt::Debug for TlsConfig {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("TlsConfig")
            .field("ca_cert_path", &self.ca_cert_path)
            .field("verify_hostname", &self.verify_hostname)
            .field(
                "danger_accept_invalid_certs",
                &self.danger_accept_invalid_certs,
            )
            .field(
                "danger_accept_invalid_hostnames",
                &self.danger_accept_invalid_hostnames,
            )
            .field("client_config", &"<ClientConfig>")
            .finish()
    }
}

/// Builder for TLS configuration.
///
/// Provides a fluent API for constructing TLS configurations with custom settings.
#[must_use = "call .build() to construct the final value"]
pub struct TlsConfigBuilder {
    ca_cert_path: Option<String>,
    verify_hostname: bool,
    danger_accept_invalid_certs: bool,
    danger_accept_invalid_hostnames: bool,
}

impl Default for TlsConfigBuilder {
    fn default() -> Self {
        Self {
            ca_cert_path: None,
            verify_hostname: true,
            danger_accept_invalid_certs: false,
            danger_accept_invalid_hostnames: false,
        }
    }
}

impl TlsConfigBuilder {
    /// Set the path to a custom CA certificate file (PEM format).
    ///
    /// If not set, system root certificates will be used.
    ///
    /// # Arguments
    ///
    /// * `path` - Path to CA certificate file in PEM format
    ///
    /// # Examples
    ///
    /// ```no_run
    /// // Requires: CA certificate file at the specified path.
    /// use fraiseql_wire::connection::TlsConfig;
    /// let tls = TlsConfig::builder()
    ///     .ca_cert_path("/etc/ssl/certs/ca.pem")
    ///     .build()?;
    /// # fraiseql_wire::Result::Ok(())
    /// ```
    pub fn ca_cert_path(mut self, path: impl Into<String>) -> Self {
        self.ca_cert_path = Some(path.into());
        self
    }

    /// Enable or disable hostname verification (default: enabled).
    ///
    /// When enabled, the certificate's subject alternative names (SANs) are verified
    /// to match the server hostname.
    ///
    /// # Arguments
    ///
    /// * `verify` - Whether to verify hostname matches certificate
    ///
    /// # Examples
    ///
    /// ```no_run
    /// // Requires: system root certificates.
    /// use fraiseql_wire::connection::TlsConfig;
    /// let tls = TlsConfig::builder()
    ///     .verify_hostname(true)
    ///     .build()?;
    /// # fraiseql_wire::Result::Ok(())
    /// ```
    pub const fn verify_hostname(mut self, verify: bool) -> Self {
        self.verify_hostname = verify;
        self
    }

    /// ⚠️ **DANGER**: Accept invalid certificates (development only).
    ///
    /// **NEVER use in production.** This disables certificate validation entirely,
    /// making the connection vulnerable to man-in-the-middle attacks.
    ///
    /// Only use for testing with self-signed certificates.
    ///
    /// # Errors
    ///
    /// [`TlsConfigBuilder::build`] returns `WireError::Config` when this option is `true`
    /// in a release build (`cfg(not(debug_assertions))`).
    ///
    /// # Examples
    ///
    /// ```no_run
    /// // Requires: debug build only (returns Err in release mode).
    /// use fraiseql_wire::connection::TlsConfig;
    /// let tls = TlsConfig::builder()
    ///     .danger_accept_invalid_certs(true)
    ///     .build()?;
    /// # fraiseql_wire::Result::Ok(())
    /// ```
    pub const fn danger_accept_invalid_certs(mut self, accept: bool) -> Self {
        self.danger_accept_invalid_certs = accept;
        self
    }

    /// ⚠️ **DANGER**: Accept invalid hostnames (development only).
    ///
    /// **NEVER use in production.** This disables hostname verification,
    /// making the connection vulnerable to man-in-the-middle attacks.
    ///
    /// Only use for testing with self-signed certificates where you can't
    /// match the hostname.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// // Requires: debug build only.
    /// use fraiseql_wire::connection::TlsConfig;
    /// let tls = TlsConfig::builder()
    ///     .danger_accept_invalid_hostnames(true)
    ///     .build()?;
    /// # fraiseql_wire::Result::Ok(())
    /// ```
    pub const fn danger_accept_invalid_hostnames(mut self, accept: bool) -> Self {
        self.danger_accept_invalid_hostnames = accept;
        self
    }

    /// Build the TLS configuration.
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - CA certificate file cannot be read
    /// - CA certificate is invalid PEM
    /// - Dangerous options are configured incorrectly
    ///
    /// # Examples
    ///
    /// ```no_run
    /// // Requires: system root certificates.
    /// use fraiseql_wire::connection::TlsConfig;
    /// let tls = TlsConfig::builder()
    ///     .verify_hostname(true)
    ///     .build()?;
    /// # fraiseql_wire::Result::Ok(())
    /// ```
    pub fn build(self) -> Result<TlsConfig> {
        // SECURITY: Validate TLS configuration before creating client
        validate_tls_security(self.danger_accept_invalid_certs)?;

        let client_config = if self.danger_accept_invalid_certs {
            // Create a client config that accepts any certificate (development only)
            let verifier = Arc::new(NoVerifier);
            Arc::new(
                ClientConfig::builder()
                    .dangerous()
                    .with_custom_certificate_verifier(verifier)
                    .with_no_client_auth(),
            )
        } else {
            // Load root certificates
            let root_store = if let Some(ca_path) = &self.ca_cert_path {
                // Load custom CA certificate from file
                self.load_custom_ca(ca_path)?
            } else {
                // Use system root certificates via rustls-native-certs
                let result = rustls_native_certs::load_native_certs();

                let mut store = RootCertStore::empty();
                for cert in result.certs {
                    let _ = store.add_parsable_certificates(std::iter::once(cert));
                }

                // Log warnings if there were errors, but don't fail
                if !result.errors.is_empty() && store.is_empty() {
                    return Err(WireError::Config(
                        "Failed to load any system root certificates".to_string(),
                    ));
                }

                store
            };

            // Create ClientConfig using the correct API for rustls 0.23
            Arc::new(
                ClientConfig::builder()
                    .with_root_certificates(root_store)
                    .with_no_client_auth(),
            )
        };

        Ok(TlsConfig {
            ca_cert_path: self.ca_cert_path,
            verify_hostname: self.verify_hostname,
            danger_accept_invalid_certs: self.danger_accept_invalid_certs,
            danger_accept_invalid_hostnames: self.danger_accept_invalid_hostnames,
            client_config,
        })
    }

    /// Load a custom CA certificate from a PEM file.
    fn load_custom_ca(&self, ca_path: &str) -> Result<RootCertStore> {
        let ca_cert_data = fs::read(ca_path).map_err(|e| {
            WireError::Config(format!(
                "Failed to read CA certificate file '{}': {}",
                ca_path, e
            ))
        })?;

        let mut reader = std::io::Cursor::new(&ca_cert_data);
        let mut root_store = RootCertStore::empty();
        let mut found_certs = 0;

        // Parse PEM file and extract certificates
        loop {
            match rustls_pemfile::read_one(&mut reader) {
                Ok(Some(Item::X509Certificate(cert))) => {
                    let _ = root_store.add_parsable_certificates(std::iter::once(cert));
                    found_certs += 1;
                }
                Ok(Some(_)) => {
                    // Skip non-certificate items (private keys, etc.)
                }
                Ok(None) => {
                    // End of file
                    break;
                }
                Err(_) => {
                    return Err(WireError::Config(format!(
                        "Failed to parse CA certificate from '{}'",
                        ca_path
                    )));
                }
            }
        }

        if found_certs == 0 {
            return Err(WireError::Config(format!(
                "No valid certificates found in '{}'",
                ca_path
            )));
        }

        Ok(root_store)
    }
}

/// Validate TLS configuration for security constraints.
///
/// Enforces that release builds cannot use `danger_accept_invalid_certs`.
/// Development builds emit a warning but proceed.
///
/// # Arguments
///
/// * `danger_accept_invalid_certs` - Whether danger mode is enabled
///
/// # Errors
///
/// Returns `WireError::Config` if `danger_accept_invalid_certs` is set in a release build.
fn validate_tls_security(danger_accept_invalid_certs: bool) -> Result<()> {
    if danger_accept_invalid_certs {
        // SECURITY: Return an error in release builds to prevent accidental production use
        #[cfg(not(debug_assertions))]
        return Err(WireError::Config(
            "TLS certificate validation bypass not permitted in release builds".into(),
        ));

        // Development builds: warn but allow
        #[cfg(debug_assertions)]
        {
            tracing::warn!("TLS certificate validation is DISABLED (development only)");
            tracing::warn!("This mode is only for development with self-signed certificates");
        }
    }
    Ok(())
}

/// Parse server name from hostname for TLS SNI (Server Name Indication).
///
/// # Arguments
///
/// * `hostname` - Hostname to parse (without port)
///
/// # Returns
///
/// A string suitable for TLS server name indication
///
/// # Errors
///
/// Returns an error if the hostname is invalid.
pub fn parse_server_name(hostname: &str) -> Result<String> {
    // Remove trailing dot if present
    let hostname = hostname.trim_end_matches('.');

    // Validate hostname (basic check)
    if hostname.is_empty() || hostname.len() > 253 {
        return Err(WireError::Config(format!(
            "Invalid hostname for TLS: '{}'",
            hostname
        )));
    }

    // Check for invalid characters
    if !hostname
        .chars()
        .all(|c| c.is_alphanumeric() || c == '-' || c == '.')
    {
        return Err(WireError::Config(format!(
            "Invalid hostname for TLS: '{}'",
            hostname
        )));
    }

    Ok(hostname.to_string())
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Install a crypto provider for rustls tests.
    /// This is needed because multiple crypto providers (ring and aws-lc-rs)
    /// may be enabled via transitive dependencies, requiring explicit selection.
    fn install_crypto_provider() {
        // Try to install ring as the default provider, ignore if already installed
        let _ = rustls::crypto::ring::default_provider().install_default();
    }

    #[test]
    fn test_tls_config_builder_defaults() {
        let tls = TlsConfigBuilder::default();
        assert!(!tls.danger_accept_invalid_certs);
        assert!(!tls.danger_accept_invalid_hostnames);
        assert!(tls.verify_hostname);
        assert!(tls.ca_cert_path.is_none());
    }

    #[test]
    fn test_tls_config_builder_with_hostname_verification() {
        install_crypto_provider();

        let tls = TlsConfig::builder()
            .verify_hostname(true)
            .build()
            .expect("Failed to build TLS config");

        assert!(tls.verify_hostname());
        assert!(!tls.danger_accept_invalid_certs());
    }

    #[test]
    fn test_tls_config_builder_with_custom_ca() {
        // This test would require an actual PEM file
    }

    #[test]
    fn test_parse_server_name_valid() {
        let _name =
            parse_server_name("localhost").expect("localhost should be a valid server name");
        let _name =
            parse_server_name("example.com").expect("example.com should be a valid server name");
        let _name = parse_server_name("db.internal.example.com")
            .expect("subdomain should be a valid server name");
    }

    #[test]
    fn test_parse_server_name_trailing_dot() {
        let _name = parse_server_name("example.com.")
            .expect("trailing dot should be accepted as valid server name");
    }

    #[test]
    fn test_parse_server_name_with_port() {
        // ServerName expects just hostname, not host:port.
        // Whether this succeeds or fails depends on the rustls version,
        // so we only verify it doesn't panic.
        let _result = parse_server_name("example.com:5432");
    }

    #[test]
    fn test_tls_config_debug() {
        install_crypto_provider();

        let tls = TlsConfig::builder()
            .verify_hostname(true)
            .build()
            .expect("Failed to build TLS config");

        let debug_str = format!("{:?}", tls);
        assert!(debug_str.contains("TlsConfig"));
        assert!(debug_str.contains("verify_hostname"));
    }

    #[test]
    #[cfg(not(debug_assertions))]
    fn test_danger_mode_returns_error_in_release_build() {
        // This test only runs in release builds; danger mode must return an error
        let result = TlsConfig::builder()
            .danger_accept_invalid_certs(true)
            .build();
        assert!(
            result.is_err(),
            "danger mode must be rejected in release builds"
        );
        let err = result.unwrap_err();
        assert!(
            err.to_string().contains("not permitted in release builds"),
            "error message must explain the restriction",
        );
    }

    #[test]
    fn test_danger_mode_allowed_in_debug_build() {
        install_crypto_provider();

        let config = TlsConfig::builder()
            .danger_accept_invalid_certs(true)
            .build()
            .expect("danger mode should be allowed in debug builds");

        assert!(config.danger_accept_invalid_certs());
    }

    #[test]
    fn test_normal_tls_config_works() {
        install_crypto_provider();

        let config = TlsConfig::builder()
            .verify_hostname(true)
            .build()
            .expect("normal TLS config should build successfully");

        assert!(!config.danger_accept_invalid_certs());
    }
}

/// A certificate verifier that accepts any certificate.
///
/// ⚠️ **DANGER**: This should ONLY be used for development/testing with self-signed certificates.
/// Using this in production is a serious security vulnerability.
#[derive(Debug)]
struct NoVerifier;

impl ServerCertVerifier for NoVerifier {
    fn verify_server_cert(
        &self,
        _end_entity: &CertificateDer<'_>,
        _intermediates: &[CertificateDer<'_>],
        _server_name: &ServerName<'_>,
        _ocsp_response: &[u8],
        _now: UnixTime,
    ) -> std::result::Result<ServerCertVerified, rustls::Error> {
        // Accept any certificate
        Ok(ServerCertVerified::assertion())
    }

    fn verify_tls12_signature(
        &self,
        _message: &[u8],
        _cert: &CertificateDer<'_>,
        _dss: &DigitallySignedStruct,
    ) -> std::result::Result<HandshakeSignatureValid, rustls::Error> {
        Ok(HandshakeSignatureValid::assertion())
    }

    fn verify_tls13_signature(
        &self,
        _message: &[u8],
        _cert: &CertificateDer<'_>,
        _dss: &DigitallySignedStruct,
    ) -> std::result::Result<HandshakeSignatureValid, rustls::Error> {
        Ok(HandshakeSignatureValid::assertion())
    }

    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
        // Support all common signature schemes
        vec![
            SignatureScheme::RSA_PKCS1_SHA256,
            SignatureScheme::RSA_PKCS1_SHA384,
            SignatureScheme::RSA_PKCS1_SHA512,
            SignatureScheme::ECDSA_NISTP256_SHA256,
            SignatureScheme::ECDSA_NISTP384_SHA384,
            SignatureScheme::ECDSA_NISTP521_SHA512,
            SignatureScheme::RSA_PSS_SHA256,
            SignatureScheme::RSA_PSS_SHA384,
            SignatureScheme::RSA_PSS_SHA512,
            SignatureScheme::ED25519,
        ]
    }
}