Since it is going to be a be long post, I'll start by resuming what my goals are:
- ✅ Create an EC private key inside the Secure Enclave ℹ (only secp256r1 is supported atm. A11 and before)
- ✅ Get the public key so I can export it ℹ (you can only get a binary format, so you may want to wrap it)
- ✖ Sign an already hashed Data to an x509 DER format ℹ (the hash used is SHA256) (see EDIT3)
- ✅ Remove the private key
xCode: 9.0
iPhone6s: iOS11.0.2
Swift: 3
Here are the attributes used to generate my key pair:
let attributes:[NSObject:AnyObject] = [
kSecAttrIsPermanent: kCFBooleanTrue,
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
kSecAttrApplicationTag: ApplicationPrivateKeyTag as CFString,
kSecAttrLabel: ApplicationPrivateKeyLabel as CFString,
kSecAttrKeyType: ( iOS10Available ) ? (kSecAttrKeyTypeECSECPrimeRandom) : (kSecAttrKeyTypeEC),
kSecAttrKeySizeInBits: ECKeySize as CFNumber,
kSecAttrTokenID: kSecAttrTokenIDSecureEnclave
]
note1: iOS10Avaibale is a boolean that already checked which version of iOS we're dealing with, so we can give the correct parameter
note2: ECKeySize == 256
I generate my keypair with:
SecKeyGeneratePair(attributes as CFDictionary, nil, nil)
Though I also tried this one, without actually getting the difference between them, is there a difference other than the function's signature?
var error: Unmanaged<CFError>?
SecKeyCreateRandomKey(attributes as CFDictionary, &error)
I am then able to get the public key (and format it to x509 format):
// Query to get the private ref
let query: [NSObject: AnyObject] = [
kSecAttrApplicationTag: ApplicationPrivateKeyTag as AnyObject,
kSecAttrLabel: ApplicationPrivateKeyLabel as AnyObject,
kSecClass: kSecClassKey,
kSecAttrKeyClass: kSecAttrKeyClassPrivate,
kSecReturnRef: kCFBooleanTrue
]
// Get private key..
// SecItemCopyMatching(query as NSDictionary, &extractedData)
// Get public SecKey..
// SecKeyCopyPublicKey(privateSecKeyRef)
// Get public key as Data
// SecKeyCopyExternalRepresentation(publicSecKey!, &error)
// Wrap public key to x509 format
// x509Header: Data = Data(bytes: [UInt8]([48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, 206, 61, 3, 1, 7, 3, 66, 0]))
var pubkeyWraped = x509Header
pubkeyWraped.append(publicKey)
pubkeyWraped.hexEncodedString() // <- here is the hex public key
If you wonder about:
- x509 header: see https://forums.developer.apple.com/message/84684#84684
- hexEncodedString(): see stackoverflow dot com /questions/39075043/how-to-convert-data-to-hex-string-in-swift#40089462
- the other functions: check the doc
note: I keep it in hex format instead of base64 format
=======================================================================================
=======================================================================================
Ok now, some precisions about my test:
- The original data is: "wubba lubba dub dub"
- dataToSign is a hash SHA256 : "23a0944d11b5a54f1970492b5265c732044ae824b7d5656acb193e7f0e51e5fa"
And once again I need to make a choice, but I can't figure it out whether I should use:
Method 1: (the one that should be used no?)
let signature = SecKeyCreateSignature(privateSecKeyRef,
.ecdsaSignatureDigestX962SHA256,
digestToSign as CFData,
&error) as Data?
I don't get the difference between:
- .ecdsaSignatureRFC4754
- .ecdsaSignatureDigestX962
- .ecdsaSignatureDigestX962SHA256 (the one documented, it should be the one to use right?)
Doc is here: https://developer.apple.com/documentation/security/certificate_key_and_trust_services/keys/signing_and_verifying
This function returns this error:
"bad digest size for signing with algorithm algid:sign:ECDSA:digest-X962:SHA256"
What am I doing wrong here?
Isn't the size expected 64 bytes?
Am I bigger, smaller?
What is it related to?
As seen on these close-related threads, secp256r1 curve should be compatible with SHA256 hash right?
- crypto.stackexchange dot com /questions/19793/does-the-size-of-a-ecdsa-key-determine-the-hash-algorithm
- crypto.stackexchange dot com /questions/18488/ecdsa-with-sha256-and-sepc192r1-curve-impossible-or-how-to-calculate-e
EDIT1:
If I give the .ecdsaSignatureMessageX962SHA256 option and that I don't pre-hash the message, I am able to verify the signature both with SecKeyVerifySignature(...) and an external function made with go.
(my hash is good since I still give the hashed form to my external go function, and well, you can check it too)
===========
FWIW: I am converting my string to data that way:
let str = "wubba lubba dub dub"
// let str = "23a0944d11b5a54f1970492b5265c732044ae824b7d5656acb193e7f0e51e5fa"
let toSign = str.data(using: .utf8)!
EDIT3: str.data(using .utf8)! will convert every single character to create a byte, not 2 character... Thus creating a 64 bytes long Data instead of a required one of 32 bytes.
Method 2:
let digestToSignLen = digestToSign.count
var rawSig = Data(count: 128)
var rawSigLen = 128
let status = digestToSign.withUnsafeBytes { (digestToSign) -> OSStatus in
rawSig.withUnsafeMutableBytes({ (rawSig) -> OSStatus in
return SecKeyRawSign(privateKeyRef!, SecPadding.sigRaw, digestToSign, digestToSignLen, rawSig, &rawSigLen)
})
}
if status == errSecSuccess {
signature = rawSig.subdata(in: 0..<rawSigLen)
}
If you wonder about why nesting everything, take a look at:
https://forums.developer.apple.com/thread/63965
if you wonder about the PKCS1* options of SecPadding for ec keys, take a look at:
security.stackexchange dot com /questions/84327/converting-ecc-private-key-to-pkcs1-format
Not sure about the actual effect of SecPadding.sigRaw
Is it supposed to refer to:
- the format of what it receives?
- the way it is going to (not) format the signature?
- both?
When I execute this code using the .sigRaw options:
- with the private key attribute kSecAttrTokenID: kSecAttrTokenIDSecureEnclave
I get an OSStatus -50 (bad argument(s))
- without the private key attribute kSecAttrTokenID: kSecAttrTokenIDSecureEnclave
- I get an OSStatus -50 (bad argument(s))
- 😮 I get what looks like a raw signature (that needs to be wraped to DER format then?)
EDIT2: After wraping to ASN.1 and checking the signature, it appear that the key is not verifed 😟
ℹ I must make it work with a pkey stored in the Secure Enclave
btw: you can find a deprecated link pointing at kSecPaddingNone in padding section of the SecKeyRawSign(...) page?
Method 3
This function is only available on macOS
func SecSignTransformCreate(_ key: UnsafeMutablePointer<Unmanaged<CFError>?>?) -> SecTransform?
I am lost 😢
So could someone help me to clarify:
What is the way to go, to correctly sign an already hashed (sha256) data to DER format, thanks to an ec-256-secure-enclave private key?
Here is the code I use to check the signature in go: play.golang dot org/p/XH2bi5lBhK
Thank you
I have the same behavior with Secure Enclave disabled …
That’s actually good news, because it removes a whole world of complexity from the issue.
bad digest size for signing with algorithm …
OK, I have two theories here:
Something super weird is going on
You’ve made some sort of ‘brain fade’ error with
O-:digestToSign
To start, I took the snippets you posted and assembled them into a complete test function (shown below). Note line 16, where I set the digest to 32 bytes of zeroes.
import UIKit
func test() {
var publicKeyMaybe: SecKey? = nil
var privateKeyMaybe: SecKey? = nil
let err = SecKeyGeneratePair([
kSecAttrIsPermanent as String: kCFBooleanTrue,
kSecAttrApplicationTag as String: UUID().uuidString,
kSecAttrLabel as String: UUID().uuidString,
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrKeySizeInBits as String: 256,
] as NSDictionary, &publicKeyMaybe, &privateKeyMaybe)
assert(err == errSecSuccess)
let privateKey = privateKeyMaybe!
let digestToSign = Data(repeating: 0, count: 32)
var error: Unmanaged<CFError>? = nil
let signature = SecKeyCreateSignature(privateKey, .ecdsaSignatureDigestX962SHA256, digestToSign as NSData, &error)
if let signature = signature {
NSLog("success, digest: %@", signature as NSData)
} else {
let error = error!.takeRetainedValue()
NSLog("failure, error: %@", String(describing: error))
}
}
I ran this on an iOS 11.0 device and it works:
2017-10-17 10:49:39.611516+0100 ECDigest[364:86907] success, digest: <30450220 … 04f8fb>
I changed line 16 to set the digest length to 31 bytes and I got the error you’re seeing:
2017-10-17 10:49:58.955706+0100 ECDigest[368:87258] failure, error: Error Domain=NSOSStatusErrorDomain Code=-50 "bad digest size for signing with algorithm algid:sign:ECDSA:digest-X962:SHA256" UserInfo={NSDescription=bad digest size for signing with algorithm algid:sign:ECDSA:digest-X962:SHA256}
As far as I can tell this error is only generated in one place, namely
SecKeyCopyECDSASignatureForDigest
in
Security/OSX/sec/Security/SecKeyAdaptors.c
. Here’s the relevant snippet:
if (CFDataGetLength(digest) != (CFIndex)di->output_size) {
SecError(errSecParam, error, CFSTR("bad digest size for signing with algorithm %@"), algorithm);
return NULL;
}
If you look through that file you’ll see it’s called by the
DIGEST_ECDSA_ADAPTORS
macro which is instantiated via
DIGEST_ECDSA_ADAPTORS(X962SHA256, ccsha256_di())
. I had a good look at
ccsha256_di()
and I can’t see how it’d return anything other than 32 for
output_size
.
Hence my conclusion: either something super weird is going on or you’re passing in a digest of the wrong length. I recommend that you double check the latter (-:
Share and Enjoy
—
Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware
let myEmail = "eskimo" + "1" + "@apple.com"