diff options
Diffstat (limited to '')
-rw-r--r-- | security/manager/ssl/rsclientcerts/src/manager.rs | 671 |
1 files changed, 671 insertions, 0 deletions
diff --git a/security/manager/ssl/rsclientcerts/src/manager.rs b/security/manager/ssl/rsclientcerts/src/manager.rs new file mode 100644 index 0000000000..507bed3a83 --- /dev/null +++ b/security/manager/ssl/rsclientcerts/src/manager.rs @@ -0,0 +1,671 @@ +/* -*- Mode: rust; rust-indent-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +use pkcs11_bindings::*; +use std::collections::{BTreeMap, BTreeSet}; +use std::sync::mpsc::{channel, Receiver, Sender}; +use std::thread; +use std::thread::JoinHandle; +use std::time::{Duration, Instant}; + +use crate::error::{Error, ErrorType}; +use crate::error_here; +use crate::util::*; + +/// Helper enum to differentiate between sessions on the modern slot and sessions on the legacy +/// slot. The former is for EC keys and RSA keys that can be used with RSA-PSS whereas the latter is +/// for RSA keys that cannot be used with RSA-PSS. +#[derive(Clone, Copy, PartialEq)] +pub enum SlotType { + Modern, + Legacy, +} + +pub trait CryptokiObject { + fn matches(&self, slot_type: SlotType, attrs: &[(CK_ATTRIBUTE_TYPE, Vec<u8>)]) -> bool; + fn get_attribute(&self, attribute: CK_ATTRIBUTE_TYPE) -> Option<&[u8]>; +} + +pub trait Sign { + fn get_signature_length( + &mut self, + data: &[u8], + params: &Option<CK_RSA_PKCS_PSS_PARAMS>, + ) -> Result<usize, Error>; + fn sign( + &mut self, + data: &[u8], + params: &Option<CK_RSA_PKCS_PSS_PARAMS>, + ) -> Result<Vec<u8>, Error>; +} + +pub trait ClientCertsBackend { + type Cert: CryptokiObject; + type Key: CryptokiObject + Sign; + + #[allow(clippy::type_complexity)] + fn find_objects(&self) -> Result<(Vec<Self::Cert>, Vec<Self::Key>), Error>; +} + +/// Helper type for sending `ManagerArguments` to the real `Manager`. +type ManagerArgumentsSender = Sender<ManagerArguments>; +/// Helper type for receiving `ManagerReturnValue`s from the real `Manager`. +type ManagerReturnValueReceiver = Receiver<ManagerReturnValue>; + +/// Helper enum that encapsulates arguments to send from the `ManagerProxy` to the real `Manager`. +/// `ManagerArguments::Stop` is a special variant that stops the background thread and drops the +/// `Manager`. +enum ManagerArguments { + OpenSession(SlotType), + CloseSession(CK_SESSION_HANDLE), + CloseAllSessions(SlotType), + StartSearch(CK_SESSION_HANDLE, Vec<(CK_ATTRIBUTE_TYPE, Vec<u8>)>), + Search(CK_SESSION_HANDLE, usize), + ClearSearch(CK_SESSION_HANDLE), + GetAttributes(CK_OBJECT_HANDLE, Vec<CK_ATTRIBUTE_TYPE>), + StartSign( + CK_SESSION_HANDLE, + CK_OBJECT_HANDLE, + Option<CK_RSA_PKCS_PSS_PARAMS>, + ), + GetSignatureLength(CK_SESSION_HANDLE, Vec<u8>), + Sign(CK_SESSION_HANDLE, Vec<u8>), + Stop, +} + +/// Helper enum that encapsulates return values from the real `Manager` that are sent back to the +/// `ManagerProxy`. `ManagerReturnValue::Stop` is a special variant that indicates that the +/// `Manager` will stop. +enum ManagerReturnValue { + OpenSession(Result<CK_SESSION_HANDLE, Error>), + CloseSession(Result<(), Error>), + CloseAllSessions(Result<(), Error>), + StartSearch(Result<(), Error>), + Search(Result<Vec<CK_OBJECT_HANDLE>, Error>), + ClearSearch(Result<(), Error>), + GetAttributes(Result<Vec<Option<Vec<u8>>>, Error>), + StartSign(Result<(), Error>), + GetSignatureLength(Result<usize, Error>), + Sign(Result<Vec<u8>, Error>), + Stop(Result<(), Error>), +} + +/// Helper macro to implement the body of each public `ManagerProxy` function. Takes a +/// `ManagerProxy` instance (should always be `self`), a `ManagerArguments` representing the +/// `Manager` function to call and the arguments to use, and the qualified type of the expected +/// `ManagerReturnValue` that will be received from the `Manager` when it is done. +macro_rules! manager_proxy_fn_impl { + ($manager:ident, $argument_enum:expr, $return_type:path) => { + match $manager.proxy_call($argument_enum) { + Ok($return_type(result)) => result, + Ok(_) => Err(error_here!(ErrorType::LibraryFailure)), + Err(e) => Err(e), + } + }; +} + +/// `ManagerProxy` synchronously proxies calls from any thread to the `Manager` that runs on a +/// single thread. This is necessary because the underlying OS APIs in use are not guaranteed to be +/// thread-safe (e.g. they may use thread-local storage). Using it should be identical to using the +/// real `Manager`. +pub struct ManagerProxy { + sender: ManagerArgumentsSender, + receiver: ManagerReturnValueReceiver, + thread_handle: Option<JoinHandle<()>>, +} + +impl ManagerProxy { + pub fn new<B: ClientCertsBackend + Send + 'static>(backend: B) -> Result<ManagerProxy, Error> { + let (proxy_sender, manager_receiver) = channel(); + let (manager_sender, proxy_receiver) = channel(); + let thread_handle = thread::Builder::new() + .name("osclientcert".into()) + .spawn(move || { + let mut real_manager = Manager::new(backend); + while let Ok(arguments) = manager_receiver.recv() { + let results = match arguments { + ManagerArguments::OpenSession(slot_type) => { + ManagerReturnValue::OpenSession(real_manager.open_session(slot_type)) + } + ManagerArguments::CloseSession(session_handle) => { + ManagerReturnValue::CloseSession( + real_manager.close_session(session_handle), + ) + } + ManagerArguments::CloseAllSessions(slot_type) => { + ManagerReturnValue::CloseAllSessions( + real_manager.close_all_sessions(slot_type), + ) + } + ManagerArguments::StartSearch(session, attrs) => { + ManagerReturnValue::StartSearch( + real_manager.start_search(session, attrs), + ) + } + ManagerArguments::Search(session, max_objects) => { + ManagerReturnValue::Search(real_manager.search(session, max_objects)) + } + ManagerArguments::ClearSearch(session) => { + ManagerReturnValue::ClearSearch(real_manager.clear_search(session)) + } + ManagerArguments::GetAttributes(object_handle, attr_types) => { + ManagerReturnValue::GetAttributes( + real_manager.get_attributes(object_handle, attr_types), + ) + } + ManagerArguments::StartSign(session, key_handle, params) => { + ManagerReturnValue::StartSign( + real_manager.start_sign(session, key_handle, params), + ) + } + ManagerArguments::GetSignatureLength(session, data) => { + ManagerReturnValue::GetSignatureLength( + real_manager.get_signature_length(session, data), + ) + } + ManagerArguments::Sign(session, data) => { + ManagerReturnValue::Sign(real_manager.sign(session, data)) + } + ManagerArguments::Stop => ManagerReturnValue::Stop(Ok(())), + }; + let stop_after_send = matches!(&results, &ManagerReturnValue::Stop(_)); + match manager_sender.send(results) { + Ok(()) => {} + Err(_) => { + break; + } + } + if stop_after_send { + break; + } + } + }); + match thread_handle { + Ok(thread_handle) => Ok(ManagerProxy { + sender: proxy_sender, + receiver: proxy_receiver, + thread_handle: Some(thread_handle), + }), + Err(_) => Err(error_here!(ErrorType::LibraryFailure)), + } + } + + fn proxy_call(&self, args: ManagerArguments) -> Result<ManagerReturnValue, Error> { + match self.sender.send(args) { + Ok(()) => {} + Err(_) => { + return Err(error_here!(ErrorType::LibraryFailure)); + } + }; + let result = match self.receiver.recv() { + Ok(result) => result, + Err(_) => { + return Err(error_here!(ErrorType::LibraryFailure)); + } + }; + Ok(result) + } + + pub fn open_session(&mut self, slot_type: SlotType) -> Result<CK_SESSION_HANDLE, Error> { + manager_proxy_fn_impl!( + self, + ManagerArguments::OpenSession(slot_type), + ManagerReturnValue::OpenSession + ) + } + + pub fn close_session(&mut self, session: CK_SESSION_HANDLE) -> Result<(), Error> { + manager_proxy_fn_impl!( + self, + ManagerArguments::CloseSession(session), + ManagerReturnValue::CloseSession + ) + } + + pub fn close_all_sessions(&mut self, slot_type: SlotType) -> Result<(), Error> { + manager_proxy_fn_impl!( + self, + ManagerArguments::CloseAllSessions(slot_type), + ManagerReturnValue::CloseAllSessions + ) + } + + pub fn start_search( + &mut self, + session: CK_SESSION_HANDLE, + attrs: Vec<(CK_ATTRIBUTE_TYPE, Vec<u8>)>, + ) -> Result<(), Error> { + manager_proxy_fn_impl!( + self, + ManagerArguments::StartSearch(session, attrs), + ManagerReturnValue::StartSearch + ) + } + + pub fn search( + &mut self, + session: CK_SESSION_HANDLE, + max_objects: usize, + ) -> Result<Vec<CK_OBJECT_HANDLE>, Error> { + manager_proxy_fn_impl!( + self, + ManagerArguments::Search(session, max_objects), + ManagerReturnValue::Search + ) + } + + pub fn clear_search(&mut self, session: CK_SESSION_HANDLE) -> Result<(), Error> { + manager_proxy_fn_impl!( + self, + ManagerArguments::ClearSearch(session), + ManagerReturnValue::ClearSearch + ) + } + + pub fn get_attributes( + &self, + object_handle: CK_OBJECT_HANDLE, + attr_types: Vec<CK_ATTRIBUTE_TYPE>, + ) -> Result<Vec<Option<Vec<u8>>>, Error> { + manager_proxy_fn_impl!( + self, + ManagerArguments::GetAttributes(object_handle, attr_types,), + ManagerReturnValue::GetAttributes + ) + } + + pub fn start_sign( + &mut self, + session: CK_SESSION_HANDLE, + key_handle: CK_OBJECT_HANDLE, + params: Option<CK_RSA_PKCS_PSS_PARAMS>, + ) -> Result<(), Error> { + manager_proxy_fn_impl!( + self, + ManagerArguments::StartSign(session, key_handle, params), + ManagerReturnValue::StartSign + ) + } + + pub fn get_signature_length( + &self, + session: CK_SESSION_HANDLE, + data: Vec<u8>, + ) -> Result<usize, Error> { + manager_proxy_fn_impl!( + self, + ManagerArguments::GetSignatureLength(session, data), + ManagerReturnValue::GetSignatureLength + ) + } + + pub fn sign(&mut self, session: CK_SESSION_HANDLE, data: Vec<u8>) -> Result<Vec<u8>, Error> { + manager_proxy_fn_impl!( + self, + ManagerArguments::Sign(session, data), + ManagerReturnValue::Sign + ) + } + + pub fn stop(&mut self) -> Result<(), Error> { + manager_proxy_fn_impl!(self, ManagerArguments::Stop, ManagerReturnValue::Stop)?; + let thread_handle = match self.thread_handle.take() { + Some(thread_handle) => thread_handle, + None => return Err(error_here!(ErrorType::LibraryFailure)), + }; + thread_handle + .join() + .map_err(|_| error_here!(ErrorType::LibraryFailure)) + } +} + +// Determines if the attributes of a given search correspond to NSS looking for all certificates or +// private keys. Returns true if so, and false otherwise. +// These searches are of the form: +// { { type: CKA_TOKEN, value: [1] }, +// { type: CKA_CLASS, value: [CKO_CERTIFICATE or CKO_PRIVATE_KEY, as serialized bytes] } } +// (although not necessarily in that order - see nssToken_TraverseCertificates and +// nssToken_FindPrivateKeys) +fn search_is_for_all_certificates_or_keys( + attrs: &[(CK_ATTRIBUTE_TYPE, Vec<u8>)], +) -> Result<bool, Error> { + if attrs.len() != 2 { + return Ok(false); + } + let token_bytes = vec![1_u8]; + let mut found_token = false; + let cko_certificate_bytes = serialize_uint(CKO_CERTIFICATE)?; + let cko_private_key_bytes = serialize_uint(CKO_PRIVATE_KEY)?; + let mut found_certificate_or_private_key = false; + for (attr_type, attr_value) in attrs.iter() { + if attr_type == &CKA_TOKEN && attr_value == &token_bytes { + found_token = true; + } + if attr_type == &CKA_CLASS + && (attr_value == &cko_certificate_bytes || attr_value == &cko_private_key_bytes) + { + found_certificate_or_private_key = true; + } + } + Ok(found_token && found_certificate_or_private_key) +} + +const SUPPORTED_ATTRIBUTES: &[CK_ATTRIBUTE_TYPE] = &[ + CKA_CLASS, + CKA_TOKEN, + CKA_LABEL, + CKA_ID, + CKA_VALUE, + CKA_ISSUER, + CKA_SERIAL_NUMBER, + CKA_SUBJECT, + CKA_PRIVATE, + CKA_KEY_TYPE, + CKA_MODULUS, + CKA_EC_PARAMS, +]; + +enum Object<B: ClientCertsBackend> { + Cert(B::Cert), + Key(B::Key), +} + +impl<B: ClientCertsBackend> Object<B> { + fn matches(&self, slot_type: SlotType, attrs: &[(CK_ATTRIBUTE_TYPE, Vec<u8>)]) -> bool { + match self { + Object::Cert(cert) => cert.matches(slot_type, attrs), + Object::Key(key) => key.matches(slot_type, attrs), + } + } + + fn get_attribute(&self, attribute: CK_ATTRIBUTE_TYPE) -> Option<&[u8]> { + match self { + Object::Cert(cert) => cert.get_attribute(attribute), + Object::Key(key) => key.get_attribute(attribute), + } + } + + fn id(&self) -> Result<&[u8], Error> { + self.get_attribute(CKA_ID) + .ok_or_else(|| error_here!(ErrorType::LibraryFailure)) + } + + fn get_signature_length( + &mut self, + data: Vec<u8>, + params: &Option<CK_RSA_PKCS_PSS_PARAMS>, + ) -> Result<usize, Error> { + match self { + Object::Cert(_) => Err(error_here!(ErrorType::InvalidArgument)), + Object::Key(key) => key.get_signature_length(&data, params), + } + } + + fn sign( + &mut self, + data: Vec<u8>, + params: &Option<CK_RSA_PKCS_PSS_PARAMS>, + ) -> Result<Vec<u8>, Error> { + match self { + Object::Cert(_) => Err(error_here!(ErrorType::InvalidArgument)), + Object::Key(key) => key.sign(&data, params), + } + } +} + +/// The `Manager` keeps track of the state of this module with respect to the PKCS #11 +/// specification. This includes what sessions are open, which search and sign operations are +/// ongoing, and what objects are known and by what handle. +pub struct Manager<B: ClientCertsBackend> { + /// A map of session to session type (modern or legacy). Sessions can be created (opened) and + /// later closed. + sessions: BTreeMap<CK_SESSION_HANDLE, SlotType>, + /// A map of searches to PKCS #11 object handles that match those searches. + searches: BTreeMap<CK_SESSION_HANDLE, Vec<CK_OBJECT_HANDLE>>, + /// A map of sign operations to a pair of the object handle and optionally some params being + /// used by each one. + signs: BTreeMap<CK_SESSION_HANDLE, (CK_OBJECT_HANDLE, Option<CK_RSA_PKCS_PSS_PARAMS>)>, + /// A map of object handles to the underlying objects. + objects: BTreeMap<CK_OBJECT_HANDLE, Object<B>>, + /// A set of certificate identifiers (not the same as handles). + cert_ids: BTreeSet<Vec<u8>>, + /// A set of key identifiers (not the same as handles). For each id in this set, there should be + /// a corresponding identical id in the `cert_ids` set. + key_ids: BTreeSet<Vec<u8>>, + /// The next session handle to hand out. + next_session: CK_SESSION_HANDLE, + /// The next object handle to hand out. + next_handle: CK_OBJECT_HANDLE, + /// The last time the implementation looked for new objects in the backend. + /// The implementation does this search no more than once every 3 seconds. + last_scan_time: Option<Instant>, + backend: B, +} + +impl<B: ClientCertsBackend> Manager<B> { + pub fn new(backend: B) -> Manager<B> { + Manager { + sessions: BTreeMap::new(), + searches: BTreeMap::new(), + signs: BTreeMap::new(), + objects: BTreeMap::new(), + cert_ids: BTreeSet::new(), + key_ids: BTreeSet::new(), + next_session: 1, + next_handle: 1, + last_scan_time: None, + backend, + } + } + + /// When a new search session is opened (provided at least 3 seconds have elapsed since the + /// last session was opened), this searches for certificates and keys to expose. We + /// de-duplicate previously-found certificates and keys by keeping track of their IDs. + fn maybe_find_new_objects(&mut self) -> Result<(), Error> { + let now = Instant::now(); + match self.last_scan_time { + Some(last_scan_time) => { + if now.duration_since(last_scan_time) < Duration::new(3, 0) { + return Ok(()); + } + } + None => {} + } + self.last_scan_time = Some(now); + let (certs, keys) = self.backend.find_objects()?; + for cert in certs { + let object = Object::Cert(cert); + if self.cert_ids.contains(object.id()?) { + continue; + } + self.cert_ids.insert(object.id()?.to_vec()); + let handle = self.get_next_handle(); + self.objects.insert(handle, object); + } + for key in keys { + let object = Object::Key(key); + if self.key_ids.contains(object.id()?) { + continue; + } + self.key_ids.insert(object.id()?.to_vec()); + let handle = self.get_next_handle(); + self.objects.insert(handle, object); + } + Ok(()) + } + + pub fn open_session(&mut self, slot_type: SlotType) -> Result<CK_SESSION_HANDLE, Error> { + let next_session = self.next_session; + self.next_session += 1; + self.sessions.insert(next_session, slot_type); + Ok(next_session) + } + + pub fn close_session(&mut self, session: CK_SESSION_HANDLE) -> Result<(), Error> { + self.sessions + .remove(&session) + .ok_or_else(|| error_here!(ErrorType::InvalidInput)) + .map(|_| ()) + } + + pub fn close_all_sessions(&mut self, slot_type: SlotType) -> Result<(), Error> { + let mut to_remove = Vec::new(); + for (session, open_slot_type) in self.sessions.iter() { + if slot_type == *open_slot_type { + to_remove.push(*session); + } + } + for session in to_remove { + if self.sessions.remove(&session).is_none() { + return Err(error_here!(ErrorType::LibraryFailure)); + } + } + Ok(()) + } + + fn get_next_handle(&mut self) -> CK_OBJECT_HANDLE { + let next_handle = self.next_handle; + self.next_handle += 1; + next_handle + } + + /// PKCS #11 specifies that search operations happen in three phases: setup, get any matches + /// (this part may be repeated if the caller uses a small buffer), and end. This implementation + /// does all of the work up front and gathers all matching objects during setup and retains them + /// until they are retrieved and consumed via `search`. + pub fn start_search( + &mut self, + session: CK_SESSION_HANDLE, + attrs: Vec<(CK_ATTRIBUTE_TYPE, Vec<u8>)>, + ) -> Result<(), Error> { + let slot_type = match self.sessions.get(&session) { + Some(slot_type) => *slot_type, + None => return Err(error_here!(ErrorType::InvalidArgument)), + }; + // If the search is for an attribute we don't support, no objects will match. This check + // saves us having to look through all of our objects. + for (attr, _) in &attrs { + if !SUPPORTED_ATTRIBUTES.contains(attr) { + self.searches.insert(session, Vec::new()); + return Ok(()); + } + } + // When NSS wants to find all certificates or all private keys, it will perform a search + // with a particular set of attributes. This implementation uses these searches as an + // indication for the backend to re-scan for new objects from tokens that may have been + // inserted or certificates that may have been imported into the OS. Since these searches + // are relatively rare, this minimizes the impact of doing these re-scans. + if search_is_for_all_certificates_or_keys(&attrs)? { + self.maybe_find_new_objects()?; + } + let mut handles = Vec::new(); + for (handle, object) in &self.objects { + if object.matches(slot_type, &attrs) { + handles.push(*handle); + } + } + self.searches.insert(session, handles); + Ok(()) + } + + /// Given a session and a maximum number of object handles to return, attempts to retrieve up to + /// that many objects from the corresponding search. Updates the search so those objects are not + /// returned repeatedly. `max_objects` must be non-zero. + pub fn search( + &mut self, + session: CK_SESSION_HANDLE, + max_objects: usize, + ) -> Result<Vec<CK_OBJECT_HANDLE>, Error> { + if max_objects == 0 { + return Err(error_here!(ErrorType::InvalidArgument)); + } + match self.searches.get_mut(&session) { + Some(search) => { + let split_at = if max_objects >= search.len() { + 0 + } else { + search.len() - max_objects + }; + let to_return = search.split_off(split_at); + if to_return.len() > max_objects { + return Err(error_here!(ErrorType::LibraryFailure)); + } + Ok(to_return) + } + None => Err(error_here!(ErrorType::InvalidArgument)), + } + } + + pub fn clear_search(&mut self, session: CK_SESSION_HANDLE) -> Result<(), Error> { + self.searches.remove(&session); + Ok(()) + } + + pub fn get_attributes( + &self, + object_handle: CK_OBJECT_HANDLE, + attr_types: Vec<CK_ATTRIBUTE_TYPE>, + ) -> Result<Vec<Option<Vec<u8>>>, Error> { + let object = match self.objects.get(&object_handle) { + Some(object) => object, + None => return Err(error_here!(ErrorType::InvalidArgument)), + }; + let mut results = Vec::with_capacity(attr_types.len()); + for attr_type in attr_types { + let result = object + .get_attribute(attr_type) + .map(|value| value.to_owned()); + results.push(result); + } + Ok(results) + } + + /// The way NSS uses PKCS #11 to sign data happens in two phases: setup and sign. This + /// implementation makes a note of which key is to be used (if it exists) during setup. When the + /// caller finishes with the sign operation, this implementation retrieves the key handle and + /// performs the signature. + pub fn start_sign( + &mut self, + session: CK_SESSION_HANDLE, + key_handle: CK_OBJECT_HANDLE, + params: Option<CK_RSA_PKCS_PSS_PARAMS>, + ) -> Result<(), Error> { + if self.signs.contains_key(&session) { + return Err(error_here!(ErrorType::InvalidArgument)); + } + self.signs.insert(session, (key_handle, params)); + Ok(()) + } + + pub fn get_signature_length( + &mut self, + session: CK_SESSION_HANDLE, + data: Vec<u8>, + ) -> Result<usize, Error> { + let (key_handle, params) = match self.signs.get(&session) { + Some((key_handle, params)) => (key_handle, params), + None => return Err(error_here!(ErrorType::InvalidArgument)), + }; + let key = match self.objects.get_mut(key_handle) { + Some(key) => key, + None => return Err(error_here!(ErrorType::InvalidArgument)), + }; + key.get_signature_length(data, params) + } + + pub fn sign(&mut self, session: CK_SESSION_HANDLE, data: Vec<u8>) -> Result<Vec<u8>, Error> { + // Performing the signature (via C_Sign, which is the only way we support) finishes the sign + // operation, so it needs to be removed here. + let (key_handle, params) = match self.signs.remove(&session) { + Some((key_handle, params)) => (key_handle, params), + None => return Err(error_here!(ErrorType::InvalidArgument)), + }; + let key = match self.objects.get_mut(&key_handle) { + Some(key) => key, + None => return Err(error_here!(ErrorType::InvalidArgument)), + }; + key.sign(data, ¶ms) + } +} |