CryptoKit and Security Framework (ECIES) interoperability

My end goal is to use eciesEncryptionCofactorX963SHA256AESGCM with a key generated on the Secure Enclave using CryptoKit, that requires Biometric Authentication.

CryptoKit does not implement the ECIES encryption algorithms, so my goal was to fall back to the Security framework.

The public key can be easily converted to a SecKey because it implements x963Representation which can then be imported as follows:

let enclaveSecKey: SecKey = SecKeyCreateWithData(enclaveKey.x963Representation as CFData, [
    kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
    kSecAttrKeyClass: kSecAttrKeyClassPublic,
    kSecAttrKeySizeInBits: 256
] as [String: Any] as CFDictionary, nil),

I have everything working except the code to decrypt with the private key. Naturally, the Secure Enclave does not expose the private key - as is its design - rather some kind of token?

I did read the Keychain documentation which notes that it is not possible to simply obtain an x963Representation of the private key (as it's a custom representation returned by the Secure Enclave).

However, my ultimate question is this: can one convert the Secure Enclave representation into something that can be used as a SecKey for encryption/decryption (without necessarily being stored in the Keychain - i.e., 'correct') as it seems both CryptoKit and Security have a means of representing the private key token returned by the Secure Enclave? (Or is one's only recourse to use the Security framework for generating and storing the keys too?)


I have also tried this code to create a SecKey representation, having retrieved the GenericPasswordConvertible out of the keychain (note the use of kSecAttrTokenID: kSecAttrTokenIDSecureEnclave) with the aforementioned goal of loading the Secure Enclave's private token as a SecKey:

let enclaveSecKey: SecKey = SecKeyCreateWithData(enclaveKey.rawRepresentation as CFData, [
    kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
    kSecAttrKeyClass: kSecAttrKeyClassPrivate,
    kSecAttrTokenID: kSecAttrTokenIDSecureEnclave,
    kSecAttrAccessible: kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
    kSecUseAuthenticationContext: try await createAuthContext(
        reason: "Decrypt data",
        fallbackTitle: "Enter your device password to decrypt data",
        mustEvaluate: true
    ),
    kSecAttrIsPermanent: true,
    kSecAttrIsExtractable: false,
    kSecAttrSynchronizable: false,
    kSecAttrKeySizeInBits: 256,
    kSecAttrAccessControl: SecAccessControlCreateWithFlags(
        nil,
        kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
        [.biometryAny, .privateKeyUsage],
        &cfSecKeyCreateError
    )!
] as [String: Any] as CFDictionary, nil)

This works, in and of itself, (i.e., it loads without error and cfSecKeyCreateError is nil, however when I try SecSecKeyCopyPublicKey I get a different, incorrect public key and - naturally, I suppose - if I attempt to decrypt data with the private key that fails with:

Optional(Swift.Unmanaged<__C.CFErrorRef>(_value: Error Domain=NSOSStatusErrorDomain Code=-50 "ECIES: Failed to aes-gcm decrypt data (err -69)" UserInfo={numberOfErrorsDeep=0, NSDescription=ECIES: Failed to aes-gcm decrypt data (err -69)}))
Answered by DTS Engineer in 751011022

can one convert the Secure Enclave representation into something that can be used as a SecKey for encryption/decryption

The following Works on My Machine™:

let ck = try SecureEnclave.P256.Signing.PrivateKey()
let sf = try secCall { SecKeyCreateWithData(ck.dataRepresentation as NSData, [
    kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
    kSecAttrKeyClass: kSecAttrKeyClassPrivate,
    kSecAttrTokenID: kSecAttrTokenIDSecureEnclave,
] as NSDictionary, $0) }
print(sf)
// <SecKeyRef:('com.apple.setoken') 0x60000266ed40>

Oh, that’s macOS 13.2.1 btw, but it should work on any SE-capable device.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Accepted Answer

can one convert the Secure Enclave representation into something that can be used as a SecKey for encryption/decryption

The following Works on My Machine™:

let ck = try SecureEnclave.P256.Signing.PrivateKey()
let sf = try secCall { SecKeyCreateWithData(ck.dataRepresentation as NSData, [
    kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
    kSecAttrKeyClass: kSecAttrKeyClassPrivate,
    kSecAttrTokenID: kSecAttrTokenIDSecureEnclave,
] as NSDictionary, $0) }
print(sf)
// <SecKeyRef:('com.apple.setoken') 0x60000266ed40>

Oh, that’s macOS 13.2.1 btw, but it should work on any SE-capable device.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thanks Quinn!

I'm a little baffled as I'm certain I tried this already but it does indeed work now, and even when I re-add the code to do biometrics in a Playground App.

I guess it was just starting over with a minimal recreation...

I wanted to follow up because I am having the same issue as the OP, and am able to successfully call SecKeyCreateWithData without error and receive a SecKeyRef. For my case, SecKeyCreateWithData seems to return different public/private keys for the same ck.dataRepresentation when called multiple times. For example, the following code block returns two different keys, and outputs different values:

        let ck = try SecureEnclave.P256.Signing.PrivateKey()
        
        let sf1 = try secCall { SecKeyCreateWithData(ck.dataRepresentation as NSData, [
            kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
            kSecAttrKeyClass: kSecAttrKeyClassPrivate,
            kSecAttrTokenID: kSecAttrTokenIDSecureEnclave,
        ] as NSDictionary, $0) }
        let publicKey1 = SecKeyCopyPublicKey(sf1)!
        
        let sf2 = try secCall { SecKeyCreateWithData(ck.dataRepresentation as NSData, [
            kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
            kSecAttrKeyClass: kSecAttrKeyClassPrivate,
            kSecAttrTokenID: kSecAttrTokenIDSecureEnclave,
        ] as NSDictionary, $0) }
        let publicKey2 = SecKeyCopyPublicKey(sf2)!

        print("Public1:\(publicKey1)")
        print("Public2:\(publicKey2)")

Any ideas for why this may be the case?

SecKeyCreateWithData seems to return different public/private keys for the same ck.dataRepresentation when called multiple times.

Why do you think that’s strange?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thanks for the response Quinn! If I use the same code to generate a private key without the Secure Element, calls to SecKeyCreateWithData (using x963Representation as the import data) result in the same public/private keypair being created, even when called multiple times. My understanding is SecureElement.P256.Signing.PrivateKey.dataRepresentation is some key data entangled/scrambled by the secure element, and calling SecKeyCreateWithData("samedata") would return a SecKeyRef that referenced the same underlying public/private key.

        let ck = P256.Signing.PrivateKey()
        
        let sf1 = try secCall { SecKeyCreateWithData(ck.x963Representation as NSData, [
            kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
            kSecAttrKeyClass: kSecAttrKeyClassPrivate,
        ] as NSDictionary, $0) }
        let publicKey1 = SecKeyCopyPublicKey(sf1)!
        
        let sf2 = try secCall { SecKeyCreateWithData(ck.x963Representation as NSData, [
            kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
            kSecAttrKeyClass: kSecAttrKeyClassPrivate,
        ] as NSDictionary, $0) }
        let publicKey2 = SecKeyCopyPublicKey(sf2)!

        print("Public1:\(publicKey1)")
        print("Public2:\(publicKey2)")

Having done some experimentation it seems that SecKeyCreateWithData simply creates a new key when kSecAttrTokenID: kSecAttrTokenIDSecureEnclave is passed - presumably because it causes the Secure Enclave to generate a new key and the Secure Enclave ignores the associated data.

This is interesting because the docs seem to infer some ability to restore from the dataRepresentation: https://developer.apple.com/documentation/cryptokit/storing_cryptokit_keys_in_the_keychain

"Keys that you store in the Secure Enclave expose a raw representation as well, but in this case the data isn’t the raw key. Instead, the Secure Enclave exports an encrypted block that only the same Secure Enclave can later use to restore the key. You can adopt the same convertibility protocol to store the Secure Enclave’s encrypted data in the keychain as a generic password, and later allow the Secure Enclave to reconstruct the key on the same device"

(emphasis mine)

Thanks all for the responses.

That’s what I had assumed too, but the technical feasibility to do so doesn’t necessarily mean that SecKeyCreateWithData will act as expected. I had initially assumed it was creating a key with the specified representation but I’ve tried creating it with bogus NSData and it still yields new keys so I now suspect that it simply generates new ones.

Intuitively, the API called by CreateWithData would presumably be a key creation API where it specifies the additional data to use with the key (e.g., an existing x963 representation) but evidently this value must be ignored by the Secure Enclave because it does not appear to generate compatible keys.

So, whilst I suspect it is technically feasible to convert between the representation, I am back to thinking that there is not actually a way to do it with available APIs.

Of course, this is all just my understanding.

Luckily SecKeyCreateWithData is available to inspect via open source releases, and your suspicion is confirmed: keyData is ignored when tokenID != NULL. Source: https://github.com/apple-oss-distributions/Security/blob/e4ea024c9bbd3bfda30ec6df270bfb4c7438d1a9/OSX/sec/Security/SecKey.m#L1352-L1358

With this in mind, the accepted reply is not actually 'exchanging' a SecureEnclave.P256.Signing.PrivateKey for a SecKey, it is creating a SecKey instance backed by a different private key, which I do not believe is intended.

I was able to use SecKeyCreateWithData to reliably recreate a SE-backed private key after suspecting the parameters arg passed to SecKeyCreateCTKKey likely contained the necessary information to reconstruct the key. I inspected the output of SecKeyCopyAttributes and found a value that looked very much like the CryptoKit dataRepresentation associated with key toid. This maps to kSecAttrTokenOID (Not exported in iOS headers for some reason), and replaces the now deprecated kSecAttrSecureEnclaveKeyBlob. The following code should satisfy the ask in the OP:

let pk = try SecureEnclave.P256.Signing.PrivateKey()
let sf = try secCall { SecKeyCreateWithData(Data.init() as NSData, [
    kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
    kSecAttrKeyClass: kSecAttrKeyClassPrivate,
    kSecAttrTokenID: kSecAttrTokenIDSecureEnclave,
    "toid": pk.dataRepresentation,
] as NSDictionary, $0) }

// verify results
print(pk.publicKey.x963Representation as NSData)
print(SecKeyCopyExternalRepresentation(SecKeyCopyPublicKey(sf)!, nil)! as NSData)

That the method must be called in this way feels like a mistake in the implementation of SecKeyCreateWithData when a tokenID is present. If I were able, I would submit a patch to use actually use the passed keyData:

From 1965be6fcf05ba02f141a20c0501cdeff0c76cd2 Mon Sep 17 00:00:00 2001
From: Judson Stephenson <Jud@users.noreply.github.com>
Date: Wed, 14 Jun 2023 00:32:30 -0500
Subject: [PATCH] Use keyData when SecKeyCreateWithData is called with a
 tokenID

---
 OSX/sec/Security/SecKey.m | 1 +
 1 file changed, 1 insertion(+)

diff --git a/OSX/sec/Security/SecKey.m b/OSX/sec/Security/SecKey.m
index 55bbaa9e..02bdb853 100644
--- a/OSX/sec/Security/SecKey.m
+++ b/OSX/sec/Security/SecKey.m
@@ -1350,6 +1350,7 @@ SecKeyRef SecKeyCreateWithData(CFDataRef keyData, CFDictionaryRef parameters, CF
 
     CFStringRef tokenID = CFDictionaryGetValue(parameters, kSecAttrTokenID);
     if (tokenID != NULL) {
+        CFDictionarySetValue(parameters, kSecAttrTokenOID, keyData);
         key = SecKeyCreateCTKKey(allocator, parameters, error);
         if (key == NULL) {
             os_log_debug(SECKEY_LOG, "Failed to create key for tokenID=%{public}@: %{public}@", tokenID, error ? *error : NULL);

This is interesting because the docs seem to infer

To quote a certain bowl of petunias: “Oh no, not again.”

The doc you referenced is misleading, even in its title. The Secure Enclave does not store keys [1], it protects them. The keys aren’t physically resident on the SE. Rather, the SE generates the key, wraps it way that only it can unwrap, and passes the wrapped key back to you.

This “stored on the SE” is a common misconception, largely created by Apple’s own documentation )-: For example, we have a document that’s now called Protecting keys with the Secure Enclave but used to be called Storing Keys in the Secure Enclave [2]. I filed a bug to get that fixed (r. 83612073), but I seem to have missed the sample code you referenced [3].

Given the above clarification, I recommend that you rethink your position on this issue and then post back if you still think something is broken.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] Well, there are keys stored on the SE, but I’m talking about the keys that you can work with, using Apple CryptoKit or the Security framework.

[2] In fact, you can access the new doc via a redirect from the old doc’s URL:

<https://developer.apple.com/documentation/security/certificate_key_and_trust_services/keys/storing_keys_in_the_secure_enclave>

[3] Not any more though. I’ve filed another bug against the Apple CryptoKit docs (r. 110763300).

Thanks for taking the time to respond again Quinn. I'm under no impression the secure enclave is storing keys (the rest of the quoted sentence is "some ability to restore from the dataRepresentation) but I realize this misconception must continually come up and appreciate you clearing it up for anyone who may be confused.

I'm not sure if my latest post was approved when you responded, but I believe the accepted reply, and the sample code posted, could also cause confusion as the linked apple-oss-distribution shows that keyData is unused when calling SecKeyCreateWithData with kSecAttrTokenID set, so the passed ck.dataRepresentation is not having any effect.

My previous post above lays out the reason, a workaround, and a patch for the Security framework to have more intuitive behavior when creating keys from the CryptoKit dataRepresentation. Hopefully it helps anyone in need!

CryptoKit and Security Framework (ECIES) interoperability
 
 
Q