acme-client 1.0.0 → 2.0.0

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.
@@ -104,8 +104,6 @@ class Acme::Client::CertificateRequest
104
104
  end
105
105
 
106
106
  def add_extension(csr)
107
- return if @names.size <= 1
108
-
109
107
  extension = OpenSSL::X509::ExtensionFactory.new.create_extension(
110
108
  'subjectAltName', @names.map { |name| "DNS:#{name}" }.join(', '), false
111
109
  )
@@ -1,16 +1,55 @@
1
1
  class Acme::Client::Error < StandardError
2
- class NotFound < Acme::Client::Error; end
3
- class BadCSR < Acme::Client::Error; end
4
- class BadNonce < Acme::Client::Error; end
5
- class Connection < Acme::Client::Error; end
6
- class Dnssec < Acme::Client::Error; end
7
- class Malformed < Acme::Client::Error; end
8
- class ServerInternal < Acme::Client::Error; end
9
- class Acme::Tls < Acme::Client::Error; end
10
- class Unauthorized < Acme::Client::Error; end
11
- class UnknownHost < Acme::Client::Error; end
12
2
  class Timeout < Acme::Client::Error; end
13
- class RateLimited < Acme::Client::Error; end
14
- class RejectedIdentifier < Acme::Client::Error; end
15
- class UnsupportedIdentifier < Acme::Client::Error; end
3
+
4
+ class ClientError < Acme::Client::Error; end
5
+ class InvalidDirectory < ClientError; end
6
+ class UnsupportedOperation < ClientError; end
7
+ class UnsupportedChallengeType < ClientError; end
8
+ class NotFound < ClientError; end
9
+ class CertificateNotReady < ClientError; end
10
+
11
+ class ServerError < Acme::Client::Error; end
12
+ class BadCSR < ServerError; end
13
+ class BadNonce < ServerError; end
14
+ class BadSignatureAlgorithm < ServerError; end
15
+ class InvalidContact < ServerError; end
16
+ class UnsupportedContact < ServerError; end
17
+ class ExternalAccountRequired < ServerError; end
18
+ class AccountDoesNotExist < ServerError; end
19
+ class Malformed < ServerError; end
20
+ class RateLimited < ServerError; end
21
+ class RejectedIdentifier < ServerError; end
22
+ class ServerInternal < ServerError; end
23
+ class Unauthorized < ServerError; end
24
+ class UnsupportedIdentifier < ServerError; end
25
+ class UserActionRequired < ServerError; end
26
+ class BadRevocationReason < ServerError; end
27
+ class Caa < ServerError; end
28
+ class Dns < ServerError; end
29
+ class Connection < ServerError; end
30
+ class Tls < ServerError; end
31
+ class IncorrectResponse < ServerError; end
32
+
33
+ ACME_ERRORS = {
34
+ 'urn:ietf:params:acme:error:badCSR' => BadCSR,
35
+ 'urn:ietf:params:acme:error:badNonce' => BadNonce,
36
+ 'urn:ietf:params:acme:error:badSignatureAlgorithm' => BadSignatureAlgorithm,
37
+ 'urn:ietf:params:acme:error:invalidContact' => InvalidContact,
38
+ 'urn:ietf:params:acme:error:unsupportedContact' => UnsupportedContact,
39
+ 'urn:ietf:params:acme:error:externalAccountRequired' => ExternalAccountRequired,
40
+ 'urn:ietf:params:acme:error:accountDoesNotExist' => AccountDoesNotExist,
41
+ 'urn:ietf:params:acme:error:malformed' => Malformed,
42
+ 'urn:ietf:params:acme:error:rateLimited' => RateLimited,
43
+ 'urn:ietf:params:acme:error:rejectedIdentifier' => RejectedIdentifier,
44
+ 'urn:ietf:params:acme:error:serverInternal' => ServerInternal,
45
+ 'urn:ietf:params:acme:error:unauthorized' => Unauthorized,
46
+ 'urn:ietf:params:acme:error:unsupportedIdentifier' => UnsupportedIdentifier,
47
+ 'urn:ietf:params:acme:error:userActionRequired' => UserActionRequired,
48
+ 'urn:ietf:params:acme:error:badRevocationReason' => BadRevocationReason,
49
+ 'urn:ietf:params:acme:error:caa' => Caa,
50
+ 'urn:ietf:params:acme:error:dns' => Dns,
51
+ 'urn:ietf:params:acme:error:connection' => Connection,
52
+ 'urn:ietf:params:acme:error:tls' => Tls,
53
+ 'urn:ietf:params:acme:error:incorrectResponse' => IncorrectResponse
54
+ }
16
55
  end
@@ -3,18 +3,20 @@
3
3
  class Acme::Client::FaradayMiddleware < Faraday::Middleware
4
4
  attr_reader :env, :response, :client
5
5
 
6
- repo_url = 'https://github.com/unixcharles/acme-client'
7
- USER_AGENT = "Acme::Client v#{Acme::Client::VERSION} (#{repo_url})".freeze
6
+ CONTENT_TYPE = 'application/jose+json'
8
7
 
9
- def initialize(app, client:)
8
+ def initialize(app, client:, mode:)
10
9
  super(app)
11
10
  @client = client
11
+ @mode = mode
12
12
  end
13
13
 
14
14
  def call(env)
15
15
  @env = env
16
- @env[:request_headers]['User-Agent'] = USER_AGENT
17
- @env.body = client.jwk.jws(header: { nonce: pop_nonce }, payload: env.body)
16
+ @env[:request_headers]['User-Agent'] = Acme::Client::USER_AGENT
17
+ @env[:request_headers]['Content-Type'] = CONTENT_TYPE
18
+
19
+ @env.body = client.jwk.jws(header: jws_header, payload: env.body)
18
20
  @app.call(env).on_complete { |response_env| on_complete(response_env) }
19
21
  rescue Faraday::TimeoutError, Faraday::ConnectionFailed
20
22
  raise Acme::Client::Error::Timeout
@@ -35,6 +37,12 @@ class Acme::Client::FaradayMiddleware < Faraday::Middleware
35
37
 
36
38
  private
37
39
 
40
+ def jws_header
41
+ headers = { nonce: pop_nonce, url: env.url.to_s }
42
+ headers[:kid] = client.kid if @mode == :kid
43
+ headers
44
+ end
45
+
38
46
  def raise_on_not_found!
39
47
  raise Acme::Client::Error::NotFound, env.url.to_s if env.status == 404
40
48
  end
@@ -52,29 +60,20 @@ class Acme::Client::FaradayMiddleware < Faraday::Middleware
52
60
  end
53
61
 
54
62
  def error_class
55
- if error_name && !error_name.empty? && Acme::Client::Error.const_defined?(error_name)
56
- Object.const_get("Acme::Client::Error::#{error_name}")
57
- else
58
- Acme::Client::Error
59
- end
63
+ Acme::Client::Error::ACME_ERRORS.fetch(error_name, Acme::Client::Error)
60
64
  end
61
65
 
62
66
  def error_name
63
67
  return unless env.body.is_a?(Hash)
64
68
  return unless env.body.key?('type')
65
-
66
- error_type_to_klass env.body['type']
67
- end
68
-
69
- def error_type_to_klass(type)
70
- type.gsub('urn:acme:error:', '').split(/[_-]/).map { |type_part| type_part[0].upcase + type_part[1..-1] }.join
69
+ env.body['type']
71
70
  end
72
71
 
73
72
  def decode_body
74
- content_type = env.response_headers['Content-Type']
73
+ content_type = env.response_headers['Content-Type'].to_s
75
74
 
76
- if content_type == 'application/json' || content_type == 'application/problem+json'
77
- JSON.parse(env.body)
75
+ if content_type.start_with?('application/json', 'application/problem+json')
76
+ JSON.load(env.body)
78
77
  else
79
78
  env.body
80
79
  end
@@ -95,20 +94,20 @@ class Acme::Client::FaradayMiddleware < Faraday::Middleware
95
94
  end
96
95
 
97
96
  def store_nonce
98
- nonces << env.response_headers['replay-nonce']
97
+ nonce = env.response_headers['replay-nonce']
98
+ nonces << nonce if nonce
99
99
  end
100
100
 
101
101
  def pop_nonce
102
102
  if nonces.empty?
103
103
  get_nonce
104
- else
105
- nonces.pop
106
104
  end
105
+
106
+ nonces.pop
107
107
  end
108
108
 
109
109
  def get_nonce
110
- response = Faraday.head(env.url, nil, 'User-Agent' => USER_AGENT)
111
- response.headers['replay-nonce']
110
+ client.get_nonce
112
111
  end
113
112
 
114
113
  def nonces
@@ -15,7 +15,7 @@ class Acme::Client::JWK::Base
15
15
  #
16
16
  # Returns a JSON String.
17
17
  def jws(header: {}, payload: {})
18
- header = jws_header.merge(header)
18
+ header = jws_header(header)
19
19
  encoded_header = Acme::Client::Util.urlsafe_base64(header.to_json)
20
20
  encoded_payload = Acme::Client::Util.urlsafe_base64(payload.to_json)
21
21
 
@@ -56,12 +56,13 @@ class Acme::Client::JWK::Base
56
56
  # typ: - Value for the `typ` field. Default 'JWT'.
57
57
  #
58
58
  # Returns a Hash.
59
- def jws_header
60
- {
59
+ def jws_header(header)
60
+ jws = {
61
61
  typ: 'JWT',
62
- alg: jwa_alg,
63
- jwk: to_h
64
- }
62
+ alg: jwa_alg
63
+ }.merge(header)
64
+ jws[:jwk] = to_h if header[:kid].nil?
65
+ jws
65
66
  end
66
67
 
67
68
  # The name of the algorithm as needed for the `alg` member of a JWS object.
@@ -82,7 +82,6 @@ class Acme::Client::JWK::ECDSA < Acme::Client::JWK::Base
82
82
 
83
83
  private
84
84
 
85
- # rubocop:disable Metrics/AbcSize
86
85
  def coordinates
87
86
  @coordinates ||= begin
88
87
  hex = public_key.to_bn.to_s(16)
@@ -96,7 +95,6 @@ class Acme::Client::JWK::ECDSA < Acme::Client::JWK::Base
96
95
  }
97
96
  end
98
97
  end
99
- # rubocop:enable Metrics/AbcSize
100
98
 
101
99
  def public_key
102
100
  @private_key.public_key
@@ -1,5 +1,7 @@
1
1
  module Acme::Client::Resources; end
2
2
 
3
- require 'acme/client/resources/registration'
4
- require 'acme/client/resources/challenges'
3
+ require 'acme/client/resources/directory'
4
+ require 'acme/client/resources/account'
5
+ require 'acme/client/resources/order'
5
6
  require 'acme/client/resources/authorization'
7
+ require 'acme/client/resources/challenges'
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Acme::Client::Resources::Account
4
+ attr_reader :url, :status, :contact, :term_of_service, :orders_url
5
+
6
+ def initialize(client, **arguments)
7
+ @client = client
8
+ assign_attributes(arguments)
9
+ end
10
+
11
+ def kid
12
+ url
13
+ end
14
+
15
+ def update(contact: nil, terms_of_service_agreed: nil)
16
+ assign_attributes **@client.account_update(
17
+ contact: contact, terms_of_service_agreed: term_of_service
18
+ ).to_h
19
+ true
20
+ end
21
+
22
+ def deactivate
23
+ assign_attributes **@client.account_deactivate.to_h
24
+ true
25
+ end
26
+
27
+ def reload
28
+ assign_attributes **@client.account.to_h
29
+ true
30
+ end
31
+
32
+ def to_h
33
+ {
34
+ url: url,
35
+ term_of_service: term_of_service,
36
+ status: status,
37
+ contact: contact
38
+ }
39
+ end
40
+
41
+ private
42
+
43
+ def assign_attributes(url:, term_of_service:, status:, contact:)
44
+ @url = url
45
+ @term_of_service = term_of_service
46
+ @status = status
47
+ @contact = Array(contact)
48
+ end
49
+ end
@@ -1,44 +1,74 @@
1
- class Acme::Client::Resources::Authorization
2
- HTTP01 = Acme::Client::Resources::Challenges::HTTP01
3
- DNS01 = Acme::Client::Resources::Challenges::DNS01
4
- TLSSNI01 = Acme::Client::Resources::Challenges::TLSSNI01
1
+ # frozen_string_literal: true
5
2
 
6
- attr_reader :client, :uri, :domain, :status, :expires, :http01, :dns01, :tls_sni01
3
+ class Acme::Client::Resources::Authorization
4
+ attr_reader :url, :identifier, :domain, :expires, :status, :wildcard
7
5
 
8
- def initialize(client, uri, response)
6
+ def initialize(client, **arguments)
9
7
  @client = client
10
- @uri = uri
11
- assign_attributes(response.body)
8
+ assign_attributes(arguments)
9
+ end
10
+
11
+ def deactivate
12
+ assign_attributes **@client.deactivate_authorization(url: url).to_h
13
+ true
12
14
  end
13
15
 
14
- def verify_status
15
- response = @client.connection.get(@uri)
16
+ def reload
17
+ assign_attributes **@client.authorization(url: url).to_h
18
+ true
19
+ end
20
+
21
+ def challenges
22
+ @challenges.map do |challenge|
23
+ initialize_challenge(challenge)
24
+ end
25
+ end
16
26
 
17
- assign_attributes(response.body)
18
- status
27
+ def http01
28
+ @http01 ||= challenges.find { |challenge|
29
+ challenge.is_a?(Acme::Client::Resources::Challenges::HTTP01)
30
+ }
31
+ end
32
+ alias_method :http, :http01
33
+
34
+ def dns01
35
+ @dns01 ||= challenges.find { |challenge|
36
+ challenge.is_a?(Acme::Client::Resources::Challenges::DNS01)
37
+ }
38
+ end
39
+ alias_method :dns, :dns01
40
+
41
+ def to_h
42
+ {
43
+ url: url,
44
+ identifier: identifier,
45
+ status: status,
46
+ expires: expires,
47
+ challenges: @challenges,
48
+ wildcard: wildcard
49
+ }
19
50
  end
20
51
 
21
52
  private
22
53
 
23
- def assign_attributes(body)
24
- @expires = Time.iso8601(body['expires']) if body.key? 'expires'
25
- @domain = body['identifier']['value']
26
- @status = body['status']
27
- assign_challenges(body['challenges'])
28
- end
29
-
30
- def assign_challenges(challenges)
31
- challenges.each do |attributes|
32
- challenge = case attributes.fetch('type')
33
- when 'http-01'
34
- @http01 ||= HTTP01.new(self)
35
- when 'dns-01'
36
- @dns01 ||= DNS01.new(self)
37
- when 'tls-sni-01'
38
- @tls_sni01 ||= TLSSNI01.new(self)
39
- end
40
-
41
- challenge.assign_attributes(attributes) if challenge
42
- end
54
+ def initialize_challenge(attributes)
55
+ arguments = {
56
+ type: attributes.fetch('type'),
57
+ status: attributes.fetch('status'),
58
+ url: attributes.fetch('url'),
59
+ token: attributes.fetch('token'),
60
+ error: attributes['error']
61
+ }
62
+ Acme::Client::Resources::Challenges.new(@client, **arguments)
63
+ end
64
+
65
+ def assign_attributes(url:, status:, expires:, challenges:, identifier:, wildcard: false)
66
+ @url = url
67
+ @identifier = identifier
68
+ @domain = identifier.fetch('value')
69
+ @status = status
70
+ @expires = expires
71
+ @challenges = challenges
72
+ @wildcard = wildcard
43
73
  end
44
74
  end
@@ -1,6 +1,21 @@
1
- module Acme::Client::Resources::Challenges; end
1
+ # frozen_string_literal: true
2
2
 
3
- require 'acme/client/resources/challenges/base'
4
- require 'acme/client/resources/challenges/http01'
5
- require 'acme/client/resources/challenges/dns01'
6
- require 'acme/client/resources/challenges/tls_sni01'
3
+ module Acme::Client::Resources::Challenges
4
+ require 'acme/client/resources/challenges/base'
5
+ require 'acme/client/resources/challenges/http01'
6
+ require 'acme/client/resources/challenges/dns01'
7
+
8
+ CHALLENGE_TYPES = {
9
+ 'http-01' => Acme::Client::Resources::Challenges::HTTP01,
10
+ 'dns-01' => Acme::Client::Resources::Challenges::DNS01
11
+ }
12
+
13
+ def self.new(client, type:, **arguments)
14
+ klass = CHALLENGE_TYPES[type]
15
+ if klass
16
+ klass.new(client, **arguments)
17
+ else
18
+ { type: type }.merge(arguments)
19
+ end
20
+ end
21
+ end
@@ -1,39 +1,43 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Acme::Client::Resources::Challenges::Base
2
- attr_reader :authorization, :status, :uri, :token, :error
4
+ attr_reader :status, :url, :token, :error
3
5
 
4
- def initialize(authorization)
5
- @authorization = authorization
6
+ def initialize(client, **arguments)
7
+ @client = client
8
+ assign_attributes(arguments)
6
9
  end
7
10
 
8
- def client
9
- authorization.client
11
+ def challenge_type
12
+ self.class::CHALLENGE_TYPE
10
13
  end
11
14
 
12
- def verify_status
13
- authorization.verify_status
15
+ def key_authorization
16
+ "#{token}.#{@client.jwk.thumbprint}"
17
+ end
14
18
 
15
- status
19
+ def reload
20
+ assign_attributes **@client.challenge(url: url).to_h
21
+ true
16
22
  end
17
23
 
18
- def request_verification
19
- response = client.connection.post(@uri, resource: 'challenge', type: challenge_type, keyAuthorization: authorization_key)
20
- response.success?
24
+ def request_validation
25
+ assign_attributes **@client.request_challenge_validation(
26
+ url: url, key_authorization: key_authorization
27
+ ).to_h
28
+ true
21
29
  end
22
30
 
23
- def assign_attributes(attributes)
24
- @status = attributes.fetch('status', 'pending')
25
- @uri = attributes.fetch('uri')
26
- @token = attributes.fetch('token')
27
- @error = attributes['error']
31
+ def to_h
32
+ { status: status, url: url, token: token, error: error }
28
33
  end
29
34
 
30
35
  private
31
36
 
32
- def challenge_type
33
- self.class::CHALLENGE_TYPE
34
- end
35
-
36
- def authorization_key
37
- "#{token}.#{client.jwk.thumbprint}"
37
+ def assign_attributes(status:, url:, token:, error: nil)
38
+ @status = status
39
+ @url = url
40
+ @token = token
41
+ @error = error
38
42
  end
39
43
  end