as2 0.4.0 → 0.5.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: '094edd2c08516e42061e5623b49e4b2f355a3ca3a19a6ed17bc2764ea1073f9c'
4
- data.tar.gz: c7678c44a070eda090eec89d3e3bacfdc2a1400b880bb39d52f2ba3e08ffe300
3
+ metadata.gz: 40b3957adcdaa34f7df9fe0ee83966f7076e80bc9dc9c02b8f27a4fb22395fda
4
+ data.tar.gz: 1d8002959f6b4afa70c94fd363c9d97c15b2f20a82ee96b1bc39ebba8a55972e
5
5
  SHA512:
6
- metadata.gz: c4ecd6838eb1e62174c584638141ba0f2af696bfd906765a0f3ffd4928e03ed3d203e9160c2ce5731439e282283fbf2b511b891719c98eeff262834b9bb1ec11
7
- data.tar.gz: 852b03c4eebc90ea86447a4074afa417556b89f8613851eb59364dc6f8b4f9f1904452da3c4160929992c2263085774ff6c8cdf46345a92e75411daf945b8123
6
+ metadata.gz: bb04b23ab84f14e131e74e3a213e71ae5f3009f805af08b7cc1c88dc3798745639e4cffea1a85811df51a522776797f236da98c82ab84863dd5891c0b37fc243
7
+ data.tar.gz: a71c9ea6c4c82637103ae1353e5b54cee88ad4f52b388883b0b409c24722247bbd0ead903ca5ef00ba1df41403ab11f1d6b4b03f9d03f58af39ccb399848adf0
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## 0.5.0, March 21, 2022
2
+
3
+ * improvements to `As2::Client`. improve compatibility with non-Mendelson AS2 servers. [#8](https://github.com/andjosh/as2/pull/8)
4
+ * improve MDN generation, especially when an error occurs. [#9](https://github.com/andjosh/as2/pull/9)
5
+ * successfully parse unsigned MDNs. [#10](https://github.com/andjosh/as2/pull/10)
6
+
1
7
  ## 0.4.0, March 3, 2022
2
8
 
3
9
  * client: correct MIC & signature verification when processing MDN response [#7](https://github.com/andjosh/as2/pull/7)
data/README.md CHANGED
@@ -25,8 +25,8 @@ along.
25
25
  4. Use of Synchronous or Asynchronous Receipts: We do not support asynchronous
26
26
  delivery of MDNs.
27
27
  5. Security Formatting: We should be reasonably compliant here.
28
- 6. Hash Function, Message Digest Choices: We currently always use sha1. If a
29
- partner asks for a different algorithm, we'll always use sha1 and partner
28
+ 6. Hash Function, Message Digest Choices: We currently always use sha256. If a
29
+ partner asks for a different algorithm, we'll always use sha256 and partner
30
30
  will see a MIC verification failure. AS2 RFC specifically prefers sha1 and
31
31
  mentions md5. Mendelson AS2 server supports a number of other algorithms.
32
32
  (sha256, sha512, etc)
data/as2.gemspec CHANGED
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
11
11
 
12
12
  spec.summary = %q{Simple AS2 server and client implementation}
13
13
  spec.description = %q{Simple AS2 server and client implementation. Follows the AS2 implementation from http://as2.mendelson-e-c.com}
14
- spec.homepage = "https://github.com/officeluv/as2"
14
+ spec.homepage = "https://github.com/andjosh/as2"
15
15
  spec.license = "MIT"
16
16
 
17
17
  # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
data/lib/as2/client.rb CHANGED
@@ -22,6 +22,14 @@ module As2
22
22
  @server_info = server_info || Config.server_info
23
23
  end
24
24
 
25
+ def as2_to
26
+ @partner.name
27
+ end
28
+
29
+ def as2_from
30
+ @server_info.name
31
+ end
32
+
25
33
  # Send a file to a partner
26
34
  #
27
35
  # * If the content parameter is omitted, then `file_name` must be a path
@@ -31,26 +39,29 @@ module As2
31
39
  #
32
40
  # @param [String] file_name
33
41
  # @param [String] content
42
+ # @param [String] content_type This is the MIME Content-Type describing the `content` param,
43
+ # and will be included in the SMIME payload. It is not the HTTP Content-Type.
34
44
  # @return [As2::Client::Result]
35
- def send_file(file_name, content: nil)
45
+ def send_file(file_name, content: nil, content_type: 'application/EDI-Consent')
46
+ outbound_mic_algorithm = 'sha256'
36
47
  outbound_message_id = As2.generate_message_id(@server_info)
37
48
 
38
49
  req = Net::HTTP::Post.new @partner.url.path
39
- req['AS2-Version'] = '1.2'
40
- req['AS2-From'] = @server_info.name
41
- req['AS2-To'] = @partner.name
42
- req['Subject'] = 'AS2 EDI Transaction'
50
+ req['AS2-Version'] = '1.0' # 1.1 includes compression support, which we dont implement.
51
+ req['AS2-From'] = as2_from
52
+ req['AS2-To'] = as2_to
53
+ req['Subject'] = 'AS2 Transaction'
43
54
  req['Content-Type'] = 'application/pkcs7-mime; smime-type=enveloped-data; name=smime.p7m'
55
+ req['Date'] = Time.now.rfc2822
44
56
  req['Disposition-Notification-To'] = @server_info.url.to_s
45
- req['Disposition-Notification-Options'] = 'signed-receipt-protocol=optional, pkcs7-signature; signed-receipt-micalg=optional, sha1'
57
+ req['Disposition-Notification-Options'] = "signed-receipt-protocol=optional, pkcs7-signature; signed-receipt-micalg=optional, #{outbound_mic_algorithm}"
46
58
  req['Content-Disposition'] = 'attachment; filename="smime.p7m"'
47
- req['Recipient-Address'] = @server_info.url.to_s
48
- req['Content-Transfer-Encoding'] = 'base64'
59
+ req['Recipient-Address'] = @partner.url.to_s
49
60
  req['Message-ID'] = outbound_message_id
50
61
 
51
62
  document_content = content || File.read(file_name)
52
63
 
53
- document_payload = "Content-Type: application/EDI-Consent\r\n"
64
+ document_payload = "Content-Type: #{content_type}\r\n"
54
65
  document_payload << "Content-Transfer-Encoding: base64\r\n"
55
66
  document_payload << "Content-Disposition: attachment; filename=#{file_name}\r\n"
56
67
  document_payload << "\r\n"
@@ -59,63 +70,38 @@ module As2
59
70
  signature = OpenSSL::PKCS7.sign @server_info.certificate, @server_info.pkey, document_payload
60
71
  signature.detached = true
61
72
  container = OpenSSL::PKCS7.write_smime signature, document_payload
62
- encrypted = OpenSSL::PKCS7.encrypt [@partner.certificate], container
63
- smime_encrypted = OpenSSL::PKCS7.write_smime encrypted
73
+ cipher = OpenSSL::Cipher::AES256.new(:CBC) # default, but we might have to make this configurable
74
+ encrypted = OpenSSL::PKCS7.encrypt [@partner.certificate], container, cipher
64
75
 
65
- req.body = smime_encrypted.sub(/^.+?\n\n/m, '')
76
+ # > HTTP can handle binary data and so there is no need to use the
77
+ # > content transfer encodings of MIME
78
+ #
79
+ # https://datatracker.ietf.org/doc/html/rfc4130#section-5.2.1
80
+ req.body = encrypted.to_der
66
81
 
67
82
  resp = nil
68
- signature_verification_error = :not_checked
69
83
  exception = nil
70
- mic_matched = nil
71
- mid_matched = nil
72
- disposition = nil
73
- plain_text_body = nil
84
+ mdn_report = {}
74
85
 
75
86
  begin
87
+ # note: to pass this traffic through a debugging proxy (like Charles)
88
+ # set ENV['http_proxy'].
76
89
  http = Net::HTTP.new(@partner.url.host, @partner.url.port)
77
90
  http.use_ssl = @partner.url.scheme == 'https'
78
91
  # http.set_debug_output $stderr
92
+ # http.verify_mode = OpenSSL::SSL::VERIFY_NONE
93
+
79
94
  http.start do
80
95
  resp = http.request(req)
81
96
  end
82
97
 
83
98
  if resp.code == '200'
84
- response_content = "Content-Type: #{resp['Content-Type']}\r\n#{resp.body}"
85
- smime = OpenSSL::PKCS7.read_smime response_content
86
- # based on As2::Message version
87
- # TODO: test cases based on valid/invalid responses. (response signed with wrong certificate, etc.)
88
- smime.verify [@partner.certificate], OpenSSL::X509::Store.new, nil, OpenSSL::PKCS7::NOVERIFY | OpenSSL::PKCS7::NOINTERN
89
- signature_verification_error = smime.error_string
90
-
91
- mail = Mail.new smime.data
92
- mail.parts.each do |part|
93
- case part.content_type
94
- when 'text/plain'
95
- plain_text_body = part.body
96
- when 'message/disposition-notification'
97
- # "The rules for constructing the AS2-disposition-notification content..."
98
- # https://datatracker.ietf.org/doc/html/rfc4130#section-7.4.3
99
-
100
- options = {}
101
- # TODO: can we use Mail built-ins for this?
102
- part.body.to_s.lines.each do |line|
103
- if line =~ /^([^:]+): (.+)$/
104
- options[$1] = $2
105
- end
106
- end
107
-
108
- disposition = options['Disposition']
109
- mid_matched = req['Message-ID'] == options['Original-Message-ID']
110
-
111
- # do mic calc using the algorithm specified by server.
112
- # (even if we specify sha1, server may send back MIC using a different algo.)
113
- received_mic, micalg = options['Received-Content-MIC'].split(',').map(&:strip)
114
- micalg ||= 'sha1'
115
- mic = As2::DigestSelector.for_code(micalg).base64digest(document_payload)
116
- mic_matched = received_mic == mic
117
- end
118
- end
99
+ mdn_report = evaluate_mdn(
100
+ mdn_content_type: resp['Content-Type'],
101
+ mdn_body: resp.body,
102
+ original_message_id: req['Message-ID'],
103
+ original_body: document_payload
104
+ )
119
105
  end
120
106
  rescue => e
121
107
  exception = e
@@ -123,14 +109,78 @@ module As2
123
109
 
124
110
  Result.new(
125
111
  response: resp,
126
- mic_matched: mic_matched,
127
- mid_matched: mid_matched,
128
- body: plain_text_body,
129
- disposition: disposition,
130
- signature_verification_error: signature_verification_error,
112
+ mic_matched: mdn_report[:mic_matched],
113
+ mid_matched: mdn_report[:mid_matched],
114
+ body: mdn_report[:plain_text_body],
115
+ disposition: mdn_report[:disposition],
116
+ signature_verification_error: mdn_report[:signature_verification_error],
131
117
  exception: exception,
132
118
  outbound_message_id: outbound_message_id
133
119
  )
134
120
  end
121
+
122
+ def evaluate_mdn(mdn_body:, mdn_content_type:, original_message_id:, original_body:)
123
+ report = {
124
+ signature_verification_error: :not_checked,
125
+ mic_matched: nil,
126
+ mid_matched: nil,
127
+ disposition: nil,
128
+ plain_text_body: nil
129
+ }
130
+
131
+ # MDN bodies we've seen so far don't include Content-Type, which causes `read_smime` to fail.
132
+ response_content = "Content-Type: #{mdn_content_type.to_s.strip}\r\n\r\n#{mdn_body}"
133
+
134
+ if mdn_content_type.start_with?('multipart/signed')
135
+ smime = OpenSSL::PKCS7.read_smime(response_content)
136
+
137
+ # create mail instance before #verify call.
138
+ # `smime.data` is emptied if verification fails, which means we wouldn't know disposition & other details.
139
+ mail = Mail.new(smime.data)
140
+
141
+ # based on As2::Message version
142
+ # TODO: test cases based on valid/invalid responses. (response signed with wrong certificate, etc.)
143
+ smime.verify [@partner.certificate], OpenSSL::X509::Store.new, nil, OpenSSL::PKCS7::NOVERIFY | OpenSSL::PKCS7::NOINTERN
144
+ report[:signature_verification_error] = smime.error_string
145
+ else
146
+ # MDN may be unsigned if an error occurred, like if we sent an unrecognized As2-From header.
147
+ mail = Mail.new(response_content)
148
+ end
149
+
150
+ mail.parts.each do |part|
151
+ if part.content_type.start_with?('text/plain')
152
+ report[:plain_text_body] = part.body.to_s.strip
153
+ elsif part.content_type.start_with?('message/disposition-notification')
154
+ # "The rules for constructing the AS2-disposition-notification content..."
155
+ # https://datatracker.ietf.org/doc/html/rfc4130#section-7.4.3
156
+
157
+ options = {}
158
+ # TODO: can we use Mail built-ins for this?
159
+ part.body.to_s.lines.each do |line|
160
+ if line =~ /^([^:]+): (.+)$/
161
+ # downcase because we've seen both 'Disposition' and 'disposition'
162
+ options[$1.to_s.downcase] = $2
163
+ end
164
+ end
165
+
166
+ report[:disposition] = options['disposition']
167
+ report[:mid_matched] = original_message_id == options['original-message-id']
168
+
169
+ if options['received-content-mic']
170
+ # do mic calc using the algorithm specified by server.
171
+ # (even if we specify sha1, server may send back MIC using a different algo.)
172
+ received_mic, micalg = options['received-content-mic'].split(',').map(&:strip)
173
+
174
+ # if they don't specify, we'll use the algorithm we specified in the outbound transmission.
175
+ # but it's only a guess & may fail.
176
+ micalg ||= outbound_mic_algorithm
177
+
178
+ mic = As2::DigestSelector.for_code(micalg).base64digest(original_body)
179
+ report[:mic_matched] = received_mic == mic
180
+ end
181
+ end
182
+ end
183
+ report
184
+ end
135
185
  end
136
186
  end
@@ -15,6 +15,7 @@ module As2
15
15
  end
16
16
 
17
17
  def self.for_code(code)
18
+ # we may receive 'sha256', 'sha-256', or 'SHA256'.
18
19
  normalized = code.strip.downcase.gsub(/[^a-z0-9]/, '')
19
20
 
20
21
  @map[normalized] || OpenSSL::Digest::SHA1
data/lib/as2/message.rb CHANGED
@@ -80,14 +80,20 @@ module As2
80
80
  end
81
81
 
82
82
  def mic
83
- # TODO: could use As2::DigestSelector if a different algo is needed.
84
- OpenSSL::Digest::SHA1.base64digest(attachment.raw_source.strip)
83
+ digest = As2::DigestSelector.for_code(mic_algorithm)
84
+ digest.base64digest(attachment.raw_source.strip)
85
+ end
86
+
87
+ def mic_algorithm
88
+ 'sha256'
85
89
  end
86
90
 
87
91
  # Return the attached file, use .filename and .body on the return value
88
92
  def attachment
89
93
  if mail.has_attachments?
90
- mail.parts.find{|a| a.content_type == "application/edi-consent"}
94
+ # TODO: match 'application/edi*', test with 'application/edi-x12'
95
+ # test also with "application/edi-consent; name=this_is_a_filename.txt"
96
+ mail.parts.find{ |a| a.content_type.match(/^application\/edi/) }
91
97
  else
92
98
  mail
93
99
  end
data/lib/as2/server.rb CHANGED
@@ -55,23 +55,12 @@ module As2
55
55
  end
56
56
  end
57
57
 
58
- send_mdn(env, message.mic)
58
+ send_mdn(env, message.mic, message.mic_algorithm)
59
59
  end
60
60
 
61
- def send_mdn(env, mic, failed = nil)
62
- report = MimeGenerator::Part.new
63
- report['Content-Type'] = 'multipart/report; report-type=disposition-notification'
64
-
65
- text = MimeGenerator::Part.new
66
- text['Content-Type'] = 'text/plain'
67
- text['Content-Transfer-Encoding'] = '7bit'
68
- text.body = "The AS2 message has been received successfully"
69
-
70
- report.add_part text
71
-
72
- notification = MimeGenerator::Part.new
73
- notification['Content-Type'] = 'message/disposition-notification'
74
- notification['Content-Transfer-Encoding'] = '7bit'
61
+ def send_mdn(env, mic, mic_algorithm, failed = nil)
62
+ # rules for MDN construction are covered in
63
+ # https://datatracker.ietf.org/doc/html/rfc4130#section-7.4.2
75
64
 
76
65
  options = {
77
66
  'Reporting-UA' => @server_info.name,
@@ -82,10 +71,25 @@ module As2
82
71
  if failed
83
72
  options['Disposition'] = 'automatic-action/MDN-sent-automatically; failed'
84
73
  options['Failure'] = failed
74
+ text_body = "There was an error with the AS2 transmission.\r\n\r\n#{failed}"
85
75
  else
86
76
  options['Disposition'] = 'automatic-action/MDN-sent-automatically; processed'
77
+ text_body = "The AS2 message has been received successfully"
87
78
  end
88
- options['Received-Content-MIC'] = "#{mic}, sha1" if mic
79
+ options['Received-Content-MIC'] = "#{mic}, #{mic_algorithm}" if mic
80
+
81
+ report = MimeGenerator::Part.new
82
+ report['Content-Type'] = 'multipart/report; report-type=disposition-notification'
83
+
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
+
90
+ notification = MimeGenerator::Part.new
91
+ notification['Content-Type'] = 'message/disposition-notification'
92
+ notification['Content-Transfer-Encoding'] = '7bit'
89
93
  notification.body = options.map{|n, v| "#{n}: #{v}"}.join("\r\n")
90
94
  report.add_part notification
91
95
 
@@ -98,15 +102,16 @@ module As2
98
102
  smime_signed = OpenSSL::PKCS7.write_smime pkcs7, msg_out.string
99
103
 
100
104
  content_type = smime_signed[/^Content-Type: (.+?)$/m, 1]
101
- smime_signed.sub!(/\A.+?^(?=---)/m, '')
105
+ # smime_signed.sub!(/\A.+?^(?=---)/m, '')
102
106
 
103
107
  headers = {}
104
108
  headers['Content-Type'] = content_type
109
+ # TODO: if MIME-Version header is actually needed, should extract it out of smime_signed.
105
110
  headers['MIME-Version'] = '1.0'
106
111
  headers['Message-ID'] = As2.generate_message_id(@server_info)
107
112
  headers['AS2-From'] = @server_info.name
108
113
  headers['AS2-To'] = env['HTTP_AS2_FROM']
109
- headers['AS2-Version'] = '1.2'
114
+ headers['AS2-Version'] = '1.0'
110
115
  headers['Connection'] = 'close'
111
116
 
112
117
  [200, headers, ["\r\n" + smime_signed]]
@@ -120,7 +125,7 @@ module As2
120
125
 
121
126
  def send_error(env, msg)
122
127
  logger(env).error msg
123
- send_mdn env, nil, msg
128
+ send_mdn env, nil, 'sha1', msg
124
129
  end
125
130
  end
126
131
  end
data/lib/as2/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module As2
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.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.4.0
4
+ version: 0.5.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: 2022-03-03 00:00:00.000000000 Z
12
+ date: 2022-03-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: mail
@@ -168,7 +168,7 @@ files:
168
168
  - lib/as2/mime_generator.rb
169
169
  - lib/as2/server.rb
170
170
  - lib/as2/version.rb
171
- homepage: https://github.com/officeluv/as2
171
+ homepage: https://github.com/andjosh/as2
172
172
  licenses:
173
173
  - MIT
174
174
  metadata: