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

Add Elliptic Curve variants for JWS and JWK utilities #88

Merged
22 commits merged into from
Jan 21, 2019

Conversation

jarrodmoldrich
Copy link
Contributor

Hello Airside/Mohemian team,

I needed EC algorithms for signing/verifying JWTs, and for decoding/encoding JWKs, so I thought it prudent to fork JOSESwift. The functionality for JWS and JWK should be complete and working, but I've yet to mirror your comprehensive test suite for RSA to the EC domain. I will be doing so over the coming weeks 🙂

Anecdotally, JWK decoding/encoding and JWS verification are working for ES256 public keys for the project I'm working on.

This is a WIP, tell me if there's any issues with it. It would be good to see this eventually hit upstream so we can retarget to master.

Cheers,

Jarrod

@ghost
Copy link

ghost commented Jul 6, 2018

👋 Hi Jarrod

That's great news! 🎉

We were actually planning on supporting EC keys in our JWK implementation and consequently using them for signing and/or encryption for the mid-term future, so any help in that domain is greatly appreciated.

@carol-mohemian and I will have a look at your code over the next days and provide some feedback. At a first glance, it already looks really great!

Once we have some progress on the test suite, we'd be happy to merge this to master and put it in the next release of JOSESwift. 😎

Thanks again, you're awesome
Daniel

@ghost ghost requested review from a user, carol-mohemian and mohemian-92817281 July 6, 2018 07:33
@jarrodmoldrich
Copy link
Contributor Author

Hi @daniel-mohemian 👋

Thank you for the warm welcome. I'll be pecking away at this in my downtime, so it might be a 4-8 week turn-around for the full test suite. Let me know if that conflicts with you, and I'll see what I can do.

Cheers,

Jarrod

Copy link
Contributor

@carol-mohemian carol-mohemian left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @jarrodmoldrich from my side as well 👋

Had a first look at your PR and it already looks really amazing, well done 👏 I only had a look at the structure and didn't get my head around the logic yet.

Just found a few typos and commented some structural improvements, looking forward to your replies 😁

public enum SignatureAlgorithm: String {
case RS256 = "RS256"
case RS512 = "RS512"
case ES256 = "ES256"
case ES384 = "ES384"
case ES521 = "ES521"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should say ES512 for key and value.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, got my P-521s and ES512s mixed up 🤦‍♂️

//
// Created by Jarrod Moldrich on 02.07.18.
//

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing license.

case yNotBase64URLUIntEncoded
case privateKeyNotBase64URLUIntEncoded
case invalidCurveType
case compressedCurvePointsUnsuported
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo Unsuported -> Unsupported.

Would either rename this error to unsupportedCompressedCurvePoints or the error below to curvePointSizeUnsupported for consistency.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I'll fix the typo.

We're specifically not supporting compressed curve points at all, so I'm not sure the other two suggestions effectively convey this limitation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you mean about the "error below". GitHub doesn't provide surrounding context, but I'd agree that the second error could be invalidCurvePointOctetLength.


static func fromKeyBitLength(_ length: Int) -> ECCurveType? {
switch length {
case 256:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should use ECCurveType.P256.keyBitLength, etc.


static func fromCoordinateOctetLength(_ length: Int) -> ECCurveType? {
switch length {
case 32:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above with ECCurveType.P256.coordinateOctetLength, etc.

Copy link
Contributor Author

@jarrodmoldrich jarrodmoldrich Jul 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand what you want. They are static functions that map a value to an enum value, not a property of an enum value. There's probably a Swiftier way to implement this, so if you could provide an example, I'd be more than happy to oblige.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just thought that you can use the ECCurveType enum and it's values for keyBitLength/coordinateOctetLength to not hardcode those length values again.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, sorry, I misunderstood. That's done 👍

let status = SecKeyRawVerify(publicKey, .sigRaw, digest, digest.count, signatureBytes, curveType.signatureOctetLength)
if status != 0 {
throw ECError.verifyingFailed(
description: "Error validating signature. (OSStatus: \(status))"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this fit in one line?


// MARK: Protocols

/// The components of an EC public key.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One whitespace too much between "EC" and "public".


import Foundation

/// A `Verifier` to verify a signature created with a elliptic curve algorithm.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an elliptic curve..

@@ -51,6 +52,8 @@ public struct Signer<KeyType> {
}
// swiftlint:disable:next force_cast
self.signer = RSASigner(algorithm: signingAlgorithm, privateKey: privateKey as! RSASigner.KeyType)
default:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why you don't add the ECSigner here?

return nil
}
self.verifier = ECVerifier(algorithm: verifyingAlgorithm, publicKey: publicKey as! ECVerifier.KeyType)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty new line.

@ghost
Copy link

ghost commented Jul 12, 2018

Sure, sounds great @jarrodmoldrich. 👍

If you want, we can also help out with some tests. It would be cool if you could create a basic test suite and maybe use some examples from the relevant RFCs as the test data. We can then still expand the test suite after merging this PR.

As said, the implementation looks really great already. 🎉
It would be cool if you could work in the comments that @carol-mohemian had if you agree. Please also make sure to put the license into every newly generated file and make sure that it says Copyright 2018 Airside Mobile Inc..

Let me know if you need anything!

@@ -26,9 +27,13 @@ import Foundation
/// An algorithm for signing and verifying.
///
/// - RS512: [RSASSA-PKCS1-v1_5 using SHA-512](https://tools.ietf.org/html/rfc7518#section-3.3)
/// - ES512: [ECDSA P-512 using SHA-512](https://tools.ietf.org/html/rfc7518#section-3.4)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you have another typo here. The curve should be P-521. 😉

@jarrodmoldrich
Copy link
Contributor Author

@daniel-mohemian It probably makes sense that I create a minimal test suite. I think the sooner this PR gets merged, the better, as there's a lot of opportunity to refactor out much of the shared functionality between the key types.

The Apache license seemed to me a little ambiguous about the copyright notice, but on a second read, a PR classes more as a 'Contribution' than a 'Derivative Work' and so I've changed to your copyright notice

I'll be AFK for the next couple of weeks, so I probably won't start on the test suite until August. Feel free to leave feedback in the meantime 🙂

@ghost
Copy link

ghost commented Aug 30, 2018

Hi @jarrodmoldrich 👋

I just wanted to check in with you to see if you need any assistance with the tests. If you want we can also take over a part of the test suite. If you're back online already, let me know what you think, otherwise, enjoy your time off. 🙂

@ghost ghost added the WIP label Aug 30, 2018
@jarrodmoldrich
Copy link
Contributor Author

Thanks for checking in, @daniel-mohemian 🙂

I'm actually back online, though my schedule was pushed back by about a month. From next week I'll be in a situation where I'll be able to work on the test suite again, so I should be able to deliver a minimal suite by the end of September. If you need something sooner for your own purposes, I'm happy for you to take the reins, otherwise there should be a trickle of check-ins next month.

@ghost
Copy link

ghost commented Sep 4, 2018

That sounds great. Just let me know if you need anything. We're not in a hurry, end of September sounds like a plan! 😄

@jarrodmoldrich
Copy link
Contributor Author

@daniel-mohemian It's a good thing you're not in a hurry! Things were a bit hectic for me this past month, but I should finally have some free time at the end of this coming week. I was going to make a start by adding test data to CryptoTestCase.swift, maybe separating/refactoring any general functionality if it makes sense, and then building the related test cases - any input is appreciated. So hopefully there should be some movement on this PR soon.

@ghost
Copy link

ghost commented Oct 3, 2018

That's fine, @jarrodmoldrich.

Refactoring and expanding the base class for crypto tests - CryptoTestCase.swift - sound great! Don't be afraid to rearrange things if you see the need. The tests are still a bit chaotic here and there. As you said, you can then add some EC specific tests.

  • Crypto/ contains tests for the crypto primitives. It would be a good place to test your EC, ECSigner, ECVerifier there.
  • JWS/ contains JWS specific tests such as roundtrips and serialization. Maybe try to add some tests that serialize, deserialize, sign and verify a JWS with EC keys.
  • JWK/ contains tests specific to the JWK representations of keys. It would be great if you could test the conversion from SecKey <->Data <-> ECPublic/PrivateKey there.

Let me know if you need anything!

@ghost
Copy link

ghost commented Nov 2, 2018

I see you added quite some tests @jarrodmoldrich. 🎉

Please ping me once they are ready to be looked at.

PS: Don't worry about the merge conflicts, I can take care of that for you before merging.

@jarrodmoldrich
Copy link
Contributor Author

Sorry for the delay, @daniel-mohemian . I've been occupied the last few months, but I'll have this ready for your review at my self-imposed deadline of Jan 14.

@ghost
Copy link

ghost commented Jan 8, 2019

Don't worry @jarrodmoldrich, we've been quite packed with work as well over the last months, so we're not in a hurry. Once you think your work is ready, just ping me and I'll take a look as soon as I can. I can also take care of the merge conflicts for you if you want. Thanks again for your efforts. 😄

@jarrodmoldrich
Copy link
Contributor Author

jarrodmoldrich commented Jan 10, 2019

Happy New Year @daniel-mohemian and the Airside crew 🎉

The test suite has been updated to test all EC features, with close to 100% coverage. I was about to tackle a merge with master, but looking at the git logs it appears I've rebased off the latest some time ago - unless there's a different branch I should be merging with?

I just realized I haven't pulled down from upstream. I'll do that now and try merging. In the meantime it might be worth reviewing before I pollute the diff with merges.

Upstream needed to be integrated with the split in the `CryptoTestCase`
test data class, as well as trivially merged with new entries in various
enums.
@jarrodmoldrich
Copy link
Contributor Author

@daniel-mohemian Is SJCommonCrypto gone for good? The EC signing implementation is using CommonCrypto to create a digest. I could fish around to see if there's some new way to write the signing algorithm, but it would be much easier to drag CC back in.

@carol-mohemian
Copy link
Contributor

Hey @jarrodmoldrich 👋 Please see the PR description of #126.

TL;DR

Swift 4.2 introduced a native common crypto module which we can/must use now.

@jarrodmoldrich
Copy link
Contributor Author

Thanks @carol-mohemian . I think it's finally ready for review.

@ghost
Copy link

ghost commented Jan 10, 2019

Awesome @jarrodmoldrich, I hope we can provide feedback soon! We're also quite packed with work but I'll do my best to get the pull request merged over the next weeks. At the very latest in February I'll have some more time on my hands. I hope this is ok for you.

We already looked over it when you first opened the pull request and it looked really good back then. 👌

Oh and happy new year to you too! 🎉

@ghost ghost removed the WIP label Jan 17, 2019
Copy link

@ghost ghost left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jarrodmoldrich I just looked over your implementation once again, and it still looks great. 👌 I only had some minor comments. Nothing in the way of merging. ✅

I will look over the tests first thing next week so expect the pull request to be merged latest by the end of next week. I'll ping you if I have questions regarding the tests.

Until then, have a nice weekend. 😎

JOSESwift/Sources/CryptoImplementation/EC.swift Outdated Show resolved Hide resolved
JOSESwift/Sources/CryptoImplementation/EC.swift Outdated Show resolved Hide resolved
JOSESwift/Sources/JWKSetCodable.swift Outdated Show resolved Hide resolved
case typeIsUnknown
}

class JWKBase: Decodable {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I see what JWKBase is supposed to do - finding out what type of encoded key we have by throwing specific "errors".

But is it used somewhere? It looks like it is not needed because further down below, we just try to decode all key types and take the first one that we can successfully decode. I think both implementations are somewhat valid approaches to determine the key type (even if they both are a bit hacky) but it looks like we just need one of them.

Copy link
Contributor Author

@jarrodmoldrich jarrodmoldrich Jan 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking back at an earlier commit I had modified init(from decoder: Decoder) to look like it is below, but during the integration I must have reverted it to its original style. I think it looks a little neater the way I've done it, so I could either revert the approach once more, or remove JWKBase. I'll defer to your judgement.

            do {
                _ = try keyContainer.decode(JWKBase.self)
            } catch JWKTypeError.typeIsECPublic {
                key = try keyContainer.decode(ECPublicKey.self)
            } catch JWKTypeError.typeIsECPrivate {
                key = try keyContainer.decode(ECPrivateKey.self)
            } catch JWKTypeError.typeIsRSAPublic {
                key = try keyContainer.decode(RSAPublicKey.self)
            } catch JWKTypeError.typeIsRSAPrivate {
                key = try keyContainer.decode(RSAPrivateKey.self)
            }

Copy link

@ghost ghost Jan 21, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like your approach, let's go for that!

One suggestion from me would be to use the raw values of JWKKeyType in the cases in JWKBase like this:

// ...
switch key {
case JWKKeyType.RSA.rawValue:
    if d == nil {
        throw JWKTypeError.typeIsRSAPublic
    } else {
        throw JWKTypeError.typeIsRSAPrivate
    }
}
// ...

What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, yes, I should be doing that. Updated, along with a fix for the catch mechanism I just noticed. (Not using optional try?.)

JOSESwift/Sources/JWSHeader.swift Show resolved Hide resolved
ghost
ghost previously approved these changes Jan 21, 2019
Copy link

@ghost ghost left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jarrodmoldrich Awesome, your tests cover pretty much all there is to cover. 👍 I'd say we're ready to merge ✅ Thanks for sticking around and providing this great test coverage!

Feel free to push the one JWKBase decoding trick we talked about here if you want, I'll leave it up to you.

Otherwise, let's merge. ↩️ 🚀 🎉


// As any two signing invocations will have different nonces, it is impossible to use pre-generated data from a
// trusted implementation (e.g. openssl) to verify the signature. Instead we will validate by verifying the
// signature. This assumes that the verification implementation is correct.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

let dict = checkAndGetDictionary(jsonData: jsonData)

checkRegularParameters(dict: dict, keyData: keyData)
XCTAssertEqual(dict["d"] as? String ?? "", keyData.expectedPrivateBase64Url)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
XCTAssertEqual(dict["d"] as? String ?? "", keyData.expectedPrivateBase64Url)
XCTAssertEqual(dict["d"] as? String ?? "", keyData.expectedPrivateBase64Url)
XCTAssertNil(dict["breeze"])

@jarrodmoldrich
Copy link
Contributor Author

@daniel-mohemian Implemented your suggestions and integrated the decoding pattern with a couple of changes above what we discussed, hopefully they are all good 🤞

Copy link

@ghost ghost left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome @jarrodmoldrich let's merge this! ✅ We'll release throughout the week, probably as 1.5.0. I'll ping you here once it's out.

Thanks again, it was great to work with you.

@ghost ghost merged commit 37422f0 into airsidemobile:master Jan 21, 2019
@jarrodmoldrich
Copy link
Contributor Author

Likewise, @daniel-mohemian 🙂

This was referenced Jan 22, 2019
@ghost
Copy link

ghost commented Jan 23, 2019

Your contribution is now released as part of 1.5.0. 🎉

This pull request was closed.
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

Successfully merging this pull request may close these issues.

2 participants