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 +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +2 -2
- data/as2.gemspec +1 -1
- data/lib/as2/client.rb +107 -57
- data/lib/as2/digest_selector.rb +1 -0
- data/lib/as2/message.rb +9 -3
- data/lib/as2/server.rb +24 -19
- data/lib/as2/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 40b3957adcdaa34f7df9fe0ee83966f7076e80bc9dc9c02b8f27a4fb22395fda
|
4
|
+
data.tar.gz: 1d8002959f6b4afa70c94fd363c9d97c15b2f20a82ee96b1bc39ebba8a55972e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
29
|
-
partner asks for a different algorithm, we'll always use
|
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/
|
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.
|
40
|
-
req['AS2-From'] =
|
41
|
-
req['AS2-To'] =
|
42
|
-
req['Subject'] = 'AS2
|
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'] =
|
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'] = @
|
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:
|
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
|
-
|
63
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
data/lib/as2/digest_selector.rb
CHANGED
data/lib/as2/message.rb
CHANGED
@@ -80,14 +80,20 @@ module As2
|
|
80
80
|
end
|
81
81
|
|
82
82
|
def mic
|
83
|
-
|
84
|
-
|
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
|
-
|
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
|
-
|
63
|
-
|
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},
|
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.
|
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
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.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-
|
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/
|
171
|
+
homepage: https://github.com/andjosh/as2
|
172
172
|
licenses:
|
173
173
|
- MIT
|
174
174
|
metadata:
|