diff --git a/.env.example b/.env.example index 3a7ed1a6..27623f05 100644 --- a/.env.example +++ b/.env.example @@ -89,6 +89,9 @@ IMAGE_GENERATIONS_ARCHITECTURE=flux MISTRALRS_IMAGE=ghcr.io/ericlbuehler/mistral.rs:cuda-80-0.3.1 # ---------------------------------------------------------------------------------- -# TDX configuration +# CC configuration - note that only one of the following can be enabled at a time +# TODO? # Enable TDX by setting ENABLE_TDX=true for confidential compute, otherwise it will be disabled ENABLE_TDX=false +# Enable SEV-SNP by setting ENABLE_SEV_SNP=true for ADM SEV-SNP, otherwise it will be disabled +ENABLE_SEV_SNP=false \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 09a3035c..e67efa16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -742,10 +742,15 @@ dependencies = [ "anyhow", "atoma-sui", "atoma-utils", + "bincode", "blake2", "dcap-rs", "flume", + "p384", "rand 0.8.5", + "sev 5.0.0", + "sha2 0.10.8", + "strum 0.26.3", "tdx", "thiserror 2.0.11", "tokio", @@ -1614,7 +1619,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde-big-array", - "sev", + "sev 4.0.0", "sysinfo", "tss-esapi", "users", @@ -2161,6 +2166,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", + "der_derive", + "flagset", "pem-rfc7468 0.7.0", "zeroize", ] @@ -2179,6 +2186,17 @@ dependencies = [ "rusticata-macros", ] +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "deranged" version = "0.3.11" @@ -2512,6 +2530,7 @@ dependencies = [ "ff 0.13.0", "generic-array", "group 0.13.0", + "hkdf", "pem-rfc7468 0.7.0", "pkcs8 0.10.2", "rand_core 0.6.4", @@ -2906,6 +2925,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d" +[[package]] +name = "flagset" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3ea1ec5f8307826a5b71094dd91fc04d4ae75d5709b20ad351c7fb4815c86ec" + [[package]] name = "flate2" version = "1.0.35" @@ -7173,6 +7198,34 @@ dependencies = [ "uuid", ] +[[package]] +name = "sev" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06afe5192a43814047ea0072f4935f830a1de3c8cb43b56c90ae6918468b94d" +dependencies = [ + "base64 0.22.1", + "bincode", + "bitfield 0.15.0", + "bitflags 1.3.2", + "byteorder", + "codicon", + "dirs 5.0.1", + "hex", + "iocuddle", + "lazy_static", + "libc", + "p384", + "rsa 0.9.7", + "serde", + "serde-big-array", + "serde_bytes", + "sha2 0.10.8", + "static_assertions", + "uuid", + "x509-cert", +] + [[package]] name = "sha1" version = "0.10.6" @@ -7708,6 +7761,15 @@ dependencies = [ "strum_macros 0.25.3", ] +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros 0.26.4", +] + [[package]] name = "strum_macros" version = "0.24.3" @@ -7734,6 +7796,19 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.98", +] + [[package]] name = "subtle" version = "2.6.1" @@ -8537,6 +8612,27 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tls_codec" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e78c9c330f8c85b2bae7c8368f2739157db9991235123aa1b15ef9502bfb6a" +dependencies = [ + "tls_codec_derive", + "zeroize", +] + +[[package]] +name = "tls_codec_derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9ef545650e79f30233c0003bcc2504d7efac6dad25fca40744de773fe2049c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "tokenizers" version = "0.21.0" @@ -9838,6 +9934,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid", + "der 0.7.9", + "spki 0.7.3", + "tls_codec", +] + [[package]] name = "x509-parser" version = "0.14.0" diff --git a/Cargo.toml b/Cargo.toml index 2b9c4767..b5a63535 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ atoma-sui = { path = "./atoma-sui" } atoma-utils = { path = "./atoma-utils" } axum = "0.7.5" base64 = "0.22.1" +bincode = "1.3.3" blake2 = "0.10.6" clap = "4.5.4" config = "0.14.0" @@ -45,15 +46,18 @@ lazy_static = "1.5.0" metrics = "0.23" metrics-exporter-prometheus = "0.14.0" once_cell = "1.20.2" +p384 = "0.13.0" prometheus = "0.13.4" rand = "0.8.5" reqwest = "0.12.1" rs_merkle = "1.4.2" +sev = "5.0.0" serde = "1.0.217" serde_json = "1.0.138" serde_yaml = "0.9.34" serial_test = "3.1.1" sha2 = "0.10.8" +strum = "0.26.3" sqlx = "0.8.2" sui-keys = { git = "https://github.com/mystenlabs/sui", package = "sui-keys", tag = "testnet-v1.41.1" } sui-sdk = { git = "https://github.com/mystenlabs/sui", package = "sui-sdk", tag = "testnet-v1.41.1" } diff --git a/Dockerfile b/Dockerfile index fb11da03..6c150365 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,7 @@ ARG TARGETPLATFORM ARG BUILDPLATFORM ARG TARGETARCH ARG ENABLE_TDX +ARG ENABLE_SEV_SNP # Install build dependencies RUN apt-get update && apt-get install -y \ @@ -14,7 +15,7 @@ RUN apt-get update && apt-get install -y \ curl \ libssl-dev \ libssl1.1 \ - && if [ "$ENABLE_TDX" = "true" ]; then \ + && if [ "$ENABLE_TDX" = "true" ] || [ "$ENABLE_SEV_SNP" = "true" ]; then \ apt-get install -y libtss2-dev; \ fi \ && rm -rf /var/lib/apt/lists/* @@ -24,8 +25,10 @@ WORKDIR /usr/src/atoma-node COPY . . # Compile -RUN if [ "$ENABLE_TDX" = "true" ]; then \ +RUN if [ "$ENABLE_TDX" = "true" ] && [ "$ENABLE_SEV_SNP" = "false" ]; then \ RUST_LOG=${TRACE_LEVEL} cargo build --release --bin atoma-node --features tdx; \ + elif [ "$ENABLE_SEV_SNP" = "true" ] && [ "$ENABLE_TDX" = "false" ]; then \ + RUST_LOG=${TRACE_LEVEL} cargo build --release --bin atoma-node --features sev-snp; \ else \ RUST_LOG=${TRACE_LEVEL} cargo build --release --bin atoma-node; \ fi diff --git a/atoma-bin/atoma_node.rs b/atoma-bin/atoma_node.rs index 1110dab0..c3b24a4d 100644 --- a/atoma-bin/atoma_node.rs +++ b/atoma-bin/atoma_node.rs @@ -5,7 +5,7 @@ use std::{ }; use anyhow::{Context, Result}; -use atoma_confidential::AtomaConfidentialCompute; +use atoma_confidential::{service::AtomaConfidentialComputeProvider, AtomaConfidentialCompute}; use atoma_daemon::{AtomaDaemonConfig, DaemonState}; use atoma_service::{ config::AtomaServiceConfig, @@ -266,6 +266,12 @@ async fn main() -> Result<()> { let (compute_shared_secret_sender, compute_shared_secret_receiver) = tokio::sync::mpsc::unbounded_channel(); + let confidential_compute_provider = config + .service + .confidential_compute_provider + .as_ref() + .and_then(|provider| AtomaConfidentialComputeProvider::from_str(provider).ok()); + let confidential_compute_service_handle = spawn_with_shutdown( AtomaConfidentialCompute::start_confidential_compute_service( client.clone(), @@ -273,6 +279,7 @@ async fn main() -> Result<()> { app_state_decryption_receiver, app_state_encryption_receiver, compute_shared_secret_receiver, + confidential_compute_provider, shutdown_receiver.clone(), ), shutdown_sender.clone(), diff --git a/atoma-confidential/Cargo.toml b/atoma-confidential/Cargo.toml index 4bee724f..2e6cfb9e 100644 --- a/atoma-confidential/Cargo.toml +++ b/atoma-confidential/Cargo.toml @@ -9,16 +9,22 @@ aes-gcm = { workspace = true } anyhow = { workspace = true } atoma-sui = { workspace = true } atoma-utils = { workspace = true } +bincode = { workspace = true } blake2 = { workspace = true } dcap-rs = { workspace = true, optional = true } flume = { workspace = true } +p384 = { workspace = true, optional = true } rand = { workspace = true } -tokio = { workspace = true } +sev = { workspace = true, optional = true, features = ["crypto_nossl"]} +sha2 = { workspace = true } +strum = { workspace = true, features = ["derive"] } tdx = { workspace = true, optional = true } thiserror = { workspace = true } +tokio = { workspace = true } tracing = { workspace = true } x25519-dalek = { workspace = true, features = ["static_secrets"] } [features] default = [] tdx = ["dep:dcap-rs", "dep:tdx" ] +sev-snp = [ "dep:p384", "dep:sev" ] diff --git a/atoma-confidential/src/lib.rs b/atoma-confidential/src/lib.rs index 9eccf266..497d61af 100644 --- a/atoma-confidential/src/lib.rs +++ b/atoma-confidential/src/lib.rs @@ -3,6 +3,8 @@ pub mod key_management; pub mod service; +#[cfg(feature = "sev-snp")] +pub mod sev_snp; #[cfg(feature = "tdx")] pub mod tdx; pub mod types; diff --git a/atoma-confidential/src/service.rs b/atoma-confidential/src/service.rs index 630b1c1f..bf17df94 100644 --- a/atoma-confidential/src/service.rs +++ b/atoma-confidential/src/service.rs @@ -6,15 +6,15 @@ use crate::{ ConfidentialComputeSharedSecretRequest, ConfidentialComputeSharedSecretResponse, }, }; +#[cfg(feature = "sev-snp")] +use crate::sev_snp::{get_compute_data_attestation as snp_attestation, SnpError}; #[cfg(feature = "tdx")] -use crate::{ - tdx::{get_compute_data_attestation, TdxError}, - ToBytes, -}; +use crate::tdx::{get_compute_data_attestation as tdx_attestation, TdxError}; use atoma_sui::client::Client; use atoma_sui::{client::AtomaSuiClientError, events::AtomaEvent}; use atoma_utils::constants::NONCE_SIZE; use std::sync::Arc; +use strum::EnumString; use thiserror::Error; use tokio::sync::{mpsc::UnboundedReceiver, oneshot, RwLock}; use tracing::instrument; @@ -66,6 +66,26 @@ pub struct AtomaConfidentialCompute { service_shared_secret_receiver: UnboundedReceiver, /// Signal receiver for coordinating graceful shutdown of the service shutdown_signal: tokio::sync::watch::Receiver, + /// Provider for the confidential compute service + confidential_compute_provider: Option, +} + +/// Represents the supported confidential compute providers. +/// +/// This enum defines the different types of confidential computing technologies +/// that can be used for secure computation and attestation. +/// +/// # Variants +/// * `IntelTdx` - Intel Trust Domain Extensions (TDX) technology +/// * `AmdSevSnp` - AMD Secure Encrypted Virtualization with Secure Nested Paging (SEV-SNP) +/// +/// The variants are serialized in kebab-case format (e.g., "intel-tdx", "amd-sev-snp") +/// when converting to/from strings using the `EnumString` trait from the `strum` crate. +#[derive(Debug, Clone, Copy, EnumString)] +#[strum(serialize_all = "kebab-case")] +pub enum AtomaConfidentialComputeProvider { + IntelTdx, // intel-tdx + AmdSevSnp, // amd-sev-snp } impl AtomaConfidentialCompute { @@ -91,6 +111,7 @@ impl AtomaConfidentialCompute { service_decryption_receiver: UnboundedReceiver, service_encryption_receiver: UnboundedReceiver, service_shared_secret_receiver: UnboundedReceiver, + confidential_compute_provider: Option, shutdown_signal: tokio::sync::watch::Receiver, ) -> Result { let key_manager = X25519KeyPairManager::new()?; @@ -103,6 +124,7 @@ impl AtomaConfidentialCompute { service_encryption_receiver, service_shared_secret_receiver, shutdown_signal, + confidential_compute_provider, }) } @@ -136,6 +158,7 @@ impl AtomaConfidentialCompute { service_decryption_receiver: UnboundedReceiver, service_encryption_receiver: UnboundedReceiver, service_shared_secret_receiver: UnboundedReceiver, + confidential_compute_provider: Option, shutdown_signal: tokio::sync::watch::Receiver, ) -> Result<()> { let mut service = Self::new( @@ -144,11 +167,12 @@ impl AtomaConfidentialCompute { service_decryption_receiver, service_encryption_receiver, service_shared_secret_receiver, + confidential_compute_provider, shutdown_signal, )?; - // NOTE: Submit the first node key rotation attestation, because the node is starting up afresh - service.submit_node_key_rotation_tdx_attestation().await?; + service.submit_node_key_rotation_attestation().await?; + service.run().await?; Ok(()) @@ -267,51 +291,112 @@ impl AtomaConfidentialCompute { /// This function can return: /// - `AtomaConfidentialComputeError::KeyManagerError` if key rotation or public key retrieval fails /// - `AtomaConfidentialComputeError::SuiClientError` if the attestation submission to Sui fails + /// + /// + /// Make sure this is under a feature flag for TDX, and add a different CC implemntation for ADM SEV-SNP #[instrument(level = "debug", skip_all)] - async fn submit_node_key_rotation_tdx_attestation(&mut self) -> Result<()> { + async fn submit_node_key_rotation_attestation(&mut self) -> Result<()> { self.key_manager.rotate_keys(); - #[cfg(feature = "tdx")] + + if let Some(_cc_provider) = self.confidential_compute_provider { + #[cfg(feature = "tdx")] + if matches!(_cc_provider, AtomaConfidentialComputeProvider::IntelTdx) { + return self.submit_tdx_attestation().await; + } + #[cfg(feature = "sev-snp")] + if matches!(_cc_provider, AtomaConfidentialComputeProvider::AmdSevSnp) { + return self.submit_sev_snp_attestation().await; + } + } + + // If we get here, either the feature flags don't match or the provider isn't supported + tracing::warn!( + target = "atoma-confidential-compute-service", + provider = ?self.confidential_compute_provider, + "No attestation implementation available for provider" + ); + Ok(()) + } + + #[cfg(feature = "tdx")] + async fn submit_tdx_attestation(&mut self) -> Result<()> { + let public_key = self.key_manager.get_public_key(); + let public_key_bytes = public_key.to_bytes(); + let tdx_quote = tdx_attestation(&public_key_bytes)?; + let tdx_quote_bytes = tdx_quote.to_bytes(); + + match self + .sui_client + .write() + .await + .submit_key_rotation_remote_attestation( + public_key_bytes, + tdx_quote_bytes, + None, + None, + None, + ) + .await { - let public_key = self.key_manager.get_public_key(); - let public_key_bytes = public_key.to_bytes(); - let tdx_quote = get_compute_data_attestation(&public_key_bytes)?; - let tdx_quote_bytes = tdx_quote.to_bytes(); - match self - .sui_client - .write() - .await - .submit_key_rotation_remote_attestation( - public_key_bytes, - tdx_quote_bytes, - None, - None, - None, - ) - .await - { - Ok((digest, key_rotation_counter)) => { - tracing::info!( - target = "atoma-tdx-service", - digest = digest, - key_rotation_counter = key_rotation_counter, - "Submitted node key rotation attestation successfully" - ); - self.key_rotation_counter = Some(key_rotation_counter); - Ok(()) - } - Err(e) => { - tracing::error!( - target = "atoma-tdx-service", - error = %e, - "Failed to submit node key rotation attestation" - ); - Err(AtomaConfidentialComputeError::SuiClientError(e)) - } + Ok((digest, key_rotation_counter)) => { + tracing::info!( + target = "atoma-confidential-compute-service", + digest = digest, + key_rotation_counter = key_rotation_counter, + "Submitted TDX attestation successfully" + ); + self.key_rotation_counter = Some(key_rotation_counter); + Ok(()) + } + Err(e) => { + tracing::error!( + target = "atoma-confidential-compute-service", + error = %e, + "Failed to submit TDX attestation" + ); + Err(AtomaConfidentialComputeError::SuiClientError(e)) } } - #[cfg(not(feature = "tdx"))] + } + + #[cfg(feature = "sev-snp")] + async fn submit_sev_snp_attestation(&mut self) -> Result<()> { + let public_key = self.key_manager.get_public_key(); + let public_key_bytes = public_key.to_bytes(); + let sev_snp_quote = snp_attestation(&public_key_bytes)?; + let sev_snp_quote_bytes = sev_snp_quote.to_bytes(); + + match self + .sui_client + .write() + .await + .submit_key_rotation_remote_attestation( + public_key_bytes, + sev_snp_quote_bytes, + None, + None, + None, + ) + .await { - Ok(()) + Ok((digest, key_rotation_counter)) => { + tracing::info!( + target = "atoma-confidential-compute-service", + digest = digest, + key_rotation_counter = key_rotation_counter, + "Submitted SEV-SNP attestation successfully" + ); + self.key_rotation_counter = Some(key_rotation_counter); + Ok(()) + } + Err(e) => { + tracing::error!( + target = "atoma-confidential-compute-service", + error = %e, + "Failed to submit SEV-SNP attestation" + ); + Err(AtomaConfidentialComputeError::SuiClientError(e)) + } } } @@ -534,7 +619,7 @@ impl AtomaConfidentialCompute { .key_rotation_counter .map_or(true, |counter| counter < event.key_rotation_counter) { - self.submit_node_key_rotation_tdx_attestation().await?; + self.submit_node_key_rotation_attestation().await?; } } _ => { @@ -560,4 +645,7 @@ pub enum AtomaConfidentialComputeError { #[cfg(feature = "tdx")] #[error("TDX device error: {0}")] TdxDeviceError(#[from] TdxError), + #[cfg(feature = "sev-snp")] + #[error("SEV-SNP device error: {0}")] + SevSnpDeviceError(#[from] SnpError), } diff --git a/atoma-confidential/src/sev_snp.rs b/atoma-confidential/src/sev_snp.rs new file mode 100644 index 00000000..c92d3f27 --- /dev/null +++ b/atoma-confidential/src/sev_snp.rs @@ -0,0 +1,173 @@ +use thiserror::Error; +use bincode::{deserialize, serialize}; +use sev::{ + certs::sev::Chain, + firmware::guest::{Firmware, AttestationReport}, +}; + +pub const SEV_SNP_REPORT_DATA_SIZE: usize = 64; + +type Result = std::result::Result; + +/// Generates an SEV-SNP attestation report for the given compute data. +/// +/// This function takes a slice of bytes representing the attested data and returns +/// an AttestationReport. The attested data must be exactly 64 bytes long, +/// matching the SEV_SNP_REPORT_DATA_SIZE constant. +/// +/// # Arguments +/// +/// * `attested_data` - A slice of bytes containing the data to be attested for (public key) +/// +/// # Returns +/// +/// * `Result` - On success, returns an AttestationReport. +/// On failure, returns a SnpError. +/// +/// # Errors +/// +/// Returns `SnpError::InvalidAttestedDataSize` if the input data size is not 64 bytes. +/// Returns `SnpError::FailedToOpenFirmware` if the firmware interface cannot be opened. +/// Returns `SnpError::FailedToGetAttestationReport` if the attestation report cannot be generated. +/// +/// # Example +/// +/// ``` +/// let data_to_attest = [1u8; 64]; +/// match get_compute_data_attestation(&data_to_attest) { +/// Ok(report) => println!("Attestation successful"), +/// Err(e) => eprintln!("Attestation failed: {}", e), +/// } +/// ``` +#[cfg(all(target_os = "linux", feature = "sev-snp"))] +pub fn get_compute_data_attestation(attested_data: &[u8]) -> Result { + if attested_data.len() != SEV_SNP_REPORT_DATA_SIZE { + return Err(SnpError::InvalidAttestedDataSize(attested_data.len())); + } + + // Open the SEV-SNP firmware interface + let mut firmware = Firmware::open().map_err(SnpError::FailedToOpenFirmware)?; + + // Message version is set as a protocol version indicator, default is 1. + let message_version = 1; + + // Virtual Machine Privilege Level, 0 = least privileged, 3 = most privileged. + // VMPL allows for page security levels in the SNP Guest. + // If VMPL is disabled on the platform, set to 0. + // See: https://docs.enclaive.cloud/confidential-cloud/technology-in-depth/amd-sev/technology/fundamentals/features/virtual-machine-privilege-levels + let vmpl = 0; + + // Ask the PSP to generate an extended attestation report for the given data (the atoma-proxy will need to verify the report) to store key-rotation event on-chain. + let (chain, report): (Chain, AttestationReport) = firmware + .get_ext_report(Some(message_version), Some(attested_data), Some(vmpl)) + .map_err(SnpError::FailedToGetAttestationReport)?; + + let snp_attestation_report = (chain, report).to_snp_attestation_report(); + + Ok(snp_attestation_report) +} + +/// A wrapper struct for the attestation report and certificate chain received from the SNP_GET_EXT_REPORT ioctl. +#[derive(Debug, Clone)] +pub struct SNPAttestationReport { + /// The chain of certificates that form the chain of trust for the attestation report. + pub chain: Chain, + /// The attestation report generated by the PSP. + pub report: AttestationReport, +} + +impl crate::ToBytes for SNPAttestationReport { + fn to_bytes(&self) -> Vec { + bincode::serialize(&self).unwrap() + } +} + +/// Trait for converting Vec to an SNPAttestationReport. +pub trait FromBytes { + fn from_bytes(bytes: Vec) -> Result; +} + +impl FromBytes for SNPAttestationReport { + fn from_bytes(bytes: Vec) -> Result { + bincode::deserialize(&bytes).map_err(|e| { + SnpError::FailedToDeserializeBytes(format!( + "Unable to deserialize bytes for SNPAttestationReport: {}", + e + )) + }) + } +} + +/// Trait for converting a tuple of (Chain, AttestationReport) to an SNPAttestationReport. +pub trait ToSNPAttestationReport { + fn to_snp_attestation_report(&self) -> SNPAttestationReport; +} + +impl ToSNPAttestationReport for (Chain, AttestationReport) { + fn to_snp_attestation_report(&self) -> SNPAttestationReport { + SNPAttestationReport { + chain: self.0, + report: self.1, + } + } +} + +/// Implements the Verifiable trait (crypto_nossl feature-gated implementation) for SNPAttestationReport. +#[cfg(all(target_os = "linux", feature = "sev-snp"))] +impl sev::snp::certs::Verifiable for SNPAttestationReport { + type Output = bool; + + /// Verifies the attestation report against the VCEK certificate. + /// Taken from: https://docs.rs/sev/5.0.0/src/sev/firmware/guest/types/snp.rs.html#351-391 + fn verify(&self) -> Result<(), SnpError> { + // According to Chapter 3 of the [Versioned Chip Endorsement Key (VCEK) Certificate and + // KDS Interface Specification][spec], the VCEK certificate certifies an ECDSA public key on curve P-384, + // and the signature hash algorithm is sha384. + // [spec]: https://www.amd.com/content/dam/amd/en/documents/epyc-technical-docs/specifications/57230.pdf + + let vcek = self.0.verify()?; + let sig = p384::ecdsa::Signature::try_from(&self.1.signature)?; + + let measurable_bytes: &[u8] = &bincode::serialize(self.1).map_err(|e| { + SnpError::FailedVerification(format!( + "Unable to serialize bytes for AttestationReport: {}", + e + )) + })?[..0x2a0]; + + use sha2::Digest; + let base_digest = sha2::Sha384::new_with_prefix(measurable_bytes); + + let verifying_key = p384::ecdsa::VerifyingKey::from_sec1_bytes(vcek.public_key_sec1()) + .map_err(|e| { + SnpError::FailedVerification(format!( + "Failed to deserialize public key from sec1 bytes: {e:?}" + )) + })?; + + use p384::ecdsa::signature::DigestVerifier; + verifying_key.verify_digest(base_digest, &sig).map_err(|e| { + SnpError::FailedVerification(format!( + "VCEK does not sign the attestation report: {e:?}" + )) + }) + } +} + +#[derive(Error, Debug)] +pub enum SnpError { + #[error("Invalid attested data size: expected {expected}, got {actual}", expected = SEV_SNP_REPORT_DATA_SIZE, actual = _0)] + InvalidAttestedDataSize(usize), + #[error("Failed to verify attestation report: {0}")] + FailedVerification(#[source] std::io::Error), + #[error("Failed to open firmware interface: {0}")] + FailedToOpenFirmware(#[source] std::io::Error), + #[error("Failed to get attestation report: {0}")] + FailedToGetAttestationReport(#[source] std::io::Error), + #[error("Failed to serialize bytes for SNPAttestationReport: {0}")] + FailedToSerializeBytes(#[source] std::io::Error), + #[error("Failed to deserialize bytes for SNPAttestationReport: {0}")] + FailedToDeserializeBytes(#[source] std::io::Error), + #[error("SEV-SNP attestation is only supported on Linux platforms")] + UnsupportedPlatform, +} diff --git a/atoma-confidential/src/tdx.rs b/atoma-confidential/src/tdx.rs index 2deaec86..7ca05e72 100644 --- a/atoma-confidential/src/tdx.rs +++ b/atoma-confidential/src/tdx.rs @@ -1,4 +1,3 @@ -use crate::ToBytes; use dcap_rs::types::quotes::{body::QuoteBody, version_4::QuoteV4}; use tdx::{ device::{Device, DeviceOptions}, @@ -51,7 +50,7 @@ pub fn get_compute_data_attestation(attested_data: &[u8]) -> Result { Ok(device.get_attestation_report()?) } -impl ToBytes for QuoteV4 { +impl crate::ToBytes for QuoteV4 { fn to_bytes(&self) -> Vec { let mut bytes = Vec::new(); bytes.extend_from_slice(&self.header.to_bytes()); diff --git a/atoma-confidential/src/types.rs b/atoma-confidential/src/types.rs index 1efa045d..ba7a3b12 100644 --- a/atoma-confidential/src/types.rs +++ b/atoma-confidential/src/types.rs @@ -81,3 +81,49 @@ pub struct ConfidentialComputeSharedSecretResponse { /// Cryptographic nonce used in the encryption process pub nonce: [u8; NONCE_SIZE], } + +/// Represents the type of Trusted Execution Environment (TEE) provider used by a node. +/// +/// This enum identifies different TEE providers that can be used for secure computation +/// and attestation in the Atoma network. Each variant corresponds to a specific TEE +/// technology from different hardware vendors. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum TEEProvider { + /// Intel Trust Domain Extensions (TDX) technology + Tdx = 0, + /// AMD SEV with Secure Nested Paging (SNP) technology + Snp = 1, + /// ARM TrustZone technology + Arm = 2, +} + +impl TEEProvider { + /// Creates a TEEProvider from its byte representation. + /// + /// # Arguments + /// * `byte` - A byte containing the TEE provider identifier (0, 1, or 2) + /// + /// # Returns + /// * `Ok(TEEProvider)` - The corresponding TEE provider variant + /// * `Err(anyhow::Error)` - If the byte value is not recognized as a valid TEE provider + /// + /// # Errors + /// Returns an error if the input byte does not correspond to a known TEE provider variant. + pub fn from_byte(byte: u8) -> Result { + Ok(match byte { + 0 => Self::Tdx, + 1 => Self::Snp, + 2 => Self::Arm, + _ => anyhow::bail!("Invalid TEE provider"), + }) + } + + /// Converts the TEE provider enum to a byte representation. + /// + /// # Returns + /// A byte representing the TEE provider variant. + #[must_use] + pub const fn to_byte(self) -> u8 { + self as u8 + } +} diff --git a/atoma-service/Cargo.toml b/atoma-service/Cargo.toml index f867a641..a359b2a3 100644 --- a/atoma-service/Cargo.toml +++ b/atoma-service/Cargo.toml @@ -61,3 +61,4 @@ tempfile = { workspace = true } [features] tdx = ["atoma-confidential/tdx"] +sev-snp = ["atoma-confidential/sev-snp"] diff --git a/atoma-service/src/config.rs b/atoma-service/src/config.rs index 282b1000..ef4fe552 100644 --- a/atoma-service/src/config.rs +++ b/atoma-service/src/config.rs @@ -43,6 +43,9 @@ pub struct AtomaServiceConfig { /// /// This field specifies the address and port on which the Atoma Service will bind. pub service_bind_address: String, + + /// Provider for the confidential compute service + pub confidential_compute_provider: Option, } impl AtomaServiceConfig { diff --git a/atoma-service/src/tests.rs b/atoma-service/src/tests.rs index 3884a28a..40fc771e 100644 --- a/atoma-service/src/tests.rs +++ b/atoma-service/src/tests.rs @@ -265,6 +265,7 @@ mod middleware { decryption_receiver, encryption_receiver, compute_shared_secret_receiver, + None, shutdown_receiver, ) .expect("Failed to create confidential compute service"); diff --git a/atoma-state/src/handlers.rs b/atoma-state/src/handlers.rs index 65a459b1..f4559fc2 100644 --- a/atoma-state/src/handlers.rs +++ b/atoma-state/src/handlers.rs @@ -733,7 +733,7 @@ pub(crate) async fn handle_state_manager_event( /// # Behavior /// /// The function performs the following steps: -/// 1. Extracts the `epoch`, `node_id`, `new_public_key`, and `tee_remote_attestation_bytes` from the event. +/// 1. Extracts the `epoch`, `node_id`, `new_public_key`, `tee_remote_attestation_bytes`, and `tee_provider` from the event. /// 2. Calls the `insert_node_public_key_rotation` method on the `AtomaStateManager` to update the node's public key in the database. #[instrument(level = "info", skip_all)] async fn handle_node_key_rotation_event( @@ -751,6 +751,7 @@ async fn handle_node_key_rotation_event( node_id, new_public_key, tee_remote_attestation_bytes, + tee_provider, } = event; state_manager .state @@ -760,6 +761,7 @@ async fn handle_node_key_rotation_event( node_id.inner, new_public_key, tee_remote_attestation_bytes, + tee_provider, ) .await?; Ok(()) diff --git a/atoma-state/src/migrations/20241202121152_node_public_key_rotation.sql b/atoma-state/src/migrations/20241202121152_node_public_key_rotation.sql index 627736a2..9442dc70 100644 --- a/atoma-state/src/migrations/20241202121152_node_public_key_rotation.sql +++ b/atoma-state/src/migrations/20241202121152_node_public_key_rotation.sql @@ -3,5 +3,5 @@ CREATE TABLE IF NOT EXISTS node_public_key_rotations ( node_small_id BIGINT PRIMARY KEY, public_key_bytes BYTEA UNIQUE NOT NULL, tdx_quote_bytes BYTEA NOT NULL, - epoch BIGINT NOT NULL + epoch BIGINT NOT NULL ); diff --git a/atoma-state/src/migrations/20250206184237_node_public_key_rotation.sql b/atoma-state/src/migrations/20250206184237_node_public_key_rotation.sql new file mode 100644 index 00000000..e89a9b20 --- /dev/null +++ b/atoma-state/src/migrations/20250206184237_node_public_key_rotation.sql @@ -0,0 +1,5 @@ +ALTER TABLE node_public_key_rotations ( + DROP COLUMN tdx_quote_bytes, + ADD COLUMN tee_quote_bytes BYTEA NOT NULL, + ADD COLUMN tee_provider BYTEA NOT NULL, +); \ No newline at end of file diff --git a/atoma-state/src/state_manager.rs b/atoma-state/src/state_manager.rs index 336f0b55..0252c0f1 100644 --- a/atoma-state/src/state_manager.rs +++ b/atoma-state/src/state_manager.rs @@ -803,21 +803,24 @@ impl AtomaState { node_small_id: u64, public_key_bytes: Vec, tee_remote_attestation_bytes: Vec, + tee_provider: u16, ) -> Result<()> { sqlx::query( - "INSERT INTO node_public_key_rotations (epoch, key_rotation_counter, node_small_id, public_key_bytes, tdx_quote_bytes) VALUES ($1, $2, $3, $4, $5) - ON CONFLICT (node_small_id) - DO UPDATE SET + "INSERT INTO node_public_key_rotations (epoch, key_rotation_counter, node_small_id, public_key_bytes, tee_quote_bytes, tee_provider) VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT (node_small_id) + DO UPDATE SET epoch = $1, key_rotation_counter = $2, - public_key_bytes = $4, - tdx_quote_bytes = $5", + public_key_bytes = $4, + tee_quote_bytes = $5, + tee_provider = $6", ) .bind(epoch as i64) .bind(key_rotation_counter as i64) .bind(node_small_id as i64) .bind(public_key_bytes) .bind(tee_remote_attestation_bytes) + .bind(tee_provider as i16) .execute(&self.db) .await?; Ok(()) @@ -4475,6 +4478,7 @@ mod tests { let node_small_id = 789u64; let public_key_bytes = vec![1, 2, 3, 4]; let tee_remote_attestation_bytes = vec![5, 6, 7, 8]; + let tee_provider = 0u16; // Insert initial rotation state_manager @@ -4484,6 +4488,7 @@ mod tests { node_small_id, public_key_bytes.clone(), tee_remote_attestation_bytes.clone(), + tee_provider, ) .await?; @@ -4501,15 +4506,16 @@ mod tests { assert_eq!(row.get::("node_small_id"), node_small_id as i64); assert_eq!(row.get::, _>("public_key_bytes"), public_key_bytes); assert_eq!( - row.get::, _>("tdx_quote_bytes"), + row.get::, _>("tee_quote_bytes"), tee_remote_attestation_bytes ); - + assert_eq!(row.get::("tee_provider"), tee_provider as i16); // Test update with new values let new_epoch = 999u64; let new_key_rotation_counter = 888u64; let new_public_key_bytes = vec![9, 10, 11, 12]; let new_tee_remote_attestation_bytes = vec![13, 14, 15, 16]; + let new_tee_provider = 2u16; state_manager .insert_node_public_key_rotation( @@ -4518,6 +4524,7 @@ mod tests { node_small_id, new_public_key_bytes.clone(), new_tee_remote_attestation_bytes.clone(), + new_tee_provider, ) .await?; @@ -4542,9 +4549,13 @@ mod tests { new_public_key_bytes ); assert_eq!( - updated_row.get::, _>("tdx_quote_bytes"), + updated_row.get::, _>("tee_quote_bytes"), new_tee_remote_attestation_bytes ); + assert_eq!( + updated_row.get::("tee_provider"), + new_tee_provider as i16 + ); // Verify only one row exists let count = sqlx::query( @@ -4568,13 +4579,13 @@ mod tests { // Test data for multiple nodes let nodes = [ - (1u64, vec![1, 2, 3], vec![4, 5, 6]), - (2u64, vec![7, 8, 9], vec![10, 11, 12]), - (3u64, vec![13, 14, 15], vec![16, 17, 18]), + (1u64, vec![1, 2, 3], vec![4, 5, 6], 0u16), + (2u64, vec![7, 8, 9], vec![10, 11, 12], 1u16), + (3u64, vec![13, 14, 15], vec![16, 17, 18], 2u16), ]; // Insert rotations for multiple nodes - for (node_id, pub_key, tee_bytes) in &nodes { + for (node_id, pub_key, tee_bytes, tee_provider) in &nodes { state_manager .insert_node_public_key_rotation( 100u64, @@ -4582,12 +4593,13 @@ mod tests { *node_id, pub_key.clone(), tee_bytes.clone(), + *tee_provider, ) .await?; } // Verify all insertions - for (node_id, pub_key, tee_bytes) in &nodes { + for (node_id, pub_key, tee_bytes, tee_provider) in &nodes { let row = sqlx::query("SELECT * FROM node_public_key_rotations WHERE node_small_id = $1") .bind(*node_id as i64) @@ -4596,7 +4608,8 @@ mod tests { assert_eq!(row.get::("node_small_id"), *node_id as i64); assert_eq!(row.get::, _>("public_key_bytes"), *pub_key); - assert_eq!(row.get::, _>("tdx_quote_bytes"), *tee_bytes); + assert_eq!(row.get::, _>("tee_quote_bytes"), *tee_bytes); + assert_eq!(row.get::("tee_provider"), *tee_provider as i16); } // Verify total count @@ -4626,6 +4639,7 @@ mod tests { node_small_id, empty_bytes.clone(), empty_bytes.clone(), + 0u16, ) .await?; @@ -4635,7 +4649,8 @@ mod tests { .await?; assert_eq!(row.get::, _>("public_key_bytes"), empty_bytes); - assert_eq!(row.get::, _>("tdx_quote_bytes"), empty_bytes); + assert_eq!(row.get::, _>("tee_quote_bytes"), empty_bytes); + assert_eq!(row.get::("tee_provider"), 0_i16); truncate_tables(&state_manager.db).await; Ok(()) diff --git a/atoma-sui/src/client.rs b/atoma-sui/src/client.rs index c6a866e8..21abf8c4 100644 --- a/atoma-sui/src/client.rs +++ b/atoma-sui/src/client.rs @@ -1024,13 +1024,13 @@ impl Client { /// Submits a transaction to rotate a node's key with remote attestation in the Atoma network. /// - /// This method creates and submits a transaction that rotates a node's key using Intel TDX remote + /// This method creates and submits a transaction that rotates a node's key using remote /// attestation. The node must have a valid node badge to perform this operation. The method requires /// both the TDX quote bytes (remote attestation proof) and the new public key bytes. /// /// # Arguments /// - /// * `tdx_quote_bytes` - A vector of bytes containing the Intel TDX remote attestation quote + /// * `tdx_quote_bytes` - A vector of bytes containing the remote attestation quote /// * `public_key_bytes` - A 32-byte array containing the new public key /// * `gas` - Optional ObjectID to use as gas for the transaction. If None, the system will /// automatically select a gas object @@ -1055,12 +1055,12 @@ impl Client { /// use sui_sdk::types::base_types::ObjectID; /// /// async fn example(client: &mut AtomaSuiClient) -> Result<()> { - /// let tdx_quote = vec![1, 2, 3, 4]; // Your TDX quote bytes + /// let quote = vec![1, 2, 3, 4]; // Your quote bytes /// let public_key = [0u8; 32]; // Your new public key /// /// // Submit with default gas settings /// let tx_digest = client.submit_key_rotation_remote_attestation( - /// tdx_quote, + /// quote, /// public_key, /// None, // default gas /// None, // default gas budget @@ -1074,12 +1074,12 @@ impl Client { #[instrument(level = "info", skip_all, fields( address = %self.wallet_ctx.active_address().unwrap(), public_key = %hex::encode(public_key_bytes), - remote_attestation_quote = %hex::encode(&tdx_quote_bytes) + remote_attestation_quote = %hex::encode("e_bytes) ))] pub async fn submit_key_rotation_remote_attestation( &mut self, public_key_bytes: [u8; 32], - tdx_quote_bytes: Vec, + quote_bytes: Vec, gas: Option, gas_budget: Option, gas_price: Option, @@ -1104,7 +1104,7 @@ impl Client { SuiJsonValue::from_object_id(self.config.atoma_db()), SuiJsonValue::from_object_id(node_badge_id), SuiJsonValue::new(public_key_bytes.to_vec().into())?, - SuiJsonValue::new(tdx_quote_bytes.into())?, + SuiJsonValue::new(quote_bytes.into())?, ], gas, gas_budget.unwrap_or(GAS_BUDGET), diff --git a/atoma-sui/src/events.rs b/atoma-sui/src/events.rs index 5cbe5597..9abad677 100644 --- a/atoma-sui/src/events.rs +++ b/atoma-sui/src/events.rs @@ -839,6 +839,11 @@ pub struct NodePublicKeyCommittmentEvent { /// The TEE remote attestation report attesting for /// the public key's generation integrity, in byte format. pub tee_remote_attestation_bytes: Vec, + + /// The TEE Provider used to generate the public key. + /// This type is used to identify which attestation + /// verification method should be used. + pub tee_provider: u16, } /// Represents an event emitted when Atoma's smart contract requests new node key rotation. diff --git a/config.example.toml b/config.example.toml index d3de6af7..44fcee2f 100644 --- a/config.example.toml +++ b/config.example.toml @@ -6,6 +6,7 @@ image_generations_service_url = "http://image-generations:80" models = ["meta-llama/Llama-3.2-3B-Instruct"] revisions = ["main"] service_bind_address = "0.0.0.0:3000" +confidential_compute_provider = "intel-tdx" [atoma_sui] http_rpc_node_addr = "https://fullnode.testnet.sui.io:443" # Current RPC node address for testnet