EC 256 Secure Enclave pkey won't sign correctly

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:


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

Accepted Reply

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

    digestToSign
    O-:

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"

Replies

Method 1: (the one that should be used no?)

Yes.

If you try Method 1 with a key not in the Secure Enclave, what do you see?

Share and Enjoy

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

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

I have the same behavior with Secure Enclave disabled, which is:

  • .ecdsaSignatureMessageX962SHA256 + not hashed data == Signature OK
  • .ecdsaSignatureDigestX962SHA256 + hashed data == "bad digest size for signing with algorithm algid:sign:ECDSA:digest-X962:SHA256" == -50

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

    digestToSign
    O-:

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"

Oh snap, you made me realize I wrongly expected "str.data(using: .utf8)!" to parse the SHA256 hexa string using 2 chars to create 1 byte.. "Feels bad man"


I'd like to clarify a last point:

  • Is "SecKeyCreateRandomKey(...)" strictly the same than "SecKeyGeneratePair(...)", or is there any difference in the way they are created?
  • If there is a difference, which one should be prefered?


Thank you!

I’m glad you got it sorted. Yay!

With regards

SecKeyCreateRandomKey
, I’ve never looked at that routine in depth but the doc comments for
SecKeyGeneratePair
in the latest SDKs says this:

It is recommended to use

SecKeyCreateRandomKey
which respects
kSecAttrIsPermanent
on all platforms.

which seems pretty definitive to me (-:

Share and Enjoy

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

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

Perfect, thank you again o/