as2 0.2.5 → 0.3.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/.gitignore +1 -0
- data/CHANGELOG.md +9 -0
- data/as2.gemspec +4 -4
- data/lib/as2/config.rb +25 -10
- data/lib/as2/message.rb +68 -19
- data/lib/as2/server.rb +9 -30
- data/lib/as2/version.rb +1 -1
- metadata +13 -12
- data/Gemfile.lock +0 -35
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3c28a94ec5915cd8930ba91814dea748017f86d0cbe2be94719fa54df5e6774f
|
4
|
+
data.tar.gz: 47239349434164df4c1833561b6942e458f9b919415d221b9ae376b4a28755fe
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b7f77c9176b2f279d5a678f72494f746b687432487df3b2c1d338754b89e5942e9af4798360a39528514cb826cc923148ac4a812cc64d65b7f9f4a104eb71b2f
|
7
|
+
data.tar.gz: efdc7d01786e56ad1aabbbd9c373aa9d835bb299c775423df0470e622916efe080a0abc3b7bc6173e39b80223824d3c3bb885b6fe8d6b93405329dcaf9f82fd5
|
data/.gitignore
CHANGED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
## 0.3.0, Dec 22, 2021
|
2
|
+
|
3
|
+
* fix MIC calculation. [#1](https://github.com/andjosh/as2/pull/1)
|
4
|
+
* allow loading of private key and certificates without local files. [#2](https://github.com/andjosh/as2/pull/2)
|
5
|
+
* fix signature verification. [#3](https://github.com/andjosh/as2/pull/3)
|
6
|
+
|
7
|
+
## prior to 0.3.0
|
8
|
+
|
9
|
+
Initial work by [@andjosh](https://github.com/andjosh) and [@datanoise](https://github.com/datanoise).
|
data/as2.gemspec
CHANGED
@@ -6,8 +6,8 @@ require 'as2/version'
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
7
|
spec.name = "as2"
|
8
8
|
spec.version = As2::VERSION
|
9
|
-
spec.authors = ["OfficeLuv"]
|
10
|
-
spec.email = ["development@officeluv.com"]
|
9
|
+
spec.authors = ["OfficeLuv", "Transfix"]
|
10
|
+
spec.email = ["development@officeluv.com", "alexdean@transfix.io"]
|
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}
|
@@ -30,8 +30,8 @@ Gem::Specification.new do |spec|
|
|
30
30
|
spec.add_dependency "mail"
|
31
31
|
spec.add_dependency "rack"
|
32
32
|
|
33
|
-
spec.add_development_dependency "bundler", "
|
34
|
-
spec.add_development_dependency "rake", "
|
33
|
+
spec.add_development_dependency "bundler", ">= 1.10"
|
34
|
+
spec.add_development_dependency "rake", ">= 10.0"
|
35
35
|
spec.add_development_dependency "thin"
|
36
36
|
spec.add_development_dependency "minitest"
|
37
37
|
end
|
data/lib/as2/config.rb
CHANGED
@@ -1,6 +1,17 @@
|
|
1
1
|
require 'uri'
|
2
2
|
module As2
|
3
3
|
module Config
|
4
|
+
def self.build_certificate(input)
|
5
|
+
if input.kind_of? OpenSSL::X509::Certificate
|
6
|
+
input
|
7
|
+
elsif input.kind_of? String
|
8
|
+
OpenSSL::X509::Certificate.new File.read(input)
|
9
|
+
else
|
10
|
+
raise ArgumentError, "Invalid certificate. Provide a string (file path)" \
|
11
|
+
" or an OpenSSL::X509::Certificate instance. Got a #{input.class} instead."
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
4
15
|
class Partner < Struct.new :name, :url, :certificate
|
5
16
|
def url=(url)
|
6
17
|
if url.kind_of? String
|
@@ -11,7 +22,7 @@ module As2
|
|
11
22
|
end
|
12
23
|
|
13
24
|
def certificate=(certificate)
|
14
|
-
self['certificate'] =
|
25
|
+
self['certificate'] = As2::Config.build_certificate(certificate)
|
15
26
|
end
|
16
27
|
end
|
17
28
|
|
@@ -25,11 +36,20 @@ module As2
|
|
25
36
|
end
|
26
37
|
|
27
38
|
def certificate=(certificate)
|
28
|
-
self['certificate'] =
|
39
|
+
self['certificate'] = As2::Config.build_certificate(certificate)
|
29
40
|
end
|
30
41
|
|
31
|
-
def pkey=(
|
32
|
-
|
42
|
+
def pkey=(input)
|
43
|
+
# looks like even though you OpenSSL::PKey.new, you still end up with
|
44
|
+
# an instance which is an OpenSSL::PKey::PKey.
|
45
|
+
if input.kind_of? OpenSSL::PKey::PKey
|
46
|
+
self['pkey'] = input
|
47
|
+
elsif input.kind_of? String
|
48
|
+
self['pkey'] = OpenSSL::PKey.read File.read(input)
|
49
|
+
else
|
50
|
+
raise ArgumentError, "Invalid private key. Provide a string (file path)" \
|
51
|
+
" or an OpenSSL::PKey instance. Got a #{input.class} instead."
|
52
|
+
end
|
33
53
|
end
|
34
54
|
|
35
55
|
def add_partner
|
@@ -67,12 +87,7 @@ module As2
|
|
67
87
|
unless @server_info.domain
|
68
88
|
raise 'Your domain name is required'
|
69
89
|
end
|
70
|
-
|
71
|
-
store.add_cert @server_info.certificate
|
72
|
-
rescue OpenSSL::X509::StoreError => err
|
73
|
-
# ignore duplicate certs
|
74
|
-
raise unless err.message == 'cert already in hash table'
|
75
|
-
end
|
90
|
+
store.add_cert @server_info.certificate
|
76
91
|
end
|
77
92
|
|
78
93
|
def partners
|
data/lib/as2/message.rb
CHANGED
@@ -1,26 +1,83 @@
|
|
1
|
-
require 'as2/base64_helper'
|
2
|
-
|
3
1
|
module As2
|
4
2
|
class Message
|
5
|
-
attr_reader :
|
3
|
+
attr_reader :verification_error
|
6
4
|
|
7
5
|
def initialize(message, private_key, public_certificate)
|
8
|
-
|
6
|
+
# TODO: might need to use OpenSSL::PKCS7.read_smime rather than .new sometimes
|
7
|
+
@pkcs7 = OpenSSL::PKCS7.new(message)
|
9
8
|
@private_key = private_key
|
10
9
|
@public_certificate = public_certificate
|
10
|
+
@verification_error = nil
|
11
11
|
end
|
12
12
|
|
13
13
|
def decrypted_message
|
14
|
-
@decrypted_message ||=
|
14
|
+
@decrypted_message ||= @pkcs7.decrypt @private_key, @public_certificate
|
15
15
|
end
|
16
16
|
|
17
17
|
def valid_signature?(partner_certificate)
|
18
|
-
|
19
|
-
|
18
|
+
content_type = mail.header_fields.find { |h| h.name == 'Content-Type' }.content_type
|
19
|
+
if content_type == "multipart/signed"
|
20
|
+
# for a "multipart/signed" message, we will do 'detatched' signature
|
21
|
+
# verification, where we supply the data to be verified as the 3rd parameter
|
22
|
+
# to OpenSSL::PKCS7#verify. this is in keeping with how this content type
|
23
|
+
# is described in the S/MIME RFC.
|
24
|
+
#
|
25
|
+
# > The multipart/signed MIME type has two parts. The first part contains
|
26
|
+
# > the MIME entity that is signed; the second part contains the "detached signature"
|
27
|
+
# > CMS SignedData object in which the encapContentInfo eContent field is absent.
|
28
|
+
#
|
29
|
+
# https://datatracker.ietf.org/doc/html/rfc3851#section-3.4.3.1
|
30
|
+
|
31
|
+
# TODO: more robust detection of content vs signature (if they're ever out of order).
|
32
|
+
content = mail.parts[0].raw_source.strip
|
33
|
+
signature = OpenSSL::PKCS7.new(mail.parts[1].body.to_s)
|
34
|
+
|
35
|
+
# using an empty CA store. see notes on NOVERIFY flag below.
|
36
|
+
store = OpenSSL::X509::Store.new
|
37
|
+
|
38
|
+
# notes on verification proces and flags used
|
39
|
+
#
|
40
|
+
# ## NOINTERN
|
41
|
+
#
|
42
|
+
# > If PKCS7_NOINTERN is set the certificates in the message itself are
|
43
|
+
# > not searched when locating the signer's certificate. This means that
|
44
|
+
# > all the signers certificates must be in the certs parameter.
|
45
|
+
#
|
46
|
+
# > One application of PKCS7_NOINTERN is to only accept messages signed
|
47
|
+
# > by a small number of certificates. The acceptable certificates would
|
48
|
+
# > be passed in the certs parameter. In this case if the signer is not
|
49
|
+
# > one of the certificates supplied in certs then the verify will fail
|
50
|
+
# > because the signer cannot be found.
|
51
|
+
#
|
52
|
+
# https://www.openssl.org/docs/manmaster/man3/PKCS7_verify.html
|
53
|
+
#
|
54
|
+
# we want this so we can be sure that the `partner_certificate` we supply
|
55
|
+
# was actually used to sign the message. otherwise we could get a positive
|
56
|
+
# verification even if `partner_certificate` didn't sign the message
|
57
|
+
# we're checking.
|
58
|
+
#
|
59
|
+
# ## NOVERIFY
|
60
|
+
#
|
61
|
+
# > If PKCS7_NOVERIFY is set the signer's certificates are not chain verified.
|
62
|
+
#
|
63
|
+
# ie: we won't attempt to connect signer (in the first param) to a root
|
64
|
+
# CA (in `store`, which is empty). alternately, we could instead remove
|
65
|
+
# this flag, and add `partner_certificate` to `store`. but what's the point?
|
66
|
+
# we'd only be verifying that `partner_certificate` is connected to `partner_certificate`.
|
67
|
+
output = signature.verify([partner_certificate], store, content, OpenSSL::PKCS7::NOVERIFY | OpenSSL::PKCS7::NOINTERN)
|
68
|
+
|
69
|
+
# when `signature.verify` fails, signature.error_string will be populated.
|
70
|
+
@verification_error = signature.error_string
|
71
|
+
|
72
|
+
output
|
73
|
+
else
|
74
|
+
# TODO: how to log this?
|
75
|
+
false
|
76
|
+
end
|
77
|
+
end
|
20
78
|
|
21
|
-
|
22
|
-
|
23
|
-
message.verify [partner_certificate], store
|
79
|
+
def mic
|
80
|
+
OpenSSL::Digest::SHA1.base64digest(attachment.raw_source.strip)
|
24
81
|
end
|
25
82
|
|
26
83
|
# Return the attached file, use .filename and .body on the return value
|
@@ -33,17 +90,9 @@ module As2
|
|
33
90
|
end
|
34
91
|
|
35
92
|
private
|
93
|
+
|
36
94
|
def mail
|
37
95
|
@mail ||= Mail.new(decrypted_message)
|
38
96
|
end
|
39
|
-
|
40
|
-
def read_smime(smime)
|
41
|
-
OpenSSL::PKCS7.read_smime(smime)
|
42
|
-
end
|
43
|
-
|
44
|
-
def decrypt_smime(smime)
|
45
|
-
message = read_smime(smime)
|
46
|
-
message.decrypt @private_key, @public_certificate
|
47
|
-
end
|
48
97
|
end
|
49
98
|
end
|
data/lib/as2/server.rb
CHANGED
@@ -2,20 +2,10 @@ require 'rack'
|
|
2
2
|
require 'logger'
|
3
3
|
require 'stringio'
|
4
4
|
require 'as2/mime_generator'
|
5
|
-
require 'as2/base64_helper'
|
6
5
|
require 'as2/message'
|
7
6
|
|
8
7
|
module As2
|
9
8
|
class Server
|
10
|
-
HEADER_MAP = {
|
11
|
-
'To' => 'HTTP_AS2_TO',
|
12
|
-
'From' => 'HTTP_AS2_FROM',
|
13
|
-
'Subject' => 'HTTP_SUBJECT',
|
14
|
-
'MIME-Version' => 'HTTP_MIME_VERSION',
|
15
|
-
'Content-Disposition' => 'HTTP_CONTENT_DISPOSITION',
|
16
|
-
'Content-Type' => 'CONTENT_TYPE',
|
17
|
-
}
|
18
|
-
|
19
9
|
attr_accessor :logger
|
20
10
|
|
21
11
|
def initialize(options = {}, &block)
|
@@ -34,18 +24,21 @@ module As2
|
|
34
24
|
return send_error(env, "Invalid partner name #{env['HTTP_AS2_FROM']}")
|
35
25
|
end
|
36
26
|
|
37
|
-
|
38
|
-
message = Message.new(
|
27
|
+
request = Rack::Request.new(env)
|
28
|
+
message = Message.new(request.body.read, @info.pkey, @info.certificate)
|
29
|
+
|
39
30
|
unless message.valid_signature?(partner.certificate)
|
40
31
|
if @options[:on_signature_failure]
|
41
|
-
@options[:on_signature_failure].call({
|
32
|
+
@options[:on_signature_failure].call({
|
33
|
+
env: env,
|
34
|
+
smime_string: message.decrypted_message,
|
35
|
+
verification_error: message.verification_error
|
36
|
+
})
|
42
37
|
else
|
43
38
|
raise "Could not verify signature"
|
44
39
|
end
|
45
40
|
end
|
46
41
|
|
47
|
-
mic = OpenSSL::Digest::SHA1.base64digest(message.decrypted_message)
|
48
|
-
|
49
42
|
if @block
|
50
43
|
begin
|
51
44
|
@block.call message.attachment.filename, message.attachment.body
|
@@ -54,24 +47,10 @@ module As2
|
|
54
47
|
end
|
55
48
|
end
|
56
49
|
|
57
|
-
send_mdn(env, mic)
|
50
|
+
send_mdn(env, message.mic)
|
58
51
|
end
|
59
52
|
|
60
53
|
private
|
61
|
-
def build_smime_text(env)
|
62
|
-
request = Rack::Request.new(env)
|
63
|
-
smime_data = StringIO.new
|
64
|
-
|
65
|
-
HEADER_MAP.each do |name, value|
|
66
|
-
smime_data.puts "#{name}: #{env[value]}"
|
67
|
-
end
|
68
|
-
|
69
|
-
smime_data.puts 'Content-Transfer-Encoding: base64'
|
70
|
-
smime_data.puts
|
71
|
-
smime_data.puts Base64Helper.ensure_base64(request.body.read)
|
72
|
-
|
73
|
-
return smime_data.string
|
74
|
-
end
|
75
54
|
|
76
55
|
def logger(env)
|
77
56
|
@logger ||= Logger.new env['rack.errors']
|
data/lib/as2/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: as2
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- OfficeLuv
|
8
|
-
|
8
|
+
- Transfix
|
9
|
+
autorequire:
|
9
10
|
bindir: exe
|
10
11
|
cert_chain: []
|
11
|
-
date:
|
12
|
+
date: 2021-12-22 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
13
14
|
- !ruby/object:Gem::Dependency
|
14
15
|
name: mail
|
@@ -42,28 +43,28 @@ dependencies:
|
|
42
43
|
name: bundler
|
43
44
|
requirement: !ruby/object:Gem::Requirement
|
44
45
|
requirements:
|
45
|
-
- - "
|
46
|
+
- - ">="
|
46
47
|
- !ruby/object:Gem::Version
|
47
48
|
version: '1.10'
|
48
49
|
type: :development
|
49
50
|
prerelease: false
|
50
51
|
version_requirements: !ruby/object:Gem::Requirement
|
51
52
|
requirements:
|
52
|
-
- - "
|
53
|
+
- - ">="
|
53
54
|
- !ruby/object:Gem::Version
|
54
55
|
version: '1.10'
|
55
56
|
- !ruby/object:Gem::Dependency
|
56
57
|
name: rake
|
57
58
|
requirement: !ruby/object:Gem::Requirement
|
58
59
|
requirements:
|
59
|
-
- - "
|
60
|
+
- - ">="
|
60
61
|
- !ruby/object:Gem::Version
|
61
62
|
version: '10.0'
|
62
63
|
type: :development
|
63
64
|
prerelease: false
|
64
65
|
version_requirements: !ruby/object:Gem::Requirement
|
65
66
|
requirements:
|
66
|
-
- - "
|
67
|
+
- - ">="
|
67
68
|
- !ruby/object:Gem::Version
|
68
69
|
version: '10.0'
|
69
70
|
- !ruby/object:Gem::Dependency
|
@@ -98,13 +99,14 @@ description: Simple AS2 server and client implementation. Follows the AS2 implem
|
|
98
99
|
from http://as2.mendelson-e-c.com
|
99
100
|
email:
|
100
101
|
- development@officeluv.com
|
102
|
+
- alexdean@transfix.io
|
101
103
|
executables: []
|
102
104
|
extensions: []
|
103
105
|
extra_rdoc_files: []
|
104
106
|
files:
|
105
107
|
- ".gitignore"
|
108
|
+
- CHANGELOG.md
|
106
109
|
- Gemfile
|
107
|
-
- Gemfile.lock
|
108
110
|
- LICENSE.txt
|
109
111
|
- README.md
|
110
112
|
- Rakefile
|
@@ -126,7 +128,7 @@ licenses:
|
|
126
128
|
- MIT
|
127
129
|
metadata:
|
128
130
|
allowed_push_host: https://rubygems.org/
|
129
|
-
post_install_message:
|
131
|
+
post_install_message:
|
130
132
|
rdoc_options: []
|
131
133
|
require_paths:
|
132
134
|
- lib
|
@@ -141,9 +143,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
141
143
|
- !ruby/object:Gem::Version
|
142
144
|
version: '0'
|
143
145
|
requirements: []
|
144
|
-
|
145
|
-
|
146
|
-
signing_key:
|
146
|
+
rubygems_version: 3.1.4
|
147
|
+
signing_key:
|
147
148
|
specification_version: 4
|
148
149
|
summary: Simple AS2 server and client implementation
|
149
150
|
test_files: []
|
data/Gemfile.lock
DELETED
@@ -1,35 +0,0 @@
|
|
1
|
-
PATH
|
2
|
-
remote: .
|
3
|
-
specs:
|
4
|
-
as2 (0.2.4)
|
5
|
-
mail
|
6
|
-
rack
|
7
|
-
|
8
|
-
GEM
|
9
|
-
remote: https://rubygems.org/
|
10
|
-
specs:
|
11
|
-
daemons (1.2.3)
|
12
|
-
eventmachine (1.0.8)
|
13
|
-
mail (2.6.3)
|
14
|
-
mime-types (>= 1.16, < 3)
|
15
|
-
mime-types (2.6.2)
|
16
|
-
minitest (5.8.1)
|
17
|
-
rack (1.6.4)
|
18
|
-
rake (10.4.2)
|
19
|
-
thin (1.6.4)
|
20
|
-
daemons (~> 1.0, >= 1.0.9)
|
21
|
-
eventmachine (~> 1.0, >= 1.0.4)
|
22
|
-
rack (~> 1.0)
|
23
|
-
|
24
|
-
PLATFORMS
|
25
|
-
ruby
|
26
|
-
|
27
|
-
DEPENDENCIES
|
28
|
-
as2!
|
29
|
-
bundler (~> 1.10)
|
30
|
-
minitest
|
31
|
-
rake (~> 10.0)
|
32
|
-
thin
|
33
|
-
|
34
|
-
BUNDLED WITH
|
35
|
-
1.10.6
|