datadog_agent_commons/ipc/
tls.rs

1//! TLS helpers for client- and server-side IPC usage.
2
3use std::{
4    path::Path,
5    sync::Arc,
6    time::{Duration, Instant},
7};
8
9use rustls::{
10    client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
11    crypto::CryptoProvider,
12    pki_types::{CertificateDer, ServerName, UnixTime},
13    server::danger::{ClientCertVerified, ClientCertVerifier},
14    version::TLS13,
15    CertificateError, ClientConfig, DigitallySignedStruct, DistinguishedName, ServerConfig, SignatureScheme,
16};
17use rustls_pki_types::{pem::PemObject as _, PrivateKeyDer};
18use saluki_error::{generic_error, ErrorContext as _, GenericError};
19use saluki_tls::{ensure_client_config_fips_compliant, ensure_server_config_fips_compliant};
20
21const DEFAULT_CERT_READ_TIMEOUT: Duration = Duration::from_secs(20);
22const DEFAULT_CERT_READ_INTERVAL: Duration = Duration::from_millis(100);
23
24#[derive(Debug)]
25struct DatadogAgentServerCertVerifier {
26    cert: CertificateDer<'static>,
27    provider: Arc<CryptoProvider>,
28}
29
30impl DatadogAgentServerCertVerifier {
31    fn from_certificate_and_provider(cert: CertificateDer<'static>, provider: Arc<CryptoProvider>) -> Self {
32        Self { cert, provider }
33    }
34}
35
36impl ServerCertVerifier for DatadogAgentServerCertVerifier {
37    fn verify_server_cert(
38        &self, end_entity: &CertificateDer<'_>, _intermediates: &[CertificateDer<'_>], _server_name: &ServerName<'_>,
39        _ocsp_response: &[u8], _now: UnixTime,
40    ) -> Result<ServerCertVerified, rustls::Error> {
41        // Exact leaf DER equality pins one server identity; certificate chains and CA semantics do not broaden trust.
42        if end_entity != &self.cert {
43            return Err(rustls::Error::InvalidCertificate(CertificateError::UnknownIssuer));
44        }
45
46        Ok(ServerCertVerified::assertion())
47    }
48
49    fn verify_tls12_signature(
50        &self, message: &[u8], cert: &CertificateDer<'_>, dss: &DigitallySignedStruct,
51    ) -> Result<HandshakeSignatureValid, rustls::Error> {
52        rustls::crypto::verify_tls12_signature(message, cert, dss, &self.provider.signature_verification_algorithms)
53    }
54
55    fn verify_tls13_signature(
56        &self, message: &[u8], cert: &CertificateDer<'_>, dss: &DigitallySignedStruct,
57    ) -> Result<HandshakeSignatureValid, rustls::Error> {
58        rustls::crypto::verify_tls13_signature(message, cert, dss, &self.provider.signature_verification_algorithms)
59    }
60
61    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
62        self.provider.signature_verification_algorithms.supported_schemes()
63    }
64}
65
66#[derive(Debug)]
67struct DatadogAgentClientCertVerifier {
68    cert: CertificateDer<'static>,
69    provider: Arc<CryptoProvider>,
70}
71
72impl DatadogAgentClientCertVerifier {
73    fn from_certificate_and_provider(cert: CertificateDer<'static>, provider: Arc<CryptoProvider>) -> Self {
74        Self { cert, provider }
75    }
76}
77
78impl ClientCertVerifier for DatadogAgentClientCertVerifier {
79    fn offer_client_auth(&self) -> bool {
80        true
81    }
82
83    fn client_auth_mandatory(&self) -> bool {
84        true
85    }
86
87    fn root_hint_subjects(&self) -> &[DistinguishedName] {
88        &[]
89    }
90
91    fn verify_client_cert(
92        &self, end_entity: &CertificateDer<'_>, _intermediates: &[CertificateDer<'_>], _now: UnixTime,
93    ) -> Result<ClientCertVerified, rustls::Error> {
94        if end_entity != &self.cert {
95            return Err(rustls::Error::InvalidCertificate(CertificateError::UnknownIssuer));
96        }
97
98        Ok(ClientCertVerified::assertion())
99    }
100
101    fn verify_tls12_signature(
102        &self, message: &[u8], cert: &CertificateDer<'_>, dss: &DigitallySignedStruct,
103    ) -> Result<HandshakeSignatureValid, rustls::Error> {
104        rustls::crypto::verify_tls12_signature(message, cert, dss, &self.provider.signature_verification_algorithms)
105    }
106
107    fn verify_tls13_signature(
108        &self, message: &[u8], cert: &CertificateDer<'_>, dss: &DigitallySignedStruct,
109    ) -> Result<HandshakeSignatureValid, rustls::Error> {
110        rustls::crypto::verify_tls13_signature(message, cert, dss, &self.provider.signature_verification_algorithms)
111    }
112
113    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
114        self.provider.signature_verification_algorithms.supported_schemes()
115    }
116}
117
118/// Builds an exact shared-certificate mTLS client configuration for Datadog Agent IPC.
119///
120/// The client accepts only a server leaf certificate whose DER encoding exactly matches the configured IPC certificate
121/// and verifies the handshake signature as proof that the server possesses its private key. The client presents the same
122/// certificate and proves possession of its private key to satisfy mandatory server-side client authentication.
123/// Certificate chains and CA trust do not broaden the accepted server identity.
124///
125/// # Errors
126///
127/// If the IPC TLS identity file cannot be read or does not contain a valid PEM-encoded certificate and private key, an
128/// error is returned.
129pub async fn build_ipc_client_ipc_tls_config<P: AsRef<Path>>(cert_path: P) -> Result<ClientConfig, GenericError> {
130    // Read the certificate file, and extract the certificate and private key from it.
131    let (parsed_cert, parsed_key) = read_and_parse_certificate_file(
132        cert_path.as_ref(),
133        DEFAULT_CERT_READ_TIMEOUT,
134        DEFAULT_CERT_READ_INTERVAL,
135    )
136    .await?;
137
138    // Create our custom certificate verifier to use the parsed certificate for server verification.
139    let crypto_provider = rustls::crypto::CryptoProvider::get_default()
140        .map(Arc::clone)
141        .ok_or_else(|| generic_error!("Default cryptography provider not yet installed."))?;
142    let agent_cert_verifier = Arc::new(DatadogAgentServerCertVerifier::from_certificate_and_provider(
143        parsed_cert.clone(),
144        crypto_provider,
145    ));
146
147    let config = ClientConfig::builder_with_protocol_versions(&[&TLS13])
148        .dangerous()
149        .with_custom_certificate_verifier(agent_cert_verifier)
150        .with_client_auth_cert(vec![parsed_cert], parsed_key)
151        .with_error_context(|| {
152            format!(
153                "Failed to build client TLS configuration from certificate file '{}'.",
154                cert_path.as_ref().display()
155            )
156        })?;
157
158    ensure_client_config_fips_compliant(&config)?;
159
160    Ok(config)
161}
162
163/// Builds an exact shared-certificate mTLS server configuration for Datadog Agent IPC.
164///
165/// The server requires every client to present a leaf certificate whose DER encoding exactly matches the configured IPC
166/// certificate and to prove possession of its private key with the handshake signature. The server presents the same
167/// certificate as its identity. Certificate chains and CA trust do not broaden the accepted client identity, and no
168/// overlap between different certificates is accepted.
169///
170/// # Errors
171///
172/// If the IPC TLS identity file cannot be read or does not contain a valid PEM-encoded certificate and private key, an
173/// error is returned.
174pub async fn build_ipc_server_tls_config<P: AsRef<Path>>(cert_path: P) -> Result<ServerConfig, GenericError> {
175    // Read the certificate file, and extract the certificate and private key from it.
176    let (parsed_cert, parsed_key) = read_and_parse_certificate_file(
177        cert_path.as_ref(),
178        DEFAULT_CERT_READ_TIMEOUT,
179        DEFAULT_CERT_READ_INTERVAL,
180    )
181    .await?;
182
183    let crypto_provider = rustls::crypto::CryptoProvider::get_default()
184        .map(Arc::clone)
185        .ok_or_else(|| generic_error!("Default cryptography provider not yet installed."))?;
186    let agent_cert_verifier = Arc::new(DatadogAgentClientCertVerifier::from_certificate_and_provider(
187        parsed_cert.clone(),
188        crypto_provider,
189    ));
190
191    let mut config = ServerConfig::builder()
192        .with_client_cert_verifier(agent_cert_verifier)
193        .with_single_cert(vec![parsed_cert], parsed_key)
194        .with_error_context(|| {
195            format!(
196                "Failed to build server TLS configuration from certificate file '{}'.",
197                cert_path.as_ref().display()
198            )
199        })?;
200
201    ensure_server_config_fips_compliant(&mut config)?;
202
203    Ok(config)
204}
205
206/// Reads and parses a certificate file from the given path with retry behavior.
207///
208/// If reading the file fails, it will retry reading it for up to `timeout` total, waiting `interval` between attempts,
209/// until it succeeds or the timeout is reached.
210///
211/// ## Errors
212///
213/// If the file can't be read after the maximum number of retries, or if the file isn't a valid certificate,
214/// an error will be returned.
215async fn read_and_parse_certificate_file(
216    cert_path: &Path, timeout: Duration, interval: Duration,
217) -> Result<(CertificateDer<'static>, PrivateKeyDer<'static>), GenericError> {
218    if timeout < interval {
219        return Err(generic_error!(
220            "Timeout is less than interval ({} <  {}).",
221            timeout.as_secs(),
222            interval.as_secs()
223        ));
224    }
225
226    let start_time = Instant::now();
227    let mut last_error = String::new();
228    while start_time.elapsed() < timeout {
229        match tokio::fs::read(cert_path).await {
230            Ok(raw_cert_data) => {
231                let parsed_cert = CertificateDer::from_pem_slice(&raw_cert_data[..])
232                    .with_error_context(|| format!("Failed to parse certificate file '{}'.", cert_path.display()))?
233                    .into_owned();
234
235                let parsed_key = PrivateKeyDer::from_pem_slice(&raw_cert_data[..])
236                    .with_error_context(|| format!("Failed to parse private key file '{}'.", cert_path.display()))?
237                    .clone_key();
238
239                return Ok((parsed_cert, parsed_key));
240            }
241            Err(e) => {
242                last_error = e.to_string();
243                tokio::time::sleep(interval).await;
244            }
245        }
246    }
247
248    Err(generic_error!(
249        "Failed to read certificate file '{}' after {} seconds: {}",
250        cert_path.display(),
251        timeout.as_secs(),
252        last_error
253    ))
254}
255
256#[cfg(test)]
257mod tests {
258    use std::{fs, io, path::PathBuf, sync::Arc, time::Duration};
259
260    use rcgen::{generate_simple_self_signed, CertifiedKey};
261    use rustls::{
262        crypto::CryptoProvider,
263        pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer, ServerName},
264        version::TLS13,
265        ClientConfig, ServerConfig,
266    };
267    use tempfile::TempDir;
268    use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
269    use tokio_rustls::{TlsAcceptor, TlsConnector};
270
271    use super::{build_ipc_client_ipc_tls_config, build_ipc_server_tls_config, DatadogAgentServerCertVerifier};
272
273    const APPLICATION_BYTE: u8 = 42;
274    const TEST_TIMEOUT: Duration = Duration::from_secs(5);
275
276    struct TestIdentity {
277        _temp_dir: TempDir,
278        cert_path: PathBuf,
279        cert_der: CertificateDer<'static>,
280        key_der: Vec<u8>,
281    }
282
283    impl TestIdentity {
284        fn localhost() -> Self {
285            let CertifiedKey { cert, signing_key } = generate_simple_self_signed(["localhost".to_owned()])
286                .expect("self-signed localhost certificate should be generated");
287            let temp_dir = tempfile::tempdir().expect("temporary certificate directory should be created");
288            let cert_path = temp_dir.path().join("ipc-cert.pem");
289            fs::write(&cert_path, format!("{}{}", cert.pem(), signing_key.serialize_pem()))
290                .expect("certificate and private key should be written");
291
292            Self {
293                _temp_dir: temp_dir,
294                cert_path,
295                cert_der: cert.der().clone(),
296                key_der: signing_key.serialize_der(),
297            }
298        }
299
300        fn private_key(&self) -> PrivateKeyDer<'static> {
301            PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(self.key_der.clone()))
302        }
303    }
304
305    fn initialize_crypto_provider() {
306        let _ = saluki_tls::initialize_default_crypto_provider();
307        assert!(
308            CryptoProvider::get_default().is_some(),
309            "default crypto provider should be installed"
310        );
311    }
312
313    fn client_config_pinned_to(server_identity: &TestIdentity, client_identity: Option<&TestIdentity>) -> ClientConfig {
314        let provider = CryptoProvider::get_default()
315            .cloned()
316            .expect("default crypto provider should be installed");
317        let verifier = Arc::new(DatadogAgentServerCertVerifier::from_certificate_and_provider(
318            server_identity.cert_der.clone(),
319            provider,
320        ));
321        let builder = ClientConfig::builder_with_protocol_versions(&[&TLS13])
322            .dangerous()
323            .with_custom_certificate_verifier(verifier);
324
325        match client_identity {
326            Some(identity) => builder
327                .with_client_auth_cert(vec![identity.cert_der.clone()], identity.private_key())
328                .expect("client TLS config should accept generated identity"),
329            None => builder.with_no_client_auth(),
330        }
331    }
332
333    async fn accept_client(server_config: ServerConfig, client_config: ClientConfig) -> io::Result<u8> {
334        tokio::time::timeout(TEST_TIMEOUT, async move {
335            let (client_io, server_io) = tokio::io::duplex(4096);
336            let client = async move {
337                let connector = TlsConnector::from(Arc::new(client_config));
338                let server_name = ServerName::try_from("localhost").expect("localhost should be a valid server name");
339                let mut stream = connector.connect(server_name, client_io).await?;
340                stream.write_u8(APPLICATION_BYTE).await?;
341                Ok::<_, io::Error>(stream)
342            };
343            let server = async move {
344                let acceptor = TlsAcceptor::from(Arc::new(server_config));
345                let mut stream = acceptor.accept(server_io).await?;
346                Ok(stream
347                    .read_u8()
348                    .await
349                    .expect("accepted client should send an application byte"))
350            };
351
352            let (_, server_result) = tokio::join!(client, server);
353            server_result
354        })
355        .await
356        .expect("TLS handshake should not time out")
357    }
358
359    #[tokio::test]
360    async fn matching_ipc_identity_completes_mtls_and_delivers_application_byte() {
361        initialize_crypto_provider();
362        let identity_a = TestIdentity::localhost();
363        let server_config = build_ipc_server_tls_config(&identity_a.cert_path)
364            .await
365            .expect("production server TLS config should build");
366        let client_config = build_ipc_client_ipc_tls_config(&identity_a.cert_path)
367            .await
368            .expect("production client TLS config should build");
369
370        assert_eq!(
371            accept_client(server_config, client_config)
372                .await
373                .expect("server should accept the matching client identity"),
374            APPLICATION_BYTE
375        );
376    }
377
378    #[tokio::test]
379    async fn missing_or_mismatched_client_certificate_makes_server_accept_fail() {
380        initialize_crypto_provider();
381        let identity_a = TestIdentity::localhost();
382        let server_config = build_ipc_server_tls_config(&identity_a.cert_path)
383            .await
384            .expect("production server TLS config should build");
385        let client_config = client_config_pinned_to(&identity_a, None);
386
387        accept_client(server_config, client_config)
388            .await
389            .expect_err("server accept should reject a missing client certificate");
390
391        let identity_b = TestIdentity::localhost();
392        let server_config = build_ipc_server_tls_config(&identity_a.cert_path)
393            .await
394            .expect("production server TLS config should build");
395        let client_config = client_config_pinned_to(&identity_a, Some(&identity_b));
396
397        accept_client(server_config, client_config)
398            .await
399            .expect_err("server accept should reject a different client certificate");
400    }
401}