acme-client 2.0.26 → 2.0.27
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 +4 -0
- data/lib/acme/client/error.rb +2 -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 +21 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d41b8df6f16f62b2bd33a0fd960d7675c02e1d065fb3bf3466b58f59aad3a740
|
|
4
|
+
data.tar.gz: 846346e6d27381ea63fa1cb82b9044bef0acc25e5ca38ebf332fb692c02b577e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 49cda221593c2902534457cd4b89e7c24fedc3ee33f52ae4af5e83df14e4baf0cd68e547ec95c54e4dffd2fa1aea2989e51f371f5abfb37cc0630e7e4176977f
|
|
7
|
+
data.tar.gz: '059a4b9b2f260ab7dc8cb3f069120c74b3f76ec4ce5d6ff22f7eb625b75c6c032bfca3709795e247f82873d022a2f1e09ef939687e7c0f74589564cae41c9c94'
|
data/CHANGELOG.md
CHANGED
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,
|
|
@@ -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
|
@@ -135,12 +135,13 @@ class Acme::Client
|
|
|
135
135
|
@kid ||= account.kid
|
|
136
136
|
end
|
|
137
137
|
|
|
138
|
-
def new_order(identifiers:, not_before: nil, not_after: nil, profile: nil)
|
|
138
|
+
def new_order(identifiers:, not_before: nil, not_after: nil, profile: nil, replaces: nil)
|
|
139
139
|
payload = {}
|
|
140
140
|
payload['identifiers'] = prepare_order_identifiers(identifiers)
|
|
141
141
|
payload['notBefore'] = not_before if not_before
|
|
142
142
|
payload['notAfter'] = not_after if not_after
|
|
143
143
|
payload['profile'] = profile if profile
|
|
144
|
+
payload['replaces'] = replaces if replaces
|
|
144
145
|
|
|
145
146
|
response = post(endpoint_for(:new_order), payload: payload)
|
|
146
147
|
arguments = attributes_from_order_response(response)
|
|
@@ -223,6 +224,15 @@ class Acme::Client
|
|
|
223
224
|
response.success?
|
|
224
225
|
end
|
|
225
226
|
|
|
227
|
+
def renewal_info(certificate:)
|
|
228
|
+
cert_id = Acme::Client::Util.ari_certificate_identifier(certificate)
|
|
229
|
+
renewal_info_url = URI.join(endpoint_for(:renewal_info).to_s + '/', cert_id).to_s
|
|
230
|
+
|
|
231
|
+
response = get(renewal_info_url)
|
|
232
|
+
attributes = attributes_from_renewal_info_response(response)
|
|
233
|
+
Acme::Client::Resources::RenewalInfo.new(self, **attributes)
|
|
234
|
+
end
|
|
235
|
+
|
|
226
236
|
def get_nonce
|
|
227
237
|
http_client = Acme::Client::HTTPClient.new_connection(url: endpoint_for(:new_nonce), options: @connection_options)
|
|
228
238
|
response = http_client.head(nil, nil)
|
|
@@ -320,6 +330,16 @@ class Acme::Client
|
|
|
320
330
|
extract_attributes(response.body, :status, :url, :token, :type, :error, :validated)
|
|
321
331
|
end
|
|
322
332
|
|
|
333
|
+
def attributes_from_renewal_info_response(response)
|
|
334
|
+
attributes = extract_attributes(
|
|
335
|
+
response.body,
|
|
336
|
+
[:suggested_window, 'suggestedWindow'],
|
|
337
|
+
[:explanation_url, 'explanationURL']
|
|
338
|
+
)
|
|
339
|
+
attributes[:retry_after] = response.headers['retry-after'] if response.headers['retry-after']
|
|
340
|
+
attributes
|
|
341
|
+
end
|
|
342
|
+
|
|
323
343
|
def extract_attributes(input, *attributes)
|
|
324
344
|
attributes
|
|
325
345
|
.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.27
|
|
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-12 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: rake
|
|
@@ -172,6 +172,7 @@ files:
|
|
|
172
172
|
- lib/acme/client/resources/challenges/unsupported_challenge.rb
|
|
173
173
|
- lib/acme/client/resources/directory.rb
|
|
174
174
|
- lib/acme/client/resources/order.rb
|
|
175
|
+
- lib/acme/client/resources/renewal_info.rb
|
|
175
176
|
- lib/acme/client/self_sign_certificate.rb
|
|
176
177
|
- lib/acme/client/util.rb
|
|
177
178
|
- lib/acme/client/version.rb
|