Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package s2n for openSUSE:Factory checked in at 2026-09-19 22:20:15 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/s2n (Old) and /work/SRC/openSUSE:Factory/.s2n.new.383539 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "s2n" Sat Sep 19 22:20:15 2026 rev:44 rq:1378851 version:1.7.10 Changes: -------- --- /work/SRC/openSUSE:Factory/s2n/s2n.changes 2026-09-11 18:07:49.591146526 +0200 +++ /work/SRC/openSUSE:Factory/.s2n.new.383539/s2n.changes 2026-09-19 22:21:07.883576575 +0200 @@ -1,0 +2,15 @@ +Thu Sep 17 14:04:22 UTC 2026 - John Paul Adrian Glaubitz <[email protected]> + +- Update to version 1.7.10 + * feat(bindings): add signature_public_key_type to Connection (#6076) + * fix: bounds-check cert pkey type before array indexing (#6054) + * fix(integration): replace BoringSSL fork git dependency with btls crate (#6067) + * feat(s2n-tls-metrics): Add hello_retry_request_count (#6078) + * fix: always free async offload op during connection teardown (#6080) + * fix: enforce JA4 list limit when building fingerprint (#6047) + * fix: zero-initialize s2n_tls13_keys in PSK binder functions (#6048) + * fix: free encoded point on write failure in s2n_ecc_evp_write_params_point (#5964) + * fix: free client pub_key on all error paths in server shared secret (#6063) + * feat(metrics-subscriber): expose compatibility profile allow-lists (#6086) + +------------------------------------------------------------------- Old: ---- v1.7.9.tar.gz New: ---- v1.7.10.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ s2n.spec ++++++ --- /var/tmp/diff_new_pack.KyqWDq/_old 2026-09-19 22:21:12.195756438 +0200 +++ /var/tmp/diff_new_pack.KyqWDq/_new 2026-09-19 22:21:12.197756522 +0200 @@ -19,7 +19,7 @@ %define library_version 1.0.0 %define library_soversion 1 Name: s2n -Version: 1.7.9 +Version: 1.7.10 Release: 0 Summary: AWS implementation of the TLS/SSL protocols License: Apache-2.0 ++++++ v1.7.9.tar.gz -> v1.7.10.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/s2n-tls-1.7.9/.github/bin/check_boringssl_fork_freshness.sh new/s2n-tls-1.7.10/.github/bin/check_boringssl_fork_freshness.sh --- old/s2n-tls-1.7.9/.github/bin/check_boringssl_fork_freshness.sh 2026-09-01 22:54:11.000000000 +0200 +++ new/s2n-tls-1.7.10/.github/bin/check_boringssl_fork_freshness.sh 1970-01-01 01:00:00.000000000 +0100 @@ -1,50 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# The script is configurable via environment variablesß -FORK_REPO="${FORK_REPO:-kaukabrizvi/boring}" # owner/repo -FORK_BRANCH="${FORK_BRANCH:-symbol-prefixing}" # branch to check -THRESHOLD_DAYS="${THRESHOLD_DAYS:-60}" # ~2 months - -if ! command -v jq >/dev/null 2>&1; then - echo "ERROR: jq is required for the BoringSSL fork freshness check." - exit 2 -fi - -commit_api="https://api.github.com/repos/${FORK_REPO}/commits/${FORK_BRANCH}" -json="$(curl -fsSL "${commit_api}")" - -last_commit_date="$(echo "${json}" | jq -r '.commit.committer.date')" -last_commit_sha="$(echo "${json}" | jq -r '.sha')" - -if [[ -z "${last_commit_date}" || "${last_commit_date}" == "null" ]]; then - echo "ERROR: Could not determine last commit date for ${FORK_REPO}@${FORK_BRANCH} from GitHub API." - exit 2 -fi - -now_epoch="$(date -u +%s)" -commit_epoch="$(date -u -d "${last_commit_date}" +%s)" -age_days="$(( (now_epoch - commit_epoch) / 86400 ))" - -if (( age_days > THRESHOLD_DAYS )); then - cat <<EOF -ERROR: BoringSSL fork branch appears stale. - - Fork: ${FORK_REPO} - Branch: ${FORK_BRANCH} - Last commit: ${last_commit_sha} - Last commit date:${last_commit_date} (~${age_days} days ago) - Threshold: ${THRESHOLD_DAYS} days - -Why this is failing: - s2n-tls currently depends on a forked BoringSSL branch (for symbol prefixing) to avoid - OpenSSL symbol collisions in integration tests. A stale fork risks outdated testing. - -Refresh [email protected]:${FORK_REPO}/tree/${FORK_BRANCH} by syncing it with the upstream repository, -resolving merge conflicts if they appear. This should be done periodically and has not been done in -~${age_days} days. -EOF - exit 1 -fi - -echo "OK: ${FORK_REPO}@${FORK_BRANCH} last commit ${last_commit_sha} at ${last_commit_date} (~${age_days} days ago) within threshold ${THRESHOLD_DAYS}." diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/s2n-tls-1.7.9/.github/workflows/boringssl-fork-freshness.yml new/s2n-tls-1.7.10/.github/workflows/boringssl-fork-freshness.yml --- old/s2n-tls-1.7.9/.github/workflows/boringssl-fork-freshness.yml 2026-09-01 22:54:11.000000000 +0200 +++ new/s2n-tls-1.7.10/.github/workflows/boringssl-fork-freshness.yml 1970-01-01 01:00:00.000000000 +0100 @@ -1,20 +0,0 @@ -name: BoringSSL fork freshness - -on: - pull_request: - -jobs: - freshness: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - - name: Install jq - run: sudo apt-get update && sudo apt-get install -y jq - - - name: Check BoringSSL fork freshness (time-bomb) - env: - FORK_REPO: kaukabrizvi/boring - FORK_BRANCH: symbol-prefixing - THRESHOLD_DAYS: "60" - run: .github/bin/check_boringssl_fork_freshness.sh diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/s2n-tls-1.7.9/api/unstable/async_offload.h new/s2n-tls-1.7.10/api/unstable/async_offload.h --- old/s2n-tls-1.7.9/api/unstable/async_offload.h 2026-09-01 22:54:11.000000000 +0200 +++ new/s2n-tls-1.7.10/api/unstable/async_offload.h 2026-09-11 02:02:31.000000000 +0200 @@ -87,7 +87,13 @@ * Retrying s2n_negotiate() will produce the same result until s2n_async_offload_op_perform() is completed. * * s2n_async_offload_op_perform() can only be called once for each triggered operation. - * + * + * The `op` is owned by s2n-tls and is freed together with its s2n_connection. The + * application MUST ensure any thread executing s2n_async_offload_op_perform() for a + * connection has finished before that connection is freed (s2n_connection_free) or + * reused (s2n_connection_wipe). s2n-tls does not synchronize with the perform thread, + * so freeing or wiping a connection during a perform call may cause a use-after-free. + * * @param op An opaque object representing the async operation */ S2N_API extern int s2n_async_offload_op_perform(struct s2n_async_offload_op *op); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/s2n-tls-1.7.9/bindings/rust/extended/s2n-tls/src/connection.rs new/s2n-tls-1.7.10/bindings/rust/extended/s2n-tls/src/connection.rs --- old/s2n-tls-1.7.9/bindings/rust/extended/s2n-tls/src/connection.rs 2026-09-01 22:54:11.000000000 +0200 +++ new/s2n-tls-1.7.10/bindings/rust/extended/s2n-tls/src/connection.rs 2026-09-11 02:02:31.000000000 +0200 @@ -1261,6 +1261,43 @@ } } + /// Returns a string describing the public key algorithm and parameters of + /// the leaf certificate for the given connection `mode`. + /// + /// Use [`Mode::Server`] to query the server certificate and [`Mode::Client`] + /// to query the client certificate. The returned string has one of the + /// following forms: + /// - RSA: `"rsa2048"`, `"rsa3072"`, `"rsa4096"`, or `"rsa<keysize>"` + /// - ECDSA: `"ecdsa_secp256r1"`, `"ecdsa_secp384r1"`, `"ecdsa_secp521r1"` + /// - ML-DSA: `"mldsa44"`, `"mldsa65"`, `"mldsa87"` + /// + /// Querying the peer's certificate ([`Mode::Client`] on a server, or + /// [`Mode::Server`] on a client) requires that the peer's certificate chain + /// has been validated during the handshake. + /// + /// Corresponds to [`s2n_conn_get_signature_public_key_type`]. + pub fn signature_public_key_type(&self, mode: Mode) -> Result<String, Error> { + // The API writes a null-terminated string into a caller-provided + // buffer. 32 bytes comfortably fits every defined output (the longest + // is "ecdsa_secp256r1" at 16 bytes including the null terminator); a + // larger output would surface as S2N_ERR_INSUFFICIENT_MEM_SIZE. + let mut buffer = [0u8; 32]; + let mut output_size = buffer.len() as u32; + unsafe { + s2n_conn_get_signature_public_key_type( + self.connection.as_ptr(), + mode.into(), + buffer.as_mut_ptr() as *mut std::ffi::c_char, + &mut output_size, + ) + .into_result()?; + } + + // `output_size` includes the null terminator; drop it before decoding. + let len = (output_size as usize).saturating_sub(1); + String::from_utf8(buffer[..len].to_vec()).map_err(|_| Error::INVALID_INPUT) + } + /// Corresponds to [`s2n_connection_get_selected_digest_algorithm`]. pub fn selected_hash_algorithm(&self) -> Result<HashAlgorithm, Error> { let mut hash_alg = s2n_tls_hash_algorithm::NONE; @@ -1973,6 +2010,67 @@ Ok(()) } + #[test] + fn signature_public_key_type_rsa() -> Result<(), Box<dyn std::error::Error>> { + // The default test cert is a 4096-bit RSA cert. + let config = build_config(&security::DEFAULT_TLS13)?; + let mut pair = TestPair::from_config(&config); + pair.handshake()?; + + // The server's own certificate and the client's view of the peer + // (server) certificate should both report "rsa4096". + assert_eq!( + pair.server.signature_public_key_type(Mode::Server)?, + "rsa4096" + ); + assert_eq!( + pair.client.signature_public_key_type(Mode::Server)?, + "rsa4096" + ); + + // No client auth was configured, so there is no validated client + // certificate to describe. + let err = pair + .server + .signature_public_key_type(Mode::Client) + .unwrap_err(); + assert_eq!(err.name(), "S2N_ERR_CERT_NOT_VALIDATED"); + + Ok(()) + } + + #[test] + fn signature_public_key_type_ecdsa() -> Result<(), Box<dyn std::error::Error>> { + // Build a config that serves an ECDSA (secp256r1) certificate so we + // exercise a non-RSA output and the longest string variant. + let keypair = SniTestCerts::AlligatorEcdsa.get(); + let config = { + let mut builder = crate::config::Builder::new(); + builder.set_security_policy(&security::DEFAULT_TLS13)?; + builder.load_pem(keypair.cert(), keypair.key())?; + builder.set_verify_host_callback( + crate::testing::InsecureAcceptAllCertificatesHandler {}, + )?; + builder.with_system_certs(false)?; + builder.trust_pem(keypair.cert())?; + builder.build()? + }; + + let mut pair = TestPair::from_config(&config); + pair.handshake()?; + + assert_eq!( + pair.server.signature_public_key_type(Mode::Server)?, + "ecdsa_secp256r1" + ); + assert_eq!( + pair.client.signature_public_key_type(Mode::Server)?, + "ecdsa_secp256r1" + ); + + Ok(()) + } + /// Reads the TLS record payload length from a record header in the buffer. /// /// TLS record header format: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/s2n-tls-1.7.9/bindings/rust/standard/integration/Cargo.toml new/s2n-tls-1.7.10/bindings/rust/standard/integration/Cargo.toml --- old/s2n-tls-1.7.9/bindings/rust/standard/integration/Cargo.toml 2026-09-01 22:54:11.000000000 +0200 +++ new/s2n-tls-1.7.10/bindings/rust/standard/integration/Cargo.toml 2026-09-11 02:02:31.000000000 +0200 @@ -58,10 +58,13 @@ # This should be the latest version of metrics subscriber available on crates.io old-metrics-subscriber = { package = "s2n-tls-metrics-subscriber", version = "0.0.5" } +# BoringSSL is provided by the `btls` crate; see the note in +# tls-harness/Cargo.toml for why it is used instead of `boring`. +# # NOTE: BoringSSL is disabled on macOS to avoid symbol collisions with # OpenSSL (see https://github.com/aws/s2n-tls/pull/5659), and on Windows because BoringSSL can't be built under the MSYS2/MinGW toolchain. -[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dev-dependencies.boring] -git = "https://github.com/kaukabrizvi/boring.git" +[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dev-dependencies.btls] +version = "0.5.6" features = ["prefix-symbols"] [build-dependencies] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/s2n-tls-1.7.9/bindings/rust/standard/integration/src/mtls/mod.rs new/s2n-tls-1.7.10/bindings/rust/standard/integration/src/mtls/mod.rs --- old/s2n-tls-1.7.9/bindings/rust/standard/integration/src/mtls/mod.rs 2026-09-01 22:54:11.000000000 +0200 +++ new/s2n-tls-1.7.10/bindings/rust/standard/integration/src/mtls/mod.rs 2026-09-11 02:02:31.000000000 +0200 @@ -28,7 +28,7 @@ // NOTE: BoringSSL tests are disabled on macOS to avoid symbol collisions with // OpenSSL; see https://github.com/aws/s2n-tls/pull/5659 for details. #[cfg(all(feature = "boringssl", not(target_os = "macos")))] -use boring::ssl::SslVersion; +use btls::ssl::SslVersion; use rustls::ClientConfig; use s2n_tls::{ @@ -221,7 +221,7 @@ fn boringssl_mtls_client(sig_type: SigType, version: SslVersion) -> BoringSslConfig { use tls_harness::harness::{Mode, TlsConfigBuilder}; - let mut builder = boring::ssl::SslContextBuilder::new_test_config(Mode::Client); + let mut builder = btls::ssl::SslContextBuilder::new_test_config(Mode::Client); builder.set_trust(sig_type); builder @@ -233,10 +233,10 @@ builder .set_private_key_file( tls_harness::get_cert_path(PemType::ClientKey, sig_type), - boring::ssl::SslFiletype::PEM, + btls::ssl::SslFiletype::PEM, ) .unwrap(); - builder.set_verify(boring::ssl::SslVerifyMode::PEER); + builder.set_verify(btls::ssl::SslVerifyMode::PEER); // Pin the protocol version builder.set_min_proto_version(Some(version)).unwrap(); @@ -252,11 +252,11 @@ fn boringssl_mtls_server(sig_type: SigType, version: SslVersion) -> BoringSslConfig { use tls_harness::harness::{Mode, TlsConfigBuilder}; - let mut builder = boring::ssl::SslContextBuilder::new_test_config(Mode::Server); + let mut builder = btls::ssl::SslContextBuilder::new_test_config(Mode::Server); builder.set_chain(sig_type); builder.set_trust(sig_type); builder.set_verify( - boring::ssl::SslVerifyMode::PEER | boring::ssl::SslVerifyMode::FAIL_IF_NO_PEER_CERT, + btls::ssl::SslVerifyMode::PEER | btls::ssl::SslVerifyMode::FAIL_IF_NO_PEER_CERT, ); // Pin the protocol version diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/s2n-tls-1.7.9/bindings/rust/standard/s2n-tls-metrics-schema/src/metric_names.rs new/s2n-tls-1.7.10/bindings/rust/standard/s2n-tls-metrics-schema/src/metric_names.rs --- old/s2n-tls-1.7.9/bindings/rust/standard/s2n-tls-metrics-schema/src/metric_names.rs 2026-09-01 22:54:11.000000000 +0200 +++ new/s2n-tls-1.7.10/bindings/rust/standard/s2n-tls-metrics-schema/src/metric_names.rs 2026-09-11 02:02:31.000000000 +0200 @@ -88,6 +88,7 @@ } pub const SSLV2_CLIENT_HELLO: &str = "sslv2_client_hello"; +pub const HELLO_RETRY_REQUEST_COUNT: &str = "hello_retry_request_count"; pub const HANDSHAKE_DURATION_US: &str = "handshake_duration_us"; pub const HANDSHAKE_COMPUTE_US: &str = "handshake_compute_us"; pub const SYNTHETIC_TRAFFIC_COUNT: &str = "synthetic_traffic_count"; @@ -103,6 +104,7 @@ COMPATIBILITY_NEGOTIATED_CNSA1, COMPATIBILITY_NEGOTIATED_CNSA2, SSLV2_CLIENT_HELLO, + HELLO_RETRY_REQUEST_COUNT, HANDSHAKE_SUCCESS_COUNT, HANDSHAKE_FAILURE_COUNT, HANDSHAKE_DURATION_US, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/s2n-tls-1.7.9/bindings/rust/standard/s2n-tls-metrics-schema/src/record.rs new/s2n-tls-1.7.10/bindings/rust/standard/s2n-tls-metrics-schema/src/record.rs --- old/s2n-tls-1.7.9/bindings/rust/standard/s2n-tls-metrics-schema/src/record.rs 2026-09-01 22:54:11.000000000 +0200 +++ new/s2n-tls-1.7.10/bindings/rust/standard/s2n-tls-metrics-schema/src/record.rs 2026-09-11 02:02:31.000000000 +0200 @@ -78,6 +78,12 @@ #[serde(default)] pub sslv2_client_hello: u64, + + /// Number of TLS 1.3 handshakes that required a HelloRetryRequest, and + /// therefore an additional round trip. + #[serde(default)] + pub hello_retry_request_count: u64, + #[serde(default)] pub supported_protocols: FrozenCounter<PROTOCOL_COUNT, Version>, #[serde(default)] @@ -155,6 +161,7 @@ negotiated_groups: FrozenCounter::default(), negotiated_signatures: FrozenCounter::default(), sslv2_client_hello: 0, + hello_retry_request_count: 0, supported_protocols: FrozenCounter::default(), supported_ciphers: FrozenCounter::default(), supported_groups: FrozenCounter::default(), @@ -303,6 +310,10 @@ writer.value(names::SSLV2_CLIENT_HELLO, &self.sslv2_client_hello); writer.value( + names::HELLO_RETRY_REQUEST_COUNT, + &self.hello_retry_request_count, + ); + writer.value( names::HANDSHAKE_SUCCESS_COUNT, &self.handshake_success_count, ); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/s2n-tls-1.7.9/bindings/rust/standard/s2n-tls-metrics-subscriber/src/compatibility.rs new/s2n-tls-1.7.10/bindings/rust/standard/s2n-tls-metrics-subscriber/src/compatibility.rs --- old/s2n-tls-1.7.9/bindings/rust/standard/s2n-tls-metrics-subscriber/src/compatibility.rs 2026-09-01 22:54:11.000000000 +0200 +++ new/s2n-tls-1.7.10/bindings/rust/standard/s2n-tls-metrics-subscriber/src/compatibility.rs 2026-09-11 02:02:31.000000000 +0200 @@ -7,6 +7,48 @@ use crate::{parsing::ClientHelloSupportedParameters, record::NegotiatedParameters}; use s2n_tls_metrics_schema::static_lists::{Cipher, Group, Signature, Version}; +/// A read-only view of a single compatibility profile's allow-lists. +/// +/// This exposes only the static allow-list data for a profile; it does not +/// expose any handshake-evaluation behavior. +#[derive(Debug, Clone, Copy)] +pub struct TlsProfileSpec { + pub allowed_versions: &'static [Version], + pub allowed_ciphers: &'static [Cipher], + pub allowed_groups: &'static [Group], + pub allowed_signatures: &'static [Signature], +} + +/// Stable identifiers for the TLS compatibility profiles measured by this crate. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum CompatibilityProfile { + General20251201, + Fips20251201, + Cnsa1, + Cnsa2, +} + +impl CompatibilityProfile { + /// Returns the read-only allow-list [`TlsProfileSpec`] for this profile. + pub fn spec(self) -> TlsProfileSpec { + fn spec_of<P: TlsProfile>() -> TlsProfileSpec { + TlsProfileSpec { + allowed_versions: P::ALLOWED_VERSIONS, + allowed_ciphers: P::ALLOWED_CIPHERS, + allowed_groups: P::ALLOWED_GROUPS, + allowed_signatures: P::ALLOWED_SIGNATURES, + } + } + + match self { + CompatibilityProfile::General20251201 => spec_of::<General20251201>(), + CompatibilityProfile::Fips20251201 => spec_of::<Fips20251201>(), + CompatibilityProfile::Cnsa1 => spec_of::<Cnsa1>(), + CompatibilityProfile::Cnsa2 => spec_of::<Cnsa2>(), + } + } +} + pub(crate) trait TlsProfile { const ALLOWED_VERSIONS: &[Version]; const ALLOWED_CIPHERS: &[Cipher]; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/s2n-tls-1.7.9/bindings/rust/standard/s2n-tls-metrics-subscriber/src/lib.rs new/s2n-tls-1.7.10/bindings/rust/standard/s2n-tls-metrics-subscriber/src/lib.rs --- old/s2n-tls-1.7.9/bindings/rust/standard/s2n-tls-metrics-subscriber/src/lib.rs 2026-09-01 22:54:11.000000000 +0200 +++ new/s2n-tls-1.7.10/bindings/rust/standard/s2n-tls-metrics-subscriber/src/lib.rs 2026-09-11 02:02:31.000000000 +0200 @@ -3,7 +3,7 @@ mod bounded_set; mod client_issue; -mod compatibility; +pub mod compatibility; pub(crate) mod counter; pub mod detector; #[cfg(feature = "fuzzing")] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/s2n-tls-1.7.9/bindings/rust/standard/s2n-tls-metrics-subscriber/src/record.rs new/s2n-tls-1.7.10/bindings/rust/standard/s2n-tls-metrics-subscriber/src/record.rs --- old/s2n-tls-1.7.9/bindings/rust/standard/s2n-tls-metrics-subscriber/src/record.rs 2026-09-01 22:54:11.000000000 +0200 +++ new/s2n-tls-1.7.10/bindings/rust/standard/s2n-tls-metrics-subscriber/src/record.rs 2026-09-11 02:02:31.000000000 +0200 @@ -101,6 +101,12 @@ // we do not attempt to detect supported parameters for SSLv2 formatted client // hellos sslv2_client_hello: AtomicU64, + + /// TLS 1.3 handshakes that required a HelloRetryRequest, and therefore an + /// additional round trip. This is detected from the handshake type, which is + /// available on both the client and server side of a connection. + hello_retry_request_count: AtomicU64, + supported_protocols: Counter<PROTOCOL_COUNT, Version>, supported_ciphers: Counter<CIPHER_COUNT, Cipher>, supported_groups: Counter<GROUP_COUNT, Group>, @@ -169,6 +175,7 @@ negotiated_signatures: Counter::new(), sslv2_client_hello: Default::default(), + hello_retry_request_count: Default::default(), supported_groups: Counter::new(), supported_ciphers: Counter::new(), supported_protocols: Counter::new(), @@ -285,6 +292,22 @@ self.negotiated_signatures.increment(&sig); } + // An HRR costs an additional round trip, so it's tracked separately from + // the negotiated parameters that it was used to negotiate. The + // HELLO_RETRY_REQUEST flag only appears in the TLS 1.3 handshake type + // labels, so it can't be confused with a TLS 1.2 flag. + match conn.handshake_type() { + Ok(handshake_type) => { + if handshake_type.contains("HELLO_RETRY_REQUEST") { + self.hello_retry_request_count + .fetch_add(1, Ordering::Relaxed); + } + } + // Not treated as an internal failure: the rest of the record is + // still accurate, so continue recording telemetry. + Err(e) => tracing::error!("failed to retrieve handshake type: {e}"), + } + // Compatibility based on the negotiated parameters. Unlike the client // hello based `compatibility_*` counters below, negotiated parameters // are available on both the client and server side, so these counters @@ -488,6 +511,7 @@ negotiated_signatures: self.negotiated_signatures.freeze(), sslv2_client_hello: self.sslv2_client_hello.load(Ordering::Relaxed), + hello_retry_request_count: self.hello_retry_request_count.load(Ordering::Relaxed), supported_protocols: self.supported_protocols.freeze(), supported_ciphers: self.supported_ciphers.freeze(), supported_groups: self.supported_groups.freeze(), @@ -545,7 +569,9 @@ #[cfg(test)] mod tests { - use crate::test_utils::{ARBITRARY_POLICY_1, TestEndpoint}; + use crate::test_utils::{ + ARBITRARY_POLICY_1, P256_PREFERRING_POLICY, STRONGLY_PREFERRED_GROUPS_POLICY, TestEndpoint, + }; use s2n_tls_metrics_schema::{ counter::FrozenCounter, static_lists::{Cipher, FiniteCounter}, @@ -553,6 +579,33 @@ use super::*; + /// A handshake that requires a HelloRetryRequest increments + /// `hello_retry_request_count`, and one that doesn't leaves it at zero. + #[test] + fn record_contents_hello_retry_request() { + // the server strongly prefers secp384r1, but the client key shares + // secp256r1, so the server sends an HRR. + let hrr_endpoint = TestEndpoint::with_server_policy(&STRONGLY_PREFERRED_GROUPS_POLICY); + hrr_endpoint.client_handshake(&P256_PREFERRING_POLICY); + hrr_endpoint.subscriber.finish_record(); + + let records = hrr_endpoint.sink.records.lock().unwrap(); + let record = &records[0].as_schema().handshake; + assert_eq!(record.handshake_success_count, 1); + assert_eq!(record.hello_retry_request_count, 1); + + // the client key shares the group that the server prefers, so no HRR + // is needed. + let endpoint = TestEndpoint::with_server_policy(&STRONGLY_PREFERRED_GROUPS_POLICY); + endpoint.client_handshake(&STRONGLY_PREFERRED_GROUPS_POLICY); + endpoint.subscriber.finish_record(); + + let records = endpoint.sink.records.lock().unwrap(); + let record = &records[0].as_schema().handshake; + assert_eq!(record.handshake_success_count, 1); + assert_eq!(record.hello_retry_request_count, 0); + } + #[test] fn record_contents_negotiated_parameters() { let endpoint = TestEndpoint::new(); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/s2n-tls-1.7.9/bindings/rust/standard/s2n-tls-metrics-subscriber/src/test_utils.rs new/s2n-tls-1.7.10/bindings/rust/standard/s2n-tls-metrics-subscriber/src/test_utils.rs --- old/s2n-tls-1.7.9/bindings/rust/standard/s2n-tls-metrics-subscriber/src/test_utils.rs 2026-09-01 22:54:11.000000000 +0200 +++ new/s2n-tls-1.7.10/bindings/rust/standard/s2n-tls-metrics-subscriber/src/test_utils.rs 2026-09-11 02:02:31.000000000 +0200 @@ -15,6 +15,17 @@ pub(crate) static ARBITRARY_POLICY_1: LazyLock<Policy> = LazyLock::new(|| Policy::from_version("20240503").unwrap()); +/// A policy whose most preferred group is `secp256r1`, so a client using it will +/// send a `secp256r1` key share. +pub(crate) static P256_PREFERRING_POLICY: LazyLock<Policy> = + LazyLock::new(|| Policy::from_version("20240503").unwrap()); + +/// A policy with `secp384r1` as a strongly preferred group. A server using this +/// policy will send a HelloRetryRequest to any client that key shares a +/// different group, e.g. a [`P256_PREFERRING_POLICY`] client. +pub(crate) static STRONGLY_PREFERRED_GROUPS_POLICY: LazyLock<Policy> = + LazyLock::new(|| Policy::from_version("20251117").unwrap()); + /// A test helper that implements [`TelemetrySink`] by collecting records into a Vec. #[derive(Debug, Clone)] pub(crate) struct VecSink { @@ -52,6 +63,10 @@ impl TestEndpoint<VecSink> { pub fn new() -> Self { + Self::with_server_policy(&DEFAULT_TLS13) + } + + pub fn with_server_policy(server_policy: &Policy) -> Self { let sink = VecSink::new(); let attribution = Attribution { service: "test_server".to_owned(), @@ -60,7 +75,7 @@ }; let subscriber = AggregatedMetricsSubscriber::new(sink.clone(), attribution); let server_config = { - let mut config = config_builder(&DEFAULT_TLS13).unwrap(); + let mut config = config_builder(server_policy).unwrap(); config.set_event_subscriber(subscriber.clone()).unwrap(); config.build().unwrap() }; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/s2n-tls-1.7.9/bindings/rust/standard/s2n-tls-metrics-subscriber/tests/snapshots/entry_emf.json new/s2n-tls-1.7.10/bindings/rust/standard/s2n-tls-metrics-subscriber/tests/snapshots/entry_emf.json --- old/s2n-tls-1.7.9/bindings/rust/standard/s2n-tls-metrics-subscriber/tests/snapshots/entry_emf.json 2026-09-01 22:54:11.000000000 +0200 +++ new/s2n-tls-1.7.10/bindings/rust/standard/s2n-tls-metrics-subscriber/tests/snapshots/entry_emf.json 2026-09-11 02:02:31.000000000 +0200 @@ -161,6 +161,9 @@ "Name": "sslv2_client_hello" }, { + "Name": "hello_retry_request_count" + }, + { "Name": "handshake_success_count" }, { @@ -225,6 +228,7 @@ "handshake_duration_us": "<DURATION>", "handshake_failure_count": 1, "handshake_success_count": 1, + "hello_retry_request_count": 0, "internal_failure": 0, "resource": "arn:aws:elasticloadbalancing:us-east-1:123:listener/abc", "service": "my-service", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/s2n-tls-1.7.9/bindings/rust/standard/tls-harness/Cargo.toml new/s2n-tls-1.7.10/bindings/rust/standard/tls-harness/Cargo.toml --- old/s2n-tls-1.7.9/bindings/rust/standard/tls-harness/Cargo.toml 2026-09-01 22:54:11.000000000 +0200 +++ new/s2n-tls-1.7.10/bindings/rust/standard/tls-harness/Cargo.toml 2026-09-11 02:02:31.000000000 +0200 @@ -1,6 +1,6 @@ [features] default = ["renegotiate"] -boringssl = ["dep:boring"] +boringssl = ["dep:btls"] renegotiate = ["s2n-tls/unstable-renegotiate"] [package] @@ -12,7 +12,7 @@ [dependencies] s2n-tls = { path = "../../extended/s2n-tls" } errno = "0.3.13" -libc = "0.2.174" +libc = "0.2.185" strum = { version = "0.28", features = ["derive"] } rustls = "0.23.31" rustls-pemfile = "2.2.0" @@ -20,18 +20,27 @@ openssl-sys = "0.9.109" byteorder = "1.5.0" foreign-types-shared = "0.1.1" -boring = { git = "https://github.com/kaukabrizvi/boring.git", features = ["prefix-symbols"], optional = true } brass-aphid-wire-decryption = "0.0.2" brass-aphid-wire-messages = "0.0.2" tracing = "0.1.43" +# BoringSSL is provided by the `btls` crate, a maintained fork of +# cloudflare/boring published to crates.io. It is used instead of `boring` +# because it supports symbol prefixing, which is required to link BoringSSL +# alongside the vendored OpenSSL above without duplicate symbol errors. +# See https://github.com/cloudflare/boring/pull/401 for the upstream effort. +# +# `optional = true` gates this behind the `boringssl` feature, so the BoringSSL +# build cost is only paid by jobs that actually run the interop tests. +# # NOTE: BoringSSL is disabled on macOS to avoid symbol collisions with # OpenSSL (see https://github.com/aws/s2n-tls/pull/5659), and on Windows # because BoringSSL can't be built under the MSYS2/MinGW toolchain. -[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies.boring] -git = "https://github.com/kaukabrizvi/boring.git" +[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies.btls] +version = "0.5.6" features = ["prefix-symbols"] +optional = true [dev-dependencies] # env_logger and log are used to enable logging for rustls, which can help with diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/s2n-tls-1.7.9/bindings/rust/standard/tls-harness/src/cohort/boringssl.rs new/s2n-tls-1.7.10/bindings/rust/standard/tls-harness/src/cohort/boringssl.rs --- old/s2n-tls-1.7.9/bindings/rust/standard/tls-harness/src/cohort/boringssl.rs 2026-09-01 22:54:11.000000000 +0200 +++ new/s2n-tls-1.7.10/bindings/rust/standard/tls-harness/src/cohort/boringssl.rs 2026-09-11 02:02:31.000000000 +0200 @@ -6,7 +6,7 @@ harness::{self, Mode, TlsConfigBuilder, TlsConnection, TlsInfo, ViewIO}, PemType, }; -use boring::ssl::{ +use btls::ssl::{ ErrorCode, ShutdownResult, Ssl, SslContext, SslContextBuilder, SslFiletype, SslMethod, SslSession, SslStream, SslVersion, }; @@ -175,11 +175,8 @@ impl TlsConfigBuilder for SslContextBuilder { type Config = BoringSslConfig; - fn new_test_config(mode: Mode) -> Self { - match mode { - Mode::Client => SslContext::builder(SslMethod::tls_client()).unwrap(), - Mode::Server => SslContext::builder(SslMethod::tls_server()).unwrap(), - } + fn new_test_config(_mode: Mode) -> Self { + SslContext::builder(SslMethod::tls()).unwrap() } fn set_chain(&mut self, sig_type: crate::SigType) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/s2n-tls-1.7.9/crypto/s2n_dhe.c new/s2n-tls-1.7.10/crypto/s2n_dhe.c --- old/s2n-tls-1.7.9/crypto/s2n_dhe.c 2026-09-01 22:54:11.000000000 +0200 +++ new/s2n-tls-1.7.10/crypto/s2n_dhe.c 2026-09-11 02:02:31.000000000 +0200 @@ -27,6 +27,8 @@ #include "utils/s2n_mem.h" #include "utils/s2n_safety.h" +DEFINE_POINTER_CLEANUP_FUNC(BIGNUM *, BN_free); + #define S2N_MIN_DH_PRIME_SIZE_BYTES (2048 / 8) /* Caller is not responsible for freeing values returned by these accessors @@ -317,7 +319,7 @@ uint16_t Yc_length = 0; struct s2n_blob Yc = { 0 }; int shared_key_size = 0; - BIGNUM *pub_key = NULL; + DEFER_CLEANUP(BIGNUM *pub_key = NULL, BN_free_pointer); POSIX_GUARD(s2n_check_all_dh_params(server_dh_params)); int server_dh_params_size = DH_size(server_dh_params->dh); @@ -347,15 +349,10 @@ POSIX_GUARD(s2n_alloc(shared_key, server_dh_params_size)); shared_key_size = DH_compute_key(shared_key->data, pub_key, server_dh_params->dh); - if (shared_key_size <= 0) { - BN_free(pub_key); - POSIX_BAIL(S2N_ERR_DH_SHARED_SECRET); - } + POSIX_ENSURE(shared_key_size > 0, S2N_ERR_DH_SHARED_SECRET); s2n_dh_pad_shared_secret(shared_key, shared_key_size, server_dh_params_size); - BN_free(pub_key); - return S2N_SUCCESS; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/s2n-tls-1.7.9/crypto/s2n_ecc_evp.c new/s2n-tls-1.7.10/crypto/s2n_ecc_evp.c --- old/s2n-tls-1.7.9/crypto/s2n_ecc_evp.c 2026-09-01 22:54:11.000000000 +0200 +++ new/s2n-tls-1.7.10/crypto/s2n_ecc_evp.c 2026-09-11 02:02:31.000000000 +0200 @@ -37,6 +37,10 @@ DEFINE_POINTER_CLEANUP_FUNC(EVP_PKEY_CTX *, EVP_PKEY_CTX_free); DEFINE_POINTER_CLEANUP_FUNC(EC_KEY *, EC_KEY_free); +#if EVP_APIS_SUPPORTED +DEFINE_POINTER_CLEANUP_FUNC(uint8_t *, OPENSSL_free); +#endif + #if !EVP_APIS_SUPPORTED DEFINE_POINTER_CLEANUP_FUNC(EC_POINT *, EC_POINT_free); #endif @@ -444,16 +448,11 @@ POSIX_ENSURE_REF(out); #if EVP_APIS_SUPPORTED - uint8_t *encoded_point = NULL; + DEFER_CLEANUP(uint8_t *encoded_point = NULL, OPENSSL_free_pointer); size_t size = EVP_PKEY_get1_tls_encodedpoint(ecc_evp_params->evp_pkey, &encoded_point); - if (size != ecc_evp_params->negotiated_curve->share_size) { - OPENSSL_free(encoded_point); - POSIX_BAIL(S2N_ERR_ECDHE_SERIALIZING); - } else { - POSIX_GUARD(s2n_stuffer_write_bytes(out, encoded_point, size)); - OPENSSL_free(encoded_point); - } + POSIX_ENSURE(size == ecc_evp_params->negotiated_curve->share_size, S2N_ERR_ECDHE_SERIALIZING); + POSIX_GUARD(s2n_stuffer_write_bytes(out, encoded_point, size)); #else uint8_t point_len = 0; struct s2n_blob point_blob = { 0 }; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/s2n-tls-1.7.9/nix/shell.sh new/s2n-tls-1.7.10/nix/shell.sh --- old/s2n-tls-1.7.9/nix/shell.sh 2026-09-01 22:54:11.000000000 +0200 +++ new/s2n-tls-1.7.10/nix/shell.sh 2026-09-11 02:02:31.000000000 +0200 @@ -250,6 +250,15 @@ echo "rust_test: Exporting s2n-tls headers and libs for Cargo" export S2N_TLS_LIB_DIR=$(pwd)/build/lib export S2N_TLS_INCLUDE_DIR=$(pwd)/api + # The btls crate (BoringSSL, used by the boringssl integration tests) links + # libstdc++ dynamically, so the test binary needs libstdc++.so.6 at runtime. + # Nix does not place it on the default loader path. Add it here, scoped to + # this function, rather than in the devshell hook: a global LD_LIBRARY_PATH + # leaks the Nix libgcc/libstdc++ into system tools (e.g. gmake) used by other + # jobs' CMake compiler checks, which then fail on GLIBC version mismatches. + local cxx_lib_dir + cxx_lib_dir=$(dirname "$(cc -print-file-name=libstdc++.so.6)") + export LD_LIBRARY_PATH="${cxx_lib_dir}${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" echo "rust_test: Running Rust integration tests" cargo test --manifest-path bindings/rust/standard/integration/Cargo.toml --features boringssl )} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/s2n-tls-1.7.9/tests/unit/s2n_async_offload_cb_test.c new/s2n-tls-1.7.10/tests/unit/s2n_async_offload_cb_test.c --- old/s2n-tls-1.7.9/tests/unit/s2n_async_offload_cb_test.c 2026-09-01 22:54:11.000000000 +0200 +++ new/s2n-tls-1.7.10/tests/unit/s2n_async_offload_cb_test.c 2026-09-11 02:02:31.000000000 +0200 @@ -45,6 +45,23 @@ return data->result; } +/* A perform that always fails. s2n_async_offload_op_perform() only advances the + * op to S2N_ASYNC_COMPLETE on success, so using this leaves the op stuck in + * S2N_ASYNC_INVOKED, reproducing the failed-perform state that broke teardown. */ +static S2N_RESULT s2n_async_offload_test_failing_perform(struct s2n_async_offload_op *op) +{ + RESULT_BAIL(S2N_ERR_VERIFY_SIGNATURE); +} + +/* Frees the heap allocation owned by the op below, so this regression test also + * exercises the op_data_free path (and lets valgrind confirm no leak). */ +static S2N_RESULT s2n_async_offload_test_op_data_free(struct s2n_async_offload_op *op) +{ + RESULT_ENSURE_REF(op); + RESULT_GUARD_POSIX(s2n_free(&op->op_data.async_pkey_verify.signature)); + return S2N_RESULT_OK; +} + static int s2n_test_handshake_async(struct s2n_connection *server_conn, struct s2n_connection *client_conn, struct s2n_async_offload_cb_test *data) { @@ -100,23 +117,6 @@ EXPECT_FAILURE_WITH_ERRNO(s2n_async_offload_op_perform(&test_op), S2N_ERR_INVALID_STATE); } - /* Test: s2n_async_offload_op_wipe refuses to wipe an in-flight op */ - { - struct s2n_async_offload_op op = { 0 }; - - /* Wipe succeeds when not invoked */ - op.async_state = S2N_ASYNC_NOT_INVOKED; - EXPECT_OK(s2n_async_offload_op_wipe(&op)); - - /* Wipe succeeds when complete */ - op.async_state = S2N_ASYNC_COMPLETE; - EXPECT_OK(s2n_async_offload_op_wipe(&op)); - - /* Wipe fails when still in flight */ - op.async_state = S2N_ASYNC_INVOKED; - EXPECT_ERROR_WITH_ERRNO(s2n_async_offload_op_wipe(&op), S2N_ERR_ASYNC_BLOCKED); - } - /* clang-format off */ struct s2n_async_offload_test_case { bool async_test; @@ -239,5 +239,33 @@ } } + /* Regression test: a failed s2n_async_offload_op_perform() leaves the op in + * S2N_ASYNC_INVOKED. s2n_connection_free() must still fully tear the + * connection down (returning S2N_SUCCESS) rather than short-circuiting on the + * in-flight op and leaking the connection. */ + { + struct s2n_connection *conn = s2n_connection_new(S2N_SERVER); + EXPECT_NOT_NULL(conn); + + struct s2n_async_offload_op *op = &conn->async_offload_op; + op->conn = conn; + op->type = S2N_ASYNC_OFFLOAD_PKEY_VERIFY; + op->perform = s2n_async_offload_test_failing_perform; + op->op_data_free = s2n_async_offload_test_op_data_free; + + /* Give the op some owned data so teardown has something real to free. */ + uint8_t sig_bytes[] = "test-signature"; + EXPECT_SUCCESS(s2n_alloc(&op->op_data.async_pkey_verify.signature, sizeof(sig_bytes))); + + /* Drive the op to INVOKED, then fail perform. Because perform fails, the + * op stays INVOKED instead of advancing to COMPLETE. */ + op->async_state = S2N_ASYNC_INVOKED; + EXPECT_FAILURE_WITH_ERRNO(s2n_async_offload_op_perform(op), S2N_ERR_VERIFY_SIGNATURE); + EXPECT_EQUAL(op->async_state, S2N_ASYNC_INVOKED); + + /* The fix: teardown is not short-circuited by the in-flight op. */ + EXPECT_SUCCESS(s2n_connection_free(conn)); + } + END_TEST(); } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/s2n-tls-1.7.9/tests/unit/s2n_ecc_evp_test.c new/s2n-tls-1.7.10/tests/unit/s2n_ecc_evp_test.c --- old/s2n-tls-1.7.9/tests/unit/s2n_ecc_evp_test.c 2026-09-01 22:54:11.000000000 +0200 +++ new/s2n-tls-1.7.10/tests/unit/s2n_ecc_evp_test.c 2026-09-11 02:02:31.000000000 +0200 @@ -172,6 +172,44 @@ } }; { + /* Test s2n_ecc_evp_write_params_point fails cleanly when the output + * stuffer is too small to hold the point. + * + * This exercises the error path where the encoded point has been + * allocated but the stuffer write fails. The allocation must be freed + * on this path (see DEFER_CLEANUP in s2n_ecc_evp_write_params_point); + * leak detection (valgrind/ASAN) in CI verifies no leak occurs. */ + for (size_t i = 0; i < s2n_all_supported_curves_list_len; i++) { + struct s2n_ecc_evp_params test_params = { 0 }; + struct s2n_stuffer wire = { 0 }; + + /* Non-growable stuffer smaller than the curve's share_size, so the + * write of the encoded point cannot succeed. */ + EXPECT_SUCCESS(s2n_stuffer_alloc(&wire, s2n_all_supported_curves_list[i]->share_size - 1)); + + test_params.negotiated_curve = s2n_all_supported_curves_list[i]; + EXPECT_SUCCESS(s2n_ecc_evp_generate_ephemeral_key(&test_params)); + EXPECT_NOT_NULL(test_params.evp_pkey); + + /* The errno differs by build: the EVP path writes via + * s2n_stuffer_write_bytes, which reports the out-of-space condition + * as S2N_ERR_STUFFER_IS_FULL. The non-EVP path writes via + * s2n_stuffer_raw_write, which returns NULL on the same condition + * and is caught by POSIX_ENSURE_REF as S2N_ERR_NULL. */ +#if EVP_APIS_SUPPORTED + EXPECT_FAILURE_WITH_ERRNO(s2n_ecc_evp_write_params_point(&test_params, &wire), + S2N_ERR_STUFFER_IS_FULL); +#else + EXPECT_FAILURE_WITH_ERRNO(s2n_ecc_evp_write_params_point(&test_params, &wire), + S2N_ERR_NULL); +#endif + + /* Clean up */ + EXPECT_SUCCESS(s2n_ecc_evp_params_free(&test_params)); + EXPECT_SUCCESS(s2n_stuffer_free(&wire)); + } + }; + { /* TEST s2n_ecc_evp_read_params_point for all supported curves */ for (size_t i = 0; i < s2n_all_supported_curves_list_len; i++) { struct s2n_ecc_evp_params write_params = { 0 }; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/s2n-tls-1.7.9/tests/unit/s2n_fingerprint_ja4_test.c new/s2n-tls-1.7.10/tests/unit/s2n_fingerprint_ja4_test.c --- old/s2n-tls-1.7.9/tests/unit/s2n_fingerprint_ja4_test.c 2026-09-01 22:54:11.000000000 +0200 +++ new/s2n-tls-1.7.10/tests/unit/s2n_fingerprint_ja4_test.c 2026-09-11 02:02:31.000000000 +0200 @@ -635,6 +635,51 @@ }; }; + /* Only the first S2N_JA4_LIST_LIMIT (99) ciphers and extensions are + * collected into the sorted list. Lists longer than the limit therefore + * produce the same hash as a list of exactly the limit, since the helpers + * generate a deterministic sequence and the extra entries are not included. + */ + { + const uint16_t counts_over_limit[] = { 99, 100, 255 }; + + /* Ciphers */ + { + uint8_t limit_hash[S2N_TEST_OUTPUT_SIZE] = { 0 }; + uint32_t limit_hash_size = 0; + EXPECT_OK(s2n_test_ja4_hash_from_cipher_count(99, + sizeof(limit_hash), limit_hash, &limit_hash_size)); + + for (size_t i = 0; i < s2n_array_len(counts_over_limit); i++) { + uint8_t hash[S2N_TEST_OUTPUT_SIZE] = { 0 }; + uint32_t hash_size = 0; + EXPECT_OK(s2n_test_ja4_hash_from_cipher_count(counts_over_limit[i], + sizeof(hash), hash, &hash_size)); + + EXPECT_EQUAL(hash_size, limit_hash_size); + EXPECT_BYTEARRAY_EQUAL(hash, limit_hash, limit_hash_size); + } + }; + + /* Extensions */ + { + uint8_t limit_hash[S2N_TEST_OUTPUT_SIZE] = { 0 }; + uint32_t limit_hash_size = 0; + EXPECT_OK(s2n_test_ja4_hash_from_extension_count(99, + sizeof(limit_hash), limit_hash, &limit_hash_size)); + + for (size_t i = 0; i < s2n_array_len(counts_over_limit); i++) { + uint8_t hash[S2N_TEST_OUTPUT_SIZE] = { 0 }; + uint32_t hash_size = 0; + EXPECT_OK(s2n_test_ja4_hash_from_extension_count(counts_over_limit[i], + sizeof(hash), hash, &hash_size)); + + EXPECT_EQUAL(hash_size, limit_hash_size); + EXPECT_BYTEARRAY_EQUAL(hash, limit_hash, limit_hash_size); + } + }; + }; + /* Test ALPN */ { /* Test basic ALPN diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/s2n-tls-1.7.9/tls/s2n_async_offload.c new/s2n-tls-1.7.10/tls/s2n_async_offload.c --- old/s2n-tls-1.7.9/tls/s2n_async_offload.c 2026-09-01 22:54:11.000000000 +0200 +++ new/s2n-tls-1.7.10/tls/s2n_async_offload.c 2026-09-11 02:02:31.000000000 +0200 @@ -64,10 +64,17 @@ return S2N_SUCCESS; } -S2N_RESULT s2n_async_offload_op_wipe(struct s2n_async_offload_op *op) +/** + * Unconditionally releases the op's resources, regardless of async_state. + * + * This is the connection teardown path (s2n_connection_free / s2n_connection_wipe). + * It MUST always fully free the op, even if a failed s2n_async_offload_op_perform() + * left the op in S2N_ASYNC_INVOKED; otherwise the remainder of teardown is skipped + * and the connection leaks. + */ +S2N_RESULT s2n_async_offload_op_free(struct s2n_async_offload_op *op) { RESULT_ENSURE_REF(op); - RESULT_ENSURE(op->async_state != S2N_ASYNC_INVOKED, S2N_ERR_ASYNC_BLOCKED); if (op->op_data_free == NULL) { return S2N_RESULT_OK; } @@ -80,6 +87,10 @@ /** * MUST be called at the end of each handshake state handler that may invoke async_offload_cb * to clean up the op object for its next use. + * + * Unlike s2n_async_offload_op_free(), this is the handshake-path cleanup and refuses + * to free an op that is still in flight (S2N_ASYNC_INVOKED): s2n still holds + * references to its data, so wiping mid-negotiation would be a logic error. */ S2N_RESULT s2n_async_offload_op_reset(struct s2n_async_offload_op *op) { @@ -90,7 +101,7 @@ } RESULT_ENSURE(op->async_state == S2N_ASYNC_COMPLETE, S2N_ERR_INVALID_STATE); - RESULT_GUARD(s2n_async_offload_op_wipe(op)); + RESULT_GUARD(s2n_async_offload_op_free(op)); return S2N_RESULT_OK; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/s2n-tls-1.7.9/tls/s2n_async_offload.h new/s2n-tls-1.7.10/tls/s2n_async_offload.h --- old/s2n-tls-1.7.9/tls/s2n_async_offload.h 2026-09-01 22:54:11.000000000 +0200 +++ new/s2n-tls-1.7.10/tls/s2n_async_offload.h 2026-09-11 02:02:31.000000000 +0200 @@ -56,6 +56,6 @@ }; S2N_RESULT s2n_async_offload_cb_invoke(struct s2n_connection *conn, struct s2n_async_offload_op *op); -S2N_RESULT s2n_async_offload_op_wipe(struct s2n_async_offload_op *op); +S2N_RESULT s2n_async_offload_op_free(struct s2n_async_offload_op *op); S2N_RESULT s2n_async_offload_op_reset(struct s2n_async_offload_op *op); bool s2n_async_offload_op_is_in_allow_list(struct s2n_config *config, s2n_async_offload_op_type op_type); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/s2n-tls-1.7.9/tls/s2n_config.c new/s2n-tls-1.7.10/tls/s2n_config.c --- old/s2n-tls-1.7.9/tls/s2n_config.c 2026-09-01 22:54:11.000000000 +0200 +++ new/s2n-tls-1.7.10/tls/s2n_config.c 2026-09-11 02:02:31.000000000 +0200 @@ -158,6 +158,8 @@ return 0; } s2n_pkey_type cert_type = s2n_cert_chain_and_key_get_pkey_type(cert_key_pair); + POSIX_ENSURE(cert_type >= 0, S2N_ERR_CERT_TYPE_UNSUPPORTED); + POSIX_ENSURE(cert_type < S2N_CERT_TYPE_COUNT, S2N_ERR_CERT_TYPE_UNSUPPORTED); struct s2n_blob s2n_map_value = { 0 }; bool key_found = false; POSIX_GUARD_RESULT(s2n_map_lookup(domain_name_to_cert_map, name, &s2n_map_value, &key_found)); @@ -545,6 +547,8 @@ POSIX_GUARD_RESULT(s2n_security_policy_validate_certificate_chain(config->security_policy, cert_key_pair)); s2n_pkey_type cert_type = s2n_cert_chain_and_key_get_pkey_type(cert_key_pair); + POSIX_ENSURE(cert_type >= 0, S2N_ERR_CERT_TYPE_UNSUPPORTED); + POSIX_ENSURE(cert_type < S2N_CERT_TYPE_COUNT, S2N_ERR_CERT_TYPE_UNSUPPORTED); config->is_rsa_cert_configured |= (cert_type == S2N_PKEY_TYPE_RSA); /* Perform all fallible checks BEFORE inserting into the domain name map. @@ -553,8 +557,6 @@ * resulting in dangling pointers and a use-after-free during SNI lookup. */ if (!config->default_certs_are_explicit) { - POSIX_ENSURE(cert_type >= 0, S2N_ERR_CERT_TYPE_UNSUPPORTED); - POSIX_ENSURE(cert_type < S2N_CERT_TYPE_COUNT, S2N_ERR_CERT_TYPE_UNSUPPORTED); if (config->default_certs_by_type.certs[cert_type] != NULL) { /* Because library-owned certificates are tracked and cleaned up via the * default_certs_by_type mapping, library-owned chains MUST be set as the default @@ -744,6 +746,8 @@ for (size_t i = 0; i < num_cert_key_pairs; i++) { POSIX_ENSURE_REF(cert_key_pairs[i]); s2n_pkey_type cert_type = s2n_cert_chain_and_key_get_pkey_type(cert_key_pairs[i]); + POSIX_ENSURE(cert_type >= 0, S2N_ERR_CERT_TYPE_UNSUPPORTED); + POSIX_ENSURE(cert_type < S2N_CERT_TYPE_COUNT, S2N_ERR_CERT_TYPE_UNSUPPORTED); S2N_ERROR_IF(new_defaults.certs[cert_type] != NULL, S2N_ERR_MULTIPLE_DEFAULT_CERTIFICATES_PER_AUTH_TYPE); new_defaults.certs[cert_type] = cert_key_pairs[i]; } @@ -751,6 +755,8 @@ POSIX_GUARD(s2n_config_clear_default_certificates(config)); for (size_t i = 0; i < num_cert_key_pairs; i++) { s2n_pkey_type cert_type = s2n_cert_chain_and_key_get_pkey_type(cert_key_pairs[i]); + POSIX_ENSURE(cert_type >= 0, S2N_ERR_CERT_TYPE_UNSUPPORTED); + POSIX_ENSURE(cert_type < S2N_CERT_TYPE_COUNT, S2N_ERR_CERT_TYPE_UNSUPPORTED); config->is_rsa_cert_configured |= (cert_type == S2N_PKEY_TYPE_RSA); config->default_certs_by_type.certs[cert_type] = cert_key_pairs[i]; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/s2n-tls-1.7.9/tls/s2n_connection.c new/s2n-tls-1.7.10/tls/s2n_connection.c --- old/s2n-tls-1.7.9/tls/s2n_connection.c 2026-09-01 22:54:11.000000000 +0200 +++ new/s2n-tls-1.7.10/tls/s2n_connection.c 2026-09-11 02:02:31.000000000 +0200 @@ -281,7 +281,7 @@ POSIX_GUARD(s2n_stuffer_free(&conn->handshake.io)); POSIX_GUARD(s2n_stuffer_free(&conn->post_handshake.in)); s2n_x509_validator_wipe(&conn->x509_validator); - POSIX_GUARD_RESULT(s2n_async_offload_op_wipe(&conn->async_offload_op)); + POSIX_GUARD_RESULT(s2n_async_offload_op_free(&conn->async_offload_op)); POSIX_GUARD(s2n_client_hello_free_raw_message(&conn->client_hello)); POSIX_GUARD(s2n_free(&conn->application_protocols_overridden)); POSIX_GUARD(s2n_free(&conn->cookie)); @@ -552,7 +552,7 @@ POSIX_GUARD(s2n_stuffer_free(&conn->in)); POSIX_GUARD_RESULT(s2n_psk_parameters_wipe(&conn->psk_params)); - POSIX_GUARD_RESULT(s2n_async_offload_op_wipe(&conn->async_offload_op)); + POSIX_GUARD_RESULT(s2n_async_offload_op_free(&conn->async_offload_op)); /* Wipe the I/O-related info and restore the original socket if necessary */ POSIX_GUARD(s2n_connection_wipe_io(conn)); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/s2n-tls-1.7.9/tls/s2n_fingerprint_ja4.c new/s2n-tls-1.7.10/tls/s2n_fingerprint_ja4.c --- old/s2n-tls-1.7.9/tls/s2n_fingerprint_ja4.c 2026-09-01 22:54:11.000000000 +0200 +++ new/s2n-tls-1.7.10/tls/s2n_fingerprint_ja4.c 2026-09-11 02:02:31.000000000 +0200 @@ -362,6 +362,7 @@ RESULT_GUARD_POSIX(s2n_stuffer_init_written(&cipher_suites, &ch->cipher_suites)); DEFER_CLEANUP(struct s2n_stuffer *iana_list = sort_space, s2n_stuffer_wipe_pointer); + size_t written_count = 0; while (s2n_stuffer_data_available(&cipher_suites)) { uint16_t iana = 0; RESULT_GUARD_POSIX(s2n_stuffer_read_uint16(&cipher_suites, &iana)); @@ -372,6 +373,14 @@ if (s2n_fingerprint_is_grease_value(iana)) { continue; } + /* The count is capped at 99 (see s2n_fingerprint_ja4_count), so there + * is no need to collect more than S2N_JA4_LIST_LIMIT entries. This keeps + * the workspace within its pre-sized bounds. + */ + if (written_count >= S2N_JA4_LIST_LIMIT) { + continue; + } + written_count++; RESULT_GUARD(s2n_stuffer_write_uint16_hex(iana_list, iana)); RESULT_GUARD_POSIX(s2n_stuffer_write_char(iana_list, S2N_JA4_LIST_DIV)); } @@ -426,6 +435,7 @@ RESULT_GUARD_POSIX(s2n_stuffer_init_written(&extensions, &ch->extensions.raw)); DEFER_CLEANUP(struct s2n_stuffer *iana_list = sort_space, s2n_stuffer_wipe_pointer); + size_t written_count = 0; while (s2n_stuffer_data_available(&extensions)) { uint16_t iana = 0; RESULT_GUARD(s2n_fingerprint_parse_extension(&extensions, &iana)); @@ -451,6 +461,15 @@ if (iana == TLS_EXTENSION_SERVER_NAME || iana == S2N_EXTENSION_ALPN) { continue; } + /* The count is capped at 99 (see s2n_fingerprint_ja4_count), so there + * is no need to collect more than S2N_JA4_LIST_LIMIT entries. This keeps + * the workspace within its pre-sized bounds. extensions_count is still + * incremented above for every extension. + */ + if (written_count >= S2N_JA4_LIST_LIMIT) { + continue; + } + written_count++; RESULT_GUARD(s2n_stuffer_write_uint16_hex(iana_list, iana)); RESULT_GUARD_POSIX(s2n_stuffer_write_char(iana_list, S2N_JA4_LIST_DIV)); } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/s2n-tls-1.7.9/tls/s2n_psk.c new/s2n-tls-1.7.10/tls/s2n_psk.c --- old/s2n-tls-1.7.9/tls/s2n_psk.c 2026-09-01 22:54:11.000000000 +0200 +++ new/s2n-tls-1.7.10/tls/s2n_psk.c 2026-09-11 02:02:31.000000000 +0200 @@ -421,7 +421,7 @@ POSIX_ENSURE_REF(binder_hash); POSIX_ENSURE_REF(output_binder); - DEFER_CLEANUP(struct s2n_tls13_keys psk_keys, s2n_tls13_keys_free); + DEFER_CLEANUP(struct s2n_tls13_keys psk_keys = { 0 }, s2n_tls13_keys_free); POSIX_GUARD(s2n_tls13_keys_init(&psk_keys, psk->hmac_alg)); POSIX_ENSURE_EQ(binder_hash->size, psk_keys.size); POSIX_ENSURE_EQ(output_binder->size, psk_keys.size); @@ -447,7 +447,7 @@ POSIX_ENSURE_REF(psk); POSIX_ENSURE_REF(binder_to_verify); - DEFER_CLEANUP(struct s2n_tls13_keys psk_keys, s2n_tls13_keys_free); + DEFER_CLEANUP(struct s2n_tls13_keys psk_keys = { 0 }, s2n_tls13_keys_free); POSIX_GUARD(s2n_tls13_keys_init(&psk_keys, psk->hmac_alg)); POSIX_ENSURE_EQ(binder_to_verify->size, psk_keys.size); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/s2n-tls-1.7.9/tls/s2n_server_cert_request.c new/s2n-tls-1.7.10/tls/s2n_server_cert_request.c --- old/s2n-tls-1.7.9/tls/s2n_server_cert_request.c 2026-09-01 22:54:11.000000000 +0200 +++ new/s2n-tls-1.7.10/tls/s2n_server_cert_request.c 2026-09-11 02:02:31.000000000 +0200 @@ -161,7 +161,10 @@ POSIX_ENSURE_REF(cert); } conn->handshake_params.our_chain_and_key = cert; - conn->handshake_params.client_cert_pkey_type = s2n_cert_chain_and_key_get_pkey_type(cert); + s2n_pkey_type cert_type = s2n_cert_chain_and_key_get_pkey_type(cert); + POSIX_ENSURE(cert_type >= 0, S2N_ERR_CERT_TYPE_UNSUPPORTED); + POSIX_ENSURE(cert_type < S2N_CERT_TYPE_COUNT, S2N_ERR_CERT_TYPE_UNSUPPORTED); + conn->handshake_params.client_cert_pkey_type = cert_type; POSIX_GUARD_RESULT(s2n_signature_algorithm_select(conn)); }
