Repository: pyca/cryptography

Issue:
Escape sequence bugs in RFC4514 parser (unreleased)
The parser merged in #7055 seems to have some bugs involving escape sequences. I tested and got these results with git `main` branch:

```python
>>> from cryptography.x509 import Name, NameAttribute, NameOID
>>> Name([NameAttribute(NameOID.COMMON_NAME, "\\123")])
<Name(CN=\\123)>
>>> Name.from_rfc4514_string(_.rfc4514_string())
<Name(CN=3)>
>>> Name([NameAttribute(NameOID.COMMON_NAME, "\\#123")])
<Name(CN=\\#123)>
>>> Name.from_rfc4514_string(_.rfc4514_string())
<Name(CN=\#123)>
```

I opened a new issue just in case to make sure this doesn't slip through the cracks.



Discussion:
@intgr Thanks for testing so quickly. Are there more edge cases you suspect exist/have test vectors for? We'll definitely want to fix anything that's wrong before release of course.

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
@@ -87,9 +87,6 @@ def _escape_dn_value(val: typing.Union[str, bytes]) -> str:
     return val
 
 
-_HEX_ESCAPE_RE = re.compile(r"\\([\da-zA-Z]{2})")
-
-
 def _unescape_dn_value(val: str) -> str:
     if not val:
         return ""
@@ -98,20 +95,15 @@ def _unescape_dn_value(val: str) -> str:
 
     # special = escaped / SPACE / SHARP / EQUALS
     # escaped = DQUOTE / PLUS / COMMA / SEMI / LANGLE / RANGLE
-    val = (
-        val.replace("\\\\", "\\")
-        .replace('\\"', '"')
-        .replace("\\+", "+")
-        .replace("\\,", ",")
-        .replace("\\;", ";")
-        .replace("\\<", "<")
-        .replace("\\>", ">")
-        .replace("\\ ", " ")
-        .replace("\\#", "#")
-        .replace("\\=", "=")
-    )
+    def sub(m):
+        val = m.group(1)
+        # Regular escape
+        if len(val) == 1:
+            return val
+        # Hex-value scape
+        return chr(int(val, 16))
 
-    return _HEX_ESCAPE_RE.sub(lambda m: chr(int(m.group(1), 16)), val)
+    return _RFC4514NameParser._PAIR_RE.sub(sub, val)
 
 
 class NameAttribute:
@@ -362,10 +354,11 @@ def __repr__(self) -> str:
 
 
 class _RFC4514NameParser:
-    _OID_RE = re.compile(r"(\d|[1-9]\d+)(\.\d|[1-9]\d+)+")
+    _OID_RE = re.compile(r"(0|([1-9]\d*))(\.(0|([1-9]\d*)))+")
     _DESCR_RE = re.compile(r"[a-zA-Z][a-zA-Z\d-]*")
 
     _PAIR = r"\\([\\ #=\"\+,;<>]|[\da-zA-Z]{2})"
+    _PAIR_RE = re.compile(_PAIR)
     _LUTF1 = r"[\x01-\x1f\x21\x24-\x2A\x2D-\x3A\x3D\x3F-\x5B\x5D-\x7F]"
     _SUTF1 = r"[\x01-\x21\x23-\x2A\x2D-\x3A\x3D\x3F-\x5B\x5D-\x7F]"
     _TUTF1 = r"[\x01-\x1F\x21\x23-\x2A\x2D-\x3A\x3D\x3F-\x5B\x5D-\x7F]"
@@ -445,6 +438,7 @@ def _parse_na(self) -> NameAttribute:
             value = self._read_re(self._HEXSTRING_RE)
             value = binascii.unhexlify(value[1:]).decode()
         else:
-            value = _unescape_dn_value(self._read_re(self._STRING_RE))
+            raw_value = self._read_re(self._STRING_RE)
+            value = _unescape_dn_value(raw_value)
 
         return NameAttribute(oid, value)


Test patch:
diff --git a/tests/x509/test_name.py b/tests/x509/test_name.py
--- a/tests/x509/test_name.py
+++ b/tests/x509/test_name.py
@@ -147,6 +147,19 @@ def test_valid(self, subtests):
             ),
             (r"CN=#616263", Name([NameAttribute(NameOID.COMMON_NAME, "abc")])),
             (r"CN=👍", Name([NameAttribute(NameOID.COMMON_NAME, "👍")])),
+            (
+                "CN=\\\\123",
+                Name([NameAttribute(NameOID.COMMON_NAME, "\\123")]),
+            ),
+            ("CN=\\\\\\;", Name([NameAttribute(NameOID.COMMON_NAME, "\\;")])),
+            (
+                "CN=\\\\#123",
+                Name([NameAttribute(NameOID.COMMON_NAME, "\\#123")]),
+            ),
+            (
+                "2.5.4.10=abc",
+                Name([NameAttribute(NameOID.ORGANIZATION_NAME, "abc")]),
+            ),
         ]:
             with subtests.test():
                 result = Name.from_rfc4514_string(value)