1818from cryptography import x509
1919from cryptography .hazmat .backends import default_backend
2020from cryptography .hazmat .primitives .asymmetric import ec as cryptography_ec
21+ from cryptography .hazmat .primitives .asymmetric import padding
22+ from cryptography .hazmat .primitives .asymmetric .rsa import RSAPublicKey
2123from cryptography .hazmat .primitives .asymmetric .utils import encode_dss_signature
2224from cryptography .hazmat .primitives .hashes import SHA256 , SHA384
2325from cryptography .x509 .oid import NameOID
@@ -128,6 +130,19 @@ def detail(self):
128130 return f"Certificate expired on { self .not_after } "
129131
130132
133+ class CertificateHasWrongRoot (BadCertificate ):
134+ def __init__ (self , * , expected , actual ):
135+ self .expected = binascii .hexlify (expected ).decode ()
136+ self .actual = binascii .hexlify (actual ).decode ()
137+
138+ @property
139+ def detail (self ):
140+ return (
141+ "Certificate is not based on expected root hash. "
142+ f"Got { self .actual !r} expected { self .expected !r} "
143+ )
144+
145+
131146class CertificateHasWrongSubject (BadCertificate ):
132147 def __init__ (self , actual , check_description ):
133148 self .check_description = check_description
@@ -141,6 +156,90 @@ def detail(self):
141156 )
142157
143158
159+ class CertificateChainBroken (BadCertificate ):
160+ def __init__ (self , previous_cert , next_cert ):
161+ self .previous_cert = previous_cert
162+ self .next_cert = next_cert
163+
164+ @property
165+ def detail (self ):
166+ return (
167+ "Certificate chain is not continuous. "
168+ f"Expected { self .previous_cert !r} to sign { self .next_cert !r} "
169+ )
170+
171+
172+ class CertificateUnsupportedKeyType (BadCertificate ):
173+ """An internal error indicating that support for some type of key is missing."""
174+
175+ def __init__ (self , cert , key ):
176+ self .cert = cert
177+ self .key = key
178+
179+ @property
180+ def detail (self ):
181+ return f"Unknown public key type for { self .cert !r} : { self .key !r} "
182+
183+
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+
198+ class CertificateCannotSign (BadCertificate ):
199+ """For intermediate/root certificates that do not have the proper
200+ metadata bits saying that they can be used to sign signatures.
201+
202+ """
203+
204+ def __init__ (self , cert , extra ):
205+ self .cert = cert
206+ self .extra = extra
207+
208+ @property
209+ def detail (self ):
210+ return (
211+ "Certificate cannot be used for signing "
212+ f"because { self .extra } : { self .cert !r} "
213+ )
214+
215+
216+ class CertificateLeafHasWrongKeyUsage (BadCertificate ):
217+ def __init__ (self , cert , key_usage ):
218+ self .cert = cert
219+ self .key_usage = key_usage
220+
221+ @property
222+ def detail (self ):
223+ return (
224+ f"Leaf certificate { self .cert !r} should have extended key usage of just "
225+ f"Code Signing. Got { self .key_usage !r} "
226+ )
227+
228+
229+ class CertificateChainNameExcluded (BadCertificate ):
230+ def __init__ (self , excluded_subtrees , current , next ):
231+ self .excluded_subtrees = excluded_subtrees
232+ self .current = current
233+ self .next = next
234+
235+ @property
236+ def detail (self ):
237+ return (
238+ f"Certificate name of { self .next !r} matches the excluded names "
239+ f"for { self .current !r} : { self .excluded_subtrees !r} "
240+ )
241+
242+
144243class BadSignature (Exception ):
145244 detail = "Unknown signature problem"
146245
@@ -255,6 +354,20 @@ async def verify_x5u(self, url):
255354 if now > cert .not_valid_after :
256355 raise CertificateExpired (cert .not_valid_after )
257356
357+ # Verify chain of trust.
358+ chain = certs [::- 1 ]
359+ root_hash = chain [0 ].fingerprint (SHA256 ())
360+ if root_hash != self .root_hash :
361+ raise CertificateHasWrongRoot (expected = self .root_hash , actual = root_hash )
362+
363+ current_cert = chain [0 ]
364+ for next_cert in chain [1 :]:
365+ self ._check_can_sign_other_certs (current_cert )
366+ self ._verify_cert_link (current_cert , next_cert )
367+ self ._check_name_constraints (current_cert , next_cert )
368+
369+ current_cert = next_cert
370+
258371 leaf_subject_name = (
259372 certs [0 ].subject .get_attributes_for_oid (NameOID .COMMON_NAME )[0 ].value
260373 )
@@ -263,13 +376,107 @@ async def verify_x5u(self, url):
263376 leaf_subject_name , check_description = self .subject_name_check .describe ()
264377 )
265378
266- root_hash = certs [- 1 ].fingerprint (SHA256 ())
267- assert root_hash == self .root_hash
379+ code_signing = cryptography .x509 .oid .ExtendedKeyUsageOID .CODE_SIGNING
380+ extended_key_usage = (
381+ certs [0 ]
382+ .extensions .get_extension_for_class (cryptography .x509 .ExtendedKeyUsage )
383+ .value
384+ )
385+ if list (extended_key_usage ) != [code_signing ]:
386+ raise CertificateLeafHasWrongKeyUsage (certs [0 ], extended_key_usage )
268387
269388 res = certs [0 ]
270389 self .cache .set (url , res )
271390 return res
272391
392+ def _verify_cert_link (self , current_cert , next_cert ):
393+ """Verify a single link in a cert chain.
394+
395+ """
396+ key = current_cert .public_key ()
397+ if isinstance (key , RSAPublicKey ):
398+ try :
399+ key .verify (
400+ next_cert .signature ,
401+ next_cert .tbs_certificate_bytes ,
402+ padding .PKCS1v15 (),
403+ next_cert .signature_hash_algorithm ,
404+ )
405+ except cryptography .exceptions .InvalidSignature :
406+ raise CertificateChainBroken (current_cert , next_cert )
407+ elif isinstance (key , cryptography_ec .EllipticCurvePublicKey ):
408+ try :
409+ key .verify (
410+ next_cert .signature ,
411+ next_cert .tbs_certificate_bytes ,
412+ cryptography_ec .ECDSA (next_cert .signature_hash_algorithm ),
413+ )
414+ except cryptography .exceptions .InvalidSignature :
415+ raise CertificateChainBroken (current_cert , next_cert )
416+ else :
417+ raise CertificateUnsupportedKeyType (current_cert , key )
418+
419+ def _check_name_constraints (self , current_cert , next_cert ):
420+ try :
421+ nc = current_cert .extensions .get_extension_for_class (
422+ cryptography .x509 .NameConstraints
423+ ).value
424+ except x509 .ExtensionNotFound :
425+ # No name constraints. This cert is therefore OK to sign
426+ # any name whatsoever.
427+ return
428+
429+ name = next_cert .subject .get_attributes_for_oid (NameOID .COMMON_NAME )[0 ].value
430+ if nc .permitted_subtrees :
431+ for constraint in nc .permitted_subtrees :
432+ if _name_constraint_matches (name , constraint ):
433+ break
434+ else :
435+ raise CertificateChainNameNotPermitted (
436+ nc .permitted_subtrees , current = current_cert , next = next_cert
437+ )
438+
439+ excluded_subtrees = nc .excluded_subtrees or []
440+
441+ for constraint in excluded_subtrees :
442+ if _name_constraint_matches (name , constraint ):
443+ raise CertificateChainNameExcluded (
444+ nc .excluded_subtrees , current = current_cert , next = next_cert
445+ )
446+
447+ def _check_can_sign_other_certs (self , cert ):
448+ basic = cert .extensions .get_extension_for_class (
449+ cryptography .x509 .BasicConstraints
450+ ).value
451+ if not basic .ca :
452+ raise CertificateCannotSign (cert , "ca is false" )
453+
454+ usage = cert .extensions .get_extension_for_class (
455+ cryptography .x509 .KeyUsage
456+ ).value
457+ usage_is_ok = usage .key_cert_sign and usage .crl_sign
458+ if not usage_is_ok :
459+ raise CertificateCannotSign (cert , "key usage is incomplete" )
460+
461+
462+ def _name_constraint_matches (hostname , name_constraint ):
463+ """Check if a name matches a constraint.
464+
465+ Taken from
466+ https://github.com/alex/x509-validator/blob/master/validator.py.
467+
468+ """
469+ if not isinstance (name_constraint , x509 .DNSName ):
470+ return False
471+ constraint_hostname = name_constraint .value
472+
473+ if constraint_hostname .startswith ("." ):
474+ return hostname .endswith (constraint_hostname )
475+ else :
476+ return hostname == constraint_hostname or hostname .endswith (
477+ "." + constraint_hostname
478+ )
479+
273480
274481def split_pem (s ):
275482 """Split a string containing many ASCII-armored PEM structures.
0 commit comments