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

Stricter RSA key generation from jwk parameters #524

Merged
merged 6 commits into from
Oct 30, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
65 changes: 47 additions & 18 deletions lib/jwt/jwk/rsa.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ class RSA < KeyBase # rubocop:disable Metrics/ClassLength
RSA_PRIVATE_KEY_ELEMENTS = %i[d p q dp dq qi].freeze
RSA_KEY_ELEMENTS = (RSA_PRIVATE_KEY_ELEMENTS + RSA_PUBLIC_KEY_ELEMENTS).freeze

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 initialize(key, params = nil, options = {})
params ||= {}

Expand All @@ -25,7 +28,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 +111,52 @@ 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

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))

raise JWT::JWKError, 'Creating a RSA key with a private key requires the CRT parameters to be defined' if sequence.size < RSA_ASN1_SEQUENCE.size
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 +168,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.key?(:d)

OpenSSL::BN.new(::JWT::Base64.url_decode(jwk_data), BINARY)
end
parameters = RSA_OPT_PARAMS - rsa_parameters.keys
return if parameters.empty? || parameters.size == RSA_OPT_PARAMS.size

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
6 changes: 5 additions & 1 deletion lib/jwt/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ def self.openssl_3?
end

def self.openssl_3_hmac_empty_key_regression?
openssl_3? && ::Gem::Version.new(OpenSSL::VERSION) <= ::Gem::Version.new('3.0.0')
openssl_3? && openssl_version <= ::Gem::Version.new('3.0.0')
end

def self.openssl_version
@openssl_version ||= ::Gem::Version.new(OpenSSL::VERSION)
end
end
93 changes: 93 additions & 0 deletions spec/jwk/rsa_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,97 @@
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 an 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) }

before do
skip 'OpenSSL prior to 2.2 does not seem to support partial parameters' if ::JWT.openssl_version < ::Gem::Version.new('2.2')
end

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'

context 'when e, n, d is given' do
let(:jwk_parameters) { all_jwk_parameters.slice(:e, :n, :d) }

it 'expects all CRT parameters given and raises error' do
expect { subject }.to raise_error(JWT::JWKError, 'Creating a RSA key with a private key requires the CRT parameters to be defined')
end
end
end

describe '.create_rsa_key_using_sets' do
before do
skip 'OpenSSL without the RSA#set_key method not supported' unless OpenSSL::PKey::RSA.new.respond_to?(:set_key)
skip 'OpenSSL 3.0 does not allow mutating objects anymore' if ::JWT.openssl_3?
end

subject(:rsa) { described_class.create_rsa_key_using_sets(rsa_parameters) }

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

describe '.create_rsa_key_using_accessors' do
before do
skip 'OpenSSL if RSA#set_key is available there is no accessors anymore' if OpenSSL::PKey::RSA.new.respond_to?(:set_key)
end

subject(:rsa) { described_class.create_rsa_key_using_accessors(rsa_parameters) }

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