jwt 1.5.6 → 2.7.0

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