lexe-tls 0.1.17

Lexe TLS configs, certs, and utilities
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
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
//! mTLS based on a shared [`RootSeed`]. We'll call this "shared seed" mTLS.
//!
//! ## Overview
//!
//! Security is based on the fact that the [`RootSeed`] is only known to the
//! user and a previously provisioned node.
//!
//! There are two forms:
//!
//! 1) "Ephemeral": Client and server both have a copy of the [`RootSeed`]. Both
//!    use the [`RootSeed`] to independently and deterministically derive a
//!    non-expiring "ephemeral cert issuing" CA which then issues a short-lived
//!    client or server end-entity cert which they present to the other side.
//!    The client and server verify that the other side has presented a cert
//!    signed by the derived CA.
//!
//! 2) "Revocable": Node (server) deterministically derives a "revocable cert
//!    issuing" CA. The app requests the node to issue an "revocable" client
//!    cert. The issued client cert does not encode an expiration. Instead, its
//!    expiration is managed at the application level via a cert store which
//!    tracks each client cert's pubkey and expiration. These client certs can
//!    be given to SDK clients and revoked at any time.
//!
//! ## Client and server cert verification
//!
//! - Client: Trusts only the [`EphemeralIssuingCaCert`], since the node always
//!   has access to the root seed and can therefore rederive the CA cert and
//!   issue itself a fresh server cert.
//!
//! - Server: Trusts *either* the [`EphemeralIssuingCaCert`] or the
//!   [`RevocableIssuingCaCert`], but with some differences:
//!   - All certs signed by the [`EphemeralIssuingCaCert`] are automatically
//!     trusted.
//!   - Certs signed by the [`RevocableIssuingCaCert`] must additionally appear
//!     in the client cert whitelist, must not be expired, and must and not be
//!     revoked.
//!
//! ## Which certs / secrets are stored where?
//!
//! - Node: Holds [`RootSeed`], derives ephemeral CA cert and eph server cert.
//! - App: Holds [`RootSeed`], derives ephemeral CA cert and eph client cert.
//! - SDK: Holds:
//!   - [`RevocableIssuingCaCert`] (DER only, no keypair)
//!   - [`RevocableClientCert`] (with keypair)
//!
//! ## Certificate hierarchy
//!
//! [`RootSeed`]
//! |
//! |___ [`EphemeralIssuingCaCert`]: Deterministically derived, expires never
//! |   |
//! |   |___ [`EphemeralClientCert`]: Expires in 90 days
//! |   |
//! |   |___ [`EphemeralServerCert`]: Expires in 90 days
//! |
//! |___ [`RevocableIssuingCaCert`]: Deterministically derived, expires never
//!     |
//!     |___ [`RevocableClientCert`]: Issued by parent; expires never.
//!     |
//!     |___ (no server cert; node always presents [`EphemeralServerCert`])
//!
//! [`RootSeed`]: lexe_common::root_seed::RootSeed
//! [`EphemeralIssuingCaCert`]: crate::shared_seed::certs::EphemeralIssuingCaCert
//! [`EphemeralClientCert`]: crate::shared_seed::certs::EphemeralClientCert
//! [`EphemeralServerCert`]: crate::shared_seed::certs::EphemeralServerCert
//! [`RevocableIssuingCaCert`]: crate::shared_seed::certs::RevocableIssuingCaCert
//! [`RevocableClientCert`]: crate::shared_seed::certs::RevocableClientCert

// TODO(max): Only the app (not an SDK) should have the power to call the "issue
// new cert" endpoint.

use std::{fmt::Display, sync::Arc};

use anyhow::Context;
use asn1_rs::{FromDer, nom::AsBytes};
use certs::{
    EphemeralClientCert, EphemeralIssuingCaCert, EphemeralServerCert,
    RevocableIssuingCaCert,
};
use lexe_common::{
    api::revocable_clients::{GetRevocableClientStatus, RevocableClientStatus},
    constants,
    env::DeployEnv,
    root_seed::RootSeed,
    time::TimestampMs,
};
use lexe_crypto::{ed25519, rng::Crng};
use rustls::{
    DigitallySignedStruct, DistinguishedName, RootCertStore,
    client::{
        WebPkiServerVerifier,
        danger::{
            HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier,
        },
    },
    pki_types::{CertificateDer, ServerName, UnixTime},
    server::{
        WebPkiClientVerifier,
        danger::{ClientCertVerified, ClientCertVerifier},
    },
};
use x509_parser::prelude::X509Certificate;

use crate::{
    ed25519_ext::Ed25519PublicKeyExt,
    lexe_ca,
    types::{LxCertificateDer, LxPrivatePkcs8KeyDer},
};

/// TLS certs for shared [`RootSeed`]-based mTLS.
pub mod certs;

/// Server-side TLS config for `UserNodeRunApi`.
/// Also returns the node's DNS name.
pub fn node_run_server_config(
    rng: &mut impl Crng,
    eph_ca_cert: &EphemeralIssuingCaCert,
    eph_ca_cert_der: &LxCertificateDer,
    rev_ca_cert: &RevocableIssuingCaCert,
    revocable_clients: Arc<dyn GetRevocableClientStatus>,
) -> anyhow::Result<(Arc<rustls::ServerConfig>, String)> {
    // Build ephemeral server cert and sign with derived CA
    let dns_name = constants::NODE_RUN_DNS;
    let eph_server_cert = EphemeralServerCert::from_rng(rng, &[dns_name])
        .context("Failed to generate ephemeral server cert")?;
    let eph_server_cert_der = eph_server_cert
        .serialize_der_ca_signed(eph_ca_cert)
        .context("Failed to sign and serialize ephemeral server cert")?;
    let eph_server_cert_key_der = eph_server_cert.serialize_key_der();

    // Construct our custom `SharedSeedClientCertVerifier`
    let client_cert_verifier = SharedSeedClientCertVerifier::new(
        eph_ca_cert_der,
        rev_ca_cert,
        revocable_clients,
    )
    .context("Failed to build shared seed client cert verifier")?;

    let mut config = lexe_tls_core::server_config_builder()
        .with_client_cert_verifier(Arc::new(client_cert_verifier))
        .with_single_cert(
            vec![eph_server_cert_der.into()],
            eph_server_cert_key_der.into(),
        )
        .context("Failed to build rustls::ServerConfig")?;
    config
        .alpn_protocols
        .clone_from(&lexe_tls_core::LEXE_ALPN_PROTOCOLS);

    Ok((Arc::new(config), dns_name.to_owned()))
}

/// Client-side TLS config for `UserNodeRunApi` which authenticates using
/// the user's root seed.
pub fn user_node_run_root_seed_client_config(
    rng: &mut impl Crng,
    deploy_env: DeployEnv,
    root_seed: &RootSeed,
) -> anyhow::Result<rustls::ClientConfig> {
    // Derive ephemeral issuing CA cert
    let eph_ca_cert = EphemeralIssuingCaCert::from_root_seed(root_seed);
    let eph_ca_cert_der = eph_ca_cert
        .serialize_der_self_signed()
        .context("Failed to sign and serialize ephemeral CA cert")?;

    // Build the client's server cert verifier:
    // - Ephemeral CA verifier trusts the ephemeral issuing CA
    // - Public Lexe verifier trusts the hard-coded Lexe cert.
    let ephemeral_ca_verifier = ephemeral_ca_verifier(&eph_ca_cert_der)
        .context("Failed to build ephemeral CA verifier")?;
    let lexe_server_verifier = lexe_ca::lexe_server_verifier(deploy_env);
    let server_cert_verifier = UserNodeRunVerifier {
        ephemeral_ca_verifier,
        lexe_server_verifier,
    };

    // Generate ephemeral client cert and sign with derived CA
    let client_cert = EphemeralClientCert::generate_from_rng(rng);
    let client_cert_der = client_cert
        .serialize_der_ca_signed(&eph_ca_cert)
        .context("Failed to sign and serialize ephemeral client cert")?;
    let client_cert_key_der = client_cert.serialize_key_der();

    let mut config = lexe_tls_core::client_config_builder()
        .dangerous()
        .with_custom_certificate_verifier(Arc::new(server_cert_verifier))
        // NOTE: .with_single_cert() uses a client cert resolver which always
        // presents our client cert when asked. Does this introduce overhead by
        // needlessly presenting our ephemeral client cert to the proxy which
        // doesn't actually require client auth? The answer is no, because the
        // proxy would then not send a CertificateRequest message during the
        // handshake, which is what actually prompts the client to send its
        // client cert. So the client never sends its cert to the proxy at all,
        // and defaults to a regular TLS handshake, with no mTLS overhead.
        // A custom client cert resolver is only needed if the proxy *also*
        // requires client auth, meaning we'd need to choose the correct cert to
        // present depending on whether the end entity is the proxy or the node.
        .with_client_auth_cert(
            vec![client_cert_der.into()],
            client_cert_key_der.into(),
        )
        .context("Failed to build rustls::ClientConfig")?;
    config
        .alpn_protocols
        .clone_from(&lexe_tls_core::LEXE_ALPN_PROTOCOLS);

    Ok(config)
}

/// Client-side TLS config for `UserNodeRunApi` which authenticates with
/// revocable client credentials.
pub fn user_node_run_revocable_client_config(
    deploy_env: DeployEnv,
    eph_ca_cert_der: &LxCertificateDer,
    rev_client_cert_der: LxCertificateDer,
    rev_client_cert_key_der: LxPrivatePkcs8KeyDer,
) -> anyhow::Result<rustls::ClientConfig> {
    // Build the client's server cert verifier:
    // - Ephemeral CA verifier trusts the ephemeral issuing CA
    // - Public Lexe verifier trusts the hard-coded Lexe cert.
    let ephemeral_ca_verifier = ephemeral_ca_verifier(eph_ca_cert_der)
        .context("Failed to build ephemeral CA verifier")?;
    let lexe_server_verifier = lexe_ca::lexe_server_verifier(deploy_env);
    let server_cert_verifier = UserNodeRunVerifier {
        ephemeral_ca_verifier,
        lexe_server_verifier,
    };

    let mut config = lexe_tls_core::client_config_builder()
        .dangerous()
        .with_custom_certificate_verifier(Arc::new(server_cert_verifier))
        // NOTE: .with_single_cert() uses a client cert resolver which always
        // presents our client cert when asked. Does this introduce overhead by
        // needlessly presenting our ephemeral client cert to the proxy which
        // doesn't actually require client auth? The answer is no, because the
        // proxy would then not send a CertificateRequest message during the
        // handshake, which is what actually prompts the client to send its
        // client cert. So the client never sends its cert to the proxy at all,
        // and defaults to a regular TLS handshake, with no mTLS overhead.
        // A custom client cert resolver is only needed if the proxy *also*
        // requires client auth, meaning we'd need to choose the correct cert to
        // present depending on whether the end entity is the proxy or the node.
        .with_client_auth_cert(
            vec![rev_client_cert_der.into()],
            rev_client_cert_key_der.into(),
        )
        .context("Failed to build rustls::ClientConfig")?;
    config
        .alpn_protocols
        .clone_from(&lexe_tls_core::LEXE_ALPN_PROTOCOLS);

    Ok(config)
}

/// Build a [`ServerCertVerifier`] which trusts the "ephemeral issuing" CA.
pub fn ephemeral_ca_verifier(
    ephemeral_ca_cert_der: &LxCertificateDer,
) -> anyhow::Result<Arc<WebPkiServerVerifier>> {
    let mut roots = RootCertStore::empty();
    roots
        .add(ephemeral_ca_cert_der.into())
        .context("Failed to re-parse ephemeral CA cert")?;
    let verifier = WebPkiServerVerifier::builder_with_provider(
        Arc::new(roots),
        lexe_tls_core::LEXE_CRYPTO_PROVIDER.clone(),
    )
    .build()
    .context("Could not build ephemeral server verifier")?;
    Ok(verifier)
}

/// The client's [`ServerCertVerifier`] for `UserNodeRunApi` TLS.
///
/// - When the user wishes to connect to a running node, it will make a request
///   to the node using a fake run DNS [`constants::NODE_RUN_DNS`]. However,
///   requests are first routed through lexe's reverse proxy, which parses the
///   fake run DNS in the SNI extension to determine whether we want to connect
///   to a running or provisioning node so it can route accordingly.
/// - The [`ServerName`] is given by the `NodeClient` reqwest client. This is
///   the gateway DNS when connecting to Lexe's proxy, otherwise it is the
///   node's fake run DNS. See `NodeClient`'s `run_url` for context.
/// - The [`UserNodeRunVerifier`] thus chooses between two "sub-verifiers"
///   according to the [`ServerName`] given to us by `reqwest`. We use the
///   public Lexe WebPKI verifier when establishing the outer TLS connection
///   with the gateway, and we use the ephemeral CA verifier for the inner TLS
///   connection which terminates inside the user node SGX enclave.
#[derive(Debug)]
struct UserNodeRunVerifier {
    /// `run.lexe.app` verifier - trusts the "ephemeral issuing" CA
    ephemeral_ca_verifier: Arc<WebPkiServerVerifier>,
    /// Lexe server verifier - trusts the Lexe CA
    lexe_server_verifier: Arc<WebPkiServerVerifier>,
}

impl ServerCertVerifier for UserNodeRunVerifier {
    fn verify_server_cert(
        &self,
        end_entity: &CertificateDer,
        intermediates: &[CertificateDer],
        server_name: &ServerName,
        ocsp_response: &[u8],
        now: UnixTime,
    ) -> Result<ServerCertVerified, rustls::Error> {
        let maybe_dns_name = match server_name {
            ServerName::DnsName(dns) => Some(dns.as_ref()),
            _ => None,
        };

        match maybe_dns_name {
            // Verify using ephemeral issuing CA when node is running
            Some(constants::NODE_RUN_DNS) =>
                self.ephemeral_ca_verifier.verify_server_cert(
                    end_entity,
                    intermediates,
                    server_name,
                    ocsp_response,
                    now,
                ),
            // Other domains (i.e., node reverse proxy) verify using lexe CA
            _ => self.lexe_server_verifier.verify_server_cert(
                end_entity,
                intermediates,
                server_name,
                ocsp_response,
                now,
            ),
        }
    }

    fn verify_tls12_signature(
        &self,
        _message: &[u8],
        _cert: &CertificateDer<'_>,
        _dss: &DigitallySignedStruct,
    ) -> Result<HandshakeSignatureValid, rustls::Error> {
        // We intentionally do not support TLSv1.2.
        let error = rustls::PeerIncompatible::ServerDoesNotSupportTls12Or13;
        Err(rustls::Error::PeerIncompatible(error))
    }

    fn verify_tls13_signature(
        &self,
        message: &[u8],
        cert: &CertificateDer<'_>,
        dss: &DigitallySignedStruct,
    ) -> Result<HandshakeSignatureValid, rustls::Error> {
        rustls::crypto::verify_tls13_signature(
            message,
            cert,
            dss,
            &lexe_tls_core::LEXE_SIGNATURE_ALGORITHMS,
        )
    }

    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
        lexe_tls_core::LEXE_SUPPORTED_VERIFY_SCHEMES.clone()
    }
}

/// A [`ClientCertVerifier`] which trusts either the "ephemeral issuing" or
/// "revocable issuing" CA.
///
/// - All certs signed by the ephemeral issuing CA are automatically trusted.
/// - Certs signed by the revocable issuing CA must additionally pass a
///   [`GetRevocableClientStatus`] check: known, not expired, and not revoked.
#[derive(Debug)]
pub struct SharedSeedClientCertVerifier {
    /// Trusts the "ephemeral issuing" CA
    ephemeral_ca_verifier: Arc<dyn ClientCertVerifier>,
    /// Trusts the "revocable issuing" CA
    revocable_ca_verifier: Arc<dyn ClientCertVerifier>,
    /// Handshake-time validity check for revocable client certs.
    revocable_clients: Arc<dyn GetRevocableClientStatus>,
}

impl SharedSeedClientCertVerifier {
    pub fn new(
        eph_ca_cert_der: &LxCertificateDer,
        rev_ca_cert: &RevocableIssuingCaCert,
        revocable_clients: Arc<dyn GetRevocableClientStatus>,
    ) -> anyhow::Result<Self> {
        let ephemeral_ca_verifier = {
            let mut eph_roots = rustls::RootCertStore::empty();
            eph_roots
                .add(eph_ca_cert_der.into())
                .context("rustls failed to deserialize CA cert DER bytes")?;

            WebPkiClientVerifier::builder_with_provider(
                Arc::new(eph_roots),
                lexe_tls_core::LEXE_CRYPTO_PROVIDER.clone(),
            )
            .build()
            .context("Failed to build ephemeral CA verifier")?
        };

        let revocable_ca_verifier = {
            let rev_ca_cert_der = rev_ca_cert
                .serialize_der_self_signed()
                .context("Failed to sign and serialize revocable CA cert")?;

            let mut rev_roots = rustls::RootCertStore::empty();
            rev_roots
                .add(rev_ca_cert_der.into())
                .context("rustls failed to deserialize CA cert DER bytes")?;

            WebPkiClientVerifier::builder_with_provider(
                Arc::new(rev_roots),
                lexe_tls_core::LEXE_CRYPTO_PROVIDER.clone(),
            )
            .build()
            .context("Failed to build ephemeral CA verifier")?
        };

        Ok(Self {
            ephemeral_ca_verifier,
            revocable_ca_verifier,
            revocable_clients,
        })
    }
}

impl ClientCertVerifier for SharedSeedClientCertVerifier {
    fn root_hint_subjects(&self) -> &[DistinguishedName] {
        &[]
    }

    fn verify_client_cert(
        &self,
        end_entity_der: &CertificateDer,
        intermediates: &[CertificateDer],
        now: UnixTime,
    ) -> Result<ClientCertVerified, rustls::Error> {
        /// Shorthand to create a [`rustls::Error`].
        ///
        /// Better to use our own error strings so grepping our codebase for
        /// observed errors brings us right to this function.
        fn rustls_err(s: impl Display) -> rustls::Error {
            rustls::Error::General(s.to_string())
        }

        // If it's signed by the ephemeral issuing CA, automatically trust it.
        if let Ok(verified) = self.ephemeral_ca_verifier.verify_client_cert(
            end_entity_der,
            intermediates,
            now,
        ) {
            return Ok(verified);
        }

        // Ensure it is signed by the revocable issuing CA.
        self.revocable_ca_verifier.verify_client_cert(
            end_entity_der,
            intermediates,
            now,
        )?;

        // Great, it was signed by the revocable issuing CA.

        // Parse the cert and get the SubjectPublicKeyInfo
        let (_remaining, end_entity) =
            X509Certificate::from_der(end_entity_der.as_bytes())
                .map_err(|_| rustls_err("Cert was not encoded correctly"))?;
        let end_entity_pk = ed25519::PublicKey::try_from_spki(
            &end_entity.tbs_certificate.subject_pki,
        )
        .map_err(|e| rustls_err(format!("Not an ed25519 pk: {e}")))?;

        // Check that the cert is known, not revoked, and not expired.
        let now = TimestampMs::from_secs(now.as_secs())
            .map_err(|_| rustls_err("Clock overflow"))?;
        let status = self
            .revocable_clients
            .get_client_status(&end_entity_pk, now)
            .ok_or_else(|| rustls_err("Unrecognized cert pk"))?;
        match status {
            RevocableClientStatus::Valid => {}
            RevocableClientStatus::Revoked =>
                return Err(rustls_err("Client was previously revoked")),
            RevocableClientStatus::Expired =>
                return Err(rustls_err("Client is expired")),
        }

        Ok(ClientCertVerified::assertion())
    }

    fn verify_tls12_signature(
        &self,
        _message: &[u8],
        _cert: &CertificateDer<'_>,
        _dss: &DigitallySignedStruct,
    ) -> Result<HandshakeSignatureValid, rustls::Error> {
        // We intentionally do not support TLSv1.2.
        let error = rustls::PeerIncompatible::ServerDoesNotSupportTls12Or13;
        Err(rustls::Error::PeerIncompatible(error))
    }

    fn verify_tls13_signature(
        &self,
        message: &[u8],
        cert: &CertificateDer<'_>,
        dss: &DigitallySignedStruct,
    ) -> Result<HandshakeSignatureValid, rustls::Error> {
        rustls::crypto::verify_tls13_signature(
            message,
            cert,
            dss,
            &lexe_tls_core::LEXE_SIGNATURE_ALGORITHMS,
        )
    }

    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
        lexe_tls_core::LEXE_SUPPORTED_VERIFY_SCHEMES.clone()
    }
}

#[cfg(test)]
mod test {
    use std::sync::{Arc, RwLock};

    use certs::RevocableClientCert;
    use lexe_api_core::revocable_clients::{
        RevocableClient, RevocableClients, RevocableClientsHandle,
    };
    use lexe_common::{
        api::auth::LexeScope, env::DeployEnv, root_seed::RootSeed,
        time::TimestampMs,
    };
    use lexe_crypto::rng::FastRng;
    use secrecy::Secret;

    use super::*;
    use crate::test_utils;

    /// App->Node TLS handshake should succeed when using the same seed.
    #[tokio::test]
    async fn user_node_run_handshake_succeeds() {
        let client_seed = RootSeed::new(Secret::new([0x42; 32]));
        let server_seed = RootSeed::new(Secret::new([0x42; 32]));

        let [client_result, server_result] =
            do_user_node_run_tls_handshake(&client_seed, &server_seed).await;

        client_result.unwrap();
        server_result.unwrap();
    }

    /// App->Node TLS handshake should fail when using different seeds.
    #[tokio::test]
    async fn user_node_run_handshake_fails_with_different_seeds() {
        let client_seed = RootSeed::new(Secret::new([0x42; 32]));
        let server_seed = RootSeed::new(Secret::new([0x69; 32]));

        let [client_result, server_result] =
            do_user_node_run_tls_handshake(&client_seed, &server_seed).await;

        assert!(client_result.unwrap_err().contains("Client didn't connect"));
        assert!(server_result.unwrap_err().contains("Server didn't accept"));
    }

    // Shorthand to do a App->Node Run TLS handshake.
    async fn do_user_node_run_tls_handshake(
        client_seed: &RootSeed,
        server_seed: &RootSeed,
    ) -> [Result<(), String>; 2] {
        let mut rng = FastRng::from_u64(20240514);
        let deploy_env = DeployEnv::Dev;

        let client_config = user_node_run_root_seed_client_config(
            &mut rng,
            deploy_env,
            client_seed,
        )
        .map(Arc::new)
        .unwrap();

        let (server_config, server_dns) = {
            let eph_ca_cert =
                EphemeralIssuingCaCert::from_root_seed(server_seed);
            let eph_ca_cert_der =
                eph_ca_cert.serialize_der_self_signed().unwrap();
            let rev_ca_cert =
                RevocableIssuingCaCert::from_root_seed(server_seed);
            let clients = Arc::new(RevocableClientsHandle(RwLock::new(
                RevocableClients::default(),
            )));

            node_run_server_config(
                &mut rng,
                &eph_ca_cert,
                &eph_ca_cert_der,
                &rev_ca_cert,
                clients,
            )
            .unwrap()
        };

        test_utils::do_tls_handshake(client_config, server_config, &server_dns)
            .await
    }

    /// Test SDK->Node TLS handshake success and failure cases
    #[tokio::test]
    async fn sdk_node_run_handshake() {
        // Success: Client has no expiration
        {
            let expiration = None;
            let is_revoked = false;

            let [client_result, server_result] =
                do_sdk_node_run_tls_handshake(expiration, is_revoked).await;

            client_result.unwrap();
            server_result.unwrap();
        }

        // Success: Client has expiration in future
        {
            let expiration = Some(TimestampMs::MAX);
            let is_revoked = false;

            let [client_result, server_result] =
                do_sdk_node_run_tls_handshake(expiration, is_revoked).await;

            client_result.unwrap();
            server_result.unwrap();
        }

        // Fail: Client is expired
        {
            let expiration = Some(TimestampMs::MIN);
            let is_revoked = false;

            let [client_result, server_result] =
                do_sdk_node_run_tls_handshake(expiration, is_revoked).await;

            assert!(client_result.unwrap_err().contains("HandshakeFailure"));
            assert!(server_result.unwrap_err().contains("Client is expired"));
        }

        // Fail: Client is revoked
        {
            let expiration = None;
            let is_revoked = true;

            let [client_result, server_result] =
                do_sdk_node_run_tls_handshake(expiration, is_revoked).await;

            assert!(client_result.unwrap_err().contains("HandshakeFailure"));
            assert!(
                server_result
                    .unwrap_err()
                    .contains("Client was previously revoked")
            );
        }
    }

    // Shorthand to do a Sdk->Node Run TLS handshake.
    async fn do_sdk_node_run_tls_handshake(
        expiration: Option<TimestampMs>,
        is_revoked: bool,
    ) -> [Result<(), String>; 2] {
        let server_seed = RootSeed::new(Secret::new([0x42; 32]));

        // Server derives ephemeral and revocable issuing CA certs
        let eph_ca_cert = EphemeralIssuingCaCert::from_root_seed(&server_seed);
        let eph_ca_cert_der = eph_ca_cert.serialize_der_self_signed().unwrap();
        let rev_ca_cert = RevocableIssuingCaCert::from_root_seed(&server_seed);

        // Server issues revocable client cert, hands cert + key to app client.
        // Server stores the newly issued client.
        // User exports the revocable client cert with key from app into SDK,
        // as well as the ephemeral issuing CA cert (without key).
        let mut rng = FastRng::from_u64(20250509);
        let rev_client_cert = RevocableClientCert::generate_from_rng(&mut rng);
        let rev_client_cert_der = rev_client_cert
            .serialize_der_ca_signed(&rev_ca_cert)
            .unwrap();
        let rev_client_cert_key_der = rev_client_cert.serialize_key_der();
        let rev_client_cert_pk = rev_client_cert.public_key();

        let rev_client = RevocableClient {
            pubkey: *rev_client_cert_pk,
            created_at: TimestampMs::from_secs_u32(420),
            expires_at: expiration,
            label: Some("hullo".to_owned()),
            scope: LexeScope::All,
            is_revoked,
        };
        let rev_client_certs = {
            let revocable_clients = RwLock::new(RevocableClients::default());
            revocable_clients
                .write()
                .unwrap()
                .clients
                .insert(*rev_client_cert_pk, rev_client);
            Arc::new(RevocableClientsHandle(revocable_clients))
        };

        // SDK client config
        let deploy_env = DeployEnv::Dev;
        let client_config = user_node_run_revocable_client_config(
            deploy_env,
            &eph_ca_cert_der,
            rev_client_cert_der,
            rev_client_cert_key_der,
        )
        .map(Arc::new)
        .unwrap();

        // Node server config
        let (server_config, server_dns) = {
            let eph_ca_cert =
                EphemeralIssuingCaCert::from_root_seed(&server_seed);
            let rev_ca_cert =
                RevocableIssuingCaCert::from_root_seed(&server_seed);

            node_run_server_config(
                &mut rng,
                &eph_ca_cert,
                &eph_ca_cert_der,
                &rev_ca_cert,
                rev_client_certs,
            )
            .unwrap()
        };

        test_utils::do_tls_handshake(client_config, server_config, &server_dns)
            .await
    }
}