action_mailer_x509 0.7.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.
@@ -0,0 +1,12 @@
1
+ require 'action_mailer_x509'
2
+
3
+ class Notifier < ActionMailer::Base #:nodoc:
4
+ x509_configuration :test
5
+ self.prepend_view_path("#{File.dirname(__FILE__)}/../views/")
6
+
7
+ def fufu(email, from, subject = 'Empty subject')
8
+ mail(subject: subject, to: email, from: from) do |format|
9
+ format.text { render 'fufu' }
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,46 @@
1
+ require 'action_mailer_x509'
2
+
3
+ module ActionMailer #:nodoc:
4
+ class Base #:nodoc:
5
+ class_attribute :x509_configuration
6
+
7
+ class << self
8
+ def x509_configuration(name = nil)
9
+ @x509_configuration = name if name
10
+ @x509_configuration
11
+ end
12
+ end
13
+
14
+ alias_method :old_mail, :mail
15
+
16
+ def configuration
17
+ ActionMailerX509.get_configuration(x509_configuration)
18
+ end
19
+
20
+ def mail(headers = {}, &block)
21
+ self.class.x509_configuration(headers.delete(:x509_configuration)) if headers[:x509_configuration]
22
+ message = old_mail(headers, &block)
23
+ x509_smime(message) if configuration && (configuration.sign_require? || configuration.crypt_require?)
24
+ end
25
+
26
+ private
27
+ # X509 SMIME signing and\or crypting
28
+ def x509_smime(message)
29
+ #raise Exception.new('Configuration is nil') unless configuration
30
+ @coded = configuration.get_crypter.encode(message.content) if configuration.crypt_require?
31
+ @signed = configuration.get_signer.sign(@coded || message.content) if configuration.sign_require?
32
+
33
+ p = Mail.new(@signed || @coded)
34
+ p.header.fields.each {|field| (message.header[field.name] = field.value)}
35
+
36
+ if @signed
37
+ message.instance_variable_set :@body_raw, p.body.to_s
38
+ else
39
+ #PATCH: header field 'Content-Transfer-Encoding' is not copied by the some mystic reasons
40
+ message.header['Content-Transfer-Encoding'] = 'base64'
41
+ message.instance_variable_set :@body_raw, Base64.encode64(p.body.to_s)
42
+ end
43
+ message
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,110 @@
1
+ require 'action_mailer_x509'
2
+
3
+ module Mail #:nodoc:
4
+ class Message #:nodoc:
5
+
6
+ def proceed(result_type = 'html', configuration = nil)
7
+ proceed_part(_proceed(configuration), result_type)
8
+ end
9
+
10
+ def method_missing(name, *args, &block)
11
+ elems = name.to_s.split('_as_')
12
+
13
+ if elems.size == 2
14
+ result = self.send(elems.first.to_sym, *args)
15
+ if result.is_a? String
16
+ convert(result, elems.last)
17
+ elsif result.respond_to? :to_a
18
+ result.to_a.map {|item| convert(item.to_s, elems.last)}
19
+ else
20
+ convert(result.to_s, elems.last)
21
+ end
22
+ end
23
+ end
24
+
25
+ def content
26
+ parts.empty? ? to_part : body.encoded
27
+ end
28
+
29
+ protected
30
+ # Returns true if the message is a multipart/alternative
31
+ def alternative?(part)
32
+ part.sub_type.downcase == 'alternative'
33
+ end
34
+
35
+
36
+ def _proceed(configuration = nil)
37
+ config = ActionMailerX509.get_configuration(configuration)
38
+ if (signed = is_signed?) || is_crypted?
39
+ raise Exception.new('Configuration is nil') unless config
40
+ result = if signed
41
+ config.get_signer.verify(patched_encoded)
42
+ else
43
+ config.get_crypter.decode(body.to_s)
44
+ end
45
+ if result && (mail = Mail.new(result)).valid?
46
+ mail._proceed(configuration)
47
+ end || result
48
+ end || self
49
+ end
50
+
51
+ def proceed_part(part, result_type = 'html')
52
+ if part.multipart?
53
+ if alternative?(part)
54
+ variants = part.parts.each_with_object({}) {|p, res| res.update(p.sub_type => p)}
55
+ @new_part = variants[result_type] || variants['html'] || variants['plain'] || part.first
56
+ end
57
+
58
+ _decode_body(result_type, @new_part || part)
59
+ else
60
+ part.decoded.to_s unless part.attachment?
61
+ end
62
+ end
63
+
64
+ # we need manually split body on parts and decode each part separate
65
+ def _decode_body(result_type, obj = self)
66
+ obj.body.split(obj.boundary) if obj.parts.blank? && obj.boundary.present?
67
+
68
+ if obj.parts.present?
69
+ obj.parts.map {|part| proceed_part(part, result_type)}.join(break_line(result_type))
70
+ else
71
+ obj.decoded
72
+ end
73
+ end
74
+
75
+ def break_line(content_type)
76
+ content_type == 'html' ? '<br>' : "\r\n"
77
+ end
78
+
79
+ def convert(text, encoding)
80
+ text.force_encoding(encoding.dasherize)
81
+ end
82
+
83
+ def to_part
84
+ part = Mail::Part.new((text = body.encoded))
85
+ part.header[:content_type] = 'text/html' if text.index(/<\w+>/)
86
+ part.header['Content-Transfer-Encoding'] = '8bit'
87
+ part.encoded
88
+ end
89
+
90
+ def valid?
91
+ header['Content-Type'].present? && body.encoded.length > 0
92
+ end
93
+
94
+ #PATCH: body.encoded return wrong result in encoded func
95
+ def patched_encoded
96
+ buffer = header.encoded
97
+ buffer << "\r\n"
98
+ buffer << body.to_s
99
+ buffer
100
+ end
101
+
102
+ def is_crypted?
103
+ (header['Content-Type'].encoded =~ /application\/(x-)?pkcs[0-9]+-mime/).present?
104
+ end
105
+
106
+ def is_signed?
107
+ (header['Content-Type'].encoded =~ /application\/(x-)?pkcs[0-9]+-signature/).present?
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,184 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rdoc/task'
4
+ require 'models/notifier'
5
+ require 'action_mailer_x509/x509'
6
+
7
+ namespace :action_mailer_x509 do
8
+ desc 'Run all checks'
9
+ task :all do
10
+ puts '*' * 20 << 'SIGN VERIFICATION'
11
+ Rake::Task['action_mailer_x509:verify_signature'].invoke
12
+ puts '*' * 20 << 'OPENSSL SIGN VERIFICATION'
13
+ Rake::Task['action_mailer_x509:verify_signature_by_openssl'].invoke
14
+ puts '*' * 20 << 'CRYPT VERIFICATION'
15
+ Rake::Task['action_mailer_x509:verify_crypt'].invoke
16
+ puts '*' * 20 << 'OPENSSL CRYPT VERIFICATION'
17
+ Rake::Task['action_mailer_x509:verify_crypt_by_openssl'].invoke
18
+ puts '*' * 20 << 'SIGN AND CRYPT VERIFICATION'
19
+ Rake::Task['action_mailer_x509:verify_sign_and_crypt'].invoke
20
+ puts '*' * 20
21
+ #Rake::Task['action_mailer_x509:verify_sign_and_crypt_by_openssl'].invoke
22
+ #puts '*' * 20
23
+ end
24
+
25
+ desc 'Generates a signed mail in a file.'
26
+ task(:generate_signed_mail => :environment) do
27
+ add_config(true, false)
28
+ mail = Notifier.fufu('<destination@foobar.com>', '<demo@foobar.com>')
29
+ path = ENV['mail'] || Rails.root.join('certs/signed_mail.txt')
30
+ File.open(path, 'wb') do |f|
31
+ f.write mail.body
32
+ end
33
+ puts "Signed mail is at #{path}"
34
+ puts 'You can use mail=filename as argument to change it.' if ENV['mail'].nil?
35
+ end
36
+
37
+ desc 'Check if signature is valid.'
38
+ task(:verify_signature => :environment) do
39
+ add_config(false, false)
40
+ raw_mail = Notifier.fufu('<destination@foobar.com>', '<demo@foobar.com>')
41
+
42
+ add_config(true, false)
43
+ mail = Notifier.fufu('<destination@foobar.com>', '<demo@foobar.com>')
44
+
45
+ verified = mail.proceed('html', Notifier.x509_configuration)
46
+ puts "Verification is #{(verified) == raw_mail.body.to_s}"
47
+ end
48
+
49
+ desc 'Check if signature is valid by openssl.'
50
+ task(:verify_signature_by_openssl => :environment) do
51
+ require 'tempfile'
52
+
53
+ add_config(true, false)
54
+ mail = Notifier.fufu("<destination@foobar.com>", "<demo@foobar.com>")
55
+
56
+ tf = Tempfile.new('actionmailer_x509')
57
+ tf.write mail.encoded
58
+ tf.flush
59
+ configuration = ActionMailerX509.get_configuration(Notifier.x509_configuration)
60
+
61
+ comm = "openssl smime -verify -in #{tf.path} -CAfile #{configuration.sign_cert} > /dev/null"
62
+
63
+ puts 'Using openssl command to verify signature...'
64
+ system(comm)
65
+ end
66
+
67
+ desc 'Generates a crypted mail in a file.'
68
+ task(:generate_crypted_mail => :environment) do
69
+ add_config(false, true)
70
+ mail = Notifier.fufu('<destination@foobar.com>', '<demo@foobar.com>')
71
+ path = ENV['mail'] || Rails.root.join('certs/cripted_mail.txt')
72
+
73
+ File.open(path, 'wb') do |f|
74
+ f.write mail.body
75
+ end
76
+
77
+ p "Crypted mail is at #{path}"
78
+ p 'You can use mail=filename as argument to change it.' if ENV['mail'].nil?
79
+ end
80
+
81
+ desc 'Check crypt.'
82
+ task(:verify_crypt => :environment) do
83
+ add_config(false, false)
84
+ raw_mail = Notifier.fufu('<destination@foobar.com>', '<demo@foobar.com>')
85
+
86
+ add_config(false, true)
87
+ mail = Notifier.fufu('<destination@foobar.com>', '<demo@foobar.com>')
88
+
89
+ decrypted = mail.proceed('html', Notifier.x509_configuration)
90
+ puts "Crypt verification is #{decrypted.to_s == raw_mail.body.decoded}"
91
+ end
92
+
93
+ desc 'Check if crypt text is valid by openssl.'
94
+ task(:verify_crypt_by_openssl => :environment) do
95
+ require 'tempfile'
96
+
97
+ add_config(false, true)
98
+ mail = Notifier.fufu("<destination@foobar.com>", "<demo@foobar.com>")
99
+
100
+ tf = Tempfile.new('actionmailer_x509')
101
+ tf.write mail.encoded
102
+ tf.flush
103
+ configuration = ActionMailerX509.get_configuration(Notifier.x509_configuration)
104
+
105
+ comm = "openssl smime -decrypt -passin pass:#{configuration.crypt_passphrase} -in #{tf.path} -recip #{configuration.crypt_cert} -inkey #{configuration.crypt_key} > /dev/null"
106
+ puts 'Using openssl command to verify crypted code...'
107
+ puts "Crypt verification is #{system(comm)}"
108
+ end
109
+
110
+ desc 'Check sign and crypt.'
111
+ task(:verify_sign_and_crypt => :environment) do
112
+ add_config(false, false)
113
+ raw_mail = Notifier.fufu('<destination@foobar.com>', '<demo@foobar.com>')
114
+
115
+ add_config
116
+ mail = Notifier.fufu('<destination@foobar.com>', '<demo@foobar.com>')
117
+
118
+ decrypted = mail.proceed('html', Notifier.x509_configuration)
119
+ puts "Verification is #{decrypted.to_s == raw_mail.body.decoded}"
120
+ end
121
+
122
+ #desc 'Check if crypt text is valid by openssl.'
123
+ #task(:verify_sign_and_crypt_by_openssl => :environment) do
124
+ # require 'tempfile'
125
+ #
126
+ # add_config
127
+ # mail = Notifier.fufu('<destination@foobar.com>', '<demo@foobar.com>')
128
+ #
129
+ # tf = Tempfile.new('actionmailer_x509')
130
+ # tf.write mail.encoded
131
+ # tf.flush
132
+ # configuration = ActionMailerX509.get_configuration(Notifier.x509_configuration)
133
+ #
134
+ # comm = "openssl smime -verify -in #{tf.path} -CAfile #{configuration.sign_cert} 2>&1 | openssl smime -decrypt -passin pass:#{configuration.crypt_passphrase} -in #{tf.path} -recip #{configuration.crypt_cert} -inkey #{configuration.crypt_key}"
135
+ # puts "Crypt and sign verification is #{system(comm)}"
136
+ #end
137
+
138
+ desc 'Performance test.'
139
+ task(:performance_test => :environment) do
140
+ require 'benchmark'
141
+
142
+ n = 100
143
+ Benchmark.bm do |x|
144
+ add_config(false, false)
145
+ x.report("#{n} raw mails: ".ljust(40)) {
146
+ n.times { Notifier.fufu('<destination@foobar.com>', '<demo@foobar.com>') }
147
+ }
148
+ add_config(true)
149
+ x.report("#{n} mails with signature: ".ljust(40)) {
150
+ n.times { Notifier.fufu('<destination@foobar.com>', '<demo@foobar.com>') }
151
+ }
152
+
153
+ add_config(false, true)
154
+ x.report("#{n} crypted mails: ".ljust(40)) {
155
+ n.times { Notifier.fufu('<destination@foobar.com>', '<demo@foobar.com>') }
156
+ }
157
+
158
+ add_config
159
+ x.report("#{n} crypted and signed mails: ".ljust(40)) {
160
+ n.times { Notifier.fufu('<destination@foobar.com>', '<demo@foobar.com>') }
161
+ }
162
+ end
163
+ end
164
+ end
165
+
166
+ private
167
+ def add_config(sign = true, crypt = true)
168
+ ActionMailerX509.add_configuration :test,
169
+ {
170
+ sign_enable: sign,
171
+ crypt_enable: crypt,
172
+ cert: 'ca.crt',
173
+ key: 'ca.key',
174
+ passphrase: 'hisp',
175
+ certs_path: Rails.root.join('certs/stock')
176
+ }
177
+ end
178
+
179
+ def Boolean(string)
180
+ return true if string == true || string =~ /^true$/i
181
+ return false if string == false || string.nil? || string =~ /^false$/i
182
+ raise ArgumentError.new("invalid value for Boolean: \"#{string}\"")
183
+ end
184
+
@@ -0,0 +1,3 @@
1
+ This is the first line
2
+
3
+ This is the 3rd line, to make sure...
data/spec/ops_spec.rb ADDED
@@ -0,0 +1,129 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Test basic functions' do
4
+ describe 'Correct' do
5
+ describe 'Signature' do
6
+ before do
7
+ add_config(false, false)
8
+ @raw_mail = Notifier.fufu('<destination@foobar.com>', '<demo@foobar.com>')
9
+
10
+ add_config(true, false)
11
+ @mail = Notifier.fufu('<destination@foobar.com>', '<demo@foobar.com>')
12
+ end
13
+
14
+ it 'Must generate signature' do
15
+ @mail.body.to_s.should_not eql @raw_mail.body.to_s
16
+ end
17
+
18
+ it 'Verification check' do
19
+ verified = @mail.proceed(Notifier.x509_configuration)
20
+ verified.should eql @raw_mail.body.to_s
21
+ end
22
+ end
23
+
24
+ describe 'Crypting' do
25
+ before do
26
+ add_config(false, false)
27
+ @raw_mail = Notifier.fufu('<destination@foobar.com>', '<demo@foobar.com>')
28
+
29
+ add_config(false, true)
30
+ @mail = Notifier.fufu('<destination@foobar.com>', '<demo@foobar.com>')
31
+ end
32
+
33
+ it 'Must generate crypted text' do
34
+ @mail.body.decoded.should_not eql @raw_mail.body.decoded
35
+ end
36
+
37
+ it 'Must generate crypted text' do
38
+ decrypted = @mail.proceed(Notifier.x509_configuration)
39
+ decrypted.to_s.should eql @raw_mail.body.decoded
40
+ end
41
+ end
42
+
43
+ it 'Crypting and Signature' do
44
+ add_config(false, false)
45
+ raw_mail = Notifier.fufu('<destination@foobar.com>', '<demo@foobar.com>')
46
+
47
+ add_config
48
+ mail = Notifier.fufu('<destination@foobar.com>', '<demo@foobar.com>')
49
+
50
+ decrypted = mail.proceed(Notifier.x509_configuration)
51
+ decrypted.to_s.should eql raw_mail.body.decoded
52
+ end
53
+
54
+ it 'Crypting and Signature by p12_certificate' do
55
+ add_config(false, false)
56
+ raw_mail = Notifier.fufu('<destination@foobar.com>', '<demo@foobar.com>')
57
+
58
+ add_config(true, true, true)
59
+ mail = Notifier.fufu('<destination@foobar.com>', '<demo@foobar.com>')
60
+
61
+ decrypted = mail.proceed(Notifier.x509_configuration)
62
+ decrypted.to_s.should eql raw_mail.body.decoded
63
+ end
64
+ end
65
+
66
+ describe 'Incorrect' do
67
+ it 'sign incorrect key' do
68
+ add_config(true, false)
69
+
70
+ mail = Notifier.fufu('<destination@foobar.com>', '<demo@foobar.com>')
71
+ mail.body.to_s.should_not be_empty
72
+
73
+ set_config_param(sign_passphrase: 'wrong')
74
+ -> { mail.proceed(Notifier.x509_configuration) }.should raise_error OpenSSL::PKey::RSAError
75
+ end
76
+
77
+ describe 'incorrect text' do
78
+ it 'sign' do
79
+ add_config(true, false)
80
+ mail = Notifier.fufu('<destination@foobar.com>', '<demo@foobar.com>')
81
+ mail.body = mail.body.to_s.gsub(/[0-9]/, 'g')
82
+ -> { mail.proceed(Notifier.x509_configuration) }.should raise_error VerificationError
83
+ end
84
+
85
+ it 'crypt' do
86
+ add_config(false, true)
87
+ mail = Notifier.fufu('<destination@foobar.com>', '<demo@foobar.com>')
88
+ mail.body = mail.body.to_s.gsub(/[0-9]/, 'g')
89
+ -> { mail.proceed(Notifier.x509_configuration) }.should raise_error DecodeError
90
+ end
91
+ end
92
+
93
+ describe 'incorrect certs' do
94
+ it 'sign' do
95
+ add_config(true, false)
96
+ set_config_param(crypt_cert: 'cert.crt',
97
+ crypt_key: 'cert.key')
98
+ mail = Notifier.fufu('<destination@foobar.com>', '<demo@foobar.com>')
99
+ mail.body = mail.body.to_s.gsub(/[0-9]/, 'g')
100
+ -> { mail.proceed(Notifier.x509_configuration) }.should raise_error VerificationError
101
+ end
102
+
103
+ it 'crypt' do
104
+ add_config(false, true)
105
+ set_config_param(crypt_cert: 'cert.crt',
106
+ crypt_key: 'cert.key')
107
+ -> { Notifier.fufu('<destination@foobar.com>', '<demo@foobar.com>') }.should raise_error OpenSSL::PKey::RSAError
108
+ end
109
+ end
110
+
111
+ describe 'valid func' do
112
+ it 'must return true on non crypted and not signed' do
113
+ add_config(false, false)
114
+ get_config.valid?.should eql true
115
+ end
116
+
117
+ it 'must return false on wrong passphrase' do
118
+ add_config
119
+ set_config_param(sign_passphrase: 'wrong')
120
+ get_config.valid?.should eql false
121
+ end
122
+
123
+ it 'must return false on wrong certificate' do
124
+ add_usual_cert_as_p12_config
125
+ get_config.valid?.should eql false
126
+ end
127
+ end
128
+ end
129
+ end