Skip to content

Commit f9fe714

Browse files
committed
Support stage cert chain, including certs with ECDSA public keys
1 parent f25035c commit f9fe714

3 files changed

Lines changed: 177 additions & 9 deletions

File tree

autograph_utils/__init__.py

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from cryptography.hazmat.backends import default_backend
2020
from cryptography.hazmat.primitives.asymmetric import ec as cryptography_ec
2121
from cryptography.hazmat.primitives.asymmetric import padding
22+
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
2223
from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature
2324
from cryptography.hazmat.primitives.hashes import SHA256, SHA384
2425
from cryptography.x509.oid import NameOID
@@ -168,6 +169,18 @@ def detail(self):
168169
)
169170

170171

172+
class CertificateUnknownPublicKey(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+
171184
class CertificateLeafHasWrongKeyUsage(BadCertificate):
172185
def __init__(self, cert, key_usage):
173186
self.cert = cert
@@ -303,15 +316,8 @@ async def verify_x5u(self, url):
303316

304317
current_cert = chain[0]
305318
for next_cert in chain[1:]:
306-
try:
307-
current_cert.public_key().verify(
308-
next_cert.signature,
309-
next_cert.tbs_certificate_bytes,
310-
padding.PKCS1v15(),
311-
next_cert.signature_hash_algorithm,
312-
)
313-
except cryptography.exceptions.InvalidSignature:
314-
raise CertificateChainBroken(current_cert, next_cert)
319+
self._verify_cert_link(current_cert, next_cert)
320+
315321
current_cert = next_cert
316322

317323
leaf_subject_name = (
@@ -335,6 +341,33 @@ async def verify_x5u(self, url):
335341
self.cache.set(url, res)
336342
return res
337343

344+
def _verify_cert_link(self, current_cert, next_cert):
345+
"""Verify a single link in a cert chain.
346+
347+
"""
348+
key = current_cert.public_key()
349+
if isinstance(key, RSAPublicKey):
350+
try:
351+
key.verify(
352+
next_cert.signature,
353+
next_cert.tbs_certificate_bytes,
354+
padding.PKCS1v15(),
355+
next_cert.signature_hash_algorithm,
356+
)
357+
except cryptography.exceptions.InvalidSignature:
358+
raise CertificateChainBroken(current_cert, next_cert)
359+
elif isinstance(key, cryptography_ec.EllipticCurvePublicKey):
360+
try:
361+
key.verify(
362+
next_cert.signature,
363+
next_cert.tbs_certificate_bytes,
364+
cryptography_ec.ECDSA(next_cert.signature_hash_algorithm),
365+
)
366+
except cryptography.exceptions.InvalidSignature:
367+
raise CertificateChainBroken(current_cert, next_cert)
368+
else:
369+
raise CertificateUnknownPublicKey(current_cert, key)
370+
338371

339372
def split_pem(s):
340373
"""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-----

tests/test_autograph_utils.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,16 @@
5959
+ "83:04:EF:01:BF:FA:03:29:B2:46:9F:3C:C5:EC:36:04"
6060
)
6161

62+
STAGE_CERT_PATH = os.path.join(
63+
TESTS_BASE, "normandy.content-signature.mozilla.org-2019-12-04-18-15-23.chain"
64+
)
65+
STAGE_CERT_CHAIN = open(STAGE_CERT_PATH, "rb").read()
66+
STAGE_CERT_LIST = autograph_utils.split_pem(STAGE_CERT_CHAIN)
67+
STAGE_ROOT_HASH = decode_mozilla_hash(
68+
"DB:74:CE:58:E4:F9:D0:9E:E0:42:36:BE:6C:C5:C4:F6:"
69+
+ "6A:E7:74:7D:C0:21:42:7A:03:BC:2F:57:0C:8B:9B:90"
70+
)
71+
6272

6373
@pytest.fixture
6474
def mock_aioresponses():
@@ -105,6 +115,7 @@ def mock_cert(real_cert):
105115
mock_cert.signature_hash_algorithm = real_cert.signature_hash_algorithm
106116
mock_cert.subject = real_cert.subject
107117
mock_cert.extensions = real_cert.extensions
118+
mock_cert.public_key = real_cert.public_key
108119

109120
return mock_cert
110121

@@ -272,6 +283,36 @@ async def test_verify_broken_chain(
272283
)
273284

274285

286+
async def test_verify_stage_cert_chain(
287+
aiohttp_session, mock_aioresponses, cache, now_fixed
288+
):
289+
mock_aioresponses.get(FAKE_CERT_URL, status=200, body=STAGE_CERT_CHAIN)
290+
s = SignatureVerifier(aiohttp_session, cache, STAGE_ROOT_HASH)
291+
await s.verify_x5u(FAKE_CERT_URL)
292+
293+
294+
async def test_unknown_key(aiohttp_session, mock_with_x5u, cache, now_fixed):
295+
certs = [
296+
cryptography.x509.load_pem_x509_certificate(pem, backend=default_backend())
297+
for pem in CERT_LIST
298+
]
299+
300+
# Change public_key for an intermediate cert
301+
real_intermediate = certs[1]
302+
mock_intermediate = mock_cert(real_intermediate)
303+
mock_intermediate.public_key = mock.Mock()
304+
certs[1] = mock_intermediate
305+
306+
with mock.patch("cryptography.x509.load_pem_x509_certificate") as load_cert_mock:
307+
load_cert_mock.side_effect = lambda *args, **kwargs: certs.pop(0)
308+
s = SignatureVerifier(aiohttp_session, cache, DEV_ROOT_HASH)
309+
with pytest.raises(autograph_utils.CertificateUnknownPublicKey) as excinfo:
310+
await s.verify_x5u(FAKE_CERT_URL)
311+
312+
assert excinfo.value.cert == mock_intermediate
313+
assert excinfo.value.key == mock_intermediate.public_key()
314+
315+
275316
async def test_verify_leaf_code_signing(
276317
aiohttp_session, mock_with_x5u, cache, now_fixed
277318
):

0 commit comments

Comments
 (0)