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 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
  - - ">="