aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorPaul Kehrer <paul.l.kehrer@gmail.com>2015-06-26 09:43:39 -0500
committerPaul Kehrer <paul.l.kehrer@gmail.com>2015-06-26 09:43:39 -0500
commit8f768dc5b9eda26510b3ffc6be862ea3e8f4a0b4 (patch)
tree73d3d549bb09cbe69ca275938217c1bab4ea2254 /src
parent77c98e3c4ef69d0cfee665cd0835670f4ac44242 (diff)
parent8cdcdfc1bd11ee57b7f53c631af2f88e0861d168 (diff)
downloadcryptography-8f768dc5b9eda26510b3ffc6be862ea3e8f4a0b4.tar.gz
cryptography-8f768dc5b9eda26510b3ffc6be862ea3e8f4a0b4.tar.bz2
cryptography-8f768dc5b9eda26510b3ffc6be862ea3e8f4a0b4.zip
Merge pull request #2045 from sigmavirus24/csr-builder
Adds CSR Builder (Redux of #1927)
Diffstat (limited to 'src')
-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.py165
-rw-r--r--src/cryptography/x509.py41
4 files changed, 220 insertions, 1 deletions
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)