From 0c9aed91697c5bc1eb16c2254406149e2395fdae Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sat, 8 Jul 2017 21:50:01 -0400 Subject: [PATCH] Fixed #3747 -- cache extensions on x.509 objects (#3769) * Fixed #3747 -- cache extensions on x.509 objects * be kind to cpython, save a dict lookup * flake8 * changelog --- CHANGELOG.rst | 7 +++ .../hazmat/backends/openssl/x509.py | 8 ++-- src/cryptography/utils.py | 14 ++++++ tests/test_cryptography_utils.py | 47 +++++++++++++++++++ 4 files changed, 72 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 28addfd3c07a..cc993bebde91 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -35,6 +35,13 @@ Changelog and :meth:`~cryptography.hazmat.primitives.asymmetric.dh.DHParameters.parameter_bytes` . +* The ``extensions`` attribute on :class:`~cryptography.x509.Certificate`, + :class:`~cryptography.x509.CertificateSigningRequest`, + :class:`~cryptography.x509.CertificateRevocationList`, and + :class:`~cryptography.x509.RevokedCertificate` now caches the computed + ``Extensions`` object. There should be no performance change, just a + performance improvement for programs accessing the ``extensions`` attribute + multiple times. 1.9 - 2017-05-29 ~~~~~~~~~~~~~~~~ diff --git a/src/cryptography/hazmat/backends/openssl/x509.py b/src/cryptography/hazmat/backends/openssl/x509.py index a04d6d532429..5bf0438ec26d 100644 --- a/src/cryptography/hazmat/backends/openssl/x509.py +++ b/src/cryptography/hazmat/backends/openssl/x509.py @@ -126,7 +126,7 @@ def signature_algorithm_oid(self): oid = _obj2txt(self._backend, alg[0].algorithm) return x509.ObjectIdentifier(oid) - @property + @utils.cached_property def extensions(self): if self._backend._lib.CRYPTOGRAPHY_OPENSSL_110_OR_GREATER: return _CERTIFICATE_EXTENSION_PARSER.parse( @@ -200,7 +200,7 @@ def revocation_date(self): ) ) - @property + @utils.cached_property def extensions(self): return _REVOKED_CERTIFICATE_EXTENSION_PARSER.parse( self._backend, self._x509_revoked @@ -334,7 +334,7 @@ def __len__(self): else: return self._backend._lib.sk_X509_REVOKED_num(revoked) - @property + @utils.cached_property def extensions(self): return _CRL_EXTENSION_PARSER.parse(self._backend, self._x509_crl) @@ -391,7 +391,7 @@ def signature_algorithm_oid(self): oid = _obj2txt(self._backend, alg[0].algorithm) return x509.ObjectIdentifier(oid) - @property + @utils.cached_property def extensions(self): x509_exts = self._backend._lib.X509_REQ_get_extensions(self._x509_req) return _CSR_EXTENSION_PARSER.parse(self._backend, x509_exts) diff --git a/src/cryptography/utils.py b/src/cryptography/utils.py index d28dc71d447f..efb12e213223 100644 --- a/src/cryptography/utils.py +++ b/src/cryptography/utils.py @@ -145,3 +145,17 @@ def deprecated(value, module_name, message, warning_class): if not isinstance(module, _ModuleWithDeprecations): sys.modules[module_name] = _ModuleWithDeprecations(module) return _DeprecatedValue(value, message, warning_class) + + +def cached_property(func): + cached_name = "_cached_{0}".format(func) + sentinel = object() + + def inner(instance): + cache = getattr(instance, cached_name, sentinel) + if cache is not sentinel: + return cache + result = func(instance) + setattr(instance, cached_name, result) + return result + return property(inner) diff --git a/tests/test_cryptography_utils.py b/tests/test_cryptography_utils.py index 037d11cc040d..290e1612ad82 100644 --- a/tests/test_cryptography_utils.py +++ b/tests/test_cryptography_utils.py @@ -4,8 +4,55 @@ from __future__ import absolute_import, division, print_function +import pytest + from cryptography import utils def test_int_from_bytes_bytearray(): assert utils.int_from_bytes(bytearray(b"\x02\x10"), "big") == 528 + + +class TestCachedProperty(object): + def test_simple(self): + accesses = [] + + class T(object): + @utils.cached_property + def t(self): + accesses.append(None) + return 14 + + assert T.t + t = T() + assert t.t == 14 + assert len(accesses) == 1 + assert t.t == 14 + assert len(accesses) == 1 + + t = T() + assert t.t == 14 + assert len(accesses) == 2 + assert t.t == 14 + assert len(accesses) == 2 + + def test_set(self): + accesses = [] + + class T(object): + @utils.cached_property + def t(self): + accesses.append(None) + return 14 + + t = T() + with pytest.raises(AttributeError): + t.t = None + assert len(accesses) == 0 + assert t.t == 14 + assert len(accesses) == 1 + with pytest.raises(AttributeError): + t.t = None + assert len(accesses) == 1 + assert t.t == 14 + assert len(accesses) == 1