acme-client 2.0.26 → 2.0.28
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/CHANGELOG.md +8 -0
- data/lib/acme/client/error/rate_limited.rb +8 -0
- data/lib/acme/client/error.rb +2 -0
- data/lib/acme/client/http_client.rb +3 -0
- data/lib/acme/client/resources/directory.rb +2 -1
- data/lib/acme/client/resources/renewal_info.rb +47 -0
- data/lib/acme/client/resources.rb +1 -0
- data/lib/acme/client/util.rb +37 -0
- data/lib/acme/client/version.rb +1 -1
- data/lib/acme/client.rb +22 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c77ea7f7e3923d5c76ddd26c802ade553e7875f0f95b753a240f06c1e77e3689
|
|
4
|
+
data.tar.gz: 4f7a6f073795c9328b7322bd39f5640fb53e89c2b3790417ea2be2f2f13c5e7e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e12eae043f2e3b8fd505b88980f45336aee50013c23297ef8dd6f3f58cb427c62be9d0d1098dee4242fe258bc12b3bf417b22e3be772c3bcf8d6eb9b15748b75
|
|
7
|
+
data.tar.gz: 24fb6340f56d6eb998d57706dbfb92f92a64f488157410d0997e38bda9c009e541c4a2e5686d936d1be5a5c40aee39bcb3d8953edea86ddea4d0915c87344cfa
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## `2.0.28`
|
|
2
|
+
|
|
3
|
+
* Make [Retry-After](https://datatracker.ietf.org/doc/html/rfc8555/#section-6.6) accessible from RateLimited#retry_after exceptions
|
|
4
|
+
|
|
5
|
+
## `2.0.27`
|
|
6
|
+
|
|
7
|
+
* Add support for Renewal Information (ARI) (RFC 9773)
|
|
8
|
+
|
|
1
9
|
## `2.0.26`
|
|
2
10
|
|
|
3
11
|
* Add support for dns-account-01 challenge (RFC draft-ietf-acme-dns-account-label-01)
|
data/lib/acme/client/error.rb
CHANGED
|
@@ -12,6 +12,7 @@ class Acme::Client::Error < StandardError
|
|
|
12
12
|
class OrderNotReloadable < ClientError; end
|
|
13
13
|
|
|
14
14
|
class ServerError < Acme::Client::Error; end
|
|
15
|
+
class AlreadyReplaced < ServerError; end
|
|
15
16
|
class AlreadyRevoked < ServerError; end
|
|
16
17
|
class BadCSR < ServerError; end
|
|
17
18
|
class BadNonce < ServerError; end
|
|
@@ -36,6 +37,7 @@ class Acme::Client::Error < StandardError
|
|
|
36
37
|
class IncorrectResponse < ServerError; end
|
|
37
38
|
|
|
38
39
|
ACME_ERRORS = {
|
|
40
|
+
'urn:ietf:params:acme:error:alreadyReplaced' => AlreadyReplaced,
|
|
39
41
|
'urn:ietf:params:acme:error:alreadyRevoked' => AlreadyRevoked,
|
|
40
42
|
'urn:ietf:params:acme:error:badCSR' => BadCSR,
|
|
41
43
|
'urn:ietf:params:acme:error:badNonce' => BadNonce,
|
|
@@ -101,6 +101,9 @@ module Acme::Client::HTTPClient
|
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
def raise_on_error!
|
|
104
|
+
if error_class == Acme::Client::Error::RateLimited
|
|
105
|
+
raise error_class.new(error_message, env.response_headers['Retry-After'])
|
|
106
|
+
end
|
|
104
107
|
raise error_class, error_message
|
|
105
108
|
end
|
|
106
109
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Acme::Client::Resources::RenewalInfo
|
|
4
|
+
attr_reader :suggested_window, :explanation_url, :retry_after
|
|
5
|
+
|
|
6
|
+
def initialize(client, **arguments)
|
|
7
|
+
@client = client
|
|
8
|
+
assign_attributes(**arguments)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def suggested_window_start
|
|
12
|
+
suggested_window&.fetch('start', nil)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def suggested_window_end
|
|
16
|
+
suggested_window&.fetch('end', nil)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def suggested_renewal_time
|
|
20
|
+
return nil unless suggested_window_start && suggested_window_end
|
|
21
|
+
|
|
22
|
+
start_time = DateTime.rfc3339(suggested_window_start).to_time
|
|
23
|
+
end_time = DateTime.rfc3339(suggested_window_end).to_time
|
|
24
|
+
window_duration = end_time - start_time
|
|
25
|
+
|
|
26
|
+
random_offset = rand(0.0..window_duration)
|
|
27
|
+
selected_time = start_time + random_offset
|
|
28
|
+
|
|
29
|
+
selected_time > Time.now ? selected_time : Time.now
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_h
|
|
33
|
+
{
|
|
34
|
+
suggested_window: suggested_window,
|
|
35
|
+
explanation_url: explanation_url,
|
|
36
|
+
retry_after: retry_after
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def assign_attributes(suggested_window:, explanation_url: nil, retry_after: nil)
|
|
43
|
+
@suggested_window = suggested_window
|
|
44
|
+
@explanation_url = explanation_url
|
|
45
|
+
@retry_after = retry_after
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/acme/client/util.rb
CHANGED
|
@@ -32,4 +32,41 @@ module Acme::Client::Util
|
|
|
32
32
|
raise ArgumentError, 'priv must be EC or RSA'
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
|
+
|
|
36
|
+
# Generates a certificate identifier for ACME Renewal Information (ARI) as per RFC 9773.
|
|
37
|
+
# The identifier is constructed by extracting the Authority Key Identifier (AKI) from
|
|
38
|
+
# the certificate extension, and the DER-encoded serial number (without tag and length bytes).
|
|
39
|
+
# Both values are base64url-encoded and concatenated with a period separator.
|
|
40
|
+
#
|
|
41
|
+
# certificate - An OpenSSL::X509::Certificate instance or PEM string.
|
|
42
|
+
#
|
|
43
|
+
# Returns a string in the format: base64url(AKI).base64url(serial)
|
|
44
|
+
def ari_certificate_identifier(certificate)
|
|
45
|
+
cert = if certificate.is_a?(OpenSSL::X509::Certificate)
|
|
46
|
+
certificate
|
|
47
|
+
else
|
|
48
|
+
OpenSSL::X509::Certificate.new(certificate)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
aki_ext = cert.extensions.find { |ext| ext.oid == 'authorityKeyIdentifier' }
|
|
52
|
+
raise ArgumentError, 'Certificate does not have an Authority Key Identifier extension' unless aki_ext
|
|
53
|
+
|
|
54
|
+
aki_value = aki_ext.value
|
|
55
|
+
hex_string = if aki_value =~ /keyid:([0-9A-Fa-f:]+)/
|
|
56
|
+
$1
|
|
57
|
+
elsif aki_value =~ /^[0-9A-Fa-f:]+$/
|
|
58
|
+
aki_value
|
|
59
|
+
else
|
|
60
|
+
raise ArgumentError, 'Could not parse Authority Key Identifier'
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
key_identifier = hex_string.split(':').map { |hex| hex.to_i(16).chr }.join
|
|
64
|
+
serial_der = OpenSSL::ASN1::Integer.new(cert.serial).to_der
|
|
65
|
+
serial_value = OpenSSL::ASN1.decode(serial_der).value.to_s(2)
|
|
66
|
+
|
|
67
|
+
aki_b64 = urlsafe_base64(key_identifier)
|
|
68
|
+
serial_b64 = urlsafe_base64(serial_value)
|
|
69
|
+
|
|
70
|
+
"#{aki_b64}.#{serial_b64}"
|
|
71
|
+
end
|
|
35
72
|
end
|
data/lib/acme/client/version.rb
CHANGED
data/lib/acme/client.rb
CHANGED
|
@@ -20,6 +20,7 @@ require 'acme/client/self_sign_certificate'
|
|
|
20
20
|
require 'acme/client/resources'
|
|
21
21
|
require 'acme/client/jwk'
|
|
22
22
|
require 'acme/client/error'
|
|
23
|
+
require 'acme/client/error/rate_limited'
|
|
23
24
|
require 'acme/client/util'
|
|
24
25
|
require 'acme/client/chain_identifier'
|
|
25
26
|
|
|
@@ -135,12 +136,13 @@ class Acme::Client
|
|
|
135
136
|
@kid ||= account.kid
|
|
136
137
|
end
|
|
137
138
|
|
|
138
|
-
def new_order(identifiers:, not_before: nil, not_after: nil, profile: nil)
|
|
139
|
+
def new_order(identifiers:, not_before: nil, not_after: nil, profile: nil, replaces: nil)
|
|
139
140
|
payload = {}
|
|
140
141
|
payload['identifiers'] = prepare_order_identifiers(identifiers)
|
|
141
142
|
payload['notBefore'] = not_before if not_before
|
|
142
143
|
payload['notAfter'] = not_after if not_after
|
|
143
144
|
payload['profile'] = profile if profile
|
|
145
|
+
payload['replaces'] = replaces if replaces
|
|
144
146
|
|
|
145
147
|
response = post(endpoint_for(:new_order), payload: payload)
|
|
146
148
|
arguments = attributes_from_order_response(response)
|
|
@@ -223,6 +225,15 @@ class Acme::Client
|
|
|
223
225
|
response.success?
|
|
224
226
|
end
|
|
225
227
|
|
|
228
|
+
def renewal_info(certificate:)
|
|
229
|
+
cert_id = Acme::Client::Util.ari_certificate_identifier(certificate)
|
|
230
|
+
renewal_info_url = URI.join(endpoint_for(:renewal_info).to_s + '/', cert_id).to_s
|
|
231
|
+
|
|
232
|
+
response = get(renewal_info_url)
|
|
233
|
+
attributes = attributes_from_renewal_info_response(response)
|
|
234
|
+
Acme::Client::Resources::RenewalInfo.new(self, **attributes)
|
|
235
|
+
end
|
|
236
|
+
|
|
226
237
|
def get_nonce
|
|
227
238
|
http_client = Acme::Client::HTTPClient.new_connection(url: endpoint_for(:new_nonce), options: @connection_options)
|
|
228
239
|
response = http_client.head(nil, nil)
|
|
@@ -320,6 +331,16 @@ class Acme::Client
|
|
|
320
331
|
extract_attributes(response.body, :status, :url, :token, :type, :error, :validated)
|
|
321
332
|
end
|
|
322
333
|
|
|
334
|
+
def attributes_from_renewal_info_response(response)
|
|
335
|
+
attributes = extract_attributes(
|
|
336
|
+
response.body,
|
|
337
|
+
[:suggested_window, 'suggestedWindow'],
|
|
338
|
+
[:explanation_url, 'explanationURL']
|
|
339
|
+
)
|
|
340
|
+
attributes[:retry_after] = response.headers['retry-after'] if response.headers['retry-after']
|
|
341
|
+
attributes
|
|
342
|
+
end
|
|
343
|
+
|
|
323
344
|
def extract_attributes(input, *attributes)
|
|
324
345
|
attributes
|
|
325
346
|
.map {|fields| Array(fields) }
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: acme-client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0.
|
|
4
|
+
version: 2.0.28
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Charles Barbier
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2025-
|
|
10
|
+
date: 2025-11-28 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: rake
|
|
@@ -155,6 +155,7 @@ files:
|
|
|
155
155
|
- lib/acme/client/certificate_request/ec_key_patch.rb
|
|
156
156
|
- lib/acme/client/chain_identifier.rb
|
|
157
157
|
- lib/acme/client/error.rb
|
|
158
|
+
- lib/acme/client/error/rate_limited.rb
|
|
158
159
|
- lib/acme/client/http_client.rb
|
|
159
160
|
- lib/acme/client/jwk.rb
|
|
160
161
|
- lib/acme/client/jwk/base.rb
|
|
@@ -172,6 +173,7 @@ files:
|
|
|
172
173
|
- lib/acme/client/resources/challenges/unsupported_challenge.rb
|
|
173
174
|
- lib/acme/client/resources/directory.rb
|
|
174
175
|
- lib/acme/client/resources/order.rb
|
|
176
|
+
- lib/acme/client/resources/renewal_info.rb
|
|
175
177
|
- lib/acme/client/self_sign_certificate.rb
|
|
176
178
|
- lib/acme/client/util.rb
|
|
177
179
|
- lib/acme/client/version.rb
|