as2 0.6.0 → 0.8.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: 96ec1c65d6997fc278267320f6228096129a87752c52127fdf2c9a6d6b1a8b43
4
+ data.tar.gz: b9305a892d2833a59ee286567132cee1b060936ae1f87f80d0de36a6660a6d2b
5
5
  SHA512:
6
- metadata.gz: 8f6beade2e29c96b6e0d65297296f26642a4882b3628b7d43aa2169b7b6b3ddea7abb5e791490b20a1ffb260cfbd214db5c9026a9cb61e34b4a9af05fd6f2d49
7
- data.tar.gz: 8bac3d37290848e67e01b689872fb6504c3407c23501040f1647f17bc42ae2101669ceb6b331908f170f2242a6acf3d4cbeeafe4ad3cf2c4f24995422e358e13
6
+ metadata.gz: 0c0aab2fb5d4282559cd0938d0bc540f0061a65d1a5eba7aa8f067a1f85fa746e599afeac5a9a4567456a737f2242bb73748b8460e4e398d612410536a3cafb8
7
+ data.tar.gz: 480ecc3810c376168a5778612c67c2db4eac48c0ea2da2fb782ba247ff0bc6400765b9b98b2835a7da9cd62d7cfdc37043dbee5b1278d3618c7782cd150b2182
@@ -9,26 +9,26 @@ name: test suite
9
9
 
10
10
  on:
11
11
  push:
12
- branches: '**'
12
+ branches: "**"
13
13
 
14
14
  jobs:
15
15
  test:
16
16
  strategy:
17
17
  fail-fast: false
18
18
  matrix:
19
- ruby: ['2.5', '2.6', '2.7', '3.0', '3.1']
19
+ ruby: ["2.7", "3.0", "3.1", "3.2"]
20
20
 
21
21
  runs-on: ubuntu-latest
22
22
 
23
23
  steps:
24
- - uses: actions/checkout@v2
25
- - name: Set up Ruby
26
- # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
27
- # change this to (see https://github.com/ruby/setup-ruby#versioning):
28
- uses: ruby/setup-ruby@v1
29
- with:
30
- ruby-version: ${{ matrix.ruby }}
31
- - name: Install dependencies
32
- run: bundle install
33
- - name: Run tests
34
- run: bundle exec rake test
24
+ - uses: actions/checkout@v2
25
+ - name: Set up Ruby
26
+ # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
27
+ # change this to (see https://github.com/ruby/setup-ruby#versioning):
28
+ uses: ruby/setup-ruby@v1
29
+ with:
30
+ ruby-version: ${{ matrix.ruby }}
31
+ - name: Install dependencies
32
+ run: bundle install
33
+ - name: Run tests
34
+ run: bundle exec rake test
data/CHANGELOG.md CHANGED
@@ -1,4 +1,20 @@
1
- ## Unreleased (future 0.6.0)
1
+ ## 0.8.0, August 25, 2023
2
+
3
+ * Quote AS2-From/AS2-To identifiers which contain spaces. [#30](https://github.com/alexdean/as2/pull/30)
4
+ * Small improvements to aid integration testing with partners. [#31](https://github.com/alexdean/as2/pull/31)
5
+
6
+ ## 0.7.0, August 25, 2023
7
+
8
+ Two improvements in compatibility with IBM Sterling, which could not understand
9
+ our existing message & MDN formats.
10
+
11
+ These changes are opt-in only, and require a config change to use. See linked PRs for
12
+ details.
13
+
14
+ * Improved formatting of MDN messages. [#25](https://github.com/alexdean/as2/pull/25)
15
+ * Improved formatting of outbound messages. [#28](https://github.com/alexdean/as2/pull/28)
16
+
17
+ ## 0.6.0, April 4, 2023
2
18
 
3
19
  * allow verification of signed MDNs which use `Content-Transfer-Encoding: binary`. [#22](https://github.com/alexdean/as2/pull/22)
4
20
  * 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
 
@@ -1,9 +1,10 @@
1
1
  module As2
2
2
  class Client
3
3
  class Result
4
- attr_reader :response, :mic_matched, :mid_matched, :body, :disposition, :signature_verification_error, :exception, :outbound_message_id
4
+ attr_reader :request, :response, :mic_matched, :mid_matched, :body, :disposition, :signature_verification_error, :exception, :outbound_message_id
5
5
 
6
- def initialize(response:, mic_matched:, mid_matched:, body:, disposition:, signature_verification_error:, exception:, outbound_message_id:)
6
+ def initialize(request:, response:, mic_matched:, mid_matched:, body:, disposition:, signature_verification_error:, exception:, outbound_message_id:)
7
+ @request = request
7
8
  @response = response
8
9
  @mic_matched = mic_matched
9
10
  @mid_matched = mid_matched
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.
@@ -52,8 +56,8 @@ module As2
52
56
 
53
57
  req = Net::HTTP::Post.new @partner.url.path
54
58
  req['AS2-Version'] = '1.0' # 1.1 includes compression support, which we dont implement.
55
- req['AS2-From'] = as2_from
56
- req['AS2-To'] = as2_to
59
+ req['AS2-From'] = As2.quoted_system_identifier(as2_from)
60
+ req['AS2-To'] = As2.quoted_system_identifier(as2_to)
57
61
  req['Subject'] = 'AS2 Transaction'
58
62
  req['Content-Type'] = 'application/pkcs7-mime; smime-type=enveloped-data; name=smime.p7m'
59
63
  req['Date'] = Time.now.rfc2822
@@ -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
@@ -91,9 +99,16 @@ module As2
91
99
  # note: to pass this traffic through a debugging proxy (like Charles)
92
100
  # set ENV['http_proxy'].
93
101
  http = Net::HTTP.new(@partner.url.host, @partner.url.port)
94
- http.use_ssl = @partner.url.scheme == 'https'
102
+
103
+ use_ssl = @partner.url.scheme == 'https'
104
+ http.use_ssl = use_ssl
105
+ if use_ssl
106
+ if @partner.tls_verify_mode
107
+ http.verify_mode = @partner.tls_verify_mode
108
+ end
109
+ end
110
+
95
111
  # http.set_debug_output $stderr
96
- # http.verify_mode = OpenSSL::SSL::VERIFY_NONE
97
112
 
98
113
  http.start do
99
114
  resp = http.request(req)
@@ -112,6 +127,7 @@ module As2
112
127
  end
113
128
 
114
129
  Result.new(
130
+ request: req,
115
131
  response: resp,
116
132
  mic_matched: mdn_report[:mic_matched],
117
133
  mid_matched: mdn_report[:mid_matched],
@@ -123,6 +139,109 @@ module As2
123
139
  )
124
140
  end
125
141
 
142
+ # 'original' body formatting
143
+ #
144
+ # 1. uses OpenSSL::PKCS7.write_smime to build MIME body
145
+ # * includes plain-text "this is an S/MIME message" note prior to initial
146
+ # MIME boundary
147
+ # 2. uses non-standard application/x-pkcs7-* content types
148
+ # 3. MIME boundaries and signature have \n line endings
149
+ #
150
+ # this format is understood by Mendelson, OpenAS2, and several commercial
151
+ # products (GoAnywhere MFT). it is not understood by IBM Sterling B2B Integrator.
152
+ #
153
+ # @param [String] document_content the content to be transmitted
154
+ # @param [String] content_type the MIME type for document_content
155
+ # @param [String] file_name The filename to be transmitted to the partner
156
+ # @return [Array]
157
+ # first item is the full document part of the transmission (including) MIME headers.
158
+ # second item is the complete HTTP body.
159
+ def format_body_v0(document_content, content_type:, file_name:)
160
+ document_payload = "Content-Type: #{content_type}\r\n"
161
+ document_payload << "Content-Transfer-Encoding: base64\r\n"
162
+ document_payload << "Content-Disposition: attachment; filename=#{file_name}\r\n"
163
+ document_payload << "\r\n"
164
+ document_payload << Base64.strict_encode64(document_content)
165
+
166
+ signature = OpenSSL::PKCS7.sign(@server_info.certificate, @server_info.pkey, document_payload)
167
+ signature.detached = true
168
+
169
+ [document_payload, OpenSSL::PKCS7.write_smime(signature, document_payload)]
170
+ end
171
+
172
+ # updated body formatting
173
+ #
174
+ # 1. no content before the first MIME boundary
175
+ # 2. uses standard application/pkcs7-* content types
176
+ # 3. MIME boundaries and signature have \r\n line endings
177
+ # 4. adds parameter smime-type=signed-data to the signature's Content-Type
178
+ #
179
+ # this format is understood by Mendelson, OpenAS2, and several commercial
180
+ # products (GoAnywhere MFT) and IBM Sterling B2B Integrator.
181
+ #
182
+ # @param [String] document_content the content to be transmitted
183
+ # @param [String] content_type the MIME type for document_content
184
+ # @param [String] file_name The filename to be transmitted to the partner
185
+ # @return [Array]
186
+ # first item is the full document part of the transmission (including) MIME headers.
187
+ # second item is the complete HTTP body.
188
+ def format_body_v1(document_content, content_type:, file_name:)
189
+ document_payload = "Content-Type: #{content_type}\r\n"
190
+ document_payload << "Content-Transfer-Encoding: base64\r\n"
191
+ document_payload << "Content-Disposition: attachment; filename=#{file_name}\r\n"
192
+ document_payload << "\r\n"
193
+ document_payload << Base64.strict_encode64(document_content)
194
+
195
+ signature = OpenSSL::PKCS7.sign(@server_info.certificate, @server_info.pkey, document_payload)
196
+ signature.detached = true
197
+
198
+ # PEM (base64-encoded) signature
199
+ bare_pem_signature = signature.to_pem
200
+ # strip off the '-----BEGIN PKCS7-----' / '-----END PKCS7-----' delimiters
201
+ bare_pem_signature.gsub!(/^-----[^\n]+\n/, '')
202
+ # and update to canonical \r\n line endings
203
+ bare_pem_signature.gsub!(/(?<!\r)\n/, "\r\n")
204
+
205
+ # this is a hack until i can determine a better way to get the micalg parameter
206
+ # from the pkcs7 signature generated above...
207
+ # https://stackoverflow.com/questions/75934159/how-does-openssl-smime-determine-micalg-parameter
208
+ #
209
+ # also tried approach outlined in https://stackoverflow.com/questions/53044007/how-to-use-sha1-digest-during-signing-with-opensslpkcs7-sign-when-creating-smi
210
+ # but the signature generated by that method lacks some essential data. verifying those
211
+ # signatures results in an openssl error "unable to find message digest"
212
+ smime_body = OpenSSL::PKCS7.write_smime(signature, document_payload)
213
+ micalg = smime_body[/^Content-Type: multipart\/signed.*micalg=\"([^"]+)/m, 1]
214
+
215
+ # generate a MIME part boundary
216
+ #
217
+ # > A good strategy is to choose a boundary that includes
218
+ # > a character sequence such as "=_" which can never appear in a
219
+ # > quoted-printable body.
220
+ #
221
+ # https://www.rfc-editor.org/rfc/rfc2045#page-21
222
+ boundary = "----=_#{SecureRandom.hex(16).upcase}"
223
+ body_boundary = "--#{boundary}"
224
+
225
+ # body's mime headers
226
+ body = "Content-Type: multipart/signed; protocol=\"application/pkcs7-signature\"; micalg=#{micalg}; boundary=\"#{boundary}\"\r\n"
227
+ body += "\r\n"
228
+
229
+ # first body part: the document
230
+ body += body_boundary + "\r\n"
231
+ body += document_payload + "\r\n"
232
+
233
+ # second body part: the signature
234
+ body += body_boundary + "\r\n"
235
+ body += "Content-Type: application/pkcs7-signature; name=smime.p7s; smime-type=signed-data\r\n"
236
+ body += "Content-Transfer-Encoding: base64\r\n"
237
+ body += "Content-Disposition: attachment; filename=\"smime.p7s\"\r\n"
238
+ body += "\r\n"
239
+ body += bare_pem_signature
240
+ body += body_boundary + "--\r\n"
241
+
242
+ [document_payload, body]
243
+ end
244
+
126
245
  def evaluate_mdn(mdn_body:, mdn_content_type:, original_message_id:, original_body:)
127
246
  report = {
128
247
  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, :tls_verify_mode, :mdn_format, :outbound_format
16
16
  def url=(url)
17
17
  if url.kind_of? String
18
18
  self['url'] = URI.parse url
@@ -21,9 +21,38 @@ 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
45
+
46
+ # if set, will be used for SSL transmissions.
47
+ # @see `verify_mode` in https://ruby-doc.org/stdlib-2.7.1/libdoc/net/http/rdoc/Net/HTTP.html
48
+ def tls_verify_mode=(mode)
49
+ valid_modes = [nil, OpenSSL::SSL::VERIFY_NONE, OpenSSL::SSL::VERIFY_PEER]
50
+ if !valid_modes.include?(mode)
51
+ raise ArgumentError, "tls_verify_mode '#{mode}' must be one of #{valid_modes.inspect}"
52
+ end
53
+
54
+ self['tls_verify_mode'] = mode
55
+ end
27
56
  end
28
57
 
29
58
  class ServerInfo < Struct.new :name, :url, :certificate, :pkey, :domain
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.
@@ -64,8 +69,8 @@ module As2
64
69
 
65
70
  options = {
66
71
  'Reporting-UA' => @server_info.name,
67
- 'Original-Recipient' => "rfc822; #{@server_info.name}",
68
- 'Final-Recipient' => "rfc822; #{@server_info.name}",
72
+ 'Original-Recipient' => "rfc822; #{As2.quoted_system_identifier(@server_info.name)}",
73
+ 'Final-Recipient' => "rfc822; #{As2.quoted_system_identifier(@server_info.name)}",
69
74
  'Original-Message-ID' => env['HTTP_MESSAGE_ID']
70
75
  }
71
76
  if failed
@@ -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 = {}
@@ -109,12 +148,68 @@ module As2
109
148
  # TODO: if MIME-Version header is actually needed, should extract it out of smime_signed.
110
149
  headers['MIME-Version'] = '1.0'
111
150
  headers['Message-ID'] = As2.generate_message_id(@server_info)
112
- headers['AS2-From'] = @server_info.name
113
- headers['AS2-To'] = env['HTTP_AS2_FROM']
151
+ headers['AS2-From'] = As2.quoted_system_identifier(@server_info.name)
152
+ headers['AS2-To'] = As2.quoted_system_identifier(as2_to)
114
153
  headers['AS2-Version'] = '1.0'
115
154
  headers['Connection'] = 'close'
116
155
 
117
- [200, headers, ["\r\n" + smime_signed]]
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'] = As2.quoted_system_identifier(@server_info.name)
195
+ headers['AS2-To'] = As2.quoted_system_identifier(as2_to)
196
+ headers['AS2-Version'] = '1.0'
197
+ headers['Connection'] = 'close'
198
+
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.8.0"
3
3
  end
data/lib/as2.rb CHANGED
@@ -35,4 +35,13 @@ module As2
35
35
  parsed = As2::Parser::DispositionNotificationOptions.parse(disposition_notification_options)
36
36
  Array(parsed['signed-receipt-micalg']).find { |m| As2::DigestSelector.valid?(m) }
37
37
  end
38
+
39
+ # surround an As2-From/As2-To value with double-quotes, if it contains a space.
40
+ def self.quoted_system_identifier(name)
41
+ if name.to_s.include?(' ')
42
+ "\"#{name}\""
43
+ else
44
+ name
45
+ end
46
+ end
38
47
  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.8.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