acme-client 2.0.32 → 2.0.34

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: 89c12bcb307e446e08b101e2ceade09f0edb11bbdbac66406706bf3cc5235f2d
4
+ data.tar.gz: cf2b1335b4e3080c344fd685731d41d15340265e152037ac9fe4c8e362f8199b
5
5
  SHA512:
6
- metadata.gz: eff5d82a38dbe7740a818c7a21b65b6cbdcd79dd82c070a7aa8a1cc0ddc8f065bb1af75f2532913614fd44ccb369e730dd16da3087882c7d6395778b5712fa4c
7
- data.tar.gz: bd35aaba72b277860c0fde3fadba19c1e4dbf46d404289b9a362615efc982140855b26141bec28fdd058de069d1ed079510ce8c57b7afc4fbae78770fb6f38d1
6
+ metadata.gz: b0a658e05c3d12188e8615cc0bad63b23649b0839fe96352cc3362be6a567e5b05527f94b645bbee093830a8f2f8e12ca39ca422f44bfa6f479e50e1740d1035
7
+ data.tar.gz: 00b4f8042dbae77cd3ae156b650f7a63927c6628142ba4b65831892937b237b096b4f00f944436444d3078afc7b2c36f50cef5a17f392a48e2de43b6a656d75b
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## `2.0.34`
2
+
3
+ * Add badAttestationStatement error
4
+
5
+ ## `2.0.33`
6
+
7
+ * Add `Acme::Client::Problem` for structured RFC 7807 / RFC 8555 ACME problem documents
8
+ * Expose `Acme::Client::Error#problem` and problem field accessors while keeping `#acme_error_body` behavior
9
+ * Add typed error mappings for all currently registered IANA ACME error types and verify them against IANA's CSV registry
10
+
1
11
  ## `2.0.32`
2
12
 
3
13
  * 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,18 @@ 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
100
+ class BadAttestationStatement < ServerError; end
70
101
 
71
102
  ACME_ERRORS = {
72
103
  'urn:ietf:params:acme:error:alreadyReplaced' => AlreadyReplaced,
@@ -89,9 +120,17 @@ class Acme::Client::Error < StandardError
89
120
  'urn:ietf:params:acme:error:userActionRequired' => UserActionRequired,
90
121
  'urn:ietf:params:acme:error:badRevocationReason' => BadRevocationReason,
91
122
  'urn:ietf:params:acme:error:caa' => Caa,
123
+ 'urn:ietf:params:acme:error:compound' => Compound,
92
124
  'urn:ietf:params:acme:error:dns' => Dns,
93
125
  'urn:ietf:params:acme:error:connection' => Connection,
94
126
  'urn:ietf:params:acme:error:tls' => Tls,
95
- 'urn:ietf:params:acme:error:incorrectResponse' => IncorrectResponse
127
+ 'urn:ietf:params:acme:error:incorrectResponse' => IncorrectResponse,
128
+ 'urn:ietf:params:acme:error:autoRenewalCanceled' => AutoRenewalCanceled,
129
+ 'urn:ietf:params:acme:error:autoRenewalExpired' => AutoRenewalExpired,
130
+ 'urn:ietf:params:acme:error:autoRenewalCancellationInvalid' => AutoRenewalCancellationInvalid,
131
+ 'urn:ietf:params:acme:error:autoRenewalRevocationNotSupported' => AutoRenewalRevocationNotSupported,
132
+ 'urn:ietf:params:acme:error:unknownDelegation' => UnknownDelegation,
133
+ 'urn:ietf:params:acme:error:onionCAARequired' => OnionCAARequired,
134
+ 'urn:ietf:params:acme:error:badAttestationStatement' => BadAttestationStatement,
96
135
  }
97
136
  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,135 @@
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
+ 'badAttestationStatement' => 'The attestation statement is unacceptable (e.g. not signed by an attestation authority trusted by the CA)'
43
+ }.freeze
44
+
45
+ def self.from(value)
46
+ case value
47
+ when nil
48
+ nil
49
+ when Acme::Client::Problem
50
+ value
51
+ when Hash
52
+ new(value)
53
+ end
54
+ end
55
+
56
+ attr_reader :raw
57
+
58
+ def initialize(raw = {})
59
+ @raw = raw || {}
60
+ end
61
+
62
+ def type
63
+ fetch('type')
64
+ end
65
+
66
+ def code
67
+ return unless type
68
+ return unless type.start_with?(ERROR_PREFIX)
69
+
70
+ type[ERROR_PREFIX.length..-1]
71
+ end
72
+
73
+ def title
74
+ fetch('title')
75
+ end
76
+
77
+ def detail
78
+ fetch('detail')
79
+ end
80
+
81
+ def status
82
+ fetch('status')
83
+ end
84
+
85
+ def instance
86
+ fetch('instance')
87
+ end
88
+
89
+ def identifier
90
+ fetch('identifier')
91
+ end
92
+
93
+ def subproblems
94
+ return [] unless raw_subproblems.is_a?(Array)
95
+
96
+ raw_subproblems.map { |subproblem| self.class.new(subproblem) }
97
+ end
98
+
99
+ def raw_subproblems
100
+ fetch('subproblems')
101
+ end
102
+
103
+ def registered?
104
+ REGISTERED_ERROR_TYPE_DESCRIPTIONS.key?(code)
105
+ end
106
+
107
+ alias standard? registered?
108
+
109
+ def description
110
+ REGISTERED_ERROR_TYPE_DESCRIPTIONS[code]
111
+ end
112
+
113
+ def matches?(type_or_code)
114
+ candidate = type_or_code.to_s
115
+ candidate == type || candidate == code || "#{ERROR_PREFIX}#{candidate}" == type
116
+ end
117
+
118
+ def message
119
+ detail || title || type
120
+ end
121
+
122
+ def to_h
123
+ raw
124
+ end
125
+
126
+ def as_json(*_arguments)
127
+ to_h
128
+ end
129
+
130
+ private
131
+
132
+ def fetch(key)
133
+ raw[key] || raw[key.to_sym]
134
+ end
135
+ 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.34'.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.34
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