hkp_client 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.editorconfig +16 -0
- data/.gitattributes +1 -0
- data/.gitignore +14 -0
- data/.hound.yml +3 -0
- data/.rspec +3 -0
- data/.rubocop.ribose.yml +65 -0
- data/.rubocop.tb.yml +650 -0
- data/.rubocop.yml +26 -0
- data/.travis.yml +17 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +21 -0
- data/README.adoc +168 -0
- data/Rakefile +6 -0
- data/bin/console +10 -0
- data/bin/setup +8 -0
- data/hkp_client.gemspec +34 -0
- data/lib/hkp_client.rb +118 -0
- data/lib/hkp_client/uri_schemes.rb +14 -0
- data/lib/hkp_client/version.rb +3 -0
- metadata +162 -0
data/.rubocop.yml
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# This project follows the Ribose OSS style guide.
|
2
|
+
# https://github.com/riboseinc/oss-guides
|
3
|
+
# All project-specific additions and overrides should be specified in this file.
|
4
|
+
|
5
|
+
inherit_from:
|
6
|
+
# Thoughtbot's style guide from: https://github.com/thoughtbot/guides
|
7
|
+
- ".rubocop.tb.yml"
|
8
|
+
# Overrides from Ribose
|
9
|
+
- ".rubocop.ribose.yml"
|
10
|
+
AllCops:
|
11
|
+
DisplayCopNames: false
|
12
|
+
StyleGuideCopsOnly: false
|
13
|
+
TargetRubyVersion: 2.3
|
14
|
+
Rails:
|
15
|
+
Enabled: false
|
16
|
+
|
17
|
+
Style/EmptyCaseCondition:
|
18
|
+
Enabled: false
|
19
|
+
|
20
|
+
Style/TrailingCommaInArguments:
|
21
|
+
Exclude:
|
22
|
+
# RSpec expectations can easily go multiline. And sometimes, it's all not
|
23
|
+
# about multiple arguments, but more about & or | operators. Comma placed
|
24
|
+
# after a single method argument which spans across many lines is confusing,
|
25
|
+
# not helpful. Hence, I'm disabling this cop for all specs.
|
26
|
+
- "spec/**/*"
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
source "https://rubygems.org"
|
2
|
+
|
3
|
+
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
|
4
|
+
|
5
|
+
# Specify your gem's dependencies in hkp_client.gemspec
|
6
|
+
gemspec
|
7
|
+
|
8
|
+
gem "codecov", require: false, group: :test
|
9
|
+
gem "simplecov", require: false, group: :test
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018 Ribose Inc.
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.adoc
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
HKP Client
|
2
|
+
==========
|
3
|
+
|
4
|
+
image:https://img.shields.io/gem/v/hkp_client.svg["Gem Version", link="https://rubygems.org/gems/hkp_client"]
|
5
|
+
image:https://img.shields.io/travis/riboseinc/hkp_client/master.svg["Build Status", link="https://travis-ci.org/riboseinc/hkp_client"]
|
6
|
+
image:https://img.shields.io/codecov/c/github/riboseinc/hkp_client.svg["Test Coverage", link="https://codecov.io/gh/riboseinc/hkp_client"]
|
7
|
+
image:https://img.shields.io/codeclimate/maintainability/riboseinc/hkp_client.svg["Maintainability", link="https://codeclimate.com/github/riboseinc/hkp_client/maintainability"]
|
8
|
+
|
9
|
+
:source-highlighter: pygments
|
10
|
+
|
11
|
+
HKP Client is a minimalist HKP (OpenPGP HTTP Keyserver Protocol) client, which
|
12
|
+
queries PGP public keyservers, and downloads public keys. It does not support
|
13
|
+
submitting keys to keyserver.
|
14
|
+
|
15
|
+
NOTE: This gem registers +hkp+, and +hkps+ URI schemes (adds them to
|
16
|
+
+URI.scheme_list+).
|
17
|
+
|
18
|
+
Usage
|
19
|
+
-----
|
20
|
+
|
21
|
+
Searching
|
22
|
+
~~~~~~~~~
|
23
|
+
|
24
|
+
Searching by some criteria. Returns search results as an array of arrays.
|
25
|
+
|
26
|
+
For exact searches, an +exact+ option can be set to true. The meaning of
|
27
|
+
"exact" is not defined by standard, and may be ignored by servers.
|
28
|
+
|
29
|
+
[source,lang=ruby]
|
30
|
+
--------------------------------------------------------------------------------
|
31
|
+
HkpClient.search "linus@example.com"
|
32
|
+
HkpClient.search "linus@example.com", exact: true
|
33
|
+
--------------------------------------------------------------------------------
|
34
|
+
|
35
|
+
Search operations returns an array of hashes. In this case, it is a one-element
|
36
|
+
array:
|
37
|
+
|
38
|
+
[source,lang=ruby]
|
39
|
+
--------------------------------------------------------------------------------
|
40
|
+
[
|
41
|
+
{
|
42
|
+
:key_id=>"06DA6D18CED048CE87E3E3A01CBBDA571B331AB5",
|
43
|
+
:algorithm=>"1",
|
44
|
+
:key_length=>"2048",
|
45
|
+
:creation_date=>"1507718293",
|
46
|
+
:expiration_date=>nil,
|
47
|
+
:flags=>nil,
|
48
|
+
:uids=>[
|
49
|
+
{
|
50
|
+
:name=>"Linus Torvalds (Example) <linus@example.com>",
|
51
|
+
:creation_date=>"1507718293",
|
52
|
+
:expiration_date=>nil,
|
53
|
+
:flags=>nil
|
54
|
+
}
|
55
|
+
]
|
56
|
+
}
|
57
|
+
]
|
58
|
+
--------------------------------------------------------------------------------
|
59
|
+
|
60
|
+
Each array item describes a primary key uploaded to keyserver, and is associated
|
61
|
+
with one or more UIDs. For field descriptions, refer to
|
62
|
+
https://tools.ietf.org/html/draft-shaw-openpgp-hkp-00#section-5.2[HKP draft,
|
63
|
+
section 5.2].
|
64
|
+
|
65
|
+
UID's name is already percent-decoded. Other values may require parsing or
|
66
|
+
casting (e.g. timestamps and numbers).
|
67
|
+
|
68
|
+
Search results may include expired keys. If that's unwanted, these keys must
|
69
|
+
be filtered out by user.
|
70
|
+
|
71
|
+
Fetching keys
|
72
|
+
~~~~~~~~~~~~~
|
73
|
+
|
74
|
+
Operation returns the ASCII-armored keyring as defined in
|
75
|
+
https://tools.ietf.org/html/rfc2440#section-11.1[RFC 2440, section 11.1],
|
76
|
+
or +nil+.
|
77
|
+
|
78
|
+
For example, executing following snippet:
|
79
|
+
|
80
|
+
[source,lang=ruby]
|
81
|
+
--------------------------------------------------------------------------------
|
82
|
+
HkpClient.get "linus@example.com"
|
83
|
+
--------------------------------------------------------------------------------
|
84
|
+
|
85
|
+
will return string similar to:
|
86
|
+
|
87
|
+
--------------------------------------------------------------------------------
|
88
|
+
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
89
|
+
Version: SKS 1.1.6
|
90
|
+
Comment: Hostname: pgp.lehigh.edu
|
91
|
+
|
92
|
+
mQENBFnd9JUBCAC1NMfsImuRAUcsKEjdLlSj0THHNytUDE8CB2I728gJAeixdZMEcPpRKHfc
|
93
|
+
BXjW+Q864S4yEY4xgaboFkg/qABA/o0PWzZn2AzhvD5gzrfpfvK4BMrgOtPya7MySgImG1NC
|
94
|
+
UYqj2vvJt4/bh8MWxSqvADB3SfLNueBQGvISeGwss9kYHEqoP0lxNSEJPJJpLKeqSov7mZOz
|
95
|
+
(…)
|
96
|
+
c2gFngOSVOrxJswb8/BUkA==
|
97
|
+
=jGC1
|
98
|
+
-----END PGP PUBLIC KEY BLOCK-----
|
99
|
+
--------------------------------------------------------------------------------
|
100
|
+
|
101
|
+
Arbitrary queries
|
102
|
+
~~~~~~~~~~~~~~~~~
|
103
|
+
|
104
|
+
When above two methods are not flexible enough, a +\#query+ method can be
|
105
|
+
called. It returns an instance of +Faraday::Response+. All the arguments
|
106
|
+
become query string parameters (with exception of +keyserver+, which is
|
107
|
+
described in the next section).
|
108
|
+
|
109
|
+
For example, a following snippet performs a +vindex+ operation for
|
110
|
+
+linus@example.com+ query. The +mr+ option specifies that search results should
|
111
|
+
be presented in a machine-readable format. By default, a human-readable format
|
112
|
+
is used, typically HTML.
|
113
|
+
|
114
|
+
[source,lang=ruby]
|
115
|
+
--------------------------------------------------------------------------------
|
116
|
+
HkpClient.query(op: "vindex", search: "linus@example.com", options: "mr")
|
117
|
+
--------------------------------------------------------------------------------
|
118
|
+
|
119
|
+
Using custom keyserver
|
120
|
+
~~~~~~~~~~~~~~~~~~~~~~
|
121
|
+
|
122
|
+
A +keyserver+ option can be passed to either +\#search+, +\#get+, or +\#query+
|
123
|
+
method. Accepted URI schemes are +http+, +https+, +hkp+, and +hkps+.
|
124
|
+
|
125
|
+
TODO: Easy support for custom certificates.
|
126
|
+
|
127
|
+
[source,lang=ruby]
|
128
|
+
--------------------------------------------------------------------------------
|
129
|
+
HkpClient.get "linus@example.com", keyserver: "hkp://server.you.want:8888"
|
130
|
+
--------------------------------------------------------------------------------
|
131
|
+
|
132
|
+
Contributing
|
133
|
+
------------
|
134
|
+
|
135
|
+
First, thank you for contributing! We love pull requests from everyone.
|
136
|
+
By participating in this project, you hereby grant Ribose Inc. the right to
|
137
|
+
grant or transfer an unlimited number of non exclusive licenses or sub-licenses
|
138
|
+
to third parties, under the copyright covering the contribution to use
|
139
|
+
the contribution by all means.
|
140
|
+
|
141
|
+
Here are a few technical guidelines to follow:
|
142
|
+
|
143
|
+
1. Open an issue to discuss a new feature prior implementing it.
|
144
|
+
2. Write tests for new features or bugfixes.
|
145
|
+
3. Make sure the entire test suite passes locally and on CI.
|
146
|
+
4. Follow our style guide (you can validate your contribution locally with
|
147
|
+
Rubocop, also Hound CI will report any offences when you open a pull
|
148
|
+
request).
|
149
|
+
|
150
|
+
Credits
|
151
|
+
-------
|
152
|
+
|
153
|
+
This gem is developed, maintained and funded by
|
154
|
+
https://www.ribose.com[Ribose Inc].
|
155
|
+
|
156
|
+
License
|
157
|
+
-------
|
158
|
+
|
159
|
+
The gem is available as open source under the terms of the
|
160
|
+
https://opensource.org/licenses/MIT[MIT License].
|
161
|
+
|
162
|
+
Resources
|
163
|
+
---------
|
164
|
+
|
165
|
+
- https://tools.ietf.org/html/draft-shaw-openpgp-hkp-00[HKP protocol definition (IETF draft)]
|
166
|
+
- http://www.mit.edu/afs/net.mit.edu/project/pks/thesis/paper/thesis.html[A PGP Public Key Server thesis]
|
167
|
+
- https://www.openpgp.org/about/standard/[More documents on OpenPGP]
|
168
|
+
- https://sks-keyservers.net/[SKS keyservers]
|
data/Rakefile
ADDED
data/bin/console
ADDED
data/bin/setup
ADDED
data/hkp_client.gemspec
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path("lib", __dir__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "hkp_client/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "hkp_client"
|
8
|
+
spec.version = HkpClient::VERSION
|
9
|
+
spec.authors = ["Ribose Inc."]
|
10
|
+
spec.email = ["open.source@ribose.com"]
|
11
|
+
|
12
|
+
spec.summary = "A minimalist client for PGP public keyservers."
|
13
|
+
spec.description = "A minimalist HKP (OpenPGP HTTP Keyserver Protocol) " \
|
14
|
+
"client, which queries PGP public keyservers, " \
|
15
|
+
"and downloads public keys."
|
16
|
+
spec.homepage = "https://github.com/riboseinc/hkp_client"
|
17
|
+
spec.license = "MIT"
|
18
|
+
|
19
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
20
|
+
f.match(%r{^(test|spec|features)/})
|
21
|
+
end
|
22
|
+
spec.bindir = "exe"
|
23
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
24
|
+
spec.require_paths = ["lib"]
|
25
|
+
|
26
|
+
spec.add_runtime_dependency "faraday"
|
27
|
+
|
28
|
+
spec.add_development_dependency "bundler", "~> 1.16"
|
29
|
+
spec.add_development_dependency "pry", "~> 0.11.0"
|
30
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
31
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
32
|
+
spec.add_development_dependency "vcr", "~> 4.0"
|
33
|
+
spec.add_development_dependency "webmock"
|
34
|
+
end
|
data/lib/hkp_client.rb
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "hkp_client/version"
|
4
|
+
require "hkp_client/uri_schemes"
|
5
|
+
|
6
|
+
require "faraday"
|
7
|
+
require "uri"
|
8
|
+
|
9
|
+
module HkpClient
|
10
|
+
DEFAULT_KEYSERVER = URI("hkp://pool.sks-keyservers.net").freeze
|
11
|
+
|
12
|
+
class Error < StandardError; end
|
13
|
+
|
14
|
+
PUB_ENTRY_FIELDS =
|
15
|
+
%i[key_id algorithm key_length creation_date expiration_date flags].freeze
|
16
|
+
|
17
|
+
UID_ENTRY_FIELDS =
|
18
|
+
%i[name creation_date expiration_date flags].freeze
|
19
|
+
|
20
|
+
module_function
|
21
|
+
|
22
|
+
# Fetches a keyring containing open PGP keys from keyserver, and returns it
|
23
|
+
# as an ASCII-armoured string.
|
24
|
+
#
|
25
|
+
# @param string [String] a search string
|
26
|
+
# @param keyserver (see #query)
|
27
|
+
# @return [String] keyring
|
28
|
+
# @return [nil] if key could not be found
|
29
|
+
# @raise [HkpClient::Error] if server responds with different HTTP code than
|
30
|
+
# 200 or 404
|
31
|
+
# @raise any other exceptions caused by networking problems
|
32
|
+
def get(string, keyserver: DEFAULT_KEYSERVER, exact: false)
|
33
|
+
resp = query(
|
34
|
+
keyserver: keyserver,
|
35
|
+
op: "get",
|
36
|
+
search: string,
|
37
|
+
options: "mr",
|
38
|
+
exact: (exact ? "on" : "off"),
|
39
|
+
)
|
40
|
+
|
41
|
+
Util.response_body_or_error_or_nil(resp)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Performs a search query on keyserver, and returns a list of key
|
45
|
+
# descriptions (see README for details).
|
46
|
+
#
|
47
|
+
# @param string (see #get)
|
48
|
+
# @param exact [Boolean] whether to perform an "exact" search
|
49
|
+
# @param keyserver (see #query)
|
50
|
+
# @return [Array<Hash>] list of found keys, possibly empty
|
51
|
+
# @raise [HkpClient::Error] if server responds with different HTTP code than
|
52
|
+
# 200 or 404
|
53
|
+
# @raise any other exceptions caused by networking problems
|
54
|
+
def search(string, keyserver: DEFAULT_KEYSERVER, exact: false)
|
55
|
+
resp = query(
|
56
|
+
keyserver: keyserver,
|
57
|
+
op: "index",
|
58
|
+
search: string,
|
59
|
+
options: "mr",
|
60
|
+
exact: (exact ? "on" : "off"),
|
61
|
+
)
|
62
|
+
|
63
|
+
resp_body = Util.response_body_or_error_or_nil(resp) || ""
|
64
|
+
Util.parse_search_response_entries(resp_body)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Makes a query to keyserver. Any surplus keyword arguments will be used
|
68
|
+
# as request parameters.
|
69
|
+
#
|
70
|
+
# @param keyserver [String] a keyserver to query
|
71
|
+
# @return [Faraday::Response]
|
72
|
+
# @raise any exceptions caused by networking problems
|
73
|
+
def query(keyserver: DEFAULT_KEYSERVER, **query_args)
|
74
|
+
uri = (URI === keyserver ? keyserver.dup : URI.parse(keyserver))
|
75
|
+
use_ssl = %w[https hkps].include?(uri.scheme)
|
76
|
+
Faraday.new(url: uri, ssl: use_ssl).get("/pks/lookup", query_args)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Utilities to be considered as kinda private API.
|
80
|
+
module Util
|
81
|
+
module_function
|
82
|
+
|
83
|
+
def response_body_or_error_or_nil(resp)
|
84
|
+
if resp.success?
|
85
|
+
resp.body
|
86
|
+
elsif resp.status == 404
|
87
|
+
nil
|
88
|
+
else
|
89
|
+
raise Error, "Server responded with #{resp.status}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# rubocop:disable Metrics/MethodLength
|
94
|
+
|
95
|
+
def parse_search_response_entries(src_string)
|
96
|
+
src_string.each_line.reduce([]) do |found_keys, line|
|
97
|
+
case line
|
98
|
+
when /\Apub:/
|
99
|
+
key = response_line_to_hash(line, PUB_ENTRY_FIELDS)
|
100
|
+
key[:uids] = []
|
101
|
+
found_keys << key
|
102
|
+
when /\Auid:/
|
103
|
+
uid = response_line_to_hash(line, UID_ENTRY_FIELDS)
|
104
|
+
uid[:name] = CGI.unescape(uid[:name])
|
105
|
+
found_keys.last[:uids] << uid
|
106
|
+
end
|
107
|
+
found_keys
|
108
|
+
end
|
109
|
+
end
|
110
|
+
# rubocop:enable Metrics/MethodLength
|
111
|
+
|
112
|
+
def response_line_to_hash(line, field_names)
|
113
|
+
_line_type, *fields = line.strip.split(":")
|
114
|
+
fields.push(nil) while fields.length < field_names.length
|
115
|
+
[field_names, fields].transpose.to_h
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|