support OCSP response parsing (#4452)

* support OCSP response parsing

* move the decorator to make pep8 happy

* add some missing docs

* review feedback

* more review feedback
diff --git a/tests/x509/test_ocsp.py b/tests/x509/test_ocsp.py
index a646f4b..aeaa6e6 100644
--- a/tests/x509/test_ocsp.py
+++ b/tests/x509/test_ocsp.py
@@ -5,6 +5,7 @@
 from __future__ import absolute_import, division, print_function
 
 import base64
+import datetime
 import os
 
 import pytest
@@ -12,6 +13,7 @@
 from cryptography import x509
 from cryptography.exceptions import UnsupportedAlgorithm
 from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
 from cryptography.x509 import ocsp
 
 from .test_x509 import _load_cert
@@ -146,3 +148,138 @@
             b"MEMwQTA/MD0wOzAJBgUrDgMCGgUABBRAC0Z68eay0wmDug1gfn5ZN0gkxAQUw5zz"
             b"/NNGCDS7zkZ/oHxb8+IIy1kCAj8g"
         )
+
+
+class TestOCSPResponse(object):
+    def test_bad_response(self):
+        with pytest.raises(ValueError):
+            ocsp.load_der_ocsp_response(b"invalid")
+
+    def test_load_response(self):
+        resp = _load_data(
+            os.path.join("x509", "ocsp", "resp-sha256.der"),
+            ocsp.load_der_ocsp_response,
+        )
+        from cryptography.hazmat.backends.openssl.backend import backend
+        issuer = _load_cert(
+            os.path.join("x509", "letsencryptx3.pem"),
+            x509.load_pem_x509_certificate,
+            backend
+        )
+        assert resp.response_status == ocsp.OCSPResponseStatus.SUCCESSFUL
+        assert (resp.signature_algorithm_oid ==
+                x509.SignatureAlgorithmOID.RSA_WITH_SHA256)
+        assert resp.signature == base64.b64decode(
+            b"I9KUlyLV/2LbNCVu1BQphxdNlU/jBzXsPYVscPjW5E93pCrSO84GkIWoOJtqsnt"
+            b"78DLcQPnF3W24NXGzSGKlSWfXIsyoXCxnBm0mIbD5ZMnKyXEnqSR33Z9He/A+ML"
+            b"A8gbrDUipGNPosesenkKUnOtFIzEGv29hV5E6AMP2ORPVsVlTAZegPJFbbVIWc0"
+            b"rZGFCXKxijDxtUtgWzBhpBAI50JbPHi+IVuaOe4aDJLYgZ0BIBNa6bDI+rScyoy"
+            b"5U0DToV7SZn6CoJ3U19X7BHdYn6TLX0xi43eXuzBGzdHnSzmsc7r/DvkAKJm3vb"
+            b"dVECXqe/gFlXJUBcZ25jhs70MUA=="
+        )
+        assert resp.tbs_response_bytes == base64.b64decode(
+            b"MIHWoUwwSjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzA"
+            b"hBgNVBAMTGkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzGA8yMDE4MDgzMDExMT"
+            b"UwMFowdTBzMEswCQYFKw4DAhoFAAQUfuZq53Kas/z4oiBkbBahLWBxCF0EFKhKa"
+            b"mMEfd265tE5t6ZFZe/zqOyhAhIDHHh6fckClQB7xfIiCztSevCAABgPMjAxODA4"
+            b"MzAxMTAwMDBaoBEYDzIwMTgwOTA2MTEwMDAwWg=="
+        )
+        issuer.public_key().verify(
+            resp.signature,
+            resp.tbs_response_bytes,
+            PKCS1v15(),
+            hashes.SHA256()
+        )
+        assert resp.certificates == []
+        assert resp.responder_key_hash is None
+        assert resp.responder_name == issuer.subject
+        assert resp.produced_at == datetime.datetime(2018, 8, 30, 11, 15)
+        assert resp.certificate_status == ocsp.OCSPCertStatus.GOOD
+        assert resp.revocation_time is None
+        assert resp.revocation_reason is None
+        assert resp.this_update == datetime.datetime(2018, 8, 30, 11, 0)
+        assert resp.next_update == datetime.datetime(2018, 9, 6, 11, 0)
+        assert resp.issuer_key_hash == (
+            b'\xa8Jjc\x04}\xdd\xba\xe6\xd19\xb7\xa6Ee\xef\xf3\xa8\xec\xa1'
+        )
+        assert resp.issuer_name_hash == (
+            b'~\xe6j\xe7r\x9a\xb3\xfc\xf8\xa2 dl\x16\xa1-`q\x08]'
+        )
+        assert isinstance(resp.hash_algorithm, hashes.SHA1)
+        assert resp.serial_number == 271024907440004808294641238224534273948400
+
+    def test_load_unauthorized(self):
+        resp = _load_data(
+            os.path.join("x509", "ocsp", "resp-unauthorized.der"),
+            ocsp.load_der_ocsp_response,
+        )
+        assert resp.response_status == ocsp.OCSPResponseStatus.UNAUTHORIZED
+        with pytest.raises(ValueError):
+            assert resp.signature_algorithm_oid
+        with pytest.raises(ValueError):
+            assert resp.signature
+        with pytest.raises(ValueError):
+            assert resp.tbs_response_bytes
+        with pytest.raises(ValueError):
+            assert resp.certificates
+        with pytest.raises(ValueError):
+            assert resp.responder_key_hash
+        with pytest.raises(ValueError):
+            assert resp.responder_name
+        with pytest.raises(ValueError):
+            assert resp.produced_at
+        with pytest.raises(ValueError):
+            assert resp.certificate_status
+        with pytest.raises(ValueError):
+            assert resp.revocation_time
+        with pytest.raises(ValueError):
+            assert resp.revocation_reason
+        with pytest.raises(ValueError):
+            assert resp.this_update
+        with pytest.raises(ValueError):
+            assert resp.next_update
+        with pytest.raises(ValueError):
+            assert resp.issuer_key_hash
+        with pytest.raises(ValueError):
+            assert resp.issuer_name_hash
+        with pytest.raises(ValueError):
+            assert resp.hash_algorithm
+        with pytest.raises(ValueError):
+            assert resp.serial_number
+
+    def test_load_revoked(self):
+        resp = _load_data(
+            os.path.join("x509", "ocsp", "resp-revoked.der"),
+            ocsp.load_der_ocsp_response,
+        )
+        assert resp.certificate_status == ocsp.OCSPCertStatus.REVOKED
+        assert resp.revocation_time == datetime.datetime(
+            2016, 9, 2, 21, 28, 48
+        )
+        assert resp.revocation_reason is None
+
+    def test_load_delegate_unknown_cert(self):
+        resp = _load_data(
+            os.path.join("x509", "ocsp", "resp-delegate-unknown-cert.der"),
+            ocsp.load_der_ocsp_response,
+        )
+        assert len(resp.certificates) == 1
+        assert isinstance(resp.certificates[0], x509.Certificate)
+        assert resp.certificate_status == ocsp.OCSPCertStatus.UNKNOWN
+
+    def test_load_responder_key_hash(self):
+        resp = _load_data(
+            os.path.join("x509", "ocsp", "resp-responder-key-hash.der"),
+            ocsp.load_der_ocsp_response,
+        )
+        assert resp.responder_name is None
+        assert resp.responder_key_hash == (
+            b'\x0f\x80a\x1c\x821a\xd5/(\xe7\x8dF8\xb4,\xe1\xc6\xd9\xe2'
+        )
+
+    def test_load_revoked_reason(self):
+        resp = _load_data(
+            os.path.join("x509", "ocsp", "resp-revoked-reason.der"),
+            ocsp.load_der_ocsp_response,
+        )
+        assert resp.revocation_reason is x509.ReasonFlags.superseded