From aac3e37289bc44be6ba71b15f77e9b6c0a54aac9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20J=C3=B6rg=20Schmidt?= Date: Fri, 3 Jan 2025 09:44:07 +0100 Subject: [PATCH] support keydb feature This adds a feature `keydb` to rc_crypto's nss create, which enables the `ensure_initialized_with_profile_dir` initialize function. This configures NSS to use a profile and persist keys into key4.db. Also adding methods for managing AES256 keys with NSS: * `authentication_with_primary_password_is_needed`: check wheather primary password is enabled * `authenticate_with_primary_password`: authenticate with primary password against NSS key database * `get_or_create_aes256_key`: retrieve a key from key4.db or, if not present, create one --- CHANGELOG.md | 5 + Cargo.lock | 1 + components/support/rc_crypto/nss/Cargo.toml | 2 + .../nss/nss_sys/src/bindings/pk11pub.rs | 1 + components/support/rc_crypto/nss/src/error.rs | 8 +- components/support/rc_crypto/nss/src/lib.rs | 3 + .../support/rc_crypto/nss/src/pk11/slot.rs | 8 + .../support/rc_crypto/nss/src/pk11/sym_key.rs | 198 +++++++++++++++++- components/support/rc_crypto/nss/src/util.rs | 130 ++++++++++++ 9 files changed, 352 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5177c683f8..b47a713563 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ### `rc_crypto` - New low level bindings for dealing with primary password. +- New feature flag `keydb` in `rc_crypto/nss`, which enables NSS key persistence: `ensure_initialized_with_profile_dir(path: impl AsRef)` initializes NSS with a profile directory and appropriate flags to persist keys (and certificates) in its internal PKCS11 software implementation. This function must be called first; if `ensure_initialized` is called before, it will fail. +- New methods for dealing with primary password and key persistence, available within the `keydb` feature: + * `authentication_with_primary_password_is_needed()`: checks whether a primary password is set and needs to be authenticated + * `authenticate_with_primary_password(primary_password: &str)`: method for authenticate NSS key store against a user-provided primary password + * `get_or_create_aes256_key(name: &str)`: retrieve a key by `name` from the internal NSS key store. If none exists, create one, persist, and return. [Full Changelog](In progress) diff --git a/Cargo.lock b/Cargo.lock index 44e642388c..6919fb70bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3221,6 +3221,7 @@ dependencies = [ "base64 0.21.2", "error-support", "nss_sys", + "once_cell", "serde", "serde_derive", "thiserror 1.0.31", diff --git a/components/support/rc_crypto/nss/Cargo.toml b/components/support/rc_crypto/nss/Cargo.toml index a557cbbabc..ccf143eeaf 100644 --- a/components/support/rc_crypto/nss/Cargo.toml +++ b/components/support/rc_crypto/nss/Cargo.toml @@ -15,7 +15,9 @@ error-support = { path = "../../error" } nss_sys = { path = "nss_sys" } serde = "1" serde_derive = "1" +once_cell = { version = "1.20.2", optional = true } [features] default = [] gecko = ["nss_sys/gecko"] +keydb = ["dep:once_cell"] diff --git a/components/support/rc_crypto/nss/nss_sys/src/bindings/pk11pub.rs b/components/support/rc_crypto/nss/nss_sys/src/bindings/pk11pub.rs index 3083596d4b..5b385a7cd8 100644 --- a/components/support/rc_crypto/nss/nss_sys/src/bindings/pk11pub.rs +++ b/components/support/rc_crypto/nss/nss_sys/src/bindings/pk11pub.rs @@ -11,6 +11,7 @@ extern "C" { pub fn PK11_GetInternalKeySlot() -> *mut PK11SlotInfo; pub fn PK11_NeedUserInit(slot: *mut PK11SlotInfo) -> PRBool; pub fn PK11_NeedLogin(slot: *mut PK11SlotInfo) -> PRBool; + pub fn PK11_IsLoggedIn(slot: *mut PK11SlotInfo, wincx: *mut c_void) -> PRBool; pub fn PK11_CheckUserPassword(slot: *mut PK11SlotInfo, password: *const c_char) -> SECStatus; pub fn PK11_GenerateRandom(data: *mut c_uchar, len: c_int) -> SECStatus; pub fn PK11_FreeSymKey(key: *mut PK11SymKey); diff --git a/components/support/rc_crypto/nss/src/error.rs b/components/support/rc_crypto/nss/src/error.rs index f3e758f589..d22ee5c9f1 100644 --- a/components/support/rc_crypto/nss/src/error.rs +++ b/components/support/rc_crypto/nss/src/error.rs @@ -4,8 +4,8 @@ #[derive(Debug, thiserror::Error)] pub enum ErrorKind { - #[error("NSS could not be initialized")] - NSSInitFailure, + #[error("NSS could not be initialized: {0}")] + NSSInitFailure(String), #[error("NSS error: {0} {1}")] NSSError(i32, String), #[error("SSL error: {0} {1}")] @@ -16,6 +16,10 @@ pub enum ErrorKind { InputError(String), #[error("Internal crypto error")] InternalError, + #[error("invalid key length")] + InvalidKeyLength, + #[error("Interior nul byte was found")] + NulError, #[error("Conversion error: {0}")] ConversionError(#[from] std::num::TryFromIntError), #[error("Base64 decode error: {0}")] diff --git a/components/support/rc_crypto/nss/src/lib.rs b/components/support/rc_crypto/nss/src/lib.rs index 923c4b5080..d41bc0ad3a 100644 --- a/components/support/rc_crypto/nss/src/lib.rs +++ b/components/support/rc_crypto/nss/src/lib.rs @@ -17,3 +17,6 @@ pub mod pkixc; pub mod secport; pub use crate::error::{Error, ErrorKind, Result}; pub use util::ensure_nss_initialized as ensure_initialized; + +#[cfg(feature = "keydb")] +pub use util::ensure_nss_initialized_with_profile_dir as ensure_initialized_with_profile_dir; diff --git a/components/support/rc_crypto/nss/src/pk11/slot.rs b/components/support/rc_crypto/nss/src/pk11/slot.rs index b04315b45e..cf83ff3c0d 100644 --- a/components/support/rc_crypto/nss/src/pk11/slot.rs +++ b/components/support/rc_crypto/nss/src/pk11/slot.rs @@ -22,3 +22,11 @@ pub fn generate_random(data: &mut [u8]) -> Result<()> { pub(crate) fn get_internal_slot() -> Result { unsafe { Slot::from_ptr(nss_sys::PK11_GetInternalSlot()) } } + +/// Safe wrapper around `PK11_GetInternalKeySlot` that +/// de-allocates memory when the slot goes out of +/// scope. +#[cfg(feature = "keydb")] +pub(crate) fn get_internal_key_slot() -> Result { + unsafe { Slot::from_ptr(nss_sys::PK11_GetInternalKeySlot()) } +} diff --git a/components/support/rc_crypto/nss/src/pk11/sym_key.rs b/components/support/rc_crypto/nss/src/pk11/sym_key.rs index f67dbde556..2879610cad 100644 --- a/components/support/rc_crypto/nss/src/pk11/sym_key.rs +++ b/components/support/rc_crypto/nss/src/pk11/sym_key.rs @@ -2,11 +2,15 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#[cfg(feature = "keydb")] +use crate::util::get_last_error; use crate::{ error::*, pk11::{context::HashAlgorithm, slot, types::SymKey}, util::{ensure_nss_initialized, map_nss_secstatus, sec_item_as_slice, ScopedPtr}, }; +#[cfg(feature = "keydb")] +use std::ffi::{c_char, CString}; use std::{ mem, os::raw::{c_uchar, c_uint, c_ulong}, @@ -54,13 +58,17 @@ pub fn hkdf_expand( )? }; map_nss_secstatus(|| unsafe { nss_sys::PK11_ExtractKeyValue(sym_key.as_mut_ptr()) })?; - // This doesn't leak, because the SECItem* returned by PK11_GetKeyData - // just refers to a buffer managed by `sym_key` which we copy into `out`. let mut key_data = unsafe { *nss_sys::PK11_GetKeyData(sym_key.as_mut_ptr()) }; if u32::try_from(len)? > key_data.len { return Err(ErrorKind::InternalError.into()); } + + // # Safety + // + // This doesn't leak, because the SECItem* returned by PK11_GetKeyData + // just refers to a buffer managed by `sym_key` which we copy into `out`. let buf = unsafe { sec_item_as_slice(&mut key_data)? }; + Ok(buf.to_vec()) } @@ -90,3 +98,189 @@ pub(crate) fn import_sym_key( )) } } + +/// Check weather a primary password has been set and NSS needs to be authenticated. +/// Only available with the `keydb` feature. +#[cfg(feature = "keydb")] +pub fn authentication_with_primary_password_is_needed() -> Result { + let slot = slot::get_internal_key_slot()?; + + unsafe { + Ok( + nss_sys::PK11_NeedLogin(slot.as_mut_ptr()) == nss_sys::PR_TRUE + && nss_sys::PK11_IsLoggedIn(slot.as_mut_ptr(), ptr::null_mut()) != nss_sys::PR_TRUE, + ) + } +} + +/// Authorize NSS key store against a user-provided primary password. +/// Only available with the `keydb` feature. +#[cfg(feature = "keydb")] +pub fn authenticate_with_primary_password(primary_password: &str) -> Result { + let slot = slot::get_internal_key_slot()?; + + let password_cstr = CString::new(primary_password).map_err(|_| ErrorKind::NulError)?; + unsafe { + Ok( + nss_sys::PK11_CheckUserPassword(slot.as_mut_ptr(), password_cstr.as_ptr()) + == nss_sys::SECStatus::SECSuccess, + ) + } +} + +/// Retrieve a key, identified by `name`, from the internal NSS key store. If none exists, create +/// one, persist, and return. +/// Only available with the `keydb` feature. +#[cfg(feature = "keydb")] +pub fn get_or_create_aes256_key(name: &str) -> Result> { + let sym_key = match get_aes256_key(name) { + Ok(sym_key) => sym_key, + Err(_) => create_aes256_key(name)?, + }; + + let mut key_data = unsafe { *nss_sys::PK11_GetKeyData(sym_key.as_mut_ptr()) }; + if key_data.len != nss_sys::AES_256_KEY_LENGTH { + return Err(ErrorKind::InvalidKeyLength.into()); + } + + // # Safety + // + // This doesn't leak, because the SECItem* returned by PK11_GetKeyData + // just refers to a buffer managed by `sym_key` which we copy into `out`. + let buf = unsafe { sec_item_as_slice(&mut key_data)? }; + + Ok(buf.to_vec()) +} + +#[cfg(feature = "keydb")] +fn get_aes256_key(name: &str) -> Result { + let slot = slot::get_internal_key_slot()?; + let name = CString::new(name).map_err(|_| ErrorKind::NulError)?; + let sym_key = unsafe { + SymKey::from_ptr(nss_sys::PK11_ListFixedKeysInSlot( + slot.as_mut_ptr(), + name.as_ptr() as *mut c_char, + ptr::null_mut(), + )) + }; + match sym_key { + Ok(sym_key) => { + // See + // https://searchfox.org/mozilla-central/source/security/manager/ssl/NSSKeyStore.cpp#163-201 + // Unfortunately we can't use PK11_ExtractKeyValue(symKey.get()) here because softoken + // marks all token objects of type CKO_SECRET_KEY as sensitive. So we have to wrap and + // unwrap symKey to obtain a non-sensitive copy of symKey as a session object. + let wrapping_key = unsafe { + SymKey::from_ptr(nss_sys::PK11_KeyGen( + slot.as_mut_ptr(), + nss_sys::CKM_AES_KEY_GEN, + ptr::null_mut(), + 16, + ptr::null_mut(), + )) + .map_err(|_| get_last_error())? + }; + let mut wrap_len = nss_sys::SECItem { + type_: nss_sys::SECItemType::siBuffer as u32, + data: ptr::null_mut(), + len: 0, + }; + map_nss_secstatus(|| unsafe { + nss_sys::PK11_WrapSymKey( + nss_sys::CKM_AES_KEY_WRAP_KWP, + ptr::null_mut(), + wrapping_key.as_mut_ptr(), + sym_key.as_mut_ptr(), + &mut wrap_len, + ) + }) + .map_err(|_| get_last_error())?; + // PK11_UnwrapSymKey takes an int keySize + if wrap_len.len > u32::MAX - 8 { + return Err(ErrorKind::InvalidKeyLength.into()); + } + // Allocate an extra 8 bytes for CKM_AES_KEY_WRAP_KWP overhead. + let wrapped_key = unsafe { + nss_sys::SECITEM_AllocItem(ptr::null_mut(), ptr::null_mut(), wrap_len.len + 8) + }; + map_nss_secstatus(|| unsafe { + nss_sys::PK11_WrapSymKey( + nss_sys::CKM_AES_KEY_WRAP_KWP, + ptr::null_mut(), + wrapping_key.as_mut_ptr(), + sym_key.as_mut_ptr(), + wrapped_key, + ) + }) + .map_err(|_| get_last_error())?; + let sym_key = unsafe { + SymKey::from_ptr(nss_sys::PK11_UnwrapSymKey( + wrapping_key.as_mut_ptr(), + nss_sys::CKM_AES_KEY_WRAP_KWP, + ptr::null_mut(), + wrapped_key, + nss_sys::CKM_AES_GCM.into(), + (nss_sys::CKA_ENCRYPT | nss_sys::CKA_DECRYPT).into(), + wrap_len.len as i32, + )) + } + .map_err(|_| get_last_error())?; + + map_nss_secstatus(|| unsafe { nss_sys::PK11_ExtractKeyValue(sym_key.as_mut_ptr()) })?; + Ok(sym_key) + } + Err(e) => Err(e), + } +} + +#[cfg(feature = "keydb")] +fn create_aes256_key(name: &str) -> Result { + let mut key_bytes: [u8; nss_sys::AES_256_KEY_LENGTH as usize] = + [0; nss_sys::AES_256_KEY_LENGTH as usize]; + map_nss_secstatus(|| unsafe { + nss_sys::PK11_GenerateRandom(key_bytes.as_mut_ptr(), nss_sys::AES_256_KEY_LENGTH as i32) + })?; + match import_and_persist_sym_key( + nss_sys::CKM_AES_GCM.into(), + nss_sys::PK11Origin::PK11_OriginGenerated, + (nss_sys::CKA_ENCRYPT | nss_sys::CKA_DECRYPT).into(), + &key_bytes, + ) { + Ok(sym_key) => { + let name = CString::new(name).map_err(|_| ErrorKind::NulError)?; + unsafe { nss_sys::PK11_SetSymKeyNickname(sym_key.as_mut_ptr(), name.as_ptr()) }; + Ok(sym_key) + } + Err(e) => Err(e), + } +} + +/// Safe wrapper around PK11_ImportSymKey that +/// de-allocates memory when the key goes out of +/// scope, and persists key in key4.db. +#[cfg(feature = "keydb")] +fn import_and_persist_sym_key( + mechanism: nss_sys::CK_MECHANISM_TYPE, + origin: nss_sys::PK11Origin, + operation: nss_sys::CK_ATTRIBUTE_TYPE, + buf: &[u8], +) -> Result { + let mut item = nss_sys::SECItem { + type_: nss_sys::SECItemType::siBuffer as u32, + data: buf.as_ptr() as *mut c_uchar, + len: c_uint::try_from(buf.len())?, + }; + let slot = slot::get_internal_key_slot()?; + unsafe { + SymKey::from_ptr(nss_sys::PK11_ImportSymKeyWithFlags( + slot.as_mut_ptr(), + mechanism, + origin as u32, + operation, + &mut item, + nss_sys::CK_FLAGS::default(), + nss_sys::PR_TRUE, + ptr::null_mut(), + )) + } +} diff --git a/components/support/rc_crypto/nss/src/util.rs b/components/support/rc_crypto/nss/src/util.rs index 606ef89933..e374238321 100644 --- a/components/support/rc_crypto/nss/src/util.rs +++ b/components/support/rc_crypto/nss/src/util.rs @@ -6,11 +6,20 @@ use crate::error::*; use nss_sys::*; use std::{ffi::CString, os::raw::c_char, sync::Once}; +#[cfg(feature = "keydb")] +use crate::pk11::slot; +#[cfg(feature = "keydb")] +use once_cell::sync::OnceCell; +#[cfg(feature = "keydb")] +use std::{fs, path::Path}; + // This is the NSS version that this crate is claiming to be compatible with. // We check it at runtime using `NSS_VersionCheck`. pub const COMPATIBLE_NSS_VERSION: &str = "3.26"; static NSS_INIT: Once = Once::new(); +#[cfg(feature = "keydb")] +static NSS_PROFILE_PATH: OnceCell = OnceCell::new(); pub fn ensure_nss_initialized() { NSS_INIT.call_once(|| { @@ -41,6 +50,107 @@ pub fn ensure_nss_initialized() { }) } +/// Use this function to initialize NSS if you want to manage keys with NSS. +/// ensure_initialized_with_profile_dir initializes NSS with a profile directory (where key4.db +/// will be stored) and appropriate flags to persist keys (and certificates) in its internal PKCS11 +/// software implementation. +/// If it has been called previously with a different path, it will fail. +/// If `ensure_initialized` has been called before, it will also fail. +#[cfg(feature = "keydb")] +pub fn ensure_nss_initialized_with_profile_dir(path: impl AsRef) -> Result<()> { + match path.as_ref().to_str() { + Some(path) => { + if let Some(old_path) = NSS_PROFILE_PATH.get() { + if old_path == path { + return Ok(()); + } else { + return Err(ErrorKind::NSSInitFailure(format!( + "already initialized with profile: {}", + old_path + )) + .into()); + } + } + } + None => { + return Err(ErrorKind::NSSInitFailure(format!( + "invalid profile path: {:?}", + path.as_ref() + )) + .into()); + } + } + + if NSS_INIT.is_completed() { + return Err(ErrorKind::NSSInitFailure( + "NSS has been already initialized without profile".to_string(), + ) + .into()); + } + + let version_ptr = CString::new(COMPATIBLE_NSS_VERSION).unwrap(); + if unsafe { NSS_VersionCheck(version_ptr.as_ptr()) == PR_FALSE } { + panic!("Incompatible NSS version!") + } + + if fs::metadata(path.as_ref()).is_err() { + return Err(ErrorKind::NSSInitFailure(format!( + "invalid profile path: {:?}", + path.as_ref() + )) + .into()); + } + + // path must be valid unicode at this point because we just checked its metadata + let c_path: CString = + CString::new(path.as_ref().to_str().unwrap()).map_err(|_| ErrorKind::NulError)?; + let empty = CString::default(); + let flags = NSS_INIT_FORCEOPEN | NSS_INIT_OPTIMIZESPACE; + + let context = unsafe { + NSS_InitContext( + c_path.as_ptr(), + empty.as_ptr(), + empty.as_ptr(), + empty.as_ptr(), + std::ptr::null_mut(), + flags, + ) + }; + if context.is_null() { + let error = get_last_error(); + return Err( + ErrorKind::NSSInitFailure(format!("could not initialize context: {}", error)).into(), + ); + } + + let slot = slot::get_internal_key_slot().map_err(|error| { + ErrorKind::NSSInitFailure(format!("could not get internal key slot: {}", error)) + })?; + + if unsafe { PK11_NeedUserInit(slot.as_mut_ptr()) } == nss_sys::PR_TRUE { + let result = unsafe { + PK11_InitPin( + slot.as_mut_ptr(), + std::ptr::null_mut(), + std::ptr::null_mut(), + ) + }; + if result != SECStatus::SECSuccess { + let error = get_last_error(); + return Err( + ErrorKind::NSSInitFailure(format!("could not initialize pin: {}", error)).into(), + ); + } + } + + NSS_PROFILE_PATH + .set(format!("{:?}", path.as_ref())) + .map_err(|error| ErrorKind::NSSInitFailure(format!("already initialized: {}", error)))?; + + Ok(()) +} + pub fn map_nss_secstatus(callback: F) -> Result<()> where F: FnOnce() -> SECStatus, @@ -122,8 +232,28 @@ macro_rules! scoped_ptr { }; } +/// Copies a SECItem into a slice +/// +/// # Safety +/// +/// This doesn't leak, because the SECItem* returned by PK11_GetKeyData +/// just refers to a buffer managed by `sym_key` which we copy into `out`. pub(crate) unsafe fn sec_item_as_slice(sec_item: &mut SECItem) -> Result<&mut [u8]> { let sec_item_buf_len = usize::try_from(sec_item.len)?; let buf = std::slice::from_raw_parts_mut(sec_item.data, sec_item_buf_len); Ok(buf) } + +#[cfg(test)] +#[cfg(feature = "keydb")] +mod test { + use super::*; + + #[test] + #[should_panic] + fn test_ensure_nss_initialized_with_profile_dir_with_previously_call_to_ensure_nss_initialized() + { + ensure_nss_initialized(); + ensure_nss_initialized_with_profile_dir("./").unwrap(); + } +}