jwt 2.8.2 → 3.1.1

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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +149 -31
  3. data/CODE_OF_CONDUCT.md +14 -14
  4. data/CONTRIBUTING.md +9 -10
  5. data/README.md +299 -234
  6. data/UPGRADING.md +47 -0
  7. data/lib/jwt/base64.rb +4 -10
  8. data/lib/jwt/claims/audience.rb +30 -0
  9. data/lib/jwt/claims/crit.rb +35 -0
  10. data/lib/jwt/claims/decode_verifier.rb +40 -0
  11. data/lib/jwt/claims/expiration.rb +32 -0
  12. data/lib/jwt/claims/issued_at.rb +22 -0
  13. data/lib/jwt/claims/issuer.rb +34 -0
  14. data/lib/jwt/claims/jwt_id.rb +35 -0
  15. data/lib/jwt/claims/not_before.rb +32 -0
  16. data/lib/jwt/claims/numeric.rb +45 -0
  17. data/lib/jwt/claims/required.rb +33 -0
  18. data/lib/jwt/claims/subject.rb +30 -0
  19. data/lib/jwt/claims/verifier.rb +61 -0
  20. data/lib/jwt/claims.rb +67 -0
  21. data/lib/jwt/configuration/container.rb +20 -1
  22. data/lib/jwt/configuration/decode_configuration.rb +24 -0
  23. data/lib/jwt/configuration/jwk_configuration.rb +1 -0
  24. data/lib/jwt/configuration.rb +8 -0
  25. data/lib/jwt/decode.rb +42 -81
  26. data/lib/jwt/encode.rb +17 -60
  27. data/lib/jwt/encoded_token.rb +236 -0
  28. data/lib/jwt/error.rb +32 -1
  29. data/lib/jwt/json.rb +1 -1
  30. data/lib/jwt/jwa/ecdsa.rb +59 -24
  31. data/lib/jwt/jwa/hmac.rb +22 -19
  32. data/lib/jwt/jwa/none.rb +8 -3
  33. data/lib/jwt/jwa/ps.rb +21 -15
  34. data/lib/jwt/jwa/rsa.rb +21 -10
  35. data/lib/jwt/jwa/signing_algorithm.rb +62 -0
  36. data/lib/jwt/jwa/unsupported.rb +9 -8
  37. data/lib/jwt/jwa.rb +76 -35
  38. data/lib/jwt/jwk/ec.rb +54 -65
  39. data/lib/jwt/jwk/hmac.rb +5 -6
  40. data/lib/jwt/jwk/key_base.rb +16 -1
  41. data/lib/jwt/jwk/key_finder.rb +35 -8
  42. data/lib/jwt/jwk/kid_as_key_digest.rb +1 -0
  43. data/lib/jwt/jwk/rsa.rb +7 -4
  44. data/lib/jwt/jwk/set.rb +2 -0
  45. data/lib/jwt/jwk.rb +1 -1
  46. data/lib/jwt/token.rb +131 -0
  47. data/lib/jwt/version.rb +24 -19
  48. data/lib/jwt.rb +18 -4
  49. data/ruby-jwt.gemspec +2 -0
  50. metadata +49 -15
  51. data/lib/jwt/claims_validator.rb +0 -37
  52. data/lib/jwt/deprecations.rb +0 -48
  53. data/lib/jwt/jwa/eddsa.rb +0 -42
  54. data/lib/jwt/jwa/hmac_rbnacl.rb +0 -50
  55. data/lib/jwt/jwa/hmac_rbnacl_fixed.rb +0 -46
  56. data/lib/jwt/jwa/wrapper.rb +0 -26
  57. data/lib/jwt/jwk/okp_rbnacl.rb +0 -110
  58. data/lib/jwt/verify.rb +0 -117
data/lib/jwt/jwa/ecdsa.rb CHANGED
@@ -2,8 +2,40 @@
2
2
 
3
3
  module JWT
4
4
  module JWA
5
- module Ecdsa
6
- module_function
5
+ # ECDSA signing algorithm
6
+ class Ecdsa
7
+ include JWT::JWA::SigningAlgorithm
8
+
9
+ def initialize(alg, digest)
10
+ @alg = alg
11
+ @digest = OpenSSL::Digest.new(digest)
12
+ end
13
+
14
+ def sign(data:, signing_key:)
15
+ raise_sign_error!("The given key is a #{signing_key.class}. It has to be an OpenSSL::PKey::EC instance") unless signing_key.is_a?(::OpenSSL::PKey::EC)
16
+ raise_sign_error!('The given key is not a private key') unless signing_key.private?
17
+
18
+ curve_definition = curve_by_name(signing_key.group.curve_name)
19
+ key_algorithm = curve_definition[:algorithm]
20
+
21
+ raise IncorrectAlgorithm, "payload algorithm is #{alg} but #{key_algorithm} signing key was provided" if alg != key_algorithm
22
+
23
+ asn1_to_raw(signing_key.dsa_sign_asn1(digest.digest(data)), signing_key)
24
+ end
25
+
26
+ def verify(data:, signature:, verification_key:)
27
+ verification_key = self.class.create_public_key_from_point(verification_key) if verification_key.is_a?(::OpenSSL::PKey::EC::Point)
28
+
29
+ raise_verify_error!("The given key is a #{verification_key.class}. It has to be an OpenSSL::PKey::EC instance") unless verification_key.is_a?(::OpenSSL::PKey::EC)
30
+
31
+ curve_definition = curve_by_name(verification_key.group.curve_name)
32
+ key_algorithm = curve_definition[:algorithm]
33
+ raise IncorrectAlgorithm, "payload algorithm is #{alg} but #{key_algorithm} verification key was provided" if alg != key_algorithm
34
+
35
+ verification_key.dsa_verify_asn1(digest.digest(data), raw_to_asn1(signature, verification_key))
36
+ rescue OpenSSL::PKey::PKeyError
37
+ raise JWT::VerificationError, 'Signature verification raised'
38
+ end
7
39
 
8
40
  NAMED_CURVES = {
9
41
  'prime256v1' => {
@@ -28,36 +60,39 @@ module JWT
28
60
  }
29
61
  }.freeze
30
62
 
31
- SUPPORTED = NAMED_CURVES.map { |_, c| c[:algorithm] }.uniq.freeze
63
+ NAMED_CURVES.each_value do |v|
64
+ register_algorithm(new(v[:algorithm], v[:digest]))
65
+ end
32
66
 
33
- def sign(algorithm, msg, key)
34
- curve_definition = curve_by_name(key.group.curve_name)
35
- key_algorithm = curve_definition[:algorithm]
36
- if algorithm != key_algorithm
37
- raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} signing key was provided"
67
+ # @api private
68
+ def self.curve_by_name(name)
69
+ NAMED_CURVES.fetch(name) do
70
+ raise UnsupportedEcdsaCurve, "The ECDSA curve '#{name}' is not supported"
38
71
  end
39
-
40
- digest = OpenSSL::Digest.new(curve_definition[:digest])
41
- asn1_to_raw(key.dsa_sign_asn1(digest.digest(msg)), key)
42
72
  end
43
73
 
44
- def verify(algorithm, public_key, signing_input, signature)
45
- curve_definition = curve_by_name(public_key.group.curve_name)
46
- key_algorithm = curve_definition[:algorithm]
47
- if algorithm != key_algorithm
48
- raise IncorrectAlgorithm, "payload algorithm is #{algorithm} but #{key_algorithm} verification key was provided"
74
+ if ::JWT.openssl_3?
75
+ def self.create_public_key_from_point(point)
76
+ sequence = OpenSSL::ASN1::Sequence([
77
+ OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ObjectId('id-ecPublicKey'), OpenSSL::ASN1::ObjectId(point.group.curve_name)]),
78
+ OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed))
79
+ ])
80
+ OpenSSL::PKey::EC.new(sequence.to_der)
81
+ end
82
+ else
83
+ def self.create_public_key_from_point(point)
84
+ OpenSSL::PKey::EC.new(point.group.curve_name).tap do |key|
85
+ key.public_key = point
86
+ end
49
87
  end
50
-
51
- digest = OpenSSL::Digest.new(curve_definition[:digest])
52
- public_key.dsa_verify_asn1(digest.digest(signing_input), raw_to_asn1(signature, public_key))
53
- rescue OpenSSL::PKey::PKeyError
54
- raise JWT::VerificationError, 'Signature verification raised'
55
88
  end
56
89
 
90
+ private
91
+
92
+ attr_reader :digest
93
+
57
94
  def curve_by_name(name)
58
- NAMED_CURVES.fetch(name) do
59
- raise UnsupportedEcdsaCurve, "The ECDSA curve '#{name}' is not supported"
60
- end
95
+ self.class.curve_by_name(name)
61
96
  end
62
97
 
63
98
  def raw_to_asn1(signature, private_key)
data/lib/jwt/jwa/hmac.rb CHANGED
@@ -2,35 +2,38 @@
2
2
 
3
3
  module JWT
4
4
  module JWA
5
- module Hmac
6
- module_function
5
+ # Implementation of the HMAC family of algorithms
6
+ class Hmac
7
+ include JWT::JWA::SigningAlgorithm
7
8
 
8
- MAPPING = {
9
- 'HS256' => OpenSSL::Digest::SHA256,
10
- 'HS384' => OpenSSL::Digest::SHA384,
11
- 'HS512' => OpenSSL::Digest::SHA512
12
- }.freeze
13
-
14
- SUPPORTED = MAPPING.keys
15
-
16
- def sign(algorithm, msg, key)
17
- key ||= ''
9
+ def initialize(alg, digest)
10
+ @alg = alg
11
+ @digest = digest
12
+ end
18
13
 
19
- raise JWT::DecodeError, 'HMAC key expected to be a String' unless key.is_a?(String)
14
+ def sign(data:, signing_key:)
15
+ signing_key ||= ''
16
+ raise_verify_error!('HMAC key expected to be a String') unless signing_key.is_a?(String)
20
17
 
21
- OpenSSL::HMAC.digest(MAPPING[algorithm].new, key, msg)
18
+ OpenSSL::HMAC.digest(digest.new, signing_key, data)
22
19
  rescue OpenSSL::HMACError => e
23
- if key == '' && e.message == 'EVP_PKEY_new_mac_key: malloc failure'
24
- raise JWT::DecodeError, 'OpenSSL 3.0 does not support nil or empty hmac_secret'
25
- end
20
+ raise_verify_error!('OpenSSL 3.0 does not support nil or empty hmac_secret') if signing_key == '' && e.message == 'EVP_PKEY_new_mac_key: malloc failure'
26
21
 
27
22
  raise e
28
23
  end
29
24
 
30
- def verify(algorithm, key, signing_input, signature)
31
- SecurityUtils.secure_compare(signature, sign(algorithm, signing_input, key))
25
+ def verify(data:, signature:, verification_key:)
26
+ SecurityUtils.secure_compare(signature, sign(data: data, signing_key: verification_key))
32
27
  end
33
28
 
29
+ register_algorithm(new('HS256', OpenSSL::Digest::SHA256))
30
+ register_algorithm(new('HS384', OpenSSL::Digest::SHA384))
31
+ register_algorithm(new('HS512', OpenSSL::Digest::SHA512))
32
+
33
+ private
34
+
35
+ attr_reader :digest
36
+
34
37
  # Copy of https://github.com/rails/rails/blob/v7.0.3.1/activesupport/lib/active_support/security_utils.rb
35
38
  # rubocop:disable Naming/MethodParameterName, Style/StringLiterals, Style/NumericPredicate
36
39
  module SecurityUtils
data/lib/jwt/jwa/none.rb CHANGED
@@ -2,10 +2,13 @@
2
2
 
3
3
  module JWT
4
4
  module JWA
5
- module None
6
- module_function
5
+ # Implementation of the none algorithm
6
+ class None
7
+ include JWT::JWA::SigningAlgorithm
7
8
 
8
- SUPPORTED = %w[none].freeze
9
+ def initialize
10
+ @alg = 'none'
11
+ end
9
12
 
10
13
  def sign(*)
11
14
  ''
@@ -14,6 +17,8 @@ module JWT
14
17
  def verify(*)
15
18
  true
16
19
  end
20
+
21
+ register_algorithm(new)
17
22
  end
18
23
  end
19
24
  end
data/lib/jwt/jwa/ps.rb CHANGED
@@ -2,29 +2,35 @@
2
2
 
3
3
  module JWT
4
4
  module JWA
5
- module Ps
6
- # RSASSA-PSS signing algorithms
5
+ # Implementation of the RSASSA-PSS family of algorithms
6
+ class Ps
7
+ include JWT::JWA::SigningAlgorithm
7
8
 
8
- module_function
9
-
10
- SUPPORTED = %w[PS256 PS384 PS512].freeze
11
-
12
- def sign(algorithm, msg, key)
13
- unless key.is_a?(::OpenSSL::PKey::RSA)
14
- raise EncodeError, "The given key is a #{key_class}. It has to be an OpenSSL::PKey::RSA instance."
15
- end
9
+ def initialize(alg)
10
+ @alg = alg
11
+ @digest_algorithm = alg.sub('PS', 'sha')
12
+ end
16
13
 
17
- translated_algorithm = algorithm.sub('PS', 'sha')
14
+ def sign(data:, signing_key:)
15
+ raise_sign_error!("The given key is a #{signing_key.class}. It has to be an OpenSSL::PKey::RSA instance.") unless signing_key.is_a?(::OpenSSL::PKey::RSA)
16
+ raise_sign_error!('The key length must be greater than or equal to 2048 bits') if signing_key.n.num_bits < 2048
18
17
 
19
- key.sign_pss(translated_algorithm, msg, salt_length: :digest, mgf1_hash: translated_algorithm)
18
+ signing_key.sign_pss(digest_algorithm, data, salt_length: :digest, mgf1_hash: digest_algorithm)
20
19
  end
21
20
 
22
- def verify(algorithm, public_key, signing_input, signature)
23
- translated_algorithm = algorithm.sub('PS', 'sha')
24
- public_key.verify_pss(translated_algorithm, signature, signing_input, salt_length: :auto, mgf1_hash: translated_algorithm)
21
+ def verify(data:, signature:, verification_key:)
22
+ verification_key.verify_pss(digest_algorithm, signature, data, salt_length: :auto, mgf1_hash: digest_algorithm)
25
23
  rescue OpenSSL::PKey::PKeyError
26
24
  raise JWT::VerificationError, 'Signature verification raised'
27
25
  end
26
+
27
+ register_algorithm(new('PS256'))
28
+ register_algorithm(new('PS384'))
29
+ register_algorithm(new('PS512'))
30
+
31
+ private
32
+
33
+ attr_reader :digest_algorithm
28
34
  end
29
35
  end
30
36
  end
data/lib/jwt/jwa/rsa.rb CHANGED
@@ -2,24 +2,35 @@
2
2
 
3
3
  module JWT
4
4
  module JWA
5
- module Rsa
6
- module_function
5
+ # Implementation of the RSA family of algorithms
6
+ class Rsa
7
+ include JWT::JWA::SigningAlgorithm
7
8
 
8
- SUPPORTED = %w[RS256 RS384 RS512].freeze
9
+ def initialize(alg)
10
+ @alg = alg
11
+ @digest = OpenSSL::Digest.new(alg.sub('RS', 'SHA'))
12
+ end
9
13
 
10
- def sign(algorithm, msg, key)
11
- unless key.is_a?(OpenSSL::PKey::RSA)
12
- raise EncodeError, "The given key is a #{key.class}. It has to be an OpenSSL::PKey::RSA instance"
13
- end
14
+ def sign(data:, signing_key:)
15
+ raise_sign_error!("The given key is a #{signing_key.class}. It has to be an OpenSSL::PKey::RSA instance") unless signing_key.is_a?(OpenSSL::PKey::RSA)
16
+ raise_sign_error!('The key length must be greater than or equal to 2048 bits') if signing_key.n.num_bits < 2048
14
17
 
15
- key.sign(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), msg)
18
+ signing_key.sign(digest, data)
16
19
  end
17
20
 
18
- def verify(algorithm, public_key, signing_input, signature)
19
- public_key.verify(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), signature, signing_input)
21
+ def verify(data:, signature:, verification_key:)
22
+ verification_key.verify(digest, signature, data)
20
23
  rescue OpenSSL::PKey::PKeyError
21
24
  raise JWT::VerificationError, 'Signature verification raised'
22
25
  end
26
+
27
+ register_algorithm(new('RS256'))
28
+ register_algorithm(new('RS384'))
29
+ register_algorithm(new('RS512'))
30
+
31
+ private
32
+
33
+ attr_reader :digest
23
34
  end
24
35
  end
25
36
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ # JSON Web Algorithms
5
+ module JWA
6
+ # Base functionality for signing algorithms
7
+ module SigningAlgorithm
8
+ # Class methods for the SigningAlgorithm module
9
+ module ClassMethods
10
+ def register_algorithm(algo)
11
+ ::JWT::JWA.register_algorithm(algo)
12
+ end
13
+ end
14
+
15
+ def self.included(klass)
16
+ klass.extend(ClassMethods)
17
+ end
18
+
19
+ attr_reader :alg
20
+
21
+ def valid_alg?(alg_to_check)
22
+ alg&.casecmp(alg_to_check)&.zero? == true
23
+ end
24
+
25
+ def header(*)
26
+ { 'alg' => alg }
27
+ end
28
+
29
+ def sign(*)
30
+ raise_sign_error!('Algorithm implementation is missing the sign method')
31
+ end
32
+
33
+ def verify(*)
34
+ raise_verify_error!('Algorithm implementation is missing the verify method')
35
+ end
36
+
37
+ def raise_verify_error!(message)
38
+ raise(DecodeError.new(message).tap { |e| e.set_backtrace(caller(1)) })
39
+ end
40
+
41
+ def raise_sign_error!(message)
42
+ raise(EncodeError.new(message).tap { |e| e.set_backtrace(caller(1)) })
43
+ end
44
+ end
45
+
46
+ class << self
47
+ def register_algorithm(algo)
48
+ algorithms[algo.alg.to_s.downcase] = algo
49
+ end
50
+
51
+ def find(algo)
52
+ algorithms.fetch(algo.to_s.downcase, Unsupported)
53
+ end
54
+
55
+ private
56
+
57
+ def algorithms
58
+ @algorithms ||= {}
59
+ end
60
+ end
61
+ end
62
+ end
@@ -2,17 +2,18 @@
2
2
 
3
3
  module JWT
4
4
  module JWA
5
+ # Represents an unsupported algorithm
5
6
  module Unsupported
6
- module_function
7
+ class << self
8
+ include JWT::JWA::SigningAlgorithm
7
9
 
8
- SUPPORTED = [].freeze
10
+ def sign(*)
11
+ raise_sign_error!('Unsupported signing method')
12
+ end
9
13
 
10
- def sign(*)
11
- raise NotImplementedError, 'Unsupported signing method'
12
- end
13
-
14
- def verify(*)
15
- raise JWT::VerificationError, 'Algorithm not supported'
14
+ def verify(*)
15
+ raise JWT::VerificationError, 'Algorithm not supported'
16
+ end
16
17
  end
17
18
  end
18
19
  end
data/lib/jwt/jwa.rb CHANGED
@@ -2,60 +2,101 @@
2
2
 
3
3
  require 'openssl'
4
4
 
5
- begin
6
- require 'rbnacl'
7
- rescue LoadError
8
- raise if defined?(RbNaCl)
9
- end
10
-
11
- require_relative 'jwa/hmac'
12
- require_relative 'jwa/eddsa'
5
+ require_relative 'jwa/signing_algorithm'
13
6
  require_relative 'jwa/ecdsa'
14
- require_relative 'jwa/rsa'
15
- require_relative 'jwa/ps'
7
+ require_relative 'jwa/hmac'
16
8
  require_relative 'jwa/none'
9
+ require_relative 'jwa/ps'
10
+ require_relative 'jwa/rsa'
17
11
  require_relative 'jwa/unsupported'
18
- require_relative 'jwa/wrapper'
19
12
 
20
13
  module JWT
14
+ # The JWA module contains all supported algorithms.
21
15
  module JWA
22
- ALGOS = [Hmac, Ecdsa, Rsa, Eddsa, Ps, None, Unsupported].tap do |l|
23
- if ::JWT.rbnacl_6_or_greater?
24
- require_relative 'jwa/hmac_rbnacl'
25
- l << Algos::HmacRbNaCl
26
- elsif ::JWT.rbnacl?
27
- require_relative 'jwa/hmac_rbnacl_fixed'
28
- l << Algos::HmacRbNaClFixed
16
+ # @api private
17
+ class VerifierContext
18
+ attr_reader :jwa
19
+
20
+ def initialize(jwa:, keys:)
21
+ @jwa = jwa
22
+ @keys = Array(keys)
23
+ end
24
+
25
+ def verify(*args, **kwargs)
26
+ @keys.any? do |key|
27
+ @jwa.verify(*args, **kwargs, verification_key: key)
28
+ end
29
+ end
30
+ end
31
+
32
+ # @api private
33
+ class SignerContext
34
+ attr_reader :jwa
35
+
36
+ def initialize(jwa:, key:)
37
+ @jwa = jwa
38
+ @key = key
39
+ end
40
+
41
+ def sign(*args, **kwargs)
42
+ @jwa.sign(*args, **kwargs, signing_key: @key)
29
43
  end
30
- end.freeze
44
+ end
31
45
 
32
46
  class << self
33
- def find(algorithm)
34
- indexed[algorithm&.downcase]
47
+ # @api private
48
+ def resolve(algorithm)
49
+ return find(algorithm) if algorithm.is_a?(String) || algorithm.is_a?(Symbol)
50
+
51
+ raise ArgumentError, 'Algorithm must be provided' if algorithm.nil?
52
+
53
+ raise ArgumentError, 'Custom algorithms are required to include JWT::JWA::SigningAlgorithm' unless algorithm.is_a?(SigningAlgorithm)
54
+
55
+ algorithm
35
56
  end
36
57
 
37
- def create(algorithm)
38
- return algorithm if JWA.implementation?(algorithm)
58
+ # @api private
59
+ def resolve_and_sort(algorithms:, preferred_algorithm:)
60
+ Array(algorithms).map { |alg| JWA.resolve(alg) }
61
+ .partition { |alg| alg.valid_alg?(preferred_algorithm) }
62
+ .flatten
63
+ end
39
64
 
40
- Wrapper.new(*find(algorithm))
65
+ # @api private
66
+ def create_signer(algorithm:, key:)
67
+ if key.is_a?(JWK::KeyBase)
68
+ validate_jwk_algorithms!(key, algorithm, DecodeError)
69
+
70
+ return key
71
+ end
72
+
73
+ SignerContext.new(jwa: resolve(algorithm), key: key)
41
74
  end
42
75
 
43
- def implementation?(algorithm)
44
- (algorithm.respond_to?(:valid_alg?) && algorithm.respond_to?(:verify)) ||
45
- (algorithm.respond_to?(:alg) && algorithm.respond_to?(:sign))
76
+ # @api private
77
+ def create_verifiers(algorithms:, keys:, preferred_algorithm:)
78
+ jwks, other_keys = keys.partition { |key| key.is_a?(JWK::KeyBase) }
79
+
80
+ validate_jwk_algorithms!(jwks, algorithms, VerificationError)
81
+
82
+ jwks + resolve_and_sort(algorithms: algorithms,
83
+ preferred_algorithm: preferred_algorithm)
84
+ .map { |jwa| VerifierContext.new(jwa: jwa, keys: other_keys) }
46
85
  end
47
86
 
48
- private
87
+ # @api private
88
+ def validate_jwk_algorithms!(jwks, algorithms, error_class)
89
+ algorithms = Array(algorithms)
90
+
91
+ return if algorithms.empty?
49
92
 
50
- def indexed
51
- @indexed ||= begin
52
- fallback = [nil, Unsupported]
53
- ALGOS.each_with_object(Hash.new(fallback)) do |cls, hash|
54
- cls.const_get(:SUPPORTED).each do |alg|
55
- hash[alg.downcase] = [alg, cls]
56
- end
93
+ return if Array(jwks).all? do |jwk|
94
+ algorithms.any? do |alg|
95
+ jwk.jwa.valid_alg?(alg)
57
96
  end
58
97
  end
98
+
99
+ raise error_class, "Provided JWKs do not support one of the specified algorithms: #{algorithms.join(', ')}"
59
100
  end
60
101
  end
61
102
  end
data/lib/jwt/jwk/ec.rb CHANGED
@@ -4,6 +4,7 @@ require 'forwardable'
4
4
 
5
5
  module JWT
6
6
  module JWK
7
+ # JWK representation for Elliptic Curve (EC) keys
7
8
  class EC < KeyBase # rubocop:disable Metrics/ClassLength
8
9
  KTY = 'EC'
9
10
  KTYS = [KTY, OpenSSL::PKey::EC, JWT::JWK::EC].freeze
@@ -65,15 +66,20 @@ module JWT
65
66
  end
66
67
 
67
68
  def []=(key, value)
68
- if EC_KEY_ELEMENTS.include?(key.to_sym)
69
- raise ArgumentError, 'cannot overwrite cryptographic key attributes'
70
- end
69
+ raise ArgumentError, 'cannot overwrite cryptographic key attributes' if EC_KEY_ELEMENTS.include?(key.to_sym)
71
70
 
72
- super(key, value)
71
+ super
73
72
  end
74
73
 
75
74
  private
76
75
 
76
+ def jwa
77
+ return super if self[:alg]
78
+
79
+ curve_name = self.class.to_openssl_curve(self[:crv])
80
+ JWA.resolve(JWA::Ecdsa.curve_by_name(curve_name)[:algorithm])
81
+ end
82
+
77
83
  def ec_key
78
84
  @ec_key ||= create_ec_key(self[:crv], self[:x], self[:y], self[:d])
79
85
  end
@@ -125,10 +131,6 @@ module JWT
125
131
  ::JWT::Base64.url_encode(octets)
126
132
  end
127
133
 
128
- def encode_open_ssl_bn(key_part)
129
- ::JWT::Base64.url_encode(key_part.to_s(BINARY))
130
- end
131
-
132
134
  def parse_ec_key(key)
133
135
  crv, x_octets, y_octets = keypair_components(key)
134
136
  octets = key.private_key&.to_bn&.to_s(BINARY)
@@ -141,67 +143,54 @@ module JWT
141
143
  }.compact
142
144
  end
143
145
 
144
- if ::JWT.openssl_3?
145
- def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d) # rubocop:disable Metrics/MethodLength
146
- curve = EC.to_openssl_curve(jwk_crv)
147
- x_octets = decode_octets(jwk_x)
148
- y_octets = decode_octets(jwk_y)
149
-
150
- point = OpenSSL::PKey::EC::Point.new(
151
- OpenSSL::PKey::EC::Group.new(curve),
152
- OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
153
- )
154
-
155
- sequence = if jwk_d
156
- # https://datatracker.ietf.org/doc/html/rfc5915.html
157
- # ECPrivateKey ::= SEQUENCE {
158
- # version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
159
- # privateKey OCTET STRING,
160
- # parameters [0] ECParameters {{ NamedCurve }} OPTIONAL,
161
- # publicKey [1] BIT STRING OPTIONAL
162
- # }
163
-
164
- OpenSSL::ASN1::Sequence([
165
- OpenSSL::ASN1::Integer(1),
166
- OpenSSL::ASN1::OctetString(OpenSSL::BN.new(decode_octets(jwk_d), 2).to_s(2)),
167
- OpenSSL::ASN1::ObjectId(curve, 0, :EXPLICIT),
168
- OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed), 1, :EXPLICIT)
169
- ])
170
- else
171
- OpenSSL::ASN1::Sequence([
172
- OpenSSL::ASN1::Sequence([OpenSSL::ASN1::ObjectId('id-ecPublicKey'), OpenSSL::ASN1::ObjectId(curve)]),
173
- OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed))
174
- ])
175
- end
146
+ def create_point(jwk_crv, jwk_x, jwk_y)
147
+ curve = EC.to_openssl_curve(jwk_crv)
148
+ x_octets = decode_octets(jwk_x)
149
+ y_octets = decode_octets(jwk_y)
176
150
 
151
+ # The details of the `Point` instantiation are covered in:
152
+ # - https://docs.ruby-lang.org/en/2.4.0/OpenSSL/PKey/EC.html
153
+ # - https://www.openssl.org/docs/manmaster/man3/EC_POINT_new.html
154
+ # - https://tools.ietf.org/html/rfc5480#section-2.2
155
+ # - https://www.secg.org/SEC1-Ver-1.0.pdf
156
+ # Section 2.3.3 of the last of these references specifies that the
157
+ # encoding of an uncompressed point consists of the byte `0x04` followed
158
+ # by the x value then the y value.
159
+ OpenSSL::PKey::EC::Point.new(
160
+ OpenSSL::PKey::EC::Group.new(curve),
161
+ OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
162
+ )
163
+ end
164
+
165
+ if ::JWT.openssl_3?
166
+ def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d)
167
+ point = create_point(jwk_crv, jwk_x, jwk_y)
168
+
169
+ return ::JWT::JWA::Ecdsa.create_public_key_from_point(point) unless jwk_d
170
+
171
+ # https://datatracker.ietf.org/doc/html/rfc5915.html
172
+ # ECPrivateKey ::= SEQUENCE {
173
+ # version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
174
+ # privateKey OCTET STRING,
175
+ # parameters [0] ECParameters {{ NamedCurve }} OPTIONAL,
176
+ # publicKey [1] BIT STRING OPTIONAL
177
+ # }
178
+
179
+ sequence = OpenSSL::ASN1::Sequence([
180
+ OpenSSL::ASN1::Integer(1),
181
+ OpenSSL::ASN1::OctetString(OpenSSL::BN.new(decode_octets(jwk_d), 2).to_s(2)),
182
+ OpenSSL::ASN1::ObjectId(point.group.curve_name, 0, :EXPLICIT),
183
+ OpenSSL::ASN1::BitString(point.to_octet_string(:uncompressed), 1, :EXPLICIT)
184
+ ])
177
185
  OpenSSL::PKey::EC.new(sequence.to_der)
178
186
  end
179
187
  else
180
188
  def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d)
181
- curve = EC.to_openssl_curve(jwk_crv)
182
-
183
- x_octets = decode_octets(jwk_x)
184
- y_octets = decode_octets(jwk_y)
185
-
186
- key = OpenSSL::PKey::EC.new(curve)
187
-
188
- # The details of the `Point` instantiation are covered in:
189
- # - https://docs.ruby-lang.org/en/2.4.0/OpenSSL/PKey/EC.html
190
- # - https://www.openssl.org/docs/manmaster/man3/EC_POINT_new.html
191
- # - https://tools.ietf.org/html/rfc5480#section-2.2
192
- # - https://www.secg.org/SEC1-Ver-1.0.pdf
193
- # Section 2.3.3 of the last of these references specifies that the
194
- # encoding of an uncompressed point consists of the byte `0x04` followed
195
- # by the x value then the y value.
196
- point = OpenSSL::PKey::EC::Point.new(
197
- OpenSSL::PKey::EC::Group.new(curve),
198
- OpenSSL::BN.new([0x04, x_octets, y_octets].pack('Ca*a*'), 2)
199
- )
200
-
201
- key.public_key = point
202
- key.private_key = OpenSSL::BN.new(decode_octets(jwk_d), 2) if jwk_d
203
-
204
- key
189
+ point = create_point(jwk_crv, jwk_x, jwk_y)
190
+
191
+ ::JWT::JWA::Ecdsa.create_public_key_from_point(point).tap do |key|
192
+ key.private_key = OpenSSL::BN.new(decode_octets(jwk_d), 2) if jwk_d
193
+ end
205
194
  end
206
195
  end
207
196
 
@@ -210,7 +199,7 @@ module JWT
210
199
  # Some base64 encoders on some platform omit a single 0-byte at
211
200
  # the start of either Y or X coordinate of the elliptic curve point.
212
201
  # This leads to an encoding error when data is passed to OpenSSL BN.
213
- # It is know to have happend to exported JWKs on a Java application and
202
+ # It is know to have happened to exported JWKs on a Java application and
214
203
  # on a Flutter/Dart application (both iOS and Android). All that is
215
204
  # needed to fix the problem is adding a leading 0-byte. We know the
216
205
  # required byte is 0 because with any other byte the point is no longer