/ letsencrypt / storage.py
storage.py
  1  """Renewable certificates storage."""
  2  import datetime
  3  import os
  4  import re
  5  import time
  6  
  7  import configobj
  8  import parsedatetime
  9  import pytz
 10  
 11  from letsencrypt import constants
 12  from letsencrypt import crypto_util
 13  from letsencrypt import errors
 14  from letsencrypt import error_handler
 15  from letsencrypt import le_util
 16  
 17  ALL_FOUR = ("cert", "privkey", "chain", "fullchain")
 18  
 19  
 20  def config_with_defaults(config=None):
 21      """Merge supplied config, if provided, on top of builtin defaults."""
 22      defaults_copy = configobj.ConfigObj(constants.RENEWER_DEFAULTS)
 23      defaults_copy.merge(config if config is not None else configobj.ConfigObj())
 24      return defaults_copy
 25  
 26  
 27  def parse_time_interval(interval, textparser=parsedatetime.Calendar()):
 28      """Parse the time specified time interval.
 29  
 30      The interval can be in the English-language format understood by
 31      parsedatetime, e.g., '10 days', '3 weeks', '6 months', '9 hours', or
 32      a sequence of such intervals like '6 months 1 week' or '3 days 12
 33      hours'. If an integer is found with no associated unit, it is
 34      interpreted by default as a number of days.
 35  
 36      :param str interval: The time interval to parse.
 37  
 38      :returns: The interpretation of the time interval.
 39      :rtype: :class:`datetime.timedelta`"""
 40  
 41      if interval.strip().isdigit():
 42          interval += " days"
 43      return datetime.timedelta(0, time.mktime(textparser.parse(
 44          interval, time.localtime(0))[0]))
 45  
 46  
 47  class RenewableCert(object):  # pylint: disable=too-many-instance-attributes
 48      """Renewable certificate.
 49  
 50      Represents a lineage of certificates that is under the management
 51      of the Let's Encrypt client, indicated by the existence of an
 52      associated renewal configuration file.
 53  
 54      Note that the notion of "current version" for a lineage is
 55      maintained on disk in the structure of symbolic links, and is not
 56      explicitly stored in any instance variable in this object. The
 57      RenewableCert object is able to determine information about the
 58      current (or other) version by accessing data on disk, but does not
 59      inherently know any of this information except by examining the
 60      symbolic links as needed. The instance variables mentioned below
 61      point to symlinks that reflect the notion of "current version" of
 62      each managed object, and it is these paths that should be used when
 63      configuring servers to use the certificate managed in a lineage.
 64      These paths are normally within the "live" directory, and their
 65      symlink targets -- the actual cert files -- are normally found
 66      within the "archive" directory.
 67  
 68      :ivar str cert: The path to the symlink representing the current
 69          version of the certificate managed by this lineage.
 70      :ivar str privkey: The path to the symlink representing the current
 71          version of the private key managed by this lineage.
 72      :ivar str chain: The path to the symlink representing the current version
 73          of the chain managed by this lineage.
 74      :ivar str fullchain: The path to the symlink representing the
 75          current version of the fullchain (combined chain and cert)
 76          managed by this lineage.
 77      :ivar configobj.ConfigObj configuration: The renewal configuration
 78          options associated with this lineage, obtained from parsing the
 79          renewal configuration file and/or systemwide defaults.
 80  
 81      """
 82      def __init__(self, config_filename, cli_config):
 83          """Instantiate a RenewableCert object from an existing lineage.
 84  
 85          :param str config_filename: the path to the renewal config file
 86              that defines this lineage.
 87          :param .RenewerConfiguration: parsed command line arguments
 88  
 89          :raises .CertStorageError: if the configuration file's name didn't end
 90              in ".conf", or the file is missing or broken.
 91  
 92          """
 93          self.cli_config = cli_config
 94          if not config_filename.endswith(".conf"):
 95              raise errors.CertStorageError(
 96                  "renewal config file name must end in .conf")
 97          self.lineagename = os.path.basename(
 98              config_filename[:-len(".conf")])
 99  
100          # self.configuration should be used to read parameters that
101          # may have been chosen based on default values from the
102          # systemwide renewal configuration; self.configfile should be
103          # used to make and save changes.
104          try:
105              self.configfile = configobj.ConfigObj(config_filename)
106          except configobj.ConfigObjError:
107              raise errors.CertStorageError(
108                  "error parsing {0}".format(config_filename))
109          # TODO: Do we actually use anything from defaults and do we want to
110          #       read further defaults from the systemwide renewal configuration
111          #       file at this stage?
112          self.configuration = config_with_defaults(self.configfile)
113  
114          if not all(x in self.configuration for x in ALL_FOUR):
115              raise errors.CertStorageError(
116                  "renewal config file {0} is missing a required "
117                  "file reference".format(self.configfile))
118  
119          self.cert = self.configuration["cert"]
120          self.privkey = self.configuration["privkey"]
121          self.chain = self.configuration["chain"]
122          self.fullchain = self.configuration["fullchain"]
123  
124          self._fix_symlinks()
125  
126      def _consistent(self):
127          """Are the files associated with this lineage self-consistent?
128  
129          :returns: Whether the files stored in connection with this
130              lineage appear to be correct and consistent with one
131              another.
132          :rtype: bool
133  
134          """
135          # Each element must be referenced with an absolute path
136          if any(not os.path.isabs(x) for x in
137                 (self.cert, self.privkey, self.chain, self.fullchain)):
138              return False
139  
140          # Each element must exist and be a symbolic link
141          if any(not os.path.islink(x) for x in
142                 (self.cert, self.privkey, self.chain, self.fullchain)):
143              return False
144          for kind in ALL_FOUR:
145              link = getattr(self, kind)
146              where = os.path.dirname(link)
147              target = os.readlink(link)
148              if not os.path.isabs(target):
149                  target = os.path.join(where, target)
150  
151              # Each element's link must point within the cert lineage's
152              # directory within the official archive directory
153              desired_directory = os.path.join(
154                  self.cli_config.archive_dir, self.lineagename)
155              if not os.path.samefile(os.path.dirname(target),
156                                      desired_directory):
157                  return False
158  
159              # The link must point to a file that exists
160              if not os.path.exists(target):
161                  return False
162  
163              # The link must point to a file that follows the archive
164              # naming convention
165              pattern = re.compile(r"^{0}([0-9]+)\.pem$".format(kind))
166              if not pattern.match(os.path.basename(target)):
167                  return False
168  
169              # It is NOT required that the link's target be a regular
170              # file (it may itself be a symlink). But we should probably
171              # do a recursive check that ultimately the target does
172              # exist?
173          # XXX: Additional possible consistency checks (e.g.
174          #      cryptographic validation of the chain being a chain,
175          #      the chain matching the cert, and the cert matching
176          #      the subject key)
177          # XXX: All four of the targets are in the same directory
178          #      (This check is redundant with the check that they
179          #      are all in the desired directory!)
180          #      len(set(os.path.basename(self.current_target(x)
181          #      for x in ALL_FOUR))) == 1
182          return True
183  
184      def _fix(self):
185          """Attempt to fix defects or inconsistencies in this lineage.
186  
187          .. todo:: Currently unimplemented.
188  
189          """
190          # TODO: Figure out what kinds of fixes are possible.  For
191          #       example, checking if there is a valid version that
192          #       we can update the symlinks to.  (Maybe involve
193          #       parsing keys and certs to see if they exist and
194          #       if a key corresponds to the subject key of a cert?)
195  
196      # TODO: In general, the symlink-reading functions below are not
197      #       cautious enough about the possibility that links or their
198      #       targets may not exist.  (This shouldn't happen, but might
199      #       happen as a result of random tampering by a sysadmin, or
200      #       filesystem errors, or crashes.)
201  
202      def _previous_symlinks(self):
203          """Returns the kind and path of all symlinks used in recovery.
204  
205          :returns: list of (kind, symlink) tuples
206          :rtype: list
207  
208          """
209          previous_symlinks = []
210          for kind in ALL_FOUR:
211              link_dir = os.path.dirname(getattr(self, kind))
212              link_base = "previous_{0}.pem".format(kind)
213              previous_symlinks.append((kind, os.path.join(link_dir, link_base)))
214  
215          return previous_symlinks
216  
217      def _fix_symlinks(self):
218          """Fixes symlinks in the event of an incomplete version update.
219  
220          If there is no problem with the current symlinks, this function
221          has no effect.
222  
223          """
224          previous_symlinks = self._previous_symlinks()
225          if all(os.path.exists(link[1]) for link in previous_symlinks):
226              for kind, previous_link in previous_symlinks:
227                  current_link = getattr(self, kind)
228                  if os.path.lexists(current_link):
229                      os.unlink(current_link)
230                  os.symlink(os.readlink(previous_link), current_link)
231  
232          for _, link in previous_symlinks:
233              if os.path.exists(link):
234                  os.unlink(link)
235  
236      def current_target(self, kind):
237          """Returns full path to which the specified item currently points.
238  
239          :param str kind: the lineage member item ("cert", "privkey",
240              "chain", or "fullchain")
241  
242          :returns: The path to the current version of the specified
243              member.
244          :rtype: str
245  
246          """
247          if kind not in ALL_FOUR:
248              raise errors.CertStorageError("unknown kind of item")
249          link = getattr(self, kind)
250          if not os.path.exists(link):
251              return None
252          target = os.readlink(link)
253          if not os.path.isabs(target):
254              target = os.path.join(os.path.dirname(link), target)
255          return os.path.abspath(target)
256  
257      def current_version(self, kind):
258          """Returns numerical version of the specified item.
259  
260          For example, if kind is "chain" and the current chain link
261          points to a file named "chain7.pem", returns the integer 7.
262  
263          :param str kind: the lineage member item ("cert", "privkey",
264              "chain", or "fullchain")
265  
266          :returns: the current version of the specified member.
267          :rtype: int
268  
269          """
270          if kind not in ALL_FOUR:
271              raise errors.CertStorageError("unknown kind of item")
272          pattern = re.compile(r"^{0}([0-9]+)\.pem$".format(kind))
273          target = self.current_target(kind)
274          if target is None or not os.path.exists(target):
275              target = ""
276          matches = pattern.match(os.path.basename(target))
277          if matches:
278              return int(matches.groups()[0])
279          else:
280              return None
281  
282      def version(self, kind, version):
283          """The filename that corresponds to the specified version and kind.
284  
285          .. warning:: The specified version may not exist in this
286             lineage. There is no guarantee that the file path returned
287             by this method actually exists.
288  
289          :param str kind: the lineage member item ("cert", "privkey",
290              "chain", or "fullchain")
291          :param int version: the desired version
292  
293          :returns: The path to the specified version of the specified member.
294          :rtype: str
295  
296          """
297          if kind not in ALL_FOUR:
298              raise errors.CertStorageError("unknown kind of item")
299          where = os.path.dirname(self.current_target(kind))
300          return os.path.join(where, "{0}{1}.pem".format(kind, version))
301  
302      def available_versions(self, kind):
303          """Which alternative versions of the specified kind of item exist?
304  
305          The archive directory where the current version is stored is
306          consulted to obtain the list of alternatives.
307  
308          :param str kind: the lineage member item (
309              ``cert``, ``privkey``, ``chain``, or ``fullchain``)
310  
311          :returns: all of the version numbers that currently exist
312          :rtype: `list` of `int`
313  
314          """
315          if kind not in ALL_FOUR:
316              raise errors.CertStorageError("unknown kind of item")
317          where = os.path.dirname(self.current_target(kind))
318          files = os.listdir(where)
319          pattern = re.compile(r"^{0}([0-9]+)\.pem$".format(kind))
320          matches = [pattern.match(f) for f in files]
321          return sorted([int(m.groups()[0]) for m in matches if m])
322  
323      def newest_available_version(self, kind):
324          """Newest available version of the specified kind of item?
325  
326          :param str kind: the lineage member item (``cert``,
327              ``privkey``, ``chain``, or ``fullchain``)
328  
329          :returns: the newest available version of this member
330          :rtype: int
331  
332          """
333          return max(self.available_versions(kind))
334  
335      def latest_common_version(self):
336          """Newest version for which all items are available?
337  
338          :returns: the newest available version for which all members
339              (``cert, ``privkey``, ``chain``, and ``fullchain``) exist
340          :rtype: int
341  
342          """
343          # TODO: this can raise CertStorageError if there is no version overlap
344          #       (it should probably return None instead)
345          # TODO: this can raise a spurious AttributeError if the current
346          #       link for any kind is missing (it should probably return None)
347          versions = [self.available_versions(x) for x in ALL_FOUR]
348          return max(n for n in versions[0] if all(n in v for v in versions[1:]))
349  
350      def next_free_version(self):
351          """Smallest version newer than all full or partial versions?
352  
353          :returns: the smallest version number that is larger than any
354              version of any item currently stored in this lineage
355          :rtype: int
356  
357          """
358          # TODO: consider locking/mutual exclusion between updating processes
359          # This isn't self.latest_common_version() + 1 because we don't want
360          # collide with a version that might exist for one file type but not
361          # for the others.
362          return max(self.newest_available_version(x) for x in ALL_FOUR) + 1
363  
364      def has_pending_deployment(self):
365          """Is there a later version of all of the managed items?
366  
367          :returns: ``True`` if there is a complete version of this
368              lineage with a larger version number than the current
369              version, and ``False`` otherwis
370          :rtype: bool
371  
372          """
373          # TODO: consider whether to assume consistency or treat
374          #       inconsistent/consistent versions differently
375          smallest_current = min(self.current_version(x) for x in ALL_FOUR)
376          return smallest_current < self.latest_common_version()
377  
378      def _update_link_to(self, kind, version):
379          """Make the specified item point at the specified version.
380  
381          (Note that this method doesn't verify that the specified version
382          exists.)
383  
384          :param str kind: the lineage member item ("cert", "privkey",
385              "chain", or "fullchain")
386          :param int version: the desired version
387  
388          """
389          if kind not in ALL_FOUR:
390              raise errors.CertStorageError("unknown kind of item")
391          link = getattr(self, kind)
392          filename = "{0}{1}.pem".format(kind, version)
393          # Relative rather than absolute target directory
394          target_directory = os.path.dirname(os.readlink(link))
395          # TODO: it could be safer to make the link first under a temporary
396          #       filename, then unlink the old link, then rename the new link
397          #       to the old link; this ensures that this process is able to
398          #       create symlinks.
399          # TODO: we might also want to check consistency of related links
400          #       for the other corresponding items
401          os.unlink(link)
402          os.symlink(os.path.join(target_directory, filename), link)
403  
404      def update_all_links_to(self, version):
405          """Change all member objects to point to the specified version.
406  
407          :param int version: the desired version
408  
409          """
410          with error_handler.ErrorHandler(self._fix_symlinks):
411              previous_links = self._previous_symlinks()
412              for kind, link in previous_links:
413                  os.symlink(self.current_target(kind), link)
414  
415              for kind in ALL_FOUR:
416                  self._update_link_to(kind, version)
417  
418              for _, link in previous_links:
419                  os.unlink(link)
420  
421      def names(self, version=None):
422          """What are the subject names of this certificate?
423  
424          (If no version is specified, use the current version.)
425  
426          :param int version: the desired version number
427          :returns: the subject names
428          :rtype: `list` of `str`
429  
430          """
431          if version is None:
432              target = self.current_target("cert")
433          else:
434              target = self.version("cert", version)
435          with open(target) as f:
436              return crypto_util.get_sans_from_cert(f.read())
437  
438      def autodeployment_is_enabled(self):
439          """Is automatic deployment enabled for this cert?
440  
441          If autodeploy is not specified, defaults to True.
442  
443          :returns: True if automatic deployment is enabled
444          :rtype: bool
445  
446          """
447          return ("autodeploy" not in self.configuration or
448                  self.configuration.as_bool("autodeploy"))
449  
450      def should_autodeploy(self):
451          """Should this lineage now automatically deploy a newer version?
452  
453          This is a policy question and does not only depend on whether
454          there is a newer version of the cert. (This considers whether
455          autodeployment is enabled, whether a relevant newer version
456          exists, and whether the time interval for autodeployment has
457          been reached.)
458  
459          :returns: whether the lineage now ought to autodeploy an
460              existing newer cert version
461          :rtype: bool
462  
463          """
464          if self.autodeployment_is_enabled():
465              if self.has_pending_deployment():
466                  interval = self.configuration.get("deploy_before_expiry",
467                                                    "5 days")
468                  autodeploy_interval = parse_time_interval(interval)
469                  expiry = crypto_util.notAfter(self.current_target("cert"))
470                  now = datetime.datetime.utcnow().replace(tzinfo=pytz.UTC)
471                  remaining = expiry - now
472                  if remaining < autodeploy_interval:
473                      return True
474          return False
475  
476      def ocsp_revoked(self, version=None):
477          # pylint: disable=no-self-use,unused-argument
478          """Is the specified cert version revoked according to OCSP?
479  
480          Also returns True if the cert version is declared as intended
481          to be revoked according to Let's Encrypt OCSP extensions.
482          (If no version is specified, uses the current version.)
483  
484          This method is not yet implemented and currently always returns
485          False.
486  
487          :param int version: the desired version number
488  
489          :returns: whether the certificate is or will be revoked
490          :rtype: bool
491  
492          """
493          # XXX: This query and its associated network service aren't
494          # implemented yet, so we currently return False (indicating that the
495          # certificate is not revoked).
496          return False
497  
498      def autorenewal_is_enabled(self):
499          """Is automatic renewal enabled for this cert?
500  
501          If autorenew is not specified, defaults to True.
502  
503          :returns: True if automatic renewal is enabled
504          :rtype: bool
505  
506          """
507          return ("autorenew" not in self.configuration or
508                  self.configuration.as_bool("autorenew"))
509  
510      def should_autorenew(self):
511          """Should we now try to autorenew the most recent cert version?
512  
513          This is a policy question and does not only depend on whether
514          the cert is expired. (This considers whether autorenewal is
515          enabled, whether the cert is revoked, and whether the time
516          interval for autorenewal has been reached.)
517  
518          Note that this examines the numerically most recent cert version,
519          not the currently deployed version.
520  
521          :returns: whether an attempt should now be made to autorenew the
522              most current cert version in this lineage
523          :rtype: bool
524  
525          """
526          if self.autorenewal_is_enabled():
527              # Consider whether to attempt to autorenew this cert now
528  
529              # Renewals on the basis of revocation
530              if self.ocsp_revoked(self.latest_common_version()):
531                  return True
532  
533              # Renewals on the basis of expiry time
534              interval = self.configuration.get("renew_before_expiry", "10 days")
535              autorenew_interval = parse_time_interval(interval)
536              expiry = crypto_util.notAfter(self.version(
537                  "cert", self.latest_common_version()))
538              now = datetime.datetime.utcnow().replace(tzinfo=pytz.UTC)
539              remaining = expiry - now
540              if remaining < autorenew_interval:
541                  return True
542          return False
543  
544      @classmethod
545      def new_lineage(cls, lineagename, cert, privkey, chain,
546                      renewalparams=None, config=None, cli_config=None):
547          # pylint: disable=too-many-locals,too-many-arguments
548          """Create a new certificate lineage.
549  
550          Attempts to create a certificate lineage -- enrolled for
551          potential future renewal -- with the (suggested) lineage name
552          lineagename, and the associated cert, privkey, and chain (the
553          associated fullchain will be created automatically). Optional
554          configurator and renewalparams record the configuration that was
555          originally used to obtain this cert, so that it can be reused
556          later during automated renewal.
557  
558          Returns a new RenewableCert object referring to the created
559          lineage. (The actual lineage name, as well as all the relevant
560          file paths, will be available within this object.)
561  
562          :param str lineagename: the suggested name for this lineage
563              (normally the current cert's first subject DNS name)
564          :param str cert: the initial certificate version in PEM format
565          :param str privkey: the private key in PEM format
566          :param str chain: the certificate chain in PEM format
567          :param configobj.ConfigObj renewalparams: parameters that
568              should be used when instantiating authenticator and installer
569              objects in the future to attempt to renew this cert or deploy
570              new versions of it
571          :param configobj.ConfigObj config: renewal configuration
572              defaults, affecting, for example, the locations of the
573              directories where the associated files will be saved
574          :param .RenewerConfiguration cli_config: parsed command line
575              arguments
576  
577          :returns: the newly-created RenewalCert object
578          :rtype: :class:`storage.renewableCert`"""
579  
580          config = config_with_defaults(config)
581          # This attempts to read the renewer config file and augment or replace
582          # the renewer defaults with any options contained in that file.  If
583          # renewer_config_file is undefined or if the file is nonexistent or
584          # empty, this .merge() will have no effect.
585          config.merge(configobj.ConfigObj(cli_config.renewer_config_file))
586  
587          # Examine the configuration and find the new lineage's name
588          for i in (cli_config.renewal_configs_dir, cli_config.archive_dir,
589                    cli_config.live_dir):
590              if not os.path.exists(i):
591                  os.makedirs(i, 0700)
592          config_file, config_filename = le_util.unique_lineage_name(
593              cli_config.renewal_configs_dir, lineagename)
594          if not config_filename.endswith(".conf"):
595              raise errors.CertStorageError(
596                  "renewal config file name must end in .conf")
597  
598          # Determine where on disk everything will go
599          # lineagename will now potentially be modified based on which
600          # renewal configuration file could actually be created
601          lineagename = os.path.basename(config_filename)[:-len(".conf")]
602          archive = os.path.join(cli_config.archive_dir, lineagename)
603          live_dir = os.path.join(cli_config.live_dir, lineagename)
604          if os.path.exists(archive):
605              raise errors.CertStorageError(
606                  "archive directory exists for " + lineagename)
607          if os.path.exists(live_dir):
608              raise errors.CertStorageError(
609                  "live directory exists for " + lineagename)
610          os.mkdir(archive)
611          os.mkdir(live_dir)
612          relative_archive = os.path.join("..", "..", "archive", lineagename)
613  
614          # Put the data into the appropriate files on disk
615          target = dict([(kind, os.path.join(live_dir, kind + ".pem"))
616                         for kind in ALL_FOUR])
617          for kind in ALL_FOUR:
618              os.symlink(os.path.join(relative_archive, kind + "1.pem"),
619                         target[kind])
620          with open(target["cert"], "w") as f:
621              f.write(cert)
622          with open(target["privkey"], "w") as f:
623              f.write(privkey)
624              # XXX: Let's make sure to get the file permissions right here
625          with open(target["chain"], "w") as f:
626              f.write(chain)
627          with open(target["fullchain"], "w") as f:
628              # assumes that OpenSSL.crypto.dump_certificate includes
629              # ending newline character
630              f.write(cert + chain)
631  
632          # Document what we've done in a new renewal config file
633          config_file.close()
634          new_config = configobj.ConfigObj(config_filename, create_empty=True)
635          for kind in ALL_FOUR:
636              new_config[kind] = target[kind]
637          if renewalparams:
638              new_config["renewalparams"] = renewalparams
639              new_config.comments["renewalparams"] = ["",
640                                                      "Options and defaults used"
641                                                      " in the renewal process"]
642          # TODO: add human-readable comments explaining other available
643          #       parameters
644          new_config.write()
645          return cls(new_config.filename, cli_config)
646  
647      def save_successor(self, prior_version, new_cert, new_privkey, new_chain):
648          """Save new cert and chain as a successor of a prior version.
649  
650          Returns the new version number that was created.
651  
652          .. note:: this function does NOT update links to deploy this
653                    version
654  
655          :param int prior_version: the old version to which this version
656              is regarded as a successor (used to choose a privkey, if the
657              key has not changed, but otherwise this information is not
658              permanently recorded anywhere)
659          :param str new_cert: the new certificate, in PEM format
660          :param str new_privkey: the new private key, in PEM format,
661              or ``None``, if the private key has not changed
662          :param str new_chain: the new chain, in PEM format
663  
664          :returns: the new version number that was created
665          :rtype: int
666  
667          """
668          # XXX: assumes official archive location rather than examining links
669          # XXX: consider using os.open for availability of os.O_EXCL
670          # XXX: ensure file permissions are correct; also create directories
671          #      if needed (ensuring their permissions are correct)
672          # Figure out what the new version is and hence where to save things
673  
674          target_version = self.next_free_version()
675          archive = self.cli_config.archive_dir
676          prefix = os.path.join(archive, self.lineagename)
677          target = dict(
678              [(kind,
679                os.path.join(prefix, "{0}{1}.pem".format(kind, target_version)))
680               for kind in ALL_FOUR])
681  
682          # Distinguish the cases where the privkey has changed and where it
683          # has not changed (in the latter case, making an appropriate symlink
684          # to an earlier privkey version)
685          if new_privkey is None:
686              # The behavior below keeps the prior key by creating a new
687              # symlink to the old key or the target of the old key symlink.
688              old_privkey = os.path.join(
689                  prefix, "privkey{0}.pem".format(prior_version))
690              if os.path.islink(old_privkey):
691                  old_privkey = os.readlink(old_privkey)
692              else:
693                  old_privkey = "privkey{0}.pem".format(prior_version)
694              os.symlink(old_privkey, target["privkey"])
695          else:
696              with open(target["privkey"], "w") as f:
697                  f.write(new_privkey)
698  
699          # Save everything else
700          with open(target["cert"], "w") as f:
701              f.write(new_cert)
702          with open(target["chain"], "w") as f:
703              f.write(new_chain)
704          with open(target["fullchain"], "w") as f:
705              f.write(new_cert + new_chain)
706          return target_version