leap_cli 1.9 → 1.9.1
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 +4 -4
- data/lib/leap_cli/commands/new.rb +2 -3
- data/lib/leap_cli/version.rb +1 -1
- metadata +6 -44
- data/vendor/acme-client/lib/acme/client/certificate.rb +0 -30
- data/vendor/acme-client/lib/acme/client/certificate_request.rb +0 -111
- data/vendor/acme-client/lib/acme/client/crypto.rb +0 -98
- data/vendor/acme-client/lib/acme/client/error.rb +0 -16
- data/vendor/acme-client/lib/acme/client/faraday_middleware.rb +0 -123
- data/vendor/acme-client/lib/acme/client/resources/authorization.rb +0 -44
- data/vendor/acme-client/lib/acme/client/resources/challenges/base.rb +0 -43
- data/vendor/acme-client/lib/acme/client/resources/challenges/dns01.rb +0 -19
- data/vendor/acme-client/lib/acme/client/resources/challenges/http01.rb +0 -18
- data/vendor/acme-client/lib/acme/client/resources/challenges/tls_sni01.rb +0 -24
- data/vendor/acme-client/lib/acme/client/resources/challenges.rb +0 -6
- data/vendor/acme-client/lib/acme/client/resources/registration.rb +0 -37
- data/vendor/acme-client/lib/acme/client/resources.rb +0 -5
- data/vendor/acme-client/lib/acme/client/self_sign_certificate.rb +0 -60
- data/vendor/acme-client/lib/acme/client/version.rb +0 -7
- data/vendor/acme-client/lib/acme/client.rb +0 -122
- data/vendor/acme-client/lib/acme-client.rb +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9e4ec8cc1569dc2e96c778ff53c1539c8e34865f
|
4
|
+
data.tar.gz: 2a2a84cc1d7f152addf10af08ebb36d9f05dface
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ee2957c11ec13d6ffc44ab8b016543f7b1e37f6e9a8f8127e018878cd4db8e9494c2c2309c5ffd70ffe93b08c894fc1f90134ea7f3a3e6b9501623a81ebff757
|
7
|
+
data.tar.gz: 27a85ee4027b6e79995de7896ddb10530a50abb79d94e4c5b4116445429fca04cffb7f5c6963ab614ec5689f1c8d4bc782e9f0f4767d608df57175b22c7d7b75
|
@@ -84,9 +84,8 @@ module LeapCli; module Commands
|
|
84
84
|
assert_bin! 'git'
|
85
85
|
ensure_dir platform_dir
|
86
86
|
Dir.chdir(platform_dir) do
|
87
|
-
log :cloning, "leap_platform into #{platform_dir}"
|
88
|
-
pty_run "git clone --branch
|
89
|
-
pty_run 'git submodule update --init'
|
87
|
+
log :cloning, "leap_platform:stable into #{platform_dir}"
|
88
|
+
pty_run "git clone --branch stable #{DEFAULT_REPO} ."
|
90
89
|
end
|
91
90
|
else
|
92
91
|
bail!
|
data/lib/leap_cli/version.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module LeapCli
|
2
2
|
unless defined?(LeapCli::VERSION)
|
3
|
-
VERSION = '1.9'
|
3
|
+
VERSION = '1.9.1'
|
4
4
|
COMPATIBLE_PLATFORM_VERSION = '0.9'..'0.99'
|
5
5
|
SUMMARY = 'Command line interface to the LEAP platform'
|
6
6
|
DESCRIPTION = 'The command "leap" can be used to manage a bevy of servers running the LEAP platform from the comfort of your own home.'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: leap_cli
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.9.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- LEAP Encryption Access Project
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2017-06-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: minitest
|
@@ -73,39 +73,19 @@ dependencies:
|
|
73
73
|
- !ruby/object:Gem::Version
|
74
74
|
version: '1.11'
|
75
75
|
- !ruby/object:Gem::Dependency
|
76
|
-
name:
|
76
|
+
name: acme-client
|
77
77
|
requirement: !ruby/object:Gem::Requirement
|
78
78
|
requirements:
|
79
79
|
- - "~>"
|
80
80
|
- !ruby/object:Gem::Version
|
81
|
-
version: '0.
|
81
|
+
version: '0.6'
|
82
82
|
type: :runtime
|
83
83
|
prerelease: false
|
84
84
|
version_requirements: !ruby/object:Gem::Requirement
|
85
85
|
requirements:
|
86
86
|
- - "~>"
|
87
87
|
- !ruby/object:Gem::Version
|
88
|
-
version: '0.
|
89
|
-
- !ruby/object:Gem::Dependency
|
90
|
-
name: faraday
|
91
|
-
requirement: !ruby/object:Gem::Requirement
|
92
|
-
requirements:
|
93
|
-
- - "~>"
|
94
|
-
- !ruby/object:Gem::Version
|
95
|
-
version: '0.9'
|
96
|
-
- - ">="
|
97
|
-
- !ruby/object:Gem::Version
|
98
|
-
version: 0.9.1
|
99
|
-
type: :runtime
|
100
|
-
prerelease: false
|
101
|
-
version_requirements: !ruby/object:Gem::Requirement
|
102
|
-
requirements:
|
103
|
-
- - "~>"
|
104
|
-
- !ruby/object:Gem::Version
|
105
|
-
version: '0.9'
|
106
|
-
- - ">="
|
107
|
-
- !ruby/object:Gem::Version
|
108
|
-
version: 0.9.1
|
88
|
+
version: '0.6'
|
109
89
|
- !ruby/object:Gem::Dependency
|
110
90
|
name: ya2yaml
|
111
91
|
requirement: !ruby/object:Gem::Requirement
|
@@ -166,23 +146,6 @@ files:
|
|
166
146
|
- lib/leap_cli/path.rb
|
167
147
|
- lib/leap_cli/util.rb
|
168
148
|
- lib/leap_cli/version.rb
|
169
|
-
- vendor/acme-client/lib/acme-client.rb
|
170
|
-
- vendor/acme-client/lib/acme/client.rb
|
171
|
-
- vendor/acme-client/lib/acme/client/certificate.rb
|
172
|
-
- vendor/acme-client/lib/acme/client/certificate_request.rb
|
173
|
-
- vendor/acme-client/lib/acme/client/crypto.rb
|
174
|
-
- vendor/acme-client/lib/acme/client/error.rb
|
175
|
-
- vendor/acme-client/lib/acme/client/faraday_middleware.rb
|
176
|
-
- vendor/acme-client/lib/acme/client/resources.rb
|
177
|
-
- vendor/acme-client/lib/acme/client/resources/authorization.rb
|
178
|
-
- vendor/acme-client/lib/acme/client/resources/challenges.rb
|
179
|
-
- vendor/acme-client/lib/acme/client/resources/challenges/base.rb
|
180
|
-
- vendor/acme-client/lib/acme/client/resources/challenges/dns01.rb
|
181
|
-
- vendor/acme-client/lib/acme/client/resources/challenges/http01.rb
|
182
|
-
- vendor/acme-client/lib/acme/client/resources/challenges/tls_sni01.rb
|
183
|
-
- vendor/acme-client/lib/acme/client/resources/registration.rb
|
184
|
-
- vendor/acme-client/lib/acme/client/self_sign_certificate.rb
|
185
|
-
- vendor/acme-client/lib/acme/client/version.rb
|
186
149
|
- vendor/base32/lib/base32.rb
|
187
150
|
- vendor/certificate_authority/lib/certificate_authority.rb
|
188
151
|
- vendor/certificate_authority/lib/certificate_authority/certificate.rb
|
@@ -231,9 +194,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
231
194
|
version: '0'
|
232
195
|
requirements: []
|
233
196
|
rubyforge_project:
|
234
|
-
rubygems_version: 2.
|
197
|
+
rubygems_version: 2.5.2
|
235
198
|
signing_key:
|
236
199
|
specification_version: 4
|
237
200
|
summary: Command line interface to the LEAP platform
|
238
201
|
test_files: []
|
239
|
-
has_rdoc:
|
@@ -1,30 +0,0 @@
|
|
1
|
-
class Acme::Client::Certificate
|
2
|
-
extend Forwardable
|
3
|
-
|
4
|
-
attr_reader :x509, :x509_chain, :request, :private_key, :url
|
5
|
-
|
6
|
-
def_delegators :x509, :to_pem, :to_der
|
7
|
-
|
8
|
-
def initialize(certificate, url, chain, request)
|
9
|
-
@x509 = certificate
|
10
|
-
@url = url
|
11
|
-
@x509_chain = chain
|
12
|
-
@request = request
|
13
|
-
end
|
14
|
-
|
15
|
-
def chain_to_pem
|
16
|
-
x509_chain.map(&:to_pem).join
|
17
|
-
end
|
18
|
-
|
19
|
-
def x509_fullchain
|
20
|
-
[x509, *x509_chain]
|
21
|
-
end
|
22
|
-
|
23
|
-
def fullchain_to_pem
|
24
|
-
x509_fullchain.map(&:to_pem).join
|
25
|
-
end
|
26
|
-
|
27
|
-
def common_name
|
28
|
-
x509.subject.to_a.find { |name, _, _| name == 'CN' }[1]
|
29
|
-
end
|
30
|
-
end
|
@@ -1,111 +0,0 @@
|
|
1
|
-
class Acme::Client::CertificateRequest
|
2
|
-
extend Forwardable
|
3
|
-
|
4
|
-
DEFAULT_KEY_LENGTH = 2048
|
5
|
-
DEFAULT_DIGEST = OpenSSL::Digest::SHA256
|
6
|
-
SUBJECT_KEYS = {
|
7
|
-
common_name: 'CN',
|
8
|
-
country_name: 'C',
|
9
|
-
organization_name: 'O',
|
10
|
-
organizational_unit: 'OU',
|
11
|
-
state_or_province: 'ST',
|
12
|
-
locality_name: 'L'
|
13
|
-
}.freeze
|
14
|
-
|
15
|
-
SUBJECT_TYPES = {
|
16
|
-
'CN' => OpenSSL::ASN1::UTF8STRING,
|
17
|
-
'C' => OpenSSL::ASN1::UTF8STRING,
|
18
|
-
'O' => OpenSSL::ASN1::UTF8STRING,
|
19
|
-
'OU' => OpenSSL::ASN1::UTF8STRING,
|
20
|
-
'ST' => OpenSSL::ASN1::UTF8STRING,
|
21
|
-
'L' => OpenSSL::ASN1::UTF8STRING
|
22
|
-
}.freeze
|
23
|
-
|
24
|
-
attr_reader :private_key, :common_name, :names, :subject
|
25
|
-
|
26
|
-
def_delegators :csr, :to_pem, :to_der
|
27
|
-
|
28
|
-
def initialize(common_name: nil, names: [], private_key: generate_private_key, subject: {}, digest: DEFAULT_DIGEST.new)
|
29
|
-
@digest = digest
|
30
|
-
@private_key = private_key
|
31
|
-
@subject = normalize_subject(subject)
|
32
|
-
@common_name = common_name || @subject[SUBJECT_KEYS[:common_name]] || @subject[:common_name]
|
33
|
-
@names = names.to_a.dup
|
34
|
-
normalize_names
|
35
|
-
@subject[SUBJECT_KEYS[:common_name]] ||= @common_name
|
36
|
-
validate_subject
|
37
|
-
end
|
38
|
-
|
39
|
-
def csr
|
40
|
-
@csr ||= generate
|
41
|
-
end
|
42
|
-
|
43
|
-
private
|
44
|
-
|
45
|
-
def generate_private_key
|
46
|
-
OpenSSL::PKey::RSA.new(DEFAULT_KEY_LENGTH)
|
47
|
-
end
|
48
|
-
|
49
|
-
def normalize_subject(subject)
|
50
|
-
@subject = subject.each_with_object({}) do |(key, value), hash|
|
51
|
-
hash[SUBJECT_KEYS.fetch(key, key)] = value.to_s
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
def normalize_names
|
56
|
-
if @common_name
|
57
|
-
@names.unshift(@common_name) unless @names.include?(@common_name)
|
58
|
-
else
|
59
|
-
raise ArgumentError, 'No common name and no list of names given' if @names.empty?
|
60
|
-
@common_name = @names.first
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
def validate_subject
|
65
|
-
validate_subject_attributes
|
66
|
-
validate_subject_common_name
|
67
|
-
end
|
68
|
-
|
69
|
-
def validate_subject_attributes
|
70
|
-
extra_keys = @subject.keys - SUBJECT_KEYS.keys - SUBJECT_KEYS.values
|
71
|
-
return if extra_keys.empty?
|
72
|
-
raise ArgumentError, "Unexpected subject attributes given: #{extra_keys.inspect}"
|
73
|
-
end
|
74
|
-
|
75
|
-
def validate_subject_common_name
|
76
|
-
return if @common_name == @subject[SUBJECT_KEYS[:common_name]]
|
77
|
-
raise ArgumentError, 'Conflicting common name given in arguments and subject'
|
78
|
-
end
|
79
|
-
|
80
|
-
def generate
|
81
|
-
OpenSSL::X509::Request.new.tap do |csr|
|
82
|
-
csr.public_key = @private_key.public_key
|
83
|
-
csr.subject = generate_subject
|
84
|
-
csr.version = 2
|
85
|
-
add_extension(csr)
|
86
|
-
csr.sign @private_key, @digest
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
def generate_subject
|
91
|
-
OpenSSL::X509::Name.new(
|
92
|
-
@subject.map {|name, value|
|
93
|
-
[name, value, SUBJECT_TYPES[name]]
|
94
|
-
}
|
95
|
-
)
|
96
|
-
end
|
97
|
-
|
98
|
-
def add_extension(csr)
|
99
|
-
return if @names.size <= 1
|
100
|
-
|
101
|
-
extension = OpenSSL::X509::ExtensionFactory.new.create_extension(
|
102
|
-
'subjectAltName', @names.map { |name| "DNS:#{name}" }.join(', '), false
|
103
|
-
)
|
104
|
-
csr.add_attribute(
|
105
|
-
OpenSSL::X509::Attribute.new(
|
106
|
-
'extReq',
|
107
|
-
OpenSSL::ASN1::Set.new([OpenSSL::ASN1::Sequence.new([extension])])
|
108
|
-
)
|
109
|
-
)
|
110
|
-
end
|
111
|
-
end
|
@@ -1,98 +0,0 @@
|
|
1
|
-
class Acme::Client::Crypto
|
2
|
-
attr_reader :private_key
|
3
|
-
|
4
|
-
def initialize(private_key)
|
5
|
-
@private_key = private_key
|
6
|
-
end
|
7
|
-
|
8
|
-
def generate_signed_jws(header:, payload:)
|
9
|
-
header = { typ: 'JWT', alg: jws_alg, jwk: jwk }.merge(header)
|
10
|
-
|
11
|
-
encoded_header = urlsafe_base64(header.to_json)
|
12
|
-
encoded_payload = urlsafe_base64(payload.to_json)
|
13
|
-
signature_data = "#{encoded_header}.#{encoded_payload}"
|
14
|
-
|
15
|
-
signature = private_key.sign digest, signature_data
|
16
|
-
encoded_signature = urlsafe_base64(signature)
|
17
|
-
|
18
|
-
{
|
19
|
-
protected: encoded_header,
|
20
|
-
payload: encoded_payload,
|
21
|
-
signature: encoded_signature
|
22
|
-
}.to_json
|
23
|
-
end
|
24
|
-
|
25
|
-
def thumbprint
|
26
|
-
urlsafe_base64 digest.digest(jwk.to_json)
|
27
|
-
end
|
28
|
-
|
29
|
-
def digest
|
30
|
-
OpenSSL::Digest::SHA256.new
|
31
|
-
end
|
32
|
-
|
33
|
-
def urlsafe_base64(data)
|
34
|
-
Base64.urlsafe_encode64(data).sub(/[\s=]*\z/, '')
|
35
|
-
end
|
36
|
-
|
37
|
-
private
|
38
|
-
|
39
|
-
def jws_alg
|
40
|
-
{ 'RSA' => 'RS256', 'EC' => 'ES256' }.fetch(jwk[:kty])
|
41
|
-
end
|
42
|
-
|
43
|
-
def jwk
|
44
|
-
@jwk ||= case private_key
|
45
|
-
when OpenSSL::PKey::RSA
|
46
|
-
rsa_jwk
|
47
|
-
when OpenSSL::PKey::EC
|
48
|
-
ec_jwk
|
49
|
-
else
|
50
|
-
raise ArgumentError, "Can't handle #{private_key} as private key, only OpenSSL::PKey::RSA and OpenSSL::PKey::EC"
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def rsa_jwk
|
55
|
-
{
|
56
|
-
e: urlsafe_base64(public_key.e.to_s(2)),
|
57
|
-
kty: 'RSA',
|
58
|
-
n: urlsafe_base64(public_key.n.to_s(2))
|
59
|
-
}
|
60
|
-
end
|
61
|
-
|
62
|
-
def ec_jwk
|
63
|
-
{
|
64
|
-
crv: curve_name,
|
65
|
-
kty: 'EC',
|
66
|
-
x: urlsafe_base64(coordinates[:x].to_s(2)),
|
67
|
-
y: urlsafe_base64(coordinates[:y].to_s(2))
|
68
|
-
}
|
69
|
-
end
|
70
|
-
|
71
|
-
def curve_name
|
72
|
-
{
|
73
|
-
'prime256v1' => 'P-256',
|
74
|
-
'secp384r1' => 'P-384',
|
75
|
-
'secp521r1' => 'P-521'
|
76
|
-
}.fetch(private_key.group.curve_name) { raise ArgumentError, 'Unknown EC curve' }
|
77
|
-
end
|
78
|
-
|
79
|
-
# rubocop:disable Metrics/AbcSize
|
80
|
-
def coordinates
|
81
|
-
@coordinates ||= begin
|
82
|
-
hex = public_key.to_bn.to_s(16)
|
83
|
-
data_len = hex.length - 2
|
84
|
-
hex_x = hex[2, data_len / 2]
|
85
|
-
hex_y = hex[2 + data_len / 2, data_len / 2]
|
86
|
-
|
87
|
-
{
|
88
|
-
x: OpenSSL::BN.new([hex_x].pack('H*'), 2),
|
89
|
-
y: OpenSSL::BN.new([hex_y].pack('H*'), 2)
|
90
|
-
}
|
91
|
-
end
|
92
|
-
end
|
93
|
-
# rubocop:enable Metrics/AbcSize
|
94
|
-
|
95
|
-
def public_key
|
96
|
-
@public_key ||= private_key.public_key
|
97
|
-
end
|
98
|
-
end
|
@@ -1,16 +0,0 @@
|
|
1
|
-
class Acme::Client::Error < StandardError
|
2
|
-
class NotFound < Acme::Client::Error; end
|
3
|
-
class BadCSR < Acme::Client::Error; end
|
4
|
-
class BadNonce < Acme::Client::Error; end
|
5
|
-
class Connection < Acme::Client::Error; end
|
6
|
-
class Dnssec < Acme::Client::Error; end
|
7
|
-
class Malformed < Acme::Client::Error; end
|
8
|
-
class ServerInternal < Acme::Client::Error; end
|
9
|
-
class Acme::Tls < Acme::Client::Error; end
|
10
|
-
class Unauthorized < Acme::Client::Error; end
|
11
|
-
class UnknownHost < Acme::Client::Error; end
|
12
|
-
class Timeout < Acme::Client::Error; end
|
13
|
-
class RateLimited < Acme::Client::Error; end
|
14
|
-
class RejectedIdentifier < Acme::Client::Error; end
|
15
|
-
class UnsupportedIdentifier < Acme::Client::Error; end
|
16
|
-
end
|
@@ -1,123 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
class Acme::Client::FaradayMiddleware < Faraday::Middleware
|
4
|
-
attr_reader :env, :response, :client
|
5
|
-
|
6
|
-
repo_url = 'https://github.com/unixcharles/acme-client'
|
7
|
-
USER_AGENT = "Acme::Client v#{Acme::Client::VERSION} (#{repo_url})".freeze
|
8
|
-
|
9
|
-
def initialize(app, client:)
|
10
|
-
super(app)
|
11
|
-
@client = client
|
12
|
-
end
|
13
|
-
|
14
|
-
def call(env)
|
15
|
-
@env = env
|
16
|
-
@env[:request_headers]['User-Agent'] = USER_AGENT
|
17
|
-
@env.body = crypto.generate_signed_jws(header: { nonce: pop_nonce }, payload: env.body)
|
18
|
-
@app.call(env).on_complete { |response_env| on_complete(response_env) }
|
19
|
-
rescue Faraday::TimeoutError
|
20
|
-
raise Acme::Client::Error::Timeout
|
21
|
-
end
|
22
|
-
|
23
|
-
def on_complete(env)
|
24
|
-
@env = env
|
25
|
-
|
26
|
-
raise_on_not_found!
|
27
|
-
store_nonce
|
28
|
-
env.body = decode_body
|
29
|
-
env.response_headers['Link'] = decode_link_headers
|
30
|
-
|
31
|
-
return if env.success?
|
32
|
-
|
33
|
-
raise_on_error!
|
34
|
-
end
|
35
|
-
|
36
|
-
private
|
37
|
-
|
38
|
-
def raise_on_not_found!
|
39
|
-
raise Acme::Client::Error::NotFound, env.url.to_s if env.status == 404
|
40
|
-
end
|
41
|
-
|
42
|
-
def raise_on_error!
|
43
|
-
raise error_class, error_message
|
44
|
-
end
|
45
|
-
|
46
|
-
def error_message
|
47
|
-
if env.body.is_a? Hash
|
48
|
-
env.body['detail']
|
49
|
-
else
|
50
|
-
"Error message: #{env.body}"
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def error_class
|
55
|
-
if error_name && !error_name.empty? && Acme::Client::Error.const_defined?(error_name)
|
56
|
-
Object.const_get("Acme::Client::Error::#{error_name}")
|
57
|
-
else
|
58
|
-
Acme::Client::Error
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
def error_name
|
63
|
-
@error_name ||= begin
|
64
|
-
return unless env.body.is_a?(Hash)
|
65
|
-
return unless env.body.key?('type')
|
66
|
-
|
67
|
-
env.body['type'].gsub('urn:acme:error:', '').split(/[_-]/).map(&:capitalize).join
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
def decode_body
|
72
|
-
content_type = env.response_headers['Content-Type']
|
73
|
-
|
74
|
-
if content_type == 'application/json' || content_type == 'application/problem+json'
|
75
|
-
JSON.load(env.body)
|
76
|
-
else
|
77
|
-
env.body
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
LINK_MATCH = /<(.*?)>;rel="([\w-]+)"/
|
82
|
-
|
83
|
-
def decode_link_headers
|
84
|
-
return unless env.response_headers.key?('Link')
|
85
|
-
link_header = env.response_headers['Link']
|
86
|
-
|
87
|
-
links = link_header.split(', ').map { |entry|
|
88
|
-
_, link, name = *entry.match(LINK_MATCH)
|
89
|
-
[name, link]
|
90
|
-
}
|
91
|
-
|
92
|
-
Hash[*links.flatten]
|
93
|
-
end
|
94
|
-
|
95
|
-
def store_nonce
|
96
|
-
nonces << env.response_headers['replay-nonce']
|
97
|
-
end
|
98
|
-
|
99
|
-
def pop_nonce
|
100
|
-
if nonces.empty?
|
101
|
-
get_nonce
|
102
|
-
else
|
103
|
-
nonces.pop
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
def get_nonce
|
108
|
-
response = Faraday.head(env.url, nil, 'User-Agent' => USER_AGENT)
|
109
|
-
response.headers['replay-nonce']
|
110
|
-
end
|
111
|
-
|
112
|
-
def nonces
|
113
|
-
client.nonces
|
114
|
-
end
|
115
|
-
|
116
|
-
def private_key
|
117
|
-
client.private_key
|
118
|
-
end
|
119
|
-
|
120
|
-
def crypto
|
121
|
-
@crypto ||= Acme::Client::Crypto.new(private_key)
|
122
|
-
end
|
123
|
-
end
|
@@ -1,44 +0,0 @@
|
|
1
|
-
class Acme::Client::Resources::Authorization
|
2
|
-
HTTP01 = Acme::Client::Resources::Challenges::HTTP01
|
3
|
-
DNS01 = Acme::Client::Resources::Challenges::DNS01
|
4
|
-
TLSSNI01 = Acme::Client::Resources::Challenges::TLSSNI01
|
5
|
-
|
6
|
-
attr_reader :client, :uri, :domain, :status, :expires, :http01, :dns01, :tls_sni01
|
7
|
-
|
8
|
-
def initialize(client, uri, response)
|
9
|
-
@client = client
|
10
|
-
@uri = uri
|
11
|
-
assign_attributes(response.body)
|
12
|
-
end
|
13
|
-
|
14
|
-
def verify_status
|
15
|
-
response = @client.connection.get(@uri)
|
16
|
-
|
17
|
-
assign_attributes(response.body)
|
18
|
-
status
|
19
|
-
end
|
20
|
-
|
21
|
-
private
|
22
|
-
|
23
|
-
def assign_attributes(body)
|
24
|
-
@expires = Time.iso8601(body['expires']) if body.key? 'expires'
|
25
|
-
@domain = body['identifier']['value']
|
26
|
-
@status = body['status']
|
27
|
-
assign_challenges(body['challenges'])
|
28
|
-
end
|
29
|
-
|
30
|
-
def assign_challenges(challenges)
|
31
|
-
challenges.each do |attributes|
|
32
|
-
challenge = case attributes.fetch('type')
|
33
|
-
when 'http-01'
|
34
|
-
@http01 ||= HTTP01.new(self)
|
35
|
-
when 'dns-01'
|
36
|
-
@dns01 ||= DNS01.new(self)
|
37
|
-
when 'tls-sni-01'
|
38
|
-
@tls_sni01 ||= TLSSNI01.new(self)
|
39
|
-
end
|
40
|
-
|
41
|
-
challenge.assign_attributes(attributes) if challenge
|
42
|
-
end
|
43
|
-
end
|
44
|
-
end
|
@@ -1,43 +0,0 @@
|
|
1
|
-
class Acme::Client::Resources::Challenges::Base
|
2
|
-
attr_reader :authorization, :status, :uri, :token, :error
|
3
|
-
|
4
|
-
def initialize(authorization)
|
5
|
-
@authorization = authorization
|
6
|
-
end
|
7
|
-
|
8
|
-
def client
|
9
|
-
authorization.client
|
10
|
-
end
|
11
|
-
|
12
|
-
def verify_status
|
13
|
-
authorization.verify_status
|
14
|
-
|
15
|
-
status
|
16
|
-
end
|
17
|
-
|
18
|
-
def request_verification
|
19
|
-
response = client.connection.post(@uri, resource: 'challenge', type: challenge_type, keyAuthorization: authorization_key)
|
20
|
-
response.success?
|
21
|
-
end
|
22
|
-
|
23
|
-
def assign_attributes(attributes)
|
24
|
-
@status = attributes.fetch('status', 'pending')
|
25
|
-
@uri = attributes.fetch('uri')
|
26
|
-
@token = attributes.fetch('token')
|
27
|
-
@error = attributes['error']
|
28
|
-
end
|
29
|
-
|
30
|
-
private
|
31
|
-
|
32
|
-
def challenge_type
|
33
|
-
self.class::CHALLENGE_TYPE
|
34
|
-
end
|
35
|
-
|
36
|
-
def authorization_key
|
37
|
-
"#{token}.#{crypto.thumbprint}"
|
38
|
-
end
|
39
|
-
|
40
|
-
def crypto
|
41
|
-
@crypto ||= Acme::Client::Crypto.new(client.private_key)
|
42
|
-
end
|
43
|
-
end
|
@@ -1,19 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
class Acme::Client::Resources::Challenges::DNS01 < Acme::Client::Resources::Challenges::Base
|
4
|
-
CHALLENGE_TYPE = 'dns-01'.freeze
|
5
|
-
RECORD_NAME = '_acme-challenge'.freeze
|
6
|
-
RECORD_TYPE = 'TXT'.freeze
|
7
|
-
|
8
|
-
def record_name
|
9
|
-
RECORD_NAME
|
10
|
-
end
|
11
|
-
|
12
|
-
def record_type
|
13
|
-
RECORD_TYPE
|
14
|
-
end
|
15
|
-
|
16
|
-
def record_content
|
17
|
-
crypto.urlsafe_base64(crypto.digest.digest(authorization_key))
|
18
|
-
end
|
19
|
-
end
|
@@ -1,18 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
class Acme::Client::Resources::Challenges::HTTP01 < Acme::Client::Resources::Challenges::Base
|
4
|
-
CHALLENGE_TYPE = 'http-01'.freeze
|
5
|
-
CONTENT_TYPE = 'text/plain'.freeze
|
6
|
-
|
7
|
-
def content_type
|
8
|
-
CONTENT_TYPE
|
9
|
-
end
|
10
|
-
|
11
|
-
def file_content
|
12
|
-
authorization_key
|
13
|
-
end
|
14
|
-
|
15
|
-
def filename
|
16
|
-
".well-known/acme-challenge/#{token}"
|
17
|
-
end
|
18
|
-
end
|
@@ -1,24 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
class Acme::Client::Resources::Challenges::TLSSNI01 < Acme::Client::Resources::Challenges::Base
|
4
|
-
CHALLENGE_TYPE = 'tls-sni-01'.freeze
|
5
|
-
|
6
|
-
def hostname
|
7
|
-
digest = crypto.digest.hexdigest(authorization_key)
|
8
|
-
"#{digest[0..31]}.#{digest[32..64]}.acme.invalid"
|
9
|
-
end
|
10
|
-
|
11
|
-
def certificate
|
12
|
-
self_sign_certificate.certificate
|
13
|
-
end
|
14
|
-
|
15
|
-
def private_key
|
16
|
-
self_sign_certificate.private_key
|
17
|
-
end
|
18
|
-
|
19
|
-
private
|
20
|
-
|
21
|
-
def self_sign_certificate
|
22
|
-
@self_sign_certificate ||= Acme::Client::SelfSignCertificate.new(subject_alt_names: [hostname])
|
23
|
-
end
|
24
|
-
end
|
@@ -1,37 +0,0 @@
|
|
1
|
-
class Acme::Client::Resources::Registration
|
2
|
-
attr_reader :id, :key, :contact, :uri, :next_uri, :recover_uri, :term_of_service_uri
|
3
|
-
|
4
|
-
def initialize(client, response)
|
5
|
-
@client = client
|
6
|
-
@uri = response.headers['location']
|
7
|
-
assign_links(response.headers['Link'])
|
8
|
-
assign_attributes(response.body)
|
9
|
-
end
|
10
|
-
|
11
|
-
def get_terms
|
12
|
-
return unless @term_of_service_uri
|
13
|
-
|
14
|
-
@client.connection.get(@term_of_service_uri).body
|
15
|
-
end
|
16
|
-
|
17
|
-
def agree_terms
|
18
|
-
return true unless @term_of_service_uri
|
19
|
-
|
20
|
-
response = @client.connection.post(@uri, resource: 'reg', agreement: @term_of_service_uri)
|
21
|
-
response.success?
|
22
|
-
end
|
23
|
-
|
24
|
-
private
|
25
|
-
|
26
|
-
def assign_links(links)
|
27
|
-
@next_uri = links['next']
|
28
|
-
@recover_uri = links['recover']
|
29
|
-
@term_of_service_uri = links['terms-of-service']
|
30
|
-
end
|
31
|
-
|
32
|
-
def assign_attributes(body)
|
33
|
-
@id = body['id']
|
34
|
-
@key = body['key']
|
35
|
-
@contact = body['contact']
|
36
|
-
end
|
37
|
-
end
|
@@ -1,60 +0,0 @@
|
|
1
|
-
class Acme::Client::SelfSignCertificate
|
2
|
-
attr_reader :private_key, :subject_alt_names, :not_before, :not_after
|
3
|
-
|
4
|
-
extend Forwardable
|
5
|
-
def_delegators :certificate, :to_pem, :to_der
|
6
|
-
|
7
|
-
def initialize(subject_alt_names:, not_before: default_not_before, not_after: default_not_after, private_key: generate_private_key)
|
8
|
-
@private_key = private_key
|
9
|
-
@subject_alt_names = subject_alt_names
|
10
|
-
@not_before = not_before
|
11
|
-
@not_after = not_after
|
12
|
-
end
|
13
|
-
|
14
|
-
def certificate
|
15
|
-
@certificate ||= begin
|
16
|
-
certificate = generate_certificate
|
17
|
-
|
18
|
-
extension_factory = generate_extension_factory(certificate)
|
19
|
-
subject_alt_name_entry = subject_alt_names.map { |d| "DNS: #{d}" }.join(',')
|
20
|
-
subject_alt_name_extension = extension_factory.create_extension('subjectAltName', subject_alt_name_entry)
|
21
|
-
certificate.add_extension(subject_alt_name_extension)
|
22
|
-
|
23
|
-
certificate.sign(private_key, digest)
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
private
|
28
|
-
|
29
|
-
def generate_private_key
|
30
|
-
OpenSSL::PKey::RSA.new(2048)
|
31
|
-
end
|
32
|
-
|
33
|
-
def default_not_before
|
34
|
-
Time.now - 3600
|
35
|
-
end
|
36
|
-
|
37
|
-
def default_not_after
|
38
|
-
Time.now + 30 * 24 * 3600
|
39
|
-
end
|
40
|
-
|
41
|
-
def digest
|
42
|
-
OpenSSL::Digest::SHA256.new
|
43
|
-
end
|
44
|
-
|
45
|
-
def generate_certificate
|
46
|
-
certificate = OpenSSL::X509::Certificate.new
|
47
|
-
certificate.not_before = not_before
|
48
|
-
certificate.not_after = not_after
|
49
|
-
certificate.public_key = private_key.public_key
|
50
|
-
certificate.version = 2
|
51
|
-
certificate
|
52
|
-
end
|
53
|
-
|
54
|
-
def generate_extension_factory(certificate)
|
55
|
-
extension_factory = OpenSSL::X509::ExtensionFactory.new
|
56
|
-
extension_factory.subject_certificate = certificate
|
57
|
-
extension_factory.issuer_certificate = certificate
|
58
|
-
extension_factory
|
59
|
-
end
|
60
|
-
end
|
@@ -1,122 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'faraday'
|
4
|
-
require 'json'
|
5
|
-
require 'openssl'
|
6
|
-
require 'digest'
|
7
|
-
require 'forwardable'
|
8
|
-
require 'base64'
|
9
|
-
require 'time'
|
10
|
-
|
11
|
-
module Acme; end
|
12
|
-
class Acme::Client; end
|
13
|
-
|
14
|
-
require 'acme/client/version'
|
15
|
-
require 'acme/client/certificate'
|
16
|
-
require 'acme/client/certificate_request'
|
17
|
-
require 'acme/client/self_sign_certificate'
|
18
|
-
require 'acme/client/crypto'
|
19
|
-
require 'acme/client/resources'
|
20
|
-
require 'acme/client/faraday_middleware'
|
21
|
-
require 'acme/client/error'
|
22
|
-
|
23
|
-
class Acme::Client
|
24
|
-
DEFAULT_ENDPOINT = 'http://127.0.0.1:4000'.freeze
|
25
|
-
DIRECTORY_DEFAULT = {
|
26
|
-
'new-authz' => '/acme/new-authz',
|
27
|
-
'new-cert' => '/acme/new-cert',
|
28
|
-
'new-reg' => '/acme/new-reg',
|
29
|
-
'revoke-cert' => '/acme/revoke-cert'
|
30
|
-
}.freeze
|
31
|
-
|
32
|
-
def initialize(private_key:, endpoint: DEFAULT_ENDPOINT, directory_uri: nil, connection_options: {})
|
33
|
-
@endpoint, @private_key, @directory_uri, @connection_options = endpoint, private_key, directory_uri, connection_options
|
34
|
-
@nonces ||= []
|
35
|
-
load_directory!
|
36
|
-
end
|
37
|
-
|
38
|
-
attr_reader :private_key, :nonces, :operation_endpoints
|
39
|
-
|
40
|
-
def register(contact:)
|
41
|
-
payload = {
|
42
|
-
resource: 'new-reg', contact: Array(contact)
|
43
|
-
}
|
44
|
-
|
45
|
-
response = connection.post(@operation_endpoints.fetch('new-reg'), payload)
|
46
|
-
::Acme::Client::Resources::Registration.new(self, response)
|
47
|
-
end
|
48
|
-
|
49
|
-
def authorize(domain:)
|
50
|
-
payload = {
|
51
|
-
resource: 'new-authz',
|
52
|
-
identifier: {
|
53
|
-
type: 'dns',
|
54
|
-
value: domain
|
55
|
-
}
|
56
|
-
}
|
57
|
-
|
58
|
-
response = connection.post(@operation_endpoints.fetch('new-authz'), payload)
|
59
|
-
::Acme::Client::Resources::Authorization.new(self, response.headers['Location'], response)
|
60
|
-
end
|
61
|
-
|
62
|
-
def fetch_authorization(uri)
|
63
|
-
response = connection.get(uri)
|
64
|
-
::Acme::Client::Resources::Authorization.new(self, uri, response)
|
65
|
-
end
|
66
|
-
|
67
|
-
def new_certificate(csr)
|
68
|
-
payload = {
|
69
|
-
resource: 'new-cert',
|
70
|
-
csr: Base64.urlsafe_encode64(csr.to_der)
|
71
|
-
}
|
72
|
-
|
73
|
-
response = connection.post(@operation_endpoints.fetch('new-cert'), payload)
|
74
|
-
::Acme::Client::Certificate.new(OpenSSL::X509::Certificate.new(response.body), response.headers['location'], fetch_chain(response), csr)
|
75
|
-
end
|
76
|
-
|
77
|
-
def revoke_certificate(certificate)
|
78
|
-
payload = { resource: 'revoke-cert', certificate: Base64.urlsafe_encode64(certificate.to_der) }
|
79
|
-
endpoint = @operation_endpoints.fetch('revoke-cert')
|
80
|
-
response = connection.post(endpoint, payload)
|
81
|
-
response.success?
|
82
|
-
end
|
83
|
-
|
84
|
-
def self.revoke_certificate(certificate, *arguments)
|
85
|
-
client = new(*arguments)
|
86
|
-
client.revoke_certificate(certificate)
|
87
|
-
end
|
88
|
-
|
89
|
-
def connection
|
90
|
-
@connection ||= Faraday.new(@endpoint, **@connection_options) do |configuration|
|
91
|
-
configuration.use Acme::Client::FaradayMiddleware, client: self
|
92
|
-
configuration.adapter Faraday.default_adapter
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
private
|
97
|
-
|
98
|
-
def fetch_chain(response, limit = 10)
|
99
|
-
links = response.headers['link']
|
100
|
-
if limit.zero? || links.nil? || links['up'].nil?
|
101
|
-
[]
|
102
|
-
else
|
103
|
-
issuer = connection.get(links['up'])
|
104
|
-
[OpenSSL::X509::Certificate.new(issuer.body), *fetch_chain(issuer, limit - 1)]
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
|
-
def load_directory!
|
109
|
-
@operation_endpoints = if @directory_uri
|
110
|
-
response = connection.get(@directory_uri)
|
111
|
-
body = response.body
|
112
|
-
{
|
113
|
-
'new-reg' => body.fetch('new-reg'),
|
114
|
-
'new-authz' => body.fetch('new-authz'),
|
115
|
-
'new-cert' => body.fetch('new-cert'),
|
116
|
-
'revoke-cert' => body.fetch('revoke-cert'),
|
117
|
-
}
|
118
|
-
else
|
119
|
-
DIRECTORY_DEFAULT
|
120
|
-
end
|
121
|
-
end
|
122
|
-
end
|
@@ -1 +0,0 @@
|
|
1
|
-
require 'acme/client'
|