From 4c917b1054349f5b3b273d80a7292cbb398c8a79 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 | 160 +++++++++++++++++++++++++++++++++++++++++++ kbs/src/token/mod.rs | 11 ++- 3 files changed, 171 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..fae3af25f4 --- /dev/null +++ b/kbs/src/token/jwk.rs @@ -0,0 +1,160 @@ +// 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()))?; + 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()))?; + + return Ok(jwkset); + } + if url.scheme() == "file" { + let file = File::open(url.path()) + .map_err(|e| JwksGetError::SourceAccess(format!("open {}: {}", url.path(), e)))?; + + return 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 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 19afed5b92..4094fe3798 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>), } }