as2 0.2.5 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|