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.
- data/History.txt +19 -1
- data/README.md +19 -2
- data/lib/mail/gpg.rb +54 -3
- data/lib/mail/gpg/gpgme_helper.rb +9 -0
- data/lib/mail/gpg/message_patch.rb +7 -0
- data/lib/mail/gpg/sign_part.rb +7 -0
- data/lib/mail/gpg/version.rb +1 -1
- data/test/gpg_test.rb +4 -2
- data/test/message_test.rb +17 -2
- data/test/sign_part_test.rb +7 -2
- metadata +2 -2
data/History.txt
CHANGED
@@ -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:
|
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
|
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
|
data/lib/mail/gpg.rb
CHANGED
@@ -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
|
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
|
data/lib/mail/gpg/sign_part.rb
CHANGED
@@ -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
|
data/lib/mail/gpg/version.rb
CHANGED
data/test/gpg_test.rb
CHANGED
@@ -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
|
-
|
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.
|
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
|
data/test/message_test.rb
CHANGED
@@ -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.
|
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:
|
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
|
data/test/sign_part_test.rb
CHANGED
@@ -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
|
-
|
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.
|
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:
|
12
|
+
date: 2014-02-17 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: mail
|