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