From eb92ed7627c2d5bb20c4d055c8037ce34326f470 Mon Sep 17 00:00:00 2001 From: Jiale Zhang Date: Wed, 20 Dec 2023 17:45:18 +0800 Subject: [PATCH] KBS: Support Verify X.509 certificate chain in Attestation Token JWK field Signed-off-by: Jiale Zhang --- kbs/config/kbs-config-grpc.toml | 3 + .../kbs-config-intel-trust-authority.toml | 3 + kbs/config/kbs-config.toml | 2 + kbs/config/kubernetes/base/kbs-config.toml | 3 + kbs/src/api/Cargo.toml | 2 +- kbs/src/api/src/config.rs | 11 +- kbs/src/api/src/lib.rs | 11 +- kbs/src/api/src/token/coco.rs | 103 +++++++++++++++--- kbs/src/api/src/token/mod.rs | 33 ++++-- kbs/src/kbs/src/main.rs | 2 +- kbs/test/data/e2e/kbs.toml | 3 + kbs/test/data/e2e/resource-kbs.toml | 3 + 12 files changed, 137 insertions(+), 42 deletions(-) diff --git a/kbs/config/kbs-config-grpc.toml b/kbs/config/kbs-config-grpc.toml index 1ef4b83acd..cc8012d57e 100644 --- a/kbs/config/kbs-config-grpc.toml +++ b/kbs/config/kbs-config-grpc.toml @@ -1,5 +1,8 @@ insecure_http = true insecure_api = true +[attestation_token_config] +attestation_token_type = "CoCo" + [grpc_config] as_addr = "http://127.0.0.1:50004" diff --git a/kbs/config/kbs-config-intel-trust-authority.toml b/kbs/config/kbs-config-intel-trust-authority.toml index a72ece2b60..df70b71b6c 100644 --- a/kbs/config/kbs-config-intel-trust-authority.toml +++ b/kbs/config/kbs-config-intel-trust-authority.toml @@ -1,6 +1,9 @@ insecure_http = true insecure_api = true +[attestation_token_config] +attestation_token_type = "CoCo" + [intel_trust_authority_config] base_url = "https://api.trustauthority.intel.com" api_key = "tBfd5kKX2x9ahbodKV1..." diff --git a/kbs/config/kbs-config.toml b/kbs/config/kbs-config.toml index 2e5abe0f6c..3846fe1c73 100644 --- a/kbs/config/kbs-config.toml +++ b/kbs/config/kbs-config.toml @@ -1,5 +1,7 @@ insecure_http = true insecure_api = true + +[attestation_token_config] attestation_token_type = "CoCo" [repository_config] diff --git a/kbs/config/kubernetes/base/kbs-config.toml b/kbs/config/kubernetes/base/kbs-config.toml index c73c130926..2e4eab3e82 100644 --- a/kbs/config/kubernetes/base/kbs-config.toml +++ b/kbs/config/kubernetes/base/kbs-config.toml @@ -4,6 +4,9 @@ auth_public_key = "/kbs/kbs.pem" # https://cert-manager.io/docs/configuration/acme/ insecure_http = true +[attestation_token_config] +attestation_token_type = "CoCo" + [as_config] work_dir = "/opt/confidential-containers/attestation-service" policy_engine = "opa" diff --git a/kbs/src/api/Cargo.toml b/kbs/src/api/Cargo.toml index 2277fd201b..03b771e1ca 100644 --- a/kbs/src/api/Cargo.toml +++ b/kbs/src/api/Cargo.toml @@ -8,7 +8,7 @@ edition.workspace = true [features] default = ["coco-as-builtin", "resource", "opa", "rustls"] -resource = ["rsa", "aes-gcm"] +resource = ["rsa", "dep:openssl", "aes-gcm"] as = [] policy = [] opa = ["policy"] diff --git a/kbs/src/api/src/config.rs b/kbs/src/api/src/config.rs index ca3f3164f1..4359f0687d 100644 --- a/kbs/src/api/src/config.rs +++ b/kbs/src/api/src/config.rs @@ -11,7 +11,7 @@ use crate::policy_engine::PolicyEngineConfig; #[cfg(feature = "resource")] use crate::resource::RepositoryConfig; #[cfg(feature = "resource")] -use crate::token::AttestationTokenVerifierType; +use crate::token::AttestationTokenVerifierConfig; use anyhow::anyhow; #[cfg(any(feature = "coco-as-builtin", feature = "coco-as-builtin-no-verifier"))] use attestation_service::config::Config as AsConfig; @@ -22,7 +22,6 @@ use serde_json::Value; use std::net::SocketAddr; use std::path::{Path, PathBuf}; -const DEFAULT_ATTESTATION_TOKEN_TYPE: &str = "CoCo"; const DEFAULT_INSECURE_API: bool = false; const DEFAULT_INSECURE_HTTP: bool = false; const DEFAULT_SOCKET: &str = "127.0.0.1:8080"; @@ -35,12 +34,9 @@ pub struct KbsConfig { #[cfg(feature = "resource")] pub repository_config: Option, - /// Attestation token result broker type. - /// - /// Possible values: - /// * `CoCo` + /// Attestation token result broker config. #[cfg(feature = "resource")] - pub attestation_token_type: AttestationTokenVerifierType, + pub attestation_token_config: AttestationTokenVerifierConfig, /// Configuration for the built-in Attestation Service. #[cfg(any(feature = "coco-as-builtin", feature = "coco-as-builtin-no-verifier"))] @@ -92,7 +88,6 @@ impl TryFrom<&Path> for KbsConfig { /// `config` crate. See `KbsConfig` for schema information. fn try_from(config_path: &Path) -> Result { let c = Config::builder() - .set_default("attestation_token_type", DEFAULT_ATTESTATION_TOKEN_TYPE)? .set_default("insecure_api", DEFAULT_INSECURE_API)? .set_default("insecure_http", DEFAULT_INSECURE_HTTP)? .set_default("sockets", vec![DEFAULT_SOCKET])? diff --git a/kbs/src/api/src/lib.rs b/kbs/src/api/src/lib.rs index a9d0180afd..a034ad0fb0 100644 --- a/kbs/src/api/src/lib.rs +++ b/kbs/src/api/src/lib.rs @@ -28,7 +28,7 @@ use semver::{BuildMetadata, Prerelease, Version, VersionReq}; use std::net::SocketAddr; use std::path::PathBuf; #[cfg(feature = "resource")] -use token::AttestationTokenVerifierType; +use token::AttestationTokenVerifierConfig; #[cfg(feature = "rustls")] use rustls::ServerConfig; @@ -112,7 +112,7 @@ pub struct ApiServer { #[cfg(feature = "resource")] repository_config: RepositoryConfig, #[cfg(feature = "resource")] - attestation_token_type: AttestationTokenVerifierType, + attestation_token_config: AttestationTokenVerifierConfig, #[cfg(feature = "policy")] policy_engine_config: PolicyEngineConfig, } @@ -131,7 +131,7 @@ impl ApiServer { http_timeout: i64, insecure_api: bool, #[cfg(feature = "resource")] repository_config: RepositoryConfig, - #[cfg(feature = "resource")] attestation_token_type: AttestationTokenVerifierType, + #[cfg(feature = "resource")] attestation_token_config: AttestationTokenVerifierConfig, #[cfg(feature = "policy")] policy_engine_config: PolicyEngineConfig, ) -> Result { if !insecure && (private_key.is_none() || certificate.is_none()) { @@ -159,7 +159,7 @@ impl ApiServer { #[cfg(feature = "resource")] repository_config, #[cfg(feature = "resource")] - attestation_token_type, + attestation_token_config, #[cfg(feature = "policy")] policy_engine_config, }) @@ -244,7 +244,8 @@ impl ApiServer { let repository = self.repository_config.initialize()?; #[cfg(feature = "resource")] - let token_verifier = self.attestation_token_type.to_token_verifier()?; + let token_verifier = + crate::token::create_token_verifier(self.attestation_token_config.clone())?; #[cfg(feature = "policy")] let policy_engine = PolicyEngine::new(&self.policy_engine_config).await?; diff --git a/kbs/src/api/src/token/coco.rs b/kbs/src/api/src/token/coco.rs index fbb57cdc84..0e103862cd 100644 --- a/kbs/src/api/src/token/coco.rs +++ b/kbs/src/api/src/token/coco.rs @@ -2,15 +2,42 @@ // Licensed under the Apache License, Version 2.0, see LICENSE for details. // SPDX-License-Identifier: Apache-2.0 -use crate::token::AttestationTokenVerifier; +use crate::token::{AttestationTokenVerifier, AttestationTokenVerifierConfig}; use anyhow::*; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use base64::Engine; -use rsa::signature::Verifier; +use openssl::hash::MessageDigest; +use openssl::pkey::PKey; +use openssl::rsa::Rsa; +use openssl::sign::Verifier; +use openssl::stack::Stack; +use openssl::x509::store::{X509Store, X509StoreBuilder}; +use openssl::x509::{X509StoreContext, X509}; use serde_json::Value; -#[derive(Default)] -pub struct CoCoAttestationTokenVerifier {} +pub struct CoCoAttestationTokenVerifier { + trust_certs: Option, +} + +impl CoCoAttestationTokenVerifier { + pub fn new(config: &AttestationTokenVerifierConfig) -> Result { + let trust_certs = match &config.trust_certs_paths { + Some(paths) => { + let mut store_builder = X509StoreBuilder::new()?; + for path in paths { + let trust_cert_pem = std::fs::read(path) + .map_err(|e| anyhow!("Load trusted certificate failed: {e}"))?; + let trust_cert = X509::from_pem(&trust_cert_pem)?; + store_builder.add_cert(trust_cert.to_owned())?; + } + Some(store_builder.build()) + } + None => None, + }; + + Ok(Self { trust_certs }) + } +} impl AttestationTokenVerifier for CoCoAttestationTokenVerifier { fn verify(&self, token: String) -> Result { @@ -41,12 +68,18 @@ impl AttestationTokenVerifier for CoCoAttestationTokenVerifier { let jwk_value = claims_value["jwk"].as_object().ok_or_else(|| anyhow!("CoCo Attestation Token Claims must contain public key (JWK format) to verify signature"))?; let jwk = serde_json::to_string(&jwk_value)?; + let rsa_jwk = serde_json::from_str::(&jwk)?; let payload = format!("{}.{}", &split_token[0], &split_token[1]) .as_bytes() .to_vec(); match header_value["alg"].as_str() { - Some("RS384") => rs384_verify(&payload, &signature, &jwk)?, + Some("RS384") => { + if rsa_jwk.alg != *"RS384" { + bail!("Unmatched RSA JWK alg"); + } + rs384_verify(&payload, &signature, &rsa_jwk)?; + } None => { bail!("Miss `alg` in JWT header") } @@ -55,6 +88,40 @@ impl AttestationTokenVerifier for CoCoAttestationTokenVerifier { } } + if let Some(trusted_store) = &self.trust_certs { + match rsa_jwk.x5c { + Some(x5c) => { + let mut cert_chain: Vec = vec![]; + for base64_der_cert in x5c { + let der_cert = URL_SAFE_NO_PAD.decode(base64_der_cert)?; + let cert = X509::from_der(&der_cert)?; + cert_chain.push(cert) + } + + let mut untrusted_stack = Stack::::new()?; + for cert in cert_chain.iter().skip(1) { + untrusted_stack.push(cert.clone())?; + } + + let mut context = X509StoreContext::new()?; + context.init(&trusted_store, &cert_chain[0], &untrusted_stack, |_| { + Result::Ok(()) + })?; + + if !context.verify_cert()? { + bail!("Untrusted Certificate in Attestation Token JWK") + } + } + None => { + if rsa_jwk.x5u.is_some() { + bail!("Now only support \"x5c\" in token JWK"); + } else { + bail!("Missing certificate in Attestation Token JWK"); + } + } + } + } + Ok(serde_json::to_string(&claims_value)?) } } @@ -66,22 +133,22 @@ struct RsaJWK { alg: String, n: String, e: String, + x5u: Option, + x5c: Option>, } -fn rs384_verify(payload: &[u8], signature: &[u8], jwk: &str) -> Result<()> { - let jwk = serde_json::from_str::(jwk)?; - if jwk.alg != *"RS384" { - bail!("Unmatched RSA JWK alg"); - } +fn rs384_verify(payload: &[u8], signature: &[u8], jwk: &RsaJWK) -> Result<()> { + let n = openssl::bn::BigNum::from_slice(&URL_SAFE_NO_PAD.decode(&jwk.n)?)?; + let e = openssl::bn::BigNum::from_slice(&URL_SAFE_NO_PAD.decode(&jwk.e)?)?; + let rsa_public_key = Rsa::from_public_components(n, e)?; + let rsa_pkey = PKey::from_rsa(rsa_public_key)?; - let pkcs1v15_signature = rsa::pkcs1v15::Signature::try_from(signature)?; + let mut verifier = Verifier::new(MessageDigest::sha384(), &rsa_pkey)?; + verifier.update(payload)?; - let n = rsa::BigUint::from_bytes_be(&URL_SAFE_NO_PAD.decode(&jwk.n)?); - let e = rsa::BigUint::from_bytes_be(&URL_SAFE_NO_PAD.decode(&jwk.e)?); - let rsa_public_key = rsa::RsaPublicKey::new(n, e)?; - let verify_key = rsa::pkcs1v15::VerifyingKey::::new(rsa_public_key); + if !verifier.verify(signature)? { + bail!("RS384 verify failed") + } - verify_key - .verify(payload, &pkcs1v15_signature) - .map_err(|e| anyhow!("RS384 verify failed: {e}")) + Ok(()) } diff --git a/kbs/src/api/src/token/mod.rs b/kbs/src/api/src/token/mod.rs index 9d86498a42..50b8aef8cf 100644 --- a/kbs/src/api/src/token/mod.rs +++ b/kbs/src/api/src/token/mod.rs @@ -22,19 +22,34 @@ pub enum AttestationTokenVerifierType { CoCo, } -impl AttestationTokenVerifierType { - pub fn to_token_verifier( - &self, - ) -> Result>> { - match self { - AttestationTokenVerifierType::CoCo => Ok(Arc::new(RwLock::new( - coco::CoCoAttestationTokenVerifier::default(), - )) - as Arc>), +#[derive(Deserialize, Debug, Clone)] +pub struct AttestationTokenVerifierConfig { + pub attestation_token_type: AttestationTokenVerifierType, + + // Trusted Certificates file (PEM format) path to verify Attestation Token Signature.3 + pub trust_certs_paths: Option>, +} + +impl Default for AttestationTokenVerifierConfig { + fn default() -> Self { + Self { + attestation_token_type: AttestationTokenVerifierType::CoCo, + trust_certs_paths: None, } } } +pub fn create_token_verifier( + config: AttestationTokenVerifierConfig, +) -> Result>> { + match config.attestation_token_type { + AttestationTokenVerifierType::CoCo => Ok(Arc::new(RwLock::new( + coco::CoCoAttestationTokenVerifier::new(&config)?, + )) + as Arc>), + } +} + impl fmt::Display for AttestationTokenVerifierType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{:?}", self) diff --git a/kbs/src/kbs/src/main.rs b/kbs/src/kbs/src/main.rs index 021844d623..c8797fbb6b 100644 --- a/kbs/src/kbs/src/main.rs +++ b/kbs/src/kbs/src/main.rs @@ -67,7 +67,7 @@ async fn main() -> Result<()> { #[cfg(feature = "resource")] kbs_config.repository_config.unwrap_or_default(), #[cfg(feature = "resource")] - kbs_config.attestation_token_type, + kbs_config.attestation_token_config, #[cfg(feature = "opa")] kbs_config.policy_engine_config.unwrap_or_default(), )?; diff --git a/kbs/test/data/e2e/kbs.toml b/kbs/test/data/e2e/kbs.toml index 009abb5da7..e5084b4195 100644 --- a/kbs/test/data/e2e/kbs.toml +++ b/kbs/test/data/e2e/kbs.toml @@ -2,6 +2,9 @@ sockets = ["127.0.0.1:8080"] auth_public_key = "./kbs.pem" insecure_http = true +[attestation_token_config] +attestation_token_type = "CoCo" + [repository_config] type = "LocalFs" dir_path = "./data/repository" diff --git a/kbs/test/data/e2e/resource-kbs.toml b/kbs/test/data/e2e/resource-kbs.toml index a29974f4c9..78eed0d55a 100644 --- a/kbs/test/data/e2e/resource-kbs.toml +++ b/kbs/test/data/e2e/resource-kbs.toml @@ -2,6 +2,9 @@ sockets = ["127.0.0.1:50002"] auth_public_key = "./kbs.pem" insecure_http = true +[attestation_token_config] +attestation_token_type = "CoCo" + [repository_config] type = "LocalFs" dir_path = "./data/repository"