use crate::curve::SharedSecret;
use crate::curve::ed25519::{
Ed25519, Ed25519PublicKey, Ed25519Signature, SoftwareEd25519PrivateKey,
};
use crate::curve::p256::{Error, P256, P256Signature, P256r1PublicKey};
use crate::noise::seal::{SEALED_SIZE, open_32, seal_32};
use crate::provider::{
CryptoKeyProvider, CryptoKeyProviderAsync, DhProvider, DhProviderAsync, SigningProvider,
SigningProviderAsync,
};
use core_foundation::{base::TCFType as _, data::CFData, dictionary::CFDictionary};
use security_framework::{
access_control::{ProtectionMode, SecAccessControl},
item::Location,
key::{Algorithm, GenerateKeyOptions, KeyType, SecKey, Token},
passwords::{
PasswordOptions, delete_generic_password_options, generic_password,
set_generic_password_options,
},
passwords_options::AccessControlOptions,
};
const EPHEMERAL_LABEL: &str = "hiss.ephemeral.p256";
const ED25519_SEED_ACCOUNT: &str = "device-identity";
#[derive(Clone)]
pub struct P256r1PrivateKey {
key: SecKey,
deletable: bool,
}
impl P256r1PublicKey {
fn as_sec_key(&self, attributes: &CFDictionary) -> Result<SecKey, Error> {
let key_data = CFData::from_buffer(self.to_bytes());
unsafe {
let mut error = std::ptr::null_mut();
let sec_key = security_framework_sys::key::SecKeyCreateWithData(
key_data.as_concrete_TypeRef(),
attributes.as_concrete_TypeRef(),
&mut error,
);
if sec_key.is_null() {
let cf_error = core_foundation::error::CFError::wrap_under_create_rule(error);
Err(Error::Platform(format!("{cf_error:?}")))
} else {
Ok(SecKey::wrap_under_create_rule(sec_key))
}
}
}
}
impl P256r1PrivateKey {
fn new(key: SecKey, deletable: bool) -> Self {
Self { key, deletable }
}
pub fn generate_ephemeral() -> Result<Self, Error> {
let mut attributes = GenerateKeyOptions::default();
attributes
.set_key_type(KeyType::ec())
.set_size_in_bits(256)
.set_token(Token::Software)
.set_label(EPHEMERAL_LABEL);
let key = SecKey::new(&attributes)
.map_err(|e| Error::Platform(format!("failed to generate SecKey: {e:?}")))?;
Ok(Self::new(key, false))
}
pub fn generate_secure_enclave_ephemeral() -> Result<Self, Error> {
let mut attributes = GenerateKeyOptions::default();
attributes
.set_key_type(KeyType::ec())
.set_size_in_bits(256)
.set_token(Token::SecureEnclave)
.set_label(EPHEMERAL_LABEL)
.set_access_control(
SecAccessControl::create_with_protection(
Some(ProtectionMode::AccessibleAfterFirstUnlockThisDeviceOnly),
AccessControlOptions::PRIVATE_KEY_USAGE.bits(),
)
.map_err(|e| Error::Platform(format!("access control: {e}")))?,
);
let key = SecKey::new(&attributes)
.map_err(|e| Error::Platform(format!("failed to generate SecKey: {e:?}")))?;
Ok(Self::new(key, false))
}
pub fn generate_secure_enclave(label: &str) -> Result<Self, Error> {
let mut attributes = GenerateKeyOptions::default();
attributes
.set_key_type(KeyType::ec())
.set_size_in_bits(256)
.set_label(label)
.set_token(Token::SecureEnclave)
.set_location(Location::DataProtectionKeychain)
.set_access_control(
SecAccessControl::create_with_protection(
Some(ProtectionMode::AccessibleAfterFirstUnlockThisDeviceOnly),
AccessControlOptions::PRIVATE_KEY_USAGE.bits(),
)
.map_err(|e| Error::Platform(format!("access control: {e}")))?,
);
let key = SecKey::new(&attributes)
.map_err(|e| Error::Platform(format!("failed to generate SecKey: {e:?}")))?;
Ok(Self::new(key, true))
}
pub fn load_from_keychain(label: &str) -> Result<Option<Self>, Error> {
use core_foundation::base::TCFType as _;
use core_foundation::boolean::CFBoolean;
use core_foundation::string::CFString;
use security_framework_sys::item::{
kSecAttrKeyClass, kSecAttrKeyClassPrivate, kSecAttrKeyType,
kSecAttrKeyTypeECSECPrimeRandom, kSecAttrLabel, kSecAttrTokenID,
kSecAttrTokenIDSecureEnclave, kSecClass, kSecClassKey, kSecReturnRef,
};
use security_framework_sys::keychain_item::SecItemCopyMatching;
let label = CFString::new(label);
unsafe {
let query = CFDictionary::from_CFType_pairs(&[
(
CFString::wrap_under_get_rule(kSecClass),
CFString::wrap_under_get_rule(kSecClassKey).as_CFType(),
),
(
CFString::wrap_under_get_rule(kSecAttrKeyType),
CFString::wrap_under_get_rule(kSecAttrKeyTypeECSECPrimeRandom).as_CFType(),
),
(
CFString::wrap_under_get_rule(kSecAttrKeyClass),
CFString::wrap_under_get_rule(kSecAttrKeyClassPrivate).as_CFType(),
),
(
CFString::wrap_under_get_rule(kSecAttrLabel),
label.as_CFType(),
),
(
CFString::wrap_under_get_rule(kSecAttrTokenID),
CFString::wrap_under_get_rule(kSecAttrTokenIDSecureEnclave).as_CFType(),
),
(
CFString::wrap_under_get_rule(kSecReturnRef),
CFBoolean::true_value().as_CFType(),
),
]);
let mut result: core_foundation::base::CFTypeRef = std::ptr::null();
let status = SecItemCopyMatching(query.as_concrete_TypeRef(), &mut result);
if status == security_framework_sys::base::errSecItemNotFound {
return Ok(None);
}
if status != security_framework_sys::base::errSecSuccess {
return Err(Error::Platform(format!(
"Keychain query failed with status {status}"
)));
}
let key = SecKey::wrap_under_create_rule(result as *mut _);
Ok(Some(Self::new(key, true)))
}
}
pub fn public(&self) -> Result<P256r1PublicKey, Error> {
let data = self
.key
.public_key()
.ok_or_else(|| Error::Platform("failed to extract public key".into()))?;
let data = data.external_representation().ok_or_else(|| {
Error::Platform("failed to extract public key external representation".into())
})?;
P256r1PublicKey::from_bytes(data.bytes())
}
pub fn dh(&self, public_key: &P256r1PublicKey) -> Result<SharedSecret<32>, Error> {
let algorithm = Algorithm::ECDHKeyExchangeStandard;
let supported = unsafe {
security_framework_sys::key::SecKeyIsAlgorithmSupported(
self.key.as_concrete_TypeRef(),
security_framework_sys::key::kSecKeyOperationTypeKeyExchange,
security_framework_sys::key::kSecKeyAlgorithmECDHKeyExchangeStandard,
)
};
if supported != 1 {
return Err(Error::Platform(
"secret key does not support key exchange".into(),
));
}
let self_public = self
.key
.public_key()
.ok_or_else(|| Error::Platform("failed to derive public key for DH".into()))?;
let public_key: SecKey = public_key.as_sec_key(&self_public.attributes())?;
let shared_secret = self
.key
.key_exchange(algorithm, &public_key, 32, None)
.map_err(|e| Error::Platform(format!("key exchange failed: {e:?}")))?;
let bytes: [u8; 32] = shared_secret
.try_into()
.map_err(|_| Error::Platform("shared secret is not 32 bytes".into()))?;
Ok(SharedSecret::new(bytes))
}
pub fn sign(&self, message: &[u8]) -> Result<P256Signature, Error> {
let signature = self
.key
.create_signature(Algorithm::ECDSASignatureMessageX962SHA256, message)
.map_err(|e| Error::Platform(format!("signing failed: {e}")))?;
P256Signature::try_from_asn1(&signature).map_err(|e| {
Error::Platform(format!(
"failed to decode ASN.1 signature {}: {e}",
hex::encode(&signature)
))
})
}
pub fn delete(self) -> Result<(), Error> {
if self.deletable {
self.key
.delete()
.map_err(|e| Error::Platform(format!("failed to delete key: {e}")))
} else {
Ok(())
}
}
}
#[derive(Clone)]
pub struct AppleSecureEnclave {
namespace: String,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum SeedError {
#[error(
"Secure Enclave P-256 identity key {label:?} not found (establish it before sealing the Ed25519 seed)"
)]
IdentityKeyMissing {
label: String,
},
#[error(transparent)]
P256(#[from] Error),
#[error("Noise-N seed envelope operation failed: {0}")]
Seal(String),
#[error("Ed25519 seed Keychain operation failed: {0}")]
Keychain(String),
#[error(
"stored Ed25519 seed envelope has wrong length: expected {expected} bytes, got {got}",
expected = SEALED_SIZE
)]
WrongSize {
got: usize,
},
}
fn seed_password_options(service: &str) -> Result<PasswordOptions, SeedError> {
let mut options = PasswordOptions::new_generic_password(service, ED25519_SEED_ACCOUNT);
let access_control = SecAccessControl::create_with_protection(
Some(ProtectionMode::AccessibleAfterFirstUnlockThisDeviceOnly),
0,
)
.map_err(|e| SeedError::Keychain(format!("failed to build seed access control: {e}")))?;
options.set_access_control(access_control);
options.set_access_synchronized(Some(false));
options.use_protected_keychain();
Ok(options)
}
impl AppleSecureEnclave {
pub fn new(namespace: impl Into<String>) -> Self {
Self {
namespace: namespace.into(),
}
}
fn p256_label(&self) -> String {
format!("{}.p256", self.namespace)
}
fn ed25519_service(&self) -> String {
format!("{}.ed25519", self.namespace)
}
pub fn load_identity(&self) -> Result<Option<P256r1PrivateKey>, Error> {
P256r1PrivateKey::load_from_keychain(&self.p256_label())
}
pub fn delete_identity(&self) -> Result<(), Error> {
match self.load_identity()? {
Some(key) => key.delete(),
None => Ok(()),
}
}
pub async fn store_seed(&self, seed: &[u8; 32]) -> Result<(), SeedError> {
let label = self.p256_label();
let se_public = offload_seed(move || {
let se_private = match P256r1PrivateKey::load_from_keychain(&label)? {
Some(key) => key,
None => return Err(SeedError::IdentityKeyMissing { label }),
};
Ok(se_private.public()?)
})
.await?;
let sealed = seal_32(self.clone(), &se_public, seed)
.await
.map_err(|e| SeedError::Seal(e.to_string()))?;
let service = self.ed25519_service();
offload_seed(move || {
let _ = delete_generic_password_options(seed_password_options(&service)?);
set_generic_password_options(&sealed, seed_password_options(&service)?).map_err(|e| {
SeedError::Keychain(format!("failed to store sealed Ed25519 seed: {e}"))
})
})
.await
}
pub async fn load_seed(&self) -> Result<Option<[u8; 32]>, SeedError> {
let service = self.ed25519_service();
let sealed_bytes =
offload_seed(
move || match generic_password(seed_password_options(&service)?) {
Ok(bytes) => Ok(Some(bytes)),
Err(e) if e.code() == security_framework_sys::base::errSecItemNotFound => {
Ok(None)
}
Err(e) => Err(SeedError::Keychain(format!(
"failed to query sealed Ed25519 seed: {e}"
))),
},
)
.await?;
let Some(sealed_bytes) = sealed_bytes else {
return Ok(None);
};
if sealed_bytes.len() != SEALED_SIZE {
return Err(SeedError::WrongSize {
got: sealed_bytes.len(),
});
}
let mut sealed = [0u8; SEALED_SIZE];
sealed.copy_from_slice(&sealed_bytes);
let label = self.p256_label();
let se_private =
offload_seed(
move || match P256r1PrivateKey::load_from_keychain(&label)? {
Some(key) => Ok(key),
None => Err(SeedError::IdentityKeyMissing { label }),
},
)
.await?;
let opened = open_32(self.clone(), se_private, &sealed)
.await
.map_err(|e| SeedError::Seal(e.to_string()))?;
Ok(Some(opened))
}
pub fn delete_seed(&self) -> Result<(), SeedError> {
let service = self.ed25519_service();
match delete_generic_password_options(seed_password_options(&service)?) {
Ok(()) => Ok(()),
Err(e) if e.code() == security_framework_sys::base::errSecItemNotFound => Ok(()),
Err(e) => Err(SeedError::Keychain(format!(
"failed to delete sealed Ed25519 seed: {e}"
))),
}
}
}
async fn offload<T, F>(op: F) -> Result<T, Error>
where
F: FnOnce() -> Result<T, Error> + Send + 'static,
T: Send + 'static,
{
tokio::task::spawn_blocking(op)
.await
.map_err(|e| Error::Platform(format!("Secure Enclave task failed to join: {e}")))?
}
async fn offload_seed<T, F>(op: F) -> Result<T, SeedError>
where
F: FnOnce() -> Result<T, SeedError> + Send + 'static,
T: Send + 'static,
{
tokio::task::spawn_blocking(op)
.await
.map_err(|e| SeedError::Keychain(format!("seed Keychain task failed to join: {e}")))?
}
impl CryptoKeyProvider<P256> for AppleSecureEnclave {
type Error = Error;
type PrivateKey = P256r1PrivateKey;
fn public_key(&self, key: &Self::PrivateKey) -> Result<P256r1PublicKey, Self::Error> {
key.public()
}
fn generate_static_key(&mut self) -> Result<Self::PrivateKey, Self::Error> {
P256r1PrivateKey::generate_secure_enclave(&self.p256_label())
}
fn generate_ephemeral_key(&mut self) -> Result<Self::PrivateKey, Self::Error> {
P256r1PrivateKey::generate_ephemeral()
}
}
impl CryptoKeyProviderAsync<P256> for AppleSecureEnclave {
async fn generate_static_key_async(&mut self) -> Result<Self::PrivateKey, Self::Error> {
let label = self.p256_label();
offload(move || P256r1PrivateKey::generate_secure_enclave(&label)).await
}
async fn generate_ephemeral_key_async(&mut self) -> Result<Self::PrivateKey, Self::Error> {
offload(P256r1PrivateKey::generate_ephemeral).await
}
}
impl DhProvider<P256> for AppleSecureEnclave {
fn dh(
&self,
key: &Self::PrivateKey,
peer: &P256r1PublicKey,
) -> Result<SharedSecret<32>, Self::Error> {
key.dh(peer)
}
}
impl DhProviderAsync<P256> for AppleSecureEnclave {
async fn dh_async(
&self,
key: &Self::PrivateKey,
peer: &P256r1PublicKey,
) -> Result<SharedSecret<32>, Self::Error> {
let key = key.clone();
let peer = *peer;
offload(move || key.dh(&peer)).await
}
}
impl SigningProvider<P256> for AppleSecureEnclave {
fn sign(&self, key: &Self::PrivateKey, message: &[u8]) -> Result<P256Signature, Self::Error> {
key.sign(message)
}
}
impl SigningProviderAsync<P256> for AppleSecureEnclave {
async fn sign_async(
&self,
key: &Self::PrivateKey,
message: &[u8],
) -> Result<P256Signature, Self::Error> {
let key = key.clone();
let message = message.to_vec();
offload(move || key.sign(&message)).await
}
}
fn apple_ed25519_generate() -> Result<SoftwareEd25519PrivateKey, crate::curve::ed25519::Error> {
let mut seed = [0u8; 32];
let status = unsafe {
security_framework_sys::random::SecRandomCopyBytes(
security_framework_sys::random::kSecRandomDefault,
seed.len(),
seed.as_mut_ptr().cast(),
)
};
if status != 0 {
return Err(crate::curve::ed25519::Error::Platform(format!(
"SecRandomCopyBytes failed with status {status}"
)));
}
if seed.iter().all(|&b| b == 0) {
return Err(crate::curve::ed25519::Error::Platform(
"SecRandomCopyBytes returned an all-zero seed".into(),
));
}
Ok(SoftwareEd25519PrivateKey::from_seed(seed))
}
impl CryptoKeyProvider<Ed25519> for AppleSecureEnclave {
type Error = crate::curve::ed25519::Error;
type PrivateKey = SoftwareEd25519PrivateKey;
fn public_key(&self, key: &Self::PrivateKey) -> Result<Ed25519PublicKey, Self::Error> {
Ok(key.public_key())
}
fn generate_static_key(&mut self) -> Result<Self::PrivateKey, Self::Error> {
apple_ed25519_generate()
}
fn generate_ephemeral_key(&mut self) -> Result<Self::PrivateKey, Self::Error> {
apple_ed25519_generate()
}
}
impl CryptoKeyProviderAsync<Ed25519> for AppleSecureEnclave {
async fn generate_static_key_async(&mut self) -> Result<Self::PrivateKey, Self::Error> {
apple_ed25519_generate()
}
async fn generate_ephemeral_key_async(&mut self) -> Result<Self::PrivateKey, Self::Error> {
apple_ed25519_generate()
}
}
impl DhProvider<Ed25519> for AppleSecureEnclave {
fn dh(
&self,
key: &Self::PrivateKey,
peer: &Ed25519PublicKey,
) -> Result<SharedSecret<32>, Self::Error> {
Ok(key.dh(peer))
}
}
impl DhProviderAsync<Ed25519> for AppleSecureEnclave {
async fn dh_async(
&self,
key: &Self::PrivateKey,
peer: &Ed25519PublicKey,
) -> Result<SharedSecret<32>, Self::Error> {
Ok(key.dh(peer))
}
}
impl SigningProvider<Ed25519> for AppleSecureEnclave {
fn sign(
&self,
key: &Self::PrivateKey,
message: &[u8],
) -> Result<Ed25519Signature, Self::Error> {
Ok(key.sign(message))
}
}
impl SigningProviderAsync<Ed25519> for AppleSecureEnclave {
async fn sign_async(
&self,
key: &Self::PrivateKey,
message: &[u8],
) -> Result<Ed25519Signature, Self::Error> {
Ok(key.sign(message))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::curve::p256::P256r1PrivateKey as SoftwareP256r1PrivateKey;
use crate::provider::ProviderExt;
#[test]
fn generate_signature_ephemeral() {
let sk1 = P256r1PrivateKey::generate_ephemeral().unwrap();
let pk1 = sk1.public().unwrap();
const MSG: &[u8] = b"Hello World";
let signature = sk1.sign(MSG).unwrap();
assert!(pk1.verify(signature, MSG));
let sk2 = P256r1PrivateKey::generate_ephemeral().unwrap();
let pk2 = sk2.public().unwrap();
assert!(!pk2.verify(signature, MSG));
sk1.delete().unwrap();
sk2.delete().unwrap();
}
#[test]
fn generate_dh_ephemerals() {
let sk1 = P256r1PrivateKey::generate_ephemeral().unwrap();
let pk1 = sk1.public().unwrap();
let sk2 = P256r1PrivateKey::generate_ephemeral().unwrap();
let pk2 = sk2.public().unwrap();
let ss1 = sk1.dh(&pk2).unwrap();
let ss2 = sk2.dh(&pk1).unwrap();
assert_eq!(ss1, ss2);
}
#[test]
fn macos_x_software() {
let sk1 = P256r1PrivateKey::generate_ephemeral().unwrap();
let pk1 = sk1.public().unwrap();
let sk2 = SoftwareP256r1PrivateKey::generate(rand::rng()).unwrap();
let pk2 = sk2.public();
let apple_dh = sk1.dh(&pk2).unwrap();
let our_dh = sk2.dh(&pk1).unwrap();
assert_eq!(apple_dh, our_dh);
}
#[tokio::test]
async fn crypto_provider_offloaded_dh_roundtrip() {
let mut provider = AppleSecureEnclave::new("uk.co.example.hiss-test");
let a = CryptoKeyProviderAsync::<P256>::generate_ephemeral_key_async(&mut provider)
.await
.unwrap();
let b = CryptoKeyProviderAsync::<P256>::generate_ephemeral_key_async(&mut provider)
.await
.unwrap();
let a_pub = provider.public(&a).unwrap();
let b_pub = provider.public(&b).unwrap();
let ab = DhProviderAsync::<P256>::dh_async(&provider, &a, &b_pub)
.await
.unwrap();
let ba = DhProviderAsync::<P256>::dh_async(&provider, &b, &a_pub)
.await
.unwrap();
assert_eq!(ab, ba);
let sig = SigningProviderAsync::<P256>::sign_async(&provider, &a, b"hello")
.await
.unwrap();
assert!(a_pub.verify(sig, b"hello"));
}
#[test]
#[ignore = "requires Secure Enclave hardware"]
fn generate_signature_secure_enclave() {
let sk1 = P256r1PrivateKey::generate_secure_enclave_ephemeral().unwrap();
let pk1 = sk1.public().unwrap();
const MSG: &[u8] = b"Hello World";
let signature = sk1.sign(MSG).unwrap();
assert!(pk1.verify(signature, MSG));
let sk2 = P256r1PrivateKey::generate_secure_enclave_ephemeral().unwrap();
let pk2 = sk2.public().unwrap();
assert!(!pk2.verify(signature, MSG));
sk1.delete().unwrap();
sk2.delete().unwrap();
}
#[tokio::test]
#[ignore = "requires codesigned test binary + Secure Enclave hardware"]
async fn ed25519_seed_keychain_round_trip() {
let mut provider = AppleSecureEnclave::new("uk.co.example.hiss-test");
let _identity = provider.generate::<P256>().unwrap();
let seed: [u8; 32] = *SoftwareEd25519PrivateKey::generate(rand::rng()).seed();
assert!(provider.load_seed().await.unwrap().is_none());
provider.store_seed(&seed).await.unwrap();
let loaded = provider
.load_seed()
.await
.unwrap()
.expect("seed present after store");
assert_eq!(loaded, seed, "loaded seed must byte-equal the stored seed");
provider.delete_seed().unwrap();
assert!(provider.load_seed().await.unwrap().is_none());
provider.delete_seed().unwrap();
provider.delete_identity().unwrap();
}
}