flowsdk 0.5.1

Safety-first, realistic, behavior-predictable messaging SDK for MQTT and more.
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
// SPDX-License-Identifier: MPL-2.0

//! Rustls-based TLS transport implementation (feature-gated)

#[cfg(feature = "rustls-tls")]
mod imp {
    use super::super::{Transport, TransportError};
    use async_trait::async_trait;
    use std::sync::Arc;
    use tokio::net::TcpStream;

    use rustls::client::danger::{ServerCertVerified, ServerCertVerifier};
    use rustls::pki_types::ServerName;
    use rustls::{ClientConfig as RustlsClientConfig, RootCertStore};
    use rustls_native_certs;
    use rustls_pki_types::pem::PemObject;
    use rustls_pki_types::{CertificateDer, PrivateKeyDer, UnixTime};

    use crate::mqtt_client::transport::parse_mqtt_address;
    use tokio_rustls::{client::TlsStream as RustlsTlsStream, TlsConnector as RustlsTlsConnector};

    /// TLS configuration for Rustls backend
    #[derive(Clone, Default, Debug)]
    pub struct RustlsTlsConfig {
        /// Use system root certificates (platform trust store). Enabled by default.
        pub use_system_roots: bool,
        /// Additional custom root certificates (DER bytes)
        pub custom_root_certs: Vec<Vec<u8>>,
        /// Optional client certificate chain (DER bytes per cert)
        pub client_cert_chain: Option<Vec<Vec<u8>>>,
        /// Optional client private key (DER bytes)
        pub client_private_key: Option<Vec<u8>>,
        /// ⚠️ DANGEROUS: Skip certificate chain validation (testing only!)
        pub danger_accept_invalid_certs: bool,
        /// ⚠️ DANGEROUS: Skip hostname verification (testing only!)
        pub danger_accept_invalid_hostnames: bool,
        /// Optional ALPN protocols (usually not needed for MQTT over TCP)
        pub alpn_protocols: Vec<Vec<u8>>,
        /// Enable TLS key logging (writes to the file specified by SSLKEYLOGFILE env var)
        pub enable_key_log: bool,
    }

    impl RustlsTlsConfig {
        /// Create a new builder for `RustlsTlsConfig`.
        pub fn builder() -> RustlsTlsConfigBuilder {
            RustlsTlsConfigBuilder::default()
        }

        /// Build a rustls ClientConfig from this configuration.
        pub fn to_client_config(&self) -> Result<RustlsClientConfig, TransportError> {
            // Prepare root store
            let mut roots = RootCertStore::empty();

            if self.use_system_roots {
                let native_certs = rustls_native_certs::load_native_certs().map_err(|e| {
                    TransportError::Tls(format!("failed to load platform root certs: {}", e))
                })?;
                for cert in native_certs {
                    roots.add(cert).map_err(|e| {
                        TransportError::Tls(format!("failed to add root cert: {:?}", e))
                    })?;
                }
            }

            // Add any custom roots
            for cert in &self.custom_root_certs {
                roots.add(cert.clone().into()).map_err(|e| {
                    TransportError::Tls(format!("failed to add custom root: {:?}", e))
                })?;
            }

            // Build rustls config with or without client auth
            let mut cfg = if let (Some(chain), Some(key)) = (
                self.client_cert_chain.clone(),
                self.client_private_key.clone(),
            ) {
                let chain_der: Vec<CertificateDer<'static>> =
                    chain.into_iter().map(CertificateDer::from).collect();
                let parsed_key = PrivateKeyDer::try_from(key).map_err(|e| {
                    TransportError::Tls(format!(
                        "failed to parse client private key DER for mTLS: {:?}",
                        e
                    ))
                })?;
                let key_der: PrivateKeyDer<'static> = parsed_key.clone_key();

                RustlsClientConfig::builder()
                    .with_root_certificates(roots)
                    .with_client_auth_cert(chain_der, key_der)
                    .map_err(|e| {
                        TransportError::Tls(format!(
                            "failed to build rustls client config with client auth: {}",
                            e
                        ))
                    })?
            } else {
                RustlsClientConfig::builder()
                    .with_root_certificates(roots)
                    .with_no_client_auth()
            };

            // Apply ALPN if provided
            if !self.alpn_protocols.is_empty() {
                cfg.alpn_protocols = self.alpn_protocols.clone();
            }

            // Apply dangerous overrides if requested
            if self.danger_accept_invalid_certs || self.danger_accept_invalid_hostnames {
                cfg.dangerous()
                    .set_certificate_verifier(Arc::new(InsecureServerCertVerifier {
                        skip_name_check: self.danger_accept_invalid_hostnames,
                    }));
            }

            // Enable TLS key logging if requested (writes to SSLKEYLOGFILE)
            if self.enable_key_log {
                cfg.key_log = Arc::new(rustls::KeyLogFile::new());
            }

            Ok(cfg)
        }
    }

    /// Builder for `RustlsTlsConfig` with ergonomic helpers
    #[derive(Default)]
    pub struct RustlsTlsConfigBuilder {
        use_system_roots: bool,
        custom_root_certs: Vec<Vec<u8>>,
        client_cert_chain: Option<Vec<Vec<u8>>>,
        client_private_key: Option<Vec<u8>>,
        danger_accept_invalid_certs: bool,
        danger_accept_invalid_hostnames: bool,
        alpn_protocols: Vec<Vec<u8>>,
        enable_key_log: bool,
    }

    impl RustlsTlsConfigBuilder {
        /// Enable/disable loading platform root certificates (default: true)
        pub fn use_system_roots(mut self, enable: bool) -> Self {
            self.use_system_roots = enable;
            self
        }

        /// Add custom root certificates from PEM bytes (may contain multiple CERTIFICATE sections).
        pub fn add_roots_from_pem(mut self, pem_data: &[u8]) -> Result<Self, TransportError> {
            let iter = CertificateDer::pem_slice_iter(pem_data);
            let mut any = false;
            for c in iter.flatten() {
                self.custom_root_certs
                    .push(c.into_owned().as_ref().to_vec());
                any = true;
            }
            if !any {
                return Err(TransportError::Tls(
                    "no valid certificates found in PEM data".to_string(),
                ));
            }
            Ok(self)
        }

        /// Load custom root certificates from a PEM file path.
        pub fn add_roots_from_pem_file(
            mut self,
            file: impl AsRef<std::path::Path>,
        ) -> Result<Self, TransportError> {
            match CertificateDer::pem_file_iter(file) {
                Ok(iter) => {
                    for item in iter.flatten() {
                        self.custom_root_certs
                            .push(item.into_owned().as_ref().to_vec());
                    }
                    Ok(self)
                }
                Err(e) => Err(TransportError::Tls(format!(
                    "failed to read/parse root certificates PEM file: {:?}",
                    e
                ))),
            }
        }

        /// Set client certificate chain and private key from PEM bytes.
        pub fn client_auth_from_pem(
            mut self,
            cert_chain_pem: &[u8],
            private_key_pem: &[u8],
        ) -> Result<Self, TransportError> {
            // Collect all CERTIFICATE sections
            let chain_iter = CertificateDer::pem_slice_iter(cert_chain_pem);
            let chain: Vec<Vec<u8>> = chain_iter
                .filter_map(|r| r.ok().map(|c| c.into_owned().as_ref().to_vec()))
                .collect();
            if chain.is_empty() {
                return Err(TransportError::Tls(
                    "no valid certificates found in client cert PEM".to_string(),
                ));
            }
            // Parse a single private key
            use std::io::Cursor;
            let mut cursor = Cursor::new(private_key_pem);
            let pk = PrivateKeyDer::from_pem_reader(&mut cursor).map_err(|e| {
                TransportError::Tls(format!("failed to parse client private key PEM: {:?}", e))
            })?;
            let der = pk.clone_key().secret_der().to_vec();

            self.client_cert_chain = Some(chain);
            self.client_private_key = Some(der);
            Ok(self)
        }

        /// Set client certificate chain and private key from PEM files.
        pub fn client_auth_from_pem_files(
            mut self,
            cert_chain_file: impl AsRef<std::path::Path>,
            private_key_file: impl AsRef<std::path::Path>,
        ) -> Result<Self, TransportError> {
            let chain = match CertificateDer::pem_file_iter(cert_chain_file) {
                Ok(iter) => iter
                    .filter_map(|r| r.ok().map(|c| c.into_owned().as_ref().to_vec()))
                    .collect(),
                Err(e) => {
                    return Err(TransportError::Tls(format!(
                        "failed to read/parse client certificate chain: {:?}",
                        e
                    )))
                }
            };
            let pk = PrivateKeyDer::from_pem_file(private_key_file).map_err(|e| {
                TransportError::Tls(format!("failed to parse client private key: {:?}", e))
            })?;
            let der = pk.clone_key().secret_der().to_vec();

            self.client_cert_chain = Some(chain);
            self.client_private_key = Some(der);
            Ok(self)
        }

        /// ⚠️ DANGEROUS: Skip certificate chain validation (testing only)
        pub fn danger_accept_invalid_certs(mut self, accept: bool) -> Self {
            self.danger_accept_invalid_certs = accept;
            self
        }

        /// ⚠️ DANGEROUS: Skip hostname verification (testing only)
        pub fn danger_accept_invalid_hostnames(mut self, accept: bool) -> Self {
            self.danger_accept_invalid_hostnames = accept;
            self
        }

        /// Configure ALPN list
        pub fn alpn_list(mut self, prots: Vec<Vec<u8>>) -> Self {
            self.alpn_protocols = prots;
            self
        }

        /// Enable TLS key logging. When enabled, TLS session keys are written to the file
        /// specified by the `SSLKEYLOGFILE` environment variable (using `rustls::KeyLogFile`).
        /// This allows tools like Wireshark to decrypt captured TLS traffic.
        pub fn enable_key_log(mut self, enable: bool) -> Self {
            self.enable_key_log = enable;
            self
        }

        /// Build the final `RustlsTlsConfig`.
        pub fn build(self) -> RustlsTlsConfig {
            RustlsTlsConfig {
                use_system_roots: self.use_system_roots,
                custom_root_certs: self.custom_root_certs,
                client_cert_chain: self.client_cert_chain,
                client_private_key: self.client_private_key,
                danger_accept_invalid_certs: self.danger_accept_invalid_certs,
                danger_accept_invalid_hostnames: self.danger_accept_invalid_hostnames,
                alpn_protocols: self.alpn_protocols,
                enable_key_log: self.enable_key_log,
            }
        }
    }

    /// ⚠️ DANGEROUS: A certificate verifier that accepts all certificates or skips name checks.
    /// This should ONLY be used for testing and development!
    #[derive(Debug)]
    struct InsecureServerCertVerifier {
        skip_name_check: bool,
    }

    impl ServerCertVerifier for InsecureServerCertVerifier {
        fn verify_server_cert(
            &self,
            end_entity: &CertificateDer<'_>,
            _intermediates: &[CertificateDer<'_>],
            server_name: &ServerName<'_>,
            _ocsp_response: &[u8],
            _now: UnixTime,
        ) -> Result<ServerCertVerified, rustls::Error> {
            // If skip_name_check is false, we should verify the hostname
            if !self.skip_name_check {
                // Perform hostname verification using rustls's built-in verification
                let cert = rustls::server::ParsedCertificate::try_from(end_entity)?;
                rustls::client::verify_server_name(&cert, server_name)?;
            }
            // Accept any certificate without validation; optionally ignore name
            Ok(ServerCertVerified::assertion())
        }

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

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

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

    /// Rustls-backed TLS transport for MQTT over TCP
    pub struct RustlsTlsTransport {
        stream: RustlsTlsStream<TcpStream>,
        peer_addr: String,
    }

    impl RustlsTlsTransport {
        /// Connect using default configuration (system roots)
        pub async fn connect_rustls(addr: &str) -> Result<Self, TransportError> {
            let cfg = RustlsTlsConfig::default();
            Self::connect_with_config(addr, cfg).await
        }

        /// Connect using an explicit configuration
        pub async fn connect_with_config(
            addr: &str,
            cfg: RustlsTlsConfig,
        ) -> Result<Self, TransportError> {
            // Parse address
            let (host, socket_addr) = parse_mqtt_address(addr)?;

            // Build rustls client config
            let client_cfg = cfg.to_client_config()?;
            let connector = RustlsTlsConnector::from(Arc::new(client_cfg));

            // Resolve server name for SNI
            let server_name = {
                use std::convert::TryFrom;
                match ServerName::try_from(host.clone()) {
                    Ok(sn) => sn,
                    Err(_e) => {
                        return Err(TransportError::InvalidAddress(format!(
                            "invalid DNS name for TLS: {}",
                            host
                        )))
                    }
                }
            };

            // Establish TCP
            let tcp = TcpStream::connect(&socket_addr).await.map_err(|e| {
                TransportError::ConnectionFailed(format!("TCP connection failed: {}", e))
            })?;

            // TLS handshake
            let tls_stream = connector
                .connect(server_name, tcp)
                .await
                .map_err(|e| TransportError::Tls(format!("TLS handshake failed: {}", e)))?;

            Ok(Self {
                stream: tls_stream,
                peer_addr: socket_addr,
            })
        }

        /// Access the underlying TLS stream
        pub fn get_ref(&self) -> &RustlsTlsStream<TcpStream> {
            &self.stream
        }

        /// Access the underlying TLS stream mutably
        pub fn get_mut(&mut self) -> &mut RustlsTlsStream<TcpStream> {
            &mut self.stream
        }
    }

    #[async_trait]
    impl Transport for RustlsTlsTransport {
        async fn connect(addr: &str) -> Result<Self, TransportError>
        where
            Self: Sized,
        {
            Self::connect_rustls(addr).await
        }

        async fn close(&mut self) -> Result<(), TransportError> {
            // Rustls/TCP closes on drop; no explicit shutdown available here
            Ok(())
        }

        fn peer_addr(&self) -> Result<String, TransportError> {
            Ok(self.peer_addr.clone())
        }

        fn local_addr(&self) -> Result<String, TransportError> {
            self.stream
                .get_ref()
                .0
                .local_addr()
                .map(|a| a.to_string())
                .map_err(TransportError::Io)
        }

        fn set_nodelay(&self, nodelay: bool) -> Result<(), TransportError> {
            self.stream
                .get_ref()
                .0
                .set_nodelay(nodelay)
                .map_err(TransportError::Io)
        }
    }

    impl tokio::io::AsyncRead for RustlsTlsTransport {
        fn poll_read(
            mut self: std::pin::Pin<&mut Self>,
            cx: &mut std::task::Context<'_>,
            buf: &mut tokio::io::ReadBuf<'_>,
        ) -> std::task::Poll<std::io::Result<()>> {
            std::pin::Pin::new(&mut self.stream).poll_read(cx, buf)
        }
    }

    impl tokio::io::AsyncWrite for RustlsTlsTransport {
        fn poll_write(
            mut self: std::pin::Pin<&mut Self>,
            cx: &mut std::task::Context<'_>,
            buf: &[u8],
        ) -> std::task::Poll<std::io::Result<usize>> {
            std::pin::Pin::new(&mut self.stream).poll_write(cx, buf)
        }

        fn poll_flush(
            mut self: std::pin::Pin<&mut Self>,
            cx: &mut std::task::Context<'_>,
        ) -> std::task::Poll<std::io::Result<()>> {
            std::pin::Pin::new(&mut self.stream).poll_flush(cx)
        }

        fn poll_shutdown(
            mut self: std::pin::Pin<&mut Self>,
            cx: &mut std::task::Context<'_>,
        ) -> std::task::Poll<std::io::Result<()>> {
            std::pin::Pin::new(&mut self.stream).poll_shutdown(cx)
        }
    }
}

#[cfg(feature = "rustls-tls")]
pub use imp::{RustlsTlsConfig, RustlsTlsTransport};