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"