From 09136e5c5cc03602bf9a7f3a4178aa17dc566f08 Mon Sep 17 00:00:00 2001 From: Jonathan PHILIPPE Date: Thu, 22 Aug 2024 10:30:57 +0200 Subject: [PATCH 1/4] Adds h003 schematic support --- README.md | 2 +- lib/epics/client.rb | 96 +++++++++++++++++++++-------- lib/epics/generic_request.rb | 6 +- lib/epics/generic_upload_request.rb | 8 +-- lib/epics/header_request.rb | 16 ++++- lib/epics/hia.rb | 16 ++--- lib/epics/ini.rb | 8 +-- lib/epics/key.rb | 4 +- lib/epics/response.rb | 28 ++++----- lib/epics/signer.rb | 2 +- spec/client_spec.rb | 24 ++++---- spec/generic_upload_spec.rb | 2 +- spec/middleware/parse_ebics_spec.rb | 3 +- spec/signer_spec.rb | 2 +- 14 files changed, 136 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index 387d95b..9b5ff91 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ EPICS is a ruby implementation of the [EBICS](https://www.ebics.org/) (Electronic Banking Internet Communication Standard). -It supports EBICS 2.5. +It supports EBICS 2.4 and 2.5. The client supports the complete initialization process comprising INI, HIA and HPB including the INI letter generation. It offers support for the most common download and upload order types diff --git a/lib/epics/client.rb b/lib/epics/client.rb index deda7c3..7c73b67 100644 --- a/lib/epics/client.rb +++ b/lib/epics/client.rb @@ -1,12 +1,20 @@ class Epics::Client extend Forwardable - attr_accessor :passphrase, :url, :host_id, :user_id, :partner_id, :keys, :keys_content + attr_accessor :passphrase, :url, :host_id, :user_id, :partner_id, :keys, :keys_content, :current_order_id + attr_reader :version attr_writer :iban, :bic, :name attr_accessor :locale def_delegators :connection, :post + VERSION_H3 = 'H003' + VERSION_H4 = 'H004' + + VERSIONS = [VERSION_H3, VERSION_H4] + + USER_AGENT = "EPICS v#{Epics::VERSION}" + def initialize(keys_content, passphrase, url, host_id, user_id, partner_id) self.keys_content = keys_content.respond_to?(:read) ? keys_content.read : keys_content if keys_content self.passphrase = passphrase @@ -16,6 +24,25 @@ def initialize(keys_content, passphrase, url, host_id, user_id, partner_id) self.user_id = user_id self.partner_id = partner_id self.locale = :de + self.current_order_id = 0 + self.version = VERSION_H4 + + yield self if block_given? + end + + def version=(version) + raise ArgumentError, "Unsupported version: #{version}" unless VERSIONS.include?(version) + + @version = version + end + + def urn_schema + case version + when VERSION_H3 + "http://www.ebics.org/#{version}" + when VERSION_H4 + "urn:org:ebics:#{version}" + end end def inspect @@ -25,24 +52,41 @@ def inspect @partner_id=\"#{self.partner_id}\"" end - def e - keys["E002"] + def next_order_id + raise 'Order ID overflow' if current_order_id >= 1679615 + self.current_order_id += 1 end - def a - keys["A006"] + def encryption_version + 'E002' end - def x - keys["X002"] + def encryption_key + keys[encryption_version] end - def bank_e - keys["#{host_id.upcase}.E002"] + def signature_version + 'A006' end - def bank_x - keys["#{host_id.upcase}.X002"] + def signature_key + keys[signature_version] + end + + def authentication_version + 'X002' + end + + def authentication_key + keys[authentication_version] + end + + def bank_encryption_key + keys["#{host_id.upcase}.#{encryption_version}"] + end + + def bank_authentication_key + keys["#{host_id.upcase}.#{authentication_version}"] end def name @@ -61,9 +105,9 @@ def order_types @order_types ||= (self.HTD; @order_types) end - def self.setup(passphrase, url, host_id, user_id, partner_id, keysize = 2048) - client = new(nil, passphrase, url, host_id, user_id, partner_id) - client.keys = %w(A006 X002 E002).each_with_object({}) do |type, memo| + def self.setup(passphrase, url, host_id, user_id, partner_id, keysize = 2048, &block) + client = new(nil, passphrase, url, host_id, user_id, partner_id, &block) + client.keys = [client.signature_version, client.authentication_version, client.encryption_version].each_with_object({}) do |type, memo| memo[type] = Epics::Key.new( OpenSSL::PKey::RSA.generate(keysize) ) end @@ -108,7 +152,7 @@ def INI end def HPB - Nokogiri::XML(download(Epics::HPB)).xpath("//xmlns:PubKeyValue", xmlns: "urn:org:ebics:H004").each do |node| + Nokogiri::XML(download(Epics::HPB)).xpath("//xmlns:PubKeyValue", xmlns: urn_schema).each do |node| type = node.parent.last_element_child.content modulus = Base64.decode64(node.at_xpath(".//*[local-name() = 'Modulus']").content) @@ -123,7 +167,7 @@ def HPB self.keys["#{host_id.upcase}.#{type}"] = Epics::Key.new(bank) end - [bank_x, bank_e] + [bank_authentication_key, bank_encryption_key] end def AZV(document) @@ -215,15 +259,15 @@ def Z54(from, to) end def HAA - Nokogiri::XML(download(Epics::HAA)).at_xpath("//xmlns:OrderTypes", xmlns: "urn:org:ebics:H004").content.split(/\s/) + Nokogiri::XML(download(Epics::HAA)).at_xpath("//xmlns:OrderTypes", xmlns: urn_schema).content.split(/\s/) end def HTD Nokogiri::XML(download(Epics::HTD)).tap do |htd| - @iban ||= htd.at_xpath("//xmlns:AccountNumber[@international='true']", xmlns: "urn:org:ebics:H004").text rescue nil - @bic ||= htd.at_xpath("//xmlns:BankCode[@international='true']", xmlns: "urn:org:ebics:H004").text rescue nil - @name ||= htd.at_xpath("//xmlns:Name", xmlns: "urn:org:ebics:H004").text rescue nil - @order_types ||= htd.search("//xmlns:OrderTypes", xmlns: "urn:org:ebics:H004").map{|o| o.content.split(/\s/) }.delete_if{|o| o == ""}.flatten + @iban ||= htd.at_xpath("//xmlns:AccountNumber[@international='true']", xmlns: urn_schema).text rescue nil + @bic ||= htd.at_xpath("//xmlns:BankCode[@international='true']", xmlns: urn_schema).text rescue nil + @name ||= htd.at_xpath("//xmlns:Name", xmlns: urn_schema).text rescue nil + @order_types ||= htd.search("//xmlns:OrderTypes", xmlns: urn_schema).map{|o| o.content.split(/\s/) }.delete_if{|o| o == ""}.flatten end.to_xml end @@ -251,14 +295,12 @@ def save_keys(path) def upload(order_type, document) order = order_type.new(self, document) - res = post(url, order.to_xml).body - order.transaction_id = res.transaction_id - - order_id = res.order_id + session = post(url, order.to_xml).body + order.transaction_id = session.transaction_id res = post(url, order.to_transfer_xml).body - return res.transaction_id, [res.order_id, order_id].detect { |id| id.to_s.chars.any? } + return res.transaction_id, [res.order_id, session.order_id].detect { |id| id.to_s.chars.any? } end def download(order_type, *args, **options) @@ -282,7 +324,7 @@ def download_and_unzip(order_type, *args, **options) end def connection - @connection ||= Faraday.new(headers: { 'Content-Type' => 'text/xml', user_agent: "EPICS v#{Epics::VERSION}"}, ssl: { verify: verify_ssl? }) do |faraday| + @connection ||= Faraday.new(headers: { 'Content-Type' => 'text/xml', user_agent: USER_AGENT}, ssl: { verify: verify_ssl? }) do |faraday| faraday.use Epics::XMLSIG, { client: self } faraday.use Epics::ParseEbics, { client: self} # faraday.use MyAdapter diff --git a/lib/epics/generic_request.rb b/lib/epics/generic_request.rb index c9ea744..6e84ed0 100644 --- a/lib/epics/generic_request.rb +++ b/lib/epics/generic_request.rb @@ -53,7 +53,7 @@ def auth_signature def to_transfer_xml Nokogiri::XML::Builder.new do |xml| - xml.send(root, 'xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#', 'xmlns' => 'urn:org:ebics:H004', 'Version' => 'H004', 'Revision' => '1') { + xml.send(root, 'xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#', 'xmlns' => client.urn_schema, 'Version' => client.version, 'Revision' => '1') { xml.header(authenticate: true) { xml.static { xml.HostID host_id @@ -76,7 +76,7 @@ def to_transfer_xml def to_receipt_xml Nokogiri::XML::Builder.new do |xml| - xml.send(root, 'xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#', 'xmlns' => 'urn:org:ebics:H004', 'Version' => 'H004', 'Revision' => '1') { + xml.send(root, 'xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#', 'xmlns' => client.urn_schema, 'Version' => client.version, 'Revision' => '1') { xml.header(authenticate: true) { xml.static { xml.HostID host_id @@ -98,7 +98,7 @@ def to_receipt_xml def to_xml Nokogiri::XML::Builder.new do |xml| - xml.send(root, 'xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#', 'xmlns' => 'urn:org:ebics:H004', 'Version' => 'H004', 'Revision'=> '1') { + xml.send(root, 'xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#', 'xmlns' => client.urn_schema, 'Version' => client.version, 'Revision'=> '1') { xml.parent.add_child(header) xml.parent.add_child(auth_signature) xml.parent.add_child(body) diff --git a/lib/epics/generic_upload_request.rb b/lib/epics/generic_upload_request.rb index 8d71fdc..58f6e43 100644 --- a/lib/epics/generic_upload_request.rb +++ b/lib/epics/generic_upload_request.rb @@ -23,8 +23,8 @@ def body xml.body { xml.DataTransfer { xml.DataEncryptionInfo(authenticate: true) { - xml.EncryptionPubKeyDigest(client.bank_e.public_digest, Version: 'E002', Algorithm: "http://www.w3.org/2001/04/xmlenc#sha256") - xml.TransactionKey Base64.encode64(client.bank_e.key.public_encrypt(self.key)).gsub(/\n/,'') + xml.EncryptionPubKeyDigest(client.bank_encryption_key.public_digest, Version: client.encryption_version, Algorithm: "http://www.w3.org/2001/04/xmlenc#sha256") + xml.TransactionKey Base64.encode64(client.bank_encryption_key.key.public_encrypt(self.key)).gsub(/\n/,'') } xml.SignatureData(encrypted_order_signature, authenticate: true) } @@ -36,7 +36,7 @@ def order_signature Nokogiri::XML::Builder.new do |xml| xml.UserSignatureData('xmlns' => 'http://www.ebics.org/S001', 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:schemaLocation' => 'http://www.ebics.org/S001 http://www.ebics.org/S001/ebics_signature.xsd') { xml.OrderSignatureData { - xml.SignatureVersion "A006" + xml.SignatureVersion client.signature_version xml.SignatureValue signature_value xml.PartnerID partner_id xml.UserID user_id @@ -46,7 +46,7 @@ def order_signature end def signature_value - client.a.sign( digester.digest(document.gsub(/\n|\r/, "")) ) + client.signature_key.sign( digester.digest(document.gsub(/\n|\r/, "")) ) end def encrypt(d) diff --git a/lib/epics/header_request.rb b/lib/epics/header_request.rb index de18006..cdf62af 100644 --- a/lib/epics/header_request.rb +++ b/lib/epics/header_request.rb @@ -2,6 +2,8 @@ class Epics::HeaderRequest extend Forwardable attr_accessor :client + BASE36_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' + PRODUCT_NAME = 'EPICS - a ruby ebics kernel' PRODUCT_LANG = 'de' @@ -25,14 +27,15 @@ def build(options = {}) xml.Product(PRODUCT_NAME, 'Language' => PRODUCT_LANG) xml.OrderDetails { xml.OrderType options[:order_type] + xml.OrderID b36encode(client.next_order_id).rjust(4, '0') if client.version == Epics::Client::VERSION_H3 xml.OrderAttribute options[:order_attribute] xml.StandardOrderParams { build_attributes(xml, options[:order_params]) } if options[:order_params] } xml.BankPubKeyDigests { - xml.Authentication(client.bank_x.public_digest, Version: 'X002', Algorithm: 'http://www.w3.org/2001/04/xmlenc#sha256') - xml.Encryption(client.bank_e.public_digest, Version: 'E002', Algorithm: 'http://www.w3.org/2001/04/xmlenc#sha256') + xml.Authentication(client.bank_authentication_key.public_digest, Version: client.authentication_version, Algorithm: 'http://www.w3.org/2001/04/xmlenc#sha256') + xml.Encryption(client.bank_encryption_key.public_digest, Version: client.encryption_version, Algorithm: 'http://www.w3.org/2001/04/xmlenc#sha256') } if options[:with_bank_pubkey_digests] xml.SecurityMedium '0000' xml.NumSegments options[:num_segments] if options[:num_segments] @@ -57,4 +60,13 @@ def build_attributes(xml, attributes) end end end + + def b36encode(number) + str = '' + while number > 0 + number, i = number.divmod(36) + str += BASE36_ALPHABET[i] + end + str.empty? ? '0' : str + end end diff --git a/lib/epics/hia.rb b/lib/epics/hia.rb index 2f60719..7505765 100644 --- a/lib/epics/hia.rb +++ b/lib/epics/hia.rb @@ -24,24 +24,24 @@ def body def order_data Nokogiri::XML::Builder.new do |xml| - xml.HIARequestOrderData('xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#', 'xmlns' => 'urn:org:ebics:H004') { + xml.HIARequestOrderData('xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#', 'xmlns' => client.urn_schema) { xml.AuthenticationPubKeyInfo { xml.PubKeyValue { xml.send('ds:RSAKeyValue') { - xml.send('ds:Modulus', Base64.strict_encode64([client.x.n].pack("H*"))) - xml.send('ds:Exponent', Base64.strict_encode64(client.x.key.e.to_s(2))) + xml.send('ds:Modulus', Base64.strict_encode64([client.authentication_key.n].pack("H*"))) + xml.send('ds:Exponent', Base64.strict_encode64(client.authentication_key.key.e.to_s(2))) } } - xml.AuthenticationVersion 'X002' + xml.AuthenticationVersion client.authentication_version } xml.EncryptionPubKeyInfo{ xml.PubKeyValue { xml.send('ds:RSAKeyValue') { - xml.send('ds:Modulus', Base64.strict_encode64([client.e.n].pack("H*"))) - xml.send('ds:Exponent', Base64.strict_encode64(client.e.key.e.to_s(2))) + xml.send('ds:Modulus', Base64.strict_encode64([client.encryption_key.n].pack("H*"))) + xml.send('ds:Exponent', Base64.strict_encode64(client.encryption_key.key.e.to_s(2))) } } - xml.EncryptionVersion 'E002' + xml.EncryptionVersion client.encryption_version } xml.PartnerID partner_id xml.UserID user_id @@ -51,7 +51,7 @@ def order_data def to_xml Nokogiri::XML::Builder.new do |xml| - xml.send(root, 'xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#', 'xmlns' => 'urn:org:ebics:H004', 'Version' => 'H004', 'Revision' => '1') { + xml.send(root, 'xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#', 'xmlns' => client.urn_schema, 'Version' => client.version, 'Revision' => '1') { xml.parent.add_child(header) xml.parent.add_child(body) } diff --git a/lib/epics/ini.rb b/lib/epics/ini.rb index 9a18ad6..44bc8fc 100644 --- a/lib/epics/ini.rb +++ b/lib/epics/ini.rb @@ -28,12 +28,12 @@ def key_signature xml.SignaturePubKeyInfo { xml.PubKeyValue { xml.send('ds:RSAKeyValue') { - xml.send('ds:Modulus', Base64.strict_encode64([client.a.n].pack("H*"))) - xml.send('ds:Exponent', Base64.strict_encode64(client.a.key.e.to_s(2))) + xml.send('ds:Modulus', Base64.strict_encode64([client.signature_key.n].pack("H*"))) + xml.send('ds:Exponent', Base64.strict_encode64(client.signature_key.key.e.to_s(2))) } xml.TimeStamp timestamp } - xml.SignatureVersion 'A006' + xml.SignatureVersion client.signature_version } xml.PartnerID partner_id xml.UserID user_id @@ -43,7 +43,7 @@ def key_signature def to_xml Nokogiri::XML::Builder.new do |xml| - xml.send(root, 'xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#', 'xmlns' => 'urn:org:ebics:H004', 'Version' => 'H004', 'Revision' => '1') { + xml.send(root, 'xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#', 'xmlns' => client.urn_schema, 'Version' => client.version, 'Revision' => '1') { xml.parent.add_child(header) xml.parent.add_child(body) } diff --git a/lib/epics/key.rb b/lib/epics/key.rb index 496c3a7..a8d0f4e 100644 --- a/lib/epics/key.rb +++ b/lib/epics/key.rb @@ -36,8 +36,8 @@ def sign(msg) msg, salt_length: :digest, mgf1_hash: 'SHA256', - ), - ).gsub("\n", '') + ), + ).gsub("\n", '') end def digester diff --git a/lib/epics/response.rb b/lib/epics/response.rb index 714127e..367e003 100644 --- a/lib/epics/response.rb +++ b/lib/epics/response.rb @@ -12,7 +12,7 @@ def technical_error? end def technical_code - doc.xpath("//xmlns:header/xmlns:mutable/xmlns:ReturnCode", xmlns: "urn:org:ebics:H004").text + doc.xpath("//xmlns:header/xmlns:mutable/xmlns:ReturnCode", xmlns: client.urn_schema).text end def business_error? @@ -20,7 +20,7 @@ def business_error? end def business_code - doc.xpath("//xmlns:body/xmlns:ReturnCode", xmlns: "urn:org:ebics:H004").text + doc.xpath("//xmlns:body/xmlns:ReturnCode", xmlns: client.urn_schema).text end def ok? @@ -28,29 +28,29 @@ def ok? end def last_segment? - !!doc.at_xpath("//xmlns:header/xmlns:mutable/*[@lastSegment='true']", xmlns: "urn:org:ebics:H004") + !!doc.at_xpath("//xmlns:header/xmlns:mutable/*[@lastSegment='true']", xmlns: client.urn_schema) end def segmented? - !!doc.at_xpath("//xmlns:header/xmlns:mutable/xmlns:SegmentNumber", xmlns: "urn:org:ebics:H004") + !!doc.at_xpath("//xmlns:header/xmlns:mutable/xmlns:SegmentNumber", xmlns: client.urn_schema) end def return_code - doc.xpath("//xmlns:ReturnCode", xmlns: "urn:org:ebics:H004").last.content + doc.xpath("//xmlns:ReturnCode", xmlns: client.urn_schema).last.content rescue NoMethodError nil end def report_text - doc.xpath("//xmlns:ReportText", xmlns: "urn:org:ebics:H004").first.content + doc.xpath("//xmlns:ReportText", xmlns: client.urn_schema).first.content end def transaction_id - doc.xpath("//xmlns:header/xmlns:static/xmlns:TransactionID", xmlns: 'urn:org:ebics:H004').text + doc.xpath("//xmlns:header/xmlns:static/xmlns:TransactionID", xmlns: client.urn_schema).text end def order_id - doc.xpath("//xmlns:header/xmlns:mutable/xmlns:OrderID", xmlns: "urn:org:ebics:H004").text + doc.xpath("//xmlns:header/xmlns:mutable/xmlns:OrderID", xmlns: client.urn_schema).text end def digest_valid? @@ -66,17 +66,17 @@ def signature_valid? signature = doc.xpath("//ds:SignedInfo", ds: "http://www.w3.org/2000/09/xmldsig#").first.canonicalize signature_value = doc.xpath("//ds:SignatureValue", ds: "http://www.w3.org/2000/09/xmldsig#").first - client.bank_x.key.verify(digester, Base64.decode64(signature_value.content), signature) + client.bank_authentication_key.key.verify(digester, Base64.decode64(signature_value.content), signature) end def public_digest_valid? - encryption_pub_key_digest = doc.xpath("//xmlns:EncryptionPubKeyDigest", xmlns: 'urn:org:ebics:H004').first + encryption_pub_key_digest = doc.xpath("//xmlns:EncryptionPubKeyDigest", xmlns: client.urn_schema).first - client.e.public_digest == encryption_pub_key_digest.content + client.encryption_key.public_digest == encryption_pub_key_digest.content end def order_data - order_data_encrypted = Base64.decode64(doc.xpath("//xmlns:OrderData", xmlns: 'urn:org:ebics:H004').first.content) + order_data_encrypted = Base64.decode64(doc.xpath("//xmlns:OrderData", xmlns: client.urn_schema).first.content) data = (cipher.update(order_data_encrypted) + cipher.final) @@ -93,9 +93,9 @@ def cipher end def transaction_key - transaction_key_encrypted = Base64.decode64(doc.xpath("//xmlns:TransactionKey", xmlns: 'urn:org:ebics:H004').first.content) + transaction_key_encrypted = Base64.decode64(doc.xpath("//xmlns:TransactionKey", xmlns: client.urn_schema).first.content) - @transaction_key ||= client.e.key.private_decrypt(transaction_key_encrypted) + @transaction_key ||= client.encryption_key.key.private_decrypt(transaction_key_encrypted) end def digester diff --git a/lib/epics/signer.rb b/lib/epics/signer.rb index 9de1227..a17cfcb 100644 --- a/lib/epics/signer.rb +++ b/lib/epics/signer.rb @@ -20,7 +20,7 @@ def sign! signature_value_node = doc.xpath("//ds:SignatureValue").first if signature_node - signature_value_node.content = Base64.encode64(client.x.key.sign(digester, signature_node.canonicalize)).gsub(/\n/,'') + signature_value_node.content = Base64.encode64(client.authentication_key.key.sign(digester, signature_node.canonicalize)).gsub(/\n/,'') end doc diff --git a/spec/client_spec.rb b/spec/client_spec.rb index 23515bb..0d30f4f 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -52,33 +52,33 @@ end end - describe '#e' do + describe '#encryption_key' do it 'the encryption key' do - expect(subject.e.public_digest).to eq("rwIxSUJAVEFDQ0sdYe+CybdspMllDG6ArNtdCzUbT1E=") + expect(subject.encryption_key.public_digest).to eq("rwIxSUJAVEFDQ0sdYe+CybdspMllDG6ArNtdCzUbT1E=") end end - describe '#x' do - it 'the signing key' do - expect(subject.x.public_digest).to eq("Jjcu97qg595PPn+0OvqBOBIskMIiStNYYXyjgWHeBhE=") + describe '#authentication_key' do + it 'the authentication key' do + expect(subject.authentication_key.public_digest).to eq("Jjcu97qg595PPn+0OvqBOBIskMIiStNYYXyjgWHeBhE=") end end - describe '#a' do - it 'the authentication key' do - expect(subject.a.public_digest).to eq("9ay3tc+I3MgJBaroeD7XJfOtHcq7IR23fljWefl0dzk=") + describe '#signature_key' do + it 'the signing key' do + expect(subject.signature_key.public_digest).to eq("9ay3tc+I3MgJBaroeD7XJfOtHcq7IR23fljWefl0dzk=") end end - describe '#bank_e' do + describe '#bank_encryption_key' do it 'the banks encryption key' do - expect(subject.bank_e.public_digest).to eq("dFAYe281vj9NB7w+VoWIdfHnjY9hNbZLbHsDOu76QAE=") + expect(subject.bank_encryption_key.public_digest).to eq("dFAYe281vj9NB7w+VoWIdfHnjY9hNbZLbHsDOu76QAE=") end end - describe '#bank_x' do + describe '#bank_authentication_key' do it 'the banks signing key' do - expect(subject.bank_x.public_digest).to eq("dFAYe281vj9NB7w+VoWIdfHnjY9hNbZLbHsDOu76QAE=") + expect(subject.bank_authentication_key.public_digest).to eq("dFAYe281vj9NB7w+VoWIdfHnjY9hNbZLbHsDOu76QAE=") end end diff --git a/spec/generic_upload_spec.rb b/spec/generic_upload_spec.rb index 3e1d1dc..63620ca 100644 --- a/spec/generic_upload_spec.rb +++ b/spec/generic_upload_spec.rb @@ -23,7 +23,7 @@ before { allow(OpenSSL::Random).to receive(:random_bytes).with(32).and_return(Base64.strict_decode64("7wtROfiX4tyN60cygJUSsHkhzxX1RVJa8vGNYnflvKc=")) } # digest requires 32 bytes it 'will be the signed document' do - key = subject.client.a.key + key = subject.client.signature_key.key verification_result = key.verify_pss( 'SHA256', diff --git a/spec/middleware/parse_ebics_spec.rb b/spec/middleware/parse_ebics_spec.rb index 5575dbb..c09cea7 100644 --- a/spec/middleware/parse_ebics_spec.rb +++ b/spec/middleware/parse_ebics_spec.rb @@ -1,9 +1,10 @@ # frozen_string_literal: true RSpec.describe Epics::ParseEbics do + let(:client) { Epics::Client.new( File.open(File.join( File.dirname(__FILE__), '..', 'fixtures', 'SIZBN001.key')), 'secret' , 'https://194.180.18.30/ebicsweb/ebicsweb', 'HOST', 'USER', 'PARTNER') } subject do Faraday.new do |builder| - builder.use Epics::ParseEbics + builder.use Epics::ParseEbics, { client: client } builder.adapter :test, Faraday::Adapter::Test::Stubs.new do |stub| stub.post('/business_nok') { [200, {}, File.read(File.join(File.dirname(__FILE__), '..', 'fixtures', 'xml', 'ebics_business_nok.xml'))] } stub.post('/technical_nok') { [200, {}, File.read(File.join(File.dirname(__FILE__), '..', 'fixtures', 'xml', 'ebics_technical_nok.xml'))] } diff --git a/spec/signer_spec.rb b/spec/signer_spec.rb index e2b0379..8cc5824 100644 --- a/spec/signer_spec.rb +++ b/spec/signer_spec.rb @@ -27,7 +27,7 @@ end it 'can be verified with the same key' do - expect(client.x.key.verify(OpenSSL::Digest::SHA256.new, Base64.decode64(subject.doc.xpath("//ds:SignatureValue").first.content), subject.signature_node.canonicalize)).to be(true) + expect(client.authentication_key.key.verify(OpenSSL::Digest::SHA256.new, Base64.decode64(subject.doc.xpath("//ds:SignatureValue").first.content), subject.signature_node.canonicalize)).to be(true) end end From f9a66bf4e1b7b1ecbc6589851c49aa4504c355e6 Mon Sep 17 00:00:00 2001 From: Jonathan PHILIPPE Date: Mon, 26 Aug 2024 16:08:54 +0200 Subject: [PATCH 2/4] Adds a005 signature support --- lib/epics.rb | 6 ++- lib/epics/client.rb | 31 ++++++++++---- lib/epics/generic_upload_request.rb | 8 ++-- lib/epics/response.rb | 9 +--- lib/epics/signature_algorithm.rb | 2 + .../{key.rb => signature_algorithm/base.rb} | 28 +++++-------- lib/epics/signature_algorithm/rsa.rb | 42 +++++++++++++++++++ lib/epics/signature_algorithm/rsapkcs1.rb | 2 + lib/epics/signature_algorithm/rsapss.rb | 24 +++++++++++ lib/epics/signer.rb | 8 +--- spec/client_spec.rb | 14 +++---- spec/generic_upload_spec.rb | 10 +---- spec/key_spec.rb | 15 ++----- spec/signer_spec.rb | 2 +- 14 files changed, 127 insertions(+), 74 deletions(-) create mode 100644 lib/epics/signature_algorithm.rb rename lib/epics/{key.rb => signature_algorithm/base.rb} (53%) create mode 100644 lib/epics/signature_algorithm/rsa.rb create mode 100644 lib/epics/signature_algorithm/rsapkcs1.rb create mode 100644 lib/epics/signature_algorithm/rsapss.rb diff --git a/lib/epics.rb b/lib/epics.rb index 2376cd5..00f33cc 100644 --- a/lib/epics.rb +++ b/lib/epics.rb @@ -10,7 +10,11 @@ require 'securerandom' require 'time' require "epics/version" -require "epics/key" +require "epics/signature_algorithm" +require "epics/signature_algorithm/base" +require "epics/signature_algorithm/rsa" +require "epics/signature_algorithm/rsapss" +require "epics/signature_algorithm/rsapkcs1" require "epics/response" require "epics/error" require 'epics/letter_renderer' diff --git a/lib/epics/client.rb b/lib/epics/client.rb index 7c73b67..7705e11 100644 --- a/lib/epics/client.rb +++ b/lib/epics/client.rb @@ -3,6 +3,7 @@ class Epics::Client attr_accessor :passphrase, :url, :host_id, :user_id, :partner_id, :keys, :keys_content, :current_order_id attr_reader :version + attr_accessor :signature_version attr_writer :iban, :bic, :name attr_accessor :locale @@ -10,6 +11,8 @@ class Epics::Client VERSION_H3 = 'H003' VERSION_H4 = 'H004' + VERSION_A5 = 'A005' + VERSION_A6 = 'A006' VERSIONS = [VERSION_H3, VERSION_H4] @@ -26,6 +29,7 @@ def initialize(keys_content, passphrase, url, host_id, user_id, partner_id) self.locale = :de self.current_order_id = 0 self.version = VERSION_H4 + self.signature_version = VERSION_A6 yield self if block_given? end @@ -65,10 +69,6 @@ def encryption_key keys[encryption_version] end - def signature_version - 'A006' - end - def signature_key keys[signature_version] end @@ -108,7 +108,12 @@ def order_types def self.setup(passphrase, url, host_id, user_id, partner_id, keysize = 2048, &block) client = new(nil, passphrase, url, host_id, user_id, partner_id, &block) client.keys = [client.signature_version, client.authentication_version, client.encryption_version].each_with_object({}) do |type, memo| - memo[type] = Epics::Key.new( OpenSSL::PKey::RSA.generate(keysize) ) + memo[type] = case type + when VERSION_A6 + Epics::SignatureAlgorithm::RsaPss.new( OpenSSL::PKey::RSA.generate(keysize) ) + else + Epics::SignatureAlgorithm::RsaPkcs1.new( OpenSSL::PKey::RSA.generate(keysize) ) + end end client @@ -164,7 +169,12 @@ def HPB bank = OpenSSL::PKey::RSA.new(OpenSSL::ASN1::Sequence(sequence).to_der) - self.keys["#{host_id.upcase}.#{type}"] = Epics::Key.new(bank) + self.keys["#{host_id.upcase}.#{type}"] = case type + when VERSION_A6 + Epics::SignatureAlgorithm::RsaPss.new(bank) + else + Epics::SignatureAlgorithm::RsaPkcs1.new(bank) + end end [bank_authentication_key, bank_encryption_key] @@ -324,7 +334,7 @@ def download_and_unzip(order_type, *args, **options) end def connection - @connection ||= Faraday.new(headers: { 'Content-Type' => 'text/xml', user_agent: USER_AGENT}, ssl: { verify: verify_ssl? }) do |faraday| + @connection ||= Faraday.new(headers: { 'Content-Type' => 'text/xml', user_agent: USER_AGENT }, ssl: { verify: verify_ssl? }) do |faraday| faraday.use Epics::XMLSIG, { client: self } faraday.use Epics::ParseEbics, { client: self} # faraday.use MyAdapter @@ -334,7 +344,12 @@ def connection def extract_keys JSON.load(self.keys_content).each_with_object({}) do |(type, key), memo| - memo[type] = Epics::Key.new(decrypt(key)) if key + memo[type] = case type + when VERSION_A6 + Epics::SignatureAlgorithm::RsaPss.new(decrypt(key)) + else + Epics::SignatureAlgorithm::RsaPkcs1.new(decrypt(key)) + end if key end end diff --git a/lib/epics/generic_upload_request.rb b/lib/epics/generic_upload_request.rb index 58f6e43..f7195e2 100644 --- a/lib/epics/generic_upload_request.rb +++ b/lib/epics/generic_upload_request.rb @@ -14,10 +14,6 @@ def cipher @cipher ||= OpenSSL::Cipher.new("aes-128-cbc").tap { |cipher| cipher.encrypt } end - def digester - @digester ||= OpenSSL::Digest::SHA256.new - end - def body Nokogiri::XML::Builder.new do |xml| xml.body { @@ -46,7 +42,9 @@ def order_signature end def signature_value - client.signature_key.sign( digester.digest(document.gsub(/\n|\r/, "")) ) + Base64.encode64( + client.signature_key.sign(client.signature_key.digester.digest(document.gsub(/\n|\r/, ""))) + ).gsub("\n", '') end def encrypt(d) diff --git a/lib/epics/response.rb b/lib/epics/response.rb index 367e003..03066c7 100644 --- a/lib/epics/response.rb +++ b/lib/epics/response.rb @@ -57,7 +57,7 @@ def digest_valid? authenticated = doc.xpath("//*[@authenticate='true']").map(&:canonicalize).join digest_value = doc.xpath("//ds:DigestValue", ds: "http://www.w3.org/2000/09/xmldsig#").first - digest = Base64.encode64(digester.digest(authenticated)).strip + digest = Base64.encode64(client.signature_key.digester.digest(authenticated)).strip digest == digest_value.content end @@ -66,7 +66,7 @@ def signature_valid? signature = doc.xpath("//ds:SignedInfo", ds: "http://www.w3.org/2000/09/xmldsig#").first.canonicalize signature_value = doc.xpath("//ds:SignatureValue", ds: "http://www.w3.org/2000/09/xmldsig#").first - client.bank_authentication_key.key.verify(digester, Base64.decode64(signature_value.content), signature) + client.bank_authentication_key.verify(signature_value.content, signature) end def public_digest_valid? @@ -97,9 +97,4 @@ def transaction_key @transaction_key ||= client.encryption_key.key.private_decrypt(transaction_key_encrypted) end - - def digester - @digester ||= OpenSSL::Digest::SHA256.new - end - end diff --git a/lib/epics/signature_algorithm.rb b/lib/epics/signature_algorithm.rb new file mode 100644 index 0000000..c79292c --- /dev/null +++ b/lib/epics/signature_algorithm.rb @@ -0,0 +1,2 @@ +module Epics::SignatureAlgorithm +end \ No newline at end of file diff --git a/lib/epics/key.rb b/lib/epics/signature_algorithm/base.rb similarity index 53% rename from lib/epics/key.rb rename to lib/epics/signature_algorithm/base.rb index a8d0f4e..9bdb62e 100644 --- a/lib/epics/key.rb +++ b/lib/epics/signature_algorithm/base.rb @@ -1,12 +1,8 @@ -class Epics::Key +class Epics::SignatureAlgorithm::Base attr_accessor :key def initialize(encoded_key, passphrase = nil) - if encoded_key.kind_of?(OpenSSL::PKey::RSA) - self.key = encoded_key - else - self.key = OpenSSL::PKey::RSA.new(encoded_key) - end + self.key = encoded_key end ### @@ -22,26 +18,22 @@ def public_digest end def n - self.key.n.to_s(16) + raise NotImplementedError end def e - self.key.e.to_s(16) + raise NotImplementedError end def sign(msg) - Base64.encode64( - key.sign_pss( - 'SHA256', - msg, - salt_length: :digest, - mgf1_hash: 'SHA256', - ), - ).gsub("\n", '') + raise NotImplementedError end - def digester - @digester ||= OpenSSL::Digest::SHA256.new + def verify(signature, msg) + raise NotImplementedError end + def digester + raise NotImplementedError + end end diff --git a/lib/epics/signature_algorithm/rsa.rb b/lib/epics/signature_algorithm/rsa.rb new file mode 100644 index 0000000..e48d4ae --- /dev/null +++ b/lib/epics/signature_algorithm/rsa.rb @@ -0,0 +1,42 @@ +class Epics::SignatureAlgorithm::Rsa < Epics::SignatureAlgorithm::Base + HASH_ALGORITHM = 'SHA256' + + def initialize(encoded_key, passphrase = nil) + if encoded_key.kind_of?(OpenSSL::PKey::RSA) + self.key = encoded_key + else + self.key = OpenSSL::PKey::RSA.new(encoded_key) + end + end + + def n + self.key.n.to_s(16) + end + + def e + self.key.e.to_s(16) + end + + def sign(msg) + key.sign( + hash_algorithm, + msg + ) + end + + def verify(signature, msg) + key.verify( + hash_algorithm, + Base64.decode64(signature), + msg + ) + end + + def hash_algorithm + HASH_ALGORITHM + end + + def digester + @digester ||= OpenSSL::Digest::SHA256.new + end +end diff --git a/lib/epics/signature_algorithm/rsapkcs1.rb b/lib/epics/signature_algorithm/rsapkcs1.rb new file mode 100644 index 0000000..85c4850 --- /dev/null +++ b/lib/epics/signature_algorithm/rsapkcs1.rb @@ -0,0 +1,2 @@ +class Epics::SignatureAlgorithm::RsaPkcs1 < Epics::SignatureAlgorithm::Rsa +end diff --git a/lib/epics/signature_algorithm/rsapss.rb b/lib/epics/signature_algorithm/rsapss.rb new file mode 100644 index 0000000..74e2bf0 --- /dev/null +++ b/lib/epics/signature_algorithm/rsapss.rb @@ -0,0 +1,24 @@ +class Epics::SignatureAlgorithm::RsaPss < Epics::SignatureAlgorithm::Rsa + def sign(msg) + key.sign_pss( + hash_algorithm, + msg, + salt_length: :digest, + mgf1_hash: mgf1_hash_algorithm, + ) + end + + def verify(signature, msg) + key.verify_pss( + hash_algorithm, + Base64.decode64(signature), + msg, + salt_length: :digest, + mgf1_hash: mgf1_hash_algorithm, + ) + end + + def mgf1_hash_algorithm + HASH_ALGORITHM + end +end diff --git a/lib/epics/signer.rb b/lib/epics/signer.rb index a17cfcb..053667d 100644 --- a/lib/epics/signer.rb +++ b/lib/epics/signer.rb @@ -7,7 +7,7 @@ def initialize(client, doc = nil) end def digest! - content_to_digest = Base64.encode64(digester.digest(doc.xpath("//*[@authenticate='true']").map(&:canonicalize).join)).strip + content_to_digest = Base64.encode64(client.authentication_key.digester.digest(doc.xpath("//*[@authenticate='true']").map(&:canonicalize).join)).strip if digest_node digest_node.content = content_to_digest @@ -20,7 +20,7 @@ def sign! signature_value_node = doc.xpath("//ds:SignatureValue").first if signature_node - signature_value_node.content = Base64.encode64(client.authentication_key.key.sign(digester, signature_node.canonicalize)).gsub(/\n/,'') + signature_value_node.content = Base64.encode64(client.authentication_key.sign(signature_node.canonicalize)).gsub(/\n/,'') end doc @@ -33,8 +33,4 @@ def digest_node def signature_node @s ||= doc.xpath("//ds:SignedInfo").first end - - def digester - OpenSSL::Digest::SHA256.new - end end diff --git a/spec/client_spec.rb b/spec/client_spec.rb index 0d30f4f..22b5e37 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -12,11 +12,11 @@ it 'holds all keys, user and bank' do expect(subject.keys).to match(a_hash_including( - "E002" => be_a(Epics::Key), - "X002" => be_a(Epics::Key), - "A006" => be_a(Epics::Key), - "SIZBN001.E002" => be_a(Epics::Key), - "SIZBN001.X002" => be_a(Epics::Key) + "E002" => be_a(Epics::SignatureAlgorithm::RsaPkcs1), + "X002" => be_a(Epics::SignatureAlgorithm::RsaPkcs1), + "A006" => be_a(Epics::SignatureAlgorithm::RsaPss), + "SIZBN001.E002" => be_a(Epics::SignatureAlgorithm::RsaPkcs1), + "SIZBN001.X002" => be_a(Epics::SignatureAlgorithm::RsaPkcs1) )) end @@ -94,7 +94,7 @@ describe '#HPB' do let(:e_key) do - Epics::Key.new(OpenSSL::PKey::RSA.new(File.read(File.join(File.dirname(__FILE__), 'fixtures', 'bank_e.pem')))) + Epics::SignatureAlgorithm::RsaPss.new(OpenSSL::PKey::RSA.new(File.read(File.join(File.dirname(__FILE__), 'fixtures', 'bank_e.pem')))) end before do @@ -103,7 +103,7 @@ .to_return(status: 200, body: File.read(File.join(File.dirname(__FILE__), 'fixtures', 'xml', 'hpb_response_ebics_ns.xml'))) end - it { expect(subject.HPB).to match([be_a(Epics::Key), be_a(Epics::Key)]) } + it { expect(subject.HPB).to match([be_a(Epics::SignatureAlgorithm::RsaPkcs1), be_a(Epics::SignatureAlgorithm::RsaPkcs1)]) } it 'changes the SIZBN001.(E|X)002 keys' do expect { subject.HPB }.to change { subject.keys["SIZBN001.E002"] } diff --git a/spec/generic_upload_spec.rb b/spec/generic_upload_spec.rb index 63620ca..99b6a23 100644 --- a/spec/generic_upload_spec.rb +++ b/spec/generic_upload_spec.rb @@ -23,15 +23,7 @@ before { allow(OpenSSL::Random).to receive(:random_bytes).with(32).and_return(Base64.strict_decode64("7wtROfiX4tyN60cygJUSsHkhzxX1RVJa8vGNYnflvKc=")) } # digest requires 32 bytes it 'will be the signed document' do - key = subject.client.signature_key.key - - verification_result = key.verify_pss( - 'SHA256', - Base64.decode64(subject.signature_value), - OpenSSL::Digest::SHA256.new.digest(document), - salt_length: :digest, - mgf1_hash: 'SHA256', - ) + verification_result = subject.client.signature_key.verify(subject.signature_value, OpenSSL::Digest::SHA256.new.digest(document)) expect(verification_result).to eq(true) end diff --git a/spec/key_spec.rb b/spec/key_spec.rb index 2fc015c..4b1b20c 100644 --- a/spec/key_spec.rb +++ b/spec/key_spec.rb @@ -1,4 +1,4 @@ -RSpec.describe Epics::Key do +RSpec.describe Epics::SignatureAlgorithm::RsaPss do subject { described_class.new( File.read(File.join( File.dirname(__FILE__), 'fixtures', 'e002.pem'))) } @@ -14,17 +14,8 @@ let(:dsi) { OpenSSL::Digest::SHA256.new.digest("ruby is great") } it 'will generated a digest that can be verified with openssl key.verify_pss' do - signed_digest = subject.sign(dsi) - - key = subject.key - - verification_result = key.verify_pss( - 'SHA256', - Base64.decode64(signed_digest), - dsi, - salt_length: :digest, - mgf1_hash: 'SHA256', - ) + signed_digest = Base64.encode64(subject.sign(dsi)).strip + verification_result = subject.verify(signed_digest, dsi) expect(verification_result).to eq(true) end diff --git a/spec/signer_spec.rb b/spec/signer_spec.rb index 8cc5824..78ca8e9 100644 --- a/spec/signer_spec.rb +++ b/spec/signer_spec.rb @@ -27,7 +27,7 @@ end it 'can be verified with the same key' do - expect(client.authentication_key.key.verify(OpenSSL::Digest::SHA256.new, Base64.decode64(subject.doc.xpath("//ds:SignatureValue").first.content), subject.signature_node.canonicalize)).to be(true) + expect(client.authentication_key.verify(subject.doc.xpath("//ds:SignatureValue").first.content, subject.signature_node.canonicalize)).to be(true) end end From d8e35b40361c868b858c1795e9e6ba41fd20bc5a Mon Sep 17 00:00:00 2001 From: Jonathan PHILIPPE Date: Thu, 26 Sep 2024 12:05:16 +0200 Subject: [PATCH 3/4] Adds h005 schematic support --- README.md | 2 +- lib/epics/client.rb | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9b5ff91..cabb05b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ EPICS is a ruby implementation of the [EBICS](https://www.ebics.org/) (Electronic Banking Internet Communication Standard). -It supports EBICS 2.4 and 2.5. +It supports EBICS 2.4, 2.5 and 3.0. The client supports the complete initialization process comprising INI, HIA and HPB including the INI letter generation. It offers support for the most common download and upload order types diff --git a/lib/epics/client.rb b/lib/epics/client.rb index 7705e11..9ed115e 100644 --- a/lib/epics/client.rb +++ b/lib/epics/client.rb @@ -11,10 +11,11 @@ class Epics::Client VERSION_H3 = 'H003' VERSION_H4 = 'H004' + VERSION_H5 = 'H005' VERSION_A5 = 'A005' VERSION_A6 = 'A006' - VERSIONS = [VERSION_H3, VERSION_H4] + VERSIONS = [VERSION_H3, VERSION_H4, VERSION_H5] USER_AGENT = "EPICS v#{Epics::VERSION}" @@ -44,7 +45,7 @@ def urn_schema case version when VERSION_H3 "http://www.ebics.org/#{version}" - when VERSION_H4 + when VERSION_H4, VERSION_H5 "urn:org:ebics:#{version}" end end From 6ea5cbcc5ca7154b40f8e82b52ee757e9f3868ab Mon Sep 17 00:00:00 2001 From: Jonathan PHILIPPE Date: Tue, 1 Oct 2024 14:21:45 +0200 Subject: [PATCH 4/4] Fixes after code review --- lib/epics/header_request.rb | 14 ++++---------- .../rsa_pss_spec.rb} | 0 2 files changed, 4 insertions(+), 10 deletions(-) rename spec/{key_spec.rb => signature_algorithm/rsa_pss_spec.rb} (100%) diff --git a/lib/epics/header_request.rb b/lib/epics/header_request.rb index cdf62af..da8573b 100644 --- a/lib/epics/header_request.rb +++ b/lib/epics/header_request.rb @@ -2,8 +2,6 @@ class Epics::HeaderRequest extend Forwardable attr_accessor :client - BASE36_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' - PRODUCT_NAME = 'EPICS - a ruby ebics kernel' PRODUCT_LANG = 'de' @@ -15,6 +13,7 @@ def initialize(client) def build(options = {}) options[:with_bank_pubkey_digests] = true if options[:with_bank_pubkey_digests].nil? + options[:security_medium] = 0 if options[:security_medium].nil? Nokogiri::XML::Builder.new do |xml| xml.header(authenticate: true) { @@ -27,7 +26,7 @@ def build(options = {}) xml.Product(PRODUCT_NAME, 'Language' => PRODUCT_LANG) xml.OrderDetails { xml.OrderType options[:order_type] - xml.OrderID b36encode(client.next_order_id).rjust(4, '0') if client.version == Epics::Client::VERSION_H3 + xml.OrderID b36encode(client.next_order_id) if client.version == Epics::Client::VERSION_H3 xml.OrderAttribute options[:order_attribute] xml.StandardOrderParams { build_attributes(xml, options[:order_params]) @@ -37,7 +36,7 @@ def build(options = {}) xml.Authentication(client.bank_authentication_key.public_digest, Version: client.authentication_version, Algorithm: 'http://www.w3.org/2001/04/xmlenc#sha256') xml.Encryption(client.bank_encryption_key.public_digest, Version: client.encryption_version, Algorithm: 'http://www.w3.org/2001/04/xmlenc#sha256') } if options[:with_bank_pubkey_digests] - xml.SecurityMedium '0000' + xml.SecurityMedium b36encode(options[:security_medium]) if options[:security_medium] xml.NumSegments options[:num_segments] if options[:num_segments] } xml.mutable { @@ -62,11 +61,6 @@ def build_attributes(xml, attributes) end def b36encode(number) - str = '' - while number > 0 - number, i = number.divmod(36) - str += BASE36_ALPHABET[i] - end - str.empty? ? '0' : str + number.to_s(36).upcase.rjust(4, '0') end end diff --git a/spec/key_spec.rb b/spec/signature_algorithm/rsa_pss_spec.rb similarity index 100% rename from spec/key_spec.rb rename to spec/signature_algorithm/rsa_pss_spec.rb