acme-client 1.0.0 → 2.0.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 +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
|