jwt 2.2.2 → 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/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
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWK
5
+ class HMAC < KeyBase
6
+ KTY = 'oct'.freeze
7
+ KTYS = [KTY, String].freeze
8
+
9
+ def initialize(keypair, kid = nil)
10
+ raise ArgumentError, 'keypair must be of type String' unless keypair.is_a?(String)
11
+
12
+ super
13
+ @kid = kid || generate_kid
14
+ end
15
+
16
+ def private?
17
+ true
18
+ end
19
+
20
+ def public_key
21
+ nil
22
+ end
23
+
24
+ # See https://tools.ietf.org/html/rfc7517#appendix-A.3
25
+ def export(options = {})
26
+ exported_hash = {
27
+ kty: KTY,
28
+ kid: kid
29
+ }
30
+
31
+ return exported_hash unless private? && options[:include_private] == true
32
+
33
+ exported_hash.merge(
34
+ k: keypair
35
+ )
36
+ end
37
+
38
+ private
39
+
40
+ def generate_kid
41
+ sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::UTF8String.new(keypair),
42
+ OpenSSL::ASN1::UTF8String.new(KTY)])
43
+ OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
44
+ end
45
+
46
+ class << self
47
+ def import(jwk_data)
48
+ jwk_k = jwk_data[:k] || jwk_data['k']
49
+ jwk_kid = jwk_data[:kid] || jwk_data['kid']
50
+
51
+ raise JWT::JWKError, 'Key format is invalid for HMAC' unless jwk_k
52
+
53
+ self.new(jwk_k, jwk_kid)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWK
5
+ class KeyBase
6
+ attr_reader :keypair, :kid
7
+
8
+ def initialize(keypair, kid = nil)
9
+ @keypair = keypair
10
+ @kid = kid
11
+ end
12
+
13
+ def self.inherited(klass)
14
+ ::JWT::JWK.classes << klass
15
+ end
16
+ end
17
+ end
18
+ end
@@ -14,6 +14,7 @@ module JWT
14
14
 
15
15
  jwk = resolve_key(kid)
16
16
 
17
+ raise ::JWT::DecodeError, 'No keys found in jwks' if jwks_keys.empty?
17
18
  raise ::JWT::DecodeError, "Could not find public key for kid #{kid}" unless jwk
18
19
 
19
20
  ::JWT::JWK.import(jwk).keypair
@@ -45,8 +46,12 @@ module JWT
45
46
  @jwks = @jwk_loader.call(opts)
46
47
  end
47
48
 
49
+ def jwks_keys
50
+ Array(jwks[:keys] || jwks['keys'])
51
+ end
52
+
48
53
  def find_key(kid)
49
- Array(jwks[:keys]).find { |key| key[:kid] == kid }
54
+ jwks_keys.find { |key| (key[:kid] || key['kid']) == kid }
50
55
  end
51
56
 
52
57
  def reloadable?
data/lib/jwt/jwk/rsa.rb CHANGED
@@ -2,16 +2,15 @@
2
2
 
3
3
  module JWT
4
4
  module JWK
5
- class RSA
6
- attr_reader :keypair
7
-
5
+ class RSA < KeyBase
8
6
  BINARY = 2
9
7
  KTY = 'RSA'.freeze
8
+ KTYS = [KTY, OpenSSL::PKey::RSA].freeze
9
+ RSA_KEY_ELEMENTS = %i[n e d p q dp dq qi].freeze
10
10
 
11
- def initialize(keypair)
11
+ def initialize(keypair, kid = nil)
12
12
  raise ArgumentError, 'keypair must be of type OpenSSL::PKey::RSA' unless keypair.is_a?(OpenSSL::PKey::RSA)
13
-
14
- @keypair = keypair
13
+ super(keypair, kid || generate_kid(keypair.public_key))
15
14
  end
16
15
 
17
16
  def private?
@@ -22,32 +21,94 @@ module JWT
22
21
  keypair.public_key
23
22
  end
24
23
 
25
- def kid
24
+ def export(options = {})
25
+ exported_hash = {
26
+ kty: KTY,
27
+ n: encode_open_ssl_bn(public_key.n),
28
+ e: encode_open_ssl_bn(public_key.e),
29
+ kid: kid
30
+ }
31
+
32
+ return exported_hash unless private? && options[:include_private] == true
33
+
34
+ append_private_parts(exported_hash)
35
+ end
36
+
37
+ private
38
+
39
+ def generate_kid(public_key)
26
40
  sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(public_key.n),
27
41
  OpenSSL::ASN1::Integer.new(public_key.e)])
28
42
  OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
29
43
  end
30
44
 
31
- def export
32
- {
33
- kty: KTY,
34
- n: ::Base64.urlsafe_encode64(public_key.n.to_s(BINARY), padding: false),
35
- e: ::Base64.urlsafe_encode64(public_key.e.to_s(BINARY), padding: false),
36
- kid: kid
37
- }
45
+ def append_private_parts(the_hash)
46
+ the_hash.merge(
47
+ d: encode_open_ssl_bn(keypair.d),
48
+ p: encode_open_ssl_bn(keypair.p),
49
+ q: encode_open_ssl_bn(keypair.q),
50
+ dp: encode_open_ssl_bn(keypair.dmp1),
51
+ dq: encode_open_ssl_bn(keypair.dmq1),
52
+ qi: encode_open_ssl_bn(keypair.iqmp)
53
+ )
38
54
  end
39
55
 
40
- def self.import(jwk_data)
41
- imported_key = OpenSSL::PKey::RSA.new
42
- if imported_key.respond_to?(:set_key)
43
- imported_key.set_key(OpenSSL::BN.new(::Base64.urlsafe_decode64(jwk_data[:n]), BINARY),
44
- OpenSSL::BN.new(::Base64.urlsafe_decode64(jwk_data[:e]), BINARY),
45
- nil)
56
+ def encode_open_ssl_bn(key_part)
57
+ ::JWT::Base64.url_encode(key_part.to_s(BINARY))
58
+ end
59
+
60
+ class << self
61
+ def import(jwk_data)
62
+ pkey_params = jwk_attributes(jwk_data, *RSA_KEY_ELEMENTS) do |value|
63
+ decode_open_ssl_bn(value)
64
+ end
65
+ kid = jwk_attributes(jwk_data, :kid)[:kid]
66
+ self.new(rsa_pkey(pkey_params), kid)
67
+ end
68
+
69
+ private
70
+
71
+ def jwk_attributes(jwk_data, *attributes)
72
+ attributes.each_with_object({}) do |attribute, hash|
73
+ value = jwk_data[attribute] || jwk_data[attribute.to_s]
74
+ value = yield(value) if block_given?
75
+ hash[attribute] = value
76
+ end
77
+ end
78
+
79
+ def rsa_pkey(rsa_parameters)
80
+ raise JWT::JWKError, 'Key format is invalid for RSA' unless rsa_parameters[:n] && rsa_parameters[:e]
81
+
82
+ populate_key(OpenSSL::PKey::RSA.new, rsa_parameters)
83
+ end
84
+
85
+ if OpenSSL::PKey::RSA.new.respond_to?(:set_key)
86
+ def populate_key(rsa_key, rsa_parameters)
87
+ rsa_key.set_key(rsa_parameters[:n], rsa_parameters[:e], rsa_parameters[:d])
88
+ rsa_key.set_factors(rsa_parameters[:p], rsa_parameters[:q]) if rsa_parameters[:p] && rsa_parameters[:q]
89
+ rsa_key.set_crt_params(rsa_parameters[:dp], rsa_parameters[:dq], rsa_parameters[:qi]) if rsa_parameters[:dp] && rsa_parameters[:dq] && rsa_parameters[:qi]
90
+ rsa_key
91
+ end
46
92
  else
47
- imported_key.n = OpenSSL::BN.new(::Base64.urlsafe_decode64(jwk_data[:n]), BINARY)
48
- imported_key.e = OpenSSL::BN.new(::Base64.urlsafe_decode64(jwk_data[:e]), BINARY)
93
+ def populate_key(rsa_key, rsa_parameters)
94
+ rsa_key.n = rsa_parameters[:n]
95
+ rsa_key.e = rsa_parameters[:e]
96
+ rsa_key.d = rsa_parameters[:d] if rsa_parameters[:d]
97
+ rsa_key.p = rsa_parameters[:p] if rsa_parameters[:p]
98
+ rsa_key.q = rsa_parameters[:q] if rsa_parameters[:q]
99
+ rsa_key.dmp1 = rsa_parameters[:dp] if rsa_parameters[:dp]
100
+ rsa_key.dmq1 = rsa_parameters[:dq] if rsa_parameters[:dq]
101
+ rsa_key.iqmp = rsa_parameters[:qi] if rsa_parameters[:qi]
102
+
103
+ rsa_key
104
+ end
105
+ end
106
+
107
+ def decode_open_ssl_bn(jwk_data)
108
+ return nil unless jwk_data
109
+
110
+ OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY)
49
111
  end
50
- self.new(imported_key)
51
112
  end
52
113
  end
53
114
  end
data/lib/jwt/jwk.rb CHANGED
@@ -1,31 +1,51 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'jwk/rsa'
4
3
  require_relative 'jwk/key_finder'
5
4
 
6
5
  module JWT
7
6
  module JWK
8
- MAPPINGS = {
9
- 'RSA' => ::JWT::JWK::RSA,
10
- OpenSSL::PKey::RSA => ::JWT::JWK::RSA
11
- }.freeze
12
-
13
7
  class << self
14
8
  def import(jwk_data)
15
- raise JWT::JWKError, 'Key type (kty) not provided' unless jwk_data[:kty]
9
+ jwk_kty = jwk_data[:kty] || jwk_data['kty']
10
+ raise JWT::JWKError, 'Key type (kty) not provided' unless jwk_kty
16
11
 
17
- MAPPINGS.fetch(jwk_data[:kty].to_s) do |kty|
12
+ mappings.fetch(jwk_kty.to_s) do |kty|
18
13
  raise JWT::JWKError, "Key type #{kty} not supported"
19
14
  end.import(jwk_data)
20
15
  end
21
16
 
22
- def create_from(keypair)
23
- MAPPINGS.fetch(keypair.class) do |klass|
17
+ def create_from(keypair, kid = nil)
18
+ mappings.fetch(keypair.class) do |klass|
24
19
  raise JWT::JWKError, "Cannot create JWK from a #{klass.name}"
25
- end.new(keypair)
20
+ end.new(keypair, kid)
21
+ end
22
+
23
+ def classes
24
+ @mappings = nil # reset the cached mappings
25
+ @classes ||= []
26
26
  end
27
27
 
28
28
  alias new create_from
29
+
30
+ private
31
+
32
+ def mappings
33
+ @mappings ||= generate_mappings
34
+ end
35
+
36
+ def generate_mappings
37
+ classes.each_with_object({}) do |klass, hash|
38
+ next unless klass.const_defined?('KTYS')
39
+ Array(klass::KTYS).each do |kty|
40
+ hash[kty] = klass
41
+ end
42
+ end
43
+ end
29
44
  end
30
45
  end
31
46
  end
47
+
48
+ require_relative 'jwk/key_base'
49
+ require_relative 'jwk/ec'
50
+ require_relative 'jwk/rsa'
51
+ require_relative 'jwk/hmac'
data/lib/jwt/signature.rb CHANGED
@@ -2,12 +2,7 @@
2
2
 
3
3
  require 'jwt/security_utils'
4
4
  require 'openssl'
5
- require 'jwt/algos/hmac'
6
- require 'jwt/algos/eddsa'
7
- require 'jwt/algos/ecdsa'
8
- require 'jwt/algos/rsa'
9
- require 'jwt/algos/ps'
10
- require 'jwt/algos/unsupported'
5
+ require 'jwt/algos'
11
6
  begin
12
7
  require 'rbnacl'
13
8
  rescue LoadError
@@ -19,31 +14,21 @@ module JWT
19
14
  # Signature logic for JWT
20
15
  module Signature
21
16
  extend self
22
- ALGOS = [
23
- Algos::Hmac,
24
- Algos::Ecdsa,
25
- Algos::Rsa,
26
- Algos::Eddsa,
27
- Algos::Ps,
28
- Algos::Unsupported
29
- ].freeze
30
17
  ToSign = Struct.new(:algorithm, :msg, :key)
31
18
  ToVerify = Struct.new(:algorithm, :public_key, :signing_input, :signature)
32
19
 
33
20
  def sign(algorithm, msg, key)
34
- algo = ALGOS.find do |alg|
35
- alg.const_get(:SUPPORTED).include? algorithm
36
- end
37
- algo.sign ToSign.new(algorithm, msg, key)
21
+ algo, code = Algos.find(algorithm)
22
+ algo.sign ToSign.new(code, msg, key)
38
23
  end
39
24
 
40
25
  def verify(algorithm, key, signing_input, signature)
26
+ return true if algorithm.casecmp('none').zero?
27
+
41
28
  raise JWT::DecodeError, 'No verification key available' unless key
42
29
 
43
- algo = ALGOS.find do |alg|
44
- alg.const_get(:SUPPORTED).include? algorithm
45
- end
46
- verified = algo.verify(ToVerify.new(algorithm, key, signing_input, signature))
30
+ algo, code = Algos.find(algorithm)
31
+ verified = algo.verify(ToVerify.new(code, key, signing_input, signature))
47
32
  raise(JWT::VerificationError, 'Signature verification raised') unless verified
48
33
  rescue OpenSSL::PKey::PKeyError
49
34
  raise JWT::VerificationError, 'Signature verification raised'
data/lib/jwt/verify.rb CHANGED
@@ -10,7 +10,7 @@ module JWT
10
10
  }.freeze
11
11
 
12
12
  class << self
13
- %w[verify_aud verify_expiration verify_iat verify_iss verify_jti verify_not_before verify_sub].each do |method_name|
13
+ %w[verify_aud verify_expiration verify_iat verify_iss verify_jti verify_not_before verify_sub verify_required_claims].each do |method_name|
14
14
  define_method method_name do |payload, options|
15
15
  new(payload, options).send(method_name)
16
16
  end
@@ -81,6 +81,13 @@ module JWT
81
81
  raise(JWT::InvalidSubError, "Invalid subject. Expected #{options_sub}, received #{sub || '<none>'}") unless sub.to_s == options_sub.to_s
82
82
  end
83
83
 
84
+ def verify_required_claims
85
+ return unless (options_required_claims = @options[:required_claims])
86
+ options_required_claims.each do |required_claim|
87
+ raise(JWT::MissingRequiredClaim, "Missing required claim #{required_claim}") unless @payload.include?(required_claim)
88
+ end
89
+ end
90
+
84
91
  private
85
92
 
86
93
  def global_leeway
data/lib/jwt/version.rb CHANGED
@@ -12,9 +12,9 @@ module JWT
12
12
  # major version
13
13
  MAJOR = 2
14
14
  # minor version
15
- MINOR = 2
15
+ MINOR = 3
16
16
  # tiny version
17
- TINY = 2
17
+ TINY = 0
18
18
  # alpha, beta, etc. tag
19
19
  PRE = nil
20
20
 
data/ruby-jwt.gemspec CHANGED
@@ -14,6 +14,10 @@ Gem::Specification.new do |spec|
14
14
  spec.homepage = 'https://github.com/jwt/ruby-jwt'
15
15
  spec.license = 'MIT'
16
16
  spec.required_ruby_version = '>= 2.1'
17
+ spec.metadata = {
18
+ 'bug_tracker_uri' => 'https://github.com/jwt/ruby-jwt/issues',
19
+ 'changelog_uri' => "https://github.com/jwt/ruby-jwt/blob/v#{JWT.gem_version}/CHANGELOG.md"
20
+ }
17
21
 
18
22
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec|gemfiles|coverage|bin)/}) }
19
23
  spec.executables = []
@@ -24,11 +28,5 @@ Gem::Specification.new do |spec|
24
28
  spec.add_development_dependency 'bundler'
25
29
  spec.add_development_dependency 'rake'
26
30
  spec.add_development_dependency 'rspec'
27
- spec.add_development_dependency 'simplecov', '< 0.18'
28
- spec.add_development_dependency 'simplecov-json'
29
- spec.add_development_dependency 'codeclimate-test-reporter'
30
- spec.add_development_dependency 'codacy-coverage'
31
- spec.add_development_dependency 'rbnacl'
32
- # RSASSA-PSS support provided by OpenSSL +2.1
33
- spec.add_development_dependency 'openssl', '~> 2.1'
31
+ spec.add_development_dependency 'simplecov'
34
32
  end