diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98ab144f..e64a5c1d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -340,7 +340,15 @@ jobs: name: Run `rpk` tests - run: cargo test --features pq-experimental name: Run `pq-experimental` tests + - run: cargo test --features underscore-wildcards + name: Run `underscore-wildcards` tests - run: cargo test --features pq-experimental,rpk name: Run `pq-experimental,rpk` tests - run: cargo test --features kx-safe-default,pq-experimental name: Run `kx-safe-default` tests + - run: cargo test --features pq-experimental,underscore-wildcards + name: Run `pq-experimental,underscore-wildcards` tests + - run: cargo test --features rpk,underscore-wildcards + name: Run `rpk,underscore-wildcards` tests + - run: cargo test --features pq-experimental,rpk,underscore-wildcards + name: Run `pq-experimental,rpk,underscore-wildcards` tests diff --git a/boring-sys/Cargo.toml b/boring-sys/Cargo.toml index b6257348..c1471f20 100644 --- a/boring-sys/Cargo.toml +++ b/boring-sys/Cargo.toml @@ -51,7 +51,7 @@ include = [ ] [package.metadata.docs.rs] -features = ["rpk", "pq-experimental"] +features = ["rpk", "pq-experimental", "underscore-wildcards"] rustdoc-args = ["--cfg", "docsrs"] [features] @@ -71,6 +71,11 @@ rpk = [] # can be provided by setting `BORING_BSSL{,_FIPS}_SOURCE_PATH`. pq-experimental = [] +# Applies a patch (`patches/underscore-wildcards.patch`) to enable +# `ffi::X509_CHECK_FLAG_UNDERSCORE_WILDCARDS`. Same caveats as +# those for `pq-experimental` feature apply. +underscore-wildcards = [] + [build-dependencies] bindgen = { workspace = true } cmake = { workspace = true } diff --git a/boring-sys/build.rs b/boring-sys/build.rs new file mode 100644 index 00000000..e71886d5 --- /dev/null +++ b/boring-sys/build.rs @@ -0,0 +1,743 @@ +use fslock::LockFile; +use std::env; +use std::ffi::OsString; +use std::fs; +use std::io; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; +use std::sync::Once; + +// NOTE: this build script is adopted from quiche (https://github.com/cloudflare/quiche) + +// Additional parameters for Android build of BoringSSL. +// +// Android NDK < 18 with GCC. +const CMAKE_PARAMS_ANDROID_NDK_OLD_GCC: &[(&str, &[(&str, &str)])] = &[ + ( + "aarch64", + &[("ANDROID_TOOLCHAIN_NAME", "aarch64-linux-android-4.9")], + ), + ( + "arm", + &[("ANDROID_TOOLCHAIN_NAME", "arm-linux-androideabi-4.9")], + ), + ( + "x86", + &[("ANDROID_TOOLCHAIN_NAME", "x86-linux-android-4.9")], + ), + ( + "x86_64", + &[("ANDROID_TOOLCHAIN_NAME", "x86_64-linux-android-4.9")], + ), +]; + +// Android NDK >= 19. +const CMAKE_PARAMS_ANDROID_NDK: &[(&str, &[(&str, &str)])] = &[ + ("aarch64", &[("ANDROID_ABI", "arm64-v8a")]), + ("arm", &[("ANDROID_ABI", "armeabi-v7a")]), + ("x86", &[("ANDROID_ABI", "x86")]), + ("x86_64", &[("ANDROID_ABI", "x86_64")]), +]; + +fn cmake_params_android() -> &'static [(&'static str, &'static str)] { + let arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap(); + let cmake_params_android = if cfg!(feature = "ndk-old-gcc") { + CMAKE_PARAMS_ANDROID_NDK_OLD_GCC + } else { + CMAKE_PARAMS_ANDROID_NDK + }; + for (android_arch, params) in cmake_params_android { + if *android_arch == arch { + return params; + } + } + &[] +} + +const CMAKE_PARAMS_APPLE: &[(&str, &[(&str, &str)])] = &[ + // iOS + ( + "aarch64-apple-ios", + &[ + ("CMAKE_OSX_ARCHITECTURES", "arm64"), + ("CMAKE_OSX_SYSROOT", "iphoneos"), + ], + ), + ( + "aarch64-apple-ios-sim", + &[ + ("CMAKE_OSX_ARCHITECTURES", "arm64"), + ("CMAKE_OSX_SYSROOT", "iphonesimulator"), + ], + ), + ( + "x86_64-apple-ios", + &[ + ("CMAKE_OSX_ARCHITECTURES", "x86_64"), + ("CMAKE_OSX_SYSROOT", "iphonesimulator"), + ], + ), + // macOS + ( + "aarch64-apple-darwin", + &[ + ("CMAKE_OSX_ARCHITECTURES", "arm64"), + ("CMAKE_OSX_SYSROOT", "macosx"), + ], + ), + ( + "x86_64-apple-darwin", + &[ + ("CMAKE_OSX_ARCHITECTURES", "x86_64"), + ("CMAKE_OSX_SYSROOT", "macosx"), + ], + ), +]; + +fn cmake_params_apple() -> &'static [(&'static str, &'static str)] { + let target = env::var("TARGET").unwrap(); + for (next_target, params) in CMAKE_PARAMS_APPLE { + if *next_target == target { + return params; + } + } + &[] +} + +fn get_apple_sdk_name() -> &'static str { + for (name, value) in cmake_params_apple() { + if *name == "CMAKE_OSX_SYSROOT" { + return value; + } + } + let target = env::var("TARGET").unwrap(); + panic!("cannot find SDK for {} in CMAKE_PARAMS_APPLE", target); +} + +/// Returns an absolute path to the BoringSSL source. +fn get_boringssl_source_path() -> String { + #[cfg(feature = "fips")] + const SUBMODULE_DIR: &str = "boringssl-fips"; + #[cfg(not(feature = "fips"))] + const SUBMODULE_DIR: &str = "boringssl"; + + static COPY_SOURCES: Once = Once::new(); + + if let Ok(src_path) = env::var("BORING_BSSL_SOURCE_PATH") { + return src_path; + } + + let out_dir = env::var("OUT_DIR").unwrap(); + let src_path = Path::new(&out_dir).join(SUBMODULE_DIR); + + COPY_SOURCES.call_once(|| { + let submodule_path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("deps") + .join(SUBMODULE_DIR); + + if !submodule_path.join("CMakeLists.txt").exists() { + println!("cargo:warning=fetching boringssl git submodule"); + + run_command(Command::new("git").args([ + "submodule", + "update", + "--init", + "--recursive", + &submodule_path.display().to_string(), + ])) + .unwrap(); + } + + let _ = fs::remove_dir_all(&src_path); + fs_extra::dir::copy(submodule_path, &out_dir, &Default::default()).unwrap(); + + // NOTE: .git can be both file and dir, depening on whether it was copied from a submodule + // or created by the patches code. + let src_git_path = src_path.join(".git"); + let _ = fs::remove_file(&src_git_path); + let _ = fs::remove_dir_all(&src_git_path); + }); + + src_path.display().to_string() +} + +/// Returns the platform-specific output path for lib. +/// +/// MSVC generator on Windows place static libs in a target sub-folder, +/// so adjust library location based on platform and build target. +/// See issue: https://github.com/alexcrichton/cmake-rs/issues/18 +fn get_boringssl_platform_output_path() -> String { + if cfg!(target_env = "msvc") { + // Code under this branch should match the logic in cmake-rs + let debug_env_var = env::var("DEBUG").expect("DEBUG variable not defined in env"); + + let deb_info = match &debug_env_var[..] { + "false" => false, + "true" => true, + unknown => panic!("Unknown DEBUG={} env var.", unknown), + }; + + let opt_env_var = env::var("OPT_LEVEL").expect("OPT_LEVEL variable not defined in env"); + + let subdir = match &opt_env_var[..] { + "0" => "Debug", + "1" | "2" | "3" => { + if deb_info { + "RelWithDebInfo" + } else { + "Release" + } + } + "s" | "z" => "MinSizeRel", + unknown => panic!("Unknown OPT_LEVEL={} env var.", unknown), + }; + + subdir.to_string() + } else { + "".to_string() + } +} + +/// Returns a new cmake::Config for building BoringSSL. +/// +/// It will add platform-specific parameters if needed. +fn get_boringssl_cmake_config() -> cmake::Config { + let arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap(); + let os = env::var("CARGO_CFG_TARGET_OS").unwrap(); + let host = env::var("HOST").unwrap(); + let target = env::var("TARGET").unwrap(); + let pwd = std::env::current_dir().unwrap(); + let src_path = get_boringssl_source_path(); + + let mut boringssl_cmake = cmake::Config::new(&src_path); + if host != target { + // Add platform-specific parameters for cross-compilation. + match os.as_ref() { + "android" => { + // We need ANDROID_NDK_HOME to be set properly. + println!("cargo:rerun-if-env-changed=ANDROID_NDK_HOME"); + let android_ndk_home = 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); + for (name, value) in cmake_params_android() { + eprintln!("android arch={} add {}={}", arch, name, value); + boringssl_cmake.define(name, value); + } + let toolchain_file = android_ndk_home.join("build/cmake/android.toolchain.cmake"); + let toolchain_file = toolchain_file.to_str().unwrap(); + eprintln!("android toolchain={}", toolchain_file); + boringssl_cmake.define("CMAKE_TOOLCHAIN_FILE", toolchain_file); + + // 21 is the minimum level tested. You can give higher value. + boringssl_cmake.define("ANDROID_NATIVE_API_LEVEL", "21"); + boringssl_cmake.define("ANDROID_STL", "c++_shared"); + } + + "macos" => { + for (name, value) in cmake_params_apple() { + eprintln!("macos arch={} add {}={}", arch, name, value); + boringssl_cmake.define(name, value); + } + } + + "ios" => { + for (name, value) in cmake_params_apple() { + eprintln!("ios arch={} add {}={}", arch, name, value); + boringssl_cmake.define(name, value); + } + + // Bitcode is always on. + let bitcode_cflag = "-fembed-bitcode"; + + // Hack for Xcode 10.1. + let target_cflag = if arch == "x86_64" { + "-target x86_64-apple-ios-simulator" + } else { + "" + }; + + let cflag = format!("{} {}", bitcode_cflag, target_cflag); + boringssl_cmake.define("CMAKE_ASM_FLAGS", &cflag); + boringssl_cmake.cflag(&cflag); + } + + "windows" => { + if host.contains("windows") { + // BoringSSL's CMakeLists.txt isn't set up for cross-compiling using Visual Studio. + // Disable assembly support so that it at least builds. + boringssl_cmake.define("OPENSSL_NO_ASM", "YES"); + } + } + + "linux" => match arch.as_str() { + "x86" => { + boringssl_cmake.define( + "CMAKE_TOOLCHAIN_FILE", + pwd.join(&src_path) + .join("src/util/32-bit-toolchain.cmake") + .as_os_str(), + ); + } + "aarch64" => { + boringssl_cmake.define( + "CMAKE_TOOLCHAIN_FILE", + pwd.join("cmake/aarch64-linux.cmake").as_os_str(), + ); + } + "arm" => { + boringssl_cmake.define( + "CMAKE_TOOLCHAIN_FILE", + pwd.join("cmake/armv7-linux.cmake").as_os_str(), + ); + } + _ => { + eprintln!( + "warning: no toolchain file configured by boring-sys for {}", + target + ); + } + }, + + _ => {} + } + } + + boringssl_cmake +} + +/// 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(); + } + }; + if !output.status.success() { + return String::new(); + } + 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 = "12.0.0"; + for (cc, cxx) in [ + ("clang-12", "clang++-12"), + ("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 pick_best_android_ndk_toolchain(toolchains_dir: &Path) -> std::io::Result { + let toolchains = std::fs::read_dir(toolchains_dir)?.collect::, _>>()?; + // First look for one of the toolchains that Google has documented. + // https://developer.android.com/ndk/guides/other_build_systems + for known_toolchain in ["linux-x86_64", "darwin-x86_64", "windows-x86_64"] { + if let Some(toolchain) = toolchains + .iter() + .find(|entry| entry.file_name() == known_toolchain) + { + return Ok(toolchain.file_name()); + } + } + // Then fall back to any subdirectory, in case Google has added support for a new host. + // (Maybe there's a linux-aarch64 toolchain now.) + if let Some(toolchain) = toolchains + .into_iter() + .find(|entry| entry.file_type().map(|ty| ty.is_dir()).unwrap_or(false)) + { + return Ok(toolchain.file_name()); + } + // Finally give up. + Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "no subdirectories at given path", + )) +} + +fn get_extra_clang_args_for_bindgen() -> Vec { + let os = env::var("CARGO_CFG_TARGET_OS").unwrap(); + + let mut params = Vec::new(); + + // Add platform-specific parameters. + #[allow(clippy::single_match)] + match os.as_ref() { + "ios" | "macos" => { + // When cross-compiling for Apple targets, tell bindgen to use SDK sysroot, + // and *don't* use system headers of the host macOS. + let sdk = get_apple_sdk_name(); + let output = std::process::Command::new("xcrun") + .args(["--show-sdk-path", "--sdk", sdk]) + .output() + .unwrap(); + if !output.status.success() { + if let Some(exit_code) = output.status.code() { + eprintln!("xcrun failed: exit code {}", exit_code); + } else { + eprintln!("xcrun failed: killed"); + } + std::io::stderr().write_all(&output.stderr).unwrap(); + // Uh... let's try anyway, I guess? + return params; + } + let mut sysroot = String::from_utf8(output.stdout).unwrap(); + // There is typically a newline at the end which confuses clang. + sysroot.truncate(sysroot.trim_end().len()); + params.push("-isysroot".to_string()); + params.push(sysroot); + } + "android" => { + let android_ndk_home = env::var("ANDROID_NDK_HOME") + .expect("Please set ANDROID_NDK_HOME for Android build"); + let mut android_sysroot = std::path::PathBuf::from(android_ndk_home); + android_sysroot.extend(["toolchains", "llvm", "prebuilt"]); + let toolchain = match pick_best_android_ndk_toolchain(&android_sysroot) { + Ok(toolchain) => toolchain, + Err(e) => { + eprintln!( + "warning: failed to find prebuilt Android NDK toolchain for bindgen: {}", + e + ); + // Uh... let's try anyway, I guess? + return params; + } + }; + android_sysroot.push(toolchain); + android_sysroot.push("sysroot"); + params.push("--sysroot".to_string()); + // If ANDROID_NDK_HOME weren't a valid UTF-8 string, + // we'd already know from env::var. + params.push(android_sysroot.into_os_string().into_string().unwrap()); + } + _ => {} + } + + params +} + +fn ensure_patches_applied() -> io::Result<()> { + let out_dir = env::var("OUT_DIR").unwrap(); + let mut lock_file = LockFile::open(&PathBuf::from(&out_dir).join(".patch_lock"))?; + let src_path = get_boringssl_source_path(); + let has_git = Path::new(&src_path).join(".git").exists(); + + lock_file.lock()?; + + // NOTE: init git in the copied files, so we can apply patches + if !has_git { + run_command(Command::new("git").args(["init"]).current_dir(&src_path))?; + } + + if cfg!(feature = "pq-experimental") { + println!("cargo:warning=applying experimental post quantum crypto patch to boringssl"); + apply_patch("boring-pq.patch")?; + } + + if cfg!(feature = "rpk") { + println!("cargo:warning=applying RPK patch to boringssl"); + apply_patch("rpk.patch")?; + } + + Ok(()) +} + +fn apply_patch(patch_name: &str) -> io::Result<()> { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let src_path = get_boringssl_source_path(); + let cmd_path = manifest_dir + .join("patches") + .join(patch_name) + .canonicalize()?; + + run_command( + Command::new("git") + .args([ + "apply", + "-v", + "--whitespace=fix", + &cmd_path.display().to_string(), + ]) + .current_dir(src_path), + )?; + + Ok(()) +} + +fn run_command(command: &mut Command) -> io::Result { + let out = command.output()?; + + println!("{}", std::str::from_utf8(&out.stdout).unwrap()); + eprintln!("{}", std::str::from_utf8(&out.stderr).unwrap()); + + if !out.status.success() { + let err = match out.status.code() { + Some(code) => format!("{:?} exited with status: {}", command, code), + None => format!("{:?} was terminated by signal", command), + }; + + return Err(io::Error::new(io::ErrorKind::Other, err)); + } + + Ok(out) +} + +fn build_boring_from_sources() -> String { + if cfg!(feature = "no-patches") { + println!( + "cargo:warning=skipping git patches application, provided\ + native BoringSSL is expected to have the patches included" + ); + } else { + ensure_patches_applied().unwrap(); + } + + 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"); + } + + if cfg!(feature = "fips-link-precompiled") { + cfg.define("FIPS", "1"); + } + + cfg.build_target("ssl").build(); + cfg.build_target("crypto").build().display().to_string() +} + +fn link_in_precompiled_bcm_o(bssl_dir: &str) { + println!("cargo:warning=linking in precompiled `bcm.o` module"); + + let bcm_o_src_path = env::var("BORING_SSL_PRECOMPILED_BCM_O") + .expect("`fips-link-precompiled` requires `BORING_SSL_PRECOMPILED_BCM_O` env variable to be specified"); + + let libcrypto_path = PathBuf::from(bssl_dir) + .join("build/crypto/libcrypto.a") + .canonicalize() + .unwrap() + .display() + .to_string(); + + let bcm_o_dst_path = PathBuf::from(bssl_dir).join("build/bcm-fips.o"); + + fs::copy(bcm_o_src_path, &bcm_o_dst_path).unwrap(); + + // check that fips module is named as expected + let out = run_command(Command::new("ar").args(["t", &libcrypto_path, "bcm.o"])).unwrap(); + + assert_eq!( + String::from_utf8(out.stdout).unwrap().trim(), + "bcm.o", + "failed to verify FIPS module name" + ); + + // insert fips bcm.o before bcm.o into libcrypto.a, + // so for all duplicate symbols the older fips bcm.o is used + // (this causes the need for extra linker flags to deal with duplicate symbols) + // (as long as the newer module does not define new symbols, one may also remove it, + // but once there are new symbols it would cause missing symbols at linking stage) + run_command(Command::new("ar").args([ + "rb", + "bcm.o", + &libcrypto_path, + bcm_o_dst_path.display().to_string().as_str(), + ])) + .unwrap(); +} + +fn check_feature_compatibility() { + #[cfg(all(feature = "fips", feature = "rpk"))] + compile_error!("`fips` and `rpk` features are mutually exclusive"); + + let no_patches_enabled = cfg!(feature = "no-patches"); + let is_external_native_lib_source = + env::var("BORING_BSSL_PATH").is_err() && env::var("BORING_BSSL_SOURCE_PATH").is_err(); + + if no_patches_enabled && is_external_native_lib_source { + panic!( + "`no-patches` feature is supposed to be used with `BORING_BSSL_PATH`\ + or `BORING_BSSL_SOURCE_PATH` env variables" + ) + } + + let features_with_patches_enabled = cfg!(any(feature = "rpk", feature = "pq-experimental")); + let patches_required = features_with_patches_enabled && !no_patches_enabled; + let build_from_sources_required = cfg!(feature = "fips-link-precompiled") || patches_required; + let is_precompiled_native_lib = env::var("BORING_BSSL_PATH").is_ok(); + + if is_precompiled_native_lib && build_from_sources_required { + panic!("precompiled BoringSSL was provided, so FIPS configuration or optional patches can't be applied"); + } +} + +fn main() { + println!("cargo:rerun-if-env-changed=BORING_BSSL_PATH"); + println!("cargo:rerun-if-env-changed=BORING_BSSL_INCLUDE_PATH"); + println!("cargo:rerun-if-env-changed=BORING_BSSL_SOURCE_PATH"); + println!("cargo:rerun-if-env-changed=BORING_SSL_PRECOMPILED_BCM_O"); + println!("cargo:rerun-if-env-changed=BORINGSSL_BUILD_DIR"); + + check_feature_compatibility(); + + let bssl_dir = env::var("BORING_BSSL_PATH").unwrap_or_else(|_| build_boring_from_sources()); + let build_path = get_boringssl_platform_output_path(); + + 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 + ); + println!( + "cargo:rustc-link-search=native={}/lib/{}", + bssl_dir, build_path + ); + } else { + println!( + "cargo:rustc-link-search=native={}/build/{}", + bssl_dir, build_path + ); + } + + if cfg!(feature = "fips-link-precompiled") { + link_in_precompiled_bcm_o(&bssl_dir); + } + + println!("cargo:rustc-link-lib=static=crypto"); + println!("cargo:rustc-link-lib=static=ssl"); + + let include_path = env::var("BORING_BSSL_INCLUDE_PATH").unwrap_or_else(|_| { + if let Ok(bssl_path) = env::var("BORING_BSSL_PATH") { + return format!("{}/include", bssl_path); + } + + let src_path = get_boringssl_source_path(); + + if Path::new(&src_path).join("include").exists() { + format!("{}/include", &src_path) + } else { + format!("{}/src/include", &src_path) + } + }); + + let mut builder = bindgen::Builder::default() + .derive_copy(true) + .derive_debug(true) + .derive_default(true) + .derive_eq(true) + .default_enum_style(bindgen::EnumVariation::NewType { + is_bitfield: false, + is_global: false, + }) + .default_macro_constant_type(bindgen::MacroTypeVariation::Signed) + .generate_comments(true) + .fit_macro_constants(false) + .size_t_is_usize(true) + .layout_tests(true) + .prepend_enum_name(true) + .clang_args(get_extra_clang_args_for_bindgen()) + .clang_args(&["-I", &include_path]); + + let target = env::var("TARGET").unwrap(); + match target.as_ref() { + // bindgen produces alignment tests that cause undefined behavior [1] + // when applied to explicitly unaligned types like OSUnalignedU64. + // + // There is no way to disable these tests for only some types + // and it's not nice to suppress warnings for the entire crate, + // so let's disable all alignment tests and hope for the best. + // + // [1]: https://github.com/rust-lang/rust-bindgen/issues/1651 + "aarch64-apple-ios" | "aarch64-apple-ios-sim" => { + builder = builder.layout_tests(false); + } + _ => {} + } + + let headers = [ + "aes.h", + "asn1_mac.h", + "asn1t.h", + "blake2.h", + "blowfish.h", + "cast.h", + "chacha.h", + "cmac.h", + "cpu.h", + "curve25519.h", + "des.h", + "dtls1.h", + "hkdf.h", + "hmac.h", + "hrss.h", + "md4.h", + "md5.h", + "obj_mac.h", + "objects.h", + "opensslv.h", + "ossl_typ.h", + "pkcs12.h", + "poly1305.h", + "rand.h", + "rc4.h", + "ripemd.h", + "siphash.h", + "srtp.h", + "trust_token.h", + "x509v3.h", + ]; + for header in &headers { + builder = builder.header( + Path::new(&include_path) + .join("openssl") + .join(header) + .to_str() + .unwrap(), + ); + } + + let bindings = builder.generate().expect("Unable to generate bindings"); + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + bindings + .write_to_file(out_path.join("bindings.rs")) + .expect("Couldn't write bindings!"); +} diff --git a/boring-sys/build/config.rs b/boring-sys/build/config.rs index 79d119fc..d05396ae 100644 --- a/boring-sys/build/config.rs +++ b/boring-sys/build/config.rs @@ -18,6 +18,7 @@ pub(crate) struct Features { pub(crate) fips_link_precompiled: bool, pub(crate) pq_experimental: bool, pub(crate) rpk: bool, + pub(crate) underscore_wildcards: bool, } pub(crate) struct Env { @@ -82,7 +83,10 @@ impl Config { ); } - let features_with_patches_enabled = self.features.rpk || self.features.pq_experimental; + let features_with_patches_enabled = self.features.rpk + || self.features.pq_experimental + || self.features.underscore_wildcards; + let patches_required = features_with_patches_enabled && !self.env.assume_patched; let build_from_sources_required = self.features.fips_link_precompiled || patches_required; @@ -98,12 +102,14 @@ impl Features { let fips_link_precompiled = env::var_os("CARGO_FEATURE_FIPS_LINK_PRECOMPILED").is_some(); let pq_experimental = env::var_os("CARGO_FEATURE_PQ_EXPERIMENTAL").is_some(); let rpk = env::var_os("CARGO_FEATURE_RPK").is_some(); + let underscore_wildcards = env::var_os("CARGO_FEATURE_UNDERSCORE_WILDCARDS").is_some(); Self { fips, fips_link_precompiled, pq_experimental, rpk, + underscore_wildcards, } } } diff --git a/boring-sys/build/main.rs b/boring-sys/build/main.rs index b2ed72f6..68f24c35 100644 --- a/boring-sys/build/main.rs +++ b/boring-sys/build/main.rs @@ -491,6 +491,11 @@ fn ensure_patches_applied(config: &Config) -> io::Result<()> { apply_patch(config, "rpk.patch")?; } + if config.features.underscore_wildcards { + println!("cargo:warning=applying underscore wildcards patch to boringssl"); + apply_patch(config, "underscore-wildcards.patch")?; + } + Ok(()) } diff --git a/boring-sys/patches/underscore-wildcards.patch b/boring-sys/patches/underscore-wildcards.patch new file mode 100644 index 00000000..f281b3a1 --- /dev/null +++ b/boring-sys/patches/underscore-wildcards.patch @@ -0,0 +1,61 @@ +https://github.com/google/boringssl/compare/master...cloudflare:boringssl:underscore-wildcards + +--- a/src/crypto/x509v3/v3_utl.c ++++ b/src/crypto/x509v3/v3_utl.c +@@ -790,7 +790,9 @@ static int wildcard_match(const unsigned char *prefix, size_t prefix_len, + // Check that the part matched by the wildcard contains only + // permitted characters and only matches a single label. + for (p = wildcard_start; p != wildcard_end; ++p) { +- if (!OPENSSL_isalnum(*p) && *p != '-') { ++ if (!OPENSSL_isalnum(*p) && *p != '-' && ++ !(*p == '_' && ++ (flags & X509_CHECK_FLAG_UNDERSCORE_WILDCARDS))) { + return 0; + } + } +--- a/src/crypto/x509/x509_test.cc ++++ b/src/crypto/x509/x509_test.cc +@@ -4500,6 +4500,31 @@ TEST(X509Test, Names) { + /*invalid_emails=*/{}, + /*flags=*/0, + }, ++ ++ // Underscores in DNS names are forbidden by default. ++ { ++ /*cert_subject=*/{}, ++ /*cert_dns_names=*/{"*.example.com"}, ++ /*cert_emails=*/{}, ++ /*valid_dns_names=*/{}, ++ /*invalid_dns_names=*/{"not_allowed.example.com"}, ++ /*valid_emails=*/{}, ++ /*invalid_emails=*/{}, ++ /*flags=*/0, ++ }, ++ ++ // Underscores in DNS names can be allowed with the right flag. ++ { ++ /*cert_subject=*/{}, ++ /*cert_dns_names=*/{"*.example.com"}, ++ /*cert_emails=*/{}, ++ /*valid_dns_names=*/{"now_allowed.example.com"}, ++ /*invalid_dns_names=*/{}, ++ /*valid_emails=*/{}, ++ /*invalid_emails=*/{}, ++ /*flags=*/X509_CHECK_FLAG_UNDERSCORE_WILDCARDS, ++ }, ++ + }; + + size_t i = 0; +--- a/src/include/openssl/x509c3.h ++++ b/src/include/openssl/x509v3.h +@@ -4497,6 +4497,8 @@ OPENSSL_EXPORT int X509_PURPOSE_get_id(const X509_PURPOSE *); + #define X509_CHECK_FLAG_SINGLE_LABEL_SUBDOMAINS 0 + // Skip the subject common name fallback if subjectAltNames is missing. + #define X509_CHECK_FLAG_NEVER_CHECK_SUBJECT 0x20 ++// Allow underscores in DNS wildcard matches. ++#define X509_CHECK_FLAG_UNDERSCORE_WILDCARDS 0x40 + + OPENSSL_EXPORT int X509_check_host(X509 *x, const char *chk, size_t chklen, + unsigned int flags, char **peername); +-- diff --git a/boring/Cargo.toml b/boring/Cargo.toml index 64564907..0a576ab4 100644 --- a/boring/Cargo.toml +++ b/boring/Cargo.toml @@ -12,7 +12,7 @@ categories = ["cryptography", "api-bindings"] edition = { workspace = true } [package.metadata.docs.rs] -features = ["rpk", "pq-experimental"] +features = ["rpk", "pq-experimental", "underscore-wildcards"] rustdoc-args = ["--cfg", "docsrs"] [features] @@ -38,6 +38,11 @@ rpk = ["boring-sys/rpk"] # `BORING_BSSL{,_FIPS}_SOURCE_PATH` and `BORING_BSSL{,_FIPS}_ASSUME_PATCHED`. pq-experimental = ["boring-sys/pq-experimental"] +# Applies a patch to enable +# `ffi::X509_CHECK_FLAG_UNDERSCORE_WILDCARDS`. Same caveats as +# those for `pq-experimental` feature apply. +underscore-wildcards = ["boring-sys/underscore-wildcards"] + # Controlling key exchange preferences at compile time # Choose key exchange preferences at compile time. This prevents the user from diff --git a/boring/src/ssl/test/mod.rs b/boring/src/ssl/test/mod.rs index 29b85ac3..90f203e4 100644 --- a/boring/src/ssl/test/mod.rs +++ b/boring/src/ssl/test/mod.rs @@ -503,12 +503,81 @@ fn verify_valid_hostname() { client.ctx().set_verify(SslVerifyMode::PEER); + let mut client = client.build().builder(); + + client.ssl().param_mut().set_host("foobar.com").unwrap(); + client.connect(); +} + +#[test] +fn verify_valid_hostname_with_wildcard() { + let mut server = Server::builder(); + + server + .ctx() + .set_certificate_chain_file("test/cert-wildcard.pem") + .unwrap(); + + let server = server.build(); + let mut client = server.client_with_root_ca(); + + client.ctx().set_verify(SslVerifyMode::PEER); + + let mut client = client.build().builder(); + client.ssl().param_mut().set_host("yes.foobar.com").unwrap(); + client.connect(); +} + +#[test] +fn verify_reject_underscore_hostname_with_wildcard() { + let mut server = Server::builder(); + + server.should_error(); + server + .ctx() + .set_certificate_chain_file("test/cert-wildcard.pem") + .unwrap(); + + let server = server.build(); + let mut client = server.client_with_root_ca(); + + client.ctx().set_verify(SslVerifyMode::PEER); + let mut client = client.build().builder(); client .ssl() .param_mut() - .set_hostflags(X509CheckFlags::NO_PARTIAL_WILDCARDS); - client.ssl().param_mut().set_host("foobar.com").unwrap(); + .set_host("not_allowed.foobar.com") + .unwrap(); + client.connect_err(); +} + +#[cfg(feature = "underscore-wildcards")] +#[test] +fn verify_allow_underscore_hostname_with_wildcard() { + let mut server = Server::builder(); + + server + .ctx() + .set_certificate_chain_file("test/cert-wildcard.pem") + .unwrap(); + + let server = server.build(); + let mut client = server.client_with_root_ca(); + + client.ctx().set_verify(SslVerifyMode::PEER); + + let mut client = client.build().builder(); + + client + .ssl() + .param_mut() + .set_hostflags(X509CheckFlags::UNDERSCORE_WILDCARDS); + client + .ssl() + .param_mut() + .set_host("now_allowed.foobar.com") + .unwrap(); client.connect(); } diff --git a/boring/src/x509/verify.rs b/boring/src/x509/verify.rs index 8bc17a58..decc3092 100644 --- a/boring/src/x509/verify.rs +++ b/boring/src/x509/verify.rs @@ -16,6 +16,8 @@ bitflags! { const MULTI_LABEL_WILDCARDS = ffi::X509_CHECK_FLAG_MULTI_LABEL_WILDCARDS as _; const SINGLE_LABEL_SUBDOMAINS = ffi::X509_CHECK_FLAG_SINGLE_LABEL_SUBDOMAINS as _; const NEVER_CHECK_SUBJECT = ffi::X509_CHECK_FLAG_NEVER_CHECK_SUBJECT as _; + #[cfg(feature = "underscore-wildcards")] + const UNDERSCORE_WILDCARDS = ffi::X509_CHECK_FLAG_UNDERSCORE_WILDCARDS as _; #[deprecated(since = "0.10.6", note = "renamed to NO_WILDCARDS")] const FLAG_NO_WILDCARDS = ffi::X509_CHECK_FLAG_NO_WILDCARDS as _; diff --git a/boring/test/cert-wildcard.pem b/boring/test/cert-wildcard.pem new file mode 100644 index 00000000..825e411f --- /dev/null +++ b/boring/test/cert-wildcard.pem @@ -0,0 +1,20 @@ +notAfter=Aug 12 11:30:03 2026 GMT +-----BEGIN CERTIFICATE----- +MIIDKDCCAhACFGwwuilXOHjBjQ584FD9drp9Uh/LMA0GCSqGSIb3DQEBCwUAMEUx +CzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl +cm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMjMxMjE4MTEzMDAzWhcNMjYwODEyMTEz +MDAzWjBcMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UE +BwwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRUwEwYDVQQDDAwqLmZvb2Jhci5j +b20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCo9CWMRLMXo1CF/iOR +h9B4NhtJF/8tR9PlG95sNvyWuQQ/8jfev+8zErplxfLkt0pJqcoiZG8g9NU0kU6o +5T+/1QgZclCAoZaS0Jqxmoo2Yk/1Qsj16pnMBc10uSDk6V9aJSX1vKwONVNSwiHA +1MhX+i7Wf7/K0niq+k7hOkhleFkWgZtUq41gXh1VfOugka7UktYnk9mrBbAMjmal +oZNn2pMMAQxVg4ThiLm3zvuWqvXASWzUZc7IAd1GbN4AtDuhs252eqE9E4iTHk7F +14wAS1JWqv666hReGHrmZJGx0xQTM9vPD1HN5t2U3KTfhO/mTlAUWVyg9tCtOzbo +Kgs1AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHG83qKMl5bPoL2s7TaJZ909NaQO +4C69ueXlD4HJEFe7L9mkeQoDaF7RwWSBwN2RZT5hzQhghRotqLA06XwKbQHji/R7 +sYYVUHunobFUHsr51tFN1BIDoAWJa0N2rm/OxbcK471eWNKjMiS2vvvPdaMxxHAx +IsjAJBJec4IxNIUNNKqCS/xNYcdiyrmmU3oFWGqb0As/eDOBw0Amd0aayasFJrRV +3KZI5OcFg/J3XvdaxMJD+RPyUysKRXg6K8jzYc/PB8LhWVXpLxjEzeO2IHCaZprh +dUTP8+Ob+ioxujvlslxc4nrrUD5EWwnpEIr7e4af27JHQVaNyHbRw6wI2uk= +-----END CERTIFICATE-----