From 91f239a53bdf7b42f8e46eb9f367978fa63e713f Mon Sep 17 00:00:00 2001 From: Mikko Ylinen Date: Thu, 8 Aug 2024 15:18:42 +0300 Subject: [PATCH] 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 | 166 +++++++++++++++++++++++++++++++++++++++++++ kbs/src/token/mod.rs | 11 ++- 3 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 kbs/src/token/jwk.rs diff --git a/kbs/Cargo.toml b/kbs/Cargo.toml index a983769c56..e61283c314 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 0000000000..cf2dbdc514 --- /dev/null +++ b/kbs/src/token/jwk.rs @@ -0,0 +1,166 @@ +// 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::path::Path; +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: Option, +} + +pub async fn get_jwks_from_file_or_url(p: &str) -> Result { + match Url::parse(p) { + Err(e) => Err(JwksGetError::SourcePath(e.to_string())), + Ok(mut url) if 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) + } + Ok(url) if url.scheme() == "file" && Path::new(url.path()).exists() => { + let file = File::open(url.path()) + .map_err(|e| JwksGetError::SourceAccess(format!("open {}: {}", url.path(), e)))?; + let reader = BufReader::new(file); + + serde_json::from_reader(reader) + .map_err(|e| JwksGetError::SourceDeserializeJson(e.to_string())) + } + Ok(url) => Err(JwksGetError::SourcePath(format!( + "unsupported scheme {} (must be either file or https) or missing file path", + url.scheme() + ))), + } +} + +impl JwkAttestationTokenVerifier { + pub async fn new(config: &AttestationTokenVerifierConfig) -> Result { + let trusted_certs = match &config.trusted_certs_paths { + Some(paths) => { + let mut keyset = jwk::JwkSet { keys: Vec::new() }; + + for path in paths.iter() { + match get_jwks_from_file_or_url(path).await { + Ok(mut jwkset) => keyset.keys.append(&mut jwkset.keys), + Err(e) => log::warn!("error getting JWKS: {:?}", e), + } + } + + match keyset.keys.len() { + 0 => None, + _ => Some(keyset), + } + } + None => None, + }; + + Ok(Self { trusted_certs }) + } +} + +#[async_trait] +impl AttestationTokenVerifier for JwkAttestationTokenVerifier { + async fn verify(&self, token: String) -> Result { + let Some(keyset) = &self.trusted_certs else { + bail!("Cannot verity 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 = keyset + .find(&kid) + .ok_or(anyhow!("Failed to find kid in trusted certificates"))?; + + let alg = Algorithm::from_str(key.common.key_algorithm.unwrap().to_string().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 99674c2a46..9d8a27a2fa 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 { @@ -21,13 +22,17 @@ pub trait AttestationTokenVerifier { #[derive(Deserialize, Debug, Clone, EnumString)] pub enum AttestationTokenVerifierType { CoCo, + Jwk, } #[derive(Deserialize, Debug, Clone)] pub struct AttestationTokenVerifierConfig { 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. pub trusted_certs_paths: Option>, } @@ -48,5 +53,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>), } }