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?

Post not yet marked as solved Up vote post of sipi Down vote post of sipi
588 views

Replies

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"

  • Thank you very much Quinn for your replay!

    Regarding your question, I don't have too much insight to the server's code, but until now we always sent self-signed certificates and I guess without making some modifications it works only like this.

    But I'm curios what is in your mind, when you ask about the "certificate that looks like it was issued by a CA"? Do you mean to create a certificate which is shared between the client and the server and to use it to sign our newly generated one?

Add a Comment

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)
  • I think the problem is, that to can query a valid SecIdentity you must have in the keychain a private key added as kSecClassKey and the corresponding certificate added as kSecClassCertificate. While your example shows that your certificate is added in the right way, unfortunately you private key is added as kSecClassGenericPassword (if you followed the example from the mentioned link). The system doesn't know, that the saved kSecClassGenericPassword is actually a private key.

  • The SecureEnclave.P256.Signing.PrivateKey(..) unfortunately doesn't add the key automatically to the keychain. You have to add it manually. The SecKeyCreateRandomKey(...) adds the key to the keychain, protect it through the secure enclave if you pass the right attributes:

    kSecAttrIsPermanent: to save it to the keychain automaticallykSecAttrTokenIDSecureEnclave: to protect it through the secure enclave

    But then as I mentioned you will face problems to convert the keys to the expected types.

  • Exactly, I store it as a GenericPassword. I would like to be able to use SecureEnclave.P256.Signing.PrivateKey(..) for crypto operations (especially Certificates), but it seems it is not possible to create SecKey out of SecureEnclave.P256.Signing.PrivateKey(..) and vice versa.