Hello,
The problem I have is that an EC generated signature using iOS/OSX does verify on multiple places: jwt.io and java code using oauth jwt library. I can sign and verify data (jws tokens) on iOS devices and those signatures verify elsewere. The inverse however is not true. I am aware of differences in generated signatures (Apple's functions output DER encoded values) and verified that using an apple issued EC key (Apple Music service allows queries using signed jwt tokens) but the results are exactly the same.
I use the following piece of code:
-(BOOL)signJWT {
/*
Private key taken from jwt.io generated page (I tested with apple issued ecc private key, same results):
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgevZzL1gdAFr88hb2
OF/2NxApJCzGCEDdfSp6VQO30hyhRANCAAQRWz+jn65BtOMvdyHKcvjBeBSDZH2r
1RTwjmYSi9R/zpBnuQ4EiMnCqfMPWiZqB4QdbAd0E7oH50VpuZ1P087G
-----END PRIVATE KEY-----
=> openssl ec -in private.pem -text
read EC key
Private-Key: (256 bit)
priv:
7a:f6:73:2f:58:1d:00:5a:fc:f2:16:f6:38:5f:f6:
37:10:29:24:2c:c6:08:40:dd:7d:2a:7a:55:03:b7:
d2:1c
pub:
04:11:5b:3f:a3:9f:ae:41:b4:e3:2f:77:21:ca:72:
f8:c1:78:14:83:64:7d:ab:d5:14:f0:8e:66:12:8b:
d4:7f:ce:90:67:b9:0e:04:88:c9:c2:a9:f3:0f:5a:
26:6a:07:84:1d:6c:07:74:13:ba:07:e7:45:69:b9:
9d:4f:d3:ce:c6
ASN1 OID: prime256v1
NIST CURVE: P-256
writing EC key
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIHr2cy9YHQBa/PIW9jhf9jcQKSQsxghA3X0qelUDt9IcoAoGCCqGSM49
AwEHoUQDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9q9UU8I5mEovUf86QZ7kOBIjJ
wqnzD1omageEHWwHdBO6B+dFabmdT9POxg==
-----END EC PRIVATE KEY-----
We now have to convert the key to suit Apple's security functions:
// For an elliptic curve public key, the format follows the ANSI X9.63 standard using a byte string of 04 || X || Y
// where || denotes concatenation
// For an elliptic curve private key, the output is formatted as the public key concatenated with the big endian
// encoding of the secret scalar, or 04 || X || Y || K
// The key must be public, 256-bit ECDSA
And so the above information gives
04115b3fa39fae41b4e32f7721ca72f8c1781483647dabd514f08e66128bd47fce9067b90e0488c9c2a9f30f5a266a07841d6c077413ba07e74569b99d4fd3cec6 ||
7af6732f581d005afcf216f6385ff6371029242cc60840dd7d2a7a5503b7d21c
Base64 encoding it produces the contents of variable aPrivateKeyB64
*/
NSString *aPrivateKeyB64 = @"BBFbP6OfrkG04y93Icpy+MF4FINkfavVFPCOZhKL1H/OkGe5DgSIycKp8w9aJmoHhB1sB3QTugfnRWm5nU/TzsZ69nMvWB0AWvzyFvY4X/Y3ECkkLMYIQN19KnpVA7fSHA==";
NSData *aPrivateKeyX509 = [Base64 decode:aPrivateKeyB64];
NSDictionary* aPrivateKeyCreateOptions =
@{
(id)kSecAttrKeyType: (id)kSecAttrKeyTypeEC,
(id)kSecAttrKeyClass: (id)kSecAttrKeyClassPrivate,
(id)kSecAttrKeySizeInBits: @256,
};
CFErrorRef aCFError = NULL;
SecKeyRef aPrivateKey = SecKeyCreateWithData((__bridge CFDataRef)aPrivateKeyX509,
(__bridge CFDictionaryRef)aPrivateKeyCreateOptions, &aCFError);
if (!aPrivateKey) {
NSLog(@"SecKeyCreateWithData() fail!"); // Tell ARC to take ownership of aCFError
return NO;
}
// End log
SecKeyAlgorithm aAlgorithm = kSecKeyAlgorithmECDSASignatureMessageX962SHA256;
// Check that the key is suitable for verification
Boolean aCanVerify = SecKeyIsAlgorithmSupported(aPrivateKey, kSecKeyOperationTypeSign, aAlgorithm);
if (!aCanVerify)
{
CFRelease(aPrivateKey);
NSLog(@"Invalid key for signing algorithm");
return NO;
}
/* Data to be tested: JWT token without signature */
NSString *mData = @"eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0";
NSData *aByteData = [mData dataUsingEncoding:NSUTF8StringEncoding];
/* Create signature */
CFDataRef aSignedDataBlob = SecKeyCreateSignature(aPrivateKey, aAlgorithm, (__bridge CFDataRef)aByteData, &aCFError);
if(aCFError){
NSError *err = CFBridgingRelease(aCFError); // ARC takes ownership
NSLog(@"SecKeyCreateSignature() failed: %@", err.description);
}
NSString *aEncodedEncryptedData = [Base64 encode:(__bridge NSData *)(aSignedDataBlob)];
NSLog(@"Printing signature data: %@", aEncodedEncryptedData);
NSLog(@"Extracting public key from provided private one ...");
// Extract public key from the private
SecKeyRef publicKey = SecKeyCopyPublicKey(aPrivateKey);
CFErrorRef error;
NSData* keyData = (NSData*)CFBridgingRelease(SecKeyCopyExternalRepresentation(publicKey, &error));
NSString* aB64KeyData = [Base64 encode:keyData];
NSLog(@"Public Key: %@", aB64KeyData);
// Verify the above signature: This always succeeds! The signature also veryfies in both
// java and jwt.io. Note: if you wish to verify the signature on jwt.io you have to extract the
// raw 64 bytes from ASN.1 encoding, see https://crypto.stackexchange.com/questions/1795/how-can-i-convert-a-der-ecdsa-signature-to-ASN.1
// or use implementation of function from https://github.com/auth0/java-jwt/blob/master/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java
BOOL aIsVerified = SecKeyVerifySignature(publicKey, aAlgorithm,
(__bridge CFDataRef)aByteData, aSignedDataBlob, &aCFError);
if (!aIsVerified) {
CFRelease(publicKey);
NSLog(@"NOT VERIFIED!!!!!!"); // Tell ARC to take ownership of aCFError
if(aCFError) {
NSError *err = CFBridgingRelease(aCFError);
NSLog(@"Error is %@", err.description);
}
return NO;
}
NSLog(@"Success verifying signature!!!!!!");
// Android generated signature! This always fails to VALIDATE, despite validating on jwt.io and android.
/* Actual raw signature is 64 bytes long: gU5DQgVhqnlpOKbq3JvpirRu7l1LioIpuNq0SR4a5sCxCl_QB7Ww04nVJlDr_wMpGDAFgZLtz2NM3eEK1aICKw
* This is converted to ASN.1 form by using the function JOSE2DER from https://github.com/auth0/java-jwt/blob/master/lib/src/main/java/com/auth0/jwt/algorithms/ECDSAAlgorithm.java and produces a valid DER value used as aExternalSig variable.
*/
NSString* aExternalSig = @"MEYCIQCBTkNCBWGqeWk4purcm+mKtG7uXUuKgim42rRJHhrmwAIhALEKX9AHtbDTidUmUOv/AykYMAWBku3PY0zd4QrVogIr";
aIsVerified = SecKeyVerifySignature(publicKey, aAlgorithm,
(__bridge CFDataRef)aByteData, (__bridge CFDataRef)aAnotherSigBytes, &aCFError);
if (!aIsVerified) {
CFRelease(publicKey);
if(aCFError) {
NSLog(@"NOT VERIFIED!!!!!!"); // Tell ARC to take ownership of aCFError
NSError *err = CFBridgingRelease(aCFError);
NSLog(@"Error is %@", err.description);
}
return NO;
}
// Code path never reached
NSLog(@"Success verifying external signature!!!!!!");
return YES;
}
I am at a loss as to why this is happening. Any help would be greatly appreciated.
EDIT: removed a comment with old signature.
EDIT2: Actual JWS token to be tested with Base64URL signature
eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.gU5DQgVhqnlpOKbq3JvpirRu7l1LioIpuNq0SR4a5sCxCl_QB7Ww04nVJlDr_wMpGDAFgZLtz2NM3eEK1aICKw