diff options
author | Paul Kehrer <paul.l.kehrer@gmail.com> | 2015-06-26 09:43:39 -0500 |
---|---|---|
committer | Paul Kehrer <paul.l.kehrer@gmail.com> | 2015-06-26 09:43:39 -0500 |
commit | 8f768dc5b9eda26510b3ffc6be862ea3e8f4a0b4 (patch) | |
tree | 73d3d549bb09cbe69ca275938217c1bab4ea2254 | |
parent | 77c98e3c4ef69d0cfee665cd0835670f4ac44242 (diff) | |
parent | 8cdcdfc1bd11ee57b7f53c631af2f88e0861d168 (diff) | |
download | cryptography-8f768dc5b9eda26510b3ffc6be862ea3e8f4a0b4.tar.gz cryptography-8f768dc5b9eda26510b3ffc6be862ea3e8f4a0b4.tar.bz2 cryptography-8f768dc5b9eda26510b3ffc6be862ea3e8f4a0b4.zip |
Merge pull request #2045 from sigmavirus24/csr-builder
Adds CSR Builder (Redux of #1927)
-rw-r--r-- | CHANGELOG.rst | 2 | ||||
-rw-r--r-- | docs/x509.rst | 70 | ||||
-rw-r--r-- | src/cryptography/hazmat/backends/interfaces.py | 6 | ||||
-rw-r--r-- | src/cryptography/hazmat/backends/multibackend.py | 9 | ||||
-rw-r--r-- | src/cryptography/hazmat/backends/openssl/backend.py | 165 | ||||
-rw-r--r-- | src/cryptography/x509.py | 41 | ||||
-rw-r--r-- | tests/hazmat/backends/test_multibackend.py | 6 | ||||
-rw-r--r-- | tests/hazmat/backends/test_openssl.py | 27 | ||||
-rw-r--r-- | tests/test_x509.py | 220 |
9 files changed, 543 insertions, 3 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5c9d08ea..bc95cf74 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -24,6 +24,8 @@ Changelog and :class:`~cryptography.hazmat.primitives.kdf.concatkdf.ConcatKDFHMAC`. * Raise a ``TypeError`` when passing objects that are not text as the value to :class:`~cryptography.x509.NameAttribute`. +* Add support for creating certificate signing requests with + :class:`~cryptography.x509.CertificateSigningRequestBuilder`. 0.9.1 - 2015-06-06 ~~~~~~~~~~~~~~~~~~ diff --git a/docs/x509.rst b/docs/x509.rst index b8e3c8ee..c4c441e7 100644 --- a/docs/x509.rst +++ b/docs/x509.rst @@ -468,6 +468,76 @@ X.509 Revoked Certificate Object The extensions encoded in the revoked certificate. +X.509 CSR (Certificate Signing Request) Builder Object +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. class:: CertificateSigningRequestBuilder + + .. versionadded:: 1.0 + + .. 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 + >>> private_key = rsa.generate_private_key( + ... public_exponent=65537, + ... key_size=2048, + ... backend=default_backend() + ... ) + >>> builder = x509.CertificateSigningRequestBuilder() + >>> builder = builder.subject_name(x509.Name([ + ... x509.NameAttribute(x509.OID_COMMON_NAME, u'cryptography.io'), + ... ])) + >>> builder = builder.add_extension( + ... x509.BasicConstraints(ca=False, path_length=None), critical=True, + ... ) + >>> request = builder.sign( + ... default_backend(), private_key, hashes.SHA256() + ... ) + >>> isinstance(request, x509.CertificateSigningRequest) + True + + .. method:: subject_name(name) + + :param name: The :class:`~cryptography.x509.Name` of the certificate + subject. + :returns: A new + :class:`~cryptography.x509.CertificateSigningRequestBuilder`. + + .. method:: add_extension(extension, critical) + + :param extension: The :class:`~cryptography.x509.Extension` to add to + the request. + :param critical: Set to `True` if the extension must be understood and + handled by whoever reads the certificate. + :returns: A new + :class:`~cryptography.x509.CertificateSigningRequestBuilder`. + + .. method:: sign(backend, private_key, algorithm) + + :param backend: Backend that will be used to sign the request. + Must support the + :class:`~cryptography.hazmat.backends.interfaces.X509Backend` + interface. + + :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 request. When the request is + signed by a certificate authority, the private key's associated + public key will be stored in the resulting certificate. + + :param algorithm: The + :class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm` + that will be used to generate the request signature. + + :returns: A new + :class:`~cryptography.x509.CertificateSigningRequest`. + + .. class:: Name .. versionadded:: 0.8 diff --git a/src/cryptography/hazmat/backends/interfaces.py b/src/cryptography/hazmat/backends/interfaces.py index eca7ddf4..4d378e6b 100644 --- a/src/cryptography/hazmat/backends/interfaces.py +++ b/src/cryptography/hazmat/backends/interfaces.py @@ -274,6 +274,12 @@ class X509Backend(object): Load an X.509 CSR from PEM encoded data. """ + @abc.abstractmethod + def create_x509_csr(self, builder, private_key, algorithm): + """ + Create and sign an X.509 CSR from a CSR builder 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 784ab84d..6e911fd5 100644 --- a/src/cryptography/hazmat/backends/multibackend.py +++ b/src/cryptography/hazmat/backends/multibackend.py @@ -342,3 +342,12 @@ class MultiBackend(object): "This backend does not support X.509.", _Reasons.UNSUPPORTED_X509 ) + + def create_x509_csr(self, builder, private_key, algorithm): + for b in self._filtered_backends(X509Backend): + return b.create_x509_csr(builder, private_key, algorithm) + + 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 4d469c40..78de79d1 100644 --- a/src/cryptography/hazmat/backends/openssl/backend.py +++ b/src/cryptography/hazmat/backends/openssl/backend.py @@ -10,7 +10,7 @@ from contextlib import contextmanager import six -from cryptography import utils +from cryptography import utils, x509 from cryptography.exceptions import ( InternalError, UnsupportedAlgorithm, _Reasons ) @@ -56,6 +56,96 @@ _OpenSSLError = collections.namedtuple("_OpenSSLError", ["code", "lib", "func", "reason"]) +def _encode_asn1_int(backend, x): + """ + Converts a python integer to a ASN1_INTEGER. The returned ASN1_INTEGER will + not be garbage collected (to support adding them to structs that take + ownership of the object). Be sure to register it for GC if it will be + discarded after use. + + """ + # Convert Python integer to OpenSSL "bignum" in case value exceeds + # machine's native integer limits (note: `int_to_bn` doesn't automatically + # GC). + i = backend._int_to_bn(x) + i = backend._ffi.gc(i, backend._lib.BN_free) + + # Wrap in a ASN.1 integer. Don't GC -- as documented. + i = backend._lib.BN_to_ASN1_INTEGER(i, backend._ffi.NULL) + assert i != backend._ffi.NULL + return i + + +def _encode_asn1_str(backend, data, length): + """ + Create an ASN1_OCTET_STRING from a Python byte string. + """ + s = backend._lib.ASN1_OCTET_STRING_new() + s = backend._ffi.gc(s, backend._lib.ASN1_OCTET_STRING_free) + backend._lib.ASN1_OCTET_STRING_set(s, data, length) + return s + + +def _encode_name(backend, attributes): + subject = backend._lib.X509_NAME_new() + subject = backend._ffi.gc(subject, backend._lib.X509_NAME_free) + for attribute in attributes: + value = attribute.value.encode('utf8') + obj = _txt2obj(backend, attribute.oid.dotted_string) + res = backend._lib.X509_NAME_add_entry_by_OBJ( + subject, + obj, + backend._lib.MBSTRING_UTF8, + value, + -1, -1, 0, + ) + assert res == 1 + return subject + + +def _txt2obj(backend, name): + """ + Converts a Python string with an ASN.1 object ID in dotted form to a + ASN1_OBJECT. + """ + name = name.encode('ascii') + obj = backend._lib.OBJ_txt2obj(name, 1) + assert obj != backend._ffi.NULL + obj = backend._ffi.gc(obj, backend._lib.ASN1_OBJECT_free) + return obj + + +def _encode_basic_constraints(backend, basic_constraints, critical): + obj = _txt2obj(backend, x509.OID_BASIC_CONSTRAINTS.dotted_string) + assert obj is not None + constraints = backend._lib.BASIC_CONSTRAINTS_new() + constraints.ca = 255 if basic_constraints.ca else 0 + if basic_constraints.ca: + constraints.pathlen = _encode_asn1_int( + backend, basic_constraints.path_length + ) + + # Fetch the encoded payload. + pp = backend._ffi.new('unsigned char **') + r = backend._lib.i2d_BASIC_CONSTRAINTS(constraints, pp) + assert r > 0 + pp = backend._ffi.gc( + pp, lambda pointer: backend._lib.OPENSSL_free(pointer[0]) + ) + + # Wrap that in an X509 extension object. + extension = backend._lib.X509_EXTENSION_create_by_OBJ( + backend._ffi.NULL, + obj, + 1 if critical else 0, + _encode_asn1_str(backend, pp[0], r), + ) + assert extension != backend._ffi.NULL + + # Return the wrapped extension. + return extension + + @utils.register_interface(CipherBackend) @utils.register_interface(CMACBackend) @utils.register_interface(DERSerializationBackend) @@ -710,6 +800,79 @@ class Backend(object): def create_cmac_ctx(self, algorithm): return _CMACContext(self, algorithm) + def create_x509_csr(self, builder, private_key, algorithm): + 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( + "Certificate signing requests aren't implemented for DSA" + " keys on OpenSSL versions less than 1.0.1." + ) + if isinstance(private_key, _EllipticCurvePrivateKey): + raise NotImplementedError( + "Certificate signing requests aren't implemented for EC" + " keys on OpenSSL versions less than 1.0.1." + ) + + # Resolve the signature algorithm. + evp_md = self._lib.EVP_get_digestbyname( + algorithm.name.encode('ascii') + ) + assert evp_md != self._ffi.NULL + + # Create an empty request. + x509_req = self._lib.X509_REQ_new() + assert x509_req != self._ffi.NULL + x509_req = self._ffi.gc(x509_req, self._lib.X509_REQ_free) + + # Set x509 version. + res = self._lib.X509_REQ_set_version(x509_req, x509.Version.v1.value) + assert res == 1 + + # Set subject name. + res = self._lib.X509_REQ_set_subject_name( + x509_req, _encode_name(self, list(builder._subject_name)) + ) + assert res == 1 + + # Set subject public key. + public_key = private_key.public_key() + res = self._lib.X509_REQ_set_pubkey( + x509_req, public_key._evp_pkey + ) + assert res == 1 + + # Add extensions. + extensions = self._lib.sk_X509_EXTENSION_new_null() + assert extensions != self._ffi.NULL + extensions = self._ffi.gc( + extensions, + self._lib.sk_X509_EXTENSION_free, + ) + for extension in builder._extensions: + if isinstance(extension.value, x509.BasicConstraints): + extension = _encode_basic_constraints( + self, + extension.value, + extension.critical + ) + else: + raise NotImplementedError('Extension not yet supported.') + res = self._lib.sk_X509_EXTENSION_push(extensions, extension) + assert res == 1 + res = self._lib.X509_REQ_add_extensions(x509_req, extensions) + assert res == 1 + + # Sign the request using the requester's private key. + res = self._lib.X509_REQ_sign( + x509_req, private_key._evp_pkey, evp_md + ) + assert res > 0 + + return _CertificateSigningRequest(self, x509_req) + def load_pem_private_key(self, data, password): return self._load_key( self._lib.PEM_read_bio_PrivateKey, diff --git a/src/cryptography/x509.py b/src/cryptography/x509.py index 4b030ca9..21e18ddd 100644 --- a/src/cryptography/x509.py +++ b/src/cryptography/x509.py @@ -1442,3 +1442,44 @@ class RevokedCertificate(object): """ Returns an Extensions object containing a list of Revoked extensions. """ + + +class CertificateSigningRequestBuilder(object): + def __init__(self, subject_name=None, extensions=[]): + """ + Creates an empty X.509 certificate request (v1). + """ + self._subject_name = subject_name + self._extensions = extensions + + def subject_name(self, name): + """ + Sets the certificate requestor's distinguished name. + """ + if not isinstance(name, Name): + raise TypeError('Expecting x509.Name object.') + if self._subject_name is not None: + raise ValueError('The subject name may only be set once.') + return CertificateSigningRequestBuilder(name, self._extensions) + + def add_extension(self, extension, critical): + """ + Adds an X.509 extension to the certificate request. + """ + if isinstance(extension, BasicConstraints): + extension = Extension(OID_BASIC_CONSTRAINTS, critical, extension) + else: + raise NotImplementedError('Unsupported X.509 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 CertificateSigningRequestBuilder( + self._subject_name, self._extensions + [extension] + ) + + def sign(self, backend, private_key, algorithm): + """ + Signs the request using the requestor's private key. + """ + return backend.create_x509_csr(self, private_key, algorithm) diff --git a/tests/hazmat/backends/test_multibackend.py b/tests/hazmat/backends/test_multibackend.py index 5871e6c8..3c05cdfa 100644 --- a/tests/hazmat/backends/test_multibackend.py +++ b/tests/hazmat/backends/test_multibackend.py @@ -203,6 +203,9 @@ class DummyX509Backend(object): def load_der_x509_csr(self, data): pass + def create_x509_csr(self, builder, private_key, algorithm): + pass + class TestMultiBackend(object): def test_ciphers(self): @@ -480,6 +483,7 @@ class TestMultiBackend(object): backend.load_der_x509_certificate(b"certdata") backend.load_pem_x509_csr(b"reqdata") backend.load_der_x509_csr(b"reqdata") + backend.create_x509_csr(object(), b"privatekey", hashes.SHA1()) backend = MultiBackend([]) with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_X509): @@ -490,3 +494,5 @@ class TestMultiBackend(object): backend.load_pem_x509_csr(b"reqdata") with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_X509): backend.load_der_x509_csr(b"reqdata") + with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_X509): + backend.create_x509_csr(object(), b"privatekey", hashes.SHA1()) diff --git a/tests/hazmat/backends/test_openssl.py b/tests/hazmat/backends/test_openssl.py index b35e7670..34fff277 100644 --- a/tests/hazmat/backends/test_openssl.py +++ b/tests/hazmat/backends/test_openssl.py @@ -21,14 +21,16 @@ from cryptography.hazmat.backends.openssl.backend import ( ) from cryptography.hazmat.backends.openssl.ec import _sn_to_elliptic_curve from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import dsa, padding +from cryptography.hazmat.primitives.asymmetric import dsa, ec, padding from cryptography.hazmat.primitives.ciphers import ( BlockCipherAlgorithm, Cipher, CipherAlgorithm ) from cryptography.hazmat.primitives.ciphers.algorithms import AES from cryptography.hazmat.primitives.ciphers.modes import CBC, CTR, Mode +from ..primitives.fixtures_dsa import DSA_KEY_2048 from ..primitives.fixtures_rsa import RSA_KEY_2048, RSA_KEY_512 +from ..primitives.test_ec import _skip_curve_unsupported from ...utils import load_vectors_from_file, raises_unsupported_algorithm @@ -453,6 +455,29 @@ class TestOpenSSLCMAC(object): backend.create_cmac_ctx(FakeAlgorithm()) +class TestOpenSSLCreateX509CSR(object): + @pytest.mark.skipif( + backend._lib.OPENSSL_VERSION_NUMBER >= 0x10001000, + reason="Requires an older OpenSSL. Must be < 1.0.1" + ) + def test_unsupported_dsa_keys(self): + private_key = DSA_KEY_2048.private_key(backend) + + with pytest.raises(NotImplementedError): + backend.create_x509_csr(object(), private_key, hashes.SHA1()) + + @pytest.mark.skipif( + backend._lib.OPENSSL_VERSION_NUMBER >= 0x10001000, + reason="Requires an older OpenSSL. Must be < 1.0.1" + ) + def test_unsupported_ec_keys(self): + _skip_curve_unsupported(backend, ec.SECP256R1()) + private_key = ec.generate_private_key(ec.SECP256R1(), backend) + + with pytest.raises(NotImplementedError): + backend.create_x509_csr(object(), private_key, hashes.SHA1()) + + class TestOpenSSLSerialisationWithOpenSSL(object): def test_pem_password_cb_buffer_too_small(self): ffi_cb, cb = backend._pem_password_cb(b"aa") diff --git a/tests/test_x509.py b/tests/test_x509.py index cf3499bf..429f2d25 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -20,6 +20,8 @@ from cryptography.hazmat.backends.interfaces import ( from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import dsa, ec, rsa +from .hazmat.primitives.fixtures_dsa import DSA_KEY_2048 +from .hazmat.primitives.fixtures_rsa import RSA_KEY_2048 from .hazmat.primitives.test_ec import _skip_curve_unsupported from .utils import load_vectors_from_file @@ -586,7 +588,7 @@ class TestRSACertificateRequest(object): x509.Extension( x509.OID_BASIC_CONSTRAINTS, True, - x509.BasicConstraints(True, 1), + x509.BasicConstraints(ca=True, path_length=1), ), ] @@ -679,6 +681,222 @@ class TestRSACertificateRequest(object): assert serialized == request_bytes +@pytest.mark.requires_backend_interface(interface=X509Backend) +class TestCertificateSigningRequestBuilder(object): + @pytest.mark.requires_backend_interface(interface=RSABackend) + def test_sign_invalid_hash_algorithm(self, backend): + private_key = RSA_KEY_2048.private_key(backend) + + builder = x509.CertificateSigningRequestBuilder() + with pytest.raises(TypeError): + builder.sign(backend, private_key, 'NotAHash') + + @pytest.mark.requires_backend_interface(interface=RSABackend) + def test_build_ca_request_with_rsa(self, backend): + private_key = RSA_KEY_2048.private_key(backend) + + request = x509.CertificateSigningRequestBuilder().subject_name( + x509.Name([ + x509.NameAttribute(x509.OID_COUNTRY_NAME, u'US'), + x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, u'Texas'), + x509.NameAttribute(x509.OID_LOCALITY_NAME, u'Austin'), + x509.NameAttribute(x509.OID_ORGANIZATION_NAME, u'PyCA'), + x509.NameAttribute(x509.OID_COMMON_NAME, u'cryptography.io'), + ]) + ).add_extension( + x509.BasicConstraints(ca=True, path_length=2), critical=True + ).sign( + backend, private_key, hashes.SHA1() + ) + + assert isinstance(request.signature_hash_algorithm, hashes.SHA1) + public_key = request.public_key() + assert isinstance(public_key, rsa.RSAPublicKey) + subject = request.subject + assert isinstance(subject, x509.Name) + assert list(subject) == [ + x509.NameAttribute(x509.OID_COUNTRY_NAME, u'US'), + x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, u'Texas'), + x509.NameAttribute(x509.OID_LOCALITY_NAME, u'Austin'), + x509.NameAttribute(x509.OID_ORGANIZATION_NAME, u'PyCA'), + x509.NameAttribute(x509.OID_COMMON_NAME, u'cryptography.io'), + ] + basic_constraints = request.extensions.get_extension_for_oid( + x509.OID_BASIC_CONSTRAINTS + ) + assert basic_constraints.value.ca is True + assert basic_constraints.value.path_length == 2 + + @pytest.mark.requires_backend_interface(interface=RSABackend) + def test_build_ca_request_with_unicode(self, backend): + private_key = RSA_KEY_2048.private_key(backend) + + request = x509.CertificateSigningRequestBuilder().subject_name( + x509.Name([ + x509.NameAttribute(x509.OID_COUNTRY_NAME, u'US'), + x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, u'Texas'), + x509.NameAttribute(x509.OID_LOCALITY_NAME, u'Austin'), + x509.NameAttribute(x509.OID_ORGANIZATION_NAME, + u'PyCA\U0001f37a'), + x509.NameAttribute(x509.OID_COMMON_NAME, u'cryptography.io'), + ]) + ).add_extension( + x509.BasicConstraints(ca=True, path_length=2), critical=True + ).sign( + backend, private_key, hashes.SHA1() + ) + + loaded_request = x509.load_pem_x509_csr( + request.public_bytes(encoding=serialization.Encoding.PEM), backend + ) + subject = loaded_request.subject + assert isinstance(subject, x509.Name) + assert list(subject) == [ + x509.NameAttribute(x509.OID_COUNTRY_NAME, u'US'), + x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, u'Texas'), + x509.NameAttribute(x509.OID_LOCALITY_NAME, u'Austin'), + x509.NameAttribute(x509.OID_ORGANIZATION_NAME, u'PyCA\U0001f37a'), + x509.NameAttribute(x509.OID_COMMON_NAME, u'cryptography.io'), + ] + + @pytest.mark.requires_backend_interface(interface=RSABackend) + def test_build_nonca_request_with_rsa(self, backend): + private_key = RSA_KEY_2048.private_key(backend) + + request = x509.CertificateSigningRequestBuilder().subject_name( + x509.Name([ + x509.NameAttribute(x509.OID_COUNTRY_NAME, u'US'), + x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, u'Texas'), + x509.NameAttribute(x509.OID_LOCALITY_NAME, u'Austin'), + x509.NameAttribute(x509.OID_ORGANIZATION_NAME, u'PyCA'), + x509.NameAttribute(x509.OID_COMMON_NAME, u'cryptography.io'), + ]) + ).add_extension( + x509.BasicConstraints(ca=False, path_length=None), critical=True, + ).sign( + backend, private_key, hashes.SHA1() + ) + + assert isinstance(request.signature_hash_algorithm, hashes.SHA1) + public_key = request.public_key() + assert isinstance(public_key, rsa.RSAPublicKey) + subject = request.subject + assert isinstance(subject, x509.Name) + assert list(subject) == [ + x509.NameAttribute(x509.OID_COUNTRY_NAME, u'US'), + x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, u'Texas'), + x509.NameAttribute(x509.OID_LOCALITY_NAME, u'Austin'), + x509.NameAttribute(x509.OID_ORGANIZATION_NAME, u'PyCA'), + x509.NameAttribute(x509.OID_COMMON_NAME, u'cryptography.io'), + ] + basic_constraints = request.extensions.get_extension_for_oid( + x509.OID_BASIC_CONSTRAINTS + ) + assert basic_constraints.value.ca is False + assert basic_constraints.value.path_length is None + + @pytest.mark.requires_backend_interface(interface=EllipticCurveBackend) + def test_build_ca_request_with_ec(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) + + request = x509.CertificateSigningRequestBuilder().subject_name( + x509.Name([ + x509.NameAttribute(x509.OID_COUNTRY_NAME, u'US'), + x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, u'Texas'), + x509.NameAttribute(x509.OID_LOCALITY_NAME, u'Austin'), + x509.NameAttribute(x509.OID_ORGANIZATION_NAME, u'PyCA'), + x509.NameAttribute(x509.OID_COMMON_NAME, u'cryptography.io'), + ]) + ).add_extension( + x509.BasicConstraints(ca=True, path_length=2), critical=True + ).sign( + backend, private_key, hashes.SHA1() + ) + + assert isinstance(request.signature_hash_algorithm, hashes.SHA1) + public_key = request.public_key() + assert isinstance(public_key, ec.EllipticCurvePublicKey) + subject = request.subject + assert isinstance(subject, x509.Name) + assert list(subject) == [ + x509.NameAttribute(x509.OID_COUNTRY_NAME, u'US'), + x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, u'Texas'), + x509.NameAttribute(x509.OID_LOCALITY_NAME, u'Austin'), + x509.NameAttribute(x509.OID_ORGANIZATION_NAME, u'PyCA'), + x509.NameAttribute(x509.OID_COMMON_NAME, u'cryptography.io'), + ] + basic_constraints = request.extensions.get_extension_for_oid( + x509.OID_BASIC_CONSTRAINTS + ) + assert basic_constraints.value.ca is True + assert basic_constraints.value.path_length == 2 + + @pytest.mark.requires_backend_interface(interface=DSABackend) + def test_build_ca_request_with_dsa(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) + + request = x509.CertificateSigningRequestBuilder().subject_name( + x509.Name([ + x509.NameAttribute(x509.OID_COUNTRY_NAME, u'US'), + x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, u'Texas'), + x509.NameAttribute(x509.OID_LOCALITY_NAME, u'Austin'), + x509.NameAttribute(x509.OID_ORGANIZATION_NAME, u'PyCA'), + x509.NameAttribute(x509.OID_COMMON_NAME, u'cryptography.io'), + ]) + ).add_extension( + x509.BasicConstraints(ca=True, path_length=2), critical=True + ).sign( + backend, private_key, hashes.SHA1() + ) + + assert isinstance(request.signature_hash_algorithm, hashes.SHA1) + public_key = request.public_key() + assert isinstance(public_key, dsa.DSAPublicKey) + subject = request.subject + assert isinstance(subject, x509.Name) + assert list(subject) == [ + x509.NameAttribute(x509.OID_COUNTRY_NAME, u'US'), + x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, u'Texas'), + x509.NameAttribute(x509.OID_LOCALITY_NAME, u'Austin'), + x509.NameAttribute(x509.OID_ORGANIZATION_NAME, u'PyCA'), + x509.NameAttribute(x509.OID_COMMON_NAME, u'cryptography.io'), + ] + basic_constraints = request.extensions.get_extension_for_oid( + x509.OID_BASIC_CONSTRAINTS + ) + assert basic_constraints.value.ca is True + assert basic_constraints.value.path_length == 2 + + def test_add_duplicate_extension(self, backend): + builder = x509.CertificateSigningRequestBuilder().add_extension( + x509.BasicConstraints(True, 2), critical=True, + ) + with pytest.raises(ValueError): + builder.add_extension( + x509.BasicConstraints(True, 2), critical=True, + ) + + def test_set_invalid_subject(self, backend): + builder = x509.CertificateSigningRequestBuilder() + with pytest.raises(TypeError): + builder.subject_name('NotAName') + + def test_add_unsupported_extension(self, backend): + builder = x509.CertificateSigningRequestBuilder() + with pytest.raises(NotImplementedError): + builder.add_extension( + x509.AuthorityKeyIdentifier('keyid', None, None), + critical=False, + ) + + @pytest.mark.requires_backend_interface(interface=DSABackend) @pytest.mark.requires_backend_interface(interface=X509Backend) class TestDSACertificate(object): |