use std::str::FromStr;
use tss_esapi::Context;
use tss_esapi::TctiNameConf;
use tss_esapi::attributes::ObjectAttributesBuilder;
use tss_esapi::constants::SessionType;
use tss_esapi::handles::{KeyHandle, ObjectHandle, SessionHandle};
use tss_esapi::interface_types::algorithm::{HashingAlgorithm, PublicAlgorithm};
use tss_esapi::interface_types::ecc::EccCurve;
use tss_esapi::interface_types::resource_handles::Hierarchy;
use tss_esapi::interface_types::session_handles::{AuthSession, PolicySession};
use tss_esapi::structures::{
Digest, EccPoint, KeyedHashScheme, PcrSelectSize, PcrSelectionList, PcrSelectionListBuilder,
PcrSlot, Private, Public, PublicBuilder, PublicEccParametersBuilder, PublicKeyedHashParameters,
SensitiveData, SymmetricDefinition, SymmetricDefinitionObject,
};
use tss_esapi::traits::{Marshall, UnMarshall};
use zeroize::Zeroizing;
use vti_common::error::AppError;
use super::TpmSettings;
use super::envelope::TpmDescriptor;
fn tpm_err(what: &str) -> impl FnOnce(tss_esapi::Error) -> AppError + '_ {
move |e| AppError::SecretStore(format!("TPM: {what}: {e}"))
}
fn open_context(tcti: &str) -> Result<Context, AppError> {
let conf = TctiNameConf::from_str(tcti)
.map_err(|e| AppError::Config(format!("TPM TCTI '{tcti}' is not valid: {e}")))?;
Context::new(conf).map_err(tpm_err("opening the TPM"))
}
fn srk_template() -> Result<Public, AppError> {
let attributes = ObjectAttributesBuilder::new()
.with_fixed_tpm(true)
.with_fixed_parent(true)
.with_sensitive_data_origin(true)
.with_user_with_auth(true)
.with_decrypt(true)
.with_restricted(true)
.build()
.map_err(tpm_err("SRK attributes"))?;
PublicBuilder::new()
.with_public_algorithm(PublicAlgorithm::Ecc)
.with_name_hashing_algorithm(HashingAlgorithm::Sha256)
.with_object_attributes(attributes)
.with_ecc_parameters(
PublicEccParametersBuilder::new_restricted_decryption_key(
SymmetricDefinitionObject::AES_128_CFB,
EccCurve::NistP256,
)
.build()
.map_err(tpm_err("SRK parameters"))?,
)
.with_ecc_unique_identifier(EccPoint::default())
.build()
.map_err(tpm_err("SRK template"))
}
fn create_srk(ctx: &mut Context) -> Result<KeyHandle, AppError> {
let template = srk_template()?;
ctx.execute_with_nullauth_session(|ctx| {
ctx.create_primary(Hierarchy::Owner, template, None, None, None, None)
})
.map(|r| r.key_handle)
.map_err(tpm_err("creating the storage root key"))
}
fn pcr_selection(pcrs: &[u8]) -> Result<PcrSelectionList, AppError> {
let slots = pcrs
.iter()
.map(|p| PcrSlot::try_from(1u32 << p).map_err(tpm_err("PCR index")))
.collect::<Result<Vec<_>, _>>()?;
PcrSelectionListBuilder::new()
.with_size_of_select(PcrSelectSize::ThreeOctets)
.with_selection(HashingAlgorithm::Sha256, &slots)
.build()
.map_err(tpm_err("PCR selection"))
}
fn pcr_policy_session(
ctx: &mut Context,
session_type: SessionType,
pcrs: &[u8],
) -> Result<(AuthSession, PolicySession), AppError> {
let session = ctx
.start_auth_session(
None,
None,
None,
session_type,
SymmetricDefinition::AES_128_CFB,
HashingAlgorithm::Sha256,
)
.map_err(tpm_err("starting a policy session"))?
.ok_or_else(|| AppError::SecretStore("TPM: no policy session returned".into()))?;
let policy = PolicySession::try_from(session).map_err(tpm_err("policy session"))?;
ctx.policy_pcr(policy, Digest::default(), pcr_selection(pcrs)?)
.map_err(tpm_err("PolicyPCR"))?;
Ok((session, policy))
}
fn flush_session(ctx: &mut Context, policy: PolicySession) {
let _ = ctx.flush_context(ObjectHandle::from(SessionHandle::from(policy)));
}
pub(super) fn seal(settings: &TpmSettings, kek: &[u8; 32]) -> Result<TpmDescriptor, AppError> {
let mut ctx = open_context(&settings.tcti)?;
let srk = create_srk(&mut ctx)?;
let result = seal_under(&mut ctx, srk, settings, kek);
let _ = ctx.flush_context(srk.into());
result
}
fn seal_under(
ctx: &mut Context,
srk: KeyHandle,
settings: &TpmSettings,
kek: &[u8; 32],
) -> Result<TpmDescriptor, AppError> {
let bound = !settings.pcrs.is_empty();
let attributes = ObjectAttributesBuilder::new()
.with_fixed_tpm(true)
.with_fixed_parent(true)
.with_no_da(true)
.with_user_with_auth(!bound)
.build()
.map_err(tpm_err("sealed object attributes"))?;
let mut builder = PublicBuilder::new()
.with_public_algorithm(PublicAlgorithm::KeyedHash)
.with_name_hashing_algorithm(HashingAlgorithm::Sha256)
.with_object_attributes(attributes)
.with_keyed_hash_parameters(PublicKeyedHashParameters::new(KeyedHashScheme::Null))
.with_keyed_hash_unique_identifier(Digest::default());
if bound {
let (_, trial) = pcr_policy_session(ctx, SessionType::Trial, &settings.pcrs)?;
let digest = ctx.policy_get_digest(trial);
flush_session(ctx, trial);
builder = builder.with_auth_policy(digest.map_err(tpm_err("policy digest"))?);
}
let public = builder.build().map_err(tpm_err("sealed object template"))?;
let sensitive = SensitiveData::try_from(kek.to_vec()).map_err(tpm_err("sealed data"))?;
let created = ctx
.execute_with_nullauth_session(|ctx| {
ctx.create(srk, public, None, Some(sensitive), None, None)
})
.map_err(tpm_err("sealing the key-encryption key"))?;
Ok(TpmDescriptor {
pcrs: settings.pcrs.clone(),
public: created
.out_public
.marshall()
.map_err(tpm_err("marshalling the sealed public part"))?,
private: created.out_private.value().to_vec(),
})
}
pub(super) fn unseal(
settings: &TpmSettings,
descriptor: &TpmDescriptor,
) -> Result<Zeroizing<[u8; 32]>, AppError> {
if descriptor.pcrs != settings.pcrs {
tracing::info!(
sealed = ?descriptor.pcrs,
configured = ?settings.pcrs,
"the TPM-sealed key is bound to different PCRs than configured; the next write \
re-seals to the configured PCRs"
);
}
let mut ctx = open_context(&settings.tcti)?;
let srk = create_srk(&mut ctx)?;
let result = unseal_under(&mut ctx, srk, descriptor);
let _ = ctx.flush_context(srk.into());
result
}
fn unseal_under(
ctx: &mut Context,
srk: KeyHandle,
descriptor: &TpmDescriptor,
) -> Result<Zeroizing<[u8; 32]>, AppError> {
let public = Public::unmarshall(&descriptor.public).map_err(tpm_err("sealed public part"))?;
let private =
Private::try_from(descriptor.private.clone()).map_err(tpm_err("sealed private part"))?;
let object = ctx
.execute_with_nullauth_session(|ctx| ctx.load(srk, private, public))
.map_err(tpm_err(
"loading the sealed key (a different TPM, or the owner hierarchy was cleared?)",
))?;
let data = if descriptor.pcrs.is_empty() {
ctx.execute_with_nullauth_session(|ctx| ctx.unseal(object.into()))
} else {
match pcr_policy_session(ctx, SessionType::Policy, &descriptor.pcrs) {
Ok((session, policy)) => {
let r = ctx.execute_with_session(Some(session), |ctx| ctx.unseal(object.into()));
flush_session(ctx, policy);
r
}
Err(e) => {
let _ = ctx.flush_context(object.into());
return Err(e);
}
}
};
let _ = ctx.flush_context(object.into());
let data = data.map_err(tpm_err(
"unsealing the key-encryption key (PCR values changed since it was sealed?)",
))?;
if data.len() != 32 {
return Err(AppError::SecretStore(
"TPM: the sealed key-encryption key is not 32 bytes".into(),
));
}
let mut kek = Zeroizing::new([0u8; 32]);
kek.copy_from_slice(data.value());
Ok(kek)
}
#[cfg(test)]
mod tests {
use super::super::super::SeedStore;
use super::super::EncryptedFileSeedStore;
use super::*;
use crate::config::{EncryptedFileConfig, KekSource};
fn test_tcti() -> Option<String> {
std::env::var("VTI_SECRETS_TEST_TCTI").ok()
}
fn store(dir: &std::path::Path, tcti: &str, pcrs: Vec<u8>) -> EncryptedFileSeedStore {
let cfg = EncryptedFileConfig::new(KekSource::Tpm {
pcrs,
tcti: Some(tcti.to_string()),
});
EncryptedFileSeedStore::from_config(&cfg, dir, "seed.enc").unwrap()
}
fn extend_pcr16(tcti: &str) {
use tss_esapi::handles::PcrHandle;
use tss_esapi::structures::DigestValues;
let mut ctx = open_context(tcti).unwrap();
let mut vals = DigestValues::new();
vals.set(
HashingAlgorithm::Sha256,
Digest::try_from(vec![0xa5u8; 32]).unwrap(),
);
ctx.execute_with_nullauth_session(|ctx| ctx.pcr_extend(PcrHandle::Pcr16, vals))
.unwrap();
}
#[tokio::test]
async fn tpm_round_trip_without_pcrs() {
let Some(tcti) = test_tcti() else { return };
let dir = tempfile::tempdir().unwrap();
let s = store(dir.path(), &tcti, vec![]);
s.set(b"tpm sealed seed").await.unwrap();
assert_eq!(s.get().await.unwrap().unwrap(), b"tpm sealed seed");
}
#[tokio::test]
async fn tpm_pcr_binding_holds_until_the_pcr_changes() {
let Some(tcti) = test_tcti() else { return };
let dir = tempfile::tempdir().unwrap();
let s = store(dir.path(), &tcti, vec![16]);
s.set(b"pcr bound").await.unwrap();
assert_eq!(s.get().await.unwrap().unwrap(), b"pcr bound");
extend_pcr16(&tcti);
match s.get().await {
Err(AppError::SecretStore(m)) => assert!(m.contains("unsealing"), "{m}"),
other => panic!("want an unseal failure after the PCR changed, got {other:?}"),
}
s.set(b"resealed").await.unwrap();
assert_eq!(s.get().await.unwrap().unwrap(), b"resealed");
}
#[tokio::test]
async fn tpm_tampered_sealed_blob_fails_closed() {
let Some(tcti) = test_tcti() else { return };
let dir = tempfile::tempdir().unwrap();
let s = store(dir.path(), &tcti, vec![]);
s.set(b"x").await.unwrap();
let mut bytes = std::fs::read(s.path()).unwrap();
bytes[70] ^= 1;
std::fs::write(s.path(), &bytes).unwrap();
assert!(s.get().await.is_err());
}
}