jwt 2.6.0 → 2.7.0

Sign up to get free protection for your applications and to get access to all the features.
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: []