Repository: certbot/certbot

Issue:
Certbot can't verify OCSP responses signed by an explicitly designated responder
If you're having trouble using Certbot and aren't sure you've found a bug or
request for a new feature, please first try asking for help at
https://community.letsencrypt.org/. There is a much larger community there of
people familiar with the project who will be able to more quickly answer your
questions.

## My operating system is (include version):

Debian stretch/sid

## I installed Certbot with (certbot-auto, OS package manager, pip, etc):

certbot-auto

## I ran this command and it produced this output:

./certbot-auto certificates --cert-name buypasstest1

Saving debug log to /var/log/letsencrypt/letsencrypt.log
Invalid signature on OCSP response for /etc/letsencrypt/live/buypasstest1/cert.pem

## Certbot's behavior differed from what I expected because:

The OCSP response is correctly signed, not by the issuer itself but by a properly designated responder.

## Here is a Certbot log showing the issue (if available):
###### Logs are stored in `/var/log/letsencrypt` by default. Feel free to redact domains, e-mail and IP addresses as you see fit.

2019-05-12 13:32:13,645:DEBUG:certbot.main:certbot version: 0.34.2
2019-05-12 13:32:13,647:DEBUG:certbot.main:Arguments: ['--cert-name', 'buypasstest1']
2019-05-12 13:32:13,647:DEBUG:certbot.main:Discovered plugins: PluginsRegistry(PluginEntryPoint#apache,PluginEntryPoint#manual,PluginEntryPoint#nginx,PluginEntryPoint#null,PluginEntryPoint#standalone,PluginEntryPoint#webroot)
2019-05-12 13:32:13,719:DEBUG:certbot.log:Root logging level set at 20
2019-05-12 13:32:13,721:INFO:certbot.log:Saving debug log to /var/log/letsencrypt/letsencrypt.log
2019-05-12 13:32:13,770:DEBUG:urllib3.connectionpool:Starting new HTTP connection (1): ocsp.buypass.com:80
2019-05-12 13:32:13,994:DEBUG:urllib3.connectionpool:http://ocsp.buypass.com:80 "POST / HTTP/1.1" 200 1702
2019-05-12 13:32:13,998:ERROR:certbot.ocsp:Invalid signature on OCSP response for /etc/letsencrypt/live/buypasstest1/cert.pem

## Here is the relevant nginx server block or Apache virtualhost for the domain I am configuring:

<doesn't matter.>



Discussion:
Quickly looking at the source code, I think the problem lies at https://github.com/certbot/certbot/blob/7fe82cf1ac4c6e40ab4a21a5a8822db200cb28ad/certbot/ocsp.py#L214.
This function takes into account only the first case of RFC6960 clause 4.2.2.2, and not the second one.

Resolving patch:
diff --git a/certbot/ocsp.py b/certbot/ocsp.py
--- a/certbot/ocsp.py
+++ b/certbot/ocsp.py
@@ -155,7 +155,7 @@ def _check_ocsp_cryptography(cert_path, chain_path, url):
 
     # Check OCSP signature
     try:
-        _check_ocsp_response(response_ocsp, request, issuer)
+        _check_ocsp_response(response_ocsp, request, issuer, cert_path)
     except UnsupportedAlgorithm as e:
         logger.error(str(e))
     except errors.Error as e:
@@ -173,7 +173,7 @@ def _check_ocsp_cryptography(cert_path, chain_path, url):
     return False
 
 
-def _check_ocsp_response(response_ocsp, request_ocsp, issuer_cert):
+def _check_ocsp_response(response_ocsp, request_ocsp, issuer_cert, cert_path):
     """Verify that the OCSP is valid for serveral criterias"""
     # Assert OCSP response corresponds to the certificate we are talking about
     if response_ocsp.serial_number != request_ocsp.serial_number:
@@ -181,7 +181,7 @@ def _check_ocsp_response(response_ocsp, request_ocsp, issuer_cert):
                              'to the certificate in request')
 
     # Assert signature is valid
-    _check_ocsp_response_signature(response_ocsp, issuer_cert)
+    _check_ocsp_response_signature(response_ocsp, issuer_cert, cert_path)
 
     # Assert issuer in response is the expected one
     if (not isinstance(response_ocsp.hash_algorithm, type(request_ocsp.hash_algorithm))
@@ -207,11 +207,52 @@ def _check_ocsp_response(response_ocsp, request_ocsp, issuer_cert):
         raise AssertionError('param nextUpdate is in the past.')
 
 
-def _check_ocsp_response_signature(response_ocsp, issuer_cert):
-    """Verify an OCSP response signature against certificate issuer"""
+def _check_ocsp_response_signature(response_ocsp, issuer_cert, cert_path):
+    """Verify an OCSP response signature against certificate issuer or responder"""
+    if response_ocsp.responder_name == issuer_cert.subject:
+        # Case where the OCSP responder is also the certificate issuer
+        logger.debug('OCSP response for certificate %s is signed by the certificate\'s issuer.',
+                     cert_path)
+        responder_cert = issuer_cert
+    else:
+        # Case where the OCSP responder is not the certificate issuer
+        logger.debug('OCSP response for certificate %s is delegated to an external responder.',
+                     cert_path)
+
+        responder_certs = [cert for cert in response_ocsp.certificates
+                           if cert.subject == response_ocsp.responder_name]
+        if not responder_certs:
+            raise AssertionError('no matching responder certificate could be found')
+
+        # We suppose here that the ACME server support only one certificate in the OCSP status
+        # request. This is currently the case for LetsEncrypt servers.
+        # See https://github.com/letsencrypt/boulder/issues/2331
+        responder_cert = responder_certs[0]
+
+        if responder_cert.issuer != issuer_cert.subject:
+            raise AssertionError('responder certificate is not signed '
+                                 'by the certificate\'s issuer')
+
+        try:
+            extension = responder_cert.extensions.get_extension_for_class(x509.ExtendedKeyUsage)
+            delegate_authorized = x509.oid.ExtendedKeyUsageOID.OCSP_SIGNING in extension.value
+        except (x509.ExtensionNotFound, IndexError):
+            delegate_authorized = False
+        if not delegate_authorized:
+            raise AssertionError('responder is not authorized by issuer to sign OCSP responses')
+
+        # Following line may raise UnsupportedAlgorithm
+        chosen_hash = responder_cert.signature_hash_algorithm
+        # For a delegate OCSP responder, we need first check that its certificate is effectively
+        # signed by the certificate issuer.
+        crypto_util.verify_signed_payload(issuer_cert.public_key(), responder_cert.signature,
+                                          responder_cert.tbs_certificate_bytes, chosen_hash)
+
     # Following line may raise UnsupportedAlgorithm
     chosen_hash = response_ocsp.signature_hash_algorithm
-    crypto_util.verify_signed_payload(issuer_cert.public_key(), response_ocsp.signature,
+    # We check that the OSCP response is effectively signed by the responder
+    # (an authorized delegate one or the certificate issuer itself).
+    crypto_util.verify_signed_payload(responder_cert.public_key(), response_ocsp.signature,
                                       response_ocsp.tbs_response_bytes, chosen_hash)
 
 


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
@@ -1,5 +1,6 @@
 """Tests for ocsp.py"""
 # pylint: disable=protected-access
+import contextlib
 import unittest
 from datetime import datetime, timedelta
 
@@ -87,11 +88,11 @@ def test_ocsp_revoked(self, mock_run, mock_determine):
         self.assertEqual(mock_run.call_count, 2)
 
     def test_determine_ocsp_server(self):
-        cert_path = test_util.vector_path('google_certificate.pem')
+        cert_path = test_util.vector_path('ocsp_certificate.pem')
 
         from certbot import ocsp
         result = ocsp._determine_ocsp_server(cert_path)
-        self.assertEqual(('http://ocsp.digicert.com', 'ocsp.digicert.com'), result)
+        self.assertEqual(('http://ocsp.test4.buypass.com', 'ocsp.test4.buypass.com'), result)
 
     @mock.patch('certbot.ocsp.logger')
     @mock.patch('certbot.util.run_script')
@@ -128,8 +129,8 @@ class OSCPTestCryptography(unittest.TestCase):
     def setUp(self):
         from certbot import ocsp
         self.checker = ocsp.RevocationChecker()
-        self.cert_path = test_util.vector_path('google_certificate.pem')
-        self.chain_path = test_util.vector_path('google_issuer_certificate.pem')
+        self.cert_path = test_util.vector_path('ocsp_certificate.pem')
+        self.chain_path = test_util.vector_path('ocsp_issuer_certificate.pem')
 
     @mock.patch('certbot.ocsp._determine_ocsp_server')
     @mock.patch('certbot.ocsp._check_ocsp_cryptography')
@@ -139,91 +140,140 @@ def test_ensure_cryptography_toggled(self, mock_revoke, mock_determine):
 
         mock_revoke.assert_called_once_with(self.cert_path, self.chain_path, 'http://example.com')
 
-    @mock.patch('certbot.ocsp.requests.post')
-    @mock.patch('certbot.ocsp.ocsp.load_der_ocsp_response')
-    def test_revoke(self, mock_ocsp_response, mock_post):
-        with mock.patch('certbot.ocsp.crypto_util.verify_signed_payload'):
-            mock_ocsp_response.return_value = _construct_mock_ocsp_response(
-                ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL)
-            mock_post.return_value = mock.Mock(status_code=200)
+    def test_revoke(self):
+        with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL):
             revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path)
-
         self.assertTrue(revoked)
 
-    @mock.patch('certbot.ocsp.crypto_util.verify_signed_payload')
-    @mock.patch('certbot.ocsp.requests.post')
-    @mock.patch('certbot.ocsp.ocsp.load_der_ocsp_response')
-    def test_revoke_resiliency(self, mock_ocsp_response, mock_post, mock_check):
+    def test_responder_is_issuer(self):
+        issuer = x509.load_pem_x509_certificate(
+            test_util.load_vector('ocsp_issuer_certificate.pem'), default_backend())
+
+        with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED,
+                        ocsp_lib.OCSPResponseStatus.SUCCESSFUL) as mocks:
+            mocks['mock_response'].return_value.responder_name = issuer.subject
+            self.checker.ocsp_revoked(self.cert_path, self.chain_path)
+        # Here responder and issuer are the same. So only the signature of the OCSP
+        # response is checked (using the issuer/responder public key).
+        self.assertEqual(mocks['mock_check'].call_count, 1)
+        self.assertEqual(mocks['mock_check'].call_args[0][0].public_numbers(),
+                         issuer.public_key().public_numbers())
+
+    def test_responder_is_authorized_delegate(self):
+        issuer = x509.load_pem_x509_certificate(
+            test_util.load_vector('ocsp_issuer_certificate.pem'), default_backend())
+        responder = x509.load_pem_x509_certificate(
+            test_util.load_vector('ocsp_responder_certificate.pem'), default_backend())
+
+        with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED,
+                        ocsp_lib.OCSPResponseStatus.SUCCESSFUL) as mocks:
+            self.checker.ocsp_revoked(self.cert_path, self.chain_path)
+        # Here responder and issuer are not the same. Two signatures will be checked then,
+        # first to verify the responder cert (using the issuer public key), second to
+        # to verify the OCSP response itself (using the responder public key).
+        self.assertEqual(mocks['mock_check'].call_count, 2)
+        self.assertEqual(mocks['mock_check'].call_args_list[0][0][0].public_numbers(),
+                         issuer.public_key().public_numbers())
+        self.assertEqual(mocks['mock_check'].call_args_list[1][0][0].public_numbers(),
+                         responder.public_key().public_numbers())
+
+    def test_revoke_resiliency(self):
         # Server return an invalid HTTP response
-        mock_ocsp_response.return_value = _construct_mock_ocsp_response(
-            ocsp_lib.OCSPCertStatus.UNKNOWN, ocsp_lib.OCSPResponseStatus.SUCCESSFUL)
-        mock_post.return_value = mock.Mock(status_code=400)
-        revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path)
-
+        with _ocsp_mock(ocsp_lib.OCSPCertStatus.UNKNOWN, ocsp_lib.OCSPResponseStatus.SUCCESSFUL,
+                        http_status_code=400):
+            revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path)
         self.assertFalse(revoked)
 
         # OCSP response in invalid
-        mock_ocsp_response.return_value = _construct_mock_ocsp_response(
-            ocsp_lib.OCSPCertStatus.UNKNOWN, ocsp_lib.OCSPResponseStatus.UNAUTHORIZED)
-        mock_post.return_value = mock.Mock(status_code=200)
-        revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path)
-
+        with _ocsp_mock(ocsp_lib.OCSPCertStatus.UNKNOWN, ocsp_lib.OCSPResponseStatus.UNAUTHORIZED):
+            revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path)
         self.assertFalse(revoked)
 
         # OCSP response is valid, but certificate status is unknown
-        mock_ocsp_response.return_value = _construct_mock_ocsp_response(
-            ocsp_lib.OCSPCertStatus.UNKNOWN, ocsp_lib.OCSPResponseStatus.SUCCESSFUL)
-        mock_post.return_value = mock.Mock(status_code=200)
-        revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path)
-
+        with _ocsp_mock(ocsp_lib.OCSPCertStatus.UNKNOWN, ocsp_lib.OCSPResponseStatus.SUCCESSFUL):
+            revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path)
         self.assertFalse(revoked)
 
         # The OCSP response says that the certificate is revoked, but certificate
         # does not contain the OCSP extension.
-        mock_ocsp_response.return_value = _construct_mock_ocsp_response(
-            ocsp_lib.OCSPCertStatus.UNKNOWN, ocsp_lib.OCSPResponseStatus.SUCCESSFUL)
-        mock_post.return_value = mock.Mock(status_code=200)
-        with mock.patch('cryptography.x509.Extensions.get_extension_for_class',
-                        side_effect=x509.ExtensionNotFound(
-                            'Not found', x509.AuthorityInformationAccessOID.OCSP)):
+        with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL):
+            with mock.patch('cryptography.x509.Extensions.get_extension_for_class',
+                            side_effect=x509.ExtensionNotFound(
+                                'Not found', x509.AuthorityInformationAccessOID.OCSP)):
+                revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path)
+        self.assertFalse(revoked)
+
+        # OCSP response uses an unsupported signature.
+        with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL,
+                        check_signature_side_effect=UnsupportedAlgorithm('foo')):
             revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path)
+        self.assertFalse(revoked)
 
+        # OSCP signature response is invalid.
+        with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL,
+                        check_signature_side_effect=InvalidSignature('foo')):
+            revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path)
         self.assertFalse(revoked)
 
-        # Valid response, OCSP extension is present,
-        # but OCSP response uses an unsupported signature.
-        mock_ocsp_response.return_value = _construct_mock_ocsp_response(
-            ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL)
-        mock_post.return_value = mock.Mock(status_code=200)
-        mock_check.side_effect = UnsupportedAlgorithm('foo')
-        revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path)
+        # Assertion error on OCSP response validity
+        with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL,
+                        check_signature_side_effect=AssertionError('foo')):
+            revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path)
+        self.assertFalse(revoked)
 
+        # No responder cert in OCSP response
+        with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED,
+                        ocsp_lib.OCSPResponseStatus.SUCCESSFUL) as mocks:
+            mocks['mock_response'].return_value.certificates = []
+            revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path)
         self.assertFalse(revoked)
 
-        # And now, the signature itself is invalid.
-        mock_ocsp_response.return_value = _construct_mock_ocsp_response(
-            ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL)
-        mock_post.return_value = mock.Mock(status_code=200)
-        mock_check.side_effect = InvalidSignature('foo')
-        revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path)
+        # Responder cert is not signed by certificate issuer
+        with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED,
+                        ocsp_lib.OCSPResponseStatus.SUCCESSFUL) as mocks:
+            cert = mocks['mock_response'].return_value.certificates[0]
+            mocks['mock_response'].return_value.certificates[0] = mock.Mock(
+                issuer='fake', subject=cert.subject)
+            revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path)
+        self.assertFalse(revoked)
 
+        with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL):
+            # This mock is necessary to avoid the first call contained in _determine_ocsp_server
+            # of the method cryptography.x509.Extensions.get_extension_for_class.
+            with mock.patch('certbot.ocsp._determine_ocsp_server') as mock_server:
+                mock_server.return_value = ('https://example.com', 'example.com')
+                with mock.patch('cryptography.x509.Extensions.get_extension_for_class',
+                                side_effect=x509.ExtensionNotFound(
+                                    'Not found', x509.AuthorityInformationAccessOID.OCSP)):
+                    revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path)
         self.assertFalse(revoked)
 
-        # Finally, assertion error on OCSP response validity
-        mock_ocsp_response.return_value = _construct_mock_ocsp_response(
-            ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL)
-        mock_post.return_value = mock.Mock(status_code=200)
-        mock_check.side_effect = AssertionError('foo')
-        revoked = self.checker.ocsp_revoked(self.cert_path, self.chain_path)
 
-        self.assertFalse(revoked)
+@contextlib.contextmanager
+def _ocsp_mock(certificate_status, response_status,
+               http_status_code=200, check_signature_side_effect=None):
+    with mock.patch('certbot.ocsp.ocsp.load_der_ocsp_response') as mock_response:
+        mock_response.return_value = _construct_mock_ocsp_response(
+            certificate_status, response_status)
+        with mock.patch('certbot.ocsp.requests.post') as mock_post:
+            mock_post.return_value = mock.Mock(status_code=http_status_code)
+            with mock.patch('certbot.ocsp.crypto_util.verify_signed_payload') as mock_check:
+                if check_signature_side_effect:
+                    mock_check.side_effect = check_signature_side_effect
+                yield {
+                    'mock_response': mock_response,
+                    'mock_post': mock_post,
+                    'mock_check': mock_check,
+                }
 
 
 def _construct_mock_ocsp_response(certificate_status, response_status):
     cert = x509.load_pem_x509_certificate(
-        test_util.load_vector('google_certificate.pem'), default_backend())
+        test_util.load_vector('ocsp_certificate.pem'), default_backend())
     issuer = x509.load_pem_x509_certificate(
-        test_util.load_vector('google_issuer_certificate.pem'), default_backend())
+        test_util.load_vector('ocsp_issuer_certificate.pem'), default_backend())
+    responder = x509.load_pem_x509_certificate(
+        test_util.load_vector('ocsp_responder_certificate.pem'), default_backend())
     builder = ocsp_lib.OCSPRequestBuilder()
     builder = builder.add_certificate(cert, issuer, hashes.SHA1())
     request = builder.build()
@@ -234,6 +284,8 @@ def _construct_mock_ocsp_response(certificate_status, response_status):
         serial_number=request.serial_number,
         issuer_key_hash=request.issuer_key_hash,
         issuer_name_hash=request.issuer_name_hash,
+        responder_name=responder.subject,
+        certificates=[responder],
         hash_algorithm=hashes.SHA1(),
         next_update=datetime.now() + timedelta(days=1),
         this_update=datetime.now() - timedelta(days=1),
diff --git a/certbot/tests/testdata/google_certificate.pem b/certbot/tests/testdata/google_certificate.pem
deleted file mode 100644
--- a/certbot/tests/testdata/google_certificate.pem
+++ /dev/null
@@ -1,41 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIHQjCCBiqgAwIBAgIQCgYwQn9bvO1pVzllk7ZFHzANBgkqhkiG9w0BAQsFADB1
-MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
-d3cuZGlnaWNlcnQuY29tMTQwMgYDVQQDEytEaWdpQ2VydCBTSEEyIEV4dGVuZGVk
-IFZhbGlkYXRpb24gU2VydmVyIENBMB4XDTE4MDUwODAwMDAwMFoXDTIwMDYwMzEy
-MDAwMFowgccxHTAbBgNVBA8MFFByaXZhdGUgT3JnYW5pemF0aW9uMRMwEQYLKwYB
-BAGCNzwCAQMTAlVTMRkwFwYLKwYBBAGCNzwCAQITCERlbGF3YXJlMRAwDgYDVQQF
-Ewc1MTU3NTUwMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQG
-A1UEBxMNU2FuIEZyYW5jaXNjbzEVMBMGA1UEChMMR2l0SHViLCBJbmMuMRMwEQYD
-VQQDEwpnaXRodWIuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
-xjyq8jyXDDrBTyitcnB90865tWBzpHSbindG/XqYQkzFMBlXmqkzC+FdTRBYyneZ
-w5Pz+XWQvL+74JW6LsWNc2EF0xCEqLOJuC9zjPAqbr7uroNLghGxYf13YdqbG5oj
-/4x+ogEG3dF/U5YIwVr658DKyESMV6eoYV9mDVfTuJastkqcwero+5ZAKfYVMLUE
-sMwFtoTDJFmVf6JlkOWwsxp1WcQ/MRQK1cyqOoUFUgYylgdh3yeCDPeF22Ax8AlQ
-xbcaI+GwfQL1FB7Jy+h+KjME9lE/UpgV6Qt2R1xNSmvFCBWu+NFX6epwFP/JRbkM
-fLz0beYFUvmMgLtwVpEPSwIDAQABo4IDeTCCA3UwHwYDVR0jBBgwFoAUPdNQpdag
-re7zSmAKZdMh1Pj41g8wHQYDVR0OBBYEFMnCU2FmnV+rJfQmzQ84mqhJ6kipMCUG
-A1UdEQQeMByCCmdpdGh1Yi5jb22CDnd3dy5naXRodWIuY29tMA4GA1UdDwEB/wQE
-AwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwdQYDVR0fBG4wbDA0
-oDKgMIYuaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL3NoYTItZXYtc2VydmVyLWcy
-LmNybDA0oDKgMIYuaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL3NoYTItZXYtc2Vy
-dmVyLWcyLmNybDBLBgNVHSAERDBCMDcGCWCGSAGG/WwCATAqMCgGCCsGAQUFBwIB
-FhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BTMAcGBWeBDAEBMIGIBggrBgEF
-BQcBAQR8MHowJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBS
-BggrBgEFBQcwAoZGaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0
-U0hBMkV4dGVuZGVkVmFsaWRhdGlvblNlcnZlckNBLmNydDAMBgNVHRMBAf8EAjAA
-MIIBfgYKKwYBBAHWeQIEAgSCAW4EggFqAWgAdgCkuQmQtBhYFIe7E6LMZ3AKPDWY
-BPkb37jjd80OyA3cEAAAAWNBYm0KAAAEAwBHMEUCIQDRZp38cTWsWH2GdBpe/uPT
-Wnsu/m4BEC2+dIcvSykZYgIgCP5gGv6yzaazxBK2NwGdmmyuEFNSg2pARbMJlUFg
-U5UAdgBWFAaaL9fC7NP14b1Esj7HRna5vJkRXMDvlJhV1onQ3QAAAWNBYm0tAAAE
-AwBHMEUCIQCi7omUvYLm0b2LobtEeRAYnlIo7n6JxbYdrtYdmPUWJQIgVgw1AZ51
-vK9ENinBg22FPxb82TvNDO05T17hxXRC2IYAdgC72d+8H4pxtZOUI5eqkntHOFeV
-CqtS6BqQlmQ2jh7RhQAAAWNBYm3fAAAEAwBHMEUCIQChzdTKUU2N+XcqcK0OJYrN
-8EYynloVxho4yPk6Dq3EPgIgdNH5u8rC3UcslQV4B9o0a0w204omDREGKTVuEpxG
-eOQwDQYJKoZIhvcNAQELBQADggEBAHAPWpanWOW/ip2oJ5grAH8mqQfaunuCVE+v
-ac+88lkDK/LVdFgl2B6kIHZiYClzKtfczG93hWvKbST4NRNHP9LiaQqdNC17e5vN
-HnXVUGw+yxyjMLGqkgepOnZ2Rb14kcTOGp4i5AuJuuaMwXmCo7jUwPwfLe1NUlVB
-Kqg6LK0Hcq4K0sZnxE8HFxiZ92WpV2AVWjRMEc/2z2shNoDvxvFUYyY1Oe67xINk
-myQKc+ygSBZzyLnXSFVWmHr3u5dcaaQGGAR42v6Ydr4iL38Hd4dOiBma+FXsXBIq
-WUjbST4VXmdaol7uzFMojA4zkxQDZAvF5XgJlAFadfySna/teik=
------END CERTIFICATE-----
diff --git a/certbot/tests/testdata/google_issuer_certificate.pem b/certbot/tests/testdata/google_issuer_certificate.pem
deleted file mode 100644
--- a/certbot/tests/testdata/google_issuer_certificate.pem
+++ /dev/null
@@ -1,26 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIEXDCCA0SgAwIBAgINAeOpMBz8cgY4P5pTHTANBgkqhkiG9w0BAQsFADBMMSAw
-HgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEGA1UEChMKR2xvYmFs
-U2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjAeFw0xNzA2MTUwMDAwNDJaFw0yMTEy
-MTUwMDAwNDJaMFQxCzAJBgNVBAYTAlVTMR4wHAYDVQQKExVHb29nbGUgVHJ1c3Qg
-U2VydmljZXMxJTAjBgNVBAMTHEdvb2dsZSBJbnRlcm5ldCBBdXRob3JpdHkgRzMw
-ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDKUkvqHv/OJGuo2nIYaNVW
-XQ5IWi01CXZaz6TIHLGp/lOJ+600/4hbn7vn6AAB3DVzdQOts7G5pH0rJnnOFUAK
-71G4nzKMfHCGUksW/mona+Y2emJQ2N+aicwJKetPKRSIgAuPOB6Aahh8Hb2XO3h9
-RUk2T0HNouB2VzxoMXlkyW7XUR5mw6JkLHnA52XDVoRTWkNty5oCINLvGmnRsJ1z
-ouAqYGVQMc/7sy+/EYhALrVJEA8KbtyX+r8snwU5C1hUrwaW6MWOARa8qBpNQcWT
-kaIeoYvy/sGIJEmjR0vFEwHdp1cSaWIr6/4g72n7OqXwfinu7ZYW97EfoOSQJeAz
-AgMBAAGjggEzMIIBLzAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUH
-AwEGCCsGAQUFBwMCMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFHfCuFCa
-Z3Z2sS3ChtCDoH6mfrpLMB8GA1UdIwQYMBaAFJviB1dnHB7AagbeWbSaLd/cGYYu
-MDUGCCsGAQUFBwEBBCkwJzAlBggrBgEFBQcwAYYZaHR0cDovL29jc3AucGtpLmdv
-b2cvZ3NyMjAyBgNVHR8EKzApMCegJaAjhiFodHRwOi8vY3JsLnBraS5nb29nL2dz
-cjIvZ3NyMi5jcmwwPwYDVR0gBDgwNjA0BgZngQwBAgIwKjAoBggrBgEFBQcCARYc
-aHR0cHM6Ly9wa2kuZ29vZy9yZXBvc2l0b3J5LzANBgkqhkiG9w0BAQsFAAOCAQEA
-HLeJluRT7bvs26gyAZ8so81trUISd7O45skDUmAge1cnxhG1P2cNmSxbWsoiCt2e
-ux9LSD+PAj2LIYRFHW31/6xoic1k4tbWXkDCjir37xTTNqRAMPUyFRWSdvt+nlPq
-wnb8Oa2I/maSJukcxDjNSfpDh/Bd1lZNgdd/8cLdsE3+wypufJ9uXO1iQpnh9zbu
-FIwsIONGl1p3A8CgxkqI/UAih3JaGOqcpcdaCIzkBaR9uYQ1X4k2Vg5APRLouzVy
-7a8IVk6wuy6pm+T7HT4LY8ibS5FEZlfAFLSW8NwsVz9SBK2Vqn1N0PIMn5xA6NZV
-c7o835DLAFshEWfC7TIe3g==
------END CERTIFICATE-----
diff --git a/certbot/tests/testdata/ocsp_certificate.pem b/certbot/tests/testdata/ocsp_certificate.pem
new file mode 100644
--- /dev/null
+++ b/certbot/tests/testdata/ocsp_certificate.pem
@@ -0,0 +1,37 @@
+-----BEGIN CERTIFICATE-----
+MIIGYDCCBEigAwIBAgIKcjrC4hZcebbtODANBgkqhkiG9w0BAQsFADBRMQswCQYD
+VQQGEwJOTzEdMBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIzAhBgNVBAMM
+GkJ1eXBhc3MgQ2xhc3MgMiBUZXN0NCBDQSA1MB4XDTE5MDUxMjE1NTgyMVoXDTE5
+MTEwODIyNTkwMFowADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK9P
+b+YhJPypm4ui+AZUHPrJ6IsB9R/6Wvgec2G/GuW/UNQFktIhU10HOHAbiJeYLqNZ
+1Cia8JD6NXXGbprOjIbZWvjulYTaLSlClcK0H7HZrcgrK60OeIGEtur27ga68RML
+hs1FG7TNyWVysifOtwW9Oo1mZQQtxViiE2Yb+Q4QqIxitnbrnFmKrVJSUHVXi8/I
+BK1yLrJiRBZMIw0wvAWcWEG2Gpp9PAbemlb11Zx8sm/RSGh7u60rmETbB2Pu941s
+XJCSQRtq5yKdtjIJTIgbe12SPkknqTqa3aUh7hgho0IymlDSeeocL60SUiUAsPEr
+QRWleodOR1ChXz5mFokCAwEAAaOCAokwggKFMAkGA1UdEwQCMAAwHwYDVR0jBBgw
+FoAUd9nQBpFm2N0ZJo1JrNowL2p7YrEwHQYDVR0OBBYEFExS23I6sLCeO6KIxzoc
+tr9s+HmiMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB
+BQUHAwIwIAYDVR0gBBkwFzALBglghEIBGgEAAgcwCAYGZ4EMAQIBMEIGA1UdHwQ7
+MDkwN6A1oDOGMWh0dHA6Ly9jcmwudGVzdDQuYnV5cGFzcy5uby9jcmwvQlBDbGFz
+czJUNENBNS5jcmwwIQYDVR0RAQH/BBcwFYITYnV5cGFzcy5wYWNhbGlzLm5ldDB4
+BggrBgEFBQcBAQRsMGowKQYIKwYBBQUHMAGGHWh0dHA6Ly9vY3NwLnRlc3Q0LmJ1
+eXBhc3MuY29tMD0GCCsGAQUFBzAChjFodHRwOi8vY3J0LnRlc3Q0LmJ1eXBhc3Mu
+bm8vY3J0L0JQQ2xhc3MyVDRDQTUuY2VyMIIBBAYKKwYBBAHWeQIEAgSB9QSB8gDw
+AHYAsMyD5aX5fWuvfAnMKEkEhyrH6IsTLGNQt8b9JuFsbHcAAAFqrMQ/cQAABAMA
+RzBFAiEA1oWB4c6q7+tqGA4HhLNACOemr9c2aIUuWxeQE7/PlSYCIEolZ7pWVs1J
+VyQW/AqeuXGB7qScwUgLh9C1uOJoeRe6AHYAsMyD5aX5fWuvfAnMKEkEhyrH6IsT
+LGNQt8b9JuFsbHcAAAFqrMQ/cQAABAMARzBFAiAoLaNvIwMDifsDAXJBsAKHlYx7
+QPLXL8onYKm8f+Sf1wIhAMepo2GX84UR7WtooqzkBZLG+PaBy1zMuUAG6mwnroF9
+MA0GCSqGSIb3DQEBCwUAA4ICAQAPWLdjNS5lLL5SEtghYebtDmNj2968NYSDvb1L
+1/uFwg3LCVRR1Xb3z1Hc/sc1W0IFXU0zOqEQiuP8jkVP7UqkaWuK5Eu0eP0zPI83
+WBZM0+eBwxwzIMK/Q7fYKTu1+vg/FlH0WhtV43DQSik66366zvPi2Tfag9IPvRei
+DOjbSOBF0o4er2oCrtI0lK5YrHOdWtD7xwQIuA606P9ucuufMf+JcmduRJsVZ2Zu
+3K32SMDdAnyjvQWZNbt1ex3G8vuFQEi690UBhPcha/SO8QvLS89wcaLJnyMIWdv7
+54cbw+fa1nLKM7qph6Mk1yb0qpomPqLmKw4T6WX36c0vDlFSpexJLGgWDFqLUxPN
+qV7cJz4mi1qaYfdWXRrnyU4bl55pHTTgEzbohV7apsmytkCe1uFNrpcTh8jzAhGN
+PQqarX9UoESR56B/ufbBGlBWi0pkV49BFks6Ue0GVKo7djoxuV6+SsmYSE+6MNPv
+IUsm54TSnwxjA8WyG7pl14g1hkGFQ4NRYJMiVqK3DMABaPxVmT7NRxUQQiM0mmM7
+EKNzLBeWHJF5ecdDR1MiIF3ayn+RiZb0r8aSQBMLwN1YwUZw+hSYz1eCd7bHN1gC
+1ksxP61f8LBz0SwDoyOTr8wY++wqF26KfoYuKQ3LjLeHvuUtL3EMnAhiyuej8ZOZ
+22spng==
+-----END CERTIFICATE-----
diff --git a/certbot/tests/testdata/ocsp_issuer_certificate.pem b/certbot/tests/testdata/ocsp_issuer_certificate.pem
new file mode 100644
--- /dev/null
+++ b/certbot/tests/testdata/ocsp_issuer_certificate.pem
@@ -0,0 +1,38 @@
+22spng==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+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
+PJrUUjM8OQ==
+-----END CERTIFICATE-----
diff --git a/certbot/tests/testdata/ocsp_responder_certificate.pem b/certbot/tests/testdata/ocsp_responder_certificate.pem
new file mode 100644
--- /dev/null
+++ b/certbot/tests/testdata/ocsp_responder_certificate.pem
@@ -0,0 +1,27 @@
+-----BEGIN CERTIFICATE-----
+MIIEpjCCAo6gAwIBAgINARMIGYlEsD1LTt6D7zANBgkqhkiG9w0BAQsFADBRMQsw
+CQYDVQQGEwJOTzEdMBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIzAhBgNV
+BAMMGkJ1eXBhc3MgQ2xhc3MgMiBUZXN0NCBDQSA1MB4XDTE5MDQwNTEwMDAwMFoX
+DTE5MDcwNDEwMDAwMFowSTELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3Mg
+QVMtOTgzMTYzMzI3MRswGQYDVQQDDBJCdXlwYXNzIFRlc3Q0IE9DU1AwggEiMA0G
+CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDKGF+kYNd1fbhYT7Vf9xouZlx+4w45
+Y5EowPoaSKFo4uUDDxkj4PwmMiH4w9Q2bGrCbZRrDrvlNVY/kwzLu4CIk6Ip0dgm
+VZGNFB3Xo9nai7rI5pn/YVvVnDIQXh1LRbekzLVyHvhRgMpRb19xN/iYsxaOJDph
+8eAgbTKf6eitvfbvn/zXHj4KGKycuULI4+mwlfV3uioT4ulbT7PTVJetgi/XXFDO
+xMjbqx6I1ZMmzKJ6LNaFlfx6GdZsaLRDCidHzGp8Fm4ZdV+UPvMZcVDQO6rvQ3wU
+iGyCqgfE5e0aFvfeLoBPBtaoT0Ht1CvGdTfVet6PXrF6gh40fdEH5Ob5AgMBAAGj
+gYQwgYEwCQYDVR0TBAIwADAfBgNVHSMEGDAWgBR32dAGkWbY3RkmjUms2jAvanti
+sTAdBgNVHQ4EFgQU3VlR+sSIVpmXklieP7IlpVUcXIowDgYDVR0PAQH/BAQDAgeA
+MBMGA1UdJQQMMAoGCCsGAQUFBwMJMA8GCSsGAQUFBzABBQQCBQAwDQYJKoZIhvcN
+AQELBQADggIBAFBRLVsBadNFAoFi0HOrfxYsiqggZGJLlgxGyi/0NBIgduG4kcpM
+THvplwBwMQEqyp5511pSEbLPAFj8EqC5c46hXZXmT49xlfRvr2Bo+qtTPV9szuWr
+8muEIejwRrkATpqWPZWR2zVTXfB90mU2oGuRvxUVmnW4v+FrCChJo7+9yTocZJKx
+p4vxYfPMeggomdGAAUz94+0ppSjOLDzs3MA8uOcR0zJ2Y7UHb7PBf/HiM3GO2uKB
+sRgdDaGIf/PNpav0xJ/abGNNNwvXzHiMgqqImsuv/JoncPQWbClNurhXpdN7xt9C
+HcLX2AdggabcogjWm4guBFuFTsL1i0l8Bsu/6iPJ7ddCeANfYzf7h6AcQq12uFl3
+070F29DtPh8D3FPWgRZZsxoANFjXErxfj4a4+DR+jhhkb9YM/wI0vCOM7W6PKxVn
+ZK5kHGOQTcQMj7RCX52gEf27M33zC7HVam+kKhGvwq7D9Bs5hZclzcbjpR4eIxT7
+tzuiy5VpPh1DRLPrphPUB4xsA1dy6zbkg8OqddG6NxD++ja/iZyzSB3SeWyO02qA
+QoK2FzDasxpZ9rT3ioAcms3wVNe4lcd4OP8gHZONuat/gvxk6OZvAld6cnIrQZYB
+Tbu89ZWvhsyI3p4YC/15pUvA95j9Y0te+G+CF22Eoyb+rtz6mMletnUB
+-----END CERTIFICATE-----