acme-client 2.0.24 → 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 +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +39 -2
- data/lib/acme/client/resources/authorization.rb +7 -0
- data/lib/acme/client/resources/challenges/dns_account01.rb +30 -0
- data/lib/acme/client/resources/challenges.rb +3 -1
- data/lib/acme/client/resources/directory.rb +6 -1
- data/lib/acme/client/resources/order.rb +5 -3
- data/lib/acme/client/version.rb +1 -1
- data/lib/acme/client.rb +8 -2
- 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: f24f8baac91c1bff36891f14b0ec6d1fb3a9265cf771f2ae943b49dac5a96d2e
|
4
|
+
data.tar.gz: 4f84da04feeea6ae55ab875ab835194bf71182e8cabf7d56e574d79d80e988ae
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1e4a73d22af2fec3e323859f5386308c046c32b443952f81c9ce1811562eae0ec92c0a3bd683c0806bd85ca605bda5468bdf7534b224b94a65a7fe1b1222cce4
|
7
|
+
data.tar.gz: 84722131dd304475f45459ff78b4a0eda6ccf1a6d0aa2ffbdb2092cb17446ffc62db288ba429ee6a2080779dde273487718cddf27cc7d07b1850210401222b63
|
data/CHANGELOG.md
CHANGED
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
|
-
|
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
|
@@ -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
|
@@ -44,13 +44,14 @@ class Acme::Client::Resources::Order
|
|
44
44
|
finalize_url: finalize_url,
|
45
45
|
authorization_urls: authorization_urls,
|
46
46
|
identifiers: identifiers,
|
47
|
-
certificate_url: certificate_url
|
47
|
+
certificate_url: certificate_url,
|
48
|
+
profile: profile
|
48
49
|
}
|
49
50
|
end
|
50
51
|
|
51
52
|
private
|
52
53
|
|
53
|
-
def assign_attributes(url: nil, 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
|
54
55
|
@url = url
|
55
56
|
@status = status
|
56
57
|
@expires = expires
|
@@ -58,5 +59,6 @@ class Acme::Client::Resources::Order
|
|
58
59
|
@authorization_urls = authorization_urls
|
59
60
|
@identifiers = identifiers
|
60
61
|
@certificate_url = certificate_url
|
62
|
+
@profile = profile
|
61
63
|
end
|
62
64
|
end
|
data/lib/acme/client/version.rb
CHANGED
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
|
@@ -299,7 +304,8 @@ class Acme::Client
|
|
299
304
|
[:finalize_url, 'finalize'],
|
300
305
|
[:authorization_urls, 'authorizations'],
|
301
306
|
[:certificate_url, 'certificate'],
|
302
|
-
:identifiers
|
307
|
+
:identifiers,
|
308
|
+
:profile
|
303
309
|
)
|
304
310
|
|
305
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.
|
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-
|
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
|