acme-client 2.0.32 → 2.0.33

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: b80e3ea41ac7fd634bb6a5f27597389cda7370255369051fe0c2c3a50ea82d82
4
- data.tar.gz: ee54f220befe3999b1f070b697b60a2d57526f9a5d6df93645ef3910e2866072
3
+ metadata.gz: b7216d10cb299e96cb201b06b84dd4d37ca10c2a37b5e32bb62f508fe76c7c77
4
+ data.tar.gz: b27c4e177e4ae6332bff52ee4b8afe04a50157bbfcb4cc17a6a9d0bfa94d6864
5
5
  SHA512:
6
- metadata.gz: eff5d82a38dbe7740a818c7a21b65b6cbdcd79dd82c070a7aa8a1cc0ddc8f065bb1af75f2532913614fd44ccb369e730dd16da3087882c7d6395778b5712fa4c
7
- data.tar.gz: bd35aaba72b277860c0fde3fadba19c1e4dbf46d404289b9a362615efc982140855b26141bec28fdd058de069d1ed079510ce8c57b7afc4fbae78770fb6f38d1
6
+ metadata.gz: df38f4e1c3f961aefb7d39ca86872c3816208349c9c917523b5215e988bad389a3d10133168092260f5c5c845210e4c80aad8705df13606e2c9767a88fda6c7c
7
+ data.tar.gz: 4960de02cea2e1fe074ea1fe6df1ba9ee62c59c6dfe7156359179f04ab6be40155f8a308676f70dc2796eef06641f6781025da7ec8fabc8ee7b36e41b446d0b6
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## `2.0.33`
2
+
3
+ * Add `Acme::Client::Problem` for structured RFC 7807 / RFC 8555 ACME problem documents
4
+ * Expose `Acme::Client::Error#problem` and problem field accessors while keeping `#acme_error_body` behavior
5
+ * Add typed error mappings for all currently registered IANA ACME error types and verify them against IANA's CSV registry
6
+
1
7
  ## `2.0.32`
2
8
 
3
9
  * Fix `valid_ip_address?` method definition. Was mistakenly defined on Object instead of `Acme::Client::CertificateRequest`
Binary file
data/acme-client.gemspec CHANGED
@@ -20,6 +20,7 @@ Gem::Specification.new do |spec|
20
20
  spec.add_development_dependency 'rspec', '~> 3.9'
21
21
  spec.add_development_dependency 'vcr', '~> 6.0'
22
22
  spec.add_development_dependency 'bigdecimal'
23
+ spec.add_development_dependency 'csv'
23
24
  spec.add_development_dependency 'webmock', '~> 3.8'
24
25
  spec.add_development_dependency 'webrick', '~> 1.7'
25
26
 
@@ -2,14 +2,14 @@ class Acme::Client::Error::RateLimited < Acme::Client::Error::ServerError
2
2
  DEFAULT_MESSAGE = 'Error message: urn:ietf:params:acme:error:rateLimited'
3
3
  DEFAULT_RETRY_SECONDS = 10
4
4
 
5
- def initialize(message = DEFAULT_MESSAGE, retry_after = nil, acme_error_body: nil, subproblems: nil)
5
+ def initialize(message = DEFAULT_MESSAGE, retry_after = nil, acme_error_body: nil, subproblems: nil, problem: nil)
6
6
  retry_after_time = case retry_after
7
7
  when Time then retry_after
8
8
  when nil then Time.now + DEFAULT_RETRY_SECONDS
9
9
  else Acme::Client::Util.parse_retry_after(retry_after) || Time.now + DEFAULT_RETRY_SECONDS
10
10
  end
11
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)
12
+ super(message, retry_after: int_retry_after, acme_error_body: acme_error_body, subproblems: subproblems, problem: problem)
13
13
  @retry_after_time = retry_after_time
14
14
  end
15
15
  end
@@ -1,5 +1,7 @@
1
+ require 'acme/client/problem'
2
+
1
3
  class Acme::Client::Error < StandardError
2
- attr_reader :retry_after, :retry_after_time, :subproblems, :acme_error_body
4
+ attr_reader :retry_after, :retry_after_time, :subproblems, :acme_error_body, :problem
3
5
 
4
6
  Subproblem = Struct.new(:type, :detail, :identifier, keyword_init: true) do
5
7
  def to_h
@@ -7,12 +9,13 @@ class Acme::Client::Error < StandardError
7
9
  end
8
10
  end
9
11
 
10
- def initialize(message = nil, retry_after: nil, acme_error_body: nil, subproblems: nil)
12
+ def initialize(message = nil, retry_after: nil, acme_error_body: nil, subproblems: nil, problem: nil)
11
13
  super(message)
12
14
  @retry_after_time = Acme::Client::Util.parse_retry_after(retry_after)
13
15
  @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
+ @problem = Acme::Client::Problem.from(problem || acme_error_body)
17
+ @acme_error_body = acme_error_body || @problem&.to_h
18
+ @subproblems = parse_subproblems(subproblems || @problem&.raw_subproblems)
16
19
  end
17
20
 
18
21
  private
@@ -31,6 +34,26 @@ class Acme::Client::Error < StandardError
31
34
 
32
35
  public
33
36
 
37
+ def type
38
+ problem&.type
39
+ end
40
+
41
+ def code
42
+ problem&.code
43
+ end
44
+
45
+ def detail
46
+ problem&.detail || message
47
+ end
48
+
49
+ def status
50
+ problem&.status
51
+ end
52
+
53
+ def identifier
54
+ problem&.identifier
55
+ end
56
+
34
57
  class Timeout < Acme::Client::Error; end
35
58
 
36
59
  class ClientError < Acme::Client::Error; end
@@ -63,10 +86,17 @@ class Acme::Client::Error < StandardError
63
86
  class UserActionRequired < ServerError; end
64
87
  class BadRevocationReason < ServerError; end
65
88
  class Caa < ServerError; end
89
+ class Compound < ServerError; end
66
90
  class Dns < ServerError; end
67
91
  class Connection < ServerError; end
68
92
  class Tls < ServerError; end
69
93
  class IncorrectResponse < ServerError; end
94
+ class AutoRenewalCanceled < ServerError; end
95
+ class AutoRenewalExpired < ServerError; end
96
+ class AutoRenewalCancellationInvalid < ServerError; end
97
+ class AutoRenewalRevocationNotSupported < ServerError; end
98
+ class UnknownDelegation < ServerError; end
99
+ class OnionCAARequired < ServerError; end
70
100
 
71
101
  ACME_ERRORS = {
72
102
  'urn:ietf:params:acme:error:alreadyReplaced' => AlreadyReplaced,
@@ -89,9 +119,16 @@ class Acme::Client::Error < StandardError
89
119
  'urn:ietf:params:acme:error:userActionRequired' => UserActionRequired,
90
120
  'urn:ietf:params:acme:error:badRevocationReason' => BadRevocationReason,
91
121
  'urn:ietf:params:acme:error:caa' => Caa,
122
+ 'urn:ietf:params:acme:error:compound' => Compound,
92
123
  'urn:ietf:params:acme:error:dns' => Dns,
93
124
  'urn:ietf:params:acme:error:connection' => Connection,
94
125
  'urn:ietf:params:acme:error:tls' => Tls,
95
- 'urn:ietf:params:acme:error:incorrectResponse' => IncorrectResponse
126
+ 'urn:ietf:params:acme:error:incorrectResponse' => IncorrectResponse,
127
+ 'urn:ietf:params:acme:error:autoRenewalCanceled' => AutoRenewalCanceled,
128
+ 'urn:ietf:params:acme:error:autoRenewalExpired' => AutoRenewalExpired,
129
+ 'urn:ietf:params:acme:error:autoRenewalCancellationInvalid' => AutoRenewalCancellationInvalid,
130
+ 'urn:ietf:params:acme:error:autoRenewalRevocationNotSupported' => AutoRenewalRevocationNotSupported,
131
+ 'urn:ietf:params:acme:error:unknownDelegation' => UnknownDelegation,
132
+ 'urn:ietf:params:acme:error:onionCAARequired' => OnionCAARequired
96
133
  }
97
134
  end
@@ -103,11 +103,12 @@ module Acme::Client::HTTPClient
103
103
  def raise_on_error!
104
104
  retry_after = env.response_headers['retry-after']
105
105
  body = env.body.is_a?(Hash) ? env.body : nil
106
+ problem = Acme::Client::Problem.from(body)
106
107
  subproblems = error_subproblems
107
108
  if error_class == Acme::Client::Error::RateLimited
108
- raise error_class.new(error_message, retry_after, acme_error_body: body, subproblems: subproblems)
109
+ raise error_class.new(error_message, retry_after, acme_error_body: body, subproblems: subproblems, problem: problem)
109
110
  end
110
- raise error_class.new(error_message, retry_after: retry_after, acme_error_body: body, subproblems: subproblems)
111
+ raise error_class.new(error_message, retry_after: retry_after, acme_error_body: body, subproblems: subproblems, problem: problem)
111
112
  end
112
113
 
113
114
  def error_message
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Acme; end
4
+ class Acme::Client; end
5
+
6
+ class Acme::Client::Problem
7
+ ERROR_PREFIX = 'urn:ietf:params:acme:error:'.freeze
8
+
9
+ # Registry descriptions for problem types, not fallback values for problem details.
10
+ REGISTERED_ERROR_TYPE_DESCRIPTIONS = {
11
+ 'accountDoesNotExist' => 'The request specified an account that does not exist',
12
+ 'alreadyRevoked' => 'The request specified a certificate to be revoked that has already been revoked',
13
+ 'badCSR' => 'The CSR is unacceptable (e.g., due to a short key)',
14
+ 'badNonce' => 'The client sent an unacceptable anti-replay nonce',
15
+ 'badPublicKey' => 'The JWS was signed by a public key the server does not support',
16
+ 'badRevocationReason' => 'The revocation reason provided is not allowed by the server',
17
+ 'badSignatureAlgorithm' => 'The JWS was signed with an algorithm the server does not support',
18
+ 'caa' => 'Certification Authority Authorization (CAA) records forbid the CA from issuing a certificate',
19
+ 'compound' => 'Specific error conditions are indicated in the "subproblems" array',
20
+ 'connection' => 'The server could not connect to validation target',
21
+ 'dns' => 'There was a problem with a DNS query during identifier validation',
22
+ 'externalAccountRequired' => 'The request must include a value for the "externalAccountBinding" field',
23
+ 'incorrectResponse' => "Response received didn't match the challenge's requirements",
24
+ 'invalidContact' => 'A contact URL for an account was invalid',
25
+ 'malformed' => 'The request message was malformed',
26
+ 'orderNotReady' => 'The request attempted to finalize an order that is not ready to be finalized',
27
+ 'rateLimited' => 'The request exceeds a rate limit',
28
+ 'rejectedIdentifier' => 'The server will not issue certificates for the identifier',
29
+ 'serverInternal' => 'The server experienced an internal error',
30
+ 'tls' => 'The server received a TLS error during validation',
31
+ 'unauthorized' => 'The client lacks sufficient authorization',
32
+ 'unsupportedContact' => 'A contact URL for an account used an unsupported protocol scheme',
33
+ 'unsupportedIdentifier' => 'An identifier is of an unsupported type',
34
+ 'userActionRequired' => 'Visit the "instance" URL and take actions specified there',
35
+ 'autoRenewalCanceled' => 'The short-term certificate is no longer available because the auto-renewal Order has been explicitly canceled by the IdO',
36
+ 'autoRenewalExpired' => 'The short-term certificate is no longer available because the auto-renewal Order has expired',
37
+ 'autoRenewalCancellationInvalid' => 'A request to cancel an auto-renewal Order that is not in state "valid" has been received',
38
+ 'autoRenewalRevocationNotSupported' => 'A request to revoke an auto-renewal Order has been received',
39
+ 'unknownDelegation' => 'An unknown configuration is listed in the delegation attribute of the order request',
40
+ 'onionCAARequired' => 'The CA only supports checking the CAA for Hidden Services in-band, but the client has not provided an in-band CAA',
41
+ 'alreadyReplaced' => 'The request specified a predecessor certificate that has already been marked as replaced'
42
+ }.freeze
43
+
44
+ def self.from(value)
45
+ case value
46
+ when nil
47
+ nil
48
+ when Acme::Client::Problem
49
+ value
50
+ when Hash
51
+ new(value)
52
+ end
53
+ end
54
+
55
+ attr_reader :raw
56
+
57
+ def initialize(raw = {})
58
+ @raw = raw || {}
59
+ end
60
+
61
+ def type
62
+ fetch('type')
63
+ end
64
+
65
+ def code
66
+ return unless type
67
+ return unless type.start_with?(ERROR_PREFIX)
68
+
69
+ type[ERROR_PREFIX.length..-1]
70
+ end
71
+
72
+ def title
73
+ fetch('title')
74
+ end
75
+
76
+ def detail
77
+ fetch('detail')
78
+ end
79
+
80
+ def status
81
+ fetch('status')
82
+ end
83
+
84
+ def instance
85
+ fetch('instance')
86
+ end
87
+
88
+ def identifier
89
+ fetch('identifier')
90
+ end
91
+
92
+ def subproblems
93
+ return [] unless raw_subproblems.is_a?(Array)
94
+
95
+ raw_subproblems.map { |subproblem| self.class.new(subproblem) }
96
+ end
97
+
98
+ def raw_subproblems
99
+ fetch('subproblems')
100
+ end
101
+
102
+ def registered?
103
+ REGISTERED_ERROR_TYPE_DESCRIPTIONS.key?(code)
104
+ end
105
+
106
+ alias standard? registered?
107
+
108
+ def description
109
+ REGISTERED_ERROR_TYPE_DESCRIPTIONS[code]
110
+ end
111
+
112
+ def matches?(type_or_code)
113
+ candidate = type_or_code.to_s
114
+ candidate == type || candidate == code || "#{ERROR_PREFIX}#{candidate}" == type
115
+ end
116
+
117
+ def message
118
+ detail || title || type
119
+ end
120
+
121
+ def to_h
122
+ raw
123
+ end
124
+
125
+ def as_json(*_arguments)
126
+ to_h
127
+ end
128
+
129
+ private
130
+
131
+ def fetch(key)
132
+ raw[key] || raw[key.to_sym]
133
+ end
134
+ end
@@ -31,10 +31,11 @@ class Acme::Client::Resources::Challenges::Base
31
31
  def typed_error
32
32
  return nil unless error
33
33
 
34
- error_type = error['type']
35
- error_detail = error['detail'] || 'Unknown error'
34
+ problem = Acme::Client::Problem.from(error)
35
+ error_type = problem&.type
36
+ error_detail = problem&.detail || 'Unknown error'
36
37
  error_class = Acme::Client::Error::ACME_ERRORS.fetch(error_type, Acme::Client::Error)
37
- error_class.new(error_detail)
38
+ error_class.new(error_detail, problem: problem)
38
39
  end
39
40
 
40
41
  def to_h
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Acme
4
4
  class Client
5
- VERSION = '2.0.32'.freeze
5
+ VERSION = '2.0.33'.freeze
6
6
  end
7
7
  end
data/lib/acme/client.rb CHANGED
@@ -14,6 +14,7 @@ module Acme; end
14
14
  class Acme::Client; end
15
15
 
16
16
  require 'acme/client/version'
17
+ require 'acme/client/problem'
17
18
  require 'acme/client/http_client'
18
19
  require 'acme/client/certificate_request'
19
20
  require 'acme/client/self_sign_certificate'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: acme-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.32
4
+ version: 2.0.33
5
5
  platform: ruby
6
6
  authors:
7
7
  - Charles Barbier
@@ -65,6 +65,20 @@ dependencies:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
67
  version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: csv
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
68
82
  - !ruby/object:Gem::Dependency
69
83
  name: webmock
70
84
  requirement: !ruby/object:Gem::Requirement
@@ -158,6 +172,7 @@ files:
158
172
  - LICENSE.txt
159
173
  - README.md
160
174
  - Rakefile
175
+ - acme-client-2.0.32.gem
161
176
  - acme-client.gemspec
162
177
  - bin/console
163
178
  - bin/generate_keystash
@@ -176,6 +191,7 @@ files:
176
191
  - lib/acme/client/jwk/ecdsa.rb
177
192
  - lib/acme/client/jwk/hmac.rb
178
193
  - lib/acme/client/jwk/rsa.rb
194
+ - lib/acme/client/problem.rb
179
195
  - lib/acme/client/resources.rb
180
196
  - lib/acme/client/resources/account.rb
181
197
  - lib/acme/client/resources/authorization.rb