mail-gpg 0.2.9 → 0.3.1
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.
- 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
|
- - ">="
|