diff --git a/MODULE.bazel b/MODULE.bazel index be00937946..7b7a99eed8 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -61,6 +61,7 @@ osx.frameworks(names = [ "IOSurface", "IOKit", "Kernel", + "LocalAuthentication", "Metal", "MetalKit", "OpenGL", diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 50679e7dc3..81d4658409 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2538,13 +2538,10 @@ version = "0.0.0" dependencies = [ "async-trait", "base64 0.22.1", - "core-foundation 0.10.1", - "core-foundation-sys", + "cc", "p256", "pretty_assertions", "rand 0.9.3", - "security-framework 3.5.1", - "security-framework-sys", "serde", "serde_json", "thiserror 2.0.18", diff --git a/codex-rs/device-key/BUILD.bazel b/codex-rs/device-key/BUILD.bazel index 4ad47f84a0..1a41661e87 100644 --- a/codex-rs/device-key/BUILD.bazel +++ b/codex-rs/device-key/BUILD.bazel @@ -1,6 +1,27 @@ +load("@rules_cc//cc:objc_library.bzl", "objc_library") load("//:defs.bzl", "codex_rust_crate") codex_rust_crate( name = "device-key", crate_name = "codex_device_key", + # Bazel wires the Objective-C provider through :macos-provider below, so skip Cargo's build.rs. + build_script_enabled = False, + deps_extra = select({ + "@platforms//os:macos": [":macos-provider"], + "//conditions:default": [], + }), +) + +objc_library( + name = "macos-provider", + srcs = ["src/platform/macos_provider.m"], + hdrs = ["src/platform/macos_provider.h"], + copts = ["-fobjc-arc"], + sdk_frameworks = [ + "Foundation", + "LocalAuthentication", + "Security", + ], + target_compatible_with = ["@platforms//os:macos"], + visibility = ["//visibility:private"], ) diff --git a/codex-rs/device-key/Cargo.toml b/codex-rs/device-key/Cargo.toml index 3ba77b1656..2214d5df58 100644 --- a/codex-rs/device-key/Cargo.toml +++ b/codex-rs/device-key/Cargo.toml @@ -3,6 +3,7 @@ name = "codex-device-key" version.workspace = true edition.workspace = true license.workspace = true +build = "build.rs" [lints] workspace = true @@ -18,11 +19,8 @@ thiserror = { workspace = true } tokio = { workspace = true, features = ["rt"] } url = { workspace = true } -[target.'cfg(target_os = "macos")'.dependencies] -core-foundation = { workspace = true } -core-foundation-sys = { workspace = true } -security-framework = { workspace = true } -security-framework-sys = { workspace = true } - [dev-dependencies] pretty_assertions = { workspace = true } + +[build-dependencies] +cc = "1" diff --git a/codex-rs/device-key/build.rs b/codex-rs/device-key/build.rs new file mode 100644 index 0000000000..4178e0f028 --- /dev/null +++ b/codex-rs/device-key/build.rs @@ -0,0 +1,17 @@ +fn main() { + println!("cargo:rerun-if-changed=src/platform/macos_provider.h"); + println!("cargo:rerun-if-changed=src/platform/macos_provider.m"); + + if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("macos") { + return; + } + + cc::Build::new() + .file("src/platform/macos_provider.m") + .flag("-fobjc-arc") + .compile("codex_device_key_macos_provider"); + + println!("cargo:rustc-link-lib=framework=Foundation"); + println!("cargo:rustc-link-lib=framework=Security"); + println!("cargo:rustc-link-lib=framework=LocalAuthentication"); +} diff --git a/codex-rs/device-key/src/platform/macos.rs b/codex-rs/device-key/src/platform/macos.rs index 8215b6e7de..a32b8587f1 100644 --- a/codex-rs/device-key/src/platform/macos.rs +++ b/codex-rs/device-key/src/platform/macos.rs @@ -6,82 +6,84 @@ use crate::DeviceKeyProvider; use crate::ProviderCreateRequest; use crate::ProviderSignature; use crate::sec1_public_key_to_spki_der; -use core_foundation::base::TCFType; -use core_foundation::base::ToVoid; -use core_foundation::boolean::CFBoolean; -use core_foundation::data::CFData; -use core_foundation::dictionary::CFMutableDictionary; -use core_foundation::error::CFError; -use core_foundation::number::CFNumber; -use core_foundation::string::CFString; -use core_foundation_sys::base::CFTypeRef; -use core_foundation_sys::string::CFStringRef; -use security_framework::access_control::ProtectionMode; -use security_framework::access_control::SecAccessControl; -use security_framework::key::Algorithm; -use security_framework::key::SecKey; -use security_framework_sys::access_control::kSecAccessControlPrivateKeyUsage; -use security_framework_sys::access_control::kSecAccessControlUserPresence; -use security_framework_sys::base::errSecItemNotFound; -use security_framework_sys::base::errSecParam; -use security_framework_sys::base::errSecSuccess; -use security_framework_sys::base::errSecUnimplemented; -use security_framework_sys::item::kSecAttrAccessControl; -use security_framework_sys::item::kSecAttrIsPermanent; -use security_framework_sys::item::kSecAttrKeyClass; -use security_framework_sys::item::kSecAttrKeyClassPrivate; -use security_framework_sys::item::kSecAttrKeySizeInBits; -use security_framework_sys::item::kSecAttrKeyType; -use security_framework_sys::item::kSecAttrKeyTypeECSECPrimeRandom; -use security_framework_sys::item::kSecAttrLabel; -use security_framework_sys::item::kSecAttrTokenID; -use security_framework_sys::item::kSecAttrTokenIDSecureEnclave; -use security_framework_sys::item::kSecClass; -use security_framework_sys::item::kSecClassKey; -use security_framework_sys::item::kSecPrivateKeyAttrs; -use security_framework_sys::item::kSecReturnRef; -use security_framework_sys::keychain_item::SecItemCopyMatching; +use std::ffi::CStr; +use std::ffi::CString; use std::ffi::c_char; -use std::ffi::c_double; -use std::ffi::c_void; +use std::ffi::c_int; use std::ptr; -use std::sync::Mutex; -use std::sync::OnceLock; +use std::slice; -#[allow(non_upper_case_globals)] -unsafe extern "C" { - static kSecAttrApplicationTag: CFStringRef; - static kSecAttrIsExtractable: CFStringRef; - static kSecUseAuthenticationContext: CFStringRef; +const MAC_STATUS_OK: c_int = 0; +const MAC_STATUS_NOT_FOUND: c_int = 1; +const MAC_STATUS_HARDWARE_UNAVAILABLE: c_int = 2; + +const MAC_KEY_CLASS_SECURE_ENCLAVE: c_int = 0; +const MAC_KEY_CLASS_OS_PROTECTED_NONEXTRACTABLE: c_int = 1; + +#[repr(C)] +struct MacBytesResult { + status: c_int, + data: *mut u8, + len: usize, + error_message: *mut c_char, } unsafe extern "C" { - fn dlopen(path: *const c_char, mode: i32) -> *mut c_void; - fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void; + fn codex_device_key_macos_create_or_load_public_key( + key_tag: *const c_char, + key_class: c_int, + ) -> MacBytesResult; + fn codex_device_key_macos_load_public_key( + key_tag: *const c_char, + key_class: c_int, + ) -> MacBytesResult; + fn codex_device_key_macos_delete(key_tag: *const c_char, key_class: c_int) -> MacBytesResult; + fn codex_device_key_macos_sign( + key_tag: *const c_char, + key_class: c_int, + payload: *const u8, + payload_len: usize, + ) -> MacBytesResult; + fn codex_device_key_macos_free_bytes_result(result: *mut MacBytesResult); } -#[link(name = "objc")] -unsafe extern "C" { - fn objc_getClass(name: *const c_char) -> ObjcId; - fn sel_registerName(name: *const c_char) -> ObjcSel; +impl MacBytesResult { + fn into_bytes(mut self) -> Result, DeviceKeyError> { + let result = match self.status { + MAC_STATUS_OK => { + if self.data.is_null() && self.len != 0 { + Err(DeviceKeyError::Platform( + "macOS device-key provider returned null data".to_string(), + )) + } else { + let bytes = if self.len == 0 { + Vec::new() + } else { + unsafe { slice::from_raw_parts(self.data.cast_const(), self.len).to_vec() } + }; + Ok(bytes) + } + } + MAC_STATUS_NOT_FOUND => Err(DeviceKeyError::KeyNotFound), + MAC_STATUS_HARDWARE_UNAVAILABLE => Err(DeviceKeyError::HardwareBackedKeysUnavailable), + _ => Err(DeviceKeyError::Platform(self.error_message())), + }; + unsafe { + codex_device_key_macos_free_bytes_result(ptr::addr_of_mut!(self)); + } + result + } + + fn error_message(&self) -> String { + if self.error_message.is_null() { + return "unknown macOS device-key provider error".to_string(); + } + unsafe { CStr::from_ptr(self.error_message) } + .to_string_lossy() + .into_owned() + } } -type ObjcId = *mut c_void; -type ObjcSel = *mut c_void; - -const LOCAL_AUTHENTICATION_FRAMEWORK_PATH: &[u8] = - b"/System/Library/Frameworks/LocalAuthentication.framework/LocalAuthentication\0"; -const LA_CONTEXT_CLASS: &[u8] = b"LAContext\0"; -const OBJC_MSG_SEND_SYMBOL: &[u8] = b"objc_msgSend\0"; -const OBJC_ALLOC_SELECTOR: &[u8] = b"alloc\0"; -const OBJC_INIT_SELECTOR: &[u8] = b"init\0"; -const OBJC_RELEASE_SELECTOR: &[u8] = b"release\0"; -const SET_TOUCH_ID_AUTHENTICATION_REUSE_DURATION_SELECTOR: &[u8] = - b"setTouchIDAuthenticationAllowableReuseDuration:\0"; -const TOUCH_ID_AUTHENTICATION_REUSE_DURATION_SECONDS: c_double = 300.0; -const RTLD_LAZY: i32 = 0x1; -const RTLD_LOCAL: i32 = 0x4; - #[derive(Debug)] pub(crate) struct MacOsDeviceKeyProvider; @@ -118,6 +120,16 @@ impl DeviceKeyProvider for MacOsDeviceKeyProvider { } } + fn delete( + &self, + key_id: &str, + protection_class: DeviceKeyProtectionClass, + ) -> Result<(), DeviceKeyError> { + let class = MacKeyClass::from_protection_class(protection_class) + .ok_or(DeviceKeyError::KeyNotFound)?; + delete_key(key_id, class) + } + fn get_public( &self, key_id: &str, @@ -125,8 +137,8 @@ impl DeviceKeyProvider for MacOsDeviceKeyProvider { ) -> Result { let class = MacKeyClass::from_protection_class(protection_class) .ok_or(DeviceKeyError::KeyNotFound)?; - let key = load_private_key(key_id, class)?.ok_or(DeviceKeyError::KeyNotFound)?; - key_info(key_id, class, &key) + let public_key = load_public_key(key_id, class)?; + key_info(key_id, class, public_key.as_slice()) } fn sign( @@ -137,15 +149,7 @@ impl DeviceKeyProvider for MacOsDeviceKeyProvider { ) -> Result { let class = MacKeyClass::from_protection_class(protection_class) .ok_or(DeviceKeyError::KeyNotFound)?; - let context = reusable_authentication_context()?; - let context = context - .lock() - .map_err(|err| DeviceKeyError::Platform(format!("LAContext mutex poisoned: {err}")))?; - let key = load_private_key_with_authentication_context(key_id, class, &context)? - .ok_or(DeviceKeyError::KeyNotFound)?; - let signature_der = key - .create_signature(Algorithm::ECDSASignatureMessageX962SHA256, payload) - .map_err(|err| DeviceKeyError::Platform(err.to_string()))?; + let signature_der = sign(key_id, class, payload)?; Ok(ProviderSignature { signature_der, algorithm: DeviceKeyAlgorithm::EcdsaP256Sha256, @@ -153,146 +157,6 @@ impl DeviceKeyProvider for MacOsDeviceKeyProvider { } } -struct LocalAuthenticationContext { - context: ObjcId, -} - -unsafe impl Send for LocalAuthenticationContext {} - -impl LocalAuthenticationContext { - fn new() -> Result { - load_local_authentication_framework()?; - let class = unsafe { objc_getClass(LA_CONTEXT_CLASS.as_ptr().cast::()) }; - if class.is_null() { - return Err(DeviceKeyError::Platform( - "LocalAuthentication.framework did not provide LAContext".to_string(), - )); - } - - let allocated = unsafe { objc_msg_send_id(class, sel(OBJC_ALLOC_SELECTOR))? }; - if allocated.is_null() { - return Err(DeviceKeyError::Platform( - "LAContext allocation returned null".to_string(), - )); - } - - let context = unsafe { objc_msg_send_id(allocated, sel(OBJC_INIT_SELECTOR))? }; - if context.is_null() { - return Err(DeviceKeyError::Platform( - "LAContext initialization returned null".to_string(), - )); - } - - unsafe { - objc_msg_send_void_f64( - context, - sel(SET_TOUCH_ID_AUTHENTICATION_REUSE_DURATION_SELECTOR), - TOUCH_ID_AUTHENTICATION_REUSE_DURATION_SECONDS, - )?; - } - Ok(Self { context }) - } - - fn as_void(&self) -> *const c_void { - self.context.cast_const() - } -} - -impl Drop for LocalAuthenticationContext { - fn drop(&mut self) { - unsafe { - let _ = objc_msg_send_void(self.context, sel(OBJC_RELEASE_SELECTOR)); - } - } -} - -fn reusable_authentication_context() --> Result<&'static Mutex, DeviceKeyError> { - static AUTHENTICATION_CONTEXT: OnceLock> = OnceLock::new(); - - if let Some(context) = AUTHENTICATION_CONTEXT.get() { - return Ok(context); - } - - let context = LocalAuthenticationContext::new()?; - if AUTHENTICATION_CONTEXT.set(Mutex::new(context)).is_err() { - return AUTHENTICATION_CONTEXT.get().ok_or_else(|| { - DeviceKeyError::Platform( - "LAContext initialization raced but no context won".to_string(), - ) - }); - } - AUTHENTICATION_CONTEXT.get().ok_or_else(|| { - DeviceKeyError::Platform("LAContext was not stored after initialization".to_string()) - }) -} - -fn sel(name: &'static [u8]) -> ObjcSel { - unsafe { sel_registerName(name.as_ptr().cast::()) } -} - -fn load_local_authentication_framework() -> Result<(), DeviceKeyError> { - let handle = unsafe { - dlopen( - LOCAL_AUTHENTICATION_FRAMEWORK_PATH - .as_ptr() - .cast::(), - RTLD_LAZY | RTLD_LOCAL, - ) - }; - if handle.is_null() { - Err(DeviceKeyError::Platform( - "failed to load LocalAuthentication.framework".to_string(), - )) - } else { - Ok(()) - } -} - -unsafe fn objc_msg_send_id(receiver: ObjcId, selector: ObjcSel) -> Result { - let msg_send: unsafe extern "C" fn(ObjcId, ObjcSel) -> ObjcId = - unsafe { std::mem::transmute(objc_msg_send_symbol()?) }; - Ok(unsafe { msg_send(receiver, selector) }) -} - -unsafe fn objc_msg_send_void(receiver: ObjcId, selector: ObjcSel) -> Result<(), DeviceKeyError> { - let msg_send: unsafe extern "C" fn(ObjcId, ObjcSel) = - unsafe { std::mem::transmute(objc_msg_send_symbol()?) }; - unsafe { msg_send(receiver, selector) }; - Ok(()) -} - -unsafe fn objc_msg_send_void_f64( - receiver: ObjcId, - selector: ObjcSel, - value: c_double, -) -> Result<(), DeviceKeyError> { - let msg_send: unsafe extern "C" fn(ObjcId, ObjcSel, c_double) = - unsafe { std::mem::transmute(objc_msg_send_symbol()?) }; - unsafe { msg_send(receiver, selector, value) }; - Ok(()) -} - -fn objc_msg_send_symbol() -> Result<*mut c_void, DeviceKeyError> { - let symbol = unsafe { - dlsym( - rtld_default(), - OBJC_MSG_SEND_SYMBOL.as_ptr().cast::(), - ) - }; - if symbol.is_null() { - Err(DeviceKeyError::Platform( - "objc_msgSend lookup returned null".to_string(), - )) - } else { - Ok(symbol) - } -} - -fn rtld_default() -> *mut c_void { - (-2_isize) as *mut c_void -} - #[derive(Debug, Clone, Copy)] enum MacKeyClass { SecureEnclave, @@ -300,6 +164,13 @@ enum MacKeyClass { } impl MacKeyClass { + fn native(self) -> c_int { + match self { + Self::SecureEnclave => MAC_KEY_CLASS_SECURE_ENCLAVE, + Self::OsProtectedNonextractable => MAC_KEY_CLASS_OS_PROTECTED_NONEXTRACTABLE, + } + } + fn protection_class(self) -> DeviceKeyProtectionClass { match self { Self::SecureEnclave => DeviceKeyProtectionClass::HardwareSecureEnclave, @@ -325,198 +196,62 @@ impl MacKeyClass { } } -fn load_private_key(key_id: &str, class: MacKeyClass) -> Result, DeviceKeyError> { - load_private_key_with_optional_authentication_context( - key_id, class, /*authentication_context*/ None, - ) -} - -fn load_private_key_with_authentication_context( - key_id: &str, - class: MacKeyClass, - authentication_context: &LocalAuthenticationContext, -) -> Result, DeviceKeyError> { - load_private_key_with_optional_authentication_context( - key_id, - class, - Some(authentication_context), - ) -} - -fn load_private_key_with_optional_authentication_context( - key_id: &str, - class: MacKeyClass, - authentication_context: Option<&LocalAuthenticationContext>, -) -> Result, DeviceKeyError> { - let tag = key_tag(key_id, class); - let tag = CFData::from_buffer(tag.as_bytes()); - let mut query = unsafe { - CFMutableDictionary::from_CFType_pairs(&[ - (kSecClass.to_void(), kSecClassKey.to_void()), - ( - kSecAttrKeyClass.to_void(), - kSecAttrKeyClassPrivate.to_void(), - ), - (kSecAttrApplicationTag.to_void(), tag.to_void()), - (kSecReturnRef.to_void(), CFBoolean::true_value().to_void()), - ]) - }; - if matches!(class, MacKeyClass::SecureEnclave) { - unsafe { - query.add( - &kSecAttrTokenID.to_void(), - &kSecAttrTokenIDSecureEnclave.to_void(), - ); - } - } - if matches!(class, MacKeyClass::OsProtectedNonextractable) { - unsafe { - query.add( - &kSecAttrIsExtractable.to_void(), - &CFBoolean::false_value().to_void(), - ); - } - } - if let Some(authentication_context) = authentication_context { - unsafe { - query.add( - &kSecUseAuthenticationContext.to_void(), - &authentication_context.as_void(), - ); - } - } - - let mut result: CFTypeRef = ptr::null(); - let status = unsafe { SecItemCopyMatching(query.as_concrete_TypeRef(), &mut result) }; - if status == errSecItemNotFound { - return Ok(None); - } - if status != errSecSuccess { - return Err(DeviceKeyError::Platform(security_error(status))); - } - if result.is_null() { - return Err(DeviceKeyError::Platform( - "Security.framework returned an empty key reference".to_string(), - )); - } - Ok(Some(unsafe { - SecKey::wrap_under_create_rule(result as *mut _) - })) -} - fn create_or_load_key_info( key_id: &str, class: MacKeyClass, ) -> Result { - let key = create_or_load_private_key(key_id, class)?; - key_info(key_id, class, &key) + let public_key = create_or_load_public_key(key_id, class)?; + key_info(key_id, class, public_key.as_slice()) } -fn create_or_load_private_key(key_id: &str, class: MacKeyClass) -> Result { - match create_private_key(key_id, class) { - Ok(key) => Ok(key), - Err(create_error) => match load_private_key(key_id, class) { - Ok(Some(key)) => Ok(key), - Ok(None) => Err(create_error), - Err(load_error) => Err(DeviceKeyError::Platform(format!( - "key creation failed ({create_error}); reload failed ({load_error})" - ))), - }, - } +fn create_or_load_public_key(key_id: &str, class: MacKeyClass) -> Result, DeviceKeyError> { + let tag = key_tag_cstring(key_id, class)?; + unsafe { codex_device_key_macos_create_or_load_public_key(tag.as_ptr(), class.native()) } + .into_bytes() } -/// Creates a macOS this-device-only P-256 signing key. -/// -/// The access-control flags below keep the private key local to this device and require -/// Security.framework to prove user presence before private-key use. The signing path also passes a -/// process-local `LAContext` so successful biometric/password authentication can be reused for later -/// signatures when macOS policy allows it. -#[allow(deprecated)] -fn create_private_key(key_id: &str, class: MacKeyClass) -> Result { - let access_control = SecAccessControl::create_with_protection( - Some(ProtectionMode::AccessibleWhenUnlockedThisDeviceOnly), - kSecAccessControlPrivateKeyUsage | kSecAccessControlUserPresence, - ) - .map_err(|err| DeviceKeyError::Platform(err.to_string()))?; - let tag = key_tag(key_id, class); - let tag_data = CFData::from_buffer(tag.as_bytes()); - let label = CFString::new(&tag); - let key_size = CFNumber::from(256); - let mut private_attrs = unsafe { - CFMutableDictionary::from_CFType_pairs(&[ - ( - kSecAttrIsPermanent.to_void(), - CFBoolean::true_value().to_void(), - ), - (kSecAttrAccessControl.to_void(), access_control.to_void()), - (kSecAttrApplicationTag.to_void(), tag_data.to_void()), - (kSecAttrLabel.to_void(), label.to_void()), - ]) - }; - if matches!(class, MacKeyClass::OsProtectedNonextractable) { - unsafe { - private_attrs.add( - &kSecAttrIsExtractable.to_void(), - &CFBoolean::false_value().to_void(), - ); - } - } - - let mut attributes = unsafe { - CFMutableDictionary::from_CFType_pairs(&[ - ( - kSecAttrKeyType.to_void(), - kSecAttrKeyTypeECSECPrimeRandom.to_void(), - ), - (kSecAttrKeySizeInBits.to_void(), key_size.to_void()), - (kSecAttrLabel.to_void(), label.to_void()), - (kSecPrivateKeyAttrs.to_void(), private_attrs.to_void()), - ]) - }; - if matches!(class, MacKeyClass::SecureEnclave) { - unsafe { - attributes.add( - &kSecAttrTokenID.to_void(), - &kSecAttrTokenIDSecureEnclave.to_void(), - ); - } - } - - SecKey::generate(attributes.to_immutable()).map_err(|err| create_key_error(class, err)) +fn load_public_key(key_id: &str, class: MacKeyClass) -> Result, DeviceKeyError> { + let tag = key_tag_cstring(key_id, class)?; + unsafe { codex_device_key_macos_load_public_key(tag.as_ptr(), class.native()) }.into_bytes() } -fn create_key_error(class: MacKeyClass, error: CFError) -> DeviceKeyError { - let code = error.code() as i32; - if matches!(class, MacKeyClass::SecureEnclave) - && (code == errSecUnimplemented || code == errSecParam) - { - return DeviceKeyError::HardwareBackedKeysUnavailable; - } +fn delete_key(key_id: &str, class: MacKeyClass) -> Result<(), DeviceKeyError> { + let tag = key_tag_cstring(key_id, class)?; + unsafe { codex_device_key_macos_delete(tag.as_ptr(), class.native()) } + .into_bytes() + .map(|_| ()) +} - DeviceKeyError::Platform(error.description().to_string()) +fn sign(key_id: &str, class: MacKeyClass, payload: &[u8]) -> Result, DeviceKeyError> { + let tag = key_tag_cstring(key_id, class)?; + unsafe { + codex_device_key_macos_sign( + tag.as_ptr(), + class.native(), + payload.as_ptr(), + payload.len(), + ) + } + .into_bytes() } fn key_info( key_id: &str, class: MacKeyClass, - private_key: &SecKey, + sec1_public_key: &[u8], ) -> Result { - let public_key = private_key.public_key().ok_or_else(|| { - DeviceKeyError::Platform("Security.framework did not return a public key".to_string()) - })?; - let public_key = public_key.external_representation().ok_or_else(|| { - DeviceKeyError::Platform( - "Security.framework did not return an exportable public key".to_string(), - ) - })?; Ok(DeviceKeyInfo { key_id: key_id.to_string(), - public_key_spki_der: sec1_public_key_to_spki_der(&public_key)?, + public_key_spki_der: sec1_public_key_to_spki_der(sec1_public_key)?, algorithm: DeviceKeyAlgorithm::EcdsaP256Sha256, protection_class: class.protection_class(), }) } +fn key_tag_cstring(key_id: &str, class: MacKeyClass) -> Result { + CString::new(key_tag(key_id, class)).map_err(|err| DeviceKeyError::Platform(err.to_string())) +} + fn key_tag(key_id: &str, class: MacKeyClass) -> String { format!( "com.openai.codex.device-key.{}.{}", @@ -524,9 +259,3 @@ fn key_tag(key_id: &str, class: MacKeyClass) -> String { key_id ) } - -fn security_error(status: i32) -> String { - security_framework::base::Error::from_code(status) - .message() - .unwrap_or_else(|| format!("Security.framework error code {status}")) -} diff --git a/codex-rs/device-key/src/platform/macos_provider.h b/codex-rs/device-key/src/platform/macos_provider.h new file mode 100644 index 0000000000..939e368f6d --- /dev/null +++ b/codex-rs/device-key/src/platform/macos_provider.h @@ -0,0 +1,50 @@ +#ifndef CODEX_DEVICE_KEY_MACOS_PROVIDER_H +#define CODEX_DEVICE_KEY_MACOS_PROVIDER_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum CodexDeviceKeyMacStatus { + CodexDeviceKeyMacStatusOk = 0, + CodexDeviceKeyMacStatusNotFound = 1, + CodexDeviceKeyMacStatusHardwareUnavailable = 2, + CodexDeviceKeyMacStatusPlatformError = 3, +} CodexDeviceKeyMacStatus; + +typedef enum CodexDeviceKeyMacKeyClass { + CodexDeviceKeyMacKeyClassSecureEnclave = 0, + CodexDeviceKeyMacKeyClassOsProtectedNonextractable = 1, +} CodexDeviceKeyMacKeyClass; + +typedef struct CodexDeviceKeyMacBytesResult { + int32_t status; + uint8_t *data; + size_t len; + char *error_message; +} CodexDeviceKeyMacBytesResult; + +CodexDeviceKeyMacBytesResult codex_device_key_macos_create_or_load_public_key( + const char *key_tag, + int32_t key_class); +CodexDeviceKeyMacBytesResult codex_device_key_macos_load_public_key( + const char *key_tag, + int32_t key_class); +CodexDeviceKeyMacBytesResult codex_device_key_macos_delete( + const char *key_tag, + int32_t key_class); +CodexDeviceKeyMacBytesResult codex_device_key_macos_sign( + const char *key_tag, + int32_t key_class, + const uint8_t *payload, + size_t payload_len); +void codex_device_key_macos_free_bytes_result(CodexDeviceKeyMacBytesResult *result); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/codex-rs/device-key/src/platform/macos_provider.m b/codex-rs/device-key/src/platform/macos_provider.m new file mode 100644 index 0000000000..e4ccd856a7 --- /dev/null +++ b/codex-rs/device-key/src/platform/macos_provider.m @@ -0,0 +1,370 @@ +#import "macos_provider.h" + +#import +#import +#import + +#include +#include + +static NSTimeInterval const CodexDeviceKeyTouchIdReuseDurationSeconds = 300.0; + +static CodexDeviceKeyMacBytesResult CodexDeviceKeyMacResultMake( + CodexDeviceKeyMacStatus status, + NSData *data, + NSString *errorMessage) { + CodexDeviceKeyMacBytesResult result = { + .status = status, + .data = NULL, + .len = 0, + .error_message = NULL, + }; + + if (data.length > 0) { + result.data = malloc(data.length); + if (result.data == NULL) { + result.status = CodexDeviceKeyMacStatusPlatformError; + errorMessage = @"failed to allocate result bytes"; + } else { + memcpy(result.data, data.bytes, data.length); + result.len = data.length; + } + } + + if (errorMessage.length > 0) { + const char *utf8 = errorMessage.UTF8String; + if (utf8 != NULL) { + size_t len = strlen(utf8); + result.error_message = malloc(len + 1); + if (result.error_message != NULL) { + memcpy(result.error_message, utf8, len + 1); + } + } + } + + return result; +} + +static CodexDeviceKeyMacBytesResult CodexDeviceKeyMacError( + CodexDeviceKeyMacStatus status, + NSString *message) { + return CodexDeviceKeyMacResultMake(status, nil, message); +} + +static NSString *CodexDeviceKeyMacCopySecurityError(OSStatus status) { + NSString *message = CFBridgingRelease(SecCopyErrorMessageString(status, NULL)); + if (message.length > 0) { + return message; + } + return [NSString stringWithFormat:@"Security.framework error code %d", status]; +} + +static NSString *CodexDeviceKeyMacCopyCFError(CFErrorRef error) { + if (error == NULL) { + return @"Security.framework returned an unknown error"; + } + NSError *nsError = CFBridgingRelease(error); + if (nsError.localizedDescription.length > 0) { + return nsError.localizedDescription; + } + return [nsError description]; +} + +static BOOL CodexDeviceKeyMacClassIsValid(int32_t keyClass) { + return keyClass == CodexDeviceKeyMacKeyClassSecureEnclave || + keyClass == CodexDeviceKeyMacKeyClassOsProtectedNonextractable; +} + +static NSData *CodexDeviceKeyMacTagData(NSString *keyTag) { + return [keyTag dataUsingEncoding:NSUTF8StringEncoding]; +} + +static NSMutableDictionary *CodexDeviceKeyMacPrivateKeyQuery( + NSString *keyTag, + int32_t keyClass, + LAContext *authenticationContext) { + NSMutableDictionary *query = [@{ + (__bridge id)kSecClass: (__bridge id)kSecClassKey, + (__bridge id)kSecAttrKeyClass: (__bridge id)kSecAttrKeyClassPrivate, + (__bridge id)kSecAttrApplicationTag: CodexDeviceKeyMacTagData(keyTag), + (__bridge id)kSecReturnRef: @YES, + } mutableCopy]; + + if (keyClass == CodexDeviceKeyMacKeyClassSecureEnclave) { + query[(__bridge id)kSecAttrTokenID] = (__bridge id)kSecAttrTokenIDSecureEnclave; + } else { + query[(__bridge id)kSecAttrIsExtractable] = @NO; + } + + if (authenticationContext != nil) { + query[(__bridge id)kSecUseAuthenticationContext] = authenticationContext; + } + + return query; +} + +static SecKeyRef CodexDeviceKeyMacCopyPrivateKey( + NSString *keyTag, + int32_t keyClass, + LAContext *authenticationContext, + CodexDeviceKeyMacStatus *status, + NSString **errorMessage) { + CFTypeRef item = NULL; + OSStatus secStatus = SecItemCopyMatching( + (__bridge CFDictionaryRef)CodexDeviceKeyMacPrivateKeyQuery( + keyTag, keyClass, authenticationContext), + &item); + if (secStatus == errSecItemNotFound) { + *status = CodexDeviceKeyMacStatusNotFound; + return NULL; + } + if (secStatus != errSecSuccess) { + *status = CodexDeviceKeyMacStatusPlatformError; + *errorMessage = CodexDeviceKeyMacCopySecurityError(secStatus); + return NULL; + } + if (item == NULL) { + *status = CodexDeviceKeyMacStatusPlatformError; + *errorMessage = @"Security.framework returned an empty key reference"; + return NULL; + } + *status = CodexDeviceKeyMacStatusOk; + return (SecKeyRef)item; +} + +static SecKeyRef CodexDeviceKeyMacCreatePrivateKey( + NSString *keyTag, + int32_t keyClass, + CodexDeviceKeyMacStatus *status, + NSString **errorMessage) { + CFErrorRef accessControlError = NULL; + SecAccessControlRef accessControl = SecAccessControlCreateWithFlags( + kCFAllocatorDefault, + kSecAttrAccessibleWhenUnlockedThisDeviceOnly, + kSecAccessControlPrivateKeyUsage | kSecAccessControlUserPresence, + &accessControlError); + if (accessControl == NULL) { + *status = CodexDeviceKeyMacStatusPlatformError; + *errorMessage = CodexDeviceKeyMacCopyCFError(accessControlError); + return NULL; + } + + NSMutableDictionary *privateAttributes = [@{ + (__bridge id)kSecAttrIsPermanent: @YES, + (__bridge id)kSecAttrAccessControl: (__bridge id)accessControl, + (__bridge id)kSecAttrApplicationTag: CodexDeviceKeyMacTagData(keyTag), + (__bridge id)kSecAttrLabel: keyTag, + } mutableCopy]; + if (keyClass == CodexDeviceKeyMacKeyClassOsProtectedNonextractable) { + privateAttributes[(__bridge id)kSecAttrIsExtractable] = @NO; + } + + NSMutableDictionary *attributes = [@{ + (__bridge id)kSecAttrKeyType: (__bridge id)kSecAttrKeyTypeECSECPrimeRandom, + (__bridge id)kSecAttrKeySizeInBits: @256, + (__bridge id)kSecAttrLabel: keyTag, + (__bridge id)kSecPrivateKeyAttrs: privateAttributes, + } mutableCopy]; + if (keyClass == CodexDeviceKeyMacKeyClassSecureEnclave) { + attributes[(__bridge id)kSecAttrTokenID] = (__bridge id)kSecAttrTokenIDSecureEnclave; + } + + CFErrorRef createError = NULL; + SecKeyRef key = SecKeyCreateRandomKey((__bridge CFDictionaryRef)attributes, &createError); + CFRelease(accessControl); + if (key != NULL) { + *status = CodexDeviceKeyMacStatusOk; + return key; + } + + NSError *nsError = createError == NULL ? nil : CFBridgingRelease(createError); + NSInteger code = nsError == nil ? 0 : nsError.code; + if (keyClass == CodexDeviceKeyMacKeyClassSecureEnclave && + (code == errSecUnimplemented || code == errSecParam)) { + *status = CodexDeviceKeyMacStatusHardwareUnavailable; + return NULL; + } + + *status = CodexDeviceKeyMacStatusPlatformError; + *errorMessage = nsError.localizedDescription.length > 0 + ? nsError.localizedDescription + : @"Security.framework failed to create a private key"; + return NULL; +} + +static CodexDeviceKeyMacBytesResult CodexDeviceKeyMacCopyPublicKeyResult(SecKeyRef privateKey) { + SecKeyRef publicKey = SecKeyCopyPublicKey(privateKey); + if (publicKey == NULL) { + return CodexDeviceKeyMacError( + CodexDeviceKeyMacStatusPlatformError, + @"Security.framework did not return a public key"); + } + + CFErrorRef error = NULL; + CFDataRef publicKeyData = SecKeyCopyExternalRepresentation(publicKey, &error); + CFRelease(publicKey); + if (publicKeyData == NULL) { + return CodexDeviceKeyMacError( + CodexDeviceKeyMacStatusPlatformError, + CodexDeviceKeyMacCopyCFError(error)); + } + + NSData *data = CFBridgingRelease(publicKeyData); + return CodexDeviceKeyMacResultMake(CodexDeviceKeyMacStatusOk, data, nil); +} + +static LAContext *CodexDeviceKeyMacReusableAuthenticationContext(void) { + static LAContext *context = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + context = [[LAContext alloc] init]; + context.touchIDAuthenticationAllowableReuseDuration = + CodexDeviceKeyTouchIdReuseDurationSeconds; + }); + return context; +} + +CodexDeviceKeyMacBytesResult codex_device_key_macos_create_or_load_public_key( + const char *keyTag, + int32_t keyClass) { + @autoreleasepool { + if (keyTag == NULL || !CodexDeviceKeyMacClassIsValid(keyClass)) { + return CodexDeviceKeyMacError( + CodexDeviceKeyMacStatusPlatformError, + @"invalid macOS device-key provider argument"); + } + + NSString *tag = [NSString stringWithUTF8String:keyTag]; + CodexDeviceKeyMacStatus status = CodexDeviceKeyMacStatusOk; + NSString *errorMessage = nil; + SecKeyRef key = CodexDeviceKeyMacCreatePrivateKey(tag, keyClass, &status, &errorMessage); + if (key == NULL) { + if (status == CodexDeviceKeyMacStatusHardwareUnavailable) { + return CodexDeviceKeyMacError(status, nil); + } + + CodexDeviceKeyMacStatus loadStatus = CodexDeviceKeyMacStatusOk; + NSString *loadErrorMessage = nil; + key = CodexDeviceKeyMacCopyPrivateKey( + tag, keyClass, nil, &loadStatus, &loadErrorMessage); + if (key == NULL) { + if (loadStatus == CodexDeviceKeyMacStatusNotFound) { + return CodexDeviceKeyMacError(status, errorMessage); + } + return CodexDeviceKeyMacError( + CodexDeviceKeyMacStatusPlatformError, + [NSString stringWithFormat: + @"key creation failed (%@); reload failed (%@)", + errorMessage ?: @"unknown error", + loadErrorMessage ?: @"unknown error"]); + } + } + + CodexDeviceKeyMacBytesResult result = CodexDeviceKeyMacCopyPublicKeyResult(key); + CFRelease(key); + return result; + } +} + +CodexDeviceKeyMacBytesResult codex_device_key_macos_load_public_key( + const char *keyTag, + int32_t keyClass) { + @autoreleasepool { + if (keyTag == NULL || !CodexDeviceKeyMacClassIsValid(keyClass)) { + return CodexDeviceKeyMacError( + CodexDeviceKeyMacStatusPlatformError, + @"invalid macOS device-key provider argument"); + } + + NSString *tag = [NSString stringWithUTF8String:keyTag]; + CodexDeviceKeyMacStatus status = CodexDeviceKeyMacStatusOk; + NSString *errorMessage = nil; + SecKeyRef key = CodexDeviceKeyMacCopyPrivateKey(tag, keyClass, nil, &status, &errorMessage); + if (key == NULL) { + return CodexDeviceKeyMacError(status, errorMessage); + } + + CodexDeviceKeyMacBytesResult result = CodexDeviceKeyMacCopyPublicKeyResult(key); + CFRelease(key); + return result; + } +} + +CodexDeviceKeyMacBytesResult codex_device_key_macos_delete( + const char *keyTag, + int32_t keyClass) { + @autoreleasepool { + if (keyTag == NULL || !CodexDeviceKeyMacClassIsValid(keyClass)) { + return CodexDeviceKeyMacError( + CodexDeviceKeyMacStatusPlatformError, + @"invalid macOS device-key provider argument"); + } + + NSString *tag = [NSString stringWithUTF8String:keyTag]; + NSMutableDictionary *query = CodexDeviceKeyMacPrivateKeyQuery(tag, keyClass, nil); + [query removeObjectForKey:(__bridge id)kSecReturnRef]; + OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query); + if (status == errSecSuccess || status == errSecItemNotFound) { + return CodexDeviceKeyMacResultMake(CodexDeviceKeyMacStatusOk, nil, nil); + } + + return CodexDeviceKeyMacError( + CodexDeviceKeyMacStatusPlatformError, + CodexDeviceKeyMacCopySecurityError(status)); + } +} + +CodexDeviceKeyMacBytesResult codex_device_key_macos_sign( + const char *keyTag, + int32_t keyClass, + const uint8_t *payload, + size_t payloadLen) { + @autoreleasepool { + if (keyTag == NULL || payload == NULL || !CodexDeviceKeyMacClassIsValid(keyClass)) { + return CodexDeviceKeyMacError( + CodexDeviceKeyMacStatusPlatformError, + @"invalid macOS device-key provider argument"); + } + + NSString *tag = [NSString stringWithUTF8String:keyTag]; + CodexDeviceKeyMacStatus status = CodexDeviceKeyMacStatusOk; + NSString *errorMessage = nil; + SecKeyRef key = CodexDeviceKeyMacCopyPrivateKey( + tag, + keyClass, + CodexDeviceKeyMacReusableAuthenticationContext(), + &status, + &errorMessage); + if (key == NULL) { + return CodexDeviceKeyMacError(status, errorMessage); + } + + NSData *payloadData = [NSData dataWithBytes:payload length:payloadLen]; + CFErrorRef error = NULL; + CFDataRef signature = SecKeyCreateSignature( + key, + kSecKeyAlgorithmECDSASignatureMessageX962SHA256, + (__bridge CFDataRef)payloadData, + &error); + CFRelease(key); + if (signature == NULL) { + return CodexDeviceKeyMacError( + CodexDeviceKeyMacStatusPlatformError, + CodexDeviceKeyMacCopyCFError(error)); + } + + NSData *signatureData = CFBridgingRelease(signature); + return CodexDeviceKeyMacResultMake(CodexDeviceKeyMacStatusOk, signatureData, nil); + } +} + +void codex_device_key_macos_free_bytes_result(CodexDeviceKeyMacBytesResult *result) { + if (result == NULL) { + return; + } + free(result->data); + free(result->error_message); + result->data = NULL; + result->len = 0; + result->error_message = NULL; +}