Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Secure Enclave and signing #186

Open
hakonk opened this issue Mar 9, 2018 · 24 comments
Open

Support for Secure Enclave and signing #186

hakonk opened this issue Mar 9, 2018 · 24 comments

Comments

@hakonk
Copy link

hakonk commented Mar 9, 2018

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
Copy link
Collaborator

lolgear commented Mar 9, 2018

@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
Copy link
Author

hakonk commented Mar 10, 2018

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

@lolgear
Copy link
Collaborator

lolgear commented Mar 12, 2018

@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
Copy link
Collaborator

lolgear commented Mar 12, 2018

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

@hakonk
Copy link
Author

hakonk commented Mar 13, 2018

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

@hakonk
Copy link
Author

hakonk commented Mar 13, 2018

@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
Copy link
Collaborator

lolgear commented Mar 13, 2018

@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
Copy link
Collaborator

lolgear commented Mar 15, 2018

@hakonk
Try latest master again :)

@hakonk
Copy link
Author

hakonk commented Mar 20, 2018

Hi, sorry for the late reply!

I will have a look at the implementation :)

@bencallis
Copy link

@hakonk how did you get on with this?

@hakonk
Copy link
Author

hakonk commented Jan 13, 2019

@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
Copy link

Hi may I know any updates on this?

@lolgear
Copy link
Collaborator

lolgear commented Mar 8, 2019

@AyeChanPyaeSone
Can you check latest master?

JWTCryptoKey.h has protocol for it.

@protocol JWTCryptoKey__Raw__Generator__Protocol
- (instancetype)initWithSecKeyRef:(SecKeyRef)key;
@end

@AyeChanPyaeSone
Copy link

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
Copy link

thanks you very much for your help 👍

@AyeChanPyaeSone
Copy link

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

@lolgear
Copy link
Collaborator

lolgear commented Mar 8, 2019

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

Thanks!

@b00tsy
Copy link

b00tsy commented Nov 1, 2019

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
Copy link
Collaborator

lolgear commented Nov 1, 2019

@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
Copy link

b00tsy commented Nov 1, 2019

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
Copy link
Collaborator

lolgear commented Nov 2, 2019

@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
Copy link
Collaborator

lolgear commented Nov 2, 2019

@b00tsy BTW, I think that

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

is unnecessary in new release.

@b00tsy
Copy link

b00tsy commented Nov 2, 2019

@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
Copy link

b00tsy commented Nov 2, 2019

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...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants