Skip to content

Commit a8633f4

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 07994e8 commit a8633f4

15 files changed

Lines changed: 792 additions & 196 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.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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.11.0"
3+
version = "0.12.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
disable-encryption = []
128131
disable-random = []
129132
gecko = ["mozbuild"]

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: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,21 @@
44
// option. This file may not be copied, modified, or distributed
55
// except according to those terms.
66

7+
#[cfg(not(feature = "disable-encryption"))]
8+
use std::os::raw::{c_char, c_uint};
9+
#[cfg(not(feature = "disable-encryption"))]
10+
use std::ptr::null;
711
use std::{os::raw::c_int, ptr::null_mut};
812

913
#[cfg(feature = "disable-encryption")]
1014
pub use recprot::AEAD_NULL_TAG;
1115
pub use recprot::RecordProtection;
1216

17+
#[cfg(not(feature = "disable-encryption"))]
18+
use crate::{Cipher, Version, hp::SSL_HkdfExpandLabelWithMech, p11::PK11SymKey};
1319
use crate::{
1420
SECItemBorrowed, SymKey,
15-
err::{Error, Res},
21+
err::{Error, Res, sec::SEC_ERROR_BAD_DATA},
1622
p11::{
1723
self, CK_ATTRIBUTE_TYPE, CK_GENERATOR_FUNCTION, CK_MECHANISM_TYPE, CKA_DECRYPT,
1824
CKA_ENCRYPT, CKA_NSS_MESSAGE, CKG_GENERATE_COUNTER_XOR, CKG_NO_GENERATE, CKM_AES_GCM,
@@ -21,10 +27,98 @@ use crate::{
2127
secstatus_to_res,
2228
};
2329

30+
#[cfg(all(feature = "blapi", feature = "disable-encryption"))]
31+
compile_error!("`blapi` and `disable-encryption` are mutually exclusive features");
32+
33+
/// Shared API contract for all `RecordProtection` backends.
34+
///
35+
/// Implemented by each cfg-selected `recprot*.rs` backend so that a
36+
/// signature change in one backend is caught at compile time across all.
37+
/// Import this trait to call AEAD methods on `RecordProtection`.
38+
pub trait RecordProtectionOps {
39+
/// Get the expansion size (authentication tag length) for this AEAD.
40+
#[must_use]
41+
fn expansion(&self) -> usize;
42+
43+
/// Encrypt plaintext with associated data.
44+
///
45+
/// # Errors
46+
///
47+
/// Returns `Error` when encryption fails.
48+
fn encrypt<'a>(
49+
&self,
50+
count: u64,
51+
aad: &[u8],
52+
input: &[u8],
53+
output: &'a mut [u8],
54+
) -> Res<&'a [u8]>;
55+
56+
/// Encrypt plaintext in place with associated data.
57+
///
58+
/// # Errors
59+
///
60+
/// Returns `Error` when encryption fails.
61+
fn encrypt_in_place(&self, count: u64, aad: &[u8], data: &mut [u8]) -> Res<usize>;
62+
63+
/// Decrypt ciphertext with associated data.
64+
///
65+
/// # Errors
66+
///
67+
/// Returns `Error` when decryption or authentication fails.
68+
fn decrypt<'a>(
69+
&self,
70+
count: u64,
71+
aad: &[u8],
72+
input: &[u8],
73+
output: &'a mut [u8],
74+
) -> Res<&'a [u8]>;
75+
76+
/// Decrypt ciphertext in place with associated data.
77+
///
78+
/// # Errors
79+
///
80+
/// Returns `Error` when decryption or authentication fails.
81+
fn decrypt_in_place(&self, count: u64, aad: &[u8], data: &mut [u8]) -> Res<usize>;
82+
}
83+
2484
#[cfg_attr(feature = "disable-encryption", path = "recprot_null.rs")]
25-
#[cfg_attr(not(feature = "disable-encryption"), path = "recprot.rs")]
85+
#[cfg_attr(
86+
all(not(feature = "disable-encryption"), feature = "blapi"),
87+
path = "recprot_blapi.rs"
88+
)]
89+
#[cfg_attr(
90+
all(not(feature = "disable-encryption"), not(feature = "blapi")),
91+
path = "recprot.rs"
92+
)]
2693
mod recprot;
2794

95+
#[cfg(not(feature = "disable-encryption"))]
96+
fn expand_label(
97+
version: Version,
98+
cipher: Cipher,
99+
secret: &SymKey,
100+
label: &str,
101+
mech: CK_MECHANISM_TYPE,
102+
key_len: c_uint,
103+
) -> Res<SymKey> {
104+
let mut ptr: *mut PK11SymKey = null_mut();
105+
unsafe {
106+
SSL_HkdfExpandLabelWithMech(
107+
version,
108+
cipher,
109+
**secret,
110+
null(),
111+
0,
112+
label.as_ptr().cast::<c_char>(),
113+
c_uint::try_from(label.len())?,
114+
mech,
115+
key_len,
116+
&raw mut ptr,
117+
)
118+
}?;
119+
SymKey::from_ptr(ptr)
120+
}
121+
28122
/// All the nonces are the same length. Exploit that.
29123
pub const NONCE_LEN: usize = 12;
30124

@@ -46,6 +140,18 @@ fn xor_nonce(base: &[u8; NONCE_LEN], count: SequenceNumber) -> [u8; NONCE_LEN] {
46140
/// All of the AEAD functions here have a tag of this length, so use a fixed offset.
47141
const TAG_LEN: usize = 16;
48142

143+
/// Split `data` into `(ct_len, tag)`, returning `SEC_ERROR_BAD_DATA` if it is
144+
/// too short to contain a tag.
145+
fn split_tag(data: &[u8]) -> Res<(usize, [u8; TAG_LEN])> {
146+
let ct_len = data
147+
.len()
148+
.checked_sub(TAG_LEN)
149+
.ok_or_else(|| Error::from(SEC_ERROR_BAD_DATA))?;
150+
let mut tag = [0u8; TAG_LEN];
151+
tag.copy_from_slice(&data[ct_len..]);
152+
Ok((ct_len, tag))
153+
}
154+
49155
pub type SequenceNumber = u64;
50156

51157
/// All the lengths used by `PK11_AEADOp` are signed. This converts to that.

0 commit comments

Comments
 (0)