Can't export EC kSecAttrTokenIDSecureEnclave public key

Hi all,


Using iOS 9 beta 2, I'm trying to export an elliptic curve public key that was generated with kSecAttrTokenIDSecureEnclave and kSecAccessControlPrivateKeyUsage but I am having a few issues.


First, I can't specify kSecAttrIsPermanent for kSecPublicKeyAttrs or SecKeyGeneratePair() fails. I guess that makes sense because kSecAttrTokenIDSecureEnclave is specified for the entire SecKeyGeneratePair() operation (it fails if I put it under kSecPrivateKeyAttrs?) and there is no reason to save an elliptic curve public key with Secure Enclave protection. But this means that later looking up the elliptic curve public key with SecItemCopyMatching and kSecReturnData fail, so there doesn't seem to be a way to get the public key material in order to export the elliptic curve public key using the KeyChain API calls.


Second, of course I have the SecKeyRef for the elliptic curve public key returned by SecKeyGeneratePair(), but on iOS there is no way to export the elliptic curve public key from this opaque handle.


Third, SecKeyRef will print out diagnostic info for the elliptic curve public key though! This is the output for a typical elliptic curve public key as returned by the OS:


<SecKeyRef curve type: kSecECCurveSecp256r1, algorithm id: 3, key type: ECPublicKey, version: 3, block size: 256 bits, y: 0620A1AE78F7EA7D79F1CA6F63F5954BD710BDBCEA9F03838A5F939F60140A7E01, x: 120DE3D293CF8B6F8A6049942ABD2C206BC7050B2330C348FDBA2999A8CB1AD90620A1AE78F7EA7D79F1CA6F63F5954BD710BDBCEA9F03838A5F939F60140A7E01, addr: 0x134672110>


x and y are specified, so for the time being I thought I could export the elliptic curve public key from the x and y dump. But x is 130 hexadecimal digits and y is 66 hexadecimal digits? Shouldn't these values be 32 bytes each?


The Apple KeyChainTouchID sample from iOS 9 beta 2 does not show how to export elliptic curve public keys, only how to generate, sign, and delete.


Things work properly with RSA, but then kSecAttrTokenIDSecureEnclave and kSecAccessControlPrivateKeyUsage can't be specified.


Confused. Any help appreciated!

Post not yet marked as solved Up vote post of fijibill Down vote post of fijibill
31k views

Replies

Hi Eskimo and all,

thank you for contributing to this thread. Lot of good information. I have read through this discussion multiple times to understand if I'm doing anything wrong.
I'm basically running into the same problem that users in this thread ran into, i.e. verifying the signed content outside iOS.
Verification is failing in both java program (BouncyCastle libs and JDK7) and openssl.

The keypair was generated using with keytype as kSecAttrKeyTypeEC. See the code below.

I'm getting dataToSign, signature and public key from a iOS device.
On Java side or OpenSSL (after adding the header) I'm able to parse the public key with no issues.

Here is the section that is used to generate the key pair.

var publicKey: SecKey? = nil
let publicKeyAttrs: [String: AnyObject] = [
kSecAttrKeyClass as String : kSecAttrKeyClassPublic,
kSecAttrApplicationTag as String : uuidStr
]
var privateKey: SecKey? = nil
let err = SecKeyGeneratePair([
kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
kSecAttrKeyType as String: kSecAttrKeyTypeEC,
kSecAttrKeySizeInBits as String: 256,
kSecAttrApplicationTag as String: uuidStr,
kSecPrivateKeyAttrs as String: [
kSecAttrIsPermanent as String: true,
kSecAttrAccessControl as String:access,
],
kSecPublicKeyAttrs as String: publicKeyAttrs
] as NSDictionary, &publicKey, &privateKey)

and code to sign the content...
...
let stringToSign: String = "SignThisString"
let dataToSign: NSMutableData = stringToSign.dataUsingEncoding(NSUTF8StringEncoding)! as! NSMutableData
NSLog("dataToSign %@", dataToSign)

let signature = NSMutableData(length: 128)!
var signatureLength = signature.length
/* Tried various padding options i.e. .PKCSSHA1, .NONE*/
let signErr = SecKeyRawSign(privateKey, .PKCS1, UnsafePointer(dataToSign.bytes), dataToSign.length, UnsafeMutablePointer(signature.mutableBytes), &signatureLength)
guard signErr == errSecSuccess else
NSLog("verify sign error %d", signErr)


NSLog("signature %@", signature)
NSLog("signatureLength %d", signatureLength)


Here is the data I'm trying to verify using a java program. The below data was output from iOs program.

String dataToSignInHex="22f3e5d2d0f6031340f987a979dba76b0f0c1a7c";
String publicKeyInHex="046052c59c1cc8aa3a48b33ec9908f8332af82402ace8754eabf087918a94a18446ddb01f4fef4936784200e6f4b66b0079f7fa363a42b5828e15c1c265e260e04";
String sigInHex="3044022021250762bbb601867165181ba1b8462cc4c52dac7bb895764068b7fa38d46014022054ee80cead07b1f022242e158a17cb097a9ee7520ed7f3bf8c4695f1a5ccfb8f";

Passed public key info

Algorithm=ECDSA
getFormat=X.509
getEncoded=[B@5b4ec310
sigBytes Length=70
parsed pk_in_hex=308201333081ec06072a8648ce3d02013081e0020101302c06072a8648ce3d0101022100ffffffff00000001000000000000000000000000ffffffffffffffffffffffff30440420ffffffff00000001000000000000000000000000fffffffffffffffffffffffc04205ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b0441046b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c2964fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5022100ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551020101034200046052c59c1cc8aa3a48b33ec9908f8332af82402ace8754eabf087918a94a18446ddb01f4fef4936784200e6f4b66b0079f7fa363a42b5828e15c1c265e260e04

Verification keeps failing in both Java and OpenSSL. Not sure what we are missing. Greatly appreciate your help.

Hi Ronny,


I see that you figured out how to validate the signature using Java. Can you please shed some light what I could be doing wrong.

I'm using BouncyCastle libraries. Here is the code to parse the public key. Does this look right?


public PublicKey decodePublicKey(byte[] encodedPublicKey) throws U2FException {
try {
X9ECParameters curve = SECNamedCurves.getByName("secp256r1");
ECPoint point;
try {
point = curve.getCurve().decodePoint(encodedPublicKey);
} catch (RuntimeException e) {
throw new U2FException("Couldn't parse user public key", e);
}

return KeyFactory.getInstance("ECDSA").generatePublic(
new ECPublicKeySpec(point,
new ECParameterSpec(
curve.getCurve(),
curve.getG(),
curve.getN(),
curve.getH())));
} catch (InvalidKeySpecException e) {
throw new U2FException("Error when decoding public key", e);
} catch (NoSuchAlgorithmException e) {
throw new U2FException("Error when decoding public key", e);
}
}

Verification is failing in both java program (BouncyCastle libs and JDK7) and openssl.

I had cause to look at this again and modified my code to make it easier to test things. To start, here’s the code I used to generate the key.

func generate() {
    let uuidStr = NSUUID().UUIDString

    let access = SecAccessControlCreateWithFlags(
        nil,
        kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
        [.TouchIDAny, .PrivateKeyUsage],
        nil
    )!

    var publicKey: SecKey? = nil
    var privateKey: SecKey? = nil
    let err = SecKeyGeneratePair([
        kSecAttrTokenID as String:          kSecAttrTokenIDSecureEnclave,
        kSecAttrKeyType as String:          kSecAttrKeyTypeEC,
        kSecAttrKeySizeInBits as String:    256,
        kSecAttrApplicationTag as String:  uuidStr,
        kSecPrivateKeyAttrs as String: [
            kSecAttrIsPermanent as String:  true,
            kSecAttrAccessControl as String:access,
        ]
    ] as NSDictionary, &publicKey, &privateKey)
    if err != errSecSuccess {
        NSLog("generate error %d", err)
    } else {
        NSLog("generate success")

        let addErr = SecItemAdd([
            kSecClass as String:                kSecClassKey,
            kSecValueRef as String:            publicKey!,
            kSecAttrApplicationTag as String:  uuidStr
        ] as NSDictionary, nil)
        if addErr != errSecSuccess {
            NSLog("add error %d", addErr)
        } else {
            NSLog("add success")
        }
    }
}

Here’s the top-level signing code:

func sign() {
    // Create some unique data to sign.

    let stringToSign = NSString(format: "This string was signed after %@.", NSDate())
    let dataToSign = stringToSign.dataUsingEncoding(NSUTF8StringEncoding)!

    // Start off our shell script with some lines that put that data in "dataToSign.dat".

    var shellScriptLines = [ "#! /bin/sh" ]
    shellScriptLines.append(String(format: "echo %@ | xxd -r -p > dataToSign.dat", QHex.hexStringWithData(dataToSign)))

    // Now add lines for each key.

    for (_, (publicKey: publicKey, privateKey: privateKey)) in self.keyPairsByUUIDStr() {
        _ = try? self.signData(dataToSign, withPrivateKey: privateKey, publicKey: publicKey, shellScriptLines: &shellScriptLines)
    }

    // Finally print the script.

    for l in shellScriptLines {
        print(l)
    }
}

This uses two helpers:

  • keyPairsByUUIDStr()
    iterates through all the keys in the keychain returning public and private keys matched by their UUID. If you’re only using one key then this level of complexity is unnecessary, so I haven’t included this code here.
  • signData(***)
    , which actually does the signing. It shown below.

-

func signData(dataToSign: NSData, withPrivateKey privateKey: SecKey, publicKey: SecKey, inout shellScriptLines: [String]) throws {
    let digestToSign = CommonCryptoAccess.sha1DigestForData(dataToSign)
    // let digestToSign = CommonCryptoAccess.sha256DigestForData(dataToSign)

    // There's no reliable API to get the correct signature buffer length to use
    // <rdar://problem/23128926> so we hard code 128 as the theoretical maximum.

    let signature = NSMutableData(length: 128)!
    var signatureLength = signature.length
    let signErr = SecKeyRawSign(privateKey, .PKCS1, UnsafePointer(digestToSign.bytes), digestToSign.length, UnsafeMutablePointer(signature.mutableBytes), &signatureLength)
    guard signErr == errSecSuccess else {
        NSLog("verify sign error %d", signErr)
        throw NSError(domain: NSOSStatusErrorDomain, code: Int(signErr), userInfo: nil)
    }
    signature.length = signatureLength

    // Add a command to create "signature.dat".

    shellScriptLines.append(String(format: "echo %@ | xxd -r -p > signature.dat", QHex.hexStringWithData(signature)))

    var matchResult: AnyObject? = nil
    let err = SecItemCopyMatching([
        kSecClass as String:                kSecClassKey,
        kSecValueRef as String:            publicKey,
        kSecReturnData as String:          true
    ] as NSDictionary, &matchResult)
    if err != errSecSuccess {
        NSLog("match error %d", err)
    } else if let keyRaw = matchResult as? NSData {

        // We take the raw key and prepend an ASN.1 prefix to it.  The end result is an
        // ASN.1 SubjectPublicKeyInfo structure, which is what OpenSSL is looking for.
        //
        // See the following DevForums post for more details on this.
        //
        // <https://forums.developer.apple.com/message/84684#84684>.

        let keyHeader = QHex.dataWithValidHexString("3059301306072a8648ce3d020106082a8648ce3d030107034200")
        let keyASN1 = NSMutableData(data: keyHeader)
        keyASN1.appendData(keyRaw)

        // Convert the key to Base64 then wrap it up as "key.pem".

        let keyBase64 = keyASN1.base64EncodedStringWithOptions([.Encoding64CharacterLineLength])
        shellScriptLines.append("cat > key.pem <<EOF")
        shellScriptLines.append("-----BEGIN PUBLIC KEY-----")
        shellScriptLines.appendContentsOf( keyBase64.componentsSeparatedByString("\r\n") )
        shellScriptLines.append("-----END PUBLIC KEY-----")
        shellScriptLines.append("EOF")
    } else {
        NSLog("match cast problem")
    }

    // Add a command to verify the signature.
    //
    // IMPORTANT: -ecdsa-with-SHA1 is a bit of a hack that works in the ancient version
    // of OpenSSL that's built in to OS X.  Alas, ecdsa-with-SHA256 does not work. If
    // you need to use a long digest, SHA256 say, you should replace this with "-sha256". 
    // That won't work on OS X but will work with other, more modern versions of OpenSSL,
    // where it detects the key type and does an ECDSA digest.

    shellScriptLines.append("openssl dgst -ecdsa-with-SHA1 -verify key.pem -signature signature.dat dataToSign.dat")
}

Finally, here’s the wrappers I use to access Common Crypto from Swift.

+ (NSData *)sha1DigestForData:(NSData *)data {
    NSMutableData *    result;

    result = [[NSMutableData alloc] initWithLength:CC_SHA1_DIGEST_LENGTH];
    assert(result != nil);

    CC_SHA1(data.bytes, (CC_LONG) data.length, result.mutableBytes);

    return result;
}

+ (NSData *)sha256DigestForData:(NSData *)data {
    NSMutableData *    result;

    result = [[NSMutableData alloc] initWithLength:CC_SHA256_DIGEST_LENGTH];
    assert(result != nil);

    CC_SHA256(data.bytes, (CC_LONG) data.length, result.mutableBytes);

    return result;
}

The end result here is a shell script that you can literally paste into a Terminal window to verify the key using the OpenSSL built in to OS X. To ensure that there wasn’t anything OS X-specific in play here, I ran the same shell script on Ubuntu 14.04.02. Verifying it using other crypto toolkits is left as an exercise for the reader (-:

Share and Enjoy

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

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

DTS will close for the winter holidays at the end of business on Wed, 23 Dec 2015 and re-open on Mon, 4 Jan 2016.

Hi Eskimo,


Thank you very much for the detailed reply. We had a break through yesterday. We are able to verify using both Open SSL and Java now. The piece we were missing was the signing of the digest vs signing of the raw string. The hashing mechanism used to create the digest on iOS side has to match the argument used to create the Signature object on java side.

Here's a working repo of what quinn explains: https://github.com/hfossli/EskimoKeys/tree/master