Skip to content

Commit

Permalink
Provide endpoints under multiple API versions
Browse files Browse the repository at this point in the history
Make the agent to provide the endpoints under multiple API versions
(currently only under versions 2.1 and 2.2).

A new configuration option is introduced, 'api_versions', which allows
the user to set the API versions to enable.  Only a subset of the
versions defined in api::SUPPORTED_API_VERSIONS can be enabled.  If a
unsupported version is set in the configuration, it will be ignored with
a warning.  The agent will fail to start if no valid API versions list
is configured.

The 'api_versions' option supports 2 keywords that can be used instead
of the explicit list of versions:

- "default": Enables all the supported API versions
- "latest": Enables only the latest supported API version

This is part of the implementation of the enhancement proposal 114:
keylime/enhancements#115

Signed-off-by: Anderson Toshiyuki Sasaki <[email protected]>
  • Loading branch information
ansasaki committed Dec 16, 2024
1 parent 7b3c817 commit 56a3206
Show file tree
Hide file tree
Showing 6 changed files with 314 additions and 102 deletions.
12 changes: 11 additions & 1 deletion keylime-agent.conf
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,17 @@
# The configuration file version
#
# To override, set KEYLIME_AGENT_VERSION environment variable.
version = "2.3"
version = "2.4"

# The enabled API versions
# This sets which of the supported API versions to enable.
# Only supported versions can be set, which are defined by
# api::SUPPORTED_API_VERSIONS
# A list of versions to enable can be provided (e.g. "2.1, 2.2")
# The following keywords are also supported:
# - "default": Enables all supported API versions
# - "latest": Enables only the latest supported API version
api_versions = "default"

# The agent's UUID.
# If you set this to "generate", Keylime will create a random UUID.
Expand Down
130 changes: 122 additions & 8 deletions keylime-agent/src/api.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,49 @@
use crate::{
agent_handler,
common::{JsonWrapper, API_VERSION},
config, errors_handler, keys_handler, notifications_handler,
quotes_handler,
agent_handler, common::JsonWrapper, config, errors_handler, keys_handler,
notifications_handler, quotes_handler, QuoteData,
};
use actix_web::{http, web, HttpRequest, HttpResponse, Responder, Scope};
use keylime::list_parser::parse_list;
use log::*;
use serde::{Deserialize, Serialize};
use thiserror::Error;

pub const SUPPORTED_API_VERSIONS: &[&str] = &[API_VERSION];
pub static SUPPORTED_API_VERSIONS: &[&str] = &["2.1", "2.2"];

#[derive(Serialize, Deserialize, Debug)]
struct KeylimeVersion {
supported_version: String,
}

#[derive(Error, Debug, PartialEq)]
pub enum APIError {
#[error("API version \"{0}\" not supported")]
UnsupportedVersion(String),
}

/// This is the handler for the GET request for the API version
pub async fn version(
req: HttpRequest,
quote_data: web::Data<QuoteData<'_>>,
) -> impl Responder {
info!(
"GET invoked from {:?} with uri {}",
req.connection_info().peer_addr().unwrap(), //#[allow_ci]
req.uri()
);

// The response reports the latest supported version or error
match quote_data.api_versions.last() {
Some(version) => {
HttpResponse::Ok().json(JsonWrapper::success(KeylimeVersion {
supported_version: version.clone(),
}))
}
None => HttpResponse::InternalServerError()
.json(JsonWrapper::error(500, "Misconfigured API version")),
}
}

/// Handles the default case for the API version scope
async fn api_default(req: HttpRequest) -> impl Responder {
let error;
Expand Down Expand Up @@ -95,8 +123,10 @@ fn configure_api_v2_2(cfg: &mut web::ServiceConfig) {
/// Get a scope configured for the given API version
pub(crate) fn get_api_scope(version: &str) -> Result<Scope, APIError> {
match version {
"v2.1" => Ok(web::scope(version).configure(configure_api_v2_1)),
"v2.2" => Ok(web::scope(version).configure(configure_api_v2_2)),
"2.1" => Ok(web::scope(format!("v{version}").as_ref())
.configure(configure_api_v2_1)),
"2.2" => Ok(web::scope(format!("v{version}").as_ref())
.configure(configure_api_v2_2)),
_ => Err(APIError::UnsupportedVersion(version.into())),
}
}
Expand All @@ -119,7 +149,10 @@ mod tests {
// Test that a valid version is successful
let version = SUPPORTED_API_VERSIONS.last().unwrap(); //#[allow_ci]
let result = get_api_scope(version);
assert!(result.is_ok());
assert!(
result.is_ok(),
"Failed to get scope for version \"{version}\""
);
let scope = result.unwrap(); //#[allow_ci]
}

Expand Down Expand Up @@ -171,4 +204,85 @@ mod tests {
assert_eq!(result.results, json!({}));
assert_eq!(result.code, 405);
}

#[actix_rt::test]
async fn test_default_version() {
let (fixture, mutex) = QuoteData::fixture().await.unwrap(); //#[allow_ci]
let quotedata = web::Data::new(fixture);
let mut app = test::init_service(
App::new()
.app_data(quotedata)
.route("/version", web::get().to(version)),
)
.await;

let req = test::TestRequest::get().uri("/version").to_request();

let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());

let body: JsonWrapper<KeylimeVersion> =
test::read_body_json(resp).await;
assert_eq!(
Some(body.results.supported_version),
SUPPORTED_API_VERSIONS.last().map(|x| x.to_string())
);
}

#[actix_rt::test]
async fn test_custom_version() {
// Get the first supported API version
let first = SUPPORTED_API_VERSIONS[0].to_string();

let (mut fixture, mutex) = QuoteData::fixture().await.unwrap(); //#[allow_ci]

// Set the API version with only the first supported version
fixture.api_versions = vec![first];
let quotedata = web::Data::new(fixture);

let mut app = test::init_service(
App::new()
.app_data(quotedata.clone())
.route("/version", web::get().to(version)),
)
.await;

let req = test::TestRequest::get().uri("/version").to_request();

let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());

// Check that the returned version is the version from the configuration
let body: JsonWrapper<KeylimeVersion> =
test::read_body_json(resp).await;
assert_eq!(
body.results.supported_version,
SUPPORTED_API_VERSIONS[0].to_string(),
);
}

#[actix_rt::test]
async fn test_misconfigured_version() {
let (mut fixture, mutex) = QuoteData::fixture().await.unwrap(); //#[allow_ci]

// Set the API version with empty Vec
fixture.api_versions = vec![];
let quotedata = web::Data::new(fixture);

let mut app = test::init_service(
App::new()
.app_data(quotedata.clone())
.route("/version", web::get().to(version)),
)
.await;

let req = test::TestRequest::get().uri("/version").to_request();

let resp = test::call_service(&app, req).await;
assert!(resp.status().is_server_error());

// Check that the returned version is the version from the configuration
let result: JsonWrapper<Value> = test::read_body_json(resp).await;
assert_eq!(result.code, 500);
}
}
110 changes: 108 additions & 2 deletions keylime-agent/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2022 Keylime Authors

use crate::{permissions, tpm};
use crate::{api::SUPPORTED_API_VERSIONS, permissions, tpm};
use config::{
builder::DefaultState, Config, ConfigBuilder, ConfigError, Environment,
File, FileFormat, Map, Source, Value, ValueKind::Table,
Expand All @@ -25,6 +25,7 @@ use thiserror::Error;
use uuid::Uuid;

pub static CONFIG_VERSION: &str = "2.0";
pub static DEFAULT_API_VERSIONS: &str = "default";
pub static DEFAULT_UUID: &str = "d432fbb3-d2f1-4a97-9ef7-75bd81c00000";
pub static DEFAULT_IP: &str = "127.0.0.1";
pub static DEFAULT_PORT: u32 = 9002;
Expand Down Expand Up @@ -149,6 +150,7 @@ pub enum KeylimeConfigError {
pub(crate) struct AgentConfig {
pub agent_data_path: String,
pub allow_payload_revocation_actions: bool,
pub api_versions: String,
pub contact_ip: String,
pub contact_port: u32,
pub dec_payload_file: String,
Expand Down Expand Up @@ -323,6 +325,7 @@ impl Default for AgentConfig {
agent_data_path: "default".to_string(),
allow_payload_revocation_actions:
DEFAULT_ALLOW_PAYLOAD_REVOCATION_ACTIONS,
api_versions: DEFAULT_API_VERSIONS.to_string(),
contact_ip: DEFAULT_CONTACT_IP.to_string(),
contact_port: DEFAULT_CONTACT_PORT,
dec_payload_file: DEFAULT_DEC_PAYLOAD_FILE.to_string(),
Expand Down Expand Up @@ -597,6 +600,55 @@ fn config_translate_keywords(
}
};

// Parse the configured API versions and check against the list of supported versions
// In case none of the configured API versions are supported, fallback to use all the supported
// versions
// If the "default" keyword is used, use all the supported versions
// If the "latest" keyword is used, use only the latest version
let api_versions: String = match config.agent.api_versions.as_ref() {
"default" => SUPPORTED_API_VERSIONS
.iter()
.map(|&s| s.to_string())
.collect::<Vec<String>>()
.join(", "),
"latest" => {
if let Some(version) =
SUPPORTED_API_VERSIONS.iter().map(|&s| s.to_string()).last()
{
version
} else {
unreachable!();
}
}
versions => {
let parsed: Vec::<String> = match parse_list(&config.agent.api_versions) {
Ok(list) => list
.iter()
.inspect(|e| { if !SUPPORTED_API_VERSIONS.contains(e) {
warn!("Skipping API version \"{e}\" obtained from 'api_versions' configuration option")
}})
.filter(|e| SUPPORTED_API_VERSIONS.contains(e))
.map(|&s| s.into())
.collect(),
Err(e) => {
warn!("Failed to parse list from 'api_versions' configuration option; using default supported versions");
SUPPORTED_API_VERSIONS.iter().map(|&s| s.into()).collect()
}
};

if parsed.is_empty() {
warn!("No supported version found in 'api_versions' configuration option; using default supported versions");
SUPPORTED_API_VERSIONS
.iter()
.map(|&s| s.to_string())
.collect::<Vec<String>>()
.join(", ")
} else {
parsed.join(", ")
}
}
};

// Validate the configuration

// If revocation notifications is enabled, verify all the required options for revocation
Expand Down Expand Up @@ -643,14 +695,15 @@ fn config_translate_keywords(

Ok(KeylimeConfig {
agent: AgentConfig {
keylime_dir: keylime_dir.display().to_string(),
agent_data_path,
api_versions,
contact_ip,
ek_handle,
iak_cert,
idevid_cert,
ima_ml_path,
ip,
keylime_dir: keylime_dir.display().to_string(),
measuredboot_ml_path,
registrar_ip,
revocation_cert,
Expand Down Expand Up @@ -891,6 +944,58 @@ mod tests {
assert!(result.is_ok());
}

#[test]
fn test_translate_api_versions_latest_keyword() {
let mut test_config = KeylimeConfig {
agent: AgentConfig {
api_versions: "latest".to_string(),
..Default::default()
},
};
let result = config_translate_keywords(&test_config);
assert!(result.is_ok());
let config = result.unwrap(); //#[allow_ci]
let version = config.agent.api_versions;
let expected = SUPPORTED_API_VERSIONS
.iter()
.map(|e| e.to_string())
.last()
.unwrap(); //#[allow_ci]
assert_eq!(version, expected);
}

#[test]
fn test_translate_api_versions_default_keyword() {
let default = KeylimeConfig::default();
let result = config_translate_keywords(&default);
assert!(result.is_ok());
let config = result.unwrap(); //#[allow_ci]
let version = config.agent.api_versions;
let expected = SUPPORTED_API_VERSIONS
.iter()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join(", ");
assert_eq!(version, expected);
}

#[test]
fn test_translate_api_versions_old_supported() {
let old = SUPPORTED_API_VERSIONS[0];

let mut test_config = KeylimeConfig {
agent: AgentConfig {
api_versions: old.to_string(),
..Default::default()
},
};
let result = config_translate_keywords(&test_config);
assert!(result.is_ok());
let config = result.unwrap(); //#[allow_ci]
let version = config.agent.api_versions;
assert_eq!(version, old);
}

#[test]
fn test_get_uuid() {
assert_eq!(get_uuid("hash_ek"), "hash_ek");
Expand Down Expand Up @@ -960,6 +1065,7 @@ mod tests {
let override_map: Map<&str, &str> = Map::from([
("KEYLIME_AGENT_AGENT_DATA_PATH", "override_agent_data_path"),
("KEYLIME_AGENT_ALLOW_PAYLOAD_REVOCATION_ACTIONS", "false"),
("KEYLIME_AGENT_API_VERSIONS", "latest"),
("KEYLIME_AGENT_CONTACT_IP", "override_contact_ip"),
("KEYLIME_AGENT_CONTACT_PORT", "9999"),
(
Expand Down
Loading

0 comments on commit 56a3206

Please sign in to comment.