acme-client 2.0.0 → 2.0.18
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 +91 -0
- data/Gemfile +5 -5
- data/README.md +75 -27
- data/Rakefile +1 -4
- data/acme-client.gemspec +9 -8
- data/bin/generate_keystash +9 -0
- data/bin/release +1 -0
- data/lib/acme/client/chain_identifier.rb +27 -0
- data/lib/acme/client/error.rb +5 -0
- data/lib/acme/client/http_client.rb +162 -0
- data/lib/acme/client/jwk/base.rb +2 -2
- data/lib/acme/client/jwk/ecdsa.rb +7 -5
- data/lib/acme/client/jwk/hmac.rb +30 -0
- data/lib/acme/client/jwk.rb +1 -0
- data/lib/acme/client/resources/account.rb +5 -5
- data/lib/acme/client/resources/authorization.rb +4 -4
- data/lib/acme/client/resources/challenges/base.rb +11 -5
- data/lib/acme/client/resources/challenges/unsupported_challenge.rb +2 -0
- data/lib/acme/client/resources/challenges.rb +2 -6
- data/lib/acme/client/resources/directory.rb +9 -23
- data/lib/acme/client/resources/order.rb +5 -5
- data/lib/acme/client/util.rb +13 -2
- data/lib/acme/client/version.rb +1 -1
- data/lib/acme/client.rb +117 -48
- metadata +55 -60
- data/.gitignore +0 -10
- data/.rspec +0 -3
- data/.rubocop.yml +0 -139
- data/.travis.yml +0 -7
- data/lib/acme/client/faraday_middleware.rb +0 -116
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 88c2953c9fcfd9a7f7825b4c69b2cf0ff86befb504914e6f78b03f6fdaab052b
|
4
|
+
data.tar.gz: bddedcd46dc0b2d1224a7d409916668aa31bfad3c6576a0d09376257c654f434
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 999d2d254b29f3fdefe3af90333091c5a034d5aa3b3c3274bfe1c7787fe9c14723bf288ba0d7e4dce5fa3aad3352ecd0ef49bd60c93f4b51ab7e5d51017a9a1e
|
7
|
+
data.tar.gz: 0d16f423760bd8f714ce94de1767201dd8c842ae3fca2ec8d93c7364ea15c61f24f9c66c4175e2d7091cf263467caeb4a5e049f996c92173d64269ba5c11629b
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,94 @@
|
|
1
|
+
## `2.0.18`
|
2
|
+
|
3
|
+
* Fix an issue public key encoding. `OpenSSL::BN` cause keys with leading zero to fail.
|
4
|
+
|
5
|
+
## `2.0.17`
|
6
|
+
|
7
|
+
* Fix bug where depending on call order `jws` get generated with the wrong `kid`
|
8
|
+
|
9
|
+
## `2.0.16`
|
10
|
+
|
11
|
+
* Refactor Directory
|
12
|
+
* Fix an issue where the client would crash when ACME provider return nonce for directory endpoint
|
13
|
+
|
14
|
+
## `2.0.15`
|
15
|
+
|
16
|
+
* Also pass connection_options to Faraday for Client#get_nonce
|
17
|
+
|
18
|
+
|
19
|
+
## `2.0.14`
|
20
|
+
|
21
|
+
* Fix Faraday HTTP exceptions leaking out, always raise `Acme::Client::Error` instead
|
22
|
+
|
23
|
+
## `2.0.13`
|
24
|
+
|
25
|
+
* Add support for External Account Binding
|
26
|
+
|
27
|
+
## `2.0.12`
|
28
|
+
|
29
|
+
* Update test matrix to current Ruby versions (2.7 to 3.2)
|
30
|
+
* Support for Faraday retry 2.x
|
31
|
+
|
32
|
+
## `2.0.11`
|
33
|
+
|
34
|
+
* Add support for error code `AlreadyRevoked` and `BadPublicKey`
|
35
|
+
|
36
|
+
## `2.0.10`
|
37
|
+
|
38
|
+
* Support for Faraday 1.0 / 2.0
|
39
|
+
|
40
|
+
## `2.0.9`
|
41
|
+
|
42
|
+
* Support for Ruby 3.0 and Faraday 0.17.x
|
43
|
+
* Raise when directory is rate limited
|
44
|
+
|
45
|
+
## `2.0.8`
|
46
|
+
|
47
|
+
* Add support for the keyChange endpoint
|
48
|
+
|
49
|
+
https://tools.ietf.org/html/rfc8555#section-7.3.5
|
50
|
+
|
51
|
+
|
52
|
+
## `2.0.7`
|
53
|
+
|
54
|
+
* Add support for alternate certificate chain
|
55
|
+
* Change `Link` headers parsing to return array of value. This add support multiple entries at the same `rel`
|
56
|
+
|
57
|
+
## `2.0.6`
|
58
|
+
|
59
|
+
* Allow Faraday up to `< 2.0`
|
60
|
+
|
61
|
+
## `2.0.5`
|
62
|
+
|
63
|
+
* Use post-as-get
|
64
|
+
* Remove deprecated keyAuthorization
|
65
|
+
|
66
|
+
## `2.0.4`
|
67
|
+
|
68
|
+
* Add an option to retry bad nonce errors
|
69
|
+
|
70
|
+
## `2.0.3`
|
71
|
+
|
72
|
+
* Do not try to set the body on GET request
|
73
|
+
|
74
|
+
## `2.0.2`
|
75
|
+
|
76
|
+
* Fix constant lookup on InvalidDirectory
|
77
|
+
* Forward connection options when fetching nonce
|
78
|
+
* Fix splats without parenthesis warning
|
79
|
+
|
80
|
+
## `2.0.1`
|
81
|
+
|
82
|
+
* Properly require URI
|
83
|
+
|
84
|
+
## `2.0.0`
|
85
|
+
|
86
|
+
* Release of the `ACMEv2` branch
|
87
|
+
|
88
|
+
## `1.0.0`
|
89
|
+
|
90
|
+
* Development for `ACMEv1` moved into `1.0.x`
|
91
|
+
|
1
92
|
## `0.6.3`
|
2
93
|
|
3
94
|
* Handle Faraday::ConnectionFailed errors as Timeout error.
|
data/Gemfile
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
|
+
|
2
3
|
gemspec
|
3
4
|
|
5
|
+
if faraday_version = ENV['FARADAY_VERSION']
|
6
|
+
gem 'faraday', faraday_version
|
7
|
+
end
|
8
|
+
|
4
9
|
group :development, :test do
|
5
10
|
gem 'pry'
|
6
|
-
gem 'rubocop', '~> 0.49.0'
|
7
11
|
gem 'ruby-prof', require: false
|
8
|
-
|
9
|
-
if Gem::Version.new(RUBY_VERSION) <= Gem::Version.new('2.2.2')
|
10
|
-
gem 'activesupport', '~> 4.2.6'
|
11
|
-
end
|
12
12
|
end
|
data/README.md
CHANGED
@@ -1,15 +1,11 @@
|
|
1
1
|
# Acme::Client
|
2
2
|
|
3
|
-
[
|
4
|
-
|
5
|
-
`acme-client` is a client implementation of the [ACMEv2](https://github.com/ietf-wg-acme/acme) protocol in Ruby.
|
3
|
+
`acme-client` is a client implementation of the ACME / [RFC 8555](https://tools.ietf.org/html/rfc8555) protocol in Ruby.
|
6
4
|
|
7
5
|
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
6
|
|
9
7
|
ACME is part of the [Letsencrypt](https://letsencrypt.org/) project, which goal is to provide free SSL/TLS certificates with automation of the acquiring and renewal process.
|
10
8
|
|
11
|
-
You can find ACMEv1 compatible client in the [acme-v1](https://github.com/unixcharles/acme-client/tree/acme-v1) branch.
|
12
|
-
|
13
9
|
## Installation
|
14
10
|
|
15
11
|
Via RubyGems:
|
@@ -23,17 +19,26 @@ gem 'acme-client'
|
|
23
19
|
```
|
24
20
|
|
25
21
|
## Usage
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
22
|
+
- [Acme::Client](#acmeclient)
|
23
|
+
- [Installation](#installation)
|
24
|
+
- [Usage](#usage)
|
25
|
+
- [Setting up a client](#setting-up-a-client)
|
26
|
+
- [Account management](#account-management)
|
27
|
+
- [Obtaining a certificate](#obtaining-a-certificate)
|
28
|
+
- [Ordering a certificate](#ordering-a-certificate)
|
29
|
+
- [Preparing for HTTP challenge](#preparing-for-http-challenge)
|
30
|
+
- [Preparing for DNS challenge](#preparing-for-dns-challenge)
|
31
|
+
- [Requesting a challenge verification](#requesting-a-challenge-verification)
|
32
|
+
- [Downloading a certificate](#downloading-a-certificate)
|
33
|
+
- [Ordering an alternative certificate](#ordering-an-alternative-certificate)
|
34
|
+
- [Extra](#extra)
|
35
|
+
- [Certificate revokation](#certificate-revokation)
|
36
|
+
- [Certificate renewal](#certificate-renewal)
|
37
|
+
- [Not implemented](#not-implemented)
|
38
|
+
- [Requirements](#requirements)
|
39
|
+
- [Development](#development)
|
40
|
+
- [Pull request?](#pull-request)
|
41
|
+
- [License](#license)
|
37
42
|
|
38
43
|
## Setting up a client
|
39
44
|
|
@@ -41,7 +46,7 @@ The client is initialized with a private key and the directory of your ACME prov
|
|
41
46
|
|
42
47
|
LetsEncrypt's `directory` is `https://acme-v02.api.letsencrypt.org/directory`.
|
43
48
|
|
44
|
-
They also have a staging
|
49
|
+
They also have a staging endpoint at `https://acme-staging-v02.api.letsencrypt.org/directory`.
|
45
50
|
|
46
51
|
`acme-ruby` expects `OpenSSL::PKey::RSA` or `OpenSSL::PKey::EC`
|
47
52
|
|
@@ -89,6 +94,25 @@ account = client.new_account(contact: 'mailto:info@example.com', terms_of_servic
|
|
89
94
|
account.kid # => <kid string>
|
90
95
|
```
|
91
96
|
|
97
|
+
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:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
client = Acme::Client.new(private_key: private_key, directory: 'https://acme-staging-v02.api.letsencrypt.org/directory')
|
101
|
+
|
102
|
+
# kid is not set, therefore a call to newAccount is made to lazy-initialize the kid
|
103
|
+
client.kid
|
104
|
+
=> "https://acme-staging-v02.api.letsencrypt.org/acme/acct/000000"
|
105
|
+
```
|
106
|
+
|
107
|
+
## External Account Binding support
|
108
|
+
|
109
|
+
You can use External Account Binding by providing a `external_account_binding` with a `kid` and `hmac_key`.
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
client = Acme::Client.new(private_key: private_key, directory: 'https://acme.zerossl.com/v2/DV90')
|
113
|
+
account = client.new_account(contact: 'mailto:info@example.com', terms_of_service_agreed: true, external_account_binding: { kid: "your kid", hmac_key: "your hmac key"})
|
114
|
+
```
|
115
|
+
|
92
116
|
## Obtaining a certificate
|
93
117
|
### Ordering a certificate
|
94
118
|
|
@@ -96,7 +120,7 @@ To order a new certificate, the client must provide a list of identifiers.
|
|
96
120
|
|
97
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.
|
98
122
|
|
99
|
-
Each authorization contains multiple challenges, typically a `dns-01` and a `http-01` challenge. The applicant is only required to complete one the challenges.
|
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.
|
100
124
|
|
101
125
|
You can access the challenge you wish to complete using the `#dns` or `#http` method.
|
102
126
|
|
@@ -151,7 +175,7 @@ challenge.request_validation
|
|
151
175
|
|
152
176
|
The validation is performed asynchronously and can take some time to be performed by the server.
|
153
177
|
|
154
|
-
You can poll until its status
|
178
|
+
You can poll until its status changes.
|
155
179
|
|
156
180
|
```ruby
|
157
181
|
while challenge.status == 'pending'
|
@@ -165,17 +189,36 @@ challenge.status # => 'valid'
|
|
165
189
|
|
166
190
|
Once all required authorizations have been validated through challenges, the order can be finalized using a CSR ([Certificate Signing Request](https://en.wikipedia.org/wiki/Certificate_signing_request)).
|
167
191
|
|
168
|
-
A CSR can be slightly tricky to generate using OpenSSL from Ruby standard library. `acme-client` provide a utility class `CertificateRequest` to help with that.
|
192
|
+
A CSR can be slightly tricky to generate using OpenSSL from Ruby standard library. `acme-client` provide a utility class `CertificateRequest` to help with that. You'll need to use a different private key for the certificate request than the one you use for your `Acme::Client` account.
|
169
193
|
|
170
194
|
Certificate generation happens asynchronously. You may need to poll.
|
171
195
|
|
172
196
|
```ruby
|
173
|
-
csr = Acme::Client::CertificateRequest.new(private_key:
|
197
|
+
csr = Acme::Client::CertificateRequest.new(private_key: a_different_private_key, subject: { common_name: 'example.com' })
|
174
198
|
order.finalize(csr: csr)
|
175
|
-
|
199
|
+
while order.status == 'processing'
|
200
|
+
sleep(1)
|
201
|
+
order.reload
|
202
|
+
end
|
176
203
|
order.certificate # => PEM-formatted certificate
|
177
204
|
```
|
178
205
|
|
206
|
+
### Ordering an alternative certificate
|
207
|
+
|
208
|
+
The provider may provide alternate certificate with different certificate chain. You can specify the required chain and the client will automatically download alternate certificate and match the chain by name.
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
begin
|
212
|
+
order.certificate(force_chain: 'DST Root CA X3')
|
213
|
+
rescue Acme::Client::Error::ForcedChainNotFound
|
214
|
+
order.certificate
|
215
|
+
end
|
216
|
+
```
|
217
|
+
|
218
|
+
Note: if the specified forced chain doesn't match an existing alternative certificate the method will raise an `Acme::Client::Error::ForcedChainNotFound` error.
|
219
|
+
|
220
|
+
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).
|
221
|
+
|
179
222
|
## Extra
|
180
223
|
|
181
224
|
### Certificate revokation
|
@@ -188,16 +231,22 @@ client.revoke(certificate: certificate)
|
|
188
231
|
|
189
232
|
### Certificate renewal
|
190
233
|
|
191
|
-
|
234
|
+
There is no renewal process, just create a new order.
|
235
|
+
|
192
236
|
|
237
|
+
### Account Key Roll-over
|
193
238
|
|
194
|
-
|
239
|
+
To change the key used for an account you can call `#account_key_change` with the new private key or jwk.
|
195
240
|
|
196
|
-
|
241
|
+
```ruby
|
242
|
+
require 'openssl'
|
243
|
+
new_private_key = OpenSSL::PKey::RSA.new(4096)
|
244
|
+
client.account_key_change(new_private_key: new_private_key)
|
245
|
+
```
|
197
246
|
|
198
247
|
## Requirements
|
199
248
|
|
200
|
-
Ruby >=
|
249
|
+
Ruby >= 3.0
|
201
250
|
|
202
251
|
## Development
|
203
252
|
|
@@ -214,4 +263,3 @@ Yes.
|
|
214
263
|
## License
|
215
264
|
|
216
265
|
[MIT License](http://opensource.org/licenses/MIT)
|
217
|
-
|
data/Rakefile
CHANGED
data/acme-client.gemspec
CHANGED
@@ -11,16 +11,17 @@ Gem::Specification.new do |spec|
|
|
11
11
|
spec.homepage = 'http://github.com/unixcharles/acme-client'
|
12
12
|
spec.license = 'MIT'
|
13
13
|
|
14
|
-
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
14
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) || f.start_with?('.') }
|
15
15
|
spec.require_paths = ['lib']
|
16
16
|
|
17
|
-
spec.required_ruby_version = '>= 2.
|
17
|
+
spec.required_ruby_version = '>= 2.3.0'
|
18
18
|
|
19
|
-
spec.add_development_dependency '
|
20
|
-
spec.add_development_dependency '
|
21
|
-
spec.add_development_dependency '
|
22
|
-
spec.add_development_dependency '
|
23
|
-
spec.add_development_dependency '
|
19
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
20
|
+
spec.add_development_dependency 'rspec', '~> 3.9'
|
21
|
+
spec.add_development_dependency 'vcr', '~> 2.9'
|
22
|
+
spec.add_development_dependency 'webmock', '~> 3.8'
|
23
|
+
spec.add_development_dependency 'webrick', '~> 1.7'
|
24
24
|
|
25
|
-
spec.add_runtime_dependency 'faraday', '
|
25
|
+
spec.add_runtime_dependency 'faraday', '>= 1.0', '< 3.0.0'
|
26
|
+
spec.add_runtime_dependency 'faraday-retry', '>= 1.0', '< 3.0.0'
|
26
27
|
end
|
data/bin/release
CHANGED
@@ -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,10 +7,13 @@ 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
|
13
|
+
class AlreadyRevoked < ServerError; end
|
12
14
|
class BadCSR < ServerError; end
|
13
15
|
class BadNonce < ServerError; end
|
16
|
+
class BadPublicKey < ServerError; end
|
14
17
|
class BadSignatureAlgorithm < ServerError; end
|
15
18
|
class InvalidContact < ServerError; end
|
16
19
|
class UnsupportedContact < ServerError; end
|
@@ -31,8 +34,10 @@ class Acme::Client::Error < StandardError
|
|
31
34
|
class IncorrectResponse < ServerError; end
|
32
35
|
|
33
36
|
ACME_ERRORS = {
|
37
|
+
'urn:ietf:params:acme:error:alreadyRevoked' => AlreadyRevoked,
|
34
38
|
'urn:ietf:params:acme:error:badCSR' => BadCSR,
|
35
39
|
'urn:ietf:params:acme:error:badNonce' => BadNonce,
|
40
|
+
'urn:ietf:params:acme:error:badPublicKey' => BadPublicKey,
|
36
41
|
'urn:ietf:params:acme:error:badSignatureAlgorithm' => BadSignatureAlgorithm,
|
37
42
|
'urn:ietf:params:acme:error:invalidContact' => InvalidContact,
|
38
43
|
'urn:ietf:params:acme:error:unsupportedContact' => UnsupportedContact,
|
@@ -0,0 +1,162 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Acme::Client::HTTPClient
|
4
|
+
# Creates and returns a new HTTP client, with default settings.
|
5
|
+
#
|
6
|
+
# @param url [URI:HTTPS]
|
7
|
+
# @param options [Hash]
|
8
|
+
# @return [Faraday::Connection]
|
9
|
+
def self.new_connection(url:, options: {})
|
10
|
+
Faraday.new(url, options) do |configuration|
|
11
|
+
configuration.use Acme::Client::HTTPClient::ErrorMiddleware
|
12
|
+
|
13
|
+
yield(configuration) if block_given?
|
14
|
+
|
15
|
+
configuration.headers[:user_agent] = Acme::Client::USER_AGENT
|
16
|
+
configuration.adapter Faraday.default_adapter
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Creates and returns a new HTTP client designed for the Acme-protocol, with default settings.
|
21
|
+
#
|
22
|
+
# @param url [URI:HTTPS]
|
23
|
+
# @param client [Acme::Client]
|
24
|
+
# @param mode [Symbol]
|
25
|
+
# @param options [Hash]
|
26
|
+
# @param bad_nonce_retry [Integer]
|
27
|
+
# @return [Faraday::Connection]
|
28
|
+
def self.new_acme_connection(url:, client:, mode:, options: {}, bad_nonce_retry: 0)
|
29
|
+
new_connection(url: url, options: options) do |configuration|
|
30
|
+
if bad_nonce_retry > 0
|
31
|
+
configuration.request(:retry,
|
32
|
+
max: bad_nonce_retry,
|
33
|
+
methods: Faraday::Connection::METHODS,
|
34
|
+
exceptions: [Acme::Client::Error::BadNonce])
|
35
|
+
end
|
36
|
+
|
37
|
+
configuration.use Acme::Client::HTTPClient::AcmeMiddleware, client: client, mode: mode
|
38
|
+
|
39
|
+
yield(configuration) if block_given?
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# ErrorMiddleware ensures the HTTP Client would not raise exceptions outside the Acme namespace.
|
44
|
+
#
|
45
|
+
# Exceptions are rescued and re-packaged as Acme exceptions.
|
46
|
+
class ErrorMiddleware < Faraday::Middleware
|
47
|
+
# Implements the Rack-alike Faraday::Middleware interface.
|
48
|
+
def call(env)
|
49
|
+
@app.call(env)
|
50
|
+
rescue Faraday::TimeoutError, Faraday::ConnectionFailed
|
51
|
+
raise Acme::Client::Error::Timeout
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# AcmeMiddleware implements the Acme-protocol requirements for JWK requests.
|
56
|
+
class AcmeMiddleware < Faraday::Middleware
|
57
|
+
attr_reader :env, :response, :client
|
58
|
+
|
59
|
+
CONTENT_TYPE = 'application/jose+json'
|
60
|
+
|
61
|
+
def initialize(app, options)
|
62
|
+
super(app)
|
63
|
+
@client = options.fetch(:client)
|
64
|
+
@mode = options.fetch(:mode)
|
65
|
+
end
|
66
|
+
|
67
|
+
def call(env)
|
68
|
+
@env = env
|
69
|
+
@env[:request_headers]['Content-Type'] = CONTENT_TYPE
|
70
|
+
|
71
|
+
if @env.method != :get
|
72
|
+
@env.body = client.jwk.jws(header: jws_header, payload: env.body)
|
73
|
+
end
|
74
|
+
|
75
|
+
@app.call(env).on_complete { |response_env| on_complete(response_env) }
|
76
|
+
end
|
77
|
+
|
78
|
+
def on_complete(env)
|
79
|
+
@env = env
|
80
|
+
|
81
|
+
raise_on_not_found!
|
82
|
+
store_nonce
|
83
|
+
env.body = decode_body
|
84
|
+
env.response_headers['Link'] = decode_link_headers
|
85
|
+
|
86
|
+
return if env.success?
|
87
|
+
|
88
|
+
raise_on_error!
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def jws_header
|
94
|
+
headers = { nonce: pop_nonce, url: env.url.to_s }
|
95
|
+
headers[:kid] = client.kid if @mode == :kid
|
96
|
+
headers
|
97
|
+
end
|
98
|
+
|
99
|
+
def raise_on_not_found!
|
100
|
+
raise Acme::Client::Error::NotFound, env.url.to_s if env.status == 404
|
101
|
+
end
|
102
|
+
|
103
|
+
def raise_on_error!
|
104
|
+
raise error_class, error_message
|
105
|
+
end
|
106
|
+
|
107
|
+
def error_message
|
108
|
+
if env.body.is_a? Hash
|
109
|
+
env.body['detail']
|
110
|
+
else
|
111
|
+
"Error message: #{env.body}"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def error_class
|
116
|
+
Acme::Client::Error::ACME_ERRORS.fetch(error_name, Acme::Client::Error)
|
117
|
+
end
|
118
|
+
|
119
|
+
def error_name
|
120
|
+
return unless env.body.is_a?(Hash)
|
121
|
+
return unless env.body.key?('type')
|
122
|
+
env.body['type']
|
123
|
+
end
|
124
|
+
|
125
|
+
def decode_body
|
126
|
+
content_type = env.response_headers['Content-Type'].to_s
|
127
|
+
|
128
|
+
if content_type.start_with?('application/json', 'application/problem+json')
|
129
|
+
JSON.load(env.body)
|
130
|
+
else
|
131
|
+
env.body
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def decode_link_headers
|
136
|
+
return unless env.response_headers.key?('Link')
|
137
|
+
link_header = env.response_headers['Link']
|
138
|
+
Acme::Client::Util.decode_link_headers(link_header)
|
139
|
+
end
|
140
|
+
|
141
|
+
def store_nonce
|
142
|
+
nonce = env.response_headers['replay-nonce']
|
143
|
+
nonces << nonce if nonce
|
144
|
+
end
|
145
|
+
|
146
|
+
def pop_nonce
|
147
|
+
if nonces.empty?
|
148
|
+
get_nonce
|
149
|
+
end
|
150
|
+
|
151
|
+
nonces.pop
|
152
|
+
end
|
153
|
+
|
154
|
+
def get_nonce
|
155
|
+
client.get_nonce
|
156
|
+
end
|
157
|
+
|
158
|
+
def nonces
|
159
|
+
client.nonces
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
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)
|
@@ -50,8 +50,8 @@ class Acme::Client::JWK::ECDSA < Acme::Client::JWK::Base
|
|
50
50
|
{
|
51
51
|
crv: @curve_params[:jwa_crv],
|
52
52
|
kty: 'EC',
|
53
|
-
x: Acme::Client::Util.urlsafe_base64(coordinates[:x]
|
54
|
-
y: Acme::Client::Util.urlsafe_base64(coordinates[:y]
|
53
|
+
x: Acme::Client::Util.urlsafe_base64(coordinates[:x]),
|
54
|
+
y: Acme::Client::Util.urlsafe_base64(coordinates[:y])
|
55
55
|
}
|
56
56
|
end
|
57
57
|
|
@@ -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
|
@@ -90,8 +92,8 @@ class Acme::Client::JWK::ECDSA < Acme::Client::JWK::Base
|
|
90
92
|
hex_y = hex[2 + data_len / 2, data_len / 2]
|
91
93
|
|
92
94
|
{
|
93
|
-
x:
|
94
|
-
y:
|
95
|
+
x: [hex_x].pack('H*'),
|
96
|
+
y: [hex_y].pack('H*')
|
95
97
|
}
|
96
98
|
end
|
97
99
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Acme::Client::JWK::HMAC < Acme::Client::JWK::Base
|
4
|
+
# Instantiate a new HMAC JWS.
|
5
|
+
#
|
6
|
+
# key - A string.
|
7
|
+
#
|
8
|
+
# Returns nothing.
|
9
|
+
def initialize(key)
|
10
|
+
@key = key
|
11
|
+
end
|
12
|
+
|
13
|
+
# Sign a message with the private key.
|
14
|
+
#
|
15
|
+
# message - A String message to sign.
|
16
|
+
#
|
17
|
+
# Returns a String signature.
|
18
|
+
def sign(message)
|
19
|
+
OpenSSL::HMAC.digest('SHA256', @key, message)
|
20
|
+
end
|
21
|
+
|
22
|
+
# The name of the algorithm as needed for the `alg` member of a JWS object.
|
23
|
+
#
|
24
|
+
# Returns a String.
|
25
|
+
def jwa_alg
|
26
|
+
# https://tools.ietf.org/html/rfc7518#section-3.1
|
27
|
+
# HMAC using SHA-256
|
28
|
+
'HS256'
|
29
|
+
end
|
30
|
+
end
|
data/lib/acme/client/jwk.rb
CHANGED