diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e33e624f6..1af234c7e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -159,3 +159,18 @@ jobs: - if: "!startsWith(matrix.os, 'windows')" run: cargo test name: Run tests (not Windows) + + test-fips: + name: Test FIPS integration + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: 'recursive' + - name: Install Rust (rustup) + run: rustup update stable --no-self-update && rustup default stable + shell: bash + - name: Install Clang-7 + run: sudo apt-get install -y clang-7 + - run: cargo test --features fips + name: Run tests diff --git a/.gitmodules b/.gitmodules index 53b702f01..93bfb089f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,7 @@ [submodule "boring-sys/deps/boringssl"] path = boring-sys/deps/boringssl url = https://github.com/google/boringssl.git - ignore = dirty \ No newline at end of file + ignore = dirty +[submodule "boring-sys/deps/boringssl-fips"] + path = boring-sys/deps/boringssl-fips + url = https://github.com/google/boringssl.git diff --git a/README.md b/README.md index 75e9718b2..da8442105 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,18 @@ _Notes_: The crate will look for headers in the `$BORING_BSSL_INCLUDE_PATH/opens _Warning_: When providing a different version of BoringSSL make sure to use a compatible one, the crate relies on the presence of certain functions. +## Building with a FIPS-validated module + +Only BoringCrypto module version ae223d6138807a13006342edfeef32e813246b39, as +certified with [certificate +3678](https://csrc.nist.gov/projects/cryptographic-module-validation-program/certificate/3678) +is supported by this crate. Support is enabled by this crate's `fips` feature. + +`boring-sys` comes with a test that FIPS is enabled/disabled depending on the feature flag. You can run it as follows: +```bash +$ cargo test --features fips fips::is_enabled +``` + ## Contribution Unless you explicitly state otherwise, any contribution intentionally diff --git a/boring-sys/Cargo.toml b/boring-sys/Cargo.toml index d0da3a187..59bf5cfe9 100644 --- a/boring-sys/Cargo.toml +++ b/boring-sys/Cargo.toml @@ -29,3 +29,7 @@ include = [ [build-dependencies] bindgen = { version = "0.59", default-features = false, features = ["runtime"] } cmake = "0.1" + +[features] +# Use a FIPS-validated version of boringssl. +fips = [] diff --git a/boring-sys/build.rs b/boring-sys/build.rs index 8a848a796..513b8bcf7 100644 --- a/boring-sys/build.rs +++ b/boring-sys/build.rs @@ -1,3 +1,6 @@ +use std::path::{Path, PathBuf}; +use std::process::Command; + // NOTE: this build script is adopted from quiche (https://github.com/cloudflare/quiche) // Additional parameters for Android build of BoringSSL. @@ -85,6 +88,11 @@ fn get_boringssl_platform_output_path() -> String { } } +#[cfg(feature = "fips")] +const BORING_SSL_PATH: &str = "deps/boringssl-fips"; +#[cfg(not(feature = "fips"))] +const BORING_SSL_PATH: &str = "deps/boringssl"; + /// Returns a new cmake::Config for building BoringSSL. /// /// It will add platform-specific parameters if needed. @@ -93,7 +101,7 @@ fn get_boringssl_cmake_config() -> cmake::Config { let os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); let pwd = std::env::current_dir().unwrap(); - let mut boringssl_cmake = cmake::Config::new("deps/boringssl"); + let mut boringssl_cmake = cmake::Config::new(BORING_SSL_PATH); // Add platform-specific parameters. match os.as_ref() { @@ -105,6 +113,7 @@ fn get_boringssl_cmake_config() -> cmake::Config { }; // We need ANDROID_NDK_HOME to be set properly. + println!("cargo:rerun-if-env-changed=ANDROID_NDK_HOME"); let android_ndk_home = std::env::var("ANDROID_NDK_HOME") .expect("Please set ANDROID_NDK_HOME for Android build"); let android_ndk_home = std::path::Path::new(&android_ndk_home); @@ -161,7 +170,8 @@ fn get_boringssl_cmake_config() -> cmake::Config { if arch == "x86" && os != "windows" { boringssl_cmake.define( "CMAKE_TOOLCHAIN_FILE", - pwd.join("deps/boringssl/src/util/32-bit-toolchain.cmake") + pwd.join(BORING_SSL_PATH) + .join("src/util/32-bit-toolchain.cmake") .as_os_str(), ); } @@ -171,42 +181,107 @@ fn get_boringssl_cmake_config() -> cmake::Config { } } -fn main() { - use std::env; - use std::path::{Path, PathBuf}; - use std::process::Command; - - if !Path::new("deps/boringssl/CMakeLists.txt").exists() { - println!("cargo:warning=fetching boringssl git submodule"); - // fetch the boringssl submodule - let status = Command::new("git") - .args(&[ - "submodule", - "update", - "--init", - "--recursive", - "deps/boringssl", - ]) - .status(); - if !status.map_or(false, |status| status.success()) { - panic!("failed to fetch submodule - consider running `git submodule update --init --recursive deps/boringssl` yourself"); +/// Verify that the toolchains match https://csrc.nist.gov/CSRC/media/projects/cryptographic-module-validation-program/documents/security-policies/140sp3678.pdf +/// See "Installation Instructions" under section 12.1. +// TODO: maybe this should also verify the Go and Ninja versions? But those haven't been an issue in practice ... +fn verify_fips_clang_version() -> (&'static str, &'static str) { + fn version(tool: &str) -> String { + let output = match Command::new(tool).arg("--version").output() { + Ok(o) => o, + Err(e) => { + eprintln!("warning: missing {}, trying other compilers: {}", tool, e); + // NOTE: hard-codes that the loop below checks the version + return String::new(); + } + }; + assert!(output.status.success()); + let output = std::str::from_utf8(&output.stdout).expect("invalid utf8 output"); + output.lines().next().expect("empty output").to_string() + } + + const REQUIRED_CLANG_VERSION: &str = "7.0.1"; + for (cc, cxx) in [ + ("clang-7", "clang++-7"), + ("clang", "clang++"), + ("cc", "c++"), + ] { + let cc_version = version(cc); + if cc_version.contains(REQUIRED_CLANG_VERSION) { + assert!( + version(cxx).contains(REQUIRED_CLANG_VERSION), + "mismatched versions of cc and c++" + ); + return (cc, cxx); + } else if cc == "cc" { + panic!( + "unsupported clang version \"{}\": FIPS requires clang {}", + cc_version, REQUIRED_CLANG_VERSION + ); + } else if !cc_version.is_empty() { + eprintln!( + "warning: FIPS requires clang version {}, skipping incompatible version \"{}\"", + REQUIRED_CLANG_VERSION, cc_version + ); } } + unreachable!() +} + +fn main() { + use std::env; + println!("cargo:rerun-if-env-changed=BORING_BSSL_PATH"); let bssl_dir = std::env::var("BORING_BSSL_PATH").unwrap_or_else(|_| { + if !Path::new(BORING_SSL_PATH).join("CMakeLists.txt").exists() { + println!("cargo:warning=fetching boringssl git submodule"); + // fetch the boringssl submodule + let status = Command::new("git") + .args(&[ + "submodule", + "update", + "--init", + "--recursive", + BORING_SSL_PATH, + ]) + .status(); + if !status.map_or(false, |status| status.success()) { + panic!("failed to fetch submodule - consider running `git submodule update --init --recursive deps/boringssl` yourself"); + } + } + let mut cfg = get_boringssl_cmake_config(); if cfg!(feature = "fuzzing") { cfg.cxxflag("-DBORINGSSL_UNSAFE_DETERMINISTIC_MODE") .cxxflag("-DBORINGSSL_UNSAFE_FUZZER_MODE"); } + if cfg!(feature = "fips") { + let (clang, clangxx) = verify_fips_clang_version(); + cfg.define("CMAKE_C_COMPILER", clang); + cfg.define("CMAKE_CXX_COMPILER", clangxx); + cfg.define("CMAKE_ASM_COMPILER", clang); + cfg.define("FIPS", "1"); + } cfg.build_target("bssl").build().display().to_string() }); let build_path = get_boringssl_platform_output_path(); - let build_dir = format!("{}/build/{}", bssl_dir, build_path); - println!("cargo:rustc-link-search=native={}", build_dir); + if cfg!(feature = "fips") { + println!( + "cargo:rustc-link-search=native={}/build/crypto/{}", + bssl_dir, build_path + ); + println!( + "cargo:rustc-link-search=native={}/build/ssl/{}", + bssl_dir, build_path + ); + } else { + println!( + "cargo:rustc-link-search=native={}/build/{}", + bssl_dir, build_path + ); + } println!("cargo:rustc-link-lib=static=crypto"); println!("cargo:rustc-link-lib=static=ssl"); @@ -216,10 +291,14 @@ fn main() { println!("cargo:rustc-cdylib-link-arg=-Wl,-undefined,dynamic_lookup"); } - let include_path = PathBuf::from( - std::env::var("BORING_BSSL_INCLUDE_PATH") - .unwrap_or_else(|_| String::from("deps/boringssl/src/include")), - ); + println!("cargo:rerun-if-env-changed=BORING_BSSL_INCLUDE_PATH"); + let include_path = std::env::var("BORING_BSSL_INCLUDE_PATH").unwrap_or_else(|_| { + if cfg!(feature = "fips") { + format!("{}/include", BORING_SSL_PATH) + } else { + format!("{}/src/include", BORING_SSL_PATH) + } + }); let mut builder = bindgen::Builder::default() .derive_copy(true) @@ -234,12 +313,13 @@ fn main() { .layout_tests(true) .prepend_enum_name(true) .rustfmt_bindings(true) - .clang_args(&["-I", include_path.to_str().unwrap()]); + .clang_args(&["-I", &include_path]); let headers = [ "aes.h", "asn1_mac.h", "asn1t.h", + #[cfg(not(feature = "fips"))] "blake2.h", "blowfish.h", "cast.h", @@ -264,11 +344,18 @@ fn main() { "ripemd.h", "siphash.h", "srtp.h", + #[cfg(not(feature = "fips"))] "trust_token.h", "x509v3.h", ]; for header in &headers { - builder = builder.header(include_path.join("openssl").join(header).to_str().unwrap()); + builder = builder.header( + Path::new(&include_path) + .join("openssl") + .join(header) + .to_str() + .unwrap(), + ); } let bindings = builder.generate().expect("Unable to generate bindings"); diff --git a/boring-sys/deps/boringssl-fips b/boring-sys/deps/boringssl-fips new file mode 160000 index 000000000..ae223d613 --- /dev/null +++ b/boring-sys/deps/boringssl-fips @@ -0,0 +1 @@ +Subproject commit ae223d6138807a13006342edfeef32e813246b39 diff --git a/boring/Cargo.toml b/boring/Cargo.toml index 72d011759..5d58c2756 100644 --- a/boring/Cargo.toml +++ b/boring/Cargo.toml @@ -21,3 +21,7 @@ boring-sys = { version = ">=1.1.0,<3.0.0", path = "../boring-sys" } [dev-dependencies] hex = "0.4" rusty-hook = "^0.11" + +[features] +# Use a FIPS-validated version of boringssl. +fips = ["boring-sys/fips"] diff --git a/boring/examples/fips_enabled.rs b/boring/examples/fips_enabled.rs new file mode 100644 index 000000000..7a57aee4f --- /dev/null +++ b/boring/examples/fips_enabled.rs @@ -0,0 +1,3 @@ +fn main() { + println!("boring::fips::enabled(): {}", boring::fips::enabled()); +} diff --git a/boring/examples/mk_certs.rs b/boring/examples/mk_certs.rs index 217786e26..b27b3974c 100644 --- a/boring/examples/mk_certs.rs +++ b/boring/examples/mk_certs.rs @@ -81,6 +81,7 @@ fn mk_request(privkey: &PKey) -> Result { } /// Make a certificate and private key signed by the given CA cert and private key +#[cfg_attr(feature = "fips", allow(unreachable_code, unused_variables))] fn mk_ca_signed_cert( ca_cert: &X509Ref, ca_privkey: &PKeyRef, @@ -98,7 +99,15 @@ fn mk_ca_signed_cert( serial.to_asn1_integer()? }; cert_builder.set_serial_number(&serial_number)?; + + #[cfg(not(feature = "fips"))] cert_builder.set_subject_name(req.subject_name())?; + #[cfg(feature = "fips")] + { + eprintln!("mk_certs not supported with FIPS module"); + std::process::exit(1); + } + cert_builder.set_issuer_name(ca_cert.subject_name())?; cert_builder.set_pubkey(&privkey)?; let not_before = Asn1Time::days_from_now(0)?; diff --git a/boring/src/fips.rs b/boring/src/fips.rs index 2d68633d7..baf044f74 100644 --- a/boring/src/fips.rs +++ b/boring/src/fips.rs @@ -20,3 +20,11 @@ pub fn enable(enabled: bool) -> Result<(), ErrorStack> { pub fn enabled() -> bool { unsafe { ffi::FIPS_mode() != 0 } } + +#[test] +fn is_enabled() { + #[cfg(feature = "fips")] + assert!(enabled()); + #[cfg(not(feature = "fips"))] + assert!(!enabled()); +} diff --git a/boring/src/pkey.rs b/boring/src/pkey.rs index c62ee341d..4054abd12 100644 --- a/boring/src/pkey.rs +++ b/boring/src/pkey.rs @@ -76,8 +76,10 @@ impl Id { pub const DH: Id = Id(ffi::EVP_PKEY_DH); pub const EC: Id = Id(ffi::EVP_PKEY_EC); pub const ED25519: Id = Id(ffi::EVP_PKEY_ED25519); + #[cfg(not(feature = "fips"))] pub const ED448: Id = Id(ffi::EVP_PKEY_ED448); pub const X25519: Id = Id(ffi::EVP_PKEY_X25519); + #[cfg(not(feature = "fips"))] pub const X448: Id = Id(ffi::EVP_PKEY_X448); /// Creates a `Id` from an integer representation. @@ -289,6 +291,7 @@ impl fmt::Debug for PKey { Id::DH => "DH", Id::EC => "EC", Id::ED25519 => "Ed25519", + #[cfg(not(feature = "fips"))] Id::ED448 => "Ed448", _ => "unknown", }; diff --git a/boring/src/ssl/mod.rs b/boring/src/ssl/mod.rs index c3acfabcc..027678b9b 100644 --- a/boring/src/ssl/mod.rs +++ b/boring/src/ssl/mod.rs @@ -487,8 +487,10 @@ impl ExtensionType { pub const PADDING: Self = Self(ffi::TLSEXT_TYPE_padding as u16); pub const EXTENDED_MASTER_SECRET: Self = Self(ffi::TLSEXT_TYPE_extended_master_secret as u16); pub const TOKEN_BINDING: Self = Self(ffi::TLSEXT_TYPE_token_binding as u16); + #[cfg(not(feature = "fips"))] pub const QUIC_TRANSPORT_PARAMETERS_LEGACY: Self = Self(ffi::TLSEXT_TYPE_quic_transport_parameters_legacy as u16); + #[cfg(not(feature = "fips"))] pub const QUIC_TRANSPORT_PARAMETERS_STANDARD: Self = Self(ffi::TLSEXT_TYPE_quic_transport_parameters_standard as u16); pub const CERT_COMPRESSION: Self = Self(ffi::TLSEXT_TYPE_cert_compression as u16); @@ -505,8 +507,11 @@ impl ExtensionType { pub const KEY_SHARE: Self = Self(ffi::TLSEXT_TYPE_key_share as u16); pub const RENEGOTIATE: Self = Self(ffi::TLSEXT_TYPE_renegotiate as u16); pub const DELEGATED_CREDENTIAL: Self = Self(ffi::TLSEXT_TYPE_delegated_credential as u16); + #[cfg(not(feature = "fips"))] pub const APPLICATION_SETTINGS: Self = Self(ffi::TLSEXT_TYPE_application_settings as u16); + #[cfg(not(feature = "fips"))] pub const ENCRYPTED_CLIENT_HELLO: Self = Self(ffi::TLSEXT_TYPE_encrypted_client_hello as u16); + #[cfg(not(feature = "fips"))] pub const ECH_IS_INNER: Self = Self(ffi::TLSEXT_TYPE_ech_is_inner as u16); pub const CERTIFICATE_TIMESTAMP: Self = Self(ffi::TLSEXT_TYPE_certificate_timestamp as u16); pub const NEXT_PROTO_NEG: Self = Self(ffi::TLSEXT_TYPE_next_proto_neg as u16); diff --git a/boring/src/ssl/test/mod.rs b/boring/src/ssl/test/mod.rs index abb6b6c82..137411f1e 100644 --- a/boring/src/ssl/test/mod.rs +++ b/boring/src/ssl/test/mod.rs @@ -1,6 +1,7 @@ #![allow(unused_imports)] use hex; +use std::cell::Cell; use std::env; use std::fs::File; use std::io::prelude::*; @@ -508,14 +509,16 @@ fn test_select_cert_error() { #[test] fn test_select_cert_unknown_extension() { let mut server = Server::builder(); - let unknown_extension = std::sync::Arc::new(std::sync::Mutex::new(None)); + let unknown_extension = std::sync::Arc::new(std::sync::Mutex::new(Some(vec![]))); server.ctx().set_select_certificate_callback({ let unknown = unknown_extension.clone(); move |client_hello| { - *unknown.lock().unwrap() = client_hello - .get_extension(ExtensionType::QUIC_TRANSPORT_PARAMETERS_LEGACY) + let ext = client_hello + .get_extension(ExtensionType::SERVER_NAME) .map(ToOwned::to_owned); + assert!(ext.is_none()); + *unknown.lock().unwrap() = ext; Ok(()) } }); diff --git a/boring/src/x509/mod.rs b/boring/src/x509/mod.rs index a02403f46..08f102084 100644 --- a/boring/src/x509/mod.rs +++ b/boring/src/x509/mod.rs @@ -197,6 +197,7 @@ impl X509StoreContextRef { unsafe { ffi::X509_STORE_CTX_get_error_depth(self.as_ptr()) as u32 } } + #[cfg(not(feature = "fips"))] /// Returns a reference to a complete valid `X509` certificate chain. /// /// This corresponds to [`X509_STORE_CTX_get0_chain`]. @@ -229,12 +230,14 @@ impl X509Builder { /// Sets the notAfter constraint on the certificate. pub fn set_not_after(&mut self, not_after: &Asn1TimeRef) -> Result<(), ErrorStack> { - unsafe { cvt(X509_set1_notAfter(self.0.as_ptr(), not_after.as_ptr())).map(|_| ()) } + // TODO: once FIPS supports `set1_notAfter`, use that instead + unsafe { cvt(X509_set_notAfter(self.0.as_ptr(), not_after.as_ptr())).map(|_| ()) } } /// Sets the notBefore constraint on the certificate. pub fn set_not_before(&mut self, not_before: &Asn1TimeRef) -> Result<(), ErrorStack> { - unsafe { cvt(X509_set1_notBefore(self.0.as_ptr(), not_before.as_ptr())).map(|_| ()) } + // TODO: once FIPS supports `set1_notBefore`, use that instead + unsafe { cvt(X509_set_notBefore(self.0.as_ptr(), not_before.as_ptr())).map(|_| ()) } } /// Sets the version of the certificate. @@ -493,18 +496,18 @@ impl X509Ref { /// Returns the certificate's Not After validity period. pub fn not_after(&self) -> &Asn1TimeRef { unsafe { - let date = X509_getm_notAfter(self.as_ptr()); + let date = X509_get0_notAfter(self.as_ptr()); assert!(!date.is_null()); - Asn1TimeRef::from_ptr(date) + Asn1TimeRef::from_ptr(date as *mut _) } } /// Returns the certificate's Not Before validity period. pub fn not_before(&self) -> &Asn1TimeRef { unsafe { - let date = X509_getm_notBefore(self.as_ptr()); + let date = X509_get0_notBefore(self.as_ptr()); assert!(!date.is_null()); - Asn1TimeRef::from_ptr(date) + Asn1TimeRef::from_ptr(date as *mut _) } } @@ -1160,6 +1163,7 @@ impl X509ReqRef { ffi::i2d_X509_REQ } + #[cfg(not(feature = "fips"))] /// Returns the numerical value of the version field of the certificate request. /// /// This corresponds to [`X509_REQ_get_version`] @@ -1169,6 +1173,7 @@ impl X509ReqRef { unsafe { X509_REQ_get_version(self.as_ptr()) as i32 } } + #[cfg(not(feature = "fips"))] /// Returns the subject name of the certificate request. /// /// This corresponds to [`X509_REQ_get_subject_name`] @@ -1401,14 +1406,12 @@ impl Stackable for X509Object { type StackType = ffi::stack_st_X509_OBJECT; } -use crate::ffi::{X509_get0_signature, X509_getm_notAfter, X509_getm_notBefore, X509_up_ref}; - -use crate::ffi::{ - ASN1_STRING_get0_data, X509_ALGOR_get0, X509_REQ_get_subject_name, X509_REQ_get_version, - X509_STORE_CTX_get0_chain, X509_set1_notAfter, X509_set1_notBefore, -}; +use crate::ffi::{X509_get0_notAfter, X509_get0_notBefore, X509_get0_signature, X509_up_ref}; use crate::ffi::X509_OBJECT_get0_X509; +use crate::ffi::{ASN1_STRING_get0_data, X509_ALGOR_get0, X509_set_notAfter, X509_set_notBefore}; +#[cfg(not(feature = "fips"))] +use crate::ffi::{X509_REQ_get_subject_name, X509_REQ_get_version, X509_STORE_CTX_get0_chain}; #[allow(bad_style)] unsafe fn X509_OBJECT_free(x: *mut ffi::X509_OBJECT) { diff --git a/hyper-boring/Cargo.toml b/hyper-boring/Cargo.toml index 74bce004d..a00d14203 100644 --- a/hyper-boring/Cargo.toml +++ b/hyper-boring/Cargo.toml @@ -14,6 +14,7 @@ exclude = ["test/*"] default = ["runtime"] runtime = ["hyper/runtime"] +fips = ["tokio-boring/fips"] [dependencies] antidote = "1.0.0" diff --git a/tokio-boring/Cargo.toml b/tokio-boring/Cargo.toml index 15f6afb5f..9622b6715 100644 --- a/tokio-boring/Cargo.toml +++ b/tokio-boring/Cargo.toml @@ -20,3 +20,6 @@ tokio = "1" futures = "0.3" tokio = { version = "1", features = ["full"] } anyhow = "1" + +[features] +fips = ["boring/fips"]