acme-client 1.0.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.rubocop.yml +36 -0
- data/README.md +138 -94
- data/lib/acme/client.rb +223 -63
- data/lib/acme/client/certificate_request.rb +0 -2
- data/lib/acme/client/error.rb +52 -13
- data/lib/acme/client/faraday_middleware.rb +23 -24
- data/lib/acme/client/jwk/base.rb +7 -6
- data/lib/acme/client/jwk/ecdsa.rb +0 -2
- data/lib/acme/client/resources.rb +4 -2
- data/lib/acme/client/resources/account.rb +49 -0
- data/lib/acme/client/resources/authorization.rb +62 -32
- data/lib/acme/client/resources/challenges.rb +20 -5
- data/lib/acme/client/resources/challenges/base.rb +26 -22
- data/lib/acme/client/resources/challenges/dns01.rb +1 -1
- data/lib/acme/client/resources/challenges/http01.rb +1 -1
- data/lib/acme/client/resources/directory.rb +75 -0
- data/lib/acme/client/resources/order.rb +58 -0
- data/lib/acme/client/version.rb +1 -1
- metadata +5 -5
- data/lib/acme/client/certificate.rb +0 -30
- data/lib/acme/client/resources/challenges/tls_sni01.rb +0 -25
- data/lib/acme/client/resources/registration.rb +0 -37
@@ -104,8 +104,6 @@ class Acme::Client::CertificateRequest
|
|
104
104
|
end
|
105
105
|
|
106
106
|
def add_extension(csr)
|
107
|
-
return if @names.size <= 1
|
108
|
-
|
109
107
|
extension = OpenSSL::X509::ExtensionFactory.new.create_extension(
|
110
108
|
'subjectAltName', @names.map { |name| "DNS:#{name}" }.join(', '), false
|
111
109
|
)
|
data/lib/acme/client/error.rb
CHANGED
@@ -1,16 +1,55 @@
|
|
1
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
2
|
class Timeout < Acme::Client::Error; end
|
13
|
-
|
14
|
-
class
|
15
|
-
class
|
3
|
+
|
4
|
+
class ClientError < Acme::Client::Error; end
|
5
|
+
class InvalidDirectory < ClientError; end
|
6
|
+
class UnsupportedOperation < ClientError; end
|
7
|
+
class UnsupportedChallengeType < ClientError; end
|
8
|
+
class NotFound < ClientError; end
|
9
|
+
class CertificateNotReady < ClientError; end
|
10
|
+
|
11
|
+
class ServerError < Acme::Client::Error; end
|
12
|
+
class BadCSR < ServerError; end
|
13
|
+
class BadNonce < ServerError; end
|
14
|
+
class BadSignatureAlgorithm < ServerError; end
|
15
|
+
class InvalidContact < ServerError; end
|
16
|
+
class UnsupportedContact < ServerError; end
|
17
|
+
class ExternalAccountRequired < ServerError; end
|
18
|
+
class AccountDoesNotExist < ServerError; end
|
19
|
+
class Malformed < ServerError; end
|
20
|
+
class RateLimited < ServerError; end
|
21
|
+
class RejectedIdentifier < ServerError; end
|
22
|
+
class ServerInternal < ServerError; end
|
23
|
+
class Unauthorized < ServerError; end
|
24
|
+
class UnsupportedIdentifier < ServerError; end
|
25
|
+
class UserActionRequired < ServerError; end
|
26
|
+
class BadRevocationReason < ServerError; end
|
27
|
+
class Caa < ServerError; end
|
28
|
+
class Dns < ServerError; end
|
29
|
+
class Connection < ServerError; end
|
30
|
+
class Tls < ServerError; end
|
31
|
+
class IncorrectResponse < ServerError; end
|
32
|
+
|
33
|
+
ACME_ERRORS = {
|
34
|
+
'urn:ietf:params:acme:error:badCSR' => BadCSR,
|
35
|
+
'urn:ietf:params:acme:error:badNonce' => BadNonce,
|
36
|
+
'urn:ietf:params:acme:error:badSignatureAlgorithm' => BadSignatureAlgorithm,
|
37
|
+
'urn:ietf:params:acme:error:invalidContact' => InvalidContact,
|
38
|
+
'urn:ietf:params:acme:error:unsupportedContact' => UnsupportedContact,
|
39
|
+
'urn:ietf:params:acme:error:externalAccountRequired' => ExternalAccountRequired,
|
40
|
+
'urn:ietf:params:acme:error:accountDoesNotExist' => AccountDoesNotExist,
|
41
|
+
'urn:ietf:params:acme:error:malformed' => Malformed,
|
42
|
+
'urn:ietf:params:acme:error:rateLimited' => RateLimited,
|
43
|
+
'urn:ietf:params:acme:error:rejectedIdentifier' => RejectedIdentifier,
|
44
|
+
'urn:ietf:params:acme:error:serverInternal' => ServerInternal,
|
45
|
+
'urn:ietf:params:acme:error:unauthorized' => Unauthorized,
|
46
|
+
'urn:ietf:params:acme:error:unsupportedIdentifier' => UnsupportedIdentifier,
|
47
|
+
'urn:ietf:params:acme:error:userActionRequired' => UserActionRequired,
|
48
|
+
'urn:ietf:params:acme:error:badRevocationReason' => BadRevocationReason,
|
49
|
+
'urn:ietf:params:acme:error:caa' => Caa,
|
50
|
+
'urn:ietf:params:acme:error:dns' => Dns,
|
51
|
+
'urn:ietf:params:acme:error:connection' => Connection,
|
52
|
+
'urn:ietf:params:acme:error:tls' => Tls,
|
53
|
+
'urn:ietf:params:acme:error:incorrectResponse' => IncorrectResponse
|
54
|
+
}
|
16
55
|
end
|
@@ -3,18 +3,20 @@
|
|
3
3
|
class Acme::Client::FaradayMiddleware < Faraday::Middleware
|
4
4
|
attr_reader :env, :response, :client
|
5
5
|
|
6
|
-
|
7
|
-
USER_AGENT = "Acme::Client v#{Acme::Client::VERSION} (#{repo_url})".freeze
|
6
|
+
CONTENT_TYPE = 'application/jose+json'
|
8
7
|
|
9
|
-
def initialize(app, client:)
|
8
|
+
def initialize(app, client:, mode:)
|
10
9
|
super(app)
|
11
10
|
@client = client
|
11
|
+
@mode = mode
|
12
12
|
end
|
13
13
|
|
14
14
|
def call(env)
|
15
15
|
@env = env
|
16
|
-
@env[:request_headers]['User-Agent'] = USER_AGENT
|
17
|
-
@env
|
16
|
+
@env[:request_headers]['User-Agent'] = Acme::Client::USER_AGENT
|
17
|
+
@env[:request_headers]['Content-Type'] = CONTENT_TYPE
|
18
|
+
|
19
|
+
@env.body = client.jwk.jws(header: jws_header, payload: env.body)
|
18
20
|
@app.call(env).on_complete { |response_env| on_complete(response_env) }
|
19
21
|
rescue Faraday::TimeoutError, Faraday::ConnectionFailed
|
20
22
|
raise Acme::Client::Error::Timeout
|
@@ -35,6 +37,12 @@ class Acme::Client::FaradayMiddleware < Faraday::Middleware
|
|
35
37
|
|
36
38
|
private
|
37
39
|
|
40
|
+
def jws_header
|
41
|
+
headers = { nonce: pop_nonce, url: env.url.to_s }
|
42
|
+
headers[:kid] = client.kid if @mode == :kid
|
43
|
+
headers
|
44
|
+
end
|
45
|
+
|
38
46
|
def raise_on_not_found!
|
39
47
|
raise Acme::Client::Error::NotFound, env.url.to_s if env.status == 404
|
40
48
|
end
|
@@ -52,29 +60,20 @@ class Acme::Client::FaradayMiddleware < Faraday::Middleware
|
|
52
60
|
end
|
53
61
|
|
54
62
|
def error_class
|
55
|
-
|
56
|
-
Object.const_get("Acme::Client::Error::#{error_name}")
|
57
|
-
else
|
58
|
-
Acme::Client::Error
|
59
|
-
end
|
63
|
+
Acme::Client::Error::ACME_ERRORS.fetch(error_name, Acme::Client::Error)
|
60
64
|
end
|
61
65
|
|
62
66
|
def error_name
|
63
67
|
return unless env.body.is_a?(Hash)
|
64
68
|
return unless env.body.key?('type')
|
65
|
-
|
66
|
-
error_type_to_klass env.body['type']
|
67
|
-
end
|
68
|
-
|
69
|
-
def error_type_to_klass(type)
|
70
|
-
type.gsub('urn:acme:error:', '').split(/[_-]/).map { |type_part| type_part[0].upcase + type_part[1..-1] }.join
|
69
|
+
env.body['type']
|
71
70
|
end
|
72
71
|
|
73
72
|
def decode_body
|
74
|
-
content_type = env.response_headers['Content-Type']
|
73
|
+
content_type = env.response_headers['Content-Type'].to_s
|
75
74
|
|
76
|
-
if content_type
|
77
|
-
JSON.
|
75
|
+
if content_type.start_with?('application/json', 'application/problem+json')
|
76
|
+
JSON.load(env.body)
|
78
77
|
else
|
79
78
|
env.body
|
80
79
|
end
|
@@ -95,20 +94,20 @@ class Acme::Client::FaradayMiddleware < Faraday::Middleware
|
|
95
94
|
end
|
96
95
|
|
97
96
|
def store_nonce
|
98
|
-
|
97
|
+
nonce = env.response_headers['replay-nonce']
|
98
|
+
nonces << nonce if nonce
|
99
99
|
end
|
100
100
|
|
101
101
|
def pop_nonce
|
102
102
|
if nonces.empty?
|
103
103
|
get_nonce
|
104
|
-
else
|
105
|
-
nonces.pop
|
106
104
|
end
|
105
|
+
|
106
|
+
nonces.pop
|
107
107
|
end
|
108
108
|
|
109
109
|
def get_nonce
|
110
|
-
|
111
|
-
response.headers['replay-nonce']
|
110
|
+
client.get_nonce
|
112
111
|
end
|
113
112
|
|
114
113
|
def nonces
|
data/lib/acme/client/jwk/base.rb
CHANGED
@@ -15,7 +15,7 @@ class Acme::Client::JWK::Base
|
|
15
15
|
#
|
16
16
|
# Returns a JSON String.
|
17
17
|
def jws(header: {}, payload: {})
|
18
|
-
header = jws_header
|
18
|
+
header = jws_header(header)
|
19
19
|
encoded_header = Acme::Client::Util.urlsafe_base64(header.to_json)
|
20
20
|
encoded_payload = Acme::Client::Util.urlsafe_base64(payload.to_json)
|
21
21
|
|
@@ -56,12 +56,13 @@ class Acme::Client::JWK::Base
|
|
56
56
|
# typ: - Value for the `typ` field. Default 'JWT'.
|
57
57
|
#
|
58
58
|
# Returns a Hash.
|
59
|
-
def jws_header
|
60
|
-
{
|
59
|
+
def jws_header(header)
|
60
|
+
jws = {
|
61
61
|
typ: 'JWT',
|
62
|
-
alg: jwa_alg
|
63
|
-
|
64
|
-
|
62
|
+
alg: jwa_alg
|
63
|
+
}.merge(header)
|
64
|
+
jws[:jwk] = to_h if header[:kid].nil?
|
65
|
+
jws
|
65
66
|
end
|
66
67
|
|
67
68
|
# The name of the algorithm as needed for the `alg` member of a JWS object.
|
@@ -82,7 +82,6 @@ class Acme::Client::JWK::ECDSA < Acme::Client::JWK::Base
|
|
82
82
|
|
83
83
|
private
|
84
84
|
|
85
|
-
# rubocop:disable Metrics/AbcSize
|
86
85
|
def coordinates
|
87
86
|
@coordinates ||= begin
|
88
87
|
hex = public_key.to_bn.to_s(16)
|
@@ -96,7 +95,6 @@ class Acme::Client::JWK::ECDSA < Acme::Client::JWK::Base
|
|
96
95
|
}
|
97
96
|
end
|
98
97
|
end
|
99
|
-
# rubocop:enable Metrics/AbcSize
|
100
98
|
|
101
99
|
def public_key
|
102
100
|
@private_key.public_key
|
@@ -1,5 +1,7 @@
|
|
1
1
|
module Acme::Client::Resources; end
|
2
2
|
|
3
|
-
require 'acme/client/resources/
|
4
|
-
require 'acme/client/resources/
|
3
|
+
require 'acme/client/resources/directory'
|
4
|
+
require 'acme/client/resources/account'
|
5
|
+
require 'acme/client/resources/order'
|
5
6
|
require 'acme/client/resources/authorization'
|
7
|
+
require 'acme/client/resources/challenges'
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Acme::Client::Resources::Account
|
4
|
+
attr_reader :url, :status, :contact, :term_of_service, :orders_url
|
5
|
+
|
6
|
+
def initialize(client, **arguments)
|
7
|
+
@client = client
|
8
|
+
assign_attributes(arguments)
|
9
|
+
end
|
10
|
+
|
11
|
+
def kid
|
12
|
+
url
|
13
|
+
end
|
14
|
+
|
15
|
+
def update(contact: nil, terms_of_service_agreed: nil)
|
16
|
+
assign_attributes **@client.account_update(
|
17
|
+
contact: contact, terms_of_service_agreed: term_of_service
|
18
|
+
).to_h
|
19
|
+
true
|
20
|
+
end
|
21
|
+
|
22
|
+
def deactivate
|
23
|
+
assign_attributes **@client.account_deactivate.to_h
|
24
|
+
true
|
25
|
+
end
|
26
|
+
|
27
|
+
def reload
|
28
|
+
assign_attributes **@client.account.to_h
|
29
|
+
true
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_h
|
33
|
+
{
|
34
|
+
url: url,
|
35
|
+
term_of_service: term_of_service,
|
36
|
+
status: status,
|
37
|
+
contact: contact
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def assign_attributes(url:, term_of_service:, status:, contact:)
|
44
|
+
@url = url
|
45
|
+
@term_of_service = term_of_service
|
46
|
+
@status = status
|
47
|
+
@contact = Array(contact)
|
48
|
+
end
|
49
|
+
end
|
@@ -1,44 +1,74 @@
|
|
1
|
-
|
2
|
-
HTTP01 = Acme::Client::Resources::Challenges::HTTP01
|
3
|
-
DNS01 = Acme::Client::Resources::Challenges::DNS01
|
4
|
-
TLSSNI01 = Acme::Client::Resources::Challenges::TLSSNI01
|
1
|
+
# frozen_string_literal: true
|
5
2
|
|
6
|
-
|
3
|
+
class Acme::Client::Resources::Authorization
|
4
|
+
attr_reader :url, :identifier, :domain, :expires, :status, :wildcard
|
7
5
|
|
8
|
-
def initialize(client,
|
6
|
+
def initialize(client, **arguments)
|
9
7
|
@client = client
|
10
|
-
|
11
|
-
|
8
|
+
assign_attributes(arguments)
|
9
|
+
end
|
10
|
+
|
11
|
+
def deactivate
|
12
|
+
assign_attributes **@client.deactivate_authorization(url: url).to_h
|
13
|
+
true
|
12
14
|
end
|
13
15
|
|
14
|
-
def
|
15
|
-
|
16
|
+
def reload
|
17
|
+
assign_attributes **@client.authorization(url: url).to_h
|
18
|
+
true
|
19
|
+
end
|
20
|
+
|
21
|
+
def challenges
|
22
|
+
@challenges.map do |challenge|
|
23
|
+
initialize_challenge(challenge)
|
24
|
+
end
|
25
|
+
end
|
16
26
|
|
17
|
-
|
18
|
-
|
27
|
+
def http01
|
28
|
+
@http01 ||= challenges.find { |challenge|
|
29
|
+
challenge.is_a?(Acme::Client::Resources::Challenges::HTTP01)
|
30
|
+
}
|
31
|
+
end
|
32
|
+
alias_method :http, :http01
|
33
|
+
|
34
|
+
def dns01
|
35
|
+
@dns01 ||= challenges.find { |challenge|
|
36
|
+
challenge.is_a?(Acme::Client::Resources::Challenges::DNS01)
|
37
|
+
}
|
38
|
+
end
|
39
|
+
alias_method :dns, :dns01
|
40
|
+
|
41
|
+
def to_h
|
42
|
+
{
|
43
|
+
url: url,
|
44
|
+
identifier: identifier,
|
45
|
+
status: status,
|
46
|
+
expires: expires,
|
47
|
+
challenges: @challenges,
|
48
|
+
wildcard: wildcard
|
49
|
+
}
|
19
50
|
end
|
20
51
|
|
21
52
|
private
|
22
53
|
|
23
|
-
def
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
end
|
54
|
+
def initialize_challenge(attributes)
|
55
|
+
arguments = {
|
56
|
+
type: attributes.fetch('type'),
|
57
|
+
status: attributes.fetch('status'),
|
58
|
+
url: attributes.fetch('url'),
|
59
|
+
token: attributes.fetch('token'),
|
60
|
+
error: attributes['error']
|
61
|
+
}
|
62
|
+
Acme::Client::Resources::Challenges.new(@client, **arguments)
|
63
|
+
end
|
64
|
+
|
65
|
+
def assign_attributes(url:, status:, expires:, challenges:, identifier:, wildcard: false)
|
66
|
+
@url = url
|
67
|
+
@identifier = identifier
|
68
|
+
@domain = identifier.fetch('value')
|
69
|
+
@status = status
|
70
|
+
@expires = expires
|
71
|
+
@challenges = challenges
|
72
|
+
@wildcard = wildcard
|
43
73
|
end
|
44
74
|
end
|
@@ -1,6 +1,21 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
require 'acme/client/resources/challenges/
|
5
|
-
require 'acme/client/resources/challenges/
|
6
|
-
require 'acme/client/resources/challenges/
|
3
|
+
module Acme::Client::Resources::Challenges
|
4
|
+
require 'acme/client/resources/challenges/base'
|
5
|
+
require 'acme/client/resources/challenges/http01'
|
6
|
+
require 'acme/client/resources/challenges/dns01'
|
7
|
+
|
8
|
+
CHALLENGE_TYPES = {
|
9
|
+
'http-01' => Acme::Client::Resources::Challenges::HTTP01,
|
10
|
+
'dns-01' => Acme::Client::Resources::Challenges::DNS01
|
11
|
+
}
|
12
|
+
|
13
|
+
def self.new(client, type:, **arguments)
|
14
|
+
klass = CHALLENGE_TYPES[type]
|
15
|
+
if klass
|
16
|
+
klass.new(client, **arguments)
|
17
|
+
else
|
18
|
+
{ type: type }.merge(arguments)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -1,39 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class Acme::Client::Resources::Challenges::Base
|
2
|
-
attr_reader :
|
4
|
+
attr_reader :status, :url, :token, :error
|
3
5
|
|
4
|
-
def initialize(
|
5
|
-
@
|
6
|
+
def initialize(client, **arguments)
|
7
|
+
@client = client
|
8
|
+
assign_attributes(arguments)
|
6
9
|
end
|
7
10
|
|
8
|
-
def
|
9
|
-
|
11
|
+
def challenge_type
|
12
|
+
self.class::CHALLENGE_TYPE
|
10
13
|
end
|
11
14
|
|
12
|
-
def
|
13
|
-
|
15
|
+
def key_authorization
|
16
|
+
"#{token}.#{@client.jwk.thumbprint}"
|
17
|
+
end
|
14
18
|
|
15
|
-
|
19
|
+
def reload
|
20
|
+
assign_attributes **@client.challenge(url: url).to_h
|
21
|
+
true
|
16
22
|
end
|
17
23
|
|
18
|
-
def
|
19
|
-
|
20
|
-
|
24
|
+
def request_validation
|
25
|
+
assign_attributes **@client.request_challenge_validation(
|
26
|
+
url: url, key_authorization: key_authorization
|
27
|
+
).to_h
|
28
|
+
true
|
21
29
|
end
|
22
30
|
|
23
|
-
def
|
24
|
-
|
25
|
-
@uri = attributes.fetch('uri')
|
26
|
-
@token = attributes.fetch('token')
|
27
|
-
@error = attributes['error']
|
31
|
+
def to_h
|
32
|
+
{ status: status, url: url, token: token, error: error }
|
28
33
|
end
|
29
34
|
|
30
35
|
private
|
31
36
|
|
32
|
-
def
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
"#{token}.#{client.jwk.thumbprint}"
|
37
|
+
def assign_attributes(status:, url:, token:, error: nil)
|
38
|
+
@status = status
|
39
|
+
@url = url
|
40
|
+
@token = token
|
41
|
+
@error = error
|
38
42
|
end
|
39
43
|
end
|