jwt 2.1.0 → 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 (80) hide show
  1. checksums.yaml +5 -5
  2. data/AUTHORS +119 -0
  3. data/CHANGELOG.md +355 -19
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/CONTRIBUTING.md +99 -0
  6. data/README.md +331 -102
  7. data/lib/jwt/algos/algo_wrapper.rb +30 -0
  8. data/lib/jwt/algos/ecdsa.rb +39 -12
  9. data/lib/jwt/algos/eddsa.rb +18 -8
  10. data/lib/jwt/algos/hmac.rb +57 -17
  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 +7 -5
  16. data/lib/jwt/algos/unsupported.rb +7 -4
  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 +143 -24
  25. data/lib/jwt/encode.rb +54 -26
  26. data/lib/jwt/error.rb +6 -0
  27. data/lib/jwt/json.rb +18 -0
  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 +8 -27
  39. data/lib/jwt/verify.rb +19 -8
  40. data/lib/jwt/version.rb +22 -2
  41. data/lib/jwt/x5c_key_finder.rb +55 -0
  42. data/lib/jwt.rb +12 -44
  43. data/ruby-jwt.gemspec +18 -10
  44. metadata +45 -118
  45. data/.codeclimate.yml +0 -20
  46. data/.ebert.yml +0 -18
  47. data/.gitignore +0 -11
  48. data/.reek.yml +0 -40
  49. data/.rspec +0 -1
  50. data/.rubocop.yml +0 -98
  51. data/.travis.yml +0 -14
  52. data/Gemfile +0 -3
  53. data/Manifest +0 -8
  54. data/Rakefile +0 -11
  55. data/lib/jwt/default_options.rb +0 -15
  56. data/lib/jwt/signature.rb +0 -50
  57. data/spec/fixtures/certs/ec256-private.pem +0 -8
  58. data/spec/fixtures/certs/ec256-public.pem +0 -4
  59. data/spec/fixtures/certs/ec256-wrong-private.pem +0 -8
  60. data/spec/fixtures/certs/ec256-wrong-public.pem +0 -4
  61. data/spec/fixtures/certs/ec384-private.pem +0 -9
  62. data/spec/fixtures/certs/ec384-public.pem +0 -5
  63. data/spec/fixtures/certs/ec384-wrong-private.pem +0 -9
  64. data/spec/fixtures/certs/ec384-wrong-public.pem +0 -5
  65. data/spec/fixtures/certs/ec512-private.pem +0 -10
  66. data/spec/fixtures/certs/ec512-public.pem +0 -6
  67. data/spec/fixtures/certs/ec512-wrong-private.pem +0 -10
  68. data/spec/fixtures/certs/ec512-wrong-public.pem +0 -6
  69. data/spec/fixtures/certs/rsa-1024-private.pem +0 -15
  70. data/spec/fixtures/certs/rsa-1024-public.pem +0 -6
  71. data/spec/fixtures/certs/rsa-2048-private.pem +0 -27
  72. data/spec/fixtures/certs/rsa-2048-public.pem +0 -9
  73. data/spec/fixtures/certs/rsa-2048-wrong-private.pem +0 -27
  74. data/spec/fixtures/certs/rsa-2048-wrong-public.pem +0 -9
  75. data/spec/fixtures/certs/rsa-4096-private.pem +0 -51
  76. data/spec/fixtures/certs/rsa-4096-public.pem +0 -14
  77. data/spec/integration/readme_examples_spec.rb +0 -202
  78. data/spec/jwt/verify_spec.rb +0 -232
  79. data/spec/jwt_spec.rb +0 -315
  80. data/spec/spec_helper.rb +0 -28
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'configuration/container'
4
+
5
+ module JWT
6
+ module Configuration
7
+ def configure
8
+ yield(configuration)
9
+ end
10
+
11
+ def configuration
12
+ @configuration ||= ::JWT::Configuration::Container.new
13
+ end
14
+ end
15
+ end
data/lib/jwt/decode.rb CHANGED
@@ -2,47 +2,166 @@
2
2
 
3
3
  require 'json'
4
4
 
5
+ require 'jwt/verify'
6
+ require 'jwt/x5c_key_finder'
7
+
5
8
  # JWT::Decode module
6
9
  module JWT
7
10
  # Decoding logic for JWT
8
11
  class Decode
9
- attr_reader :header, :payload, :signature
10
-
11
- def self.base64url_decode(str)
12
- str += '=' * (4 - str.length.modulo(4))
13
- Base64.decode64(str.tr('-_', '+/'))
14
- end
12
+ def initialize(jwt, key, verify, options, &keyfinder)
13
+ raise(JWT::DecodeError, 'Nil JSON web token') unless jwt
15
14
 
16
- def initialize(jwt, verify)
17
15
  @jwt = jwt
16
+ @key = key
17
+ @options = options
18
+ @segments = jwt.split('.')
18
19
  @verify = verify
19
- @header = ''
20
- @payload = ''
21
20
  @signature = ''
21
+ @keyfinder = keyfinder
22
22
  end
23
23
 
24
24
  def decode_segments
25
- header_segment, payload_segment, crypto_segment = raw_segments
26
- @header, @payload = decode_header_and_payload(header_segment, payload_segment)
27
- @signature = Decode.base64url_decode(crypto_segment.to_s) if @verify
28
- signing_input = [header_segment, payload_segment].join('.')
29
- [@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]
30
36
  end
31
37
 
32
38
  private
33
39
 
34
- def raw_segments
35
- segments = @jwt.split('.')
36
- required_num_segments = @verify ? [3] : [2, 3]
37
- raise(JWT::DecodeError, 'Not enough or too many segments') unless required_num_segments.include? segments.length
38
- segments
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')
50
+ end
51
+
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?
56
+ end
57
+
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
64
+ end
65
+
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
71
+
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
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('.')
39
160
  end
40
161
 
41
- def decode_header_and_payload(header_segment, payload_segment)
42
- header = JSON.parse(Decode.base64url_decode(header_segment))
43
- payload = JSON.parse(Decode.base64url_decode(payload_segment))
44
- [header, payload]
45
- rescue JSON::ParserError
162
+ def parse_and_decode(segment)
163
+ JWT::JSON.parse(::JWT::Base64.url_decode(segment))
164
+ rescue ::JSON::ParserError
46
165
  raise JWT::DecodeError, 'Invalid segment encoding'
47
166
  end
48
167
  end
data/lib/jwt/encode.rb CHANGED
@@ -1,51 +1,79 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
3
+ require_relative 'algos'
4
+ require_relative 'claims_validator'
4
5
 
5
6
  # JWT::Encode module
6
7
  module JWT
7
8
  # Encoding logic for JWT
8
9
  class Encode
9
- attr_reader :payload, :key, :algorithm, :header_fields, :segments
10
+ ALG_KEY = 'alg'
10
11
 
11
- def self.base64url_encode(str)
12
- Base64.encode64(str).tr('+/', '-_').gsub(/[\n=]/, '')
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
13
18
  end
14
19
 
15
- def initialize(payload, key, algorithm, header_fields)
16
- @payload = payload
17
- @key = key
18
- @algorithm = algorithm
19
- @header_fields = header_fields
20
- @segments = encode_segments
20
+ def segments
21
+ validate_claims!
22
+ combine(encoded_header_and_payload, encoded_signature)
21
23
  end
22
24
 
23
25
  private
24
26
 
27
+ def resolve_algorithm(algorithm)
28
+ return algorithm if Algos.implementation?(algorithm)
29
+
30
+ Algos.create(algorithm)
31
+ end
32
+
25
33
  def encoded_header
26
- header = { 'alg' => @algorithm }.merge(@header_fields)
27
- Encode.base64url_encode(JSON.generate(header))
34
+ @encoded_header ||= encode_header
28
35
  end
29
36
 
30
37
  def encoded_payload
31
- raise InvalidPayload, 'exp claim must be an integer' if @payload && @payload.is_a?(Hash) && @payload.key?('exp') && !@payload['exp'].is_a?(Integer)
32
- Encode.base64url_encode(JSON.generate(@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)
33
69
  end
34
70
 
35
- def encoded_signature(signing_input)
36
- if @algorithm == 'none'
37
- ''
38
- else
39
- signature = JWT::Signature.sign(@algorithm, signing_input, @key)
40
- Encode.base64url_encode(signature)
41
- end
71
+ def encode_data(data)
72
+ ::JWT::Base64.url_encode(JWT::JSON.generate(data))
42
73
  end
43
74
 
44
- def encode_segments
45
- header = encoded_header
46
- payload = encoded_payload
47
- signature = encoded_signature([header, payload].join('.'))
48
- [header, payload, signature].join('.')
75
+ def combine(*parts)
76
+ parts.join('.')
49
77
  end
50
78
  end
51
79
  end
data/lib/jwt/error.rb CHANGED
@@ -3,14 +3,20 @@
3
3
  module JWT
4
4
  class EncodeError < StandardError; end
5
5
  class DecodeError < StandardError; end
6
+ class RequiredDependencyError < StandardError; end
7
+
6
8
  class VerificationError < DecodeError; end
7
9
  class ExpiredSignature < DecodeError; end
8
10
  class IncorrectAlgorithm < DecodeError; end
9
11
  class ImmatureSignature < DecodeError; end
10
12
  class InvalidIssuerError < DecodeError; end
13
+ class UnsupportedEcdsaCurve < IncorrectAlgorithm; end
11
14
  class InvalidIatError < DecodeError; end
12
15
  class InvalidAudError < DecodeError; end
13
16
  class InvalidSubError < DecodeError; end
14
17
  class InvalidJtiError < DecodeError; end
15
18
  class InvalidPayload < DecodeError; end
19
+ class MissingRequiredClaim < DecodeError; end
20
+
21
+ class JWKError < DecodeError; end
16
22
  end
data/lib/jwt/json.rb ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module JWT
6
+ # JSON wrapper
7
+ class JSON
8
+ class << self
9
+ def generate(data)
10
+ ::JSON.generate(data)
11
+ end
12
+
13
+ def parse(data)
14
+ ::JSON.parse(data)
15
+ end
16
+ end
17
+ end
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