Repository: pyca/cryptography

Issue:
Add feature to create CRLs
I'd like to be able to create CRLs.

`<reaperhulk> Wulf: that's a missing feature right now. A CertificateRevocationListBuilder needs to be implemented`



Discussion:
#2559 was the start of this

Since I need something to track outstanding work items:
- [x] add_revoked_certificate support in CertificateRevocationListBuilder
- [x] RevokedCertificateBuilder to build revoked certificates for the CRL builder (#2571)
- [ ] CRL Entry extension support in RevokedCertificateBuilder
- [ ] ECDSA signature support in CertificateRevocationListBuilder
- [ ] DSA signature support in CertificateRevocationListBuilder


Resolving patch:
diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py
--- a/src/cryptography/hazmat/backends/openssl/backend.py
+++ b/src/cryptography/hazmat/backends/openssl/backend.py
@@ -37,8 +37,9 @@
     _RSAPrivateKey, _RSAPublicKey
 )
 from cryptography.hazmat.backends.openssl.x509 import (
-    _Certificate, _CertificateRevocationList, _CertificateSigningRequest,
-    _DISTPOINT_TYPE_FULLNAME, _DISTPOINT_TYPE_RELATIVENAME, _RevokedCertificate
+    _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.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"])
@@ -164,6 +165,40 @@ def _encode_crl_number(backend, crl_number):
     return pp, r
 
 
+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)
+    pp = backend._ffi.new('unsigned char **')
+    r = backend._lib.i2d_ASN1_ENUMERATED(asn1enum, pp)
+    backend.openssl_assert(r > 0)
+    pp = backend._ffi.gc(
+        pp, lambda pointer: backend._lib.OPENSSL_free(pointer[0])
+    )
+    return pp, r
+
+
+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)
+    pp = backend._ffi.new('unsigned char **')
+    r = backend._lib.i2d_ASN1_GENERALIZEDTIME(time, pp)
+    backend.openssl_assert(r > 0)
+    pp = backend._ffi.gc(
+        pp, lambda pointer: backend._lib.OPENSSL_free(pointer[0])
+    )
+    return pp, r
+
+
 def _encode_certificate_policies(backend, certificate_policies):
     cp = backend._lib.sk_POLICYINFO_new_null()
     backend.openssl_assert(cp != backend._ffi.NULL)
@@ -645,6 +680,12 @@ def _encode_general_subtree(backend, subtrees):
     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):
@@ -1505,7 +1546,6 @@ def create_x509_crl(self, builder, private_key, algorithm):
         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
 
         # Add extensions.
         self._create_x509_extensions(
@@ -1583,7 +1623,14 @@ def create_x509_revoked_certificate(self, builder):
             calendar.timegm(builder._revocation_date.timetuple())
         )
         self.openssl_assert(res != self._ffi.NULL)
-        # TODO: add crl entry extensions
+        # 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):
diff --git a/src/cryptography/x509/base.py b/src/cryptography/x509/base.py
--- a/src/cryptography/x509/base.py
+++ b/src/cryptography/x509/base.py
@@ -650,6 +650,21 @@ def revocation_date(self, time):
             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")


Test patch:
diff --git a/tests/test_x509_crlbuilder.py b/tests/test_x509_crlbuilder.py
--- a/tests/test_x509_crlbuilder.py
+++ b/tests/test_x509_crlbuilder.py
@@ -351,6 +351,9 @@ 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(
@@ -360,6 +363,8 @@ def test_sign_with_revoked_certificates(self, backend):
             2
         ).revocation_date(
             datetime.datetime(2012, 1, 1, 1, 1)
+        ).add_extension(
+            invalidity_date, False
         ).build(backend)
         builder = x509.CertificateRevocationListBuilder().issuer_name(
             x509.Name([
@@ -384,4 +389,7 @@ def test_sign_with_revoked_certificates(self, backend):
         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) == 0
+        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_revokedcertbuilder.py b/tests/test_x509_revokedcertbuilder.py
--- a/tests/test_x509_revokedcertbuilder.py
+++ b/tests/test_x509_revokedcertbuilder.py
@@ -48,6 +48,22 @@ def test_set_revocation_date_twice(self):
         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(
@@ -78,3 +94,67 @@ def test_create_revoked(self, 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