# 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"),