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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ccaa8fb7fadbae1a6d93f99ad319ca95261994cbec494ab885028aae2f894a6a
4
- data.tar.gz: 224d05d0f66cb37ffcbcf1bf05f649e0278dc097518b927dcc4a2e635ba29357
3
+ metadata.gz: 88c2953c9fcfd9a7f7825b4c69b2cf0ff86befb504914e6f78b03f6fdaab052b
4
+ data.tar.gz: bddedcd46dc0b2d1224a7d409916668aa31bfad3c6576a0d09376257c654f434
5
5
  SHA512:
6
- metadata.gz: c461b3d255fc35d3b21411632ff4f26700fd0969547f8a7a93879c719e2383f58badfc0278bc45df4780b293094af3b2d560494ac3f00076dae23affbe697682
7
- data.tar.gz: 477869ea08075d8e9d70afa80f4576f6a0ffb936bfb70244b0bf6f8d34d2da593d76acf6231d266fd920eeb0fbf4045e4605869c77f9b06377487cbb4902c014
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
- [![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](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
- * [Setting up a client](#setting-up-a-client)
27
- * [Account management](#account-management)
28
- * [Obtaining a certificate](#obtaining-a-certificate)
29
- * [Ordering a certificate](#ordering-a-certificate)
30
- * [Completing an HTTP challenge](#preparing-for-http-challenge)
31
- * [Completing an DNS challenge](#preparing-for-dns-challenge)
32
- * [Requesting a challenge verification](#requesting-a-challenge-verification)
33
- * [Downloading a certificate](#downloading-a-certificate)
34
- * [Extra](#extra)
35
- * [Certificate revokation](#certificate-revokation)
36
- * [Certificate renewal](#certificate-renewal)
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 enpoind at `https://acme-staging-v02.api.letsencrypt.org/directory`.
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 change.
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: private_key, subject: { common_name: 'example.com' })
197
+ csr = Acme::Client::CertificateRequest.new(private_key: a_different_private_key, subject: { common_name: 'example.com' })
174
198
  order.finalize(csr: csr)
175
- sleep(1) while order.status == 'processing'
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
- The is no renewal process, just create a new order.
234
+ There is no renewal process, just create a new order.
235
+
192
236
 
237
+ ### Account Key Roll-over
193
238
 
194
- ## Not implemented
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
- - Account Key Roll-over.
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 >= 2.1
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
@@ -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,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.1.0'
17
+ spec.required_ruby_version = '>= 2.3.0'
18
18
 
19
- spec.add_development_dependency 'bundler', '~> 1.6', '>= 1.6.9'
20
- spec.add_development_dependency 'rake', '~> 10.0'
21
- spec.add_development_dependency 'rspec', '~> 3.3', '>= 3.3.0'
22
- spec.add_development_dependency 'vcr', '~> 2.9', '>= 2.9.3'
23
- spec.add_development_dependency 'webmock', '~> 1.21', '>= 1.21.0'
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', '~> 0.9', '>= 0.9.1'
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
@@ -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)
data/bin/release CHANGED
@@ -13,6 +13,7 @@ def `(command)
13
13
  end
14
14
  end
15
15
 
16
+ `git add CHANGELOG.md`
16
17
  `git add lib/acme/client/version.rb`
17
18
  `git commit -m "bump to #{version}"`
18
19
  `git pull --rebase origin master`
@@ -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
@@ -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
@@ -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].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
 
@@ -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| [bn.to_s(16)].pack('H*') }
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: OpenSSL::BN.new([hex_x].pack('H*'), 2),
94
- y: OpenSSL::BN.new([hex_y].pack('H*'), 2)
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
@@ -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'