acme-client 2.0.11 → 2.0.18

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3ff7c41acdce5ad9f39a371998b37466d8affe4cb3de5e856d44d36e512180ea
4
- data.tar.gz: cb9fda5e72bf22dc93659a238b05dd419652584238a024658ebde04aaefc9e80
3
+ metadata.gz: 88c2953c9fcfd9a7f7825b4c69b2cf0ff86befb504914e6f78b03f6fdaab052b
4
+ data.tar.gz: bddedcd46dc0b2d1224a7d409916668aa31bfad3c6576a0d09376257c654f434
5
5
  SHA512:
6
- metadata.gz: eea011baa47710043bab4f22e6556d6ece49ae1a87005e30ec25a41d1abeb93bf6a049ecadf742d706c6ebef7c6ece862704aadd96c252e8c6d801bf814c221a
7
- data.tar.gz: 77a75476bd154d46349acee637aad44e08ef911a676e0da5de9267b5f1cc1b845cd2d8030511fd25e2ee31354cdb269f4151a6c1ae36816ff75908864effa6d6
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
@@ -8,6 +8,5 @@ end
8
8
 
9
9
  group :development, :test do
10
10
  gem 'pry'
11
- gem 'rubocop', '~> 0.49.0'
12
11
  gem 'ruby-prof', require: false
13
12
  end
data/README.md CHANGED
@@ -1,15 +1,11 @@
1
1
  # Acme::Client
2
2
 
3
- [![Build Status](https://travis-ci.org/unixcharles/acme-client.svg?branch=master)](https://travis-ci.org/unixcharles/acme-client)
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
- 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:
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(private_key: new_private_key)
244
+ client.account_key_change(new_private_key: new_private_key)
241
245
  ```
242
246
 
243
247
  ## Requirements
244
248
 
245
- Ruby >= 2.1
249
+ Ruby >= 3.0
246
250
 
247
251
  ## Development
248
252
 
data/Rakefile CHANGED
@@ -3,7 +3,4 @@ require 'bundler/gem_tasks'
3
3
  require 'rspec/core/rake_task'
4
4
  RSpec::Core::RakeTask.new(:spec)
5
5
 
6
- require 'rubocop/rake_task'
7
- RuboCop::RakeTask.new
8
-
9
- task default: [:spec, :rubocop]
6
+ task default: [:spec]
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', '~> 1.0'
26
+ spec.add_runtime_dependency 'faraday-retry', '>= 1.0', '< 3.0.0'
28
27
  end
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'acme-client'
5
+
6
+ require File.join(File.dirname(__FILE__), '../spec/support/ssl_helper')
7
+
8
+
9
+ SSLHelper::KEYSTASH.generate_keystash!(size: 200)
@@ -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].to_s(2)),
54
- y: Acme::Client::Util.urlsafe_base64(coordinates[:y].to_s(2))
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: OpenSSL::BN.new([hex_x].pack('H*'), 2),
96
- y: OpenSSL::BN.new([hex_y].pack('H*'), 2)
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
@@ -19,3 +19,4 @@ end
19
19
  require 'acme/client/jwk/base'
20
20
  require 'acme/client/jwk/rsa'
21
21
  require 'acme/client/jwk/ecdsa'
22
+ require 'acme/client/jwk/hmac'
@@ -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(url, connection_options)
21
- @url, @connection_options = url, connection_options
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 ||= load_directory
55
- end
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
- result[key] = URI(body[entry]) if body[entry]
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
@@ -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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Acme
4
4
  class Client
5
- VERSION = '2.0.11'.freeze
5
+ VERSION = '2.0.18'.freeze
6
6
  end
7
7
  end
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
- @directory = Acme::Client::Resources::Directory.new(URI(directory), @connection_options)
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
- response = post(endpoint_for(:new_account), payload: payload, mode: :jws)
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
- connection = new_connection(endpoint: endpoint_for(:new_nonce))
215
- response = connection.head(nil, nil, 'User-Agent' => USER_AGENT)
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
- @directory.meta
237
+ directory.meta
222
238
  end
223
239
 
224
240
  def terms_of_service
225
- @directory.terms_of_service
241
+ directory.terms_of_service
226
242
  end
227
243
 
228
244
  def website
229
- @directory.website
245
+ directory.website
230
246
  end
231
247
 
232
248
  def caa_identities
233
- @directory.caa_identities
249
+ directory.caa_identities
234
250
  end
235
251
 
236
252
  def external_account_required
237
- @directory.external_account_required
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: :kid)
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(endpoint: endpoint, mode: mode)
326
- end
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
- @directory.endpoint_for(key)
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.11
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: 2022-06-01 00:00:00.000000000 Z
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: '0'
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: '0'
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/faraday_middleware.rb
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.2.20
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
@@ -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
@@ -1,12 +0,0 @@
1
- /.bundle/
2
- /.yardoc
3
- /Gemfile.lock
4
- /_yardoc/
5
- /coverage/
6
- /doc/
7
- /pkg/
8
- /spec/reports/
9
- /tmp/
10
- /vendor/bundle
11
- /.idea/
12
- .tool-versions
data/.rspec DELETED
@@ -1,3 +0,0 @@
1
- --format documentation
2
- --color
3
- --order rand
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