2020-02-05 10:00:08 -05:00
|
|
|
"""A mixin class for OCSP response prefetching for Apache plugin.
|
|
|
|
|
|
|
|
|
|
The OCSP prefetching functionality solves multiple issues in Apache httpd
|
|
|
|
|
that make using OCSP must-staple error prone.
|
|
|
|
|
|
|
|
|
|
The prefetching functionality works by storing a value to PluginStorage,
|
|
|
|
|
noting certificates that Certbot should keep OCSP staples (OCSP responses)
|
|
|
|
|
updated for alongside of the information when the last response was
|
|
|
|
|
updated by Certbot.
|
|
|
|
|
|
|
|
|
|
When Certbot is invoked, typically by scheduled "certbot renew" and the
|
|
|
|
|
TTL from "lastupdate" value in PluginStorage entry has expired,
|
|
|
|
|
Certbot then proceeds to fetch a new OCSP response from the OCSP servers
|
|
|
|
|
pointed by the certificate.
|
|
|
|
|
|
|
|
|
|
The OCSP response is validated and if valid, stored to Apache DBM
|
|
|
|
|
cache. A high internal cache expiry value is set for Apache in order
|
|
|
|
|
to make it to not to discard the stored response and try to renew
|
|
|
|
|
the staple itself letting Certbot to renew it on its subsequent run
|
|
|
|
|
instead.
|
|
|
|
|
|
|
|
|
|
The DBM cache file used by Apache is a lightweight key-value storage.
|
|
|
|
|
For OCSP response caching, the sha1 hash of certificate fingerprint
|
|
|
|
|
is used as a key. The value consists of expiry time as timestamp
|
|
|
|
|
in microseconds, \x01 delimiter and the raw OCSP response.
|
|
|
|
|
|
|
|
|
|
When restarting Apache, Certbot backups the current OCSP response
|
|
|
|
|
cache, and restores it after the restart has happened. This is
|
|
|
|
|
done because Apache deletes and then recreates the file upon
|
|
|
|
|
restart.
|
|
|
|
|
"""
|
|
|
|
|
|
2020-02-03 15:18:52 -05:00
|
|
|
from datetime import datetime
|
2020-01-22 13:51:09 -05:00
|
|
|
import logging
|
|
|
|
|
import time
|
|
|
|
|
|
2020-04-15 17:50:19 -04:00
|
|
|
from acme.magic_typing import Dict, Union # pylint: disable=unused-import, no-name-in-module
|
2020-01-22 13:51:09 -05:00
|
|
|
|
|
|
|
|
from certbot import errors
|
2020-02-12 10:55:10 -05:00
|
|
|
from certbot import ocsp
|
2020-01-24 08:02:25 -05:00
|
|
|
from certbot.plugins.enhancements import OCSPPrefetchEnhancement
|
2020-01-22 13:51:09 -05:00
|
|
|
|
|
|
|
|
from certbot.compat import filesystem
|
|
|
|
|
from certbot.compat import os
|
|
|
|
|
from certbot_apache._internal import apache_util
|
|
|
|
|
from certbot_apache._internal import constants
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
2020-01-31 13:06:52 -05:00
|
|
|
|
2020-02-04 06:13:04 -05:00
|
|
|
class DBMHandler(object):
|
|
|
|
|
"""Context manager to handle DBM file reads and writes"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, filename, mode):
|
|
|
|
|
self.filename = filename
|
|
|
|
|
self.filemode = mode
|
|
|
|
|
self.bsddb = False
|
|
|
|
|
self.database = None
|
|
|
|
|
|
|
|
|
|
def __enter__(self):
|
|
|
|
|
"""Open the DBM file and return the filehandle"""
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
import bsddb
|
|
|
|
|
self.bsddb = True
|
|
|
|
|
try:
|
2020-02-04 13:13:28 -05:00
|
|
|
self.database = bsddb.hashopen(self.filename, self.filemode)
|
2020-02-04 06:13:04 -05:00
|
|
|
except Exception:
|
|
|
|
|
raise errors.PluginError("Unable to open dbm database file.")
|
|
|
|
|
except ImportError:
|
|
|
|
|
# Python3 doesn't have bsddb module, so we use dbm.ndbm instead
|
|
|
|
|
import dbm
|
2020-02-04 13:13:28 -05:00
|
|
|
if self.filename.endswith(".db"):
|
|
|
|
|
self.filename = self.filename[:-3]
|
2020-02-04 06:13:04 -05:00
|
|
|
try:
|
|
|
|
|
self.database = dbm.ndbm.open(self.filename, self.filemode) # pylint: disable=no-member
|
|
|
|
|
except Exception:
|
|
|
|
|
# This is raised if a file cannot be found
|
|
|
|
|
raise errors.PluginError("Unable to open dbm database file.")
|
|
|
|
|
return self.database
|
|
|
|
|
|
|
|
|
|
def __exit__(self, *args):
|
|
|
|
|
"""Close the DBM file"""
|
|
|
|
|
if self.bsddb:
|
|
|
|
|
self.database.sync()
|
|
|
|
|
self.database.close()
|
|
|
|
|
|
|
|
|
|
|
2020-01-22 13:51:09 -05:00
|
|
|
class OCSPPrefetchMixin(object):
|
|
|
|
|
"""OCSPPrefetchMixin implements OCSP response prefetching"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
2020-04-15 17:50:19 -04:00
|
|
|
self._ocsp_prefetch = {} # type: Dict[str, Dict[str, Union[str, float]]]
|
2020-01-24 09:01:50 -05:00
|
|
|
# This is required because of python super() call chain.
|
|
|
|
|
# Additionally, mypy isn't able to figure the chain out and needs to be
|
|
|
|
|
# disabled for this line. See https://github.com/python/mypy/issues/5887
|
2020-01-22 13:51:09 -05:00
|
|
|
super(OCSPPrefetchMixin, self).__init__(*args, **kwargs) # type: ignore
|
2020-04-08 19:42:26 -04:00
|
|
|
self._ocsp_store = os.path.join(self.config.work_dir, "ocsp", "ocsp_cache.db")
|
|
|
|
|
self._ocsp_work = os.path.join(self.config.work_dir, "ocsp_work", "ocsp_cache.db")
|
2020-01-22 13:51:09 -05:00
|
|
|
|
|
|
|
|
def _ensure_ocsp_dirs(self):
|
|
|
|
|
"""Makes sure that the OCSP directory paths exist."""
|
2020-04-08 19:42:26 -04:00
|
|
|
for path in [os.path.dirname(self._ocsp_work),
|
|
|
|
|
os.path.dirname(self._ocsp_store)]:
|
2020-01-22 13:51:09 -05:00
|
|
|
if not os.path.isdir(path):
|
2020-02-03 15:18:52 -05:00
|
|
|
filesystem.makedirs(path, 0o755)
|
2020-01-22 13:51:09 -05:00
|
|
|
|
|
|
|
|
def _ensure_ocsp_prefetch_compatibility(self):
|
|
|
|
|
"""Make sure that the operating system supports the required libraries
|
|
|
|
|
to manage Apache DBM files.
|
|
|
|
|
|
|
|
|
|
:raises: errors.NotSupportedError
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
import bsddb # pylint: disable=unused-variable
|
|
|
|
|
except ImportError:
|
|
|
|
|
import dbm
|
|
|
|
|
if not hasattr(dbm, 'ndbm') or dbm.ndbm.library != 'Berkeley DB': # pylint: disable=no-member
|
|
|
|
|
msg = ("Unfortunately your operating system does not have a "
|
|
|
|
|
"compatible database module available for managing "
|
|
|
|
|
"Apache OCSP stapling cache database.")
|
|
|
|
|
raise errors.NotSupportedError(msg)
|
|
|
|
|
|
2020-04-15 17:50:19 -04:00
|
|
|
def _ocsp_refresh_needed(self, lastupdate):
|
2020-04-08 19:42:26 -04:00
|
|
|
"""Refreshes OCSP response for a certificate if it's due
|
2020-01-22 13:51:09 -05:00
|
|
|
|
2020-04-15 17:50:19 -04:00
|
|
|
:param int lastupdate: Last update timestamp from pluginstorage entry
|
2020-01-22 13:51:09 -05:00
|
|
|
|
|
|
|
|
:returns: If OCSP response was updated
|
|
|
|
|
:rtype: bool
|
|
|
|
|
|
|
|
|
|
"""
|
2020-04-15 17:50:19 -04:00
|
|
|
ttl = lastupdate + constants.OCSP_INTERNAL_TTL
|
2020-01-22 13:51:09 -05:00
|
|
|
if ttl < time.time():
|
|
|
|
|
return True
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ocsp_refresh(self, cert_path, chain_path):
|
|
|
|
|
"""Refresh the OCSP response for a certificate
|
|
|
|
|
|
|
|
|
|
:param str cert_path: Filesystem path to certificate file
|
|
|
|
|
:param str chain_path: Filesystem path to certificate chain file
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
self._ensure_ocsp_dirs()
|
2020-02-03 15:18:52 -05:00
|
|
|
try:
|
2020-04-08 19:42:26 -04:00
|
|
|
ocsp_workfile = os.path.join(
|
|
|
|
|
os.path.dirname(self._ocsp_work),
|
|
|
|
|
apache_util.certid_sha1_hex(cert_path))
|
|
|
|
|
handler = ocsp.RevocationChecker()
|
2020-04-15 17:50:19 -04:00
|
|
|
if not os.path.isfile(cert_path):
|
|
|
|
|
raise OCSPCertificateError("Certificate has been removed from the system.")
|
|
|
|
|
|
2020-04-29 18:20:15 -04:00
|
|
|
if not handler.ocsp_revoked_by_paths(cert_path, chain_path, 10, ocsp_workfile):
|
2020-04-08 19:42:26 -04:00
|
|
|
# Guaranteed good response
|
|
|
|
|
cert_sha = apache_util.certid_sha1(cert_path)
|
|
|
|
|
# dbm.open automatically adds the file extension
|
|
|
|
|
self._write_to_dbm(self._ocsp_store, cert_sha,
|
|
|
|
|
self._ocsp_response_dbm(ocsp_workfile))
|
|
|
|
|
else:
|
|
|
|
|
logger.warning("Encountered an issue while trying to prefetch OCSP "
|
|
|
|
|
"response for certificate: %s", cert_path)
|
2020-04-15 17:50:19 -04:00
|
|
|
raise OCSPCertificateError("Certificate has been revoked.")
|
2020-04-08 19:42:26 -04:00
|
|
|
finally:
|
|
|
|
|
try:
|
|
|
|
|
os.remove(ocsp_workfile)
|
|
|
|
|
except OSError:
|
|
|
|
|
# The OCSP workfile did not exist because of an OCSP response fetching error
|
|
|
|
|
pass
|
2020-01-22 13:51:09 -05:00
|
|
|
|
2020-02-04 06:13:04 -05:00
|
|
|
def _write_to_dbm(self, filename, key, value):
|
2020-02-04 13:13:28 -05:00
|
|
|
"""Helper method to write an OCSP response cache value to DBM.
|
2020-02-04 06:13:04 -05:00
|
|
|
|
|
|
|
|
:param filename: DBM database filename
|
|
|
|
|
:param bytes key: Database key name
|
|
|
|
|
:param bytes value: Database entry value
|
|
|
|
|
"""
|
2020-02-04 13:13:28 -05:00
|
|
|
tmp_file = os.path.join(
|
2020-04-08 19:42:26 -04:00
|
|
|
os.path.dirname(self._ocsp_work),
|
2020-02-04 13:13:28 -05:00
|
|
|
"tmp_" + os.path.basename(filename)
|
|
|
|
|
)
|
2020-02-04 06:13:04 -05:00
|
|
|
|
2020-02-04 13:13:28 -05:00
|
|
|
apache_util.safe_copy(filename, tmp_file)
|
|
|
|
|
|
|
|
|
|
with DBMHandler(tmp_file, 'w') as db:
|
2020-02-04 06:13:04 -05:00
|
|
|
db[key] = value
|
|
|
|
|
|
2020-03-04 13:21:00 -05:00
|
|
|
filesystem.replace(tmp_file, filename)
|
2020-02-04 13:13:28 -05:00
|
|
|
|
2020-01-26 16:42:29 -05:00
|
|
|
def _ocsp_ttl(self, next_update):
|
|
|
|
|
"""Calculates Apache internal TTL for the next OCSP staple
|
|
|
|
|
update.
|
|
|
|
|
|
|
|
|
|
The resulting TTL is half of the time between now
|
|
|
|
|
and the time noted by nextUpdate field in OCSP response.
|
|
|
|
|
|
|
|
|
|
If nextUpdate value is None, a default value will be
|
|
|
|
|
used instead.
|
|
|
|
|
|
|
|
|
|
:param next_update: datetime value for nextUpdate or None
|
|
|
|
|
|
|
|
|
|
:returns: TTL in seconds.
|
|
|
|
|
:rtype: int
|
|
|
|
|
"""
|
2020-04-08 19:42:26 -04:00
|
|
|
# hour in seconds
|
|
|
|
|
hour = 3600
|
|
|
|
|
suberror = ""
|
2020-01-30 09:58:14 -05:00
|
|
|
if next_update is not None:
|
2020-02-19 13:44:37 -05:00
|
|
|
now = datetime.utcnow()
|
2020-02-03 15:18:52 -05:00
|
|
|
res_ttl = int((next_update - now).total_seconds())
|
2020-01-26 16:42:29 -05:00
|
|
|
if res_ttl > 0:
|
2020-04-08 19:42:26 -04:00
|
|
|
safe_ttl = res_ttl - 30 * hour
|
|
|
|
|
if safe_ttl > hour:
|
|
|
|
|
# Use nextUpdate - 30h if it's over an hour from now
|
|
|
|
|
return safe_ttl
|
|
|
|
|
else:
|
|
|
|
|
suberror = ("OCSP response nextUpdate timestamp too "
|
|
|
|
|
"early: {}. Certbot cannot ensure a safe TTL"
|
|
|
|
|
"for OCSP staple prefeching.").format(next_update)
|
|
|
|
|
else:
|
|
|
|
|
suberror = ("OCSP response nextUpdate timestamp too "
|
|
|
|
|
"early: {}").format(next_update)
|
|
|
|
|
else:
|
|
|
|
|
suberror = ("OCSP response nextUpdate not provided with response. "
|
|
|
|
|
"Staple should not be prefetched.")
|
|
|
|
|
raise errors.PluginError(suberror)
|
2020-01-26 16:42:29 -05:00
|
|
|
|
2020-01-22 13:51:09 -05:00
|
|
|
def _ocsp_response_dbm(self, workfile):
|
|
|
|
|
"""Creates a dbm entry for OCSP response data
|
|
|
|
|
|
|
|
|
|
:param str workfile: File path for raw OCSP response
|
|
|
|
|
|
|
|
|
|
:returns: OCSP response cache data that Apache can use
|
|
|
|
|
:rtype: string
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
2020-01-26 16:42:29 -05:00
|
|
|
handler = ocsp.RevocationChecker()
|
|
|
|
|
_, _, next_update = handler.ocsp_times(workfile)
|
|
|
|
|
ttl = self._ocsp_ttl(next_update)
|
|
|
|
|
|
2020-01-22 13:51:09 -05:00
|
|
|
with open(workfile, 'rb') as fh:
|
|
|
|
|
response = fh.read()
|
|
|
|
|
return apache_util.get_apache_ocsp_struct(ttl, response)
|
|
|
|
|
|
|
|
|
|
def _ocsp_prefetch_save(self, cert_path, chain_path):
|
|
|
|
|
"""Saves status of current OCSP prefetch, including the last update
|
|
|
|
|
time to determine if an update is needed on later run.
|
|
|
|
|
|
|
|
|
|
:param str cert_path: Filesystem path to certificate
|
|
|
|
|
:param str chain_path: Filesystem path to certificate chain file
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
status = {
|
|
|
|
|
"lastupdate": time.time(),
|
|
|
|
|
"chain_path": chain_path
|
|
|
|
|
}
|
2020-04-15 17:50:19 -04:00
|
|
|
self._ocsp_prefetch[cert_path] = status
|
2020-01-22 13:51:09 -05:00
|
|
|
self.storage.put("ocsp_prefetch", self._ocsp_prefetch)
|
|
|
|
|
self.storage.save()
|
|
|
|
|
|
2020-04-15 17:50:19 -04:00
|
|
|
def _ocsp_prefetch_remove(self, cert_path):
|
|
|
|
|
"""Removes OCSP prefetch configuration from PluginStorage object for
|
|
|
|
|
a certificate.
|
|
|
|
|
|
|
|
|
|
:param str cert_path: Filesystem path to certificate
|
|
|
|
|
"""
|
|
|
|
|
self._ocsp_prefetch.pop(cert_path)
|
|
|
|
|
self.storage.put("ocsp_prefetch", self._ocsp_prefetch)
|
|
|
|
|
|
2020-01-22 13:51:09 -05:00
|
|
|
def _ocsp_prefetch_fetch_state(self):
|
|
|
|
|
"""
|
|
|
|
|
Populates the OCSP prefetch state from the pluginstorage.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
self._ocsp_prefetch = self.storage.fetch("ocsp_prefetch")
|
|
|
|
|
except KeyError:
|
|
|
|
|
self._ocsp_prefetch = dict()
|
|
|
|
|
|
|
|
|
|
def _ocsp_prefetch_backup_db(self):
|
|
|
|
|
"""
|
2020-02-03 15:18:52 -05:00
|
|
|
Copies the active dbm file to work directory. Logs a debug error
|
|
|
|
|
message if unable to copy, but does not error out as it would
|
|
|
|
|
prevent other critical functions that need to be carried out for
|
|
|
|
|
Apache httpd.
|
2020-02-19 11:07:30 -05:00
|
|
|
|
|
|
|
|
Erroring out here would prevent any restarts done by Apache plugin.
|
2020-01-22 13:51:09 -05:00
|
|
|
"""
|
|
|
|
|
self._ensure_ocsp_dirs()
|
|
|
|
|
try:
|
2020-02-26 11:51:25 -05:00
|
|
|
apache_util.safe_copy(
|
2020-04-08 19:42:26 -04:00
|
|
|
self._ocsp_store,
|
|
|
|
|
self._ocsp_work
|
|
|
|
|
)
|
2020-02-26 13:08:20 -05:00
|
|
|
except errors.PluginError:
|
2020-01-22 13:51:09 -05:00
|
|
|
logger.debug("Encountered an issue while trying to backup OCSP dbm file")
|
|
|
|
|
|
|
|
|
|
def _ocsp_prefetch_restore_db(self):
|
|
|
|
|
"""
|
2020-02-03 15:18:52 -05:00
|
|
|
Restores the active dbm file from work directory. Logs a debug error
|
|
|
|
|
message if unable to restore, but does not error out as it would
|
|
|
|
|
prevent other critical functions that need to be carried out for
|
|
|
|
|
Apache httpd.
|
|
|
|
|
|
2020-04-08 19:42:26 -04:00
|
|
|
Erroring out here would prevent any restarts done by Apache plugin.
|
2020-01-22 13:51:09 -05:00
|
|
|
"""
|
|
|
|
|
self._ensure_ocsp_dirs()
|
|
|
|
|
try:
|
2020-04-08 19:42:26 -04:00
|
|
|
filesystem.replace(self._ocsp_work, self._ocsp_store)
|
2020-01-22 13:51:09 -05:00
|
|
|
except IOError:
|
|
|
|
|
logger.debug("Encountered an issue when trying to restore OCSP dbm file")
|
|
|
|
|
|
|
|
|
|
def enable_ocsp_prefetch(self, lineage, domains):
|
|
|
|
|
"""Enable OCSP Stapling and prefetching of the responses.
|
|
|
|
|
|
|
|
|
|
In OCSP, each client (e.g. browser) would have to query the
|
|
|
|
|
OCSP Responder to validate that the site certificate was not revoked.
|
|
|
|
|
|
2020-02-19 11:07:30 -05:00
|
|
|
Enabling OCSP Stapling would allow the web-server to query the OCSP
|
2020-01-22 13:51:09 -05:00
|
|
|
Responder, and staple its response to the offered certificate during
|
|
|
|
|
TLS. i.e. clients would not have to query the OCSP responder.
|
|
|
|
|
|
2020-02-19 11:07:30 -05:00
|
|
|
OCSP prefetching functionality addresses some of the pain points in
|
|
|
|
|
the implementation that's currently preset in Apache httpd. The
|
|
|
|
|
mitigation provided by Certbot are:
|
|
|
|
|
* OCSP staples get backed up before, and restored after httpd restart
|
|
|
|
|
* Valid OCSP staples do not get overwritten with errors in case of
|
|
|
|
|
network connectivity or OCSP responder issues
|
|
|
|
|
* The staples get updated asynchronically in the background instead
|
|
|
|
|
of blocking a incoming request.
|
2020-01-22 13:51:09 -05:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
# Fail early if we are not able to support this
|
|
|
|
|
self._ensure_ocsp_prefetch_compatibility()
|
|
|
|
|
prefetch_vhosts = set()
|
|
|
|
|
for domain in domains:
|
|
|
|
|
matched_vhosts = self.choose_vhosts(domain, create_if_no_ssl=False)
|
|
|
|
|
# We should be handling only SSL vhosts
|
|
|
|
|
for vh in matched_vhosts:
|
|
|
|
|
if vh.ssl:
|
|
|
|
|
prefetch_vhosts.add(vh)
|
|
|
|
|
|
2020-02-03 15:18:52 -05:00
|
|
|
if not prefetch_vhosts:
|
|
|
|
|
raise errors.MisconfigurationError(
|
|
|
|
|
"Could not find VirtualHost to enable OCSP prefetching on."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# The try - block is huge, but required for handling rollback properly.
|
2020-01-22 13:51:09 -05:00
|
|
|
for vh in prefetch_vhosts:
|
|
|
|
|
self._enable_ocsp_stapling(vh, None, prefetch=True)
|
2020-02-03 15:18:52 -05:00
|
|
|
|
|
|
|
|
self._ensure_ocsp_dirs()
|
2020-01-22 13:51:09 -05:00
|
|
|
self.restart()
|
|
|
|
|
# Ensure Apache has enough time to properly restart and create the file
|
|
|
|
|
time.sleep(2)
|
2020-02-03 15:18:52 -05:00
|
|
|
self._ocsp_refresh(lineage.cert_path, lineage.chain_path)
|
|
|
|
|
self._ocsp_prefetch_save(lineage.cert_path, lineage.chain_path)
|
|
|
|
|
self.save("Enabled OCSP prefetching")
|
|
|
|
|
except errors.PluginError as err:
|
|
|
|
|
# Revert the OCSP prefetch configuration
|
|
|
|
|
self.recovery_routine()
|
|
|
|
|
self.restart()
|
|
|
|
|
msg = ("Encountered an error while trying to enable OCSP prefetch "
|
2020-04-08 19:42:26 -04:00
|
|
|
"enhancement: %s\nOCSP prefetch was not enabled.")
|
2020-02-03 15:18:52 -05:00
|
|
|
raise errors.PluginError(msg % str(err))
|
2020-01-22 13:51:09 -05:00
|
|
|
|
2020-04-22 13:46:10 -04:00
|
|
|
def deploy_ocsp_prefetch(self, lineage):
|
|
|
|
|
"""When certificate gets renewed, ensure that we're able to serve an appropriate OCSP
|
|
|
|
|
staple after the restart that replaces the certificate."""
|
|
|
|
|
|
|
|
|
|
self._ocsp_prefetch_fetch_state()
|
|
|
|
|
if not self._ocsp_prefetch:
|
|
|
|
|
# No OCSP prefetching enabled for any certificate
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
if lineage.cert_path in self._ocsp_prefetch:
|
|
|
|
|
pf = self._ocsp_prefetch[lineage.cert_path]
|
|
|
|
|
try:
|
|
|
|
|
self._ocsp_try_refresh(lineage.cert_path)
|
|
|
|
|
except OCSPCertificateError:
|
|
|
|
|
# This error was logged and handled already down the stack. Return to avoid save.
|
|
|
|
|
return
|
|
|
|
|
self._ocsp_prefetch_save(lineage.cert_path, pf["chain_path"])
|
|
|
|
|
|
2020-01-22 13:51:09 -05:00
|
|
|
def update_ocsp_prefetch(self, _unused_lineage):
|
|
|
|
|
"""Checks all certificates that are managed by OCSP prefetch, and
|
|
|
|
|
refreshes OCSP responses for them if required."""
|
|
|
|
|
|
|
|
|
|
self._ocsp_prefetch_fetch_state()
|
|
|
|
|
if not self._ocsp_prefetch:
|
|
|
|
|
# No OCSP prefetching enabled for any certificate
|
|
|
|
|
return
|
|
|
|
|
|
2020-04-15 17:50:19 -04:00
|
|
|
# make a copy of the list of dictionary keys as we might remove items mid-iteration
|
|
|
|
|
for cert_path in list(self._ocsp_prefetch):
|
|
|
|
|
pf = self._ocsp_prefetch[cert_path]
|
|
|
|
|
if self._ocsp_refresh_needed(pf["lastupdate"]):
|
2020-04-08 19:42:26 -04:00
|
|
|
try:
|
2020-04-22 13:46:10 -04:00
|
|
|
self._ocsp_try_refresh(cert_path)
|
|
|
|
|
except OCSPCertificateError:
|
|
|
|
|
# We want to skip saving in this case, as we just removed the
|
|
|
|
|
# certificate from prefetch pool.
|
2020-04-15 17:50:19 -04:00
|
|
|
continue
|
|
|
|
|
self._ocsp_prefetch_save(cert_path, pf["chain_path"])
|
2020-01-24 08:02:25 -05:00
|
|
|
|
2020-04-22 13:46:10 -04:00
|
|
|
def _ocsp_try_refresh(self, cert_path):
|
|
|
|
|
"""Attempt to refresh OCSP staple for a certificate.
|
|
|
|
|
|
|
|
|
|
:param str cert_path: Path to certificate
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
pf = self._ocsp_prefetch[cert_path]
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
self._ocsp_refresh(cert_path, pf["chain_path"])
|
|
|
|
|
except OCSPCertificateError as err:
|
|
|
|
|
self._ocsp_prefetch_remove(cert_path)
|
|
|
|
|
msg = ("Error when trying to prefetch OCSP staple: {} " +
|
|
|
|
|
"OCSP prefetch functionality removed for the certificate").format(err)
|
|
|
|
|
logger.warning(msg)
|
|
|
|
|
raise
|
|
|
|
|
except errors.PluginError as err:
|
|
|
|
|
msg = "Encountered a issue when trying to renew OCSP staple: {}".format(err)
|
|
|
|
|
logger.warning(msg)
|
|
|
|
|
|
2020-01-24 08:02:25 -05:00
|
|
|
def restart(self):
|
2020-02-03 15:18:52 -05:00
|
|
|
"""Reloads the Apache server. When restarting, Apache deletes
|
|
|
|
|
the DBM cache file used to store OCSP staples. In this override
|
|
|
|
|
function, Certbot checks the pluginstorage if we're supposed to
|
|
|
|
|
manage OCSP prefetching. If needed, Certbot will backup the DBM
|
|
|
|
|
file, restoring it after calling restart.
|
2020-01-24 08:02:25 -05:00
|
|
|
|
|
|
|
|
:raises .errors.MisconfigurationError: If either the config test
|
|
|
|
|
or reload fails.
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
if not self._ocsp_prefetch:
|
|
|
|
|
# Try to populate OCSP prefetch structure from pluginstorage
|
|
|
|
|
self._ocsp_prefetch_fetch_state()
|
|
|
|
|
if self._ocsp_prefetch:
|
|
|
|
|
# OCSP prefetching is enabled, so back up the db
|
|
|
|
|
self._ocsp_prefetch_backup_db()
|
|
|
|
|
|
2020-01-24 09:06:11 -05:00
|
|
|
try:
|
2020-01-31 13:06:52 -05:00
|
|
|
# Ignored because mypy doesn't know that this class is used as
|
|
|
|
|
# a mixin and fails because object has no restart method.
|
2020-01-24 15:35:22 -05:00
|
|
|
super(OCSPPrefetchMixin, self).restart() # type: ignore
|
2020-01-31 13:06:52 -05:00
|
|
|
finally:
|
|
|
|
|
if self._ocsp_prefetch:
|
|
|
|
|
# Restore the backed up dbm database
|
|
|
|
|
self._ocsp_prefetch_restore_db()
|
2020-01-24 08:02:25 -05:00
|
|
|
|
|
|
|
|
|
2020-04-15 17:50:19 -04:00
|
|
|
class OCSPCertificateError(errors.PluginError):
|
|
|
|
|
"""Error that prompts for removal of certificate from OCSP prefetch pool."""
|
|
|
|
|
|
|
|
|
|
|
2020-01-24 08:02:25 -05:00
|
|
|
OCSPPrefetchEnhancement.register(OCSPPrefetchMixin) # pylint: disable=no-member
|