Repository: pyca/cryptography

Issue:
X.509 OID 2.5.4.45 cannot be read because it's a bit string, not text
\_\_repr\_\_ of the certificate object for the attached X.509 certificate fails with the exception
      ValueError: Unsupported ASN1 string type. Type: 3

I tracked it down to OID 2.5.4.45, which is of type "BIT STRING (64 bit)", according to https://lapo.it/asn1js/, which is the correct type for the OID if we are to believe http://www.alvestrand.no/objectid/2.5.4.45.html.

It looks like 'cryptography' is insisting name attributes must be text, and using decode_asn._asn1_string_to_utf8 accordingly, but in this case there is no text conversion and decode_asn1._asn1_string_to_bytes should have been used instead.

Attached, [sup_ds.zip](https://github.com/pyca/cryptography/files/1004488/sup_ds.zip) containing:
sup_ds.crt: A sample offending certificate.
sup_ds.py: Reproducing code.
sup_ds.out: Traceback when run against cryptography 1.8.1, openssl 1.1.0e.


Discussion:
The TBSCertificate ASN.1 definition contains fields "issuerUniqueID" and "subjectUniqueID". Are you sure that your uniqueIdentifier should actually go in the Subject?
You're probably right that subjectUniqueID makes more sense, but I have no control over how these certificates are built, I have to read them as I find them. They're from the British smart-meter programme. 

I'm well aware that non-text name attributes are unusual. These certs trip up a lot of software, not just 'cryptography'. But it's not strictly illegal as far as I know.

I managed to read the certificate by monkeypatching `_decode_x509_name_entry` to fall back to `_asn1_string_to_bytes` if `_asn1_string_to_utf8` fails, and removing the `text_type` check in `NameAttribute.__init__`. I could make these changes into a proper pull request, if I could get some indication that it won't be rejected on principle.
rfc5280 4.1.2.4 states:

> The type of the component AttributeValue is determined by the AttributeType; in general it will be a DirectoryString.
> ...
> This specification does not restrict the set of attribute types that may appear in names.  However, conforming implementations MUST be prepared to receive certificates with issuer names containing the set of attribute types defined below.  This specification RECOMMENDS support for additional attribute types.

These "below" types are text types.

So I guess your certificate doesn't strictly violate the standard. But I still think someone messed up :-)
If all you want is to read the certs, you can also use https://github.com/wbond/asn1crypto/. But it lacks high-level functions like verifying if the cert is valid.
I've implemented support for this, but now I'm wondering if I went too far by adding support for unused bits. @wbond I know you built an `OctetBitString` concept for things that are encoded as bit strings but in reality will always be octets and make no sense to address as bits. It looks like you use that for x500UniqueIdentifier. Did you find somewhere in the spec that requires that the unused bits always be zero?
Is this still an issue people run into as 2022 approaches? Or has the British smart meter system gotten smarter? (The German smart meter program has also caused us grief in the past -- what is it about that industry...)

We could potentially support this now, but the public API declares `NameAttribute` values as always being `str` so we'd need to alter the API to allow `bytes` for this weird edge case, which isn't great. 
Oh yes, this is still an issue. I still maintain a monkeypatch that effects my 2017 PR, since without it there are certificates that I just can't read.  Problem unchanged, solution unchanged.
Someone I missed your comment a few years ago @reaperhulk. I don't recall exactly the development that went into that choice, but most likely it was reading certificates in the wild.

I did find https://openssl-users.openssl.narkive.com/RohW1xwV/invalid-x500uniqueidentifier-bitstring-in-openssl, where there is some discussion and Stephen Henson of OpenSSL implied unused bits will always be 0 for that use case. 🤷‍♂️ 

Resolving patch:
diff --git a/src/cryptography/x509/name.py b/src/cryptography/x509/name.py
--- a/src/cryptography/x509/name.py
+++ b/src/cryptography/x509/name.py
@@ -2,6 +2,7 @@
 # 2.0, and the BSD License. See the LICENSE file in the root of this repository
 # for complete details.
 
+import binascii
 import typing
 import warnings
 
@@ -13,6 +14,7 @@
 
 
 class _ASN1Type(utils.Enum):
+    BitString = 3
     UTF8String = 12
     NumericString = 18
     PrintableString = 19
@@ -54,12 +56,17 @@ class _ASN1Type(utils.Enum):
 }
 
 
-def _escape_dn_value(val: str) -> str:
+def _escape_dn_value(val: typing.Union[str, bytes]) -> str:
     """Escape special characters in RFC4514 Distinguished Name value."""
 
     if not val:
         return ""
 
+    # RFC 4514 Section 2.4 defines the value as being the # (U+0023) character
+    # followed by the hexadecimal encoding of the octets.
+    if isinstance(val, bytes):
+        return "#" + binascii.hexlify(val).decode("utf8")
+
     # See https://tools.ietf.org/html/rfc4514#section-2.4
     val = val.replace("\\", "\\\\")
     val = val.replace('"', '\\"')
@@ -82,7 +89,7 @@ class NameAttribute(object):
     def __init__(
         self,
         oid: ObjectIdentifier,
-        value: str,
+        value: typing.Union[str, bytes],
         _type=_SENTINEL,
         *,
         _validate=True,
@@ -91,14 +98,22 @@ def __init__(
             raise TypeError(
                 "oid argument must be an ObjectIdentifier instance."
             )
-
-        if not isinstance(value, str):
-            raise TypeError("value argument must be a str.")
+        if _type == _ASN1Type.BitString:
+            if oid != NameOID.X500_UNIQUE_IDENTIFIER:
+                raise TypeError(
+                    "oid must be X500_UNIQUE_IDENTIFIER for BitString type."
+                )
+            if not isinstance(value, bytes):
+                raise TypeError("value must be bytes for BitString")
+        else:
+            if not isinstance(value, str):
+                raise TypeError("value argument must be a str")
 
         if (
             oid == NameOID.COUNTRY_NAME
             or oid == NameOID.JURISDICTION_COUNTRY_NAME
         ):
+            assert isinstance(value, str)
             c_len = len(value.encode("utf8"))
             if c_len != 2 and _validate is True:
                 raise ValueError(
@@ -132,7 +147,7 @@ def oid(self) -> ObjectIdentifier:
         return self._oid
 
     @property
-    def value(self) -> str:
+    def value(self) -> typing.Union[str, bytes]:
         return self._value
 
     @property


Test patch:
diff --git a/tests/x509/test_x509.py b/tests/x509/test_x509.py
--- a/tests/x509/test_x509.py
+++ b/tests/x509/test_x509.py
@@ -4555,6 +4555,29 @@ def test_load_ecdsa_cert(self, backend):
         )
         assert isinstance(num.curve, ec.SECP384R1)
 
+    def test_load_bitstring_dn(self, backend):
+        cert = _load_cert(
+            os.path.join("x509", "scottishpower-bitstring-dn.pem"),
+            x509.load_pem_x509_certificate,
+            backend,
+        )
+        assert cert.subject == x509.Name(
+            [
+                x509.NameAttribute(x509.NameOID.COMMON_NAME, "ScottishPower"),
+                x509.NameAttribute(
+                    x509.NameOID.ORGANIZATIONAL_UNIT_NAME, "02"
+                ),
+                x509.NameAttribute(
+                    NameOID.X500_UNIQUE_IDENTIFIER,
+                    b"\x00\x70\xb3\xd5\x1f\x30\x5f\x00\x01",
+                    _ASN1Type.BitString,
+                ),
+            ]
+        )
+        assert repr(cert.subject) == (
+            "<Name(CN=ScottishPower,OU=02,2.5.4.45=#0070b3d51f305f0001)>"
+        )
+
     def test_signature(self, backend):
         cert = _load_cert(
             os.path.join("x509", "ecdsa_root.pem"),
@@ -4771,6 +4794,19 @@ def test_init_bad_value(self):
                 b"bytes",  # type:ignore[arg-type]
             )
 
+    def test_init_bitstring_not_bytes(self):
+        with pytest.raises(TypeError):
+            x509.NameAttribute(
+                x509.ObjectIdentifier("2.5.4.45"), "str", _ASN1Type.BitString
+            )
+
+    def test_init_bitstring_not_allowed_random_oid(self):
+        # We only allow BitString type with X500_UNIQUE_IDENTIFIER
+        with pytest.raises(TypeError):
+            x509.NameAttribute(
+                x509.NameOID.COMMON_NAME, b"ok", _ASN1Type.BitString
+            )
+
     def test_init_none_value(self):
         with pytest.raises(TypeError):
             x509.NameAttribute(
@@ -4824,6 +4860,14 @@ def test_distinguished_name(self):
         na = x509.NameAttribute(NameOID.BUSINESS_CATEGORY, "banking")
         assert na.rfc4514_string() == "2.5.4.15=banking"
 
+        # non-utf8 attribute (bitstring with raw bytes)
+        na = x509.NameAttribute(
+            x509.ObjectIdentifier("2.5.4.45"),
+            b"\x01\x02\x03\x04",
+            _ASN1Type.BitString,
+        )
+        assert na.rfc4514_string() == "2.5.4.45=#01020304"
+
     def test_distinguished_name_custom_attrs(self):
         name = x509.Name(
             [
@@ -5132,6 +5176,22 @@ def test_bytes(self, backend):
             b"b060355040a0c0450794341"
         )
 
+    def test_bitstring_encoding(self):
+        name = x509.Name(
+            [
+                x509.NameAttribute(NameOID.COMMON_NAME, "cryptography.io"),
+                x509.NameAttribute(
+                    x509.ObjectIdentifier("2.5.4.45"),
+                    b"\x01\x02",
+                    _ASN1Type.BitString,
+                ),
+            ]
+        )
+        assert name.public_bytes() == binascii.unhexlify(
+            b"30273118301606035504030c0f63727970746f6772617068792e696f310b3"
+            b"009060355042d03020102"
+        )
+
     def test_bmpstring_bytes(self, backend):
         # For this test we need an odd length string. BMPString is UCS-2
         # encoded so it will always be even length and OpenSSL will error if