How to use public key exported from Secure Enclave

I'm trying to send secure message between our server and iOS app. I see the following forum post about how to do this:


https://forums.developer.apple.com/message/84684#84684


except it doesn't work. I even use the sample code in the git repo which also fails key verification:


https://github.com/hfossli/EskimoKeys/tree/master


FWIW, here is my output of running the sample code:


#! /bin/sh
echo 36ead61ad53f77e2222223cbb71dfae17cfd1ad92a86457a6b051dfcce323cc6 | xxd -r -p > digest.dat
echo 304502207c71eb3c78c658d0677c845cfacdb1f9ff3af79269abc34d4028842000316600022100edab9c28e7dd1f9bb041ddf535b9f12c1b0a4d69c43563289a223f493471b746 | xxd -r -p > signature.dat
cat > key.pem <<EOF
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAER9cNNUo+aE5URYaETT9K9clKstFt
+/1X5qP8f2NxDQgP7VBkRArwzRlyltp8rh668JUPwWYKCjdtHqLFA/h3KA==
-----END PUBLIC KEY-----
EOF
openssl dgst -ecdsa-with-SHA1 -verify key.pem -signature signature.dat digest.dat


Runing the script I get the following error message:


"Verification Failure"


Unfortunately, I don't know enough about openssl to figure what the problem is. FWIW I can analyze the public key with the following script:


$ openssl asn1parse -in tempkey.b64 -inform PEM


0:d=0 hl=2 l= 89 cons: SEQUENCE

2:d=1 hl=2 l= 19 cons: SEQUENCE

4:d=2 hl=2 l= 7 prim: OBJECT :id-ecPublicKey

13:d=2 hl=2 l= 8 prim: OBJECT :prime256v1

23:d=1 hl=2 l= 66 prim: BIT STRING


So the public key looks valid, but I can't do the digest verification as the above-mentioned forum post describes.


Any ideas? Thanks.


Doug Hill

Answered by DTS Engineer in 228975022

Since posting about this on that other thread I’ve had a chance to dig into it in depth, even so far as to get it working on the Mac (thanks to my boss for getting me a shiny new Touch Bar MacBook Pro :-). The resulting project is too big to post in its entirety but I can post some snippets.

First, here’s how I set up the dictionary passed to

SecKeyGeneratePair
.
let access = SecAccessControlCreateWithFlags(
    nil,
    kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
    [.touchIDAny, .privateKeyUsage], 
    nil
)!
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,
    ],
]

Notes:

  • This puts the private key on the Secure Enclave but leaves the public key only in memory. I then manually add the public key to the keychain at this point. AFAIK there’s no way to generate both keys in permanent storage (r. 30040961).

  • Of course I only need to add the public key to the keychain if I’m using

    SecItemCopyMatching
    to get the key bits, which isn’t necessary on iOS 10 (see below).

Here’s how I wrap the key in a DER-encoded ASN.1

SubjectPublicKeyInfo
structure:
private static func subjectPublicKeyInfo(for key: SecKey) throws -> Data {
    let keyData = try SecKeyCopyExternalRepresentationCompat(key)
    let keyInfo = try SecKeyGetKeyInfoCompat(key)

    // Create an OID based on the key type and size.

    let algorithm: NanoDER
    switch keyInfo.keyType {
        …
        case kSecAttrKeyTypeEC as NSString as String:
            // These values per RFC 5480.
            // 
            // <http://www.ietf.org/rfc/rfc5480.txt>
            let namedCurve: NanoDER
            switch keyInfo.keySizeInBits {
                case 192: namedCurve = .objectIdentifier([1, 2, 840, 10045, 3, 1,  1])  // secp192r1
                case 224: namedCurve = .objectIdentifier([1, 3, 132, 0, 33])            // secp224r1
                case 256: namedCurve = .objectIdentifier([1, 2, 840, 10045, 3, 1,  7])  // secp256r1
                case 384: namedCurve = .objectIdentifier([1, 3, 132, 0, 34])            // secp384r1
                case 512: namedCurve = .objectIdentifier([1, 3, 132, 0, 35])            // secp521r1
                default:
                    throw NSError(domain: NSOSStatusErrorDomain, code: Int(errSecUnimplemented), userInfo: nil)
            }
            algorithm = .sequence([
                .objectIdentifier([1, 2, 840, 10045, 2, 1]),        // ecPublicKey
                namedCurve 
            ])
        default:
            throw NSError(domain: NSOSStatusErrorDomain, code: Int(errSecUnimplemented), userInfo: nil)
    }

    // Wrap the algorithm and key data in a `SubjectPublicKeyInfo` structure.

    return Data(bytes: NanoDER.sequence([
        algorithm, 
        .bitString([UInt8](keyData), unusedBits: 0)
    ]).bytes)
}

Notes:

  • SecKeyCopyExternalRepresentationCompat
    is a wrapper around
    SecKeyCopyExternalRepresentation
    . It exists to provide iOS 9 compatibility, where it gets the raw key bits via
    SecItemCopyMatching
    .
  • SecKeyGetKeyInfoCompat
    is basically a wrapper around
    SecKeyCopyAttributes
    , again with iOS 9 compatibility using
    SecItemCopyMatching
    .
  • NanoDER
    is a really small DER builder library of my own creation. I’m using it to add the
    SubjectPublicKeyInfo
    header to the front of the key bytes. In the specific case of a Secure Enclave key you can hard code that header, as discussed in that earlier thread you referenced.

With this in place I can sign data like this:

let signature: Data
do {
    signature = try SecKeyCreateSignatureCompat(privateKey, DigestAlgorithm.sha1, dataToSign: dataToSign)
} catch {
    NSLog("signing problem")
    throw error
}

and export the key as a PEM like this:

let keyBase64 = keyData.base64EncodedString(options: [.lineLength64Characters])
lines.append("-----BEGIN PUBLIC KEY-----")
lines.append( contentsOf: keyBase64.components(separatedBy: "\r\n") )
lines.append("-----END PUBLIC KEY-----")

Notes:

  • SecKeyCreateSignatureCompat
    is another compatibility wrapper, bouncing to
    SecKeyCreateSignature
    on iOS 10 and later and
    SecKeyRawSign
    on iOS 9.
  • I’m using SHA-1 to simplify my testing. In a real app you’d want to use something better.

phew

Share and Enjoy

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

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

Since posting about this on that other thread I’ve had a chance to dig into it in depth, even so far as to get it working on the Mac (thanks to my boss for getting me a shiny new Touch Bar MacBook Pro :-). The resulting project is too big to post in its entirety but I can post some snippets.

First, here’s how I set up the dictionary passed to

SecKeyGeneratePair
.
let access = SecAccessControlCreateWithFlags(
    nil,
    kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
    [.touchIDAny, .privateKeyUsage], 
    nil
)!
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,
    ],
]

Notes:

  • This puts the private key on the Secure Enclave but leaves the public key only in memory. I then manually add the public key to the keychain at this point. AFAIK there’s no way to generate both keys in permanent storage (r. 30040961).

  • Of course I only need to add the public key to the keychain if I’m using

    SecItemCopyMatching
    to get the key bits, which isn’t necessary on iOS 10 (see below).

Here’s how I wrap the key in a DER-encoded ASN.1

SubjectPublicKeyInfo
structure:
private static func subjectPublicKeyInfo(for key: SecKey) throws -> Data {
    let keyData = try SecKeyCopyExternalRepresentationCompat(key)
    let keyInfo = try SecKeyGetKeyInfoCompat(key)

    // Create an OID based on the key type and size.

    let algorithm: NanoDER
    switch keyInfo.keyType {
        …
        case kSecAttrKeyTypeEC as NSString as String:
            // These values per RFC 5480.
            // 
            // <http://www.ietf.org/rfc/rfc5480.txt>
            let namedCurve: NanoDER
            switch keyInfo.keySizeInBits {
                case 192: namedCurve = .objectIdentifier([1, 2, 840, 10045, 3, 1,  1])  // secp192r1
                case 224: namedCurve = .objectIdentifier([1, 3, 132, 0, 33])            // secp224r1
                case 256: namedCurve = .objectIdentifier([1, 2, 840, 10045, 3, 1,  7])  // secp256r1
                case 384: namedCurve = .objectIdentifier([1, 3, 132, 0, 34])            // secp384r1
                case 512: namedCurve = .objectIdentifier([1, 3, 132, 0, 35])            // secp521r1
                default:
                    throw NSError(domain: NSOSStatusErrorDomain, code: Int(errSecUnimplemented), userInfo: nil)
            }
            algorithm = .sequence([
                .objectIdentifier([1, 2, 840, 10045, 2, 1]),        // ecPublicKey
                namedCurve 
            ])
        default:
            throw NSError(domain: NSOSStatusErrorDomain, code: Int(errSecUnimplemented), userInfo: nil)
    }

    // Wrap the algorithm and key data in a `SubjectPublicKeyInfo` structure.

    return Data(bytes: NanoDER.sequence([
        algorithm, 
        .bitString([UInt8](keyData), unusedBits: 0)
    ]).bytes)
}

Notes:

  • SecKeyCopyExternalRepresentationCompat
    is a wrapper around
    SecKeyCopyExternalRepresentation
    . It exists to provide iOS 9 compatibility, where it gets the raw key bits via
    SecItemCopyMatching
    .
  • SecKeyGetKeyInfoCompat
    is basically a wrapper around
    SecKeyCopyAttributes
    , again with iOS 9 compatibility using
    SecItemCopyMatching
    .
  • NanoDER
    is a really small DER builder library of my own creation. I’m using it to add the
    SubjectPublicKeyInfo
    header to the front of the key bytes. In the specific case of a Secure Enclave key you can hard code that header, as discussed in that earlier thread you referenced.

With this in place I can sign data like this:

let signature: Data
do {
    signature = try SecKeyCreateSignatureCompat(privateKey, DigestAlgorithm.sha1, dataToSign: dataToSign)
} catch {
    NSLog("signing problem")
    throw error
}

and export the key as a PEM like this:

let keyBase64 = keyData.base64EncodedString(options: [.lineLength64Characters])
lines.append("-----BEGIN PUBLIC KEY-----")
lines.append( contentsOf: keyBase64.components(separatedBy: "\r\n") )
lines.append("-----END PUBLIC KEY-----")

Notes:

  • SecKeyCreateSignatureCompat
    is another compatibility wrapper, bouncing to
    SecKeyCreateSignature
    on iOS 10 and later and
    SecKeyRawSign
    on iOS 9.
  • I’m using SHA-1 to simplify my testing. In a real app you’d want to use something better.

phew

Share and Enjoy

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

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

Hello Quinn!


Thanks so much for walking through this code.


I think I now realize the reason the signature verification was failing for me with the previous sample code I was using. The digest was created with SHA-256, whereas you are using SHA-1.


Just out of curiosity, is it possible to use SHA-256? Given that SHA-1 has been deprecated, it would be great to use the newer hash function. I'm guessing this depends on openssl being able to handle SHA-256 digests with EC keys.


Will try this out some more and let you know how it goes.


Doug Hill

Ok, I changed the digest algorithm in your latest sample code to SHA-256 and the signature is verified successfully on my Debian box. I guess there were other issues with previous sample code.


Thanks again.


Doug Hill

Just out of curiosity, is it possible to use SHA-256?

Yes.

Given that SHA-1 has been deprecated, it would be great to use the newer hash function.

Agreed.

I'm guessing this depends on openssl being able to handle SHA-256 digests with EC keys.

Right. Unfortunately the OpenSSL on macOS is so old that it doesn’t support EC with SHA-256. Tweaking the code to output SHA-256 isn’t hard, but that makes testing less straightforward. Personally I have Ubuntu in a VM for testing this sort of thing. It possible to update your Mac’s OpenSSL (via Homebrew or whatever) but I prefer to keep my Mac as vanilla as possible to avoid misleading the folks I support.

Share and Enjoy

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

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

Could you share NanoDER source code?

No, sorry.

Share and Enjoy

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

How to use public key exported from Secure Enclave
 
 
Q