acme-client 2.0.9 → 2.0.31

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d1fcaf1206f38a9ded860ad3685de94d3c2743a04222d63a6d5a14ca35146daa
4
- data.tar.gz: 766a3bf59e3877a3a4ab457e369f9d09626204afacc8e1b8cb23015cde9ccf12
3
+ metadata.gz: 55d60fbf24893ea26c59535b2ffe6041916e678d4bbea5982666c5f9eb0f2bae
4
+ data.tar.gz: b62818cd2dc3ca8b9676e7ec355dfcec40eaf90b3f9b7abc26b2dddea0cc4473
5
5
  SHA512:
6
- metadata.gz: 814205156a08f49d3481545f35306f63e1f342267f126edc528b22c78ce477b4696fd4a9f401a501bb8ffe33c57f42e8e6826de830e5321289555972d841bcb3
7
- data.tar.gz: 493a4a2ebe8803b3949797bd74061d500bb66e7a79c5d62e9dfb98af7b995a46d3a2847c88d14ad7585f8b25567ccdabdf5bdaa25eafdb045a1f3aaed7b05f13
6
+ metadata.gz: b3311579f5f990f433e2ede868ce5baf6ce910eb38b19889107436a0dea91f7d96c36619e6039e0e4b4382402e8bc81dce939fee0f915c850b068b80ced67c1d
7
+ data.tar.gz: 45b7de2260bad0703cfa5f865a33e5367789cde176e0a906224fc7e7ad29ba5eeb9f756ce79b29719460c03264c26c5808702737be912d2f5df9cce38003b2f9
data/CHANGELOG.md CHANGED
@@ -1,3 +1,99 @@
1
+ ## `2.0.31`
2
+
3
+ * Expose Retry-After header on all
4
+ * ARI improvement
5
+ * Expose full error message on Error#acme_error_body
6
+ * Expose error subproblems (RFC7807) on Error#subproblems
7
+
8
+ ## `2.0.30`
9
+
10
+ * Add a default message to RateLimited error
11
+
12
+ This fix avoid argument error on RateLimited object when stubbing without passing arguments.
13
+
14
+ ## `2.0.29`
15
+
16
+ * IP support to the CertificateRequest helper
17
+
18
+ ## `2.0.28`
19
+
20
+ * Make [Retry-After](https://datatracker.ietf.org/doc/html/rfc8555/#section-6.6) accessible from RateLimited#retry_after exceptions
21
+
22
+ ## `2.0.27`
23
+
24
+ * Add support for Renewal Information (ARI) (RFC 9773)
25
+
26
+ ## `2.0.26`
27
+
28
+ * Add support for dns-account-01 challenge (RFC draft-ietf-acme-dns-account-label-01)
29
+
30
+ ## `2.0.25`
31
+
32
+ * Add support for profiles extension
33
+
34
+ ## `2.0.24`
35
+
36
+ * Add support for account orders url attribute.
37
+
38
+ ## `2.0.23`
39
+
40
+ * Allow Order to be create without url. Location is not always required in the specification.
41
+
42
+ ## `2.0.22`
43
+
44
+ * Loosen base64 dependency constraint
45
+
46
+ ## `2.0.21`
47
+
48
+ * Add validated attribute to challenges
49
+
50
+ ## `2.0.20`
51
+
52
+ * Add OrderNotReady exception
53
+
54
+ ## `2.0.19`
55
+
56
+ * Fix an issue CSR generation. Version should be set to zero according to the spec. It's causing issue with some ACME server implementation.
57
+
58
+ ## `2.0.18`
59
+
60
+ * Fix an issue public key encoding. `OpenSSL::BN` cause keys with leading zero to fail.
61
+
62
+ ## `2.0.17`
63
+
64
+ * Fix bug where depending on call order `jws` get generated with the wrong `kid`
65
+
66
+ ## `2.0.16`
67
+
68
+ * Refactor Directory
69
+ * Fix an issue where the client would crash when ACME provider return nonce for directory endpoint
70
+
71
+ ## `2.0.15`
72
+
73
+ * Also pass connection_options to Faraday for Client#get_nonce
74
+
75
+
76
+ ## `2.0.14`
77
+
78
+ * Fix Faraday HTTP exceptions leaking out, always raise `Acme::Client::Error` instead
79
+
80
+ ## `2.0.13`
81
+
82
+ * Add support for External Account Binding
83
+
84
+ ## `2.0.12`
85
+
86
+ * Update test matrix to current Ruby versions (2.7 to 3.2)
87
+ * Support for Faraday retry 2.x
88
+
89
+ ## `2.0.11`
90
+
91
+ * Add support for error code `AlreadyRevoked` and `BadPublicKey`
92
+
93
+ ## `2.0.10`
94
+
95
+ * Support for Faraday 1.0 / 2.0
96
+
1
97
  ## `2.0.9`
2
98
 
3
99
  * Support for Ruby 3.0 and Faraday 0.17.x
data/Gemfile CHANGED
@@ -8,10 +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
-
14
- if Gem::Version.new(RUBY_VERSION) <= Gem::Version.new('2.2.2')
15
- gem 'activesupport', '~> 4.2.6'
16
- end
17
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
 
@@ -115,9 +120,11 @@ To order a new certificate, the client must provide a list of identifiers.
115
120
 
116
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.
117
122
 
118
- 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.
123
+ Each authorization contains multiple challenges, typically a `dns-01`, `dns-account-01`, and a `http-01` challenge. The applicant is only required to complete one of the challenges.
119
124
 
120
- You can access the challenge you wish to complete using the `#dns` or `#http` method.
125
+ The `dns-account-01` challenge prepends an account-specific label before `_acme-challenge`, producing a record name of the form `_<label>._acme-challenge` so different clients can validate the same domain concurrently.
126
+
127
+ You can access the challenge you wish to complete using the `#dns`, `#dns_account`, or `#http` methods.
121
128
 
122
129
  ```ruby
123
130
  order = client.new_order(identifiers: ['example.com'])
@@ -160,6 +167,25 @@ dns_challenge.record_type # => 'TXT'
160
167
  dns_challenge.record_content # => 'HRV3PS5sRDyV-ous4HJk4z24s5JjmUTjcCaUjFt28-8'
161
168
  ```
162
169
 
170
+ ### Preparing for DNS-Account-01 challenge
171
+
172
+ To complete the DNS-Account-01 challenge, you must set a DNS TXT record using an account-specific name. This allows multiple ACME clients to validate the same domain concurrently without conflicts.
173
+
174
+ The DNSAccount01 object has utility methods to generate the required DNS record:
175
+
176
+ ```ruby
177
+ dns_account_challenge = authorization.dns_account
178
+
179
+ dns_account_challenge.record_name # => '_ujmmovf2vn55tgye._acme-challenge'
180
+ dns_account_challenge.record_type # => 'TXT'
181
+ dns_account_challenge.record_content # => 'HRV3PS5sRDyV-ous4HJk4z24s5JjmUTjcCaUjFt28-8'
182
+ ```
183
+
184
+ The record name includes an account-specific label derived from your account URL, ensuring different clients can validate simultaneously:
185
+
186
+ - **DNS-01**: `_acme-challenge.example.com` (shared)
187
+ - **DNS-Account-01**: `_ujmmovf2vn55tgye._acme-challenge.example.com` (account-specific)
188
+
163
189
  ### Requesting a challenge verification
164
190
 
165
191
  Once you are ready to complete the challenge, you can request the server perform the verification.
@@ -200,8 +226,7 @@ order.certificate # => PEM-formatted certificate
200
226
 
201
227
  ### Ordering an alternative certificate
202
228
 
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:
229
+ 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
230
 
206
231
  ```ruby
207
232
  begin
@@ -237,12 +262,28 @@ To change the key used for an account you can call `#account_key_change` with th
237
262
  ```ruby
238
263
  require 'openssl'
239
264
  new_private_key = OpenSSL::PKey::RSA.new(4096)
240
- client.account_key_change(private_key: new_private_key)
265
+ client.account_key_change(new_private_key: new_private_key)
241
266
  ```
242
267
 
268
+ ### Profile Extension
269
+
270
+ Provide a CA profile when creating a new order:
271
+
272
+ ```ruby
273
+ order = client.new_order(identifiers: ['example.com'], profile: 'shortlived')
274
+ ```
275
+
276
+ ACME servers may list supported profiles in the directory endpoint:
277
+
278
+ ```ruby
279
+ client.profiles => {"classic": "https://example.com/docs/classic", "shortlived": "https://example.com/docs/shortlived"}
280
+ ```
281
+
282
+ See the [RFC draft of certificate profiles](https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/) for more info.
283
+
243
284
  ## Requirements
244
285
 
245
- Ruby >= 2.1
286
+ Ruby >= 3.0
246
287
 
247
288
  ## Development
248
289
 
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,21 +11,19 @@ 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.1.0'
17
+ spec.required_ruby_version = '>= 2.3.0'
18
18
 
19
- spec.add_development_dependency 'bundler', '>= 1.17.3'
20
- if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.2')
21
- spec.add_development_dependency 'rake', '>= 12.0'
22
- else
23
- spec.add_development_dependency 'rake', '~> 13.0'
24
- end
19
+ spec.add_development_dependency 'rake', '~> 13.0'
25
20
  spec.add_development_dependency 'rspec', '~> 3.9'
26
- spec.add_development_dependency 'vcr', '~> 2.9'
21
+ spec.add_development_dependency 'vcr', '~> 6.0'
22
+ spec.add_development_dependency 'bigdecimal'
27
23
  spec.add_development_dependency 'webmock', '~> 3.8'
28
- spec.add_development_dependency 'webrick'
24
+ spec.add_development_dependency 'webrick', '~> 1.7'
29
25
 
30
- spec.add_runtime_dependency 'faraday', '>= 0.17', '< 2.0.0'
26
+ spec.add_runtime_dependency 'base64', '~> 0.2'
27
+ spec.add_runtime_dependency 'faraday', '>= 1.0', '< 3.0.0'
28
+ spec.add_runtime_dependency 'faraday-retry', '>= 1.0', '< 3.0.0'
31
29
  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)
@@ -89,7 +89,7 @@ class Acme::Client::CertificateRequest
89
89
  end
90
90
  csr.public_key = @private_key
91
91
  csr.subject = generate_subject
92
- csr.version = 2
92
+ csr.version = 0
93
93
  add_extension(csr)
94
94
  csr.sign @private_key, @digest
95
95
  end
@@ -104,8 +104,15 @@ class Acme::Client::CertificateRequest
104
104
  end
105
105
 
106
106
  def add_extension(csr)
107
+ san = @names.map do |name|
108
+ if valid_ip_address?(name)
109
+ "IP:#{name}"
110
+ else
111
+ "DNS:#{name}"
112
+ end
113
+ end
107
114
  extension = OpenSSL::X509::ExtensionFactory.new.create_extension(
108
- 'subjectAltName', @names.map { |name| "DNS:#{name}" }.join(', '), false
115
+ 'subjectAltName', san.join(', '), false
109
116
  )
110
117
  csr.add_attribute(
111
118
  OpenSSL::X509::Attribute.new(
@@ -116,4 +123,14 @@ class Acme::Client::CertificateRequest
116
123
  end
117
124
  end
118
125
 
126
+ def valid_ip_address?(address)
127
+ require 'ipaddr'
128
+ begin
129
+ ip = IPAddr.new(address)
130
+ true
131
+ rescue IPAddr::InvalidAddressError, IPAddr::AddressFamilyError
132
+ false
133
+ end
134
+ end
135
+
119
136
  require 'acme/client/certificate_request/ec_key_patch'
@@ -0,0 +1,15 @@
1
+ class Acme::Client::Error::RateLimited < Acme::Client::Error::ServerError
2
+ DEFAULT_MESSAGE = 'Error message: urn:ietf:params:acme:error:rateLimited'
3
+ DEFAULT_RETRY_SECONDS = 10
4
+
5
+ def initialize(message = DEFAULT_MESSAGE, retry_after = nil, acme_error_body: nil, subproblems: nil)
6
+ retry_after_time = case retry_after
7
+ when Time then retry_after
8
+ when nil then Time.now + DEFAULT_RETRY_SECONDS
9
+ else Acme::Client::Util.parse_retry_after(retry_after) || Time.now + DEFAULT_RETRY_SECONDS
10
+ end
11
+ int_retry_after = retry_after.nil? ? DEFAULT_RETRY_SECONDS : [(retry_after_time - Time.now).ceil, 0].max
12
+ super(message, retry_after: int_retry_after, acme_error_body: acme_error_body, subproblems: subproblems)
13
+ @retry_after_time = retry_after_time
14
+ end
15
+ end
@@ -1,4 +1,36 @@
1
1
  class Acme::Client::Error < StandardError
2
+ attr_reader :retry_after, :retry_after_time, :subproblems, :acme_error_body
3
+
4
+ Subproblem = Struct.new(:type, :detail, :identifier, keyword_init: true) do
5
+ def to_h
6
+ { type: type, detail: detail, identifier: identifier }
7
+ end
8
+ end
9
+
10
+ def initialize(message = nil, retry_after: nil, acme_error_body: nil, subproblems: nil)
11
+ super(message)
12
+ @retry_after_time = Acme::Client::Util.parse_retry_after(retry_after)
13
+ @retry_after = @retry_after_time ? [(@retry_after_time - Time.now).ceil, 0].max : nil
14
+ @acme_error_body = acme_error_body
15
+ @subproblems = parse_subproblems(subproblems)
16
+ end
17
+
18
+ private
19
+
20
+ def parse_subproblems(raw)
21
+ return [] if raw.nil? || !raw.is_a?(Array)
22
+
23
+ raw.map do |sp|
24
+ Subproblem.new(
25
+ type: sp['type'],
26
+ detail: sp['detail'],
27
+ identifier: sp['identifier']
28
+ )
29
+ end
30
+ end
31
+
32
+ public
33
+
2
34
  class Timeout < Acme::Client::Error; end
3
35
 
4
36
  class ClientError < Acme::Client::Error; end
@@ -8,10 +40,15 @@ class Acme::Client::Error < StandardError
8
40
  class NotFound < ClientError; end
9
41
  class CertificateNotReady < ClientError; end
10
42
  class ForcedChainNotFound < ClientError; end
43
+ class OrderNotReady < ClientError; end
44
+ class OrderUrlNil < ClientError; end
11
45
 
12
46
  class ServerError < Acme::Client::Error; end
47
+ class AlreadyReplaced < ServerError; end
48
+ class AlreadyRevoked < ServerError; end
13
49
  class BadCSR < ServerError; end
14
50
  class BadNonce < ServerError; end
51
+ class BadPublicKey < ServerError; end
15
52
  class BadSignatureAlgorithm < ServerError; end
16
53
  class InvalidContact < ServerError; end
17
54
  class UnsupportedContact < ServerError; end
@@ -32,14 +69,18 @@ class Acme::Client::Error < StandardError
32
69
  class IncorrectResponse < ServerError; end
33
70
 
34
71
  ACME_ERRORS = {
72
+ 'urn:ietf:params:acme:error:alreadyReplaced' => AlreadyReplaced,
73
+ 'urn:ietf:params:acme:error:alreadyRevoked' => AlreadyRevoked,
35
74
  'urn:ietf:params:acme:error:badCSR' => BadCSR,
36
75
  'urn:ietf:params:acme:error:badNonce' => BadNonce,
76
+ 'urn:ietf:params:acme:error:badPublicKey' => BadPublicKey,
37
77
  'urn:ietf:params:acme:error:badSignatureAlgorithm' => BadSignatureAlgorithm,
38
78
  'urn:ietf:params:acme:error:invalidContact' => InvalidContact,
39
79
  'urn:ietf:params:acme:error:unsupportedContact' => UnsupportedContact,
40
80
  'urn:ietf:params:acme:error:externalAccountRequired' => ExternalAccountRequired,
41
81
  'urn:ietf:params:acme:error:accountDoesNotExist' => AccountDoesNotExist,
42
82
  'urn:ietf:params:acme:error:malformed' => Malformed,
83
+ 'urn:ietf:params:acme:error:orderNotReady' => OrderNotReady,
43
84
  'urn:ietf:params:acme:error:rateLimited' => RateLimited,
44
85
  'urn:ietf:params:acme:error:rejectedIdentifier' => RejectedIdentifier,
45
86
  'urn:ietf:params:acme:error:serverInternal' => ServerInternal,
@@ -0,0 +1,173 @@
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
+ retry_after = env.response_headers['retry-after']
105
+ body = env.body.is_a?(Hash) ? env.body : nil
106
+ subproblems = error_subproblems
107
+ if error_class == Acme::Client::Error::RateLimited
108
+ raise error_class.new(error_message, retry_after, acme_error_body: body, subproblems: subproblems)
109
+ end
110
+ raise error_class.new(error_message, retry_after: retry_after, acme_error_body: body, subproblems: subproblems)
111
+ end
112
+
113
+ def error_message
114
+ if env.body.is_a? Hash
115
+ env.body['detail']
116
+ else
117
+ "Error message: #{env.body}"
118
+ end
119
+ end
120
+
121
+ def error_class
122
+ Acme::Client::Error::ACME_ERRORS.fetch(error_name, Acme::Client::Error)
123
+ end
124
+
125
+ def error_name
126
+ return unless env.body.is_a?(Hash)
127
+ return unless env.body.key?('type')
128
+ env.body['type']
129
+ end
130
+
131
+ def error_subproblems
132
+ return unless env.body.is_a?(Hash)
133
+ env.body['subproblems']
134
+ end
135
+
136
+ def decode_body
137
+ content_type = env.response_headers['Content-Type'].to_s
138
+
139
+ if content_type.start_with?('application/json', 'application/problem+json')
140
+ JSON.load(env.body)
141
+ else
142
+ env.body
143
+ end
144
+ end
145
+
146
+ def decode_link_headers
147
+ return unless env.response_headers.key?('Link')
148
+ link_header = env.response_headers['Link']
149
+ Acme::Client::Util.decode_link_headers(link_header)
150
+ end
151
+
152
+ def store_nonce
153
+ nonce = env.response_headers['replay-nonce']
154
+ nonces << nonce if nonce
155
+ end
156
+
157
+ def pop_nonce
158
+ if nonces.empty?
159
+ get_nonce
160
+ end
161
+
162
+ nonces.pop
163
+ end
164
+
165
+ def get_nonce
166
+ client.get_nonce
167
+ end
168
+
169
+ def nonces
170
+ client.nonces
171
+ end
172
+ end
173
+ 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'
@@ -34,16 +34,18 @@ class Acme::Client::Resources::Account
34
34
  url: url,
35
35
  term_of_service: term_of_service,
36
36
  status: status,
37
- contact: contact
37
+ contact: contact,
38
+ orders: orders_url
38
39
  }
39
40
  end
40
41
 
41
42
  private
42
43
 
43
- def assign_attributes(url:, term_of_service:, status:, contact:)
44
+ def assign_attributes(url:, term_of_service:, status:, contact:, orders: nil)
44
45
  @url = url
45
46
  @term_of_service = term_of_service
46
47
  @status = status
47
48
  @contact = Array(contact)
49
+ @orders_url = orders
48
50
  end
49
51
  end