aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/x509/reference.rst12
-rw-r--r--src/cryptography/hazmat/backends/openssl/backend.py80
-rw-r--r--src/cryptography/x509/base.py15
-rw-r--r--tests/test_x509_crlbuilder.py10
-rw-r--r--tests/test_x509_revokedcertbuilder.py74
5 files changed, 187 insertions, 4 deletions
diff --git a/docs/x509/reference.rst b/docs/x509/reference.rst
index 72fd44be..bbea490e 100644
--- a/docs/x509/reference.rst
+++ b/docs/x509/reference.rst
@@ -947,6 +947,16 @@ X.509 Revoked Certificate Builder
: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.
@@ -1956,6 +1966,8 @@ These classes may be present within a :class:`CertificatePolicies` instance.
A list of integers.
+.. _crl_entry_extensions:
+
CRL Entry Extensions
~~~~~~~~~~~~~~~~~~~~
diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py
index c0c9ebe2..6c7a6840 100644
--- a/src/cryptography/hazmat/backends/openssl/backend.py
+++ b/src/cryptography/hazmat/backends/openssl/backend.py
@@ -53,7 +53,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"])
@@ -164,6 +164,68 @@ def _encode_crl_number(backend, crl_number):
return pp, r
+_CRL_ENTRY_REASONFLAGS = {
+ 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 _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_REASONFLAGS[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_issuer(backend, certificate_issuer):
+ general_names = _encode_general_names(backend, certificate_issuer)
+ 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 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 +707,12 @@ _CRL_EXTENSION_ENCODE_HANDLERS = {
ExtensionOID.CRL_NUMBER: _encode_crl_number,
}
+_CRL_ENTRY_EXTENSION_ENCODE_HANDLERS = {
+ CRLEntryExtensionOID.CERTIFICATE_ISSUER: _encode_certificate_issuer,
+ CRLEntryExtensionOID.CRL_REASON: _encode_crl_reason,
+ CRLEntryExtensionOID.INVALIDITY_DATE: _encode_invalidity_date,
+}
+
class _PasswordUserdata(object):
def __init__(self, password):
@@ -1505,7 +1573,6 @@ class Backend(object):
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 +1650,14 @@ class Backend(object):
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
index bc927e87..55e965f7 100644
--- a/src/cryptography/x509/base.py
+++ b/src/cryptography/x509/base.py
@@ -650,6 +650,21 @@ class RevokedCertificateBuilder(object):
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")
diff --git a/tests/test_x509_crlbuilder.py b/tests/test_x509_crlbuilder.py
index de3adcd4..763a6472 100644
--- a/tests/test_x509_crlbuilder.py
+++ b/tests/test_x509_crlbuilder.py
@@ -351,6 +351,9 @@ class TestCertificateRevocationListBuilder(object):
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 @@ class TestCertificateRevocationListBuilder(object):
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 @@ class TestCertificateRevocationListBuilder(object):
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
index 9f79387b..0ef92ff6 100644
--- a/tests/test_x509_revokedcertbuilder.py
+++ b/tests/test_x509_revokedcertbuilder.py
@@ -48,6 +48,16 @@ class TestRevokedCertificateBuilder(object):
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
+ )
+
@pytest.mark.requires_backend_interface(interface=X509Backend)
def test_no_serial_number(self, backend):
builder = x509.RevokedCertificateBuilder().revocation_date(
@@ -78,3 +88,67 @@ class TestRevokedCertificateBuilder(object):
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