as2 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 44bae86d51af027c42242dcc2b746014fc1e5c408bdba4d8fda43373a3b61470
4
- data.tar.gz: e7043ac582ceb07fa9527f029ea128015e2f96331c3217758b022e5fe2b64e3a
3
+ metadata.gz: 78765b4a0fba8aaa7e3531ee9e3841a0774d72b6a1d09b18a69b53117cf26e22
4
+ data.tar.gz: c020c365f57a2dd428501b453bb349483bc98baf6db263dab7dfdfbab33b70a6
5
5
  SHA512:
6
- metadata.gz: 8f6beade2e29c96b6e0d65297296f26642a4882b3628b7d43aa2169b7b6b3ddea7abb5e791490b20a1ffb260cfbd214db5c9026a9cb61e34b4a9af05fd6f2d49
7
- data.tar.gz: 8bac3d37290848e67e01b689872fb6504c3407c23501040f1647f17bc42ae2101669ceb6b331908f170f2242a6acf3d4cbeeafe4ad3cf2c4f24995422e358e13
6
+ metadata.gz: 13155476fe9dab95fa56e29ec8dc842c86d4cdba0bf296aec518801d4ff8ef3f963bdb7e20d9e17bbc58bfdf334d9c760ccd712b267f70882b8e7ec79fa9230a
7
+ data.tar.gz: 1c6806d55b297222c0133e27f53914143ba0b3b35891c8076e93befe9d375534551b492890092432015a793dd88d97d8489ce69635e6ceeb8495f00880c5bd82
data/CHANGELOG.md CHANGED
@@ -1,4 +1,15 @@
1
- ## Unreleased (future 0.6.0)
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. If a
30
- partner asks for a different algorithm, we'll always use sha256 and partner
31
- will see a MIC verification failure. AS2 RFC specifically prefers sha1 and
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
- document_payload = "Content-Type: #{content_type}\r\n"
69
- document_payload << "Content-Transfer-Encoding: base64\r\n"
70
- document_payload << "Content-Disposition: attachment; filename=#{file_name}\r\n"
71
- document_payload << "\r\n"
72
- document_payload << Base64.strict_encode64(document_content)
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 [@partner.certificate], container, cipher
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
- text = MimeGenerator::Part.new
85
- text['Content-Type'] = 'text/plain'
86
- text['Content-Transfer-Encoding'] = '7bit'
87
- text.body = text_body
88
- report.add_part text
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
- notification = MimeGenerator::Part.new
91
- notification['Content-Type'] = 'message/disposition-notification'
92
- notification['Content-Transfer-Encoding'] = '7bit'
93
- notification.body = options.map{|n, v| "#{n}: #{v}"}.join("\r\n")
94
- report.add_part notification
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
- pkcs7 = OpenSSL::PKCS7.sign @server_info.certificate, @server_info.pkey, msg_out.string
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
- content_type = smime_signed[/^Content-Type: (.+?)$/m, 1]
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'] = env['HTTP_AS2_FROM']
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
- [200, headers, ["\r\n" + smime_signed]]
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
@@ -1,3 +1,3 @@
1
1
  module As2
2
- VERSION = "0.6.0"
2
+ VERSION = "0.7.0"
3
3
  end
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.6.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-04-04 00:00:00.000000000 Z
12
+ date: 2023-08-25 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: mail