Secure enclave protected private key in CryptoKit vs Security framework

My goal is:

  • Generate a public and private key pair
  • Add the private key to the keychain and protect it with the secure enclave
  • Create a self-signed certificate with the public key and send it to a server
  • Add the certificate to the keychain
  • When I communicate with the server I want to create a SecIdentity during the client challenge which is basically a SecCertificate + SecKey combo.

For the certificate generation I would like to use the swift-certificates library to not have to compose manually the certificate fields and signature.

My problem is that the swift-certificates during the Certificate initialisation needs a SecureEnclave.P256.Signing.PrivateKey private key and to add a key to the keychain we need a SecKey object. And unfortunately there is no clean way to create from one of them the other one. I read several threads here about this, but I haven't found a clean solution for it.

I tried to approach the problem from two directions:

First:

  • Create the key with the SecKeyCreateRandomKey, mark in the attributes that I want to protect the key with secure enclave and also mark that I want the private key to be kSecAttrIsPermanent so it is automatically saved in the keychain
  • The SecKeyCreateRandomKey returns a SecKey which is a reference to the private key from the keychain
  • (!) Unfortunately I haven't found a clean way to convert a SecKey to a -> SecureEnclave.P256.Signing.PrivateKey
  • There is a workaround to SecKeyCopyAttributes of the private key and to extract the bytes from the attributes["toid"], but I guess it's not safe to use an undocumented key ("toid") if there is no constant defined to it (the name could be changed in future releases)

Second approach:

  • Create a SecureEnclave.P256.Signing.PrivateKey
  • Create the Certificate using the swift-certificates
  • The created private key is protected by the secure enclave but it's not added automatically to the keychain so we should add it to can query after that the SecIdentity
  • (!) Unfortunately I haven't found a way to convert the SecureEnclave.P256.Signing.PrivateKey to -> SecKey.
  • There are threads which say that the SecKeyCreateWithData(...) helps us, but unfortunately if we set the kSecAttrTokenIDSecureEnclave in the attribute dictionary, the method creates a brand new key for us, regardless the passed data. So the initial key will never be the same as the newly created SecKey. This we can see in the method's implementation.

So I got stuck with both approaches because seemingly there is no clean way to switch between SecureEnclave.P256.Signing.PrivateKey and SecKey.

One solution would be to compose manually the certificate, without swift-certificates because like that we would not need a SecureEnclave.P256.Signing.PrivateKey object. But I would like to avoid the manual composition and signature calculation...

Anybody has any idea?

the initial key will never be the same as the newly created SecKey.

Indeed. I just tried this here in my office. This code prints true:

let priv = P256.Signing.PrivateKey()
let privData = priv.x963Representation
let privRef = try secCall { SecKeyCreateWithData(privData as NSData, [
    kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
    kSecAttrKeyClass: kSecAttrKeyClassPrivate,
    kSecAttrKeySizeInBits: 256,
] as NSDictionary, $0) }

let pubRef = try secCall { SecKeyCopyPublicKey(privRef) }

let pubData = priv.publicKey.x963Representation
let pubRefData = try secCall { SecKeyCopyExternalRepresentation(pubRef, $0) } as Data

print(pubData == pubRefData)

but if you switch it over to an SE-protected key it prints false:

func testSE() throws {
    let priv = try SecureEnclave.P256.Signing.PrivateKey()
    let privData = priv.dataRepresentation
    let privRef = try secCall { SecKeyCreateWithData(privData as NSData, [
        kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
        kSecAttrKeyClass: kSecAttrKeyClassPrivate,
        kSecAttrKeySizeInBits: 256,
        kSecAttrTokenID: kSecAttrTokenIDSecureEnclave,
    ] as NSDictionary, $0) }

    let pubRef = try secCall { SecKeyCopyPublicKey(privRef) }
    
    let pubData = priv.publicKey.x963Representation
    let pubRefData = try secCall { SecKeyCopyExternalRepresentation(pubRef, $0) } as Data
    
    print(pubData == pubRefData)
}

Bummer.

A different developer pointed this out in another thread but I missed it )-:

Double bummer.

I encourage you to file an enhancement request for an API to make this work. Please post your bug number, just for the record.


Coming back to your big picture question, you’re only boxed into this corner because your certificate is self-signed. Does that have to be the case? Will your server accept a certificate that looks like it was issued by a CA?

Share and Enjoy

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

We are having similar challenge. It seems, that something ist missing in the API.

Use Case

  • Only registered App-Instances are allowed to connect to the API (we use DCAppAttest durcing the registration, out of scope here)
  • Communication between App and the API is authenticated using mutual TLS
  • The private Key is held in the Secure Enclave and is not transferable
  • Certificate ist issued by the Backend API upon the successful registration

Implementation

Keypair is generated in the Secure Enclave:

var error: Unmanaged<CFError>? = nil;
guard let accessControl = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, [.privateKeyUsage], &error) else {
      throw error!.takeRetainedValue() as Error
}

self.privateKey = try SecureEnclave.P256.Signing.PrivateKey.init(
    accessControl: accessControl
);

Certificate Signing Request ist created using CryptoKit Certificates:

let privateKeyCertificate = Certificate.PrivateKey(privateKey)
let attributes = CertificateSigningRequest.Attributes()
let csr = try CertificateSigningRequest(version: .v1, subject: subject, privateKey: privateKeyCertificate, attributes: attributes, signatureAlgorithm: .ecdsaWithSHA256)

let csrAsPEM = try csr.serializeAsPEM(discriminator: CertificateSigningRequest.defaultPEMDiscriminator).pemString

CSR is then sent to Backend für validation, if everything checks up server signs the CSR and returns the certificate.

We then store the SecureEnclave.P256.Signing.PrivateKey in the Keychain as described here: Storing CryptoKit Keys in the Keychain using GenericPasswordConvertible.

The Certificate is also stored in the Keychain:

let addQuery: [String: Any] = [
    kSecClass as String: kSecClassCertificate,
    kSecValueRef as String: cert,
    kSecAttrLabel as String: "My Certificate"
]
let status = SecItemAdd(addQuery as CFDictionary, nil)

We are stuck in the last Step: retrieve the SecIdentity from the KeyChain. We get Error -25300, no matter what. Lates query is like this:

let query: [String: Any] = [
  kSecClass as String: kSecClassIdentity,
  kSecReturnData  as String: kCFBooleanTrue,
  kSecReturnAttributes as String: kCFBooleanTrue,
  kSecReturnRef as String: kCFBooleanTrue,
  kSecMatchLimit as String: kSecMatchLimitAll
]
               
var identityItem: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &identityItem)
Secure enclave protected private key in CryptoKit vs Security framework
 
 
Q