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 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