hkp_client 0.1.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.
@@ -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/**/*"
@@ -0,0 +1,17 @@
1
+ dist: trusty
2
+ language: ruby
3
+ sudo: false
4
+
5
+ before_install:
6
+ - gem install bundler -v 1.16.1
7
+
8
+ rvm:
9
+ - 2.5
10
+ - 2.4
11
+ - 2.3
12
+ - 2.2
13
+ - ruby-head
14
+
15
+ matrix:
16
+ allow_failures:
17
+ - rvm: ruby-head
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
@@ -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.
@@ -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]
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec # rubocop:disable Style/HashSyntax
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "hkp_client"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ require "pry"
10
+ Pry.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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
@@ -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
@@ -0,0 +1,14 @@
1
+ require "uri"
2
+
3
+ module URI
4
+ class HKP < HTTP
5
+ DEFAULT_PORT = 11371
6
+ end
7
+
8
+ class HKPS < HTTPS
9
+ DEFAULT_PORT = 443
10
+ end
11
+ end
12
+
13
+ URI.scheme_list["HKP".freeze] ||= URI::HKP
14
+ URI.scheme_list["HKPS".freeze] ||= URI::HKPS
@@ -0,0 +1,3 @@
1
+ module HkpClient
2
+ VERSION = "0.1.0".freeze
3
+ end