Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

kbs: token: add verifier with JSON Web Keys #458

Merged
merged 2 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
2 changes: 1 addition & 1 deletion kbs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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?;
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 {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())
}
}
13 changes: 11 additions & 2 deletions 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,25 +23,33 @@ 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>,
}

pub fn create_token_verifier(
pub async fn create_token_verifier(
config: AttestationTokenVerifierConfig,
) -> Result<Arc<RwLock<dyn AttestationTokenVerifier + Send + Sync>>> {
match config.attestation_token_type {
AttestationTokenVerifierType::CoCo => Ok(Arc::new(RwLock::new(
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>>),
}
}
Loading