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
29 changes: 17 additions & 12 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,34 @@ resolver = "2"
license = "MIT OR Apache-2.0"

[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_bytes = "0.11"
# Deps reused by vaportpm-attest's no_std/UEFI build carry default-features = false
# HERE, at the workspace level -- a member cannot override default-features when
# inheriting (Cargo ignores it), so it must be set once at the source. `alloc` (the
# no_std base each needs) is included; each crate layers `std` on via its own features
# (attest's `std`, verify directly), keeping versions consistent across both crates.
serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] }
serde_json = { version = "1.0", default-features = false, features = ["alloc"] }
serde_bytes = { version = "0.11", default-features = false, features = ["alloc"] }
serde-big-array = "0.5"
zerocopy = { version = "0.8", features = ["derive"] }
base64 = "0.22"
sha1 = "0.10"
sha2 = "0.10"
hmac = "0.12"
anyhow = "1.0"
base64 = { version = "0.22", default-features = false, features = ["alloc"] }
sha1 = { version = "0.10", default-features = false }
sha2 = { version = "0.10", default-features = false }
hmac = { version = "0.12", default-features = false }
anyhow = { version = "1.0", default-features = false }
thiserror = "1.0"
hex = "0.4"
hex = { version = "0.4", default-features = false, features = ["alloc"] }

# X.509 and crypto for verification
der = { version = "0.7", features = ["alloc", "pem", "oid"] }
der = { version = "0.7", default-features = false, features = ["alloc", "pem", "oid"] }
spki = { version = "0.7", features = ["alloc"] }
x509-cert = { version = "0.2", features = ["pem"] }
x509-cert = { version = "0.2", default-features = false, features = ["pem"] }
p256 = { version = "0.13", features = ["ecdsa", "pem", "pkcs8"] }
p384 = { version = "0.13", features = ["ecdsa", "pem", "pkcs8"] }
ecdsa = { version = "0.16", features = ["verifying", "der"] }
rsa = { version = "0.9", features = ["sha2"] }
coset = "0.3"
ciborium = "0.2"
ciborium = { version = "0.2", default-features = false }

# Time types for certificate validation
pki-types = { package = "rustls-pki-types", version = "=1.13.0", default-features = false, features = ["std"] }
Expand Down
96 changes: 60 additions & 36 deletions crates/vaportpm-attest/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,61 +1,84 @@
[package]
name = "vaportpm-attest"
version = "0.1.0"
version = "0.2.0"
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.
# All deps are inherited from the workspace, which pins versions and sets
# default-features = false for the no_std/UEFI build (a member cannot override
# default-features when inheriting). std/alloc are layered on via the features below.
[dependencies]
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"] }
# Core TPM 2.0 (always compiled).
anyhow = { workspace = true }
sha1 = { workspace = true }
sha2 = { workspace = true }
hmac = { workspace = true }
hex = { workspace = true }

# std-only dependencies (attestation/quote/cert paths) — gated behind the `std` feature
thiserror = { workspace = true, optional = true }
base64 = { workspace = true, optional = true }
# Attestation surface (quote + cert-chain assembly + unified-JSON output), enabled by
# `attest`. Inherits each crate's no_std + alloc base from the workspace.
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)
serde_bytes = { workspace = true, optional = true }
base64 = { workspace = true, optional = true }
der = { workspace = true, optional = true }
x509-cert = { workspace = true, optional = true }
# CBOR for the AWS Nitro NSM attestation document (reached via a TPM vendor command,
# so no_std-capable -- part of `attest`, NOT `std`).
ciborium = { workspace = true, optional = true }

# std-only error derive (CLI / device paths).
thiserror = { 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.
# On UEFI (soft-float, no SSE) force the portable software hash backends. The
# optimized SIMD/asm paths emit 128-bit vector ops LLVM cannot lower for
# x86_64-unknown-uefi. Features union with the base deps, so Linux keeps its 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"] }
sha1 = { workspace = true, features = ["force-soft"] }
sha2 = { workspace = true, 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.
default = ["std", "http-fetch"]

# The attestation surface: TPM2_Quote + AK + cert-chain assembly + unified-JSON
# output (modules a9n, cert, roots). no_std + alloc capable. A UEFI caller enables
# this WITHOUT std and supplies its own CertFetcher (EFI_TCG2 for the TPM,
# EFI_TCP4 HTTP for AIA intermediate fetches).
attest = [
"dep:serde",
"dep:serde_json",
"dep:serde_bytes",
"dep:base64",
"dep:der",
"dep:x509-cert",
"dep:ciborium",
]

# Built-in std HTTP CertFetcher (StdHttpFetcher, over std::net::TcpStream). Requires
# std; on by default. Disable (default-features = false) to drop std net I/O and
# supply your own CertFetcher.
http-fetch = ["std"]

# Full std build: TPM device I/O via /dev/tpm*, the CLI binary, Nitro NSM, and the
# attest surface. With `--no-default-features` only the no_std core remains: the
# TpmTransport trait, command marshalling and PCR ops (suitable for UEFI over
# EFI_TCG2_PROTOCOL); add `attest` for the quote/cert/JSON surface there too.
std = [
"attest",
"anyhow/std",
"hex/std",
"sha1/std",
"sha2/std",
"hmac/std",
"serde/std",
"serde_json/std",
"der/std",
"x509-cert/std",
"base64/std",
"ciborium/std",
"serde_bytes/std",
"dep:thiserror",
"dep:base64",
"dep:serde",
"dep:ciborium",
"dep:serde_bytes",
"dep:serde_json",
"dep:der",
"dep:x509-cert",
]

[lib]
Expand All @@ -65,4 +88,5 @@ path = "src/lib.rs"
[[bin]]
name = "vaportpm-attest"
path = "src/bin/attest.rs"
required-features = ["std"]
# The CLI calls attest(), which needs the built-in std HTTP fetcher.
required-features = ["http-fetch"]
57 changes: 36 additions & 21 deletions crates/vaportpm-attest/src/a9n.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@
//! - Reading PCR values
//! - Generating attestation documents

use alloc::collections::BTreeMap;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;
use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};

use crate::cert::{der_to_pem, fetch_cert_chain, DER_SEQUENCE_LONG};
use crate::cert::{der_to_pem, fetch_cert_chain, CertFetcher, DER_SEQUENCE_LONG};
use crate::{KeyOps, NsmOps, NvOps, PcrOps, PublicKey, Tpm, TPM_RH_ENDORSEMENT};

/// GCP AK certificate NV index (ECC)
Expand All @@ -22,7 +26,7 @@ const GCP_AK_TEMPLATE_NV_INDEX_ECC: u32 = 0x01c10003;
/// Result type for attestation helper functions
/// Contains: (ak_pubkeys, attestation_data, gcp_attestation, ak_handle)
type AttestResult = (
HashMap<String, EccPublicKeyCoords>,
BTreeMap<String, EccPublicKeyCoords>,
AttestationData,
Option<GcpAttestationData>,
Option<u32>,
Expand All @@ -33,9 +37,9 @@ type AttestResult = (
pub struct AttestationOutput {
/// Nonce/challenge used for this attestation (hex-encoded)
pub nonce: String,
pub pcrs: HashMap<String, BTreeMap<u8, String>>,
pub pcrs: BTreeMap<String, BTreeMap<u8, String>>,
/// Attestation Key public keys (hex-encoded ECC coordinates)
pub ak_pubkeys: HashMap<String, EccPublicKeyCoords>,
pub ak_pubkeys: BTreeMap<String, EccPublicKeyCoords>,
pub attestation: AttestationContainer,
}

Expand All @@ -49,7 +53,7 @@ pub struct EccPublicKeyCoords {
/// Container for both TPM and optional platform-specific attestations
#[derive(Debug, Serialize, Deserialize)]
pub struct AttestationContainer {
pub tpm: HashMap<String, AttestationData>,
pub tpm: BTreeMap<String, AttestationData>,
#[serde(skip_serializing_if = "Option::is_none")]
pub nitro: Option<NitroAttestationData>,
#[serde(skip_serializing_if = "Option::is_none")]
Expand Down Expand Up @@ -119,13 +123,14 @@ fn is_gcp_tpm(tpm: &mut Tpm) -> bool {
///
/// # Errors
/// Returns an error if the platform is not recognized (only AWS Nitro and GCP are supported)
pub fn attest(nonce: &[u8]) -> Result<String> {
let mut tpm = Tpm::open_direct()?;

/// Produce the unified attestation JSON for an already-open TPM, using `fetcher` to
/// retrieve any AIA intermediate certificates. no_std + alloc: a UEFI caller passes a
/// `Tpm` over EFI_TCG2 and a `CertFetcher` over EFI_TCP4.
pub fn attest_with(tpm: &mut Tpm, nonce: &[u8], fetcher: &dyn CertFetcher) -> Result<String> {
// Step 1: Detect platform
// GCP detection is cheap - just checks for NV index existence
let is_nitro = tpm.is_nitro_tpm()?;
let is_gcp = !is_nitro && is_gcp_tpm(&mut tpm);
let is_gcp = !is_nitro && is_gcp_tpm(tpm);

// Step 2: Read all allocated PCRs from all banks
let all_pcrs = tpm.read_all_allocated_pcrs()?;
Expand All @@ -150,7 +155,7 @@ pub fn attest(nonce: &[u8]) -> Result<String> {
}

// Build PCRs output
let mut pcrs_by_alg: HashMap<String, BTreeMap<u8, String>> = HashMap::new();
let mut pcrs_by_alg: BTreeMap<String, BTreeMap<u8, String>> = BTreeMap::new();
let pcr_map = pcrs_by_alg.entry(pcr_alg.name().to_string()).or_default();
for (idx, value) in &pcr_values {
pcr_map.insert(*idx, hex::encode(value));
Expand All @@ -159,26 +164,28 @@ pub fn attest(nonce: &[u8]) -> Result<String> {
// Step 5: Create or retrieve AK and sign PCRs with TPM2_Quote
let (signing_key_public_keys, attestation_data, gcp_attestation, ak_handle) = if is_gcp {
// GCP path: recreate AK from Google's template
attest_gcp(&mut tpm, nonce, &pcr_values, pcr_alg)?
attest_gcp(tpm, nonce, &pcr_values, pcr_alg, fetcher)?
} else if is_nitro {
// Nitro path: create long-term AK, use TPM2_Quote
// SHA-384 is hardcoded — the Quote must attest the same PCR bank that
// the Nitro NSM document signs, so they can be cross-verified.
attest_nitro(&mut tpm, nonce, &pcr_values)?
attest_nitro(tpm, nonce, &pcr_values)?
} else {
return Err(anyhow!(
"Unknown platform - only AWS Nitro and GCP Shielded VM are supported"
));
};

let mut tpm_attestations = HashMap::new();
let mut tpm_attestations = BTreeMap::new();
tpm_attestations.insert("ecc_p256".to_string(), attestation_data);

// Step 6: Get Nitro attestation if on AWS
// Step 6: Get the Nitro NSM document if on AWS (binds the AK pubkey to the Nitro
// root). This rides the TPM vendor command (0x20000001), so it works no_std/UEFI.
let nitro_attestation = if is_nitro {
if let Some(pk) = signing_key_public_keys.get("ecc_p256") {
let public_key_hex = format!("04{}{}", pk.x, pk.y);
let public_key_bytes = hex::decode(&public_key_hex)?;
let public_key_bytes = hex::decode(&public_key_hex)
.map_err(|e| anyhow!("invalid AK public key hex: {e}"))?;

match tpm.nsm_attest(
None, // user_data
Expand Down Expand Up @@ -221,6 +228,13 @@ pub fn attest(nonce: &[u8]) -> Result<String> {
Ok(json)
}

/// Convenience std entrypoint: open /dev/tpm0 and use the built-in HTTP fetcher.
#[cfg(feature = "http-fetch")]
pub fn attest(nonce: &[u8]) -> Result<String> {
let mut tpm = Tpm::open_direct()?;
attest_with(&mut tpm, nonce, &crate::cert::StdHttpFetcher)
}

/// Nitro attestation path: create restricted AK and use TPM2_Quote
///
/// Creates a TCG-compliant restricted AK in the endorsement hierarchy, then uses
Expand All @@ -234,7 +248,7 @@ fn attest_nitro(tpm: &mut Tpm, nonce: &[u8], pcr_values: &[(u8, Vec<u8>)]) -> Re
// Trust comes from Nitro NSM document binding the AK public key
let signing_key = tpm.create_restricted_ak(TPM_RH_ENDORSEMENT)?;

let mut signing_key_public_keys = HashMap::new();
let mut signing_key_public_keys = BTreeMap::new();
signing_key_public_keys.insert(
"ecc_p256".to_string(),
EccPublicKeyCoords {
Expand Down Expand Up @@ -271,6 +285,7 @@ fn attest_gcp(
nonce: &[u8],
pcr_values: &[(u8, Vec<u8>)],
pcr_alg: crate::TpmAlg,
fetcher: &dyn CertFetcher,
) -> Result<AttestResult> {
// Read ECC AK template from NV RAM (prefer ECC over RSA for ECDSA signing)
let ak_template = tpm.nv_read(GCP_AK_TEMPLATE_NV_INDEX_ECC)?;
Expand All @@ -281,7 +296,7 @@ fn attest_gcp(
// Extract ECC public key coordinates
let signing_key_public_keys = match &ak_result.public_key {
PublicKey::Ecc(ecc) => {
let mut pks = HashMap::new();
let mut pks = BTreeMap::new();
pks.insert(
"ecc_p256".to_string(),
EccPublicKeyCoords {
Expand All @@ -307,7 +322,7 @@ fn attest_gcp(
let quote_result = tpm.quote(ak_result.handle, nonce, &pcr_selection)?;

// Read AK certificate chain from NV RAM
let ak_cert_chain = read_gcp_ak_cert_chain(tpm)?;
let ak_cert_chain = read_gcp_ak_cert_chain(tpm, fetcher)?;

let attestation_data = AttestationData {
attest_data: hex::encode(&quote_result.attest_data),
Expand Down Expand Up @@ -339,7 +354,7 @@ fn build_pcr_bitmap(pcr_values: &[(u8, Vec<u8>)]) -> Vec<u8> {
}

/// Read GCP ECC AK certificate chain from NV RAM and fetch issuer certs
fn read_gcp_ak_cert_chain(tpm: &mut Tpm) -> Result<String> {
fn read_gcp_ak_cert_chain(tpm: &mut Tpm, fetcher: &dyn CertFetcher) -> Result<String> {
// Read ECC AK certificate (matches the ECC AK template we use)
let ak_cert = tpm.nv_read(GCP_AK_CERT_NV_INDEX_ECC)?;

Expand All @@ -351,7 +366,7 @@ fn read_gcp_ak_cert_chain(tpm: &mut Tpm) -> Result<String> {
}

// Build full chain by fetching issuer certs via AIA
let chain = fetch_cert_chain(&ak_cert)?;
let chain = fetch_cert_chain(&ak_cert, fetcher)?;

// Convert all certs to PEM and concatenate
let pem_chain: String = chain
Expand Down
Loading
Loading