Skip to content

Commit d9ee800

Browse files
authored
Merge pull request #5 from mozilla-services/chain-of-trust
Chain of trust
2 parents 45025b8 + 1311acb commit d9ee800

3 files changed

Lines changed: 564 additions & 2 deletions

File tree

autograph_utils/__init__.py

Lines changed: 209 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
from cryptography import x509
1919
from cryptography.hazmat.backends import default_backend
2020
from 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
2123
from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature
2224
from cryptography.hazmat.primitives.hashes import SHA256, SHA384
2325
from 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+
131146
class 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+
144243
class 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

274481
def split_pem(s):
275482
"""Split a string containing many ASCII-armored PEM structures.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIC9jCCAnugAwIBAgIIFc3kt9BoLrYwCgYIKoZIzj0EAwMwgaMxCzAJBgNVBAYT
3+
AlVTMRwwGgYDVQQKExNNb3ppbGxhIENvcnBvcmF0aW9uMS8wLQYDVQQLEyZNb3pp
4+
bGxhIEFNTyBQcm9kdWN0aW9uIFNpZ25pbmcgU2VydmljZTFFMEMGA1UEAww8Q29u
5+
dGVudCBTaWduaW5nIEludGVybWVkaWF0ZS9lbWFpbEFkZHJlc3M9Zm94c2VjQG1v
6+
emlsbGEuY29tMB4XDTE5MDkxNTE4MTUyM1oXDTE5MTIwNDE4MTUyM1owgaIxCzAJ
7+
BgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFp
8+
biBWaWV3MRwwGgYDVQQKExNNb3ppbGxhIENvcnBvcmF0aW9uMRcwFQYDVQQLEw5D
9+
bG91ZCBTZXJ2aWNlczEvMC0GA1UEAxMmbm9ybWFuZHkuY29udGVudC1zaWduYXR1
10+
cmUubW96aWxsYS5vcmcwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQOOLFQYkC6GJxU
11+
/aQ3JhVMhXbWlMomiUp/dPkZzXNmP+ETpg6jRWrE+cn/MHeoQDXzGAvq+nFdtg41
12+
KentqOEC8ZIp92rf30wf7ZQTLmBKcorQr7fqrqR/iq05ZBgUcnWjezB5MA4GA1Ud
13+
DwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAfBgNVHSMEGDAWgBQlZawr
14+
qt0eUz/t6OdN45oKfmzy6DAxBgNVHREEKjAogiZub3JtYW5keS5jb250ZW50LXNp
15+
Z25hdHVyZS5tb3ppbGxhLm9yZzAKBggqhkjOPQQDAwNpADBmAjEA7pOfGEFQuzQj
16+
9gfA/HdQaTbPG1uxxC4G9Z/glfmN2C6HhfF/hoYbtumkiDk1/uYLAjEA51kt/QWq
17+
S/XsYvIoYME2k+FlOORyag3vgAh4hqBzENI2GI1suRiWpy+Kabj4nRHi
18+
-----END CERTIFICATE-----
19+
-----BEGIN CERTIFICATE-----
20+
MIIF1TCCA72gAwIBAgIEAQAAAjANBgkqhkiG9w0BAQsFADCBqDELMAkGA1UEBhMC
21+
VVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRwwGgYDVQQK
22+
ExNBZGRvbnMgVGVzdCBTaWduaW5nMSQwIgYDVQQDExt0ZXN0LmFkZG9ucy5zaWdu
23+
aW5nLnJvb3QuY2ExMDAuBgkqhkiG9w0BCQEWIW9wc2VjK3N0YWdlcm9vdGFkZG9u
24+
c0Btb3ppbGxhLmNvbTAeFw0xOTAyMTMyMDA5MDVaFw0yMTAyMTIyMDA5MDVaMIGj
25+
MQswCQYDVQQGEwJVUzEcMBoGA1UEChMTTW96aWxsYSBDb3Jwb3JhdGlvbjEvMC0G
26+
A1UECxMmTW96aWxsYSBBTU8gUHJvZHVjdGlvbiBTaWduaW5nIFNlcnZpY2UxRTBD
27+
BgNVBAMMPENvbnRlbnQgU2lnbmluZyBJbnRlcm1lZGlhdGUvZW1haWxBZGRyZXNz
28+
PWZveHNlY0Btb3ppbGxhLmNvbTB2MBAGByqGSM49AgEGBSuBBAAiA2IABHDV3ITF
29+
Xlo2Ick9r99Uc7qTEmfehktWi0nQPMVkD2vWxB/yLT6/vw+DT9zedMDJlZ+RQvO8
30+
6kpgr8YQYG2KzEKQMn4Xc24s+lIiDd9fbmkfQsTXl+8BIFVyvy0otUd46aOCAbYw
31+
ggGyMAwGA1UdEwQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMBYGA1UdJQEB/wQMMAoG
32+
CCsGAQUFBwMDMB0GA1UdDgQWBBQlZawrqt0eUz/t6OdN45oKfmzy6DCB1QYDVR0j
33+
BIHNMIHKgBSE6l/Nb0ySL+rR9PXIo7LCDLqm9qGBrqSBqzCBqDELMAkGA1UEBhMC
34+
VVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRwwGgYDVQQK
35+
ExNBZGRvbnMgVGVzdCBTaWduaW5nMSQwIgYDVQQDExt0ZXN0LmFkZG9ucy5zaWdu
36+
aW5nLnJvb3QuY2ExMDAuBgkqhkiG9w0BCQEWIW9wc2VjK3N0YWdlcm9vdGFkZG9u
37+
c0Btb3ppbGxhLmNvbYIBATAzBglghkgBhvhCAQQEJhYkaHR0cDovL2FkZG9ucy5h
38+
bGxpem9tLm9yZy9jYS9jcmwucGVtME4GA1UdHgRHMEWgQzAggh4uY29udGVudC1z
39+
aWduYXR1cmUubW96aWxsYS5vcmcwH4IdY29udGVudC1zaWduYXR1cmUubW96aWxs
40+
YS5vcmcwDQYJKoZIhvcNAQELBQADggIBAHcshk/LD4STyAdiqiNlt+lNEW8CvW+Y
41+
Y3WAQGLoKrQdE3gAvrc0RxeLdLbp1jyxGzrRWY95IJvdTHjpeZBQrz4+NSv9t9hd
42+
cQtLbXN2rb8+q9U3Z5bxuqMgynaNVX2ax/dwHOl7Elg99ukOtYvhdAlzZlNW+o2M
43+
m2SYqe0zKbPH6wGMB9JAOhKtY2C+3VlxZyC6IvuzGF0VbKGIHO7sMnnJXqXAhrjv
44+
pq8lBMC1+BAgVKuN0OrU9aVxFl3sx1UhvKnLHbbHb2y+Dmfrrj2aViP8wkm7TmuP
45+
pYFE4c53n0+jDp7eSdinzKZLb70q1AgpkyuR5MIDjWxVhIVNr/T/1ZZx4iftF0BW
46+
Wtriel/JqIBLbBum0d2QD0fGDam9ia8HqEG28P9Q/SJrLI1hTQTaKW46BqAIb/c/
47+
7RyyE3C3X7ATjj8ksgHgi5P6u0pHl7HPsqWEMZodleLlVy/BX2sbx6EbK2E/CTsp
48+
DkpiqcDBISzXwGztMkA5L3WkbJwZc1stNWe0LLXmaVqFIvyJXSpa+4N5F09e/nEf
49+
VqqAwTewqkApsV1nc+aLAxUxK3VBZJ7xtJalwiqGALXhRSADsFMV5nvv7mWQyxvb
50+
WawvwHt8BrO0k3dRlXqx0w1yP9MZibBoX0MT+IntNHp45/cxb9i/6PLAxhWriIiE
51+
y2t6gkAeRU3f
52+
-----END CERTIFICATE-----
53+
-----BEGIN CERTIFICATE-----
54+
MIIHYzCCBUugAwIBAgIBATANBgkqhkiG9w0BAQwFADCBqDELMAkGA1UEBhMCVVMx
55+
CzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRwwGgYDVQQKExNB
56+
ZGRvbnMgVGVzdCBTaWduaW5nMSQwIgYDVQQDExt0ZXN0LmFkZG9ucy5zaWduaW5n
57+
LnJvb3QuY2ExMDAuBgkqhkiG9w0BCQEWIW9wc2VjK3N0YWdlcm9vdGFkZG9uc0Bt
58+
b3ppbGxhLmNvbTAeFw0xNTAyMTAxNTI4NTFaFw0yNTAyMDcxNTI4NTFaMIGoMQsw
59+
CQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcx
60+
HDAaBgNVBAoTE0FkZG9ucyBUZXN0IFNpZ25pbmcxJDAiBgNVBAMTG3Rlc3QuYWRk
61+
b25zLnNpZ25pbmcucm9vdC5jYTEwMC4GCSqGSIb3DQEJARYhb3BzZWMrc3RhZ2Vy
62+
b290YWRkb25zQG1vemlsbGEuY29tMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
63+
CgKCAgEAv/OSHh5uUMMKKuBh83kikuJ+BW4fQCHVZvADZh2qHNH8pSaME/YqMItP
64+
5XQ1N5oLq1tRQO77AKn+eYPDAQkg+9VV+ct4u76YctcU/gvjieGKQ0fvuDH18QLD
65+
hqa4DHgDmpCa/w+Eqzd54HaFj7ew9Bb7GZPHuZfk7Ct9fcN6kHneEj3KeuLiqzSV
66+
VCRFV9RTlrUdsc1/VwF4A97JTXc3HJeWJO3azOlFpaJ8QHhmgXLLmB59HPeZ10Sf
67+
9QwVGaKcn7yLuwtIA+wDhs8iwGZWcgmknW4DkkRDbQo7L+//4kVK+Yqq0HamZArm
68+
vE4xENvbwOze4XYkCO3PwgmCotU7K5D3sMUUxkOaodlemO9OqRW8vJOJH3b6mhST
69+
aunQR9/GOJ7sl4egrn2fOVZhBvM29lyBCKBffeQgtIMcKpeEKa4TNx4nTrWu1J9k
70+
jHlvNeVL3FzMzJXRPl0RV71cYak+G6GnQ4fg3+4ZSSPxTvbwRJAO2xajkURxFSZo
71+
sXcjYG8iPTSrDazj4LN2+882t4Q2/rMYpkowwLGbvJqHiw2tg9/hpLn1K4W18vcC
72+
vFgzNRrTdKaJ/KjD17eJl8s8oPA7TiophPeezy1WzAc4mdlXS6A85b0mKDDU2A/4
73+
3YmltjsSmizR2LnfeNs125EsCWxSUrAsnUYRO+lJOyNr7GGKGscCAwZVN6OCAZQw
74+
ggGQMAwGA1UdEwQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMBYGA1UdJQEB/wQMMAoG
75+
CCsGAQUFBwMDMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0
76+
aWZpY2F0ZTAzBglghkgBhvhCAQQEJhYkaHR0cDovL2FkZG9ucy5tb3ppbGxhLm9y
77+
Zy9jYS9jcmwucGVtMB0GA1UdDgQWBBSE6l/Nb0ySL+rR9PXIo7LCDLqm9jCB1QYD
78+
VR0jBIHNMIHKgBSE6l/Nb0ySL+rR9PXIo7LCDLqm9qGBrqSBqzCBqDELMAkGA1UE
79+
BhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRwwGgYD
80+
VQQKExNBZGRvbnMgVGVzdCBTaWduaW5nMSQwIgYDVQQDExt0ZXN0LmFkZG9ucy5z
81+
aWduaW5nLnJvb3QuY2ExMDAuBgkqhkiG9w0BCQEWIW9wc2VjK3N0YWdlcm9vdGFk
82+
ZG9uc0Btb3ppbGxhLmNvbYIBATANBgkqhkiG9w0BAQwFAAOCAgEAck21RaAcTzbT
83+
vmqqcCezBd5Gej6jV53HItXfF06tLLzAxKIU1loLH/330xDdOGyiJdvUATDVn8q6
84+
5v4Kae2awON6ytWZp9b0sRdtlLsRo8EWOoRszCqiMWdl1gnGMaV7e2ycz/tR+PoK
85+
GxHCh8rbOtG0eiVJIyRijLDjtExW8Eg+uz6Zkg1IWXqInj7Gqr23FOqD76uAfE82
86+
YTWW3lzxpP3gL7pmV5G7ob/tIyAfrPEB4w0Nt2HEl9h7NDtKPMprrOLPkrI9eAVU
87+
QeeI3RpAKnXOFQkqPYPXIlAaJ6qxtYa6tWHOqRyS1xKnvy/uWjEtU3tYJ5eUL1+2
88+
vzNTdakJgkZDRdDNg0V3NYwza6BwL80VPSfqc1H6R8CU1uj+kjTlCEsoTPLeW7k5
89+
t+lKHFMj0HZLNymgDD5f9UpI7yiOAIF0z4WKAMv/f12vnAPwmOPuOikRNOv0nNuL
90+
RIpKO53Cd7aV5PdB0pNSPNjc6V+5IPrepALNQhKIpzoHA4oG+LlVVy4R3csPcj4e
91+
zQQ9gt3NC2OXF4hveHfKZdCnb+BBl4S71QMYYCCTe+EDCsIGuyXWD/K2hfLD8TPW
92+
thPX5WNsS8bwno2ccqncVLQ4PZxOIB83DFBFmAvTuBiAYWq874rneTXqInHyeCq+
93+
819l9s72pDsFaGevmm0Us9bYuufTS5U=
94+
-----END CERTIFICATE-----

0 commit comments

Comments
 (0)