diff --git a/.github/workflows/cargo.yml b/.github/workflows/cargo.yml index 6f24591d..910e72e8 100644 --- a/.github/workflows/cargo.yml +++ b/.github/workflows/cargo.yml @@ -53,7 +53,14 @@ jobs: shell: bash run: chmod +x ./scripts/install-protoc.sh && ./scripts/install-protoc.sh $HOME - shell: bash - run: cargo clippy --workspace --all-features + run: | + if [[ "${{ inputs.runner }}" == "windows-2022" ]]; then + # we don't technially support the datadog-fips crate on windows + # right now anyway, so let's set this so that the windows build + # doesn't fail. + export AWS_LC_FIPS_SYS_NO_ASM=1 + fi + cargo clippy --workspace --all-features build: name: Build diff --git a/Cargo.lock b/Cargo.lock index 561d6e8b..495e57fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -272,12 +272,27 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "aws-lc-fips-sys" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d9c2e952a1f57e8cbc78b058a968639e70c4ce8b9c0a5e6363d4e5670eed795" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "regex", +] + [[package]] name = "aws-lc-rs" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b756939cb2f8dc900aa6dcd505e6e2428e9cae7ff7b028c49e3946efa70878" dependencies = [ + "aws-lc-fips-sys", "aws-lc-sys", "zeroize", ] @@ -728,6 +743,16 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +[[package]] +name = "datadog-fips" +version = "0.1.0" +dependencies = [ + "reqwest", + "rustls", + "rustls-native-certs 0.8.1", + "tracing", +] + [[package]] name = "datadog-protos" version = "0.1.0" @@ -1009,6 +1034,7 @@ dependencies = [ name = "dogstatsd" version = "0.1.0" dependencies = [ + "datadog-fips", "datadog-protos", "ddsketch-agent", "derive_more", diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 00000000..63b1f336 --- /dev/null +++ b/clippy.toml @@ -0,0 +1,3 @@ +disallowed-methods = [ + { path = "reqwest::Client::builder", reason = "prefer the FIPS-compatible adapter", replacement = "datadog_fips::reqwest_adapter::create_reqwest_client_builder" }, +] diff --git a/crates/datadog-fips/Cargo.toml b/crates/datadog-fips/Cargo.toml new file mode 100644 index 00000000..9758f41b --- /dev/null +++ b/crates/datadog-fips/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "datadog-fips" +version = "0.1.0" +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +reqwest = { version = "0.12.4", features = ["json", "http2"], default-features = false } +rustls = { version = "0.23.18", default-features = false, features = ["fips"], optional = true } +rustls-native-certs = { version = "0.8.1", optional = true } +tracing = { version = "0.1.40", default-features = false } + +[features] +default = [ "reqwest/rustls-tls" ] +fips = [ "reqwest/rustls-tls-no-provider", "rustls", "rustls-native-certs" ] diff --git a/crates/datadog-fips/README.md b/crates/datadog-fips/README.md new file mode 100644 index 00000000..090f4baf --- /dev/null +++ b/crates/datadog-fips/README.md @@ -0,0 +1,11 @@ +# Datadog FIPS for Serverless + +Crate which provides utils to build FIPS compliant components. + +Please add the following to your `clippy.toml`: + +``` +disallowed-methods = [ + { path = "reqwest::Client::builder", reason = "prefer the FIPS-compatible adapter", replacement = "datadog_fips::reqwest_adapter::create_reqwest_client_builder" }, +] +``` diff --git a/crates/datadog-fips/src/lib.rs b/crates/datadog-fips/src/lib.rs new file mode 100644 index 00000000..3e16bec6 --- /dev/null +++ b/crates/datadog-fips/src/lib.rs @@ -0,0 +1,4 @@ +// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +pub mod reqwest_adapter; diff --git a/crates/datadog-fips/src/reqwest_adapter.rs b/crates/datadog-fips/src/reqwest_adapter.rs new file mode 100644 index 00000000..375bf2fa --- /dev/null +++ b/crates/datadog-fips/src/reqwest_adapter.rs @@ -0,0 +1,63 @@ +use reqwest::ClientBuilder; +use std::error::Error; +#[cfg(feature = "fips")] +use tracing::debug; + +/// Creates a reqwest client builder with TLS configuration. +/// When the "fips" feature is enabled, it uses a FIPS-compliant TLS configuration. +/// Otherwise, it uses reqwest's default rustls TLS implementation. +#[cfg(not(feature = "fips"))] +pub fn create_reqwest_client_builder() -> Result> { + // Just return the default builder with rustls TLS. This is the one place we should be okay + // to call reqwest::Client::builder(). + #[allow(clippy::disallowed_methods)] + Ok(reqwest::Client::builder().use_rustls_tls()) +} + +/// Creates a reqwest client builder with FIPS-compliant TLS configuration. +/// This version loads native root certificates and verifies FIPS compliance. +#[cfg(feature = "fips")] +pub fn create_reqwest_client_builder() -> Result> { + // Get the runtime crypto provider that should have been configured at the start of the + // application using something like rustls::crypto::default_fips_provider().install_default() + let provider = + rustls::crypto::CryptoProvider::get_default().ok_or("No crypto provider configured")?; + + if !provider.fips() { + return Err("Crypto provider is not FIPS-compliant".into()); + } + + let mut root_cert_store = rustls::RootCertStore::empty(); + let native_certs = rustls_native_certs::load_native_certs(); + let mut valid_count = 0; + for cert in native_certs.certs { + match root_cert_store.add(cert) { + Ok(()) => valid_count += 1, + Err(err) => { + debug!("Failed to parse certificate: {:?}", err); + } + } + } + if valid_count == 0 { + return Err("No valid certificates found in native root store".into()); + } + + // FIPS typically requires TLS 1.2 or higher + let versions = rustls::ALL_VERSIONS.to_vec(); + let config_builder = rustls::ClientConfig::builder_with_provider(provider.clone()) + .with_protocol_versions(&versions) + .map_err(|_| "Failed to set protocol versions")?; + + let config = config_builder + .with_root_certificates(root_cert_store) + .with_no_client_auth(); + + if !config.fips() { + return Err("The final TLS configuration is not FIPS-compliant".into()); + } + debug!("Client builder is configured with FIPS."); + + // This is the one place that it is okay to call reqwest::Client::builder(). + #[allow(clippy::disallowed_methods)] + Ok(reqwest::Client::builder().use_preconfigured_tls(config)) +} diff --git a/crates/dogstatsd/Cargo.toml b/crates/dogstatsd/Cargo.toml index 6caa862f..73e07d3a 100644 --- a/crates/dogstatsd/Cargo.toml +++ b/crates/dogstatsd/Cargo.toml @@ -24,6 +24,7 @@ tokio-util = { version = "0.7.11", default-features = false } tracing = { version = "0.1.40", default-features = false } regex = { version = "1.10.6", default-features = false } zstd = { version = "0.13.3", default-features = false } +datadog-fips = { path = "../datadog-fips", default-features = false } [dev-dependencies] mockito = { version = "1.5.0", default-features = false } @@ -32,4 +33,4 @@ tracing-test = { version = "0.2.5", default-features = false } [features] default = [ "reqwest/rustls-tls" ] -fips = [ "reqwest/rustls-tls-no-provider" ] +fips = [ "reqwest/rustls-tls-no-provider", "datadog-fips/fips" ] diff --git a/crates/dogstatsd/src/datadog.rs b/crates/dogstatsd/src/datadog.rs index 4bbcd13d..ba1f7a12 100644 --- a/crates/dogstatsd/src/datadog.rs +++ b/crates/dogstatsd/src/datadog.rs @@ -4,6 +4,7 @@ //!Types to serialize data into the Datadog API use crate::flusher::ShippingError; +use datadog_fips::reqwest_adapter::create_reqwest_client_builder; use datadog_protos::metrics::SketchPayload; use derive_more::{Display, Into}; use protobuf::Message; @@ -12,6 +13,7 @@ use reqwest; use reqwest::{Client, Response}; use serde::{Serialize, Serializer}; use serde_json; +use std::error::Error; use std::io::Write; use std::sync::OnceLock; use std::time::Duration; @@ -285,12 +287,12 @@ pub enum RetryStrategy { LinearBackoff(u64, u64), // attempts, delay } -fn build_client(https_proxy: Option, timeout: Duration) -> Result { - let mut builder = Client::builder().timeout(timeout); +fn build_client(https_proxy: Option, timeout: Duration) -> Result> { + let mut builder = create_reqwest_client_builder()?.timeout(timeout); if let Some(proxy) = https_proxy { builder = builder.proxy(reqwest::Proxy::https(proxy)?); } - builder.build() + Ok(builder.build()?) } #[derive(Debug, Serialize, Clone, Copy)]