sepafm 0.1.5 → 1.0.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/.yardopts +6 -0
- data/lib/sepa/application_request.rb +87 -0
- data/lib/sepa/application_response.rb +37 -2
- data/lib/sepa/attribute_checks.rb +30 -0
- data/lib/sepa/banks/danske/danske_response.rb +86 -0
- data/lib/sepa/banks/danske/soap_danske.rb +81 -3
- data/lib/sepa/banks/nordea/nordea_response.rb +18 -0
- data/lib/sepa/banks/nordea/soap_nordea.rb +25 -2
- data/lib/sepa/client.rb +203 -18
- data/lib/sepa/error_messages.rb +54 -13
- data/lib/sepa/response.rb +118 -11
- data/lib/sepa/soap_builder.rb +53 -2
- data/lib/sepa/utilities.rb +167 -6
- data/lib/sepa/version.rb +3 -1
- data/lib/sepafm.rb +57 -4
- data/readme.md +74 -60
- data/test/sepa/sepa_test.rb +1 -1
- metadata +5 -3
data/lib/sepa/response.rb
CHANGED
@@ -1,10 +1,27 @@
|
|
1
1
|
module Sepa
|
2
|
+
|
3
|
+
# Handles soap responses got back from the bank. Bank specific functionality is defined in
|
4
|
+
# subclasses. Handles i.e. logic to make sure the response's integrity has not been compromised
|
5
|
+
# and has methods to extract content from the response.
|
2
6
|
class Response
|
3
7
|
include ActiveModel::Validations
|
4
8
|
include Utilities
|
5
9
|
include ErrorMessages
|
6
10
|
|
7
|
-
|
11
|
+
# The raw soap response in xml
|
12
|
+
#
|
13
|
+
# @return [String]
|
14
|
+
attr_reader :soap
|
15
|
+
|
16
|
+
# Possible Savon::Error with which the {Response} was initialized
|
17
|
+
#
|
18
|
+
# @return [String]
|
19
|
+
attr_reader :error
|
20
|
+
|
21
|
+
# The command with which the response was initialized
|
22
|
+
#
|
23
|
+
# @return [Symbol]
|
24
|
+
attr_reader :command
|
8
25
|
|
9
26
|
validate :document_must_validate_against_schema
|
10
27
|
validate :client_errors
|
@@ -13,6 +30,16 @@ module Sepa
|
|
13
30
|
validate :verify_signature
|
14
31
|
validate :verify_certificate
|
15
32
|
|
33
|
+
# Initializes the response with a options hash
|
34
|
+
#
|
35
|
+
# @param hash [Hash] Hash of options
|
36
|
+
# @example Possible keys in options hash
|
37
|
+
# {
|
38
|
+
# response: "something",
|
39
|
+
# command: :get_user_info,
|
40
|
+
# error: "I'm error",
|
41
|
+
# encryption_private_key: OpenSSL::PKey::RSA
|
42
|
+
# }
|
16
43
|
def initialize(hash = {})
|
17
44
|
@soap = hash[:response]
|
18
45
|
@command = hash[:command]
|
@@ -20,13 +47,27 @@ module Sepa
|
|
20
47
|
@encryption_private_key = hash[:encryption_private_key]
|
21
48
|
end
|
22
49
|
|
50
|
+
# Returns the soap of the response as a Nokogiri document
|
51
|
+
#
|
52
|
+
# @return [Nokogiri::XML] The soap as Nokogiri document
|
23
53
|
def doc
|
24
54
|
@doc ||= xml_doc @soap
|
25
55
|
end
|
26
56
|
|
27
|
-
# Verifies that all digest values in the response match the actual ones.
|
28
|
-
#
|
29
|
-
#
|
57
|
+
# Verifies that all digest values in the response match the actual ones. Takes an optional
|
58
|
+
# verbose parameter to show which digests didn't match. The digest embedded in the document are
|
59
|
+
# first retrieved with {#find_digest_values} method and if none are found, false is returned.
|
60
|
+
# After this, nodes to calculate hashes from are retrieved and hashes using
|
61
|
+
# {#find_nodes_to_verify} method and after this the calculated digests are compared with the
|
62
|
+
# embedded ones. If the all match, true is returned. If some digests failed to verify and
|
63
|
+
# verbose parameter was passed, digests that failed to verify are printed to screen and
|
64
|
+
# false is returned. Otherwise just false is returned.
|
65
|
+
#
|
66
|
+
# @param options [Hash]
|
67
|
+
# @return [false] if hashes don't match or aren't found
|
68
|
+
# @return [true] if hashes match
|
69
|
+
# @example Options hash
|
70
|
+
# { verbose: true }
|
30
71
|
def hashes_match?(options = {})
|
31
72
|
digests = find_digest_values
|
32
73
|
|
@@ -53,17 +94,28 @@ module Sepa
|
|
53
94
|
false
|
54
95
|
end
|
55
96
|
|
56
|
-
# Verifies the signature by extracting the public key from the certificate
|
57
|
-
#
|
97
|
+
# Verifies the signature by extracting the public key from the certificate embedded in the
|
98
|
+
# response and verifying the signature value with that. Makes a call to {#validate_signature}
|
99
|
+
# to do the actual verification. Passes `:exclusive` to {#validate_signature} so that exclusive
|
100
|
+
# mode of xml canonicalization is used.
|
101
|
+
#
|
102
|
+
# @return [true] if signature is valid
|
103
|
+
# @return [false] if signature fails to verify
|
58
104
|
def signature_is_valid?
|
59
105
|
validate_signature(doc, certificate, :exclusive)
|
60
106
|
end
|
61
107
|
|
62
|
-
# Gets the application response from the response as an xml document
|
108
|
+
# Gets the application response from the response as an xml document. Makes a call to
|
109
|
+
# {#extract_application_response} to do the extraction.
|
110
|
+
#
|
111
|
+
# @return [String] The application response as a raw xml document
|
63
112
|
def application_response
|
64
113
|
@application_response ||= extract_application_response(BXD)
|
65
114
|
end
|
66
115
|
|
116
|
+
# Returns the file references in a download file list response
|
117
|
+
#
|
118
|
+
# @return [Array] File references
|
67
119
|
def file_references
|
68
120
|
return unless @command == :download_file_list
|
69
121
|
|
@@ -74,12 +126,23 @@ module Sepa
|
|
74
126
|
end
|
75
127
|
end
|
76
128
|
|
129
|
+
# Returns the certificate embedded in the response
|
130
|
+
#
|
131
|
+
# @return [OpenSSL::X509::Certificate] if the certificate is found
|
132
|
+
# @return [nil] if the certificate can't be found
|
133
|
+
# @raise [OpenSSL::X509::CertificateError] if the certificate cannot be processed
|
77
134
|
def certificate
|
78
135
|
@certificate ||= begin
|
79
136
|
extract_cert(doc, 'BinarySecurityToken', OASIS_SECEXT)
|
80
137
|
end
|
81
138
|
end
|
82
139
|
|
140
|
+
# Returns the content of the response according to {#command}. When command is `:download_file`,
|
141
|
+
# content is returned as a base64 encoded string, when {#command} is `:download_file_list`, the
|
142
|
+
# content is returned as xml, when {#command} is `:get_user_info`, the content is returned as xml
|
143
|
+
# and when {#command} is `:upload_file`, content is returned as xml
|
144
|
+
#
|
145
|
+
# @return [String] the content as xml or base64 encoded string
|
83
146
|
def content
|
84
147
|
@content ||= begin
|
85
148
|
xml = xml_doc(application_response)
|
@@ -103,22 +166,35 @@ module Sepa
|
|
103
166
|
end
|
104
167
|
end
|
105
168
|
|
169
|
+
# Returns the raw soap as xml
|
170
|
+
#
|
171
|
+
# @return [String]
|
106
172
|
def to_s
|
107
173
|
@soap
|
108
174
|
end
|
109
175
|
|
176
|
+
# @abstract
|
110
177
|
def bank_encryption_certificate; end
|
111
178
|
|
179
|
+
# @abstract
|
112
180
|
def bank_signing_certificate; end
|
113
181
|
|
182
|
+
# @abstract
|
114
183
|
def bank_root_certificate; end
|
115
184
|
|
185
|
+
# @abstract
|
116
186
|
def own_encryption_certificate; end
|
117
187
|
|
188
|
+
# @abstract
|
118
189
|
def own_signing_certificate; end
|
119
190
|
|
191
|
+
# @abstract
|
120
192
|
def ca_certificate; end
|
121
193
|
|
194
|
+
# Returns the response code of the response
|
195
|
+
#
|
196
|
+
# @return [String] if the response code can be found
|
197
|
+
# @return [nil] if the response code cannot be found
|
122
198
|
def response_code
|
123
199
|
node = doc.at('xmlns|ResponseCode', xmlns: BXD)
|
124
200
|
node.content if node
|
@@ -126,8 +202,10 @@ module Sepa
|
|
126
202
|
|
127
203
|
private
|
128
204
|
|
129
|
-
# Finds all reference nodes with digest values in the document and returns
|
130
|
-
#
|
205
|
+
# Finds all reference nodes with digest values in the document and returns a hash with uri as
|
206
|
+
# the key and digest as the value.
|
207
|
+
#
|
208
|
+
# @return [Hash] hash of digests with reference uri as the key
|
131
209
|
def find_digest_values
|
132
210
|
references = {}
|
133
211
|
reference_nodes = doc.css('xmlns|Reference', xmlns: DSIG)
|
@@ -142,8 +220,11 @@ module Sepa
|
|
142
220
|
references
|
143
221
|
end
|
144
222
|
|
145
|
-
# Finds nodes to verify by comparing their id's to the uris' in the
|
146
|
-
#
|
223
|
+
# Finds nodes to verify by comparing their id's to the uris' in the references hash. Then
|
224
|
+
# calculates the hashes of those nodes and returns them in a hash
|
225
|
+
#
|
226
|
+
# @param references [Hash]
|
227
|
+
# @return [Hash] hash of calculated digests with reference uri as the key
|
147
228
|
def find_nodes_to_verify(references)
|
148
229
|
nodes = {}
|
149
230
|
|
@@ -157,12 +238,18 @@ module Sepa
|
|
157
238
|
nodes
|
158
239
|
end
|
159
240
|
|
241
|
+
# Validates the document against soap schema unless {#error} is present or command is
|
242
|
+
# `:get_bank_certificate`
|
160
243
|
def document_must_validate_against_schema
|
161
244
|
return if @error || command.to_sym == :get_bank_certificate
|
162
245
|
|
163
246
|
check_validity_against_schema(doc, 'soap.xsd')
|
164
247
|
end
|
165
248
|
|
249
|
+
# Extracts and returns application response from the response
|
250
|
+
#
|
251
|
+
# @return [String] application response as raw xml if it can be found
|
252
|
+
# @return [nil] if application response cannot be found
|
166
253
|
def extract_application_response(namespace)
|
167
254
|
ar_node = doc.at('xmlns|ApplicationResponse', xmlns: namespace)
|
168
255
|
if ar_node
|
@@ -170,15 +257,22 @@ module Sepa
|
|
170
257
|
end
|
171
258
|
end
|
172
259
|
|
260
|
+
# Handles errors that have been passed from client
|
173
261
|
def client_errors
|
174
262
|
client_error = error.to_s
|
175
263
|
errors.add(:base, client_error) unless client_error.empty?
|
176
264
|
end
|
177
265
|
|
266
|
+
# Find node by it's reference URI in soap header
|
267
|
+
#
|
268
|
+
# @param uri [String] the node's URI
|
269
|
+
# @return [Nokogiri::XML::Node]
|
178
270
|
def find_node_by_uri(uri)
|
179
271
|
doc.at("[xmlns|Id='#{uri}']", xmlns: OASIS_UTILITY)
|
180
272
|
end
|
181
273
|
|
274
|
+
# Validates response code in response. "00" and "24" are currently considered valid.
|
275
|
+
# Validation is not run if {#error} is present
|
182
276
|
def validate_response_code
|
183
277
|
return if @error
|
184
278
|
|
@@ -187,14 +281,19 @@ module Sepa
|
|
187
281
|
end
|
188
282
|
end
|
189
283
|
|
284
|
+
# Validates hashes in the response. {#hashes_match?} must return true for validation to pass.
|
285
|
+
# Is not run if {#error} is present or response code is not ok.
|
190
286
|
def validate_hashes
|
191
287
|
return if @error
|
192
288
|
return unless response_code_is_ok?
|
289
|
+
|
193
290
|
unless hashes_match?
|
194
291
|
errors.add(:base, HASH_ERROR_MESSAGE)
|
195
292
|
end
|
196
293
|
end
|
197
294
|
|
295
|
+
# Validate signature in the response. Validation is not run if {#error} is present or response
|
296
|
+
# is not ok.
|
198
297
|
def verify_signature
|
199
298
|
return if @error
|
200
299
|
return unless response_code_is_ok?
|
@@ -204,6 +303,9 @@ module Sepa
|
|
204
303
|
end
|
205
304
|
end
|
206
305
|
|
306
|
+
# Validates certificate in the soap. The certificate must be present and signed by the bank's
|
307
|
+
# root certificate for the validation to pass. Is not run if {#error} is present or response
|
308
|
+
# code is not ok.
|
207
309
|
def verify_certificate
|
208
310
|
return if @error
|
209
311
|
return unless response_code_is_ok?
|
@@ -213,6 +315,11 @@ module Sepa
|
|
213
315
|
end
|
214
316
|
end
|
215
317
|
|
318
|
+
# Checks whether response code in the response is ok. Response code is considered ok if it is
|
319
|
+
# "00" or "24".
|
320
|
+
#
|
321
|
+
# @return [true] if response code is ok
|
322
|
+
# @return [false] if response code is not ok
|
216
323
|
def response_code_is_ok?
|
217
324
|
return true if %w(00 24).include? response_code
|
218
325
|
|
data/lib/sepa/soap_builder.rb
CHANGED
@@ -1,10 +1,20 @@
|
|
1
1
|
module Sepa
|
2
|
+
|
3
|
+
# Builds a soap message with given parameters. This class is extended with proper bank module
|
4
|
+
# depending on bank.
|
2
5
|
class SoapBuilder
|
3
6
|
include Utilities
|
4
7
|
|
8
|
+
# Application request built with the same parameters as the soap
|
9
|
+
#
|
10
|
+
# @return [ApplicationRequest]
|
5
11
|
attr_reader :application_request
|
6
12
|
|
7
|
-
# SoapBuilder
|
13
|
+
# Initializes the {SoapBuilder} with the params hash and then extends the {SoapBuilder} with the
|
14
|
+
# correct bank module. The {SoapBuilder} class is usually created by the client which handles
|
15
|
+
# parameter validation.
|
16
|
+
#
|
17
|
+
# @param params [Hash] options hash
|
8
18
|
def initialize(params)
|
9
19
|
@bank = params[:bank]
|
10
20
|
@own_signing_certificate = params[:own_signing_certificate]
|
@@ -27,13 +37,16 @@ module Sepa
|
|
27
37
|
find_correct_bank_extension
|
28
38
|
end
|
29
39
|
|
40
|
+
# Returns the soap as raw xml
|
41
|
+
#
|
42
|
+
# @return [String] the soap as xml
|
30
43
|
def to_xml
|
31
|
-
# Returns a complete SOAP message in xml format
|
32
44
|
find_correct_build.to_xml
|
33
45
|
end
|
34
46
|
|
35
47
|
private
|
36
48
|
|
49
|
+
# Extends the class with proper module depending on bank
|
37
50
|
def find_correct_bank_extension
|
38
51
|
case @bank
|
39
52
|
when :danske
|
@@ -43,6 +56,13 @@ module Sepa
|
|
43
56
|
end
|
44
57
|
end
|
45
58
|
|
59
|
+
# Calculates digest hash for the given node in the given document. The node is canonicalized
|
60
|
+
# exclusively before digest calculation.
|
61
|
+
#
|
62
|
+
# @param doc [Nokogiri::XML] Document that contains the node
|
63
|
+
# @param node [String] The name of the node
|
64
|
+
# @return [String] the base64 encoded string
|
65
|
+
# @todo remove this method and use {Utilities#calculate_digest}
|
46
66
|
def calculate_digest(doc, node)
|
47
67
|
sha1 = OpenSSL::Digest::SHA1.new
|
48
68
|
node = doc.at_css(node)
|
@@ -55,6 +75,14 @@ module Sepa
|
|
55
75
|
encode(sha1.digest(canon_node)).gsub(/\s+/, "")
|
56
76
|
end
|
57
77
|
|
78
|
+
# Calculates signature for the given node in the given document. Uses the signing private key
|
79
|
+
# given to SoapBuilder for the signing. The node is canonicalized exclusively before signature
|
80
|
+
# calculation.
|
81
|
+
#
|
82
|
+
# @param doc [Nokogiri::XML] Document that contains the node
|
83
|
+
# @param node [String] Name of the node to calculate signature from
|
84
|
+
# @return [String] the base64 encoded signature
|
85
|
+
# @todo refactor to use canonicalization from utilities
|
58
86
|
def calculate_signature(doc, node)
|
59
87
|
sha1 = OpenSSL::Digest::SHA1.new
|
60
88
|
node = doc.at_css(node)
|
@@ -68,21 +96,43 @@ module Sepa
|
|
68
96
|
encode(signature).gsub(/\s+/, "")
|
69
97
|
end
|
70
98
|
|
99
|
+
# Loads soap header template to be later populated
|
100
|
+
#
|
101
|
+
# @return [Nokogiri::XML] the header as Nokogiri document
|
71
102
|
def load_header_template
|
72
103
|
path = File.open("#{SOAP_TEMPLATE_PATH}/header.xml")
|
73
104
|
Nokogiri::XML(path)
|
74
105
|
end
|
75
106
|
|
107
|
+
# Sets value to a node's content in the given document
|
108
|
+
# @param doc [Nokogiri::XML] The document that contains the node
|
109
|
+
# @param node [String] The name of the node which value is about to be set
|
110
|
+
# @param value [#to_s] The value which will be set to the node
|
76
111
|
def set_node(doc, node, value)
|
77
112
|
doc.at_css(node).content = value
|
78
113
|
end
|
79
114
|
|
115
|
+
# Adds soap body to header template
|
116
|
+
#
|
117
|
+
# @return [Nokogiri::XML] the soap with added body as a nokogiri document
|
80
118
|
def add_body_to_header
|
81
119
|
body = @template.at_css('env|Body')
|
82
120
|
@header_template.root.add_child(body)
|
83
121
|
@header_template
|
84
122
|
end
|
85
123
|
|
124
|
+
# Add needed information to soap header. Mainly security related stuff. The process is as
|
125
|
+
# follows:
|
126
|
+
# 1. The reference id of the security token is set using {#set_token_id} method
|
127
|
+
# 2. Created and expires timestamps are set. Expires is set to be 5 minutes after creation.
|
128
|
+
# 3. Timestamp reference id is set with {#set_node_id} method
|
129
|
+
# 4. The digest of timestamp node is calculated and set to correct node
|
130
|
+
# 5. The reference id of body is set with {#set_node_id}
|
131
|
+
# 6. The digest of body is calculated and set to correct node
|
132
|
+
# 7. The signature of SignedInfo node is calculated and added to correct node
|
133
|
+
# 8. Own signing certificate is formatted (Begin and end certificate removed and linebreaks
|
134
|
+
# removed) and embedded in the soap
|
135
|
+
# @todo split into smaller methods
|
86
136
|
def process_header
|
87
137
|
set_token_id
|
88
138
|
|
@@ -108,6 +158,7 @@ module Sepa
|
|
108
158
|
set_node(@header_template, 'wsse|BinarySecurityToken', formatted_cert)
|
109
159
|
end
|
110
160
|
|
161
|
+
# Generates a random token id and sets it to correct node
|
111
162
|
def set_token_id
|
112
163
|
security_token_id = "token-#{SecureRandom.uuid}"
|
113
164
|
|
data/lib/sepa/utilities.rb
CHANGED
@@ -1,6 +1,13 @@
|
|
1
1
|
module Sepa
|
2
|
+
|
3
|
+
# Contains utility methods that are used in this gem.
|
2
4
|
module Utilities
|
3
5
|
|
6
|
+
# Calculates a SHA1 digest for a given node. Before the calculation, the node is canonicalized
|
7
|
+
# exclusively.
|
8
|
+
#
|
9
|
+
# @param node [Nokogiri::Node] the node which the digest is calculated from
|
10
|
+
# @return [String] the calculated digest
|
4
11
|
def calculate_digest(node)
|
5
12
|
sha1 = OpenSSL::Digest::SHA1.new
|
6
13
|
|
@@ -9,9 +16,12 @@ module Sepa
|
|
9
16
|
encode(sha1.digest(canon_node)).gsub(/\s+/, "")
|
10
17
|
end
|
11
18
|
|
12
|
-
# Takes a certificate, adds begin and end
|
13
|
-
#
|
14
|
-
#
|
19
|
+
# Takes a certificate, adds begin and end certificate texts and splits it into multiple lines so
|
20
|
+
# that OpenSSL can read it.
|
21
|
+
#
|
22
|
+
# @param cert_value [#to_s] the certificate to be processed
|
23
|
+
# @return [String] the processed certificate
|
24
|
+
# @todo rename maybe because this seems more formatting than {#format_cert}
|
15
25
|
def process_cert_value(cert_value)
|
16
26
|
cert = "-----BEGIN CERTIFICATE-----\n"
|
17
27
|
cert << cert_value.to_s.gsub(/\s+/, "").scan(/.{1,64}/).join("\n")
|
@@ -19,6 +29,13 @@ module Sepa
|
|
19
29
|
cert << "-----END CERTIFICATE-----"
|
20
30
|
end
|
21
31
|
|
32
|
+
# Removes begin and end certificate texts from a certificate and removes whitespaces to make the
|
33
|
+
# certificate read to be embedded in xml.
|
34
|
+
#
|
35
|
+
# @param cert [#to_s] The certificate to be formatted
|
36
|
+
# @return [String] the formatted certificate
|
37
|
+
# @todo rename maybe
|
38
|
+
# @see #process_cert_value
|
22
39
|
def format_cert(cert)
|
23
40
|
cert = cert.to_s
|
24
41
|
cert = cert.split('-----BEGIN CERTIFICATE-----')[1]
|
@@ -26,12 +43,23 @@ module Sepa
|
|
26
43
|
cert.gsub!(/\s+/, "")
|
27
44
|
end
|
28
45
|
|
46
|
+
# Removes begin and end certificate request texts from a certificate signing request and removes
|
47
|
+
# whitespaces
|
48
|
+
#
|
49
|
+
# @param cert_request [String] the certificate request to be formatted
|
50
|
+
# @return [String] the formatted certificate request
|
51
|
+
# @todo rename
|
29
52
|
def format_cert_request(cert_request)
|
30
53
|
cert_request = cert_request.split('-----BEGIN CERTIFICATE REQUEST-----')[1]
|
31
54
|
cert_request = cert_request.split('-----END CERTIFICATE REQUEST-----')[0]
|
32
55
|
cert_request.gsub!(/\s+/, "")
|
33
56
|
end
|
34
57
|
|
58
|
+
# Validates whether a doc is valid against a schema. Adds error using ActiveModel validations if
|
59
|
+
# document is not valid against the schema.
|
60
|
+
#
|
61
|
+
# @param doc [Nokogiri::XML::Document] the document to validate
|
62
|
+
# @param schema [String] name of the schema file in {SCHEMA_PATH}
|
35
63
|
def check_validity_against_schema(doc, schema)
|
36
64
|
Dir.chdir(SCHEMA_PATH) do
|
37
65
|
xsd = Nokogiri::XML::Schema(IO.read(schema))
|
@@ -41,8 +69,16 @@ module Sepa
|
|
41
69
|
end
|
42
70
|
end
|
43
71
|
|
44
|
-
# Extracts a certificate from a document and
|
45
|
-
#
|
72
|
+
# Extracts a certificate from a document and returns it as an OpenSSL X509 certificate. Returns
|
73
|
+
# nil if the node cannot be found
|
74
|
+
#
|
75
|
+
# @param doc [Nokogiri::XML::Document] the document that contains the certificate node
|
76
|
+
# @param node [String] the name of the node that contains the certificate
|
77
|
+
# @param namespace [String] the namespace of the certificate node
|
78
|
+
# @return [OpenSSL::X509::Certificate] the extracted certificate if it is extracted successfully
|
79
|
+
# @return [nil] if the certificate cannot be found
|
80
|
+
# @raise [OpenSSL::X509::CertificateError] if there is a problem with the certificate
|
81
|
+
# @todo refactor not to fail
|
46
82
|
def extract_cert(doc, node, namespace)
|
47
83
|
cert_raw = doc.at("xmlns|#{node}", 'xmlns' => namespace)
|
48
84
|
|
@@ -56,10 +92,17 @@ module Sepa
|
|
56
92
|
x509_certificate(cert)
|
57
93
|
rescue => e
|
58
94
|
fail OpenSSL::X509::CertificateError,
|
59
|
-
"The certificate could not be processed. It's most likely corrupted.
|
95
|
+
"The certificate could not be processed. It's most likely corrupted. " \
|
96
|
+
"OpenSSL had this to say: #{e}."
|
60
97
|
end
|
61
98
|
end
|
62
99
|
|
100
|
+
# Checks whether a certificate signing request is valid
|
101
|
+
#
|
102
|
+
# @param cert_request [#to_s] the certificate signing request
|
103
|
+
# @return [true] if the certificate signing request is valid
|
104
|
+
# @return [false] if the certificate signing request is not valid
|
105
|
+
# @todo rename
|
63
106
|
def cert_request_valid?(cert_request)
|
64
107
|
begin
|
65
108
|
OpenSSL::X509::Request.new cert_request
|
@@ -70,6 +113,12 @@ module Sepa
|
|
70
113
|
true
|
71
114
|
end
|
72
115
|
|
116
|
+
# Loads a soap or application request xml template according to a parameter and command.
|
117
|
+
#
|
118
|
+
# @param template [String] path to a template directory. Currently supported values are defined
|
119
|
+
# in contants {AR_TEMPLATE_PATH} and {SOAP_TEMPLATE_PATH}.
|
120
|
+
# @return [Nokogiri::XML::Document] the loaded template
|
121
|
+
# @raise [ArgumentError] if a template cannot be found for a command
|
73
122
|
def load_body_template(template)
|
74
123
|
path = "#{template}/"
|
75
124
|
|
@@ -95,49 +144,144 @@ module Sepa
|
|
95
144
|
xml_doc(File.open(path))
|
96
145
|
end
|
97
146
|
|
147
|
+
# Gets current utc time in iso-format
|
148
|
+
#
|
149
|
+
# @return [String] current utc time in iso-format
|
98
150
|
def iso_time
|
99
151
|
@iso_time ||= Time.now.utc.iso8601
|
100
152
|
end
|
101
153
|
|
154
|
+
# Calculates an HMAC for a given pin and certificate signing request. Used by Nordea certificate
|
155
|
+
# requests.
|
156
|
+
#
|
157
|
+
# @param pin [#to_s] the one-time pin number got from bank
|
158
|
+
# @param csr [#to_s] the certificate signing request
|
159
|
+
# @return [String] the generated HMAC for the values
|
102
160
|
def hmac(pin, csr)
|
103
161
|
encode(OpenSSL::HMAC.digest('sha1', pin, csr)).chop
|
104
162
|
end
|
105
163
|
|
164
|
+
# Converts a certificate signing request from base64 encoded string to binary string
|
165
|
+
#
|
166
|
+
# @param csr [#to_s] certificate signing request in base64 encoded format
|
167
|
+
# @return [String] the certificate signing request in binary format
|
106
168
|
def csr_to_binary(csr)
|
107
169
|
OpenSSL::X509::Request.new(csr).to_der
|
108
170
|
end
|
109
171
|
|
172
|
+
# Canonicalizes a node inclusively
|
173
|
+
#
|
174
|
+
# @param doc [Nokogiri::XML::Document] the document that contains the node
|
175
|
+
# @param namespace [String] the namespace of the node
|
176
|
+
# @param node [String] name of the node
|
177
|
+
# @return [String] the canonicalized node if the node can be found
|
178
|
+
# @return [nil] if the node cannot be found
|
110
179
|
def canonicalized_node(doc, namespace, node)
|
111
180
|
content_node = doc.at("xmlns|#{node}", xmlns: namespace)
|
112
181
|
content_node.canonicalize if content_node
|
113
182
|
end
|
114
183
|
|
184
|
+
# Converts an xml string to a nokogiri document
|
185
|
+
#
|
186
|
+
# @param value [to_s] the xml document
|
187
|
+
# @return [Nokogiri::XML::Document] the xml document
|
115
188
|
def xml_doc(value)
|
116
189
|
Nokogiri::XML value
|
117
190
|
end
|
118
191
|
|
192
|
+
# Decodes a base64 encoded string
|
193
|
+
#
|
194
|
+
# @param value [#to_s] the base64 encoded string
|
195
|
+
# @return [String] the decoded string
|
119
196
|
def decode(value)
|
120
197
|
Base64.decode64 value
|
121
198
|
end
|
122
199
|
|
200
|
+
# Canonicalizes an xml node exclusively without comments
|
201
|
+
#
|
202
|
+
# @param value [Nokogiri::XML::Node, #canonicalize] the node to be canonicalized
|
203
|
+
# @return [String] the canonicalized node
|
123
204
|
def canonicalize_exclusively(value)
|
124
205
|
value.canonicalize(mode = Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0,
|
125
206
|
inclusive_namespaces = nil,
|
126
207
|
with_comments = false)
|
127
208
|
end
|
128
209
|
|
210
|
+
# Creates a new OpenSSL X509 certificate from a string
|
211
|
+
#
|
212
|
+
# @param value [#to_s] the string from which to create the certificate
|
213
|
+
# @return [OpenSSL::X509::Certificate] the OpenSSL X509 certificate
|
214
|
+
# @example Example certificate to convert
|
215
|
+
# "-----BEGIN CERTIFICATE-----
|
216
|
+
# MIIDwTCCAqmgAwIBAgIEAX1JuTANBgkqhkiG9w0BAQUFADBkMQswCQYDVQQGEwJT
|
217
|
+
# RTEeMBwGA1UEChMVTm9yZGVhIEJhbmsgQUIgKHB1YmwpMR8wHQYDVQQDExZOb3Jk
|
218
|
+
# ZWEgQ29ycG9yYXRlIENBIDAxMRQwEgYDVQQFEws1MTY0MDYtMDEyMDAeFw0xMzA1
|
219
|
+
# MDIxMjI2MzRaFw0xNTA1MDIxMjI2MzRaMEQxCzAJBgNVBAYTAkZJMSAwHgYDVQQD
|
220
|
+
# DBdOb3JkZWEgRGVtbyBDZXJ0aWZpY2F0ZTETMBEGA1UEBRMKNTc4MDg2MDIzODCB
|
221
|
+
# nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwtFEfAtbJuGzQwwRumZkvYh2BjGY
|
222
|
+
# VsAMUeiKtOne3bZSeisfCq+TXqL1gI9LofyeAQ9I/sDm6tL80yrD5iaSUqVm6A73
|
223
|
+
# 9MsmpW/iyZcVf7ms8xAN51ESUgN6akwZCU9pH62ngJDj2gUsktY0fpsoVsARdrvO
|
224
|
+
# Fk0fTSUXKWd6LbcCAwEAAaOCAR0wggEZMAkGA1UdEwQCMAAwEQYDVR0OBAoECEBw
|
225
|
+
# 2cj7+XMAMBMGA1UdIAQMMAowCAYGKoVwRwEDMBMGA1UdIwQMMAqACEALddbbzwun
|
226
|
+
# MDcGCCsGAQUFBwEBBCswKTAnBggrBgEFBQcwAYYbaHR0cDovL29jc3Aubm9yZGVh
|
227
|
+
# LnNlL0NDQTAxMA4GA1UdDwEB/wQEAwIFoDCBhQYDVR0fBH4wfDB6oHigdoZ0bGRh
|
228
|
+
# cCUzQS8vbGRhcC5uYi5zZS9jbiUzRE5vcmRlYStDb3Jwb3JhdGUrQ0ErMDElMkNv
|
229
|
+
# JTNETm9yZGVhK0JhbmsrQUIrJTI4cHVibCUyOSUyQ2MlM0RTRSUzRmNlcnRpZmlj
|
230
|
+
# YXRlcmV2b2NhdGlvbmxpc3QwDQYJKoZIhvcNAQEFBQADggEBACLUPB1Gmq6286/s
|
231
|
+
# ROADo7N+w3eViGJ2fuOTLMy4R0UHOznKZNsuk4zAbS2KycbZsE5py4L8o+IYoaS8
|
232
|
+
# 8YHtEeckr2oqHnPpz/0Eg7wItj8Ad+AFWJqzbn6Hu/LQhlnl5JEzXzl3eZj9oiiJ
|
233
|
+
# 1q/2CGXvFomY7S4tgpWRmYULtCK6jode0NhgNnAgOI9uy76pSS16aDoiQWUJqQgV
|
234
|
+
# ydowAnqS9h9aQ6gedwbOdtkWmwKMDVXU6aRz9Gvk+JeYJhtpuP3OPNGbbC5L7NVd
|
235
|
+
# no+B6AtwxmG3ozd+mPcMeVuz6kKLAmQyIiBSrRNa5OrTkq/CUzxO9WUgTnm/Sri7
|
236
|
+
# zReR6mU=
|
237
|
+
# -----END CERTIFICATE-----"
|
129
238
|
def x509_certificate(value)
|
130
239
|
OpenSSL::X509::Certificate.new value
|
131
240
|
end
|
132
241
|
|
242
|
+
# Base64 encodes a given value
|
243
|
+
#
|
244
|
+
# @param value [#to_s] the value to be encoded
|
245
|
+
# @return [String] the base64 encoded string
|
133
246
|
def encode(value)
|
134
247
|
Base64.encode64 value
|
135
248
|
end
|
136
249
|
|
250
|
+
# Creates a new OpenSSL RSA key from a string key
|
251
|
+
#
|
252
|
+
# @param key_as_string [to_s] the key as a string
|
253
|
+
# @return [OpenSSL::PKey::RSA] the OpenSSL RSA key
|
254
|
+
# @example Example key to convert
|
255
|
+
# "-----BEGIN PRIVATE KEY-----
|
256
|
+
# MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAMLRRHwLWybhs0MM
|
257
|
+
# EbpmZL2IdgYxmFbADFHoirTp3t22UnorHwqvk16i9YCPS6H8ngEPSP7A5urS/NMq
|
258
|
+
# w+YmklKlZugO9/TLJqVv4smXFX+5rPMQDedRElIDempMGQlPaR+tp4CQ49oFLJLW
|
259
|
+
# NH6bKFbAEXa7zhZNH00lFylnei23AgMBAAECgYEAqt912/7x4jaQTrxlSELLFVp9
|
260
|
+
# eo1BesVTiPwXvPpsGbbyvGjZ/ztkXNs9zZbh1aCGzZMkiR2U7F5GlsiprlIif4cF
|
261
|
+
# 6Xz7rCjaAs7iDRt9PjhjVuqNGR2I+VIIlbQ9XWFJ3lJFW3v7TIZ8JbLnn0XOFz+Z
|
262
|
+
# BBSSGTK1zTNh4TBQtjECQQDe5M3uu9m4RwSw9R6GaDw/IFQZgr0oWSv0WIjRwvwW
|
263
|
+
# nFnSX2lbkNAjulP0daGsmn7vxIpqZxPxwcrU4wFqTF5dAkEA38DnbCm3YfogzwLH
|
264
|
+
# Nre2hBmGqjWarhtxqtRarrkgnmOd8W0Z1Hb1dSHrliUSVSrINbK5ZdEV15Rpu7VD
|
265
|
+
# OePzIwJAPMslS+8alANyyR0iJUC65fDYX1jkZOPldDDNqIDJJxWf/hwd7WaTDpuc
|
266
|
+
# mHmZDi3ZX2Y45oqUywSzYNtFoIuR1QJAZYUZuyqmSK77SdGB36K1DfSi9AFEQDC1
|
267
|
+
# fwPAbTwTv6mFFPAiYxLiRZXxVPtW+QtjMXH4ymh2V4y/+GnCqbZyLwJBAJQSDAME
|
268
|
+
# Sn4Uz7Zjk3UrBIbMYEv0u2mcCypwsb0nGE5/gzDPjGE9cxWW+rXARIs+sNQVClnh
|
269
|
+
# 45nhdfYxOjgYff0=
|
270
|
+
# -----END PRIVATE KEY-----"
|
137
271
|
def rsa_key(key_as_string)
|
138
272
|
OpenSSL::PKey::RSA.new key_as_string
|
139
273
|
end
|
140
274
|
|
275
|
+
# Generates a random id for a node in soap and sets it to the soap header
|
276
|
+
#
|
277
|
+
# @param document [Nokogiri::XML::Document] the document that contains the node
|
278
|
+
# @param namespace [String] the namespace of the node
|
279
|
+
# @param node [String] name of the node
|
280
|
+
# @param position [Integer] the soap header might contain many references and this parameter
|
281
|
+
# defines which reference is used. Numbering starts from 0.
|
282
|
+
# @return [String] the generated id of the node
|
283
|
+
# @todo create functionality to automatically add reference nodes to header so than position is
|
284
|
+
# not needed
|
141
285
|
def set_node_id(document, namespace, node, position)
|
142
286
|
node_id = "#{node.downcase}-#{SecureRandom.uuid}"
|
143
287
|
document.at("xmlns|#{node}", xmlns: namespace)['wsu:Id'] = node_id
|
@@ -146,6 +290,15 @@ module Sepa
|
|
146
290
|
node_id
|
147
291
|
end
|
148
292
|
|
293
|
+
# Verifies that a signature has been created with the private key of a certificate
|
294
|
+
#
|
295
|
+
# @param doc [Nokogiri::XML::Document] the document that contains the signature
|
296
|
+
# @param certificate [OpenSSL::X509::Certificate] the certificate to verify the signature
|
297
|
+
# against
|
298
|
+
# @param canonicalization_method [Symbol] The canonicalization method that has been used to
|
299
|
+
# canonicalize the SignedInfo node. Accepts `:normal` or `:exclusive`.
|
300
|
+
# @return [true] if signature verifies
|
301
|
+
# @return [false] if signature fails to verify or if it cannot be found
|
149
302
|
def validate_signature(doc, certificate, canonicalization_method)
|
150
303
|
node = doc.at('xmlns|SignedInfo', xmlns: DSIG)
|
151
304
|
|
@@ -165,6 +318,14 @@ module Sepa
|
|
165
318
|
certificate.public_key.verify(OpenSSL::Digest::SHA1.new, signature, node)
|
166
319
|
end
|
167
320
|
|
321
|
+
# Verifies that a certificate has been signed by the private key of a root certificate
|
322
|
+
#
|
323
|
+
# @param certificate [OpenSSL::X509::Certificate] the certificate to verify
|
324
|
+
# @param root_certificate [OpenSSL::X509::Certificate] the root certificate
|
325
|
+
# @return [true] if the certificate has been signed by the private key of the root certificate
|
326
|
+
# @return [false] if the certificate has not been signed by the private key of the root
|
327
|
+
# certificate, the certificates are nil or the subject of the root certificate is not the
|
328
|
+
# issuer of the certificate
|
168
329
|
def verify_certificate_against_root_certificate(certificate, root_certificate)
|
169
330
|
return false unless certificate && root_certificate
|
170
331
|
return false unless root_certificate.subject == certificate.issuer
|
data/lib/sepa/version.rb
CHANGED