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): |