jwt 2.2.2 → 2.4.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWK
5
+ class HMAC < KeyBase
6
+ KTY = 'oct'
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,19 @@
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
+ super
15
+ ::JWT::JWK.classes << klass
16
+ end
17
+ end
18
+ end
19
+ 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,16 @@
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
- KTY = 'RSA'.freeze
7
+ KTY = 'RSA'
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
13
 
14
- @keypair = keypair
14
+ super(keypair, kid || generate_kid(keypair.public_key))
15
15
  end
16
16
 
17
17
  def private?
@@ -22,32 +22,94 @@ module JWT
22
22
  keypair.public_key
23
23
  end
24
24
 
25
- def kid
25
+ def export(options = {})
26
+ exported_hash = {
27
+ kty: KTY,
28
+ n: encode_open_ssl_bn(public_key.n),
29
+ e: encode_open_ssl_bn(public_key.e),
30
+ kid: kid
31
+ }
32
+
33
+ return exported_hash unless private? && options[:include_private] == true
34
+
35
+ append_private_parts(exported_hash)
36
+ end
37
+
38
+ private
39
+
40
+ def generate_kid(public_key)
26
41
  sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(public_key.n),
27
42
  OpenSSL::ASN1::Integer.new(public_key.e)])
28
43
  OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
29
44
  end
30
45
 
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
- }
46
+ def append_private_parts(the_hash)
47
+ the_hash.merge(
48
+ d: encode_open_ssl_bn(keypair.d),
49
+ p: encode_open_ssl_bn(keypair.p),
50
+ q: encode_open_ssl_bn(keypair.q),
51
+ dp: encode_open_ssl_bn(keypair.dmp1),
52
+ dq: encode_open_ssl_bn(keypair.dmq1),
53
+ qi: encode_open_ssl_bn(keypair.iqmp)
54
+ )
55
+ end
56
+
57
+ def encode_open_ssl_bn(key_part)
58
+ Base64.urlsafe_encode64(key_part.to_s(BINARY), padding: false)
38
59
  end
39
60
 
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)
61
+ class << self
62
+ def import(jwk_data)
63
+ pkey_params = jwk_attributes(jwk_data, *RSA_KEY_ELEMENTS) do |value|
64
+ decode_open_ssl_bn(value)
65
+ end
66
+ kid = jwk_attributes(jwk_data, :kid)[:kid]
67
+ self.new(rsa_pkey(pkey_params), kid)
68
+ end
69
+
70
+ private
71
+
72
+ def jwk_attributes(jwk_data, *attributes)
73
+ attributes.each_with_object({}) do |attribute, hash|
74
+ value = jwk_data[attribute] || jwk_data[attribute.to_s]
75
+ value = yield(value) if block_given?
76
+ hash[attribute] = value
77
+ end
78
+ end
79
+
80
+ def rsa_pkey(rsa_parameters)
81
+ raise JWT::JWKError, 'Key format is invalid for RSA' unless rsa_parameters[:n] && rsa_parameters[:e]
82
+
83
+ populate_key(OpenSSL::PKey::RSA.new, rsa_parameters)
84
+ end
85
+
86
+ if OpenSSL::PKey::RSA.new.respond_to?(:set_key)
87
+ def populate_key(rsa_key, rsa_parameters)
88
+ rsa_key.set_key(rsa_parameters[:n], rsa_parameters[:e], rsa_parameters[:d])
89
+ rsa_key.set_factors(rsa_parameters[:p], rsa_parameters[:q]) if rsa_parameters[:p] && rsa_parameters[:q]
90
+ rsa_key.set_crt_params(rsa_parameters[:dp], rsa_parameters[:dq], rsa_parameters[:qi]) if rsa_parameters[:dp] && rsa_parameters[:dq] && rsa_parameters[:qi]
91
+ rsa_key
92
+ end
46
93
  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)
94
+ def populate_key(rsa_key, rsa_parameters)
95
+ rsa_key.n = rsa_parameters[:n]
96
+ rsa_key.e = rsa_parameters[:e]
97
+ rsa_key.d = rsa_parameters[:d] if rsa_parameters[:d]
98
+ rsa_key.p = rsa_parameters[:p] if rsa_parameters[:p]
99
+ rsa_key.q = rsa_parameters[:q] if rsa_parameters[:q]
100
+ rsa_key.dmp1 = rsa_parameters[:dp] if rsa_parameters[:dp]
101
+ rsa_key.dmq1 = rsa_parameters[:dq] if rsa_parameters[:dq]
102
+ rsa_key.iqmp = rsa_parameters[:qi] if rsa_parameters[:qi]
103
+
104
+ rsa_key
105
+ end
106
+ end
107
+
108
+ def decode_open_ssl_bn(jwk_data)
109
+ return nil unless jwk_data
110
+
111
+ OpenSSL::BN.new(Base64.urlsafe_decode64(jwk_data), BINARY)
49
112
  end
50
- self.new(imported_key)
51
113
  end
52
114
  end
53
115
  end
data/lib/jwt/jwk.rb CHANGED
@@ -1,31 +1,52 @@
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
+
40
+ Array(klass::KTYS).each do |kty|
41
+ hash[kty] = klass
42
+ end
43
+ end
44
+ end
29
45
  end
30
46
  end
31
47
  end
48
+
49
+ require_relative 'jwk/key_base'
50
+ require_relative 'jwk/ec'
51
+ require_relative 'jwk/rsa'
52
+ require_relative 'jwk/hmac'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JWT
2
4
  # Collection of security methods
3
5
  #
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
@@ -18,33 +13,19 @@ end
18
13
  module JWT
19
14
  # Signature logic for JWT
20
15
  module Signature
21
- extend self
22
- ALGOS = [
23
- Algos::Hmac,
24
- Algos::Ecdsa,
25
- Algos::Rsa,
26
- Algos::Eddsa,
27
- Algos::Ps,
28
- Algos::Unsupported
29
- ].freeze
16
+ module_function
17
+
30
18
  ToSign = Struct.new(:algorithm, :msg, :key)
31
19
  ToVerify = Struct.new(:algorithm, :public_key, :signing_input, :signature)
32
20
 
33
21
  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)
22
+ algo, code = Algos.find(algorithm)
23
+ algo.sign ToSign.new(code, msg, key)
38
24
  end
39
25
 
40
26
  def verify(algorithm, key, signing_input, signature)
41
- raise JWT::DecodeError, 'No verification key available' unless key
42
-
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))
47
- raise(JWT::VerificationError, 'Signature verification raised') unless verified
27
+ algo, code = Algos.find(algorithm)
28
+ algo.verify(ToVerify.new(code, key, signing_input, signature))
48
29
  rescue OpenSSL::PKey::PKeyError
49
30
  raise JWT::VerificationError, 'Signature verification raised'
50
31
  ensure
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
@@ -19,6 +19,7 @@ module JWT
19
19
  def verify_claims(payload, options)
20
20
  options.each do |key, val|
21
21
  next unless key.to_s =~ /verify/
22
+
22
23
  Verify.send(key, payload, options) if val
23
24
  end
24
25
  end
@@ -53,9 +54,14 @@ module JWT
53
54
 
54
55
  iss = @payload['iss']
55
56
 
56
- return if Array(options_iss).map(&:to_s).include?(iss.to_s)
57
+ options_iss = Array(options_iss).map { |item| item.is_a?(Symbol) ? item.to_s : item }
57
58
 
58
- raise(JWT::InvalidIssuerError, "Invalid issuer. Expected #{options_iss}, received #{iss || '<none>'}")
59
+ case iss
60
+ when *options_iss
61
+ nil
62
+ else
63
+ raise(JWT::InvalidIssuerError, "Invalid issuer. Expected #{options_iss}, received #{iss || '<none>'}")
64
+ end
59
65
  end
60
66
 
61
67
  def verify_jti
@@ -77,10 +83,19 @@ module JWT
77
83
 
78
84
  def verify_sub
79
85
  return unless (options_sub = @options[:sub])
86
+
80
87
  sub = @payload['sub']
81
88
  raise(JWT::InvalidSubError, "Invalid subject. Expected #{options_sub}, received #{sub || '<none>'}") unless sub.to_s == options_sub.to_s
82
89
  end
83
90
 
91
+ def verify_required_claims
92
+ return unless (options_required_claims = @options[:required_claims])
93
+
94
+ options_required_claims.each do |required_claim|
95
+ raise(JWT::MissingRequiredClaim, "Missing required claim #{required_claim}") unless @payload.include?(required_claim)
96
+ end
97
+ end
98
+
84
99
  private
85
100
 
86
101
  def global_leeway
data/lib/jwt/version.rb CHANGED
@@ -1,4 +1,3 @@
1
- # encoding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  # Moments version builder module
@@ -12,11 +11,11 @@ module JWT
12
11
  # major version
13
12
  MAJOR = 2
14
13
  # minor version
15
- MINOR = 2
14
+ MINOR = 4
16
15
  # tiny version
17
- TINY = 2
16
+ TINY = 0
18
17
  # alpha, beta, etc. tag
19
- PRE = nil
18
+ PRE = 'beta1'
20
19
 
21
20
  # Build version string
22
21
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'jwt/error'
5
+
6
+ module JWT
7
+ # If the x5c header certificate chain can be validated by trusted root
8
+ # certificates, and none of the certificates are revoked, returns the public
9
+ # key from the first certificate.
10
+ # See https://tools.ietf.org/html/rfc7515#section-4.1.6
11
+ class X5cKeyFinder
12
+ def initialize(root_certificates, crls = nil)
13
+ raise(ArgumentError, 'Root certificates must be specified') unless root_certificates
14
+
15
+ @store = build_store(root_certificates, crls)
16
+ end
17
+
18
+ def from(x5c_header_or_certificates)
19
+ signing_certificate, *certificate_chain = parse_certificates(x5c_header_or_certificates)
20
+ store_context = OpenSSL::X509::StoreContext.new(@store, signing_certificate, certificate_chain)
21
+
22
+ if store_context.verify
23
+ signing_certificate.public_key
24
+ else
25
+ error = "Certificate verification failed: #{store_context.error_string}."
26
+ if (current_cert = store_context.current_cert)
27
+ error = "#{error} Certificate subject: #{current_cert.subject}."
28
+ end
29
+
30
+ raise(JWT::VerificationError, error)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def build_store(root_certificates, crls)
37
+ store = OpenSSL::X509::Store.new
38
+ store.purpose = OpenSSL::X509::PURPOSE_ANY
39
+ store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK | OpenSSL::X509::V_FLAG_CRL_CHECK_ALL
40
+ root_certificates.each { |certificate| store.add_cert(certificate) }
41
+ crls&.each { |crl| store.add_crl(crl) }
42
+ store
43
+ end
44
+
45
+ def parse_certificates(x5c_header_or_certificates)
46
+ if x5c_header_or_certificates.all? { |obj| obj.is_a?(OpenSSL::X509::Certificate) }
47
+ x5c_header_or_certificates
48
+ else
49
+ x5c_header_or_certificates.map do |encoded|
50
+ OpenSSL::X509::Certificate.new(::Base64.strict_decode64(encoded))
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
data/lib/jwt.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'jwt/base64'
3
+ require 'base64'
4
4
  require 'jwt/json'
5
5
  require 'jwt/decode'
6
6
  require 'jwt/default_options'
data/ruby-jwt.gemspec CHANGED
@@ -1,4 +1,6 @@
1
- lib = File.expand_path('../lib/', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
2
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
5
  require 'jwt/version'
4
6
 
@@ -13,7 +15,11 @@ Gem::Specification.new do |spec|
13
15
  spec.description = 'A pure ruby implementation of the RFC 7519 OAuth JSON Web Token (JWT) standard.'
14
16
  spec.homepage = 'https://github.com/jwt/ruby-jwt'
15
17
  spec.license = 'MIT'
16
- spec.required_ruby_version = '>= 2.1'
18
+ spec.required_ruby_version = '>= 2.5'
19
+ spec.metadata = {
20
+ 'bug_tracker_uri' => 'https://github.com/jwt/ruby-jwt/issues',
21
+ 'changelog_uri' => "https://github.com/jwt/ruby-jwt/blob/v#{JWT.gem_version}/CHANGELOG.md"
22
+ }
17
23
 
18
24
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec|gemfiles|coverage|bin)/}) }
19
25
  spec.executables = []
@@ -24,11 +30,5 @@ Gem::Specification.new do |spec|
24
30
  spec.add_development_dependency 'bundler'
25
31
  spec.add_development_dependency 'rake'
26
32
  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'
33
+ spec.add_development_dependency 'simplecov'
34
34
  end