use std::sync::Arc;
use ed25519_dalek::{
Signature as Ed25519Signature, Signer as Ed25519Signer, SigningKey as Ed25519SigningKey,
VerifyingKey as Ed25519VerifyingKey,
};
use hkdf::Hkdf;
use sha2::{Digest as Sha2Digest, Sha256};
use zeroize::Zeroizing;
use crate::error::{Result, TeeError};
use crate::traits::TeeProvider;
use tenzro_types::tee::AttestationReport;
const HKDF_INFO: &[u8] = b"tenzro/sealed-agent-ed25519/v1";
const SALT_DOMAIN: &[u8] = b"tenzro/agent-key-salt/v1";
#[derive(Clone)]
pub struct AgentKeyHandle {
agent_did: String,
epoch: u64,
measurement: [u8; 32],
verifying_key: Ed25519VerifyingKey,
secret_scalar: Arc<Zeroizing<[u8; 32]>>,
}
impl std::fmt::Debug for AgentKeyHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AgentKeyHandle")
.field("agent_did", &self.agent_did)
.field("epoch", &self.epoch)
.field("measurement", &hex::encode(self.measurement))
.field("pubkey", &hex::encode(self.verifying_key.to_bytes()))
.finish()
}
}
impl AgentKeyHandle {
pub fn agent_did(&self) -> &str {
&self.agent_did
}
pub fn epoch(&self) -> u64 {
self.epoch
}
pub fn measurement(&self) -> [u8; 32] {
self.measurement
}
pub fn pubkey(&self) -> [u8; 32] {
self.verifying_key.to_bytes()
}
pub fn verifying_key(&self) -> &Ed25519VerifyingKey {
&self.verifying_key
}
pub fn sign(&self, message: &[u8]) -> Ed25519Signature {
let signing = self.signing_key_local();
signing.sign(message)
}
pub fn sign_prehash(&self, prehash: &[u8; 32]) -> [u8; 64] {
let sig = self.sign(prehash.as_slice());
sig.to_bytes()
}
fn signing_key_local(&self) -> Ed25519SigningKey {
Ed25519SigningKey::from_bytes(&self.secret_scalar)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AgentKeyAttestationPacket {
pub agent_did_hash: [u8; 32],
pub epoch: u64,
pub pubkey: [u8; 32],
}
impl AgentKeyAttestationPacket {
pub fn from_handle(handle: &AgentKeyHandle) -> Self {
let mut h = Sha256::new();
h.update(handle.agent_did.as_bytes());
let mut did_hash = [0u8; 32];
did_hash.copy_from_slice(&h.finalize());
Self {
agent_did_hash: did_hash,
epoch: handle.epoch,
pubkey: handle.pubkey(),
}
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(32 + 8 + 32);
out.extend_from_slice(&self.agent_did_hash);
out.extend_from_slice(&self.epoch.to_be_bytes());
out.extend_from_slice(&self.pubkey);
out
}
pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
if bytes.len() != 72 {
return None;
}
let mut agent_did_hash = [0u8; 32];
agent_did_hash.copy_from_slice(&bytes[..32]);
let mut epoch_be = [0u8; 8];
epoch_be.copy_from_slice(&bytes[32..40]);
let mut pubkey = [0u8; 32];
pubkey.copy_from_slice(&bytes[40..]);
Some(Self {
agent_did_hash,
epoch: u64::from_be_bytes(epoch_be),
pubkey,
})
}
}
fn agent_salt(agent_did: &str, epoch: u64) -> [u8; 32] {
let mut h = Sha256::new();
h.update(SALT_DOMAIN);
h.update(agent_did.as_bytes());
h.update(epoch.to_be_bytes());
let mut out = [0u8; 32];
out.copy_from_slice(&h.finalize());
out
}
pub(crate) fn handle_from_ikm(
agent_did: &str,
epoch: u64,
measurement: [u8; 32],
ikm: &[u8],
) -> Result<AgentKeyHandle> {
let salt = agent_salt(agent_did, epoch);
let hk = Hkdf::<Sha256>::new(Some(&salt), ikm);
let mut scalar = Zeroizing::new([0u8; 32]);
hk.expand(HKDF_INFO, scalar.as_mut())
.map_err(|e| TeeError::CryptoError(format!("HKDF expand failed: {}", e)))?;
let signing = Ed25519SigningKey::from_bytes(&scalar);
let verifying = signing.verifying_key();
drop(signing);
Ok(AgentKeyHandle {
agent_did: agent_did.to_string(),
epoch,
measurement,
verifying_key: verifying,
secret_scalar: Arc::new(scalar),
})
}
pub async fn seal_agent_keypair(
provider: &dyn TeeProvider,
agent_did: &str,
) -> Result<AgentKeyHandle> {
seal_agent_keypair_epoch(provider, agent_did, 0).await
}
async fn seal_agent_keypair_epoch(
provider: &dyn TeeProvider,
agent_did: &str,
epoch: u64,
) -> Result<AgentKeyHandle> {
if !provider.is_available().await? {
return Err(TeeError::not_available(format!(
"TEE provider {:?} unavailable for agent_did={}",
provider.vendor(),
agent_did
)));
}
let user_data = {
let mut h = Sha256::new();
h.update(SALT_DOMAIN);
h.update(agent_did.as_bytes());
h.update(epoch.to_be_bytes());
h.finalize().to_vec()
};
let report = provider.generate_attestation(&user_data).await?;
let mut measurement = [0u8; 32];
if report.measurement.len() >= 32 {
measurement.copy_from_slice(&report.measurement[..32]);
} else {
let mut h = Sha256::new();
h.update(&report.measurement);
measurement.copy_from_slice(&h.finalize());
}
let mut ikm_buf = Zeroizing::new(Vec::<u8>::with_capacity(96));
ikm_buf.extend_from_slice(&measurement);
ikm_buf.extend_from_slice(&report.quote);
ikm_buf.extend_from_slice(report.id.as_bytes());
let handle = handle_from_ikm(agent_did, epoch, measurement, &ikm_buf)?;
Ok(handle)
}
pub async fn attest_agent_key(
provider: &dyn TeeProvider,
handle: &AgentKeyHandle,
) -> Result<AttestationReport> {
let packet = AgentKeyAttestationPacket::from_handle(handle);
let packed = pack_user_data_for_vendor(provider.vendor(), &packet.to_bytes());
provider.generate_attestation(&packed).await
}
pub fn pack_user_data_for_vendor(vendor: tenzro_types::tee::TeeVendor, packet: &[u8]) -> Vec<u8> {
use tenzro_types::tee::TeeVendor;
match vendor {
TeeVendor::IntelTdx | TeeVendor::AmdSevSnp | TeeVendor::AMDSEV | TeeVendor::IntelSGX => {
let mut h = Sha256::new();
h.update(packet);
let digest = h.finalize();
let mut out = vec![0u8; 64];
out[..32].copy_from_slice(&digest);
out[32..].copy_from_slice(&digest);
out
}
TeeVendor::AWSNitro
| TeeVendor::AwsNitro
| TeeVendor::NvidiaGpu
| TeeVendor::ARMTrustZone
| TeeVendor::Generic => packet.to_vec(),
}
}
pub async fn rotate_agent_key(
provider: &dyn TeeProvider,
previous: &AgentKeyHandle,
) -> Result<AgentKeyHandle> {
let next_epoch = previous
.epoch
.checked_add(1)
.ok_or_else(|| TeeError::internal("agent key rotation epoch overflow"))?;
let handle = seal_agent_keypair_epoch(provider, &previous.agent_did, next_epoch).await?;
if handle.pubkey() == previous.pubkey() {
return Err(TeeError::internal(
"rotate_agent_key produced identical pubkey across epochs",
));
}
Ok(handle)
}
#[cfg(test)]
mod tests {
use super::*;
fn fixed_handle(agent_did: &str, epoch: u64) -> AgentKeyHandle {
let measurement = {
let mut h = Sha256::new();
h.update(b"test-measurement");
h.update(agent_did.as_bytes());
let mut out = [0u8; 32];
out.copy_from_slice(&h.finalize());
out
};
handle_from_ikm(agent_did, epoch, measurement, &[0xABu8; 96]).unwrap()
}
#[test]
fn handle_is_deterministic_for_same_inputs() {
let a = fixed_handle("did:tenzro:machine:alice", 0);
let b = fixed_handle("did:tenzro:machine:alice", 0);
assert_eq!(a.pubkey(), b.pubkey());
}
#[test]
fn epoch_changes_key() {
let a = fixed_handle("did:tenzro:machine:alice", 0);
let b = fixed_handle("did:tenzro:machine:alice", 1);
assert_ne!(a.pubkey(), b.pubkey());
assert_eq!(a.epoch(), 0);
assert_eq!(b.epoch(), 1);
}
#[test]
fn did_changes_key() {
let a = fixed_handle("did:tenzro:machine:alice", 0);
let b = fixed_handle("did:tenzro:machine:bob", 0);
assert_ne!(a.pubkey(), b.pubkey());
}
#[test]
fn sign_and_verify_roundtrip() {
use ed25519_dalek::Verifier;
let handle = fixed_handle("did:tenzro:machine:alice", 0);
let msg = b"some bytes";
let sig = handle.sign(msg);
handle.verifying_key().verify(msg, &sig).expect("self-verify");
}
#[test]
fn sign_prehash_returns_64_bytes() {
let handle = fixed_handle("did:tenzro:machine:alice", 0);
let digest = [0x11u8; 32];
let sig = handle.sign_prehash(&digest);
assert_eq!(sig.len(), 64);
}
#[test]
fn clone_shares_secret_without_disturbing_origin() {
let handle = fixed_handle("did:tenzro:machine:alice", 0);
let clone = handle.clone();
let msg = b"hi";
let sig_a = handle.sign(msg);
let sig_b = clone.sign(msg);
assert_eq!(sig_a.to_bytes(), sig_b.to_bytes());
}
#[test]
fn attestation_packet_roundtrip() {
let handle = fixed_handle("did:tenzro:machine:alice", 7);
let packet = AgentKeyAttestationPacket::from_handle(&handle);
assert_eq!(packet.epoch, 7);
assert_eq!(packet.pubkey, handle.pubkey());
let bytes = packet.to_bytes();
assert_eq!(bytes.len(), 72);
let parsed = AgentKeyAttestationPacket::from_bytes(&bytes).unwrap();
assert_eq!(parsed, packet);
}
#[test]
fn pack_user_data_tdx_fits_64_bytes() {
use tenzro_types::tee::TeeVendor;
let packet = vec![0x55u8; 72];
let packed = pack_user_data_for_vendor(TeeVendor::IntelTdx, &packet);
assert_eq!(packed.len(), 64);
assert_eq!(packed[..32], packed[32..]);
}
#[test]
fn pack_user_data_nitro_is_verbatim() {
use tenzro_types::tee::TeeVendor;
let packet = vec![0x55u8; 72];
let packed = pack_user_data_for_vendor(TeeVendor::AWSNitro, &packet);
assert_eq!(packed, packet);
}
#[test]
fn debug_omits_secret() {
let handle = fixed_handle("did:tenzro:machine:alice", 0);
let debug = format!("{:?}", handle);
let secret_hex = hex::encode(handle.secret_scalar.as_slice());
assert!(!debug.contains(&secret_hex), "Debug leaked secret");
assert!(debug.contains(&hex::encode(handle.pubkey())));
}
}