jwt 2.2.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # JWT
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/jwt.svg)](https://badge.fury.io/rb/jwt)
4
- [![Build Status](https://travis-ci.org/jwt/ruby-jwt.svg)](https://travis-ci.org/jwt/ruby-jwt)
4
+ [![Build Status](https://github.com/jwt/ruby-jwt/workflows/test/badge.svg?branch=master)](https://github.com/jwt/ruby-jwt/actions)
5
5
  [![Code Climate](https://codeclimate.com/github/jwt/ruby-jwt/badges/gpa.svg)](https://codeclimate.com/github/jwt/ruby-jwt)
6
6
  [![Test Coverage](https://codeclimate.com/github/jwt/ruby-jwt/badges/coverage.svg)](https://codeclimate.com/github/jwt/ruby-jwt/coverage)
7
7
  [![Issue Count](https://codeclimate.com/github/jwt/ruby-jwt/badges/issue_count.svg)](https://codeclimate.com/github/jwt/ruby-jwt)
8
- [![Ebert](https://ebertapp.io/github/jwt/ruby-jwt.svg)](https://ebertapp.io/github/jwt/ruby-jwt)
8
+ [![SourceLevel](https://app.sourcelevel.io/github/jwt/-/ruby-jwt.svg)](https://app.sourcelevel.io/github/jwt/-/ruby-jwt)
9
9
 
10
10
  A ruby implementation of the [RFC 7519 OAuth JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519) standard.
11
11
 
@@ -16,11 +16,17 @@ If you have further questions related to development or usage, join us: [ruby-jw
16
16
  * Ruby 1.9.3 support was dropped at December 31st, 2016.
17
17
  * Version 1.5.3 yanked. See: [#132](https://github.com/jwt/ruby-jwt/issues/132) and [#133](https://github.com/jwt/ruby-jwt/issues/133)
18
18
 
19
+ ## Sponsors
20
+
21
+ |Logo|Message|
22
+ |-|-|
23
+ |![auth0 logo](https://user-images.githubusercontent.com/83319/31722733-de95bbde-b3ea-11e7-96bf-4f4e8f915588.png)|If you want to quickly add secure token-based authentication to Ruby projects, feel free to check Auth0's Ruby SDK and free plan at [auth0.com/developers](https://auth0.com/developers?utm_source=GHsponsor&utm_medium=GHsponsor&utm_campaign=rubyjwt&utm_content=auth)|
24
+
19
25
  ## Installing
20
26
 
21
27
  ### Using Rubygems:
22
28
  ```bash
23
- sudo gem install jwt
29
+ gem install jwt
24
30
  ```
25
31
 
26
32
  ### Using Bundler:
@@ -32,7 +38,7 @@ And run `bundle install`
32
38
 
33
39
  ## Algorithms and Usage
34
40
 
35
- The JWT spec supports NONE, HMAC, RSASSA, ECDSA and RSASSA-PSS algorithms for cryptographic signing. Currently the jwt gem supports NONE, HMAC, RSASSA and ECDSA. If you are using cryptographic signing, you need to specify the algorithm in the options hash whenever you call JWT.decode to ensure that an attacker [cannot bypass the algorithm verification step](https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/). **It is strongly recommended that you hard code the algorithm, as you may leave yourself vulnerable by dynamically picking the algorithm**
41
+ The JWT spec supports NONE, HMAC, RSASSA, ECDSA and RSASSA-PSS algorithms for cryptographic signing. Currently the jwt gem supports NONE, HMAC, RSASSA and ECDSA. If you are using cryptographic signing, you need to specify the algorithm in the options hash whenever you call JWT.decode to ensure that an attacker [cannot bypass the algorithm verification step](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/). **It is strongly recommended that you hard code the algorithm, as you may leave yourself vulnerable by dynamically picking the algorithm**
36
42
 
37
43
  See: [ JSON Web Algorithms (JWA) 3.1. "alg" (Algorithm) Header Parameter Values for JWS](https://tools.ietf.org/html/rfc7518#section-3.1)
38
44
 
@@ -70,6 +76,7 @@ puts decoded_token
70
76
  * HS512 - HMAC using SHA-512 hash algorithm
71
77
 
72
78
  ```ruby
79
+ # The secret must be a string. A JWT::DecodeError will be raised if it isn't provided.
73
80
  hmac_secret = 'my$ecretK3y'
74
81
 
75
82
  token = JWT.encode payload, hmac_secret, 'HS256'
@@ -270,6 +277,12 @@ rescue JWT::ExpiredSignature
270
277
  end
271
278
  ```
272
279
 
280
+ The Expiration Claim verification can be disabled.
281
+ ```ruby
282
+ # Decode token without raising JWT::ExpiredSignature error
283
+ JWT.decode token, hmac_secret, true, { verify_expiration: false, algorithm: 'HS256' }
284
+ ```
285
+
273
286
  **Adding Leeway**
274
287
 
275
288
  ```ruby
@@ -310,6 +323,12 @@ rescue JWT::ImmatureSignature
310
323
  end
311
324
  ```
312
325
 
326
+ The Not Before Claim verification can be disabled.
327
+ ```ruby
328
+ # Decode token without raising JWT::ImmatureSignature error
329
+ JWT.decode token, hmac_secret, true, { verify_not_before: false, algorithm: 'HS256' }
330
+ ```
331
+
313
332
  **Adding Leeway**
314
333
 
315
334
  ```ruby
@@ -391,6 +410,8 @@ begin
391
410
  #decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: true, algorithm: 'HS256' }
392
411
  # Alternatively, pass a proc with your own code to check if the JTI has already been used
393
412
  decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: proc { |jti| my_validation_method(jti) }, algorithm: 'HS256' }
413
+ # or
414
+ decoded_token = JWT.decode token, hmac_secret, true, { verify_jti: proc { |jti, payload| my_validation_method(jti, payload) }, algorithm: 'HS256' }
394
415
  rescue JWT::InvalidJtiError
395
416
  # Handle invalid token, e.g. logout user or deny access
396
417
  puts 'Error'
@@ -439,12 +460,42 @@ rescue JWT::InvalidSubError
439
460
  end
440
461
  ```
441
462
 
463
+ ### Finding a Key
464
+
465
+ To dynamically find the key for verifying the JWT signature, pass a block to the decode block. The block receives headers and the original payload as parameters. It should return with the key to verify the signature that was used to sign the JWT.
466
+
467
+ ```ruby
468
+ issuers = %w[My_Awesome_Company1 My_Awesome_Company2]
469
+ iss_payload = { data: 'data', iss: issuers.first }
470
+
471
+ secrets = { issuers.first => hmac_secret, issuers.last => 'hmac_secret2' }
472
+
473
+ token = JWT.encode iss_payload, hmac_secret, 'HS256'
474
+
475
+ begin
476
+ # Add iss to the validation to check if the token has been manipulated
477
+ decoded_token = JWT.decode(token, nil, true, { iss: issuers, verify_iss: true, algorithm: 'HS256' }) do |_headers, payload|
478
+ secrets[payload['iss']]
479
+ end
480
+ rescue JWT::InvalidIssuerError
481
+ # Handle invalid token, e.g. logout user or deny access
482
+ end
483
+ ```
484
+
485
+ ### Required Claims
486
+
487
+ You can specify claims that must be present for decoding to be successful. JWT::MissingRequiredClaim will be raised if any are missing
488
+ ```ruby
489
+ # Will raise a JWT::ExpiredSignature error if the 'exp' claim is absent
490
+ JWT.decode token, hmac_secret, true, { required_claims: ['exp'], algorithm: 'HS256' }
491
+ ```
492
+
442
493
  ### JSON Web Key (JWK)
443
494
 
444
495
  JWK is a JSON structure representing a cryptographic key. Currently only supports RSA public keys.
445
496
 
446
497
  ```ruby
447
- jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048))
498
+ jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), "optional-kid")
448
499
  payload, headers = { data: 'data' }, { kid: jwk.kid }
449
500
 
450
501
  token = JWT.encode(payload, jwk.keypair, 'RS512', headers)
@@ -460,10 +511,28 @@ begin
460
511
  rescue JWT::JWKError
461
512
  # Handle problems with the provided JWKs
462
513
  rescue JWT::DecodeError
463
- # Handle other decode related issues e.g. no kid in header, no matching public key found etc.
514
+ # Handle other decode related issues e.g. no kid in header, no matching public key found etc.
464
515
  end
465
516
  ```
466
517
 
518
+ or by passing JWK as a simple Hash
519
+
520
+ ```
521
+ jwks = { keys: [{ ... }] } # keys accepts both of string and symbol
522
+ JWT.decode(token, nil, true, { algorithms: ['RS512'], jwks: jwks})
523
+ ```
524
+
525
+ ### Importing and exporting JSON Web Keys
526
+
527
+ The ::JWT::JWK class can be used to import and export both the public key (default behaviour) and the private key. To include the private key in the export pass the `include_private` parameter to the export method.
528
+
529
+ ```ruby
530
+ jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048))
531
+
532
+ jwk_hash = jwk.export
533
+ jwk_hash_with_private_key = jwk.export(include_private: true)
534
+ ```
535
+
467
536
  # Development and Tests
468
537
 
469
538
  We depend on [Bundler](http://rubygems.org/gems/bundler) for defining gemspec and performing releases to rubygems.org, which can be done with
@@ -472,10 +541,11 @@ We depend on [Bundler](http://rubygems.org/gems/bundler) for defining gemspec an
472
541
  rake release
473
542
  ```
474
543
 
475
- The tests are written with rspec. Given you have installed the dependencies via bundler, you can run tests with
544
+ The tests are written with rspec. [Appraisal](https://github.com/thoughtbot/appraisal) is used to ensure compatibility with 3rd party dependencies providing cryptographic features.
476
545
 
477
546
  ```bash
478
- bundle exec rspec
547
+ bundle install
548
+ bundle exec appraisal rake test
479
549
  ```
480
550
 
481
551
  **If you want a release cut with your PR, please include a version bump according to [Semantic Versioning](http://semver.org/)**
data/Rakefile CHANGED
@@ -1,11 +1,14 @@
1
+ require 'bundler/setup'
1
2
  require 'bundler/gem_tasks'
2
3
 
3
4
  begin
4
5
  require 'rspec/core/rake_task'
6
+ require 'rubocop/rake_task'
5
7
 
6
8
  RSpec::Core::RakeTask.new(:test)
9
+ RuboCop::RakeTask.new(:rubocop)
7
10
 
8
- task default: :test
11
+ task default: %i[rubocop test]
9
12
  rescue LoadError
10
13
  puts 'RSpec rake tasks not available. Please run "bundle install" to install missing dependencies.'
11
14
  end
@@ -3,18 +3,25 @@ module JWT
3
3
  module Eddsa
4
4
  module_function
5
5
 
6
- SUPPORTED = %w[ED25519].freeze
6
+ SUPPORTED = %w[ED25519 EdDSA].freeze
7
7
 
8
8
  def sign(to_sign)
9
9
  algorithm, msg, key = to_sign.values
10
- raise EncodeError, "Key given is a #{key.class} but has to be an RbNaCl::Signatures::Ed25519::SigningKey" if key.class != RbNaCl::Signatures::Ed25519::SigningKey
11
- raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key.primitive} signing key was provided" if algorithm.downcase.to_sym != key.primitive
10
+ if key.class != RbNaCl::Signatures::Ed25519::SigningKey
11
+ raise EncodeError, "Key given is a #{key.class} but has to be an RbNaCl::Signatures::Ed25519::SigningKey"
12
+ end
13
+ unless SUPPORTED.map(&:downcase).map(&:to_sym).include?(algorithm.downcase.to_sym)
14
+ raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key.primitive} signing key was provided"
15
+ end
16
+
12
17
  key.sign(msg)
13
18
  end
14
19
 
15
20
  def verify(to_verify)
16
21
  algorithm, public_key, signing_input, signature = to_verify.values
17
- raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{public_key.primitive} verification key was provided" if algorithm.downcase.to_sym != public_key.primitive
22
+ unless SUPPORTED.map(&:downcase).map(&:to_sym).include?(algorithm.downcase.to_sym)
23
+ raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key.primitive} signing key was provided"
24
+ end
18
25
  raise DecodeError, "key given is a #{public_key.class} but has to be a RbNaCl::Signatures::Ed25519::VerifyKey" if public_key.class != RbNaCl::Signatures::Ed25519::VerifyKey
19
26
  public_key.verify(signature, signing_input)
20
27
  end
@@ -7,6 +7,7 @@ module JWT
7
7
 
8
8
  def sign(to_sign)
9
9
  algorithm, msg, key = to_sign.values
10
+ key ||= ''
10
11
  authenticator, padded_key = SecurityUtils.rbnacl_fixup(algorithm, key)
11
12
  if authenticator && padded_key
12
13
  authenticator.auth(padded_key, msg.encode('binary'))
@@ -0,0 +1,15 @@
1
+ module JWT
2
+ module Algos
3
+ module None
4
+ module_function
5
+
6
+ SUPPORTED = %w[none].freeze
7
+
8
+ def sign(*); end
9
+
10
+ def verify(*)
11
+ true
12
+ end
13
+ end
14
+ end
15
+ end
@@ -3,14 +3,15 @@ module JWT
3
3
  module Unsupported
4
4
  module_function
5
5
 
6
- SUPPORTED = Object.new.tap { |object| object.define_singleton_method(:include?) { |*| true } }
7
- def verify(*)
8
- raise JWT::VerificationError, 'Algorithm not supported'
9
- end
6
+ SUPPORTED = [].freeze
10
7
 
11
8
  def sign(*)
12
9
  raise NotImplementedError, 'Unsupported signing method'
13
10
  end
11
+
12
+ def verify(*)
13
+ raise JWT::VerificationError, 'Algorithm not supported'
14
+ end
14
15
  end
15
16
  end
16
17
  end
data/lib/jwt/algos.rb ADDED
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jwt/algos/hmac'
4
+ require 'jwt/algos/eddsa'
5
+ require 'jwt/algos/ecdsa'
6
+ require 'jwt/algos/rsa'
7
+ require 'jwt/algos/ps'
8
+ require 'jwt/algos/none'
9
+ require 'jwt/algos/unsupported'
10
+
11
+ # JWT::Signature module
12
+ module JWT
13
+ # Signature logic for JWT
14
+ module Algos
15
+ extend self
16
+
17
+ ALGOS = [
18
+ Algos::Hmac,
19
+ Algos::Ecdsa,
20
+ Algos::Rsa,
21
+ Algos::Eddsa,
22
+ Algos::Ps,
23
+ Algos::None,
24
+ Algos::Unsupported
25
+ ].freeze
26
+
27
+ def find(algorithm)
28
+ indexed[algorithm && algorithm.downcase]
29
+ end
30
+
31
+ private
32
+
33
+ def indexed
34
+ @indexed ||= begin
35
+ fallback = [Algos::Unsupported, nil]
36
+ ALGOS.each_with_object(Hash.new(fallback)) do |alg, hash|
37
+ alg.const_get(:SUPPORTED).each do |code|
38
+ hash[code.downcase] = [alg, code]
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -2,7 +2,7 @@ require_relative './error'
2
2
 
3
3
  module JWT
4
4
  class ClaimsValidator
5
- INTEGER_CLAIMS = %i[
5
+ NUMERIC_CLAIMS = %i[
6
6
  exp
7
7
  iat
8
8
  nbf
@@ -13,21 +13,23 @@ module JWT
13
13
  end
14
14
 
15
15
  def validate!
16
- validate_int_claims
16
+ validate_numeric_claims
17
17
 
18
18
  true
19
19
  end
20
20
 
21
21
  private
22
22
 
23
- def validate_int_claims
24
- INTEGER_CLAIMS.each do |claim|
25
- validate_is_int(claim) if @payload.key?(claim)
23
+ def validate_numeric_claims
24
+ NUMERIC_CLAIMS.each do |claim|
25
+ validate_is_numeric(claim) if @payload.key?(claim)
26
26
  end
27
27
  end
28
28
 
29
- def validate_is_int(claim)
30
- raise InvalidPayload, "#{claim} claim must be an Integer but it is a #{@payload[claim].class}" unless @payload[claim].is_a?(Integer)
29
+ def validate_is_numeric(claim)
30
+ return if @payload[claim].is_a?(Numeric)
31
+
32
+ raise InvalidPayload, "#{claim} claim must be a Numeric value but it is a #{@payload[claim].class}"
31
33
  end
32
34
  end
33
35
  end
data/lib/jwt/decode.rb CHANGED
@@ -33,25 +33,34 @@ module JWT
33
33
  private
34
34
 
35
35
  def verify_signature
36
- @key = find_key(&@keyfinder) if @keyfinder
37
- @key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks]).key_for(header['kid']) if @options[:jwks]
38
-
39
36
  raise(JWT::IncorrectAlgorithm, 'An algorithm must be specified') if allowed_algorithms.empty?
37
+ raise(JWT::IncorrectAlgorithm, 'Token is missing alg header') unless header['alg']
40
38
  raise(JWT::IncorrectAlgorithm, 'Expected a different algorithm') unless options_includes_algo_in_header?
41
39
 
40
+ @key = find_key(&@keyfinder) if @keyfinder
41
+ @key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks]).key_for(header['kid']) if @options[:jwks]
42
+
42
43
  Signature.verify(header['alg'], @key, signing_input, @signature)
43
44
  end
44
45
 
45
46
  def options_includes_algo_in_header?
46
- allowed_algorithms.include? header['alg']
47
+ allowed_algorithms.any? { |alg| alg.casecmp(header['alg']).zero? }
47
48
  end
48
49
 
49
50
  def allowed_algorithms
50
- if @options.key?(:algorithm)
51
- [@options[:algorithm]]
51
+ # Order is very important - first check for string keys, next for symbols
52
+ algos = if @options.key?('algorithm')
53
+ @options['algorithm']
54
+ elsif @options.key?(:algorithm)
55
+ @options[:algorithm]
56
+ elsif @options.key?('algorithms')
57
+ @options['algorithms']
58
+ elsif @options.key?(:algorithms)
59
+ @options[:algorithms]
52
60
  else
53
- @options[:algorithms] || []
61
+ []
54
62
  end
63
+ Array(algos)
55
64
  end
56
65
 
57
66
  def find_key(&keyfinder)
@@ -62,11 +71,13 @@ module JWT
62
71
 
63
72
  def verify_claims
64
73
  Verify.verify_claims(payload, @options)
74
+ Verify.verify_required_claims(payload, @options)
65
75
  end
66
76
 
67
77
  def validate_segment_count!
68
78
  return if segment_length == 3
69
79
  return if !@verify && segment_length == 2 # If no verifying required, the signature is not needed
80
+ return if segment_length == 2 && header['alg'] == 'none'
70
81
 
71
82
  raise(JWT::DecodeError, 'Not enough or too many segments')
72
83
  end
@@ -76,7 +87,7 @@ module JWT
76
87
  end
77
88
 
78
89
  def decode_crypto
79
- @signature = JWT::Base64.url_decode(@segments[2])
90
+ @signature = JWT::Base64.url_decode(@segments[2] || '')
80
91
  end
81
92
 
82
93
  def header
@@ -9,7 +9,8 @@ module JWT
9
9
  verify_aud: false,
10
10
  verify_sub: false,
11
11
  leeway: 0,
12
- algorithms: ['HS256']
12
+ algorithms: ['HS256'],
13
+ required_claims: []
13
14
  }.freeze
14
15
  end
15
16
  end
data/lib/jwt/encode.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative './algos'
3
4
  require_relative './claims_validator'
4
5
 
5
6
  # JWT::Encode module
@@ -10,10 +11,10 @@ module JWT
10
11
  ALG_KEY = 'alg'.freeze
11
12
 
12
13
  def initialize(options)
13
- @payload = options[:payload]
14
- @key = options[:key]
15
- @algorithm = options[:algorithm]
16
- @headers = options[:headers].each_with_object({}) { |(key, value), headers| headers[key.to_s] = value }
14
+ @payload = options[:payload]
15
+ @key = options[:key]
16
+ _, @algorithm = Algos.find(options[:algorithm])
17
+ @headers = options[:headers].each_with_object({}) { |(key, value), headers| headers[key.to_s] = value }
17
18
  end
18
19
 
19
20
  def segments
data/lib/jwt/error.rb CHANGED
@@ -1,20 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JWT
4
- EncodeError = Class.new(StandardError)
5
- DecodeError = Class.new(StandardError)
6
- RequiredDependencyError = Class.new(StandardError)
4
+ class EncodeError < StandardError; end
5
+ class DecodeError < StandardError; end
6
+ class RequiredDependencyError < StandardError; end
7
7
 
8
- VerificationError = Class.new(DecodeError)
9
- ExpiredSignature = Class.new(DecodeError)
10
- IncorrectAlgorithm = Class.new(DecodeError)
11
- ImmatureSignature = Class.new(DecodeError)
12
- InvalidIssuerError = Class.new(DecodeError)
13
- InvalidIatError = Class.new(DecodeError)
14
- InvalidAudError = Class.new(DecodeError)
15
- InvalidSubError = Class.new(DecodeError)
16
- InvalidJtiError = Class.new(DecodeError)
17
- InvalidPayload = Class.new(DecodeError)
8
+ class VerificationError < DecodeError; end
9
+ class ExpiredSignature < DecodeError; end
10
+ class IncorrectAlgorithm < DecodeError; end
11
+ class ImmatureSignature < DecodeError; end
12
+ class InvalidIssuerError < DecodeError; end
13
+ class InvalidIatError < DecodeError; end
14
+ class InvalidAudError < DecodeError; end
15
+ class InvalidSubError < DecodeError; end
16
+ class InvalidJtiError < DecodeError; end
17
+ class InvalidPayload < DecodeError; end
18
+ class MissingRequiredClaim < DecodeError; end
18
19
 
19
- JWKError = Class.new(DecodeError)
20
+ class JWKError < DecodeError; end
20
21
  end
data/lib/jwt/jwk/ec.rb ADDED
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module JWT
6
+ module JWK
7
+ class EC < KeyBase
8
+ extend Forwardable
9
+ def_delegators :@keypair, :public_key
10
+
11
+ KTY = 'EC'.freeze
12
+ KTYS = [KTY, OpenSSL::PKey::EC].freeze
13
+ BINARY = 2
14
+
15
+ def initialize(keypair, kid = nil)
16
+ raise ArgumentError, 'keypair must be of type OpenSSL::PKey::EC' unless keypair.is_a?(OpenSSL::PKey::EC)
17
+
18
+ kid ||= generate_kid(keypair)
19
+ super(keypair, kid)
20
+ end
21
+
22
+ def private?
23
+ @keypair.private_key?
24
+ end
25
+
26
+ def export(options = {})
27
+ crv, x_octets, y_octets = keypair_components(keypair)
28
+ exported_hash = {
29
+ kty: KTY,
30
+ crv: crv,
31
+ x: encode_octets(x_octets),
32
+ y: encode_octets(y_octets),
33
+ kid: kid
34
+ }
35
+ return exported_hash unless private? && options[:include_private] == true
36
+
37
+ append_private_parts(exported_hash)
38
+ end
39
+
40
+ private
41
+
42
+ def append_private_parts(the_hash)
43
+ octets = keypair.private_key.to_bn.to_s(BINARY)
44
+ the_hash.merge(
45
+ d: encode_octets(octets)
46
+ )
47
+ end
48
+
49
+ def generate_kid(ec_keypair)
50
+ _crv, x_octets, y_octets = keypair_components(ec_keypair)
51
+ sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(x_octets, BINARY)),
52
+ OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(y_octets, BINARY))])
53
+ OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
54
+ end
55
+
56
+ def keypair_components(ec_keypair)
57
+ encoded_point = ec_keypair.public_key.to_bn.to_s(BINARY)
58
+ case ec_keypair.group.curve_name
59
+ when 'prime256v1'
60
+ crv = 'P-256'
61
+ x_octets, y_octets = encoded_point.unpack('xa32a32')
62
+ when 'secp384r1'
63
+ crv = 'P-384'
64
+ x_octets, y_octets = encoded_point.unpack('xa48a48')
65
+ when 'secp521r1'
66
+ crv = 'P-521'
67
+ x_octets, y_octets = encoded_point.unpack('xa66a66')
68
+ else
69
+ raise JWT::JWKError, "Unsupported curve '#{ec_keypair.group.curve_name}'"
70
+ end
71
+ [crv, x_octets, y_octets]
72
+ end
73
+
74
+ def encode_octets(octets)
75
+ ::JWT::Base64.url_encode(octets)
76
+ end
77
+
78
+ def encode_open_ssl_bn(key_part)
79
+ ::JWT::Base64.url_encode(key_part.to_s(BINARY))
80
+ end
81
+
82
+ class << self
83
+ def import(jwk_data)
84
+ # See https://tools.ietf.org/html/rfc7518#section-6.2.1 for an
85
+ # explanation of the relevant parameters.
86
+
87
+ jwk_crv, jwk_x, jwk_y, jwk_d, jwk_kid = jwk_attrs(jwk_data, %i[crv x y d kid])
88
+ raise JWT::JWKError, 'Key format is invalid for EC' unless jwk_crv && jwk_x && jwk_y
89
+
90
+ new(ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d), jwk_kid)
91
+ end
92
+
93
+ def to_openssl_curve(crv)
94
+ # The JWK specs and OpenSSL use different names for the same curves.
95
+ # See https://tools.ietf.org/html/rfc5480#section-2.1.1.1 for some
96
+ # pointers on different names for common curves.
97
+ case crv
98
+ when 'P-256' then 'prime256v1'
99
+ when 'P-384' then 'secp384r1'
100
+ when 'P-521' then 'secp521r1'
101
+ else raise JWT::JWKError, 'Invalid curve provided'
102
+ end
103
+ end
104
+
105
+ private
106
+
107
+ def jwk_attrs(jwk_data, attrs)
108
+ attrs.map do |attr|
109
+ jwk_data[attr] || jwk_data[attr.to_s]
110
+ end
111
+ end
112
+
113
+ def ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d)
114
+ curve = to_openssl_curve(jwk_crv)
115
+
116
+ x_octets = decode_octets(jwk_x)
117
+ y_octets = decode_octets(jwk_y)
118
+
119
+ key = OpenSSL::PKey::EC.new(curve)
120
+
121
+ # The details of the `Point` instantiation are covered in:
122
+ # - https://docs.ruby-lang.org/en/2.4.0/OpenSSL/PKey/EC.html
123
+ # - https://www.openssl.org/docs/manmaster/man3/EC_POINT_new.html
124
+ # - https://tools.ietf.org/html/rfc5480#section-2.2
125
+ # - https://www.secg.org/SEC1-Ver-1.0.pdf
126
+ # Section 2.3.3 of the last of these references specifies that the
127
+ # encoding of an uncompressed point consists of the byte `0x04` followed
128
+ # by the x value then the y value.
129
+ point = OpenSSL::PKey::EC::Point.new(
130
+ OpenSSL::PKey::EC::Group.new(curve),
131
+ OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
132
+ )
133
+
134
+ key.public_key = point
135
+ key.private_key = OpenSSL::BN.new(decode_octets(jwk_d), 2) if jwk_d
136
+
137
+ key
138
+ end
139
+
140
+ def decode_octets(jwk_data)
141
+ ::JWT::Base64.url_decode(jwk_data)
142
+ end
143
+
144
+ def decode_open_ssl_bn(jwk_data)
145
+ OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY)
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end