From 6a0718faddbc7b6b57f86417f6daa468c18ea248 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sun, 4 Jun 2017 13:36:58 -0400 Subject: 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__ --- CHANGELOG.rst | 3 ++ docs/x509/certificate-transparency.rst | 6 ++-- docs/x509/reference.rst | 26 ++++++++++++++ .../hazmat/backends/openssl/decode_asn1.py | 18 ++++++++++ src/cryptography/hazmat/backends/openssl/x509.py | 41 +++++++++++++++++++++ src/cryptography/x509/__init__.py | 7 ++-- src/cryptography/x509/extensions.py | 36 +++++++++++++++++++ tests/test_x509_ext.py | 42 ++++++++++++++++++++++ 8 files changed, 173 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 22411d1f..080ebd66 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,9 @@ Changelog 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 0d344d2b..f9e651ed 100644 --- a/docs/x509/certificate-transparency.rst +++ b/docs/x509/certificate-transparency.rst @@ -11,7 +11,7 @@ issued. .. 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 @@ issued. .. class:: Version - .. versionadded:: 1.9 + .. versionadded:: 2.0 An enumeration for SignedCertificateTimestamp versions. @@ -63,7 +63,7 @@ issued. .. 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 24d1c07b..5a903b95 100644 --- a/docs/x509/reference.rst +++ b/docs/x509/reference.rst @@ -1814,6 +1814,32 @@ X.509 Extensions :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 282e30f0..ab97dc19 100644 --- a/src/cryptography/hazmat/backends/openssl/decode_asn1.py +++ b/src/cryptography/hazmat/backends/openssl/decode_asn1.py @@ -597,6 +597,21 @@ def _decode_inhibit_any_policy(backend, asn1_int): 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 @@ _EXTENSION_HANDLERS = { 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 5b3304f3..43456382 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 @@ class _CertificateSigningRequest(object): 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 c5465fbb..b1a32ef6 100644 --- a/src/cryptography/x509/__init__.py +++ b/src/cryptography/x509/__init__.py @@ -23,9 +23,9 @@ from cryptography.x509.extensions import ( 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 @@ __all__ = [ "InvalidityDate", "UnrecognizedExtension", "PolicyConstraints", + "PrecertificateSignedCertificateTimestamps", ] diff --git a/src/cryptography/x509/extensions.py b/src/cryptography/x509/extensions.py index aa30f8ff..1b64f4a5 100644 --- a/src/cryptography/x509/extensions.py +++ b/src/cryptography/x509/extensions.py @@ -18,6 +18,9 @@ from cryptography import utils 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 ( @@ -1151,6 +1154,39 @@ class InvalidityDate(object): invalidity_date = utils.read_only_property("_invalidity_date") +@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 ( + "".format( + list(self) + ) + ) + + @utils.register_interface(ExtensionType) class UnrecognizedExtension(object): def __init__(self, oid, value): diff --git a/tests/test_x509_ext.py b/tests/test_x509_ext.py index b89abdda..595ec703 100644 --- a/tests/test_x509_ext.py +++ b/tests/test_x509_ext.py @@ -3666,6 +3666,48 @@ class TestInhibitAnyPolicyExtension(object): assert iap.skip_certs == 5 +@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([])) == ( + "" + ) + + 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): -- cgit v1.2.3