sepafm 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/sepa/application_request.rb +13 -11
- data/lib/sepa/application_response.rb +23 -23
- data/lib/sepa/attribute_checks.rb +0 -21
- data/lib/sepa/banks/danske/danske_response.rb +40 -14
- data/lib/sepa/banks/danske/soap_danske.rb +127 -125
- data/lib/sepa/banks/nordea/nordea_response.rb +11 -11
- data/lib/sepa/banks/nordea/soap_nordea.rb +2 -2
- data/lib/sepa/client.rb +40 -3
- data/lib/sepa/response.rb +68 -79
- data/lib/sepa/soap_builder.rb +20 -19
- data/lib/sepa/utilities.rb +30 -15
- data/lib/sepa/version.rb +1 -1
- data/lib/sepafm.rb +14 -0
- data/{README.md → readme.md} +1 -1
- data/test/sepa/banks/danske/danske_cert_response_test.rb +28 -14
- data/test/sepa/banks/danske/danske_cert_soap_builder_test.rb +3 -3
- data/test/sepa/banks/danske/danske_generic_soap_builder_test.rb +4 -4
- data/test/sepa/banks/danske/responses/create_cert.xml +14 -37
- data/test/sepa/banks/nordea/nordea_application_request_test.rb +9 -9
- data/test/sepa/banks/nordea/nordea_application_response_test.rb +69 -57
- data/test/sepa/banks/nordea/nordea_cert_application_request_test.rb +2 -2
- data/test/sepa/banks/nordea/nordea_cert_request_soap_builder_test.rb +1 -1
- data/test/sepa/banks/nordea/nordea_generic_soap_builder_test.rb +4 -4
- data/test/sepa/banks/nordea/nordea_response_test.rb +56 -34
- data/test/sepa/client_test.rb +43 -34
- data/test/sepa/fixtures.rb +1 -1
- data/test/sepa/sepa_test.rb +1 -1
- data/test/test_helper.rb +15 -0
- metadata +3 -3
@@ -1,20 +1,20 @@
|
|
1
1
|
module Sepa
|
2
2
|
class NordeaResponse < Response
|
3
|
+
include Utilities
|
3
4
|
|
4
|
-
def
|
5
|
-
|
5
|
+
def own_signing_cert
|
6
|
+
application_response = extract_application_response(NORDEA_PKI)
|
7
|
+
at = 'xmlns|Certificate > xmlns|Certificate'
|
8
|
+
node = Nokogiri::XML(application_response).at(at, xmlns: NORDEA_XML_DATA)
|
6
9
|
|
7
|
-
|
8
|
-
@application_response = extract_application_response('http://bxd.fi/CertificateService')
|
9
|
-
@content = extract_own_cert
|
10
|
-
end
|
11
|
-
end
|
10
|
+
return unless node
|
12
11
|
|
13
|
-
|
14
|
-
|
15
|
-
|
12
|
+
cert_value = process_cert_value node.content
|
13
|
+
cert = x509_certificate cert_value
|
14
|
+
cert_plain = cert.to_s
|
16
15
|
|
17
|
-
|
16
|
+
encode cert_plain
|
18
17
|
end
|
18
|
+
|
19
19
|
end
|
20
20
|
end
|
@@ -18,7 +18,7 @@ module Sepa
|
|
18
18
|
end
|
19
19
|
|
20
20
|
def set_body_contents
|
21
|
-
set_node(@template, 'cer|ApplicationRequest', @
|
21
|
+
set_node(@template, 'cer|ApplicationRequest', @application_request.to_base64)
|
22
22
|
set_node(@template, 'cer|SenderId', @customer_id)
|
23
23
|
set_node(@template, 'cer|RequestId', request_id)
|
24
24
|
set_node(@template, 'cer|Timestamp', iso_time)
|
@@ -34,7 +34,7 @@ module Sepa
|
|
34
34
|
end
|
35
35
|
|
36
36
|
def common_set_body_contents
|
37
|
-
set_node(@template, 'bxd|ApplicationRequest', @
|
37
|
+
set_node(@template, 'bxd|ApplicationRequest', @application_request.to_base64)
|
38
38
|
set_node(@template, 'bxd|SenderId', @customer_id)
|
39
39
|
set_node(@template, 'bxd|RequestId', request_id)
|
40
40
|
set_node(@template, 'bxd|Timestamp', iso_time)
|
data/lib/sepa/client.rb
CHANGED
@@ -35,6 +35,8 @@ module Sepa
|
|
35
35
|
def initialize(hash = {})
|
36
36
|
self.attributes hash
|
37
37
|
self.environment ||= 'PRODUCTION'
|
38
|
+
self.language ||= 'EN'
|
39
|
+
self.status ||= 'NEW'
|
38
40
|
end
|
39
41
|
|
40
42
|
def bank=(value)
|
@@ -56,13 +58,26 @@ module Sepa
|
|
56
58
|
|
57
59
|
soap = SoapBuilder.new(create_hash).to_xml
|
58
60
|
client = Savon.client(wsdl: wsdl)
|
59
|
-
|
61
|
+
|
62
|
+
begin
|
63
|
+
response = client.call(command, xml: soap)
|
64
|
+
response &&= response.to_xml
|
65
|
+
rescue Savon::Error => e
|
66
|
+
response = nil
|
67
|
+
error = e.to_s
|
68
|
+
end
|
69
|
+
|
70
|
+
options = {
|
71
|
+
response: response,
|
72
|
+
error: error,
|
73
|
+
command: command
|
74
|
+
}
|
60
75
|
|
61
76
|
case bank
|
62
77
|
when :nordea
|
63
|
-
NordeaResponse.new
|
78
|
+
NordeaResponse.new options
|
64
79
|
when :danske
|
65
|
-
DanskeResponse.new
|
80
|
+
DanskeResponse.new options
|
66
81
|
end
|
67
82
|
end
|
68
83
|
|
@@ -87,5 +102,27 @@ module Sepa
|
|
87
102
|
@private_key = OpenSSL::PKey::RSA.new(@private_key) if @private_key
|
88
103
|
end
|
89
104
|
|
105
|
+
# Returns path to WSDL file
|
106
|
+
def wsdl
|
107
|
+
case bank
|
108
|
+
when :nordea
|
109
|
+
if command == :get_certificate
|
110
|
+
file = "wsdl_nordea_cert.xml"
|
111
|
+
else
|
112
|
+
file = "wsdl_nordea.xml"
|
113
|
+
end
|
114
|
+
when :danske
|
115
|
+
if [:get_bank_certificate, :create_certificate].include? command
|
116
|
+
file = "wsdl_danske_cert.xml"
|
117
|
+
else
|
118
|
+
file = "wsdl_danske.xml"
|
119
|
+
end
|
120
|
+
else
|
121
|
+
return nil
|
122
|
+
end
|
123
|
+
|
124
|
+
"#{WSDL_PATH}/#{file}"
|
125
|
+
end
|
126
|
+
|
90
127
|
end
|
91
128
|
end
|
data/lib/sepa/response.rb
CHANGED
@@ -3,27 +3,21 @@ module Sepa
|
|
3
3
|
include ActiveModel::Validations
|
4
4
|
include Utilities
|
5
5
|
|
6
|
-
attr_reader :soap, :
|
6
|
+
attr_reader :soap, :error, :command
|
7
7
|
|
8
8
|
validates :soap, presence: true
|
9
9
|
validate :validate_document_format
|
10
10
|
validate :document_must_validate_against_schema
|
11
|
+
validate :client_errors
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
@
|
16
|
-
|
17
|
-
|
18
|
-
# Check if command is one of the generic commands which should behave the same way across
|
19
|
-
# different banks
|
20
|
-
if GENERIC_COMMANDS.include? command
|
21
|
-
xsd = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd'
|
13
|
+
def initialize(hash = {})
|
14
|
+
@soap = hash[:response]
|
15
|
+
@command = hash[:command]
|
16
|
+
@error = hash[:error]
|
17
|
+
end
|
22
18
|
|
23
|
-
|
24
|
-
|
25
|
-
@content = extract_content
|
26
|
-
end
|
19
|
+
def doc
|
20
|
+
@doc ||= xml_doc @soap
|
27
21
|
end
|
28
22
|
|
29
23
|
# Verifies that all digest values in the response match the actual ones.
|
@@ -31,7 +25,7 @@ module Sepa
|
|
31
25
|
# i.e. verbose: true
|
32
26
|
def hashes_match?(options = {})
|
33
27
|
digests = find_digest_values
|
34
|
-
nodes = find_nodes_to_verify(
|
28
|
+
nodes = find_nodes_to_verify(digests)
|
35
29
|
|
36
30
|
verified_digests = digests.select do |uri, digest|
|
37
31
|
uri = uri.sub(/^#/, '')
|
@@ -55,58 +49,76 @@ module Sepa
|
|
55
49
|
# Verifies the signature by extracting the public key from the certificate
|
56
50
|
# embedded in the soap header and verifying the signature value with that.
|
57
51
|
def signature_is_valid?
|
58
|
-
node =
|
52
|
+
node = doc.at('xmlns|SignedInfo', xmlns: DSIG)
|
59
53
|
|
60
|
-
node = node
|
61
|
-
mode = Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0,
|
62
|
-
inclusive_namespaces = nil, with_comments = false
|
63
|
-
)
|
54
|
+
node = canonicalize_exclusively node
|
64
55
|
|
65
|
-
signature =
|
66
|
-
'xmlns|SignatureValue',
|
67
|
-
'xmlns' => 'http://www.w3.org/2000/09/xmldsig#'
|
68
|
-
).content
|
56
|
+
signature = doc.at('xmlns|SignatureValue', xmlns: DSIG).content
|
69
57
|
|
70
|
-
signature =
|
58
|
+
signature = decode(signature)
|
71
59
|
|
72
60
|
certificate.public_key.verify(OpenSSL::Digest::SHA1.new, signature, node)
|
73
61
|
end
|
74
62
|
|
75
|
-
# Gets the application response from the response as an
|
76
|
-
# document
|
63
|
+
# Gets the application response from the response as an xml document
|
77
64
|
def application_response
|
78
|
-
|
79
|
-
ar = Base64.decode64(ar)
|
80
|
-
Nokogiri::XML(ar)
|
65
|
+
@application_response ||= extract_application_response(BXD)
|
81
66
|
end
|
82
67
|
|
83
68
|
def file_references
|
84
69
|
return unless @command == :download_file_list
|
85
70
|
|
86
71
|
@file_references ||= begin
|
87
|
-
|
88
|
-
descriptors =
|
72
|
+
xml = xml_doc content
|
73
|
+
descriptors = xml.css('FileDescriptor')
|
89
74
|
descriptors.map { |descriptor| descriptor.at('FileReference').content }
|
90
75
|
end
|
91
76
|
end
|
92
77
|
|
78
|
+
def certificate
|
79
|
+
@certificate ||= begin
|
80
|
+
extract_cert(doc, 'BinarySecurityToken', OASIS_SECEXT)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def content
|
85
|
+
@content ||= begin
|
86
|
+
xml = xml_doc(application_response)
|
87
|
+
|
88
|
+
case @command
|
89
|
+
when :download_file
|
90
|
+
content_node = xml.at('xmlns|Content', xmlns: XML_DATA)
|
91
|
+
content_node.content if content_node
|
92
|
+
when :download_file_list
|
93
|
+
content_node = xml.remove_namespaces!.at('FileDescriptors')
|
94
|
+
content_node.to_xml if content_node
|
95
|
+
when :get_user_info
|
96
|
+
canonicalized_node(xml, XML_DATA, 'UserFileTypes')
|
97
|
+
when :upload_file
|
98
|
+
signature_node = xml.at('xmlns|Signature', xmlns: DSIG)
|
99
|
+
if signature_node
|
100
|
+
signature_node.remove
|
101
|
+
xml.canonicalize
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def to_s
|
108
|
+
@soap
|
109
|
+
end
|
110
|
+
|
93
111
|
private
|
94
112
|
|
95
113
|
# Finds all reference nodes with digest values in the document and returns
|
96
114
|
# a hash with uri as the key and digest as the value.
|
97
115
|
def find_digest_values
|
98
116
|
references = {}
|
99
|
-
reference_nodes =
|
100
|
-
'xmlns|Reference',
|
101
|
-
'xmlns' => 'http://www.w3.org/2000/09/xmldsig#'
|
102
|
-
)
|
117
|
+
reference_nodes = doc.css('xmlns|Reference', xmlns: DSIG)
|
103
118
|
|
104
119
|
reference_nodes.each do |node|
|
105
120
|
uri = node.attr('URI')
|
106
|
-
digest_value = node.
|
107
|
-
'xmlns|DigestValue',
|
108
|
-
'xmlns' => 'http://www.w3.org/2000/09/xmldsig#'
|
109
|
-
).content
|
121
|
+
digest_value = node.at('xmlns|DigestValue', xmlns: DSIG).content
|
110
122
|
|
111
123
|
references[uri] = digest_value
|
112
124
|
end
|
@@ -116,17 +128,12 @@ module Sepa
|
|
116
128
|
|
117
129
|
# Finds nodes to verify by comparing their id's to the uris' in the
|
118
130
|
# references hash.
|
119
|
-
def find_nodes_to_verify(
|
131
|
+
def find_nodes_to_verify(references)
|
120
132
|
nodes = {}
|
121
133
|
|
122
|
-
references.each do |uri,
|
134
|
+
references.each do |uri, _digest_value|
|
123
135
|
uri = uri.sub(/^#/, '')
|
124
|
-
|
125
|
-
|
126
|
-
node = doc.at_css(
|
127
|
-
"[wsu|Id='#{uri}']",
|
128
|
-
'wsu' => wsu
|
129
|
-
)
|
136
|
+
node = find_node_by_uri(uri)
|
130
137
|
|
131
138
|
nodes[uri] = calculate_digest(node)
|
132
139
|
end
|
@@ -135,45 +142,27 @@ module Sepa
|
|
135
142
|
end
|
136
143
|
|
137
144
|
def validate_document_format
|
138
|
-
unless
|
139
|
-
errors.add(:base, 'Document must be a
|
145
|
+
unless doc.respond_to?(:canonicalize)
|
146
|
+
errors.add(:base, 'Document must be a valid XML file')
|
140
147
|
end
|
141
148
|
end
|
142
149
|
|
143
150
|
def document_must_validate_against_schema
|
144
|
-
check_validity_against_schema(
|
151
|
+
check_validity_against_schema(doc, 'soap.xsd')
|
145
152
|
end
|
146
153
|
|
147
|
-
def
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
case @command
|
152
|
-
when :download_file
|
153
|
-
content_node = xml.at('xmlns|Content', xmlns: xmlns)
|
154
|
-
content_node.content if content_node
|
155
|
-
when :download_file_list
|
156
|
-
content_node = xml.remove_namespaces!.at('FileDescriptors')
|
157
|
-
content_node.to_xml if content_node
|
158
|
-
when :get_user_info
|
159
|
-
canonicalized_node(xml, xmlns, 'UserFileTypes')
|
160
|
-
when :upload_file
|
161
|
-
signature_node = xml.at('xmlns|Signature', xmlns: 'http://www.w3.org/2000/09/xmldsig#')
|
162
|
-
if signature_node
|
163
|
-
signature_node.remove
|
164
|
-
xml.canonicalize
|
165
|
-
end
|
166
|
-
end
|
154
|
+
def extract_application_response(namespace)
|
155
|
+
ar_node = doc.at('xmlns|ApplicationResponse', xmlns: namespace)
|
156
|
+
decode(ar_node.content)
|
167
157
|
end
|
168
158
|
|
169
|
-
def
|
170
|
-
|
171
|
-
|
172
|
-
|
159
|
+
def client_errors
|
160
|
+
client_error = error.to_s
|
161
|
+
errors.add(:base, client_error) unless client_error.empty?
|
162
|
+
end
|
173
163
|
|
174
|
-
|
175
|
-
|
176
|
-
end
|
164
|
+
def find_node_by_uri(uri)
|
165
|
+
doc.at("[xmlns|Id='#{uri}']", xmlns: OASIS_UTILITY)
|
177
166
|
end
|
178
167
|
|
179
168
|
end
|
data/lib/sepa/soap_builder.rb
CHANGED
@@ -2,26 +2,27 @@ module Sepa
|
|
2
2
|
class SoapBuilder
|
3
3
|
include Utilities
|
4
4
|
|
5
|
-
attr_reader :
|
5
|
+
attr_reader :application_request
|
6
6
|
|
7
7
|
# SoapBuilder creates the SOAP structure.
|
8
8
|
def initialize(params)
|
9
|
-
@bank
|
10
|
-
@
|
11
|
-
@
|
12
|
-
@
|
13
|
-
@customer_id
|
14
|
-
@
|
15
|
-
@
|
16
|
-
@
|
17
|
-
@
|
18
|
-
@
|
19
|
-
@
|
20
|
-
@
|
21
|
-
@
|
22
|
-
|
23
|
-
@
|
24
|
-
@
|
9
|
+
@bank = params[:bank]
|
10
|
+
@cert = params[:cert]
|
11
|
+
@command = params[:command]
|
12
|
+
@content = params[:content]
|
13
|
+
@customer_id = params[:customer_id]
|
14
|
+
@enc_cert = params[:enc_cert]
|
15
|
+
@environment = params[:environment]
|
16
|
+
@file_reference = params[:file_reference]
|
17
|
+
@file_type = params[:file_type]
|
18
|
+
@language = params[:language]
|
19
|
+
@private_key = params[:private_key]
|
20
|
+
@status = params[:status]
|
21
|
+
@target_id = params[:target_id]
|
22
|
+
|
23
|
+
@application_request = ApplicationRequest.new params
|
24
|
+
@header_template = load_header_template
|
25
|
+
@template = load_body_template SOAP_TEMPLATE_PATH
|
25
26
|
|
26
27
|
find_correct_bank_extension
|
27
28
|
end
|
@@ -51,7 +52,7 @@ module Sepa
|
|
51
52
|
inclusive_namespaces = nil, with_comments = false
|
52
53
|
)
|
53
54
|
|
54
|
-
|
55
|
+
encode(sha1.digest(canon_node)).gsub(/\s+/, "")
|
55
56
|
end
|
56
57
|
|
57
58
|
def calculate_signature(doc, node)
|
@@ -64,7 +65,7 @@ module Sepa
|
|
64
65
|
)
|
65
66
|
|
66
67
|
signature = @private_key.sign(sha1, canon_signed_info_node)
|
67
|
-
|
68
|
+
encode(signature).gsub(/\s+/, "")
|
68
69
|
end
|
69
70
|
|
70
71
|
def load_header_template
|
data/lib/sepa/utilities.rb
CHANGED
@@ -4,12 +4,9 @@ module Sepa
|
|
4
4
|
def calculate_digest(node)
|
5
5
|
sha1 = OpenSSL::Digest::SHA1.new
|
6
6
|
|
7
|
-
canon_node = node
|
8
|
-
mode = Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0,
|
9
|
-
inclusive_namespaces = nil, with_comments = false
|
10
|
-
)
|
7
|
+
canon_node = canonicalize_exclusively node
|
11
8
|
|
12
|
-
|
9
|
+
encode(sha1.digest(canon_node)).gsub(/\s+/, "")
|
13
10
|
end
|
14
11
|
|
15
12
|
# Takes a certificate, adds begin and end
|
@@ -17,9 +14,9 @@ module Sepa
|
|
17
14
|
# can read it.
|
18
15
|
def process_cert_value(cert_value)
|
19
16
|
cert = "-----BEGIN CERTIFICATE-----\n"
|
20
|
-
cert
|
21
|
-
cert
|
22
|
-
cert
|
17
|
+
cert << cert_value.to_s.gsub(/\s+/, "").scan(/.{1,64}/).join("\n")
|
18
|
+
cert << "\n"
|
19
|
+
cert << "-----END CERTIFICATE-----"
|
23
20
|
end
|
24
21
|
|
25
22
|
def format_cert(cert)
|
@@ -47,18 +44,14 @@ module Sepa
|
|
47
44
|
# Extracts a certificate from a document and return it as an OpenSSL X509 certificate
|
48
45
|
# Return nil is the node cannot be found
|
49
46
|
def extract_cert(doc, node, namespace)
|
50
|
-
return nil unless doc.respond_to? :at
|
51
|
-
|
52
47
|
cert_raw = doc.at("xmlns|#{node}", 'xmlns' => namespace)
|
53
48
|
|
54
|
-
return nil if cert_raw.nil?
|
55
|
-
|
56
49
|
cert_raw = cert_raw.content.gsub(/\s+/, "")
|
57
50
|
|
58
51
|
cert = process_cert_value(cert_raw)
|
59
52
|
|
60
53
|
begin
|
61
|
-
|
54
|
+
x509_certificate(cert)
|
62
55
|
rescue => e
|
63
56
|
fail OpenSSL::X509::CertificateError,
|
64
57
|
"The certificate could not be processed. It's most likely corrupted. OpenSSL had this to say: #{e}."
|
@@ -97,7 +90,7 @@ module Sepa
|
|
97
90
|
fail ArgumentError
|
98
91
|
end
|
99
92
|
|
100
|
-
|
93
|
+
xml_doc(File.open(path))
|
101
94
|
end
|
102
95
|
|
103
96
|
# Checks that the certificate in the application response is signed with the
|
@@ -116,7 +109,7 @@ module Sepa
|
|
116
109
|
end
|
117
110
|
|
118
111
|
def hmac(pin, csr)
|
119
|
-
|
112
|
+
encode(OpenSSL::HMAC.digest('sha1', pin, csr)).chop
|
120
113
|
end
|
121
114
|
|
122
115
|
def csr_to_binary(csr)
|
@@ -128,5 +121,27 @@ module Sepa
|
|
128
121
|
content_node.canonicalize if content_node
|
129
122
|
end
|
130
123
|
|
124
|
+
def xml_doc(value)
|
125
|
+
Nokogiri::XML value
|
126
|
+
end
|
127
|
+
|
128
|
+
def decode(value)
|
129
|
+
Base64.decode64 value
|
130
|
+
end
|
131
|
+
|
132
|
+
def canonicalize_exclusively(value)
|
133
|
+
value.canonicalize(mode = Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0,
|
134
|
+
inclusive_namespaces = nil,
|
135
|
+
with_comments = false)
|
136
|
+
end
|
137
|
+
|
138
|
+
def x509_certificate(value)
|
139
|
+
OpenSSL::X509::Certificate.new value
|
140
|
+
end
|
141
|
+
|
142
|
+
def encode(value)
|
143
|
+
Base64.encode64 value
|
144
|
+
end
|
145
|
+
|
131
146
|
end
|
132
147
|
end
|