acme-client 2.0.3 → 2.0.8
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 +5 -5
- data/.gitignore +1 -0
- data/.rubocop.yml +51 -56
- data/.travis.yml +7 -5
- data/CHANGELOG.md +25 -0
- data/Gemfile +1 -0
- data/README.md +52 -18
- data/acme-client.gemspec +10 -6
- data/lib/acme/client.rb +76 -18
- data/lib/acme/client/chain_identifier.rb +27 -0
- data/lib/acme/client/error.rb +1 -0
- data/lib/acme/client/faraday_middleware.rb +1 -9
- data/lib/acme/client/jwk/base.rb +2 -2
- data/lib/acme/client/jwk/ecdsa.rb +3 -1
- data/lib/acme/client/resources/account.rb +1 -1
- data/lib/acme/client/resources/authorization.rb +1 -1
- data/lib/acme/client/resources/challenges.rb +2 -6
- data/lib/acme/client/resources/challenges/base.rb +9 -11
- data/lib/acme/client/resources/challenges/unsupported_challenge.rb +2 -0
- data/lib/acme/client/resources/order.rb +3 -3
- data/lib/acme/client/util.rb +11 -0
- data/lib/acme/client/version.rb +1 -1
- metadata +21 -44
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9cb714bd4f0321181c50087f976122251233340d09f4ed612cf91070e0986d70
|
4
|
+
data.tar.gz: db7df58ad9a1e4794bef25dfbc309efad73b9b1cbb6100f77d9552cbb734fb5d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8d7eb7d76ea95ab724810af93ad4e79af03fe62752509a793b5310b01b670b7c57f737bbb3a68de72ab4e84f01f9d613e45937a50119ddfe3b0f5d0aa628f45e
|
7
|
+
data.tar.gz: 38b349fdcdb68f5ec493951ec4a480966256e591f6d9cffc4266116bff6d2aed6e8f0e5493acf985f6a81c0cad7c20a69e4d7bd1cedb95357b297cdcfb1b494c
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
@@ -7,133 +7,128 @@ AllCops:
|
|
7
7
|
Rails:
|
8
8
|
Enabled: false
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
- 'lib/acme-client.rb'
|
10
|
+
Layout/AlignParameters:
|
11
|
+
EnforcedStyle: with_fixed_indentation
|
13
12
|
|
14
|
-
|
13
|
+
Layout/ElseAlignment:
|
15
14
|
Enabled: false
|
16
15
|
|
17
|
-
|
18
|
-
|
16
|
+
Layout/FirstParameterIndentation:
|
17
|
+
EnforcedStyle: consistent
|
19
18
|
|
20
|
-
|
19
|
+
Layout/IndentationWidth:
|
21
20
|
Enabled: false
|
22
21
|
|
23
22
|
Layout/MultilineOperationIndentation:
|
24
23
|
Enabled: false
|
25
24
|
|
26
|
-
|
27
|
-
EnforcedStyle: only_raise
|
28
|
-
|
29
|
-
Layout/AlignParameters:
|
30
|
-
EnforcedStyle: with_fixed_indentation
|
31
|
-
|
32
|
-
Layout/ElseAlignment:
|
25
|
+
Layout/SpaceInsideBlockBraces:
|
33
26
|
Enabled: false
|
34
27
|
|
35
|
-
|
28
|
+
Lint/AmbiguousOperator:
|
36
29
|
Enabled: false
|
37
30
|
|
38
|
-
|
31
|
+
Lint/AssignmentInCondition:
|
39
32
|
Enabled: false
|
40
33
|
|
41
|
-
|
34
|
+
Lint/EndAlignment:
|
42
35
|
Enabled: false
|
43
36
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
Style/TrailingCommaInArguments:
|
48
|
-
Enabled: false
|
37
|
+
Lint/UnusedMethodArgument:
|
38
|
+
AllowUnusedKeywordArguments: true
|
49
39
|
|
50
|
-
|
40
|
+
Metrics/AbcSize:
|
51
41
|
Enabled: false
|
52
42
|
|
53
43
|
Metrics/BlockLength:
|
54
44
|
Enabled: false
|
55
45
|
|
56
|
-
|
46
|
+
Metrics/ClassLength:
|
57
47
|
Enabled: false
|
58
48
|
|
59
|
-
|
60
|
-
Enabled:
|
49
|
+
Metrics/CyclomaticComplexity:
|
50
|
+
Enabled: false
|
61
51
|
|
62
52
|
Metrics/LineLength:
|
63
53
|
Max: 140
|
64
54
|
|
55
|
+
Metrics/MethodLength:
|
56
|
+
Max: 15
|
57
|
+
Enabled: false
|
58
|
+
|
65
59
|
Metrics/ParameterLists:
|
66
60
|
Max: 5
|
67
61
|
CountKeywordArgs: false
|
68
62
|
|
69
|
-
|
63
|
+
Metrics/PerceivedComplexity:
|
70
64
|
Enabled: false
|
71
65
|
|
72
|
-
|
66
|
+
Security/JSONLoad:
|
73
67
|
Enabled: false
|
74
68
|
|
75
|
-
Style/
|
69
|
+
Style/AccessorMethodName:
|
76
70
|
Enabled: false
|
77
71
|
|
78
|
-
Style/
|
79
|
-
|
80
|
-
|
81
|
-
Lint/UnusedMethodArgument:
|
82
|
-
AllowUnusedKeywordArguments: true
|
72
|
+
Style/Alias:
|
73
|
+
Enabled: false
|
83
74
|
|
84
|
-
|
85
|
-
|
75
|
+
Style/BlockDelimiters:
|
76
|
+
EnforcedStyle: semantic
|
86
77
|
|
87
|
-
Style/
|
78
|
+
Style/ClassAndModuleChildren:
|
88
79
|
Enabled: false
|
89
80
|
|
90
|
-
Style/
|
81
|
+
Style/Documentation:
|
91
82
|
Enabled: false
|
92
83
|
|
93
|
-
Style/
|
84
|
+
Style/DoubleNegation:
|
94
85
|
Enabled: false
|
95
86
|
|
96
|
-
Style/
|
97
|
-
|
87
|
+
Style/FileName:
|
88
|
+
Exclude:
|
89
|
+
- 'lib/acme-client.rb'
|
98
90
|
|
99
|
-
Style/
|
91
|
+
Style/GlobalVars:
|
100
92
|
Enabled: false
|
101
93
|
|
102
94
|
Style/GuardClause:
|
103
95
|
Enabled: false
|
104
96
|
|
105
|
-
Style/
|
97
|
+
Style/IfUnlessModifier:
|
106
98
|
Enabled: false
|
107
99
|
|
108
|
-
|
100
|
+
Style/Lambda:
|
109
101
|
Enabled: false
|
110
102
|
|
111
|
-
|
103
|
+
Style/ModuleFunction:
|
112
104
|
Enabled: false
|
113
105
|
|
114
|
-
|
106
|
+
Style/MultilineBlockChain:
|
115
107
|
Enabled: false
|
116
108
|
|
117
|
-
|
109
|
+
Style/MultipleComparison:
|
118
110
|
Enabled: false
|
119
111
|
|
120
|
-
|
112
|
+
Style/MutableConstant:
|
121
113
|
Enabled: false
|
122
114
|
|
123
|
-
|
115
|
+
Style/ParallelAssignment:
|
124
116
|
Enabled: false
|
125
117
|
|
126
|
-
Style/
|
118
|
+
Style/PercentLiteralDelimiters:
|
127
119
|
Enabled: false
|
128
120
|
|
129
|
-
Style/
|
130
|
-
|
121
|
+
Style/SignalException:
|
122
|
+
EnforcedStyle: only_raise
|
131
123
|
|
132
|
-
Style/
|
124
|
+
Style/SymbolArray:
|
133
125
|
Enabled: false
|
134
126
|
|
135
|
-
|
136
|
-
Enabled:
|
127
|
+
Style/StringLiterals:
|
128
|
+
Enabled: single_quotes
|
137
129
|
|
138
|
-
Style/
|
130
|
+
Style/TrailingCommaInArguments:
|
139
131
|
Enabled: false
|
132
|
+
|
133
|
+
Style/TrivialAccessors:
|
134
|
+
AllowPredicates: true
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,28 @@
|
|
1
|
+
## `2.0.8`
|
2
|
+
|
3
|
+
* Add support for the keyChange endpoint
|
4
|
+
|
5
|
+
https://tools.ietf.org/html/rfc8555#section-7.3.5
|
6
|
+
|
7
|
+
|
8
|
+
## `2.0.7`
|
9
|
+
|
10
|
+
* Add support for alternate certificate chain
|
11
|
+
* Change `Link` headers parsing to return array of value. This add support multiple entries at the same `rel`
|
12
|
+
|
13
|
+
## `2.0.6`
|
14
|
+
|
15
|
+
* Allow Faraday up to `< 2.0`
|
16
|
+
|
17
|
+
## `2.0.5`
|
18
|
+
|
19
|
+
* Use post-as-get
|
20
|
+
* Remove deprecated keyAuthorization
|
21
|
+
|
22
|
+
## `2.0.4`
|
23
|
+
|
24
|
+
* Add an option to retry bad nonce errors
|
25
|
+
|
1
26
|
## `2.0.3`
|
2
27
|
|
3
28
|
* Do not try to set the body on GET request
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
[](https://travis-ci.org/unixcharles/acme-client)
|
4
4
|
|
5
|
-
`acme-client` is a client implementation of the [
|
5
|
+
`acme-client` is a client implementation of the ACMEv2 / [RFC 8555](https://tools.ietf.org/html/rfc8555) protocol in Ruby.
|
6
6
|
|
7
7
|
You can find the ACME reference implementations of the [server](https://github.com/letsencrypt/boulder) in Go and the [client](https://github.com/certbot/certbot) in Python.
|
8
8
|
|
@@ -23,17 +23,26 @@ gem 'acme-client'
|
|
23
23
|
```
|
24
24
|
|
25
25
|
## Usage
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
26
|
+
- [Acme::Client](#acmeclient)
|
27
|
+
- [Installation](#installation)
|
28
|
+
- [Usage](#usage)
|
29
|
+
- [Setting up a client](#setting-up-a-client)
|
30
|
+
- [Account management](#account-management)
|
31
|
+
- [Obtaining a certificate](#obtaining-a-certificate)
|
32
|
+
- [Ordering a certificate](#ordering-a-certificate)
|
33
|
+
- [Preparing for HTTP challenge](#preparing-for-http-challenge)
|
34
|
+
- [Preparing for DNS challenge](#preparing-for-dns-challenge)
|
35
|
+
- [Requesting a challenge verification](#requesting-a-challenge-verification)
|
36
|
+
- [Downloading a certificate](#downloading-a-certificate)
|
37
|
+
- [Ordering an alternative certificate](#ordering-an-alternative-certificate)
|
38
|
+
- [Extra](#extra)
|
39
|
+
- [Certificate revokation](#certificate-revokation)
|
40
|
+
- [Certificate renewal](#certificate-renewal)
|
41
|
+
- [Not implemented](#not-implemented)
|
42
|
+
- [Requirements](#requirements)
|
43
|
+
- [Development](#development)
|
44
|
+
- [Pull request?](#pull-request)
|
45
|
+
- [License](#license)
|
37
46
|
|
38
47
|
## Setting up a client
|
39
48
|
|
@@ -91,7 +100,7 @@ account.kid # => <kid string>
|
|
91
100
|
|
92
101
|
If you already have an existing account (for example one created in ACME v1) please note that unless the `kid` is provided at initialization, the client will lazy load the `kid` by doing a `POST` to `newAccount` whenever the `kid` is required. Therefore, you can easily get your `kid` for an existing account and (if needed) store it for reuse:
|
93
102
|
|
94
|
-
```
|
103
|
+
```ruby
|
95
104
|
client = Acme::Client.new(private_key: private_key, directory: 'https://acme-staging-v02.api.letsencrypt.org/directory')
|
96
105
|
|
97
106
|
# kid is not set, therefore a call to newAccount is made to lazy-initialize the kid
|
@@ -182,10 +191,30 @@ Certificate generation happens asynchronously. You may need to poll.
|
|
182
191
|
```ruby
|
183
192
|
csr = Acme::Client::CertificateRequest.new(private_key: a_different_private_key, subject: { common_name: 'example.com' })
|
184
193
|
order.finalize(csr: csr)
|
185
|
-
|
194
|
+
while order.status == 'processing'
|
195
|
+
sleep(1)
|
196
|
+
order.reload
|
197
|
+
end
|
186
198
|
order.certificate # => PEM-formatted certificate
|
187
199
|
```
|
188
200
|
|
201
|
+
### Ordering an alternative certificate
|
202
|
+
|
203
|
+
Let's Encrypt is [transitioning](https://letsencrypt.org/2019/04/15/transitioning-to-isrg-root.html) to use a new intermediate certificate. Starting January 11, 2021 new certificates will be signed by their own intermediate. To ease the transition on clients Let's Encrypt will continue signing an alternative version of the certificate using the old, cross-signed intermediate until September 29, 2021. In order to utilize an alternative certificate the `Order#certificate` method accepts a `force_chain` keyword argument, which takes the issuer name of the intermediate certificate.
|
204
|
+
For example, to download the cross-signed certificate after January 11, 2021, call `Order#certificate` as follows:
|
205
|
+
|
206
|
+
```ruby
|
207
|
+
begin
|
208
|
+
order.certificate(force_chain: 'DST Root CA X3')
|
209
|
+
rescue Acme::Client::Error::ForcedChainNotFound
|
210
|
+
order.certificate
|
211
|
+
end
|
212
|
+
```
|
213
|
+
|
214
|
+
Note: if the specified forced chain doesn't match an existing alternative certificate the method will raise an `Acme::Client::Error::ForcedChainNotFound` error.
|
215
|
+
|
216
|
+
Learn more about the original Github issue for this client [here](https://github.com/unixcharles/acme-client/issues/186), information from Let's Encrypt [here](https://letsencrypt.org/2019/04/15/transitioning-to-isrg-root.html), and cross-signing [here](https://letsencrypt.org/certificates/#cross-signing).
|
217
|
+
|
189
218
|
## Extra
|
190
219
|
|
191
220
|
### Certificate revokation
|
@@ -198,12 +227,18 @@ client.revoke(certificate: certificate)
|
|
198
227
|
|
199
228
|
### Certificate renewal
|
200
229
|
|
201
|
-
|
230
|
+
There is no renewal process, just create a new order.
|
231
|
+
|
202
232
|
|
233
|
+
### Account Key Roll-over
|
203
234
|
|
204
|
-
|
235
|
+
To change the key used for an account you can call `#account_key_change` with the new private key or jwk.
|
205
236
|
|
206
|
-
|
237
|
+
```ruby
|
238
|
+
require 'openssl'
|
239
|
+
new_private_key = OpenSSL::PKey::RSA.new(4096)
|
240
|
+
client.account_key_change(private_key: new_private_key)
|
241
|
+
```
|
207
242
|
|
208
243
|
## Requirements
|
209
244
|
|
@@ -224,4 +259,3 @@ Yes.
|
|
224
259
|
## License
|
225
260
|
|
226
261
|
[MIT License](http://opensource.org/licenses/MIT)
|
227
|
-
|
data/acme-client.gemspec
CHANGED
@@ -16,11 +16,15 @@ Gem::Specification.new do |spec|
|
|
16
16
|
|
17
17
|
spec.required_ruby_version = '>= 2.1.0'
|
18
18
|
|
19
|
-
spec.add_development_dependency 'bundler', '
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
19
|
+
spec.add_development_dependency 'bundler', '>= 1.17.3'
|
20
|
+
if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.2')
|
21
|
+
spec.add_development_dependency 'rake', '>= 12.0'
|
22
|
+
else
|
23
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
24
|
+
end
|
25
|
+
spec.add_development_dependency 'rspec', '~> 3.9'
|
26
|
+
spec.add_development_dependency 'vcr', '~> 2.9'
|
27
|
+
spec.add_development_dependency 'webmock', '~> 3.8'
|
24
28
|
|
25
|
-
spec.add_runtime_dependency 'faraday', '
|
29
|
+
spec.add_runtime_dependency 'faraday', '>= 0.17', '< 2.0.0'
|
26
30
|
end
|
data/lib/acme/client.rb
CHANGED
@@ -20,6 +20,7 @@ require 'acme/client/faraday_middleware'
|
|
20
20
|
require 'acme/client/jwk'
|
21
21
|
require 'acme/client/error'
|
22
22
|
require 'acme/client/util'
|
23
|
+
require 'acme/client/chain_identifier'
|
23
24
|
|
24
25
|
class Acme::Client
|
25
26
|
DEFAULT_DIRECTORY = 'http://127.0.0.1:4000/directory'.freeze
|
@@ -29,7 +30,7 @@ class Acme::Client
|
|
29
30
|
pem: 'application/pem-certificate-chain'
|
30
31
|
}
|
31
32
|
|
32
|
-
def initialize(jwk: nil, kid: nil, private_key: nil, directory: DEFAULT_DIRECTORY, connection_options: {})
|
33
|
+
def initialize(jwk: nil, kid: nil, private_key: nil, directory: DEFAULT_DIRECTORY, connection_options: {}, bad_nonce_retry: 0)
|
33
34
|
if jwk.nil? && private_key.nil?
|
34
35
|
raise ArgumentError, 'must specify jwk or private_key'
|
35
36
|
end
|
@@ -41,6 +42,7 @@ class Acme::Client
|
|
41
42
|
end
|
42
43
|
|
43
44
|
@kid, @connection_options = kid, connection_options
|
45
|
+
@bad_nonce_retry = bad_nonce_retry
|
44
46
|
@directory = Acme::Client::Resources::Directory.new(URI(directory), @connection_options)
|
45
47
|
@nonces ||= []
|
46
48
|
end
|
@@ -83,13 +85,35 @@ class Acme::Client
|
|
83
85
|
Acme::Client::Resources::Account.new(self, url: kid, **arguments)
|
84
86
|
end
|
85
87
|
|
88
|
+
def account_key_change(new_private_key: nil, new_jwk: nil)
|
89
|
+
if new_private_key.nil? && new_jwk.nil?
|
90
|
+
raise ArgumentError, 'must specify new_jwk or new_private_key'
|
91
|
+
end
|
92
|
+
old_jwk = jwk
|
93
|
+
new_jwk ||= Acme::Client::JWK.from_private_key(new_private_key)
|
94
|
+
|
95
|
+
inner_payload_header = {
|
96
|
+
url: endpoint_for(:key_change)
|
97
|
+
}
|
98
|
+
inner_payload = {
|
99
|
+
account: kid,
|
100
|
+
oldKey: old_jwk.to_h
|
101
|
+
}
|
102
|
+
payload = JSON.parse(new_jwk.jws(header: inner_payload_header, payload: inner_payload))
|
103
|
+
|
104
|
+
response = post(endpoint_for(:key_change), payload: payload, mode: :kid)
|
105
|
+
arguments = attributes_from_account_response(response)
|
106
|
+
@jwk = new_jwk
|
107
|
+
Acme::Client::Resources::Account.new(self, url: kid, **arguments)
|
108
|
+
end
|
109
|
+
|
86
110
|
def account
|
87
111
|
@kid ||= begin
|
88
112
|
response = post(endpoint_for(:new_account), payload: { onlyReturnExisting: true }, mode: :jwk)
|
89
113
|
response.headers.fetch(:location)
|
90
114
|
end
|
91
115
|
|
92
|
-
response =
|
116
|
+
response = post_as_get(@kid)
|
93
117
|
arguments = attributes_from_account_response(response)
|
94
118
|
Acme::Client::Resources::Account.new(self, url: @kid, **arguments)
|
95
119
|
end
|
@@ -100,13 +124,7 @@ class Acme::Client
|
|
100
124
|
|
101
125
|
def new_order(identifiers:, not_before: nil, not_after: nil)
|
102
126
|
payload = {}
|
103
|
-
payload['identifiers'] =
|
104
|
-
identifiers
|
105
|
-
else
|
106
|
-
Array(identifiers).map do |identifier|
|
107
|
-
{ type: 'dns', value: identifier }
|
108
|
-
end
|
109
|
-
end
|
127
|
+
payload['identifiers'] = prepare_order_identifiers(identifiers)
|
110
128
|
payload['notBefore'] = not_before if not_before
|
111
129
|
payload['notAfter'] = not_after if not_after
|
112
130
|
|
@@ -116,7 +134,7 @@ class Acme::Client
|
|
116
134
|
end
|
117
135
|
|
118
136
|
def order(url:)
|
119
|
-
response =
|
137
|
+
response = post_as_get(url)
|
120
138
|
arguments = attributes_from_order_response(response)
|
121
139
|
Acme::Client::Resources::Order.new(self, **arguments.merge(url: url))
|
122
140
|
end
|
@@ -132,13 +150,28 @@ class Acme::Client
|
|
132
150
|
Acme::Client::Resources::Order.new(self, **arguments)
|
133
151
|
end
|
134
152
|
|
135
|
-
def certificate(url:)
|
153
|
+
def certificate(url:, force_chain: nil)
|
136
154
|
response = download(url, format: :pem)
|
137
|
-
response.body
|
155
|
+
pem = response.body
|
156
|
+
|
157
|
+
return pem if force_chain.nil?
|
158
|
+
|
159
|
+
return pem if ChainIdentifier.new(pem).match_name?(force_chain)
|
160
|
+
|
161
|
+
alternative_urls = Array(response.headers.dig('link', 'alternate'))
|
162
|
+
alternative_urls.each do |alternate_url|
|
163
|
+
response = download(alternate_url, format: :pem)
|
164
|
+
pem = response.body
|
165
|
+
if ChainIdentifier.new(pem).match_name?(force_chain)
|
166
|
+
return pem
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
raise Acme::Client::Error::ForcedChainNotFound, "Could not find any matching chain for `#{force_chain}`"
|
138
171
|
end
|
139
172
|
|
140
173
|
def authorization(url:)
|
141
|
-
response =
|
174
|
+
response = post_as_get(url)
|
142
175
|
arguments = attributes_from_authorization_response(response)
|
143
176
|
Acme::Client::Resources::Authorization.new(self, url: url, **arguments)
|
144
177
|
end
|
@@ -150,13 +183,13 @@ class Acme::Client
|
|
150
183
|
end
|
151
184
|
|
152
185
|
def challenge(url:)
|
153
|
-
response =
|
186
|
+
response = post_as_get(url)
|
154
187
|
arguments = attributes_from_challenge_response(response)
|
155
188
|
Acme::Client::Resources::Challenges.new(self, **arguments)
|
156
189
|
end
|
157
190
|
|
158
|
-
def request_challenge_validation(url:, key_authorization:)
|
159
|
-
response = post(url, payload: {
|
191
|
+
def request_challenge_validation(url:, key_authorization: nil)
|
192
|
+
response = post(url, payload: {})
|
160
193
|
arguments = attributes_from_challenge_response(response)
|
161
194
|
Acme::Client::Resources::Challenges.new(self, **arguments)
|
162
195
|
end
|
@@ -205,6 +238,20 @@ class Acme::Client
|
|
205
238
|
|
206
239
|
private
|
207
240
|
|
241
|
+
def prepare_order_identifiers(identifiers)
|
242
|
+
if identifiers.is_a?(Hash)
|
243
|
+
[identifiers]
|
244
|
+
else
|
245
|
+
Array(identifiers).map do |identifier|
|
246
|
+
if identifier.is_a?(String)
|
247
|
+
{ type: 'dns', value: identifier }
|
248
|
+
else
|
249
|
+
identifier
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
208
255
|
def attributes_from_account_response(response)
|
209
256
|
extract_attributes(
|
210
257
|
response.body,
|
@@ -251,14 +298,19 @@ class Acme::Client
|
|
251
298
|
connection.post(url, payload)
|
252
299
|
end
|
253
300
|
|
301
|
+
def post_as_get(url, mode: :kid)
|
302
|
+
connection = connection_for(url: url, mode: mode)
|
303
|
+
connection.post(url, nil)
|
304
|
+
end
|
305
|
+
|
254
306
|
def get(url, mode: :kid)
|
255
307
|
connection = connection_for(url: url, mode: mode)
|
256
308
|
connection.get(url)
|
257
309
|
end
|
258
310
|
|
259
311
|
def download(url, format:)
|
260
|
-
connection = connection_for(url: url, mode: :
|
261
|
-
connection.
|
312
|
+
connection = connection_for(url: url, mode: :kid)
|
313
|
+
connection.post do |request|
|
262
314
|
request.url(url)
|
263
315
|
request.headers['Accept'] = CONTENT_TYPES.fetch(format)
|
264
316
|
end
|
@@ -280,6 +332,12 @@ class Acme::Client
|
|
280
332
|
|
281
333
|
def new_connection(endpoint:)
|
282
334
|
Faraday.new(endpoint, **@connection_options) do |configuration|
|
335
|
+
if @bad_nonce_retry > 0
|
336
|
+
configuration.request(:retry,
|
337
|
+
max: @bad_nonce_retry,
|
338
|
+
methods: Faraday::Connection::METHODS,
|
339
|
+
exceptions: [Acme::Client::Error::BadNonce])
|
340
|
+
end
|
283
341
|
yield(configuration) if block_given?
|
284
342
|
configuration.adapter Faraday.default_adapter
|
285
343
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class Acme::Client
|
2
|
+
class ChainIdentifier
|
3
|
+
def initialize(pem_certificate_chain)
|
4
|
+
@pem_certificate_chain = pem_certificate_chain
|
5
|
+
end
|
6
|
+
|
7
|
+
def match_name?(name)
|
8
|
+
issuers.any? do |issuer|
|
9
|
+
issuer.include?(name)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def issuers
|
16
|
+
x509_certificates.map(&:issuer).map(&:to_s)
|
17
|
+
end
|
18
|
+
|
19
|
+
def x509_certificates
|
20
|
+
@x509_certificates ||= splitted_pem_certificates.map { |pem| OpenSSL::X509::Certificate.new(pem) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def splitted_pem_certificates
|
24
|
+
@pem_certificate_chain.each_line.slice_after(/END CERTIFICATE/).map(&:join)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/acme/client/error.rb
CHANGED
@@ -7,6 +7,7 @@ class Acme::Client::Error < StandardError
|
|
7
7
|
class UnsupportedChallengeType < ClientError; end
|
8
8
|
class NotFound < ClientError; end
|
9
9
|
class CertificateNotReady < ClientError; end
|
10
|
+
class ForcedChainNotFound < ClientError; end
|
10
11
|
|
11
12
|
class ServerError < Acme::Client::Error; end
|
12
13
|
class BadCSR < ServerError; end
|
@@ -82,18 +82,10 @@ class Acme::Client::FaradayMiddleware < Faraday::Middleware
|
|
82
82
|
end
|
83
83
|
end
|
84
84
|
|
85
|
-
LINK_MATCH = /<(.*?)>;rel="([\w-]+)"/
|
86
|
-
|
87
85
|
def decode_link_headers
|
88
86
|
return unless env.response_headers.key?('Link')
|
89
87
|
link_header = env.response_headers['Link']
|
90
|
-
|
91
|
-
links = link_header.split(', ').map { |entry|
|
92
|
-
_, link, name = *entry.match(LINK_MATCH)
|
93
|
-
[name, link]
|
94
|
-
}
|
95
|
-
|
96
|
-
Hash[*links.flatten]
|
88
|
+
Acme::Client::Util.decode_link_headers(link_header)
|
97
89
|
end
|
98
90
|
|
99
91
|
def store_nonce
|
data/lib/acme/client/jwk/base.rb
CHANGED
@@ -14,10 +14,10 @@ class Acme::Client::JWK::Base
|
|
14
14
|
# payload - A Hash of payload data.
|
15
15
|
#
|
16
16
|
# Returns a JSON String.
|
17
|
-
def jws(header: {}, payload:
|
17
|
+
def jws(header: {}, payload:)
|
18
18
|
header = jws_header(header)
|
19
19
|
encoded_header = Acme::Client::Util.urlsafe_base64(header.to_json)
|
20
|
-
encoded_payload = Acme::Client::Util.urlsafe_base64(payload.to_json)
|
20
|
+
encoded_payload = Acme::Client::Util.urlsafe_base64(payload.nil? ? '' : payload.to_json)
|
21
21
|
|
22
22
|
signature_data = "#{encoded_header}.#{encoded_payload}"
|
23
23
|
signature = sign(signature_data)
|
@@ -73,8 +73,10 @@ class Acme::Client::JWK::ECDSA < Acme::Client::JWK::Base
|
|
73
73
|
# BigNumbers
|
74
74
|
bns = ints.map(&:value)
|
75
75
|
|
76
|
+
byte_size = (@private_key.group.degree + 7) / 8
|
77
|
+
|
76
78
|
# Binary R/S values
|
77
|
-
r, s = bns.map { |bn|
|
79
|
+
r, s = bns.map { |bn| bn.to_s(2).rjust(byte_size, "\x00") }
|
78
80
|
|
79
81
|
# JWS wants raw R/S concatenated.
|
80
82
|
[r, s].join
|
@@ -4,6 +4,7 @@ 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/unsupported_challenge'
|
7
8
|
|
8
9
|
CHALLENGE_TYPES = {
|
9
10
|
'http-01' => Acme::Client::Resources::Challenges::HTTP01,
|
@@ -11,11 +12,6 @@ module Acme::Client::Resources::Challenges
|
|
11
12
|
}
|
12
13
|
|
13
14
|
def self.new(client, type:, **arguments)
|
14
|
-
|
15
|
-
if klass
|
16
|
-
klass.new(client, **arguments)
|
17
|
-
else
|
18
|
-
{ type: type }.merge(arguments)
|
19
|
-
end
|
15
|
+
CHALLENGE_TYPES.fetch(type, Unsupported).new(client, **arguments)
|
20
16
|
end
|
21
17
|
end
|
@@ -5,7 +5,7 @@ class Acme::Client::Resources::Challenges::Base
|
|
5
5
|
|
6
6
|
def initialize(client, **arguments)
|
7
7
|
@client = client
|
8
|
-
assign_attributes(arguments)
|
8
|
+
assign_attributes(**arguments)
|
9
9
|
end
|
10
10
|
|
11
11
|
def challenge_type
|
@@ -21,17 +21,9 @@ class Acme::Client::Resources::Challenges::Base
|
|
21
21
|
true
|
22
22
|
end
|
23
23
|
|
24
|
-
def send_challenge_vallidation(url:, key_authorization:)
|
25
|
-
@client.request_challenge_validation(
|
26
|
-
url: url,
|
27
|
-
key_authorization: key_authorization
|
28
|
-
).to_h
|
29
|
-
end
|
30
|
-
|
31
24
|
def request_validation
|
32
|
-
assign_attributes(**
|
33
|
-
url: url
|
34
|
-
key_authorization: key_authorization
|
25
|
+
assign_attributes(**send_challenge_validation(
|
26
|
+
url: url
|
35
27
|
))
|
36
28
|
true
|
37
29
|
end
|
@@ -42,6 +34,12 @@ class Acme::Client::Resources::Challenges::Base
|
|
42
34
|
|
43
35
|
private
|
44
36
|
|
37
|
+
def send_challenge_validation(url:)
|
38
|
+
@client.request_challenge_validation(
|
39
|
+
url: url
|
40
|
+
).to_h
|
41
|
+
end
|
42
|
+
|
45
43
|
def assign_attributes(status:, url:, token:, error: nil)
|
46
44
|
@status = status
|
47
45
|
@url = url
|
@@ -5,7 +5,7 @@ class Acme::Client::Resources::Order
|
|
5
5
|
|
6
6
|
def initialize(client, **arguments)
|
7
7
|
@client = client
|
8
|
-
assign_attributes(arguments)
|
8
|
+
assign_attributes(**arguments)
|
9
9
|
end
|
10
10
|
|
11
11
|
def reload
|
@@ -24,9 +24,9 @@ class Acme::Client::Resources::Order
|
|
24
24
|
true
|
25
25
|
end
|
26
26
|
|
27
|
-
def certificate
|
27
|
+
def certificate(force_chain: nil)
|
28
28
|
if certificate_url
|
29
|
-
@client.certificate(url: certificate_url)
|
29
|
+
@client.certificate(url: certificate_url, force_chain: force_chain)
|
30
30
|
else
|
31
31
|
raise Acme::Client::Error::CertificateNotReady, 'No certificate_url to collect the order'
|
32
32
|
end
|
data/lib/acme/client/util.rb
CHANGED
@@ -3,6 +3,17 @@ module Acme::Client::Util
|
|
3
3
|
Base64.urlsafe_encode64(data).sub(/[\s=]*\z/, '')
|
4
4
|
end
|
5
5
|
|
6
|
+
LINK_MATCH = /<(.*?)>\s?;\s?rel="([\w-]+)"/
|
7
|
+
|
8
|
+
# See RFC 8288 - https://tools.ietf.org/html/rfc8288#section-3
|
9
|
+
def decode_link_headers(link_header)
|
10
|
+
link_header.split(',').each_with_object({}) { |entry, hash|
|
11
|
+
_, link, name = *entry.match(LINK_MATCH)
|
12
|
+
hash[name] ||= []
|
13
|
+
hash[name].push(link)
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
6
17
|
# Sets public key on CSR or cert.
|
7
18
|
#
|
8
19
|
# obj - An OpenSSL::X509::Certificate or OpenSSL::X509::Request instance.
|
data/lib/acme/client/version.rb
CHANGED
metadata
CHANGED
@@ -1,69 +1,57 @@
|
|
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.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Charles Barbier
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-04-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "~>"
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '1.6'
|
20
17
|
- - ">="
|
21
18
|
- !ruby/object:Gem::Version
|
22
|
-
version: 1.
|
19
|
+
version: 1.17.3
|
23
20
|
type: :development
|
24
21
|
prerelease: false
|
25
22
|
version_requirements: !ruby/object:Gem::Requirement
|
26
23
|
requirements:
|
27
|
-
- - "~>"
|
28
|
-
- !ruby/object:Gem::Version
|
29
|
-
version: '1.6'
|
30
24
|
- - ">="
|
31
25
|
- !ruby/object:Gem::Version
|
32
|
-
version: 1.
|
26
|
+
version: 1.17.3
|
33
27
|
- !ruby/object:Gem::Dependency
|
34
28
|
name: rake
|
35
29
|
requirement: !ruby/object:Gem::Requirement
|
36
30
|
requirements:
|
37
31
|
- - "~>"
|
38
32
|
- !ruby/object:Gem::Version
|
39
|
-
version: '
|
33
|
+
version: '13.0'
|
40
34
|
type: :development
|
41
35
|
prerelease: false
|
42
36
|
version_requirements: !ruby/object:Gem::Requirement
|
43
37
|
requirements:
|
44
38
|
- - "~>"
|
45
39
|
- !ruby/object:Gem::Version
|
46
|
-
version: '
|
40
|
+
version: '13.0'
|
47
41
|
- !ruby/object:Gem::Dependency
|
48
42
|
name: rspec
|
49
43
|
requirement: !ruby/object:Gem::Requirement
|
50
44
|
requirements:
|
51
45
|
- - "~>"
|
52
46
|
- !ruby/object:Gem::Version
|
53
|
-
version: '3.
|
54
|
-
- - ">="
|
55
|
-
- !ruby/object:Gem::Version
|
56
|
-
version: 3.3.0
|
47
|
+
version: '3.9'
|
57
48
|
type: :development
|
58
49
|
prerelease: false
|
59
50
|
version_requirements: !ruby/object:Gem::Requirement
|
60
51
|
requirements:
|
61
52
|
- - "~>"
|
62
53
|
- !ruby/object:Gem::Version
|
63
|
-
version: '3.
|
64
|
-
- - ">="
|
65
|
-
- !ruby/object:Gem::Version
|
66
|
-
version: 3.3.0
|
54
|
+
version: '3.9'
|
67
55
|
- !ruby/object:Gem::Dependency
|
68
56
|
name: vcr
|
69
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -71,9 +59,6 @@ dependencies:
|
|
71
59
|
- - "~>"
|
72
60
|
- !ruby/object:Gem::Version
|
73
61
|
version: '2.9'
|
74
|
-
- - ">="
|
75
|
-
- !ruby/object:Gem::Version
|
76
|
-
version: 2.9.3
|
77
62
|
type: :development
|
78
63
|
prerelease: false
|
79
64
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -81,49 +66,40 @@ dependencies:
|
|
81
66
|
- - "~>"
|
82
67
|
- !ruby/object:Gem::Version
|
83
68
|
version: '2.9'
|
84
|
-
- - ">="
|
85
|
-
- !ruby/object:Gem::Version
|
86
|
-
version: 2.9.3
|
87
69
|
- !ruby/object:Gem::Dependency
|
88
70
|
name: webmock
|
89
71
|
requirement: !ruby/object:Gem::Requirement
|
90
72
|
requirements:
|
91
73
|
- - "~>"
|
92
74
|
- !ruby/object:Gem::Version
|
93
|
-
version: '
|
94
|
-
- - ">="
|
95
|
-
- !ruby/object:Gem::Version
|
96
|
-
version: 1.21.0
|
75
|
+
version: '3.8'
|
97
76
|
type: :development
|
98
77
|
prerelease: false
|
99
78
|
version_requirements: !ruby/object:Gem::Requirement
|
100
79
|
requirements:
|
101
80
|
- - "~>"
|
102
81
|
- !ruby/object:Gem::Version
|
103
|
-
version: '
|
104
|
-
- - ">="
|
105
|
-
- !ruby/object:Gem::Version
|
106
|
-
version: 1.21.0
|
82
|
+
version: '3.8'
|
107
83
|
- !ruby/object:Gem::Dependency
|
108
84
|
name: faraday
|
109
85
|
requirement: !ruby/object:Gem::Requirement
|
110
86
|
requirements:
|
111
|
-
- - "~>"
|
112
|
-
- !ruby/object:Gem::Version
|
113
|
-
version: '0.9'
|
114
87
|
- - ">="
|
115
88
|
- !ruby/object:Gem::Version
|
116
|
-
version: 0.
|
89
|
+
version: '0.17'
|
90
|
+
- - "<"
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: 2.0.0
|
117
93
|
type: :runtime
|
118
94
|
prerelease: false
|
119
95
|
version_requirements: !ruby/object:Gem::Requirement
|
120
96
|
requirements:
|
121
|
-
- - "~>"
|
122
|
-
- !ruby/object:Gem::Version
|
123
|
-
version: '0.9'
|
124
97
|
- - ">="
|
125
98
|
- !ruby/object:Gem::Version
|
126
|
-
version: 0.
|
99
|
+
version: '0.17'
|
100
|
+
- - "<"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: 2.0.0
|
127
103
|
description:
|
128
104
|
email:
|
129
105
|
- unixcharles@gmail.com
|
@@ -148,6 +124,7 @@ files:
|
|
148
124
|
- lib/acme/client.rb
|
149
125
|
- lib/acme/client/certificate_request.rb
|
150
126
|
- lib/acme/client/certificate_request/ec_key_patch.rb
|
127
|
+
- lib/acme/client/chain_identifier.rb
|
151
128
|
- lib/acme/client/error.rb
|
152
129
|
- lib/acme/client/faraday_middleware.rb
|
153
130
|
- lib/acme/client/jwk.rb
|
@@ -161,6 +138,7 @@ files:
|
|
161
138
|
- lib/acme/client/resources/challenges/base.rb
|
162
139
|
- lib/acme/client/resources/challenges/dns01.rb
|
163
140
|
- lib/acme/client/resources/challenges/http01.rb
|
141
|
+
- lib/acme/client/resources/challenges/unsupported_challenge.rb
|
164
142
|
- lib/acme/client/resources/directory.rb
|
165
143
|
- lib/acme/client/resources/order.rb
|
166
144
|
- lib/acme/client/self_sign_certificate.rb
|
@@ -185,8 +163,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
185
163
|
- !ruby/object:Gem::Version
|
186
164
|
version: '0'
|
187
165
|
requirements: []
|
188
|
-
|
189
|
-
rubygems_version: 2.5.2.3
|
166
|
+
rubygems_version: 3.1.2
|
190
167
|
signing_key:
|
191
168
|
specification_version: 4
|
192
169
|
summary: Client for the ACME protocol.
|