aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAndre Caron <andre.l.caron@gmail.com>2015-05-18 20:55:29 -0400
committerIan Cordasco <graffatcolmingov@gmail.com>2015-07-18 21:52:27 -0500
commit9bbfcea022820e9783e22f5a8f1fe959c9b245eb (patch)
tree76875a811c7fb08af155d3c1c4eecdcf81ae21a2
parent32a92b6afaf0086f2b0e6b9cf7235576b06503b0 (diff)
downloadcryptography-9bbfcea022820e9783e22f5a8f1fe959c9b245eb.tar.gz
cryptography-9bbfcea022820e9783e22f5a8f1fe959c9b245eb.tar.bz2
cryptography-9bbfcea022820e9783e22f5a8f1fe959c9b245eb.zip
Adds certificate builder.
-rw-r--r--docs/x509/reference.rst83
-rw-r--r--src/cryptography/hazmat/backends/openssl/backend.py96
-rw-r--r--src/cryptography/x509.py90
-rw-r--r--tests/test_x509.py50
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):