sepafm 0.1.0 → 0.1.1
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/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
|