diff options
author | Andre Caron <andre.l.caron@gmail.com> | 2015-05-18 20:55:29 -0400 |
---|---|---|
committer | Ian Cordasco <graffatcolmingov@gmail.com> | 2015-07-18 21:52:27 -0500 |
commit | 9bbfcea022820e9783e22f5a8f1fe959c9b245eb (patch) | |
tree | 76875a811c7fb08af155d3c1c4eecdcf81ae21a2 | |
parent | 32a92b6afaf0086f2b0e6b9cf7235576b06503b0 (diff) | |
download | cryptography-9bbfcea022820e9783e22f5a8f1fe959c9b245eb.tar.gz cryptography-9bbfcea022820e9783e22f5a8f1fe959c9b245eb.tar.bz2 cryptography-9bbfcea022820e9783e22f5a8f1fe959c9b245eb.zip |
Adds certificate builder.
-rw-r--r-- | docs/x509/reference.rst | 83 | ||||
-rw-r--r-- | src/cryptography/hazmat/backends/openssl/backend.py | 96 | ||||
-rw-r--r-- | src/cryptography/x509.py | 90 | ||||
-rw-r--r-- | tests/test_x509.py | 50 |
4 files changed, 319 insertions, 0 deletions
diff --git a/docs/x509/reference.rst b/docs/x509/reference.rst index 9179468f..65e3880d 100644 --- a/docs/x509/reference.rst +++ b/docs/x509/reference.rst @@ -388,6 +388,89 @@ X.509 CRL (Certificate Revocation List) Object The extensions encoded in the CRL. +X.509 Certificate Builder +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. class:: CertificateBuilder + + .. method:: __init__() + + Creates an empty certificate (version 1). + + .. method:: set_version(version) + + Sets the X.509 version that will be used in the certificate. + + :param version: The :class:`~cryptography.x509.Version` that will be + used by the certificate. + + .. method:: set_issuer_name(name) + + Sets the issuer's distinguished name. + + :param public_key: The :class:`~cryptography.x509.Name` that describes + the issuer (CA). + + .. method:: set_subject_name(name) + + Sets the subject's distinguished name. + + :param public_key: The :class:`~cryptography.x509.Name` that describes + the subject (requester). + + .. method:: set_public_key(public_key) + + Sets the subject's public key. + + :param public_key: The subject's public key. + + .. method:: set_serial_number(serial_number) + + Sets the certificate's serial number (an integer). The CA's policy + determines how it attributes serial numbers to certificates. The only + requirement is that this number uniquely identify the certificate given + the issuer. + + :param serial_number: Integer number that will be used by the CA to + identify this certificate (most notably during certificate + revocation checking). + + .. method:: set_not_valid_before(time) + + Sets the certificate's activation time. This is the time from which + clients can start trusting the certificate. It may be different from + the time at which the certificate was created. + + :param time: The `datetime.datetime` object (in UTC) that marks the + activation time for the certificate. The certificate may not be + trusted clients if it is used before this time. + + .. method:: set_not_valid_after(time) + + Sets the certificate's expiration time. This is the time from which + clients should no longer trust the certificate. The CA's policy will + determine how long the certificate should remain in use. + + :param time: The `datetime.datetime` object (in UTC) that marks the + expiration time for the certificate. The certificate may not be + trusted clients if it is used after this time. + + .. method:: add_extension(extension) + + Adds an X.509 extension to the certificate. + + :param extension: The :class:`~cryptography.x509.Extension` to add to + the certificate. + + .. method:: sign(backend, private_key, algorithm) + + Sign the certificate using the CA's private key. + + :param algorithm: The + :class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm` that + will be used to generate the signature. + + X.509 CSR (Certificate Signing Request) Object ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py index 7ccb39a4..04f631f9 100644 --- a/src/cryptography/hazmat/backends/openssl/backend.py +++ b/src/cryptography/hazmat/backends/openssl/backend.py @@ -4,6 +4,7 @@ from __future__ import absolute_import, division, print_function +import calendar import collections import itertools from contextlib import contextmanager @@ -94,6 +95,22 @@ def _encode_asn1_str_gc(backend, data, length): return s +def _make_asn1_int(backend, x): + i = backend._lib.ASN1_INTEGER_new() + # i = backend._ffi.gc(i, backend._lib.ASN1_INTEGER_free) + backend._lib.ASN1_INTEGER_set(i, x) + return i + + +def _make_asn1_str(backend, x, n=None): + if n is None: + n = len(x) + 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, x, n) + return s + + def _encode_name(backend, attributes): """ The X509_NAME created will not be gc'd. Use _encode_name_gc if needed. @@ -988,6 +1005,85 @@ class Backend(object): return _CertificateSigningRequest(self, x509_req) + def sign_x509_certificate(self, builder, private_key, algorithm): + # TODO: check type of private key parameter. + if not isinstance(builder, x509.CertificateBuilder): + raise TypeError('Builder type mismatch.') + if not isinstance(algorithm, hashes.HashAlgorithm): + raise TypeError('Algorithm must be a registered hash algorithm.') + + # Resolve the signature algorithm. + evp_md = self._lib.EVP_get_digestbyname( + algorithm.name.encode('ascii') + ) + assert evp_md != self._ffi.NULL + + # Create an empty certificate. + x509_cert = self._lib.X509_new() + x509_cert = self._ffi.gc(x509_cert, backend._lib.X509_free) + + # Set the x509 version. + res = self._lib.X509_set_version(x509_cert, builder._version.value) + assert res == 1 + + # Set the subject's name. + res = self._lib.X509_set_subject_name( + x509_cert, _encode_name(self, list(builder._subject_name)) + ) + assert res == 1 + + # Set the subject's public key. + res = self._lib.X509_set_pubkey( + x509_cert, builder._public_key._evp_pkey + ) + assert res == 1 + + # Set the certificate serial number. + serial_number = _make_asn1_int(self, builder._serial_number) + self._lib.X509_set_serialNumber(x509_cert, serial_number) + + # Set the "not before" time. + res = self._lib.ASN1_TIME_set( + self._lib.X509_get_notBefore(x509_cert), + calendar.timegm(builder._not_valid_before.timetuple()) + ) + assert res != self._ffi.NULL + + # Set the "not after" time. + res = self._lib.ASN1_TIME_set( + self._lib.X509_get_notAfter(x509_cert), + calendar.timegm(builder._not_valid_after.timetuple()) + ) + assert res != self._ffi.NULL + + # Add extensions. + for i, extension in enumerate(builder._extensions): + if isinstance(extension.value, x509.BasicConstraints): + extension = _encode_basic_constraints( + self, + extension.value.ca, + extension.value.path_length, + extension.critical + ) + else: + raise ValueError('Extension not yet supported.') + res = self._lib.X509_add_ext(x509_cert, extension, i) + assert res == 1 + + # Set the issuer name. + res = self._lib.X509_set_issuer_name( + x509_cert, _encode_name(self, list(builder._issuer_name)) + ) + assert res == 1 + + # Sign the certificate with the issuer's private key. + res = self._lib.X509_sign( + x509_cert, private_key._evp_pkey, evp_md + ) + assert res > 0 + + return _Certificate(self, x509_cert) + 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 58e1a37c..7cb42f57 100644 --- a/src/cryptography/x509.py +++ b/src/cryptography/x509.py @@ -5,6 +5,7 @@ from __future__ import absolute_import, division, print_function import abc +import datetime import ipaddress from email.utils import parseaddr from enum import Enum @@ -1594,3 +1595,92 @@ class CertificateSigningRequestBuilder(object): if self._subject_name is None: raise ValueError("A CertificateSigningRequest must have a subject") return backend.create_x509_csr(self, private_key, algorithm) + + +class CertificateBuilder(object): + def __init__(self): + """ + Creates an empty X.509 certificate (version 1). + """ + self._version = Version.v1 + self._issuer_name = None + self._subject_name = None + self._public_key = None + self._serial_number = None + self._not_valid_before = None + self._not_valid_after = None + self._extensions = [] + + def set_version(self, version): + """ + Sets the X.509 version required by decoders. + """ + if not isinstance(version, Version): + raise TypeError('Expecting x509.Version object.') + self._version = version + + def set_issuer_name(self, name): + """ + Sets the CA's distinguished name. + """ + if not isinstance(name, Name): + raise TypeError('Expecting x509.Name object.') + self._issuer_name = name + + def set_subject_name(self, name): + """ + Sets the requestor's distinguished name. + """ + if not isinstance(name, Name): + raise TypeError('Expecting x509.Name object.') + self._subject_name = name + + def set_public_key(self, public_key): + """ + Sets the requestor's public key (as found in the signing request). + """ + # TODO: check type. + self._public_key = public_key + + def set_serial_number(self, serial_number): + """ + Sets the certificate serial number. + """ + if not isinstance(serial_number, six.integer_types): + raise TypeError('Serial number must be of integral type.') + self._serial_number = serial_number + + def set_not_valid_before(self, time): + """ + Sets the certificate activation time. + """ + # TODO: require UTC datetime? + if not isinstance(time, datetime.datetime): + raise TypeError('Expecting datetime object.') + self._not_valid_before = time + + def set_not_valid_after(self, time): + """ + Sets the certificate expiration time. + """ + # TODO: require UTC datetime? + if not isinstance(time, datetime.datetime): + raise TypeError('Expecting datetime object.') + self._not_valid_after = time + + def add_extension(self, extension): + """ + Adds an X.509 extension to the certificate. + """ + if not isinstance(extension, Extension): + raise TypeError('Expecting x509.Extension object.') + for e in self._extensions: + if e.oid == extension.oid: + raise ValueError('This extension has already been set.') + self._extensions.append(extension) + + def sign(self, backend, private_key, algorithm): + """ + Signs the certificate using the CA's private key. + """ + return backend.sign_x509_certificate(self, private_key, algorithm) diff --git a/tests/test_x509.py b/tests/test_x509.py index 94eeab2b..92f40473 100644 --- a/tests/test_x509.py +++ b/tests/test_x509.py @@ -775,6 +775,56 @@ class TestRSACertificateRequest(object): assert hash(request1) == hash(request2) assert hash(request1) != hash(request3) + def test_build_cert(self, backend): + issuer_private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + backend=backend, + ) + subject_private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + backend=backend, + ) + + builder = x509.CertificateBuilder() + builder.set_version(x509.Version.v3) + builder.set_serial_number(777) + builder.set_issuer_name(x509.Name([ + x509.NameAttribute(x509.OID_COUNTRY_NAME, 'US'), + x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, 'Texas'), + x509.NameAttribute(x509.OID_LOCALITY_NAME, 'Austin'), + x509.NameAttribute(x509.OID_ORGANIZATION_NAME, 'PyCA'), + x509.NameAttribute(x509.OID_COMMON_NAME, 'cryptography.io'), + ])) + builder.set_subject_name(x509.Name([ + x509.NameAttribute(x509.OID_COUNTRY_NAME, 'US'), + x509.NameAttribute(x509.OID_STATE_OR_PROVINCE_NAME, 'Texas'), + x509.NameAttribute(x509.OID_LOCALITY_NAME, 'Austin'), + x509.NameAttribute(x509.OID_ORGANIZATION_NAME, 'PyCA'), + x509.NameAttribute(x509.OID_COMMON_NAME, 'cryptography.io'), + ])) + builder.set_public_key(subject_private_key.public_key()) + builder.add_extension(x509.Extension( + x509.OID_BASIC_CONSTRAINTS, + True, + x509.BasicConstraints(False, None), + )) + not_valid_before = datetime.datetime(2002, 1, 1, 12, 1) + not_valid_after = datetime.datetime(2030, 12, 31, 8, 30) + builder.set_not_valid_before(not_valid_before) + builder.set_not_valid_after(not_valid_after) + cert = builder.sign(backend, issuer_private_key, hashes.SHA1()) + + assert cert.version is x509.Version.v3 + assert cert.not_valid_before == not_valid_before + assert cert.not_valid_after == not_valid_after + basic_constraints = cert.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=X509Backend) class TestCertificateSigningRequestBuilder(object): |