-
Notifications
You must be signed in to change notification settings - Fork 90
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 <[email protected]>
- Loading branch information
Showing
3 changed files
with
177 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<jwk::JwkSet>, | ||
} | ||
|
||
pub async fn get_jwks_from_file_or_url(p: &str) -> Result<jwk::JwkSet, JwksGetError> { | ||
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::<OpenIDConfig>() | ||
.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::<jwk::JwkSet>() | ||
.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<Self> { | ||
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<String> { | ||
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::<Value>(&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()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters