Skip to content

Commit c7279e5

Browse files
committed
Support name constraints
1 parent f9fe714 commit c7279e5

2 files changed

Lines changed: 146 additions & 0 deletions

File tree

autograph_utils/__init__.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,20 @@ def detail(self):
181181
return f"Unknown public key type for {self.cert!r}: {self.key!r}"
182182

183183

184+
class CertificateChainNameNotPermitted(BadCertificate):
185+
def __init__(self, permitted_subtrees, current, next):
186+
self.permitted_subtrees = permitted_subtrees
187+
self.current = current
188+
self.next = next
189+
190+
@property
191+
def detail(self):
192+
return (
193+
f"Certificate name of {self.next!r} does not match the permitted names "
194+
f"for {self.current!r}: {self.permitted_subtrees!r}"
195+
)
196+
197+
184198
class CertificateLeafHasWrongKeyUsage(BadCertificate):
185199
def __init__(self, cert, key_usage):
186200
self.cert = cert
@@ -194,6 +208,20 @@ def detail(self):
194208
)
195209

196210

211+
class CertificateChainNameExcluded(BadCertificate):
212+
def __init__(self, excluded_subtrees, current, next):
213+
self.excluded_subtrees = excluded_subtrees
214+
self.current = current
215+
self.next = next
216+
217+
@property
218+
def detail(self):
219+
return (
220+
f"Certificate name of {self.next!r} matches the excluded names "
221+
f"for {self.current!r}: {self.excluded_subtrees!r}"
222+
)
223+
224+
197225
class BadSignature(Exception):
198226
detail = "Unknown signature problem"
199227

@@ -317,6 +345,7 @@ async def verify_x5u(self, url):
317345
current_cert = chain[0]
318346
for next_cert in chain[1:]:
319347
self._verify_cert_link(current_cert, next_cert)
348+
self._check_name_constraints(current_cert, next_cert)
320349

321350
current_cert = next_cert
322351

@@ -368,6 +397,51 @@ def _verify_cert_link(self, current_cert, next_cert):
368397
else:
369398
raise CertificateUnknownPublicKey(current_cert, key)
370399

400+
def _check_name_constraints(self, current_cert, next_cert):
401+
try:
402+
nc = current_cert.extensions.get_extension_for_class(
403+
cryptography.x509.NameConstraints
404+
).value
405+
except x509.ExtensionNotFound:
406+
return
407+
408+
name = next_cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
409+
if nc.permitted_subtrees:
410+
for constraint in nc.permitted_subtrees:
411+
if _name_constraint_matches(name, constraint):
412+
break
413+
else:
414+
raise CertificateChainNameNotPermitted(
415+
nc.permitted_subtrees, current=current_cert, next=next_cert
416+
)
417+
418+
excluded_subtrees = nc.excluded_subtrees or []
419+
420+
for constraint in excluded_subtrees:
421+
if _name_constraint_matches(name, constraint):
422+
raise CertificateChainNameExcluded(
423+
nc.excluded_subtrees, current=current_cert, next=next_cert
424+
)
425+
426+
427+
def _name_constraint_matches(hostname, name_constraint):
428+
"""Check if a name matches a constraint.
429+
430+
Taken from
431+
https://github.com/alex/x509-validator/blob/master/validator.py.
432+
433+
"""
434+
if not isinstance(name_constraint, x509.DNSName):
435+
return False
436+
constraint_hostname = name_constraint.value
437+
438+
if constraint_hostname.startswith("."):
439+
return hostname.endswith(constraint_hostname)
440+
else:
441+
return hostname == constraint_hostname or hostname.endswith(
442+
"." + constraint_hostname
443+
)
444+
371445

372446
def split_pem(s):
373447
"""Split a string containing many ASCII-armored PEM structures.

tests/test_autograph_utils.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,78 @@ async def test_unknown_key(aiohttp_session, mock_with_x5u, cache, now_fixed):
313313
assert excinfo.value.key == mock_intermediate.public_key()
314314

315315

316+
async def test_verify_name_constraints_raises(
317+
aiohttp_session, mock_with_x5u, cache, now_fixed
318+
):
319+
certs = [
320+
cryptography.x509.load_pem_x509_certificate(pem, backend=default_backend())
321+
for pem in STAGE_CERT_LIST
322+
]
323+
# Intermediate cert has the name constraint.
324+
intermediate = certs[1]
325+
# Change name of leaf cert.
326+
mock_leaf = mock_cert(certs[0])
327+
fake_name = mock.Mock()
328+
fake_name.value = "bazinga.allizom.org"
329+
mock_leaf.subject = mock.Mock()
330+
mock_leaf.subject.get_attributes_for_oid.return_value = [fake_name]
331+
certs[0] = mock_leaf
332+
333+
with mock.patch("cryptography.x509.load_pem_x509_certificate") as load_cert_mock:
334+
load_cert_mock.side_effect = lambda *args, **kwargs: certs.pop(0)
335+
s = SignatureVerifier(aiohttp_session, cache, STAGE_ROOT_HASH)
336+
with pytest.raises(autograph_utils.CertificateChainNameNotPermitted) as excinfo:
337+
await s.verify_x5u(FAKE_CERT_URL)
338+
339+
assert " does not match the permitted names " in excinfo.value.detail
340+
assert excinfo.value.current == intermediate
341+
assert excinfo.value.next == mock_leaf
342+
343+
344+
async def test_verify_name_constraints_excludes(
345+
aiohttp_session, mock_with_x5u, cache, now_fixed
346+
):
347+
certs = [
348+
cryptography.x509.load_pem_x509_certificate(pem, backend=default_backend())
349+
for pem in STAGE_CERT_LIST
350+
]
351+
# Intermediate cert has the name constraint.
352+
real_intermediate = certs[1]
353+
real_constraints = real_intermediate.extensions.get_extension_for_class(
354+
cryptography.x509.NameConstraints
355+
).value
356+
357+
# Reverse meaning of constraints.
358+
def get_extension_mock(x509_cls):
359+
if x509_cls == cryptography.x509.NameConstraints:
360+
reversed = mock.Mock()
361+
reversed.permitted_subtrees = real_constraints.excluded_subtrees
362+
reversed.excluded_subtrees = real_constraints.permitted_subtrees
363+
364+
m = mock.Mock()
365+
m.value = reversed
366+
return m
367+
368+
return real_intermediate.get_extension_for_class(x509_cls)
369+
370+
intermediate = mock_cert(real_intermediate)
371+
intermediate.extensions = mock.Mock()
372+
intermediate.extensions.get_extension_for_class.side_effect = get_extension_mock
373+
certs[1] = intermediate
374+
375+
leaf = certs[0]
376+
377+
with mock.patch("cryptography.x509.load_pem_x509_certificate") as load_cert_mock:
378+
load_cert_mock.side_effect = lambda *args, **kwargs: certs.pop(0)
379+
s = SignatureVerifier(aiohttp_session, cache, STAGE_ROOT_HASH)
380+
with pytest.raises(autograph_utils.CertificateChainNameExcluded) as excinfo:
381+
await s.verify_x5u(FAKE_CERT_URL)
382+
383+
assert " matches the excluded names " in excinfo.value.detail
384+
assert excinfo.value.current == intermediate
385+
assert excinfo.value.next == leaf
386+
387+
316388
async def test_verify_leaf_code_signing(
317389
aiohttp_session, mock_with_x5u, cache, now_fixed
318390
):

0 commit comments

Comments
 (0)