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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f24f8baac91c1bff36891f14b0ec6d1fb3a9265cf771f2ae943b49dac5a96d2e
4
- data.tar.gz: 4f84da04feeea6ae55ab875ab835194bf71182e8cabf7d56e574d79d80e988ae
3
+ metadata.gz: d41b8df6f16f62b2bd33a0fd960d7675c02e1d065fb3bf3466b58f59aad3a740
4
+ data.tar.gz: 846346e6d27381ea63fa1cb82b9044bef0acc25e5ca38ebf332fb692c02b577e
5
5
  SHA512:
6
- metadata.gz: 1e4a73d22af2fec3e323859f5386308c046c32b443952f81c9ce1811562eae0ec92c0a3bd683c0806bd85ca605bda5468bdf7534b224b94a65a7fe1b1222cce4
7
- data.tar.gz: 84722131dd304475f45459ff78b4a0eda6ccf1a6d0aa2ffbdb2092cb17446ffc62db288ba429ee6a2080779dde273487718cddf27cc7d07b1850210401222b63
6
+ metadata.gz: 49cda221593c2902534457cd4b89e7c24fedc3ee33f52ae4af5e83df14e4baf0cd68e547ec95c54e4dffd2fa1aea2989e51f371f5abfb37cc0630e7e4176977f
7
+ data.tar.gz: '059a4b9b2f260ab7dc8cb3f069120c74b3f76ec4ce5d6ff22f7eb625b75c6c032bfca3709795e247f82873d022a2f1e09ef939687e7c0f74589564cae41c9c94'
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## `2.0.27`
2
+
3
+ * Add support for Renewal Information (ARI) (RFC 9773)
4
+
1
5
  ## `2.0.26`
2
6
 
3
7
  * Add support for dns-account-01 challenge (RFC draft-ietf-acme-dns-account-label-01)
@@ -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,
@@ -7,7 +7,8 @@ class Acme::Client::Resources::Directory
7
7
  new_order: 'newOrder',
8
8
  new_authz: 'newAuthz',
9
9
  revoke_certificate: 'revokeCert',
10
- key_change: 'keyChange'
10
+ key_change: 'keyChange',
11
+ renewal_info: 'renewalInfo'
11
12
  }
12
13
 
13
14
  DIRECTORY_META = {
@@ -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
@@ -5,3 +5,4 @@ require 'acme/client/resources/account'
5
5
  require 'acme/client/resources/order'
6
6
  require 'acme/client/resources/authorization'
7
7
  require 'acme/client/resources/challenges'
8
+ require 'acme/client/resources/renewal_info'
@@ -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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Acme
4
4
  class Client
5
- VERSION = '2.0.26'.freeze
5
+ VERSION = '2.0.27'.freeze
6
6
  end
7
7
  end
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.26
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-09-24 00:00:00.000000000 Z
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