diff --git a/middleware/admin/certificate_v2.py b/middleware/admin/certificate_v2.py index e84591d2..f5bf6a2e 100644 --- a/middleware/admin/certificate_v2.py +++ b/middleware/admin/certificate_v2.py @@ -23,6 +23,8 @@ import re from pathlib import Path import base64 +import ecdsa +import hashlib from .certificate_v1 import HSMCertificate from .utils import is_nonempty_hex_string from sgx.envelope import SgxQuote @@ -62,14 +64,10 @@ def get_value(self): raise NotImplementedError(f"{type(self).__name__} can't provide a value") def get_pubkey(self): - # TODO: this should yield not implemented - # TODO: implementation should be down to each specific subclass - return None + raise NotImplementedError(f"{type(self).__name__} can't provide a public key") def is_valid(self, certifier): - # TODO: this should yield not implemented - # TODO: implementation should be down to each specific subclass - return True + raise NotImplementedError(f"{type(self).__name__} can't be queried for validity") def get_tweak(self): return None @@ -97,7 +95,7 @@ def _init_with_map(self, element_map): @property def message(self): - return self._message.hex() + return SgxQuote(self._message) @property def custom_data(self): @@ -107,9 +105,25 @@ def custom_data(self): def signature(self): return self._signature.hex() + def is_valid(self, certifier): + try: + # Validate custom data + expected = hashlib.sha256(self._custom_data).digest() + if expected != self.message.report_body.report_data.field[:len(expected)]: + return False + + # Verify signature against the certifier + return certifier.get_pubkey().verify_digest( + self._signature, + hashlib.sha256(self._message).digest(), + ecdsa.util.sigdecode_der, + ) + except Exception: + return False + def get_value(self): return { - "sgx_quote": SgxQuote(self._message), + "sgx_quote": self.message, "message": self.custom_data, } @@ -117,7 +131,7 @@ def to_dict(self): return { "name": self.name, "type": "sgx_quote", - "message": self.message, + "message": self._message.hex(), "custom_data": self.custom_data, "signature": self.signature, "signed_by": self.signed_by, @@ -163,6 +177,12 @@ def auth_data(self): def signature(self): return self._signature.hex() + def is_valid(self, certifier): + return True + + def get_pubkey(self): + return ecdsa.VerifyingKey.from_string(self._key, ecdsa.NIST256p) + def to_dict(self): return { "name": self.name, @@ -206,6 +226,12 @@ def _init_with_map(self, element_map): def message(self): return base64.b64encode(self._message).decode("ASCII") + def is_valid(self, certifier): + return True + + def get_pubkey(self): + return None + def to_dict(self): return { "name": self.name, diff --git a/middleware/tests/admin/test_certificate_v2.py b/middleware/tests/admin/test_certificate_v2.py index 99a82099..ae292645 100644 --- a/middleware/tests/admin/test_certificate_v2.py +++ b/middleware/tests/admin/test_certificate_v2.py @@ -20,9 +20,13 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +import ecdsa +import hashlib from unittest import TestCase -from unittest.mock import patch +from unittest.mock import Mock, patch +from parameterized import parameterized from admin.certificate_v1 import HSMCertificate +from sgx.envelope import SgxQuote from admin.certificate_v2 import HSMCertificateV2, HSMCertificateV2Element, \ HSMCertificateV2ElementSGXQuote, \ HSMCertificateV2ElementSGXAttestationKey, \ @@ -36,43 +40,62 @@ def test_behavior_inherited(self): def test_create_empty_certificate_ok(self): cert = HSMCertificateV2() - self.assertEqual({'version': 2, 'targets': [], 'elements': []}, cert.to_dict()) + self.assertEqual({"version": 2, "targets": [], "elements": []}, cert.to_dict()) def test_parse_identity(self): cert = HSMCertificateV2(TEST_CERTIFICATE) self.assertEqual(TEST_CERTIFICATE, cert.to_dict()) - @patch("admin.certificate_v2.SgxQuote") - def test_validate_and_get_values_value(self, SgxQuoteMock): - SgxQuoteMock.return_value = "an-sgx-quote" + def mock_element(self, which_one_invalid): + class MockElement: + def __init__(self, d): + self.d = d + self.name = d["name"] + self.signed_by = d["signed_by"] + + def is_valid(self, c): + return self.name != which_one_invalid + + def get_value(self): + return f"the value for {self.name}" + + def get_tweak(self): + return None + + def mock_element_factory(k, d): + return MockElement(d) + + HSMCertificateV2.ELEMENT_FACTORY = mock_element_factory + + def test_validate_and_get_values_value(self): + self.mock_element(True) + cert = HSMCertificateV2(TEST_CERTIFICATE) + self.assertEqual({ + "quote": (True, "the value for quote", None), + }, cert.validate_and_get_values("a-root-of-trust")) + + @parameterized.expand([ + ("invalid_quote", "quote"), + ("invalid_attestation", "attestation"), + ("invalid_qe", "quoting_enclave"), + ("invalid_plf", "platform_ca"), + ]) + def test_validate_and_get_values_invalid(self, _, invalid_name): + self.mock_element(invalid_name) cert = HSMCertificateV2(TEST_CERTIFICATE) self.assertEqual({ - "quote": ( - True, { - "sgx_quote": "an-sgx-quote", - "message": "504f5748534d3a352e343a3a736778f36f7bc09aab50c0886a442b2" - "d04b18186720bda7a753643066cd0bc0a4191800c4d091913d39750" - "dc8975adbdd261bd10c1c2e110faa47cfbe30e740895552bbdcb3c1" - "7c7aee714cec8ad900341bfd987b452280220dcbd6e7191f67ea420" - "9b00000000000000000000000000000000", - }, None) - }, cert.validate_and_get_values('a-root-of-trust')) - SgxQuoteMock.assert_called_with(bytes.fromhex( - "03000200000000000a000f00939a7233f79c4ca9940a0db3957f0607ceae3549bc7273eb34" - "d562f4564fc182000000000e0e100fffff0100000000000000000001000000000000000000" - "00000000000000000000000000000000000000000000050000000000000007000000000000" - "00d32688d3c1f3dfcc8b0b36eac7c89d49af331800bd56248044166fa6699442c100000000" - "00000000000000000000000000000000000000000000000000000000718c2f1a0efbd513e0" - "16fafd6cf62a624442f2d83708d4b33ab5a8d8c1cd4dd00000000000000000000000000000" - "00000000000000000000000000000000000000000000000000000000000000000000000000" - "00000000000000000000000000000000000000000000000000000000000000000000000000" - "00000000000000006400010000000000000000000000000000000000000000000000000000" - "00000000000000000000000000000000000000000000000000000000000000000000009e95" - "bb875c1a728071f70ad8c9d03f1744c19acb0580921e611ac9104f7701d000000000000000" - "00000000000000000000000000000000000000000000000000")) + "quote": (False, invalid_name), + }, cert.validate_and_get_values("a-root-of-trust")) class TestHSMCertificateV2Element(TestCase): + def setUp(self): + class TestElement(HSMCertificateV2Element): + def __init__(self): + pass + + self.instance = TestElement() + def test_from_dict_unknown_type(self): with self.assertRaises(ValueError) as e: HSMCertificateV2Element.from_dict({ @@ -103,12 +126,41 @@ def test_from_dict_no_signed_by(self): }) self.assertIn("Missing certifier", str(e.exception)) + def test_cant_instantiate(self): + with self.assertRaises(NotImplementedError): + HSMCertificateV2Element() + + def test_get_pubkey_notimplemented(self): + with self.assertRaises(NotImplementedError): + self.instance.get_pubkey() + + def test_get_value_notimplemented(self): + with self.assertRaises(NotImplementedError): + self.instance.get_value() + + def test_is_valid_notimplemented(self): + with self.assertRaises(NotImplementedError): + self.instance.is_valid("a-certifier") + class TestHSMCertificateV2ElementSGXQuote(TestCase): + TEST_MESSAGE = \ + "03000200000000000a000f00939a7233f79c4ca9940a0db3957f0607ceae3549bc7273eb34d562f"\ + "4564fc182000000000e0e100fffff01000000000000000000010000000000000000000000000000"\ + "000000000000000000000000000000000005000000000000000700000000000000d32688d3c1f3d"\ + "fcc8b0b36eac7c89d49af331800bd56248044166fa6699442c10000000000000000000000000000"\ + "000000000000000000000000000000000000718c2f1a0efbd513e016fafd6cf62a624442f2d8370"\ + "8d4b33ab5a8d8c1cd4dd00000000000000000000000000000000000000000000000000000000000"\ + "0000000000000000000000000000000000000000000000000000000000000000000000000000000"\ + "0000000000000000000000000000000000000000000000000000000640001000000000000000000"\ + "0000000000000000000000000000000000000000000000000000000000000000000000000000000"\ + "00000000000000000000000005d53b30e22f66979d36721e10ab7722557257a9ef8ba77ec7fe430"\ + "493c3542f90000000000000000000000000000000000000000000000000000000000000000" + def setUp(self): self.elem = HSMCertificateV2ElementSGXQuote({ "name": "thename", - "message": "aabbcc", + "message": self.TEST_MESSAGE, "custom_data": "ddeeff", "signature": "112233", "signed_by": "whosigned", @@ -117,7 +169,9 @@ def setUp(self): def test_props(self): self.assertEqual("thename", self.elem.name) self.assertEqual("whosigned", self.elem.signed_by) - self.assertEqual("aabbcc", self.elem.message) + self.assertIsInstance(self.elem.message, SgxQuote) + self.assertEqual(bytes.fromhex(self.TEST_MESSAGE), + self.elem.message.get_raw_data()) self.assertEqual("ddeeff", self.elem.custom_data) self.assertEqual("112233", self.elem.signature) @@ -125,7 +179,7 @@ def test_dict_ok(self): self.assertEqual({ "name": "thename", "type": "sgx_quote", - "message": "aabbcc", + "message": self.TEST_MESSAGE, "custom_data": "ddeeff", "signature": "112233", "signed_by": "whosigned", @@ -154,7 +208,7 @@ def test_from_dict_invalid_custom_data(self): HSMCertificateV2Element.from_dict({ "name": "quote", "type": "sgx_quote", - "message": "aabbccdd", + "message": self.TEST_MESSAGE, "custom_data": "not-hex", "signature": "445566778899", "signed_by": "attestation" @@ -166,13 +220,68 @@ def test_from_dict_invalid_signature(self): HSMCertificateV2Element.from_dict({ "name": "quote", "type": "sgx_quote", - "message": "aabbccdd", + "message": self.TEST_MESSAGE, "custom_data": "112233", "signature": "not-hex", "signed_by": "attestation" }) self.assertIn("Invalid signature", str(e.exception)) + def test_get_pubkey_notimplemented(self): + with self.assertRaises(NotImplementedError): + self.elem.get_pubkey() + + def test_is_valid_ok(self): + pk = ecdsa.SigningKey.generate(ecdsa.NIST256p) + certifier = Mock() + certifier.get_pubkey.return_value = pk.verifying_key + + valid_elem = HSMCertificateV2ElementSGXQuote({ + "name": "thename", + "message": self.TEST_MESSAGE, + "custom_data": "10061982", + "signature": pk.sign_digest( + hashlib.sha256(bytes.fromhex(self.TEST_MESSAGE)).digest(), + sigencode=ecdsa.util.sigencode_der + ).hex(), + "signed_by": "whosigned", + }) + self.assertTrue(valid_elem.is_valid(certifier)) + + def test_is_valid_custom_data_mismatch(self): + pk = ecdsa.SigningKey.generate(ecdsa.NIST256p) + certifier = Mock() + certifier.get_pubkey.return_value = pk.verifying_key + + valid_elem = HSMCertificateV2ElementSGXQuote({ + "name": "thename", + "message": self.TEST_MESSAGE, + "custom_data": "11061982", + "signature": pk.sign_digest( + hashlib.sha256(bytes.fromhex(self.TEST_MESSAGE)).digest(), + sigencode=ecdsa.util.sigencode_der + ).hex(), + "signed_by": "whosigned", + }) + self.assertFalse(valid_elem.is_valid(certifier)) + + def test_is_valid_signature_mismatch(self): + pk = ecdsa.SigningKey.generate(ecdsa.NIST256p) + certifier = Mock() + certifier.get_pubkey.return_value = pk.verifying_key + + valid_elem = HSMCertificateV2ElementSGXQuote({ + "name": "thename", + "message": self.TEST_MESSAGE, + "custom_data": "10061982", + "signature": pk.sign_digest( + hashlib.sha256(b"something else").digest(), + sigencode=ecdsa.util.sigencode_der + ).hex(), + "signed_by": "whosigned", + }) + self.assertFalse(valid_elem.is_valid(certifier)) + class TestHSMCertificateV2ElementSGXAttestationKey(TestCase): def setUp(self): @@ -262,6 +371,10 @@ def test_from_dict_invalid_signature(self): }) self.assertIn("Invalid signature", str(e.exception)) + def test_get_value_notimplemented(self): + with self.assertRaises(NotImplementedError): + self.elem.get_value() + class TestHSMCertificateV2ElementX509(TestCase): def setUp(self): @@ -300,6 +413,10 @@ def test_from_dict_invalid_message(self): }) self.assertIn("Invalid message", str(e.exception)) + def test_get_value_notimplemented(self): + with self.assertRaises(NotImplementedError): + self.elem.get_value() + def test_from_pem(self): self.assertEqual({ "name": "thename",