Skip to content

Commit 41effad

Browse files
committed
feat: add blapi feature to bypass PKCS#11 in RecordProtection
Calls freebl AES-GCM and ChaCha20-Poly1305 primitives directly instead of going through `PK11_AEADOp` → softoken, eliminating the `sftk_SessionFromHandle` mutex and hash-table overhead (~7.6% CPU on the hot path). `RecordProtection::new` takes a `Mode` so callers that know their direction (send vs. receive) create only the context they need. NOTE: bypasses softoken's FIPS self-test gate. Intentional for neqo (non-FIPS).
1 parent 9b94ca3 commit 41effad

14 files changed

Lines changed: 718 additions & 70 deletions

File tree

.github/workflows/check.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ jobs:
8383
with:
8484
version: ${{ matrix.rust-toolchain }}
8585
components: ${{ matrix.rust-toolchain == 'stable' && 'llvm-tools' || '' }} ${{ matrix.rust-toolchain == 'nightly' && startsWith(matrix.os, 'ubuntu') && !endsWith(matrix.os, 'arm') && 'rust-src ' || '' }}
86-
tools: ${{ matrix.rust-toolchain == 'stable' && 'cargo-llvm-cov' || '' }} ${{ matrix.rust-toolchain == 'nightly' && startsWith(matrix.os, 'ubuntu') && !endsWith(matrix.os, 'arm') && 'cargo-careful ' || '' }}
86+
tools: ${{ matrix.rust-toolchain == 'stable' && 'cargo-llvm-cov' || '' }} ${{ matrix.rust-toolchain == 'nightly' && startsWith(matrix.os, 'ubuntu') && !endsWith(matrix.os, 'arm') && 'cargo-careful ' || '' }} cargo-hack
8787
token: ${{ secrets.GITHUB_TOKEN }}
8888

8989
- uses: mozilla/actions/nss@27cbe8fb5d338c2861b787e5de10410559065db1 # v1.1.3
@@ -96,6 +96,11 @@ jobs:
9696
# shellcheck disable=SC2086
9797
cargo check $BUILD_TYPE --locked --all-targets
9898
99+
- name: Check feature powerset
100+
run: |
101+
# shellcheck disable=SC2086
102+
cargo hack check $BUILD_TYPE --locked --feature-powerset --no-dev-deps --exclude-features gecko --mutually-exclusive-features blapi,disable-encryption
103+
99104
- name: Run tests and determine coverage
100105
env:
101106
RUST_LOG: trace
@@ -119,6 +124,11 @@ jobs:
119124
cargo $CAREFUL test $BUILD_TYPE --locked $TRIPLE
120125
fi
121126
127+
- name: Test with blapi feature
128+
run: |
129+
# shellcheck disable=SC2086
130+
cargo test $BUILD_TYPE --locked --features blapi
131+
122132
- name: CodeCov Windows workaround
123133
if: ${{ startsWith(matrix.os, 'windows') && matrix.type == 'debug' && matrix.rust-toolchain == 'stable' }}
124134
run: |

.github/workflows/clippy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ jobs:
5959
# respective default features only. Can reveal warnings otherwise
6060
# hidden given that a plain cargo clippy combines all features of the
6161
# workspace. See e.g. https://github.com/mozilla/neqo/pull/1695.
62-
- run: cargo hack clippy --feature-powerset --no-dev-deps --exclude-features gecko -- -D warnings
62+
- run: cargo hack clippy --feature-powerset --no-dev-deps --exclude-features gecko --mutually-exclusive-features blapi,disable-encryption -- -D warnings
6363
- run: cargo clippy --locked --workspace --all-targets -- -D warnings
6464
- run: cargo doc --locked --workspace --no-deps --document-private-items
6565
env:

Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "nss-rs"
3-
version = "0.10.0"
3+
version = "0.11.0"
44
authors = ["Martin Thomson <mt@lowentropy.net>", "Andy Leiserson <aleiserson@mozilla.com>", "John M. Schanck <jschanck@mozilla.com>", "Benjamin Beurdouche <beurdouche@mozilla.com>", "Anna Weine <anna.weine@mozilla.com>"]
55
categories = ["network-programming", "web-programming"]
66
keywords = ["nss", "crypto", "mozilla", "firefox"]
@@ -124,6 +124,9 @@ verbose_file_reads = "warn"
124124

125125
[features]
126126
bench = ["log/release_max_level_info"]
127+
# Bypass PKCS#11 session layer for RecordProtection AEAD operations by calling
128+
# freebl directly. Improves performance but skips softoken's FIPS self-test gate.
129+
blapi = []
127130
deny-warnings = []
128131
disable-encryption = []
129132
disable-random = []

build.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,9 @@ fn dynamic_link() {
229229
for lib in dynamic_libs {
230230
println!("cargo:rustc-link-lib=dylib={lib}");
231231
}
232+
if env::var("CARGO_FEATURE_BLAPI").is_ok() {
233+
println!("cargo:rustc-link-lib=static=freebl");
234+
}
232235
}
233236

234237
fn static_link() {
@@ -396,19 +399,57 @@ fn pkg_config() -> Result<Vec<String>, Box<dyn Error>> {
396399
let cfg_str = String::from_utf8(cfg)?;
397400

398401
let mut flags: Vec<String> = Vec::new();
402+
let mut lib_dirs: Vec<PathBuf> = Vec::new();
399403

400404
for f in cfg_str.split_whitespace() {
401405
if f.starts_with("-I") {
402406
flags.push(String::from(f));
403407
} else if let Some(path) = f.strip_prefix("-L") {
404408
println!("cargo:rustc-link-search=native={path}");
409+
lib_dirs.push(PathBuf::from(path));
405410
} else if let Some(lib) = f.strip_prefix("-l") {
406411
println!("cargo:rustc-link-lib=dylib={lib}");
407412
} else {
408413
println!("cargo:warning=Unknown flag from pkg-config: {f}");
409414
}
410415
}
411416

417+
if env::var("CARGO_FEATURE_BLAPI").is_ok() {
418+
// Probe for freebl in preference order:
419+
//
420+
// 1. libfreebl.a — static archive from a source build (e.g. Homebrew on macOS, or a
421+
// standalone NSS_DIR build). All internal symbols guaranteed present.
422+
//
423+
// 2. libfreeblpriv3 — some package managers (e.g. FreeBSD ports) separate the
424+
// private/internal freebl API into this library (it is what libsoftokn3 links against),
425+
// while libfreebl3 only exports the public API.
426+
//
427+
// 3. libfreebl3 — on other systems (NetBSD, Debian, …) libfreebl3 exports the internal API
428+
// directly and libfreeblpriv3 is absent.
429+
let has = |name: &str| lib_dirs.iter().any(|dir| dir.join(name).exists());
430+
let dylib_ext = if env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos") {
431+
"dylib"
432+
} else {
433+
"so"
434+
};
435+
let has_dylib = |stem: &str| has(&format!("{stem}.{dylib_ext}"));
436+
let link = [
437+
(has("libfreebl.a"), "static=freebl"),
438+
(has_dylib("libfreeblpriv3"), "dylib=freeblpriv3"),
439+
(has_dylib("libfreebl3"), "dylib=freebl3"),
440+
]
441+
.into_iter()
442+
.find_map(|(found, link)| found.then_some(link))
443+
.unwrap_or_else(|| {
444+
panic!(
445+
"blapi feature requires freebl: libfreebl.a, libfreeblpriv3, \
446+
and libfreebl3 were all absent from the pkg-config library \
447+
paths. Set NSS_DIR to a standalone NSS source build."
448+
)
449+
});
450+
println!("cargo:rustc-link-lib={link}");
451+
}
452+
412453
Ok(flags)
413454
}
414455

src/aead/mod.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,18 @@ use crate::{
2121
secstatus_to_res,
2222
};
2323

24+
#[cfg(all(feature = "blapi", feature = "disable-encryption"))]
25+
compile_error!("`blapi` and `disable-encryption` are mutually exclusive features");
26+
2427
#[cfg_attr(feature = "disable-encryption", path = "recprot_null.rs")]
25-
#[cfg_attr(not(feature = "disable-encryption"), path = "recprot.rs")]
28+
#[cfg_attr(
29+
all(not(feature = "disable-encryption"), feature = "blapi"),
30+
path = "recprot_blapi.rs"
31+
)]
32+
#[cfg_attr(
33+
all(not(feature = "disable-encryption"), not(feature = "blapi")),
34+
path = "recprot.rs"
35+
)]
2636
mod recprot;
2737

2838
/// All the nonces are the same length. Exploit that.

src/aead/recprot.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use std::{
1010
ptr::{null, null_mut},
1111
};
1212

13-
use super::{COUNTER_LEN, NONCE_LEN, TAG_LEN, c_int_len, xor_nonce};
13+
use super::{COUNTER_LEN, Mode, NONCE_LEN, TAG_LEN, c_int_len, xor_nonce};
1414
use crate::{
1515
Cipher, Error, Res, SECItemBorrowed, SymKey, Version,
1616
constants::{TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256},
@@ -133,7 +133,13 @@ impl RecordProtection {
133133
/// # Errors
134134
///
135135
/// Returns `Error` when the underlying crypto operations fail.
136-
pub fn new(version: Version, cipher: Cipher, secret: &SymKey, prefix: &str) -> Res<Self> {
136+
pub fn new(
137+
version: Version,
138+
cipher: Cipher,
139+
secret: &SymKey,
140+
prefix: &str,
141+
_mode: Mode,
142+
) -> Res<Self> {
137143
let (mech, key_len) = cipher_mech_and_key_len(cipher)?;
138144
let key = expand_label(
139145
version,
@@ -174,8 +180,8 @@ impl RecordProtection {
174180

175181
/// Get the expansion size (authentication tag length) for this AEAD.
176182
#[must_use]
177-
#[expect(clippy::missing_const_for_fn, clippy::unused_self)]
178-
pub fn expansion(&self) -> usize {
183+
#[expect(clippy::unused_self)]
184+
pub const fn expansion(&self) -> usize {
179185
TAG_LEN
180186
}
181187

0 commit comments

Comments
 (0)