Skip to main content

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    version::TLS13,
14    CertificateError, ClientConfig, DigitallySignedStruct, ServerConfig, SignatureScheme,
15};
16use rustls_pki_types::{pem::PemObject as _, PrivateKeyDer};
17use saluki_error::{generic_error, ErrorContext as _, GenericError};
18use saluki_tls::{ensure_client_config_fips_compliant, ensure_server_config_fips_compliant};
19
20const DEFAULT_CERT_READ_TIMEOUT: Duration = Duration::from_secs(20);
21const DEFAULT_CERT_READ_INTERVAL: Duration = Duration::from_millis(100);
22
23#[derive(Debug)]
24struct DatadogAgentServerCertVerifier {
25    cert: CertificateDer<'static>,
26    provider: Arc<CryptoProvider>,
27}
28
29impl DatadogAgentServerCertVerifier {
30    fn from_certificate_and_provider(cert: CertificateDer<'static>, provider: Arc<CryptoProvider>) -> Self {
31        Self { cert, provider }
32    }
33}
34
35impl ServerCertVerifier for DatadogAgentServerCertVerifier {
36    fn verify_server_cert(
37        &self, end_entity: &CertificateDer<'_>, _intermediates: &[CertificateDer<'_>], _server_name: &ServerName<'_>,
38        _ocsp_response: &[u8], _now: UnixTime,
39    ) -> Result<ServerCertVerified, rustls::Error> {
40        // We only care about if the server certificate matches the one we have.
41        //
42        // This explicitly ignores things like the server using a CA certificate as an end-entity certificate and all of
43        // that. We just want to verify that the server certificate is the one we expect.
44        if end_entity != &self.cert {
45            return Err(rustls::Error::InvalidCertificate(CertificateError::UnknownIssuer));
46        }
47
48        Ok(ServerCertVerified::assertion())
49    }
50
51    fn verify_tls12_signature(
52        &self, message: &[u8], cert: &CertificateDer<'_>, dss: &DigitallySignedStruct,
53    ) -> Result<HandshakeSignatureValid, rustls::Error> {
54        rustls::crypto::verify_tls12_signature(message, cert, dss, &self.provider.signature_verification_algorithms)
55    }
56
57    fn verify_tls13_signature(
58        &self, message: &[u8], cert: &CertificateDer<'_>, dss: &DigitallySignedStruct,
59    ) -> Result<HandshakeSignatureValid, rustls::Error> {
60        rustls::crypto::verify_tls13_signature(message, cert, dss, &self.provider.signature_verification_algorithms)
61    }
62
63    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
64        self.provider.signature_verification_algorithms.supported_schemes()
65    }
66}
67
68/// Builds a client TLS configuration suitable for IPC usage with the Datadog Agent.
69///
70/// All IPC for the Datadog Agent uses mutual TLS, where both client _and_ server verify each other's certificate, but
71/// crucially, use the _same_ certificate on both sides.
72///
73/// ## Errors
74///
75/// If there is an issue reading the IPC TLS certificate file, or if the file isn't a valid PEM-encoded certificate, an
76/// error is returned.
77pub async fn build_ipc_client_ipc_tls_config<P: AsRef<Path>>(cert_path: P) -> Result<ClientConfig, GenericError> {
78    // Read the certificate file, and extract the certificate and private key from it.
79    let (parsed_cert, parsed_key) = read_and_parse_certificate_file(
80        cert_path.as_ref(),
81        DEFAULT_CERT_READ_TIMEOUT,
82        DEFAULT_CERT_READ_INTERVAL,
83    )
84    .await?;
85
86    // Create our custom certificate verifier to use the parsed certificate for server verification.
87    let crypto_provider = rustls::crypto::CryptoProvider::get_default()
88        .map(Arc::clone)
89        .ok_or_else(|| generic_error!("Default cryptography provider not yet installed."))?;
90    let agent_cert_verifier = Arc::new(DatadogAgentServerCertVerifier::from_certificate_and_provider(
91        parsed_cert.clone(),
92        crypto_provider,
93    ));
94
95    let config = ClientConfig::builder_with_protocol_versions(&[&TLS13])
96        .dangerous()
97        .with_custom_certificate_verifier(agent_cert_verifier)
98        .with_client_auth_cert(vec![parsed_cert], parsed_key)
99        .with_error_context(|| {
100            format!(
101                "Failed to build client TLS configuration from certificate file '{}'.",
102                cert_path.as_ref().display()
103            )
104        })?;
105
106    ensure_client_config_fips_compliant(&config)?;
107
108    Ok(config)
109}
110
111/// Builds a server TLS configuration suitable for IPC usage with the Datadog Agent.
112///
113/// All IPC for the Datadog Agent uses mutual TLS, where both client _and_ server verify each other's certificate, but
114/// crucially, use the _same_ certificate on both sides.
115///
116/// ## Errors
117///
118/// If there is an issue reading the IPC TLS certificate file, or if the file isn't a valid PEM-encoded certificate, an
119/// error is returned.
120pub async fn build_ipc_server_tls_config<P: AsRef<Path>>(cert_path: P) -> Result<ServerConfig, GenericError> {
121    // Read the certificate file, and extract the certificate and private key from it.
122    let (parsed_cert, parsed_key) = read_and_parse_certificate_file(
123        cert_path.as_ref(),
124        DEFAULT_CERT_READ_TIMEOUT,
125        DEFAULT_CERT_READ_INTERVAL,
126    )
127    .await?;
128
129    let mut config = ServerConfig::builder()
130        .with_no_client_auth()
131        .with_single_cert(vec![parsed_cert], parsed_key)
132        .with_error_context(|| {
133            format!(
134                "Failed to build server TLS configuration from certificate file '{}'.",
135                cert_path.as_ref().display()
136            )
137        })?;
138
139    ensure_server_config_fips_compliant(&mut config)?;
140
141    Ok(config)
142}
143
144/// Reads and parses a certificate file from the given path with retry behavior.
145///
146/// If reading the file fails, it will retry reading it for up to `timeout` total, waiting `interval` between attempts,
147/// until it succeeds or the timeout is reached.
148///
149/// ## Errors
150///
151/// If the file can't be read after the maximum number of retries, or if the file isn't a valid certificate,
152/// an error will be returned.
153async fn read_and_parse_certificate_file(
154    cert_path: &Path, timeout: Duration, interval: Duration,
155) -> Result<(CertificateDer<'static>, PrivateKeyDer<'static>), GenericError> {
156    if timeout < interval {
157        return Err(generic_error!(
158            "Timeout is less than interval ({} <  {}).",
159            timeout.as_secs(),
160            interval.as_secs()
161        ));
162    }
163
164    let start_time = Instant::now();
165    let mut last_error = String::new();
166    while start_time.elapsed() < timeout {
167        match tokio::fs::read(cert_path).await {
168            Ok(raw_cert_data) => {
169                let parsed_cert = CertificateDer::from_pem_slice(&raw_cert_data[..])
170                    .with_error_context(|| format!("Failed to parse certificate file '{}'.", cert_path.display()))?
171                    .into_owned();
172
173                let parsed_key = PrivateKeyDer::from_pem_slice(&raw_cert_data[..])
174                    .with_error_context(|| format!("Failed to parse private key file '{}'.", cert_path.display()))?
175                    .clone_key();
176
177                return Ok((parsed_cert, parsed_key));
178            }
179            Err(e) => {
180                last_error = e.to_string();
181                tokio::time::sleep(interval).await;
182            }
183        }
184    }
185
186    Err(generic_error!(
187        "Failed to read certificate file '{}' after {} seconds: {}",
188        cert_path.display(),
189        timeout.as_secs(),
190        last_error
191    ))
192}