mirror of
https://github.com/certbot/certbot.git
synced 2026-08-01 02:44:21 +02:00
Merge pull request #10448 from certbot/no-openssl-ocsp
remove openssl-based ocsp checking
This commit is contained in:
@@ -16,114 +16,8 @@ from cryptography.hazmat.primitives import hashes
|
|||||||
from cryptography.x509 import ocsp as ocsp_lib
|
from cryptography.x509 import ocsp as ocsp_lib
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from certbot import errors
|
|
||||||
from certbot.tests import util as test_util
|
from certbot.tests import util as test_util
|
||||||
|
|
||||||
out = """Missing = in header key=value
|
|
||||||
ocsp: Use -help for summary.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class OCSPTestOpenSSL(unittest.TestCase):
|
|
||||||
"""
|
|
||||||
OCSP revocation tests using OpenSSL binary.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
from certbot import ocsp
|
|
||||||
with mock.patch('certbot.ocsp.subprocess.run') as mock_run:
|
|
||||||
with mock.patch('certbot.util.exe_exists') as mock_exists:
|
|
||||||
mock_run.stderr = out
|
|
||||||
mock_exists.return_value = True
|
|
||||||
self.checker = ocsp.RevocationChecker(enforce_openssl_binary_usage=True)
|
|
||||||
|
|
||||||
@mock.patch('certbot.ocsp.logger.info')
|
|
||||||
@mock.patch('certbot.ocsp.subprocess.run')
|
|
||||||
@mock.patch('certbot.util.exe_exists')
|
|
||||||
def test_init(self, mock_exists, mock_run, mock_log):
|
|
||||||
mock_run.return_value.stderr = out
|
|
||||||
mock_exists.return_value = True
|
|
||||||
|
|
||||||
from certbot import ocsp
|
|
||||||
checker = ocsp.RevocationChecker(enforce_openssl_binary_usage=True)
|
|
||||||
assert mock_run.call_count == 1
|
|
||||||
assert checker.host_args("x") == ["Host=x"]
|
|
||||||
|
|
||||||
mock_run.return_value.stderr = out.partition("\n")[2]
|
|
||||||
checker = ocsp.RevocationChecker(enforce_openssl_binary_usage=True)
|
|
||||||
assert checker.host_args("x") == ["Host", "x"]
|
|
||||||
assert checker.broken is False
|
|
||||||
|
|
||||||
mock_exists.return_value = False
|
|
||||||
mock_run.call_count = 0
|
|
||||||
checker = ocsp.RevocationChecker(enforce_openssl_binary_usage=True)
|
|
||||||
assert mock_run.call_count == 0
|
|
||||||
assert mock_log.call_count == 1
|
|
||||||
assert checker.broken is True
|
|
||||||
|
|
||||||
@mock.patch('certbot.ocsp._determine_ocsp_server')
|
|
||||||
@mock.patch('certbot.ocsp.crypto_util.notAfter')
|
|
||||||
@mock.patch('certbot.util.run_script')
|
|
||||||
def test_ocsp_revoked(self, mock_run, mock_na, mock_determine):
|
|
||||||
now = datetime.now(timezone.utc)
|
|
||||||
cert_obj = mock.MagicMock()
|
|
||||||
cert_obj.cert_path = "x"
|
|
||||||
cert_obj.chain_path = "y"
|
|
||||||
mock_na.return_value = now + timedelta(hours=2)
|
|
||||||
|
|
||||||
self.checker.broken = True
|
|
||||||
mock_determine.return_value = ("", "")
|
|
||||||
assert self.checker.ocsp_revoked(cert_obj) is False
|
|
||||||
|
|
||||||
self.checker.broken = False
|
|
||||||
mock_run.return_value = tuple(openssl_happy[1:])
|
|
||||||
assert self.checker.ocsp_revoked(cert_obj) is False
|
|
||||||
assert mock_run.call_count == 0
|
|
||||||
|
|
||||||
mock_determine.return_value = ("http://x.co", "x.co")
|
|
||||||
assert self.checker.ocsp_revoked(cert_obj) is False
|
|
||||||
mock_run.side_effect = errors.SubprocessError("Unable to load certificate launcher")
|
|
||||||
assert self.checker.ocsp_revoked(cert_obj) is False
|
|
||||||
assert mock_run.call_count == 2
|
|
||||||
|
|
||||||
# cert expired
|
|
||||||
mock_na.return_value = now
|
|
||||||
mock_determine.return_value = ("", "")
|
|
||||||
count_before = mock_determine.call_count
|
|
||||||
assert self.checker.ocsp_revoked(cert_obj) is False
|
|
||||||
assert mock_determine.call_count == count_before
|
|
||||||
|
|
||||||
def test_determine_ocsp_server(self):
|
|
||||||
cert_path = test_util.vector_path('ocsp_certificate.pem')
|
|
||||||
|
|
||||||
from certbot import ocsp
|
|
||||||
result = ocsp._determine_ocsp_server(cert_path)
|
|
||||||
assert ('http://ocsp.test4.buypass.com', 'ocsp.test4.buypass.com') == result
|
|
||||||
|
|
||||||
@mock.patch('certbot.ocsp.logger')
|
|
||||||
@mock.patch('certbot.util.run_script')
|
|
||||||
def test_translate_ocsp(self, mock_run, mock_log):
|
|
||||||
# pylint: disable=protected-access
|
|
||||||
mock_run.return_value = openssl_confused
|
|
||||||
from certbot import ocsp
|
|
||||||
assert ocsp._translate_ocsp_query(*openssl_happy) is False
|
|
||||||
assert ocsp._translate_ocsp_query(*openssl_confused) is False
|
|
||||||
assert mock_log.debug.call_count == 1
|
|
||||||
assert mock_log.warning.call_count == 0
|
|
||||||
mock_log.debug.call_count = 0
|
|
||||||
assert ocsp._translate_ocsp_query(*openssl_unknown) is False
|
|
||||||
assert mock_log.debug.call_count == 1
|
|
||||||
assert mock_log.warning.call_count == 0
|
|
||||||
assert ocsp._translate_ocsp_query(*openssl_expired_ocsp) is False
|
|
||||||
assert mock_log.debug.call_count == 2
|
|
||||||
assert ocsp._translate_ocsp_query(*openssl_broken) is False
|
|
||||||
assert mock_log.warning.call_count == 1
|
|
||||||
mock_log.info.call_count = 0
|
|
||||||
assert ocsp._translate_ocsp_query(*openssl_revoked) is True
|
|
||||||
assert mock_log.info.call_count == 0
|
|
||||||
assert ocsp._translate_ocsp_query(*openssl_expired_ocsp_revoked) is True
|
|
||||||
assert mock_log.info.call_count == 1
|
|
||||||
|
|
||||||
|
|
||||||
class OSCPTestCryptography(unittest.TestCase):
|
class OSCPTestCryptography(unittest.TestCase):
|
||||||
"""
|
"""
|
||||||
@@ -330,59 +224,5 @@ def _construct_mock_ocsp_response(certificate_status, response_status):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=line-too-long
|
|
||||||
openssl_confused = ("", """
|
|
||||||
/etc/letsencrypt/live/example.org/cert.pem: good
|
|
||||||
This Update: Dec 17 00:00:00 2016 GMT
|
|
||||||
Next Update: Dec 24 00:00:00 2016 GMT
|
|
||||||
""",
|
|
||||||
"""
|
|
||||||
Response Verify Failure
|
|
||||||
139903674214048:error:27069065:OCSP routines:OCSP_basic_verify:certificate verify error:ocsp_vfy.c:138:Verify error:unable to get local issuer certificate
|
|
||||||
""")
|
|
||||||
|
|
||||||
openssl_happy = ("blah.pem", """
|
|
||||||
blah.pem: good
|
|
||||||
This Update: Dec 20 18:00:00 2016 GMT
|
|
||||||
Next Update: Dec 27 18:00:00 2016 GMT
|
|
||||||
""",
|
|
||||||
"Response verify OK")
|
|
||||||
|
|
||||||
openssl_revoked = ("blah.pem", """
|
|
||||||
blah.pem: revoked
|
|
||||||
This Update: Dec 20 01:00:00 2016 GMT
|
|
||||||
Next Update: Dec 27 01:00:00 2016 GMT
|
|
||||||
Revocation Time: Dec 20 01:46:34 2016 GMT
|
|
||||||
""",
|
|
||||||
"""Response verify OK""")
|
|
||||||
|
|
||||||
openssl_unknown = ("blah.pem", """
|
|
||||||
blah.pem: unknown
|
|
||||||
This Update: Dec 20 18:00:00 2016 GMT
|
|
||||||
Next Update: Dec 27 18:00:00 2016 GMT
|
|
||||||
""",
|
|
||||||
"Response verify OK")
|
|
||||||
|
|
||||||
openssl_broken = ("", "tentacles", "Response verify OK")
|
|
||||||
|
|
||||||
openssl_expired_ocsp = ("blah.pem", """
|
|
||||||
blah.pem: WARNING: Status times invalid.
|
|
||||||
140659132298912:error:2707307D:OCSP routines:OCSP_check_validity:status expired:ocsp_cl.c:372:
|
|
||||||
good
|
|
||||||
This Update: Apr 6 00:00:00 2016 GMT
|
|
||||||
Next Update: Apr 13 00:00:00 2016 GMT
|
|
||||||
""",
|
|
||||||
"""Response verify OK""")
|
|
||||||
|
|
||||||
openssl_expired_ocsp_revoked = ("blah.pem", """
|
|
||||||
blah.pem: WARNING: Status times invalid.
|
|
||||||
140659132298912:error:2707307D:OCSP routines:OCSP_check_validity:status expired:ocsp_cl.c:372:
|
|
||||||
revoked
|
|
||||||
This Update: Apr 6 00:00:00 2016 GMT
|
|
||||||
Next Update: Apr 13 00:00:00 2016 GMT
|
|
||||||
""",
|
|
||||||
"""Response verify OK""")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover
|
sys.exit(pytest.main(sys.argv[1:] + [__file__])) # pragma: no cover
|
||||||
|
|||||||
@@ -3,11 +3,7 @@ from datetime import datetime
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from datetime import timezone
|
from datetime import timezone
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
from subprocess import PIPE
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import warnings
|
|
||||||
|
|
||||||
from cryptography import x509
|
from cryptography import x509
|
||||||
from cryptography.exceptions import InvalidSignature
|
from cryptography.exceptions import InvalidSignature
|
||||||
@@ -20,8 +16,6 @@ import requests
|
|||||||
|
|
||||||
from certbot import crypto_util
|
from certbot import crypto_util
|
||||||
from certbot import errors
|
from certbot import errors
|
||||||
from certbot import util
|
|
||||||
from certbot.compat.os import getenv
|
|
||||||
from certbot.interfaces import RenewableCert
|
from certbot.interfaces import RenewableCert
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -29,30 +23,6 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class RevocationChecker:
|
class RevocationChecker:
|
||||||
"""This class figures out OCSP checking on this system, and performs it."""
|
"""This class figures out OCSP checking on this system, and performs it."""
|
||||||
|
|
||||||
def __init__(self, enforce_openssl_binary_usage: bool = False) -> None:
|
|
||||||
self.broken = False
|
|
||||||
if enforce_openssl_binary_usage:
|
|
||||||
warnings.warn("enforce_openssl_binary_usage parameter is deprecated "
|
|
||||||
"and will be removed in an upcoming certbot major version update",
|
|
||||||
DeprecationWarning)
|
|
||||||
self.use_openssl_binary = enforce_openssl_binary_usage
|
|
||||||
|
|
||||||
if self.use_openssl_binary:
|
|
||||||
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 = subprocess.run(["openssl", "ocsp", "-header", "var", "val"],
|
|
||||||
stdout=PIPE, stderr=PIPE, universal_newlines=True,
|
|
||||||
check=False, env=util.env_no_snap_for_external_calls())
|
|
||||||
if "Missing =" in test_host_format.stderr:
|
|
||||||
self.host_args = lambda host: ["Host=" + host]
|
|
||||||
else:
|
|
||||||
self.host_args = lambda host: ["Host", host]
|
|
||||||
|
|
||||||
def ocsp_revoked(self, cert: RenewableCert) -> bool:
|
def ocsp_revoked(self, cert: RenewableCert) -> bool:
|
||||||
"""Get revoked status for a particular cert version.
|
"""Get revoked status for a particular cert version.
|
||||||
|
|
||||||
@@ -76,9 +46,6 @@ class RevocationChecker:
|
|||||||
:rtype: bool
|
:rtype: bool
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if self.broken:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Let's Encrypt doesn't update OCSP for expired certificates,
|
# Let's Encrypt doesn't update OCSP for expired certificates,
|
||||||
# so don't check OCSP if the cert is expired.
|
# so don't check OCSP if the cert is expired.
|
||||||
# https://github.com/certbot/certbot/issues/7152
|
# https://github.com/certbot/certbot/issues/7152
|
||||||
@@ -90,47 +57,8 @@ class RevocationChecker:
|
|||||||
if not host or not url:
|
if not host or not url:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if self.use_openssl_binary:
|
|
||||||
return self._check_ocsp_openssl_bin(cert_path, chain_path, host, url, timeout)
|
|
||||||
return _check_ocsp_cryptography(cert_path, chain_path, url, timeout)
|
return _check_ocsp_cryptography(cert_path, chain_path, url, timeout)
|
||||||
|
|
||||||
def _check_ocsp_openssl_bin(self, cert_path: str, chain_path: str,
|
|
||||||
host: str, url: str, timeout: int) -> bool:
|
|
||||||
# Minimal implementation of proxy selection logic as seen in, e.g., cURL
|
|
||||||
# Some things that won't work, but may well be in use somewhere:
|
|
||||||
# - username and password for proxy authentication
|
|
||||||
# - proxies accepting TLS connections
|
|
||||||
# - proxy exclusion through NO_PROXY
|
|
||||||
env_http_proxy = getenv('http_proxy')
|
|
||||||
env_HTTP_PROXY = getenv('HTTP_PROXY')
|
|
||||||
proxy_host = None
|
|
||||||
if env_http_proxy is not None or env_HTTP_PROXY is not None:
|
|
||||||
proxy_host = env_http_proxy if env_http_proxy is not None else env_HTTP_PROXY
|
|
||||||
if proxy_host is None:
|
|
||||||
url_opts = ["-url", url]
|
|
||||||
else:
|
|
||||||
if proxy_host.startswith('http://'):
|
|
||||||
proxy_host = proxy_host[len('http://'):]
|
|
||||||
url_opts = ["-host", proxy_host, "-path", url]
|
|
||||||
# jdkasten thanks "Bulletproof SSL and TLS - Ivan Ristic" for documenting this!
|
|
||||||
cmd = ["openssl", "ocsp",
|
|
||||||
"-no_nonce",
|
|
||||||
"-issuer", chain_path,
|
|
||||||
"-cert", cert_path,
|
|
||||||
"-CAfile", chain_path,
|
|
||||||
"-verify_other", chain_path,
|
|
||||||
"-trust_other",
|
|
||||||
"-timeout", str(timeout),
|
|
||||||
"-header"] + self.host_args(host) + url_opts
|
|
||||||
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(cert_path: str) -> tuple[Optional[str], Optional[str]]:
|
def _determine_ocsp_server(cert_path: str) -> tuple[Optional[str], Optional[str]]:
|
||||||
"""Extract the OCSP server host from a certificate.
|
"""Extract the OCSP server host from a certificate.
|
||||||
@@ -303,29 +231,3 @@ def _check_ocsp_response_signature(response_ocsp: 'ocsp.OCSPResponse',
|
|||||||
raise AssertionError("no signature hash algorithm defined")
|
raise AssertionError("no signature hash algorithm defined")
|
||||||
crypto_util.verify_signed_payload(responder_cert.public_key(), response_ocsp.signature,
|
crypto_util.verify_signed_payload(responder_cert.public_key(), response_ocsp.signature,
|
||||||
response_ocsp.tbs_response_bytes, chosen_response_hash)
|
response_ocsp.tbs_response_bytes, chosen_response_hash)
|
||||||
|
|
||||||
|
|
||||||
def _translate_ocsp_query(cert_path: str, ocsp_output: str, ocsp_errors: str) -> bool:
|
|
||||||
"""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 ("Response verify OK" not 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.warning("Unable to properly parse OCSP output: %s\nstderr:%s",
|
|
||||||
ocsp_output, ocsp_errors)
|
|
||||||
return False
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
certbot.ocsp.RevocationChecker.__init__ no longer accepts the parameter `enforce_openssl_binary_usage` and always uses the cryptography Python library for OCSP checking.
|
||||||
@@ -9,10 +9,5 @@
|
|||||||
# deprecation warnings and gives time for plugins that don't use the deprecated
|
# deprecation warnings and gives time for plugins that don't use the deprecated
|
||||||
# API to propagate, especially for plugins packaged as an external snap, before
|
# API to propagate, especially for plugins packaged as an external snap, before
|
||||||
# we release breaking changes.
|
# we release breaking changes.
|
||||||
#
|
|
||||||
# The current warnings being ignored are:
|
|
||||||
# 1) Planning to remove support for checking OCSP via OpenSSL binary.
|
|
||||||
# See https://github.com/certbot/certbot/issues/10291.
|
|
||||||
filterwarnings =
|
filterwarnings =
|
||||||
error
|
error
|
||||||
ignore:enforce_openssl_binary_usage parameter is deprecated:DeprecationWarning
|
|
||||||
|
|||||||
Reference in New Issue
Block a user