mirror of
https://github.com/certbot/certbot.git
synced 2026-05-28 04:34:11 -04:00
Some code uses `logging.debug` and `logging.info` instead of the file-specific logger in `logger.debug` and `logger.info`.
120 lines
4.1 KiB
Python
120 lines
4.1 KiB
Python
"""Tools for checking certificate revocation."""
|
|
import logging
|
|
import re
|
|
|
|
from subprocess import Popen, PIPE
|
|
|
|
from certbot import errors
|
|
from certbot import util
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class RevocationChecker(object):
|
|
"This class figures out OCSP checking on this system, and performs it."
|
|
|
|
def __init__(self):
|
|
self.broken = False
|
|
|
|
if not util.exe_exists("openssl"):
|
|
logger.info("openssl not installed, can't check revocation")
|
|
self.broken = True
|
|
return
|
|
|
|
# New versions of openssl want -header var=val, old ones want -header var val
|
|
test_host_format = Popen(["openssl", "ocsp", "-header", "var", "val"],
|
|
stdout=PIPE, stderr=PIPE, universal_newlines=True)
|
|
_out, err = test_host_format.communicate()
|
|
if "Missing =" in err:
|
|
self.host_args = lambda host: ["Host=" + host]
|
|
else:
|
|
self.host_args = lambda host: ["Host", host]
|
|
|
|
|
|
def ocsp_revoked(self, cert_path, chain_path):
|
|
"""Get revoked status for a particular cert version.
|
|
|
|
.. todo:: Make this a non-blocking call
|
|
|
|
:param str cert_path: Path to certificate
|
|
:param str chain_path: Path to intermediate cert
|
|
:rtype bool or None:
|
|
:returns: True if revoked; False if valid or the check failed
|
|
|
|
"""
|
|
if self.broken:
|
|
return False
|
|
|
|
|
|
url, host = self.determine_ocsp_server(cert_path)
|
|
if not host:
|
|
return False
|
|
# jdkasten thanks "Bulletproof SSL and TLS - Ivan Ristic" for documenting this!
|
|
cmd = ["openssl", "ocsp",
|
|
"-no_nonce",
|
|
"-issuer", chain_path,
|
|
"-cert", cert_path,
|
|
"-url", url,
|
|
"-CAfile", chain_path,
|
|
"-verify_other", chain_path,
|
|
"-trust_other",
|
|
"-header"] + self.host_args(host)
|
|
logger.debug("Querying OCSP for %s", cert_path)
|
|
logger.debug(" ".join(cmd))
|
|
try:
|
|
output, err = util.run_script(cmd, log=logger.debug)
|
|
except errors.SubprocessError:
|
|
logger.info("OCSP check failed for %s (are we offline?)", cert_path)
|
|
return False
|
|
|
|
return _translate_ocsp_query(cert_path, output, err)
|
|
|
|
|
|
def determine_ocsp_server(self, cert_path):
|
|
"""Extract the OCSP server host from a certificate.
|
|
|
|
:param str cert_path: Path to the cert we're checking OCSP for
|
|
:rtype tuple:
|
|
:returns: (OCSP server URL or None, OCSP server host or None)
|
|
|
|
"""
|
|
try:
|
|
url, _err = util.run_script(
|
|
["openssl", "x509", "-in", cert_path, "-noout", "-ocsp_uri"],
|
|
log=logger.debug)
|
|
except errors.SubprocessError:
|
|
logger.info("Cannot extract OCSP URI from %s", cert_path)
|
|
return None, None
|
|
|
|
url = url.rstrip()
|
|
host = url.partition("://")[2].rstrip("/")
|
|
if host:
|
|
return url, host
|
|
else:
|
|
logger.info("Cannot process OCSP host from URL (%s) in cert at %s", url, cert_path)
|
|
return None, None
|
|
|
|
def _translate_ocsp_query(cert_path, ocsp_output, ocsp_errors):
|
|
"""Parse openssl's weird output to work out what it means."""
|
|
|
|
states = ("good", "revoked", "unknown")
|
|
patterns = [r"{0}: (WARNING.*)?{1}".format(cert_path, s) for s in states]
|
|
good, revoked, unknown = (re.search(p, ocsp_output, flags=re.DOTALL) for p in patterns)
|
|
|
|
warning = good.group(1) if good else None
|
|
|
|
if (not "Response verify OK" in ocsp_errors) or (good and warning) or unknown:
|
|
logger.info("Revocation status for %s is unknown", cert_path)
|
|
logger.debug("Uncertain output:\n%s\nstderr:\n%s", ocsp_output, ocsp_errors)
|
|
return False
|
|
elif good and not warning:
|
|
return False
|
|
elif revoked:
|
|
warning = revoked.group(1)
|
|
if warning:
|
|
logger.info("OCSP revocation warning: %s", warning)
|
|
return True
|
|
else:
|
|
logger.warn("Unable to properly parse OCSP output: %s\nstderr:%s",
|
|
ocsp_output, ocsp_errors)
|
|
return False
|
|
|