Skip to content

Commit 30d0a65

Browse files
committed
Stricter RSA key generation from jwk parameters
1 parent 37a0d9e commit 30d0a65

File tree

2 files changed

+134
-41
lines changed

2 files changed

+134
-41
lines changed

lib/jwt/jwk/rsa.rb

Lines changed: 64 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -73,65 +73,88 @@ def import(jwk_data)
7373
new(rsa_pkey(pkey_params), kid: jwk_attributes(jwk_data, :kid)[:kid])
7474
end
7575

76-
private
76+
RSA_OPT_PARAMS = %i[p q dp dq qi].freeze
77+
RSA_ASN1_SEQUENCE = (%i[n e d] + RSA_OPT_PARAMS).freeze # https://www.rfc-editor.org/rfc/rfc3447#appendix-A.1.2
7778

78-
def jwk_attributes(jwk_data, *attributes)
79-
attributes.each_with_object({}) do |attribute, hash|
80-
value = jwk_data[attribute] || jwk_data[attribute.to_s]
81-
value = yield(value) if block_given?
82-
hash[attribute] = value
79+
def create_rsa_key_using_der(rsa_parameters)
80+
validate_rsa_parameters!(rsa_parameters)
81+
82+
sequence = RSA_ASN1_SEQUENCE.each_with_object([]) do |key, arr|
83+
next if rsa_parameters[key].nil?
84+
85+
arr << OpenSSL::ASN1::Integer.new(rsa_parameters[key])
8386
end
84-
end
8587

86-
def rsa_pkey(rsa_parameters)
87-
raise JWT::JWKError, 'Key format is invalid for RSA' unless rsa_parameters[:n] && rsa_parameters[:e]
88+
if sequence.size > 2 # Append "two-prime" version for private key
89+
sequence.unshift(OpenSSL::ASN1::Integer.new(0))
90+
end
8891

89-
create_rsa_key(rsa_parameters)
92+
OpenSSL::PKey::RSA.new(OpenSSL::ASN1::Sequence(sequence).to_der)
9093
end
9194

92-
if ::JWT.openssl_3?
93-
ASN1_SEQUENCE = %i[n e d p q dp dq qi].freeze
94-
def create_rsa_key(rsa_parameters)
95-
sequence = ASN1_SEQUENCE.each_with_object([]) do |key, arr|
96-
next if rsa_parameters[key].nil?
95+
def create_rsa_key_using_sets(rsa_parameters)
96+
validate_rsa_parameters!(rsa_parameters)
9797

98-
arr << OpenSSL::ASN1::Integer.new(rsa_parameters[key])
99-
end
100-
101-
if sequence.size > 2 # For a private key
102-
sequence.unshift(OpenSSL::ASN1::Integer.new(0))
103-
end
98+
OpenSSL::PKey::RSA.new.tap do |rsa_key|
99+
rsa_key.set_key(rsa_parameters[:n], rsa_parameters[:e], rsa_parameters[:d])
100+
rsa_key.set_factors(rsa_parameters[:p], rsa_parameters[:q]) if rsa_parameters[:p] && rsa_parameters[:q]
101+
rsa_key.set_crt_params(rsa_parameters[:dp], rsa_parameters[:dq], rsa_parameters[:qi]) if rsa_parameters[:dp] && rsa_parameters[:dq] && rsa_parameters[:qi]
102+
end
103+
end
104104

105-
OpenSSL::PKey::RSA.new(OpenSSL::ASN1::Sequence(sequence).to_der)
105+
def create_rsa_key_using_accessors(rsa_parameters) # rubocop:disable Metrics/AbcSize
106+
validate_rsa_parameters!(rsa_parameters)
107+
108+
OpenSSL::PKey::RSA.new.tap do |rsa_key|
109+
rsa_key.n = rsa_parameters[:n]
110+
rsa_key.e = rsa_parameters[:e]
111+
rsa_key.d = rsa_parameters[:d] if rsa_parameters[:d]
112+
rsa_key.p = rsa_parameters[:p] if rsa_parameters[:p]
113+
rsa_key.q = rsa_parameters[:q] if rsa_parameters[:q]
114+
rsa_key.dmp1 = rsa_parameters[:dp] if rsa_parameters[:dp]
115+
rsa_key.dmq1 = rsa_parameters[:dq] if rsa_parameters[:dq]
116+
rsa_key.iqmp = rsa_parameters[:qi] if rsa_parameters[:qi]
106117
end
118+
end
119+
120+
if ::JWT.openssl_3?
121+
alias create_rsa_key create_rsa_key_using_der
107122
elsif OpenSSL::PKey::RSA.new.respond_to?(:set_key)
108-
def create_rsa_key(rsa_parameters)
109-
OpenSSL::PKey::RSA.new.tap do |rsa_key|
110-
rsa_key.set_key(rsa_parameters[:n], rsa_parameters[:e], rsa_parameters[:d])
111-
rsa_key.set_factors(rsa_parameters[:p], rsa_parameters[:q]) if rsa_parameters[:p] && rsa_parameters[:q]
112-
rsa_key.set_crt_params(rsa_parameters[:dp], rsa_parameters[:dq], rsa_parameters[:qi]) if rsa_parameters[:dp] && rsa_parameters[:dq] && rsa_parameters[:qi]
113-
end
114-
end
123+
alias create_rsa_key create_rsa_key_using_sets
115124
else
116-
def create_rsa_key(rsa_parameters) # rubocop:disable Metrics/AbcSize
117-
OpenSSL::PKey::RSA.new.tap do |rsa_key|
118-
rsa_key.n = rsa_parameters[:n]
119-
rsa_key.e = rsa_parameters[:e]
120-
rsa_key.d = rsa_parameters[:d] if rsa_parameters[:d]
121-
rsa_key.p = rsa_parameters[:p] if rsa_parameters[:p]
122-
rsa_key.q = rsa_parameters[:q] if rsa_parameters[:q]
123-
rsa_key.dmp1 = rsa_parameters[:dp] if rsa_parameters[:dp]
124-
rsa_key.dmq1 = rsa_parameters[:dq] if rsa_parameters[:dq]
125-
rsa_key.iqmp = rsa_parameters[:qi] if rsa_parameters[:qi]
126-
end
127-
end
125+
alias create_rsa_key create_rsa_key_using_accessors
128126
end
129127

130128
def decode_open_ssl_bn(jwk_data)
131129
return nil unless jwk_data
132130

133131
OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY)
134132
end
133+
134+
private
135+
136+
def validate_rsa_parameters!(rsa_parameters)
137+
return unless rsa_parameters[:d]
138+
139+
return if RSA_OPT_PARAMS.all? { |k| rsa_parameters.keys.include?(k) }
140+
return if RSA_OPT_PARAMS.none? { |k| rsa_parameters.keys.include?(k) }
141+
142+
raise JWT::JWKError, 'When one of p, q, dp, dq or qi is given all the other optimization parameters also needs to be defined'
143+
end
144+
145+
def jwk_attributes(jwk_data, *attributes)
146+
attributes.each_with_object({}) do |attribute, hash|
147+
value = jwk_data[attribute] || jwk_data[attribute.to_s]
148+
value = yield(value) if block_given?
149+
hash[attribute] = value
150+
end
151+
end
152+
153+
def rsa_pkey(rsa_parameters)
154+
raise JWT::JWKError, 'Key format is invalid for RSA' unless rsa_parameters[:n] && rsa_parameters[:e]
155+
156+
create_rsa_key(rsa_parameters)
157+
end
135158
end
136159
end
137160
end

spec/jwk/rsa_spec.rb

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,74 @@
123123
end
124124
end
125125
end
126+
127+
shared_examples 'creating an RSA object from JWK parameters' do
128+
let(:rsa_parameters) { jwk_parameters.transform_values { |value| described_class.decode_open_ssl_bn(value) } }
129+
let(:all_jwk_parameters) { described_class.new(rsa_key).export(include_private: true) }
130+
131+
context 'when e and n is given' do
132+
let(:jwk_parameters) { all_jwk_parameters.slice(:e, :n) }
133+
134+
it 'creates a valid RSA object representing a public key' do
135+
expect(subject).to be_a(::OpenSSL::PKey::RSA)
136+
expect(subject.private?).to eq(false)
137+
end
138+
end
139+
140+
context 'when e, n, d is given' do
141+
let(:jwk_parameters) { all_jwk_parameters.slice(:e, :n, :d) }
142+
143+
it 'creates a valid RSA object representing a private key' do
144+
expect(subject).to be_a(::OpenSSL::PKey::RSA)
145+
expect(subject.private?).to eq(true)
146+
end
147+
148+
it 'can be used for encryption and decryption' do
149+
expect(subject.private_decrypt(subject.public_encrypt('secret'))).to eq('secret')
150+
end
151+
152+
it 'can be used for signing and verification' do
153+
data = 'data_to_sign'
154+
signature = subject.sign(OpenSSL::Digest.new('SHA512'), data)
155+
expect(subject.verify(OpenSSL::Digest.new('SHA512'), signature, data)).to eq(true)
156+
end
157+
end
158+
159+
context 'when e, n, d, p and q is given' do
160+
let(:jwk_parameters) { all_jwk_parameters.slice(:e, :n, :d, :p, :q) }
161+
162+
it 'creates a valid RSA object representing a public key' do
163+
expect { subject }.to raise_error(JWT::JWKError, 'When one of p, q, dp, dq or qi is given all the other optimization parameters also needs to be defined')
164+
end
165+
end
166+
167+
context 'when n, e, d, p, q, dp, dq, qi is given' do
168+
let(:jwk_parameters) { all_jwk_parameters.slice(:n, :e, :d, :p, :q, :dp, :dq, :qi) }
169+
170+
it 'creates a valid RSA object representing a public key' do
171+
expect(subject).to be_a(::OpenSSL::PKey::RSA)
172+
expect(subject.private?).to eq(true)
173+
end
174+
end
175+
end
176+
177+
describe '.create_rsa_key_using_der' do
178+
subject(:rsa) { described_class.create_rsa_key_using_der(rsa_parameters) }
179+
180+
include_examples 'creating an RSA object from JWK parameters'
181+
end
182+
183+
if OpenSSL::PKey::RSA.new.respond_to?(:set_key) # Very old OpenSSL versions (pre 1.1.0)
184+
describe '.create_rsa_key_using_sets' do
185+
subject(:rsa) { described_class.create_rsa_key_using_sets(rsa_parameters) }
186+
187+
include_examples 'creating an RSA object from JWK parameters'
188+
end
189+
elsif !::JWK.openssl_3?
190+
describe '.create_rsa_key_using_accessors' do
191+
subject(:rsa) { described_class.create_rsa_key_using_accessors(rsa_parameters) }
192+
193+
include_examples 'creating an RSA object from JWK parameters'
194+
end
195+
end
126196
end

0 commit comments

Comments
 (0)