Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 51 additions & 14 deletions crates/vaportpm-attest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -29,3 +65,4 @@ path = "src/lib.rs"
[[bin]]
name = "vaportpm-attest"
path = "src/bin/attest.rs"
required-features = ["std"]
2 changes: 2 additions & 0 deletions crates/vaportpm-attest/src/ek.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down
144 changes: 118 additions & 26 deletions crates/vaportpm-attest/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<u8> {
let total_size = 10 + self.data.len(); // header is 10 bytes
let mut result = Vec::new();
Expand Down Expand Up @@ -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<Vec<u8>>;
}

impl Tpm {
/// Open the TPM device (defaults to /dev/tpmrm0)
pub fn open() -> Result<Self> {
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<Self> {
let device = OpenOptions::new()
Expand All @@ -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> {
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<ResponseBuffer> {
#[cfg(feature = "std")]
impl TpmTransport for FileTransport {
fn transmit_raw(&mut self, command: &[u8]) -> Result<Vec<u8>> {
// Write command
self.device
.write_all(command)
Expand All @@ -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<dyn TpmTransport>,
}

impl Tpm {
/// Open the TPM device (defaults to /dev/tpmrm0)
#[cfg(feature = "std")]
pub fn open() -> Result<Self> {
Self::open_path("/dev/tpmrm0")
}

/// Open a specific TPM device path
#[cfg(feature = "std")]
pub fn open_path(path: &str) -> Result<Self> {
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> {
Self::open_path("/dev/tpm0")
}

/// Build a TPM context over an arbitrary transport (e.g. UEFI TCG2).
pub fn with_transport(transport: Box<dyn TpmTransport>) -> 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<ResponseBuffer> {
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)
Expand Down
3 changes: 2 additions & 1 deletion crates/vaportpm-attest/src/nv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions crates/vaportpm-attest/src/pcr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
Loading