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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8a3998531af86ec28183bebe5adaa77a0cba1b66f43d9b2270b53470b34f5d76
4
- data.tar.gz: '079dfb30bfb10cb2a22d33502c155dda1d6c7069a87436b9a62e58d1521d2fa6'
3
+ metadata.gz: 3c28a94ec5915cd8930ba91814dea748017f86d0cbe2be94719fa54df5e6774f
4
+ data.tar.gz: 47239349434164df4c1833561b6942e458f9b919415d221b9ae376b4a28755fe
5
5
  SHA512:
6
- metadata.gz: f968aeb0ab8f1836bbcad82eec78c8a13f5507edef264df0cc397b767524628ef5936f64fa6b84cb8bffec1c4eae492336c9f64c24376d6fc42a17911ece450c
7
- data.tar.gz: e05c3239d2a17c7f5c236544d184fdb038c80d4917c11187b54b77091fc17efc9e9048428384976c2c45a28f955d6ba321f63c576b98220e647ac8b16dc3a860
6
+ metadata.gz: b7f77c9176b2f279d5a678f72494f746b687432487df3b2c1d338754b89e5942e9af4798360a39528514cb826cc923148ac4a812cc64d65b7f9f4a104eb71b2f
7
+ data.tar.gz: efdc7d01786e56ad1aabbbd9c373aa9d835bb299c775423df0470e622916efe080a0abc3b7bc6173e39b80223824d3c3bb885b6fe8d6b93405329dcaf9f82fd5
data/.gitignore CHANGED
@@ -1,2 +1,3 @@
1
1
  certs
2
2
  .DS_Store
3
+ Gemfile.lock
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", "~> 1.10"
34
- spec.add_development_dependency "rake", "~> 10.0"
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'] = OpenSSL::X509::Certificate.new File.read(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'] = OpenSSL::X509::Certificate.new File.read(certificate)
39
+ self['certificate'] = As2::Config.build_certificate(certificate)
29
40
  end
30
41
 
31
- def pkey=(pkey)
32
- self['pkey'] = OpenSSL::PKey.read File.read(pkey)
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
- begin
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 :original_message
3
+ attr_reader :verification_error
6
4
 
7
5
  def initialize(message, private_key, public_certificate)
8
- @original_message = message
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 ||= decrypt_smime(original_message)
14
+ @decrypted_message ||= @pkcs7.decrypt @private_key, @public_certificate
15
15
  end
16
16
 
17
17
  def valid_signature?(partner_certificate)
18
- store = OpenSSL::X509::Store.new
19
- store.add_cert(partner_certificate)
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
- smime = Base64Helper.ensure_body_base64(decrypted_message)
22
- message = read_smime(smime)
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
- smime_string = build_smime_text(env)
38
- message = Message.new(smime_string, @info.pkey, @info.certificate)
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({env: env, smime_string: smime_string})
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
@@ -1,3 +1,3 @@
1
1
  module As2
2
- VERSION = "0.2.5"
2
+ VERSION = "0.3.0"
3
3
  end
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.2.5
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - OfficeLuv
8
- autorequire:
8
+ - Transfix
9
+ autorequire:
9
10
  bindir: exe
10
11
  cert_chain: []
11
- date: 2019-03-12 00:00:00.000000000 Z
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
- rubyforge_project:
145
- rubygems_version: 2.7.6
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