aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.rst4
-rw-r--r--docs/x509/reference.rst45
-rw-r--r--src/cryptography/hazmat/backends/openssl/backend.py19
-rw-r--r--src/cryptography/hazmat/backends/openssl/decode_asn1.py21
-rw-r--r--src/cryptography/x509/__init__.py6
-rw-r--r--src/cryptography/x509/extensions.py56
-rw-r--r--src/cryptography/x509/oid.py2
-rw-r--r--tests/x509/test_x509.py52
-rw-r--r--tests/x509/test_x509_ext.py63
9 files changed, 258 insertions, 10 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 32560318..51488ce7 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -41,7 +41,9 @@ Changelog
* 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 5e1c95c4..f07272a6 100644
--- a/docs/x509/reference.rst
+++ b/docs/x509/reference.rst
@@ -1606,6 +1606,45 @@ X.509 Extensions
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 @@ instances. The following common OIDs are available as constants.
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 d9a5bdf2..3a889344 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.interfaces import (
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 @@ class Backend(object):
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 a66f65f6..9c2d763e 100644
--- a/src/cryptography/hazmat/backends/openssl/decode_asn1.py
+++ b/src/cryptography/hazmat/backends/openssl/decode_asn1.py
@@ -9,6 +9,8 @@ import ipaddress
from email.utils import parseaddr
+from asn1crypto.core import Integer, SequenceOf
+
import idna
import six
@@ -16,11 +18,16 @@ import six
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 @@ class _X509ExtensionParser(object):
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 3b747302..176ed8ca 100644
--- a/src/cryptography/x509/__init__.py
+++ b/src/cryptography/x509/__init__.py
@@ -23,8 +23,8 @@ from cryptography.x509.extensions import (
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 @@ __all__ = [
"Extensions",
"Extension",
"ExtendedKeyUsage",
+ "TLSFeature",
+ "TLSFeatureType",
"OCSPNoCheck",
"BasicConstraints",
"CRLNumber",
diff --git a/src/cryptography/x509/extensions.py b/src/cryptography/x509/extensions.py
index d90465b2..9eff3431 100644
--- a/src/cryptography/x509/extensions.py
+++ b/src/cryptography/x509/extensions.py
@@ -733,6 +733,62 @@ class OCSPNoCheck(object):
@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 4a6fa3c1..7f8c9031 100644
--- a/src/cryptography/x509/oid.py
+++ b/src/cryptography/x509/oid.py
@@ -85,6 +85,7 @@ class ExtensionOID(object):
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 @@ _OID_NAMES = {
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 533862ab..e41fdc76 100644
--- a/tests/x509/test_x509.py
+++ b/tests/x509/test_x509.py
@@ -1092,6 +1092,18 @@ class TestRSACertificate(object):
"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 @@ class TestCertificateBuilder(object):
@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 fc8651c8..5b9fb347 100644
--- a/tests/x509/test_x509_ext.py
+++ b/tests/x509/test_x509_ext.py
@@ -92,6 +92,69 @@ class TestExtension(object):
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):