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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8e599862b78aee7485c6182edf2c4989883d02e5
4
- data.tar.gz: 38d4dd8203a9e59be4eb0e452b8a6579bffe35b3
3
+ metadata.gz: 5372354dbe32dbf5c05196f257b8172582e5ffcc
4
+ data.tar.gz: 983703ee19cac1078e36c37bfde1b13906fd0ffa
5
5
  SHA512:
6
- metadata.gz: 8e72e14d9b22ad5394fa6c6ae85d05aacc61524ff3428b69d3d025349cda9da4ce7b65aa6a24b59390e8b9dfe6bfc7dbb9e97dbb3de0381bf04f0d27ad5a04e2
7
- data.tar.gz: 5c8ae280275650dda0c15e8011412f0f2afd352a1eaa95402e46b5ee299105b9acc2616b2a5495bbf9a6b6feea263e0f5a38009dacf4cf2dffa6d4c0dcde8e80
6
+ metadata.gz: aeb9b25b2c04e4c0a71b1eea09631e399a82dbcef6b707b75a4053bca637e66dbdd1d3eabf88673c16a75b8ffd623fabdbdc1e9fe906c10b0ceee4debe452c75
7
+ data.tar.gz: 241a812c8341db86deae1bf48927e5a1c5e92491c505ab67706a9c3e83768c341207dc4d00dcc434d4fbff29252c6625ea5a55110a91a9d033bfb2a534701de6
@@ -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
- open("#{@keyserver}/pks/lookup?options=mr&search=#{URI.escape name}") do |f|
28
- f.each_line do |l|
29
- components = l.strip.split(':')
30
- if components.shift == 'pub'
31
- results << components
32
- end
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
- open("#{@keyserver}/pks/lookup?options=mr&op=get&search=0x#{URI.escape id}") do |f|
41
- return clean_key f.read
42
- end
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
+
@@ -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
- STANDARD_HEADERS.each do |field|
112
- if h = cleartext_mail.header[field]
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
- # MUST containt exactly two body parts
131
- if encrypted_mail.parts.length != 2
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 3136 first part not a valid version part '#{encrypted_mail.parts[0]}'"
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
- %w(from to cc bcc subject reply_to in_reply_to).each do |field|
140
- send field, encrypted_mail.send(field)
141
- end
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 3136 mandates exactly two body parts, found '#{signed_mail.parts.length}'"
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
- cipher_mail.header.fields.each do |field|
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
- signed_text
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
@@ -17,7 +17,7 @@ module Mail
17
17
  # checks validity of signatures (true / false)
18
18
  def signature_valid?
19
19
  sigs = self.signatures
20
- sigs.any? && sigs.detect{|s|!s.valid?}.blank?
20
+ sigs.any? && sigs.all?{|s|s.valid?}
21
21
  end
22
22
 
23
23
  # list of all signatures from verify_result
@@ -1,5 +1,5 @@
1
1
  module Mail
2
2
  module Gpg
3
- VERSION = "0.2.9"
3
+ VERSION = "0.3.1"
4
4
  end
5
5
  end
@@ -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 "pry-nav"
27
+ spec.add_development_dependency "byebug"
28
28
  spec.add_development_dependency "shoulda-context", '~> 1.1'
29
29
  end
@@ -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 "keyserver setup" do
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
- context "with url specified" do
31
+ context "with server #{url}" do
9
32
 
10
- setup do
11
- @hkp = Hkp.new("hkp://my-key-server.net")
12
- end
33
+ context 'client setup' do
13
34
 
14
- should "use specified keyserver" do
15
- assert url = @hkp.instance_variable_get("@keyserver")
16
- assert_equal "hkp://my-key-server.net", url
17
- end
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
- context "without url specified" do
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 found a non-empty keyserver" do
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-----\nHash: SHA1\n\ni am signed\n-----END PGP SIGNED MESSAGE-----", stripped_body
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
@@ -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.2.9
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: 2016-11-15 00:00:00.000000000 Z
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: pry-nav
110
+ name: byebug
111
111
  requirement: !ruby/object:Gem::Requirement
112
112
  requirements:
113
113
  - - ">="