Skip to content

Commit 8191784

Browse files
committed
Stricter RSA key generation from parameters
1 parent 771630d commit 8191784

File tree

2 files changed

+119
-18
lines changed

2 files changed

+119
-18
lines changed

lib/jwt/jwk/rsa.rb

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def initialize(key, params = nil, options = {})
2525
end
2626

2727
def keypair
28-
@keypair ||= create_rsa_key(jwk_attributes(*(RSA_KEY_ELEMENTS - [:kty])))
28+
@keypair ||= self.class.create_rsa_key(jwk_attributes(*(RSA_KEY_ELEMENTS - [:kty])))
2929
end
3030

3131
def private?
@@ -108,31 +108,53 @@ def encode_open_ssl_bn(key_part)
108108
::JWT::Base64.url_encode(key_part.to_s(BINARY))
109109
end
110110

111-
if ::JWT.openssl_3?
112-
ASN1_SEQUENCE = %i[n e d p q dp dq qi].freeze
113-
def create_rsa_key(rsa_parameters)
114-
sequence = ASN1_SEQUENCE.each_with_object([]) do |key, arr|
111+
def decode_open_ssl_bn(jwk_data)
112+
self.class.decode_open_ssl_bn(jwk_data)
113+
end
114+
115+
class << self
116+
def import(jwk_data)
117+
new(jwk_data)
118+
end
119+
120+
def decode_open_ssl_bn(jwk_data)
121+
return nil unless jwk_data
122+
123+
OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY)
124+
end
125+
126+
RSA_OPT_PARAMS = %i[p q dp dq qi].freeze
127+
RSA_ASN1_SEQUENCE = (%i[n e d] + RSA_OPT_PARAMS).freeze # https://www.rfc-editor.org/rfc/rfc3447#appendix-A.1.2
128+
129+
def create_rsa_key_using_der(rsa_parameters)
130+
validate_rsa_parameters!(rsa_parameters)
131+
132+
sequence = RSA_ASN1_SEQUENCE.each_with_object([]) do |key, arr|
115133
next if rsa_parameters[key].nil?
116134

117135
arr << OpenSSL::ASN1::Integer.new(rsa_parameters[key])
118136
end
119137

120-
if sequence.size > 2 # For a private key
138+
if sequence.size > 2 # Append "two-prime" version for private key
121139
sequence.unshift(OpenSSL::ASN1::Integer.new(0))
122140
end
123141

124142
OpenSSL::PKey::RSA.new(OpenSSL::ASN1::Sequence(sequence).to_der)
125143
end
126-
elsif OpenSSL::PKey::RSA.new.respond_to?(:set_key)
127-
def create_rsa_key(rsa_parameters)
144+
145+
def create_rsa_key_using_sets(rsa_parameters)
146+
validate_rsa_parameters!(rsa_parameters)
147+
128148
OpenSSL::PKey::RSA.new.tap do |rsa_key|
129149
rsa_key.set_key(rsa_parameters[:n], rsa_parameters[:e], rsa_parameters[:d])
130150
rsa_key.set_factors(rsa_parameters[:p], rsa_parameters[:q]) if rsa_parameters[:p] && rsa_parameters[:q]
131151
rsa_key.set_crt_params(rsa_parameters[:dp], rsa_parameters[:dq], rsa_parameters[:qi]) if rsa_parameters[:dp] && rsa_parameters[:dq] && rsa_parameters[:qi]
132152
end
133153
end
134-
else
135-
def create_rsa_key(rsa_parameters) # rubocop:disable Metrics/AbcSize
154+
155+
def create_rsa_key_using_accessors(rsa_parameters) # rubocop:disable Metrics/AbcSize
156+
validate_rsa_parameters!(rsa_parameters)
157+
136158
OpenSSL::PKey::RSA.new.tap do |rsa_key|
137159
rsa_key.n = rsa_parameters[:n]
138160
rsa_key.e = rsa_parameters[:e]
@@ -144,17 +166,22 @@ def create_rsa_key(rsa_parameters) # rubocop:disable Metrics/AbcSize
144166
rsa_key.iqmp = rsa_parameters[:qi] if rsa_parameters[:qi]
145167
end
146168
end
147-
end
148169

149-
def decode_open_ssl_bn(jwk_data)
150-
return nil unless jwk_data
170+
def validate_rsa_parameters!(rsa_parameters)
171+
return unless rsa_parameters[:d]
151172

152-
OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY)
153-
end
173+
return if RSA_OPT_PARAMS.all? { |k| rsa_parameters.keys.include?(k) }
174+
return if RSA_OPT_PARAMS.none? { |k| rsa_parameters.keys.include?(k) }
154175

155-
class << self
156-
def import(jwk_data)
157-
new(jwk_data)
176+
raise JWT::JWKError, 'When one of p, q, dp, dq or qi is given all the other optimization parameters also needs to be defined' # https://www.rfc-editor.org/rfc/rfc7518.html#section-6.3.2
177+
end
178+
179+
if ::JWT.openssl_3?
180+
alias create_rsa_key create_rsa_key_using_der
181+
elsif OpenSSL::PKey::RSA.new.respond_to?(:set_key)
182+
alias create_rsa_key create_rsa_key_using_sets
183+
else
184+
alias create_rsa_key create_rsa_key_using_accessors
158185
end
159186
end
160187
end

spec/jwk/rsa_spec.rb

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,4 +135,78 @@
135135
end
136136
end
137137
end
138+
139+
shared_examples 'creating an RSA object from complete JWK parameters' do
140+
let(:rsa_parameters) { jwk_parameters.transform_values { |value| described_class.decode_open_ssl_bn(value) } }
141+
let(:all_jwk_parameters) { described_class.new(rsa_key).export(include_private: true) }
142+
143+
context 'when public parameters (e, n) are given' do
144+
let(:jwk_parameters) { all_jwk_parameters.slice(:e, :n) }
145+
146+
it 'creates a valid RSA object representing a public key' do
147+
expect(subject).to be_a(::OpenSSL::PKey::RSA)
148+
expect(subject.private?).to eq(false)
149+
end
150+
end
151+
152+
context 'when only e, n, d, p and q are given' do
153+
let(:jwk_parameters) { all_jwk_parameters.slice(:e, :n, :d, :p, :q) }
154+
155+
it 'raises an error telling all the exponents are required' do
156+
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')
157+
end
158+
end
159+
160+
context 'when all key components n, e, d, p, q, dp, dq, qi are given' do
161+
let(:jwk_parameters) { all_jwk_parameters.slice(:n, :e, :d, :p, :q, :dp, :dq, :qi) }
162+
163+
it 'creates a valid RSA object representing a public key' do
164+
expect(subject).to be_a(::OpenSSL::PKey::RSA)
165+
expect(subject.private?).to eq(true)
166+
end
167+
end
168+
end
169+
170+
shared_examples 'creating RSA object from partial JWK parameters' do
171+
context 'when e, n, d is given' do
172+
let(:jwk_parameters) { all_jwk_parameters.slice(:e, :n, :d) }
173+
174+
it 'creates a valid RSA object representing a private key' do
175+
expect(subject).to be_a(::OpenSSL::PKey::RSA)
176+
expect(subject.private?).to eq(true)
177+
end
178+
179+
it 'can be used for encryption and decryption' do
180+
expect(subject.private_decrypt(subject.public_encrypt('secret'))).to eq('secret')
181+
end
182+
183+
it 'can be used for signing and verification' do
184+
data = 'data_to_sign'
185+
signature = subject.sign(OpenSSL::Digest.new('SHA512'), data)
186+
expect(subject.verify(OpenSSL::Digest.new('SHA512'), signature, data)).to eq(true)
187+
end
188+
end
189+
end
190+
191+
describe '.create_rsa_key_using_der' do
192+
subject(:rsa) { described_class.create_rsa_key_using_der(rsa_parameters) }
193+
194+
include_examples 'creating an RSA object from complete JWK parameters'
195+
end
196+
197+
if OpenSSL::PKey::RSA.new.respond_to?(:set_key) # Very old OpenSSL versions (pre 1.1.0)
198+
describe '.create_rsa_key_using_sets' do
199+
subject(:rsa) { described_class.create_rsa_key_using_sets(rsa_parameters) }
200+
201+
include_examples 'creating an RSA object from complete JWK parameters'
202+
include_examples 'creating RSA object from partial JWK parameters'
203+
end
204+
elsif !::JWK.openssl_3?
205+
describe '.create_rsa_key_using_accessors' do
206+
subject(:rsa) { described_class.create_rsa_key_using_accessors(rsa_parameters) }
207+
208+
include_examples 'creating an RSA object from complete JWK parameters'
209+
include_examples 'creating RSA object from partial JWK parameters'
210+
end
211+
end
138212
end

0 commit comments

Comments
 (0)