[WIP] add support for the TLSFeature extension in x509 (#3899)

* add support for the TLSFeature extension in x509

This extension is used for OCSP Must-Staple.

* fix changelog link

* pep8

* refactor to support the sequence properly and add status_request_v2

* update some language

* add test vector, implement eq/ne/hash on TLSFeature

* address review comments
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 3256031..51488ce 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -41,7 +41,9 @@
 * Added support for using labels with
   :class:`~cryptography.hazmat.primitives.asymmetric.padding.OAEP` when using
   OpenSSL 1.0.2 or greater.
-
+* Add support for the :class:`~cryptography.x509.TLSFeature`
+  extension. This is commonly used for enabling ``OCSP Must-Staple`` in
+  certificates.
 
 
 .. _v2-0-3:
diff --git a/docs/x509/reference.rst b/docs/x509/reference.rst
index 5e1c95c..f07272a 100644
--- a/docs/x509/reference.rst
+++ b/docs/x509/reference.rst
@@ -1606,6 +1606,45 @@
 
         Returns :attr:`~cryptography.x509.oid.ExtensionOID.OCSP_NO_CHECK`.
 
+
+.. class:: TLSFeature(features)
+
+    .. versionadded:: 2.1
+
+    The TLS Feature extension is defined in :rfc:`7633` and is used in
+    certificates for OCSP Must-Staple. The object is iterable to get every
+    element.
+
+    :param list features: A list of features to enable from the
+        :class:`~cryptography.x509.TLSFeatureType` enum. At this time only
+        ``status_request`` or ``status_request_v2`` are allowed.
+
+    .. attribute:: oid
+
+        :type: :class:`ObjectIdentifier`
+
+        Returns :attr:`~cryptography.x509.oid.ExtensionOID.TLS_FEATURE`.
+
+.. class:: TLSFeatureType
+
+    .. versionadded:: 2.1
+
+    An enumeration of TLS Feature types.
+
+    .. attribute:: status_request
+
+        This feature type is defined in :rfc:`6066` and, when embedded in
+        an X.509 certificate, signals to the client that it should require
+        a stapled OCSP response in the TLS handshake. Commonly known as OCSP
+        Must-Staple in certificates.
+
+    .. attribute:: status_request_v2
+
+        This feature type is defined in :rfc:`6961`. This value is not
+        commonly used and if you want to enable OCSP Must-Staple you should
+        use ``status_request``.
+
+
 .. class:: NameConstraints(permitted_subtrees, excluded_subtrees)
 
     .. versionadded:: 1.0
@@ -2673,6 +2712,12 @@
         identifier for the :class:`~cryptography.x509.OCSPNoCheck` extension
         type.
 
+    .. attribute:: TLS_FEATURE
+
+        Corresponds to the dotted string ``"1.3.6.1.5.5.7.1.24"``. The
+        identifier for the :class:`~cryptography.x509.TLSFeature` extension
+        type.
+
     .. attribute:: CRL_NUMBER
 
         Corresponds to the dotted string ``"2.5.29.20"``. The identifier for
diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py
index d9a5bdf..3a88934 100644
--- a/src/cryptography/hazmat/backends/openssl/backend.py
+++ b/src/cryptography/hazmat/backends/openssl/backend.py
@@ -23,6 +23,7 @@
 from cryptography.hazmat.backends.openssl import aead
 from cryptography.hazmat.backends.openssl.ciphers import _CipherContext
 from cryptography.hazmat.backends.openssl.cmac import _CMACContext
+from cryptography.hazmat.backends.openssl.decode_asn1 import _Integers
 from cryptography.hazmat.backends.openssl.dh import (
     _DHParameters, _DHPrivateKey, _DHPublicKey,
     _dh_params_dup
@@ -943,18 +944,22 @@
             res = add_func(x509_obj, x509_extension, i)
             self.openssl_assert(res >= 1)
 
+    def _create_raw_x509_extension(self, extension, value):
+        obj = _txt2obj_gc(self, extension.oid.dotted_string)
+        return self._lib.X509_EXTENSION_create_by_OBJ(
+            self._ffi.NULL, obj, 1 if extension.critical else 0, value
+        )
+
     def _create_x509_extension(self, handlers, extension):
         if isinstance(extension.value, x509.UnrecognizedExtension):
-            obj = _txt2obj_gc(self, extension.oid.dotted_string)
             value = _encode_asn1_str_gc(
                 self, extension.value.value, len(extension.value.value)
             )
-            return self._lib.X509_EXTENSION_create_by_OBJ(
-                self._ffi.NULL,
-                obj,
-                1 if extension.critical else 0,
-                value
-            )
+            return self._create_raw_x509_extension(extension, value)
+        elif isinstance(extension.value, x509.TLSFeature):
+            asn1 = _Integers([x.value for x in extension.value]).dump()
+            value = _encode_asn1_str_gc(self, asn1, len(asn1))
+            return self._create_raw_x509_extension(extension, value)
         else:
             try:
                 encode = handlers[extension.oid]
diff --git a/src/cryptography/hazmat/backends/openssl/decode_asn1.py b/src/cryptography/hazmat/backends/openssl/decode_asn1.py
index a66f65f..9c2d763 100644
--- a/src/cryptography/hazmat/backends/openssl/decode_asn1.py
+++ b/src/cryptography/hazmat/backends/openssl/decode_asn1.py
@@ -9,6 +9,8 @@
 
 from email.utils import parseaddr
 
+from asn1crypto.core import Integer, SequenceOf
+
 import idna
 
 import six
@@ -16,11 +18,16 @@
 from six.moves import urllib_parse
 
 from cryptography import x509
+from cryptography.x509.extensions import _TLS_FEATURE_TYPE_TO_ENUM
 from cryptography.x509.oid import (
     CRLEntryExtensionOID, CertificatePoliciesOID, ExtensionOID
 )
 
 
+class _Integers(SequenceOf):
+    _child_spec = Integer
+
+
 def _obj2txt(backend, obj):
     # Set to 80 on the recommendation of
     # https://www.openssl.org/docs/crypto/OBJ_nid2ln.html#return_values
@@ -210,6 +217,20 @@
                 raise x509.DuplicateExtension(
                     "Duplicate {0} extension found".format(oid), oid
                 )
+
+            # This OID is only supported in OpenSSL 1.1.0+ but we want
+            # to support it in all versions of OpenSSL so we decode it
+            # ourselves.
+            if oid == ExtensionOID.TLS_FEATURE:
+                data = backend._lib.X509_EXTENSION_get_data(ext)
+                parsed = _Integers.load(_asn1_string_to_bytes(backend, data))
+                value = x509.TLSFeature(
+                    [_TLS_FEATURE_TYPE_TO_ENUM[x.native] for x in parsed]
+                )
+                extensions.append(x509.Extension(oid, critical, value))
+                seen_oids.add(oid)
+                continue
+
             try:
                 handler = self.handlers[oid]
             except KeyError:
diff --git a/src/cryptography/x509/__init__.py b/src/cryptography/x509/__init__.py
index 3b74730..176ed8c 100644
--- a/src/cryptography/x509/__init__.py
+++ b/src/cryptography/x509/__init__.py
@@ -23,8 +23,8 @@
     InhibitAnyPolicy, InvalidityDate, IssuerAlternativeName, KeyUsage,
     NameConstraints, NoticeReference, OCSPNoCheck, PolicyConstraints,
     PolicyInformation, PrecertificateSignedCertificateTimestamps, ReasonFlags,
-    SubjectAlternativeName, SubjectKeyIdentifier, UnrecognizedExtension,
-    UserNotice
+    SubjectAlternativeName, SubjectKeyIdentifier, TLSFeature, TLSFeatureType,
+    UnrecognizedExtension, UserNotice
 )
 from cryptography.x509.general_name import (
     DNSName, DirectoryName, GeneralName, IPAddress, OtherName, RFC822Name,
@@ -130,6 +130,8 @@
     "Extensions",
     "Extension",
     "ExtendedKeyUsage",
+    "TLSFeature",
+    "TLSFeatureType",
     "OCSPNoCheck",
     "BasicConstraints",
     "CRLNumber",
diff --git a/src/cryptography/x509/extensions.py b/src/cryptography/x509/extensions.py
index d90465b..9eff343 100644
--- a/src/cryptography/x509/extensions.py
+++ b/src/cryptography/x509/extensions.py
@@ -733,6 +733,62 @@
 
 
 @utils.register_interface(ExtensionType)
+class TLSFeature(object):
+    oid = ExtensionOID.TLS_FEATURE
+
+    def __init__(self, features):
+        features = list(features)
+        if (
+            not all(isinstance(x, TLSFeatureType) for x in features) or
+            len(features) == 0
+        ):
+            raise TypeError(
+                "features must be a list of elements from the TLSFeatureType "
+                "enum"
+            )
+
+        self._features = features
+
+    def __iter__(self):
+        return iter(self._features)
+
+    def __len__(self):
+        return len(self._features)
+
+    def __repr__(self):
+        return "<TLSFeature(features={0._features})>".format(self)
+
+    def __eq__(self, other):
+        if not isinstance(other, TLSFeature):
+            return NotImplemented
+
+        return self._features == other._features
+
+    def __getitem__(self, idx):
+        return self._features[idx]
+
+    def __ne__(self, other):
+        return not self == other
+
+    def __hash__(self):
+        return hash(tuple(self._features))
+
+
+class TLSFeatureType(Enum):
+    # status_request is defined in RFC 6066 and is used for what is commonly
+    # called OCSP Must-Staple when present in the TLS Feature extension in an
+    # X.509 certificate.
+    status_request = 5
+    # status_request_v2 is defined in RFC 6961 and allows multiple OCSP
+    # responses to be provided. It is not currently in use by clients or
+    # servers.
+    status_request_v2 = 17
+
+
+_TLS_FEATURE_TYPE_TO_ENUM = dict((x.value, x) for x in TLSFeatureType)
+
+
+@utils.register_interface(ExtensionType)
 class InhibitAnyPolicy(object):
     oid = ExtensionOID.INHIBIT_ANY_POLICY
 
diff --git a/src/cryptography/x509/oid.py b/src/cryptography/x509/oid.py
index 4a6fa3c..7f8c903 100644
--- a/src/cryptography/x509/oid.py
+++ b/src/cryptography/x509/oid.py
@@ -85,6 +85,7 @@
     AUTHORITY_INFORMATION_ACCESS = ObjectIdentifier("1.3.6.1.5.5.7.1.1")
     SUBJECT_INFORMATION_ACCESS = ObjectIdentifier("1.3.6.1.5.5.7.1.11")
     OCSP_NO_CHECK = ObjectIdentifier("1.3.6.1.5.5.7.48.1.5")
+    TLS_FEATURE = ObjectIdentifier("1.3.6.1.5.5.7.1.24")
     CRL_NUMBER = ObjectIdentifier("2.5.29.20")
     PRECERT_SIGNED_CERTIFICATE_TIMESTAMPS = (
         ObjectIdentifier("1.3.6.1.4.1.11129.2.4.2")
@@ -255,6 +256,7 @@
     ExtensionOID.SUBJECT_INFORMATION_ACCESS: "subjectInfoAccess",
     ExtensionOID.OCSP_NO_CHECK: "OCSPNoCheck",
     ExtensionOID.CRL_NUMBER: "cRLNumber",
+    ExtensionOID.TLS_FEATURE: "TLSFeature",
     AuthorityInformationAccessOID.OCSP: "OCSP",
     AuthorityInformationAccessOID.CA_ISSUERS: "caIssuers",
     CertificatePoliciesOID.CPS_QUALIFIER: "id-qt-cps",
diff --git a/tests/x509/test_x509.py b/tests/x509/test_x509.py
index 533862a..e41fdc7 100644
--- a/tests/x509/test_x509.py
+++ b/tests/x509/test_x509.py
@@ -1092,6 +1092,18 @@
                 "graphy.io')>])>, ...)>"
             )
 
+    def test_parse_tls_feature_extension(self, backend):
+        cert = _load_cert(
+            os.path.join("x509", "tls-feature-ocsp-staple.pem"),
+            x509.load_pem_x509_certificate,
+            backend
+        )
+        ext = cert.extensions.get_extension_for_class(x509.TLSFeature)
+        assert ext.critical is False
+        assert ext.value == x509.TLSFeature(
+            [x509.TLSFeatureType.status_request]
+        )
+
 
 @pytest.mark.requires_backend_interface(interface=RSABackend)
 @pytest.mark.requires_backend_interface(interface=X509Backend)
@@ -2610,6 +2622,46 @@
 
     @pytest.mark.requires_backend_interface(interface=RSABackend)
     @pytest.mark.requires_backend_interface(interface=X509Backend)
+    @pytest.mark.parametrize(
+        "add_ext",
+        [
+            x509.TLSFeature([x509.TLSFeatureType.status_request]),
+            x509.TLSFeature([x509.TLSFeatureType.status_request_v2]),
+            x509.TLSFeature([
+                x509.TLSFeatureType.status_request,
+                x509.TLSFeatureType.status_request_v2
+            ])
+        ]
+    )
+    def test_tls_feature(self, add_ext, backend):
+        issuer_private_key = RSA_KEY_2048.private_key(backend)
+        subject_private_key = RSA_KEY_2048.private_key(backend)
+
+        not_valid_before = datetime.datetime(2002, 1, 1, 12, 1)
+        not_valid_after = datetime.datetime(2030, 12, 31, 8, 30)
+
+        cert = x509.CertificateBuilder().subject_name(
+            x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, u'US')])
+        ).issuer_name(
+            x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, u'US')])
+        ).not_valid_before(
+            not_valid_before
+        ).not_valid_after(
+            not_valid_after
+        ).public_key(
+            subject_private_key.public_key()
+        ).serial_number(
+            123
+        ).add_extension(
+            add_ext, critical=False
+        ).sign(issuer_private_key, hashes.SHA256(), backend)
+
+        ext = cert.extensions.get_extension_for_class(x509.TLSFeature)
+        assert ext.critical is False
+        assert ext.value == add_ext
+
+    @pytest.mark.requires_backend_interface(interface=RSABackend)
+    @pytest.mark.requires_backend_interface(interface=X509Backend)
     def test_key_usage(self, backend):
         issuer_private_key = RSA_KEY_2048.private_key(backend)
         subject_private_key = RSA_KEY_2048.private_key(backend)
diff --git a/tests/x509/test_x509_ext.py b/tests/x509/test_x509_ext.py
index fc8651c..5b9fb34 100644
--- a/tests/x509/test_x509_ext.py
+++ b/tests/x509/test_x509_ext.py
@@ -92,6 +92,69 @@
         assert ext1 != object()
 
 
+class TestTLSFeature(object):
+    def test_not_enum_type(self):
+        with pytest.raises(TypeError):
+            x509.TLSFeature([3])
+
+    def test_empty_list(self):
+        with pytest.raises(TypeError):
+            x509.TLSFeature([])
+
+    def test_repr(self):
+        ext1 = x509.TLSFeature([x509.TLSFeatureType.status_request])
+        assert repr(ext1) == (
+            "<TLSFeature(features=[<TLSFeatureType.status_request: 5>])>"
+        )
+
+    def test_eq(self):
+        ext1 = x509.TLSFeature([x509.TLSFeatureType.status_request])
+        ext2 = x509.TLSFeature([x509.TLSFeatureType.status_request])
+        assert ext1 == ext2
+
+    def test_ne(self):
+        ext1 = x509.TLSFeature([x509.TLSFeatureType.status_request])
+        ext2 = x509.TLSFeature([x509.TLSFeatureType.status_request_v2])
+        ext3 = x509.TLSFeature([
+            x509.TLSFeatureType.status_request,
+            x509.TLSFeatureType.status_request_v2
+        ])
+        assert ext1 != ext2
+        assert ext1 != ext3
+        assert ext1 != object()
+
+    def test_hash(self):
+        ext1 = x509.TLSFeature([x509.TLSFeatureType.status_request])
+        ext2 = x509.TLSFeature([x509.TLSFeatureType.status_request])
+        ext3 = x509.TLSFeature([
+            x509.TLSFeatureType.status_request,
+            x509.TLSFeatureType.status_request_v2
+        ])
+        assert hash(ext1) == hash(ext2)
+        assert hash(ext1) != hash(ext3)
+
+    def test_iter(self):
+        ext1_features = [x509.TLSFeatureType.status_request]
+        ext1 = x509.TLSFeature(ext1_features)
+        assert len(ext1) == 1
+        assert list(ext1) == ext1_features
+        ext2_features = [
+            x509.TLSFeatureType.status_request,
+            x509.TLSFeatureType.status_request_v2,
+        ]
+        ext2 = x509.TLSFeature(ext2_features)
+        assert len(ext2) == 2
+        assert list(ext2) == ext2_features
+
+    def test_indexing(self):
+        ext = x509.TLSFeature([
+            x509.TLSFeatureType.status_request,
+            x509.TLSFeatureType.status_request_v2,
+        ])
+        assert ext[-1] == ext[1]
+        assert ext[0] == x509.TLSFeatureType.status_request
+
+
 class TestUnrecognizedExtension(object):
     def test_invalid_oid(self):
         with pytest.raises(TypeError):