From b985821ec1d7acaf7aa4b3be354c04b64a7e39b8 Mon Sep 17 00:00:00 2001 From: Olivier Le Thanh Duong Date: Thu, 13 Jun 2024 09:37:11 +0200 Subject: [PATCH 01/10] Problem: Could not install on Python 12 via pip install -e because of deps problem. Solution : upgrade aiohttp version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 123e07c91..f2c52c87a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ ] dependencies = [ "pydantic[dotenv]~=1.10.13", - "aiohttp==3.8.6", + "aiohttp==3.9.5", "aiodns==3.1.0", "setproctitle==1.3.3", "pyyaml==6.0.1", From 25fabeed0db1a4715b5fa96e610f479917a53e2d Mon Sep 17 00:00:00 2001 From: Olivier Le Thanh Duong Date: Thu, 13 Jun 2024 09:38:24 +0200 Subject: [PATCH 02/10] Problem: Crash in log when VM was printing control char --- src/aleph/vm/hypervisors/qemu/qemuvm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aleph/vm/hypervisors/qemu/qemuvm.py b/src/aleph/vm/hypervisors/qemu/qemuvm.py index 87ba9724e..808b67bea 100644 --- a/src/aleph/vm/hypervisors/qemu/qemuvm.py +++ b/src/aleph/vm/hypervisors/qemu/qemuvm.py @@ -132,7 +132,7 @@ async def _process_stdout(self): return for queue in self.log_queues: await queue.put(("stdout", line)) - print(self, line.decode().strip()) + print(self, line) def _get_qmpclient(self) -> Optional[qmp.QEMUMonitorProtocol]: if not (self.qmp_socket_path and self.qmp_socket_path.exists()): From d9de7ba58e2da82cadce53f0f4e7b0e5d4f7446e Mon Sep 17 00:00:00 2001 From: Olivier Le Thanh Duong Date: Thu, 13 Jun 2024 09:39:51 +0200 Subject: [PATCH 03/10] Raise log level for VM termination in controller so we always display when it finish --- src/aleph/vm/controllers/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/aleph/vm/controllers/__main__.py b/src/aleph/vm/controllers/__main__.py index 39d606784..e3e9ec633 100644 --- a/src/aleph/vm/controllers/__main__.py +++ b/src/aleph/vm/controllers/__main__.py @@ -87,7 +87,7 @@ async def handle_persistent_vm(config: Configuration, execution: Union[MicroVM, execution.start_printing_logs() await process.wait() - logger.info(f"Process terminated with {process.returncode}") + logger.warning(f"Process terminated with {process.returncode}") async def run_persistent_vm(config: Configuration): From 499abf39aba98e3cf3631ba50c726e65c5677f99 Mon Sep 17 00:00:00 2001 From: Olivier Le Thanh Duong Date: Thu, 13 Jun 2024 09:41:04 +0200 Subject: [PATCH 04/10] comment --- src/aleph/vm/orchestrator/views/authentication.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/aleph/vm/orchestrator/views/authentication.py b/src/aleph/vm/orchestrator/views/authentication.py index a50dde45e..2d43c07d3 100644 --- a/src/aleph/vm/orchestrator/views/authentication.py +++ b/src/aleph/vm/orchestrator/views/authentication.py @@ -237,6 +237,7 @@ async def wrapper(request): logging.exception(e) raise + # authenticated_sender is the authenticted wallet address of the requester (as a string) response = await handler(request, authenticated_sender) return response From fab93a748d690c92c5630d3625323355f76102a2 Mon Sep 17 00:00:00 2001 From: Olivier Le Thanh Duong Date: Thu, 13 Jun 2024 09:46:06 +0200 Subject: [PATCH 05/10] Problem: Error were not properly returned in allocation endpoint --- src/aleph/vm/orchestrator/views/operator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/aleph/vm/orchestrator/views/operator.py b/src/aleph/vm/orchestrator/views/operator.py index 399159d00..ab0498176 100644 --- a/src/aleph/vm/orchestrator/views/operator.py +++ b/src/aleph/vm/orchestrator/views/operator.py @@ -10,7 +10,9 @@ from aleph.vm.conf import settings from aleph.vm.models import VmExecution -from aleph.vm.orchestrator.run import create_vm_execution +from aleph.vm.orchestrator.run import ( + create_vm_execution_or_raise_http_error, +) from aleph.vm.orchestrator.views import authenticate_api_request from aleph.vm.orchestrator.views.authentication import ( authenticate_websocket_message, From 23469bb7d99484a0026238e9861ad9c88d2704aa Mon Sep 17 00:00:00 2001 From: Olivier Le Thanh Duong Date: Thu, 13 Jun 2024 09:47:34 +0200 Subject: [PATCH 06/10] Add Qemu confidential controler implementation --- ...fidential_instance_message_from_aleph.json | 51 ++++++ pyproject.toml | 1 + src/aleph/vm/controllers/__main__.py | 6 + src/aleph/vm/controllers/configuration.py | 16 +- src/aleph/vm/controllers/qemu/client.py | 75 +++++++++ src/aleph/vm/controllers/qemu/instance.py | 12 +- .../controllers/qemu_confidential/__init__.py | 0 .../controllers/qemu_confidential/instance.py | 117 ++++++++++++++ .../hypervisors/qemu_confidential/__init__.py | 0 .../hypervisors/qemu_confidential/qemuvm.py | 122 ++++++++++++++ src/aleph/vm/models.py | 5 +- src/aleph/vm/orchestrator/run.py | 31 +--- src/aleph/vm/orchestrator/supervisor.py | 6 + src/aleph/vm/orchestrator/views/operator.py | 149 +++++++++++++++++- 14 files changed, 554 insertions(+), 37 deletions(-) create mode 100644 examples/confidential_instance_message_from_aleph.json create mode 100644 src/aleph/vm/controllers/qemu/client.py create mode 100644 src/aleph/vm/controllers/qemu_confidential/__init__.py create mode 100644 src/aleph/vm/controllers/qemu_confidential/instance.py create mode 100644 src/aleph/vm/hypervisors/qemu_confidential/__init__.py create mode 100644 src/aleph/vm/hypervisors/qemu_confidential/qemuvm.py diff --git a/examples/confidential_instance_message_from_aleph.json b/examples/confidential_instance_message_from_aleph.json new file mode 100644 index 000000000..5ea2e1c00 --- /dev/null +++ b/examples/confidential_instance_message_from_aleph.json @@ -0,0 +1,51 @@ +{ + "chain": "ETH", + "item_hash": "fake-hash-fake-hash-fake-hash-fake-hash-fake-hash-fake-hash-hash", + "sender": "0x9319Ad3B7A8E0eE24f2E639c40D8eD124C5520Ba", + "type": "INSTANCE", + "channel": "Fun-dApps", + "confirmed": true, + "content": { + "address": "0x9319Ad3B7A8E0eE24f2E639c40D8eD124C5520Ba", + "allow_amend": false, + "variables": { + "VM_CUSTOM_NUMBER": "32" + }, + "environment": { + "reproducible": true, + "internet": true, + "aleph_api": true, + "shared_cache": true + }, + "resources": { + "vcpus": 1, + "memory": 512, + "seconds": 30 + }, + "rootfs": { + "parent": { + "ref": "549ec451d9b099cad112d4aaa2c00ac40fb6729a92ff252ff22eef0b5c3cb613", + "use_latest": true + }, + "persistence": "host", + "size_mib": 5000 + }, + "authorized_keys": [ + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDj95BHGUx0/z2G/tTrEi8o49i70xvjcEUdSs3j4A33jE7pAphrfRVbuFMgFubcm8n9r5ftd/H8SjjTL4hY9YvWV5ZuMf92GUga3n4wgevvPlBszYZCy/idxFl0vtHYC1CcK9v4tVb9onhDt8FOJkf2m6PmDyvC+6tl6LwoerXTeeiKr5VnTB4KOBkammtFmix3d1X1SZd/cxdwZIHcQ7BNsqBm2w/YzVba6Z4ZnFUelBkQtMQqNs2aV51O1pFFqtZp2mM71D5d8vn9pOtqJ5QmY5IW6NypcyqKJZg5o6QguK5rdXLkc7AWro27BiaHIENl3w0wazp9EDO9zPAGJ6lz olivier@lanius" + ], + + "time": 1619017773.8950517 + }, + "item_content": "{\"address\":\"0x9319Ad3B7A8E0eE24f2E639c40D8eD124C5520Ba\",\"allow_amend\":false,\"variables\":{\"VM_CUSTOM_NUMBER\":\"32\"},\"environment\":{\"reproducible\":true,\"internet\":true,\"aleph_api\":true,\"shared_cache\":true},\"resources\":{\"vcpus\":1,\"memory\":128,\"seconds\":30},\"rootfs\":{\"parent\":{\"ref\":\"549ec451d9b099cad112d4aaa2c00ac40fb6729a92ff252ff22eef0b5c3cb613\",\"use_latest\":true},\"persistence\":\"host\",\"size_mib\":20000},\"cloud_config\":{\"password\":\"password\",\"chpasswd\":{\"expire\":\"False\"}},\"volumes\":[{\"mount\":\"/opt/venv\",\"ref\":\"5f31b0706f59404fad3d0bff97ef89ddf24da4761608ea0646329362c662ba51\",\"use_latest\":false},{\"comment\":\"Working data persisted on the VM supervisor, not available on other nodes\",\"mount\":\"/var/lib/example\",\"name\":\"data\",\"persistence\":\"host\",\"size_mib\":5}],\"replaces\":\"0x9319Ad3B7A8E0eE24f2E639c40D8eD124C5520Ba\",\"time\":1619017773.8950517}", + "item_type": "inline", + "signature": "0x372da8230552b8c3e65c05b31a0ff3a24666d66c575f8e11019f62579bf48c2b7fe2f0bbe907a2a5bf8050989cdaf8a59ff8a1cbcafcdef0656c54279b4aa0c71b", + "size": 749, + "time": 1619017773.8950577, + "confirmations": [ + { + "chain": "ETH", + "height": 12284734, + "hash": "0x67f2f3cde5e94e70615c92629c70d22dc959a118f46e9411b29659c2fce87cdc" + } + ] +} diff --git a/pyproject.toml b/pyproject.toml index f2c52c87a..8a771f0d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,7 @@ dependencies = [ "aiohttp_cors~=0.7.0", "pyroute2==0.7.12", "jwcrypto==1.5.6", + "python-cpuid==0.1.0" ] [project.urls] diff --git a/src/aleph/vm/controllers/__main__.py b/src/aleph/vm/controllers/__main__.py index e3e9ec633..386b3d7ba 100644 --- a/src/aleph/vm/controllers/__main__.py +++ b/src/aleph/vm/controllers/__main__.py @@ -10,11 +10,13 @@ from aleph.vm.hypervisors.firecracker.microvm import MicroVM from aleph.vm.hypervisors.qemu.qemuvm import QemuVM +from aleph.vm.hypervisors.qemu_confidential.qemuvm import QemuConfidentialVM from aleph.vm.network.hostnetwork import Network, make_ipv6_allocator from .configuration import ( Configuration, HypervisorType, + QemuConfidentialVMConfiguration, QemuVMConfiguration, VMConfiguration, ) @@ -70,6 +72,10 @@ async def execute_persistent_vm(config: Configuration): execution.prepare_start() process = await execution.start(config.vm_configuration.config_file_path) + elif isinstance(config.vm_configuration, QemuConfidentialVMConfiguration): # FIXME + assert isinstance(config.vm_configuration, QemuConfidentialVMConfiguration) + execution = QemuConfidentialVM(config.vm_configuration) + process = await execution.start() else: assert isinstance(config.vm_configuration, QemuVMConfiguration) execution = QemuVM(config.vm_configuration) diff --git a/src/aleph/vm/controllers/configuration.py b/src/aleph/vm/controllers/configuration.py index be8d1986c..9405f8f3b 100644 --- a/src/aleph/vm/controllers/configuration.py +++ b/src/aleph/vm/controllers/configuration.py @@ -29,6 +29,20 @@ class QemuVMConfiguration(BaseModel): interface_name: Optional[str] +class QemuConfidentialVMConfiguration(BaseModel): + qemu_bin_path: str + cloud_init_drive_path: Optional[str] + image_path: str + monitor_socket_path: Path + qmp_socket_path: Path + vcpu_count: int + mem_size_mb: int + interface_name: Optional[str] + ovmf_path: Path + sev_session_file: Path + sev_dh_cert_file: Path + + class HypervisorType(str, Enum): qemu = "qemu" firecracker = "firecracker" @@ -37,7 +51,7 @@ class HypervisorType(str, Enum): class Configuration(BaseModel): vm_id: int settings: Settings - vm_configuration: Union[QemuVMConfiguration, VMConfiguration] + vm_configuration: Union[QemuConfidentialVMConfiguration, QemuVMConfiguration, VMConfiguration] hypervisor: HypervisorType = HypervisorType.firecracker diff --git a/src/aleph/vm/controllers/qemu/client.py b/src/aleph/vm/controllers/qemu/client.py new file mode 100644 index 000000000..2b7870994 --- /dev/null +++ b/src/aleph/vm/controllers/qemu/client.py @@ -0,0 +1,75 @@ +import qmp +from pydantic import BaseModel + + +class VmSevInfo(BaseModel): + enabled: bool + api_major: int + api_minor: int + build_id: int + policy: int + state: str + handle: int + + +class QemuVmClient: + def __init__(self, vm): + self.vm = vm + if not (vm.qmp_socket_path and vm.qmp_socket_path.exists()): + raise Exception + client = qmp.QEMUMonitorProtocol(str(vm.qmp_socket_path)) + client.connect() + + # qmp_client = qmp.QEMUMonitorProtocol(address=("localhost", vm.qmp_port)) + self.qmp_client = client + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + def close(self) -> None: + self.qmp_client.close() + + def query_sev_info(self) -> VmSevInfo: + caps = self.qmp_client.command("query-sev") + return VmSevInfo( + enabled=caps["enabled"], + api_major=caps["api-major"], + api_minor=caps["api-minor"], + handle=caps["handle"], + state=caps["state"], + build_id=caps["build-id"], + policy=caps["policy"], + ) + + def query_launch_measure(self) -> str: + measure = self.qmp_client.command("query-sev-launch-measure") + return measure["data"] + + def inject_secret(self, packet_header: str, secret: str) -> None: + """ + Injects the secret in the SEV secret area. + + :param packet_header: The packet header, as a base64 string. + :param secret: The encoded secret, as a base64 string. + """ + + self.qmp_client.command( + "sev-inject-launch-secret", + **{"packet-header": packet_header, "secret": secret}, + ) + + def continue_execution(self) -> None: + """ + Resumes the execution of the VM. + """ + self.qmp_client.command("cont") + + def query_status(self) -> None: + """ + Get running status. + """ + # {'status': 'prelaunch', 'singlestep': False, 'running': False} + return self.qmp_client.command("query-status") diff --git a/src/aleph/vm/controllers/qemu/instance.py b/src/aleph/vm/controllers/qemu/instance.py index 7d6128e20..f22e9c9d7 100644 --- a/src/aleph/vm/controllers/qemu/instance.py +++ b/src/aleph/vm/controllers/qemu/instance.py @@ -149,7 +149,6 @@ class AlephQemuInstance(Generic[ConfigurationType], CloudInitMixin, AlephVmContr is_instance: bool qemu_process: Optional[Process] support_snapshot = False - qmp_socket_path = None persistent = True _queue_cancellers: dict[asyncio.Queue, Callable] = {} controller_configuration: Configuration @@ -219,7 +218,7 @@ async def configure(self): logger.debug(f"Making Qemu configuration: {self} ") monitor_socket_path = settings.EXECUTION_ROOT / (str(self.vm_id) + "-monitor.socket") - self.qmp_socket_path = qmp_socket_path = settings.EXECUTION_ROOT / (str(self.vm_id) + "-qmp.socket") + cloud_init_drive = await self._create_cloud_init_drive() image_path = str(self.resources.rootfs_path) @@ -237,7 +236,7 @@ async def configure(self): cloud_init_drive_path=cloud_init_drive_path, image_path=image_path, monitor_socket_path=monitor_socket_path, - qmp_socket_path=qmp_socket_path, + qmp_socket_path=self.qmp_socket_path, vcpu_count=vcpu_count, mem_size_mb=mem_size_mb, interface_name=interface_name, @@ -246,7 +245,7 @@ async def configure(self): configuration = Configuration( vm_id=self.vm_id, settings=settings, vm_configuration=vm_configuration, hypervisor=HypervisorType.qemu ) - + logger.debug(configuration) save_controller_configuration(self.vm_hash, configuration) def save_controller_configuration(self): @@ -260,6 +259,10 @@ def save_controller_configuration(self): def _journal_stdout_name(self) -> str: return f"vm-{self.vm_hash}-stdout" + @property + def qmp_socket_path(self) -> Path: + return settings.EXECUTION_ROOT / f"{self.vm_id}-qmp.socket" + @property def _journal_stderr_name(self) -> str: return f"vm-{self.vm_hash}-stderr" @@ -276,7 +279,6 @@ async def wait_for_init(self) -> None: if not ip: msg = "Host IP not available" raise ValueError(msg) - ip = ip.split("/", 1)[0] attempts = 30 diff --git a/src/aleph/vm/controllers/qemu_confidential/__init__.py b/src/aleph/vm/controllers/qemu_confidential/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/aleph/vm/controllers/qemu_confidential/instance.py b/src/aleph/vm/controllers/qemu_confidential/instance.py new file mode 100644 index 000000000..61fb85a2f --- /dev/null +++ b/src/aleph/vm/controllers/qemu_confidential/instance.py @@ -0,0 +1,117 @@ +import asyncio +import logging +import shutil +from asyncio.subprocess import Process +from typing import Callable, Optional + +from aleph_message.models import ItemHash +from aleph_message.models.execution.environment import MachineResources + +from aleph.vm.conf import settings +from aleph.vm.controllers.configuration import ( + Configuration, + HypervisorType, + QemuConfidentialVMConfiguration, + save_controller_configuration, +) +from aleph.vm.controllers.qemu import AlephQemuInstance +from aleph.vm.controllers.qemu.instance import ( + AlephQemuResources, + ConfigurationType, + logger, +) +from aleph.vm.network.interfaces import TapInterface + +logger = logging.getLogger(__name__) + + +class AlephQemuConfidentialResources(AlephQemuResources): + pass + + +class AlephQemuConfidentialInstance(AlephQemuInstance): + vm_id: int + vm_hash: ItemHash + resources: AlephQemuResources + enable_console: bool + enable_networking: bool + hardware_resources: MachineResources + tap_interface: Optional[TapInterface] = None + vm_configuration: Optional[ConfigurationType] + is_instance: bool + qemu_process: Optional[Process] + support_snapshot = False + persistent = True + _queue_cancellers: dict[asyncio.Queue, Callable] = {} + controller_configuration: Configuration + + def __repr__(self): + return f"" + + def __str__(self): + return f"vm-{self.vm_id}" + + def __init__( + self, + vm_id: int, + vm_hash: ItemHash, + resources: AlephQemuResources, + enable_networking: bool = False, + enable_console: Optional[bool] = None, + hardware_resources: MachineResources = MachineResources(), + tap_interface: Optional[TapInterface] = None, + ): + super().__init__( + vm_id, vm_hash, resources, enable_networking, enable_console, hardware_resources, tap_interface + ) + + async def setup(self): + pass + + async def configure(self): + """Configure the VM by saving controller service configuration""" + + logger.debug(f"Making Qemu configuration: {self} ") + monitor_socket_path = settings.EXECUTION_ROOT / (str(self.vm_id) + "-monitor.socket") + + cloud_init_drive = await self._create_cloud_init_drive() + + image_path = str(self.resources.rootfs_path) + vcpu_count = self.hardware_resources.vcpus + mem_size_mib = self.hardware_resources.memory + mem_size_mb = str(int(mem_size_mib / 1024 / 1024 * 1000 * 1000)) + + vm_session_path = settings.CONFIDENTIAL_SESSION_DIRECTORY / self.vm_hash + session_file_path = vm_session_path / "vm_session.b64" + godh_file_path = vm_session_path / "vm_godh.b64" + + qemu_bin_path = shutil.which("qemu-system-x86_64") + interface_name = None + if self.tap_interface: + interface_name = self.tap_interface.device_name + cloud_init_drive_path = str(cloud_init_drive.path_on_host) if cloud_init_drive else None + vm_configuration = QemuConfidentialVMConfiguration( + qemu_bin_path=qemu_bin_path, + cloud_init_drive_path=cloud_init_drive_path, + image_path=image_path, + monitor_socket_path=monitor_socket_path, + qmp_socket_path=self.qmp_socket_path, + vcpu_count=vcpu_count, + mem_size_mb=mem_size_mb, + interface_name=interface_name, + ovmf_path="/home/olivier/custom-OVMF.fd", + sev_session_file=session_file_path, + sev_dh_cert_file=godh_file_path, + ) + + configuration = Configuration( + vm_id=self.vm_id, settings=settings, vm_configuration=vm_configuration, hypervisor=HypervisorType.qemu + ) + logger.debug(configuration) + + save_controller_configuration(self.vm_hash, configuration) + + async def wait_for_init(self) -> None: + """Wait for the init process of the instance to be ready.""" + # FIXME: Cannot ping since network is not set up yet. + return diff --git a/src/aleph/vm/hypervisors/qemu_confidential/__init__.py b/src/aleph/vm/hypervisors/qemu_confidential/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/aleph/vm/hypervisors/qemu_confidential/qemuvm.py b/src/aleph/vm/hypervisors/qemu_confidential/qemuvm.py new file mode 100644 index 000000000..15bc95b45 --- /dev/null +++ b/src/aleph/vm/hypervisors/qemu_confidential/qemuvm.py @@ -0,0 +1,122 @@ +import asyncio +from asyncio.subprocess import Process +from pathlib import Path + +from cpuid.features import secure_encryption_info + +from aleph.vm.controllers.configuration import QemuConfidentialVMConfiguration +from aleph.vm.controllers.qemu.instance import logger +from aleph.vm.hypervisors.qemu.qemuvm import QemuVM + + +class QemuConfidentialVM(QemuVM): + + sev_policy: str = "0x1" # FIXME have it passed from guest + sev_dh_cert_file: Path # "vm_godh.b64" + sev_session_file: Path # "vm_session.b64" + + def __repr__(self) -> str: + if self.qemu_process: + return f"" + else: + return "" + + def __init__(self, config: QemuConfidentialVMConfiguration): + super().__init__(config) + self.qemu_bin_path = config.qemu_bin_path + self.cloud_init_drive_path = config.cloud_init_drive_path + self.image_path = config.image_path + self.monitor_socket_path = config.monitor_socket_path + self.qmp_socket_path = config.qmp_socket_path + self.vcpu_count = config.vcpu_count + self.mem_size_mb = config.mem_size_mb + self.interface_name = config.interface_name + self.log_queues: list[asyncio.Queue] = [] + self.ovmf_path: Path = config.ovmf_path + self.sev_session_file = config.sev_session_file + self.sev_dh_cert_file = config.sev_dh_cert_file + + def prepare_start(self): + pass + + async def start( + self, + ) -> Process: + # Based on the command + # qemu-system-x86_64 -enable-kvm -m 2048 -net nic,model=virtio + # -net tap,ifname=tap0,script=no,downscript=no -drive file=alpine.qcow2,media=disk,if=virtio -nographic + # hardware_resources.published ports -> not implemented at the moment + # hardware_resources.seconds -> only for microvm + + # TODO : ensure this is ok at launch + sev_info = secure_encryption_info() + if sev_info is None: + raise ValueError("Not running on an AMD SEV platform?") + godh = self.sev_dh_cert_file + launch_blob = self.sev_session_file + + if not (godh.is_file() and launch_blob.is_file()): + raise FileNotFoundError("Missing guest owner certificates, cannot start the VM.`") + args = [ + self.qemu_bin_path, + "-enable-kvm", + "-nodefaults", + "-m", + str(self.mem_size_mb), + "-smp", + str(self.vcpu_count), + "-drive", + f"if=pflash,format=raw,unit=0,file={self.ovmf_path},readonly=on", + "-drive", + f"file={self.image_path},media=disk,if=virtio,format=qcow2", + # To debug you can pass gtk or curses instead + "-display", + "none", + "--no-reboot", # Rebooting from inside the VM shuts down the machine + # Listen for commands on this socket + "-monitor", + f"unix:{self.monitor_socket_path},server,nowait", + # Listen for commands on this socket (QMP protocol in json). Supervisor use it to send shutdown or start + # command + "-qmp", + f"unix:{self.qmp_socket_path},server,nowait", + # Tell to put the output to std fd, so we can include them in the log + "-nographic", + "-serial", + "stdio", + "--no-reboot", # Rebooting from inside the VM shuts down the machine + "-S", + # Confidential options + "-object", + f"sev-guest,id=sev0,policy={self.sev_policy},cbitpos={sev_info.c_bit_position}," + f"reduced-phys-bits={sev_info.phys_addr_reduction}," + f"dh-cert-file={godh},session-file={launch_blob}", + "-machine", + "confidential-guest-support=sev0", + # Linux kernel 6.9 added a control on the RDRAND function to ensure that the random numbers generation + # works well, on Qemu emulation for confidential computing the CPU model us faked and this makes control + # raise an error and prevent boot. Passing the argument --cpu host instruct the VM to use the same CPU + # model than the host thus the VM's kernel knows which method is used to get random numbers (Intel and + # AMD have different methods) and properly boot. + "-cpu", + "host", + # Uncomment following for debug + # "-serial", "telnet:localhost:4321,server,nowait", + # "-snapshot", # Do not save anything to disk + ] + if self.interface_name: + # script=no, downscript=no tell qemu not to try to set up the network itself + args += ["-net", "nic,model=virtio", "-net", f"tap,ifname={self.interface_name},script=no,downscript=no"] + + if self.cloud_init_drive_path: + args += ["-cdrom", f"{self.cloud_init_drive_path}"] + print(*args) + self.qemu_process = proc = await asyncio.create_subprocess_exec( + *args, + stdin=asyncio.subprocess.DEVNULL, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + + logger.debug(f"started QemuConfidentialVM vm {self}, {proc}") + return proc diff --git a/src/aleph/vm/models.py b/src/aleph/vm/models.py index 3dfa738db..89f34cd53 100644 --- a/src/aleph/vm/models.py +++ b/src/aleph/vm/models.py @@ -26,6 +26,9 @@ from aleph.vm.controllers.firecracker.snapshot_manager import SnapshotManager from aleph.vm.controllers.interface import AlephVmControllerInterface from aleph.vm.controllers.qemu.instance import AlephQemuInstance, AlephQemuResources +from aleph.vm.controllers.qemu_confidential.instance import ( + AlephQemuConfidentialInstance, +) from aleph.vm.network.interfaces import TapInterface from aleph.vm.orchestrator.metrics import ( ExecutionRecord, @@ -236,7 +239,7 @@ def create( ) elif self.hypervisor == HypervisorType.qemu: assert isinstance(self.resources, AlephQemuResources) - self.vm = vm = AlephQemuInstance( + self.vm = vm = AlephQemuConfidentialInstance( vm_id=vm_id, vm_hash=self.vm_hash, resources=self.resources, diff --git a/src/aleph/vm/orchestrator/run.py b/src/aleph/vm/orchestrator/run.py index 8dec7e963..7f7be1812 100644 --- a/src/aleph/vm/orchestrator/run.py +++ b/src/aleph/vm/orchestrator/run.py @@ -57,31 +57,12 @@ async def create_vm_execution(vm_hash: ItemHash, pool: VmPool, persistent: bool logger.debug(f"Message: {message.json(indent=4, sort_keys=True, exclude_none=True)}") - try: - execution = await pool.create_a_vm( - vm_hash=vm_hash, - message=message.content, - original=original_message.content, - persistent=persistent, - ) - except ResourceDownloadError as error: - logger.exception(error) - pool.forget_vm(vm_hash=vm_hash) - raise HTTPBadRequest(reason="Code, runtime or data not available") from error - except FileTooLargeError as error: - raise HTTPInternalServerError(reason=error.args[0]) from error - except VmSetupError as error: - logger.exception(error) - pool.forget_vm(vm_hash=vm_hash) - raise HTTPInternalServerError(reason="Error during vm initialisation") from error - except MicroVMFailedInitError as error: - logger.exception(error) - pool.forget_vm(vm_hash=vm_hash) - raise HTTPInternalServerError(reason="Error during runtime initialisation") from error - except HostNotFoundError as error: - logger.exception(error) - pool.forget_vm(vm_hash=vm_hash) - raise HTTPInternalServerError(reason="Host did not respond to ping") from error + execution = await pool.create_a_vm( + vm_hash=vm_hash, + message=message.content, + original=original_message.content, + persistent=persistent, + ) return execution diff --git a/src/aleph/vm/orchestrator/supervisor.py b/src/aleph/vm/orchestrator/supervisor.py index 2786c6c4b..c909c793f 100644 --- a/src/aleph/vm/orchestrator/supervisor.py +++ b/src/aleph/vm/orchestrator/supervisor.py @@ -47,10 +47,13 @@ update_allocations, ) from .views.operator import ( + get_sev_certificate, operate_confidential_initialize, operate_erase, operate_expire, operate_reboot, + operate_sev_inject_secret, + operate_sev_measurement, operate_stop, stream_logs, ) @@ -107,6 +110,9 @@ def setup_webapp(): web.post("/control/machine/{ref}/stop", operate_stop), web.post("/control/machine/{ref}/erase", operate_erase), web.post("/control/machine/{ref}/reboot", operate_reboot), + web.get("/control/machine/{ref}/sev/measurement", operate_sev_measurement), + web.post("/control/machine/{ref}/sev/inject_disk_secret", operate_sev_inject_secret), + web.get("/about/sev/certificate", get_sev_certificate), # no auth # /status APIs are used to check that the VM Orchestrator is running properly web.get("/status/check/fastapi", status_check_fastapi), web.get("/status/check/fastapi/legacy", status_check_fastapi_legacy), diff --git a/src/aleph/vm/orchestrator/views/operator.py b/src/aleph/vm/orchestrator/views/operator.py index ab0498176..d24bbe392 100644 --- a/src/aleph/vm/orchestrator/views/operator.py +++ b/src/aleph/vm/orchestrator/views/operator.py @@ -1,25 +1,27 @@ +import json import logging from datetime import timedelta import aiohttp.web_exceptions +import pydantic from aiohttp import web from aiohttp.web_urldispatcher import UrlMappingMatchInfo from aleph_message.exceptions import UnknownHashError from aleph_message.models import ItemHash from aleph_message.models.execution import BaseExecutableContent +from pydantic import BaseModel from aleph.vm.conf import settings +from aleph.vm.controllers.qemu.client import QemuVmClient from aleph.vm.models import VmExecution -from aleph.vm.orchestrator.run import ( - create_vm_execution_or_raise_http_error, -) +from aleph.vm.orchestrator.run import create_vm_execution_or_raise_http_error from aleph.vm.orchestrator.views import authenticate_api_request from aleph.vm.orchestrator.views.authentication import ( authenticate_websocket_message, require_jwk_authentication, ) from aleph.vm.pool import VmPool -from aleph.vm.utils import cors_allow_all +from aleph.vm.utils import cors_allow_all, dumps_for_json logger = logging.getLogger(__name__) @@ -136,6 +138,50 @@ async def operate_expire(request: web.Request, authenticated_sender: str) -> web return web.Response(status=200, body=f"Expiring VM with ref {vm_hash} in {timeout} seconds") +@cors_allow_all +@require_jwk_authentication +async def operate_start(request: web.Request, authenticated_sender: str) -> web.Response: + """Start the confidential virtual machine if possible.""" + # TODO: Add user authentication + vm_hash = get_itemhash_or_400(request.match_info) + + pool: VmPool = request.app["vm_pool"] + logger.debug(f"Iterating through running executions... {pool.executions}") + execution = get_execution_or_404(vm_hash, pool=pool) + + if not is_sender_authorized(authenticated_sender, execution.message): + return web.Response(status=403, body="Unauthorized sender") + + if execution.is_running: + return web.Response(status=403, body=f"VM with ref {vm_hash} already running") + + if not execution.is_confidential: + return web.Response(status=403, body=f"Operation not allowed for VM {vm_hash} because it isn't confidential") + + post = await request.post() + + vm_session_path = settings.CONFIDENTIAL_SESSION_DIRECTORY / vm_hash + vm_session_path.mkdir(exist_ok=True) + + session_file_content = post.get("session") + if not session_file_content: + return web.Response(status=403, body=f"Session file required for VM with ref {vm_hash}") + + session_file_path = vm_session_path / "vm_session.b64" + session_file_path.write_bytes(session_file_content.file.read()) + + godh_file_content = post.get("godh") + if not godh_file_content: + return web.Response(status=403, body=f"GODH file required for VM with ref {vm_hash}") + + godh_file_path = vm_session_path / "vm_godh.b64" + godh_file_path.write_bytes(godh_file_content.file.read()) + + pool.systemd_manager.enable_and_start(execution.controller_service) + + return web.Response(status=200, body=f"Started VM with ref {vm_hash}") + + @cors_allow_all @require_jwk_authentication async def operate_confidential_initialize(request: web.Request, authenticated_sender: str) -> web.Response: @@ -226,12 +272,105 @@ async def operate_reboot(request: web.Request, authenticated_sender: str) -> web await pool.stop_vm(vm_hash) pool.forget_vm(vm_hash) - await create_vm_execution(vm_hash=vm_hash, pool=pool) + await create_vm_execution_or_raise_http_error(vm_hash=vm_hash, pool=pool) return web.Response(status=200, body=f"Rebooted VM with ref {vm_hash}") else: return web.Response(status=200, body="Starting VM (was not running) with ref {vm_hash}") +@cors_allow_all +@require_jwk_authentication +async def operate_sev_measurement(request: web.Request, authenticated_sender) -> web.Response: + """ + Fetch the sev measurement for the VM + """ + vm_hash = get_itemhash_or_400(request.match_info) + pool: VmPool = request.app["vm_pool"] + execution = get_execution_or_404(vm_hash, pool=pool) + + if not is_sender_authorized(authenticated_sender, execution.message): + return web.Response(status=403, body="Unauthorized sender") + + if not execution.is_running: + raise web.HTTPForbidden(body="Operation not running") + vm_client = QemuVmClient(execution.vm) + vm_sev_info = vm_client.query_sev_info() + launch_measure = vm_client.query_launch_measure() + + return web.json_response( + data={"sev_info": vm_sev_info, "launch_measure": launch_measure}, + status=200, + dumps=dumps_for_json, + ) + + +class InjectSecretParams(BaseModel): + """ + packet_header: as base64 string + secret : encrypted secret table as base64 string + """ + + packet_header: str + secret: str + + +@cors_allow_all +@require_jwk_authentication +async def operate_sev_inject_secret(request: web.Request, authenticated_sender) -> web.Response: + """ + Send secret to the VM and start it + """ + try: + data = await request.json() + params = InjectSecretParams.parse_obj(data) + except json.JSONDecodeError: + return web.HTTPBadRequest(reason="Body is not valid JSON") + except pydantic.ValidationError as error: + return web.json_response(data=error.json(), status=web.HTTPBadRequest.status_code) + + vm_hash = get_itemhash_or_400(request.match_info) + pool: VmPool = request.app["vm_pool"] + execution = get_execution_or_404(vm_hash, pool=pool) + if not is_sender_authorized(authenticated_sender, execution.message): + return web.Response(status=403, body="Unauthorized sender") + + # if not execution.is_running: + # raise web.HTTPForbidden(body="Operation not running") + vm_client = QemuVmClient(execution.vm) + vm_client.inject_secret(params.packet_header, params.secret) + vm_client.continue_execution() + + status = vm_client.query_status() + print(status["status"] != "running") + + return web.json_response( + data={"status": status}, + status=200, + dumps=dumps_for_json, + ) + + +@cors_allow_all +async def get_sev_certificate(request: web.Request) -> web.Response: + """ + Download the platform certificate as base64 encoded string. + """ + sevctl_bin = "/home/olivier/.cargo/bin/sevctl" + cmd = f"sudo {sevctl_bin} export /dev/stdout" + process = await asyncio.create_subprocess_shell( + cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.DEVNULL + ) + stdout, stderr = await process.communicate() + b64_certificate = base64.b64encode(stdout) + if process.returncode != 0: + logger.warning( + f"Could not extract certificates: {stderr.decode()}", + ) + raise web.HTTPInternalServerError(reason="Impossible to extract the certificate, check with the platform owner") + + return web.Response(status=200, body=b64_certificate) + + @cors_allow_all @require_jwk_authentication async def operate_erase(request: web.Request, authenticated_sender: str) -> web.Response: From e849af7845f1300ce9982104000d12eefd7ded93 Mon Sep 17 00:00:00 2001 From: Olivier Le Thanh Duong Date: Thu, 13 Jun 2024 09:51:30 +0200 Subject: [PATCH 07/10] remove duplicate endpoint --- src/aleph/vm/orchestrator/supervisor.py | 2 - src/aleph/vm/orchestrator/views/operator.py | 65 --------------------- 2 files changed, 67 deletions(-) diff --git a/src/aleph/vm/orchestrator/supervisor.py b/src/aleph/vm/orchestrator/supervisor.py index c909c793f..728684081 100644 --- a/src/aleph/vm/orchestrator/supervisor.py +++ b/src/aleph/vm/orchestrator/supervisor.py @@ -47,7 +47,6 @@ update_allocations, ) from .views.operator import ( - get_sev_certificate, operate_confidential_initialize, operate_erase, operate_expire, @@ -112,7 +111,6 @@ def setup_webapp(): web.post("/control/machine/{ref}/reboot", operate_reboot), web.get("/control/machine/{ref}/sev/measurement", operate_sev_measurement), web.post("/control/machine/{ref}/sev/inject_disk_secret", operate_sev_inject_secret), - web.get("/about/sev/certificate", get_sev_certificate), # no auth # /status APIs are used to check that the VM Orchestrator is running properly web.get("/status/check/fastapi", status_check_fastapi), web.get("/status/check/fastapi/legacy", status_check_fastapi_legacy), diff --git a/src/aleph/vm/orchestrator/views/operator.py b/src/aleph/vm/orchestrator/views/operator.py index d24bbe392..531637900 100644 --- a/src/aleph/vm/orchestrator/views/operator.py +++ b/src/aleph/vm/orchestrator/views/operator.py @@ -138,50 +138,6 @@ async def operate_expire(request: web.Request, authenticated_sender: str) -> web return web.Response(status=200, body=f"Expiring VM with ref {vm_hash} in {timeout} seconds") -@cors_allow_all -@require_jwk_authentication -async def operate_start(request: web.Request, authenticated_sender: str) -> web.Response: - """Start the confidential virtual machine if possible.""" - # TODO: Add user authentication - vm_hash = get_itemhash_or_400(request.match_info) - - pool: VmPool = request.app["vm_pool"] - logger.debug(f"Iterating through running executions... {pool.executions}") - execution = get_execution_or_404(vm_hash, pool=pool) - - if not is_sender_authorized(authenticated_sender, execution.message): - return web.Response(status=403, body="Unauthorized sender") - - if execution.is_running: - return web.Response(status=403, body=f"VM with ref {vm_hash} already running") - - if not execution.is_confidential: - return web.Response(status=403, body=f"Operation not allowed for VM {vm_hash} because it isn't confidential") - - post = await request.post() - - vm_session_path = settings.CONFIDENTIAL_SESSION_DIRECTORY / vm_hash - vm_session_path.mkdir(exist_ok=True) - - session_file_content = post.get("session") - if not session_file_content: - return web.Response(status=403, body=f"Session file required for VM with ref {vm_hash}") - - session_file_path = vm_session_path / "vm_session.b64" - session_file_path.write_bytes(session_file_content.file.read()) - - godh_file_content = post.get("godh") - if not godh_file_content: - return web.Response(status=403, body=f"GODH file required for VM with ref {vm_hash}") - - godh_file_path = vm_session_path / "vm_godh.b64" - godh_file_path.write_bytes(godh_file_content.file.read()) - - pool.systemd_manager.enable_and_start(execution.controller_service) - - return web.Response(status=200, body=f"Started VM with ref {vm_hash}") - - @cors_allow_all @require_jwk_authentication async def operate_confidential_initialize(request: web.Request, authenticated_sender: str) -> web.Response: @@ -350,27 +306,6 @@ async def operate_sev_inject_secret(request: web.Request, authenticated_sender) ) -@cors_allow_all -async def get_sev_certificate(request: web.Request) -> web.Response: - """ - Download the platform certificate as base64 encoded string. - """ - sevctl_bin = "/home/olivier/.cargo/bin/sevctl" - cmd = f"sudo {sevctl_bin} export /dev/stdout" - process = await asyncio.create_subprocess_shell( - cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.DEVNULL - ) - stdout, stderr = await process.communicate() - b64_certificate = base64.b64encode(stdout) - if process.returncode != 0: - logger.warning( - f"Could not extract certificates: {stderr.decode()}", - ) - raise web.HTTPInternalServerError(reason="Impossible to extract the certificate, check with the platform owner") - - return web.Response(status=200, body=b64_certificate) - - @cors_allow_all @require_jwk_authentication async def operate_erase(request: web.Request, authenticated_sender: str) -> web.Response: From c4484da51f8c1d425175fa2936fbbb722daceade Mon Sep 17 00:00:00 2001 From: Olivier Le Thanh Duong Date: Thu, 13 Jun 2024 13:39:54 +0200 Subject: [PATCH 08/10] fix test in test_about_certificates --- 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 52426d48c..5b3007c78 100644 --- a/tests/supervisor/test_views.py +++ b/tests/supervisor/test_views.py @@ -168,6 +168,6 @@ async def test_about_certificates(aiohttp_client): 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()]) + is_file_mock.assert_has_calls([call()]) certificates_expected_dir = sev_client.certificates_archive export_mock.assert_called_once_with(["sevctl", "export", str(certificates_expected_dir)], check=True) From 21a357ca2925f8a7507d12a22a1e77d1f04bf6b7 Mon Sep 17 00:00:00 2001 From: Olivier Le Thanh Duong Date: Fri, 14 Jun 2024 10:11:20 +0200 Subject: [PATCH 09/10] Add TODO comment Co-authored-by: nesitor --- src/aleph/vm/controllers/qemu_confidential/instance.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/aleph/vm/controllers/qemu_confidential/instance.py b/src/aleph/vm/controllers/qemu_confidential/instance.py index 61fb85a2f..765558c16 100644 --- a/src/aleph/vm/controllers/qemu_confidential/instance.py +++ b/src/aleph/vm/controllers/qemu_confidential/instance.py @@ -26,6 +26,7 @@ class AlephQemuConfidentialResources(AlephQemuResources): + # TODO: Implement download of the custom OVMF bootloader to use if specified, if not only use the default one. pass From 3bc1969df0a29178c157af553c5c72a92e3963a7 Mon Sep 17 00:00:00 2001 From: Olivier Le Thanh Duong Date: Fri, 14 Jun 2024 10:07:53 +0200 Subject: [PATCH 10/10] Only run Confidentifial if is_confidential --- src/aleph/vm/models.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/aleph/vm/models.py b/src/aleph/vm/models.py index 89f34cd53..b5ad64f75 100644 --- a/src/aleph/vm/models.py +++ b/src/aleph/vm/models.py @@ -28,6 +28,7 @@ from aleph.vm.controllers.qemu.instance import AlephQemuInstance, AlephQemuResources from aleph.vm.controllers.qemu_confidential.instance import ( AlephQemuConfidentialInstance, + AlephQemuConfidentialResources, ) from aleph.vm.network.interfaces import TapInterface from aleph.vm.orchestrator.metrics import ( @@ -238,15 +239,26 @@ def create( prepare_jailer=prepare, ) elif self.hypervisor == HypervisorType.qemu: - assert isinstance(self.resources, AlephQemuResources) - self.vm = vm = AlephQemuConfidentialInstance( - vm_id=vm_id, - vm_hash=self.vm_hash, - resources=self.resources, - enable_networking=self.message.environment.internet, - hardware_resources=self.message.resources, - tap_interface=tap_interface, - ) + if self.is_confidential: + assert isinstance(self.resources, AlephQemuConfidentialResources) + self.vm = vm = AlephQemuConfidentialInstance( + vm_id=vm_id, + vm_hash=self.vm_hash, + resources=self.resources, + enable_networking=self.message.environment.internet, + hardware_resources=self.message.resources, + tap_interface=tap_interface, + ) + else: + assert isinstance(self.resources, AlephQemuResources) + self.vm = vm = AlephQemuInstance( + vm_id=vm_id, + vm_hash=self.vm_hash, + resources=self.resources, + enable_networking=self.message.environment.internet, + hardware_resources=self.message.resources, + tap_interface=tap_interface, + ) else: raise Exception("Unknown VM") else: