Merge pull request #2085 from alex/encode-san

Initial code to encode SANs
diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py
index 73a5863..bbec618 100644
--- a/src/cryptography/hazmat/backends/openssl/backend.py
+++ b/src/cryptography/hazmat/backends/openssl/backend.py
@@ -8,6 +8,8 @@
 import itertools
 from contextlib import contextmanager
 
+import idna
+
 import six
 
 from cryptography import utils, x509
@@ -136,6 +138,45 @@
     return pp, r
 
 
+def _encode_subject_alt_name(backend, san):
+    general_names = backend._lib.GENERAL_NAMES_new()
+    assert general_names != backend._ffi.NULL
+    general_names = backend._ffi.gc(
+        general_names, backend._lib.GENERAL_NAMES_free
+    )
+
+    for alt_name in san:
+        if isinstance(alt_name, x509.DNSName):
+            gn = backend._lib.GENERAL_NAME_new()
+            assert gn != backend._ffi.NULL
+            gn.type = backend._lib.GEN_DNS
+
+            ia5 = backend._lib.ASN1_IA5STRING_new()
+            assert ia5 != backend._ffi.NULL
+
+            if alt_name.value.startswith(u"*."):
+                value = b"*." + idna.encode(alt_name.value[2:])
+            else:
+                value = idna.encode(alt_name.value)
+
+            res = backend._lib.ASN1_STRING_set(ia5, value, len(value))
+            assert res == 1
+            gn.d.dNSName = ia5
+        else:
+            raise NotImplementedError("Only DNSNames are supported right now")
+
+        res = backend._lib.sk_GENERAL_NAME_push(general_names, gn)
+        assert res != 0
+
+    pp = backend._ffi.new("unsigned char **")
+    r = backend._lib.i2d_GENERAL_NAMES(general_names, pp)
+    assert r > 0
+    pp = backend._ffi.gc(
+        pp, lambda pointer: backend._lib.OPENSSL_free(pointer[0])
+    )
+    return pp, r
+
+
 @utils.register_interface(CipherBackend)
 @utils.register_interface(CMACBackend)
 @utils.register_interface(DERSerializationBackend)
@@ -841,12 +882,14 @@
             self._lib.sk_X509_EXTENSION_free,
         )
         for extension in builder._extensions:
-            obj = _txt2obj(self, extension.oid.dotted_string)
             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,
diff --git a/tests/test_x509.py b/tests/test_x509.py
index 1e0c9cd..7855297 100644
--- a/tests/test_x509.py
+++ b/tests/test_x509.py
@@ -911,6 +911,49 @@
                 ])
             )
 
+    def test_subject_alt_names(self, backend):
+        private_key = RSA_KEY_2048.private_key(backend)
+
+        csr = x509.CertificateSigningRequestBuilder().subject_name(
+            x509.Name([
+                x509.NameAttribute(x509.OID_COMMON_NAME, u"SAN"),
+            ])
+        ).add_extension(
+            x509.SubjectAlternativeName([
+                x509.DNSName(u"example.com"),
+                x509.DNSName(u"*.example.com"),
+            ]),
+            critical=False,
+        ).sign(private_key, hashes.SHA256(), backend)
+
+        assert len(csr.extensions) == 1
+        ext = csr.extensions.get_extension_for_oid(
+            x509.OID_SUBJECT_ALTERNATIVE_NAME
+        )
+        assert not ext.critical
+        assert ext.oid == x509.OID_SUBJECT_ALTERNATIVE_NAME
+        assert list(ext.value) == [
+            x509.DNSName(u"example.com"),
+            x509.DNSName(u"*.example.com"),
+        ]
+
+    def test_subject_alt_name_unsupported_general_name(self, backend):
+        private_key = RSA_KEY_2048.private_key(backend)
+
+        builder = x509.CertificateSigningRequestBuilder().subject_name(
+            x509.Name([
+                x509.NameAttribute(x509.OID_COMMON_NAME, u"SAN"),
+            ])
+        ).add_extension(
+            x509.SubjectAlternativeName([
+                x509.RFC822Name(u"test@example.com"),
+            ]),
+            critical=False,
+        )
+
+        with pytest.raises(NotImplementedError):
+            builder.sign(private_key, hashes.SHA256(), backend)
+
 
 @pytest.mark.requires_backend_interface(interface=DSABackend)
 @pytest.mark.requires_backend_interface(interface=X509Backend)