diff --git a/crates/vaportpm-attest/Cargo.toml b/crates/vaportpm-attest/Cargo.toml index c3e88f6..3811791 100644 --- a/crates/vaportpm-attest/Cargo.toml +++ b/crates/vaportpm-attest/Cargo.toml @@ -5,22 +5,58 @@ edition = "2021" description = "Cloud vTPM attestation - minimal TPM 2.0 implementation without C dependencies" license.workspace = true +# no_std-capable core dependencies (always compiled). +# Declared directly (not via workspace) so default-features can be disabled, +# which is required for the no_std/UEFI build. [dependencies] -anyhow = { workspace = true } -thiserror = { workspace = true } -sha1 = { workspace = true } -sha2 = { workspace = true } -hmac = { workspace = true } -base64 = { workspace = true } -serde = { workspace = true } -ciborium = { workspace = true } -serde_bytes = { workspace = true } -hex = { workspace = true } -serde_json = { workspace = true } +anyhow = { version = "1.0", default-features = false } +sha1 = { version = "0.10", default-features = false } +sha2 = { version = "0.10", default-features = false } +hmac = { version = "0.12", default-features = false } +hex = { version = "0.4", default-features = false, features = ["alloc"] } -# X.509 certificate parsing -der = { workspace = true } -x509-cert = { workspace = true } +# std-only dependencies (attestation/quote/cert paths) — gated behind the `std` feature +thiserror = { workspace = true, optional = true } +base64 = { workspace = true, optional = true } +serde = { workspace = true, optional = true } +ciborium = { workspace = true, optional = true } +serde_bytes = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } + +# X.509 certificate parsing (std-only path) +der = { workspace = true, optional = true } +x509-cert = { workspace = true, optional = true } + +# On UEFI (soft-float, no SSE) force the portable software hash backends. +# The optimized SIMD/asm paths emit 128-bit vector ops that LLVM cannot lower +# for x86_64-unknown-uefi. Cargo unions these features with the base deps, so +# the Linux build keeps its native fast path. +[target.'cfg(target_os = "uefi")'.dependencies] +sha1 = { version = "0.10", default-features = false, features = ["force-soft"] } +sha2 = { version = "0.10", default-features = false, features = ["force-soft"] } + +[features] +default = ["std"] +# `std` enables the full attestation surface: TPM device I/O via /dev/tpm*, the +# quote/AK/cert-chain paths (a9n, cert, roots, nsm) and the CLI binary. +# With `--no-default-features` only the no_std core remains: the TpmTransport +# trait, command marshalling and PCR operations (e.g. pcr_extend), suitable for +# UEFI use over EFI_TCG2_PROTOCOL. +std = [ + "anyhow/std", + "hex/std", + "sha1/std", + "sha2/std", + "hmac/std", + "dep:thiserror", + "dep:base64", + "dep:serde", + "dep:ciborium", + "dep:serde_bytes", + "dep:serde_json", + "dep:der", + "dep:x509-cert", +] [lib] name = "vaportpm_attest" @@ -29,3 +65,4 @@ path = "src/lib.rs" [[bin]] name = "vaportpm-attest" path = "src/bin/attest.rs" +required-features = ["std"] diff --git a/crates/vaportpm-attest/src/ek.rs b/crates/vaportpm-attest/src/ek.rs index 105578b..bbd6b73 100644 --- a/crates/vaportpm-attest/src/ek.rs +++ b/crates/vaportpm-attest/src/ek.rs @@ -7,6 +7,8 @@ //! - Creating keys from templates (GCP AK) //! - TPM2_Quote for PCR attestation +use alloc::vec; +use alloc::vec::Vec; use anyhow::{bail, Result}; use crate::{ diff --git a/crates/vaportpm-attest/src/lib.rs b/crates/vaportpm-attest/src/lib.rs index 9267f3c..92efee3 100644 --- a/crates/vaportpm-attest/src/lib.rs +++ b/crates/vaportpm-attest/src/lib.rs @@ -2,28 +2,52 @@ //! Minimal TPM 2.0 protocol implementation //! -//! Direct communication with TPM via /dev/tpmrm0 without any C dependencies. -//! Based on TPM 2.0 specification for command/response protocol. +//! TPM 2.0 command/response marshalling without any C dependencies. +//! +//! TPM I/O is abstracted behind the [`TpmTransport`] trait. With the default +//! `std` feature, [`Tpm::open`] talks to a Linux TPM device (`/dev/tpmrm0`). +//! With `--no-default-features` the crate is `no_std` (requires `alloc`) and +//! exposes only the transport-agnostic core (command marshalling + PCR ops), +//! letting a UEFI caller supply a transport over `EFI_TCG2_PROTOCOL`. + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; -use anyhow::{bail, Context, Result}; +use alloc::boxed::Box; +use alloc::vec::Vec; +use anyhow::{bail, Result}; + +#[cfg(feature = "std")] +use anyhow::Context; +#[cfg(feature = "std")] use std::fs::{File, OpenOptions}; +#[cfg(feature = "std")] use std::io::{Read, Write}; +#[cfg(feature = "std")] pub mod a9n; +#[cfg(feature = "std")] pub mod cert; pub mod ek; +#[cfg(feature = "std")] pub mod nsm; pub mod nv; pub mod pcr; +#[cfg(feature = "std")] pub mod roots; // Re-export extension traits for convenience pub use ek::KeyOps; -pub use nsm::NsmOps; pub use nv::NvOps; pub use pcr::PcrOps; +#[cfg(feature = "std")] +pub use nsm::NsmOps; + +#[cfg(feature = "std")] pub use a9n::attest; +#[cfg(feature = "std")] pub use cert::{der_to_pem, extract_aki, extract_ski, pem_to_der}; /// TPM 2.0 command codes @@ -344,6 +368,7 @@ impl CommandBuffer { } /// Finalize command with a vendor-specific command code + #[cfg(feature = "std")] fn finalize_vendor(mut self, tag: TpmSt, vendor_code: u32) -> Vec { let total_size = 10 + self.data.len(); // header is 10 bytes let mut result = Vec::new(); @@ -430,17 +455,28 @@ impl ResponseBuffer { } } -/// TPM 2.0 device context -pub struct Tpm { - device: File, +/// Transport for exchanging raw, fully-framed TPM 2.0 messages. +/// +/// Implementors move bytes to/from the TPM by whatever mechanism is available: +/// a Linux device file (see [`FileTransport`]) under `std`, or +/// `EFI_TCG2_PROTOCOL.SubmitCommand` in a UEFI environment. The implementor is +/// responsible only for delivering the command and returning the complete +/// response (10-byte header + body); header validation and response-code +/// checking are handled by the [`Tpm`] wrapper. +pub trait TpmTransport { + /// Send a fully-framed TPM command and return the complete response bytes + /// (TPM response header followed by its body). + fn transmit_raw(&mut self, command: &[u8]) -> Result>; } -impl Tpm { - /// Open the TPM device (defaults to /dev/tpmrm0) - pub fn open() -> Result { - Self::open_path("/dev/tpmrm0") - } +/// Linux TPM device-file transport (`/dev/tpmrm0` or `/dev/tpm0`). +#[cfg(feature = "std")] +pub struct FileTransport { + device: File, +} +#[cfg(feature = "std")] +impl FileTransport { /// Open a specific TPM device path pub fn open_path(path: &str) -> Result { let device = OpenOptions::new() @@ -451,16 +487,11 @@ impl Tpm { Ok(Self { device }) } +} - /// Open direct TPM device (/dev/tpm0) - required for vendor commands - pub fn open_direct() -> Result { - Self::open_path("/dev/tpm0") - } - - /// Send a command and receive response - /// - /// Returns a ResponseBuffer containing the response body (without the header) - pub(crate) fn transmit(&mut self, command: &[u8]) -> Result { +#[cfg(feature = "std")] +impl TpmTransport for FileTransport { + fn transmit_raw(&mut self, command: &[u8]) -> Result> { // Write command self.device .write_all(command) @@ -472,26 +503,87 @@ impl Tpm { .read_exact(&mut header_buf) .context("Failed to read TPM response header")?; - // Parse response header let header = TpmResponseHeader::from_bytes(&header_buf); - if header.size < 10 { bail!("Invalid TPM response size: {}", header.size); } - // Read response body (excluding header) + // Read response body (excluding header) and return the full response let body_size = header.size as usize - 10; - let mut body = vec![0u8; body_size]; + let mut response = Vec::with_capacity(header.size as usize); + response.extend_from_slice(&header_buf); + let mut body = alloc::vec![0u8; body_size]; self.device .read_exact(&mut body) .context("Failed to read TPM response body")?; + response.extend_from_slice(&body); + + Ok(response) + } +} + +/// TPM 2.0 context — marshals commands and dispatches them over a [`TpmTransport`]. +pub struct Tpm { + transport: Box, +} + +impl Tpm { + /// Open the TPM device (defaults to /dev/tpmrm0) + #[cfg(feature = "std")] + pub fn open() -> Result { + Self::open_path("/dev/tpmrm0") + } + + /// Open a specific TPM device path + #[cfg(feature = "std")] + pub fn open_path(path: &str) -> Result { + Ok(Self::with_transport(Box::new(FileTransport::open_path( + path, + )?))) + } + + /// Open direct TPM device (/dev/tpm0) - required for vendor commands + #[cfg(feature = "std")] + pub fn open_direct() -> Result { + Self::open_path("/dev/tpm0") + } + + /// Build a TPM context over an arbitrary transport (e.g. UEFI TCG2). + pub fn with_transport(transport: Box) -> Self { + Self { transport } + } + + /// Send a command and receive response + /// + /// Returns a ResponseBuffer containing the response body (without the header) + pub(crate) fn transmit(&mut self, command: &[u8]) -> Result { + let response = self.transport.transmit_raw(command)?; + + if response.len() < 10 { + bail!( + "Invalid TPM response: {} bytes (need at least 10)", + response.len() + ); + } + + // Parse response header (first 10 bytes) + let mut header_buf = [0u8; 10]; + header_buf.copy_from_slice(&response[..10]); + let header = TpmResponseHeader::from_bytes(&header_buf); + + if (header.size as usize) < 10 || header.size as usize > response.len() { + bail!("Invalid TPM response size: {}", header.size); + } // Check response code if header.code != TpmRc::Success as u32 { bail!("TPM command failed with code: 0x{:08X}", header.code); } - Ok(ResponseBuffer::new(body)) + // Return the response body (excluding the 10-byte header) + Ok(ResponseBuffer::new( + response[10..header.size as usize].to_vec(), + )) } /// Flush a context (close a handle) diff --git a/crates/vaportpm-attest/src/nv.rs b/crates/vaportpm-attest/src/nv.rs index 82605f3..7d11d96 100644 --- a/crates/vaportpm-attest/src/nv.rs +++ b/crates/vaportpm-attest/src/nv.rs @@ -6,6 +6,7 @@ use crate::{CommandBuffer, Tpm, TpmCc, TpmSt}; use crate::{TPM_CAP_HANDLES, TPM_RH_OWNER}; +use alloc::vec::Vec; use anyhow::{bail, Result}; /// TPM handle types @@ -235,7 +236,7 @@ impl NvOps for Tpm { let mut offset = 0usize; while offset < data.len() { - let chunk_size = std::cmp::min(MAX_CHUNK, data.len() - offset); + let chunk_size = core::cmp::min(MAX_CHUNK, data.len() - offset); let chunk = &data[offset..offset + chunk_size]; let command = CommandBuffer::new() diff --git a/crates/vaportpm-attest/src/pcr.rs b/crates/vaportpm-attest/src/pcr.rs index 0c7e88b..0df203a 100644 --- a/crates/vaportpm-attest/src/pcr.rs +++ b/crates/vaportpm-attest/src/pcr.rs @@ -5,6 +5,7 @@ //! Extension trait providing PCR-related functionality for TPM. use crate::{CommandBuffer, Tpm, TpmAlg, TpmCc, TpmSt, TPM_CAP_PCRS}; +use alloc::vec::Vec; use anyhow::{bail, Result}; use sha1::Sha1; use sha2::{Digest, Sha256, Sha384, Sha512};