Repository: certbot/certbot

Issue:
OCSP query can hang for a really long time, needs a shorter timeout added.
## My operating system is (include version):
CentOS 6.

## I installed Certbot with (certbot-auto, OS package manager, pip, etc):
`certbot-auto`.

## I ran this command and it produced this output:
See the forum thread: https://community.letsencrypt.org/t/renewal-failure-to-due-apparent-hang-during-connection/117143

## Certbot's behavior differed from what I expected because:
Certbot should not hang for 20 minutes (allegedly) if it cannot talk to the OCSP server for any reason.

I suggest that both the `openssl` and `requests` versions of the OCSP query should use a suitably shorter timeout parameter, maybe 30 seconds.

## Here is a Certbot log showing the issue (if available):

    Processing /etc/letsencrypt/renewal/[redacted].conf

    ^CExiting abnormally:
    Traceback (most recent call last):
    File “/opt/eff.org/certbot/venv/bin/letsencrypt”, line 11, in
    load_entry_point(‘letsencrypt==0.7.0’, ‘console_scripts’, ‘letsencrypt’)()
    File “/opt/eff.org/certbot/venv/lib64/python3.6/site-packages/certbot/main.py”, line 15, in main
    return internal_main.main(cli_args)
    File “/opt/eff.org/certbot/venv/lib64/python3.6/site-packages/certbot/_internal/main.py”, line 1347, in main
    return config.func(config, plugins)
    File “/opt/eff.org/certbot/venv/lib64/python3.6/site-packages/certbot/_internal/main.py”, line 1255, in renew
    renewal.handle_renewal_request(config)
    File “/opt/eff.org/certbot/venv/lib64/python3.6/site-packages/certbot/_internal/renewal.py”, line 433, in handle_renewal_request
    if should_renew(lineage_config, renewal_candidate):
    File “/opt/eff.org/certbot/venv/lib64/python3.6/site-packages/certbot/_internal/renewal.py”, line 264, in should_renew
    if lineage.should_autorenew():
    File “/opt/eff.org/certbot/venv/lib64/python3.6/site-packages/certbot/_internal/storage.py”, line 946, in should_autorenew
    if self.ocsp_revoked(self.latest_common_version()):
    File “/opt/eff.org/certbot/venv/lib64/python3.6/site-packages/certbot/_internal/storage.py”, line 906, in ocsp_revoked
    chain_path)
    File “/opt/eff.org/certbot/venv/lib64/python3.6/site-packages/certbot/ocsp.py”, line 100, in ocsp_revoked_by_paths
    return _check_ocsp_cryptography(cert_path, chain_path, url)
    File “/opt/eff.org/certbot/venv/lib64/python3.6/site-packages/certbot/ocsp.py”, line 168, in _check_ocsp_cryptography
    headers={‘Content-Type’: ‘application/ocsp-request’})
    File “/opt/eff.org/certbot/venv/lib64/python3.6/site-packages/requests/api.py”, line 116, in post
    return request(‘post’, url, data=data, json=json, **kwargs)
    File “/opt/eff.org/certbot/venv/lib64/python3.6/site-packages/requests/api.py”, line 60, in request
    return session.request(method=method, url=url, **kwargs)
    File “/opt/eff.org/certbot/venv/lib64/python3.6/site-packages/requests/sessions.py”, line 533, in request
    resp = self.send(prep, **send_kwargs)
    File “/opt/eff.org/certbot/venv/lib64/python3.6/site-packages/requests/sessions.py”, line 646, in send
    r = adapter.send(request, **kwargs)
    File “/opt/eff.org/certbot/venv/lib64/python3.6/site-packages/requests/adapters.py”, line 449, in send
    timeout=timeout
    File “/opt/eff.org/certbot/venv/lib64/python3.6/site-packages/urllib3/connectionpool.py”, line 672, in urlopen
    chunked=chunked,
    File “/opt/eff.org/certbot/venv/lib64/python3.6/site-packages/urllib3/connectionpool.py”, line 387, in _make_request
    conn.request(method, url, **httplib_request_kw)
    File “/opt/rh/rh-python36/root/usr/lib64/python3.6/http/client.py”, line 1254, in request
    self._send_request(method, url, body, headers, encode_chunked)
    File “/opt/rh/rh-python36/root/usr/lib64/python3.6/http/client.py”, line 1300, in _send_request
    self.endheaders(body, encode_chunked=encode_chunked)
    File “/opt/rh/rh-python36/root/usr/lib64/python3.6/http/client.py”, line 1249, in endheaders
    self._send_output(message_body, encode_chunked=encode_chunked)
    File “/opt/rh/rh-python36/root/usr/lib64/python3.6/http/client.py”, line 1036, in _send_output
    self.send(msg)
    File “/opt/rh/rh-python36/root/usr/lib64/python3.6/http/client.py”, line 974, in send
    self.connect()
    File “/opt/eff.org/certbot/venv/lib64/python3.6/site-packages/urllib3/connection.py”, line 184, in connect
    conn = self._new_conn()
    File “/opt/eff.org/certbot/venv/lib64/python3.6/site-packages/urllib3/connection.py”, line 157, in _new_conn
    (self._dns_host, self.port), self.timeout, **extra_kw
    File “/opt/eff.org/certbot/venv/lib64/python3.6/site-packages/urllib3/util/connection.py”, line 74, in create_connection
    sock.connect(sa)
    KeyboardInterrupt
    Please see the logfiles in /var/log/letsencrypt for more details.


Discussion:

Resolving patch:
diff --git a/certbot/certbot/ocsp.py b/certbot/certbot/ocsp.py
--- a/certbot/certbot/ocsp.py
+++ b/certbot/certbot/ocsp.py
@@ -70,12 +70,13 @@ def ocsp_revoked(self, cert):
         """
         return self.ocsp_revoked_by_paths(cert.cert_path, cert.chain_path)
 
-    def ocsp_revoked_by_paths(self, cert_path, chain_path):
-        # type: (str, str) -> bool
+    def ocsp_revoked_by_paths(self, cert_path, chain_path, timeout=10):
+        # type: (str, str, int) -> bool
         """Performs the OCSP revocation check
 
         :param str cert_path: Certificate filepath
-        :param str chain_path: Certificate chain filepath
+        :param str chain_path: Certificate chain
+        :param int timeout: Timeout (in seconds) for the OCSP query
 
         :returns: True if revoked; False if valid or the check failed or cert is expired.
         :rtype: bool
@@ -96,11 +97,11 @@ def ocsp_revoked_by_paths(self, cert_path, chain_path):
             return False
 
         if self.use_openssl_binary:
-            return self._check_ocsp_openssl_bin(cert_path, chain_path, host, url)
-        return _check_ocsp_cryptography(cert_path, chain_path, url)
+            return self._check_ocsp_openssl_bin(cert_path, chain_path, host, url, timeout)
+        return _check_ocsp_cryptography(cert_path, chain_path, url, timeout)
 
-    def _check_ocsp_openssl_bin(self, cert_path, chain_path, host, url):
-        # type: (str, str, str, str) -> bool
+    def _check_ocsp_openssl_bin(self, cert_path, chain_path, host, url, timeout):
+        # type: (str, str, str, str, int) -> bool
         # jdkasten thanks "Bulletproof SSL and TLS - Ivan Ristic" for documenting this!
         cmd = ["openssl", "ocsp",
                "-no_nonce",
@@ -110,6 +111,7 @@ def _check_ocsp_openssl_bin(self, cert_path, chain_path, host, url):
                "-CAfile", chain_path,
                "-verify_other", chain_path,
                "-trust_other",
+               "-timeout", str(timeout),
                "-header"] + self.host_args(host)
         logger.debug("Querying OCSP for %s", cert_path)
         logger.debug(" ".join(cmd))
@@ -152,8 +154,8 @@ def _determine_ocsp_server(cert_path):
     return None, None
 
 
-def _check_ocsp_cryptography(cert_path, chain_path, url):
-    # type: (str, str, str) -> bool
+def _check_ocsp_cryptography(cert_path, chain_path, url, timeout):
+    # type: (str, str, str, int) -> bool
     # Retrieve OCSP response
     with open(chain_path, 'rb') as file_handler:
         issuer = x509.load_pem_x509_certificate(file_handler.read(), default_backend())
@@ -165,7 +167,8 @@ def _check_ocsp_cryptography(cert_path, chain_path, url):
     request_binary = request.public_bytes(serialization.Encoding.DER)
     try:
         response = requests.post(url, data=request_binary,
-                                 headers={'Content-Type': 'application/ocsp-request'})
+                                 headers={'Content-Type': 'application/ocsp-request'},
+                                 timeout=timeout)
     except requests.exceptions.RequestException:
         logger.info("OCSP check failed for %s (are we offline?)", cert_path, exc_info=True)
         return False


Test patch:
diff --git a/certbot/tests/ocsp_test.py b/certbot/tests/ocsp_test.py
--- a/certbot/tests/ocsp_test.py
+++ b/certbot/tests/ocsp_test.py
@@ -162,11 +162,11 @@ def setUp(self):
 
     @mock.patch('certbot.ocsp._determine_ocsp_server')
     @mock.patch('certbot.ocsp._check_ocsp_cryptography')
-    def test_ensure_cryptography_toggled(self, mock_revoke, mock_determine):
+    def test_ensure_cryptography_toggled(self, mock_check, mock_determine):
         mock_determine.return_value = ('http://example.com', 'example.com')
         self.checker.ocsp_revoked(self.cert_obj)
 
-        mock_revoke.assert_called_once_with(self.cert_path, self.chain_path, 'http://example.com')
+        mock_check.assert_called_once_with(self.cert_path, self.chain_path, 'http://example.com', 10)
 
     def test_revoke(self):
         with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL):