From c3d38b5d80a955aee4b160bb97464a20c4992da7 Mon Sep 17 00:00:00 2001 From: Marti Raudsepp Date: Sat, 8 Dec 2018 03:26:07 +0200 Subject: Add RFC 4514 Distinguished Name formatting for Name, RDN and NameAttribute (#4304) --- AUTHORS.rst | 1 + CHANGELOG.rst | 4 +++ docs/x509/reference.rst | 26 +++++++++++++- src/cryptography/x509/extensions.py | 4 +-- src/cryptography/x509/name.py | 70 ++++++++++++++++++++++++++++++++++-- tests/x509/test_x509.py | 72 ++++++++++++++++++------------------- tests/x509/test_x509_ext.py | 41 +++++++-------------- 7 files changed, 146 insertions(+), 72 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index ed9ac84e..8ba7e0ed 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -41,3 +41,4 @@ PGP key fingerprints are enclosed in parentheses. * Jeremy Lainé * Denis Gladkikh * John Pacific (2CF6 0381 B5EF 29B7 D48C 2020 7BB9 71A0 E891 44D9) +* Marti Raudsepp diff --git a/CHANGELOG.rst b/CHANGELOG.rst index eb7a4d89..25c7c8c3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -23,6 +23,10 @@ Changelog * 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 15891059..ac6bbcdc 100644 --- a/docs/x509/reference.rst +++ b/docs/x509/reference.rst @@ -583,7 +583,7 @@ X.509 CRL (Certificate Revocation List) Object .. doctest:: >>> crl.issuer - , value='US')>, , value='cryptography.io')>])> + .. attribute:: next_update @@ -1246,6 +1246,14 @@ X.509 CSR (Certificate Signing Request) Builder Object :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 @@ X.509 CSR (Certificate Signing Request) Builder Object 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 @@ X.509 CSR (Certificate Signing Request) Builder Object :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 @@ X.509 CSR (Certificate Signing Request) Builder Object 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 12071b66..bdd445d9 100644 --- a/src/cryptography/x509/extensions.py +++ b/src/cryptography/x509/extensions.py @@ -541,8 +541,8 @@ class DistributionPoint(object): def __repr__(self): return ( "".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 5548eda8..470862c2 100644 --- a/src/cryptography/x509/name.py +++ b/src/cryptography/x509/name.py @@ -36,6 +36,41 @@ _NAMEOID_DEFAULT_TYPE = { 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 @@ class NameAttribute(object): 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 @@ class RelativeDistinguishedName(object): 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 @@ class RelativeDistinguishedName(object): return len(self._attributes) def __repr__(self): - return "".format(list(self)) + return "".format(self.rfc4514_string()) class Name(object): @@ -154,6 +208,18 @@ class Name(object): " 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 @@ class Name(object): return sum(len(rdn) for rdn in self._attributes) def __repr__(self): - return "".format(list(self)) + return "".format(self.rfc4514_string()) diff --git a/tests/x509/test_x509.py b/tests/x509/test_x509.py index 15cfe43d..f4520811 100644 --- a/tests/x509/test_x509.py +++ b/tests/x509/test_x509.py @@ -1138,30 +1138,11 @@ class TestRSACertificate(object): x509.load_pem_x509_certificate, backend ) - if not six.PY2: - assert repr(cert) == ( - ", value='GT487" - "42965')>, , value='See www.rapidssl.com/re" - "sources/cps (c)14')>, , value='Domain Cont" - "rol Validated - RapidSSL(R)')>, , value='www.cryptograp" - "hy.io')>])>, ...)>" - ) - else: - assert repr(cert) == ( - ", value=u'GT48" - "742965')>, , value=u'See www.rapidssl.com/" - "resources/cps (c)14')>, , value=u'Domain C" - "ontrol Validated - RapidSSL(R)')>, , value=u'www.crypto" - "graphy.io')>])>, ...)>" - ) + assert repr(cert) == ( + ", ...)>" + ) def test_parse_tls_feature_extension(self, backend): cert = _load_cert( @@ -3933,6 +3914,18 @@ class TestNameAttribute(object): "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 @@ class TestName(object): x509.NameAttribute(NameOID.ORGANIZATION_NAME, u'PyCA'), ]) - if not six.PY2: - assert repr(name) == ( - ", value='cryptography.io')>, , valu" - "e='PyCA')>])>" - ) - else: - assert repr(name) == ( - ", value=u'cryptography.io')>, , val" - "ue=u'PyCA')>])>" - ) + assert repr(name) == "" + + 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 152db964..6de105fa 100644 --- a/tests/x509/test_x509_ext.py +++ b/tests/x509/test_x509_ext.py @@ -1135,16 +1135,14 @@ class TestAuthorityKeyIdentifier(object): if not six.PY2: assert repr(aki) == ( ", value='myC" - "N')>])>)>], authority_cert_serial_number=1234)>" + "cert_issuer=[)>], author" + "ity_cert_serial_number=1234)>" ) else: assert repr(aki) == ( - ", value=u'myCN')" - ">])>)>], authority_cert_serial_number=1234)>" + ")>], author" + "ity_cert_serial_number=1234)>" ) def test_eq(self): @@ -1719,16 +1717,7 @@ class TestDirectoryName(object): 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) == ( - ", value='value1')>])>)>" - ) - else: - assert repr(gn) == ( - ", value=u'value1')>])>)>" - ) + assert repr(gn) == ")>" def test_eq(self): name = x509.Name([ @@ -3656,22 +3645,16 @@ class TestDistributionPoint(object): if not six.PY2: assert repr(dp) == ( ", value='myCN')>])>, reasons=frozenset(" - "{}), crl_issuer=[<" - "DirectoryName(value=, value='Important CA')>])>)" - ">])>" + "tinguishedName(CN=myCN)>, reasons=frozenset({}), crl_issuer=[)>])>" ) else: assert repr(dp) == ( ", value=u'myCN')>])>, reasons=frozenset" - "([]), crl_issuer=[" - ", value=u'Important CA')>])" - ">)>])>" + "tinguishedName(CN=myCN)>, reasons=frozenset([]), crl_issuer=[)>])>" ) def test_hash(self): -- cgit v1.2.3