Skip to content

Commit

Permalink
Stricter RSA key generation from parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
anakinj committed Oct 24, 2022
1 parent 771630d commit 8191784
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 18 deletions.
63 changes: 45 additions & 18 deletions lib/jwt/jwk/rsa.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def initialize(key, params = nil, options = {})
end

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

def private?
Expand Down Expand Up @@ -108,31 +108,53 @@ def encode_open_ssl_bn(key_part)
::JWT::Base64.url_encode(key_part.to_s(BINARY))
end

if ::JWT.openssl_3?
ASN1_SEQUENCE = %i[n e d p q dp dq qi].freeze
def create_rsa_key(rsa_parameters)
sequence = ASN1_SEQUENCE.each_with_object([]) do |key, arr|
def decode_open_ssl_bn(jwk_data)
self.class.decode_open_ssl_bn(jwk_data)
end

class << self
def import(jwk_data)
new(jwk_data)
end

def decode_open_ssl_bn(jwk_data)
return nil unless jwk_data

OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY)
end

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

def create_rsa_key_using_der(rsa_parameters)
validate_rsa_parameters!(rsa_parameters)

sequence = RSA_ASN1_SEQUENCE.each_with_object([]) do |key, arr|
next if rsa_parameters[key].nil?

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

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

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 create_rsa_key_using_sets(rsa_parameters)
validate_rsa_parameters!(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 create_rsa_key_using_accessors(rsa_parameters) # rubocop:disable Metrics/AbcSize
validate_rsa_parameters!(rsa_parameters)

OpenSSL::PKey::RSA.new.tap do |rsa_key|
rsa_key.n = rsa_parameters[:n]
rsa_key.e = rsa_parameters[:e]
Expand All @@ -144,17 +166,22 @@ def create_rsa_key(rsa_parameters) # rubocop:disable Metrics/AbcSize
rsa_key.iqmp = rsa_parameters[:qi] if rsa_parameters[:qi]
end
end
end

def decode_open_ssl_bn(jwk_data)
return nil unless jwk_data
def validate_rsa_parameters!(rsa_parameters)
return unless rsa_parameters[:d]

OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY)
end
return if RSA_OPT_PARAMS.all? { |k| rsa_parameters.keys.include?(k) }
return if RSA_OPT_PARAMS.none? { |k| rsa_parameters.keys.include?(k) }

class << self
def import(jwk_data)
new(jwk_data)
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
end

if ::JWT.openssl_3?
alias create_rsa_key create_rsa_key_using_der
elsif OpenSSL::PKey::RSA.new.respond_to?(:set_key)
alias create_rsa_key create_rsa_key_using_sets
else
alias create_rsa_key create_rsa_key_using_accessors
end
end
end
Expand Down
74 changes: 74 additions & 0 deletions spec/jwk/rsa_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,78 @@
end
end
end

shared_examples 'creating an RSA object from complete JWK parameters' do
let(:rsa_parameters) { jwk_parameters.transform_values { |value| described_class.decode_open_ssl_bn(value) } }
let(:all_jwk_parameters) { described_class.new(rsa_key).export(include_private: true) }

context 'when public parameters (e, n) are given' do
let(:jwk_parameters) { all_jwk_parameters.slice(:e, :n) }

it 'creates a valid RSA object representing a public key' do
expect(subject).to be_a(::OpenSSL::PKey::RSA)
expect(subject.private?).to eq(false)
end
end

context 'when only e, n, d, p and q are given' do
let(:jwk_parameters) { all_jwk_parameters.slice(:e, :n, :d, :p, :q) }

it 'raises an error telling all the exponents are required' do
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')
end
end

context 'when all key components n, e, d, p, q, dp, dq, qi are given' do
let(:jwk_parameters) { all_jwk_parameters.slice(:n, :e, :d, :p, :q, :dp, :dq, :qi) }

it 'creates a valid RSA object representing a public key' do
expect(subject).to be_a(::OpenSSL::PKey::RSA)
expect(subject.private?).to eq(true)
end
end
end

shared_examples 'creating RSA object from partial JWK parameters' do
context 'when e, n, d is given' do
let(:jwk_parameters) { all_jwk_parameters.slice(:e, :n, :d) }

it 'creates a valid RSA object representing a private key' do
expect(subject).to be_a(::OpenSSL::PKey::RSA)
expect(subject.private?).to eq(true)
end

it 'can be used for encryption and decryption' do
expect(subject.private_decrypt(subject.public_encrypt('secret'))).to eq('secret')
end

it 'can be used for signing and verification' do
data = 'data_to_sign'
signature = subject.sign(OpenSSL::Digest.new('SHA512'), data)
expect(subject.verify(OpenSSL::Digest.new('SHA512'), signature, data)).to eq(true)
end
end
end

describe '.create_rsa_key_using_der' do
subject(:rsa) { described_class.create_rsa_key_using_der(rsa_parameters) }

include_examples 'creating an RSA object from complete JWK parameters'
end

if OpenSSL::PKey::RSA.new.respond_to?(:set_key) # Very old OpenSSL versions (pre 1.1.0)
describe '.create_rsa_key_using_sets' do
subject(:rsa) { described_class.create_rsa_key_using_sets(rsa_parameters) }

include_examples 'creating an RSA object from complete JWK parameters'
include_examples 'creating RSA object from partial JWK parameters'
end
elsif !::JWK.openssl_3?
describe '.create_rsa_key_using_accessors' do
subject(:rsa) { described_class.create_rsa_key_using_accessors(rsa_parameters) }

include_examples 'creating an RSA object from complete JWK parameters'
include_examples 'creating RSA object from partial JWK parameters'
end
end
end

0 comments on commit 8191784

Please sign in to comment.