aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPaul Kehrer <paul.l.kehrer@gmail.com>2015-12-19 23:32:08 -0600
committerPaul Kehrer <paul.l.kehrer@gmail.com>2015-12-24 18:49:11 -0600
commitbfac2d10305cf72d634e0e74a87fd08d4cd07257 (patch)
treea93206af48941b2539019d8bb6e290f17956ad97
parent48f17cb225abcf43f77915d152f6cc15b762c702 (diff)
downloadcryptography-bfac2d10305cf72d634e0e74a87fd08d4cd07257.tar.gz
cryptography-bfac2d10305cf72d634e0e74a87fd08d4cd07257.tar.bz2
cryptography-bfac2d10305cf72d634e0e74a87fd08d4cd07257.zip
CertificateRevocationListBuilder
RSA keys only. Currently does not support CRL extensions or CRLEntry extensions.
-rw-r--r--docs/hazmat/backends/interfaces.rst19
-rw-r--r--docs/x509/reference.rst82
-rw-r--r--src/cryptography/hazmat/backends/openssl/backend.py68
-rw-r--r--src/cryptography/x509/__init__.py2
-rw-r--r--src/cryptography/x509/base.py66
-rw-r--r--tests/hazmat/backends/test_openssl.py9
-rw-r--r--tests/test_x509_crlbuilder.py222
7 files changed, 464 insertions, 4 deletions
diff --git a/docs/hazmat/backends/interfaces.rst b/docs/hazmat/backends/interfaces.rst
index 3a7224fa..2952d85a 100644
--- a/docs/hazmat/backends/interfaces.rst
+++ b/docs/hazmat/backends/interfaces.rst
@@ -589,6 +589,25 @@ A specific ``backend`` may provide one or more of these interfaces.
:returns: A new instance of
:class:`~cryptography.x509.CertificateRevocationList`.
+ .. 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 object with the
+ :class:`~cryptography.x509.CertificateRevocationList` interface.
.. class:: DHBackend
diff --git a/docs/x509/reference.rst b/docs/x509/reference.rst
index 4f4ce4fa..0697e636 100644
--- a/docs/x509/reference.rst
+++ b/docs/x509/reference.rst
@@ -761,6 +761,88 @@ 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)
+ >>> crl = builder.sign(
+ ... private_key=private_key, algorithm=hashes.SHA256(),
+ ... backend=default_backend()
+ ... )
+ >>> isinstance(crl, x509.CertificateRevocationList)
+ True
+
+ .. 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 the CRL's activation time. This is the time from which
+ clients can start trusting the CRL. It may be different from
+ the time at which the 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 the CRL. The CRL may not be trusted if it is
+ used before this time.
+
+ .. method:: next_update(time)
+
+ Sets the CRL's next update time. This is the time by which
+ a new CRL will be issued. The next CRL could be issued before this
+ , but it will not be issued any later than the indicated date.
+
+ :param time: The :class:`datetime.datetime` object (in UTC) that marks the
+ next update time for the CRL.
+
+ .. method:: sign(private_key, algorithm, backend)
+
+ Sign the 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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py
index c3eccb06..6d19b806 100644
--- a/src/cryptography/hazmat/backends/openssl/backend.py
+++ b/src/cryptography/hazmat/backends/openssl/backend.py
@@ -1456,7 +1456,73 @@ class Backend(object):
return _Certificate(self, x509_cert)
def create_x509_crl(self, builder, private_key, algorithm):
- raise NotImplementedError
+ 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 isinstance(private_key, _DSAPrivateKey):
+ raise NotImplementedError(
+ "CRL signatures aren't implemented for DSA"
+ " keys at this time."
+ )
+ if isinstance(private_key, _EllipticCurvePrivateKey):
+ raise NotImplementedError(
+ "CRL signatures aren't implemented for EC"
+ " keys at this time."
+ )
+
+ 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)
+ # TODO: support revoked certificates
+
+ # TODO: add support for CRL extensions
+ 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 load_pem_private_key(self, data, password):
return self._load_key(
diff --git a/src/cryptography/x509/__init__.py b/src/cryptography/x509/__init__.py
index c4434fd1..4978b199 100644
--- a/src/cryptography/x509/__init__.py
+++ b/src/cryptography/x509/__init__.py
@@ -6,6 +6,7 @@ from __future__ import absolute_import, division, print_function
from cryptography.x509.base import (
Certificate, CertificateBuilder, CertificateRevocationList,
+ CertificateRevocationListBuilder,
CertificateSigningRequest, CertificateSigningRequestBuilder,
InvalidVersion, RevokedCertificate,
Version, load_der_x509_certificate, load_der_x509_crl, load_der_x509_csr,
@@ -152,6 +153,7 @@ __all__ = [
"OtherName",
"Certificate",
"CertificateRevocationList",
+ "CertificateRevocationListBuilder",
"CertificateSigningRequest",
"RevokedCertificate",
"CertificateSigningRequestBuilder",
diff --git a/src/cryptography/x509/base.py b/src/cryptography/x509/base.py
index 057d0e9b..6bca2c52 100644
--- a/src/cryptography/x509/base.py
+++ b/src/cryptography/x509/base.py
@@ -518,3 +518,69 @@ 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 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)
diff --git a/tests/hazmat/backends/test_openssl.py b/tests/hazmat/backends/test_openssl.py
index fd4030f6..6c824d27 100644
--- a/tests/hazmat/backends/test_openssl.py
+++ b/tests/hazmat/backends/test_openssl.py
@@ -501,9 +501,12 @@ class TestOpenSSLSignX509Certificate(object):
backend.create_x509_certificate(object(), private_key, DummyHash())
-def test_crl_creation_not_implemented():
- with pytest.raises(NotImplementedError):
- backend.create_x509_crl("", "", "")
+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())
class TestOpenSSLSerialisationWithOpenSSL(object):
diff --git a/tests/test_x509_crlbuilder.py b/tests/test_x509_crlbuilder.py
new file mode 100644
index 00000000..c6b23174
--- /dev/null
+++ b/tests/test_x509_crlbuilder.py
@@ -0,0 +1,222 @@
+# 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 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))
+
+ @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.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_unsupported(self, backend):
+ private_key = DSA_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(NotImplementedError):
+ builder.sign(private_key, hashes.SHA256(), backend)
+
+ @pytest.mark.requires_backend_interface(interface=EllipticCurveBackend)
+ @pytest.mark.requires_backend_interface(interface=X509Backend)
+ def test_sign_ec_key_unsupported(self, backend):
+ _skip_curve_unsupported(backend, ec.SECP256R1())
+ private_key = ec.generate_private_key(ec.SECP256R1(), 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(NotImplementedError):
+ builder.sign(private_key, hashes.SHA256(), backend)