Skip to content

Commit c3d38b5

Browse files
intgrreaperhulk
authored andcommitted
Add RFC 4514 Distinguished Name formatting for Name, RDN and NameAttribute (pyca#4304)
1 parent 7e42282 commit c3d38b5

File tree

7 files changed

+146
-72
lines changed

7 files changed

+146
-72
lines changed

Diff for: AUTHORS.rst

+1
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,4 @@ PGP key fingerprints are enclosed in parentheses.
4141
* Jeremy Lainé <[email protected]>
4242
* Denis Gladkikh <[email protected]>
4343
* John Pacific <[email protected]> (2CF6 0381 B5EF 29B7 D48C 2020 7BB9 71A0 E891 44D9)
44+
* Marti Raudsepp <[email protected]>

Diff for: CHANGELOG.rst

+4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ Changelog
2323
* Added initial support for parsing PKCS12 files with
2424
:func:`~cryptography.hazmat.primitives.serialization.pkcs12.load_key_and_certificates`.
2525
* Added support for :class:`~cryptography.x509.IssuingDistributionPoint`.
26+
* Added `rfc4514_string()` method to :class:`~cryptography.x509.Name`,
27+
:class:`~cryptography.x509.RelativeDistinguishedName` and
28+
:class:`~cryptography.x509.NameAttribute` to format the name or component as
29+
a RFC 4514 Distinguished Name string.
2630

2731
.. _v2-4-2:
2832

Diff for: docs/x509/reference.rst

+25-1
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,7 @@ X.509 CRL (Certificate Revocation List) Object
583583
.. doctest::
584584

585585
>>> crl.issuer
586-
<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')>])>
586+
<Name(C=US, CN=cryptography.io)>
587587

588588
.. attribute:: next_update
589589

@@ -1246,6 +1246,14 @@ X.509 CSR (Certificate Signing Request) Builder Object
12461246

12471247
:return bytes: The DER encoded name.
12481248

1249+
.. method:: rfc4514_string()
1250+
1251+
.. versionadded:: 2.5
1252+
1253+
:return str: Format the given name as a `RFC 4514`_ Distinguished Name
1254+
string, for example ``CN=mydomain.com, O=My Org, C=US``.
1255+
1256+
12491257
.. class:: Version
12501258

12511259
.. versionadded:: 0.7
@@ -1279,6 +1287,13 @@ X.509 CSR (Certificate Signing Request) Builder Object
12791287

12801288
The value of the attribute.
12811289

1290+
.. method:: rfc4514_string()
1291+
1292+
.. versionadded:: 2.5
1293+
1294+
:return str: Format the given attribute as a `RFC 4514`_ Distinguished
1295+
Name string.
1296+
12821297

12831298
.. class:: RelativeDistinguishedName(attributes)
12841299

@@ -1295,6 +1310,13 @@ X.509 CSR (Certificate Signing Request) Builder Object
12951310
:returns: A list of :class:`NameAttribute` instances that match the OID
12961311
provided. The list should contain zero or one values.
12971312

1313+
.. method:: rfc4514_string()
1314+
1315+
.. versionadded:: 2.5
1316+
1317+
:return str: Format the given RDN set as a `RFC 4514`_ Distinguished
1318+
Name string.
1319+
12981320

12991321
.. class:: ObjectIdentifier
13001322

@@ -1309,6 +1331,8 @@ X.509 CSR (Certificate Signing Request) Builder Object
13091331

13101332
The dotted string value of the OID (e.g. ``"2.5.4.3"``)
13111333

1334+
.. _`RFC 4514`: https://tools.ietf.org/html/rfc4514
1335+
13121336
.. _general_name_classes:
13131337

13141338
General Name Classes

Diff for: src/cryptography/x509/extensions.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -541,8 +541,8 @@ def __init__(self, full_name, relative_name, reasons, crl_issuer):
541541
def __repr__(self):
542542
return (
543543
"<DistributionPoint(full_name={0.full_name}, relative_name={0.rela"
544-
"tive_name}, reasons={0.reasons}, crl_issuer={0.crl_is"
545-
"suer})>".format(self)
544+
"tive_name}, reasons={0.reasons}, crl_issuer={0.crl_issuer})>"
545+
.format(self)
546546
)
547547

548548
def __eq__(self, other):

Diff for: src/cryptography/x509/name.py

+68-2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,41 @@ class _ASN1Type(Enum):
3636
NameOID.DOMAIN_COMPONENT: _ASN1Type.IA5String,
3737
}
3838

39+
#: Short attribute names from RFC 4514:
40+
#: https://tools.ietf.org/html/rfc4514#page-7
41+
_NAMEOID_TO_NAME = {
42+
NameOID.COMMON_NAME: 'CN',
43+
NameOID.LOCALITY_NAME: 'L',
44+
NameOID.STATE_OR_PROVINCE_NAME: 'ST',
45+
NameOID.ORGANIZATION_NAME: 'O',
46+
NameOID.ORGANIZATIONAL_UNIT_NAME: 'OU',
47+
NameOID.COUNTRY_NAME: 'C',
48+
NameOID.STREET_ADDRESS: 'STREET',
49+
NameOID.DOMAIN_COMPONENT: 'DC',
50+
NameOID.USER_ID: 'UID',
51+
}
52+
53+
54+
def _escape_dn_value(val):
55+
"""Escape special characters in RFC4514 Distinguished Name value."""
56+
57+
# See https://tools.ietf.org/html/rfc4514#section-2.4
58+
val = val.replace('\\', '\\\\')
59+
val = val.replace('"', '\\"')
60+
val = val.replace('+', '\\+')
61+
val = val.replace(',', '\\,')
62+
val = val.replace(';', '\\;')
63+
val = val.replace('<', '\\<')
64+
val = val.replace('>', '\\>')
65+
val = val.replace('\0', '\\00')
66+
67+
if val[0] in ('#', ' '):
68+
val = '\\' + val
69+
if val[-1] == ' ':
70+
val = val[:-1] + '\\ '
71+
72+
return val
73+
3974

4075
class NameAttribute(object):
4176
def __init__(self, oid, value, _type=_SENTINEL):
@@ -80,6 +115,16 @@ def __init__(self, oid, value, _type=_SENTINEL):
80115
oid = utils.read_only_property("_oid")
81116
value = utils.read_only_property("_value")
82117

118+
def rfc4514_string(self):
119+
"""
120+
Format as RFC4514 Distinguished Name string.
121+
122+
Use short attribute name if available, otherwise fall back to OID
123+
dotted string.
124+
"""
125+
key = _NAMEOID_TO_NAME.get(self.oid, self.oid.dotted_string)
126+
return '%s=%s' % (key, _escape_dn_value(self.value))
127+
83128
def __eq__(self, other):
84129
if not isinstance(other, NameAttribute):
85130
return NotImplemented
@@ -117,6 +162,15 @@ def __init__(self, attributes):
117162
def get_attributes_for_oid(self, oid):
118163
return [i for i in self if i.oid == oid]
119164

165+
def rfc4514_string(self):
166+
"""
167+
Format as RFC4514 Distinguished Name string.
168+
169+
Within each RDN, attributes are joined by '+', although that is rarely
170+
used in certificates.
171+
"""
172+
return '+'.join(attr.rfc4514_string() for attr in self._attributes)
173+
120174
def __eq__(self, other):
121175
if not isinstance(other, RelativeDistinguishedName):
122176
return NotImplemented
@@ -136,7 +190,7 @@ def __len__(self):
136190
return len(self._attributes)
137191

138192
def __repr__(self):
139-
return "<RelativeDistinguishedName({0!r})>".format(list(self))
193+
return "<RelativeDistinguishedName({0})>".format(self.rfc4514_string())
140194

141195

142196
class Name(object):
@@ -154,6 +208,18 @@ def __init__(self, attributes):
154208
" or a list RelativeDistinguishedName"
155209
)
156210

211+
def rfc4514_string(self):
212+
"""
213+
Format as RFC4514 Distinguished Name string.
214+
For example 'CN=foobar.com,O=Foo Corp,C=US'
215+
216+
An X.509 name is a two-level structure: a list of sets of attributes.
217+
Each list element is separated by ',' and within each list element, set
218+
elements are separated by '+'. The latter is almost never used in
219+
real world certificates.
220+
"""
221+
return ', '.join(attr.rfc4514_string() for attr in self._attributes)
222+
157223
def get_attributes_for_oid(self, oid):
158224
return [i for i in self if i.oid == oid]
159225

@@ -187,4 +253,4 @@ def __len__(self):
187253
return sum(len(rdn) for rdn in self._attributes)
188254

189255
def __repr__(self):
190-
return "<Name({0!r})>".format(list(self))
256+
return "<Name({0})>".format(self.rfc4514_string())

Diff for: tests/x509/test_x509.py

+34-38
Original file line numberDiff line numberDiff line change
@@ -1138,30 +1138,11 @@ def test_certificate_repr(self, backend):
11381138
x509.load_pem_x509_certificate,
11391139
backend
11401140
)
1141-
if not six.PY2:
1142-
assert repr(cert) == (
1143-
"<Certificate(subject=<Name([<NameAttribute(oid=<ObjectIdentif"
1144-
"ier(oid=2.5.4.11, name=organizationalUnitName)>, value='GT487"
1145-
"42965')>, <NameAttribute(oid=<ObjectIdentifier(oid=2.5.4.11, "
1146-
"name=organizationalUnitName)>, value='See www.rapidssl.com/re"
1147-
"sources/cps (c)14')>, <NameAttribute(oid=<ObjectIdentifier(oi"
1148-
"d=2.5.4.11, name=organizationalUnitName)>, value='Domain Cont"
1149-
"rol Validated - RapidSSL(R)')>, <NameAttribute(oid=<ObjectIde"
1150-
"ntifier(oid=2.5.4.3, name=commonName)>, value='www.cryptograp"
1151-
"hy.io')>])>, ...)>"
1152-
)
1153-
else:
1154-
assert repr(cert) == (
1155-
"<Certificate(subject=<Name([<NameAttribute(oid=<ObjectIdentif"
1156-
"ier(oid=2.5.4.11, name=organizationalUnitName)>, value=u'GT48"
1157-
"742965')>, <NameAttribute(oid=<ObjectIdentifier(oid=2.5.4.11,"
1158-
" name=organizationalUnitName)>, value=u'See www.rapidssl.com/"
1159-
"resources/cps (c)14')>, <NameAttribute(oid=<ObjectIdentifier("
1160-
"oid=2.5.4.11, name=organizationalUnitName)>, value=u'Domain C"
1161-
"ontrol Validated - RapidSSL(R)')>, <NameAttribute(oid=<Object"
1162-
"Identifier(oid=2.5.4.3, name=commonName)>, value=u'www.crypto"
1163-
"graphy.io')>])>, ...)>"
1164-
)
1141+
assert repr(cert) == (
1142+
"<Certificate(subject=<Name(OU=GT48742965, OU=See www.rapidssl.com"
1143+
"/resources/cps (c)14, OU=Domain Control Validated - RapidSSL(R), "
1144+
"CN=www.cryptography.io)>, ...)>"
1145+
)
11651146

11661147
def test_parse_tls_feature_extension(self, backend):
11671148
cert = _load_cert(
@@ -3933,6 +3914,18 @@ def test_repr(self):
39333914
"nName)>, value=u'value')>"
39343915
)
39353916

3917+
def test_distinugished_name(self):
3918+
# Escaping
3919+
na = x509.NameAttribute(NameOID.COMMON_NAME, u'James "Jim" Smith, III')
3920+
assert na.rfc4514_string() == r'CN=James \"Jim\" Smith\, III'
3921+
na = x509.NameAttribute(NameOID.USER_ID, u'# escape+,;\0this ')
3922+
assert na.rfc4514_string() == r'UID=\# escape\+\,\;\00this\ '
3923+
3924+
# Nonstandard attribute OID
3925+
na = x509.NameAttribute(NameOID.EMAIL_ADDRESS, u'[email protected]')
3926+
assert (na.rfc4514_string() ==
3927+
3928+
39363929

39373930
class TestRelativeDistinguishedName(object):
39383931
def test_init_empty(self):
@@ -4120,20 +4113,23 @@ def test_repr(self):
41204113
x509.NameAttribute(NameOID.ORGANIZATION_NAME, u'PyCA'),
41214114
])
41224115

4123-
if not six.PY2:
4124-
assert repr(name) == (
4125-
"<Name([<NameAttribute(oid=<ObjectIdentifier(oid=2.5.4.3, name"
4126-
"=commonName)>, value='cryptography.io')>, <NameAttribute(oid="
4127-
"<ObjectIdentifier(oid=2.5.4.10, name=organizationName)>, valu"
4128-
"e='PyCA')>])>"
4129-
)
4130-
else:
4131-
assert repr(name) == (
4132-
"<Name([<NameAttribute(oid=<ObjectIdentifier(oid=2.5.4.3, name"
4133-
"=commonName)>, value=u'cryptography.io')>, <NameAttribute(oid"
4134-
"=<ObjectIdentifier(oid=2.5.4.10, name=organizationName)>, val"
4135-
"ue=u'PyCA')>])>"
4136-
)
4116+
assert repr(name) == "<Name(CN=cryptography.io, O=PyCA)>"
4117+
4118+
def test_rfc4514_string(self):
4119+
n = x509.Name([
4120+
x509.RelativeDistinguishedName([
4121+
x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, u'Sales'),
4122+
x509.NameAttribute(NameOID.COMMON_NAME, u'J. Smith'),
4123+
]),
4124+
x509.RelativeDistinguishedName([
4125+
x509.NameAttribute(NameOID.DOMAIN_COMPONENT, u'example'),
4126+
]),
4127+
x509.RelativeDistinguishedName([
4128+
x509.NameAttribute(NameOID.DOMAIN_COMPONENT, u'net'),
4129+
]),
4130+
])
4131+
assert (n.rfc4514_string() ==
4132+
'OU=Sales+CN=J. Smith, DC=example, DC=net')
41374133

41384134
def test_not_nameattribute(self):
41394135
with pytest.raises(TypeError):

Diff for: tests/x509/test_x509_ext.py

+12-29
Original file line numberDiff line numberDiff line change
@@ -1135,16 +1135,14 @@ def test_repr(self):
11351135
if not six.PY2:
11361136
assert repr(aki) == (
11371137
"<AuthorityKeyIdentifier(key_identifier=b'digest', authority_"
1138-
"cert_issuer=[<DirectoryName(value=<Name([<NameAttribute(oid="
1139-
"<ObjectIdentifier(oid=2.5.4.3, name=commonName)>, value='myC"
1140-
"N')>])>)>], authority_cert_serial_number=1234)>"
1138+
"cert_issuer=[<DirectoryName(value=<Name(CN=myCN)>)>], author"
1139+
"ity_cert_serial_number=1234)>"
11411140
)
11421141
else:
11431142
assert repr(aki) == (
1144-
"<AuthorityKeyIdentifier(key_identifier='digest', authority_ce"
1145-
"rt_issuer=[<DirectoryName(value=<Name([<NameAttribute(oid=<Ob"
1146-
"jectIdentifier(oid=2.5.4.3, name=commonName)>, value=u'myCN')"
1147-
">])>)>], authority_cert_serial_number=1234)>"
1143+
"<AuthorityKeyIdentifier(key_identifier='digest', authority_"
1144+
"cert_issuer=[<DirectoryName(value=<Name(CN=myCN)>)>], author"
1145+
"ity_cert_serial_number=1234)>"
11481146
)
11491147

11501148
def test_eq(self):
@@ -1719,16 +1717,7 @@ def test_not_name(self):
17191717
def test_repr(self):
17201718
name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, u'value1')])
17211719
gn = x509.DirectoryName(name)
1722-
if not six.PY2:
1723-
assert repr(gn) == (
1724-
"<DirectoryName(value=<Name([<NameAttribute(oid=<ObjectIdentif"
1725-
"ier(oid=2.5.4.3, name=commonName)>, value='value1')>])>)>"
1726-
)
1727-
else:
1728-
assert repr(gn) == (
1729-
"<DirectoryName(value=<Name([<NameAttribute(oid=<ObjectIdentif"
1730-
"ier(oid=2.5.4.3, name=commonName)>, value=u'value1')>])>)>"
1731-
)
1720+
assert repr(gn) == "<DirectoryName(value=<Name(CN=value1)>)>"
17321721

17331722
def test_eq(self):
17341723
name = x509.Name([
@@ -3656,22 +3645,16 @@ def test_repr(self):
36563645
if not six.PY2:
36573646
assert repr(dp) == (
36583647
"<DistributionPoint(full_name=None, relative_name=<RelativeDis"
3659-
"tinguishedName([<NameAttribute(oid=<ObjectIdentifier(oid=2.5."
3660-
"4.3, name=commonName)>, value='myCN')>])>, reasons=frozenset("
3661-
"{<ReasonFlags.ca_compromise: 'cACompromise'>}), crl_issuer=[<"
3662-
"DirectoryName(value=<Name([<NameAttribute(oid=<ObjectIdentifi"
3663-
"er(oid=2.5.4.3, name=commonName)>, value='Important CA')>])>)"
3664-
">])>"
3648+
"tinguishedName(CN=myCN)>, reasons=frozenset({<ReasonFlags.ca_"
3649+
"compromise: 'cACompromise'>}), crl_issuer=[<DirectoryName(val"
3650+
"ue=<Name(CN=Important CA)>)>])>"
36653651
)
36663652
else:
36673653
assert repr(dp) == (
36683654
"<DistributionPoint(full_name=None, relative_name=<RelativeDis"
3669-
"tinguishedName([<NameAttribute(oid=<ObjectIdentifier(oid=2.5."
3670-
"4.3, name=commonName)>, value=u'myCN')>])>, reasons=frozenset"
3671-
"([<ReasonFlags.ca_compromise: 'cACompromise'>]), crl_issuer=["
3672-
"<DirectoryName(value=<Name([<NameAttribute(oid=<ObjectIdentif"
3673-
"ier(oid=2.5.4.3, name=commonName)>, value=u'Important CA')>])"
3674-
">)>])>"
3655+
"tinguishedName(CN=myCN)>, reasons=frozenset([<ReasonFlags.ca_"
3656+
"compromise: 'cACompromise'>]), crl_issuer=[<DirectoryName(val"
3657+
"ue=<Name(CN=Important CA)>)>])>"
36753658
)
36763659

36773660
def test_hash(self):

0 commit comments

Comments
 (0)