diff options
46 files changed, 2355 insertions, 275 deletions
diff --git a/.mention-bot b/.mention-bot index 3c998c98..a05018e6 100644 --- a/.mention-bot +++ b/.mention-bot @@ -1,3 +1,3 @@ { - "userBlacklist": ["dreid"] + "userBlacklist": ["dreid", "exarkun", "dstufft"] } diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3e24633e..717c9e71 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,21 @@ Changelog .. note:: This version is not yet released and is under active development. +* **BACKWARDS INCOMPATIBLE:** + :class:`~cryptography.x509.RevokedCertificate` + :attr:`~cryptography.x509.RevokedCertificate.extensions` now uses extension + classes rather than returning raw values inside the + :class:`~cryptography.x509.Extension` + :attr:`~cryptography.x509.Extension.value`. The new classes + are: + + * :class:`~cryptography.x509.CertificateIssuer` + * :class:`~cryptography.x509.CRLReason` + * :class:`~cryptography.x509.InvalidityDate` +* Deprecated support for OpenSSL 0.9.8 and 1.0.0. At this time there is no time + table for actually dropping support, however we strongly encourage all users + to upgrade, as those versions no longer receives support from the OpenSSL + project. * The :class:`~cryptography.x509.Certificate` class now has :attr:`~cryptography.x509.Certificate.signature` and :attr:`~cryptography.x509.Certificate.tbs_certificate_bytes` attributes. @@ -31,6 +46,11 @@ Changelog * :class:`~cryptography.x509.AuthorityKeyIdentifier` * :class:`~cryptography.x509.CRLNumber` * :class:`~cryptography.x509.IssuerAlternativeName` +* Added :class:`~cryptography.x509.CertificateRevocationListBuilder` and + :class:`~cryptography.x509.RevokedCertificateBuilder` to allow creation of + CRLs. +* Unrecognized non-critical X.509 extensions are now parsed into an + :class:`~cryptography.x509.UnrecognizedExtension` object. 1.1.2 - 2015-12-10 ~~~~~~~~~~~~~~~~~~ diff --git a/docs/conf.py b/docs/conf.py index 5a4c41b1..dcc9c626 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -71,7 +71,7 @@ master_doc = 'index' # General information about the project. project = 'Cryptography' -copyright = '2013-2015, Individual Contributors' +copyright = '2013-2016, Individual Contributors' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/docs/development/submitting-patches.rst b/docs/development/submitting-patches.rst index 66105843..563bc81f 100644 --- a/docs/development/submitting-patches.rst +++ b/docs/development/submitting-patches.rst @@ -151,6 +151,6 @@ So, specifically: .. _`Write comments as complete sentences.`: http://nedbatchelder.com/blog/201401/comments_should_be_sentences.html .. _`syntax`: http://sphinx-doc.org/domains.html#info-field-lists -.. _`Studies have shown`: https://smartbear.com/smartbear/media/pdfs/wp-cc-11-best-practices-of-peer-code-review.pdf +.. _`Studies have shown`: https://smartbear.com/SmartBear/media/pdfs/11_Best_Practices_for_Peer_Code_Review.pdf .. _`our mailing list`: https://mail.python.org/mailman/listinfo/cryptography-dev -.. _`doc8`: https://github.com/stackforge/doc8 +.. _`doc8`: https://github.com/openstack/doc8 diff --git a/docs/development/test-vectors.rst b/docs/development/test-vectors.rst index 70766d53..ad945f2f 100644 --- a/docs/development/test-vectors.rst +++ b/docs/development/test-vectors.rst @@ -141,6 +141,10 @@ Custom X.509 Vectors * ``unsupported_extension.pem`` - An RSA 2048 bit self-signed certificate containing an unsupported extension type. The OID was encoded as "1.2.3.4" with an ``extnValue`` of "value". +* ``unsupported_extension_2.pem`` - A ``secp256r1`` certificate + containing two unsupported extensions. The OIDs are ``1.3.6.1.4.1.41482.2`` + with an ``extnValue`` of ``1.3.6.1.4.1.41482.1.2`` and + ``1.3.6.1.4.1.45724.2.1.1`` with an ``extnValue`` of ``\x03\x02\x040`` * ``unsupported_extension_critical.pem`` - An RSA 2048 bit self-signed certificate containing an unsupported extension type marked critical. The OID was encoded as "1.2.3.4" with an ``extnValue`` of "value". diff --git a/docs/faq.rst b/docs/faq.rst index 0b7bdce4..10c8656b 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -14,5 +14,5 @@ to NaCl. If you prefer NaCl's design, we highly recommend `PyNaCl`_. -.. _`NaCl`: http://nacl.cr.yp.to/ +.. _`NaCl`: https://nacl.cr.yp.to/ .. _`PyNaCl`: https://pynacl.readthedocs.org diff --git a/docs/hazmat/backends/interfaces.rst b/docs/hazmat/backends/interfaces.rst index 442bd0de..73011dd0 100644 --- a/docs/hazmat/backends/interfaces.rst +++ b/docs/hazmat/backends/interfaces.rst @@ -547,8 +547,8 @@ A specific ``backend`` may provide one or more of these interfaces. :class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm` that will be used to generate the request signature. - :returns: A new object with the - :class:`~cryptography.x509.CertificateSigningRequest` interface. + :returns: A new instance of + :class:`~cryptography.x509.CertificateSigningRequest`. .. method:: create_x509_certificate(builder, private_key, algorithm) @@ -567,9 +567,36 @@ A specific ``backend`` may provide one or more of these interfaces. :class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm` that will be used to generate the certificate signature. - :returns: A new object with the - :class:`~cryptography.x509.Certificate` interface. + :returns: A new instance of :class:`~cryptography.x509.Certificate`. + + .. method:: create_x509_crl(builder, private_key, algorithm) + + .. versionadded:: 1.2 + + :param builder: An instance of + :class:`~cryptography.x509.CertificateRevocationListBuilder`. + + :param private_key: The + :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey`, + :class:`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey` or + :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey` + that will be used to sign the CRL. + + :param algorithm: The + :class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm` + that will be used to generate the CRL signature. + + :returns: A new instance of + :class:`~cryptography.x509.CertificateRevocationList`. + + .. method:: create_x509_revoked_certificate(builder) + + .. versionadded:: 1.2 + + :param builder: An instance of RevokedCertificateBuilder. + :returns: A new instance of + :class:`~cryptography.x509.RevokedCertificate`. .. class:: DHBackend diff --git a/docs/hazmat/bindings/openssl.rst b/docs/hazmat/bindings/openssl.rst index 0ec0a3d6..99cd7a48 100644 --- a/docs/hazmat/bindings/openssl.rst +++ b/docs/hazmat/bindings/openssl.rst @@ -46,4 +46,4 @@ OpenSSL. .. _`CFFI`: https://cffi.readthedocs.org/ .. _`OpenSSL`: https://www.openssl.org/ -.. _`thread safety facilities`: https://www.openssl.org/docs/crypto/threads.html +.. _`thread safety facilities`: https://www.openssl.org/docs/manmaster/crypto/threads.html diff --git a/docs/hazmat/primitives/asymmetric/ec.rst b/docs/hazmat/primitives/asymmetric/ec.rst index c1619dd0..8e3a3659 100644 --- a/docs/hazmat/primitives/asymmetric/ec.rst +++ b/docs/hazmat/primitives/asymmetric/ec.rst @@ -504,7 +504,7 @@ Key Interfaces .. _`some concern`: https://crypto.stackexchange.com/questions/10263/should-we-trust-the-nist-recommended-ecc-parameters .. _`less than 224 bits`: http://www.ecrypt.eu.org/ecrypt2/documents/D.SPA.20.pdf .. _`elliptic curve diffie-hellman is faster than diffie-hellman`: http://digitalcommons.unl.edu/cgi/viewcontent.cgi?article=1100&context=cseconfwork -.. _`minimize the number of security concerns for elliptic-curve cryptography`: http://cr.yp.to/ecdh/curve25519-20060209.pdf +.. _`minimize the number of security concerns for elliptic-curve cryptography`: https://cr.yp.to/ecdh/curve25519-20060209.pdf .. _`SafeCurves`: http://safecurves.cr.yp.to/ .. _`ECDSA`: https://en.wikipedia.org/wiki/ECDSA .. _`EdDSA`: https://en.wikipedia.org/wiki/EdDSA diff --git a/docs/hazmat/primitives/asymmetric/serialization.rst b/docs/hazmat/primitives/asymmetric/serialization.rst index f14f4037..b94c0e10 100644 --- a/docs/hazmat/primitives/asymmetric/serialization.rst +++ b/docs/hazmat/primitives/asymmetric/serialization.rst @@ -118,7 +118,12 @@ all begin with ``-----BEGIN {format}-----`` and end with ``-----END :class:`~cryptography.hazmat.backends.interfaces.PEMSerializationBackend` provider. - :returns: A new instance of a private key. + :returns: One of + :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey`, + :class:`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey`, + or + :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey` + depending on the contents of ``data``. :raises ValueError: If the PEM data could not be decrypted or if its structure could not be decoded successfully. @@ -136,7 +141,8 @@ all begin with ``-----BEGIN {format}-----`` and end with ``-----END .. versionadded:: 0.6 Deserialize a public key from PEM encoded data to one of the supported - asymmetric public key types. + asymmetric public key types. The PEM encoded data is typically a + ``subjectPublicKeyInfo`` payload as specified in :rfc:`5280`. .. doctest:: @@ -151,7 +157,13 @@ all begin with ``-----BEGIN {format}-----`` and end with ``-----END :class:`~cryptography.hazmat.backends.interfaces.PEMSerializationBackend` provider. - :returns: A new instance of a public key. + + :returns: One of + :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`, + :class:`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey`, + or + :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey` + depending on the contents of ``data``. :raises ValueError: If the PEM data's structure could not be decoded successfully. @@ -183,7 +195,12 @@ the rest. :class:`~cryptography.hazmat.backends.interfaces.DERSerializationBackend` provider. - :returns: A new instance of a private key. + :returns: One of + :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey`, + :class:`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey`, + or + :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey` + depending on the contents of ``data``. :raises ValueError: If the DER data could not be decrypted or if its structure could not be decoded successfully. @@ -210,7 +227,8 @@ the rest. .. versionadded:: 0.8 Deserialize a public key from DER encoded data to one of the supported - asymmetric public key types. + asymmetric public key types. The DER encoded data is typically a + ``subjectPublicKeyInfo`` payload as specified in :rfc:`5280`. :param bytes data: The DER encoded key data. @@ -218,7 +236,12 @@ the rest. :class:`~cryptography.hazmat.backends.interfaces.DERSerializationBackend` provider. - :returns: A new instance of a public key. + :returns: One of + :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`, + :class:`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey`, + or + :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey` + depending on the contents of ``data``. :raises ValueError: If the DER data's structure could not be decoded successfully. @@ -275,7 +298,12 @@ DSA keys look almost identical but begin with ``ssh-dss`` rather than :class:`~cryptography.hazmat.backends.interfaces.EllipticCurveBackend` depending on the key's type. - :returns: A new instance of a public key type. + :returns: One of + :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`, + :class:`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey`, + or + :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey` + depending on the contents of ``data``. :raises ValueError: If the OpenSSH data could not be properly decoded or if the key is not in the proper format. diff --git a/docs/installation.rst b/docs/installation.rst index 16c42d2d..f9d2261a 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -21,6 +21,10 @@ Currently we test ``cryptography`` on Python 2.6, 2.7, 3.3, 3.4, 3.5, and PyPy * x86-64 Debian Wheezy (7.x), Jessie (8.x), and Debian Sid (unstable) * 32-bit and 64-bit Python on 64-bit Windows Server 2012 +.. warning:: + Python 2.6 is no longer supported by the Python core team. A future version + of cryptography will drop support for this version. + We test compiling with ``clang`` as well as ``gcc`` and use the following OpenSSL releases: @@ -33,6 +37,11 @@ OpenSSL releases: * ``OpenSSL 1.0.1f`` * ``OpenSSL 1.0.2-latest`` +.. warning:: + OpenSSL versions 0.9.8 and 1.0.0 are no longer supported by the OpenSSL + project. A future version of cryptography will drop support for these + releases. + On Windows ---------- @@ -58,6 +67,8 @@ to include the proper locations. For example: C:\> set INCLUDE=C:\OpenSSL-win64\include;%INCLUDE% C:\> pip install cryptography +If you need to rebuild ``cryptography`` for any reason be sure to clear the +local `wheel cache`_. .. _build-on-linux: @@ -173,7 +184,7 @@ Building cryptography on OS X ----------------------------- The wheel package on OS X is a statically linked build (as of 1.0.1) so for -users on 10.10 (Yosemite) and above you only need one step: +users with pip 1.5 or above you only need one step: .. code-block:: console @@ -182,8 +193,8 @@ users on 10.10 (Yosemite) and above you only need one step: If you want to build cryptography yourself or are on an older OS X version cryptography requires the presence of a C compiler, development headers, and the proper libraries. On OS X much of this is provided by Apple's Xcode -development tools. To install the Xcode command line tools open a terminal -window and run: +development tools. To install the Xcode command line tools (on OS X 10.9+) +open a terminal window and run: .. code-block:: console @@ -227,6 +238,9 @@ You can also build cryptography statically: $ sudo port install openssl $ env CRYPTOGRAPHY_OSX_NO_LINK_FLAGS=1 LDFLAGS="/opt/local/lib/libssl.a /opt/local/lib/libcrypto.a" CFLAGS="-I/opt/local/include" pip install cryptography +If you need to rebuild ``cryptography`` for any reason be sure to clear the +local `wheel cache`_. + Building cryptography with conda -------------------------------- @@ -257,3 +271,4 @@ information, consult `Greg Wilson's blog post`_ on the subject. .. _`Greg Wilson's blog post`: http://software-carpentry.org/blog/2014/04/mr-biczo-was-right.html .. _virtualenv: https://virtualenv.pypa.io/en/latest/ .. _openssl.org: https://openssl.org/source/ +.. _`wheel cache`: https://pip.pypa.io/en/stable/reference/pip_install/#caching diff --git a/docs/limitations.rst b/docs/limitations.rst index 0dfc49ca..503bdfe4 100644 --- a/docs/limitations.rst +++ b/docs/limitations.rst @@ -15,5 +15,5 @@ software in Python is potentially vulnerable to this attack. The Likelihood: unlikely, Remediation Cost: expensive to repair" and we do not consider this a high risk for most users. -.. _`Memory wiping`: http://blogs.msdn.com/b/oldnewthing/archive/2013/05/29/10421912.aspx +.. _`Memory wiping`: https://blogs.msdn.microsoft.com/oldnewthing/20130529-00/?p=4223/ .. _`CERT secure coding guidelines`: https://www.securecoding.cert.org/confluence/display/c/MEM03-C.+Clear+sensitive+information+stored+in+reusable+resources diff --git a/docs/x509/reference.rst b/docs/x509/reference.rst index 4f4ce4fa..8bb3f40d 100644 --- a/docs/x509/reference.rst +++ b/docs/x509/reference.rst @@ -761,6 +761,113 @@ X.509 CSR (Certificate Signing Request) Object key embedded in the CSR). This data may be used to validate the CSR signature. +X.509 Certificate Revocation List Builder +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. class:: CertificateRevocationListBuilder + + .. versionadded:: 1.2 + + .. doctest:: + + >>> from cryptography import x509 + >>> from cryptography.hazmat.backends import default_backend + >>> from cryptography.hazmat.primitives import hashes + >>> from cryptography.hazmat.primitives.asymmetric import rsa + >>> from cryptography.x509.oid import NameOID + >>> import datetime + >>> one_day = datetime.timedelta(1, 0, 0) + >>> private_key = rsa.generate_private_key( + ... public_exponent=65537, + ... key_size=2048, + ... backend=default_backend() + ... ) + >>> builder = x509.CertificateRevocationListBuilder() + >>> builder = builder.issuer_name(x509.Name([ + ... x509.NameAttribute(NameOID.COMMON_NAME, u'cryptography.io CA'), + ... ])) + >>> builder = builder.last_update(datetime.datetime.today()) + >>> builder = builder.next_update(datetime.datetime.today() + one_day) + >>> revoked_cert = x509.RevokedCertificateBuilder().serial_number( + ... 333 + ... ).revocation_date( + ... datetime.datetime.today() + ... ).build(default_backend()) + >>> builder = builder.add_revoked_certificate(revoked_cert) + >>> crl = builder.sign( + ... private_key=private_key, algorithm=hashes.SHA256(), + ... backend=default_backend() + ... ) + >>> len(crl) + 1 + + .. method:: issuer_name(name) + + Sets the issuer's distinguished name. + + :param name: The :class:`~cryptography.x509.Name` that describes the + issuer (CA). + + .. method:: last_update(time) + + Sets this CRL's activation time. This is the time from which + clients can start trusting this CRL. It may be different from + the time at which this CRL was created. This is also known as the + ``thisUpdate`` time. + + :param time: The :class:`datetime.datetime` object (in UTC) that marks + the activation time for this CRL. The CRL may not be trusted if it + is used before this time. + + .. method:: next_update(time) + + Sets this CRL's next update time. This is the time by which + a new CRL will be issued. The CA is allowed to issue a new CRL before + this date, however clients are not required to check for it. + + :param time: The :class:`datetime.datetime` object (in UTC) that marks + the next update time for this CRL. + + .. method:: add_extension(extension, critical) + + Adds an X.509 extension to this CRL. + + :param extension: An extension with the + :class:`~cryptography.x509.ExtensionType` interface. + + :param critical: Set to ``True`` if the extension must be understood and + handled by whoever reads the CRL. + + .. method:: add_revoked_certificate(revoked_certificate) + + Adds a revoked certificate to this CRL. + + :param revoked_certificate: An instance of + :class:`~cryptography.x509.RevokedCertificate`. These can be + obtained from an existing CRL or created with + :class:`~cryptography.x509.RevokedCertificateBuilder`. + + .. method:: sign(private_key, algorithm, backend) + + Sign this CRL using the CA's private key. + + :param private_key: The + :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey`, + :class:`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey` or + :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey` + that will be used to sign the certificate. + + :param algorithm: The + :class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm` that + will be used to generate the signature. + + :param backend: Backend that will be used to build the CRL. + Must support the + :class:`~cryptography.hazmat.backends.interfaces.X509Backend` + interface. + + :returns: :class:`~cryptography.x509.CertificateRevocationList` + X.509 Revoked Certificate Object ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -800,8 +907,66 @@ X.509 Revoked Certificate Object >>> for ext in revoked_certificate.extensions: ... print(ext) - <Extension(oid=<ObjectIdentifier(oid=2.5.29.24, name=invalidityDate)>, critical=False, value=2015-01-01 00:00:00)> - <Extension(oid=<ObjectIdentifier(oid=2.5.29.21, name=cRLReason)>, critical=False, value=ReasonFlags.key_compromise)> + <Extension(oid=<ObjectIdentifier(oid=2.5.29.24, name=invalidityDate)>, critical=False, value=<InvalidityDate(invalidity_date=2015-01-01 00:00:00)>)> + <Extension(oid=<ObjectIdentifier(oid=2.5.29.21, name=cRLReason)>, critical=False, value=<CRLReason(reason=ReasonFlags.key_compromise)>)> + +X.509 Revoked Certificate Builder +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. class:: RevokedCertificateBuilder + + This class is used to create :class:`~cryptography.x509.RevokedCertificate` + objects that can be used with the + :class:`~cryptography.x509.CertificateRevocationListBuilder`. + + .. versionadded:: 1.2 + + .. doctest:: + + >>> from cryptography import x509 + >>> from cryptography.hazmat.backends import default_backend + >>> import datetime + >>> builder = x509.RevokedCertificateBuilder() + >>> builder = builder.revocation_date(datetime.datetime.today()) + >>> builder = builder.serial_number(3333) + >>> revoked_certificate = builder.build(default_backend()) + >>> isinstance(revoked_certificate, x509.RevokedCertificate) + True + + .. method:: serial_number(serial_number) + + Sets the revoked certificate's serial number. + + :param serial_number: Integer number that is used to identify the + revoked certificate. + + .. method:: revocation_date(time) + + Sets the certificate's revocation date. + + :param time: The :class:`datetime.datetime` object (in UTC) that marks the + revocation time for the certificate. + + .. method:: add_extension(extension, critical) + + Adds an X.509 extension to this revoked certificate. + + :param extension: An instance of one of the + :ref:`CRL entry extensions <crl_entry_extensions>`. + + :param critical: Set to ``True`` if the extension must be understood and + handled. + + .. method:: build(backend) + + Create a revoked certificate object using the provided backend. + + :param backend: Backend that will be used to build the revoked + certificate. Must support the + :class:`~cryptography.hazmat.backends.interfaces.X509Backend` + interface. + + :returns: :class:`~cryptography.x509.RevokedCertificate` X.509 CSR (Certificate Signing Request) Builder Object ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -1715,6 +1880,27 @@ X.509 Extensions :type: int +.. class:: UnrecognizedExtension + + .. versionadded:: 1.2 + + A generic extension class used to hold the raw value of **non-critical** + extensions that ``cryptography`` does not know how to parse. Extensions + marked critical will raise + :class:`~cryptography.x509.UnsupportedExtension`. + + .. attribute:: oid + + :type: :class:`ObjectIdentifier` + + Returns the OID associated with this extension. + + .. attribute:: value + + :type: byte + + Returns the DER encoded bytes payload of the extension. + .. class:: CertificatePolicies(policies) .. versionadded:: 0.9 @@ -1801,6 +1987,89 @@ These classes may be present within a :class:`CertificatePolicies` instance. A list of integers. +.. _crl_entry_extensions: + +CRL Entry Extensions +~~~~~~~~~~~~~~~~~~~~ + +These extensions are only valid within a :class:`RevokedCertificate` object. + +.. class:: CertificateIssuer(general_names) + + .. versionadded:: 1.2 + + The certificate issuer is an extension that is only valid inside + :class:`~cryptography.x509.RevokedCertificate` objects. If the + ``indirectCRL`` property of the parent CRL's IssuingDistributionPoint + extension is set, then this extension identifies the certificate issuer + associated with the revoked certificate. The object is iterable to get + every element. + + :param list general_names: A list of :class:`GeneralName` instances. + + .. attribute:: oid + + :type: :class:`ObjectIdentifier` + + Returns + :attr:`~cryptography.x509.oid.CRLEntryExtensionOID.CERTIFICATE_ISSUER`. + + .. method:: get_values_for_type(type) + + :param type: A :class:`GeneralName` instance. This is one of the + :ref:`general name classes <general_name_classes>`. + + :returns: A list of values extracted from the matched general names. + The type of the returned values depends on the :class:`GeneralName`. + +.. class:: CRLReason(reason) + + .. versionadded:: 1.2 + + CRL reason (also known as ``reasonCode``) is an extension that is only + valid inside :class:`~cryptography.x509.RevokedCertificate` objects. It + identifies a reason for the certificate revocation. + + :param reason: A value from the + :class:`~cryptography.x509.oid.CRLEntryExtensionOID` enum. + + .. attribute:: oid + + :type: :class:`ObjectIdentifier` + + Returns + :attr:`~cryptography.x509.oid.CRLEntryExtensionOID.CRL_REASON`. + + .. attribute:: reason + + :type: An element from :class:`~cryptography.x509.ReasonFlags` + +.. class:: InvalidityDate(invalidity_date) + + .. versionadded:: 1.2 + + Invalidity date is an extension that is only valid inside + :class:`~cryptography.x509.RevokedCertificate` objects. It provides + the date on which it is known or suspected that the private key was + compromised or that the certificate otherwise became invalid. + This date may be earlier than the revocation date in the CRL entry, + which is the date at which the CA processed the revocation. + + :param invalidity_date: The :class:`datetime.datetime` when it is known + or suspected that the private key was compromised. + + .. attribute:: oid + + :type: :class:`ObjectIdentifier` + + Returns + :attr:`~cryptography.x509.oid.CRLEntryExtensionOID.INVALIDITY_DATE`. + + .. attribute:: invalidity_date + + :type: :class:`datetime.datetime` + + Object Identifiers ~~~~~~~~~~~~~~~~~~ @@ -2123,6 +2392,22 @@ instances. The following common OIDs are available as constants. the ``CRLNumber`` extension type. This extension only has meaning for certificate revocation lists. +.. class:: CRLEntryExtensionOID + + .. versionadded:: 1.2 + + .. attribute:: CERTIFICATE_ISSUER + + Corresponds to the dotted string ``"2.5.29.29"``. + + .. attribute:: CRL_REASON + + Corresponds to the dotted string ``"2.5.29.21"``. + + .. attribute:: INVALIDITY_DATE + + Corresponds to the dotted string ``"2.5.29.24"``. + Exceptions ~~~~~~~~~~ .. currentmodule:: cryptography.x509 @@ -2150,7 +2435,8 @@ Exceptions .. class:: UnsupportedExtension - This is raised when a certificate contains an unsupported extension type. + This is raised when a certificate contains an unsupported extension type + that is marked ``critical``. .. attribute:: oid @@ -37,7 +37,7 @@ requirements = [ "idna>=2.0", "pyasn1>=0.1.8", "six>=1.4.1", - "setuptools", + "setuptools>=1.0", ] setup_requirements = [] diff --git a/src/_cffi_src/build_commoncrypto.py b/src/_cffi_src/build_commoncrypto.py index 1c2692a7..4e69b6d1 100644 --- a/src/_cffi_src/build_commoncrypto.py +++ b/src/_cffi_src/build_commoncrypto.py @@ -22,6 +22,7 @@ ffi = build_ffi_for_binding( "seckey", "seckeychain", "sectransform", + "sectrust", ], extra_link_args=[ "-framework", "Security", "-framework", "CoreFoundation" diff --git a/src/_cffi_src/build_constant_time.py b/src/_cffi_src/build_constant_time.py index 6d9a8f54..7a11f7b5 100644 --- a/src/_cffi_src/build_constant_time.py +++ b/src/_cffi_src/build_constant_time.py @@ -5,9 +5,8 @@ from __future__ import absolute_import, division, print_function import os -import sys -from _cffi_src.utils import build_ffi, extra_link_args +from _cffi_src.utils import build_ffi, compiler_type, extra_link_args with open(os.path.join( @@ -24,5 +23,5 @@ ffi = build_ffi( module_name="_constant_time", cdef_source=types, verify_source=functions, - extra_link_args=extra_link_args(sys.platform), + extra_link_args=extra_link_args(compiler_type()), ) diff --git a/src/_cffi_src/build_openssl.py b/src/_cffi_src/build_openssl.py index c856e3d9..c47b3082 100644 --- a/src/_cffi_src/build_openssl.py +++ b/src/_cffi_src/build_openssl.py @@ -7,7 +7,9 @@ from __future__ import absolute_import, division, print_function import os import sys -from _cffi_src.utils import build_ffi_for_binding, extra_link_args +from _cffi_src.utils import ( + build_ffi_for_binding, compiler_type, extra_link_args +) def _get_openssl_libraries(platform): @@ -92,5 +94,5 @@ ffi = build_ffi_for_binding( pre_include=_OSX_PRE_INCLUDE, post_include=_OSX_POST_INCLUDE, libraries=_get_openssl_libraries(sys.platform), - extra_link_args=extra_link_args(sys.platform), + extra_link_args=extra_link_args(compiler_type()), ) diff --git a/src/_cffi_src/build_padding.py b/src/_cffi_src/build_padding.py index 5df93d80..4c5096a1 100644 --- a/src/_cffi_src/build_padding.py +++ b/src/_cffi_src/build_padding.py @@ -5,9 +5,8 @@ from __future__ import absolute_import, division, print_function import os -import sys -from _cffi_src.utils import build_ffi, extra_link_args +from _cffi_src.utils import build_ffi, compiler_type, extra_link_args with open(os.path.join( @@ -24,5 +23,5 @@ ffi = build_ffi( module_name="_padding", cdef_source=types, verify_source=functions, - extra_link_args=extra_link_args(sys.platform), + extra_link_args=extra_link_args(compiler_type()), ) diff --git a/src/_cffi_src/commoncrypto/sectrust.py b/src/_cffi_src/commoncrypto/sectrust.py new file mode 100644 index 00000000..b787afad --- /dev/null +++ b/src/_cffi_src/commoncrypto/sectrust.py @@ -0,0 +1,22 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import absolute_import, division, print_function + +INCLUDES = """ +#include <Security/SecTrust.h> +""" + +TYPES = """ +""" + +FUNCTIONS = """ +OSStatus SecTrustCopyAnchorCertificates(CFArrayRef *); +""" + +MACROS = """ +""" + +CUSTOMIZATIONS = """ +""" diff --git a/src/_cffi_src/openssl/asn1.py b/src/_cffi_src/openssl/asn1.py index ddf4b9c5..30bd2451 100644 --- a/src/_cffi_src/openssl/asn1.py +++ b/src/_cffi_src/openssl/asn1.py @@ -95,13 +95,16 @@ ASN1_UTCTIME *ASN1_UTCTIME_set(ASN1_UTCTIME *, time_t); /* ASN1 GENERALIZEDTIME */ int ASN1_GENERALIZEDTIME_set_string(ASN1_GENERALIZEDTIME *, const char *); +ASN1_GENERALIZEDTIME *ASN1_GENERALIZEDTIME_set(ASN1_GENERALIZEDTIME *, time_t); void ASN1_GENERALIZEDTIME_free(ASN1_GENERALIZEDTIME *); +int i2d_ASN1_GENERALIZEDTIME(ASN1_GENERALIZEDTIME *, unsigned char **); /* ASN1 ENUMERATED */ ASN1_ENUMERATED *ASN1_ENUMERATED_new(void); void ASN1_ENUMERATED_free(ASN1_ENUMERATED *); int ASN1_ENUMERATED_set(ASN1_ENUMERATED *, long); long ASN1_ENUMERATED_get(ASN1_ENUMERATED *); +int i2d_ASN1_ENUMERATED(ASN1_ENUMERATED *, unsigned char **); ASN1_VALUE *ASN1_item_d2i(ASN1_VALUE **, const unsigned char **, long, const ASN1_ITEM *); diff --git a/src/_cffi_src/openssl/bignum.py b/src/_cffi_src/openssl/bignum.py index ae035007..455afdc1 100644 --- a/src/_cffi_src/openssl/bignum.py +++ b/src/_cffi_src/openssl/bignum.py @@ -71,6 +71,8 @@ int BN_mask_bits(BIGNUM *, int); """ MACROS = """ +int BN_num_bytes(const BIGNUM *); + int BN_zero(BIGNUM *); int BN_one(BIGNUM *); int BN_mod(BIGNUM *, const BIGNUM *, const BIGNUM *, BN_CTX *); diff --git a/src/_cffi_src/openssl/err.py b/src/_cffi_src/openssl/err.py index 6ec13775..9d97be16 100644 --- a/src/_cffi_src/openssl/err.py +++ b/src/_cffi_src/openssl/err.py @@ -230,6 +230,7 @@ static const int RSA_R_DIGEST_TOO_BIG_FOR_RSA_KEY; static const int RSA_R_BLOCK_TYPE_IS_NOT_01; static const int RSA_R_BLOCK_TYPE_IS_NOT_02; static const int RSA_R_PKCS_DECODING_ERROR; +static const int RSA_R_OAEP_DECODING_ERROR; static const int RSA_F_RSA_SIGN; """ diff --git a/src/_cffi_src/openssl/pem.py b/src/_cffi_src/openssl/pem.py index 846e64e3..4eb6bb45 100644 --- a/src/_cffi_src/openssl/pem.py +++ b/src/_cffi_src/openssl/pem.py @@ -79,6 +79,7 @@ MACROS = """ int PEM_write_bio_ECPrivateKey(BIO *, EC_KEY *, const EVP_CIPHER *, unsigned char *, int, pem_password_cb *, void *); +int PEM_write_bio_DHparams(BIO *, DH *); """ CUSTOMIZATIONS = """ diff --git a/src/_cffi_src/openssl/x509.py b/src/_cffi_src/openssl/x509.py index 0fc49ac5..c5eb600a 100644 --- a/src/_cffi_src/openssl/x509.py +++ b/src/_cffi_src/openssl/x509.py @@ -193,6 +193,8 @@ X509_EXTENSION *X509_REVOKED_get_ext(X509_REVOKED *, int); int X509_REVOKED_add_ext(X509_REVOKED *, X509_EXTENSION*, int); int X509_REVOKED_add1_ext_i2d(X509_REVOKED *, int, void *, int, unsigned long); +int X509_REVOKED_set_revocationDate(X509_REVOKED *, ASN1_TIME *); + X509_CRL *X509_CRL_new(void); X509_CRL *d2i_X509_CRL_bio(BIO *, X509_CRL **); X509_EXTENSION *X509_CRL_get_ext(X509_CRL *, int); @@ -268,6 +270,8 @@ void PKCS8_PRIV_KEY_INFO_free(PKCS8_PRIV_KEY_INFO *); """ MACROS = """ +X509_REVOKED *Cryptography_X509_REVOKED_dup(X509_REVOKED *); + int i2d_X509_CINF(X509_CINF *, unsigned char **); int i2d_X509_CRL_INFO(X509_CRL_INFO *, unsigned char **); int i2d_X509_REQ_INFO(X509_REQ_INFO *, unsigned char **); @@ -290,6 +294,7 @@ X509_EXTENSIONS *sk_X509_EXTENSION_new_null(void); int sk_X509_EXTENSION_num(X509_EXTENSIONS *); X509_EXTENSION *sk_X509_EXTENSION_value(X509_EXTENSIONS *, int); int sk_X509_EXTENSION_push(X509_EXTENSIONS *, X509_EXTENSION *); +int sk_X509_EXTENSION_insert(X509_EXTENSIONS *, X509_EXTENSION *, int); X509_EXTENSION *sk_X509_EXTENSION_delete(X509_EXTENSIONS *, int); void sk_X509_EXTENSION_free(X509_EXTENSIONS *); @@ -362,4 +367,12 @@ int (*i2d_ECPrivateKey_bio)(BIO *, EC_KEY *) = NULL; EC_KEY *(*o2i_ECPublicKey)(EC_KEY **, const unsigned char **, long) = NULL; int (*i2o_ECPublicKey)(EC_KEY *, unsigned char **) = NULL; #endif + +/* X509_REVOKED_dup only exists on 1.0.2+. It is implemented using + IMPLEMENT_ASN1_DUP_FUNCTION. The below is the equivalent so we have + it available on all OpenSSLs. */ +X509_REVOKED *Cryptography_X509_REVOKED_dup(X509_REVOKED *rev) { + return ASN1_item_dup(ASN1_ITEM_rptr(X509_REVOKED), rev); +} + """ diff --git a/src/_cffi_src/utils.py b/src/_cffi_src/utils.py index 0b00353e..bdce2f3b 100644 --- a/src/_cffi_src/utils.py +++ b/src/_cffi_src/utils.py @@ -5,6 +5,8 @@ from __future__ import absolute_import, division, print_function import sys +from distutils.ccompiler import new_compiler +from distutils.dist import Distribution from cffi import FFI @@ -79,10 +81,23 @@ def build_ffi(module_name, cdef_source, verify_source, libraries=[], return ffi -def extra_link_args(platform): - if platform != "win32": - return [] +def extra_link_args(compiler_type): + if compiler_type == 'msvc': + # Enable NX and ASLR for Windows builds on MSVC. These are enabled by + # default on Python 3.3+ but not on 2.x. + return ['/NXCOMPAT', '/DYNAMICBASE'] else: - # Enable NX and ASLR for Windows builds. These are enabled by default - # on Python 3.3+ but not on 2.x. - return ["/NXCOMPAT", "/DYNAMICBASE"] + return [] + + +def compiler_type(): + """ + Gets the compiler type from distutils. On Windows with MSVC it will be + "msvc". On OS X and linux it is "unix". + """ + dist = Distribution() + dist.parse_config_files() + cmd = dist.get_command_obj('build') + cmd.ensure_finalized() + compiler = new_compiler(compiler=cmd.compiler) + return compiler.compiler_type diff --git a/src/cryptography/__about__.py b/src/cryptography/__about__.py index fbc1e735..f25cbc89 100644 --- a/src/cryptography/__about__.py +++ b/src/cryptography/__about__.py @@ -20,4 +20,4 @@ __author__ = "The cryptography developers" __email__ = "cryptography-dev@python.org" __license__ = "BSD or Apache License, Version 2.0" -__copyright__ = "Copyright 2013-2015 {0}".format(__author__) +__copyright__ = "Copyright 2013-2016 {0}".format(__author__) diff --git a/src/cryptography/hazmat/backends/interfaces.py b/src/cryptography/hazmat/backends/interfaces.py index 92d9653a..5b9e6f38 100644 --- a/src/cryptography/hazmat/backends/interfaces.py +++ b/src/cryptography/hazmat/backends/interfaces.py @@ -292,6 +292,20 @@ class X509Backend(object): Create and sign an X.509 certificate from a CertificateBuilder object. """ + @abc.abstractmethod + def create_x509_crl(self, builder, private_key, algorithm): + """ + Create and sign an X.509 CertificateRevocationList from a + CertificateRevocationListBuilder object. + """ + + @abc.abstractmethod + def create_x509_revoked_certificate(self, builder): + """ + Create a RevokedCertificate object from a RevokedCertificateBuilder + object. + """ + @six.add_metaclass(abc.ABCMeta) class DHBackend(object): diff --git a/src/cryptography/hazmat/backends/multibackend.py b/src/cryptography/hazmat/backends/multibackend.py index bbaaf424..65f18531 100644 --- a/src/cryptography/hazmat/backends/multibackend.py +++ b/src/cryptography/hazmat/backends/multibackend.py @@ -384,3 +384,21 @@ class MultiBackend(object): "This backend does not support X.509.", _Reasons.UNSUPPORTED_X509 ) + + def create_x509_crl(self, builder, private_key, algorithm): + for b in self._filtered_backends(X509Backend): + return b.create_x509_crl(builder, private_key, algorithm) + + raise UnsupportedAlgorithm( + "This backend does not support X.509.", + _Reasons.UNSUPPORTED_X509 + ) + + def create_x509_revoked_certificate(self, builder): + for b in self._filtered_backends(X509Backend): + return b.create_x509_revoked_certificate(builder) + + raise UnsupportedAlgorithm( + "This backend does not support X.509.", + _Reasons.UNSUPPORTED_X509 + ) diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py index e69554f9..e8b0322e 100644 --- a/src/cryptography/hazmat/backends/openssl/backend.py +++ b/src/cryptography/hazmat/backends/openssl/backend.py @@ -37,8 +37,9 @@ from cryptography.hazmat.backends.openssl.rsa import ( _RSAPrivateKey, _RSAPublicKey ) from cryptography.hazmat.backends.openssl.x509 import ( - _Certificate, _CertificateRevocationList, _CertificateSigningRequest, - _DISTPOINT_TYPE_FULLNAME, _DISTPOINT_TYPE_RELATIVENAME + _CRL_ENTRY_REASON_ENUM_TO_CODE, _Certificate, _CertificateRevocationList, + _CertificateSigningRequest, _DISTPOINT_TYPE_FULLNAME, + _DISTPOINT_TYPE_RELATIVENAME, _RevokedCertificate ) from cryptography.hazmat.bindings._openssl import ffi as _ffi from cryptography.hazmat.bindings.openssl import binding @@ -53,7 +54,7 @@ from cryptography.hazmat.primitives.ciphers.algorithms import ( from cryptography.hazmat.primitives.ciphers.modes import ( CBC, CFB, CFB8, CTR, ECB, GCM, OFB ) -from cryptography.x509.oid import ExtensionOID, NameOID +from cryptography.x509.oid import CRLEntryExtensionOID, ExtensionOID, NameOID _MemoryBIO = collections.namedtuple("_MemoryBIO", ["bio", "char_ptr"]) @@ -115,10 +116,9 @@ def _encode_asn1_str_gc(backend, data, length): return s -def _encode_inhibit_any_policy(backend, inhibit_any_policy): - asn1int = _encode_asn1_int_gc(backend, inhibit_any_policy.skip_certs) - pp = backend._ffi.new('unsigned char **') - r = backend._lib.i2d_ASN1_INTEGER(asn1int, pp) +def _encode_extension_to_der(backend, i2d_func, value): + pp = backend._ffi.new("unsigned char **") + r = i2d_func(value, pp) backend.openssl_assert(r > 0) pp = backend._ffi.gc( pp, lambda pointer: backend._lib.OPENSSL_free(pointer[0]) @@ -126,6 +126,13 @@ def _encode_inhibit_any_policy(backend, inhibit_any_policy): return pp, r +def _encode_inhibit_any_policy(backend, inhibit_any_policy): + asn1int = _encode_asn1_int_gc(backend, inhibit_any_policy.skip_certs) + return _encode_extension_to_der( + backend, backend._lib.i2d_ASN1_INTEGER, asn1int + ) + + def _encode_name(backend, attributes): """ The X509_NAME created will not be gc'd. Use _encode_name_gc if needed. @@ -153,6 +160,41 @@ def _encode_name_gc(backend, attributes): return subject +def _encode_crl_number(backend, crl_number): + asn1int = _encode_asn1_int_gc(backend, crl_number.crl_number) + return _encode_extension_to_der( + backend, backend._lib.i2d_ASN1_INTEGER, asn1int + ) + + +def _encode_crl_reason(backend, crl_reason): + asn1enum = backend._lib.ASN1_ENUMERATED_new() + backend.openssl_assert(asn1enum != backend._ffi.NULL) + asn1enum = backend._ffi.gc(asn1enum, backend._lib.ASN1_ENUMERATED_free) + res = backend._lib.ASN1_ENUMERATED_set( + asn1enum, _CRL_ENTRY_REASON_ENUM_TO_CODE[crl_reason.reason] + ) + backend.openssl_assert(res == 1) + + return _encode_extension_to_der( + backend, backend._lib.i2d_ASN1_ENUMERATED, asn1enum + ) + + +def _encode_invalidity_date(backend, invalidity_date): + time = backend._lib.ASN1_GENERALIZEDTIME_set( + backend._ffi.NULL, calendar.timegm( + invalidity_date.invalidity_date.timetuple() + ) + ) + backend.openssl_assert(time != backend._ffi.NULL) + time = backend._ffi.gc(time, backend._lib.ASN1_GENERALIZEDTIME_free) + + return _encode_extension_to_der( + backend, backend._lib.i2d_ASN1_GENERALIZEDTIME, time + ) + + def _encode_certificate_policies(backend, certificate_policies): cp = backend._lib.sk_POLICYINFO_new_null() backend.openssl_assert(cp != backend._ffi.NULL) @@ -200,13 +242,9 @@ def _encode_certificate_policies(backend, certificate_policies): pi.qualifiers = pqis - pp = backend._ffi.new('unsigned char **') - r = backend._lib.i2d_CERTIFICATEPOLICIES(cp, pp) - backend.openssl_assert(r > 0) - pp = backend._ffi.gc( - pp, lambda pointer: backend._lib.OPENSSL_free(pointer[0]) + return _encode_extension_to_der( + backend, backend._lib.i2d_CERTIFICATEPOLICIES, cp ) - return pp, r def _encode_notice_reference(backend, notice): @@ -282,13 +320,9 @@ def _encode_key_usage(backend, key_usage): res = set_bit(ku, 8, 0) backend.openssl_assert(res == 1) - pp = backend._ffi.new('unsigned char **') - r = backend._lib.i2d_ASN1_BIT_STRING(ku, pp) - backend.openssl_assert(r > 0) - pp = backend._ffi.gc( - pp, lambda pointer: backend._lib.OPENSSL_free(pointer[0]) + return _encode_extension_to_der( + backend, backend._lib.i2d_ASN1_BIT_STRING, ku ) - return pp, r def _encode_authority_key_identifier(backend, authority_keyid): @@ -312,13 +346,9 @@ def _encode_authority_key_identifier(backend, authority_keyid): backend, authority_keyid.authority_cert_serial_number ) - pp = backend._ffi.new('unsigned char **') - r = backend._lib.i2d_AUTHORITY_KEYID(akid, pp) - backend.openssl_assert(r > 0) - pp = backend._ffi.gc( - pp, lambda pointer: backend._lib.OPENSSL_free(pointer[0]) + return _encode_extension_to_der( + backend, backend._lib.i2d_AUTHORITY_KEYID, akid ) - return pp, r def _encode_basic_constraints(backend, basic_constraints): @@ -332,14 +362,9 @@ def _encode_basic_constraints(backend, basic_constraints): backend, basic_constraints.path_length ) - # Fetch the encoded payload. - pp = backend._ffi.new('unsigned char **') - r = backend._lib.i2d_BASIC_CONSTRAINTS(constraints, pp) - backend.openssl_assert(r > 0) - pp = backend._ffi.gc( - pp, lambda pointer: backend._lib.OPENSSL_free(pointer[0]) + return _encode_extension_to_der( + backend, backend._lib.i2d_BASIC_CONSTRAINTS, constraints ) - return pp, r def _encode_authority_information_access(backend, authority_info_access): @@ -359,13 +384,9 @@ def _encode_authority_information_access(backend, authority_info_access): res = backend._lib.sk_ACCESS_DESCRIPTION_push(aia, ad) backend.openssl_assert(res >= 1) - pp = backend._ffi.new('unsigned char **') - r = backend._lib.i2d_AUTHORITY_INFO_ACCESS(aia, pp) - backend.openssl_assert(r > 0) - pp = backend._ffi.gc( - pp, lambda pointer: backend._lib.OPENSSL_free(pointer[0]) + return _encode_extension_to_der( + backend, backend._lib.i2d_AUTHORITY_INFO_ACCESS, aia ) - return pp, r def _encode_general_names(backend, names): @@ -384,24 +405,16 @@ def _encode_alt_name(backend, san): general_names = backend._ffi.gc( general_names, backend._lib.GENERAL_NAMES_free ) - pp = backend._ffi.new("unsigned char **") - r = backend._lib.i2d_GENERAL_NAMES(general_names, pp) - backend.openssl_assert(r > 0) - pp = backend._ffi.gc( - pp, lambda pointer: backend._lib.OPENSSL_free(pointer[0]) + return _encode_extension_to_der( + backend, backend._lib.i2d_GENERAL_NAMES, general_names ) - return pp, r def _encode_subject_key_identifier(backend, ski): asn1_str = _encode_asn1_str_gc(backend, ski.digest, len(ski.digest)) - pp = backend._ffi.new("unsigned char **") - r = backend._lib.i2d_ASN1_OCTET_STRING(asn1_str, pp) - backend.openssl_assert(r > 0) - pp = backend._ffi.gc( - pp, lambda pointer: backend._lib.OPENSSL_free(pointer[0]) + return _encode_extension_to_der( + backend, backend._lib.i2d_ASN1_OCTET_STRING, asn1_str ) - return pp, r def _encode_general_name(backend, name): @@ -499,15 +512,10 @@ def _encode_extended_key_usage(backend, extended_key_usage): res = backend._lib.sk_ASN1_OBJECT_push(eku, obj) backend.openssl_assert(res >= 1) - pp = backend._ffi.new('unsigned char **') - r = backend._lib.i2d_EXTENDED_KEY_USAGE( - backend._ffi.cast("EXTENDED_KEY_USAGE *", eku), pp - ) - backend.openssl_assert(r > 0) - pp = backend._ffi.gc( - pp, lambda pointer: backend._lib.OPENSSL_free(pointer[0]) + eku_ptr = backend._ffi.cast("EXTENDED_KEY_USAGE *", eku) + return _encode_extension_to_der( + backend, backend._lib.i2d_EXTENDED_KEY_USAGE, eku_ptr ) - return pp, r _CRLREASONFLAGS = { @@ -562,13 +570,9 @@ def _encode_crl_distribution_points(backend, crl_distribution_points): res = backend._lib.sk_DIST_POINT_push(cdp, dp) backend.openssl_assert(res >= 1) - pp = backend._ffi.new('unsigned char **') - r = backend._lib.i2d_CRL_DIST_POINTS(cdp, pp) - backend.openssl_assert(r > 0) - pp = backend._ffi.gc( - pp, lambda pointer: backend._lib.OPENSSL_free(pointer[0]) + return _encode_extension_to_der( + backend, backend._lib.i2d_CRL_DIST_POINTS, cdp ) - return pp, r def _encode_name_constraints(backend, name_constraints): @@ -584,13 +588,9 @@ def _encode_name_constraints(backend, name_constraints): ) nc.excludedSubtrees = excluded - pp = backend._ffi.new('unsigned char **') - r = backend._lib.Cryptography_i2d_NAME_CONSTRAINTS(nc, pp) - assert r > 0 - pp = backend._ffi.gc( - pp, lambda pointer: backend._lib.OPENSSL_free(pointer[0]) + return _encode_extension_to_der( + backend, backend._lib.Cryptography_i2d_NAME_CONSTRAINTS, nc ) - return pp, r def _encode_general_subtree(backend, subtrees): @@ -625,6 +625,21 @@ _EXTENSION_ENCODE_HANDLERS = { ExtensionOID.NAME_CONSTRAINTS: _encode_name_constraints, } +_CRL_EXTENSION_ENCODE_HANDLERS = { + ExtensionOID.ISSUER_ALTERNATIVE_NAME: _encode_alt_name, + ExtensionOID.AUTHORITY_KEY_IDENTIFIER: _encode_authority_key_identifier, + ExtensionOID.AUTHORITY_INFORMATION_ACCESS: ( + _encode_authority_information_access + ), + ExtensionOID.CRL_NUMBER: _encode_crl_number, +} + +_CRL_ENTRY_EXTENSION_ENCODE_HANDLERS = { + CRLEntryExtensionOID.CERTIFICATE_ISSUER: _encode_alt_name, + CRLEntryExtensionOID.CRL_REASON: _encode_crl_reason, + CRLEntryExtensionOID.INVALIDITY_DATE: _encode_invalidity_date, +} + class _PasswordUserdata(object): def __init__(self, password): @@ -899,17 +914,14 @@ class Backend(object): assert bn != self._ffi.NULL if six.PY3: # Python 3 has constant time from_bytes, so use that. - - bn_num_bytes = (self._lib.BN_num_bits(bn) + 7) // 8 + bn_num_bytes = self._lib.BN_num_bytes(bn) bin_ptr = self._ffi.new("unsigned char[]", bn_num_bytes) bin_len = self._lib.BN_bn2bin(bn, bin_ptr) # A zero length means the BN has value 0 self.openssl_assert(bin_len >= 0) return int.from_bytes(self._ffi.buffer(bin_ptr)[:bin_len], "big") - else: # Under Python 2 the best we can do is hex() - hex_cdata = self._lib.BN_bn2hex(bn) self.openssl_assert(hex_cdata != self._ffi.NULL) hex_str = self._ffi.string(hex_cdata) @@ -1312,30 +1324,21 @@ class Backend(object): self.openssl_assert(res == 1) # Add extensions. - extensions = self._lib.sk_X509_EXTENSION_new_null() - self.openssl_assert(extensions != self._ffi.NULL) - extensions = self._ffi.gc( - extensions, - self._lib.sk_X509_EXTENSION_free, + sk_extension = self._lib.sk_X509_EXTENSION_new_null() + self.openssl_assert(sk_extension != self._ffi.NULL) + sk_extension = self._ffi.gc( + sk_extension, self._lib.sk_X509_EXTENSION_free ) - for extension in builder._extensions: - try: - encode = _EXTENSION_ENCODE_HANDLERS[extension.oid] - except KeyError: - raise NotImplementedError('Extension not yet supported.') - - pp, r = encode(self, extension.value) - obj = _txt2obj_gc(self, extension.oid.dotted_string) - extension = self._lib.X509_EXTENSION_create_by_OBJ( - self._ffi.NULL, - obj, - 1 if extension.critical else 0, - _encode_asn1_str_gc(self, pp[0], r), - ) - self.openssl_assert(extension != self._ffi.NULL) - res = self._lib.sk_X509_EXTENSION_push(extensions, extension) - self.openssl_assert(res >= 1) - res = self._lib.X509_REQ_add_extensions(x509_req, extensions) + # gc is not necessary for CSRs, as sk_X509_EXTENSION_free + # will release all the X509_EXTENSIONs. + self._create_x509_extensions( + extensions=builder._extensions, + handlers=_EXTENSION_ENCODE_HANDLERS, + x509_obj=sk_extension, + add_func=self._lib.sk_X509_EXTENSION_insert, + gc=False + ) + res = self._lib.X509_REQ_add_extensions(x509_req, sk_extension) self.openssl_assert(res == 1) # Sign the request using the requester's private key. @@ -1416,24 +1419,13 @@ class Backend(object): self.openssl_assert(res != self._ffi.NULL) # Add extensions. - for i, extension in enumerate(builder._extensions): - try: - encode = _EXTENSION_ENCODE_HANDLERS[extension.oid] - except KeyError: - raise NotImplementedError('Extension not yet supported.') - - pp, r = encode(self, extension.value) - obj = _txt2obj_gc(self, extension.oid.dotted_string) - extension = self._lib.X509_EXTENSION_create_by_OBJ( - self._ffi.NULL, - obj, - 1 if extension.critical else 0, - _encode_asn1_str_gc(self, pp[0], r) - ) - self.openssl_assert(extension != self._ffi.NULL) - extension = self._ffi.gc(extension, self._lib.X509_EXTENSION_free) - res = self._lib.X509_add_ext(x509_cert, extension, i) - self.openssl_assert(res == 1) + self._create_x509_extensions( + extensions=builder._extensions, + handlers=_EXTENSION_ENCODE_HANDLERS, + x509_obj=x509_cert, + add_func=self._lib.X509_add_ext, + gc=True + ) # Set the issuer name. res = self._lib.X509_set_issuer_name( @@ -1455,6 +1447,147 @@ class Backend(object): return _Certificate(self, x509_cert) + def create_x509_crl(self, builder, private_key, algorithm): + if not isinstance(builder, x509.CertificateRevocationListBuilder): + raise TypeError('Builder type mismatch.') + if not isinstance(algorithm, hashes.HashAlgorithm): + raise TypeError('Algorithm must be a registered hash algorithm.') + + if self._lib.OPENSSL_VERSION_NUMBER <= 0x10001000: + if isinstance(private_key, _DSAPrivateKey): + raise NotImplementedError( + "CRL signatures aren't implemented for DSA" + " keys on OpenSSL versions less than 1.0.1." + ) + if isinstance(private_key, _EllipticCurvePrivateKey): + raise NotImplementedError( + "CRL signatures aren't implemented for EC" + " keys on OpenSSL versions less than 1.0.1." + ) + + evp_md = self._lib.EVP_get_digestbyname( + algorithm.name.encode('ascii') + ) + self.openssl_assert(evp_md != self._ffi.NULL) + + # Create an empty CRL. + x509_crl = self._lib.X509_CRL_new() + x509_crl = self._ffi.gc(x509_crl, backend._lib.X509_CRL_free) + + # Set the x509 CRL version. We only support v2 (integer value 1). + res = self._lib.X509_CRL_set_version(x509_crl, 1) + self.openssl_assert(res == 1) + + # Set the issuer name. + res = self._lib.X509_CRL_set_issuer_name( + x509_crl, _encode_name_gc(self, list(builder._issuer_name)) + ) + self.openssl_assert(res == 1) + + # Set the last update time. + last_update = self._lib.ASN1_TIME_set( + self._ffi.NULL, calendar.timegm(builder._last_update.timetuple()) + ) + self.openssl_assert(last_update != self._ffi.NULL) + last_update = self._ffi.gc(last_update, self._lib.ASN1_TIME_free) + res = self._lib.X509_CRL_set_lastUpdate(x509_crl, last_update) + self.openssl_assert(res == 1) + + # Set the next update time. + next_update = self._lib.ASN1_TIME_set( + self._ffi.NULL, calendar.timegm(builder._next_update.timetuple()) + ) + self.openssl_assert(next_update != self._ffi.NULL) + next_update = self._ffi.gc(next_update, self._lib.ASN1_TIME_free) + res = self._lib.X509_CRL_set_nextUpdate(x509_crl, next_update) + self.openssl_assert(res == 1) + + # Add extensions. + self._create_x509_extensions( + extensions=builder._extensions, + handlers=_CRL_EXTENSION_ENCODE_HANDLERS, + x509_obj=x509_crl, + add_func=self._lib.X509_CRL_add_ext, + gc=True + ) + + # add revoked certificates + for revoked_cert in builder._revoked_certificates: + # Duplicating because the X509_CRL takes ownership and will free + # this memory when X509_CRL_free is called. + revoked = self._lib.Cryptography_X509_REVOKED_dup( + revoked_cert._x509_revoked + ) + self.openssl_assert(revoked != self._ffi.NULL) + res = self._lib.X509_CRL_add0_revoked(x509_crl, revoked) + self.openssl_assert(res == 1) + + res = self._lib.X509_CRL_sign( + x509_crl, private_key._evp_pkey, evp_md + ) + if res == 0: + errors = self._consume_errors() + self.openssl_assert(errors[0][1] == self._lib.ERR_LIB_RSA) + self.openssl_assert( + errors[0][3] == self._lib.RSA_R_DIGEST_TOO_BIG_FOR_RSA_KEY + ) + raise ValueError("Digest too big for RSA key") + + return _CertificateRevocationList(self, x509_crl) + + def _create_x509_extensions(self, extensions, handlers, x509_obj, + add_func, gc): + for i, extension in enumerate(extensions): + try: + encode = handlers[extension.oid] + except KeyError: + raise NotImplementedError( + 'Extension not supported: {0}'.format(extension.oid) + ) + + pp, r = encode(self, extension.value) + obj = _txt2obj_gc(self, extension.oid.dotted_string) + x509_extension = self._lib.X509_EXTENSION_create_by_OBJ( + self._ffi.NULL, + obj, + 1 if extension.critical else 0, + _encode_asn1_str_gc(self, pp[0], r) + ) + self.openssl_assert(x509_extension != self._ffi.NULL) + if gc: + x509_extension = self._ffi.gc( + x509_extension, self._lib.X509_EXTENSION_free + ) + res = add_func(x509_obj, x509_extension, i) + self.openssl_assert(res >= 1) + + def create_x509_revoked_certificate(self, builder): + if not isinstance(builder, x509.RevokedCertificateBuilder): + raise TypeError('Builder type mismatch.') + + x509_revoked = self._lib.X509_REVOKED_new() + self.openssl_assert(x509_revoked != self._ffi.NULL) + x509_revoked = self._ffi.gc(x509_revoked, self._lib.X509_REVOKED_free) + serial_number = _encode_asn1_int_gc(self, builder._serial_number) + res = self._lib.X509_REVOKED_set_serialNumber( + x509_revoked, serial_number + ) + self.openssl_assert(res == 1) + res = self._lib.ASN1_TIME_set( + x509_revoked.revocationDate, + calendar.timegm(builder._revocation_date.timetuple()) + ) + self.openssl_assert(res != self._ffi.NULL) + # add CRL entry extensions + self._create_x509_extensions( + extensions=builder._extensions, + handlers=_CRL_ENTRY_EXTENSION_ENCODE_HANDLERS, + x509_obj=x509_revoked, + add_func=self._lib.X509_REVOKED_add_ext, + gc=True + ) + return _RevokedCertificate(self, None, x509_revoked) + def load_pem_private_key(self, data, password): return self._load_key( self._lib.PEM_read_bio_PrivateKey, @@ -2136,6 +2269,9 @@ class Backend(object): generalized_time = self._ffi.gc( generalized_time, self._lib.ASN1_GENERALIZEDTIME_free ) + return self._parse_asn1_generalized_time(generalized_time) + + def _parse_asn1_generalized_time(self, generalized_time): time = self._asn1_string_to_ascii( self._ffi.cast("ASN1_STRING *", generalized_time) ) diff --git a/src/cryptography/hazmat/backends/openssl/rsa.py b/src/cryptography/hazmat/backends/openssl/rsa.py index 664f6d35..033cd3b1 100644 --- a/src/cryptography/hazmat/backends/openssl/rsa.py +++ b/src/cryptography/hazmat/backends/openssl/rsa.py @@ -138,6 +138,7 @@ def _handle_rsa_enc_dec_error(backend, key): decoding_errors = [ backend._lib.RSA_R_BLOCK_TYPE_IS_NOT_01, backend._lib.RSA_R_BLOCK_TYPE_IS_NOT_02, + backend._lib.RSA_R_OAEP_DECODING_ERROR, ] if backend._lib.Cryptography_HAS_RSA_R_PKCS_DECODING_ERROR: decoding_errors.append(backend._lib.RSA_R_PKCS_DECODING_ERROR) diff --git a/src/cryptography/hazmat/backends/openssl/x509.py b/src/cryptography/hazmat/backends/openssl/x509.py index 7e89ac67..b8614e0b 100644 --- a/src/cryptography/hazmat/backends/openssl/x509.py +++ b/src/cryptography/hazmat/backends/openssl/x509.py @@ -4,8 +4,8 @@ from __future__ import absolute_import, division, print_function -import datetime import ipaddress +import operator from email.utils import parseaddr @@ -19,7 +19,7 @@ from cryptography import utils, x509 from cryptography.exceptions import UnsupportedAlgorithm from cryptography.hazmat.primitives import hashes, serialization from cryptography.x509.oid import ( - CRLExtensionOID, CertificatePoliciesOID, ExtensionOID + CRLEntryExtensionOID, CertificatePoliciesOID, ExtensionOID ) @@ -213,6 +213,15 @@ class _X509ExtensionParser(object): "Critical extension {0} is not currently supported" .format(oid), oid ) + else: + # Dump the DER payload into an UnrecognizedExtension object + data = backend._lib.X509_EXTENSION_get_data(ext) + backend.openssl_assert(data != backend._ffi.NULL) + der = backend._ffi.buffer(data.data, data.length)[:] + unrecognized = x509.UnrecognizedExtension(oid, der) + extensions.append( + x509.Extension(oid, critical, unrecognized) + ) else: # For extensions which are not supported by OpenSSL we pass the # extension object directly to the parsing routine so it can @@ -679,7 +688,19 @@ def _decode_inhibit_any_policy(backend, asn1_int): return x509.InhibitAnyPolicy(skip_certs) -_CRL_REASON_CODE_TO_ENUM = { +# CRLReason ::= ENUMERATED { +# unspecified (0), +# keyCompromise (1), +# cACompromise (2), +# affiliationChanged (3), +# superseded (4), +# cessationOfOperation (5), +# certificateHold (6), +# -- value 7 is not used +# removeFromCRL (8), +# privilegeWithdrawn (9), +# aACompromise (10) } +_CRL_ENTRY_REASON_CODE_TO_ENUM = { 0: x509.ReasonFlags.unspecified, 1: x509.ReasonFlags.key_compromise, 2: x509.ReasonFlags.ca_compromise, @@ -693,13 +714,27 @@ _CRL_REASON_CODE_TO_ENUM = { } +_CRL_ENTRY_REASON_ENUM_TO_CODE = { + x509.ReasonFlags.unspecified: 0, + x509.ReasonFlags.key_compromise: 1, + x509.ReasonFlags.ca_compromise: 2, + x509.ReasonFlags.affiliation_changed: 3, + x509.ReasonFlags.superseded: 4, + x509.ReasonFlags.cessation_of_operation: 5, + x509.ReasonFlags.certificate_hold: 6, + x509.ReasonFlags.remove_from_crl: 8, + x509.ReasonFlags.privilege_withdrawn: 9, + x509.ReasonFlags.aa_compromise: 10 +} + + def _decode_crl_reason(backend, enum): enum = backend._ffi.cast("ASN1_ENUMERATED *", enum) enum = backend._ffi.gc(enum, backend._lib.ASN1_ENUMERATED_free) code = backend._lib.ASN1_ENUMERATED_get(enum) try: - return _CRL_REASON_CODE_TO_ENUM[code] + return x509.CRLReason(_CRL_ENTRY_REASON_CODE_TO_ENUM[code]) except KeyError: raise ValueError("Unsupported reason code: {0}".format(code)) @@ -711,12 +746,9 @@ def _decode_invalidity_date(backend, inv_date): generalized_time = backend._ffi.gc( generalized_time, backend._lib.ASN1_GENERALIZEDTIME_free ) - time = backend._ffi.string( - backend._lib.ASN1_STRING_data( - backend._ffi.cast("ASN1_STRING *", generalized_time) - ) - ).decode("ascii") - return datetime.datetime.strptime(time, "%Y%m%d%H%M%SZ") + return x509.InvalidityDate( + backend._parse_asn1_generalized_time(generalized_time) + ) def _decode_cert_issuer(backend, ext): @@ -739,16 +771,24 @@ def _decode_cert_issuer(backend, ext): backend._consume_errors() raise ValueError( "The {0} extension is corrupted and can't be parsed".format( - CRLExtensionOID.CERTIFICATE_ISSUER)) + CRLEntryExtensionOID.CERTIFICATE_ISSUER)) gns = backend._ffi.gc(gns, backend._lib.GENERAL_NAMES_free) - return x509.GeneralNames(_decode_general_names(backend, gns)) + return x509.CertificateIssuer(_decode_general_names(backend, gns)) @utils.register_interface(x509.RevokedCertificate) class _RevokedCertificate(object): - def __init__(self, backend, x509_revoked): + def __init__(self, backend, crl, x509_revoked): self._backend = backend + # The X509_REVOKED_value is a X509_REVOKED * that has + # no reference counting. This means when X509_CRL_free is + # called then the CRL and all X509_REVOKED * are freed. Since + # you can retain a reference to a single revoked certificate + # and let the CRL fall out of scope we need to retain a + # private reference to the CRL inside the RevokedCertificate + # object to prevent the gc from being called inappropriately. + self._crl = crl self._x509_revoked = x509_revoked @property @@ -853,26 +893,34 @@ class _CertificateRevocationList(object): self._backend.openssl_assert(res == 1) return self._backend._read_mem_bio(bio) - def _revoked_certificates(self): + def _revoked_cert(self, idx): revoked = self._backend._lib.X509_CRL_get_REVOKED(self._x509_crl) - revoked_list = [] - if revoked != self._backend._ffi.NULL: - num = self._backend._lib.sk_X509_REVOKED_num(revoked) - for i in range(num): - r = self._backend._lib.sk_X509_REVOKED_value(revoked, i) - self._backend.openssl_assert(r != self._backend._ffi.NULL) - revoked_list.append(_RevokedCertificate(self._backend, r)) - - return revoked_list + r = self._backend._lib.sk_X509_REVOKED_value(revoked, idx) + self._backend.openssl_assert(r != self._backend._ffi.NULL) + return _RevokedCertificate(self._backend, self, r) def __iter__(self): - return iter(self._revoked_certificates()) + for i in range(len(self)): + yield self._revoked_cert(i) def __getitem__(self, idx): - return self._revoked_certificates()[idx] + if isinstance(idx, slice): + start, stop, step = idx.indices(len(self)) + return [self._revoked_cert(i) for i in range(start, stop, step)] + else: + idx = operator.index(idx) + if idx < 0: + idx += len(self) + if not 0 <= idx < len(self): + raise IndexError + return self._revoked_cert(idx) def __len__(self): - return len(self._revoked_certificates()) + revoked = self._backend._lib.X509_CRL_get_REVOKED(self._x509_crl) + if revoked == self._backend._ffi.NULL: + return 0 + else: + return self._backend._lib.sk_X509_REVOKED_num(revoked) @property def extensions(self): @@ -975,13 +1023,13 @@ _EXTENSION_HANDLERS = { } _REVOKED_EXTENSION_HANDLERS = { - CRLExtensionOID.CRL_REASON: _decode_crl_reason, - CRLExtensionOID.INVALIDITY_DATE: _decode_invalidity_date, - CRLExtensionOID.CERTIFICATE_ISSUER: _decode_cert_issuer, + CRLEntryExtensionOID.CRL_REASON: _decode_crl_reason, + CRLEntryExtensionOID.INVALIDITY_DATE: _decode_invalidity_date, + CRLEntryExtensionOID.CERTIFICATE_ISSUER: _decode_cert_issuer, } _REVOKED_UNSUPPORTED_EXTENSIONS = set([ - CRLExtensionOID.CERTIFICATE_ISSUER, + CRLEntryExtensionOID.CERTIFICATE_ISSUER, ]) _CRL_EXTENSION_HANDLERS = { diff --git a/src/cryptography/hazmat/bindings/openssl/binding.py b/src/cryptography/hazmat/bindings/openssl/binding.py index 07b6b9ac..8e419439 100644 --- a/src/cryptography/hazmat/bindings/openssl/binding.py +++ b/src/cryptography/hazmat/bindings/openssl/binding.py @@ -8,6 +8,7 @@ import collections import os import threading import types +import warnings from cryptography.exceptions import InternalError from cryptography.hazmat.bindings._openssl import ffi, lib @@ -180,3 +181,11 @@ class Binding(object): # condition registering the OpenSSL locks. On Python 3.4+ the import lock # is per module so this approach will not work. Binding.init_static_locks() + +if Binding.lib.SSLeay() < 0x10001000: + warnings.warn( + "OpenSSL versions less than 1.0.1 are no longer supported by the " + "OpenSSL project, please upgrade. A future version of cryptography " + "will drop support for these versions.", + DeprecationWarning + ) diff --git a/src/cryptography/utils.py b/src/cryptography/utils.py index 4449e85a..b85d50d3 100644 --- a/src/cryptography/utils.py +++ b/src/cryptography/utils.py @@ -12,7 +12,10 @@ import sys import warnings +# the functions deprecated in 1.0 are on an arbitrarily extended deprecation +# cycle and should not be removed until we agree on when that cycle ends. DeprecatedIn10 = DeprecationWarning +DeprecatedIn12 = PendingDeprecationWarning def read_only_property(name): diff --git a/src/cryptography/x509/__init__.py b/src/cryptography/x509/__init__.py index c4434fd1..a1deb7f4 100644 --- a/src/cryptography/x509/__init__.py +++ b/src/cryptography/x509/__init__.py @@ -6,20 +6,22 @@ from __future__ import absolute_import, division, print_function from cryptography.x509.base import ( Certificate, CertificateBuilder, CertificateRevocationList, + CertificateRevocationListBuilder, CertificateSigningRequest, CertificateSigningRequestBuilder, - InvalidVersion, RevokedCertificate, + InvalidVersion, RevokedCertificate, RevokedCertificateBuilder, Version, load_der_x509_certificate, load_der_x509_crl, load_der_x509_csr, load_pem_x509_certificate, load_pem_x509_crl, load_pem_x509_csr, ) from cryptography.x509.extensions import ( AccessDescription, AuthorityInformationAccess, AuthorityKeyIdentifier, BasicConstraints, CRLDistributionPoints, - CRLNumber, CertificatePolicies, DistributionPoint, DuplicateExtension, - ExtendedKeyUsage, Extension, ExtensionNotFound, ExtensionType, Extensions, - GeneralNames, InhibitAnyPolicy, IssuerAlternativeName, KeyUsage, + CRLNumber, CRLReason, CertificateIssuer, CertificatePolicies, + DistributionPoint, DuplicateExtension, ExtendedKeyUsage, Extension, + ExtensionNotFound, ExtensionType, Extensions, GeneralNames, + InhibitAnyPolicy, InvalidityDate, IssuerAlternativeName, KeyUsage, NameConstraints, NoticeReference, OCSPNoCheck, PolicyInformation, ReasonFlags, SubjectAlternativeName, SubjectKeyIdentifier, - UnsupportedExtension, UserNotice + UnrecognizedExtension, UnsupportedExtension, UserNotice ) from cryptography.x509.general_name import ( DNSName, DirectoryName, GeneralName, IPAddress, OtherName, RFC822Name, @@ -28,9 +30,9 @@ from cryptography.x509.general_name import ( ) from cryptography.x509.name import Name, NameAttribute from cryptography.x509.oid import ( - AuthorityInformationAccessOID, CRLExtensionOID, CertificatePoliciesOID, - ExtendedKeyUsageOID, ExtensionOID, NameOID, ObjectIdentifier, - SignatureAlgorithmOID, _SIG_OIDS_TO_HASH + AuthorityInformationAccessOID, CRLEntryExtensionOID, CRLExtensionOID, + CertificatePoliciesOID, ExtendedKeyUsageOID, ExtensionOID, NameOID, + ObjectIdentifier, SignatureAlgorithmOID, _SIG_OIDS_TO_HASH ) @@ -95,9 +97,9 @@ OID_ANY_POLICY = CertificatePoliciesOID.ANY_POLICY OID_CPS_QUALIFIER = CertificatePoliciesOID.CPS_QUALIFIER OID_CPS_USER_NOTICE = CertificatePoliciesOID.CPS_USER_NOTICE -OID_CERTIFICATE_ISSUER = CRLExtensionOID.CERTIFICATE_ISSUER -OID_CRL_REASON = CRLExtensionOID.CRL_REASON -OID_INVALIDITY_DATE = CRLExtensionOID.INVALIDITY_DATE +OID_CERTIFICATE_ISSUER = CRLEntryExtensionOID.CERTIFICATE_ISSUER +OID_CRL_REASON = CRLEntryExtensionOID.CRL_REASON +OID_INVALIDITY_DATE = CRLEntryExtensionOID.INVALIDITY_DATE OID_CA_ISSUERS = AuthorityInformationAccessOID.CA_ISSUERS OID_OCSP = AuthorityInformationAccessOID.OCSP @@ -152,8 +154,10 @@ __all__ = [ "OtherName", "Certificate", "CertificateRevocationList", + "CertificateRevocationListBuilder", "CertificateSigningRequest", "RevokedCertificate", + "RevokedCertificateBuilder", "CertificateSigningRequestBuilder", "CertificateBuilder", "Version", @@ -161,4 +165,9 @@ __all__ = [ "OID_CA_ISSUERS", "OID_OCSP", "_GENERAL_NAMES", + "CRLExtensionOID", + "CertificateIssuer", + "CRLReason", + "InvalidityDate", + "UnrecognizedExtension", ] diff --git a/src/cryptography/x509/base.py b/src/cryptography/x509/base.py index 057d0e9b..55e965f7 100644 --- a/src/cryptography/x509/base.py +++ b/src/cryptography/x509/base.py @@ -518,3 +518,159 @@ class CertificateBuilder(object): raise ValueError("A certificate must have a public key") return backend.create_x509_certificate(self, private_key, algorithm) + + +class CertificateRevocationListBuilder(object): + def __init__(self, issuer_name=None, last_update=None, next_update=None, + extensions=[], revoked_certificates=[]): + self._issuer_name = issuer_name + self._last_update = last_update + self._next_update = next_update + self._extensions = extensions + self._revoked_certificates = revoked_certificates + + def issuer_name(self, issuer_name): + if not isinstance(issuer_name, Name): + raise TypeError('Expecting x509.Name object.') + if self._issuer_name is not None: + raise ValueError('The issuer name may only be set once.') + return CertificateRevocationListBuilder( + issuer_name, self._last_update, self._next_update, + self._extensions, self._revoked_certificates + ) + + def last_update(self, last_update): + if not isinstance(last_update, datetime.datetime): + raise TypeError('Expecting datetime object.') + if self._last_update is not None: + raise ValueError('Last update may only be set once.') + if last_update <= _UNIX_EPOCH: + raise ValueError('The last update date must be after the unix' + ' epoch (1970 January 1).') + if self._next_update is not None and last_update > self._next_update: + raise ValueError( + 'The last update date must be before the next update date.' + ) + return CertificateRevocationListBuilder( + self._issuer_name, last_update, self._next_update, + self._extensions, self._revoked_certificates + ) + + def next_update(self, next_update): + if not isinstance(next_update, datetime.datetime): + raise TypeError('Expecting datetime object.') + if self._next_update is not None: + raise ValueError('Last update may only be set once.') + if next_update <= _UNIX_EPOCH: + raise ValueError('The last update date must be after the unix' + ' epoch (1970 January 1).') + if self._last_update is not None and next_update < self._last_update: + raise ValueError( + 'The next update date must be after the last update date.' + ) + return CertificateRevocationListBuilder( + self._issuer_name, self._last_update, next_update, + self._extensions, self._revoked_certificates + ) + + def add_extension(self, extension, critical): + """ + Adds an X.509 extension to the certificate revocation list. + """ + if not isinstance(extension, ExtensionType): + raise TypeError("extension must be an ExtensionType") + + extension = Extension(extension.oid, critical, extension) + + # TODO: This is quadratic in the number of extensions + for e in self._extensions: + if e.oid == extension.oid: + raise ValueError('This extension has already been set.') + return CertificateRevocationListBuilder( + self._issuer_name, self._last_update, self._next_update, + self._extensions + [extension], self._revoked_certificates + ) + + def add_revoked_certificate(self, revoked_certificate): + """ + Adds a revoked certificate to the CRL. + """ + if not isinstance(revoked_certificate, RevokedCertificate): + raise TypeError("Must be an instance of RevokedCertificate") + + return CertificateRevocationListBuilder( + self._issuer_name, self._last_update, + self._next_update, self._extensions, + self._revoked_certificates + [revoked_certificate] + ) + + def sign(self, private_key, algorithm, backend): + if self._issuer_name is None: + raise ValueError("A CRL must have an issuer name") + + if self._last_update is None: + raise ValueError("A CRL must have a last update time") + + if self._next_update is None: + raise ValueError("A CRL must have a next update time") + + return backend.create_x509_crl(self, private_key, algorithm) + + +class RevokedCertificateBuilder(object): + def __init__(self, serial_number=None, revocation_date=None, + extensions=[]): + self._serial_number = serial_number + self._revocation_date = revocation_date + self._extensions = extensions + + def serial_number(self, number): + if not isinstance(number, six.integer_types): + raise TypeError('Serial number must be of integral type.') + if self._serial_number is not None: + raise ValueError('The serial number may only be set once.') + if number < 0: + raise ValueError('The serial number should be non-negative.') + if utils.bit_length(number) > 160: # As defined in RFC 5280 + raise ValueError('The serial number should not be more than 160 ' + 'bits.') + return RevokedCertificateBuilder( + number, self._revocation_date, self._extensions + ) + + def revocation_date(self, time): + if not isinstance(time, datetime.datetime): + raise TypeError('Expecting datetime object.') + if self._revocation_date is not None: + raise ValueError('The revocation date may only be set once.') + if time <= _UNIX_EPOCH: + raise ValueError('The revocation date must be after the unix' + ' epoch (1970 January 1).') + return RevokedCertificateBuilder( + self._serial_number, time, self._extensions + ) + + def add_extension(self, extension, critical): + if not isinstance(extension, ExtensionType): + raise TypeError("extension must be an ExtensionType") + + extension = Extension(extension.oid, critical, extension) + + # TODO: This is quadratic in the number of extensions + for e in self._extensions: + if e.oid == extension.oid: + raise ValueError('This extension has already been set.') + return RevokedCertificateBuilder( + self._serial_number, self._revocation_date, + self._extensions + [extension] + ) + + def build(self, backend): + if self._serial_number is None: + raise ValueError("A revoked certificate must have a serial number") + if self._revocation_date is None: + raise ValueError( + "A revoked certificate must have a revocation date" + ) + + return backend.create_x509_revoked_certificate(self) diff --git a/src/cryptography/x509/extensions.py b/src/cryptography/x509/extensions.py index 15feb717..f7b5d7f5 100644 --- a/src/cryptography/x509/extensions.py +++ b/src/cryptography/x509/extensions.py @@ -5,6 +5,7 @@ from __future__ import absolute_import, division, print_function import abc +import datetime import hashlib import ipaddress from enum import Enum @@ -18,7 +19,9 @@ from cryptography import utils from cryptography.hazmat.primitives import constant_time, serialization from cryptography.x509.general_name import GeneralName, IPAddress, OtherName from cryptography.x509.name import Name -from cryptography.x509.oid import ExtensionOID, ObjectIdentifier +from cryptography.x509.oid import ( + CRLEntryExtensionOID, ExtensionOID, ObjectIdentifier +) class _SubjectPublicKeyInfo(univ.Sequence): @@ -88,6 +91,13 @@ class Extensions(object): raise ExtensionNotFound("No {0} extension was found".format(oid), oid) def get_extension_for_class(self, extclass): + if extclass is UnrecognizedExtension: + raise TypeError( + "UnrecognizedExtension can't be used with " + "get_extension_for_class because more than one instance of the" + " class may be present." + ) + for ext in self: if isinstance(ext.value, extclass): return ext @@ -102,6 +112,9 @@ class Extensions(object): def __len__(self): return len(self._extensions) + def __getitem__(self, idx): + return self._extensions[idx] + def __repr__(self): return ( "<Extensions({0})>".format(self._extensions) @@ -127,6 +140,9 @@ class CRLNumber(object): def __ne__(self, other): return not self == other + def __hash__(self): + return hash(self.crl_number) + def __repr__(self): return "<CRLNumber({0})>".format(self.crl_number) @@ -226,6 +242,9 @@ class SubjectKeyIdentifier(object): def __ne__(self, other): return not self == other + def __hash__(self): + return hash(self.digest) + @utils.register_interface(ExtensionType) class AuthorityInformationAccess(object): @@ -258,6 +277,9 @@ class AuthorityInformationAccess(object): def __ne__(self, other): return not self == other + def __getitem__(self, idx): + return self._descriptions[idx] + class AccessDescription(object): def __init__(self, access_method, access_location): @@ -330,6 +352,9 @@ class BasicConstraints(object): def __ne__(self, other): return not self == other + def __hash__(self): + return hash((self.ca, self.path_length)) + @utils.register_interface(ExtensionType) class CRLDistributionPoints(object): @@ -364,6 +389,9 @@ class CRLDistributionPoints(object): def __ne__(self, other): return not self == other + def __getitem__(self, idx): + return self._distribution_points[idx] + class DistributionPoint(object): def __init__(self, full_name, relative_name, reasons, crl_issuer): @@ -486,6 +514,9 @@ class CertificatePolicies(object): def __ne__(self, other): return not self == other + def __getitem__(self, idx): + return self._policies[idx] + class PolicyInformation(object): def __init__(self, policy_identifier, policy_qualifiers): @@ -885,6 +916,9 @@ class GeneralNames(object): def __ne__(self, other): return not self == other + def __getitem__(self, idx): + return self._general_names[idx] + @utils.register_interface(ExtensionType) class SubjectAlternativeName(object): @@ -911,6 +945,9 @@ class SubjectAlternativeName(object): return self._general_names == other._general_names + def __getitem__(self, idx): + return self._general_names[idx] + def __ne__(self, other): return not self == other @@ -942,3 +979,127 @@ class IssuerAlternativeName(object): def __ne__(self, other): return not self == other + + def __getitem__(self, idx): + return self._general_names[idx] + + +@utils.register_interface(ExtensionType) +class CertificateIssuer(object): + oid = CRLEntryExtensionOID.CERTIFICATE_ISSUER + + def __init__(self, general_names): + self._general_names = GeneralNames(general_names) + + def __iter__(self): + return iter(self._general_names) + + def __len__(self): + return len(self._general_names) + + def get_values_for_type(self, type): + return self._general_names.get_values_for_type(type) + + def __repr__(self): + return "<CertificateIssuer({0})>".format(self._general_names) + + def __eq__(self, other): + if not isinstance(other, CertificateIssuer): + return NotImplemented + + return self._general_names == other._general_names + + def __ne__(self, other): + return not self == other + + def __getitem__(self, idx): + return self._general_names[idx] + + +@utils.register_interface(ExtensionType) +class CRLReason(object): + oid = CRLEntryExtensionOID.CRL_REASON + + def __init__(self, reason): + if not isinstance(reason, ReasonFlags): + raise TypeError("reason must be an element from ReasonFlags") + + self._reason = reason + + def __repr__(self): + return "<CRLReason(reason={0})>".format(self._reason) + + def __eq__(self, other): + if not isinstance(other, CRLReason): + return NotImplemented + + return self.reason == other.reason + + def __ne__(self, other): + return not self == other + + def __hash__(self): + return hash(self.reason) + + reason = utils.read_only_property("_reason") + + +@utils.register_interface(ExtensionType) +class InvalidityDate(object): + oid = CRLEntryExtensionOID.INVALIDITY_DATE + + def __init__(self, invalidity_date): + if not isinstance(invalidity_date, datetime.datetime): + raise TypeError("invalidity_date must be a datetime.datetime") + + self._invalidity_date = invalidity_date + + def __repr__(self): + return "<InvalidityDate(invalidity_date={0})>".format( + self._invalidity_date + ) + + def __eq__(self, other): + if not isinstance(other, InvalidityDate): + return NotImplemented + + return self.invalidity_date == other.invalidity_date + + def __ne__(self, other): + return not self == other + + def __hash__(self): + return hash(self.invalidity_date) + + invalidity_date = utils.read_only_property("_invalidity_date") + + +@utils.register_interface(ExtensionType) +class UnrecognizedExtension(object): + def __init__(self, oid, value): + if not isinstance(oid, ObjectIdentifier): + raise TypeError("oid must be an ObjectIdentifier") + self._oid = oid + self._value = value + + oid = utils.read_only_property("_oid") + value = utils.read_only_property("_value") + + def __repr__(self): + return ( + "<UnrecognizedExtension(oid={0.oid}, value={0.value!r})>".format( + self + ) + ) + + def __eq__(self, other): + if not isinstance(other, UnrecognizedExtension): + return NotImplemented + + return self.oid == other.oid and self.value == other.value + + def __ne__(self, other): + return not self == other + + def __hash__(self): + return hash((self.oid, self.value)) diff --git a/src/cryptography/x509/oid.py b/src/cryptography/x509/oid.py index 94295d66..ced15103 100644 --- a/src/cryptography/x509/oid.py +++ b/src/cryptography/x509/oid.py @@ -88,12 +88,20 @@ class ExtensionOID(object): CRL_NUMBER = ObjectIdentifier("2.5.29.20") -class CRLExtensionOID(object): +class CRLEntryExtensionOID(object): CERTIFICATE_ISSUER = ObjectIdentifier("2.5.29.29") CRL_REASON = ObjectIdentifier("2.5.29.21") INVALIDITY_DATE = ObjectIdentifier("2.5.29.24") +CRLExtensionOID = utils.deprecated( + CRLEntryExtensionOID, + __name__, + "CRLExtensionOID has been renamed to CRLEntryExtensionOID", + utils.DeprecatedIn12 +) + + class NameOID(object): COMMON_NAME = ObjectIdentifier("2.5.4.3") COUNTRY_NAME = ObjectIdentifier("2.5.4.6") @@ -220,9 +228,9 @@ _OID_NAMES = { ExtensionOID.SUBJECT_ALTERNATIVE_NAME: "subjectAltName", ExtensionOID.ISSUER_ALTERNATIVE_NAME: "issuerAltName", ExtensionOID.BASIC_CONSTRAINTS: "basicConstraints", - CRLExtensionOID.CRL_REASON: "cRLReason", - CRLExtensionOID.INVALIDITY_DATE: "invalidityDate", - CRLExtensionOID.CERTIFICATE_ISSUER: "certificateIssuer", + CRLEntryExtensionOID.CRL_REASON: "cRLReason", + CRLEntryExtensionOID.INVALIDITY_DATE: "invalidityDate", + CRLEntryExtensionOID.CERTIFICATE_ISSUER: "certificateIssuer", ExtensionOID.NAME_CONSTRAINTS: "nameConstraints", ExtensionOID.CRL_DISTRIBUTION_POINTS: "cRLDistributionPoints", ExtensionOID.CERTIFICATE_POLICIES: "certificatePolicies", diff --git a/tests/hazmat/backends/test_multibackend.py b/tests/hazmat/backends/test_multibackend.py index 81a64ce0..74835716 100644 --- a/tests/hazmat/backends/test_multibackend.py +++ b/tests/hazmat/backends/test_multibackend.py @@ -218,6 +218,12 @@ class DummyX509Backend(object): def create_x509_certificate(self, builder, private_key, algorithm): pass + def create_x509_crl(self, builder, private_key, algorithm): + pass + + def create_x509_revoked_certificate(self, builder): + pass + class TestMultiBackend(object): def test_ciphers(self): @@ -514,6 +520,8 @@ class TestMultiBackend(object): backend.load_der_x509_csr(b"reqdata") backend.create_x509_csr(object(), b"privatekey", hashes.SHA1()) backend.create_x509_certificate(object(), b"privatekey", hashes.SHA1()) + backend.create_x509_crl(object(), b"privatekey", hashes.SHA1()) + backend.create_x509_revoked_certificate(object()) backend = MultiBackend([]) with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_X509): @@ -534,3 +542,9 @@ class TestMultiBackend(object): backend.create_x509_certificate( object(), b"privatekey", hashes.SHA1() ) + with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_X509): + backend.create_x509_crl( + object(), b"privatekey", hashes.SHA1() + ) + with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_X509): + backend.create_x509_revoked_certificate(object()) diff --git a/tests/hazmat/backends/test_openssl.py b/tests/hazmat/backends/test_openssl.py index d048fe68..ad2daf7d 100644 --- a/tests/hazmat/backends/test_openssl.py +++ b/tests/hazmat/backends/test_openssl.py @@ -4,6 +4,7 @@ from __future__ import absolute_import, division, print_function +import datetime import os import subprocess import sys @@ -13,7 +14,7 @@ import pretend import pytest -from cryptography import utils +from cryptography import utils, x509 from cryptography.exceptions import InternalError, _Reasons from cryptography.hazmat.backends.interfaces import RSABackend from cryptography.hazmat.backends.openssl.backend import ( @@ -500,8 +501,108 @@ class TestOpenSSLSignX509Certificate(object): with pytest.raises(TypeError): backend.create_x509_certificate(object(), private_key, DummyHash()) + @pytest.mark.skipif( + backend._lib.OPENSSL_VERSION_NUMBER >= 0x10001000, + reason="Requires an older OpenSSL. Must be < 1.0.1" + ) + def test_sign_with_dsa_private_key_is_unsupported(self): + private_key = DSA_KEY_2048.private_key(backend) + builder = x509.CertificateBuilder() + builder = builder.subject_name( + x509.Name([x509.NameAttribute(x509.NameOID.COUNTRY_NAME, u'US')]) + ).issuer_name( + x509.Name([x509.NameAttribute(x509.NameOID.COUNTRY_NAME, u'US')]) + ).serial_number( + 1 + ).public_key( + private_key.public_key() + ).not_valid_before( + datetime.datetime(2002, 1, 1, 12, 1) + ).not_valid_after( + datetime.datetime(2032, 1, 1, 12, 1) + ) + + with pytest.raises(NotImplementedError): + builder.sign(private_key, hashes.SHA512(), backend) + + @pytest.mark.skipif( + backend._lib.OPENSSL_VERSION_NUMBER >= 0x10001000, + reason="Requires an older OpenSSL. Must be < 1.0.1" + ) + def test_sign_with_ec_private_key_is_unsupported(self): + _skip_curve_unsupported(backend, ec.SECP256R1()) + private_key = ec.generate_private_key(ec.SECP256R1(), backend) + builder = x509.CertificateBuilder() + builder = builder.subject_name( + x509.Name([x509.NameAttribute(x509.NameOID.COUNTRY_NAME, u'US')]) + ).issuer_name( + x509.Name([x509.NameAttribute(x509.NameOID.COUNTRY_NAME, u'US')]) + ).serial_number( + 1 + ).public_key( + private_key.public_key() + ).not_valid_before( + datetime.datetime(2002, 1, 1, 12, 1) + ).not_valid_after( + datetime.datetime(2032, 1, 1, 12, 1) + ) + + with pytest.raises(NotImplementedError): + builder.sign(private_key, hashes.SHA512(), backend) + + +class TestOpenSSLSignX509CertificateRevocationList(object): + def test_invalid_builder(self): + private_key = RSA_KEY_2048.private_key(backend) + + with pytest.raises(TypeError): + backend.create_x509_crl(object(), private_key, hashes.SHA256()) + + @pytest.mark.skipif( + backend._lib.OPENSSL_VERSION_NUMBER >= 0x10001000, + reason="Requires an older OpenSSL. Must be < 1.0.1" + ) + def test_sign_with_dsa_private_key_is_unsupported(self): + private_key = DSA_KEY_2048.private_key(backend) + builder = x509.CertificateRevocationListBuilder() + builder = builder.issuer_name( + x509.Name([x509.NameAttribute(x509.NameOID.COUNTRY_NAME, u'US')]) + ).last_update( + datetime.datetime(2002, 1, 1, 12, 1) + ).next_update( + datetime.datetime(2032, 1, 1, 12, 1) + ) + + with pytest.raises(NotImplementedError): + builder.sign(private_key, hashes.SHA1(), backend) + + @pytest.mark.skipif( + backend._lib.OPENSSL_VERSION_NUMBER >= 0x10001000, + reason="Requires an older OpenSSL. Must be < 1.0.1" + ) + def test_sign_with_ec_private_key_is_unsupported(self): + _skip_curve_unsupported(backend, ec.SECP256R1()) + private_key = ec.generate_private_key(ec.SECP256R1(), backend) + builder = x509.CertificateRevocationListBuilder() + builder = builder.issuer_name( + x509.Name([x509.NameAttribute(x509.NameOID.COUNTRY_NAME, u'US')]) + ).last_update( + datetime.datetime(2002, 1, 1, 12, 1) + ).next_update( + datetime.datetime(2032, 1, 1, 12, 1) + ) + + with pytest.raises(NotImplementedError): + builder.sign(private_key, hashes.SHA512(), backend) + + +class TestOpenSSLCreateRevokedCertificate(object): + def test_invalid_builder(self): + with pytest.raises(TypeError): + backend.create_x509_revoked_certificate(object()) + -class TestOpenSSLSerialisationWithOpenSSL(object): +class TestOpenSSLSerializationWithOpenSSL(object): def test_pem_password_cb_buffer_too_small(self): ffi_cb, userdata = backend._pem_password_cb(b"aa") handle = backend._ffi.new_handle(userdata) diff --git a/tests/hazmat/primitives/test_rsa.py b/tests/hazmat/primitives/test_rsa.py index 0b83fd65..b6213d6d 100644 --- a/tests/hazmat/primitives/test_rsa.py +++ b/tests/hazmat/primitives/test_rsa.py @@ -1194,6 +1194,43 @@ class TestRSADecryption(object): ) assert message == binascii.unhexlify(example["message"]) + @pytest.mark.supported( + only_if=lambda backend: backend.rsa_padding_supported( + padding.OAEP( + mgf=padding.MGF1(algorithm=hashes.SHA1()), + algorithm=hashes.SHA1(), + label=None + ) + ), + skip_message="Does not support OAEP." + ) + def test_invalid_oaep_decryption(self, backend): + # More recent versions of OpenSSL may raise RSA_R_OAEP_DECODING_ERROR + # This test triggers it and confirms that we properly handle it. Other + # backends should also return the proper ValueError. + private_key = RSA_KEY_512.private_key(backend) + + ciphertext = private_key.public_key().encrypt( + b'secure data', + padding.OAEP( + mgf=padding.MGF1(algorithm=hashes.SHA1()), + algorithm=hashes.SHA1(), + label=None + ) + ) + + private_key_alt = RSA_KEY_512_ALT.private_key(backend) + + with pytest.raises(ValueError): + private_key_alt.decrypt( + ciphertext, + padding.OAEP( + mgf=padding.MGF1(algorithm=hashes.SHA1()), + algorithm=hashes.SHA1(), + label=None + ) + ) + def test_unsupported_oaep_mgf(self, backend): private_key = RSA_KEY_512.private_key(backend) with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_MGF): diff --git a/tests/test_x509.py b/tests/test_x509.py index ae2746e3..6145edb1 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -173,6 +173,20 @@ class TestCertificateRevocationList(object): # Check that len() works for CRLs. assert len(crl) == 12 + def test_revoked_cert_retrieval_retain_only_revoked(self, backend): + """ + This test attempts to trigger the crash condition described in + https://github.com/pyca/cryptography/issues/2557 + PyPy does gc at its own pace, so it will only be reliable on CPython. + """ + revoked = _load_cert( + os.path.join("x509", "custom", "crl_all_reasons.pem"), + x509.load_pem_x509_crl, + backend + )[11] + assert revoked.revocation_date == datetime.datetime(2015, 1, 1, 0, 0) + assert revoked.serial_number == 11 + def test_extensions(self, backend): crl = _load_cert( os.path.join("x509", "custom", "crl_ian_aia_aki.pem"), @@ -318,7 +332,6 @@ class TestCertificateRevocationList(object): @pytest.mark.requires_backend_interface(interface=X509Backend) class TestRevokedCertificate(object): - def test_revoked_basics(self, backend): crl = _load_cert( os.path.join("x509", "custom", "crl_all_reasons.pem"), @@ -342,12 +355,12 @@ class TestRevokedCertificate(object): backend ) - exp_issuer = x509.GeneralNames([ + exp_issuer = [ x509.DirectoryName(x509.Name([ x509.NameAttribute(x509.OID_COUNTRY_NAME, u"US"), x509.NameAttribute(x509.OID_COMMON_NAME, u"cryptography.io"), ])) - ]) + ] # First revoked cert doesn't have extensions, test if it is handled # correctly. @@ -366,29 +379,28 @@ class TestRevokedCertificate(object): rev1 = crl[1] assert isinstance(rev1.extensions, x509.Extensions) - reason = rev1.extensions.get_extension_for_oid( - x509.OID_CRL_REASON).value - assert reason == x509.ReasonFlags.unspecified + reason = rev1.extensions.get_extension_for_class( + x509.CRLReason).value + assert reason == x509.CRLReason(x509.ReasonFlags.unspecified) - issuer = rev1.extensions.get_extension_for_oid( - x509.OID_CERTIFICATE_ISSUER).value - assert issuer == exp_issuer + issuer = rev1.extensions.get_extension_for_class( + x509.CertificateIssuer).value + assert issuer == x509.CertificateIssuer(exp_issuer) - date = rev1.extensions.get_extension_for_oid( - x509.OID_INVALIDITY_DATE).value - assert isinstance(date, datetime.datetime) - assert date.isoformat() == "2015-01-01T00:00:00" + date = rev1.extensions.get_extension_for_class( + x509.InvalidityDate).value + assert date == x509.InvalidityDate(datetime.datetime(2015, 1, 1, 0, 0)) # Check if all reason flags can be found in the CRL. flags = set(x509.ReasonFlags) for rev in crl: try: - r = rev.extensions.get_extension_for_oid(x509.OID_CRL_REASON) + r = rev.extensions.get_extension_for_class(x509.CRLReason) except x509.ExtensionNotFound: # Not all revoked certs have a reason extension. pass else: - flags.discard(r.value) + flags.discard(r.value.reason) assert len(flags) == 0 @@ -446,6 +458,23 @@ class TestRevokedCertificate(object): with pytest.raises(ValueError): crl[0].extensions + def test_indexing(self, backend): + crl = _load_cert( + os.path.join("x509", "custom", "crl_all_reasons.pem"), + x509.load_pem_x509_crl, + backend + ) + + with pytest.raises(IndexError): + crl[-13] + with pytest.raises(IndexError): + crl[12] + + assert crl[-1].serial_number == crl[11].serial_number + assert len(crl[2:4]) == 2 + assert crl[2:4][0].serial_number == crl[2].serial_number + assert crl[2:4][1].serial_number == crl[3].serial_number + @pytest.mark.requires_backend_interface(interface=RSABackend) @pytest.mark.requires_backend_interface(interface=X509Backend) @@ -1064,7 +1093,11 @@ class TestRSACertificateRequest(object): backend ) extensions = request.extensions - assert len(extensions) == 0 + assert len(extensions) == 1 + assert extensions[0].oid == x509.ObjectIdentifier("1.2.3.4") + assert extensions[0].value == x509.UnrecognizedExtension( + x509.ObjectIdentifier("1.2.3.4"), b"value" + ) def test_request_basic_constraints(self, backend): request = _load_cert( @@ -1710,57 +1743,6 @@ class TestCertificateBuilder(object): with pytest.raises(TypeError): builder.sign(private_key, object(), backend) - @pytest.mark.requires_backend_interface(interface=DSABackend) - @pytest.mark.requires_backend_interface(interface=X509Backend) - def test_sign_with_dsa_private_key_is_unsupported(self, backend): - if backend._lib.OPENSSL_VERSION_NUMBER >= 0x10001000: - pytest.skip("Requires an older OpenSSL. Must be < 1.0.1") - - private_key = DSA_KEY_2048.private_key(backend) - builder = x509.CertificateBuilder() - builder = builder.subject_name( - x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, u'US')]) - ).issuer_name( - x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, u'US')]) - ).serial_number( - 1 - ).public_key( - private_key.public_key() - ).not_valid_before( - datetime.datetime(2002, 1, 1, 12, 1) - ).not_valid_after( - datetime.datetime(2032, 1, 1, 12, 1) - ) - - with pytest.raises(NotImplementedError): - builder.sign(private_key, hashes.SHA512(), backend) - - @pytest.mark.requires_backend_interface(interface=EllipticCurveBackend) - @pytest.mark.requires_backend_interface(interface=X509Backend) - def test_sign_with_ec_private_key_is_unsupported(self, backend): - if backend._lib.OPENSSL_VERSION_NUMBER >= 0x10001000: - pytest.skip("Requires an older OpenSSL. Must be < 1.0.1") - - _skip_curve_unsupported(backend, ec.SECP256R1()) - private_key = ec.generate_private_key(ec.SECP256R1(), backend) - builder = x509.CertificateBuilder() - builder = builder.subject_name( - x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, u'US')]) - ).issuer_name( - x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, u'US')]) - ).serial_number( - 1 - ).public_key( - private_key.public_key() - ).not_valid_before( - datetime.datetime(2002, 1, 1, 12, 1) - ).not_valid_after( - datetime.datetime(2032, 1, 1, 12, 1) - ) - - with pytest.raises(NotImplementedError): - builder.sign(private_key, hashes.SHA512(), backend) - @pytest.mark.parametrize( "cdp", [ diff --git a/tests/test_x509_crlbuilder.py b/tests/test_x509_crlbuilder.py new file mode 100644 index 00000000..32a07487 --- /dev/null +++ b/tests/test_x509_crlbuilder.py @@ -0,0 +1,449 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import absolute_import, division, print_function + +import datetime + +import pytest + +from cryptography import x509 +from cryptography.hazmat.backends.interfaces import ( + DSABackend, EllipticCurveBackend, RSABackend, X509Backend +) +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.x509.oid import AuthorityInformationAccessOID, NameOID + +from .hazmat.primitives.fixtures_dsa import DSA_KEY_2048 +from .hazmat.primitives.fixtures_rsa import RSA_KEY_2048, RSA_KEY_512 +from .hazmat.primitives.test_ec import _skip_curve_unsupported + + +class TestCertificateRevocationListBuilder(object): + def test_issuer_name_invalid(self): + builder = x509.CertificateRevocationListBuilder() + with pytest.raises(TypeError): + builder.issuer_name("notanx509name") + + def test_set_issuer_name_twice(self): + builder = x509.CertificateRevocationListBuilder().issuer_name( + x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, u'US')]) + ) + with pytest.raises(ValueError): + builder.issuer_name( + x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, u'US')]) + ) + + def test_last_update_invalid(self): + builder = x509.CertificateRevocationListBuilder() + with pytest.raises(TypeError): + builder.last_update("notadatetime") + + def test_last_update_before_unix_epoch(self): + builder = x509.CertificateRevocationListBuilder() + with pytest.raises(ValueError): + builder.last_update(datetime.datetime(1960, 8, 10)) + + def test_set_last_update_twice(self): + builder = x509.CertificateRevocationListBuilder().last_update( + datetime.datetime(2002, 1, 1, 12, 1) + ) + with pytest.raises(ValueError): + builder.last_update(datetime.datetime(2002, 1, 1, 12, 1)) + + def test_next_update_invalid(self): + builder = x509.CertificateRevocationListBuilder() + with pytest.raises(TypeError): + builder.next_update("notadatetime") + + def test_next_update_before_unix_epoch(self): + builder = x509.CertificateRevocationListBuilder() + with pytest.raises(ValueError): + builder.next_update(datetime.datetime(1960, 8, 10)) + + def test_set_next_update_twice(self): + builder = x509.CertificateRevocationListBuilder().next_update( + datetime.datetime(2002, 1, 1, 12, 1) + ) + with pytest.raises(ValueError): + builder.next_update(datetime.datetime(2002, 1, 1, 12, 1)) + + def test_last_update_after_next_update(self): + builder = x509.CertificateRevocationListBuilder() + + builder = builder.next_update( + datetime.datetime(2002, 1, 1, 12, 1) + ) + with pytest.raises(ValueError): + builder.last_update(datetime.datetime(2003, 1, 1, 12, 1)) + + def test_next_update_after_last_update(self): + builder = x509.CertificateRevocationListBuilder() + + builder = builder.last_update( + datetime.datetime(2002, 1, 1, 12, 1) + ) + with pytest.raises(ValueError): + builder.next_update(datetime.datetime(2001, 1, 1, 12, 1)) + + def test_add_extension_checks_for_duplicates(self): + builder = x509.CertificateRevocationListBuilder().add_extension( + x509.CRLNumber(1), False + ) + + with pytest.raises(ValueError): + builder.add_extension(x509.CRLNumber(2), False) + + def test_add_invalid_extension(self): + builder = x509.CertificateRevocationListBuilder() + + with pytest.raises(TypeError): + builder.add_extension( + object(), False + ) + + def test_add_invalid_revoked_certificate(self): + builder = x509.CertificateRevocationListBuilder() + + with pytest.raises(TypeError): + builder.add_revoked_certificate(object()) + + @pytest.mark.requires_backend_interface(interface=RSABackend) + @pytest.mark.requires_backend_interface(interface=X509Backend) + def test_no_issuer_name(self, backend): + private_key = RSA_KEY_2048.private_key(backend) + builder = x509.CertificateRevocationListBuilder().last_update( + datetime.datetime(2002, 1, 1, 12, 1) + ).next_update( + datetime.datetime(2030, 1, 1, 12, 1) + ) + + with pytest.raises(ValueError): + builder.sign(private_key, hashes.SHA256(), backend) + + @pytest.mark.requires_backend_interface(interface=RSABackend) + @pytest.mark.requires_backend_interface(interface=X509Backend) + def test_no_last_update(self, backend): + private_key = RSA_KEY_2048.private_key(backend) + builder = x509.CertificateRevocationListBuilder().issuer_name( + x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, u'US')]) + ).next_update( + datetime.datetime(2030, 1, 1, 12, 1) + ) + + with pytest.raises(ValueError): + builder.sign(private_key, hashes.SHA256(), backend) + + @pytest.mark.requires_backend_interface(interface=RSABackend) + @pytest.mark.requires_backend_interface(interface=X509Backend) + def test_no_next_update(self, backend): + private_key = RSA_KEY_2048.private_key(backend) + builder = x509.CertificateRevocationListBuilder().issuer_name( + x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, u'US')]) + ).last_update( + datetime.datetime(2030, 1, 1, 12, 1) + ) + + with pytest.raises(ValueError): + builder.sign(private_key, hashes.SHA256(), backend) + + @pytest.mark.requires_backend_interface(interface=RSABackend) + @pytest.mark.requires_backend_interface(interface=X509Backend) + def test_sign_empty_list(self, backend): + private_key = RSA_KEY_2048.private_key(backend) + last_update = datetime.datetime(2002, 1, 1, 12, 1) + next_update = datetime.datetime(2030, 1, 1, 12, 1) + builder = x509.CertificateRevocationListBuilder().issuer_name( + x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, u"cryptography.io CA") + ]) + ).last_update(last_update).next_update(next_update) + + crl = builder.sign(private_key, hashes.SHA256(), backend) + assert len(crl) == 0 + assert crl.last_update == last_update + assert crl.next_update == next_update + + @pytest.mark.parametrize( + "extension", + [ + x509.CRLNumber(13), + x509.AuthorityKeyIdentifier( + b"\xc3\x9c\xf3\xfc\xd3F\x084\xbb\xceF\x7f\xa0|[\xf3\xe2\x08" + b"\xcbY", + None, + None + ), + x509.AuthorityInformationAccess([ + x509.AccessDescription( + AuthorityInformationAccessOID.CA_ISSUERS, + x509.DNSName(u"cryptography.io") + ) + ]), + x509.IssuerAlternativeName([ + x509.UniformResourceIdentifier(u"https://cryptography.io"), + ]) + ] + ) + @pytest.mark.requires_backend_interface(interface=RSABackend) + @pytest.mark.requires_backend_interface(interface=X509Backend) + def test_sign_extensions(self, backend, extension): + private_key = RSA_KEY_2048.private_key(backend) + last_update = datetime.datetime(2002, 1, 1, 12, 1) + next_update = datetime.datetime(2030, 1, 1, 12, 1) + builder = x509.CertificateRevocationListBuilder().issuer_name( + x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, u"cryptography.io CA") + ]) + ).last_update( + last_update + ).next_update( + next_update + ).add_extension( + extension, False + ) + + crl = builder.sign(private_key, hashes.SHA256(), backend) + assert len(crl) == 0 + assert len(crl.extensions) == 1 + ext = crl.extensions.get_extension_for_class(type(extension)) + assert ext.critical is False + assert ext.value == extension + + @pytest.mark.requires_backend_interface(interface=RSABackend) + @pytest.mark.requires_backend_interface(interface=X509Backend) + def test_sign_multiple_extensions_critical(self, backend): + private_key = RSA_KEY_2048.private_key(backend) + last_update = datetime.datetime(2002, 1, 1, 12, 1) + next_update = datetime.datetime(2030, 1, 1, 12, 1) + ian = x509.IssuerAlternativeName([ + x509.UniformResourceIdentifier(u"https://cryptography.io"), + ]) + crl_number = x509.CRLNumber(13) + builder = x509.CertificateRevocationListBuilder().issuer_name( + x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, u"cryptography.io CA") + ]) + ).last_update( + last_update + ).next_update( + next_update + ).add_extension( + crl_number, False + ).add_extension( + ian, True + ) + + crl = builder.sign(private_key, hashes.SHA256(), backend) + assert len(crl) == 0 + assert len(crl.extensions) == 2 + ext1 = crl.extensions.get_extension_for_class(x509.CRLNumber) + assert ext1.critical is False + assert ext1.value == crl_number + ext2 = crl.extensions.get_extension_for_class( + x509.IssuerAlternativeName + ) + assert ext2.critical is True + assert ext2.value == ian + + @pytest.mark.requires_backend_interface(interface=RSABackend) + @pytest.mark.requires_backend_interface(interface=X509Backend) + def test_add_unsupported_extension(self, backend): + private_key = RSA_KEY_2048.private_key(backend) + last_update = datetime.datetime(2002, 1, 1, 12, 1) + next_update = datetime.datetime(2030, 1, 1, 12, 1) + builder = x509.CertificateRevocationListBuilder().issuer_name( + x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, u"cryptography.io CA") + ]) + ).last_update( + last_update + ).next_update( + next_update + ).add_extension( + x509.OCSPNoCheck(), False + ) + with pytest.raises(NotImplementedError): + builder.sign(private_key, hashes.SHA256(), backend) + + @pytest.mark.requires_backend_interface(interface=RSABackend) + @pytest.mark.requires_backend_interface(interface=X509Backend) + def test_sign_rsa_key_too_small(self, backend): + private_key = RSA_KEY_512.private_key(backend) + last_update = datetime.datetime(2002, 1, 1, 12, 1) + next_update = datetime.datetime(2030, 1, 1, 12, 1) + builder = x509.CertificateRevocationListBuilder().issuer_name( + x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, u"cryptography.io CA") + ]) + ).last_update( + last_update + ).next_update( + next_update + ) + + with pytest.raises(ValueError): + builder.sign(private_key, hashes.SHA512(), backend) + + @pytest.mark.requires_backend_interface(interface=RSABackend) + @pytest.mark.requires_backend_interface(interface=X509Backend) + def test_sign_with_invalid_hash(self, backend): + private_key = RSA_KEY_2048.private_key(backend) + last_update = datetime.datetime(2002, 1, 1, 12, 1) + next_update = datetime.datetime(2030, 1, 1, 12, 1) + builder = x509.CertificateRevocationListBuilder().issuer_name( + x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, u"cryptography.io CA") + ]) + ).last_update( + last_update + ).next_update( + next_update + ) + + with pytest.raises(TypeError): + builder.sign(private_key, object(), backend) + + @pytest.mark.requires_backend_interface(interface=DSABackend) + @pytest.mark.requires_backend_interface(interface=X509Backend) + def test_sign_dsa_key(self, backend): + if backend._lib.OPENSSL_VERSION_NUMBER < 0x10001000: + pytest.skip("Requires a newer OpenSSL. Must be >= 1.0.1") + private_key = DSA_KEY_2048.private_key(backend) + invalidity_date = x509.InvalidityDate( + datetime.datetime(2002, 1, 1, 0, 0) + ) + ian = x509.IssuerAlternativeName([ + x509.UniformResourceIdentifier(u"https://cryptography.io"), + ]) + revoked_cert0 = x509.RevokedCertificateBuilder().serial_number( + 2 + ).revocation_date( + datetime.datetime(2012, 1, 1, 1, 1) + ).add_extension( + invalidity_date, False + ).build(backend) + last_update = datetime.datetime(2002, 1, 1, 12, 1) + next_update = datetime.datetime(2030, 1, 1, 12, 1) + builder = x509.CertificateRevocationListBuilder().issuer_name( + x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, u"cryptography.io CA") + ]) + ).last_update( + last_update + ).next_update( + next_update + ).add_revoked_certificate( + revoked_cert0 + ).add_extension( + ian, False + ) + + crl = builder.sign(private_key, hashes.SHA256(), backend) + assert crl.extensions.get_extension_for_class( + x509.IssuerAlternativeName + ).value == ian + assert crl[0].serial_number == revoked_cert0.serial_number + assert crl[0].revocation_date == revoked_cert0.revocation_date + assert len(crl[0].extensions) == 1 + ext = crl[0].extensions.get_extension_for_class(x509.InvalidityDate) + assert ext.critical is False + assert ext.value == invalidity_date + + @pytest.mark.requires_backend_interface(interface=EllipticCurveBackend) + @pytest.mark.requires_backend_interface(interface=X509Backend) + def test_sign_ec_key_unsupported(self, backend): + if backend._lib.OPENSSL_VERSION_NUMBER < 0x10001000: + pytest.skip("Requires a newer OpenSSL. Must be >= 1.0.1") + _skip_curve_unsupported(backend, ec.SECP256R1()) + private_key = ec.generate_private_key(ec.SECP256R1(), backend) + invalidity_date = x509.InvalidityDate( + datetime.datetime(2002, 1, 1, 0, 0) + ) + ian = x509.IssuerAlternativeName([ + x509.UniformResourceIdentifier(u"https://cryptography.io"), + ]) + revoked_cert0 = x509.RevokedCertificateBuilder().serial_number( + 2 + ).revocation_date( + datetime.datetime(2012, 1, 1, 1, 1) + ).add_extension( + invalidity_date, False + ).build(backend) + last_update = datetime.datetime(2002, 1, 1, 12, 1) + next_update = datetime.datetime(2030, 1, 1, 12, 1) + builder = x509.CertificateRevocationListBuilder().issuer_name( + x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, u"cryptography.io CA") + ]) + ).last_update( + last_update + ).next_update( + next_update + ).add_revoked_certificate( + revoked_cert0 + ).add_extension( + ian, False + ) + + crl = builder.sign(private_key, hashes.SHA256(), backend) + assert crl.extensions.get_extension_for_class( + x509.IssuerAlternativeName + ).value == ian + assert crl[0].serial_number == revoked_cert0.serial_number + assert crl[0].revocation_date == revoked_cert0.revocation_date + assert len(crl[0].extensions) == 1 + ext = crl[0].extensions.get_extension_for_class(x509.InvalidityDate) + assert ext.critical is False + assert ext.value == invalidity_date + + @pytest.mark.requires_backend_interface(interface=RSABackend) + @pytest.mark.requires_backend_interface(interface=X509Backend) + def test_sign_with_revoked_certificates(self, backend): + private_key = RSA_KEY_2048.private_key(backend) + last_update = datetime.datetime(2002, 1, 1, 12, 1) + next_update = datetime.datetime(2030, 1, 1, 12, 1) + invalidity_date = x509.InvalidityDate( + datetime.datetime(2002, 1, 1, 0, 0) + ) + revoked_cert0 = x509.RevokedCertificateBuilder().serial_number( + 38 + ).revocation_date( + datetime.datetime(2011, 1, 1, 1, 1) + ).build(backend) + revoked_cert1 = x509.RevokedCertificateBuilder().serial_number( + 2 + ).revocation_date( + datetime.datetime(2012, 1, 1, 1, 1) + ).add_extension( + invalidity_date, False + ).build(backend) + builder = x509.CertificateRevocationListBuilder().issuer_name( + x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, u"cryptography.io CA") + ]) + ).last_update( + last_update + ).next_update( + next_update + ).add_revoked_certificate( + revoked_cert0 + ).add_revoked_certificate( + revoked_cert1 + ) + + crl = builder.sign(private_key, hashes.SHA256(), backend) + assert len(crl) == 2 + assert crl.last_update == last_update + assert crl.next_update == next_update + assert crl[0].serial_number == revoked_cert0.serial_number + assert crl[0].revocation_date == revoked_cert0.revocation_date + assert len(crl[0].extensions) == 0 + assert crl[1].serial_number == revoked_cert1.serial_number + assert crl[1].revocation_date == revoked_cert1.revocation_date + assert len(crl[1].extensions) == 1 + ext = crl[1].extensions.get_extension_for_class(x509.InvalidityDate) + assert ext.critical is False + assert ext.value == invalidity_date diff --git a/tests/test_x509_ext.py b/tests/test_x509_ext.py index d9743c8e..03a3730a 100644 --- a/tests/test_x509_ext.py +++ b/tests/test_x509_ext.py @@ -5,6 +5,7 @@ from __future__ import absolute_import, division, print_function import binascii +import datetime import ipaddress import os @@ -74,6 +75,173 @@ class TestExtension(object): assert ext1 != object() +class TestUnrecognizedExtension(object): + def test_invalid_oid(self): + with pytest.raises(TypeError): + x509.UnrecognizedExtension("notanoid", b"somedata") + + def test_eq(self): + ext1 = x509.UnrecognizedExtension( + x509.ObjectIdentifier("1.2.3.4"), b"\x03\x02\x01" + ) + ext2 = x509.UnrecognizedExtension( + x509.ObjectIdentifier("1.2.3.4"), b"\x03\x02\x01" + ) + assert ext1 == ext2 + + def test_ne(self): + ext1 = x509.UnrecognizedExtension( + x509.ObjectIdentifier("1.2.3.4"), b"\x03\x02\x01" + ) + ext2 = x509.UnrecognizedExtension( + x509.ObjectIdentifier("1.2.3.4"), b"\x03\x02\x02" + ) + ext3 = x509.UnrecognizedExtension( + x509.ObjectIdentifier("1.2.3.5"), b"\x03\x02\x01" + ) + assert ext1 != ext2 + assert ext1 != ext3 + assert ext1 != object() + + def test_repr(self): + ext1 = x509.UnrecognizedExtension( + x509.ObjectIdentifier("1.2.3.4"), b"\x03\x02\x01" + ) + if six.PY3: + assert repr(ext1) == ( + "<UnrecognizedExtension(oid=<ObjectIdentifier(oid=1.2.3.4, " + "name=Unknown OID)>, value=b'\\x03\\x02\\x01')>" + ) + else: + assert repr(ext1) == ( + "<UnrecognizedExtension(oid=<ObjectIdentifier(oid=1.2.3.4, " + "name=Unknown OID)>, value='\\x03\\x02\\x01')>" + ) + + def test_hash(self): + ext1 = x509.UnrecognizedExtension( + x509.ObjectIdentifier("1.2.3.4"), b"\x03\x02\x01" + ) + ext2 = x509.UnrecognizedExtension( + x509.ObjectIdentifier("1.2.3.4"), b"\x03\x02\x01" + ) + ext3 = x509.UnrecognizedExtension( + x509.ObjectIdentifier("1.2.3.5"), b"\x03\x02\x01" + ) + assert hash(ext1) == hash(ext2) + assert hash(ext1) != hash(ext3) + + +class TestCertificateIssuer(object): + def test_iter_names(self): + ci = x509.CertificateIssuer([ + x509.DNSName(u"cryptography.io"), + x509.DNSName(u"crypto.local"), + ]) + assert len(ci) == 2 + assert list(ci) == [ + x509.DNSName(u"cryptography.io"), + x509.DNSName(u"crypto.local"), + ] + + def test_indexing(self): + ci = x509.CertificateIssuer([ + x509.DNSName(u"cryptography.io"), + x509.DNSName(u"crypto.local"), + x509.DNSName(u"another.local"), + x509.RFC822Name(u"email@another.local"), + x509.UniformResourceIdentifier(u"http://another.local"), + ]) + assert ci[-1] == ci[4] + assert ci[2:6:2] == [ci[2], ci[4]] + + def test_eq(self): + ci1 = x509.CertificateIssuer([x509.DNSName(u"cryptography.io")]) + ci2 = x509.CertificateIssuer([x509.DNSName(u"cryptography.io")]) + assert ci1 == ci2 + + def test_ne(self): + ci1 = x509.CertificateIssuer([x509.DNSName(u"cryptography.io")]) + ci2 = x509.CertificateIssuer([x509.DNSName(u"somethingelse.tld")]) + assert ci1 != ci2 + assert ci1 != object() + + def test_repr(self): + ci = x509.CertificateIssuer([x509.DNSName(u"cryptography.io")]) + assert repr(ci) == ( + "<CertificateIssuer(<GeneralNames([<DNSName(value=cryptography.io" + ")>])>)>" + ) + + def test_get_values_for_type(self): + ci = x509.CertificateIssuer( + [x509.DNSName(u"cryptography.io")] + ) + names = ci.get_values_for_type(x509.DNSName) + assert names == [u"cryptography.io"] + + +class TestCRLReason(object): + def test_invalid_reason_flags(self): + with pytest.raises(TypeError): + x509.CRLReason("notareason") + + def test_eq(self): + reason1 = x509.CRLReason(x509.ReasonFlags.unspecified) + reason2 = x509.CRLReason(x509.ReasonFlags.unspecified) + assert reason1 == reason2 + + def test_ne(self): + reason1 = x509.CRLReason(x509.ReasonFlags.unspecified) + reason2 = x509.CRLReason(x509.ReasonFlags.ca_compromise) + assert reason1 != reason2 + assert reason1 != object() + + def test_hash(self): + reason1 = x509.CRLReason(x509.ReasonFlags.unspecified) + reason2 = x509.CRLReason(x509.ReasonFlags.unspecified) + reason3 = x509.CRLReason(x509.ReasonFlags.ca_compromise) + + assert hash(reason1) == hash(reason2) + assert hash(reason1) != hash(reason3) + + def test_repr(self): + reason1 = x509.CRLReason(x509.ReasonFlags.unspecified) + assert repr(reason1) == ( + "<CRLReason(reason=ReasonFlags.unspecified)>" + ) + + +class TestInvalidityDate(object): + def test_invalid_invalidity_date(self): + with pytest.raises(TypeError): + x509.InvalidityDate("notadate") + + def test_eq(self): + invalid1 = x509.InvalidityDate(datetime.datetime(2015, 1, 1, 1, 1)) + invalid2 = x509.InvalidityDate(datetime.datetime(2015, 1, 1, 1, 1)) + assert invalid1 == invalid2 + + def test_ne(self): + invalid1 = x509.InvalidityDate(datetime.datetime(2015, 1, 1, 1, 1)) + invalid2 = x509.InvalidityDate(datetime.datetime(2015, 1, 1, 1, 2)) + assert invalid1 != invalid2 + assert invalid1 != object() + + def test_repr(self): + invalid1 = x509.InvalidityDate(datetime.datetime(2015, 1, 1, 1, 1)) + assert repr(invalid1) == ( + "<InvalidityDate(invalidity_date=2015-01-01 01:01:00)>" + ) + + def test_hash(self): + invalid1 = x509.InvalidityDate(datetime.datetime(2015, 1, 1, 1, 1)) + invalid2 = x509.InvalidityDate(datetime.datetime(2015, 1, 1, 1, 1)) + invalid3 = x509.InvalidityDate(datetime.datetime(2015, 1, 1, 1, 2)) + assert hash(invalid1) == hash(invalid2) + assert hash(invalid1) != hash(invalid3) + + class TestNoticeReference(object): def test_notice_numbers_not_all_int(self): with pytest.raises(TypeError): @@ -269,6 +437,16 @@ class TestCertificatePolicies(object): assert cp != cp2 assert cp != object() + def test_indexing(self): + pi = x509.PolicyInformation(x509.ObjectIdentifier("1.2.3"), [u"test"]) + pi2 = x509.PolicyInformation(x509.ObjectIdentifier("1.2.4"), [u"test"]) + pi3 = x509.PolicyInformation(x509.ObjectIdentifier("1.2.5"), [u"test"]) + pi4 = x509.PolicyInformation(x509.ObjectIdentifier("1.2.6"), [u"test"]) + pi5 = x509.PolicyInformation(x509.ObjectIdentifier("1.2.7"), [u"test"]) + cp = x509.CertificatePolicies([pi, pi2, pi3, pi4, pi5]) + assert cp[-1] == cp[4] + assert cp[2:6:2] == [cp[2], cp[4]] + @pytest.mark.requires_backend_interface(interface=RSABackend) @pytest.mark.requires_backend_interface(interface=X509Backend) @@ -595,6 +773,20 @@ class TestSubjectKeyIdentifier(object): assert ski != ski2 assert ski != object() + def test_hash(self): + ski1 = x509.SubjectKeyIdentifier( + binascii.unhexlify(b"092384932230498bc980aa8098456f6ff7ff3ac9") + ) + ski2 = x509.SubjectKeyIdentifier( + binascii.unhexlify(b"092384932230498bc980aa8098456f6ff7ff3ac9") + ) + ski3 = x509.SubjectKeyIdentifier( + binascii.unhexlify(b"aa8098456f6ff7ff3ac9092384932230498bc980") + ) + + assert hash(ski1) == hash(ski2) + assert hash(ski1) != hash(ski3) + class TestAuthorityKeyIdentifier(object): def test_authority_cert_issuer_not_generalname(self): @@ -720,6 +912,13 @@ class TestBasicConstraints(object): "<BasicConstraints(ca=True, path_length=None)>" ) + def test_hash(self): + na = x509.BasicConstraints(ca=True, path_length=None) + na2 = x509.BasicConstraints(ca=True, path_length=None) + na3 = x509.BasicConstraints(ca=True, path_length=0) + assert hash(na) == hash(na2) + assert hash(na) != hash(na3) + def test_eq(self): na = x509.BasicConstraints(ca=True, path_length=None) na2 = x509.BasicConstraints(ca=True, path_length=None) @@ -833,17 +1032,33 @@ class TestExtensions(object): assert exc.value.oid == x509.ObjectIdentifier("1.2.3.4") + @pytest.mark.requires_backend_interface(interface=EllipticCurveBackend) def test_unsupported_extension(self, backend): - # TODO: this will raise an exception when all extensions are complete cert = _load_cert( os.path.join( - "x509", "custom", "unsupported_extension.pem" + "x509", "custom", "unsupported_extension_2.pem" ), x509.load_pem_x509_certificate, backend ) extensions = cert.extensions - assert len(extensions) == 0 + assert len(extensions) == 2 + assert extensions[0].critical is False + assert extensions[0].oid == x509.ObjectIdentifier( + "1.3.6.1.4.1.41482.2" + ) + assert extensions[0].value == x509.UnrecognizedExtension( + x509.ObjectIdentifier("1.3.6.1.4.1.41482.2"), + b"1.3.6.1.4.1.41482.1.2" + ) + assert extensions[1].critical is False + assert extensions[1].oid == x509.ObjectIdentifier( + "1.3.6.1.4.1.45724.2.1.1" + ) + assert extensions[1].value == x509.UnrecognizedExtension( + x509.ObjectIdentifier("1.3.6.1.4.1.45724.2.1.1"), + b"\x03\x02\x040" + ) def test_no_extensions_get_for_class(self, backend): cert = _load_cert( @@ -858,6 +1073,21 @@ class TestExtensions(object): exts.get_extension_for_class(x509.IssuerAlternativeName) assert exc.value.oid == ExtensionOID.ISSUER_ALTERNATIVE_NAME + def test_unrecognized_extension_for_class(self): + exts = x509.Extensions([]) + with pytest.raises(TypeError): + exts.get_extension_for_class(x509.UnrecognizedExtension) + + def test_indexing(self, backend): + cert = _load_cert( + os.path.join("x509", "cryptography.io.pem"), + x509.load_pem_x509_certificate, + backend + ) + exts = cert.extensions + assert exts[-1] == exts[7] + assert exts[2:6:2] == [exts[2], exts[4]] + def test_one_extension_get_for_class(self, backend): cert = _load_cert( os.path.join( @@ -1379,6 +1609,17 @@ class TestGeneralNames(object): x509.DNSName(u"crypto.local"), ] + def test_indexing(self): + gn = x509.GeneralNames([ + x509.DNSName(u"cryptography.io"), + x509.DNSName(u"crypto.local"), + x509.DNSName(u"another.local"), + x509.RFC822Name(u"email@another.local"), + x509.UniformResourceIdentifier(u"http://another.local"), + ]) + assert gn[-1] == gn[4] + assert gn[2:6:2] == [gn[2], gn[4]] + def test_invalid_general_names(self): with pytest.raises(TypeError): x509.GeneralNames( @@ -1434,6 +1675,17 @@ class TestIssuerAlternativeName(object): x509.DNSName(u"crypto.local"), ] + def test_indexing(self): + ian = x509.IssuerAlternativeName([ + x509.DNSName(u"cryptography.io"), + x509.DNSName(u"crypto.local"), + x509.DNSName(u"another.local"), + x509.RFC822Name(u"email@another.local"), + x509.UniformResourceIdentifier(u"http://another.local"), + ]) + assert ian[-1] == ian[4] + assert ian[2:6:2] == [ian[2], ian[4]] + def test_invalid_general_names(self): with pytest.raises(TypeError): x509.IssuerAlternativeName( @@ -1506,6 +1758,13 @@ class TestCRLNumber(object): with pytest.raises(TypeError): x509.CRLNumber("notanumber") + def test_hash(self): + c1 = x509.CRLNumber(1) + c2 = x509.CRLNumber(1) + c3 = x509.CRLNumber(2) + assert hash(c1) == hash(c2) + assert hash(c1) != hash(c3) + class TestSubjectAlternativeName(object): def test_get_values_for_type(self): @@ -1526,6 +1785,17 @@ class TestSubjectAlternativeName(object): x509.DNSName(u"crypto.local"), ] + def test_indexing(self): + san = x509.SubjectAlternativeName([ + x509.DNSName(u"cryptography.io"), + x509.DNSName(u"crypto.local"), + x509.DNSName(u"another.local"), + x509.RFC822Name(u"email@another.local"), + x509.UniformResourceIdentifier(u"http://another.local"), + ]) + assert san[-1] == san[4] + assert san[2:6:2] == [san[2], san[4]] + def test_invalid_general_names(self): with pytest.raises(TypeError): x509.SubjectAlternativeName( @@ -2028,6 +2298,32 @@ class TestAuthorityInformationAccess(object): assert aia != aia2 assert aia != object() + def test_indexing(self): + aia = x509.AuthorityInformationAccess([ + x509.AccessDescription( + AuthorityInformationAccessOID.OCSP, + x509.UniformResourceIdentifier(u"http://ocsp.domain.com") + ), + x509.AccessDescription( + AuthorityInformationAccessOID.CA_ISSUERS, + x509.UniformResourceIdentifier(u"http://domain.com/ca.crt") + ), + x509.AccessDescription( + AuthorityInformationAccessOID.OCSP, + x509.UniformResourceIdentifier(u"http://ocsp2.domain.com") + ), + x509.AccessDescription( + AuthorityInformationAccessOID.OCSP, + x509.UniformResourceIdentifier(u"http://ocsp3.domain.com") + ), + x509.AccessDescription( + AuthorityInformationAccessOID.OCSP, + x509.UniformResourceIdentifier(u"http://ocsp4.domain.com") + ), + ]) + assert aia[-1] == aia[4] + assert aia[2:6:2] == [aia[2], aia[4]] + @pytest.mark.requires_backend_interface(interface=RSABackend) @pytest.mark.requires_backend_interface(interface=X509Backend) @@ -2756,6 +3052,32 @@ class TestCRLDistributionPoints(object): assert cdp != cdp4 assert cdp != object() + def test_indexing(self): + ci = x509.CRLDistributionPoints([ + x509.DistributionPoint( + None, None, None, + [x509.UniformResourceIdentifier(u"uri://thing")], + ), + x509.DistributionPoint( + None, None, None, + [x509.UniformResourceIdentifier(u"uri://thing2")], + ), + x509.DistributionPoint( + None, None, None, + [x509.UniformResourceIdentifier(u"uri://thing3")], + ), + x509.DistributionPoint( + None, None, None, + [x509.UniformResourceIdentifier(u"uri://thing4")], + ), + x509.DistributionPoint( + None, None, None, + [x509.UniformResourceIdentifier(u"uri://thing5")], + ), + ]) + assert ci[-1] == ci[4] + assert ci[2:6:2] == [ci[2], ci[4]] + @pytest.mark.requires_backend_interface(interface=RSABackend) @pytest.mark.requires_backend_interface(interface=X509Backend) diff --git a/tests/test_x509_revokedcertbuilder.py b/tests/test_x509_revokedcertbuilder.py new file mode 100644 index 00000000..5aa90630 --- /dev/null +++ b/tests/test_x509_revokedcertbuilder.py @@ -0,0 +1,160 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import absolute_import, division, print_function + +import datetime + +import pytest + +from cryptography import x509 +from cryptography.hazmat.backends.interfaces import X509Backend + + +class TestRevokedCertificateBuilder(object): + def test_serial_number_must_be_integer(self): + with pytest.raises(TypeError): + x509.RevokedCertificateBuilder().serial_number("notanx509name") + + def test_serial_number_must_be_non_negative(self): + with pytest.raises(ValueError): + x509.RevokedCertificateBuilder().serial_number(-1) + + def test_serial_number_must_be_less_than_160_bits_long(self): + with pytest.raises(ValueError): + # 2 raised to the 160th power is actually 161 bits + x509.RevokedCertificateBuilder().serial_number(2 ** 160) + + def test_set_serial_number_twice(self): + builder = x509.RevokedCertificateBuilder().serial_number(3) + with pytest.raises(ValueError): + builder.serial_number(4) + + def test_revocation_date_invalid(self): + with pytest.raises(TypeError): + x509.RevokedCertificateBuilder().revocation_date("notadatetime") + + def test_revocation_date_before_unix_epoch(self): + with pytest.raises(ValueError): + x509.RevokedCertificateBuilder().revocation_date( + datetime.datetime(1960, 8, 10) + ) + + def test_set_revocation_date_twice(self): + builder = x509.RevokedCertificateBuilder().revocation_date( + datetime.datetime(2002, 1, 1, 12, 1) + ) + with pytest.raises(ValueError): + builder.revocation_date(datetime.datetime(2002, 1, 1, 12, 1)) + + def test_add_extension_checks_for_duplicates(self): + builder = x509.RevokedCertificateBuilder().add_extension( + x509.CRLReason(x509.ReasonFlags.ca_compromise), False + ) + + with pytest.raises(ValueError): + builder.add_extension( + x509.CRLReason(x509.ReasonFlags.ca_compromise), False + ) + + def test_add_invalid_extension(self): + with pytest.raises(TypeError): + x509.RevokedCertificateBuilder().add_extension( + "notanextension", False + ) + + @pytest.mark.requires_backend_interface(interface=X509Backend) + def test_no_serial_number(self, backend): + builder = x509.RevokedCertificateBuilder().revocation_date( + datetime.datetime(2002, 1, 1, 12, 1) + ) + + with pytest.raises(ValueError): + builder.build(backend) + + @pytest.mark.requires_backend_interface(interface=X509Backend) + def test_no_revocation_date(self, backend): + builder = x509.RevokedCertificateBuilder().serial_number(3) + + with pytest.raises(ValueError): + builder.build(backend) + + @pytest.mark.requires_backend_interface(interface=X509Backend) + def test_create_revoked(self, backend): + serial_number = 333 + revocation_date = datetime.datetime(2002, 1, 1, 12, 1) + builder = x509.RevokedCertificateBuilder().serial_number( + serial_number + ).revocation_date( + revocation_date + ) + + revoked_certificate = builder.build(backend) + assert revoked_certificate.serial_number == serial_number + assert revoked_certificate.revocation_date == revocation_date + assert len(revoked_certificate.extensions) == 0 + + @pytest.mark.parametrize( + "extension", + [ + x509.InvalidityDate(datetime.datetime(2015, 1, 1, 0, 0)), + x509.CRLReason(x509.ReasonFlags.ca_compromise), + x509.CertificateIssuer([ + x509.DNSName(u"cryptography.io"), + ]) + ] + ) + @pytest.mark.requires_backend_interface(interface=X509Backend) + def test_add_extensions(self, backend, extension): + serial_number = 333 + revocation_date = datetime.datetime(2002, 1, 1, 12, 1) + builder = x509.RevokedCertificateBuilder().serial_number( + serial_number + ).revocation_date( + revocation_date + ).add_extension( + extension, False + ) + + revoked_certificate = builder.build(backend) + assert revoked_certificate.serial_number == serial_number + assert revoked_certificate.revocation_date == revocation_date + assert len(revoked_certificate.extensions) == 1 + ext = revoked_certificate.extensions.get_extension_for_class( + type(extension) + ) + assert ext.critical is False + assert ext.value == extension + + @pytest.mark.requires_backend_interface(interface=X509Backend) + def test_add_multiple_extensions(self, backend): + serial_number = 333 + revocation_date = datetime.datetime(2002, 1, 1, 12, 1) + invalidity_date = x509.InvalidityDate( + datetime.datetime(2015, 1, 1, 0, 0) + ) + certificate_issuer = x509.CertificateIssuer([ + x509.DNSName(u"cryptography.io"), + ]) + crl_reason = x509.CRLReason(x509.ReasonFlags.aa_compromise) + builder = x509.RevokedCertificateBuilder().serial_number( + serial_number + ).revocation_date( + revocation_date + ).add_extension( + invalidity_date, True + ).add_extension( + crl_reason, True + ).add_extension( + certificate_issuer, True + ) + + revoked_certificate = builder.build(backend) + assert len(revoked_certificate.extensions) == 3 + for ext_data in [invalidity_date, certificate_issuer, crl_reason]: + ext = revoked_certificate.extensions.get_extension_for_class( + type(ext_data) + ) + assert ext.critical is True + assert ext.value == ext_data diff --git a/vectors/cryptography_vectors/__about__.py b/vectors/cryptography_vectors/__about__.py index 63f0b572..11bd69fb 100644 --- a/vectors/cryptography_vectors/__about__.py +++ b/vectors/cryptography_vectors/__about__.py @@ -20,4 +20,4 @@ __author__ = "The cryptography developers" __email__ = "cryptography-dev@python.org" __license__ = "BSD or Apache License, Version 2.0" -__copyright__ = "Copyright 2013-2015 %s" % __author__ +__copyright__ = "Copyright 2013-2016 %s" % __author__ diff --git a/vectors/cryptography_vectors/x509/custom/unsupported_extension_2.pem b/vectors/cryptography_vectors/x509/custom/unsupported_extension_2.pem new file mode 100644 index 00000000..4b7d2565 --- /dev/null +++ b/vectors/cryptography_vectors/x509/custom/unsupported_extension_2.pem @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICIjCCAQygAwIBAgIEIHHwozALBgkqhkiG9w0BAQswDzENMAsGA1UEAxMEdGVz +dDAeFw0xNTA4MTEwOTAwMzNaFw0xNjA4MTAwOTAwMzNaMCkxJzAlBgNVBAMTHll1 +YmljbyBVMkYgRUUgU2VyaWFsIDU0NDMzODA4MzBZMBMGByqGSM49AgEGCCqGSM49 +AwEHA0IABPdFG1pBjBBQVhLrD39Qg1vKjuR2kRdBZnwLI/zgzztQpf4ffpkrkB/3 +E0TXj5zg8gN9sgMkX48geBe+tBEpvMmjOzA5MCIGCSsGAQQBgsQKAgQVMS4zLjYu +MS40LjEuNDE0ODIuMS4yMBMGCysGAQQBguUcAgEBBAQDAgQwMAsGCSqGSIb3DQEB +CwOCAQEAb3YpnmHHduNuWEXlLqlnww9034ZeZaojhPAYSLR8d5NPk9gc0hkjQKmI +aaBM7DsaHbcHMKpXoMGTQSC++NCZTcKvZ0Lt12mp5HRnM1NNBPol8Hte5fLmvW4t +Q9EzLl4gkz7LSlORxTuwTbae1eQqNdxdeB+0ilMFCEUc+3NGCNM0RWd+sP5+gzMX +BDQAI1Sc9XaPIg8t3du5JChAl1ifpu/uERZ2WQgtxeBDO6z1Xoa5qz4svf5oURjP +ZjxS0WUKht48Z2rIjk5lZzERSaY3RrX3UtrnZEIzCmInXOrcRPeAD4ZutpiwuHe6 +2ABsjuMRnKbATbOUiLdknNyPYYQz2g== +-----END CERTIFICATE----- |