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
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 }