From c33ffd7527a4ce77010425fedfbeed27856c8aa8 Mon Sep 17 00:00:00 2001 From: Paul Kehrer Date: Fri, 25 Dec 2015 10:59:22 -0600 Subject: RevokedCertificateBuilder --- docs/x509/reference.rst | 48 +++++++++++++ .../hazmat/backends/openssl/backend.py | 21 +++++- src/cryptography/x509/__init__.py | 3 +- src/cryptography/x509/base.py | 44 ++++++++++++ tests/hazmat/backends/test_openssl.py | 4 +- tests/test_x509_revokedcertbuilder.py | 80 ++++++++++++++++++++++ 6 files changed, 195 insertions(+), 5 deletions(-) create mode 100644 tests/test_x509_revokedcertbuilder.py diff --git a/docs/x509/reference.rst b/docs/x509/reference.rst index e4711be3..8d8bda4b 100644 --- a/docs/x509/reference.rst +++ b/docs/x509/reference.rst @@ -895,6 +895,54 @@ X.509 Revoked Certificate Object , critical=False, value=2015-01-01 00:00:00)> , critical=False, value=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:: 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py index a60bf82b..81316da5 100644 --- a/src/cryptography/hazmat/backends/openssl/backend.py +++ b/src/cryptography/hazmat/backends/openssl/backend.py @@ -38,7 +38,7 @@ from cryptography.hazmat.backends.openssl.rsa import ( ) from cryptography.hazmat.backends.openssl.x509 import ( _Certificate, _CertificateRevocationList, _CertificateSigningRequest, - _DISTPOINT_TYPE_FULLNAME, _DISTPOINT_TYPE_RELATIVENAME + _DISTPOINT_TYPE_FULLNAME, _DISTPOINT_TYPE_RELATIVENAME, _RevokedCertificate ) from cryptography.hazmat.bindings._openssl import ffi as _ffi from cryptography.hazmat.bindings.openssl import binding @@ -1559,7 +1559,24 @@ class Backend(object): self.openssl_assert(res >= 1) def create_x509_revoked_certificate(self, builder): - raise NotImplementedError("Not yet implemented") + 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) + # TODO: add crl entry extensions + return _RevokedCertificate(self, None, x509_revoked) 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 4978b199..5653144c 100644 --- a/src/cryptography/x509/__init__.py +++ b/src/cryptography/x509/__init__.py @@ -8,7 +8,7 @@ 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, ) @@ -156,6 +156,7 @@ __all__ = [ "CertificateRevocationListBuilder", "CertificateSigningRequest", "RevokedCertificate", + "RevokedCertificateBuilder", "CertificateSigningRequestBuilder", "CertificateBuilder", "Version", diff --git a/src/cryptography/x509/base.py b/src/cryptography/x509/base.py index 49cbcf75..e29a3105 100644 --- a/src/cryptography/x509/base.py +++ b/src/cryptography/x509/base.py @@ -602,3 +602,47 @@ class CertificateRevocationListBuilder(object): 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 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/tests/hazmat/backends/test_openssl.py b/tests/hazmat/backends/test_openssl.py index af064d18..c8d35893 100644 --- a/tests/hazmat/backends/test_openssl.py +++ b/tests/hazmat/backends/test_openssl.py @@ -510,8 +510,8 @@ class TestOpenSSLSignX509CertificateRevocationList(object): class TestOpenSSLCreateRevokedCertificate(object): - def test_not_yet_implemented(self): - with pytest.raises(NotImplementedError): + def test_invalid_builder(self): + with pytest.raises(TypeError): backend.create_x509_revoked_certificate(object()) diff --git a/tests/test_x509_revokedcertbuilder.py b/tests/test_x509_revokedcertbuilder.py new file mode 100644 index 00000000..9f79387b --- /dev/null +++ b/tests/test_x509_revokedcertbuilder.py @@ -0,0 +1,80 @@ +# 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)) + + @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 -- cgit v1.2.3