Class: Acme::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/acme/client.rb,
lib/acme/client.rb,
lib/acme/client/version.rb,
lib/acme/client/chain_identifier.rb

Defined Under Namespace

Modules: JWK, Resources, Util Classes: CertificateRequest, ChainIdentifier, Error, FaradayMiddleware, SelfSignCertificate

Constant Summary collapse

DEFAULT_DIRECTORY =
'http://127.0.0.1:4000/directory'.freeze
USER_AGENT =
"Acme::Client v#{Acme::Client::VERSION} (#{repo_url})".freeze
CONTENT_TYPES =
{
  pem: 'application/pem-certificate-chain'
}
VERSION =
'2.0.10'.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(jwk: nil, kid: nil, private_key: nil, directory: DEFAULT_DIRECTORY, connection_options: {}, bad_nonce_retry: 0) ⇒ Client

Returns a new instance of Client.



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/acme/client.rb', line 34

def initialize(jwk: nil, kid: nil, private_key: nil, directory: DEFAULT_DIRECTORY, connection_options: {}, bad_nonce_retry: 0)
  if jwk.nil? && private_key.nil?
    raise ArgumentError, 'must specify jwk or private_key'
  end

  @jwk = if jwk
    jwk
  else
    Acme::Client::JWK.from_private_key(private_key)
  end

  @kid, @connection_options = kid, connection_options
  @bad_nonce_retry = bad_nonce_retry
  @directory = Acme::Client::Resources::Directory.new(URI(directory), @connection_options)
  @nonces ||= []
end

Instance Attribute Details

#jwkObject (readonly)

Returns the value of attribute jwk.



51
52
53
# File 'lib/acme/client.rb', line 51

def jwk
  @jwk
end

#noncesObject (readonly)

Returns the value of attribute nonces.



51
52
53
# File 'lib/acme/client.rb', line 51

def nonces
  @nonces
end

Instance Method Details

#accountObject



111
112
113
114
115
116
117
118
119
120
# File 'lib/acme/client.rb', line 111

def 
  @kid ||= begin
    response = post(endpoint_for(:new_account), payload: { onlyReturnExisting: true }, mode: :jwk)
    response.headers.fetch(:location)
  end

  response = post_as_get(@kid)
  arguments = (response)
  Acme::Client::Resources::Account.new(self, url: @kid, **arguments)
end

#account_deactivateObject



83
84
85
86
87
# File 'lib/acme/client.rb', line 83

def 
  response = post(kid, payload: { status: 'deactivated' })
  arguments = (response)
  Acme::Client::Resources::Account.new(self, url: kid, **arguments)
end

#account_key_change(new_private_key: nil, new_jwk: nil) ⇒ Object



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/acme/client.rb', line 89

def (new_private_key: nil, new_jwk: nil)
  if new_private_key.nil? && new_jwk.nil?
    raise ArgumentError, 'must specify new_jwk or new_private_key'
  end
  old_jwk = jwk
  new_jwk ||= Acme::Client::JWK.from_private_key(new_private_key)

  inner_payload_header = {
    url: endpoint_for(:key_change)
  }
  inner_payload = {
    account: kid,
    oldKey: old_jwk.to_h
  }
  payload = JSON.parse(new_jwk.jws(header: inner_payload_header, payload: inner_payload))

  response = post(endpoint_for(:key_change), payload: payload, mode: :kid)
  arguments = (response)
  @jwk = new_jwk
  Acme::Client::Resources::Account.new(self, url: kid, **arguments)
end

#account_update(contact: nil, terms_of_service_agreed: nil) ⇒ Object



73
74
75
76
77
78
79
80
81
# File 'lib/acme/client.rb', line 73

def (contact: nil, terms_of_service_agreed: nil)
  payload = {}
  payload[:contact] = Array(contact) if contact
  payload[:termsOfServiceAgreed] = terms_of_service_agreed if terms_of_service_agreed

  response = post(kid, payload: payload)
  arguments = (response)
  Acme::Client::Resources::Account.new(self, url: kid, **arguments)
end

#authorization(url:) ⇒ Object



174
175
176
177
178
# File 'lib/acme/client.rb', line 174

def authorization(url:)
  response = post_as_get(url)
  arguments = attributes_from_authorization_response(response)
  Acme::Client::Resources::Authorization.new(self, url: url, **arguments)
end

#caa_identitiesObject



232
233
234
# File 'lib/acme/client.rb', line 232

def caa_identities
  @directory.caa_identities
end

#certificate(url:, force_chain: nil) ⇒ Object



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/acme/client.rb', line 154

def certificate(url:, force_chain: nil)
  response = download(url, format: :pem)
  pem = response.body

  return pem if force_chain.nil?

  return pem if ChainIdentifier.new(pem).match_name?(force_chain)

  alternative_urls = Array(response.headers.dig('link', 'alternate'))
  alternative_urls.each do |alternate_url|
    response = download(alternate_url, format: :pem)
    pem = response.body
    if ChainIdentifier.new(pem).match_name?(force_chain)
      return pem
    end
  end

  raise Acme::Client::Error::ForcedChainNotFound, "Could not find any matching chain for `#{force_chain}`"
end

#challenge(url:) ⇒ Object



186
187
188
189
190
# File 'lib/acme/client.rb', line 186

def challenge(url:)
  response = post_as_get(url)
  arguments = attributes_from_challenge_response(response)
  Acme::Client::Resources::Challenges.new(self, **arguments)
end

#deactivate_authorization(url:) ⇒ Object



180
181
182
183
184
# File 'lib/acme/client.rb', line 180

def deactivate_authorization(url:)
  response = post(url, payload: { status: 'deactivated' })
  arguments = attributes_from_authorization_response(response)
  Acme::Client::Resources::Authorization.new(self, url: url, **arguments)
end

#external_account_requiredObject



236
237
238
# File 'lib/acme/client.rb', line 236

def 
  @directory.
end

#finalize(url:, csr:) ⇒ Object



143
144
145
146
147
148
149
150
151
152
# File 'lib/acme/client.rb', line 143

def finalize(url:, csr:)
  unless csr.respond_to?(:to_der)
    raise ArgumentError, 'csr must respond to `#to_der`'
  end

  base64_der_csr = Acme::Client::Util.urlsafe_base64(csr.to_der)
  response = post(url, payload: { csr: base64_der_csr })
  arguments = attributes_from_order_response(response)
  Acme::Client::Resources::Order.new(self, **arguments)
end

#get_nonceObject



213
214
215
216
217
218
# File 'lib/acme/client.rb', line 213

def get_nonce
  connection = new_connection(endpoint: endpoint_for(:new_nonce))
  response = connection.head(nil, nil, 'User-Agent' => USER_AGENT)
  nonces << response.headers['replay-nonce']
  true
end

#kidObject



122
123
124
# File 'lib/acme/client.rb', line 122

def kid
  @kid ||= .kid
end

#metaObject



220
221
222
# File 'lib/acme/client.rb', line 220

def meta
  @directory.meta
end

#new_account(contact:, terms_of_service_agreed: nil) ⇒ Object



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/acme/client.rb', line 53

def (contact:, terms_of_service_agreed: nil)
  payload = {
    contact: Array(contact)
  }

  if terms_of_service_agreed
    payload[:termsOfServiceAgreed] = terms_of_service_agreed
  end

  response = post(endpoint_for(:new_account), payload: payload, mode: :jws)
  @kid = response.headers.fetch(:location)

  if response.body.nil? || response.body.empty?
    
  else
    arguments = (response)
    Acme::Client::Resources::Account.new(self, url: @kid, **arguments)
  end
end

#new_order(identifiers:, not_before: nil, not_after: nil) ⇒ Object



126
127
128
129
130
131
132
133
134
135
# File 'lib/acme/client.rb', line 126

def new_order(identifiers:, not_before: nil, not_after: nil)
  payload = {}
  payload['identifiers'] = prepare_order_identifiers(identifiers)
  payload['notBefore'] = not_before if not_before
  payload['notAfter'] = not_after if not_after

  response = post(endpoint_for(:new_order), payload: payload)
  arguments = attributes_from_order_response(response)
  Acme::Client::Resources::Order.new(self, **arguments)
end

#order(url:) ⇒ Object



137
138
139
140
141
# File 'lib/acme/client.rb', line 137

def order(url:)
  response = post_as_get(url)
  arguments = attributes_from_order_response(response)
  Acme::Client::Resources::Order.new(self, **arguments.merge(url: url))
end

#request_challenge_validation(url:, key_authorization: nil) ⇒ Object



192
193
194
195
196
# File 'lib/acme/client.rb', line 192

def request_challenge_validation(url:, key_authorization: nil)
  response = post(url, payload: {})
  arguments = attributes_from_challenge_response(response)
  Acme::Client::Resources::Challenges.new(self, **arguments)
end

#revoke(certificate:, reason: nil) ⇒ Object



198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/acme/client.rb', line 198

def revoke(certificate:, reason: nil)
  der_certificate = if certificate.respond_to?(:to_der)
    certificate.to_der
  else
    OpenSSL::X509::Certificate.new(certificate).to_der
  end

  base64_der_certificate = Acme::Client::Util.urlsafe_base64(der_certificate)
  payload = { certificate: base64_der_certificate }
  payload[:reason] = reason unless reason.nil?

  response = post(endpoint_for(:revoke_certificate), payload: payload)
  response.success?
end

#terms_of_serviceObject



224
225
226
# File 'lib/acme/client.rb', line 224

def terms_of_service
  @directory.terms_of_service
end

#websiteObject



228
229
230
# File 'lib/acme/client.rb', line 228

def website
  @directory.website
end