/ letsencrypt / client.py
client.py
  1  """Let's Encrypt client API."""
  2  import logging
  3  import os
  4  
  5  from cryptography.hazmat.backends import default_backend
  6  from cryptography.hazmat.primitives.asymmetric import rsa
  7  import OpenSSL
  8  
  9  from acme import client as acme_client
 10  from acme import jose
 11  from acme import messages
 12  
 13  from letsencrypt import account
 14  from letsencrypt import auth_handler
 15  from letsencrypt import configuration
 16  from letsencrypt import constants
 17  from letsencrypt import continuity_auth
 18  from letsencrypt import crypto_util
 19  from letsencrypt import errors
 20  from letsencrypt import error_handler
 21  from letsencrypt import le_util
 22  from letsencrypt import reverter
 23  from letsencrypt import storage
 24  
 25  from letsencrypt.display import ops as display_ops
 26  from letsencrypt.display import enhancements
 27  
 28  
 29  logger = logging.getLogger(__name__)
 30  
 31  
 32  def _acme_from_config_key(config, key):
 33      # TODO: Allow for other alg types besides RS256
 34      return acme_client.Client(directory=config.server, key=key,
 35                                verify_ssl=(not config.no_verify_ssl))
 36  
 37  
 38  def register(config, account_storage, tos_cb=None):
 39      """Register new account with an ACME CA.
 40  
 41      This function takes care of generating fresh private key,
 42      registering the account, optionally accepting CA Terms of Service
 43      and finally saving the account. It should be called prior to
 44      initialization of `Client`, unless account has already been created.
 45  
 46      :param .IConfig config: Client configuration.
 47  
 48      :param .AccountStorage account_storage: Account storage where newly
 49          registered account will be saved to. Save happens only after TOS
 50          acceptance step, so any account private keys or
 51          `.RegistrationResource` will not be persisted if `tos_cb`
 52          returns ``False``.
 53  
 54      :param tos_cb: If ACME CA requires the user to accept a Terms of
 55          Service before registering account, client action is
 56          necessary. For example, a CLI tool would prompt the user
 57          acceptance. `tos_cb` must be a callable that should accept
 58          `.RegistrationResource` and return a `bool`: ``True`` iff the
 59          Terms of Service present in the contained
 60          `.Registration.terms_of_service` is accepted by the client, and
 61          ``False`` otherwise. ``tos_cb`` will be called only if the
 62          client acction is necessary, i.e. when ``terms_of_service is not
 63          None``. This argument is optional, if not supplied it will
 64          default to automatic acceptance!
 65  
 66      :raises letsencrypt.errors.Error: In case of any client problems, in
 67          particular registration failure, or unaccepted Terms of Service.
 68      :raises acme.errors.Error: In case of any protocol problems.
 69  
 70      :returns: Newly registered and saved account, as well as protocol
 71          API handle (should be used in `Client` initialization).
 72      :rtype: `tuple` of `.Account` and `acme.client.Client`
 73  
 74      """
 75      # Log non-standard actions, potentially wrong API calls
 76      if account_storage.find_all():
 77          logger.info("There are already existing accounts for %s", config.server)
 78      if config.email is None:
 79          logger.warn("Registering without email!")
 80  
 81      # Each new registration shall use a fresh new key
 82      key = jose.JWKRSA(key=jose.ComparableRSAKey(
 83          rsa.generate_private_key(
 84              public_exponent=65537,
 85              key_size=config.rsa_key_size,
 86              backend=default_backend())))
 87      acme = _acme_from_config_key(config, key)
 88      # TODO: add phone?
 89      regr = acme.register(messages.NewRegistration.from_data(email=config.email))
 90  
 91      if regr.terms_of_service is not None:
 92          if tos_cb is not None and not tos_cb(regr):
 93              raise errors.Error(
 94                  "Registration cannot proceed without accepting "
 95                  "Terms of Service.")
 96          regr = acme.agree_to_tos(regr)
 97  
 98      acc = account.Account(regr, key)
 99      account.report_new_account(acc, config)
100      account_storage.save(acc)
101      return acc, acme
102  
103  
104  class Client(object):
105      """ACME protocol client.
106  
107      :ivar .IConfig config: Client configuration.
108      :ivar .Account account: Account registered with `register`.
109      :ivar .AuthHandler auth_handler: Authorizations handler that will
110          dispatch DV and Continuity challenges to appropriate
111          authenticators (providing `.IAuthenticator` interface).
112      :ivar .IAuthenticator dv_auth: Prepared (`.IAuthenticator.prepare`)
113          authenticator that can solve the `.constants.DV_CHALLENGES`.
114      :ivar .IInstaller installer: Installer.
115      :ivar acme.client.Client acme: Optional ACME client API handle.
116         You might already have one from `register`.
117  
118      """
119  
120      def __init__(self, config, account_, dv_auth, installer, acme=None):
121          """Initialize a client."""
122          self.config = config
123          self.account = account_
124          self.dv_auth = dv_auth
125          self.installer = installer
126  
127          # Initialize ACME if account is provided
128          if acme is None and self.account is not None:
129              acme = _acme_from_config_key(config, self.account.key)
130          self.acme = acme
131  
132          # TODO: Check if self.config.enroll_autorenew is None. If
133          # so, set it based to the default: figure out if dv_auth is
134          # standalone (then default is False, otherwise default is True)
135  
136          if dv_auth is not None:
137              cont_auth = continuity_auth.ContinuityAuthenticator(config,
138                                                                  installer)
139              self.auth_handler = auth_handler.AuthHandler(
140                  dv_auth, cont_auth, self.acme, self.account)
141          else:
142              self.auth_handler = None
143  
144      def _obtain_certificate(self, domains, csr):
145          """Obtain certificate.
146  
147          Internal function with precondition that `domains` are
148          consistent with identifiers present in the `csr`.
149  
150          :param list domains: Domain names.
151          :param .le_util.CSR csr: DER-encoded Certificate Signing
152              Request. The key used to generate this CSR can be different
153              than `authkey`.
154  
155          :returns: `.CertificateResource` and certificate chain (as
156              returned by `.fetch_chain`).
157          :rtype: tuple
158  
159          """
160          if self.auth_handler is None:
161              msg = ("Unable to obtain certificate because authenticator is "
162                     "not set.")
163              logger.warning(msg)
164              raise errors.Error(msg)
165          if self.account.regr is None:
166              raise errors.Error("Please register with the ACME server first.")
167  
168          logger.debug("CSR: %s, domains: %s", csr, domains)
169  
170          authzr = self.auth_handler.get_authorizations(domains)
171          certr = self.acme.request_issuance(
172              jose.ComparableX509(OpenSSL.crypto.load_certificate_request(
173                  OpenSSL.crypto.FILETYPE_ASN1, csr.data)),
174              authzr)
175          return certr, self.acme.fetch_chain(certr)
176  
177      def obtain_certificate_from_csr(self, csr):
178          """Obtain certficiate from CSR.
179  
180          :param .le_util.CSR csr: DER-encoded Certificate Signing
181              Request.
182  
183          :returns: `.CertificateResource` and certificate chain (as
184              returned by `.fetch_chain`).
185          :rtype: tuple
186  
187          """
188          return self._obtain_certificate(
189              # TODO: add CN to domains?
190              crypto_util.get_sans_from_csr(
191                  csr.data, OpenSSL.crypto.FILETYPE_ASN1), csr)
192  
193      def obtain_certificate(self, domains):
194          """Obtains a certificate from the ACME server.
195  
196          `.register` must be called before `.obtain_certificate`
197  
198          :param set domains: domains to get a certificate
199  
200          :returns: `.CertificateResource`, certificate chain (as
201              returned by `.fetch_chain`), and newly generated private key
202              (`.le_util.Key`) and DER-encoded Certificate Signing Request
203              (`.le_util.CSR`).
204          :rtype: tuple
205  
206          """
207          # Create CSR from names
208          key = crypto_util.init_save_key(
209              self.config.rsa_key_size, self.config.key_dir)
210          csr = crypto_util.init_save_csr(key, domains, self.config.csr_dir)
211  
212          return self._obtain_certificate(domains, csr) + (key, csr)
213  
214      def obtain_and_enroll_certificate(self, domains, plugins):
215          """Obtain and enroll certificate.
216  
217          Get a new certificate for the specified domains using the specified
218          authenticator and installer, and then create a new renewable lineage
219          containing it.
220  
221          :param list domains: Domains to request.
222          :param plugins: A PluginsFactory object.
223  
224          :returns: A new :class:`letsencrypt.storage.RenewableCert` instance
225              referred to the enrolled cert lineage, or False if the cert could
226              not be obtained.
227  
228          """
229          certr, chain, key, _ = self.obtain_certificate(domains)
230  
231          # TODO: remove this dirty hack
232          self.config.namespace.authenticator = plugins.find_init(
233              self.dv_auth).name
234          if self.installer is not None:
235              self.config.namespace.installer = plugins.find_init(
236                  self.installer).name
237  
238          # XXX: We clearly need a more general and correct way of getting
239          # options into the configobj for the RenewableCert instance.
240          # This is a quick-and-dirty way to do it to allow integration
241          # testing to start.  (Note that the config parameter to new_lineage
242          # ideally should be a ConfigObj, but in this case a dict will be
243          # accepted in practice.)
244          params = vars(self.config.namespace)
245          config = {}
246          cli_config = configuration.RenewerConfiguration(self.config.namespace)
247  
248          if (cli_config.config_dir != constants.CLI_DEFAULTS["config_dir"] or
249                  cli_config.work_dir != constants.CLI_DEFAULTS["work_dir"]):
250              logger.warning(
251                  "Non-standard path(s), might not work with crontab installed "
252                  "by your operating system package manager")
253  
254          lineage = storage.RenewableCert.new_lineage(
255              domains[0], OpenSSL.crypto.dump_certificate(
256                  OpenSSL.crypto.FILETYPE_PEM, certr.body),
257              key.pem, crypto_util.dump_pyopenssl_chain(chain),
258              params, config, cli_config)
259          return lineage
260  
261      def save_certificate(self, certr, chain_cert,
262                           cert_path, chain_path, fullchain_path):
263          """Saves the certificate received from the ACME server.
264  
265          :param certr: ACME "certificate" resource.
266          :type certr: :class:`acme.messages.Certificate`
267  
268          :param list chain_cert:
269          :param str cert_path: Candidate path to a certificate.
270          :param str chain_path: Candidate path to a certificate chain.
271          :param str fullchain_path: Candidate path to a full cert chain.
272  
273          :returns: cert_path, chain_path, and fullchain_path as absolute
274              paths to the actual files
275          :rtype: `tuple` of `str`
276  
277          :raises IOError: If unable to find room to write the cert files
278  
279          """
280          for path in cert_path, chain_path, fullchain_path:
281              le_util.make_or_verify_dir(
282                  os.path.dirname(path), 0o755, os.geteuid(),
283                  self.config.strict_permissions)
284  
285          cert_pem = OpenSSL.crypto.dump_certificate(
286              OpenSSL.crypto.FILETYPE_PEM, certr.body)
287          cert_file, act_cert_path = le_util.unique_file(cert_path, 0o644)
288          try:
289              cert_file.write(cert_pem)
290          finally:
291              cert_file.close()
292          logger.info("Server issued certificate; certificate written to %s",
293                      act_cert_path)
294  
295          cert_chain_abspath = None
296          fullchain_abspath = None
297          if chain_cert:
298              chain_pem = crypto_util.dump_pyopenssl_chain(chain_cert)
299              cert_chain_abspath = _save_chain(chain_pem, chain_path)
300              fullchain_abspath = _save_chain(cert_pem + chain_pem,
301                                              fullchain_path)
302  
303          return os.path.abspath(act_cert_path), cert_chain_abspath, fullchain_abspath
304  
305      def deploy_certificate(self, domains, privkey_path,
306                             cert_path, chain_path, fullchain_path):
307          """Install certificate
308  
309          :param list domains: list of domains to install the certificate
310          :param str privkey_path: path to certificate private key
311          :param str cert_path: certificate file path (optional)
312          :param str chain_path: chain file path
313  
314          """
315          if self.installer is None:
316              logger.warning("No installer specified, client is unable to deploy"
317                             "the certificate")
318              raise errors.Error("No installer available")
319  
320          chain_path = None if chain_path is None else os.path.abspath(chain_path)
321  
322          with error_handler.ErrorHandler(self.installer.recovery_routine):
323              for dom in domains:
324                  self.installer.deploy_cert(
325                      domain=dom, cert_path=os.path.abspath(cert_path),
326                      key_path=os.path.abspath(privkey_path),
327                      chain_path=chain_path,
328                      fullchain_path=fullchain_path)
329  
330              self.installer.save("Deployed Let's Encrypt Certificate")
331              # sites may have been enabled / final cleanup
332              self.installer.restart()
333  
334      def enhance_config(self, domains, redirect=None):
335          """Enhance the configuration.
336  
337          .. todo:: This needs to handle the specific enhancements offered by the
338              installer. We will also have to find a method to pass in the chosen
339              values efficiently.
340  
341          :param list domains: list of domains to configure
342  
343          :param redirect: If traffic should be forwarded from HTTP to HTTPS.
344          :type redirect: bool or None
345  
346          :raises .errors.Error: if no installer is specified in the
347              client.
348  
349          """
350          if self.installer is None:
351              logger.warning("No installer is specified, there isn't any "
352                             "configuration to enhance.")
353              raise errors.Error("No installer available")
354  
355          if redirect is None:
356              redirect = enhancements.ask("redirect")
357  
358          # When support for more enhancements are added, the call to the
359          # plugin's `enhance` function should be wrapped by an ErrorHandler
360          if redirect:
361              self.redirect_to_ssl(domains)
362  
363      def redirect_to_ssl(self, domains):
364          """Redirect all traffic from HTTP to HTTPS
365  
366          :param vhost: list of ssl_vhosts
367          :type vhost: :class:`letsencrypt.interfaces.IInstaller`
368  
369          """
370          with error_handler.ErrorHandler(self.installer.recovery_routine):
371              for dom in domains:
372                  try:
373                      self.installer.enhance(dom, "redirect")
374                  except errors.PluginError:
375                      logger.warn("Unable to perform redirect for %s", dom)
376                      raise
377  
378              self.installer.save("Add Redirects")
379              self.installer.restart()
380  
381  
382  def validate_key_csr(privkey, csr=None):
383      """Validate Key and CSR files.
384  
385      Verifies that the client key and csr arguments are valid and correspond to
386      one another. This does not currently check the names in the CSR due to
387      the inability to read SANs from CSRs in python crypto libraries.
388  
389      If csr is left as None, only the key will be validated.
390  
391      :param privkey: Key associated with CSR
392      :type privkey: :class:`letsencrypt.le_util.Key`
393  
394      :param .le_util.CSR csr: CSR
395  
396      :raises .errors.Error: when validation fails
397  
398      """
399      # TODO: Handle all of these problems appropriately
400      # The client can eventually do things like prompt the user
401      # and allow the user to take more appropriate actions
402  
403      # Key must be readable and valid.
404      if privkey.pem and not crypto_util.valid_privkey(privkey.pem):
405          raise errors.Error("The provided key is not a valid key")
406  
407      if csr:
408          if csr.form == "der":
409              csr_obj = OpenSSL.crypto.load_certificate_request(
410                  OpenSSL.crypto.FILETYPE_ASN1, csr.data)
411              csr = le_util.CSR(csr.file, OpenSSL.crypto.dump_certificate(
412                  OpenSSL.crypto.FILETYPE_PEM, csr_obj), "pem")
413  
414          # If CSR is provided, it must be readable and valid.
415          if csr.data and not crypto_util.valid_csr(csr.data):
416              raise errors.Error("The provided CSR is not a valid CSR")
417  
418          # If both CSR and key are provided, the key must be the same key used
419          # in the CSR.
420          if csr.data and privkey.pem:
421              if not crypto_util.csr_matches_pubkey(
422                      csr.data, privkey.pem):
423                  raise errors.Error("The key and CSR do not match")
424  
425  
426  def rollback(default_installer, checkpoints, config, plugins):
427      """Revert configuration the specified number of checkpoints.
428  
429      :param int checkpoints: Number of checkpoints to revert.
430  
431      :param config: Configuration.
432      :type config: :class:`letsencrypt.interfaces.IConfig`
433  
434      """
435      # Misconfigurations are only a slight problems... allow the user to rollback
436      installer = display_ops.pick_installer(
437          config, default_installer, plugins, question="Which installer "
438          "should be used for rollback?")
439  
440      # No Errors occurred during init... proceed normally
441      # If installer is None... couldn't find an installer... there shouldn't be
442      # anything to rollback
443      if installer is not None:
444          installer.rollback_checkpoints(checkpoints)
445          installer.restart()
446  
447  
448  def view_config_changes(config):
449      """View checkpoints and associated configuration changes.
450  
451      .. note:: This assumes that the installation is using a Reverter object.
452  
453      :param config: Configuration.
454      :type config: :class:`letsencrypt.interfaces.IConfig`
455  
456      """
457      rev = reverter.Reverter(config)
458      rev.recovery_routine()
459      rev.view_config_changes()
460  
461  
462  def _save_chain(chain_pem, chain_path):
463      """Saves chain_pem at a unique path based on chain_path.
464  
465      :param str chain_pem: certificate chain in PEM format
466      :param str chain_path: candidate path for the cert chain
467  
468      :returns: absolute path to saved cert chain
469      :rtype: str
470  
471      """
472      chain_file, act_chain_path = le_util.unique_file(chain_path, 0o644)
473      try:
474          chain_file.write(chain_pem)
475      finally:
476          chain_file.close()
477  
478      logger.info("Cert chain written to %s", act_chain_path)
479  
480      # This expects a valid chain file
481      return os.path.abspath(act_chain_path)