Coverage for tests/admin/test_sgx_utils.py: 99%
352 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-10-30 06:22 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-10-30 06:22 +0000
1# The MIT License (MIT)
2#
3# Copyright (c) 2021 RSK Labs Ltd
4#
5# Permission is hereby granted, free of charge, to any person obtaining a copy of
6# this software and associated documentation files (the "Software"), to deal in
7# the Software without restriction, including without limitation the rights to
8# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
9# of the Software, and to permit persons to whom the Software is furnished to do
10# so, subject to the following conditions:
11#
12# The above copyright notice and this permission notice shall be included in all
13# copies or substantial portions of the Software.
14#
15# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21# SOFTWARE.
24from unittest import TestCase
25from unittest.mock import patch, Mock
26from parameterized import parameterized
27from types import SimpleNamespace
28from datetime import datetime
29from cryptography import x509
30from cryptography.hazmat.primitives.serialization import PublicFormat, Encoding
31from hashlib import sha256
32import ecdsa
33from admin.sgx_utils import get_sgx_extensions, get_tcb_info, validate_tcb_info, \
34 get_qeid_info, validate_qeid_info, get_intel_pcs_x509_crl
35import logging
37logging.disable(logging.CRITICAL)
39TEST_CERTIFICATE = """
40-----BEGIN CERTIFICATE-----
41MIIE8jCCBJmgAwIBAgIVAJzdeT0t5GnBg8UERKXiUnECGiOPMAoGCCqGSM49BAMC
42MHAxIjAgBgNVBAMMGUludGVsIFNHWCBQQ0sgUGxhdGZvcm0gQ0ExGjAYBgNVBAoM
43EUludGVsIENvcnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UE
44CAwCQ0ExCzAJBgNVBAYTAlVTMB4XDTI1MDYyNDIyMDEyMFoXDTMyMDYyNDIyMDEy
45MFowcDEiMCAGA1UEAwwZSW50ZWwgU0dYIFBDSyBDZXJ0aWZpY2F0ZTEaMBgGA1UE
46CgwRSW50ZWwgQ29ycG9yYXRpb24xFDASBgNVBAcMC1NhbnRhIENsYXJhMQswCQYD
47VQQIDAJDQTELMAkGA1UEBhMCVVMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAT8
48IZG02RowuzOQKmXZCmBjiMPUPW0+E1kVl1ufUkd3yBtA71B8bgt8DHaoUFZ1YAxu
49ZZhPDvV13zZF9fzdyUxXo4IDDjCCAwowHwYDVR0jBBgwFoAUlW9dzb0b4elAScnU
509DPOAVcL3lQwawYDVR0fBGQwYjBgoF6gXIZaaHR0cHM6Ly9hcGkudHJ1c3RlZHNl
51cnZpY2VzLmludGVsLmNvbS9zZ3gvY2VydGlmaWNhdGlvbi92My9wY2tjcmw/Y2E9
52cGxhdGZvcm0mZW5jb2Rpbmc9ZGVyMB0GA1UdDgQWBBT2LlxjZqMdkMyID5G57bLZ
53kj0QPDAOBgNVHQ8BAf8EBAMCBsAwDAYDVR0TAQH/BAIwADCCAjsGCSqGSIb4TQEN
54AQSCAiwwggIoMB4GCiqGSIb4TQENAQEEELbSV7okFcKjOLO+IPh8XykwggFlBgoq
55hkiG+E0BDQECMIIBVTAQBgsqhkiG+E0BDQECAQIBEDAQBgsqhkiG+E0BDQECAgIB
56EDAQBgsqhkiG+E0BDQECAwIBAzAQBgsqhkiG+E0BDQECBAIBAzARBgsqhkiG+E0B
57DQECBQICAP8wEQYLKoZIhvhNAQ0BAgYCAgD/MBAGCyqGSIb4TQENAQIHAgEBMBAG
58CyqGSIb4TQENAQIIAgEAMBAGCyqGSIb4TQENAQIJAgEAMBAGCyqGSIb4TQENAQIK
59AgEAMBAGCyqGSIb4TQENAQILAgEAMBAGCyqGSIb4TQENAQIMAgEAMBAGCyqGSIb4
60TQENAQINAgEAMBAGCyqGSIb4TQENAQIOAgEAMBAGCyqGSIb4TQENAQIPAgEAMBAG
61CyqGSIb4TQENAQIQAgEAMBAGCyqGSIb4TQENAQIRAgENMB8GCyqGSIb4TQENAQIS
62BBAQEAMD//8BAAAAAAAAAAAAMBAGCiqGSIb4TQENAQMEAgAAMBQGCiqGSIb4TQEN
63AQQEBgBgagAAADAPBgoqhkiG+E0BDQEFCgEBMB4GCiqGSIb4TQENAQYEEA1Xvw11
64FROIHprYDubgabwwRAYKKoZIhvhNAQ0BBzA2MBAGCyqGSIb4TQENAQcBAQH/MBAG
65CyqGSIb4TQENAQcCAQEAMBAGCyqGSIb4TQENAQcDAQEAMAoGCCqGSM49BAMCA0cA
66MEQCICNTsVzcOYLck4STK/PAdCcTIqquVTmQRh40TSUBEaS4AiAzUN6Q3n/vEnSz
67fQgkG+9VlNOMURjIGOl1qtg9jop4CQ==
68-----END CERTIFICATE-----
69"""
71TEST_CERTIFICATE_ROOT = """
72-----BEGIN CERTIFICATE-----
73MIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw
74aDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv
75cnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ
76BgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG
77A1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0
78aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT
79AlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7
801OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB
81uzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ
82MEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50
83ZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV
84Ur9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI
85KoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg
86AiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI=
87-----END CERTIFICATE-----
88"""
91class TestSgxUtils(TestCase):
92 def test_get_sgx_extensions_ok(self):
93 certificate = x509.load_pem_x509_certificate(TEST_CERTIFICATE.encode())
94 extensions = get_sgx_extensions(certificate)
95 self.assertEqual("b6d257ba2415c2a338b3be20f87c5f29", extensions["ppid"])
96 tcb = extensions["tcb"]
97 self.assertEqual(0x10, tcb["comp01"])
98 self.assertEqual(0x10, tcb["comp02"])
99 self.assertEqual(0x03, tcb["comp03"])
100 self.assertEqual(0x03, tcb["comp04"])
101 self.assertEqual(0xff, tcb["comp05"])
102 self.assertEqual(0xff, tcb["comp06"])
103 self.assertEqual(0x01, tcb["comp07"])
104 self.assertEqual(0x00, tcb["comp08"])
105 self.assertEqual(0x00, tcb["comp09"])
106 self.assertEqual(0x00, tcb["comp10"])
107 self.assertEqual(0x00, tcb["comp11"])
108 self.assertEqual(0x00, tcb["comp12"])
109 self.assertEqual(0x00, tcb["comp13"])
110 self.assertEqual(0x00, tcb["comp14"])
111 self.assertEqual(0x00, tcb["comp15"])
112 self.assertEqual(0x00, tcb["comp16"])
113 self.assertEqual(0x0d, tcb["pcesvn"])
114 self.assertEqual("10100303ffff01000000000000000000", tcb["cpusvn"])
115 self.assertEqual("0000", extensions["pceid"])
116 self.assertEqual("00606a000000", extensions["fmspc"])
118 def test_get_sgx_extensions_not_found(self):
119 certificate = x509.load_pem_x509_certificate(TEST_CERTIFICATE_ROOT.encode())
120 self.assertIsNone(get_sgx_extensions(certificate))
123@patch("admin.sgx_utils.X509CertificateValidator")
124@patch("admin.sgx_utils.x509")
125@patch("admin.sgx_utils.split_pem_certificates")
126@patch("admin.sgx_utils.requests")
127class TestSgxUtilsGetTcbInfo(TestCase):
128 def configure_mocks(self, requests, split_pem_certificates, mock_x509,
129 X509CertificateValidator, issuer_sig=None):
130 mock_response = Mock()
131 requests.get.return_value = mock_response
132 mock_response.status_code = 200
133 mock_response.headers = {
134 "Content-Type": "application/json",
135 "warning": "this is a warning",
136 "TCB-Info-Issuer-Chain": "the issuer chain",
137 }
138 mock_response.text = """
139 {
140 "tcbInfo": {
141 "a": 1,
142 "b": 2,
143 "c": 3
144 },
145 "something": "else",
146 "another": "thing",
147 "signature": "<SIG>"
148 }
149 """
150 self.mock_response = mock_response
152 issuer_privkey = ecdsa.SigningKey.generate(curve=ecdsa.NIST256p)
153 issuer_pubkey = issuer_privkey.verifying_key
154 tcb_info_digest = sha256('{"a":1,"b":2,"c":3}'.encode()).digest()
155 if issuer_sig is None:
156 self.issuer_sig = issuer_privkey.sign_digest(
157 tcb_info_digest, sigencode=ecdsa.util.sigencode_string)
158 else:
159 self.issuer_sig = issuer_sig
160 mock_response.text = mock_response.text.replace("<SIG>", self.issuer_sig.hex())
162 split_pem_certificates.return_value = ["cert-0", "cert-1"]
163 self.cert0 = Mock()
164 self.cert1 = Mock()
166 self.cert0pk = Mock()
167 self.cert0.public_key.return_value = self.cert0pk
168 self.cert0pk.public_bytes.return_value = issuer_pubkey.to_string("compressed")
170 def load_cert(pem):
171 if pem == b"cert-0":
172 return self.cert0
173 elif pem == b"cert-1":
174 return self.cert1
175 else:
176 raise Exception("Unknown cert")
178 mock_x509.load_pem_x509_certificate.side_effect = load_cert
180 self.validator = Mock()
181 self.validator.validate.return_value = {
182 "valid": True,
183 "warnings": ["w1", "w2"],
184 }
185 X509CertificateValidator.return_value = self.validator
187 @parameterized.expand([
188 ("early"),
189 ("standard"),
190 ])
191 def test_get_tcb_info_ok(self, requests, split_pem_certificates, mock_x509,
192 X509CertificateValidator, update):
193 self.configure_mocks(requests, split_pem_certificates, mock_x509,
194 X509CertificateValidator)
196 self.assertEqual({
197 "tcb_info": {
198 "tcbInfo": {
199 "a": 1,
200 "b": 2,
201 "c": 3,
202 },
203 "something": "else",
204 "another": "thing",
205 "signature": self.issuer_sig.hex(),
206 },
207 "warnings": [f"Getting the-url?fmspc=an-fmspc&update={update}: "
208 "this is a warning", "w1", "w2"],
209 }, get_tcb_info("the-url", "an-fmspc", self.cert1, update=update))
211 requests.get.assert_called_with(f"the-url?fmspc=an-fmspc&update={update}")
212 X509CertificateValidator.assert_called_with(get_intel_pcs_x509_crl)
213 self.validator.validate.assert_called_once()
214 self.assertEqual(self.cert0, self.validator.validate.call_args_list[0].args[0])
215 self.assertEqual(self.cert1, self.validator.validate.call_args_list[0].args[1])
216 self.assertIsInstance(self.validator.validate.call_args_list[0].args[2], datetime)
217 self.cert0pk.public_bytes.assert_called_once()
218 self.cert0pk.public_bytes.assert_called_with(
219 Encoding.X962, PublicFormat.CompressedPoint)
221 def test_get_tcb_info_err_get(self, requests, split_pem_certificates, mock_x509,
222 X509CertificateValidator):
223 self.configure_mocks(requests, split_pem_certificates, mock_x509,
224 X509CertificateValidator)
225 self.mock_response.status_code = 404
227 with self.assertRaises(RuntimeError) as e:
228 get_tcb_info("the-url", "an-fmspc", self.cert1, update="upd")
229 self.assertIn("replied with status", str(e.exception))
231 requests.get.assert_called_with("the-url?fmspc=an-fmspc&update=upd")
232 X509CertificateValidator.assert_not_called()
233 self.validator.validate.assert_not_called()
234 self.cert0pk.public_bytes.assert_not_called()
236 def test_get_tcb_info_err_ctype(self, requests, split_pem_certificates, mock_x509,
237 X509CertificateValidator):
238 self.configure_mocks(requests, split_pem_certificates, mock_x509,
239 X509CertificateValidator)
240 self.mock_response.headers["Content-Type"] = "not/json"
242 with self.assertRaises(RuntimeError) as e:
243 get_tcb_info("the-url", "an-fmspc", self.cert1, update="upd")
244 self.assertIn("content-type", str(e.exception))
246 requests.get.assert_called_with("the-url?fmspc=an-fmspc&update=upd")
247 X509CertificateValidator.assert_not_called()
248 self.validator.validate.assert_not_called()
249 self.cert0pk.public_bytes.assert_not_called()
251 def test_get_tcb_info_err_nochain(self, requests, split_pem_certificates, mock_x509,
252 X509CertificateValidator):
253 self.configure_mocks(requests, split_pem_certificates, mock_x509,
254 X509CertificateValidator)
255 self.mock_response.headers["TCB-Info-Issuer-Chain"] = None
257 with self.assertRaises(RuntimeError) as e:
258 get_tcb_info("the-url", "an-fmspc", self.cert1, update="upd")
259 self.assertIn("certification chain", str(e.exception))
261 requests.get.assert_called_with("the-url?fmspc=an-fmspc&update=upd")
262 X509CertificateValidator.assert_not_called()
263 self.validator.validate.assert_not_called()
264 self.cert0pk.public_bytes.assert_not_called()
266 def test_get_tcb_info_err_shortchain(self, requests, split_pem_certificates,
267 mock_x509, X509CertificateValidator):
268 self.configure_mocks(requests, split_pem_certificates, mock_x509,
269 X509CertificateValidator)
270 split_pem_certificates.return_value = ["cert-0"]
272 with self.assertRaises(RuntimeError) as e:
273 get_tcb_info("the-url", "an-fmspc", self.cert1, update="upd")
274 self.assertIn("at least two certificates", str(e.exception))
276 requests.get.assert_called_with("the-url?fmspc=an-fmspc&update=upd")
277 X509CertificateValidator.assert_not_called()
278 self.validator.validate.assert_not_called()
279 self.cert0pk.public_bytes.assert_not_called()
281 def test_get_tcb_info_err_rot(self, requests, split_pem_certificates,
282 mock_x509, X509CertificateValidator):
283 self.configure_mocks(requests, split_pem_certificates, mock_x509,
284 X509CertificateValidator)
286 other_root = Mock()
287 other_root.subject = "other root"
289 with self.assertRaises(RuntimeError) as e:
290 get_tcb_info("the-url", "an-fmspc", other_root, update="upd")
291 self.assertIn("does not match", str(e.exception))
292 self.assertIn("other root", str(e.exception))
294 requests.get.assert_called_with("the-url?fmspc=an-fmspc&update=upd")
295 X509CertificateValidator.assert_not_called()
296 self.validator.validate.assert_not_called()
297 self.cert0pk.public_bytes.assert_not_called()
299 def test_get_tcb_info_err_chain_iv(self, requests, split_pem_certificates,
300 mock_x509, X509CertificateValidator):
301 self.configure_mocks(requests, split_pem_certificates, mock_x509,
302 X509CertificateValidator)
304 self.validator.validate.return_value = {
305 "valid": False,
306 "reason": "oops"
307 }
309 with self.assertRaises(RuntimeError) as e:
310 get_tcb_info("the-url", "an-fmspc", self.cert1, update="upd")
311 self.assertIn("issuer chain", str(e.exception))
312 self.assertIn("oops", str(e.exception))
314 requests.get.assert_called_with("the-url?fmspc=an-fmspc&update=upd")
315 X509CertificateValidator.assert_called_with(get_intel_pcs_x509_crl)
316 self.validator.validate.assert_called_once()
317 self.assertEqual(self.cert0, self.validator.validate.call_args_list[0].args[0])
318 self.assertEqual(self.cert1, self.validator.validate.call_args_list[0].args[1])
319 self.assertIsInstance(self.validator.validate.call_args_list[0].args[2], datetime)
320 self.cert0pk.public_bytes.assert_not_called()
322 def test_get_tcb_info_err_tcb_sig(self, requests, split_pem_certificates,
323 mock_x509, X509CertificateValidator):
324 isig = bytes.fromhex("aa"*32+"bb"*32)
325 self.configure_mocks(requests, split_pem_certificates, mock_x509,
326 X509CertificateValidator, issuer_sig=isig)
328 with self.assertRaises(RuntimeError) as e:
329 get_tcb_info("the-url", "an-fmspc", self.cert1, update="upd")
330 self.assertIn("Signature verification failed", str(e.exception))
332 requests.get.assert_called_with("the-url?fmspc=an-fmspc&update=upd")
333 X509CertificateValidator.assert_called_with(get_intel_pcs_x509_crl)
334 self.validator.validate.assert_called_once()
335 self.assertEqual(self.cert0, self.validator.validate.call_args_list[0].args[0])
336 self.assertEqual(self.cert1, self.validator.validate.call_args_list[0].args[1])
337 self.assertIsInstance(self.validator.validate.call_args_list[0].args[2], datetime)
338 self.cert0pk.public_bytes.assert_called_once()
339 self.cert0pk.public_bytes.assert_called_with(
340 Encoding.X962, PublicFormat.CompressedPoint)
343class TestSgxUtilsValidateTcbInfo(TestCase):
344 def setUp(self):
345 self.pck_info = {
346 "ppid": "aa"*32,
347 "tcb": {
348 "comp01": 11,
349 "comp02": 22,
350 "comp03": 33,
351 "comp04": 44,
352 "comp05": 55,
353 "comp06": 66,
354 "comp07": 77,
355 "comp08": 88,
356 "comp09": 99,
357 "comp10": 1010,
358 "comp11": 1111,
359 "comp12": 1212,
360 "comp13": 1313,
361 "comp14": 1414,
362 "comp15": 1515,
363 "comp16": 1616,
364 "pcesvn": 1717,
365 "cpusvn": "bb"*32,
366 },
367 "pceid": "abcd",
368 "fmspc": "cc"*6
369 }
371 self.tcb_info = {
372 "tcbInfo": {
373 "id": "SGX",
374 "version": 3,
375 "issueDate": "2025-09-05T03:41:29Z",
376 "nextUpdate": "2025-10-05T03:41:29Z",
377 "fmspc": "00606a000000",
378 "pceId": "0000",
379 "tcbType": 0,
380 "tcbEvaluationDataNumber": 1234,
381 "tcbLevels": [
382 {
383 "tcb": {
384 "sgxtcbcomponents": [
385 {
386 "svn": 10,
387 },
388 {
389 "svn": 20,
390 },
391 {
392 "svn": 30,
393 },
394 {
395 "svn": 3,
396 },
397 {
398 "svn": 255
399 },
400 {
401 "svn": 255
402 },
403 {
404 "svn": 1
405 },
406 {
407 "svn": 0
408 },
409 {
410 "svn": 0
411 },
412 {
413 "svn": 0
414 },
415 {
416 "svn": 0
417 },
418 {
419 "svn": 0
420 },
421 {
422 "svn": 0
423 },
424 {
425 "svn": 0
426 },
427 {
428 "svn": 0
429 },
430 {
431 "svn": 0
432 }
433 ],
434 "pcesvn": 13
435 },
436 "tcbDate": "doesntmatter",
437 "tcbStatus": "neither",
438 "advisoryIDs": [
439 "we-dont-care",
440 "or-about-this-one"
441 ]
442 },
443 {
444 "tcb": {
445 "sgxtcbcomponents": [
446 {
447 "svn": 10
448 },
449 {
450 "svn": 21
451 },
452 {
453 "svn": 32
454 },
455 {
456 "svn": 43
457 },
458 {
459 "svn": 54
460 },
461 {
462 "svn": 65
463 },
464 {
465 "svn": 76
466 },
467 {
468 "svn": 87
469 },
470 {
471 "svn": 98
472 },
473 {
474 "svn": 1009
475 },
476 {
477 "svn": 1110
478 },
479 {
480 "svn": 1211
481 },
482 {
483 "svn": 1312
484 },
485 {
486 "svn": 1413
487 },
488 {
489 "svn": 1514
490 },
491 {
492 "svn": 1615
493 }
494 ],
495 "pcesvn": 1716
496 },
497 "tcbDate": "the-date-we-want",
498 "tcbStatus": "the-status-we-want",
499 "advisoryIDs": [
500 "the-advisory-1",
501 "the-advisory-2",
502 "the-advisory-3"
503 ]
504 }
505 ]
506 },
507 "signature": "not-used-here"
508 }
510 def test_validate_tcb_info_ok(self):
511 self.assertEqual({
512 "valid": True,
513 "status": "the-status-we-want",
514 "date": "the-date-we-want",
515 "advisories": ["the-advisory-1", "the-advisory-2", "the-advisory-3"],
516 "svns": [
517 "Comp 01: 11 >= 10",
518 "Comp 02: 22 >= 21",
519 "Comp 03: 33 >= 32",
520 "Comp 04: 44 >= 43",
521 "Comp 05: 55 >= 54",
522 "Comp 06: 66 >= 65",
523 "Comp 07: 77 >= 76",
524 "Comp 08: 88 >= 87",
525 "Comp 09: 99 >= 98",
526 "Comp 10: 1010 >= 1009",
527 "Comp 11: 1111 >= 1110",
528 "Comp 12: 1212 >= 1211",
529 "Comp 13: 1313 >= 1312",
530 "Comp 14: 1414 >= 1413",
531 "Comp 15: 1515 >= 1514",
532 "Comp 16: 1616 >= 1615",
533 "PCESVN: 1717 >= 1716",
534 ],
535 "edn": 1234,
536 }, validate_tcb_info(self.pck_info, self.tcb_info["tcbInfo"]))
538 def test_validate_tcb_info_notfound(self):
539 self.tcb_info["tcbInfo"]["tcbLevels"] = self.tcb_info["tcbInfo"]["tcbLevels"][0:1]
540 self.assertEqual({
541 "valid": False,
542 "reason": "TCB level is unsupported",
543 }, validate_tcb_info(self.pck_info, self.tcb_info["tcbInfo"]))
545 def test_validate_tcb_info_malformed(self):
546 self.tcb_info = {
547 "tcbInfo": {
548 "something": "else"
549 }
550 }
552 res = validate_tcb_info(self.pck_info, self.tcb_info["tcbInfo"])
553 self.assertFalse(res["valid"])
554 self.assertIn("While validating TCB information", res["reason"])
557@patch("admin.sgx_utils.X509CertificateValidator")
558@patch("admin.sgx_utils.x509")
559@patch("admin.sgx_utils.split_pem_certificates")
560@patch("admin.sgx_utils.requests")
561class TestSgxUtilsGetQeIdInfo(TestCase):
562 def configure_mocks(self, requests, split_pem_certificates, mock_x509,
563 X509CertificateValidator, issuer_sig=None):
564 mock_response = Mock()
565 requests.get.return_value = mock_response
566 mock_response.status_code = 200
567 mock_response.headers = {
568 "Content-Type": "application/json",
569 "warning": "this is a warning",
570 "SGX-Enclave-Identity-Issuer-Chain": "the issuer chain",
571 }
572 mock_response.text = """
573 {
574 "enclaveIdentity": {
575 "a": 1,
576 "b": 2,
577 "c": 3
578 },
579 "something": "else",
580 "another": "thing",
581 "signature": "<SIG>"
582 }
583 """
584 self.mock_response = mock_response
586 issuer_privkey = ecdsa.SigningKey.generate(curve=ecdsa.NIST256p)
587 issuer_pubkey = issuer_privkey.verifying_key
588 qeid_info_digest = sha256('{"a":1,"b":2,"c":3}'.encode()).digest()
589 if issuer_sig is None:
590 self.issuer_sig = issuer_privkey.sign_digest(
591 qeid_info_digest, sigencode=ecdsa.util.sigencode_string)
592 else:
593 self.issuer_sig = issuer_sig
594 mock_response.text = mock_response.text.replace("<SIG>", self.issuer_sig.hex())
596 split_pem_certificates.return_value = ["cert-0", "cert-1"]
597 self.cert0 = Mock()
598 self.cert1 = Mock()
600 self.cert0pk = Mock()
601 self.cert0.public_key.return_value = self.cert0pk
602 self.cert0pk.public_bytes.return_value = issuer_pubkey.to_string("compressed")
604 def load_cert(pem):
605 if pem == b"cert-0":
606 return self.cert0
607 elif pem == b"cert-1":
608 return self.cert1
609 else:
610 raise Exception("Unknown cert")
612 mock_x509.load_pem_x509_certificate.side_effect = load_cert
614 self.validator = Mock()
615 self.validator.validate.return_value = {
616 "valid": True,
617 "warnings": ["w1", "w2"],
618 }
619 X509CertificateValidator.return_value = self.validator
621 @parameterized.expand([
622 ("early"),
623 ("standard"),
624 ])
625 def test_get_qeid_info_ok(self, requests, split_pem_certificates, mock_x509,
626 X509CertificateValidator, update):
627 self.configure_mocks(requests, split_pem_certificates, mock_x509,
628 X509CertificateValidator)
630 self.assertEqual({
631 "qeid_info": {
632 "enclaveIdentity": {
633 "a": 1,
634 "b": 2,
635 "c": 3,
636 },
637 "something": "else",
638 "another": "thing",
639 "signature": self.issuer_sig.hex(),
640 },
641 "warnings": [f"Getting the-url?update={update}: "
642 "this is a warning", "w1", "w2"],
643 }, get_qeid_info("the-url", self.cert1, update=update))
645 requests.get.assert_called_with(f"the-url?update={update}")
646 X509CertificateValidator.assert_called_with(get_intel_pcs_x509_crl)
647 self.validator.validate.assert_called_once()
648 self.assertEqual(self.cert0, self.validator.validate.call_args_list[0].args[0])
649 self.assertEqual(self.cert1, self.validator.validate.call_args_list[0].args[1])
650 self.assertIsInstance(self.validator.validate.call_args_list[0].args[2], datetime)
651 self.cert0pk.public_bytes.assert_called_once()
652 self.cert0pk.public_bytes.assert_called_with(
653 Encoding.X962, PublicFormat.CompressedPoint)
655 def test_get_qeid_info_err_get(self, requests, split_pem_certificates, mock_x509,
656 X509CertificateValidator):
657 self.configure_mocks(requests, split_pem_certificates, mock_x509,
658 X509CertificateValidator)
659 self.mock_response.status_code = 404
661 with self.assertRaises(RuntimeError) as e:
662 get_qeid_info("the-url", self.cert1, update="upd")
663 self.assertIn("replied with status", str(e.exception))
665 requests.get.assert_called_with("the-url?update=upd")
666 X509CertificateValidator.assert_not_called()
667 self.validator.validate.assert_not_called()
668 self.cert0pk.public_bytes.assert_not_called()
670 def test_get_qeid_info_err_ctype(self, requests, split_pem_certificates, mock_x509,
671 X509CertificateValidator):
672 self.configure_mocks(requests, split_pem_certificates, mock_x509,
673 X509CertificateValidator)
674 self.mock_response.headers["Content-Type"] = "not/json"
676 with self.assertRaises(RuntimeError) as e:
677 get_qeid_info("the-url", self.cert1, update="upd")
678 self.assertIn("content-type", str(e.exception))
680 requests.get.assert_called_with("the-url?update=upd")
681 X509CertificateValidator.assert_not_called()
682 self.validator.validate.assert_not_called()
683 self.cert0pk.public_bytes.assert_not_called()
685 def test_get_qeid_info_err_nochain(self, requests, split_pem_certificates, mock_x509,
686 X509CertificateValidator):
687 self.configure_mocks(requests, split_pem_certificates, mock_x509,
688 X509CertificateValidator)
689 self.mock_response.headers["SGX-Enclave-Identity-Issuer-Chain"] = None
691 with self.assertRaises(RuntimeError) as e:
692 get_qeid_info("the-url", self.cert1, update="upd")
693 self.assertIn("certification chain", str(e.exception))
695 requests.get.assert_called_with("the-url?update=upd")
696 X509CertificateValidator.assert_not_called()
697 self.validator.validate.assert_not_called()
698 self.cert0pk.public_bytes.assert_not_called()
700 def test_get_qeid_info_err_shortchain(self, requests, split_pem_certificates,
701 mock_x509, X509CertificateValidator):
702 self.configure_mocks(requests, split_pem_certificates, mock_x509,
703 X509CertificateValidator)
704 split_pem_certificates.return_value = ["cert-0"]
706 with self.assertRaises(RuntimeError) as e:
707 get_qeid_info("the-url", self.cert1, update="upd")
708 self.assertIn("at least two certificates", str(e.exception))
710 requests.get.assert_called_with("the-url?update=upd")
711 X509CertificateValidator.assert_not_called()
712 self.validator.validate.assert_not_called()
713 self.cert0pk.public_bytes.assert_not_called()
715 def test_get_qeid_info_err_rot(self, requests, split_pem_certificates,
716 mock_x509, X509CertificateValidator):
717 self.configure_mocks(requests, split_pem_certificates, mock_x509,
718 X509CertificateValidator)
720 other_root = Mock()
721 other_root.subject = "other root"
723 with self.assertRaises(RuntimeError) as e:
724 get_qeid_info("the-url", other_root, update="upd")
725 self.assertIn("does not match", str(e.exception))
726 self.assertIn("other root", str(e.exception))
728 requests.get.assert_called_with("the-url?update=upd")
729 X509CertificateValidator.assert_not_called()
730 self.validator.validate.assert_not_called()
731 self.cert0pk.public_bytes.assert_not_called()
733 def test_get_qeid_info_err_chain_iv(self, requests, split_pem_certificates,
734 mock_x509, X509CertificateValidator):
735 self.configure_mocks(requests, split_pem_certificates, mock_x509,
736 X509CertificateValidator)
738 self.validator.validate.return_value = {
739 "valid": False,
740 "reason": "oops"
741 }
743 with self.assertRaises(RuntimeError) as e:
744 get_qeid_info("the-url", self.cert1, update="upd")
745 self.assertIn("issuer chain", str(e.exception))
746 self.assertIn("oops", str(e.exception))
748 requests.get.assert_called_with("the-url?update=upd")
749 X509CertificateValidator.assert_called_with(get_intel_pcs_x509_crl)
750 self.validator.validate.assert_called_once()
751 self.assertEqual(self.cert0, self.validator.validate.call_args_list[0].args[0])
752 self.assertEqual(self.cert1, self.validator.validate.call_args_list[0].args[1])
753 self.assertIsInstance(self.validator.validate.call_args_list[0].args[2], datetime)
754 self.cert0pk.public_bytes.assert_not_called()
756 def test_get_qeid_info_err_qeid_sig(self, requests, split_pem_certificates,
757 mock_x509, X509CertificateValidator):
758 isig = bytes.fromhex("aa"*32+"bb"*32)
759 self.configure_mocks(requests, split_pem_certificates, mock_x509,
760 X509CertificateValidator, issuer_sig=isig)
762 with self.assertRaises(RuntimeError) as e:
763 get_qeid_info("the-url", self.cert1, update="upd")
764 self.assertIn("Signature verification failed", str(e.exception))
766 requests.get.assert_called_with("the-url?update=upd")
767 X509CertificateValidator.assert_called_with(get_intel_pcs_x509_crl)
768 self.validator.validate.assert_called_once()
769 self.assertEqual(self.cert0, self.validator.validate.call_args_list[0].args[0])
770 self.assertEqual(self.cert1, self.validator.validate.call_args_list[0].args[1])
771 self.assertIsInstance(self.validator.validate.call_args_list[0].args[2], datetime)
772 self.cert0pk.public_bytes.assert_called_once()
773 self.cert0pk.public_bytes.assert_called_with(
774 Encoding.X962, PublicFormat.CompressedPoint)
777class TestSgxUtilsValidateQeIdInfo(TestCase):
778 def setUp(self):
779 self.qe_report_info = SimpleNamespace(**{
780 "mrsigner": bytes.fromhex("aa"*32),
781 "isvprodid": 789,
782 "isvsvn": 123,
783 "miscselect": 0xcd6789ab,
784 "attributes": SimpleNamespace(**{
785 "flags": 0x44aa33bb22cc11,
786 "xfrm": 0x88dd77ee66ff55ee,
787 })
788 })
790 self.qeid_info = {
791 "id": "QE",
792 "version": 2,
793 "issueDate": "the issue date",
794 "nextUpdate": "the next update",
795 "tcbEvaluationDataNumber": 456,
796 "miscselect": "ab0000cd",
797 "miscselectMask": "FF0000FF",
798 "attributes": "11002200330044000055006600770088",
799 "attributesMask": "FF00FF00FF00FF0000FF00FF00FF00FF",
800 "mrsigner": "AA"*32,
801 "isvprodid": 789,
802 "tcbLevels": [
803 {
804 "tcb": {
805 "isvsvn": 130
806 },
807 "tcbDate": "2024-03-13T00:00:00Z",
808 "tcbStatus": "UpToDate"
809 },
810 {
811 "tcb": {
812 "isvsvn": 125
813 },
814 "tcbDate": "2021-11-10T00:00:00Z",
815 "tcbStatus": "OutOfDate",
816 "advisoryIDs": ["we don't", "care", "at", "all"]
817 },
818 {
819 "tcb": {
820 "isvsvn": 120
821 },
822 "tcbDate": "the tcb date",
823 "tcbStatus": "the tcb status",
824 "advisoryIDs": [
825 "advisory-1",
826 "advisory-2"
827 ]
828 }
829 ]
830 }
832 def test_validate_qeid_info_ok(self):
833 self.assertEqual({
834 "valid": True,
835 "status": "the tcb status",
836 "date": "the tcb date",
837 "advisories": ["advisory-1", "advisory-2"],
838 "edn": 456,
839 "isvsvn": "123 >= 120",
840 }, validate_qeid_info(self.qe_report_info, self.qeid_info))
842 def test_validate_qeid_info_mrsigner(self):
843 self.qe_report_info.mrsigner = "bb"*32
845 res = validate_qeid_info(self.qe_report_info, self.qeid_info)
846 self.assertFalse(res["valid"])
847 self.assertIn("QE MRSIGNER", res["reason"])
849 def test_validate_qeid_info_isvprodid(self):
850 self.qe_report_info.isvprodid = 444
852 res = validate_qeid_info(self.qe_report_info, self.qeid_info)
853 self.assertFalse(res["valid"])
854 self.assertIn("QE ISVPRODID", res["reason"])
856 def test_validate_qeid_info_miscselect(self):
857 self.qe_report_info.miscselect = 555
859 res = validate_qeid_info(self.qe_report_info, self.qeid_info)
860 self.assertFalse(res["valid"])
861 self.assertIn("QE MISCSELECT", res["reason"])
863 def test_validate_qeid_info_attribute_flags(self):
864 self.qe_report_info.attributes.flags = 666
866 res = validate_qeid_info(self.qe_report_info, self.qeid_info)
867 self.assertFalse(res["valid"])
868 self.assertIn("QE ATTRIBUTES", res["reason"])
870 def test_validate_qeid_info_attribute_xfrm(self):
871 self.qe_report_info.attributes.xfrm = 777
873 res = validate_qeid_info(self.qe_report_info, self.qeid_info)
874 self.assertFalse(res["valid"])
875 self.assertIn("QE ATTRIBUTES", res["reason"])
877 def test_validate_qeid_info_level_notfound(self):
878 self.qe_report_info.isvsvn = 119
880 res = validate_qeid_info(self.qe_report_info, self.qeid_info)
881 self.assertFalse(res["valid"])
882 self.assertIn("QE TCB level is not supported", res["reason"])
884 def test_validate_qeid_info_malformed(self):
885 self.qe_report_info = {
886 "something": "else"
887 }
889 res = validate_qeid_info(self.qe_report_info, self.qeid_info)
890 self.assertFalse(res["valid"])
891 self.assertIn("While validating QE identity information", res["reason"])