@@ -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+
184198class 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+
197225class 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
372446def split_pem (s ):
373447 """Split a string containing many ASCII-armored PEM structures.
0 commit comments