EC signature verification failed (ccerr -7)

Hello, I am working on simple task, i have a JWT and I need to verify it's signature.

My sample data:

JWT eyJhbGciOiJFUzI1NiIsImtpZCI6IjhEWVpUTFRPVUtNSjdUU0w4SFZVVTlUVEdXMV9NTVVSSURJV1JUVE0iLCJ0eXAiOiJKV1QifQ.eyJub25jZSI6Indkd2R3ZHdkIiwiZGF0ZVRpbWUiOiIyMDIzLTEwLTEzVDEyOjQ4OjQwWiJ9.nWBfRne9VOKV4NGt32gWtoA5fUKzu0RzkhUYSYwUA7cvalsABUcWrpEk0fhztPZDK7KrDF8P2Pquhoxq4p-FUg

Public key (JWK)

{
      "kid": "8DYZTLTOUKMJ7TSL8HVUU9TTGW1_MMURIDIWRTTM",
      "use": "sig",
      "kty": "EC",
      "alg": "ES256",
      "crv": "P-256",
      "x": "8dyzTlToUkMJ7tsl8HVuU9tTGw1_mmuridIWrttMRCs",
      "y": "AVqGlqJ88QgP_uVLea7gIUnA-p9iYwnJyYt7o-uH4oU"
}

When you put the data to the [https://jwt.io] you get "Signature Verified" message. So the data should be ok.

I have implementation in Objective-C, but i get this weird error message when the SecKeyVerifySignature is called.

Here is my code:

#import <Cocoa/Cocoa.h>
#import <CommonCrypto/CommonDigest.h>


NSString* base64StringWithPadding(NSString *encodedString)  {
    NSString* stringTobeEncoded = [[encodedString stringByReplacingOccurrencesOfString:@"-" withString:@"+"]
                                   stringByReplacingOccurrencesOfString:@"_" withString:@"/"];
    int paddingCount = encodedString.length % 4;
    for (int i = 0; i < paddingCount; i++) {
        stringTobeEncoded = [stringTobeEncoded stringByAppendingString:@"="];
    }
    return stringTobeEncoded;
}

SecKeyRef restorePublicKey(NSString *x, NSString *y) {
    
    NSString *xBase64Padd = base64StringWithPadding(x);
    NSString *yBase64Padd = base64StringWithPadding(y);
    
    NSData *xData = [[NSData alloc] initWithBase64EncodedString:xBase64Padd 
                                                        options:kNilOptions]; //32bytes
    NSData *yData = [[NSData alloc] initWithBase64EncodedString:yBase64Padd 
                                                        options:kNilOptions]; //32bytes
    
 
    NSMutableData *publicKeyData = [NSMutableData data];

    // Append the constant byte '04' to indicate uncompressed format
    const unsigned char uncompressedByte = 0x04;
    [publicKeyData appendBytes:&uncompressedByte length:1];
    
    // Append the X-coordinate and Y-coordinate
    [publicKeyData appendData:xData];
    [publicKeyData appendData:yData];

    NSDictionary *keyAttributes = @{
        (id)kSecAttrKeyType: (id)kSecAttrKeyTypeECSECPrimeRandom,
        (id)kSecAttrKeyClass: (id)kSecAttrKeyClassPublic,
        (id)kSecAttrKeySizeInBits: @256,
        (id)kSecAttrIsPermanent:@NO
    };

    CFErrorRef error = NULL;
    SecKeyRef publicKeyRef = 
        SecKeyCreateWithData(
                             (__bridge CFDataRef)publicKeyData,
                             (__bridge CFDictionaryRef)keyAttributes,
                             &error
                             );
    if(!publicKeyRef) {
        NSError *err = CFBridgingRelease(error);
        NSLog(@"%@", err.description);
    }
    return publicKeyRef;
}


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        NSDictionary *jwk = 
        @{
            @"kid": @"8DYZTLTOUKMJ7TSL8HVUU9TTGW1_MMURIDIWRTTM",
            @"use": @"sig",
            @"kty": @"EC",
            @"alg": @"ES256",
            @"crv": @"P-256",
            @"x": @"8dyzTlToUkMJ7tsl8HVuU9tTGw1_mmuridIWrttMRCs",
            @"y": @"AVqGlqJ88QgP_uVLea7gIUnA-p9iYwnJyYt7o-uH4oU"
        };

        SecKeyRef publicKeyRef = restorePublicKey(jwk[@"x"], jwk[@"y"]);
        
        NSString* header = @"eyJhbGciOiJFUzI1NiIsImtpZCI6IjhEWVpUTFRPVUtNSjdUU0w4SFZVVTlUVEdXMV9NTVVSSURJV1JUVE0iLCJ0eXAiOiJKV1QifQ";
        NSString* payload = @"eyJub25jZSI6Indkd2R3ZHdkIiwiZGF0ZVRpbWUiOiIyMDIzLTEwLTEzVDEyOjQ4OjQwWiJ9";
        NSString* signature = @"nWBfRne9VOKV4NGt32gWtoA5fUKzu0RzkhUYSYwUA7cvalsABUcWrpEk0fhztPZDK7KrDF8P2Pquhoxq4p-FUg";
        
        NSString *headerAndPayload = [NSString stringWithFormat:@"%@.%@", header, payload];
        NSData *signedData = [headerAndPayload dataUsingEncoding:NSUTF8StringEncoding];
        
        NSString *signatureBase64Padd = base64StringWithPadding(signature);
        NSData *signatureData = [[NSData alloc] initWithBase64EncodedString:signatureBase64Padd 
                                                                    options:kNilOptions];
        
        bool canVerify = SecKeyIsAlgorithmSupported(publicKeyRef, kSecKeyOperationTypeVerify, kSecKeyAlgorithmECDSASignatureMessageX962SHA256);
        
        if(canVerify) {
            CFErrorRef error = NULL;
            BOOL verifyStatus =
                SecKeyVerifySignature(
                                      publicKeyRef,
                                      kSecKeyAlgorithmECDSASignatureMessageX962SHA256,
                                      (__bridge CFDataRef)signedData,
                                      (__bridge CFDataRef)signatureData,
                                      &error
                                      );
            if(error) {
                NSError *err = CFBridgingRelease(error);
                NSLog(@"%@", err.description);
            }
        } else {
            NSLog(@"Cannot verify.");
        }
    }
    return NSApplicationMain(argc, argv);
}

I dont know where is the problem. I've tried everything.

Replies

The signature looks kinda wonky. Consider this programs:

import Foundation

func main() {
    let privateKey = SecKeyCreateRandomKey([
        kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
        kSecAttrKeySizeInBits: 256,
    ] as NSDictionary, nil)!
    let message = Data("Hello Cruel World!".utf8)
    let signature = SecKeyCreateSignature(
        privateKey,
        .ecdsaSignatureMessageX962SHA256,
        message as NSData,
        nil
    )
    print(signature.map { ($0 as NSData).debugDescription } ?? "nil")
}

main()

It generates a new P256 key, signs a dummy message with it using the same algorithm you’re using, and dumps the signature. When I run it I see this:

<30450220 68888d82 785d2bd8 5dc7da89 5d141254 d2707037 1c3b28db e9b857f1 7abca556 0221009e 1f9344fa 084d99d1 aed89624 902254fc e7acb3db 57d008c3 40853482 629d7d>

Now consider this:

% base64 -D > yours.dat
nWBfRne9VOKV4NGt32gWtoA5fUKzu0RzkhUYSYwUA7cvalsABUcWrpEk0fhztPZDK7KrDF8P2Pquhoxq4p-FUg
% xxd yours.dat
00000000: 9d60 5f46 77bd 54e2 95e0 d1ad df68 16b6  .`_Fw.T......h..
00000010: 8039 7d42 b3bb 4473 9215 1849 8c14 03b7  .9}B..Ds...I....
00000020: 2f6a 5b00 0547 16ae 9124 d1f8 73b4 f643  /j[..G...$..s..C

Here I just decoded the Base64 value in your signature string and dumped it as hex. Contrast that to this:

% xxd mine.dat
00000000: 3045 0220 6888 8d82 785d 2bd8 5dc7 da89  0E. h...x]+.]...
00000010: 5d14 1254 d270 7037 1c3b 28db e9b8 57f1  ]..T.pp7.;(...W.
00000020: 7abc a556 0221 009e 1f93 44fa 084d 99d1  z..V.!....D..M..
00000030: aed8 9624 9022 54fc e7ac b3db 57d0 08c3  ...$."T.....W...
00000040: 4085 3482 629d 7d                        @.4.b.}
% dumpasn mine.dat
zsh: command not found: dumpasn
% dumpasn1 mine.dat
  0  69: SEQUENCE {
  2  32:   INTEGER
       :     68 88 8D 82 78 5D 2B D8 5D C7 DA 89 5D 14 12 54
       :     D2 70 70 37 1C 3B 28 DB E9 B8 57 F1 7A BC A5 56
 36  33:   INTEGER
       :     00 9E 1F 93 44 FA 08 4D 99 D1 AE D8 96 24 90 22
       :     54 FC E7 AC B3 DB 57 D0 08 C3 40 85 34 82 62 9D
       :     7D
       :   }

0 warnings, 0 errors.
% dumpasn1 -p -a mine.dat
SEQUENCE {
  INTEGER
    68 88 8D 82 78 5D 2B D8 5D C7 DA 89 5D 14 12 54
    D2 70 70 37 1C 3B 28 DB E9 B8 57 F1 7A BC A5 56
  INTEGER
    00 9E 1F 93 44 FA 08 4D 99 D1 AE D8 96 24 90 22
    54 FC E7 AC B3 DB 57 D0 08 C3 40 85 34 82 62 9D
    7D
  }

Note how my signature has an ASN.1 structure. That’s the X9.62 part of kSecKeyAlgorithmECDSASignatureMessageX962SHA256.


The most common non-X9.62 format for P256 signature is the raw representation. This is, for example, what I use in the QJWT code you’ll find here.

The weird part is that your signature is too short for that. Consider this code:

import Foundation
import CryptoKit

func main() {
    let privateKey = P256.Signing.PrivateKey()
    let message = Data("Hello Cruel World!".utf8)
    let sha256 = SHA256.hash(data: message)
    let signature = try! privateKey.signature(for: sha256)
    print(signature.rawRepresentation, (signature.rawRepresentation as NSData).debugDescription)
    print(signature.derRepresentation, (signature.derRepresentation as NSData).debugDescription)
}

main()

It prints:

64 bytes <9d9d05bf 68453dfe a3bc65f5 dbe4e049 1df40f0c 8ec213e9 467c1fef 3b6567fe 44ada33d 5e00390b 3f56f342 c6e2b611 992fe2a8 ae6af5b2 0fd1b84c 61378cd8>
71 bytes <30450221 009d9d05 bf68453d fea3bc65 f5dbe4e0 491df40f 0c8ec213 e9467c1f ef3b6567 fe022044 ada33d5e 00390b3f 56f342c6 e2b61199 2fe2a8ae 6af5b20f d1b84c61 378cd8>

Note how the raw representation is 64 bytes, whereas yours is 48.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Hello, thanks for your quick response and very useful hints.

"X9.62 part of kSecKeyAlgorithmECDSASignatureMessageX962SHA256"

This is the "part" which i missed :), the signature was not in this format. So, I've moved to CryptoKit and now everything works as expected.

Add a Comment