sepafm 0.1.5 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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