acme-client 2.0.3 → 2.0.8

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: c5cf05983ff0530ff6a437895b70d686deb7ffd8
4
- data.tar.gz: 351f8408667c94ff7107f95b48215b589736db34
2
+ SHA256:
3
+ metadata.gz: 9cb714bd4f0321181c50087f976122251233340d09f4ed612cf91070e0986d70
4
+ data.tar.gz: db7df58ad9a1e4794bef25dfbc309efad73b9b1cbb6100f77d9552cbb734fb5d
5
5
  SHA512:
6
- metadata.gz: 419dde577de805c570f1b502ec660eb7734aba59f8243f7e7febc46a2eacc86e5e93d922bca0085dc5eb3cd783e1ccd1927a8b2844c55ed8d12a717d66603f44
7
- data.tar.gz: d0a6e1682e3d3bbe04f645e1faf5b6ef11675e24cece7475ec42c134a9aa7c15c85c61e1518c5670793b4dfa42a3aad83234627aa249c7e4c74cba3d44669b22
6
+ metadata.gz: 8d7eb7d76ea95ab724810af93ad4e79af03fe62752509a793b5310b01b670b7c57f737bbb3a68de72ab4e84f01f9d613e45937a50119ddfe3b0f5d0aa628f45e
7
+ data.tar.gz: 38b349fdcdb68f5ec493951ec4a480966256e591f6d9cffc4266116bff6d2aed6e8f0e5493acf985f6a81c0cad7c20a69e4d7bd1cedb95357b297cdcfb1b494c
data/.gitignore CHANGED
@@ -9,3 +9,4 @@
9
9
  /tmp/
10
10
  /vendor/bundle
11
11
  /.idea/
12
+ .tool-versions
data/.rubocop.yml CHANGED
@@ -7,133 +7,128 @@ AllCops:
7
7
  Rails:
8
8
  Enabled: false
9
9
 
10
- Style/FileName:
11
- Exclude:
12
- - 'lib/acme-client.rb'
10
+ Layout/AlignParameters:
11
+ EnforcedStyle: with_fixed_indentation
13
12
 
14
- Lint/AssignmentInCondition:
13
+ Layout/ElseAlignment:
15
14
  Enabled: false
16
15
 
17
- Style/ClassAndModuleChildren:
18
- Enabled: false
16
+ Layout/FirstParameterIndentation:
17
+ EnforcedStyle: consistent
19
18
 
20
- Style/Documentation:
19
+ Layout/IndentationWidth:
21
20
  Enabled: false
22
21
 
23
22
  Layout/MultilineOperationIndentation:
24
23
  Enabled: false
25
24
 
26
- Style/SignalException:
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
- Style/MultipleComparison:
28
+ Lint/AmbiguousOperator:
36
29
  Enabled: false
37
30
 
38
- Layout/IndentationWidth:
31
+ Lint/AssignmentInCondition:
39
32
  Enabled: false
40
33
 
41
- Style/SymbolArray:
34
+ Lint/EndAlignment:
42
35
  Enabled: false
43
36
 
44
- Layout/FirstParameterIndentation:
45
- EnforcedStyle: consistent
46
-
47
- Style/TrailingCommaInArguments:
48
- Enabled: false
37
+ Lint/UnusedMethodArgument:
38
+ AllowUnusedKeywordArguments: true
49
39
 
50
- Style/PercentLiteralDelimiters:
40
+ Metrics/AbcSize:
51
41
  Enabled: false
52
42
 
53
43
  Metrics/BlockLength:
54
44
  Enabled: false
55
45
 
56
- Layout/SpaceInsideBlockBraces:
46
+ Metrics/ClassLength:
57
47
  Enabled: false
58
48
 
59
- Style/StringLiterals:
60
- Enabled: single_quotes
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
- Lint/EndAlignment:
63
+ Metrics/PerceivedComplexity:
70
64
  Enabled: false
71
65
 
72
- Style/ParallelAssignment:
66
+ Security/JSONLoad:
73
67
  Enabled: false
74
68
 
75
- Style/ModuleFunction:
69
+ Style/AccessorMethodName:
76
70
  Enabled: false
77
71
 
78
- Style/TrivialAccessors:
79
- AllowPredicates: true
80
-
81
- Lint/UnusedMethodArgument:
82
- AllowUnusedKeywordArguments: true
72
+ Style/Alias:
73
+ Enabled: false
83
74
 
84
- Metrics/MethodLength:
85
- Max: 15
75
+ Style/BlockDelimiters:
76
+ EnforcedStyle: semantic
86
77
 
87
- Style/DoubleNegation:
78
+ Style/ClassAndModuleChildren:
88
79
  Enabled: false
89
80
 
90
- Style/IfUnlessModifier:
81
+ Style/Documentation:
91
82
  Enabled: false
92
83
 
93
- Style/MultilineBlockChain:
84
+ Style/DoubleNegation:
94
85
  Enabled: false
95
86
 
96
- Style/BlockDelimiters:
97
- EnforcedStyle: semantic
87
+ Style/FileName:
88
+ Exclude:
89
+ - 'lib/acme-client.rb'
98
90
 
99
- Style/Lambda:
91
+ Style/GlobalVars:
100
92
  Enabled: false
101
93
 
102
94
  Style/GuardClause:
103
95
  Enabled: false
104
96
 
105
- Style/Alias:
97
+ Style/IfUnlessModifier:
106
98
  Enabled: false
107
99
 
108
- Lint/AmbiguousOperator:
100
+ Style/Lambda:
109
101
  Enabled: false
110
102
 
111
- Metrics/MethodLength:
103
+ Style/ModuleFunction:
112
104
  Enabled: false
113
105
 
114
- Metrics/PerceivedComplexity:
106
+ Style/MultilineBlockChain:
115
107
  Enabled: false
116
108
 
117
- Metrics/CyclomaticComplexity:
109
+ Style/MultipleComparison:
118
110
  Enabled: false
119
111
 
120
- Metrics/AbcSize:
112
+ Style/MutableConstant:
121
113
  Enabled: false
122
114
 
123
- Metrics/ClassLength:
115
+ Style/ParallelAssignment:
124
116
  Enabled: false
125
117
 
126
- Style/MutableConstant:
118
+ Style/PercentLiteralDelimiters:
127
119
  Enabled: false
128
120
 
129
- Style/GlobalVars:
130
- Enabled: false
121
+ Style/SignalException:
122
+ EnforcedStyle: only_raise
131
123
 
132
- Style/ExpandPathArguments:
124
+ Style/SymbolArray:
133
125
  Enabled: false
134
126
 
135
- Security/JSONLoad:
136
- Enabled: false
127
+ Style/StringLiterals:
128
+ Enabled: single_quotes
137
129
 
138
- Style/AccessorMethodName:
130
+ Style/TrailingCommaInArguments:
139
131
  Enabled: false
132
+
133
+ Style/TrivialAccessors:
134
+ AllowPredicates: true
data/.travis.yml CHANGED
@@ -1,8 +1,10 @@
1
1
  language: ruby
2
2
  cache: bundler
3
3
  rvm:
4
- - 2.1
5
- - 2.2
6
- - 2.3.3
7
- - 2.4.0
8
- - 2.6.1
4
+ - 2.5
5
+ - 2.6
6
+ - 2.7
7
+
8
+ before_install:
9
+ - gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true
10
+ - gem install bundler -v '< 2'
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
@@ -1,4 +1,5 @@
1
1
  source 'https://rubygems.org'
2
+
2
3
  gemspec
3
4
 
4
5
  group :development, :test do
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![Build Status](https://travis-ci.org/unixcharles/acme-client.svg?branch=master)](https://travis-ci.org/unixcharles/acme-client)
4
4
 
5
- `acme-client` is a client implementation of the [ACMEv2](https://github.com/ietf-wg-acme/acme) protocol in Ruby.
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
- * [Setting up a client](#setting-up-a-client)
27
- * [Account management](#account-management)
28
- * [Obtaining a certificate](#obtaining-a-certificate)
29
- * [Ordering a certificate](#ordering-a-certificate)
30
- * [Completing an HTTP challenge](#preparing-for-http-challenge)
31
- * [Completing an DNS challenge](#preparing-for-dns-challenge)
32
- * [Requesting a challenge verification](#requesting-a-challenge-verification)
33
- * [Downloading a certificate](#downloading-a-certificate)
34
- * [Extra](#extra)
35
- * [Certificate revokation](#certificate-revokation)
36
- * [Certificate renewal](#certificate-renewal)
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
- sleep(1) while order.status == 'processing'
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
- The is no renewal process, just create a new order.
230
+ There is no renewal process, just create a new order.
231
+
202
232
 
233
+ ### Account Key Roll-over
203
234
 
204
- ## Not implemented
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
- - Account Key Roll-over.
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', '~> 1.6', '>= 1.6.9'
20
- spec.add_development_dependency 'rake', '~> 10.0'
21
- spec.add_development_dependency 'rspec', '~> 3.3', '>= 3.3.0'
22
- spec.add_development_dependency 'vcr', '~> 2.9', '>= 2.9.3'
23
- spec.add_development_dependency 'webmock', '~> 1.21', '>= 1.21.0'
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', '~> 0.9', '>= 0.9.1'
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 = post(@kid)
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'] = if identifiers.is_a?(Hash)
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 = get(url)
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 = get(url)
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 = get(url)
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: { keyAuthorization: key_authorization })
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: :download)
261
- connection.get do |request|
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
@@ -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
@@ -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| [bn.to_s(16)].pack('H*') }
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
@@ -5,7 +5,7 @@ class Acme::Client::Resources::Account
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 kid
@@ -5,7 +5,7 @@ class Acme::Client::Resources::Authorization
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 deactivate
@@ -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
- klass = CHALLENGE_TYPES[type]
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(**send_challenge_vallidation(
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
@@ -0,0 +1,2 @@
1
+ class Acme::Client::Resources::Challenges::Unsupported < Acme::Client::Resources::Challenges::Base
2
+ end
@@ -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
@@ -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.
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Acme
4
4
  class Client
5
- VERSION = '2.0.3'.freeze
5
+ VERSION = '2.0.8'.freeze
6
6
  end
7
7
  end
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.3
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: 2019-04-24 00:00:00.000000000 Z
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.6.9
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.6.9
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: '10.0'
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: '10.0'
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.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.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: '1.21'
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: '1.21'
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.9.1
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.9.1
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
- rubyforge_project:
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.