diff --git a/lib/jwt/jwk/rsa.rb b/lib/jwt/jwk/rsa.rb index aa8d4835..9c8cfc86 100644 --- a/lib/jwt/jwk/rsa.rb +++ b/lib/jwt/jwk/rsa.rb @@ -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 ||= {} @@ -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? @@ -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] @@ -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 diff --git a/lib/jwt/version.rb b/lib/jwt/version.rb index 7ad81897..12e1773a 100644 --- a/lib/jwt/version.rb +++ b/lib/jwt/version.rb @@ -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 diff --git a/spec/jwk/rsa_spec.rb b/spec/jwk/rsa_spec.rb index d677f54a..b888e6b0 100644 --- a/spec/jwk/rsa_spec.rb +++ b/spec/jwk/rsa_spec.rb @@ -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