acme-client 2.0.0 → 2.0.18
Sign up to get free protection for your applications and to get access to all the features.
- 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