Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for fifth PEM file that contains everything #9917

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions certbot/certbot/_internal/renewal.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,10 @@ def renew_cert(config: configuration.NamespaceConfig, domains: Optional[List[str
prior_version = lineage.latest_common_version()
# TODO: Check return value of save_successor
lineage.save_successor(prior_version, new_cert, new_key.pem, new_chain, config)
# NOTE: Saving the successor first and then updating the symlinks in that order
# is important to keep the update "atomic".
# Only when the new files have been completely written, the symlink may be
# reassigned.
lineage.update_all_links_to(lineage.latest_common_version())
lineage.truncate()

Expand Down
101 changes: 61 additions & 40 deletions certbot/certbot/_internal/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@

logger = logging.getLogger(__name__)

ALL_FOUR = ("cert", "privkey", "chain", "fullchain")
ALL_FIVE = ("cert", "privkey", "chain", "fullchain", "everything")
README = "README"
CURRENT_VERSION = parse_loose_version(certbot.__version__)
BASE_PRIVKEY_MODE = 0o600
Expand Down Expand Up @@ -128,7 +128,7 @@ def write_renewal_config(o_filename: str, n_filename: str, archive_dir: str,
:param str o_filename: Absolute path to the previous version of config file
:param str n_filename: Absolute path to the new destination of config file
:param str archive_dir: Absolute path to the archive directory
:param dict target: Maps ALL_FOUR to their symlink paths
:param dict target: Maps ALL_FIVE to their symlink paths
:param dict relevant_data: Renewal configuration options to save

:returns: Configuration object for the new config file
Expand All @@ -138,7 +138,7 @@ def write_renewal_config(o_filename: str, n_filename: str, archive_dir: str,
config = configobj.ConfigObj(o_filename, encoding='utf-8', default_encoding='utf-8')
config["version"] = certbot.__version__
config["archive_dir"] = archive_dir
for kind in ALL_FOUR:
for kind in ALL_FIVE:
config[kind] = target[kind]

if "renewalparams" not in config:
Expand Down Expand Up @@ -200,7 +200,7 @@ def update_configuration(lineagename: str, archive_dir: str, target: Mapping[str

:param str lineagename: Name of the lineage being modified
:param str archive_dir: Absolute path to the archive directory
:param dict target: Maps ALL_FOUR to their symlink paths
:param dict target: Maps ALL_FIVE to their symlink paths
:param .NamespaceConfig cli_config: parsed command line
arguments

Expand Down Expand Up @@ -254,6 +254,7 @@ def _write_live_readme_to(readme_path: str, is_base_dir: bool = False) -> None:
f.write("This directory contains your keys and certificates.\n\n"
"`{prefix}privkey.pem` : the private key for your certificate.\n"
"`{prefix}fullchain.pem`: the certificate file used in most server software.\n"
"`{prefix}everything.pem`: the private key, certificate and chain combined in a single file used in some server software.\n"
"`{prefix}chain.pem` : used for OCSP stapling in Nginx >=1.3.7.\n"
"`{prefix}cert.pem` : will break many server configurations, and "
"should not be used\n"
Expand Down Expand Up @@ -382,7 +383,7 @@ def delete_files(config: configuration.NamespaceConfig, certname: str) -> None:
# it's not guaranteed that the files are in our default storage
# structure. so, first delete the cert files.
directory_names = set()
for kind in ALL_FOUR:
for kind in ALL_FIVE:
link = renewal_config.get(kind)
try:
os.remove(link)
Expand Down Expand Up @@ -450,6 +451,9 @@ class RenewableCert(interfaces.RenewableCert):
:ivar str fullchain: The path to the symlink representing the
current version of the fullchain (combined chain and cert)
managed by this lineage.
:ivar str everything: The path to the symlink representing the
current version of everything (combined private key, cert and chain)
managed by this lineage.
:ivar configobj.ConfigObj configuration: The renewal configuration
options associated with this lineage, obtained from parsing the
renewal configuration file and/or systemwide defaults.
Expand Down Expand Up @@ -485,7 +489,7 @@ def __init__(self, config_filename: str, cli_config: configuration.NamespaceConf
# file at this stage?
self.configuration = config_with_defaults(self.configfile)

if not all(x in self.configuration for x in ALL_FOUR):
if not all(x in self.configuration for x in ALL_FIVE):
raise errors.CertStorageError(
"renewal config file {0} is missing a required "
"file reference".format(self.configfile))
Expand All @@ -502,6 +506,7 @@ def __init__(self, config_filename: str, cli_config: configuration.NamespaceConf
self.privkey = self.configuration["privkey"]
self.chain = self.configuration["chain"]
self.fullchain = self.configuration["fullchain"]
self.everything = self.configuration["everything"]
self.live_dir = os.path.dirname(self.cert)

self._fix_symlinks()
Expand Down Expand Up @@ -529,6 +534,11 @@ def fullchain_path(self) -> str:
"""Duck type for self.fullchain"""
return self.fullchain

@property
def fullchain_path(self) -> str:
"""Duck type for self.everything"""
return self.everything

@property
def lineagename(self) -> str:
"""Name given to the certificate lineage.
Expand Down Expand Up @@ -583,7 +593,7 @@ def reuse_key(self) -> bool:

def _check_symlinks(self) -> None:
"""Raises an exception if a symlink doesn't exist"""
for kind in ALL_FOUR:
for kind in ALL_FIVE:
link = getattr(self, kind)
if not os.path.islink(link):
raise errors.CertStorageError(
Expand All @@ -595,7 +605,7 @@ def _check_symlinks(self) -> None:

def _update_symlinks(self) -> None:
"""Updates symlinks to use archive_dir"""
for kind in ALL_FOUR:
for kind in ALL_FIVE:
link = getattr(self, kind)
previous_link = get_link_target(link)
new_link = os.path.join(self.relative_archive_dir(link),
Expand All @@ -614,18 +624,18 @@ def _consistent(self) -> bool:

"""
# Each element must be referenced with an absolute path
for x in (self.cert, self.privkey, self.chain, self.fullchain):
for x in (self.cert, self.privkey, self.chain, self.fullchain, self.everything):
if not os.path.isabs(x):
logger.debug("Element %s is not referenced with an "
"absolute path.", x)
return False

# Each element must exist and be a symbolic link
for x in (self.cert, self.privkey, self.chain, self.fullchain):
for x in (self.cert, self.privkey, self.chain, self.fullchain, self.everything):
if not os.path.islink(x):
logger.debug("Element %s is not a symbolic link.", x)
return False
for kind in ALL_FOUR:
for kind in ALL_FIVE:
link = getattr(self, kind)
target = get_link_target(link)

Expand Down Expand Up @@ -669,7 +679,7 @@ def _consistent(self) -> bool:
# (This check is redundant with the check that they
# are all in the desired directory!)
# len(set(os.path.basename(self.current_target(x)
# for x in ALL_FOUR))) == 1
# for x in ALL_FIVE))) == 1
return True

def _fix(self) -> None:
Expand Down Expand Up @@ -698,7 +708,7 @@ def _previous_symlinks(self) -> List[Tuple[str, str]]:

"""
previous_symlinks = []
for kind in ALL_FOUR:
for kind in ALL_FIVE:
link_dir = os.path.dirname(getattr(self, kind))
link_base = "previous_{0}.pem".format(kind)
previous_symlinks.append((kind, os.path.join(link_dir, link_base)))
Expand Down Expand Up @@ -728,14 +738,14 @@ def current_target(self, kind: str) -> Optional[str]:
"""Returns full path to which the specified item currently points.

:param str kind: the lineage member item ("cert", "privkey",
"chain", or "fullchain")
"chain", "fullchain", or "everything")

:returns: The path to the current version of the specified
member.
:rtype: str or None

"""
if kind not in ALL_FOUR:
if kind not in ALL_FIVE:
raise errors.CertStorageError("unknown kind of item")
link = getattr(self, kind)
if not os.path.exists(link):
Expand All @@ -751,13 +761,13 @@ def current_version(self, kind: str) -> Optional[int]:
points to a file named "chain7.pem", returns the integer 7.

:param str kind: the lineage member item ("cert", "privkey",
"chain", or "fullchain")
"chain", "fullchain", or "everything")

:returns: the current version of the specified member.
:rtype: int

"""
if kind not in ALL_FOUR:
if kind not in ALL_FIVE:
raise errors.CertStorageError("unknown kind of item")
pattern = re.compile(r"^{0}([0-9]+)\.pem$".format(kind))
target = self.current_target(kind)
Expand All @@ -779,14 +789,14 @@ def version(self, kind: str, version: int) -> str:
by this method actually exists.

:param str kind: the lineage member item ("cert", "privkey",
"chain", or "fullchain")
"chain", "fullchain", or "everything")
:param int version: the desired version

:returns: The path to the specified version of the specified member.
:rtype: str

"""
if kind not in ALL_FOUR:
if kind not in ALL_FIVE:
raise errors.CertStorageError("unknown kind of item")
link = self.current_target(kind)
if not link:
Expand All @@ -801,13 +811,13 @@ def available_versions(self, kind: str) -> List[int]:
consulted to obtain the list of alternatives.

:param str kind: the lineage member item (
``cert``, ``privkey``, ``chain``, or ``fullchain``)
``cert``, ``privkey``, ``chain``, ``fullchain``, or ``everything``)

:returns: all of the version numbers that currently exist
:rtype: `list` of `int`

"""
if kind not in ALL_FOUR:
if kind not in ALL_FIVE:
raise errors.CertStorageError("unknown kind of item")
link = self.current_target(kind)
if not link:
Expand All @@ -822,7 +832,7 @@ def newest_available_version(self, kind: str) -> int:
"""Newest available version of the specified kind of item?

:param str kind: the lineage member item (``cert``,
``privkey``, ``chain``, or ``fullchain``)
``privkey``, ``chain``, ``fullchain``, or ``everything``)

:returns: the newest available version of this member
:rtype: int
Expand All @@ -834,15 +844,15 @@ def latest_common_version(self) -> int:
"""Newest version for which all items are available?

:returns: the newest available version for which all members
(``cert, ``privkey``, ``chain``, and ``fullchain``) exist
(``cert, ``privkey``, ``chain``, ``fullchain``, and ``everything``) exist
:rtype: int

"""
# TODO: this can raise CertStorageError if there is no version overlap
# (it should probably return None instead)
# TODO: this can raise a spurious AttributeError if the current
# link for any kind is missing (it should probably return None)
versions = [self.available_versions(x) for x in ALL_FOUR]
versions = [self.available_versions(x) for x in ALL_FIVE]
return max(n for n in versions[0] if all(n in v for v in versions[1:]))

def next_free_version(self) -> int:
Expand All @@ -857,7 +867,7 @@ def next_free_version(self) -> int:
# This isn't self.latest_common_version() + 1 because we don't want
# collide with a version that might exist for one file type but not
# for the others.
return max(self.newest_available_version(x) for x in ALL_FOUR) + 1
return max(self.newest_available_version(x) for x in ALL_FIVE) + 1

def ensure_deployed(self) -> bool:
"""Make sure we've deployed the latest version.
Expand All @@ -884,7 +894,7 @@ def has_pending_deployment(self) -> bool:

"""
all_versions: List[int] = []
for item in ALL_FOUR:
for item in ALL_FIVE:
version = self.current_version(item)
if version is None:
raise errors.Error(f"{item} is required but missing for this certificate.")
Expand All @@ -901,11 +911,11 @@ def _update_link_to(self, kind: str, version: int) -> None:
exists.)

:param str kind: the lineage member item ("cert", "privkey",
"chain", or "fullchain")
"chain", "fullchain", or "everything")
:param int version: the desired version

"""
if kind not in ALL_FOUR:
if kind not in ALL_FIVE:
raise errors.CertStorageError("unknown kind of item")
link = getattr(self, kind)
filename = "{0}{1}.pem".format(kind, version)
Expand Down Expand Up @@ -934,7 +944,7 @@ def update_all_links_to(self, version: int) -> None:
raise errors.Error(f"Target {kind} does not exist!")
os.symlink(target, link)

for kind in ALL_FOUR:
for kind in ALL_FIVE:
self._update_link_to(kind, version)

for _, link in previous_links:
Expand Down Expand Up @@ -1039,10 +1049,11 @@ def new_lineage(cls, lineagename: str, cert: bytes, privkey: bytes, chain: bytes
Attempts to create a certificate lineage -- enrolled for
potential future renewal -- with the (suggested) lineage name
lineagename, and the associated cert, privkey, and chain (the
associated fullchain will be created automatically). Optional
configurator and renewalparams record the configuration that was
originally used to obtain this cert, so that it can be reused
later during automated renewal.
associated fullchain and the combination of everything will
be created automatically). Optional configurator and
renewalparams record the configuration that was originally
used to obtain this cert, so that it can be reused later
during automated renewal.

Returns a new RenewableCert object referring to the created
lineage. (The actual lineage name, as well as all the relevant
Expand Down Expand Up @@ -1093,9 +1104,9 @@ def new_lineage(cls, lineagename: str, cert: bytes, privkey: bytes, chain: bytes
logger.debug("Creating directory %s.", i)

# Put the data into the appropriate files on disk
target = {kind: os.path.join(live_dir, kind + ".pem") for kind in ALL_FOUR}
archive_target = {kind: os.path.join(archive, kind + "1.pem") for kind in ALL_FOUR}
for kind in ALL_FOUR:
target = {kind: os.path.join(live_dir, kind + ".pem") for kind in ALL_FIVE}
archive_target = {kind: os.path.join(archive, kind + "1.pem") for kind in ALL_FIVE}
for kind in ALL_FIVE:
os.symlink(_relpath_from_file(archive_target[kind], target[kind]), target[kind])
with open(target["cert"], "wb") as f_b:
logger.debug("Writing certificate to %s.", target["cert"])
Expand All @@ -1112,6 +1123,9 @@ def new_lineage(cls, lineagename: str, cert: bytes, privkey: bytes, chain: bytes
# ending newline character
logger.debug("Writing full chain to %s.", target["fullchain"])
f_b.write(cert + chain)
with util.safe_open(archive_target["everything"], "wb", chmod=BASE_PRIVKEY_MODE) as f_a:
logger.debug("Writing everything to %s.", target["everything"])
f_a.write(privkey + cert + chain)

# Write a README file to the live directory
readme_path = os.path.join(live_dir, README)
Expand Down Expand Up @@ -1202,7 +1216,7 @@ def save_successor(self, prior_version: int, new_cert: bytes, new_privkey: bytes
self.cli_config = cli_config
target_version = self.next_free_version()
target = {kind: os.path.join(self.archive_dir, "{0}{1}.pem".format(kind, target_version))
for kind in ALL_FOUR}
for kind in ALL_FIVE}

old_privkey = os.path.join(
self.archive_dir, "privkey{0}.pem".format(prior_version))
Expand All @@ -1219,6 +1233,10 @@ def save_successor(self, prior_version: int, new_cert: bytes, new_privkey: bytes
old_privkey = f"privkey{prior_version}.pem"
logger.debug("Writing symlink to old private key, %s.", old_privkey)
os.symlink(old_privkey, target["privkey"])
with open(old_privkey, "rb") as f:
# Read the old private key into new_privkey as we need the
# private key to create the combined file with everything
new_privkey = f.read()
else:
with util.safe_open(target["privkey"], "wb", chmod=BASE_PRIVKEY_MODE) as f:
logger.debug("Writing new private key to %s.", target["privkey"])
Expand All @@ -1239,8 +1257,11 @@ def save_successor(self, prior_version: int, new_cert: bytes, new_privkey: bytes
with open(target["fullchain"], "wb") as f:
logger.debug("Writing full chain to %s.", target["fullchain"])
f.write(new_cert + new_chain)
with util.safe_open(target["everything"], "wb", chmod=BASE_PRIVKEY_MODE) as f:
logger.debug("Writing everything to %s.", target["everything"])
f.write(new_privkey + new_cert + new_chain)

symlinks = {kind: self.configuration[kind] for kind in ALL_FOUR}
symlinks = {kind: self.configuration[kind] for kind in ALL_FIVE}
# Update renewal config file
self.configfile = update_configuration(
self.lineagename, self.archive_dir, symlinks, cli_config)
Expand All @@ -1255,7 +1276,7 @@ def save_new_config_values(self, cli_config: configuration.NamespaceConfig) -> N
arguments
"""
self.cli_config = cli_config
symlinks = {kind: self.configuration[kind] for kind in ALL_FOUR}
symlinks = {kind: self.configuration[kind] for kind in ALL_FIVE}
# Update renewal config file
self.configfile = update_configuration(
self.lineagename, self.archive_dir, symlinks, cli_config)
Expand All @@ -1282,7 +1303,7 @@ def truncate(self, num_prior_certs_to_keep: int = 5) -> None:
for ver in versions_to_delete:
logger.debug("Deleting %s/cert%d.pem and related items during clean up",
archive, ver)
for kind in ALL_FOUR:
for kind in ALL_FIVE:
item_path = os.path.join(archive, f"{kind}{ver}.pem")
try:
if os.path.exists(item_path):
Expand Down