mail-gpg 0.1.2 → 0.1.3

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