as2 0.6.0 → 0.7.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/CHANGELOG.md +12 -1
- data/README.md +3 -5
- data/lib/as2/client.rb +120 -9
- data/lib/as2/config.rb +19 -1
- data/lib/as2/server.rb +111 -16
- data/lib/as2/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 78765b4a0fba8aaa7e3531ee9e3841a0774d72b6a1d09b18a69b53117cf26e22
|
4
|
+
data.tar.gz: c020c365f57a2dd428501b453bb349483bc98baf6db263dab7dfdfbab33b70a6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 13155476fe9dab95fa56e29ec8dc842c86d4cdba0bf296aec518801d4ff8ef3f963bdb7e20d9e17bbc58bfdf334d9c760ccd712b267f70882b8e7ec79fa9230a
|
7
|
+
data.tar.gz: 1c6806d55b297222c0133e27f53914143ba0b3b35891c8076e93befe9d375534551b492890092432015a793dd88d97d8489ce69635e6ceeb8495f00880c5bd82
|
data/CHANGELOG.md
CHANGED
@@ -1,4 +1,15 @@
|
|
1
|
-
##
|
1
|
+
## 0.7.0, August 25, 2023
|
2
|
+
|
3
|
+
Two improvements in compatibility with IBM Sterling, which could not understand
|
4
|
+
our existing message & MDN formats.
|
5
|
+
|
6
|
+
These changes are opt-in only, and require a config change to use. See linked PRs for
|
7
|
+
details.
|
8
|
+
|
9
|
+
* Improved formatting of MDN messages. [#25](https://github.com/alexdean/as2/pull/25)
|
10
|
+
* Improved formatting of outbound messages. [#28](https://github.com/alexdean/as2/pull/28)
|
11
|
+
|
12
|
+
## 0.6.0, April 4, 2023
|
2
13
|
|
3
14
|
* allow verification of signed MDNs which use `Content-Transfer-Encoding: binary`. [#22](https://github.com/alexdean/as2/pull/22)
|
4
15
|
* Improve example server to make it more useful for local testing & development. [#17](https://github.com/alexdean/as2/pull/17)
|
data/README.md
CHANGED
@@ -26,11 +26,9 @@ along.
|
|
26
26
|
4. Use of Synchronous or Asynchronous Receipts: We do not support asynchronous
|
27
27
|
delivery of MDNs.
|
28
28
|
5. Security Formatting: We should be reasonably compliant here.
|
29
|
-
6. Hash Function, Message Digest Choices: We currently always use sha256
|
30
|
-
|
31
|
-
|
32
|
-
mentions md5. Mendelson AS2 server supports a number of other algorithms.
|
33
|
-
(sha256, sha512, etc)
|
29
|
+
6. Hash Function, Message Digest Choices: We currently always use sha256 for
|
30
|
+
signing. Since [#20](https://github.com/alexdean/as2/pull/20) we have supported
|
31
|
+
allowing partners to request which algorithm we use for MIC generation in MDNs.
|
34
32
|
2. AS2 partners may agree to use separate certificates for data encryption and data signing.
|
35
33
|
We do not support separate certificates for these purposes.
|
36
34
|
|
data/lib/as2/client.rb
CHANGED
@@ -4,6 +4,10 @@ module As2
|
|
4
4
|
class Client
|
5
5
|
attr_reader :partner, :server_info
|
6
6
|
|
7
|
+
def self.valid_outbound_formats
|
8
|
+
['v0', 'v1']
|
9
|
+
end
|
10
|
+
|
7
11
|
# @param [As2::Config::Partner,String] partner The partner to send a message to.
|
8
12
|
# If a string is given, it should be a partner name which has been registered
|
9
13
|
# via a call to #add_partner.
|
@@ -64,18 +68,22 @@ module As2
|
|
64
68
|
req['Message-ID'] = outbound_message_id
|
65
69
|
|
66
70
|
document_content = content || File.read(file_name)
|
71
|
+
outbound_format = @partner&.outbound_format || 'v0'
|
67
72
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
+
if outbound_format == 'v1'
|
74
|
+
format_method = :format_body_v1
|
75
|
+
else
|
76
|
+
format_method = :format_body_v0
|
77
|
+
end
|
78
|
+
|
79
|
+
document_payload, request_body = send(format_method,
|
80
|
+
document_content,
|
81
|
+
content_type: content_type,
|
82
|
+
file_name: file_name
|
83
|
+
)
|
73
84
|
|
74
|
-
signature = OpenSSL::PKCS7.sign @server_info.certificate, @server_info.pkey, document_payload
|
75
|
-
signature.detached = true
|
76
|
-
container = OpenSSL::PKCS7.write_smime signature, document_payload
|
77
85
|
cipher = OpenSSL::Cipher::AES256.new(:CBC) # default, but we might have to make this configurable
|
78
|
-
encrypted = OpenSSL::PKCS7.encrypt
|
86
|
+
encrypted = OpenSSL::PKCS7.encrypt([@partner.certificate], request_body, cipher)
|
79
87
|
|
80
88
|
# > HTTP can handle binary data and so there is no need to use the
|
81
89
|
# > content transfer encodings of MIME
|
@@ -123,6 +131,109 @@ module As2
|
|
123
131
|
)
|
124
132
|
end
|
125
133
|
|
134
|
+
# 'original' body formatting
|
135
|
+
#
|
136
|
+
# 1. uses OpenSSL::PKCS7.write_smime to build MIME body
|
137
|
+
# * includes plain-text "this is an S/MIME message" note prior to initial
|
138
|
+
# MIME boundary
|
139
|
+
# 2. uses non-standard application/x-pkcs7-* content types
|
140
|
+
# 3. MIME boundaries and signature have \n line endings
|
141
|
+
#
|
142
|
+
# this format is understood by Mendelson, OpenAS2, and several commercial
|
143
|
+
# products (GoAnywhere MFT). it is not understood by IBM Sterling B2B Integrator.
|
144
|
+
#
|
145
|
+
# @param [String] document_content the content to be transmitted
|
146
|
+
# @param [String] content_type the MIME type for document_content
|
147
|
+
# @param [String] file_name The filename to be transmitted to the partner
|
148
|
+
# @return [Array]
|
149
|
+
# first item is the full document part of the transmission (including) MIME headers.
|
150
|
+
# second item is the complete HTTP body.
|
151
|
+
def format_body_v0(document_content, content_type:, file_name:)
|
152
|
+
document_payload = "Content-Type: #{content_type}\r\n"
|
153
|
+
document_payload << "Content-Transfer-Encoding: base64\r\n"
|
154
|
+
document_payload << "Content-Disposition: attachment; filename=#{file_name}\r\n"
|
155
|
+
document_payload << "\r\n"
|
156
|
+
document_payload << Base64.strict_encode64(document_content)
|
157
|
+
|
158
|
+
signature = OpenSSL::PKCS7.sign(@server_info.certificate, @server_info.pkey, document_payload)
|
159
|
+
signature.detached = true
|
160
|
+
|
161
|
+
[document_payload, OpenSSL::PKCS7.write_smime(signature, document_payload)]
|
162
|
+
end
|
163
|
+
|
164
|
+
# updated body formatting
|
165
|
+
#
|
166
|
+
# 1. no content before the first MIME boundary
|
167
|
+
# 2. uses standard application/pkcs7-* content types
|
168
|
+
# 3. MIME boundaries and signature have \r\n line endings
|
169
|
+
# 4. adds parameter smime-type=signed-data to the signature's Content-Type
|
170
|
+
#
|
171
|
+
# this format is understood by Mendelson, OpenAS2, and several commercial
|
172
|
+
# products (GoAnywhere MFT) and IBM Sterling B2B Integrator.
|
173
|
+
#
|
174
|
+
# @param [String] document_content the content to be transmitted
|
175
|
+
# @param [String] content_type the MIME type for document_content
|
176
|
+
# @param [String] file_name The filename to be transmitted to the partner
|
177
|
+
# @return [Array]
|
178
|
+
# first item is the full document part of the transmission (including) MIME headers.
|
179
|
+
# second item is the complete HTTP body.
|
180
|
+
def format_body_v1(document_content, content_type:, file_name:)
|
181
|
+
document_payload = "Content-Type: #{content_type}\r\n"
|
182
|
+
document_payload << "Content-Transfer-Encoding: base64\r\n"
|
183
|
+
document_payload << "Content-Disposition: attachment; filename=#{file_name}\r\n"
|
184
|
+
document_payload << "\r\n"
|
185
|
+
document_payload << Base64.strict_encode64(document_content)
|
186
|
+
|
187
|
+
signature = OpenSSL::PKCS7.sign(@server_info.certificate, @server_info.pkey, document_payload)
|
188
|
+
signature.detached = true
|
189
|
+
|
190
|
+
# PEM (base64-encoded) signature
|
191
|
+
bare_pem_signature = signature.to_pem
|
192
|
+
# strip off the '-----BEGIN PKCS7-----' / '-----END PKCS7-----' delimiters
|
193
|
+
bare_pem_signature.gsub!(/^-----[^\n]+\n/, '')
|
194
|
+
# and update to canonical \r\n line endings
|
195
|
+
bare_pem_signature.gsub!(/(?<!\r)\n/, "\r\n")
|
196
|
+
|
197
|
+
# this is a hack until i can determine a better way to get the micalg parameter
|
198
|
+
# from the pkcs7 signature generated above...
|
199
|
+
# https://stackoverflow.com/questions/75934159/how-does-openssl-smime-determine-micalg-parameter
|
200
|
+
#
|
201
|
+
# also tried approach outlined in https://stackoverflow.com/questions/53044007/how-to-use-sha1-digest-during-signing-with-opensslpkcs7-sign-when-creating-smi
|
202
|
+
# but the signature generated by that method lacks some essential data. verifying those
|
203
|
+
# signatures results in an openssl error "unable to find message digest"
|
204
|
+
smime_body = OpenSSL::PKCS7.write_smime(signature, document_payload)
|
205
|
+
micalg = smime_body[/^Content-Type: multipart\/signed.*micalg=\"([^"]+)/m, 1]
|
206
|
+
|
207
|
+
# generate a MIME part boundary
|
208
|
+
#
|
209
|
+
# > A good strategy is to choose a boundary that includes
|
210
|
+
# > a character sequence such as "=_" which can never appear in a
|
211
|
+
# > quoted-printable body.
|
212
|
+
#
|
213
|
+
# https://www.rfc-editor.org/rfc/rfc2045#page-21
|
214
|
+
boundary = "----=_#{SecureRandom.hex(16).upcase}"
|
215
|
+
body_boundary = "--#{boundary}"
|
216
|
+
|
217
|
+
# body's mime headers
|
218
|
+
body = "Content-Type: multipart/signed; protocol=\"application/pkcs7-signature\"; micalg=#{micalg}; boundary=\"#{boundary}\"\r\n"
|
219
|
+
body += "\r\n"
|
220
|
+
|
221
|
+
# first body part: the document
|
222
|
+
body += body_boundary + "\r\n"
|
223
|
+
body += document_payload + "\r\n"
|
224
|
+
|
225
|
+
# second body part: the signature
|
226
|
+
body += body_boundary + "\r\n"
|
227
|
+
body += "Content-Type: application/pkcs7-signature; name=smime.p7s; smime-type=signed-data\r\n"
|
228
|
+
body += "Content-Transfer-Encoding: base64\r\n"
|
229
|
+
body += "Content-Disposition: attachment; filename=\"smime.p7s\"\r\n"
|
230
|
+
body += "\r\n"
|
231
|
+
body += bare_pem_signature
|
232
|
+
body += body_boundary + "--\r\n"
|
233
|
+
|
234
|
+
[document_payload, body]
|
235
|
+
end
|
236
|
+
|
126
237
|
def evaluate_mdn(mdn_body:, mdn_content_type:, original_message_id:, original_body:)
|
127
238
|
report = {
|
128
239
|
signature_verification_error: :not_checked,
|
data/lib/as2/config.rb
CHANGED
@@ -12,7 +12,7 @@ module As2
|
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
|
-
class Partner < Struct.new :name, :url, :certificate
|
15
|
+
class Partner < Struct.new :name, :url, :certificate, :mdn_format, :outbound_format
|
16
16
|
def url=(url)
|
17
17
|
if url.kind_of? String
|
18
18
|
self['url'] = URI.parse url
|
@@ -21,6 +21,24 @@ module As2
|
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
|
+
def mdn_format=(format)
|
25
|
+
format_s = format.to_s
|
26
|
+
valid_formats = As2::Server.valid_mdn_formats
|
27
|
+
if !valid_formats.include?(format_s)
|
28
|
+
raise ArgumentError, "mdn_format '#{format_s}' must be one of #{valid_formats.inspect}"
|
29
|
+
end
|
30
|
+
self['mdn_format'] = format_s
|
31
|
+
end
|
32
|
+
|
33
|
+
def outbound_format=(format)
|
34
|
+
format_s = format.to_s
|
35
|
+
valid_formats = As2::Client.valid_outbound_formats
|
36
|
+
if !valid_formats.include?(format_s)
|
37
|
+
raise ArgumentError, "outbound_format '#{format_s}' must be one of #{valid_formats.inspect}"
|
38
|
+
end
|
39
|
+
self['outbound_format'] = format_s
|
40
|
+
end
|
41
|
+
|
24
42
|
def certificate=(certificate)
|
25
43
|
self['certificate'] = As2::Config.build_certificate(certificate)
|
26
44
|
end
|
data/lib/as2/server.rb
CHANGED
@@ -8,6 +8,11 @@ module As2
|
|
8
8
|
class Server
|
9
9
|
attr_accessor :logger
|
10
10
|
|
11
|
+
# each of these should be understandable by send_mdn
|
12
|
+
def self.valid_mdn_formats
|
13
|
+
['v0', 'v1']
|
14
|
+
end
|
15
|
+
|
11
16
|
# @param [As2::Config::ServerInfo] server_info Config used for naming of this
|
12
17
|
# server and key/certificate selection. If omitted, the main As2::Config.server_info is used.
|
13
18
|
# @param [As2::Config::Partner] partner Which partner to receive messages from.
|
@@ -81,27 +86,61 @@ module As2
|
|
81
86
|
report = MimeGenerator::Part.new
|
82
87
|
report['Content-Type'] = 'multipart/report; report-type=disposition-notification'
|
83
88
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
report.add_part
|
89
|
+
text_part = MimeGenerator::Part.new
|
90
|
+
text_part['Content-Type'] = 'text/plain'
|
91
|
+
text_part['Content-Transfer-Encoding'] = '7bit'
|
92
|
+
text_part.body = text_body
|
93
|
+
report.add_part text_part
|
89
94
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
report.add_part
|
95
|
+
notification_part = MimeGenerator::Part.new
|
96
|
+
notification_part['Content-Type'] = 'message/disposition-notification'
|
97
|
+
notification_part['Content-Transfer-Encoding'] = '7bit'
|
98
|
+
notification_part.body = options.map{|n, v| "#{n}: #{v}"}.join("\r\n")
|
99
|
+
report.add_part notification_part
|
95
100
|
|
96
101
|
msg_out = StringIO.new
|
97
|
-
|
98
102
|
report.write msg_out
|
103
|
+
mdn_text = msg_out.string
|
104
|
+
|
105
|
+
mdn_format = @partner&.mdn_format || 'v0'
|
106
|
+
if mdn_format == 'v1'
|
107
|
+
format_method = :format_mdn_v1
|
108
|
+
else
|
109
|
+
format_method = :format_mdn_v0
|
110
|
+
end
|
99
111
|
|
100
|
-
|
112
|
+
headers, body = send(
|
113
|
+
format_method,
|
114
|
+
mdn_text,
|
115
|
+
as2_to: env['HTTP_AS2_FROM']
|
116
|
+
)
|
117
|
+
|
118
|
+
[200, headers, ["\r\n" + body]]
|
119
|
+
end
|
120
|
+
|
121
|
+
# 'original' MDN formatting
|
122
|
+
#
|
123
|
+
# 1. uses OpenSSL::PKCS7.write_smime to build MIME body
|
124
|
+
# * includes MIME headers in HTTP body
|
125
|
+
# * includes plain-text "this is an S/MIME message" note prior to initial
|
126
|
+
# MIME boundary
|
127
|
+
# 2. uses non-standard application/x-pkcs7-* content types
|
128
|
+
# 3. MIME boundaries and signature have \n line endings
|
129
|
+
#
|
130
|
+
# this format is understood by Mendelson, OpenAS2, and several commercial
|
131
|
+
# products (GoAnywhere MFT). it is not understood by IBM Sterling B2B Integrator.
|
132
|
+
#
|
133
|
+
# @param [String] mdn_text MIME multipart/report body containing text/plain
|
134
|
+
# and message/disposition-notification parts
|
135
|
+
# @param [String] mic_algorithm
|
136
|
+
# @param [String] as2_to
|
137
|
+
def format_mdn_v0(mdn_text, as2_to:)
|
138
|
+
pkcs7 = OpenSSL::PKCS7.sign @server_info.certificate, @server_info.pkey, mdn_text
|
101
139
|
pkcs7.detached = true
|
102
|
-
smime_signed = OpenSSL::PKCS7.write_smime pkcs7, msg_out.string
|
103
140
|
|
104
|
-
|
141
|
+
body = OpenSSL::PKCS7.write_smime pkcs7, mdn_text
|
142
|
+
|
143
|
+
content_type = body[/^Content-Type: (.+?)$/m, 1]
|
105
144
|
# smime_signed.sub!(/\A.+?^(?=---)/m, '')
|
106
145
|
|
107
146
|
headers = {}
|
@@ -110,11 +149,67 @@ module As2
|
|
110
149
|
headers['MIME-Version'] = '1.0'
|
111
150
|
headers['Message-ID'] = As2.generate_message_id(@server_info)
|
112
151
|
headers['AS2-From'] = @server_info.name
|
113
|
-
headers['AS2-To'] =
|
152
|
+
headers['AS2-To'] = as2_to
|
153
|
+
headers['AS2-Version'] = '1.0'
|
154
|
+
headers['Connection'] = 'close'
|
155
|
+
|
156
|
+
[headers, body]
|
157
|
+
end
|
158
|
+
|
159
|
+
def format_mdn_v1(mdn_text, as2_to:)
|
160
|
+
pkcs7 = OpenSSL::PKCS7.sign(@server_info.certificate, @server_info.pkey, mdn_text)
|
161
|
+
pkcs7.detached = true
|
162
|
+
|
163
|
+
# PEM (base64-encoded) signature
|
164
|
+
bare_pem_signature = pkcs7.to_pem
|
165
|
+
# strip off the '-----BEGIN PKCS7-----' / '-----END PKCS7-----' delimiters
|
166
|
+
bare_pem_signature.gsub!(/^-----[^\n]+\n/, '')
|
167
|
+
# and update to canonical \r\n line endings
|
168
|
+
bare_pem_signature.gsub!(/(?<!\r)\n/, "\r\n")
|
169
|
+
|
170
|
+
# this is a hack until i can determine a better way to get the micalg parameter
|
171
|
+
# from the pkcs7 signature generated above...
|
172
|
+
# https://stackoverflow.com/questions/75934159/how-does-openssl-smime-determine-micalg-parameter
|
173
|
+
#
|
174
|
+
# also tried approach outlined in https://stackoverflow.com/questions/53044007/how-to-use-sha1-digest-during-signing-with-opensslpkcs7-sign-when-creating-smi
|
175
|
+
# but the signature generated by that method lacks some essential data. verifying those
|
176
|
+
# signatures results in an openssl error "unable to find message digest"
|
177
|
+
smime_body = OpenSSL::PKCS7.write_smime(pkcs7, mdn_text)
|
178
|
+
micalg = smime_body[/^Content-Type: multipart\/signed.*micalg=\"([^"]+)/m, 1]
|
179
|
+
|
180
|
+
# generate a MIME part boundary
|
181
|
+
#
|
182
|
+
# > A good strategy is to choose a boundary that includes
|
183
|
+
# > a character sequence such as "=_" which can never appear in a
|
184
|
+
# > quoted-printable body.
|
185
|
+
#
|
186
|
+
# https://www.rfc-editor.org/rfc/rfc2045#page-21
|
187
|
+
boundary = "----=_#{SecureRandom.hex(16).upcase}"
|
188
|
+
body_boundary = "--#{boundary}"
|
189
|
+
|
190
|
+
headers = {}
|
191
|
+
headers['Content-Type'] = "multipart/signed; protocol=\"application/pkcs7-signature\"; micalg=\"#{micalg}\"; boundary=\"#{boundary}\""
|
192
|
+
headers['MIME-Version'] = '1.0'
|
193
|
+
headers['Message-ID'] = As2.generate_message_id(@server_info)
|
194
|
+
headers['AS2-From'] = @server_info.name
|
195
|
+
headers['AS2-To'] = as2_to
|
114
196
|
headers['AS2-Version'] = '1.0'
|
115
197
|
headers['Connection'] = 'close'
|
116
198
|
|
117
|
-
|
199
|
+
# this is the MDN report, with text/plain and message/disposition-notification parts
|
200
|
+
body = body_boundary + "\r\n"
|
201
|
+
body += mdn_text + "\r\n"
|
202
|
+
|
203
|
+
# this is the signature generated over that report
|
204
|
+
body += body_boundary + "\r\n"
|
205
|
+
body += "Content-Type: application/pkcs7-signature; name=\"smime.p7s\"\r\n"
|
206
|
+
body += "Content-Transfer-Encoding: base64\r\n"
|
207
|
+
body += "Content-Disposition: attachment; filename=\"smime.p7s\"\r\n"
|
208
|
+
body += "\r\n"
|
209
|
+
body += bare_pem_signature
|
210
|
+
body += body_boundary + "--\r\n"
|
211
|
+
|
212
|
+
[headers, body]
|
118
213
|
end
|
119
214
|
|
120
215
|
private
|
data/lib/as2/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: as2
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- OfficeLuv
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date: 2023-
|
12
|
+
date: 2023-08-25 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: mail
|