Refs #3461 -- parse SCTs from x.509 extension (#3480)

* Stub API for SCTs, feedback wanted

* grr, flake8

* finish up the __init__

* Initial implementation and tests

* write a test. it fails because computer

* get the tests passing and fix some TODOs

* changelog entry

* This can go now

* Put a skip in this test

* grump

* Removed unreachable code

* moved changelog to the correct section

* Use the deocrator for expressing requirements

* This needs f for the right entry_type

* coverage

* syntax error

* tests for coverage

* better sct eq tests

* docs

* technically correct, the most useless kind of correct

* typo and more details

* bug

* drop __eq__
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 22411d1..080ebd6 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -18,6 +18,9 @@
   and
   :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey`
   in favor of ``verify``.
+* Added support for parsing
+  :class:`~cryptography.x509.certificate_transparency.SignedCertificateTimestamp`
+  objects from X.509 certificate extensions.
 
 1.9 - 2017-05-29
 ~~~~~~~~~~~~~~~~
diff --git a/docs/x509/certificate-transparency.rst b/docs/x509/certificate-transparency.rst
index 0d344d2..f9e651e 100644
--- a/docs/x509/certificate-transparency.rst
+++ b/docs/x509/certificate-transparency.rst
@@ -11,7 +11,7 @@
 
 .. class:: SignedCertificateTimestamp
 
-    .. versionadded:: 1.9
+    .. versionadded:: 2.0
 
     SignedCertificateTimestamps (SCTs) are small cryptographically signed
     assertions that the specified certificate has been submitted to a
@@ -53,7 +53,7 @@
 
 .. class:: Version
 
-    .. versionadded:: 1.9
+    .. versionadded:: 2.0
 
     An enumeration for SignedCertificateTimestamp versions.
 
@@ -63,7 +63,7 @@
 
 .. class:: LogEntryType
 
-    .. versionadded:: 1.9
+    .. versionadded:: 2.0
 
     An enumeration for SignedCertificateTimestamp log entry types.
 
diff --git a/docs/x509/reference.rst b/docs/x509/reference.rst
index 24d1c07..5a903b9 100644
--- a/docs/x509/reference.rst
+++ b/docs/x509/reference.rst
@@ -1814,6 +1814,32 @@
         :returns: A list of values extracted from the matched general names.
 
 
+.. class:: PrecertificateSignedCertificateTimestamps(scts)
+
+    .. versionadded:: 2.0
+
+    This extension contains
+    :class:`~cryptography.x509.certificate_transparency.SignedCertificateTimestamp`
+    instances which were issued for the pre-certificate corresponding to this
+    certificate. These can be used to verify that the certificate is included
+    in a public Certificate Transparency log.
+
+    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.PRECERT_SIGNED_CERTIFICATE_TIMESTAMPS`.
+
+
 .. class:: AuthorityInformationAccess(descriptions)
 
     .. versionadded:: 0.9
diff --git a/src/cryptography/hazmat/backends/openssl/decode_asn1.py b/src/cryptography/hazmat/backends/openssl/decode_asn1.py
index 282e30f..ab97dc1 100644
--- a/src/cryptography/hazmat/backends/openssl/decode_asn1.py
+++ b/src/cryptography/hazmat/backends/openssl/decode_asn1.py
@@ -597,6 +597,21 @@
     return x509.InhibitAnyPolicy(skip_certs)
 
 
+def _decode_precert_signed_certificate_timestamps(backend, asn1_scts):
+    from cryptography.hazmat.backends.openssl.x509 import (
+        _SignedCertificateTimestamp
+    )
+    asn1_scts = backend._ffi.cast("Cryptography_STACK_OF_SCT *", asn1_scts)
+    asn1_scts = backend._ffi.gc(asn1_scts, backend._lib.SCT_LIST_free)
+
+    scts = []
+    for i in range(backend._lib.sk_SCT_num(asn1_scts)):
+        sct = backend._lib.sk_SCT_value(asn1_scts, i)
+
+        scts.append(_SignedCertificateTimestamp(backend, asn1_scts, sct))
+    return x509.PrecertificateSignedCertificateTimestamps(scts)
+
+
 #    CRLReason ::= ENUMERATED {
 #        unspecified             (0),
 #        keyCompromise           (1),
@@ -751,6 +766,9 @@
     ExtensionOID.ISSUER_ALTERNATIVE_NAME: _decode_issuer_alt_name,
     ExtensionOID.NAME_CONSTRAINTS: _decode_name_constraints,
     ExtensionOID.POLICY_CONSTRAINTS: _decode_policy_constraints,
+    ExtensionOID.PRECERT_SIGNED_CERTIFICATE_TIMESTAMPS: (
+        _decode_precert_signed_certificate_timestamps
+    ),
 }
 
 _REVOKED_EXTENSION_HANDLERS = {
diff --git a/src/cryptography/hazmat/backends/openssl/x509.py b/src/cryptography/hazmat/backends/openssl/x509.py
index 5b3304f..4345638 100644
--- a/src/cryptography/hazmat/backends/openssl/x509.py
+++ b/src/cryptography/hazmat/backends/openssl/x509.py
@@ -4,6 +4,7 @@
 
 from __future__ import absolute_import, division, print_function
 
+import datetime
 import operator
 import warnings
 
@@ -433,3 +434,43 @@
             return False
 
         return True
+
+
+@utils.register_interface(
+    x509.certificate_transparency.SignedCertificateTimestamp
+)
+class _SignedCertificateTimestamp(object):
+    def __init__(self, backend, sct_list, sct):
+        self._backend = backend
+        # Keep the SCT_LIST that this SCT came from alive.
+        self._sct_list = sct_list
+        self._sct = sct
+
+    @property
+    def version(self):
+        version = self._backend._lib.SCT_get_version(self._sct)
+        assert version == self._backend._lib.SCT_VERSION_V1
+        return x509.certificate_transparency.Version.v1
+
+    @property
+    def log_id(self):
+        out = self._backend._ffi.new("unsigned char **")
+        log_id_length = self._backend._lib.SCT_get0_log_id(self._sct, out)
+        assert log_id_length >= 0
+        return self._backend._ffi.buffer(out[0], log_id_length)[:]
+
+    @property
+    def timestamp(self):
+        timestamp = self._backend._lib.SCT_get_timestamp(self._sct)
+        milliseconds = timestamp % 1000
+        return datetime.datetime.utcfromtimestamp(
+            timestamp // 1000
+        ).replace(microsecond=milliseconds * 1000)
+
+    @property
+    def entry_type(self):
+        entry_type = self._backend._lib.SCT_get_log_entry_type(self._sct)
+        # We currently only support loading SCTs from the X.509 extension, so
+        # we only have precerts.
+        assert entry_type == self._backend._lib.CT_LOG_ENTRY_TYPE_PRECERT
+        return x509.certificate_transparency.LogEntryType.PRE_CERTIFICATE
diff --git a/src/cryptography/x509/__init__.py b/src/cryptography/x509/__init__.py
index c5465fb..b1a32ef 100644
--- a/src/cryptography/x509/__init__.py
+++ b/src/cryptography/x509/__init__.py
@@ -23,9 +23,9 @@
     ExtensionNotFound, ExtensionType, Extensions, GeneralNames,
     InhibitAnyPolicy, InvalidityDate, IssuerAlternativeName, KeyUsage,
     NameConstraints, NoticeReference, OCSPNoCheck, PolicyConstraints,
-    PolicyInformation, ReasonFlags, SubjectAlternativeName,
-    SubjectKeyIdentifier, UnrecognizedExtension, UnsupportedExtension,
-    UserNotice
+    PolicyInformation, PrecertificateSignedCertificateTimestamps, ReasonFlags,
+    SubjectAlternativeName, SubjectKeyIdentifier, UnrecognizedExtension,
+    UnsupportedExtension, UserNotice
 )
 from cryptography.x509.general_name import (
     DNSName, DirectoryName, GeneralName, IPAddress, OtherName, RFC822Name,
@@ -185,4 +185,5 @@
     "InvalidityDate",
     "UnrecognizedExtension",
     "PolicyConstraints",
+    "PrecertificateSignedCertificateTimestamps",
 ]
diff --git a/src/cryptography/x509/extensions.py b/src/cryptography/x509/extensions.py
index aa30f8f..1b64f4a 100644
--- a/src/cryptography/x509/extensions.py
+++ b/src/cryptography/x509/extensions.py
@@ -18,6 +18,9 @@
 from cryptography.hazmat.primitives import constant_time, serialization
 from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey
 from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
+from cryptography.x509.certificate_transparency import (
+    SignedCertificateTimestamp
+)
 from cryptography.x509.general_name import GeneralName, IPAddress, OtherName
 from cryptography.x509.name import RelativeDistinguishedName
 from cryptography.x509.oid import (
@@ -1152,6 +1155,39 @@
 
 
 @utils.register_interface(ExtensionType)
+class PrecertificateSignedCertificateTimestamps(object):
+    oid = ExtensionOID.PRECERT_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
+
+    def __iter__(self):
+        return iter(self._signed_certificate_timestamps)
+
+    def __len__(self):
+        return len(self._signed_certificate_timestamps)
+
+    def __getitem__(self, idx):
+        return self._signed_certificate_timestamps[idx]
+
+    def __repr__(self):
+        return (
+            "<PrecertificateSignedCertificateTimestamps({0})>".format(
+                list(self)
+            )
+        )
+
+
+@utils.register_interface(ExtensionType)
 class UnrecognizedExtension(object):
     def __init__(self, oid, value):
         if not isinstance(oid, ObjectIdentifier):
diff --git a/tests/test_x509_ext.py b/tests/test_x509_ext.py
index b89abdd..595ec70 100644
--- a/tests/test_x509_ext.py
+++ b/tests/test_x509_ext.py
@@ -3668,6 +3668,48 @@
 
 @pytest.mark.requires_backend_interface(interface=RSABackend)
 @pytest.mark.requires_backend_interface(interface=X509Backend)
+@pytest.mark.supported(
+    only_if=lambda backend: backend._lib.CRYPTOGRAPHY_OPENSSL_110F_OR_GREATER,
+    skip_message="Requires OpenSSL 1.1.0f+",
+)
+class TestPrecertificateSignedCertificateTimestampsExtension(object):
+    def test_init(self):
+        with pytest.raises(TypeError):
+            x509.PrecertificateSignedCertificateTimestamps([object()])
+
+    def test_repr(self):
+        assert repr(x509.PrecertificateSignedCertificateTimestamps([])) == (
+            "<PrecertificateSignedCertificateTimestamps([])>"
+        )
+
+    def test_simple(self, backend):
+        cert = _load_cert(
+            os.path.join("x509", "badssl-sct.pem"),
+            x509.load_pem_x509_certificate,
+            backend
+        )
+        scts = cert.extensions.get_extension_for_class(
+            x509.PrecertificateSignedCertificateTimestamps
+        ).value
+        assert len(scts) == 1
+        [sct] = scts
+        assert scts[0] == sct
+        assert sct.version == x509.certificate_transparency.Version.v1
+        assert sct.log_id == (
+            b"\xa7\xceJNb\x07\xe0\xad\xde\xe5\xfd\xaaK\x1f\x86v\x87g\xb5\xd0"
+            b"\x02\xa5]G1\x0e~g\n\x95\xea\xb2"
+        )
+        assert sct.timestamp == datetime.datetime(
+            2016, 11, 17, 1, 56, 25, 396000
+        )
+        assert (
+            sct.entry_type ==
+            x509.certificate_transparency.LogEntryType.PRE_CERTIFICATE
+        )
+
+
+@pytest.mark.requires_backend_interface(interface=RSABackend)
+@pytest.mark.requires_backend_interface(interface=X509Backend)
 class TestInvalidExtension(object):
     def test_invalid_certificate_policies_data(self, backend):
         cert = _load_cert(