Add RFC 4514 Distinguished Name formatting for Name, RDN and NameAttribute (#4304)

diff --git a/AUTHORS.rst b/AUTHORS.rst
index ed9ac84..8ba7e0e 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -41,3 +41,4 @@
 * Jeremy Lainé <jeremy.laine@m4x.org>
 * Denis Gladkikh <denis@gladkikh.email>
 * John Pacific <me@johnpacific.com> (2CF6 0381 B5EF 29B7 D48C 2020 7BB9 71A0 E891 44D9)
+* Marti Raudsepp <marti@juffo.org>
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index eb7a4d8..25c7c8c 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -23,6 +23,10 @@
 * Added initial support for parsing PKCS12 files with
   :func:`~cryptography.hazmat.primitives.serialization.pkcs12.load_key_and_certificates`.
 * Added support for :class:`~cryptography.x509.IssuingDistributionPoint`.
+* Added `rfc4514_string()` method to :class:`~cryptography.x509.Name`,
+  :class:`~cryptography.x509.RelativeDistinguishedName` and
+  :class:`~cryptography.x509.NameAttribute` to format the name or component as
+  a RFC 4514 Distinguished Name string.
 
 .. _v2-4-2:
 
diff --git a/docs/x509/reference.rst b/docs/x509/reference.rst
index 1589105..ac6bbcd 100644
--- a/docs/x509/reference.rst
+++ b/docs/x509/reference.rst
@@ -583,7 +583,7 @@
         .. doctest::
 
             >>> crl.issuer
-            <Name([<NameAttribute(oid=<ObjectIdentifier(oid=2.5.4.6, name=countryName)>, value='US')>, <NameAttribute(oid=<ObjectIdentifier(oid=2.5.4.3, name=commonName)>, value='cryptography.io')>])>
+            <Name(C=US, CN=cryptography.io)>
 
     .. attribute:: next_update
 
@@ -1246,6 +1246,14 @@
 
         :return bytes: The DER encoded name.
 
+    .. method:: rfc4514_string()
+
+        .. versionadded:: 2.5
+
+        :return str: Format the given name as a `RFC 4514`_ Distinguished Name
+            string, for example ``CN=mydomain.com, O=My Org, C=US``.
+
+
 .. class:: Version
 
     .. versionadded:: 0.7
@@ -1279,6 +1287,13 @@
 
         The value of the attribute.
 
+    .. method:: rfc4514_string()
+
+        .. versionadded:: 2.5
+
+        :return str: Format the given attribute as a `RFC 4514`_ Distinguished
+            Name string.
+
 
 .. class:: RelativeDistinguishedName(attributes)
 
@@ -1295,6 +1310,13 @@
         :returns: A list of :class:`NameAttribute` instances that match the OID
             provided.  The list should contain zero or one values.
 
+    .. method:: rfc4514_string()
+
+        .. versionadded:: 2.5
+
+        :return str: Format the given RDN set as a `RFC 4514`_ Distinguished
+            Name string.
+
 
 .. class:: ObjectIdentifier
 
@@ -1309,6 +1331,8 @@
 
         The dotted string value of the OID (e.g. ``"2.5.4.3"``)
 
+.. _`RFC 4514`: https://tools.ietf.org/html/rfc4514
+
 .. _general_name_classes:
 
 General Name Classes
diff --git a/src/cryptography/x509/extensions.py b/src/cryptography/x509/extensions.py
index 12071b6..bdd445d 100644
--- a/src/cryptography/x509/extensions.py
+++ b/src/cryptography/x509/extensions.py
@@ -541,8 +541,8 @@
     def __repr__(self):
         return (
             "<DistributionPoint(full_name={0.full_name}, relative_name={0.rela"
-            "tive_name}, reasons={0.reasons}, crl_issuer={0.crl_is"
-            "suer})>".format(self)
+            "tive_name}, reasons={0.reasons}, crl_issuer={0.crl_issuer})>"
+            .format(self)
         )
 
     def __eq__(self, other):
diff --git a/src/cryptography/x509/name.py b/src/cryptography/x509/name.py
index 5548eda..470862c 100644
--- a/src/cryptography/x509/name.py
+++ b/src/cryptography/x509/name.py
@@ -36,6 +36,41 @@
     NameOID.DOMAIN_COMPONENT: _ASN1Type.IA5String,
 }
 
+#: Short attribute names from RFC 4514:
+#: https://tools.ietf.org/html/rfc4514#page-7
+_NAMEOID_TO_NAME = {
+    NameOID.COMMON_NAME: 'CN',
+    NameOID.LOCALITY_NAME: 'L',
+    NameOID.STATE_OR_PROVINCE_NAME: 'ST',
+    NameOID.ORGANIZATION_NAME: 'O',
+    NameOID.ORGANIZATIONAL_UNIT_NAME: 'OU',
+    NameOID.COUNTRY_NAME: 'C',
+    NameOID.STREET_ADDRESS: 'STREET',
+    NameOID.DOMAIN_COMPONENT: 'DC',
+    NameOID.USER_ID: 'UID',
+}
+
+
+def _escape_dn_value(val):
+    """Escape special characters in RFC4514 Distinguished Name value."""
+
+    # See https://tools.ietf.org/html/rfc4514#section-2.4
+    val = val.replace('\\', '\\\\')
+    val = val.replace('"', '\\"')
+    val = val.replace('+', '\\+')
+    val = val.replace(',', '\\,')
+    val = val.replace(';', '\\;')
+    val = val.replace('<', '\\<')
+    val = val.replace('>', '\\>')
+    val = val.replace('\0', '\\00')
+
+    if val[0] in ('#', ' '):
+        val = '\\' + val
+    if val[-1] == ' ':
+        val = val[:-1] + '\\ '
+
+    return val
+
 
 class NameAttribute(object):
     def __init__(self, oid, value, _type=_SENTINEL):
@@ -80,6 +115,16 @@
     oid = utils.read_only_property("_oid")
     value = utils.read_only_property("_value")
 
+    def rfc4514_string(self):
+        """
+        Format as RFC4514 Distinguished Name string.
+
+        Use short attribute name if available, otherwise fall back to OID
+        dotted string.
+        """
+        key = _NAMEOID_TO_NAME.get(self.oid, self.oid.dotted_string)
+        return '%s=%s' % (key, _escape_dn_value(self.value))
+
     def __eq__(self, other):
         if not isinstance(other, NameAttribute):
             return NotImplemented
@@ -117,6 +162,15 @@
     def get_attributes_for_oid(self, oid):
         return [i for i in self if i.oid == oid]
 
+    def rfc4514_string(self):
+        """
+        Format as RFC4514 Distinguished Name string.
+
+        Within each RDN, attributes are joined by '+', although that is rarely
+        used in certificates.
+        """
+        return '+'.join(attr.rfc4514_string() for attr in self._attributes)
+
     def __eq__(self, other):
         if not isinstance(other, RelativeDistinguishedName):
             return NotImplemented
@@ -136,7 +190,7 @@
         return len(self._attributes)
 
     def __repr__(self):
-        return "<RelativeDistinguishedName({0!r})>".format(list(self))
+        return "<RelativeDistinguishedName({0})>".format(self.rfc4514_string())
 
 
 class Name(object):
@@ -154,6 +208,18 @@
                 " or a list RelativeDistinguishedName"
             )
 
+    def rfc4514_string(self):
+        """
+        Format as RFC4514 Distinguished Name string.
+        For example 'CN=foobar.com,O=Foo Corp,C=US'
+
+        An X.509 name is a two-level structure: a list of sets of attributes.
+        Each list element is separated by ',' and within each list element, set
+        elements are separated by '+'. The latter is almost never used in
+        real world certificates.
+        """
+        return ', '.join(attr.rfc4514_string() for attr in self._attributes)
+
     def get_attributes_for_oid(self, oid):
         return [i for i in self if i.oid == oid]
 
@@ -187,4 +253,4 @@
         return sum(len(rdn) for rdn in self._attributes)
 
     def __repr__(self):
-        return "<Name({0!r})>".format(list(self))
+        return "<Name({0})>".format(self.rfc4514_string())
diff --git a/tests/x509/test_x509.py b/tests/x509/test_x509.py
index 15cfe43..f452081 100644
--- a/tests/x509/test_x509.py
+++ b/tests/x509/test_x509.py
@@ -1138,30 +1138,11 @@
             x509.load_pem_x509_certificate,
             backend
         )
-        if not six.PY2:
-            assert repr(cert) == (
-                "<Certificate(subject=<Name([<NameAttribute(oid=<ObjectIdentif"
-                "ier(oid=2.5.4.11, name=organizationalUnitName)>, value='GT487"
-                "42965')>, <NameAttribute(oid=<ObjectIdentifier(oid=2.5.4.11, "
-                "name=organizationalUnitName)>, value='See www.rapidssl.com/re"
-                "sources/cps (c)14')>, <NameAttribute(oid=<ObjectIdentifier(oi"
-                "d=2.5.4.11, name=organizationalUnitName)>, value='Domain Cont"
-                "rol Validated - RapidSSL(R)')>, <NameAttribute(oid=<ObjectIde"
-                "ntifier(oid=2.5.4.3, name=commonName)>, value='www.cryptograp"
-                "hy.io')>])>, ...)>"
-            )
-        else:
-            assert repr(cert) == (
-                "<Certificate(subject=<Name([<NameAttribute(oid=<ObjectIdentif"
-                "ier(oid=2.5.4.11, name=organizationalUnitName)>, value=u'GT48"
-                "742965')>, <NameAttribute(oid=<ObjectIdentifier(oid=2.5.4.11,"
-                " name=organizationalUnitName)>, value=u'See www.rapidssl.com/"
-                "resources/cps (c)14')>, <NameAttribute(oid=<ObjectIdentifier("
-                "oid=2.5.4.11, name=organizationalUnitName)>, value=u'Domain C"
-                "ontrol Validated - RapidSSL(R)')>, <NameAttribute(oid=<Object"
-                "Identifier(oid=2.5.4.3, name=commonName)>, value=u'www.crypto"
-                "graphy.io')>])>, ...)>"
-            )
+        assert repr(cert) == (
+            "<Certificate(subject=<Name(OU=GT48742965, OU=See www.rapidssl.com"
+            "/resources/cps (c)14, OU=Domain Control Validated - RapidSSL(R), "
+            "CN=www.cryptography.io)>, ...)>"
+        )
 
     def test_parse_tls_feature_extension(self, backend):
         cert = _load_cert(
@@ -3933,6 +3914,18 @@
                 "nName)>, value=u'value')>"
             )
 
+    def test_distinugished_name(self):
+        # Escaping
+        na = x509.NameAttribute(NameOID.COMMON_NAME, u'James "Jim" Smith, III')
+        assert na.rfc4514_string() == r'CN=James \"Jim\" Smith\, III'
+        na = x509.NameAttribute(NameOID.USER_ID, u'# escape+,;\0this ')
+        assert na.rfc4514_string() == r'UID=\# escape\+\,\;\00this\ '
+
+        # Nonstandard attribute OID
+        na = x509.NameAttribute(NameOID.EMAIL_ADDRESS, u'somebody@example.com')
+        assert (na.rfc4514_string() ==
+                '1.2.840.113549.1.9.1=somebody@example.com')
+
 
 class TestRelativeDistinguishedName(object):
     def test_init_empty(self):
@@ -4120,20 +4113,23 @@
             x509.NameAttribute(NameOID.ORGANIZATION_NAME, u'PyCA'),
         ])
 
-        if not six.PY2:
-            assert repr(name) == (
-                "<Name([<NameAttribute(oid=<ObjectIdentifier(oid=2.5.4.3, name"
-                "=commonName)>, value='cryptography.io')>, <NameAttribute(oid="
-                "<ObjectIdentifier(oid=2.5.4.10, name=organizationName)>, valu"
-                "e='PyCA')>])>"
-            )
-        else:
-            assert repr(name) == (
-                "<Name([<NameAttribute(oid=<ObjectIdentifier(oid=2.5.4.3, name"
-                "=commonName)>, value=u'cryptography.io')>, <NameAttribute(oid"
-                "=<ObjectIdentifier(oid=2.5.4.10, name=organizationName)>, val"
-                "ue=u'PyCA')>])>"
-            )
+        assert repr(name) == "<Name(CN=cryptography.io, O=PyCA)>"
+
+    def test_rfc4514_string(self):
+        n = x509.Name([
+            x509.RelativeDistinguishedName([
+                x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, u'Sales'),
+                x509.NameAttribute(NameOID.COMMON_NAME, u'J.  Smith'),
+            ]),
+            x509.RelativeDistinguishedName([
+                x509.NameAttribute(NameOID.DOMAIN_COMPONENT, u'example'),
+            ]),
+            x509.RelativeDistinguishedName([
+                x509.NameAttribute(NameOID.DOMAIN_COMPONENT, u'net'),
+            ]),
+        ])
+        assert (n.rfc4514_string() ==
+                'OU=Sales+CN=J.  Smith, DC=example, DC=net')
 
     def test_not_nameattribute(self):
         with pytest.raises(TypeError):
diff --git a/tests/x509/test_x509_ext.py b/tests/x509/test_x509_ext.py
index 152db96..6de105f 100644
--- a/tests/x509/test_x509_ext.py
+++ b/tests/x509/test_x509_ext.py
@@ -1135,16 +1135,14 @@
         if not six.PY2:
             assert repr(aki) == (
                 "<AuthorityKeyIdentifier(key_identifier=b'digest', authority_"
-                "cert_issuer=[<DirectoryName(value=<Name([<NameAttribute(oid="
-                "<ObjectIdentifier(oid=2.5.4.3, name=commonName)>, value='myC"
-                "N')>])>)>], authority_cert_serial_number=1234)>"
+                "cert_issuer=[<DirectoryName(value=<Name(CN=myCN)>)>], author"
+                "ity_cert_serial_number=1234)>"
             )
         else:
             assert repr(aki) == (
-                "<AuthorityKeyIdentifier(key_identifier='digest', authority_ce"
-                "rt_issuer=[<DirectoryName(value=<Name([<NameAttribute(oid=<Ob"
-                "jectIdentifier(oid=2.5.4.3, name=commonName)>, value=u'myCN')"
-                ">])>)>], authority_cert_serial_number=1234)>"
+                "<AuthorityKeyIdentifier(key_identifier='digest', authority_"
+                "cert_issuer=[<DirectoryName(value=<Name(CN=myCN)>)>], author"
+                "ity_cert_serial_number=1234)>"
             )
 
     def test_eq(self):
@@ -1719,16 +1717,7 @@
     def test_repr(self):
         name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, u'value1')])
         gn = x509.DirectoryName(name)
-        if not six.PY2:
-            assert repr(gn) == (
-                "<DirectoryName(value=<Name([<NameAttribute(oid=<ObjectIdentif"
-                "ier(oid=2.5.4.3, name=commonName)>, value='value1')>])>)>"
-            )
-        else:
-            assert repr(gn) == (
-                "<DirectoryName(value=<Name([<NameAttribute(oid=<ObjectIdentif"
-                "ier(oid=2.5.4.3, name=commonName)>, value=u'value1')>])>)>"
-            )
+        assert repr(gn) == "<DirectoryName(value=<Name(CN=value1)>)>"
 
     def test_eq(self):
         name = x509.Name([
@@ -3656,22 +3645,16 @@
         if not six.PY2:
             assert repr(dp) == (
                 "<DistributionPoint(full_name=None, relative_name=<RelativeDis"
-                "tinguishedName([<NameAttribute(oid=<ObjectIdentifier(oid=2.5."
-                "4.3, name=commonName)>, value='myCN')>])>, reasons=frozenset("
-                "{<ReasonFlags.ca_compromise: 'cACompromise'>}), crl_issuer=[<"
-                "DirectoryName(value=<Name([<NameAttribute(oid=<ObjectIdentifi"
-                "er(oid=2.5.4.3, name=commonName)>, value='Important CA')>])>)"
-                ">])>"
+                "tinguishedName(CN=myCN)>, reasons=frozenset({<ReasonFlags.ca_"
+                "compromise: 'cACompromise'>}), crl_issuer=[<DirectoryName(val"
+                "ue=<Name(CN=Important CA)>)>])>"
             )
         else:
             assert repr(dp) == (
                 "<DistributionPoint(full_name=None, relative_name=<RelativeDis"
-                "tinguishedName([<NameAttribute(oid=<ObjectIdentifier(oid=2.5."
-                "4.3, name=commonName)>, value=u'myCN')>])>, reasons=frozenset"
-                "([<ReasonFlags.ca_compromise: 'cACompromise'>]), crl_issuer=["
-                "<DirectoryName(value=<Name([<NameAttribute(oid=<ObjectIdentif"
-                "ier(oid=2.5.4.3, name=commonName)>, value=u'Important CA')>])"
-                ">)>])>"
+                "tinguishedName(CN=myCN)>, reasons=frozenset([<ReasonFlags.ca_"
+                "compromise: 'cACompromise'>]), crl_issuer=[<DirectoryName(val"
+                "ue=<Name(CN=Important CA)>)>])>"
             )
 
     def test_hash(self):