as2 0.4.0 → 0.5.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 +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:
|