From 9db52f0e1e2c8eacb7b208d4a567b5fe55e6f71f Mon Sep 17 00:00:00 2001 From: Anderson Toshiyuki Sasaki Date: Wed, 20 Nov 2024 11:34:39 +0100 Subject: [PATCH 1/2] Use a single TPM context and avoid race conditions during tests This implements a singleton to use a single instance of the tss_espi::Context. This also avoids race conditions by using a thread-safe mutex to access the context. The tests were modified to cleanup generated key handles between test cases, preventing leftover handles to fill the TPM memory. Introduce a mutex to allow only a single test that create keys in the TPM to run at once. It is required that tests that create keys in the TPM to run the tpm::testing::lock_tests() to obtain the mutex and be able to continue executing without the risk of other tests filling the TPM memory. The QuoteData::fixture implementation (used only during tests) was modified to flush the created key contexts when dropped. It also was modified to lock the global mutex by calling the tpm::testing::lock_tests() and provide the respective guard, preventing other tests that use the fixture (which necessarily create TPM keys) to run in parallel. Note that it is necessary to drop the QuoteData explicitly to guarantee that the keys are flushed before releasing the global mutex. Finally, the following changes were made to the tests/run.sh: * Do not use tpm2-abrmd anymore, and use only the swtpm socket instead * Stop the started processes at exit With all these changes, the tests/run.sh can now be executed locally without any special setting. Signed-off-by: Anderson Toshiyuki Sasaki --- Cargo.lock | 1 + keylime-agent/src/agent_handler.rs | 7 +- keylime-agent/src/common.rs | 21 +- keylime-agent/src/keys_handler.rs | 19 +- keylime-agent/src/main.rs | 112 ++-- keylime-agent/src/notifications_handler.rs | 7 +- keylime-agent/src/quotes_handler.rs | 44 +- keylime/Cargo.toml | 1 + keylime/src/tpm.rs | 663 +++++++++++++-------- tests/run.sh | 74 ++- 10 files changed, 606 insertions(+), 343 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 30785643..1bd17587 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1192,6 +1192,7 @@ dependencies = [ "static_assertions", "tempfile", "thiserror", + "tokio", "tss-esapi", ] diff --git a/keylime-agent/src/agent_handler.rs b/keylime-agent/src/agent_handler.rs index d75f3fa9..c710ec83 100644 --- a/keylime-agent/src/agent_handler.rs +++ b/keylime-agent/src/agent_handler.rs @@ -21,7 +21,7 @@ pub(crate) struct AgentInfo { // It should return a AgentInfo object as JSON async fn info( req: HttpRequest, - data: web::Data, + data: web::Data>, ) -> impl Responder { debug!("Returning agent information"); @@ -87,7 +87,7 @@ mod tests { #[actix_rt::test] async fn test_agent_info() { - let mut quotedata = QuoteData::fixture().unwrap(); //#[allow_ci] + let (mut quotedata, mutex) = QuoteData::fixture().await.unwrap(); //#[allow_ci] quotedata.hash_alg = keylime::algorithms::HashAlgorithm::Sha256; quotedata.enc_alg = keylime::algorithms::EncryptionAlgorithm::Rsa; quotedata.sign_alg = keylime::algorithms::SignAlgorithm::RsaSsa; @@ -112,6 +112,9 @@ mod tests { assert_eq!(result.results.tpm_hash_alg.as_str(), "sha256"); assert_eq!(result.results.tpm_enc_alg.as_str(), "rsa"); assert_eq!(result.results.tpm_sign_alg.as_str(), "rsassa"); + + // Explicitly drop QuoteData to cleanup keys + drop(data); } #[actix_rt::test] diff --git a/keylime-agent/src/common.rs b/keylime-agent/src/common.rs index a6cb0702..8afb6f52 100644 --- a/keylime-agent/src/common.rs +++ b/keylime-agent/src/common.rs @@ -274,9 +274,10 @@ mod tests { Context, }; + #[tokio::test] #[cfg(feature = "testing")] - #[test] - fn test_agent_data() -> Result<()> { + async fn test_agent_data() -> Result<()> { + let _mutex = tpm::testing::lock_tests().await; let mut config = KeylimeConfig::default(); let mut ctx = tpm::Context::new()?; @@ -319,13 +320,21 @@ mod tests { tpm_signing_alg, ek_hash.as_bytes(), ); + assert!(valid); + + // Cleanup created keys + let ak_handle = ctx.load_ak(ek_result.key_handle, &ak)?; + ctx.flush_context(ak_handle.into()); + ctx.flush_context(ek_result.key_handle.into()); + Ok(()) } + #[tokio::test] #[cfg(feature = "testing")] - #[test] - fn test_hash() -> Result<()> { + async fn test_hash() -> Result<()> { + let _mutex = tpm::testing::lock_tests().await; let mut config = KeylimeConfig::default(); let mut ctx = tpm::Context::new()?; @@ -342,6 +351,10 @@ mod tests { let result = hash_ek_pubkey(ek_result.public); assert!(result.is_ok()); + + // Cleanup created keys + ctx.flush_context(ek_result.key_handle.into()); + Ok(()) } } diff --git a/keylime-agent/src/keys_handler.rs b/keylime-agent/src/keys_handler.rs index da241616..a5217f29 100644 --- a/keylime-agent/src/keys_handler.rs +++ b/keylime-agent/src/keys_handler.rs @@ -133,7 +133,7 @@ fn try_combine_keys( async fn u_key( body: web::Json, req: HttpRequest, - quote_data: web::Data, + quote_data: web::Data>, ) -> impl Responder { debug!("Received ukey"); @@ -247,7 +247,7 @@ async fn u_key( async fn v_key( body: web::Json, req: HttpRequest, - quote_data: web::Data, + quote_data: web::Data>, ) -> impl Responder { debug!("Received vkey"); @@ -317,7 +317,7 @@ async fn v_key( async fn pubkey( req: HttpRequest, - data: web::Data, + data: web::Data>, ) -> impl Responder { match crypto::pkey_pub_to_pem(&data.pub_key) { Ok(pubkey) => { @@ -367,7 +367,7 @@ async fn get_symm_key( async fn verify( param: web::Query, req: HttpRequest, - data: web::Data, + data: web::Data>, ) -> impl Responder { if param.challenge.is_empty() { warn!( @@ -875,7 +875,7 @@ mod tests { #[cfg(feature = "testing")] async fn test_u_or_v_key(key_len: usize, payload: Option<&[u8]>) { let test_config = KeylimeConfig::default(); - let mut fixture = QuoteData::fixture().unwrap(); //#[allow_ci] + let (mut fixture, mutex) = QuoteData::fixture().await.unwrap(); //#[allow_ci] // Create temporary working directory and secure mount let temp_workdir = tempfile::tempdir().unwrap(); //#[allow_ci] @@ -1073,6 +1073,9 @@ mod tests { keys_tx.send((KeyMessage::Shutdown, None)).await.unwrap(); //#[allow_ci] payload_tx.send(PayloadMessage::Shutdown).await.unwrap(); //#[allow_ci] arbiter.join(); + + // Explicitly drop QuoteData to cleanup keys + drop(quotedata); } #[cfg(feature = "testing")] @@ -1090,7 +1093,8 @@ mod tests { #[cfg(feature = "testing")] #[actix_rt::test] async fn test_pubkey() { - let quotedata = web::Data::new(QuoteData::fixture().unwrap()); //#[allow_ci] + 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.clone()).route( &format!("/{API_VERSION}/keys/pubkey"), @@ -1110,5 +1114,8 @@ mod tests { assert!(pkey_pub_from_pem(&result.results.pubkey) .unwrap() //#[allow_ci] .public_eq("edata.pub_key)); + + // Explicitly drop QuoteData to cleanup keys + drop(quotedata); } } diff --git a/keylime-agent/src/main.rs b/keylime-agent/src/main.rs index ce8b258e..78646c3e 100644 --- a/keylime-agent/src/main.rs +++ b/keylime-agent/src/main.rs @@ -98,8 +98,8 @@ static NOTFOUND: &[u8] = b"Not Found"; // This data is passed in to the actix httpserver threads that // handle quotes. #[derive(Debug)] -pub struct QuoteData { - tpmcontext: Mutex, +pub struct QuoteData<'a> { + tpmcontext: Mutex>, priv_key: PKey, pub_key: PKey, ak_handle: KeyHandle, @@ -273,14 +273,6 @@ async fn main() -> Result<()> { let mut ctx = tpm::Context::new()?; - // Retrieve the TPM Vendor, this allows us to warn if someone is using a - // Software TPM ("SW") - if tss_esapi::utils::get_tpm_vendor(ctx.as_mut())?.contains("SW") { - warn!("INSECURE: Keylime is currently using a software TPM emulator rather than a real hardware TPM."); - warn!("INSECURE: The security of Keylime is NOT linked to a hardware root of trust."); - warn!("INSECURE: Only use Keylime in this mode for testing or debugging purposes."); - } - cfg_if::cfg_if! { if #[cfg(feature = "legacy-python-actions")] { warn!("The support for legacy python revocation actions is deprecated and will be removed on next major release"); @@ -313,7 +305,7 @@ async fn main() -> Result<()> { } else { Auth::try_from(tpm_ownerpassword.as_bytes())? }; - ctx.as_mut().tr_set_auth(Hierarchy::Endorsement.into(), auth) + ctx.tr_set_auth(Hierarchy::Endorsement.into(), auth) .map_err(|e| { Error::Configuration(config::KeylimeConfigError::Generic(format!( "Failed to set TPM context password for Endorsement Hierarchy: {e}" @@ -406,7 +398,7 @@ async fn main() -> Result<()> { /// If handle is not set in config, recreate IDevID according to template info!("Recreating IDevID."); let regen_idev = ctx.create_idevid(asym_alg, name_alg)?; - ctx.as_mut().flush_context(regen_idev.handle.into())?; + ctx.flush_context(regen_idev.handle.into())?; // Flush after creating to make room for AK and EK and IAK regen_idev } else { @@ -780,7 +772,7 @@ async fn main() -> Result<()> { )?; // Flush EK if we created it if config.agent.ek_handle.is_empty() { - ctx.as_mut().flush_context(ek_result.key_handle.into())?; + ctx.flush_context(ek_result.key_handle.into())?; } let mackey = general_purpose::STANDARD.encode(key.value()); let auth_tag = @@ -1062,6 +1054,11 @@ mod testing { use crate::{config::KeylimeConfig, crypto::CryptoError}; use thiserror::Error; + use std::sync::{Arc, Mutex, OnceLock}; + use tokio::sync::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; + + use keylime::tpm::testing::lock_tests; + #[derive(Error, Debug)] pub(crate) enum MainTestError { /// Algorithm error @@ -1093,8 +1090,22 @@ mod testing { TSSError(#[from] tss_esapi::Error), } - impl QuoteData { - pub(crate) fn fixture() -> std::result::Result { + impl<'a> Drop for QuoteData<'a> { + /// Flush the created AK when dropping + fn drop(&mut self) { + self.tpmcontext + .lock() + .unwrap() //#[allow_ci] + .flush_context(self.ak_handle.into()); + } + } + + impl<'a> QuoteData<'a> { + pub(crate) async fn fixture() -> std::result::Result< + (Self, AsyncMutexGuard<'static, ()>), + MainTestError, + > { + let mutex = lock_tests().await; let test_config = KeylimeConfig::default(); let mut ctx = tpm::Context::new()?; @@ -1103,9 +1114,6 @@ mod testing { test_config.agent.tpm_encryption_alg.as_str(), )?; - // Gather EK and AK key values and certs - let ek_result = ctx.create_ek(tpm_encryption_alg, None)?; - let tpm_hash_alg = keylime::algorithms::HashAlgorithm::try_from( test_config.agent.tpm_hash_alg.as_str(), )?; @@ -1115,14 +1123,19 @@ mod testing { test_config.agent.tpm_signing_alg.as_str(), )?; - let ak_result = ctx.create_ak( - ek_result.key_handle, - tpm_hash_alg, - tpm_signing_alg, - )?; - let ak_handle = ctx.load_ak(ek_result.key_handle, &ak_result)?; - let ak_tpm2b_pub = - PublicBuffer::try_from(ak_result.public)?.marshall()?; + // Gather EK and AK key values and certs + let ek_result = ctx.create_ek(tpm_encryption_alg, None).unwrap(); //#[allow_ci] + let ak_result = ctx + .create_ak( + ek_result.key_handle, + tpm_hash_alg, + tpm_signing_alg, + ) + .unwrap(); //#[allow_ci] + let ak_handle = + ctx.load_ak(ek_result.key_handle, &ak_result).unwrap(); //#[allow_ci] + + ctx.flush_context(ek_result.key_handle.into()).unwrap(); //#[allow_ci] let rsa_key_path = Path::new(env!("CARGO_MANIFEST_DIR")) .join("test-data") @@ -1177,28 +1190,31 @@ mod testing { Err(err) => None, }; - Ok(QuoteData { - tpmcontext: Mutex::new(ctx), - priv_key: nk_priv, - pub_key: nk_pub, - ak_handle, - keys_tx, - payload_tx, - revocation_tx, - hash_alg: keylime::algorithms::HashAlgorithm::Sha256, - enc_alg: keylime::algorithms::EncryptionAlgorithm::Rsa, - sign_alg: keylime::algorithms::SignAlgorithm::RsaSsa, - agent_uuid: test_config.agent.uuid, - allow_payload_revocation_actions: test_config - .agent - .allow_payload_revocation_actions, - secure_size: test_config.agent.secure_size, - work_dir, - ima_ml_file, - measuredboot_ml_file, - ima_ml: Mutex::new(MeasurementList::new()), - secure_mount, - }) + Ok(( + QuoteData { + tpmcontext: Mutex::new(ctx), + priv_key: nk_priv, + pub_key: nk_pub, + ak_handle, + keys_tx, + payload_tx, + revocation_tx, + hash_alg: keylime::algorithms::HashAlgorithm::Sha256, + enc_alg: keylime::algorithms::EncryptionAlgorithm::Rsa, + sign_alg: keylime::algorithms::SignAlgorithm::RsaSsa, + agent_uuid: test_config.agent.uuid, + allow_payload_revocation_actions: test_config + .agent + .allow_payload_revocation_actions, + secure_size: test_config.agent.secure_size, + work_dir, + ima_ml_file, + measuredboot_ml_file, + ima_ml: Mutex::new(MeasurementList::new()), + secure_mount, + }, + mutex, + )) } } } diff --git a/keylime-agent/src/notifications_handler.rs b/keylime-agent/src/notifications_handler.rs index 0eeeae8f..953af3da 100644 --- a/keylime-agent/src/notifications_handler.rs +++ b/keylime-agent/src/notifications_handler.rs @@ -15,7 +15,7 @@ use std::path::{Path, PathBuf}; async fn revocation( body: web::Json, req: HttpRequest, - data: web::Data, + data: web::Data>, ) -> impl Responder { info!("Received revocation"); @@ -138,7 +138,7 @@ mod tests { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/actions"), ); - let mut fixture = QuoteData::fixture().unwrap(); //#[allow_ci] + let (mut fixture, mutex) = QuoteData::fixture().await.unwrap(); //#[allow_ci] // Replace the channels on the fixture with some local ones let (mut revocation_tx, mut revocation_rx) = @@ -188,5 +188,8 @@ mod tests { let resp = test::call_service(&app, req).await; assert!(resp.status().is_success()); + + // Explicitly drop QuoteData to cleanup keys + drop(quotedata); } } diff --git a/keylime-agent/src/quotes_handler.rs b/keylime-agent/src/quotes_handler.rs index ce72592e..cebd0d25 100644 --- a/keylime-agent/src/quotes_handler.rs +++ b/keylime-agent/src/quotes_handler.rs @@ -50,7 +50,7 @@ pub(crate) struct KeylimeQuote { async fn identity( req: HttpRequest, param: web::Query, - data: web::Data, + data: web::Data>, ) -> impl Responder { // nonce can only be in alphanumerical format if !param.nonce.chars().all(char::is_alphanumeric) { @@ -139,7 +139,7 @@ async fn identity( async fn integrity( req: HttpRequest, param: web::Query, - data: web::Data, + data: web::Data>, ) -> impl Responder { // nonce, mask can only be in alphanumerical format if !param.nonce.chars().all(char::is_alphanumeric) { @@ -387,7 +387,8 @@ mod tests { #[actix_rt::test] async fn test_identity() { - let quotedata = web::Data::new(QuoteData::fixture().unwrap()); //#[allow_ci] + 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.clone()).route( &format!("/{API_VERSION}/quotes/identity"), @@ -418,17 +419,22 @@ mod tests { let mut context = quotedata.tpmcontext.lock().unwrap(); //#[allow_ci] tpm::testing::check_quote( - context.as_mut(), + &mut context, quotedata.ak_handle, &result.results.quote, b"1234567890ABCDEFHIJ", ) .expect("unable to verify quote"); + + // Explicitly drop QuoteData to cleanup keys + drop(context); + drop(quotedata); } #[actix_rt::test] async fn test_integrity_pre() { - let quotedata = web::Data::new(QuoteData::fixture().unwrap()); //#[allow_ci] + 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.clone()).route( &format!("/{API_VERSION}/quotes/integrity"), @@ -470,7 +476,7 @@ mod tests { let mut context = quotedata.tpmcontext.lock().unwrap(); //#[allow_ci] tpm::testing::check_quote( - context.as_mut(), + &mut context, quotedata.ak_handle, &result.results.quote, b"1234567890ABCDEFHIJ", @@ -482,11 +488,15 @@ mod tests { } else { panic!("IMA file was None"); //#[allow_ci] } + + // Explicitly drop QuoteData to cleanup keys + drop(quotedata); } #[actix_rt::test] async fn test_integrity_post() { - let quotedata = web::Data::new(QuoteData::fixture().unwrap()); //#[allow_ci] + 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.clone()).route( &format!("/{API_VERSION}/quotes/integrity"), @@ -529,22 +539,27 @@ mod tests { let mut context = quotedata.tpmcontext.lock().unwrap(); //#[allow_ci] tpm::testing::check_quote( - context.as_mut(), + &mut context, quotedata.ak_handle, &result.results.quote, b"1234567890ABCDEFHIJ", ) .expect("unable to verify quote"); + + // Explicitly drop QuoteData to cleanup keys + drop(context); + drop(quotedata); } #[actix_rt::test] async fn test_missing_ima_file() { - let mut quotedata = QuoteData::fixture().unwrap(); //#[allow_ci] - // Remove the IMA log file from the context - quotedata.ima_ml_file = None; - let data = web::Data::new(quotedata); + let (mut fixture, mutex) = QuoteData::fixture().await.unwrap(); //#[allow_ci] + + // Remove the IMA log file from the context + fixture.ima_ml_file = None; + let quotedata = web::Data::new(fixture); let mut app = - test::init_service(App::new().app_data(data.clone()).route( + test::init_service(App::new().app_data(quotedata.clone()).route( &format!("/{API_VERSION}/quotes/integrity"), web::get().to(integrity), )) @@ -563,6 +578,9 @@ mod tests { test::read_body_json(resp).await; assert!(result.results.ima_measurement_list.is_none()); assert!(result.results.ima_measurement_list_entry.is_none()); + + // Explicitly drop QuoteData to cleanup keys + drop(quotedata); } #[actix_rt::test] diff --git a/keylime/Cargo.toml b/keylime/Cargo.toml index 97cd5bf3..97e8e47c 100644 --- a/keylime/Cargo.toml +++ b/keylime/Cargo.toml @@ -21,6 +21,7 @@ thiserror.workspace = true tss-esapi.workspace = true picky-asn1-der.workspace = true picky-asn1-x509.workspace = true +tokio.workspace = true [dev-dependencies] tempfile.workspace = true diff --git a/keylime/src/tpm.rs b/keylime/src/tpm.rs index 753f96a3..dafddf02 100644 --- a/keylime/src/tpm.rs +++ b/keylime/src/tpm.rs @@ -6,9 +6,12 @@ use crate::algorithms::{ }; use base64::{engine::general_purpose, Engine as _}; use log::*; -use std::convert::{TryFrom, TryInto}; -use std::io::Read; -use std::str::FromStr; +use std::{ + convert::{TryFrom, TryInto}, + io::Read, + str::FromStr, + sync::{Arc, Mutex, OnceLock}, +}; use thiserror::Error; use openssl::{ @@ -33,7 +36,7 @@ use tss_esapi::{ }, handles::{ AuthHandle, KeyHandle, ObjectHandle, PcrHandle, PersistentTpmHandle, - TpmHandle, + SessionHandle, TpmHandle, }, interface_types::{ algorithm::{AsymmetricAlgorithm, HashingAlgorithm, PublicAlgorithm}, @@ -50,7 +53,7 @@ use tss_esapi::{ PcrSelectionListBuilder, PcrSlot, PublicBuilder, PublicEccParametersBuilder, PublicKeyRsa, PublicRsaParametersBuilder, RsaExponent, RsaScheme, Signature, SignatureScheme, - SymmetricDefinitionObject, + SymmetricDefinitionObject, Ticket, VerifiedTicket, }, tcti_ldr::TctiNameConf, traits::Marshall, @@ -127,6 +130,10 @@ pub enum TpmError { #[error("Error loading AK object")] TSSLoadAKError { source: tss_esapi::Error }, + /// Error flushing object handle + #[error("Error flushing object handle")] + TSSFlushContext { source: tss_esapi::Error }, + /// Error creating new persistent TPM handle #[error("Error creating handle for persistent TPM object in {handle}")] TSSNewPersistentHandleError { @@ -220,6 +227,10 @@ pub enum TpmError { #[error("Error setting authentication session attributes")] TSSSessionSetAttributesError { source: tss_esapi::Error }, + /// Error setting authentication to object handle + #[error("Error setting authentication to object handle")] + TSSTrSetAuth { source: tss_esapi::Error }, + /// Error converting to TSS Digest from digest value #[error("Error converting to TSS Digest from digest value")] TSSDigestFromValue { source: tss_esapi::Error }, @@ -244,6 +255,10 @@ pub enum TpmError { #[error("Error generating quote")] TSSQuoteError { source: tss_esapi::Error }, + /// Error verifying signature + #[error("Error verifying signature")] + TSSVerifySign { source: tss_esapi::Error }, + /// Unexpected attested type in quote #[error("Unexpected attested type in quote: expected {expected:?} got {got:?}")] UnexpectedAttestedType { @@ -453,23 +468,13 @@ pub struct IAKPublic { /// Wrapper around tss_esapi::Context. #[derive(Debug)] -pub struct Context { - inner: tss_esapi::Context, -} - -impl AsRef for Context { - fn as_ref(&self) -> &tss_esapi::Context { - &self.inner - } +pub struct Context<'a> { + inner: &'a Arc>, } -impl AsMut for Context { - fn as_mut(&mut self) -> &mut tss_esapi::Context { - &mut self.inner - } -} +static TPM_CTX: OnceLock>> = OnceLock::new(); -impl Context { +impl<'a> Context<'a> { /// Creates a connection context. pub fn new() -> Result { let tcti_path = match std::env::var("TCTI") { @@ -488,11 +493,28 @@ impl Context { source: error, } })?; - Ok(Self { - inner: tss_esapi::Context::new(tcti).map_err(|error| { - TpmError::TSSTctiContextError { source: error } - })?, - }) + + let ctx = TPM_CTX.get_or_init(|| + { + let mut tpmctx = tss_esapi::Context::new(tcti).map_err(|error| { + TpmError::TSSTctiContextError { source: error }}).expect("Failed to create TPM context"); + + // Retrieve the TPM Vendor, this allows us to warn if someone is using a + // Software TPM ("SW") + if tss_esapi::utils::get_tpm_vendor(&mut tpmctx).unwrap().contains("SW") { //#[allow_ci] + warn!("INSECURE: Keylime is currently using a software TPM emulator rather than a real hardware TPM."); + warn!("INSECURE: The security of Keylime is NOT linked to a hardware root of trust."); + warn!("INSECURE: Only use Keylime in this mode for testing or debugging purposes."); + } + + Arc::new(Mutex::new(tpmctx)) + }); + + Ok(Self { inner: ctx }) + } + + pub fn inner(self) -> Arc> { + Arc::clone(self.inner) } // Tries to parse the EK certificate and re-encodes it to remove potential padding @@ -519,16 +541,16 @@ impl Context { alg: EncryptionAlgorithm, handle: Option<&str>, ) -> Result { + let mut ctx = self.inner.lock().unwrap(); //#[allow_ci] + // Retrieve EK handle, EK pub cert, and TPM pub object - let key_handle = match handle { + let key_handle: KeyHandle = match handle { Some(v) => { if v.is_empty() { - ek::create_ek_object( - &mut self.inner, - alg.into(), - DefaultKey, - ) - .map_err(|source| TpmError::TSSCreateEKError { source })? + ek::create_ek_object(&mut ctx, alg.into(), DefaultKey) + .map_err(|source| TpmError::TSSCreateEKError { + source, + })? } else { let handle = u32::from_str_radix(v.trim_start_matches("0x"), 16) @@ -536,33 +558,29 @@ impl Context { origin: v.to_string(), source, })?; - self.inner - .tr_from_tpm_public(TpmHandle::Persistent( - PersistentTpmHandle::new(handle).map_err( - |source| { - TpmError::TSSNewPersistentHandleError { - handle: v.to_string(), - source, - } - }, - )?, - )) - .map_err(|source| { - TpmError::TSSHandleFromPersistentHandleError { + + ctx.tr_from_tpm_public(TpmHandle::Persistent( + PersistentTpmHandle::new(handle).map_err( + |source| TpmError::TSSNewPersistentHandleError { handle: v.to_string(), source, - } - })? - .into() + }, + )?, + )) + .map_err(|source| { + TpmError::TSSHandleFromPersistentHandleError { + handle: v.to_string(), + source, + } + })? + .into() } } - None => { - ek::create_ek_object(&mut self.inner, alg.into(), DefaultKey) - .map_err(|source| TpmError::TSSCreateEKError { source })? - } + None => ek::create_ek_object(&mut ctx, alg.into(), DefaultKey) + .map_err(|source| TpmError::TSSCreateEKError { source })?, }; - let cert = match ek::retrieve_ek_pubcert(&mut self.inner, alg.into()) - { + + let cert = match ek::retrieve_ek_pubcert(&mut ctx, alg.into()) { Ok(cert) => match self.check_ek_cert(&cert) { Ok(cert_checked) => Some(cert_checked), Err(_) => { @@ -575,8 +593,8 @@ impl Context { None } }; - let (tpm_pub, _, _) = self - .inner + + let (tpm_pub, _, _) = ctx .read_public(key_handle) .map_err(|source| TpmError::TSSReadPublicError { source })?; Ok(EKResult { @@ -602,7 +620,7 @@ impl Context { sign_alg: SignAlgorithm, ) -> Result { let ak = ak::create_ak( - &mut self.inner, + &mut self.inner.lock().unwrap(), //#[allow_ci] handle, hash_alg.into(), sign_alg.into(), @@ -632,7 +650,7 @@ impl Context { ak: &AKResult, ) -> Result { let ak_handle = ak::load_ak( - &mut self.inner, + &mut self.inner.lock().unwrap(), //#[allow_ci] handle, None, ak.private.clone(), @@ -656,13 +674,13 @@ impl Context { handle: &str, password: &str, ) -> Result { + let mut ctx = self.inner.lock().unwrap(); //#[allow_ci] let handle = u32::from_str_radix(handle.trim_start_matches("0x"), 16) .map_err(|source| TpmError::NumParse { origin: handle.to_string(), source, })?; - let key_handle: KeyHandle = self - .inner + let key_handle: KeyHandle = ctx .tr_from_tpm_public(TpmHandle::Persistent( PersistentTpmHandle::new(handle).map_err(|source| { TpmError::TSSNewPersistentHandleError { @@ -690,12 +708,12 @@ impl Context { } else { Auth::try_from(password.as_bytes())? }; - self.as_mut().tr_set_auth(key_handle.into(), auth).map_err( - |source| TpmError::TSSHandleSetAuthError { + ctx.tr_set_auth(key_handle.into(), auth).map_err(|source| { + TpmError::TSSHandleSetAuthError { handle: handle.to_string(), source, - }, - )?; + } + })?; }; Ok(key_handle) @@ -710,6 +728,8 @@ impl Context { let idevid_handle = self.get_key_handle(handle, password)?; let (idevid_pub, _, _) = self .inner + .lock() + .unwrap() //#[allow_ci] .read_public(idevid_handle) .map_err(|source| TpmError::TSSReadPublicError { source })?; Ok(IDevIDResult { @@ -727,6 +747,8 @@ impl Context { let iak_handle = self.get_key_handle(handle, password)?; let (iak_pub, _, _) = self .inner + .lock() + .unwrap() //#[allow_ci] .read_public(iak_handle) .map_err(|source| TpmError::TSSReadPublicError { source })?; Ok(IAKResult { @@ -766,6 +788,8 @@ impl Context { let primary_key = self .inner + .lock() + .unwrap() //#[allow_ci] .execute_with_nullauth_session(|ctx| { ctx.create_primary( Hierarchy::Endorsement, @@ -981,6 +1005,8 @@ impl Context { let primary_key = self .inner + .lock() + .unwrap() //#[allow_ci] .execute_with_nullauth_session(|ctx| { ctx.create_primary( Hierarchy::Endorsement, @@ -1169,8 +1195,8 @@ impl Context { &mut self, ses_type: SessionType, ) -> Result { - let Some(session) = self - .inner + let mut ctx = self.inner.lock().unwrap(); //#[allow_ci] + let Some(session) = ctx .start_auth_session( None, None, @@ -1193,8 +1219,7 @@ impl Context { .with_decrypt(true) .build(); - self.inner - .tr_sess_set_attributes(session, ses_attrs, ses_attrs_mask) + ctx.tr_sess_set_attributes(session, ses_attrs, ses_attrs_mask) .map_err(|source| TpmError::TSSSessionSetAttributesError { source, })?; @@ -1212,8 +1237,10 @@ impl Context { let ek_auth = self.create_empty_session(SessionType::Policy)?; + let mut ctx = self.inner.lock().unwrap(); //#[allow_ci] + // We authorize ses2 with PolicySecret(ENDORSEMENT) as per PolicyA - let _ = self.inner.execute_with_nullauth_session(|context| { + let _ = ctx.execute_with_nullauth_session(|context| { context.policy_secret( ek_auth.try_into()?, AuthHandle::Endorsement, @@ -1224,14 +1251,20 @@ impl Context { ) })?; - self.inner + let result = ctx .execute_with_sessions( (Some(AuthSession::Password), Some(ek_auth), None), |context| { context.activate_credential(ak, ek, credential, secret) }, ) - .map_err(TpmError::from) + .map_err(TpmError::from); + + // Clear sessions after use + ctx.flush_context(SessionHandle::from(ek_auth).into())?; + ctx.clear_sessions(); + + result } /// This function certifies an attestation key with the IAK, using any qualifying data provided, @@ -1242,7 +1275,9 @@ impl Context { ak: KeyHandle, iak: KeyHandle, ) -> Result<(Attest, Signature)> { - self.inner + let mut ctx = self.inner.lock().unwrap(); //#[allow_ci] + + let result = ctx .execute_with_sessions( ( Some(AuthSession::Password), @@ -1258,7 +1293,12 @@ impl Context { ) }, ) - .map_err(TpmError::from) + .map_err(TpmError::from); + + // Clear sessions after use + ctx.clear_sessions(); + + result } /// This function extends PCR#16 with the digest, then creates a PcrList @@ -1270,10 +1310,13 @@ impl Context { hash_alg: HashingAlgorithm, ) -> Result { // extend digest into pcr16 - self.inner.execute_with_nullauth_session(|ctx| { - ctx.pcr_reset(PcrHandle::Pcr16)?; - ctx.pcr_extend(PcrHandle::Pcr16, digest.to_owned()) - })?; + self.inner + .lock() + .unwrap() //#[allow_ci] + .execute_with_nullauth_session(|ctx| { + ctx.pcr_reset(PcrHandle::Pcr16)?; + ctx.pcr_extend(PcrHandle::Pcr16, digest.to_owned()) + })?; // translate mask to vec of pcrs let mut pcrs = read_mask(mask)?; @@ -1311,8 +1354,10 @@ impl Context { let pcrlist = self.build_pcr_list(nk_digest, mask, hash_alg.into())?; - let (attestation, sig, pcrs_read, pcr_data) = - self.inner.execute_with_nullauth_session(|ctx| { + let mut ctx = self.inner.lock().unwrap(); //#[allow_ci] + + let (attestation, sig, pcrs_read, pcr_data) = ctx + .execute_with_nullauth_session(|ctx| { perform_quote_and_pcr_read( ctx, ak_handle, @@ -1329,6 +1374,8 @@ impl Context { /// Get the name of the object pub fn get_name(&mut self, handle: ObjectHandle) -> Result { self.inner + .lock() + .unwrap() //#[allow_ci] .tr_get_name(handle) .map_err(|source| TpmError::TSSGetNameError { source }) } @@ -1351,6 +1398,8 @@ impl Context { ) -> Result> { let (credential, secret) = self .inner + .lock() + .unwrap() //#[allow_ci] .make_credential(ek_handle, credential, name) .map_err(|source| TpmError::TSSMakeCredentialError { source })?; @@ -1375,6 +1424,57 @@ impl Context { Ok(blob) } + + /// Flush object handle context + /// + /// # Arguments: + /// + /// * handle (ObjectHandle): The object handle to flush + pub fn flush_context(&mut self, handle: ObjectHandle) -> Result<()> { + self.inner + .lock() + .unwrap() //#[allow_ci] + .flush_context(handle) + .map_err(|source| TpmError::TSSFlushContext { source }) + } + + /// Set authentication to object handle + /// + /// # Arguments: + /// + /// * handle (ObjectHandle): Object handle + /// * auth (Auth): Authentication to set to the handle + pub fn tr_set_auth( + &mut self, + handle: ObjectHandle, + auth: Auth, + ) -> Result<()> { + self.inner + .lock() + .unwrap() //#[allow_ci] + .tr_set_auth(handle, auth) + .map_err(|source| TpmError::TSSTrSetAuth { source }) + } + + /// Verify signature + /// + /// # Arguments: + /// + /// * key_handle (KeyHandle): The public key handle + /// * digest (Digest): The signed Digest + /// * signature (Signature): The signature to verify + pub fn verify_signature( + &mut self, + key_handle: KeyHandle, + digest: Digest, + signature: Signature, + ) -> Result { + self.inner + .lock() + .unwrap() //#[allow_ci] + .verify_signature(key_handle, digest, signature) + .map_err(|source| TpmError::TSSVerifySign { source }) + } } // Ensure that TPML_PCR_SELECTION and TPML_DIGEST have known sizes @@ -1813,18 +1913,19 @@ pub fn get_idevid_template( pub mod testing { use super::*; - use std::io::prelude::*; + #[cfg(feature = "testing")] + use tokio::sync::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard}; use tss_esapi::{ constants::structure_tags::StructureTag, - structures::{Attest, AttestBuffer, DigestList, Ticket}, + structures::{Attest, AttestBuffer, DigestList}, tss2_esys::{ Tss2_MU_TPMT_SIGNATURE_Unmarshal, TPM2B_ATTEST, TPM2B_DIGEST, TPMS_PCR_SELECTION, TPMT_SIGNATURE, }, }; - #[cfg(test)] - use std::{fs::File, io::BufReader, path::Path}; + #[cfg(feature = "testing")] + pub static MUTEX: OnceLock>> = OnceLock::new(); macro_rules! create_unmarshal_fn { ($func:ident, $tpmobj:ty, $unmarshal:ident) => { @@ -1856,6 +1957,15 @@ pub mod testing { Tss2_MU_TPMT_SIGNATURE_Unmarshal ); + /// Initialize testing mutex + #[cfg(feature = "testing")] + pub async fn lock_tests<'a>() -> AsyncMutexGuard<'a, ()> { + MUTEX + .get_or_init(|| Arc::new(AsyncMutex::new(()))) + .lock() + .await + } + /// Deserialize a TPML_PCR_SELECTION from a &[u8] slice. /// The deserialization will adjust the data endianness as necessary. fn deserialize_pcrsel(pcrsel_vec: &[u8]) -> Result { @@ -2003,7 +2113,7 @@ pub mod testing { Ok((pcrlist, pcrdata)) } - fn decode_quote_string( + pub fn decode_quote_string( quote: &str, ) -> Result<(AttestBuffer, Signature, PcrSelectionList, PcrData)> { if !quote.starts_with('r') { @@ -2046,7 +2156,7 @@ pub mod testing { /// Reference: /// https://github.com/tpm2-software/tpm2-tools/blob/master/tools/tpm2_checkquote.c pub fn check_quote( - context: &mut tss_esapi::Context, + context: &mut Context, ak_handle: KeyHandle, quote: &str, nonce: &[u8], @@ -2115,10 +2225,165 @@ pub mod testing { Ok(()) } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + #[cfg(feature = "testing")] + use std::{ + fs::File, + io::{BufRead, BufReader}, + path::Path, + }; + + #[test] + fn test_pubkey_to_digest() { + use openssl::pkey::PKey; + use openssl::rsa::Rsa; + + let rsa = Rsa::generate(2048).unwrap(); //#[allow_ci] + let pkey = PKey::from_rsa(rsa).unwrap(); //#[allow_ci] + + assert!(pubkey_to_tpm_digest(pkey.as_ref(), HashAlgorithm::Sha256) + .is_ok()); + } + + #[test] + fn test_mask() { + assert_eq!(read_mask(0x0).unwrap(), vec![]); //#[allow_ci] + + assert_eq!(read_mask(0x1).unwrap(), vec![PcrSlot::Slot0]); //#[allow_ci] + + assert_eq!(read_mask(0x2).unwrap(), vec![PcrSlot::Slot1]); //#[allow_ci] + + assert_eq!(read_mask(0x4).unwrap(), vec![PcrSlot::Slot2]); //#[allow_ci] + + assert_eq!( + read_mask(0x5).unwrap(), //#[allow_ci] + vec![PcrSlot::Slot0, PcrSlot::Slot2] + ); + + assert_eq!( + read_mask(0x6).unwrap(), //#[allow_ci] + vec![PcrSlot::Slot1, PcrSlot::Slot2] + ); + + assert_eq!(read_mask(0x800000).unwrap(), vec![PcrSlot::Slot23]); //#[allow_ci] + + assert_eq!( + read_mask(0xffffff).unwrap(), //#[allow_ci] + vec![ + PcrSlot::Slot0, + PcrSlot::Slot1, + PcrSlot::Slot2, + PcrSlot::Slot3, + PcrSlot::Slot4, + PcrSlot::Slot5, + PcrSlot::Slot6, + PcrSlot::Slot7, + PcrSlot::Slot8, + PcrSlot::Slot9, + PcrSlot::Slot10, + PcrSlot::Slot11, + PcrSlot::Slot12, + PcrSlot::Slot13, + PcrSlot::Slot14, + PcrSlot::Slot15, + PcrSlot::Slot16, + PcrSlot::Slot17, + PcrSlot::Slot18, + PcrSlot::Slot19, + PcrSlot::Slot20, + PcrSlot::Slot21, + PcrSlot::Slot22, + PcrSlot::Slot23 + ] + ); + + assert!(read_mask(0x1ffffff).is_err()); + } + + #[test] + fn test_check_mask() { + // Test simply reading a mask + let r = read_mask(0xFFFF); + assert!(r.is_ok(), "Result: {r:?}"); + + // Test with mask containing the PCR + let should_be_true = check_mask(0xFFFF, &PcrSlot::Slot10) + .expect("failed to check mask"); + assert!(should_be_true); + + // Test a mask not containing the specific PCR + let should_be_false = check_mask(0xFFFD, &PcrSlot::Slot1) + .expect("failed to check mask"); + assert!(!should_be_false); + + // Test that trying a mask with bits not in the range from 0 to 23 fails + let r = check_mask(1 << 24, &PcrSlot::Slot1); + assert!(r.is_err()); + } + + #[test] + fn test_get_idevid_template() { + let cases = [ + ("H-1", (AsymmetricAlgorithm::Rsa, HashingAlgorithm::Sha256)), + ("H-2", (AsymmetricAlgorithm::Ecc, HashingAlgorithm::Sha256)), + ("H-3", (AsymmetricAlgorithm::Ecc, HashingAlgorithm::Sha384)), + ("H-4", (AsymmetricAlgorithm::Ecc, HashingAlgorithm::Sha512)), + ("H-5", (AsymmetricAlgorithm::Ecc, HashingAlgorithm::Sm3_256)), + ]; + + for (input, output) in cases { + let algs = get_idevid_template("manual", input, "", "") + .expect("failed to get IDevID template"); + assert_eq!(algs, output); + } + + let auto = ["", "detect", "default"]; + + for keyword in auto { + let algs = get_idevid_template("H-1", keyword, "", "") + .expect("failed to get IDevID template"); + assert_eq!( + algs, + (AsymmetricAlgorithm::Rsa, HashingAlgorithm::Sha256) + ); + } + } #[test] #[cfg(feature = "testing")] - fn test_create_ek() { + fn test_quote_encode_decode() { + let quote_path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("test-data") + .join("test-quote.txt"); + + let f = + File::open(quote_path).expect("unable to open test-quote.txt"); + let mut f = BufReader::new(f); + let mut buf = String::new(); + let _ = f.read_line(&mut buf).expect("unable to read quote"); + let buf = buf.trim_end(); + + let (att, sig, pcrsel, pcrdata) = testing::decode_quote_string(buf) + .expect("unable to decode quote"); + + let attestation: Attest = + att.try_into().expect("unable to unmarshal attestation"); + + let encoded = encode_quote_string(attestation, sig, pcrsel, pcrdata) + .expect("unable to encode quote"); + + assert_eq!(encoded, buf); + } + + #[tokio::test] + #[cfg(feature = "testing")] + async fn test_create_ek() { + let _mutex = testing::lock_tests().await; let mut ctx = Context::new().unwrap(); //#[allow_ci] let algs = [EncryptionAlgorithm::Rsa, EncryptionAlgorithm::Ecc]; // TODO: create persistent handle and add to be tested: Some("0x81000000"), @@ -2128,17 +2393,23 @@ pub mod testing { for handle in handles { let r = ctx.create_ek(alg, handle); assert!(r.is_ok()); + let ek = r.unwrap(); //#[allow_ci] + + // Flush context to free TPM memory + let r = ctx.flush_context(ek.key_handle.into()); + assert!(r.is_ok(), "Result: {r:?}"); } } } - #[test] + #[tokio::test] #[cfg(feature = "testing")] - fn test_create_and_load_ak() { + async fn test_create_and_load_ak() { + let _mutex = testing::lock_tests().await; let mut ctx = Context::new().unwrap(); //#[allow_ci] let r = ctx.create_ek(EncryptionAlgorithm::Rsa, None); - assert!(r.is_ok()); + assert!(r.is_ok(), "Result: {r:?}"); let ek_result = r.unwrap(); //#[allow_ci] let ek_handle = ek_result.key_handle; @@ -2165,19 +2436,28 @@ pub mod testing { for sign in sign_algs { for hash in hash_algs { let r = ctx.create_ak(ek_handle, hash, sign); - assert!(r.is_ok()); - + assert!(r.is_ok(), "Result: {r:?}"); let ak = r.unwrap(); //#[allow_ci] let r = ctx.load_ak(ek_handle, &ak); - assert!(r.is_ok()); + assert!(r.is_ok(), "Result: {r:?}"); + let handle = r.unwrap(); //#[allow_ci] + + // Flush context to free TPM memory + let r = ctx.flush_context(handle.into()); + assert!(r.is_ok(), "Result: {r:?}"); } } + + // Flush context to free TPM memory + let r = ctx.flush_context(ek_handle.into()); + assert!(r.is_ok(), "Result: {r:?}"); } - #[test] + #[tokio::test] #[cfg(feature = "testing")] - fn test_create_idevid() { + async fn test_create_idevid() { + let _mutex = testing::lock_tests().await; let asym_algs = [AsymmetricAlgorithm::Rsa, AsymmetricAlgorithm::Ecc]; let hash_algs = [ HashingAlgorithm::Sha256, @@ -2193,15 +2473,23 @@ pub mod testing { for asym in asym_algs { for hash in hash_algs { + println!("Creating IDevID with {asym:?} and {hash:?}"); let r = ctx.create_idevid(asym, hash); - assert!(r.is_ok()); + assert!(r.is_ok(), "Result: {r:?}"); + println!( + "Successfully created IDevID with {asym:?} and {hash:?}" + ); + let idevid = r.unwrap(); //#[allow_ci] + let r = ctx.flush_context(idevid.handle.into()); + assert!(r.is_ok(), "Result: {r:?}"); } } } - #[test] + #[tokio::test] #[cfg(feature = "testing")] - fn test_create_iak() { + async fn test_create_iak() { + let _mutex = testing::lock_tests().await; let mut ctx = Context::new().unwrap(); //#[allow_ci] let asym_algs = [AsymmetricAlgorithm::Rsa, AsymmetricAlgorithm::Ecc]; @@ -2217,15 +2505,23 @@ pub mod testing { for asym in asym_algs { for hash in hash_algs { + println!("Creating IAK with {asym:?} and {hash:?}"); let r = ctx.create_iak(asym, hash); - assert!(r.is_ok()) + assert!(r.is_ok(), "Result: {r:?}"); + println!( + "Successfully created IAK with {asym:?} and {hash:?}" + ); + let iak = r.unwrap(); //#[allow_ci] + let r = ctx.flush_context(iak.handle.into()); + assert!(r.is_ok(), "Result: {r:?}"); } } } - #[test] + #[tokio::test] #[cfg(feature = "testing")] - fn test_activate_credential() { + async fn test_activate_credential() { + let _mutex = testing::lock_tests().await; let mut ctx = Context::new().unwrap(); //#[allow_ci] // Create EK @@ -2255,7 +2551,7 @@ pub mod testing { // Generate random challenge let mut challenge: [u8; 32] = [0; 32]; let r = openssl::rand::rand_priv_bytes(&mut challenge); - assert!(r.is_ok()); + assert!(r.is_ok(), "Result: {r:?}"); let credential = Digest::try_from(challenge.as_ref()) .expect("Failed to convert random bytes to Digest structure"); @@ -2270,11 +2566,18 @@ pub mod testing { .activate_credential(keyblob, ak_handle, ek_handle) .expect("failed to decrypt challenge"); assert_eq!(decrypted, credential); + + // Flush context to free TPM memory + let r = ctx.flush_context(ek_handle.into()); + assert!(r.is_ok(), "Result: {r:?}"); + let r = ctx.flush_context(ak_handle.into()); + assert!(r.is_ok(), "Result: {r:?}"); } - #[test] + #[tokio::test] #[cfg(feature = "testing")] - fn test_certify_credential_with_iak() { + async fn test_certify_credential_with_iak() { + let _mutex = testing::lock_tests().await; let mut ctx = Context::new().unwrap(); //#[allow_ci] // Create EK @@ -2307,152 +2610,14 @@ pub mod testing { ak_handle, iak_handle, ); - assert!(r.is_ok()); - } - - #[test] - fn test_check_mask() { - // Test simply reading a mask - let r = read_mask(0xFFFF); - assert!(r.is_ok()); - - // Test with mask containing the PCR - let should_be_true = check_mask(0xFFFF, &PcrSlot::Slot10) - .expect("failed to check mask"); - assert!(should_be_true); - - // Test a mask not containing the specific PCR - let should_be_false = check_mask(0xFFFD, &PcrSlot::Slot1) - .expect("failed to check mask"); - assert!(!should_be_false); - - // Test that trying a mask with bits not in the range from 0 to 23 fails - let r = check_mask(1 << 24, &PcrSlot::Slot1); - assert!(r.is_err()); - } - - #[test] - fn test_get_idevid_template() { - let cases = [ - ("H-1", (AsymmetricAlgorithm::Rsa, HashingAlgorithm::Sha256)), - ("H-2", (AsymmetricAlgorithm::Ecc, HashingAlgorithm::Sha256)), - ("H-3", (AsymmetricAlgorithm::Ecc, HashingAlgorithm::Sha384)), - ("H-4", (AsymmetricAlgorithm::Ecc, HashingAlgorithm::Sha512)), - ("H-5", (AsymmetricAlgorithm::Ecc, HashingAlgorithm::Sm3_256)), - ]; - - for (input, output) in cases { - let algs = get_idevid_template("manual", input, "", "") - .expect("failed to get IDevID template"); - assert_eq!(algs, output); - } - - let auto = ["", "detect", "default"]; - - for keyword in auto { - let algs = get_idevid_template("H-1", keyword, "", "") - .expect("failed to get IDevID template"); - assert_eq!( - algs, - (AsymmetricAlgorithm::Rsa, HashingAlgorithm::Sha256) - ); - } - } - - #[test] - fn test_quote_encode_decode() { - let quote_path = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("test-data") - .join("test-quote.txt"); - - let f = - File::open(quote_path).expect("unable to open test-quote.txt"); - let mut f = BufReader::new(f); - let mut buf = String::new(); - let _ = f.read_line(&mut buf).expect("unable to read quote"); - let buf = buf.trim_end(); - - let (att, sig, pcrsel, pcrdata) = - decode_quote_string(buf).expect("unable to decode quote"); - - let attestation: Attest = - att.try_into().expect("unable to unmarshal attestation"); - - let encoded = encode_quote_string(attestation, sig, pcrsel, pcrdata) - .expect("unable to encode quote"); - - assert_eq!(encoded, buf); - } -} - -#[cfg(test)] -pub mod tests { - use super::*; - - #[test] - fn test_pubkey_to_digest() { - use openssl::pkey::PKey; - use openssl::rsa::Rsa; - - let rsa = Rsa::generate(2048).unwrap(); //#[allow_ci] - let pkey = PKey::from_rsa(rsa).unwrap(); //#[allow_ci] - - assert!(pubkey_to_tpm_digest(pkey.as_ref(), HashAlgorithm::Sha256) - .is_ok()); - } - - #[test] - fn test_mask() { - assert_eq!(read_mask(0x0).unwrap(), vec![]); //#[allow_ci] - - assert_eq!(read_mask(0x1).unwrap(), vec![PcrSlot::Slot0]); //#[allow_ci] - - assert_eq!(read_mask(0x2).unwrap(), vec![PcrSlot::Slot1]); //#[allow_ci] - - assert_eq!(read_mask(0x4).unwrap(), vec![PcrSlot::Slot2]); //#[allow_ci] - - assert_eq!( - read_mask(0x5).unwrap(), //#[allow_ci] - vec![PcrSlot::Slot0, PcrSlot::Slot2] - ); - - assert_eq!( - read_mask(0x6).unwrap(), //#[allow_ci] - vec![PcrSlot::Slot1, PcrSlot::Slot2] - ); - - assert_eq!(read_mask(0x800000).unwrap(), vec![PcrSlot::Slot23]); //#[allow_ci] - - assert_eq!( - read_mask(0xffffff).unwrap(), //#[allow_ci] - vec![ - PcrSlot::Slot0, - PcrSlot::Slot1, - PcrSlot::Slot2, - PcrSlot::Slot3, - PcrSlot::Slot4, - PcrSlot::Slot5, - PcrSlot::Slot6, - PcrSlot::Slot7, - PcrSlot::Slot8, - PcrSlot::Slot9, - PcrSlot::Slot10, - PcrSlot::Slot11, - PcrSlot::Slot12, - PcrSlot::Slot13, - PcrSlot::Slot14, - PcrSlot::Slot15, - PcrSlot::Slot16, - PcrSlot::Slot17, - PcrSlot::Slot18, - PcrSlot::Slot19, - PcrSlot::Slot20, - PcrSlot::Slot21, - PcrSlot::Slot22, - PcrSlot::Slot23 - ] - ); - - assert!(read_mask(0x1ffffff).is_err()); + assert!(r.is_ok(), "Result: {r:?}"); + + // Flush context to free TPM memory + let r = ctx.flush_context(ek_handle.into()); + assert!(r.is_ok(), "Result: {r:?}"); + let r = ctx.flush_context(ak_handle.into()); + assert!(r.is_ok(), "Result: {r:?}"); + let r = ctx.flush_context(iak_handle.into()); + assert!(r.is_ok(), "Result: {r:?}"); } } diff --git a/tests/run.sh b/tests/run.sh index b120313f..e4ae2f80 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -2,27 +2,63 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright 2021 Keylime Authors +# Store the old TCTI setting +OLD_TCTI=$TCTI +OLD_TPM2TOOLS_TCTI=$TPM2TOOLS_TCTI + set -euf -o pipefail -echo "-------- Setting up Virtual TPM" -mkdir /tmp/tpmdir +echo "-------- Setting up Software TPM" + +# Create temporary directories +TEMPDIR=$(mktemp -d) +TPMDIR="${TEMPDIR}/tpmdir" +mkdir -p ${TPMDIR} + +# Manufacture a new Software TPM swtpm_setup --tpm2 \ - --tpmstate /tmp/tpmdir \ + --tpmstate ${TPMDIR} \ --createek --decryption --create-ek-cert \ --create-platform-cert \ + --lock-nvram \ + --not-overwrite \ + --pcr-banks sha256 \ --display -swtpm socket --tpm2 \ - --tpmstate dir=/tmp/tpmdir \ - --flags startup-clear \ - --ctrl type=tcp,port=2322 \ - --server type=tcp,port=2321 \ - --daemon -tpm2-abrmd \ - --logger=stdout \ - --tcti=swtpm: \ - --allow-root \ - --session \ - --flush-all & + +function start_swtpm { + # Initialize the swtpm socket + swtpm socket --tpm2 \ + --tpmstate dir=${TPMDIR} \ + --flags startup-clear \ + --ctrl type=tcp,port=2322 \ + --server type=tcp,port=2321 \ + --log level=1 & + SWTPM_PID=$! +} + +function stop_swtpm { + # Stop swtpm if running + if [[ -n "$SWTPM_PID" ]]; then + echo "Stopping swtpm" + kill $SWTPM_PID + fi +} + +# Set cleanup function to run at exit +function cleanup { + + echo "-------- Restore TCTI settings" + TCTI=$OLD_TCTI + TPM2TOOLS_TCTI=$OLD_TPM2TOOLS_TCTI + + echo "-------- Cleanup processes" + stop_swtpm +} +trap cleanup EXIT + +# Set the TCTI to use the swtpm socket +export TCTI=swtpm +export TPM2TOOLS_TCTI=swtpm echo "-------- Running clippy" # The cargo denies are currently disabled, because that will require a bunch of dep cleanup @@ -32,13 +68,14 @@ echo "-------- Building" RUST_BACKTRACE=1 cargo build echo "-------- Testing" +start_swtpm mkdir -p /var/lib/keylime -TCTI=tabrmd:bus_type=session RUST_BACKTRACE=1 RUST_LOG=info \ +RUST_BACKTRACE=1 RUST_LOG=info \ KEYLIME_CONFIG=$PWD/keylime-agent.conf \ cargo test --features testing -- --nocapture echo "-------- Testing with coverage" -TCTI=tabrmd:bus_type=session RUST_BACKTRACE=1 RUST_LOG=info \ +RUST_BACKTRACE=1 RUST_LOG=info \ KEYLIME_CONFIG=$PWD/keylime-agent.conf \ cargo tarpaulin --verbose \ --target-dir target/tarpaulin \ @@ -47,5 +84,4 @@ cargo tarpaulin --verbose \ --ignore-panics --ignore-tests \ --out Html --out Json \ --all-features \ - --engine llvm \ - -- --test-threads=1 + --engine llvm From 847377d8a4ff9f07b9000fa481d41f064e216bd3 Mon Sep 17 00:00:00 2001 From: Anderson Toshiyuki Sasaki Date: Wed, 20 Nov 2024 13:11:02 +0100 Subject: [PATCH 2/2] tests/setup_swtpm.sh: Add script to setup temporary TPM Add the tests/setup_swtpm.sh script which setup a Software TPM in a temporary directory, starts the swtpm socket, and sets the environment TCTI accordingly. This allows the tests to be executed locally, even with the "testing" feature. Unfortunately, it is not possible to cleanup some of the transient objects created during tests, being necessary to cleanup manually between runs by running: $ tpm2_flushcontext -t -l -s Another caveat is that the tests need to run on a single thread to avoid test cases that create objects to run in parallel, which can fill up the TPM memory with transient object contexts. For this, please run the tests on a single thread: $ cargo test --features=testing -- --test-threads=1 The swtpm socket process is stopped when exiting from the started shell. Fixes: #259 Signed-off-by: Anderson Toshiyuki Sasaki --- tests/setup_swtpm.sh | 65 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100755 tests/setup_swtpm.sh diff --git a/tests/setup_swtpm.sh b/tests/setup_swtpm.sh new file mode 100755 index 00000000..aee4d953 --- /dev/null +++ b/tests/setup_swtpm.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2021 Keylime Authors + +# Store the old TCTI setting +OLD_TCTI=$TCTI +OLD_TPM2TOOLS_TCTI=$TPM2TOOLS_TCTI + +set -euf -o pipefail + +if [[ $# -eq 0 ]] || [[ -z "$1" ]]; then + TEMPDIR=$(mktemp -d) + TPMDIR="${TEMPDIR}/tpmdir" + mkdir -p ${TPMDIR} +else + echo "Using TPM state from $1" + TPMDIR=$1 +fi + +# Manufacture a new Software TPM +swtpm_setup --tpm2 \ + --tpmstate ${TPMDIR} \ + --createek --decryption --create-ek-cert \ + --create-platform-cert \ + --lock-nvram \ + --not-overwrite \ + --pcr-banks sha256 \ + --display + +function start_swtpm { + # Initialize the swtpm socket + swtpm socket --tpm2 \ + --tpmstate dir=${TPMDIR} \ + --flags startup-clear \ + --ctrl type=tcp,port=2322 \ + --server type=tcp,port=2321 \ + --log level=1 & + SWTPM_PID=$! +} + +function stop_swtpm { + # Stop swtpm if running + if [[ -n "$SWTPM_PID" ]]; then + echo "Stopping swtpm" + kill $SWTPM_PID + fi +} + +# Set cleanup function to run at exit +function cleanup { + echo "-------- Restore TCTI settings" + TCTI=$OLD_TCTI + TPM2TOOLS_TCTI=$OLD_TPM2TOOLS_TCTI + + echo "-------- Cleanup processes" + stop_swtpm +} +trap cleanup EXIT + +# Set the TCTI to use the swtpm socket +export TCTI=swtpm +export TPM2TOOLS_TCTI=swtpm + +start_swtpm +bash