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