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 support for JWKs with EC key type #371

Merged
merged 5 commits into from Dec 1, 2020

Conversation

richardlarocque
Copy link
Contributor

Adds support for JWKs with "kty" value "EC". This commit includes
support for curves ("crv") "P-256", "P-384", "P-521".

For additional details on these JWKs and their contents, see
https://tools.ietf.org/html/rfc7518#section-6.2.1.

This implementation adheres closely to the pattern set by
JWT::JWK::RSA. It keeps the same coding style and method names.
It also inherits a few quirks:

  • It ignores any private key ("d") values when importing JWKs.
  • It never emits private key ("d") values when exporting JWKs.
  • An import followed by an export does not preserve the "kid" value.

These behaviors seem strange to me, but I worry that changing the
convention would be surprising and potentially dangerous to existing
users of this library.

lib/jwt/jwk/ec.rb Outdated Show resolved Hide resolved
@richardlarocque richardlarocque force-pushed the richardlarocque/ec_jwk_upstreamable branch from 6346031 to 64bc94d Compare September 19, 2020 00:35
@anakinj
Copy link
Member

anakinj commented Sep 22, 2020

Looks great, thanks for your contribution.

Wondering about this 'An import followed by an export does not preserve the "kid" value.', this behaviour was not intended and is fixed in #320 for the RSA keys. Maybe this feature could be perfect from the beginning in regards to that :)

About not exporting private keys, I recall me thinking that it could be dangerous to export the private key by default. If we would like to support this we could add a parameter to the export method that would allow inclusion of the private parts.

lib/jwt/jwk/ec.rb Outdated Show resolved Hide resolved
jwk_crv, jwk_x, jwk_y, jwk_kid = jwk_attrs(jwk_data, %i[crv x y kid])
raise Jwt::JWKError, 'Key format is invalid for EC' unless jwk_crv && jwk_x && jwk_y

new(ec_pkey(jwk_crv, jwk_x, jwk_y), kid=jwk_kid)

Choose a reason for hiding this comment

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

Useless assignment to variable - kid.


def export
crv, x_octets, y_octets = keypair_components(keypair)
sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(x_octets, BINARY)),

Choose a reason for hiding this comment

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

Useless assignment to variable - sequence.

jwk_crv, jwk_x, jwk_y, jwk_kid = jwk_attrs(jwk_data, %i[crv x y kid])
raise Jwt::JWKError, 'Key format is invalid for EC' unless jwk_crv && jwk_x && jwk_y

new(ec_pkey(jwk_crv, jwk_x, jwk_y), kid=jwk_kid)

Choose a reason for hiding this comment

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

Surrounding space missing for operator =.

@richardlarocque richardlarocque force-pushed the richardlarocque/ec_jwk_upstreamable branch from 6b6895e to 7562cdf Compare September 23, 2020 18:29
crv = 'P-521'
x_octets, y_octets = encoded_point.unpack('xa66a66')
else
raise "Unsupported curve '#{ec_keypair.group.curve_name}'"
Copy link
Member

Choose a reason for hiding this comment

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

Missed this the first time around. The JWT::JWKError could be used here also

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My bad. Should be fixed now.

@phlegx
Copy link
Contributor

phlegx commented Sep 24, 2020

Hi! Why JWT::JWK::RSA and JWT::JWK::EC don't export private keys? It is defined in RFC7517 that also private keys can be exported. Also other libraries do that.

@anakinj
Copy link
Member

anakinj commented Sep 25, 2020

Hi @phlegx

Think that it would be a nice feature.

As mentioned before the main reason for it was to avoid accidental leaking of the private key, so still think that including the private key in the export should not be the default behaviour. In other words the private key should only be included when asked for.

@anakinj
Copy link
Member

anakinj commented Sep 25, 2020

@richardlarocque Looks good.

@phlegx
Copy link
Contributor

phlegx commented Sep 25, 2020

@anakinj the RFC describes JWK with:

A JSON Web Key (JWK) is a JavaScript Object Notation (JSON) data structure that represents a cryptographic key.

So, JWK is only a data structure that represents a cryptographic key. How a user expose a JWK to a web endpoint is not part of the RFC7517 and should not affect the behavior or structure of a JWK.

@anakinj
Copy link
Member

anakinj commented Sep 25, 2020

The need until now has been focused on the public parts of the keys (signature verification and exports in tests to simulate public JWK endpoints). So therefore the private parts have been totally left out.

Think that if we would support exporting of the private keys we should protect the uneducated user from leaking sensitive information and require the user to actually ask for the private parts to be included.

Made a quick proposal on how the private parts of the key could be included in the export #373

@phlegx
Copy link
Contributor

phlegx commented Sep 25, 2020

@anakinj it is clear for me thx! It is a good compromise for me to add an option if private key should be exported or not. We should focus to add this feature to RSA, EC and HMAC and if possible release a new version.

@phlegx
Copy link
Contributor

phlegx commented Sep 28, 2020

@richardlarocque I suggest to merge master branch to your PR, that includes the abstract class. Then you can adapt your PR to the abstract structure and the include_private option.

richardlarocque added a commit to getoutreach/ruby-jwt that referenced this pull request Sep 30, 2020
Merges in upstream changes to JWK code that occurred while
jwt#371 was under review.
@richardlarocque
Copy link
Contributor Author

Merged in upstream changes to fix conflicts. Please take another look and merge if desired. (I don't have permission to hit the "merge" button, even if there is an approving review.)


module JWT
module JWK
class EC
Copy link
Contributor

Choose a reason for hiding this comment

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

You have to inherit from abstract class here:

class EC < KeyAbstract

Comment on lines 9 to 10
attr_reader :keypair
attr_reader :kid
Copy link
Contributor

@phlegx phlegx Sep 30, 2020

Choose a reason for hiding this comment

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

This can be removed, because already included from abstract class.

# remove this two lines.
attr_reader :keypair
attr_reader :kid

def initialize(keypair, kid = nil)
raise ArgumentError, 'keypair must be of type OpenSSL::PKey::EC' unless keypair.is_a?(OpenSSL::PKey::EC)

@keypair = keypair
Copy link
Contributor

@phlegx phlegx Sep 30, 2020

Choose a reason for hiding this comment

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

Remove the line @keypair = keypair and add super instead.

@kid = kid || generate_kid(@keypair)
end

def export
Copy link
Contributor

Choose a reason for hiding this comment

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

Add parameter options like: def export(options = {})

Copy link
Contributor

@phlegx phlegx left a comment

Choose a reason for hiding this comment

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

Can you please implement methods private? and public_key like JWK HMAC and RSA (PR #373) does?
Or see how RSA JWK implements this methods:

def private?


def export
crv, x_octets, y_octets = keypair_components(keypair)
{
Copy link
Contributor

@phlegx phlegx Sep 30, 2020

Choose a reason for hiding this comment

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

Export private key if option include_private is set to true like HMAC or RSA JWK does.

@excpt excpt self-requested a review October 5, 2020 12:42
@richardlarocque
Copy link
Contributor Author

Sorry, I got distracted with other things and ended up not paying much attention to this PR.

I find that abstract classes are not very common in Ruby. There's no need to define stub methods that throw exceptions when they're not overridden; that's what NoMethodError is for. Ruby provides no compile-time enforcement of interfaces, so the best way to prove that two classes implement the same interface is with generic unit tests. If you need to share functionality between classes, that's usually done with mixins. In other words, the usual arguments for an abstract class apply as well in Ruby. (At least, that's my interpretation of https://www.poodr.com/, which is my preferred reference on Ruby style.)

But I'm not the one who has to live with this codebase. I'll defer to the maintainers on this one.

@excpt, @anakinj, do you have opinions on @phlegx's suggestions? I'm happy to implement it however you prefer.

@anakinj
Copy link
Member

anakinj commented Oct 6, 2020

I somewhat agree about the empty stubs. The naming is always hard, maybe in Ruby base classes are more common than abstracts.

But then again something that would bind these different types of keys together would be of benefit. Had a thought when this was discussed was that the list of supported types could be generated using something shared between these classes. Is that shared thing a base class or mixin i have no strong opinion on.

@anakinj
Copy link
Member

anakinj commented Oct 9, 2020

Added something small to the thing I'm currently working on in regards to the Base class for the different key types.
89247c4

And as @richardlarocque suggested I also removed the empty method stubs.

@phlegx
Copy link
Contributor

phlegx commented Oct 14, 2020

@richardlarocque can you please adapt your code to @anakinj base class and solve review issues?

@richardlarocque
Copy link
Contributor Author

I have reservations about rebasing on top of an uncommitted branch.

My goal in opening this PR was to get this functionality into the ruby-jwt upstream. I built this PR so that it fit the style of the project as it existed at the time, so that it would be easy to merge. My preference is that we merge this more or less as it is, in conformance with the old style.

If this project is in the middle of a refactor, and as a result we can't accept the PR as-is right now, that's OK. I just had bad timing, I guess. But I'd rather not get involved with the refactoring myself. I'd prefer to wait until that refactoring lands in master and then rebase my change against that.

I don't want this feature's fate to be tied to that of some refactoring work. I'd like to keep the two things separate.

I am willing to add support for the include_private flag on export and adding inheritance from KeyAbstract, though. That stuff is in master, so I'd be OK with making this PR conform to those standards, if that is sufficient to get this PR merged.

@d11wtq
Copy link

d11wtq commented Oct 27, 2020

Just a comment to say thank you so much for doing this. We need this functionality too and exploring the alternative JWT gems, I didn't like what I saw in some of the solutions. Would love to see this one get across the line. I'm happy to help in any way I can do too ❤️

@phlegx
Copy link
Contributor

phlegx commented Oct 27, 2020

@richardlarocque all other PR have been merged and are in master. Can you please adapt your code style and options to RSA and HMAC? Thank you so much.

@phlegx
Copy link
Contributor

phlegx commented Nov 6, 2020

@anakinj @excpt should I open a new PR with this code changes, so that we can continue?

@anakinj
Copy link
Member

anakinj commented Nov 7, 2020

@richardlarocque do you mind someone continue with this to get it into the next release or are you planning on continuing on this on your own?

Would be really great to get this into master.

@richardlarocque
Copy link
Contributor Author

Sorry I'm late to the follow-up! I can try to rebase in the next few days, but if one of you wants to beat me to it, feel free.

richardlarocque and others added 4 commits November 30, 2020 07:08
Adds support for JWKs with "kty" value "EC".  This commit includes
support for curves ("crv") "P-256", "P-384", "P-521".

For additional details on these JWKs and their contents, see
https://tools.ietf.org/html/rfc7518#section-6.2.1.

This implementation adheres closely to the pattern set by
`JWT::JWK::RSA`.  It keeps the same coding style and method names.
It also inherits a few quirks:
- It ignores any private key ("d") values when importing JWKs.
- It never emits private key ("d") values when exporting JWKs.
- An import followed by an export does not preserve the "kid" value.

These behaviors seem strange to me, but I worry that changing the
convention would be surprising and potentially dangerous to existing
users of this library.
Updates the implementation of JWT:JWK::EC so it will import and
export custom "kid" values.

See also jwt#320, which proposes
doing the same for JWT::JWK::RSA.
@richardlarocque richardlarocque force-pushed the richardlarocque/ec_jwk_upstreamable branch 2 times, most recently from 50a1500 to 17967f1 Compare November 30, 2020 16:59
def initialize(keypair, kid = nil)
raise ArgumentError, 'keypair must be of type OpenSSL::PKey::EC' unless keypair.is_a?(OpenSSL::PKey::EC)

kid = kid || generate_kid(keypair)

Choose a reason for hiding this comment

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

Use self-assignment shorthand ||=.

Updates `JWT::JWK::EC` to extend `KeyBase` and support other conventions
like the `JWT::JWK::KTYS` array.

Adds support for `#export(include_private: true)` to `JWT::JWK::EC` and
updates tests to match the new behavior.
@richardlarocque richardlarocque force-pushed the richardlarocque/ec_jwk_upstreamable branch from 17967f1 to 162f256 Compare November 30, 2020 17:05
@phlegx
Copy link
Contributor

phlegx commented Dec 1, 2020

Nice work @richardlarocque! Thx

@anakinj anakinj merged commit 752ee22 into jwt:master Dec 1, 2020
@anakinj
Copy link
Member

anakinj commented Dec 1, 2020

Thanks @richardlarocque for the contribution and patience.

@phlegx
Copy link
Contributor

phlegx commented Dec 1, 2020

@anakinj is a release planned?

@anakinj
Copy link
Member

anakinj commented Dec 4, 2020

@phlegx I would like to get the version in a shape that it would work with Ruby 2.1 and 2.2 as the .gemspec claims. Then the version could be the last version on the 2.x branch.

There is a few PRs open that addresses this issue.

@excpt hope you agree :)

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.

None yet

5 participants