diff --git a/.github/workflows/azure.yml b/.github/workflows/azure.yml new file mode 100644 index 00000000..f1def1ea --- /dev/null +++ b/.github/workflows/azure.yml @@ -0,0 +1,23 @@ +name: "SNP End-to-End Tests" + +on: + push: + branches: [main] + workflow_dispatch: + +defaults: + run: + shell: bash + +# Cancel previous running actions for the same PR +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + snp-vtpm-test: + runs-on: [self-hosted] + steps: + - name: "Check out the code" + uses: actions/checkout@v4 + diff --git a/.github/workflows/snp.yml b/.github/workflows/snp.yml index 06cf96a9..6050f9d9 100644 --- a/.github/workflows/snp.yml +++ b/.github/workflows/snp.yml @@ -44,7 +44,7 @@ jobs: # Build SNP applications and embed the attestation service's certificate. - name: "Build SNP applications" - run: ./scripts/accli_wrapper.sh applications build --clean --as-cert-path ./certs/cert.pem --in-cvm + run: ./scripts/accli_wrapper.sh applications build --clean --as-cert-dir ./certs/cert.pem --in-cvm - name: "Run supported SNP applications" run: | diff --git a/Cargo.lock b/Cargo.lock index 2c77c469..f25b910b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,6 +89,7 @@ name = "accli" version = "0.9.2" dependencies = [ "anyhow", + "az-snp-vtpm", "base64 0.22.1", "bytes", "chrono", diff --git a/accless/libs/attestation/CMakeLists.txt b/accless/libs/attestation/CMakeLists.txt index 134c8154..43ac3ba0 100644 --- a/accless/libs/attestation/CMakeLists.txt +++ b/accless/libs/attestation/CMakeLists.txt @@ -25,6 +25,7 @@ add_library(${CMAKE_PROJECT_TARGET} mock_snp.cpp utils.cpp snp.cpp + vcek_cache.cpp ) target_link_libraries(${CMAKE_PROJECT_TARGET} PUBLIC azguestattestation diff --git a/accless/libs/attestation/http_client.cpp b/accless/libs/attestation/http_client.cpp index 6d69d6b5..24261ed9 100644 --- a/accless/libs/attestation/http_client.cpp +++ b/accless/libs/attestation/http_client.cpp @@ -1,4 +1,5 @@ #include "attestation.h" +#include "vcek_cache.h" #include #include @@ -19,6 +20,11 @@ static size_t curlWriteCallback(char *ptr, size_t size, size_t nmemb, } HttpClient::HttpClient(const std::string &certPath) : certPath_(certPath) { + // Initialize process-wide VCEK cache once. This is only populated if + // we are deployed inside an Azure cVM, otherwise will return empty + // strings. + accless::attestation::snp::getVcekPemBundle(); + curl_ = curl_easy_init(); if (!curl_) { throw std::runtime_error("accless(att): failed to init curl"); diff --git a/accless/libs/attestation/utils.cpp b/accless/libs/attestation/utils.cpp index ee41ae28..52fe23bf 100644 --- a/accless/libs/attestation/utils.cpp +++ b/accless/libs/attestation/utils.cpp @@ -1,4 +1,5 @@ #include "attestation.h" +#include "vcek_cache.h" #include @@ -49,6 +50,15 @@ std::string buildRequestBody(const std::string "eB64, body["quote"] = quoteB64; body["runtimeData"]["data"] = runtimeB64; body["runtimeData"]["dataType"] = "Binary"; + + const auto &vcekPem = accless::attestation::snp::getVcekCertPem(); + const auto &chainPem = accless::attestation::snp::getVcekChainPem(); + + if (!vcekPem.empty()) { + body["collateral"]["vcekCertPem"] = vcekPem; + body["collateral"]["certificateChainPem"] = chainPem; + } + return body.dump(); } } // namespace accless::attestation::utils diff --git a/accless/libs/attestation/vcek_cache.cpp b/accless/libs/attestation/vcek_cache.cpp new file mode 100644 index 00000000..c1126026 --- /dev/null +++ b/accless/libs/attestation/vcek_cache.cpp @@ -0,0 +1,123 @@ +#include "vcek_cache.h" + +#include +#include + +#include +#include + +namespace accless::attestation::snp { +namespace { + +constexpr const char *THIM_URL = + "http://169.254.169.254/metadata/THIM/amd/certification"; +constexpr const char *THIM_METADATA_HEADER = "Metadata:true"; + +struct VcekCache { + std::once_flag once; + std::string vcekCert; + std::string certChain; + std::string bundle; + std::string error; +}; + +VcekCache g_cache; + +size_t curlWriteCallback(char *ptr, size_t size, size_t nmemb, void *userdata) { + auto *out = static_cast(userdata); + if (!out) + return 0; + const size_t total = size * nmemb; + out->append(ptr, total); + return total; +} + +// Perform one-time VCEK fetch, but *never throw*. If unavailable, returns empty +// PEM strings. +void initVcekCache() { + CURL *curl = curl_easy_init(); + if (!curl) { + g_cache.error = "failed to init curl for VCEK fetch"; + return; + } + + std::string response; + char errbuf[CURL_ERROR_SIZE] = {0}; + + curl_easy_setopt(curl, CURLOPT_URL, THIM_URL); + curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlWriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf); + curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 500L); // do not block + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 300L); + + // Add Metadata:true header + struct curl_slist *headers = nullptr; + headers = curl_slist_append(headers, THIM_METADATA_HEADER); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + + CURLcode res = curl_easy_perform(curl); + long status = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status); + + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, nullptr); + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + + // IMDS unreachable → not a CVM → silently return empty values + if (res != CURLE_OK) { + g_cache.error = std::string("VCEK fetch failed: curl error: ") + + (errbuf[0] ? errbuf : curl_easy_strerror(res)); + return; + } + + if (status != 200) { + g_cache.error = + "VCEK fetch failed: HTTP status " + std::to_string(status); + return; + } + + // Parse JSON. + try { + auto json = nlohmann::json::parse(response); + + g_cache.vcekCert = json.value("vcekCert", ""); + g_cache.certChain = json.value("certificateChain", ""); + + // Normalize newlines + if (!g_cache.vcekCert.empty() && g_cache.vcekCert.back() != '\n') + g_cache.vcekCert.push_back('\n'); + + if (!g_cache.certChain.empty() && g_cache.certChain.back() != '\n') + g_cache.certChain.push_back('\n'); + + g_cache.bundle = g_cache.vcekCert + g_cache.certChain; + } catch (const std::exception &e) { + g_cache.error = std::string("VCEK fetch JSON parse error: ") + e.what(); + // Leave empty certs + return; + } +} + +void ensureInitialized() { std::call_once(g_cache.once, initVcekCache); } + +} // namespace + +// --- Public API --- + +const std::string &getVcekPemBundle() { + ensureInitialized(); + return g_cache.bundle; +} + +const std::string &getVcekCertPem() { + ensureInitialized(); + return g_cache.vcekCert; +} + +const std::string &getVcekChainPem() { + ensureInitialized(); + return g_cache.certChain; +} +} // namespace accless::attestation::snp diff --git a/accless/libs/attestation/vcek_cache.h b/accless/libs/attestation/vcek_cache.h new file mode 100644 index 00000000..8e3c3c44 --- /dev/null +++ b/accless/libs/attestation/vcek_cache.h @@ -0,0 +1,14 @@ +#pragma once + +#include +#include + +namespace accless::attestation::snp { + +// Returns concatenated PEM (VCEK + chain) or an empty string on failure. +const std::string &getVcekPemBundle(); + +// If you prefer separate pieces: +const std::string &getVcekCertPem(); +const std::string &getVcekChainPem(); +} // namespace accless::attestation::snp diff --git a/accli/Cargo.toml b/accli/Cargo.toml index a90669f3..a9ae286b 100644 --- a/accli/Cargo.toml +++ b/accli/Cargo.toml @@ -17,6 +17,7 @@ path = "src/lib.rs" [dependencies] anyhow.workspace = true +az-snp-vtpm.workspace = true base64.workspace = true bytes.workspace = true chrono.workspace = true diff --git a/accli/src/tasks/experiments/plot.rs b/accli/src/tasks/experiments/plot.rs index 46529480..5ca9eb8f 100644 --- a/accli/src/tasks/experiments/plot.rs +++ b/accli/src/tasks/experiments/plot.rs @@ -13,7 +13,14 @@ use csv::ReaderBuilder; use log::{debug, error, info}; use plotters::prelude::*; use serde::Deserialize; -use std::{collections::BTreeMap, env, fs, path::PathBuf, str}; +use std::{ + collections::BTreeMap, + env, + fs::{self, File}, + io::{BufRead, BufReader}, + path::PathBuf, + str, +}; fn get_all_data_files(exp: &Experiment) -> Result> { let data_path = format!("{}/{exp}/data", Env::experiments_root().display()); @@ -1079,7 +1086,7 @@ fn plot_escrow_xput(data_files: &Vec) { root.fill(&WHITE).unwrap(); let x_max = 200; - let y_max: f64 = 5.0; + let y_max: f64 = 2.0; let mut chart = ChartBuilder::on(&root) .x_label_area_size(40) .y_label_area_size(40) @@ -1120,15 +1127,19 @@ fn plot_escrow_xput(data_files: &Vec) { .unwrap(); for (baseline, values) in data { - info!("{baseline}"); // Draw line + let mut point_exceeded: Option<(i32, f64)> = None; chart .draw_series(LineSeries::new( (0..values.len()) .zip(values[0..].iter()) - .filter(|(_, y)| **y <= y_max) + // .filter(|(_, y)| **y <= y_max) .map(|(x, y)| { - ( + // Draw the line until the last point that exceeds y_max. + if let Some(point_exceeded) = point_exceeded { + return point_exceeded; + } + let point = ( match baseline { EscrowBaseline::Trustee | EscrowBaseline::Accless @@ -1139,7 +1150,13 @@ fn plot_escrow_xput(data_files: &Vec) { EscrowBaseline::ManagedHSM => REQUEST_COUNTS_MHSM[x] as i32, }, *y, - ) + ); + + if point_exceeded.is_none() & (*y > y_max) { + point_exceeded = Some(point); + } + + point }), EscrowBaseline::get_color(&baseline) .unwrap() @@ -1245,23 +1262,64 @@ fn plot_escrow_xput(data_files: &Vec) { ); } +fn get_escrow_cost_data(path: &str, n: usize) -> Vec { + let file = File::open(path).expect("get_escrow_cost_data(): could not open file (path={path})"); + let reader = BufReader::new(file); + + // 1. Parse the file and filter for NumRequests == 1 + let data: Vec = reader + .lines() + .skip(1) // Skip CSV header + .filter_map(|line| line.ok()) // Unwrap lines safely + .filter_map(|line| { + let parts: Vec<&str> = line.split(',').collect(); + + // Check if first column (NumRequests) is "1" + if parts.first()?.trim() == "1" { + // Parse second column (TimeElapsed) + parts.get(1)?.trim().parse::().ok() + } else { + None + } + }) + .collect(); + + if data.is_empty() { + panic!("get_escrow_cost_data(): no entries for 1 request found (path={path})"); + } + + // 2. Cycle through the data to fill N positions + data.into_iter().cycle().take(n).collect() +} + fn plot_escrow_cost() { - // Rounded monthly cost (in USD) of a Standard_DCas_v5 as of 17/04/2025 + // Rounded monthly cost (in USD) of a Standard_DCas_v5 as of 17/04/2025. const UNIT_MONTHLY_COST_DC2: u32 = 62; - // FIXME: grab this values from the escrow-xput run. - const ACCLESS_LATENCY_D2: &[f64] = &[ - 0.091594, 0.100394, 0.107495, 0.111224, 0.11829, 0.122359, 0.126534, 0.131722, 0.127414, - 0.123334, - ]; - let trustee_latency_single_req = 0.05; + let trustee_latency_single_req = get_escrow_cost_data( + &format!( + "{}/escrow-xput/data/{}.csv", + Env::experiments_root().display(), + EscrowBaseline::Trustee + ), + 1_usize, + )[0]; // Variables: let trustee_unit_cost = UNIT_MONTHLY_COST_DC2; let accless_unit_cost = UNIT_MONTHLY_COST_DC2 * 3; + let accless_single_auth_unit_cost = UNIT_MONTHLY_COST_DC2; let num_max_users = 10; - let accless_latency: Vec<(u32, f64)> = (1..=ACCLESS_LATENCY_D2.len() as u32) - .map(|x| (x, ACCLESS_LATENCY_D2[x as usize - 1])) + let accless_latency_ys = get_escrow_cost_data( + &format!( + "{}/escrow-xput/data/{}.csv", + Env::experiments_root().display(), + EscrowBaseline::Accless + ), + num_max_users as usize, + ); + let accless_latency: Vec<(u32, f64)> = (1..=accless_latency_ys.len() as u32) + .map(|x| (x, accless_latency_ys[x as usize - 1])) .collect(); let mut plot_path = Env::experiments_root() @@ -1272,6 +1330,7 @@ fn plot_escrow_cost() { let root = SVGBackend::new(&plot_path, (400, 300)).into_drawing_area(); root.fill(&WHITE).unwrap(); + let y_max = 75.0; let mut chart = ChartBuilder::on(&root) .margin(10) .margin_top(40) @@ -1281,7 +1340,7 @@ fn plot_escrow_cost() { .x_label_area_size(40) .y_label_area_size(40) .right_y_label_area_size(40) - .build_cartesian_2d(1u32..num_max_users, 0.0f64..300.0) + .build_cartesian_2d(1u32..num_max_users, 0.0f64..y_max) .unwrap() .set_secondary_coord( 1u32..num_max_users, @@ -1401,24 +1460,49 @@ fn plot_escrow_cost() { })) .unwrap(); + // Accless-single-auth cost + let accless_cost: Vec<(u32, f64)> = (1..=num_max_users) + .map(|x| (x, (accless_single_auth_unit_cost) as f64)) + .collect(); + chart + .draw_secondary_series(LineSeries::new( + accless_cost.clone(), + EscrowBaseline::get_color(&EscrowBaseline::AcclessSingleAuth) + .unwrap() + .stroke_width(STROKE_WIDTH), + )) + .unwrap(); + chart + .draw_secondary_series(accless_cost.into_iter().map(|(x, y)| { + Circle::new( + (x, y), + 7, + EscrowBaseline::get_color(&EscrowBaseline::AcclessSingleAuth) + .unwrap() + .filled(), + ) + })) + .unwrap(); + + // Draw black frame. chart .plotting_area() .draw(&PathElement::new( - vec![(0, 1.0), (num_max_users, 1.0)], + vec![(1u32, y_max), (num_max_users, y_max)], BLACK, )) .unwrap(); chart .plotting_area() .draw(&PathElement::new( - vec![(num_max_users, 0.0), (num_max_users, 1.0)], + vec![(num_max_users, 0.0), (num_max_users, y_max)], BLACK, )) .unwrap(); fn legend_label_pos_for_baseline(baseline: &EscrowBaseline) -> (i32, i32) { let legend_x_start = 20; - let legend_y_pos = 6; + let legend_y_pos = 2; match baseline { EscrowBaseline::Accless => (legend_x_start, legend_y_pos), diff --git a/accli/src/tasks/experiments/ubench.rs b/accli/src/tasks/experiments/ubench.rs index 74c5272d..b9ade720 100644 --- a/accli/src/tasks/experiments/ubench.rs +++ b/accli/src/tasks/experiments/ubench.rs @@ -11,10 +11,13 @@ use crate::{ }, }; use anyhow::Result; +use az_snp_vtpm::{hcl::HclReport, report::AttestationReport, vtpm}; +use base64::Engine; use clap::Args; use futures::stream::{self, StreamExt}; use log::error; use std::{ + collections::HashMap, fs, fs::File, io::{BufWriter, Write}, @@ -48,7 +51,7 @@ pub struct UbenchRunArgs { // Trustee methods and constants // ------------------------------------------------------------------------- -const TEE: &str = "azsnpvtpm"; +const TEE: &str = "az-snp-vtpm"; fn get_coco_code_dir() -> String { format!( @@ -81,13 +84,212 @@ fn get_kbs_client_path() -> String { format!("{}/trustee/target/release/kbs-client", get_coco_code_dir()) } +async fn compute_reference_values() -> Result> { + let report = vtpm::get_report()?; + let quote = vtpm::get_quote(&[])?; + let hcl_report = HclReport::new(report.clone())?; + let snp_report: AttestationReport = hcl_report.try_into()?; + + let reference_values: HashMap<&str, String> = HashMap::from([ + ( + "measurement", + base64::engine::general_purpose::STANDARD.encode(snp_report.measurement), + ), + ( + "snp_pcr11", + hex::encode( + quote + .pcrs_sha256() + .nth(11) + .expect("Expected at least 12 PCRs in quote"), + ), + ), + ( + "tcb_bootloader", + snp_report.reported_tcb.bootloader.to_string(), + ), + ( + "tcb_microcode", + snp_report.reported_tcb.microcode.to_string(), + ), + ("tcb_snp", snp_report.reported_tcb.snp.to_string()), + ("tcb_tee", snp_report.reported_tcb.tee.to_string()), + ("abi_major", snp_report.policy.abi_major().to_string()), + ("abi_minor", snp_report.policy.abi_minor().to_string()), + ( + "single_socket", + snp_report.policy.single_socket_required().to_string(), + ), + ("smt_allowed", snp_report.policy.smt_allowed().to_string()), + ( + "smt_enabled", + snp_report.plat_info.smt_enabled().to_string(), + ), + ( + "tsme_enabled", + snp_report.plat_info.tsme_enabled().to_string(), + ), + ]); + + Ok(reference_values) +} + +async fn set_reference_values(escrow_url: &str) -> Result<()> { + let reference_values = compute_reference_values().await?; + + for (name, value) in &reference_values { + let output = Command::new("sudo") + .args([ + "-E", + &get_kbs_client_path(), + "--url", + &format!("https://{escrow_url}:8080"), + "--cert-file", + &get_https_cert(), + "config", + "--auth-private-key", + &get_kbs_key(), + "set-sample-reference-value", + name, + value, + ]) + .output()?; + + if !output.status.success() { + let reason = format!("error setting reference value (name={name})"); + error!("set_reference_values(): {reason}"); + error!( + "set_reference_values(): stdout: {}", + String::from_utf8_lossy(&output.stdout) + ); + error!( + "set_reference_values(): stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + anyhow::bail!(reason); + } + } + + Ok(()) +} + +async fn set_attestation_policy(escrow_url: &str) -> Result<()> { + let reference_values = compute_reference_values().await?; + let tee_att_policy_rego = format!( + r#" +package policy +import rego.v1 + +default executables := 33 +default hardware := 97 +default configuration := 36 +default file_system := 0 +default instance_identity := 0 +default runtime_opaque := 0 +default storage_opaque := 0 +default sourced_data := 0 + +az := input.az_snp_vtpm + +executables := 3 if {{ + az + az.measurement == "{measurement}" + az.tpm.pcr11 == "{snp_pcr11}" +}} + +hardware := 2 if {{ + az + az.reported_tcb_bootloader == "{tcb_bootloader}" + az.reported_tcb_microcode == "{tcb_microcode}" + az.reported_tcb_snp == "{tcb_snp}" + az.reported_tcb_tee == "{tcb_tee}" +}} + +configuration := 2 if {{ + az + az.platform_smt_enabled == "{smt_enabled}" + az.platform_tsme_enabled == "{tsme_enabled}" + az.policy_abi_major == "{abi_major}" + az.policy_abi_minor == "{abi_minor}" + az.policy_single_socket == "{single_socket}" + az.policy_smt_allowed == "{smt_allowed}" +}} + +trust_claims := {{ + "executables": executables, + "hardware": hardware, + "configuration": configuration, + "file-system": file_system, + "instance-identity": instance_identity, + "runtime-opaque": runtime_opaque, + "storage-opaque": storage_opaque, + "sourced-data": sourced_data, +}} +"#, + measurement = reference_values["measurement"], + snp_pcr11 = reference_values["snp_pcr11"], + tcb_bootloader = reference_values["tcb_bootloader"], + tcb_microcode = reference_values["tcb_microcode"], + tcb_snp = reference_values["tcb_snp"], + tcb_tee = reference_values["tcb_tee"], + smt_enabled = reference_values["smt_enabled"], + tsme_enabled = reference_values["tsme_enabled"], + abi_major = reference_values["abi_major"], + abi_minor = reference_values["abi_minor"], + single_socket = reference_values["single_socket"], + smt_allowed = reference_values["smt_allowed"], + ); + + let tmp_file = "/tmp/tee_attestation_policy.rego"; + fs::write(tmp_file, &tee_att_policy_rego)?; + + let output = Command::new("sudo") + .args([ + "-E", + &get_kbs_client_path(), + "--url", + &format!("https://{escrow_url}:8080"), + "--cert-file", + &get_https_cert(), + "config", + "--auth-private-key", + &get_kbs_key(), + "set-attestation-policy", + "--policy-file", + tmp_file, + "--id", + "default_cpu", + ]) + .output()?; + + if !output.status.success() { + let reason = "error setting attestation policy"; + error!("set_attestation_policy(): {reason}"); + error!( + "set_attestation_policy(): stdout: {}", + String::from_utf8_lossy(&output.stdout) + ); + error!( + "set_attestation_policy(): stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + anyhow::bail!(reason); + } + + Ok(()) +} + async fn set_resource_policy(escrow_url: &str) -> Result<()> { let tee_policy_rego = format!( r#" package policy default allow = false allow if {{ -input["submods"]["cpu"]["ear.veraison.annotated-evidence"]["{}"] + az := input.submods.cpu0["ear.veraison.annotated-evidence"]["{}"] + + # FIXME (#67): if we comment this out, no matter how we configure the + # policies or reference values, we can not get it to 'affirming'. + # input.submods.cpu0["ear.status"] == "affirming" }} "#, TEE @@ -96,7 +298,7 @@ input["submods"]["cpu"]["ear.veraison.annotated-evidence"]["{}"] let tmp_file = "/tmp/tee_policy.rego"; fs::write(tmp_file, &tee_policy_rego)?; - Command::new("sudo") + let output = Command::new("sudo") .args([ "-E", &get_kbs_client_path(), @@ -113,6 +315,20 @@ input["submods"]["cpu"]["ear.veraison.annotated-evidence"]["{}"] ]) .output()?; + if !output.status.success() { + let reason = "error setting resource policy"; + error!("set_resource_policy(): {reason}"); + error!( + "set_resource_policy(): stdout: {}", + String::from_utf8_lossy(&output.stdout) + ); + error!( + "set_resource_policy(): stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + anyhow::bail!(reason); + } + Ok(()) } @@ -131,13 +347,27 @@ async fn generate_attestation_token(escrow_url: &str) -> Result<()> { ]) .output()?; + if !output.status.success() { + let reason = "error generating attestation token"; + error!("generate_attestation_token(): {reason}"); + error!( + "generate_attestation_token(): stdout: {}", + String::from_utf8_lossy(&output.stdout) + ); + error!( + "generate_attestation_token(): stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + anyhow::bail!(reason); + } + fs::write(get_attestation_token(), output.stdout)?; Ok(()) } pub async fn get_trustee_resource(escrow_url: String) -> Result<()> { - Command::new("sudo") + let output = Command::new("sudo") .args([ "-E", &get_kbs_client_path(), @@ -158,6 +388,20 @@ pub async fn get_trustee_resource(escrow_url: String) -> Result<()> { ]) .output()?; + if !output.status.success() { + let reason = "error getting trustee resource"; + error!("get_trustee_resource(): {reason}"); + error!( + "get_trustee_resource(): stdout: {}", + String::from_utf8_lossy(&output.stdout) + ); + error!( + "get_trustee_resource(): stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + anyhow::bail!(reason); + } + Ok(()) } @@ -257,6 +501,11 @@ async fn run_escrow_ubench(escrow_url: &str, run_args: &UbenchRunArgs) -> Result writeln!(csv_file, "NumRequests,TimeElapsed").unwrap(); if run_args.baseline == EscrowBaseline::Trustee { + // We set the reference values from this vTPM. This is not secure, as + // ideally this would come from a source of truth, not the entity + // itelf we are trying to attest. + set_reference_values(escrow_url).await?; + set_attestation_policy(escrow_url).await?; set_resource_policy(escrow_url).await?; // TODO: ideally we would generate the attestation token with // each new request but, unfortunately, there seems to be some diff --git a/attestation-service/src/amd.rs b/attestation-service/src/amd.rs index 2acf0bc0..b787a1a0 100644 --- a/attestation-service/src/amd.rs +++ b/attestation-service/src/amd.rs @@ -1,4 +1,5 @@ use crate::{ + request::snp::Collateral, state::AttestationServiceState, types::snp::{SnpCa, SnpProcType, SnpVcek}, }; @@ -178,6 +179,7 @@ where async fn get_snp_ca( proc_type: &SnpProcType, state: &Arc, + maybe_ca: Option, ) -> Result { debug!("get_snp_ca(): getting CA chain for SNP processor (type={proc_type})"); @@ -186,15 +188,24 @@ async fn get_snp_ca( 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. + // Slow path: parse from collateral (if provided) or fetch from AMD's KDS. + debug!("get_snp_ca(): cache miss, fetching CA from AMD's KDS"); - let ca = fetch_ca_from_kds(proc_type).await?; + let ca = if let Some(ca_str) = maybe_ca { + let ca_chain = Chain::from_pem_bytes(ca_str.as_bytes())?; + ca_chain.verify()?; + + trace!("get_snp_ca(): verified CA's signature from collateral"); + ca_chain + } else { + // This method also verifies the CA signatures. + fetch_ca_from_kds(proc_type).await? + }; // Cache CA for future use. { @@ -254,13 +265,25 @@ where /// 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). -pub async fn get_snp_vcek(report: &R, state: &Arc) -> Result +/// When running in Azure cVMs, clients may self-report their collateral using +/// Azure's THIM layer, in which case we use that instead of AMD's KDS to +/// populate the cache. +pub async fn get_snp_vcek( + report: &R, + state: &Arc, + maybe_collateral: Option, +) -> Result where R: AmdKdsReport, { + // Split the collateral into the certificate chain and the VCEK proper. + let (maybe_vcek, maybe_chain): (Option, Option) = maybe_collateral + .map(|c| (Some(c.vcek_cert_pem), Some(c.certificate_chain_pem))) + .unwrap_or((None, None)); + // Fetch the certificate chain from the processor model. let proc_type = get_processor_model(report)?; - let ca = get_snp_ca(&proc_type, state).await?; + let ca = get_snp_ca(&proc_type, state, maybe_chain).await?; // Work-out cache key from report. let tcb_version = report.tcb_version(); @@ -274,15 +297,19 @@ where 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?; + // Slow path: fetch collateral from request or, if empty, from AMD's KDS. + let vcek = if let Some(vcek_str) = maybe_vcek { + debug!("get_snp_vcek(): cache miss, parsing VCEK from collateral"); + SnpVcek::from_bytes(vcek_str.as_bytes())? + } else { + debug!("get_snp_vcek(): cache miss, fetching VCEK from AMD's KDS"); + 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()?; diff --git a/attestation-service/src/azure_cvm.rs b/attestation-service/src/azure_cvm.rs index e2f698ea..e12cb7c0 100644 --- a/attestation-service/src/azure_cvm.rs +++ b/attestation-service/src/azure_cvm.rs @@ -237,7 +237,7 @@ pub async fn verify_snp_vtpm_report( }; // Verify the SNP report using the host's VCEK. - let vcek = match get_snp_vcek(&snp_report, &state).await { + let vcek = match get_snp_vcek(&snp_report, &state, payload.collateral).await { Ok(report) => report, Err(e) => { error!("verify_snp_report(): error fetching SNP VCEK (error={e:?})"); diff --git a/attestation-service/src/request.rs b/attestation-service/src/request.rs index 69f17571..1948324f 100644 --- a/attestation-service/src/request.rs +++ b/attestation-service/src/request.rs @@ -96,6 +96,15 @@ pub mod snp { _data_type: String, } + #[derive(Debug, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct Collateral { + /// PEM string with the VCEK certificate. + pub vcek_cert_pem: String, + /// PEM string with the ASK+ARK chain. + pub certificate_chain_pem: String, + } + /// This structure corresponds to the JSON we send to verify an SNP report, /// either for bare-metal or vTPM. #[derive(Debug, Deserialize)] @@ -111,5 +120,7 @@ pub mod snp { /// easier to access as a standalone field, and we check its /// integrity from the quote itself, which is signed by the QE. pub runtime_data: RuntimeData, + /// Optional client-provided VCEK collateral (for Azure CVMs). + pub collateral: Option, } } diff --git a/attestation-service/src/snp.rs b/attestation-service/src/snp.rs index e020ed08..06f80c4b 100644 --- a/attestation-service/src/snp.rs +++ b/attestation-service/src/snp.rs @@ -147,7 +147,7 @@ pub async fn verify_snp_report( }; // Fetch the VCEK certificate. - let vcek = match get_snp_vcek(&report, &state).await { + let vcek = match get_snp_vcek(&report, &state, payload.collateral).await { Ok(report) => report, Err(e) => { error!("verify_snp_report(): error fetching SNP VCEK (error={e:?})"); diff --git a/config/ansible/trustee.yaml b/config/ansible/trustee.yaml index 35dcedc6..153aeeca 100644 --- a/config/ansible/trustee.yaml +++ b/config/ansible/trustee.yaml @@ -122,6 +122,48 @@ environment: PATH: "/home/{{ ansible_user }}/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + # Generate JWK set from token key. + - name: "Generate JWKS from EC public key point" + when: inventory_hostname == "accless-trustee-server" + shell: | + set -euo pipefail + + openssl ec -in work/token.key -pubout -outform DER -conv_form uncompressed > /tmp/token-pub.der + tail -c 65 /tmp/token-pub.der > /tmp/token-point.bin + + python3 - << 'PY' + import base64, json, sys + + pt = open("/tmp/token-point.bin","rb").read() + if len(pt) != 65 or pt[0] != 4: + sys.exit("unexpected EC point format") + x, y = pt[1:33], pt[33:65] + b64 = lambda b: base64.urlsafe_b64encode(b).rstrip(b'=').decode() + jwk = { + "kty": "EC", + "crv": "P-256", + "x": b64(x), + "y": b64(y), + "kid": "simple", + "alg": "ES256", + "use": "sig" + } + payload = json.dumps({"keys":[jwk]}, indent=2) + print(payload) + open("work/token.jwks","w").write(json.dumps({"keys":[jwk]})) + PY + args: + chdir: "{{ trustee_code_dir }}/kbs/test" + executable: /bin/bash + - name: "Ensure trusted_jwk_sets is configured in kbs_config" + when: inventory_hostname == "accless-trustee-server" + ansible.builtin.lineinfile: + path: "{{ trustee_code_dir }}/kbs/test/config/kbs.toml" + insertafter: '^trusted_certs_paths' + regexp: '^trusted_jwk_sets' + line: 'trusted_jwk_sets = ["file://{{ trustee_code_dir }}/kbs/test/work/token.jwks"]' + + # Load server key material for the client. - name: "Load server key material (https.crt)" when: inventory_hostname == "accless-trustee-server" ansible.builtin.slurp: diff --git a/ubench/escrow-xput/.gitignore b/ubench/escrow-xput/.gitignore deleted file mode 100644 index 378eac25..00000000 --- a/ubench/escrow-xput/.gitignore +++ /dev/null @@ -1 +0,0 @@ -build diff --git a/ubench/escrow-xput/CMakeLists.txt b/ubench/escrow-xput/CMakeLists.txt deleted file mode 100644 index d4c83ddf..00000000 --- a/ubench/escrow-xput/CMakeLists.txt +++ /dev/null @@ -1,59 +0,0 @@ -cmake_minimum_required(VERSION 3.8.0) -project(accless-ubench) - -set(CMAKE_PROJECT_TARGET accless-ubench) -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_EXPORT_COMPILE_COMMANDS ON) -set(CMAKE_CXX_FLAGS "-g ${CMAKE_CXX_FLAGS} -O3") - -include(FetchContent) - -# Fetch the external dependency from GitHub -FetchContent_Declare( - AzGuestAttestation - GIT_REPOSITORY https://github.com/faasm/azure-cvm-guest-attestation.git - GIT_TAG main -) -FetchContent_MakeAvailable(AzGuestAttestation) - -# ------------------------------------------------------------------------------ -# This project has two main dependencies: -# 1. The rabe Rust library for CP-ABE (and its CPP bindings): -# - https://github.com/faasm/rabe -# 2. The azure guest attestaion library to retrieve cVM attestation reports -# from a vTPM in an Azure cVM, and interact with an instance of the -# azure attestation service: -# - https://github.com/faasm/azure-cvm-guest-attestation -# -# We install the former manually, before running this script, as part of an -# Ansible provisioning script. -# ------------------------------------------------------------------------------ - -set(AZ_GUEST_ATTESTATION_INCLUDE_DIRS - ${azguestattestation_SOURCE_DIR}/AttestationClient - ${azguestattestation_SOURCE_DIR}/AttestationClient/include - ${azguestattestation_SOURCE_DIR}/LinuxTpm/include - ${azguestattestation_SOURCE_DIR}/external/jsoncpp-0.10.7/include -) - -set(TLESS_WORKFLOW_HEADERS - ${CMAKE_CURRENT_LIST_DIR}/include - ${AZ_GUEST_ATTESTATION_INCLUDE_DIRS} - # /usr/include/azguestattestation1 - /usr/include/rabe/ -) -set(TLESS_WORKFLOW_LIBS - azguestattestation - curl - # Order matters: librabe-cpp must preceede librabe - "/usr/local/lib/rabe/librabe-cpp.a" - "/usr/local/lib/rabe/librabe.a" -) - -add_executable(${CMAKE_PROJECT_TARGET} - src/main.cpp - src/logger.cpp - src/utils.cpp -) -target_include_directories(${CMAKE_PROJECT_TARGET} PUBLIC ${TLESS_WORKFLOW_HEADERS}) -target_link_libraries(${CMAKE_PROJECT_TARGET} ${TLESS_WORKFLOW_LIBS}) diff --git a/ubench/escrow-xput/README.md b/ubench/escrow-xput/README.md deleted file mode 100644 index 21ff7fc2..00000000 --- a/ubench/escrow-xput/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Escrow Throughput-Latency - Accless Baseline - -This directory holds the source code for the sample function that we use to -measure the overheads of Accless access control mechanisms. - -Secret key-release in Accless works as follows: -0. Application code generates a private/public key pair (needed here?) -1. Application code fetches a HW attestation report including the public key - as additional data. -2. Application code validates the HW attestation report by sending it to an - instance of the MAA service, and receiving a signed JWT in response. -3. Inside the JWT, encrypted with our public key, we have a symmetric key. -4. We use the symmetric key to decrypt the attestation chain (in this demo, - its just a sample payload). -5. Based on the attestation chain, we generate a secret. diff --git a/ubench/escrow-xput/assets/test_context.data b/ubench/escrow-xput/assets/test_context.data deleted file mode 100644 index c4ddeeb1..00000000 Binary files a/ubench/escrow-xput/assets/test_context.data and /dev/null differ diff --git a/ubench/escrow-xput/include/logger.h b/ubench/escrow-xput/include/logger.h deleted file mode 100644 index d6803f1e..00000000 --- a/ubench/escrow-xput/include/logger.h +++ /dev/null @@ -1,9 +0,0 @@ -#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/ubench/escrow-xput/include/utils.h b/ubench/escrow-xput/include/utils.h deleted file mode 100644 index c4109754..00000000 --- a/ubench/escrow-xput/include/utils.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -#include - -std::string base64decode(const std::string &data); diff --git a/ubench/escrow-xput/src/logger.cpp b/ubench/escrow-xput/src/logger.cpp deleted file mode 100644 index b6df46f2..00000000 --- a/ubench/escrow-xput/src/logger.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#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/ubench/escrow-xput/src/main.cpp b/ubench/escrow-xput/src/main.cpp deleted file mode 100644 index 12eda4ed..00000000 --- a/ubench/escrow-xput/src/main.cpp +++ /dev/null @@ -1,529 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#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; -} - -/* - * This method populates all the attestation parameters to send to the MAA, - * including reading the TPM measurements. - */ -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; -} - -/* - * This method gets a JWT from the MAA in response of a set of attestation - * parameters. - */ -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; -} - -/* - * This methodd gets a JWT from our own attestation service by POST-ing the - * SNP report. - */ -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; - } -} - -/* - * This method gets the HCL report from a vTPM in Azure, and extracts the SNP - * report out of it. - */ -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); -} - -int main(int argc, char **argv) { - bool maa = ((argc == 2) && (std::string(argv[1]) == "--maa")); - bool once = ((argc == 2) && (std::string(argv[1]) == "--once")); - - if (once) { - runOnce(); - } else { - doBenchmark(maa); - } -} diff --git a/ubench/escrow-xput/src/utils.cpp b/ubench/escrow-xput/src/utils.cpp deleted file mode 100644 index eec2fffb..00000000 --- a/ubench/escrow-xput/src/utils.cpp +++ /dev/null @@ -1,14 +0,0 @@ -#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'; }); -}