Skip to main content

lexe_api/
tls_acceptor.rs

1//! TLS acceptor that injects the verified client certificate into request
2//! extensions after the TLS handshake completes successfully.
3//!
4//! [`CertInjectorAcceptor`] wraps axum-server's `RustlsAcceptor` to extract the
5//! client's end-entity cert and inject it as [`VerifiedTlsClientCert`].
6//!
7//! [`CertInjectorAcceptor`]: crate::tls_acceptor::CertInjectorAcceptor
8//! [`VerifiedTlsClientCert`]: crate::tls_acceptor::VerifiedTlsClientCert
9
10use std::io;
11
12use axum_server::{accept::Accept, tls_rustls::RustlsAcceptor};
13use futures::{FutureExt, future::Map};
14use rustls::pki_types::CertificateDer;
15use tokio::io::{AsyncRead, AsyncWrite};
16use tokio_rustls::server::TlsStream;
17use tower::Layer;
18use tower_http::add_extension::{AddExtension, AddExtensionLayer};
19
20/// The verified end-entity client certificate from an mTLS connection.
21///
22/// During the mTLS handshake the client presents its cert chain leaf-first;
23/// this is that leaf (end-entity) cert, after it passed verification against
24/// the server's configured [`ClientCertVerifier`]. The rest of the chain is
25/// discarded: the client typically doesn't send the CA cert, and we only need
26/// the end-entity cert to identify the client.
27///
28/// `None` if the client presented no certificate (non-mTLS connection).
29///
30/// [`ClientCertVerifier`]: rustls::server::danger::ClientCertVerifier
31#[derive(Clone, Debug)]
32pub struct VerifiedTlsClientCert(pub Option<CertificateDer<'static>>);
33
34/// An acceptor that wraps [`RustlsAcceptor`] to inject the verified client
35/// certificate into request extensions.
36///
37/// After the TLS handshake completes, this acceptor extracts the end-entity
38/// peer certificate from the [`TlsStream`] and wraps the service with an
39/// [`AddExtension`] layer containing [`VerifiedTlsClientCert`].
40///
41/// # Example
42///
43/// ```ignore
44/// use axum_server::tls_rustls::{RustlsAcceptor, RustlsConfig};
45/// use lexe_api::tls_acceptor::CertInjectorAcceptor;
46///
47/// let rustls_config = RustlsConfig::from_config(tls_config);
48/// let rustls_acceptor = RustlsAcceptor::new(rustls_config);
49/// let acceptor = CertInjectorAcceptor::new(rustls_acceptor);
50///
51/// axum_server::from_tcp(listener)
52///     .acceptor(acceptor)
53///     .serve(make_service)
54///     .await
55/// ```
56#[derive(Clone, Debug)]
57pub struct CertInjectorAcceptor {
58    inner: RustlsAcceptor,
59}
60
61impl CertInjectorAcceptor {
62    /// Create a new [`CertInjectorAcceptor`] wrapping the given
63    /// [`RustlsAcceptor`].
64    pub fn new(inner: RustlsAcceptor) -> Self {
65        Self { inner }
66    }
67}
68
69impl<I, S> Accept<I, S> for CertInjectorAcceptor
70where
71    I: AsyncRead + AsyncWrite + Unpin + Send + 'static,
72    S: Send + 'static,
73{
74    type Stream = TlsStream<I>;
75    type Service = AddExtension<S, VerifiedTlsClientCert>;
76    type Future = Map<
77        <RustlsAcceptor as Accept<I, S>>::Future,
78        fn(
79            io::Result<(TlsStream<I>, S)>,
80        ) -> io::Result<(
81            TlsStream<I>,
82            AddExtension<S, VerifiedTlsClientCert>,
83        )>,
84    >;
85
86    fn accept(&self, stream: I, service: S) -> Self::Future {
87        // Perform the TLS handshake via the inner rustls acceptor
88        self.inner
89            .accept(stream, service)
90            .map(|result| -> io::Result<_> {
91                let (tls_stream, service) = result?;
92
93                // Extract the verified end-entity client cert, if any.
94                // The cert chain returned by `peer_certificates()` is leaf
95                // first, i.e. the end-entity cert comes first.
96                let (_, server_conn) = tls_stream.get_ref();
97                let client_cert = server_conn
98                    .peer_certificates()
99                    .and_then(|chain| chain.first())
100                    .cloned();
101                let verified_cert = VerifiedTlsClientCert(client_cert);
102
103                // Wrap service to inject the cert into http request extensions
104                let service =
105                    AddExtensionLayer::new(verified_cert).layer(service);
106                Ok((tls_stream, service))
107            })
108    }
109}
110
111#[cfg(test)]
112mod test {
113    use std::sync::Arc;
114
115    use axum::{Router, extract::Request, routing::post};
116    use lexe_api_core::error::BackendApiError;
117    use lexe_common::root_seed::RootSeed;
118    use lexe_crypto::rng::FastRng;
119    use lexe_tls::{
120        LEXE_ALPN_PROTOCOLS, LEXE_CRYPTO_PROVIDER, client_config_builder,
121        server_config_builder,
122        shared_seed::certs::{
123            EphemeralClientCert, EphemeralIssuingCaCert, EphemeralServerCert,
124        },
125    };
126    use rustls::{RootCertStore, server::WebPkiClientVerifier};
127
128    use super::VerifiedTlsClientCert;
129    use crate::{
130        rest::RestClient, server::LxJson, test_utils::with_test_server,
131    };
132
133    /// A client presenting a cert via mTLS should have its end-entity cert
134    /// extracted by [`CertInjectorAcceptor`] and injected into the handler's
135    /// extensions, byte-for-byte.
136    #[tokio::test]
137    async fn injects_client_cert_into_handler() {
138        const DNS: &str = "localhost";
139        let mut rng = FastRng::from_u64(20240514);
140
141        // The ephemeral issuing CA that signs both end-entity certs.
142        let ca_cert = EphemeralIssuingCaCert::from_root_seed(
143            &RootSeed::from_rng(&mut rng),
144        );
145        let ca_cert_der = ca_cert.serialize_der_self_signed().unwrap();
146        let mut roots = RootCertStore::empty();
147        roots.add(ca_cert_der.into()).unwrap();
148        let roots = Arc::new(roots);
149
150        // Server cert (SAN=localhost) and client cert, both CA-signed.
151        let server_cert =
152            EphemeralServerCert::from_rng(&mut rng, &[DNS]).unwrap();
153        let server_cert_der =
154            server_cert.serialize_der_ca_signed(&ca_cert).unwrap();
155        let client_cert = EphemeralClientCert::generate_from_rng(&mut rng);
156        let client_cert_der =
157            client_cert.serialize_der_ca_signed(&ca_cert).unwrap();
158        // The exact DER bytes we expect to recover from the handler.
159        let expected_cert_der = client_cert_der.0.clone();
160
161        // Server requires + verifies client certs signed by the CA.
162        let client_verifier = WebPkiClientVerifier::builder_with_provider(
163            roots.clone(),
164            LEXE_CRYPTO_PROVIDER.clone(),
165        )
166        .build()
167        .unwrap();
168        let mut server_config = server_config_builder()
169            .with_client_cert_verifier(client_verifier)
170            .with_single_cert(
171                vec![server_cert_der.into()],
172                server_cert.serialize_key_der().into(),
173            )
174            .unwrap();
175        server_config
176            .alpn_protocols
177            .clone_from(&LEXE_ALPN_PROTOCOLS);
178        let server_config = Arc::new(server_config);
179
180        // Client trusts the CA and presents its client cert.
181        let mut client_config = client_config_builder()
182            .with_root_certificates(roots)
183            .with_client_auth_cert(
184                vec![client_cert_der.into()],
185                client_cert.serialize_key_der().into(),
186            )
187            .unwrap();
188        client_config
189            .alpn_protocols
190            .clone_from(&LEXE_ALPN_PROTOCOLS);
191
192        // Echoes back the DER of the end-entity client cert the injector
193        // recovered, so the test can verify it survived the round trip.
194        async fn handler(req: Request) -> LxJson<Option<Vec<u8>>> {
195            let cert_der = req
196                .extensions()
197                .get::<VerifiedTlsClientCert>()
198                .and_then(|cert| cert.0.as_ref())
199                .map(|cert| cert.as_ref().to_vec());
200            LxJson(cert_der)
201        }
202
203        let router = Router::new().route("/client_cert", post(handler));
204        with_test_server(server_config, DNS, router, |server_url| async move {
205            let rest =
206                RestClient::new("test-client", "test-server", client_config);
207            let url = format!("{server_url}/client_cert");
208            let http_req = rest.post(url, &());
209            let recovered_cert_der: Option<Vec<u8>> = rest
210                .send::<_, BackendApiError>(http_req)
211                .await
212                .expect("Request failed");
213
214            // The injector recovered the exact end-entity cert, byte-for-byte.
215            assert_eq!(recovered_cert_der, Some(expected_cert_der));
216        })
217        .await;
218    }
219}