From d78c9f3db6b2787c436ed65cae21cc361be4dc4e Mon Sep 17 00:00:00 2001 From: Mikko Ylinen Date: Thu, 8 Aug 2024 15:06:19 +0300 Subject: [PATCH 1/2] kbs: make token verifier initialization async This is useful if any token verifier needs initialization data pulled remotely. Signed-off-by: Mikko Ylinen --- kbs/src/lib.rs | 2 +- kbs/src/token/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/kbs/src/lib.rs b/kbs/src/lib.rs index 5d5177598..b46abf82f 100644 --- a/kbs/src/lib.rs +++ b/kbs/src/lib.rs @@ -240,7 +240,7 @@ impl ApiServer { #[cfg(feature = "resource")] let token_verifier = - crate::token::create_token_verifier(self.attestation_token_config.clone())?; + crate::token::create_token_verifier(self.attestation_token_config.clone()).await?; #[cfg(feature = "policy")] let policy_engine = PolicyEngine::new(&self.policy_engine_config).await?; diff --git a/kbs/src/token/mod.rs b/kbs/src/token/mod.rs index b0160ded3..19afed5b9 100644 --- a/kbs/src/token/mod.rs +++ b/kbs/src/token/mod.rs @@ -34,7 +34,7 @@ pub struct AttestationTokenVerifierConfig { pub trusted_certs_paths: Vec, } -pub fn create_token_verifier( +pub async fn create_token_verifier( config: AttestationTokenVerifierConfig, ) -> Result>> { match config.attestation_token_type { From ce559482b2070e4d53d537b240437123d1deb655 Mon Sep 17 00:00:00 2001 From: Mikko Ylinen Date: Thu, 8 Aug 2024 15:18:42 +0300 Subject: [PATCH 2/2] kbs: token: add verifier with JSON Web Keys Add a new token verifier that uses JSON Web Keys (JWK) from the configured JWK Set sources. JWK Sets can be provided locally using file:// URL schema or they can be downloaded automatically via OpenID Connect configuration URLs providing a pointer via "jwks_uri". Signed-off-by: Mikko Ylinen --- kbs/Cargo.toml | 2 +- kbs/src/token/jwk.rs | 161 +++++++++++++++++++++++++++++++++++++++++++ kbs/src/token/mod.rs | 11 ++- 3 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 kbs/src/token/jwk.rs diff --git a/kbs/Cargo.toml b/kbs/Cargo.toml index a983769c5..e61283c31 100644 --- a/kbs/Cargo.toml +++ b/kbs/Cargo.toml @@ -10,7 +10,7 @@ edition.workspace = true default = ["coco-as-builtin", "resource", "opa", "rustls"] # Feature that allows to access resources from KBS -resource = ["rsa", "dep:openssl", "reqwest", "aes-gcm"] +resource = ["rsa", "dep:openssl", "reqwest", "aes-gcm", "jsonwebtoken"] # Support a backend attestation service for KBS as = [] diff --git a/kbs/src/token/jwk.rs b/kbs/src/token/jwk.rs new file mode 100644 index 000000000..aa975e075 --- /dev/null +++ b/kbs/src/token/jwk.rs @@ -0,0 +1,161 @@ +// Copyright (c) 2024 by Intel Corporation +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +use crate::token::{AttestationTokenVerifier, AttestationTokenVerifierConfig}; +use anyhow::*; +use async_trait::async_trait; +use jsonwebtoken::{decode, decode_header, jwk, Algorithm, DecodingKey, Validation}; +use reqwest::{get, Url}; +use serde::Deserialize; +use serde_json::Value; +use std::fs::File; +use std::io::BufReader; +use std::result::Result::Ok; +use std::str::FromStr; +use thiserror::Error; + +const OPENID_CONFIG_URL_SUFFIX: &str = ".well-known/openid-configuration"; + +#[derive(Error, Debug)] +pub enum JwksGetError { + #[error("Invalid source path: {0}")] + SourcePath(String), + #[error("Failed to access source: {0}")] + SourceAccess(String), + #[error("Failed to deserialize source data: {0}")] + SourceDeserializeJson(String), +} + +#[derive(Deserialize)] +struct OpenIDConfig { + jwks_uri: String, +} + +pub struct JwkAttestationTokenVerifier { + trusted_certs: jwk::JwkSet, +} + +pub async fn get_jwks_from_file_or_url(p: &str) -> Result { + let mut url = Url::parse(p).map_err(|e| JwksGetError::SourcePath(e.to_string()))?; + match url.scheme() { + "https" => { + url.set_path(OPENID_CONFIG_URL_SUFFIX); + + let oidc = get(url.as_str()) + .await + .map_err(|e| JwksGetError::SourceAccess(e.to_string()))? + .json::() + .await + .map_err(|e| JwksGetError::SourceDeserializeJson(e.to_string()))?; + + let jwkset = get(oidc.jwks_uri) + .await + .map_err(|e| JwksGetError::SourceAccess(e.to_string()))? + .json::() + .await + .map_err(|e| JwksGetError::SourceDeserializeJson(e.to_string()))?; + + Ok(jwkset) + } + "file" => { + let file = File::open(url.path()) + .map_err(|e| JwksGetError::SourceAccess(format!("open {}: {}", url.path(), e)))?; + + serde_json::from_reader(BufReader::new(file)) + .map_err(|e| JwksGetError::SourceDeserializeJson(e.to_string())) + } + _ => Err(JwksGetError::SourcePath(format!( + "unsupported scheme {} (must be either file or https)", + url.scheme() + ))), + } +} + +impl JwkAttestationTokenVerifier { + pub async fn new(config: &AttestationTokenVerifierConfig) -> Result { + let mut trusted_certs = jwk::JwkSet { keys: Vec::new() }; + + for path in config.trusted_certs_paths.iter() { + match get_jwks_from_file_or_url(path).await { + Ok(mut jwkset) => trusted_certs.keys.append(&mut jwkset.keys), + Err(e) => log::warn!("error getting JWKS: {:?}", e), + } + } + + Ok(Self { trusted_certs }) + } +} + +#[async_trait] +impl AttestationTokenVerifier for JwkAttestationTokenVerifier { + async fn verify(&self, token: String) -> Result { + if self.trusted_certs.keys.is_empty() { + bail!("Cannot verify token since trusted JWK Set is empty"); + }; + + let kid = decode_header(&token) + .context("Failed to decode attestation token header")? + .kid + .ok_or(anyhow!("Failed to decode kid in the token header"))?; + + let key = &self + .trusted_certs + .find(&kid) + .ok_or(anyhow!("Failed to find Jwk with kid {kid} in JwkSet"))?; + + let key_alg = key + .common + .key_algorithm + .ok_or(anyhow!("Failed to find key_algorithm in Jwk"))? + .to_string(); + + let alg = Algorithm::from_str(key_alg.as_str())?; + + let dkey = DecodingKey::from_jwk(key)?; + let token_data = decode::(&token, &dkey, &Validation::new(alg)) + .context("Failed to decode attestation token")?; + + Ok(serde_json::to_string(&token_data.claims)?) + } +} + +#[cfg(test)] +mod tests { + use crate::token::jwk::get_jwks_from_file_or_url; + use rstest::rstest; + + #[rstest] + #[case("https://", true)] + #[case("http://example.com", true)] + #[case("file:///does/not/exist/keys.jwks", true)] + #[case("/does/not/exist/keys.jwks", true)] + #[tokio::test] + async fn test_source_path_validation(#[case] source_path: &str, #[case] expect_error: bool) { + assert_eq!( + expect_error, + get_jwks_from_file_or_url(source_path).await.is_err() + ) + } + + #[rstest] + #[case( + "{\"keys\":[{\"kty\":\"oct\",\"alg\":\"HS256\",\"kid\":\"coco123\",\"k\":\"foobar\"}]}", + false + )] + #[case( + "{\"keys\":[{\"kty\":\"oct\",\"alg\":\"COCO42\",\"kid\":\"coco123\",\"k\":\"foobar\"}]}", + true + )] + #[tokio::test] + async fn test_source_reads(#[case] json: &str, #[case] expect_error: bool) { + let tmp_dir = tempfile::tempdir().expect("to get tmpdir"); + let jwks_file = tmp_dir.path().join("test.jwks"); + + let _ = std::fs::write(&jwks_file, json).expect("to get testdata written to tmpdir"); + + let p = "file://".to_owned() + jwks_file.to_str().expect("to get path as str"); + + assert_eq!(expect_error, get_jwks_from_file_or_url(&p).await.is_err()) + } +} diff --git a/kbs/src/token/mod.rs b/kbs/src/token/mod.rs index 19afed5b9..5f4ab6fb9 100644 --- a/kbs/src/token/mod.rs +++ b/kbs/src/token/mod.rs @@ -10,6 +10,7 @@ use strum::EnumString; use tokio::sync::RwLock; mod coco; +mod jwk; #[async_trait] pub trait AttestationTokenVerifier { @@ -22,6 +23,7 @@ pub trait AttestationTokenVerifier { pub enum AttestationTokenVerifierType { #[default] CoCo, + Jwk, } #[derive(Deserialize, Debug, Clone)] @@ -29,7 +31,10 @@ pub struct AttestationTokenVerifierConfig { #[serde(default)] pub attestation_token_type: AttestationTokenVerifierType, - // Trusted Certificates file (PEM format) path to verify Attestation Token Signature. + /// Trusted Certificates file (PEM format) path (for "CoCo") or a valid Url + /// (file:// and https:// schemes accepted) pointing to a local JWKSet file + /// or to an OpenID configuration url giving a pointer to JWKSet certificates + /// (for "Jwk") to verify Attestation Token Signature. #[serde(default)] pub trusted_certs_paths: Vec, } @@ -42,5 +47,9 @@ pub async fn create_token_verifier( coco::CoCoAttestationTokenVerifier::new(&config)?, )) as Arc>), + AttestationTokenVerifierType::Jwk => Ok(Arc::new(RwLock::new( + jwk::JwkAttestationTokenVerifier::new(&config).await?, + )) + as Arc>), } }