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
|
2020-07-27 18:57:16 -04:00
|
|
|
that make using OCSP must-staple error prone. See
|
|
|
|
|
https://blog.hboeck.de/archives/886-The-Problem-with-OCSP-Stapling-and-Must-Staple-and-why-Certificate-Revocation-is-still-broken.html
|
|
|
|
|
for more details on what these problems are, however, the solution implemented
|
|
|
|
|
here is to have Certbot fetch the OCSP response for Apache to staple. This
|
|
|
|
|
happens whenever a certificate is obtained or updated and periodically when
|
|
|
|
|
`certbot renew` is run even if the certificate is not renewed.
|
2020-02-05 10:00:08 -05:00
|
|
|
|
2020-07-27 18:57:16 -04:00
|
|
|
The prefetching functionality works by storing values using
|
|
|
|
|
certbot.plugins.storage noting which certificates Certbot should keep OCSP
|
|
|
|
|
staples (OCSP responses) updated for alongside of the information when the last
|
|
|
|
|
response was updated by Certbot.
|
2020-02-05 10:00:08 -05:00
|
|
|
|
|
|
|
|
When Certbot is invoked, typically by scheduled "certbot renew" and the
|
2020-07-27 18:57:16 -04:00
|
|
|
TTL from the "lastupdate" value in PluginStorage entry has expired,
|
2020-02-05 10:00:08 -05:00
|
|
|
Certbot then proceeds to fetch a new OCSP response from the OCSP servers
|
2020-07-27 18:57:16 -04:00
|
|
|
pointed by the certificate. The OCSP response is validated and if valid, stored
|
|
|
|
|
for Apache to use.
|
2020-02-05 10:00:08 -05:00
|
|
|
|
2020-07-27 18:57:16 -04:00
|
|
|
Apache is configured to cache the OCSP responses it has stapled in a DBM file.
|
|
|
|
|
This file was not intended to be modified by external programs so much of the
|
|
|
|
|
knowledge here is based on testing and reading the Apache source code. This
|
|
|
|
|
fact also complicates things like handling race conditions and compatibility
|
|
|
|
|
with the different DBM formats that Apache can use as described below.
|
2020-02-05 10:00:08 -05:00
|
|
|
|
2020-07-27 18:57:16 -04:00
|
|
|
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 Certbot modifies to this file, 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. When this file is read or modified by Certbot, extra care is taken to
|
|
|
|
|
try and avoid race conditions with Apache the best we can. Apache does not need
|
|
|
|
|
to be reloaded/restarted to pick up changes to this file.
|
2020-02-05 10:00:08 -05:00
|
|
|
|
|
|
|
|
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-07-27 18:57:16 -04:00
|
|
|
|
|
|
|
|
There are multiple incompatible libraries for DBM files which Apache can be
|
|
|
|
|
configured to use. To deal with this, as of writing this, this mixin is only
|
|
|
|
|
used in the Debian specific override classes and we expect Apache to use the
|
|
|
|
|
DBM libaries it was configured to use in the Debian Apache packages at the time
|
|
|
|
|
this feature was written. Python itself may or may not have support for this
|
|
|
|
|
DBM format and if it does not, this feature is not offered to the user.
|
|
|
|
|
|
2020-02-05 10:00:08 -05:00
|
|
|
"""
|
|
|
|
|
|
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-06-17 16:28:06 -04:00
|
|
|
from certbot import interfaces
|
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
|
|
|
|
|
"""
|
2020-07-10 15:35:34 -04:00
|
|
|
if not ocsp.CRYPTOGRAPHY_OCSP_AVAILABLE:
|
|
|
|
|
raise errors.NotSupportedError(
|
|
|
|
|
"You need version 2.5+ of the Python library cryptography to"
|
|
|
|
|
" use this feature.")
|
2020-01-22 13:51:09 -05:00
|
|
|
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
|
|
|
|
|
|
2020-04-30 17:18:05 -04:00
|
|
|
:raises .errors.PluginError: If the OCSP response should not be
|
|
|
|
|
configured for use with Apache
|
|
|
|
|
|
|
|
|
|
:raises OCSPCertificateError: If the given certificate should be
|
|
|
|
|
removed from the OCSP prefetch pool.
|
2020-01-22 13:51:09 -05:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
self._ensure_ocsp_dirs()
|
2020-05-20 17:59:20 -04:00
|
|
|
# Ensure cert_path exists before trying to use it.
|
2020-04-30 20:12:49 -04:00
|
|
|
if not os.path.isfile(cert_path):
|
|
|
|
|
raise OCSPCertificateError("Certificate has been removed from the system.")
|
|
|
|
|
handler = ocsp.RevocationChecker()
|
2020-06-17 16:28:06 -04:00
|
|
|
ocsp_response = handler.ocsp_response_by_paths(cert_path, chain_path)
|
|
|
|
|
if ocsp_response is None:
|
|
|
|
|
raise errors.PluginError("Unable to obtain an OCSP response.")
|
|
|
|
|
elif ocsp_response.certificate_status == interfaces.OCSPCertStatus.REVOKED:
|
|
|
|
|
raise OCSPCertificateError("Certificate has been revoked.")
|
|
|
|
|
elif ocsp_response.certificate_status == interfaces.OCSPCertStatus.UNKNOWN:
|
|
|
|
|
raise OCSPCertificateError("Certificate is unknown to the OCSP responder.")
|
|
|
|
|
else:
|
|
|
|
|
# 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_response))
|
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-04-30 17:40:01 -04:00
|
|
|
|
|
|
|
|
:raises .errors.PluginError: If there was an error copying
|
|
|
|
|
Apache's DBM file
|
|
|
|
|
|
2020-02-04 06:13:04 -05:00
|
|
|
"""
|
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.
|
|
|
|
|
|
2020-04-30 17:18:05 -04:00
|
|
|
The resulting TTL is the duration until there is 30 hours from
|
|
|
|
|
the time noted by the nextUpdate field in the OCSP response.
|
2020-01-26 16:42:29 -05:00
|
|
|
|
2020-04-30 17:18:05 -04:00
|
|
|
If nextUpdate value is None, an exception is raised.
|
2020-01-26 16:42:29 -05:00
|
|
|
|
|
|
|
|
:param next_update: datetime value for nextUpdate or None
|
|
|
|
|
|
2020-04-30 17:18:05 -04:00
|
|
|
:raises .errors.PluginError: If the OCSP response should not be
|
|
|
|
|
configured for use with Apache
|
|
|
|
|
|
2020-01-26 16:42:29 -05:00
|
|
|
:returns: TTL in seconds.
|
|
|
|
|
:rtype: int
|
2020-04-30 17:18:05 -04:00
|
|
|
|
2020-01-26 16:42:29 -05:00
|
|
|
"""
|
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-06-17 16:28:06 -04:00
|
|
|
def _ocsp_response_dbm(self, ocsp_response):
|
2020-01-22 13:51:09 -05:00
|
|
|
"""Creates a dbm entry for OCSP response data
|
|
|
|
|
|
2020-06-17 16:28:06 -04:00
|
|
|
:param interfaces.OCSPResponse ocsp_response: Good OCSP response
|
2020-01-22 13:51:09 -05:00
|
|
|
|
2020-04-30 17:18:05 -04:00
|
|
|
:raises .errors.PluginError: If the OCSP response should not be
|
|
|
|
|
configured for use with Apache
|
|
|
|
|
|
2020-01-22 13:51:09 -05:00
|
|
|
:returns: OCSP response cache data that Apache can use
|
|
|
|
|
:rtype: string
|
|
|
|
|
|
|
|
|
|
"""
|
2020-06-17 16:28:06 -04:00
|
|
|
ttl = self._ocsp_ttl(ocsp_response.next_update)
|
|
|
|
|
response = ocsp_response.bytes
|
2020-01-22 13:51:09 -05:00
|
|
|
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-05-21 14:13:41 -04:00
|
|
|
self.storage.save()
|
2020-04-15 17:50:19 -04:00
|
|
|
|
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")
|
|
|
|
|
|
2020-04-30 18:16:43 -04:00
|
|
|
def enable_ocsp_prefetch(self, lineage, domains, *_unused_args, **_unused_kwargs):
|
2020-01-22 13:51:09 -05:00
|
|
|
"""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-30 18:16:43 -04:00
|
|
|
def deploy_ocsp_prefetch(self, lineage, *_unused_args, **_unused_kwargs):
|
2020-04-22 13:46:10 -04:00
|
|
|
"""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:
|
|
|
|
|
try:
|
2020-05-20 19:40:15 -04:00
|
|
|
self._ocsp_try_refresh(lineage.cert_path, lineage.chain_path)
|
2020-04-22 13:46:10 -04:00
|
|
|
except OCSPCertificateError:
|
|
|
|
|
# This error was logged and handled already down the stack. Return to avoid save.
|
|
|
|
|
return
|
2020-05-20 19:40:15 -04:00
|
|
|
self._ocsp_prefetch_save(lineage.cert_path, lineage.chain_path)
|
2020-04-22 13:46:10 -04:00
|
|
|
|
2020-04-30 18:16:43 -04:00
|
|
|
def update_ocsp_prefetch(self, _unused_lineage, *_unused_args, **_unused_kwargs):
|
2020-01-22 13:51:09 -05:00
|
|
|
"""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-05-20 19:40:15 -04:00
|
|
|
chain_path = pf["chain_path"]
|
2020-04-08 19:42:26 -04:00
|
|
|
try:
|
2020-05-20 19:40:15 -04:00
|
|
|
self._ocsp_try_refresh(cert_path, chain_path)
|
2020-04-22 13:46:10 -04:00
|
|
|
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
|
2020-05-20 19:40:15 -04:00
|
|
|
self._ocsp_prefetch_save(cert_path, chain_path)
|
2020-01-24 08:02:25 -05:00
|
|
|
|
2020-05-20 19:40:15 -04:00
|
|
|
def _ocsp_try_refresh(self, cert_path, chain_path):
|
2020-04-22 13:46:10 -04:00
|
|
|
"""Attempt to refresh OCSP staple for a certificate.
|
|
|
|
|
|
|
|
|
|
:param str cert_path: Path to certificate
|
2020-05-20 19:40:15 -04:00
|
|
|
:param str chain_path: Filesystem path to certificate chain file
|
2020-04-30 17:18:05 -04:00
|
|
|
|
|
|
|
|
:raises OCSPCertificateError: If the given certificate should be
|
|
|
|
|
removed from the OCSP prefetch pool.
|
|
|
|
|
|
2020-04-22 13:46:10 -04:00
|
|
|
"""
|
|
|
|
|
try:
|
2020-05-20 19:40:15 -04:00
|
|
|
self._ocsp_refresh(cert_path, chain_path)
|
2020-04-22 13:46:10 -04:00
|
|
|
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
|