fraiseql-wire 2.3.1

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
//! 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.
    #[must_use]
    pub fn client_config(&self) -> Arc<ClientConfig> {
        self.client_config.clone()
    }

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

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

    /// Check if invalid hostnames are accepted (development only).
    #[must_use]
    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;

/// 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,
        ]
    }
}