From 56a32066bb4dc9e9bdedc4d90101e3b3a33694b2 Mon Sep 17 00:00:00 2001 From: Anderson Toshiyuki Sasaki Date: Fri, 1 Nov 2024 11:28:33 +0100 Subject: [PATCH] Provide endpoints under multiple API versions 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: https://github.com/keylime/enhancements/pull/115 Signed-off-by: Anderson Toshiyuki Sasaki --- keylime-agent.conf | 12 ++- keylime-agent/src/api.rs | 130 +++++++++++++++++++++++++-- keylime-agent/src/config.rs | 110 ++++++++++++++++++++++- keylime-agent/src/errors_handler.rs | 31 +++++-- keylime-agent/src/main.rs | 82 ++++++++++------- keylime-agent/src/version_handler.rs | 51 ----------- 6 files changed, 314 insertions(+), 102 deletions(-) delete mode 100644 keylime-agent/src/version_handler.rs diff --git a/keylime-agent.conf b/keylime-agent.conf index d6f3e218..71f6096a 100644 --- a/keylime-agent.conf +++ b/keylime-agent.conf @@ -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. diff --git a/keylime-agent/src/api.rs b/keylime-agent/src/api.rs index 9e0eb402..496ebade 100644 --- a/keylime-agent/src/api.rs +++ b/keylime-agent/src/api.rs @@ -1,14 +1,19 @@ 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 { @@ -16,6 +21,29 @@ pub enum APIError { UnsupportedVersion(String), } +/// This is the handler for the GET request for the API version +pub async fn version( + req: HttpRequest, + quote_data: web::Data>, +) -> 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; @@ -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 { 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())), } } @@ -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] } @@ -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 = + 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 = + 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 = test::read_body_json(resp).await; + assert_eq!(result.code, 500); + } } diff --git a/keylime-agent/src/config.rs b/keylime-agent/src/config.rs index d5209dd5..aa37473c 100644 --- a/keylime-agent/src/config.rs +++ b/keylime-agent/src/config.rs @@ -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, @@ -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; @@ -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, @@ -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(), @@ -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::>() + .join(", "), + "latest" => { + if let Some(version) = + SUPPORTED_API_VERSIONS.iter().map(|&s| s.to_string()).last() + { + version + } else { + unreachable!(); + } + } + versions => { + let parsed: Vec:: = 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::>() + .join(", ") + } else { + parsed.join(", ") + } + } + }; + // Validate the configuration // If revocation notifications is enabled, verify all the required options for revocation @@ -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, @@ -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::>() + .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"); @@ -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"), ( diff --git a/keylime-agent/src/errors_handler.rs b/keylime-agent/src/errors_handler.rs index 0d218af1..19546879 100644 --- a/keylime-agent/src/errors_handler.rs +++ b/keylime-agent/src/errors_handler.rs @@ -1,7 +1,10 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright 2021 Keylime Authors -use crate::common::{APIVersion, JsonWrapper, API_VERSION}; +use crate::{ + common::{APIVersion, JsonWrapper}, + QuoteData, +}; use actix_web::{ body, dev, error::{InternalError, JsonPayloadError, PathError, QueryPayloadError}, @@ -11,24 +14,35 @@ use actix_web::{ }; use log::*; -pub(crate) async fn app_default(req: HttpRequest) -> impl Responder { +pub(crate) async fn app_default( + req: HttpRequest, + quote_data: web::Data>, +) -> impl Responder { let error; let response; let message; + let api_versions = quote_data + .api_versions + .iter() + .map(|v| format!("/{v}")) + .collect::>() + .join(", "); + match req.head().method { http::Method::GET => { error = 400; message = format!( - "Not Implemented: Use /version or /{API_VERSION} interfaces" + "Not Implemented: Use {api_versions} or /version interfaces" ); response = HttpResponse::BadRequest() .json(JsonWrapper::error(error, &message)); } http::Method::POST => { error = 400; - message = - format!("Not Implemented: Use /{API_VERSION} interface"); + message = format!( + "Not Implemented: Use {api_versions} or /version interfaces" + ); response = HttpResponse::BadRequest() .json(JsonWrapper::error(error, &message)); } @@ -127,7 +141,12 @@ mod tests { use serde_json::{json, Value}; async fn test_default(resource: Resource, allow: &str) { - let mut app = test::init_service(App::new().service(resource)).await; + 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).service(resource), + ) + .await; if allow.contains("GET") { let req = test::TestRequest::get().uri("/").to_request(); diff --git a/keylime-agent/src/main.rs b/keylime-agent/src/main.rs index 7d0a4610..336e19d5 100644 --- a/keylime-agent/src/main.rs +++ b/keylime-agent/src/main.rs @@ -45,7 +45,6 @@ mod quotes_handler; mod registrar_agent; mod revocation; mod secure_mount; -mod version_handler; use actix_web::{dev::Service, http, middleware, rt, web, App, HttpServer}; use base64::{engine::general_purpose, Engine as _}; @@ -102,27 +101,28 @@ static NOTFOUND: &[u8] = b"Not Found"; // handle quotes. #[derive(Debug)] pub struct QuoteData<'a> { - tpmcontext: Mutex>, - priv_key: PKey, - pub_key: PKey, + agent_uuid: String, ak_handle: KeyHandle, - payload_tx: mpsc::Sender, - revocation_tx: mpsc::Sender, + allow_payload_revocation_actions: bool, + api_versions: Vec, + enc_alg: keylime::algorithms::EncryptionAlgorithm, + hash_alg: keylime::algorithms::HashAlgorithm, + ima_ml: Mutex, + ima_ml_file: Option>, keys_tx: mpsc::Sender<( keys_handler::KeyMessage, Option>, )>, - hash_alg: keylime::algorithms::HashAlgorithm, - enc_alg: keylime::algorithms::EncryptionAlgorithm, - sign_alg: keylime::algorithms::SignAlgorithm, - agent_uuid: String, - allow_payload_revocation_actions: bool, - secure_size: String, - work_dir: PathBuf, - ima_ml_file: Option>, measuredboot_ml_file: Option>, - ima_ml: Mutex, + payload_tx: mpsc::Sender, + priv_key: PKey, + pub_key: PKey, + revocation_tx: mpsc::Sender, secure_mount: PathBuf, + secure_size: String, + sign_alg: keylime::algorithms::SignAlgorithm, + tpmcontext: Mutex>, + work_dir: PathBuf, } #[actix_web::main] @@ -272,7 +272,16 @@ async fn main() -> Result<()> { info!("Running the service as {}...", user_group); } - info!("Starting server with API version {}...", API_VERSION); + // Parse the configured API versions + let api_versions = parse_list(&config.agent.api_versions)? + .iter() + .map(|s| s.to_string()) + .collect::>(); + + info!( + "Starting server with API versions: {}", + &config.agent.api_versions + ); let mut ctx = tpm::Context::new()?; @@ -736,24 +745,25 @@ async fn main() -> Result<()> { .map_err(Error::from); let quotedata = web::Data::new(QuoteData { - tpmcontext: Mutex::new(ctx), - priv_key: nk_priv, - pub_key: nk_pub, + agent_uuid: agent_uuid.clone(), ak_handle, + allow_payload_revocation_actions, + api_versions: api_versions.clone(), + enc_alg: tpm_encryption_alg, + hash_alg: tpm_hash_alg, + ima_ml: Mutex::new(MeasurementList::new()), + ima_ml_file, keys_tx: keys_tx.clone(), + measuredboot_ml_file, payload_tx: payload_tx.clone(), + priv_key: nk_priv, + pub_key: nk_pub, revocation_tx: revocation_tx.clone(), - hash_alg: tpm_hash_alg, - enc_alg: tpm_encryption_alg, - sign_alg: tpm_signing_alg, - agent_uuid: agent_uuid.clone(), - allow_payload_revocation_actions, + secure_mount: PathBuf::from(&mount), secure_size, + sign_alg: tpm_signing_alg, + tpmcontext: Mutex::new(ctx), work_dir, - ima_ml_file, - measuredboot_ml_file, - ima_ml: Mutex::new(MeasurementList::new()), - secure_mount: PathBuf::from(&mount), }); let actix_server = HttpServer::new(move || { @@ -788,17 +798,14 @@ async fn main() -> Result<()> { .error_handler(errors_handler::path_parser_error), ); - let enabled_api_versions = api::SUPPORTED_API_VERSIONS; - - for version in enabled_api_versions { + for version in &api_versions { // This should never fail, thus unwrap should never panic let scope = api::get_api_scope(version).unwrap(); //#[allow_ci] app = app.service(scope); } app.service( - web::resource("/version") - .route(web::get().to(version_handler::version)), + web::resource("/version").route(web::get().to(api::version)), ) .service( web::resource(r"/v{major:\d+}.{minor:\d+}{tail}*") @@ -949,6 +956,7 @@ fn read_in_file(path: String) -> std::io::Result { } #[cfg(feature = "testing")] +#[cfg(test)] mod testing { use super::*; use crate::{config::KeylimeConfig, crypto::CryptoError}; @@ -1077,7 +1085,7 @@ mod testing { // Allow setting the binary bios measurements log path when testing let mut measuredboot_ml_path = Path::new(&test_config.agent.measuredboot_ml_path); - let env_mb_path; + let env_mb_path: String; #[cfg(feature = "testing")] if let Ok(v) = std::env::var("TPM_BINARY_MEASUREMENTS") { env_mb_path = v; @@ -1090,8 +1098,14 @@ mod testing { Err(err) => None, }; + let api_versions = api::SUPPORTED_API_VERSIONS + .iter() + .map(|&s| s.to_string()) + .collect::>(); + Ok(( QuoteData { + api_versions, tpmcontext: Mutex::new(ctx), priv_key: nk_priv, pub_key: nk_pub, diff --git a/keylime-agent/src/version_handler.rs b/keylime-agent/src/version_handler.rs deleted file mode 100644 index cafec030..00000000 --- a/keylime-agent/src/version_handler.rs +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2022 Keylime Authors - -use crate::common::{JsonWrapper, API_VERSION}; -use actix_web::{web, HttpRequest, HttpResponse, Responder}; -use log::*; -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, Debug)] -struct KeylimeVersion { - supported_version: String, -} - -// This is the handler for the GET request for the API version -pub async fn version(req: HttpRequest) -> impl Responder { - info!( - "GET invoked from {:?} with uri {}", - req.connection_info().peer_addr().unwrap(), //#[allow_ci] - req.uri() - ); - - let response = JsonWrapper::success(KeylimeVersion { - supported_version: API_VERSION[1..].to_string(), - }); - - HttpResponse::Ok().json(response) -} - -#[cfg(feature = "testing")] -#[cfg(test)] -mod tests { - use super::*; - use actix_web::{test, web, App}; - - #[actix_rt::test] - async fn test_version() { - let mut app = test::init_service( - App::new().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 = - test::read_body_json(resp).await; - assert_eq!(body.results.supported_version, API_VERSION[1..]); - } -}