leap_cli 1.8.1 → 1.9
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/bin/leap +6 -12
- data/lib/leap_cli.rb +3 -23
- data/lib/leap_cli/bootstrap.rb +36 -12
- data/lib/leap_cli/commands/common.rb +88 -46
- data/lib/leap_cli/commands/new.rb +24 -17
- data/lib/leap_cli/commands/pre.rb +3 -1
- data/lib/leap_cli/core_ext/hash.rb +19 -0
- data/lib/leap_cli/leapfile.rb +47 -32
- data/lib/leap_cli/log.rb +196 -88
- data/lib/leap_cli/path.rb +5 -5
- data/lib/leap_cli/util.rb +28 -18
- data/lib/leap_cli/version.rb +8 -3
- data/vendor/acme-client/lib/acme-client.rb +1 -0
- data/vendor/acme-client/lib/acme/client.rb +122 -0
- data/vendor/acme-client/lib/acme/client/certificate.rb +30 -0
- data/vendor/acme-client/lib/acme/client/certificate_request.rb +111 -0
- data/vendor/acme-client/lib/acme/client/crypto.rb +98 -0
- data/vendor/acme-client/lib/acme/client/error.rb +16 -0
- data/vendor/acme-client/lib/acme/client/faraday_middleware.rb +123 -0
- data/vendor/acme-client/lib/acme/client/resources.rb +5 -0
- data/vendor/acme-client/lib/acme/client/resources/authorization.rb +44 -0
- data/vendor/acme-client/lib/acme/client/resources/challenges.rb +6 -0
- data/vendor/acme-client/lib/acme/client/resources/challenges/base.rb +43 -0
- data/vendor/acme-client/lib/acme/client/resources/challenges/dns01.rb +19 -0
- data/vendor/acme-client/lib/acme/client/resources/challenges/http01.rb +18 -0
- data/vendor/acme-client/lib/acme/client/resources/challenges/tls_sni01.rb +24 -0
- data/vendor/acme-client/lib/acme/client/resources/registration.rb +37 -0
- data/vendor/acme-client/lib/acme/client/self_sign_certificate.rb +60 -0
- data/vendor/acme-client/lib/acme/client/version.rb +7 -0
- data/vendor/base32/lib/base32.rb +67 -0
- data/vendor/certificate_authority/lib/certificate_authority.rb +2 -1
- data/vendor/certificate_authority/lib/certificate_authority/certificate.rb +4 -4
- data/vendor/certificate_authority/lib/certificate_authority/certificate_revocation_list.rb +7 -5
- data/vendor/certificate_authority/lib/certificate_authority/core_extensions.rb +46 -0
- data/vendor/certificate_authority/lib/certificate_authority/distinguished_name.rb +6 -2
- data/vendor/certificate_authority/lib/certificate_authority/extensions.rb +10 -3
- data/vendor/certificate_authority/lib/certificate_authority/key_material.rb +11 -9
- data/vendor/certificate_authority/lib/certificate_authority/ocsp_handler.rb +3 -3
- data/vendor/certificate_authority/lib/certificate_authority/pkcs11_key_material.rb +0 -2
- data/vendor/certificate_authority/lib/certificate_authority/serial_number.rb +8 -2
- data/vendor/certificate_authority/lib/certificate_authority/validations.rb +31 -0
- data/vendor/rsync_command/lib/rsync_command.rb +49 -12
- metadata +50 -91
- data/lib/leap/platform.rb +0 -90
- data/lib/leap_cli/config/environment.rb +0 -180
- data/lib/leap_cli/config/filter.rb +0 -178
- data/lib/leap_cli/config/manager.rb +0 -419
- data/lib/leap_cli/config/node.rb +0 -77
- data/lib/leap_cli/config/object.rb +0 -428
- data/lib/leap_cli/config/object_list.rb +0 -209
- data/lib/leap_cli/config/provider.rb +0 -22
- data/lib/leap_cli/config/secrets.rb +0 -87
- data/lib/leap_cli/config/sources.rb +0 -11
- data/lib/leap_cli/config/tag.rb +0 -25
- data/lib/leap_cli/lib_ext/capistrano_connections.rb +0 -16
- data/lib/leap_cli/logger.rb +0 -237
- data/lib/leap_cli/remote/leap_plugin.rb +0 -192
- data/lib/leap_cli/remote/puppet_plugin.rb +0 -26
- data/lib/leap_cli/remote/rsync_plugin.rb +0 -35
- data/lib/leap_cli/remote/tasks.rb +0 -51
- data/lib/leap_cli/ssh_key.rb +0 -195
- data/lib/leap_cli/util/remote_command.rb +0 -158
- data/lib/leap_cli/util/secret.rb +0 -55
- data/lib/leap_cli/util/x509.rb +0 -33
@@ -0,0 +1,98 @@
|
|
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
|
@@ -0,0 +1,16 @@
|
|
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
|
@@ -0,0 +1,123 @@
|
|
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
|
@@ -0,0 +1,44 @@
|
|
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
|
@@ -0,0 +1,43 @@
|
|
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
|
@@ -0,0 +1,19 @@
|
|
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
|
@@ -0,0 +1,18 @@
|
|
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
|
@@ -0,0 +1,24 @@
|
|
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
|
@@ -0,0 +1,37 @@
|
|
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
|