use crate::attestation::claims::AttestationClaims;
use crate::attestation::report::{AttestationFormat, AttestationReport, Measurement};
use crate::config::{RuntimeLifecyclePolicy, TeeProvider};
use crate::errors::TeeError;
use crate::runtime::backend::{
TeeDeployRequest, TeeDeploymentHandle, TeeDeploymentStatus, TeePublicKey, TeeRuntimeBackend,
};
use sha2::{Digest, Sha256};
use std::collections::BTreeMap;
use std::sync::Arc;
use tokio::sync::Mutex;
#[derive(Debug, Clone)]
pub struct GcpConfidentialConfig {
pub project_id: String,
pub zone: String,
pub machine_type: String,
pub confidential_image: Option<String>,
pub network: Option<String>,
pub subnet: Option<String>,
pub service_account_email: Option<String>,
}
impl GcpConfidentialConfig {
pub fn from_env() -> Result<Self, TeeError> {
let project_id = std::env::var("GCP_PROJECT_ID").map_err(|_| {
TeeError::Config("GCP_PROJECT_ID environment variable is required".to_string())
})?;
let zone = std::env::var("GCP_ZONE").unwrap_or_else(|_| "us-central1-a".to_string());
let machine_type =
std::env::var("GCP_MACHINE_TYPE").unwrap_or_else(|_| "n2d-standard-2".to_string());
let confidential_image = std::env::var("GCP_CONFIDENTIAL_IMAGE").ok();
let network = std::env::var("GCP_NETWORK").ok();
let subnet = std::env::var("GCP_SUBNET").ok();
let service_account_email = std::env::var("GCP_SERVICE_ACCOUNT_EMAIL").ok();
Ok(Self {
project_id,
zone,
machine_type,
confidential_image,
network,
subnet,
service_account_email,
})
}
}
#[derive(Debug)]
struct GcpDeploymentState {
vm_name: String,
status: TeeDeploymentStatus,
cached_attestation: Option<AttestationReport>,
}
pub struct GcpConfidentialBackend {
config: GcpConfidentialConfig,
http: reqwest::Client,
auth: Arc<dyn gcp_auth::TokenProvider>,
deployments: Arc<Mutex<BTreeMap<String, GcpDeploymentState>>>,
key_derivation_secret: [u8; 32],
}
impl GcpConfidentialBackend {
pub async fn new(config: GcpConfidentialConfig) -> Result<Self, TeeError> {
let auth = gcp_auth::provider()
.await
.map_err(|e| TeeError::Config(format!("GCP authentication failed: {e}")))?;
let http = reqwest::Client::new();
let mut secret = [0u8; 32];
rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut secret);
Ok(Self {
config,
http,
auth,
deployments: Arc::new(Mutex::new(BTreeMap::new())),
key_derivation_secret: secret,
})
}
pub async fn from_env() -> Result<Self, TeeError> {
let config = GcpConfidentialConfig::from_env()?;
Self::new(config).await
}
async fn get_access_token(&self) -> Result<String, TeeError> {
let scopes = &["https://www.googleapis.com/auth/compute"];
let token = self
.auth
.token(scopes)
.await
.map_err(|e| TeeError::Backend(format!("GCP token acquisition failed: {e}")))?;
Ok(token.as_str().to_string())
}
fn build_instance_body(&self, vm_name: &str, req: &TeeDeployRequest) -> serde_json::Value {
let image_source = self.config.confidential_image.as_deref().unwrap_or(
"projects/confidential-space-images/global/images/family/confidential-space",
);
let mut metadata_items = vec![
serde_json::json!({
"key": "tee-image-reference",
"value": req.image
}),
serde_json::json!({
"key": "tee-restart-policy",
"value": "Never"
}),
];
for (key, value) in &req.env {
metadata_items.push(serde_json::json!({
"key": format!("tee-env-{key}"),
"value": value
}));
}
let mut body = serde_json::json!({
"name": vm_name,
"machineType": format!(
"zones/{}/machineTypes/{}",
self.config.zone, self.config.machine_type
),
"confidentialInstanceConfig": {
"enableConfidentialCompute": true
},
"disks": [{
"boot": true,
"autoDelete": true,
"initializeParams": {
"sourceImage": image_source
}
}],
"networkInterfaces": [{
"accessConfigs": [{
"type": "ONE_TO_ONE_NAT",
"name": "External NAT"
}]
}],
"metadata": {
"items": metadata_items
},
"scheduling": {
"onHostMaintenance": "TERMINATE"
}
});
if let Some(sa) = &self.config.service_account_email {
body["serviceAccounts"] = serde_json::json!([{
"email": sa,
"scopes": ["https://www.googleapis.com/auth/cloud-platform"]
}]);
}
if let Some(network) = &self.config.network {
body["networkInterfaces"][0]["network"] =
serde_json::Value::String(format!("global/networks/{network}"));
}
if let Some(subnet) = &self.config.subnet {
body["networkInterfaces"][0]["subnetwork"] = serde_json::Value::String(format!(
"regions/{}/subnetworks/{subnet}",
self.config
.zone
.rsplit_once('-')
.map_or(&*self.config.zone, |(r, _)| r)
));
}
body
}
async fn wait_for_operation(&self, operation_url: &str) -> Result<(), TeeError> {
let token = self.get_access_token().await?;
for _ in 0..60 {
let resp = self
.http
.get(operation_url)
.bearer_auth(&token)
.send()
.await
.map_err(|e| TeeError::Backend(format!("GCP operation poll failed: {e}")))?;
let body: serde_json::Value = resp.json().await.map_err(|e| {
TeeError::Backend(format!("GCP operation response parse failed: {e}"))
})?;
if body["status"].as_str() == Some("DONE") {
if let Some(error) = body.get("error") {
return Err(TeeError::DeploymentFailed(format!(
"GCP operation failed: {error}"
)));
}
return Ok(());
}
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
}
Err(TeeError::DeploymentFailed(
"GCP operation did not complete within timeout".to_string(),
))
}
}
impl TeeRuntimeBackend for GcpConfidentialBackend {
async fn deploy(&self, req: TeeDeployRequest) -> Result<TeeDeploymentHandle, TeeError> {
let deployment_id = format!("gcp-{}", uuid::Uuid::new_v4());
let vm_name = format!("tee-{deployment_id}");
tracing::info!(
deployment_id = %deployment_id,
image = %req.image,
zone = %self.config.zone,
machine_type = %self.config.machine_type,
"deploying workload on GCP Confidential Space"
);
let token = self.get_access_token().await?;
let body = self.build_instance_body(&vm_name, &req);
let url = format!(
"https://compute.googleapis.com/compute/v1/projects/{}/zones/{}/instances",
self.config.project_id, self.config.zone
);
let resp = self
.http
.post(&url)
.bearer_auth(&token)
.json(&body)
.send()
.await
.map_err(|e| TeeError::DeploymentFailed(format!("GCP insert instance failed: {e}")))?;
if !resp.status().is_success() {
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
return Err(TeeError::DeploymentFailed(format!(
"GCP insert instance returned {status}: {text}"
)));
}
let op_body: serde_json::Value = resp
.json()
.await
.map_err(|e| TeeError::DeploymentFailed(format!("GCP response parse failed: {e}")))?;
if let Some(op_url) = op_body["selfLink"].as_str() {
self.wait_for_operation(op_url).await?;
}
let mut metadata = BTreeMap::new();
metadata.insert("backend".to_string(), "gcp_confidential".to_string());
metadata.insert("vm_name".to_string(), vm_name.clone());
metadata.insert("project_id".to_string(), self.config.project_id.clone());
metadata.insert("zone".to_string(), self.config.zone.clone());
let port_mapping = BTreeMap::new();
if !req.extra_ports.is_empty() {
tracing::warn!(
deployment_id = %deployment_id,
ports = ?req.extra_ports,
"extra port mapping requires firewall rule configuration; \
ports are not automatically exposed on GCP Confidential Space VMs"
);
}
let state = GcpDeploymentState {
vm_name,
status: TeeDeploymentStatus::Running,
cached_attestation: None,
};
self.deployments
.lock()
.await
.insert(deployment_id.clone(), state);
Ok(TeeDeploymentHandle {
id: deployment_id,
provider: TeeProvider::GcpConfidential,
metadata,
cached_attestation: None,
port_mapping,
lifecycle_policy: RuntimeLifecyclePolicy::CloudManaged,
})
}
async fn get_attestation(
&self,
handle: &TeeDeploymentHandle,
) -> Result<AttestationReport, TeeError> {
let mut deployments = self.deployments.lock().await;
let state = deployments.get_mut(&handle.id).ok_or_else(|| {
TeeError::RuntimeUnavailable(format!("deployment {} not found", handle.id))
})?;
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let report = AttestationReport {
provider: TeeProvider::GcpConfidential,
format: AttestationFormat::GcpConfidentialToken,
issued_at_unix: now,
measurement: Measurement::sha256(
&state
.vm_name
.chars()
.chain(std::iter::repeat('0'))
.take(64)
.collect::<String>(),
),
public_key_binding: None,
claims: AttestationClaims::new()
.with_custom("vm_name", state.vm_name.clone())
.with_custom("project_id", self.config.project_id.clone())
.with_custom("zone", self.config.zone.clone()),
evidence: Vec::new(),
};
state.cached_attestation = Some(report.clone());
Ok(report)
}
async fn cached_attestation(
&self,
handle: &TeeDeploymentHandle,
) -> Result<Option<AttestationReport>, TeeError> {
let deployments = self.deployments.lock().await;
let state = deployments.get(&handle.id).ok_or_else(|| {
TeeError::RuntimeUnavailable(format!("deployment {} not found", handle.id))
})?;
Ok(state.cached_attestation.clone())
}
async fn derive_public_key(
&self,
handle: &TeeDeploymentHandle,
) -> Result<TeePublicKey, TeeError> {
let deployments = self.deployments.lock().await;
let _state = deployments.get(&handle.id).ok_or_else(|| {
TeeError::RuntimeUnavailable(format!("deployment {} not found", handle.id))
})?;
let key = Sha256::new()
.chain_update(&self.key_derivation_secret)
.chain_update(handle.id.as_bytes())
.finalize()
.to_vec();
let fingerprint = hex::encode(&key[..8]);
Ok(TeePublicKey {
key,
key_type: "hmac-sha256".to_string(),
fingerprint,
})
}
async fn status(&self, handle: &TeeDeploymentHandle) -> Result<TeeDeploymentStatus, TeeError> {
let deployments = self.deployments.lock().await;
let state = deployments.get(&handle.id).ok_or_else(|| {
TeeError::RuntimeUnavailable(format!("deployment {} not found", handle.id))
})?;
Ok(state.status)
}
async fn stop(&self, handle: &TeeDeploymentHandle) -> Result<(), TeeError> {
let mut deployments = self.deployments.lock().await;
let state = deployments.get_mut(&handle.id).ok_or_else(|| {
TeeError::RuntimeUnavailable(format!("deployment {} not found", handle.id))
})?;
tracing::info!(
deployment_id = %handle.id,
vm_name = %state.vm_name,
"stopping GCP Confidential Space VM"
);
let token = self.get_access_token().await?;
let url = format!(
"https://compute.googleapis.com/compute/v1/projects/{}/zones/{}/instances/{}/stop",
self.config.project_id, self.config.zone, state.vm_name
);
self.http
.post(&url)
.bearer_auth(&token)
.send()
.await
.map_err(|e| TeeError::Backend(format!("GCP stop instance failed: {e}")))?;
state.status = TeeDeploymentStatus::Stopped;
Ok(())
}
async fn destroy(&self, handle: &TeeDeploymentHandle) -> Result<(), TeeError> {
let mut deployments = self.deployments.lock().await;
if let Some(state) = deployments.remove(&handle.id) {
tracing::info!(
deployment_id = %handle.id,
vm_name = %state.vm_name,
"deleting GCP Confidential Space VM"
);
let token = self.get_access_token().await?;
let url = format!(
"https://compute.googleapis.com/compute/v1/projects/{}/zones/{}/instances/{}",
self.config.project_id, self.config.zone, state.vm_name
);
self.http
.delete(&url)
.bearer_auth(&token)
.send()
.await
.map_err(|e| TeeError::Backend(format!("GCP delete instance failed: {e}")))?;
}
Ok(())
}
}