acme-client 2.0.22 → 2.0.26

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: a4c163dbc54a7e15b4e33aadbc892e9f9cca5fb4d2332873e9f0a4ca4b46d144
4
- data.tar.gz: 7fedb1bf7034c1a52a5f05169782e31b31749ecd6d8beb1a22da60ed44d49731
3
+ metadata.gz: f24f8baac91c1bff36891f14b0ec6d1fb3a9265cf771f2ae943b49dac5a96d2e
4
+ data.tar.gz: 4f84da04feeea6ae55ab875ab835194bf71182e8cabf7d56e574d79d80e988ae
5
5
  SHA512:
6
- metadata.gz: b1431a5b7890db3433aded0eaa9bccbdd14b60d9d95da840ee8d42e18c4f304e57b78431fa1313a77b06cb7e3e114dcba59471d48b8a1b3e099cc06eb4728df1
7
- data.tar.gz: c2e0b53200d61ae3d38ea75b656dc92de9751ad32a6a86a3346cd8ac843fccab097c70dd5e1d6870597510c33b6cdfde866de6e31f919d96fc23c91edc83384c
6
+ metadata.gz: 1e4a73d22af2fec3e323859f5386308c046c32b443952f81c9ce1811562eae0ec92c0a3bd683c0806bd85ca605bda5468bdf7534b224b94a65a7fe1b1222cce4
7
+ data.tar.gz: 84722131dd304475f45459ff78b4a0eda6ccf1a6d0aa2ffbdb2092cb17446ffc62db288ba429ee6a2080779dde273487718cddf27cc7d07b1850210401222b63
data/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## `2.0.26`
2
+
3
+ * Add support for dns-account-01 challenge (RFC draft-ietf-acme-dns-account-label-01)
4
+
5
+ ## `2.0.25`
6
+
7
+ * Add support for profiles extension
8
+
9
+ ## `2.0.24`
10
+
11
+ * Add support for account orders url attribute.
12
+
13
+ ## `2.0.23`
14
+
15
+ * Allow Order to be create without url. Location is not always required in the specification.
16
+
1
17
  ## `2.0.22`
2
18
 
3
19
  * Loosen base64 dependency constraint
data/README.md CHANGED
@@ -120,9 +120,11 @@ To order a new certificate, the client must provide a list of identifiers.
120
120
 
121
121
  The returned order will contain a list of `Authorization` that need to be completed in other to finalize the order, generally one per identifier.
122
122
 
123
- Each authorization contains multiple challenges, typically a `dns-01` and a `http-01` challenge. The applicant is only required to complete one of the challenges.
123
+ Each authorization contains multiple challenges, typically a `dns-01`, `dns-account-01`, and a `http-01` challenge. The applicant is only required to complete one of the challenges.
124
124
 
125
- You can access the challenge you wish to complete using the `#dns` or `#http` method.
125
+ The `dns-account-01` challenge prepends an account-specific label before `_acme-challenge`, producing a record name of the form `_<label>._acme-challenge` so different clients can validate the same domain concurrently.
126
+
127
+ You can access the challenge you wish to complete using the `#dns`, `#dns_account`, or `#http` methods.
126
128
 
127
129
  ```ruby
128
130
  order = client.new_order(identifiers: ['example.com'])
@@ -165,6 +167,25 @@ dns_challenge.record_type # => 'TXT'
165
167
  dns_challenge.record_content # => 'HRV3PS5sRDyV-ous4HJk4z24s5JjmUTjcCaUjFt28-8'
166
168
  ```
167
169
 
170
+ ### Preparing for DNS-Account-01 challenge
171
+
172
+ To complete the DNS-Account-01 challenge, you must set a DNS TXT record using an account-specific name. This allows multiple ACME clients to validate the same domain concurrently without conflicts.
173
+
174
+ The DNSAccount01 object has utility methods to generate the required DNS record:
175
+
176
+ ```ruby
177
+ dns_account_challenge = authorization.dns_account
178
+
179
+ dns_account_challenge.record_name # => '_ujmmovf2vn55tgye._acme-challenge'
180
+ dns_account_challenge.record_type # => 'TXT'
181
+ dns_account_challenge.record_content # => 'HRV3PS5sRDyV-ous4HJk4z24s5JjmUTjcCaUjFt28-8'
182
+ ```
183
+
184
+ The record name includes an account-specific label derived from your account URL, ensuring different clients can validate simultaneously:
185
+
186
+ - **DNS-01**: `_acme-challenge.example.com` (shared)
187
+ - **DNS-Account-01**: `_ujmmovf2vn55tgye._acme-challenge.example.com` (account-specific)
188
+
168
189
  ### Requesting a challenge verification
169
190
 
170
191
  Once you are ready to complete the challenge, you can request the server perform the verification.
@@ -244,6 +265,22 @@ new_private_key = OpenSSL::PKey::RSA.new(4096)
244
265
  client.account_key_change(new_private_key: new_private_key)
245
266
  ```
246
267
 
268
+ ### Profile Extension
269
+
270
+ Provide a CA profile when creating a new order:
271
+
272
+ ```ruby
273
+ order = client.new_order(identifiers: ['example.com'], profile: 'shortlived')
274
+ ```
275
+
276
+ ACME servers may list supported profiles in the directory endpoint:
277
+
278
+ ```ruby
279
+ client.profiles => {"classic": "https://example.com/docs/classic", "shortlived": "https://example.com/docs/shortlived"}
280
+ ```
281
+
282
+ See the [RFC draft of certificate profiles](https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/) for more info.
283
+
247
284
  ## Requirements
248
285
 
249
286
  Ruby >= 3.0
@@ -9,6 +9,7 @@ class Acme::Client::Error < StandardError
9
9
  class CertificateNotReady < ClientError; end
10
10
  class ForcedChainNotFound < ClientError; end
11
11
  class OrderNotReady < ClientError; end
12
+ class OrderNotReloadable < ClientError; end
12
13
 
13
14
  class ServerError < Acme::Client::Error; end
14
15
  class AlreadyRevoked < ServerError; end
@@ -34,16 +34,18 @@ class Acme::Client::Resources::Account
34
34
  url: url,
35
35
  term_of_service: term_of_service,
36
36
  status: status,
37
- contact: contact
37
+ contact: contact,
38
+ orders: orders_url
38
39
  }
39
40
  end
40
41
 
41
42
  private
42
43
 
43
- def assign_attributes(url:, term_of_service:, status:, contact:)
44
+ def assign_attributes(url:, term_of_service:, status:, contact:, orders: nil)
44
45
  @url = url
45
46
  @term_of_service = term_of_service
46
47
  @status = status
47
48
  @contact = Array(contact)
49
+ @orders_url = orders
48
50
  end
49
51
  end
@@ -38,6 +38,13 @@ class Acme::Client::Resources::Authorization
38
38
  end
39
39
  alias_method :dns, :dns01
40
40
 
41
+ def dns_account_01
42
+ @dns_account_01 ||= challenges.find { |challenge|
43
+ challenge.is_a?(Acme::Client::Resources::Challenges::DNSAccount01)
44
+ }
45
+ end
46
+ alias_method :dns_account, :dns_account_01
47
+
41
48
  def to_h
42
49
  {
43
50
  url: url,
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ # DNS-Account-01 challenge following draft-ietf-acme-dns-account-label-01
4
+ # Enables multiple ACME clients to validate the same domain concurrently
5
+ class Acme::Client::Resources::Challenges::DNSAccount01 < Acme::Client::Resources::Challenges::Base
6
+ CHALLENGE_TYPE = 'dns-account-01'.freeze
7
+ RECORD_PREFIX = '_'.freeze
8
+ RECORD_SUFFIX = '._acme-challenge'.freeze
9
+ RECORD_TYPE = 'TXT'.freeze
10
+ DIGEST = OpenSSL::Digest::SHA256
11
+ BASE32_ALPHABET = 'abcdefghijklmnopqrstuvwxyz234567'.freeze # RFC 4648 lowercase alphabet
12
+
13
+ # Generates account-specific DNS record name using SHA256(account_url) + Base32
14
+ # Format: _<base32_label>._acme-challenge
15
+ def record_name
16
+ digest = DIGEST.digest(@client.kid)[0, 10] # First 10 octets for label
17
+ bits = digest.unpack1('B*')
18
+ label = bits.scan(/.{5}/).map { |chunk| BASE32_ALPHABET[chunk.to_i(2)] }.join
19
+ "#{RECORD_PREFIX}#{label}#{RECORD_SUFFIX}"
20
+ end
21
+
22
+ def record_type
23
+ RECORD_TYPE
24
+ end
25
+
26
+ def record_content
27
+ Acme::Client::Util.urlsafe_base64(DIGEST.digest(key_authorization))
28
+ end
29
+ end
30
+
@@ -4,11 +4,13 @@ module Acme::Client::Resources::Challenges
4
4
  require 'acme/client/resources/challenges/base'
5
5
  require 'acme/client/resources/challenges/http01'
6
6
  require 'acme/client/resources/challenges/dns01'
7
+ require 'acme/client/resources/challenges/dns_account01'
7
8
  require 'acme/client/resources/challenges/unsupported_challenge'
8
9
 
9
10
  CHALLENGE_TYPES = {
10
11
  'http-01' => Acme::Client::Resources::Challenges::HTTP01,
11
- 'dns-01' => Acme::Client::Resources::Challenges::DNS01
12
+ 'dns-01' => Acme::Client::Resources::Challenges::DNS01,
13
+ 'dns-account-01' => Acme::Client::Resources::Challenges::DNSAccount01
12
14
  }
13
15
 
14
16
  def self.new(client, type:, **arguments)
@@ -14,7 +14,8 @@ class Acme::Client::Resources::Directory
14
14
  terms_of_service: 'termsOfService',
15
15
  website: 'website',
16
16
  caa_identities: 'caaIdentities',
17
- external_account_required: 'externalAccountRequired'
17
+ external_account_required: 'externalAccountRequired',
18
+ profiles: 'profiles'
18
19
  }
19
20
 
20
21
  def initialize(client, **arguments)
@@ -45,6 +46,10 @@ class Acme::Client::Resources::Directory
45
46
  meta[DIRECTORY_META[:external_account_required]]
46
47
  end
47
48
 
49
+ def profiles
50
+ meta[DIRECTORY_META[:profiles]]
51
+ end
52
+
48
53
  def meta
49
54
  @directory[:meta]
50
55
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Acme::Client::Resources::Order
4
- attr_reader :url, :status, :contact, :finalize_url, :identifiers, :authorization_urls, :expires, :certificate_url
4
+ attr_reader :url, :status, :contact, :finalize_url, :identifiers, :authorization_urls, :expires, :certificate_url, :profile
5
5
 
6
6
  def initialize(client, **arguments)
7
7
  @client = client
@@ -9,6 +9,10 @@ class Acme::Client::Resources::Order
9
9
  end
10
10
 
11
11
  def reload
12
+ if url.nil?
13
+ raise Acme::Client::Error::OrderNotReloadable, 'Finalized orders are not reloadable for this CA'
14
+ end
15
+
12
16
  assign_attributes(**@client.order(url: url).to_h)
13
17
  true
14
18
  end
@@ -40,13 +44,14 @@ class Acme::Client::Resources::Order
40
44
  finalize_url: finalize_url,
41
45
  authorization_urls: authorization_urls,
42
46
  identifiers: identifiers,
43
- certificate_url: certificate_url
47
+ certificate_url: certificate_url,
48
+ profile: profile
44
49
  }
45
50
  end
46
51
 
47
52
  private
48
53
 
49
- def assign_attributes(url:, status:, expires:, finalize_url:, authorization_urls:, identifiers:, certificate_url: nil)
54
+ def assign_attributes(url: nil, status:, expires:, finalize_url:, authorization_urls:, identifiers:, certificate_url: nil, profile: nil) # rubocop:disable Layout/LineLength,Metrics/ParameterLists
50
55
  @url = url
51
56
  @status = status
52
57
  @expires = expires
@@ -54,5 +59,6 @@ class Acme::Client::Resources::Order
54
59
  @authorization_urls = authorization_urls
55
60
  @identifiers = identifiers
56
61
  @certificate_url = certificate_url
62
+ @profile = profile
57
63
  end
58
64
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Acme
4
4
  class Client
5
- VERSION = '2.0.22'.freeze
5
+ VERSION = '2.0.26'.freeze
6
6
  end
7
7
  end
data/lib/acme/client.rb CHANGED
@@ -135,11 +135,12 @@ class Acme::Client
135
135
  @kid ||= account.kid
136
136
  end
137
137
 
138
- def new_order(identifiers:, not_before: nil, not_after: nil)
138
+ def new_order(identifiers:, not_before: nil, not_after: nil, profile: 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
+ payload['profile'] = profile if profile
143
144
 
144
145
  response = post(endpoint_for(:new_order), payload: payload)
145
146
  arguments = attributes_from_order_response(response)
@@ -253,6 +254,10 @@ class Acme::Client
253
254
  directory.external_account_required
254
255
  end
255
256
 
257
+ def profiles
258
+ directory.profiles
259
+ end
260
+
256
261
  private
257
262
 
258
263
  def load_directory
@@ -286,6 +291,7 @@ class Acme::Client
286
291
  response.body,
287
292
  :status,
288
293
  [:term_of_service, 'termsOfServiceAgreed'],
294
+ :orders,
289
295
  :contact
290
296
  )
291
297
  end
@@ -298,7 +304,8 @@ class Acme::Client
298
304
  [:finalize_url, 'finalize'],
299
305
  [:authorization_urls, 'authorizations'],
300
306
  [:certificate_url, 'certificate'],
301
- :identifiers
307
+ :identifiers,
308
+ :profile
302
309
  )
303
310
 
304
311
  attributes[:url] = response.headers[:location] if response.headers[:location]
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.22
4
+ version: 2.0.26
5
5
  platform: ruby
6
6
  authors:
7
7
  - Charles Barbier
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-07-01 00:00:00.000000000 Z
10
+ date: 2025-09-24 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rake
@@ -167,6 +167,7 @@ files:
167
167
  - lib/acme/client/resources/challenges.rb
168
168
  - lib/acme/client/resources/challenges/base.rb
169
169
  - lib/acme/client/resources/challenges/dns01.rb
170
+ - lib/acme/client/resources/challenges/dns_account01.rb
170
171
  - lib/acme/client/resources/challenges/http01.rb
171
172
  - lib/acme/client/resources/challenges/unsupported_challenge.rb
172
173
  - lib/acme/client/resources/directory.rb