Repository: pyca/cryptography

Issue:
Allow creation of custom x509 Extensions in CSR  
We need the possibility to add arbitrary x509 Extensions to a CSR and later allow (our) CA to sign that CSR and include these extensions in the cert.

As far as i can tell, this is currently not possible with the `x509.UnrecognizedExtension` (python 3.3.4, cryptography 1.2.2)

```
key = rsa.generate_private_key(
            public_exponent=65537,
            key_size=2048,
            backend=default_backend()
        )
custom_oid = x509.ObjectIdentifier('1.3.6.1.4.1.34380.1.1.13')
csr = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([
            # Provide various details about who we are.
            x509.NameAttribute(NameOID.COUNTRY_NAME, u"US"),
            x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u"CA"),
            x509.NameAttribute(NameOID.LOCALITY_NAME, u"San Francisco"),
            x509.NameAttribute(NameOID.ORGANIZATION_NAME, u"My Company"),
            x509.NameAttribute(NameOID.COMMON_NAME, u"mysite.com"),
        ])).add_extension(
                x509.UnrecognizedExtension(custom_oid, b'foobar'),
        critical=False,
        # Sign the CSR with our private key.
        ).sign(key, hashes.SHA256(), default_backend())
```

Adding the [custom extension](https://docs.puppetlabs.com/puppet/4.3/reference/ssl_attributes_extensions.html#puppet-specific-registered-ids) results in:

```
Traceback (most recent call last):
  File "/Users/foo/.pyenv/versions/3.3.4/lib/python3.3/site-packages/cryptography/hazmat/backends/openssl/backend.py", line 1552, in _create_x509_extensions
    encode = handlers[extension.oid]
KeyError: <ObjectIdentifier(oid=1.3.6.1.4.1.34380.1.1.13, name=Unknown OID)>

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
....
  File "./XXX/x509.py", line 68, in createCsr
    ).sign(key, hashes.SHA256(), default_backend())
  File "/Users/foo/.pyenv/versions/3.3.4/lib/python3.3/site-packages/cryptography/x509/base.py", line 354, in sign
    return backend.create_x509_csr(self, private_key, algorithm)
  File "/Users/foo/.pyenv/versions/3.3.4/lib/python3.3/site-packages/cryptography/hazmat/backends/multibackend.py", line 372, in create_x509_csr
    return b.create_x509_csr(builder, private_key, algorithm)
  File "/Users/foo/.pyenv/versions/3.3.4/lib/python3.3/site-packages/cryptography/hazmat/backends/openssl/backend.py", line 1349, in create_x509_csr
    gc=False
  File "/Users/foo/.pyenv/versions/3.3.4/lib/python3.3/site-packages/cryptography/hazmat/backends/openssl/backend.py", line 1555, in _create_x509_extensions
    'Extension not supported: {0}'.format(extension.oid)
NotImplementedError: Extension not supported: <ObjectIdentifier(oid=1.3.6.1.4.1.34380.1.1.13, name=Unknown OID)>
```

same exception is thrown in cryptography-1.3.dev1 (current master):

```
  File "/Users/foo/.pyenv/versions/3.3.4/lib/python3.3/site-packages/cryptography/hazmat/backends/openssl/backend.py", line 975, in _create_x509_extensions
    'Extension not supported: {0}'.format(extension.oid)
NotImplementedError: Extension not supported: <ObjectIdentifier(oid=1.3.6.1.4.1.34380.1.1.13, name=Unknown OID)>
```

We'd be glad if you could add this feature, or point us in the right direction if the error lies on our end.
So far, thanks for the great crypto plugin.



Discussion:
Should be relatively straigthforward, just need to add support for `UnknownExtension` hereish: https://github.com/pyca/cryptography/blob/master/src%2Fcryptography%2Fhazmat%2Fbackends%2Fopenssl%2Fbackend.py#L971-L976

Tentatively included in the 1.3 release


Resolving patch:
diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py
--- a/src/cryptography/hazmat/backends/openssl/backend.py
+++ b/src/cryptography/hazmat/backends/openssl/backend.py
@@ -31,8 +31,7 @@
 from cryptography.hazmat.backends.openssl.encode_asn1 import (
     _CRL_ENTRY_EXTENSION_ENCODE_HANDLERS,
     _CRL_EXTENSION_ENCODE_HANDLERS, _EXTENSION_ENCODE_HANDLERS,
-    _encode_asn1_int_gc, _encode_asn1_str_gc, _encode_name_gc,
-    _txt2obj_gc,
+    _encode_asn1_int_gc, _encode_asn1_str_gc, _encode_name_gc, _txt2obj_gc,
 )
 from cryptography.hazmat.backends.openssl.hashes import _HashContext
 from cryptography.hazmat.backends.openssl.hmac import _HMACContext
@@ -968,6 +967,31 @@ def create_x509_crl(self, builder, private_key, algorithm):
     def _create_x509_extensions(self, extensions, handlers, x509_obj,
                                 add_func, gc):
         for i, extension in enumerate(extensions):
+            x509_extension = self._create_x509_extension(
+                handlers, extension
+            )
+            self.openssl_assert(x509_extension != self._ffi.NULL)
+
+            if gc:
+                x509_extension = self._ffi.gc(
+                    x509_extension, self._lib.X509_EXTENSION_free
+                )
+            res = add_func(x509_obj, x509_extension, i)
+            self.openssl_assert(res >= 1)
+
+    def _create_x509_extension(self, handlers, extension):
+        if isinstance(extension.value, x509.UnrecognizedExtension):
+            obj = _txt2obj_gc(self, extension.oid.dotted_string)
+            value = _encode_asn1_str_gc(
+                self, extension.value.value, len(extension.value.value)
+            )
+            return self._lib.X509_EXTENSION_create_by_OBJ(
+                self._ffi.NULL,
+                obj,
+                1 if extension.critical else 0,
+                value
+            )
+        else:
             try:
                 encode = handlers[extension.oid]
             except KeyError:
@@ -987,32 +1011,25 @@ def _create_x509_extensions(self, extensions, handlers, x509_obj,
                 x509_extension == self._ffi.NULL and
                 extension.oid == x509.OID_CERTIFICATE_ISSUER
             ):
-                # This path exists to support OpenSSL 0.9.8, which does
-                # not know how to encode a CERTIFICATE_ISSUER for CRLs. Once we
+                # This path exists to support OpenSSL 0.9.8, which does not
+                # know how to encode a CERTIFICATE_ISSUER for CRLs. Once we
                 # drop 0.9.8 support we can remove this.
                 self._consume_errors()
                 pp = backend._ffi.new("unsigned char **")
                 r = self._lib.i2d_GENERAL_NAMES(ext_struct, pp)
                 backend.openssl_assert(r > 0)
                 pp = backend._ffi.gc(
-                    pp, lambda pointer: backend._lib.OPENSSL_free(pointer[0])
+                    pp,
+                    lambda pointer: backend._lib.OPENSSL_free(pointer[0])
                 )
                 obj = _txt2obj_gc(self, extension.oid.dotted_string)
-                x509_extension = self._lib.X509_EXTENSION_create_by_OBJ(
+                return 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)
                 )
-
-            self.openssl_assert(x509_extension != self._ffi.NULL)
-
-            if gc:
-                x509_extension = self._ffi.gc(
-                    x509_extension, self._lib.X509_EXTENSION_free
-                )
-            res = add_func(x509_obj, x509_extension, i)
-            self.openssl_assert(res >= 1)
+            return x509_extension
 
     def create_x509_revoked_certificate(self, builder):
         if not isinstance(builder, x509.RevokedCertificateBuilder):


Test patch:
diff --git a/tests/test_x509.py b/tests/test_x509.py
--- a/tests/test_x509.py
+++ b/tests/test_x509.py
@@ -2382,6 +2382,39 @@ def test_build_ca_request_with_path_length_none(self, backend):
         )
         assert basic_constraints.value.path_length is None
 
+    @pytest.mark.parametrize(
+        "unrecognized", [
+            x509.UnrecognizedExtension(
+                x509.ObjectIdentifier("1.2.3.4.5"),
+                b"abcdef",
+            )
+        ]
+    )
+    @pytest.mark.requires_backend_interface(interface=RSABackend)
+    @pytest.mark.requires_backend_interface(interface=X509Backend)
+    def test_unrecognized_extension(self, backend, unrecognized):
+        private_key = RSA_KEY_2048.private_key(backend)
+
+        cert = x509.CertificateBuilder().subject_name(
+            x509.Name([x509.NameAttribute(x509.OID_COUNTRY_NAME, u'US')])
+        ).issuer_name(
+            x509.Name([x509.NameAttribute(x509.OID_COUNTRY_NAME, u'US')])
+        ).not_valid_before(
+            datetime.datetime(2002, 1, 1, 12, 1)
+        ).not_valid_after(
+            datetime.datetime(2030, 12, 31, 8, 30)
+        ).public_key(
+            private_key.public_key()
+        ).serial_number(
+            123
+        ).add_extension(
+            unrecognized, critical=False
+        ).sign(private_key, hashes.SHA256(), backend)
+
+        ext = cert.extensions.get_extension_for_oid(unrecognized.oid)
+
+        assert ext.value == unrecognized
+
 
 @pytest.mark.requires_backend_interface(interface=X509Backend)
 class TestCertificateSigningRequestBuilder(object):