epics 2.9.0 → 2.11.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 +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +74 -4
- data/lib/epics/client.rb +38 -9
- data/lib/epics/ful.rb +18 -0
- data/lib/epics/generic_request.rb +14 -0
- data/lib/epics/hia.rb +2 -0
- data/lib/epics/ini.rb +1 -0
- data/lib/epics/letter_renderer.rb +23 -3
- data/lib/epics/version.rb +1 -1
- data/lib/epics/x_509_certificate.rb +15 -0
- data/lib/epics/z01.rb +17 -0
- data/lib/epics.rb +3 -0
- data/lib/letter/ini_with_certs.erb +335 -0
- data/lib/letter/locales/de.yml +1 -0
- data/lib/letter/locales/en.yml +1 -0
- data/lib/letter/locales/fr.yml +1 -0
- data/spec/client_spec.rb +150 -58
- data/spec/orders/ful_spec.rb +22 -0
- data/spec/orders/hia_spec.rb +31 -7
- data/spec/orders/ini_spec.rb +27 -8
- data/spec/orders/x_509_certificate_spec.rb +26 -0
- data/spec/orders/z01_spec.rb +11 -0
- data/spec/support/x_509_crt_generator.rb +22 -0
- metadata +15 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1a9354e4e83108afe41607057bd8ce1fcba27fc03476a521269911f1f1fea57f
|
4
|
+
data.tar.gz: 82333eed938cdb0a31fcb54b206a6c5653eef950ee7b120f9487de012e4b22f0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9d74d5100f8698e179df3d9b7eb3f3b3154d64313b975e6645b99ebdfd389e6b1695021640f39b753c685d47055216bf2ecda404c01b1c32f049f16b4caae769
|
7
|
+
data.tar.gz: 3d79c5800b76e7dfbe259e27e8ae1d1f14a77dc99a5b676603bd184a3cff5d3780682f13b14c796a0f92182805cec119ddf4dd585977b4b81f2c0195df00cd2d
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,14 @@
|
|
1
1
|
### Unreleased
|
2
2
|
|
3
|
+
### 2.11.0
|
4
|
+
|
5
|
+
- [ENHANCEMENT] Added FUL order type (thanks to @scollon-pl)
|
6
|
+
|
7
|
+
### 2.10.0
|
8
|
+
|
9
|
+
- [ENHANCEMENT] Added X.509 certificates support for INI and HIA (thanks to @vnoviskyi)
|
10
|
+
- [ENHANCEMENT] Added Z01 order type (thanks to @Nymuxyzo)
|
11
|
+
|
3
12
|
### 2.9.0
|
4
13
|
|
5
14
|
- [ENHANCEMENT] Added HEV order type (thanks to @jplot)
|
data/README.md
CHANGED
@@ -10,7 +10,7 @@ It supports EBICS 2.5.
|
|
10
10
|
|
11
11
|
The client supports the complete initialization process comprising INI, HIA and HPB including the
|
12
12
|
INI letter generation. It offers support for the most common download and upload order types
|
13
|
-
(STA HAA HTD HPD PTK HAC HKD BKA C52 C53 C54 CD1 CDB CDD CCT VMK FDL).
|
13
|
+
(STA HAA HTD HPD PTK HAC HKD BKA C52 C53 C54 CD1 CDB CDD CCT VMK FDL FUL).
|
14
14
|
|
15
15
|
## Installation
|
16
16
|
|
@@ -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
|
-
|
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,
|
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
|
@@ -185,6 +193,10 @@ class Epics::Client
|
|
185
193
|
upload(Epics::XCT, document)
|
186
194
|
end
|
187
195
|
|
196
|
+
def FUL(document)
|
197
|
+
upload(Epics::FUL, document)
|
198
|
+
end
|
199
|
+
|
188
200
|
def STA(from = nil, to = nil)
|
189
201
|
download(Epics::STA, from: from, to: to)
|
190
202
|
end
|
@@ -225,6 +237,10 @@ class Epics::Client
|
|
225
237
|
download_and_unzip(Epics::C5N, from: from, to: to)
|
226
238
|
end
|
227
239
|
|
240
|
+
def Z01(from, to)
|
241
|
+
download_and_unzip(Epics::Z01, from: from, to: to)
|
242
|
+
end
|
243
|
+
|
228
244
|
def Z52(from, to)
|
229
245
|
download_and_unzip(Epics::Z52, from: from, to: to)
|
230
246
|
end
|
@@ -273,6 +289,19 @@ class Epics::Client
|
|
273
289
|
def save_keys(path)
|
274
290
|
File.write(path, dump_keys)
|
275
291
|
end
|
292
|
+
|
293
|
+
def x_509_certificate(type)
|
294
|
+
content = x_509_certificates_content[type.to_sym]
|
295
|
+
return if content.nil? || content.empty?
|
296
|
+
Epics::X509Certificate.new(content)
|
297
|
+
end
|
298
|
+
|
299
|
+
def x_509_certificate_hash(type)
|
300
|
+
content = x_509_certificates_content[type.to_sym]
|
301
|
+
return if content.nil? || content.empty?
|
302
|
+
cert = OpenSSL::X509::Certificate.new(content)
|
303
|
+
Digest::SHA256.hexdigest(cert.to_der).upcase
|
304
|
+
end
|
276
305
|
|
277
306
|
private
|
278
307
|
|
@@ -313,7 +342,7 @@ class Epics::Client
|
|
313
342
|
faraday.use Epics::XMLSIG, { client: self }
|
314
343
|
faraday.use Epics::ParseEbics, { client: self}
|
315
344
|
# faraday.use MyAdapter
|
316
|
-
|
345
|
+
faraday.response :logger, ::Logger.new(STDOUT), bodies: true if debug_mode # log requests/response to STDOUT
|
317
346
|
end
|
318
347
|
end
|
319
348
|
|
data/lib/epics/ful.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Epics::FUL < Epics::GenericUploadRequest
|
4
|
+
def header
|
5
|
+
ful_order_params = Hash.new.tap do |params|
|
6
|
+
params[:FileFormat] = options[:file_format]
|
7
|
+
end
|
8
|
+
|
9
|
+
client.header_request.build(
|
10
|
+
nonce: nonce,
|
11
|
+
timestamp: timestamp,
|
12
|
+
order_type: 'FUL',
|
13
|
+
order_attribute: 'DZHNN',
|
14
|
+
custom_order_params: { FULOrderParams: ful_order_params },
|
15
|
+
mutable: { TransactionPhase: 'Initialisation' }
|
16
|
+
)
|
17
|
+
end
|
18
|
+
end
|
@@ -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/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
|
-
|
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
|
-
|
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
|
data/lib/epics/version.rb
CHANGED
@@ -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
@@ -26,12 +26,14 @@ require "epics/htd"
|
|
26
26
|
require "epics/haa"
|
27
27
|
require "epics/sta"
|
28
28
|
require "epics/fdl"
|
29
|
+
require "epics/ful"
|
29
30
|
require "epics/vmk"
|
30
31
|
require "epics/bka"
|
31
32
|
require "epics/c52"
|
32
33
|
require "epics/c53"
|
33
34
|
require "epics/c54"
|
34
35
|
require "epics/c5n"
|
36
|
+
require "epics/z01"
|
35
37
|
require "epics/z52"
|
36
38
|
require "epics/z53"
|
37
39
|
require "epics/z54"
|
@@ -58,6 +60,7 @@ require "epics/hia"
|
|
58
60
|
require "epics/ini"
|
59
61
|
require "epics/hev"
|
60
62
|
require "epics/signer"
|
63
|
+
require "epics/x_509_certificate"
|
61
64
|
require "epics/client"
|
62
65
|
|
63
66
|
I18n.load_path += Dir[File.join(File.dirname(__FILE__), 'letter/locales', '*.yml')]
|