pwnedkeys-tools 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.
- checksums.yaml +7 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/CONTRIBUTING.md +14 -0
- data/Dockerfile +11 -0
- data/LICENCE +674 -0
- data/README.md +109 -0
- data/bin/pwnedkeys-prove-pwned +5 -0
- data/bin/pwnedkeys-query +57 -0
- data/docker-wrappers/pwnedkeys-prove-pwned +7 -0
- data/docker-wrappers/pwnedkeys-query +7 -0
- data/lib/openssl/pkey.rb +77 -0
- data/lib/openssl/pkey/ec.rb +16 -0
- data/lib/openssl/pkey/rsa.rb +12 -0
- data/lib/openssl/x509/certificate.rb +12 -0
- data/lib/openssl/x509/request.rb +12 -0
- data/lib/openssl/x509/spki.rb +177 -0
- data/lib/pwnedkeys/request.rb +159 -0
- data/lib/pwnedkeys/response.rb +107 -0
- data/pwnedkeys-tools.gemspec +46 -0
- metadata +211 -0
@@ -0,0 +1,159 @@
|
|
1
|
+
require "base64"
|
2
|
+
require "json"
|
3
|
+
require "net/http"
|
4
|
+
require "openssl"
|
5
|
+
|
6
|
+
require "openssl/pkey/rsa"
|
7
|
+
require "openssl/pkey/ec"
|
8
|
+
|
9
|
+
module Pwnedkeys
|
10
|
+
class Request
|
11
|
+
class Error < StandardError; end
|
12
|
+
class VerificationError < Error; end
|
13
|
+
|
14
|
+
def initialize(spki)
|
15
|
+
@spki = if spki.is_a?(OpenSSL::X509::SPKI)
|
16
|
+
spki
|
17
|
+
elsif spki.is_a?(String)
|
18
|
+
begin
|
19
|
+
OpenSSL::X509::SPKI.new(spki)
|
20
|
+
rescue OpenSSL::ASN1::ASN1Error, OpenSSL::X509::SPKIError
|
21
|
+
raise Error,
|
22
|
+
"Invalid SPKI ASN.1 string"
|
23
|
+
end
|
24
|
+
else
|
25
|
+
raise Error,
|
26
|
+
"Invalid argument type passed to Pwnedkeys::Request.new (need OpenSSL::X509::SPKI or string, got #{spki.class})"
|
27
|
+
end
|
28
|
+
|
29
|
+
# Verify key type is OK
|
30
|
+
key_params
|
31
|
+
end
|
32
|
+
|
33
|
+
def pwned?
|
34
|
+
retry_count = 10
|
35
|
+
|
36
|
+
loop do
|
37
|
+
uri = URI(ENV["PWNEDKEYS_API_URL"] || "https://v1.pwnedkeys.com")
|
38
|
+
uri.path += "/#{@spki.spki_fingerprint.hexdigest}"
|
39
|
+
|
40
|
+
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
|
41
|
+
req = Net::HTTP::Get.new(uri.path)
|
42
|
+
req["User-Agent"] = "pwnedkeys-tools/0.0.0"
|
43
|
+
http.request(req)
|
44
|
+
end
|
45
|
+
|
46
|
+
if res.code == "200"
|
47
|
+
verify!(res.body)
|
48
|
+
return true
|
49
|
+
elsif res.code == "404"
|
50
|
+
return false
|
51
|
+
elsif (500..599) === res.code.to_i && retry_count > 0
|
52
|
+
# Server-side error, let's try a few more times
|
53
|
+
sleep 1
|
54
|
+
retry_count -= 1
|
55
|
+
else
|
56
|
+
raise Error,
|
57
|
+
"Unable to determine pwnage, error status code returned from #{uri}: #{res.code}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def verify!(res)
|
65
|
+
json = JSON.parse(res)
|
66
|
+
header = JSON.parse(unb64(json["protected"]))
|
67
|
+
|
68
|
+
key = @spki.to_key
|
69
|
+
|
70
|
+
verify_data = "#{json["protected"]}.#{json["payload"]}"
|
71
|
+
|
72
|
+
unless key.verify(hash_func.new, format_sig(unb64(json["signature"])), verify_data)
|
73
|
+
raise VerificationError,
|
74
|
+
"Response signature cannot be validated by provided key"
|
75
|
+
end
|
76
|
+
|
77
|
+
unless header["alg"] == key_alg
|
78
|
+
raise VerificationError,
|
79
|
+
"Incorrect alg parameter. Got #{header["alg"]}, expected #{key_alg} for #{key.class} key"
|
80
|
+
end
|
81
|
+
|
82
|
+
unless header["kid"] == @spki.spki_fingerprint.hexdigest
|
83
|
+
raise VerificationError,
|
84
|
+
"Key ID in response doesn't match. Got #{header["kid"]}, expected #{@spki.spki_fingerprint.hexdigest}"
|
85
|
+
end
|
86
|
+
|
87
|
+
unless unb64(json["payload"]) =~ /key is pwned/
|
88
|
+
raise VerificationError,
|
89
|
+
"Response payload does not include magic string 'key is pwned', got #{unb64(json["payload"])}"
|
90
|
+
end
|
91
|
+
|
92
|
+
# The gauntlet has been run and you have been found... worthy
|
93
|
+
end
|
94
|
+
|
95
|
+
def unb64(s)
|
96
|
+
Base64.urlsafe_decode64(s)
|
97
|
+
end
|
98
|
+
|
99
|
+
def key_alg
|
100
|
+
key_params[:key_alg]
|
101
|
+
end
|
102
|
+
|
103
|
+
def hash_func
|
104
|
+
key_params[:hash_func]
|
105
|
+
end
|
106
|
+
|
107
|
+
def format_sig(sig)
|
108
|
+
key_params[:format_sig].call(sig)
|
109
|
+
end
|
110
|
+
|
111
|
+
def ec_sig(jose_sig)
|
112
|
+
# *Real* EC signatures are a two-element ASN.1 sequence containing
|
113
|
+
# the R and S values. RFC7518, in its infinite wisdom, has decided that
|
114
|
+
# that is not good enough, and instead it wants the signatures in raw
|
115
|
+
# concatenated R/S as octet strings. Because of *course* it does.
|
116
|
+
OpenSSL::ASN1::Sequence.new(split_in_two_equal_halves(jose_sig).map do |i|
|
117
|
+
OpenSSL::ASN1::Integer.new(i.unpack("C*").inject(0) { |v, i| v * 256 + i })
|
118
|
+
end).to_der
|
119
|
+
end
|
120
|
+
|
121
|
+
def split_in_two_equal_halves(s)
|
122
|
+
[s[0..(s.length / 2 - 1)], s[(s.length / 2)..(s.length - 1)]]
|
123
|
+
end
|
124
|
+
|
125
|
+
def key_params
|
126
|
+
case @spki.to_key
|
127
|
+
when OpenSSL::PKey::RSA then {
|
128
|
+
key_alg: "RS256",
|
129
|
+
hash_func: OpenSSL::Digest::SHA256,
|
130
|
+
format_sig: ->(sig) { sig },
|
131
|
+
}
|
132
|
+
when OpenSSL::PKey::EC
|
133
|
+
case @spki.to_key.public_key.group.curve_name
|
134
|
+
when "prime256v1" then {
|
135
|
+
key_alg: "ES256",
|
136
|
+
hash_func: OpenSSL::Digest::SHA256,
|
137
|
+
format_sig: ->(sig) { ec_sig(sig) },
|
138
|
+
}
|
139
|
+
when "secp384r1" then {
|
140
|
+
key_alg: "ES384",
|
141
|
+
hash_func: OpenSSL::Digest::SHA384,
|
142
|
+
format_sig: ->(sig) { ec_sig(sig) },
|
143
|
+
}
|
144
|
+
when "secp521r1" then {
|
145
|
+
key_alg: "ES512",
|
146
|
+
hash_func: OpenSSL::Digest::SHA512,
|
147
|
+
# The components of P-521 keys are 521 bits each, which is padded
|
148
|
+
# out to be 528 bits -- 66 octets.
|
149
|
+
format_sig: ->(sig) { ec_sig(sig) },
|
150
|
+
}
|
151
|
+
else
|
152
|
+
raise Error, "EC key containing unsupported curve #{@spki.to_key.group.curve_name}"
|
153
|
+
end
|
154
|
+
else
|
155
|
+
raise Error, "Unsupported key type #{@key.class}"
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require "base64"
|
2
|
+
require "json"
|
3
|
+
require "openssl"
|
4
|
+
|
5
|
+
require "openssl/pkey/rsa"
|
6
|
+
require "openssl/pkey/ec"
|
7
|
+
|
8
|
+
module Pwnedkeys
|
9
|
+
class Response
|
10
|
+
class Error < StandardError; end
|
11
|
+
|
12
|
+
def initialize(key)
|
13
|
+
@key = if key.kind_of?(OpenSSL::PKey::PKey)
|
14
|
+
key
|
15
|
+
elsif key.is_a?(String)
|
16
|
+
begin
|
17
|
+
OpenSSL::PKey.read(key)
|
18
|
+
rescue OpenSSL::PKey::PKeyError
|
19
|
+
raise Error,
|
20
|
+
"Unable to parse provided key data"
|
21
|
+
end
|
22
|
+
else
|
23
|
+
raise Error,
|
24
|
+
"Invalid argument type passed to Pwnedkeys::Response.new (need OpenSSL::PKey::PKey or string, got #{key.class})"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_json(*spki_format)
|
29
|
+
header = {
|
30
|
+
alg: key_alg,
|
31
|
+
kid: @key.to_spki(*spki_format).spki_fingerprint.hexdigest,
|
32
|
+
}
|
33
|
+
|
34
|
+
obj = {
|
35
|
+
payload: b64("This key is pwned! See https://pwnedkeys.com for more info."),
|
36
|
+
protected: b64(header.to_json),
|
37
|
+
}
|
38
|
+
|
39
|
+
obj[:signature] = b64(sign(obj))
|
40
|
+
obj.to_json
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def b64(s)
|
46
|
+
Base64.urlsafe_encode64(s).sub(/=*\z/, '')
|
47
|
+
end
|
48
|
+
|
49
|
+
def key_alg
|
50
|
+
key_params[:key_alg]
|
51
|
+
end
|
52
|
+
|
53
|
+
def hash_func
|
54
|
+
key_params[:hash_func]
|
55
|
+
end
|
56
|
+
|
57
|
+
def format_sig(sig)
|
58
|
+
key_params[:format_sig].call(sig)
|
59
|
+
end
|
60
|
+
|
61
|
+
def jose_sig(ec_sig, len)
|
62
|
+
# EC signatures are a two-element ASN.1 sequence containing
|
63
|
+
# the R and S values. RFC7518, in its infinite wisdom, has decided that
|
64
|
+
# that is not good enough, and instead it wants the signatures in raw
|
65
|
+
# concatenated R/S as octet strings. Because of *course* it does.
|
66
|
+
OpenSSL::ASN1.decode(ec_sig).value.map { |n| [sprintf("%0#{len * 2}x", n.value)].pack("H*") }.join
|
67
|
+
end
|
68
|
+
|
69
|
+
def key_params
|
70
|
+
case @key
|
71
|
+
when OpenSSL::PKey::RSA then {
|
72
|
+
key_alg: "RS256",
|
73
|
+
hash_func: OpenSSL::Digest::SHA256,
|
74
|
+
format_sig: ->(sig) { sig },
|
75
|
+
}
|
76
|
+
when OpenSSL::PKey::EC
|
77
|
+
case @key.public_key.group.curve_name
|
78
|
+
when "prime256v1" then {
|
79
|
+
key_alg: "ES256",
|
80
|
+
hash_func: OpenSSL::Digest::SHA256,
|
81
|
+
format_sig: ->(sig) { jose_sig(sig, 32) },
|
82
|
+
}
|
83
|
+
when "secp384r1" then {
|
84
|
+
key_alg: "ES384",
|
85
|
+
hash_func: OpenSSL::Digest::SHA384,
|
86
|
+
format_sig: ->(sig) { jose_sig(sig, 48) },
|
87
|
+
}
|
88
|
+
when "secp521r1" then {
|
89
|
+
key_alg: "ES512",
|
90
|
+
hash_func: OpenSSL::Digest::SHA512,
|
91
|
+
# The components of P-521 keys are 521 bits each, which is padded
|
92
|
+
# out to be 528 bits -- 66 octets.
|
93
|
+
format_sig: ->(sig) { jose_sig(sig, 66) },
|
94
|
+
}
|
95
|
+
else
|
96
|
+
raise Error, "EC key containing unsupported curve #{@key.public_key.group.curve_name}"
|
97
|
+
end
|
98
|
+
else
|
99
|
+
raise Error, "Unsupported key type #{@key.class}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def sign(obj)
|
104
|
+
format_sig(@key.sign(hash_func.new, obj[:protected] + "." + obj[:payload]))
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
begin
|
2
|
+
require 'git-version-bump'
|
3
|
+
rescue LoadError
|
4
|
+
nil
|
5
|
+
end
|
6
|
+
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.name = "pwnedkeys-tools"
|
9
|
+
|
10
|
+
s.version = GVB.version rescue "0.0.0.1.NOGVB"
|
11
|
+
s.date = GVB.date rescue Time.now.strftime("%Y-%m-%d")
|
12
|
+
|
13
|
+
s.platform = Gem::Platform::RUBY
|
14
|
+
|
15
|
+
s.summary = "A set of command-line tools useful for working with the pwnedkeys.com service"
|
16
|
+
s.description = <<~EOF
|
17
|
+
The scripts in this package are designed to be used in conjunction with the
|
18
|
+
pwnedkeys.com compromised keys database. They include:
|
19
|
+
|
20
|
+
* `pwnedkeys-prove-pwned`, which generates a signed attestation of
|
21
|
+
compromise suitable for being served by the pwnedkeys.com V1 API.
|
22
|
+
|
23
|
+
* `pwnedkeys-query`, which takes a public or private key, CSR, or X.509
|
24
|
+
certificate and looks it up in the pwnedkeys.com database.
|
25
|
+
EOF
|
26
|
+
|
27
|
+
s.authors = ["Matt Palmer"]
|
28
|
+
s.email = ["matt@hezmatt.org"]
|
29
|
+
s.homepage = "https://github.com/pwnedkeys/pwnedkeys-tools"
|
30
|
+
|
31
|
+
s.files = `git ls-files -z`.split("\0").reject { |f| f =~ /^(\.|G|spec|Rakefile)/ }
|
32
|
+
s.executables = ["pwnedkeys-prove-pwned", "pwnedkeys-query"]
|
33
|
+
|
34
|
+
s.required_ruby_version = ">= 2.5.0"
|
35
|
+
|
36
|
+
s.add_development_dependency 'bundler'
|
37
|
+
s.add_development_dependency 'github-release'
|
38
|
+
s.add_development_dependency 'git-version-bump'
|
39
|
+
s.add_development_dependency 'guard-rspec'
|
40
|
+
s.add_development_dependency 'rack-test'
|
41
|
+
s.add_development_dependency 'rake', "~> 12.0"
|
42
|
+
s.add_development_dependency 'redcarpet'
|
43
|
+
s.add_development_dependency 'rspec'
|
44
|
+
s.add_development_dependency 'simplecov'
|
45
|
+
s.add_development_dependency 'yard'
|
46
|
+
end
|
metadata
ADDED
@@ -0,0 +1,211 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pwnedkeys-tools
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Matt Palmer
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-12-18 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: github-release
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: git-version-bump
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: guard-rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rack-test
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rake
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '12.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '12.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: redcarpet
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rspec
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: simplecov
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: yard
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
description: |
|
154
|
+
The scripts in this package are designed to be used in conjunction with the
|
155
|
+
pwnedkeys.com compromised keys database. They include:
|
156
|
+
|
157
|
+
* `pwnedkeys-prove-pwned`, which generates a signed attestation of
|
158
|
+
compromise suitable for being served by the pwnedkeys.com V1 API.
|
159
|
+
|
160
|
+
* `pwnedkeys-query`, which takes a public or private key, CSR, or X.509
|
161
|
+
certificate and looks it up in the pwnedkeys.com database.
|
162
|
+
email:
|
163
|
+
- matt@hezmatt.org
|
164
|
+
executables:
|
165
|
+
- pwnedkeys-prove-pwned
|
166
|
+
- pwnedkeys-query
|
167
|
+
extensions: []
|
168
|
+
extra_rdoc_files: []
|
169
|
+
files:
|
170
|
+
- CODE_OF_CONDUCT.md
|
171
|
+
- CONTRIBUTING.md
|
172
|
+
- Dockerfile
|
173
|
+
- LICENCE
|
174
|
+
- README.md
|
175
|
+
- bin/pwnedkeys-prove-pwned
|
176
|
+
- bin/pwnedkeys-query
|
177
|
+
- docker-wrappers/pwnedkeys-prove-pwned
|
178
|
+
- docker-wrappers/pwnedkeys-query
|
179
|
+
- lib/openssl/pkey.rb
|
180
|
+
- lib/openssl/pkey/ec.rb
|
181
|
+
- lib/openssl/pkey/rsa.rb
|
182
|
+
- lib/openssl/x509/certificate.rb
|
183
|
+
- lib/openssl/x509/request.rb
|
184
|
+
- lib/openssl/x509/spki.rb
|
185
|
+
- lib/pwnedkeys/request.rb
|
186
|
+
- lib/pwnedkeys/response.rb
|
187
|
+
- pwnedkeys-tools.gemspec
|
188
|
+
homepage: https://github.com/pwnedkeys/pwnedkeys-tools
|
189
|
+
licenses: []
|
190
|
+
metadata: {}
|
191
|
+
post_install_message:
|
192
|
+
rdoc_options: []
|
193
|
+
require_paths:
|
194
|
+
- lib
|
195
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
196
|
+
requirements:
|
197
|
+
- - ">="
|
198
|
+
- !ruby/object:Gem::Version
|
199
|
+
version: 2.5.0
|
200
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
201
|
+
requirements:
|
202
|
+
- - ">="
|
203
|
+
- !ruby/object:Gem::Version
|
204
|
+
version: '0'
|
205
|
+
requirements: []
|
206
|
+
rubyforge_project:
|
207
|
+
rubygems_version: 2.7.6
|
208
|
+
signing_key:
|
209
|
+
specification_version: 4
|
210
|
+
summary: A set of command-line tools useful for working with the pwnedkeys.com service
|
211
|
+
test_files: []
|