Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JWK Common Parameters #520

Merged
merged 16 commits into from
Oct 14, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions lib/jwt/jwk/ec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ def members
end

def export(options = {})
exported_hash = members.merge(kid: kid)
kid # Make sure a kid is generated
exported_hash = common_parameters.merge(members)

return exported_hash unless private? && options[:include_private] == true

Expand Down Expand Up @@ -93,11 +94,17 @@ class << self
def import(jwk_data)
# See https://tools.ietf.org/html/rfc7518#section-6.2.1 for an
# explanation of the relevant parameters.

jwk_crv, jwk_x, jwk_y, jwk_d, jwk_kid = jwk_attrs(jwk_data, %i[crv x y d kid])
parameters = jwk_data.transform_keys(&:to_sym)
jwk_kty = parameters.delete(:kty) # Will be re-added upon export
jwk_crv = parameters.delete(:crv)
jwk_x = parameters.delete(:x)
jwk_y = parameters.delete(:y)
jwk_d = parameters.delete(:d)

raise JWT::JWKError, "Incorrect 'kty' value: #{jwk_kty}, expected #{KTY}" unless jwk_kty == KTY
raise JWT::JWKError, 'Key format is invalid for EC' unless jwk_crv && jwk_x && jwk_y

new(ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d), kid: jwk_kid)
new(ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d), common_parameters: parameters)
end

def to_openssl_curve(crv)
Expand All @@ -115,12 +122,6 @@ def to_openssl_curve(crv)

private

def jwk_attrs(jwk_data, attrs)
bellebaum marked this conversation as resolved.
Show resolved Hide resolved
attrs.map do |attr|
jwk_data[attr] || jwk_data[attr.to_s]
end
end

if ::JWT.openssl_3?
def ec_pkey(jwk_crv, jwk_x, jwk_y, jwk_d) # rubocop:disable Metrics/MethodLength
curve = to_openssl_curve(jwk_crv)
Expand Down
16 changes: 9 additions & 7 deletions lib/jwt/jwk/hmac.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ def public_key

# See https://tools.ietf.org/html/rfc7517#appendix-A.3
def export(options = {})
exported_hash = {
kty: KTY,
kid: kid
}
exported_hash = common_parameters.merge({
kty: KTY,
kid: kid
})

return exported_hash unless private? && options[:include_private] == true

Expand All @@ -54,12 +54,14 @@ def key_digest

class << self
def import(jwk_data)
jwk_k = jwk_data[:k] || jwk_data['k']
jwk_kid = jwk_data[:kid] || jwk_data['kid']
parameters = jwk_data.transform_keys(&:to_sym)
jwk_kty = parameters.delete(:kty) # Will be re-added upon export
jwk_k = parameters.delete(:k)

raise JWT::JWKError, "Incorrect 'kty' value: #{jwk_kty}, expected #{KTY}" unless jwk_kty == KTY
raise JWT::JWKError, 'Key format is invalid for HMAC' unless jwk_k

new(jwk_k, kid: jwk_kid)
new(jwk_k, common_parameters: parameters)
end
end
end
Expand Down
9 changes: 7 additions & 2 deletions lib/jwt/jwk/key_base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,19 @@ def initialize(options)
options = { kid: options }
end

@kid = options[:kid]
@common_parameters = options[:common_parameters] || {}
bellebaum marked this conversation as resolved.
Show resolved Hide resolved
@common_parameters = @common_parameters.transform_keys(&:to_sym) # Uniform interface
@common_parameters[:kid] = options[:kid] if options[:kid] # kid can be specified outside common_parameters

@kid_generator = options[:kid_generator] || ::JWT.configuration.jwk.kid_generator
end

def kid
@kid ||= generate_kid
@common_parameters[:kid] ||= generate_kid
bellebaum marked this conversation as resolved.
Show resolved Hide resolved
end

attr_accessor :common_parameters
bellebaum marked this conversation as resolved.
Show resolved Hide resolved

private

attr_reader :kid_generator
Expand Down
24 changes: 13 additions & 11 deletions lib/jwt/jwk/rsa.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ def public_key
end

def export(options = {})
exported_hash = members.merge(kid: kid)
kid # Make sure a kid is generated
bellebaum marked this conversation as resolved.
Show resolved Hide resolved
exported_hash = common_parameters.merge(members)

return exported_hash unless private? && options[:include_private] == true

Expand Down Expand Up @@ -70,7 +71,14 @@ def import(jwk_data)
pkey_params = jwk_attributes(jwk_data, *RSA_KEY_ELEMENTS) do |value|
decode_open_ssl_bn(value)
end
new(rsa_pkey(pkey_params), kid: jwk_attributes(jwk_data, :kid)[:kid])
parameters = jwk_data.transform_keys(&:to_sym)
jwk_kty = parameters.delete(:kty) # Will be re-added upon export
RSA_KEY_ELEMENTS.each { |e| parameters.delete e }
bellebaum marked this conversation as resolved.
Show resolved Hide resolved

raise JWT::JWKError, "Incorrect 'kty' value: #{jwk_kty}, expected #{KTY}" unless jwk_kty == KTY
raise JWT::JWKError, 'Key format is invalid for RSA' unless pkey_params[:n] && pkey_params[:e]

new(rsa_pkey(pkey_params), common_parameters: parameters)
end

private
Expand All @@ -83,15 +91,9 @@ def jwk_attributes(jwk_data, *attributes)
end
end

def rsa_pkey(rsa_parameters)
raise JWT::JWKError, 'Key format is invalid for RSA' unless rsa_parameters[:n] && rsa_parameters[:e]

create_rsa_key(rsa_parameters)
end

if ::JWT.openssl_3?
ASN1_SEQUENCE = %i[n e d p q dp dq qi].freeze
def create_rsa_key(rsa_parameters)
def rsa_pkey(rsa_parameters)
bellebaum marked this conversation as resolved.
Show resolved Hide resolved
sequence = ASN1_SEQUENCE.each_with_object([]) do |key, arr|
next if rsa_parameters[key].nil?

Expand All @@ -105,15 +107,15 @@ def create_rsa_key(rsa_parameters)
OpenSSL::PKey::RSA.new(OpenSSL::ASN1::Sequence(sequence).to_der)
end
elsif OpenSSL::PKey::RSA.new.respond_to?(:set_key)
def create_rsa_key(rsa_parameters)
def rsa_pkey(rsa_parameters)
OpenSSL::PKey::RSA.new.tap do |rsa_key|
rsa_key.set_key(rsa_parameters[:n], rsa_parameters[:e], rsa_parameters[:d])
rsa_key.set_factors(rsa_parameters[:p], rsa_parameters[:q]) if rsa_parameters[:p] && rsa_parameters[:q]
rsa_key.set_crt_params(rsa_parameters[:dp], rsa_parameters[:dq], rsa_parameters[:qi]) if rsa_parameters[:dp] && rsa_parameters[:dq] && rsa_parameters[:qi]
end
end
else
def create_rsa_key(rsa_parameters) # rubocop:disable Metrics/AbcSize
def rsa_pkey(rsa_parameters) # rubocop:disable Metrics/AbcSize
OpenSSL::PKey::RSA.new.tap do |rsa_key|
rsa_key.n = rsa_parameters[:n]
rsa_key.e = rsa_parameters[:e]
Expand Down
9 changes: 9 additions & 0 deletions spec/jwk/ec_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,15 @@
expect(subject).to include(:d)
end
end

context 'when a common parameter is given' do
let(:parameters) { { use: 'sig' } }
let(:keypair) { ec_key }
subject { described_class.new(keypair, common_parameters: parameters).export }
it 'returns a hash including the common parameter' do
expect(subject).to include(:use)
end
end
end

describe '.import' do
Expand Down
9 changes: 9 additions & 0 deletions spec/jwk/hmac_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@
expect(subject.kid).to eq('custom_key_identifier')
end
end

context 'with a common parameter' do
let(:exported_key) {
super().merge(use: 'sig')
}
it 'imports that common parameter' do
expect(subject.common_parameters).to include(:use)
end
end
end
end
end
24 changes: 21 additions & 3 deletions spec/jwk/rsa_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,19 +78,37 @@
end
end

context 'when kid is given as in a hash parameter' do
context 'when kid is given in a hash parameter' do
it 'uses the given kid' do
expect(described_class.new(OpenSSL::PKey::RSA.new(2048), kid: 'given').kid).to eq('given')
end
end

context 'when kid is given as a common JWK parameter' do
it 'uses the given kid' do
expect(described_class.new(OpenSSL::PKey::RSA.new(2048), common_parameters: { kid: 'given' }).kid).to eq('given')
end
end
end

describe '.common_parameters' do
context 'when a common parameters hash is given' do
it 'imports the common parameter' do
expect(described_class.new(OpenSSL::PKey::RSA.new(2048), common_parameters: { use: 'sig' }).common_parameters).to include(:use)
end

it 'converts string keys to symbol keys' do
expect(described_class.new(OpenSSL::PKey::RSA.new(2048), common_parameters: { 'use' => 'sig' }).common_parameters).to include(:use)
end
end
end

describe '.import' do
subject { described_class.import(params) }
let(:exported_key) { described_class.new(rsa_key).export }

context 'when keypair is imported with symbol keys' do
let(:params) { { e: exported_key[:e], n: exported_key[:n] } }
let(:params) { { kty: 'RSA', e: exported_key[:e], n: exported_key[:n] } }
it 'returns a hash with the public parts of the key' do
expect(subject).to be_a described_class
expect(subject.private?).to eq false
Expand All @@ -99,7 +117,7 @@
end

context 'when keypair is imported with string keys from JSON' do
let(:params) { { 'e' => exported_key[:e], 'n' => exported_key[:n] } }
let(:params) { { 'kty' => 'RSA', 'e' => exported_key[:e], 'n' => exported_key[:n] } }
it 'returns a hash with the public parts of the key' do
expect(subject).to be_a described_class
expect(subject.private?).to eq false
Expand Down
23 changes: 19 additions & 4 deletions spec/jwk_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,18 @@
expect(subject.export).to eq(params)
end
end

context 'when a common JWK parameter is specified' do
it 'returns the defined common JWK parameter' do
params[:use] = 'sig'
expect(subject.export).to eq(params)
end
end
end

describe '.new' do
let(:kid) { nil }
subject { described_class.new(keypair, kid) }
let(:options) { nil }
subject { described_class.new(keypair, options) }

context 'when RSA key is given' do
let(:keypair) { rsa_key }
Expand All @@ -61,9 +68,17 @@

context 'when kid is given' do
let(:keypair) { rsa_key }
let(:kid) { 'CUSTOM_KID' }
let(:options) { 'CUSTOM_KID' }
it 'sets the kid' do
expect(subject.kid).to eq(kid)
expect(subject.kid).to eq(options)
end
end

context 'when a common parameter is given' do
let(:keypair) { rsa_key }
let(:options) { { common_parameters: { 'use' => 'sig' } } }
it 'sets the common parameter' do
expect(subject.common_parameters).to eq({ use: 'sig' })
end
end
end
Expand Down