From 1b0b99e6776a655684c4283236538093afa1bf09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20R=C3=BCth?= <1324490+janrueth@users.noreply.github.com> Date: Mon, 13 Apr 2026 10:48:07 +0200 Subject: [PATCH 1/2] Add TLS 1.2 PRF module and bindings Expose a new boring::prf::tls1_prf helper that wraps the TLS 1.2 PRF and document it with RFC 5246 context. Add vector tests for SHA-224, SHA-256, SHA-384, and SHA-512 to verify output compatibility. Declare CRYPTO_tls1_prf in boring-sys as a manual extern because the symbol is exported by BoringSSL but declared only in an internal header, so bindgen does not emit it. Keep this as a small explicit declaration in lib.rs as the simplest and most robust approach for a single missing symbol, instead of adding fragile bindgen plumbing for internal headers. --- boring-sys/src/lib.rs | 22 +++++ boring/src/lib.rs | 1 + boring/src/prf.rs | 210 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 233 insertions(+) create mode 100644 boring/src/prf.rs diff --git a/boring-sys/src/lib.rs b/boring-sys/src/lib.rs index e29b51257..f7e88d0fb 100644 --- a/boring-sys/src/lib.rs +++ b/boring-sys/src/lib.rs @@ -67,3 +67,25 @@ pub fn init() { CRYPTO_library_init(); } } + +extern "C" { + /// Calculates `out_len` bytes of the TLS 1.2 PRF using `digest` and writes + /// them to `out`. + /// + /// This symbol is exported by BoringSSL, but it is declared in an internal + /// header (`crypto/fipsmodule/tls/internal.h`) and is therefore not present + /// in generated bindgen output. + pub fn CRYPTO_tls1_prf( + digest: *const EVP_MD, + out: *mut u8, + out_len: usize, + secret: *const u8, + secret_len: usize, + label: *const u8, + label_len: usize, + seed1: *const u8, + seed1_len: usize, + seed2: *const u8, + seed2_len: usize, + ) -> c_int; +} diff --git a/boring/src/lib.rs b/boring/src/lib.rs index 98c4f34e2..9f1d0f55f 100644 --- a/boring/src/lib.rs +++ b/boring/src/lib.rs @@ -143,6 +143,7 @@ pub mod nid; pub mod pkcs12; pub mod pkcs5; pub mod pkey; +pub mod prf; pub mod rand; pub mod rsa; pub mod sha; diff --git a/boring/src/prf.rs b/boring/src/prf.rs new file mode 100644 index 000000000..19100cdd4 --- /dev/null +++ b/boring/src/prf.rs @@ -0,0 +1,210 @@ +//! TLS pseudo-random function helpers. +//! +//! This module exposes the TLS 1.2 PRF as defined in RFC 5246, Section 5. +//! +//! # Example +//! +//! ``` +//! use boring::hash::MessageDigest; +//! +//! let mut out = [0u8; 48]; +//! boring::prf::tls1_prf( +//! MessageDigest::sha256(), +//! &mut out, +//! b"secret", +//! b"label", +//! b"seed", +//! ) +//! .unwrap(); +//! ``` + +use std::ptr; + +use openssl_macros::corresponds; + +use crate::cvt; +use crate::error::ErrorStack; +use crate::ffi; +use crate::hash::MessageDigest; + +/// Computes TLS 1.2 PRF output using `digest`. +/// +/// `seed` should contain the full PRF seed for the calling protocol. In TLS +/// 1.2 this is often a concatenation of random values. +#[corresponds(CRYPTO_tls1_prf)] +pub fn tls1_prf( + digest: MessageDigest, + out: &mut [u8], + secret: &[u8], + label: &[u8], + seed: &[u8], +) -> Result<(), ErrorStack> { + unsafe { + ffi::init(); + + cvt(ffi::CRYPTO_tls1_prf( + digest.as_ptr(), + out.as_mut_ptr(), + out.len(), + secret.as_ptr(), + secret.len(), + label.as_ptr(), + label.len(), + seed.as_ptr(), + seed.len(), + ptr::null(), + 0, + )) + } +} + +#[cfg(test)] +mod tests { + use hex::FromHex; + + use crate::hash::MessageDigest; + + #[test] + fn tls12_prf_vectors() { + struct TestVector { + name: &'static str, + digest: fn() -> MessageDigest, + secret: &'static str, + label: &'static [u8], + seed: &'static str, + expected: &'static str, + } + + // Test vectors from: + // https://mailarchive.ietf.org/arch/msg/tls/fzVCzk-z3FShgGJ6DOXqM1ydxms/ + let vectors = [ + TestVector { + name: "TLS1.2 PRF-SHA224", + digest: MessageDigest::sha224, + secret: "e1 88 28 74 03 52 b5 30 d6 9b 34 c6 59 7d ea 2e", + label: b"test label", + seed: "f5 a3 fe 6d 34 e2 e2 85 60 fd ca f6 82 3f 90 91", + expected: " + 22 4d 8a f3 c0 45 33 93 + a9 77 97 89 d2 1c f7 da + 5e e6 2a e6 b6 17 87 3d + 48 94 28 ef c8 dd 58 d1 + 56 6e 70 29 e2 ca 3a 5e + cd 35 5d c6 4d 4d 92 7e + 2f bd 78 c4 23 3e 86 04 + b1 47 49 a7 7a 92 a7 0f + dd f6 14 bc 0d f6 23 d7 + 98 60 4e 4c a5 51 27 94 + d8 02 a2 58 e8 2f 86 cf + ", + }, + TestVector { + name: "TLS1.2 PRF-SHA256", + digest: MessageDigest::sha256, + secret: "9b be 43 6b a9 40 f0 17 b1 76 52 84 9a 71 db 35", + label: b"test label", + seed: "a0 ba 9f 93 6c da 31 18 27 a6 f7 96 ff d5 19 8c", + expected: " + e3 f2 29 ba 72 7b e1 7b + 8d 12 26 20 55 7c d4 53 + c2 aa b2 1d 07 c3 d4 95 + 32 9b 52 d4 e6 1e db 5a + 6b 30 17 91 e9 0d 35 c9 + c9 a4 6b 4e 14 ba f9 af + 0f a0 22 f7 07 7d ef 17 + ab fd 37 97 c0 56 4b ab + 4f bc 91 66 6e 9d ef 9b + 97 fc e3 4f 79 67 89 ba + a4 80 82 d1 22 ee 42 c5 + a7 2e 5a 51 10 ff f7 01 + 87 34 7b 66 + ", + }, + TestVector { + name: "TLS1.2 PRF-SHA512", + digest: MessageDigest::sha512, + secret: "b0 32 35 23 c1 85 35 99 58 4d 88 56 8b bb 05 eb", + label: b"test label", + seed: "d4 64 0e 12 e4 bc db fb 43 7f 03 e6 ae 41 8e e5", + expected: " + 12 61 f5 88 c7 98 c5 c2 + 01 ff 03 6e 7a 9c b5 ed + cd 7f e3 f9 4c 66 9a 12 + 2a 46 38 d7 d5 08 b2 83 + 04 2d f6 78 98 75 c7 14 + 7e 90 6d 86 8b c7 5c 45 + e2 0e b4 0c 1c f4 a1 71 + 3b 27 37 1f 68 43 25 92 + f7 dc 8e a8 ef 22 3e 12 + ea 85 07 84 13 11 bf 68 + 65 3d 0c fc 40 56 d8 11 + f0 25 c4 5d df a6 e6 fe + c7 02 f0 54 b4 09 d6 f2 + 8d d0 a3 23 3e 49 8d a4 + 1a 3e 75 c5 63 0e ed be + 22 fe 25 4e 33 a1 b0 e9 + f6 b9 82 66 75 be c7 d0 + 1a 84 56 58 dc 9c 39 75 + 45 40 1d 40 b9 f4 6c 7a + 40 0e e1 b8 f8 1c a0 a6 + 0d 1a 39 7a 10 28 bf f5 + d2 ef 50 66 12 68 42 fb + 8d a4 19 76 32 bd b5 4f + f6 63 3f 86 bb c8 36 e6 + 40 d4 d8 98 + ", + }, + TestVector { + name: "TLS1.2 PRF-SHA384", + digest: MessageDigest::sha384, + secret: "b8 0b 73 3d 6c ee fc dc 71 56 6e a4 8e 55 67 df", + label: b"test label", + seed: "cd 66 5c f6 a8 44 7d d6 ff 8b 27 55 5e db 74 65", + expected: " + 7b 0c 18 e9 ce d4 10 ed + 18 04 f2 cf a3 4a 33 6a + 1c 14 df fb 49 00 bb 5f + d7 94 21 07 e8 1c 83 cd + e9 ca 0f aa 60 be 9f e3 + 4f 82 b1 23 3c 91 46 a0 + e5 34 cb 40 0f ed 27 00 + 88 4f 9d c2 36 f8 0e dd + 8b fa 96 11 44 c9 e8 d7 + 92 ec a7 22 a7 b3 2f c3 + d4 16 d4 73 eb c2 c5 fd + 4a bf da d0 5d 91 84 25 + 9b 5b f8 cd 4d 90 fa 0d + 31 e2 de c4 79 e4 f1 a2 + 60 66 f2 ee a9 a6 92 36 + a3 e5 26 55 c9 e9 ae e6 + 91 c8 f3 a2 68 54 30 8d + 5e aa 3b e8 5e 09 90 70 + 3d 73 e5 6f + ", + }, + ]; + + for vector in vectors { + let secret = decode_hex(vector.secret); + let seed = decode_hex(vector.seed); + let expected = decode_hex(vector.expected); + let mut out = vec![0u8; expected.len()]; + + super::tls1_prf( + (vector.digest)(), + out.as_mut_slice(), + secret.as_slice(), + vector.label, + seed.as_slice(), + ) + .unwrap(); + + assert_eq!(out, expected, "{}", vector.name); + } + } + + fn decode_hex(input: &str) -> Vec { + let compact: String = input.chars().filter(|c| !c.is_whitespace()).collect(); + Vec::from_hex(compact).unwrap() + } +} From 51c9d9c17fb58e30b114a0448e2807554a73ad0f Mon Sep 17 00:00:00 2001 From: Kornel Date: Mon, 13 Apr 2026 14:40:29 +0100 Subject: [PATCH 2/2] Put prf behind a feature flag --- .github/workflows/ci.yml | 6 +++--- boring-sys/src/lib.rs | 45 ++++++++++++++++++++++------------------ boring/Cargo.toml | 3 +++ boring/src/lib.rs | 1 + boring/src/prf.rs | 2 +- 5 files changed, 33 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e184afb3..59ab7b3b9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,7 +58,7 @@ jobs: path: target key: clippy-${{ steps.rust-version.outputs.version }}-${{ hashFiles('Cargo.lock') }} - name: Run clippy - run: cargo clippy --all --all-targets --features rpk,mlkem + run: cargo clippy --all --all-targets --features rpk,prf,mlkem - name: Check docs run: cargo doc --no-deps -p boring -p boring-sys -p hyper-boring -p tokio-boring --features rpk,underscore-wildcards env: @@ -426,9 +426,9 @@ jobs: shell: bash - run: cargo check --no-default-features name: Check `--no-default-features` - - run: cargo check --features mlkem,credential + - run: cargo check --features prf,mlkem,credential name: Check `mlkem,credential` - - run: cargo test --features rpk + - run: cargo test --features rpk,prf name: Run `rpk` tests - run: cargo test --features underscore-wildcards name: Run `underscore-wildcards` tests diff --git a/boring-sys/src/lib.rs b/boring-sys/src/lib.rs index f7e88d0fb..1810d174c 100644 --- a/boring-sys/src/lib.rs +++ b/boring-sys/src/lib.rs @@ -68,24 +68,29 @@ pub fn init() { } } -extern "C" { - /// Calculates `out_len` bytes of the TLS 1.2 PRF using `digest` and writes - /// them to `out`. - /// - /// This symbol is exported by BoringSSL, but it is declared in an internal - /// header (`crypto/fipsmodule/tls/internal.h`) and is therefore not present - /// in generated bindgen output. - pub fn CRYPTO_tls1_prf( - digest: *const EVP_MD, - out: *mut u8, - out_len: usize, - secret: *const u8, - secret_len: usize, - label: *const u8, - label_len: usize, - seed1: *const u8, - seed1_len: usize, - seed2: *const u8, - seed2_len: usize, - ) -> c_int; +pub mod internal { + use super::EVP_MD; + use std::os::raw::c_int; + + extern "C" { + /// Calculates `out_len` bytes of the TLS 1.2 PRF using `digest` and writes + /// them to `out`. + /// + /// This symbol is exported by BoringSSL, but it is declared in an internal + /// header (`crypto/fipsmodule/tls/internal.h`) and is therefore not present + /// in generated bindgen output. + pub fn CRYPTO_tls1_prf( + digest: *const EVP_MD, + out: *mut u8, + out_len: usize, + secret: *const u8, + secret_len: usize, + label: *const u8, + label_len: usize, + seed1: *const u8, + seed1_len: usize, + seed2: *const u8, + seed2_len: usize, + ) -> c_int; + } } diff --git a/boring/Cargo.toml b/boring/Cargo.toml index b39f7aadf..0aa34592f 100644 --- a/boring/Cargo.toml +++ b/boring/Cargo.toml @@ -32,6 +32,9 @@ pq-experimental = [] # Interface for ML-KEM (FIPS 203) post-quantum key encapsulation. Does not affect ciphers used in TLS. mlkem = [] +# Expose internal `tls1_prf` +prf = [] + # SslCredential API credential = [] diff --git a/boring/src/lib.rs b/boring/src/lib.rs index 9f1d0f55f..5533b746f 100644 --- a/boring/src/lib.rs +++ b/boring/src/lib.rs @@ -143,6 +143,7 @@ pub mod nid; pub mod pkcs12; pub mod pkcs5; pub mod pkey; +#[cfg(feature = "prf")] pub mod prf; pub mod rand; pub mod rsa; diff --git a/boring/src/prf.rs b/boring/src/prf.rs index 19100cdd4..6bf59b2a7 100644 --- a/boring/src/prf.rs +++ b/boring/src/prf.rs @@ -42,7 +42,7 @@ pub fn tls1_prf( unsafe { ffi::init(); - cvt(ffi::CRYPTO_tls1_prf( + cvt(ffi::internal::CRYPTO_tls1_prf( digest.as_ptr(), out.as_mut_ptr(), out.len(),