Can't generate keypair through Secure Enclave on macOS

Hello,
I'm trying to generate a keypair using Secure Enclave feature on a Macbook Pro with a Touch Bar (macOS 10.13.6). I followed this doc: https://developer.apple.com/documentation/security/certificate_key_and_trust_services/keys/storing_keys_in_the_secure_enclave?language=objc
The following code works perfectly on my iPhone 7 Plus (iOS 11.4.1) but not on my MBP:


+ (bool) generateKeypairWithinEnclave:(NSData*) keyID {
CFErrorRef error = NULL;
SecAccessControlRef access = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
kSecAccessControlPrivateKeyUsage,
&error);
if (error) {
NSError *err = CFBridgingRelease(error);
NSLog(@"Error during access control flags creation: %@\n", err);
return false;
}
NSDictionary* attributes =
@{ (id)kSecAttrKeyType: (id)kSecAttrKeyTypeECSECPrimeRandom,
(id)kSecAttrKeySizeInBits: @256,
(id)kSecAttrTokenID: (id)kSecAttrTokenIDSecureEnclave,
(id)kSecPrivateKeyAttrs:
@{ (id)kSecAttrIsPermanent: @YES,
(id)kSecAttrApplicationTag: keyID,
(id)kSecAttrLabel: @"Enclave test",
(id)kSecAttrAccessControl: (__bridge id)access,
},
};
SecKeyRef privateKeyRef = SecKeyCreateRandomKey((__bridge CFDictionaryRef)attributes, &error);
if (!privateKeyRef) {
NSError *err = CFBridgingRelease(error);
NSLog(@"Error during secure enclave key registering: %@\n", err);
return false;
}
NSLog(@"Key with id %@ successfully generated within the enclave\n", keyID);
return true;
}


I always get this error:
"Error Domain=NSOSStatusErrorDomain Code=-50 "failed to generate asymmetric keypair" (paramErr: error in user parameter list) UserInfo={NSDescription=failed to generate asymmetric keypair}"
Does anybody have a solution ?
Thanks

Answered by DTS Engineer in 328134022

I already tried to sign an app containing only the code above (and a main) with: a Mac Developer, a Mac App Distribution and a Developer ID Application certificates.

Thanks for confirming that. Given this I ran an old test project that exercises this stuff here in my office, and it also fails with

errSecParam
(-50). However, I know it used to work, so clearly something weird is happening. After some digging I uncovered the problem.

For the iOS-style keychain to work your app must have an application identifier entitlement (this is

com.apple.application-identifier
on the Mac, or just plain
application-identifier
on iOS-based platforms). This allows the keychain to identify your code reliably.

This requirement isn’t a problem on iOS because all iOS apps have that entitlement (occasionally we see it cause problems on the iOS Simulator, when Xcode and the iOS Simulator’s understanding of how things work gets out of whack, but those usually get fixed pretty quickly). On macOS, however, things are more nuanced. macOS has a long history of not using this entitlement (indeed, of not using code signing at all!), so you have to explicitly opt in to it.

Xcode 8 and 9 differ in how they handle this, which is why my project that used to work no longer does.

I confirm that this is the problem by dumping the entitlements of my built binary:

$ codesign -d --entitlements :- TouchIDToOpenSSLMac.app
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.get-task-allow</key>
<true/>
</dict>
</plist>

To fix this you have to force Xcode to generate this entitlement. The trick is to enable some other feature that requires this entitlement. The one I chose was keychain sharing. I went to the Keychain Sharing slice in the Capabilities editor and enabled it, then removed all the items from the Keychain Groups list. This resulted in entitlements like this:

$ codesign -d --entitlements :- TouchIDToOpenSSLMac.app
<plist version="1.0">
<dict>
<key>com.apple.application-identifier</key>
<string>XXXXXXXXXX.com.example.apple-samplecode.eskimo1.TouchIDToOpenSSLMac</string>
<key>com.apple.developer.team-identifier</key>
<string>XXXXXXXXXX</string>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.get-task-allow</key>
<true/>
<key>keychain-access-groups</key>
<array/>
</dict>
</plist>

With that, my Secure Enclave code started working again. Yay!

And speaking of code, pasted in below is the code I used for this test.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
func SecKeyGeneratePair(params: KeyParams, appTagPrefix: String) throws -> (publicKey: SecKey, privateKey: SecKey) {
let appTag = "\(appTagPrefix)\(UUID().uuidString)".data(using: .utf8)!
let paramsDict: [String:Any]
switch params {
case .rsa(let keySizeInBits):
paramsDict = [
kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
kSecAttrKeySizeInBits as String: keySizeInBits,
kSecAttrApplicationTag as String: appTag,
kSecAttrIsPermanent as String: true
]
case .ec(let keySizeInBits):
paramsDict = [
kSecAttrKeyType as String: kSecAttrKeyTypeEC,
kSecAttrKeySizeInBits as String: keySizeInBits,
kSecAttrApplicationTag as String: appTag,
kSecAttrIsPermanent as String: true
]
case .onSecureEnclave:
let access = SecAccessControlCreateWithFlags(
nil,
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
[.touchIDAny, .privateKeyUsage],
nil
)!
// For reasons that are not clear <rdar://problem/30040961>, you can't set
// `kSecAttrIsPermanent` at either the top level or in the `kSecPublicKeyAttrs`
// sub-dictionary when generating a key on the Secure Enclave. So, we have to
// deal with adding the public key to the keychain after the fact.
paramsDict = [
kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
kSecAttrKeyType as String: kSecAttrKeyTypeEC,
kSecAttrKeySizeInBits as String: 256,
kSecAttrApplicationTag as String: appTag,
kSecPrivateKeyAttrs as String: [
kSecAttrIsPermanent as String: true,
kSecAttrAccessControl as String: access,
],
]
}
var publicKey: SecKey? = nil
var privateKey: SecKey? = nil
let err = SecKeyGeneratePair(paramsDict as NSDictionary, &publicKey, &privateKey)
guard err == errSecSuccess else {
throw NSError(domain: NSOSStatusErrorDomain, code: Int(err), userInfo: nil)
}
let result = (publicKey: publicKey!, privateKey: privateKey!)
if case .onSecureEnclave = params {
// Add the public key to the normal keychain, as discussed above.
//
// Note that, if we get an error adding this key, we bail out with an orphaned private
// key in the Secure Enclave. +++ clean it up
let addErr = SecItemAdd([
kSecClass as String: kSecClassKey,
kSecValueRef as String: result.publicKey,
kSecAttrApplicationTag as String: appTag
] as NSDictionary, nil)
// let addErr = errSecParam
guard addErr == errSecSuccess else {
throw NSError(domain: NSOSStatusErrorDomain, code: Int(err), userInfo: nil)
}
}
return result
}
enum KeyParams {
case rsa(keySizeInBits: Int)
case ec(keySizeInBits: Int)
case onSecureEnclave
}

How is your app signed? To use the Secure Enclave you must have access to the iOS-style keychain (see this post) and that requires you to be signed appropriately. For the moment I recommend that you create a small test app that’s signed for Mac App Store development and see if that fixes things.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

I already tried to sign an app containing only the code above (and a main) with: a Mac Developer, a Mac App Distribution and a Developer ID Application certificates.


I get the same error message each time. Do I need to do something else to access to the "iOS-style keychain"?
In the other thread that you linked, you said "iOS style (...) It also kicks in on macOS when you use iCloud Keychain", does that mean I need to use the kSecAttrSynchronizable attributein order to use the Secure Enclave keypair generation?

Accepted Answer

I already tried to sign an app containing only the code above (and a main) with: a Mac Developer, a Mac App Distribution and a Developer ID Application certificates.

Thanks for confirming that. Given this I ran an old test project that exercises this stuff here in my office, and it also fails with

errSecParam
(-50). However, I know it used to work, so clearly something weird is happening. After some digging I uncovered the problem.

For the iOS-style keychain to work your app must have an application identifier entitlement (this is

com.apple.application-identifier
on the Mac, or just plain
application-identifier
on iOS-based platforms). This allows the keychain to identify your code reliably.

This requirement isn’t a problem on iOS because all iOS apps have that entitlement (occasionally we see it cause problems on the iOS Simulator, when Xcode and the iOS Simulator’s understanding of how things work gets out of whack, but those usually get fixed pretty quickly). On macOS, however, things are more nuanced. macOS has a long history of not using this entitlement (indeed, of not using code signing at all!), so you have to explicitly opt in to it.

Xcode 8 and 9 differ in how they handle this, which is why my project that used to work no longer does.

I confirm that this is the problem by dumping the entitlements of my built binary:

$ codesign -d --entitlements :- TouchIDToOpenSSLMac.app
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.get-task-allow</key>
<true/>
</dict>
</plist>

To fix this you have to force Xcode to generate this entitlement. The trick is to enable some other feature that requires this entitlement. The one I chose was keychain sharing. I went to the Keychain Sharing slice in the Capabilities editor and enabled it, then removed all the items from the Keychain Groups list. This resulted in entitlements like this:

$ codesign -d --entitlements :- TouchIDToOpenSSLMac.app
<plist version="1.0">
<dict>
<key>com.apple.application-identifier</key>
<string>XXXXXXXXXX.com.example.apple-samplecode.eskimo1.TouchIDToOpenSSLMac</string>
<key>com.apple.developer.team-identifier</key>
<string>XXXXXXXXXX</string>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.get-task-allow</key>
<true/>
<key>keychain-access-groups</key>
<array/>
</dict>
</plist>

With that, my Secure Enclave code started working again. Yay!

And speaking of code, pasted in below is the code I used for this test.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
func SecKeyGeneratePair(params: KeyParams, appTagPrefix: String) throws -> (publicKey: SecKey, privateKey: SecKey) {
let appTag = "\(appTagPrefix)\(UUID().uuidString)".data(using: .utf8)!
let paramsDict: [String:Any]
switch params {
case .rsa(let keySizeInBits):
paramsDict = [
kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
kSecAttrKeySizeInBits as String: keySizeInBits,
kSecAttrApplicationTag as String: appTag,
kSecAttrIsPermanent as String: true
]
case .ec(let keySizeInBits):
paramsDict = [
kSecAttrKeyType as String: kSecAttrKeyTypeEC,
kSecAttrKeySizeInBits as String: keySizeInBits,
kSecAttrApplicationTag as String: appTag,
kSecAttrIsPermanent as String: true
]
case .onSecureEnclave:
let access = SecAccessControlCreateWithFlags(
nil,
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
[.touchIDAny, .privateKeyUsage],
nil
)!
// For reasons that are not clear <rdar://problem/30040961>, you can't set
// `kSecAttrIsPermanent` at either the top level or in the `kSecPublicKeyAttrs`
// sub-dictionary when generating a key on the Secure Enclave. So, we have to
// deal with adding the public key to the keychain after the fact.
paramsDict = [
kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
kSecAttrKeyType as String: kSecAttrKeyTypeEC,
kSecAttrKeySizeInBits as String: 256,
kSecAttrApplicationTag as String: appTag,
kSecPrivateKeyAttrs as String: [
kSecAttrIsPermanent as String: true,
kSecAttrAccessControl as String: access,
],
]
}
var publicKey: SecKey? = nil
var privateKey: SecKey? = nil
let err = SecKeyGeneratePair(paramsDict as NSDictionary, &publicKey, &privateKey)
guard err == errSecSuccess else {
throw NSError(domain: NSOSStatusErrorDomain, code: Int(err), userInfo: nil)
}
let result = (publicKey: publicKey!, privateKey: privateKey!)
if case .onSecureEnclave = params {
// Add the public key to the normal keychain, as discussed above.
//
// Note that, if we get an error adding this key, we bail out with an orphaned private
// key in the Secure Enclave. +++ clean it up
let addErr = SecItemAdd([
kSecClass as String: kSecClassKey,
kSecValueRef as String: result.publicKey,
kSecAttrApplicationTag as String: appTag
] as NSDictionary, nil)
// let addErr = errSecParam
guard addErr == errSecSuccess else {
throw NSError(domain: NSOSStatusErrorDomain, code: Int(err), userInfo: nil)
}
}
return result
}
enum KeyParams {
case rsa(keySizeInBits: Int)
case ec(keySizeInBits: Int)
case onSecureEnclave
}

It worked too!
It could be a good idea to add this info to the Secure Enclave doc page.
Thanks

It worked too!

Excellent news.

It could be a good idea to add this info to the Secure Enclave doc page.

I’m pretty sure you know what to do about that (-:

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
I’m pretty sure you know what to do about that (-:

Yes! Bug report 43791027 filed.

Thanks again 🙂

Is this <key>keychain-access-groups</key> really require? Or only <key>com.apple.application-identifier</key> <key>com.apple.developer.team-identifier</key>

enough?

I have Mac App, which fails on creating random key if I do not add <key>keychain-access-groups</key> with some values.

But in my demo app, it works without adding <key>keychain-access-groups</key>.

Any suggestion would be helpful

Can't generate keypair through Secure Enclave on macOS
 
 
Q