acme-client 2.0.6 → 2.0.9
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/.github/workflows/rubocop.yml +23 -0
- data/.github/workflows/test.yml +26 -0
- data/CHANGELOG.md +17 -0
- data/Gemfile +4 -0
- data/README.md +47 -16
- data/acme-client.gemspec +1 -0
- data/lib/acme/client/chain_identifier.rb +27 -0
- data/lib/acme/client/error.rb +1 -0
- data/lib/acme/client/faraday_middleware.rb +4 -12
- 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/base.rb +1 -1
- data/lib/acme/client/resources/directory.rb +6 -2
- 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
- data/lib/acme/client.rb +40 -2
- metadata +20 -4
- data/.travis.yml +0 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d1fcaf1206f38a9ded860ad3685de94d3c2743a04222d63a6d5a14ca35146daa
|
4
|
+
data.tar.gz: 766a3bf59e3877a3a4ab457e369f9d09626204afacc8e1b8cb23015cde9ccf12
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 814205156a08f49d3481545f35306f63e1f342267f126edc528b22c78ce477b4696fd4a9f401a501bb8ffe33c57f42e8e6826de830e5321289555972d841bcb3
|
7
|
+
data.tar.gz: 493a4a2ebe8803b3949797bd74061d500bb66e7a79c5d62e9dfb98af7b995a46d3a2847c88d14ad7585f8b25567ccdabdf5bdaa25eafdb045a1f3aaed7b05f13
|
@@ -0,0 +1,23 @@
|
|
1
|
+
name: Lint
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches: [ master ]
|
6
|
+
pull_request:
|
7
|
+
branches: [ master ]
|
8
|
+
|
9
|
+
jobs:
|
10
|
+
rubocop:
|
11
|
+
runs-on: ubuntu-latest
|
12
|
+
strategy:
|
13
|
+
matrix:
|
14
|
+
ruby-version: ['3.0']
|
15
|
+
steps:
|
16
|
+
- uses: actions/checkout@v2
|
17
|
+
- name: Set up Ruby
|
18
|
+
uses: ruby/setup-ruby@v1
|
19
|
+
with:
|
20
|
+
ruby-version: ${{ matrix.ruby-version }}
|
21
|
+
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
22
|
+
- name: Run tests
|
23
|
+
run: bundle exec rake rubocop
|
@@ -0,0 +1,26 @@
|
|
1
|
+
name: CI
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches: [ master ]
|
6
|
+
pull_request:
|
7
|
+
branches: [ master ]
|
8
|
+
|
9
|
+
jobs:
|
10
|
+
test:
|
11
|
+
runs-on: ubuntu-latest
|
12
|
+
strategy:
|
13
|
+
matrix:
|
14
|
+
ruby-version: ['2.6', '2.7', '3.0']
|
15
|
+
faraday-version: ['~> 0.17.3', '~> 1.7']
|
16
|
+
env:
|
17
|
+
FARADAY_VERSION: ${{ matrix.faraday-version }}
|
18
|
+
steps:
|
19
|
+
- uses: actions/checkout@v2
|
20
|
+
- name: Set up Ruby
|
21
|
+
uses: ruby/setup-ruby@v1
|
22
|
+
with:
|
23
|
+
ruby-version: ${{ matrix.ruby-version }}
|
24
|
+
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
25
|
+
- name: Run tests
|
26
|
+
run: bundle exec rake spec
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,20 @@
|
|
1
|
+
## `2.0.9`
|
2
|
+
|
3
|
+
* Support for Ruby 3.0 and Faraday 0.17.x
|
4
|
+
* Raise when directory is rate limited
|
5
|
+
|
6
|
+
## `2.0.8`
|
7
|
+
|
8
|
+
* Add support for the keyChange endpoint
|
9
|
+
|
10
|
+
https://tools.ietf.org/html/rfc8555#section-7.3.5
|
11
|
+
|
12
|
+
|
13
|
+
## `2.0.7`
|
14
|
+
|
15
|
+
* Add support for alternate certificate chain
|
16
|
+
* Change `Link` headers parsing to return array of value. This add support multiple entries at the same `rel`
|
17
|
+
|
1
18
|
## `2.0.6`
|
2
19
|
|
3
20
|
* Allow Faraday up to `< 2.0`
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -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
|
@@ -184,11 +193,28 @@ csr = Acme::Client::CertificateRequest.new(private_key: a_different_private_key,
|
|
184
193
|
order.finalize(csr: csr)
|
185
194
|
while order.status == 'processing'
|
186
195
|
sleep(1)
|
187
|
-
|
196
|
+
order.reload
|
188
197
|
end
|
189
198
|
order.certificate # => PEM-formatted certificate
|
190
199
|
```
|
191
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
|
+
|
192
218
|
## Extra
|
193
219
|
|
194
220
|
### Certificate revokation
|
@@ -204,9 +230,15 @@ client.revoke(certificate: certificate)
|
|
204
230
|
There is no renewal process, just create a new order.
|
205
231
|
|
206
232
|
|
207
|
-
|
233
|
+
### Account Key Roll-over
|
234
|
+
|
235
|
+
To change the key used for an account you can call `#account_key_change` with the new private key or jwk.
|
208
236
|
|
209
|
-
|
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
|
+
```
|
210
242
|
|
211
243
|
## Requirements
|
212
244
|
|
@@ -227,4 +259,3 @@ Yes.
|
|
227
259
|
## License
|
228
260
|
|
229
261
|
[MIT License](http://opensource.org/licenses/MIT)
|
230
|
-
|
data/acme-client.gemspec
CHANGED
@@ -25,6 +25,7 @@ Gem::Specification.new do |spec|
|
|
25
25
|
spec.add_development_dependency 'rspec', '~> 3.9'
|
26
26
|
spec.add_development_dependency 'vcr', '~> 2.9'
|
27
27
|
spec.add_development_dependency 'webmock', '~> 3.8'
|
28
|
+
spec.add_development_dependency 'webrick'
|
28
29
|
|
29
30
|
spec.add_runtime_dependency 'faraday', '>= 0.17', '< 2.0.0'
|
30
31
|
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
|
@@ -5,10 +5,10 @@ class Acme::Client::FaradayMiddleware < Faraday::Middleware
|
|
5
5
|
|
6
6
|
CONTENT_TYPE = 'application/jose+json'
|
7
7
|
|
8
|
-
def initialize(app,
|
8
|
+
def initialize(app, options)
|
9
9
|
super(app)
|
10
|
-
@client = client
|
11
|
-
@mode = mode
|
10
|
+
@client = options.fetch(:client)
|
11
|
+
@mode = options.fetch(:mode)
|
12
12
|
end
|
13
13
|
|
14
14
|
def call(env)
|
@@ -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
|
@@ -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
|
@@ -68,9 +68,13 @@ class Acme::Client::Resources::Directory
|
|
68
68
|
end
|
69
69
|
|
70
70
|
def fetch_directory
|
71
|
-
connection = Faraday.new(url: @directory, **@connection_options)
|
71
|
+
connection = Faraday.new(url: @directory, **@connection_options) do |configuration|
|
72
|
+
configuration.use Acme::Client::FaradayMiddleware, client: nil, mode: nil
|
73
|
+
|
74
|
+
configuration.adapter Faraday.default_adapter
|
75
|
+
end
|
72
76
|
connection.headers[:user_agent] = Acme::Client::USER_AGENT
|
73
77
|
response = connection.get(@url)
|
74
|
-
|
78
|
+
response.body
|
75
79
|
end
|
76
80
|
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
|
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
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
|
@@ -84,6 +85,28 @@ class Acme::Client
|
|
84
85
|
Acme::Client::Resources::Account.new(self, url: kid, **arguments)
|
85
86
|
end
|
86
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
|
+
|
87
110
|
def account
|
88
111
|
@kid ||= begin
|
89
112
|
response = post(endpoint_for(:new_account), payload: { onlyReturnExisting: true }, mode: :jwk)
|
@@ -127,9 +150,24 @@ class Acme::Client
|
|
127
150
|
Acme::Client::Resources::Order.new(self, **arguments)
|
128
151
|
end
|
129
152
|
|
130
|
-
def certificate(url:)
|
153
|
+
def certificate(url:, force_chain: nil)
|
131
154
|
response = download(url, format: :pem)
|
132
|
-
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}`"
|
133
171
|
end
|
134
172
|
|
135
173
|
def authorization(url:)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
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.9
|
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-08-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -80,6 +80,20 @@ dependencies:
|
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '3.8'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: webrick
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
83
97
|
- !ruby/object:Gem::Dependency
|
84
98
|
name: faraday
|
85
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -107,10 +121,11 @@ executables: []
|
|
107
121
|
extensions: []
|
108
122
|
extra_rdoc_files: []
|
109
123
|
files:
|
124
|
+
- ".github/workflows/rubocop.yml"
|
125
|
+
- ".github/workflows/test.yml"
|
110
126
|
- ".gitignore"
|
111
127
|
- ".rspec"
|
112
128
|
- ".rubocop.yml"
|
113
|
-
- ".travis.yml"
|
114
129
|
- CHANGELOG.md
|
115
130
|
- Gemfile
|
116
131
|
- LICENSE.txt
|
@@ -124,6 +139,7 @@ files:
|
|
124
139
|
- lib/acme/client.rb
|
125
140
|
- lib/acme/client/certificate_request.rb
|
126
141
|
- lib/acme/client/certificate_request/ec_key_patch.rb
|
142
|
+
- lib/acme/client/chain_identifier.rb
|
127
143
|
- lib/acme/client/error.rb
|
128
144
|
- lib/acme/client/faraday_middleware.rb
|
129
145
|
- lib/acme/client/jwk.rb
|
@@ -162,7 +178,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
162
178
|
- !ruby/object:Gem::Version
|
163
179
|
version: '0'
|
164
180
|
requirements: []
|
165
|
-
rubygems_version: 3.
|
181
|
+
rubygems_version: 3.1.2
|
166
182
|
signing_key:
|
167
183
|
specification_version: 4
|
168
184
|
summary: Client for the ACME protocol.
|