diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index fb168edf735..7e8178da002 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -7,7 +7,10 @@ permissions: pull-requests: write env: - FEATURES: "async,ffi,qlog" + # `rpk` covers the boring 4 vs 5 RPK arms in + # `tokio-quiche/src/settings/config.rs`; see the same comment in + # `stable.yml`. + FEATURES: "async,ffi,qlog,rpk" RUSTFLAGS: "-D warnings" RUSTDOCFLAGS: "--cfg docsrs" RUSTTOOLCHAIN: "nightly" @@ -68,12 +71,15 @@ jobs: # duplicate builds for PRs created from internal branches. if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository # `quiche-fuzz` calls `RAND_reset_for_fuzzing`, which BoringSSL only - # exports when `BORINGSSL_UNSAFE_DETERMINISTIC_MODE` is defined. - # `boring-sys` doesn't expose a feature for that, but cmake-rs honors - # `CFLAGS`/`CXXFLAGS`, so inject the defines there. + # exports when built with `FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION`. + # BoringSSL 5.x consolidated the older + # `BORINGSSL_UNSAFE_{DETERMINISTIC,FUZZER}_MODE` defines into this + # single switch. `boring-sys` doesn't expose a feature for it, but + # cmake-rs (via cc-rs) honors `CFLAGS`/`CXXFLAGS`, so inject the + # define there. env: - CFLAGS: "-DBORINGSSL_UNSAFE_DETERMINISTIC_MODE -DBORINGSSL_UNSAFE_FUZZER_MODE" - CXXFLAGS: "-DBORINGSSL_UNSAFE_DETERMINISTIC_MODE -DBORINGSSL_UNSAFE_FUZZER_MODE" + CFLAGS: "-DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION" + CXXFLAGS: "-DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION" steps: - name: Checkout sources uses: actions/checkout@v4 diff --git a/.github/workflows/stable.yml b/.github/workflows/stable.yml index caa2ef17f25..31784470b9c 100644 --- a/.github/workflows/stable.yml +++ b/.github/workflows/stable.yml @@ -7,7 +7,11 @@ permissions: pull-requests: write env: - DEFAULT_OPTIONS: "--features=async,ffi,qlog --workspace" + # `rpk` is included so the boring 4 vs 5 RPK arms in + # `tokio-quiche/src/settings/config.rs` are exercised by CI; + # without it neither arm is compiled and a regression on either + # version would slip through. + DEFAULT_OPTIONS: "--features=async,ffi,qlog,rpk --workspace" # Used by `quiche_multiarch`, which can't link `boring` for foreign targets. NO_BORING_OPTIONS: "--features=ffi,qlog --workspace --exclude h3i --exclude tokio-quiche" RUSTFLAGS: "-D warnings" @@ -20,6 +24,10 @@ concurrency: jobs: quiche: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + boring_version: ["latest", "4"] # Only run on "pull_request" event for external PRs. This is to avoid # duplicate builds for PRs created from internal branches. if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository @@ -38,6 +46,17 @@ jobs: sudo apt-get update sudo apt-get install libexpat1-dev libfreetype6-dev libfontconfig1-dev + - name: Maybe pin boring to latest 4.x + if: matrix.boring_version == '4' + run: | + # Pin boring 4. cargo's resolver normally picks the highest match + # (5). Generate the lockfile first, then downgrade to a known-good + # 4.x via `--precise`. `quiche/src/build.rs` and + # `tokio-quiche/build.rs` then read the lockfile and flip + # `cfg(boring_v4)` on. + cargo generate-lockfile + cargo update -p boring --precise 4.22.0 + - name: Unused dependency check uses: bnjbvr/cargo-machete@main @@ -130,11 +149,21 @@ jobs: matrix: target: ["x86_64-pc-windows-msvc", "i686-pc-windows-msvc", "x86_64-pc-windows-gnu", "i686-pc-windows-gnu"] include: + # i686 (32-bit) BoringSSL builds require SSE2 explicitly. The + # toolchain file in BoringSSL sets `-msse2`, but cmake-rs + # passes `-DCMAKE_C_FLAGS=...` on the command line which + # overrides that, so we re-add it here. + - target: "i686-pc-windows-msvc" + cxxflags: "-msse2" - target: "i686-pc-windows-gnu" - cflags: "-mlong-double-64" # bindgen detects that Rust doesn't support 80-bit long double + # `-mlong-double-64`: bindgen detects that Rust doesn't + # support 80-bit long double. + # `-msse2`: same reason as i686-pc-windows-msvc above. + cflags: "-mlong-double-64 -msse2" + cxxflags: "-mlong-double-64 -msse2" env: CFLAGS: ${{ matrix.cflags }} - CXXFLAGS: ${{ matrix.cflags }} + CXXFLAGS: ${{ matrix.cxxflags }} BINDGEN_EXTRA_CLANG_ARGS: ${{ matrix.cflags }} # Only run on "pull_request" event for external PRs. This is to avoid # duplicate builds for PRs created from internal branches. @@ -149,18 +178,55 @@ jobs: toolchain: ${{ env.RUSTTOOLCHAIN }} targets: ${{ matrix.target }} + # The windows-2022 image preinstalls MSYS2 (gcc 14.2.0) at + # `C:\msys64` but doesn't put it on PATH. For x86_64-pc-windows-gnu + # we rely on these preinstalled tools and just point the C/C++ + # toolchain at them, matching `boring`'s own CI workflow. - name: Set up MinGW for 64 bit if: matrix.target == 'x86_64-pc-windows-gnu' - uses: bwoodsend/setup-winlibs-action@v1.10 - with: - tag: 12.2.0-16.0.0-10.0.0-msvcrt-r5 - + shell: bash + run: | + echo >> "$GITHUB_ENV" CC=gcc + echo >> "$GITHUB_ENV" CXX=g++ + echo >> "$GITHUB_ENV" 'C_INCLUDE_PATH=C:\msys64\usr\include' + echo >> "$GITHUB_ENV" 'CPLUS_INCLUDE_PATH=C:\msys64\usr\include' + echo >> "$GITHUB_ENV" 'LIBRARY_PATH=C:\msys64\usr\lib' + + # For i686 we install a fresh 32-bit MSYS2 toolchain (and a + # 32-bit cmake from MSYS2, since the system cmake is 64-bit and + # picks up the wrong `gcc`). Mirrors `boring`'s CI workflow. - name: Set up MinGW for 32 bit if: matrix.target == 'i686-pc-windows-gnu' - uses: bwoodsend/setup-winlibs-action@v1.10 + uses: msys2/setup-msys2@v2 + id: msys2 with: - architecture: i686 - tag: 12.2.0-16.0.0-10.0.0-msvcrt-r5 + msystem: MINGW32 + path-type: inherit + install: >- + mingw-w64-i686-gcc + mingw-w64-i686-cmake + + - name: Set up 32-bit MSYS2 env vars + if: matrix.target == 'i686-pc-windows-gnu' + shell: bash + run: | + MSYS_ROOT='${{ steps.msys2.outputs.msys2-location }}' + test -d "$MSYS_ROOT\\mingw32\\bin" + echo >> $GITHUB_PATH "$MSYS_ROOT\\mingw32\\bin" + echo >> $GITHUB_PATH "$MSYS_ROOT\\usr\\bin" + echo >> $GITHUB_ENV CC="$MSYS_ROOT\\mingw32\\bin\\gcc" + echo >> $GITHUB_ENV CXX="$MSYS_ROOT\\mingw32\\bin\\g++" + echo >> $GITHUB_ENV AR="$MSYS_ROOT\\mingw32\\bin\\ar" + echo >> $GITHUB_ENV CFLAGS="$CFLAGS -I$MSYS_ROOT\\mingw32\\include" + echo >> $GITHUB_ENV CXXFLAGS="$CXXFLAGS -I$MSYS_ROOT\\mingw32\\include" + echo >> $GITHUB_ENV BINDGEN_EXTRA_CLANG_ARGS="$BINDGEN_EXTRA_CLANG_ARGS -I$MSYS_ROOT\\mingw32\\include" + echo >> $GITHUB_ENV LIBRARY_PATH="$MSYS_ROOT\\mingw32\\lib" + echo >> $GITHUB_ENV LDFLAGS="-L$MSYS_ROOT\\mingw32\\lib" + # Force cmake-rs to use the MinGW Makefiles generator instead + # of MSYS Makefiles; cmake-rs's default for windows-gnu picks + # the latter, which then can't find `cl.exe` (cmake's default + # Windows compiler) and fails configuration. + echo >> $GITHUB_ENV "CMAKE_GENERATOR=MinGW Makefiles" - name: Install dependencies uses: crazy-max/ghaction-chocolatey@v3 diff --git a/Cargo.toml b/Cargo.toml index b32eff88703..bb8fe8c9c45 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ categories = ["network-programming"] unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(capture_keylogs)', 'cfg(test_invalid_len_compilation_fail)', + 'cfg(boring_v4)', ] } [workspace.metadata.release] @@ -40,7 +41,20 @@ publish = false [workspace.dependencies] anyhow = { version = "1" } assert_matches = { version = "1" } -boring = { version = "4.3" } +# Accept both `boring` 4.x (>= 4.21, where the APIs we use stabilised) +# and `boring` 5.x. The active version is picked by cargo's resolver +# based on the lockfile / downstream constraints; quiche's `build.rs` +# detects which one was selected and emits `cfg(boring_v4)` for the +# legacy 4.x case so source code can switch on it. The `boring_v4` +# cfg is meant to be a transitional flag; new code should live in the +# `not(boring_v4)` arm so the cfg can be retired by simply deleting +# the legacy arms once internal users have moved off 4.x. See +# `quiche/src/build.rs` and the `boringssl-boring-crate` feature in +# `quiche/Cargo.toml`. +# +# `boring-sys` uses `links = "boringssl"`, so only one major version +# can ever be in the dep graph at a time. +boring = { version = ">=4.21, <6" } buffer-pool = { version = "0.2.1", path = "./buffer-pool" } bytes = { version = "1.11.1" } crossbeam = { version = "0.8.1", default-features = false } diff --git a/Cross.toml b/Cross.toml index 337d60ab194..d2efb888beb 100644 --- a/Cross.toml +++ b/Cross.toml @@ -27,3 +27,15 @@ pre-build = [ "ln -sf /usr/bin/gcc /usr/local/bin/i686-linux-gnu-gcc", "ln -sf /usr/bin/g++ /usr/local/bin/i686-linux-gnu-g++", ] + +# BoringSSL's x86 assembly requires SSE2. `boring-sys`'s cmake build +# doesn't add `-msse2` for i686 targets (its 32-bit-toolchain.cmake +# sets it as a CACHE STRING, but cmake-rs's `-DCMAKE_C_FLAGS=...` on +# the command line wins), so inject it via the target-scoped +# `CFLAGS`/`CXXFLAGS` env vars. `cc-rs` (and through it, `cmake-rs`) +# honors these and passes them along to BoringSSL's cmake build. +[target.i686-unknown-linux-gnu.env] +passthrough = [ + "CFLAGS_i686_unknown_linux_gnu=-msse2 -mfpmath=sse", + "CXXFLAGS_i686_unknown_linux_gnu=-msse2 -mfpmath=sse", +] diff --git a/quiche/Cargo.toml b/quiche/Cargo.toml index 60981eab071..9c132651320 100644 --- a/quiche/Cargo.toml +++ b/quiche/Cargo.toml @@ -28,6 +28,22 @@ workspace = true default = ["boringssl-boring-crate"] # Use the BoringSSL library provided by the boring crate. +# +# Both `boring` 4.x (>= 4.21) and 5.x are supported. Cargo's resolver +# picks the version based on the lockfile and downstream constraints; +# `build.rs` detects which major version was selected at compile time +# and emits a `cfg(boring_v4)` flag for the legacy 4.x case. The two +# majors differ in their default TLS curve list (5.x advertises +# post-quantum key shares) and in their Rust API for raw public +# keys; see `tokio-quiche/src/settings/config.rs` for the per-version +# code. `boring_v4` is a transitional cfg meant to be retired once +# internal users have moved off 4.x. +# +# Downstream code that depends on `boring` directly (e.g. to construct +# an `SslContextBuilder` for `Config::with_boring_ssl_ctx_builder`) +# will resolve to the same `boring` version as quiche, since +# `boring-sys` uses `links = "boringssl"` and cargo allows only one +# copy in the dep graph. boringssl-boring-crate = ["boring", "foreign-types-shared"] # Allow client connections to provide a custom DCID when initiating a diff --git a/quiche/examples/Makefile b/quiche/examples/Makefile index 6662c62d498..eb0e4389252 100644 --- a/quiche/examples/Makefile +++ b/quiche/examples/Makefile @@ -28,16 +28,16 @@ LIBS = $(LIB_DIR)/libquiche.a -lev -ldl -pthread -lm all: client server http3-client http3-server client: client.c $(INCLUDE_DIR)/quiche.h $(LIB_DIR)/libquiche.a - $(CC) $(CFLAGS) $(LDFLAGS) $< -o $@ $(INCS) $(LIBS) + $(CXX) $(CFLAGS) $(LDFLAGS) -x c $< -x none -o $@ $(INCS) $(LIBS) server: server.c $(INCLUDE_DIR)/quiche.h $(LIB_DIR)/libquiche.a - $(CC) $(CFLAGS) $(LDFLAGS) $< -o $@ $(INCS) $(LIBS) + $(CXX) $(CFLAGS) $(LDFLAGS) -x c $< -x none -o $@ $(INCS) $(LIBS) http3-client: http3-client.c $(INCLUDE_DIR)/quiche.h $(LIB_DIR)/libquiche.a - $(CC) $(CFLAGS) $(LDFLAGS) $< -o $@ $(INCS) $(LIBS) + $(CXX) $(CFLAGS) $(LDFLAGS) -x c $< -x none -o $@ $(INCS) $(LIBS) http3-server: http3-server.c $(INCLUDE_DIR)/quiche.h $(LIB_DIR)/libquiche.a - $(CC) $(CFLAGS) $(LDFLAGS) $< -o $@ $(INCS) $(LIBS) + $(CXX) $(CFLAGS) $(LDFLAGS) -x c $< -x none -o $@ $(INCS) $(LIBS) $(LIB_DIR)/libquiche.a: $(shell find $(SOURCE_DIR) -type f -name '*.rs') cd .. && cargo build --target-dir $(BUILD_DIR) --features ffi diff --git a/quiche/include/quiche.h b/quiche/include/quiche.h index 4b866e60d4a..1472e86f093 100644 --- a/quiche/include/quiche.h +++ b/quiche/include/quiche.h @@ -168,6 +168,9 @@ int quiche_config_load_verify_locations_from_file(quiche_config *config, int quiche_config_load_verify_locations_from_directory(quiche_config *config, const char *path); +// Configures the TLS curve preference list (colon-separated, e.g. "X25519MLKEM768:X25519:P-256:P-384"). +int quiche_config_set_curves_list(quiche_config *config, const char *curves); + // Configures whether to verify the peer's certificate. void quiche_config_verify_peer(quiche_config *config, bool v); diff --git a/quiche/src/build.rs b/quiche/src/build.rs index f82b7457694..097a94200db 100644 --- a/quiche/src/build.rs +++ b/quiche/src/build.rs @@ -30,6 +30,83 @@ Cflags: -I${{includedir}} out_file.write_all(output.as_bytes()).unwrap(); } +/// Returns true if cargo resolved `boring` to a 4.x version. +/// +/// Walks up from `OUT_DIR` looking for a `Cargo.lock`, then scans it +/// for the `boring` package. We use `Cargo.lock` rather than shelling +/// out to `cargo metadata` because (a) the lockfile is guaranteed to +/// exist at this point in the build, (b) parsing it is cheap and has +/// no extra dependencies, and (c) it avoids re-entering cargo from a +/// build script. +fn detect_boring_v4() -> bool { + let Some(lockfile) = find_cargo_lock() else { + // No lockfile (shouldn't happen in normal cargo builds, but + // be conservative). Assume 5.x — the default and forward- + // looking version. Downstream can fix this by generating a + // lockfile (`cargo generate-lockfile`). + println!( + "cargo:warning=quiche: Cargo.lock not found; assuming boring 5.x" + ); + return false; + }; + + println!("cargo:rerun-if-changed={}", lockfile.display()); + + let contents = match std::fs::read_to_string(&lockfile) { + Ok(s) => s, + Err(e) => { + println!( + "cargo:warning=quiche: failed to read {}: {e}; assuming boring 5.x", + lockfile.display(), + ); + return false; + }, + }; + + // The lockfile is TOML but a regex-light scan is enough: find a + // `[[package]]` whose `name = "boring"` (not "boring-sys") and + // read its `version`. + let mut in_boring = false; + for line in contents.lines() { + let line = line.trim(); + if line == "[[package]]" { + in_boring = false; + continue; + } + if line == "name = \"boring\"" { + in_boring = true; + continue; + } + if in_boring { + if let Some(rest) = line.strip_prefix("version = \"") { + let version = rest.trim_end_matches('"'); + let major = version.split('.').next().unwrap_or(""); + return major == "4"; + } + } + } + + // `boring` not present in the lockfile (e.g. + // `boringssl-boring-crate` is off). Doesn't matter what we return + // since the `cfg` won't be observed. + false +} + +fn find_cargo_lock() -> Option { + // Start from `CARGO_MANIFEST_DIR` and walk up. Cargo guarantees + // the lockfile lives at the workspace root, which is an ancestor + // of the manifest dir. + let manifest_dir = + std::path::PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR")?); + for dir in manifest_dir.ancestors() { + let candidate = dir.join("Cargo.lock"); + if candidate.is_file() { + return Some(candidate); + } + } + None +} + fn target_dir_path() -> std::path::PathBuf { let out_dir = std::env::var("OUT_DIR").unwrap(); let out_dir = std::path::Path::new(&out_dir); @@ -44,7 +121,18 @@ fn target_dir_path() -> std::path::PathBuf { } fn main() { + // Emit `cfg(boring_v4)` if boring version 4.x is detected. This is used to + // pick which APIs to expect and to guide test expectations. (Larger post + // quantum key shares are enabled by default in boring 5.x but not boring + // 4.x.) + // + // The cfg is always registered (even when the backend feature is + // off) so rustc doesn't warn about unknown cfg names. + println!("cargo::rustc-check-cfg=cfg(boring_v4)"); if cfg!(feature = "boringssl-boring-crate") { + if detect_boring_v4() { + println!("cargo:rustc-cfg=boring_v4"); + } println!("cargo:rustc-link-lib=static=ssl"); println!("cargo:rustc-link-lib=static=crypto"); } diff --git a/quiche/src/ffi.rs b/quiche/src/ffi.rs index d02d985ab1b..b25265418a7 100644 --- a/quiche/src/ffi.rs +++ b/quiche/src/ffi.rs @@ -200,6 +200,19 @@ pub extern "C" fn quiche_config_load_verify_locations_from_directory( } } +#[no_mangle] +pub extern "C" fn quiche_config_set_curves_list( + config: &mut Config, curves: *const c_char, +) -> c_int { + let curves = unsafe { ffi::CStr::from_ptr(curves).to_str().unwrap() }; + + match config.set_curves_list(curves) { + Ok(_) => 0, + + Err(e) => e.to_c() as c_int, + } +} + #[no_mangle] pub extern "C" fn quiche_config_verify_peer(config: &mut Config, v: bool) { config.verify_peer(v); diff --git a/quiche/src/h3/mod.rs b/quiche/src/h3/mod.rs index 1d9e903c1e4..b33be1e7cd0 100644 --- a/quiche/src/h3/mod.rs +++ b/quiche/src/h3/mod.rs @@ -3651,7 +3651,10 @@ mod tests { fn h3_handshake_0rtt() { let mut buf = [0; 65535]; - let mut config = crate::Config::new(crate::PROTOCOL_VERSION).unwrap(); + // Test drives the handshake one Initial at a time; requires a + // single-Initial ClientHello. + let mut config = + crate::test_utils::config_no_pq(crate::PROTOCOL_VERSION).unwrap(); config .load_cert_chain_from_pem_file("examples/cert.crt") .unwrap(); diff --git a/quiche/src/lib.rs b/quiche/src/lib.rs index e4d95500a04..e0cc54dbbc4 100644 --- a/quiche/src/lib.rs +++ b/quiche/src/lib.rs @@ -760,6 +760,16 @@ impl Config { self.tls_ctx.load_verify_locations_from_directory(dir) } + /// Configures the TLS curve preference list. + /// + /// `curves` is a colon-separated list of curve (a.k.a. group) names, in + /// order of preference, e.g. `"X25519MLKEM768:X25519:P-256:P-384"`. + /// Corresponds to `SSL_CTX_set1_curves_list` (a.k.a. + /// `SSL_CTX_set1_groups_list`). + pub fn set_curves_list(&mut self, curves: &str) -> Result<()> { + self.tls_ctx.set_curves_list(curves) + } + /// Configures whether to verify the peer's certificate. /// /// This should usually be `true` for client-side connections and `false` diff --git a/quiche/src/test_utils.rs b/quiche/src/test_utils.rs index 2f403e699fc..04990756bbc 100644 --- a/quiche/src/test_utils.rs +++ b/quiche/src/test_utils.rs @@ -30,6 +30,28 @@ use smallvec::smallvec; use crate::recovery::Sent; +/// Curve preference list that excludes post-quantum groups. +/// +/// Several tests were written against a pre-PQ world where the ClientHello +/// fit in a single Initial packet; those tests opt out of PQ to keep that +/// invariant. See [`config_no_pq`] and +/// [`Config::set_curves_list`](crate::Config::set_curves_list). +const NO_PQ_CURVES: &str = "X25519:P-256:P-384"; + +/// Returns a `Config` equivalent to `Config::new(version)` but with +/// post-quantum TLS curves disabled. +/// +/// Use this as a drop-in replacement for `Config::new(version)` in tests +/// whose expected packet counts/sizes were calibrated against a +/// single-Initial ClientHello. A post-quantum keyshare pushes the +/// ClientHello across two Initial packets, which perturbs those +/// expectations. +pub fn config_no_pq(version: u32) -> Result { + let mut config = Config::new(version)?; + config.set_curves_list(NO_PQ_CURVES)?; + Ok(config) +} + pub struct Pipe where F: BufFactory, @@ -57,6 +79,14 @@ impl Pipe { Ok(config) } + /// Like [`default_config`](Self::default_config) but with post-quantum + /// TLS curves disabled. See [`config_no_pq`] for the rationale. + pub fn default_config_no_pq(cc_algorithm_name: &str) -> Result { + let mut config = Self::default_config(cc_algorithm_name)?; + config.set_curves_list(NO_PQ_CURVES)?; + Ok(config) + } + #[cfg(feature = "boringssl-boring-crate")] pub fn default_tls_ctx_builder() -> boring::ssl::SslContextBuilder { let mut ctx_builder = diff --git a/quiche/src/tests.rs b/quiche/src/tests.rs index 50ebe86a61c..61b3a3abf98 100644 --- a/quiche/src/tests.rs +++ b/quiche/src/tests.rs @@ -32,6 +32,28 @@ use crate::Header; use rstest::rstest; +/// Pick a numeric expectation based on the active `boring` major version. +/// +/// `boring` 5.x ships BoringSSL with post-quantum (X25519MLKEM768) key +/// shares enabled by default, which inflates the ClientHello and ripples +/// through into byte counts and per-epoch packet numbers in several +/// handshake-adjacent assertions below. `boring` 4.x doesn't, so each +/// such assertion has two flavours. Wrap them in this macro so the +/// per-version values stay side-by-side at the call site. The active +/// version is detected by `build.rs` (see `cfg(boring_v4)`). +macro_rules! by_boring { + (b4: $b4:expr, b5: $b5:expr $(,)?) => {{ + #[cfg(boring_v4)] + { + $b4 + } + #[cfg(not(boring_v4))] + { + $b5 + } + }}; +} + #[test] fn transport_params() { // Server encodes, client decodes. @@ -367,8 +389,6 @@ fn verify_client_anonymous() { fn missing_initial_source_connection_id( #[values("cubic", "bbr2_gcongestion")] cc_algorithm_name: &str, ) { - let mut buf = [0; 65535]; - let mut pipe = test_utils::Pipe::new(cc_algorithm_name).unwrap(); // Reset initial_source_connection_id. @@ -377,12 +397,14 @@ fn missing_initial_source_connection_id( .initial_source_connection_id = None; assert_eq!(pipe.client.encode_transport_params(), Ok(())); - // Client sends initial flight. - let (len, _) = pipe.client.send(&mut buf).unwrap(); + // Client sends initial flight. The ClientHello may span multiple Initial + // packets (e.g. when post-quantum key shares are advertised), so deliver + // the whole flight before checking the server's reaction. + let flight = test_utils::emit_flight(&mut pipe.client).unwrap(); // Server rejects transport parameters. assert_eq!( - pipe.server_recv(&mut buf[..len]), + test_utils::process_flight(&mut pipe.server, flight), Err(Error::InvalidTransportParam) ); } @@ -391,8 +413,6 @@ fn missing_initial_source_connection_id( fn invalid_initial_source_connection_id( #[values("cubic", "bbr2_gcongestion")] cc_algorithm_name: &str, ) { - let mut buf = [0; 65535]; - let mut pipe = test_utils::Pipe::new(cc_algorithm_name).unwrap(); // Scramble initial_source_connection_id. @@ -401,12 +421,14 @@ fn invalid_initial_source_connection_id( .initial_source_connection_id = Some(b"bogus value".to_vec().into()); assert_eq!(pipe.client.encode_transport_params(), Ok(())); - // Client sends initial flight. - let (len, _) = pipe.client.send(&mut buf).unwrap(); + // Client sends initial flight. The ClientHello may span multiple Initial + // packets (e.g. when post-quantum key shares are advertised), so deliver + // the whole flight before checking the server's reaction. + let flight = test_utils::emit_flight(&mut pipe.client).unwrap(); // Server rejects transport parameters. assert_eq!( - pipe.server_recv(&mut buf[..len]), + test_utils::process_flight(&mut pipe.server, flight), Err(Error::InvalidTransportParam) ); } @@ -636,7 +658,9 @@ fn handshake_0rtt( ) { let mut buf = [0; 65535]; - let mut config = Config::new(PROTOCOL_VERSION).unwrap(); + // Test assumes the server transitions to early-data state after a + // single `server_recv`, which requires a single-Initial ClientHello. + let mut config = test_utils::config_no_pq(PROTOCOL_VERSION).unwrap(); assert_eq!(config.set_cc_algorithm_name(cc_algorithm_name), Ok(())); config .load_cert_chain_from_pem_file("examples/cert.crt") @@ -700,7 +724,9 @@ fn handshake_0rtt_reordered( ) { let mut buf = [0; 65535]; - let mut config = Config::new(PROTOCOL_VERSION).unwrap(); + // Test assumes the server transitions to early-data state after a + // single `server_recv`, which requires a single-Initial ClientHello. + let mut config = test_utils::config_no_pq(PROTOCOL_VERSION).unwrap(); assert_eq!(config.set_cc_algorithm_name(cc_algorithm_name), Ok(())); config .load_cert_chain_from_pem_file("examples/cert.crt") @@ -902,7 +928,12 @@ fn crypto_limit(#[values("cubic", "bbr2_gcongestion")] cc_algorithm_name: &str) fn limit_handshake_data( #[values("cubic", "bbr2_gcongestion")] cc_algorithm_name: &str, ) { - let mut config = Config::new(PROTOCOL_VERSION).unwrap(); + // This test relies on `cert-big.crt` forcing the server's handshake + // flight above the default `client_sent * MAX_AMPLIFICATION_FACTOR` + // anti-amplification cap, which in turn relies on the ClientHello + // fitting in a single Initial packet (so `client_sent` stays small). + // Use a no-PQ client config to guarantee that. + let mut config = test_utils::config_no_pq(PROTOCOL_VERSION).unwrap(); assert_eq!(config.set_cc_algorithm_name(cc_algorithm_name), Ok(())); config .load_cert_chain_from_pem_file("examples/cert-big.crt") @@ -914,7 +945,7 @@ fn limit_handshake_data( .set_application_protos(&[b"proto1", b"proto2"]) .unwrap(); - let mut pipe = test_utils::Pipe::with_server_config(&mut config).unwrap(); + let mut pipe = test_utils::Pipe::with_config(&mut config).unwrap(); let flight = test_utils::emit_flight(&mut pipe.client).unwrap(); let client_sent = flight.iter().fold(0, |out, p| out + p.0.len()); @@ -959,7 +990,11 @@ fn custom_limit_handshake_data( #[rstest] fn amplification_limited_stat() { - let mut config = Config::new(PROTOCOL_VERSION).unwrap(); + // `cert-big.crt` is sized so the server's handshake flight exceeds the + // default `client_sent * MAX_AMPLIFICATION_FACTOR` anti-amplification + // cap; that only holds when the ClientHello fits in a single Initial, + // so use a no-PQ config. + let mut config = test_utils::config_no_pq(PROTOCOL_VERSION).unwrap(); config .load_cert_chain_from_pem_file("examples/cert-big.crt") .unwrap(); @@ -970,7 +1005,7 @@ fn amplification_limited_stat() { .set_application_protos(&[b"proto1", b"proto2"]) .unwrap(); - let mut pipe = test_utils::Pipe::with_server_config(&mut config).unwrap(); + let mut pipe = test_utils::Pipe::with_config(&mut config).unwrap(); let flight = test_utils::emit_flight(&mut pipe.client).unwrap(); test_utils::process_flight(&mut pipe.server, flight).unwrap(); @@ -1109,7 +1144,9 @@ fn streamio_mixed_actions( fn zero_rtt(#[values("cubic", "bbr2_gcongestion")] cc_algorithm_name: &str) { let mut buf = [0; 65535]; - let mut config = Config::new(PROTOCOL_VERSION).unwrap(); + // Test assumes the server transitions to early-data state after a + // single `server_recv`, which requires a single-Initial ClientHello. + let mut config = test_utils::config_no_pq(PROTOCOL_VERSION).unwrap(); assert_eq!(config.set_cc_algorithm_name(cc_algorithm_name), Ok(())); config .load_cert_chain_from_pem_file("examples/cert.crt") @@ -5716,7 +5753,7 @@ fn retry_missing_original_destination_connection_id( // Client receives Retry and sends new Initial. assert_eq!(pipe.client_recv(&mut buf[..len]), Ok(len)); - let (len, _) = pipe.client.send(&mut buf).unwrap(); + let client_flight = test_utils::emit_flight(&mut pipe.client).unwrap(); // Server accepts connection and send first flight. But original // destination connection ID is ignored. @@ -5729,7 +5766,7 @@ fn retry_missing_original_destination_connection_id( &mut config, ) .unwrap(); - assert_eq!(pipe.server_recv(&mut buf[..len]), Ok(len)); + test_utils::process_flight(&mut pipe.server, client_flight).unwrap(); let flight = test_utils::emit_flight(&mut pipe.server).unwrap(); @@ -5778,7 +5815,7 @@ fn retry_invalid_original_destination_connection_id( // Client receives Retry and sends new Initial. assert_eq!(pipe.client_recv(&mut buf[..len]), Ok(len)); - let (len, _) = pipe.client.send(&mut buf).unwrap(); + let client_flight = test_utils::emit_flight(&mut pipe.client).unwrap(); // Server accepts connection and send first flight. But original // destination connection ID is invalid. @@ -5792,7 +5829,7 @@ fn retry_invalid_original_destination_connection_id( &mut config, ) .unwrap(); - assert_eq!(pipe.server_recv(&mut buf[..len]), Ok(len)); + test_utils::process_flight(&mut pipe.server, client_flight).unwrap(); let flight = test_utils::emit_flight(&mut pipe.server).unwrap(); @@ -5910,7 +5947,10 @@ fn retry_invalid_source_connection_id( // Client receives Retry and sends new Initial. assert_eq!(pipe.client_recv(&mut buf[..len]), Ok(len)); - let (len, send_info) = pipe.client.send(&mut buf).unwrap(); + let client_flight = test_utils::emit_flight(&mut pipe.client).unwrap(); + // `accept_with_retry` below needs a client source address; take it from + // the first client-emitted datagram of the flight. + let send_info = client_flight[0].1; // Server accepts connection and send first flight. But retry source // connection ID is invalid. @@ -5928,7 +5968,7 @@ fn retry_invalid_source_connection_id( &mut config, ) .unwrap(); - assert_eq!(pipe.server_recv(&mut buf[..len]), Ok(len)); + test_utils::process_flight(&mut pipe.server, client_flight).unwrap(); let flight = test_utils::emit_flight(&mut pipe.server).unwrap(); @@ -6408,7 +6448,7 @@ fn client_rst_stream_while_bytes_in_flight( if cc_algorithm_name == "cubic" { Ok(12000) } else { - Ok(13878) + Ok(by_boring!(b4: 13878, b5: 15030)) } ); let server_flight = test_utils::emit_flight(&mut pipe.server).unwrap(); @@ -6429,7 +6469,7 @@ fn client_rst_stream_while_bytes_in_flight( // tx_buffered goes down to 0 after the reset and acks are // processed. A full cwnd's worth of packets can be sent. let expected_cwnd = match cc_algorithm_name { - "bbr2" | "bbr2_gcongestion" => 27756, + "bbr2" | "bbr2_gcongestion" => by_boring!(b4: 27756, b5: 30060), _ => 24000, }; @@ -6498,7 +6538,7 @@ fn client_rst_stream_while_bytes_in_flight_with_packet_loss( if cc_algorithm_name == "cubic" { Ok(12000) } else { - Ok(13878) + Ok(by_boring!(b4: 13878, b5: 15030)) } ); let mut server_flight = test_utils::emit_flight(&mut pipe.server).unwrap(); @@ -6518,7 +6558,7 @@ fn client_rst_stream_while_bytes_in_flight_with_packet_loss( // tx_buffered goes down to 0 after the reset and acks are // processed. A full cwnd's worth of packets can be sent. let expected_cwnd = match cc_algorithm_name { - "bbr2" | "bbr2_gcongestion" => 26556, + "bbr2" | "bbr2_gcongestion" => by_boring!(b4: 26556, b5: 28860), _ => 8400, }; @@ -6580,7 +6620,7 @@ fn sends_ack_only_pkt_when_full_cwnd_and_ack_elicited( if cc_algorithm_name == "cubic" { Ok(12000) } else { - Ok(12299) + Ok(by_boring!(b4: 12299, b5: 13587)) } ); @@ -6655,7 +6695,7 @@ fn sends_ack_only_pkt_when_full_cwnd_and_ack_elicited_despite_max_unacknowledgin if cc_algorithm_name == "cubic" { Ok(12000) } else { - Ok(12299) + Ok(by_boring!(b4: 12299, b5: 13587)) } ); @@ -6740,13 +6780,21 @@ fn validate_peer_sent_ack_range( let flight = test_utils::emit_flight(&mut pipe.client).unwrap(); test_utils::process_flight(&mut pipe.server, flight).unwrap(); - let expected_max_active_pkt_sent = 3; + // Expected pkt counts below reflect the post-handshake state. When the + // ClientHello spans multiple Initial packets (as with post-quantum + // keyshares, on boring 5) the handshake exchanges one extra packet + // per side compared to the classical (boring 4) case, which is why + // these counts are one higher under boring 5. + let expected_max_active_pkt_sent = by_boring!(b4: 3, b5: 4); let recovery = &pipe.server.paths.get_active().unwrap().recovery; assert_eq!( recovery.largest_sent_pkt_num_on_path(epoch).unwrap(), expected_max_active_pkt_sent ); - assert_eq!(recovery.get_largest_acked_on_epoch(epoch).unwrap(), 3); + assert_eq!( + recovery.get_largest_acked_on_epoch(epoch).unwrap(), + by_boring!(b4: 3, b5: 4) + ); assert_eq!(recovery.sent_packets_len(epoch), 0); // Verify largest sent on the connection assert_eq!( @@ -6765,8 +6813,14 @@ fn validate_peer_sent_ack_range( pipe.send_pkt_to_server(pkt_type, &frames, &mut buf) .unwrap(); let recovery = &pipe.server.paths.get_active().unwrap().recovery; - assert_eq!(recovery.largest_sent_pkt_num_on_path(epoch).unwrap(), 4); - assert_eq!(recovery.get_largest_acked_on_epoch(epoch).unwrap(), 3); + assert_eq!( + recovery.largest_sent_pkt_num_on_path(epoch).unwrap(), + by_boring!(b4: 4, b5: 5) + ); + assert_eq!( + recovery.get_largest_acked_on_epoch(epoch).unwrap(), + by_boring!(b4: 3, b5: 4) + ); assert_eq!(recovery.sent_packets_len(epoch), 1); // Send an invalid ACK range to the server and expect server error @@ -6825,26 +6879,34 @@ fn validate_peer_sent_ack_range_for_multi_path( let epoch = packet::Epoch::Application; let pkt_type = Type::Short; - // active path - let expected_max_active_pkt_sent = 7; + // active path. Pkt counts are one higher under boring 5 because the + // ClientHello spans two Initial packets (post-quantum keyshares); + // see `validate_peer_sent_ack_range` above for details. + let expected_max_active_pkt_sent = by_boring!(b4: 7, b5: 8); let active_path = &pipe.server.paths.get_mut(0).unwrap(); let p1_recovery = &active_path.recovery; assert_eq!( p1_recovery.largest_sent_pkt_num_on_path(epoch).unwrap(), expected_max_active_pkt_sent ); - assert_eq!(p1_recovery.get_largest_acked_on_epoch(epoch).unwrap(), 6); + assert_eq!( + p1_recovery.get_largest_acked_on_epoch(epoch).unwrap(), + by_boring!(b4: 6, b5: 7) + ); assert_eq!(p1_recovery.sent_packets_len(epoch), 1); // non-active path - let expected_max_second_pkt_sent = 5; + let expected_max_second_pkt_sent = by_boring!(b4: 5, b5: 6); let second_path = &pipe.server.paths.get_mut(probed_pid).unwrap(); let p2_recovery = &second_path.recovery; assert_eq!( p2_recovery.largest_sent_pkt_num_on_path(epoch).unwrap(), expected_max_second_pkt_sent ); - assert_eq!(p2_recovery.get_largest_acked_on_epoch(epoch).unwrap(), 5); + assert_eq!( + p2_recovery.get_largest_acked_on_epoch(epoch).unwrap(), + by_boring!(b4: 5, b5: 6) + ); assert_eq!(p2_recovery.sent_packets_len(epoch), 0); // Verify largest sent on the connection is the max of the two paths @@ -6872,15 +6934,27 @@ fn validate_peer_sent_ack_range_for_multi_path( let active_path = &pipe.server.paths.get_mut(0).unwrap(); assert!(active_path.active()); let p1_recovery = &active_path.recovery; - assert_eq!(p1_recovery.largest_sent_pkt_num_on_path(epoch).unwrap(), 7); - assert_eq!(p1_recovery.get_largest_acked_on_epoch(epoch).unwrap(), 7); + assert_eq!( + p1_recovery.largest_sent_pkt_num_on_path(epoch).unwrap(), + by_boring!(b4: 7, b5: 8) + ); + assert_eq!( + p1_recovery.get_largest_acked_on_epoch(epoch).unwrap(), + by_boring!(b4: 7, b5: 8) + ); assert_eq!(p1_recovery.sent_packets_len(epoch), 0); // non-active path let second_path = &pipe.server.paths.get_mut(probed_pid).unwrap(); let p2_recovery = &second_path.recovery; - assert_eq!(p2_recovery.largest_sent_pkt_num_on_path(epoch).unwrap(), 5); - assert_eq!(p2_recovery.get_largest_acked_on_epoch(epoch).unwrap(), 5); + assert_eq!( + p2_recovery.largest_sent_pkt_num_on_path(epoch).unwrap(), + by_boring!(b4: 5, b5: 6) + ); + assert_eq!( + p2_recovery.get_largest_acked_on_epoch(epoch).unwrap(), + by_boring!(b4: 5, b5: 6) + ); assert_eq!(p2_recovery.sent_packets_len(epoch), 0); // Send a large invalid ACK range to the server. Range is not inclusive so @@ -7963,7 +8037,11 @@ fn coalesce_padding_short( ) { let mut buf = [0; 65535]; - let mut pipe = test_utils::Pipe::new(cc_algorithm_name).unwrap(); + // Test asserts a specific coalesce/pad shape that assumes a + // single-Initial ClientHello. + let mut config = + test_utils::Pipe::default_config_no_pq(cc_algorithm_name).unwrap(); + let mut pipe = test_utils::Pipe::with_config(&mut config).unwrap(); // Client sends first flight. let (len, _) = pipe.client.send(&mut buf).unwrap(); @@ -7999,7 +8077,10 @@ fn handshake_anti_deadlock( ) { let mut buf = [0; 65535]; - let mut config = Config::new(PROTOCOL_VERSION).unwrap(); + // Test drives the handshake one packet at a time and expects the + // server to have handshake keys after a single `server_recv`; that + // requires a single-Initial ClientHello. + let mut config = test_utils::config_no_pq(PROTOCOL_VERSION).unwrap(); assert_eq!(config.set_cc_algorithm_name(cc_algorithm_name), Ok(())); config .load_cert_chain_from_pem_file("examples/cert-big.crt") @@ -8011,7 +8092,7 @@ fn handshake_anti_deadlock( .set_application_protos(&[b"proto1", b"proto2"]) .unwrap(); - let mut pipe = test_utils::Pipe::with_server_config(&mut config).unwrap(); + let mut pipe = test_utils::Pipe::with_config(&mut config).unwrap(); assert!(!pipe.client.handshake_status().has_handshake_keys); assert!(!pipe.client.handshake_status().peer_verified_address); @@ -8054,7 +8135,11 @@ fn handshake_packet_type_corruption( ) { let mut buf = [0; 65535]; - let mut pipe = test_utils::Pipe::new(cc_algorithm_name).unwrap(); + // Test drives the handshake one packet at a time, which assumes a + // single-Initial ClientHello. + let mut config = + test_utils::Pipe::default_config_no_pq(cc_algorithm_name).unwrap(); + let mut pipe = test_utils::Pipe::with_config(&mut config).unwrap(); // Client sends padded Initial. let (len, _) = pipe.client.send(&mut buf).unwrap(); @@ -9018,7 +9103,7 @@ fn update_max_datagram_size( if cc_algorithm_name == "cubic" { 12000 } else { - 13421 + by_boring!(b4: 13421, b5: 14573) }, ); } @@ -9093,18 +9178,22 @@ fn send_capacity( if cc_algorithm_name == "cubic" { 12000 } else { - 13873 + by_boring!(b4: 13873, b5: 15025) } ); assert_eq!(pipe.server.stream_send(0, &buf[..5000], false), Ok(5000)); assert_eq!(pipe.server.stream_send(4, &buf[..5000], false), Ok(5000)); + // Offer enough bytes on the third stream that the connection-level + // `tx_cap` is the binding constraint rather than the input buffer; + // this keeps the test invariant "no connection send capacity left after + // three sends" true across backends with different handshake sizes. assert_eq!( - pipe.server.stream_send(8, &buf[..5000], false), + pipe.server.stream_send(8, &buf[..6000], false), if cc_algorithm_name == "cubic" { Ok(2000) } else { - Ok(3873) + Ok(by_boring!(b4: 3873, b5: 5025)) } ); @@ -9222,7 +9311,9 @@ fn in_handshake_config( Ok(()) ); - let mut client_config = Config::new(PROTOCOL_VERSION)?; + // Test drives the handshake one packet at a time; requires a + // single-Initial ClientHello. + let mut client_config = test_utils::config_no_pq(PROTOCOL_VERSION)?; client_config.load_cert_chain_from_pem_file("examples/cert.crt")?; client_config.load_priv_key_from_pem_file("examples/cert.key")?; @@ -9309,8 +9400,10 @@ fn max_streams_threshold_after_handshake_callback_update( ) .unwrap(); + // Test drives the handshake one packet at a time; requires a + // single-Initial ClientHello. let mut client_config = - test_utils::Pipe::default_config(cc_algorithm_name).unwrap(); + test_utils::Pipe::default_config_no_pq(cc_algorithm_name).unwrap(); for config in [&mut client_config, &mut server_config] { config @@ -9614,9 +9707,25 @@ fn initial_cwnd( CUSTOM_INITIAL_CONGESTION_WINDOW_PACKETS * 1200 ); } else { - // TODO understand where these adjustments come from and why they vary - // by OS target. - let expected = CUSTOM_INITIAL_CONGESTION_WINDOW_PACKETS * 1200 + 1447; + // For BBR2 in Startup mode the cwnd grows by exactly + // `bytes_acked` per ACK (see `BBRv2::update_congestion_window`), + // so `tx_cap` here equals `initial_cwnd` plus the bytes the + // server sent during the handshake that have already been + // acknowledged. That total varies a byte or two across architectures, + // primarily because the ACK frame's `ack_delay` field is a VarInt + // of microseconds since receipt and the elapsed time differs by a + // tick or two between platforms. + // + // Pin the lower bound (catches gross regressions like initial + // cwnd not being honored) and allow a small upper-bound + // tolerance, well below a packet so any meaningful regression + // would still trip the assertion. + // Handshake size (and hence the extra acked bytes on top of + // `initial_cwnd`) is larger under boring 5 because the + // ClientHello carries a post-quantum key share by default. + let expected = CUSTOM_INITIAL_CONGESTION_WINDOW_PACKETS * 1200 + + by_boring!(b4: 1447, b5: 2598); + const TOLERANCE: usize = 4; assert!( pipe.server.tx_cap >= expected, @@ -9625,10 +9734,10 @@ fn initial_cwnd( expected ); assert!( - pipe.server.tx_cap <= expected + 1, + pipe.server.tx_cap <= expected + TOLERANCE, "{} vs {}", pipe.server.tx_cap, - expected + 1 + expected + TOLERANCE ); } @@ -11368,8 +11477,7 @@ fn resilience_against_migration_attack( let mut recv_buf = [0; DATA_BYTES]; let send1_bytes = pipe.server.stream_send(1, &buf, true).unwrap(); assert_eq!(send1_bytes, match cc_algorithm_name { - "bbr2" => 13880, - "bbr2_gcongestion" => 13880, + "bbr2" | "bbr2_gcongestion" => by_boring!(b4: 13880, b5: 15032), _ => 12000, }); assert_eq!( diff --git a/quiche/src/tls/boringssl.rs b/quiche/src/tls/boringssl.rs index 4dc3b552855..f2e4d8681f1 100644 --- a/quiche/src/tls/boringssl.rs +++ b/quiche/src/tls/boringssl.rs @@ -310,6 +310,13 @@ extern "C" { ) -> c_int; fn SSL_CTX_set_early_data_enabled(ctx: *mut SSL_CTX, enabled: i32); + // BoringSSL exports `SSL_CTX_set1_groups_list` as a real symbol; on + // OpenSSL it is a header macro. See `openssl_quictls.rs` for the + // OpenSSL shim. + pub(super) fn SSL_CTX_set1_groups_list( + ctx: *mut SSL_CTX, groups: *const c_char, + ) -> c_int; + pub(super) fn SSL_CTX_set_session_cache_mode( ctx: *mut SSL_CTX, mode: c_int, ) -> c_int; diff --git a/quiche/src/tls/mod.rs b/quiche/src/tls/mod.rs index 4fb4bd4c8f0..0c600a1bbab 100644 --- a/quiche/src/tls/mod.rs +++ b/quiche/src/tls/mod.rs @@ -328,6 +328,18 @@ impl Context { }) } + pub fn set_curves_list(&mut self, curves: &str) -> Result<()> { + // Note: BoringSSL exports `SSL_CTX_set1_groups_list` as a real + // function; OpenSSL (and openssl-quictls) defines it as a macro + // that expands to `SSL_CTX_ctrl`. Each backend provides a + // `SSL_CTX_set1_groups_list` shim in the per-vendor module so this + // call site can be backend-agnostic. + let cstr = ffi::CString::new(curves).map_err(|_| Error::TlsFail)?; + map_result(unsafe { + SSL_CTX_set1_groups_list(self.as_mut_ptr(), cstr.as_ptr()) + }) + } + fn as_mut_ptr(&mut self) -> *mut SSL_CTX { self.0 } diff --git a/tokio-quiche/Cargo.toml b/tokio-quiche/Cargo.toml index bf119ca3398..c4278fc32b4 100644 --- a/tokio-quiche/Cargo.toml +++ b/tokio-quiche/Cargo.toml @@ -9,6 +9,7 @@ keywords = ["quic", "http3", "tokio"] categories = { workspace = true } readme = "README.md" rust-version = "1.88" +build = "build.rs" [features] default = ["qlog-gzip", "qlog-zstd"] diff --git a/tokio-quiche/build.rs b/tokio-quiche/build.rs new file mode 100644 index 00000000000..10e9df67037 --- /dev/null +++ b/tokio-quiche/build.rs @@ -0,0 +1,67 @@ +fn main() { + // Emit `cfg(boring_v4)` if boring version 4.x is detected. This is used to + // pick which APIs to expect and to guide test expectations. (Larger post + // quantum key shares are enabled by default in boring 5.x but not boring + // 4.x.) Mirrors `quiche/src/build.rs`. + println!("cargo::rustc-check-cfg=cfg(boring_v4)"); + if detect_boring_v4() { + println!("cargo:rustc-cfg=boring_v4"); + } +} + +/// Returns true if cargo resolved `boring` to a 4.x version. +fn detect_boring_v4() -> bool { + let Some(lockfile) = find_cargo_lock() else { + println!( + "cargo:warning=tokio-quiche: Cargo.lock not found; assuming boring 5.x" + ); + return false; + }; + + println!("cargo:rerun-if-changed={}", lockfile.display()); + + let contents = match std::fs::read_to_string(&lockfile) { + Ok(s) => s, + Err(e) => { + println!( + "cargo:warning=tokio-quiche: failed to read {}: {e}; assuming boring 5.x", + lockfile.display(), + ); + return false; + }, + }; + + let mut in_boring = false; + for line in contents.lines() { + let line = line.trim(); + if line == "[[package]]" { + in_boring = false; + continue; + } + if line == "name = \"boring\"" { + in_boring = true; + continue; + } + if in_boring { + if let Some(rest) = line.strip_prefix("version = \"") { + let version = rest.trim_end_matches('"'); + let major = version.split('.').next().unwrap_or(""); + return major == "4"; + } + } + } + + false +} + +fn find_cargo_lock() -> Option { + let manifest_dir = + std::path::PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR")?); + for dir in manifest_dir.ancestors() { + let candidate = dir.join("Cargo.lock"); + if candidate.is_file() { + return Some(candidate); + } + } + None +} diff --git a/tokio-quiche/src/settings/config.rs b/tokio-quiche/src/settings/config.rs index a3a6e1436ee..5b9ab8d957c 100644 --- a/tokio-quiche/src/settings/config.rs +++ b/tokio-quiche/src/settings/config.rs @@ -238,8 +238,12 @@ fn quiche_config_with_tls( // TODO: don't compile this enum variant unless rpk feature is enabled panic!("Can't use RPK when compiled without rpk feature"); }, - #[cfg(feature = "rpk")] + #[cfg(all(feature = "rpk", boring_v4))] CertificateKind::RawPublicKey => { + // boring 4.x exposes a dedicated `SslContextBuilder::new_rpk()` + // constructor plus `set_rpk_certificate` / + // `set_null_chain_private_key`. This arm goes away when + // `cfg(boring_v4)` is retired. let mut ssl_ctx_builder = boring::ssl::SslContextBuilder::new_rpk()?; let raw_public_key = read_file(tls.cert)?; ssl_ctx_builder.set_rpk_certificate(&raw_public_key)?; @@ -254,6 +258,33 @@ fn quiche_config_with_tls( ssl_ctx_builder, )?) }, + #[cfg(all(feature = "rpk", not(boring_v4)))] + CertificateKind::RawPublicKey => { + // boring 5.x replaced the dedicated `SslContextBuilder::new_rpk()` + // entry point with a credential-based API: build an + // `SslCredential` configured for raw public keys and add it + // to a regular `SslContextBuilder` via `add_credential`. + let raw_public_key = read_file(tls.cert)?; + let raw_private_key = read_file(tls.private_key)?; + let pkey = + boring::pkey::PKey::private_key_from_pem(&raw_private_key)?; + + let mut credential_builder = + boring::ssl::SslCredential::new_raw_public_key()?; + credential_builder.set_spki_bytes(Some(&raw_public_key))?; + credential_builder.set_private_key(&pkey)?; + let credential = credential_builder.build(); + + let mut ssl_ctx_builder = boring::ssl::SslContextBuilder::new( + boring::ssl::SslMethod::tls(), + )?; + ssl_ctx_builder.add_credential(&credential)?; + + Ok(quiche::Config::with_boring_ssl_ctx_builder( + quiche::PROTOCOL_VERSION, + ssl_ctx_builder, + )?) + }, CertificateKind::X509 => { let mut config = quiche::Config::new(quiche::PROTOCOL_VERSION).unwrap();