From 8dd12faeb2d1a7d8648c34dfbd7f4f57a1eeb4ac Mon Sep 17 00:00:00 2001
From: Carlos Segarra
Date: Fri, 21 Nov 2025 15:41:58 +0000
Subject: [PATCH 01/52] [accless] E: Unify Headers Under Shared Dir
---
accless/CMakeLists.txt | 32 ++++++++++++++++-------
accless/src/accless.cpp | 14 +++++-----
accless/src/dag.cpp | 2 +-
accless/src/utils.cpp | 2 +-
applications/CMakeLists.txt | 13 +--------
applications/test/CMakeLists.txt | 2 +-
applications/test/att-client-sgx/main.cpp | 6 ++---
applications/test/att-client-snp/main.cpp | 6 ++---
8 files changed, 40 insertions(+), 37 deletions(-)
diff --git a/accless/CMakeLists.txt b/accless/CMakeLists.txt
index db8914c..cc99387 100644
--- a/accless/CMakeLists.txt
+++ b/accless/CMakeLists.txt
@@ -65,12 +65,24 @@ add_subdirectory(./libs/jwt/cpp-bindings)
add_subdirectory(./libs/abe4/cpp-bindings)
add_subdirectory(./libs/base64)
-set(ACCLESS_COMMON_HEADERS
- ${ACCLESS_ROOT}/include
- ${ACCLESS_ROOT}/libs/jwt/cpp-bindings
- ${ACCLESS_ROOT}/libs/abe4/cpp-bindings
- ${ACCLESS_ROOT}/libs/base64
-)
+# Create a directory for namespaced headers. This allows including accless
+# headers with a namespace prefix, e.g. #include "accless/attestation/attestation.h"
+set(ACCLESS_INCLUDE_PREFIX_DIR ${CMAKE_CURRENT_BINARY_DIR}/include_prefix)
+set(ACCLESS_INCLUDE_PREFIX_DIR ${ACCLESS_INCLUDE_PREFIX_DIR} PARENT_SCOPE)
+if (EXISTS ${ACCLESS_INCLUDE_PREFIX_DIR})
+ file(REMOVE_RECURSE ${ACCLESS_INCLUDE_PREFIX_DIR})
+endif()
+file(MAKE_DIRECTORY ${ACCLESS_INCLUDE_PREFIX_DIR}/accless)
+# Create symlinks for common libraries
+set(ACCLESS_LIBS_DIR ${CMAKE_CURRENT_SOURCE_DIR}/libs)
+file(CREATE_LINK ${ACCLESS_LIBS_DIR}/jwt/cpp-bindings ${ACCLESS_INCLUDE_PREFIX_DIR}/accless/jwt SYMBOLIC)
+file(CREATE_LINK ${ACCLESS_LIBS_DIR}/abe4/cpp-bindings ${ACCLESS_INCLUDE_PREFIX_DIR}/accless/abe4 SYMBOLIC)
+file(CREATE_LINK ${ACCLESS_LIBS_DIR}/base64 ${ACCLESS_INCLUDE_PREFIX_DIR}/accless/base64 SYMBOLIC)
+# Symlink individual headers from accless/include
+file(CREATE_LINK ${CMAKE_CURRENT_SOURCE_DIR}/include/accless.h ${ACCLESS_INCLUDE_PREFIX_DIR}/accless/accless.h SYMBOLIC)
+file(CREATE_LINK ${CMAKE_CURRENT_SOURCE_DIR}/include/dag.h ${ACCLESS_INCLUDE_PREFIX_DIR}/accless/dag.h SYMBOLIC)
+file(CREATE_LINK ${CMAKE_CURRENT_SOURCE_DIR}/include/utils.h ${ACCLESS_INCLUDE_PREFIX_DIR}/accless/utils.h SYMBOLIC)
+
set(ACCLESS_COMMON_LIBRARIES accless::jwt accless::abe4 accless::base64)
if (CMAKE_SYSTEM_NAME STREQUAL "WASI")
@@ -96,18 +108,20 @@ else ()
add_subdirectory(./libs/attestation)
add_subdirectory(./libs/s3)
+ # Create symlinks for native-only libraries
+ file(CREATE_LINK ${ACCLESS_LIBS_DIR}/attestation ${ACCLESS_INCLUDE_PREFIX_DIR}/accless/attestation SYMBOLIC)
+ file(CREATE_LINK ${ACCLESS_LIBS_DIR}/s3 ${ACCLESS_INCLUDE_PREFIX_DIR}/accless/s3 SYMBOLIC)
+
set(ACCLESS_LIBRARIES
accless::s3
accless::attestation
# We use the installed curl as part of azure-cvm-attestation
/usr/local/attestationcurl/lib/libcurl.a
)
- set(ACCLESS_HEADERS ${ACCLESS_ROOT}/libs)
endif()
target_include_directories(${CMAKE_PROJECT_TARGET} PUBLIC
- ${ACCLESS_COMMON_HEADERS}
- ${ACCLESS_HEADERS}
+ ${ACCLESS_INCLUDE_PREFIX_DIR}
)
target_link_libraries(${CMAKE_PROJECT_TARGET} PUBLIC
${ACCLESS_COMMON_LIBRARIES}
diff --git a/accless/src/accless.cpp b/accless/src/accless.cpp
index a064cd9..a23eefa 100644
--- a/accless/src/accless.cpp
+++ b/accless/src/accless.cpp
@@ -1,15 +1,15 @@
-#include "accless.h"
-#include "base64.h"
-#include "dag.h"
-#include "jwt.h"
-#include "utils.h"
+#include "accless/accless.h"
+#include "accless/base64/base64.h"
+#include "accless/dag.h"
+#include "accless/jwt/jwt.h"
+#include "accless/utils.h"
#ifdef __faasm
// Faasm includes
#include
#else
-#include "attestation/attestation.h"
-#include "s3/S3Wrapper.hpp"
+#include "accless/attestation/attestation.h"
+#include "accless/s3/S3Wrapper.hpp"
#endif
#include
diff --git a/accless/src/dag.cpp b/accless/src/dag.cpp
index 980abb6..773850d 100644
--- a/accless/src/dag.cpp
+++ b/accless/src/dag.cpp
@@ -1,4 +1,4 @@
-#include "dag.h"
+#include "accless/dag.h"
#include
#include
diff --git a/accless/src/utils.cpp b/accless/src/utils.cpp
index 169612d..1f008e8 100644
--- a/accless/src/utils.cpp
+++ b/accless/src/utils.cpp
@@ -1,4 +1,4 @@
-#include "utils.h"
+#include "accless/utils.h"
#ifdef __faasm
extern "C" {
diff --git a/applications/CMakeLists.txt b/applications/CMakeLists.txt
index 8cb35a8..18c146b 100644
--- a/applications/CMakeLists.txt
+++ b/applications/CMakeLists.txt
@@ -22,21 +22,10 @@ add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../accless ${ACCLESS_BUILD_DIRECTORY}
# Prepare variables for workflow compilation
if (CMAKE_SYSTEM_NAME STREQUAL "WASI")
set(ACCLESS_LIBRARIES faasm accless::accless)
- set(ACCLESS_HEADERS
- ${CMAKE_CURRENT_LIST_DIR}/../accless/include
- ${CMAKE_CURRENT_LIST_DIR}/../accless/libs/jwt/cpp-bindings
- ${CMAKE_CURRENT_LIST_DIR}/../accless/libs/abe4/cpp-bindings
- ${CMAKE_CURRENT_LIST_DIR}/../accless/libs/base64
- )
else ()
set(ACCLESS_LIBRARIES accless::accless)
- set(ACCLESS_HEADERS
- ${CMAKE_CURRENT_LIST_DIR}/../accless/include
- ${CMAKE_CURRENT_LIST_DIR}/../accless/libs/jwt/cpp-bindings
- ${CMAKE_CURRENT_LIST_DIR}/../accless/libs/abe4/cpp-bindings
- ${CMAKE_CURRENT_LIST_DIR}/../accless/libs/base64
- )
endif()
+set(ACCLESS_HEADERS ${ACCLESS_INCLUDE_PREFIX_DIR})
# =============================================================================
# Application subdirectories
diff --git a/applications/test/CMakeLists.txt b/applications/test/CMakeLists.txt
index f5c132d..c372446 100644
--- a/applications/test/CMakeLists.txt
+++ b/applications/test/CMakeLists.txt
@@ -1,3 +1,3 @@
# Mocked client replicating the behaviour of SGX-Faasm during remote attestation.
add_subdirectory(./att-client-sgx)
-add_subdirectory(att-client-snp)
+add_subdirectory(./att-client-snp)
diff --git a/applications/test/att-client-sgx/main.cpp b/applications/test/att-client-sgx/main.cpp
index 8a9343c..9831f99 100644
--- a/applications/test/att-client-sgx/main.cpp
+++ b/applications/test/att-client-sgx/main.cpp
@@ -1,6 +1,6 @@
-#include "abe4.h"
-#include "attestation/attestation.h"
-#include "jwt.h"
+#include "accless/abe4/abe4.h"
+#include "accless/attestation/attestation.h"
+#include "accless/jwt/jwt.h"
#include
diff --git a/applications/test/att-client-snp/main.cpp b/applications/test/att-client-snp/main.cpp
index 494af5b..9f9a9be 100644
--- a/applications/test/att-client-snp/main.cpp
+++ b/applications/test/att-client-snp/main.cpp
@@ -1,6 +1,6 @@
-#include "abe4.h"
-#include "attestation/attestation.h"
-#include "jwt.h"
+#include "accless/abe4/abe4.h"
+#include "accless/attestation/attestation.h"
+#include "accless/jwt/jwt.h"
#include
From c00830c264bd74a543628db7b462125c85372aa5 Mon Sep 17 00:00:00 2001
From: Carlos Segarra
Date: Fri, 21 Nov 2025 16:07:12 +0000
Subject: [PATCH 02/52] [accli] E: Clarify ApplicationsCommand Help
---
accli/src/main.rs | 3 +++
1 file changed, 3 insertions(+)
diff --git a/accli/src/main.rs b/accli/src/main.rs
index a5bb16f..cc118f9 100644
--- a/accli/src/main.rs
+++ b/accli/src/main.rs
@@ -292,10 +292,13 @@ enum AcclessCommand {
enum ApplicationsCommand {
/// Build the Accless applications
Build {
+ /// Force a clean build.
#[arg(long)]
clean: bool,
+ /// Force a debug build.
#[arg(long)]
debug: bool,
+ /// Path to the attestation service's public certificate PEM file.
#[arg(long)]
cert_path: Option,
},
From ad4f89f4807419951b1581f1efca973e39b84324 Mon Sep 17 00:00:00 2001
From: Carlos Segarra
Date: Fri, 21 Nov 2025 17:15:16 +0000
Subject: [PATCH 03/52] [scripts] E: Remove Obsolete Scripts
---
scripts/build_cli.sh | 23 -----------------
scripts/patch_jwt_cert.sh | 52 ---------------------------------------
2 files changed, 75 deletions(-)
delete mode 100755 scripts/build_cli.sh
delete mode 100755 scripts/patch_jwt_cert.sh
diff --git a/scripts/build_cli.sh b/scripts/build_cli.sh
deleted file mode 100755
index 7fddf54..0000000
--- a/scripts/build_cli.sh
+++ /dev/null
@@ -1,23 +0,0 @@
-#!/bin/bash
-
-THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" >/dev/null 2>&1 && pwd )"
-PROJ_ROOT="${THIS_DIR}/.."
-
-pushd ${PROJ_ROOT}>>/dev/null
-
-# ----------------------------
-# Environment vars
-# ----------------------------
-
-export VERSION=$(cat ${PROJ_ROOT}/VERSION)
-
-docker run \
- --rm -it \
- --name tless-build \
- --net host \
- -v ${PROJ_ROOT}/workflows:/code/faasm-examples/workflows \
- -w /code/faasm-examples \
- ghcr.io/coco-serverless/tless-experiments:${VERSION} \
- bash
-
-popd >> /dev/null
diff --git a/scripts/patch_jwt_cert.sh b/scripts/patch_jwt_cert.sh
deleted file mode 100755
index e82643a..0000000
--- a/scripts/patch_jwt_cert.sh
+++ /dev/null
@@ -1,52 +0,0 @@
-#!/bin/bash
-
-# This script patches the known-good X509 certificate in the JWT parsing
-# library after we deploy one instance of the attribute-providing-service.
-# The service's certificate depends on the IP of where it is deployed, and
-# it must be hard-coded inside the function code (for correct measurement).
-# This file patches the source code once we have the deployed the service.
-
-set -euo pipefail
-
-# Get directories
-THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]:-${(%):-%x}}" )" >/dev/null 2>&1 && pwd )"
-PROJ_ROOT="${THIS_DIR}/.."
-
-# Define file paths
-CERT_FILE="${PROJ_ROOT}/attestation-service/certs/cert.pem"
-RUST_FILE="${PROJ_ROOT}/accless/libs/jwt/src/lib.rs"
-
-# Define markers
-START_MARKER="// BEGIN: AUTO-INJECTED CERT"
-END_MARKER="// END: AUTO-INJECTED CERT"
-
-if [[ ! -f "${CERT_FILE}" ]]; then
- echo "accless: patch: error: Certificate file not found at ${CERT_FILE}"
- echo "accless: patch: please run 'attestation-service/bin/gen_keys.sh' first."
- exit 1
-fi
-
-if [[ ! -f "${RUST_FILE}" ]]; then
- echo "accless: patch: error: JWT library file not found at ${RUST_FILE}"
- exit 1
-fi
-
-echo "accless: patch: Reading new certificate from ${CERT_FILE}"
-
-# Read the certificate and format it as a Rust raw string literal
-# We use awk to wrap the file content in `r#"` and `"#,\n`
-NEW_CERT_BLOCK=$(awk 'BEGIN {print "r#\""} {print} END {print "\"#,"}' "${CERT_FILE}")
-
-# Use sed to replace the block between the markers
-# This command finds the block, including the marker lines, and replaces it
-# with the markers *plus* the new certificate block.
-sed -i.bak "/${START_MARKER}/,/${END_MARKER}/c\\
-${START_MARKER}\
-${NEW_CERT_BLOCK}\
-${END_MARKER}\
-" "${RUST_FILE}"
-
-# Remove the backup file created by sed
-rm -f "${RUST_FILE}.bak"
-
-echo "accless: patch: Successfully patched ${RUST_FILE} with new certificate."
From f1a5ffa95c41909aa79e619a2fede687c4e573bb Mon Sep 17 00:00:00 2001
From: Carlos Segarra
Date: Sat, 22 Nov 2025 11:55:13 +0000
Subject: [PATCH 04/52] [accli] E: Add AttestationService Task
---
accli/src/main.rs | 66 +++++++++++++++++-
accli/src/tasks/attestation_service.rs | 95 ++++++++++++++++++++++++++
accli/src/tasks/mod.rs | 1 +
3 files changed, 161 insertions(+), 1 deletion(-)
create mode 100644 accli/src/tasks/attestation_service.rs
diff --git a/accli/src/main.rs b/accli/src/main.rs
index cc118f9..3a4a8a1 100644
--- a/accli/src/main.rs
+++ b/accli/src/main.rs
@@ -3,6 +3,7 @@ use crate::{
tasks::{
accless::Accless,
applications::Applications,
+ attestation_service::AttestationService,
azure::Azure,
dev::Dev,
docker::{Docker, DockerContainer},
@@ -11,7 +12,7 @@ use crate::{
},
};
use clap::{Parser, Subcommand};
-use std::{collections::HashMap, process};
+use std::{collections::HashMap, path::PathBuf, process};
pub mod attestation_service;
pub mod env;
@@ -64,6 +65,44 @@ enum Command {
#[command(subcommand)]
s3_command: S3Command,
},
+ /// Build and run the attestation service
+ AttestationService {
+ #[command(subcommand)]
+ attestation_service_command: AttestationServiceCommand,
+ },
+}
+
+#[derive(Debug, Subcommand)]
+enum AttestationServiceCommand {
+ /// Build the attestation service
+ Build {},
+ /// Run the attestation service
+ Run {
+ /// Directory where to look-for and store TLS certificates.
+ #[arg(long)]
+ certs_dir: Option,
+ /// Port to bind the server to.
+ #[arg(long)]
+ port: Option,
+ /// URL to fetch SGX platform collateral information.
+ #[arg(long)]
+ sgx_pccs_url: Option,
+ /// Whether to overwrite the existing TLS certificates (if any).
+ #[arg(long)]
+ force_clean_certs: bool,
+ /// Run the attestation service in mock mode, skipping quote
+ /// verification.
+ #[arg(long, default_value_t = false)]
+ mock: bool,
+ },
+ Health {
+ /// URL of the attestation service
+ #[arg(long)]
+ url: Option,
+ /// Path to the attestation service's public certificate PEM file
+ #[arg(long)]
+ cert_path: Option,
+ },
}
#[derive(Debug, Subcommand)]
@@ -760,6 +799,31 @@ async fn main() -> anyhow::Result<()> {
}
},
},
+ Command::AttestationService {
+ attestation_service_command,
+ } => match attestation_service_command {
+ AttestationServiceCommand::Build {} => {
+ AttestationService::build()?;
+ }
+ AttestationServiceCommand::Run {
+ certs_dir,
+ port,
+ sgx_pccs_url,
+ force_clean_certs,
+ mock,
+ } => {
+ AttestationService::run(
+ certs_dir.as_deref(),
+ *port,
+ sgx_pccs_url.as_deref(),
+ *force_clean_certs,
+ *mock,
+ )?;
+ }
+ AttestationServiceCommand::Health { url, cert_path } => {
+ AttestationService::health(url.clone(), cert_path.clone()).await?;
+ }
+ },
}
Ok(())
diff --git a/accli/src/tasks/attestation_service.rs b/accli/src/tasks/attestation_service.rs
new file mode 100644
index 0000000..9d17c7b
--- /dev/null
+++ b/accli/src/tasks/attestation_service.rs
@@ -0,0 +1,95 @@
+use crate::env::Env;
+use anyhow::Result;
+use log::info;
+use reqwest;
+use std::{
+ fs,
+ process::{Command, Stdio},
+};
+
+pub struct AttestationService;
+
+impl AttestationService {
+ pub fn build() -> Result<()> {
+ let mut cmd = Command::new("cargo");
+ cmd.arg("build")
+ .arg("-p")
+ .arg("attestation-service")
+ .arg("--release");
+ let status = cmd.status()?;
+ if !status.success() {
+ anyhow::bail!("Failed to build attestation service");
+ }
+ Ok(())
+ }
+
+ pub fn run(
+ certs_dir: Option<&std::path::Path>,
+ port: Option,
+ sgx_pccs_url: Option<&std::path::Path>,
+ force_clean_certs: bool,
+ mock: bool,
+ ) -> Result<()> {
+ let mut cmd = Command::new(
+ Env::proj_root()
+ .join("target")
+ .join("release")
+ .join("attestation-service"),
+ );
+ if let Some(certs_dir) = certs_dir {
+ cmd.arg("--certs-dir").arg(certs_dir);
+ }
+ if let Some(port) = port {
+ cmd.arg("--port").arg(port.to_string());
+ }
+ if let Some(sgx_pccs_url) = sgx_pccs_url {
+ cmd.arg("--sgx-pccs-url").arg(sgx_pccs_url);
+ }
+ if force_clean_certs {
+ cmd.arg("--force-clean-certs");
+ }
+ if mock {
+ cmd.arg("--mock");
+ }
+ let status = cmd
+ .stdout(Stdio::inherit())
+ .stderr(Stdio::inherit())
+ .status()?;
+ if !status.success() {
+ anyhow::bail!("Failed to run attestation service");
+ }
+ Ok(())
+ }
+
+ pub async fn health(url: Option, cert_path: Option) -> Result<()> {
+ let url = url.or_else(|| std::env::var("ACCLESS_AS_URL").ok());
+ let cert_path = cert_path.or_else(|| {
+ std::env::var("ACCLESS_AS_CERT_PATH")
+ .ok()
+ .map(std::path::PathBuf::from)
+ });
+
+ let url = match url {
+ Some(url) => url,
+ None => {
+ anyhow::bail!("Attestation service URL not provided. Set --url or ACCLESS_AS_URL")
+ }
+ };
+
+ let client = match cert_path {
+ Some(cert_path) => {
+ let cert = fs::read(cert_path)?;
+ let cert = reqwest::Certificate::from_pem(&cert)?;
+ reqwest::Client::builder()
+ .add_root_certificate(cert)
+ .build()
+ }
+ None => reqwest::Client::builder().build(),
+ }?;
+
+ let response = client.get(format!("{}/health", url)).send().await?;
+ info!("Health check response: {}", response.text().await?);
+
+ Ok(())
+ }
+}
diff --git a/accli/src/tasks/mod.rs b/accli/src/tasks/mod.rs
index a692518..ef7bf82 100644
--- a/accli/src/tasks/mod.rs
+++ b/accli/src/tasks/mod.rs
@@ -1,5 +1,6 @@
pub mod accless;
pub mod applications;
+pub mod attestation_service;
pub mod azure;
pub mod dev;
pub mod docker;
From d1326602fd834149d44bb1409ecd6ff53192a966 Mon Sep 17 00:00:00 2001
From: Carlos Segarra
Date: Sat, 22 Nov 2025 13:44:32 +0000
Subject: [PATCH 05/52] [scripts] E: Add Scripts To Spawn SNP cVM
---
scripts/apt.sh | 4 +-
scripts/common/logging.sh | 97 +++++++++++++
scripts/common/utils.sh | 41 ++++++
scripts/snp/.gitignore | 1 +
scripts/snp/cloud-init/meta-data.in | 2 +
scripts/snp/cloud-init/user-data.in | 43 ++++++
scripts/snp/run.sh | 55 ++++++++
scripts/snp/setup.sh | 211 ++++++++++++++++++++++++++++
scripts/snp/versions.sh | 4 +
9 files changed, 457 insertions(+), 1 deletion(-)
create mode 100644 scripts/common/logging.sh
create mode 100644 scripts/common/utils.sh
create mode 100644 scripts/snp/.gitignore
create mode 100644 scripts/snp/cloud-init/meta-data.in
create mode 100644 scripts/snp/cloud-init/user-data.in
create mode 100755 scripts/snp/run.sh
create mode 100755 scripts/snp/setup.sh
create mode 100644 scripts/snp/versions.sh
diff --git a/scripts/apt.sh b/scripts/apt.sh
index e036a85..b07e4d8 100755
--- a/scripts/apt.sh
+++ b/scripts/apt.sh
@@ -4,4 +4,6 @@ sudo apt install -y \
clang-format \
libfontconfig1-dev \
libssl-dev \
- pkg-config > /dev/null 2>&1
+ ovmf \
+ pkg-config \
+ qemu-system-x86 > /dev/null 2>&1
diff --git a/scripts/common/logging.sh b/scripts/common/logging.sh
new file mode 100644
index 0000000..cc3f331
--- /dev/null
+++ b/scripts/common/logging.sh
@@ -0,0 +1,97 @@
+#!/bin/bash
+
+#
+# Logging functions.
+#
+
+#===================================================================================================
+# Include Guard
+#===================================================================================================
+
+# Skip this file if already included.
+if [[ -n "${__LOGGING_SH_INCLUDED:-}" ]]; then
+ return
+fi
+readonly __LOGGING_SH_INCLUDED=1
+
+#===================================================================================================
+# Constants
+#===================================================================================================
+
+# Colors
+readonly RED='\033[0;31m' # Red
+readonly GREEN='\033[0;32m' # Green
+readonly YELLOW='\033[0;33m' # Yellow
+readonly NC='\033[0m' # No Color
+
+#==================================================================================================
+# Functions
+#==================================================================================================
+
+#
+# Description
+#
+# Prints an error message on stderr.
+#
+# Arguments
+#
+# $1 - The error message to print.
+#
+# Usage Example
+#
+# print_error "Print an error message."
+#
+print_error() {
+ echo -e "${RED}[ERROR] ${1}${NC}" >&2
+}
+
+#
+# Description
+#
+# Prints a success message on stdout.
+#
+# Arguments
+#
+# $1 - The success message to print.
+#
+# Usage Example
+#
+# print_success "Print a success message."
+#
+print_success() {
+ echo -e "${GREEN}[SUCCESS] ${1}${NC}"
+}
+
+#
+# Description
+#
+# Prints a message on stdout.
+#
+# Arguments
+#
+# $1 - The message to print.
+#
+# Usage Example
+#
+# print_info "Print an informational message."
+#
+print_info() {
+ echo -e "[INFO] ${1}"
+}
+
+#
+# Description
+#
+# Prints a warning message on stderr.
+#
+# Arguments
+#
+# $1 - The warning message to print.
+#
+# Usage Example
+#
+# print_warning "Print a warning message."
+#
+print_warning() {
+ echo -e "${YELLOW}[WARN] ${1}${NC}" >&2
+}
diff --git a/scripts/common/utils.sh b/scripts/common/utils.sh
new file mode 100644
index 0000000..9b81d4e
--- /dev/null
+++ b/scripts/common/utils.sh
@@ -0,0 +1,41 @@
+#!/bin/bash
+
+# Copyright(c) The Maintainers of Nanvix.
+# Licensed under the MIT License.
+
+#
+# Utility functions.
+#
+
+#===================================================================================================
+# Include Guard
+#===================================================================================================
+
+# Skip this file if already included.
+if [[ -n "${__UTILS_SH_INCLUDED:-}" ]]; then
+ return
+fi
+readonly __UTILS_SH_INCLUDED=1
+
+#==================================================================================================
+# Imports
+#==================================================================================================
+
+source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/logging.sh"
+
+#==================================================================================================
+# Functions
+#==================================================================================================
+
+#
+# Checks if the --clean flag was passed.
+# Returns 0 (true) if the flag is present, 1 (false) otherwise.
+#
+has_clean_flag() {
+ for arg in "$@"; do
+ if [[ "$arg" == "--clean" ]]; then
+ return 0 # 0 means "true" (success)
+ fi
+ done
+ return 1 # 1 means "false" (failure)
+}
diff --git a/scripts/snp/.gitignore b/scripts/snp/.gitignore
new file mode 100644
index 0000000..53752db
--- /dev/null
+++ b/scripts/snp/.gitignore
@@ -0,0 +1 @@
+output
diff --git a/scripts/snp/cloud-init/meta-data.in b/scripts/snp/cloud-init/meta-data.in
new file mode 100644
index 0000000..5e212f0
--- /dev/null
+++ b/scripts/snp/cloud-init/meta-data.in
@@ -0,0 +1,2 @@
+instance-id: ${INSTANCE_ID}
+local-hostname: accless-snp
diff --git a/scripts/snp/cloud-init/user-data.in b/scripts/snp/cloud-init/user-data.in
new file mode 100644
index 0000000..905a95e
--- /dev/null
+++ b/scripts/snp/cloud-init/user-data.in
@@ -0,0 +1,43 @@
+#cloud-config
+
+users:
+ - name: ubuntu
+ gecos: Ubuntu
+ sudo: ALL=(ALL) NOPASSWD:ALL
+ groups: [sudo, docker]
+ shell: /bin/bash
+ ssh_authorized_keys:
+ - "${SSH_PUB_KEY}"
+
+growpart:
+ mode: auto
+ devices: ['/']
+ ignore_growroot_disabled: false
+
+package_update: true
+packages:
+ - cargo
+ - docker.io
+ - git
+ - linux-generic
+ - rustc
+
+runcmd:
+ # Make sure ubuntu is in docker group (in case group created after user).
+ - [ sh, -c, 'usermod -aG docker ubuntu || true' ]
+ # Enable and start docker.
+ - [ systemctl, enable, --now, docker ]
+ # Enable and start sshd.
+ - [ systemctl, enable, --now, ssh.service ]
+ # Load SEV guest driver so /dev/sev-guest exists.
+ - [ sh, -c, 'modprobe sev-guest || echo "modprobe sev-guest failed (maybe built-in?)"' ]
+ # Change password.
+ - [ sh, -c, 'echo "ubuntu:ubuntu" | chpasswd' ]
+ # Make sure home is owned by ubuntu>
+ - [ chown, "-R", "ubuntu:ubuntu", "/home/ubuntu" ]
+ # Clone Accless repo.
+ - [ sudo, "-u", "ubuntu", "bash", "-lc", "cd /home/ubuntu && git clone https://github.com/faasm/accless.git accless" ]
+ # Quick debug markers in console.
+ - [ sh, -c, 'echo "[cloud-init] Docker + chpasswd + sev-guest setup done"' ]
+
+final_message: "Accless SNP test instance v2 is ready."
diff --git a/scripts/snp/run.sh b/scripts/snp/run.sh
new file mode 100755
index 0000000..3120b6b
--- /dev/null
+++ b/scripts/snp/run.sh
@@ -0,0 +1,55 @@
+#!/bin/bash
+
+set -euo pipefail
+
+THIS_DIR="$(dirname "$(realpath "$0")")"
+SCRIPTS_DIR="${THIS_DIR}/.."
+OUTPUT_DIR="${THIS_DIR}/output"
+
+source "${SCRIPTS_DIR}/common/utils.sh"
+source "${THIS_DIR}/versions.sh"
+
+#
+# Helper method to get the C-bit position directly from hardware.
+#
+get_cbitpos() {
+ modprobe cpuid
+ local ebx=$(sudo dd if=/dev/cpu/0/cpuid ibs=16 count=32 skip=134217728 2> /dev/null | tail -c 16 | od -An -t u4 -j 4 -N 4 | sed -re 's|^ *||')
+ local cbitpos=$((ebx & 0x3f))
+ echo $cbitpos
+}
+
+#
+# Run an SNP guest using QEMU.
+#
+run_qemu() {
+ local qemu_dir="${OUTPUT_DIR}/qemu/qemu-${QEMU_VERSION}"
+ local qemu="${qemu_dir}/build/qemu-system-x86_64"
+ local qemu_bios_dir="${qemu_dir}/pc-bios"
+
+ local kernel="${OUTPUT_DIR}/vmlinuz-noble"
+ local ovmf="${OUTPUT_DIR}/ovmf/ovmf-${OVMF_VERSION}/Build/AmdSev/RELEASE_GCC5/FV/OVMF.fd"
+ local disk_image="${OUTPUT_DIR}/disk.img"
+ local seed_image="${OUTPUT_DIR}/seed.img"
+ local cbitpos=$(get_cbitpos)
+
+ # Can SSH into the VM witih:
+ # ssh -p 2222 -i ${OUTPUT_DIR}/snp-key ubuntu@localhost
+ ${qemu} \
+ -L "${qemu_bios_dir}" \
+ -enable-kvm \
+ -nographic \
+ -machine q35,confidential-guest-support=sev0,vmport=off \
+ -cpu EPYC-v4 \
+ -smp 6 -m 6G \
+ -bios ${ovmf} \
+ -object memory-backend-memfd,id=ram1,size=6G,share=true,prealloc=false \
+ -machine memory-backend=ram1 \
+ -object sev-snp-guest,id=sev0,cbitpos=${cbitpos},reduced-phys-bits=1 \
+ -drive "if=virtio,format=qcow2,file=${disk_image}" \
+ -drive "if=virtio,format=raw,file=${seed_image}" \
+ -netdev user,id=net0,hostfwd=tcp::2222-:22 \
+ -device e1000,netdev=net0
+}
+
+run_qemu
diff --git a/scripts/snp/setup.sh b/scripts/snp/setup.sh
new file mode 100755
index 0000000..f0117cd
--- /dev/null
+++ b/scripts/snp/setup.sh
@@ -0,0 +1,211 @@
+#!/bin/bash
+
+set -euo pipefail
+
+THIS_DIR="$(dirname "$(realpath "$0")")"
+SCRIPTS_DIR="${THIS_DIR}/.."
+OUTPUT_DIR="${THIS_DIR}/output"
+
+source "${SCRIPTS_DIR}/common/utils.sh"
+source "${THIS_DIR}/versions.sh"
+
+clean() {
+ print_info "Cleaning and re-creating ${OUTPUT_DIR} directory..."
+ rm -rf ${OUTPUT_DIR}
+ mkdir -p ${OUTPUT_DIR}
+}
+
+# TODO: check host kernel
+
+#
+# Fetch the linux kernel image.
+#
+install_apt_deps() {
+ print_info "Installing APT dependencies..."
+ sudo apt install -y cloud-utils ovmf > /dev/null 2>&1
+}
+
+#
+# Build up-to-date QEMU (need >= 9.x).
+#
+build_qemu() {
+ print_info "Building and installing QEMU (v${QEMU_VERSION}) from source (will take a minute)..."
+ local qemu_out_dir="${OUTPUT_DIR}/qemu"
+ mkdir -p ${qemu_out_dir}
+ pushd ${qemu_out_dir} >> /dev/null
+
+ wget https://download.qemu.org/qemu-${QEMU_VERSION}.tar.xz > /dev/null 2>&1
+ tar xvJf qemu-${QEMU_VERSION}.tar.xz > /dev/null 2>&1
+ pushd qemu-${QEMU_VERSION} >> /dev/null
+ ./configure --enable-slirp > /dev/null 2>&1
+ make -j $(nproc) > /dev/null 2>&1
+ popd >> /dev/null
+
+ popd >> /dev/null
+ print_success "Successfully built QEMU (v${QEMU_VERSION})!"
+}
+
+#
+# Build up-to-date OVMF version.
+#
+build_ovmf() {
+ print_info "Building and installing OVMF (${OVMF_VERSION}) from source..."
+ local ovmf_out_dir="${OUTPUT_DIR}/ovmf"
+ mkdir -p ${ovmf_out_dir}
+ pushd ${ovmf_out_dir} >> /dev/null
+
+ if [ ! -d "ovmf-${OVMF_VERSION}" ]; then
+ git clone -b ${OVMF_VERSION} https://github.com/tianocore/edk2.git ovmf-${OVMF_VERSION} > /dev/null 2>&1
+ fi
+
+ pushd ovmf-${OVMF_VERSION} >> /dev/null
+ git submodule update --init --recursive > /dev/null 2>&1
+ make -C BaseTools clean > /dev/null 2>&1
+ make -C BaseTools -j $(nproc) > /dev/null 2>&1
+ . ./edksetup.sh --reconfig
+ build -a X64 -b RELEASE -t GCC5 -p OvmfPkg/OvmfPkgX64.dsc > /dev/null 2>&1
+ touch OvmfPkg/AmdSev/Grub/grub.efi > /dev/null 2>&1
+ build -a X64 -b RELEASE -t GCC5 -p OvmfPkg/AmdSev/AmdSevX64.dsc > /dev/null 2>&1
+ popd >> /dev/null
+
+ popd >> /dev/null
+ print_success "Successfully built OVMF (${OVMF_VERSION})!"
+}
+
+#
+# Fetch the linux kernel image.
+#
+fetch_kernel() {
+ print_info "Fetching Linux kernel..."
+ wget \
+ https://cloud-images.ubuntu.com/noble/20251113/unpacked/noble-server-cloudimg-amd64-vmlinuz-generic \
+ -O ${OUTPUT_DIR}/vmlinuz-noble > /dev/null 2>&1
+ if [ $? -eq 0 ]; then
+ print_success "Linux kernel fetched successfully."
+ else
+ print_error "Failed to fetch Linux kernel."
+ exit 1
+ fi
+}
+
+#
+# Fetch the linux kernel image.
+#
+fetch_disk_image() {
+ print_info "Fetching cloud-init disk image..."
+ wget \
+ https://cloud-images.ubuntu.com/noble/20251113/noble-server-cloudimg-amd64.img \
+ -O ${OUTPUT_DIR}/disk.img > /dev/null 2>&1
+ if [ $? -eq 0 ]; then
+ print_success "cloud-init disk image fetched successfully."
+ else
+ print_error "Failed to fetch cloud-init disk image."
+ exit 1
+ fi
+
+ local qemu_img="${OUTPUT_DIR}/qemu/qemu-${QEMU_VERSION}/build/qemu-img"
+ ${qemu_img} resize "${OUTPUT_DIR}/disk.img" +20G > /dev/null 2>&1
+ print_success "cloud-init disk image resized successfully."
+}
+
+#
+# Generate ephemeral keypair for VM.
+#
+generate_ephemeral_keys() {
+ print_info "Generating ephemeral keypair..."
+ ssh-keygen -q -t ed25519 -N "" -f ${OUTPUT_DIR}/snp-key <<< y >/dev/null 2>&1
+ print_info "Keypair generated succesfully!"
+}
+
+#
+# Prepare the cloudinit overlay disk image.
+#
+prepare_cloudinit_image() {
+ print_info "Preparing cloud-init overlay image..."
+
+ local in_dir="${THIS_DIR}/cloud-init"
+ local out_dir="${OUTPUT_DIR}/cloud-init"
+
+ mkdir -p ${out_dir}
+ INSTANCE_ID="accless-snp-$(date +%s)" envsubst '${INSTANCE_ID}' \
+ < ${in_dir}/meta-data.in > ${out_dir}/meta-data
+ SSH_PUB_KEY=$(cat "${OUTPUT_DIR}/snp-key.pub") envsubst '${SSH_PUB_KEY}' \
+ < ${in_dir}/user-data.in > ${out_dir}/user-data
+
+ cloud-localds "${OUTPUT_DIR}/seed.img" "${out_dir}/user-data" "${out_dir}/meta-data"
+
+ print_success "cloud-init overlay prepared successfully!"
+}
+
+usage() {
+ print_info "Usage: $0 [--clean] [--component ]"
+ exit 1
+}
+
+main() {
+ local component=""
+
+ while [[ "$#" -gt 0 ]]; do
+ case $1 in
+ --clean)
+ clean
+ shift
+ ;;
+ --component)
+ if [[ -z "$2" || "$2" == --* ]]; then
+ echo "Error: --component requires a value."
+ usage
+ fi
+ component="$2"
+ shift 2
+ ;;
+ -h | --help)
+ usage
+ ;;
+ *)
+ print_error "Unknown option: $1"
+ usage
+ ;;
+ esac
+ done
+
+ if [[ -n "$component" ]]; then
+ case "$component" in
+ apt)
+ install_apt_deps
+ ;;
+ qemu)
+ build_qemu
+ ;;
+ ovmf)
+ build_ovmf
+ ;;
+ kernel)
+ fetch_kernel
+ ;;
+ disk)
+ fetch_disk_image
+ ;;
+ keys)
+ generate_ephemeral_keys
+ ;;
+ cloudinit)
+ prepare_cloudinit_image
+ ;;
+ *)
+ print_error "Error: Invalid component '$component'"
+ usage
+ ;;
+ esac
+ else
+ install_apt_deps
+ build_qemu
+ build_ovmf
+ fetch_kernel
+ fetch_disk_image
+ generate_ephemeral_keys
+ prepare_cloudinit_image
+ fi
+}
+
+main "$@"
diff --git a/scripts/snp/versions.sh b/scripts/snp/versions.sh
new file mode 100644
index 0000000..d6cbd54
--- /dev/null
+++ b/scripts/snp/versions.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+export OVMF_VERSION="edk2-stable202511"
+export QEMU_VERSION="10.1.2"
From 568b2d13f7af24d99f35f79ea7ce086ca6d26dca Mon Sep 17 00:00:00 2001
From: Carlos Segarra
Date: Sat, 22 Nov 2025 13:57:32 +0000
Subject: [PATCH 06/52] [accli] B: Ignore Paths In Dev
---
accli/src/tasks/applications.rs | 30 ++++++++++++++-------
accli/src/tasks/dev.rs | 11 ++++++--
accli/src/tasks/docker.rs | 48 +++++++++++++++++++++++++++++++++
3 files changed, 77 insertions(+), 12 deletions(-)
diff --git a/accli/src/tasks/applications.rs b/accli/src/tasks/applications.rs
index cfc6b54..cbd6bb6 100644
--- a/accli/src/tasks/applications.rs
+++ b/accli/src/tasks/applications.rs
@@ -18,18 +18,28 @@ impl Applications {
if debug {
cmd.push("--debug".to_string());
}
- if let Some(cert_path) = cert_path {
+ if let Some(cert_path_str) = cert_path {
+ let cert_path = Path::new(cert_path_str);
+ if !cert_path.exists() {
+ anyhow::bail!("Certificate path does not exist: {}", cert_path.display());
+ }
+ if !cert_path.is_file() {
+ anyhow::bail!("Certificate path is not a file: {}", cert_path.display());
+ }
+ let docker_cert_path = Docker::get_docker_path(cert_path)?;
cmd.push("--cert-path".to_string());
- cmd.push(cert_path.to_string());
+ let docker_cert_path_str = docker_cert_path.to_str().ok_or_else(|| {
+ anyhow::anyhow!(
+ "Docker path for certificate is not valid UTF-8: {}",
+ docker_cert_path.display()
+ )
+ })?;
+ cmd.push(docker_cert_path_str.to_string());
}
let workdir = Path::new(DOCKER_ACCLESS_CODE_MOUNT_DIR).join("applications");
- Docker::run(
- &cmd,
- true,
- Some(workdir.to_str().unwrap()),
- &[],
- false,
- capture_output,
- )
+ let workdir_str = workdir.to_str().ok_or_else(|| {
+ anyhow::anyhow!("Workdir path is not valid UTF-8: {}", workdir.display())
+ })?;
+ Docker::run(&cmd, true, Some(workdir_str), &[], false, capture_output)
}
}
diff --git a/accli/src/tasks/dev.rs b/accli/src/tasks/dev.rs
index 7905432..bb5221d 100644
--- a/accli/src/tasks/dev.rs
+++ b/accli/src/tasks/dev.rs
@@ -209,7 +209,14 @@ impl Dev {
}
fn is_excluded(entry: &walkdir::DirEntry) -> bool {
- let excluded_dirs = ["build-wasm", "build-native", "target", "venv", "venv-bm"];
+ let excluded_dirs = [
+ "build-wasm",
+ "build-native",
+ "output",
+ "target",
+ "venv",
+ "venv-bm",
+ ];
entry.file_type().is_dir()
&& entry
.file_name()
@@ -218,7 +225,7 @@ impl Dev {
.unwrap_or(false)
}
- for entry in walkdir::WalkDir::new(".")
+ for entry in walkdir::WalkDir::new(Env::proj_root())
.into_iter()
.filter_entry(|e| !is_excluded(e))
.filter_map(Result::ok)
diff --git a/accli/src/tasks/docker.rs b/accli/src/tasks/docker.rs
index 75afbb2..7617bcf 100644
--- a/accli/src/tasks/docker.rs
+++ b/accli/src/tasks/docker.rs
@@ -3,6 +3,7 @@ use clap::ValueEnum;
use log::error;
use std::{
fmt,
+ path::{Path, PathBuf},
process::{Command, Stdio},
str::FromStr,
};
@@ -50,6 +51,53 @@ pub const DOCKER_ACCLESS_CODE_MOUNT_DIR: &str = "/code/accless";
impl Docker {
const ACCLESS_DEV_CONTAINER_NAME: &'static str = "accless-dev";
+ /// # Description
+ ///
+ /// This function takes a path from the host filesystem and maps it to the
+ /// corresponding path inside the Docker container.
+ /// It first converts the `host_path` to an absolute path and canonicalizes
+ /// it. This process also verifies that the path exists.
+ /// Then, it checks if the path is within the project's root directory.
+ /// If it is, it strips the project root prefix and prepends the Docker
+ /// mount directory path (`/code/accless`).
+ ///
+ /// # Arguments
+ ///
+ /// * `host_path` - A reference to a `Path` on the host filesystem. It can
+ /// be either an absolute path or a path relative to the current working
+ /// directory.
+ ///
+ /// # Returns
+ ///
+ /// A `anyhow::Result` which is:
+ /// - `Ok(PathBuf)`: The mapped path inside the Docker container.
+ /// - `Err(anyhow::Error)`: An error if:
+ /// - The path does not exist or cannot be canonicalized.
+ /// - The path is outside the project's root directory.
+ pub fn get_docker_path(host_path: &Path) -> anyhow::Result {
+ let absolute_host_path = if host_path.is_absolute() {
+ host_path.to_path_buf()
+ } else {
+ std::env::current_dir()?.join(host_path)
+ };
+ let absolute_host_path = absolute_host_path.canonicalize().map_err(|e| {
+ anyhow::anyhow!("Error canonicalizing path {}: {}", host_path.display(), e)
+ })?;
+
+ let proj_root = Env::proj_root();
+ if absolute_host_path.starts_with(&proj_root) {
+ let relative_path = absolute_host_path.strip_prefix(&proj_root).unwrap();
+ let docker_path = Path::new(DOCKER_ACCLESS_CODE_MOUNT_DIR).join(relative_path);
+ Ok(docker_path)
+ } else {
+ anyhow::bail!(
+ "Path {} is outside the project root directory {}",
+ absolute_host_path.display(),
+ proj_root.display()
+ );
+ }
+ }
+
pub fn get_docker_tag(ctr: &DockerContainer) -> String {
// Prepare image tag
let version = match Env::get_version() {
From fb7c36687f9eb2289dd320f109360af0ae900a0d Mon Sep 17 00:00:00 2001
From: Carlos Segarra
Date: Sat, 22 Nov 2025 16:10:47 +0000
Subject: [PATCH 07/52] [scripts] E: Refine SNP SetUp Scripts
---
scripts/apt.sh | 5 ++---
scripts/snp/cloud-init/user-data.in | 34 +++++++++++++++++++++++++----
scripts/snp/run.sh | 6 ++++-
scripts/snp/setup.sh | 19 ++++++++++++++--
scripts/snp/versions.sh | 2 ++
5 files changed, 56 insertions(+), 10 deletions(-)
diff --git a/scripts/apt.sh b/scripts/apt.sh
index b07e4d8..705c540 100755
--- a/scripts/apt.sh
+++ b/scripts/apt.sh
@@ -1,9 +1,8 @@
#!/bin/bash
sudo apt install -y \
+ build-essential \
clang-format \
libfontconfig1-dev \
libssl-dev \
- ovmf \
- pkg-config \
- qemu-system-x86 > /dev/null 2>&1
+ pkg-config > /dev/null 2>&1
diff --git a/scripts/snp/cloud-init/user-data.in b/scripts/snp/cloud-init/user-data.in
index 905a95e..632790b 100644
--- a/scripts/snp/cloud-init/user-data.in
+++ b/scripts/snp/cloud-init/user-data.in
@@ -16,11 +16,10 @@ growpart:
package_update: true
packages:
- - cargo
- docker.io
- git
- - linux-generic
- - rustc
+ - linux-modules-extra-${GUEST_KERNEL_VERSION}
+ - python3-venv
runcmd:
# Make sure ubuntu is in docker group (in case group created after user).
@@ -33,10 +32,37 @@ runcmd:
- [ sh, -c, 'modprobe sev-guest || echo "modprobe sev-guest failed (maybe built-in?)"' ]
# Change password.
- [ sh, -c, 'echo "ubuntu:ubuntu" | chpasswd' ]
+ - [ bash, -c, '
+ if [ "$(id -u ubuntu)" -eq 1000 ]; then
+ echo "[cloud-init] Changing ubuntu UID/GID from 1000 to 2000";
+
+ # Change group first
+ groupmod -g 2000 ubuntu;
+
+ # Change user UID
+ usermod -u 2000 ubuntu;
+
+ # Fix ownership of files that still belong to uid/gid 1000
+ # Restrict to typical places to avoid scanning the whole fs if you prefer
+ for path in /home /var /etc; do
+ find "$path" -xdev -uid 1000 -exec chown -h 2000:2000 {} \; || true
+ find "$path" -xdev -gid 1000 -exec chgrp -h 2000 {} \; || true
+ done
+
+ echo "[cloud-init] ubuntu is now: $(id ubuntu)";
+ else
+ echo "[cloud-init] ubuntu UID is $(id -u ubuntu), not 1000; skipping UID change.";
+ fi
+ ' ]
# Make sure home is owned by ubuntu>
- [ chown, "-R", "ubuntu:ubuntu", "/home/ubuntu" ]
+ # Install rust.
+ - [ sudo, "-u", "ubuntu", "bash", "-lc", "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y" ]
# Clone Accless repo.
- - [ sudo, "-u", "ubuntu", "bash", "-lc", "cd /home/ubuntu && git clone https://github.com/faasm/accless.git accless" ]
+ # FIXME: remove branch name.
+ - [ sudo, "-u", "ubuntu", "bash", "-lc", "cd /home/ubuntu && git clone -b feature-escrow-func https://github.com/faasm/tless.git accless" ]
+ # Fetch Accless docker image.
+ - [ sudo, "-u", "ubuntu", "bash", "-lc", "docker pull ghcr.io/faasm/accless-experiments:${ACCLESS_VERSION}" ]
# Quick debug markers in console.
- [ sh, -c, 'echo "[cloud-init] Docker + chpasswd + sev-guest setup done"' ]
diff --git a/scripts/snp/run.sh b/scripts/snp/run.sh
index 3120b6b..d19292a 100755
--- a/scripts/snp/run.sh
+++ b/scripts/snp/run.sh
@@ -28,6 +28,7 @@ run_qemu() {
local qemu_bios_dir="${qemu_dir}/pc-bios"
local kernel="${OUTPUT_DIR}/vmlinuz-noble"
+ local initrd="${OUTPUT_DIR}/initrd-noble"
local ovmf="${OUTPUT_DIR}/ovmf/ovmf-${OVMF_VERSION}/Build/AmdSev/RELEASE_GCC5/FV/OVMF.fd"
local disk_image="${OUTPUT_DIR}/disk.img"
local seed_image="${OUTPUT_DIR}/seed.img"
@@ -43,9 +44,12 @@ run_qemu() {
-cpu EPYC-v4 \
-smp 6 -m 6G \
-bios ${ovmf} \
+ -kernel ${kernel} \
+ -append "root=/dev/vda1 console=ttyS0" \
+ -initrd ${initrd} \
-object memory-backend-memfd,id=ram1,size=6G,share=true,prealloc=false \
-machine memory-backend=ram1 \
- -object sev-snp-guest,id=sev0,cbitpos=${cbitpos},reduced-phys-bits=1 \
+ -object sev-snp-guest,id=sev0,cbitpos=${cbitpos},reduced-phys-bits=1,kernel-hashes=on \
-drive "if=virtio,format=qcow2,file=${disk_image}" \
-drive "if=virtio,format=raw,file=${seed_image}" \
-netdev user,id=net0,hostfwd=tcp::2222-:22 \
diff --git a/scripts/snp/setup.sh b/scripts/snp/setup.sh
index f0117cd..b53a652 100755
--- a/scripts/snp/setup.sh
+++ b/scripts/snp/setup.sh
@@ -4,6 +4,7 @@ set -euo pipefail
THIS_DIR="$(dirname "$(realpath "$0")")"
SCRIPTS_DIR="${THIS_DIR}/.."
+ROOT_DIR="${SCRIPTS_DIR}/.."
OUTPUT_DIR="${THIS_DIR}/output"
source "${SCRIPTS_DIR}/common/utils.sh"
@@ -78,7 +79,7 @@ build_ovmf() {
fetch_kernel() {
print_info "Fetching Linux kernel..."
wget \
- https://cloud-images.ubuntu.com/noble/20251113/unpacked/noble-server-cloudimg-amd64-vmlinuz-generic \
+ https://cloud-images.ubuntu.com/noble/${UBUNTU_VERSION}/unpacked/noble-server-cloudimg-amd64-vmlinuz-generic \
-O ${OUTPUT_DIR}/vmlinuz-noble > /dev/null 2>&1
if [ $? -eq 0 ]; then
print_success "Linux kernel fetched successfully."
@@ -86,6 +87,17 @@ fetch_kernel() {
print_error "Failed to fetch Linux kernel."
exit 1
fi
+
+ print_info "Fetching initrd..."
+ wget \
+ https://cloud-images.ubuntu.com/noble/${UBUNTU_VERSION}/unpacked/noble-server-cloudimg-amd64-initrd-generic \
+ -O ${OUTPUT_DIR}/initrd-noble > /dev/null 2>&1
+ if [ $? -eq 0 ]; then
+ print_success "Kernel initrd fetched successfully."
+ else
+ print_error "Failed to fetch kernel initrd."
+ exit 1
+ fi
}
#
@@ -129,7 +141,10 @@ prepare_cloudinit_image() {
mkdir -p ${out_dir}
INSTANCE_ID="accless-snp-$(date +%s)" envsubst '${INSTANCE_ID}' \
< ${in_dir}/meta-data.in > ${out_dir}/meta-data
- SSH_PUB_KEY=$(cat "${OUTPUT_DIR}/snp-key.pub") envsubst '${SSH_PUB_KEY}' \
+ ACCLESS_VERSION=$(cat "${ROOT_DIR}/VERSION") \
+ GUEST_KERNEL_VERSION=${GUEST_KERNEL_VERSION} \
+ SSH_PUB_KEY=$(cat "${OUTPUT_DIR}/snp-key.pub") \
+ envsubst '${ACCLESS_VERSION} ${GUEST_KERNEL_VERSION} ${SSH_PUB_KEY}' \
< ${in_dir}/user-data.in > ${out_dir}/user-data
cloud-localds "${OUTPUT_DIR}/seed.img" "${out_dir}/user-data" "${out_dir}/meta-data"
diff --git a/scripts/snp/versions.sh b/scripts/snp/versions.sh
index d6cbd54..36cbc8b 100644
--- a/scripts/snp/versions.sh
+++ b/scripts/snp/versions.sh
@@ -1,4 +1,6 @@
#!/bin/bash
+export GUEST_KERNEL_VERSION="6.8.0-87-generic"
export OVMF_VERSION="edk2-stable202511"
export QEMU_VERSION="10.1.2"
+export UBUNTU_VERSION="20251113"
From 3b0b05412eaffb9379ef499f7bed898fb1e13dbd Mon Sep 17 00:00:00 2001
From: Carlos Segarra
Date: Sat, 22 Nov 2025 16:11:05 +0000
Subject: [PATCH 08/52] [attestation-service] E: Log Node URL On Startup
---
attestation-service/src/main.rs | 1 +
attestation-service/src/tls.rs | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/attestation-service/src/main.rs b/attestation-service/src/main.rs
index 833e51e..ebe4d97 100644
--- a/attestation-service/src/main.rs
+++ b/attestation-service/src/main.rs
@@ -85,6 +85,7 @@ async fn main() -> Result<()> {
let listener = TcpListener::bind(addr).await;
info!("Accless attestation server running on https://{}", addr);
+ info!("External IP: {}", tls::get_node_url()?);
loop {
let (stream, _) = listener.as_ref().expect("error listening").accept().await?;
let service = TowerToHyperService::new(app.clone());
diff --git a/attestation-service/src/tls.rs b/attestation-service/src/tls.rs
index 52e3241..d7ee135 100644
--- a/attestation-service/src/tls.rs
+++ b/attestation-service/src/tls.rs
@@ -59,7 +59,7 @@ pub fn get_public_certificate_path(certs_dir: &Path) -> PathBuf {
/// # Returns
///
/// The external node IP as a string.
-fn get_node_url() -> Result {
+pub fn get_node_url() -> Result {
let output = Command::new("ip")
.args(["-o", "route", "get", "to", "8.8.8.8"])
.output()?;
From a4f5be7d3a3ac56217d7ba74f2b256c0de13ece0 Mon Sep 17 00:00:00 2001
From: Carlos Segarra
Date: Sat, 22 Nov 2025 16:11:27 +0000
Subject: [PATCH 09/52] [accli] E: Add --rebuild Flag To attestation-service
run
---
accli/src/main.rs | 5 +++++
accli/src/tasks/attestation_service.rs | 5 +++++
2 files changed, 10 insertions(+)
diff --git a/accli/src/main.rs b/accli/src/main.rs
index 3a4a8a1..33cca38 100644
--- a/accli/src/main.rs
+++ b/accli/src/main.rs
@@ -94,6 +94,9 @@ enum AttestationServiceCommand {
/// verification.
#[arg(long, default_value_t = false)]
mock: bool,
+ /// Rebuild the attestation service before running.
+ #[arg(long, default_value_t = false)]
+ rebuild: bool,
},
Health {
/// URL of the attestation service
@@ -811,6 +814,7 @@ async fn main() -> anyhow::Result<()> {
sgx_pccs_url,
force_clean_certs,
mock,
+ rebuild,
} => {
AttestationService::run(
certs_dir.as_deref(),
@@ -818,6 +822,7 @@ async fn main() -> anyhow::Result<()> {
sgx_pccs_url.as_deref(),
*force_clean_certs,
*mock,
+ *rebuild,
)?;
}
AttestationServiceCommand::Health { url, cert_path } => {
diff --git a/accli/src/tasks/attestation_service.rs b/accli/src/tasks/attestation_service.rs
index 9d17c7b..7ddf35f 100644
--- a/accli/src/tasks/attestation_service.rs
+++ b/accli/src/tasks/attestation_service.rs
@@ -29,7 +29,12 @@ impl AttestationService {
sgx_pccs_url: Option<&std::path::Path>,
force_clean_certs: bool,
mock: bool,
+ rebuild: bool,
) -> Result<()> {
+ if rebuild {
+ Self::build()?;
+ }
+
let mut cmd = Command::new(
Env::proj_root()
.join("target")
From 1d429a33665588ae2d8567c167f1970ba9c55343 Mon Sep 17 00:00:00 2001
From: Carlos Segarra
Date: Sat, 22 Nov 2025 16:30:54 +0000
Subject: [PATCH 10/52] [accli] E: Pass /dev/sev-guest To Docker If Available
---
accli/src/tasks/docker.rs | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/accli/src/tasks/docker.rs b/accli/src/tasks/docker.rs
index 7617bcf..fb26d44 100644
--- a/accli/src/tasks/docker.rs
+++ b/accli/src/tasks/docker.rs
@@ -268,6 +268,10 @@ impl Docker {
run_cmd.arg("--network").arg("host");
}
+ if Path::new("/dev/sev-guest").exists() {
+ run_cmd.arg("--device=/dev/sev-guest");
+ }
+
if mount {
run_cmd
.arg("-v")
From 23c96fbd76a9ad8ddfcf3a280997b0c64bb70af5 Mon Sep 17 00:00:00 2001
From: Carlos Segarra
Date: Sat, 22 Nov 2025 16:40:18 +0000
Subject: [PATCH 11/52] [applications] E: Add Preliminary Escrow-Xput Function
---
applications/CMakeLists.txt | 4 +-
applications/functions/CMakeLists.txt | 1 +
.../functions/escrow-xput/CMakeLists.txt | 10 +
.../functions/escrow-xput/include/logger.h | 9 +
.../functions/escrow-xput/include/utils.h | 5 +
.../functions/escrow-xput/src/logger.cpp | 22 +
.../functions/escrow-xput/src/main.cpp | 604 ++++++++++++++++++
.../functions/escrow-xput/src/utils.cpp | 14 +
8 files changed, 666 insertions(+), 3 deletions(-)
create mode 100644 applications/functions/CMakeLists.txt
create mode 100644 applications/functions/escrow-xput/CMakeLists.txt
create mode 100644 applications/functions/escrow-xput/include/logger.h
create mode 100644 applications/functions/escrow-xput/include/utils.h
create mode 100644 applications/functions/escrow-xput/src/logger.cpp
create mode 100644 applications/functions/escrow-xput/src/main.cpp
create mode 100644 applications/functions/escrow-xput/src/utils.cpp
diff --git a/applications/CMakeLists.txt b/applications/CMakeLists.txt
index 18c146b..4d23bfb 100644
--- a/applications/CMakeLists.txt
+++ b/applications/CMakeLists.txt
@@ -31,11 +31,9 @@ set(ACCLESS_HEADERS ${ACCLESS_INCLUDE_PREFIX_DIR})
# Application subdirectories
# =============================================================================
-# Test applications.
if (NOT CMAKE_SYSTEM_NAME STREQUAL "WASI")
+ add_subdirectory(./functions)
add_subdirectory(./test)
endif ()
# To-Do: add workflows
-
-# To-Do: add functions
diff --git a/applications/functions/CMakeLists.txt b/applications/functions/CMakeLists.txt
new file mode 100644
index 0000000..71c5603
--- /dev/null
+++ b/applications/functions/CMakeLists.txt
@@ -0,0 +1 @@
+add_subdirectory(./escrow-xput)
diff --git a/applications/functions/escrow-xput/CMakeLists.txt b/applications/functions/escrow-xput/CMakeLists.txt
new file mode 100644
index 0000000..dccda16
--- /dev/null
+++ b/applications/functions/escrow-xput/CMakeLists.txt
@@ -0,0 +1,10 @@
+set(CMAKE_PROJECT_TARGET escrow-xput)
+
+add_executable(${CMAKE_PROJECT_TARGET}
+ src/main.cpp
+ # src/logger.cpp
+ # src/utils.cpp
+)
+
+target_include_directories(${CMAKE_PROJECT_TARGET} PRIVATE ${ACCLESS_HEADERS})
+target_link_libraries(${CMAKE_PROJECT_TARGET} PRIVATE accless::accless)
diff --git a/applications/functions/escrow-xput/include/logger.h b/applications/functions/escrow-xput/include/logger.h
new file mode 100644
index 0000000..d6803f1
--- /dev/null
+++ b/applications/functions/escrow-xput/include/logger.h
@@ -0,0 +1,9 @@
+#pragma once
+
+#include
+
+class Logger : public attest::AttestationLogger {
+ public:
+ void Log(const char *log_tag, LogLevel level, const char *function,
+ const int line, const char *fmt, ...);
+};
diff --git a/applications/functions/escrow-xput/include/utils.h b/applications/functions/escrow-xput/include/utils.h
new file mode 100644
index 0000000..c410975
--- /dev/null
+++ b/applications/functions/escrow-xput/include/utils.h
@@ -0,0 +1,5 @@
+#pragma once
+
+#include
+
+std::string base64decode(const std::string &data);
diff --git a/applications/functions/escrow-xput/src/logger.cpp b/applications/functions/escrow-xput/src/logger.cpp
new file mode 100644
index 0000000..b6df46f
--- /dev/null
+++ b/applications/functions/escrow-xput/src/logger.cpp
@@ -0,0 +1,22 @@
+#include "logger.h"
+
+#include
+#include
+#include
+
+void Logger::Log(const char *log_tag, LogLevel level, const char *function,
+ const int line, const char *fmt, ...) {
+ va_list args;
+ va_start(args, fmt);
+ size_t len = std::vsnprintf(NULL, 0, fmt, args);
+ va_end(args);
+
+ std::vector str(len + 1);
+
+ va_start(args, fmt);
+ std::vsnprintf(&str[0], len + 1, fmt, args);
+ va_end(args);
+
+ // Uncomment for debug logs
+ // std::cout << std::string(str.begin(), str.end()) << std::endl;
+}
diff --git a/applications/functions/escrow-xput/src/main.cpp b/applications/functions/escrow-xput/src/main.cpp
new file mode 100644
index 0000000..c3a590a
--- /dev/null
+++ b/applications/functions/escrow-xput/src/main.cpp
@@ -0,0 +1,604 @@
+#include "accless/abe4/abe4.h"
+#include "accless/attestation/attestation.h"
+#include "accless/jwt/jwt.h"
+
+#include
+
+/*
+#include "AttestationClient.h"
+#include "AttestationClientImpl.h"
+#include "AttestationParameters.h"
+#include "HclReportParser.h"
+#include "TpmCertOperations.h"
+
+#include "logger.h"
+#include "tless_abe.h"
+#include "utils.h"
+
+using json = nlohmann::json;
+
+using namespace attest;
+
+std::vector split(const std::string &str, char delim) {
+ std::vector result;
+ std::stringstream ss(str);
+ std::string token;
+
+ while (std::getline(ss, token, delim)) {
+ result.push_back(token);
+ }
+
+ return result;
+}
+
+// Get the URL of our own attestation service (**not** MAA)
+std::string getAttestationServiceUrl() {
+ const char *val = std::getenv("AS_URL");
+ return val ? std::string(val) : "https://127.0.0.1:8443";
+}
+
+void tpmRenewAkCert() {
+ TpmCertOperations tpmCertOps;
+ bool renewalRequired = false;
+ auto result = tpmCertOps.IsAkCertRenewalRequired(renewalRequired);
+ if (result.code_ != AttestationResult::ErrorCode::SUCCESS) {
+ std::cerr << "accless: error checking AkCert renewal state"
+ << std::endl;
+
+ if (result.tpm_error_code_ != 0) {
+ std::cerr << "accless: internal TPM error occured: "
+ << result.description_ << std::endl;
+ throw std::runtime_error("internal TPM error");
+ } else if (result.code_ == attest::AttestationResult::ErrorCode::
+ ERROR_AK_CERT_PROVISIONING_FAILED) {
+ std::cerr << "accless: attestation key cert provisioning delayed"
+ << std::endl;
+ throw std::runtime_error("internal TPM error");
+ }
+ }
+
+ if (renewalRequired) {
+ auto replaceResult = tpmCertOps.RenewAndReplaceAkCert();
+ if (replaceResult.code_ != AttestationResult::ErrorCode::SUCCESS) {
+ std::cerr << "accless: failed to renew AkCert: "
+ << result.description_ << std::endl;
+ throw std::runtime_error("accless: internal TPM error");
+ }
+ }
+}
+
+AttestationResult parseClientPayload(
+ const unsigned char *clientPayload,
+ std::unordered_map &clientPayloadMap) {
+ AttestationResult result(AttestationResult::ErrorCode::SUCCESS);
+ assert(clientPayload != nullptr);
+
+ Json::Value root;
+ Json::Reader reader;
+ std::string clientPayloadStr(
+ const_cast(reinterpret_cast(clientPayload)));
+ bool success = reader.parse(clientPayloadStr, root);
+ if (!success) {
+ std::cout << "accless: error parsing the client payload JSON"
+ << std::endl;
+ result.code_ =
+ AttestationResult::ErrorCode::ERROR_INVALID_INPUT_PARAMETER;
+ result.description_ = std::string("Invalid client payload Json");
+ return result;
+ }
+
+ for (Json::Value::iterator it = root.begin(); it != root.end(); ++it) {
+ clientPayloadMap[it.key().asString()] = it->asString();
+ }
+
+ return result;
+}
+
+AttestationParameters
+getAzureAttestationParameters(AttestationClient *attestationClient) {
+ std::string attestationUrl = "https://accless.eus.attest.azure.net";
+
+ // Client parameters
+ attest::ClientParameters clientParams = {};
+ clientParams.attestation_endpoint_url =
+ (unsigned char *)attestationUrl.c_str();
+ // TODO: can we add a public key here?
+ std::string nonce = "foo";
+ std::string clientPayload = "{\"nonce\":\"" + nonce + "\"}";
+ clientParams.client_payload = (unsigned char *)clientPayload.c_str();
+ clientParams.version = CLIENT_PARAMS_VERSION;
+
+ AttestationParameters params = {};
+ std::unordered_map clientPayloadMap;
+ if (clientParams.client_payload != nullptr) {
+ auto result =
+ parseClientPayload(clientParams.client_payload, clientPayloadMap);
+ if (result.code_ != AttestationResult::ErrorCode::SUCCESS) {
+ std::cout << "accless: error parsing client payload" << std::endl;
+ throw std::runtime_error("error parsing client payload");
+ }
+ }
+
+ auto result = ((AttestationClientImpl *)attestationClient)
+ ->getAttestationParameters(clientPayloadMap, params);
+ if (result.code_ != AttestationResult::ErrorCode::SUCCESS) {
+ std::cout << "accless: failed to get attestation parameters"
+ << std::endl;
+ throw std::runtime_error("failed to get attestation parameters");
+ }
+
+ return params;
+}
+
+std::string maaGetJwtFromParams(AttestationClient *attestationClient,
+ const AttestationParameters ¶ms,
+ const std::string &attestationUri) {
+ bool is_cvm = false;
+ bool attestation_success = true;
+ std::string jwt_str;
+
+ unsigned char *jwt = nullptr;
+ auto attResult = ((AttestationClientImpl *)attestationClient)
+ ->Attest(params, attestationUri, &jwt);
+ if (attResult.code_ != attest::AttestationResult::ErrorCode::SUCCESS) {
+ std::cerr
+ << "accless: error getting attestation from attestation client"
+ << std::endl;
+ Uninitialize();
+ throw std::runtime_error(
+ "failed to get attestation from attestation client");
+ }
+
+ std::string jwtStr = reinterpret_cast(jwt);
+ attestationClient->Free(jwt);
+
+ return jwtStr;
+}
+
+size_t curlWriteCallback(char *ptr, size_t size, size_t nmemb, void *userdata) {
+ size_t totalSize = size * nmemb;
+ auto *response = static_cast(userdata);
+ response->append(ptr, totalSize);
+ return totalSize;
+}
+
+std::string asGetJwtFromReport(const std::string &asUrl,
+ const std::vector &snpReport) {
+ std::string jwt;
+
+ CURL *curl = curl_easy_init();
+ if (!curl) {
+ std::cerr << "accless: failed to initialize CURL" << std::endl;
+ throw std::runtime_error("curl error");
+ }
+
+ curl_easy_setopt(curl, CURLOPT_URL, asUrl.c_str());
+ curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
+ curl_easy_setopt(
+ curl, CURLOPT_CAINFO,
+ "/home/tless/git/faasm/tless/attestation-service/certs/cert.pem");
+ curl_easy_setopt(curl, CURLOPT_POST, 1L);
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, snpReport.data());
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, snpReport.size());
+
+ struct curl_slist *headers = nullptr;
+ headers =
+ curl_slist_append(headers, "Content-Type: application/octet-stream");
+ curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
+
+ // Set write function and data
+ curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlWriteCallback);
+ curl_easy_setopt(curl, CURLOPT_WRITEDATA, &jwt);
+
+ // Perform the request
+ CURLcode res = curl_easy_perform(curl);
+ if (res != CURLE_OK) {
+ std::cerr << "accless: CURL error: " << curl_easy_strerror(res)
+ << std::endl;
+ curl_easy_cleanup(curl);
+ curl_slist_free_all(headers);
+ throw std::runtime_error("curl error");
+ }
+
+ curl_easy_cleanup(curl);
+ curl_slist_free_all(headers);
+
+ return jwt;
+}
+
+void validateJwtClaims(const std::string &jwtStr, bool verbose = false) {
+ // Prase attestation token to extract isolation tee details
+ auto tokens = split(jwtStr, '.');
+ if (tokens.size() < 3) {
+ std::cerr << "accless: error validating jwt: not enough tokens"
+ << std::endl;
+ throw std::runtime_error("accless: error validating jwt");
+ }
+
+ json attestationClaims = json::parse(base64decode(tokens[1]));
+ std::string attestationType;
+ std::string complianceStatus;
+ try {
+ attestationType =
+ attestationClaims["x-ms-isolation-tee"]["x-ms-attestation-type"]
+ .get();
+ complianceStatus =
+ attestationClaims["x-ms-isolation-tee"]["x-ms-compliance-status"]
+ .get();
+ } catch (...) {
+ std::cerr << "accless: jwt does not have the expected claims"
+ << std::endl;
+ throw std::runtime_error("accless: error validating jwt");
+ }
+
+ if (!((attestationType == "sevsnpvm") &&
+ (complianceStatus == "azure-compliant-cvm"))) {
+ std::cerr << "accless: jwt validation does not pass" << std::endl;
+ }
+
+ if (verbose) {
+ std::cout << "accless: jwt validation passed" << std::endl;
+ }
+}
+
+std::vector getSnpReportFromTPM() {
+ // First, get HCL report
+ Tpm tpm;
+ Buffer hclReport = tpm.GetHCLReport();
+
+ Buffer snpReport;
+ Buffer runtimeData;
+ HclReportParser reportParser;
+
+ auto result = reportParser.ExtractSnpReportAndRuntimeDataFromHclReport(
+ hclReport, snpReport, runtimeData);
+ if (result.code_ != AttestationResult::ErrorCode::SUCCESS) {
+ std::cerr << "accless: error parsing snp report from HCL report"
+ << std::endl;
+ throw std::runtime_error("error parsing HCL report");
+ }
+
+ return snpReport;
+}
+
+void decrypt(const std::string &jwtStr, tless::abe::CpAbeContextWrapper &ctx,
+ std::vector &cipherText, bool compare = false) {
+ // TODO: in theory, the attributes should be extracted frm the JWT
+ std::vector attributes = {"foo", "bar"};
+
+ auto actualPlainText = ctx.cpAbeDecrypt(attributes, cipherText);
+ if (actualPlainText.empty()) {
+ std::cerr << "accless: error decrypting cipher-text" << std::endl;
+ throw std::runtime_error("error decrypting secret");
+ }
+
+ if (compare) {
+ // Compare
+ std::string plainText =
+ "dance like no one's watching, encrypt like everyone is!";
+ std::string actualPlainTextStr;
+ actualPlainTextStr.assign(
+ reinterpret_cast(actualPlainText.data()),
+ actualPlainText.size());
+ if (actualPlainTextStr == plainText) {
+ std::cout << "accless: key-release succeeded" << std::endl;
+ }
+ std::cout << "accless: actual plain-text: " << actualPlainTextStr
+ << std::endl;
+ }
+}
+
+// TODO: do another benchmark where we query our attestation service instead,
+// and compare it with the MAA
+std::chrono::duration runRequests(int numRequests, int maxParallelism,
+ bool maa = false) {
+ // ---------------------- Set Up CP-ABE -----------------------------------
+
+ // Initialize CP-ABE ctx and create a sample secret
+ auto &ctx = tless::abe::CpAbeContextWrapper::get(
+ tless::abe::ContextFetchMode::Create);
+ std::string plainText =
+ "dance like no one's watching, encrypt like everyone is!";
+ std::string policy = "\"foo\" and \"bar\"";
+ auto cipherText = ctx.cpAbeEncrypt(policy, plainText);
+
+ // Renew vTPM certificates if needed
+ tpmRenewAkCert();
+
+ // ----------------------- Benchmark -------------------------------------
+
+ std::counting_semaphore semaphore(maxParallelism);
+ std::vector threads;
+ auto start = std::chrono::steady_clock::now();
+
+ if (maa) {
+ // FIXME: the MAA benchmark has some spurious race conditions
+
+ std::string attestationUri = "https://accless.eus.attest.azure.net";
+
+ // Initialize Azure Attestation client
+ AttestationClient *attestationClient = nullptr;
+ Logger *logHandle = new Logger();
+ if (!Initialize(logHandle, &attestationClient)) {
+ std::cerr << "accless: failed to create attestation client object"
+ << std::endl;
+ Uninitialize();
+ throw std::runtime_error(
+ "failed to create attestation client object");
+ }
+
+ // Fetching the vTPM measurements is not thread-safe, but would happen
+ // in each client anyway, so we execute it only once, but still measure
+ // the time it takes
+ auto attParams = getAzureAttestationParameters(attestationClient);
+
+ // In the loop, to measure scalability, we only send the HW report for
+ // validation with the attestation service (be it Azure or our own att.
+ // service)
+ for (int i = 1; i < numRequests; ++i) {
+ semaphore.acquire();
+ threads.emplace_back([&semaphore, attestationClient, &attParams,
+ attestationUri]() {
+ // Validate some of the claims in the JWT
+ auto jwtStr = maaGetJwtFromParams(attestationClient, attParams,
+ attestationUri);
+
+ // TODO: validate JWT signature
+
+ // TODO: somehow get the public key from the JWT
+ validateJwtClaims(jwtStr);
+
+ // Release semaphore
+ semaphore.release();
+ });
+ }
+
+ // Do it once from the main thread to store the return value for
+ // decryption
+ auto jwtStr =
+ maaGetJwtFromParams(attestationClient, attParams, attestationUri);
+
+ for (auto &t : threads) {
+ if (t.joinable()) {
+ t.join();
+ }
+ }
+
+ // Similarly, the decrypt stage is compute-bound, so by running many
+ // instances in parallel we are saturating the local CPU. This step is
+ // fully distributed, so no issue with running it just once
+ decrypt(jwtStr, ctx, cipherText);
+
+ Uninitialize();
+ } else {
+ std::string asUrl = getAttestationServiceUrl();
+
+ // Fetching the vTPM measurements is not thread-safe, but would happen
+ // in each client anyway, so we execute it only once, but still measure
+ // the time it takes
+ auto snpReport = getSnpReportFromTPM();
+
+ // In the loop, to measure scalability, we only send the HW report for
+ // validation with the attestation service (be it Azure or our own att.
+ // service)
+ for (int i = 1; i < numRequests; ++i) {
+ semaphore.acquire();
+ threads.emplace_back([&semaphore, &asUrl, &snpReport]() {
+ // Get a JWT from the attestation service if report valid
+ auto jwtStr =
+ asGetJwtFromReport(asUrl + "/verify-snp-report", snpReport);
+
+ // TODO: somehow get the public key from the JWT
+ // TODO: validate some claims in the JWT
+
+ // Release semaphore
+ semaphore.release();
+ });
+ }
+
+ // Do it once from the main thread to store the return value for
+ // decryption
+ auto jwtStr =
+ asGetJwtFromReport(asUrl + "/verify-snp-report", snpReport);
+
+ for (auto &t : threads) {
+ if (t.joinable()) {
+ t.join();
+ }
+ }
+
+ // Similarly, the decrypt stage is compute-bound, so by running many
+ // instances in parallel we are saturating the local CPU. This step is
+ // fully distributed, so no issue with running it just once
+ decrypt(jwtStr, ctx, cipherText);
+ }
+
+ auto end = std::chrono::steady_clock::now();
+ std::chrono::duration elapsedSecs = end - start;
+ std::cout << "Elapsed time (" << numRequests << "): " << elapsedSecs.count()
+ << " seconds\n";
+
+ return elapsedSecs;
+}
+
+void doBenchmark(bool maa = false) {
+ // Write elapsed time to CSV
+ std::string fileName = maa ? "accless-maa.csv" : "accless.csv";
+ std::ofstream csvFile(fileName, std::ios::out);
+ csvFile << "NumRequests,TimeElapsed\n";
+
+ // WARNING: this is copied from invrs/src/tasks/ubench.rs and must be
+ // kept in sync!
+ std::vector numRequests = {1, 10, 50, 100, 200, 400, 600, 800, 1000};
+ int numRepeats = maa ? 1 : 3;
+ int maxParallelism = 100;
+ try {
+ for (const auto &i : numRequests) {
+ for (int j = 0; j < numRepeats; j++) {
+ auto elapsedTimeSecs = runRequests(i, maxParallelism, maa);
+ csvFile << i << "," << elapsedTimeSecs.count() << '\n';
+ }
+ }
+ } catch (...) {
+ std::cout << "accless: error running benchmark" << std::endl;
+ }
+
+ csvFile.close();
+}
+
+void runOnce(bool maa = false) {
+ // Renew TPM certificates if needed
+ tpmRenewAkCert();
+
+ // Initialize CP-ABE ctx
+ auto &ctx = tless::abe::CpAbeContextWrapper::get(
+ tless::abe::ContextFetchMode::Create);
+ std::string plainText =
+ "dance like no one's watching, encrypt like everyone is!";
+ std::string policy = "\"foo\" and \"bar\"";
+ auto cipherText = ctx.cpAbeEncrypt(policy, plainText);
+
+ std::string jwtStr;
+ if (maa) {
+ // TODO: attest MAA
+ std::string attestationUri = "https://accless.eus.attest.azure.net";
+
+ // Initialize Azure Attestation client
+ AttestationClient *attestationClient = nullptr;
+ Logger *logHandle = new Logger();
+ if (!Initialize(logHandle, &attestationClient)) {
+ std::cerr << "accless: failed to create attestation client object"
+ << std::endl;
+ Uninitialize();
+ throw std::runtime_error(
+ "failed to create attestation client object");
+ }
+
+ auto attParams = getAzureAttestationParameters(attestationClient);
+ jwtStr =
+ maaGetJwtFromParams(attestationClient, attParams, attestationUri);
+ validateJwtClaims(jwtStr);
+
+ Uninitialize();
+ } else {
+ std::string asUrl = getAttestationServiceUrl();
+
+ // TODO: attest AS
+
+ auto snpReport = getSnpReportFromTPM();
+ jwtStr = asGetJwtFromReport(asUrl + "/verify-snp-report", snpReport);
+ std::cout << "out: " << jwtStr << std::endl;
+ }
+
+ // TODO: jwtStr is now a JWE, so we must decrypt it
+
+ decrypt(jwtStr, ctx, cipherText);
+}
+*/
+
+/**
+ * @brief Performs a single secret-key-release operation using Accless.
+ *
+ * This function is the main body of Accless secret-key-release operation. It
+ * relies on an instance of the attestation-service running, and on being
+ * deployed in a genuine SNP cVM. Either in a para-virtualized environment on
+ * Azure, or on bare-metal.
+ */
+int doAcclessSkr() {
+ // Get the ID and MPK we need to encrypt ciphertexts with attributes from
+ // this attestation service instance.
+ auto [id, partialMpk] = accless::attestation::getAttestationServiceState();
+ std::cout << "escrow-xput: got attesation service's state" << std::endl;
+ std::string mpk = accless::abe4::packFullKey({id}, {partialMpk});
+ std::cout << "escrow-xput: packed partial MPK into full MPK" << std::endl;
+
+ std::string gid = "baz";
+ std::string wfId = "foo";
+ std::string nodeId = "bar";
+
+ // Pick the simplest policy that only relies on the attributes `wf` and
+ // `node` which are provided by the attestation-service after a succesful
+ // remote attestation.
+ std::string policy = id + ".wf:" + wfId + " & " + id + ".node:" + nodeId;
+
+ // Generate a test ciphertext that only us, after a succesful attestation,
+ // should be able to decrypt.
+ std::cout << "escrow-xput: encrypting cp-abe with policy: " << policy
+ << std::endl;
+ auto [gt, ct] = accless::abe4::encrypt(mpk, policy);
+ if (gt.empty() || ct.empty()) {
+ std::cerr << "escrow-xput: error running cp-abe encryption"
+ << std::endl;
+ return 1;
+ }
+ std::cout << "escrow-xput: ran CP-ABE encryption" << std::endl;
+
+ // TODO: Should start measuring now!
+ std::cout << "escrow-xput: running remote attestation..." << std::endl;
+ try {
+ const std::string jwt =
+ accless::attestation::snp::getAttestationJwt(gid, wfId, nodeId);
+ if (jwt.empty()) {
+ std::cerr << "escrow-xput: empty JWT returned" << std::endl;
+ return 1;
+ }
+
+ if (!accless::jwt::verify(jwt)) {
+ std::cerr << "escrow-xput: JWT signature verification failed"
+ << std::endl;
+ return 1;
+ }
+
+ // Get the partial USK from the JWT, and wrap it in a full key for
+ // CP-ABE decryption.
+ std::string partialUskB64 =
+ accless::jwt::getProperty(jwt, "partial_usk_b64");
+ if (partialUskB64.empty()) {
+ std::cerr
+ << "att-client-snp: JWT is missing 'partial_usk_b64' field"
+ << std::endl;
+ return 1;
+ }
+ std::string uskB64 = accless::abe4::packFullKey({id}, {partialUskB64});
+
+ // Run decryption.
+ std::optional decrypted_gt =
+ accless::abe4::decrypt(uskB64, gid, policy, ct);
+ if (!decrypted_gt.has_value()) {
+ std::cerr << "att-client-snp: CP-ABE decryption failed"
+ << std::endl;
+ return 1;
+ } else if (decrypted_gt.value() != gt) {
+ std::cerr << "att-client-snp: CP-ABE decrypted ciphertexts do not"
+ << " match!" << std::endl;
+ std::cerr << "att-client-snp: Original GT: " << gt << std::endl;
+ std::cerr << "att-client-snp: Decrypted GT: "
+ << decrypted_gt.value() << std::endl;
+ return 1;
+ }
+
+ // End of experiment.
+ } catch (const std::exception &ex) {
+ std::cerr << "escrow-xput: error: " << ex.what() << std::endl;
+ } catch (...) {
+ std::cerr << "escrow-xput: unexpected error" << std::endl;
+ }
+
+ std::cout << "escrow-xput: experiment succesful" << std::endl;
+ return 0;
+}
+
+int main(int argc, char **argv) {
+ bool maa = ((argc == 2) && (std::string(argv[1]) == "--maa"));
+ bool once = ((argc == 2) && (std::string(argv[1]) == "--once"));
+
+ doAcclessSkr();
+
+ /*
+ if (once) {
+ runOnce();
+ } else {
+ doBenchmark(maa);
+ }
+ */
+}
diff --git a/applications/functions/escrow-xput/src/utils.cpp b/applications/functions/escrow-xput/src/utils.cpp
new file mode 100644
index 0000000..eec2fff
--- /dev/null
+++ b/applications/functions/escrow-xput/src/utils.cpp
@@ -0,0 +1,14 @@
+#include "utils.h"
+
+#include
+#include
+#include
+
+std::string base64decode(const std::string &data) {
+ using namespace boost::archive::iterators;
+ using It =
+ transform_width, 8, 6>;
+ return boost::algorithm::trim_right_copy_if(
+ std::string(It(std::begin(data)), It(std::end(data))),
+ [](char c) { return c == '\0'; });
+}
From f4dc3933b19646752d8f1af8eabe9bc00e3c1021 Mon Sep 17 00:00:00 2001
From: Carlos Segarra
Date: Mon, 24 Nov 2025 15:41:45 +0000
Subject: [PATCH 12/52] [attestation-service] E: Verify Bare-Metal SNP Reports
Closes #25
---
Cargo.lock | 210 +++++++++---------------------
Cargo.toml | 2 +-
GEMINI.md | 13 +-
attestation-service/Cargo.toml | 3 +-
attestation-service/src/amd.rs | 122 ++++++++++++++++++
attestation-service/src/lib.rs | 24 ++--
attestation-service/src/main.rs | 4 +-
attestation-service/src/sgx.rs | 2 +-
attestation-service/src/snp.rs | 212 +++++++++++++++++++++++++++++--
attestation-service/src/state.rs | 52 ++++++--
10 files changed, 455 insertions(+), 189 deletions(-)
create mode 100644 attestation-service/src/amd.rs
diff --git a/Cargo.lock b/Cargo.lock
index d2a03b2..d407361 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -475,11 +475,11 @@ dependencies = [
"serde",
"serde_json",
"serial_test",
+ "sev",
"snpguest",
"tempfile",
"tokio",
"tokio-rustls",
- "ureq",
]
[[package]]
@@ -537,7 +537,7 @@ dependencies = [
"serde_json",
"serde_path_to_error",
"serde_urlencoded",
- "sync_wrapper 1.0.2",
+ "sync_wrapper",
"tokio",
"tower",
"tower-layer",
@@ -560,7 +560,7 @@ dependencies = [
"mime",
"pin-project-lite",
"rustversion",
- "sync_wrapper 1.0.2",
+ "sync_wrapper",
"tower-layer",
"tower-service",
"tracing",
@@ -572,6 +572,12 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
+[[package]]
+name = "base64"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+
[[package]]
name = "base64"
version = "0.21.7"
@@ -625,6 +631,26 @@ version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c821a6e124197eb56d907ccc2188eab1038fb919c914f47976e64dd8dbc855d1"
+[[package]]
+name = "bitfield"
+version = "0.19.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21ba6517c6b0f2bf08be60e187ab64b038438f22dd755614d8fe4d4098c46419"
+dependencies = [
+ "bitfield-macros",
+]
+
+[[package]]
+name = "bitfield-macros"
+version = "0.19.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f48d6ace212fdf1b45fd6b566bb40808415344642b76c3224c07c8df9da81e97"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.108",
+]
+
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -876,12 +902,6 @@ dependencies = [
"cc",
]
-[[package]]
-name = "codicon"
-version = "3.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "12170080f3533d6f09a19f81596f836854d0fa4867dc32c8172b8474b4e9de61"
-
[[package]]
name = "color_quant"
version = "1.1.0"
@@ -1277,34 +1297,13 @@ dependencies = [
"subtle",
]
-[[package]]
-name = "dirs"
-version = "5.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
-dependencies = [
- "dirs-sys 0.4.1",
-]
-
[[package]]
name = "dirs"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
dependencies = [
- "dirs-sys 0.5.0",
-]
-
-[[package]]
-name = "dirs-sys"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
-dependencies = [
- "libc",
- "option-ext",
- "redox_users 0.4.6",
- "windows-sys 0.48.0",
+ "dirs-sys",
]
[[package]]
@@ -1315,7 +1314,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
dependencies = [
"libc",
"option-ext",
- "redox_users 0.5.2",
+ "redox_users",
"windows-sys 0.61.2",
]
@@ -1546,7 +1545,7 @@ dependencies = [
"core-foundation",
"core-graphics",
"core-text",
- "dirs 6.0.0",
+ "dirs",
"dwrote",
"float-ord",
"freetype-sys",
@@ -2109,7 +2108,7 @@ dependencies = [
"tokio",
"tokio-rustls",
"tower-service",
- "webpki-roots 1.0.3",
+ "webpki-roots",
]
[[package]]
@@ -2160,7 +2159,7 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"socket2 0.6.1",
- "system-configuration 0.6.1",
+ "system-configuration",
"tokio",
"tower-service",
"tracing",
@@ -2370,7 +2369,7 @@ dependencies = [
"socket2 0.5.10",
"widestring",
"windows-sys 0.48.0",
- "winreg",
+ "winreg 0.50.0",
]
[[package]]
@@ -3446,17 +3445,6 @@ dependencies = [
"bitflags 2.10.0",
]
-[[package]]
-name = "redox_users"
-version = "0.4.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
-dependencies = [
- "getrandom 0.2.16",
- "libredox",
- "thiserror 1.0.69",
-]
-
[[package]]
name = "redox_users"
version = "0.5.2"
@@ -3499,11 +3487,11 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "reqwest"
-version = "0.11.27"
+version = "0.11.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
+checksum = "46a1f7aa4f35e5e8b4160449f51afc758f0ce6454315a9fa7d0d113e958c41eb"
dependencies = [
- "base64 0.21.7",
+ "base64 0.13.1",
"bytes",
"encoding_rs",
"futures-core",
@@ -3515,26 +3503,22 @@ dependencies = [
"hyper-tls 0.5.0",
"ipnet",
"js-sys",
+ "lazy_static",
"log",
"mime",
"native-tls",
- "once_cell",
"percent-encoding",
"pin-project-lite",
- "rustls-pemfile",
"serde",
"serde_json",
"serde_urlencoded",
- "sync_wrapper 0.1.2",
- "system-configuration 0.5.1",
"tokio",
"tokio-native-tls",
- "tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
- "winreg",
+ "winreg 0.10.1",
]
[[package]]
@@ -3571,7 +3555,7 @@ dependencies = [
"serde",
"serde_json",
"serde_urlencoded",
- "sync_wrapper 1.0.2",
+ "sync_wrapper",
"tokio",
"tokio-native-tls",
"tokio-rustls",
@@ -3584,7 +3568,7 @@ dependencies = [
"wasm-bindgen-futures",
"wasm-streams",
"web-sys",
- "webpki-roots 1.0.3",
+ "webpki-roots",
]
[[package]]
@@ -3853,15 +3837,6 @@ dependencies = [
"serde_derive",
]
-[[package]]
-name = "serde-big-array"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f"
-dependencies = [
- "serde",
-]
-
[[package]]
name = "serde-human-bytes"
version = "0.1.1"
@@ -3872,16 +3847,6 @@ dependencies = [
"serde",
]
-[[package]]
-name = "serde_bytes"
-version = "0.11.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8"
-dependencies = [
- "serde",
- "serde_core",
-]
-
[[package]]
name = "serde_core"
version = "1.0.228"
@@ -3979,26 +3944,21 @@ dependencies = [
[[package]]
name = "sev"
-version = "5.0.0"
+version = "7.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b06afe5192a43814047ea0072f4935f830a1de3c8cb43b56c90ae6918468b94d"
+checksum = "c2ff74d7e7d1cc172f3a45adec74fbeee928d71df095b85aaaf66eb84e1e31e6"
dependencies = [
"base64 0.22.1",
- "bincode",
- "bitfield",
- "bitflags 1.3.2",
+ "bitfield 0.19.4",
+ "bitflags 2.10.0",
"byteorder",
- "codicon",
- "dirs 5.0.1",
+ "dirs",
"hex",
"iocuddle",
"lazy_static",
"libc",
"openssl",
"rdrand",
- "serde",
- "serde-big-array",
- "serde_bytes",
"static_assertions",
"uuid",
]
@@ -4037,7 +3997,7 @@ version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb"
dependencies = [
- "dirs 6.0.0",
+ "dirs",
]
[[package]]
@@ -4118,14 +4078,13 @@ dependencies = [
[[package]]
name = "snpguest"
-version = "0.8.3"
-source = "git+https://github.com/faasm/snpguest.git#d3697058d4981db9c8dca1f6321cab3ca5cb029d"
+version = "0.10.0"
dependencies = [
"anyhow",
"asn1-rs",
"base64 0.22.1",
"bincode",
- "bitfield",
+ "bitfield 0.15.0",
"clap",
"clap-num",
"colorful",
@@ -4135,7 +4094,7 @@ dependencies = [
"nix",
"openssl",
"rand 0.8.5",
- "reqwest 0.11.27",
+ "reqwest 0.11.10",
"serde",
"sev",
"x509-parser",
@@ -4223,12 +4182,6 @@ dependencies = [
"unicode-ident",
]
-[[package]]
-name = "sync_wrapper"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
-
[[package]]
name = "sync_wrapper"
version = "1.0.2"
@@ -4249,17 +4202,6 @@ dependencies = [
"syn 2.0.108",
]
-[[package]]
-name = "system-configuration"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
-dependencies = [
- "bitflags 1.3.2",
- "core-foundation",
- "system-configuration-sys 0.5.0",
-]
-
[[package]]
name = "system-configuration"
version = "0.6.1"
@@ -4268,17 +4210,7 @@ checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
dependencies = [
"bitflags 2.10.0",
"core-foundation",
- "system-configuration-sys 0.6.0",
-]
-
-[[package]]
-name = "system-configuration-sys"
-version = "0.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
-dependencies = [
- "core-foundation-sys",
- "libc",
+ "system-configuration-sys",
]
[[package]]
@@ -4559,7 +4491,7 @@ dependencies = [
"futures-core",
"futures-util",
"pin-project-lite",
- "sync_wrapper 1.0.2",
+ "sync_wrapper",
"tokio",
"tower-layer",
"tower-service",
@@ -4711,24 +4643,6 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
-[[package]]
-name = "ureq"
-version = "2.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
-dependencies = [
- "base64 0.22.1",
- "flate2",
- "log",
- "once_cell",
- "rustls",
- "rustls-pki-types",
- "serde",
- "serde_json",
- "url",
- "webpki-roots 0.26.11",
-]
-
[[package]]
name = "url"
version = "2.5.7"
@@ -4957,15 +4871,6 @@ dependencies = [
"wasm-bindgen",
]
-[[package]]
-name = "webpki-roots"
-version = "0.26.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
-dependencies = [
- "webpki-roots 1.0.3",
-]
-
[[package]]
name = "webpki-roots"
version = "1.0.3"
@@ -5352,6 +5257,15 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "winreg"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
+dependencies = [
+ "winapi",
+]
+
[[package]]
name = "winreg"
version = "0.50.0"
diff --git a/Cargo.toml b/Cargo.toml
index 649a1d3..ba8956a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -65,6 +65,7 @@ serde = { version = "^1.0", features = ["derive"] }
serde_json = "^1.0"
serde_with = "3.8.1"
serde_yaml = "0.9"
+sev = "7.1.0"
shellexpand = "^3.1"
sha2 = "0.10"
shell-words = "^1.1.0"
@@ -73,7 +74,6 @@ subtle = "2.6.1"
tempfile = "3.23.0"
tokio = { version = "1" }
tokio-rustls = "0.26.2"
-ureq = { version = "2" }
uuid = { version = "^1.3" }
walkdir = "2"
warp = "^0.3"
diff --git a/GEMINI.md b/GEMINI.md
index 0e0bc1b..e989544 100644
--- a/GEMINI.md
+++ b/GEMINI.md
@@ -84,10 +84,10 @@ optional `--help` flag: `./scrips/accli_wrapper.sh --help`.
error handling through the `anyhow` crate.
- For each new method, make sure to add extensive documentation in the following format:
```rust
+///
///
-/// # Description
-///
-///
+///
+///
///
/// # Arguments
///
@@ -102,6 +102,13 @@ optional `--help` flag: `./scrips/accli_wrapper.sh --help`.
///
/// ASK --(signs)-->
+/// VCEK --(signs)--> Report
+pub type SnpCa = Chain;
+
+/// SNP-enabled processor type.
+pub type SnpProcType = ProcType;
+
+/// SNP attestation report
+pub type SnpReport = AttestationReport;
+
+/// Vendor Chip Endorsement Key
+pub type SnpVcek = Certificate;
+
+/// # Description
+///
+/// We cache VCEK certificates to validate SNP reports by the processor type,
+/// and the reported TCB. Note that even though the TCB version is
+/// self-reported, it is included in the report and signed by the PSP.
+pub type SnpVcekCacheKey = (SnpProcType, TcbVersion);
+
+const AMD_KDS_SITE: &str = "https://kdsintf.amd.com";
+
+fn proc_type_to_kds_url(proc_type: &SnpProcType) -> &str {
+ match proc_type {
+ SnpProcType::Genoa | SnpProcType::Siena | SnpProcType::Bergamo => "Genoa",
+ SnpProcType::Milan => "Milan",
+ SnpProcType::Turin => "Turin",
+ }
+}
+
+/// Fetches AMD's ceritifcate authorities from AMD's Key Distribution Service.
+pub async fn fetch_ca_from_kds(proc_type: &SnpProcType) -> Result {
+ const AMD_KDS_CERT_CHAIN: &str = "cert_chain";
+
+ let proc_str = proc_type_to_kds_url(proc_type);
+ let url: String = format!("{AMD_KDS_SITE}/vcek/v1/{proc_str}/{AMD_KDS_CERT_CHAIN}");
+ trace!("fetch_ca_from_kds(): fetching AMD's CA (url={url})");
+ let client = reqwest::Client::new();
+ let response = client.get(url).send().await?;
+ let response = response.error_for_status()?;
+
+ let body = response.bytes().await?;
+ let ca_chain = Chain::from_pem_bytes(&body)?;
+
+ // Before returning it, verify the signatures.
+ ca_chain.verify()?;
+ trace!("fetch_ca_from_kds(): verified CA's signature");
+
+ Ok(ca_chain)
+}
+
+/// Fetches a processor's Vendor-Chip Endorsement Key (VCEK) from AMD's KDS.
+pub async fn fetch_vcek_from_kds(
+ proc_type: &SnpProcType,
+ att_report: &SnpReport,
+) -> Result {
+ const KDS_VCEK: &str = "/vcek/v1";
+
+ // The URL generation part in this function is adapted from the snpguest crate.
+ let hw_id: String = if att_report.chip_id != [0; 64] {
+ match proc_type {
+ ProcType::Turin => {
+ let shorter_bytes: &[u8] = &att_report.chip_id[0..8];
+ hex::encode(shorter_bytes)
+ }
+ _ => hex::encode(att_report.chip_id),
+ }
+ } else {
+ let reason = "fetch_vcek_from_kds(): hardware ID is 0s on attestation report";
+ error!("{reason}");
+ anyhow::bail!(reason);
+ };
+ let url: String = match proc_type {
+ ProcType::Turin => {
+ let fmc = if let Some(fmc) = att_report.reported_tcb.fmc {
+ fmc
+ } else {
+ return Err(anyhow::anyhow!("A Turin processor must have a fmc value"));
+ };
+ format!(
+ "{AMD_KDS_SITE}{KDS_VCEK}/{}/\
+ {hw_id}?fmcSPL={:02}&blSPL={:02}&teeSPL={:02}&snpSPL={:02}&ucodeSPL={:02}",
+ proc_type_to_kds_url(proc_type),
+ fmc,
+ att_report.reported_tcb.bootloader,
+ att_report.reported_tcb.tee,
+ att_report.reported_tcb.snp,
+ att_report.reported_tcb.microcode
+ )
+ }
+ _ => {
+ format!(
+ "{AMD_KDS_SITE}{KDS_VCEK}/{}/\
+ {hw_id}?blSPL={:02}&teeSPL={:02}&snpSPL={:02}&ucodeSPL={:02}",
+ proc_type_to_kds_url(proc_type),
+ att_report.reported_tcb.bootloader,
+ att_report.reported_tcb.tee,
+ att_report.reported_tcb.snp,
+ att_report.reported_tcb.microcode
+ )
+ }
+ };
+
+ trace!("fetch_vcek_from_kds(): fetching node's VCEK (url={url})");
+ let client = reqwest::Client::new();
+ let response = client.get(url).send().await?;
+ let response = response.error_for_status()?;
+
+ let body = response.bytes().await?;
+ let vcek = Certificate::from_bytes(&body)?;
+
+ Ok(vcek)
+}
diff --git a/attestation-service/src/lib.rs b/attestation-service/src/lib.rs
index 6c62e7f..cac74d1 100644
--- a/attestation-service/src/lib.rs
+++ b/attestation-service/src/lib.rs
@@ -1,20 +1,20 @@
-use env_logger::Env;
+use env_logger::{Builder, Env};
+use log::LevelFilter;
use std::sync::Once;
static INIT: Once = Once::new();
-pub fn init_logging(is_test: bool) {
+pub fn init_logging() {
INIT.call_once(|| {
- let default_filter = if is_test {
- // In tests, be more chatty by default.
- "info,attestation_service=info,accli=info"
- } else {
- // In normal runs, keep everything else at error.
- "error,attestation_service=info,accli=info"
- };
+ let env = Env::default().filter_or("RUST_LOG", "info");
+ let mut builder = Builder::from_env(env);
- let _ = env_logger::Builder::from_env(Env::default().default_filter_or(default_filter))
- .is_test(is_test)
- .try_init();
+ // Disable noisy modules.
+ let noisy_modules: Vec<&str> = vec!["hickory_proto", "hyper_util", "hyper", "reqwest"];
+ for module in &noisy_modules {
+ builder.filter_module(module, LevelFilter::Off);
+ }
+
+ builder.init();
});
}
diff --git a/attestation-service/src/main.rs b/attestation-service/src/main.rs
index ebe4d97..ab83cfd 100644
--- a/attestation-service/src/main.rs
+++ b/attestation-service/src/main.rs
@@ -14,6 +14,8 @@ use rustls::crypto::CryptoProvider;
use std::{net::SocketAddr, path::PathBuf, sync::Arc};
use tokio::net::TcpListener;
+#[cfg(feature = "snp")]
+mod amd;
#[cfg(feature = "azure-cvm")]
mod azure_cvm;
mod ecdhe;
@@ -59,7 +61,7 @@ async fn health() -> impl IntoResponse {
async fn main() -> Result<()> {
// Initialise logging and parse CLI arguments.
let cli = Cli::parse();
- attestation_service::init_logging(false);
+ attestation_service::init_logging();
// Initialise crypto provider and TLS config, this also sets up the TLS
// certificates if necessary.
diff --git a/attestation-service/src/sgx.rs b/attestation-service/src/sgx.rs
index 7dd2d14..ba2bb00 100644
--- a/attestation-service/src/sgx.rs
+++ b/attestation-service/src/sgx.rs
@@ -227,6 +227,7 @@ pub async fn verify_sgx_report(
);
}
};
+ // FIXME(#55): also check the MRENCLAVE measurement against a reference value.
let verified_report = match dcap_qvl::verify::verify("e_bytes, &collateral, now) {
Ok(tcb) => tcb,
Err(e) => {
@@ -237,7 +238,6 @@ pub async fn verify_sgx_report(
);
}
};
-
info!("verififed sgx report (status={})", verified_report.status);
match verified_report.report {
diff --git a/attestation-service/src/snp.rs b/attestation-service/src/snp.rs
index a080d42..bbdd0ac 100644
--- a/attestation-service/src/snp.rs
+++ b/attestation-service/src/snp.rs
@@ -1,15 +1,18 @@
use crate::{
+ amd::{SnpCa, SnpProcType, SnpReport, SnpVcek, fetch_ca_from_kds, fetch_vcek_from_kds},
ecdhe,
jwt::{self, JwtClaims},
mock::{MockQuote, MockQuoteType},
request::{NodeData, Tee},
state::AttestationServiceState,
};
+use anyhow::Result;
use axum::{Extension, Json, http::StatusCode, response::IntoResponse};
use base64::{Engine as _, engine::general_purpose};
use log::{debug, error, info};
use serde::Deserialize;
use serde_json::json;
+use sev::{certs::snp::Verifiable, parser::ByteParser};
use std::sync::Arc;
#[derive(Debug, Deserialize)]
@@ -26,8 +29,6 @@ struct RuntimeData {
_data_type: String,
}
-/// # Description
-///
/// This struct corresponds to the request that SNP-Knative sends to verify an
/// SNP attestation report.
#[derive(Debug, Deserialize)]
@@ -45,6 +46,139 @@ pub struct SnpRequest {
runtime_data: RuntimeData,
}
+/// Extract the report payload from the PSP reposnse.
+///
+/// The attestation-service receives from SNP clients the literal response
+/// returned by the PSP. The structure of this response is described in Table 25
+/// [1]. We observe that the actual report is padded, so this method extracts
+/// the actual attestation report from the PSP response. Note that crates like
+/// snpguest manipulate the actual report, and not the PSP response. They would
+/// rely on the `sev` crate to do the parsing but, annoyingly, it does not
+/// expose a public API for us to parse the PSP response from bytes.
+///
+/// [1] https://www.amd.com/content/dam/amd/en/documents/developer/56860.pdf
+///
+/// # Arguments
+///
+/// - `psp_response`: the raw PSP response from the SNP_GET_REPORT command.
+///
+/// # Returns
+///
+/// The report byte array within the PSP response.
+fn extract_report(data: &[u8]) -> Result> {
+ const OFFSET_STATUS: usize = 0x00;
+ const OFFSET_REPORT_SIZE: usize = 0x04;
+ const OFFSET_REPORT_DATA: usize = 0x20;
+
+ // We need at least 0x20 (32) bytes to reach the report data start.
+ if data.len() < OFFSET_REPORT_DATA {
+ let reason = format!(
+ "PSP response buffer too short to contain header (got={}, minimum={OFFSET_REPORT_DATA})",
+ data.len()
+ );
+ error!("{reason}");
+ anyhow::bail!(reason);
+ }
+
+ // Check status.
+ let status_bytes: [u8; 4] = data[OFFSET_STATUS..OFFSET_STATUS + 4].try_into()?;
+ let status = u32::from_le_bytes(status_bytes);
+ if status != 0 {
+ let reason = format!("PSP reported firmware error (error={:#x})", status);
+ error!("{}", reason);
+ anyhow::bail!(reason);
+ }
+
+ // Get the report size.
+ let size_bytes: [u8; 4] = data[OFFSET_REPORT_SIZE..OFFSET_REPORT_SIZE + 4].try_into()?;
+ let report_size = u32::from_le_bytes(size_bytes) as usize;
+
+ // Validate that the buffer actually holds the amount of data declared.
+ let required_len = OFFSET_REPORT_DATA + report_size;
+ if data.len() < required_len {
+ let reason = "report is shorter than expected size";
+ error!("{}", reason);
+ anyhow::bail!(reason);
+ }
+
+ // Extract report. We slice from 0x20 to (0x20 + size) and convert to an owned
+ // vec.
+ let report_payload = data[OFFSET_REPORT_DATA..required_len].to_vec();
+ Ok(report_payload)
+}
+
+async fn get_snp_ca(
+ proc_type: &SnpProcType,
+ state: &Arc,
+) -> Result {
+ debug!("get_snp_ca(): getting CA chain for SNP processor (type={proc_type})");
+
+ // Fast path: read CA from the cache.
+ let ca: Option = {
+ let cache = state.amd_signing_keys.read().await;
+ cache.get(proc_type).cloned()
+ };
+
+ if let Some(ca) = ca {
+ debug!("get_snp_ca(): cache hit, fetching CA from local cache");
+ return Ok(ca);
+ }
+
+ // This method also verifies the CA signatures.
+ debug!("get_snp_ca(): cache miss, fetching CA from AMD's KDS");
+ let ca = fetch_ca_from_kds(proc_type).await?;
+
+ // Cache CA for future use.
+ {
+ let mut cache = state.amd_signing_keys.write().await;
+ cache.insert(proc_type.clone(), ca.clone());
+ }
+
+ Ok(ca)
+}
+
+/// Helper method to fetch the VCEK certificate to validate an SNP quote. We
+/// cache the certificates based on the platform and TCB info to avoid
+/// round-trips to the AMD servers during verification (in the general case).
+async fn get_snp_vcek(report: &SnpReport, state: &Arc) -> Result {
+ // Fetch the certificate chain from the processor model.
+ let proc_type = snpguest::fetch::get_processor_model(report)?;
+ let ca = get_snp_ca(&proc_type, state).await?;
+
+ // Work-out cache key from report.
+ let tcb_version = report.reported_tcb;
+ let cache_key = (proc_type.clone(), tcb_version);
+ debug!(
+ "get_snp_vcek(): fetching VCEK key for report (proc_type={proc_type}, tcb={tcb_version})"
+ );
+
+ // Fast path: read VCEK from the cache.
+ let vcek: Option = {
+ let cache = state.snp_vcek_cache.read().await;
+ cache.get(&cache_key).cloned()
+ };
+
+ if let Some(vcek) = vcek {
+ debug!("get_snp_vcek(): cache hit, fetching VCEK from local cache");
+ return Ok(vcek);
+ }
+
+ // Slow path: fetch collateral from AMD's KDS.
+ debug!("get_snp_vcek(): cache miss, fetching VCEK from AMD's KDS");
+ let vcek = fetch_vcek_from_kds(&proc_type, report).await?;
+
+ // Once we fetch a new VCEK, verify its certificate chain before caching it.
+ (&ca.ask, &vcek).verify()?;
+
+ // Cache VCEK for future use.
+ {
+ let mut cache = state.snp_vcek_cache.write().await;
+ cache.insert(cache_key, vcek.clone());
+ }
+
+ Ok(vcek)
+}
+
pub async fn verify_snp_report(
Extension(state): Extension>,
Json(payload): Json,
@@ -54,7 +188,7 @@ pub async fn verify_snp_report(
let quote_bytes = match general_purpose::URL_SAFE.decode(&raw_quote_b64) {
Ok(bytes) => bytes,
Err(e) => {
- error!("invalid base64 string in SNP quote (error={e:?})");
+ error!("verify_snp_report(): invalid base64 string in SNP quote (error={e:?})");
return (
StatusCode::BAD_REQUEST,
Json(json!({ "error": "invalid base64 in quote" })),
@@ -66,17 +200,17 @@ pub async fn verify_snp_report(
match MockQuote::from_bytes("e_bytes) {
Ok(mock_quote) => {
if mock_quote.quote_type != MockQuoteType::Snp {
- error!("invalid mock SNP quote (error=wrong quote type)");
+ error!("verify_snp_report(): invalid mock SNP quote (error=wrong quote type)");
return (
StatusCode::BAD_REQUEST,
Json(json!({ "error": "invalid mock SNP quote" })),
);
}
- info!("received mock SNP quote, skipping verification");
+ info!("verify_snp_report(): received mock SNP quote, skipping verification");
mock_quote.user_data
}
Err(e) => {
- error!("invalid mock SNP quote (error={e:?})");
+ error!("verify_snp_report(): invalid mock SNP quote (error={e:?})");
return (
StatusCode::BAD_REQUEST,
Json(json!({ "error": "invalid mock SNP quote" })),
@@ -84,12 +218,66 @@ pub async fn verify_snp_report(
}
}
} else {
- // FIXME(#25): validate SNP reports on bare metal
- error!("missing logic to validate SNP reports on bare metal");
- return (
- StatusCode::NOT_IMPLEMENTED,
- Json(json!({ "error": "SNP report validation not implemented" })),
- );
+ // Even though the response from the PSP to SNP_GET_REPORT is padded to 4000
+ // bytes [1], the snpguest crate expects the AttestationReport to be the
+ // exact size in bytes, without padding [2]. We receive from the client
+ // the raw response from the PSP, so we must remove the padding first.
+ // The structure of the PSP response can be found in Table 25 [3].
+ //
+ // [1] https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/sev-guest.h
+ // [2] https://github.com/virtee/sev/blob/c7b6bbb4e9c0fe85199723ab082ccadf39a494f0/src/firmware/linux/guest/types.rs#L169-L183
+ // [3] https://www.amd.com/content/dam/amd/en/documents/developer/56860.pdf
+ let report_body = match extract_report("e_bytes) {
+ Ok(report_body) => report_body,
+ Err(e) => {
+ error!("verify_snp_report(): error extracting report body (error={e:?})");
+ return (
+ StatusCode::BAD_REQUEST,
+ Json(json!({ "error": "invalid SNP quote" })),
+ );
+ }
+ };
+
+ // Parse the attestation report from bytes.
+ let report: SnpReport = match SnpReport::from_bytes(&report_body) {
+ Ok(report) => report,
+ Err(e) => {
+ error!("verify_snp_report(): error parsing bytes to SNP report (error={e:?})");
+ return (
+ StatusCode::BAD_REQUEST,
+ Json(json!({ "error": "error parsing SNP report" })),
+ );
+ }
+ };
+
+ // Fetch the VCEK certificate.
+ let vcek = match get_snp_vcek(&report, &state).await {
+ Ok(report) => report,
+ Err(e) => {
+ error!("verify_snp_report(): error fetching SNP VCEK (error={e:?})");
+ return (
+ StatusCode::BAD_REQUEST,
+ Json(json!({ "error": "error fetching SNP VCEK" })),
+ );
+ }
+ };
+
+ // FIXME(#55): also check the SNP measurement against a reference value.
+ match snpguest::verify::attestation::verify_attestation(&vcek, &report) {
+ Ok(()) => {
+ info!("verify_snp_report(): verified SNP report");
+
+ // Report data to owned vec.
+ report.report_data.to_vec()
+ }
+ Err(e) => {
+ error!("error verifying SNP report (error={e:?})");
+ return (
+ StatusCode::BAD_REQUEST,
+ Json(json!({ "error": "error verifying SNP report" })),
+ );
+ }
+ }
};
// Use the enclave held data (runtime_data) public key, to derive an
diff --git a/attestation-service/src/state.rs b/attestation-service/src/state.rs
index 5fedbb5..c1daa9a 100644
--- a/attestation-service/src/state.rs
+++ b/attestation-service/src/state.rs
@@ -1,3 +1,5 @@
+#[cfg(feature = "snp")]
+use crate::amd::{SnpCa, SnpProcType, SnpVcek, SnpVcekCacheKey};
#[cfg(feature = "azure-cvm")]
use crate::azure_cvm;
#[cfg(feature = "sgx")]
@@ -7,13 +9,24 @@ use abe4::scheme::types::{PartialMPK, PartialMSK};
use anyhow::Result;
#[cfg(feature = "sgx")]
use jsonwebtoken::EncodingKey;
-use std::{collections::HashMap, path::PathBuf};
+use std::{
+ collections::{BTreeMap, HashMap},
+ path::PathBuf,
+};
use tokio::sync::RwLock;
-/// Unique identifier for the demo attestation service.
+/// Unique alphanumeric identifier for the demo attestation service.
const ATTESTATION_SERVICE_ID: &str = "4CL3SSD3M0";
pub struct AttestationServiceState {
+ // General attestation service fields.
+ /// Run the attestation handlers in mock mode, skipping quote verification
+ /// while still exercising the rest of the request flow.
+ pub mock_attestation: bool,
+ /// JWT encoding key derived from the service's public certificate.
+ pub jwt_encoding_key: EncodingKey,
+
+ // Fields related to attribute-based encryption.
/// Unique ID for this attestation service. This is the field that must be
/// included in the template graph, and is the field we use to run CP-ABE
/// key generation.
@@ -24,9 +37,10 @@ pub struct AttestationServiceState {
/// Master Pulic Key for the attestation service as one of the authorities
/// of the decentralized CP-ABE scheme.
pub partial_mpk: PartialMPK,
- #[cfg(feature = "azure-cvm")]
- pub vcek_pem: Vec,
- pub jwt_encoding_key: EncodingKey,
+
+ // Fields related to verifying attestation reports from TEEs.
+
+ // Intel SGX.
/// URL to a Provisioning Certificate Caching Service (PCCS) to verify SGX
/// quotes.
#[cfg(feature = "sgx")]
@@ -36,9 +50,21 @@ pub struct AttestationServiceState {
/// collaterals.
#[cfg(feature = "sgx")]
pub sgx_collateral_cache: RwLock>,
- /// Run the attestation handlers in mock mode, skipping quote verification
- /// while still exercising the rest of the request flow.
- pub mock_attestation: bool,
+
+ // Amd SEV-SNP.
+ #[cfg(feature = "snp")]
+ /// AMD's root (ARK) and signing (ASK) keys, which make up the ceritificate
+ /// chain of SNP reports:
+ pub amd_signing_keys: RwLock>,
+ /// Cache of VCEK certificates used to validate the signatures of SNP
+ /// reports.
+ ///
+ /// We use a BTreeMap to workaround the lack of Hash traits for the cache
+ /// key, which is a tuple of types we don't control.
+ pub snp_vcek_cache: RwLock>,
+
+ #[cfg(feature = "azure-cvm")]
+ pub vcek_pem: Vec,
}
impl AttestationServiceState {
@@ -57,18 +83,24 @@ impl AttestationServiceState {
let (partial_msk, partial_mpk): (PartialMSK, PartialMPK) =
abe4::scheme::setup_partial(&mut rng, ATTESTATION_SERVICE_ID);
+ // Fetch AMD signing keys.
+
Ok(Self {
+ mock_attestation,
+ jwt_encoding_key: jwt::generate_encoding_key(&certs_dir)?,
id: ATTESTATION_SERVICE_ID.to_string(),
partial_msk,
partial_mpk,
#[cfg(feature = "azure-cvm")]
vceck_pem: azure_cvm::fetch_vcek_pem()?,
- jwt_encoding_key: jwt::generate_encoding_key(&certs_dir)?,
#[cfg(feature = "sgx")]
sgx_pccs_url,
#[cfg(feature = "sgx")]
sgx_collateral_cache: RwLock::new(HashMap::new()),
- mock_attestation,
+ #[cfg(feature = "snp")]
+ amd_signing_keys: RwLock::new(BTreeMap::new()),
+ #[cfg(feature = "snp")]
+ snp_vcek_cache: RwLock::new(BTreeMap::new()),
})
}
}
From 43df72e6b3cd5ff5be5659d39e2e40a2229831b2 Mon Sep 17 00:00:00 2001
From: Carlos Segarra
Date: Mon, 24 Nov 2025 15:48:21 +0000
Subject: [PATCH 13/52] [accli] E: Launch Applications By Name
---
Cargo.lock | 1 +
accli/src/main.rs | 14 +++++++++++++-
accli/src/tasks/applications.rs | 33 +++++++++++++++++++++++++++++++++
3 files changed, 47 insertions(+), 1 deletion(-)
diff --git a/Cargo.lock b/Cargo.lock
index d407361..fdd4a30 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4079,6 +4079,7 @@ dependencies = [
[[package]]
name = "snpguest"
version = "0.10.0"
+source = "git+https://github.com/faasm/snpguest.git#cb4dcf19403edd0b3be458f82df7d72c89e6a0e6"
dependencies = [
"anyhow",
"asn1-rs",
diff --git a/accli/src/main.rs b/accli/src/main.rs
index 33cca38..e356ef5 100644
--- a/accli/src/main.rs
+++ b/accli/src/main.rs
@@ -2,7 +2,7 @@ use crate::{
env::Env,
tasks::{
accless::Accless,
- applications::Applications,
+ applications::{self, Applications},
attestation_service::AttestationService,
azure::Azure,
dev::Dev,
@@ -344,6 +344,15 @@ enum ApplicationsCommand {
#[arg(long)]
cert_path: Option,
},
+ /// Run one of the Accless applications
+ Run {
+ /// Type of the application to run
+ #[arg(long)]
+ app_type: applications::ApplicationType,
+ /// Name of the application to run
+ #[arg(long)]
+ app_name: applications::Functions,
+ },
}
#[tokio::main]
@@ -380,6 +389,9 @@ async fn main() -> anyhow::Result<()> {
} => {
Applications::build(*clean, *debug, cert_path.as_deref(), false)?;
}
+ ApplicationsCommand::Run { app_type, app_name } => {
+ Applications::run(app_type.clone(), app_name.clone())?;
+ }
},
Command::Dev { dev_command } => match dev_command {
DevCommand::BumpVersion {
diff --git a/accli/src/tasks/applications.rs b/accli/src/tasks/applications.rs
index cbd6bb6..c413960 100644
--- a/accli/src/tasks/applications.rs
+++ b/accli/src/tasks/applications.rs
@@ -1,6 +1,18 @@
use crate::tasks::docker::{DOCKER_ACCLESS_CODE_MOUNT_DIR, Docker};
+use clap::ValueEnum;
use std::path::Path;
+#[derive(Clone, Debug, ValueEnum)]
+pub enum ApplicationType {
+ Function,
+}
+
+#[derive(Clone, Debug, ValueEnum)]
+pub enum Functions {
+ #[value(name = "escrow-xput")]
+ EscrowXput,
+}
+
#[derive(Debug)]
pub struct Applications {}
@@ -42,4 +54,25 @@ impl Applications {
})?;
Docker::run(&cmd, true, Some(workdir_str), &[], false, capture_output)
}
+
+ pub fn run(app_type: ApplicationType, app_name: Functions) -> anyhow::Result
From 978c73775233cd8a9240a7bdeb1daa42d4cf3c5f Mon Sep 17 00:00:00 2001
From: Carlos Segarra
Date: Wed, 26 Nov 2025 14:38:39 +0000
Subject: [PATCH 47/52] [scripts] B: Make Sure Workon.sh Is Run In Ctr
---
config/docker/accless-experiments.dockerfile | 2 +-
scripts/docker/entrypoint.sh | 6 +++++-
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/config/docker/accless-experiments.dockerfile b/config/docker/accless-experiments.dockerfile
index b818d0f..4cb7b99 100644
--- a/config/docker/accless-experiments.dockerfile
+++ b/config/docker/accless-experiments.dockerfile
@@ -118,4 +118,4 @@ COPY scripts/docker/entrypoint.sh /usr/local/bin/docker_entrypoint.sh
RUN chmod +x /usr/local/bin/docker_entrypoint.sh
ENTRYPOINT ["/usr/local/bin/docker_entrypoint.sh"]
-CMD ["/bin/bash", "-c"]
+CMD ["/bin/bash", "-l"]
diff --git a/scripts/docker/entrypoint.sh b/scripts/docker/entrypoint.sh
index 3e13790..0a145ba 100755
--- a/scripts/docker/entrypoint.sh
+++ b/scripts/docker/entrypoint.sh
@@ -55,4 +55,8 @@ fi
echo ". /code/accless/scripts/workon.sh" >> ${HOME}/.bashrc
echo ". ${HOME}/.cargo/env" >> ${HOME}/.bashrc
-exec /usr/sbin/gosu ${USER_NAME} "$@"
+exec /usr/sbin/gosu "$USER_NAME" bash -c \
+ 'source /code/accless/scripts/workon.sh 2>/dev/null || true; \
+ source "$HOME/.cargo/env" 2>/dev/null || true; \
+ exec "$@"' \
+ bash "$@"
From 38b5ce98e90ffc3b95f975bf8bbed29ba0dbc905 Mon Sep 17 00:00:00 2001
From: Carlos Segarra
Date: Wed, 26 Nov 2025 14:41:36 +0000
Subject: [PATCH 48/52] [accli] B: Create PID File Dirs In AS BG Spawn
---
accli/src/tasks/attestation_service.rs | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/accli/src/tasks/attestation_service.rs b/accli/src/tasks/attestation_service.rs
index 9156be3..795924e 100644
--- a/accli/src/tasks/attestation_service.rs
+++ b/accli/src/tasks/attestation_service.rs
@@ -82,7 +82,9 @@ impl AttestationService {
.stderr(Stdio::null())
.spawn()
.context("Failed to spawn attestation service in background")?;
- fs::write(PID_FILE_PATH, child.id().to_string()).context("Failed to write PID file")?;
+ fs::create_dir_all(PID_FILE_PATH).context("run(): failed to create PID file dirs")?;
+ fs::write(PID_FILE_PATH, child.id().to_string())
+ .context("run(): failed to write PID file")?;
info!("run(): attestation service spawned (PID={})", child.id());
Ok(())
} else {
From 5f35f1b0881a03d9917fe5d5e0b0140618c860ef Mon Sep 17 00:00:00 2001
From: Carlos Segarra
Date: Thu, 27 Nov 2025 10:24:06 +0000
Subject: [PATCH 49/52] [accli] B: Fix Spawn Of Background AS
---
.github/workflows/snp.yml | 4 ++--
accli/src/tasks/attestation_service.rs | 7 +++++--
2 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/snp.yml b/.github/workflows/snp.yml
index c0cef6c..6aa3f7d 100644
--- a/.github/workflows/snp.yml
+++ b/.github/workflows/snp.yml
@@ -49,8 +49,8 @@ jobs:
else
BRANCH=${GITHUB_REF_NAME}
fi
- ./scripts/accli_wrapper.sh dev cvm run \
- "git fetch origin $BRANCH && git checkout $BRANCH && git reset --hard origin/$BRANCH"
+ ./scripts/accli_wrapper.sh dev cvm run -- \
+ git fetch origin $BRANCH && git checkout $BRANCH && git reset --hard origin/$BRANCH
# Build SNP applications and embed the attestation service's certificate.
- name: "Build SNP applications"
diff --git a/accli/src/tasks/attestation_service.rs b/accli/src/tasks/attestation_service.rs
index 795924e..322c4ca 100644
--- a/accli/src/tasks/attestation_service.rs
+++ b/accli/src/tasks/attestation_service.rs
@@ -8,6 +8,7 @@ use nix::{
use reqwest;
use std::{
fs,
+ path::Path,
process::{Command, Stdio},
};
@@ -81,8 +82,10 @@ impl AttestationService {
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
- .context("Failed to spawn attestation service in background")?;
- fs::create_dir_all(PID_FILE_PATH).context("run(): failed to create PID file dirs")?;
+ .context("run(): failed to spawn attestation service in background")?;
+ let pid_file_path = Path::new(PID_FILE_PATH);
+ fs::create_dir_all(pid_file_path.parent().unwrap())
+ .context("run(): failed to create PID file dirs")?;
fs::write(PID_FILE_PATH, child.id().to_string())
.context("run(): failed to write PID file")?;
info!("run(): attestation service spawned (PID={})", child.id());
From b6946f19eab6af5e6c63997cdf0bbee22fb4b5c8 Mon Sep 17 00:00:00 2001
From: Carlos Segarra
Date: Thu, 27 Nov 2025 12:24:32 +0000
Subject: [PATCH 50/52] [scripts] E: Check Host Reqs In SNP Setup
---
accli/src/tasks/cvm.rs | 3 +++
scripts/snp/setup.sh | 35 ++++++++++++++++++++++++++++++++++-
2 files changed, 37 insertions(+), 1 deletion(-)
diff --git a/accli/src/tasks/cvm.rs b/accli/src/tasks/cvm.rs
index 2732a14..1e41186 100644
--- a/accli/src/tasks/cvm.rs
+++ b/accli/src/tasks/cvm.rs
@@ -33,6 +33,7 @@ pub fn parse_host_guest_path(s: &str) -> anyhow::Result<(PathBuf, PathBuf)> {
#[derive(Debug, Clone, Copy)]
pub enum Component {
+ Check,
Apt,
Qemu,
Ovmf,
@@ -47,6 +48,7 @@ impl FromStr for Component {
fn from_str(s: &str) -> Result {
match s {
+ "check" => Ok(Component::Check),
"apt" => Ok(Component::Apt),
"qemu" => Ok(Component::Qemu),
"ovmf" => Ok(Component::Ovmf),
@@ -62,6 +64,7 @@ impl FromStr for Component {
impl std::fmt::Display for Component {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
+ Component::Check => write!(f, "check"),
Component::Apt => write!(f, "apt"),
Component::Qemu => write!(f, "qemu"),
Component::Ovmf => write!(f, "ovmf"),
diff --git a/scripts/snp/setup.sh b/scripts/snp/setup.sh
index 550ffed..bfa98b5 100755
--- a/scripts/snp/setup.sh
+++ b/scripts/snp/setup.sh
@@ -16,7 +16,36 @@ clean() {
mkdir -p ${OUTPUT_DIR}
}
-# TODO: check host kernel
+#
+# Check the host system is configured properly.
+check_host_reqs() {
+ local device="/dev/sev"
+ local required_group="kvm"
+ local user="$(id -un)"
+
+ print_info "Checking host system requirements..."
+
+ # Check `/dev/sev` exists.
+ if [[ ! -e "$device" ]]; then
+ print_error "check_host_reqs(): $device does not exist"
+ exit 1
+ fi
+
+ # Check `/dev/sev` is owned by the `kvm` group.
+ device_group="$(stat -c "%G" "$device")"
+ if [[ "$device_group" != "$required_group" ]]; then
+ pritn_error "check_host_reqs(): $device is owned by group '$device_group', expected '$required_group'."
+ exit 1
+ fi
+
+ # Check calling user is in the `kvm` group.
+ if ! id -nG "$user" | grep -qw "$required_group"; then
+ print_error "check_host_reqs(): user '$user' is not in group '$required_group'."
+ exit 1
+ fi
+
+ print_success "check_host_reqs(): $device is owned by group '$required_group' and user '$user' is in that group."
+}
#
# Fetch the linux kernel image.
@@ -339,6 +368,9 @@ main() {
if [[ -n "$component" ]]; then
case "$component" in
+ check)
+ check_host_reqs
+ ;;
apt)
install_apt_deps
;;
@@ -366,6 +398,7 @@ main() {
;;
esac
else
+ check_host_reqs
install_apt_deps
build_qemu
build_ovmf
From 8ca0c2cbe5da3613bb575eb9c62a841f3b40fb63 Mon Sep 17 00:00:00 2001
From: Carlos Segarra
Date: Thu, 27 Nov 2025 12:28:11 +0000
Subject: [PATCH 51/52] [attestation-service] B: Properly Poll Certs
---
attestation-service/tests/as_api_tests.rs | 26 +++++++++++++++++++++--
1 file changed, 24 insertions(+), 2 deletions(-)
diff --git a/attestation-service/tests/as_api_tests.rs b/attestation-service/tests/as_api_tests.rs
index 89e45a2..12f1e90 100644
--- a/attestation-service/tests/as_api_tests.rs
+++ b/attestation-service/tests/as_api_tests.rs
@@ -7,10 +7,13 @@ use serial_test::serial;
use std::{
path::{Path, PathBuf},
process::Stdio,
- time::Duration,
+ time::{Duration, Instant},
};
use tempfile::tempdir;
-use tokio::process::{Child, Command};
+use tokio::{
+ process::{Child, Command},
+ time::sleep,
+};
struct ChildGuard(Child);
@@ -127,6 +130,25 @@ async fn test_att_clients() -> Result<()> {
let cert_path = get_public_certificate_path(&certs_dir);
+ // Wait until cert path to be ready.
+ let deadline = Instant::now() + Duration::from_secs(15);
+ let poll_interval = Duration::from_millis(100);
+ loop {
+ if cert_path.exists() {
+ break;
+ }
+ if Instant::now() >= deadline {
+ let reason = format!(
+ "timed-out waiting for certs to become available (path={})",
+ cert_path.display()
+ );
+ error!("test_att_clients(): {reason}");
+ anyhow::bail!(reason);
+ }
+
+ sleep(poll_interval).await;
+ }
+
// While it is starting, rebuild the test application so that we can inject the
// new certificates. Note that we need to pass the certificate's path
// _inside_ the container, as application build happens inside the
From 9100a3ef82fa60b5b50392009e4723c7b5274b92 Mon Sep 17 00:00:00 2001
From: Carlos Segarra
Date: Thu, 27 Nov 2025 13:24:27 +0000
Subject: [PATCH 52/52] [ci] B: Fix Syntax Error In snp.yml
---
.github/workflows/snp.yml | 19 +++++++------------
1 file changed, 7 insertions(+), 12 deletions(-)
diff --git a/.github/workflows/snp.yml b/.github/workflows/snp.yml
index 6aa3f7d..4b1dcc1 100644
--- a/.github/workflows/snp.yml
+++ b/.github/workflows/snp.yml
@@ -25,16 +25,7 @@ jobs:
- name: "Check out the code"
uses: actions/checkout@v4
- - name: "Cache SNP artefacts"
- id: cache-snp
- uses: actions/cache@v4
- with:
- path: scripts/snp/output
- key: snp-setup-${{ runner.os }}-${{ hashFiles('scripts/snp/**') }}
-
- # Only re-run on a cache miss (setup may take a while)
- name: "Run SNP setup"
- if: steps.cache-snp.outputs.cache-hit != 'true'
run: ./scripts/accli_wrapper.sh dev cvm setup --clean
- name: "Start attestation service in the background"
@@ -50,7 +41,7 @@ jobs:
BRANCH=${GITHUB_REF_NAME}
fi
./scripts/accli_wrapper.sh dev cvm run -- \
- git fetch origin $BRANCH && git checkout $BRANCH && git reset --hard origin/$BRANCH
+ "git fetch origin $BRANCH && git checkout $BRANCH && git reset --hard origin/$BRANCH"
# Build SNP applications and embed the attestation service's certificate.
- name: "Build SNP applications"
@@ -59,6 +50,10 @@ jobs:
- name: "Run supported SNP applications"
run: |
# First get the external IP so that we can reach the attestation-service from the cVM.
- AS_URL=$(accli attestation-service health --url "https://0.0.0.0:8443" --cert-path ./certs/cert.pem \
+ AS_URL=$(./scripts/accli_wrapper.sh attestation-service health --url "https://0.0.0.0:8443" --cert-path ./certs/cert.pem 2>&1 \
| grep "attestation service is healthy and reachable on:" | awk '{print $NF}')
- ./scripts/accli_wrapper.sh applications run function escrow-xput --as-url ${AS_URL} --as-cert-path ./certs/cert.pem
+ echo "Got AS URL: ${AS_URL}"
+ ./scripts/accli_wrapper.sh applications run function escrow-xput --as-url ${AS_URL} --as-cert-path ./certs/cert.pem --in-cvm
+
+ - name: "Stop attestation service in the background"
+ run: ./scripts/accli_wrapper.sh attestation-service stop