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:
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:
phew
Share and Enjoy
—
Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware
let myEmail = "eskimo" + "1" + "@apple.com"