aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPaul Kehrer <paul.l.kehrer@gmail.com>2015-08-03 16:04:06 +0100
committerPaul Kehrer <paul.l.kehrer@gmail.com>2015-08-03 16:04:06 +0100
commitb69aa4cdacb6e85845f590e528f7e9a471d7dd36 (patch)
tree943886277c3549cc62c6f5a40d7a5d7d9a274061
parent93b5e3efab0337c51c84c27208d9034b607f09e2 (diff)
parent46479d0d21755b47e14659ecd73bb0caf4074268 (diff)
downloadcryptography-b69aa4cdacb6e85845f590e528f7e9a471d7dd36.tar.gz
cryptography-b69aa4cdacb6e85845f590e528f7e9a471d7dd36.tar.bz2
cryptography-b69aa4cdacb6e85845f590e528f7e9a471d7dd36.zip
Merge pull request #2098 from sigmavirus24/cert-builder
Adds certificate builder.
-rw-r--r--CHANGELOG.rst7
-rw-r--r--docs/x509/reference.rst134
-rw-r--r--src/cryptography/hazmat/backends/interfaces.py6
-rw-r--r--src/cryptography/hazmat/backends/multibackend.py9
-rw-r--r--src/cryptography/hazmat/backends/openssl/backend.py107
-rw-r--r--src/cryptography/x509.py147
-rw-r--r--tests/hazmat/backends/test_multibackend.py8
-rw-r--r--tests/hazmat/backends/test_openssl.py29
-rw-r--r--tests/test_x509.py346
9 files changed, 792 insertions, 1 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 4506a466..f07a9087 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -45,6 +45,13 @@ Changelog
* :class:`~cryptography.x509.KeyUsage`
* :class:`~cryptography.x509.SubjectAlternativeName`
+* Add support for creating signed certificates with
+ :class:`~cryptography.x509.CertificateBuilder`. This includes support for
+ the following extensions
+
+ * :class:`~cryptography.x509.BasicConstraints`
+ * :class:`~cryptography.x509.SubjectAlternativeName`
+
0.9.3 - 2015-07-09
~~~~~~~~~~~~~~~~~~
diff --git a/docs/x509/reference.rst b/docs/x509/reference.rst
index c7d45c77..799126b9 100644
--- a/docs/x509/reference.rst
+++ b/docs/x509/reference.rst
@@ -388,6 +388,140 @@ X.509 CRL (Certificate Revocation List) Object
The extensions encoded in the CRL.
+X.509 Certificate Builder
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. class:: CertificateBuilder
+
+ .. 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
+ >>> import datetime
+ >>> import uuid
+ >>> one_day = datetime.timedelta(1, 0, 0)
+ >>> private_key = rsa.generate_private_key(
+ ... public_exponent=65537,
+ ... key_size=2048,
+ ... backend=default_backend()
+ ... )
+ >>> public_key = rsa.generate_private_key(
+ ... public_exponent=65537,
+ ... key_size=2048,
+ ... backend=default_backend()
+ ... ).public_key()
+ >>> builder = x509.CertificateBuilder()
+ >>> builder = builder.subject_name(x509.Name([
+ ... x509.NameAttribute(x509.OID_COMMON_NAME, u'cryptography.io'),
+ ... ]))
+ >>> builder = builder.issuer_name(x509.Name([
+ ... x509.NameAttribute(x509.OID_COMMON_NAME, u'cryptography.io'),
+ ... ]))
+ >>> builder = builder.not_valid_before(datetime.datetime.today() - one_day)
+ >>> builder = builder.not_valid_after(datetime.datetime(2018, 8, 2))
+ >>> builder = builder.serial_number(int(uuid.uuid4()))
+ >>> builder = builder.public_key(public_key)
+ >>> builder = builder.add_extension(
+ ... x509.BasicConstraints(ca=False, path_length=None), critical=True,
+ ... )
+ >>> certificate = builder.sign(
+ ... private_key=private_key, algorithm=hashes.SHA256(),
+ ... backend=default_backend()
+ ... )
+ >>> isinstance(certificate, x509.Certificate)
+ True
+
+ .. method:: issuer_name(name)
+
+ Sets the issuer's distinguished name.
+
+ :param name: The :class:`~cryptography.x509.Name` that describes the
+ issuer (CA).
+
+ .. method:: subject_name(name)
+
+ Sets the subject's distinguished name.
+
+ :param name: The :class:`~cryptography.x509.Name` that describes the
+ subject.
+
+ .. method:: public_key(public_key)
+
+ Sets the subject's public key.
+
+ :param public_key: The subject's public key. This can be one of
+ :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`,
+ :class:`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey` or
+ :class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey`
+
+ .. method:: 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). Users are encouraged to use a method of
+ generating 20 bytes of entropy, e.g., UUID4. For more information
+ on secure random number generation, see :doc:`/random-numbers`.
+
+ .. method:: 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 :class:`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:: 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 :class:`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, critical)
+
+ Adds an X.509 extension to the certificate.
+
+ :param extension: The extension to add to the certificate. Can be one
+ of :class:`~cryptography.x509.BasicConstraints` or
+ :class:`~cryptography.x509.SubjectAlternativeName`.
+
+ :param critical: Set to ``True`` if the extension must be understood and
+ handled by whoever reads the certificate.
+
+ .. method:: sign(backend, private_key, algorithm)
+
+ Sign the certificate using the CA's private key.
+
+ :param backend: Backend that will be used to build the certificate.
+ 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 certificate.
+
+ :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/interfaces.py b/src/cryptography/hazmat/backends/interfaces.py
index 4d378e6b..49ccda18 100644
--- a/src/cryptography/hazmat/backends/interfaces.py
+++ b/src/cryptography/hazmat/backends/interfaces.py
@@ -280,6 +280,12 @@ class X509Backend(object):
Create and sign an X.509 CSR from a CSR builder object.
"""
+ @abc.abstractmethod
+ def sign_x509_certificate(self, builder, private_key, algorithm):
+ """
+ Sign an X.509 Certificate from a CertificateBuilder 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 6e911fd5..8008989e 100644
--- a/src/cryptography/hazmat/backends/multibackend.py
+++ b/src/cryptography/hazmat/backends/multibackend.py
@@ -351,3 +351,12 @@ class MultiBackend(object):
"This backend does not support X.509.",
_Reasons.UNSUPPORTED_X509
)
+
+ def sign_x509_certificate(self, builder, private_key, algorithm):
+ for b in self._filtered_backends(X509Backend):
+ return b.sign_x509_certificate(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 5ab46d44..0176de21 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
@@ -78,6 +79,12 @@ def _encode_asn1_int(backend, x):
return i
+def _encode_asn1_int_gc(backend, x):
+ i = _encode_asn1_int(backend, x)
+ i = backend._ffi.gc(i, backend._lib.ASN1_INTEGER_free)
+ return i
+
+
def _encode_asn1_str(backend, data, length):
"""
Create an ASN1_OCTET_STRING from a Python byte string.
@@ -1063,6 +1070,106 @@ class Backend(object):
return _CertificateSigningRequest(self, x509_req)
+ def sign_x509_certificate(self, builder, private_key, algorithm):
+ 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.')
+
+ if self._lib.OPENSSL_VERSION_NUMBER <= 0x10001000:
+ if isinstance(private_key, _DSAPrivateKey):
+ raise NotImplementedError(
+ "Certificate signatures aren't implemented for DSA"
+ " keys on OpenSSL versions less than 1.0.1."
+ )
+ if isinstance(private_key, _EllipticCurvePrivateKey):
+ raise NotImplementedError(
+ "Certificate signatures 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 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 = _encode_asn1_int_gc(self, builder._serial_number)
+ res = self._lib.X509_set_serialNumber(x509_cert, serial_number)
+ assert res == 1
+
+ # 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):
+ pp, r = _encode_basic_constraints(self, extension.value)
+ elif isinstance(extension.value, x509.SubjectAlternativeName):
+ pp, r = _encode_subject_alt_name(self, extension.value)
+ else:
+ raise NotImplementedError('Extension not yet supported.')
+
+ obj = _txt2obj(self, extension.oid.dotted_string)
+ extension = self._lib.X509_EXTENSION_create_by_OBJ(
+ self._ffi.NULL,
+ obj,
+ 1 if extension.critical else 0,
+ _encode_asn1_str_gc(self, pp[0], r)
+ )
+ 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
+ )
+ if res == 0:
+ errors = self._consume_errors()
+ assert errors[0][1] == self._lib.ERR_LIB_RSA
+ assert errors[0][3] == self._lib.RSA_R_DIGEST_TOO_BIG_FOR_RSA_KEY
+ raise ValueError("Digest too big for RSA key")
+
+ 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 75552fc1..a831506e 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
@@ -17,6 +18,7 @@ from six.moves import urllib_parse
from cryptography import utils
from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.primitives.asymmetric import dsa, ec, rsa
_OID_NAMES = {
@@ -95,6 +97,8 @@ _GENERAL_NAMES = {
8: "registeredID",
}
+_UNIX_EPOCH = datetime.datetime(1970, 1, 1)
+
class Version(Enum):
v1 = 0
@@ -1598,3 +1602,146 @@ 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, issuer_name=None, subject_name=None,
+ public_key=None, serial_number=None, not_valid_before=None,
+ not_valid_after=None, extensions=[]):
+ self._version = Version.v3
+ self._issuer_name = issuer_name
+ self._subject_name = subject_name
+ self._public_key = public_key
+ self._serial_number = serial_number
+ self._not_valid_before = not_valid_before
+ self._not_valid_after = not_valid_after
+ self._extensions = extensions
+
+ def issuer_name(self, name):
+ """
+ Sets the CA's distinguished name.
+ """
+ if not isinstance(name, Name):
+ raise TypeError('Expecting x509.Name object.')
+ if self._issuer_name is not None:
+ raise ValueError('The issuer name may only be set once.')
+ return CertificateBuilder(
+ name, self._subject_name, self._public_key,
+ self._serial_number, self._not_valid_before,
+ self._not_valid_after, self._extensions
+ )
+
+ def subject_name(self, name):
+ """
+ Sets the 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 CertificateBuilder(
+ self._issuer_name, name, self._public_key,
+ self._serial_number, self._not_valid_before,
+ self._not_valid_after, self._extensions
+ )
+
+ def public_key(self, key):
+ """
+ Sets the requestor's public key (as found in the signing request).
+ """
+ if not isinstance(key, (dsa.DSAPublicKey, rsa.RSAPublicKey,
+ ec.EllipticCurvePublicKey)):
+ raise TypeError('Expecting one of DSAPublicKey, RSAPublicKey,'
+ ' or EllipticCurvePublicKey.')
+ if self._public_key is not None:
+ raise ValueError('The public key may only be set once.')
+ return CertificateBuilder(
+ self._issuer_name, self._subject_name, key,
+ self._serial_number, self._not_valid_before,
+ self._not_valid_after, self._extensions
+ )
+
+ def serial_number(self, number):
+ """
+ Sets the certificate serial number.
+ """
+ if not isinstance(number, six.integer_types):
+ raise TypeError('Serial number must be of integral type.')
+ if self._serial_number is not None:
+ raise ValueError('The serial number may only be set once.')
+ if number < 0:
+ raise ValueError('The serial number should be non-negative.')
+ if utils.bit_length(number) > 160: # As defined in RFC 5280
+ raise ValueError('The serial number should not be more than 160 '
+ 'bits.')
+ return CertificateBuilder(
+ self._issuer_name, self._subject_name,
+ self._public_key, number, self._not_valid_before,
+ self._not_valid_after, self._extensions
+ )
+
+ def not_valid_before(self, time):
+ """
+ Sets the certificate activation time.
+ """
+ if not isinstance(time, datetime.datetime):
+ raise TypeError('Expecting datetime object.')
+ if self._not_valid_before is not None:
+ raise ValueError('The not valid before may only be set once.')
+ if time <= _UNIX_EPOCH:
+ raise ValueError('The not valid before date must be after the unix'
+ ' epoch (1970 January 1).')
+ return CertificateBuilder(
+ self._issuer_name, self._subject_name,
+ self._public_key, self._serial_number, time,
+ self._not_valid_after, self._extensions
+ )
+
+ def not_valid_after(self, time):
+ """
+ Sets the certificate expiration time.
+ """
+ if not isinstance(time, datetime.datetime):
+ raise TypeError('Expecting datetime object.')
+ if self._not_valid_after is not None:
+ raise ValueError('The not valid after may only be set once.')
+ if time <= _UNIX_EPOCH:
+ raise ValueError('The not valid after date must be after the unix'
+ ' epoch (1970 January 1).')
+ return CertificateBuilder(
+ self._issuer_name, self._subject_name,
+ self._public_key, self._serial_number, self._not_valid_before,
+ time, self._extensions
+ )
+
+ def add_extension(self, extension, critical):
+ """
+ Adds an X.509 extension to the certificate.
+ """
+ if isinstance(extension, BasicConstraints):
+ extension = Extension(OID_BASIC_CONSTRAINTS, critical, extension)
+ elif isinstance(extension, SubjectAlternativeName):
+ extension = Extension(
+ OID_SUBJECT_ALTERNATIVE_NAME, critical, extension
+ )
+ elif isinstance(extension, InhibitAnyPolicy):
+ extension = Extension(OID_INHIBIT_ANY_POLICY, 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 CertificateBuilder(
+ self._issuer_name, self._subject_name,
+ self._public_key, self._serial_number, self._not_valid_before,
+ self._not_valid_after, self._extensions + [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/hazmat/backends/test_multibackend.py b/tests/hazmat/backends/test_multibackend.py
index 3c05cdfa..d516af16 100644
--- a/tests/hazmat/backends/test_multibackend.py
+++ b/tests/hazmat/backends/test_multibackend.py
@@ -206,6 +206,9 @@ class DummyX509Backend(object):
def create_x509_csr(self, builder, private_key, algorithm):
pass
+ def sign_x509_certificate(self, builder, private_key, algorithm):
+ pass
+
class TestMultiBackend(object):
def test_ciphers(self):
@@ -484,6 +487,7 @@ class TestMultiBackend(object):
backend.load_pem_x509_csr(b"reqdata")
backend.load_der_x509_csr(b"reqdata")
backend.create_x509_csr(object(), b"privatekey", hashes.SHA1())
+ backend.sign_x509_certificate(object(), b"privatekey", hashes.SHA1())
backend = MultiBackend([])
with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_X509):
@@ -496,3 +500,7 @@ class TestMultiBackend(object):
backend.load_der_x509_csr(b"reqdata")
with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_X509):
backend.create_x509_csr(object(), b"privatekey", hashes.SHA1())
+ with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_X509):
+ backend.sign_x509_certificate(
+ object(), b"privatekey", hashes.SHA1()
+ )
diff --git a/tests/hazmat/backends/test_openssl.py b/tests/hazmat/backends/test_openssl.py
index 040e3677..c2a4f544 100644
--- a/tests/hazmat/backends/test_openssl.py
+++ b/tests/hazmat/backends/test_openssl.py
@@ -4,6 +4,7 @@
from __future__ import absolute_import, division, print_function
+import datetime
import os
import subprocess
import sys
@@ -14,6 +15,7 @@ import pretend
import pytest
from cryptography import utils
+from cryptography import x509
from cryptography.exceptions import InternalError, _Reasons
from cryptography.hazmat.backends.interfaces import RSABackend
from cryptography.hazmat.backends.openssl.backend import (
@@ -505,6 +507,33 @@ class TestOpenSSLCreateX509CSR(object):
backend.create_x509_csr(object(), private_key, hashes.SHA1())
+class TestOpenSSLSignX509Certificate(object):
+ def test_requires_certificate_builder(self):
+ private_key = RSA_KEY_2048.private_key(backend)
+
+ with pytest.raises(TypeError):
+ backend.sign_x509_certificate(object(), private_key, DummyHash())
+
+ def test_checks_for_unsupported_extensions(self):
+ private_key = RSA_KEY_2048.private_key(backend)
+ builder = x509.CertificateBuilder().subject_name(x509.Name([
+ x509.NameAttribute(x509.OID_COUNTRY_NAME, u'US'),
+ ])).public_key(
+ private_key.public_key()
+ ).serial_number(
+ 777
+ ).not_valid_before(
+ datetime.datetime(1999, 1, 1)
+ ).not_valid_after(
+ datetime.datetime(2020, 1, 1)
+ ).add_extension(
+ x509.InhibitAnyPolicy(0), False
+ )
+
+ with pytest.raises(NotImplementedError):
+ builder.sign(backend, 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 98cf49be..ac20f649 100644
--- a/tests/test_x509.py
+++ b/tests/test_x509.py
@@ -22,7 +22,7 @@ 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.fixtures_rsa import RSA_KEY_2048, RSA_KEY_512
from .hazmat.primitives.test_ec import _skip_curve_unsupported
from .utils import load_vectors_from_file
@@ -775,6 +775,350 @@ class TestRSACertificateRequest(object):
assert hash(request1) == hash(request2)
assert hash(request1) != hash(request3)
+ def test_build_cert(self, backend):
+ issuer_private_key = RSA_KEY_2048.private_key(backend)
+ subject_private_key = RSA_KEY_2048.private_key(backend)
+
+ not_valid_before = datetime.datetime(2002, 1, 1, 12, 1)
+ not_valid_after = datetime.datetime(2030, 12, 31, 8, 30)
+
+ builder = x509.CertificateBuilder().serial_number(
+ 777
+ ).issuer_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'),
+ ])).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'),
+ ])).public_key(
+ subject_private_key.public_key()
+ ).add_extension(
+ x509.BasicConstraints(ca=False, path_length=None), True,
+ ).add_extension(
+ x509.SubjectAlternativeName([x509.DNSName(u"cryptography.io")]),
+ critical=False,
+ ).not_valid_before(
+ not_valid_before
+ ).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
+ subject_alternative_name = cert.extensions.get_extension_for_oid(
+ x509.OID_SUBJECT_ALTERNATIVE_NAME
+ )
+ assert list(subject_alternative_name.value) == [
+ x509.DNSName(u"cryptography.io"),
+ ]
+
+
+class TestCertificateBuilder(object):
+ def test_issuer_name_must_be_a_name_type(self):
+ builder = x509.CertificateBuilder()
+
+ with pytest.raises(TypeError):
+ builder.issuer_name("subject")
+
+ with pytest.raises(TypeError):
+ builder.issuer_name(object)
+
+ def test_issuer_name_may_only_be_set_once(self):
+ name = x509.Name([
+ x509.NameAttribute(x509.OID_COUNTRY_NAME, u'US'),
+ ])
+ builder = x509.CertificateBuilder().issuer_name(name)
+
+ with pytest.raises(ValueError):
+ builder.issuer_name(name)
+
+ def test_subject_name_must_be_a_name_type(self):
+ builder = x509.CertificateBuilder()
+
+ with pytest.raises(TypeError):
+ builder.subject_name("subject")
+
+ with pytest.raises(TypeError):
+ builder.subject_name(object)
+
+ def test_subject_name_may_only_be_set_once(self):
+ name = x509.Name([
+ x509.NameAttribute(x509.OID_COUNTRY_NAME, u'US'),
+ ])
+ builder = x509.CertificateBuilder().subject_name(name)
+
+ with pytest.raises(ValueError):
+ builder.subject_name(name)
+
+ @pytest.mark.requires_backend_interface(interface=RSABackend)
+ @pytest.mark.requires_backend_interface(interface=X509Backend)
+ def test_public_key_must_be_public_key(self, backend):
+ private_key = RSA_KEY_2048.private_key(backend)
+ builder = x509.CertificateBuilder()
+
+ with pytest.raises(TypeError):
+ builder.public_key(private_key)
+
+ @pytest.mark.requires_backend_interface(interface=RSABackend)
+ @pytest.mark.requires_backend_interface(interface=X509Backend)
+ def test_public_key_may_only_be_set_once(self, backend):
+ private_key = RSA_KEY_2048.private_key(backend)
+ public_key = private_key.public_key()
+ builder = x509.CertificateBuilder().public_key(public_key)
+
+ with pytest.raises(ValueError):
+ builder.public_key(public_key)
+
+ def test_serial_number_must_be_an_integer_type(self):
+ with pytest.raises(TypeError):
+ x509.CertificateBuilder().serial_number(10.0)
+
+ def test_serial_number_must_be_non_negative(self):
+ with pytest.raises(ValueError):
+ x509.CertificateBuilder().serial_number(-10)
+
+ def test_serial_number_must_be_less_than_160_bits_long(self):
+ with pytest.raises(ValueError):
+ # 2 raised to the 160th power is actually 161 bits
+ x509.CertificateBuilder().serial_number(2 ** 160)
+
+ def test_serial_number_may_only_be_set_once(self):
+ builder = x509.CertificateBuilder().serial_number(10)
+
+ with pytest.raises(ValueError):
+ builder.serial_number(20)
+
+ def test_invalid_not_valid_after(self):
+ with pytest.raises(TypeError):
+ x509.CertificateBuilder().not_valid_after(104204304504)
+
+ with pytest.raises(TypeError):
+ x509.CertificateBuilder().not_valid_after(datetime.time())
+
+ with pytest.raises(ValueError):
+ x509.CertificateBuilder().not_valid_after(
+ datetime.datetime(1960, 8, 10)
+ )
+
+ def test_not_valid_after_may_only_be_set_once(self):
+ builder = x509.CertificateBuilder().not_valid_after(
+ datetime.datetime.now()
+ )
+
+ with pytest.raises(ValueError):
+ builder.not_valid_after(
+ datetime.datetime.now()
+ )
+
+ def test_invalid_not_valid_before(self):
+ with pytest.raises(TypeError):
+ x509.CertificateBuilder().not_valid_before(104204304504)
+
+ with pytest.raises(TypeError):
+ x509.CertificateBuilder().not_valid_before(datetime.time())
+
+ with pytest.raises(ValueError):
+ x509.CertificateBuilder().not_valid_before(
+ datetime.datetime(1960, 8, 10)
+ )
+
+ def test_not_valid_before_may_only_be_set_once(self):
+ builder = x509.CertificateBuilder().not_valid_before(
+ datetime.datetime.now()
+ )
+
+ with pytest.raises(ValueError):
+ builder.not_valid_before(
+ datetime.datetime.now()
+ )
+
+ def test_add_extension_checks_for_duplicates(self):
+ builder = x509.CertificateBuilder().add_extension(
+ x509.BasicConstraints(ca=False, path_length=None), True,
+ )
+
+ with pytest.raises(ValueError):
+ builder.add_extension(
+ x509.BasicConstraints(ca=False, path_length=None), True,
+ )
+
+ def test_add_unsupported_extension(self):
+ builder = x509.CertificateBuilder()
+
+ with pytest.raises(NotImplementedError):
+ builder.add_extension(object(), False)
+
+ @pytest.mark.requires_backend_interface(interface=RSABackend)
+ @pytest.mark.requires_backend_interface(interface=X509Backend)
+ def test_sign_with_unsupported_hash(self, backend):
+ private_key = RSA_KEY_2048.private_key(backend)
+ builder = x509.CertificateBuilder()
+
+ with pytest.raises(TypeError):
+ builder.sign(backend, private_key, object())
+
+ @pytest.mark.requires_backend_interface(interface=DSABackend)
+ @pytest.mark.requires_backend_interface(interface=X509Backend)
+ def test_sign_with_dsa_private_key_is_unsupported(self, backend):
+ if backend._lib.OPENSSL_VERSION_NUMBER >= 0x10001000:
+ pytest.skip("Requires an older OpenSSL. Must be < 1.0.1")
+
+ private_key = DSA_KEY_2048.private_key(backend)
+ builder = x509.CertificateBuilder()
+
+ with pytest.raises(NotImplementedError):
+ builder.sign(backend, private_key, hashes.SHA512())
+
+ @pytest.mark.requires_backend_interface(interface=EllipticCurveBackend)
+ @pytest.mark.requires_backend_interface(interface=X509Backend)
+ def test_sign_with_ec_private_key_is_unsupported(self, backend):
+ if backend._lib.OPENSSL_VERSION_NUMBER >= 0x10001000:
+ pytest.skip("Requires an older OpenSSL. Must be < 1.0.1")
+
+ _skip_curve_unsupported(backend, ec.SECP256R1())
+ private_key = ec.generate_private_key(ec.SECP256R1(), backend)
+ builder = x509.CertificateBuilder()
+
+ with pytest.raises(NotImplementedError):
+ builder.sign(backend, private_key, hashes.SHA512())
+
+ @pytest.mark.requires_backend_interface(interface=DSABackend)
+ @pytest.mark.requires_backend_interface(interface=X509Backend)
+ def test_build_cert_with_dsa_private_key(self, backend):
+ if backend._lib.OPENSSL_VERSION_NUMBER < 0x10001000:
+ pytest.skip("Requires a newer OpenSSL. Must be >= 1.0.1")
+
+ issuer_private_key = DSA_KEY_2048.private_key(backend)
+ subject_private_key = DSA_KEY_2048.private_key(backend)
+
+ not_valid_before = datetime.datetime(2002, 1, 1, 12, 1)
+ not_valid_after = datetime.datetime(2030, 12, 31, 8, 30)
+
+ builder = x509.CertificateBuilder().serial_number(
+ 777
+ ).issuer_name(x509.Name([
+ x509.NameAttribute(x509.OID_COUNTRY_NAME, u'US'),
+ ])).subject_name(x509.Name([
+ x509.NameAttribute(x509.OID_COUNTRY_NAME, u'US'),
+ ])).public_key(
+ subject_private_key.public_key()
+ ).add_extension(
+ x509.BasicConstraints(ca=False, path_length=None), True,
+ ).add_extension(
+ x509.SubjectAlternativeName([x509.DNSName(u"cryptography.io")]),
+ critical=False,
+ ).not_valid_before(
+ not_valid_before
+ ).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
+ subject_alternative_name = cert.extensions.get_extension_for_oid(
+ x509.OID_SUBJECT_ALTERNATIVE_NAME
+ )
+ assert list(subject_alternative_name.value) == [
+ x509.DNSName(u"cryptography.io"),
+ ]
+
+ @pytest.mark.requires_backend_interface(interface=EllipticCurveBackend)
+ @pytest.mark.requires_backend_interface(interface=X509Backend)
+ def test_build_cert_with_ec_private_key(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())
+ issuer_private_key = ec.generate_private_key(ec.SECP256R1(), backend)
+ subject_private_key = ec.generate_private_key(ec.SECP256R1(), backend)
+
+ not_valid_before = datetime.datetime(2002, 1, 1, 12, 1)
+ not_valid_after = datetime.datetime(2030, 12, 31, 8, 30)
+
+ builder = x509.CertificateBuilder().serial_number(
+ 777
+ ).issuer_name(x509.Name([
+ x509.NameAttribute(x509.OID_COUNTRY_NAME, u'US'),
+ ])).subject_name(x509.Name([
+ x509.NameAttribute(x509.OID_COUNTRY_NAME, u'US'),
+ ])).public_key(
+ subject_private_key.public_key()
+ ).add_extension(
+ x509.BasicConstraints(ca=False, path_length=None), True,
+ ).add_extension(
+ x509.SubjectAlternativeName([x509.DNSName(u"cryptography.io")]),
+ critical=False,
+ ).not_valid_before(
+ not_valid_before
+ ).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
+ subject_alternative_name = cert.extensions.get_extension_for_oid(
+ x509.OID_SUBJECT_ALTERNATIVE_NAME
+ )
+ assert list(subject_alternative_name.value) == [
+ x509.DNSName(u"cryptography.io"),
+ ]
+
+ @pytest.mark.requires_backend_interface(interface=RSABackend)
+ @pytest.mark.requires_backend_interface(interface=X509Backend)
+ def test_build_cert_with_rsa_key_too_small(self, backend):
+ issuer_private_key = RSA_KEY_512.private_key(backend)
+ subject_private_key = RSA_KEY_512.private_key(backend)
+
+ not_valid_before = datetime.datetime(2002, 1, 1, 12, 1)
+ not_valid_after = datetime.datetime(2030, 12, 31, 8, 30)
+
+ builder = x509.CertificateBuilder().serial_number(
+ 777
+ ).issuer_name(x509.Name([
+ x509.NameAttribute(x509.OID_COUNTRY_NAME, u'US'),
+ ])).subject_name(x509.Name([
+ x509.NameAttribute(x509.OID_COUNTRY_NAME, u'US'),
+ ])).public_key(
+ subject_private_key.public_key()
+ ).not_valid_before(
+ not_valid_before
+ ).not_valid_after(
+ not_valid_after
+ )
+
+ with pytest.raises(ValueError):
+ builder.sign(backend, issuer_private_key, hashes.SHA512())
+
@pytest.mark.requires_backend_interface(interface=X509Backend)
class TestCertificateSigningRequestBuilder(object):