epics 2.8.0 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f97fb2e1d069feca084d74aff47a5a55a4f0f8d6aa2865567544450f68cf35c8
4
- data.tar.gz: 4028071efc11da0c35e2c4e1ca9997512538083fbbfeebd6eb00ce985ea76f45
3
+ metadata.gz: 33e735bd9012ec4df1bc24d5f1621345adfa6aeba90dbf5c3adcc9be9673c696
4
+ data.tar.gz: f8c5b93115aeb9b60d3be9cbcd36251b6046f5093ed92482c2b4b1d8d4555e90
5
5
  SHA512:
6
- metadata.gz: 90c7e7bf8b9dd58f367a432eb8b1beec2b3faff2bde2f186575555b4098e6db494769ba0f567e2c419bb0387344d2bf6a10f6201544a2e15f09eb583bc89fe70
7
- data.tar.gz: 4b9e25b55194e4a299717d8d9cd6f1296d6e0298b20e4d405518488e90d90ed9c3e5e92c9361e00e51fcb6759d366a5100d629f4a1c582f4ab242471363f9009
6
+ metadata.gz: 6021ed35c892dc544032a95e89ef5a8361f27ea022a5a2776dfea3adea1e7ebbbb2642e145a0c4efb01157c228953c171a511747d21087e520a1b59411f82f81
7
+ data.tar.gz: 71ef90f98028d9e4bff31aa077d52fdbd7cadc12e8126792f7e71bb6e00b4b409a1ad9e36b54e2e345115e602734a7ddaaa26f3b214e54503055929a5600f114
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  ### Unreleased
2
2
 
3
+ ### 2.10.0
4
+
5
+ - [ENHANCEMENT] Added X.509 certificates support for INI and HIA (thanks to @vnoviskyi)
6
+ - [ENHANCEMENT] Added Z01 order type (thanks to @Nymuxyzo)
7
+
8
+ ### 2.9.0
9
+
10
+ - [ENHANCEMENT] Added HEV order type (thanks to @jplot)
11
+
3
12
  ### 2.8.0
4
13
 
5
14
  - [ENHANCEMENT] Added BKA order type
data/README.md CHANGED
@@ -41,7 +41,7 @@ Once the paperwork is done, your bank should provide you with:
41
41
  Take these parameters and start setting up an UserID (repeat this for every user you want to initialize):
42
42
 
43
43
  ```ruby
44
- e = Epics::Client.setup("my-super-secret", "https://ebics.sandbox", "EBICS_HOST_ID", "EBICS_USER_ID", "EBICS_PARTNER_ID")
44
+ e = Epics::Client.setup("my-super-secret", "https://ebics.sandbox", "EBICS_HOST_ID", "EBICS_USER_ID", "EBICS_PARTNER_ID", 4096)
45
45
  ```
46
46
 
47
47
  To use the keys later, just store them in a file
@@ -200,6 +200,76 @@ that are hiding some strange names from you:
200
200
  If you need more sophisticated EBICS order types, please read the next section
201
201
  about the supported functionalities.
202
202
 
203
+ ### Using X.509 Certificates
204
+
205
+ Epics supports using X.509 self-signed certificates for INI and HIA requests, as required by some banks. This is in addition to the classic key-based workflow.
206
+
207
+ #### When to Use
208
+
209
+ Some banks require X.509 certificates for EBICS initialization (INI/HIA).
210
+
211
+ You can generate your own X.509 certificate using Ruby’s OpenSSL library:
212
+
213
+ This examples showcases the generation of the X.509 certificate A file and can be applied the same way for the others.
214
+ ```ruby
215
+ key = client.a.key # or e key, or x key
216
+ name = OpenSSL::X509::Name.parse('/CN=Test Certificate/O=MyOrg/C=DE')
217
+ cert = OpenSSL::X509::Certificate.new
218
+ cert.version = 2
219
+ cert.serial = SecureRandom.random_number(2**64)
220
+ cert.subject = name
221
+ cert.issuer = name
222
+ cert.public_key = key.public_key
223
+ cert.not_before = Time.current
224
+ cert.not_after = cert.not_before + 1.year
225
+
226
+ ef = OpenSSL::X509::ExtensionFactory.new
227
+ ef.subject_certificate = cert
228
+ ef.issuer_certificate = cert
229
+ cert.add_extension(ef.create_extension('basicConstraints', 'CA:FALSE', true))
230
+ cert.add_extension(ef.create_extension('keyUsage', 'digitalSignature,nonRepudiation,keyEncipherment', true))
231
+
232
+ cert.sign(key, OpenSSL::Digest.new('SHA256'))
233
+ cert
234
+
235
+ # Save to file
236
+ File.write("cert_a.pem", cert.to_pem)
237
+ ```
238
+ You can now use the contents of the generated certificate file in PEM format as your
239
+ `x_509_certificate_a_content`, `x_509_certificate_x_content`, or `x_509_certificate_e_content`
240
+ in the client initialization.
241
+
242
+ **Note:** For production environments, your bank may require certificates issued by a trusted authority. Be sure to confirm your bank’s requirements before proceeding.
243
+
244
+ #### Initializing the Client with X.509 Certificates
245
+ ```ruby
246
+ # Load your certificate data (PEM or DER encoded)
247
+ certificate_a = File.read("cert_a.pem")
248
+ certificate_x = File.read("cert_x.pem")
249
+ certificate_e = File.read("cert_e.pem")
250
+
251
+ client = Epics::Client.new(
252
+ keys, # your key data as before
253
+ 'passphrase',
254
+ 'url',
255
+ 'host',
256
+ 'user',
257
+ 'partner',
258
+ x_509_certificate_a_content: certificate_a,
259
+ x_509_certificate_x_content: certificate_x,
260
+ x_509_certificate_e_content: certificate_e,
261
+ debug_mode: true # Optional: enables verbose logging of EBICS requests/responses
262
+ )
263
+ ```
264
+ ### Example: Generating the Initialization Letter with Certificates
265
+
266
+ ```ruby
267
+ renderer = Epics::LetterRenderer.new(client)
268
+ letter = renderer.render("Your Bank Name")
269
+ File.write("initialization_letter.txt", letter)
270
+ ```
271
+ If all three certificates are present, the INI letter will use certificate hashes as required for certificate-based registration.
272
+
203
273
  ## Issues and Feature Requests
204
274
 
205
275
  [Railslove](http://railslove.com) is commited to provide the best developer tools for integrating
@@ -258,8 +328,8 @@ EPICS_VERIFY_SSL=false
258
328
  ## Contributing
259
329
  Railslove has a [Contributor License Agreement (CLA)](https://github.com/railslove/epics/blob/master/CONTRIBUTING.md) which clarifies the intellectual property rights for contributions from individuals or entities. To ensure every developer has signed the CLA, we use [CLA Assistant](https://cla-assistant.io/).
260
330
 
261
- After checking out the repo, run `bin/setup` to install dependencies.
262
- Then, run `rspec` to run the tests.
331
+ After checking out the repo, run `bin/setup` to install dependencies.
332
+ Then, run `rspec` to run the tests.
263
333
  You can also run `bin/console` for an interactive prompt that will allow you to experiment.
264
334
 
265
335
  0. Contact team@railslove.com for information about the CLA
data/lib/epics/client.rb CHANGED
@@ -1,12 +1,14 @@
1
1
  class Epics::Client
2
2
  extend Forwardable
3
3
 
4
- attr_accessor :passphrase, :url, :host_id, :user_id, :partner_id, :keys, :keys_content, :locale, :product_name
5
- attr_writer :iban, :bic, :name
4
+ attr_accessor :passphrase, :url, :host_id, :user_id, :partner_id, :keys, :keys_content, :locale, :product_name,
5
+ :x_509_certificates_content, :debug_mode
6
6
 
7
+ attr_writer :iban, :bic, :name
8
+
7
9
  def_delegators :connection, :post
8
-
9
- def initialize(keys_content, passphrase, url, host_id, user_id, partner_id, locale: Epics::DEFAULT_LOCALE, product_name: Epics::DEFAULT_PRODUCT_NAME)
10
+
11
+ def initialize(keys_content, passphrase, url, host_id, user_id, partner_id, options = {})
10
12
  self.keys_content = keys_content.respond_to?(:read) ? keys_content.read : keys_content if keys_content
11
13
  self.passphrase = passphrase
12
14
  self.keys = extract_keys if keys_content
@@ -14,8 +16,14 @@ class Epics::Client
14
16
  self.host_id = host_id
15
17
  self.user_id = user_id
16
18
  self.partner_id = partner_id
17
- self.locale = locale
18
- self.product_name = product_name
19
+ self.locale = options[:locale] || Epics::DEFAULT_LOCALE
20
+ self.product_name = options[:product_name] || Epics::DEFAULT_PRODUCT_NAME
21
+ self.debug_mode = !!options[:debug_mode]
22
+ self.x_509_certificates_content = {
23
+ a: options[:x_509_certificate_a_content],
24
+ x: options[:x_509_certificate_x_content],
25
+ e: options[:x_509_certificate_e_content]
26
+ }
19
27
  end
20
28
 
21
29
  def inspect
@@ -61,8 +69,8 @@ class Epics::Client
61
69
  @order_types ||= (self.HTD; @order_types)
62
70
  end
63
71
 
64
- def self.setup(passphrase, url, host_id, user_id, partner_id, keysize = 2048)
65
- client = new(nil, passphrase, url, host_id, user_id, partner_id)
72
+ def self.setup(passphrase, url, host_id, user_id, partner_id, keysize = 2048, options = {})
73
+ client = new(nil, passphrase, url, host_id, user_id, partner_id, options)
66
74
  client.keys = %w(A006 X002 E002).each_with_object({}) do |type, memo|
67
75
  memo[type] = Epics::Key.new( OpenSSL::PKey::RSA.generate(keysize) )
68
76
  end
@@ -107,6 +115,13 @@ class Epics::Client
107
115
  post(url, Epics::INI.new(self).to_xml).body.ok?
108
116
  end
109
117
 
118
+ def HEV
119
+ res = post(url, Epics::HEV.new(self).to_xml).body
120
+ res.doc.xpath("//xmlns:VersionNumber", xmlns: 'http://www.ebics.org/H000').each_with_object({}) do |node, versions|
121
+ versions[node['ProtocolVersion']] = node.content
122
+ end
123
+ end
124
+
110
125
  def HPB
111
126
  Nokogiri::XML(download(Epics::HPB)).xpath("//xmlns:PubKeyValue", xmlns: "urn:org:ebics:H004").each do |node|
112
127
  type = node.parent.last_element_child.content
@@ -218,6 +233,10 @@ class Epics::Client
218
233
  download_and_unzip(Epics::C5N, from: from, to: to)
219
234
  end
220
235
 
236
+ def Z01(from, to)
237
+ download_and_unzip(Epics::Z01, from: from, to: to)
238
+ end
239
+
221
240
  def Z52(from, to)
222
241
  download_and_unzip(Epics::Z52, from: from, to: to)
223
242
  end
@@ -266,6 +285,19 @@ class Epics::Client
266
285
  def save_keys(path)
267
286
  File.write(path, dump_keys)
268
287
  end
288
+
289
+ def x_509_certificate(type)
290
+ content = x_509_certificates_content[type.to_sym]
291
+ return if content.nil? || content.empty?
292
+ Epics::X509Certificate.new(content)
293
+ end
294
+
295
+ def x_509_certificate_hash(type)
296
+ content = x_509_certificates_content[type.to_sym]
297
+ return if content.nil? || content.empty?
298
+ cert = OpenSSL::X509::Certificate.new(content)
299
+ Digest::SHA256.hexdigest(cert.to_der).upcase
300
+ end
269
301
 
270
302
  private
271
303
 
@@ -306,7 +338,7 @@ class Epics::Client
306
338
  faraday.use Epics::XMLSIG, { client: self }
307
339
  faraday.use Epics::ParseEbics, { client: self}
308
340
  # faraday.use MyAdapter
309
- # faraday.response :logger # log requests to STDOUT
341
+ faraday.response :logger, ::Logger.new(STDOUT), bodies: true if debug_mode # log requests/response to STDOUT
310
342
  end
311
343
  end
312
344
 
@@ -105,4 +105,18 @@ class Epics::GenericRequest
105
105
  }
106
106
  end.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML, encoding: 'utf-8')
107
107
  end
108
+
109
+ private
110
+
111
+ def x509_data_xml(xml, x_509_certificate)
112
+ return unless x_509_certificate
113
+
114
+ xml.send('ds:X509Data') do
115
+ xml.send('ds:X509IssuerSerial') do
116
+ xml.send('ds:X509IssuerName', x_509_certificate.issuer)
117
+ xml.send('ds:X509SerialNumber', x_509_certificate.version)
118
+ end
119
+ xml.send('ds:X509Certificate', x_509_certificate.data)
120
+ end
121
+ end
108
122
  end
data/lib/epics/hev.rb ADDED
@@ -0,0 +1,19 @@
1
+ class Epics::HEV < Epics::GenericRequest
2
+ def root
3
+ "ebicsHEVRequest"
4
+ end
5
+
6
+ def body
7
+ Nokogiri::XML::Builder.new do |xml|
8
+ xml.HostID host_id
9
+ end.doc.root
10
+ end
11
+
12
+ def to_xml
13
+ Nokogiri::XML::Builder.new do |xml|
14
+ xml.send(root, 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xsi:schemaLocation' => 'http://www.ebics.org/H000 http://www.ebics.org/H000/ebics_hev.xsd', 'xmlns' => 'http://www.ebics.org/H000') {
15
+ xml.parent.add_child(body)
16
+ }
17
+ end.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML, encoding: 'utf-8')
18
+ end
19
+ end
data/lib/epics/hia.rb CHANGED
@@ -26,6 +26,7 @@ class Epics::HIA < Epics::GenericRequest
26
26
  Nokogiri::XML::Builder.new do |xml|
27
27
  xml.HIARequestOrderData('xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#', 'xmlns' => 'urn:org:ebics:H004') {
28
28
  xml.AuthenticationPubKeyInfo {
29
+ x509_data_xml(xml, client.x_509_certificate(:x))
29
30
  xml.PubKeyValue {
30
31
  xml.send('ds:RSAKeyValue') {
31
32
  xml.send('ds:Modulus', Base64.strict_encode64([client.x.n].pack("H*")))
@@ -35,6 +36,7 @@ class Epics::HIA < Epics::GenericRequest
35
36
  xml.AuthenticationVersion 'X002'
36
37
  }
37
38
  xml.EncryptionPubKeyInfo{
39
+ x509_data_xml(xml, client.x_509_certificate(:e))
38
40
  xml.PubKeyValue {
39
41
  xml.send('ds:RSAKeyValue') {
40
42
  xml.send('ds:Modulus', Base64.strict_encode64([client.e.n].pack("H*")))
data/lib/epics/ini.rb CHANGED
@@ -26,6 +26,7 @@ class Epics::INI < Epics::GenericRequest
26
26
  Nokogiri::XML::Builder.new do |xml|
27
27
  xml.SignaturePubKeyOrderData('xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#', 'xmlns' => 'http://www.ebics.org/S001') {
28
28
  xml.SignaturePubKeyInfo {
29
+ x509_data_xml(xml, client.x_509_certificate(:a))
29
30
  xml.PubKeyValue {
30
31
  xml.send('ds:RSAKeyValue') {
31
32
  xml.send('ds:Modulus', Base64.strict_encode64([client.a.n].pack("H*")))
@@ -1,7 +1,6 @@
1
1
  class Epics::LetterRenderer
2
2
  extend Forwardable
3
3
 
4
- TEMPLATE_PATH = File.join(File.dirname(__FILE__), '../letter/', 'ini.erb')
5
4
  I18N_SCOPE = 'epics.letter'
6
5
 
7
6
  def initialize(client)
@@ -12,11 +11,32 @@ class Epics::LetterRenderer
12
11
  I18n.translate(key, **{ locale: @client.locale, scope: I18N_SCOPE }.merge(options))
13
12
  end
14
13
 
15
- alias_method :t, :translate
14
+ alias t translate
16
15
 
17
16
  def_delegators :@client, :host_id, :user_id, :partner_id, :a, :x, :e
18
17
 
19
18
  def render(bankname)
20
- ERB.new(File.read(TEMPLATE_PATH)).result(binding)
19
+ template_path = File.join(File.dirname(__FILE__), '../letter/', template_filename)
20
+ ERB.new(File.read(template_path)).result(binding)
21
+ end
22
+
23
+ def template_filename
24
+ use_x_509_certificate_template? ? 'ini_with_certs.erb' : 'ini.erb'
25
+ end
26
+
27
+ def use_x_509_certificate_template?
28
+ x_509_certificate_a_hash && x_509_certificate_x_hash && x_509_certificate_e_hash
29
+ end
30
+
31
+ def x_509_certificate_a_hash
32
+ @client.x_509_certificate_hash(:a)
33
+ end
34
+
35
+ def x_509_certificate_x_hash
36
+ @client.x_509_certificate_hash(:x)
37
+ end
38
+
39
+ def x_509_certificate_e_hash
40
+ @client.x_509_certificate_hash(:e)
21
41
  end
22
42
  end
@@ -12,9 +12,17 @@ class Epics::Response
12
12
  end
13
13
 
14
14
  def technical_code
15
+ mutable_return_code.empty? ? system_return_code : mutable_return_code
16
+ end
17
+
18
+ def mutable_return_code
15
19
  doc.xpath("//xmlns:header/xmlns:mutable/xmlns:ReturnCode", xmlns: "urn:org:ebics:H004").text
16
20
  end
17
21
 
22
+ def system_return_code
23
+ doc.xpath("//xmlns:SystemReturnCode/xmlns:ReturnCode", xmlns: 'http://www.ebics.org/H000').text
24
+ end
25
+
18
26
  def business_error?
19
27
  !["", "000000"].include?(business_code)
20
28
  end
data/lib/epics/signer.rb CHANGED
@@ -17,7 +17,7 @@ class Epics::Signer
17
17
  end
18
18
 
19
19
  def sign!
20
- signature_value_node = doc.xpath("//ds:SignatureValue").first
20
+ signature_value_node = doc.xpath("//ds:SignatureValue", ds: "http://www.w3.org/2000/09/xmldsig#").first
21
21
 
22
22
  if signature_node
23
23
  signature_value_node.content = Base64.encode64(client.x.key.sign(digester, signature_node.canonicalize)).gsub(/\n/,'')
@@ -27,11 +27,11 @@ class Epics::Signer
27
27
  end
28
28
 
29
29
  def digest_node
30
- @d ||= doc.xpath("//ds:DigestValue").first
30
+ @d ||= doc.xpath("//ds:DigestValue", ds: "http://www.w3.org/2000/09/xmldsig#").first
31
31
  end
32
32
 
33
33
  def signature_node
34
- @s ||= doc.xpath("//ds:SignedInfo").first
34
+ @s ||= doc.xpath("//ds:SignedInfo", ds: "http://www.w3.org/2000/09/xmldsig#").first
35
35
  end
36
36
 
37
37
  def digester
data/lib/epics/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Epics
4
- VERSION = '2.8.0'
4
+ VERSION = '2.10.0'
5
5
  end
@@ -0,0 +1,15 @@
1
+ class Epics::X509Certificate
2
+ extend Forwardable
3
+
4
+ attr_reader :certificate
5
+
6
+ def_delegators :certificate, :issuer, :version
7
+
8
+ def initialize(crt_content)
9
+ @certificate = OpenSSL::X509::Certificate.new(crt_content)
10
+ end
11
+
12
+ def data
13
+ Base64.strict_encode64(@certificate.to_der)
14
+ end
15
+ end
data/lib/epics/z01.rb ADDED
@@ -0,0 +1,17 @@
1
+ class Epics::Z01 < Epics::GenericRequest
2
+ def header
3
+ client.header_request.build(
4
+ nonce: nonce,
5
+ timestamp: timestamp,
6
+ order_type: 'Z01',
7
+ order_attribute: 'DZHNN',
8
+ order_params: {
9
+ DateRange: {
10
+ Start: options[:from],
11
+ End: options[:to]
12
+ }
13
+ },
14
+ mutable: { TransactionPhase: 'Initialisation' }
15
+ )
16
+ end
17
+ end
data/lib/epics.rb CHANGED
@@ -32,6 +32,7 @@ require "epics/c52"
32
32
  require "epics/c53"
33
33
  require "epics/c54"
34
34
  require "epics/c5n"
35
+ require "epics/z01"
35
36
  require "epics/z52"
36
37
  require "epics/z53"
37
38
  require "epics/z54"
@@ -56,7 +57,9 @@ require "epics/crz"
56
57
  require "epics/xct"
57
58
  require "epics/hia"
58
59
  require "epics/ini"
60
+ require "epics/hev"
59
61
  require "epics/signer"
62
+ require "epics/x_509_certificate"
60
63
  require "epics/client"
61
64
 
62
65
  I18n.load_path += Dir[File.join(File.dirname(__FILE__), 'letter/locales', '*.yml')]