both parse and encode the ASN1 string type for Name attributes (#3896)

* both parse and encode the ASN1 string type for Name attributes

Previously cryptography encoded everything (except country names) as
UTF8String. This caused problems with chain building in libraries like
NSS where the subject and issuer are expected to match byte-for-byte.

With this change we now parse and store the ASN1 string type as a
private _type in NameAttribute. We then use this to encode when issuing
a new certificate. This allows the CertificateBuilder to properly
construct an identical issuer and fixes the issue with NSS.

* make the sentinel private too
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index a56c67b..6b4d538 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -41,6 +41,8 @@
 * Added support for using labels with
   :class:`~cryptography.hazmat.primitives.asymmetric.padding.OAEP` when using
   OpenSSL 1.0.2 or greater.
+* Improved compatibility with NSS when issuing certificates from an issuer
+  that has a subject with non-``UTF8String`` string types.
 * Add support for the :class:`~cryptography.x509.DeltaCRLIndicator` extension.
 * Add support for the :class:`~cryptography.x509.TLSFeature`
   extension. This is commonly used for enabling ``OCSP Must-Staple`` in
diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py
index 3a88934..ede35ec 100644
--- a/src/cryptography/hazmat/backends/openssl/backend.py
+++ b/src/cryptography/hazmat/backends/openssl/backend.py
@@ -95,12 +95,6 @@
         self._ffi = self._binding.ffi
         self._lib = self._binding.lib
 
-        # Set the default string mask for encoding ASN1 strings to UTF8. This
-        # is the default for newer OpenSSLs for several years (1.0.1h+) and is
-        # recommended in RFC 2459.
-        res = self._lib.ASN1_STRING_set_default_mask_asc(b"utf8only")
-        self.openssl_assert(res == 1)
-
         self._cipher_registry = {}
         self._register_default_ciphers()
         self.activate_osrandom_engine()
diff --git a/src/cryptography/hazmat/backends/openssl/decode_asn1.py b/src/cryptography/hazmat/backends/openssl/decode_asn1.py
index ec55a9e..2665fb2 100644
--- a/src/cryptography/hazmat/backends/openssl/decode_asn1.py
+++ b/src/cryptography/hazmat/backends/openssl/decode_asn1.py
@@ -11,6 +11,7 @@
 
 from cryptography import x509
 from cryptography.x509.extensions import _TLS_FEATURE_TYPE_TO_ENUM
+from cryptography.x509.name import _ASN1_TYPE_TO_ENUM
 from cryptography.x509.oid import (
     CRLEntryExtensionOID, CertificatePoliciesOID, ExtensionOID
 )
@@ -51,8 +52,9 @@
     backend.openssl_assert(data != backend._ffi.NULL)
     value = _asn1_string_to_utf8(backend, data)
     oid = _obj2txt(backend, obj)
+    type = _ASN1_TYPE_TO_ENUM[data.type]
 
-    return x509.NameAttribute(x509.ObjectIdentifier(oid), value)
+    return x509.NameAttribute(x509.ObjectIdentifier(oid), value, type)
 
 
 def _decode_x509_name(backend, x509_name):
diff --git a/src/cryptography/hazmat/backends/openssl/encode_asn1.py b/src/cryptography/hazmat/backends/openssl/encode_asn1.py
index 6b86768..e45e105 100644
--- a/src/cryptography/hazmat/backends/openssl/encode_asn1.py
+++ b/src/cryptography/hazmat/backends/openssl/encode_asn1.py
@@ -14,7 +14,7 @@
     _CRL_ENTRY_REASON_ENUM_TO_CODE, _DISTPOINT_TYPE_FULLNAME,
     _DISTPOINT_TYPE_RELATIVENAME
 )
-from cryptography.x509.oid import CRLEntryExtensionOID, ExtensionOID, NameOID
+from cryptography.x509.oid import CRLEntryExtensionOID, ExtensionOID
 
 
 def _encode_asn1_int(backend, x):
@@ -118,17 +118,9 @@
 def _encode_name_entry(backend, attribute):
     value = attribute.value.encode('utf8')
     obj = _txt2obj_gc(backend, attribute.oid.dotted_string)
-    if attribute.oid in [
-        NameOID.COUNTRY_NAME, NameOID.JURISDICTION_COUNTRY_NAME
-    ]:
-        # Per RFC5280 Appendix A.1 countryName should be encoded as
-        # PrintableString, not UTF8String. EV Guidelines section 9.2.5 says
-        # jurisdictionCountryName follows the same rules as countryName.
-        type = backend._lib.MBSTRING_ASC
-    else:
-        type = backend._lib.MBSTRING_UTF8
+
     name_entry = backend._lib.X509_NAME_ENTRY_create_by_OBJ(
-        backend._ffi.NULL, obj, type, value, -1
+        backend._ffi.NULL, obj, attribute._type.value, value, -1
     )
     return name_entry
 
diff --git a/src/cryptography/x509/name.py b/src/cryptography/x509/name.py
index 108b60c..2fbaee9 100644
--- a/src/cryptography/x509/name.py
+++ b/src/cryptography/x509/name.py
@@ -4,14 +4,33 @@
 
 from __future__ import absolute_import, division, print_function
 
+from enum import Enum
+
 import six
 
 from cryptography import utils
 from cryptography.x509.oid import NameOID, ObjectIdentifier
 
 
+class _ASN1Type(Enum):
+    UTF8String = 12
+    NumericString = 18
+    PrintableString = 19
+    T61String = 20
+    IA5String = 22
+    UTCTime = 23
+    GeneralizedTime = 24
+    VisibleString = 26
+    UniversalString = 28
+    BMPString = 30
+
+
+_ASN1_TYPE_TO_ENUM = dict((i.value, i) for i in _ASN1Type)
+_SENTINEL = object()
+
+
 class NameAttribute(object):
-    def __init__(self, oid, value):
+    def __init__(self, oid, value, _type=_SENTINEL):
         if not isinstance(oid, ObjectIdentifier):
             raise TypeError(
                 "oid argument must be an ObjectIdentifier instance."
@@ -22,16 +41,33 @@
                 "value argument must be a text type."
             )
 
-        if oid == NameOID.COUNTRY_NAME and len(value.encode("utf8")) != 2:
-            raise ValueError(
-                "Country name must be a 2 character country code"
-            )
+        if (
+            oid == NameOID.COUNTRY_NAME or
+            oid == NameOID.JURISDICTION_COUNTRY_NAME
+        ):
+            if len(value.encode("utf8")) != 2:
+                raise ValueError(
+                    "Country name must be a 2 character country code"
+                )
+
+            if _type == _SENTINEL:
+                _type = _ASN1Type.PrintableString
 
         if len(value) == 0:
             raise ValueError("Value cannot be an empty string")
 
+        # Set the default string type for encoding ASN1 strings to UTF8. This
+        # is the default for newer OpenSSLs for several years (1.0.1h+) and is
+        # recommended in RFC 2459.
+        if _type == _SENTINEL:
+            _type = _ASN1Type.UTF8String
+
+        if not isinstance(_type, _ASN1Type):
+            raise TypeError("_type must be from the _ASN1Type enum")
+
         self._oid = oid
         self._value = value
+        self._type = _type
 
     oid = utils.read_only_property("_oid")
     value = utils.read_only_property("_value")
diff --git a/tests/x509/test_x509.py b/tests/x509/test_x509.py
index 06aef66..0ce0a63 100644
--- a/tests/x509/test_x509.py
+++ b/tests/x509/test_x509.py
@@ -28,6 +28,7 @@
 from cryptography.hazmat.primitives.asymmetric.utils import (
     decode_dss_signature
 )
+from cryptography.x509.name import _ASN1Type
 from cryptography.x509.oid import (
     AuthorityInformationAccessOID, ExtendedKeyUsageOID, ExtensionOID,
     NameOID, SignatureAlgorithmOID
@@ -1496,6 +1497,43 @@
             x509.DNSName(b"cryptography.io"),
         ]
 
+    def test_build_cert_private_type_encoding(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)
+        name = x509.Name([
+            x509.NameAttribute(
+                NameOID.STATE_OR_PROVINCE_NAME, u'Texas',
+                _ASN1Type.PrintableString),
+            x509.NameAttribute(NameOID.LOCALITY_NAME, u'Austin'),
+            x509.NameAttribute(
+                NameOID.COMMON_NAME, u'cryptography.io', _ASN1Type.IA5String),
+        ])
+        builder = x509.CertificateBuilder().serial_number(
+            777
+        ).issuer_name(
+            name
+        ).subject_name(
+            name
+        ).public_key(
+            subject_private_key.public_key()
+        ).not_valid_before(
+            not_valid_before
+        ).not_valid_after(not_valid_after)
+        cert = builder.sign(issuer_private_key, hashes.SHA256(), backend)
+
+        for dn in (cert.subject, cert.issuer):
+            assert dn.get_attributes_for_oid(
+                NameOID.STATE_OR_PROVINCE_NAME
+            )[0]._type == _ASN1Type.PrintableString
+            assert dn.get_attributes_for_oid(
+                NameOID.STATE_OR_PROVINCE_NAME
+            )[0]._type == _ASN1Type.PrintableString
+            assert dn.get_attributes_for_oid(
+                NameOID.LOCALITY_NAME
+            )[0]._type == _ASN1Type.UTF8String
+
     def test_build_cert_printable_string_country_name(self, backend):
         issuer_private_key = RSA_KEY_2048.private_key(backend)
         subject_private_key = RSA_KEY_2048.private_key(backend)
@@ -3628,6 +3666,26 @@
         with pytest.raises(ValueError):
             x509.NameAttribute(NameOID.ORGANIZATION_NAME, u'')
 
+    def test_country_name_type(self):
+        na = x509.NameAttribute(NameOID.COUNTRY_NAME, u"US")
+        assert na._type == _ASN1Type.PrintableString
+        na2 = x509.NameAttribute(
+            NameOID.COUNTRY_NAME, u"US", _ASN1Type.IA5String
+        )
+        assert na2._type == _ASN1Type.IA5String
+
+    def test_types(self):
+        na = x509.NameAttribute(NameOID.COMMON_NAME, u"common")
+        assert na._type == _ASN1Type.UTF8String
+        na2 = x509.NameAttribute(
+            NameOID.COMMON_NAME, u"common", _ASN1Type.IA5String
+        )
+        assert na2._type == _ASN1Type.IA5String
+
+    def test_invalid_type(self):
+        with pytest.raises(TypeError):
+            x509.NameAttribute(NameOID.COMMON_NAME, u"common", "notanenum")
+
     def test_eq(self):
         assert x509.NameAttribute(
             x509.ObjectIdentifier('2.999.1'), u'value'