diff --git a/Cargo.toml b/Cargo.toml
index 9f1f160..59cb053 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -24,12 +24,11 @@ x509-cert = { version = "0.2", 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"
-# Certificate chain validation (rustls ecosystem)
-rustls-rustcrypto = { version = "=0.0.2-alpha", default-features = false, features = ["std", "zeroize"] }
-webpki = { package = "rustls-webpki", version = "=0.102.8", default-features = false, features = ["alloc"] }
+# Time types for certificate validation
pki-types = { package = "rustls-pki-types", version = "=1.13.0", default-features = false, features = ["std"] }
[profile.release]
diff --git a/Makefile b/Makefile
index 69bdd34..43e56ad 100644
--- a/Makefile
+++ b/Makefile
@@ -8,6 +8,9 @@ build:
release-x86:
cargo build --workspace --release --target x86_64-unknown-linux-musl
+release-aarch64:
+ cargo build --workspace --release --target aarch64-unknown-linux-musl
+
check:
cargo check --workspace --all-targets
diff --git a/README.md b/README.md
index f9feb82..8f0327e 100644
--- a/README.md
+++ b/README.md
@@ -1,19 +1,10 @@
# v[apor]TPM
-> What does the "v" in vTPM stand for?
-
-Cloud vTPM attestation library for Rust. Zero C dependencies.
-
-## The Name
+**Cloud vTPM attestation library for Rust. Zero C dependencies.**
-```
-vTPM β v[apor]TPM
-lockboot β [g]lockboot
-```
-
-Physical TPM trust is vapor. It evaporates under scrutiny - supply chain attacks, firmware vulnerabilities, the whole theater. The only meaningful TPM trust lives in cloud vTPMs, where the hypervisor **is** the root of trust.
+> What does the "v" in vTPM stand for?
-The "v" always stood for vapor. Everyone just forgot.
+Physical TPM trust is vapor. It evaporates under scrutiny - supply chain attacks, firmware vulnerabilities, the whole theater. The only meaningful TPM trust lives in cloud vTPMs, where the hypervisor **is** the root of trust. The "v" always stood for vapor. Everyone just forgot.
## Crates
@@ -36,10 +27,12 @@ You handle **policy decisions**:
| Platform | Status | Trust Anchor |
|----------|--------|--------------|
-| AWS Nitro | β
Working | Nitro Root CA |
-| GCP Shielded VM | π Planned | Google AK certificate |
+| AWS EC2 with Nitro v4+ | β
Working | Nitro Root CA |
+| GCP Confidential VM | β
Working | Google EK/AK CA Root |
| Azure Trusted Launch | π Planned | Microsoft AK certificate |
+Please note that GCP 'Shielded VM' with vTPM isn't enough, a 'Confidential VM' is necessary as Google doesn't provision AK certificates without that feature enabled (be it Intel TDX or AMD SEV)
+
## Quick Start
### Generate Attestation (on cloud instance)
@@ -54,15 +47,12 @@ let json = attest(b"challenge-nonce")?;
### Verify Attestation (anywhere)
```rust
-use vaportpm_verify::{verify_attestation_json, VerificationResult};
+use vaportpm_verify::verify_attestation_json;
let result = verify_attestation_json(&json)?;
-
-// Check the trust root is acceptable
-if result.root_pubkey_hash == KNOWN_AWS_NITRO_ROOT_HASH {
- println!("Verified via: {:?}", result.method);
- println!("Nonce: {}", result.nonce);
-}
+// Verification succeeded - attestation is from a supported cloud provider
+println!("Provider: {:?}", result.provider);
+println!("PCRs: {:?}", result.pcrs);
```
## License
diff --git a/crates/vaportpm-attest/AWS-NITRO.md b/crates/vaportpm-attest/AWS-NITRO.md
index 574f4dc..7ceb51e 100644
--- a/crates/vaportpm-attest/AWS-NITRO.md
+++ b/crates/vaportpm-attest/AWS-NITRO.md
@@ -16,9 +16,9 @@ By combining these, we get a chain of trust from AWS hardware to arbitrary appli
```mermaid
flowchart TD
A["AWS Nitro Root CA
Verification returns root pubkey hash"]
- B["Nitro Attestation Document
COSE Sign1 structure containing:
β’ nitrotpm_pcrs: SHA-384 PCR values
β’ public_key: Application key binding
β’ nonce: Freshness proof
β’ Certificate chain to AWS root"]
- C["TPM Attestation Key (AK)
β’ ECC P-256 signing key
β’ authPolicy = PolicyPCR(SHA-384 bank)
β’ Can only sign when PCRs match"]
- D["TPM2B_ATTEST
β’ extraData: nonce (matches Nitro)
β’ certifiedName: includes authPolicy
β’ Proves: AK bound to PCR values"]
+ B["Nitro Attestation Document
COSE Sign1 structure containing:
β’ nitrotpm_pcrs: SHA-384 PCR values
β’ public_key: AK public key binding
β’ nonce: Freshness proof
β’ Certificate chain to AWS root"]
+ C["TPM Attestation Key (AK)
β’ ECC P-256 signing key
β’ Long-term key (no PCR binding)
β’ Bound to Nitro document via public_key"]
+ D["TPM2_Quote (TPMS_ATTEST)
β’ extraData: nonce (matches Nitro)
β’ pcrDigest: hash of quoted PCRs
β’ Proves: PCR values at quote time"]
A -->|"Signs (ECDSA P-384)"| B
B -->|"public_key field binds"| C
@@ -29,9 +29,9 @@ flowchart TD
The Nitro document signs `nitrotpm_pcrs` which contains **SHA-384** PCR values. For a coherent chain of trust:
-- The AK's `authPolicy` references the SHA-384 PCR bank
-- Verification computes policy from SHA-384 PCRs
-- The signed Nitro PCRs match what the AK is bound to
+- TPM2_Quote includes SHA-384 PCRs in the signed attestation
+- The Nitro document contains signed SHA-384 PCR values
+- Verification compares the Quote's PCRs against the signed Nitro values
This ensures a single, verifiable path from AWS hardware to the attested data.
@@ -40,26 +40,21 @@ This ensures a single, verifiable path from AWS hardware to the attested data.
### Generation (on Nitro instance)
```rust
-// 1. Detect Nitro and choose PCR bank
+// 1. Detect Nitro and read PCR values (SHA-384)
let is_nitro = tpm.is_nitro_tpm()?;
-let pcr_alg = if is_nitro { TpmAlg::Sha384 } else { TpmAlg::Sha256 };
+let pcr_values = tpm.read_all_allocated_pcrs()?; // Reads SHA-384 bank
-// 2. Read PCR values from chosen bank
-let pcr_values = tpm.read_pcrs(pcr_alg)?;
+// 2. Create restricted AK in endorsement hierarchy (TCG-compliant AK profile)
+let ak = tpm.create_restricted_ak(TPM_RH_ENDORSEMENT)?;
-// 3. Compute PCR policy digest
-let auth_policy = Tpm::calculate_pcr_policy_digest(&pcr_values, pcr_alg)?;
+// 3. Quote PCRs with AK (signs PCR values)
+// Note: nonce is caller-provided for freshness/replay protection
+let quote_result = tpm.quote(ak.handle, &nonce, &pcr_selection)?;
-// 4. Create AK bound to this policy
-let ak = tpm.create_primary_ecc_key_with_policy(TPM_RH_OWNER, &auth_policy)?;
-
-// 5. AK self-certifies (proves it exists with this policy)
-let certify_result = tpm.certify(ak.handle, ak.handle, &nonce)?;
-
-// 6. Get Nitro attestation binding the AK public key
+// 4. Get Nitro attestation binding the AK public key
let nitro_doc = tpm.nsm_attest(
None, // user_data
- Some(nonce.to_vec()), // nonce (same as TPM)
+ Some(nonce.to_vec()), // nonce (same as TPM Quote)
Some(ak_public_key_secg), // public_key (binds AK)
)?;
```
@@ -76,20 +71,14 @@ let nitro_result = verify_nitro_attestation(&nitro_doc)?;
// 3. Verify AK public key matches Nitro's public_key binding
assert!(ak_pubkey == nitro_result.document.public_key);
-// 4. Verify TPM signature over TPM2B_ATTEST
-verify_ecdsa_p256(&attest_data, &signature, &ak_pubkey)?;
-
-// 5. Compute expected AK name from PCR policy
-let policy = calculate_pcr_policy(&sha384_pcrs, TpmAlg::Sha384)?;
-let expected_name = compute_ecc_p256_name(&ak_x, &ak_y, &policy);
-
-// 6. Verify certified name matches (proves PCR binding)
-assert!(attest_info.certified_name == expected_name);
+// 4. Verify TPM Quote signature
+verify_ecdsa_p256("e_attest_data, &signature, &ak_pubkey)?;
-// 7. Verify nonces match (proves freshness and binding)
-assert!(tpm_nonce == nitro_nonce);
+// 5. Verify nonces match (proves freshness and binding)
+let quote_info = parse_quote_attest("e_attest_data)?;
+assert!(quote_info.nonce == nitro_nonce);
-// 8. Verify PCR values match signed Nitro document
+// 6. Verify PCR values match signed Nitro document
assert!(output.pcrs["sha384"] == nitro_result.document.pcrs);
```
@@ -99,11 +88,11 @@ assert!(output.pcrs["sha384"] == nitro_result.document.pcrs);
1. **Hardware Root of Trust** - The Nitro document is signed by AWS hardware (certificate chain to AWS root CA)
-2. **PCR Integrity** - The SHA-384 PCR values in the attestation match what AWS hardware measured
+2. **PCR Integrity** - The SHA-384 PCR values in the attestation match what AWS hardware measured (signed in Nitro document)
-3. **Key Binding** - The AK is bound to specific PCR values via `authPolicy` (it cannot sign unless PCRs match)
+3. **Key Binding** - The AK is bound to the Nitro document via the `public_key` field
-4. **Freshness** - The nonce in both TPM and Nitro attestations proves the attestation is fresh
+4. **Freshness** - The nonce in both TPM Quote and Nitro document proves the attestation is fresh
5. **AK Authenticity** - The Nitro document's `public_key` field proves the AK belongs to this Nitro instance
@@ -193,7 +182,7 @@ COSE_Sign1 = [
}
```
-Note: Only SHA-384 PCRs are included because that's the bank bound to the AK and signed in the Nitro document.
+Note: Only SHA-384 PCRs are included because that's the bank signed in the Nitro document.
## References
diff --git a/crates/vaportpm-attest/CLAUDE.md b/crates/vaportpm-attest/CLAUDE.md
deleted file mode 100644
index c2d319e..0000000
--- a/crates/vaportpm-attest/CLAUDE.md
+++ /dev/null
@@ -1,30 +0,0 @@
-# vaportpm-attest
-
-Minimal TPM 2.0 protocol implementation in pure Rust without C dependencies.
-
-## Build
-
-Debug build:
-```bash
-cargo build
-```
-
-For deployment, we target static musl binaries:
-```bash
-cargo build --target x86_64-unknown-linux-musl
-```
-
-## Architecture
-
-- Direct communication with TPM via /dev/tpmrm0 (resource manager) or /dev/tpm0 (direct)
-- No tpm2-tss or other C library dependencies
-- Implements TPM 2.0 command/response protocol per TCG specification
-
-## Key Modules
-
-- `credential.rs` - Policy session operations and TPM object name computation
-- `ek.rs` - EK and signing key operations
-- `pcr.rs` - PCR read/extend operations
-- `a9n.rs` - Attestation generation
-- `nv.rs` - NV RAM read/write operations
-- `nsm.rs` - AWS Nitro-specific vendor commands
diff --git a/crates/vaportpm-attest/Cargo.toml b/crates/vaportpm-attest/Cargo.toml
index 33b5183..c3e88f6 100644
--- a/crates/vaportpm-attest/Cargo.toml
+++ b/crates/vaportpm-attest/Cargo.toml
@@ -18,6 +18,14 @@ serde_bytes = { workspace = true }
hex = { workspace = true }
serde_json = { workspace = true }
+# X.509 certificate parsing
+der = { workspace = true }
+x509-cert = { workspace = true }
+
[lib]
name = "vaportpm_attest"
path = "src/lib.rs"
+
+[[bin]]
+name = "vaportpm-attest"
+path = "src/bin/attest.rs"
diff --git a/crates/vaportpm-attest/GCP.md b/crates/vaportpm-attest/GCP.md
new file mode 100644
index 0000000..8186a6d
--- /dev/null
+++ b/crates/vaportpm-attest/GCP.md
@@ -0,0 +1,198 @@
+# GCP Confidential VM Attestation
+
+This document describes how GCP Confidential VMs perform TPM-based attestation using
+Attestation Key (AK) certificates.
+
+## Overview
+
+**Important**: Only GCP Confidential VMs receive AK certificates from Google's CA
+hierarchy. Standard Shielded VMs (with Secure Boot enabled but no confidential
+computing) have a vTPM but do NOT receive Google-signed AK certificates.
+
+GCP Confidential VMs have a virtual TPM (vTPM) with an Attestation Key (AK) that is
+certified by Google's EK/AK CA hierarchy. The attestation flow is:
+
+1. VM requests an AK certificate from Google's metadata service
+2. Google issues a certificate binding the AK public key to the VM's identity
+3. The VM uses the AK to sign TPM2_Quote attestations
+4. Verifiers validate the quote signature using the certificate chain
+
+## Certificate Chain Structure
+
+```
+EK/AK CA Root (self-signed, offline)
+ β
+ βββ EK/AK CA Intermediate (online issuer)
+ β
+ βββ AK Certificate (per-VM, short-lived)
+```
+
+### Root CA Certificate
+
+- **Subject/Issuer**: `CN=EK/AK CA Root, OU=Google Cloud, O=Google LLC, L=Mountain View, ST=California, C=US`
+- **Validity**: ~100 years (July 2022 - July 2122)
+- **Key Type**: RSA 4096-bit
+- **Basic Constraints**: `CA:TRUE`
+- **Key Usage**: `Certificate Sign, CRL Sign`
+- **Extended Key Usage**: TPM EK Certificate (`2.23.133.8.1`)
+
+### Intermediate CA Certificate
+
+- **Subject**: `CN=EK/AK CA Intermediate, OU=Google Cloud, O=Google LLC, L=Mountain View, ST=California, C=US`
+- **Issuer**: Root CA
+- **Validity**: ~98 years
+- **Key Type**: RSA 4096-bit
+- **Basic Constraints**: `CA:TRUE`
+- **Key Usage**: `Certificate Sign, CRL Sign`
+- **Extended Key Usage**: TPM EK Certificate (`2.23.133.8.1`)
+
+### AK Leaf Certificate
+
+- **Subject**: `CN=, OU=, O=Google Compute Engine, L=`
+- **Issuer**: Intermediate CA
+- **Validity**: 30 years (per-instance)
+- **Key Type**: ECDSA P-256
+- **Basic Constraints**: `CA:FALSE` (critical)
+- **Key Usage**: `Digital Signature` (critical)
+- **Extended Key Usage**: None (only Key Usage is present)
+
+## AK Template (NV RAM)
+
+Google provisions an AK template at NV index `0x01c10003` (ECC) that defines the key properties.
+The template is a `TPMT_PUBLIC` structure:
+
+```
+type: ecc (0x23)
+nameAlg: sha256 (0x0b)
+attributes: fixedtpm|fixedparent|sensitivedataorigin|userwithauth|restricted|sign (0x50072)
+scheme: ecdsa with sha256
+curve: NIST P-256
+unique:
+```
+
+Key properties:
+
+| Attribute | Meaning |
+|-----------|---------|
+| `restricted` | Can only sign TPM-generated structures (Quotes, certifications) |
+| `sign` | Signing key (no decrypt capability) |
+| `fixedtpm` | Key cannot be duplicated outside this TPM |
+| `fixedparent` | Key cannot be moved to a different parent |
+
+The `unique` field contains pre-computed ECC coordinates. When `TPM2_CreatePrimary` is called
+with this template in the endorsement hierarchy, the TPM deterministically derives a key
+matching the certificate Google provisions at `0x01c10002`.
+
+This is a TCG-compliant Attestation Key profile: a restricted signing key in the endorsement
+hierarchy, bound to the platform via the EK seed.
+
+## GCP Instance Identity Extension
+
+AK certificates include a custom extension that binds the certificate to the VM:
+
+**OID**: `1.3.6.1.4.1.11129.2.1.21`
+
+This extension contains a DER-encoded structure with:
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `zone` | UTF8String | GCP zone (e.g., `us-central1-f`) |
+| `project` | UTF8String | GCP project ID |
+| `instance_id` | INTEGER | Numeric instance ID |
+| `instance_name` | UTF8String | Instance name |
+| Additional fields | Various | Confidential Computing flags |
+
+Example from a real certificate:
+```
+zone: us-central1-f
+project: lockboot
+instance_id: 3414240648225485836
+instance_name: instance-20260202-065609
+```
+
+## TPM Quote Structure
+
+The TPM2_Quote structure signed by the AK contains:
+
+```
+TPM2B_ATTEST {
+ magic: 0xFF544347 ("TCG\xFF")
+ type: TPM_ST_ATTEST_QUOTE (0x8018)
+ qualifiedSigner: Hash of signing key name
+ extraData: Nonce passed to attest()
+ clockInfo: TPM clock values
+ firmwareVersion: TPM firmware version
+ attested: TPMS_QUOTE_INFO {
+ pcrSelect: Which PCRs are included
+ pcrDigest: SHA-256 of concatenated PCR values
+ }
+}
+```
+
+The signature is ECDSA over the TPM2B_ATTEST structure.
+
+The nonce in `extraData` is whatever value was passed to the `attest()` function. In a
+challenge-response protocol, this would be a verifier-provided challenge. The library
+itself is agnostic to how the nonce is generated or validatedβthat's up to the calling
+application's attestation flow.
+
+## Verification Process
+
+1. **Parse certificate chain** from AK cert PEM (leaf β intermediate β root)
+
+2. **Validate X.509 extensions**:
+ - Leaf: `CA:FALSE`, `digitalSignature` key usage
+ - Intermediate/Root: `CA:TRUE`, `keyCertSign` key usage
+ - Intermediates: TPM EK Certificate EKU (`2.23.133.8.1`)
+
+3. **Verify certificate signatures**: Each cert signed by the next in chain
+
+4. **Check validity dates**: All certs must be valid at verification time
+
+5. **Verify issuer/subject chaining**: Each cert's Issuer matches parent's Subject
+
+6. **Extract AK public key** from leaf certificate
+
+7. **Verify TPM Quote signature** using AK public key
+
+8. **Validate nonce** in Quote.extraData matches expected challenge
+
+9. **Verify PCR digest**: Recompute digest from claimed PCR values, compare to Quote
+
+10. **Check root CA**: Hash root's public key and verify it matches known GCP root
+
+## Security Considerations
+
+### What This Validates
+
+- The AK was created on a GCP Confidential VM vTPM certified by Google
+- The Quote was signed by that specific AK
+- PCR values were selected by the Quote at signing time
+- The nonce can prove freshness if verifier-provided (replay protection)
+
+### What This Does NOT Validate
+
+- The PCR values themselves are correct for the expected software state
+- The VM is actually running the software you expect
+- No malware modified memory after boot (vTPM measures boot, not runtime)
+
+### Trust Assumptions
+
+- Google's EK/AK CA infrastructure is not compromised
+- The embedded root CA public key hash is authentic
+- Time source is accurate for certificate validation
+
+## Differences from AWS Nitro
+
+| Aspect | GCP Confidential VM | AWS Nitro |
+|--------|---------------------|-----------|
+| Trust Root | X.509 certificate chain | COSE-signed NSM document |
+| Key Binding | AK certificate includes VM identity | NSM document has public_key field |
+| PCR Source | TPM2_Quote | Nitro document (nitrotpm_pcrs) |
+| Algorithm | ECDSA P-256 | ECDSA P-384 |
+
+## References
+
+- [TCG TPM 2.0 Library Specification](https://trustedcomputinggroup.org/resource/tpm-library-specification/)
+- [GCP Confidential VM Documentation](https://cloud.google.com/confidential-computing/confidential-vm/docs)
+- [RFC 5280 - X.509 PKI Certificate Profile](https://tools.ietf.org/html/rfc5280)
diff --git a/crates/vaportpm-attest/README.md b/crates/vaportpm-attest/README.md
index 41cfbf7..66fa97e 100644
--- a/crates/vaportpm-attest/README.md
+++ b/crates/vaportpm-attest/README.md
@@ -1,62 +1,33 @@
# vaportpm-attest
-Cloud vTPM attestation in pure Rust with **zero C dependencies**.
+Produces a self-contained attestation document from a cloud vTPM. The output can be verified offline using [`vaportpm-verify`](../vaportpm-verify/).
-> Physical TPM trust is vapor. Cloud vTPM is where the real trust lives.
-
----
-
-Most people wrap the crusty C-based `tss2` monolith. Instead, this library implements the bare minimum TPM 2.0 wire-protocol, yeeting bytes directly to `/dev/tpm0`:
-
-- No libtss2, or any C toolchain
-- Direct protocol command/response serialization
-- Minimal, and focused on cloud attestation
-- Comprehensive `selftest` binary
-
-## Features
-
-### PCR Operations (`PcrOps` trait)
-
-- Read PCR values from all active banks (SHA-1, SHA-256, SHA-384, SHA-512)
-- Extend PCRs with automatic multi-bank support
-- Query which banks a PCR is allocated in
-- Read all allocated PCRs from the TPM
-
-### Key Management
-
-- Create primary ECC P-256 signing keys
-- Sign data with TPM keys (ECDSA signatures)
-- Create PCR-sealed keys (policy-based authorization)
-
-### NV RAM Operations (`NvOps` trait)
+```rust
+use vaportpm_attest::attest;
-- **Read Operations:**
- - Read from TPM Non-Volatile storage
- - Read NV index public information
- - Enumerate all NV indices
- - Read Endorsement Key (EK) certificates
-- **Write Operations:**
- - Define new NV spaces with attributes
- - Write data to NV indices
- - Undefine (delete) NV spaces
- - Find free NV indices
+let json = attest(nonce)?;
+// Send json to verifier
+```
-### Attestation
+The library auto-detects the cloud platform (AWS Nitro, GCP Confidential VM) and produces a JSON document containing:
+- TPM2_Quote (signed PCR values)
+- Platform-specific trust chain (Nitro document or GCP AK certificate)
+- PCR values and nonce for verification
-- Certify keys using other keys
-- Generate attestation structures
-- PCR policy digest calculation
+---
-### AWS Nitro Security Module (NSM) Support (`NsmOps` trait)
+Implements the TPM 2.0 wire protocol in pure Rustβno `tss2` or C dependencies. While low-level TPM operations are exposed via extension traits, the primary interface is `attest()`.
-- Request attestation documents from AWS Nitro Secure Module
-- Includes PCR values, optional user data, nonce, and public key
-- Automatic detection of Nitro TPM via vendor string
+## Low-Level TPM Operations
-### TPM Vendor Detection
+The following traits are available for direct TPM interaction:
-- Query TPM manufacturer and vendor information
-- Check if running on AWS Nitro TPM (`is_nitro_tpm()`)
+| Trait | Operations |
+|-------|------------|
+| `PcrOps` | Read/extend PCRs across all hash banks |
+| `NvOps` | Read/write NV RAM, enumerate indices |
+| `KeyOps` | Create signing keys, TPM2_Quote |
+| `NsmOps` | AWS Nitro Security Module attestation |
## Quick Start
@@ -151,40 +122,6 @@ fn main() -> anyhow::Result<()> {
}
```
-## Running Tests
-
-### `selftest` - Comprehensive TPM Test Suite
-
-The "self test" binary exercises all major functionality:
-
-```bash
-cargo run --bin selftest
-```
-
-It performs:
-1. Query TPM properties (manufacturer, firmware version, etc.)
-2. Detect AWS Nitro TPM
-3. Query active PCR banks
-4. Read all non-zero PCR values
-5. Extend PCR 23 and verify the change
-6. Create a primary ECC signing key
-7. Sign test data
-8. Access the Endorsement Key
-9. Read EK certificates from NV RAM
-10. Enumerate all NV indices
-11. Create PCR-sealed keys
-12. Certify keys (attestation)
-
-### `nsmtest` - AWS Nitro Security Module Test
-
-Test NSM attestation functionality (requires AWS Nitro TPM):
-
-```bash
-cargo run --bin nsmtest
-```
-
-This requests an attestation document from the Nitro Secure Module and displays the result.
-
## Requirements
- Linux with TPM 2.0 support
@@ -221,7 +158,7 @@ The library implements the TPM 2.0 command/response protocol as specified in the
- `TPM2_NV_Write` - Write to NV storage
- `TPM2_NV_UndefineSpace` - Delete NV indices
- `TPM2_PolicyPCR` - PCR policy operations
-- `TPM2_Certify` - Key certification/attestation
+- `TPM2_Quote` - PCR attestation
**Vendor-Specific Commands:**
- `TPM2_CC_VENDOR_AWS_NSM_REQUEST` (0x20000001) - AWS Nitro Security Module attestation
@@ -233,11 +170,11 @@ The library uses extension traits to organize functionality:
- **`PcrOps`** - PCR read/extend operations
- **`NvOps`** - NV RAM read/write operations
- **`NsmOps`** - AWS Nitro Security Module operations
-- **`EkOps`** - Endorsement Key operations (create standard EK, signing keys)
+- **`KeyOps`** - Key operations (create signing keys, TPM2_Quote)
Import the traits you need:
```rust
-use vaportpm_attest::{Tpm, PcrOps, NvOps, NsmOps, EkOps};
+use vaportpm_attest::{Tpm, PcrOps, NvOps, NsmOps, KeyOps};
```
### Hash Algorithms
@@ -252,36 +189,11 @@ Supports multiple PCR banks:
### Attestation Model
-The library implements a TPM-based attestation model where:
-
-1. **Endorsement Key (EK)** - A TPM's identity, certified by the manufacturer. Created using the TCG standard template for deterministic key derivation. The EK is decrypt-only (cannot sign).
-
-2. **Attestation Key (AK)** - A signing key created with a PCR policy. The AK can only be used when PCRs match specific values, cryptographically binding the key to the system state.
-
-3. **PCR Policy** - A SHA-256 digest computed from PCR values. When an AK is created with an `authPolicy`, it can only sign when `TPM2_PolicyPCR` succeeds with matching values.
-
-4. **TPM2_Certify** - The AK self-certifies, producing a `TPM2B_ATTEST` structure containing the AK's name (which includes its `authPolicy`). This proves the AK exists and is bound to specific PCR values.
-
-### Chain of Trust
-
-```mermaid
-flowchart TD
- A["Trust Anchor
Nitro Root CA / TPM Manufacturer / Cloud Provider"]
- B["Platform Attestation
Nitro Document / EK Certificate / AK Certificate"]
- C["Signing Key (AK)
Bound to PCR values via authPolicy"]
- D["TPM2B_ATTEST + Signature
Contains: nonce, AK name (proves PCR binding)"]
-
- A -->|signs| B
- B -->|binds| C
- C -->|signs| D
-```
+The library generates TPM2_Quote attestations signed by a long-lived Attestation Key (AK). The AK's authenticity is proven via platform-specific trust anchors:
-### Platform-Specific Trust
+- **AWS Nitro**: The AK public key is embedded in a Nitro attestation document (via `nsm_attest`), which is signed by Amazon's Nitro CA chain.
+- **GCP Confidential VM**: The AK has a certificate stored in TPM NV RAM, signed by Google's CA chain.
-| Platform | Trust Anchor | AK Binding Method |
-|----------|-------------|-------------------|
-| AWS Nitro | Nitro Root CA | Nitro document `public_key` field |
-| GCP Shielded VM | Google CA | AK certificate (NV 0x01c10000) |
-| Azure Trusted Launch | Microsoft CA | AK certificate (NV 0x01C101D0) |
+Both paths produce a TPM2_Quote (signed PCR values + nonce) that can be verified against the platform's root of trust.
See [AWS-NITRO.md](./AWS-NITRO.md) for detailed AWS Nitro attestation documentation.
diff --git a/crates/vaportpm-attest/certs/aws-nitro-root.pem b/crates/vaportpm-attest/certs/aws-nitro-root.pem
new file mode 100644
index 0000000..221cc0b
--- /dev/null
+++ b/crates/vaportpm-attest/certs/aws-nitro-root.pem
@@ -0,0 +1,14 @@
+-----BEGIN CERTIFICATE-----
+MIICETCCAZagAwIBAgIRAPkxdWgbkK/hHUbMtOTn+FYwCgYIKoZIzj0EAwMwSTEL
+MAkGA1UEBhMCVVMxDzANBgNVBAoMBkFtYXpvbjEMMAoGA1UECwwDQVdTMRswGQYD
+VQQDDBJhd3Mubml0cm8tZW5jbGF2ZXMwHhcNMTkxMDI4MTMyODA1WhcNNDkxMDI4
+MTQyODA1WjBJMQswCQYDVQQGEwJVUzEPMA0GA1UECgwGQW1hem9uMQwwCgYDVQQL
+DANBV1MxGzAZBgNVBAMMEmF3cy5uaXRyby1lbmNsYXZlczB2MBAGByqGSM49AgEG
+BSuBBAAiA2IABPwCVOumCMHzaHDimtqQvkY4MpJzbolL//Zy2YlES1BR5TSksfbb
+48C8WBoyt7F2Bw7eEtaaP+ohG2bnUs990d0JX28TcPQXCEPZ3BABIeTPYwEoCWZE
+h8l5YoQwTcU/9KNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUkCW1DdkF
+R+eWw5b6cp3PmanfS5YwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2kAMGYC
+MQCjfy+Rocm9Xue4YnwWmNJVA44fA0P5W2OpYow9OYCVRaEevL8uO1XYru5xtMPW
+rfMCMQCi85sWBbJwKKXdS6BptQFuZbT73o/gBh1qUxl/nNr12UO8Yfwr6wPLb+6N
+IwLz3/Y=
+-----END CERTIFICATE-----
diff --git a/crates/vaportpm-attest/certs/aws-nitro-root.txt b/crates/vaportpm-attest/certs/aws-nitro-root.txt
new file mode 100644
index 0000000..f8075df
--- /dev/null
+++ b/crates/vaportpm-attest/certs/aws-nitro-root.txt
@@ -0,0 +1,39 @@
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number:
+ f9:31:75:68:1b:90:af:e1:1d:46:cc:b4:e4:e7:f8:56
+ Signature Algorithm: ecdsa-with-SHA384
+ Issuer: C=US, O=Amazon, OU=AWS, CN=aws.nitro-enclaves
+ Validity
+ Not Before: Oct 28 13:28:05 2019 GMT
+ Not After : Oct 28 14:28:05 2049 GMT
+ Subject: C=US, O=Amazon, OU=AWS, CN=aws.nitro-enclaves
+ Subject Public Key Info:
+ Public Key Algorithm: id-ecPublicKey
+ Public-Key: (384 bit)
+ pub:
+ 04:fc:02:54:eb:a6:08:c1:f3:68:70:e2:9a:da:90:
+ be:46:38:32:92:73:6e:89:4b:ff:f6:72:d9:89:44:
+ 4b:50:51:e5:34:a4:b1:f6:db:e3:c0:bc:58:1a:32:
+ b7:b1:76:07:0e:de:12:d6:9a:3f:ea:21:1b:66:e7:
+ 52:cf:7d:d1:dd:09:5f:6f:13:70:f4:17:08:43:d9:
+ dc:10:01:21:e4:cf:63:01:28:09:66:44:87:c9:79:
+ 62:84:30:4d:c5:3f:f4
+ ASN1 OID: secp384r1
+ NIST CURVE: P-384
+ X509v3 extensions:
+ X509v3 Basic Constraints: critical
+ CA:TRUE
+ X509v3 Subject Key Identifier:
+ 90:25:B5:0D:D9:05:47:E7:96:C3:96:FA:72:9D:CF:99:A9:DF:4B:96
+ X509v3 Key Usage: critical
+ Digital Signature, Certificate Sign, CRL Sign
+ Signature Algorithm: ecdsa-with-SHA384
+ Signature Value:
+ 30:66:02:31:00:a3:7f:2f:91:a1:c9:bd:5e:e7:b8:62:7c:16:
+ 98:d2:55:03:8e:1f:03:43:f9:5b:63:a9:62:8c:3d:39:80:95:
+ 45:a1:1e:bc:bf:2e:3b:55:d8:ae:ee:71:b4:c3:d6:ad:f3:02:
+ 31:00:a2:f3:9b:16:05:b2:70:28:a5:dd:4b:a0:69:b5:01:6e:
+ 65:b4:fb:de:8f:e0:06:1d:6a:53:19:7f:9c:da:f5:d9:43:bc:
+ 61:fc:2b:eb:03:cb:6f:ee:8d:23:02:f3:df:f6
diff --git a/crates/vaportpm-attest/certs/gcp-ekak-root.pem b/crates/vaportpm-attest/certs/gcp-ekak-root.pem
new file mode 100644
index 0000000..080fdea
--- /dev/null
+++ b/crates/vaportpm-attest/certs/gcp-ekak-root.pem
@@ -0,0 +1,35 @@
+-----BEGIN CERTIFICATE-----
+MIIGATCCA+mgAwIBAgIUAKZdpPnjKPOANcOnPU9yQyvfFdwwDQYJKoZIhvcNAQEL
+BQAwfjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcT
+DU1vdW50YWluIFZpZXcxEzARBgNVBAoTCkdvb2dsZSBMTEMxFTATBgNVBAsTDEdv
+b2dsZSBDbG91ZDEWMBQGA1UEAxMNRUsvQUsgQ0EgUm9vdDAgFw0yMjA3MDgwMDQw
+MzRaGA8yMTIyMDcwODA1NTcyM1owfjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNh
+bGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEzARBgNVBAoTCkdvb2ds
+ZSBMTEMxFTATBgNVBAsTDEdvb2dsZSBDbG91ZDEWMBQGA1UEAxMNRUsvQUsgQ0Eg
+Um9vdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJ0l9VCoyJZLSol8
+KyhNpbS7pBnuicE6ptrdtxAWIR2TnLxSgxNFiR7drtofxI0ruceoCIpsa9NHIKrz
+3sM/N/E8mFNHiJAuyVf3pPpmDpLJZQ1qe8yHkpGSs3Kj3s5YYWtEecCVfzNs4MtK
+vGfA+WKB49A6Noi8R9R1GonLIN6wSXX3kP1ibRn0NGgdqgfgRe5HC3kKAhjZ6scT
+8Eb1SGlaByGzE5WoGTnNbyifkyx9oUZxXVJsqv2q611W3apbPxcgev8z5JXQUbrr
+Q7EbO0StK1DsKRsKLuD+YLxjrBRQ4UeIN5WHp6G0vgYiOptHm6YKZxQemO/kVMLR
+zsm1AYH7eNOFekcBIKRjSqpk5m4ud04qum6f0hBj3iE/Pe+DvIbVhLh9ItAunISG
+QPA9dYEgfA/qWir+pU7LV3phpLeGhull8G/zYmQhF3heg0buIR70aavzT8iLAQrx
+VMNRZJEGMwIN/tq8YiT3+3EZIcSqq6GAGjiuVw3NIsXC3+CuSJGQ5GbDp49Lc6VW
+PHeWeFvwSUGgxKXq5r1+PRsoYgK6S4hhecgXEX5c7Rta6TcFlEFb0XK9fpy1dr89
+LeFGxUBpdDvKxDRLMm3FQen8rmR/PSReEcJsaqbUP/q7Pc7k0RfF9Mb6AfPZfnqg
+pYJQ+IFSr9EjRSW1wPcL03zoTP47AgMBAAGjdTBzMA4GA1UdDwEB/wQEAwIBBjAQ
+BgNVHSUECTAHBgVngQUIATAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRJ50pb
+Vin1nXm3pjA8A7KP5xTdTDAfBgNVHSMEGDAWgBRJ50pbVin1nXm3pjA8A7KP5xTd
+TDANBgkqhkiG9w0BAQsFAAOCAgEAlfHRvOB3CJoLTl1YG/AvjGoZkpNMyp5X5je1
+ICCQ68b296En9hIUlcYY/+nuEPSPUjDA3izwJ8DAfV4REgpQzqoh6XhR3TgyfHXj
+J6DC7puzEgtzF1+wHShUpBoe3HKuL4WhB3rvwk2SEsudBu92o9BuBjcDJ/GW5GRt
+pD/H71HAE8rI9jJ41nS0FvkkjaX0glsntMVUXiwcta8GI0QOE2ijsJBwk41uQGt0
+YOj2SGlEwNAC5DBTB5kZ7+6X9xGE6/c+M3TAA0ONoX18rNfif94cCx/mPYOs8pUk
+ANRAQ4aTRBvpBrryGT8R1ahTBkMeRQG3tdsLHRT8fJCFUANd5WLWsi83005y/WuM
+z8/gFKc0PL+F+MubCsJ1ODPTRscH93QlS4zEMg5hDAIks+fDoRJ2QiROqo7GAqbT
+c7STKfGcr9+pa63na7f3oy1sZPWPdxB8tx5z3lghiPP3ktQx/yK/1Fwf1hgxJHFy
+/2UcaGuOXRRRTPyEnppZp82Kigs9aPHWtaVm2/LrXX2fvT9iM/k0CovNAj8rztHx
+sUEoA0xJnSOJNPpe9PRdjsTj7/u3Xu6hQLNNidBHgI3Hcmi704HMMd/3yZ424OOr
+S32ylpeU1oeQHFrLE6hYX4/ttMETbmESIKd2rTgstPotSvkuB5TljbKYPR+lq7hQ
+av16U4E=
+-----END CERTIFICATE-----
diff --git a/crates/vaportpm-attest/certs/gcp-ekak-root.txt b/crates/vaportpm-attest/certs/gcp-ekak-root.txt
new file mode 100644
index 0000000..f3a2554
--- /dev/null
+++ b/crates/vaportpm-attest/certs/gcp-ekak-root.txt
@@ -0,0 +1,93 @@
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number:
+ a6:5d:a4:f9:e3:28:f3:80:35:c3:a7:3d:4f:72:43:2b:df:15:dc
+ Signature Algorithm: sha256WithRSAEncryption
+ Issuer: C=US, ST=California, L=Mountain View, O=Google LLC, OU=Google Cloud, CN=EK/AK CA Root
+ Validity
+ Not Before: Jul 8 00:40:34 2022 GMT
+ Not After : Jul 8 05:57:23 2122 GMT
+ Subject: C=US, ST=California, L=Mountain View, O=Google LLC, OU=Google Cloud, CN=EK/AK CA Root
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ Public-Key: (4096 bit)
+ Modulus:
+ 00:9d:25:f5:50:a8:c8:96:4b:4a:89:7c:2b:28:4d:
+ a5:b4:bb:a4:19:ee:89:c1:3a:a6:da:dd:b7:10:16:
+ 21:1d:93:9c:bc:52:83:13:45:89:1e:dd:ae:da:1f:
+ c4:8d:2b:b9:c7:a8:08:8a:6c:6b:d3:47:20:aa:f3:
+ de:c3:3f:37:f1:3c:98:53:47:88:90:2e:c9:57:f7:
+ a4:fa:66:0e:92:c9:65:0d:6a:7b:cc:87:92:91:92:
+ b3:72:a3:de:ce:58:61:6b:44:79:c0:95:7f:33:6c:
+ e0:cb:4a:bc:67:c0:f9:62:81:e3:d0:3a:36:88:bc:
+ 47:d4:75:1a:89:cb:20:de:b0:49:75:f7:90:fd:62:
+ 6d:19:f4:34:68:1d:aa:07:e0:45:ee:47:0b:79:0a:
+ 02:18:d9:ea:c7:13:f0:46:f5:48:69:5a:07:21:b3:
+ 13:95:a8:19:39:cd:6f:28:9f:93:2c:7d:a1:46:71:
+ 5d:52:6c:aa:fd:aa:eb:5d:56:dd:aa:5b:3f:17:20:
+ 7a:ff:33:e4:95:d0:51:ba:eb:43:b1:1b:3b:44:ad:
+ 2b:50:ec:29:1b:0a:2e:e0:fe:60:bc:63:ac:14:50:
+ e1:47:88:37:95:87:a7:a1:b4:be:06:22:3a:9b:47:
+ 9b:a6:0a:67:14:1e:98:ef:e4:54:c2:d1:ce:c9:b5:
+ 01:81:fb:78:d3:85:7a:47:01:20:a4:63:4a:aa:64:
+ e6:6e:2e:77:4e:2a:ba:6e:9f:d2:10:63:de:21:3f:
+ 3d:ef:83:bc:86:d5:84:b8:7d:22:d0:2e:9c:84:86:
+ 40:f0:3d:75:81:20:7c:0f:ea:5a:2a:fe:a5:4e:cb:
+ 57:7a:61:a4:b7:86:86:e9:65:f0:6f:f3:62:64:21:
+ 17:78:5e:83:46:ee:21:1e:f4:69:ab:f3:4f:c8:8b:
+ 01:0a:f1:54:c3:51:64:91:06:33:02:0d:fe:da:bc:
+ 62:24:f7:fb:71:19:21:c4:aa:ab:a1:80:1a:38:ae:
+ 57:0d:cd:22:c5:c2:df:e0:ae:48:91:90:e4:66:c3:
+ a7:8f:4b:73:a5:56:3c:77:96:78:5b:f0:49:41:a0:
+ c4:a5:ea:e6:bd:7e:3d:1b:28:62:02:ba:4b:88:61:
+ 79:c8:17:11:7e:5c:ed:1b:5a:e9:37:05:94:41:5b:
+ d1:72:bd:7e:9c:b5:76:bf:3d:2d:e1:46:c5:40:69:
+ 74:3b:ca:c4:34:4b:32:6d:c5:41:e9:fc:ae:64:7f:
+ 3d:24:5e:11:c2:6c:6a:a6:d4:3f:fa:bb:3d:ce:e4:
+ d1:17:c5:f4:c6:fa:01:f3:d9:7e:7a:a0:a5:82:50:
+ f8:81:52:af:d1:23:45:25:b5:c0:f7:0b:d3:7c:e8:
+ 4c:fe:3b
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Key Usage: critical
+ Certificate Sign, CRL Sign
+ X509v3 Extended Key Usage:
+ Endorsement Key Certificate
+ X509v3 Basic Constraints: critical
+ CA:TRUE
+ X509v3 Subject Key Identifier:
+ 49:E7:4A:5B:56:29:F5:9D:79:B7:A6:30:3C:03:B2:8F:E7:14:DD:4C
+ X509v3 Authority Key Identifier:
+ 49:E7:4A:5B:56:29:F5:9D:79:B7:A6:30:3C:03:B2:8F:E7:14:DD:4C
+ Signature Algorithm: sha256WithRSAEncryption
+ Signature Value:
+ 95:f1:d1:bc:e0:77:08:9a:0b:4e:5d:58:1b:f0:2f:8c:6a:19:
+ 92:93:4c:ca:9e:57:e6:37:b5:20:20:90:eb:c6:f6:f7:a1:27:
+ f6:12:14:95:c6:18:ff:e9:ee:10:f4:8f:52:30:c0:de:2c:f0:
+ 27:c0:c0:7d:5e:11:12:0a:50:ce:aa:21:e9:78:51:dd:38:32:
+ 7c:75:e3:27:a0:c2:ee:9b:b3:12:0b:73:17:5f:b0:1d:28:54:
+ a4:1a:1e:dc:72:ae:2f:85:a1:07:7a:ef:c2:4d:92:12:cb:9d:
+ 06:ef:76:a3:d0:6e:06:37:03:27:f1:96:e4:64:6d:a4:3f:c7:
+ ef:51:c0:13:ca:c8:f6:32:78:d6:74:b4:16:f9:24:8d:a5:f4:
+ 82:5b:27:b4:c5:54:5e:2c:1c:b5:af:06:23:44:0e:13:68:a3:
+ b0:90:70:93:8d:6e:40:6b:74:60:e8:f6:48:69:44:c0:d0:02:
+ e4:30:53:07:99:19:ef:ee:97:f7:11:84:eb:f7:3e:33:74:c0:
+ 03:43:8d:a1:7d:7c:ac:d7:e2:7f:de:1c:0b:1f:e6:3d:83:ac:
+ f2:95:24:00:d4:40:43:86:93:44:1b:e9:06:ba:f2:19:3f:11:
+ d5:a8:53:06:43:1e:45:01:b7:b5:db:0b:1d:14:fc:7c:90:85:
+ 50:03:5d:e5:62:d6:b2:2f:37:d3:4e:72:fd:6b:8c:cf:cf:e0:
+ 14:a7:34:3c:bf:85:f8:cb:9b:0a:c2:75:38:33:d3:46:c7:07:
+ f7:74:25:4b:8c:c4:32:0e:61:0c:02:24:b3:e7:c3:a1:12:76:
+ 42:24:4e:aa:8e:c6:02:a6:d3:73:b4:93:29:f1:9c:af:df:a9:
+ 6b:ad:e7:6b:b7:f7:a3:2d:6c:64:f5:8f:77:10:7c:b7:1e:73:
+ de:58:21:88:f3:f7:92:d4:31:ff:22:bf:d4:5c:1f:d6:18:31:
+ 24:71:72:ff:65:1c:68:6b:8e:5d:14:51:4c:fc:84:9e:9a:59:
+ a7:cd:8a:8a:0b:3d:68:f1:d6:b5:a5:66:db:f2:eb:5d:7d:9f:
+ bd:3f:62:33:f9:34:0a:8b:cd:02:3f:2b:ce:d1:f1:b1:41:28:
+ 03:4c:49:9d:23:89:34:fa:5e:f4:f4:5d:8e:c4:e3:ef:fb:b7:
+ 5e:ee:a1:40:b3:4d:89:d0:47:80:8d:c7:72:68:bb:d3:81:cc:
+ 31:df:f7:c9:9e:36:e0:e3:ab:4b:7d:b2:96:97:94:d6:87:90:
+ 1c:5a:cb:13:a8:58:5f:8f:ed:b4:c1:13:6e:61:12:20:a7:76:
+ ad:38:2c:b4:fa:2d:4a:f9:2e:07:94:e5:8d:b2:98:3d:1f:a5:
+ ab:b8:50:6a:fd:7a:53:81
diff --git a/crates/vaportpm-attest/certs/pem2txt.sh b/crates/vaportpm-attest/certs/pem2txt.sh
new file mode 100755
index 0000000..534222e
--- /dev/null
+++ b/crates/vaportpm-attest/certs/pem2txt.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+# Convert all .pem certificates to human-readable .txt files
+# Usage: ./pem2txt.sh
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
+for pem in "$SCRIPT_DIR"/*.pem; do
+ [ -f "$pem" ] || continue
+ txt="${pem%.pem}.txt"
+ echo "Converting: $(basename "$pem") -> $(basename "$txt")"
+ openssl x509 -in "$pem" -text -noout > "$txt"
+done
+
+echo "Done."
diff --git a/crates/vaportpm-attest/src/a9n.rs b/crates/vaportpm-attest/src/a9n.rs
index 727fbfb..c077dfb 100644
--- a/crates/vaportpm-attest/src/a9n.rs
+++ b/crates/vaportpm-attest/src/a9n.rs
@@ -3,45 +3,48 @@
//! TPM attestation functionality
//!
//! Provides high-level attestation operations including:
-//! - Retrieving EK certificates from NV RAM
//! - Creating and certifying attestation keys (AK)
//! - Reading PCR values
//! - Generating attestation documents
-use anyhow::{anyhow, Context, Result};
-use base64::{engine::general_purpose::STANDARD, Engine as _};
+use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
-use crate::credential::compute_ecc_p256_name;
-use crate::{EkOps, NsmOps, NvOps, PcrOps, Tpm, TPM_RH_OWNER};
-use crate::{NV_INDEX_ECC_P256_EK_CERT, NV_INDEX_ECC_P384_EK_CERT, NV_INDEX_RSA_2048_EK_CERT};
-
-/// DER SEQUENCE tag with 2-byte length (0x30 0x82)
-/// Used to detect valid X.509 certificates in DER format
-const DER_SEQUENCE_LONG: [u8; 2] = [0x30, 0x82];
+use crate::cert::{der_to_pem, fetch_cert_chain, DER_SEQUENCE_LONG};
+use crate::{KeyOps, NsmOps, NvOps, PcrOps, PublicKey, Tpm, TPM_RH_ENDORSEMENT};
+
+/// GCP AK template NV index (RSA) - used for GCP detection
+const GCP_AK_TEMPLATE_NV_INDEX_RSA: u32 = 0x01c10001;
+/// GCP AK certificate NV index (ECC)
+const GCP_AK_CERT_NV_INDEX_ECC: u32 = 0x01c10002;
+/// GCP AK template NV index (ECC)
+const GCP_AK_TEMPLATE_NV_INDEX_ECC: u32 = 0x01c10003;
+/// GCP TPM manufacturer ID: "GOOG"
+const GCP_MANUFACTURER_GOOG: u32 = 0x474F4F47;
+/// TPM property: manufacturer
+const TPM_PT_MANUFACTURER: u32 = 0x00000105;
+
+/// Result type for attestation helper functions
+/// Contains: (ak_pubkeys, attestation_data, gcp_attestation, ak_handle)
+type AttestResult = (
+ HashMap,
+ AttestationData,
+ Option,
+ Option,
+);
/// Complete attestation output containing all TPM attestation data
#[derive(Debug, Serialize, Deserialize)]
pub struct AttestationOutput {
- pub ek_certificates: EkCertificates,
+ /// Nonce/challenge used for this attestation (hex-encoded)
+ pub nonce: String,
pub pcrs: HashMap>,
- pub ek_public_keys: HashMap,
- pub signing_key_public_keys: HashMap,
+ /// Attestation Key public keys (hex-encoded ECC coordinates)
+ pub ak_pubkeys: HashMap,
pub attestation: AttestationContainer,
}
-/// Endorsement Key certificates in PEM format
-#[derive(Debug, Serialize, Deserialize)]
-pub struct EkCertificates {
- #[serde(skip_serializing_if = "Option::is_none")]
- pub rsa_2048: Option,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub ecc_p256: Option,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub ecc_p384: Option,
-}
-
/// ECC public key coordinates
#[derive(Debug, Serialize, Deserialize)]
pub struct EccPublicKeyCoords {
@@ -49,123 +52,105 @@ pub struct EccPublicKeyCoords {
pub y: String,
}
-/// Container for both TPM and optional Nitro attestations
+/// Container for both TPM and optional platform-specific attestations
#[derive(Debug, Serialize, Deserialize)]
pub struct AttestationContainer {
pub tpm: HashMap,
#[serde(skip_serializing_if = "Option::is_none")]
pub nitro: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub gcp: Option,
}
-/// TPM attestation data (certify response with NIZK proof)
+/// GCP Shielded VM attestation data
+///
+/// Contains the AK certificate chain from NV RAM.
+/// The AK is a long-term key provisioned by Google (not PCR-bound).
+/// The Quote data and signature are in `attestation.tpm`.
+#[derive(Debug, Serialize, Deserialize)]
+pub struct GcpAttestationData {
+ /// AK certificate chain in PEM format (leaf first, root last)
+ pub ak_cert_chain: String,
+}
+
+/// TPM attestation data (Quote response)
#[derive(Debug, Serialize, Deserialize)]
pub struct AttestationData {
- /// The nonce/challenge provided to the attestation (hex-encoded)
- /// This is duplicated from attest_data.extraData for easy access.
- /// Verification MUST check this matches the nonce in attest_data.
- pub nonce: String,
- /// TPM2B_ATTEST structure from TPM2_Certify (hex-encoded)
+ /// TPM2B_ATTEST structure from TPM2_Quote (hex-encoded)
pub attest_data: String,
/// ECDSA signature over attest_data (DER, hex-encoded)
pub signature: String,
}
/// Nitro Enclave attestation data
+///
+/// The AK public key and nonce are inside the signed document,
+/// and also available at the top level of AttestationOutput.
#[derive(Debug, Serialize, Deserialize)]
pub struct NitroAttestationData {
- pub public_key: String,
- pub nonce: String,
+ /// COSE Sign1 NSM document (hex-encoded)
pub document: String,
}
-/// Convert DER-encoded data to PEM format
-pub fn der_to_pem(der: &[u8], label: &str) -> String {
- let base64_encoded = STANDARD.encode(der);
- let mut pem = format!("-----BEGIN {}-----\n", label);
- for chunk in base64_encoded.as_bytes().chunks(64) {
- pem.push_str(std::str::from_utf8(chunk).unwrap());
- pem.push('\n');
+/// Detect if running on GCP Shielded VM
+///
+/// Detection based on:
+/// 1. TPM manufacturer ID is "GOOG"
+/// 2. GCP AK template NV index exists
+fn is_gcp_tpm(tpm: &mut Tpm) -> bool {
+ // Check manufacturer
+ if let Ok(manufacturer) = tpm.get_property(TPM_PT_MANUFACTURER) {
+ if manufacturer == GCP_MANUFACTURER_GOOG {
+ // Verify AK template exists
+ if tpm.nv_readpublic(GCP_AK_TEMPLATE_NV_INDEX_RSA).is_ok() {
+ return true;
+ }
+ }
}
- pem.push_str(&format!("-----END {}-----\n", label));
- pem
+ false
}
/// Generate a complete TPM attestation document
///
-/// This function:
-/// 1. Retrieves EK certificates from NV RAM
-/// 2. Creates the TCG standard EK (matches certificate public key)
-/// 3. Detects platform and chooses PCR bank (SHA-384 for Nitro, SHA-256 otherwise)
-/// 4. Reads PCR values from the chosen bank
-/// 5. Computes PCR policy and creates a signing key (AK) bound to it
-/// 6. AK self-certifies via TPM2_Certify (proves AK exists with this policy)
-/// 7. If on AWS Nitro, gets Nitro attestation binding the AK public key
+/// This function performs platform-specific TPM2_Quote attestation:
+///
+/// 1. Detects platform (Nitro or GCP)
+/// 2. Reads PCR values
+/// 3. Creates or retrieves AK (Attestation Key):
+/// - Nitro: Creates long-term AK (bound via Nitro NSM document)
+/// - GCP: Recreates AK from Google's template (bound via certificate chain)
+/// 4. Signs PCRs with TPM2_Quote
+/// 5. Includes platform-specific attestation:
+/// - Nitro: COSE Sign1 document binding AK public key
+/// - GCP: AK certificate chain from NV RAM
///
/// # Arguments
/// * `nonce` - User-provided nonce/challenge to include in attestation
///
/// # Returns
/// JSON-encoded attestation document containing all attestation data
+///
+/// # Errors
+/// Returns an error if the platform is not recognized (only AWS Nitro and GCP are supported)
pub fn attest(nonce: &[u8]) -> Result {
let mut tpm = Tpm::open_direct()?;
- // Step 1: Retrieve EK certificates from NV RAM
- let mut ek_certs = EkCertificates {
- rsa_2048: None,
- ecc_p256: None,
- ecc_p384: None,
- };
-
- // Try to read RSA EK cert
- if let Ok(cert) = tpm.nv_read(NV_INDEX_RSA_2048_EK_CERT) {
- if cert.starts_with(&DER_SEQUENCE_LONG) {
- ek_certs.rsa_2048 = Some(der_to_pem(&cert, "CERTIFICATE"));
- }
- }
-
- // Try to read ECC P-256 EK cert
- if let Ok(cert) = tpm.nv_read(NV_INDEX_ECC_P256_EK_CERT) {
- if cert.starts_with(&DER_SEQUENCE_LONG) {
- ek_certs.ecc_p256 = Some(der_to_pem(&cert, "CERTIFICATE"));
- }
- }
-
- // Try to read ECC P-384 EK cert
- if let Ok(cert) = tpm.nv_read(NV_INDEX_ECC_P384_EK_CERT) {
- if cert.starts_with(&DER_SEQUENCE_LONG) {
- ek_certs.ecc_p384 = Some(der_to_pem(&cert, "CERTIFICATE"));
- }
- }
-
- // Step 2: Create TCG standard EK (public key should match certificate)
- // Note: Standard EK is decrypt-only, cannot sign. We use it only for
- // identity verification (comparing public key with certificate).
- let ek = tpm.create_standard_ek().context(
- "Failed to create standard EK - endorsement hierarchy may require authentication",
- )?;
+ // 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 mut ek_public_keys = HashMap::new();
- ek_public_keys.insert(
- "ecc_p256".to_string(),
- EccPublicKeyCoords {
- x: hex::encode(&ek.public_key.x),
- y: hex::encode(&ek.public_key.y),
- },
- );
+ // Step 2: Read all allocated PCRs from all banks
+ let all_pcrs = tpm.read_all_allocated_pcrs()?;
- // Step 3: Detect platform and choose PCR bank
- // Nitro TPMs use SHA-384 for signed PCRs, so we bind AK to SHA-384 bank
- // Other platforms use SHA-256
- let is_nitro = tpm.is_nitro_tpm()?;
+ // Choose PCR bank based on platform
+ // Nitro uses SHA-384 for signed PCRs, others use SHA-256
let pcr_alg = if is_nitro {
crate::TpmAlg::Sha384
} else {
crate::TpmAlg::Sha256
};
- // Step 4: Read all allocated PCRs from all banks
- let all_pcrs = tpm.read_all_allocated_pcrs()?;
-
// Get PCR values for the chosen bank
let pcr_values: Vec<(u8, Vec)> = all_pcrs
.iter()
@@ -177,72 +162,47 @@ pub fn attest(nonce: &[u8]) -> Result {
return Err(anyhow!("No {:?} PCRs allocated on this TPM", pcr_alg));
}
- // Only include PCRs relevant to the chain of trust in output
+ // Build PCRs output
let mut pcrs_by_alg: HashMap> = HashMap::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));
}
- // Step 5: Compute policy from PCR values
- let auth_policy = Tpm::calculate_pcr_policy_digest(&pcr_values, pcr_alg)?;
-
- // Create signing key (AK) bound to this policy
- let signing_key = tpm.create_primary_ecc_key_with_policy(TPM_RH_OWNER, &auth_policy)?;
-
- let mut signing_key_public_keys = HashMap::new();
- signing_key_public_keys.insert(
- "ecc_p256".to_string(),
- EccPublicKeyCoords {
- x: hex::encode(&signing_key.public_key.x),
- y: hex::encode(&signing_key.public_key.y),
- },
- );
-
- // Compute AK name (used for PCR policy verification)
- let _ak_name = compute_ecc_p256_name(
- &signing_key.public_key.x,
- &signing_key.public_key.y,
- &auth_policy,
- );
-
- // Step 6: AK self-certifies via TPM2_Certify (produces TPM2B_ATTEST + signature)
- // This produces TPM2B_ATTEST containing the AK's name (which includes authPolicy)
- let cert_result = tpm.certify(
- signing_key.handle, // object to certify (AK itself)
- signing_key.handle, // signing key (AK)
- nonce, // qualifying data (becomes extraData in TPM2B_ATTEST)
- )?;
-
- let attestation_data = AttestationData {
- nonce: hex::encode(nonce),
- attest_data: hex::encode(&cert_result.attest_data),
- signature: hex::encode(&cert_result.signature),
+ // 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)?
+ } else if is_nitro {
+ // Nitro path: create long-term AK, use TPM2_Quote
+ attest_nitro(&mut tpm, nonce, &pcr_values, pcr_alg)?
+ } else {
+ return Err(anyhow!(
+ "Unknown platform - only AWS Nitro and GCP Shielded VM are supported"
+ ));
};
let mut tpm_attestations = HashMap::new();
tpm_attestations.insert("ecc_p256".to_string(), attestation_data);
- // Get Nitro attestation if available (we already detected Nitro earlier)
+ // Step 6: Get Nitro attestation if on AWS
let nitro_attestation = if is_nitro {
- // Encode signing key public key in SECG format (0x04 || X || Y)
- let mut public_key_secg =
- Vec::with_capacity(1 + signing_key.public_key.x.len() + signing_key.public_key.y.len());
- public_key_secg.push(0x04); // Uncompressed point indicator
- public_key_secg.extend_from_slice(&signing_key.public_key.x);
- public_key_secg.extend_from_slice(&signing_key.public_key.y);
-
- match tpm.nsm_attest(
- None, // user_data
- Some(nonce.to_vec()), // nonce
- Some(public_key_secg.clone()), // public_key
- ) {
- Ok(document) => Some(NitroAttestationData {
- public_key: hex::encode(&public_key_secg),
- nonce: hex::encode(nonce),
- document: hex::encode(&document),
- }),
- Err(_e) => None,
+ 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)?;
+
+ match tpm.nsm_attest(
+ None, // user_data
+ Some(nonce.to_vec()), // nonce
+ Some(public_key_bytes), // public_key
+ ) {
+ Ok(document) => Some(NitroAttestationData {
+ document: hex::encode(&document),
+ }),
+ Err(_e) => None,
+ }
+ } else {
+ None
}
} else {
None
@@ -251,18 +211,19 @@ pub fn attest(nonce: &[u8]) -> Result {
let attestation = AttestationContainer {
tpm: tpm_attestations,
nitro: nitro_attestation,
+ gcp: gcp_attestation,
};
// Cleanup TPM handles
- tpm.flush_context(signing_key.handle)?;
- tpm.flush_context(ek.handle)?;
+ if let Some(handle) = ak_handle {
+ tpm.flush_context(handle)?;
+ }
- // Step 7: Build and output JSON
+ // Step 5: Build and output JSON
let output = AttestationOutput {
- ek_certificates: ek_certs,
+ nonce: hex::encode(nonce),
pcrs: pcrs_by_alg,
- ek_public_keys,
- signing_key_public_keys,
+ ak_pubkeys: signing_key_public_keys,
attestation,
};
@@ -270,3 +231,144 @@ pub fn attest(nonce: &[u8]) -> Result {
Ok(json)
}
+
+/// Nitro attestation path: create restricted AK and use TPM2_Quote
+///
+/// Creates a TCG-compliant restricted AK in the endorsement hierarchy, then uses
+/// TPM2_Quote to sign the PCR values. The AK is bound to the Nitro NSM document.
+fn attest_nitro(
+ tpm: &mut Tpm,
+ nonce: &[u8],
+ pcr_values: &[(u8, Vec)],
+ pcr_alg: crate::TpmAlg,
+) -> Result {
+ // Create restricted AK in endorsement hierarchy (TCG-compliant AK profile)
+ // 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();
+ signing_key_public_keys.insert(
+ "ecc_p256".to_string(),
+ EccPublicKeyCoords {
+ x: hex::encode(&signing_key.public_key.x),
+ y: hex::encode(&signing_key.public_key.y),
+ },
+ );
+
+ // Build PCR selection bitmap for Quote
+ let pcr_bitmap = build_pcr_bitmap(pcr_values);
+ let pcr_selection = vec![(pcr_alg, pcr_bitmap.as_slice())];
+
+ // Perform TPM2_Quote - signs PCR values with AK
+ let quote_result = tpm.quote(signing_key.handle, nonce, &pcr_selection)?;
+
+ let attestation_data = AttestationData {
+ attest_data: hex::encode("e_result.attest_data),
+ signature: hex::encode("e_result.signature),
+ };
+
+ Ok((
+ signing_key_public_keys,
+ attestation_data,
+ None,
+ Some(signing_key.handle),
+ ))
+}
+
+/// GCP attestation path: recreate AK from template and use TPM2_Quote
+fn attest_gcp(
+ tpm: &mut Tpm,
+ nonce: &[u8],
+ pcr_values: &[(u8, Vec)],
+ pcr_alg: crate::TpmAlg,
+) -> Result {
+ // 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)?;
+
+ // Recreate AK from template in endorsement hierarchy
+ let ak_result = tpm.create_primary_from_template(TPM_RH_ENDORSEMENT, &ak_template)?;
+
+ // Extract ECC public key coordinates
+ let signing_key_public_keys = match &ak_result.public_key {
+ PublicKey::Ecc(ecc) => {
+ let mut pks = HashMap::new();
+ pks.insert(
+ "ecc_p256".to_string(),
+ EccPublicKeyCoords {
+ x: hex::encode(&ecc.x),
+ y: hex::encode(&ecc.y),
+ },
+ );
+ pks
+ }
+ PublicKey::Rsa(_) => {
+ return Err(anyhow!(
+ "GCP ECC AK template unexpectedly created an RSA key"
+ ));
+ }
+ };
+
+ // Build PCR selection bitmap for Quote
+ // Include all PCRs from pcr_values
+ let pcr_bitmap = build_pcr_bitmap(pcr_values);
+ let pcr_selection = vec![(pcr_alg, pcr_bitmap.as_slice())];
+
+ // Perform TPM2_Quote - signs PCR values with AK
+ 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 attestation_data = AttestationData {
+ attest_data: hex::encode("e_result.attest_data),
+ signature: hex::encode("e_result.signature),
+ };
+
+ let gcp_attestation = Some(GcpAttestationData { ak_cert_chain });
+
+ Ok((
+ signing_key_public_keys,
+ attestation_data,
+ gcp_attestation,
+ Some(ak_result.handle),
+ ))
+}
+
+/// Build PCR bitmap from list of (index, value) pairs
+fn build_pcr_bitmap(pcr_values: &[(u8, Vec)]) -> Vec {
+ // TPM uses 3 bytes for PCR selection (24 PCRs max)
+ let mut bitmap = vec![0u8; 3];
+ for (idx, _) in pcr_values {
+ if *idx < 24 {
+ let byte_idx = (*idx / 8) as usize;
+ let bit_idx = *idx % 8;
+ bitmap[byte_idx] |= 1 << bit_idx;
+ }
+ }
+ bitmap
+}
+
+/// Read GCP ECC AK certificate chain from NV RAM and fetch issuer certs
+fn read_gcp_ak_cert_chain(tpm: &mut Tpm) -> Result {
+ // Read ECC AK certificate (matches the ECC AK template we use)
+ let ak_cert = tpm.nv_read(GCP_AK_CERT_NV_INDEX_ECC)?;
+
+ if !ak_cert.starts_with(&DER_SEQUENCE_LONG) {
+ return Err(anyhow!(
+ "GCP AK certificate is not in DER format (got {:02x?})",
+ &ak_cert[..ak_cert.len().min(4)]
+ ));
+ }
+
+ // Build full chain by fetching issuer certs via AIA
+ let chain = fetch_cert_chain(&ak_cert)?;
+
+ // Convert all certs to PEM and concatenate
+ let pem_chain: String = chain
+ .iter()
+ .map(|cert| der_to_pem(cert, "CERTIFICATE"))
+ .collect::>()
+ .join("");
+
+ Ok(pem_chain)
+}
diff --git a/crates/vaportpm-attest/src/bin/attest.rs b/crates/vaportpm-attest/src/bin/attest.rs
index d7fe8d5..d644db5 100644
--- a/crates/vaportpm-attest/src/bin/attest.rs
+++ b/crates/vaportpm-attest/src/bin/attest.rs
@@ -3,13 +3,27 @@
//! Generate TPM attestation document
//!
//! Outputs a JSON attestation document to stdout.
+//!
+//! Usage:
+//! vaportpm-attest [NONCE_HEX]
+//!
+//! Nonce is determined by (in order of priority):
+//! 1. Command-line argument (hex-encoded)
+//! 2. Stdin if not a tty (hex-encoded, whitespace trimmed)
+//! 3. Random 32-byte nonce
+use std::io::{self, IsTerminal, Read};
use std::process::ExitCode;
use vaportpm_attest::attest;
fn main() -> ExitCode {
- // Generate a random nonce if none provided
- let nonce: Vec = (0..32).map(|_| rand()).collect();
+ let nonce = match get_nonce() {
+ Ok(n) => n,
+ Err(e) => {
+ eprintln!("error: {}", e);
+ return ExitCode::FAILURE;
+ }
+ };
match attest(&nonce) {
Ok(json) => {
@@ -23,20 +37,42 @@ fn main() -> ExitCode {
}
}
-/// Simple random byte generator using /dev/urandom
-fn rand() -> u8 {
- use std::fs::File;
- use std::io::Read;
+/// Get nonce from command-line argument, stdin, or generate random
+fn get_nonce() -> Result, String> {
+ let args: Vec = std::env::args().collect();
- thread_local! {
- static URANDOM: std::cell::RefCell