yourkarma / JWT

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

Support for Secure Enclave and signing #186

Open hakonk opened 6 years ago

hakonk commented 6 years ago

Hi! Thanks for making this awesome lib. I have a question regarding Secure Enclave and signing on the iOS platform (i.e., with devices affording this capability).

From reading the documentation, it seems the private key used for signing has to be passed via a String or Data instance. However, if the private key is generated and stored in the secure enclave it is my understanding it cannot be retrieved and thus not be used with the current implementation of yourcarma/JWT. From what I understand, signing will have to be done via SecKeyCreateSignature where the private key will be passed in as a SecKey (see code example below). Do you have any plans for supporting this in the future?

let access = SecAccessControlCreateWithFlags(
    kCFAllocatorDefault,
    kSecAttrAccessibleAlways,
    .privateKeyUsage,
    nil)!

let attributes: [CFString: Any] = [
    kSecAttrKeyType: kSecAttrKeyTypeEC,
    kSecAttrKeySizeInBits: 256,
    kSecAttrTokenID: kSecAttrTokenIDSecureEnclave,
    kSecPrivateKeyAttrs: [
        kSecAttrApplicationTag: "privateKeyTag",
        kSecAttrAccessControl: access,
        kSecAttrCanSign: true
    ]
]

let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, nil)

let signature = SecKeyCreateSignature(privateKey!, .ecdsaSignatureMessageX962SHA256, joinedData as CFData, nil) as? Data

Regards, Håkon

lolgear commented 6 years ago

@hakonk Hi! Nice to hear that this library helps you :) Well, your method could work only in iOS 10.0 and later :)

Yes, somewhere in future these API methods could replace existing API methods.

For now we could add one more node in inheritance tree ( Look at implementation of RS algorithms ). Yes, SecKeyCreateSignature is a great replacement for inconvenience old API. It will be in account.

If you have ready-to-use solution - feel free to PR :)

hakonk commented 6 years ago

Thanks for replying so quickly! If I find a solution, I'll see if I can make a PR :)

lolgear commented 6 years ago

@hakonk maybe you have test data for EC algorithms? I suppose that your investigation into Security framework could have this task (EC algorithms usage) under the hood.

lolgear commented 6 years ago

@hakonk happy to announce, you could check latest master for that ;)

hakonk commented 6 years ago

@lolgear Great! I'll have a look :)

hakonk commented 6 years ago

@lolgear

After looking into the code base, I'm having some difficulties understanding how this can be used with the existing implementation. While signing the secret is provided as a string with RS256:

NSString *algorithmName = @"RS256";

  id <JWTAlgorithmDataHolderProtocol> signDataHolder = [JWTAlgorithmRSFamilyDataHolder new].keyExtractorType([JWTCryptoKeyExtractor privateKeyWithPEMBase64].type).algorithmName(algorithmName).secret(privateKey);
JWTCodingBuilder *signBuilder = [JWTEncodingBuilder encodePayload:claims].addHolder(signDataHolder);
  JWTCodingResultType *signResult = signBuilder.result;

I can't seem to figure out how I should use the existing API to sign using a SecKeyRef instead. Could you please enlighten me?

Thanks in advance, I appreciate you taking the time to pursue the issue.

lolgear commented 6 years ago

@hakonk Well... Yes, it isn't supported yet. But no worry, you could add method for this easily.

First of all, you could set keys as JWTCryptoKeyProtocol items.

Look at convenient setters in JWTAlgorithmRSFamilyDataHolder

holder
.signKey(item_which_sign)
.verifyKey(item_which_verify)

Now you want to provide a key for that.

This library assume that your keys are stored in .pem format or in .p12 format. But it always convert put them into SecKeyRef instance which is wrapped into JWTCryptoKey.

So, you only need to add method that set SecKeyRef for JWTCryptoKey items.

But here is another trouble which could be avoided. This library add keys and put them into Security Enclave ( into keychain ). After usage it removes them. It identifies keys by tags. ( That is why it is a condition to protocol JWTCryptoKeyProtocol - item should have property tag ).

So, at the end you have something like:

JWTCryptoKeyPrivate *key = [[JWTCryptoKeyPrivate alloc] initWithRawSecKeyRef:keyRef];
holder.signKey(key);

UPD: modern Apple API support added instead, sorry :)

lolgear commented 6 years ago

@hakonk Try latest master again :)

hakonk commented 6 years ago

Hi, sorry for the late reply!

I will have a look at the implementation :)

bencallis commented 5 years ago

@hakonk how did you get on with this?

hakonk commented 5 years ago

@bencallis Since I ended up approaching the problem in a different way, I no longer was dependent on this library, and thus I didn't look into what was needed to make it work with the signatures created in the Secure Enclave. Are you looking into the issue yourself?

AyeChanPyaeSone commented 5 years ago

Hi may I know any updates on this?

lolgear commented 5 years ago

@AyeChanPyaeSone Can you check latest master?

JWTCryptoKey.h has protocol for it.

@protocol JWTCryptoKey__Raw__Generator__Protocol
- (instancetype)initWithSecKeyRef:(SecKeyRef)key;
@end
AyeChanPyaeSone commented 5 years ago

JWTCryptoKeyPrivate *key = [[JWTCryptoKeyPrivate alloc] initWithSecKeyRef:privateKey];

//Add fake data as secretData its a bug from library
id <JWTAlgorithmDataHolderProtocol> signDataHolder = [JWTAlgorithmRSFamilyDataHolder new]
.signKey(key)
.algorithmName(algorithmName)
.secretData(fakeData);

is this correct approach? I am using the algorith "ES256" please let me know is there anything wrong. However when i pass to server the signature seems invalid.

AyeChanPyaeSone commented 5 years ago

thanks you very much for your help 👍

AyeChanPyaeSone commented 5 years ago

I have to add secretData because it forced me to add secretData.

lolgear commented 5 years ago

@AyeChanPyaeSone I have added you to Gitter room. Could we continue there?

Thanks!

b00tsy commented 5 years ago

Hi @lolgear, thanks for developing and maintaining this library for such a long time.

I'm currently experimenting with secure enclave keys and ran into this issue:

let payload = ["deviceId": "123123123"]

let privateJWTKey = JWTCryptoKeyPrivate(secKeyRef: privateKeyRef)
let publicJWTKey = JWTCryptoKeyPublic(secKeyRef: publicKeyRef)

var signDataHolder = JWTAlgorithmRSFamilyDataHolder.createWithAlgorithm256()
signDataHolder = signDataHolder?.signKey(privateJWTKey)
signDataHolder = signDataHolder?.verifyKey(publicJWTKey)
signDataHolder = signDataHolder?.secretData(Data()) // as required due to a bug

let signBuilder = JWTEncodingBuilder.encodePayload(payload)?.addHolder(signDataHolder)
let signResult = signBuilder?.encode
let signResultSuccess = signResult?.successResult
let encoded = signResultSuccess?.encoded
let signResultError = signResult?.errorResult
let signError = signResultError?.error

This results in that error:

algid:sign:RSA:message-PKCS1v15:SHA256: algorithm not supported by the key

Digging into encodeWithAlgorithm:withHeaders:withPayload:withSecretData:withError I see that theAlgorithmName is equal to @"RS256" althought it should be @"ES256" (keys in the secure enclave are ES256 to my understanding).

In chooseAlgorithm: of JWTAlgorithmAsymmetricBase also this case is chosen JWTAlgorithmAsymmetricBase__AlgorithmType__RS: return kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA256;although it should be this case JWTAlgorithmAsymmetricBase__AlgorithmType__ES: return kSecKeyAlgorithmECDSASignatureMessageX962SHA256;(or I'm using the JWTAlgorithmRSFamilyDataHolder wrong).

If I manually override the values the encode succeeds without error. Decoding however (both with JWT and jwt.io) fails.

Do you have any hints? Btw. I'm on 3.0.0-beta.12.

Thanks!

lolgear commented 5 years ago

@b00tsy Yeah, I see. Raw SecKey values aren't determine correct types of encryption. Well, we have the following problem dualistic problem.

Algorithm in JWT is a name of preferred algorithm for encryption. Algorithm in Security framework is a way to encrypt/decrypt data.

On one side we have public property of desired encryption ( JWT part ), on other side - private property of a key entity.

Would you like to map private property of key to a public property of JWT?

Public JWT property -> Security Algorithm ( nice good ) Private property of key -> Security Algorithm -> Public JWT property (???)

You do everything correct, but what you really want is to specify algorithm type ( by public JWT name ) to allow and sign further actions.

Next, what you can do in this version ( I don't test it ) is to set algorithmName directly

var signDataHolder = JWTAlgorithmRSFamilyDataHolder.createWithAlgorithm256().algorithmName(@"ES256");

You can dump key to its string representation, as I remember, by calling

@interface JWTCryptoSecurity (ExternalRepresentation)
+ (NSData *)externalRepresentationForKey:(SecKeyRef)key error:(NSError *__autoreleasing*)error;
@end
b00tsy commented 5 years ago

Thanks for the quick response.

This works without error (both signing and verifying):

let paylod = ["deviceId": "123123123"]

let privateJWTKey = JWTCryptoKeyPrivate(secKeyRef: privateKeyRef)
let publicJWTKey = JWTCryptoKeyPublic(secKeyRef: publicKeyRef)

var signDataHolder = JWTAlgorithmRSFamilyDataHolder.createWithAlgorithm256()
signDataHolder = signDataHolder?.algorithmName("ES256")
signDataHolder = signDataHolder?.signKey(privateJWTKey)
signDataHolder = signDataHolder?.verifyKey(publicJWTKey)
signDataHolder = signDataHolder?.secretData(Data()) // setting .secretData(Data()) is currently mandatory because of a bug

let signBuilder = JWTEncodingBuilder.encodePayload(paylod)?.addHolder(signDataHolder)
let signResult = signBuilder?.encode
let signResultSuccess = signResult?.successResult
let encoded = signResultSuccess?.encoded
let signResultError = signResult?.errorResult
let signError = signResultError?.error

let verifyBuilder = JWTDecodingBuilder.decodeMessage(encoded)?.addHolder(signDataHolder)
let verifyResult = verifyBuilder?.decode
let verifyResultSuccess = verifyResult?.successResult
let headers = verifyResultSuccess?.headers
let payload = verifyResultSuccess?.payload
let claimsSet = verifyResultSuccess?.claimsSet
let verifyResultError = verifyResult?.errorResult
let verifyError = verifyResultError?.error

However, verifying externally fails (jwt.io, or nodejs jsonwebtoken or jose).

jsonwebtoken is very specific about the error: TypeError: "ES256" signatures must be "64" bytes, saw "70".

Any hints?

PS sorry about trashing your issues with help requests. We could make a howto afterwards for how to use the secure enclave with your library (all other JWT libraries have no support for secure enclave at all)...

lolgear commented 5 years ago

@b00tsy Could you attach example project with keys that you are using? Maybe some kind of aligning or unnecessary symbols at the end of signature could cause this issue.

lolgear commented 5 years ago

@b00tsy BTW, I think that

.secretData(Data()) // setting .secretData(Data()) is currently mandatory because of a bug

is unnecessary in new release.

b00tsy commented 5 years ago

@lolgear

The private key itself is not exportable (within secure enclave chip on the processor). This is a (hopefully) working example which requires the pod EllipticCurveKeyPair.

Call it from objective-c via [TestKeyManager testECKeyJWT]. To get a key in the secure encalve you need to run it on a real device (not the simulator).

Create a separate swift file with this:

import EllipticCurveKeyPair

struct KeyPairDemo {
    static let manager: EllipticCurveKeyPair.Manager = {
        let publicAccessControl = EllipticCurveKeyPair.AccessControl(protection: kSecAttrAccessibleAlwaysThisDeviceOnly, flags: [])
        let privateAccessControl = EllipticCurveKeyPair.AccessControl(protection: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, flags: [.privateKeyUsage])

        let publicLabel = "key.public.test"
        let privateLabel = "key.private.test"

        let config = EllipticCurveKeyPair.Config(
            publicLabel: publicLabel,
            privateLabel: privateLabel,
            operationPrompt: "PROMPT!?!",
            publicKeyAccessControl: publicAccessControl,
            privateKeyAccessControl: privateAccessControl,
            publicKeyAccessGroup: nil,
            privateKeyAccessGroup: nil,
            fallbackToKeychainIfSecureEnclaveIsNotAvailable: true)
        return EllipticCurveKeyPair.Manager(config: config)
    }()
}

@objcMembers class TestKeyManager: NSObject {
    class func publicKeyRef() -> SecKey? {
        do {
            let publicKey = try KeyPairDemo.manager.publicKey()
            let publicKeyRef = publicKey.underlying
            return publicKeyRef
        } catch {
            print("\(error)")
        }
        return nil
    }

    class func privateKeyRef() -> SecKey? {
        do {
            let privateKey = try KeyPairDemo.manager.privateKey()
            let privateKeyRef = privateKey.underlying
            return privateKeyRef
        } catch {
            print("\(error)")
        }
        return nil
    }

    class func testECKeyJWT() {
        do {

            let privateKeyRef = TestKeyManager.privateKeyRef()
            let publicKeyRef = TestKeyManager.publicKeyRef()

            let paylod = ["deviceId": "123123123"]

            let privateJWTKey = JWTCryptoKeyPrivate(secKeyRef: privateKeyRef)
            let publicJWTKey = JWTCryptoKeyPublic(secKeyRef: publicKeyRef)

            var signDataHolder = JWTAlgorithmRSFamilyDataHolder.createWithAlgorithm256()
            signDataHolder = signDataHolder?.algorithmName("ES256")
            signDataHolder = signDataHolder?.signKey(privateJWTKey)
            signDataHolder = signDataHolder?.verifyKey(publicJWTKey)
            signDataHolder = signDataHolder?.secretData(Data()) // setting .secretData(Data()) is currently mandatory because of a bug

            let signBuilder = JWTEncodingBuilder.encodePayload(paylod)?.addHolder(signDataHolder)
            let signResult = signBuilder?.encode
            let signResultSuccess = signResult?.successResult
            let encoded = signResultSuccess?.encoded
            let signResultError = signResult?.errorResult
            let signError = signResultError?.error
            print("\(encoded)")

            let verifyBuilder = JWTDecodingBuilder.decodeMessage(encoded)?.addHolder(signDataHolder)
            let verifyResult = verifyBuilder?.decode
            let verifyResultSuccess = verifyResult?.successResult
            let headers = verifyResultSuccess?.headers
            let payload = verifyResultSuccess?.payload
            let claimsSet = verifyResultSuccess?.claimsSet
            let verifyResultError = verifyResult?.errorResult
            let verifyError = verifyResultError?.error
            print("\(payload)")

        } catch {
            print("\(error)")
        }
    }
}
b00tsy commented 5 years ago

A very simple approach to circumvent all the issues (incompatiblity with other third party tools regarding jwt, encryption, signing and validation) created with the key and the key format of keys from the secure encalve could be to save a normal RSA key in the keychain and encrypt it on top of that with a key that's in the secure enclave... That would have the best ratio of compatibility and security for me...