Skip to content

Commit 23c6f5d

Browse files
committed
feat: add blapi feature to bypass PKCS#11 in RecordProtection
Add `--features blapi` which replaces `RecordProtection`'s hot path from `PK11_AEADOp` (PKCS#11 → softoken → freebl) to direct freebl calls (`AES_AEAD`, `ChaCha20Poly1305_Encrypt/Decrypt`), eliminating the substantial `sftk_SessionFromHandle` mutex and hash-table overheads.
1 parent 0ea3df7 commit 23c6f5d

5 files changed

Lines changed: 478 additions & 7 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ verbose_file_reads = "warn"
124124

125125
[features]
126126
bench = ["log/release_max_level_info"]
127+
blapi = []
127128
deny-warnings = []
128129
disable-encryption = []
129130
disable-random = []

build.rs

Lines changed: 6 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+
// freebl loader stub: routes AES_AEAD / ChaCha20Poly1305_* to libfreebl3
233+
// at runtime, bypassing the PKCS#11 session overhead.
234+
println!("cargo:rustc-link-lib=static=freebl");
232235
}
233236

234237
fn static_link() {
@@ -409,6 +412,9 @@ fn pkg_config() -> Result<Vec<String>, Box<dyn Error>> {
409412
}
410413
}
411414

415+
// freebl loader stub: routes AES_AEAD / ChaCha20Poly1305_* to libfreebl3.
416+
println!("cargo:rustc-link-lib=static=freebl");
417+
412418
Ok(flags)
413419
}
414420

src/aead.rs

Lines changed: 335 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use crate::{
1919
secstatus_to_res,
2020
};
2121

22-
#[cfg(not(feature = "disable-encryption"))]
22+
#[cfg(all(not(feature = "disable-encryption"), not(feature = "blapi")))]
2323
mod recprot {
2424
use std::{
2525
fmt,
@@ -238,6 +238,328 @@ mod recprot {
238238
}
239239
}
240240

241+
#[cfg(all(not(feature = "disable-encryption"), feature = "blapi"))]
242+
mod recprot {
243+
use std::{
244+
fmt,
245+
os::raw::{c_char, c_int, c_uint, c_ulong},
246+
ptr::{null, null_mut},
247+
};
248+
249+
use crate::{
250+
Cipher, Error, Res, SymKey, Version,
251+
constants::{TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256},
252+
err::{sec::SEC_ERROR_BAD_DATA, secstatus_to_res},
253+
freebl::{self, AesCtx, ChaCha20Ctx},
254+
hp::SSL_HkdfExpandLabelWithMech,
255+
p11::{CK_MECHANISM_TYPE, CKM_HKDF_DATA, PK11SymKey},
256+
};
257+
258+
// Compile-time conversions of module constants to C types used in every
259+
// freebl call — avoids repeated infallible try_from at each call site.
260+
#[expect(clippy::cast_possible_truncation, reason = "NONCE_LEN = 12 and TAG_LEN = 16 both fit in u32")]
261+
const NONCE_LEN_C: c_uint = super::NONCE_LEN as c_uint;
262+
#[expect(clippy::cast_possible_truncation, reason = "NONCE_LEN = 12 and TAG_LEN = 16 both fit in u32")]
263+
const TAG_LEN_C: c_uint = super::TAG_LEN as c_uint;
264+
const NONCE_LEN_UL: c_ulong = super::NONCE_LEN as c_ulong;
265+
const TAG_BITS_UL: c_ulong = (super::TAG_LEN * 8) as c_ulong;
266+
#[expect(
267+
clippy::cast_possible_truncation,
268+
reason = "CK_GCM_MESSAGE_PARAMS is a small fixed struct"
269+
)]
270+
const GCM_PARAMS_LEN_C: c_uint = size_of::<freebl::CK_GCM_MESSAGE_PARAMS>() as c_uint;
271+
272+
enum CipherSpec {
273+
Aes(c_uint), // AES-GCM with this key length
274+
ChaCha(c_uint), // ChaCha20-Poly1305 with this key length (always 32)
275+
}
276+
277+
impl CipherSpec {
278+
const fn key_len(&self) -> c_uint {
279+
match self {
280+
Self::Aes(n) | Self::ChaCha(n) => *n,
281+
}
282+
}
283+
}
284+
285+
const fn cipher_spec(cipher: Cipher) -> Res<CipherSpec> {
286+
match cipher {
287+
TLS_AES_128_GCM_SHA256 => Ok(CipherSpec::Aes(16)),
288+
TLS_AES_256_GCM_SHA384 => Ok(CipherSpec::Aes(32)),
289+
TLS_CHACHA20_POLY1305_SHA256 => Ok(CipherSpec::ChaCha(32)),
290+
_ => Err(Error::UnsupportedCipher),
291+
}
292+
}
293+
294+
fn expand_label(
295+
version: Version,
296+
cipher: Cipher,
297+
secret: &SymKey,
298+
label: &str,
299+
key_len: c_uint,
300+
) -> Res<SymKey> {
301+
let mut ptr: *mut PK11SymKey = null_mut();
302+
unsafe {
303+
SSL_HkdfExpandLabelWithMech(
304+
version,
305+
cipher,
306+
**secret,
307+
null(),
308+
0,
309+
label.as_ptr().cast::<c_char>(),
310+
c_uint::try_from(label.len())?,
311+
CK_MECHANISM_TYPE::from(CKM_HKDF_DATA),
312+
key_len,
313+
&raw mut ptr,
314+
)
315+
}?;
316+
SymKey::from_ptr(ptr)
317+
}
318+
319+
enum RecordCipher {
320+
Aes {
321+
ctx_encrypt: AesCtx,
322+
ctx_decrypt: AesCtx,
323+
},
324+
ChaCha(ChaCha20Ctx),
325+
}
326+
327+
/// Dispatch an AEAD operation to the appropriate freebl primitive.
328+
///
329+
/// # Safety
330+
///
331+
/// `output`, `tag`, and `input` must be valid for `output_max`, `TAG_LEN`,
332+
/// and `input_len` bytes respectively. `output` and `input` may overlap
333+
/// (in-place); `tag` must not overlap the `output` region.
334+
#[expect(clippy::too_many_arguments, reason = "Thin wrapper over two 10-argument C functions.")]
335+
unsafe fn aead_op(
336+
cipher: &RecordCipher,
337+
encrypt: bool,
338+
nonce: &mut [u8; super::NONCE_LEN],
339+
aad: &[u8],
340+
output: *mut u8,
341+
output_max: c_uint,
342+
tag: *mut u8,
343+
input: *const u8,
344+
input_len: c_uint,
345+
) -> Res<usize> {
346+
let mut out_len: c_uint = 0;
347+
let aad_len = c_uint::try_from(aad.len())?;
348+
match cipher {
349+
RecordCipher::Aes { ctx_encrypt, ctx_decrypt } => {
350+
let ctx = if encrypt { ctx_encrypt } else { ctx_decrypt };
351+
let mut params = freebl::CK_GCM_MESSAGE_PARAMS {
352+
pIv: nonce.as_mut_ptr(),
353+
ulIvLen: NONCE_LEN_UL,
354+
ulIvFixedBits: 0,
355+
ivGenerator: 0,
356+
pTag: tag,
357+
ulTagBits: TAG_BITS_UL,
358+
};
359+
secstatus_to_res(unsafe {
360+
freebl::AES_AEAD(
361+
**ctx,
362+
output,
363+
&raw mut out_len,
364+
output_max,
365+
input,
366+
input_len,
367+
std::ptr::from_mut(&mut params).cast(),
368+
GCM_PARAMS_LEN_C,
369+
aad.as_ptr(),
370+
aad_len,
371+
)
372+
})?;
373+
}
374+
RecordCipher::ChaCha(ctx) => {
375+
let f = if encrypt {
376+
freebl::ChaCha20Poly1305_Encrypt
377+
} else {
378+
freebl::ChaCha20Poly1305_Decrypt
379+
};
380+
secstatus_to_res(unsafe {
381+
f(
382+
**ctx, output, &raw mut out_len, output_max,
383+
input, input_len, nonce.as_ptr(), NONCE_LEN_C,
384+
aad.as_ptr(), aad_len, tag,
385+
)
386+
})?;
387+
}
388+
}
389+
Ok(usize::try_from(out_len)?)
390+
}
391+
392+
pub struct RecordProtection {
393+
cipher: RecordCipher,
394+
nonce_base: [u8; super::NONCE_LEN],
395+
}
396+
397+
impl RecordProtection {
398+
/// Create a new AEAD instance.
399+
///
400+
/// # Errors
401+
///
402+
/// Returns `Error` when the underlying crypto operations fail.
403+
pub fn new(version: Version, cipher: Cipher, secret: &SymKey, prefix: &str) -> Res<Self> {
404+
let spec = cipher_spec(cipher)?;
405+
// Closure captures version/cipher/secret; binds result so key_bytes borrow stays live.
406+
let derive = |suffix, len| {
407+
expand_label(version, cipher, secret, &format!("{prefix}{suffix}"), len)
408+
};
409+
let key_sym = derive("key", spec.key_len())?;
410+
let key_bytes = key_sym.key_data()?;
411+
412+
let nonce_base: [u8; super::NONCE_LEN] = derive("iv", NONCE_LEN_C)?
413+
.key_data()?
414+
.try_into()
415+
.map_err(|_| Error::Internal)?;
416+
417+
let record_cipher = match spec {
418+
CipherSpec::ChaCha(key_len) => RecordCipher::ChaCha(ChaCha20Ctx::from_ptr(unsafe {
419+
freebl::ChaCha20Poly1305_CreateContext(key_bytes.as_ptr(), key_len, TAG_LEN_C)
420+
})?),
421+
CipherSpec::Aes(key_len) => {
422+
let make_aes_ctx = |encrypt: c_int| unsafe {
423+
freebl::AES_CreateContext(key_bytes.as_ptr(), null(), freebl::NSS_AES_GCM, encrypt, key_len, 16)
424+
};
425+
RecordCipher::Aes {
426+
ctx_encrypt: AesCtx::from_ptr(make_aes_ctx(1))?,
427+
ctx_decrypt: AesCtx::from_ptr(make_aes_ctx(0))?,
428+
}
429+
}
430+
};
431+
432+
Ok(Self { cipher: record_cipher, nonce_base })
433+
}
434+
435+
/// Get the expansion size (authentication tag length) for this AEAD.
436+
#[must_use]
437+
#[expect(clippy::unused_self)]
438+
pub const fn expansion(&self) -> usize {
439+
super::TAG_LEN
440+
}
441+
442+
/// Encrypt plaintext with associated data.
443+
///
444+
/// # Errors
445+
///
446+
/// Returns `Error` when encryption fails.
447+
pub fn encrypt<'a>(
448+
&self,
449+
count: u64,
450+
aad: &[u8],
451+
input: &[u8],
452+
output: &'a mut [u8],
453+
) -> Res<&'a [u8]> {
454+
if output.len() < input.len().checked_add(super::TAG_LEN).ok_or(Error::IntegerOverflow)? {
455+
return Err(Error::from(SEC_ERROR_BAD_DATA));
456+
}
457+
let mut nonce = super::xor_nonce(&self.nonce_base, count);
458+
let input_len = c_uint::try_from(input.len())?;
459+
let out_len = unsafe {
460+
aead_op(
461+
&self.cipher, true, &mut nonce, aad,
462+
output.as_mut_ptr(), input_len,
463+
output[input.len()..].as_mut_ptr(),
464+
input.as_ptr(), input_len,
465+
)
466+
}?;
467+
debug_assert_eq!(out_len, input.len());
468+
Ok(&output[..out_len + super::TAG_LEN])
469+
}
470+
471+
/// Encrypt plaintext in place with associated data.
472+
///
473+
/// # Errors
474+
///
475+
/// Returns `Error` when encryption fails.
476+
pub fn encrypt_in_place(&self, count: u64, aad: &[u8], data: &mut [u8]) -> Res<usize> {
477+
if data.len() < self.expansion() {
478+
return Err(Error::from(SEC_ERROR_BAD_DATA));
479+
}
480+
let pt_len = data.len() - self.expansion();
481+
let mut nonce = super::xor_nonce(&self.nonce_base, count);
482+
let data_ptr = data.as_mut_ptr();
483+
let pt_len_c = c_uint::try_from(pt_len)?;
484+
let out_len = unsafe {
485+
aead_op(
486+
&self.cipher, true, &mut nonce, aad,
487+
data_ptr, pt_len_c,
488+
data_ptr.add(pt_len),
489+
data_ptr.cast_const(), pt_len_c,
490+
)
491+
}?;
492+
debug_assert_eq!(out_len, pt_len);
493+
Ok(data.len())
494+
}
495+
496+
/// Decrypt ciphertext with associated data.
497+
///
498+
/// # Errors
499+
///
500+
/// Returns `Error` when decryption or authentication fails.
501+
pub fn decrypt<'a>(
502+
&self,
503+
count: u64,
504+
aad: &[u8],
505+
input: &[u8],
506+
output: &'a mut [u8],
507+
) -> Res<&'a [u8]> {
508+
let ct_len = input
509+
.len()
510+
.checked_sub(super::TAG_LEN)
511+
.ok_or_else(|| Error::from(SEC_ERROR_BAD_DATA))?;
512+
if output.len() < ct_len {
513+
return Err(Error::from(SEC_ERROR_BAD_DATA));
514+
}
515+
let mut tag = [0u8; super::TAG_LEN];
516+
tag.copy_from_slice(&input[ct_len..ct_len + super::TAG_LEN]);
517+
let mut nonce = super::xor_nonce(&self.nonce_base, count);
518+
let out_len = unsafe {
519+
aead_op(
520+
&self.cipher, false, &mut nonce, aad,
521+
output.as_mut_ptr(), c_uint::try_from(output.len())?,
522+
tag.as_mut_ptr(),
523+
input.as_ptr(), c_uint::try_from(ct_len)?,
524+
)
525+
}?;
526+
Ok(&output[..out_len])
527+
}
528+
529+
/// Decrypt ciphertext in place with associated data.
530+
///
531+
/// # Errors
532+
///
533+
/// Returns `Error` when decryption or authentication fails.
534+
pub fn decrypt_in_place(&self, count: u64, aad: &[u8], data: &mut [u8]) -> Res<usize> {
535+
let ct_len = data
536+
.len()
537+
.checked_sub(super::TAG_LEN)
538+
.ok_or_else(|| Error::from(SEC_ERROR_BAD_DATA))?;
539+
let mut tag = [0u8; super::TAG_LEN];
540+
tag.copy_from_slice(&data[ct_len..ct_len + super::TAG_LEN]);
541+
let mut nonce = super::xor_nonce(&self.nonce_base, count);
542+
let data_ptr = data.as_mut_ptr();
543+
let out_len = unsafe {
544+
aead_op(
545+
&self.cipher, false, &mut nonce, aad,
546+
data_ptr, c_uint::try_from(data.len())?,
547+
tag.as_mut_ptr(),
548+
data_ptr.cast_const(), c_uint::try_from(ct_len)?,
549+
)
550+
}?;
551+
debug_assert_eq!(out_len, ct_len);
552+
Ok(out_len)
553+
}
554+
}
555+
556+
impl fmt::Debug for RecordProtection {
557+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
558+
write!(f, "[AEAD Context]")
559+
}
560+
}
561+
}
562+
241563
#[cfg(feature = "disable-encryption")]
242564
mod recprot {
243565
use std::fmt;
@@ -464,6 +786,17 @@ const TAG_LEN: usize = 16;
464786

465787
pub type SequenceNumber = u64;
466788

789+
fn xor_nonce(base: &[u8; NONCE_LEN], count: SequenceNumber) -> [u8; NONCE_LEN] {
790+
let mut nonce = *base;
791+
for (n, &s) in nonce[NONCE_LEN - COUNTER_LEN..]
792+
.iter_mut()
793+
.zip(&count.to_be_bytes())
794+
{
795+
*n ^= s;
796+
}
797+
nonce
798+
}
799+
467800
/// All the lengths used by `PK11_AEADOp` are signed. This converts to that.
468801
fn c_int_len<T>(l: T) -> Res<c_int>
469802
where
@@ -513,12 +846,7 @@ impl Aead {
513846
}
514847

515848
fn make_nonce(nonce: &mut [u8; NONCE_LEN], seq: SequenceNumber) {
516-
for (n, &s) in nonce[NONCE_LEN - COUNTER_LEN..]
517-
.iter_mut()
518-
.zip(&seq.to_be_bytes())
519-
{
520-
*n ^= s;
521-
}
849+
*nonce = xor_nonce(nonce, seq);
522850
}
523851

524852
pub fn import_key(algorithm: AeadAlgorithms, key: &[u8]) -> Result<SymKey, Error> {

0 commit comments

Comments
 (0)