# HG changeset patch
# User Paul Kehrer <paul.l.kehrer@gmail.com>
# Date 1593961267 18000
#      Sun Jul 05 10:01:07 2020 -0500
# Node ID 5aca1c6f3a7de44d1d353abb5436b320c2f83415
# Parent  58f4019b101f4bbf76707d174ef25136358d0e41
Support parsing SCTs in OCSPResponse (#5298)

* Support parsing SCTs in OCSPResponse

* s/typically/only and pep8

* remove unused vector

Co-authored-by: Szilárd Pfeiffer <szilard.pfeiffer@balasys.hu>

diff --git a/CHANGELOG.rst b/CHANGELOG.rst
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -37,6 +37,8 @@
   :class:`~cryptography.fernet.Fernet`.
 * Added support for the :class:`~cryptography.x509.SubjectInformationAccess`
   X.509 extension.
+* Added support for parsing
+  :class:`~cryptography.x509.SignedCertificateTimestamps` in OCSP responses.
 
 .. _v2-9-2:
 
diff --git a/docs/x509/reference.rst b/docs/x509/reference.rst
--- a/docs/x509/reference.rst
+++ b/docs/x509/reference.rst
@@ -2099,6 +2099,33 @@
         Returns :attr:`~cryptography.x509.oid.ExtensionOID.PRECERT_POISON`.
 
 
+.. class:: SignedCertificateTimestamps(scts)
+
+    .. versionadded:: 3.0
+
+    This extension contains
+    :class:`~cryptography.x509.certificate_transparency.SignedCertificateTimestamp`
+    instances. These can be used to verify that the certificate is included
+    in a public Certificate Transparency log. This extension is only found
+    in OCSP responses. For SCTs in an X.509 certificate see
+    :class:`~cryptography.x509.PrecertificateSignedCertificateTimestamps`.
+
+    It is an iterable containing one or more
+    :class:`~cryptography.x509.certificate_transparency.SignedCertificateTimestamp`
+    objects.
+
+    :param list scts: A ``list`` of
+        :class:`~cryptography.x509.certificate_transparency.SignedCertificateTimestamp`
+        objects.
+
+    .. attribute:: oid
+
+        :type: :class:`ObjectIdentifier`
+
+        Returns
+        :attr:`~cryptography.x509.oid.ExtensionOID.SIGNED_CERTIFICATE_TIMESTAMPS`.
+
+
 .. class:: DeltaCRLIndicator(crl_number)
 
     .. versionadded:: 2.1
@@ -3142,6 +3169,12 @@
 
         Corresponds to the dotted string ``"1.3.6.1.4.1.11129.2.4.3"``.
 
+    .. attribute:: SIGNED_CERTIFICATE_TIMESTAMPS
+
+        .. versionadded:: 3.0
+
+        Corresponds to the dotted string ``"1.3.6.1.4.1.11129.2.4.5"``.
+
     .. attribute:: POLICY_CONSTRAINTS
 
         Corresponds to the dotted string ``"2.5.29.36"``. The identifier for the
diff --git a/src/cryptography/hazmat/backends/openssl/decode_asn1.py b/src/cryptography/hazmat/backends/openssl/decode_asn1.py
--- a/src/cryptography/hazmat/backends/openssl/decode_asn1.py
+++ b/src/cryptography/hazmat/backends/openssl/decode_asn1.py
@@ -652,7 +652,7 @@
     return x509.InhibitAnyPolicy(skip_certs)
 
 
-def _decode_precert_signed_certificate_timestamps(backend, asn1_scts):
+def _decode_scts(backend, asn1_scts):
     from cryptography.hazmat.backends.openssl.x509 import (
         _SignedCertificateTimestamp
     )
@@ -664,7 +664,19 @@
         sct = backend._lib.sk_SCT_value(asn1_scts, i)
 
         scts.append(_SignedCertificateTimestamp(backend, asn1_scts, sct))
-    return x509.PrecertificateSignedCertificateTimestamps(scts)
+    return scts
+
+
+def _decode_precert_signed_certificate_timestamps(backend, asn1_scts):
+    return x509.PrecertificateSignedCertificateTimestamps(
+        _decode_scts(backend, asn1_scts)
+    )
+
+
+def _decode_signed_certificate_timestamps(backend, asn1_scts):
+    return x509.SignedCertificateTimestamps(
+        _decode_scts(backend, asn1_scts)
+    )
 
 
 #    CRLReason ::= ENUMERATED {
@@ -872,7 +884,13 @@
 
 # All revoked extensions are valid single response extensions, see:
 # https://tools.ietf.org/html/rfc6960#section-4.4.5
-_OCSP_SINGLERESP_EXTENSION_HANDLERS = _REVOKED_EXTENSION_HANDLERS.copy()
+_OCSP_SINGLERESP_EXTENSION_HANDLERS_NO_SCT = _REVOKED_EXTENSION_HANDLERS.copy()
+_OCSP_SINGLERESP_EXTENSION_HANDLERS = (
+    _OCSP_SINGLERESP_EXTENSION_HANDLERS_NO_SCT.copy()
+)
+_OCSP_SINGLERESP_EXTENSION_HANDLERS[
+    ExtensionOID.SIGNED_CERTIFICATE_TIMESTAMPS
+] = _decode_signed_certificate_timestamps
 
 _CERTIFICATE_EXTENSION_PARSER_NO_SCT = _X509ExtensionParser(
     ext_count=lambda backend, x: backend._lib.X509_get_ext_count(x),
@@ -921,3 +939,9 @@
     get_ext=lambda backend, x, i: backend._lib.OCSP_SINGLERESP_get_ext(x, i),
     handlers=_OCSP_SINGLERESP_EXTENSION_HANDLERS,
 )
+
+_OCSP_SINGLERESP_EXT_PARSER_NO_SCT = _X509ExtensionParser(
+    ext_count=lambda backend, x: backend._lib.OCSP_SINGLERESP_get_ext_count(x),
+    get_ext=lambda backend, x, i: backend._lib.OCSP_SINGLERESP_get_ext(x, i),
+    handlers=_OCSP_SINGLERESP_EXTENSION_HANDLERS_NO_SCT
+)
diff --git a/src/cryptography/hazmat/backends/openssl/ocsp.py b/src/cryptography/hazmat/backends/openssl/ocsp.py
--- a/src/cryptography/hazmat/backends/openssl/ocsp.py
+++ b/src/cryptography/hazmat/backends/openssl/ocsp.py
@@ -11,6 +11,7 @@
 from cryptography.hazmat.backends.openssl.decode_asn1 import (
     _CRL_ENTRY_REASON_CODE_TO_ENUM, _OCSP_BASICRESP_EXT_PARSER,
     _OCSP_REQ_EXT_PARSER, _OCSP_SINGLERESP_EXT_PARSER,
+    _OCSP_SINGLERESP_EXT_PARSER_NO_SCT,
     _asn1_integer_to_int,
     _asn1_string_to_bytes, _decode_x509_name, _obj2txt,
     _parse_asn1_generalized_time,
@@ -323,9 +324,14 @@
     @utils.cached_property
     @_requires_successful_response
     def single_extensions(self):
-        return _OCSP_SINGLERESP_EXT_PARSER.parse(
-            self._backend, self._single
-        )
+        if self._backend._lib.CRYPTOGRAPHY_OPENSSL_110_OR_GREATER:
+            return _OCSP_SINGLERESP_EXT_PARSER.parse(
+                self._backend, self._single
+            )
+        else:
+            return _OCSP_SINGLERESP_EXT_PARSER_NO_SCT.parse(
+                self._backend, self._single
+            )
 
     def public_bytes(self, encoding):
         if encoding is not serialization.Encoding.DER:
diff --git a/src/cryptography/x509/__init__.py b/src/cryptography/x509/__init__.py
--- a/src/cryptography/x509/__init__.py
+++ b/src/cryptography/x509/__init__.py
@@ -24,7 +24,8 @@
     IssuingDistributionPoint, KeyUsage, NameConstraints, NoticeReference,
     OCSPNoCheck, OCSPNonce, PolicyConstraints, PolicyInformation,
     PrecertPoison, PrecertificateSignedCertificateTimestamps, ReasonFlags,
-    SubjectAlternativeName, SubjectInformationAccess, SubjectKeyIdentifier,
+    SignedCertificateTimestamps, SubjectAlternativeName,
+    SubjectInformationAccess, SubjectKeyIdentifier,
     TLSFeature, TLSFeatureType, UnrecognizedExtension, UserNotice
 )
 from cryptography.x509.general_name import (
@@ -187,4 +188,5 @@
     "PrecertificateSignedCertificateTimestamps",
     "PrecertPoison",
     "OCSPNonce",
+    "SignedCertificateTimestamps",
 ]
diff --git a/src/cryptography/x509/extensions.py b/src/cryptography/x509/extensions.py
--- a/src/cryptography/x509/extensions.py
+++ b/src/cryptography/x509/extensions.py
@@ -1439,6 +1439,49 @@
 
 
 @utils.register_interface(ExtensionType)
+class SignedCertificateTimestamps(object):
+    oid = ExtensionOID.SIGNED_CERTIFICATE_TIMESTAMPS
+
+    def __init__(self, signed_certificate_timestamps):
+        signed_certificate_timestamps = list(signed_certificate_timestamps)
+        if not all(
+            isinstance(sct, SignedCertificateTimestamp)
+            for sct in signed_certificate_timestamps
+        ):
+            raise TypeError(
+                "Every item in the signed_certificate_timestamps list must be "
+                "a SignedCertificateTimestamp"
+            )
+        self._signed_certificate_timestamps = signed_certificate_timestamps
+
+    __len__, __iter__, __getitem__ = _make_sequence_methods(
+        "_signed_certificate_timestamps"
+    )
+
+    def __repr__(self):
+        return (
+            "<SignedCertificateTimestamps({})>".format(
+                list(self)
+            )
+        )
+
+    def __hash__(self):
+        return hash(tuple(self._signed_certificate_timestamps))
+
+    def __eq__(self, other):
+        if not isinstance(other, SignedCertificateTimestamps):
+            return NotImplemented
+
+        return (
+            self._signed_certificate_timestamps ==
+            other._signed_certificate_timestamps
+        )
+
+    def __ne__(self, other):
+        return not self == other
+
+
+@utils.register_interface(ExtensionType)
 class OCSPNonce(object):
     oid = OCSPExtensionOID.NONCE
 
diff --git a/src/cryptography/x509/oid.py b/src/cryptography/x509/oid.py
--- a/src/cryptography/x509/oid.py
+++ b/src/cryptography/x509/oid.py
@@ -37,6 +37,9 @@
     PRECERT_POISON = (
         ObjectIdentifier("1.3.6.1.4.1.11129.2.4.3")
     )
+    SIGNED_CERTIFICATE_TIMESTAMPS = (
+        ObjectIdentifier("1.3.6.1.4.1.11129.2.4.5")
+    )
 
 
 class OCSPExtensionOID(object):
@@ -231,6 +234,9 @@
     ExtensionOID.PRECERT_SIGNED_CERTIFICATE_TIMESTAMPS: (
         "signedCertificateTimestampList"
     ),
+    ExtensionOID.SIGNED_CERTIFICATE_TIMESTAMPS: (
+        "signedCertificateTimestampList"
+    ),
     ExtensionOID.PRECERT_POISON: "ctPoison",
     CRLEntryExtensionOID.CRL_REASON: "cRLReason",
     CRLEntryExtensionOID.INVALIDITY_DATE: "invalidityDate",
diff --git a/tests/x509/test_ocsp.py b/tests/x509/test_ocsp.py
--- a/tests/x509/test_ocsp.py
+++ b/tests/x509/test_ocsp.py
@@ -566,6 +566,75 @@
             )
 
 
+class TestSignedCertificateTimestampsExtension(object):
+    def test_init(self):
+        with pytest.raises(TypeError):
+            x509.SignedCertificateTimestamps([object()])
+
+    def test_repr(self):
+        assert repr(x509.SignedCertificateTimestamps([])) == (
+            "<SignedCertificateTimestamps([])>"
+        )
+
+    @pytest.mark.supported(
+        only_if=lambda backend: (
+            backend._lib.CRYPTOGRAPHY_OPENSSL_110F_OR_GREATER),
+        skip_message="Requires OpenSSL 1.1.0f+",
+    )
+    def test_eq(self, backend):
+        sct1 = _load_data(
+            os.path.join("x509", "ocsp", "resp-sct-extension.der"),
+            ocsp.load_der_ocsp_response,
+        ).single_extensions.get_extension_for_class(
+            x509.SignedCertificateTimestamps
+        ).value
+        sct2 = _load_data(
+            os.path.join("x509", "ocsp", "resp-sct-extension.der"),
+            ocsp.load_der_ocsp_response,
+        ).single_extensions.get_extension_for_class(
+            x509.SignedCertificateTimestamps
+        ).value
+        assert sct1 == sct2
+
+    @pytest.mark.supported(
+        only_if=lambda backend: (
+            backend._lib.CRYPTOGRAPHY_OPENSSL_110F_OR_GREATER),
+        skip_message="Requires OpenSSL 1.1.0f+",
+    )
+    def test_ne(self, backend):
+        sct1 = _load_data(
+            os.path.join("x509", "ocsp", "resp-sct-extension.der"),
+            ocsp.load_der_ocsp_response,
+        ).single_extensions.get_extension_for_class(
+            x509.SignedCertificateTimestamps
+        ).value
+        sct2 = x509.SignedCertificateTimestamps([])
+        assert sct1 != sct2
+        assert sct1 != object()
+
+    @pytest.mark.supported(
+        only_if=lambda backend: (
+            backend._lib.CRYPTOGRAPHY_OPENSSL_110F_OR_GREATER),
+        skip_message="Requires OpenSSL 1.1.0f+",
+    )
+    def test_hash(self, backend):
+        sct1 = _load_data(
+            os.path.join("x509", "ocsp", "resp-sct-extension.der"),
+            ocsp.load_der_ocsp_response,
+        ).single_extensions.get_extension_for_class(
+            x509.SignedCertificateTimestamps
+        ).value
+        sct2 = _load_data(
+            os.path.join("x509", "ocsp", "resp-sct-extension.der"),
+            ocsp.load_der_ocsp_response,
+        ).single_extensions.get_extension_for_class(
+            x509.SignedCertificateTimestamps
+        ).value
+        sct3 = x509.SignedCertificateTimestamps([])
+        assert hash(sct1) == hash(sct2)
+        assert hash(sct1) != hash(sct3)
+
+
 class TestOCSPResponse(object):
     def test_bad_response(self):
         with pytest.raises(ValueError):
@@ -756,6 +825,48 @@
         with pytest.raises(ValueError):
             resp.public_bytes(serialization.Encoding.PEM)
 
+    @pytest.mark.supported(
+        only_if=lambda backend: (
+            backend._lib.CRYPTOGRAPHY_OPENSSL_110F_OR_GREATER),
+        skip_message="Requires OpenSSL 1.1.0f+",
+    )
+    def test_single_extensions_sct(self, backend):
+        resp = _load_data(
+            os.path.join("x509", "ocsp", "resp-sct-extension.der"),
+            ocsp.load_der_ocsp_response,
+        )
+        assert len(resp.single_extensions) == 1
+        ext = resp.single_extensions[0]
+        assert ext.oid == x509.ObjectIdentifier("1.3.6.1.4.1.11129.2.4.5")
+        assert len(ext.value) == 4
+        log_ids = [base64.b64encode(sct.log_id) for sct in ext.value]
+        assert log_ids == [
+            b'RJRlLrDuzq/EQAfYqP4owNrmgr7YyzG1P9MzlrW2gag=',
+            b'b1N2rDHwMRnYmQCkURX/dxUcEdkCwQApBo2yCJo32RM=',
+            b'u9nfvB+KcbWTlCOXqpJ7RzhXlQqrUugakJZkNo4e0YU=',
+            b'7ku9t3XOYLrhQmkfq+GeZqMPfl+wctiDAMR7iXqo/cs='
+        ]
+
+    @pytest.mark.supported(
+        only_if=lambda backend: (
+            not backend._lib.CRYPTOGRAPHY_OPENSSL_110F_OR_GREATER),
+        skip_message="Requires OpenSSL < 1.1.0f",
+    )
+    def test_skips_single_extensions_scts_if_unsupported(self, backend):
+        resp = _load_data(
+            os.path.join("x509", "ocsp", "resp-sct-extension.der"),
+            ocsp.load_der_ocsp_response,
+        )
+        with pytest.raises(x509.ExtensionNotFound):
+            resp.single_extensions.get_extension_for_class(
+                x509.SignedCertificateTimestamps
+            )
+
+        ext = resp.single_extensions.get_extension_for_oid(
+            x509.ExtensionOID.SIGNED_CERTIFICATE_TIMESTAMPS
+        )
+        assert isinstance(ext.value, x509.UnrecognizedExtension)
+
     def test_single_extensions(self, backend):
         resp = _load_data(
             os.path.join("x509", "ocsp", "resp-single-extension-reason.der"),