diff --git a/Cargo.lock b/Cargo.lock index 2383a5775..b72df7a1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -413,7 +413,7 @@ dependencies = [ "env_logger 0.10.2", "jsonwebtoken", "jwt-simple", - "kbs-types", + "kbs-types 0.6.0", "lazy_static", "log", "mobc", @@ -537,7 +537,7 @@ dependencies = [ "env_logger 0.10.2", "futures", "hex", - "kbs-types", + "kbs-types 0.6.0", "lazy_static", "log", "openssl", @@ -578,7 +578,7 @@ dependencies = [ "csv-rs", "hyper", "hyper-tls", - "kbs-types", + "kbs-types 0.5.3", "log", "nix", "occlum_dcap", @@ -1299,7 +1299,7 @@ dependencies = [ "anyhow", "base64 0.21.7", "ctr", - "kbs-types", + "kbs-types 0.5.3", "rand", "rsa 0.9.6", "serde", @@ -1369,6 +1369,36 @@ dependencies = [ "cipher", ] +[[package]] +name = "curl" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e2161dd6eba090ff1594084e95fd67aeccf04382ffea77999ea94ed42ec67b6" +dependencies = [ + "curl-sys", + "libc", + "openssl-probe", + "openssl-sys", + "schannel", + "socket2", + "windows-sys 0.52.0", +] + +[[package]] +name = "curl-sys" +version = "0.4.72+curl-8.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29cbdc8314c447d11e8fd156dcdd031d9e02a7a976163e396b548c03153bc9ea" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", + "windows-sys 0.52.0", +] + [[package]] name = "darling" version = "0.13.4" @@ -2492,6 +2522,16 @@ dependencies = [ "serde_json", ] +[[package]] +name = "kbs-types" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "febd73b2b1df274ea454d81ddf76f596af9754410b7ed6f988f2e1782a175da3" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "kbs_protocol" version = "0.1.0" @@ -2503,7 +2543,7 @@ dependencies = [ "base64 0.21.7", "crypto", "jwt-simple", - "kbs-types", + "kbs-types 0.5.3", "log", "reqwest", "resource_uri", @@ -4065,6 +4105,38 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +[[package]] +name = "s390_pv" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f75b3a6c5bb5b3e8e4fdced5e8b406fcfaf909a96975c4010e839799bf25f48" +dependencies = [ + "byteorder", + "curl", + "foreign-types", + "log", + "openssl", + "openssl-sys", + "s390_pv_core", + "serde", + "thiserror", + "zerocopy", +] + +[[package]] +name = "s390_pv_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dacc93d1903ab065f327d8c71745a9a48f4e812dd6707cec62538d5a84654627" +dependencies = [ + "byteorder", + "libc", + "log", + "serde", + "thiserror", + "zerocopy", +] + [[package]] name = "salsa20" version = "0.10.2" @@ -5365,13 +5437,15 @@ dependencies = [ "intel-tee-quote-verification-rs", "jsonwebkey", "jsonwebtoken", - "kbs-types", + "kbs-types 0.6.0", "log", "openssl", "rstest", + "s390_pv", "scroll 0.11.0", "serde", "serde_json", + "serde_with", "serial_test", "sev", "shadow-rs", diff --git a/Cargo.toml b/Cargo.toml index 4ddc162dd..9d3fef791 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ clap = { version = "4", features = ["derive"] } config = "0.13.3" env_logger = "0.10.0" hex = "0.4.3" -kbs-types = "0.5.3" +kbs-types = "0.6.0" jsonwebtoken = { version = "9", default-features = false } log = "0.4.17" prost = "0.11.0" @@ -38,6 +38,7 @@ regorus = { version = "0.1.5", default-features = false, features = ["regex", "b rstest = "0.18.1" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.89" +serde_with = { version = "1.11.0", features = ["base64"] } serial_test = "0.9.0" sha2 = "0.10" shadow-rs = "0.19.0" @@ -46,4 +47,4 @@ thiserror = "1.0" tokio = { version = "1.23.0", features = ["full"] } tempfile = "3.4.0" tonic = "0.8.1" -tonic-build = "0.8.0" +tonic-build = "0.8.0" \ No newline at end of file diff --git a/attestation-service/README.md b/attestation-service/README.md index d720f4e34..7ebe09b1b 100644 --- a/attestation-service/README.md +++ b/attestation-service/README.md @@ -13,6 +13,7 @@ Today, the AS can validate evidence from the following TEEs: - Hygon CSV - Intel TDX with vTPM on Azure - AMD SEV-SNP with vTPM on Azure +- IBM Secure Execution (SE) # Overview ``` @@ -80,6 +81,7 @@ Please refer to the individual verifiers for the specific format of the evidence - Azure TDX vTPM: [Evidence](./verifier/src/az_tdx_vtpm/mod.rs) - Arm CCA: [CcaEvidence](./verifier/src/cca/mod.rs) - Hygon CSV: [CsvEvidence](./verifier/src/csv/mod.rs) +- IBM Secure Execution (SE) [(SeEvidence)](./verifier/src/se/mod.rs) ## Output @@ -132,6 +134,7 @@ Supported Verifier Drivers: - `azsnpvtpm`: Verifier Driver for Azure vTPM based on SNP (Azure SNP vTPM) - `cca`: Verifier Driver for Confidential Compute Architecture (Arm CCA). - `csv`: Verifier Driver for China Security Virtualization (Hygon CSV). +- `se`: Verifier Driver for IBM Secure Execution (SE). ### Policy Engine diff --git a/attestation-service/attestation-service/Cargo.toml b/attestation-service/attestation-service/Cargo.toml index 1178717b1..51a7287d1 100644 --- a/attestation-service/attestation-service/Cargo.toml +++ b/attestation-service/attestation-service/Cargo.toml @@ -13,6 +13,7 @@ az-tdx-vtpm-verifier = [ "verifier/az-tdx-vtpm-verifier" ] snp-verifier = [ "verifier/snp-verifier" ] csv-verifier = [ "verifier/csv-verifier" ] cca-verifier = [ "verifier/cca-verifier" ] +se-verifier = [ "verifier/se-verifier" ] # Only for testing and CI rvps-builtin = [ "reference-value-provider-service" ] diff --git a/attestation-service/attestation-service/src/bin/grpc/mod.rs b/attestation-service/attestation-service/src/bin/grpc/mod.rs index 063b65b06..faba652bc 100644 --- a/attestation-service/attestation-service/src/bin/grpc/mod.rs +++ b/attestation-service/attestation-service/src/bin/grpc/mod.rs @@ -15,7 +15,10 @@ use tonic::transport::Server; use tonic::{Request, Response, Status}; use crate::as_api::attestation_service_server::{AttestationService, AttestationServiceServer}; -use crate::as_api::{AttestationRequest, AttestationResponse, SetPolicyRequest, SetPolicyResponse}; +use crate::as_api::{ + AttestationRequest, AttestationResponse, ChallengeRequest, ChallengeResponse, SetPolicyRequest, + SetPolicyResponse, +}; use crate::rvps_api::reference_value_provider_service_server::{ ReferenceValueProviderService, ReferenceValueProviderServiceServer, @@ -37,6 +40,7 @@ fn to_kbs_tee(tee: &str) -> anyhow::Result { "azsnpvtpm" => Tee::AzSnpVtpm, "cca" => Tee::Cca, "aztdxvtpm" => Tee::AzTdxVtpm, + "se" => Tee::Se, other => bail!("Unsupported TEE type: {other}"), }; @@ -195,6 +199,39 @@ impl AttestationService for Arc> { let res = AttestationResponse { attestation_token }; Ok(Response::new(res)) } + + async fn get_attestation_challenge( + &self, + request: Request, + ) -> Result, Status> { + let request: ChallengeRequest = request.into_inner(); + info!("get_attestation_challenge API called."); + debug!("get_attestation_challenge: {request:#?}"); + + let inner_tee = request + .inner + .get("tee") + .ok_or(Status::aborted("Error parse inner_tee tee"))?; + let tee_params = request + .inner + .get("tee_params") + .map_or(Err(Status::aborted("Error parse inner_tee tee_params")), Ok)?; + let tee = to_kbs_tee(&inner_tee) + .map_err(|e| Status::aborted(format!("Error parse TEE type: {e}")))?; + + let attestation_challenge = self + .read() + .await + .attestation_service + .generate_supplemental_challenge(tee, tee_params.clone()) + .await + .map_err(|e| Status::aborted(format!("Challenge: {e:?}")))?; + + let res = ChallengeResponse { + attestation_challenge, + }; + Ok(Response::new(res)) + } } #[tonic::async_trait] diff --git a/attestation-service/attestation-service/src/bin/restful-as.rs b/attestation-service/attestation-service/src/bin/restful-as.rs index 1f25da310..e03fd71f4 100644 --- a/attestation-service/attestation-service/src/bin/restful-as.rs +++ b/attestation-service/attestation-service/src/bin/restful-as.rs @@ -13,7 +13,7 @@ use strum::{AsRefStr, EnumString}; use thiserror::Error; use tokio::sync::RwLock; -use crate::restful::{attestation, get_policies, set_policy}; +use crate::restful::{attestation, get_challenge, get_policies, set_policy}; mod restful; @@ -48,6 +48,9 @@ enum WebApi { #[strum(serialize = "/policy")] Policy, + + #[strum(serialize = "/challenge")] + Challenge, } #[derive(Error, Debug)] @@ -100,6 +103,7 @@ async fn main() -> Result<(), RestfulError> { .route(web::post().to(set_policy)) .route(web::get().to(get_policies)), ) + .service(web::resource(WebApi::Challenge.as_ref()).route(web::post().to(get_challenge))) .app_data(web::Data::clone(&attestation_service)) }); diff --git a/attestation-service/attestation-service/src/bin/restful/mod.rs b/attestation-service/attestation-service/src/bin/restful/mod.rs index f6b96eeec..be0ee21a4 100644 --- a/attestation-service/attestation-service/src/bin/restful/mod.rs +++ b/attestation-service/attestation-service/src/bin/restful/mod.rs @@ -1,7 +1,7 @@ -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; use actix_web::{body::BoxBody, web, HttpRequest, HttpResponse, ResponseError}; -use anyhow::{bail, Context}; +use anyhow::{anyhow, bail, Context}; use attestation_service::{AttestationService, HashAlgorithm}; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use kbs_types::Tee; @@ -44,6 +44,14 @@ pub struct AttestationRequest { policy_ids: Vec, } +#[derive(Debug, Serialize, Deserialize)] +pub struct ChallengeRequest { + // ChallengeRequest uses HashMap to pass variables like: + // tee, tee_params etc + #[serde(flatten)] + inner: HashMap, +} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] enum Data { @@ -62,6 +70,7 @@ fn to_tee(tee: &str) -> anyhow::Result { "csv" => Tee::Csv, "sample" => Tee::Sample, "aztdxvtpm" => Tee::AzTdxVtpm, + "se" => Tee::Se, other => bail!("tee `{other} not supported`"), }; @@ -179,6 +188,36 @@ pub async fn set_policy( Ok(HttpResponse::Ok().body("")) } +/// This handler uses json extractor +pub async fn get_challenge( + request: web::Json, + cocoas: web::Data>>, +) -> Result { + info!("get_challenge API called."); + let request: ChallengeRequest = request.into_inner(); + + debug!("get_challenge: {request:#?}"); + let inner_tee = request + .inner + .get("tee") + .as_ref() + .map(|s| s.as_str()) + .ok_or(anyhow!("Failed to get inner tee"))?; + let tee_params = request + .inner + .get("tee_params") + .ok_or(anyhow!("Failed to get inner tee_params"))?; + + let tee = to_tee(inner_tee)?; + let challenge = cocoas + .read() + .await + .generate_supplemental_challenge(tee, tee_params.to_string()) + .await + .context("generate challenge")?; + Ok(HttpResponse::Ok().body(challenge)) +} + /// GET /policy /// GET /policy/{policy_id} /// diff --git a/attestation-service/attestation-service/src/lib.rs b/attestation-service/attestation-service/src/lib.rs index 4119abf25..f987b4305 100644 --- a/attestation-service/attestation-service/src/lib.rs +++ b/attestation-service/attestation-service/src/lib.rs @@ -274,6 +274,17 @@ impl AttestationService { pub async fn register_reference_value(&mut self, message: &str) -> Result<()> { self.rvps.verify_and_extract(message).await } + + pub async fn generate_supplemental_challenge( + &self, + tee: Tee, + tee_parameters: String, + ) -> Result { + let verifier = verifier::to_verifier(&tee)?; + verifier + .generate_supplemental_challenge(tee_parameters) + .await + } } /// Get the expected init/runtime data and potential claims due to the given input diff --git a/attestation-service/docs/parsed_claims.md b/attestation-service/docs/parsed_claims.md index 6249cb578..e061004f4 100644 --- a/attestation-service/docs/parsed_claims.md +++ b/attestation-service/docs/parsed_claims.md @@ -93,6 +93,14 @@ The claim inherit the fields from the SEV-SNP claim with and additional `tpm` hi Note: The TD Report and TD Quote are fetched during early boot in this TEE. Kernel, Initrd and rootfs are measured into the vTPM's registers. +## IBM Secure Execution (SE) +- `se.version`: The version this quote structure. +- `se.cuid`: The config uid. +- `se.hdr.tag`: SE header tag (seht) +- `se.image.phkh`: SE image public host key hash +- `se.attestation.phkh`: SE attestation public host key hash +- `se.user_data`: Custom attestation key owner data. + ## AMD SEV-SNP - `snp.measurement` Launch Digest covering initial guest memory diff --git a/attestation-service/protos/attestation.proto b/attestation-service/protos/attestation.proto index 43a01e40a..8e9b531b7 100644 --- a/attestation-service/protos/attestation.proto +++ b/attestation-service/protos/attestation.proto @@ -77,8 +77,18 @@ message SetPolicyRequest { } message SetPolicyResponse {} +message ChallengeRequest { + // ChallengeRequest uses HashMap to pass variables like: + // tee, tee_params etc + map inner = 1; +} +message ChallengeResponse { + string attestation_challenge = 1; +} + service AttestationService { rpc AttestationEvaluate(AttestationRequest) returns (AttestationResponse) {}; rpc SetAttestationPolicy(SetPolicyRequest) returns (SetPolicyResponse) {}; + rpc GetAttestationChallenge(ChallengeRequest) returns (ChallengeResponse) {}; // Get the GetPolicyRequest.user and GetPolicyRequest.tee specified Policy(.rego) } diff --git a/attestation-service/verifier/Cargo.toml b/attestation-service/verifier/Cargo.toml index 4bc3bb221..8dc16e0a7 100644 --- a/attestation-service/verifier/Cargo.toml +++ b/attestation-service/verifier/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [features] default = [ "all-verifier" ] -all-verifier = [ "tdx-verifier", "sgx-verifier", "snp-verifier", "az-snp-vtpm-verifier", "az-tdx-vtpm-verifier", "csv-verifier", "cca-verifier" ] +all-verifier = [ "tdx-verifier", "sgx-verifier", "snp-verifier", "az-snp-vtpm-verifier", "az-tdx-vtpm-verifier", "csv-verifier", "cca-verifier", "se-verifier" ] tdx-verifier = [ "eventlog-rs", "scroll", "intel-tee-quote-verification-rs" ] sgx-verifier = [ "scroll", "intel-tee-quote-verification-rs" ] az-snp-vtpm-verifier = [ "az-snp-vtpm", "sev", "snp-verifier" ] @@ -13,6 +13,7 @@ az-tdx-vtpm-verifier = [ "az-tdx-vtpm", "openssl", "tdx-verifier" ] snp-verifier = [ "asn1-rs", "openssl", "sev", "x509-parser" ] csv-verifier = [ "openssl", "csv-rs", "codicon" ] cca-verifier = [ "ear", "jsonwebtoken", "veraison-apiclient" ] +se-verifier = [ "openssl", "pv", "serde_with", "tokio/sync" ] [dependencies] anyhow.workspace = true @@ -35,10 +36,13 @@ jsonwebtoken = { workspace = true, default-features = false, optional = true } kbs-types.workspace = true log.workspace = true openssl = { version = "0.10.55", optional = true } +pv = { version = "0.10.0", package = "s390_pv", optional = true } scroll = { version = "0.11.0", default-features = false, features = ["derive"], optional = true } serde.workspace = true serde_json.workspace = true +serde_with = { workspace = true, optional = true } sev = { version = "3.1.1", features = ["openssl", "snp"], optional = true } +tokio = { workspace = true, optional = true, default-features = false } intel-tee-quote-verification-rs = { git = "https://github.com/intel/SGXDataCenterAttestationPrimitives", tag = "DCAP_1.21", optional = true } strum.workspace = true veraison-apiclient = { git = "https://github.com/chendave/rust-apiclient", branch = "token", optional = true } @@ -54,3 +58,4 @@ assert-json-diff.workspace = true rstest.workspace = true serial_test.workspace = true tokio.workspace = true + diff --git a/attestation-service/verifier/src/lib.rs b/attestation-service/verifier/src/lib.rs index 811e530aa..f0e2e7a28 100644 --- a/attestation-service/verifier/src/lib.rs +++ b/attestation-service/verifier/src/lib.rs @@ -28,6 +28,9 @@ pub mod csv; #[cfg(feature = "cca-verifier")] pub mod cca; +#[cfg(feature = "se-verifier")] +pub mod se; + pub fn to_verifier(tee: &Tee) -> Result> { match tee { Tee::Sev => todo!(), @@ -99,6 +102,16 @@ pub fn to_verifier(tee: &Tee) -> Result> { } } } + + Tee::Se => { + cfg_if::cfg_if! { + if #[cfg(feature = "se-verifier")] { + Ok(Box::::default() as Box) + } else { + bail!("feature `se-verifier` is not enabled for `verifier` crate.") + } + } + } } } @@ -152,6 +165,17 @@ pub trait Verifier { expected_report_data: &ReportData, expected_init_data_hash: &InitDataHash, ) -> Result; + + /// Generate the supplemental challenge + /// + /// Some TEE like IBM SE need a `challenge` generated on verifier side + /// and pass it to attester side. This challenge is used by attester to + /// generate the evidence + /// + /// A optional `tee_parameters` comes from the attester side as the input. + async fn generate_supplemental_challenge(&self, _tee_parameters: String) -> Result { + Ok(String::new()) + } } /// Padding or truncate the given data slice to the given `len` bytes. diff --git a/attestation-service/verifier/src/se/ibmse.rs b/attestation-service/verifier/src/se/ibmse.rs new file mode 100644 index 000000000..2328de346 --- /dev/null +++ b/attestation-service/verifier/src/se/ibmse.rs @@ -0,0 +1,311 @@ +// Copyright (C) Copyright IBM Corp. 2024 +// +// SPDX-License-Identifier: Apache-2.0 +// + +use crate::TeeEvidenceParsedClaim; +use anyhow::{anyhow, bail, Context, Result}; +use core::result::Result::Ok; +use log::{debug, info, warn}; +use openssl::encrypt::{Decrypter, Encrypter}; +use openssl::pkey::{PKey, Private, Public}; +use openssl::rsa::{Padding, Rsa}; +use pv::attest::{ + AdditionalData, AttestationFlags, AttestationItems, AttestationMeasAlg, AttestationMeasurement, + AttestationRequest, AttestationVersion, +}; +use pv::misc::{open_file, read_certs}; +use pv::request::{BootHdrTags, CertVerifier, HkdVerifier, ReqEncrCtx, Request, SymKeyType}; +use pv::uv::ConfigUid; +use serde::{Deserialize, Serialize}; +use serde_with::{base64::Base64, serde_as}; +use std::{env, fs}; + +const DEFAULT_SE_HOST_KEY_DOCUMENTS_ROOT: &str = "/run/confidential-containers/ibmse/hkds"; + +const DEFAULT_SE_CERTIFICATES_ROOT: &str = "/run/confidential-containers/ibmse/certs"; + +const DEFAULT_SE_CERTIFICATE_ROOT_CA: &str = "/run/confidential-containers/ibmse/certs/ca"; + +const DEFAULT_SE_CERTIFICATE_REVOCATION_LISTS_ROOT: &str = + "/run/confidential-containers/ibmse/crls"; + +const DEFAULT_SE_IMAGE_HEADER_FILE: &str = "/run/confidential-containers/ibmse/hdr/hdr.bin"; + +const DEFAULT_SE_MEASUREMENT_ENCR_KEY_PRIVATE: &str = + "/run/confidential-containers/ibmse/rsa/encrypt_key.pem"; + +const DEFAULT_SE_MEASUREMENT_ENCR_KEY_PUBLIC: &str = + "/run/confidential-containers/ibmse/rsa/encrypt_key.pub"; + +macro_rules! env_or_default { + ($env:literal, $default:ident) => { + match env::var($env) { + Ok(env_path) => env_path, + Err(_) => $default.into(), + } + }; +} + +fn list_files_in_folder(dir: &str) -> Result> { + let mut file_paths = Vec::new(); + + for entry in fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + + if path.is_file() { + if let Some(path_str) = path.to_str() { + file_paths.push(path_str.to_string()); + } + } + } + + Ok(file_paths) +} + +#[serde_as] +#[derive(Debug, Serialize, Deserialize)] +pub struct SeAttestationResponse { + #[serde_as(as = "Base64")] + measurement: Vec, + #[serde_as(as = "Base64")] + additional_data: Vec, + #[serde_as(as = "Base64")] + user_data: Vec, + #[serde_as(as = "Base64")] + cuid: ConfigUid, + #[serde_as(as = "Base64")] + encr_measurement_key: Vec, + #[serde_as(as = "Base64")] + encr_request_nonce: Vec, + #[serde_as(as = "Base64")] + image_hdr_tags: BootHdrTags, +} + +#[repr(C)] +#[serde_as] +#[derive(Debug, Serialize, Deserialize)] +pub struct SeAttestationClaims { + #[serde_as(as = "Base64")] + cuid: ConfigUid, + #[serde_as(as = "Base64")] + user_data: Vec, + version: u32, + #[serde_as(as = "Base64")] + image_phkh: Vec, + #[serde_as(as = "Base64")] + attestation_phkh: Vec, + #[serde_as(as = "Base64")] + tag: [u8; 16], +} + +#[serde_as] +#[derive(Debug, Serialize, Deserialize)] +pub struct SeAttestationRequest { + #[serde_as(as = "Base64")] + request_blob: Vec, + measurement_size: u32, + additional_size: u32, + #[serde_as(as = "Base64")] + encr_measurement_key: Vec, + #[serde_as(as = "Base64")] + encr_request_nonce: Vec, + #[serde_as(as = "Base64")] + image_hdr_tags: BootHdrTags, +} + +#[derive(Debug)] +pub struct RealSeVerifier { + rsa_private_key: PKey, + rsa_public_key: PKey, +} + +impl RealSeVerifier { + pub fn new() -> Result { + let pri_key_file = env_or_default!( + "SE_MEASUREMENT_ENCR_KEY_PRIVATE", + DEFAULT_SE_MEASUREMENT_ENCR_KEY_PRIVATE + ); + let priv_contents = fs::read(pri_key_file)?; + let rsa_private_key = Rsa::private_key_from_pem(&priv_contents)?; + let rsa_private_key = PKey::from_rsa(rsa_private_key)?; + + let pub_key_file = env_or_default!( + "SE_MEASUREMENT_ENCR_KEY_PUBLIC", + DEFAULT_SE_MEASUREMENT_ENCR_KEY_PUBLIC + ); + let pub_contents = fs::read(pub_key_file)?; + let rsa = Rsa::public_key_from_pem(&pub_contents)?; + let rsa_public_key = PKey::from_rsa(rsa)?; + + Ok(Self { + rsa_private_key, + rsa_public_key, + }) + } + + fn decrypt(&self, ciphertext: &[u8]) -> Result> { + let mut decrypter = Decrypter::new(&self.rsa_private_key)?; + decrypter.set_rsa_padding(Padding::PKCS1)?; + + let buffer_len = decrypter.decrypt_len(ciphertext)?; + let mut decrypted_hmac_key = vec![0; buffer_len]; + let decrypted_len = decrypter.decrypt(ciphertext, &mut decrypted_hmac_key)?; + decrypted_hmac_key.truncate(decrypted_len); + + Ok(decrypted_hmac_key) + } + + fn encrypt(&self, text: &[u8]) -> Result> { + let mut encrypter = Encrypter::new(&self.rsa_public_key)?; + encrypter.set_rsa_padding(Padding::PKCS1)?; + + let buffer_len = encrypter.encrypt_len(text)?; + let mut encrypted_hmac_key = vec![0; buffer_len]; + let len = encrypter.encrypt(text, &mut encrypted_hmac_key)?; + encrypted_hmac_key.truncate(len); + + Ok(encrypted_hmac_key) + } + + pub fn evaluate(&self, evidence: &[u8]) -> Result { + info!("IBM SE verify API called."); + + // evidence is serialized SeAttestationResponse String bytes + let se_response: SeAttestationResponse = serde_json::from_slice(evidence)?; + + let meas_key = self + .decrypt(&se_response.encr_measurement_key) + .context("decrypt Measurement Key")?; + let nonce = self + .decrypt(&se_response.encr_request_nonce) + .context("decrypt Request Nonce")?; + + if nonce.len() != 16 { + bail!("The nonce vector must have exactly 16 elements."); + } + + let nonce_array: [u8; 16] = nonce + .try_into() + .map_err(|_| anyhow!("Failed to convert nonce from Vec to [u8; 16]."))?; + + let meas_key = PKey::hmac(&meas_key)?; + let items = AttestationItems::new( + &se_response.image_hdr_tags, + &se_response.cuid, + Some(&se_response.user_data), + Some(&nonce_array), + Some(&se_response.additional_data), + ); + + let measurement = + AttestationMeasurement::calculate(items, AttestationMeasAlg::HmacSha512, &meas_key)?; + + if !measurement.eq_secure(&se_response.measurement) { + debug!("Recieved: {:?}", se_response.measurement); + debug!("Calculated: {:?}", measurement.as_ref()); + warn!("Attestation measurement verification failed. Calculated and received attestation measurement are not equal."); + bail!("Failed to verify the measurement!"); + } + + // TODO check self.user_data.image_btph with previous saved value + + let mut att_flags = AttestationFlags::default(); + att_flags.set_image_phkh(); + att_flags.set_attest_phkh(); + let add_data = AdditionalData::from_slice(&se_response.additional_data, &att_flags)?; + debug!("additional_data: {:?}", add_data); + let image_phkh = add_data + .image_public_host_key_hash() + .ok_or(anyhow!("Failed to get image_public_host_key_hash."))?; + let attestation_phkh = add_data + .attestation_public_host_key_hash() + .ok_or(anyhow!("Failed to get attestation_public_host_key_hash."))?; + + // TODO image_phkh and attestation_phkh with previous saved value + + let claims = SeAttestationClaims { + cuid: se_response.cuid, + user_data: se_response.user_data.clone(), + version: AttestationVersion::One as u32, + image_phkh: image_phkh.to_vec(), + attestation_phkh: attestation_phkh.to_vec(), + tag: *se_response.image_hdr_tags.tag(), + }; + + serde_json::to_value(claims).context("build json value from the se claims") + } + + pub async fn generate_supplemental_challenge(&self, _tee_parameters: String) -> Result { + let se_certificate_root = + env_or_default!("SE_CERTIFICATES_ROOT", DEFAULT_SE_CERTIFICATES_ROOT); + let certs = list_files_in_folder(&se_certificate_root)?; + + let crl_root = env_or_default!( + "SE_CERTIFICATE_REVOCATION_LISTS_ROOT", + DEFAULT_SE_CERTIFICATE_REVOCATION_LISTS_ROOT + ); + let crls = list_files_in_folder(&crl_root)?; + + let root_ca_path = + env_or_default!("SE_CERTIFICATE_ROOT_CA", DEFAULT_SE_CERTIFICATE_ROOT_CA); + let verifier = + CertVerifier::new(certs.as_slice(), crls.as_slice(), Some(root_ca_path), false)?; + + let mut arcb = AttestationRequest::new( + AttestationVersion::One, + AttestationMeasAlg::HmacSha512, + AttestationFlags::default(), + )?; + + let hkds_root = env_or_default!( + "DEFAULT_SE_HOST_KEY_DOCUMENTS_ROOT", + DEFAULT_SE_HOST_KEY_DOCUMENTS_ROOT + ); + let hkds = list_files_in_folder(&hkds_root)?; + for hkd in &hkds { + let hk = std::fs::read(hkd).context("read host-key document")?; + let certs = read_certs(&hk)?; + if certs.is_empty() { + warn!("The host key document in '{hkd}' contains empty certificate!"); + } + if certs.len() != 1 { + warn!("The host key document in '{hkd}' contains more than one certificate!") + } + let c = certs + .first() + .ok_or(anyhow!("File does not contain a X509 certificate"))?; + verifier.verify(c)?; + arcb.add_hostkey(c.public_key()?); + } + + let encr_ctx = ReqEncrCtx::random(SymKeyType::Aes256)?; + let request_blob = arcb.encrypt(&encr_ctx)?; + let conf_data = arcb.confidential_data(); + let encr_measurement_key = + self.encrypt(conf_data.measurement_key())?; + let nonce = conf_data + .nonce() + .as_ref() + .ok_or(anyhow!("Failed to get nonce binding"))? + .value(); + let encr_request_nonce = self.encrypt(nonce)?; + + let se_img_hdr = env_or_default!("SE_IMAGE_HEADER_FILE", DEFAULT_SE_IMAGE_HEADER_FILE); + let mut hdr_file = open_file(se_img_hdr)?; + let image_hdr_tags = BootHdrTags::from_se_image(&mut hdr_file)?; + + let se_attestation_request = SeAttestationRequest { + request_blob, + measurement_size: AttestationMeasAlg::HmacSha512.exp_size(), + additional_size: arcb.flags().expected_additional_size(), + encr_measurement_key, + encr_request_nonce, + image_hdr_tags, + }; + + let challenge = serde_json::to_string(&se_attestation_request)?; + Ok(challenge) + } +} diff --git a/attestation-service/verifier/src/se/mod.rs b/attestation-service/verifier/src/se/mod.rs new file mode 100644 index 000000000..8704c8fbd --- /dev/null +++ b/attestation-service/verifier/src/se/mod.rs @@ -0,0 +1,45 @@ +// Copyright (C) Copyright IBM Corp. 2024 +// +// SPDX-License-Identifier: Apache-2.0 +// + +use anyhow::Result; +use async_trait::async_trait; +use ibmse::RealSeVerifier; +use log::warn; +use tokio::sync::OnceCell; + +use crate::{InitDataHash, ReportData, TeeEvidenceParsedClaim, Verifier}; + +pub mod ibmse; + +static ONCE: OnceCell = OnceCell::const_new(); + +#[derive(Debug, Default)] +pub struct SeVerifier; + +#[async_trait] +impl Verifier for SeVerifier { + async fn evaluate( + &self, + evidence: &[u8], + _expected_report_data: &ReportData, + _expected_init_data_hash: &InitDataHash, + ) -> Result { + let se_verifier = ONCE + .get_or_try_init(|| async { RealSeVerifier::new() }) + .await?; + warn!("IBM SE does not support initdata."); + se_verifier.evaluate(evidence) + } + + async fn generate_supplemental_challenge( + &self, + _tee_parameters: String, + ) -> Result { + let se_verifier = ONCE + .get_or_try_init(|| async { RealSeVerifier::new() }) + .await?; + se_verifier.generate_supplemental_challenge(_tee_parameters).await + } +} diff --git a/kbs/src/api/src/attestation/coco/builtin.rs b/kbs/src/api/src/attestation/coco/builtin.rs index fb3a983ec..1433b5f70 100644 --- a/kbs/src/api/src/attestation/coco/builtin.rs +++ b/kbs/src/api/src/attestation/coco/builtin.rs @@ -6,7 +6,9 @@ use crate::attestation::Attest; use anyhow::*; use async_trait::async_trait; use attestation_service::{config::Config as AsConfig, AttestationService, Data, HashAlgorithm}; -use kbs_types::{Attestation, Tee}; +use base64::{engine::general_purpose::STANDARD, Engine}; +use kbs_types::{Attestation, Challenge, Tee}; +use rand::{thread_rng, Rng}; use serde_json::json; use tokio::sync::RwLock; @@ -44,6 +46,34 @@ impl Attest for BuiltInCoCoAs { ) .await } + + async fn generate_challenge(&self, tee: Tee, tee_parameters: String) -> Result { + let nonce = match tee { + Tee::Se => { + self.inner + .read() + .await + .generate_supplemental_challenge(tee, tee_parameters) + .await? + } + _ => { + let mut nonce: Vec = vec![0; 32]; + + thread_rng() + .try_fill(&mut nonce[..]) + .map_err(anyhow::Error::from)?; + + STANDARD.encode(&nonce) + } + }; + + let challenge = Challenge { + nonce, + extra_params: String::new(), + }; + + Ok(challenge) + } } impl BuiltInCoCoAs { diff --git a/kbs/src/api/src/attestation/coco/grpc.rs b/kbs/src/api/src/attestation/coco/grpc.rs index 92ccd6a3f..46b925486 100644 --- a/kbs/src/api/src/attestation/coco/grpc.rs +++ b/kbs/src/api/src/attestation/coco/grpc.rs @@ -5,18 +5,23 @@ use crate::attestation::Attest; use anyhow::*; use async_trait::async_trait; -use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; -use kbs_types::{Attestation, Tee}; +use base64::{ + engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD}, + Engine, +}; +use kbs_types::{Attestation, Challenge, Tee}; use log::info; use mobc::{Manager, Pool}; +use rand::{thread_rng, Rng}; use serde::Deserialize; use serde_json::json; +use std::collections::HashMap; use tokio::sync::Mutex; use tonic::transport::Channel; use self::attestation::{ attestation_request::RuntimeData, attestation_service_client::AttestationServiceClient, - AttestationRequest, SetPolicyRequest, + AttestationRequest, ChallengeRequest, SetPolicyRequest, }; mod attestation { @@ -118,6 +123,46 @@ impl Attest for GrpcClientPool { Ok(token) } + + async fn generate_challenge(&self, tee: Tee, tee_parameters: String) -> Result { + let nonce = match tee { + Tee::Se => { + let tee = serde_json::to_string(&tee) + .context("CoCo AS client: serialize tee type failed.")? + .trim_end_matches('"') + .trim_start_matches('"') + .to_string(); + let mut inner = HashMap::new(); + inner.insert(String::from("tee"), tee); + inner.insert(String::from("tee_params"), tee_parameters); + let req = tonic::Request::new(ChallengeRequest { inner }); + + let mut client = { self.pool.lock().await.get().await? }; + + client + .get_attestation_challenge(req) + .await? + .into_inner() + .attestation_challenge + } + _ => { + let mut nonce: Vec = vec![0; 32]; + + thread_rng() + .try_fill(&mut nonce[..]) + .map_err(anyhow::Error::from)?; + + STANDARD.encode(&nonce) + } + }; + + let challenge = Challenge { + nonce, + extra_params: String::new(), + }; + + Ok(challenge) + } } pub struct GrpcManager { diff --git a/kbs/src/api/src/attestation/mod.rs b/kbs/src/api/src/attestation/mod.rs index 5ef2656f1..5d75a03bf 100644 --- a/kbs/src/api/src/attestation/mod.rs +++ b/kbs/src/api/src/attestation/mod.rs @@ -6,11 +6,13 @@ use anyhow::*; use async_trait::async_trait; #[cfg(any(feature = "coco-as-builtin", feature = "coco-as-builtin-no-verifier"))] use attestation_service::config::Config as AsConfig; +use base64::{engine::general_purpose::STANDARD, Engine}; #[cfg(feature = "coco-as-grpc")] use coco::grpc::*; #[cfg(feature = "intel-trust-authority-as")] use intel_trust_authority::*; -use kbs_types::Tee; +use kbs_types::{Challenge, Tee}; +use rand::{thread_rng, Rng}; #[cfg(feature = "coco-as")] #[allow(missing_docs)] @@ -32,6 +34,21 @@ pub trait Attest: Send + Sync { /// Verify Attestation Evidence /// Return Attestation Results Token async fn verify(&self, tee: Tee, nonce: &str, attestation: &str) -> Result; + + /// generate the Challenge to pass to attester based on Tee and nonce + async fn generate_challenge(&self, _tee: Tee, _tee_parameters: String) -> Result { + let mut nonce: Vec = vec![0; 32]; + + thread_rng() + .try_fill(&mut nonce[..]) + .map_err(anyhow::Error::from)?; + + let nonce = STANDARD.encode(&nonce); + Ok(Challenge { + nonce, + extra_params: String::new(), + }) + } } /// Attestation Service @@ -89,4 +106,21 @@ impl AttestationService { AttestationService::IntelTA(inner) => inner.set_policy(policy_id, policy).await, } } + + pub async fn generate_challenge(&self, tee: Tee, tee_parameters: String) -> Result { + match self { + #[cfg(feature = "coco-as-grpc")] + AttestationService::CoCoASgRPC(inner) => { + inner.generate_challenge(tee, tee_parameters).await + } + #[cfg(any(feature = "coco-as-builtin", feature = "coco-as-builtin-no-verifier"))] + AttestationService::CoCoASBuiltIn(inner) => { + inner.generate_challenge(tee, tee_parameters).await + } + #[cfg(feature = "intel-trust-authority-as")] + AttestationService::IntelTA(inner) => { + inner.generate_challenge(tee, tee_parameters).await + } + } + } } diff --git a/kbs/src/api/src/http/attest.rs b/kbs/src/api/src/http/attest.rs index 320123ce8..c8dd2426e 100644 --- a/kbs/src/api/src/http/attest.rs +++ b/kbs/src/api/src/http/attest.rs @@ -7,8 +7,9 @@ use crate::{raise_error, session::SessionStatus}; use super::*; use anyhow::anyhow; -use base64::engine::general_purpose::URL_SAFE_NO_PAD; +use base64::engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD}; use base64::Engine; +use kbs_types::Challenge; use log::{debug, error, info}; use serde_json::json; @@ -17,11 +18,17 @@ pub(crate) async fn auth( request: web::Json, map: web::Data, timeout: web::Data, + attestation_service: web::Data>, ) -> Result { info!("Auth API called."); debug!("Auth Request: {:?}", &request); - let session = SessionStatus::auth(request.0, **timeout) + let challenge = attestation_service + .generate_challenge(request.tee, request.extra_params.clone()) + .await + .map_err(|e| Error::FailedAuthentication(format!("generate challenge: {e:?}")))?; + + let session = SessionStatus::auth(request.0, **timeout, challenge) .map_err(|e| Error::FailedAuthentication(format!("Session: {e}")))?; let response = HttpResponse::Ok() diff --git a/kbs/src/api/src/session.rs b/kbs/src/api/src/session.rs index 56f2f80a0..6c075defd 100644 --- a/kbs/src/api/src/session.rs +++ b/kbs/src/api/src/session.rs @@ -7,26 +7,13 @@ use actix_web::cookie::{ Cookie, }; use anyhow::{bail, Result}; -use base64::engine::general_purpose::STANDARD; -use base64::Engine; use kbs_types::{Challenge, Request}; use log::warn; -use rand::{thread_rng, Rng}; use semver::Version; use uuid::Uuid; pub(crate) static KBS_SESSION_ID: &str = "kbs-session-id"; -fn nonce() -> Result { - let mut nonce: Vec = vec![0; 32]; - - thread_rng() - .try_fill(&mut nonce[..]) - .map_err(anyhow::Error::from)?; - - Ok(STANDARD.encode(&nonce)) -} - /// Finite State Machine model for RCAR handshake pub(crate) enum SessionStatus { Authed { @@ -64,7 +51,7 @@ macro_rules! impl_member { } impl SessionStatus { - pub fn auth(request: Request, timeout: i64) -> Result { + pub fn auth(request: Request, timeout: i64, challenge: Challenge) -> Result { let version = Version::parse(&request.version).map_err(anyhow::Error::from)?; if !crate::VERSION_REQ.matches(&version) { bail!("Invalid Request version {}", request.version); @@ -75,10 +62,7 @@ impl SessionStatus { Ok(Self::Authed { request, - challenge: Challenge { - nonce: nonce()?, - extra_params: String::new(), - }, + challenge, id, timeout, })