Repository: wbond/asn1crypto

Issue:
NameTypeAndValue of type "unique_indentifier" cannot be prepared
Loosely related to issue #228 and PR #241.

Note that the `NameTypeAndValue` class can hold a `unique_identifier` which is an `OctetBitString`:
https://github.com/wbond/asn1crypto/blob/af8a325794b3c1c96860746dbde4ad46218645fe/asn1crypto/x509.py#L652
https://github.com/wbond/asn1crypto/blob/af8a325794b3c1c96860746dbde4ad46218645fe/asn1crypto/x509.py#L677

However, the `prepped_value` property relies on the value being a unicode string:
https://github.com/wbond/asn1crypto/blob/af8a325794b3c1c96860746dbde4ad46218645fe/asn1crypto/x509.py#L700-L712

For this reason, attempting to hash a `Name` with a `RDNSequence` that includes a `unique_identifier` fails with the following error:
```python
../../miniconda/envs/parsec/lib/python3.9/site-packages/asn1crypto/x509.py:1055: in hashable
    return self.chosen.hashable
../../miniconda/envs/parsec/lib/python3.9/site-packages/asn1crypto/x509.py:949: in hashable
    return '\x1E'.join(rdn.hashable for rdn in self)
../../miniconda/envs/parsec/lib/python3.9/site-packages/asn1crypto/x509.py:949: in <genexpr>
    return '\x1E'.join(rdn.hashable for rdn in self)
../../miniconda/envs/parsec/lib/python3.9/site-packages/asn1crypto/x509.py:856: in hashable
    values = self._get_values(self)
../../miniconda/envs/parsec/lib/python3.9/site-packages/asn1crypto/x509.py:925: in _get_values
    for ntv in rdn:
../../miniconda/envs/parsec/lib/python3.9/site-packages/asn1crypto/x509.py:931: in <listcomp>
    [output.update([(ntv['type'].native, ntv.prepped_value)]) for ntv in rdn]
../../miniconda/envs/parsec/lib/python3.9/site-packages/asn1crypto/x509.py:711: in prepped_value
    self._prepped = self._ldap_string_prep(self['value'].native)
../../miniconda/envs/parsec/lib/python3.9/site-packages/asn1crypto/x509.py:749: in _ldap_string_prep
    string = re.sub('[\u00ad\u1806\u034f\u180b-\u180d\ufe0f-\uff00\ufffc]+', '', string)
pattern = '[\xad᠆͏᠋-᠍️-\uff00￼]+', repl = '', string = b'test_ca', count = 0, flags = 0
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
    def sub(pattern, repl, string, count=0, flags=0):
        """Return the string obtained by replacing the leftmost
        non-overlapping occurrences of the pattern in string by the
        replacement repl.  repl can be either a string or a callable;
        if a string, backslash escapes in it are processed.  If it is
        a callable, it's passed the Match object and must return
        a replacement string to be used."""
>       return _compile(pattern, flags).sub(repl, string, count)
E       TypeError: cannot use a string pattern on a bytes-like object
../../miniconda/envs/parsec/lib/python3.9/re.py:210: TypeError
```


Discussion:

Patch:
diff --git a/asn1crypto/x509.py b/asn1crypto/x509.py
index a67ab1a..38aa770 100644
--- a/asn1crypto/x509.py
+++ b/asn1crypto/x509.py
@@ -27,7 +27,7 @@ import unicodedata
 from ._errors import unwrap
 from ._iri import iri_to_uri, uri_to_iri
 from ._ordereddict import OrderedDict
-from ._types import type_name, str_cls, bytes_to_list
+from ._types import type_name, str_cls, byte_cls, bytes_to_list
 from .algos import AlgorithmIdentifier, AnyAlgorithmIdentifier, DigestAlgorithm, SignedDigestAlgorithm
 from .core import (
     Any,
@@ -708,7 +708,13 @@ class NameTypeAndValue(Sequence):
         """
 
         if self._prepped is None:
-            self._prepped = self._ldap_string_prep(self['value'].native)
+            native = self['value'].native
+            if isinstance(native, str_cls):
+                self._prepped = self._ldap_string_prep(native)
+            else:
+                if isinstance(native, byte_cls):
+                    native = ' ' + native.decode('cp1252') + ' '
+                self._prepped = native
         return self._prepped
 
     def __ne__(self, other):


Test patch:
diff --git a/tests/test_x509.py b/tests/test_x509.py
index c177fe6..43e0bea 100644
--- a/tests/test_x509.py
+++ b/tests/test_x509.py
@@ -485,6 +485,23 @@ class X509Tests(unittest.TestCase):
         self.assertEqual("unique_identifier", complex_name.chosen[3][0]['type'].native)
         self.assertIsInstance(complex_name.chosen[3][0]['value'], core.OctetBitString)
 
+    def test_name_hashable(self):
+        complex_name = x509.Name.build(
+            {
+                'country_name': 'US',
+                'tpm_manufacturer': 'Acme Co',
+                'unique_identifier': b'\x04\x10\x03\x09',
+                'email_address': 'will@codexns.io'
+            }
+        )
+        self.assertEqual(
+            "country_name:  us \x1e"
+            "email_address:  will@codexns.io \x1e"
+            "tpm_manufacturer:  acme  co \x1e"
+            "unique_identifier:  \x04\x10\x03\x09 ",
+            complex_name.hashable
+        )
+
     def test_v1_cert(self):
         cert = self._load_cert('chromium/ndn.ca.crt')
         tbs_cert = cert['tbs_certificate']