hkp_client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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