Skip to content

Commit 323ffc1

Browse files
committed
Handle x5t using JWK key finder
1 parent e1645c5 commit 323ffc1

File tree

7 files changed

+58
-193
lines changed

7 files changed

+58
-193
lines changed

lib/jwt/base64.rb

+6
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ def url_encode(str)
1313
::Base64.urlsafe_encode64(str, padding: false)
1414
end
1515

16+
# Encode a string with Base64 complying with RFC 4648 (padded).
17+
# @api private
18+
def strict_encode(str)
19+
::Base64.strict_encode64(str)
20+
end
21+
1622
# Decode a string with URL-safe Base64 complying with RFC 4648.
1723
# @api private
1824
def url_decode(str)

lib/jwt/decode.rb

+4-7
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
require 'json'
44
require 'jwt/x5c_key_finder'
5-
require 'jwt/x5t_key_finder'
65

76
module JWT
87
# The Decode class is responsible for decoding and verifying JWT tokens.
@@ -61,13 +60,11 @@ def verify_algo
6160

6261
def set_key
6362
@key = find_key(&@keyfinder) if @keyfinder
64-
@key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks], allow_nil_kid: @options[:allow_nil_kid]).key_for(token.header['kid']) if @options[:jwks]
63+
@key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks], allow_nil_kid: @options[:allow_nil_kid]).call(token) if @options[:jwks]
6564

66-
if (x5c_options = @options[:x5c])
67-
@key = X5cKeyFinder.new(x5c_options[:root_certificates], x5c_options[:crls]).from(token.header['x5c'])
68-
elsif (x5t_options = @options[:x5t])
69-
@key = X5tKeyFinder.new(x5t_options[:certificates]).from(token.header)
70-
end
65+
return unless (x5c_options = @options[:x5c])
66+
67+
@key = X5cKeyFinder.new(x5c_options[:root_certificates], x5c_options[:crls]).from(token.header['x5c'])
7168
end
7269

7370
def allowed_and_valid_algorithms

lib/jwt/jwk/key_finder.rb

+22-9
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,10 @@ def initialize(options)
2222

2323
# Returns the verification key for the given kid
2424
# @param [String] kid the key id
25-
def key_for(kid)
26-
raise ::JWT::DecodeError, 'No key id (kid) found from token headers' unless kid || @allow_nil_kid
27-
raise ::JWT::DecodeError, 'Invalid type for kid header parameter' unless kid.nil? || kid.is_a?(String)
25+
def key_for(kid, key_field = :kid)
26+
raise ::JWT::DecodeError, "Invalid type for #{key_field} header parameter" unless kid.nil? || kid.is_a?(String)
2827

29-
jwk = resolve_key(kid)
28+
jwk = resolve_key(kid, key_field)
3029

3130
raise ::JWT::DecodeError, 'No keys found in jwks' unless @jwks.any?
3231
raise ::JWT::DecodeError, "Could not find public key for kid #{kid}" unless jwk
@@ -37,22 +36,36 @@ def key_for(kid)
3736
# Returns the key for the given token
3837
# @param [JWT::EncodedToken] token the token
3938
def call(token)
40-
key_for(token.header['kid'])
39+
kid = token.header['kid']
40+
x5t = token.header['x5t']
41+
x5c = token.header['x5c']
42+
43+
if kid
44+
key_for(kid, :kid)
45+
elsif x5t
46+
key_for(x5t, :x5t)
47+
elsif x5c
48+
key_for(x5c, :x5c)
49+
elsif @allow_nil_kid
50+
key_for(kid)
51+
else
52+
raise ::JWT::DecodeError, 'No key id (kid) or x5t found from token headers'
53+
end
4154
end
4255

4356
private
4457

45-
def resolve_key(kid)
46-
key_matcher = ->(key) { (kid.nil? && @allow_nil_kid) || key[:kid] == kid }
58+
def resolve_key(kid, key_field)
59+
key_matcher = ->(key) { (kid.nil? && @allow_nil_kid) || key[key_field] == kid }
4760

4861
# First try without invalidation to facilitate application caching
49-
@jwks ||= JWT::JWK::Set.new(@jwks_loader.call(kid: kid))
62+
@jwks ||= JWT::JWK::Set.new(@jwks_loader.call(key_field => kid))
5063
jwk = @jwks.find { |key| key_matcher.call(key) }
5164

5265
return jwk if jwk
5366

5467
# Second try, invalidate for backwards compatibility
55-
@jwks = JWT::JWK::Set.new(@jwks_loader.call(invalidate: true, kid_not_found: true, kid: kid))
68+
@jwks = JWT::JWK::Set.new(@jwks_loader.call(invalidate: true, kid_not_found: true, key_field => kid))
5669
@jwks.find { |key| key_matcher.call(key) }
5770
end
5871
end

lib/jwt/jwk/rsa.rb

+5-1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ def verify_key
5151
def export(options = {})
5252
exported = parameters.clone
5353
exported.reject! { |k, _| RSA_PRIVATE_KEY_ELEMENTS.include? k } unless private? && options[:include_private] == true
54+
55+
exported[:x5c] = Base64.strict_encode(rsa_key.to_der) if options[:x5c]
56+
exported[:x5t] = Base64.url_encode(OpenSSL::Digest::SHA1.new(rsa_key.to_der).digest) if options[:x5t]
57+
5458
exported
5559
end
5660

@@ -67,7 +71,7 @@ def key_digest
6771
def []=(key, value)
6872
raise ArgumentError, 'cannot overwrite cryptographic key attributes' if RSA_KEY_ELEMENTS.include?(key.to_sym)
6973

70-
super(key, value)
74+
super
7175
end
7276

7377
private

lib/jwt/x5t_key_finder.rb

-36
This file was deleted.

spec/jwt/jwk/decode_with_jwk_spec.rb

+21-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
describe '.decode for JWK usecase' do
55
let(:keypair) { test_pkey('rsa-2048-private.pem') }
66
let(:jwk) { JWT::JWK.new(keypair) }
7-
let(:public_jwks) { { keys: [jwk.export, { kid: 'not_the_correct_one', kty: 'oct', k: 'secret' }] } }
7+
let(:valid_key) { jwk.export }
8+
let(:public_jwks) { { keys: [valid_key, { kid: 'not_the_correct_one', kty: 'oct', k: 'secret' }] } }
89
let(:token_payload) { { 'data' => 'something' } }
910
let(:token_headers) { { kid: jwk.kid } }
1011
let(:algorithm) { 'RS512' }
@@ -38,6 +39,24 @@
3839
end
3940
end
4041

42+
context 'and x5t is in the set' do
43+
let(:valid_key) { jwk.export(x5t: true) }
44+
let(:token_headers) { { x5t: Base64.urlsafe_encode64(OpenSSL::Digest::SHA1.new(keypair.to_der).digest, padding: false) } }
45+
it 'is able to decode the token' do
46+
payload, _header = described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: public_jwks })
47+
expect(payload).to eq(token_payload)
48+
end
49+
end
50+
51+
context 'and x5c is in the set' do
52+
let(:valid_key) { jwk.export(x5c: true) }
53+
let(:token_headers) { { x5c: Base64.strict_encode64(keypair.to_der) } }
54+
it 'is able to decode the token' do
55+
payload, _header = described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: public_jwks })
56+
expect(payload).to eq(token_payload)
57+
end
58+
end
59+
4160
context 'no keys are found in the set' do
4261
let(:public_jwks) { { keys: [] } }
4362
it 'raises an exception' do
@@ -51,7 +70,7 @@
5170
let(:token_headers) { {} }
5271
it 'raises an exception' do
5372
expect { described_class.decode(signed_token, nil, true, { algorithms: [algorithm], jwks: public_jwks }) }.to raise_error(
54-
JWT::DecodeError, 'No key id (kid) found from token headers'
73+
JWT::DecodeError, 'No key id (kid) or x5t found from token headers'
5574
)
5675
end
5776
end

spec/jwt/x5t_key_finder_spec.rb

-138
This file was deleted.

0 commit comments

Comments
 (0)