rubygems-openpgp 0.4.0 → 0.5.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0657ab763223c89affb7cfdbdfe7227ba093a0c0
4
+ data.tar.gz: 7b16826241b21a82815b8baa681697ad7e0f3983
5
+ SHA512:
6
+ metadata.gz: b3c5a89a986acbcd84aaee04aae27aa5a68966529c830232be62cb64a3f7c62c59d1a18e749007477516227bcdf5969a3c0b1507a62aaa55c6bd84c82f6a9244
7
+ data.tar.gz: adeb6e5b33bb455ff52f90405744417d566af3b721cc829576b8eba9eeed7c0cc7fa5072393bf0e4ce4c1c6020f0e8e85883c97d89ad5482894a26431f628bd6
checksums.yaml.gz.asc ADDED
@@ -0,0 +1,11 @@
1
+ -----BEGIN PGP SIGNATURE-----
2
+ Version: GnuPG v1.4.11 (GNU/Linux)
3
+
4
+ iQEcBAABAgAGBQJRPJV6AAoJEP5F5V2hilTW55oH/RVctpv/kKf8erckN0vvNZUa
5
+ KdtcULIoD0P8u26uiL8zTM4noWi2f3kkfv1fAd9026IsJarOpucfDmpZpTDb88Xw
6
+ gElUTXKhG7bnoPhuEfrz6/h5cwMpQREktIbVwtCF0ED7aRLbxcUChx+BfFLLeUW1
7
+ yLnwi8aN2EJlcL1w8fvDmPux6IppuZ/NPxo65dXVHIsjpyLvhrNhqaEOEHv2AP9b
8
+ xNWSV+sn8i9YfVUZhmZLLakWkmfoRGrZYXQYisTQbKfN5Kfeyg3uJRcQ9kk8Kmua
9
+ 5UBTLEk/T2meyaN7FQ8l23P5ZNxcRhqDWnGKJMxRZxaShVJk4P8hmhgICL/YX6w=
10
+ =CtND
11
+ -----END PGP SIGNATURE-----
data.tar.gz.asc CHANGED
@@ -1,11 +1,11 @@
1
1
  -----BEGIN PGP SIGNATURE-----
2
- Version: GnuPG/MacGPG2 v2.0.17 (Darwin)
2
+ Version: GnuPG v1.4.11 (GNU/Linux)
3
3
 
4
- iQEcBAABCgAGBQJRKNObAAoJEP5F5V2hilTWjFYH/RPGxYJA41R43Yrgqu56vtAU
5
- iPY/pg041S42UnObIIFgA76fMeixzfFCdBkvFFnlDuqmbSLxtiJt0Q/ql5c/hFzA
6
- S/rKibb1I9nlzOJ3p/GO78HzaD/M2Ja/jdrJtt+w7LVqSVtfDOyS+lPe3wxzf6SS
7
- 7hgpycQv6GX3C5AEGY/nP0btkDxNtYMGGgcYz3WfrOEQFDIKS6IU4iZCj5c6Zw4n
8
- yD2pAvHWoqdkYy0y+eUsi83E1zCL9fHEK3R/I+c9Kc4TQF2OXFmGZdc7wbYwtdPb
9
- yDqlTNo8mbm+kxek1P4PobHJ7QADHRC7kOSBMOXVw80JALUThAt11Y+bHOHv3/Y=
10
- =58pK
4
+ iQEcBAABAgAGBQJRPJV3AAoJEP5F5V2hilTWJnAH/jyebr8gp9guDxLVuN3lodD9
5
+ OLjYgkI97P7e597lL7LDyMpadyo01yQjB1+xfKNQqgAaUtVUglfaeGjU+vnvpfAz
6
+ 5Hx/SdztlhDnzK+W71wzdBJ6/8Nco6wDhDe8dQcM3BgSK2ijvHgCfYMqy9SoKGgC
7
+ ZHNWq+a2cPd8nglgaR2iTlN+0kzQq/f4WdGFOuCliq5E5ItFlCzMiSnVUfAvyT2G
8
+ lOvnxYvzMOItxQMHU9lVvGvYhUxsUd4bvRMBvy3rdbErnp3zFFSu2tHTEvoJt2+v
9
+ LvzBaiQ+u00qirlusp1amEHurUK7bPSuTSWN98fnrh5DaFwzh/hd1Cjrr0FY1xY=
10
+ =rKlC
11
11
  -----END PGP SIGNATURE-----
data/README.md CHANGED
@@ -1,6 +1,12 @@
1
1
  rubygems-openpgp
2
2
  ================
3
3
 
4
+ Information for gem users and gem developers is slowly and surely migrating
5
+ to the [rubygems-openpgp Certificate
6
+ Authority](http://rubygems-openpgp-ca.org). You probably want to go
7
+ there unless you're interested in working on the plugin itself.
8
+
9
+
4
10
  Software Assurance
5
11
  ------------------
6
12
 
@@ -44,33 +50,28 @@ Signing example
44
50
  Verification Example
45
51
  --------------------
46
52
 
53
+ A detailed walkthrough of verifiction is available at
54
+ [The Complete Guide to Verifying Gems with
55
+ rubygems-openpgp](http://www.rubygems-openpgp-ca.org/blog/the-complete-guide-to-verifying-gems-with-rubygems-openpgp.html)
56
+
57
+ ### TLDR?
58
+
47
59
  A test gem **openpgp_signed_hola** is on rubygems.org. To try out
48
60
  this extension:
49
61
 
50
- gem install openpgp_signed_hola-0.0.0.gem --verify
62
+ gem install openpgp_signed_hola-0.0.0.gem --verify --trust --get-key
51
63
 
52
- But That Just Failed!
53
- ---------------------
54
64
 
55
- You probably don't have my public key yet. You need my public key to
56
- verify the digital signature. You also want to perform some
57
- authentication of that public key.
65
+ ### But That Just Failed!
58
66
 
59
- [Notes on retrieving and authenticating public keys.](./doc/retrieving-and-authenticating-keys.md)
67
+ You probably don't *trust* my public key. More information is
68
+ available at [The Complete Guide to Verifying Gems with
69
+ rubygems-openpgp](http://www.rubygems-openpgp-ca.org/blog/the-complete-guide-to-verifying-gems-with-rubygems-openpgp.html)
60
70
 
61
71
  Verifying your initial install
62
72
  ------------------------------
63
73
 
64
- All versions of this gem should be signed. But the first time you
65
- install the package you run into a bit of a chicken-and-the-egg
66
- problem. You can't verify the package until you've installed a copy.
67
- But if that copy isn't verified, if could already be compromised.
68
-
69
- But don't worry. You can use a stand-alone signature to verify your
70
- initial install. Since the stand-alone signature is on github, and
71
- the software package is on rubygems.org, a malicious user would need
72
- to compromise both sites to publish a compromised gem and
73
- compromised/forged digital signature.
74
-
75
- [Notes on verifying the initial install.](./doc/verifying-initial-install.md)
74
+ You can verify your initial install with a detached signature.
75
+ [Here's
76
+ how.](http://www.rubygems-openpgp-ca.org/blog/the-complete-guide-to-verifying-your-initial-install.html)
76
77
 
@@ -1,284 +1,2 @@
1
- require 'rubygems'
2
- require 'rubygems/package'
3
- require 'rubygems/user_interaction'
4
- require 'shellwords'
5
- require 'open3'
6
- require 'tempfile'
7
- require 'gpg_status_parser'
8
-
9
- # Exception for this class
10
- class Gem::OpenPGPException < RuntimeError; end
11
-
12
- # A wrapper that shells out the real OpenPGP crypto work
13
- # to gpg.
14
- module Gem::OpenPGP
15
- extend Shellwords
16
- extend Gem::UserInteraction
17
-
18
- # Given a string of data, generate and return a detached
19
- # signature. By defualt, this will use your primary secret key.
20
- # This can be overridden by specifying a key_id for another
21
- # private key.
22
- def self.detach_sign data, key_id=nil, homedir=nil
23
- is_gpg_available
24
- is_key_valid key_id if key_id
25
- is_homedir_valid homedir if homedir
26
-
27
- key_flag = ""
28
- key_flag = "-u #{shellescape(key_id)}" if key_id
29
-
30
- homedir_flag = ""
31
- homedir_flag = "--homedir #{shellescape(homedir)}" if homedir
32
-
33
- gpg_args = "#{key_flag} #{homedir_flag} --detach-sign --armor"
34
- gpg_results = run_gpg(gpg_args, data)
35
- did_gpg_error? gpg_results
36
-
37
- gpg_results[:stdout]
38
- end
39
-
40
- # Extract the info we care about, throw away the rest
41
- def self.verify_extract_status_info message, status_info
42
- case message.status
43
- when :GOODSIG, :BADSIG, :ERRSIG
44
- status_info[:good_or_bad] = message.status
45
- status_info[:uid] = (message.args[:username] || "").strip
46
-
47
- when :SIG_ID
48
- when :VALIDSIG, :EXPSIG, :BADSIG
49
- status_info[:sig_status] = message.status
50
- status_info[:primary_key] = "0x#{message.args[:primary_key_fpr][-9..-1]}"
51
- when :TRUST_UNDEFINED, :TRUST_NEVER, :TRUST_MARGINAL, :TRUST_FULLY, :TRUST_ULTIMATE
52
- status_info[:trust_status] = message.status
53
- when :NO_PUBKEY
54
- status_info[:failure] = "You don't have the public key. Use --get-key to automagically retrieve from keyservers"
55
- when :IMPORTED, :IMPORT_OK, :IMPORT_RES
56
- #silently_ignore
57
- when :KEYEXPIRED, :SIGEXPIRED
58
- # recalculating trust db, ignore.
59
- else
60
- puts "unexpected message: #{message.status} #{message.args.inspect}"
61
- end
62
- end
63
-
64
- # Print info about the sig, check that we like it, and possibly abort
65
- def self.verify_check_sig status_info
66
- sig_msg = "Signature for #{status_info[:file_name]} from user #{status_info[:uid]} key #{status_info[:primary_key]} is #{status_info[:good_or_bad]}, #{status_info[:sig_status]} and #{status_info[:trust_status]}"
67
- if status_info[:trust_status] == :TRUST_NEVER
68
- say add_color(sig_msg, :red)
69
- raise Gem::OpenPGPException, "Never Trusted. Won't install."
70
- elsif status_info[:trust_status] == :TRUST_UNDEFINED
71
- say add_color(sig_msg, :yellow)
72
- if options[:trust] && !options[:no_trust]
73
- raise Gem::OpenPGPException, "Trust Undefined and you've specified --trust. Won't install."
74
- end
75
- else
76
- say add_color(sig_msg , :green)
77
- end
78
- end
79
-
80
- # Given a string containing data, and a string containing
81
- # a detached signature, verify the data. If we can't verify
82
- # then raise an exception.
83
- #
84
- # Optionally tell gpg to retrive the key if it's not provided
85
- def self.verify file_name, data, sig, get_key=false, homedir=nil
86
- is_gpg_available
87
- is_homedir_valid homedir if homedir
88
-
89
- data_file = create_tempfile data
90
- sig_file = create_tempfile sig
91
-
92
- get_key_params = "--keyserver pool.sks-keyservers.net --keyserver-options auto-key-retrieve"
93
- get_key_params = "" if get_key != true
94
-
95
- homedir_flags = ""
96
- homedir_flags = "--homedir #{homedir}" if homedir
97
-
98
- gpg_args = "#{get_key_params} #{homedir_flags} --verify #{sig_file.path} #{data_file.path}"
99
-
100
- status_info = {:file_name => file_name}
101
- gpg_results = run_gpg(gpg_args) { |message| verify_extract_status_info(message, status_info) }
102
-
103
- if status_info[:failure]
104
- say add_color(status_info[:failure], :red)
105
- raise Gem::OpenPGPException, "Fail!"
106
- else
107
- verify_check_sig status_info
108
- end
109
-
110
- did_gpg_error? gpg_results
111
-
112
- [gpg_results[:stderr], gpg_results[:status]]
113
- end
114
-
115
- # Signs an existing gemfile by iterating the tar'ed up contents,
116
- # and signing any contents. creating a new file with original contents
117
- # and OpenPGP sigs. The OpenPGP sigs are saved as .asc files so they
118
- # won't conflict with X509 sigs.
119
- #
120
- # Optional param "key" allows you to use a different private
121
- # key than the GPG default.
122
- def self.sign_gem gem, key=nil, homedir=nil
123
- unsigned_gem = gem + ".unsigned"
124
-
125
- begin
126
- FileUtils.mv gem, unsigned_gem
127
- rescue Errno::ENOENT => ex
128
- raise Gem::CommandLineError, "The gem #{gem} does not seem to exist. (#{ex.message})"
129
- end
130
-
131
- unsigned_gem_file = File.open(unsigned_gem, "r")
132
- signed_gem_file = File.open(gem, "w")
133
-
134
- signed_gem = Gem::Package::TarWriter.new(signed_gem_file)
135
-
136
- Gem::Package::TarReader.new(unsigned_gem_file).each do |f|
137
- say(f.full_name.inspect)
138
-
139
- if f.full_name[-4..-1] == ".asc"
140
- say("Skipping old signature file #{f.full_name}")
141
- next
142
- end
143
-
144
- say("Signing #{f.full_name.inspect}...")
145
-
146
- file_contents = f.read()
147
-
148
- signed_gem.add_file(f.full_name, 0644) do |outfile|
149
- outfile.write(file_contents)
150
- end
151
-
152
- signed_gem.add_file(f.full_name + ".asc", 0644) do |outfile|
153
- outfile.write(Gem::OpenPGP.detach_sign(file_contents,key,homedir))
154
- end
155
-
156
- end
157
-
158
- signed_gem_file.close
159
- unsigned_gem_file.close
160
- File.delete unsigned_gem_file
161
-
162
- rescue Exception => ex
163
- if unsigned_gem_file
164
- FileUtils.mv unsigned_gem_file, gem
165
- end
166
-
167
- raise
168
- end
169
-
170
- def self.verify_gem gem, get_key=false, homedir=nil
171
-
172
- begin
173
- file = File.open(gem,"r")
174
- rescue Errno::ENOENT => ex
175
- raise Gem::CommandLineError, "Gem #{gem} not found. Note you can only verify local gems at this time, so you may need to run 'gem fetch #{gem}' before verifying."
176
- end
177
-
178
- tar_files = {}
179
-
180
- Gem::Package::TarReader.new(file).each do |f|
181
- tar_files[f.full_name] = f.read()
182
- end
183
-
184
- tar_files.keys.each do |file_name|
185
- next if file_name[-4..-1] == ".asc"
186
-
187
- sig_file_name = file_name + ".asc"
188
- if !tar_files.has_key? sig_file_name
189
- say add_color("WARNING!!! No sig found for #{file_name}", :red)
190
- raise Gem::OpenPGPException, "Can't verify without sig, aborting!!!"
191
- end
192
-
193
- begin
194
- err, res = Gem::OpenPGP.verify(file_name, tar_files[file_name], tar_files[sig_file_name], get_key, homedir)
195
-
196
- rescue Gem::OpenPGPException => ex
197
- color_code = "31"
198
- say add_color(ex.message, :red)
199
- raise
200
- end
201
- end
202
-
203
- ensure
204
- file.close unless file.nil?
205
- end
206
-
207
- private
208
-
209
- def self.did_gpg_error? gpg_results
210
- if gpg_results[:status] != 0
211
- say add_color("gpg returned unexpected status code", :red)
212
- say add_color(gpg_results[:stderr], :red)
213
- raise Gem::OpenPGPException, "gpg returned unexpected error code #{gpg_results[:status]}"
214
- end
215
- end
216
-
217
- # Tests to see if gpg is installed and available.
218
- def self.is_gpg_available
219
- err_msg = "Unable to find a working gnupg installation. Make sure gnupg is installed and you can call 'gpg --version' from a command prompt."
220
- `gpg --version`
221
- raise Gem::OpenPGPException, err_msg if $? != 0
222
- rescue Errno::ENOENT => ex
223
- raise Gem::OpenPGPException, err_msg if $? != 0
224
- end
225
-
226
- def self.is_key_valid key_id
227
- valid = /^0x[A-Za-z0-9]{8,8}/.match(key_id)
228
- if valid.nil?
229
- err_msg = "Invalid key id. Keys should be in form of 0xDEADBEEF"
230
- raise Gem::OpenPGPException, err_msg
231
- end
232
- end
233
-
234
- def self.is_homedir_valid homedir
235
- if !File.exists? homedir
236
- raise OpenPGPException, "Bad homedir #{homedir.inspect}"
237
- end
238
- end
239
-
240
- def self.run_gpg args, data=nil, &block
241
- exit_status = nil
242
- status_file = Tempfile.new("status")
243
-
244
- full_gpg_command = "gpg --status-file #{status_file.path} #{args}"
245
- gpg_results = Open3.popen3(full_gpg_command) do |stdin, stdout, stderr, wait_thr|
246
- stdin.write data if data
247
- stdin.close
248
- exit_status = wait_thr.value
249
- GPGStatusParser.parse(status_file, &block)
250
- out = stdout.read()
251
- err = stderr.read()
252
- {:status => exit_status, :stdout => out, :err => err}
253
- end
254
- gpg_results
255
- ensure
256
- status_file.close
257
- status_file.unlink
258
- end
259
-
260
- def self.create_tempfile data
261
- temp_file = Tempfile.new("rubygems_gpg")
262
- temp_file.binmode
263
- temp_file.write(data)
264
- temp_file.close
265
- temp_file
266
- end
267
-
268
- def self.add_color s, color=:green
269
- color_code = case color
270
- when :green then "32"
271
- when :red then "31"
272
- when :yellow then "33"
273
- else raise RuntimeError, "Invalid color #{color.inspect}"
274
- end
275
-
276
- #TODO - NO-OP on windows
277
- "\033[#{color_code}m#{s}\033[0m"
278
- end
279
-
280
- def self.options
281
- @options ||= {}
282
- @options
283
- end
284
- end
1
+ require 'rubygems/openpgp/signing'
2
+ require 'rubygems/openpgp/verification'
@@ -0,0 +1,24 @@
1
+ module Gem::OpenPGP
2
+ extend Gem::UserInteraction
3
+
4
+ private
5
+
6
+ def self.did_gpg_error? gpg_results
7
+ if gpg_results[:status] != 0
8
+ say add_color("gpg returned unexpected status code", :red)
9
+ say add_color(gpg_results[:stdout], :yellow)
10
+ say add_color(gpg_results[:stderr], :red)
11
+ raise Gem::OpenPGPException, "gpg returned unexpected error code #{gpg_results[:status]}"
12
+ end
13
+ end
14
+
15
+ # Tests to see if gpg is installed and available.
16
+ def self.is_gpg_available
17
+ err_msg = "Unable to find a working gnupg installation. Make sure gnupg is installed and you can call 'gpg --version' from a command prompt."
18
+ `gpg --version`
19
+ raise Gem::OpenPGPException, err_msg if $? != 0
20
+ rescue Errno::ENOENT => ex
21
+ raise Gem::OpenPGPException, err_msg if $? != 0
22
+ end
23
+
24
+ end
@@ -0,0 +1,74 @@
1
+ module Gem::OpenPGP
2
+ # Capture used signing keys so we can see if they changed.
3
+ module KeyMaster
4
+ SETTING_DIR = ".rubygems-openpgp"
5
+ KEYMASTER_FILE = "known_gems"
6
+
7
+ def self.full_setting_filename
8
+ home_dir = ENV["HOME"] || ENV["HOMEPATH"]
9
+ File.join(home_dir, SETTING_DIR, KEYMASTER_FILE)
10
+ end
11
+
12
+ def self.touch_keymaster_file
13
+ home_dir = ENV["HOME"] || ENV["HOMEPATH"]
14
+
15
+ setting_dir = File.join(home_dir, SETTING_DIR)
16
+ Dir.mkdir(setting_dir, 0700) if !File.directory? setting_dir
17
+
18
+ if !File.exists?(full_setting_filename)
19
+ File.open(full_setting_filename,"w").close
20
+ end
21
+ end
22
+
23
+ def self.load_fingerprints
24
+ touch_keymaster_file
25
+
26
+ home_dir = ENV["HOME"] || ENV["HOMEPATH"]
27
+
28
+ fingerprints = {}
29
+ File.open(full_setting_filename) do |f|
30
+ f.readlines.each do |line|
31
+ gem, fingerprint = line.strip.split("\t")
32
+ fingerprints[gem] = fingerprint
33
+ end
34
+ end
35
+
36
+ fingerprints
37
+ end
38
+
39
+ def self.save_fingerprints fingerprints
40
+ touch_keymaster_file
41
+
42
+ File.open(full_setting_filename,"w") do |f|
43
+ fingerprints.each_pair do |gem, fingerprint|
44
+ f.puts("#{gem}\t#{fingerprint}")
45
+ end
46
+ end
47
+ end
48
+
49
+ def self.get_fingerprint gem_name
50
+ load_fingerprints[gem_name]
51
+ end
52
+
53
+ def self.add_fingerprint gem_name, fingerprint
54
+ fingerprints = load_fingerprints
55
+ fingerprints[gem_name] = fingerprint
56
+ save_fingerprints fingerprints
57
+ end
58
+
59
+ # Check that an existing gem fingerprint matches a given fingerprint.
60
+ # In the case we haven't seen the gem before, we'll add the fingerprint
61
+ # and won't complain.
62
+ def self.check_fingerprint gem_name, fingerprint
63
+ fingerprints = load_fingerprints
64
+
65
+ if !fingerprints.has_key?(gem_name)
66
+ add_fingerprint gem_name, fingerprint
67
+ return true
68
+ end
69
+
70
+ fingerprints[gem_name] == fingerprint
71
+ end
72
+
73
+ end
74
+ end
@@ -0,0 +1 @@
1
+ class Gem::OpenPGPException < RuntimeError; end
@@ -0,0 +1,10 @@
1
+ module Gem::OpenPGP
2
+ private
3
+
4
+ # Store options we get from command line here so sign/verify code
5
+ # can get to it.
6
+ def self.options
7
+ @options ||= {}
8
+ @options
9
+ end
10
+ end
@@ -0,0 +1,31 @@
1
+ require 'rubygems/command_manager'
2
+ require 'rubygems/gem_openpgp'
3
+
4
+ Gem::CommandManager.instance.register_command :sign
5
+
6
+ # gem build hooks
7
+ b = Gem::CommandManager.instance[:build]
8
+ b.add_option("--sign", "Sign gem with OpenPGP.") do |value, options|
9
+ Gem::OpenPGP.options[:sign] = true
10
+ end
11
+
12
+ b.add_option('--key KEY', "Specify key id if you don't want to use your default gpg key") do |key, options|
13
+ Gem::OpenPGP.options[:key] = key
14
+ end
15
+
16
+ class Gem::Commands::BuildCommand
17
+ alias_method :original_execute, :execute
18
+ def execute
19
+ original_execute
20
+
21
+ if Gem::OpenPGP.options[:sign]
22
+ gemspec = get_one_gem_name
23
+ if File.exist? gemspec then
24
+ spec = Gem::Specification.load gemspec
25
+ file_name = File.join(".", File.basename(spec.cache_file))
26
+ Gem::OpenPGP.sign_gem file_name, key=Gem::OpenPGP.options[:key]
27
+ end
28
+ end
29
+ end
30
+ end
31
+
@@ -0,0 +1,109 @@
1
+ require 'rubygems'
2
+ require 'rubygems/package'
3
+ require 'rubygems/user_interaction'
4
+ require 'gpg_status_parser'
5
+ require 'rubygems/openpgp/gpg_helpers'
6
+ require 'rubygems/openpgp/openpgpexception'
7
+ require 'shellwords'
8
+
9
+ module Gem::OpenPGP
10
+ extend Shellwords
11
+
12
+ # Given a string of data, generate and return a detached
13
+ # signature. By defualt, this will use your primary secret key.
14
+ # This can be overridden by specifying a key_id for another
15
+ # private key.
16
+ def self.detach_sign data, key_id=nil, homedir=nil
17
+ is_gpg_available
18
+ is_key_valid key_id if key_id
19
+ is_homedir_valid homedir if homedir
20
+
21
+ key_flag = ""
22
+ key_flag = "-u #{shellescape(key_id)}" if key_id
23
+
24
+ homedir_flag = ""
25
+ homedir_flag = "--homedir #{shellescape(homedir)}" if homedir
26
+
27
+ gpg_args = "#{key_flag} #{homedir_flag} --detach-sign --armor"
28
+ gpg_results = GPGStatusParser.run_gpg(gpg_args, data)
29
+ did_gpg_error? gpg_results
30
+
31
+ gpg_results[:stdout]
32
+ end
33
+
34
+ # Signs an existing gemfile by iterating the tar'ed up contents,
35
+ # and signing any contents. creating a new file with original contents
36
+ # and OpenPGP sigs. The OpenPGP sigs are saved as .asc files so they
37
+ # won't conflict with X509 sigs.
38
+ #
39
+ # Optional param "key" allows you to use a different private
40
+ # key than the GPG default.
41
+ def self.sign_gem gem, key=nil, homedir=nil
42
+ unsigned_gem = gem + ".unsigned"
43
+
44
+ begin
45
+ FileUtils.mv gem, unsigned_gem
46
+ rescue Errno::ENOENT => ex
47
+ raise Gem::CommandLineError, "The gem #{gem} does not seem to exist. (#{ex.message})"
48
+ end
49
+
50
+ unsigned_gem_file = File.open(unsigned_gem, "r")
51
+ signed_gem_file = File.open(gem, "w")
52
+
53
+ signed_gem = Gem::Package::TarWriter.new(signed_gem_file)
54
+
55
+ Gem::Package::TarReader.new(unsigned_gem_file).each do |f|
56
+
57
+ if f.full_name[-4..-1] == ".asc"
58
+ say("Skipping old OpenPGP signature file #{f.full_name}")
59
+ next
60
+ end
61
+
62
+ file_contents = f.read()
63
+
64
+ # Copy file no matter what
65
+ signed_gem.add_file(f.full_name, 0644) do |outfile|
66
+ outfile.write(file_contents)
67
+ end
68
+
69
+ # Only sign if it's really part of the gem and not
70
+ # a X.509 sig
71
+ if f.full_name[-3..-1] == ".gz"
72
+ say add_color("Signing #{f.full_name.inspect}...",:green)
73
+ signed_gem.add_file(f.full_name + ".asc", 0644) do |outfile|
74
+ outfile.write(Gem::OpenPGP.detach_sign(file_contents,key,homedir))
75
+ end
76
+ elsif f.full_name[-4..-1] != ".sig"
77
+ say add_color("Not signing #{f.full_name.inspect}. Didn't expect to see that...",:yellow)
78
+ end
79
+ end
80
+
81
+ signed_gem_file.close
82
+ unsigned_gem_file.close
83
+ File.delete unsigned_gem_file
84
+
85
+ rescue Exception => ex
86
+ if unsigned_gem_file
87
+ FileUtils.mv unsigned_gem_file, gem
88
+ end
89
+
90
+ raise
91
+ end
92
+
93
+ private
94
+
95
+ def self.is_key_valid key_id
96
+ valid = /^0x[A-Za-z0-9]{8,8}/.match(key_id)
97
+ if valid.nil?
98
+ err_msg = "Invalid key id. Keys should be in form of 0xDEADBEEF"
99
+ raise Gem::OpenPGPException, err_msg
100
+ end
101
+ end
102
+
103
+ def self.is_homedir_valid homedir
104
+ if !File.exists? homedir
105
+ raise OpenPGPException, "Bad homedir #{homedir.inspect}"
106
+ end
107
+ end
108
+
109
+ end
@@ -0,0 +1,180 @@
1
+ require 'rubygems'
2
+ require 'rubygems/package'
3
+ require 'rubygems/user_interaction'
4
+
5
+ require 'tempfile'
6
+ require 'rbconfig'
7
+
8
+ require 'gpg_status_parser'
9
+
10
+ require 'rubygems/openpgp/keymaster'
11
+ require 'rubygems/openpgp/options'
12
+ require 'rubygems/openpgp/gpg_helpers'
13
+ require 'rubygems/openpgp/openpgpexception'
14
+
15
+ module Gem::OpenPGP
16
+ extend Gem::UserInteraction
17
+
18
+ # Given a string containing data, and a string containing
19
+ # a detached signature, verify the data. If we can't verify
20
+ # then raise an exception.
21
+ #
22
+ # Optionally tell gpg to retrive the key if it's not provided
23
+ #
24
+ # returns the fingerprint used to sign the file
25
+ def self.verify file_name, data, sig, get_key=false, homedir=nil
26
+ is_gpg_available
27
+ is_homedir_valid homedir if homedir
28
+
29
+ data_file = create_tempfile data
30
+ sig_file = create_tempfile sig
31
+
32
+ get_key_params = "--keyserver pool.sks-keyservers.net --keyserver-options auto-key-retrieve"
33
+ get_key_params = "" if get_key != true
34
+
35
+ homedir_flags = ""
36
+ homedir_flags = "--homedir #{homedir}" if homedir
37
+
38
+ gpg_args = "#{get_key_params} #{homedir_flags} --verify #{sig_file.path} #{data_file.path}"
39
+
40
+ status_info = {:file_name => file_name}
41
+ gpg_results = GPGStatusParser.run_gpg(gpg_args) { |message| verify_extract_status_info(message, status_info) }
42
+
43
+ if status_info[:failure]
44
+ say add_color(status_info[:failure], :red)
45
+ raise Gem::OpenPGPException, "Fail!"
46
+ else
47
+ verify_check_sig status_info
48
+ end
49
+
50
+ did_gpg_error? gpg_results
51
+
52
+ status_info[:primary_key_fingerprint]
53
+ end
54
+
55
+ def self.verify_gem gem, get_key=false, homedir=nil
56
+ raise Gem::CommandLineError, "Gem #{gem} not found." if !File.exists?(gem)
57
+
58
+ gem_name = if Gem::VERSION[0..1] == "2." #gotta be a better way
59
+ Gem::Package.new(gem).spec.name
60
+ else
61
+ Gem::Format.from_file_by_path(gem).spec.name
62
+ end
63
+
64
+ say("Verifying #{gem_name}...")
65
+
66
+ file = File.open(gem,"r")
67
+
68
+ fingerprints = []
69
+ tar_files = {}
70
+
71
+ Gem::Package::TarReader.new(file).each do |f|
72
+ tar_files[f.full_name] = f.read()
73
+ end
74
+
75
+ tar_files.keys.each do |file_name|
76
+ next if [".asc",".sig"].include? file_name[-4..-1]
77
+
78
+ if file_name[-3..-1] != ".gz"
79
+ say add_color("Skipping #{file_name}. Only expected .gz files...", :yellow)
80
+ next
81
+ end
82
+
83
+ sig_file_name = file_name + ".asc"
84
+ if !tar_files.has_key? sig_file_name
85
+ say add_color("WARNING!!! No sig found for #{file_name}", :red)
86
+ raise Gem::OpenPGPException, "Can't verify without sig, aborting!!!"
87
+ end
88
+
89
+ begin
90
+ fingerprints << Gem::OpenPGP.verify(file_name, tar_files[file_name], tar_files[sig_file_name], get_key, homedir)
91
+ rescue Gem::OpenPGPException => ex
92
+ color_code = "31"
93
+ say add_color(ex.message, :red)
94
+ raise
95
+ end
96
+ end
97
+
98
+ # Verify fingerprint
99
+ fingerprints.uniq.each do |fp|
100
+ verify_gem_check_fingerprint gem_name, fp
101
+ end
102
+
103
+ ensure
104
+ file.close unless file.nil?
105
+ end
106
+
107
+ private
108
+
109
+ # Extract the info we care about, throw away the rest
110
+ def self.verify_extract_status_info message, status_info
111
+ case message.status
112
+ when :GOODSIG, :BADSIG, :ERRSIG
113
+ status_info[:good_or_bad] = message.status
114
+ status_info[:uid] = (message.args[:username] || "").strip
115
+
116
+ when :SIG_ID
117
+ when :VALIDSIG, :EXPSIG, :BADSIG
118
+ status_info[:sig_status] = message.status
119
+ status_info[:primary_key] = "0x#{message.args[:primary_key_fpr][-9..-1]}"
120
+ status_info[:primary_key_fingerprint] = message.args[:primary_key_fpr]
121
+ when :TRUST_UNDEFINED, :TRUST_NEVER, :TRUST_MARGINAL, :TRUST_FULLY, :TRUST_ULTIMATE
122
+ status_info[:trust_status] = message.status
123
+ when :NO_PUBKEY
124
+ status_info[:failure] = "You don't have the public key. Use --get-key to automagically retrieve from keyservers"
125
+ when :IMPORTED, :IMPORT_OK, :IMPORT_RES
126
+ #silently_ignore
127
+ when :KEYEXPIRED, :SIGEXPIRED
128
+ # recalculating trust db, ignore.
129
+ else
130
+ puts "unexpected message: #{message.status} #{message.args.inspect}"
131
+ end
132
+ end
133
+
134
+ # Print info about the sig, check that we like it, and possibly abort
135
+ def self.verify_check_sig status_info
136
+ sig_msg = "Signature for #{status_info[:file_name]} from user #{status_info[:uid]} key #{status_info[:primary_key]} is #{status_info[:good_or_bad]}, #{status_info[:sig_status]} and #{status_info[:trust_status]}"
137
+ if status_info[:trust_status] == :TRUST_NEVER
138
+ say add_color(sig_msg, :red)
139
+ raise Gem::OpenPGPException, "Never Trusted. Won't install."
140
+ elsif status_info[:trust_status] == :TRUST_UNDEFINED
141
+ say add_color(sig_msg, :yellow)
142
+ if options[:trust] && !options[:no_trust]
143
+ raise Gem::OpenPGPException, "Trust Undefined and you've specified --trust. Won't install."
144
+ end
145
+ else
146
+ say add_color(sig_msg , :green)
147
+ end
148
+ end
149
+
150
+ def self.verify_gem_check_fingerprint gem_name, fingerprint
151
+ if !Gem::OpenPGP::KeyMaster.check_fingerprint(gem_name, fingerprint)
152
+ raise Gem::OpenPGPException, "Gem #{gem_name} fingerprint #{fingerprint} didn't match fingerprint in #{Gem::OpenPGP::KeyMaster.full_setting_filename}. Won't install!"
153
+ end
154
+ end
155
+
156
+ def self.create_tempfile data
157
+ temp_file = Tempfile.new("rubygems_gpg")
158
+ temp_file.binmode
159
+ temp_file.write(data)
160
+ temp_file.close
161
+ temp_file
162
+ end
163
+
164
+ def self.add_color s, color=:green
165
+ color_code = case color
166
+ when :green then "32"
167
+ when :red then "31"
168
+ when :yellow then "33"
169
+ else raise RuntimeError, "Invalid color #{color.inspect}"
170
+ end
171
+
172
+ if (RbConfig::CONFIG['host_os'] =~ /mswin|mingw/)
173
+ s # no colors on windows
174
+ else
175
+ "\033[#{color_code}m#{s}\033[0m"
176
+ end
177
+
178
+ end
179
+
180
+ end
@@ -0,0 +1,45 @@
1
+ require 'rubygems/command_manager'
2
+ require 'rubygems/gem_openpgp'
3
+
4
+ Gem::CommandManager.instance.register_command :verify
5
+
6
+ # gem install hooks
7
+ i = Gem::CommandManager.instance[:install]
8
+ i.add_option("--verify",
9
+ 'Verifies a local gem that has been signed via OpenPGP.' +
10
+ 'This helps to ensure the gem has not been tampered with in transit.') do |value, options|
11
+ Gem::OpenPGP.options[:verify] = true
12
+ end
13
+
14
+ i.add_option("--no-verify",
15
+ "Don't verify a gem, even if --verify has previously been specified") do |value, options|
16
+ Gem::OpenPGP.options[:no_verify] = true
17
+ end
18
+
19
+ i = Gem::CommandManager.instance[:install]
20
+ i.add_option("--trust",
21
+ 'Enforce gnupg trust settings. Only install if trusted.') do |value, options|
22
+ Gem::OpenPGP.options[:trust] = true
23
+ end
24
+
25
+ i.add_option("--no-trust",
26
+ "Ignoure gnupg trust settings, even if --trust has previously been specified") do |value, options|
27
+ Gem::OpenPGP.options[:no_trust] = true
28
+ end
29
+
30
+ i.add_option('--get-key', "If the key is not available, download it from a keyserver") do |key, options|
31
+ Gem::OpenPGP.options[:get_key] = true
32
+ end
33
+
34
+ Gem.pre_install do |installer|
35
+ begin
36
+ # --no-verify overrides --verify
37
+ if Gem::OpenPGP.options[:verify] && !Gem::OpenPGP.options[:no_verify]
38
+ Gem::OpenPGP.verify_gem(installer.gem,
39
+ Gem::OpenPGP.options[:get_key])
40
+ end
41
+ rescue Gem::OpenPGPException => ex
42
+ installer.alert_error(ex.message)
43
+ installer.terminate_interaction(1)
44
+ end
45
+ end
@@ -1,72 +1,4 @@
1
- require 'rubygems/command_manager'
2
1
  require 'rubygems/gem_openpgp'
2
+ require 'rubygems/openpgp/sign_plugins'
3
+ require 'rubygems/openpgp/verify_plugins'
3
4
 
4
- Gem::CommandManager.instance.register_command :sign
5
- Gem::CommandManager.instance.register_command :verify
6
-
7
- # gem build hooks
8
- b = Gem::CommandManager.instance[:build]
9
- b.add_option("--sign", "Sign gem with OpenPGP.") do |value, options|
10
- Gem::OpenPGP.options[:sign] = true
11
- end
12
-
13
- b.add_option('--key KEY', "Specify key id if you don't want to use your default gpg key") do |key, options|
14
- Gem::OpenPGP.options[:key] = key
15
- end
16
-
17
- class Gem::Commands::BuildCommand
18
- alias_method :original_execute, :execute
19
- def execute
20
- original_execute
21
-
22
- if Gem::OpenPGP.options[:sign]
23
- gemspec = get_one_gem_name
24
- if File.exist? gemspec then
25
- spec = Gem::Specification.load gemspec
26
- file_name = File.join(".", File.basename(spec.cache_file))
27
- Gem::OpenPGP.sign_gem file_name, key=Gem::OpenPGP.options[:key]
28
- end
29
- end
30
- end
31
- end
32
-
33
- # gem install hooks
34
- i = Gem::CommandManager.instance[:install]
35
- i.add_option("--verify",
36
- 'Verifies a local gem that has been signed via OpenPGP.' +
37
- 'This helps to ensure the gem has not been tampered with in transit.') do |value, options|
38
- Gem::OpenPGP.options[:verify] = true
39
- end
40
-
41
- i.add_option("--no-verify",
42
- "Don't verify a gem, even if --verify has previously been specified") do |value, options|
43
- Gem::OpenPGP.options[:no_verify] = true
44
- end
45
-
46
- i = Gem::CommandManager.instance[:install]
47
- i.add_option("--trust",
48
- 'Enforce gnupg trust settings. Only install if trusted.') do |value, options|
49
- Gem::OpenPGP.options[:trust] = true
50
- end
51
-
52
- i.add_option("--no-trust",
53
- "Ignoure gnupg trust settings, even if --trust has previously been specified") do |value, options|
54
- Gem::OpenPGP.options[:no_trust] = true
55
- end
56
-
57
- i.add_option('--get-key', "If the key is not available, download it from a keyserver") do |key, options|
58
- Gem::OpenPGP.options[:get_key] = true
59
- end
60
-
61
- Gem.pre_install do |installer|
62
- begin
63
- # --no-verify overrides --verify
64
- if Gem::OpenPGP.options[:verify] && !Gem::OpenPGP.options[:no_verify]
65
- Gem::OpenPGP.verify_gem(installer.gem,
66
- Gem::OpenPGP.options[:get_key])
67
- end
68
- rescue Gem::OpenPGPException => ex
69
- installer.alert_error(ex.message)
70
- installer.terminate_interaction(1)
71
- end
72
- end
@@ -0,0 +1,31 @@
1
+ require 'test/unit'
2
+ require 'mocha/setup'
3
+
4
+ require 'rubygems/openpgp/keymaster'
5
+
6
+ class KeymasterTest < Test::Unit::TestCase
7
+ def setup
8
+ Gem::OpenPGP::KeyMaster.stubs(:load_fingerprints => {"foo" => "DEADBEEF"},
9
+ :save_fingerprints => nil)
10
+ end
11
+
12
+
13
+ def test_good_fingerprint
14
+ assert_block("Good fingerprint test") do
15
+ Gem::OpenPGP::KeyMaster.check_fingerprint("foo", "DEADBEEF") == true
16
+ end
17
+ end
18
+
19
+ def test_bad_fingerprint
20
+ assert_block("Bad fingerprint test") do
21
+ Gem::OpenPGP::KeyMaster.check_fingerprint("foo", "C00FFEE") != true
22
+ end
23
+ end
24
+
25
+ def test_new_gem
26
+ assert_block("New gem test") do
27
+ Gem::OpenPGP::KeyMaster.check_fingerprint("bar", "C00FFEE") == true
28
+ end
29
+ end
30
+
31
+ end
@@ -1,4 +1,6 @@
1
1
  require 'test/unit'
2
+ require 'mocha/setup'
3
+
2
4
  require 'rubygems_plugin'
3
5
  require 'rubygems/gem_openpgp'
4
6
  require 'tmpdir'
@@ -19,6 +21,8 @@ class RubygemsPluginTest < Test::Unit::TestCase
19
21
  end
20
22
 
21
23
  def test_gem_sign_and_verify
24
+ Gem::OpenPGP.stubs(:verify_gem_check_fingerprint => true)
25
+
22
26
  in_tmp_gpg_homedir do |gpg_home|
23
27
  assert_raise Gem::OpenPGPException do
24
28
  Gem::OpenPGP.verify_gem UNSIGNED_GEM, false, gpg_home
@@ -41,12 +45,12 @@ class RubygemsPluginTest < Test::Unit::TestCase
41
45
 
42
46
  sig = Gem::OpenPGP.detach_sign data, key_id=nil, homedir=gpg_home
43
47
  assert_nothing_raised do
44
- Gem::OpenPGP.verify data, sig, false, homedir=gpg_home
48
+ Gem::OpenPGP.verify "<file>", data, sig, false, homedir=gpg_home
45
49
  end
46
50
 
47
51
  # BAD SIG
48
52
  assert_raise(Gem::OpenPGPException) do
49
- Gem::OpenPGP.verify data + "\n", sig, false, homedir=gpg_home
53
+ Gem::OpenPGP.verify "<file>", data + "\n", sig, false, homedir=gpg_home
50
54
  end
51
55
  end
52
56
  end
metadata CHANGED
@@ -1,76 +1,96 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: rubygems-openpgp
3
- version: !ruby/object:Gem::Version
4
- prerelease:
5
- version: 0.4.0
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
6
5
  platform: ruby
7
- authors:
6
+ authors:
8
7
  - Grant Olson
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
-
13
- date: 2013-02-23 00:00:00 Z
14
- dependencies:
15
- - !ruby/object:Gem::Dependency
11
+ date: 2013-03-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
16
14
  name: gpg_status_parser
17
- prerelease: false
18
- requirement: &id001 !ruby/object:Gem::Requirement
19
- none: false
20
- requirements:
21
- - - ">="
22
- - !ruby/object:Gem::Version
23
- version: 0.3.0
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.4.0
24
20
  type: :runtime
25
- version_requirements: *id001
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.4.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: mocha
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: 0.13.2
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: 0.13.2
26
41
  description: Digitally sign gems via OpenPGP.
27
42
  email: kgo@grant-olson.net
28
43
  executables: []
29
-
30
44
  extensions: []
31
-
32
- extra_rdoc_files:
45
+ extra_rdoc_files:
33
46
  - README.md
34
- files:
47
+ files:
35
48
  - LICENSE
36
49
  - Rakefile
37
50
  - lib/rubygems_plugin.rb
38
51
  - lib/rubygems/commands/verify_command.rb
39
52
  - lib/rubygems/commands/sign_command.rb
40
53
  - lib/rubygems/gem_openpgp.rb
54
+ - lib/rubygems/openpgp/gpg_helpers.rb
55
+ - lib/rubygems/openpgp/options.rb
56
+ - lib/rubygems/openpgp/signing.rb
57
+ - lib/rubygems/openpgp/verification.rb
58
+ - lib/rubygems/openpgp/keymaster.rb
59
+ - lib/rubygems/openpgp/verify_plugins.rb
60
+ - lib/rubygems/openpgp/sign_plugins.rb
61
+ - lib/rubygems/openpgp/openpgpexception.rb
41
62
  - README.md
63
+ - test/test_keymaster.rb
42
64
  - test/test_rubygems-openpgp.rb
43
65
  - test/pablo_escobar_seckey.asc
44
66
  - test/pablo_escobar_pubkey.asc
45
67
  - test/unsigned_hola-0.0.0.gem
46
- homepage: https://github.com/grant-olson/rubygems-openpgp
47
- licenses:
68
+ homepage: https://rubygems-openpgp-ca.org
69
+ licenses:
48
70
  - BSD 3 Clause
71
+ metadata: {}
49
72
  post_install_message:
50
73
  rdoc_options: []
51
-
52
- require_paths:
74
+ require_paths:
53
75
  - lib
54
- required_ruby_version: !ruby/object:Gem::Requirement
55
- none: false
56
- requirements:
57
- - - ">="
58
- - !ruby/object:Gem::Version
59
- version: "0"
60
- required_rubygems_version: !ruby/object:Gem::Requirement
61
- none: false
62
- requirements:
63
- - - ">="
64
- - !ruby/object:Gem::Version
65
- version: "0"
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - '>='
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ required_rubygems_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
66
86
  requirements: []
67
-
68
87
  rubyforge_project:
69
- rubygems_version: 1.8.24
88
+ rubygems_version: 2.0.0.rc.2
70
89
  signing_key:
71
- specification_version: 3
90
+ specification_version: 4
72
91
  summary: Sign gems via OpenPGP
73
- test_files:
92
+ test_files:
93
+ - test/test_keymaster.rb
74
94
  - test/test_rubygems-openpgp.rb
75
95
  - test/pablo_escobar_seckey.asc
76
96
  - test/pablo_escobar_pubkey.asc
metadata.gz.asc CHANGED
@@ -1,11 +1,11 @@
1
1
  -----BEGIN PGP SIGNATURE-----
2
- Version: GnuPG/MacGPG2 v2.0.17 (Darwin)
2
+ Version: GnuPG v1.4.11 (GNU/Linux)
3
3
 
4
- iQEcBAABCgAGBQJRKNOhAAoJEP5F5V2hilTWcBgIAIob5FD4SK6UxfnXQk0U+5nP
5
- huK6hsanhLbH3m3fqz2BkB/WBVonF0sqO2zNIw+qxnIl/r426iXmxTfNYF3F+0Ri
6
- mP7YF+HBNfuElraRBBKUXSCQoB0yxqLpx3mcDwjB//0Q9CWXdeCLzefbs7+4ca4q
7
- rBezyxSxRXun2qBMBs7FSZYkC6rsP58EgTi555WfxymiqEZ9fEi1r4rEM37F+z2w
8
- FBMlYjTJhATOIhHTi8nFQoGgjFcQG9a7cgABHGZwB9IR6k5O6IeTHVXCQvOlg4au
9
- cj6HKS8YKpJGZPlPIJVpLmW9UhUQukg4AwTpPAHIeIiwZYahitQtm0Ra5MnSJtc=
10
- =bh2W
4
+ iQEcBAABAgAGBQJRPJV0AAoJEP5F5V2hilTWUlgH/34nV018I/aZnDLYCQyFqJSJ
5
+ Ek96fj//cmCbbN55YG8ptgB4NyZBBsc0Nvhe7CEsKFbcZJoutqoPXKeE0A3Qe9lp
6
+ gnogg7mwcm5ndn2dxBH1fd2it0fS3vG09i5r7MMqsE5LzaLJ6OcmYWmNnV2HiPTr
7
+ LQeqyC4hFyweIYpgO8sgdCDjrAaWDTBfoHmvHZ8xHZRZypJP45EdQ/+brp+DZmKu
8
+ K5E7LpV9VOFFobb91a4RYuzqulSK5tQELSXkh2IcDPDyEu0FZl7iP3S6N6xuDUQc
9
+ Zvtg1UpyMWs31DlCeivJhmQbFBbaVgjcqyHrEXnMWAIRq4XQxA7sZcxgg+K9j7k=
10
+ =1hJH
11
11
  -----END PGP SIGNATURE-----