/ 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