From 82e83e42a6e72f4c384712fa452f3813eef4ab7c Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Tue, 28 May 2024 21:01:20 +0200 Subject: [PATCH 1/9] Problem: The server don't have a directory to save the platform certificates generated by sevctl. Solution: Set that directory field on settings class and ensure to create the folder on initialization step. --- src/aleph/vm/conf.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/aleph/vm/conf.py b/src/aleph/vm/conf.py index ef69ffb14..9a5c938ba 100644 --- a/src/aleph/vm/conf.py +++ b/src/aleph/vm/conf.py @@ -267,6 +267,11 @@ class Settings(BaseSettings): "with SEV and SEV-ES", ) + CONFIDENTIAL_DIRECTORY: Path = Field( + None, + description="Confidential Computing default directory. Default to EXECUTION_ROOT/confidential", + ) + # Tests on programs FAKE_DATA_PROGRAM: Optional[Path] = None @@ -409,6 +414,7 @@ def setup(self): os.makedirs(self.EXECUTION_LOG_DIRECTORY, exist_ok=True) os.makedirs(self.PERSISTENT_VOLUMES_DIR, exist_ok=True) + os.makedirs(self.CONFIDENTIAL_DIRECTORY, exist_ok=True) self.API_SERVER = self.API_SERVER.rstrip("/") @@ -467,6 +473,8 @@ def __init__( self.EXECUTION_LOG_DIRECTORY = self.EXECUTION_ROOT / "executions" if not self.JAILER_BASE_DIR: self.JAILER_BASE_DIR = self.EXECUTION_ROOT / "jailer" + if not self.CONFIDENTIAL_DIRECTORY: + self.CONFIDENTIAL_DIRECTORY = self.EXECUTION_ROOT / "confidential" class Config: env_prefix = "ALEPH_VM_" From 39bee9a13a0a3c29c50f4670dd53ec2342f87e82 Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Tue, 28 May 2024 21:07:15 +0200 Subject: [PATCH 2/9] Problem: The aren't an endpoint to be able to get the confidential platform certificates to start the VM key exchange. Solution: Create that endpoint and return the platform certificates generated by the `sevctl` command. --- src/aleph/vm/conf.py | 1 + src/aleph/vm/orchestrator/resources.py | 15 +++++++++++++++ src/aleph/vm/orchestrator/supervisor.py | 10 +++++++++- src/aleph/vm/sevclient.py | 22 ++++++++++++++++++++++ 4 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 src/aleph/vm/sevclient.py diff --git a/src/aleph/vm/conf.py b/src/aleph/vm/conf.py index 9a5c938ba..53d1a1d27 100644 --- a/src/aleph/vm/conf.py +++ b/src/aleph/vm/conf.py @@ -387,6 +387,7 @@ def check(self): assert ( check_system_module("kvm_amd/parameters/sev_es") == "Y" ), "SEV-ES feature isn't enabled, enable it in BIOS" + assert is_command_available("sevctl"), "Command `sevctl` not found, run `cargo install sevctl`" assert self.ENABLE_QEMU_SUPPORT, "Qemu Support is needed for confidential computing and it's disabled, " "enable it setting the env variable `ENABLE_QEMU_SUPPORT=True` in configuration" diff --git a/src/aleph/vm/orchestrator/resources.py b/src/aleph/vm/orchestrator/resources.py index 448a822c5..e1b00f79e 100644 --- a/src/aleph/vm/orchestrator/resources.py +++ b/src/aleph/vm/orchestrator/resources.py @@ -122,6 +122,21 @@ async def about_system_usage(_: web.Request): return web.json_response(text=usage.json(exclude_none=True)) +@cors_allow_all +async def about_certificates(request: web.Request): + """Public endpoint to expose platform certificates for confidential computing.""" + + if not settings.ENABLE_CONFIDENTIAL_COMPUTING: + return web.HTTPBadRequest(reason="Confidential computing setting not enabled on that server") + + sev_client = request.app["sev_client"] + + if not sev_client.certificates_archive.is_file(): + sev_client.export_certificates() + + return web.FileResponse(sev_client.certificates_archive) + + class Allocation(BaseModel): """An allocation is the set of resources that are currently allocated on this orchestrator. It contains the item_hashes of all persistent VMs, instances, on-demand VMs and jobs. diff --git a/src/aleph/vm/orchestrator/supervisor.py b/src/aleph/vm/orchestrator/supervisor.py index 892106ba0..0bd55beef 100644 --- a/src/aleph/vm/orchestrator/supervisor.py +++ b/src/aleph/vm/orchestrator/supervisor.py @@ -21,7 +21,7 @@ from aleph.vm.version import __version__ from .metrics import create_tables, setup_engine -from .resources import about_system_usage +from .resources import about_system_usage, about_certificates from .tasks import ( start_payment_monitoring_task, start_watch_for_messages_task, @@ -52,6 +52,7 @@ operate_stop, stream_logs, ) +from ..sevclient import SevClient logger = logging.getLogger(__name__) @@ -95,6 +96,7 @@ def setup_webapp(): web.get("/about/executions/details", about_executions), web.get("/about/executions/records", about_execution_records), web.get("/about/usage/system", about_system_usage), + web.get("/about/certificates", about_certificates), web.get("/about/config", about_config), # /control APIs are used to control the VMs and access their logs web.post("/control/allocation/notify", notify_allocation), @@ -159,6 +161,12 @@ def run(): app["secret_token"] = secret_token app["vm_pool"] = pool + # Store sevctl app singleton only if confidential feature is enabled + if settings.ENABLE_CONFIDENTIAL_COMPUTING: + sev_client = SevClient(settings.CONFIDENTIAL_DIRECTORY) + app["sev_client"] = sev_client + # TODO: Review and check sevctl first initialization steps, like (sevctl generate and sevctl provision) + logger.debug(f"Login to /about pages {protocol}://{hostname}/about/login?token={secret_token}") try: diff --git a/src/aleph/vm/sevclient.py b/src/aleph/vm/sevclient.py new file mode 100644 index 000000000..aa5b43a73 --- /dev/null +++ b/src/aleph/vm/sevclient.py @@ -0,0 +1,22 @@ +import subprocess +from pathlib import Path + + +class SevClient: + def __init__(self, sev_dir: Path): + self.sev_dir = sev_dir + self.certificates_dir = sev_dir / "platform" + self.certificates_dir.mkdir(exist_ok=True, parents=True) + self.certificates_archive = self.certificates_dir / "certs_export.cert" + + def sevctl_cmd(self, *args) -> subprocess.CompletedProcess: + result = subprocess.run( + ["sevctl", *args], + capture_output=True, + text=True, + ) + + return result + + def export_certificates(self): + _ = self.sevctl_cmd("export", self.certificates_archive) From 5f2ef4995db1297fdc645356032e41eab023426c Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Tue, 28 May 2024 21:11:26 +0200 Subject: [PATCH 3/9] Fix: Solved code quality issues. --- src/aleph/vm/orchestrator/supervisor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/aleph/vm/orchestrator/supervisor.py b/src/aleph/vm/orchestrator/supervisor.py index 0bd55beef..054188838 100644 --- a/src/aleph/vm/orchestrator/supervisor.py +++ b/src/aleph/vm/orchestrator/supervisor.py @@ -20,8 +20,9 @@ from aleph.vm.pool import VmPool from aleph.vm.version import __version__ +from ..sevclient import SevClient from .metrics import create_tables, setup_engine -from .resources import about_system_usage, about_certificates +from .resources import about_certificates, about_system_usage from .tasks import ( start_payment_monitoring_task, start_watch_for_messages_task, @@ -52,7 +53,6 @@ operate_stop, stream_logs, ) -from ..sevclient import SevClient logger = logging.getLogger(__name__) From 143909631041dd529538aa71060a5130f6e768de Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Tue, 28 May 2024 22:27:49 +0200 Subject: [PATCH 4/9] Fix: Added 2 test cases for that endpoint. --- src/aleph/vm/orchestrator/supervisor.py | 2 +- tests/supervisor/test_views.py | 54 +++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/aleph/vm/orchestrator/supervisor.py b/src/aleph/vm/orchestrator/supervisor.py index 054188838..a2a712445 100644 --- a/src/aleph/vm/orchestrator/supervisor.py +++ b/src/aleph/vm/orchestrator/supervisor.py @@ -18,9 +18,9 @@ from aleph.vm.conf import settings from aleph.vm.pool import VmPool +from aleph.vm.sevclient import SevClient from aleph.vm.version import __version__ -from ..sevclient import SevClient from .metrics import create_tables, setup_engine from .resources import about_certificates, about_system_usage from .tasks import ( diff --git a/tests/supervisor/test_views.py b/tests/supervisor/test_views.py index 254e326df..6d9ddd80d 100644 --- a/tests/supervisor/test_views.py +++ b/tests/supervisor/test_views.py @@ -1,8 +1,13 @@ +from pathlib import Path +from unittest import mock +from unittest.mock import call + import pytest from aiohttp import web from aleph.vm.conf import settings from aleph.vm.orchestrator.supervisor import setup_webapp +from aleph.vm.sevclient import SevClient @pytest.mark.asyncio @@ -121,3 +126,52 @@ def get_persistent_executions(self): ) assert response.status == 200 assert await response.json() == {"success": True, "successful": [], "failing": [], "errors": {}} + + +@pytest.mark.asyncio +async def test_about_certificates_missing_setting(aiohttp_client): + """Test that the certificates system endpoint returns an error if the setting isn't enabled""" + settings.ENABLE_CONFIDENTIAL_COMPUTING = False + + app = setup_webapp() + app["sev_client"] = SevClient(Path().resolve()) + client = await aiohttp_client(app) + response: web.Response = await client.get("/about/certificates") + assert response.status == 400 + assert await response.text() == "400: Confidential computing setting not enabled on that server" + + +@pytest.mark.asyncio +async def test_about_certificates(aiohttp_client): + """Test that the certificates system endpoint responds. No auth needed""" + + settings.ENABLE_QEMU_SUPPORT = True + settings.ENABLE_CONFIDENTIAL_COMPUTING = True + settings.CONFIDENTIAL_DIRECTORY = Path().resolve() + settings.setup() + + with mock.patch( + "pathlib.Path.is_file", + return_value=False, + ) as is_file_mock: + with mock.patch( + "subprocess.run", + return_value=True, + ) as export_mock: + app = setup_webapp() + sev_client = SevClient(settings.CONFIDENTIAL_DIRECTORY) + app["sev_client"] = sev_client + # Create mock file to return it + Path(sev_client.certificates_archive).touch(exist_ok=True) + + client = await aiohttp_client(app) + response: web.Response = await client.get("/about/certificates") + assert response.status == 200 + is_file_mock.assert_has_calls([call(), call()]) + certificates_expected_dir = sev_client.certificates_archive + export_mock.assert_called_once_with( + ["sevctl", "export", certificates_expected_dir], capture_output=True, text=True + ) + + # Remove file mock + Path(sev_client.certificates_archive).unlink() From 3c294777b7c99802dee27386e690b2adba0b9a7f Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Tue, 4 Jun 2024 18:31:58 +0200 Subject: [PATCH 5/9] Fix: Added PR suggestions. --- src/aleph/vm/orchestrator/resources.py | 5 +++-- src/aleph/vm/sevclient.py | 14 +++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/aleph/vm/orchestrator/resources.py b/src/aleph/vm/orchestrator/resources.py index e1b00f79e..1babc9542 100644 --- a/src/aleph/vm/orchestrator/resources.py +++ b/src/aleph/vm/orchestrator/resources.py @@ -11,6 +11,7 @@ from pydantic import BaseModel, Field from aleph.vm.conf import settings +from aleph.vm.sevclient import SevClient from aleph.vm.utils import cors_allow_all @@ -129,10 +130,10 @@ async def about_certificates(request: web.Request): if not settings.ENABLE_CONFIDENTIAL_COMPUTING: return web.HTTPBadRequest(reason="Confidential computing setting not enabled on that server") - sev_client = request.app["sev_client"] + sev_client: SevClient = request.app["sev_client"] if not sev_client.certificates_archive.is_file(): - sev_client.export_certificates() + await sev_client.export_certificates() return web.FileResponse(sev_client.certificates_archive) diff --git a/src/aleph/vm/sevclient.py b/src/aleph/vm/sevclient.py index aa5b43a73..8ac2b6477 100644 --- a/src/aleph/vm/sevclient.py +++ b/src/aleph/vm/sevclient.py @@ -1,6 +1,7 @@ -import subprocess from pathlib import Path +from aleph.vm.utils import run_in_subprocess + class SevClient: def __init__(self, sev_dir: Path): @@ -9,14 +10,13 @@ def __init__(self, sev_dir: Path): self.certificates_dir.mkdir(exist_ok=True, parents=True) self.certificates_archive = self.certificates_dir / "certs_export.cert" - def sevctl_cmd(self, *args) -> subprocess.CompletedProcess: - result = subprocess.run( + async def sevctl_cmd(self, *args) -> bytes: + result = await run_in_subprocess( ["sevctl", *args], - capture_output=True, - text=True, + check=True, ) return result - def export_certificates(self): - _ = self.sevctl_cmd("export", self.certificates_archive) + async def export_certificates(self): + _ = await self.sevctl_cmd("export", self.certificates_archive) From 7c6c3ac74a6a71dd445fa3a37bd383bdf6401e0e Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Tue, 4 Jun 2024 18:33:39 +0200 Subject: [PATCH 6/9] Fix: Modified test mock to let the tests work --- tests/supervisor/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/supervisor/test_views.py b/tests/supervisor/test_views.py index 6d9ddd80d..c87ea9e08 100644 --- a/tests/supervisor/test_views.py +++ b/tests/supervisor/test_views.py @@ -155,7 +155,7 @@ async def test_about_certificates(aiohttp_client): return_value=False, ) as is_file_mock: with mock.patch( - "subprocess.run", + "aleph.vm.utils.run_in_subprocess", return_value=True, ) as export_mock: app = setup_webapp() From 5d7044bd282da7b5d807a51278fa9ef9b9473e07 Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Wed, 5 Jun 2024 11:22:48 +0200 Subject: [PATCH 7/9] Fix: Solve test issues after code quality fixes --- src/aleph/vm/sevclient.py | 2 +- tests/supervisor/test_views.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/aleph/vm/sevclient.py b/src/aleph/vm/sevclient.py index 8ac2b6477..0139c572a 100644 --- a/src/aleph/vm/sevclient.py +++ b/src/aleph/vm/sevclient.py @@ -19,4 +19,4 @@ async def sevctl_cmd(self, *args) -> bytes: return result async def export_certificates(self): - _ = await self.sevctl_cmd("export", self.certificates_archive) + _ = await self.sevctl_cmd("export", str(self.certificates_archive)) diff --git a/tests/supervisor/test_views.py b/tests/supervisor/test_views.py index c87ea9e08..b957a21f7 100644 --- a/tests/supervisor/test_views.py +++ b/tests/supervisor/test_views.py @@ -155,7 +155,7 @@ async def test_about_certificates(aiohttp_client): return_value=False, ) as is_file_mock: with mock.patch( - "aleph.vm.utils.run_in_subprocess", + "aleph.vm.sevclient.run_in_subprocess", return_value=True, ) as export_mock: app = setup_webapp() @@ -170,7 +170,7 @@ async def test_about_certificates(aiohttp_client): is_file_mock.assert_has_calls([call(), call()]) certificates_expected_dir = sev_client.certificates_archive export_mock.assert_called_once_with( - ["sevctl", "export", certificates_expected_dir], capture_output=True, text=True + ["sevctl", "export", str(certificates_expected_dir)], check=True ) # Remove file mock From 017115edccedbb351fb051131c35e178a01e62e9 Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Wed, 5 Jun 2024 11:59:30 +0200 Subject: [PATCH 8/9] Fix: Solve code quality issues. --- tests/supervisor/test_views.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/supervisor/test_views.py b/tests/supervisor/test_views.py index b957a21f7..41b609ab3 100644 --- a/tests/supervisor/test_views.py +++ b/tests/supervisor/test_views.py @@ -169,9 +169,7 @@ async def test_about_certificates(aiohttp_client): assert response.status == 200 is_file_mock.assert_has_calls([call(), call()]) certificates_expected_dir = sev_client.certificates_archive - export_mock.assert_called_once_with( - ["sevctl", "export", str(certificates_expected_dir)], check=True - ) + export_mock.assert_called_once_with(["sevctl", "export", str(certificates_expected_dir)], check=True) # Remove file mock Path(sev_client.certificates_archive).unlink() From 68413be5a28e8f7cb30279505758785085891333 Mon Sep 17 00:00:00 2001 From: "Andres D. Molins" Date: Wed, 5 Jun 2024 13:04:33 +0200 Subject: [PATCH 9/9] Fix: Solved more PR comments. --- src/aleph/vm/orchestrator/resources.py | 5 +---- src/aleph/vm/sevclient.py | 9 ++++---- tests/supervisor/test_views.py | 30 ++++++++++++-------------- 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/aleph/vm/orchestrator/resources.py b/src/aleph/vm/orchestrator/resources.py index 1babc9542..fe9deab26 100644 --- a/src/aleph/vm/orchestrator/resources.py +++ b/src/aleph/vm/orchestrator/resources.py @@ -132,10 +132,7 @@ async def about_certificates(request: web.Request): sev_client: SevClient = request.app["sev_client"] - if not sev_client.certificates_archive.is_file(): - await sev_client.export_certificates() - - return web.FileResponse(sev_client.certificates_archive) + return web.FileResponse(await sev_client.get_certificates()) class Allocation(BaseModel): diff --git a/src/aleph/vm/sevclient.py b/src/aleph/vm/sevclient.py index 0139c572a..fe9eb1c00 100644 --- a/src/aleph/vm/sevclient.py +++ b/src/aleph/vm/sevclient.py @@ -11,12 +11,13 @@ def __init__(self, sev_dir: Path): self.certificates_archive = self.certificates_dir / "certs_export.cert" async def sevctl_cmd(self, *args) -> bytes: - result = await run_in_subprocess( + return await run_in_subprocess( ["sevctl", *args], check=True, ) - return result + async def get_certificates(self) -> Path: + if not self.certificates_archive.is_file(): + _ = await self.sevctl_cmd("export", str(self.certificates_archive)) - async def export_certificates(self): - _ = await self.sevctl_cmd("export", str(self.certificates_archive)) + return self.certificates_archive diff --git a/tests/supervisor/test_views.py b/tests/supervisor/test_views.py index 41b609ab3..52426d48c 100644 --- a/tests/supervisor/test_views.py +++ b/tests/supervisor/test_views.py @@ -1,3 +1,4 @@ +import tempfile from pathlib import Path from unittest import mock from unittest.mock import call @@ -147,7 +148,6 @@ async def test_about_certificates(aiohttp_client): settings.ENABLE_QEMU_SUPPORT = True settings.ENABLE_CONFIDENTIAL_COMPUTING = True - settings.CONFIDENTIAL_DIRECTORY = Path().resolve() settings.setup() with mock.patch( @@ -158,18 +158,16 @@ async def test_about_certificates(aiohttp_client): "aleph.vm.sevclient.run_in_subprocess", return_value=True, ) as export_mock: - app = setup_webapp() - sev_client = SevClient(settings.CONFIDENTIAL_DIRECTORY) - app["sev_client"] = sev_client - # Create mock file to return it - Path(sev_client.certificates_archive).touch(exist_ok=True) - - client = await aiohttp_client(app) - response: web.Response = await client.get("/about/certificates") - assert response.status == 200 - is_file_mock.assert_has_calls([call(), call()]) - certificates_expected_dir = sev_client.certificates_archive - export_mock.assert_called_once_with(["sevctl", "export", str(certificates_expected_dir)], check=True) - - # Remove file mock - Path(sev_client.certificates_archive).unlink() + with tempfile.TemporaryDirectory() as tmp_dir: + app = setup_webapp() + sev_client = SevClient(Path(tmp_dir)) + app["sev_client"] = sev_client + # Create mock file to return it + Path(sev_client.certificates_archive).touch(exist_ok=True) + + client = await aiohttp_client(app) + response: web.Response = await client.get("/about/certificates") + assert response.status == 200 + is_file_mock.assert_has_calls([call(), call()]) + certificates_expected_dir = sev_client.certificates_archive + export_mock.assert_called_once_with(["sevctl", "export", str(certificates_expected_dir)], check=True)