mail-gpg 0.2.9 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/History.txt +13 -0
- data/README.md +12 -0
- data/lib/hkp.rb +83 -12
- data/lib/mail/gpg.rb +19 -28
- data/lib/mail/gpg/inline_decrypted_message.rb +1 -3
- data/lib/mail/gpg/inline_signed_message.rb +2 -1
- data/lib/mail/gpg/verify_result_attribute.rb +1 -1
- data/lib/mail/gpg/version.rb +1 -1
- data/mail-gpg.gemspec +1 -1
- data/test/hkp_test.rb +78 -12
- data/test/inline_signed_message_test.rb +1 -1
- data/test/message_test.rb +2 -0
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5372354dbe32dbf5c05196f257b8172582e5ffcc
|
4
|
+
data.tar.gz: 983703ee19cac1078e36c37bfde1b13906fd0ffa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aeb9b25b2c04e4c0a71b1eea09631e399a82dbcef6b707b75a4053bca637e66dbdd1d3eabf88673c16a75b8ffd623fabdbdc1e9fe906c10b0ceee4debe452c75
|
7
|
+
data.tar.gz: 241a812c8341db86deae1bf48927e5a1c5e92491c505ab67706a9c3e83768c341207dc4d00dcc434d4fbff29252c6625ea5a55110a91a9d033bfb2a534701de6
|
data/History.txt
CHANGED
@@ -1,3 +1,16 @@
|
|
1
|
+
== 0.3.1 2017-04-13
|
2
|
+
|
3
|
+
* fixes a bug with signature verification that only surfaced in environments
|
4
|
+
where ActiveSupport isn't loaded. Thanks @mashedcode for pointing this out.
|
5
|
+
|
6
|
+
== 0.3.0 2016-12-27
|
7
|
+
|
8
|
+
* [MIGHT BREAK THINGS] All mail headers will preserved now, if you want to
|
9
|
+
suppress headers you'll have to remove them yourself from now on.
|
10
|
+
* Strip "headers" when stripping inline signature (patch by @duckdalbe)
|
11
|
+
* support hkps URI scheme
|
12
|
+
* bugfix for verifying the "encapsulated" variant of pgp/mime (patch by @duckdalbe)
|
13
|
+
|
1
14
|
== 0.2.9 2016-11-15
|
2
15
|
|
3
16
|
* add missing require to test case (patch by @ge-fa)
|
data/README.md
CHANGED
@@ -151,6 +151,18 @@ You can specify the keyserver url when initializing the class:
|
|
151
151
|
hkp = Hkp.new("hkp://my-key-server.de")
|
152
152
|
```
|
153
153
|
|
154
|
+
Or, if you want to override how ssl certificates should be treated in case of
|
155
|
+
TLS-secured keyservers (the default is `VERIFY_PEER`):
|
156
|
+
|
157
|
+
```
|
158
|
+
hkp = Hkp.new(keyserver: "hkps://another.key-server.com",
|
159
|
+
ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE)
|
160
|
+
```
|
161
|
+
|
162
|
+
If no port is specified in hkp or hkps URIs (as in the examples above), port
|
163
|
+
11371 will be used for hkp and port 443 for hkps URIs. Standard `http` or
|
164
|
+
`https` URIs with or without explicitly set ports work as well.
|
165
|
+
|
154
166
|
If no url is given, this gem will try to determine the default keyserver
|
155
167
|
url from the system's gpg config (using `gpgconf` if available or by
|
156
168
|
parsing the `gpg.conf` file). As a last resort, the server-pool at
|
data/lib/hkp.rb
CHANGED
@@ -1,8 +1,66 @@
|
|
1
|
-
require 'open-uri'
|
2
1
|
require 'gpgme'
|
2
|
+
require 'openssl'
|
3
|
+
require 'net/http'
|
3
4
|
|
4
|
-
# simple HKP client for public key retrieval
|
5
|
+
# simple HKP client for public key search and retrieval
|
5
6
|
class Hkp
|
7
|
+
|
8
|
+
class TooManyRedirects < StandardError; end
|
9
|
+
|
10
|
+
class InvalidResponse < StandardError; end
|
11
|
+
|
12
|
+
|
13
|
+
class Client
|
14
|
+
|
15
|
+
MAX_REDIRECTS = 3
|
16
|
+
|
17
|
+
def initialize(server, ssl_verify_mode: OpenSSL::SSL::VERIFY_PEER)
|
18
|
+
uri = URI server
|
19
|
+
@host = uri.host
|
20
|
+
@port = uri.port
|
21
|
+
@use_ssl = false
|
22
|
+
@ssl_verify_mode = ssl_verify_mode
|
23
|
+
|
24
|
+
# set port and ssl flag according to URI scheme
|
25
|
+
case uri.scheme.downcase
|
26
|
+
when 'hkp'
|
27
|
+
# use the HKP default port unless another port has been given
|
28
|
+
@port ||= 11371
|
29
|
+
when /\A(hkp|http)s\z/
|
30
|
+
# hkps goes through 443 by default
|
31
|
+
@port ||= 443
|
32
|
+
@use_ssl = true
|
33
|
+
end
|
34
|
+
@port ||= 80
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
def get(path, redirect_depth = 0)
|
39
|
+
Net::HTTP.start @host, @port, use_ssl: @use_ssl,
|
40
|
+
verify_mode: @ssl_verify_mode do |http|
|
41
|
+
|
42
|
+
request = Net::HTTP::Get.new path
|
43
|
+
response = http.request request
|
44
|
+
|
45
|
+
case response.code.to_i
|
46
|
+
when 200
|
47
|
+
return response.body
|
48
|
+
when 301, 302
|
49
|
+
if redirect_depth >= MAX_REDIRECTS
|
50
|
+
raise TooManyRedirects
|
51
|
+
else
|
52
|
+
http_get response['location'], redirect_depth + 1
|
53
|
+
end
|
54
|
+
else
|
55
|
+
raise InvalidResponse, response.code
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
|
6
64
|
def initialize(options = {})
|
7
65
|
if String === options
|
8
66
|
options = { keyserver: options }
|
@@ -15,6 +73,7 @@ class Hkp
|
|
15
73
|
!!@options[:raise_errors]
|
16
74
|
end
|
17
75
|
|
76
|
+
#
|
18
77
|
# hkp.search 'user@host.com'
|
19
78
|
# will return an array of arrays, one for each matching key found, containing
|
20
79
|
# the key id as the first elment and any further info returned by the key
|
@@ -24,27 +83,33 @@ class Hkp
|
|
24
83
|
# and what info they return besides the key id
|
25
84
|
def search(name)
|
26
85
|
[].tap do |results|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
86
|
+
result = hkp_client.get "/pks/lookup?options=mr&search=#{URI.escape name}"
|
87
|
+
|
88
|
+
result.each_line do |l|
|
89
|
+
components = l.strip.split(':')
|
90
|
+
if components.shift == 'pub'
|
91
|
+
results << components
|
33
92
|
end
|
34
|
-
end
|
93
|
+
end if result
|
35
94
|
end
|
95
|
+
|
96
|
+
rescue
|
97
|
+
raise $! if raise_errors?
|
98
|
+
nil
|
36
99
|
end
|
37
100
|
|
101
|
+
|
38
102
|
# returns the key data as returned from the server as a string
|
39
103
|
def fetch(id)
|
40
|
-
|
41
|
-
|
42
|
-
|
104
|
+
result = hkp_client.get "/pks/lookup?options=mr&op=get&search=0x#{URI.escape id}"
|
105
|
+
return clean_key(result) if result
|
106
|
+
|
43
107
|
rescue Exception
|
44
108
|
raise $! if raise_errors?
|
45
109
|
nil
|
46
110
|
end
|
47
111
|
|
112
|
+
|
48
113
|
# fetches key data by id and imports the found key(s) into GPG, returning the full hex fingerprints of the
|
49
114
|
# imported key(s) as an array. Given there are no collisions with the id given / the server has returned
|
50
115
|
# exactly one key this will be a one element array.
|
@@ -57,6 +122,11 @@ class Hkp
|
|
57
122
|
end
|
58
123
|
|
59
124
|
private
|
125
|
+
|
126
|
+
def hkp_client
|
127
|
+
@hkp_client ||= Client.new @keyserver, ssl_verify_mode: @options[:ssl_verify_mode]
|
128
|
+
end
|
129
|
+
|
60
130
|
def clean_key(key)
|
61
131
|
if key =~ /(-----BEGIN PGP PUBLIC KEY BLOCK-----.*-----END PGP PUBLIC KEY BLOCK-----)/m
|
62
132
|
return $1
|
@@ -83,3 +153,4 @@ class Hkp
|
|
83
153
|
end
|
84
154
|
|
85
155
|
end
|
156
|
+
|
data/lib/mail/gpg.rb
CHANGED
@@ -100,51 +100,33 @@ module Mail
|
|
100
100
|
false
|
101
101
|
end
|
102
102
|
|
103
|
-
STANDARD_HEADERS = %w(from to cc bcc reply_to subject in_reply_to return_path message_id)
|
104
|
-
MORE_HEADERS = %w(Auto-Submitted OpenPGP References)
|
105
|
-
|
106
103
|
private
|
107
104
|
|
108
105
|
def self.construct_mail(cleartext_mail, options, &block)
|
109
106
|
Mail.new do
|
110
107
|
self.perform_deliveries = cleartext_mail.perform_deliveries
|
111
|
-
|
112
|
-
|
113
|
-
self.header[field] = h.value
|
114
|
-
end
|
115
|
-
end
|
108
|
+
Mail::Gpg.copy_headers cleartext_mail, self
|
109
|
+
# necessary?
|
116
110
|
if cleartext_mail.message_id
|
117
111
|
header['Message-ID'] = cleartext_mail['Message-ID'].value
|
118
112
|
end
|
119
|
-
cleartext_mail.header.fields.each do |field|
|
120
|
-
if MORE_HEADERS.include?(field.name) or field.name =~ /^(List|X)-/
|
121
|
-
header[field.name] = field.value
|
122
|
-
end
|
123
|
-
end
|
124
113
|
instance_eval &block
|
125
114
|
end
|
126
115
|
end
|
127
116
|
|
128
117
|
# decrypts PGP/MIME (RFC 3156, section 4) encrypted mail
|
129
118
|
def self.decrypt_pgp_mime(encrypted_mail, options)
|
130
|
-
|
131
|
-
|
132
|
-
raise EncodingError, "RFC 3136 mandates exactly two body parts, found '#{encrypted_mail.parts.length}'"
|
119
|
+
if encrypted_mail.parts.length < 2
|
120
|
+
raise EncodingError, "RFC 3156 mandates exactly two body parts, found '#{encrypted_mail.parts.length}'"
|
133
121
|
end
|
134
122
|
if !VersionPart.isVersionPart? encrypted_mail.parts[0]
|
135
|
-
raise EncodingError, "RFC
|
123
|
+
raise EncodingError, "RFC 3156 first part not a valid version part '#{encrypted_mail.parts[0]}'"
|
136
124
|
end
|
137
125
|
decrypted = DecryptedPart.new(encrypted_mail.parts[1], options)
|
138
|
-
Mail.new(decrypted) do
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
# copy header fields
|
143
|
-
# headers from the encrypted part (which are already set by Mail.new
|
144
|
-
# above) will be preserved.
|
145
|
-
encrypted_mail.header.fields.each do |field|
|
146
|
-
header[field.name] = field.value if field.name =~ /^X-/ && header[field.name].nil?
|
147
|
-
end
|
126
|
+
Mail.new(decrypted.raw_source) do
|
127
|
+
# headers from the encrypted part (set by the initializer above) take
|
128
|
+
# precedence over those from the outer mail.
|
129
|
+
Mail::Gpg.copy_headers encrypted_mail, self, overwrite: false
|
148
130
|
verify_result decrypted.verify_result if options[:verify]
|
149
131
|
end
|
150
132
|
end
|
@@ -168,7 +150,7 @@ module Mail
|
|
168
150
|
def self.signature_valid_pgp_mime?(signed_mail, options)
|
169
151
|
# MUST contain exactly two body parts
|
170
152
|
if signed_mail.parts.length != 2
|
171
|
-
raise EncodingError, "RFC
|
153
|
+
raise EncodingError, "RFC 3156 mandates exactly two body parts, found '#{signed_mail.parts.length}'"
|
172
154
|
end
|
173
155
|
result, verify_result = SignPart.verify_signature(signed_mail.parts[0], signed_mail.parts[1], options)
|
174
156
|
signed_mail.verify_result = verify_result
|
@@ -196,6 +178,15 @@ module Mail
|
|
196
178
|
return result
|
197
179
|
end
|
198
180
|
|
181
|
+
# copies all header fields from mail in first argument to that given last
|
182
|
+
def self.copy_headers(from, to, overwrite: true)
|
183
|
+
from.header.fields.each do |field|
|
184
|
+
if overwrite || to.header[field.name].nil?
|
185
|
+
to.header[field.name] = field.value
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
199
190
|
|
200
191
|
# check if PGP/MIME encrypted (RFC 3156)
|
201
192
|
def self.encrypted_mime?(mail)
|
@@ -15,9 +15,7 @@ module Mail
|
|
15
15
|
def self.setup(cipher_mail, options = {})
|
16
16
|
if cipher_mail.multipart?
|
17
17
|
self.new do
|
18
|
-
|
19
|
-
header[field.name] = field.value
|
20
|
-
end
|
18
|
+
Mail::Gpg.copy_headers cipher_mail, self
|
21
19
|
cipher_mail.parts.each do |part|
|
22
20
|
p = VerifiedPart.new do |p|
|
23
21
|
if part.has_content_type? && /application\/(?:octet-stream|pgp-encrypted)/ =~ part.mime_type
|
@@ -62,7 +62,8 @@ module Mail
|
|
62
62
|
signed_text.gsub! INLINE_SIG_RE, ''
|
63
63
|
signed_text.strip!
|
64
64
|
end
|
65
|
-
|
65
|
+
# Strip possible inline-"headers" (e.g. "Hash: SHA256", or "Comment: something").
|
66
|
+
signed_text.gsub(/(.*^-----BEGIN PGP SIGNED MESSAGE-----\n)(.*?)^$(.+)/m, '\1\3')
|
66
67
|
end
|
67
68
|
|
68
69
|
end
|
data/lib/mail/gpg/version.rb
CHANGED
data/mail-gpg.gemspec
CHANGED
@@ -24,6 +24,6 @@ Gem::Specification.new do |spec|
|
|
24
24
|
spec.add_development_dependency "test-unit", "~> 3.0"
|
25
25
|
spec.add_development_dependency "rake"
|
26
26
|
spec.add_development_dependency "actionmailer", ">= 3.2.0"
|
27
|
-
spec.add_development_dependency "
|
27
|
+
spec.add_development_dependency "byebug"
|
28
28
|
spec.add_development_dependency "shoulda-context", '~> 1.1'
|
29
29
|
end
|
data/test/hkp_test.rb
CHANGED
@@ -1,34 +1,100 @@
|
|
1
1
|
require 'test_helper'
|
2
|
+
require 'byebug'
|
2
3
|
require 'hkp'
|
3
4
|
|
4
5
|
class HkpTest < Test::Unit::TestCase
|
5
6
|
|
6
|
-
context "
|
7
|
+
context "hpk client" do
|
8
|
+
{
|
9
|
+
"http://pool.sks-keyservers.net:11371" => {
|
10
|
+
host: 'pool.sks-keyservers.net',
|
11
|
+
ssl: false,
|
12
|
+
port: 11371
|
13
|
+
},
|
14
|
+
"https://hkps.pool.sks-keyservers.net" => {
|
15
|
+
host: 'hkps.pool.sks-keyservers.net',
|
16
|
+
ssl: true,
|
17
|
+
port: 443
|
18
|
+
},
|
19
|
+
"hkp://pool.sks-keyservers.net" => {
|
20
|
+
host: 'pool.sks-keyservers.net',
|
21
|
+
ssl: false,
|
22
|
+
port: 11371
|
23
|
+
},
|
24
|
+
"hkps://hkps.pool.sks-keyservers.net" => {
|
25
|
+
host: 'hkps.pool.sks-keyservers.net',
|
26
|
+
ssl: true,
|
27
|
+
port: 443
|
28
|
+
},
|
29
|
+
}.each do |url, data|
|
7
30
|
|
8
|
-
|
31
|
+
context "with server #{url}" do
|
9
32
|
|
10
|
-
|
11
|
-
@hkp = Hkp.new("hkp://my-key-server.net")
|
12
|
-
end
|
33
|
+
context 'client setup' do
|
13
34
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
35
|
+
setup do
|
36
|
+
@client = Hkp::Client.new url
|
37
|
+
end
|
38
|
+
|
39
|
+
should "have correct port" do
|
40
|
+
assert_equal data[:port], @client.instance_variable_get("@port")
|
41
|
+
end
|
42
|
+
|
43
|
+
should "have correct ssl setting" do
|
44
|
+
assert_equal data[:ssl], @client.instance_variable_get("@use_ssl")
|
45
|
+
end
|
46
|
+
|
47
|
+
should "have correct host" do
|
48
|
+
assert_equal data[:host], @client.instance_variable_get("@host")
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
if ENV['ONLINE_TESTS']
|
54
|
+
|
55
|
+
context 'key search' do
|
18
56
|
|
57
|
+
setup do
|
58
|
+
@hkp = Hkp.new keyserver: url,
|
59
|
+
ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE
|
60
|
+
end
|
61
|
+
|
62
|
+
should 'find key' do
|
63
|
+
assert result = @hkp.search('jk@jkraemer.net')
|
64
|
+
assert result.size > 0
|
65
|
+
end
|
66
|
+
|
67
|
+
should 'fetch key' do
|
68
|
+
assert result = @hkp.fetch('584C8BEE17CAC560')
|
69
|
+
assert_match 'PGP PUBLIC KEY BLOCK', result
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
19
77
|
end
|
78
|
+
end
|
20
79
|
|
21
|
-
|
22
|
-
|
80
|
+
context 'key search' do
|
81
|
+
|
82
|
+
context "without keyserver url" do
|
23
83
|
setup do
|
24
84
|
@hkp = Hkp.new
|
25
85
|
end
|
26
86
|
|
27
|
-
should "have
|
87
|
+
should "have a non-empty keyserver" do
|
28
88
|
assert url = @hkp.instance_variable_get("@keyserver")
|
29
89
|
assert !url.blank?
|
30
90
|
end
|
31
91
|
|
92
|
+
if ENV['ONLINE_TESTS']
|
93
|
+
should 'find key' do
|
94
|
+
assert result = @hkp.search('jk@jkraemer.net')
|
95
|
+
assert result.size > 0
|
96
|
+
end
|
97
|
+
end
|
32
98
|
end
|
33
99
|
|
34
100
|
end
|
@@ -19,7 +19,7 @@ class InlineSignedMessageTest < Test::Unit::TestCase
|
|
19
19
|
should 'strip signature from signed text' do
|
20
20
|
body = self.class.inline_sign(@mail, 'i am signed')
|
21
21
|
assert stripped_body = Mail::Gpg::InlineSignedMessage.strip_inline_signature(body)
|
22
|
-
assert_equal "-----BEGIN PGP SIGNED MESSAGE-----\
|
22
|
+
assert_equal "-----BEGIN PGP SIGNED MESSAGE-----\n\ni am signed\n-----END PGP SIGNED MESSAGE-----", stripped_body
|
23
23
|
end
|
24
24
|
|
25
25
|
should 'not change unsigned text' do
|
data/test/message_test.rb
CHANGED
@@ -80,6 +80,7 @@ class MessageTest < Test::Unit::TestCase
|
|
80
80
|
@mail.header['List-Owner'] = 'test-owner@lists.example.org'
|
81
81
|
@mail.header['List-Post'] = '<mailto:test@lists.example.org> (Subscribers only)'
|
82
82
|
@mail.header['List-Unsubscribe'] = 'bar'
|
83
|
+
@mail.header['Date'] = 'Sun, 25 Dec 2016 16:56:52 -0500'
|
83
84
|
@mail.header['OpenPGP'] = 'id=0x0123456789abcdef0123456789abcdefdeadbeef (present on keyservers); (Only encrypted and signed emails are accepted)'
|
84
85
|
@mail.deliver
|
85
86
|
end
|
@@ -91,6 +92,7 @@ class MessageTest < Test::Unit::TestCase
|
|
91
92
|
assert_equal 'test-owner@lists.example.org', @mails.first.header['List-Owner'].value
|
92
93
|
assert_equal '<mailto:test@lists.example.org> (Subscribers only)', @mails.first.header['List-Post'].value
|
93
94
|
assert_equal 'bar', @mails.first.header['List-Unsubscribe'].value
|
95
|
+
assert_equal 'Sun, 25 Dec 2016 16:56:52 -0500', @mails.first.header['Date'].value
|
94
96
|
assert_equal 'id=0x0123456789abcdef0123456789abcdefdeadbeef (present on keyservers); (Only encrypted and signed emails are accepted)', @mails.first.header['OpenPGP'].value
|
95
97
|
end
|
96
98
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mail-gpg
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jens Kraemer
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2017-04-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: mail
|
@@ -107,7 +107,7 @@ dependencies:
|
|
107
107
|
- !ruby/object:Gem::Version
|
108
108
|
version: 3.2.0
|
109
109
|
- !ruby/object:Gem::Dependency
|
110
|
-
name:
|
110
|
+
name: byebug
|
111
111
|
requirement: !ruby/object:Gem::Requirement
|
112
112
|
requirements:
|
113
113
|
- - ">="
|