action_mailer_x509 0.7.0

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