acme-client 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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