mail-gpg 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,24 @@
1
+ == 0.1.3 2014-02-17
2
+
3
+ * Signature checking implemented, thanks to Morten Andersen
4
+
5
+ == 0.1.2 2013-11-19
6
+
7
+ * bugfix release
8
+
9
+ == 0.1.1 2013-11-14
10
+
11
+ * bugfix release
12
+
13
+ == 0.1.0 2013-11-06
14
+
15
+ * decryption support (thanks to Morten Andersen)
16
+ * sign-only operation (thanks to FewKinG)
17
+ * keyserver url lookup (thanks to FewKinG)
18
+
1
19
  == 0.0.6 2013-08-28
2
20
 
3
- * bugfix: onlyu encrypt to specified keys if :keys option is present
21
+ * bugfix: only encrypt to specified keys if :keys option is present
4
22
 
5
23
  == 0.0.5 2013-08-28
6
24
 
data/README.md CHANGED
@@ -96,13 +96,15 @@ if mail.encrypted?
96
96
  end
97
97
  ```
98
98
 
99
+ Set the `:verify` option to `true` when calling `decrypt` to decrypt *and* verify signatures.
100
+
99
101
  A `GPGME::Error::BadPassphrase` will be raised if the password for the private key is incorrect.
100
102
  A `EncodingError` will be raised if the encrypted mails is not encoded correctly as a [RFC 3156](http://www.ietf.org/rfc/rfc3156.txt) message.
101
103
 
102
104
 
103
105
  ### Signing only
104
106
 
105
- Just leave the the `:encrypt` option out or pass `encrypt: false`, i.e.
107
+ Just leave the `:encrypt` option out or pass `encrypt: false`, i.e.
106
108
 
107
109
 
108
110
  Mail.new do
@@ -110,6 +112,21 @@ Just leave the the `:encrypt` option out or pass `encrypt: false`, i.e.
110
112
  gpg sign: true
111
113
  end.deliver
112
114
 
115
+ ### Verify signature(s)
116
+
117
+ Receive the mail as usual. Check if it is signed using the `signed?` method. Check the signature of the mail with the `signature_valid?` method:
118
+
119
+ ```ruby
120
+ mail = Mail.first
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?}"
125
+ end
126
+ ```
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.
113
130
 
114
131
  ### Key import from public key servers
115
132
 
@@ -163,7 +180,7 @@ around with your personal gpg keychain.
163
180
 
164
181
  ## Todo
165
182
 
166
- * signature verification for received mails
183
+ * signature verification for received mails with inline PGP
167
184
  * on the fly import of recipients' keys from public key servers based on email address or key id
168
185
  * handle encryption errors due to missing keys - maybe return a list of failed
169
186
  recipients
@@ -47,14 +47,16 @@ module Mail
47
47
  def self.sign(cleartext_mail, options = {})
48
48
  construct_mail(cleartext_mail, options) do
49
49
  options[:sign_as] ||= cleartext_mail.from
50
- add_part SignPart.new(cleartext_mail, options)
51
50
  add_part Mail::Part.new(cleartext_mail)
51
+ add_part SignPart.new(cleartext_mail, options)
52
52
 
53
53
  content_type "multipart/signed; micalg=pgp-sha1; protocol=\"application/pgp-signature\"; boundary=#{boundary}"
54
54
  body.preamble = options[:preamble] || "This is an OpenPGP/MIME signed message (RFC 4880 and 3156)"
55
55
  end
56
56
  end
57
57
 
58
+ # options are:
59
+ # :verify: decrypt and verify
58
60
  def self.decrypt(encrypted_mail, options = {})
59
61
  if encrypted_mime?(encrypted_mail)
60
62
  decrypt_pgp_mime(encrypted_mail, options)
@@ -65,12 +67,33 @@ module Mail
65
67
  end
66
68
  end
67
69
 
70
+ def self.signature_valid?(signed_mail, options = {})
71
+ if signed_mime?(signed_mail)
72
+ signature_valid_pgp_mime?(signed_mail, options)
73
+ else
74
+ raise EncodingError, "Unsupported signature format '#{signed_mail.content_type}'"
75
+ end
76
+ end
77
+
78
+ # true if a mail is encrypted
68
79
  def self.encrypted?(mail)
69
80
  return true if encrypted_mime?(mail)
70
81
  return true if encrypted_inline?(mail)
71
82
  false
72
83
  end
73
84
 
85
+ # true if a mail is signed.
86
+ #
87
+ # throws EncodingError if called on an encrypted mail (so only call this method if encrypted? is false)
88
+ def self.signed?(mail)
89
+ return true if signed_mime?(mail)
90
+ return true if signed_inline?(mail)
91
+ if encrypted?(mail)
92
+ raise EncodingError, 'Unable to determine signature on an encrypted mail, use :verify option on decrypt()'
93
+ end
94
+ false
95
+ end
96
+
74
97
  private
75
98
 
76
99
  def self.construct_mail(cleartext_mail, options, &block)
@@ -113,7 +136,16 @@ module Mail
113
136
  InlineDecryptedMessage.new(encrypted_mail, options)
114
137
  end
115
138
 
116
- # check if PGP/MIME (RFC 3156)
139
+ # check signature for PGP/MIME (RFC 3156, section 5) signed mail
140
+ def self.signature_valid_pgp_mime?(signed_mail, options)
141
+ # MUST contain exactly two body parts
142
+ if signed_mail.parts.length != 2
143
+ raise EncodingError, "RFC 3136 mandates exactly two body parts, found '#{signed_mail.parts.length}'"
144
+ end
145
+ SignPart.signature_valid?(signed_mail.parts[0], signed_mail.parts[1], options)
146
+ end
147
+
148
+ # check if PGP/MIME encrypted (RFC 3156)
117
149
  def self.encrypted_mime?(mail)
118
150
  mail.has_content_type? &&
119
151
  'multipart/encrypted' == mail.mime_type &&
@@ -121,7 +153,7 @@ module Mail
121
153
  end
122
154
 
123
155
  # check if inline PGP (i.e. if any parts of the mail includes
124
- # the PGP MESSAGE marker
156
+ # the PGP MESSAGE marker)
125
157
  def self.encrypted_inline?(mail)
126
158
  return true if mail.body.include?('-----BEGIN PGP MESSAGE-----')
127
159
  if mail.multipart?
@@ -134,5 +166,24 @@ module Mail
134
166
  end
135
167
  false
136
168
  end
169
+
170
+ # check if PGP/MIME signed (RFC 3156)
171
+ def self.signed_mime?(mail)
172
+ mail.has_content_type? &&
173
+ 'multipart/signed' == mail.mime_type &&
174
+ 'application/pgp-signature' == mail.content_type_parameters[:protocol]
175
+ end
176
+
177
+ # check if inline PGP (i.e. if any parts of the mail includes
178
+ # the PGP SIGNED marker)
179
+ def self.signed_inline?(mail)
180
+ return true if mail.body.include?('-----BEGIN PGP SIGNED MESSAGE-----')
181
+ if mail.multipart?
182
+ mail.parts.each do |part|
183
+ return true if part.body.include?('-----BEGIN PGP SIGNED MESSAGE-----')
184
+ end
185
+ end
186
+ false
187
+ end
137
188
  end
138
189
  end
@@ -72,6 +72,15 @@ module Mail
72
72
  crypto.sign GPGME::Data.new(plain), options
73
73
  end
74
74
 
75
+ 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
80
+ end
81
+ return signed
82
+ end
83
+
75
84
  private
76
85
 
77
86
  # normalizes the list of recipients' emails, key ids and key data to a
@@ -59,6 +59,13 @@ module Mail
59
59
  Mail::Gpg.decrypt(self, options)
60
60
  end
61
61
 
62
+ def signed?
63
+ Mail::Gpg.signed?(self)
64
+ end
65
+
66
+ def signature_valid?(options = {})
67
+ Mail::Gpg.signature_valid?(self, options)
68
+ end
62
69
  end
63
70
  end
64
71
  end
@@ -12,6 +12,13 @@ module Mail
12
12
  end
13
13
  end
14
14
 
15
+ def self.signature_valid?(plain, signature, options = {})
16
+ if !(signature.has_content_type? && ('application/pgp-signature' == signature.mime_type))
17
+ return false
18
+ end
19
+
20
+ GpgmeHelper.sign_verify(plain.encoded, signature.body.encoded, options)
21
+ end
15
22
  end
16
23
  end
17
24
  end
@@ -1,5 +1,5 @@
1
1
  module Mail
2
2
  module Gpg
3
- VERSION = "0.1.2"
3
+ VERSION = "0.1.3"
4
4
  end
5
5
  end
@@ -29,15 +29,17 @@ class GpgTest < Test::Unit::TestCase
29
29
  end
30
30
 
31
31
  def check_signature(mail = @mail, signed = @signed)
32
+ assert signed.signed?
32
33
  assert signature = signed.parts.detect{|p| p.content_type =~ /signature\.asc/}.body.to_s
33
34
  GPGME::Crypto.new.verify(signature, signed_text: mail.encoded) do |sig|
34
35
  assert true == sig.valid?
35
36
  end
37
+ assert Mail::Gpg.signature_valid?(signed)
36
38
  end
37
39
 
38
40
  def check_mime_structure_signed(mail = @mail, signed = @signed)
39
41
  assert_equal 2, signed.parts.size
40
- sign_part, orig_part = signed.parts
42
+ orig_part, sign_part = signed.parts
41
43
 
42
44
  assert_equal 'application/pgp-signature; name=signature.asc', sign_part.content_type
43
45
  assert_equal orig_part.content_type, @mail.content_type
@@ -153,7 +155,7 @@ class GpgTest < Test::Unit::TestCase
153
155
  end
154
156
 
155
157
  should 'have multiple parts in original content' do
156
- assert original_part = @signed.parts.last
158
+ assert original_part = @signed.parts.first
157
159
  assert original_part.multipart?
158
160
  assert_equal 2, original_part.parts.size
159
161
  assert_match /sign me!/, original_part.parts.first.body.to_s
@@ -52,13 +52,28 @@ class MessageTest < Test::Unit::TestCase
52
52
  assert m = @mails.first
53
53
  assert_equal 'test', m.subject
54
54
  assert !m.encrypted?
55
+ assert m.signed?
55
56
  assert m.multipart?
57
+ assert m.signature_valid?
56
58
  assert sign_part = m.parts.last
57
- assert m = Mail::Message.new(m.parts.last)
59
+ assert m = Mail::Message.new(m.parts.first)
58
60
  assert !m.multipart?
59
- GPGME::Crypto.new.verify(sign_part.body.to_s, signed_text: @mail.encoded) do |sig|
61
+ GPGME::Crypto.new.verify(sign_part.body.to_s, signed_text: m.encoded) do |sig|
60
62
  assert true == sig.valid?
61
63
  end
64
+ assert_equal 'i am unencrypted', m.body.to_s
65
+ end
66
+
67
+ should "fail signature on tampered body" do
68
+ assert_equal 1, @mails.size
69
+ assert m = @mails.first
70
+ assert_equal 'test', m.subject
71
+ assert !m.encrypted?
72
+ assert m.signed?
73
+ assert m.multipart?
74
+ assert m.signature_valid?
75
+ m.parts.first.body = 'replaced body'
76
+ assert !m.signature_valid?
62
77
  end
63
78
  end
64
79
  end
@@ -1,15 +1,20 @@
1
+ require 'test_helper'
1
2
  require 'mail/gpg/sign_part'
2
3
 
3
4
  class SignPartTest < Test::Unit::TestCase
4
5
  context 'SignPart' do
5
6
  setup do
6
- mail = Mail.new do
7
+ @mail = Mail.new do
7
8
  to 'jane@foo.bar'
8
9
  from 'joe@foo.bar'
9
10
  subject 'test'
10
11
  body 'i am unsigned'
11
12
  end
12
- @part = Mail::Gpg::SignPart.new(mail)
13
+ end
14
+
15
+ should 'roundtrip successfully' do
16
+ signature_part = Mail::Gpg::SignPart.new(@mail, password: 'abc')
17
+ assert Mail::Gpg::SignPart.signature_valid?(@mail, signature_part)
13
18
  end
14
19
  end
15
20
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mail-gpg
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-11-19 00:00:00.000000000 Z
12
+ date: 2014-02-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: mail