epics 2.10.0 → 3.0.0.rc1

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.
Files changed (245) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/.semaphore/semaphore.yml +17 -8
  4. data/CHANGELOG.md +97 -0
  5. data/EBICS_REFERENCES.md +169 -0
  6. data/README.md +219 -50
  7. data/lib/epics/azv.rb +3 -10
  8. data/lib/epics/b2b.rb +3 -10
  9. data/lib/epics/bka.rb +3 -14
  10. data/lib/epics/builders/body_builder/base.rb +26 -0
  11. data/lib/epics/builders/body_builder/v2.rb +9 -0
  12. data/lib/epics/builders/body_builder/v3.rb +9 -0
  13. data/lib/epics/builders/body_builder.rb +6 -0
  14. data/lib/epics/builders/data_encryption_info_builder.rb +29 -0
  15. data/lib/epics/builders/data_transfer_builder/base.rb +52 -0
  16. data/lib/epics/builders/data_transfer_builder/v2.rb +9 -0
  17. data/lib/epics/builders/data_transfer_builder/v3.rb +12 -0
  18. data/lib/epics/builders/data_transfer_builder.rb +6 -0
  19. data/lib/epics/builders/header_builder/base.rb +26 -0
  20. data/lib/epics/builders/header_builder/v2.rb +9 -0
  21. data/lib/epics/builders/header_builder/v3.rb +9 -0
  22. data/lib/epics/builders/header_builder.rb +6 -0
  23. data/lib/epics/builders/mutable_builder.rb +28 -0
  24. data/lib/epics/builders/order_details_builder/base.rb +100 -0
  25. data/lib/epics/builders/order_details_builder/v2.rb +21 -0
  26. data/lib/epics/builders/order_details_builder/v3.rb +51 -0
  27. data/lib/epics/builders/order_details_builder.rb +10 -0
  28. data/lib/epics/builders/request_builder.rb +25 -0
  29. data/lib/epics/builders/static_builder/base.rb +75 -0
  30. data/lib/epics/builders/static_builder/v2.rb +9 -0
  31. data/lib/epics/builders/static_builder/v3.rb +9 -0
  32. data/lib/epics/builders/static_builder.rb +9 -0
  33. data/lib/epics/builders/transfer_receipt_builder.rb +19 -0
  34. data/lib/epics/builders/xml_builder/base.rb +92 -0
  35. data/lib/epics/builders/xml_builder/v24.rb +31 -0
  36. data/lib/epics/builders/xml_builder/v25.rb +31 -0
  37. data/lib/epics/builders/xml_builder/v3.rb +31 -0
  38. data/lib/epics/builders/xml_builder.rb +7 -0
  39. data/lib/epics/builders.rb +13 -0
  40. data/lib/epics/c2s.rb +3 -10
  41. data/lib/epics/c52.rb +2 -14
  42. data/lib/epics/c53.rb +2 -14
  43. data/lib/epics/c54.rb +2 -14
  44. data/lib/epics/c5n.rb +3 -14
  45. data/lib/epics/ccs.rb +3 -10
  46. data/lib/epics/cct.rb +3 -10
  47. data/lib/epics/cd1.rb +3 -10
  48. data/lib/epics/cdb.rb +3 -10
  49. data/lib/epics/cdd.rb +3 -10
  50. data/lib/epics/cds.rb +3 -10
  51. data/lib/epics/cdz.rb +3 -14
  52. data/lib/epics/cip.rb +3 -10
  53. data/lib/epics/client.rb +491 -123
  54. data/lib/epics/crypt/aes.rb +56 -0
  55. data/lib/epics/crypt/x_509.rb +52 -0
  56. data/lib/epics/crypt.rb +5 -0
  57. data/lib/epics/crz.rb +3 -14
  58. data/lib/epics/factories/crypt/aes_factory.rb +5 -0
  59. data/lib/epics/factories/crypt.rb +4 -0
  60. data/lib/epics/factories/request_factory/base.rb +236 -0
  61. data/lib/epics/factories/request_factory/v2.rb +194 -0
  62. data/lib/epics/factories/request_factory/v24.rb +32 -0
  63. data/lib/epics/factories/request_factory/v25.rb +31 -0
  64. data/lib/epics/factories/request_factory/v3.rb +305 -0
  65. data/lib/epics/factories/request_factory.rb +8 -0
  66. data/lib/epics/factories.rb +5 -0
  67. data/lib/epics/fdl.rb +3 -18
  68. data/lib/epics/ful.rb +6 -0
  69. data/lib/epics/generic_request.rb +31 -92
  70. data/lib/epics/generic_upload_request.rb +14 -67
  71. data/lib/epics/haa.rb +3 -9
  72. data/lib/epics/hac.rb +3 -14
  73. data/lib/epics/handlers/auth_signature_handler.rb +102 -0
  74. data/lib/epics/handlers/order_data_handler/base.rb +98 -0
  75. data/lib/epics/handlers/order_data_handler/v2.rb +39 -0
  76. data/lib/epics/handlers/order_data_handler/v24.rb +11 -0
  77. data/lib/epics/handlers/order_data_handler/v25.rb +11 -0
  78. data/lib/epics/handlers/order_data_handler/v3.rb +41 -0
  79. data/lib/epics/handlers/order_data_handler.rb +8 -0
  80. data/lib/epics/handlers/user_signature_handler/base.rb +10 -0
  81. data/lib/epics/handlers/user_signature_handler/v2.rb +17 -0
  82. data/lib/epics/handlers/user_signature_handler/v3.rb +17 -0
  83. data/lib/epics/handlers/user_signature_handler.rb +6 -0
  84. data/lib/epics/handlers.rb +6 -0
  85. data/lib/epics/hev.rb +2 -15
  86. data/lib/epics/hia.rb +4 -55
  87. data/lib/epics/hkd.rb +3 -9
  88. data/lib/epics/hpb.rb +3 -13
  89. data/lib/epics/hpd.rb +3 -9
  90. data/lib/epics/htd.rb +3 -9
  91. data/lib/epics/ini.rb +4 -46
  92. data/lib/epics/keyring.rb +17 -0
  93. data/lib/epics/letter_renderer.rb +17 -22
  94. data/lib/epics/ptk.rb +3 -14
  95. data/lib/epics/response.rb +34 -29
  96. data/lib/epics/services/crypt_service.rb +62 -0
  97. data/lib/epics/services/digest_resolver/base.rb +30 -0
  98. data/lib/epics/services/digest_resolver/v2.rb +13 -0
  99. data/lib/epics/services/digest_resolver/v3.rb +19 -0
  100. data/lib/epics/services/digest_resolver.rb +6 -0
  101. data/lib/epics/services.rb +5 -0
  102. data/lib/epics/signature.rb +50 -0
  103. data/lib/epics/{key.rb → signature_algorithm/base.rb} +14 -21
  104. data/lib/epics/signature_algorithm/rsa.rb +50 -0
  105. data/lib/epics/signature_algorithm/rsapkcs1.rb +16 -0
  106. data/lib/epics/signature_algorithm/rsapss.rb +20 -0
  107. data/lib/epics/signature_algorithm.rb +2 -0
  108. data/lib/epics/sta.rb +3 -14
  109. data/lib/epics/version.rb +1 -1
  110. data/lib/epics/vmk.rb +3 -14
  111. data/lib/epics/wss.rb +3 -9
  112. data/lib/epics/xct.rb +3 -10
  113. data/lib/epics/xds.rb +3 -10
  114. data/lib/epics/xe2.rb +3 -10
  115. data/lib/epics/xe3.rb +3 -10
  116. data/lib/epics/xek.rb +5 -0
  117. data/lib/epics/yct.rb +6 -0
  118. data/lib/epics/z01.rb +2 -14
  119. data/lib/epics/z52.rb +2 -14
  120. data/lib/epics/z53.rb +2 -14
  121. data/lib/epics/z54.rb +2 -14
  122. data/lib/epics/zsr.rb +5 -0
  123. data/lib/epics.rb +69 -5
  124. data/lib/letter/ini.erb +27 -6
  125. data/spec/aes_spec.rb +46 -0
  126. data/spec/auth_signature_handler_spec.rb +58 -0
  127. data/spec/builders_spec.rb +616 -0
  128. data/spec/certificate_handling_spec.rb +189 -0
  129. data/spec/client_setup_block_spec.rb +41 -0
  130. data/spec/client_spec.rb +143 -91
  131. data/spec/crypt_service_spec.rb +132 -0
  132. data/spec/date_range_spec.rb +46 -0
  133. data/spec/digest_resolver_spec.rb +164 -0
  134. data/spec/fixtures/bank_x509.pem +3 -0
  135. data/spec/fixtures/xml/haa.xml +40 -1
  136. data/spec/fixtures/xml/haa_receipt.xml +33 -30
  137. data/spec/fixtures/xml/haa_response_h004.xml +4 -0
  138. data/spec/fixtures/xml/haa_response_h005.xml +38 -0
  139. data/spec/fixtures/xml/hia_request_order_data.xml +24 -1
  140. data/spec/fixtures/xml/hpb_request.xml +1 -1
  141. data/spec/fixtures/xml/hpb_response_x509.xml +16 -0
  142. data/spec/fixtures/xml/htd_order_data_h005.xml +55 -0
  143. data/spec/fixtures/xml/jruby/hia.xml +20 -22
  144. data/spec/fixtures/xml/jruby/ini.xml +1 -3
  145. data/spec/fixtures/xml/ruby/hia.xml +21 -1
  146. data/spec/fixtures/xml/ruby/ini.xml +21 -1
  147. data/spec/handlers_spec.rb +267 -0
  148. data/spec/hpb_malformed_certificate_spec.rb +36 -0
  149. data/spec/key_file_decryption_spec.rb +27 -0
  150. data/spec/key_file_preservation_spec.rb +78 -0
  151. data/spec/keyring_spec.rb +349 -0
  152. data/spec/middleware/parse_ebics_spec.rb +2 -1
  153. data/spec/order_id_spec.rb +65 -0
  154. data/spec/orders/azv_spec.rb +63 -7
  155. data/spec/orders/b2b_spec.rb +68 -3
  156. data/spec/orders/bka_spec.rb +34 -4
  157. data/spec/orders/c2s_spec.rb +62 -9
  158. data/spec/orders/c52_spec.rb +34 -4
  159. data/spec/orders/c53_spec.rb +34 -4
  160. data/spec/orders/c54_spec.rb +36 -4
  161. data/spec/orders/c5n_spec.rb +36 -4
  162. data/spec/orders/camt_parity_spec.rb +80 -0
  163. data/spec/orders/ccs_spec.rb +62 -9
  164. data/spec/orders/cct_spec.rb +83 -9
  165. data/spec/orders/cd1_spec.rb +50 -7
  166. data/spec/orders/cdb_spec.rb +64 -7
  167. data/spec/orders/cdd_spec.rb +64 -7
  168. data/spec/orders/cds_spec.rb +74 -0
  169. data/spec/orders/cdz_spec.rb +67 -4
  170. data/spec/orders/cip_spec.rb +61 -9
  171. data/spec/orders/crz_spec.rb +67 -4
  172. data/spec/orders/fdl_spec.rb +143 -13
  173. data/spec/orders/ful_spec.rb +63 -0
  174. data/spec/orders/haa_spec.rb +115 -9
  175. data/spec/orders/hac_spec.rb +89 -10
  176. data/spec/orders/hev_spec.rb +1 -6
  177. data/spec/orders/hia_spec.rb +25 -22
  178. data/spec/orders/hkd_spec.rb +23 -4
  179. data/spec/orders/hpb_spec.rb +7 -4
  180. data/spec/orders/hpd_spec.rb +23 -4
  181. data/spec/orders/htd_spec.rb +23 -4
  182. data/spec/orders/ini_spec.rb +23 -18
  183. data/spec/orders/ptk_spec.rb +27 -4
  184. data/spec/orders/sta_spec.rb +129 -11
  185. data/spec/orders/vmk_spec.rb +36 -4
  186. data/spec/orders/wss_spec.rb +29 -3
  187. data/spec/orders/x_509_certificate_spec.rb +9 -1
  188. data/spec/orders/xct_spec.rb +63 -0
  189. data/spec/orders/xds_spec.rb +13 -6
  190. data/spec/orders/xe2_spec.rb +64 -7
  191. data/spec/orders/xe3_spec.rb +64 -7
  192. data/spec/orders/xek_spec.rb +31 -0
  193. data/spec/orders/yct_spec.rb +43 -0
  194. data/spec/orders/z01_spec.rb +34 -4
  195. data/spec/orders/z52_spec.rb +34 -4
  196. data/spec/orders/z53_spec.rb +34 -4
  197. data/spec/orders/z54_spec.rb +36 -4
  198. data/spec/orders/zsr_spec.rb +31 -0
  199. data/spec/request_factory_spec.rb +444 -0
  200. data/spec/response_business_error_spec.rb +45 -0
  201. data/spec/response_validation_spec.rb +23 -0
  202. data/spec/{key_spec.rb → signature_algorithm/rsa_pss_spec.rb} +4 -13
  203. data/spec/support/ebics_matcher.rb +17 -9
  204. data/spec/support/ebics_shared_examples.rb +642 -0
  205. data/spec/support/h005_client_helper.rb +23 -0
  206. data/spec/upload_digest_spec.rb +69 -0
  207. data/spec/user_signature_handler_spec.rb +25 -0
  208. data/spec/version_support_error_spec.rb +76 -0
  209. data/spec/xsd/{ebics_hev.xsd → H000/ebics_hev.xsd} +0 -0
  210. data/spec/xsd/H003/ebics.xsd +11 -0
  211. data/spec/xsd/H003/ebics_keymgmt_request.xsd +555 -0
  212. data/spec/xsd/H003/ebics_keymgmt_response.xsd +131 -0
  213. data/spec/xsd/H003/ebics_orders.xsd +1811 -0
  214. data/spec/xsd/H003/ebics_request.xsd +354 -0
  215. data/spec/xsd/H003/ebics_response.xsd +161 -0
  216. data/spec/xsd/{ebics_signature.xsd → H003/ebics_signature.xsd} +0 -0
  217. data/spec/xsd/H003/ebics_types.xsd +2361 -0
  218. data/spec/xsd/H003/xmldsig-core-schema.xsd +318 -0
  219. data/spec/xsd/H004/ebics_signature.xsd +217 -0
  220. data/spec/xsd/H005/ebics_H005.xsd +11 -0
  221. data/spec/xsd/H005/ebics_keymgmt_request_H005.xsd +523 -0
  222. data/spec/xsd/H005/ebics_keymgmt_response_H005.xsd +137 -0
  223. data/spec/xsd/H005/ebics_orders_H005.xsd +2094 -0
  224. data/spec/xsd/H005/ebics_request_H005.xsd +349 -0
  225. data/spec/xsd/H005/ebics_response_H005.xsd +167 -0
  226. data/spec/xsd/H005/ebics_signature_S002.xsd +177 -0
  227. data/spec/xsd/H005/ebics_types_H005.xsd +1885 -0
  228. data/spec/xsd/H005/xmldsig-core-schema.xsd +318 -0
  229. metadata +200 -36
  230. data/lib/epics/header_request.rb +0 -58
  231. data/lib/epics/middleware/xmlsig.rb +0 -16
  232. data/lib/epics/signer.rb +0 -40
  233. data/lib/epics/x_509_certificate.rb +0 -15
  234. data/lib/letter/ini_with_certs.erb +0 -335
  235. data/spec/generic_upload_spec.rb +0 -58
  236. data/spec/hpb_spec.rb +0 -15
  237. data/spec/signer_spec.rb +0 -34
  238. /data/spec/xsd/{ebics_H004.xsd → H004/ebics_H004.xsd} +0 -0
  239. /data/spec/xsd/{ebics_keymgmt_request_H004.xsd → H004/ebics_keymgmt_request_H004.xsd} +0 -0
  240. /data/spec/xsd/{ebics_keymgmt_response_H004.xsd → H004/ebics_keymgmt_response_H004.xsd} +0 -0
  241. /data/spec/xsd/{ebics_orders_H004.xsd → H004/ebics_orders_H004.xsd} +0 -0
  242. /data/spec/xsd/{ebics_request_H004.xsd → H004/ebics_request_H004.xsd} +0 -0
  243. /data/spec/xsd/{ebics_response_H004.xsd → H004/ebics_response_H004.xsd} +0 -0
  244. /data/spec/xsd/{ebics_types_H004.xsd → H004/ebics_types_H004.xsd} +0 -0
  245. /data/spec/xsd/{xmldsig-core-schema.xsd → H004/xmldsig-core-schema.xsd} +0 -0
@@ -0,0 +1,189 @@
1
+ RSpec.describe 'X.509 certificate handling' do
2
+ let(:url) { 'https://example.com/ebics' }
3
+ let(:passphrase) { 'secret' }
4
+
5
+ def setup_client(**options)
6
+ Epics::Client.setup(passphrase, url, 'HOST01', 'USER01', 'PARTNER01', 1024, **options)
7
+ end
8
+
9
+ def foreign_certificate
10
+ key = OpenSSL::PKey::RSA.generate(1024)
11
+ Epics::Crypt::X509.new(generate_x_509_crt(key, '/C=GB/O=Other/CN=other.example.org'))
12
+ end
13
+
14
+ describe 'Client.setup on H005' do
15
+ subject(:client) { setup_client(version: Epics::Keyring::VERSION_30) }
16
+
17
+ it 'generates a certificate for every user key' do
18
+ certificates = [client.keyring.user_signature, client.keyring.user_authentication,
19
+ client.keyring.user_encryption].map(&:certificate)
20
+
21
+ expect(certificates).to all(be_a(Epics::Crypt::X509))
22
+ end
23
+
24
+ it 'generates certificates that match their key' do
25
+ signature = client.keyring.user_signature
26
+
27
+ expect(signature.certificate.certificate.public_key.to_pem)
28
+ .to eq(signature.key.key.public_key.to_pem)
29
+ end
30
+
31
+ it 'names the subject after the client identity' do
32
+ expect(client.keyring.user_signature.certificate.certificate.subject.to_s)
33
+ .to include('CN=USER01', 'OU=PARTNER01', 'O=HOST01')
34
+ end
35
+
36
+ it 'accepts a custom subject' do
37
+ client = setup_client(version: Epics::Keyring::VERSION_30, certificate_subject: '/CN=Custom/O=Acme/C=CH')
38
+
39
+ expect(client.keyring.user_signature.certificate.certificate.subject.to_s).to include('CN=Custom')
40
+ end
41
+
42
+ it 'accepts a custom validity' do
43
+ not_after = Time.utc(2030, 1, 1)
44
+ client = setup_client(version: Epics::Keyring::VERSION_30, certificate_not_after: not_after)
45
+
46
+ expect(client.keyring.user_signature.certificate.certificate.not_after).to eq(not_after)
47
+ end
48
+
49
+ it 'keeps a supplied certificate instead of generating one' do
50
+ supplied = foreign_certificate
51
+ client = setup_client(version: Epics::Keyring::VERSION_30,
52
+ x_509_certificate_a_content: supplied.to_pem)
53
+
54
+ expect(client.keyring.user_signature.certificate.fingerprint).to eq(supplied.fingerprint)
55
+ expect(client.keyring.user_authentication.certificate).not_to be_nil
56
+ end
57
+
58
+ it 'can be turned off' do
59
+ client = setup_client(version: Epics::Keyring::VERSION_30, generate_certificates: false)
60
+
61
+ expect(client.keyring.user_signature.certificate).to be_nil
62
+ end
63
+
64
+ it 'does not mutate the options hash it was given' do
65
+ options = { version: Epics::Keyring::VERSION_30, signature_version: Epics::Signature::A_VERSION_5 }
66
+ Epics::Client.setup(passphrase, url, 'HOST01', 'USER01', 'PARTNER01', 1024, options)
67
+
68
+ expect(options).to include(signature_version: Epics::Signature::A_VERSION_5)
69
+ end
70
+ end
71
+
72
+ describe 'Client.setup on H004' do
73
+ subject(:client) { setup_client }
74
+
75
+ it 'generates no certificates' do
76
+ expect(client.keyring.user_signature.certificate).to be_nil
77
+ end
78
+
79
+ it 'still accepts them explicitly' do
80
+ client = setup_client(generate_certificates: true)
81
+
82
+ expect(client.keyring.user_signature.certificate).to be_a(Epics::Crypt::X509)
83
+ end
84
+ end
85
+
86
+ describe 'persistence' do
87
+ let(:client) { setup_client(version: Epics::Keyring::VERSION_30) }
88
+
89
+ def reload(json, **options)
90
+ Epics::Client.new(json, passphrase, url, 'HOST01', 'USER01', 'PARTNER01',
91
+ version: Epics::Keyring::VERSION_30, **options)
92
+ end
93
+
94
+ it 'restores user certificates from a key file' do
95
+ fingerprints = %i[user_signature user_authentication user_encryption]
96
+ .map { |slot| client.keyring.send(slot).certificate.fingerprint }
97
+ reloaded = reload(client.send(:dump_keys))
98
+
99
+ expect(%i[user_signature user_authentication user_encryption]
100
+ .map { |slot| reloaded.keyring.send(slot).certificate.fingerprint }).to eq(fingerprints)
101
+ end
102
+
103
+ it 'keeps a restored certificate when no option is passed' do
104
+ reloaded = reload(client.send(:dump_keys))
105
+
106
+ expect(reloaded.keyring.user_signature.certificate).not_to be_nil
107
+ end
108
+
109
+ it 'lets an explicit option override a restored certificate' do
110
+ supplied = foreign_certificate
111
+ reloaded = reload(client.send(:dump_keys), x_509_certificate_a_content: supplied.to_pem)
112
+
113
+ expect(reloaded.keyring.user_signature.certificate.fingerprint).to eq(supplied.fingerprint)
114
+ end
115
+
116
+ it 'keeps the bare form for keys without a certificate' do
117
+ h004 = setup_client
118
+ dumped = JSON.parse(h004.send(:dump_keys))
119
+
120
+ expect(dumped.values).to all(be_a(String))
121
+ end
122
+ end
123
+
124
+ describe 'invalid certificate content' do
125
+ it 'raises and names the option' do
126
+ expect do
127
+ setup_client(version: Epics::Keyring::VERSION_30, x_509_certificate_a_content: 'not-a-pem')
128
+ end.to raise_error(Epics::InvalidCertificateError, /x_509_certificate_a_content/)
129
+ end
130
+
131
+ it 'ignores empty content' do
132
+ client = setup_client(version: Epics::Keyring::VERSION_30, x_509_certificate_x_content: '')
133
+
134
+ expect(client.keyring.user_authentication.certificate).to be_a(Epics::Crypt::X509)
135
+ end
136
+ end
137
+
138
+ # H005 carries the public key inside ds:X509Data, so a request built without a
139
+ # certificate would contain no key material at all.
140
+ describe 'H005 key management without certificates', without_certificates: true do
141
+ let(:client) do
142
+ setup_client(version: Epics::Keyring::VERSION_30, generate_certificates: false)
143
+ end
144
+
145
+ it 'refuses to build INI' do
146
+ expect { Epics::INI.new(client).key_signature }
147
+ .to raise_error(Epics::MissingCertificateError, /x_509_certificate_a_content/)
148
+ end
149
+
150
+ it 'refuses to build HIA' do
151
+ expect { Epics::HIA.new(client).order_data }
152
+ .to raise_error(Epics::MissingCertificateError, /x_509_certificate_x_content/)
153
+ end
154
+ end
155
+
156
+ describe 'H005 key management with certificates' do
157
+ let(:client) { setup_client(version: Epics::Keyring::VERSION_30) }
158
+
159
+ it 'carries the certificate in INI order data' do
160
+ expect(Epics::INI.new(client).key_signature)
161
+ .to include("<ds:X509Certificate>#{client.keyring.user_signature.certificate.data}</ds:X509Certificate>")
162
+ end
163
+
164
+ it 'carries both certificates in HIA order data' do
165
+ order_data = Epics::HIA.new(client).order_data
166
+
167
+ expect(order_data).to include(client.keyring.user_authentication.certificate.data)
168
+ expect(order_data).to include(client.keyring.user_encryption.certificate.data)
169
+ end
170
+
171
+ it 'validates INI order data against the S002 schema' do
172
+ schema = Nokogiri::XML::Schema(File.open(File.join(File.dirname(__FILE__), 'xsd', 'H005',
173
+ 'ebics_signature_S002.xsd')))
174
+
175
+ expect(schema.validate(Nokogiri::XML(Epics::INI.new(client).key_signature))).to be_empty
176
+ end
177
+ end
178
+
179
+ describe 'H004 key management' do
180
+ let(:client) { setup_client }
181
+
182
+ it 'still sends the raw modulus and exponent' do
183
+ order_data = Epics::INI.new(client).key_signature
184
+
185
+ expect(order_data).to include('Modulus')
186
+ expect(order_data).not_to include('X509Data')
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,41 @@
1
+ RSpec.describe 'Epics::Client.setup with a block' do
2
+ # Keys are generated after #initialize returns, so the block cannot be forwarded to
3
+ # .new -- it would see an empty keyring.
4
+ it 'yields a client whose keys exist' do
5
+ yielded = nil
6
+ Epics::Client.setup('secret', 'https://example.com/ebics', 'HOST01', 'USER01', 'PARTNER01', 1024) do |client|
7
+ yielded = { signature: client.signature_key, authentication: client.authentication_key,
8
+ encryption: client.encryption_key }
9
+ end
10
+
11
+ expect(yielded.values).to all(be_a(Epics::SignatureAlgorithm::Base))
12
+ end
13
+
14
+ it 'yields the client that is returned' do
15
+ yielded = nil
16
+ returned = Epics::Client.setup('secret', 'https://example.com/ebics', 'HOST01', 'USER01', 'PARTNER01', 1024) do |c|
17
+ yielded = c
18
+ end
19
+
20
+ expect(yielded).to be(returned)
21
+ end
22
+
23
+ it 'can render the initialisation letter from inside the block' do
24
+ expect do
25
+ Epics::Client.setup('secret', 'https://example.com/ebics', 'HOST01', 'USER01', 'PARTNER01', 1024,
26
+ version: Epics::Keyring::VERSION_30) do |client|
27
+ client.ini_letter('Test Bank')
28
+ end
29
+ end.not_to raise_error
30
+ end
31
+
32
+ it 'yields after certificates have been generated on H005' do
33
+ yielded = nil
34
+ Epics::Client.setup('secret', 'https://example.com/ebics', 'HOST01', 'USER01', 'PARTNER01', 1024,
35
+ version: Epics::Keyring::VERSION_30) do |client|
36
+ yielded = client.keyring.user_signature.certificate
37
+ end
38
+
39
+ expect(yielded).to be_a(Epics::Crypt::X509)
40
+ end
41
+ end
data/spec/client_spec.rb CHANGED
@@ -12,18 +12,13 @@ RSpec.describe Epics::Client do
12
12
 
13
13
  it 'holds all keys, user and bank' do
14
14
  expect(subject.keys).to match(a_hash_including(
15
- 'E002' => be_a(Epics::Key),
16
- 'X002' => be_a(Epics::Key),
17
- 'A006' => be_a(Epics::Key),
18
- 'SIZBN001.E002' => be_a(Epics::Key),
19
- 'SIZBN001.X002' => be_a(Epics::Key)
15
+ 'E002' => be_a(Epics::SignatureAlgorithm::RsaPkcs1),
16
+ 'X002' => be_a(Epics::SignatureAlgorithm::RsaPkcs1),
17
+ 'A006' => be_a(Epics::SignatureAlgorithm::RsaPss),
18
+ 'SIZBN001.E002' => be_a(Epics::SignatureAlgorithm::RsaPkcs1),
19
+ 'SIZBN001.X002' => be_a(Epics::SignatureAlgorithm::RsaPkcs1)
20
20
  ))
21
21
  end
22
-
23
- it 'assigns x_509_certificates_content' do
24
- subject.x_509_certificates_content = { a: 'cert_a', x: 'cert_x', e: 'cert_e' }
25
- expect(subject.x_509_certificates_content).to eq({ a: 'cert_a', x: 'cert_x', e: 'cert_e' })
26
- end
27
22
  end
28
23
 
29
24
  context 'environment settings' do
@@ -56,33 +51,33 @@ RSpec.describe Epics::Client do
56
51
  end
57
52
  end
58
53
 
59
- describe '#e' do
54
+ describe '#encryption_key' do
60
55
  it 'the encryption key' do
61
- expect(subject.e.public_digest).to eq('rwIxSUJAVEFDQ0sdYe+CybdspMllDG6ArNtdCzUbT1E=')
56
+ expect(subject.encryption_key.public_digest).to eq('rwIxSUJAVEFDQ0sdYe+CybdspMllDG6ArNtdCzUbT1E=')
62
57
  end
63
58
  end
64
59
 
65
- describe '#x' do
66
- it 'the signing key' do
67
- expect(subject.x.public_digest).to eq('Jjcu97qg595PPn+0OvqBOBIskMIiStNYYXyjgWHeBhE=')
60
+ describe '#authentication_key' do
61
+ it 'the authentication key' do
62
+ expect(subject.authentication_key.public_digest).to eq('Jjcu97qg595PPn+0OvqBOBIskMIiStNYYXyjgWHeBhE=')
68
63
  end
69
64
  end
70
65
 
71
- describe '#a' do
72
- it 'the authentication key' do
73
- expect(subject.a.public_digest).to eq('9ay3tc+I3MgJBaroeD7XJfOtHcq7IR23fljWefl0dzk=')
66
+ describe '#signature_key' do
67
+ it 'the signing key' do
68
+ expect(subject.signature_key.public_digest).to eq('9ay3tc+I3MgJBaroeD7XJfOtHcq7IR23fljWefl0dzk=')
74
69
  end
75
70
  end
76
71
 
77
- describe '#bank_e' do
72
+ describe '#bank_encryption_key' do
78
73
  it 'the banks encryption key' do
79
- expect(subject.bank_e.public_digest).to eq('dFAYe281vj9NB7w+VoWIdfHnjY9hNbZLbHsDOu76QAE=')
74
+ expect(subject.bank_encryption_key.public_digest).to eq('dFAYe281vj9NB7w+VoWIdfHnjY9hNbZLbHsDOu76QAE=')
80
75
  end
81
76
  end
82
77
 
83
- describe '#bank_x' do
78
+ describe '#bank_authentication_key' do
84
79
  it 'the banks signing key' do
85
- expect(subject.bank_x.public_digest).to eq('dFAYe281vj9NB7w+VoWIdfHnjY9hNbZLbHsDOu76QAE=')
80
+ expect(subject.bank_authentication_key.public_digest).to eq('dFAYe281vj9NB7w+VoWIdfHnjY9hNbZLbHsDOu76QAE=')
86
81
  end
87
82
  end
88
83
 
@@ -100,7 +95,8 @@ RSpec.describe Epics::Client do
100
95
 
101
96
  describe '#HPB' do
102
97
  let(:e_key) do
103
- Epics::Key.new(OpenSSL::PKey::RSA.new(File.read(File.join(File.dirname(__FILE__), 'fixtures', 'bank_e.pem'))))
98
+ Epics::SignatureAlgorithm::RsaPss.new(OpenSSL::PKey::RSA.new(File.read(File.join(File.dirname(__FILE__),
99
+ 'fixtures', 'bank_e.pem'))))
104
100
  end
105
101
 
106
102
  before do
@@ -110,7 +106,7 @@ RSpec.describe Epics::Client do
110
106
  'hpb_response_ebics_ns.xml')))
111
107
  end
112
108
 
113
- it { expect(subject.HPB).to match([be_a(Epics::Key), be_a(Epics::Key)]) }
109
+ it { expect(subject.HPB).to match([be_a(Epics::SignatureAlgorithm::RsaPkcs1), be_a(Epics::SignatureAlgorithm::RsaPkcs1)]) }
114
110
 
115
111
  it 'changes the SIZBN001.(E|X)002 keys' do
116
112
  expect { subject.HPB }.to(change { subject.keys['SIZBN001.E002'] })
@@ -135,6 +131,47 @@ RSpec.describe Epics::Client do
135
131
  it { expect(subject.keys['SIZBN001.E002'].public_digest).to eq(e_key.public_digest) }
136
132
  it { expect(subject.keys['SIZBN001.X002'].public_digest).to eq(e_key.public_digest) }
137
133
  end
134
+
135
+ describe 'with X.509 certificates (EBICS 3.0 / H005 format)' do
136
+ let(:bank_cert) { OpenSSL::X509::Certificate.new(File.read(File.join(File.dirname(__FILE__), 'fixtures', 'bank_x509.pem'))) }
137
+
138
+ before do
139
+ allow(subject).to receive(:download).with(Epics::HPB).and_return(File.read(File.join(File.dirname(__FILE__),
140
+ 'fixtures', 'xml', 'hpb_response_x509.xml')))
141
+ end
142
+
143
+ it 'parses X002 bank authentication key from certificate' do
144
+ subject.HPB
145
+ expect(subject.bank_authentication_key).not_to be_nil
146
+ expect(subject.bank_authentication_key).to be_a(Epics::SignatureAlgorithm::RsaPkcs1)
147
+ end
148
+
149
+ it 'parses E002 bank encryption key from certificate' do
150
+ subject.HPB
151
+ expect(subject.bank_encryption_key).not_to be_nil
152
+ expect(subject.bank_encryption_key).to be_a(Epics::SignatureAlgorithm::RsaPkcs1)
153
+ end
154
+
155
+ it 'stores certificate in keyring for bank authentication' do
156
+ subject.HPB
157
+ expect(subject.keyring.bank_authentication.certificate).not_to be_nil
158
+ end
159
+
160
+ it 'stores certificate in keyring for bank encryption' do
161
+ subject.HPB
162
+ expect(subject.keyring.bank_encryption.certificate).not_to be_nil
163
+ end
164
+
165
+ it 'returns authentication and encryption keys' do
166
+ expect(subject.HPB).to match([be_a(Epics::SignatureAlgorithm::RsaPkcs1),
167
+ be_a(Epics::SignatureAlgorithm::RsaPkcs1)])
168
+ end
169
+
170
+ it 'parses public key from certificate correctly' do
171
+ subject.HPB
172
+ expect(subject.bank_authentication_key.key.to_pem).to eq(bank_cert.public_key.to_pem)
173
+ end
174
+ end
138
175
  end
139
176
 
140
177
  describe '#CDB' do
@@ -225,6 +262,73 @@ RSpec.describe Epics::Client do
225
262
  end
226
263
  end
227
264
 
265
+ describe '#HTD (H005)' do
266
+ let(:subject) do
267
+ Epics::Client.new(key, 'secret', 'https://zkbisotb.zkb.ch/ebicsweb/ebicsweb', 'ZKBISOTB', 'EBIX', 'EBICS',
268
+ version: Epics::Keyring::VERSION_30)
269
+ end
270
+
271
+ before do
272
+ allow(subject).to receive(:download).and_return(File.read(File.join(File.dirname(__FILE__), 'fixtures', 'xml',
273
+ 'htd_order_data_h005.xml')))
274
+ end
275
+
276
+ it 'parses H005 Service elements and returns array of service hashes' do
277
+ subject.HTD
278
+ expect(subject.instance_variable_get('@order_types')).to match_array([
279
+ { service_name: 'OTH', scope: 'BIL',
280
+ service_option: 'CH004TPE', container: 'ZIP', msg_name: 'msc' },
281
+ { service_name: 'STM', scope: 'BIL',
282
+ service_option: 'CH004TPE', container: 'ZIP', msg_name: 'pain.001' },
283
+ { service_name: 'SCT', scope: 'BIL',
284
+ service_option: 'CH004TPE', container: 'ZIP', msg_name: 'pain.008' },
285
+ { service_name: 'CAMT', scope: 'BIL',
286
+ service_option: 'CH004TPE', container: 'ZIP', msg_name: 'camt.052' },
287
+ { service_name: 'CAMT', scope: 'BIL',
288
+ service_option: 'CH004TPE', container: 'ZIP', msg_name: 'camt.053' }
289
+ ])
290
+ end
291
+ end
292
+
293
+ describe '#HAA' do
294
+ before do
295
+ allow(subject).to receive(:download).and_return(File.read(File.join(File.dirname(__FILE__), 'fixtures', 'xml',
296
+ 'haa_response_h004.xml')))
297
+ end
298
+
299
+ it 'returns order types as array of strings for H004' do
300
+ expect(subject.HAA).to match_array(%w[PTK HPD HTD STA HVD HPB HAA HVT HVU HVZ INI SPR PUB HIA HCA HSA HVE
301
+ HVS CCS CCT CIP CD1 CDB CDD])
302
+ end
303
+ end
304
+
305
+ describe '#HAA (H005)' do
306
+ let(:subject) do
307
+ Epics::Client.new(key, 'secret', 'https://zkbisotb.zkb.ch/ebicsweb/ebicsweb', 'ZKBISOTB', 'EBIX', 'EBICS',
308
+ version: Epics::Keyring::VERSION_30)
309
+ end
310
+
311
+ before do
312
+ allow(subject).to receive(:download).and_return(File.read(File.join(File.dirname(__FILE__), 'fixtures', 'xml',
313
+ 'haa_response_h005.xml')))
314
+ end
315
+
316
+ it 'parses H005 Service elements and returns array of service hashes' do
317
+ expect(subject.HAA).to match_array([
318
+ { service_name: 'OTH', scope: 'BIL', service_option: 'CH004TPE',
319
+ container: 'ZIP', msg_name: 'msc' },
320
+ { service_name: 'STM', scope: 'BIL', service_option: 'CH004TPE',
321
+ container: 'ZIP', msg_name: 'pain.001' },
322
+ { service_name: 'SCT', scope: 'BIL', service_option: 'CH004TPE',
323
+ container: 'ZIP', msg_name: 'pain.008' },
324
+ { service_name: 'CAMT', scope: 'BIL', service_option: 'CH004TPE',
325
+ container: 'ZIP', msg_name: 'camt.052' },
326
+ { service_name: 'CAMT', scope: 'BIL', service_option: 'CH004TPE',
327
+ container: 'ZIP', msg_name: 'camt.053' }
328
+ ])
329
+ end
330
+ end
331
+
228
332
  describe '#C53/C52/C54/Z52/Z53/Z54 types with zipped data' do
229
333
  before do
230
334
  allow(subject).to receive(:download).and_return(File.read(File.join(File.dirname(__FILE__), 'fixtures',
@@ -251,77 +355,25 @@ RSpec.describe Epics::Client do
251
355
  end
252
356
  end
253
357
 
254
- describe '#x_509_certificate' do
255
- let(:epics_client) do
256
- Epics::Client.new(key, 'secret', 'https://194.180.18.30/ebicsweb/ebicsweb', 'SIZBN001', 'EBIX', 'EBICS')
257
- end
258
-
259
- context 'with valid certificates content' do
260
- it 'returns an instance of Epics::X509Certificate' do
261
- epics_client.x_509_certificates_content = {
262
- a: generate_x_509_crt(epics_client.a.key, '/C=GB/O=TestOrg/CN=test.example.org'),
263
- x: generate_x_509_crt(epics_client.x.key, '/C=GB/O=TestOrg/CN=test.example.org'),
264
- e: generate_x_509_crt(epics_client.e.key, '/C=GB/O=TestOrg/CN=test.example.org')
265
- }
266
-
267
- expect(epics_client.x_509_certificate(:a)).to be_a(Epics::X509Certificate)
268
- expect(epics_client.x_509_certificate(:x)).to be_a(Epics::X509Certificate)
269
- expect(epics_client.x_509_certificate(:e)).to be_a(Epics::X509Certificate)
270
- end
271
- end
272
-
273
- context 'with nil or empty content' do
274
- it 'returns nil when content is nil' do
275
- epics_client.x_509_certificates_content = { a: nil, x: nil, e: nil }
276
- expect(epics_client.x_509_certificate(:a)).to be_nil
277
- expect(epics_client.x_509_certificate(:x)).to be_nil
278
- expect(epics_client.x_509_certificate(:e)).to be_nil
279
- end
280
-
281
- it 'returns nil when content is empty string' do
282
- epics_client.x_509_certificates_content = { a: '', x: '', e: '' }
283
- expect(epics_client.x_509_certificate(:a)).to be_nil
284
- expect(epics_client.x_509_certificate(:x)).to be_nil
285
- expect(epics_client.x_509_certificate(:e)).to be_nil
286
- end
287
- end
288
- end
358
+ describe 'x509 certificate assignment via options' do
359
+ it 'assigns certificates to signatures from constructor options' do
360
+ epics_client = Epics::Client.new(key, 'secret', 'https://194.180.18.30/ebicsweb/ebicsweb', 'SIZBN001', 'EBIX', 'EBICS',
361
+ x_509_certificate_a_content: generate_x_509_crt(OpenSSL::PKey::RSA.new(2048), '/C=GB/O=TestOrg/CN=test.example.org'),
362
+ x_509_certificate_x_content: generate_x_509_crt(OpenSSL::PKey::RSA.new(2048), '/C=GB/O=TestOrg/CN=test.example.org'),
363
+ x_509_certificate_e_content: generate_x_509_crt(OpenSSL::PKey::RSA.new(2048), '/C=GB/O=TestOrg/CN=test.example.org'))
289
364
 
290
- describe '#x_509_certificate_hash' do
291
- let(:epics_client) do
292
- Epics::Client.new(key, 'secret', 'https://194.180.18.30/ebicsweb/ebicsweb', 'SIZBN001', 'EBIX', 'EBICS')
293
- end
294
- context 'with valid certificates content' do
295
- it 'returns SHA256 hash' do
296
- epics_client.x_509_certificates_content = {
297
- a: generate_x_509_crt(epics_client.a.key, '/C=GB/O=TestOrg/CN=test.example.org'),
298
- x: generate_x_509_crt(epics_client.x.key, '/C=GB/O=TestOrg/CN=test.example.org'),
299
- e: generate_x_509_crt(epics_client.e.key, '/C=GB/O=TestOrg/CN=test.example.org')
300
- }
301
-
302
- expect(epics_client.x_509_certificate_hash(:a)).to be_a(String)
303
- expect(epics_client.x_509_certificate_hash(:x)).to be_a(String)
304
- expect(epics_client.x_509_certificate_hash(:e)).to be_a(String)
305
- expect(epics_client.x_509_certificate_hash(:a).size).to eq(64)
306
- expect(epics_client.x_509_certificate_hash(:x).size).to eq(64)
307
- expect(epics_client.x_509_certificate_hash(:e).size).to eq(64)
308
- end
365
+ expect(epics_client.keyring.user_signature.certificate).to be_a(Epics::Crypt::X509)
366
+ expect(epics_client.keyring.user_authentication.certificate).to be_a(Epics::Crypt::X509)
367
+ expect(epics_client.keyring.user_encryption.certificate).to be_a(Epics::Crypt::X509)
309
368
  end
310
369
 
311
- context 'with nil or empty content' do
312
- it 'returns nil when content is nil' do
313
- epics_client.x_509_certificates_content = { a: nil, x: nil, e: nil }
314
- expect(epics_client.x_509_certificate_hash(:a)).to be_nil
315
- expect(epics_client.x_509_certificate_hash(:x)).to be_nil
316
- expect(epics_client.x_509_certificate_hash(:e)).to be_nil
317
- end
370
+ it 'leaves certificates nil when no options provided' do
371
+ epics_client = Epics::Client.new(key, 'secret', 'https://194.180.18.30/ebicsweb/ebicsweb', 'SIZBN001', 'EBIX',
372
+ 'EBICS')
318
373
 
319
- it 'returns nil when content is empty string' do
320
- epics_client.x_509_certificates_content = { a: '', x: '', e: '' }
321
- expect(epics_client.x_509_certificate_hash(:a)).to be_nil
322
- expect(epics_client.x_509_certificate_hash(:x)).to be_nil
323
- expect(epics_client.x_509_certificate_hash(:e)).to be_nil
324
- end
374
+ expect(epics_client.keyring.user_signature.certificate).to be_nil
375
+ expect(epics_client.keyring.user_authentication.certificate).to be_nil
376
+ expect(epics_client.keyring.user_encryption.certificate).to be_nil
325
377
  end
326
378
  end
327
379
  end
@@ -0,0 +1,132 @@
1
+ RSpec.describe Epics::Services::CryptService do
2
+ subject(:service) { described_class.new }
3
+
4
+ let(:rsa_key) { OpenSSL::PKey::RSA.generate(2048) }
5
+ let(:rsa_algo) { Epics::SignatureAlgorithm::Rsa.new(rsa_key) }
6
+
7
+ describe '#encrypt' do
8
+ let(:data) { 'data to encrypt' }
9
+ let(:rsa_pss_algo) { Epics::SignatureAlgorithm::RsaPss.new(rsa_key) }
10
+
11
+ context 'with A006 (RSA-PSS sign)' do
12
+ let(:signature) { Epics::Signature.new(Epics::Signature::A_VERSION_6, rsa_pss_algo) }
13
+
14
+ it 'delegates to signature_algorithm#sign' do
15
+ expect(rsa_pss_algo).to receive(:sign).with(data).and_call_original
16
+ service.encrypt(signature, data)
17
+ end
18
+ end
19
+
20
+ context 'with X002' do
21
+ let(:signature) { Epics::Signature.new(Epics::Signature::X_VERSION_2, rsa_pss_algo) }
22
+
23
+ it 'delegates to signature_algorithm#sign' do
24
+ expect(rsa_pss_algo).to receive(:sign).with(data).and_call_original
25
+ service.encrypt(signature, data)
26
+ end
27
+ end
28
+
29
+ context 'with A005 (RSA PKCS#1 private encrypt)' do
30
+ let(:signature) { Epics::Signature.new(Epics::Signature::A_VERSION_5, rsa_algo) }
31
+
32
+ it 'delegates to signature_algorithm#private_encrypt' do
33
+ expect(rsa_algo).to receive(:private_encrypt).with(data).and_call_original
34
+ service.encrypt(signature, data)
35
+ end
36
+ end
37
+ end
38
+
39
+ describe '#encrypt_by_key / #decrypt_by_key' do
40
+ let(:plaintext) { 'Hello EBICS world!' }
41
+
42
+ it 'round-trips: decrypt(encrypt(data)) == data' do
43
+ aes_key = OpenSSL::Cipher::AES.new(128, :CBC).random_key
44
+ encrypted = service.encrypt_by_key(aes_key, plaintext)
45
+ decrypted = service.decrypt_by_key(aes_key, encrypted)
46
+ expect(decrypted).to eq(plaintext)
47
+ end
48
+
49
+ it 'produces ciphertext different from plaintext' do
50
+ aes_key = OpenSSL::Cipher::AES.new(128, :CBC).random_key
51
+ encrypted = service.encrypt_by_key(aes_key, plaintext)
52
+ expect(encrypted).not_to eq(plaintext)
53
+ end
54
+
55
+ it 'produces different ciphertext with different keys' do
56
+ key1 = OpenSSL::Cipher::AES.new(128, :CBC).random_key
57
+ key2 = OpenSSL::Cipher::AES.new(128, :CBC).random_key
58
+ enc1 = service.encrypt_by_key(key1, plaintext)
59
+ enc2 = service.encrypt_by_key(key2, plaintext)
60
+ expect(enc1).not_to eq(enc2)
61
+ end
62
+ end
63
+
64
+ describe '#encrypt_transaction_key' do
65
+ it 'encrypts with RSA public key and is decryptable with private key' do
66
+ transaction_key = OpenSSL::Cipher::AES.new(128, :CBC).random_key
67
+ encrypted = service.encrypt_transaction_key(rsa_algo, transaction_key)
68
+ decrypted = rsa_key.private_decrypt(encrypted)
69
+ expect(decrypted).to eq(transaction_key)
70
+ end
71
+ end
72
+
73
+ describe '#hash' do
74
+ it 'returns SHA256 digest by default' do
75
+ result = service.hash('test')
76
+ expect(result).to eq(OpenSSL::Digest::SHA256.digest('test'))
77
+ end
78
+
79
+ it 'supports alternative algorithms' do
80
+ result = service.hash('test', 'sha512')
81
+ expect(result).to eq(OpenSSL::Digest::SHA512.digest('test'))
82
+ end
83
+
84
+ it 'returns binary digest' do
85
+ result = service.hash('test')
86
+ expect(result.encoding).to eq(Encoding::BINARY)
87
+ end
88
+ end
89
+
90
+ describe '#calculate_digest' do
91
+ it 'returns SHA256 hash of "exponent modulus" (hex, lowercase, no leading zeros)' do
92
+ exponent_hex = rsa_key.e.to_s(16).gsub(/^0*/, '').downcase
93
+ modulus_hex = rsa_key.n.to_s(16).gsub(/^0*/, '').downcase
94
+ expected = OpenSSL::Digest::SHA256.digest("#{exponent_hex} #{modulus_hex}")
95
+
96
+ result = service.calculate_digest(rsa_algo)
97
+ expect(result).to eq(expected)
98
+ end
99
+
100
+ it 'strips leading zeros from exponent and modulus' do
101
+ key_text = service.calculate_key('000ABC', '00DEF123')
102
+ expect(key_text).to eq('abc def123')
103
+ end
104
+ end
105
+
106
+ describe '#calculate_key' do
107
+ it 'concatenates exponent and modulus lowercase without leading zeros' do
108
+ expect(service.calculate_key('10001', 'ABCDEF')).to eq('10001 abcdef')
109
+ end
110
+
111
+ it 'strips leading zeros' do
112
+ expect(service.calculate_key('010001', '00ABCDEF')).to eq('10001 abcdef')
113
+ end
114
+ end
115
+
116
+ describe '#calculate_certificate_fingerprint' do
117
+ let(:dn) { '/C=DE/O=TestBank/CN=test.ebics.org' }
118
+ let(:cert_pem) { generate_x_509_crt(rsa_key, dn) }
119
+ let(:x509) { Epics::Crypt::X509.new(cert_pem) }
120
+
121
+ it 'returns SHA256 hash of DER-encoded certificate' do
122
+ expected = OpenSSL::Digest::SHA256.digest(x509.to_der)
123
+ result = service.calculate_certificate_fingerprint(x509)
124
+ expect(result).to eq(expected)
125
+ end
126
+
127
+ it 'returns binary digest' do
128
+ result = service.calculate_certificate_fingerprint(x509)
129
+ expect(result.encoding).to eq(Encoding::BINARY)
130
+ end
131
+ end
132
+ end