acme-client 2.0.11 → 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 +31 -0
- data/Gemfile +0 -1
- data/README.md +13 -9
- data/Rakefile +1 -4
- data/acme-client.gemspec +3 -4
- data/bin/generate_keystash +9 -0
- data/lib/acme/client/http_client.rb +162 -0
- data/lib/acme/client/jwk/ecdsa.rb +4 -4
- data/lib/acme/client/jwk/hmac.rb +30 -0
- data/lib/acme/client/jwk.rb +1 -0
- data/lib/acme/client/resources/authorization.rb +1 -1
- data/lib/acme/client/resources/directory.rb +9 -28
- data/lib/acme/client/util.rb +2 -2
- data/lib/acme/client/version.rb +1 -1
- data/lib/acme/client.rb +45 -43
- metadata +18 -29
- data/.github/workflows/rubocop.yml +0 -23
- data/.github/workflows/test.yml +0 -26
- data/.gitignore +0 -12
- data/.rspec +0 -3
- data/.rubocop.yml +0 -134
- data/lib/acme/client/faraday_middleware.rb +0 -111
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,34 @@
|
|
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
|
+
|
1
32
|
## `2.0.11`
|
2
33
|
|
3
34
|
* Add support for error code `AlreadyRevoked` and `BadPublicKey`
|
data/Gemfile
CHANGED
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 / [RFC 8555](https://tools.ietf.org/html/rfc8555) 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:
|
@@ -108,6 +104,15 @@ client.kid
|
|
108
104
|
=> "https://acme-staging-v02.api.letsencrypt.org/acme/acct/000000"
|
109
105
|
```
|
110
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
|
+
|
111
116
|
## Obtaining a certificate
|
112
117
|
### Ordering a certificate
|
113
118
|
|
@@ -200,8 +205,7 @@ order.certificate # => PEM-formatted certificate
|
|
200
205
|
|
201
206
|
### Ordering an alternative certificate
|
202
207
|
|
203
|
-
|
204
|
-
For example, to download the cross-signed certificate after January 11, 2021, call `Order#certificate` as follows:
|
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.
|
205
209
|
|
206
210
|
```ruby
|
207
211
|
begin
|
@@ -237,12 +241,12 @@ To change the key used for an account you can call `#account_key_change` with th
|
|
237
241
|
```ruby
|
238
242
|
require 'openssl'
|
239
243
|
new_private_key = OpenSSL::PKey::RSA.new(4096)
|
240
|
-
client.account_key_change(
|
244
|
+
client.account_key_change(new_private_key: new_private_key)
|
241
245
|
```
|
242
246
|
|
243
247
|
## Requirements
|
244
248
|
|
245
|
-
Ruby >=
|
249
|
+
Ruby >= 3.0
|
246
250
|
|
247
251
|
## Development
|
248
252
|
|
data/Rakefile
CHANGED
data/acme-client.gemspec
CHANGED
@@ -11,18 +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
17
|
spec.required_ruby_version = '>= 2.3.0'
|
18
18
|
|
19
|
-
spec.add_development_dependency 'bundler', '>= 1.17.3'
|
20
19
|
spec.add_development_dependency 'rake', '~> 13.0'
|
21
20
|
spec.add_development_dependency 'rspec', '~> 3.9'
|
22
21
|
spec.add_development_dependency 'vcr', '~> 2.9'
|
23
22
|
spec.add_development_dependency 'webmock', '~> 3.8'
|
24
|
-
spec.add_development_dependency 'webrick'
|
23
|
+
spec.add_development_dependency 'webrick', '~> 1.7'
|
25
24
|
|
26
25
|
spec.add_runtime_dependency 'faraday', '>= 1.0', '< 3.0.0'
|
27
|
-
spec.add_runtime_dependency 'faraday-retry', '
|
26
|
+
spec.add_runtime_dependency 'faraday-retry', '>= 1.0', '< 3.0.0'
|
28
27
|
end
|
@@ -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
|
@@ -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
|
|
@@ -92,8 +92,8 @@ class Acme::Client::JWK::ECDSA < Acme::Client::JWK::Base
|
|
92
92
|
hex_y = hex[2 + data_len / 2, data_len / 2]
|
93
93
|
|
94
94
|
{
|
95
|
-
x:
|
96
|
-
y:
|
95
|
+
x: [hex_x].pack('H*'),
|
96
|
+
y: [hex_y].pack('H*')
|
97
97
|
}
|
98
98
|
end
|
99
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
@@ -56,7 +56,7 @@ class Acme::Client::Resources::Authorization
|
|
56
56
|
type: attributes.fetch('type'),
|
57
57
|
status: attributes.fetch('status'),
|
58
58
|
url: attributes.fetch('url'),
|
59
|
-
token: attributes.fetch('token'),
|
59
|
+
token: attributes.fetch('token', nil),
|
60
60
|
error: attributes['error']
|
61
61
|
}
|
62
62
|
Acme::Client::Resources::Challenges.new(@client, **arguments)
|
@@ -17,12 +17,13 @@ class Acme::Client::Resources::Directory
|
|
17
17
|
external_account_required: 'externalAccountRequired'
|
18
18
|
}
|
19
19
|
|
20
|
-
def initialize(
|
21
|
-
@
|
20
|
+
def initialize(client, **arguments)
|
21
|
+
@client = client
|
22
|
+
assign_attributes(**arguments)
|
22
23
|
end
|
23
24
|
|
24
25
|
def endpoint_for(key)
|
25
|
-
directory.fetch(key) do |missing_key|
|
26
|
+
@directory.fetch(key) do |missing_key|
|
26
27
|
raise Acme::Client::Error::UnsupportedOperation,
|
27
28
|
"Directory at #{@url} does not include `#{missing_key}`"
|
28
29
|
end
|
@@ -45,36 +46,16 @@ class Acme::Client::Resources::Directory
|
|
45
46
|
end
|
46
47
|
|
47
48
|
def meta
|
48
|
-
directory[:meta]
|
49
|
+
@directory[:meta]
|
49
50
|
end
|
50
51
|
|
51
52
|
private
|
52
53
|
|
53
|
-
def directory
|
54
|
-
@directory
|
55
|
-
|
56
|
-
|
57
|
-
def load_directory
|
58
|
-
body = fetch_directory
|
59
|
-
result = {}
|
60
|
-
result[:meta] = body.delete('meta')
|
54
|
+
def assign_attributes(directory:)
|
55
|
+
@directory = {}
|
56
|
+
@directory[:meta] = directory.delete('meta')
|
61
57
|
DIRECTORY_RESOURCES.each do |key, entry|
|
62
|
-
|
63
|
-
end
|
64
|
-
result
|
65
|
-
rescue JSON::ParserError => exception
|
66
|
-
raise Acme::Client::Error::InvalidDirectory,
|
67
|
-
"Invalid directory url\n#{@directory} did not return a valid directory\n#{exception.inspect}"
|
68
|
-
end
|
69
|
-
|
70
|
-
def fetch_directory
|
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
|
58
|
+
@directory[key] = URI(directory[entry]) if directory[entry]
|
75
59
|
end
|
76
|
-
connection.headers[:user_agent] = Acme::Client::USER_AGENT
|
77
|
-
response = connection.get(@url)
|
78
|
-
response.body
|
79
60
|
end
|
80
61
|
end
|
data/lib/acme/client/util.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
module Acme::Client::Util
|
2
|
+
extend self
|
3
|
+
|
2
4
|
def urlsafe_base64(data)
|
3
5
|
Base64.urlsafe_encode64(data).sub(/[\s=]*\z/, '')
|
4
6
|
end
|
@@ -30,6 +32,4 @@ module Acme::Client::Util
|
|
30
32
|
raise ArgumentError, 'priv must be EC or RSA'
|
31
33
|
end
|
32
34
|
end
|
33
|
-
|
34
|
-
extend self
|
35
35
|
end
|
data/lib/acme/client/version.rb
CHANGED
data/lib/acme/client.rb
CHANGED
@@ -14,10 +14,10 @@ module Acme; end
|
|
14
14
|
class Acme::Client; end
|
15
15
|
|
16
16
|
require 'acme/client/version'
|
17
|
+
require 'acme/client/http_client'
|
17
18
|
require 'acme/client/certificate_request'
|
18
19
|
require 'acme/client/self_sign_certificate'
|
19
20
|
require 'acme/client/resources'
|
20
|
-
require 'acme/client/faraday_middleware'
|
21
21
|
require 'acme/client/jwk'
|
22
22
|
require 'acme/client/error'
|
23
23
|
require 'acme/client/util'
|
@@ -44,13 +44,14 @@ class Acme::Client
|
|
44
44
|
|
45
45
|
@kid, @connection_options = kid, connection_options
|
46
46
|
@bad_nonce_retry = bad_nonce_retry
|
47
|
-
@
|
47
|
+
@directory_url = URI(directory)
|
48
48
|
@nonces ||= []
|
49
49
|
end
|
50
50
|
|
51
51
|
attr_reader :jwk, :nonces
|
52
52
|
|
53
|
-
def new_account(contact:, terms_of_service_agreed: nil)
|
53
|
+
def new_account(contact:, terms_of_service_agreed: nil, external_account_binding: nil)
|
54
|
+
new_account_endpoint = endpoint_for(:new_account)
|
54
55
|
payload = {
|
55
56
|
contact: Array(contact)
|
56
57
|
}
|
@@ -59,7 +60,18 @@ class Acme::Client
|
|
59
60
|
payload[:termsOfServiceAgreed] = terms_of_service_agreed
|
60
61
|
end
|
61
62
|
|
62
|
-
|
63
|
+
if external_account_binding
|
64
|
+
kid, hmac_key = external_account_binding.values_at(:kid, :hmac_key)
|
65
|
+
if kid.nil? || hmac_key.nil?
|
66
|
+
raise ArgumentError, 'must specify kid and hmac_key key for external_account_binding'
|
67
|
+
end
|
68
|
+
|
69
|
+
hmac = Acme::Client::JWK::HMAC.new(Base64.urlsafe_decode64(hmac_key))
|
70
|
+
external_account_payload = hmac.jws(header: { kid: kid, url: new_account_endpoint }, payload: @jwk)
|
71
|
+
payload[:externalAccountBinding] = JSON.parse(external_account_payload)
|
72
|
+
end
|
73
|
+
|
74
|
+
response = post(new_account_endpoint, payload: payload, mode: :jws)
|
63
75
|
@kid = response.headers.fetch(:location)
|
64
76
|
|
65
77
|
if response.body.nil? || response.body.empty?
|
@@ -211,34 +223,50 @@ class Acme::Client
|
|
211
223
|
end
|
212
224
|
|
213
225
|
def get_nonce
|
214
|
-
|
215
|
-
response =
|
226
|
+
http_client = Acme::Client::HTTPClient.new_connection(url: endpoint_for(:new_nonce), options: @connection_options)
|
227
|
+
response = http_client.head(nil, nil)
|
216
228
|
nonces << response.headers['replay-nonce']
|
217
229
|
true
|
218
230
|
end
|
219
231
|
|
232
|
+
def directory
|
233
|
+
@directory ||= load_directory
|
234
|
+
end
|
235
|
+
|
220
236
|
def meta
|
221
|
-
|
237
|
+
directory.meta
|
222
238
|
end
|
223
239
|
|
224
240
|
def terms_of_service
|
225
|
-
|
241
|
+
directory.terms_of_service
|
226
242
|
end
|
227
243
|
|
228
244
|
def website
|
229
|
-
|
245
|
+
directory.website
|
230
246
|
end
|
231
247
|
|
232
248
|
def caa_identities
|
233
|
-
|
249
|
+
directory.caa_identities
|
234
250
|
end
|
235
251
|
|
236
252
|
def external_account_required
|
237
|
-
|
253
|
+
directory.external_account_required
|
238
254
|
end
|
239
255
|
|
240
256
|
private
|
241
257
|
|
258
|
+
def load_directory
|
259
|
+
Acme::Client::Resources::Directory.new(self, directory: fetch_directory)
|
260
|
+
end
|
261
|
+
|
262
|
+
def fetch_directory
|
263
|
+
response = get(@directory_url)
|
264
|
+
response.body
|
265
|
+
rescue JSON::ParserError => exception
|
266
|
+
raise Acme::Client::Error::InvalidDirectory,
|
267
|
+
"Invalid directory url\n#{@directory_url} did not return a valid directory\n#{exception.inspect}"
|
268
|
+
end
|
269
|
+
|
242
270
|
def prepare_order_identifiers(identifiers)
|
243
271
|
if identifiers.is_a?(Hash)
|
244
272
|
[identifiers]
|
@@ -304,7 +332,7 @@ class Acme::Client
|
|
304
332
|
connection.post(url, nil)
|
305
333
|
end
|
306
334
|
|
307
|
-
def get(url, mode: :
|
335
|
+
def get(url, mode: :get)
|
308
336
|
connection = connection_for(url: url, mode: mode)
|
309
337
|
connection.get(url)
|
310
338
|
end
|
@@ -320,41 +348,15 @@ class Acme::Client
|
|
320
348
|
def connection_for(url:, mode:)
|
321
349
|
uri = URI(url)
|
322
350
|
endpoint = "#{uri.scheme}://#{uri.hostname}:#{uri.port}"
|
351
|
+
|
323
352
|
@connections ||= {}
|
324
353
|
@connections[mode] ||= {}
|
325
|
-
@connections[mode][endpoint] ||= new_acme_connection(
|
326
|
-
|
327
|
-
|
328
|
-
def new_acme_connection(endpoint:, mode:)
|
329
|
-
new_connection(endpoint: endpoint) do |configuration|
|
330
|
-
configuration.use Acme::Client::FaradayMiddleware, client: self, mode: mode
|
331
|
-
end
|
332
|
-
end
|
333
|
-
|
334
|
-
def new_connection(endpoint:)
|
335
|
-
Faraday.new(endpoint, **@connection_options) do |configuration|
|
336
|
-
if @bad_nonce_retry > 0
|
337
|
-
configuration.request(:retry,
|
338
|
-
max: @bad_nonce_retry,
|
339
|
-
methods: Faraday::Connection::METHODS,
|
340
|
-
exceptions: [Acme::Client::Error::BadNonce])
|
341
|
-
end
|
342
|
-
yield(configuration) if block_given?
|
343
|
-
configuration.adapter Faraday.default_adapter
|
344
|
-
end
|
345
|
-
end
|
346
|
-
|
347
|
-
def fetch_chain(response, limit = 10)
|
348
|
-
links = response.headers['link']
|
349
|
-
if limit.zero? || links.nil? || links['up'].nil?
|
350
|
-
[]
|
351
|
-
else
|
352
|
-
issuer = get(links['up'])
|
353
|
-
[OpenSSL::X509::Certificate.new(issuer.body), *fetch_chain(issuer, limit - 1)]
|
354
|
-
end
|
354
|
+
@connections[mode][endpoint] ||= Acme::Client::HTTPClient.new_acme_connection(
|
355
|
+
url: URI(endpoint), mode: mode, client: self, options: @connection_options, bad_nonce_retry: @bad_nonce_retry
|
356
|
+
)
|
355
357
|
end
|
356
358
|
|
357
359
|
def endpoint_for(key)
|
358
|
-
|
360
|
+
directory.endpoint_for(key)
|
359
361
|
end
|
360
362
|
end
|
metadata
CHANGED
@@ -1,29 +1,15 @@
|
|
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.18
|
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: 2024-06-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: bundler
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - ">="
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: 1.17.3
|
20
|
-
type: :development
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - ">="
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: 1.17.3
|
27
13
|
- !ruby/object:Gem::Dependency
|
28
14
|
name: rake
|
29
15
|
requirement: !ruby/object:Gem::Requirement
|
@@ -84,16 +70,16 @@ dependencies:
|
|
84
70
|
name: webrick
|
85
71
|
requirement: !ruby/object:Gem::Requirement
|
86
72
|
requirements:
|
87
|
-
- - "
|
73
|
+
- - "~>"
|
88
74
|
- !ruby/object:Gem::Version
|
89
|
-
version: '
|
75
|
+
version: '1.7'
|
90
76
|
type: :development
|
91
77
|
prerelease: false
|
92
78
|
version_requirements: !ruby/object:Gem::Requirement
|
93
79
|
requirements:
|
94
|
-
- - "
|
80
|
+
- - "~>"
|
95
81
|
- !ruby/object:Gem::Version
|
96
|
-
version: '
|
82
|
+
version: '1.7'
|
97
83
|
- !ruby/object:Gem::Dependency
|
98
84
|
name: faraday
|
99
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -118,16 +104,22 @@ dependencies:
|
|
118
104
|
name: faraday-retry
|
119
105
|
requirement: !ruby/object:Gem::Requirement
|
120
106
|
requirements:
|
121
|
-
- - "
|
107
|
+
- - ">="
|
122
108
|
- !ruby/object:Gem::Version
|
123
109
|
version: '1.0'
|
110
|
+
- - "<"
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: 3.0.0
|
124
113
|
type: :runtime
|
125
114
|
prerelease: false
|
126
115
|
version_requirements: !ruby/object:Gem::Requirement
|
127
116
|
requirements:
|
128
|
-
- - "
|
117
|
+
- - ">="
|
129
118
|
- !ruby/object:Gem::Version
|
130
119
|
version: '1.0'
|
120
|
+
- - "<"
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: 3.0.0
|
131
123
|
description:
|
132
124
|
email:
|
133
125
|
- unixcharles@gmail.com
|
@@ -135,11 +127,6 @@ executables: []
|
|
135
127
|
extensions: []
|
136
128
|
extra_rdoc_files: []
|
137
129
|
files:
|
138
|
-
- ".github/workflows/rubocop.yml"
|
139
|
-
- ".github/workflows/test.yml"
|
140
|
-
- ".gitignore"
|
141
|
-
- ".rspec"
|
142
|
-
- ".rubocop.yml"
|
143
130
|
- CHANGELOG.md
|
144
131
|
- Gemfile
|
145
132
|
- LICENSE.txt
|
@@ -147,6 +134,7 @@ files:
|
|
147
134
|
- Rakefile
|
148
135
|
- acme-client.gemspec
|
149
136
|
- bin/console
|
137
|
+
- bin/generate_keystash
|
150
138
|
- bin/release
|
151
139
|
- bin/setup
|
152
140
|
- lib/acme-client.rb
|
@@ -155,10 +143,11 @@ files:
|
|
155
143
|
- lib/acme/client/certificate_request/ec_key_patch.rb
|
156
144
|
- lib/acme/client/chain_identifier.rb
|
157
145
|
- lib/acme/client/error.rb
|
158
|
-
- lib/acme/client/
|
146
|
+
- lib/acme/client/http_client.rb
|
159
147
|
- lib/acme/client/jwk.rb
|
160
148
|
- lib/acme/client/jwk/base.rb
|
161
149
|
- lib/acme/client/jwk/ecdsa.rb
|
150
|
+
- lib/acme/client/jwk/hmac.rb
|
162
151
|
- lib/acme/client/jwk/rsa.rb
|
163
152
|
- lib/acme/client/resources.rb
|
164
153
|
- lib/acme/client/resources/account.rb
|
@@ -192,7 +181,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
192
181
|
- !ruby/object:Gem::Version
|
193
182
|
version: '0'
|
194
183
|
requirements: []
|
195
|
-
rubygems_version: 3.
|
184
|
+
rubygems_version: 3.4.20
|
196
185
|
signing_key:
|
197
186
|
specification_version: 4
|
198
187
|
summary: Client for the ACME protocol.
|
@@ -1,23 +0,0 @@
|
|
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
|
data/.github/workflows/test.yml
DELETED
@@ -1,26 +0,0 @@
|
|
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', truffleruby-head]
|
15
|
-
faraday-version: ['~> 1.7', '~> 2.0']
|
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/.gitignore
DELETED
data/.rspec
DELETED
data/.rubocop.yml
DELETED
@@ -1,134 +0,0 @@
|
|
1
|
-
AllCops:
|
2
|
-
TargetRubyVersion: 2.1
|
3
|
-
Exclude:
|
4
|
-
- 'bin/*'
|
5
|
-
- 'vendor/**/*'
|
6
|
-
|
7
|
-
Rails:
|
8
|
-
Enabled: false
|
9
|
-
|
10
|
-
Layout/AlignParameters:
|
11
|
-
EnforcedStyle: with_fixed_indentation
|
12
|
-
|
13
|
-
Layout/ElseAlignment:
|
14
|
-
Enabled: false
|
15
|
-
|
16
|
-
Layout/FirstParameterIndentation:
|
17
|
-
EnforcedStyle: consistent
|
18
|
-
|
19
|
-
Layout/IndentationWidth:
|
20
|
-
Enabled: false
|
21
|
-
|
22
|
-
Layout/MultilineOperationIndentation:
|
23
|
-
Enabled: false
|
24
|
-
|
25
|
-
Layout/SpaceInsideBlockBraces:
|
26
|
-
Enabled: false
|
27
|
-
|
28
|
-
Lint/AmbiguousOperator:
|
29
|
-
Enabled: false
|
30
|
-
|
31
|
-
Lint/AssignmentInCondition:
|
32
|
-
Enabled: false
|
33
|
-
|
34
|
-
Lint/EndAlignment:
|
35
|
-
Enabled: false
|
36
|
-
|
37
|
-
Lint/UnusedMethodArgument:
|
38
|
-
AllowUnusedKeywordArguments: true
|
39
|
-
|
40
|
-
Metrics/AbcSize:
|
41
|
-
Enabled: false
|
42
|
-
|
43
|
-
Metrics/BlockLength:
|
44
|
-
Enabled: false
|
45
|
-
|
46
|
-
Metrics/ClassLength:
|
47
|
-
Enabled: false
|
48
|
-
|
49
|
-
Metrics/CyclomaticComplexity:
|
50
|
-
Enabled: false
|
51
|
-
|
52
|
-
Metrics/LineLength:
|
53
|
-
Max: 140
|
54
|
-
|
55
|
-
Metrics/MethodLength:
|
56
|
-
Max: 15
|
57
|
-
Enabled: false
|
58
|
-
|
59
|
-
Metrics/ParameterLists:
|
60
|
-
Max: 5
|
61
|
-
CountKeywordArgs: false
|
62
|
-
|
63
|
-
Metrics/PerceivedComplexity:
|
64
|
-
Enabled: false
|
65
|
-
|
66
|
-
Security/JSONLoad:
|
67
|
-
Enabled: false
|
68
|
-
|
69
|
-
Style/AccessorMethodName:
|
70
|
-
Enabled: false
|
71
|
-
|
72
|
-
Style/Alias:
|
73
|
-
Enabled: false
|
74
|
-
|
75
|
-
Style/BlockDelimiters:
|
76
|
-
EnforcedStyle: semantic
|
77
|
-
|
78
|
-
Style/ClassAndModuleChildren:
|
79
|
-
Enabled: false
|
80
|
-
|
81
|
-
Style/Documentation:
|
82
|
-
Enabled: false
|
83
|
-
|
84
|
-
Style/DoubleNegation:
|
85
|
-
Enabled: false
|
86
|
-
|
87
|
-
Style/FileName:
|
88
|
-
Exclude:
|
89
|
-
- 'lib/acme-client.rb'
|
90
|
-
|
91
|
-
Style/GlobalVars:
|
92
|
-
Enabled: false
|
93
|
-
|
94
|
-
Style/GuardClause:
|
95
|
-
Enabled: false
|
96
|
-
|
97
|
-
Style/IfUnlessModifier:
|
98
|
-
Enabled: false
|
99
|
-
|
100
|
-
Style/Lambda:
|
101
|
-
Enabled: false
|
102
|
-
|
103
|
-
Style/ModuleFunction:
|
104
|
-
Enabled: false
|
105
|
-
|
106
|
-
Style/MultilineBlockChain:
|
107
|
-
Enabled: false
|
108
|
-
|
109
|
-
Style/MultipleComparison:
|
110
|
-
Enabled: false
|
111
|
-
|
112
|
-
Style/MutableConstant:
|
113
|
-
Enabled: false
|
114
|
-
|
115
|
-
Style/ParallelAssignment:
|
116
|
-
Enabled: false
|
117
|
-
|
118
|
-
Style/PercentLiteralDelimiters:
|
119
|
-
Enabled: false
|
120
|
-
|
121
|
-
Style/SignalException:
|
122
|
-
EnforcedStyle: only_raise
|
123
|
-
|
124
|
-
Style/SymbolArray:
|
125
|
-
Enabled: false
|
126
|
-
|
127
|
-
Style/StringLiterals:
|
128
|
-
Enabled: single_quotes
|
129
|
-
|
130
|
-
Style/TrailingCommaInArguments:
|
131
|
-
Enabled: false
|
132
|
-
|
133
|
-
Style/TrivialAccessors:
|
134
|
-
AllowPredicates: true
|
@@ -1,111 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
class Acme::Client::FaradayMiddleware < Faraday::Middleware
|
4
|
-
attr_reader :env, :response, :client
|
5
|
-
|
6
|
-
CONTENT_TYPE = 'application/jose+json'
|
7
|
-
|
8
|
-
def initialize(app, options)
|
9
|
-
super(app)
|
10
|
-
@client = options.fetch(:client)
|
11
|
-
@mode = options.fetch(:mode)
|
12
|
-
end
|
13
|
-
|
14
|
-
def call(env)
|
15
|
-
@env = env
|
16
|
-
@env[:request_headers]['User-Agent'] = Acme::Client::USER_AGENT
|
17
|
-
@env[:request_headers]['Content-Type'] = CONTENT_TYPE
|
18
|
-
|
19
|
-
if @env.method != :get
|
20
|
-
@env.body = client.jwk.jws(header: jws_header, payload: env.body)
|
21
|
-
end
|
22
|
-
|
23
|
-
@app.call(env).on_complete { |response_env| on_complete(response_env) }
|
24
|
-
rescue Faraday::TimeoutError, Faraday::ConnectionFailed
|
25
|
-
raise Acme::Client::Error::Timeout
|
26
|
-
end
|
27
|
-
|
28
|
-
def on_complete(env)
|
29
|
-
@env = env
|
30
|
-
|
31
|
-
raise_on_not_found!
|
32
|
-
store_nonce
|
33
|
-
env.body = decode_body
|
34
|
-
env.response_headers['Link'] = decode_link_headers
|
35
|
-
|
36
|
-
return if env.success?
|
37
|
-
|
38
|
-
raise_on_error!
|
39
|
-
end
|
40
|
-
|
41
|
-
private
|
42
|
-
|
43
|
-
def jws_header
|
44
|
-
headers = { nonce: pop_nonce, url: env.url.to_s }
|
45
|
-
headers[:kid] = client.kid if @mode == :kid
|
46
|
-
headers
|
47
|
-
end
|
48
|
-
|
49
|
-
def raise_on_not_found!
|
50
|
-
raise Acme::Client::Error::NotFound, env.url.to_s if env.status == 404
|
51
|
-
end
|
52
|
-
|
53
|
-
def raise_on_error!
|
54
|
-
raise error_class, error_message
|
55
|
-
end
|
56
|
-
|
57
|
-
def error_message
|
58
|
-
if env.body.is_a? Hash
|
59
|
-
env.body['detail']
|
60
|
-
else
|
61
|
-
"Error message: #{env.body}"
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
def error_class
|
66
|
-
Acme::Client::Error::ACME_ERRORS.fetch(error_name, Acme::Client::Error)
|
67
|
-
end
|
68
|
-
|
69
|
-
def error_name
|
70
|
-
return unless env.body.is_a?(Hash)
|
71
|
-
return unless env.body.key?('type')
|
72
|
-
env.body['type']
|
73
|
-
end
|
74
|
-
|
75
|
-
def decode_body
|
76
|
-
content_type = env.response_headers['Content-Type'].to_s
|
77
|
-
|
78
|
-
if content_type.start_with?('application/json', 'application/problem+json')
|
79
|
-
JSON.load(env.body)
|
80
|
-
else
|
81
|
-
env.body
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
def decode_link_headers
|
86
|
-
return unless env.response_headers.key?('Link')
|
87
|
-
link_header = env.response_headers['Link']
|
88
|
-
Acme::Client::Util.decode_link_headers(link_header)
|
89
|
-
end
|
90
|
-
|
91
|
-
def store_nonce
|
92
|
-
nonce = env.response_headers['replay-nonce']
|
93
|
-
nonces << nonce if nonce
|
94
|
-
end
|
95
|
-
|
96
|
-
def pop_nonce
|
97
|
-
if nonces.empty?
|
98
|
-
get_nonce
|
99
|
-
end
|
100
|
-
|
101
|
-
nonces.pop
|
102
|
-
end
|
103
|
-
|
104
|
-
def get_nonce
|
105
|
-
client.get_nonce
|
106
|
-
end
|
107
|
-
|
108
|
-
def nonces
|
109
|
-
client.nonces
|
110
|
-
end
|
111
|
-
end
|