mail-gpg 0.1.7 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 431a878dcca05522a519c9edca6a42e658c9b0c2
4
+ data.tar.gz: 18f9aeb942581062f8644e14391520d825829473
5
+ SHA512:
6
+ metadata.gz: baf5ef3a729d141a0d76d1fe1466a8cbbdc954e373af0c23e58020d64933af087cf3839a1ff6bdc9ccfebaa539aa2af5026b6c543848998912344361718a3163
7
+ data.tar.gz: 0052629db9f5c1a595c10d2dbf491be5f9544ae0fdb37b7541437bb2bf4866f559a9f7cd98d235d9cd414494e049905069c7dd730181d9fb749deec277943eaa
data/History.txt CHANGED
@@ -1,3 +1,14 @@
1
+ == 0.2.1 2014-07-23
2
+
3
+ * keep original message id if set
4
+
5
+ == 0.2.0 2014-07-16
6
+
7
+ * implement signature verification for inline signed messages
8
+ * make signature verification more consistent with decryption by providing
9
+ Mail::Message#verify which strips raw signature data and initializes verify_result member.
10
+ * add Mail::Message#signatures as an easy way to retrieve all signatures on a Message
11
+
1
12
  == 0.1.7 2014-06-03
2
13
 
3
14
  * preserve References: header
data/README.md CHANGED
@@ -119,14 +119,28 @@ Receive the mail as usual. Check if it is signed using the `signed?` method. Che
119
119
  ```ruby
120
120
  mail = Mail.first
121
121
  if !mail.encrypted? && mail.signed?
122
- # do not call signed on encrypted mails. The signature on encrypted mails
123
- # must be checked by setting the :verify option when decrypting
124
- puts "signature(s) valid: #{mail.signature_valid?}"
122
+ verified = mail.verify
123
+ puts "signature(s) valid: #{verified.signature_valid?}"
124
+ puts "message signed by: #{verified.signatures.map{|sig|sig.from}.join("\n")}"
125
125
  end
126
126
  ```
127
127
 
128
- Note that for encrypted mails the signatures can not be checked using these methods. For encrypted mails
129
- the `:verify` option for the `decrypt` operation must be used instead.
128
+ Note that for encrypted mails the signatures can not be checked using these
129
+ methods. For encrypted mails the `:verify` option for the `decrypt` operation
130
+ must be used instead:
131
+
132
+ ```ruby
133
+ if mail.encrypted?
134
+ decrypted = mail.decrypt(verify: true, password: 's3cr3t')
135
+ puts "signature(s) valid: #{decrypted.signature_valid?}"
136
+ puts "message signed by: #{decrypted.signatures.map{|sig|sig.from}.join("\n")}"
137
+ end
138
+ ```
139
+
140
+ It's important to actually use the information contained in the `signatures`
141
+ array to check if the message really has been signed by the person that you (or
142
+ your users) think is the sender - usually by comparing the key id of the
143
+ signature with the key id of the expected sender.
130
144
 
131
145
  ### Key import from public key servers
132
146
 
@@ -201,5 +215,7 @@ around with your personal gpg keychain.
201
215
 
202
216
  Thanks to:
203
217
 
218
+ * [Planio GmbH](https://plan.io) for sponsoring the ongoing maintenance and development of this library
204
219
  * [morten-andersen](https://github.com/morten-andersen) for implementing decryption support for PGP/MIME and inline encrypted messages
205
- * [FewKinG](https://github.com/FewKinG) for implementing the sign only featur and keyserver url lookup
220
+ * [FewKinG](https://github.com/FewKinG) for implementing the sign only feature and keyserver url lookup
221
+ * [Fup Duck](https://github.com/duckdalbe) for various tweaks and fixes
data/lib/mail/gpg.rb CHANGED
@@ -3,6 +3,7 @@ require 'mail/message'
3
3
  require 'gpgme'
4
4
 
5
5
  require 'mail/gpg/version'
6
+ require 'mail/gpg/missing_keys_error'
6
7
  require 'mail/gpg/version_part'
7
8
  require 'mail/gpg/decrypted_part'
8
9
  require 'mail/gpg/encrypted_part'
@@ -11,6 +12,8 @@ require 'mail/gpg/gpgme_helper'
11
12
  require 'mail/gpg/message_patch'
12
13
  require 'mail/gpg/rails'
13
14
  require 'mail/gpg/signed_part'
15
+ require 'mail/gpg/mime_signed_message'
16
+ require 'mail/gpg/inline_signed_message'
14
17
 
15
18
  module Mail
16
19
  module Gpg
@@ -71,6 +74,8 @@ module Mail
71
74
  def self.signature_valid?(signed_mail, options = {})
72
75
  if signed_mime?(signed_mail)
73
76
  signature_valid_pgp_mime?(signed_mail, options)
77
+ elsif signed_inline?(signed_mail)
78
+ signature_valid_inline?(signed_mail, options)
74
79
  else
75
80
  raise EncodingError, "Unsupported signature format '#{signed_mail.content_type}'"
76
81
  end
@@ -108,6 +113,9 @@ module Mail
108
113
  self.header[field] = h.value
109
114
  end
110
115
  end
116
+ if cleartext_mail.message_id
117
+ header['Message-ID'] = cleartext_mail['Message-ID'].value
118
+ end
111
119
  cleartext_mail.header.fields.each do |field|
112
120
  if MORE_HEADERS.include?(field.name) or field.name =~ /^X-/
113
121
  header[field.name] = field.value
@@ -126,7 +134,8 @@ module Mail
126
134
  if !VersionPart.isVersionPart? encrypted_mail.parts[0]
127
135
  raise EncodingError, "RFC 3136 first part not a valid version part '#{encrypted_mail.parts[0]}'"
128
136
  end
129
- Mail.new(DecryptedPart.new(encrypted_mail.parts[1], options)) do
137
+ decrypted = DecryptedPart.new(encrypted_mail.parts[1], options)
138
+ Mail.new(decrypted) do
130
139
  %w(from to cc bcc subject reply_to in_reply_to).each do |field|
131
140
  send field, encrypted_mail.send(field)
132
141
  end
@@ -136,6 +145,7 @@ module Mail
136
145
  encrypted_mail.header.fields.each do |field|
137
146
  header[field.name] = field.value if field.name =~ /^X-/ && header[field.name].nil?
138
147
  end
148
+ verify_result decrypted.verify_result if options[:verify]
139
149
  end
140
150
  end
141
151
 
@@ -144,15 +154,49 @@ module Mail
144
154
  InlineDecryptedMessage.new(encrypted_mail, options)
145
155
  end
146
156
 
157
+ def self.verify(signed_mail, options = {})
158
+ if signed_mime?(signed_mail)
159
+ Mail::Gpg::MimeSignedMessage.new signed_mail, options
160
+ elsif signed_inline?(signed_mail)
161
+ Mail::Gpg::InlineSignedMessage.new signed_mail, options
162
+ else
163
+ signed_mail
164
+ end
165
+ end
166
+
147
167
  # check signature for PGP/MIME (RFC 3156, section 5) signed mail
148
168
  def self.signature_valid_pgp_mime?(signed_mail, options)
149
169
  # MUST contain exactly two body parts
150
170
  if signed_mail.parts.length != 2
151
171
  raise EncodingError, "RFC 3136 mandates exactly two body parts, found '#{signed_mail.parts.length}'"
152
172
  end
153
- SignPart.signature_valid?(signed_mail.parts[0], signed_mail.parts[1], options)
173
+ result, verify_result = SignPart.verify_signature(signed_mail.parts[0], signed_mail.parts[1], options)
174
+ signed_mail.verify_result = verify_result
175
+ return result
154
176
  end
155
177
 
178
+ # check signature for inline signed mail
179
+ def self.signature_valid_inline?(signed_mail, options)
180
+ result = nil
181
+ if signed_mail.multipart?
182
+ signed_mail.parts.each do |part|
183
+ if signed_inline?(part)
184
+ if result.nil?
185
+ result = true
186
+ signed_mail.verify_result = []
187
+ end
188
+ result &= signature_valid_inline?(part, options)
189
+ signed_mail.verify_result << part.verify_result
190
+ end
191
+ end
192
+ else
193
+ result, verify_result = GpgmeHelper.inline_verify(signed_mail.body.to_s, options)
194
+ signed_mail.verify_result = verify_result
195
+ end
196
+ return result
197
+ end
198
+
199
+
156
200
  # check if PGP/MIME encrypted (RFC 3156)
157
201
  def self.encrypted_mime?(mail)
158
202
  mail.has_content_type? &&
@@ -1,6 +1,7 @@
1
+ require 'mail/gpg/verified_part'
1
2
  module Mail
2
3
  module Gpg
3
- class DecryptedPart < Mail::Part
4
+ class DecryptedPart < VerifiedPart
4
5
 
5
6
  # options are:
6
7
  #
@@ -11,6 +12,7 @@ module Mail
11
12
  end
12
13
 
13
14
  decrypted = GpgmeHelper.decrypt(cipher_part.body.decoded, options)
15
+ self.verify_result = decrypted.verify_result if options[:verify]
14
16
  super(decrypted)
15
17
  end
16
18
  end
@@ -0,0 +1,9 @@
1
+ require 'gpgme'
2
+ require 'mail/gpg/verify_result_attribute'
3
+
4
+ # extend GPGME::Data with an attribute to hold the result of signature
5
+ # verifications
6
+ class GPGME::Data
7
+ include Mail::Gpg::VerifyResultAttribute
8
+ end
9
+
@@ -1,3 +1,5 @@
1
+ require 'mail/gpg/gpgme_ext'
2
+
1
3
  # GPGME methods for encryption/decryption/signing
2
4
  module Mail
3
5
  module Gpg
@@ -11,6 +13,10 @@ module Mail
11
13
 
12
14
  recipient_keys = keys_for_data options[:recipients], options.delete(:keys)
13
15
 
16
+ if recipient_keys.empty?
17
+ raise MissingKeysError.new('No keys to encrypt to!')
18
+ end
19
+
14
20
  flags = 0
15
21
  flags |= GPGME::ENCRYPT_ALWAYS_TRUST if options[:always_trust]
16
22
 
@@ -46,6 +52,7 @@ module Mail
46
52
  begin
47
53
  if options[:verify]
48
54
  ctx.decrypt_verify(cipher_data, plain_data)
55
+ plain_data.verify_result = ctx.verify_result
49
56
  else
50
57
  ctx.decrypt(cipher_data, plain_data)
51
58
  end
@@ -72,13 +79,36 @@ module Mail
72
79
  crypto.sign GPGME::Data.new(plain), options
73
80
  end
74
81
 
82
+ # returns [success(bool), VerifyResult(from gpgme)]
83
+ # success will be true when there is at least one sig and no invalid sig
75
84
  def self.sign_verify(plain, signature, options = {})
76
- signed = false
77
- GPGME::Crypto.new.verify(signature, signed_text: plain) do |sig|
78
- return false if !sig.valid? # just one invalid signature leads to false
79
- signed = true
85
+ signed_data = GPGME::Data.new(plain)
86
+ signature = GPGME::Data.new(signature)
87
+
88
+ success = verify_result = nil
89
+ GPGME::Ctx.new(options) do |ctx|
90
+ ctx.verify signature, signed_data, nil
91
+ verify_result = ctx.verify_result
92
+ signatures = verify_result.signatures
93
+ success = signatures &&
94
+ signatures.size > 0 &&
95
+ signatures.detect{|s| !s.valid? }.nil?
96
+ end
97
+ return [success, verify_result]
98
+ end
99
+
100
+ def self.inline_verify(signed_text, options = {})
101
+ signed_data = GPGME::Data.new(signed_text)
102
+ success = verify_result = nil
103
+ GPGME::Ctx.new(options) do |ctx|
104
+ ctx.verify signed_data, nil
105
+ verify_result = ctx.verify_result
106
+ signatures = verify_result.signatures
107
+ success = signatures &&
108
+ signatures.size > 0 &&
109
+ signatures.detect{|s| !s.valid? }.nil?
80
110
  end
81
- return signed
111
+ return [success, verify_result]
82
112
  end
83
113
 
84
114
  private
@@ -1,3 +1,5 @@
1
+ require 'mail/gpg/verified_part'
2
+
1
3
  # decryption of the so called 'PGP-Inline' message types
2
4
  # this is not a standard, so the implementation is based on the notes
3
5
  # here http://binblog.info/2008/03/12/know-your-pgp-implementation/
@@ -17,11 +19,12 @@ module Mail
17
19
  header[field.name] = field.value
18
20
  end
19
21
  cipher_mail.parts.each do |part|
20
- part Mail::Part.new do |p|
22
+ p = VerifiedPart.new do |p|
21
23
  if part.has_content_type? && /application\/(?:octet-stream|pgp-encrypted)/ =~ part.mime_type
22
24
  # encrypted attachment, we set the content_type to the generic 'application/octet-stream'
23
25
  # and remove the .pgp/gpg/asc from name/filename in header fields
24
26
  decrypted = GpgmeHelper.decrypt(part.decoded, options)
27
+ p.verify_result decrypted.verify_result if options[:verify]
25
28
  p.content_type part.content_type.sub(/application\/(?:octet-stream|pgp-encrypted)/, 'application/octet-stream')
26
29
  .sub(/name=(?:"')?(.*)\.(?:pgp|gpg|asc)(?:"')?/, 'name="\1"')
27
30
  p.content_disposition part.content_disposition.sub(/filename=(?:"')?(.*)\.(?:pgp|gpg|asc)(?:"')?/, 'filename="\1"')
@@ -29,7 +32,9 @@ module Mail
29
32
  p.body Mail::Encodings::Base64::encode(decrypted.to_s)
30
33
  else
31
34
  if part.body.include?('-----BEGIN PGP MESSAGE-----')
32
- p.body GpgmeHelper.decrypt(part.decoded, options).to_s
35
+ decrypted = GpgmeHelper.decrypt(part.decoded, options)
36
+ p.verify_result decrypted.verify_result if options[:verify]
37
+ p.body decrypted.to_s
33
38
  else
34
39
  p.content_type part.content_type
35
40
  p.content_transfer_encoding part.content_transfer_encoding
@@ -37,6 +42,7 @@ module Mail
37
42
  end
38
43
  end
39
44
  end
45
+ add_part p
40
46
  end
41
47
  end # of multipart
42
48
  else
@@ -46,6 +52,7 @@ module Mail
46
52
  header[field.name] = field.value
47
53
  end
48
54
  body decrypted.to_s
55
+ verify_result decrypted.verify_result if options[:verify] && '' != decrypted
49
56
  end
50
57
  end
51
58
  end
@@ -0,0 +1,72 @@
1
+ require 'mail/gpg/verified_part'
2
+
3
+ module Mail
4
+ module Gpg
5
+ class InlineSignedMessage < Mail::Message
6
+
7
+ def initialize(signed_mail, options = {})
8
+ if signed_mail.multipart?
9
+ super() do
10
+ global_verify_result = []
11
+ signed_mail.header.fields.each do |field|
12
+ header[field.name] = field.value
13
+ end
14
+ signed_mail.parts.each do |part|
15
+ if Mail::Gpg.signed_inline?(part)
16
+ signed_text = part.body.to_s
17
+ success, vr = GpgmeHelper.inline_verify(signed_text, options)
18
+ p = VerifiedPart.new(part)
19
+ if success
20
+ p.body self.class.strip_inline_signature signed_text
21
+ end
22
+ p.verify_result vr
23
+ global_verify_result << vr
24
+ add_part p
25
+ else
26
+ add_part part
27
+ end
28
+ end
29
+ verify_result global_verify_result
30
+ end # of multipart
31
+ else
32
+ super() do
33
+ signed_mail.header.fields.each do |field|
34
+ header[field.name] = field.value
35
+ end
36
+ signed_text = signed_mail.body.to_s
37
+ success, vr = GpgmeHelper.inline_verify(signed_text, options)
38
+ if success
39
+ body self.class.strip_inline_signature signed_text
40
+ else
41
+ body signed_text
42
+ end
43
+ verify_result vr
44
+ end
45
+ end
46
+ end
47
+
48
+ END_SIGNED_TEXT = '-----END PGP SIGNED MESSAGE-----'
49
+ END_SIGNED_TEXT_RE = /^#{END_SIGNED_TEXT}\s*$/
50
+ INLINE_SIG_RE = Regexp.new('^-----BEGIN PGP SIGNATURE-----\s*$.*^-----END PGP SIGNATURE-----\s*$', Regexp::MULTILINE)
51
+ BEGIN_SIG_RE = /^(-----BEGIN PGP SIGNATURE-----)\s*$/
52
+
53
+
54
+ # utility method to remove inline signature and related pgp markers
55
+ def self.strip_inline_signature(signed_text)
56
+ if signed_text =~ INLINE_SIG_RE
57
+ signed_text = signed_text.dup
58
+ if signed_text !~ END_SIGNED_TEXT_RE
59
+ # insert the 'end of signed text' marker in case it is missing
60
+ signed_text = signed_text.gsub BEGIN_SIG_RE, "-----END PGP SIGNED MESSAGE-----\n\\1"
61
+ end
62
+ signed_text.gsub! INLINE_SIG_RE, ''
63
+ signed_text.strip!
64
+ end
65
+ signed_text
66
+ end
67
+
68
+ end
69
+ end
70
+ end
71
+
72
+
@@ -1,4 +1,5 @@
1
1
  require 'mail/gpg/delivery_handler'
2
+ require 'mail/gpg/verify_result_attribute'
2
3
 
3
4
  module Mail
4
5
  module Gpg
@@ -7,6 +8,7 @@ module Mail
7
8
  def self.included(base)
8
9
  base.class_eval do
9
10
  attr_accessor :raise_encryption_errors
11
+ include VerifyResultAttribute
10
12
  end
11
13
  end
12
14
 
@@ -51,21 +53,35 @@ module Mail
51
53
  end
52
54
  end
53
55
 
56
+ # true if this mail is encrypted
54
57
  def encrypted?
55
58
  Mail::Gpg.encrypted?(self)
56
59
  end
57
60
 
61
+ # returns the decrypted mail object.
62
+ #
63
+ # pass verify: true to verify signatures as well. The gpgme verification
64
+ # result will be available via decrypted_mail.verify_result
58
65
  def decrypt(options = {})
59
66
  Mail::Gpg.decrypt(self, options)
60
67
  end
61
68
 
69
+ # true if this mail is signed (but not encrypted)
62
70
  def signed?
63
71
  Mail::Gpg.signed?(self)
64
72
  end
65
73
 
66
- def signature_valid?(options = {})
67
- Mail::Gpg.signature_valid?(self, options)
74
+ # verify signatures. returns a new mail object with signatures removed and
75
+ # populated verify_result.
76
+ #
77
+ # verified = signed_mail.verify()
78
+ # verified.signature_valid?
79
+ # signers = mail.signatures.map{|sig| sig.from}
80
+ def verify(options = {})
81
+ Mail::Gpg.verify(self, options)
68
82
  end
83
+
84
+
69
85
  end
70
86
  end
71
87
  end
@@ -0,0 +1,30 @@
1
+ require 'mail/gpg/verified_part'
2
+
3
+ module Mail
4
+ module Gpg
5
+ class MimeSignedMessage < Mail::Message
6
+
7
+ def initialize(signed_mail, options = {})
8
+ content_part, signature = signed_mail.parts
9
+ success, vr = SignPart.verify_signature(content_part, signature, options)
10
+ super() do
11
+ verify_result vr
12
+ signed_mail.header.fields.each do |field|
13
+ header[field.name] = field.value
14
+ end
15
+ content_part.header.fields.each do |field|
16
+ header[field.name] = field.value
17
+ end
18
+ if content_part.multipart?
19
+ content_part.parts.each{|part| add_part part}
20
+ else
21
+ body content_part.body.to_s
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+
30
+
@@ -0,0 +1,6 @@
1
+ module Mail
2
+ module Gpg
3
+ class MissingKeysError < StandardError
4
+ end
5
+ end
6
+ end
@@ -12,7 +12,13 @@ module Mail
12
12
  end
13
13
  end
14
14
 
15
+ # true if all signatures are valid
15
16
  def self.signature_valid?(plain_part, signature_part, options = {})
17
+ verify_signature(plain_part, signature_part, options)[0]
18
+ end
19
+
20
+ # will return [success(boolean), verify_result(as returned by gpgme)]
21
+ def self.verify_signature(plain_part, signature_part, options = {})
16
22
  if !(signature_part.has_content_type? &&
17
23
  ('application/pgp-signature' == signature_part.mime_type))
18
24
  return false
@@ -0,0 +1,10 @@
1
+ require 'mail/gpg/verify_result_attribute'
2
+
3
+ module Mail
4
+ module Gpg
5
+ class VerifiedPart < Mail::Part
6
+ include VerifyResultAttribute
7
+ end
8
+ end
9
+ end
10
+
@@ -0,0 +1,31 @@
1
+ module Mail
2
+ module Gpg
3
+ module VerifyResultAttribute
4
+
5
+ # the result of signature verification, as provided by GPGME
6
+ def verify_result(result = nil)
7
+ if result
8
+ self.verify_result = result
9
+ else
10
+ @verify_result
11
+ end
12
+ end
13
+ def verify_result=(result)
14
+ @verify_result = result
15
+ end
16
+
17
+ # checks validity of signatures (true / false)
18
+ def signature_valid?
19
+ sigs = self.signatures
20
+ sigs.any? && sigs.detect{|s|!s.valid?}.blank?
21
+ end
22
+
23
+ # list of all signatures from verify_result
24
+ def signatures
25
+ [verify_result].flatten.compact.map do |vr|
26
+ vr.signatures
27
+ end.flatten.compact
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,5 +1,5 @@
1
1
  module Mail
2
2
  module Gpg
3
- VERSION = "0.1.7"
3
+ VERSION = "0.2.1"
4
4
  end
5
5
  end
@@ -26,6 +26,10 @@ class DecryptedPartTest < Test::Unit::TestCase
26
26
  assert mail == @mail
27
27
  assert mail.message_id == @mail.message_id
28
28
  assert mail.message_id != @part.message_id
29
+ assert vr = mail.verify_result
30
+ assert sig = vr.signatures.first
31
+ assert sig.to_s=~ /Joe/
32
+ assert sig.valid?
29
33
  end
30
34
 
31
35
  should 'raise encoding error for non gpg mime type' do
@@ -33,5 +37,7 @@ class DecryptedPartTest < Test::Unit::TestCase
33
37
  part.content_type = 'text/plain'
34
38
  assert_raise(EncodingError) { Mail::Gpg::DecryptedPart.new(part) }
35
39
  end
40
+
41
+
36
42
  end
37
43
  end
data/test/gpg_test.rb CHANGED
@@ -36,6 +36,11 @@ class GpgTest < Test::Unit::TestCase
36
36
  assert sig.valid?
37
37
  end
38
38
  assert Mail::Gpg.signature_valid?(signed)
39
+ assert verified = signed.verify
40
+ assert verified.verify_result.present?
41
+ assert verified.verify_result.signatures.any?
42
+ assert verified.signatures.any?
43
+ assert verified.signature_valid?
39
44
  end
40
45
 
41
46
  def check_mime_structure_signed(mail = @mail, signed = @signed)
@@ -250,6 +255,10 @@ class GpgTest < Test::Unit::TestCase
250
255
  should 'decrypt and verify' do
251
256
  assert mail = Mail::Gpg.decrypt(@encrypted, { :verify => true, :password => 'abc' })
252
257
  assert mail == @mail
258
+ assert mail.verify_result
259
+ assert sig = mail.signatures.first
260
+ assert sig.to_s =~ /Joe/
261
+ assert sig.valid?
253
262
  end
254
263
  end
255
264
 
@@ -330,7 +339,7 @@ class GpgTest < Test::Unit::TestCase
330
339
  context 'multipart mail' do
331
340
  setup do
332
341
  @mail.add_file 'Rakefile'
333
- @encrypted = Mail::Gpg.encrypt(@mail)
342
+ @encrypted = Mail::Gpg.encrypt(@mail, sign: true, password: 'abc')
334
343
  end
335
344
 
336
345
  should 'have same recipients and subject' do
@@ -355,10 +364,16 @@ class GpgTest < Test::Unit::TestCase
355
364
  assert_match /Rakefile/, m.parts.last.content_disposition
356
365
  end
357
366
 
358
- should 'decrypt' do
359
- assert mail = Mail::Gpg.decrypt(@encrypted, { :password => 'abc' })
367
+ should 'decrypt and verify' do
368
+ assert mail = Mail::Gpg.decrypt(@encrypted, { :verify => true, :password => 'abc' })
360
369
  assert mail == @mail
361
370
  assert mail.parts[1] == @mail.parts[1]
371
+ assert mail.verify_result
372
+ assert signatures = mail.signatures
373
+ assert_equal 1, signatures.size
374
+ assert sig = signatures[0]
375
+ assert sig.to_s =~ /Joe/
376
+ assert sig.valid?
362
377
  end
363
378
  end
364
379
  end
@@ -17,15 +17,19 @@ class InlineDecryptedMessageTest < Test::Unit::TestCase
17
17
  end
18
18
 
19
19
  context "inline message" do
20
- should "decrypt body" do
20
+ should "decrypt and verify body" do
21
21
  mail = Mail.new(@mail)
22
22
  mail.body = InlineDecryptedMessageTest.encrypt(mail, mail.body.to_s)
23
23
 
24
24
  assert !mail.multipart?
25
25
  assert mail.encrypted?
26
- assert decrypted = mail.decrypt(:password => 'abc')
26
+ assert decrypted = mail.decrypt(:password => 'abc', verify: true)
27
27
  assert decrypted == @mail
28
28
  assert !decrypted.encrypted?
29
+ assert vr = decrypted.verify_result
30
+ assert sig = vr.signatures.first
31
+ assert sig.to_s=~ /Joe/
32
+ assert sig.valid?
29
33
  end
30
34
  end
31
35
 
@@ -55,7 +59,7 @@ class InlineDecryptedMessageTest < Test::Unit::TestCase
55
59
  end
56
60
 
57
61
  context "cleartext body and encrypted attachment message" do
58
- should "decrypt attachment" do
62
+ should "decrypt and verify attachment" do
59
63
  rakefile = File.open('Rakefile') { |file| file.read }
60
64
  mail = Mail.new(@mail)
61
65
  mail.content_type = 'multipart/mixed'
@@ -68,7 +72,7 @@ class InlineDecryptedMessageTest < Test::Unit::TestCase
68
72
 
69
73
  assert mail.multipart?
70
74
  assert mail.encrypted?
71
- assert decrypted = mail.decrypt(:password => 'abc')
75
+ assert decrypted = mail.decrypt(password: 'abc', verify: true)
72
76
  assert !decrypted.encrypted?
73
77
  check_headers(@mail, decrypted)
74
78
  assert_equal 2, decrypted.parts.length
@@ -76,11 +80,17 @@ class InlineDecryptedMessageTest < Test::Unit::TestCase
76
80
  assert /application\/octet-stream; (?:charset=UTF-8; )?name=Rakefile/ =~ decrypted.parts[1].content_type
77
81
  assert_equal 'attachment; filename=Rakefile', decrypted.parts[1].content_disposition
78
82
  assert_equal rakefile, decrypted.parts[1].body.decoded
83
+
84
+ assert_nil decrypted.parts[0].verify_result
85
+ assert vr = decrypted.parts[1].verify_result
86
+ assert sig = vr.signatures.first
87
+ assert sig.to_s=~ /Joe/
88
+ assert sig.valid?
79
89
  end
80
90
  end
81
91
 
82
92
  context "encrypted body and attachment message" do
83
- should "decrypt" do
93
+ should "decrypt and verify" do
84
94
  rakefile = File.open('Rakefile') { |file| file.read }
85
95
  mail = Mail.new(@mail)
86
96
  mail.content_type = 'multipart/mixed'
@@ -94,7 +104,7 @@ class InlineDecryptedMessageTest < Test::Unit::TestCase
94
104
 
95
105
  assert mail.multipart?
96
106
  assert mail.encrypted?
97
- assert decrypted = mail.decrypt(:password => 'abc')
107
+ assert decrypted = mail.decrypt(password: 'abc', verify: true)
98
108
  assert !decrypted.encrypted?
99
109
  check_headers(@mail, decrypted)
100
110
  assert_equal 2, decrypted.parts.length
@@ -102,6 +112,12 @@ class InlineDecryptedMessageTest < Test::Unit::TestCase
102
112
  assert /application\/octet-stream; (?:charset=UTF-8; )?name=Rakefile/ =~ decrypted.parts[1].content_type
103
113
  assert_equal 'attachment; filename=Rakefile', decrypted.parts[1].content_disposition
104
114
  assert_equal rakefile, decrypted.parts[1].body.decoded
115
+ decrypted.parts.each do |part|
116
+ assert vr = part.verify_result
117
+ assert sig = vr.signatures.first
118
+ assert sig.to_s=~ /Joe/
119
+ assert sig.valid?
120
+ end
105
121
  end
106
122
  end
107
123
  end
@@ -0,0 +1,121 @@
1
+ require 'test_helper'
2
+
3
+ # test cases for PGP inline signed messages (i.e. non-mime)
4
+ class InlineSignedMessageTest < Test::Unit::TestCase
5
+
6
+ context "InlineSignedMessage" do
7
+
8
+ setup do
9
+ (@mails = Mail::TestMailer.deliveries).clear
10
+ @mail = Mail.new do
11
+ to 'jane@foo.bar'
12
+ from 'joe@foo.bar'
13
+ subject 'test'
14
+ body 'i am unencrypted'
15
+ end
16
+ end
17
+
18
+ context 'strip_inline_signature' do
19
+ should 'strip signature from signed text' do
20
+ body = self.class.inline_sign(@mail, 'i am signed')
21
+ assert stripped_body = Mail::Gpg::InlineSignedMessage.strip_inline_signature(body)
22
+ assert_equal "-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA1\n\ni am signed\n-----END PGP SIGNED MESSAGE-----", stripped_body
23
+ end
24
+
25
+ should 'not change unsigned text' do
26
+ assert stripped_body = Mail::Gpg::InlineSignedMessage.strip_inline_signature("foo\nbar\n")
27
+ assert_equal "foo\nbar\n", stripped_body
28
+ end
29
+ end
30
+
31
+ context "signed message" do
32
+ should "verify body" do
33
+ mail = Mail.new(@mail)
34
+ mail.body = self.class.inline_sign(mail, mail.body.to_s)
35
+ assert !mail.multipart?
36
+ assert mail.signed?
37
+ assert verified = mail.verify
38
+ assert verified.signature_valid?
39
+ assert sig = verified.signatures.first
40
+ assert sig.to_s=~ /Joe/
41
+ assert sig.valid?
42
+ end
43
+
44
+ should "detect invalid sig" do
45
+ mail = Mail.new(@mail)
46
+ mail.body = self.class.inline_sign(mail, mail.body.to_s).gsub /i am/, 'i was'
47
+ assert !mail.multipart?
48
+ assert mail.signed?
49
+ assert verified = mail.verify
50
+ assert !verified.signature_valid?
51
+ assert vr = verified.verify_result
52
+ assert sig = verified.signatures.first
53
+ assert sig.to_s=~ /Joe/
54
+ assert !sig.valid?
55
+ end
56
+
57
+ end
58
+
59
+ context "message with signed attachment" do
60
+ should "check attachment signature" do
61
+ mail = Mail.new(@mail)
62
+ mail.body = 'foobar'
63
+ mail.part do |p|
64
+ p.body = self.class.inline_sign(mail, 'sign me!')
65
+ end
66
+ assert mail.multipart?
67
+ assert mail.signed?
68
+ assert verified = mail.verify
69
+ assert verified.signature_valid?
70
+ assert vr = verified.parts.last.verify_result
71
+ assert !verified.parts.first.signed?
72
+ assert verified.parts.last.signed?
73
+ assert Mail::Gpg.signed_inline?(verified.parts.last)
74
+ assert_equal [vr], verified.verify_result
75
+ assert sig = verified.signatures.first
76
+ assert sig.to_s=~ /Joe/
77
+ assert sig.valid?
78
+ end
79
+
80
+ should "detect invalid sig" do
81
+ mail = Mail.new(@mail)
82
+ mail.body = 'foobar'
83
+ mail.part do |p|
84
+ p.body = self.class.inline_sign(mail, 'i am signed!').gsub /i am/, 'i was'
85
+ end
86
+ mail.part do |p|
87
+ p.body = self.class.inline_sign(mail, 'i am signed!')
88
+ end
89
+
90
+ assert mail.multipart?
91
+ assert mail.signed?
92
+ assert verified = mail.verify
93
+ assert !verified.signature_valid?
94
+ assert vr = verified.verify_result
95
+ assert_equal 2, vr.size
96
+
97
+ invalid = verified.parts[1]
98
+ assert !invalid.signature_valid?
99
+ assert sig = invalid.verify_result.signatures.first
100
+ assert sig.to_s=~ /Joe/
101
+ assert !sig.valid?
102
+
103
+ valid = verified.parts[2]
104
+ assert valid.signature_valid?
105
+ assert sig = valid.verify_result.signatures.first
106
+ assert sig.to_s=~ /Joe/
107
+ assert sig.valid?
108
+ end
109
+ end
110
+ end
111
+
112
+
113
+ def self.inline_sign(mail, plain, armor = true)
114
+ GPGME::Crypto.new.clearsign(plain,
115
+ password: 'abc',
116
+ signers: mail.from,
117
+ armor: armor).to_s
118
+ end
119
+
120
+ end
121
+
data/test/message_test.rb CHANGED
@@ -42,6 +42,35 @@ class MessageTest < Test::Unit::TestCase
42
42
  @mail.gpg sign: true, password: 'abc'
43
43
  end
44
44
 
45
+ context 'with multiple parts' do
46
+ setup do
47
+ p = Mail::Part.new do
48
+ body 'and another part'
49
+ end
50
+ @mail.add_part p
51
+ p = Mail::Part.new do
52
+ body 'and a third part'
53
+ end
54
+ @mail.add_part p
55
+
56
+ @mail.deliver
57
+ @signed = @mails.first
58
+ @verified = @signed.verify
59
+ end
60
+
61
+ should 'verify signature' do
62
+ assert @verified.signature_valid?
63
+ end
64
+
65
+ should 'have original three parts' do
66
+ assert_equal 3, @mail.parts.size
67
+ assert_equal 3, @verified.parts.size
68
+ assert_equal 'i am unencrypted', @verified.parts[0].body.to_s
69
+ assert_equal 'and another part', @verified.parts[1].body.to_s
70
+ assert_equal 'and a third part', @verified.parts[2].body.to_s
71
+ end
72
+ end
73
+
45
74
  context "" do
46
75
  setup do
47
76
  @mail.header['Auto-Submitted'] = 'foo'
@@ -59,14 +88,18 @@ class MessageTest < Test::Unit::TestCase
59
88
  assert !m.encrypted?
60
89
  assert m.signed?
61
90
  assert m.multipart?
62
- assert m.signature_valid?
63
91
  assert sign_part = m.parts.last
64
- #assert m = Mail::Message.new(m.parts.first)
65
- #assert !m.multipart?
66
92
  GPGME::Crypto.new.verify(sign_part.body.to_s, signed_text: m.parts.first.encoded) do |sig|
67
93
  assert sig.valid?
68
94
  end
69
- assert_equal 'i am unencrypted', m.parts.first.body.to_s
95
+ end
96
+
97
+ should 'verify signed mail' do
98
+ assert m = @mails.first
99
+ assert verified = m.verify
100
+ assert verified.signature_valid?
101
+ assert !verified.multipart?
102
+ assert_equal 'i am unencrypted', verified.body.to_s
70
103
  end
71
104
 
72
105
  should "fail signature on tampered body" do
@@ -76,13 +109,36 @@ class MessageTest < Test::Unit::TestCase
76
109
  assert !m.encrypted?
77
110
  assert m.signed?
78
111
  assert m.multipart?
79
- assert m.signature_valid?
112
+ assert verified = m.verify
113
+ assert verified.signature_valid?
80
114
  m.parts.first.body = 'replaced body'
81
- assert !m.signature_valid?
115
+ assert verified = m.verify
116
+ assert !verified.signature_valid?
82
117
  end
83
118
  end
84
119
  end
85
120
 
121
+ context 'with encryption and signing' do
122
+ setup do
123
+ @mail.gpg encrypt: true, sign: true, password: 'abc'
124
+ @mail.deliver
125
+ end
126
+
127
+ should 'decrypt and check signature' do
128
+ assert_equal 1, @mails.size
129
+ assert m = @mails.first
130
+ assert_equal 'test', m.subject
131
+ assert m.multipart?
132
+ assert m.encrypted?
133
+ assert decrypted = m.decrypt(:password => 'abc', verify: true)
134
+ assert_equal 'test', decrypted.subject
135
+ assert decrypted == @mail
136
+ assert_equal 'i am unencrypted', decrypted.body.to_s
137
+ assert decrypted.signature_valid?
138
+ assert_equal 1, decrypted.signatures.size
139
+ end
140
+ end
141
+
86
142
  context "with gpg turned on" do
87
143
  setup do
88
144
  @mail.gpg encrypt: true
@@ -94,7 +150,7 @@ class MessageTest < Test::Unit::TestCase
94
150
  end
95
151
 
96
152
  should "raise encryption error" do
97
- assert_raises(GPGME::Error::InvalidValue){
153
+ assert_raises(Mail::Gpg::MissingKeysError){
98
154
  @mail.deliver
99
155
  }
100
156
  end
@@ -106,6 +162,7 @@ class MessageTest < Test::Unit::TestCase
106
162
  end
107
163
  end
108
164
 
165
+
109
166
  context "" do
110
167
  setup do
111
168
  @mail.deliver
metadata CHANGED
@@ -1,64 +1,58 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mail-gpg
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
5
- prerelease:
4
+ version: 0.2.1
6
5
  platform: ruby
7
6
  authors:
8
7
  - Jens Kraemer
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2014-06-03 00:00:00.000000000 Z
11
+ date: 2014-07-23 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: mail
16
15
  requirement: !ruby/object:Gem::Requirement
17
- none: false
18
16
  requirements:
19
17
  - - ~>
20
18
  - !ruby/object:Gem::Version
21
19
  version: '2.5'
22
- - - ! '>='
20
+ - - '>='
23
21
  - !ruby/object:Gem::Version
24
22
  version: 2.5.3
25
23
  type: :runtime
26
24
  prerelease: false
27
25
  version_requirements: !ruby/object:Gem::Requirement
28
- none: false
29
26
  requirements:
30
27
  - - ~>
31
28
  - !ruby/object:Gem::Version
32
29
  version: '2.5'
33
- - - ! '>='
30
+ - - '>='
34
31
  - !ruby/object:Gem::Version
35
32
  version: 2.5.3
36
33
  - !ruby/object:Gem::Dependency
37
34
  name: gpgme
38
35
  requirement: !ruby/object:Gem::Requirement
39
- none: false
40
36
  requirements:
41
37
  - - ~>
42
38
  - !ruby/object:Gem::Version
43
39
  version: '2.0'
44
- - - ! '>='
40
+ - - '>='
45
41
  - !ruby/object:Gem::Version
46
42
  version: 2.0.2
47
43
  type: :runtime
48
44
  prerelease: false
49
45
  version_requirements: !ruby/object:Gem::Requirement
50
- none: false
51
46
  requirements:
52
47
  - - ~>
53
48
  - !ruby/object:Gem::Version
54
49
  version: '2.0'
55
- - - ! '>='
50
+ - - '>='
56
51
  - !ruby/object:Gem::Version
57
52
  version: 2.0.2
58
53
  - !ruby/object:Gem::Dependency
59
54
  name: bundler
60
55
  requirement: !ruby/object:Gem::Requirement
61
- none: false
62
56
  requirements:
63
57
  - - ~>
64
58
  - !ruby/object:Gem::Version
@@ -66,7 +60,6 @@ dependencies:
66
60
  type: :development
67
61
  prerelease: false
68
62
  version_requirements: !ruby/object:Gem::Requirement
69
- none: false
70
63
  requirements:
71
64
  - - ~>
72
65
  - !ruby/object:Gem::Version
@@ -74,7 +67,6 @@ dependencies:
74
67
  - !ruby/object:Gem::Dependency
75
68
  name: minitest
76
69
  requirement: !ruby/object:Gem::Requirement
77
- none: false
78
70
  requirements:
79
71
  - - ~>
80
72
  - !ruby/object:Gem::Version
@@ -82,7 +74,6 @@ dependencies:
82
74
  type: :development
83
75
  prerelease: false
84
76
  version_requirements: !ruby/object:Gem::Requirement
85
- none: false
86
77
  requirements:
87
78
  - - ~>
88
79
  - !ruby/object:Gem::Version
@@ -90,55 +81,48 @@ dependencies:
90
81
  - !ruby/object:Gem::Dependency
91
82
  name: rake
92
83
  requirement: !ruby/object:Gem::Requirement
93
- none: false
94
84
  requirements:
95
- - - ! '>='
85
+ - - '>='
96
86
  - !ruby/object:Gem::Version
97
87
  version: '0'
98
88
  type: :development
99
89
  prerelease: false
100
90
  version_requirements: !ruby/object:Gem::Requirement
101
- none: false
102
91
  requirements:
103
- - - ! '>='
92
+ - - '>='
104
93
  - !ruby/object:Gem::Version
105
94
  version: '0'
106
95
  - !ruby/object:Gem::Dependency
107
96
  name: actionmailer
108
97
  requirement: !ruby/object:Gem::Requirement
109
- none: false
110
98
  requirements:
111
- - - ! '>='
99
+ - - '>='
112
100
  - !ruby/object:Gem::Version
113
101
  version: 3.2.0
114
102
  type: :development
115
103
  prerelease: false
116
104
  version_requirements: !ruby/object:Gem::Requirement
117
- none: false
118
105
  requirements:
119
- - - ! '>='
106
+ - - '>='
120
107
  - !ruby/object:Gem::Version
121
108
  version: 3.2.0
122
109
  - !ruby/object:Gem::Dependency
123
110
  name: pry-nav
124
111
  requirement: !ruby/object:Gem::Requirement
125
- none: false
126
112
  requirements:
127
- - - ! '>='
113
+ - - '>='
128
114
  - !ruby/object:Gem::Version
129
115
  version: '0'
130
116
  type: :development
131
117
  prerelease: false
132
118
  version_requirements: !ruby/object:Gem::Requirement
133
- none: false
134
119
  requirements:
135
- - - ! '>='
120
+ - - '>='
136
121
  - !ruby/object:Gem::Version
137
122
  version: '0'
138
123
  - !ruby/object:Gem::Dependency
139
124
  name: shoulda-context
140
125
  requirement: !ruby/object:Gem::Requirement
141
- none: false
142
126
  requirements:
143
127
  - - ~>
144
128
  - !ruby/object:Gem::Version
@@ -146,15 +130,13 @@ dependencies:
146
130
  type: :development
147
131
  prerelease: false
148
132
  version_requirements: !ruby/object:Gem::Requirement
149
- none: false
150
133
  requirements:
151
134
  - - ~>
152
135
  - !ruby/object:Gem::Version
153
136
  version: '1.1'
154
- description: ! 'GPG/MIME encryption plugin for the Ruby Mail Library
155
-
156
- This tiny gem adds GPG capabilities to Mail::Message and ActionMailer::Base. Because
157
- privacy matters.'
137
+ description: |-
138
+ GPG/MIME encryption plugin for the Ruby Mail Library
139
+ This tiny gem adds GPG capabilities to Mail::Message and ActionMailer::Base. Because privacy matters.
158
140
  email:
159
141
  - jk@jkraemer.net
160
142
  executables: []
@@ -174,13 +156,19 @@ files:
174
156
  - lib/mail/gpg/decrypted_part.rb
175
157
  - lib/mail/gpg/delivery_handler.rb
176
158
  - lib/mail/gpg/encrypted_part.rb
159
+ - lib/mail/gpg/gpgme_ext.rb
177
160
  - lib/mail/gpg/gpgme_helper.rb
178
161
  - lib/mail/gpg/inline_decrypted_message.rb
162
+ - lib/mail/gpg/inline_signed_message.rb
179
163
  - lib/mail/gpg/message_patch.rb
164
+ - lib/mail/gpg/mime_signed_message.rb
165
+ - lib/mail/gpg/missing_keys_error.rb
180
166
  - lib/mail/gpg/rails.rb
181
167
  - lib/mail/gpg/rails/action_mailer_base_patch.rb
182
168
  - lib/mail/gpg/sign_part.rb
183
169
  - lib/mail/gpg/signed_part.rb
170
+ - lib/mail/gpg/verified_part.rb
171
+ - lib/mail/gpg/verify_result_attribute.rb
184
172
  - lib/mail/gpg/version.rb
185
173
  - lib/mail/gpg/version_part.rb
186
174
  - mail-gpg.gemspec
@@ -195,6 +183,7 @@ files:
195
183
  - test/gpghome/trustdb.gpg
196
184
  - test/hkp_test.rb
197
185
  - test/inline_decrypted_message_test.rb
186
+ - test/inline_signed_message_test.rb
198
187
  - test/message_test.rb
199
188
  - test/sign_part_test.rb
200
189
  - test/test_helper.rb
@@ -202,27 +191,26 @@ files:
202
191
  homepage: https://github.com/jkraemer/mail-gpg
203
192
  licenses:
204
193
  - MIT
194
+ metadata: {}
205
195
  post_install_message:
206
196
  rdoc_options: []
207
197
  require_paths:
208
198
  - lib
209
199
  required_ruby_version: !ruby/object:Gem::Requirement
210
- none: false
211
200
  requirements:
212
- - - ! '>='
201
+ - - '>='
213
202
  - !ruby/object:Gem::Version
214
203
  version: '0'
215
204
  required_rubygems_version: !ruby/object:Gem::Requirement
216
- none: false
217
205
  requirements:
218
- - - ! '>='
206
+ - - '>='
219
207
  - !ruby/object:Gem::Version
220
208
  version: '0'
221
209
  requirements: []
222
210
  rubyforge_project:
223
- rubygems_version: 1.8.23.2
211
+ rubygems_version: 2.2.2
224
212
  signing_key:
225
- specification_version: 3
213
+ specification_version: 4
226
214
  summary: GPG/MIME encryption plugin for the Ruby Mail Library
227
215
  test_files:
228
216
  - test/action_mailer_test.rb
@@ -236,7 +224,9 @@ test_files:
236
224
  - test/gpghome/trustdb.gpg
237
225
  - test/hkp_test.rb
238
226
  - test/inline_decrypted_message_test.rb
227
+ - test/inline_signed_message_test.rb
239
228
  - test/message_test.rb
240
229
  - test/sign_part_test.rb
241
230
  - test/test_helper.rb
242
231
  - test/version_part_test.rb
232
+ has_rdoc: