Skip to content

Commit af943cf

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 af943cf

17 files changed

Lines changed: 897 additions & 220 deletions

File tree

.github/actions/check-android/action.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ runs:
4747
with:
4848
version: stable
4949
targets: ${{ inputs.target }}
50-
tools: cargo-ndk@^4
50+
tools: cargo-ndk@^4, cargo-hack
5151
token: ${{ inputs.github-token }}
5252

5353
- uses: mozilla/actions/nss@27cbe8fb5d338c2861b787e5de10410559065db1 # v1.1.3
@@ -62,7 +62,7 @@ runs:
6262
TARGET: ${{ startsWith(inputs.target, 'arm') && 'arm64-v8a' || inputs.target }}
6363
API_LEVEL: ${{ inputs.api-level }}
6464
WD: ${{ inputs.working-directory }}
65-
run: cd "$WD" && cargo ndk --platform "$API_LEVEL" --target "$TARGET" test --no-run
65+
run: cd "$WD" && cargo hack --feature-powerset --exclude-features gecko,blapi ndk --platform "$API_LEVEL" --target "$TARGET" test --no-run
6666

6767
# FIXME: Enable emulator testing for aarch64 once Google ships Android
6868
# emulator binaries for ARM64 Linux hosts.

.github/actions/check-vm/action.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,15 @@ runs:
102102
;;
103103
esac
104104
cargo fmt --all -- --check
105+
cargo install cargo-hack --locked
105106
case "$PLATFORM" in
106107
freebsd) cargo install cargo-llvm-cov --locked
107108
cargo llvm-cov test --locked --no-fail-fast --codecov --output-path codecov.json
109+
cargo hack test --locked --no-fail-fast --feature-powerset --exclude-features gecko,blapi
108110
;;
109111
*) # FIXME: No profiler support on other platforms, error is: cannot find crate for profiler_builtins
110112
cargo test --locked --no-fail-fast # We do this instead for now
113+
cargo hack test --locked --no-fail-fast --feature-powerset --exclude-features gecko,blapi
111114
;;
112115
esac
113116
cargo test --locked --no-fail-fast --release

.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 feature powerset
128+
run: |
129+
# shellcheck disable=SC2086
130+
cargo hack test $BUILD_TYPE --locked --feature-powerset --exclude-features gecko --mutually-exclusive-features blapi,disable-encryption
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: 23 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=dylib=freebl3");
234+
}
232235
}
233236

234237
fn static_link() {
@@ -396,19 +399,39 @@ 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+
let dylib_ext = if env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos") {
419+
"dylib"
420+
} else {
421+
"so"
422+
};
423+
if !lib_dirs
424+
.iter()
425+
.any(|dir| dir.join(format!("libfreebl3.{dylib_ext}")).exists())
426+
{
427+
panic!(
428+
"blapi feature requires libfreebl3.{dylib_ext} in the pkg-config \
429+
library paths. Set NSS_DIR to a standalone NSS source build."
430+
);
431+
}
432+
println!("cargo:rustc-link-lib=dylib=freebl3");
433+
}
434+
412435
Ok(flags)
413436
}
414437

src/aead/mod.rs

Lines changed: 164 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,144 @@
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

1317
use crate::{
14-
SECItemBorrowed, SymKey,
15-
err::{Error, Res},
18+
Cipher, SECItemBorrowed, SymKey,
19+
constants::{TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256},
20+
err::{Error, Res, sec::SEC_ERROR_BAD_DATA},
1621
p11::{
1722
self, CK_ATTRIBUTE_TYPE, CK_GENERATOR_FUNCTION, CK_MECHANISM_TYPE, CKA_DECRYPT,
1823
CKA_ENCRYPT, CKA_NSS_MESSAGE, CKG_GENERATE_COUNTER_XOR, CKG_NO_GENERATE, CKM_AES_GCM,
1924
CKM_CHACHA20_POLY1305, Context, PK11_AEADOp, PK11_CreateContextBySymKey,
2025
},
2126
secstatus_to_res,
2227
};
28+
#[cfg(not(feature = "disable-encryption"))]
29+
use crate::{
30+
Version,
31+
hp::SSL_HkdfExpandLabelWithMech,
32+
p11::{CKM_HKDF_DATA, PK11SymKey},
33+
};
34+
35+
#[cfg(all(feature = "blapi", feature = "disable-encryption"))]
36+
compile_error!("`blapi` and `disable-encryption` are mutually exclusive features");
37+
38+
/// Shared API contract for all `RecordProtection` backends.
39+
///
40+
/// Implemented by each cfg-selected `recprot*.rs` backend so that a
41+
/// signature change in one backend is caught at compile time across all.
42+
/// Import this trait to call AEAD methods on `RecordProtection`.
43+
pub trait RecordProtectionOps {
44+
/// Get the expansion size (authentication tag length) for this AEAD.
45+
#[must_use]
46+
fn expansion(&self) -> usize;
47+
48+
/// Encrypt plaintext with associated data.
49+
///
50+
/// # Errors
51+
///
52+
/// Returns `Error` when encryption fails.
53+
fn encrypt<'a>(
54+
&self,
55+
count: u64,
56+
aad: &[u8],
57+
input: &[u8],
58+
output: &'a mut [u8],
59+
) -> Res<&'a [u8]>;
60+
61+
/// Encrypt plaintext in place with associated data.
62+
///
63+
/// # Errors
64+
///
65+
/// Returns `Error` when encryption fails.
66+
fn encrypt_in_place(&self, count: u64, aad: &[u8], data: &mut [u8]) -> Res<usize>;
67+
68+
/// Decrypt ciphertext with associated data.
69+
///
70+
/// # Errors
71+
///
72+
/// Returns `Error` when decryption or authentication fails.
73+
fn decrypt<'a>(
74+
&self,
75+
count: u64,
76+
aad: &[u8],
77+
input: &[u8],
78+
output: &'a mut [u8],
79+
) -> Res<&'a [u8]>;
80+
81+
/// Decrypt ciphertext in place with associated data.
82+
///
83+
/// # Errors
84+
///
85+
/// Returns `Error` when decryption or authentication fails.
86+
fn decrypt_in_place(&self, count: u64, aad: &[u8], data: &mut [u8]) -> Res<usize>;
87+
}
2388

2489
#[cfg_attr(feature = "disable-encryption", path = "recprot_null.rs")]
25-
#[cfg_attr(not(feature = "disable-encryption"), path = "recprot.rs")]
90+
#[cfg_attr(
91+
all(not(feature = "disable-encryption"), feature = "blapi"),
92+
path = "recprot_blapi.rs"
93+
)]
94+
#[cfg_attr(
95+
all(not(feature = "disable-encryption"), not(feature = "blapi")),
96+
path = "recprot.rs"
97+
)]
2698
mod recprot;
2799

100+
#[cfg(not(feature = "disable-encryption"))]
101+
fn expand_label(
102+
version: Version,
103+
cipher: Cipher,
104+
secret: &SymKey,
105+
label: &str,
106+
mech: CK_MECHANISM_TYPE,
107+
key_len: c_uint,
108+
) -> Res<SymKey> {
109+
let mut ptr: *mut PK11SymKey = null_mut();
110+
unsafe {
111+
SSL_HkdfExpandLabelWithMech(
112+
version,
113+
cipher,
114+
**secret,
115+
null(),
116+
0,
117+
label.as_ptr().cast::<c_char>(),
118+
c_uint::try_from(label.len())?,
119+
mech,
120+
key_len,
121+
&raw mut ptr,
122+
)
123+
}?;
124+
SymKey::from_ptr(ptr)
125+
}
126+
127+
#[cfg(not(feature = "disable-encryption"))]
128+
fn expand_hkdf_label(
129+
version: Version,
130+
cipher: Cipher,
131+
secret: &SymKey,
132+
label: &str,
133+
key_len: c_uint,
134+
) -> Res<SymKey> {
135+
expand_label(
136+
version,
137+
cipher,
138+
secret,
139+
label,
140+
CK_MECHANISM_TYPE::from(CKM_HKDF_DATA),
141+
key_len,
142+
)
143+
}
144+
28145
/// All the nonces are the same length. Exploit that.
29146
pub const NONCE_LEN: usize = 12;
30147

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

166+
/// Split `data` into `(ct_len, tag)`, returning `SEC_ERROR_BAD_DATA` if it is
167+
/// too short to contain a tag.
168+
fn split_tag(data: &[u8]) -> Res<(usize, [u8; TAG_LEN])> {
169+
let ct_len = data
170+
.len()
171+
.checked_sub(TAG_LEN)
172+
.ok_or_else(|| Error::from(SEC_ERROR_BAD_DATA))?;
173+
let mut tag = [0u8; TAG_LEN];
174+
tag.copy_from_slice(&data[ct_len..]);
175+
Ok((ct_len, tag))
176+
}
177+
49178
pub type SequenceNumber = u64;
50179

51180
/// All the lengths used by `PK11_AEADOp` are signed. This converts to that.
@@ -82,20 +211,43 @@ pub enum AeadAlgorithms {
82211
ChaCha20Poly1305,
83212
}
84213

214+
impl AeadAlgorithms {
215+
#[must_use]
216+
pub const fn key_len(self) -> usize {
217+
match self {
218+
Self::Aes128Gcm => 16,
219+
Self::Aes256Gcm | Self::ChaCha20Poly1305 => 32,
220+
}
221+
}
222+
223+
#[must_use]
224+
pub fn p11_mech(self) -> CK_MECHANISM_TYPE {
225+
CK_MECHANISM_TYPE::from(match self {
226+
Self::Aes128Gcm | Self::Aes256Gcm => CKM_AES_GCM,
227+
Self::ChaCha20Poly1305 => CKM_CHACHA20_POLY1305,
228+
})
229+
}
230+
}
231+
232+
impl TryFrom<Cipher> for AeadAlgorithms {
233+
type Error = Error;
234+
fn try_from(cipher: Cipher) -> Res<Self> {
235+
match cipher {
236+
TLS_AES_128_GCM_SHA256 => Ok(Self::Aes128Gcm),
237+
TLS_AES_256_GCM_SHA384 => Ok(Self::Aes256Gcm),
238+
TLS_CHACHA20_POLY1305_SHA256 => Ok(Self::ChaCha20Poly1305),
239+
_ => Err(Error::UnsupportedCipher),
240+
}
241+
}
242+
}
243+
85244
pub struct Aead {
86245
mode: Mode,
87246
ctx: Context,
88247
nonce_base: [u8; NONCE_LEN],
89248
}
90249

91250
impl Aead {
92-
fn mech(algorithm: AeadAlgorithms) -> CK_MECHANISM_TYPE {
93-
CK_MECHANISM_TYPE::from(match algorithm {
94-
AeadAlgorithms::Aes128Gcm | AeadAlgorithms::Aes256Gcm => CKM_AES_GCM,
95-
AeadAlgorithms::ChaCha20Poly1305 => CKM_CHACHA20_POLY1305,
96-
})
97-
}
98-
99251
pub fn import_key(algorithm: AeadAlgorithms, key: &[u8]) -> Result<SymKey, Error> {
100252
let slot = p11::Slot::internal().map_err(|_| Error::Internal)?;
101253

@@ -105,7 +257,7 @@ impl Aead {
105257
let ptr = unsafe {
106258
p11::PK11_ImportSymKey(
107259
*slot,
108-
Self::mech(algorithm),
260+
algorithm.p11_mech(),
109261
p11::PK11Origin::PK11_OriginUnwrap,
110262
CK_ATTRIBUTE_TYPE::from(CKA_ENCRYPT | CKA_DECRYPT),
111263
key_item_ptr,
@@ -125,7 +277,7 @@ impl Aead {
125277

126278
let ptr = unsafe {
127279
PK11_CreateContextBySymKey(
128-
Self::mech(algorithm),
280+
algorithm.p11_mech(),
129281
mode.p11mode(),
130282
**key,
131283
SECItemBorrowed::wrap(&nonce_base[..])?.as_ref(),

0 commit comments

Comments
 (0)