diff --git a/Cargo.toml b/Cargo.toml index 9fdfa290a0..becf3f72ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,7 @@ jwt-simple = { version = "0.12", default-features = false, features = [ kbs_protocol = { git = "https://github.com/confidential-containers/guest-components.git", rev = "7be23a1", default-features = false } kbs-types = "0.14.0" kms = { git = "https://github.com/confidential-containers/guest-components.git", rev = "7be23a1", default-features = false } -jsonwebtoken = { version = "10", default-features = false, features = ["aws_lc_rs"] } +jsonwebtoken = { version = "10", features = ["aws_lc_rs"] } lazy_static = "1.4.0" log = "0.4.28" openssl = "0.10.75" @@ -56,7 +56,7 @@ regorus = { version = "0.5.0", default-features = false, features = [ "std", ] } reqwest = { version = "0.12", default-features = false, features = [ - "default-tls", + "rustls-tls", "json", ] } rstest = "0.26.1" diff --git a/deps/verifier/Cargo.toml b/deps/verifier/Cargo.toml index 6ed62ae834..4859fb45d9 100644 --- a/deps/verifier/Cargo.toml +++ b/deps/verifier/Cargo.toml @@ -55,7 +55,7 @@ csv-rs = { git = "https://github.com/openanolis/csv-rs", rev = "b67a07e", option eventlog = { path = "../eventlog", optional = true } hex.workspace = true jsonwebkey = "0.3.5" -jsonwebtoken = { workspace = true, default-features = false, optional = true } +jsonwebtoken = { workspace = true, optional = true } kbs-types.workspace = true openssl = { version = "0.10.75", optional = true } pv = { version = "0.10.0", package = "s390_pv", optional = true } diff --git a/kbs/Cargo.toml b/kbs/Cargo.toml index 979ecbd9e0..372ce1286c 100644 --- a/kbs/Cargo.toml +++ b/kbs/Cargo.toml @@ -27,6 +27,9 @@ coco-as-grpc = ["coco-as", "mobc", "tonic", "tonic-prost", "prost"] # Use Intel TA as backend attestation service intel-trust-authority-as = ["as", "az-cvm-vtpm"] +# Use the keylime /verify/evidence handler as backend attestation service +keylime-as = ["as"] + # Use aliyun KMS as KBS backend aliyun = ["kms/aliyun"] @@ -57,7 +60,7 @@ cryptoki = { version = "0.10.0", optional = true } derivative.workspace = true env_logger.workspace = true hex.workspace = true -jsonwebtoken = { workspace = true, default-features = false } +jsonwebtoken.workspace = true jwt-simple.workspace = true kbs-types.workspace = true kms = { workspace = true, default-features = false } diff --git a/kbs/config/kbs-config-keylime-as.toml b/kbs/config/kbs-config-keylime-as.toml new file mode 100644 index 0000000000..b53a6bd3d2 --- /dev/null +++ b/kbs/config/kbs-config-keylime-as.toml @@ -0,0 +1,20 @@ +[http_server] +insecure_http = true + +[attestation_token] +insecure_key = true + +[attestation_service] +type = "keylime-tee" +base_url = "https://0.0.0.0:8881" +api_version_major = 2 +api_version_minor = 4 +cv_ca_path = "/var/lib/keylime/cv_ca" + +[[plugins]] +name = "resource" +type = "LocalFs" +dir_path = "/opt/confidential-containers/kbs/repository" + +[admin] +insecure_api = true diff --git a/kbs/src/attestation/backend.rs b/kbs/src/attestation/backend.rs index ff0cf58352..1241bf7661 100644 --- a/kbs/src/attestation/backend.rs +++ b/kbs/src/attestation/backend.rs @@ -50,6 +50,7 @@ lazy_static! { pub type TeeEvidence = serde_json::Value; /// IndependentEvidence is one set of evidence from one attester. +#[derive(Debug)] pub struct IndependentEvidence { pub tee: Tee, pub tee_evidence: TeeEvidence, @@ -167,6 +168,13 @@ impl AttestationService { .map_err(|e| Error::AttestationServiceInitialization { source: e })?; Arc::new(intel_ta) as _ } + #[cfg(feature = "keylime-as")] + AttestationServiceConfig::Keylime(cfg) => { + let keylime = super::keylime::KeylimeTeeHandler::new(cfg) + .await + .map_err(|e| Error::AttestationServiceInitialization { source: e })?; + Arc::new(keylime) as _ + } }; let session_map = Arc::new(SessionMap::new()); diff --git a/kbs/src/attestation/config.rs b/kbs/src/attestation/config.rs index a12d7703db..f8ecf6948f 100644 --- a/kbs/src/attestation/config.rs +++ b/kbs/src/attestation/config.rs @@ -45,6 +45,10 @@ pub enum AttestationServiceConfig { #[cfg(feature = "intel-trust-authority-as")] #[serde(alias = "intel_ta")] IntelTA(super::intel_trust_authority::IntelTrustAuthorityConfig), + + #[cfg(feature = "keylime-as")] + #[serde(alias = "keylime-tee")] + Keylime(super::keylime::KeylimeVerifierConfig), } impl Default for AttestationServiceConfig { @@ -54,6 +58,8 @@ impl Default for AttestationServiceConfig { AttestationServiceConfig::CoCoASBuiltIn(attestation_service::config::Config::default()) } else if #[cfg(feature = "coco-as-grpc")] { AttestationServiceConfig::CoCoASGrpc(super::coco::grpc::GrpcConfig::default()) + } else if #[cfg(feature = "keylime-as")] { + AttestationServiceConfig::Keylime(super::keylime::KeylimeVerifierConfig::default()) } else { AttestationServiceConfig::IntelTA(super::intel_trust_authority::IntelTrustAuthorityConfig::default()) } diff --git a/kbs/src/attestation/keylime.rs b/kbs/src/attestation/keylime.rs new file mode 100644 index 0000000000..e9a92b9ef5 --- /dev/null +++ b/kbs/src/attestation/keylime.rs @@ -0,0 +1,456 @@ +// Copyright (c) 2025 by Red Hat. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +use crate::attestation::backend::{Attest, IndependentEvidence}; +use anyhow::{anyhow, bail, Context, Result}; +use async_trait::async_trait; +use base64::{ + prelude::{BASE64_STANDARD, BASE64_URL_SAFE_NO_PAD}, + Engine, +}; +use derivative::Derivative; +use jsonwebtoken::{ + jwk::{AlgorithmParameters, CommonParameters, Jwk, KeyAlgorithm, RSAKeyParameters, RSAKeyType}, + Algorithm, EncodingKey, Header, +}; +use kbs_types::{RuntimeData, Tee, TeePubKey}; +use openssl::{pkey::Public, rsa::Rsa}; +use reqwest::{ + header::CONTENT_TYPE, + tls::{Certificate, Identity}, + Client, ClientBuilder, +}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Map, Number, Value}; +use std::fs; +use time::{Duration, OffsetDateTime}; + +/// Configuration of the Keylime verifier. +#[derive(Clone, Debug, Derivative, Deserialize, PartialEq, Default)] +pub struct KeylimeVerifierConfig { + /// Base URL of the verifier. + pub base_url: String, + /// API version (i.e. verifier version {MAJOR}.{MINOR}). + pub api_version_major: u8, + pub api_version_minor: u8, + /// Path of the verifier CA certificates. + pub cv_ca_path: String, +} + +/// Configuration of the KBS frontend for the Keylime verifier. +pub struct KeylimeTeeHandler { + /// Verifier config. + config: KeylimeVerifierConfig, + /// HTTP client to communicate with the verifier. + client: Client, + /// Verifier private key, used to sign JWTs on behalf of verifier. + priv_key: EncodingKey, + /// Verifier public key, used to verify JWT signatures on behalf of verifier. + pub_key: Rsa, +} + +/// HTTP response status. +#[derive(Debug, Deserialize)] +enum KeylimeAttestationStatus { + #[serde(rename = "Success")] + Success, + + #[serde(rename = "Internal Server Error")] + InternalServerError, + + #[serde(rename = "Internal Server Error: Failed to process attestation data")] + FailedAttestationDataProcess, +} + +/// JSON response from Keylime /verify/evidence API. +#[derive(Debug, Deserialize)] +struct KeylimeTeeResponse { + /// HTTP response status codes. + #[serde(rename = "code")] + _code: usize, + /// HTTP response status. + status: KeylimeAttestationStatus, + /// Attestation results. + results: KeylimeTeeResults, +} + +/// Keylime /verify/evidence attestation results. +#[derive(Debug, Deserialize)] +struct KeylimeTeeResults { + /// Indicates if attestation was successful. + valid: bool, + /// Failure reasons. May be empty if attestation is successful. + failures: Vec, + /// Evidence claims. May be empty if attestation is unsuccessful. + claims: Map, +} + +/// Attestation failure reason object. +#[derive(Debug, Deserialize)] +struct KeylimeTeeFailureReason { + /// Type of failure, encoded by Keylime verifier. + #[serde(rename = "type")] + _err_type: KeylimeTeeErrorType, + /// Error message. + #[serde(rename = "message")] + _message: String, +} + +/// Keylime verifier TEE attestation error types. +#[derive(Debug, Deserialize)] +enum KeylimeTeeErrorType { + #[serde(rename = "tee_attestation.freshness_hash_failed")] + FreshnessHash, + #[serde(rename = "tee_attestation.invalid_signature")] + InvalidSignature, + #[serde(rename = "tee_attestation.vcek_fetch")] + SevSnpVcekFetch, + #[serde(rename = "tee_attestation.invalid_public_key")] + SevSnpInvalidPublicKey, +} + +#[async_trait] +impl Attest for KeylimeTeeHandler { + async fn verify(&self, evidence_to_verify: Vec) -> Result { + let tee_data = TeeData::try_from(evidence_to_verify) + .context("unable to deserialize independent evidence to Keylime TEE data")?; + + log::debug!("Keylime TEE data: {:#?}", tee_data); + + let req = json!({ + "type": "tee".to_string(), + "data": &tee_data.req, + }); + + // Send the evidence and receive a response back from the verifier. + let resp = self + .client + .post(format!( + "{}/v{}.{}/verify/evidence", + self.config.base_url, self.config.api_version_major, self.config.api_version_minor + )) + .header(CONTENT_TYPE, "application/json") + .json(&req) + .send() + .await + .context("Failed to POST attestation HTTP request")?; + + let mut response: KeylimeTeeResponse = { + let resp_text = resp + .text() + .await + .context("unable to fetch response text from /verify/evidence endpoint")?; + + serde_json::from_str(&resp_text) + .context("unable to deserialize keylime /verify/evidence results")? + }; + + log::debug!("keylime response: {:#?}", response); + + // Handle the verifier's attestation response based on the attestation's result. + match response.status { + KeylimeAttestationStatus::Success => { + if response.results.valid { + // TEE attestation succeeded. Construct a JWT with the claims from the verifier + // and sign it with Keylime's private key. + self.make_jwt( + &mut response.results.claims, + BASE64_STANDARD + .decode(tee_data.req.nonce) + .context("unable to decode runtime nonce from base64")?, + tee_data.pubkey, + ) + } else { + // TEE attestation failed, return the reasons for failure to the client. + bail!("client attestation failed: {:?}", response.results.failures); + } + } + KeylimeAttestationStatus::InternalServerError => { + Err(anyhow!("Keylime internal server error")) + } + KeylimeAttestationStatus::FailedAttestationDataProcess => Err(anyhow!( + "Keylime internal server error: Failed to process attestation data" + )), + } + } +} + +impl KeylimeTeeHandler { + /// Configure the KBS frontend for the Keylime verifier. Keylime requires mTLS for all + /// commmunication, so it is assumed that the KBS has access to the certificates needed to + /// establish its identity for mTLS. The KBS also signs and validates JWTs on behalf of the + /// verifier (that is, using the verifier's private/public keys). With that, it is assumed that + /// the KBS also has access to the server's keypair. + pub async fn new(config: KeylimeVerifierConfig) -> Result { + // Retrieve the verifier's CA certificate. + let ca_cert = { + let path = format!("{}/cacert.crt", config.cv_ca_path); + let x509_pem = fs::read(&path).context(format!( + "unable to read Keylime CA certificate file from {}", + path + ))?; + + Certificate::from_pem(&x509_pem).context( + format!( + "bytes in Keylime verifier CA certificate file ({}) not a valid PEM-encoded X509 certificate", + path + ) + )? + }; + + // Build the TLS identity from client ceritifcate and private key. + let tls_identity = { + let mut vec = vec![]; + + // Retrieve the verifier's client X509 certificate. + let cli_cert = { + let path = format!("{}/client-cert.crt", config.cv_ca_path); + + fs::read(&path).context(format!( + "unable to read Keylime client X509 certificate file from {}", + path + ))? + }; + + vec.extend_from_slice(&cli_cert); + + // Retrieve the verifier's client private key. + let cli_priv_key = { + let path = format!("{}/client-private.pem", config.cv_ca_path); + + fs::read(&path).context(format!( + "unable to read Keylime client private key file from {}", + path + ))? + }; + + vec.extend_from_slice(&cli_priv_key); + + vec + }; + + // Retrieve the verifier's server public key. + let pub_key = { + let path = format!("{}/server-public.pem", config.cv_ca_path); + + let pem = fs::read(&path).context(format!( + "unable to read Keylime server public key file from {}", + path + ))?; + + Rsa::public_key_from_pem(&pem).context("unable to decode RSA server public key")? + }; + + // Retrieve the verifier's server private key. + let priv_key = { + let path = format!("{}/server-private.pem", config.cv_ca_path); + + let pem = fs::read(&path).context(format!( + "unable to read Keylime server private key file from {}", + path + ))?; + + EncodingKey::from_rsa_pem(&pem) + .context("unable to create JWT encoding key from Keylime Verifier private key")? + }; + + // Build an HTTP client to communicate with the Keylime verifier. Establish an identity + // using the verifier's certificates. + let client = ClientBuilder::new() + .identity( + Identity::from_pem(tls_identity.as_slice()) + .context("unable to establish client certificate authentication identity")?, + ) + .add_root_certificate(ca_cert) + .danger_accept_invalid_certs(true) + .build() + .context("unable to build HTTP client to Keylime verifier")?; + + Ok(Self { + config: config.clone(), + client, + priv_key, + pub_key, + }) + } + + /// Marshal a JSON Web Token containing the claims attested for a specific client by the + /// Keylime verifier. Sign the JWT with the verifier's private key to establish authenticity. + fn make_jwt( + &self, + claims: &mut Map, + nonce: Vec, + tee_pubkey: TeePubKey, + ) -> Result { + let mut jwt_claims: Map = Map::new(); + let exp = { + let now = OffsetDateTime::now_utc(); + + now.checked_add(Duration::minutes(5)) + .context("unable to calculate token expiration") + }?; + + jwt_claims.insert( + "exp".to_string(), + Value::Number( + Number::from_i128(exp.unix_timestamp().into()) + .context("unable to set expiration unix timestamp")?, + ), + ); + + jwt_claims.insert( + "nonce".to_string(), + serde_json::to_value(nonce).context("unable to serialize nonce to JSON value")?, + ); + + jwt_claims.insert( + "tee-pubkey".to_string(), + serde_json::to_value(tee_pubkey) + .context("unable to serialize TEE public key to JSON value")?, + ); + + jwt_claims.insert( + "tee-claims".to_string(), + serde_json::to_value(claims) + .context("unable to serialize TEE claims map to JSON value")?, + ); + + let header = Header { + alg: Algorithm::RS256, + jwk: Some(Jwk { + common: CommonParameters { + key_algorithm: Some(KeyAlgorithm::RS256), + ..Default::default() + }, + algorithm: AlgorithmParameters::RSA(RSAKeyParameters { + key_type: RSAKeyType::RSA, + n: BASE64_URL_SAFE_NO_PAD.encode(self.pub_key.n().to_vec()), + e: BASE64_URL_SAFE_NO_PAD.encode(self.pub_key.e().to_vec()), + }), + }), + ..Default::default() + }; + + let token = jsonwebtoken::encode(&header, &jwt_claims, &self.priv_key) + .context("unable to create JWT from claims")?; + + Ok(token) + } +} + +#[derive(Debug, Deserialize, Serialize)] +struct TeeData { + req: TeeRequest, + pubkey: TeePubKey, +} + +impl TryFrom> for TeeData { + type Error = anyhow::Error; + + fn try_from(evidence_list: Vec) -> Result { + // Only one set of TEE evidence can be verified by Keylime at the moment. + if evidence_list.len() != 1 { + bail!("only one TEE evidence type currently supported for keylime attestation"); + } + + let data = &evidence_list[0]; + + match data.tee { + Tee::Snp => (), + _ => bail!("invalid TEE"), + } + + // Get the TEE architecture evidence and runtime data. + let tee_arch_evidence: TeeArchitectureEvidence = + serde_json::from_value(data.tee_evidence.clone()) + .context("unable to deserialize independent TEE evidence")?; + let runtime: RuntimeData = serde_json::from_value(data.runtime_data.clone()) + .context("unable to deserialize independent runtime data")?; + + let tee_pubkey = runtime.tee_pubkey; + + // Only EC keys are permitted as the TEE public key. + let TeePubKey::EC { + crv: _, + alg: _, + x, + y, + } = tee_pubkey.clone() + else { + bail!("TEE public key must be elliptic curve key"); + }; + + // Build a request to specify the type of TEE and its respective evidence for the Keylime + // /verify/evidence TEE handler. + let req = TeeRequest { + tee_evidence: TeeEvidence::from(tee_arch_evidence), + nonce: runtime.nonce, + x, + y, + }; + + Ok(Self { + req, + pubkey: tee_pubkey, + }) + } +} + +/// A /verify/evidence TEE attestation request to the Keylime verifier. +#[derive(Debug, Deserialize, Serialize)] +struct TeeRequest { + /// The TEE evidence (also containing the TEE architecture in which to deserialize the evidence + /// as). + #[serde(rename = "tee-evidence")] + tee_evidence: TeeEvidence, + /// The nonce produced by the KBS challenge. + nonce: String, + /// TEE public key x coordinate. + #[serde(rename = "tee-pubkey-x-b64")] + x: String, + /// TEE public key y coordinate. + #[serde(rename = "tee-pubkey-y-b64")] + y: String, +} + +/// Evidence formatting based on TEE architecture. +#[derive(Debug, Deserialize, Serialize)] +struct TeeEvidence { + /// TEE architecture. + tee: Tee, + /// Attestation evidence. + evidence: TeeArchitectureEvidence, +} + +impl From for TeeEvidence { + fn from(evidence: TeeArchitectureEvidence) -> Self { + let tee: Tee = (&evidence).into(); + + Self { tee, evidence } + } +} + +/// Evidence formatting based on TEE architecture. Enum is untagged due to the underyling data +/// being parsed based off the Evidence's `tee` member. +#[derive(Debug, Deserialize, Serialize)] +#[serde(untagged)] +enum TeeArchitectureEvidence { + /// SEV-SNP evidence. + Snp { + /// SEV-SNP attestation report. + #[serde(rename = "snp-report")] + snp_report: String, + /// Optional ceritficate buffer from hypervisor. + #[serde(rename = "certs-buf")] + certs_buf: Option, + }, +} + +impl From<&TeeArchitectureEvidence> for Tee { + fn from(evidence: &TeeArchitectureEvidence) -> Self { + match evidence { + TeeArchitectureEvidence::Snp { .. } => Tee::Snp, + } + } +} diff --git a/kbs/src/attestation/mod.rs b/kbs/src/attestation/mod.rs index 10d9ca88d2..31f2df6073 100644 --- a/kbs/src/attestation/mod.rs +++ b/kbs/src/attestation/mod.rs @@ -8,6 +8,9 @@ pub mod coco; #[cfg(feature = "intel-trust-authority-as")] pub mod intel_trust_authority; +#[cfg(feature = "keylime-as")] +pub mod keylime; + pub mod backend; pub mod config; pub mod session;