Merge pull request #2095 from reaperhulk/nc-the-hard-part-redux

name constraints - support IP addresses with netmask
diff --git a/src/_cffi_src/openssl/asn1.py b/src/_cffi_src/openssl/asn1.py
index 01d6f4c..5f8ca69 100644
--- a/src/_cffi_src/openssl/asn1.py
+++ b/src/_cffi_src/openssl/asn1.py
@@ -157,6 +157,7 @@
 int ASN1_STRING_set_default_mask_asc(char *);
 
 int i2d_ASN1_TYPE(ASN1_TYPE *, unsigned char **);
+ASN1_TYPE *d2i_ASN1_TYPE(ASN1_TYPE **, const unsigned char **, long);
 """
 
 CUSTOMIZATIONS = """
diff --git a/src/_cffi_src/openssl/x509v3.py b/src/_cffi_src/openssl/x509v3.py
index 0f5306d..8e42b65 100644
--- a/src/_cffi_src/openssl/x509v3.py
+++ b/src/_cffi_src/openssl/x509v3.py
@@ -193,6 +193,9 @@
 NAME_CONSTRAINTS *NAME_CONSTRAINTS_new(void);
 void NAME_CONSTRAINTS_free(NAME_CONSTRAINTS *);
 
+OTHERNAME *OTHERNAME_new(void);
+void OTHERNAME_free(OTHERNAME *);
+
 void *X509V3_set_ctx_nodb(X509V3_CTX *);
 
 int i2d_GENERAL_NAMES(GENERAL_NAMES *, unsigned char **);
diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py
index d649377..637b28c 100644
--- a/src/cryptography/hazmat/backends/openssl/backend.py
+++ b/src/cryptography/hazmat/backends/openssl/backend.py
@@ -83,14 +83,22 @@
     Create an ASN1_OCTET_STRING from a Python byte string.
     """
     s = backend._lib.ASN1_OCTET_STRING_new()
+    res = backend._lib.ASN1_OCTET_STRING_set(s, data, length)
+    assert res == 1
+    return s
+
+
+def _encode_asn1_str_gc(backend, data, length):
+    s = _encode_asn1_str(backend, data, length)
     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):
+    """
+    The X509_NAME created will not be gc'd. Use _encode_name_gc if needed.
+    """
     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)
@@ -105,6 +113,12 @@
     return subject
 
 
+def _encode_name_gc(backend, attributes):
+    subject = _encode_name(backend, attributes)
+    subject = backend._ffi.gc(subject, backend._lib.X509_NAME_free)
+    return subject
+
+
 def _txt2obj(backend, name):
     """
     Converts a Python string with an ASN.1 object ID in dotted form to a
@@ -171,6 +185,42 @@
             )
             assert obj != backend._ffi.NULL
             gn.d.registeredID = obj
+        elif isinstance(alt_name, x509.DirectoryName):
+            gn = backend._lib.GENERAL_NAME_new()
+            assert gn != backend._ffi.NULL
+            name = _encode_name(backend, alt_name.value)
+            gn.type = backend._lib.GEN_DIRNAME
+            gn.d.directoryName = name
+        elif isinstance(alt_name, x509.IPAddress):
+            gn = backend._lib.GENERAL_NAME_new()
+            assert gn != backend._ffi.NULL
+            ipaddr = _encode_asn1_str(
+                backend, alt_name.value.packed, len(alt_name.value.packed)
+            )
+            gn.type = backend._lib.GEN_IPADD
+            gn.d.iPAddress = ipaddr
+        elif isinstance(alt_name, x509.OtherName):
+            gn = backend._lib.GENERAL_NAME_new()
+            assert gn != backend._ffi.NULL
+            other_name = backend._lib.OTHERNAME_new()
+            assert other_name != backend._ffi.NULL
+
+            type_id = backend._lib.OBJ_txt2obj(
+                alt_name.type_id.dotted_string.encode('ascii'), 1
+            )
+            assert type_id != backend._ffi.NULL
+            data = backend._ffi.new("unsigned char[]", alt_name.value)
+            data_ptr_ptr = backend._ffi.new("unsigned char **")
+            data_ptr_ptr[0] = data
+            value = backend._lib.d2i_ASN1_TYPE(
+                backend._ffi.NULL, data_ptr_ptr, len(alt_name.value)
+            )
+            if value == backend._ffi.NULL:
+                raise ValueError("Invalid ASN.1 data")
+            other_name.type_id = type_id
+            other_name.value = value
+            gn.type = backend._lib.GEN_OTHERNAME
+            gn.d.otherName = other_name
         else:
             raise NotImplementedError(
                 "Only DNSName and RegisteredID supported right now"
@@ -874,7 +924,7 @@
 
         # Set subject name.
         res = self._lib.X509_REQ_set_subject_name(
-            x509_req, _encode_name(self, builder._subject_name)
+            x509_req, _encode_name_gc(self, builder._subject_name)
         )
         assert res == 1
 
@@ -905,7 +955,7 @@
                 self._ffi.NULL,
                 obj,
                 1 if extension.critical else 0,
-                _encode_asn1_str(self, pp[0], r),
+                _encode_asn1_str_gc(self, pp[0], r),
             )
             assert extension != self._ffi.NULL
             res = self._lib.sk_X509_EXTENSION_push(extensions, extension)
diff --git a/tests/test_x509.py b/tests/test_x509.py
index 9c97e96..cb61726 100644
--- a/tests/test_x509.py
+++ b/tests/test_x509.py
@@ -6,6 +6,7 @@
 
 import binascii
 import datetime
+import ipaddress
 import os
 
 import pytest
@@ -995,6 +996,18 @@
                 x509.DNSName(u"example.com"),
                 x509.DNSName(u"*.example.com"),
                 x509.RegisteredID(x509.ObjectIdentifier("1.2.3.4.5.6.7")),
+                x509.DirectoryName(x509.Name([
+                    x509.NameAttribute(x509.OID_COMMON_NAME, u'PyCA'),
+                    x509.NameAttribute(
+                        x509.OID_ORGANIZATION_NAME, u'We heart UTF8!\u2122'
+                    )
+                ])),
+                x509.IPAddress(ipaddress.ip_address(u"127.0.0.1")),
+                x509.IPAddress(ipaddress.ip_address(u"ff::")),
+                x509.OtherName(
+                    type_id=x509.ObjectIdentifier("1.2.3.3.3.3"),
+                    value=b"0\x03\x02\x01\x05"
+                ),
             ]),
             critical=False,
         ).sign(private_key, hashes.SHA256(), backend)
@@ -1009,8 +1022,39 @@
             x509.DNSName(u"example.com"),
             x509.DNSName(u"*.example.com"),
             x509.RegisteredID(x509.ObjectIdentifier("1.2.3.4.5.6.7")),
+            x509.DirectoryName(x509.Name([
+                x509.NameAttribute(x509.OID_COMMON_NAME, u'PyCA'),
+                x509.NameAttribute(
+                    x509.OID_ORGANIZATION_NAME, u'We heart UTF8!\u2122'
+                ),
+            ])),
+            x509.IPAddress(ipaddress.ip_address(u"127.0.0.1")),
+            x509.IPAddress(ipaddress.ip_address(u"ff::")),
+            x509.OtherName(
+                type_id=x509.ObjectIdentifier("1.2.3.3.3.3"),
+                value=b"0\x03\x02\x01\x05"
+            ),
         ]
 
+    def test_invalid_asn1_othername(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.OtherName(
+                    type_id=x509.ObjectIdentifier("1.2.3.3.3.3"),
+                    value=b"\x01\x02\x01\x05"
+                ),
+            ]),
+            critical=False,
+        )
+        with pytest.raises(ValueError):
+            builder.sign(private_key, hashes.SHA256(), backend)
+
     def test_subject_alt_name_unsupported_general_name(self, backend):
         private_key = RSA_KEY_2048.private_key(backend)