jwt 2.6.0 → 2.7.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c18ec5fbed5aff7aa65bfe9e7893583c677d0d18269c1ebd9cff7761916e298
4
- data.tar.gz: e1e270fff52673d769982888b97c741a4b5c38b34214ee9d547857c0244ff0db
3
+ metadata.gz: f2ff80d9f32962a3c98d469c1b1078c84631291153f89481cccd9fc8d311e925
4
+ data.tar.gz: af54f20921b46237194671b957f9d0e2d04ec5ac501f8afa767f0d9d97e3acc6
5
5
  SHA512:
6
- metadata.gz: 452b6056da93ed535d8e93fc17d3ec69105a623b217f38d816ab1dc298dbcb93b1e45143732c3d13b752f421495e3b56f8cf51b5a8a802570bc5832072f28a26
7
- data.tar.gz: 0a5616fd089942547a6222eb6a7fd22f7825e0b7f219336a6e5904f58ede7bf58e454390c2ebcb2cc026951549d739661c515db1ec3cda9d2ede7009ea668bc8
6
+ metadata.gz: 7bc55b74e5565674e38b6f706ca2e9d70cc25ee679f80dd6dad160d1f5e8070749d919f65e487acfd7131a22246be66b2d82e517deb9c38df7bed86aaf7b61e8
7
+ data.tar.gz: ed53859b1ac5423666d2351b7411014d74b6343fa0255bd5ed0b7ef5b58f8b073a6cc7905959866993ca4bbd7540b5e0a1fc3a08ce70f18fe82c9126f8cbc9b1
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [v2.7.0](https://github.com/jwt/ruby-jwt/tree/v2.7.0) (2023-02-01)
4
+
5
+ [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.6.0...v2.7.0)
6
+
7
+ **Features:**
8
+
9
+ - Support OKP (Ed25519) keys for JWKs [#540](https://github.com/jwt/ruby-jwt/pull/540) ([@anakinj](https://github.com/anakinj))
10
+ - JWK Sets can now be used for tokens with nil kid [#543](https://github.com/jwt/ruby-jwt/pull/543) ([@bellebaum](https://github.com/bellebaum))
11
+
12
+ **Fixes and enhancements:**
13
+
14
+ - Fix issue with multiple keys returned by keyfinder and multiple allowed algorithms [#545](https://github.com/jwt/ruby-jwt/pull/545) ([@mpospelov](https://github.com/mpospelov))
15
+ - Non-string `kid` header values are now rejected [#543](https://github.com/jwt/ruby-jwt/pull/543) ([@bellebaum](https://github.com/bellebaum))
16
+
3
17
  ## [v2.6.0](https://github.com/jwt/ruby-jwt/tree/v2.6.0) (2022-12-22)
4
18
 
5
19
  [Full Changelog](https://github.com/jwt/ruby-jwt/compare/v2.5.0...v2.6.0)
@@ -13,7 +27,7 @@
13
27
 
14
28
  **Fixes and enhancements:**
15
29
 
16
- - Raise descriptive error on empty hmac_secret and OpenSSL 3.0/openssl gem <3.0.1[#530](https://github.com/jwt/ruby-jwt/pull/530) ([@jonmchan](https://github.com/jonmchan)).
30
+ - Raise descriptive error on empty hmac_secret and OpenSSL 3.0/openssl gem <3.0.1 [#530](https://github.com/jwt/ruby-jwt/pull/530) ([@jonmchan](https://github.com/jonmchan)).
17
31
 
18
32
  ## [v2.5.0](https://github.com/jwt/ruby-jwt/tree/v2.5.0) (2022-08-25)
19
33
 
data/README.md CHANGED
@@ -569,7 +569,7 @@ end
569
569
 
570
570
  ### JSON Web Key (JWK)
571
571
 
572
- JWK is a JSON structure representing a cryptographic key. This gem currently supports RSA, EC and HMAC keys.
572
+ JWK is a JSON structure representing a cryptographic key. This gem currently supports RSA, EC, OKP and HMAC keys. OKP support requires [RbNaCl](https://github.com/RubyCrypto/rbnacl) and currently only supports the Ed25519 curve.
573
573
 
574
574
  To encode a JWT using your JWK:
575
575
 
@@ -579,7 +579,7 @@ jwk = JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), optional_parameters)
579
579
 
580
580
  # Encoding
581
581
  payload = { data: 'data' }
582
- token = JWT.encode(payload, jwk.keypair, jwk[:alg], kid: jwk[:kid])
582
+ token = JWT.encode(payload, jwk.signing_key, jwk[:alg], kid: jwk[:kid])
583
583
 
584
584
  # JSON Web Key Set for advertising your signing keys
585
585
  jwks_hash = JWT::JWK::Set.new(jwk).export
@@ -601,6 +601,9 @@ This can be used to implement caching of remotely fetched JWK Sets.
601
601
  If the requested `kid` is not found from the given set the loader will be called a second time with the `kid_not_found` option set to `true`.
602
602
  The application can choose to implement some kind of JWK cache invalidation or other mechanism to handle such cases.
603
603
 
604
+ Tokens without a specified `kid` are rejected by default.
605
+ This behaviour may be overwritten by setting the `allow_nil_jwks` option for `decode` to `true`.
606
+
604
607
  ```ruby
605
608
  jwks_loader = ->(options) do
606
609
  # The jwk loader would fetch the set of JWKs from a trusted source.
@@ -650,8 +653,8 @@ jwk_hash = jwk.export
650
653
  jwk_hash_with_private_key = jwk.export(include_private: true)
651
654
 
652
655
  # Export as OpenSSL key
653
- public_key = jwk.public_key
654
- private_key = jwk.keypair if jwk.private?
656
+ public_key = jwk.verify_key
657
+ private_key = jwk.signing_key if jwk.private?
655
658
 
656
659
  # You can also import and export entire JSON Web Key Sets
657
660
  jwks_hash = { keys: [{ kty: 'oct', k: 'my-secret', kid: 'my-kid' }] }
data/lib/jwt/decode.rb CHANGED
@@ -52,25 +52,25 @@ module JWT
52
52
  def verify_algo
53
53
  raise(JWT::IncorrectAlgorithm, 'An algorithm must be specified') if allowed_algorithms.empty?
54
54
  raise(JWT::IncorrectAlgorithm, 'Token is missing alg header') unless alg_in_header
55
- raise(JWT::IncorrectAlgorithm, 'Expected a different algorithm') unless valid_alg_in_header?
55
+ raise(JWT::IncorrectAlgorithm, 'Expected a different algorithm') if allowed_and_valid_algorithms.empty?
56
56
  end
57
57
 
58
58
  def set_key
59
59
  @key = find_key(&@keyfinder) if @keyfinder
60
- @key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks]).key_for(header['kid']) if @options[:jwks]
60
+ @key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks], allow_nil_kid: @options[:allow_nil_kid]).key_for(header['kid']) if @options[:jwks]
61
61
  if (x5c_options = @options[:x5c])
62
62
  @key = X5cKeyFinder.new(x5c_options[:root_certificates], x5c_options[:crls]).from(header['x5c'])
63
63
  end
64
64
  end
65
65
 
66
66
  def verify_signature_for?(key)
67
- allowed_algorithms.any? do |alg|
67
+ allowed_and_valid_algorithms.any? do |alg|
68
68
  alg.verify(data: signing_input, signature: @signature, verification_key: key)
69
69
  end
70
70
  end
71
71
 
72
- def valid_alg_in_header?
73
- allowed_algorithms.any? { |alg| alg.valid_alg?(alg_in_header) }
72
+ def allowed_and_valid_algorithms
73
+ @allowed_and_valid_algorithms ||= allowed_algorithms.select { |alg| alg.valid_alg?(alg_in_header) }
74
74
  end
75
75
 
76
76
  # Order is very important - first check for string keys, next for symbols
data/lib/jwt/jwk/ec.rb CHANGED
@@ -5,9 +5,6 @@ require 'forwardable'
5
5
  module JWT
6
6
  module JWK
7
7
  class EC < KeyBase # rubocop:disable Metrics/ClassLength
8
- extend Forwardable
9
- def_delegators :keypair, :public_key
10
-
11
8
  KTY = 'EC'
12
9
  KTYS = [KTY, OpenSSL::PKey::EC, JWT::JWK::EC].freeze
13
10
  BINARY = 2
@@ -24,17 +21,29 @@ module JWT
24
21
  key_params = extract_key_params(key)
25
22
 
26
23
  params = params.transform_keys(&:to_sym)
27
- check_jwk(key_params, params)
24
+ check_jwk_params!(key_params, params)
28
25
 
29
26
  super(options, key_params.merge(params))
30
27
  end
31
28
 
32
29
  def keypair
33
- @keypair ||= create_ec_key(self[:crv], self[:x], self[:y], self[:d])
30
+ ec_key
34
31
  end
35
32
 
36
33
  def private?
37
- keypair.private_key?
34
+ ec_key.private_key?
35
+ end
36
+
37
+ def signing_key
38
+ ec_key
39
+ end
40
+
41
+ def verify_key
42
+ ec_key
43
+ end
44
+
45
+ def public_key
46
+ ec_key
38
47
  end
39
48
 
40
49
  def members
@@ -48,7 +57,7 @@ module JWT
48
57
  end
49
58
 
50
59
  def key_digest
51
- _crv, x_octets, y_octets = keypair_components(keypair)
60
+ _crv, x_octets, y_octets = keypair_components(ec_key)
52
61
  sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(x_octets, BINARY)),
53
62
  OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(y_octets, BINARY))])
54
63
  OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
@@ -64,12 +73,16 @@ module JWT
64
73
 
65
74
  private
66
75
 
76
+ def ec_key
77
+ @ec_key ||= create_ec_key(self[:crv], self[:x], self[:y], self[:d])
78
+ end
79
+
67
80
  def extract_key_params(key)
68
81
  case key
69
82
  when JWT::JWK::EC
70
83
  key.export(include_private: true)
71
84
  when OpenSSL::PKey::EC # Accept OpenSSL key as input
72
- @keypair = key # Preserve the object to avoid recreation
85
+ @ec_key = key # Preserve the object to avoid recreation
73
86
  parse_ec_key(key)
74
87
  when Hash
75
88
  key.transform_keys(&:to_sym)
@@ -78,10 +91,10 @@ module JWT
78
91
  end
79
92
  end
80
93
 
81
- def check_jwk(keypair, params)
94
+ def check_jwk_params!(key_params, params)
82
95
  raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (EC_KEY_ELEMENTS & params.keys).empty?
83
- raise JWT::JWKError, "Incorrect 'kty' value: #{keypair[:kty]}, expected #{KTY}" unless keypair[:kty] == KTY
84
- raise JWT::JWKError, 'Key format is invalid for EC' unless keypair[:crv] && keypair[:x] && keypair[:y]
96
+ raise JWT::JWKError, "Incorrect 'kty' value: #{key_params[:kty]}, expected #{KTY}" unless key_params[:kty] == KTY
97
+ raise JWT::JWKError, 'Key format is invalid for EC' unless key_params[:crv] && key_params[:x] && key_params[:y]
85
98
  end
86
99
 
87
100
  def keypair_components(ec_keypair)
data/lib/jwt/jwk/hmac.rb CHANGED
@@ -24,7 +24,7 @@ module JWT
24
24
  end
25
25
 
26
26
  def keypair
27
- self[:k]
27
+ secret
28
28
  end
29
29
 
30
30
  def private?
@@ -35,6 +35,14 @@ module JWT
35
35
  nil
36
36
  end
37
37
 
38
+ def verify_key
39
+ secret
40
+ end
41
+
42
+ def signing_key
43
+ secret
44
+ end
45
+
38
46
  # See https://tools.ietf.org/html/rfc7517#appendix-A.3
39
47
  def export(options = {})
40
48
  exported = parameters.clone
@@ -46,8 +54,6 @@ module JWT
46
54
  HMAC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
47
55
  end
48
56
 
49
- alias signing_key keypair # for backwards compatibility
50
-
51
57
  def key_digest
52
58
  sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::UTF8String.new(signing_key),
53
59
  OpenSSL::ASN1::UTF8String.new(KTY)])
@@ -64,6 +70,10 @@ module JWT
64
70
 
65
71
  private
66
72
 
73
+ def secret
74
+ self[:k]
75
+ end
76
+
67
77
  def extract_key_params(key)
68
78
  case key
69
79
  when JWT::JWK::HMAC
@@ -4,6 +4,7 @@ module JWT
4
4
  module JWK
5
5
  class KeyFinder
6
6
  def initialize(options)
7
+ @allow_nil_kid = options[:allow_nil_kid]
7
8
  jwks_or_loader = options[:jwks]
8
9
 
9
10
  @jwks_loader = if jwks_or_loader.respond_to?(:call)
@@ -14,28 +15,31 @@ module JWT
14
15
  end
15
16
 
16
17
  def key_for(kid)
17
- raise ::JWT::DecodeError, 'No key id (kid) found from token headers' unless kid
18
+ raise ::JWT::DecodeError, 'No key id (kid) found from token headers' unless kid || @allow_nil_kid
19
+ raise ::JWT::DecodeError, 'Invalid type for kid header parameter' unless kid.nil? || kid.is_a?(String)
18
20
 
19
21
  jwk = resolve_key(kid)
20
22
 
21
23
  raise ::JWT::DecodeError, 'No keys found in jwks' unless @jwks.any?
22
24
  raise ::JWT::DecodeError, "Could not find public key for kid #{kid}" unless jwk
23
25
 
24
- jwk.keypair
26
+ jwk.verify_key
25
27
  end
26
28
 
27
29
  private
28
30
 
29
31
  def resolve_key(kid)
32
+ key_matcher = ->(key) { (kid.nil? && @allow_nil_kid) || key[:kid] == kid }
33
+
30
34
  # First try without invalidation to facilitate application caching
31
35
  @jwks ||= JWT::JWK::Set.new(@jwks_loader.call(kid: kid))
32
- jwk = @jwks.find { |key| key[:kid] == kid }
36
+ jwk = @jwks.find { |key| key_matcher.call(key) }
33
37
 
34
38
  return jwk if jwk
35
39
 
36
40
  # Second try, invalidate for backwards compatibility
37
41
  @jwks = JWT::JWK::Set.new(@jwks_loader.call(invalidate: true, kid_not_found: true, kid: kid))
38
- @jwks.find { |key| key[:kid] == kid }
42
+ @jwks.find { |key| key_matcher.call(key) }
39
43
  end
40
44
  end
41
45
  end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWK
5
+ class OKPRbNaCl < KeyBase
6
+ KTY = 'OKP'
7
+ KTYS = [KTY, JWT::JWK::OKPRbNaCl, RbNaCl::Signatures::Ed25519::SigningKey, RbNaCl::Signatures::Ed25519::VerifyKey].freeze
8
+ OKP_PUBLIC_KEY_ELEMENTS = %i[kty n x].freeze
9
+ OKP_PRIVATE_KEY_ELEMENTS = %i[d].freeze
10
+
11
+ def initialize(key, params = nil, options = {})
12
+ params ||= {}
13
+
14
+ # For backwards compatibility when kid was a String
15
+ params = { kid: params } if params.is_a?(String)
16
+
17
+ key_params = extract_key_params(key)
18
+
19
+ params = params.transform_keys(&:to_sym)
20
+ check_jwk_params!(key_params, params)
21
+ super(options, key_params.merge(params))
22
+ end
23
+
24
+ def verify_key
25
+ return @verify_key if defined?(@verify_key)
26
+
27
+ @verify_key = verify_key_from_parameters
28
+ end
29
+
30
+ def signing_key
31
+ return @signing_key if defined?(@signing_key)
32
+
33
+ @signing_key = signing_key_from_parameters
34
+ end
35
+
36
+ def key_digest
37
+ Thumbprint.new(self).to_s
38
+ end
39
+
40
+ def private?
41
+ !signing_key.nil?
42
+ end
43
+
44
+ def members
45
+ OKP_PUBLIC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
46
+ end
47
+
48
+ def export(options = {})
49
+ exported = parameters.clone
50
+ exported.reject! { |k, _| OKP_PRIVATE_KEY_ELEMENTS.include?(k) } unless private? && options[:include_private] == true
51
+ exported
52
+ end
53
+
54
+ private
55
+
56
+ def extract_key_params(key)
57
+ case key
58
+ when JWT::JWK::KeyBase
59
+ key.export(include_private: true)
60
+ when RbNaCl::Signatures::Ed25519::SigningKey
61
+ @signing_key = key
62
+ @verify_key = key.verify_key
63
+ parse_okp_key_params(@verify_key, @signing_key)
64
+ when RbNaCl::Signatures::Ed25519::VerifyKey
65
+ @signing_key = nil
66
+ @verify_key = key
67
+ parse_okp_key_params(@verify_key)
68
+ when Hash
69
+ key.transform_keys(&:to_sym)
70
+ else
71
+ raise ArgumentError, 'key must be of type RbNaCl::Signatures::Ed25519::SigningKey, RbNaCl::Signatures::Ed25519::VerifyKey or Hash with key parameters'
72
+ end
73
+ end
74
+
75
+ def check_jwk_params!(key_params, _given_params)
76
+ raise JWT::JWKError, "Incorrect 'kty' value: #{key_params[:kty]}, expected #{KTY}" unless key_params[:kty] == KTY
77
+ end
78
+
79
+ def parse_okp_key_params(verify_key, signing_key = nil)
80
+ params = {
81
+ kty: KTY,
82
+ crv: 'Ed25519',
83
+ x: ::JWT::Base64.url_encode(verify_key.to_bytes)
84
+ }
85
+
86
+ if signing_key
87
+ params[:d] = ::JWT::Base64.url_encode(signing_key.to_bytes)
88
+ end
89
+
90
+ params
91
+ end
92
+
93
+ def verify_key_from_parameters
94
+ RbNaCl::Signatures::Ed25519::VerifyKey.new(::JWT::Base64.url_decode(self[:x]))
95
+ end
96
+
97
+ def signing_key_from_parameters
98
+ return nil unless self[:d]
99
+
100
+ RbNaCl::Signatures::Ed25519::SigningKey.new(::JWT::Base64.url_decode(self[:d]))
101
+ end
102
+
103
+ class << self
104
+ def import(jwk_data)
105
+ new(jwk_data)
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
data/lib/jwt/jwk/rsa.rb CHANGED
@@ -22,21 +22,29 @@ module JWT
22
22
  key_params = extract_key_params(key)
23
23
 
24
24
  params = params.transform_keys(&:to_sym)
25
- check_jwk(key_params, params)
25
+ check_jwk_params!(key_params, params)
26
26
 
27
27
  super(options, key_params.merge(params))
28
28
  end
29
29
 
30
30
  def keypair
31
- @keypair ||= self.class.create_rsa_key(jwk_attributes(*(RSA_KEY_ELEMENTS - [:kty])))
31
+ rsa_key
32
32
  end
33
33
 
34
34
  def private?
35
- keypair.private?
35
+ rsa_key.private?
36
36
  end
37
37
 
38
38
  def public_key
39
- keypair.public_key
39
+ rsa_key.public_key
40
+ end
41
+
42
+ def signing_key
43
+ rsa_key if private?
44
+ end
45
+
46
+ def verify_key
47
+ rsa_key.public_key
40
48
  end
41
49
 
42
50
  def export(options = {})
@@ -65,12 +73,16 @@ module JWT
65
73
 
66
74
  private
67
75
 
76
+ def rsa_key
77
+ @rsa_key ||= self.class.create_rsa_key(jwk_attributes(*(RSA_KEY_ELEMENTS - [:kty])))
78
+ end
79
+
68
80
  def extract_key_params(key)
69
81
  case key
70
82
  when JWT::JWK::RSA
71
83
  key.export(include_private: true)
72
84
  when OpenSSL::PKey::RSA # Accept OpenSSL key as input
73
- @keypair = key # Preserve the object to avoid recreation
85
+ @rsa_key = key # Preserve the object to avoid recreation
74
86
  parse_rsa_key(key)
75
87
  when Hash
76
88
  key.transform_keys(&:to_sym)
@@ -79,10 +91,10 @@ module JWT
79
91
  end
80
92
  end
81
93
 
82
- def check_jwk(keypair, params)
94
+ def check_jwk_params!(key_params, params)
83
95
  raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (RSA_KEY_ELEMENTS & params.keys).empty?
84
- raise JWT::JWKError, "Incorrect 'kty' value: #{keypair[:kty]}, expected #{KTY}" unless keypair[:kty] == KTY
85
- raise JWT::JWKError, 'Key format is invalid for RSA' unless keypair[:n] && keypair[:e]
96
+ raise JWT::JWKError, "Incorrect 'kty' value: #{key_params[:kty]}, expected #{KTY}" unless key_params[:kty] == KTY
97
+ raise JWT::JWKError, 'Key format is invalid for RSA' unless key_params[:n] && key_params[:e]
86
98
  end
87
99
 
88
100
  def parse_rsa_key(key)
data/lib/jwt/jwk.rb CHANGED
@@ -52,3 +52,4 @@ require_relative 'jwk/key_base'
52
52
  require_relative 'jwk/ec'
53
53
  require_relative 'jwk/rsa'
54
54
  require_relative 'jwk/hmac'
55
+ require_relative 'jwk/okp_rbnacl' if ::JWT.rbnacl?
data/lib/jwt/version.rb CHANGED
@@ -11,7 +11,7 @@ module JWT
11
11
  # major version
12
12
  MAJOR = 2
13
13
  # minor version
14
- MINOR = 6
14
+ MINOR = 7
15
15
  # tiny version
16
16
  TINY = 0
17
17
  # alpha, beta, etc. tag
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jwt
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.6.0
4
+ version: 2.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tim Rudat
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-12-22 00:00:00.000000000 Z
11
+ date: 2023-02-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: appraisal
@@ -121,6 +121,7 @@ files:
121
121
  - lib/jwt/jwk/key_base.rb
122
122
  - lib/jwt/jwk/key_finder.rb
123
123
  - lib/jwt/jwk/kid_as_key_digest.rb
124
+ - lib/jwt/jwk/okp_rbnacl.rb
124
125
  - lib/jwt/jwk/rsa.rb
125
126
  - lib/jwt/jwk/set.rb
126
127
  - lib/jwt/jwk/thumbprint.rb
@@ -134,7 +135,7 @@ licenses:
134
135
  - MIT
135
136
  metadata:
136
137
  bug_tracker_uri: https://github.com/jwt/ruby-jwt/issues
137
- changelog_uri: https://github.com/jwt/ruby-jwt/blob/v2.6.0/CHANGELOG.md
138
+ changelog_uri: https://github.com/jwt/ruby-jwt/blob/v2.7.0/CHANGELOG.md
138
139
  rubygems_mfa_required: 'true'
139
140
  post_install_message:
140
141
  rdoc_options: []