From d1b2d29528b7794b41e66fc2136e395a02f8529b Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Thu, 30 May 2024 05:59:35 +0200 Subject: Merging upstream version 1.73.0+dfsg1. Signed-off-by: Daniel Baumann --- vendor/security-framework/src/access_control.rs | 40 + vendor/security-framework/src/authorization.rs | 819 +++++++++ vendor/security-framework/src/base.rs | 88 + vendor/security-framework/src/certificate.rs | 320 ++++ vendor/security-framework/src/cipher_suite.rs | 246 +++ vendor/security-framework/src/dlsym.rs | 50 + vendor/security-framework/src/identity.rs | 83 + vendor/security-framework/src/import_export.rs | 174 ++ vendor/security-framework/src/item.rs | 689 ++++++++ vendor/security-framework/src/key.rs | 387 ++++ vendor/security-framework/src/lib.rs | 92 + vendor/security-framework/src/os/macos/access.rs | 14 + .../security-framework/src/os/macos/certificate.rs | 269 +++ .../src/os/macos/certificate_oids.rs | 32 + .../src/os/macos/code_signing.rs | 485 +++++ .../src/os/macos/digest_transform.rs | 196 +++ .../src/os/macos/encrypt_transform.rs | 256 +++ vendor/security-framework/src/os/macos/identity.rs | 86 + .../src/os/macos/import_export.rs | 345 ++++ vendor/security-framework/src/os/macos/item.rs | 47 + vendor/security-framework/src/os/macos/key.rs | 38 + vendor/security-framework/src/os/macos/keychain.rs | 280 +++ .../src/os/macos/keychain_item.rs | 26 + vendor/security-framework/src/os/macos/mod.rs | 52 + .../security-framework/src/os/macos/passwords.rs | 525 ++++++ .../src/os/macos/secure_transport.rs | 647 +++++++ .../security-framework/src/os/macos/transform.rs | 54 + vendor/security-framework/src/os/mod.rs | 4 + vendor/security-framework/src/passwords.rs | 332 ++++ vendor/security-framework/src/passwords_options.rs | 129 ++ vendor/security-framework/src/policy.rs | 104 ++ vendor/security-framework/src/random.rs | 39 + vendor/security-framework/src/secure_transport.rs | 1848 ++++++++++++++++++++ vendor/security-framework/src/trust.rs | 396 +++++ vendor/security-framework/src/trust_settings.rs | 305 ++++ 35 files changed, 9497 insertions(+) create mode 100644 vendor/security-framework/src/access_control.rs create mode 100644 vendor/security-framework/src/authorization.rs create mode 100644 vendor/security-framework/src/base.rs create mode 100644 vendor/security-framework/src/certificate.rs create mode 100644 vendor/security-framework/src/cipher_suite.rs create mode 100644 vendor/security-framework/src/dlsym.rs create mode 100644 vendor/security-framework/src/identity.rs create mode 100644 vendor/security-framework/src/import_export.rs create mode 100644 vendor/security-framework/src/item.rs create mode 100644 vendor/security-framework/src/key.rs create mode 100644 vendor/security-framework/src/lib.rs create mode 100644 vendor/security-framework/src/os/macos/access.rs create mode 100644 vendor/security-framework/src/os/macos/certificate.rs create mode 100644 vendor/security-framework/src/os/macos/certificate_oids.rs create mode 100644 vendor/security-framework/src/os/macos/code_signing.rs create mode 100644 vendor/security-framework/src/os/macos/digest_transform.rs create mode 100644 vendor/security-framework/src/os/macos/encrypt_transform.rs create mode 100644 vendor/security-framework/src/os/macos/identity.rs create mode 100644 vendor/security-framework/src/os/macos/import_export.rs create mode 100644 vendor/security-framework/src/os/macos/item.rs create mode 100644 vendor/security-framework/src/os/macos/key.rs create mode 100644 vendor/security-framework/src/os/macos/keychain.rs create mode 100644 vendor/security-framework/src/os/macos/keychain_item.rs create mode 100644 vendor/security-framework/src/os/macos/mod.rs create mode 100644 vendor/security-framework/src/os/macos/passwords.rs create mode 100644 vendor/security-framework/src/os/macos/secure_transport.rs create mode 100644 vendor/security-framework/src/os/macos/transform.rs create mode 100644 vendor/security-framework/src/os/mod.rs create mode 100644 vendor/security-framework/src/passwords.rs create mode 100644 vendor/security-framework/src/passwords_options.rs create mode 100644 vendor/security-framework/src/policy.rs create mode 100644 vendor/security-framework/src/random.rs create mode 100644 vendor/security-framework/src/secure_transport.rs create mode 100644 vendor/security-framework/src/trust.rs create mode 100644 vendor/security-framework/src/trust_settings.rs (limited to 'vendor/security-framework/src') diff --git a/vendor/security-framework/src/access_control.rs b/vendor/security-framework/src/access_control.rs new file mode 100644 index 000000000..15a6d700d --- /dev/null +++ b/vendor/security-framework/src/access_control.rs @@ -0,0 +1,40 @@ +//! Access Control support. + +use std::ptr::{self, null}; + +use core_foundation::base::{TCFType, CFOptionFlags, kCFAllocatorDefault}; +use security_framework_sys::access_control::{SecAccessControlGetTypeID, SecAccessControlCreateWithFlags}; +use security_framework_sys::base::{SecAccessControlRef, errSecParam}; +use crate::base::{Error, Result}; + +declare_TCFType! { + /// A type representing sec access control settings. + SecAccessControl, SecAccessControlRef +} +impl_TCFType!( + SecAccessControl, + SecAccessControlRef, + SecAccessControlGetTypeID +); + +unsafe impl Sync for SecAccessControl {} +unsafe impl Send for SecAccessControl {} + +impl SecAccessControl { + /// Create `AccessControl` object from flags + pub fn create_with_flags(flags: CFOptionFlags) -> Result { + unsafe { + let access_control = SecAccessControlCreateWithFlags( + kCFAllocatorDefault, + null(), + flags, + ptr::null_mut(), + ); + if access_control.is_null() { + Err(Error::from_code(errSecParam)) + } else { + Ok(Self::wrap_under_create_rule(access_control)) + } + } + } +} diff --git a/vendor/security-framework/src/authorization.rs b/vendor/security-framework/src/authorization.rs new file mode 100644 index 000000000..f19f8ecc9 --- /dev/null +++ b/vendor/security-framework/src/authorization.rs @@ -0,0 +1,819 @@ +//! Authorization Services support. + +/// # Potential improvements +/// +/// * When generic specialization stabilizes prevent copying from `CString` +/// arguments. +/// * `AuthorizationCopyRightsAsync` +/// * Provide constants for well known item names +use crate::base::{Error, Result}; +#[cfg(all(target_os = "macos", feature = "job-bless"))] +use core_foundation::base::Boolean; +use core_foundation::base::{CFTypeRef, TCFType}; +use core_foundation::bundle::CFBundleRef; +use core_foundation::dictionary::{CFDictionary, CFDictionaryRef}; +#[cfg(all(target_os = "macos", feature = "job-bless"))] +use core_foundation::error::CFError; +#[cfg(all(target_os = "macos", feature = "job-bless"))] +use core_foundation::error::CFErrorRef; +use core_foundation::string::{CFString, CFStringRef}; +use security_framework_sys::authorization as sys; +use security_framework_sys::base::errSecConversionError; +use std::convert::TryFrom; +use std::ffi::{CStr, CString}; +use std::fs::File; +use std::mem::MaybeUninit; +use std::os::raw::c_void; +use std::ptr::addr_of; +use std::{convert::TryInto, marker::PhantomData}; +use sys::AuthorizationExternalForm; + +macro_rules! optional_str_to_cfref { + ($string:ident) => {{ + $string + .map(CFString::new) + .map_or(std::ptr::null(), |cfs| cfs.as_concrete_TypeRef()) + }}; +} + +macro_rules! cstring_or_err { + ($x:expr) => {{ + CString::new($x).map_err(|_| Error::from_code(errSecConversionError)) + }}; +} + +bitflags::bitflags! { + /// The flags used to specify authorization options. + pub struct Flags: sys::AuthorizationFlags { + /// An empty flag set that you use as a placeholder when you don't want + /// any of the other flags. + const DEFAULTS = sys::kAuthorizationFlagDefaults; + + /// A flag that permits user interaction as needed. + const INTERACTION_ALLOWED = sys::kAuthorizationFlagInteractionAllowed; + + /// A flag that permits the Security Server to attempt to grant the + /// rights requested. + const EXTEND_RIGHTS = sys::kAuthorizationFlagExtendRights; + + /// A flag that permits the Security Server to grant rights on an + /// individual basis. + const PARTIAL_RIGHTS = sys::kAuthorizationFlagPartialRights; + + /// A flag that instructs the Security Server to revoke authorization. + const DESTROY_RIGHTS = sys::kAuthorizationFlagDestroyRights; + + /// A flag that instructs the Security Server to preauthorize the rights + /// requested. + const PREAUTHORIZE = sys::kAuthorizationFlagPreAuthorize; + } +} + +impl Default for Flags { + #[inline(always)] + fn default() -> Flags { + Flags::DEFAULTS + } +} + +/// Information about an authorization right or the environment. +#[repr(C)] +pub struct AuthorizationItem(sys::AuthorizationItem); + +impl AuthorizationItem { + /// The required name of the authorization right or environment data. + /// + /// If `name` isn't convertable to a `CString` it will return + /// Err(errSecConversionError). + #[must_use] pub fn name(&self) -> &str { + unsafe { + CStr::from_ptr(self.0.name) + .to_str() + .expect("AuthorizationItem::name failed to convert &str to CStr") + } + } + + /// The information pertaining to the name field. Do not rely on NULL + /// termination of string data. + #[inline] + #[must_use] pub fn value(&self) -> Option<&[u8]> { + if self.0.value.is_null() { + return None; + } + + let value = + unsafe { std::slice::from_raw_parts(self.0.value as *const u8, self.0.valueLength) }; + + Some(value) + } +} + +/// A set of authorization items returned and owned by the Security Server. +#[derive(Debug)] +#[repr(C)] +pub struct AuthorizationItemSet<'a> { + inner: *const sys::AuthorizationItemSet, + phantom: PhantomData<&'a sys::AuthorizationItemSet>, +} + +impl<'a> Drop for AuthorizationItemSet<'a> { + #[inline] + fn drop(&mut self) { + unsafe { + sys::AuthorizationFreeItemSet(self.inner as *mut sys::AuthorizationItemSet); + } + } +} + +/// Used by `AuthorizationItemSetBuilder` to store data pointed to by +/// `sys::AuthorizationItemSet`. +#[derive(Debug)] +pub struct AuthorizationItemSetStorage { + /// The layout of this is a little awkward because of the requirements of + /// Apple's APIs. `items` contains pointers to data owned by `names` and + /// `values`, so we must not modify them once `items` has been set up. + names: Vec, + values: Vec>>, + items: Vec, + + /// Must not be given to APIs which would attempt to modify it. + /// + /// See `AuthorizationItemSet` for sets owned by the Security Server which + /// are writable. + pub set: sys::AuthorizationItemSet, +} + +impl Default for AuthorizationItemSetStorage { + #[inline] + fn default() -> Self { + AuthorizationItemSetStorage { + names: Vec::new(), + values: Vec::new(), + items: Vec::new(), + set: sys::AuthorizationItemSet { + count: 0, + items: std::ptr::null_mut(), + }, + } + } +} + +/// A convenience `AuthorizationItemSetBuilder` builder which enabled you to use +/// rust types. All names and values passed in will be copied. +#[derive(Debug, Default)] +pub struct AuthorizationItemSetBuilder { + storage: AuthorizationItemSetStorage, +} + +// Stores AuthorizationItems contiguously, and their items separately +impl AuthorizationItemSetBuilder { + /// Creates a new `AuthorizationItemSetStore`, which simplifies creating + /// owned vectors of `AuthorizationItem`s. + #[inline(always)] + #[must_use] + pub fn new() -> AuthorizationItemSetBuilder { + Default::default() + } + + /// Adds an `AuthorizationItem` with the name set to a right and an empty + /// value. + /// + /// If `name` isn't convertable to a `CString` it will return + /// Err(errSecConversionError). + pub fn add_right>>(mut self, name: N) -> Result { + self.storage.names.push(cstring_or_err!(name)?); + self.storage.values.push(None); + Ok(self) + } + + /// Adds an `AuthorizationItem` with arbitrary data. + /// + /// If `name` isn't convertable to a `CString` it will return + /// Err(errSecConversionError). + pub fn add_data(mut self, name: N, value: V) -> Result + where + N: Into>, + V: Into>, + { + self.storage.names.push(cstring_or_err!(name)?); + self.storage.values.push(Some(value.into())); + Ok(self) + } + + /// Adds an `AuthorizationItem` with NULL terminated string data. + /// + /// If `name` or `value` isn't convertable to a `CString` it will return + /// Err(errSecConversionError). + pub fn add_string(mut self, name: N, value: V) -> Result + where + N: Into>, + V: Into>, + { + self.storage.names.push(cstring_or_err!(name)?); + self.storage + .values + .push(Some(cstring_or_err!(value)?.to_bytes().to_vec())); + Ok(self) + } + + /// Creates the `sys::AuthorizationItemSet`, and gives you ownership of the + /// data it points to. + #[must_use] + pub fn build(mut self) -> AuthorizationItemSetStorage { + self.storage.items = self + .storage + .names + .iter() + .zip(self.storage.values.iter()) + .map(|(n, v)| sys::AuthorizationItem { + name: n.as_ptr(), + value: v + .as_ref() + .map_or(std::ptr::null_mut(), |v| v.as_ptr() as *mut c_void), + valueLength: v.as_ref().map_or(0, |v| v.len()), + flags: 0, + }) + .collect(); + + self.storage.set = sys::AuthorizationItemSet { + count: self.storage.items.len() as u32, + items: self.storage.items.as_ptr() as *mut sys::AuthorizationItem, + }; + + self.storage + } +} + +/// Used by `Authorization::set_item` to define the rules of he right. +#[derive(Copy, Clone)] +pub enum RightDefinition<'a> { + /// The dictionary will contain the keys and values that define the rules. + FromDictionary(&'a CFDictionary), + + /// The specified right's rules will be duplicated. + FromExistingRight(&'a str), +} + +/// A wrapper around `AuthorizationCreate` and functions which operate on an +/// `AuthorizationRef`. +#[derive(Debug)] +pub struct Authorization { + handle: sys::AuthorizationRef, + free_flags: Flags, +} + +impl TryFrom for Authorization { + type Error = Error; + + /// Internalizes the external representation of an authorization reference. + #[cold] + fn try_from(external_form: AuthorizationExternalForm) -> Result { + let mut handle = MaybeUninit::::uninit(); + + let status = unsafe { + sys::AuthorizationCreateFromExternalForm(&external_form, handle.as_mut_ptr()) + }; + + if status != sys::errAuthorizationSuccess { + return Err(Error::from_code(status)); + } + + let auth = Authorization { + handle: unsafe { handle.assume_init() }, + free_flags: Default::default(), + }; + + Ok(auth) + } +} + +impl Authorization { + /// Creates an authorization object which has no environment or associated + /// rights. + #[inline] + pub fn default() -> Result { + Self::new(None, None, Default::default()) + } + + /// Creates an authorization reference and provides an option to authorize + /// or preauthorize rights. + /// + /// `rights` should be the names of the rights you want to create. + /// + /// `environment` is used when authorizing or preauthorizing rights. Not + /// used in OS X v10.2 and earlier. In macOS 10.3 and later, you can pass + /// icon or prompt data to be used in the authentication dialog box. In + /// macOS 10.4 and later, you can also pass a user name and password in + /// order to authorize a user without user interaction. + pub fn new( + // FIXME: this should have been by reference + rights: Option, + environment: Option, + flags: Flags, + ) -> Result { + let rights_ptr = rights.as_ref().map_or(std::ptr::null(), |r| { + addr_of!(r.set) as *const sys::AuthorizationItemSet + }); + + let env_ptr = environment.as_ref().map_or(std::ptr::null(), |e| { + addr_of!(e.set) as *const sys::AuthorizationItemSet + }); + + let mut handle = MaybeUninit::::uninit(); + + let status = unsafe { + sys::AuthorizationCreate(rights_ptr, env_ptr, flags.bits(), handle.as_mut_ptr()) + }; + + if status != sys::errAuthorizationSuccess { + return Err(Error::from_code(status)); + } + + Ok(Authorization { + handle: unsafe { handle.assume_init() }, + free_flags: Default::default(), + }) + } + + /// Internalizes the external representation of an authorization reference. + #[deprecated(since = "2.0.1", note = "Please use the TryFrom trait instead")] + pub fn from_external_form(external_form: sys::AuthorizationExternalForm) -> Result { + external_form.try_into() + } + + /// By default the rights acquired will be retained by the Security Server. + /// Use this to ensure they are destroyed and to prevent shared rights' + /// continued used by other processes. + #[inline(always)] + pub fn destroy_rights(mut self) { + self.free_flags = Flags::DESTROY_RIGHTS; + } + + /// Retrieve's the right's definition as a dictionary. Use `right_exists` + /// if you want to avoid retrieving the dictionary. + /// + /// `name` can be a wildcard right name. + /// + /// If `name` isn't convertable to a `CString` it will return + /// Err(errSecConversionError). + pub fn get_right>>(name: T) -> Result> { + let name = cstring_or_err!(name)?; + let mut dict = MaybeUninit::::uninit(); + + let status = unsafe { sys::AuthorizationRightGet(name.as_ptr(), dict.as_mut_ptr()) }; + + if status != sys::errAuthorizationSuccess { + return Err(Error::from_code(status)); + } + + let dict = unsafe { CFDictionary::wrap_under_create_rule(dict.assume_init()) }; + + Ok(dict) + } + + /// Checks if a right exists within the policy database. This is the same as + /// `get_right`, but avoids a dictionary allocation. + /// + /// If `name` isn't convertable to a `CString` it will return + /// Err(errSecConversionError). + pub fn right_exists>>(name: T) -> Result { + let name = cstring_or_err!(name)?; + + let status = unsafe { sys::AuthorizationRightGet(name.as_ptr(), std::ptr::null_mut()) }; + + Ok(status == sys::errAuthorizationSuccess) + } + + /// Removes a right from the policy database. + /// + /// `name` cannot be a wildcard right name. + /// + /// If `name` isn't convertable to a `CString` it will return + /// Err(errSecConversionError). + pub fn remove_right>>(&self, name: T) -> Result<()> { + let name = cstring_or_err!(name)?; + + let status = unsafe { sys::AuthorizationRightRemove(self.handle, name.as_ptr()) }; + + if status != sys::errAuthorizationSuccess { + return Err(Error::from_code(status)); + } + + Ok(()) + } + + /// Creates or updates a right entry in the policy database. Your process + /// must have a code signature in order to be able to add rights to the + /// authorization database. + /// + /// `name` cannot be a wildcard right. + /// + /// `definition` can be either a `CFDictionaryRef` containing keys defining + /// the rules or a `CFStringRef` representing the name of another right + /// whose rules you wish to duplicaate. + /// + /// `description` is a key which can be used to look up localized + /// descriptions. + /// + /// `bundle` will be used to get localizations from if not the main bundle. + /// + /// `localeTableName` will be used to get localizations if provided. + /// + /// If `name` isn't convertable to a `CString` it will return + /// Err(errSecConversionError). + pub fn set_right>>( + &self, + name: T, + definition: RightDefinition<'_>, + description: Option<&str>, + bundle: Option, + locale: Option<&str>, + ) -> Result<()> { + let name = cstring_or_err!(name)?; + + let definition_cfstring: CFString; + let definition_ref = match definition { + RightDefinition::FromDictionary(def) => def.as_CFTypeRef(), + RightDefinition::FromExistingRight(def) => { + definition_cfstring = CFString::new(def); + definition_cfstring.as_CFTypeRef() + } + }; + + let status = unsafe { + sys::AuthorizationRightSet( + self.handle, + name.as_ptr(), + definition_ref, + optional_str_to_cfref!(description), + bundle.unwrap_or(std::ptr::null_mut()), + optional_str_to_cfref!(locale), + ) + }; + + if status != sys::errAuthorizationSuccess { + return Err(Error::from_code(status)); + } + + Ok(()) + } + + /// An authorization plugin can store the results of an authentication + /// operation by calling the `SetContextValue` function. You can then + /// retrieve this supporting data, such as the user name. + /// + /// `tag` should specify the type of data the Security Server should return. + /// If `None`, all available information is retreieved. + /// + /// If `tag` isn't convertable to a `CString` it will return + /// Err(errSecConversionError). + pub fn copy_info>>(&self, tag: Option) -> Result> { + let tag_with_nul: CString; + + let tag_ptr = match tag { + Some(tag) => { + tag_with_nul = cstring_or_err!(tag)?; + tag_with_nul.as_ptr() + } + None => std::ptr::null(), + }; + + let mut inner = MaybeUninit::<*mut sys::AuthorizationItemSet>::uninit(); + + let status = + unsafe { sys::AuthorizationCopyInfo(self.handle, tag_ptr, inner.as_mut_ptr()) }; + + if status != sys::errAuthorizationSuccess { + return Err(Error::from(status)); + } + + let set = AuthorizationItemSet { + inner: unsafe { inner.assume_init() }, + phantom: PhantomData, + }; + + Ok(set) + } + + /// Creates an external representation of an authorization reference so that + /// you can transmit it between processes. + pub fn make_external_form(&self) -> Result { + let mut external_form = MaybeUninit::::uninit(); + + let status = + unsafe { sys::AuthorizationMakeExternalForm(self.handle, external_form.as_mut_ptr()) }; + + if status != sys::errAuthorizationSuccess { + return Err(Error::from(status)); + } + + Ok(unsafe { external_form.assume_init() }) + } + + /// Runs an executable tool with root privileges. + /// Discards executable's output + #[cfg(target_os = "macos")] + #[inline(always)] + pub fn execute_with_privileges( + &self, + command: P, + arguments: I, + flags: Flags, + ) -> Result<()> + where + P: AsRef, + I: IntoIterator, + S: AsRef, + { + use std::os::unix::ffi::OsStrExt; + + let arguments = arguments + .into_iter().flat_map(|a| CString::new(a.as_ref().as_bytes())) + .collect::>(); + self.execute_with_privileges_internal(command.as_ref().as_os_str().as_bytes(), &arguments, flags, false)?; + Ok(()) + } + + /// Runs an executable tool with root privileges, + /// and returns a `File` handle to its communication pipe + #[cfg(target_os = "macos")] + #[inline(always)] + pub fn execute_with_privileges_piped( + &self, + command: P, + arguments: I, + flags: Flags, + ) -> Result + where + P: AsRef, + I: IntoIterator, + S: AsRef, + { + use std::os::unix::ffi::OsStrExt; + + let arguments = arguments + .into_iter().flat_map(|a| CString::new(a.as_ref().as_bytes())) + .collect::>(); + Ok(self.execute_with_privileges_internal(command.as_ref().as_os_str().as_bytes(), &arguments, flags, true)?.unwrap()) + } + + /// Submits the executable for the given label as a `launchd` job. + #[cfg(all(target_os = "macos", feature = "job-bless"))] + pub fn job_bless(&self, label: &str) -> Result<(), CFError> { + #[link(name = "ServiceManagement", kind = "framework")] + extern "C" { + static kSMDomainSystemLaunchd: CFStringRef; + + fn SMJobBless( + domain: CFStringRef, + executableLabel: CFStringRef, + auth: sys::AuthorizationRef, + error: *mut CFErrorRef, + ) -> Boolean; + } + + unsafe { + let mut error = std::ptr::null_mut(); + SMJobBless( + kSMDomainSystemLaunchd, + CFString::new(label).as_concrete_TypeRef(), + self.handle, + &mut error, + ); + if !error.is_null() { + return Err(CFError::wrap_under_create_rule(error)); + } + + Ok(()) + } + } + + // Runs an executable tool with root privileges. + #[cfg(target_os = "macos")] + fn execute_with_privileges_internal( + &self, + command: &[u8], + arguments: &[CString], + flags: Flags, + make_pipe: bool, + ) -> Result> { + use std::os::unix::io::{FromRawFd, RawFd}; + + let c_cmd = cstring_or_err!(command)?; + + let mut c_args = arguments.iter().map(|a| a.as_ptr() as _).collect::>(); + c_args.push(std::ptr::null_mut()); + + let mut pipe: *mut libc::FILE = std::ptr::null_mut(); + + let status = unsafe { + sys::AuthorizationExecuteWithPrivileges( + self.handle, + c_cmd.as_ptr(), + flags.bits(), + c_args.as_ptr(), + if make_pipe { &mut pipe } else { std::ptr::null_mut() }, + ) + }; + + crate::cvt(status)?; + Ok(if make_pipe { + if pipe.is_null() { + return Err(Error::from_code(32)); // EPIPE? + } + Some(unsafe { File::from_raw_fd(libc::fileno(pipe) as RawFd) }) + } else { + None + }) + } +} + +impl Drop for Authorization { + #[inline] + fn drop(&mut self) { + unsafe { + sys::AuthorizationFree(self.handle, self.free_flags.bits()); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use core_foundation::string::CFString; + + #[test] + fn test_create_default_authorization() { + Authorization::default().unwrap(); + } + + #[test] + fn test_create_allowed_authorization() -> Result<()> { + let rights = AuthorizationItemSetBuilder::new() + .add_right("system.hdd.smart")? + .add_right("system.login.done")? + .build(); + + Authorization::new(Some(rights), None, Flags::EXTEND_RIGHTS).unwrap(); + + Ok(()) + } + + #[test] + fn test_create_then_destroy_allowed_authorization() -> Result<()> { + let rights = AuthorizationItemSetBuilder::new() + .add_right("system.hdd.smart")? + .add_right("system.login.done")? + .build(); + + let auth = Authorization::new(Some(rights), None, Flags::EXTEND_RIGHTS).unwrap(); + auth.destroy_rights(); + + Ok(()) + } + + #[test] + fn test_create_authorization_requiring_interaction() -> Result<()> { + let rights = AuthorizationItemSetBuilder::new() + .add_right("system.privilege.admin")? + .build(); + + let error = Authorization::new(Some(rights), None, Flags::EXTEND_RIGHTS).unwrap_err(); + + assert_eq!(error.code(), sys::errAuthorizationInteractionNotAllowed); + + Ok(()) + } + + fn create_credentials_env() -> Result { + let set = AuthorizationItemSetBuilder::new() + .add_string( + "username", + option_env!("USER").expect("You must set the USER environment variable"), + )? + .add_string( + "password", + option_env!("PASSWORD").expect("You must set the PASSWORD environment varible"), + )? + .build(); + + Ok(set) + } + + #[test] + fn test_create_authorization_with_bad_credentials() -> Result<()> { + let rights = AuthorizationItemSetBuilder::new() + .add_right("system.privilege.admin")? + .build(); + + let env = AuthorizationItemSetBuilder::new() + .add_string("username", "Tim Apple")? + .add_string("password", "butterfly")? + .build(); + + let error = + Authorization::new(Some(rights), Some(env), Flags::INTERACTION_ALLOWED).unwrap_err(); + + assert_eq!(error.code(), sys::errAuthorizationDenied); + + Ok(()) + } + + #[test] + fn test_create_authorization_with_credentials() -> Result<()> { + if option_env!("PASSWORD").is_none() { + return Ok(()); + } + + let rights = AuthorizationItemSetBuilder::new() + .add_right("system.privilege.admin")? + .build(); + + let env = create_credentials_env()?; + + Authorization::new(Some(rights), Some(env), Flags::EXTEND_RIGHTS).unwrap(); + + Ok(()) + } + + #[test] + fn test_query_authorization_database() -> Result<()> { + assert!(Authorization::right_exists("system.hdd.smart")?); + assert!(!Authorization::right_exists("EMPTY")?); + + let dict = Authorization::get_right("system.hdd.smart").unwrap(); + + let key = CFString::from_static_string("class"); + assert!(dict.contains_key(&key)); + + let invalid_key = CFString::from_static_string("EMPTY"); + assert!(!dict.contains_key(&invalid_key)); + + Ok(()) + } + + /// This test will only pass if its process has a valid code signature. + #[test] + fn test_modify_authorization_database() -> Result<()> { + if option_env!("PASSWORD").is_none() { + return Ok(()); + } + + let rights = AuthorizationItemSetBuilder::new() + .add_right("config.modify.")? + .build(); + + let env = create_credentials_env()?; + + let auth = Authorization::new(Some(rights), Some(env), Flags::EXTEND_RIGHTS).unwrap(); + + assert!(!Authorization::right_exists("TEST_RIGHT")?); + + auth.set_right( + "TEST_RIGHT", + RightDefinition::FromExistingRight("system.hdd.smart"), + None, + None, + None, + ) + .unwrap(); + + assert!(Authorization::right_exists("TEST_RIGHT")?); + + auth.remove_right("TEST_RIGHT").unwrap(); + + assert!(!Authorization::right_exists("TEST_RIGHT")?); + + Ok(()) + } + + /// This test will succeed if authorization popup is approved. + #[test] + fn test_execute_with_privileges() -> Result<()> { + if option_env!("PASSWORD").is_none() { + return Ok(()); + } + + let rights = AuthorizationItemSetBuilder::new() + .add_right("system.privilege.admin")? + .build(); + + let auth = Authorization::new( + Some(rights), + None, + Flags::DEFAULTS + | Flags::INTERACTION_ALLOWED + | Flags::PREAUTHORIZE + | Flags::EXTEND_RIGHTS, + )?; + + let file = auth.execute_with_privileges_piped("/bin/ls", ["/"], Flags::DEFAULTS)?; + + use std::io::{self, BufRead}; + for line in io::BufReader::new(file).lines() { + let _ = line.unwrap(); + } + + Ok(()) + } +} diff --git a/vendor/security-framework/src/base.rs b/vendor/security-framework/src/base.rs new file mode 100644 index 000000000..8c5f8e72f --- /dev/null +++ b/vendor/security-framework/src/base.rs @@ -0,0 +1,88 @@ +//! Support types for other modules. + +use core_foundation::string::CFString; +use core_foundation_sys::base::OSStatus; +use std::error; +use std::fmt; +use std::num::NonZeroI32; +use std::result; + +/// A `Result` type commonly returned by functions. +pub type Result = result::Result; + +/// A Security Framework error. +#[derive(Copy, Clone)] +pub struct Error(NonZeroI32); + +impl fmt::Debug for Error { + #[cold] + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut builder = fmt.debug_struct("Error"); + builder.field("code", &self.0); + if let Some(message) = self.message() { + builder.field("message", &message); + } + builder.finish() + } +} + +impl Error { + /// Creates a new `Error` from a status code. + /// The code must not be zero + #[inline] + #[must_use] + pub fn from_code(code: OSStatus) -> Self { + Self(NonZeroI32::new(code).unwrap_or_else(|| NonZeroI32::new(1).unwrap())) + } + + /// Returns a string describing the current error, if available. + #[inline(always)] + #[must_use] + pub fn message(self) -> Option { + self.inner_message() + } + + #[cold] + fn inner_message(self) -> Option { + use core_foundation::base::TCFType; + use security_framework_sys::base::SecCopyErrorMessageString; + use std::ptr; + + unsafe { + let s = SecCopyErrorMessageString(self.code(), ptr::null_mut()); + if s.is_null() { + None + } else { + Some(CFString::wrap_under_create_rule(s).to_string()) + } + } + } + + /// Returns the code of the current error. + #[inline(always)] + #[must_use] + pub fn code(self) -> OSStatus { + self.0.get() as _ + } +} + +impl From for Error { + #[inline(always)] + #[must_use] + fn from(code: OSStatus) -> Self { + Self::from_code(code) + } +} + +impl fmt::Display for Error { + #[cold] + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(message) = self.message() { + write!(fmt, "{}", message) + } else { + write!(fmt, "error code {}", self.code()) + } + } +} + +impl error::Error for Error {} diff --git a/vendor/security-framework/src/certificate.rs b/vendor/security-framework/src/certificate.rs new file mode 100644 index 000000000..e33168a75 --- /dev/null +++ b/vendor/security-framework/src/certificate.rs @@ -0,0 +1,320 @@ +//! Certificate support. + +use core_foundation::array::{CFArray, CFArrayRef}; +use core_foundation::base::{TCFType, ToVoid}; +use core_foundation::data::CFData; +use core_foundation::dictionary::CFMutableDictionary; +use core_foundation::string::CFString; +use core_foundation_sys::base::kCFAllocatorDefault; +#[cfg(target_os = "ios")] +use security_framework_sys::base::{errSecNotTrusted, errSecSuccess}; +use security_framework_sys::base::{errSecParam, SecCertificateRef}; +use security_framework_sys::certificate::*; +use security_framework_sys::keychain_item::SecItemDelete; +use std::fmt; +use std::ptr; + +use crate::base::{Error, Result}; +use crate::cvt; +#[cfg(any(feature = "OSX_10_12", target_os = "ios"))] +use crate::key; +#[cfg(target_os = "macos")] +use crate::os::macos::keychain::SecKeychain; +#[cfg(any(feature = "OSX_10_12", target_os = "ios"))] +use core_foundation::base::FromVoid; +#[cfg(any(feature = "OSX_10_13", target_os = "ios"))] +use core_foundation::error::{CFError, CFErrorRef}; +#[cfg(any(feature = "OSX_10_12", target_os = "ios"))] +use core_foundation::number::CFNumber; +#[cfg(feature = "serial-number-bigint")] +use num_bigint::BigUint; +use security_framework_sys::item::kSecValueRef; +#[cfg(any(feature = "OSX_10_12", target_os = "ios"))] +use std::ops::Deref; + +declare_TCFType! { + /// A type representing a certificate. + SecCertificate, SecCertificateRef +} +impl_TCFType!(SecCertificate, SecCertificateRef, SecCertificateGetTypeID); + +unsafe impl Sync for SecCertificate {} +unsafe impl Send for SecCertificate {} + +impl fmt::Debug for SecCertificate { + #[cold] + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.debug_struct("SecCertificate") + .field("subject", &self.subject_summary()) + .finish() + } +} + +impl SecCertificate { + /// Creates a `SecCertificate` from DER encoded certificate data. + pub fn from_der(der_data: &[u8]) -> Result { + let der_data = CFData::from_buffer(der_data); + unsafe { + let certificate = + SecCertificateCreateWithData(kCFAllocatorDefault, der_data.as_concrete_TypeRef()); + if certificate.is_null() { + Err(Error::from_code(errSecParam)) + } else { + Ok(Self::wrap_under_create_rule(certificate)) + } + } + } + + /// Returns DER encoded data describing this certificate. + #[must_use] + pub fn to_der(&self) -> Vec { + unsafe { + let der_data = SecCertificateCopyData(self.0); + CFData::wrap_under_create_rule(der_data).to_vec() + } + } + + /// Adds a certificate to a keychain. + #[cfg(target_os="macos")] + pub fn add_to_keychain(&self, keychain: Option) -> Result<()> { + let kch = match keychain { + Some(kch) => kch, + _ => SecKeychain::default()?, + }; + cvt(unsafe { + SecCertificateAddToKeychain(self.as_CFTypeRef() as *mut _, kch.as_CFTypeRef() as *mut _) + }) + } + + /// Returns a human readable summary of this certificate. + #[must_use] + pub fn subject_summary(&self) -> String { + unsafe { + let summary = SecCertificateCopySubjectSummary(self.0); + CFString::wrap_under_create_rule(summary).to_string() + } + } + + /// Returns a vector of email addresses for the subject of the certificate. + pub fn email_addresses(&self) -> Result, Error> { + let mut array: CFArrayRef = ptr::null(); + unsafe { + cvt(SecCertificateCopyEmailAddresses( + self.as_concrete_TypeRef(), + &mut array, + ))?; + + let array = CFArray::::wrap_under_create_rule(array); + Ok(array.into_iter().map(|p| p.to_string()).collect()) + } + } + + #[cfg(any(feature = "OSX_10_12", target_os = "ios"))] + /// Returns DER encoded X.509 distinguished name of the certificate issuer. + #[must_use] + pub fn issuer(&self) -> Vec { + unsafe { + let issuer = SecCertificateCopyNormalizedIssuerSequence(self.0); + CFData::wrap_under_create_rule(issuer).to_vec() + } + } + + #[cfg(any(feature = "OSX_10_12", target_os = "ios"))] + /// Returns DER encoded X.509 distinguished name of the certificate subject. + #[must_use] + pub fn subject(&self) -> Vec { + unsafe { + let subject = SecCertificateCopyNormalizedSubjectSequence(self.0); + CFData::wrap_under_create_rule(subject).to_vec() + } + } + + #[cfg(any(feature = "OSX_10_13", target_os = "ios"))] + /// Returns DER encoded serial number of the certificate. + pub fn serial_number_bytes(&self) -> Result, CFError> { + unsafe { + let mut error: CFErrorRef = ptr::null_mut(); + let serial_number = SecCertificateCopySerialNumberData(self.0, &mut error); + if error.is_null() { + Ok(CFData::wrap_under_create_rule(serial_number).to_vec()) + } else { + Err(CFError::wrap_under_create_rule(error)) + } + } + } + + /// Use `BigUint::from_bytes_be(serial_number_bytes())` instead + #[deprecated(note = "use serial_number_bytes()")] + #[cfg(feature = "serial-number-bigint")] + pub fn serial_number(&self) -> Result { + Ok(BigUint::from_bytes_be(&self.serial_number_bytes()?)) + } + + #[cfg(any(feature = "OSX_10_12", target_os = "ios"))] + /// Returns DER encoded subjectPublicKeyInfo of certificate if available. This can be used + /// for certificate pinning. + pub fn public_key_info_der(&self) -> Result>> { + // Imported from TrustKit + // https://github.com/datatheorem/TrustKit/blob/master/TrustKit/Pinning/TSKSPKIHashCache.m + let public_key = self.public_key()?; + Ok(self.pk_to_der(public_key)) + } + + #[cfg(any(feature = "OSX_10_12", target_os = "ios"))] + #[must_use] + fn pk_to_der(&self, public_key: key::SecKey) -> Option> { + use security_framework_sys::item::kSecAttrKeyType; + use security_framework_sys::item::kSecAttrKeySizeInBits; + + let public_key_attributes = public_key.attributes(); + let public_key_type = public_key_attributes + .find(unsafe { kSecAttrKeyType }.cast::())?; + let public_keysize = public_key_attributes + .find(unsafe { kSecAttrKeySizeInBits }.cast::())?; + let public_keysize = unsafe { CFNumber::from_void(*public_keysize.deref()) }; + let public_keysize_val = public_keysize.to_i64()? as u32; + let hdr_bytes = get_asn1_header_bytes( + unsafe { CFString::wrap_under_get_rule(*public_key_type.deref() as _) }, + public_keysize_val, + )?; + let public_key_data = public_key.external_representation()?; + let mut out = Vec::with_capacity(hdr_bytes.len() + public_key_data.len() as usize); + out.extend_from_slice(hdr_bytes); + out.extend_from_slice(public_key_data.bytes()); + Some(out) + } + + #[cfg(any(feature = "OSX_10_12", target_os = "ios"))] + /// Get public key from certificate + pub fn public_key(&self) -> Result { + use crate::policy::SecPolicy; + use crate::trust::SecTrust; + use std::slice::from_ref; + + let policy = SecPolicy::create_x509(); + let mut trust = SecTrust::create_with_certificates(from_ref(self), from_ref(&policy))?; + #[allow(deprecated)] + #[cfg(not(target_os = "ios"))] + trust.evaluate()?; + #[cfg(target_os = "ios")] + cvt(match trust.evaluate_with_error() { + Ok(_) => errSecSuccess, + Err(_) => errSecNotTrusted, + })?; + trust.copy_public_key() + } + + /// Translates to `SecItemDelete`, passing in the `SecCertificateRef` + pub fn delete(&self) -> Result<(), Error> { + let query = CFMutableDictionary::from_CFType_pairs(&[( + unsafe { kSecValueRef }.to_void(), + self.to_void(), + )]); + + cvt(unsafe { SecItemDelete(query.as_concrete_TypeRef()) }) + } +} + +#[cfg(any(feature = "OSX_10_12", target_os = "ios"))] +fn get_asn1_header_bytes(pkt: CFString, ksz: u32) -> Option<&'static [u8]> { + use security_framework_sys::item::kSecAttrKeyTypeRSA; + use security_framework_sys::item::kSecAttrKeyTypeECSECPrimeRandom; + + if pkt == unsafe { CFString::wrap_under_get_rule(kSecAttrKeyTypeRSA) } && ksz == 2048 { + return Some(&RSA_2048_ASN1_HEADER); + } + if pkt == unsafe { CFString::wrap_under_get_rule(kSecAttrKeyTypeRSA) } && ksz == 4096 { + return Some(&RSA_4096_ASN1_HEADER); + } + if pkt == unsafe { CFString::wrap_under_get_rule(kSecAttrKeyTypeECSECPrimeRandom) } + && ksz == 256 + { + return Some(&EC_DSA_SECP_256_R1_ASN1_HEADER); + } + if pkt == unsafe { CFString::wrap_under_get_rule(kSecAttrKeyTypeECSECPrimeRandom) } + && ksz == 384 + { + return Some(&EC_DSA_SECP_384_R1_ASN1_HEADER); + } + None +} + +#[cfg(any(feature = "OSX_10_12", target_os = "ios"))] +const RSA_2048_ASN1_HEADER: [u8; 24] = [ + 0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, + 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00, +]; + +#[cfg(any(feature = "OSX_10_12", target_os = "ios"))] +const RSA_4096_ASN1_HEADER: [u8; 24] = [ + 0x30, 0x82, 0x02, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, + 0x01, 0x05, 0x00, 0x03, 0x82, 0x02, 0x0f, 0x00, +]; + +#[cfg(any(feature = "OSX_10_12", target_os = "ios"))] +const EC_DSA_SECP_256_R1_ASN1_HEADER: [u8; 26] = [ + 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x08, 0x2a, + 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, +]; + +#[cfg(any(feature = "OSX_10_12", target_os = "ios"))] +const EC_DSA_SECP_384_R1_ASN1_HEADER: [u8; 23] = [ + 0x30, 0x76, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, 0x06, 0x05, 0x2b, + 0x81, 0x04, 0x00, 0x22, 0x03, 0x62, 0x00, +]; + +#[cfg(test)] +mod test { + use crate::test::certificate; + #[cfg(feature = "serial-number-bigint")] + use num_bigint::BigUint; + #[cfg(any(feature = "OSX_10_12", target_os = "ios"))] + use x509_parser::prelude::*; + + #[test] + fn subject_summary() { + let cert = certificate(); + assert_eq!("foobar.com", cert.subject_summary()); + } + + #[test] + fn email_addresses() { + let cert = certificate(); + assert_eq!(Vec::::new(), cert.email_addresses().unwrap()); + } + + #[test] + #[cfg(any(feature = "OSX_10_12", target_os = "ios"))] + fn issuer() { + let cert = certificate(); + let issuer = cert.issuer(); + let (_, name) = X509Name::from_der(&issuer).unwrap(); + let name_str = name.to_string_with_registry(oid_registry()).unwrap(); + assert_eq!( + "C=US, ST=CALIFORNIA, L=PALO ALTO, O=FOOBAR LLC, OU=DEV LAND, CN=FOOBAR.COM", + name_str + ); + } + + #[test] + #[cfg(any(feature = "OSX_10_12", target_os = "ios"))] + fn subject() { + let cert = certificate(); + let subject = cert.subject(); + let (_, name) = X509Name::from_der(&subject).unwrap(); + let name_str = name.to_string_with_registry(oid_registry()).unwrap(); + assert_eq!( + "C=US, ST=CALIFORNIA, L=PALO ALTO, O=FOOBAR LLC, OU=DEV LAND, CN=FOOBAR.COM", + name_str + ); + } + + #[test] + #[cfg(feature = "serial-number-bigint")] + #[allow(deprecated)] + fn serial_number() { + let cert = certificate(); + let serial_number = cert.serial_number().unwrap(); + assert_eq!(BigUint::from(16452297291294946383_u128), serial_number); + } +} diff --git a/vendor/security-framework/src/cipher_suite.rs b/vendor/security-framework/src/cipher_suite.rs new file mode 100644 index 000000000..4462b5e5e --- /dev/null +++ b/vendor/security-framework/src/cipher_suite.rs @@ -0,0 +1,246 @@ +//! Cipher Suites supported by Secure Transport + +use security_framework_sys::cipher_suite::*; + +macro_rules! make_suites { + ($($suite:ident),+) => { + /// TLS cipher suites. + #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] + pub struct CipherSuite(SSLCipherSuite); + + #[allow(missing_docs)] + impl CipherSuite { + $( + pub const $suite: Self = Self($suite); + )+ + + #[inline(always)] + #[must_use] + pub fn from_raw(raw: SSLCipherSuite) -> Self { + Self(raw) + } + + #[inline(always)] + #[must_use] + pub fn to_raw(&self) -> SSLCipherSuite { + self.0 + } + } + } +} + +make_suites! { + // The commented out ones up here are aliases of the matching TLS suites + SSL_NULL_WITH_NULL_NULL, + SSL_RSA_WITH_NULL_MD5, + SSL_RSA_WITH_NULL_SHA, + SSL_RSA_EXPORT_WITH_RC4_40_MD5, + SSL_RSA_WITH_RC4_128_MD5, + SSL_RSA_WITH_RC4_128_SHA, + SSL_RSA_EXPORT_WITH_RC2_CBC_40_MD5, + SSL_RSA_WITH_IDEA_CBC_SHA, + SSL_RSA_EXPORT_WITH_DES40_CBC_SHA, + SSL_RSA_WITH_DES_CBC_SHA, + //SSL_RSA_WITH_3DES_EDE_CBC_SHA, + SSL_DH_DSS_EXPORT_WITH_DES40_CBC_SHA, + SSL_DH_DSS_WITH_DES_CBC_SHA, + //SSL_DH_DSS_WITH_3DES_EDE_CBC_SHA, + SSL_DH_RSA_EXPORT_WITH_DES40_CBC_SHA, + SSL_DH_RSA_WITH_DES_CBC_SHA, + //SSL_DH_RSA_WITH_3DES_EDE_CBC_SHA, + SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA, + SSL_DHE_DSS_WITH_DES_CBC_SHA, + //SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA, + SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA, + SSL_DHE_RSA_WITH_DES_CBC_SHA, + //SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA, + SSL_DH_anon_EXPORT_WITH_RC4_40_MD5, + //SSL_DH_anon_WITH_RC4_128_MD5, + SSL_DH_anon_EXPORT_WITH_DES40_CBC_SHA, + SSL_DH_anon_WITH_DES_CBC_SHA, + //SSL_DH_anon_WITH_3DES_EDE_CBC_SHA, + SSL_FORTEZZA_DMS_WITH_NULL_SHA, + SSL_FORTEZZA_DMS_WITH_FORTEZZA_CBC_SHA, + + /* TLS addenda using AES, per RFC 3268 */ + TLS_RSA_WITH_AES_128_CBC_SHA, + TLS_DH_DSS_WITH_AES_128_CBC_SHA, + TLS_DH_RSA_WITH_AES_128_CBC_SHA, + TLS_DHE_DSS_WITH_AES_128_CBC_SHA, + TLS_DHE_RSA_WITH_AES_128_CBC_SHA, + TLS_DH_anon_WITH_AES_128_CBC_SHA, + TLS_RSA_WITH_AES_256_CBC_SHA, + TLS_DH_DSS_WITH_AES_256_CBC_SHA, + TLS_DH_RSA_WITH_AES_256_CBC_SHA, + TLS_DHE_DSS_WITH_AES_256_CBC_SHA, + TLS_DHE_RSA_WITH_AES_256_CBC_SHA, + TLS_DH_anon_WITH_AES_256_CBC_SHA, + + /* ECDSA addenda, RFC 4492 */ + TLS_ECDH_ECDSA_WITH_NULL_SHA, + TLS_ECDH_ECDSA_WITH_RC4_128_SHA, + TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA, + TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA, + TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA, + TLS_ECDHE_ECDSA_WITH_NULL_SHA, + TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, + TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA, + TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + TLS_ECDH_RSA_WITH_NULL_SHA, + TLS_ECDH_RSA_WITH_RC4_128_SHA, + TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA, + TLS_ECDH_RSA_WITH_AES_128_CBC_SHA, + TLS_ECDH_RSA_WITH_AES_256_CBC_SHA, + TLS_ECDHE_RSA_WITH_NULL_SHA, + TLS_ECDHE_RSA_WITH_RC4_128_SHA, + TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, + TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + TLS_ECDH_anon_WITH_NULL_SHA, + TLS_ECDH_anon_WITH_RC4_128_SHA, + TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA, + TLS_ECDH_anon_WITH_AES_128_CBC_SHA, + TLS_ECDH_anon_WITH_AES_256_CBC_SHA, + + /* TLS 1.2 addenda, RFC 5246 */ + + /* Initial state. */ + TLS_NULL_WITH_NULL_NULL, + + /* Server provided RSA certificate for key exchange. */ + TLS_RSA_WITH_NULL_MD5, + TLS_RSA_WITH_NULL_SHA, + TLS_RSA_WITH_RC4_128_MD5, + TLS_RSA_WITH_RC4_128_SHA, + TLS_RSA_WITH_3DES_EDE_CBC_SHA, + //TLS_RSA_WITH_AES_128_CBC_SHA, + //TLS_RSA_WITH_AES_256_CBC_SHA, + TLS_RSA_WITH_NULL_SHA256, + TLS_RSA_WITH_AES_128_CBC_SHA256, + TLS_RSA_WITH_AES_256_CBC_SHA256, + + /* Server-authenticated (and optionally client-authenticated) Diffie-Hellman. */ + TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA, + TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA, + TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA, + TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA, + //TLS_DH_DSS_WITH_AES_128_CBC_SHA, + //TLS_DH_RSA_WITH_AES_128_CBC_SHA, + //TLS_DHE_DSS_WITH_AES_128_CBC_SHA, + //TLS_DHE_RSA_WITH_AES_128_CBC_SHA, + //TLS_DH_DSS_WITH_AES_256_CBC_SHA, + //TLS_DH_RSA_WITH_AES_256_CBC_SHA, + //TLS_DHE_DSS_WITH_AES_256_CBC_SHA, + //TLS_DHE_RSA_WITH_AES_256_CBC_SHA, + TLS_DH_DSS_WITH_AES_128_CBC_SHA256, + TLS_DH_RSA_WITH_AES_128_CBC_SHA256, + TLS_DHE_DSS_WITH_AES_128_CBC_SHA256, + TLS_DHE_RSA_WITH_AES_128_CBC_SHA256, + TLS_DH_DSS_WITH_AES_256_CBC_SHA256, + TLS_DH_RSA_WITH_AES_256_CBC_SHA256, + TLS_DHE_DSS_WITH_AES_256_CBC_SHA256, + TLS_DHE_RSA_WITH_AES_256_CBC_SHA256, + + /* Completely anonymous Diffie-Hellman */ + TLS_DH_anon_WITH_RC4_128_MD5, + TLS_DH_anon_WITH_3DES_EDE_CBC_SHA, + //TLS_DH_anon_WITH_AES_128_CBC_SHA, + //TLS_DH_anon_WITH_AES_256_CBC_SHA, + TLS_DH_anon_WITH_AES_128_CBC_SHA256, + TLS_DH_anon_WITH_AES_256_CBC_SHA256, + + /* Addendum from RFC 4279, TLS PSK */ + + TLS_PSK_WITH_RC4_128_SHA, + TLS_PSK_WITH_3DES_EDE_CBC_SHA, + TLS_PSK_WITH_AES_128_CBC_SHA, + TLS_PSK_WITH_AES_256_CBC_SHA, + TLS_DHE_PSK_WITH_RC4_128_SHA, + TLS_DHE_PSK_WITH_3DES_EDE_CBC_SHA, + TLS_DHE_PSK_WITH_AES_128_CBC_SHA, + TLS_DHE_PSK_WITH_AES_256_CBC_SHA, + TLS_RSA_PSK_WITH_RC4_128_SHA, + TLS_RSA_PSK_WITH_3DES_EDE_CBC_SHA, + TLS_RSA_PSK_WITH_AES_128_CBC_SHA, + TLS_RSA_PSK_WITH_AES_256_CBC_SHA, + + /* RFC 4785 - Pre-Shared Key (PSK) Ciphersuites with NULL Encryption */ + + TLS_PSK_WITH_NULL_SHA, + TLS_DHE_PSK_WITH_NULL_SHA, + TLS_RSA_PSK_WITH_NULL_SHA, + + /* Addenda from rfc 5288 AES Galois Counter Mode (GCM) Cipher Suites + for TLS. */ + TLS_RSA_WITH_AES_128_GCM_SHA256, + TLS_RSA_WITH_AES_256_GCM_SHA384, + TLS_DHE_RSA_WITH_AES_128_GCM_SHA256, + TLS_DHE_RSA_WITH_AES_256_GCM_SHA384, + TLS_DH_RSA_WITH_AES_128_GCM_SHA256, + TLS_DH_RSA_WITH_AES_256_GCM_SHA384, + TLS_DHE_DSS_WITH_AES_128_GCM_SHA256, + TLS_DHE_DSS_WITH_AES_256_GCM_SHA384, + TLS_DH_DSS_WITH_AES_128_GCM_SHA256, + TLS_DH_DSS_WITH_AES_256_GCM_SHA384, + TLS_DH_anon_WITH_AES_128_GCM_SHA256, + TLS_DH_anon_WITH_AES_256_GCM_SHA384, + + /* RFC 5487 - PSK with SHA-256/384 and AES GCM */ + TLS_PSK_WITH_AES_128_GCM_SHA256, + TLS_PSK_WITH_AES_256_GCM_SHA384, + TLS_DHE_PSK_WITH_AES_128_GCM_SHA256, + TLS_DHE_PSK_WITH_AES_256_GCM_SHA384, + TLS_RSA_PSK_WITH_AES_128_GCM_SHA256, + TLS_RSA_PSK_WITH_AES_256_GCM_SHA384, + + TLS_PSK_WITH_AES_128_CBC_SHA256, + TLS_PSK_WITH_AES_256_CBC_SHA384, + TLS_PSK_WITH_NULL_SHA256, + TLS_PSK_WITH_NULL_SHA384, + + TLS_DHE_PSK_WITH_AES_128_CBC_SHA256, + TLS_DHE_PSK_WITH_AES_256_CBC_SHA384, + TLS_DHE_PSK_WITH_NULL_SHA256, + TLS_DHE_PSK_WITH_NULL_SHA384, + + TLS_RSA_PSK_WITH_AES_128_CBC_SHA256, + TLS_RSA_PSK_WITH_AES_256_CBC_SHA384, + TLS_RSA_PSK_WITH_NULL_SHA256, + TLS_RSA_PSK_WITH_NULL_SHA384, + + + /* Addenda from rfc 5289 Elliptic Curve Cipher Suites with + HMAC SHA-256/384. */ + TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, + TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384, + TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256, + TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384, + TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, + TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384, + TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256, + TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384, + + /* Addenda from rfc 5289 Elliptic Curve Cipher Suites with + SHA-256/384 and AES Galois Counter Mode (GCM) */ + TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256, + TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384, + TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256, + TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384, + + /* RFC 5746 - Secure Renegotiation */ + TLS_EMPTY_RENEGOTIATION_INFO_SCSV, + /* + * Tags for SSL 2 cipher kinds which are not specified + * for SSL 3. + */ + SSL_RSA_WITH_RC2_CBC_MD5, + SSL_RSA_WITH_IDEA_CBC_MD5, + SSL_RSA_WITH_DES_CBC_MD5, + SSL_RSA_WITH_3DES_EDE_CBC_MD5, + SSL_NO_SUCH_CIPHERSUITE +} diff --git a/vendor/security-framework/src/dlsym.rs b/vendor/security-framework/src/dlsym.rs new file mode 100644 index 000000000..d3229298f --- /dev/null +++ b/vendor/security-framework/src/dlsym.rs @@ -0,0 +1,50 @@ +// dlsym.rs is taken from mio +// https://github.com/carllerche/mio/blob/master/src/sys/unix/dlsym.rs + +use std::marker; +use std::mem; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use libc; + +macro_rules! dlsym { + (fn $name:ident($($t:ty),*) -> $ret:ty) => ( + #[allow(bad_style)] + static $name: $crate::dlsym::DlSym $ret> = + $crate::dlsym::DlSym { + name: concat!(stringify!($name), "\0"), + addr: ::std::sync::atomic::AtomicUsize::new(0), + _marker: ::std::marker::PhantomData, + }; + ) +} + +pub struct DlSym { + pub name: &'static str, + pub addr: AtomicUsize, + pub _marker: marker::PhantomData, +} + +impl DlSym { + pub fn get(&self) -> Option<&F> { + assert_eq!(mem::size_of::(), mem::size_of::()); + unsafe { + if self.addr.load(Ordering::SeqCst) == 0 { + self.addr.store(fetch(self.name), Ordering::SeqCst); + } + if self.addr.load(Ordering::SeqCst) == 1 { + None + } else { + mem::transmute::<&AtomicUsize, Option<&F>>(&self.addr) + } + } + } +} + +unsafe fn fetch(name: &str) -> usize { + assert_eq!(name.as_bytes()[name.len() - 1], 0); + match libc::dlsym(libc::RTLD_DEFAULT, name.as_ptr() as *const _) as usize { + 0 => 1, + n => n, + } +} diff --git a/vendor/security-framework/src/identity.rs b/vendor/security-framework/src/identity.rs new file mode 100644 index 000000000..337752511 --- /dev/null +++ b/vendor/security-framework/src/identity.rs @@ -0,0 +1,83 @@ +//! Identity support. + +use core_foundation::base::TCFType; +use core_foundation::base::ToVoid; +use core_foundation::dictionary::CFMutableDictionary; +use security_framework_sys::base::SecIdentityRef; +use security_framework_sys::identity::{SecIdentityCopyCertificate, SecIdentityCopyPrivateKey, SecIdentityGetTypeID}; +use security_framework_sys::item::kSecValueRef; +use security_framework_sys::keychain_item::SecItemDelete; +use std::fmt; +use std::ptr; + +use crate::base::Error; +use crate::base::Result; +use crate::certificate::SecCertificate; +use crate::cvt; +use crate::key::SecKey; + +declare_TCFType! { + /// A type representing an identity. + /// + /// Identities are a certificate paired with the corresponding private key. + SecIdentity, SecIdentityRef +} +impl_TCFType!(SecIdentity, SecIdentityRef, SecIdentityGetTypeID); + +unsafe impl Sync for SecIdentity {} +unsafe impl Send for SecIdentity {} + +impl fmt::Debug for SecIdentity { + #[cold] + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut builder = fmt.debug_struct("SecIdentity"); + if let Ok(cert) = self.certificate() { + builder.field("certificate", &cert); + } + if let Ok(key) = self.private_key() { + builder.field("private_key", &key); + } + builder.finish() + } +} + +impl SecIdentity { + /// Returns the certificate corresponding to this identity. + pub fn certificate(&self) -> Result { + unsafe { + let mut certificate = ptr::null_mut(); + cvt(SecIdentityCopyCertificate(self.0, &mut certificate))?; + Ok(SecCertificate::wrap_under_create_rule(certificate)) + } + } + + /// Returns the private key corresponding to this identity. + pub fn private_key(&self) -> Result { + unsafe { + let mut key = ptr::null_mut(); + cvt(SecIdentityCopyPrivateKey(self.0, &mut key))?; + Ok(SecKey::wrap_under_create_rule(key)) + } + } + + /// Translates to `SecItemDelete`, passing in the `SecIdentityRef` + pub fn delete(&self) -> Result<(), Error> { + let query = CFMutableDictionary::from_CFType_pairs(&[( + unsafe { kSecValueRef }.to_void(), + self.to_void(), + )]); + + cvt(unsafe { SecItemDelete(query.as_concrete_TypeRef()) }) + } +} + +#[cfg(test)] +mod test { + use super::SecIdentity; + + #[test] + fn identity_has_send_bound() { + fn assert_send() {} + assert_send::(); + } +} diff --git a/vendor/security-framework/src/import_export.rs b/vendor/security-framework/src/import_export.rs new file mode 100644 index 000000000..378a2d5be --- /dev/null +++ b/vendor/security-framework/src/import_export.rs @@ -0,0 +1,174 @@ +//! Security Framework type import/export support. + +use core_foundation::array::CFArray; +use core_foundation::base::{CFType, TCFType}; +use core_foundation::data::CFData; +use core_foundation::dictionary::CFDictionary; +use core_foundation::string::CFString; +use security_framework_sys::import_export::*; +use std::ptr; + +use crate::base::Result; +use crate::certificate::SecCertificate; +use crate::cvt; +use crate::identity::SecIdentity; +#[cfg(target_os = "macos")] +use crate::os::macos::access::SecAccess; +#[cfg(target_os = "macos")] +use crate::os::macos::keychain::SecKeychain; +use crate::trust::SecTrust; + +/// Information about an imported identity. +pub struct ImportedIdentity { + /// The label of the identity. + pub label: Option, + /// The ID of the identity. Typically the SHA-1 hash of the public key. + pub key_id: Option>, + /// A `SecTrust` object set up to validate this identity. + pub trust: Option, + /// A certificate chain validating this identity. + pub cert_chain: Option>, + /// The identity itself. + pub identity: Option, + _p: (), +} + +/// A builder type to import an identity from PKCS#12 formatted data. +#[derive(Default)] +pub struct Pkcs12ImportOptions { + passphrase: Option, + #[cfg(target_os = "macos")] + keychain: Option, + #[cfg(target_os = "macos")] + access: Option, +} + +#[cfg(target_os = "macos")] +impl crate::Pkcs12ImportOptionsInternals for Pkcs12ImportOptions { + #[inline(always)] + fn keychain(&mut self, keychain: SecKeychain) -> &mut Self { + self.keychain = Some(keychain); + self + } + + #[inline(always)] + fn access(&mut self, access: SecAccess) -> &mut Self { + self.access = Some(access); + self + } +} + +impl Pkcs12ImportOptions { + /// Creates a new builder with default options. + #[inline(always)] + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Specifies the passphrase to be used to decrypt the data. + /// + /// This must be specified, as unencrypted PKCS#12 data is not supported. + #[inline] + pub fn passphrase(&mut self, passphrase: &str) -> &mut Self { + self.passphrase = Some(CFString::new(passphrase)); + self + } + + /// Imports identities from PKCS#12 encoded data. + pub fn import(&self, pkcs12_data: &[u8]) -> Result> { + unsafe { + let pkcs12_data = CFData::from_buffer(pkcs12_data); + + let mut options = vec![]; + + if let Some(ref passphrase) = self.passphrase { + options.push(( + CFString::wrap_under_get_rule(kSecImportExportPassphrase), + passphrase.as_CFType(), + )); + } + + self.import_setup(&mut options); + + let options = CFDictionary::from_CFType_pairs(&options); + + let mut raw_items = ptr::null(); + cvt(SecPKCS12Import( + pkcs12_data.as_concrete_TypeRef(), + options.as_concrete_TypeRef(), + &mut raw_items, + ))?; + let raw_items = CFArray::>::wrap_under_create_rule(raw_items); + + let mut items = vec![]; + + for raw_item in &raw_items { + let label = raw_item + .find(kSecImportItemLabel) + .map(|label| CFString::wrap_under_get_rule((*label).cast()).to_string()); + let key_id = raw_item + .find(kSecImportItemKeyID) + .map(|key_id| CFData::wrap_under_get_rule((*key_id).cast()).to_vec()); + let trust = raw_item + .find(kSecImportItemTrust) + .map(|trust| SecTrust::wrap_under_get_rule(*trust as *mut _)); + let cert_chain = raw_item.find(kSecImportItemCertChain.cast()).map( + |cert_chain| { + CFArray::::wrap_under_get_rule((*cert_chain).cast()) + .iter() + .map(|c| c.clone()) + .collect() + }, + ); + let identity = raw_item + .find(kSecImportItemIdentity) + .map(|identity| SecIdentity::wrap_under_get_rule(*identity as *mut _)); + + items.push(ImportedIdentity { + label, + key_id, + trust, + cert_chain, + identity, + _p: (), + }); + } + + Ok(items) + } + } + + #[cfg(target_os = "macos")] + fn import_setup(&self, options: &mut Vec<(CFString, CFType)>) { + unsafe { + if let Some(ref keychain) = self.keychain { + options.push(( + CFString::wrap_under_get_rule(kSecImportExportKeychain), + keychain.as_CFType(), + )); + } + + if let Some(ref access) = self.access { + options.push(( + CFString::wrap_under_get_rule(kSecImportExportAccess), + access.as_CFType(), + )); + } + } + } + + #[cfg(not(target_os = "macos"))] + fn import_setup(&self, _: &mut Vec<(CFString, CFType)>) {} +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn missing_passphrase() { + let data = include_bytes!("../test/server.p12"); + assert!(Pkcs12ImportOptions::new().import(data).is_err()); + } +} diff --git a/vendor/security-framework/src/item.rs b/vendor/security-framework/src/item.rs new file mode 100644 index 000000000..6f252e00f --- /dev/null +++ b/vendor/security-framework/src/item.rs @@ -0,0 +1,689 @@ +//! Support to search for items in a keychain. + +use core_foundation::array::CFArray; +use core_foundation::base::{CFType, TCFType, ToVoid}; +use core_foundation::boolean::CFBoolean; +use core_foundation::data::CFData; +use core_foundation::date::CFDate; +use core_foundation::dictionary::{CFDictionary, CFMutableDictionary}; +use core_foundation::number::CFNumber; +use core_foundation::string::CFString; +use core_foundation_sys::base::{CFCopyDescription, CFGetTypeID, CFRelease, CFTypeRef}; +use core_foundation_sys::string::CFStringRef; +use security_framework_sys::item::*; +use security_framework_sys::keychain_item::{SecItemAdd, SecItemCopyMatching}; +use std::collections::HashMap; +use std::fmt; +use std::ptr; + +use crate::base::Result; +use crate::certificate::SecCertificate; +use crate::cvt; +use crate::identity::SecIdentity; +use crate::key::SecKey; +#[cfg(target_os = "macos")] +use crate::os::macos::keychain::SecKeychain; + +/// Specifies the type of items to search for. +#[derive(Debug, Copy, Clone)] +pub struct ItemClass(CFStringRef); + +impl ItemClass { + /// Look for `SecKeychainItem`s corresponding to generic passwords. + #[inline(always)] + #[must_use] + pub fn generic_password() -> Self { + unsafe { Self(kSecClassGenericPassword) } + } + + /// Look for `SecKeychainItem`s corresponding to internet passwords. + #[inline(always)] + #[must_use] + pub fn internet_password() -> Self { + unsafe { Self(kSecClassInternetPassword) } + } + + /// Look for `SecCertificate`s. + #[inline(always)] + #[must_use] + pub fn certificate() -> Self { + unsafe { Self(kSecClassCertificate) } + } + + /// Look for `SecKey`s. + #[inline(always)] + #[must_use] + pub fn key() -> Self { + unsafe { Self(kSecClassKey) } + } + + /// Look for `SecIdentity`s. + #[inline(always)] + #[must_use] + pub fn identity() -> Self { + unsafe { Self(kSecClassIdentity) } + } + + #[inline] + fn to_value(self) -> CFType { + unsafe { CFType::wrap_under_get_rule(self.0.cast()) } + } +} + +/// Specifies the type of keys to search for. +#[derive(Debug, Copy, Clone)] +pub struct KeyClass(CFStringRef); + +impl KeyClass { + /// `kSecAttrKeyClassPublic` + #[inline(always)] + #[must_use] pub fn public() -> Self { + unsafe { Self(kSecAttrKeyClassPublic) } + } + /// `kSecAttrKeyClassPrivate` + #[inline(always)] + #[must_use] pub fn private() -> Self { + unsafe { Self(kSecAttrKeyClassPrivate) } + } + /// `kSecAttrKeyClassSymmetric` + #[inline(always)] + #[must_use] pub fn symmetric() -> Self { + unsafe { Self(kSecAttrKeyClassSymmetric) } + } + + #[inline] + fn to_value(self) -> CFType { + unsafe { CFType::wrap_under_get_rule(self.0.cast()) } + } +} + +/// Specifies the number of results returned by a search +#[derive(Debug, Copy, Clone)] +pub enum Limit { + /// Always return all results + All, + + /// Return up to the specified number of results + Max(i64), +} + +impl Limit { + #[inline] + fn to_value(self) -> CFType { + match self { + Self::All => unsafe { CFString::wrap_under_get_rule(kSecMatchLimitAll).into_CFType() }, + Self::Max(l) => CFNumber::from(l).into_CFType(), + } + } +} + +impl From for Limit { + #[inline] + fn from(limit: i64) -> Self { + Self::Max(limit) + } +} + +/// A builder type to search for items in keychains. +#[derive(Default)] +pub struct ItemSearchOptions { + #[cfg(target_os = "macos")] + keychains: Option>, + #[cfg(not(target_os = "macos"))] + keychains: Option>, + class: Option, + key_class: Option, + load_refs: bool, + load_attributes: bool, + load_data: bool, + limit: Option, + label: Option, + service: Option, + account: Option, + access_group: Option, + pub_key_hash: Option, + app_label: Option, +} + +#[cfg(target_os = "macos")] +impl crate::ItemSearchOptionsInternals for ItemSearchOptions { + #[inline] + fn keychains(&mut self, keychains: &[SecKeychain]) -> &mut Self { + self.keychains = Some(CFArray::from_CFTypes(keychains)); + self + } +} + +impl ItemSearchOptions { + /// Creates a new builder with default options. + #[inline(always)] + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Search only for items of the specified class. + #[inline(always)] + pub fn class(&mut self, class: ItemClass) -> &mut Self { + self.class = Some(class); + self + } + + /// Search only for keys of the specified class. Also sets self.class to + /// ItemClass::key(). + #[inline(always)] + pub fn key_class(&mut self, key_class: KeyClass) -> &mut Self { + self.class(ItemClass::key()); + self.key_class = Some(key_class); + self + } + + /// Load Security Framework objects (`SecCertificate`, `SecKey`, etc) for + /// the results. + #[inline(always)] + pub fn load_refs(&mut self, load_refs: bool) -> &mut Self { + self.load_refs = load_refs; + self + } + + /// Load Security Framework object attributes for + /// the results. + #[inline(always)] + pub fn load_attributes(&mut self, load_attributes: bool) -> &mut Self { + self.load_attributes = load_attributes; + self + } + + /// Load Security Framework objects data for + /// the results. + #[inline(always)] + pub fn load_data(&mut self, load_data: bool) -> &mut Self { + self.load_data = load_data; + self + } + + /// Limit the number of search results. + /// + /// If this is not called, the default limit is 1. + #[inline(always)] + pub fn limit>(&mut self, limit: T) -> &mut Self { + self.limit = Some(limit.into()); + self + } + + /// Search for an item with the given label. + #[inline(always)] + pub fn label(&mut self, label: &str) -> &mut Self { + self.label = Some(CFString::new(label)); + self + } + + /// Search for an item with the given service. + #[inline(always)] + pub fn service(&mut self, service: &str) -> &mut Self { + self.service = Some(CFString::new(service)); + self + } + + /// Search for an item with the given account. + #[inline(always)] + pub fn account(&mut self, account: &str) -> &mut Self { + self.account = Some(CFString::new(account)); + self + } + + /// Sets `kSecAttrAccessGroup` to `kSecAttrAccessGroupToken` + #[inline(always)] + pub fn access_group_token(&mut self) -> &mut Self { + self.access_group = unsafe { Some(CFString::wrap_under_get_rule(kSecAttrAccessGroupToken)) }; + self + } + + /// Search for a certificate with the given public key hash. + /// + /// This is only compatible with [`ItemClass::certificate`], to search for + /// a key by public key hash use [`ItemSearchOptions::application_label`] + /// instead. + #[inline(always)] + pub fn pub_key_hash(&mut self, pub_key_hash: &[u8]) -> &mut Self { + self.pub_key_hash = Some(CFData::from_buffer(pub_key_hash)); + self + } + + /// Search for a key with the given public key hash. + /// + /// This is only compatible with [`ItemClass::key`], to search for a + /// certificate by the public key hash use [`ItemSearchOptions::pub_key_hash`] + /// instead. + #[inline(always)] + pub fn application_label(&mut self, app_label: &[u8]) -> &mut Self { + self.app_label = Some(CFData::from_buffer(app_label)); + self + } + + /// Search for objects. + pub fn search(&self) -> Result> { + unsafe { + let mut params = vec![]; + + if let Some(ref keychains) = self.keychains { + params.push(( + CFString::wrap_under_get_rule(kSecMatchSearchList), + keychains.as_CFType(), + )); + } + + if let Some(class) = self.class { + params.push((CFString::wrap_under_get_rule(kSecClass), class.to_value())); + } + + if let Some(key_class) = self.key_class { + params.push((CFString::wrap_under_get_rule(kSecAttrKeyClass), key_class.to_value())); + } + + if self.load_refs { + params.push(( + CFString::wrap_under_get_rule(kSecReturnRef), + CFBoolean::true_value().into_CFType(), + )); + } + + if self.load_attributes { + params.push(( + CFString::wrap_under_get_rule(kSecReturnAttributes), + CFBoolean::true_value().into_CFType(), + )); + } + + if self.load_data { + params.push(( + CFString::wrap_under_get_rule(kSecReturnData), + CFBoolean::true_value().into_CFType(), + )); + } + + if let Some(limit) = self.limit { + params.push(( + CFString::wrap_under_get_rule(kSecMatchLimit), + limit.to_value(), + )); + } + + if let Some(ref label) = self.label { + params.push(( + CFString::wrap_under_get_rule(kSecAttrLabel), + label.as_CFType(), + )); + } + + if let Some(ref service) = self.service { + params.push(( + CFString::wrap_under_get_rule(kSecAttrService), + service.as_CFType(), + )); + } + + if let Some(ref account) = self.account { + params.push(( + CFString::wrap_under_get_rule(kSecAttrAccount), + account.as_CFType(), + )); + } + + if let Some(ref access_group) = self.access_group { + params.push(( + CFString::wrap_under_get_rule(kSecAttrAccessGroup), + access_group.as_CFType(), + )); + } + + if let Some(ref pub_key_hash) = self.pub_key_hash { + params.push(( + CFString::wrap_under_get_rule(kSecAttrPublicKeyHash), + pub_key_hash.as_CFType(), + )); + } + + if let Some(ref app_label) = self.app_label { + params.push(( + CFString::wrap_under_get_rule(kSecAttrApplicationLabel), + app_label.as_CFType(), + )); + } + + let params = CFDictionary::from_CFType_pairs(¶ms); + + let mut ret = ptr::null(); + cvt(SecItemCopyMatching(params.as_concrete_TypeRef(), &mut ret))?; + if ret.is_null() { + // SecItemCopyMatching returns NULL if no load_* was specified, + // causing a segfault. + return Ok(vec![]); + } + let type_id = CFGetTypeID(ret); + + let mut items = vec![]; + + if type_id == CFArray::::type_id() { + let array: CFArray = CFArray::wrap_under_create_rule(ret as *mut _); + for item in array.iter() { + items.push(get_item(item.as_CFTypeRef())); + } + } else { + items.push(get_item(ret)); + // This is a bit janky, but get_item uses wrap_under_get_rule + // which bumps the refcount but we want create semantics + CFRelease(ret); + } + + Ok(items) + } + } +} + +unsafe fn get_item(item: CFTypeRef) -> SearchResult { + let type_id = CFGetTypeID(item); + + if type_id == CFData::type_id() { + let data = CFData::wrap_under_get_rule(item as *mut _); + let mut buf = Vec::new(); + buf.extend_from_slice(data.bytes()); + return SearchResult::Data(buf); + } + + if type_id == CFDictionary::<*const u8, *const u8>::type_id() { + return SearchResult::Dict(CFDictionary::wrap_under_get_rule(item as *mut _)); + } + + #[cfg(target_os = "macos")] + { + use crate::os::macos::keychain_item::SecKeychainItem; + if type_id == SecKeychainItem::type_id() { + return SearchResult::Ref(Reference::KeychainItem( + SecKeychainItem::wrap_under_get_rule(item as *mut _), + )); + } + } + + let reference = if type_id == SecCertificate::type_id() { + Reference::Certificate(SecCertificate::wrap_under_get_rule(item as *mut _)) + } else if type_id == SecKey::type_id() { + Reference::Key(SecKey::wrap_under_get_rule(item as *mut _)) + } else if type_id == SecIdentity::type_id() { + Reference::Identity(SecIdentity::wrap_under_get_rule(item as *mut _)) + } else { + panic!("Got bad type from SecItemCopyMatching: {}", type_id); + }; + + SearchResult::Ref(reference) +} + +/// An enum including all objects whose references can be returned from a search. +/// Note that generic _Keychain Items_, such as passwords and preferences, do +/// not have specific object types; they are modeled using dictionaries and so +/// are available directly as search results in variant `SearchResult::Dict`. +#[derive(Debug)] +pub enum Reference { + /// A `SecIdentity`. + Identity(SecIdentity), + /// A `SecCertificate`. + Certificate(SecCertificate), + /// A `SecKey`. + Key(SecKey), + /// A `SecKeychainItem`. + /// + /// Only defined on OSX + #[cfg(target_os = "macos")] + KeychainItem(crate::os::macos::keychain_item::SecKeychainItem), + #[doc(hidden)] + __NonExhaustive, +} + +/// An individual search result. +pub enum SearchResult { + /// A reference to the Security Framework object, if asked for. + Ref(Reference), + /// A dictionary of data about the Security Framework object, if asked for. + Dict(CFDictionary), + /// The Security Framework object as bytes, if asked for. + Data(Vec), + /// An unknown representation of the Security Framework object. + Other, +} + +impl fmt::Debug for SearchResult { + #[cold] + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + Self::Ref(ref reference) => fmt + .debug_struct("SearchResult::Ref") + .field("reference", reference) + .finish(), + Self::Data(ref buf) => fmt + .debug_struct("SearchResult::Data") + .field("data", buf) + .finish(), + Self::Dict(_) => { + let mut debug = fmt.debug_struct("SearchResult::Dict"); + for (k, v) in self.simplify_dict().unwrap() { + debug.field(&k, &v); + } + debug.finish() + } + Self::Other => write!(fmt, "SearchResult::Other"), + } + } +} + +impl SearchResult { + /// If the search result is a `CFDict`, simplify that to a + /// `HashMap`. This transformation isn't + /// comprehensive, it only supports `CFString`, `CFDate`, and `CFData` + /// value types. + #[must_use] + pub fn simplify_dict(&self) -> Option> { + match *self { + Self::Dict(ref d) => unsafe { + let mut retmap = HashMap::new(); + let (keys, values) = d.get_keys_and_values(); + for (k, v) in keys.iter().zip(values.iter()) { + let keycfstr = CFString::wrap_under_get_rule((*k).cast()); + let val: String = match CFGetTypeID(*v) { + cfstring if cfstring == CFString::type_id() => { + format!("{}", CFString::wrap_under_get_rule((*v).cast())) + } + cfdata if cfdata == CFData::type_id() => { + let buf = CFData::wrap_under_get_rule((*v).cast()); + let mut vec = Vec::new(); + vec.extend_from_slice(buf.bytes()); + format!("{}", String::from_utf8_lossy(&vec)) + } + cfdate if cfdate == CFDate::type_id() => format!( + "{}", + CFString::wrap_under_create_rule(CFCopyDescription(*v)) + ), + _ => String::from("unknown"), + }; + retmap.insert(format!("{}", keycfstr), val); + } + Some(retmap) + }, + _ => None, + } + } +} + +/// Builder-pattern struct for specifying options for `add_item` (`SecAddItem` +/// wrapper). +/// +/// When finished populating options, call `to_dictionary()` and pass the +/// resulting `CFDictionary` to `add_item`. +pub struct ItemAddOptions { + /// The value (by ref or data) of the item to add, required. + pub value: ItemAddValue, + /// Optional kSecAttrLabel attribute. + pub label: Option, + /// Optional keychain location. + pub location: Option, +} + +impl ItemAddOptions { + /// Specifies the item to add. + #[must_use] pub fn new(value: ItemAddValue) -> Self { + Self{ value, label: None, location: None } + } + /// Specifies the `kSecAttrLabel` attribute. + pub fn set_label(&mut self, label: impl Into) -> &mut Self { + self.label = Some(label.into()); + self + } + /// Specifies which keychain to add the item to. + pub fn set_location(&mut self, location: Location) -> &mut Self { + self.location = Some(location); + self + } + /// Populates a `CFDictionary` to be passed to + pub fn to_dictionary(&self) -> CFDictionary { + let mut dict = CFMutableDictionary::from_CFType_pairs(&[]); + + let class_opt = match &self.value { + ItemAddValue::Ref(ref_) => ref_.class(), + ItemAddValue::Data { class, .. } => Some(*class), + }; + if let Some(class) = class_opt { + dict.add(&unsafe { kSecClass }.to_void(), &class.0.to_void()); + } + + let value_pair = match &self.value { + ItemAddValue::Ref(ref_) => (unsafe { kSecValueRef }.to_void(), ref_.ref_()), + ItemAddValue::Data { data, .. } => (unsafe { kSecValueData }.to_void(), data.to_void()), + }; + dict.add(&value_pair.0, &value_pair.1); + + if let Some(location) = &self.location { + match location { + #[cfg(any(feature = "OSX_10_15", target_os = "ios"))] + Location::DataProtectionKeychain => { + dict.add( + &unsafe { kSecUseDataProtectionKeychain }.to_void(), + &CFBoolean::true_value().to_void(), + ); + } + #[cfg(target_os = "macos")] + Location::DefaultFileKeychain => {} + #[cfg(target_os = "macos")] + Location::FileKeychain(keychain) => { + dict.add(&unsafe { kSecUseKeychain }.to_void(), &keychain.to_void()); + }, + } + } + + let label = self.label.as_deref().map(CFString::from); + if let Some(label) = &label { + dict.add(&unsafe { kSecAttrLabel }.to_void(), &label.to_void()); + } + + dict.to_immutable() + } +} + +/// Value of an item to add to the keychain. +pub enum ItemAddValue { + /// Pass item by Ref (kSecValueRef) + Ref(AddRef), + /// Pass item by Data (kSecValueData) + Data { + /// The item class (kSecClass). + class: ItemClass, + /// The item data. + data: CFData, + }, +} + +/// Type of Ref to add to the keychain. +pub enum AddRef { + /// SecKey + Key(SecKey), + /// SecIdentity + Identity(SecIdentity), + /// SecCertificate + Certificate(SecCertificate), +} + +impl AddRef { + fn class(&self) -> Option { + match self { + AddRef::Key(_) => Some(ItemClass::key()), + // kSecClass should not be specified when adding a SecIdentityRef: + // https://developer.apple.com/forums/thread/25751 + AddRef::Identity(_) => None, + AddRef::Certificate(_) => Some(ItemClass::certificate()), + } + } + fn ref_(&self) -> CFTypeRef { + match self { + AddRef::Key(key) => key.as_CFTypeRef(), + AddRef::Identity(id) => id.as_CFTypeRef(), + AddRef::Certificate(cert) => cert.as_CFTypeRef(), + } + } +} + +/// Which keychain to add an item to. +/// +/// +pub enum Location { + /// Store the item in the newer DataProtectionKeychain. This is the only + /// keychain on iOS. On macOS, this is the newer and more consistent + /// keychain implementation. Keys stored in the Secure Enclave _must_ use + /// this keychain. + /// + /// This keychain requires the calling binary to be codesigned with + /// entitlements for the KeychainAccessGroups it is supposed to + /// access. + #[cfg(any(feature = "OSX_10_15", target_os = "ios"))] + DataProtectionKeychain, + /// Store the key in the default file-based keychain. On macOS, defaults to + /// the Login keychain. + #[cfg(target_os = "macos")] + DefaultFileKeychain, + /// Store the key in a specific file-based keychain. + #[cfg(target_os = "macos")] + FileKeychain(crate::os::macos::keychain::SecKeychain), +} + +/// Translates to `SecItemAdd`. Use `ItemAddOptions` to build an `add_params` +/// `CFDictionary`. +pub fn add_item(add_params: CFDictionary) -> Result<()> { + cvt(unsafe { SecItemAdd(add_params.as_concrete_TypeRef(), std::ptr::null_mut()) }) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn find_nothing() { + assert!(ItemSearchOptions::new().search().is_err()); + } + + #[test] + fn limit_two() { + let results = ItemSearchOptions::new() + .class(ItemClass::certificate()) + .limit(2) + .search() + .unwrap(); + assert_eq!(results.len(), 2); + } + + #[test] + fn limit_all() { + let results = ItemSearchOptions::new() + .class(ItemClass::certificate()) + .limit(Limit::All) + .search() + .unwrap(); + assert!(results.len() >= 2); + } +} diff --git a/vendor/security-framework/src/key.rs b/vendor/security-framework/src/key.rs new file mode 100644 index 000000000..6609ffded --- /dev/null +++ b/vendor/security-framework/src/key.rs @@ -0,0 +1,387 @@ +//! Encryption key support + +use crate::cvt; +use core_foundation::{ + base::TCFType, string::{CFStringRef, CFString}, + dictionary::CFMutableDictionary, +}; +use core_foundation::base::ToVoid; +#[cfg(any(feature = "OSX_10_12", target_os = "ios"))] +use core_foundation::boolean::CFBoolean; +#[cfg(any(feature = "OSX_10_12", target_os = "ios"))] +use core_foundation::data::CFData; +#[cfg(any(feature = "OSX_10_12", target_os = "ios"))] +use core_foundation::dictionary::CFDictionary; +#[cfg(any(feature = "OSX_10_12", target_os = "ios"))] +use core_foundation::number::CFNumber; +#[cfg(any(feature = "OSX_10_12", target_os = "ios"))] +use core_foundation::error::{CFError, CFErrorRef}; + +use security_framework_sys::{ + item::{kSecAttrKeyTypeRSA, kSecValueRef}, + keychain_item::SecItemDelete +}; +#[cfg(any(feature = "OSX_10_12", target_os = "ios"))] +use security_framework_sys::{item::{ + kSecAttrIsPermanent, kSecAttrLabel, kSecAttrKeyType, + kSecAttrKeySizeInBits, kSecPrivateKeyAttrs +}}; +#[cfg(target_os="macos")] +use security_framework_sys::item::{ + kSecAttrKeyType3DES, kSecAttrKeyTypeDSA, kSecAttrKeyTypeAES, + kSecAttrKeyTypeDES, kSecAttrKeyTypeRC4, kSecAttrKeyTypeCAST, +}; + +use security_framework_sys::key::SecKeyGetTypeID; +use security_framework_sys::base::SecKeyRef; + +#[cfg(any(feature = "OSX_10_12", target_os = "ios"))] +pub use security_framework_sys::key::Algorithm; + +#[cfg(any(feature = "OSX_10_12", target_os = "ios"))] +use security_framework_sys::key::{ + SecKeyCopyAttributes, SecKeyCopyExternalRepresentation, + SecKeyCreateSignature, SecKeyCreateRandomKey, + SecKeyCopyPublicKey, +}; +#[cfg(any(feature = "OSX_10_12", target_os = "ios"))] +use security_framework_sys::item::kSecAttrApplicationLabel; +use std::fmt; + +use crate::base::Error; +#[cfg(any(feature = "OSX_10_12", target_os = "ios"))] +use crate::item::Location; + +/// Types of `SecKey`s. +#[derive(Debug, Copy, Clone)] +pub struct KeyType(CFStringRef); + +#[allow(missing_docs)] +impl KeyType { + #[inline(always)] + #[must_use] + pub fn rsa() -> Self { + unsafe { Self(kSecAttrKeyTypeRSA) } + } + + #[cfg(target_os = "macos")] + #[inline(always)] + #[must_use] + pub fn dsa() -> Self { + unsafe { Self(kSecAttrKeyTypeDSA) } + } + + #[cfg(target_os = "macos")] + #[inline(always)] + #[must_use] + pub fn aes() -> Self { + unsafe { Self(kSecAttrKeyTypeAES) } + } + + #[cfg(target_os = "macos")] + #[inline(always)] + #[must_use] + pub fn des() -> Self { + unsafe { Self(kSecAttrKeyTypeDES) } + } + + #[cfg(target_os = "macos")] + #[inline(always)] + #[must_use] + pub fn triple_des() -> Self { + unsafe { Self(kSecAttrKeyType3DES) } + } + + #[cfg(target_os = "macos")] + #[inline(always)] + #[must_use] + pub fn rc4() -> Self { + unsafe { Self(kSecAttrKeyTypeRC4) } + } + + #[cfg(target_os = "macos")] + #[inline(always)] + #[must_use] + pub fn cast() -> Self { + unsafe { Self(kSecAttrKeyTypeCAST) } + } + + #[cfg(any(feature = "OSX_10_9", target_os = "ios"))] + #[inline(always)] + #[must_use] + pub fn ec() -> Self { + use security_framework_sys::item::kSecAttrKeyTypeEC; + + unsafe { Self(kSecAttrKeyTypeEC) } + } + + pub(crate) fn to_str(self) -> CFString { + unsafe { CFString::wrap_under_get_rule(self.0) } + } +} + +declare_TCFType! { + /// A type representing an encryption key. + SecKey, SecKeyRef +} +impl_TCFType!(SecKey, SecKeyRef, SecKeyGetTypeID); + +unsafe impl Sync for SecKey {} +unsafe impl Send for SecKey {} + +impl SecKey { + #[cfg(any(feature = "OSX_10_12", target_os = "ios"))] + /// Translates to `SecKeyCreateRandomKey` + /// `GenerateKeyOptions` provides a helper to create an attribute + /// `CFDictionary`. + pub fn generate(attributes: CFDictionary) -> Result { + let mut error: CFErrorRef = ::std::ptr::null_mut(); + let sec_key = unsafe { SecKeyCreateRandomKey(attributes.as_concrete_TypeRef(), &mut error)}; + if !error.is_null() { + Err(unsafe { CFError::wrap_under_create_rule(error) }) + } else { + Ok(unsafe { SecKey::wrap_under_create_rule(sec_key) }) + } + } + + /// Returns the programmatic identifier for the key. For keys of class + /// kSecAttrKeyClassPublic and kSecAttrKeyClassPrivate, the value is the + /// hash of the public key. + #[cfg(any(feature = "OSX_10_12", target_os = "ios"))] + pub fn application_label(&self) -> Option> { + self.attributes() + .find(unsafe { kSecAttrApplicationLabel.to_void() }) + .map(|v| unsafe { CFData::wrap_under_get_rule(v.cast()) }.to_vec()) + } + + #[cfg(any(feature = "OSX_10_12", target_os = "ios"))] + /// Translates to `SecKeyCopyAttributes` + #[must_use] + pub fn attributes(&self) -> CFDictionary { + let pka = unsafe { SecKeyCopyAttributes(self.to_void() as _) }; + unsafe { CFDictionary::wrap_under_create_rule(pka) } + } + + #[cfg(any(feature = "OSX_10_12", target_os = "ios"))] + /// Translates to `SecKeyCopyExternalRepresentation` + #[must_use] + pub fn external_representation(&self) -> Option { + let mut error: CFErrorRef = ::std::ptr::null_mut(); + let data = unsafe { SecKeyCopyExternalRepresentation(self.to_void() as _, &mut error) }; + if data.is_null() { + return None; + } + Some(unsafe { CFData::wrap_under_create_rule(data) }) + } + + #[cfg(any(feature = "OSX_10_12", target_os = "ios"))] + /// Translates to `SecKeyCopyPublicKey` + #[must_use] + pub fn public_key(&self) -> Option { + let pub_seckey = unsafe { SecKeyCopyPublicKey(self.0.cast()) }; + if pub_seckey.is_null() { + return None; + } + + Some(unsafe { SecKey::wrap_under_create_rule(pub_seckey) }) + } + + #[cfg(any(feature = "OSX_10_12", target_os = "ios"))] + /// Creates the cryptographic signature for a block of data using a private + /// key and specified algorithm. + pub fn create_signature(&self, algorithm: Algorithm, input: &[u8]) -> Result, CFError> { + let mut error: CFErrorRef = std::ptr::null_mut(); + + let output = unsafe { + SecKeyCreateSignature( + self.as_concrete_TypeRef(), + algorithm.into(), + CFData::from_buffer(input).as_concrete_TypeRef(), + &mut error, + ) + }; + + if !error.is_null() { + Err(unsafe { CFError::wrap_under_create_rule(error) }) + } else { + let output = unsafe { CFData::wrap_under_create_rule(output) }; + Ok(output.to_vec()) + } + } + + /// Verifies the cryptographic signature for a block of data using a public + /// key and specified algorithm. + #[cfg(any(feature = "OSX_10_12", target_os = "ios"))] + pub fn verify_signature(&self, algorithm: Algorithm, signed_data: &[u8], signature: &[u8]) -> Result { + use security_framework_sys::key::SecKeyVerifySignature; + let mut error: CFErrorRef = std::ptr::null_mut(); + + let valid = unsafe { + SecKeyVerifySignature( + self.as_concrete_TypeRef(), + algorithm.into(), + CFData::from_buffer(signed_data).as_concrete_TypeRef(), + CFData::from_buffer(signature).as_concrete_TypeRef(), + &mut error, + ) + }; + + if !error.is_null() { + return Err(unsafe { CFError::wrap_under_create_rule(error) })?; + } + Ok(valid != 0) + } + + /// Translates to `SecItemDelete`, passing in the `SecKeyRef` + pub fn delete(&self) -> Result<(), Error> { + let query = CFMutableDictionary::from_CFType_pairs(&[( + unsafe { kSecValueRef }.to_void(), + self.to_void(), + )]); + + cvt(unsafe { SecItemDelete(query.as_concrete_TypeRef()) }) + } +} + +/// Where to generate the key. +pub enum Token { + /// Generate the key in software, compatible with all `KeyType`s. + Software, + /// Generate the key in the Secure Enclave such that the private key is not + /// extractable. Only compatible with `KeyType::ec()`. + SecureEnclave, +} + +/// Helper for creating `CFDictionary` attributes for `SecKey::generate` +/// Recommended reading: +/// +#[derive(Default)] +#[cfg(any(feature = "OSX_10_12", target_os = "ios"))] +pub struct GenerateKeyOptions { + /// kSecAttrKeyType + pub key_type: Option, + /// kSecAttrKeySizeInBits + pub size_in_bits: Option, + /// kSecAttrLabel + pub label: Option, + /// kSecAttrTokenID + pub token: Option, + /// Which keychain to store the key in, if any. + pub location: Option, +} + +#[cfg(any(feature = "OSX_10_12", target_os = "ios"))] +impl GenerateKeyOptions { + /// Set `key_type` + pub fn set_key_type(&mut self, key_type: KeyType) -> &mut Self { + self.key_type = Some(key_type); + self + } + /// Set `size_in_bits` + pub fn set_size_in_bits(&mut self, size_in_bits: u32) -> &mut Self { + self.size_in_bits = Some(size_in_bits); + self + } + /// Set `label` + pub fn set_label(&mut self, label: impl Into) -> &mut Self { + self.label = Some(label.into()); + self + } + /// Set `token` + pub fn set_token(&mut self, token: Token) -> &mut Self { + self.token = Some(token); + self + } + /// Set `location` + pub fn set_location(&mut self, location: Location) -> &mut Self { + self.location = Some(location); + self + } + + /// Collect options into a `CFDictioanry` + pub fn to_dictionary(&self) -> CFDictionary { + #[cfg(target_os = "macos")] + use security_framework_sys::item::kSecUseKeychain; + use security_framework_sys::item::{ + kSecAttrTokenID, kSecAttrTokenIDSecureEnclave, kSecPublicKeyAttrs, + }; + + let is_permanent = CFBoolean::from(self.location.is_some()); + let private_attributes = CFMutableDictionary::from_CFType_pairs(&[( + unsafe { kSecAttrIsPermanent }.to_void(), + is_permanent.to_void(), + )]); + + let public_attributes = CFMutableDictionary::from_CFType_pairs(&[( + unsafe { kSecAttrIsPermanent }.to_void(), + is_permanent.to_void(), + )]); + + let key_type = self.key_type.unwrap_or_else(KeyType::rsa).to_str(); + + let size_in_bits = self.size_in_bits.unwrap_or(match () { + _ if key_type == KeyType::rsa().to_str() => 2048, + _ if key_type == KeyType::ec().to_str() => 256, + _ => 256, + }); + let size_in_bits = CFNumber::from(size_in_bits as i32); + + let mut attribute_key_values = vec![ + (unsafe { kSecAttrKeyType }.to_void(), key_type.to_void()), + ( + unsafe { kSecAttrKeySizeInBits }.to_void(), + size_in_bits.to_void(), + ), + ( + unsafe { kSecPrivateKeyAttrs }.to_void(), + private_attributes.to_void(), + ), + ( + unsafe { kSecPublicKeyAttrs }.to_void(), + public_attributes.to_void(), + ), + ]; + let label = self.label.as_deref().map(CFString::new); + if let Some(label) = &label { + attribute_key_values.push((unsafe { kSecAttrLabel }.to_void(), label.to_void())); + } + + #[cfg(target_os = "macos")] + match &self.location { + #[cfg(feature = "OSX_10_15")] + Some(Location::DataProtectionKeychain) => { + use security_framework_sys::item::kSecUseDataProtectionKeychain; + attribute_key_values.push(( + unsafe { kSecUseDataProtectionKeychain }.to_void(), + CFBoolean::true_value().to_void(), + )); + } + Some(Location::FileKeychain(keychain)) => { + attribute_key_values.push(( + unsafe { kSecUseKeychain }.to_void(), + keychain.as_concrete_TypeRef().to_void(), + )); + } + _ => {} + } + + match self.token.as_ref().unwrap_or(&Token::Software) { + Token::Software => {}, + Token::SecureEnclave => { + attribute_key_values.push(( + unsafe { kSecAttrTokenID }.to_void(), + unsafe { kSecAttrTokenIDSecureEnclave }.to_void(), + )); + } + } + + CFMutableDictionary::from_CFType_pairs(&attribute_key_values).to_immutable() + } +} + +impl fmt::Debug for SecKey { + #[cold] + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.debug_struct("SecKey").finish_non_exhaustive() + } +} diff --git a/vendor/security-framework/src/lib.rs b/vendor/security-framework/src/lib.rs new file mode 100644 index 000000000..304c80734 --- /dev/null +++ b/vendor/security-framework/src/lib.rs @@ -0,0 +1,92 @@ +//! Wrappers around the OSX Security Framework. +#![warn(missing_docs)] +#![allow(non_upper_case_globals)] +#![allow(clippy::manual_non_exhaustive)] // MSRV + +#[macro_use] +extern crate core_foundation; + +use core_foundation_sys::base::OSStatus; +use security_framework_sys::base::errSecSuccess; + +use crate::base::{Error, Result}; +#[cfg(target_os = "macos")] +use crate::os::macos::access::SecAccess; +#[cfg(target_os = "macos")] +use crate::os::macos::keychain::SecKeychain; + +#[cfg(test)] +macro_rules! p { + ($e:expr) => { + match $e { + Ok(s) => s, + Err(e) => panic!("{:?}", e), + } + }; +} + +#[cfg(all(not(feature = "OSX_10_13"), any(feature = "alpn", feature = "session-tickets")))] +#[macro_use] +mod dlsym; + +pub mod access_control; +#[cfg(target_os = "macos")] +pub mod authorization; +pub mod base; +#[cfg(any(target_os = "macos", target_os = "ios"))] +pub mod certificate; +pub mod cipher_suite; +#[cfg(any(target_os = "macos", target_os = "ios"))] +pub mod identity; +#[cfg(any(target_os = "macos", target_os = "ios"))] +pub mod import_export; +#[cfg(any(target_os = "macos", target_os = "ios"))] +pub mod item; +#[cfg(any(target_os = "macos", target_os = "ios"))] +pub mod key; +pub mod os; +pub mod passwords; +pub mod passwords_options; +#[cfg(any(target_os = "macos", target_os = "ios"))] +pub mod policy; +pub mod random; +#[cfg(any(target_os = "macos", target_os = "ios"))] +pub mod secure_transport; +#[cfg(any(target_os = "macos", target_os = "ios"))] +pub mod trust; +#[cfg(target_os = "macos")] +pub mod trust_settings; + +#[cfg(target_os = "macos")] +trait Pkcs12ImportOptionsInternals { + fn keychain(&mut self, keychain: SecKeychain) -> &mut Self; + fn access(&mut self, access: SecAccess) -> &mut Self; +} + +#[cfg(target_os = "macos")] +trait ItemSearchOptionsInternals { + fn keychains(&mut self, keychains: &[SecKeychain]) -> &mut Self; +} + +trait AsInner { + type Inner; + fn as_inner(&self) -> Self::Inner; +} + +#[inline(always)] +fn cvt(err: OSStatus) -> Result<()> { + match err { + errSecSuccess => Ok(()), + err => Err(Error::from_code(err)), + } +} + +#[cfg(test)] +mod test { + use crate::certificate::SecCertificate; + + pub fn certificate() -> SecCertificate { + let certificate = include_bytes!("../test/server.der"); + p!(SecCertificate::from_der(certificate)) + } +} diff --git a/vendor/security-framework/src/os/macos/access.rs b/vendor/security-framework/src/os/macos/access.rs new file mode 100644 index 000000000..1c41d85d9 --- /dev/null +++ b/vendor/security-framework/src/os/macos/access.rs @@ -0,0 +1,14 @@ +//! Access functionality. + +use core_foundation::base::TCFType; +use security_framework_sys::access::SecAccessGetTypeID; +use security_framework_sys::base::SecAccessRef; + +declare_TCFType! { + /// A type representing access settings. + SecAccess, SecAccessRef +} +impl_TCFType!(SecAccess, SecAccessRef, SecAccessGetTypeID); + +unsafe impl Sync for SecAccess {} +unsafe impl Send for SecAccess {} diff --git a/vendor/security-framework/src/os/macos/certificate.rs b/vendor/security-framework/src/os/macos/certificate.rs new file mode 100644 index 000000000..7f910dc6c --- /dev/null +++ b/vendor/security-framework/src/os/macos/certificate.rs @@ -0,0 +1,269 @@ +//! OSX specific extensions to certificate functionality. + +use core_foundation::array::{CFArray, CFArrayIterator}; +use core_foundation::base::TCFType; +use core_foundation::base::ToVoid; +use core_foundation::data::CFData; +use core_foundation::dictionary::CFDictionary; +use core_foundation::error::CFError; +use core_foundation::string::CFString; +use security_framework_sys::certificate::*; +use std::convert::TryInto; +use std::os::raw::c_void; +use std::ptr; + +use crate::base::Error; +use crate::certificate::SecCertificate; +use crate::cvt; +use crate::key::SecKey; +use crate::os::macos::certificate_oids::CertificateOid; +use crate::os::macos::digest_transform::{Builder, DigestType}; + +/// An extension trait adding OSX specific functionality to `SecCertificate`. +pub trait SecCertificateExt { + /// Returns the common name associated with the certificate. + fn common_name(&self) -> Result; + + /// Returns the public key associated with the certificate. + #[cfg_attr(not(feature = "OSX_10_14"), deprecated(note = "Uses deprecated SecCertificateCopyPublicKey. Enable OSX_10_14 feature to avoid it"))] + fn public_key(&self) -> Result; + + /// Returns the set of properties associated with the certificate. + /// + /// The `keys` argument can optionally be used to filter the properties loaded to an explicit + /// subset. + fn properties(&self, keys: Option<&[CertificateOid]>) + -> Result; + + /// Returns the SHA-256 fingerprint of the certificate. + fn fingerprint(&self) -> Result<[u8; 32], CFError> { unimplemented!() } +} + +impl SecCertificateExt for SecCertificate { + fn common_name(&self) -> Result { + unsafe { + let mut string = ptr::null(); + cvt(SecCertificateCopyCommonName( + self.as_concrete_TypeRef(), + &mut string, + ))?; + Ok(CFString::wrap_under_create_rule(string).to_string()) + } + } + + #[cfg(feature = "OSX_10_14")] + fn public_key(&self) -> Result { + unsafe { + let key = SecCertificateCopyKey(self.as_concrete_TypeRef()); + if key.is_null() { + return Err(Error::from_code(-26275)); + } + Ok(SecKey::wrap_under_create_rule(key)) + } + } + + #[cfg(not(feature = "OSX_10_14"))] + fn public_key(&self) -> Result { + #[allow(deprecated)] + unsafe { + let mut key = ptr::null_mut(); + cvt(SecCertificateCopyPublicKey( + self.as_concrete_TypeRef(), + &mut key, + ))?; + Ok(SecKey::wrap_under_create_rule(key)) + } + } + + fn properties( + &self, + keys: Option<&[CertificateOid]>, + ) -> Result { + unsafe { + let keys = keys.map(|oids| { + let oids = oids.iter().map(|oid| oid.to_str()).collect::>(); + CFArray::from_CFTypes(&oids) + }); + + let keys = match keys { + Some(ref keys) => keys.as_concrete_TypeRef(), + None => ptr::null_mut(), + }; + + let mut error = ptr::null_mut(); + + let dictionary = SecCertificateCopyValues(self.as_concrete_TypeRef(), keys, &mut error); + + if error.is_null() { + Ok(CertificateProperties(CFDictionary::wrap_under_create_rule( + dictionary, + ))) + } else { + Err(CFError::wrap_under_create_rule(error)) + } + } + } + + /// Returns the SHA-256 fingerprint of the certificate. + fn fingerprint(&self) -> Result<[u8; 32], CFError> { + let data = CFData::from_buffer(&self.to_der()); + let hash = Builder::new() + .type_(DigestType::sha2()) + .length(256) + .execute(&data)?; + Ok(hash.bytes().try_into().unwrap()) + } +} + +/// Properties associated with a certificate. +pub struct CertificateProperties(CFDictionary); + +impl CertificateProperties { + /// Retrieves a specific property identified by its OID. + #[must_use] pub fn get(&self, oid: CertificateOid) -> Option { + unsafe { + self.0.find(oid.as_ptr().cast::()).map(|value| { + CertificateProperty(CFDictionary::wrap_under_get_rule(*value as *mut _)) + }) + } + } +} + +/// A property associated with a certificate. +pub struct CertificateProperty(CFDictionary); + +impl CertificateProperty { + /// Returns the label of this property. + #[must_use] + pub fn label(&self) -> CFString { + unsafe { + CFString::wrap_under_get_rule((*self.0.get(kSecPropertyKeyLabel.to_void())).cast()) + } + } + + /// Returns an enum of the underlying data for this property. + #[must_use] + pub fn get(&self) -> PropertyType { + unsafe { + let type_ = + CFString::wrap_under_get_rule(*self.0.get(kSecPropertyKeyType.to_void()) as *mut _); + let value = self.0.get(kSecPropertyKeyValue.to_void()); + + if type_ == CFString::wrap_under_get_rule(kSecPropertyTypeSection) { + PropertyType::Section(PropertySection(CFArray::wrap_under_get_rule( + (*value).cast(), + ))) + } else if type_ == CFString::wrap_under_get_rule(kSecPropertyTypeString) { + PropertyType::String(CFString::wrap_under_get_rule((*value).cast())) + } else { + PropertyType::__Unknown + } + } + } +} + +/// A "section" property. +/// +/// Sections are sequences of other properties. +pub struct PropertySection(CFArray); + +impl PropertySection { + /// Returns an iterator over the properties in this section. + #[inline(always)] + #[must_use] + pub fn iter(&self) -> PropertySectionIter<'_> { + PropertySectionIter(self.0.iter()) + } +} + +impl<'a> IntoIterator for &'a PropertySection { + type IntoIter = PropertySectionIter<'a>; + type Item = CertificateProperty; + + #[inline(always)] + fn into_iter(self) -> PropertySectionIter<'a> { + self.iter() + } +} + +/// An iterator over the properties in a section. +pub struct PropertySectionIter<'a>(CFArrayIterator<'a, CFDictionary>); + +impl<'a> Iterator for PropertySectionIter<'a> { + type Item = CertificateProperty; + + #[inline] + fn next(&mut self) -> Option { + self.0.next().map(|t| CertificateProperty(t.clone())) + } + + #[inline(always)] + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } +} + +/// An enum of the various types of properties. +pub enum PropertyType { + /// A section. + Section(PropertySection), + /// A string. + String(CFString), + #[doc(hidden)] + __Unknown, +} + +#[cfg(test)] +mod test { + use super::*; + use crate::os::macos::certificate_oids::CertificateOid; + use crate::test::certificate; + use std::collections::HashMap; + + #[test] + fn common_name() { + let certificate = certificate(); + assert_eq!("foobar.com", p!(certificate.common_name())); + } + + #[test] + #[allow(deprecated)] + fn public_key() { + let certificate = certificate(); + p!(certificate.public_key()); + } + + #[test] + fn fingerprint() { + let certificate = certificate(); + let fingerprint = p!(certificate.fingerprint()); + assert_eq!( + "af9dd180a326ae08b37e6398f9262f8b9d4c55674a233a7c84975024f873655d", + hex::encode(fingerprint) + ); + } + + #[test] + fn signature_algorithm() { + let certificate = certificate(); + let properties = certificate + .properties(Some(&[CertificateOid::x509_v1_signature_algorithm()])) + .unwrap(); + let value = properties + .get(CertificateOid::x509_v1_signature_algorithm()) + .unwrap(); + let section = match value.get() { + PropertyType::Section(section) => section, + _ => panic!(), + }; + let properties = section + .iter() + .map(|p| (p.label().to_string(), p.get())) + .collect::>(); + let algorithm = match properties["Algorithm"] { + PropertyType::String(ref s) => s.to_string(), + _ => panic!(), + }; + assert_eq!(algorithm, "1.2.840.113549.1.1.5"); + } +} diff --git a/vendor/security-framework/src/os/macos/certificate_oids.rs b/vendor/security-framework/src/os/macos/certificate_oids.rs new file mode 100644 index 000000000..d820afe4a --- /dev/null +++ b/vendor/security-framework/src/os/macos/certificate_oids.rs @@ -0,0 +1,32 @@ +//! OIDs associated with certificate properties. +use core_foundation::base::TCFType; +use core_foundation::string::CFString; +use core_foundation_sys::string::CFStringRef; +use security_framework_sys::certificate_oids::kSecOIDX509V1SignatureAlgorithm; + +/// An identifier of a property of a certificate. +#[derive(Copy, Clone)] +pub struct CertificateOid(CFStringRef); + +#[allow(missing_docs)] +impl CertificateOid { + #[inline(always)] + #[must_use] + pub fn x509_v1_signature_algorithm() -> Self { + unsafe { Self(kSecOIDX509V1SignatureAlgorithm) } + } + + /// Returns the underlying raw pointer corresponding to this OID. + #[inline(always)] + #[must_use] + pub fn as_ptr(&self) -> CFStringRef { + self.0 + } + + /// Returns the string representation of the OID. + #[inline] + #[must_use] + pub fn to_str(&self) -> CFString { + unsafe { CFString::wrap_under_get_rule(self.0) } + } +} diff --git a/vendor/security-framework/src/os/macos/code_signing.rs b/vendor/security-framework/src/os/macos/code_signing.rs new file mode 100644 index 000000000..0a3a9c9ae --- /dev/null +++ b/vendor/security-framework/src/os/macos/code_signing.rs @@ -0,0 +1,485 @@ +//! Code signing services. + +use std::{fmt::Debug, mem::MaybeUninit, str::FromStr}; + +use core_foundation::{ + base::{TCFType, TCFTypeRef, ToVoid}, + data::CFDataRef, + dictionary::CFMutableDictionary, + number::CFNumber, + string::{CFString, CFStringRef}, + url::CFURL, +}; +use libc::pid_t; +use security_framework_sys::code_signing::{ + kSecCSBasicValidateOnly, kSecCSCheckAllArchitectures, kSecCSCheckGatekeeperArchitectures, + kSecCSCheckNestedCode, kSecCSCheckTrustedAnchors, kSecCSConsiderExpiration, + kSecCSDoNotValidateExecutable, kSecCSDoNotValidateResources, kSecCSEnforceRevocationChecks, + kSecCSFullReport, kSecCSNoNetworkAccess, kSecCSQuickCheck, kSecCSReportProgress, + kSecCSRestrictSidebandData, kSecCSRestrictSymlinks, kSecCSRestrictToAppLike, + kSecCSSingleThreaded, kSecCSStrictValidate, kSecCSUseSoftwareSigningCert, kSecCSValidatePEH, + kSecGuestAttributeAudit, kSecGuestAttributePid, SecCodeCheckValidity, + SecCodeCopyGuestWithAttributes, SecCodeCopyPath, SecCodeCopySelf, SecCodeGetTypeID, SecCodeRef, + SecRequirementCreateWithString, SecRequirementGetTypeID, SecRequirementRef, + SecStaticCodeCheckValidity, SecStaticCodeCreateWithPath, SecStaticCodeGetTypeID, + SecStaticCodeRef, +}; + +use crate::{cvt, Result}; + +bitflags::bitflags! { + + /// Values that can be used in the flags parameter to most code signing + /// functions. + pub struct Flags: u32 { + /// Use the default behaviour. + const NONE = 0; + + /// For multi-architecture (universal) Mach-O programs, validate all + /// architectures included. + const CHECK_ALL_ARCHITECTURES = kSecCSCheckAllArchitectures; + + /// Do not validate the contents of the main executable. + const DO_NOT_VALIDATE_EXECUTABLE = kSecCSDoNotValidateExecutable; + + /// Do not validate the presence and contents of all bundle resources + /// if any. + const DO_NOT_VALIDATE_RESOURCES = kSecCSDoNotValidateResources; + + /// Do not validate either the main executable or the bundle resources, + /// if any. + const BASIC_VALIDATE_ONLY = kSecCSBasicValidateOnly; + + /// For code in bundle form, locate and recursively check embedded code. + const CHECK_NESTED_CODE = kSecCSCheckNestedCode; + + /// Perform additional checks to ensure the validity of code in bundle + /// form. + const STRICT_VALIDATE = kSecCSStrictValidate; + + /// Apple have not documented this flag. + const FULL_REPORT = kSecCSFullReport; + + /// Apple have not documented this flag. + const CHECK_GATEKEEPER_ARCHITECTURES = kSecCSCheckGatekeeperArchitectures; + + /// Apple have not documented this flag. + const RESTRICT_SYMLINKS = kSecCSRestrictSymlinks; + + /// Apple have not documented this flag. + const RESTRICT_TO_APP_LIKE = kSecCSRestrictToAppLike; + + /// Apple have not documented this flag. + const RESTRICT_SIDEBAND_DATA = kSecCSRestrictSidebandData; + + /// Apple have not documented this flag. + const USE_SOFTWARE_SIGNING_CERT = kSecCSUseSoftwareSigningCert; + + /// Apple have not documented this flag. + const VALIDATE_PEH = kSecCSValidatePEH; + + /// Apple have not documented this flag. + const SINGLE_THREADED = kSecCSSingleThreaded; + + /// Apple have not documented this flag. + const QUICK_CHECK = kSecCSQuickCheck; + + /// Apple have not documented this flag. + const CHECK_TRUSTED_ANCHORS = kSecCSCheckTrustedAnchors; + + /// Apple have not documented this flag. + const REPORT_PROGRESS = kSecCSReportProgress; + + /// Apple have not documented this flag. + const NO_NETWORK_ACCESS = kSecCSNoNetworkAccess; + + /// Apple have not documented this flag. + const ENFORCE_REVOCATION_CHECKS = kSecCSEnforceRevocationChecks; + + /// Apple have not documented this flag. + const CONSIDER_EXPIRATION = kSecCSConsiderExpiration; + } +} + +impl Default for Flags { + #[inline(always)] + fn default() -> Self { + Self::NONE + } +} + +/// A helper to create guest attributes, which are normally passed as a +/// `CFDictionary` with varying types. +pub struct GuestAttributes { + inner: CFMutableDictionary, +} + +impl GuestAttributes { + // Not implemented: + // - architecture + // - canonical + // - dynamic code + // - dynamic code info plist + // - hash + // - mach port + // - sub-architecture + + /// Creates a new, empty `GuestAttributes`. You must add values to it in + /// order for it to be of any use. + #[must_use] + pub fn new() -> Self { + Self { + inner: CFMutableDictionary::new(), + } + } + + /// The guest's audit token. + pub fn set_audit_token(&mut self, token: CFDataRef) { + let key = unsafe { CFString::wrap_under_get_rule(kSecGuestAttributeAudit) }; + self.inner.add(&key.as_CFTypeRef(), &token.to_void()); + } + + /// The guest's pid. + pub fn set_pid(&mut self, pid: pid_t) { + let key = unsafe { CFString::wrap_under_get_rule(kSecGuestAttributePid) }; + let pid = CFNumber::from(pid); + self.inner.add(&key.as_CFTypeRef(), &pid.as_CFTypeRef()); + } + + /// Support for arbirtary guest attributes. + pub fn set_other>(&mut self, key: CFStringRef, value: V) { + self.inner.add(&key.as_void_ptr(), &value.to_void()); + } +} + +impl Default for GuestAttributes { + fn default() -> Self { + Self::new() + } +} + +declare_TCFType! { + /// A code object representing signed code running on the system. + SecRequirement, SecRequirementRef +} +impl_TCFType!(SecRequirement, SecRequirementRef, SecRequirementGetTypeID); + +impl FromStr for SecRequirement { + type Err = crate::base::Error; + + fn from_str(s: &str) -> Result { + let text = CFString::new(s); + let mut requirement = MaybeUninit::uninit(); + + unsafe { + cvt(SecRequirementCreateWithString( + text.as_concrete_TypeRef(), + 0, + requirement.as_mut_ptr(), + ))?; + + Ok(Self::wrap_under_create_rule(requirement.assume_init())) + } + } +} + +declare_TCFType! { + /// A code object representing signed code running on the system. + SecCode, SecCodeRef +} +impl_TCFType!(SecCode, SecCodeRef, SecCodeGetTypeID); + +impl Debug for SecCode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("SecCode") + } +} + +impl SecCode { + /// Retrieves the code object for the code making the call. + pub fn for_self(flags: Flags) -> Result { + let mut code = MaybeUninit::uninit(); + + unsafe { + cvt(SecCodeCopySelf(flags.bits(), code.as_mut_ptr()))?; + Ok(Self::wrap_under_create_rule(code.assume_init())) + } + } + + /// Performs dynamic validation of signed code. + pub fn check_validity(&self, flags: Flags, requirement: &SecRequirement) -> Result<()> { + unsafe { + cvt(SecCodeCheckValidity( + self.as_concrete_TypeRef(), + flags.bits(), + requirement.as_concrete_TypeRef(), + )) + } + } + + /// Asks a code host to identify one of its guests given + /// the type and value of specific attributes of the guest code. + /// + /// If `host` is `None` then the code signing root of trust (currently, the + // system kernel) should be used as the code host. + pub fn copy_guest_with_attribues( + host: Option<&SecCode>, + attrs: &GuestAttributes, + flags: Flags, + ) -> Result { + let mut code = MaybeUninit::uninit(); + + let host = match host { + Some(host) => host.as_concrete_TypeRef(), + None => std::ptr::null_mut(), + }; + + unsafe { + cvt(SecCodeCopyGuestWithAttributes( + host, + attrs.inner.as_concrete_TypeRef(), + flags.bits(), + code.as_mut_ptr(), + ))?; + + Ok(SecCode::wrap_under_create_rule(code.assume_init())) + } + } + + /// Retrieves the location on disk of signed code, given a code or static + /// code object. + pub fn path(&self, flags: Flags) -> Result { + let mut url = MaybeUninit::uninit(); + + // The docs say we can pass a SecCodeRef instead of a SecStaticCodeRef. + unsafe { + cvt(SecCodeCopyPath( + self.as_CFTypeRef() as _, + flags.bits(), + url.as_mut_ptr(), + ))?; + + Ok(CFURL::wrap_under_create_rule(url.assume_init())) + } + } +} + +declare_TCFType! { + /// A static code object representing signed code on disk. + SecStaticCode, SecStaticCodeRef +} +impl_TCFType!(SecStaticCode, SecStaticCodeRef, SecStaticCodeGetTypeID); + +impl SecStaticCode { + /// Creates a static code object representing the code at a specified file + /// system path. + pub fn from_path(path: &CFURL, flags: Flags) -> Result { + let mut code = MaybeUninit::uninit(); + + unsafe { + cvt(SecStaticCodeCreateWithPath( + path.as_concrete_TypeRef(), + flags.bits(), + code.as_mut_ptr(), + ))?; + + Ok(Self::wrap_under_get_rule(code.assume_init())) + } + } + + /// Retrieves the location on disk of signed code, given a code or static + /// code object. + pub fn path(&self, flags: Flags) -> Result { + let mut url = MaybeUninit::uninit(); + + // The docs say we can pass a SecCodeRef instead of a SecStaticCodeRef. + unsafe { + cvt(SecCodeCopyPath( + self.as_concrete_TypeRef(), + flags.bits(), + url.as_mut_ptr(), + ))?; + + Ok(CFURL::wrap_under_create_rule(url.assume_init())) + } + } + + /// Performs dynamic validation of signed code. + pub fn check_validity(&self, flags: Flags, requirement: &SecRequirement) -> Result<()> { + unsafe { + cvt(SecStaticCodeCheckValidity( + self.as_concrete_TypeRef(), + flags.bits(), + requirement.as_concrete_TypeRef(), + )) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use core_foundation::data::CFData; + use libc::{c_uint, c_void, KERN_SUCCESS}; + + #[test] + fn path_to_static_code_and_back() { + let path = CFURL::from_path("/bin/bash", false).unwrap(); + let code = SecStaticCode::from_path(&path, Flags::NONE).unwrap(); + assert_eq!(code.path(Flags::NONE).unwrap(), path); + } + + #[test] + fn self_to_path() { + let path = CFURL::from_path(std::env::current_exe().unwrap(), false).unwrap(); + let code = SecCode::for_self(Flags::NONE).unwrap(); + assert_eq!(code.path(Flags::NONE).unwrap(), path); + } + + #[test] + fn bash_is_signed_by_apple() { + let path = CFURL::from_path("/bin/bash", false).unwrap(); + let code = SecStaticCode::from_path(&path, Flags::NONE).unwrap(); + let requirement: SecRequirement = "anchor apple".parse().unwrap(); + code.check_validity(Flags::NONE, &requirement).unwrap(); + } + + #[cfg(target_arch = "aarch64")] + #[test] + fn self_is_not_signed_by_apple() { + let code = SecCode::for_self(Flags::NONE).unwrap(); + let requirement: SecRequirement = "anchor apple".parse().unwrap(); + + assert_eq!( + code.check_validity(Flags::NONE, &requirement) + .unwrap_err() + .code(), + // "code failed to satisfy specified code requirement(s)" + -67050 + ); + } + + #[cfg(not(target_arch = "aarch64"))] + #[test] + fn self_is_not_signed_by_apple() { + let code = SecCode::for_self(Flags::NONE).unwrap(); + let requirement: SecRequirement = "anchor apple".parse().unwrap(); + + assert_eq!( + code.check_validity(Flags::NONE, &requirement) + .unwrap_err() + .code(), + // "code object is not signed at all" + -67062 + ); + } + + #[test] + fn copy_kernel_guest_with_launchd_pid() { + let mut attrs = GuestAttributes::new(); + attrs.set_pid(1); + + assert_eq!( + SecCode::copy_guest_with_attribues(None, &attrs, Flags::NONE) + .unwrap() + .path(Flags::NONE) + .unwrap() + .get_string() + .to_string(), + "file:///sbin/launchd" + ); + } + + #[test] + fn copy_current_guest_with_launchd_pid() { + let host_code = SecCode::for_self(Flags::NONE).unwrap(); + + let mut attrs = GuestAttributes::new(); + attrs.set_pid(1); + + assert_eq!( + SecCode::copy_guest_with_attribues(Some(&host_code), &attrs, Flags::NONE) + .unwrap_err() + .code(), + // "host has no guest with the requested attributes" + -67065 + ); + } + + #[test] + fn copy_kernel_guest_with_unmatched_pid() { + let mut attrs = GuestAttributes::new(); + attrs.set_pid(999_999_999); + + assert_eq!( + SecCode::copy_guest_with_attribues(None, &attrs, Flags::NONE) + .unwrap_err() + .code(), + // "UNIX[No such process]" + 100003 + ); + } + + #[test] + fn copy_kernel_guest_with_current_token() { + let mut token: [u8; 32] = [0; 32]; + let mut token_len = 32u32; + + enum OpaqueTaskName {} + + extern "C" { + fn mach_task_self() -> *const OpaqueTaskName; + fn task_info( + task_name: *const OpaqueTaskName, + task_flavor: u32, + out: *mut c_void, + out_len: *mut u32, + ) -> i32; + } + + const TASK_AUDIT_TOKEN: c_uint = 15; + + let result = unsafe { + task_info( + mach_task_self(), + TASK_AUDIT_TOKEN, + token.as_mut_ptr() as *mut c_void, + &mut token_len, + ) + }; + + assert_eq!(result, KERN_SUCCESS); + + let token_data = CFData::from_buffer(&token); + + let mut attrs = GuestAttributes::new(); + attrs.set_audit_token(token_data.as_concrete_TypeRef()); + + assert_eq!( + SecCode::copy_guest_with_attribues(None, &attrs, Flags::NONE) + .unwrap() + .path(Flags::NONE) + .unwrap() + .to_path() + .unwrap(), + std::env::current_exe().unwrap() + ); + } + + #[test] + fn copy_kernel_guest_with_unmatched_token() { + let token: [u8; 32] = [0; 32]; + let token_data = CFData::from_buffer(&token); + + let mut attrs = GuestAttributes::new(); + attrs.set_audit_token(token_data.as_concrete_TypeRef()); + + assert_eq!( + SecCode::copy_guest_with_attribues(None, &attrs, Flags::NONE) + .unwrap_err() + .code(), + // "UNIX[No such process]" + 100003 + ); + } +} diff --git a/vendor/security-framework/src/os/macos/digest_transform.rs b/vendor/security-framework/src/os/macos/digest_transform.rs new file mode 100644 index 000000000..7577f083e --- /dev/null +++ b/vendor/security-framework/src/os/macos/digest_transform.rs @@ -0,0 +1,196 @@ +//! Digest Transform support + +use core_foundation::base::{CFIndex, TCFType}; +use core_foundation::data::CFData; +use core_foundation::error::CFError; +use core_foundation::string::CFString; +use core_foundation_sys::base::CFTypeRef; +use core_foundation_sys::data::CFDataRef; +use core_foundation_sys::string::CFStringRef; +use security_framework_sys::digest_transform::*; +use security_framework_sys::transform::*; +use std::ptr; + +use crate::os::macos::transform::SecTransform; + +#[derive(Debug, Copy, Clone)] +/// A type of digest. +pub struct DigestType(CFStringRef); + +#[allow(missing_docs)] +impl DigestType { + #[inline(always)] + #[must_use] + pub fn hmac_md5() -> Self { + unsafe { Self(kSecDigestHMACMD5) } + } + + #[inline(always)] + #[must_use] + pub fn hmac_sha1() -> Self { + unsafe { Self(kSecDigestHMACSHA1) } + } + + #[inline(always)] + #[must_use] + pub fn hmac_sha2() -> Self { + unsafe { Self(kSecDigestHMACSHA2) } + } + + #[inline(always)] + #[must_use] + pub fn md2() -> Self { + unsafe { Self(kSecDigestMD2) } + } + + #[inline(always)] + #[must_use] + pub fn md4() -> Self { + unsafe { Self(kSecDigestMD4) } + } + + #[inline(always)] + #[must_use] + pub fn md5() -> Self { + unsafe { Self(kSecDigestMD5) } + } + + #[inline(always)] + #[must_use] + pub fn sha1() -> Self { + unsafe { Self(kSecDigestSHA1) } + } + + #[inline(always)] + #[must_use] + pub fn sha2() -> Self { + unsafe { Self(kSecDigestSHA2) } + } + + #[inline(always)] + fn to_type(self) -> CFTypeRef { + self.0 as CFTypeRef + } +} + +/// A builder for digest transform operations. +pub struct Builder { + digest_type: Option, + digest_length: Option, + hmac_key: Option, +} + +impl Default for Builder { + #[inline(always)] + fn default() -> Self { + Self::new() + } +} + +impl Builder { + /// Returns a new builder with default settings. + #[inline(always)] + #[must_use] + pub fn new() -> Self { + Self { + digest_type: None, + digest_length: None, + hmac_key: None, + } + } + + /// Sets the type of digest to perform. + /// + /// If not set, an appropriate digest will be selected for you. + #[inline] + pub fn type_(&mut self, digest_type: DigestType) -> &mut Self { + self.digest_type = Some(digest_type); + self + } + + /// Sets the output length of the digest. + /// + /// If not set, an appropriate length will be selected for you. Some digest + /// types only support specific output lengths. + #[inline] + pub fn length(&mut self, digest_length: CFIndex) -> &mut Self { + self.digest_length = Some(digest_length); + self + } + + /// Sets the key used for HMAC digests. + /// + /// Only applies to `HmacMd5`, `HmacSha1`, and `HmacSha2` digests. + #[inline] + pub fn hmac_key(&mut self, hmac_key: CFData) -> &mut Self { + self.hmac_key = Some(hmac_key); + self + } + + /// Computes the digest of the data. + pub fn execute(&self, data: &CFData) -> Result { + unsafe { + let digest_type = match self.digest_type { + Some(ref digest_type) => digest_type.to_type(), + None => ptr::null(), + }; + + let digest_length = self.digest_length.unwrap_or(0); + + let mut error = ptr::null_mut(); + let transform = SecDigestTransformCreate(digest_type, digest_length, &mut error); + if transform.is_null() { + return Err(CFError::wrap_under_create_rule(error)); + } + let mut transform = SecTransform::wrap_under_create_rule(transform); + + if let Some(ref hmac_key) = self.hmac_key { + let key = CFString::wrap_under_get_rule(kSecDigestHMACKeyAttribute); + transform.set_attribute(&key, hmac_key)?; + } + + let key = CFString::wrap_under_get_rule(kSecTransformInputAttributeName); + transform.set_attribute(&key, data)?; + + let result = transform.execute()?; + Ok(CFData::wrap_under_get_rule( + result.as_CFTypeRef() as CFDataRef + )) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use core_foundation::data::CFData; + use hex; + + #[test] + fn md5() { + let data = CFData::from_buffer("The quick brown fox jumps over the lazy dog".as_bytes()); + let hash = Builder::new() + .type_(DigestType::md5()) + .execute(&data) + .unwrap(); + assert_eq!( + hex::encode(hash.bytes()), + "9e107d9d372bb6826bd81d3542a419d6" + ); + } + + #[test] + fn hmac_sha1() { + let data = CFData::from_buffer("The quick brown fox jumps over the lazy dog".as_bytes()); + let key = CFData::from_buffer(b"key"); + let hash = Builder::new() + .type_(DigestType::hmac_sha1()) + .hmac_key(key) + .execute(&data) + .unwrap(); + assert_eq!( + hex::encode(hash.bytes()), + "de7c9b85b8b78aa6bc8a7a36f70a90701c9db4d9" + ); + } +} diff --git a/vendor/security-framework/src/os/macos/encrypt_transform.rs b/vendor/security-framework/src/os/macos/encrypt_transform.rs new file mode 100644 index 000000000..f0ab3c4a2 --- /dev/null +++ b/vendor/security-framework/src/os/macos/encrypt_transform.rs @@ -0,0 +1,256 @@ +//! Encryption and Decryption transform support. + +use core_foundation::base::TCFType; +use core_foundation::data::CFData; +use core_foundation::error::CFError; +use core_foundation::string::CFString; +use core_foundation_sys::data::CFDataRef; +use core_foundation_sys::string::CFStringRef; +use security_framework_sys::encrypt_transform::*; +use security_framework_sys::transform::*; +use std::ptr; + +use crate::key::SecKey; +use crate::os::macos::transform::SecTransform; + +#[derive(Debug, Copy, Clone)] +/// The padding scheme to use for encryption. +pub struct Padding(CFStringRef); + +impl Padding { + /// Do not pad. + #[inline(always)] + #[must_use] + pub fn none() -> Self { + unsafe { Self(kSecPaddingNoneKey) } + } + + /// Use PKCS#1 padding. + #[inline(always)] + #[must_use] + pub fn pkcs1() -> Self { + unsafe { Self(kSecPaddingPKCS1Key) } + } + + /// Use PKCS#5 padding. + #[inline(always)] + #[must_use] + pub fn pkcs5() -> Self { + unsafe { Self(kSecPaddingPKCS5Key) } + } + + /// Use PKCS#7 padding. + #[inline(always)] + #[must_use] + pub fn pkcs7() -> Self { + unsafe { Self(kSecPaddingPKCS7Key) } + } + + /// Use OAEP padding. + #[inline(always)] + #[must_use] + pub fn oaep() -> Self { + unsafe { Self(kSecPaddingOAEPKey) } + } + + #[inline] + fn to_str(self) -> CFString { + unsafe { CFString::wrap_under_get_rule(self.0) } + } +} + +/// The cipher mode to use. +/// +/// Only applies to AES encryption. +#[derive(Debug, Copy, Clone)] +pub struct Mode(CFStringRef); + +#[allow(missing_docs)] +impl Mode { + #[inline(always)] + #[must_use] + pub fn none() -> Self { + unsafe { Self(kSecModeNoneKey) } + } + + #[inline(always)] + #[must_use] + pub fn ecb() -> Self { + unsafe { Self(kSecModeECBKey) } + } + + #[inline(always)] + #[must_use] + pub fn cbc() -> Self { + unsafe { Self(kSecModeCBCKey) } + } + + #[inline(always)] + #[must_use] + pub fn cfb() -> Self { + unsafe { Self(kSecModeCFBKey) } + } + + #[inline(always)] + #[must_use] + pub fn ofb() -> Self { + unsafe { Self(kSecModeOFBKey) } + } + + fn to_str(self) -> CFString { + unsafe { CFString::wrap_under_get_rule(self.0) } + } +} + +/// A builder for encryption and decryption transform operations. +#[derive(Default)] +pub struct Builder { + padding: Option, + mode: Option, + iv: Option, +} + +impl Builder { + /// Creates a new `Builder` with a default configuration. + #[inline(always)] + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Selects the padding scheme to use. + /// + /// If not set, an appropriate scheme will be selected for you. + #[inline(always)] + pub fn padding(&mut self, padding: Padding) -> &mut Self { + self.padding = Some(padding); + self + } + + /// Selects the encryption mode to use. + /// + /// If not set, an appropriate mode will be selected for you. + #[inline(always)] + pub fn mode(&mut self, mode: Mode) -> &mut Self { + self.mode = Some(mode); + self + } + + /// Sets the initialization vector to use. + /// + /// If not set, an appropriate value will be supplied for you. + #[inline(always)] + pub fn iv(&mut self, iv: CFData) -> &mut Self { + self.iv = Some(iv); + self + } + + /// Encrypts data with a provided key. + pub fn encrypt(&self, key: &SecKey, data: &CFData) -> Result { + unsafe { + let mut error = ptr::null_mut(); + let transform = SecEncryptTransformCreate(key.as_concrete_TypeRef(), &mut error); + if transform.is_null() { + return Err(CFError::wrap_under_create_rule(error)); + } + let transform = SecTransform::wrap_under_create_rule(transform); + + self.finish(transform, data) + } + } + + /// Decrypts data with a provided key. + pub fn decrypt(&self, key: &SecKey, data: &CFData) -> Result { + unsafe { + let mut error = ptr::null_mut(); + let transform = SecDecryptTransformCreate(key.as_concrete_TypeRef(), &mut error); + if transform.is_null() { + return Err(CFError::wrap_under_create_rule(error)); + } + let transform = SecTransform::wrap_under_create_rule(transform); + + self.finish(transform, data) + } + } + + fn finish(&self, mut transform: SecTransform, data: &CFData) -> Result { + unsafe { + if let Some(ref padding) = self.padding { + let key = CFString::wrap_under_get_rule(kSecPaddingKey); + transform.set_attribute(&key, &padding.to_str())?; + } + + if let Some(ref mode) = self.mode { + let key = CFString::wrap_under_get_rule(kSecEncryptionMode); + transform.set_attribute(&key, &mode.to_str())?; + } + + if let Some(ref iv) = self.iv { + let key = CFString::wrap_under_get_rule(kSecIVKey); + transform.set_attribute(&key, iv)?; + } + + let key = CFString::wrap_under_get_rule(kSecTransformInputAttributeName); + transform.set_attribute(&key, data)?; + + let result = transform.execute()?; + Ok(CFData::wrap_under_get_rule( + result.as_CFTypeRef() as CFDataRef + )) + } + } +} + +#[cfg(test)] +mod test { + use core_foundation::data::CFData; + use hex::FromHex; + + use super::*; + use crate::key::SecKey; + use crate::os::macos::item::KeyType; + use crate::os::macos::key::SecKeyExt; + + #[test] + fn cbc_mmt_256() { + // test 9 + let key = "87725bd43a45608814180773f0e7ab95a3c859d83a2130e884190e44d14c6996"; + let iv = "e49651988ebbb72eb8bb80bb9abbca34"; + let ciphertext = "5b97a9d423f4b97413f388d9a341e727bb339f8e18a3fac2f2fb85abdc8f135deb30054a\ + 1afdc9b6ed7da16c55eba6b0d4d10c74e1d9a7cf8edfaeaa684ac0bd9f9d24ba674955c7\ + 9dc6be32aee1c260b558ff07e3a4d49d24162011ff254db8be078e8ad07e648e6bf56793\ + 76cb4321a5ef01afe6ad8816fcc7634669c8c4389295c9241e45fff39f3225f7745032da\ + eebe99d4b19bcb215d1bfdb36eda2c24"; + let plaintext = "bfe5c6354b7a3ff3e192e05775b9b75807de12e38a626b8bf0e12d5fff78e4f1775aa7d79\ + 2d885162e66d88930f9c3b2cdf8654f56972504803190386270f0aa43645db187af41fcea\ + 639b1f8026ccdd0c23e0de37094a8b941ecb7602998a4b2604e69fc04219585d854600e0a\ + d6f99a53b2504043c08b1c3e214d17cde053cbdf91daa999ed5b47c37983ba3ee254bc5c7\ + 93837daaa8c85cfc12f7f54f699f"; + + let key = Vec::::from_hex(key).unwrap(); + let key = CFData::from_buffer(&key); + let key = SecKey::from_data(KeyType::aes(), &key).unwrap(); + + let iv = Vec::::from_hex(iv).unwrap(); + + let ciphertext = Vec::::from_hex(ciphertext).unwrap(); + + let plaintext = Vec::::from_hex(plaintext).unwrap(); + + let decrypted = Builder::new() + .padding(Padding::none()) + .iv(CFData::from_buffer(&iv)) + .decrypt(&key, &CFData::from_buffer(&ciphertext)) + .unwrap(); + + assert_eq!(plaintext, decrypted.bytes()); + + let encrypted = Builder::new() + .padding(Padding::none()) + .iv(CFData::from_buffer(&iv)) + .encrypt(&key, &CFData::from_buffer(&plaintext)) + .unwrap(); + + assert_eq!(ciphertext, encrypted.bytes()); + } +} diff --git a/vendor/security-framework/src/os/macos/identity.rs b/vendor/security-framework/src/os/macos/identity.rs new file mode 100644 index 000000000..d54cbe00d --- /dev/null +++ b/vendor/security-framework/src/os/macos/identity.rs @@ -0,0 +1,86 @@ +//! OSX specific extensions to identity functionality. +use core_foundation::array::CFArray; +use core_foundation::base::TCFType; +use security_framework_sys::identity::SecIdentityCreateWithCertificate; +use std::ptr; + +use crate::base::Result; +use crate::certificate::SecCertificate; +use crate::cvt; +use crate::identity::SecIdentity; +use crate::os::macos::keychain::SecKeychain; + +/// An extension trait adding OSX specific functionality to `SecIdentity`. +pub trait SecIdentityExt { + /// Creates an identity corresponding to a certificate, looking in the + /// provided keychains for the corresponding private key. + /// + /// To search the default keychains, use an empty slice for `keychains`. + /// + /// + fn with_certificate( + keychains: &[SecKeychain], + certificate: &SecCertificate, + ) -> Result; +} + +impl SecIdentityExt for SecIdentity { + fn with_certificate(keychains: &[SecKeychain], certificate: &SecCertificate) -> Result { + let keychains = CFArray::from_CFTypes(keychains); + unsafe { + let mut identity = ptr::null_mut(); + cvt(SecIdentityCreateWithCertificate( + if keychains.len() > 0 {keychains.as_CFTypeRef()} else {ptr::null()}, + certificate.as_concrete_TypeRef(), + &mut identity, + ))?; + Ok(Self::wrap_under_create_rule(identity)) + } + } +} + +#[cfg(test)] +mod test { + use tempfile::tempdir; + + use super::*; + use crate::identity::SecIdentity; + use crate::os::macos::certificate::SecCertificateExt; + use crate::os::macos::import_export::ImportOptions; + use crate::os::macos::keychain::CreateOptions; + use crate::os::macos::test::identity; + use crate::test; + + #[test] + fn certificate() { + let dir = p!(tempdir()); + let identity = identity(dir.path()); + let certificate = p!(identity.certificate()); + assert_eq!("foobar.com", p!(certificate.common_name())); + } + + #[test] + fn private_key() { + let dir = p!(tempdir()); + let identity = identity(dir.path()); + p!(identity.private_key()); + } + + #[test] + fn with_certificate() { + let dir = p!(tempdir()); + + let mut keychain = p!(CreateOptions::new() + .password("foobar") + .create(dir.path().join("test.keychain"))); + + let key = include_bytes!("../../../test/server.key"); + p!(ImportOptions::new() + .filename("server.key") + .keychain(&mut keychain) + .import(key)); + + let cert = test::certificate(); + p!(SecIdentity::with_certificate(&[keychain], &cert)); + } +} diff --git a/vendor/security-framework/src/os/macos/import_export.rs b/vendor/security-framework/src/os/macos/import_export.rs new file mode 100644 index 000000000..05a4b4907 --- /dev/null +++ b/vendor/security-framework/src/os/macos/import_export.rs @@ -0,0 +1,345 @@ +//! OSX specific extensions to import/export functionality. + +use core_foundation::array::CFArray; +use core_foundation::base::{CFType, TCFType}; +use core_foundation::data::CFData; +use core_foundation::string::CFString; +use security_framework_sys::base::errSecSuccess; +use security_framework_sys::import_export::*; +use std::ptr; +use std::str::FromStr; + +use crate::base::{Error, Result}; +use crate::certificate::SecCertificate; +use crate::identity::SecIdentity; +use crate::import_export::Pkcs12ImportOptions; +use crate::key::SecKey; +use crate::os::macos::access::SecAccess; +use crate::os::macos::keychain::SecKeychain; + +/// An extension trait adding OSX specific functionality to `Pkcs12ImportOptions`. +pub trait Pkcs12ImportOptionsExt { + /// Specifies the keychain in which to import the identity. + /// + /// If this is not called, the default keychain will be used. + fn keychain(&mut self, keychain: SecKeychain) -> &mut Self; + + /// Specifies the access control to be associated with the identity. + fn access(&mut self, access: SecAccess) -> &mut Self; +} + +impl Pkcs12ImportOptionsExt for Pkcs12ImportOptions { + #[inline(always)] + fn keychain(&mut self, keychain: SecKeychain) -> &mut Self { + crate::Pkcs12ImportOptionsInternals::keychain(self, keychain) + } + + #[inline(always)] + fn access(&mut self, access: SecAccess) -> &mut Self { + crate::Pkcs12ImportOptionsInternals::access(self, access) + } +} + +/// A builder type to import Security Framework types from serialized formats. +#[derive(Default)] +pub struct ImportOptions<'a> { + filename: Option, + passphrase: Option, + secure_passphrase: bool, + no_access_control: bool, + alert_title: Option, + alert_prompt: Option, + items: Option<&'a mut SecItems>, + keychain: Option, +} + +impl<'a> ImportOptions<'a> { + /// Creates a new builder with default options. + #[inline(always)] + #[must_use] + pub fn new() -> ImportOptions<'a> { + ImportOptions::default() + } + + /// Sets the filename from which the imported data came. + /// + /// The extension of the file will used as a hint for parsing. + #[inline] + pub fn filename(&mut self, filename: &str) -> &mut ImportOptions<'a> { + self.filename = Some(CFString::from_str(filename).unwrap()); + self + } + + /// Sets the passphrase to be used to decrypt the imported data. + #[inline] + pub fn passphrase(&mut self, passphrase: &str) -> &mut ImportOptions<'a> { + self.passphrase = Some(CFString::from_str(passphrase).unwrap().into_CFType()); + self + } + + /// Sets the passphrase to be used to decrypt the imported data. + #[inline] + pub fn passphrase_bytes(&mut self, passphrase: &[u8]) -> &mut ImportOptions<'a> { + self.passphrase = Some(CFData::from_buffer(passphrase).into_CFType()); + self + } + + /// If set, the user will be prompted to imput the passphrase used to + /// decrypt the imported data. + #[inline(always)] + pub fn secure_passphrase(&mut self, secure_passphrase: bool) -> &mut ImportOptions<'a> { + self.secure_passphrase = secure_passphrase; + self + } + + /// If set, imported items will have no access controls imposed on them. + #[inline(always)] + pub fn no_access_control(&mut self, no_access_control: bool) -> &mut ImportOptions<'a> { + self.no_access_control = no_access_control; + self + } + + /// Sets the title of the alert popup used with the `secure_passphrase` + /// option. + #[inline] + pub fn alert_title(&mut self, alert_title: &str) -> &mut ImportOptions<'a> { + self.alert_title = Some(CFString::from_str(alert_title).unwrap()); + self + } + + /// Sets the prompt of the alert popup used with the `secure_passphrase` + /// option. + #[inline] + pub fn alert_prompt(&mut self, alert_prompt: &str) -> &mut ImportOptions<'a> { + self.alert_prompt = Some(CFString::from_str(alert_prompt).unwrap()); + self + } + + /// Sets the object into which imported items will be placed. + #[inline(always)] + pub fn items(&mut self, items: &'a mut SecItems) -> &mut ImportOptions<'a> { + self.items = Some(items); + self + } + + /// Sets the keychain into which items will be imported. + /// + /// This must be specified to import `SecIdentity`s. + #[inline] + pub fn keychain(&mut self, keychain: &SecKeychain) -> &mut ImportOptions<'a> { + self.keychain = Some(keychain.clone()); + self + } + + /// Imports items from serialized data. + pub fn import(&mut self, data: &[u8]) -> Result<()> { + let data = CFData::from_buffer(data); + let data = data.as_concrete_TypeRef(); + + let filename = match self.filename { + Some(ref filename) => filename.as_concrete_TypeRef(), + None => ptr::null(), + }; + + let mut key_params = SecItemImportExportKeyParameters { + version: SEC_KEY_IMPORT_EXPORT_PARAMS_VERSION, + flags: 0, + passphrase: ptr::null(), + alertTitle: ptr::null(), + alertPrompt: ptr::null(), + accessRef: ptr::null_mut(), + keyUsage: ptr::null_mut(), + keyAttributes: ptr::null(), + }; + + if let Some(ref passphrase) = self.passphrase { + key_params.passphrase = passphrase.as_CFTypeRef(); + } + + if self.secure_passphrase { + key_params.flags |= kSecKeySecurePassphrase; + } + + if self.no_access_control { + key_params.flags |= kSecKeyNoAccessControl; + } + + if let Some(ref alert_title) = self.alert_title { + key_params.alertTitle = alert_title.as_concrete_TypeRef(); + } + + if let Some(ref alert_prompt) = self.alert_prompt { + key_params.alertPrompt = alert_prompt.as_concrete_TypeRef(); + } + + let keychain = match self.keychain { + Some(ref keychain) => keychain.as_concrete_TypeRef(), + None => ptr::null_mut(), + }; + + let mut raw_items = ptr::null(); + let items_ref = match self.items { + Some(_) => std::ptr::addr_of_mut!(raw_items), + None => ptr::null_mut(), + }; + + unsafe { + let ret = SecItemImport( + data, + filename, + ptr::null_mut(), + ptr::null_mut(), + 0, + &key_params, + keychain, + items_ref, + ); + if ret != errSecSuccess { + return Err(Error::from_code(ret)); + } + + if let Some(ref mut items) = self.items { + let raw_items = CFArray::::wrap_under_create_rule(raw_items); + for item in raw_items.iter() { + let type_id = item.type_of(); + if type_id == SecCertificate::type_id() { + items.certificates.push(SecCertificate::wrap_under_get_rule( + item.as_CFTypeRef() as *mut _, + )); + } else if type_id == SecIdentity::type_id() { + items.identities.push(SecIdentity::wrap_under_get_rule( + item.as_CFTypeRef() as *mut _, + )); + } else if type_id == SecKey::type_id() { + items + .keys + .push(SecKey::wrap_under_get_rule(item.as_CFTypeRef() as *mut _)); + } else { + panic!("Got bad type from SecItemImport: {}", type_id); + } + } + } + } + + Ok(()) + } +} + +/// A type which holds items imported from serialized data. +/// +/// Pass a reference to `ImportOptions::items`. +#[derive(Default)] +pub struct SecItems { + /// Imported certificates. + pub certificates: Vec, + /// Imported identities. + pub identities: Vec, + /// Imported keys. + pub keys: Vec, +} + +#[cfg(test)] +mod test { + use super::*; + use crate::import_export::*; + use crate::os::macos::keychain; + use hex; + use tempfile::tempdir; + + #[test] + fn certificate() { + let data = include_bytes!("../../../test/server.der"); + let mut items = SecItems::default(); + ImportOptions::new() + .filename("server.der") + .items(&mut items) + .import(data) + .unwrap(); + assert_eq!(1, items.certificates.len()); + assert_eq!(0, items.identities.len()); + assert_eq!(0, items.keys.len()); + } + + #[test] + fn key() { + let data = include_bytes!("../../../test/server.key"); + let mut items = SecItems::default(); + ImportOptions::new() + .filename("server.key") + .items(&mut items) + .import(data) + .unwrap(); + assert_eq!(0, items.certificates.len()); + assert_eq!(0, items.identities.len()); + assert_eq!(1, items.keys.len()); + } + + #[test] + fn identity() { + let dir = tempdir().unwrap(); + let keychain = keychain::CreateOptions::new() + .password("password") + .create(dir.path().join("identity.keychain")) + .unwrap(); + + let data = include_bytes!("../../../test/server.p12"); + let mut items = SecItems::default(); + ImportOptions::new() + .filename("server.p12") + .passphrase("password123") + .items(&mut items) + .keychain(&keychain) + .import(data) + .unwrap(); + assert_eq!(1, items.identities.len()); + assert_eq!(0, items.certificates.len()); + assert_eq!(0, items.keys.len()); + } + + #[test] + #[ignore] // since it requires manual intervention + fn secure_passphrase_identity() { + let dir = tempdir().unwrap(); + let keychain = keychain::CreateOptions::new() + .password("password") + .create(dir.path().join("identity.keychain")) + .unwrap(); + + let data = include_bytes!("../../../test/server.p12"); + let mut items = SecItems::default(); + ImportOptions::new() + .filename("server.p12") + .secure_passphrase(true) + .alert_title("alert title") + .alert_prompt("alert prompt") + .items(&mut items) + .keychain(&keychain) + .import(data) + .unwrap(); + assert_eq!(1, items.identities.len()); + assert_eq!(0, items.certificates.len()); + assert_eq!(0, items.keys.len()); + } + + #[test] + fn pkcs12_import() { + use super::Pkcs12ImportOptionsExt; + + let dir = tempdir().unwrap(); + let keychain = keychain::CreateOptions::new() + .password("password") + .create(dir.path().join("pkcs12_import")) + .unwrap(); + + let data = include_bytes!("../../../test/server.p12"); + let identities = p!(Pkcs12ImportOptions::new() + .passphrase("password123") + .keychain(keychain) + .import(data)); + assert_eq!(1, identities.len()); + assert_eq!( + hex::encode(identities[0].key_id.as_ref().unwrap()), + "ed6492936dcc8907e397e573b36e633458dc33f1" + ); + } +} diff --git a/vendor/security-framework/src/os/macos/item.rs b/vendor/security-framework/src/os/macos/item.rs new file mode 100644 index 000000000..18a4d2e8f --- /dev/null +++ b/vendor/security-framework/src/os/macos/item.rs @@ -0,0 +1,47 @@ +//! OSX specific functionality for items. +use crate::item::ItemSearchOptions; +use crate::os::macos::keychain::SecKeychain; +use crate::ItemSearchOptionsInternals; + +// Moved to crate::Key +pub use crate::key::KeyType; + +/// An extension trait adding OSX specific functionality to `ItemSearchOptions`. +pub trait ItemSearchOptionsExt { + /// Search within the specified keychains. + /// + /// If this is not called, the default keychain will be searched. + fn keychains(&mut self, keychains: &[SecKeychain]) -> &mut Self; +} + +impl ItemSearchOptionsExt for ItemSearchOptions { + #[inline(always)] + fn keychains(&mut self, keychains: &[SecKeychain]) -> &mut Self { + ItemSearchOptionsInternals::keychains(self, keychains) + } +} + +#[cfg(test)] +mod test { + use crate::item::*; + use crate::os::macos::certificate::SecCertificateExt; + use crate::os::macos::item::ItemSearchOptionsExt; + use crate::os::macos::test::keychain; + use tempfile::tempdir; + + #[test] + fn find_certificate() { + let dir = p!(tempdir()); + let keychain = keychain(dir.path()); + let results = p!(ItemSearchOptions::new() + .keychains(&[keychain]) + .class(ItemClass::certificate()) + .search()); + assert_eq!(1, results.len()); + let certificate = match results[0] { + SearchResult::Ref(Reference::Certificate(ref cert)) => cert, + _ => panic!("expected certificate"), + }; + assert_eq!("foobar.com", p!(certificate.common_name())); + } +} diff --git a/vendor/security-framework/src/os/macos/key.rs b/vendor/security-framework/src/os/macos/key.rs new file mode 100644 index 000000000..f6a20e93c --- /dev/null +++ b/vendor/security-framework/src/os/macos/key.rs @@ -0,0 +1,38 @@ +//! OSX specific functionality for keys. +use core_foundation::base::TCFType; +use core_foundation::data::CFData; +use core_foundation::dictionary::CFDictionary; +use core_foundation::error::CFError; +use core_foundation::string::CFString; +use security_framework_sys::item::kSecAttrKeyType; +use security_framework_sys::key::SecKeyCreateFromData; +use std::ptr; + +use crate::key::{KeyType, SecKey}; + +/// An extension trait adding OSX specific functionality to `SecKey`. +pub trait SecKeyExt { + /// Creates a new `SecKey` from a buffer containing key data. + fn from_data(key_type: KeyType, key_data: &CFData) -> Result; +} + +impl SecKeyExt for SecKey { + fn from_data(key_type: KeyType, key_data: &CFData) -> Result { + unsafe { + let key = CFString::wrap_under_get_rule(kSecAttrKeyType); + let dict = CFDictionary::from_CFType_pairs(&[(key, key_type.to_str())]); + + let mut err = ptr::null_mut(); + let key = SecKeyCreateFromData( + dict.as_concrete_TypeRef(), + key_data.as_concrete_TypeRef(), + &mut err, + ); + if key.is_null() { + Err(CFError::wrap_under_create_rule(err)) + } else { + Ok(Self::wrap_under_create_rule(key)) + } + } + } +} diff --git a/vendor/security-framework/src/os/macos/keychain.rs b/vendor/security-framework/src/os/macos/keychain.rs new file mode 100644 index 000000000..68e1c0cb4 --- /dev/null +++ b/vendor/security-framework/src/os/macos/keychain.rs @@ -0,0 +1,280 @@ +//! Keychain support. + +use core_foundation::base::{Boolean, TCFType}; +use security_framework_sys::base::{errSecSuccess, SecKeychainRef}; +use security_framework_sys::keychain::*; +use std::ffi::CString; +use std::os::raw::c_void; +use std::os::unix::ffi::OsStrExt; +use std::path::Path; +use std::ptr; + +use crate::base::{Error, Result}; +use crate::cvt; +use crate::os::macos::access::SecAccess; + +pub use security_framework_sys::keychain::SecPreferencesDomain; + +declare_TCFType! { + /// A type representing a keychain. + SecKeychain, SecKeychainRef +} +impl_TCFType!(SecKeychain, SecKeychainRef, SecKeychainGetTypeID); + +unsafe impl Sync for SecKeychain {} +unsafe impl Send for SecKeychain {} + +impl SecKeychain { + /// Creates a `SecKeychain` object corresponding to the user's default + /// keychain. + #[inline] + pub fn default() -> Result { + unsafe { + let mut keychain = ptr::null_mut(); + cvt(SecKeychainCopyDefault(&mut keychain))?; + Ok(Self::wrap_under_create_rule(keychain)) + } + } + + /// Creates a `SecKeychain` object corresponding to the user's default + /// keychain for the given domain. + pub fn default_for_domain(domain: SecPreferencesDomain) -> Result { + unsafe { + let mut keychain = ptr::null_mut(); + cvt(SecKeychainCopyDomainDefault(domain, &mut keychain))?; + Ok(Self::wrap_under_create_rule(keychain)) + } + } + + /// Opens a keychain from a file. + pub fn open>(path: P) -> Result { + let path_name = [ + path.as_ref().as_os_str().as_bytes(), + std::slice::from_ref(&0) + ].concat(); + + unsafe { + let mut keychain = ptr::null_mut(); + cvt(SecKeychainOpen(path_name.as_ptr().cast(), &mut keychain))?; + Ok(Self::wrap_under_create_rule(keychain)) + } + } + + /// Unlocks the keychain. + /// + /// If a password is not specified, the user will be prompted to enter it. + pub fn unlock(&mut self, password: Option<&str>) -> Result<()> { + let (len, ptr, use_password) = match password { + Some(password) => (password.len(), password.as_ptr().cast(), true), + None => (0, ptr::null(), false), + }; + + unsafe { + cvt(SecKeychainUnlock( + self.as_concrete_TypeRef(), + len as u32, + ptr, + use_password as Boolean, + )) + } + } + + /// Sets settings of the keychain. + #[inline] + pub fn set_settings(&mut self, settings: &KeychainSettings) -> Result<()> { + unsafe { + cvt(SecKeychainSetSettings( + self.as_concrete_TypeRef(), + &settings.0, + )) + } + } + + #[cfg(target_os = "macos")] + /// Disables the user interface for keychain services functions that + /// automatically display a user interface. + pub fn disable_user_interaction() -> Result { + let code = unsafe { SecKeychainSetUserInteractionAllowed(0u8) }; + + if code != errSecSuccess { + Err(Error::from_code(code)) + } else { + Ok(KeychainUserInteractionLock) + } + } + + #[cfg(target_os = "macos")] + /// Indicates whether keychain services functions that normally display a + /// user interaction are allowed to do so. + pub fn user_interaction_allowed() -> Result { + let mut state: Boolean = 0; + let code = unsafe { SecKeychainGetUserInteractionAllowed(&mut state) }; + + if code != errSecSuccess { + Err(Error::from_code(code)) + } else { + Ok(state != 0) + } + } +} + +/// A builder type to create new keychains. +#[derive(Default)] +pub struct CreateOptions { + password: Option, + prompt_user: bool, + access: Option, +} + +impl CreateOptions { + /// Creates a new builder with default options. + #[inline(always)] + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Sets the password to be used to protect the keychain. + #[inline] + pub fn password(&mut self, password: &str) -> &mut Self { + self.password = Some(password.into()); + self + } + + /// If set, the user will be prompted to provide a password used to + /// protect the keychain. + #[inline(always)] + pub fn prompt_user(&mut self, prompt_user: bool) -> &mut Self { + self.prompt_user = prompt_user; + self + } + + /// Sets the access control applied to the keychain. + #[inline(always)] + pub fn access(&mut self, access: SecAccess) -> &mut Self { + self.access = Some(access); + self + } + + /// Creates a new keychain at the specified location on the filesystem. + pub fn create>(&self, path: P) -> Result { + unsafe { + let path_name = path.as_ref().as_os_str().as_bytes(); + // FIXME + let path_name = CString::new(path_name).unwrap(); + + let (password, password_len) = match self.password { + Some(ref password) => (password.as_ptr().cast::(), password.len() as u32), + None => (ptr::null(), 0), + }; + + let access = match self.access { + Some(ref access) => access.as_concrete_TypeRef(), + None => ptr::null_mut(), + }; + + let mut keychain = ptr::null_mut(); + cvt(SecKeychainCreate( + path_name.as_ptr(), + password_len, + password, + self.prompt_user as Boolean, + access, + &mut keychain, + ))?; + + Ok(SecKeychain::wrap_under_create_rule(keychain)) + } + } +} + +/// Settings associated with a `SecKeychain`. +pub struct KeychainSettings(SecKeychainSettings); + +impl KeychainSettings { + /// Creates a new `KeychainSettings` with default settings. + #[inline] + #[must_use] + pub fn new() -> Self { + Self(SecKeychainSettings { + version: SEC_KEYCHAIN_SETTINGS_VERS1, + lockOnSleep: 0, + useLockInterval: 0, + lockInterval: i32::max_value() as u32, + }) + } + + /// If set, the keychain will automatically lock when the computer sleeps. + /// + /// Defaults to `false`. + #[inline(always)] + pub fn set_lock_on_sleep(&mut self, lock_on_sleep: bool) { + self.0.lockOnSleep = lock_on_sleep as Boolean; + } + + /// Sets the interval of time in seconds after which the keychain is + /// automatically locked. + /// + /// Defaults to `None`. + pub fn set_lock_interval(&mut self, lock_interval: Option) { + match lock_interval { + Some(lock_interval) => { + self.0.useLockInterval = 1; + self.0.lockInterval = lock_interval; + } + None => { + self.0.useLockInterval = 0; + self.0.lockInterval = i32::max_value() as u32; + } + } + } +} + +impl Default for KeychainSettings { + #[inline(always)] + fn default() -> Self { + Self::new() + } +} + +#[cfg(target_os = "macos")] +#[must_use = "The user interaction is disabled for the lifetime of the returned object"] +/// Automatically re-enables user interaction. +pub struct KeychainUserInteractionLock; + +#[cfg(target_os = "macos")] +impl Drop for KeychainUserInteractionLock { + #[inline(always)] + fn drop(&mut self) { + unsafe { SecKeychainSetUserInteractionAllowed(1u8) }; + } +} + +#[cfg(test)] +mod test { + use tempfile::tempdir; + + use super::*; + + #[test] + fn create_options() { + let dir = tempdir().unwrap(); + + let mut keychain = CreateOptions::new() + .password("foobar") + .create(dir.path().join("test.keychain")) + .unwrap(); + + keychain.set_settings(&KeychainSettings::new()).unwrap(); + } + + #[test] + fn disable_user_interaction() { + assert!(SecKeychain::user_interaction_allowed().unwrap()); + { + let _lock = SecKeychain::disable_user_interaction().unwrap(); + assert!(!SecKeychain::user_interaction_allowed().unwrap()); + } + assert!(SecKeychain::user_interaction_allowed().unwrap()); + } +} diff --git a/vendor/security-framework/src/os/macos/keychain_item.rs b/vendor/security-framework/src/os/macos/keychain_item.rs new file mode 100644 index 000000000..fd7b452a2 --- /dev/null +++ b/vendor/security-framework/src/os/macos/keychain_item.rs @@ -0,0 +1,26 @@ +//! Keychain item support. + +use core_foundation::base::TCFType; +use security_framework_sys::base::SecKeychainItemRef; +use security_framework_sys::keychain_item::SecKeychainItemGetTypeID; +use std::fmt; + +declare_TCFType! { + /// A type representing a keychain item. + SecKeychainItem, SecKeychainItemRef +} +impl_TCFType!( + SecKeychainItem, + SecKeychainItemRef, + SecKeychainItemGetTypeID +); + +unsafe impl Sync for SecKeychainItem {} +unsafe impl Send for SecKeychainItem {} + +impl fmt::Debug for SecKeychainItem { + #[cold] + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.debug_struct("SecKeychainItem").finish_non_exhaustive() + } +} diff --git a/vendor/security-framework/src/os/macos/mod.rs b/vendor/security-framework/src/os/macos/mod.rs new file mode 100644 index 000000000..5fe7d0d2c --- /dev/null +++ b/vendor/security-framework/src/os/macos/mod.rs @@ -0,0 +1,52 @@ +//! OSX specific extensions. + +pub mod access; +pub mod certificate; +pub mod certificate_oids; +pub mod code_signing; +pub mod digest_transform; +pub mod encrypt_transform; +pub mod identity; +pub mod import_export; +pub mod item; +pub mod key; +pub mod keychain; +pub mod keychain_item; +pub mod passwords; +pub mod secure_transport; +pub mod transform; + +#[cfg(test)] +pub mod test { + use crate::identity::SecIdentity; + use crate::item::{ItemClass, ItemSearchOptions, Reference, SearchResult}; + use crate::os::macos::item::ItemSearchOptionsExt; + use crate::os::macos::keychain::SecKeychain; + use std::fs::File; + use std::io::prelude::*; + use std::path::Path; + + pub fn identity(dir: &Path) -> SecIdentity { + // FIXME https://github.com/rust-lang/rust/issues/30018 + let keychain = keychain(dir); + let mut items = p!(ItemSearchOptions::new() + .class(ItemClass::identity()) + .keychains(&[keychain]) + .search()); + match items.pop().unwrap() { + SearchResult::Ref(Reference::Identity(identity)) => identity, + _ => panic!("expected identity"), + } + } + + pub fn keychain(dir: &Path) -> SecKeychain { + let path = dir.join("server.keychain"); + let mut file = p!(File::create(&path)); + p!(file.write_all(include_bytes!("../../../test/server.keychain"))); + drop(file); + + let mut keychain = p!(SecKeychain::open(&path)); + p!(keychain.unlock(Some("password123"))); + keychain + } +} diff --git a/vendor/security-framework/src/os/macos/passwords.rs b/vendor/security-framework/src/os/macos/passwords.rs new file mode 100644 index 000000000..94e1d6fa5 --- /dev/null +++ b/vendor/security-framework/src/os/macos/passwords.rs @@ -0,0 +1,525 @@ +//! Password support. + +use crate::os::macos::keychain::SecKeychain; +use crate::os::macos::keychain_item::SecKeychainItem; +use core_foundation::array::CFArray; +use core_foundation::base::TCFType; +pub use security_framework_sys::keychain::{SecAuthenticationType, SecProtocolType}; +use security_framework_sys::keychain::{ + SecKeychainAddGenericPassword, SecKeychainAddInternetPassword, SecKeychainFindGenericPassword, + SecKeychainFindInternetPassword, +}; +use security_framework_sys::keychain_item::{ + SecKeychainItemDelete, SecKeychainItemFreeContent, SecKeychainItemModifyAttributesAndData, +}; +use std::fmt; +use std::fmt::Write; +use std::ops::Deref; +use std::ptr; +use std::slice; + +use crate::base::Result; +use crate::cvt; + +/// Password slice. Use `.as_ref()` to get `&[u8]` or `.to_owned()` to get `Vec` +pub struct SecKeychainItemPassword { + data: *const u8, + data_len: usize, +} + +impl fmt::Debug for SecKeychainItemPassword { + #[cold] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for _ in 0..self.data_len { + f.write_char('•')?; + } + Ok(()) + } +} + +impl AsRef<[u8]> for SecKeychainItemPassword { + #[inline] + fn as_ref(&self) -> &[u8] { + unsafe { slice::from_raw_parts(self.data, self.data_len) } + } +} + +impl Deref for SecKeychainItemPassword { + type Target = [u8]; + #[inline(always)] + fn deref(&self) -> &Self::Target { + self.as_ref() + } +} + +impl Drop for SecKeychainItemPassword { + #[inline] + fn drop(&mut self) { + unsafe { + SecKeychainItemFreeContent(ptr::null_mut(), self.data as *mut _); + } + } +} + +impl SecKeychainItem { + /// Modify keychain item in-place, replacing its password with the given one + pub fn set_password(&mut self, password: &[u8]) -> Result<()> { + unsafe { + cvt(SecKeychainItemModifyAttributesAndData( + self.as_CFTypeRef() as *mut _, + ptr::null(), + password.len() as u32, + password.as_ptr().cast(), + ))?; + } + Ok(()) + } + + /// Delete this item from its keychain + #[inline] + pub fn delete(self) { + unsafe { + SecKeychainItemDelete(self.as_CFTypeRef() as *mut _); + } + } +} + +/// Find a generic password. +/// +/// The underlying system supports passwords with 0 values, so this +/// returns a vector of bytes rather than a string. +/// +/// * `keychains` is an array of keychains to search or None to search +/// the default keychain. +/// * `service` is the name of the service to search for. +/// * `account` is the name of the account to search for. +pub fn find_generic_password( + keychains: Option<&[SecKeychain]>, + service: &str, + account: &str, +) -> Result<(SecKeychainItemPassword, SecKeychainItem)> { + let keychains_or_none = keychains.map(CFArray::from_CFTypes); + + let keychains_or_null = match keychains_or_none { + None => ptr::null(), + Some(ref keychains) => keychains.as_CFTypeRef(), + }; + + let mut data_len = 0; + let mut data = ptr::null_mut(); + let mut item = ptr::null_mut(); + + unsafe { + cvt(SecKeychainFindGenericPassword( + keychains_or_null, + service.len() as u32, + service.as_ptr().cast(), + account.len() as u32, + account.as_ptr().cast(), + &mut data_len, + &mut data, + &mut item, + ))?; + Ok(( + SecKeychainItemPassword { + data: data as *const _, + data_len: data_len as usize, + }, + SecKeychainItem::wrap_under_create_rule(item), + )) + } +} + +/// * `keychains` is an array of keychains to search or None to search +/// the default keychain. +/// * `server`: server name. +/// * `security_domain`: security domain. This parameter is optional. +/// * `account`: account name. +/// * `path`: the path. +/// * `port`: The TCP/IP port number. +/// * `protocol`: The protocol associated with this password. +/// * `authentication_type`: The authentication scheme used. +#[allow(clippy::too_many_arguments)] +pub fn find_internet_password( + keychains: Option<&[SecKeychain]>, + server: &str, + security_domain: Option<&str>, + account: &str, + path: &str, + port: Option, + protocol: SecProtocolType, + authentication_type: SecAuthenticationType, +) -> Result<(SecKeychainItemPassword, SecKeychainItem)> { + let keychains_or_none = keychains.map(CFArray::from_CFTypes); + + let keychains_or_null = match keychains_or_none { + None => ptr::null(), + Some(ref keychains) => keychains.as_CFTypeRef(), + }; + + let mut data_len = 0; + let mut data = ptr::null_mut(); + let mut item = ptr::null_mut(); + + unsafe { + cvt(SecKeychainFindInternetPassword( + keychains_or_null, + server.len() as u32, + server.as_ptr().cast(), + security_domain.map_or(0, |s| s.len() as u32), + security_domain + .map_or(ptr::null(), |s| s.as_ptr().cast()), + account.len() as u32, + account.as_ptr().cast(), + path.len() as u32, + path.as_ptr().cast(), + port.unwrap_or(0), + protocol, + authentication_type, + &mut data_len, + &mut data, + &mut item, + ))?; + Ok(( + SecKeychainItemPassword { + data: data as *const _, + data_len: data_len as usize, + }, + SecKeychainItem::wrap_under_create_rule(item), + )) + } +} + +impl SecKeychain { + /// Find application password in this keychain + #[inline] + pub fn find_generic_password( + &self, + service: &str, + account: &str, + ) -> Result<(SecKeychainItemPassword, SecKeychainItem)> { + find_generic_password(Some(&[self.clone()]), service, account) + } + + /// Find internet password in this keychain + #[inline] + #[allow(clippy::too_many_arguments)] + pub fn find_internet_password( + &self, + server: &str, + security_domain: Option<&str>, + account: &str, + path: &str, + port: Option, + protocol: SecProtocolType, + authentication_type: SecAuthenticationType, + ) -> Result<(SecKeychainItemPassword, SecKeychainItem)> { + find_internet_password( + Some(&[self.clone()]), + server, + security_domain, + account, + path, + port, + protocol, + authentication_type, + ) + } + + /// Update existing or add new internet password + #[allow(clippy::too_many_arguments)] + pub fn set_internet_password( + &self, + server: &str, + security_domain: Option<&str>, + account: &str, + path: &str, + port: Option, + protocol: SecProtocolType, + authentication_type: SecAuthenticationType, + password: &[u8], + ) -> Result<()> { + match self.find_internet_password( + server, + security_domain, + account, + path, + port, + protocol, + authentication_type, + ) { + Ok((_, mut item)) => item.set_password(password), + _ => self.add_internet_password( + server, + security_domain, + account, + path, + port, + protocol, + authentication_type, + password, + ), + } + } + + /// Set a generic password. + /// + /// * `keychain_opt` is the keychain to use or None to use the default + /// keychain. + /// * `service` is the associated service name for the password. + /// * `account` is the associated account name for the password. + /// * `password` is the password itself. + pub fn set_generic_password( + &self, + service: &str, + account: &str, + password: &[u8], + ) -> Result<()> { + match self.find_generic_password(service, account) { + Ok((_, mut item)) => item.set_password(password), + _ => self.add_generic_password(service, account, password), + } + } + + /// Add application password to the keychain, without checking if it exists already + /// + /// See `set_generic_password()` + #[inline] + pub fn add_generic_password( + &self, + service: &str, + account: &str, + password: &[u8], + ) -> Result<()> { + unsafe { + cvt(SecKeychainAddGenericPassword( + self.as_CFTypeRef() as *mut _, + service.len() as u32, + service.as_ptr().cast(), + account.len() as u32, + account.as_ptr().cast(), + password.len() as u32, + password.as_ptr().cast(), + ptr::null_mut(), + ))?; + } + Ok(()) + } + + /// Add internet password to the keychain, without checking if it exists already + /// + /// See `set_internet_password()` + #[inline] + #[allow(clippy::too_many_arguments)] + pub fn add_internet_password( + &self, + server: &str, + security_domain: Option<&str>, + account: &str, + path: &str, + port: Option, + protocol: SecProtocolType, + authentication_type: SecAuthenticationType, + password: &[u8], + ) -> Result<()> { + unsafe { + cvt(SecKeychainAddInternetPassword( + self.as_CFTypeRef() as *mut _, + server.len() as u32, + server.as_ptr().cast(), + security_domain.map_or(0, |s| s.len() as u32), + security_domain + .map_or(ptr::null(), |s| s.as_ptr().cast()), + account.len() as u32, + account.as_ptr().cast(), + path.len() as u32, + path.as_ptr().cast(), + port.unwrap_or(0), + protocol, + authentication_type, + password.len() as u32, + password.as_ptr().cast(), + ptr::null_mut(), + ))?; + } + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::os::macos::keychain::{CreateOptions, SecKeychain}; + use tempfile::tempdir; + use tempfile::TempDir; + + fn temp_keychain_setup(name: &str) -> (TempDir, SecKeychain) { + let dir = tempdir().expect("TempDir::new"); + let keychain = CreateOptions::new() + .password("foobar") + .create(dir.path().join(name.to_string() + ".keychain")) + .expect("create keychain"); + + (dir, keychain) + } + + fn temp_keychain_teardown(dir: TempDir) { + dir.close().expect("temp dir close"); + } + + #[test] + fn missing_password_temp() { + let (dir, keychain) = temp_keychain_setup("missing_password"); + let keychains = vec![keychain]; + + let service = "temp_this_service_does_not_exist"; + let account = "this_account_is_bogus"; + let found = find_generic_password(Some(&keychains), service, account); + + assert!(found.is_err()); + + temp_keychain_teardown(dir); + } + + #[test] + #[cfg(feature = "default_keychain_tests")] + fn missing_password_default() { + let service = "default_this_service_does_not_exist"; + let account = "this_account_is_bogus"; + let found = find_generic_password(None, service, account); + + assert!(found.is_err()); + } + + #[test] + fn round_trip_password_temp() { + let (dir, keychain) = temp_keychain_setup("round_trip_password"); + + let service = "test_round_trip_password_temp"; + let account = "temp_this_is_the_test_account"; + let password = String::from("deadbeef").into_bytes(); + + keychain + .set_generic_password(service, account, &password) + .expect("set_generic_password"); + let (found, item) = keychain + .find_generic_password(service, account) + .expect("find_generic_password"); + assert_eq!(found.to_owned(), password); + + item.delete(); + + temp_keychain_teardown(dir); + } + + #[test] + #[cfg(feature = "default_keychain_tests")] + fn round_trip_password_default() { + let service = "test_round_trip_password_default"; + let account = "this_is_the_test_account"; + let password = String::from("deadbeef").into_bytes(); + + SecKeychain::default() + .expect("default keychain") + .set_generic_password(service, account, &password) + .expect("set_generic_password"); + let (found, item) = + find_generic_password(None, service, account).expect("find_generic_password"); + assert_eq!(&*found, &password[..]); + + item.delete(); + } + + #[test] + fn change_password_temp() { + let (dir, keychain) = temp_keychain_setup("change_password"); + let keychains = vec![keychain]; + + let service = "test_change_password_temp"; + let account = "this_is_the_test_account"; + let pw1 = String::from("password1").into_bytes(); + let pw2 = String::from("password2").into_bytes(); + + keychains[0] + .set_generic_password(service, account, &pw1) + .expect("set_generic_password1"); + let (found, _) = find_generic_password(Some(&keychains), service, account) + .expect("find_generic_password1"); + assert_eq!(found.as_ref(), &pw1[..]); + + keychains[0] + .set_generic_password(service, account, &pw2) + .expect("set_generic_password2"); + let (found, item) = find_generic_password(Some(&keychains), service, account) + .expect("find_generic_password2"); + assert_eq!(&*found, &pw2[..]); + + item.delete(); + + temp_keychain_teardown(dir); + } + + #[test] + #[cfg(feature = "default_keychain_tests")] + fn change_password_default() { + let service = "test_change_password_default"; + let account = "this_is_the_test_account"; + let pw1 = String::from("password1").into_bytes(); + let pw2 = String::from("password2").into_bytes(); + + SecKeychain::default() + .expect("default keychain") + .set_generic_password(service, account, &pw1) + .expect("set_generic_password1"); + let (found, _) = + find_generic_password(None, service, account).expect("find_generic_password1"); + assert_eq!(found.to_owned(), pw1); + + SecKeychain::default() + .expect("default keychain") + .set_generic_password(service, account, &pw2) + .expect("set_generic_password2"); + let (found, item) = + find_generic_password(None, service, account).expect("find_generic_password2"); + assert_eq!(found.to_owned(), pw2); + + item.delete(); + } + + #[test] + fn cross_keychain_corruption_temp() { + let (dir1, keychain1) = temp_keychain_setup("cross_corrupt1"); + let (dir2, keychain2) = temp_keychain_setup("cross_corrupt2"); + let keychains1 = vec![keychain1.clone()]; + let keychains2 = vec![keychain2.clone()]; + let both_keychains = vec![keychain1, keychain2]; + + let service = "temp_this_service_does_not_exist"; + let account = "this_account_is_bogus"; + let password = String::from("deadbeef").into_bytes(); + + // Make sure this password doesn't exist in either keychain. + let found = find_generic_password(Some(&both_keychains), service, account); + assert!(found.is_err()); + + // Set a password in one keychain. + keychains1[0] + .set_generic_password(service, account, &password) + .expect("set_generic_password"); + + // Make sure it's found in that keychain. + let (found, item) = find_generic_password(Some(&keychains1), service, account) + .expect("find_generic_password1"); + assert_eq!(found.to_owned(), password); + + // Make sure it's _not_ found in the other keychain. + let found = find_generic_password(Some(&keychains2), service, account); + assert!(found.is_err()); + + // Cleanup. + item.delete(); + + temp_keychain_teardown(dir1); + temp_keychain_teardown(dir2); + } +} diff --git a/vendor/security-framework/src/os/macos/secure_transport.rs b/vendor/security-framework/src/os/macos/secure_transport.rs new file mode 100644 index 000000000..d98790b89 --- /dev/null +++ b/vendor/security-framework/src/os/macos/secure_transport.rs @@ -0,0 +1,647 @@ +//! OSX specific extensions to Secure Transport functionality. + +use core_foundation::array::CFArray; +use core_foundation::base::TCFType; +use security_framework_sys::secure_transport::*; +use std::ptr; +use std::slice; + +use crate::base::Result; +use crate::certificate::SecCertificate; +use crate::secure_transport::{MidHandshakeSslStream, SslContext}; +use crate::{cvt, AsInner}; + +/// An extension trait adding OSX specific functionality to the `SslContext` +/// type. +pub trait SslContextExt { + /// Returns the DER encoded data specifying the parameters used for + /// Diffie-Hellman key exchange. + fn diffie_hellman_params(&self) -> Result>; + + /// Sets the parameters used for Diffie-Hellman key exchange, in the + /// DER format used by OpenSSL. + /// + /// If a cipher suite which uses Diffie-Hellman key exchange is selected, + /// parameters will automatically be generated if none are provided with + /// this method, but this process can take up to 30 seconds. + /// + /// This can only be called on server-side sessions. + fn set_diffie_hellman_params(&mut self, dh_params: &[u8]) -> Result<()>; + + /// Returns the certificate authorities used to validate client + /// certificates. + fn certificate_authorities(&self) -> Result>>; + + /// Sets the certificate authorities used to validate client certificates, + /// replacing any that are already present. + fn set_certificate_authorities(&mut self, certs: &[SecCertificate]) -> Result<()>; + + /// Adds certificate authorities used to validate client certificates. + fn add_certificate_authorities(&mut self, certs: &[SecCertificate]) -> Result<()>; + + /// If enabled, server identity changes are allowed during renegotiation. + /// + /// It is disabled by default to protect against triple handshake attacks. + /// + /// Requires the `OSX_10_11` (or greater) feature. + #[cfg(feature = "OSX_10_11")] + fn allow_server_identity_change(&self) -> Result; + + /// If enabled, server identity changes are allowed during renegotiation. + /// + /// It is disabled by default to protect against triple handshake attacks. + /// + /// Requires the `OSX_10_11` (or greater) feature. + #[cfg(feature = "OSX_10_11")] + fn set_allow_server_identity_change(&mut self, value: bool) -> Result<()>; + + /// If enabled, fallback countermeasures will be used during negotiation. + /// + /// It should be enabled when renegotiating with a peer with a lower + /// maximum protocol version due to an earlier failure to connect. + /// + /// Requires the `OSX_10_10` (or greater) feature. + #[cfg(feature = "OSX_10_10")] + fn fallback(&self) -> Result; + + /// If enabled, fallback countermeasures will be used during negotiation. + /// + /// It should be enabled when renegotiating with a peer with a lower + /// maximum protocol version due to an earlier failure to connect. + /// + /// Requires the `OSX_10_10` (or greater) feature. + #[cfg(feature = "OSX_10_10")] + fn set_fallback(&mut self, value: bool) -> Result<()>; + + /// If enabled, the handshake process will pause and return when the client + /// hello is recieved to support server name identification. + /// + /// Requires the `OSX_10_11` (or greater) feature. + #[cfg(feature = "OSX_10_11")] + fn break_on_client_hello(&self) -> Result; + + /// If enabled, the handshake process will pause and return when the client + /// hello is recieved to support server name identification. + /// + /// Requires the `OSX_10_11` (or greater) feature. + #[cfg(feature = "OSX_10_11")] + fn set_break_on_client_hello(&mut self, value: bool) -> Result<()>; +} + +macro_rules! impl_options { + ($($(#[$a:meta])* const $opt:ident: $get:ident & $set:ident,)*) => { + $( + $(#[$a])* + #[inline] + fn $set(&mut self, value: bool) -> Result<()> { + unsafe { + cvt(SSLSetSessionOption(self.as_inner(), + $opt, + value as ::core_foundation::base::Boolean)) + } + } + + $(#[$a])* + #[inline] + fn $get(&self) -> Result { + let mut value = 0; + unsafe { cvt(SSLGetSessionOption(self.as_inner(), $opt, &mut value))?; } + Ok(value != 0) + } + )* + } +} + +impl SslContextExt for SslContext { + fn diffie_hellman_params(&self) -> Result> { + unsafe { + let mut ptr = ptr::null(); + let mut len = 0; + cvt(SSLGetDiffieHellmanParams( + self.as_inner(), + &mut ptr, + &mut len, + ))?; + if ptr.is_null() { + Ok(None) + } else { + Ok(Some(slice::from_raw_parts(ptr.cast::(), len))) + } + } + } + + fn set_diffie_hellman_params(&mut self, dh_params: &[u8]) -> Result<()> { + unsafe { + cvt(SSLSetDiffieHellmanParams( + self.as_inner(), + dh_params.as_ptr().cast(), + dh_params.len(), + )) + } + } + + fn certificate_authorities(&self) -> Result>> { + unsafe { + let mut raw_certs = ptr::null(); + cvt(SSLCopyCertificateAuthorities( + self.as_inner(), + &mut raw_certs, + ))?; + if raw_certs.is_null() { + Ok(None) + } else { + let certs = CFArray::::wrap_under_create_rule(raw_certs) + .iter() + .map(|c| c.clone()) + .collect(); + Ok(Some(certs)) + } + } + } + + fn set_certificate_authorities(&mut self, certs: &[SecCertificate]) -> Result<()> { + unsafe { + let certs = CFArray::from_CFTypes(certs); + cvt(SSLSetCertificateAuthorities( + self.as_inner(), + certs.as_CFTypeRef(), + 1, + )) + } + } + + fn add_certificate_authorities(&mut self, certs: &[SecCertificate]) -> Result<()> { + unsafe { + let certs = CFArray::from_CFTypes(certs); + cvt(SSLSetCertificateAuthorities( + self.as_inner(), + certs.as_CFTypeRef(), + 0, + )) + } + } + + impl_options! { + #[cfg(feature = "OSX_10_11")] + const kSSLSessionOptionAllowServerIdentityChange: allow_server_identity_change & set_allow_server_identity_change, + #[cfg(feature = "OSX_10_10")] + const kSSLSessionOptionFallback: fallback & set_fallback, + #[cfg(feature = "OSX_10_11")] + const kSSLSessionOptionBreakOnClientHello: break_on_client_hello & set_break_on_client_hello, + } +} + +/// An extension trait adding OSX specific functionality to the +/// `MidHandshakeSslStream` type. +pub trait MidHandshakeSslStreamExt { + /// Returns `true` iff `break_on_client_hello` was set and the handshake + /// has progressed to that point. + /// + /// Requires the `OSX_10_11` (or greater) feature. + #[cfg(feature = "OSX_10_11")] + fn client_hello_received(&self) -> bool; +} + +impl MidHandshakeSslStreamExt for MidHandshakeSslStream { + #[cfg(feature = "OSX_10_11")] + fn client_hello_received(&self) -> bool { + self.error().code() == errSSLClientHelloReceived + } +} + +#[cfg(test)] +mod test { + use std::io::prelude::*; + use std::net::{TcpListener, TcpStream}; + use std::thread; + use tempfile::tempdir; + + use super::*; + use crate::cipher_suite::CipherSuite; + use crate::os::macos::test::identity; + use crate::secure_transport::*; + use crate::test::certificate; + + #[test] + fn server_client() { + let listener = p!(TcpListener::bind("localhost:0")); + let port = p!(listener.local_addr()).port(); + + let handle = thread::spawn(move || { + let dir = p!(tempdir()); + + let mut ctx = p!(SslContext::new( + SslProtocolSide::SERVER, + SslConnectionType::STREAM + )); + let identity = identity(dir.path()); + p!(ctx.set_certificate(&identity, &[])); + + let stream = p!(listener.accept()).0; + let mut stream = p!(ctx.handshake(stream)); + + let mut buf = [0; 12]; + p!(stream.read(&mut buf)); + assert_eq!(&buf[..], b"hello world!"); + }); + + let mut ctx = p!(SslContext::new( + SslProtocolSide::CLIENT, + SslConnectionType::STREAM + )); + p!(ctx.set_break_on_server_auth(true)); + let stream = p!(TcpStream::connect(("localhost", port))); + + let stream = match ctx.handshake(stream) { + Ok(_) => panic!("unexpected success"), + Err(HandshakeError::Interrupted(stream)) => stream, + Err(err) => panic!("unexpected error {:?}", err), + }; + + assert!(stream.server_auth_completed()); + let mut peer_trust = p!(stream.context().peer_trust2()).unwrap(); + p!(peer_trust.set_anchor_certificates(&[certificate()])); + p!(peer_trust.evaluate_with_error()); + + let mut stream = p!(stream.handshake()); + p!(stream.write_all(b"hello world!")); + + handle.join().unwrap(); + } + + #[test] + #[ignore] + fn server_client_builders() { + let listener = p!(TcpListener::bind("localhost:0")); + let port = p!(listener.local_addr()).port(); + + let handle = thread::spawn(move || { + let dir = p!(tempdir()); + + let identity = identity(dir.path()); + let builder = ServerBuilder::new(&identity, &[]); + + let stream = p!(listener.accept()).0; + let mut stream = p!(builder.handshake(stream)); + + let mut buf = [0; 12]; + p!(stream.read(&mut buf)); + assert_eq!(&buf[..], b"hello world!"); + }); + + let stream = p!(TcpStream::connect(("localhost", port))); + let mut stream = p!(ClientBuilder::new() + .anchor_certificates(&[certificate()]) + .handshake("foobar.com", stream)); + + p!(stream.write_all(b"hello world!")); + + handle.join().unwrap(); + } + + #[test] + fn client_bad_cert() { + let _ = env_logger::try_init(); + + let listener = p!(TcpListener::bind("localhost:0")); + let port = p!(listener.local_addr()).port(); + + let handle = thread::spawn(move || { + let dir = p!(tempdir()); + + let mut ctx = p!(SslContext::new( + SslProtocolSide::SERVER, + SslConnectionType::STREAM + )); + let identity = identity(dir.path()); + p!(ctx.set_certificate(&identity, &[])); + + let stream = p!(listener.accept()).0; + let _ = ctx.handshake(stream); + }); + + let stream = p!(TcpStream::connect(("localhost", port))); + assert!(ClientBuilder::new() + .handshake("foobar.com", stream) + .is_err()); + + handle.join().unwrap(); + } + + #[test] + #[ignore] + fn client() { + let listener = p!(TcpListener::bind("localhost:0")); + let port = p!(listener.local_addr()).port(); + + let handle = thread::spawn(move || { + let dir = p!(tempdir()); + + let mut ctx = p!(SslContext::new( + SslProtocolSide::SERVER, + SslConnectionType::STREAM + )); + let identity = identity(dir.path()); + p!(ctx.set_certificate(&identity, &[])); + + let stream = p!(listener.accept()).0; + let mut stream = p!(ctx.handshake(stream)); + + let mut buf = [0; 12]; + p!(stream.read(&mut buf)); + assert_eq!(&buf[..], b"hello world!"); + }); + + let stream = p!(TcpStream::connect(("localhost", port))); + let mut stream = p!(ClientBuilder::new() + .anchor_certificates(&[certificate()]) + .handshake("foobar.com", stream)); + p!(stream.write_all(b"hello world!")); + + handle.join().unwrap(); + } + + #[test] + fn negotiated_cipher() { + let listener = p!(TcpListener::bind("localhost:0")); + let port = p!(listener.local_addr()).port(); + + let handle = thread::spawn(move || { + let dir = p!(tempdir()); + + let mut ctx = p!(SslContext::new( + SslProtocolSide::SERVER, + SslConnectionType::STREAM + )); + let identity = identity(dir.path()); + p!(ctx.set_certificate(&identity, &[])); + p!(ctx.set_enabled_ciphers(&[ + CipherSuite::TLS_DHE_RSA_WITH_AES_256_CBC_SHA256, + CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 + ])); + + let stream = p!(listener.accept()).0; + let mut stream = p!(ctx.handshake(stream)); + assert_eq!( + CipherSuite::TLS_DHE_RSA_WITH_AES_256_CBC_SHA256, + p!(stream.context().negotiated_cipher()) + ); + let mut buf = [0; 1]; + p!(stream.read(&mut buf)); + }); + + let mut ctx = p!(SslContext::new( + SslProtocolSide::CLIENT, + SslConnectionType::STREAM + )); + p!(ctx.set_break_on_server_auth(true)); + p!(ctx.set_enabled_ciphers(&[ + CipherSuite::TLS_DHE_PSK_WITH_AES_128_CBC_SHA256, + CipherSuite::TLS_DHE_RSA_WITH_AES_256_CBC_SHA256 + ])); + let stream = p!(TcpStream::connect(("localhost", port))); + + let stream = match ctx.handshake(stream) { + Ok(_) => panic!("unexpected success"), + Err(HandshakeError::Interrupted(stream)) => stream, + Err(err) => panic!("unexpected error {:?}", err), + }; + + let mut stream = p!(stream.handshake()); + assert_eq!( + CipherSuite::TLS_DHE_RSA_WITH_AES_256_CBC_SHA256, + p!(stream.context().negotiated_cipher()) + ); + p!(stream.write(&[0])); + + handle.join().unwrap(); + } + + #[test] + fn dh_params() { + let params = include_bytes!("../../../test/dhparam.der"); + + let mut ctx = p!(SslContext::new( + SslProtocolSide::SERVER, + SslConnectionType::STREAM + )); + assert!(p!(ctx.diffie_hellman_params()).is_none()); + p!(ctx.set_diffie_hellman_params(params)); + assert_eq!(p!(ctx.diffie_hellman_params()).unwrap(), ¶ms[..]); + } + + #[test] + fn try_authenticate_no_cert() { + let listener = p!(TcpListener::bind("localhost:0")); + let port = p!(listener.local_addr()).port(); + + let handle = thread::spawn(move || { + let dir = p!(tempdir()); + + let mut ctx = p!(SslContext::new( + SslProtocolSide::SERVER, + SslConnectionType::STREAM + )); + let identity = identity(dir.path()); + p!(ctx.set_certificate(&identity, &[])); + p!(ctx.set_client_side_authenticate(SslAuthenticate::TRY)); + let cert = certificate(); + p!(ctx.add_certificate_authorities(&[cert])); + + let stream = p!(listener.accept()).0; + let mut stream = p!(ctx.handshake(stream)); + let mut buf = [0; 1]; + p!(stream.read(&mut buf)); + }); + + let mut ctx = p!(SslContext::new( + SslProtocolSide::CLIENT, + SslConnectionType::STREAM + )); + p!(ctx.set_break_on_server_auth(true)); + let stream = p!(TcpStream::connect(("localhost", port))); + + let stream = match ctx.handshake(stream) { + Ok(_) => panic!("unexpected success"), + Err(HandshakeError::Interrupted(stream)) => stream, + Err(err) => panic!("unexpected error {:?}", err), + }; + + let mut stream = p!(stream.handshake()); + p!(stream.write(&[0])); + + handle.join().unwrap(); + } + + #[test] + fn always_authenticate_no_cert() { + let listener = p!(TcpListener::bind("localhost:0")); + let port = p!(listener.local_addr()).port(); + + let handle = thread::spawn(move || { + let dir = p!(tempdir()); + + let mut ctx = p!(SslContext::new( + SslProtocolSide::SERVER, + SslConnectionType::STREAM + )); + let identity = identity(dir.path()); + p!(ctx.set_certificate(&identity, &[])); + p!(ctx.set_client_side_authenticate(SslAuthenticate::ALWAYS)); + + let stream = p!(listener.accept()).0; + + match ctx.handshake(stream) { + Ok(_) => panic!("unexpected success"), + Err(HandshakeError::Failure(_)) => {} + Err(err) => panic!("unexpected error {:?}", err), + } + }); + + let mut ctx = p!(SslContext::new( + SslProtocolSide::CLIENT, + SslConnectionType::STREAM + )); + p!(ctx.set_break_on_server_auth(true)); + let stream = p!(TcpStream::connect(("localhost", port))); + + let stream = match ctx.handshake(stream) { + Ok(_) => panic!("unexpected success"), + Err(HandshakeError::Interrupted(stream)) => stream, + Err(err) => panic!("unexpected error {:?}", err), + }; + + match stream.handshake() { + Ok(_) => panic!("unexpected success"), + Err(HandshakeError::Failure(_)) => {} + Err(err) => panic!("unexpected error {:?}", err), + } + + handle.join().unwrap(); + } + + #[test] + fn always_authenticate_with_cert() { + let listener = p!(TcpListener::bind("localhost:0")); + let port = p!(listener.local_addr()).port(); + + let handle = thread::spawn(move || { + let dir = p!(tempdir()); + + let mut ctx = p!(SslContext::new( + SslProtocolSide::SERVER, + SslConnectionType::STREAM + )); + let identity = identity(dir.path()); + p!(ctx.set_certificate(&identity, &[])); + p!(ctx.set_client_side_authenticate(SslAuthenticate::ALWAYS)); + + let stream = p!(listener.accept()).0; + + match ctx.handshake(stream) { + Ok(_) => panic!("unexpected success"), + Err(HandshakeError::Failure(_)) => {} + Err(err) => panic!("unexpected error {:?}", err), + } + }); + + let mut ctx = p!(SslContext::new( + SslProtocolSide::CLIENT, + SslConnectionType::STREAM + )); + p!(ctx.set_break_on_server_auth(true)); + let dir = p!(tempdir()); + let identity = identity(dir.path()); + p!(ctx.set_certificate(&identity, &[])); + let stream = p!(TcpStream::connect(("localhost", port))); + + let stream = match ctx.handshake(stream) { + Ok(_) => panic!("unexpected success"), + Err(HandshakeError::Interrupted(stream)) => stream, + Err(err) => panic!("unexpected error {:?}", err), + }; + + match stream.handshake() { + Ok(_) => panic!("unexpected success"), + Err(HandshakeError::Failure(_)) => {} + Err(err) => panic!("unexpected error {:?}", err), + } + + handle.join().unwrap(); + } + + #[test] + fn certificate_authorities() { + let mut ctx = p!(SslContext::new( + SslProtocolSide::SERVER, + SslConnectionType::STREAM + )); + assert!(p!(ctx.certificate_authorities()).is_none()); + p!(ctx.set_certificate_authorities(&[certificate()])); + assert_eq!(p!(ctx.certificate_authorities()).unwrap().len(), 1); + } + + #[test] + #[ignore] + fn close() { + let listener = p!(TcpListener::bind("localhost:0")); + let port = p!(listener.local_addr()).port(); + + let handle = thread::spawn(move || { + let dir = p!(tempdir()); + + let identity = identity(dir.path()); + let builder = ServerBuilder::new(&identity, &[]); + + let stream = p!(listener.accept()).0; + let mut stream = p!(builder.handshake(stream)); + p!(stream.close()); + }); + + let stream = p!(TcpStream::connect(("localhost", port))); + let mut stream = p!(ClientBuilder::new() + .anchor_certificates(&[certificate()]) + .handshake("foobar.com", stream)); + + let mut buf = [0; 1]; + assert_eq!(p!(stream.read(&mut buf)), 0); + p!(stream.close()); + + p!(handle.join()); + } + + #[test] + #[ignore] + fn short_read() { + let listener = p!(TcpListener::bind("localhost:0")); + let port = p!(listener.local_addr()).port(); + + let handle = thread::spawn(move || { + let dir = p!(tempdir()); + + let identity = identity(dir.path()); + let builder = ServerBuilder::new(&identity, &[]); + + let stream = p!(listener.accept()).0; + let mut stream = p!(builder.handshake(stream)); + + stream.write_all(b"hello").unwrap(); + // make sure stream doesn't close + stream + }); + + let stream = p!(TcpStream::connect(("localhost", port))); + let mut stream = p!(ClientBuilder::new() + .anchor_certificates(&[certificate()]) + .handshake("foobar.com", stream)); + + let mut b = [0; 1]; + stream.read_exact(&mut b).unwrap(); + assert_eq!(stream.context().buffered_read_size().unwrap(), 4); + let mut b = [0; 5]; + let read = stream.read(&mut b).unwrap(); + assert_eq!(read, 4); + + p!(handle.join()); + } +} diff --git a/vendor/security-framework/src/os/macos/transform.rs b/vendor/security-framework/src/os/macos/transform.rs new file mode 100644 index 000000000..d03bc1f76 --- /dev/null +++ b/vendor/security-framework/src/os/macos/transform.rs @@ -0,0 +1,54 @@ +//! Transform support + +use core_foundation::base::{CFType, TCFType}; +use core_foundation::error::CFError; +use core_foundation::string::CFString; +use security_framework_sys::transform::*; +use std::ptr; + +declare_TCFType! { + /// A type representing a transform. + SecTransform, SecTransformRef +} +impl_TCFType!(SecTransform, SecTransformRef, SecTransformGetTypeID); + +unsafe impl Sync for SecTransform {} +unsafe impl Send for SecTransform {} + +impl SecTransform { + /// Sets an attribute of the transform. + pub fn set_attribute(&mut self, key: &CFString, value: &T) -> Result<(), CFError> + where + T: TCFType, + { + unsafe { + let mut error = ptr::null_mut(); + SecTransformSetAttribute( + self.0, + key.as_concrete_TypeRef(), + value.as_CFTypeRef(), + &mut error, + ); + if !error.is_null() { + return Err(CFError::wrap_under_create_rule(error)); + } + + Ok(()) + } + } + + /// Executes the transform. + /// + /// The return type depends on the type of transform. + pub fn execute(&mut self) -> Result { + unsafe { + let mut error = ptr::null_mut(); + let result = SecTransformExecute(self.0, &mut error); + if result.is_null() { + return Err(CFError::wrap_under_create_rule(error)); + } + + Ok(CFType::wrap_under_create_rule(result)) + } + } +} diff --git a/vendor/security-framework/src/os/mod.rs b/vendor/security-framework/src/os/mod.rs new file mode 100644 index 000000000..ec7e38b34 --- /dev/null +++ b/vendor/security-framework/src/os/mod.rs @@ -0,0 +1,4 @@ +//! OS specific extensions. + +#[cfg(target_os = "macos")] +pub mod macos; diff --git a/vendor/security-framework/src/passwords.rs b/vendor/security-framework/src/passwords.rs new file mode 100644 index 000000000..83dad6d28 --- /dev/null +++ b/vendor/security-framework/src/passwords.rs @@ -0,0 +1,332 @@ +//! Support for password entries in the keychain. Works on both iOS and macOS. +//! +//! If you want the extended keychain facilities only available on macOS, use the +//! version of these functions in the macOS extensions module. + +use crate::base::Result; +use crate::passwords_options::PasswordOptions; +use crate::{cvt, Error}; +use core_foundation::base::TCFType; +use core_foundation::boolean::CFBoolean; +use core_foundation::data::CFData; +use core_foundation::dictionary::CFDictionary; +use core_foundation::string::CFString; +use core_foundation_sys::base::{CFGetTypeID, CFRelease, CFTypeRef}; +use core_foundation_sys::data::CFDataRef; +use security_framework_sys::base::{errSecDuplicateItem, errSecParam}; +use security_framework_sys::item::{kSecReturnData, kSecValueData}; +use security_framework_sys::keychain::{SecAuthenticationType, SecProtocolType}; +use security_framework_sys::keychain_item::{ + SecItemAdd, SecItemCopyMatching, SecItemDelete, SecItemUpdate, +}; + +/// Set a generic password for the given service and account. +/// Creates or updates a keychain entry. +pub fn set_generic_password(service: &str, account: &str, password: &[u8]) -> Result<()> { + let mut options = PasswordOptions::new_generic_password(service, account); + set_password_internal(&mut options, password) +} + +/// Get the generic password for the given service and account. If no matching +/// keychain entry exists, fails with error code `errSecItemNotFound`. +pub fn get_generic_password(service: &str, account: &str) -> Result> { + let mut options = PasswordOptions::new_generic_password(service, account); + options.query.push(( + unsafe { CFString::wrap_under_get_rule(kSecReturnData) }, + CFBoolean::from(true).into_CFType(), + )); + let params = CFDictionary::from_CFType_pairs(&options.query); + let mut ret: CFTypeRef = std::ptr::null(); + cvt(unsafe { SecItemCopyMatching(params.as_concrete_TypeRef(), &mut ret) })?; + get_password_and_release(ret) +} + +/// Delete the generic password keychain entry for the given service and account. +/// If none exists, fails with error code `errSecItemNotFound`. +pub fn delete_generic_password(service: &str, account: &str) -> Result<()> { + let options = PasswordOptions::new_generic_password(service, account); + let params = CFDictionary::from_CFType_pairs(&options.query); + cvt(unsafe { SecItemDelete(params.as_concrete_TypeRef()) }) +} + +/// Set an internet password for the given endpoint parameters. +/// Creates or updates a keychain entry. +#[allow(clippy::too_many_arguments)] +pub fn set_internet_password( + server: &str, + security_domain: Option<&str>, + account: &str, + path: &str, + port: Option, + protocol: SecProtocolType, + authentication_type: SecAuthenticationType, + password: &[u8], +) -> Result<()> { + let mut options = PasswordOptions::new_internet_password( + server, + security_domain, + account, + path, + port, + protocol, + authentication_type, + ); + set_password_internal(&mut options, password) +} + +/// Get the internet password for the given endpoint parameters. If no matching +/// keychain entry exists, fails with error code `errSecItemNotFound`. +pub fn get_internet_password( + server: &str, + security_domain: Option<&str>, + account: &str, + path: &str, + port: Option, + protocol: SecProtocolType, + authentication_type: SecAuthenticationType, +) -> Result> { + let mut options = PasswordOptions::new_internet_password( + server, + security_domain, + account, + path, + port, + protocol, + authentication_type, + ); + options.query.push(( + unsafe { CFString::wrap_under_get_rule(kSecReturnData) }, + CFBoolean::from(true).into_CFType(), + )); + let params = CFDictionary::from_CFType_pairs(&options.query); + let mut ret: CFTypeRef = std::ptr::null(); + cvt(unsafe { SecItemCopyMatching(params.as_concrete_TypeRef(), &mut ret) })?; + get_password_and_release(ret) +} + +/// Delete the internet password for the given endpoint parameters. +/// If none exists, fails with error code `errSecItemNotFound`. +pub fn delete_internet_password( + server: &str, + security_domain: Option<&str>, + account: &str, + path: &str, + port: Option, + protocol: SecProtocolType, + authentication_type: SecAuthenticationType, +) -> Result<()> { + let options = PasswordOptions::new_internet_password( + server, + security_domain, + account, + path, + port, + protocol, + authentication_type, + ); + let params = CFDictionary::from_CFType_pairs(&options.query); + cvt(unsafe { SecItemDelete(params.as_concrete_TypeRef()) }) +} + +// This starts by trying to create the password with the given query params. +// If the creation attempt reveals that one exists, its password is updated. +fn set_password_internal(options: &mut PasswordOptions, password: &[u8]) -> Result<()> { + let query_len = options.query.len(); + options.query.push(( + unsafe { CFString::wrap_under_get_rule(kSecValueData) }, + CFData::from_buffer(password).into_CFType(), + )); + + let params = CFDictionary::from_CFType_pairs(&options.query); + let mut ret = std::ptr::null(); + let status = unsafe { SecItemAdd(params.as_concrete_TypeRef(), &mut ret) }; + if status == errSecDuplicateItem { + let params = CFDictionary::from_CFType_pairs(&options.query[0..query_len]); + let update = CFDictionary::from_CFType_pairs(&options.query[query_len..]); + cvt(unsafe { SecItemUpdate(params.as_concrete_TypeRef(), update.as_concrete_TypeRef()) }) + } else { + cvt(status) + } +} + +// Having retrieved a password entry, this copies and returns the password. +// +// # Safety +// The data element passed in is assumed to have been returned from a Copy +// call, so it's released after we are done with it. +fn get_password_and_release(data: CFTypeRef) -> Result> { + if !data.is_null() { + let type_id = unsafe { CFGetTypeID(data) }; + if type_id == CFData::type_id() { + let val = unsafe { CFData::wrap_under_create_rule(data as CFDataRef) }; + let mut vec = Vec::new(); + vec.extend_from_slice(val.bytes()); + return Ok(vec); + } else { + // unexpected: we got a reference to some other type. + // Release it to make sure there's no leak, but + // we can't return the password in this case. + unsafe { CFRelease(data) }; + } + } + Err(Error::from_code(errSecParam)) +} + +#[cfg(test)] +mod test { + use super::*; + use security_framework_sys::base::errSecItemNotFound; + + #[test] + fn missing_generic() { + let name = "a string not likely to already be in the keychain as service or account"; + let result = delete_generic_password(name, name); + match result { + Ok(()) => (), // this is ok because the name _might_ be in the keychain + Err(err) if err.code() == errSecItemNotFound => (), + Err(err) => panic!("missing_generic: delete failed with status: {}", err.code()), + }; + let result = get_generic_password(name, name); + match result { + Ok(bytes) => panic!("missing_generic: get returned {:?}", bytes), + Err(err) if err.code() == errSecItemNotFound => (), + Err(err) => panic!("missing_generic: get failed with status: {}", err.code()), + }; + let result = delete_generic_password(name, name); + match result { + Ok(()) => panic!("missing_generic: second delete found a password"), + Err(err) if err.code() == errSecItemNotFound => (), + Err(err) => panic!("missing_generic: delete failed with status: {}", err.code()), + }; + } + + #[test] + fn roundtrip_generic() { + let name = "roundtrip_generic"; + set_generic_password(name, name, name.as_bytes()).expect("set_generic_password"); + let pass = get_generic_password(name, name).expect("get_generic_password"); + assert_eq!(name.as_bytes(), pass); + delete_generic_password(name, name).expect("delete_generic_password") + } + + #[test] + fn update_generic() { + let name = "update_generic"; + set_generic_password(name, name, name.as_bytes()).expect("set_generic_password"); + let alternate = "update_generic_alternate"; + set_generic_password(name, name, alternate.as_bytes()).expect("set_generic_password"); + let pass = get_generic_password(name, name).expect("get_generic_password"); + assert_eq!(pass, alternate.as_bytes()); + delete_generic_password(name, name).expect("delete_generic_password") + } + + #[test] + fn missing_internet() { + let name = "a string not likely to already be in the keychain as service or account"; + let (server, domain, account, path, port, protocol, auth) = ( + name, + None, + name, + "/", + Some(8080u16), + SecProtocolType::HTTP, + SecAuthenticationType::Any, + ); + let result = delete_internet_password(server, domain, account, path, port, protocol, auth); + match result { + Ok(()) => (), // this is ok because the name _might_ be in the keychain + Err(err) if err.code() == errSecItemNotFound => (), + Err(err) => panic!( + "missing_internet: delete failed with status: {}", + err.code() + ), + }; + let result = get_internet_password(server, domain, account, path, port, protocol, auth); + match result { + Ok(bytes) => panic!("missing_internet: get returned {:?}", bytes), + Err(err) if err.code() == errSecItemNotFound => (), + Err(err) => panic!("missing_internet: get failed with status: {}", err.code()), + }; + let result = delete_internet_password(server, domain, account, path, port, protocol, auth); + match result { + Ok(()) => panic!("missing_internet: second delete found a password"), + Err(err) if err.code() == errSecItemNotFound => (), + Err(err) => panic!( + "missing_internet: delete failed with status: {}", + err.code() + ), + }; + } + + #[test] + fn roundtrip_internet() { + let name = "roundtrip_internet"; + let (server, domain, account, path, port, protocol, auth) = ( + name, + None, + name, + "/", + Some(8080u16), + SecProtocolType::HTTP, + SecAuthenticationType::Any, + ); + set_internet_password( + server, + domain, + account, + path, + port, + protocol, + auth, + name.as_bytes(), + ) + .expect("set_internet_password"); + let pass = get_internet_password(server, domain, account, path, port, protocol, auth) + .expect("get_internet_password"); + assert_eq!(name.as_bytes(), pass); + delete_internet_password(server, domain, account, path, port, protocol, auth) + .expect("delete_internet_password"); + } + + #[test] + fn update_internet() { + let name = "update_internet"; + let (server, domain, account, path, port, protocol, auth) = ( + name, + None, + name, + "/", + Some(8080u16), + SecProtocolType::HTTP, + SecAuthenticationType::Any, + ); + set_internet_password( + server, + domain, + account, + path, + port, + protocol, + auth, + name.as_bytes(), + ) + .expect("set_internet_password"); + let alternate = "alternate_internet_password"; + set_internet_password( + server, + domain, + account, + path, + port, + protocol, + auth, + alternate.as_bytes(), + ) + .expect("set_internet_password"); + let pass = get_internet_password(server, domain, account, path, port, protocol, auth) + .expect("get_internet_password"); + assert_eq!(pass, alternate.as_bytes()); + delete_internet_password(server, domain, account, path, port, protocol, auth) + .expect("delete_internet_password"); + } +} diff --git a/vendor/security-framework/src/passwords_options.rs b/vendor/security-framework/src/passwords_options.rs new file mode 100644 index 000000000..d94bcf97d --- /dev/null +++ b/vendor/security-framework/src/passwords_options.rs @@ -0,0 +1,129 @@ +//! Support for password options, to be used with the passwords module + +use core_foundation::{string::CFString, base::{CFType, TCFType, CFOptionFlags}, number::CFNumber}; +use security_framework_sys::{keychain::{SecProtocolType, SecAuthenticationType}, access_control::*}; +use security_framework_sys::item::{ + kSecAttrAccessControl, kSecAttrAccount, kSecAttrAuthenticationType, kSecAttrPath, kSecAttrPort, kSecAttrProtocol, + kSecAttrSecurityDomain, kSecAttrServer, kSecAttrService, kSecClass, kSecClassGenericPassword, + kSecClassInternetPassword, +}; +use crate::access_control::SecAccessControl; + +/// `PasswordOptions` constructor +pub struct PasswordOptions { + /// query built for the keychain request + pub query: Vec<(CFString, CFType)>, +} + +bitflags::bitflags! { + /// The option flags used to configure the evaluation of a `SecAccessControl`. + pub struct AccessControlOptions: CFOptionFlags { + /** Constraint to access an item with either biometry or passcode. */ + const USER_PRESENCE = kSecAccessControlUserPresence; + #[cfg(feature = "OSX_10_13")] + /** Constraint to access an item with Touch ID for any enrolled fingers, or Face ID. */ + const BIOMETRY_ANY = kSecAccessControlBiometryAny; + #[cfg(feature = "OSX_10_13")] + /** Constraint to access an item with Touch ID for currently enrolled fingers, or from Face ID with the currently enrolled user. */ + const BIOMETRY_CURRENT_SET = kSecAccessControlBiometryCurrentSet; + /** Constraint to access an item with a passcode. */ + const DEVICE_PASSCODE = kSecAccessControlDevicePasscode; + #[cfg(feature = "OSX_10_15")] + /** Constraint to access an item with a watch. */ + const WATCH = kSecAccessControlWatch; + /** Indicates that at least one constraint must be satisfied. */ + const OR = kSecAccessControlOr; + /** Indicates that all constraints must be satisfied. */ + const AND = kSecAccessControlAnd; + /** Enable a private key to be used in signing a block of data or verifying a signed block. */ + const PRIVATE_KEY_USAGE = kSecAccessControlPrivateKeyUsage; + /** Option to use an application-provided password for data encryption key generation. */ + const APPLICATION_PASSWORD = kSecAccessControlApplicationPassword; + } +} + +impl PasswordOptions { + /// Create a new generic password options + /// Generic passwords are identified by service and account. They have other + /// attributes, but this interface doesn't allow specifying them. + #[must_use] pub fn new_generic_password(service: &str, account: &str) -> Self { + let query = vec![ + ( + unsafe { CFString::wrap_under_get_rule(kSecClass) }, + unsafe { CFString::wrap_under_get_rule(kSecClassGenericPassword).into_CFType() }, + ), + ( + unsafe { CFString::wrap_under_get_rule(kSecAttrService) }, + CFString::from(service).into_CFType(), + ), + ( + unsafe { CFString::wrap_under_get_rule(kSecAttrAccount) }, + CFString::from(account).into_CFType(), + ), + ]; + Self { query } + } + + /// Create a new internet password options + /// Internet passwords are identified by a number of attributes. + /// They can have others, but this interface doesn't allow specifying them. + #[must_use] pub fn new_internet_password( + server: &str, + security_domain: Option<&str>, + account: &str, + path: &str, + port: Option, + protocol: SecProtocolType, + authentication_type: SecAuthenticationType, + ) -> Self { + let mut query = vec![ + ( + unsafe { CFString::wrap_under_get_rule(kSecClass) }, + unsafe { CFString::wrap_under_get_rule(kSecClassInternetPassword) }.into_CFType(), + ), + ( + unsafe { CFString::wrap_under_get_rule(kSecAttrServer) }, + CFString::from(server).into_CFType(), + ), + ( + unsafe { CFString::wrap_under_get_rule(kSecAttrPath) }, + CFString::from(path).into_CFType(), + ), + ( + unsafe { CFString::wrap_under_get_rule(kSecAttrAccount) }, + CFString::from(account).into_CFType(), + ), + ( + unsafe { CFString::wrap_under_get_rule(kSecAttrProtocol) }, + CFNumber::from(protocol as i32).into_CFType(), + ), + ( + unsafe { CFString::wrap_under_get_rule(kSecAttrAuthenticationType) }, + CFNumber::from(authentication_type as i32).into_CFType(), + ), + ]; + if let Some(domain) = security_domain { + query.push(( + unsafe { CFString::wrap_under_get_rule(kSecAttrSecurityDomain) }, + CFString::from(domain).into_CFType(), + )) + } + if let Some(port) = port { + query.push(( + unsafe { CFString::wrap_under_get_rule(kSecAttrPort) }, + CFNumber::from(i32::from(port)).into_CFType(), + )) + } + Self { query } + } + + /// Add access control to the password + pub fn set_access_control_options(&mut self, options: AccessControlOptions) { + self.query.push(( + unsafe { CFString::wrap_under_get_rule(kSecAttrAccessControl) }, + SecAccessControl::create_with_flags(options.bits()) + .unwrap() + .into_CFType(), + )) + } +} diff --git a/vendor/security-framework/src/policy.rs b/vendor/security-framework/src/policy.rs new file mode 100644 index 000000000..85f361cb0 --- /dev/null +++ b/vendor/security-framework/src/policy.rs @@ -0,0 +1,104 @@ +//! Security Policies support. +#[cfg(any(feature = "OSX_10_9", target_os = "ios"))] +use core_foundation::base::CFOptionFlags; +use core_foundation::base::TCFType; +use core_foundation::string::CFString; +#[cfg(any(feature = "OSX_10_9", target_os = "ios"))] +use security_framework_sys::base::errSecParam; +use security_framework_sys::base::SecPolicyRef; +use security_framework_sys::policy::*; +use std::fmt; +use std::ptr; + +use crate::secure_transport::SslProtocolSide; +#[cfg(any(feature = "OSX_10_9", target_os = "ios"))] +use crate::Error; + +declare_TCFType! { + /// A type representing a certificate validation policy. + SecPolicy, SecPolicyRef +} +impl_TCFType!(SecPolicy, SecPolicyRef, SecPolicyGetTypeID); + +unsafe impl Sync for SecPolicy {} +unsafe impl Send for SecPolicy {} + +impl fmt::Debug for SecPolicy { + #[cold] + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.debug_struct("SecPolicy").finish() + } +} + +#[cfg(any(feature = "OSX_10_9", target_os = "ios"))] +bitflags::bitflags! { + /// The flags used to specify revocation policy options. + pub struct RevocationPolicy: CFOptionFlags { + /// Perform revocation checking using OCSP (Online Certificate Status Protocol). + const OCSP_METHOD = kSecRevocationOCSPMethod; + /// Perform revocation checking using the CRL (Certification Revocation List) method. + const CRL_METHOD = kSecRevocationCRLMethod; + /// Prefer CRL revocation checking over OCSP; by default, OCSP is preferred. + const PREFER_CRL = kSecRevocationPreferCRL; + /// Require a positive response to pass the policy. + const REQUIRE_POSITIVE_RESPONSE = kSecRevocationRequirePositiveResponse; + /// Consult only locally cached replies; do not use network access. + const NETWORK_ACCESS_DISABLED = kSecRevocationNetworkAccessDisabled; + /// Perform either OCSP or CRL checking. + const USE_ANY_METHOD_AVAILABLE = kSecRevocationUseAnyAvailableMethod; + } +} + +impl SecPolicy { + /// Creates a `SecPolicy` for evaluating SSL certificate chains. + /// + /// The side which you are evaluating should be provided (i.e. pass `SslSslProtocolSide::SERVER` if + /// you are a client looking to validate a server's certificate chain). + pub fn create_ssl(protocol_side: SslProtocolSide, hostname: Option<&str>) -> Self { + let hostname = hostname.map(CFString::new); + let hostname = hostname + .as_ref() + .map(|s| s.as_concrete_TypeRef()) + .unwrap_or(ptr::null_mut()); + let is_server = protocol_side == SslProtocolSide::SERVER; + unsafe { + let policy = SecPolicyCreateSSL(is_server as _, hostname); + Self::wrap_under_create_rule(policy) + } + } + + #[cfg(any(feature = "OSX_10_9", target_os = "ios"))] + /// Creates a `SecPolicy` for checking revocation of certificates. + /// + /// If you do not specify this policy creating a `SecTrust` object, the system defaults + /// will be used during evaluation. + pub fn create_revocation(options: RevocationPolicy) -> crate::Result { + let policy = unsafe { SecPolicyCreateRevocation(options.bits()) }; + + if policy.is_null() { + Err(Error::from_code(errSecParam)) + } else { + Ok(unsafe { Self::wrap_under_create_rule(policy) }) + } + } + + /// Returns a policy object for the default X.509 policy. + #[must_use] + pub fn create_x509() -> Self { + unsafe { + let policy = SecPolicyCreateBasicX509(); + Self::wrap_under_create_rule(policy) + } + } +} + +#[cfg(test)] +mod test { + use crate::policy::SecPolicy; + use crate::secure_transport::SslProtocolSide; + + #[test] + fn create_ssl() { + SecPolicy::create_ssl(SslProtocolSide::SERVER, Some("certifi.org")); + } +} diff --git a/vendor/security-framework/src/random.rs b/vendor/security-framework/src/random.rs new file mode 100644 index 000000000..7bd7f6141 --- /dev/null +++ b/vendor/security-framework/src/random.rs @@ -0,0 +1,39 @@ +//! Randomness support. + +use security_framework_sys::random::{SecRandomCopyBytes, SecRandomRef, kSecRandomDefault}; +use std::io; + +/// A source of random data. +pub struct SecRandom(SecRandomRef); + +unsafe impl Sync for SecRandom {} +unsafe impl Send for SecRandom {} + +impl Default for SecRandom { + #[inline(always)] + fn default() -> Self { + unsafe { Self(kSecRandomDefault) } + } +} + +impl SecRandom { + /// Fills the buffer with cryptographically secure random bytes. + pub fn copy_bytes(&self, buf: &mut [u8]) -> io::Result<()> { + if unsafe { SecRandomCopyBytes(self.0, buf.len(), buf.as_mut_ptr().cast()) } == 0 { + Ok(()) + } else { + Err(io::Error::last_os_error()) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn basic() { + let mut buf = [0; 10]; + SecRandom::default().copy_bytes(&mut buf).unwrap(); + } +} diff --git a/vendor/security-framework/src/secure_transport.rs b/vendor/security-framework/src/secure_transport.rs new file mode 100644 index 000000000..18ce794e7 --- /dev/null +++ b/vendor/security-framework/src/secure_transport.rs @@ -0,0 +1,1848 @@ +//! SSL/TLS encryption support using Secure Transport. +//! +//! # Examples +//! +//! To connect as a client to a server with a certificate trusted by the system: +//! +//! ```rust +//! use std::io::prelude::*; +//! use std::net::TcpStream; +//! use security_framework::secure_transport::ClientBuilder; +//! +//! let stream = TcpStream::connect("google.com:443").unwrap(); +//! let mut stream = ClientBuilder::new().handshake("google.com", stream).unwrap(); +//! +//! stream.write_all(b"GET / HTTP/1.0\r\n\r\n").unwrap(); +//! let mut page = vec![]; +//! stream.read_to_end(&mut page).unwrap(); +//! println!("{}", String::from_utf8_lossy(&page)); +//! ``` +//! +//! To connect to a server with a certificate that's *not* trusted by the +//! system, specify the root certificates for the server's chain to the +//! `ClientBuilder`: +//! +//! ```rust,no_run +//! use std::io::prelude::*; +//! use std::net::TcpStream; +//! use security_framework::secure_transport::ClientBuilder; +//! +//! # let root_cert = unsafe { std::mem::zeroed() }; +//! let stream = TcpStream::connect("my_server.com:443").unwrap(); +//! let mut stream = ClientBuilder::new() +//! .anchor_certificates(&[root_cert]) +//! .handshake("my_server.com", stream) +//! .unwrap(); +//! +//! stream.write_all(b"GET / HTTP/1.0\r\n\r\n").unwrap(); +//! let mut page = vec![]; +//! stream.read_to_end(&mut page).unwrap(); +//! println!("{}", String::from_utf8_lossy(&page)); +//! ``` +//! +//! For more advanced configuration, the `SslContext` type can be used directly. +//! +//! To run a server: +//! +//! ```rust,no_run +//! use std::net::TcpListener; +//! use std::thread; +//! use security_framework::secure_transport::{SslContext, SslProtocolSide, SslConnectionType}; +//! +//! // Create a TCP listener and start accepting on it. +//! let mut listener = TcpListener::bind("0.0.0.0:443").unwrap(); +//! +//! for stream in listener.incoming() { +//! let stream = stream.unwrap(); +//! thread::spawn(move || { +//! // Create a new context configured to operate on the server side of +//! // a traditional SSL/TLS session. +//! let mut ctx = SslContext::new(SslProtocolSide::SERVER, SslConnectionType::STREAM) +//! .unwrap(); +//! +//! // Install the certificate chain that we will be using. +//! # let identity = unsafe { std::mem::zeroed() }; +//! # let intermediate_cert = unsafe { std::mem::zeroed() }; +//! # let root_cert = unsafe { std::mem::zeroed() }; +//! ctx.set_certificate(identity, &[intermediate_cert, root_cert]).unwrap(); +//! +//! // Perform the SSL/TLS handshake and get our stream. +//! let mut stream = ctx.handshake(stream).unwrap(); +//! }); +//! } +//! +//! ``` +#[allow(unused_imports)] +use core_foundation::array::{CFArray, CFArrayRef}; + +use core_foundation::base::{Boolean, TCFType}; +#[cfg(feature = "alpn")] +use core_foundation::string::CFString; +use core_foundation_sys::base::{kCFAllocatorDefault, OSStatus}; +use std::os::raw::c_void; + +#[allow(unused_imports)] +use security_framework_sys::base::{ + errSecBadReq, errSecIO, errSecNotTrusted, errSecSuccess, errSecTrustSettingDeny, + errSecUnimplemented, +}; + +use security_framework_sys::secure_transport::*; +use std::any::Any; +use std::cmp; +use std::fmt; +use std::io; +use std::io::prelude::*; +use std::marker::PhantomData; +use std::panic::{self, AssertUnwindSafe}; +use std::ptr; +use std::result; +use std::slice; + +use crate::base::{Error, Result}; +use crate::certificate::SecCertificate; +use crate::cipher_suite::CipherSuite; +use crate::identity::SecIdentity; +use crate::import_export::Pkcs12ImportOptions; +use crate::policy::SecPolicy; +use crate::trust::SecTrust; +use crate::{cvt, AsInner}; +use security_framework_sys::base::errSecParam; + +/// Specifies a side of a TLS session. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct SslProtocolSide(SSLProtocolSide); + +impl SslProtocolSide { + /// The server side of the session. + pub const SERVER: Self = Self(kSSLServerSide); + + /// The client side of the session. + pub const CLIENT: Self = Self(kSSLClientSide); +} + +/// Specifies the type of TLS session. +#[derive(Debug, Copy, Clone)] +pub struct SslConnectionType(SSLConnectionType); + +impl SslConnectionType { + /// A traditional TLS stream. + pub const STREAM: Self = Self(kSSLStreamType); + + /// A DTLS session. + pub const DATAGRAM: Self = Self(kSSLDatagramType); +} + +/// An error or intermediate state after a TLS handshake attempt. +#[derive(Debug)] +pub enum HandshakeError { + /// The handshake failed. + Failure(Error), + /// The handshake was interrupted midway through. + Interrupted(MidHandshakeSslStream), +} + +impl From for HandshakeError { + #[inline(always)] + fn from(err: Error) -> Self { + Self::Failure(err) + } +} + +/// An error or intermediate state after a TLS handshake attempt. +#[derive(Debug)] +pub enum ClientHandshakeError { + /// The handshake failed. + Failure(Error), + /// The handshake was interrupted midway through. + Interrupted(MidHandshakeClientBuilder), +} + +impl From for ClientHandshakeError { + #[inline(always)] + fn from(err: Error) -> Self { + Self::Failure(err) + } +} + +/// An SSL stream midway through the handshake process. +#[derive(Debug)] +pub struct MidHandshakeSslStream { + stream: SslStream, + error: Error, +} + +impl MidHandshakeSslStream { + /// Returns a shared reference to the inner stream. + #[inline(always)] + #[must_use] + pub fn get_ref(&self) -> &S { + self.stream.get_ref() + } + + /// Returns a mutable reference to the inner stream. + #[inline(always)] + pub fn get_mut(&mut self) -> &mut S { + self.stream.get_mut() + } + + /// Returns a shared reference to the `SslContext` of the stream. + #[inline(always)] + #[must_use] + pub fn context(&self) -> &SslContext { + self.stream.context() + } + + /// Returns a mutable reference to the `SslContext` of the stream. + #[inline(always)] + pub fn context_mut(&mut self) -> &mut SslContext { + self.stream.context_mut() + } + + /// Returns `true` iff `break_on_server_auth` was set and the handshake has + /// progressed to that point. + #[inline(always)] + #[must_use] + pub fn server_auth_completed(&self) -> bool { + self.error.code() == errSSLPeerAuthCompleted + } + + /// Returns `true` iff `break_on_cert_requested` was set and the handshake + /// has progressed to that point. + #[inline(always)] + #[must_use] + pub fn client_cert_requested(&self) -> bool { + self.error.code() == errSSLClientCertRequested + } + + /// Returns `true` iff the underlying stream returned an error with the + /// `WouldBlock` kind. + #[inline(always)] + #[must_use] + pub fn would_block(&self) -> bool { + self.error.code() == errSSLWouldBlock + } + + /// Returns the error which caused the handshake interruption. + #[inline(always)] + #[must_use] + pub fn error(&self) -> &Error { + &self.error + } + + /// Restarts the handshake process. + #[inline(always)] + pub fn handshake(self) -> result::Result, HandshakeError> { + self.stream.handshake() + } +} + +/// An SSL stream midway through the handshake process. +#[derive(Debug)] +pub struct MidHandshakeClientBuilder { + stream: MidHandshakeSslStream, + domain: Option, + certs: Vec, + trust_certs_only: bool, + danger_accept_invalid_certs: bool, +} + +impl MidHandshakeClientBuilder { + /// Returns a shared reference to the inner stream. + #[inline(always)] + #[must_use] + pub fn get_ref(&self) -> &S { + self.stream.get_ref() + } + + /// Returns a mutable reference to the inner stream. + #[inline(always)] + pub fn get_mut(&mut self) -> &mut S { + self.stream.get_mut() + } + + /// Returns the error which caused the handshake interruption. + #[inline(always)] + #[must_use] + pub fn error(&self) -> &Error { + self.stream.error() + } + + /// Restarts the handshake process. + pub fn handshake(self) -> result::Result, ClientHandshakeError> { + let MidHandshakeClientBuilder { + stream, + domain, + certs, + trust_certs_only, + danger_accept_invalid_certs, + } = self; + + let mut result = stream.handshake(); + loop { + let stream = match result { + Ok(stream) => return Ok(stream), + Err(HandshakeError::Interrupted(stream)) => stream, + Err(HandshakeError::Failure(err)) => { + return Err(ClientHandshakeError::Failure(err)) + } + }; + + if stream.would_block() { + let ret = MidHandshakeClientBuilder { + stream, + domain, + certs, + trust_certs_only, + danger_accept_invalid_certs, + }; + return Err(ClientHandshakeError::Interrupted(ret)); + } + + if stream.server_auth_completed() { + if danger_accept_invalid_certs { + result = stream.handshake(); + continue; + } + let mut trust = match stream.context().peer_trust2()? { + Some(trust) => trust, + None => { + result = stream.handshake(); + continue; + } + }; + trust.set_anchor_certificates(&certs)?; + trust.set_trust_anchor_certificates_only(self.trust_certs_only)?; + let policy = SecPolicy::create_ssl(SslProtocolSide::SERVER, domain.as_deref()); + trust.set_policy(&policy)?; + trust.evaluate_with_error().map_err(|error| { + #[cfg(feature = "log")] + log::warn!("SecTrustEvaluateWithError: {}", error.to_string()); + Error::from_code(error.code() as _) + })?; + result = stream.handshake(); + continue; + } + + let err = Error::from_code(stream.error().code()); + return Err(ClientHandshakeError::Failure(err)); + } + } +} + +/// Specifies the state of a TLS session. +#[derive(Debug, PartialEq, Eq)] +pub struct SessionState(SSLSessionState); + +impl SessionState { + /// The session has not yet started. + pub const IDLE: Self = Self(kSSLIdle); + + /// The session is in the handshake process. + pub const HANDSHAKE: Self = Self(kSSLHandshake); + + /// The session is connected. + pub const CONNECTED: Self = Self(kSSLConnected); + + /// The session has been terminated. + pub const CLOSED: Self = Self(kSSLClosed); + + /// The session has been aborted due to an error. + pub const ABORTED: Self = Self(kSSLAborted); +} + +/// Specifies a server's requirement for client certificates. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct SslAuthenticate(SSLAuthenticate); + +impl SslAuthenticate { + /// Do not request a client certificate. + pub const NEVER: Self = Self(kNeverAuthenticate); + + /// Require a client certificate. + pub const ALWAYS: Self = Self(kAlwaysAuthenticate); + + /// Request but do not require a client certificate. + pub const TRY: Self = Self(kTryAuthenticate); +} + +/// Specifies the state of client certificate processing. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct SslClientCertificateState(SSLClientCertificateState); + +impl SslClientCertificateState { + /// A client certificate has not been requested or sent. + pub const NONE: Self = Self(kSSLClientCertNone); + + /// A client certificate has been requested but not recieved. + pub const REQUESTED: Self = Self(kSSLClientCertRequested); + /// A client certificate has been received and successfully validated. + pub const SENT: Self = Self(kSSLClientCertSent); + + /// A client certificate has been received but has failed to validate. + pub const REJECTED: Self = Self(kSSLClientCertRejected); +} + +/// Specifies protocol versions. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct SslProtocol(SSLProtocol); + +impl SslProtocol { + /// No protocol has been or should be negotiated or specified; use the default. + pub const UNKNOWN: Self = Self(kSSLProtocolUnknown); + + /// The SSL 3.0 protocol is preferred, though SSL 2.0 may be used if the peer does not support + /// SSL 3.0. + pub const SSL3: Self = Self(kSSLProtocol3); + + /// The TLS 1.0 protocol is preferred, though lower versions may be used + /// if the peer does not support TLS 1.0. + pub const TLS1: Self = Self(kTLSProtocol1); + + /// The TLS 1.1 protocol is preferred, though lower versions may be used + /// if the peer does not support TLS 1.1. + pub const TLS11: Self = Self(kTLSProtocol11); + + /// The TLS 1.2 protocol is preferred, though lower versions may be used + /// if the peer does not support TLS 1.2. + pub const TLS12: Self = Self(kTLSProtocol12); + + /// The TLS 1.3 protocol is preferred, though lower versions may be used + /// if the peer does not support TLS 1.3. + pub const TLS13: Self = Self(kTLSProtocol13); + + /// Only the SSL 2.0 protocol is accepted. + pub const SSL2: Self = Self(kSSLProtocol2); + + /// The `DTLSv1` protocol is preferred. + pub const DTLS1: Self = Self(kDTLSProtocol1); + + /// Only the SSL 3.0 protocol is accepted. + pub const SSL3_ONLY: Self = Self(kSSLProtocol3Only); + + /// Only the TLS 1.0 protocol is accepted. + pub const TLS1_ONLY: Self = Self(kTLSProtocol1Only); + + /// All supported TLS/SSL versions are accepted. + pub const ALL: Self = Self(kSSLProtocolAll); +} + +declare_TCFType! { + /// A Secure Transport SSL/TLS context object. + SslContext, SSLContextRef +} + +impl_TCFType!(SslContext, SSLContextRef, SSLContextGetTypeID); + +impl fmt::Debug for SslContext { + #[cold] + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut builder = fmt.debug_struct("SslContext"); + if let Ok(state) = self.state() { + builder.field("state", &state); + } + builder.finish() + } +} + +unsafe impl Sync for SslContext {} +unsafe impl Send for SslContext {} + +impl AsInner for SslContext { + type Inner = SSLContextRef; + + #[inline(always)] + fn as_inner(&self) -> SSLContextRef { + self.0 + } +} + +macro_rules! impl_options { + ($($(#[$a:meta])* const $opt:ident: $get:ident & $set:ident,)*) => { + $( + $(#[$a])* + #[inline(always)] + pub fn $set(&mut self, value: bool) -> Result<()> { + unsafe { cvt(SSLSetSessionOption(self.0, $opt, value as Boolean)) } + } + + $(#[$a])* + #[inline] + pub fn $get(&self) -> Result { + let mut value = 0; + unsafe { cvt(SSLGetSessionOption(self.0, $opt, &mut value))?; } + Ok(value != 0) + } + )* + } +} + +impl SslContext { + /// Creates a new `SslContext` for the specified side and type of SSL + /// connection. + #[inline] + pub fn new(side: SslProtocolSide, type_: SslConnectionType) -> Result { + unsafe { + let ctx = SSLCreateContext(kCFAllocatorDefault, side.0, type_.0); + Ok(Self(ctx)) + } + } + + /// Sets the fully qualified domain name of the peer. + /// + /// This will be used on the client side of a session to validate the + /// common name field of the server's certificate. It has no effect if + /// called on a server-side `SslContext`. + /// + /// It is *highly* recommended to call this method before starting the + /// handshake process. + #[inline] + pub fn set_peer_domain_name(&mut self, peer_name: &str) -> Result<()> { + unsafe { + // SSLSetPeerDomainName doesn't need a null terminated string + cvt(SSLSetPeerDomainName( + self.0, + peer_name.as_ptr().cast(), + peer_name.len(), + )) + } + } + + /// Returns the peer domain name set by `set_peer_domain_name`. + pub fn peer_domain_name(&self) -> Result { + unsafe { + let mut len = 0; + cvt(SSLGetPeerDomainNameLength(self.0, &mut len))?; + let mut buf = vec![0; len]; + cvt(SSLGetPeerDomainName( + self.0, + buf.as_mut_ptr().cast(), + &mut len, + ))?; + Ok(String::from_utf8(buf).unwrap()) + } + } + + /// Sets the certificate to be used by this side of the SSL session. + /// + /// This must be called before the handshake for server-side connections, + /// and can be used on the client-side to specify a client certificate. + /// + /// The `identity` corresponds to the leaf certificate and private + /// key, and the `certs` correspond to extra certificates in the chain. + pub fn set_certificate( + &mut self, + identity: &SecIdentity, + certs: &[SecCertificate], + ) -> Result<()> { + let mut arr = vec![identity.as_CFType()]; + arr.extend(certs.iter().map(|c| c.as_CFType())); + let certs = CFArray::from_CFTypes(&arr); + + unsafe { cvt(SSLSetCertificate(self.0, certs.as_concrete_TypeRef())) } + } + + /// Sets the peer ID of this session. + /// + /// A peer ID is an opaque sequence of bytes that will be used by Secure + /// Transport to identify the peer of an SSL session. If the peer ID of + /// this session matches that of a previously terminated session, the + /// previous session can be resumed without requiring a full handshake. + #[inline] + pub fn set_peer_id(&mut self, peer_id: &[u8]) -> Result<()> { + unsafe { cvt(SSLSetPeerID(self.0, peer_id.as_ptr().cast(), peer_id.len())) } + } + + /// Returns the peer ID of this session. + pub fn peer_id(&self) -> Result> { + unsafe { + let mut ptr = ptr::null(); + let mut len = 0; + cvt(SSLGetPeerID(self.0, &mut ptr, &mut len))?; + if ptr.is_null() { + Ok(None) + } else { + Ok(Some(slice::from_raw_parts(ptr.cast(), len))) + } + } + } + + /// Returns the list of ciphers that are supported by Secure Transport. + pub fn supported_ciphers(&self) -> Result> { + unsafe { + let mut num_ciphers = 0; + cvt(SSLGetNumberSupportedCiphers(self.0, &mut num_ciphers))?; + let mut ciphers = vec![0; num_ciphers]; + cvt(SSLGetSupportedCiphers( + self.0, + ciphers.as_mut_ptr(), + &mut num_ciphers, + ))?; + Ok(ciphers.iter().map(|c| CipherSuite::from_raw(*c)).collect()) + } + } + + /// Returns the list of ciphers that are eligible to be used for + /// negotiation. + pub fn enabled_ciphers(&self) -> Result> { + unsafe { + let mut num_ciphers = 0; + cvt(SSLGetNumberEnabledCiphers(self.0, &mut num_ciphers))?; + let mut ciphers = vec![0; num_ciphers]; + cvt(SSLGetEnabledCiphers( + self.0, + ciphers.as_mut_ptr(), + &mut num_ciphers, + ))?; + Ok(ciphers.iter().map(|c| CipherSuite::from_raw(*c)).collect()) + } + } + + /// Sets the list of ciphers that are eligible to be used for negotiation. + pub fn set_enabled_ciphers(&mut self, ciphers: &[CipherSuite]) -> Result<()> { + let ciphers = ciphers.iter().map(|c| c.to_raw()).collect::>(); + unsafe { + cvt(SSLSetEnabledCiphers( + self.0, + ciphers.as_ptr(), + ciphers.len(), + )) + } + } + + /// Returns the cipher being used by the session. + #[inline] + pub fn negotiated_cipher(&self) -> Result { + unsafe { + let mut cipher = 0; + cvt(SSLGetNegotiatedCipher(self.0, &mut cipher))?; + Ok(CipherSuite::from_raw(cipher)) + } + } + + /// Sets the requirements for client certificates. + /// + /// Should only be called on server-side sessions. + #[inline] + pub fn set_client_side_authenticate(&mut self, auth: SslAuthenticate) -> Result<()> { + unsafe { cvt(SSLSetClientSideAuthenticate(self.0, auth.0)) } + } + + /// Returns the state of client certificate processing. + #[inline] + pub fn client_certificate_state(&self) -> Result { + let mut state = 0; + + unsafe { + cvt(SSLGetClientCertificateState(self.0, &mut state))?; + } + Ok(SslClientCertificateState(state)) + } + + /// Returns the `SecTrust` object corresponding to the peer. + /// + /// This can be used in conjunction with `set_break_on_server_auth` to + /// validate certificates which do not have roots in the default set. + pub fn peer_trust2(&self) -> Result> { + // Calling SSLCopyPeerTrust on an idle connection does not seem to be well defined, + // so explicitly check for that + if self.state()? == SessionState::IDLE { + return Err(Error::from_code(errSecBadReq)); + } + + unsafe { + let mut trust = ptr::null_mut(); + cvt(SSLCopyPeerTrust(self.0, &mut trust))?; + if trust.is_null() { + Ok(None) + } else { + Ok(Some(SecTrust::wrap_under_create_rule(trust))) + } + } + } + + /// Returns the state of the session. + #[inline] + pub fn state(&self) -> Result { + unsafe { + let mut state = 0; + cvt(SSLGetSessionState(self.0, &mut state))?; + Ok(SessionState(state)) + } + } + + /// Returns the protocol version being used by the session. + #[inline] + pub fn negotiated_protocol_version(&self) -> Result { + unsafe { + let mut version = 0; + cvt(SSLGetNegotiatedProtocolVersion(self.0, &mut version))?; + Ok(SslProtocol(version)) + } + } + + /// Returns the maximum protocol version allowed by the session. + #[inline] + pub fn protocol_version_max(&self) -> Result { + unsafe { + let mut version = 0; + cvt(SSLGetProtocolVersionMax(self.0, &mut version))?; + Ok(SslProtocol(version)) + } + } + + /// Sets the maximum protocol version allowed by the session. + #[inline] + pub fn set_protocol_version_max(&mut self, max_version: SslProtocol) -> Result<()> { + unsafe { cvt(SSLSetProtocolVersionMax(self.0, max_version.0)) } + } + + /// Returns the minimum protocol version allowed by the session. + #[inline] + pub fn protocol_version_min(&self) -> Result { + unsafe { + let mut version = 0; + cvt(SSLGetProtocolVersionMin(self.0, &mut version))?; + Ok(SslProtocol(version)) + } + } + + /// Sets the minimum protocol version allowed by the session. + #[inline] + pub fn set_protocol_version_min(&mut self, min_version: SslProtocol) -> Result<()> { + unsafe { cvt(SSLSetProtocolVersionMin(self.0, min_version.0)) } + } + + /// Returns the set of protocols selected via ALPN if it succeeded. + #[cfg(feature = "alpn")] + pub fn alpn_protocols(&self) -> Result> { + let mut array: CFArrayRef = ptr::null(); + unsafe { + #[cfg(feature = "OSX_10_13")] + { + cvt(SSLCopyALPNProtocols(self.0, &mut array))?; + } + + #[cfg(not(feature = "OSX_10_13"))] + { + dlsym! { fn SSLCopyALPNProtocols(SSLContextRef, *mut CFArrayRef) -> OSStatus } + if let Some(f) = SSLCopyALPNProtocols.get() { + cvt(f(self.0, &mut array))?; + } else { + return Err(Error::from_code(errSecUnimplemented)); + } + } + + if array.is_null() { + return Ok(vec![]); + } + + let array = CFArray::::wrap_under_create_rule(array); + Ok(array.into_iter().map(|p| p.to_string()).collect()) + } + } + + /// Configures the set of protocols use for ALPN. + /// + /// This is only used for client-side connections. + #[cfg(feature = "alpn")] + pub fn set_alpn_protocols(&mut self, protocols: &[&str]) -> Result<()> { + // When CFMutableArray is added to core-foundation and IntoIterator trait + // is implemented for CFMutableArray, the code below should directly collect + // into a CFMutableArray. + let protocols = CFArray::from_CFTypes( + &protocols + .iter() + .map(|proto| CFString::new(proto)) + .collect::>(), + ); + + #[cfg(feature = "OSX_10_13")] + { + unsafe { cvt(SSLSetALPNProtocols(self.0, protocols.as_concrete_TypeRef())) } + } + #[cfg(not(feature = "OSX_10_13"))] + { + dlsym! { fn SSLSetALPNProtocols(SSLContextRef, CFArrayRef) -> OSStatus } + if let Some(f) = SSLSetALPNProtocols.get() { + unsafe { cvt(f(self.0, protocols.as_concrete_TypeRef())) } + } else { + Err(Error::from_code(errSecUnimplemented)) + } + } + } + + /// Sets whether the client sends the `SessionTicket` extension in its `ClientHello`. + /// + /// On its own, this will just cause the client to send an empty `SessionTicket` extension on + /// every connection. [`SslContext::set_peer_id`] must also be used to key the session + /// ticket returned by the server. + /// + /// [`SslContext::set_peer_id`]: #method.set_peer_id + #[cfg(feature = "session-tickets")] + pub fn set_session_tickets_enabled(&mut self, enabled: bool) -> Result<()> { + #[cfg(feature = "OSX_10_13")] + { + unsafe { cvt(SSLSetSessionTicketsEnabled(self.0, enabled as Boolean)) } + } + #[cfg(not(feature = "OSX_10_13"))] + { + dlsym! { fn SSLSetSessionTicketsEnabled(SSLContextRef, Boolean) -> OSStatus } + if let Some(f) = SSLSetSessionTicketsEnabled.get() { + unsafe { cvt(f(self.0, enabled as Boolean)) } + } else { + Err(Error::from_code(errSecUnimplemented)) + } + } + } + + /// Sets whether a protocol is enabled or not. + /// + /// # Note + /// + /// On OSX this is a deprecated API in favor of `set_protocol_version_max` and + /// `set_protocol_version_min`, although if you're working with OSX 10.8 or before you may have + /// to use this API instead. + #[cfg(target_os = "macos")] + #[deprecated(note = "use `set_protocol_version_max`")] + pub fn set_protocol_version_enabled( + &mut self, + protocol: SslProtocol, + enabled: bool, + ) -> Result<()> { + unsafe { + cvt(SSLSetProtocolVersionEnabled( + self.0, + protocol.0, + enabled as Boolean, + )) + } + } + + /// Returns the number of bytes which can be read without triggering a + /// `read` call in the underlying stream. + #[inline] + pub fn buffered_read_size(&self) -> Result { + unsafe { + let mut size = 0; + cvt(SSLGetBufferedReadSize(self.0, &mut size))?; + Ok(size) + } + } + + impl_options! { + /// If enabled, the handshake process will pause and return instead of + /// automatically validating a server's certificate. + const kSSLSessionOptionBreakOnServerAuth: break_on_server_auth & set_break_on_server_auth, + /// If enabled, the handshake process will pause and return after + /// the server requests a certificate from the client. + const kSSLSessionOptionBreakOnCertRequested: break_on_cert_requested & set_break_on_cert_requested, + /// If enabled, the handshake process will pause and return instead of + /// automatically validating a client's certificate. + const kSSLSessionOptionBreakOnClientAuth: break_on_client_auth & set_break_on_client_auth, + /// If enabled, TLS false start will be performed if an appropriate + /// cipher suite is negotiated. + /// + /// Requires the `OSX_10_9` (or greater) feature. + #[cfg(feature = "OSX_10_9")] + const kSSLSessionOptionFalseStart: false_start & set_false_start, + /// If enabled, 1/n-1 record splitting will be enabled for TLS 1.0 + /// connections using block ciphers to mitigate the BEAST attack. + /// + /// Requires the `OSX_10_9` (or greater) feature. + #[cfg(feature = "OSX_10_9")] + const kSSLSessionOptionSendOneByteRecord: send_one_byte_record & set_send_one_byte_record, + } + + fn into_stream(self, stream: S) -> Result> + where + S: Read + Write, + { + unsafe { + let ret = SSLSetIOFuncs(self.0, read_func::, write_func::); + if ret != errSecSuccess { + return Err(Error::from_code(ret)); + } + + let stream = Connection { + stream, + err: None, + panic: None, + }; + let stream = Box::into_raw(Box::new(stream)); + let ret = SSLSetConnection(self.0, stream.cast()); + if ret != errSecSuccess { + let _conn = Box::from_raw(stream); + return Err(Error::from_code(ret)); + } + + Ok(SslStream { + ctx: self, + _m: PhantomData, + }) + } + } + + /// Performs the SSL/TLS handshake. + pub fn handshake(self, stream: S) -> result::Result, HandshakeError> + where + S: Read + Write, + { + self.into_stream(stream) + .map_err(HandshakeError::Failure) + .and_then(SslStream::handshake) + } +} + +struct Connection { + stream: S, + err: Option, + panic: Option>, +} + +// the logic here is based off of libcurl's +#[cold] +fn translate_err(e: &io::Error) -> OSStatus { + match e.kind() { + io::ErrorKind::NotFound => errSSLClosedGraceful, + io::ErrorKind::ConnectionReset => errSSLClosedAbort, + io::ErrorKind::WouldBlock | + io::ErrorKind::NotConnected => errSSLWouldBlock, + _ => errSecIO, + } +} + +unsafe extern "C" fn read_func( + connection: SSLConnectionRef, + data: *mut c_void, + data_length: *mut usize, +) -> OSStatus +where + S: Read, +{ + let conn: &mut Connection = &mut *(connection as *mut _); + let data = slice::from_raw_parts_mut(data.cast::(), *data_length); + let mut start = 0; + let mut ret = errSecSuccess; + + while start < data.len() { + match panic::catch_unwind(AssertUnwindSafe(|| conn.stream.read(&mut data[start..]))) { + Ok(Ok(0)) => { + ret = errSSLClosedNoNotify; + break; + } + Ok(Ok(len)) => start += len, + Ok(Err(e)) => { + ret = translate_err(&e); + conn.err = Some(e); + break; + } + Err(e) => { + ret = errSecIO; + conn.panic = Some(e); + break; + } + } + } + + *data_length = start; + ret +} + +unsafe extern "C" fn write_func( + connection: SSLConnectionRef, + data: *const c_void, + data_length: *mut usize, +) -> OSStatus +where + S: Write, +{ + let conn: &mut Connection = &mut *(connection as *mut _); + let data = slice::from_raw_parts(data as *mut u8, *data_length); + let mut start = 0; + let mut ret = errSecSuccess; + + while start < data.len() { + match panic::catch_unwind(AssertUnwindSafe(|| conn.stream.write(&data[start..]))) { + Ok(Ok(0)) => { + ret = errSSLClosedNoNotify; + break; + } + Ok(Ok(len)) => start += len, + Ok(Err(e)) => { + ret = translate_err(&e); + conn.err = Some(e); + break; + } + Err(e) => { + ret = errSecIO; + conn.panic = Some(e); + break; + } + } + } + + *data_length = start; + ret +} + +/// A type implementing SSL/TLS encryption over an underlying stream. +pub struct SslStream { + ctx: SslContext, + _m: PhantomData, +} + +impl fmt::Debug for SslStream { + #[cold] + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.debug_struct("SslStream") + .field("context", &self.ctx) + .field("stream", self.get_ref()) + .finish() + } +} + +impl Drop for SslStream { + fn drop(&mut self) { + unsafe { + let mut conn = ptr::null(); + let ret = SSLGetConnection(self.ctx.0, &mut conn); + assert!(ret == errSecSuccess); + let _ = Box::>::from_raw(conn as *mut _); + } + } +} + +impl SslStream { + fn handshake(mut self) -> result::Result> { + match unsafe { SSLHandshake(self.ctx.0) } { + errSecSuccess => Ok(self), + reason @ errSSLPeerAuthCompleted + | reason @ errSSLClientCertRequested + | reason @ errSSLWouldBlock + | reason @ errSSLClientHelloReceived => { + Err(HandshakeError::Interrupted(MidHandshakeSslStream { + stream: self, + error: Error::from_code(reason), + })) + } + err => { + self.check_panic(); + Err(HandshakeError::Failure(Error::from_code(err))) + } + } + } + + /// Returns a shared reference to the inner stream. + #[inline(always)] + #[must_use] + pub fn get_ref(&self) -> &S { + &self.connection().stream + } + + /// Returns a mutable reference to the underlying stream. + #[inline(always)] + pub fn get_mut(&mut self) -> &mut S { + &mut self.connection_mut().stream + } + + /// Returns a shared reference to the `SslContext` of the stream. + #[inline(always)] + #[must_use] + pub fn context(&self) -> &SslContext { + &self.ctx + } + + /// Returns a mutable reference to the `SslContext` of the stream. + #[inline(always)] + pub fn context_mut(&mut self) -> &mut SslContext { + &mut self.ctx + } + + /// Shuts down the connection. + pub fn close(&mut self) -> result::Result<(), io::Error> { + unsafe { + let ret = SSLClose(self.ctx.0); + if ret == errSecSuccess { + Ok(()) + } else { + Err(self.get_error(ret)) + } + } + } + + fn connection(&self) -> &Connection { + unsafe { + let mut conn = ptr::null(); + let ret = SSLGetConnection(self.ctx.0, &mut conn); + assert!(ret == errSecSuccess); + + &mut *(conn as *mut Connection) + } + } + + fn connection_mut(&mut self) -> &mut Connection { + unsafe { + let mut conn = ptr::null(); + let ret = SSLGetConnection(self.ctx.0, &mut conn); + assert!(ret == errSecSuccess); + + &mut *(conn as *mut Connection) + } + } + + #[cold] + fn check_panic(&mut self) { + let conn = self.connection_mut(); + if let Some(err) = conn.panic.take() { + panic::resume_unwind(err); + } + } + + #[cold] + fn get_error(&mut self, ret: OSStatus) -> io::Error { + self.check_panic(); + + if let Some(err) = self.connection_mut().err.take() { + err + } else { + io::Error::new(io::ErrorKind::Other, Error::from_code(ret)) + } + } +} + +impl Read for SslStream { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + // Below we base our return value off the amount of data read, so a + // zero-length buffer might cause us to erroneously interpret this + // request as an error. Instead short-circuit that logic and return + // `Ok(0)` instead. + if buf.is_empty() { + return Ok(0); + } + + // If some data was buffered but not enough to fill `buf`, SSLRead + // will try to read a new packet. This is bad because there may be + // no more data but the socket is remaining open (e.g HTTPS with + // Connection: keep-alive). + let buffered = self.context().buffered_read_size().unwrap_or(0); + let to_read = if buffered > 0 { + cmp::min(buffered, buf.len()) + } else { + buf.len() + }; + + unsafe { + let mut nread = 0; + let ret = SSLRead(self.ctx.0, buf.as_mut_ptr().cast(), to_read, &mut nread); + // SSLRead can return an error at the same time it returns the last + // chunk of data (!) + if nread > 0 { + return Ok(nread); + } + + match ret { + errSSLClosedGraceful | errSSLClosedAbort | errSSLClosedNoNotify => Ok(0), + // this error isn't fatal + errSSLPeerAuthCompleted => self.read(buf), + _ => Err(self.get_error(ret)), + } + } + } +} + +impl Write for SslStream { + fn write(&mut self, buf: &[u8]) -> io::Result { + // Like above in read, short circuit a 0-length write + if buf.is_empty() { + return Ok(0); + } + unsafe { + let mut nwritten = 0; + let ret = SSLWrite( + self.ctx.0, + buf.as_ptr().cast(), + buf.len(), + &mut nwritten, + ); + // just to be safe, base success off of nwritten rather than ret + // for the same reason as in read + if nwritten > 0 { + Ok(nwritten) + } else { + Err(self.get_error(ret)) + } + } + } + + fn flush(&mut self) -> io::Result<()> { + self.connection_mut().stream.flush() + } +} + +/// A builder type to simplify the creation of client side `SslStream`s. +#[derive(Debug)] +pub struct ClientBuilder { + identity: Option, + certs: Vec, + chain: Vec, + protocol_min: Option, + protocol_max: Option, + trust_certs_only: bool, + use_sni: bool, + danger_accept_invalid_certs: bool, + danger_accept_invalid_hostnames: bool, + whitelisted_ciphers: Vec, + blacklisted_ciphers: Vec, + #[cfg(feature = "alpn")] + alpn: Option>, + #[cfg(feature = "session-tickets")] + enable_session_tickets: bool, +} + +impl Default for ClientBuilder { + #[inline(always)] + fn default() -> Self { + Self::new() + } +} + +impl ClientBuilder { + /// Creates a new builder with default options. + #[inline] + #[must_use] + pub fn new() -> Self { + Self { + identity: None, + certs: Vec::new(), + chain: Vec::new(), + protocol_min: None, + protocol_max: None, + trust_certs_only: false, + use_sni: true, + danger_accept_invalid_certs: false, + danger_accept_invalid_hostnames: false, + whitelisted_ciphers: Vec::new(), + blacklisted_ciphers: Vec::new(), + #[cfg(feature = "alpn")] + alpn: None, + #[cfg(feature = "session-tickets")] + enable_session_tickets: false, + } + } + + /// Specifies the set of root certificates to trust when + /// verifying the server's certificate. + #[inline] + pub fn anchor_certificates(&mut self, certs: &[SecCertificate]) -> &mut Self { + self.certs = certs.to_owned(); + self + } + + /// Add the certificate the set of root certificates to trust + /// when verifying the server's certificate. + #[inline] + pub fn add_anchor_certificate(&mut self, certs: &SecCertificate) -> &mut Self { + self.certs.push(certs.to_owned()); + self + } + + /// Specifies whether to trust the built-in certificates in addition + /// to specified anchor certificates. + #[inline(always)] + pub fn trust_anchor_certificates_only(&mut self, only: bool) -> &mut Self { + self.trust_certs_only = only; + self + } + + /// Specifies whether to trust invalid certificates. + /// + /// # Warning + /// + /// You should think very carefully before using this method. If invalid + /// certificates are trusted, *any* certificate for *any* site will be + /// trusted for use. This includes expired certificates. This introduces + /// significant vulnerabilities, and should only be used as a last resort. + #[inline(always)] + pub fn danger_accept_invalid_certs(&mut self, noverify: bool) -> &mut Self { + self.danger_accept_invalid_certs = noverify; + self + } + + /// Specifies whether to use Server Name Indication (SNI). + #[inline(always)] + pub fn use_sni(&mut self, use_sni: bool) -> &mut Self { + self.use_sni = use_sni; + self + } + + /// Specifies whether to verify that the server's hostname matches its certificate. + /// + /// # Warning + /// + /// You should think very carefully before using this method. If hostnames are not verified, + /// *any* valid certificate for *any* site will be trusted for use. This introduces significant + /// vulnerabilities, and should only be used as a last resort. + #[inline(always)] + pub fn danger_accept_invalid_hostnames( + &mut self, + danger_accept_invalid_hostnames: bool, + ) -> &mut Self { + self.danger_accept_invalid_hostnames = danger_accept_invalid_hostnames; + self + } + + /// Set a whitelist of enabled ciphers. Any ciphers not whitelisted will be disabled. + pub fn whitelist_ciphers(&mut self, whitelisted_ciphers: &[CipherSuite]) -> &mut Self { + self.whitelisted_ciphers = whitelisted_ciphers.to_owned(); + self + } + + /// Set a blacklist of disabled ciphers. Blacklisted ciphers will be disabled. + pub fn blacklist_ciphers(&mut self, blacklisted_ciphers: &[CipherSuite]) -> &mut Self { + self.blacklisted_ciphers = blacklisted_ciphers.to_owned(); + self + } + + /// Use the specified identity as a SSL/TLS client certificate. + pub fn identity(&mut self, identity: &SecIdentity, chain: &[SecCertificate]) -> &mut Self { + self.identity = Some(identity.clone()); + self.chain = chain.to_owned(); + self + } + + /// Configure the minimum protocol that this client will support. + #[inline(always)] + pub fn protocol_min(&mut self, min: SslProtocol) -> &mut Self { + self.protocol_min = Some(min); + self + } + + /// Configure the minimum protocol that this client will support. + #[inline(always)] + pub fn protocol_max(&mut self, max: SslProtocol) -> &mut Self { + self.protocol_max = Some(max); + self + } + + /// Configures the set of protocols used for ALPN. + #[cfg(feature = "alpn")] + pub fn alpn_protocols(&mut self, protocols: &[&str]) -> &mut Self { + self.alpn = Some(protocols.iter().map(|s| s.to_string()).collect()); + self + } + + /// Configures the use of the RFC 5077 `SessionTicket` extension. + /// + /// Defaults to `false`. + #[cfg(feature = "session-tickets")] + #[inline(always)] + pub fn enable_session_tickets(&mut self, enable: bool) -> &mut Self { + self.enable_session_tickets = enable; + self + } + + /// Initiates a new SSL/TLS session over a stream connected to the specified domain. + /// + /// If both SNI and hostname verification are disabled, the value of `domain` will be ignored. + pub fn handshake( + &self, + domain: &str, + stream: S, + ) -> result::Result, ClientHandshakeError> + where + S: Read + Write, + { + // the logic for trust validation is in MidHandshakeClientBuilder::connect, so run all + // of the handshake logic through that. + let stream = MidHandshakeSslStream { + stream: self.ctx_into_stream(domain, stream)?, + error: Error::from(errSecSuccess), + }; + + let certs = self.certs.clone(); + let stream = MidHandshakeClientBuilder { + stream, + domain: if self.danger_accept_invalid_hostnames { + None + } else { + Some(domain.to_string()) + }, + certs, + trust_certs_only: self.trust_certs_only, + danger_accept_invalid_certs: self.danger_accept_invalid_certs, + }; + stream.handshake() + } + + fn ctx_into_stream(&self, domain: &str, stream: S) -> Result> + where + S: Read + Write, + { + let mut ctx = SslContext::new(SslProtocolSide::CLIENT, SslConnectionType::STREAM)?; + + if self.use_sni { + ctx.set_peer_domain_name(domain)?; + } + if let Some(ref identity) = self.identity { + ctx.set_certificate(identity, &self.chain)?; + } + #[cfg(feature = "alpn")] + { + if let Some(ref alpn) = self.alpn { + ctx.set_alpn_protocols(&alpn.iter().map(|s| &**s).collect::>())?; + } + } + #[cfg(feature = "session-tickets")] + { + if self.enable_session_tickets { + // We must use the domain here to ensure that we go through certificate validation + // again rather than resuming the session if the domain changes. + ctx.set_peer_id(domain.as_bytes())?; + ctx.set_session_tickets_enabled(true)?; + } + } + ctx.set_break_on_server_auth(true)?; + self.configure_protocols(&mut ctx)?; + self.configure_ciphers(&mut ctx)?; + + ctx.into_stream(stream) + } + + fn configure_protocols(&self, ctx: &mut SslContext) -> Result<()> { + if let Some(min) = self.protocol_min { + ctx.set_protocol_version_min(min)?; + } + if let Some(max) = self.protocol_max { + ctx.set_protocol_version_max(max)?; + } + Ok(()) + } + + fn configure_ciphers(&self, ctx: &mut SslContext) -> Result<()> { + let mut ciphers = if self.whitelisted_ciphers.is_empty() { + ctx.enabled_ciphers()? + } else { + self.whitelisted_ciphers.clone() + }; + + if !self.blacklisted_ciphers.is_empty() { + ciphers.retain(|cipher| !self.blacklisted_ciphers.contains(cipher)); + } + + ctx.set_enabled_ciphers(&ciphers)?; + Ok(()) + } +} + +/// A builder type to simplify the creation of server-side `SslStream`s. +#[derive(Debug)] +pub struct ServerBuilder { + identity: SecIdentity, + certs: Vec, +} + +impl ServerBuilder { + /// Creates a new `ServerBuilder` which will use the specified identity + /// and certificate chain for handshakes. + #[must_use] + pub fn new(identity: &SecIdentity, certs: &[SecCertificate]) -> Self { + Self { + identity: identity.clone(), + certs: certs.to_owned(), + } + } + + /// Creates a new `ServerBuilder` which will use the identity + /// from the given PKCS #12 data. + /// + /// This operation fails if PKCS #12 file contains zero or more than one identity. + /// + /// This is a shortcut for the most common operation. + pub fn from_pkcs12(pkcs12_der: &[u8], passphrase: &str) -> Result { + let mut identities: Vec<(SecIdentity, Vec)> = Pkcs12ImportOptions::new() + .passphrase(passphrase) + .import(pkcs12_der)? + .into_iter() + .filter_map(|idendity| { + let certs = idendity.cert_chain.unwrap_or_default(); + idendity.identity.map(|identity| (identity, certs)) + }) + .collect(); + if identities.len() == 1 { + let (identity, certs) = identities.pop().unwrap(); + Ok(ServerBuilder::new(&identity, &certs)) + } else { + // This error code is not really helpful + Err(Error::from_code(errSecParam)) + } + } + + /// Create a SSL context for lower-level stream initialization. + pub fn new_ssl_context(&self) -> Result { + let mut ctx = SslContext::new(SslProtocolSide::SERVER, SslConnectionType::STREAM)?; + ctx.set_certificate(&self.identity, &self.certs)?; + Ok(ctx) + } + + /// Initiates a new SSL/TLS session over a stream. + pub fn handshake(&self, stream: S) -> Result> + where + S: Read + Write, + { + match self.new_ssl_context()?.handshake(stream) { + Ok(stream) => Ok(stream), + Err(HandshakeError::Interrupted(stream)) => Err(*stream.error()), + Err(HandshakeError::Failure(err)) => Err(err), + } + } +} + +#[cfg(test)] +mod test { + use std::io; + use std::io::prelude::*; + use std::net::TcpStream; + + use super::*; + + #[test] + fn server_builder_from_pkcs12() { + let pkcs12_der = include_bytes!("../test/server.p12"); + ServerBuilder::from_pkcs12(pkcs12_der, "password123").unwrap(); + } + + #[test] + fn connect() { + let mut ctx = p!(SslContext::new( + SslProtocolSide::CLIENT, + SslConnectionType::STREAM + )); + p!(ctx.set_peer_domain_name("google.com")); + let stream = p!(TcpStream::connect("google.com:443")); + p!(ctx.handshake(stream)); + } + + #[test] + fn connect_bad_domain() { + let mut ctx = p!(SslContext::new( + SslProtocolSide::CLIENT, + SslConnectionType::STREAM + )); + p!(ctx.set_peer_domain_name("foobar.com")); + let stream = p!(TcpStream::connect("google.com:443")); + match ctx.handshake(stream) { + Ok(_) => panic!("expected failure"), + Err(_) => {} + } + } + + #[test] + fn load_page() { + let mut ctx = p!(SslContext::new( + SslProtocolSide::CLIENT, + SslConnectionType::STREAM + )); + p!(ctx.set_peer_domain_name("google.com")); + let stream = p!(TcpStream::connect("google.com:443")); + let mut stream = p!(ctx.handshake(stream)); + p!(stream.write_all(b"GET / HTTP/1.0\r\n\r\n")); + p!(stream.flush()); + let mut buf = vec![]; + p!(stream.read_to_end(&mut buf)); + println!("{}", String::from_utf8_lossy(&buf)); + } + + #[test] + fn client_no_session_ticket_resumption() { + for _ in 0..2 { + let stream = p!(TcpStream::connect("google.com:443")); + + // Manually handshake here. + let stream = MidHandshakeSslStream { + stream: ClientBuilder::new() + .ctx_into_stream("google.com", stream) + .unwrap(), + error: Error::from(errSecSuccess), + }; + + let mut result = stream.handshake(); + + if let Err(HandshakeError::Interrupted(stream)) = result { + assert!(stream.server_auth_completed()); + result = stream.handshake(); + } else { + panic!("Unexpectedly skipped server auth"); + } + + assert!(result.is_ok()); + } + } + + #[test] + #[cfg(feature = "session-tickets")] + fn client_session_ticket_resumption() { + // The first time through this loop, we should do a full handshake. The second time, we + // should immediately finish the handshake without breaking on server auth. + for i in 0..2 { + let stream = p!(TcpStream::connect("google.com:443")); + let mut builder = ClientBuilder::new(); + builder.enable_session_tickets(true); + + // Manually handshake here. + let stream = MidHandshakeSslStream { + stream: builder.ctx_into_stream("google.com", stream).unwrap(), + error: Error::from(errSecSuccess), + }; + + let mut result = stream.handshake(); + + if let Err(HandshakeError::Interrupted(stream)) = result { + assert!(stream.server_auth_completed()); + assert_eq!( + i, 0, + "Session ticket resumption did not work, server auth was not skipped" + ); + result = stream.handshake(); + } else { + assert_eq!(i, 1, "Unexpectedly skipped server auth"); + } + + assert!(result.is_ok()); + } + } + + #[test] + #[cfg(feature = "alpn")] + fn client_alpn_accept() { + let mut ctx = p!(SslContext::new( + SslProtocolSide::CLIENT, + SslConnectionType::STREAM + )); + p!(ctx.set_peer_domain_name("google.com")); + p!(ctx.set_alpn_protocols(&vec!["h2"])); + let stream = p!(TcpStream::connect("google.com:443")); + let stream = ctx.handshake(stream).unwrap(); + assert_eq!(vec!["h2"], stream.context().alpn_protocols().unwrap()); + } + + #[test] + #[cfg(feature = "alpn")] + fn client_alpn_reject() { + let mut ctx = p!(SslContext::new( + SslProtocolSide::CLIENT, + SslConnectionType::STREAM + )); + p!(ctx.set_peer_domain_name("google.com")); + p!(ctx.set_alpn_protocols(&vec!["h2c"])); + let stream = p!(TcpStream::connect("google.com:443")); + let stream = ctx.handshake(stream).unwrap(); + assert!(stream.context().alpn_protocols().is_err()); + } + + #[test] + fn client_no_anchor_certs() { + let stream = p!(TcpStream::connect("google.com:443")); + assert!(ClientBuilder::new() + .trust_anchor_certificates_only(true) + .handshake("google.com", stream) + .is_err()); + } + + #[test] + fn client_bad_domain() { + let stream = p!(TcpStream::connect("google.com:443")); + assert!(ClientBuilder::new() + .handshake("foobar.com", stream) + .is_err()); + } + + #[test] + fn client_bad_domain_ignored() { + let stream = p!(TcpStream::connect("google.com:443")); + ClientBuilder::new() + .danger_accept_invalid_hostnames(true) + .handshake("foobar.com", stream) + .unwrap(); + } + + #[test] + fn connect_no_verify_ssl() { + let stream = p!(TcpStream::connect("expired.badssl.com:443")); + let mut builder = ClientBuilder::new(); + builder.danger_accept_invalid_certs(true); + builder.handshake("expired.badssl.com", stream).unwrap(); + } + + #[test] + fn load_page_client() { + let stream = p!(TcpStream::connect("google.com:443")); + let mut stream = p!(ClientBuilder::new().handshake("google.com", stream)); + p!(stream.write_all(b"GET / HTTP/1.0\r\n\r\n")); + p!(stream.flush()); + let mut buf = vec![]; + p!(stream.read_to_end(&mut buf)); + println!("{}", String::from_utf8_lossy(&buf)); + } + + #[test] + #[cfg_attr(target_os = "ios", ignore)] // FIXME what's going on with ios? + fn cipher_configuration() { + let mut ctx = p!(SslContext::new( + SslProtocolSide::SERVER, + SslConnectionType::STREAM + )); + let ciphers = p!(ctx.enabled_ciphers()); + let ciphers = ciphers + .iter() + .enumerate() + .filter_map(|(i, c)| if i % 2 == 0 { Some(*c) } else { None }) + .collect::>(); + p!(ctx.set_enabled_ciphers(&ciphers)); + assert_eq!(ciphers, p!(ctx.enabled_ciphers())); + } + + #[test] + fn test_builder_whitelist_ciphers() { + let stream = p!(TcpStream::connect("google.com:443")); + + let ctx = p!(SslContext::new( + SslProtocolSide::CLIENT, + SslConnectionType::STREAM + )); + assert!(p!(ctx.enabled_ciphers()).len() > 1); + + let ciphers = p!(ctx.enabled_ciphers()); + let cipher = ciphers.first().unwrap(); + let stream = p!(ClientBuilder::new() + .whitelist_ciphers(&[*cipher]) + .ctx_into_stream("google.com", stream)); + + assert_eq!(1, p!(stream.context().enabled_ciphers()).len()); + } + + #[test] + #[cfg_attr(target_os = "ios", ignore)] // FIXME same issue as cipher_configuration + fn test_builder_blacklist_ciphers() { + let stream = p!(TcpStream::connect("google.com:443")); + + let ctx = p!(SslContext::new( + SslProtocolSide::CLIENT, + SslConnectionType::STREAM + )); + let num = p!(ctx.enabled_ciphers()).len(); + assert!(num > 1); + + let ciphers = p!(ctx.enabled_ciphers()); + let cipher = ciphers.first().unwrap(); + let stream = p!(ClientBuilder::new() + .blacklist_ciphers(&[*cipher]) + .ctx_into_stream("google.com", stream)); + + assert_eq!(num - 1, p!(stream.context().enabled_ciphers()).len()); + } + + #[test] + fn idle_context_peer_trust() { + let ctx = p!(SslContext::new( + SslProtocolSide::SERVER, + SslConnectionType::STREAM + )); + assert!(ctx.peer_trust2().is_err()); + } + + #[test] + fn peer_id() { + let mut ctx = p!(SslContext::new( + SslProtocolSide::SERVER, + SslConnectionType::STREAM + )); + assert!(p!(ctx.peer_id()).is_none()); + p!(ctx.set_peer_id(b"foobar")); + assert_eq!(p!(ctx.peer_id()), Some(&b"foobar"[..])); + } + + #[test] + fn peer_domain_name() { + let mut ctx = p!(SslContext::new( + SslProtocolSide::CLIENT, + SslConnectionType::STREAM + )); + assert_eq!("", p!(ctx.peer_domain_name())); + p!(ctx.set_peer_domain_name("foobar.com")); + assert_eq!("foobar.com", p!(ctx.peer_domain_name())); + } + + #[test] + #[should_panic(expected = "blammo")] + fn write_panic() { + struct ExplodingStream(TcpStream); + + impl Read for ExplodingStream { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.0.read(buf) + } + } + + impl Write for ExplodingStream { + fn write(&mut self, _: &[u8]) -> io::Result { + panic!("blammo"); + } + + fn flush(&mut self) -> io::Result<()> { + self.0.flush() + } + } + + let mut ctx = p!(SslContext::new( + SslProtocolSide::CLIENT, + SslConnectionType::STREAM + )); + p!(ctx.set_peer_domain_name("google.com")); + let stream = p!(TcpStream::connect("google.com:443")); + let _ = ctx.handshake(ExplodingStream(stream)); + } + + #[test] + #[should_panic(expected = "blammo")] + fn read_panic() { + struct ExplodingStream(TcpStream); + + impl Read for ExplodingStream { + fn read(&mut self, _: &mut [u8]) -> io::Result { + panic!("blammo"); + } + } + + impl Write for ExplodingStream { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.0.write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + self.0.flush() + } + } + + let mut ctx = p!(SslContext::new( + SslProtocolSide::CLIENT, + SslConnectionType::STREAM + )); + p!(ctx.set_peer_domain_name("google.com")); + let stream = p!(TcpStream::connect("google.com:443")); + let _ = ctx.handshake(ExplodingStream(stream)); + } + + #[test] + fn zero_length_buffers() { + let mut ctx = p!(SslContext::new( + SslProtocolSide::CLIENT, + SslConnectionType::STREAM + )); + p!(ctx.set_peer_domain_name("google.com")); + let stream = p!(TcpStream::connect("google.com:443")); + let mut stream = ctx.handshake(stream).unwrap(); + assert_eq!(stream.write(b"").unwrap(), 0); + assert_eq!(stream.read(&mut []).unwrap(), 0); + } +} diff --git a/vendor/security-framework/src/trust.rs b/vendor/security-framework/src/trust.rs new file mode 100644 index 000000000..fcf19a733 --- /dev/null +++ b/vendor/security-framework/src/trust.rs @@ -0,0 +1,396 @@ +//! Trust evaluation support. + +use core_foundation::array::CFArray; +#[cfg(target_os = "macos")] +use core_foundation::array::CFArrayRef; +use core_foundation::base::TCFType; +#[cfg(any(feature = "OSX_10_9", target_os = "ios"))] +use core_foundation::data::CFData; +use core_foundation::date::CFDate; +use core_foundation_sys::base::{Boolean, CFIndex}; + +use security_framework_sys::trust::*; +use std::ptr; + +use crate::base::Result; +use crate::certificate::SecCertificate; +use crate::cvt; +use crate::key::SecKey; +use crate::policy::SecPolicy; +use core_foundation::error::{CFError, CFErrorRef}; + +/// The result of trust evaluation. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct TrustResult(SecTrustResultType); + +impl TrustResult { + /// An invalid setting or result. + pub const INVALID: Self = Self(kSecTrustResultInvalid); + + /// You may proceed. + pub const PROCEED: Self = Self(kSecTrustResultProceed); + + /// Indicates a denial by the user, do not proceed. + pub const DENY: Self = Self(kSecTrustResultDeny); + + /// The certificate is implicitly trusted. + pub const UNSPECIFIED: Self = Self(kSecTrustResultUnspecified); + + /// Indicates a trust policy failure that the user can override. + pub const RECOVERABLE_TRUST_FAILURE: Self = Self(kSecTrustResultRecoverableTrustFailure); + + /// Indicates a trust policy failure that the user cannot override. + pub const FATAL_TRUST_FAILURE: Self = Self(kSecTrustResultFatalTrustFailure); + + /// An error not related to trust validation. + pub const OTHER_ERROR: Self = Self(kSecTrustResultOtherError); +} + +impl TrustResult { + /// Returns true if the result is "successful" - specifically `PROCEED` or `UNSPECIFIED`. + #[inline] + #[must_use] + pub fn success(self) -> bool { + matches!(self, Self::PROCEED | Self::UNSPECIFIED) + } +} + +declare_TCFType! { + /// A type representing a trust evaluation for a certificate. + SecTrust, SecTrustRef +} +impl_TCFType!(SecTrust, SecTrustRef, SecTrustGetTypeID); + +unsafe impl Sync for SecTrust {} +unsafe impl Send for SecTrust {} + +#[cfg(target_os = "macos")] +bitflags::bitflags! { + /// The option flags used to configure the evaluation of a `SecTrust`. + pub struct TrustOptions: SecTrustOptionFlags { + /// Allow expired certificates (except for the root certificate). + const ALLOW_EXPIRED = kSecTrustOptionAllowExpired; + /// Allow CA certificates as leaf certificates. + const LEAF_IS_CA = kSecTrustOptionLeafIsCA; + /// Allow network downloads of CA certificates. + const FETCH_ISSUER_FROM_NET = kSecTrustOptionFetchIssuerFromNet; + /// Allow expired root certificates. + const ALLOW_EXPIRED_ROOT = kSecTrustOptionAllowExpiredRoot; + /// Require a positive revocation check for each certificate. + const REQUIRE_REVOCATION_PER_CERT = kSecTrustOptionRequireRevPerCert; + /// Use TrustSettings instead of anchors. + const USE_TRUST_SETTINGS = kSecTrustOptionUseTrustSettings; + /// Treat properly self-signed certificates as anchors implicitly. + const IMPLICIT_ANCHORS = kSecTrustOptionImplicitAnchors; + } +} + +impl SecTrust { + /// Creates a `SecTrustRef` that is configured with a certificate chain, for validating + /// that chain against a collection of policies. + pub fn create_with_certificates( + certs: &[SecCertificate], + policies: &[SecPolicy], + ) -> Result { + let cert_array = CFArray::from_CFTypes(certs); + let policy_array = CFArray::from_CFTypes(policies); + let mut trust = ptr::null_mut(); + unsafe { + cvt(SecTrustCreateWithCertificates( + cert_array.as_CFTypeRef(), + policy_array.as_CFTypeRef(), + &mut trust, + ))?; + Ok(Self(trust)) + } + } + + /// Sets the date and time against which the certificates in this trust object + /// are verified. + #[inline] + pub fn set_trust_verify_date(&mut self, date: &CFDate) -> Result<()> { + unsafe { cvt(SecTrustSetVerifyDate(self.0, date.as_concrete_TypeRef())) } + } + + /// Sets additional anchor certificates used to validate trust. + pub fn set_anchor_certificates(&mut self, certs: &[SecCertificate]) -> Result<()> { + let certs = CFArray::from_CFTypes(certs); + + unsafe { + cvt(SecTrustSetAnchorCertificates( + self.0, + certs.as_concrete_TypeRef(), + )) + } + } + + /// Retrieves the anchor (root) certificates stored by macOS + #[cfg(target_os = "macos")] + pub fn copy_anchor_certificates() -> Result> { + let mut array: CFArrayRef = ptr::null(); + + unsafe { + cvt(SecTrustCopyAnchorCertificates(&mut array))?; + } + + if array.is_null() { + return Ok(vec![]); + } + + let array = unsafe { CFArray::::wrap_under_create_rule(array) }; + Ok(array.into_iter().map(|c| c.clone()).collect()) + } + + /// If set to `true`, only the certificates specified by + /// `set_anchor_certificates` will be trusted, but not globally trusted + /// certificates. + #[inline] + pub fn set_trust_anchor_certificates_only(&mut self, only: bool) -> Result<()> { + unsafe { cvt(SecTrustSetAnchorCertificatesOnly(self.0, only as Boolean)) } + } + + /// Sets the policy used to evaluate trust. + #[inline] + pub fn set_policy(&mut self, policy: &SecPolicy) -> Result<()> { + unsafe { cvt(SecTrustSetPolicies(self.0, policy.as_CFTypeRef())) } + } + + /// Sets option flags for customizing evaluation of a trust object. + #[cfg(target_os = "macos")] + #[inline] + pub fn set_options(&mut self, options: TrustOptions) -> Result<()> { + unsafe { cvt(SecTrustSetOptions(self.0, options.bits())) } + } + + /// Indicates whether this trust object is permitted to + /// fetch missing intermediate certificates from the network. + #[cfg(any(feature = "OSX_10_9", target_os = "ios"))] + pub fn get_network_fetch_allowed(&mut self) -> Result { + let mut allowed = 0; + + unsafe { cvt(SecTrustGetNetworkFetchAllowed(self.0, &mut allowed))? }; + + Ok(allowed != 0) + } + + /// Specifies whether this trust object is permitted to + /// fetch missing intermediate certificates from the network. + #[cfg(any(feature = "OSX_10_9", target_os = "ios"))] + #[inline] + pub fn set_network_fetch_allowed(&mut self, allowed: bool) -> Result<()> { + unsafe { cvt(SecTrustSetNetworkFetchAllowed(self.0, allowed as u8)) } + } + + /// Attaches Online Certificate Status Protocol (OSCP) response data + /// to this trust object. + #[cfg(any(feature = "OSX_10_9", target_os = "ios"))] + pub fn set_trust_ocsp_response>>( + &mut self, + ocsp_response: I, + ) -> Result<()> { + let response: Vec = ocsp_response + .into_iter() + .map(|bytes| CFData::from_buffer(bytes.as_ref())) + .collect(); + + let response = CFArray::from_CFTypes(&response); + + unsafe { cvt(SecTrustSetOCSPResponse(self.0, response.as_CFTypeRef())) } + } + + /// Attaches signed certificate timestamp data to this trust object. + #[cfg(any(feature = "OSX_10_14", target_os = "ios"))] + pub fn set_signed_certificate_timestamps>>( + &mut self, + scts: I, + ) -> Result<()> { + let scts: Vec = scts + .into_iter() + .map(|bytes| CFData::from_buffer(bytes.as_ref())) + .collect(); + + let scts = CFArray::from_CFTypes(&scts); + + unsafe { cvt(SecTrustSetSignedCertificateTimestamps(self.0, scts.as_concrete_TypeRef())) } + } + + /// Returns the public key for a leaf certificate after it has been evaluated. + #[inline] + pub fn copy_public_key(&mut self) -> Result { + unsafe { + Ok(SecKey::wrap_under_create_rule(SecTrustCopyPublicKey( + self.0, + ))) + } + } + + /// Evaluates trust. + #[deprecated(note = "use evaluate_with_error")] + pub fn evaluate(&self) -> Result { + #[allow(deprecated)] + unsafe { + let mut result = kSecTrustResultInvalid; + cvt(SecTrustEvaluate(self.0, &mut result))?; + Ok(TrustResult(result)) + } + } + + /// Evaluates trust. Requires macOS 10.14 or iOS, otherwise it just calls `evaluate()` + pub fn evaluate_with_error(&self) -> Result<(), CFError> { + #[cfg(any(feature = "OSX_10_14", target_os = "ios"))] + unsafe { + let mut error: CFErrorRef = ::std::ptr::null_mut(); + if !SecTrustEvaluateWithError(self.0, &mut error) { + assert!(!error.is_null()); + let error = CFError::wrap_under_create_rule(error); + return Err(error); + } + Ok(()) + } + #[cfg(not(any(feature = "OSX_10_14", target_os = "ios")))] + #[allow(deprecated)] + { + use security_framework_sys::base::errSecNotTrusted; + use security_framework_sys::base::errSecTrustSettingDeny; + + let code = match self.evaluate() { + Ok(res) if res.success() => return Ok(()), + Ok(TrustResult::DENY) => errSecTrustSettingDeny, + Ok(_) => errSecNotTrusted, + Err(err) => err.code(), + }; + Err(cferror_from_osstatus(code)) + } + } + + /// Returns the number of certificates in an evaluated certificate chain. + /// + /// Note: evaluate must first be called on the `SecTrust`. + #[inline(always)] + #[must_use] + pub fn certificate_count(&self) -> CFIndex { + unsafe { SecTrustGetCertificateCount(self.0) } + } + + /// Returns a specific certificate from the certificate chain used to evaluate trust. + /// + /// Note: evaluate must first be called on the `SecTrust`. + #[deprecated(note = "deprecated by Apple")] + #[must_use] + pub fn certificate_at_index(&self, ix: CFIndex) -> Option { + #[allow(deprecated)] + unsafe { + if self.certificate_count() <= ix { + None + } else { + let certificate = SecTrustGetCertificateAtIndex(self.0, ix); + Some(SecCertificate::wrap_under_get_rule(certificate.cast())) + } + } + } +} + +#[cfg(not(any(feature = "OSX_10_14", target_os = "ios")))] +extern "C" { + fn CFErrorCreate(allocator: core_foundation_sys::base::CFAllocatorRef, domain: core_foundation_sys::string::CFStringRef, code: CFIndex, userInfo: core_foundation_sys::dictionary::CFDictionaryRef) -> CFErrorRef; +} + +#[cfg(not(any(feature = "OSX_10_14", target_os = "ios")))] +fn cferror_from_osstatus(code: core_foundation_sys::base::OSStatus) -> CFError { + unsafe { + let error = CFErrorCreate(ptr::null_mut(), core_foundation_sys::error::kCFErrorDomainOSStatus, code as _, ptr::null_mut()); + assert!(!error.is_null()); + CFError::wrap_under_create_rule(error) + } +} + +#[cfg(test)] +mod test { + use crate::policy::SecPolicy; + use crate::secure_transport::SslProtocolSide; + use crate::test::certificate; + use crate::trust::SecTrust; + + #[test] + #[allow(deprecated)] + fn create_with_certificates() { + let cert = certificate(); + let ssl_policy = SecPolicy::create_ssl(SslProtocolSide::CLIENT, Some("certifi.io")); + let trust = SecTrust::create_with_certificates(&[cert], &[ssl_policy]).unwrap(); + assert_eq!(trust.evaluate().unwrap().success(), false) + } + + #[test] + fn create_with_certificates_new() { + let cert = certificate(); + let ssl_policy = SecPolicy::create_ssl(SslProtocolSide::CLIENT, Some("certifi.io")); + let trust = SecTrust::create_with_certificates(&[cert], &[ssl_policy]).unwrap(); + assert!(trust.evaluate_with_error().is_err()); + } + + #[test] + #[allow(deprecated)] + fn certificate_count_and_at_index() { + let cert = certificate(); + let ssl_policy = SecPolicy::create_ssl(SslProtocolSide::CLIENT, Some("certifi.io")); + let trust = SecTrust::create_with_certificates(&[cert], &[ssl_policy]).unwrap(); + trust.evaluate().unwrap(); + + let count = trust.certificate_count(); + assert_eq!(count, 1); + + let cert_bytes = trust.certificate_at_index(0).unwrap().to_der(); + assert_eq!(cert_bytes, certificate().to_der()); + } + + #[test] + #[allow(deprecated)] + fn certificate_count_and_at_index_new() { + let cert = certificate(); + let ssl_policy = SecPolicy::create_ssl(SslProtocolSide::CLIENT, Some("certifi.io")); + let trust = SecTrust::create_with_certificates(&[cert], &[ssl_policy]).unwrap(); + assert!(trust.evaluate_with_error().is_err()); + + let count = trust.certificate_count(); + assert_eq!(count, 1); + + let cert_bytes = trust.certificate_at_index(0).unwrap().to_der(); + assert_eq!(cert_bytes, certificate().to_der()); + } + + #[test] + #[allow(deprecated)] + fn certificate_at_index_out_of_bounds() { + let cert = certificate(); + let ssl_policy = SecPolicy::create_ssl(SslProtocolSide::CLIENT, Some("certifi.io")); + + let trust = SecTrust::create_with_certificates(&[cert.clone()], &[ssl_policy.clone()]).unwrap(); + trust.evaluate().unwrap(); + assert!(trust.certificate_at_index(1).is_none()); + + let trust = SecTrust::create_with_certificates(&[cert], &[ssl_policy]).unwrap(); + assert!(trust.evaluate_with_error().is_err()); + assert!(trust.certificate_at_index(1).is_none()); + } + + #[test] + #[allow(deprecated)] + fn set_policy() { + let cert = certificate(); + let ssl_policy = SecPolicy::create_ssl(SslProtocolSide::CLIENT, Some("certifi.io.bogus")); + let mut trust = SecTrust::create_with_certificates(&[cert], &[ssl_policy]).unwrap(); + let ssl_policy = SecPolicy::create_ssl(SslProtocolSide::CLIENT, Some("certifi.io")); + trust.set_policy(&ssl_policy).unwrap(); + assert_eq!(trust.evaluate().unwrap().success(), false) + } + + #[test] + fn set_policy_new() { + let cert = certificate(); + let ssl_policy = SecPolicy::create_ssl(SslProtocolSide::CLIENT, Some("certifi.io.bogus")); + let mut trust = SecTrust::create_with_certificates(&[cert], &[ssl_policy]).unwrap(); + let ssl_policy = SecPolicy::create_ssl(SslProtocolSide::CLIENT, Some("certifi.io")); + trust.set_policy(&ssl_policy).unwrap(); + assert!(trust.evaluate_with_error().is_err()); + } +} diff --git a/vendor/security-framework/src/trust_settings.rs b/vendor/security-framework/src/trust_settings.rs new file mode 100644 index 000000000..23efdf1f7 --- /dev/null +++ b/vendor/security-framework/src/trust_settings.rs @@ -0,0 +1,305 @@ +//! Querying trust settings. + +use core_foundation::array::{CFArray, CFArrayRef}; +use core_foundation::base::{CFIndex, TCFType}; +use core_foundation::dictionary::CFDictionary; +use core_foundation::number::CFNumber; +use core_foundation::string::CFString; + +use core_foundation_sys::base::CFTypeRef; +use security_framework_sys::base::errSecNoTrustSettings; +use security_framework_sys::base::errSecSuccess; +use security_framework_sys::trust_settings::*; + +use std::ptr; + +use crate::base::Error; +use crate::base::Result; +use crate::certificate::SecCertificate; +use crate::cvt; + +/// Which set of trust settings to query +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[repr(u32)] +pub enum Domain { + /// Per-user trust settings + User = kSecTrustSettingsDomainUser, + /// Locally administered, system-wide trust settings + Admin = kSecTrustSettingsDomainAdmin, + /// System trust settings + System = kSecTrustSettingsDomainSystem, +} + +impl From for SecTrustSettingsDomain { + #[inline] + fn from(domain: Domain) -> SecTrustSettingsDomain { + match domain { + Domain::User => kSecTrustSettingsDomainUser, + Domain::Admin => kSecTrustSettingsDomainAdmin, + Domain::System => kSecTrustSettingsDomainSystem, + } + } +} + +/// Trust settings for a specific certificate in a specific domain +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum TrustSettingsForCertificate { + /// Not used + Invalid, + + /// This is a root certificate and is trusted, either explicitly or + /// implicitly. + TrustRoot, + + /// This is a non-root certificate but is explicitly trusted. + TrustAsRoot, + + /// Cert is explicitly distrusted. + Deny, + + /// Neither trusted nor distrusted. + Unspecified, +} + +impl TrustSettingsForCertificate { + /// Create from `kSecTrustSettingsResult*` constant + fn new(value: i64) -> Self { + if value < 0 || value > i64::from(u32::max_value()) { + return Self::Invalid; + } + match value as u32 { + kSecTrustSettingsResultTrustRoot => Self::TrustRoot, + kSecTrustSettingsResultTrustAsRoot => Self::TrustAsRoot, + kSecTrustSettingsResultDeny => Self::Deny, + kSecTrustSettingsResultUnspecified => Self::Unspecified, + _ => Self::Invalid, + } + } +} + +/// Allows access to the certificates and their trust settings in a given domain. +pub struct TrustSettings { + domain: Domain, +} + +impl TrustSettings { + /// Create a new `TrustSettings` for the given domain. + /// + /// You can call `iter()` to discover the certificates with settings in this domain. + /// + /// Then you can call `tls_trust_settings_for_certificate()` with a given certificate + /// to learn what the aggregate trust setting for that certificate within this domain. + #[inline(always)] + #[must_use] + pub fn new(domain: Domain) -> Self { + Self { domain } + } + + /// Create an iterator over the certificates with settings in this domain. + /// This produces an empty iterator if there are no such certificates. + pub fn iter(&self) -> Result { + let array = unsafe { + let mut array_ptr: CFArrayRef = ptr::null_mut(); + + // SecTrustSettingsCopyCertificates returns errSecNoTrustSettings + // if no items have trust settings in the given domain. We map + // that to an empty TrustSettings iterator. + match SecTrustSettingsCopyCertificates(self.domain.into(), &mut array_ptr) { + errSecNoTrustSettings => CFArray::from_CFTypes(&[]), + errSecSuccess => CFArray::::wrap_under_create_rule(array_ptr), + err => return Err(Error::from_code(err)), + } + }; + + Ok(TrustSettingsIter { index: 0, array }) + } + + ///set trust settings to ""always trust this root certificate regardless of use.". + /// Sets the trust settings for the provided certificate to "always trust this root certificate + /// regardless of use." + /// + /// This method configures the trust settings for the specified certificate, indicating that it should + /// always be trusted as a TLS root certificate, regardless of its usage. + /// + /// If successful, the trust settings are updated for the certificate in the given domain. If the + /// certificate had no previous trust settings in the domain, new trust settings are created. If the + /// certificate had existing trust settings, they are replaced with the new settings. + /// + /// It is not possible to modify per-user trust settings when not running in a GUI + /// environment, if you try it will return error `2070: errSecInternalComponent` + #[cfg(target_os="macos")] + pub fn set_trust_settings_always(&self, cert: &SecCertificate) -> Result<()> { + let domain = self.domain; + let trust_settings: CFTypeRef = ptr::null_mut(); + cvt(unsafe { + SecTrustSettingsSetTrustSettings( + cert.as_CFTypeRef() as *mut _, + domain.into(), + trust_settings, + ) + }) + } + + /// Returns the aggregate trust setting for the given certificate. + /// + /// This tells you whether the certificate should be trusted as a TLS + /// root certificate. + /// + /// If the certificate has no trust settings in the given domain, the + /// `errSecItemNotFound` error is returned. + /// + /// If the certificate has no specific trust settings for TLS in the + /// given domain `None` is returned. + /// + /// Otherwise, the specific trust settings are aggregated and returned. + pub fn tls_trust_settings_for_certificate(&self, cert: &SecCertificate) + -> Result> { + let trust_settings = unsafe { + let mut array_ptr: CFArrayRef = ptr::null_mut(); + let cert_ptr = cert.as_CFTypeRef() as *mut _; + cvt(SecTrustSettingsCopyTrustSettings(cert_ptr, + self.domain.into(), + &mut array_ptr))?; + CFArray::::wrap_under_create_rule(array_ptr) + }; + + for settings in trust_settings.iter() { + // Reject settings for non-SSL policies + let is_not_ssl_policy = { + let policy_name_key = CFString::from_static_string("kSecTrustSettingsPolicyName"); + let ssl_policy_name = CFString::from_static_string("sslServer"); + + let maybe_name: Option = settings + .find(policy_name_key.as_CFTypeRef().cast()) + .map(|name| unsafe { CFString::wrap_under_get_rule((*name).cast()) }); + + matches!(maybe_name, Some(ref name) if name != &ssl_policy_name) + }; + + if is_not_ssl_policy { + continue; + } + + // Evaluate "effective trust settings" for this usage constraint. + let maybe_trust_result = { + let settings_result_key = CFString::from_static_string("kSecTrustSettingsResult"); + settings + .find(settings_result_key.as_CFTypeRef().cast()) + .map(|num| unsafe { CFNumber::wrap_under_get_rule((*num).cast()) }) + .and_then(|num| num.to_i64()) + }; + + // "Note that an empty Trust Settings array means "always trust this cert, + // with a resulting kSecTrustSettingsResult of kSecTrustSettingsResultTrustRoot"." + let trust_result = TrustSettingsForCertificate::new(maybe_trust_result + .unwrap_or_else(|| i64::from(kSecTrustSettingsResultTrustRoot))); + + match trust_result { + TrustSettingsForCertificate::Unspecified | + TrustSettingsForCertificate::Invalid => { continue; }, + _ => return Ok(Some(trust_result)), + } + } + + // There were no more specific settings. This might mean the certificate + // is to be trusted anyway (since, eg, it's in system store), but leave + // the caller to make this decision. + Ok(None) + } +} + +/// Iterator over certificates. +pub struct TrustSettingsIter { + array: CFArray, + index: CFIndex, +} + +impl Iterator for TrustSettingsIter { + type Item = SecCertificate; + + #[inline] + fn next(&mut self) -> Option { + if self.index >= self.array.len() { + None + } else { + let cert = self.array.get(self.index).unwrap(); + self.index += 1; + Some(cert.clone()) + } + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + let left = (self.array.len() as usize).saturating_sub(self.index as usize); + (left, Some(left)) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::test::certificate; + + fn list_for_domain(domain: Domain) { + println!("--- domain: {:?}", domain); + let ts = TrustSettings::new(domain); + let iterator = ts.iter().unwrap(); + + for (i, cert) in iterator.enumerate() { + println!("cert({:?}) = {:?}", i, cert); + println!(" settings = {:?}", ts.tls_trust_settings_for_certificate(&cert)); + } + println!("---"); + } + + #[test] + fn list_for_user() { + list_for_domain(Domain::User); + } + + #[test] + fn list_for_system() { + list_for_domain(Domain::System); + } + + #[test] + fn list_for_admin() { + list_for_domain(Domain::Admin); + } + + #[test] + fn test_system_certs_are_present() { + let system = TrustSettings::new(Domain::System).iter().unwrap().count(); + + // 168 at the time of writing + assert!(system > 100); + } + + #[test] + fn test_isrg_root_exists_and_is_trusted() { + let ts = TrustSettings::new(Domain::System); + assert_eq!( + ts.iter() + .unwrap() + .find(|cert| cert.subject_summary() == "ISRG Root X1") + .and_then(|cert| ts.tls_trust_settings_for_certificate(&cert).unwrap()), + None + ); + // ^ this is a case where None means "always trust", according to Apple docs: + // + // "Note that an empty Trust Settings array means "always trust this cert, + // with a resulting kSecTrustSettingsResult of kSecTrustSettingsResultTrustRoot"." + } + + #[test] + fn test_unknown_cert_is_not_trusted() { + let ts = TrustSettings::new(Domain::System); + let cert = certificate(); + assert_eq!(ts.tls_trust_settings_for_certificate(&cert) + .err() + .unwrap() + .message(), + Some("The specified item could not be found in the keychain.".into())); + } +} + -- cgit v1.2.3