diff options
| -rw-r--r-- | AUTHORS.rst | 1 | ||||
| -rw-r--r-- | CHANGELOG.rst | 4 | ||||
| -rw-r--r-- | docs/x509/reference.rst | 26 | ||||
| -rw-r--r-- | src/cryptography/x509/extensions.py | 4 | ||||
| -rw-r--r-- | src/cryptography/x509/name.py | 70 | ||||
| -rw-r--r-- | tests/x509/test_x509.py | 72 | ||||
| -rw-r--r-- | 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é <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 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 -            <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 @@ 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 (              "<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 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 "<RelativeDistinguishedName({0!r})>".format(list(self)) +        return "<RelativeDistinguishedName({0})>".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 "<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 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) == ( -                "<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 @@ 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) == ( -                "<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 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) == (                  "<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 @@ 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) == ( -                "<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 @@ class TestDistributionPoint(object):          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): | 
