Skip to content

Commit

Permalink
kbs: token: add verifier with JSON Web Keys
Browse files Browse the repository at this point in the history
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
mythi committed Aug 29, 2024
1 parent d78c9f3 commit 8d81645
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 2 deletions.
2 changes: 1 addition & 1 deletion kbs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down
161 changes: 161 additions & 0 deletions kbs/src/token/jwk.rs
Original file line number Diff line number Diff line change
@@ -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<jwk::JwkSet, JwksGetError> {
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::<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)
}
"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<Self> {
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<String> {
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::<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())
}
}
11 changes: 10 additions & 1 deletion kbs/src/token/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use strum::EnumString;
use tokio::sync::RwLock;

mod coco;
mod jwk;

#[async_trait]
pub trait AttestationTokenVerifier {
Expand All @@ -22,14 +23,18 @@ pub trait AttestationTokenVerifier {
pub enum AttestationTokenVerifierType {
#[default]
CoCo,
Jwk,
}

#[derive(Deserialize, Debug, Clone)]
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<String>,
}
Expand All @@ -42,5 +47,9 @@ pub async fn create_token_verifier(
coco::CoCoAttestationTokenVerifier::new(&config)?,
))
as Arc<RwLock<dyn AttestationTokenVerifier + Send + Sync>>),
AttestationTokenVerifierType::Jwk => Ok(Arc::new(RwLock::new(
jwk::JwkAttestationTokenVerifier::new(&config).await?,
))
as Arc<RwLock<dyn AttestationTokenVerifier + Send + Sync>>),
}
}

0 comments on commit 8d81645

Please sign in to comment.