jwt 1.5.6 → 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.
Files changed (76) hide show
  1. checksums.yaml +5 -5
  2. data/AUTHORS +119 -0
  3. data/CHANGELOG.md +457 -8
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/CONTRIBUTING.md +99 -0
  6. data/README.md +399 -78
  7. data/lib/jwt/algos/algo_wrapper.rb +30 -0
  8. data/lib/jwt/algos/ecdsa.rb +62 -0
  9. data/lib/jwt/algos/eddsa.rb +33 -0
  10. data/lib/jwt/algos/hmac.rb +73 -0
  11. data/lib/jwt/algos/hmac_rbnacl.rb +53 -0
  12. data/lib/jwt/algos/hmac_rbnacl_fixed.rb +52 -0
  13. data/lib/jwt/algos/none.rb +19 -0
  14. data/lib/jwt/algos/ps.rb +41 -0
  15. data/lib/jwt/algos/rsa.rb +21 -0
  16. data/lib/jwt/algos/unsupported.rb +19 -0
  17. data/lib/jwt/algos.rb +67 -0
  18. data/lib/jwt/base64.rb +19 -0
  19. data/lib/jwt/claims_validator.rb +37 -0
  20. data/lib/jwt/configuration/container.rb +21 -0
  21. data/lib/jwt/configuration/decode_configuration.rb +46 -0
  22. data/lib/jwt/configuration/jwk_configuration.rb +27 -0
  23. data/lib/jwt/configuration.rb +15 -0
  24. data/lib/jwt/decode.rb +140 -29
  25. data/lib/jwt/encode.rb +79 -0
  26. data/lib/jwt/error.rb +8 -0
  27. data/lib/jwt/json.rb +10 -9
  28. data/lib/jwt/jwk/ec.rb +236 -0
  29. data/lib/jwt/jwk/hmac.rb +103 -0
  30. data/lib/jwt/jwk/key_base.rb +55 -0
  31. data/lib/jwt/jwk/key_finder.rb +46 -0
  32. data/lib/jwt/jwk/kid_as_key_digest.rb +15 -0
  33. data/lib/jwt/jwk/okp_rbnacl.rb +110 -0
  34. data/lib/jwt/jwk/rsa.rb +203 -0
  35. data/lib/jwt/jwk/set.rb +80 -0
  36. data/lib/jwt/jwk/thumbprint.rb +26 -0
  37. data/lib/jwt/jwk.rb +55 -0
  38. data/lib/jwt/security_utils.rb +32 -0
  39. data/lib/jwt/verify.rb +58 -51
  40. data/lib/jwt/version.rb +24 -4
  41. data/lib/jwt/x5c_key_finder.rb +55 -0
  42. data/lib/jwt.rb +15 -176
  43. data/ruby-jwt.gemspec +19 -10
  44. metadata +51 -101
  45. data/.codeclimate.yml +0 -20
  46. data/.gitignore +0 -11
  47. data/.rspec +0 -1
  48. data/.rubocop.yml +0 -2
  49. data/.travis.yml +0 -13
  50. data/Gemfile +0 -4
  51. data/Manifest +0 -8
  52. data/Rakefile +0 -11
  53. data/spec/fixtures/certs/ec256-private.pem +0 -8
  54. data/spec/fixtures/certs/ec256-public.pem +0 -4
  55. data/spec/fixtures/certs/ec256-wrong-private.pem +0 -8
  56. data/spec/fixtures/certs/ec256-wrong-public.pem +0 -4
  57. data/spec/fixtures/certs/ec384-private.pem +0 -9
  58. data/spec/fixtures/certs/ec384-public.pem +0 -5
  59. data/spec/fixtures/certs/ec384-wrong-private.pem +0 -9
  60. data/spec/fixtures/certs/ec384-wrong-public.pem +0 -5
  61. data/spec/fixtures/certs/ec512-private.pem +0 -10
  62. data/spec/fixtures/certs/ec512-public.pem +0 -6
  63. data/spec/fixtures/certs/ec512-wrong-private.pem +0 -10
  64. data/spec/fixtures/certs/ec512-wrong-public.pem +0 -6
  65. data/spec/fixtures/certs/rsa-1024-private.pem +0 -15
  66. data/spec/fixtures/certs/rsa-1024-public.pem +0 -6
  67. data/spec/fixtures/certs/rsa-2048-private.pem +0 -27
  68. data/spec/fixtures/certs/rsa-2048-public.pem +0 -9
  69. data/spec/fixtures/certs/rsa-2048-wrong-private.pem +0 -27
  70. data/spec/fixtures/certs/rsa-2048-wrong-public.pem +0 -9
  71. data/spec/fixtures/certs/rsa-4096-private.pem +0 -51
  72. data/spec/fixtures/certs/rsa-4096-public.pem +0 -14
  73. data/spec/integration/readme_examples_spec.rb +0 -190
  74. data/spec/jwt/verify_spec.rb +0 -197
  75. data/spec/jwt_spec.rb +0 -240
  76. data/spec/spec_helper.rb +0 -31
data/lib/jwt/decode.rb CHANGED
@@ -1,57 +1,168 @@
1
1
  # frozen_string_literal: true
2
- require 'jwt/json'
2
+
3
+ require 'json'
4
+
3
5
  require 'jwt/verify'
6
+ require 'jwt/x5c_key_finder'
4
7
 
5
8
  # JWT::Decode module
6
9
  module JWT
7
- extend JWT::Json
8
-
9
10
  # Decoding logic for JWT
10
11
  class Decode
11
- attr_reader :header, :payload, :signature
12
-
13
12
  def initialize(jwt, key, verify, options, &keyfinder)
13
+ raise(JWT::DecodeError, 'Nil JSON web token') unless jwt
14
+
14
15
  @jwt = jwt
15
16
  @key = key
16
- @verify = verify
17
17
  @options = options
18
+ @segments = jwt.split('.')
19
+ @verify = verify
20
+ @signature = ''
18
21
  @keyfinder = keyfinder
19
22
  end
20
23
 
21
24
  def decode_segments
22
- header_segment, payload_segment, crypto_segment = raw_segments(@jwt, @verify)
23
- @header, @payload = decode_header_and_payload(header_segment, payload_segment)
24
- @signature = Decode.base64url_decode(crypto_segment.to_s) if @verify
25
- signing_input = [header_segment, payload_segment].join('.')
26
- [@header, @payload, @signature, signing_input]
25
+ validate_segment_count!
26
+ if @verify
27
+ decode_signature
28
+ verify_algo
29
+ set_key
30
+ verify_signature
31
+ verify_claims
32
+ end
33
+ raise(JWT::DecodeError, 'Not enough or too many segments') unless header && payload
34
+
35
+ [payload, header]
27
36
  end
28
37
 
29
- def raw_segments(jwt, verify)
30
- segments = jwt.split('.')
31
- required_num_segments = verify ? [3] : [2, 3]
32
- raise(JWT::DecodeError, 'Not enough or too many segments') unless required_num_segments.include? segments.length
33
- segments
38
+ private
39
+
40
+ def verify_signature
41
+ return unless @key || @verify
42
+
43
+ return if none_algorithm?
44
+
45
+ raise JWT::DecodeError, 'No verification key available' unless @key
46
+
47
+ return if Array(@key).any? { |key| verify_signature_for?(key) }
48
+
49
+ raise(JWT::VerificationError, 'Signature verification failed')
34
50
  end
35
- private :raw_segments
36
51
 
37
- def decode_header_and_payload(header_segment, payload_segment)
38
- header = JWT.decode_json(Decode.base64url_decode(header_segment))
39
- payload = JWT.decode_json(Decode.base64url_decode(payload_segment))
40
- [header, payload]
52
+ def verify_algo
53
+ raise(JWT::IncorrectAlgorithm, 'An algorithm must be specified') if allowed_algorithms.empty?
54
+ raise(JWT::IncorrectAlgorithm, 'Token is missing alg header') unless alg_in_header
55
+ raise(JWT::IncorrectAlgorithm, 'Expected a different algorithm') if allowed_and_valid_algorithms.empty?
41
56
  end
42
- private :decode_header_and_payload
43
57
 
44
- def self.base64url_decode(str)
45
- str += '=' * (4 - str.length.modulo(4))
46
- Base64.decode64(str.tr('-_', '+/'))
58
+ def set_key
59
+ @key = find_key(&@keyfinder) if @keyfinder
60
+ @key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks], allow_nil_kid: @options[:allow_nil_kid]).key_for(header['kid']) if @options[:jwks]
61
+ if (x5c_options = @options[:x5c])
62
+ @key = X5cKeyFinder.new(x5c_options[:root_certificates], x5c_options[:crls]).from(header['x5c'])
63
+ end
47
64
  end
48
65
 
49
- def verify
50
- @options.each do |key, val|
51
- next unless key.to_s =~ /verify/
66
+ def verify_signature_for?(key)
67
+ allowed_and_valid_algorithms.any? do |alg|
68
+ alg.verify(data: signing_input, signature: @signature, verification_key: key)
69
+ end
70
+ end
52
71
 
53
- Verify.send(key, payload, @options) if val
72
+ def allowed_and_valid_algorithms
73
+ @allowed_and_valid_algorithms ||= allowed_algorithms.select { |alg| alg.valid_alg?(alg_in_header) }
74
+ end
75
+
76
+ # Order is very important - first check for string keys, next for symbols
77
+ ALGORITHM_KEYS = ['algorithm',
78
+ :algorithm,
79
+ 'algorithms',
80
+ :algorithms].freeze
81
+
82
+ def given_algorithms
83
+ ALGORITHM_KEYS.each do |alg_key|
84
+ alg = @options[alg_key]
85
+ return Array(alg) if alg
54
86
  end
87
+ []
88
+ end
89
+
90
+ def allowed_algorithms
91
+ @allowed_algorithms ||= resolve_allowed_algorithms
92
+ end
93
+
94
+ def resolve_allowed_algorithms
95
+ algs = given_algorithms.map do |alg|
96
+ if Algos.implementation?(alg)
97
+ alg
98
+ else
99
+ Algos.create(alg)
100
+ end
101
+ end
102
+
103
+ sort_by_alg_header(algs)
104
+ end
105
+
106
+ # Move algorithms matching the JWT alg header to the beginning of the list
107
+ def sort_by_alg_header(algs)
108
+ return algs if algs.size <= 1
109
+
110
+ algs.partition { |alg| alg.valid_alg?(alg_in_header) }.flatten
111
+ end
112
+
113
+ def find_key(&keyfinder)
114
+ key = (keyfinder.arity == 2 ? yield(header, payload) : yield(header))
115
+ # key can be of type [string, nil, OpenSSL::PKey, Array]
116
+ return key if key && !Array(key).empty?
117
+
118
+ raise JWT::DecodeError, 'No verification key available'
119
+ end
120
+
121
+ def verify_claims
122
+ Verify.verify_claims(payload, @options)
123
+ Verify.verify_required_claims(payload, @options)
124
+ end
125
+
126
+ def validate_segment_count!
127
+ return if segment_length == 3
128
+ return if !@verify && segment_length == 2 # If no verifying required, the signature is not needed
129
+ return if segment_length == 2 && none_algorithm?
130
+
131
+ raise(JWT::DecodeError, 'Not enough or too many segments')
132
+ end
133
+
134
+ def segment_length
135
+ @segments.count
136
+ end
137
+
138
+ def none_algorithm?
139
+ alg_in_header == 'none'
140
+ end
141
+
142
+ def decode_signature
143
+ @signature = ::JWT::Base64.url_decode(@segments[2] || '')
144
+ end
145
+
146
+ def alg_in_header
147
+ header['alg']
148
+ end
149
+
150
+ def header
151
+ @header ||= parse_and_decode @segments[0]
152
+ end
153
+
154
+ def payload
155
+ @payload ||= parse_and_decode @segments[1]
156
+ end
157
+
158
+ def signing_input
159
+ @segments.first(2).join('.')
160
+ end
161
+
162
+ def parse_and_decode(segment)
163
+ JWT::JSON.parse(::JWT::Base64.url_decode(segment))
164
+ rescue ::JSON::ParserError
165
+ raise JWT::DecodeError, 'Invalid segment encoding'
55
166
  end
56
167
  end
57
168
  end
data/lib/jwt/encode.rb ADDED
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'algos'
4
+ require_relative 'claims_validator'
5
+
6
+ # JWT::Encode module
7
+ module JWT
8
+ # Encoding logic for JWT
9
+ class Encode
10
+ ALG_KEY = 'alg'
11
+
12
+ def initialize(options)
13
+ @payload = options[:payload]
14
+ @key = options[:key]
15
+ @algorithm = resolve_algorithm(options[:algorithm])
16
+ @headers = options[:headers].transform_keys(&:to_s)
17
+ @headers[ALG_KEY] = @algorithm.alg
18
+ end
19
+
20
+ def segments
21
+ validate_claims!
22
+ combine(encoded_header_and_payload, encoded_signature)
23
+ end
24
+
25
+ private
26
+
27
+ def resolve_algorithm(algorithm)
28
+ return algorithm if Algos.implementation?(algorithm)
29
+
30
+ Algos.create(algorithm)
31
+ end
32
+
33
+ def encoded_header
34
+ @encoded_header ||= encode_header
35
+ end
36
+
37
+ def encoded_payload
38
+ @encoded_payload ||= encode_payload
39
+ end
40
+
41
+ def encoded_signature
42
+ @encoded_signature ||= encode_signature
43
+ end
44
+
45
+ def encoded_header_and_payload
46
+ @encoded_header_and_payload ||= combine(encoded_header, encoded_payload)
47
+ end
48
+
49
+ def encode_header
50
+ encode_data(@headers)
51
+ end
52
+
53
+ def encode_payload
54
+ encode_data(@payload)
55
+ end
56
+
57
+ def signature
58
+ @algorithm.sign(data: encoded_header_and_payload, signing_key: @key)
59
+ end
60
+
61
+ def validate_claims!
62
+ return unless @payload.is_a?(Hash)
63
+
64
+ ClaimsValidator.new(@payload).validate!
65
+ end
66
+
67
+ def encode_signature
68
+ ::JWT::Base64.url_encode(signature)
69
+ end
70
+
71
+ def encode_data(data)
72
+ ::JWT::Base64.url_encode(JWT::JSON.generate(data))
73
+ end
74
+
75
+ def combine(*parts)
76
+ parts.join('.')
77
+ end
78
+ end
79
+ end
data/lib/jwt/error.rb CHANGED
@@ -1,14 +1,22 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module JWT
4
+ class EncodeError < StandardError; end
3
5
  class DecodeError < StandardError; end
6
+ class RequiredDependencyError < StandardError; end
7
+
4
8
  class VerificationError < DecodeError; end
5
9
  class ExpiredSignature < DecodeError; end
6
10
  class IncorrectAlgorithm < DecodeError; end
7
11
  class ImmatureSignature < DecodeError; end
8
12
  class InvalidIssuerError < DecodeError; end
13
+ class UnsupportedEcdsaCurve < IncorrectAlgorithm; end
9
14
  class InvalidIatError < DecodeError; end
10
15
  class InvalidAudError < DecodeError; end
11
16
  class InvalidSubError < DecodeError; end
12
17
  class InvalidJtiError < DecodeError; end
13
18
  class InvalidPayload < DecodeError; end
19
+ class MissingRequiredClaim < DecodeError; end
20
+
21
+ class JWKError < DecodeError; end
14
22
  end
data/lib/jwt/json.rb CHANGED
@@ -1,17 +1,18 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'json'
3
4
 
4
5
  module JWT
5
- # JSON fallback implementation or ruby 1.8.x
6
- module Json
7
- def decode_json(encoded)
8
- JSON.parse(encoded)
9
- rescue JSON::ParserError
10
- raise JWT::DecodeError, 'Invalid segment encoding'
11
- end
6
+ # JSON wrapper
7
+ class JSON
8
+ class << self
9
+ def generate(data)
10
+ ::JSON.generate(data)
11
+ end
12
12
 
13
- def encode_json(raw)
14
- JSON.generate(raw)
13
+ def parse(data)
14
+ ::JSON.parse(data)
15
+ end
15
16
  end
16
17
  end
17
18
  end
data/lib/jwt/jwk/ec.rb ADDED
@@ -0,0 +1,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module JWT
6
+ module JWK
7
+ class EC < KeyBase # rubocop:disable Metrics/ClassLength
8
+ KTY = 'EC'
9
+ KTYS = [KTY, OpenSSL::PKey::EC, JWT::JWK::EC].freeze
10
+ BINARY = 2
11
+ EC_PUBLIC_KEY_ELEMENTS = %i[kty crv x y].freeze
12
+ EC_PRIVATE_KEY_ELEMENTS = %i[d].freeze
13
+ EC_KEY_ELEMENTS = (EC_PRIVATE_KEY_ELEMENTS + EC_PUBLIC_KEY_ELEMENTS).freeze
14
+
15
+ def initialize(key, params = nil, options = {})
16
+ params ||= {}
17
+
18
+ # For backwards compatibility when kid was a String
19
+ params = { kid: params } if params.is_a?(String)
20
+
21
+ key_params = extract_key_params(key)
22
+
23
+ params = params.transform_keys(&:to_sym)
24
+ check_jwk_params!(key_params, params)
25
+
26
+ super(options, key_params.merge(params))
27
+ end
28
+
29
+ def keypair
30
+ ec_key
31
+ end
32
+
33
+ def private?
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
47
+ end
48
+
49
+ def members
50
+ EC_PUBLIC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
51
+ end
52
+
53
+ def export(options = {})
54
+ exported = parameters.clone
55
+ exported.reject! { |k, _| EC_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true
56
+ exported
57
+ end
58
+
59
+ def key_digest
60
+ _crv, x_octets, y_octets = keypair_components(ec_key)
61
+ sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(x_octets, BINARY)),
62
+ OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(y_octets, BINARY))])
63
+ OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
64
+ end
65
+
66
+ def []=(key, value)
67
+ if EC_KEY_ELEMENTS.include?(key.to_sym)
68
+ raise ArgumentError, 'cannot overwrite cryptographic key attributes'
69
+ end
70
+
71
+ super(key, value)
72
+ end
73
+
74
+ private
75
+
76
+ def ec_key
77
+ @ec_key ||= create_ec_key(self[:crv], self[:x], self[:y], self[:d])
78
+ end
79
+
80
+ def extract_key_params(key)
81
+ case key
82
+ when JWT::JWK::EC
83
+ key.export(include_private: true)
84
+ when OpenSSL::PKey::EC # Accept OpenSSL key as input
85
+ @ec_key = key # Preserve the object to avoid recreation
86
+ parse_ec_key(key)
87
+ when Hash
88
+ key.transform_keys(&:to_sym)
89
+ else
90
+ raise ArgumentError, 'key must be of type OpenSSL::PKey::EC or Hash with key parameters'
91
+ end
92
+ end
93
+
94
+ def check_jwk_params!(key_params, params)
95
+ raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (EC_KEY_ELEMENTS & params.keys).empty?
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]
98
+ end
99
+
100
+ def keypair_components(ec_keypair)
101
+ encoded_point = ec_keypair.public_key.to_bn.to_s(BINARY)
102
+ case ec_keypair.group.curve_name
103
+ when 'prime256v1'
104
+ crv = 'P-256'
105
+ x_octets, y_octets = encoded_point.unpack('xa32a32')
106
+ when 'secp256k1'
107
+ crv = 'P-256K'
108
+ x_octets, y_octets = encoded_point.unpack('xa32a32')
109
+ when 'secp384r1'
110
+ crv = 'P-384'
111
+ x_octets, y_octets = encoded_point.unpack('xa48a48')
112
+ when 'secp521r1'
113
+ crv = 'P-521'
114
+ x_octets, y_octets = encoded_point.unpack('xa66a66')
115
+ else
116
+ raise JWT::JWKError, "Unsupported curve '#{ec_keypair.group.curve_name}'"
117
+ end
118
+ [crv, x_octets, y_octets]
119
+ end
120
+
121
+ def encode_octets(octets)
122
+ return unless octets
123
+
124
+ ::JWT::Base64.url_encode(octets)
125
+ end
126
+
127
+ def encode_open_ssl_bn(key_part)
128
+ ::JWT::Base64.url_encode(key_part.to_s(BINARY))
129
+ end
130
+
131
+ def parse_ec_key(key)
132
+ crv, x_octets, y_octets = keypair_components(key)
133
+ octets = key.private_key&.to_bn&.to_s(BINARY)
134
+ {
135
+ kty: KTY,
136
+ crv: crv,
137
+ x: encode_octets(x_octets),
138
+ y: encode_octets(y_octets),
139
+ d: encode_octets(octets)
140
+ }.compact
141
+ end
142
+
143
+ if ::JWT.openssl_3?
144
+ def create_ec_key(jwk_crv, jwk_x, jwk_y, jwk_d) # rubocop:disable Metrics/MethodLength
145
+ curve = EC.to_openssl_curve(jwk_crv)
146
+
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
176
+
177
+ OpenSSL::PKey::EC.new(sequence.to_der)
178
+ end
179
+ else
180
+ 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
205
+ end
206
+ end
207
+
208
+ def decode_octets(jwk_data)
209
+ ::JWT::Base64.url_decode(jwk_data)
210
+ end
211
+
212
+ def decode_open_ssl_bn(jwk_data)
213
+ OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY)
214
+ end
215
+
216
+ class << self
217
+ def import(jwk_data)
218
+ new(jwk_data)
219
+ end
220
+
221
+ def to_openssl_curve(crv)
222
+ # The JWK specs and OpenSSL use different names for the same curves.
223
+ # See https://tools.ietf.org/html/rfc5480#section-2.1.1.1 for some
224
+ # pointers on different names for common curves.
225
+ case crv
226
+ when 'P-256' then 'prime256v1'
227
+ when 'P-384' then 'secp384r1'
228
+ when 'P-521' then 'secp521r1'
229
+ when 'P-256K' then 'secp256k1'
230
+ else raise JWT::JWKError, 'Invalid curve provided'
231
+ end
232
+ end
233
+ end
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWK
5
+ class HMAC < KeyBase
6
+ KTY = 'oct'
7
+ KTYS = [KTY, String, JWT::JWK::HMAC].freeze
8
+ HMAC_PUBLIC_KEY_ELEMENTS = %i[kty].freeze
9
+ HMAC_PRIVATE_KEY_ELEMENTS = %i[k].freeze
10
+ HMAC_KEY_ELEMENTS = (HMAC_PRIVATE_KEY_ELEMENTS + HMAC_PUBLIC_KEY_ELEMENTS).freeze
11
+
12
+ def initialize(key, params = nil, options = {})
13
+ params ||= {}
14
+
15
+ # For backwards compatibility when kid was a String
16
+ params = { kid: params } if params.is_a?(String)
17
+
18
+ key_params = extract_key_params(key)
19
+
20
+ params = params.transform_keys(&:to_sym)
21
+ check_jwk(key_params, params)
22
+
23
+ super(options, key_params.merge(params))
24
+ end
25
+
26
+ def keypair
27
+ secret
28
+ end
29
+
30
+ def private?
31
+ true
32
+ end
33
+
34
+ def public_key
35
+ nil
36
+ end
37
+
38
+ def verify_key
39
+ secret
40
+ end
41
+
42
+ def signing_key
43
+ secret
44
+ end
45
+
46
+ # See https://tools.ietf.org/html/rfc7517#appendix-A.3
47
+ def export(options = {})
48
+ exported = parameters.clone
49
+ exported.reject! { |k, _| HMAC_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true
50
+ exported
51
+ end
52
+
53
+ def members
54
+ HMAC_KEY_ELEMENTS.each_with_object({}) { |i, h| h[i] = self[i] }
55
+ end
56
+
57
+ def key_digest
58
+ sequence = OpenSSL::ASN1::Sequence([OpenSSL::ASN1::UTF8String.new(signing_key),
59
+ OpenSSL::ASN1::UTF8String.new(KTY)])
60
+ OpenSSL::Digest::SHA256.hexdigest(sequence.to_der)
61
+ end
62
+
63
+ def []=(key, value)
64
+ if HMAC_KEY_ELEMENTS.include?(key.to_sym)
65
+ raise ArgumentError, 'cannot overwrite cryptographic key attributes'
66
+ end
67
+
68
+ super(key, value)
69
+ end
70
+
71
+ private
72
+
73
+ def secret
74
+ self[:k]
75
+ end
76
+
77
+ def extract_key_params(key)
78
+ case key
79
+ when JWT::JWK::HMAC
80
+ key.export(include_private: true)
81
+ when String # Accept String key as input
82
+ { kty: KTY, k: key }
83
+ when Hash
84
+ key.transform_keys(&:to_sym)
85
+ else
86
+ raise ArgumentError, 'key must be of type String or Hash with key parameters'
87
+ end
88
+ end
89
+
90
+ def check_jwk(keypair, params)
91
+ raise ArgumentError, 'cannot overwrite cryptographic key attributes' unless (HMAC_KEY_ELEMENTS & params.keys).empty?
92
+ raise JWT::JWKError, "Incorrect 'kty' value: #{keypair[:kty]}, expected #{KTY}" unless keypair[:kty] == KTY
93
+ raise JWT::JWKError, 'Key format is invalid for HMAC' unless keypair[:k]
94
+ end
95
+
96
+ class << self
97
+ def import(jwk_data)
98
+ new(jwk_data)
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end