yourkarma / JWT

A JSON Web Token implementation in Objective-C.
MIT License
351 stars 107 forks source link

ES256 ? #116

Open JanC opened 7 years ago

JanC commented 7 years ago

Hi, this is not an issue but a question. Is there a plan to support the Elliptic Curve Digital Signature Algorithm ES256?

cheers, Jan

lolgear commented 7 years ago

Hi! Sure!

lolgear commented 7 years ago

@JanC do you have samples of ec256 keys (private/public) (PEM format)? It seems that found samples are broken :/

JanC commented 7 years ago

Hi, according to OpenSSL, I think this is what we need

openssl ecparam -genkey -name prime256v1  -out ec256-private.pem
openssl ec -in ec256-private.pem -pubout -out ec256-public.pem 

If you need a PKCS8 format you can convert it from PEM using

openssl pkcs8 -topk8 -nocrypt -in ec256-private.pem -out ec256-private.p8

I have some real keys from Apple's push notification service so I'd be happy to try to see if it works

lolgear commented 7 years ago

@JanC I try to retrieve key from PEM file, but it always said

Error Domain=NSOSStatusErrorDomain Code=-50 "EC public key creation from data failed" (paramErr: error in user parameter list) UserInfo={NSDescription=EC public key creation from data failed}

The error message indifferent for both keys (public/private). I could make a PR with this example. It would be great to have a response from you about problem. ( Google doesn't help or my skills in googling are too weak. )

JanC commented 7 years ago

sure, I can help to investigate

On 15 Feb 2017, at 12:48, Dmitry notifications@github.com wrote:

I try to retrieve key from PEM file, but it always said

Error Domain=NSOSStatusErrorDomain Code=-50 "EC public key creation from data failed" (paramErr: error in user parameter list) UserInfo={NSDescription=EC public key creation from data failed} The error message indifferent for both keys. I could make a PR with this example. It would be great to have a response from you about problem. ( Google doesn't help or my skills in googling are too weak. )

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.

lolgear commented 7 years ago

@JanC, please, have a look at latest master ( PR will be merged as soon as possible )

JanC commented 7 years ago

Hi, I'm running the same problem when reading the EC file. Looks like this is related: https://forums.developer.apple.com/thread/72445

lolgear commented 7 years ago

@JanC, hah, you revealed me at developer forums. :) Well, I am right that header should be stripped from key. The main question is: how to strip it in a right way? (Some parts were already written). Maybe we need some convertion code that could cut off all header related stuff from key for Security API.

JanC commented 7 years ago

:) Probably, I'm trying to found out the difference between a key generated using openssl and a key ec key using SecKeyGeneratePair

NSDictionary * keyPairAttr = @{(id)kSecAttrKeySizeInBits : @(256),
                               (id)kSecAttrKeyType : (id)kSecAttrKeyTypeEC
                               };

SecKeyRef publicKey;
SecKeyRef privateKey;
CFErrorRef cfError = NULL;

OSStatus status = SecKeyGeneratePair((CFDictionaryRef)keyPairAttr, &publicKey, &privateKey);
if(status != noErr) {
    NSLog(@"status: %@", @(status));
}

NSData* data = (__bridge_transfer NSData*) SecKeyCopyExternalRepresentation(privateKey, &cfError);

if(cfError != NULL) {
    NSLog(@"error: %@", (__bridge NSError *)cfError);
} else {
    [data writeToFile:@"/tmp/private.asn1" atomically:YES];
}

cfError = NULL;
data = (__bridge_transfer NSData*) SecKeyCopyExternalRepresentation(publicKey, &cfError);
if(cfError != NULL) {
    NSLog(@"error: %@", (__bridge NSError *)cfError);
} else {
    [data writeToFile:@"/tmp/public.asn1" atomically:YES];
}

and the ASN.1 structure is indeed just:

./dumpasn1 private.asn1 

  0  28: OCTET STRING
       :   59 5D 75 44 0D A8 4F C6 67 62 0A CD 96 09 83 6F
       :   E7 CE 8C 12 58 15 8E 17 62 31 C5 31
Warning: Further data follows ASN.1 data at position 30.

1 warning, 0 errors.
lolgear commented 7 years ago

@JanC well, we need to strip asn1 header from openssl key, am I correct? But why apple keys doesn't work well?

JanC commented 7 years ago

well, we need to strip asn1 header from openssl key, am I correct?

I believe that could be the solution

But why apple keys doesn't work well?

Which ones? The one generated via SecKeyGeneratePair or the one I've got from Apple push notification service?

The real push notification private key I've got from Apple also has the EC headers:

cat APNsAuthKey.p8 | grep -v PRIVATE | base64 -D > APNsAuthKey.asn1
./dumpasn1 APNsAuthKey.asn1 

0 147: SEQUENCE {
 3   1:   INTEGER 0
 6  19:   SEQUENCE {
 8   7:     OBJECT IDENTIFIER '1 2 840 10045 2 1'
17   8:     OBJECT IDENTIFIER '1 2 840 10045 3 1 7'
      :     }
27 121:   OCTET STRING, encapsulates {
29 119:     SEQUENCE {
31   1:       INTEGER 1
34  32:       OCTET STRING
      :         XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX
      :         XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX
68  10:       [0] {
70   8:         OBJECT IDENTIFIER '1 2 840 10045 3 1 7'
      :         }
80  68:       [1] {
82  66:         BIT STRING
      :           XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX
      :           XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX
      :           XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX
      :           XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX
      :           XX
      :         }
      :       }
      :     }
      :   }
lolgear commented 7 years ago

@JanC oh-oh-oh, I believe Apple has functions that cut headers from keys. So, the path is:

JanC commented 7 years ago

and ? [ ] add key retrieve functions

whose result would then feed the existing SecKeyCreateWithData ?

lolgear commented 7 years ago

@JanC If I am correct, PEM files contain several structures/headers ( right? ) for different formats. However, we need to strip specific headers from, for example, public rsa key ( already implemented ). It finds ASN.1 first, then it checks X509 header sequence and skip it if needed. After cleaning data from all necessary ( or not ) structures/headers we get the key data for SecKeyCreateWithData Your suggestion could be a candy code for all this stuff :)

JanC commented 7 years ago

yes that's how I see it as well :)

Btw "PEM" is just a way to encode and contain any ASN.1 key material (certificate, keys ..). So a PEM contains a header such as -----BEGIN PRIVATE KEY-----, -----BEGIN EC PRIVATE KEY----- or -----BEGIN CERTIFICATE----- ... and the base64 encoded ASN.1 structure. The ASN.1 structure contains itself the entire key or certificate material that is defined by its ASN.1 attributes. Be it a X.509 certificate or a EC keys.

It appears that we have to read the EC ASN.1 structure and get rid of those EC headers (I honestly ignore what a EC key is composed of)

lolgear commented 7 years ago

@JanC "I honestly ignore what a EC key is composed of" that's why we have troubles with SecKeyCreateWithData ( Apple engineers ignore it too )

JanC commented 7 years ago

:) Btw this is also useful tool since it puts names on the ASN.1 object identifiers https://lapo.it/asn1js/

lolgear commented 6 years ago

@JanC could you add test data for EC keys? I would like to add support for it even if only latest OS could use it.

flash23 commented 6 years ago

Hi,

I downloaded 3.0.0-beta.7, I don't see support for es256.

Will this be ready and when for 3.0.0 release?

thx

JanC commented 6 years ago

@lolgear, sorry I missed your comment. Yes I will upload some keys that I use

lolgear commented 6 years ago

@flash23 still in progress. I suppose that ES256 is available only for macOS 10.13 and iOS 11 ( system API )

flash23 commented 6 years ago

Hi @lolgear @JanC

I made ES256 with openssl I verified it and it works. I am in hurry so I cant wait ;)

But maybe you can use the code for your lib. I will switch to your lib when u implement it

-(NSData*) SHA256withECDSA_JWT:(NSString*)privateKeyPEM and:(NSData*)dataToSign
{
    char *pemC = (char*)[privateKeyPEM cStringUsingEncoding:NSASCIIStringEncoding];

    EC_KEY *privEC;
    privEC = readPrivatePemToEC(pemC);

    NSMutableData *digest = [NSMutableData dataWithLength:CC_SHA256_DIGEST_LENGTH];
    CC_SHA256(dataToSign.bytes, (unsigned int)dataToSign.length, digest.mutableBytes);

    ECDSA_SIG *signature;
    signature = ECDSA_do_sign(digest.bytes, (unsigned int)digest.length, privEC);

    int signatureLength = i2d_ECDSA_SIG(signature, NULL);
    NSMutableData *derSignature = [[NSMutableData alloc] initWithLength:signatureLength];
    uint8_t *signatureBytes2 = derSignature.mutableBytes;
    i2d_ECDSA_SIG(signature, &signatureBytes2);

    const unsigned char* signatureBytes = [derSignature bytes];

    if (derSignature.length < 8 || signatureBytes[0] != (uint8_t)0x30) {
        //assert!
    }

    int offset;
    if (signatureBytes[1] > 0) {
        offset = 2;
    } else if (signatureBytes[1] == (uint8_t) 0x81) {
        offset = 3;
    } else {
        //assert!
    }

    int rLength = signatureBytes[offset + 1];

    int i = rLength;
    while ((i > 0) && (signatureBytes[(offset + 2 + rLength) - i] == 0))
        i--;

    int sLength = signatureBytes[offset + 2 + rLength + 1];

    int j = sLength;
    while ((j > 0) && (signatureBytes[(offset + 2 + rLength + 2 + sLength) - j] == 0))
        j--;

    int rawLen = MAX(i, j);
    rawLen = MAX(rawLen, 64 / 2);

    if ((signatureBytes[offset - 1] & 0xff) != derSignature.length - offset
        || (signatureBytes[offset - 1] & 0xff) != 2 + rLength + 2 + sLength
        || signatureBytes[offset] != 2
        || signatureBytes[offset + 2 + rLength] != 2) {
        //assert!
    }

    NSMutableData *concatSignature = [[NSMutableData alloc] initWithCapacity:2 * rawLen];

    [concatSignature replaceBytesInRange:NSMakeRange(rawLen - i, i) withBytes:(signatureBytes + (offset + 2 + rLength) - i)];
    [concatSignature replaceBytesInRange:NSMakeRange(2 * rawLen - j, j) withBytes:(signatureBytes + (offset + 2 + rLength + 2 + sLength) - j)];

    EC_KEY_free(privEC);
    ECDSA_SIG_free(signature);

    return concatSignature;
}
NSMutableDictionary *dicPayload = [[NSMutableDictionary alloc] init];
//dicPayload all useful info goes in here 
NSError *error;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dicPayload options:0 error:&error];

NSString *payloadBase64 = [jsonData base64EncodedStringWithOptions:0];
payloadBase64 = [payloadBase64 stringByReplacingOccurrencesOfString:@"/" withString:@"_"];
payloadBase64 = [payloadBase64 stringByReplacingOccurrencesOfString:@"+" withString:@"-"];
payloadBase64 = [payloadBase64 stringByReplacingOccurrencesOfString:@"=" withString:@""];

NSMutableDictionary *dicHeader = [[NSMutableDictionary alloc] init];

[dicHeader setObject:@"ES256" forKey:@"alg"];
jsonData = [NSJSONSerialization dataWithJSONObject:dicHeader options:0 error:&error];
NSString *headerBase64 = [jsonData base64EncodedStringWithOptions:0];
headerBase64 = [headerBase64 stringByReplacingOccurrencesOfString:@"/" withString:@"_"];
headerBase64 = [headerBase64 stringByReplacingOccurrencesOfString:@"+" withString:@"-"];
headerBase64 = [headerBase64 stringByReplacingOccurrencesOfString:@"=" withString:@""];

NSString *toSignStr = [NSString stringWithFormat:@"%@.%@",headerBase64,payloadBase64];
NSData *toSignDat = [toSignStr dataUsingEncoding:NSUTF8StringEncoding];

NSData *signedHash = [self SHA256withECDSA_JWT:privateKeyPEM and:toSignDat];
NSString *signedHashBase64 = [signedHash base64EncodedStringWithOptions:0];
signedHashBase64 = [signedHashBase64 stringByReplacingOccurrencesOfString:@"/" withString:@"_"];
signedHashBase64 = [signedHashBase64 stringByReplacingOccurrencesOfString:@"+" withString:@"-"];
signedHashBase64 = [signedHashBase64 stringByReplacingOccurrencesOfString:@"=" withString:@""];

JWTtoken = [NSString stringWithFormat:@"%@.%@.%@",headerBase64,payloadBase64,signedHashBase64];
JanC commented 6 years ago

@flash23 thanks

@lolgear, here is a key directly from Apple in the format they deliver it

-----BEGIN PRIVATE KEY-----
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgaPxM+EysxcazERan
WfD/SwnB6HeAgfMLyVFBMXsrxU2gCgYIKoZIzj0DAQehRANCAASl233aKSR+l3e/
7qF80JQ2YG6PqEt3HYRyUPbfBQbaJNPfPDVsyrxo1wjYnYv7edt82vkF70pwDoGA
yCIXB1Jj
-----END PRIVATE KEY-----
lolgear commented 6 years ago

@JanC Nice! What about Public counterpart?

JanC commented 6 years ago

No, Apple gives us only the private key and they keep the public one for verification of the signature.

lolgear commented 6 years ago

@JanC Hoh, I would like to check both verification and signing in tests :)

grandima commented 6 years ago

Hi @flash23 could you provide a full example of your code or at least readPrivatePemToEC implementation? It'd be very helpful.

lolgear commented 6 years ago

@JanC Hi! Could you test latest master?

JanC commented 6 years ago

Hi I tried with the latest master (3.0.0-beta.11) and the following snippet still gives me the error

Error Domain=NSOSStatusErrorDomain Code=-50 "EC public key creation from data failed" UserInfo={NSDescription=EC public key creation from data failed}

Is it actually the correct way to sign?

NSString *algorithmName = @"ES256";

NSString *privateKey = @"-----BEGIN PRIVATE KEY-----\n"
                       "MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgaPxM+EysxcazERan\n"
                       "WfD/SwnB6HeAgfMLyVFBMXsrxU2gCgYIKoZIzj0DAQehRANCAASl233aKSR+l3e/\n"
                       "7qF80JQ2YG6PqEt3HYRyUPbfBQbaJNPfPDVsyrxo1wjYnYv7edt82vkF70pwDoGA\n"
                       "yCIXB1Jj\n"
                       "-----END PRIVATE KEY-----";

id <JWTAlgorithmDataHolderProtocol> signDataHolder = [JWTAlgorithmRSFamilyDataHolder new]
        .keyExtractorType([JWTCryptoKeyExtractor privateKeyWithPEMBase64].type)
        .algorithmName(algorithmName)
        .secret(privateKey);

// sign
NSDictionary *payloadDictionary = @{ @"hello": @"world" };

JWTCodingBuilder *signBuilder = [JWTEncodingBuilder encodePayload:payloadDictionary].addHolder(signDataHolder);
JWTCodingResultType *signResult = signBuilder.result;
NSString *token = nil;
if (signResult.successResult) {
    NSLog(@"%@ success: %@", self.debugDescription, signResult.successResult.encoded);
    token = signResult.successResult.encoded;
} else {
    NSLog(@"Signing failed: %@", signResult.errorResult.error);
}
ethanhuang13 commented 6 years ago

@JanC If you want to sign Apple's JWT on iOS/macOS, you could also try CupertinoJWT, which was pre-released recently. I've wrote it because I couldn't find a solution for creating JWT used for Apple's server APIs on iOS/macOS.

JanC commented 6 years ago

@ethanhuang13 thanks, that works fine

fengsh998 commented 5 years ago

Hi @lolgear @JanC

I made ES256 with openssl I verified it and it works. I am in hurry so I cant wait ;)

But maybe you can use the code for your lib. I will switch to your lib when u implement it

-(NSData*) SHA256withECDSA_JWT:(NSString*)privateKeyPEM and:(NSData*)dataToSign
{
    char *pemC = (char*)[privateKeyPEM cStringUsingEncoding:NSASCIIStringEncoding];

    EC_KEY *privEC;
    privEC = readPrivatePemToEC(pemC);

    NSMutableData *digest = [NSMutableData dataWithLength:CC_SHA256_DIGEST_LENGTH];
    CC_SHA256(dataToSign.bytes, (unsigned int)dataToSign.length, digest.mutableBytes);

    ECDSA_SIG *signature;
    signature = ECDSA_do_sign(digest.bytes, (unsigned int)digest.length, privEC);

    int signatureLength = i2d_ECDSA_SIG(signature, NULL);
    NSMutableData *derSignature = [[NSMutableData alloc] initWithLength:signatureLength];
    uint8_t *signatureBytes2 = derSignature.mutableBytes;
    i2d_ECDSA_SIG(signature, &signatureBytes2);

    const unsigned char* signatureBytes = [derSignature bytes];

    if (derSignature.length < 8 || signatureBytes[0] != (uint8_t)0x30) {
        //assert!
    }

    int offset;
    if (signatureBytes[1] > 0) {
        offset = 2;
    } else if (signatureBytes[1] == (uint8_t) 0x81) {
        offset = 3;
    } else {
        //assert!
    }

    int rLength = signatureBytes[offset + 1];

    int i = rLength;
    while ((i > 0) && (signatureBytes[(offset + 2 + rLength) - i] == 0))
        i--;

    int sLength = signatureBytes[offset + 2 + rLength + 1];

    int j = sLength;
    while ((j > 0) && (signatureBytes[(offset + 2 + rLength + 2 + sLength) - j] == 0))
        j--;

    int rawLen = MAX(i, j);
    rawLen = MAX(rawLen, 64 / 2);

    if ((signatureBytes[offset - 1] & 0xff) != derSignature.length - offset
        || (signatureBytes[offset - 1] & 0xff) != 2 + rLength + 2 + sLength
        || signatureBytes[offset] != 2
        || signatureBytes[offset + 2 + rLength] != 2) {
        //assert!
    }

    NSMutableData *concatSignature = [[NSMutableData alloc] initWithCapacity:2 * rawLen];

    [concatSignature replaceBytesInRange:NSMakeRange(rawLen - i, i) withBytes:(signatureBytes + (offset + 2 + rLength) - i)];
    [concatSignature replaceBytesInRange:NSMakeRange(2 * rawLen - j, j) withBytes:(signatureBytes + (offset + 2 + rLength + 2 + sLength) - j)];

    EC_KEY_free(privEC);
    ECDSA_SIG_free(signature);

    return concatSignature;
}
NSMutableDictionary *dicPayload = [[NSMutableDictionary alloc] init];
//dicPayload all useful info goes in here 
NSError *error;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dicPayload options:0 error:&error];

NSString *payloadBase64 = [jsonData base64EncodedStringWithOptions:0];
payloadBase64 = [payloadBase64 stringByReplacingOccurrencesOfString:@"/" withString:@"_"];
payloadBase64 = [payloadBase64 stringByReplacingOccurrencesOfString:@"+" withString:@"-"];
payloadBase64 = [payloadBase64 stringByReplacingOccurrencesOfString:@"=" withString:@""];

NSMutableDictionary *dicHeader = [[NSMutableDictionary alloc] init];

[dicHeader setObject:@"ES256" forKey:@"alg"];
jsonData = [NSJSONSerialization dataWithJSONObject:dicHeader options:0 error:&error];
NSString *headerBase64 = [jsonData base64EncodedStringWithOptions:0];
headerBase64 = [headerBase64 stringByReplacingOccurrencesOfString:@"/" withString:@"_"];
headerBase64 = [headerBase64 stringByReplacingOccurrencesOfString:@"+" withString:@"-"];
headerBase64 = [headerBase64 stringByReplacingOccurrencesOfString:@"=" withString:@""];

NSString *toSignStr = [NSString stringWithFormat:@"%@.%@",headerBase64,payloadBase64];
NSData *toSignDat = [toSignStr dataUsingEncoding:NSUTF8StringEncoding];

NSData *signedHash = [self SHA256withECDSA_JWT:privateKeyPEM and:toSignDat];
NSString *signedHashBase64 = [signedHash base64EncodedStringWithOptions:0];
signedHashBase64 = [signedHashBase64 stringByReplacingOccurrencesOfString:@"/" withString:@"_"];
signedHashBase64 = [signedHashBase64 stringByReplacingOccurrencesOfString:@"+" withString:@"-"];
signedHashBase64 = [signedHashBase64 stringByReplacingOccurrencesOfString:@"=" withString:@""];

JWTtoken = [NSString stringWithFormat:@"%@.%@.%@",headerBase64,payloadBase64,signedHashBase64];

i need demo, can you help me?i email, fengsh998@163.com

lolgear commented 5 years ago

@fengsh998 You can use Tests project and Certificates.xcasset inside it.