simple-gnupg-keyserver 1.0.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.
- data/.gitignore +6 -0
- data/History.txt +7 -0
- data/README.rdoc +129 -0
- data/Rakefile +39 -0
- data/lib/simpleHKP.rb +504 -0
- metadata +94 -0
data/History.txt
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
= rGem-simple-gnupg-keyserver
|
2
|
+
|
3
|
+
home :: https://github.com/stephengaito/rGem-simple-gnupg-keyserver
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
SimpleHKP is a simple Ruby/rack based GnuPG HKP key server.
|
8
|
+
|
9
|
+
Its sole task is to supply a limited number of GnuPG _public_ keys
|
10
|
+
to/from MonkeySphere on a limited number of machines/servers. It is
|
11
|
+
_not_ meant to server hundreds of keys, so it is not meant as a truely
|
12
|
+
public key server.
|
13
|
+
|
14
|
+
If you need to run your own _large_ scale production Key Sever then use
|
15
|
+
{SKS}[https://bitbucket.org/skskeyserver/sks-keyserver/wiki/Home]. You
|
16
|
+
might want to read Paul Bauer's {How To Setup A Free PGP Key Server in
|
17
|
+
Ubuntu}[http://www.bauer-power.net/2010/05/how-to-setup-free-pgp-key-server-in.html].
|
18
|
+
|
19
|
+
=== Key storage
|
20
|
+
|
21
|
+
Since this is a _simple_ key server, all keys are stored as flat
|
22
|
+
ASCII armored files in the "keys" directory.
|
23
|
+
|
24
|
+
=== Key search
|
25
|
+
|
26
|
+
For each uploaded key, the key's "gpg2 --with-fingerprint --with-colon"
|
27
|
+
output (key data) is stored in an internal ruby Hash using the key's
|
28
|
+
file name as hash key. All searches become Ruby regular expressions
|
29
|
+
which are matched against each key's key data.
|
30
|
+
|
31
|
+
=== Security:
|
32
|
+
|
33
|
+
_ALL_ uploaded key data is passed through the external gpg2 command.
|
34
|
+
Any security weaknesses of gpg2 are weaknesses of this SimpleHKP rack
|
35
|
+
application.
|
36
|
+
|
37
|
+
This rack application is written in Ruby, so any security weaknesses of
|
38
|
+
Ruby and/or the Ruby web-server you are using are also weaknesses of
|
39
|
+
this SimpleHKP rack application.
|
40
|
+
|
41
|
+
By default all files associated with the normal running of the
|
42
|
+
SimpleHKP rack application are stored in the "simpleHKP" directory of
|
43
|
+
the current working directory. This "simpleHKP" directory _SHOULD_
|
44
|
+
_NOT_ be accessible from any other web-server (such as NGinx).
|
45
|
+
|
46
|
+
This rack application is intentionally as self contained and simple as
|
47
|
+
possible. If you have any security concerns, this rack application is
|
48
|
+
easily readable.
|
49
|
+
|
50
|
+
== SYNOPSIS:
|
51
|
+
|
52
|
+
A typical rackup.ru file might be:
|
53
|
+
|
54
|
+
require 'simpleHKP'
|
55
|
+
|
56
|
+
simpleHKPoptions = {
|
57
|
+
... some options ...
|
58
|
+
}
|
59
|
+
|
60
|
+
run SimpleHKP.new(simpleHKPoptions)
|
61
|
+
|
62
|
+
Your webserver should be configured to bind to ports 11371 (for use as
|
63
|
+
an HKP keyserver) and 80 (for use by standard browsers).
|
64
|
+
|
65
|
+
Options can be set by creating a SimpleHKP instance with a hash of
|
66
|
+
key/value pairs.
|
67
|
+
|
68
|
+
The current default options are:
|
69
|
+
|
70
|
+
defaultOptions = {
|
71
|
+
'debug' => false, # should debug output to logged?
|
72
|
+
'simpleHKPdir' => 'simpleHKP', # base disk path to simpleHKP disk space
|
73
|
+
'keyDir' => 'keys', # subdir to key storage directory
|
74
|
+
'mediaDir' => 'media', # subdir to any css, js, images etc
|
75
|
+
'htmlDir' => 'html', # subdir to html partials
|
76
|
+
'mimeMap' => { # a file ext to mime mapping
|
77
|
+
'css' => 'text/css',
|
78
|
+
'html' => 'text/html',
|
79
|
+
'js' => 'text/javascript'
|
80
|
+
}
|
81
|
+
}
|
82
|
+
|
83
|
+
The following HTML partials can be used to over-ride the web pages for
|
84
|
+
the use of humans:
|
85
|
+
|
86
|
+
* header.html
|
87
|
+
* defaultBody.html
|
88
|
+
* lookupForm.html
|
89
|
+
* uploadForm.html
|
90
|
+
* footer.html
|
91
|
+
|
92
|
+
== REQUIREMENTS:
|
93
|
+
|
94
|
+
There are explicitly no external Ruby requirements other than Ruby and
|
95
|
+
a Ruby webserver (such as puma)
|
96
|
+
|
97
|
+
The webserver *requires* GnuPG2 installed.
|
98
|
+
|
99
|
+
== INSTALL:
|
100
|
+
|
101
|
+
To install the simple-gnupg-keyserver gem:
|
102
|
+
|
103
|
+
$ gem install simple-gnupg-keyserver
|
104
|
+
|
105
|
+
== LICENSE:
|
106
|
+
|
107
|
+
(The MIT License)
|
108
|
+
|
109
|
+
Copyright (c) 2015 Stephen Gaito
|
110
|
+
|
111
|
+
Permission is hereby granted, free of charge, to any person obtaining a
|
112
|
+
copy of this software and associated documentation files (the
|
113
|
+
'Software'), to deal in the Software without restriction, including
|
114
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
115
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
116
|
+
permit persons to whom the Software is furnished to do so, subject to
|
117
|
+
the following conditions:
|
118
|
+
|
119
|
+
The above copyright notice and this permission notice shall be included
|
120
|
+
in all copies or substantial portions of the Software.
|
121
|
+
|
122
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
123
|
+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
124
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
125
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
126
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
127
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
128
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
129
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
|
3
|
+
# To release this ruby gem type:
|
4
|
+
# rake release VERSION=x.y.z
|
5
|
+
# where x.y.z is the appropriate version number of this gem.
|
6
|
+
|
7
|
+
require 'rubygems'
|
8
|
+
require 'hoe'
|
9
|
+
|
10
|
+
# Hoe.plugin :compiler
|
11
|
+
# Hoe.plugin :gem_prelude_sucks
|
12
|
+
# Hoe.plugin :inline
|
13
|
+
# Hoe.plugin :minitest
|
14
|
+
# Hoe.plugin :racc
|
15
|
+
# Hoe.plugin :rubyforge
|
16
|
+
|
17
|
+
# generate the Manifest.txt file (before we invoke Hoe.spec)
|
18
|
+
manifest = FileList[
|
19
|
+
'.gitignore',
|
20
|
+
'History.*',
|
21
|
+
'README.*',
|
22
|
+
'Rakefile',
|
23
|
+
'lib/**/*.rb',
|
24
|
+
'test/**/*.rb',
|
25
|
+
];
|
26
|
+
File.open('Manifest.txt', 'w') do | manifestFile |
|
27
|
+
manifestFile.puts("Manifest.txt");
|
28
|
+
manifestFile.write(manifest.to_a.join("\n"));
|
29
|
+
end
|
30
|
+
|
31
|
+
# For dependency documentation see:
|
32
|
+
# http://guides.rubygems.org/patterns/
|
33
|
+
|
34
|
+
Hoe.spec 'simple-gnupg-keyserver' do
|
35
|
+
developer('Stephen Gaito', 'stephen@perceptisys.co.uk')
|
36
|
+
license 'MIT'
|
37
|
+
end
|
38
|
+
|
39
|
+
# vim: syntax=ruby
|
data/lib/simpleHKP.rb
ADDED
@@ -0,0 +1,504 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'pp'
|
3
|
+
#require 'yaml'
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
# This code was inspired by:
|
7
|
+
# Sebi2020's Informatikonline Easy-HKP
|
8
|
+
# https://github.com/Sebi2020/easy-hkp
|
9
|
+
|
10
|
+
# It conforms to:
|
11
|
+
# The OpenPGP HTTP Keyserver Protocol (HKP)
|
12
|
+
# draft-shaw-openpgp-hkp-00.txt
|
13
|
+
# http://tools.ietf.org/html/draft-shaw-openpgp-hkp-00
|
14
|
+
|
15
|
+
# See also the doc/DETAILS file in the gnupg2 source code
|
16
|
+
|
17
|
+
# Copyright (C) 2015 Stephen Gaito
|
18
|
+
#
|
19
|
+
# (The MIT License)
|
20
|
+
#
|
21
|
+
# Copyright (c) 2015 Stephen Gaito
|
22
|
+
#
|
23
|
+
# Permission is hereby granted, free of charge, to any person obtaining a
|
24
|
+
# copy of this software and associated documentation files (the
|
25
|
+
# 'Software'), to deal in the Software without restriction, including
|
26
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
27
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
28
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
29
|
+
# the following conditions:
|
30
|
+
#
|
31
|
+
# The above copyright notice and this permission notice shall be included
|
32
|
+
# in all copies or substantial portions of the Software.
|
33
|
+
#
|
34
|
+
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
35
|
+
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
36
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
37
|
+
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
38
|
+
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
39
|
+
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
40
|
+
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
41
|
+
|
42
|
+
class SimpleHKP
|
43
|
+
|
44
|
+
VERSION = "1.0.0"
|
45
|
+
|
46
|
+
def loadKeys
|
47
|
+
@keyLookupData = Hash.new
|
48
|
+
puts "SimpleHKP: loading keys" if @debug
|
49
|
+
Dir.glob(@keyDir+'/**/*.asc') do | aFile |
|
50
|
+
puts aFile if @debug
|
51
|
+
@keyLookupData[aFile] = `gpg2 --with-fingerprint --with-colons #{aFile}`
|
52
|
+
puts @keyLookupData[aFile] if @debug
|
53
|
+
end
|
54
|
+
puts "SimpleHKP: finished loading keys" if @debug
|
55
|
+
end
|
56
|
+
|
57
|
+
def loadHtml
|
58
|
+
puts "SimpleHKP: loading html partials" if @debug
|
59
|
+
Dir.chdir(@htmlDir) do
|
60
|
+
@headerHtml = "<html><head></head><body class=\"simpleHKP-body\">\n"
|
61
|
+
@headerHtml << "<p><a href=\"/\">SimpleHKP</a></p>"
|
62
|
+
@headerHtml = File.open('header.html','r').read if
|
63
|
+
File.exists?('header.html')
|
64
|
+
puts @headerHtml if @debug
|
65
|
+
|
66
|
+
@defaultBody = ""
|
67
|
+
@defaultBody << "<h1 class=\"simpleHKP-welcome\">Welcome to SimpleHKP</h1>\n"
|
68
|
+
@defaultBody << "<ul class=\"simpleHKP-tasksList\">\n"
|
69
|
+
@defaultBody << " <li class=\"simpleHKP-taskItem\"><a href=\"lookup?op=form\">Search</a></li>\n"
|
70
|
+
@defaultBody << " <li class=\"simpleHKP-taskItem\"><a href=\"add\">Upload new key</a></li>\n"
|
71
|
+
@defaultBody << " <li class=\"simpleHKP-taskItem\"><a href=\"reload\">Reload existing keys</a></li>\n"
|
72
|
+
@defaultBody << "</ul>\n"
|
73
|
+
@defaultBody = File.open('defaultBody.html','r').read if
|
74
|
+
File.exists?('defaultBody.html')
|
75
|
+
puts @defaultBody if @debug
|
76
|
+
|
77
|
+
@lookupForm = ""
|
78
|
+
@lookupForm << "<div class=\"simpleHKP-lookupFormDiv\">\n"
|
79
|
+
@lookupForm << " <h1 class=\"simpleHKP-lookupFormTitle\">Search GnuPG keys</h1>\n"
|
80
|
+
@lookupForm << " <form action=\"lookup\" method=\"get\" id=\"simpleHKP-lookupForm\">\n"
|
81
|
+
@lookupForm << " <input type=\"text\" name=\"search\" size=\"80\" />\n"
|
82
|
+
@lookupForm << " <input type=\"hidden\" name=\"op\" value=\"index\" />\n"
|
83
|
+
@lookupForm << " <input type=\"submit\" value=\"Search\" />\n"
|
84
|
+
@lookupForm << " </form>\n"
|
85
|
+
@lookupForm << "</div>\n"
|
86
|
+
@lookupForm = File.open('lookupForm.html','r').read if
|
87
|
+
File.exists?('lookupForm.html')
|
88
|
+
puts @lookupForm if @debug
|
89
|
+
|
90
|
+
@uploadForm = ""
|
91
|
+
@uploadForm << "<div class=\"simpleHKP-uploadFormDiv\">\n"
|
92
|
+
@uploadForm << " <h1 class=\"simpleHKP-uploadFormTitle\">Paste GnuPG key to be uploaded below</h1>\n"
|
93
|
+
@uploadForm << " <form action=\"add\" method=\"post\" id=\"simpleHKP-uploadForm\">\n"
|
94
|
+
@uploadForm << " <textarea name=\"keytext\" form=\"simpleHKP-uploadForm\" rows = \"30\" cols=\"70\" ></textarea>\n"
|
95
|
+
@uploadForm << " <input type=\"submit\" value=\"Upload\" />\n"
|
96
|
+
@uploadForm << " </form>\n"
|
97
|
+
@uploadForm << "</div>\n"
|
98
|
+
@uploadForm = File.open('uploadForm.html','r').read if
|
99
|
+
File.exists?('uploadForm.html')
|
100
|
+
puts @uploadForm if @debug
|
101
|
+
|
102
|
+
@footer = "</body></html>\n"
|
103
|
+
@footer = File.open('footer.html','r').read if
|
104
|
+
File.exists?('footer.html')
|
105
|
+
puts @footer if @debug
|
106
|
+
end
|
107
|
+
puts "SimpleHKP: finished loading html partials" if @debug
|
108
|
+
end
|
109
|
+
|
110
|
+
def initialize(options = {})
|
111
|
+
#
|
112
|
+
# setup the default options
|
113
|
+
#
|
114
|
+
defaultOptions = {
|
115
|
+
'debug' => false,
|
116
|
+
'simpleHKPdir' => 'simpleHKP',
|
117
|
+
'keyDir' => 'keys',
|
118
|
+
'mediaDir' => 'media',
|
119
|
+
'htmlDir' => 'html',
|
120
|
+
'mimeMap' => {
|
121
|
+
'css' => 'text/css',
|
122
|
+
'html' => 'text/html',
|
123
|
+
'js' => 'text/javascript'
|
124
|
+
}
|
125
|
+
}
|
126
|
+
#
|
127
|
+
# merge the options mimeMap into the default options mimeMap
|
128
|
+
#
|
129
|
+
defaultOptions['mimeMap'].merge!(delete(options['mimeMap'])) if
|
130
|
+
options.has_key?('mimeMap')
|
131
|
+
#
|
132
|
+
# merge in the rest of the options into the default options
|
133
|
+
#
|
134
|
+
@options = defaultOptions.merge(options)
|
135
|
+
#
|
136
|
+
# setup the required variables
|
137
|
+
#
|
138
|
+
@debug = @options['debug']
|
139
|
+
@baseDir = @options['simpleHKPdir']
|
140
|
+
@keyDir = @baseDir+'/'+@options['keyDir']
|
141
|
+
@mediaDir = @baseDir+'/'+@options['mediaDir']
|
142
|
+
@htmlDir = @baseDir+'/'+@options['htmlDir']
|
143
|
+
@mimeMap = @options['mimeMap']
|
144
|
+
if @debug then
|
145
|
+
puts "SimpleHKP options:"
|
146
|
+
pp @options
|
147
|
+
end
|
148
|
+
#
|
149
|
+
# ensure the required directories all exist
|
150
|
+
#
|
151
|
+
FileUtils.mkdir_p(@keyDir)
|
152
|
+
FileUtils.mkdir_p(@mediaDir)
|
153
|
+
FileUtils.mkdir_p(@htmlDir)
|
154
|
+
#
|
155
|
+
# load the existing keys and the html partials
|
156
|
+
loadKeys
|
157
|
+
loadHtml
|
158
|
+
end
|
159
|
+
|
160
|
+
def ppEnv(env)
|
161
|
+
Dir.pwd+' '+
|
162
|
+
# '['+URI.decode_www_form(env["rack.input"].rewind.read).pretty_inspect+']'+
|
163
|
+
env.pretty_inspect
|
164
|
+
end
|
165
|
+
|
166
|
+
def replyInternalError(env, exception)
|
167
|
+
@statusCode = 500
|
168
|
+
@body << @headerHtml
|
169
|
+
@body << "<p>Internal server error!</p>\n"
|
170
|
+
@body << "<pre>#{exception}</pre>\n" if @debug
|
171
|
+
@body << @footer
|
172
|
+
if @debug then
|
173
|
+
puts "\n\n-------------------------------------------------------------"
|
174
|
+
puts exception
|
175
|
+
puts "-------------------------------------------------------------"
|
176
|
+
puts ppEnv(env)
|
177
|
+
puts "-------------------------------------------------------------\n\n"
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def replyNotImplemented
|
182
|
+
@statusCode = 501
|
183
|
+
@body << @headerHtml
|
184
|
+
@body << '<p>Not implemented!<p>'
|
185
|
+
@body << @footer
|
186
|
+
end
|
187
|
+
|
188
|
+
def replyBadRequest(message)
|
189
|
+
@statusCode = 400
|
190
|
+
@body << @headerHtml
|
191
|
+
@body << '<p>Bad request!</p>'
|
192
|
+
@body << "<p>#{message}</p>"
|
193
|
+
@body << @footer
|
194
|
+
end
|
195
|
+
|
196
|
+
def replyNotFound
|
197
|
+
@statusCode = 404
|
198
|
+
@body << @headerHtml
|
199
|
+
@body << '<p>Not found!</p>'
|
200
|
+
@body << @footer
|
201
|
+
end
|
202
|
+
|
203
|
+
def replyDefaultBody
|
204
|
+
@body << @headerHtml
|
205
|
+
@body << @defaultBody
|
206
|
+
@body << @footer
|
207
|
+
end
|
208
|
+
|
209
|
+
def replyLookupForm
|
210
|
+
@body << @headerHtml
|
211
|
+
@body << @lookupForm
|
212
|
+
@body << @footer
|
213
|
+
end
|
214
|
+
|
215
|
+
def replyUploadForm
|
216
|
+
@body << @headerHtml
|
217
|
+
@body << @uploadForm
|
218
|
+
@body << @footer
|
219
|
+
end
|
220
|
+
|
221
|
+
def replyFile(env)
|
222
|
+
fileName = env['REQUEST_PATH'].sub(/^.*\/media\//,'')
|
223
|
+
if File.exists?(fileName) then
|
224
|
+
fileExt = File.extname(fileName).sub(/^\./,'')
|
225
|
+
@header['Content-Type'] = @mimeMap[fileExt] if
|
226
|
+
@mimeMap.has_key?(fileExt)
|
227
|
+
@body << File.open(fileName,'r').read
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def storeKey(env)
|
232
|
+
#
|
233
|
+
# decode the post data
|
234
|
+
#
|
235
|
+
keys = URI.decode_www_form(env['rack.input'].read)
|
236
|
+
#
|
237
|
+
# ensure we are being sent a key
|
238
|
+
#
|
239
|
+
return replyBadRequest("No keytext field in post data") unless
|
240
|
+
keys[0][0] =~ /keytext/
|
241
|
+
pp keys[0][1] if @debug
|
242
|
+
#
|
243
|
+
# send the key data through gpg2 to extract the keyID
|
244
|
+
#
|
245
|
+
gpgKeyData = ""
|
246
|
+
IO.popen('gpg2 --with-fingerprint --with-colons -', 'r+') do | pipe |
|
247
|
+
puts 'gpg2 --with-fingerprint --with-colons -' if @debug
|
248
|
+
pipe.write(keys[0][1])
|
249
|
+
pipe.close_write
|
250
|
+
gpgKeyData = pipe.read
|
251
|
+
end
|
252
|
+
puts gpgKeyData if @debug
|
253
|
+
#
|
254
|
+
# look for the keyID of the public key
|
255
|
+
#
|
256
|
+
keyID = nil
|
257
|
+
gpgKeyData.each_line do | aLine |
|
258
|
+
keyData = aLine.split(/:/)
|
259
|
+
next unless keyData[0] =~ /pub/
|
260
|
+
keyID = keyData[4]
|
261
|
+
break
|
262
|
+
end
|
263
|
+
return replyBadRequest("No keyID found in gpg2 result using uploaded keytext data") if keyID.nil?
|
264
|
+
#
|
265
|
+
# record the fact that we have stored this key for later lookup
|
266
|
+
#
|
267
|
+
keyFile = "#{@keyDir}/#{keyID}.asc"
|
268
|
+
@keyLookupData[keyFile] = gpgKeyData
|
269
|
+
pp @keyLookupData if @debug
|
270
|
+
#
|
271
|
+
# store this key as a flat file with the name of the keyID.asc
|
272
|
+
#
|
273
|
+
File.open(keyFile,'w') do | keyFile |
|
274
|
+
keyFile.write(keys[0][1])
|
275
|
+
end
|
276
|
+
#
|
277
|
+
# return OK
|
278
|
+
#
|
279
|
+
@statusCode = 200
|
280
|
+
@body << @headerHtml
|
281
|
+
@body << "<h1 class=\"simpleHKP-storedKey\">Stored key: [#{keyID}]</h1>"
|
282
|
+
@body << "<h2 class=\"simpleHKP-keyDataKeyID\">Key data for key: [#{keyID}]</h2>"
|
283
|
+
@body << '<pre class="simpleHKP-keyData">'+gpgKeyData+"</pre>\n"
|
284
|
+
@body << @footer
|
285
|
+
end
|
286
|
+
|
287
|
+
def lookUpKey(queryString)
|
288
|
+
return replyBadRequest("No search field in queryString") unless
|
289
|
+
queryString.has_key?('search')
|
290
|
+
#
|
291
|
+
# normalize the search string
|
292
|
+
#
|
293
|
+
searchString = queryString['search']
|
294
|
+
searchString.gsub!(/0x/,'')
|
295
|
+
searchRegexp = Regexp.new(searchString,
|
296
|
+
Regexp::IGNORECASE | Regexp::MULTILINE)
|
297
|
+
puts searchRegexp if @debug
|
298
|
+
#
|
299
|
+
# (linearly) look through the hash of known keys
|
300
|
+
# looking for the FIRST match
|
301
|
+
#
|
302
|
+
keyFile = nil
|
303
|
+
@keyLookupData.each_pair do | aKeyFile, keyData |
|
304
|
+
next unless keyData =~ searchRegexp
|
305
|
+
puts "FOUND #{aKeyFile} (#{keyData})" if @debug
|
306
|
+
keyFile = aKeyFile
|
307
|
+
break
|
308
|
+
end
|
309
|
+
return replyNotFound if keyFile.nil?
|
310
|
+
|
311
|
+
if queryString.has_key?('options') &&
|
312
|
+
queryString['options'] == 'mr' then
|
313
|
+
#
|
314
|
+
# return the key data in machine readable format
|
315
|
+
#
|
316
|
+
@header = {
|
317
|
+
'Content-Type' => 'application/pgp-keys; charset=utf-8',
|
318
|
+
'Content-Disposition' =>
|
319
|
+
'attachment; filename=' + keyFile
|
320
|
+
}
|
321
|
+
puts @header if @debug
|
322
|
+
@body << File.open(keyFile,'r').read
|
323
|
+
else
|
324
|
+
#
|
325
|
+
# return the key data for a human to read
|
326
|
+
#
|
327
|
+
keyID = File.basename(keyFile, '.*')
|
328
|
+
@body << @headerHtml
|
329
|
+
@body << @lookupForm
|
330
|
+
@body << '<h1 class="simpleHKP-keyAsckeyId">Key: '+keyID+'</h1>'
|
331
|
+
@body << '<h2 class="simpleHKP-keyDataTitle">Key data:</h2>'
|
332
|
+
@body << '<pre class="simpleHKP-keyData">'
|
333
|
+
@body << @keyLookupData[keyFile]
|
334
|
+
@body << '</pre>'
|
335
|
+
@body << '<h2 class="simpleHKP-keyAscTitle">Key contents:</h2>'
|
336
|
+
@body << '<pre class="simpleHKP-keyAsc">'
|
337
|
+
@body << File.open(keyFile,'r').read
|
338
|
+
@body << '</pre>'
|
339
|
+
@body << @footer
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
def extractKeyInfo(keyFile)
|
344
|
+
keyInfo = Array.new
|
345
|
+
return keyInfo unless @keyLookupData.has_key?(keyFile)
|
346
|
+
#
|
347
|
+
# extract the detailed key information from the PUBLIC key
|
348
|
+
# see the doc/DETAILS file in the gnupg2 source code
|
349
|
+
#
|
350
|
+
@keyLookupData[keyFile].each_line do | aLine |
|
351
|
+
next unless aLine =~ /^pub/
|
352
|
+
keyData = aLine.split(/:/)
|
353
|
+
keyInfo.push(keyData[9]) # -1 = user ID
|
354
|
+
keyInfo.push(keyData[4]) # 0 = key ID
|
355
|
+
keyInfo.push(keyData[3]) # 1 = key type (algorithm)
|
356
|
+
keyInfo.push(keyData[2]) # 2 = key length
|
357
|
+
keyInfo.push(keyData[5]) # 3 = creation date
|
358
|
+
keyInfo.push(keyData[6]) # 4 = expiration date
|
359
|
+
keyInfo.push(keyData[1]) # 5 = flags
|
360
|
+
end
|
361
|
+
|
362
|
+
keyInfo
|
363
|
+
end
|
364
|
+
|
365
|
+
def indexKeys(queryString)
|
366
|
+
return replyBadRequest("No search field in queryString") unless
|
367
|
+
queryString.has_key?('search')
|
368
|
+
#
|
369
|
+
# normalize the search string
|
370
|
+
#
|
371
|
+
searchString = queryString['search']
|
372
|
+
searchString.gsub!(/0x/,'')
|
373
|
+
searchRegexp = Regexp.new(searchString,
|
374
|
+
Regexp::IGNORECASE | Regexp::MULTILINE)
|
375
|
+
puts searchRegexp if @debug
|
376
|
+
#
|
377
|
+
# accumulate the keyFiles of any keys that match the serach
|
378
|
+
#
|
379
|
+
keys = Array.new
|
380
|
+
@keyLookupData.each_pair do | aKeyFile, keyData |
|
381
|
+
next unless keyData =~ searchRegexp
|
382
|
+
puts "FOUND #{aKeyFile} (#{keyData})" if @debug
|
383
|
+
keys.push(aKeyFile)
|
384
|
+
end
|
385
|
+
|
386
|
+
if queryString.has_key?('options') &&
|
387
|
+
queryString['options'] == 'mr' then
|
388
|
+
#
|
389
|
+
# return a machine readable list of the keys found
|
390
|
+
#
|
391
|
+
@header = { 'Content-Type' => 'text/plain' }
|
392
|
+
@body << "info:1:#{keys.size}\n"
|
393
|
+
keys.each do | aKeyFile |
|
394
|
+
keyInfo = extractKeyInfo(aKeyFile)
|
395
|
+
next if keyInfo.empty?
|
396
|
+
userID = keyInfo.shift
|
397
|
+
@body << "pub:#{keyInfo.join(':')}\n"
|
398
|
+
keyID = keyInfo.shift
|
399
|
+
keyType = keyInfo.shift
|
400
|
+
keyLength = keyInfo.shift
|
401
|
+
@body << "uid:#{userID}:#{keyInfo.join(':')}\n"
|
402
|
+
@body << "\n"
|
403
|
+
end
|
404
|
+
else
|
405
|
+
#
|
406
|
+
# return a (simple) human readable list of the keys found
|
407
|
+
#
|
408
|
+
@body << @headerHtml
|
409
|
+
@body << @lookupForm
|
410
|
+
@body << '<h1 class="simpleHKP-searchString">Keys matching: ['+searchString+']</h1><ul class="simpleHKP-searchList">'
|
411
|
+
keys.each do | aKeyFile |
|
412
|
+
keyInfo = extractKeyInfo(aKeyFile)
|
413
|
+
userID = keyInfo.shift
|
414
|
+
keyID = keyInfo.shift
|
415
|
+
keyType = keyInfo.shift
|
416
|
+
keyLength = keyInfo.shift
|
417
|
+
created = keyInfo.shift
|
418
|
+
expires = keyInfo.shift
|
419
|
+
flags = keyInfo.shift
|
420
|
+
keyStr = " <li class=\"simpleHKP-searchItem\"><a href=\"lookup?op=get&search=#{keyID}\">#{keyID}: "
|
421
|
+
keyStr << userID
|
422
|
+
keyStr << "</a></li>\n"
|
423
|
+
@body << keyStr
|
424
|
+
end
|
425
|
+
@body << '</ul>'
|
426
|
+
@body << @footer
|
427
|
+
end
|
428
|
+
pp @body if @debug
|
429
|
+
end
|
430
|
+
|
431
|
+
def decodeQueryString(env)
|
432
|
+
queryHash = Hash.new
|
433
|
+
URI.decode_www_form(env['QUERY_STRING']).each do | aKeyValue |
|
434
|
+
queryHash[aKeyValue[0]] = aKeyValue[1]
|
435
|
+
end
|
436
|
+
queryHash
|
437
|
+
end
|
438
|
+
|
439
|
+
def call(env)
|
440
|
+
#
|
441
|
+
# initialize the response parts
|
442
|
+
#
|
443
|
+
@statusCode = 200
|
444
|
+
@header = {"Content-Type" => "text/html; charset=utf-8"}
|
445
|
+
@body = Array.new
|
446
|
+
#
|
447
|
+
# decode the request
|
448
|
+
#
|
449
|
+
begin
|
450
|
+
puts ppEnv(env) if @debug
|
451
|
+
case env['REQUEST_METHOD']
|
452
|
+
when 'POST'
|
453
|
+
case env['REQUEST_PATH']
|
454
|
+
when /add$/i
|
455
|
+
storeKey(env)
|
456
|
+
else
|
457
|
+
replyNotImplemented
|
458
|
+
end
|
459
|
+
when 'GET'
|
460
|
+
case env['REQUEST_PATH']
|
461
|
+
when /add$/i
|
462
|
+
replyUploadForm
|
463
|
+
when /lookup$/i
|
464
|
+
queryString = decodeQueryString(env)
|
465
|
+
if queryString.has_key?('op') then
|
466
|
+
case queryString['op']
|
467
|
+
when /get/i
|
468
|
+
lookUpKey(queryString)
|
469
|
+
when /index/i
|
470
|
+
indexKeys(queryString)
|
471
|
+
else
|
472
|
+
replyLookupForm
|
473
|
+
end
|
474
|
+
else
|
475
|
+
replyBadRequest("No op field in queryString")
|
476
|
+
end
|
477
|
+
when /media\//i
|
478
|
+
replyFile(env)
|
479
|
+
when /reload$/i
|
480
|
+
loadKeys
|
481
|
+
loadHtml
|
482
|
+
replyDefaultBody
|
483
|
+
else
|
484
|
+
replyDefaultBody
|
485
|
+
end
|
486
|
+
else
|
487
|
+
replyBadRequest("Unknown request method")
|
488
|
+
end
|
489
|
+
rescue Exception => exception
|
490
|
+
replyInternalError(env, exception)
|
491
|
+
end
|
492
|
+
#
|
493
|
+
# send the response
|
494
|
+
#
|
495
|
+
if @debug then
|
496
|
+
puts "SimpleHKP reply:"
|
497
|
+
pp @statusCode
|
498
|
+
pp @header
|
499
|
+
puts @body.join("\n")
|
500
|
+
end
|
501
|
+
[ @statusCode, @header, @body.flatten ]
|
502
|
+
end
|
503
|
+
|
504
|
+
end
|
metadata
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: simple-gnupg-keyserver
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Stephen Gaito
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2015-03-08 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rdoc
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '4.0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '4.0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: hoe
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '3.13'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '3.13'
|
46
|
+
description: ! "SimpleHKP is a simple Ruby/rack based GnuPG HKP key server.\n\nIts
|
47
|
+
sole task is to supply a limited number of GnuPG _public_ keys \nto/from MonkeySphere
|
48
|
+
on a limited number of machines/servers. It is \n_not_ meant to server hundreds
|
49
|
+
of keys, so it is not meant as a truely \npublic key server.\n\nIf you need to run
|
50
|
+
your own _large_ scale production Key Sever then use \n{SKS}[https://bitbucket.org/skskeyserver/sks-keyserver/wiki/Home].
|
51
|
+
You \nmight want to read Paul Bauer's {How To Setup A Free PGP Key Server in \nUbuntu}[http://www.bauer-power.net/2010/05/how-to-setup-free-pgp-key-server-in.html]."
|
52
|
+
email:
|
53
|
+
- stephen@perceptisys.co.uk
|
54
|
+
executables: []
|
55
|
+
extensions: []
|
56
|
+
extra_rdoc_files:
|
57
|
+
- Manifest.txt
|
58
|
+
- History.txt
|
59
|
+
- README.rdoc
|
60
|
+
files:
|
61
|
+
- Manifest.txt
|
62
|
+
- .gitignore
|
63
|
+
- History.txt
|
64
|
+
- README.rdoc
|
65
|
+
- Rakefile
|
66
|
+
- lib/simpleHKP.rb
|
67
|
+
homepage: https://github.com/stephengaito/rGem-simple-gnupg-keyserver
|
68
|
+
licenses:
|
69
|
+
- MIT
|
70
|
+
post_install_message:
|
71
|
+
rdoc_options:
|
72
|
+
- --main
|
73
|
+
- README.rdoc
|
74
|
+
require_paths:
|
75
|
+
- lib
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
77
|
+
none: false
|
78
|
+
requirements:
|
79
|
+
- - ! '>='
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
requirements: []
|
89
|
+
rubyforge_project:
|
90
|
+
rubygems_version: 1.8.23
|
91
|
+
signing_key:
|
92
|
+
specification_version: 3
|
93
|
+
summary: SimpleHKP is a simple Ruby/rack based GnuPG HKP key server
|
94
|
+
test_files: []
|