From 21a610cc5cf3ff21445cf3a5aeb2568ffddc8705 Mon Sep 17 00:00:00 2001
From: HarryR <303926+HarryR@users.noreply.github.com>
Date: Tue, 3 Feb 2026 06:32:13 +0000
Subject: [PATCH 1/2] GCP support + more tests & fixtures
---
Cargo.toml | 5 +-
README.md | 23 +-
crates/vaportpm-attest/AWS-NITRO.md | 58 +-
crates/vaportpm-attest/CLAUDE.md | 30 -
crates/vaportpm-attest/Cargo.toml | 4 +
crates/vaportpm-attest/GCP.md | 163 ++++
crates/vaportpm-attest/README.md | 142 +--
.../vaportpm-attest/certs/aws-nitro-root.pem | 14 +
.../vaportpm-attest/certs/aws-nitro-root.txt | 39 +
.../vaportpm-attest/certs/gcp-ekak-root.pem | 35 +
.../vaportpm-attest/certs/gcp-ekak-root.txt | 93 ++
crates/vaportpm-attest/certs/pem2txt.sh | 14 +
crates/vaportpm-attest/src/a9n.rs | 419 +++++----
crates/vaportpm-attest/src/bin/nsmtest.rs | 46 -
crates/vaportpm-attest/src/bin/selftest.rs | 806 ------------------
crates/vaportpm-attest/src/cert.rs | 378 ++++++++
crates/vaportpm-attest/src/credential.rs | 209 -----
crates/vaportpm-attest/src/ek.rs | 413 ++++-----
crates/vaportpm-attest/src/lib.rs | 52 +-
crates/vaportpm-attest/src/roots.rs | 147 ++++
crates/vaportpm-verify/Cargo.toml | 14 +-
crates/vaportpm-verify/README.md | 60 +-
.../src/bin/selftest-verify.rs | 306 -------
crates/vaportpm-verify/src/bin/test-verify.rs | 183 ----
crates/vaportpm-verify/src/gcp.rs | 143 ++++
crates/vaportpm-verify/src/lib.rs | 555 +++++++-----
crates/vaportpm-verify/src/nitro.rs | 12 +-
crates/vaportpm-verify/src/tpm.rs | 587 ++-----------
crates/vaportpm-verify/src/x509.rs | 409 ++++++---
.../vaportpm-verify/test-gcp-amd-fixture.json | 48 ++
.../vaportpm-verify/test-gcp-tdx-fixture.json | 48 ++
.../vaportpm-verify/test-nitro-fixture.json | 29 +-
32 files changed, 2445 insertions(+), 3039 deletions(-)
delete mode 100644 crates/vaportpm-attest/CLAUDE.md
create mode 100644 crates/vaportpm-attest/GCP.md
create mode 100644 crates/vaportpm-attest/certs/aws-nitro-root.pem
create mode 100644 crates/vaportpm-attest/certs/aws-nitro-root.txt
create mode 100644 crates/vaportpm-attest/certs/gcp-ekak-root.pem
create mode 100644 crates/vaportpm-attest/certs/gcp-ekak-root.txt
create mode 100755 crates/vaportpm-attest/certs/pem2txt.sh
delete mode 100644 crates/vaportpm-attest/src/bin/nsmtest.rs
delete mode 100644 crates/vaportpm-attest/src/bin/selftest.rs
create mode 100644 crates/vaportpm-attest/src/cert.rs
delete mode 100644 crates/vaportpm-attest/src/credential.rs
create mode 100644 crates/vaportpm-attest/src/roots.rs
delete mode 100644 crates/vaportpm-verify/src/bin/selftest-verify.rs
delete mode 100644 crates/vaportpm-verify/src/bin/test-verify.rs
create mode 100644 crates/vaportpm-verify/src/gcp.rs
create mode 100644 crates/vaportpm-verify/test-gcp-amd-fixture.json
create mode 100644 crates/vaportpm-verify/test-gcp-tdx-fixture.json
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/README.md b/README.md
index f9feb82..036db09 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
-
-```
-vTPM β v[apor]TPM
-lockboot β [g]lockboot
-```
+**Cloud vTPM attestation library for Rust. Zero C dependencies.**
-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 AK certificate |
| 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)
@@ -60,7 +53,7 @@ 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!("Verified via: {:?}", result.provider);
println!("Nonce: {}", result.nonce);
}
```
diff --git a/crates/vaportpm-attest/AWS-NITRO.md b/crates/vaportpm-attest/AWS-NITRO.md
index 574f4dc..17f8bc9 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,20 @@ 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 long-term AK (no PCR binding - trust via Nitro document)
+let ak = tpm.create_primary_ecc_key(TPM_RH_OWNER)?;
-// 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)
+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 +70,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 +87,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
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..626632f 100644
--- a/crates/vaportpm-attest/Cargo.toml
+++ b/crates/vaportpm-attest/Cargo.toml
@@ -18,6 +18,10 @@ 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"
diff --git a/crates/vaportpm-attest/GCP.md b/crates/vaportpm-attest/GCP.md
new file mode 100644
index 0000000..67951db
--- /dev/null
+++ b/crates/vaportpm-attest/GCP.md
@@ -0,0 +1,163 @@
+# 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)
+
+## 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 (challenge from verifier)
+ 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 DER-encoded attest structure.
+
+## 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 proves freshness (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 + TPM2_Quote |
+| 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..3a19dc4 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 output = attest(nonce, user_data)?;
+// output.to_json() -> send 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 Shielded 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-----
+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
+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..e3a438d 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, TPM_RH_OWNER};
+
+/// 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,143 @@ pub fn attest(nonce: &[u8]) -> Result {
Ok(json)
}
+
+/// Nitro attestation path: create long-term AK and use TPM2_Quote
+///
+/// Creates an AK without PCR binding (long-term key), then uses TPM2_Quote
+/// to sign the PCR values. The AK is bound to the Nitro NSM document instead.
+fn attest_nitro(
+ tpm: &mut Tpm,
+ nonce: &[u8],
+ pcr_values: &[(u8, Vec)],
+ pcr_alg: crate::TpmAlg,
+) -> Result {
+ // Create long-term AK (no PCR binding - trust comes from Nitro NSM document)
+ let signing_key = tpm.create_primary_ecc_key(TPM_RH_OWNER)?;
+
+ 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/nsmtest.rs b/crates/vaportpm-attest/src/bin/nsmtest.rs
deleted file mode 100644
index 4b2827d..0000000
--- a/crates/vaportpm-attest/src/bin/nsmtest.rs
+++ /dev/null
@@ -1,46 +0,0 @@
-// SPDX-License-Identifier: MIT OR Apache-2.0
-
-//! AWS Nitro Security Module (NSM) test binary
-//!
-//! Tests NSM functionality via TPM vendor commands
-
-#![allow(clippy::needless_borrows_for_generic_args)]
-
-use anyhow::Result;
-use vaportpm_attest::{NsmOps, Tpm};
-
-fn main() -> Result<()> {
- println!("AWS Nitro TPM - NSM Test");
- println!("========================\n");
-
- // Open direct TPM device for NSM vendor commands
- println!("Opening TPM device (/dev/tpm0 - required for NSM vendor commands)...");
- let mut tpm = Tpm::open_direct()?;
- println!("β TPM device opened successfully\n");
-
- // Test 2: Attestation
- println!("Test 2: NSM Attestation");
- println!("-----------------------");
-
- println!("Requesting attestation document (no user_data, nonce, or public_key)...");
- match tpm.nsm_attest(None, None, None) {
- Ok(attestation_doc) => {
- println!("β Attestation successful!\n");
-
- println!("Attestation Document:");
- println!(" Size: {} bytes", attestation_doc.len());
- println!(
- " First 64 bytes (hex): {}",
- hex::encode(&attestation_doc[..64.min(attestation_doc.len())])
- );
-
- println!("\nβ All NSM tests passed!");
- Ok(())
- }
- Err(e) => {
- println!("β Attestation failed!");
- println!(" Error: {}", e);
- Err(e)
- }
- }
-}
diff --git a/crates/vaportpm-attest/src/bin/selftest.rs b/crates/vaportpm-attest/src/bin/selftest.rs
deleted file mode 100644
index c484fb5..0000000
--- a/crates/vaportpm-attest/src/bin/selftest.rs
+++ /dev/null
@@ -1,806 +0,0 @@
-// SPDX-License-Identifier: MIT OR Apache-2.0
-
-//! TPM 2.0 selftest binary
-//!
-//! Connects to the local TPM and runs basic functionality tests
-
-#![allow(clippy::needless_borrows_for_generic_args)]
-
-use anyhow::Result;
-use sha2::{Digest, Sha256};
-
-use vaportpm_attest::{
- compute_ecc_p256_name, der_to_pem, EkOps, NvOps, PcrOps, Tpm, TpmAlg,
- NV_INDEX_ECC_P256_EK_CERT, NV_INDEX_ECC_P384_EK_CERT, NV_INDEX_RSA_2048_EK_CERT,
- TPM_RH_ENDORSEMENT, TPM_RH_OWNER,
-};
-
-// TPM fixed property identifiers (TPM_PT)
-const TPM_PT_FAMILY_INDICATOR: u32 = 0x00000100;
-const TPM_PT_LEVEL: u32 = 0x00000101;
-const TPM_PT_REVISION: u32 = 0x00000102;
-const TPM_PT_DAY_OF_YEAR: u32 = 0x00000103;
-const TPM_PT_YEAR: u32 = 0x00000104;
-const TPM_PT_MANUFACTURER: u32 = 0x00000105;
-const TPM_PT_VENDOR_STRING_1: u32 = 0x00000106;
-const TPM_PT_VENDOR_TPM_TYPE: u32 = 0x0000010A;
-const TPM_PT_FIRMWARE_VERSION_1: u32 = 0x0000010B;
-const TPM_PT_FIRMWARE_VERSION_2: u32 = 0x0000010C;
-
-fn main() -> Result<()> {
- // Parse command-line arguments
- let args: Vec = std::env::args().collect();
-
- if args.iter().any(|arg| arg == "--help" || arg == "-h") {
- print_help();
- return Ok(());
- }
-
- // Run standard TPM tests
- standard_tests()
-}
-
-fn print_help() {
- println!("TPM 2.0 Selftest");
- println!("================\n");
- println!("Usage: selftest [OPTIONS]\n");
- println!("Options:");
- println!(" --help, -h Show this help message\n");
- println!("Runs standard TPM tests");
-}
-
-fn standard_tests() -> Result<()> {
- println!("TPM 2.0 Selftest");
- println!("================\n");
-
- // Open TPM device
- println!("Opening TPM device...");
- let mut tpm = Tpm::open()?;
- println!("β TPM device opened successfully\n");
-
- // Test -1: Query TPM properties (manufacturer, version, etc.)
- println!("Test -1: Querying TPM properties");
- println!("----------------------------------");
-
- // Helper to convert u32 to ASCII string (for manufacturer ID)
- let u32_to_ascii = |val: u32| -> String {
- let bytes = val.to_be_bytes();
- String::from_utf8_lossy(&bytes)
- .trim_end_matches('\0')
- .to_string()
- };
-
- // Query all properties
- match tpm.get_property(TPM_PT_FAMILY_INDICATOR) {
- Ok(val) => println!(
- "Family Indicator: {} (\"{}\")",
- u32_to_ascii(val),
- u32_to_ascii(val)
- ),
- Err(e) => println!("Family Indicator: Error: {}", e),
- }
-
- match tpm.get_property(TPM_PT_LEVEL) {
- Ok(val) => println!("Level: {}", val),
- Err(e) => println!("Level: Error: {}", e),
- }
-
- match tpm.get_property(TPM_PT_REVISION) {
- Ok(val) => println!(
- "Revision: {}.{}",
- (val >> 16) & 0xFFFF,
- val & 0xFFFF
- ),
- Err(e) => println!("Revision: Error: {}", e),
- }
-
- match tpm.get_property(TPM_PT_DAY_OF_YEAR) {
- Ok(val) => println!("Day of Year: {}", val),
- Err(e) => println!("Day of Year: Error: {}", e),
- }
-
- match tpm.get_property(TPM_PT_YEAR) {
- Ok(val) => println!("Year: {}", val),
- Err(e) => println!("Year: Error: {}", e),
- }
-
- match tpm.get_property(TPM_PT_MANUFACTURER) {
- Ok(val) => println!(
- "Manufacturer: 0x{:08X} (\"{}\")",
- val,
- u32_to_ascii(val)
- ),
- Err(e) => println!("Manufacturer: Error: {}", e),
- }
-
- // Vendor string is 16 bytes total across 4 properties
- let mut vendor_string = String::new();
- for i in 0..4 {
- match tpm.get_property(TPM_PT_VENDOR_STRING_1 + i) {
- Ok(val) => {
- vendor_string.push_str(&u32_to_ascii(val));
- if i == 0 {
- print!(
- "Vendor String {}: 0x{:08X} (\"{}\")",
- i + 1,
- val,
- u32_to_ascii(val)
- );
- } else {
- print!(
- "\nVendor String {}: 0x{:08X} (\"{}\")",
- i + 1,
- val,
- u32_to_ascii(val)
- );
- }
- }
- Err(e) => print!("\nVendor String {}: Error: {}", i + 1, e),
- }
- }
- println!("\nFull Vendor String: \"{}\"", vendor_string.trim());
-
- match tpm.get_property(TPM_PT_VENDOR_TPM_TYPE) {
- Ok(val) => println!("Vendor TPM Type: 0x{:08X}", val),
- Err(e) => println!("Vendor TPM Type: Error: {}", e),
- }
-
- match tpm.get_property(TPM_PT_FIRMWARE_VERSION_1) {
- Ok(val) => println!(
- "Firmware Version 1: 0x{:08X} ({}.{})",
- val,
- (val >> 16) & 0xFFFF,
- val & 0xFFFF
- ),
- Err(e) => println!("Firmware Version 1: Error: {}", e),
- }
-
- match tpm.get_property(TPM_PT_FIRMWARE_VERSION_2) {
- Ok(val) => println!(
- "Firmware Version 2: 0x{:08X} ({}.{})",
- val,
- (val >> 16) & 0xFFFF,
- val & 0xFFFF
- ),
- Err(e) => println!("Firmware Version 2: Error: {}", e),
- }
-
- println!("\nβ TPM properties queried successfully\n");
-
- // Test -0.5: Check if this is a Nitro TPM
- println!("Test -0.5: Checking for AWS Nitro TPM");
- println!("--------------------------------------");
- match tpm.is_nitro_tpm() {
- Ok(true) => println!("β This is an AWS Nitro TPM"),
- Ok(false) => println!("β This is NOT an AWS Nitro TPM"),
- Err(e) => println!("β Could not determine TPM type: {}", e),
- }
- println!();
-
- // Test 0: Query active PCR banks
- println!("Test 0: Querying active PCR banks");
- println!("----------------------------------");
- let banks = tpm.get_active_pcr_banks()?;
-
- if banks.is_empty() {
- println!("β No active PCR banks found!");
- } else {
- println!("Active PCR banks:");
- for bank in &banks {
- println!(
- " - {} (0x{:04X}) - {} bytes per digest",
- bank.name(),
- *bank as u16,
- bank.digest_size().unwrap_or(0)
- );
- }
- }
- println!("β Found {} active PCR bank(s)\n", banks.len());
-
- // Test 0.5: Query allocated PCRs
- println!("Test 0.5: Querying allocated PCRs (0-31)");
- println!("-----------------------------------------");
- let allocated_pcrs = tpm.get_allocated_pcrs()?;
- println!(
- "Allocated PCRs: {} out of 32 possible",
- allocated_pcrs.len()
- );
- for (pcr_idx, banks) in &allocated_pcrs {
- let bank_names: Vec = banks.iter().map(|b| b.name().to_string()).collect();
- println!(" PCR {:2}: {}", pcr_idx, bank_names.join(", "));
- }
- println!();
-
- // Test 1: Read all non-zero PCR values from all banks
- println!("Test 1: Reading all non-zero PCR values (all banks)");
- println!("----------------------------------------------------");
- let pcrs = tpm.read_nonzero_pcrs_all_banks()?;
-
- if pcrs.is_empty() {
- println!("No non-zero PCRs found (all PCRs are zero in all banks)");
- } else {
- for (index, alg, value) in &pcrs {
- println!("PCR {:2} [{}]: {}", index, alg.name(), hex::encode(&value));
- }
- }
-
- println!("\nβ PCR read successful");
- println!(" Found {} non-zero PCR values\n", pcrs.len());
-
- // Test 2: Extend PCR 23 (application-specific PCR)
- println!("Test 2: Extending PCR 23 with test data");
- println!("----------------------------------------");
- let test_extend_data = b"TPM selftest measurement v1.0";
- println!(
- "Data to extend: {:?}",
- std::str::from_utf8(test_extend_data).unwrap()
- );
- println!("Data length: {} bytes", test_extend_data.len());
-
- // Check which banks PCR 23 is allocated in
- println!("\nChecking which banks PCR 23 is allocated in...");
- let allocated_banks = tpm.get_pcr_allocated_banks(23)?;
- println!("PCR 23 is allocated in {} bank(s):", allocated_banks.len());
- for bank in &allocated_banks {
- println!(" - {}", bank.name());
- }
-
- // Read PCR 23 before extension (all banks)
- println!("\nPCR 23 values BEFORE extension:");
- let pcr23_before = tpm.pcr_read_all_banks(&[23])?;
- for (_index, alg, value) in &pcr23_before {
- println!(" [{}]: {}", alg.name(), hex::encode(&value));
- }
-
- // Extend PCR 23 with all allocated banks
- println!("\nExtending PCR 23...");
- tpm.pcr_extend(23, test_extend_data)?;
- println!("β PCR 23 extended successfully");
- println!(
- " Extended {} bank(s): {}",
- allocated_banks.len(),
- allocated_banks
- .iter()
- .map(|a| a.name())
- .collect::>()
- .join(", ")
- );
-
- // Read PCR 23 after extension (all banks)
- println!("\nPCR 23 values AFTER extension:");
- let pcr23_after = tpm.pcr_read_all_banks(&[23])?;
- for (_index, alg, value) in &pcr23_after {
- println!(" [{}]: {}", alg.name(), hex::encode(&value));
- }
-
- // Verify the value changed
- if pcr23_before != pcr23_after {
- println!("\nβ PCR 23 value changed as expected");
- } else {
- println!("\nβ Warning: PCR 23 value did not change");
- }
- println!();
-
- // Test 3: Create primary ECC key
- println!("Test 3: Creating primary ECC signing key");
- println!("-----------------------------------------");
- let key_result = tpm.create_primary_ecc_key(TPM_RH_OWNER)?;
- println!("β Primary key created");
- println!(" Handle: 0x{:08X}", key_result.handle);
- println!(" Public X: {}", hex::encode(&key_result.public_key.x));
- println!(" Public Y: {}", hex::encode(&key_result.public_key.y));
- println!();
-
- // Test 4: Sign data
- println!("Test 4: Signing test data");
- println!("-------------------------");
- let test_data = b"Hello, TPM!";
- let digest = Sha256::digest(test_data);
- println!("Test data: {:?}", std::str::from_utf8(test_data).unwrap());
- println!("SHA256 digest: {}", hex::encode(&digest));
-
- let signature = tpm.sign(key_result.handle, &digest)?;
- println!("β Signature created (DER-encoded)");
- println!(
- " Signature ({} bytes): {}",
- signature.len(),
- hex::encode(&signature)
- );
- println!();
-
- // Test 5: Check for Endorsement Key
- println!("Test 5: Checking for Endorsement Key (EK)");
- println!("------------------------------------------");
- match tpm.create_primary_ecc_key(TPM_RH_ENDORSEMENT) {
- Ok(ek_result) => {
- println!("β EK created/accessed in endorsement hierarchy");
- println!(" EK Handle: 0x{:08X}", ek_result.handle);
- println!(" EK Public X: {}", hex::encode(&ek_result.public_key.x));
- println!(" EK Public Y: {}", hex::encode(&ek_result.public_key.y));
-
- // Flush EK
- tpm.flush_context(ek_result.handle)?;
- }
- Err(e) => {
- println!("β Could not access endorsement hierarchy: {}", e);
- println!(" This is normal for:");
- println!(" - swtpm without EK provisioning");
- println!(" - Some local TPMs with EK password set");
- println!(" - Restricted TPM configurations");
- }
- }
- println!();
-
- // Test 6: Check for EK Certificate in NV RAM
- println!("Test 6: Checking for EK Certificate in NV RAM");
- println!("----------------------------------------------");
-
- check_ek_cert(
- &mut tpm,
- NV_INDEX_RSA_2048_EK_CERT,
- "RSA 2048 EK",
- "rsa_ek_cert",
- );
- check_ek_cert(
- &mut tpm,
- NV_INDEX_ECC_P256_EK_CERT,
- "ECC P-256 EK",
- "ecc_p256_ek_cert",
- );
- check_ek_cert(
- &mut tpm,
- NV_INDEX_ECC_P384_EK_CERT,
- "ECC P-384 EK",
- "ecc_p384_ek_cert",
- );
-
- println!();
-
- // Test 6.5: List all NV indices
- println!("Test 6.5: Enumerating all NV RAM indices");
- println!("-----------------------------------------");
-
- let nv_indices = tpm.nv_indices()?;
- println!("Found {} NV indices\n", nv_indices.len());
-
- for nv_index in &nv_indices {
- println!("NV Index: 0x{:08X}", nv_index);
-
- // Get NV index info
- match tpm.nv_readpublic(*nv_index) {
- Ok(info) => {
- println!(" Name Algorithm: 0x{:04X}", info.name_alg);
- println!(" Attributes: 0x{:08X}", info.attributes);
- decode_nv_attributes(info.attributes, 4);
- println!(" Auth Policy: {} bytes", info.auth_policy.len());
- if !info.auth_policy.is_empty() {
- println!(" {}", hex::encode(&info.auth_policy));
- }
- println!(" Data Size: {} bytes", info.data_size);
-
- // Try to read the data (may fail if auth is required)
- if info.data_size > 0 && info.data_size <= 2048 {
- match tpm.nv_read(*nv_index) {
- Ok(data) => {
- println!(" Data (hex dump):");
- hex_dump(&data, 4);
- }
- Err(e) => {
- println!(" Data: ", e);
- }
- }
- } else if info.data_size > 2048 {
- println!(" Data: ", info.data_size);
- }
- }
- Err(e) => {
- println!(" Error reading public info: {}", e);
- }
- }
- println!();
- }
-
- // Test 7: PCR-sealed key
- println!("Test 7: Creating PCR-sealed signing key");
- println!("-----------------------------------------");
-
- // Read ALL allocated PCRs (including zeros) for proper security policy
- println!("Reading all allocated PCRs...");
- let all_pcrs = tpm.read_all_allocated_pcrs()?;
- println!("Sealing to ALL {} allocated PCR values:", all_pcrs.len());
-
- for (index, alg, value) in &all_pcrs {
- println!(
- " PCR {:2} [{}]: {}",
- index,
- alg.name(),
- hex::encode(&value)
- );
- }
-
- // Get SHA-256 PCR values (must use SHA-256 for consistency with verification)
- let pcr_values: Vec<(u8, Vec)> = all_pcrs
- .iter()
- .filter(|(_, alg, _)| *alg == TpmAlg::Sha256)
- .map(|(idx, _, val)| (*idx, val.clone()))
- .collect();
-
- if pcr_values.is_empty() {
- anyhow::bail!("No SHA-256 PCRs allocated on this TPM");
- }
-
- println!("\nCreating key sealed to {} SHA-256 PCRs", pcr_values.len());
-
- // Compute policy from the PCR values we already have (SHA-256 bank)
- let auth_policy = Tpm::calculate_pcr_policy_digest(&pcr_values, TpmAlg::Sha256)?;
- let sealed_key = tpm.create_primary_ecc_key_with_policy(TPM_RH_OWNER, &auth_policy)?;
- println!("β PCR-sealed key created");
- println!(" Handle: 0x{:08X}", sealed_key.handle);
- println!(" Public X: {}", hex::encode(&sealed_key.public_key.x));
- println!(" Public Y: {}", hex::encode(&sealed_key.public_key.y));
-
- // Test 8: Certify the PCR-sealed key with the EK
- println!("\nTest 8: Certifying PCR-sealed key with EK");
- println!("------------------------------------------");
-
- // First, we need the EK
- println!("Creating/accessing EK...");
- let ek = match tpm.create_primary_ecc_key(TPM_RH_ENDORSEMENT) {
- Ok(ek) => {
- println!("β EK handle: 0x{:08X}", ek.handle);
- ek
- }
- Err(e) => {
- println!("β Could not access EK: {}", e);
- println!("Skipping certification test (EK not available)");
- tpm.flush_context(sealed_key.handle)?;
- println!();
- tpm.flush_context(key_result.handle)?;
- println!("All tests passed!");
- return Ok(());
- }
- };
-
- // Generate qualifying data (using a nonce/challenge for this example)
- let qualifying_data = b"attestation-challenge-12345";
- println!(
- "Using qualifying data: {:?}",
- std::str::from_utf8(qualifying_data).unwrap()
- );
-
- // Certify the PCR-sealed key using the EK
- println!("\nCertifying PCR-sealed key with EK...");
- let cert_result = tpm.certify(sealed_key.handle, ek.handle, qualifying_data)?;
-
- println!("β Certification complete!");
- println!(
- " Attestation data: {} bytes",
- cert_result.attest_data.len()
- );
- println!(
- " Signature: {} bytes (DER-encoded)",
- cert_result.signature.len()
- );
-
- // Save attestation for inspection
- if let Err(e) = std::fs::write("/tmp/attestation.bin", &cert_result.attest_data) {
- eprintln!(" Warning: Could not write /tmp/attestation.bin: {}", e);
- } else {
- println!(" Saved attestation to: /tmp/attestation.bin");
- }
-
- if let Err(e) = std::fs::write("/tmp/attestation_signature.der", &cert_result.signature) {
- eprintln!(
- " Warning: Could not write /tmp/attestation_signature.der: {}",
- e
- );
- } else {
- println!(" Saved signature to: /tmp/attestation_signature.der");
- }
-
- // Cleanup EK and sealed key (but keep key_result for more tests)
- tpm.flush_context(ek.handle)?;
- tpm.flush_context(sealed_key.handle)?;
-
- // Test 9: Standard EK creation
- println!("\nTest 9: Standard EK Creation (TCG Template)");
- println!("--------------------------------------------");
-
- // Try to create the TCG standard EK
- match tpm.create_standard_ek() {
- Ok(standard_ek) => {
- println!("β Standard EK created using TCG EK Credential Profile template");
- println!(" Handle: 0x{:08X}", standard_ek.handle);
- println!(" Public X: {}", hex::encode(&standard_ek.public_key.x));
- println!(" Public Y: {}", hex::encode(&standard_ek.public_key.y));
- println!(" Note: Certificate comparison requires vaportpm_attest-verify selftest");
-
- tpm.flush_context(standard_ek.handle)?;
- }
- Err(e) => {
- println!("β Could not create standard EK: {}", e);
- println!(" (Endorsement hierarchy may require authentication)");
- }
- }
- println!();
-
- // Test 10: ReadPublic and name verification
- println!("Test 10: ReadPublic and Name Verification");
- println!("------------------------------------------");
-
- let read_result = tpm.read_public(key_result.handle)?;
- println!("ReadPublic returned:");
- println!(" Public area: {} bytes", read_result.public_area.len());
- println!(" Name: {}", hex::encode(&read_result.name));
-
- // Compute expected name and compare
- // For our signing key, authPolicy is empty
- let computed_name = compute_ecc_p256_name(
- &key_result.public_key.x,
- &key_result.public_key.y,
- &[], // empty policy for basic signing key
- );
- println!(" Computed name: {}", hex::encode(&computed_name));
-
- if read_result.name == computed_name {
- println!("β TPM's name matches our computed name");
- } else {
- println!("β Name mismatch - TPM uses different computation");
- println!(" (This is expected if key has non-empty authPolicy)");
- }
- println!();
-
- // Test 11: Policy session operations
- println!("Test 11: Policy Session Operations");
- println!("-----------------------------------");
-
- // Start a policy session
- let policy_session = tpm.start_policy_session()?;
- println!("β Policy session started: 0x{:08X}", policy_session);
-
- // Get initial policy digest (should be all zeros)
- let initial_digest = tpm.policy_get_digest(policy_session)?;
- println!(" Initial policy digest: {}", hex::encode(&initial_digest));
-
- let expected_empty = vec![0u8; 32];
- if initial_digest == expected_empty {
- println!("β Initial digest is empty (all zeros) as expected");
- } else {
- println!("β Initial digest is not empty - unexpected");
- }
-
- // Execute PolicySecret(TPM_RH_ENDORSEMENT)
- println!("\nExecuting PolicySecret(TPM_RH_ENDORSEMENT)...");
- match tpm.policy_secret(policy_session, TPM_RH_ENDORSEMENT) {
- Ok(()) => {
- println!("β PolicySecret executed successfully");
-
- // Get updated policy digest
- let updated_digest = tpm.policy_get_digest(policy_session)?;
- println!(" Updated policy digest: {}", hex::encode(&updated_digest));
-
- // Expected digest for PolicySecret(TPM_RH_ENDORSEMENT) with SHA-256
- // This is the standard EK authPolicy
- let expected_ek_policy: [u8; 32] = [
- 0x83, 0x71, 0x97, 0x67, 0x44, 0x84, 0xB3, 0xF8, 0x1A, 0x90, 0xCC, 0x8D, 0x46, 0xA5,
- 0xD7, 0x24, 0xFD, 0x52, 0xD7, 0x6E, 0x06, 0x52, 0x0B, 0x64, 0xF2, 0xA1, 0xDA, 0x1B,
- 0x33, 0x14, 0x69, 0xAA,
- ];
- println!(
- " Expected EK policy: {}",
- hex::encode(&expected_ek_policy)
- );
-
- if updated_digest == expected_ek_policy {
- println!("β Policy digest matches standard EK authPolicy!");
- } else {
- println!("β Policy digest does not match expected value");
- }
- }
- Err(e) => {
- println!("β PolicySecret failed: {}", e);
- println!(" (This is normal if endorsement hierarchy requires authentication)");
- }
- }
-
- // Flush policy session
- tpm.flush_context(policy_session)?;
- println!("β Policy session flushed");
-
- println!();
-
- // Cleanup: Flush the signing key handle
- tpm.flush_context(key_result.handle)?;
-
- println!("======================");
- println!("All tests completed!");
- println!("======================");
- Ok(())
-}
-
-/// Check for and display an EK certificate from NV RAM
-fn check_ek_cert(tpm: &mut Tpm, nv_index: u32, description: &str, filename_base: &str) {
- print!("Checking {} cert (0x{:08X})... ", description, nv_index);
- match tpm.nv_read(nv_index) {
- Ok(cert) => {
- println!("β Found!");
- println!(" Size: {} bytes", cert.len());
- if !cert.is_empty() {
- let der_path = format!("/tmp/{}.der", filename_base);
- let pem_path = format!("/tmp/{}.pem", filename_base);
-
- // Save raw DER to file
- if let Err(e) = std::fs::write(&der_path, &cert) {
- eprintln!(" Warning: Could not write {}: {}", der_path, e);
- } else {
- println!(" Saved DER to: {}", der_path);
- println!(
- " Verify with: openssl x509 -inform DER -in {} -text -noout",
- der_path
- );
- }
-
- // Check if it looks like a DER-encoded certificate
- if cert.starts_with(&[0x30, 0x82]) {
- println!(" Format: DER-encoded X.509 certificate");
-
- let pem = der_to_pem(&cert, "CERTIFICATE");
-
- // Save PEM to file
- if let Err(e) = std::fs::write(&pem_path, &pem) {
- eprintln!(" Warning: Could not write {}: {}", pem_path, e);
- } else {
- println!(" Saved PEM to: {}", pem_path);
- println!(" Verify with: openssl x509 -in {} -text -noout", pem_path);
- }
-
- println!("\n{}", pem);
- } else {
- println!(" Format: Unknown (not standard DER)");
- println!(
- " First 32 bytes: {}",
- hex::encode(&cert[..cert.len().min(32)])
- );
- }
- }
- }
- Err(e) => {
- println!("Not found");
- println!(" Error: {}", e);
- }
- }
-}
-
-/// Decode NV index attributes bitfield
-fn decode_nv_attributes(attrs: u32, indent_spaces: usize) {
- let indent = " ".repeat(indent_spaces);
-
- // TPMA_NV bit definitions from TPM 2.0 Part 2, Section 13.2
- if attrs & (1 << 1) != 0 {
- println!("{} - PPWRITE: Platform can write", indent);
- }
- if attrs & (1 << 2) != 0 {
- println!("{} - OWNERWRITE: Owner can write", indent);
- }
- if attrs & (1 << 3) != 0 {
- println!("{} - AUTHWRITE: Auth required for write", indent);
- }
- if attrs & (1 << 4) != 0 {
- println!("{} - POLICYWRITE: Policy required for write", indent);
- }
-
- // Bits 7-10: TPM_NT (NV Type)
- let nv_type = (attrs >> 4) & 0xF;
- print!("{} - TYPE: ", indent);
- match nv_type {
- 0x0 => println!("Ordinary (0x0)"),
- 0x1 => println!("Counter (0x1)"),
- 0x2 => println!("Bits (0x2)"),
- 0x4 => println!("Extend (0x4)"),
- 0x8 => println!("PIN Fail (0x8)"),
- 0x9 => println!("PIN Pass (0x9)"),
- _ => println!("Unknown (0x{:X})", nv_type),
- }
-
- if attrs & (1 << 10) != 0 {
- println!("{} - POLICY_DELETE: Policy required to delete", indent);
- }
- if attrs & (1 << 11) != 0 {
- println!("{} - WRITELOCKED: Currently write-locked", indent);
- }
- if attrs & (1 << 12) != 0 {
- println!("{} - WRITEALL: Must write full size at once", indent);
- }
- if attrs & (1 << 13) != 0 {
- println!(
- "{} - WRITEDEFINE: Can be written after definition",
- indent
- );
- }
- if attrs & (1 << 14) != 0 {
- println!(
- "{} - WRITE_STCLEAR: Write locked until TPM restart",
- indent
- );
- }
- if attrs & (1 << 15) != 0 {
- println!("{} - GLOBALLOCK: Write locked by global lock", indent);
- }
- if attrs & (1 << 16) != 0 {
- println!("{} - PPREAD: Platform can read", indent);
- }
- if attrs & (1 << 17) != 0 {
- println!("{} - OWNERREAD: Owner can read", indent);
- }
- if attrs & (1 << 18) != 0 {
- println!("{} - AUTHREAD: Auth required for read", indent);
- }
- if attrs & (1 << 19) != 0 {
- println!("{} - POLICYREAD: Policy required for read", indent);
- }
- if attrs & (1 << 20) != 0 {
- println!(
- "{} - NO_DA: Not subject to dictionary attack protection",
- indent
- );
- }
- if attrs & (1 << 21) != 0 {
- println!("{} - ORDERLY: Only updated on orderly shutdown", indent);
- }
- if attrs & (1 << 22) != 0 {
- println!("{} - CLEAR_STCLEAR: Cleared on TPM reset", indent);
- }
- if attrs & (1 << 23) != 0 {
- println!("{} - READLOCKED: Currently read-locked", indent);
- }
- if attrs & (1 << 24) != 0 {
- println!("{} - WRITTEN: Has been written", indent);
- }
- if attrs & (1 << 25) != 0 {
- println!("{} - PLATFORMCREATE: Created by platform", indent);
- }
- if attrs & (1 << 26) != 0 {
- println!("{} - READ_STCLEAR: Readable after restart", indent);
- }
-}
-
-/// Display a hex dump of data with optional indentation
-fn hex_dump(data: &[u8], indent_spaces: usize) {
- let indent = " ".repeat(indent_spaces);
- const BYTES_PER_LINE: usize = 16;
-
- for (line_num, chunk) in data.chunks(BYTES_PER_LINE).enumerate() {
- let offset = line_num * BYTES_PER_LINE;
- print!("{} {:04x} ", indent, offset);
-
- // Print hex bytes
- for (i, byte) in chunk.iter().enumerate() {
- print!("{:02x} ", byte);
- if i == 7 {
- print!(" "); // Extra space in the middle
- }
- }
-
- // Padding for last line if not full
- if chunk.len() < BYTES_PER_LINE {
- for i in chunk.len()..BYTES_PER_LINE {
- print!(" ");
- if i == 7 {
- print!(" ");
- }
- }
- }
-
- // Print ASCII representation
- print!(" |");
- for byte in chunk {
- let c = if *byte >= 0x20 && *byte <= 0x7e {
- *byte as char
- } else {
- '.'
- };
- print!("{}", c);
- }
- println!("|");
- }
-}
diff --git a/crates/vaportpm-attest/src/cert.rs b/crates/vaportpm-attest/src/cert.rs
new file mode 100644
index 0000000..e4c949d
--- /dev/null
+++ b/crates/vaportpm-attest/src/cert.rs
@@ -0,0 +1,378 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+//! X.509 certificate parsing and chain fetching
+//!
+//! Provides utilities for working with X.509 certificates:
+//! - PEM/DER conversion
+//! - Certificate chain fetching via AIA (Authority Information Access) URLs
+//! - Extension extraction (SKI, AKI, AIA)
+
+use anyhow::{anyhow, Result};
+use base64::{engine::general_purpose::STANDARD, Engine as _};
+use der::oid::ObjectIdentifier;
+use der::Decode;
+use std::io::{BufRead, BufReader, Read, Write};
+use std::net::TcpStream;
+use std::time::Duration;
+use x509_cert::Certificate;
+
+/// DER SEQUENCE tag with 2-byte length (0x30 0x82)
+/// Used to detect valid X.509 certificates in DER format
+pub const DER_SEQUENCE_LONG: [u8; 2] = [0x30, 0x82];
+
+// X.509 extension OIDs
+const OID_SUBJECT_KEY_IDENTIFIER: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.29.14");
+const OID_AUTHORITY_KEY_IDENTIFIER: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.29.35");
+const OID_AUTHORITY_INFO_ACCESS: ObjectIdentifier =
+ ObjectIdentifier::new_unwrap("1.3.6.1.5.5.7.1.1");
+
+// AIA access method OID for caIssuers
+const OID_CA_ISSUERS: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.6.1.5.5.7.48.2");
+
+/// 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');
+ }
+ pem.push_str(&format!("-----END {}-----\n", label));
+ pem
+}
+
+/// Convert PEM-encoded certificate to DER
+pub fn pem_to_der(pem: &str) -> Result> {
+ let mut in_cert = false;
+ let mut base64_data = String::new();
+
+ for line in pem.lines() {
+ if line.contains("-----BEGIN CERTIFICATE-----") {
+ in_cert = true;
+ } else if line.contains("-----END CERTIFICATE-----") {
+ break;
+ } else if in_cert {
+ base64_data.push_str(line.trim());
+ }
+ }
+
+ if base64_data.is_empty() {
+ return Err(anyhow!("No certificate found in PEM data"));
+ }
+
+ STANDARD
+ .decode(&base64_data)
+ .map_err(|e| anyhow!("Base64 decode error: {}", e))
+}
+
+/// Parse a DER-encoded certificate
+fn parse_certificate(cert_der: &[u8]) -> Option {
+ Certificate::from_der(cert_der).ok()
+}
+
+/// Check if a certificate is self-signed (issuer == subject)
+pub fn is_self_signed(cert_der: &[u8]) -> bool {
+ let cert = match parse_certificate(cert_der) {
+ Some(c) => c,
+ None => return false,
+ };
+ cert.tbs_certificate.issuer == cert.tbs_certificate.subject
+}
+
+/// Extract Subject Key Identifier (SKI) from a DER certificate
+///
+/// SKI is in extension OID 2.5.29.14
+/// Returns the raw key identifier bytes (typically 20 bytes SHA-1)
+pub fn extract_ski(cert_der: &[u8]) -> Option> {
+ let cert = parse_certificate(cert_der)?;
+ let extensions = cert.tbs_certificate.extensions.as_ref()?;
+
+ for ext in extensions.iter() {
+ if ext.extn_id == OID_SUBJECT_KEY_IDENTIFIER {
+ // SubjectKeyIdentifier ::= KeyIdentifier
+ // KeyIdentifier ::= OCTET STRING
+ // The extn_value is already an OctetString, containing the DER-encoded OCTET STRING
+ let inner = der::asn1::OctetString::from_der(ext.extn_value.as_bytes()).ok()?;
+ return Some(inner.as_bytes().to_vec());
+ }
+ }
+ None
+}
+
+/// Extract Authority Key Identifier (AKI) from a DER certificate
+///
+/// AKI is in extension OID 2.5.29.35
+/// Returns the keyIdentifier field (typically 20 bytes SHA-1)
+pub fn extract_aki(cert_der: &[u8]) -> Option> {
+ let cert = parse_certificate(cert_der)?;
+ let extensions = cert.tbs_certificate.extensions.as_ref()?;
+
+ for ext in extensions.iter() {
+ if ext.extn_id == OID_AUTHORITY_KEY_IDENTIFIER {
+ // AuthorityKeyIdentifier ::= SEQUENCE {
+ // keyIdentifier [0] KeyIdentifier OPTIONAL,
+ // authorityCertIssuer [1] GeneralNames OPTIONAL,
+ // authorityCertSerialNumber [2] CertificateSerialNumber OPTIONAL
+ // }
+ let bytes = ext.extn_value.as_bytes();
+ return parse_aki_extension(bytes);
+ }
+ }
+ None
+}
+
+/// Parse the AuthorityKeyIdentifier extension value
+fn parse_aki_extension(bytes: &[u8]) -> Option> {
+ // AuthorityKeyIdentifier ::= SEQUENCE { keyIdentifier [0] IMPLICIT OCTET STRING, ... }
+ // bytes is the raw extension value
+
+ // Skip SEQUENCE header
+ if bytes.first()? != &0x30 {
+ return None;
+ }
+ let (seq_len, seq_start) = parse_der_length(&bytes[1..])?;
+ let seq_bytes = bytes.get(1 + seq_start..1 + seq_start + seq_len)?;
+
+ if seq_bytes.is_empty() {
+ return None;
+ }
+
+ // keyIdentifier is [0] IMPLICIT OCTET STRING
+ // Tag 0x80 = context-specific, primitive, tag 0
+ if seq_bytes[0] == 0x80 {
+ let (len, value_start) = parse_der_length(&seq_bytes[1..])?;
+ return Some(
+ seq_bytes
+ .get(1 + value_start..1 + value_start + len)?
+ .to_vec(),
+ );
+ }
+ None
+}
+
+/// Parse DER length encoding, returns (length, bytes_consumed)
+fn parse_der_length(bytes: &[u8]) -> Option<(usize, usize)> {
+ let len_byte = *bytes.first()?;
+ if len_byte & 0x80 == 0 {
+ // Short form
+ Some((len_byte as usize, 1))
+ } else {
+ // Long form
+ let num_bytes = (len_byte & 0x7F) as usize;
+ if num_bytes == 0 || num_bytes > 4 {
+ return None;
+ }
+ let mut len = 0usize;
+ for i in 0..num_bytes {
+ len = (len << 8) | (*bytes.get(1 + i)? as usize);
+ }
+ Some((len, 1 + num_bytes))
+ }
+}
+
+/// Extract Authority Information Access URL (caIssuers) from a DER certificate
+pub fn extract_aia_url(cert_der: &[u8]) -> Option {
+ let cert = parse_certificate(cert_der)?;
+ let extensions = cert.tbs_certificate.extensions.as_ref()?;
+
+ for ext in extensions.iter() {
+ if ext.extn_id == OID_AUTHORITY_INFO_ACCESS {
+ return parse_aia_extension(ext.extn_value.as_bytes());
+ }
+ }
+ None
+}
+
+/// Parse the AuthorityInfoAccessSyntax extension to find caIssuers URL
+fn parse_aia_extension(bytes: &[u8]) -> Option {
+ // AuthorityInfoAccessSyntax ::= SEQUENCE OF AccessDescription
+ // AccessDescription ::= SEQUENCE { accessMethod OID, accessLocation GeneralName }
+
+ // Skip outer SEQUENCE header
+ if bytes.first()? != &0x30 {
+ return None;
+ }
+ let (seq_len, seq_start) = parse_der_length(&bytes[1..])?;
+ let mut pos = 1 + seq_start;
+ let seq_end = pos + seq_len;
+
+ while pos < seq_end && pos < bytes.len() {
+ // Each AccessDescription is a SEQUENCE
+ if bytes.get(pos)? != &0x30 {
+ break;
+ }
+ let (desc_len, desc_header) = parse_der_length(&bytes[pos + 1..])?;
+ let desc_start = pos + 1 + desc_header;
+ let desc_end = desc_start + desc_len;
+
+ // Parse the OID at the start of the description
+ if let Ok(oid) = ObjectIdentifier::from_der(&bytes[desc_start..desc_end]) {
+ if oid == OID_CA_ISSUERS {
+ // Skip past the OID to get the GeneralName
+ let oid_encoded_len = oid.as_bytes().len() + 2; // +2 for tag and length
+ let gn_start = desc_start + oid_encoded_len;
+
+ // GeneralName uniformResourceIdentifier [6] IA5String
+ // Tag 0x86 = context-specific, primitive, tag 6
+ if bytes.get(gn_start)? == &0x86 {
+ let (url_len, url_header) = parse_der_length(&bytes[gn_start + 1..])?;
+ let url_start = gn_start + 1 + url_header;
+ let url_bytes = bytes.get(url_start..url_start + url_len)?;
+ return String::from_utf8(url_bytes.to_vec()).ok();
+ }
+ }
+ }
+
+ // Move to next AccessDescription
+ pos = desc_end;
+ }
+ None
+}
+
+/// Fetch the complete certificate chain by following AKI/SKI or AIA URLs
+///
+/// First attempts to find issuer certificates from embedded trust anchors
+/// using AKI/SKI matching. Falls back to AIA URL fetching if no embedded
+/// cert matches.
+pub fn fetch_cert_chain(leaf_cert: &[u8]) -> Result>> {
+ use crate::roots;
+
+ let mut chain = vec![leaf_cert.to_vec()];
+ let mut current_cert = leaf_cert.to_vec();
+
+ // Follow chain up to 10 levels (more than enough for any chain)
+ for _ in 0..10 {
+ // Check if current cert is self-signed (root)
+ if is_self_signed(¤t_cert) {
+ break;
+ }
+
+ // First, try to find issuer from embedded certs using AKI/SKI
+ if let Some(aki) = extract_aki(¤t_cert) {
+ if let Some(issuer_pem) = roots::find_issuer_by_aki(&aki) {
+ let issuer_der = pem_to_der(issuer_pem)?;
+ chain.push(issuer_der.clone());
+ current_cert = issuer_der;
+ continue;
+ }
+ }
+
+ // Fallback: Extract AIA URL from current certificate
+ let aia_url = match extract_aia_url(¤t_cert) {
+ Some(url) => url,
+ None => {
+ // No AIA URL and no embedded cert - can't fetch more certs
+ break;
+ }
+ };
+
+ // Fetch issuer certificate via HTTP
+ let issuer_cert = fetch_certificate(&aia_url)?;
+
+ if !issuer_cert.starts_with(&DER_SEQUENCE_LONG) {
+ return Err(anyhow!(
+ "Fetched certificate is not in DER format from {}",
+ aia_url
+ ));
+ }
+
+ chain.push(issuer_cert.clone());
+ current_cert = issuer_cert;
+ }
+
+ Ok(chain)
+}
+
+/// Fetch a certificate from an HTTP URL (no TLS support - AIA URLs are HTTP)
+pub fn fetch_certificate(url: &str) -> Result> {
+ // Parse URL - only support http://
+ if !url.starts_with("http://") {
+ return Err(anyhow!("Only HTTP URLs are supported: {}", url));
+ }
+
+ let url_without_scheme = &url[7..]; // Skip "http://"
+ let (host_port, path) = match url_without_scheme.find('/') {
+ Some(idx) => (&url_without_scheme[..idx], &url_without_scheme[idx..]),
+ None => (url_without_scheme, "/"),
+ };
+
+ let (host, port) = match host_port.find(':') {
+ Some(idx) => (
+ &host_port[..idx],
+ host_port[idx + 1..].parse::().unwrap_or(80),
+ ),
+ None => (host_port, 80u16),
+ };
+
+ // Connect with timeout
+ let addr = format!("{}:{}", host, port);
+ let mut stream =
+ TcpStream::connect(&addr).map_err(|e| anyhow!("Failed to connect to {}: {}", addr, e))?;
+ stream.set_read_timeout(Some(Duration::from_secs(10)))?;
+ stream.set_write_timeout(Some(Duration::from_secs(10)))?;
+
+ // Send HTTP/1.1 GET request
+ let request = format!(
+ "GET {} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\nUser-Agent: vaportpm-attest\r\n\r\n",
+ path, host
+ );
+ stream.write_all(request.as_bytes())?;
+
+ // Read response
+ let mut reader = BufReader::new(stream);
+
+ // Parse status line
+ let mut status_line = String::new();
+ reader.read_line(&mut status_line)?;
+ if !status_line.starts_with("HTTP/1.1 200") && !status_line.starts_with("HTTP/1.0 200") {
+ return Err(anyhow!("HTTP request failed: {}", status_line.trim()));
+ }
+
+ // Parse headers to find Content-Length or chunked transfer
+ let mut content_length: Option = None;
+ let mut chunked = false;
+ loop {
+ let mut header = String::new();
+ reader.read_line(&mut header)?;
+ if header == "\r\n" || header == "\n" {
+ break;
+ }
+ let header_lower = header.to_lowercase();
+ if header_lower.starts_with("content-length:") {
+ if let Some(len_str) = header.split(':').nth(1) {
+ content_length = len_str.trim().parse().ok();
+ }
+ } else if header_lower.starts_with("transfer-encoding:") && header_lower.contains("chunked")
+ {
+ chunked = true;
+ }
+ }
+
+ // Read body
+ let mut body = Vec::new();
+ if chunked {
+ // Read chunked encoding
+ loop {
+ let mut chunk_size_line = String::new();
+ reader.read_line(&mut chunk_size_line)?;
+ let chunk_size = usize::from_str_radix(chunk_size_line.trim(), 16).unwrap_or(0);
+ if chunk_size == 0 {
+ break;
+ }
+ let mut chunk = vec![0u8; chunk_size];
+ reader.read_exact(&mut chunk)?;
+ body.extend(chunk);
+ // Read trailing \r\n
+ let mut trailing = [0u8; 2];
+ let _ = reader.read_exact(&mut trailing);
+ }
+ } else if let Some(len) = content_length {
+ body.resize(len, 0);
+ reader.read_exact(&mut body)?;
+ } else {
+ // Read until connection closes
+ reader.read_to_end(&mut body)?;
+ }
+
+ Ok(body)
+}
diff --git a/crates/vaportpm-attest/src/credential.rs b/crates/vaportpm-attest/src/credential.rs
deleted file mode 100644
index 69f1d2a..0000000
--- a/crates/vaportpm-attest/src/credential.rs
+++ /dev/null
@@ -1,209 +0,0 @@
-// SPDX-License-Identifier: MIT OR Apache-2.0
-
-//! TPM2 policy session operations
-//!
-//! Provides policy session management for TPM authorization flows.
-//! Also includes utility functions for computing TPM object names.
-
-use anyhow::Result;
-use sha2::{Digest, Sha256};
-
-use crate::{CommandBuffer, Tpm, TpmAlg, TpmCc, TpmSt, TPM_RH_NULL, TPM_RS_PW, TPM_SE_POLICY};
-
-/// Result from ReadPublic
-#[derive(Debug)]
-pub struct ReadPublicResult {
- /// The raw TPMT_PUBLIC structure
- pub public_area: Vec,
- /// The object's name as computed by the TPM
- pub name: Vec,
-}
-
-/// Policy session operations
-impl Tpm {
- /// Read the public area and name of an object
- ///
- /// This is useful for verifying that our name computation matches
- /// what the TPM actually has for an object.
- pub fn read_public(&mut self, object_handle: u32) -> Result {
- let command = CommandBuffer::new()
- .write_u32(object_handle)
- .finalize(TpmSt::NoSessions, TpmCc::ReadPublic);
-
- let mut resp = self.transmit(&command)?;
-
- // Parse response: outPublic (TPM2B_PUBLIC), name (TPM2B_NAME), qualifiedName (TPM2B_NAME)
- let public_area = resp.read_tpm2b()?;
- let name = resp.read_tpm2b()?;
- let _qualified_name = resp.read_tpm2b()?;
-
- Ok(ReadPublicResult { public_area, name })
- }
-
- /// Start a policy session
- ///
- /// Creates a new policy session that can be used for policy-based authorization.
- /// The session must be flushed after use with `flush_context()`.
- pub fn start_policy_session(&mut self) -> Result {
- // TPM2_StartAuthSession
- // tpmKey = TPM_RH_NULL (no salt)
- // bind = TPM_RH_NULL (no bind)
- // nonceCaller = empty (TPM will generate)
- // encryptedSalt = empty
- // sessionType = TPM_SE_POLICY
- // symmetric = TPM_ALG_NULL
- // authHash = TPM_ALG_SHA256
-
- // Generate a random nonce (some TPMs require non-empty nonceCaller)
- // Use hash of current time as simple entropy source
- let nonce_data = Sha256::digest(
- std::time::SystemTime::now()
- .duration_since(std::time::UNIX_EPOCH)
- .unwrap_or_default()
- .as_nanos()
- .to_le_bytes(),
- );
- let nonce_caller = &nonce_data[..16]; // Use 16 bytes
-
- let command = CommandBuffer::new()
- .write_u32(TPM_RH_NULL) // tpmKey
- .write_u32(TPM_RH_NULL) // bind
- .write_tpm2b(nonce_caller) // nonceCaller (16 bytes)
- .write_u16(0) // encryptedSalt size (no salt)
- .write_u8(TPM_SE_POLICY) // sessionType
- .write_u16(TpmAlg::Null as u16) // symmetric.algorithm = TPM_ALG_NULL
- .write_u16(TpmAlg::Sha256 as u16) // authHash = TPM_ALG_SHA256
- .finalize(TpmSt::NoSessions, TpmCc::StartAuthSession);
-
- let mut resp = self.transmit(&command)?;
-
- // Parse response: sessionHandle, nonceTPM
- let session_handle = resp.read_u32()?;
- let _nonce_tpm = resp.read_tpm2b()?; // We don't need the nonce for simple policy
-
- Ok(session_handle)
- }
-
- /// Execute PolicySecret command
- ///
- /// Satisfies a policy that requires PolicySecret(authHandle).
- /// For standard EK, authHandle should be TPM_RH_ENDORSEMENT.
- pub fn policy_secret(&mut self, policy_session: u32, auth_handle: u32) -> Result<()> {
- // TPM2_PolicySecret
- // authHandle: the entity providing authorization (TPM_RH_ENDORSEMENT)
- // policySession: the policy session to update
- // nonceTPM: empty
- // cpHashA: empty
- // policyRef: empty
- // expiration: 0
-
- let command = CommandBuffer::new()
- .write_u32(auth_handle) // authHandle
- .write_u32(policy_session) // policySession
- // Authorization for authHandle (password session, empty)
- .write_u32(9) // authorizationSize
- .write_u32(TPM_RS_PW)
- .write_u16(0) // nonce
- .write_u8(0) // attributes
- .write_u16(0) // password
- // Parameters
- .write_u16(0) // nonceTPM size
- .write_u16(0) // cpHashA size
- .write_u16(0) // policyRef size
- .write_u32(0) // expiration (INT32, 0 = no expiration)
- .finalize(TpmSt::Sessions, TpmCc::PolicySecret);
-
- let mut resp = self.transmit(&command)?;
-
- // Parse response - skip timeout and policyTicket
- let _parameter_size = resp.read_u32()?;
- // We don't need the timeout or ticket for our purposes
-
- Ok(())
- }
-
- /// Get the current policy digest from a policy session
- ///
- /// Useful for debugging to verify the policy matches expectations.
- pub fn policy_get_digest(&mut self, policy_session: u32) -> Result> {
- // TPM2_PolicyGetDigest
- // Input: policySession handle
- // Output: policyDigest (TPM2B_DIGEST)
-
- let command = CommandBuffer::new()
- .write_u32(policy_session)
- .finalize(TpmSt::NoSessions, TpmCc::PolicyGetDigest);
-
- let mut resp = self.transmit(&command)?;
-
- // Parse response: policyDigest (TPM2B_DIGEST)
- let digest = resp.read_tpm2b()?;
-
- Ok(digest.to_vec())
- }
-}
-
-/// Compute TPM object name from public key and authPolicy
-///
-/// name = nameAlg || H(TPMT_PUBLIC)
-///
-/// For ECC P-256 signing keys with PCR policy.
-pub fn compute_ecc_p256_name(pubkey_x: &[u8], pubkey_y: &[u8], auth_policy: &[u8]) -> Vec {
- // Build TPMT_PUBLIC for ECC P-256 signing key
- let mut public_area = Vec::new();
-
- // type: TPM_ALG_ECC (0x0023)
- public_area.extend_from_slice(&0x0023u16.to_be_bytes());
- // nameAlg: TPM_ALG_SHA256 (0x000B)
- public_area.extend_from_slice(&0x000Bu16.to_be_bytes());
- // objectAttributes: fixedTPM | fixedParent | sensitiveDataOrigin | userWithAuth | decrypt | sign
- // bits: 1,4,5,6,17,18 = 0x00060072
- public_area.extend_from_slice(&0x00060072u32.to_be_bytes());
- // authPolicy (TPM2B_DIGEST)
- public_area.extend_from_slice(&(auth_policy.len() as u16).to_be_bytes());
- public_area.extend_from_slice(auth_policy);
- // parameters (TPMS_ECC_PARMS):
- // symmetric: TPM_ALG_NULL (0x0010)
- public_area.extend_from_slice(&0x0010u16.to_be_bytes());
- // scheme: TPM_ALG_NULL (0x0010)
- public_area.extend_from_slice(&0x0010u16.to_be_bytes());
- // curveID: TPM_ECC_NIST_P256 (0x0003)
- public_area.extend_from_slice(&0x0003u16.to_be_bytes());
- // kdf: TPM_ALG_NULL (0x0010)
- public_area.extend_from_slice(&0x0010u16.to_be_bytes());
- // unique (TPMS_ECC_POINT):
- // x (TPM2B_ECC_PARAMETER)
- public_area.extend_from_slice(&(pubkey_x.len() as u16).to_be_bytes());
- public_area.extend_from_slice(pubkey_x);
- // y (TPM2B_ECC_PARAMETER)
- public_area.extend_from_slice(&(pubkey_y.len() as u16).to_be_bytes());
- public_area.extend_from_slice(pubkey_y);
-
- // name = nameAlg || H(TPMT_PUBLIC)
- let mut name = Vec::new();
- name.extend_from_slice(&0x000Bu16.to_be_bytes()); // SHA256
- name.extend_from_slice(&Sha256::digest(&public_area));
-
- name
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_compute_name_deterministic() {
- let x = [0x01u8; 32];
- let y = [0x02u8; 32];
- let policy = [0x03u8; 32];
-
- let name1 = compute_ecc_p256_name(&x, &y, &policy);
- let name2 = compute_ecc_p256_name(&x, &y, &policy);
-
- assert_eq!(name1, name2);
- // Name should be 2 (alg) + 32 (hash) = 34 bytes
- assert_eq!(name1.len(), 34);
- // Should start with SHA256 algorithm ID
- assert_eq!(&name1[0..2], &[0x00, 0x0B]);
- }
-}
diff --git a/crates/vaportpm-attest/src/ek.rs b/crates/vaportpm-attest/src/ek.rs
index f5b4c37..74054fd 100644
--- a/crates/vaportpm-attest/src/ek.rs
+++ b/crates/vaportpm-attest/src/ek.rs
@@ -1,66 +1,58 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
-//! EK and key operations
+//! Key operations
//!
//! Provides TPM key management operations including:
-//! - Creating ECC primary keys (standard and custom templates)
-//! - TCG-compliant standard EK creation
-//! - Signing operations
-//! - Key certification
+//! - Creating ECC primary keys
+//! - Creating keys from templates (GCP AK)
+//! - TPM2_Quote for PCR attestation
use anyhow::{bail, Result};
use crate::{
- CertifyResult, CommandBuffer, EccPublicKey, ObjectAttributes, PrimaryKeyResult, ResponseBuffer,
- Tpm, TpmAlg, TpmCc, TpmEccCurve, TpmSt, TPM_RH_ENDORSEMENT, TPM_RH_NULL, TPM_RS_PW,
+ CommandBuffer, EccPublicKey, ObjectAttributes, PrimaryKeyResult, PublicKey, QuoteResult,
+ ResponseBuffer, RsaPublicKey, TemplateKeyResult, Tpm, TpmAlg, TpmCc, TpmEccCurve, TpmSt,
};
-/// Standard EK authPolicy digest for SHA-256 (TCG EK Credential Profile 2.6)
-/// This is PolicySecret(TPM_RH_ENDORSEMENT) with SHA-256
-const STANDARD_EK_AUTH_POLICY: [u8; 32] = [
- 0x83, 0x71, 0x97, 0x67, 0x44, 0x84, 0xB3, 0xF8, 0x1A, 0x90, 0xCC, 0x8D, 0x46, 0xA5, 0xD7, 0x24,
- 0xFD, 0x52, 0xD7, 0x6E, 0x06, 0x52, 0x0B, 0x64, 0xF2, 0xA1, 0xDA, 0x1B, 0x33, 0x14, 0x69, 0xAA,
-];
-
-/// EK and key operations extension trait
-pub trait EkOps {
- /// Create a primary ECC P-256 signing key in the specified hierarchy (no policy)
+/// Key operations extension trait
+pub trait KeyOps {
+ /// Create a primary ECC P-256 signing key in the specified hierarchy
fn create_primary_ecc_key(&mut self, hierarchy: u32) -> Result;
- /// Create a primary ECC P-256 signing key with a specific authPolicy
- fn create_primary_ecc_key_with_policy(
+ /// Create a primary key from a raw TPMT_PUBLIC template
+ ///
+ /// This takes template bytes (as stored in NV RAM by cloud providers like GCP)
+ /// and creates a primary key in the specified hierarchy.
+ fn create_primary_from_template(
&mut self,
hierarchy: u32,
- auth_policy: &[u8],
- ) -> Result;
+ template: &[u8],
+ ) -> Result;
- /// Create the TCG standard ECC P-256 Endorsement Key
+ /// TPM2_Quote - sign current PCR values with a signing key
+ ///
+ /// This command generates a signed attestation over the selected PCRs.
+ /// The quote includes:
+ /// - The current PCR values (hashed into a digest)
+ /// - A nonce/qualifying data (for freshness)
+ /// - Signed by the specified key (typically an AK)
///
- /// This creates an EK using the TCG standard template, which should produce
- /// a key whose public key matches the one in the EK certificate (if the certificate
- /// was issued for this TPM using the standard template).
+ /// # Arguments
+ /// * `sign_handle` - Handle of the signing key (AK)
+ /// * `qualifying_data` - Nonce/challenge data (becomes extraData in TPMS_ATTEST)
+ /// * `pcr_selection` - List of (algorithm, PCR indices) to include
///
- /// The standard EK is a decrypt-only key (cannot sign) with:
- /// - Attributes: fixedTPM, fixedParent, sensitiveDataOrigin, adminWithPolicy, restricted, decrypt
- /// - authPolicy: PolicySecret(TPM_RH_ENDORSEMENT)
- /// - Symmetric: AES-128-CFB
- /// - Curve: NIST P-256
- fn create_standard_ek(&mut self) -> Result;
-
- /// Sign data with a TPM key (returns DER-encoded ECDSA signature)
- fn sign(&mut self, key_handle: u32, digest: &[u8]) -> Result>;
-
- /// Certify a key using another key (e.g., certify signing key with EK)
- /// Returns (attestation_data, signature)
- fn certify(
+ /// # Returns
+ /// QuoteResult containing the TPMS_ATTEST structure (type=QUOTE) and signature
+ fn quote(
&mut self,
- object_handle: u32,
sign_handle: u32,
qualifying_data: &[u8],
- ) -> Result;
+ pcr_selection: &[(TpmAlg, &[u8])],
+ ) -> Result;
}
-impl EkOps for Tpm {
+impl KeyOps for Tpm {
fn create_primary_ecc_key(&mut self, hierarchy: u32) -> Result {
let public_area = build_ecc_public_area();
@@ -114,18 +106,20 @@ impl EkOps for Tpm {
Ok(PrimaryKeyResult { handle, public_key })
}
- fn create_standard_ek(&mut self) -> Result {
- let public_area = build_standard_ek_public_area();
-
+ fn create_primary_from_template(
+ &mut self,
+ hierarchy: u32,
+ template: &[u8],
+ ) -> Result {
let command = CommandBuffer::new()
- .write_u32(TPM_RH_ENDORSEMENT)
+ .write_u32(hierarchy)
.write_auth_empty_pw()
// inSensitive (TPM2B_SENSITIVE_CREATE)
.write_u16(4)
.write_u16(0) // userAuth size = 0
.write_u16(0) // data size = 0
- // inPublic (TPM2B_PUBLIC)
- .write_tpm2b(&public_area)
+ // inPublic (TPM2B_PUBLIC) - the raw template from NV RAM
+ .write_tpm2b(template)
// outsideInfo (TPM2B_DATA) - empty
.write_u16(0)
// creationPCR (TPML_PCR_SELECTION) - empty
@@ -143,7 +137,12 @@ impl EkOps for Tpm {
// Read outPublic (TPM2B_PUBLIC)
let public_size = resp.read_u16()? as usize;
let public_data = resp.read_bytes(public_size)?;
- let public_key = parse_ecc_public_key(public_data)?;
+
+ // Parse the public key based on algorithm type
+ let public_key = parse_public_key(public_data)?;
+
+ // Save the raw public bytes for later use
+ let public_bytes = public_data.to_vec();
// Skip remaining CreatePrimary output parameters
let bytes_read = resp.offset() - param_start;
@@ -162,97 +161,40 @@ impl EkOps for Tpm {
);
}
- Ok(PrimaryKeyResult { handle, public_key })
- }
-
- fn sign(&mut self, key_handle: u32, digest: &[u8]) -> Result> {
- if digest.len() != 32 {
- bail!("Digest must be 32 bytes for SHA-256");
- }
-
- let command = CommandBuffer::new()
- .write_u32(key_handle)
- .write_auth_empty_pw()
- // digest (TPM2B_DIGEST)
- .write_tpm2b(digest)
- // inScheme (TPMT_SIG_SCHEME) - ECDSA with SHA256
- .write_u16(TpmAlg::EcDsa as u16)
- .write_u16(TpmAlg::Sha256 as u16)
- // validation (TPMT_TK_HASHCHECK) - NULL ticket
- .write_u16(0x8024) // TPM_ST_HASHCHECK
- .write_u32(TPM_RH_NULL)
- .write_u16(0) // digest size = 0
- .finalize(TpmSt::Sessions, TpmCc::Sign);
- let mut resp = self.transmit(&command)?;
-
- // Parse response
- let parameter_size = resp.read_u32()?;
- let param_start = resp.offset();
-
- // TPMT_SIGNATURE
- let sig_alg = resp.read_u16()?;
- if sig_alg != TpmAlg::EcDsa as u16 {
- bail!("Unexpected signature algorithm: 0x{:04X}", sig_alg);
- }
-
- let hash_alg = resp.read_u16()?;
- if hash_alg != TpmAlg::Sha256 as u16 {
- bail!("Unexpected hash algorithm: 0x{:04X}", hash_alg);
- }
-
- // TPMS_SIGNATURE_ECC
- let r = resp.read_tpm2b()?;
- let s = resp.read_tpm2b()?;
-
- // Verify we read exactly parameter_size bytes
- let bytes_read = resp.offset() - param_start;
- if bytes_read != parameter_size as usize {
- bail!(
- "Parameter size mismatch in Sign: TPM said {} bytes, we read {} bytes",
- parameter_size,
- bytes_read
- );
- }
-
- // Convert to DER-encoded signature
- Ok(encode_ecdsa_der_signature(&r, &s))
+ Ok(TemplateKeyResult {
+ handle,
+ public_key,
+ public_bytes,
+ })
}
- fn certify(
+ fn quote(
&mut self,
- object_handle: u32,
sign_handle: u32,
qualifying_data: &[u8],
- ) -> Result {
+ pcr_selection: &[(TpmAlg, &[u8])],
+ ) -> Result {
+ // Build TPML_PCR_SELECTION
+ let pcr_select = build_pcr_selection(pcr_selection);
+
let command = CommandBuffer::new()
- .write_u32(object_handle)
.write_u32(sign_handle)
- // Authorization area - two sessions (one for each handle)
- // Total auth size = 2 * 9 = 18 bytes
- .write_u32(18)
- // Auth for objectHandle (password session, empty password)
- .write_u32(TPM_RS_PW)
- .write_u16(0) // nonce
- .write_u8(0) // attributes
- .write_u16(0) // password
- // Auth for signHandle (password session, empty password)
- .write_u32(TPM_RS_PW)
- .write_u16(0) // nonce
- .write_u8(0) // attributes
- .write_u16(0) // password
+ .write_auth_empty_pw()
// qualifyingData (TPM2B_DATA)
.write_tpm2b(qualifying_data)
// inScheme (TPMT_SIG_SCHEME) - ECDSA with SHA256
.write_u16(TpmAlg::EcDsa as u16)
.write_u16(TpmAlg::Sha256 as u16)
- .finalize(TpmSt::Sessions, TpmCc::Certify);
+ // PCR selection (TPML_PCR_SELECTION)
+ .write_bytes(&pcr_select)
+ .finalize(TpmSt::Sessions, TpmCc::Quote);
let mut resp = self.transmit(&command)?;
// Parse response
let parameter_size = resp.read_u32()?;
let param_start = resp.offset();
- // certifyInfo (TPM2B_ATTEST)
+ // quoted (TPM2B_ATTEST)
let attest_data = resp.read_tpm2b()?;
// signature (TPMT_SIGNATURE)
@@ -274,7 +216,7 @@ impl EkOps for Tpm {
let bytes_read = resp.offset() - param_start;
if bytes_read != parameter_size as usize {
bail!(
- "Parameter size mismatch in Certify: TPM said {} bytes, we read {} bytes",
+ "Parameter size mismatch in Quote: TPM said {} bytes, we read {} bytes",
parameter_size,
bytes_read
);
@@ -282,66 +224,36 @@ impl EkOps for Tpm {
let signature = encode_ecdsa_der_signature(&r, &s);
- Ok(CertifyResult {
+ Ok(QuoteResult {
attest_data: attest_data.to_vec(),
signature,
})
}
+}
- fn create_primary_ecc_key_with_policy(
- &mut self,
- hierarchy: u32,
- auth_policy: &[u8],
- ) -> Result {
- let public_area = build_ecc_public_area_with_policy(auth_policy);
-
- let command = CommandBuffer::new()
- .write_u32(hierarchy)
- .write_auth_empty_pw()
- // inSensitive (TPM2B_SENSITIVE_CREATE)
- .write_u16(4)
- .write_u16(0) // userAuth size = 0
- .write_u16(0) // data size = 0
- // inPublic (TPM2B_PUBLIC) - with authPolicy
- .write_tpm2b(&public_area)
- // outsideInfo (TPM2B_DATA) - empty
- .write_u16(0)
- // creationPCR (TPML_PCR_SELECTION) - empty
- .write_u32(0)
- .finalize(TpmSt::Sessions, TpmCc::CreatePrimary);
- let mut resp = self.transmit(&command)?;
-
- // Parse response
- let handle = resp.read_u32()?;
- let parameter_size = resp.read_u32()?;
-
- // Track where parameters start
- let param_start = resp.offset();
-
- // Read outPublic (TPM2B_PUBLIC)
- let public_size = resp.read_u16()? as usize;
- let public_data = resp.read_bytes(public_size)?;
- let public_key = parse_ecc_public_key(public_data)?;
-
- // Skip remaining CreatePrimary output parameters
- let bytes_read = resp.offset() - param_start;
- if bytes_read < parameter_size as usize {
- let remaining = parameter_size as usize - bytes_read;
- resp.read_bytes(remaining)?;
- }
-
- // Verify we read exactly parameter_size bytes
- let final_bytes_read = resp.offset() - param_start;
- if final_bytes_read != parameter_size as usize {
- bail!(
- "Parameter size mismatch: TPM said {} bytes, we read {} bytes",
- parameter_size,
- final_bytes_read
- );
- }
-
- Ok(PrimaryKeyResult { handle, public_key })
+/// Build TPML_PCR_SELECTION structure
+///
+/// # Arguments
+/// * `selections` - List of (algorithm, PCR bitmap) pairs
+/// PCR bitmap is a byte array where bit N of byte M indicates PCR (M*8 + N)
+fn build_pcr_selection(selections: &[(TpmAlg, &[u8])]) -> Vec {
+ let mut buf = Vec::new();
+
+ // count (4 bytes)
+ buf.extend_from_slice(&(selections.len() as u32).to_be_bytes());
+
+ for (alg, pcr_bitmap) in selections {
+ // TPMS_PCR_SELECTION:
+ // hash (2 bytes) - algorithm
+ buf.extend_from_slice(&(*alg as u16).to_be_bytes());
+ // sizeofSelect (1 byte) - typically 3 for 24 PCRs
+ let size = pcr_bitmap.len().min(255) as u8;
+ buf.push(size);
+ // pcrSelect (variable) - bitmap
+ buf.extend_from_slice(pcr_bitmap);
}
+
+ buf
}
/// Build a TPM2B_PUBLIC structure for an ECC P-256 signing key
@@ -371,72 +283,93 @@ fn build_ecc_public_area() -> Vec {
.into_vec()
}
-/// Build a TPM2B_PUBLIC structure for an ECC P-256 signing key with authPolicy
-fn build_ecc_public_area_with_policy(auth_policy: &[u8]) -> Vec {
- let attrs = ObjectAttributes::new()
- .fixed_tpm()
- .fixed_parent()
- .sensitive_data_origin()
- .user_with_auth()
- .decrypt()
- .sign_encrypt();
-
- CommandBuffer::new()
- // TPMT_PUBLIC
- .write_u16(TpmAlg::Ecc as u16) // type
- .write_u16(TpmAlg::Sha256 as u16) // nameAlg
- .write_u32(attrs.value()) // objectAttributes
- .write_tpm2b(auth_policy) // authPolicy
- // parameters (TPMS_ECC_PARMS)
- .write_u16(TpmAlg::Null as u16) // symmetric
- .write_u16(TpmAlg::Null as u16) // scheme
- .write_u16(TpmEccCurve::NistP256 as u16) // curveID
- .write_u16(TpmAlg::Null as u16) // kdf
- // unique (TPMS_ECC_POINT) - empty
- .write_u16(0) // x size
- .write_u16(0) // y size
- .into_vec()
-}
-
-/// Build a TPM2B_PUBLIC structure for the TCG standard ECC P-256 EK
-///
-/// Per TCG EK Credential Profile 2.6, the standard EK template (Template L-2) has:
-/// - Object attributes: 0x000300b2 (fixedTPM, fixedParent, sensitiveDataOrigin,
-/// adminWithPolicy, restricted, decrypt)
-/// - authPolicy: PolicySecret(TPM_RH_ENDORSEMENT)
-/// - Symmetric: AES-128-CFB
-/// - Curve: NIST P-256
-/// - Unique: x = 32 zero bytes, y = 32 zero bytes
-fn build_standard_ek_public_area() -> Vec {
- let attrs = ObjectAttributes::new()
- .fixed_tpm()
- .fixed_parent()
- .sensitive_data_origin()
- .admin_with_policy()
- .restricted()
- .decrypt();
+/// Parse public key from TPMT_PUBLIC structure (RSA or ECC)
+pub(crate) fn parse_public_key(data: &[u8]) -> Result {
+ let mut resp = ResponseBuffer::new(data.to_vec());
- // Per TCG EK Credential Profile, unique field must be 32 zero bytes for x and y
- let zero_32 = [0u8; 32];
+ // Peek at key type to determine parsing strategy
+ let key_type = resp.read_u16()?;
+ let name_alg = resp.read_u16()?;
+ let object_attributes = resp.read_u32()?;
+ let auth_policy = resp.read_tpm2b()?;
- CommandBuffer::new()
- // TPMT_PUBLIC
- .write_u16(TpmAlg::Ecc as u16) // type
- .write_u16(TpmAlg::Sha256 as u16) // nameAlg
- .write_u32(attrs.value()) // objectAttributes
- .write_tpm2b(&STANDARD_EK_AUTH_POLICY) // authPolicy
- // parameters (TPMS_ECC_PARMS)
- // symmetric (TPMT_SYM_DEF_OBJECT) - AES-128-CFB
- .write_u16(TpmAlg::Aes as u16)
- .write_u16(128) // keyBits
- .write_u16(TpmAlg::Cfb as u16) // mode
- .write_u16(TpmAlg::Null as u16) // scheme (decrypt-only, no signing)
- .write_u16(TpmEccCurve::NistP256 as u16) // curveID
- .write_u16(TpmAlg::Null as u16) // kdf
- // unique (TPMS_ECC_POINT) - 32 zero bytes each per TCG template
- .write_tpm2b(&zero_32) // x
- .write_tpm2b(&zero_32) // y
- .into_vec()
+ match key_type {
+ 0x0001 => {
+ // RSA key (TPM_ALG_RSA)
+ // Parse TPMS_RSA_PARMS
+ let symmetric = resp.read_u16()?;
+
+ // If symmetric is not NULL, read symmetric details
+ if symmetric != TpmAlg::Null as u16 {
+ let _key_bits = resp.read_u16()?;
+ let _mode = resp.read_u16()?;
+ }
+
+ let scheme = resp.read_u16()?;
+
+ // If scheme is not NULL, read scheme details
+ if scheme != TpmAlg::Null as u16 {
+ let _scheme_detail = resp.read_u16()?;
+ }
+
+ let key_bits = resp.read_u16()?;
+ let exponent = resp.read_u32()?;
+
+ // Read unique (TPM2B_PUBLIC_KEY_RSA = TPM2B containing modulus)
+ let modulus = resp.read_tpm2b()?;
+
+ Ok(PublicKey::Rsa(RsaPublicKey {
+ key_type,
+ name_alg,
+ object_attributes,
+ auth_policy,
+ symmetric,
+ scheme,
+ key_bits,
+ exponent,
+ modulus,
+ }))
+ }
+ 0x0023 => {
+ // ECC key (TPM_ALG_ECC)
+ // Parse TPMS_ECC_PARMS
+ let symmetric = resp.read_u16()?;
+
+ // If symmetric is not NULL, read symmetric details
+ if symmetric != TpmAlg::Null as u16 {
+ let _key_bits = resp.read_u16()?;
+ let _mode = resp.read_u16()?;
+ }
+
+ let scheme = resp.read_u16()?;
+
+ // Only read scheme details if scheme is not NULL
+ if scheme != TpmAlg::Null as u16 {
+ let _scheme_detail = resp.read_u16()?;
+ }
+
+ let curve_id = resp.read_u16()?;
+ let kdf = resp.read_u16()?;
+
+ // Read unique (TPMS_ECC_POINT)
+ let x = resp.read_tpm2b()?;
+ let y = resp.read_tpm2b()?;
+
+ Ok(PublicKey::Ecc(EccPublicKey {
+ key_type,
+ name_alg,
+ object_attributes,
+ auth_policy,
+ symmetric,
+ scheme,
+ curve_id,
+ kdf,
+ x,
+ y,
+ }))
+ }
+ _ => bail!("Unsupported key type: 0x{:04X}", key_type),
+ }
}
/// Parse ECC public key from TPMT_PUBLIC structure
diff --git a/crates/vaportpm-attest/src/lib.rs b/crates/vaportpm-attest/src/lib.rs
index 2b8ad78..9267f3c 100644
--- a/crates/vaportpm-attest/src/lib.rs
+++ b/crates/vaportpm-attest/src/lib.rs
@@ -10,22 +10,21 @@ use std::fs::{File, OpenOptions};
use std::io::{Read, Write};
pub mod a9n;
-pub mod credential;
+pub mod cert;
pub mod ek;
pub mod nsm;
pub mod nv;
pub mod pcr;
+pub mod roots;
// Re-export extension traits for convenience
-pub use ek::EkOps;
+pub use ek::KeyOps;
pub use nsm::NsmOps;
pub use nv::NvOps;
pub use pcr::PcrOps;
-// Re-export credential functions
-pub use credential::{compute_ecc_p256_name, ReadPublicResult};
-
-pub use a9n::{attest, der_to_pem};
+pub use a9n::attest;
+pub use cert::{der_to_pem, extract_aki, extract_ski, pem_to_der};
/// TPM 2.0 command codes
#[repr(u32)]
@@ -72,6 +71,7 @@ pub enum TpmRc {
#[repr(u16)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TpmAlg {
+ Rsa = 0x0001,
Sha1 = 0x0004,
Sha256 = 0x000B,
Sha384 = 0x000C,
@@ -80,6 +80,7 @@ pub enum TpmAlg {
Cfb = 0x0043,
Ecc = 0x0023,
EcDsa = 0x0018,
+ RsaSsa = 0x0014,
Null = 0x0010,
}
@@ -98,6 +99,7 @@ impl TpmAlg {
/// Get the algorithm name as a string
pub fn name(&self) -> &'static str {
match self {
+ TpmAlg::Rsa => "rsa",
TpmAlg::Sha1 => "sha1",
TpmAlg::Sha256 => "sha256",
TpmAlg::Sha384 => "sha384",
@@ -106,6 +108,7 @@ impl TpmAlg {
TpmAlg::Cfb => "cfb",
TpmAlg::Ecc => "ecc",
TpmAlg::EcDsa => "ecdsa",
+ TpmAlg::RsaSsa => "rsassa",
TpmAlg::Null => "null",
}
}
@@ -113,12 +116,14 @@ impl TpmAlg {
/// Try to convert a u16 to a TpmAlg
pub fn from_u16(val: u16) -> Option {
match val {
+ 0x0001 => Some(TpmAlg::Rsa),
0x0004 => Some(TpmAlg::Sha1),
0x000B => Some(TpmAlg::Sha256),
0x000C => Some(TpmAlg::Sha384),
0x000D => Some(TpmAlg::Sha512),
0x0023 => Some(TpmAlg::Ecc),
0x0018 => Some(TpmAlg::EcDsa),
+ 0x0014 => Some(TpmAlg::RsaSsa),
0x0010 => Some(TpmAlg::Null),
_ => None,
}
@@ -585,15 +590,44 @@ pub struct EccPublicKey {
pub y: Vec,
}
+/// RSA public key information parsed from TPMT_PUBLIC
+#[derive(Debug, Clone)]
+pub struct RsaPublicKey {
+ pub key_type: u16,
+ pub name_alg: u16,
+ pub object_attributes: u32,
+ pub auth_policy: Vec,
+ pub symmetric: u16,
+ pub scheme: u16,
+ pub key_bits: u16,
+ pub exponent: u32,
+ pub modulus: Vec,
+}
+
+/// Generic public key that can be either RSA or ECC
+#[derive(Debug, Clone)]
+pub enum PublicKey {
+ Rsa(RsaPublicKey),
+ Ecc(EccPublicKey),
+}
+
/// Result from creating a primary key
pub struct PrimaryKeyResult {
pub handle: u32,
pub public_key: EccPublicKey,
}
-/// Result from TPM2_Certify
+/// Result from creating a primary key from a template (may be RSA or ECC)
+pub struct TemplateKeyResult {
+ pub handle: u32,
+ pub public_key: PublicKey,
+ /// The raw TPM2B_PUBLIC bytes returned by the TPM
+ pub public_bytes: Vec,
+}
+
+/// Result from TPM2_Quote
#[derive(Debug)]
-pub struct CertifyResult {
- pub attest_data: Vec, // TPMS_ATTEST structure
+pub struct QuoteResult {
+ pub attest_data: Vec, // TPMS_ATTEST structure (type=QUOTE)
pub signature: Vec, // DER-encoded ECDSA signature
}
diff --git a/crates/vaportpm-attest/src/roots.rs b/crates/vaportpm-attest/src/roots.rs
new file mode 100644
index 0000000..044eebd
--- /dev/null
+++ b/crates/vaportpm-attest/src/roots.rs
@@ -0,0 +1,147 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+//! Embedded trust anchor certificates
+//!
+//! These certificates are the canonical trust anchors for cloud provider
+//! attestation verification. Hashes are derived from the certificate data,
+//! not hardcoded separately.
+
+use crate::cert::{extract_ski, pem_to_der};
+use std::sync::OnceLock;
+
+// ============================================================================
+// Embedded certificate PEMs
+// ============================================================================
+
+/// AWS Nitro Enclave Root CA certificate (P-384/ES384)
+///
+/// Subject: CN=aws.nitro-enclaves, OU=AWS, O=Amazon, C=US
+/// Valid: 2019-10-28 to 2049-10-28
+pub const AWS_NITRO_ROOT_PEM: &str = include_str!("../certs/aws-nitro-root.pem");
+
+/// GCP EK/AK CA Root certificate (RSA-4096)
+///
+/// This is the shared root CA for all GCP Shielded VMs (both Intel TDX and AMD SEV).
+/// Intermediate certificates are project-specific and must be fetched by the attestor.
+/// Subject: CN=EK/AK CA Root, OU=Google Cloud, O=Google LLC, L=Mountain View, ST=California, C=US
+/// Valid: 2022-07-08 to 2122-07-08
+pub const GCP_EKAK_ROOT_PEM: &str = include_str!("../certs/gcp-ekak-root.pem");
+
+// ============================================================================
+// Certificate metadata
+// ============================================================================
+
+/// Certificate metadata extracted from PEM
+#[derive(Debug, Clone)]
+pub struct CertInfo {
+ /// PEM-encoded certificate
+ pub pem: &'static str,
+ /// Subject Key Identifier (typically SHA-1 of public key)
+ pub ski: Vec,
+}
+
+/// Lazily-initialized certificate info cache
+static CERT_INFOS: OnceLock> = OnceLock::new();
+
+/// Get all embedded certificate infos, initializing on first call
+fn get_cert_infos() -> &'static [CertInfo] {
+ CERT_INFOS.get_or_init(|| {
+ // These are compile-time embedded certs - panic if they fail to parse
+ vec![
+ extract_cert_info(AWS_NITRO_ROOT_PEM, "AWS Nitro root"),
+ extract_cert_info(GCP_EKAK_ROOT_PEM, "GCP EK/AK root"),
+ ]
+ })
+}
+
+/// Extract certificate info from PEM
+///
+/// Panics if the certificate cannot be parsed - these are embedded constants
+/// that must always be valid.
+fn extract_cert_info(pem: &'static str, name: &str) -> CertInfo {
+ let der = pem_to_der(pem).unwrap_or_else(|e| panic!("{name} cert: invalid PEM: {e}"));
+ let ski = extract_ski(&der).unwrap_or_else(|| panic!("{name} cert: missing SKI extension"));
+
+ CertInfo { pem, ski }
+}
+
+// ============================================================================
+// Lookup functions
+// ============================================================================
+
+/// Find an issuer certificate by matching a child certificate's AKI to a parent's SKI
+///
+/// Returns the PEM-encoded certificate if found.
+pub fn find_issuer_by_aki(aki: &[u8]) -> Option<&'static str> {
+ for info in get_cert_infos() {
+ if info.ski == aki {
+ return Some(info.pem);
+ }
+ }
+ None
+}
+
+/// Get the SKI for an embedded certificate
+///
+/// Returns the Subject Key Identifier bytes.
+pub fn get_ski(pem: &str) -> Option> {
+ for info in get_cert_infos() {
+ if info.pem == pem {
+ return Some(info.ski.clone());
+ }
+ }
+ None
+}
+
+/// Check if a certificate (by PEM) is a known trust anchor
+pub fn is_known_root(pem: &str) -> bool {
+ pem == AWS_NITRO_ROOT_PEM || pem == GCP_EKAK_ROOT_PEM
+}
+
+/// Get all embedded root certificate PEMs
+pub fn get_all_roots() -> &'static [&'static str] {
+ &[AWS_NITRO_ROOT_PEM, GCP_EKAK_ROOT_PEM]
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_aws_nitro_root_has_ski() {
+ let der = pem_to_der(AWS_NITRO_ROOT_PEM).expect("Valid PEM");
+ let ski = extract_ski(&der);
+ assert!(ski.is_some(), "AWS Nitro root should have SKI");
+ let ski = ski.unwrap();
+ assert_eq!(ski.len(), 20, "SKI should be 20 bytes (SHA-1)");
+ }
+
+ #[test]
+ fn test_gcp_root_has_ski() {
+ let der = pem_to_der(GCP_EKAK_ROOT_PEM).expect("Valid PEM");
+ let ski = extract_ski(&der);
+ assert!(ski.is_some(), "GCP root should have SKI");
+ }
+
+ #[test]
+ fn test_is_known_root() {
+ assert!(is_known_root(AWS_NITRO_ROOT_PEM));
+ assert!(is_known_root(GCP_EKAK_ROOT_PEM));
+ assert!(!is_known_root("some random pem"));
+ }
+
+ #[test]
+ fn test_get_all_roots() {
+ let roots = get_all_roots();
+ assert_eq!(roots.len(), 2);
+ assert!(roots.contains(&AWS_NITRO_ROOT_PEM));
+ assert!(roots.contains(&GCP_EKAK_ROOT_PEM));
+ }
+
+ #[test]
+ fn test_get_ski() {
+ let ski = get_ski(AWS_NITRO_ROOT_PEM);
+ assert!(ski.is_some());
+ assert_eq!(ski.unwrap().len(), 20);
+ }
+}
diff --git a/crates/vaportpm-verify/Cargo.toml b/crates/vaportpm-verify/Cargo.toml
index fa34697..edd0481 100644
--- a/crates/vaportpm-verify/Cargo.toml
+++ b/crates/vaportpm-verify/Cargo.toml
@@ -13,15 +13,14 @@ der = { workspace = true }
spki = { workspace = true }
x509-cert = { workspace = true }
-# Certificate chain validation (rustls ecosystem)
-webpki = { workspace = true }
+# Time types
pki-types = { workspace = true }
-rustls-rustcrypto = { workspace = true }
-# Signature verification (still needed for TPM AK signature)
+# Signature verification
p256 = { workspace = true }
p384 = { workspace = true }
ecdsa = { workspace = true }
+rsa = { workspace = true }
# COSE/CBOR for Nitro
coset = { workspace = true }
@@ -39,10 +38,3 @@ serde_json = { workspace = true }
name = "vaportpm_verify"
path = "src/lib.rs"
-[[bin]]
-name = "selftest-verify"
-path = "src/bin/selftest-verify.rs"
-
-[[bin]]
-name = "test-verify"
-path = "src/bin/test-verify.rs"
diff --git a/crates/vaportpm-verify/README.md b/crates/vaportpm-verify/README.md
index d685112..856c602 100644
--- a/crates/vaportpm-verify/README.md
+++ b/crates/vaportpm-verify/README.md
@@ -1,10 +1,10 @@
# vaportpm-verify
-Verification library for TPM and Nitro attestations produced by `vaportpm-attest`.
+Verification library for attestations produced by `vaportpm-attest`.
## Overview
-This crate verifies attestation documents without requiring TPM access. It can run anywhere - on a server, in a browser (via WASM), or in any environment that needs to verify attestations.
+This crate verifies attestation documents without requiring TPM or internet access. It can run anywhere - on a server, in a browser (via WASM), or in any environment that needs to verify attestations.
## Features
@@ -26,7 +26,7 @@ fn verify(json: &str) -> Result<(), Box> {
// Verify the entire attestation with current time
let result = verify_attestation_output(&output, UnixTime::now())?;
- println!("Verified via: {:?}", result.method);
+ println!("Verified via: {:?}", result.provider);
println!("Nonce: {}", result.nonce);
println!("Root CA hash: {}", result.root_pubkey_hash);
@@ -43,25 +43,35 @@ fn verify(json: &str) -> Result<(), Box> {
| COSE Signature | ECDSA P-384 signature over Nitro document |
| Certificate Chain | Validates chain, returns root pubkey hash |
| Public Key Binding | AK public key matches signed `public_key` field |
-| TPM Signature | AK's ECDSA P-256 signature over TPM2B_ATTEST |
-| PCR Policy | Certified name matches computed policy from SHA-384 PCRs |
-| Nonce Binding | TPM nonce matches Nitro nonce (freshness) |
+| TPM Quote Signature | AK's ECDSA P-256 signature over TPM2_Quote |
+| Nonce Binding | TPM Quote nonce matches Nitro nonce (freshness) |
| PCR Values | Claimed PCRs match signed values in Nitro document |
+### GCP Path (Google Cloud)
+
+| Check | Description |
+|-------|-------------|
+| AK Certificate Chain | Validates chain to Google CA, returns root pubkey hash |
+| TPM Quote Signature | AK's ECDSA P-256 signature over TPM2_Quote |
+| Nonce Verification | Quote extraData matches expected nonce |
+| PCR Digest | Quote's pcrDigest matches hash of claimed PCRs |
+
### Verification Result
```rust
pub struct VerificationResult {
/// The verified nonce (hex-encoded)
pub nonce: String,
+ /// Cloud provider (AWS, GCP)
+ pub provider: CloudProvider,
+ /// PCR values from the attestation
+ pub pcrs: BTreeMap,
/// SHA-256 hash of the root CA's public key
pub root_pubkey_hash: String,
- /// How verification was performed
- pub method: VerificationMethod,
}
```
-The `root_pubkey_hash` identifies the trust anchor. For AWS Nitro, this is the hash of the Nitro Root CA's public key.
+The `root_pubkey_hash` identifies the trust anchor. For AWS Nitro, this is the hash of the Nitro Root CA's public key. For GCP, this is the hash of Google's AK Root CA.
## API
@@ -85,13 +95,9 @@ Returns `VerificationResult` containing:
## Security Considerations
-### Trust Model
-
-This library is **trust-agnostic**. It verifies cryptographic signatures and returns the `root_pubkey_hash` - the SHA-256 hash of the root CA's public key. **You** decide whether to trust that root.
-
-For AWS Nitro, you would check that `root_pubkey_hash` matches the known AWS Nitro Root CA public key hash.
+The library embeds the hashes of known Amazon & Google certificate authorities and verification will fail if it encounters an unknown root of trust, the full certificate chain must be provided in the attestation.
-### What You Must Verify Separately
+However, beyond that it's up to the application to decide on the following:
1. **PCR Semantics** - This library verifies PCR *values*, not their *meaning*. You need to know what software produces which measurements.
@@ -99,27 +105,3 @@ For AWS Nitro, you would check that `root_pubkey_hash` matches the known AWS Nit
3. **Application Logic** - The attestation proves system state at a point in time. Your application must decide if that state is acceptable.
-### Verification Flow
-
-```mermaid
-flowchart TD
- A[AttestationOutput JSON] --> B{Nitro Present?}
- B -->|Yes| C[Verify Nitro Document]
- B -->|No| X[Error: Unsupported]
-
- C --> D[Verify COSE Signature]
- D --> E[Validate Cert Chain]
- E --> F[Extract signed public_key]
-
- F --> G[Verify TPM Signature]
- G --> H[Parse TPM2B_ATTEST]
- H --> I[Compute PCR Policy]
- I --> J{Name Match?}
-
- J -->|Yes| K[Verify Nonce Binding]
- J -->|No| Y[Error: PCR Mismatch]
-
- K --> L{Nonces Match?}
- L -->|Yes| M[Success: Return VerificationResult]
- L -->|No| Z[Error: Freshness Failed]
-```
\ No newline at end of file
diff --git a/crates/vaportpm-verify/src/bin/selftest-verify.rs b/crates/vaportpm-verify/src/bin/selftest-verify.rs
deleted file mode 100644
index 22d6acc..0000000
--- a/crates/vaportpm-verify/src/bin/selftest-verify.rs
+++ /dev/null
@@ -1,306 +0,0 @@
-// SPDX-License-Identifier: MIT OR Apache-2.0
-
-//! Verification selftest binary
-//!
-//! Tests verification functionality against a real TPM.
-//! Uses vaportpm_attest for TPM operations and vaportpm_verify for verification.
-
-#![allow(clippy::needless_borrows_for_generic_args)]
-
-use sha2::{Digest, Sha256};
-use std::collections::BTreeMap;
-
-use vaportpm_attest::{
- der_to_pem, EkOps, NvOps, PcrOps, Tpm, TpmAlg, NV_INDEX_ECC_P256_EK_CERT, TPM_RH_ENDORSEMENT,
- TPM_RH_OWNER,
-};
-use vaportpm_verify::{
- calculate_pcr_policy, compute_ecc_p256_name, extract_public_key, hash_public_key,
- parse_cert_chain_pem, verify_ecdsa_p256,
-};
-
-fn main() -> Result<(), Box> {
- println!("Verification Selftest (Real TPM)");
- println!("=================================\n");
-
- // Open TPM device
- println!("Opening TPM device...");
- let mut tpm = Tpm::open()?;
- println!("β TPM device opened successfully\n");
-
- // Test 1: Software verification of TPM signature
- println!("Test 1: TPM Signature with Software Verification");
- println!("-------------------------------------------------");
-
- // Create a signing key
- let key_result = tpm.create_primary_ecc_key(TPM_RH_OWNER)?;
- println!("Created signing key: 0x{:08X}", key_result.handle);
-
- // Sign some data with the TPM
- let test_data = b"Test data for signature verification";
- let digest = Sha256::digest(test_data);
- println!("Test data: {:?}", std::str::from_utf8(test_data).unwrap());
- println!("SHA256 digest: {}", hex::encode(&digest));
-
- let signature = tpm.sign(key_result.handle, &digest)?;
- println!("TPM signature: {} bytes (DER)", signature.len());
-
- // Verify using vaportpm_attest-verify (p256 crate)
- let mut pubkey = vec![0x04];
- pubkey.extend(&key_result.public_key.x);
- pubkey.extend(&key_result.public_key.y);
-
- match verify_ecdsa_p256(test_data, &signature, &pubkey) {
- Ok(()) => {
- println!("β Signature verified successfully using software (p256 crate)");
- }
- Err(e) => {
- println!("β Signature verification FAILED: {}", e);
- }
- }
-
- // Test with wrong data (should fail)
- let wrong_data = b"Wrong data";
- match verify_ecdsa_p256(wrong_data, &signature, &pubkey) {
- Ok(()) => {
- println!("β FAIL: Verification should have failed with wrong data");
- }
- Err(_) => {
- println!("β Correctly rejected signature for wrong data");
- }
- }
-
- tpm.flush_context(key_result.handle)?;
- println!();
-
- // Test 2: Standard EK and Certificate Comparison
- println!("Test 2: Standard EK vs Certificate Comparison");
- println!("----------------------------------------------");
-
- match tpm.create_standard_ek() {
- Ok(standard_ek) => {
- println!("β Standard EK created using TCG template");
- println!(" EK X: {}", hex::encode(&standard_ek.public_key.x));
- println!(" EK Y: {}", hex::encode(&standard_ek.public_key.y));
-
- // Try to read EK certificate from NV RAM
- match tpm.nv_read(NV_INDEX_ECC_P256_EK_CERT) {
- Ok(cert_der) => {
- println!(
- "\n Found EK certificate in NV RAM ({} bytes)",
- cert_der.len()
- );
-
- if cert_der.starts_with(&[0x30, 0x82]) {
- // Convert DER to PEM for parsing
- let pem = der_to_pem(&cert_der, "CERTIFICATE");
-
- match parse_cert_chain_pem(&pem) {
- Ok(chain) => {
- match extract_public_key(&chain[0]) {
- Ok(cert_pubkey) => {
- println!(
- " Certificate pubkey: {} bytes",
- cert_pubkey.len()
- );
-
- // Build EK pubkey in same format
- let mut ek_pubkey = vec![0x04];
- ek_pubkey.extend(&standard_ek.public_key.x);
- ek_pubkey.extend(&standard_ek.public_key.y);
-
- if ek_pubkey == cert_pubkey {
- println!("β Standard EK matches certificate!");
- println!(" Deterministic key derivation verified.");
- } else {
- println!("β Standard EK does NOT match certificate");
- println!(
- " EK from TPM: {}",
- hex::encode(&ek_pubkey)
- );
- println!(
- " EK from cert: {}",
- hex::encode(&cert_pubkey)
- );
- println!(" This may indicate:");
- println!(" - Certificate was issued with a different template");
- println!(" - TPM was re-provisioned after certificate issuance");
- }
- }
- Err(e) => {
- println!(
- " Could not extract public key from certificate: {}",
- e
- );
- }
- }
- }
- Err(e) => {
- println!(" Could not parse certificate: {}", e);
- }
- }
- } else {
- println!(" Certificate is not in standard DER format");
- }
- }
- Err(e) => {
- println!(" No EK certificate in NV RAM: {}", e);
- println!(" (Cannot compare - certificate not available)");
- }
- }
-
- tpm.flush_context(standard_ek.handle)?;
- }
- Err(e) => {
- println!("β Could not create standard EK: {}", e);
- println!(" (Endorsement hierarchy may require authentication)");
- }
- }
- println!();
-
- // Test 3: PCR Policy Calculation and Verification
- println!("Test 3: PCR Policy Calculation");
- println!("-------------------------------");
-
- // Read actual PCR values from TPM
- let all_pcrs = tpm.read_all_allocated_pcrs()?;
- let sha256_pcrs: Vec<(u8, Vec)> = all_pcrs
- .iter()
- .filter(|(_, alg, _)| *alg == TpmAlg::Sha256)
- .map(|(idx, _, val)| (*idx, val.clone()))
- .collect();
-
- println!("Read {} SHA-256 PCRs from TPM", sha256_pcrs.len());
-
- // Convert to BTreeMap for calculate_pcr_policy
- let pcr_map: BTreeMap = sha256_pcrs
- .iter()
- .map(|(idx, val)| (*idx, hex::encode(val)))
- .collect();
-
- // Calculate policy using vaportpm_attest-verify
- let policy_hex = calculate_pcr_policy(&pcr_map, TpmAlg::Sha256)?;
- println!("Calculated PCR policy: {}...", &policy_hex[..32]);
-
- // Calculate policy using vaportpm_attest (should match)
- let policy_from_tpm = Tpm::calculate_pcr_policy_digest(&sha256_pcrs, TpmAlg::Sha256)?;
- println!(
- "Policy from vaportpm_attest: {}...",
- hex::encode(&policy_from_tpm[..16])
- );
-
- if policy_hex == hex::encode(&policy_from_tpm) {
- println!("β Policy calculations match between crates");
- } else {
- println!("β Policy calculations differ!");
- }
- println!();
-
- // Test 4: ReadPublic and Name Computation
- println!("Test 4: ReadPublic and Name Verification");
- println!("-----------------------------------------");
-
- // Create a key and verify name computation
- let test_key = tpm.create_primary_ecc_key(TPM_RH_OWNER)?;
- let read_result = tpm.read_public(test_key.handle)?;
-
- println!("TPM ReadPublic returned:");
- println!(" Public area: {} bytes", read_result.public_area.len());
- println!(" Name from TPM: {}", hex::encode(&read_result.name));
-
- // Compute name using vaportpm_attest-verify
- let computed_name = compute_ecc_p256_name(
- &test_key.public_key.x,
- &test_key.public_key.y,
- &[], // empty policy for basic signing key
- );
- println!(" Computed name: {}", hex::encode(&computed_name));
-
- if read_result.name == computed_name {
- println!("β TPM's name matches computed name");
- } else {
- println!("β Name mismatch - key may have non-empty authPolicy");
- }
-
- tpm.flush_context(test_key.handle)?;
- println!();
-
- // Test 5: Public Key Hashing
- println!("Test 5: Public Key Hashing");
- println!("--------------------------");
-
- match tpm.create_primary_ecc_key(TPM_RH_ENDORSEMENT) {
- Ok(ek) => {
- let mut ek_pubkey = vec![0x04];
- ek_pubkey.extend(&ek.public_key.x);
- ek_pubkey.extend(&ek.public_key.y);
-
- let hash = hash_public_key(&ek_pubkey);
- println!("EK public key hash: {}", hash);
- println!(" (This would be the trust anchor identifier)");
-
- tpm.flush_context(ek.handle)?;
- println!("β Public key hash computed");
- }
- Err(e) => {
- println!("β Could not access EK: {}", e);
- }
- }
- println!();
-
- // Test 6: Certify with Signature Verification
- println!("Test 6: TPM2_Certify with Software Verification");
- println!("------------------------------------------------");
-
- // Create an AK (signing key) and a key to certify
- let ak = tpm.create_primary_ecc_key(TPM_RH_OWNER)?;
- println!("Created AK: 0x{:08X}", ak.handle);
-
- // Create PCR-sealed key to certify
- let pcr_values: Vec<(u8, Vec)> = sha256_pcrs.clone();
- let auth_policy = Tpm::calculate_pcr_policy_digest(&pcr_values, TpmAlg::Sha256)?;
- let sealed_key = tpm.create_primary_ecc_key_with_policy(TPM_RH_OWNER, &auth_policy)?;
- println!("Created PCR-sealed key: 0x{:08X}", sealed_key.handle);
-
- // Certify the sealed key with AK
- let qualifying_data = b"test-certification-nonce";
- let cert_result = tpm.certify(sealed_key.handle, ak.handle, qualifying_data)?;
- println!("TPM2_Certify returned:");
- println!(
- " Attestation data: {} bytes",
- cert_result.attest_data.len()
- );
- println!(" Signature: {} bytes", cert_result.signature.len());
-
- // Verify AK signature over attestation data using vaportpm_attest-verify
- let mut ak_pubkey = vec![0x04];
- ak_pubkey.extend(&ak.public_key.x);
- ak_pubkey.extend(&ak.public_key.y);
-
- match verify_ecdsa_p256(&cert_result.attest_data, &cert_result.signature, &ak_pubkey) {
- Ok(()) => {
- println!("β Certification signature verified successfully");
- }
- Err(e) => {
- println!("β Certification signature verification FAILED: {}", e);
- }
- }
-
- // Verify the certified name matches our computed name
- let expected_name = compute_ecc_p256_name(
- &sealed_key.public_key.x,
- &sealed_key.public_key.y,
- &auth_policy,
- );
- println!("Expected certified name: {}", hex::encode(&expected_name));
-
- tpm.flush_context(ak.handle)?;
- tpm.flush_context(sealed_key.handle)?;
- println!();
-
- println!("===========================");
- println!("All verification tests completed!");
- println!("===========================");
-
- Ok(())
-}
diff --git a/crates/vaportpm-verify/src/bin/test-verify.rs b/crates/vaportpm-verify/src/bin/test-verify.rs
deleted file mode 100644
index e48a883..0000000
--- a/crates/vaportpm-verify/src/bin/test-verify.rs
+++ /dev/null
@@ -1,183 +0,0 @@
-// SPDX-License-Identifier: MIT OR Apache-2.0
-
-//! Test binary for verifying TPM and Nitro attestations
-
-use vaportpm_attest::attest;
-use vaportpm_verify::{
- extract_public_key, hash_public_key, parse_cert_chain_pem, verify_ecdsa_p256,
- verify_nitro_attestation, verify_tpm_attestation, AttestationOutput, UnixTime,
-};
-
-fn main() -> Result<(), Box> {
- println!("Generating attestation...");
- let nonce = b"test-nonce-12345";
- let attestation_json = attest(nonce)?;
-
- println!("Attestation generated successfully!");
- println!("JSON length: {} bytes", attestation_json.len());
-
- // Parse the output
- let output: AttestationOutput = serde_json::from_str(&attestation_json)?;
-
- // Check what we got
- println!("\nEK Certificates present:");
- println!(" RSA-2048: {}", output.ek_certificates.rsa_2048.is_some());
- println!(" ECC P-256: {}", output.ek_certificates.ecc_p256.is_some());
- println!(" ECC P-384: {}", output.ek_certificates.ecc_p384.is_some());
-
- println!(
- "\nTPM attestations: {:?}",
- output.attestation.tpm.keys().collect::>()
- );
- println!("Nitro attestation: {}", output.attestation.nitro.is_some());
-
- // Verify TPM attestation
- for (key_type, attestation) in &output.attestation.tpm {
- println!("\nVerifying TPM attestation for {}...", key_type);
-
- let cert_pem = match key_type.as_str() {
- "rsa_2048" => output.ek_certificates.rsa_2048.as_ref(),
- "ecc_p256" => output.ek_certificates.ecc_p256.as_ref(),
- "ecc_p384" => output.ek_certificates.ecc_p384.as_ref(),
- _ => None,
- };
-
- let ek_pk = output.ek_public_keys.get(key_type);
- let ak_pk = output.signing_key_public_keys.get(key_type);
-
- // Debug info
- println!(
- " Attest data (nonce): {} bytes",
- attestation.attest_data.len() / 2
- );
- println!(
- " Signature length: {} bytes",
- attestation.signature.len() / 2
- );
-
- if let Some(ek) = ek_pk {
- println!(
- " EK pubkey X: {}...",
- &ek.x[..std::cmp::min(16, ek.x.len())]
- );
- println!(
- " EK pubkey Y: {}...",
- &ek.y[..std::cmp::min(16, ek.y.len())]
- );
- }
-
- if let Some(ak) = ak_pk {
- println!(
- " AK pubkey X: {}...",
- &ak.x[..std::cmp::min(16, ak.x.len())]
- );
- println!(
- " AK pubkey Y: {}...",
- &ak.y[..std::cmp::min(16, ak.y.len())]
- );
- }
-
- // Compare EK pubkey with certificate
- if let Some(cert) = cert_pem {
- if let Ok(chain) = parse_cert_chain_pem(cert) {
- if let Ok(cert_pubkey) = extract_public_key(&chain[0]) {
- println!(" Cert pubkey length: {} bytes", cert_pubkey.len());
- println!(
- " Cert pubkey: {}...",
- hex::encode(&cert_pubkey[..std::cmp::min(20, cert_pubkey.len())])
- );
-
- // Check if EK from attestation matches certificate
- if let Some(ek) = ek_pk {
- let ek_x = hex::decode(&ek.x)?;
- let ek_y = hex::decode(&ek.y)?;
- let mut ek_pubkey = vec![0x04];
- ek_pubkey.extend(&ek_x);
- ek_pubkey.extend(&ek_y);
-
- if ek_pubkey == cert_pubkey {
- println!(" EK pubkey MATCHES certificate!");
- } else {
- println!(" EK pubkey DOES NOT MATCH certificate");
- println!(" EK from output: {}", hex::encode(&ek_pubkey));
- println!(" EK from cert: {}", hex::encode(&cert_pubkey));
- }
- }
- }
- }
- }
-
- // Try verification
- if let (Some(cert), Some(ek), Some(ak)) = (cert_pem, ek_pk, ak_pk) {
- match verify_tpm_attestation(
- &attestation.attest_data,
- &attestation.signature,
- &ak.x,
- &ak.y,
- &ek.x,
- &ek.y,
- cert,
- ) {
- Ok(result) => {
- println!(" Verification SUCCESS!");
- println!(" Root pubkey hash: {}", result.root_pubkey_hash);
-
- // Decode and show the nonce
- let nonce_bytes = hex::decode(&result.nonce)?;
- if let Ok(nonce_str) = std::str::from_utf8(&nonce_bytes) {
- println!(" Nonce: {}", nonce_str);
- } else {
- println!(" Nonce: {} (binary)", result.nonce);
- }
- }
- Err(e) => {
- println!(" Verification FAILED: {}", e);
-
- // Try manual verification with AK
- println!("\n Trying manual AK signature verification...");
- let ak_x = hex::decode(&ak.x)?;
- let ak_y = hex::decode(&ak.y)?;
- let mut ak_pubkey = vec![0x04];
- ak_pubkey.extend(&ak_x);
- ak_pubkey.extend(&ak_y);
-
- let nonce_data = hex::decode(&attestation.attest_data)?;
- let signature = hex::decode(&attestation.signature)?;
-
- match verify_ecdsa_p256(&nonce_data, &signature, &ak_pubkey) {
- Ok(()) => {
- println!(" AK signature verification SUCCESS!");
- let ak_hash = hash_public_key(&ak_pubkey);
- println!(" AK pubkey hash: {}", ak_hash);
- }
- Err(e2) => {
- println!(" AK signature verification also failed: {}", e2);
- }
- }
- }
- }
- } else {
- println!(
- " Missing certificate, EK, or AK public key for {}",
- key_type
- );
- }
- }
-
- // Try Nitro if present
- if let Some(ref nitro) = output.attestation.nitro {
- println!("\nVerifying Nitro attestation...");
- match verify_nitro_attestation(&nitro.document, Some(nonce), None, UnixTime::now()) {
- Ok(result) => {
- println!(" Verification SUCCESS!");
- println!(" Root pubkey hash: {}", result.root_pubkey_hash);
- println!(" Module ID: {}", result.document.module_id);
- }
- Err(e) => {
- println!(" Verification FAILED: {}", e);
- }
- }
- }
-
- Ok(())
-}
diff --git a/crates/vaportpm-verify/src/gcp.rs b/crates/vaportpm-verify/src/gcp.rs
new file mode 100644
index 0000000..3d21a23
--- /dev/null
+++ b/crates/vaportpm-verify/src/gcp.rs
@@ -0,0 +1,143 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+
+//! GCP Shielded VM attestation verification
+
+use std::collections::BTreeMap;
+
+use pki_types::UnixTime;
+use sha2::{Digest, Sha256};
+
+use crate::error::VerifyError;
+use crate::tpm::{parse_quote_attest, verify_ecdsa_p256, TpmQuoteInfo};
+use crate::x509::{extract_public_key, parse_cert_chain_pem, validate_tpm_cert_chain};
+use crate::{roots, VerificationResult};
+
+use vaportpm_attest::a9n::{AttestationOutput, GcpAttestationData};
+
+/// Verify GCP Shielded VM attestation
+///
+/// This verification path:
+/// 1. Parses TPM2_Quote attestation to extract PCR digest and nonce
+/// 2. Validates AK certificate chain to Google's root CA
+/// 3. Verifies Quote signature with AK public key from certificate
+/// 4. Verifies PCR digest matches claimed PCR values
+pub fn verify_gcp_attestation(
+ output: &AttestationOutput,
+ gcp: &GcpAttestationData,
+ time: UnixTime,
+) -> Result {
+ // Get TPM attestation (contains Quote data and signature)
+ let (_, tpm_attestation) = output
+ .attestation
+ .tpm
+ .iter()
+ .next()
+ .ok_or_else(|| VerifyError::NoValidAttestation("Missing TPM attestation".into()))?;
+
+ // Parse TPM2_Quote attestation (type = QUOTE, not CERTIFY)
+ let quote_data = hex::decode(&tpm_attestation.attest_data)?;
+ let quote_info = parse_quote_attest("e_data)?;
+
+ // Verify top-level nonce matches nonce in Quote (prevents tampering)
+ let nonce_from_field = hex::decode(&output.nonce)?;
+ if nonce_from_field != quote_info.nonce {
+ return Err(VerifyError::InvalidAttest(format!(
+ "Nonce field does not match nonce in Quote. \
+ Field: {}, Quote: {}",
+ output.nonce,
+ hex::encode("e_info.nonce)
+ )));
+ }
+
+ // Validate AK certificate chain to GCP root
+ let chain_result = validate_tpm_cert_chain(&parse_cert_chain_pem(&gcp.ak_cert_chain)?, time)?;
+
+ // Extract AK public key from leaf certificate
+ let certs = parse_cert_chain_pem(&gcp.ak_cert_chain)?;
+ let ak_pubkey = extract_public_key(&certs[0])?;
+
+ // Verify Quote signature with AK public key from certificate
+ let signature = hex::decode(&tpm_attestation.signature)?;
+ verify_ecdsa_p256("e_data, &signature, &ak_pubkey)?;
+
+ // Verify PCR digest matches claimed PCR values
+ // The Quote contains a digest of the selected PCRs - this MUST be verified
+ let pcrs = output.pcrs.get("sha256").ok_or_else(|| {
+ VerifyError::InvalidAttest("Missing SHA-256 PCRs - required for GCP attestation".into())
+ })?;
+ if pcrs.is_empty() {
+ return Err(VerifyError::InvalidAttest(
+ "SHA-256 PCRs map is empty - at least one PCR required".into(),
+ ));
+ }
+ verify_pcr_digest_matches("e_info, pcrs)?;
+
+ // Verify root is a known GCP root - fail if not recognized
+ let provider = roots::provider_from_hash(&chain_result.root_pubkey_hash).ok_or_else(|| {
+ VerifyError::ChainValidation(format!(
+ "Unknown root CA: {}. Only known cloud provider roots are trusted.",
+ chain_result.root_pubkey_hash
+ ))
+ })?;
+
+ Ok(VerificationResult {
+ nonce: hex::encode("e_info.nonce),
+ provider,
+ pcrs: pcrs.clone(),
+ root_pubkey_hash: chain_result.root_pubkey_hash,
+ })
+}
+
+/// Verify that the PCR digest in a Quote matches the claimed PCR values
+fn verify_pcr_digest_matches(
+ quote_info: &TpmQuoteInfo,
+ pcrs: &BTreeMap,
+) -> Result<(), VerifyError> {
+ // The PCR digest is SHA-256(concatenation of selected PCR values in order)
+ // The selection order is determined by the pcr_select field
+
+ // Build the expected digest by concatenating PCR values in selection order
+ let mut hasher = Sha256::new();
+
+ for (alg, bitmap) in "e_info.pcr_select {
+ // Only handle SHA-256 PCRs for now
+ if *alg != 0x000B {
+ // TPM_ALG_SHA256
+ continue;
+ }
+
+ // Iterate through bitmap to find selected PCRs
+ for (byte_idx, byte_val) in bitmap.iter().enumerate() {
+ for bit_idx in 0..8 {
+ if byte_val & (1 << bit_idx) != 0 {
+ let pcr_idx = (byte_idx * 8 + bit_idx) as u8;
+ if let Some(pcr_value_hex) = pcrs.get(&pcr_idx) {
+ let pcr_value = hex::decode(pcr_value_hex).map_err(|e| {
+ VerifyError::InvalidAttest(format!(
+ "Invalid PCR {} hex value: {}",
+ pcr_idx, e
+ ))
+ })?;
+ hasher.update(&pcr_value);
+ } else {
+ return Err(VerifyError::InvalidAttest(format!(
+ "PCR {} selected in Quote but not present in attestation",
+ pcr_idx
+ )));
+ }
+ }
+ }
+ }
+ }
+
+ let computed_digest = hasher.finalize();
+ if computed_digest.as_ref() != quote_info.pcr_digest {
+ return Err(VerifyError::InvalidAttest(format!(
+ "PCR digest mismatch. Quote digest: {}, Computed from PCRs: {}",
+ hex::encode("e_info.pcr_digest),
+ hex::encode(computed_digest)
+ )));
+ }
+
+ Ok(())
+}
diff --git a/crates/vaportpm-verify/src/lib.rs b/crates/vaportpm-verify/src/lib.rs
index 65c80ba..c5dc8b8 100644
--- a/crates/vaportpm-verify/src/lib.rs
+++ b/crates/vaportpm-verify/src/lib.rs
@@ -6,80 +6,159 @@
//! - TPM attestation verification with EK public key matching
//! - X.509 certificate chain validation
//! - AWS Nitro COSE Sign1 document verification
+//! - GCP Shielded VM AK certificate verification
//! - Root certificate public key hash extraction (SHA-256, hex-encoded)
mod error;
+mod gcp;
mod nitro;
mod tpm;
mod x509;
+use std::collections::BTreeMap;
+
use serde::Serialize;
// Re-export error type
pub use error::VerifyError;
-// Re-export TPM types and functions
-pub use tpm::{
- calculate_pcr_policy, parse_tpm2b_attest, verify_ecdsa_p256, verify_pcr_policy,
- verify_tpm_attestation, verify_tpm_signature_only, TpmAttestInfo,
-};
+// Re-export TPM types and functions (only those used by verification paths)
+pub use tpm::{parse_quote_attest, verify_ecdsa_p256, TpmQuoteInfo};
-// Re-export from vaportpm_attest (single source of truth for TPM crypto)
-pub use vaportpm_attest::{compute_ecc_p256_name, TpmAlg};
+// Re-export from vaportpm_attest
+pub use vaportpm_attest::TpmAlg;
// Re-export Nitro types and functions
pub use nitro::{verify_nitro_attestation, NitroDocument, NitroVerifyResult};
// Re-export X.509 utility functions
pub use x509::{
- extract_public_key, hash_public_key, parse_and_validate_cert_chain,
- parse_and_validate_tpm_cert_chain, parse_cert_chain_pem, validate_cert_chain,
+ extract_public_key, hash_public_key, parse_and_validate_tpm_cert_chain, parse_cert_chain_pem,
validate_tpm_cert_chain, ChainValidationResult, MAX_CHAIN_DEPTH,
};
// Re-export time type for testing
pub use pki_types::UnixTime;
+/// Cloud provider identification
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
+pub enum CloudProvider {
+ /// Amazon Web Services (Nitro TPM)
+ Aws,
+ /// Google Cloud Platform (Shielded VM)
+ Gcp,
+}
+
+/// Known root CA certificates and public key hashes for cloud providers
+///
+/// Certificates are embedded from vaportpm_attest. Hashes are derived from
+/// the certificate data at runtime, not hardcoded separately.
+pub mod roots {
+ use super::CloudProvider;
+ use crate::x509::{extract_public_key, hash_public_key, parse_cert_chain_pem};
+ use std::sync::OnceLock;
+
+ // Re-export embedded root certificate PEMs from vaportpm_attest
+ // Note: Intermediates are project-specific and must be fetched by the attestor
+ pub use vaportpm_attest::roots::{AWS_NITRO_ROOT_PEM, GCP_EKAK_ROOT_PEM};
+
+ // Re-export SKI/AKI lookup
+ pub use vaportpm_attest::roots::find_issuer_by_aki;
+
+ /// Cached root public key hashes (computed on first access)
+ static ROOT_HASHES: OnceLock = OnceLock::new();
+
+ struct RootHashes {
+ aws_nitro: String,
+ gcp_ekak_amd: String,
+ }
+
+ fn compute_root_hashes() -> RootHashes {
+ let aws_hash = compute_pubkey_hash(AWS_NITRO_ROOT_PEM).unwrap_or_default();
+ let gcp_amd_hash = compute_pubkey_hash(GCP_EKAK_ROOT_PEM).unwrap_or_default();
+
+ RootHashes {
+ aws_nitro: aws_hash,
+ gcp_ekak_amd: gcp_amd_hash,
+ }
+ }
+
+ fn compute_pubkey_hash(pem: &str) -> Option {
+ let certs = parse_cert_chain_pem(pem).ok()?;
+ let cert = certs.first()?;
+ let pubkey = extract_public_key(cert).ok()?;
+ Some(hash_public_key(&pubkey))
+ }
+
+ fn get_hashes() -> &'static RootHashes {
+ ROOT_HASHES.get_or_init(compute_root_hashes)
+ }
+
+ /// AWS Nitro Enclave Root CA public key hash (SHA-256)
+ ///
+ /// Derived from the embedded AWS Nitro root certificate.
+ pub fn aws_nitro_root_hash() -> &'static str {
+ &get_hashes().aws_nitro
+ }
+
+ /// GCP Shielded VM EK/AK Root CA public key hash (SHA-256) - AMD/SEV
+ ///
+ /// Derived from the embedded GCP EK/AK root certificate for AMD instances.
+ pub fn gcp_ekak_root_amd_hash() -> &'static str {
+ &get_hashes().gcp_ekak_amd
+ }
+
+ /// Look up cloud provider from root public key hash
+ ///
+ /// Returns `Some(CloudProvider)` if the hash matches a known root CA,
+ /// or `None` if the root is not recognized.
+ pub fn provider_from_hash(hash: &str) -> Option {
+ let hashes = get_hashes();
+ if hash == hashes.aws_nitro {
+ Some(CloudProvider::Aws)
+ } else if hash == hashes.gcp_ekak_amd {
+ Some(CloudProvider::Gcp)
+ } else {
+ None
+ }
+ }
+
+ /// Check if a public key hash matches a known trust anchor
+ pub fn is_known_root_hash(hash: &str) -> bool {
+ provider_from_hash(hash).is_some()
+ }
+}
+
// Re-export types from vaportpm_attest for convenience
pub use vaportpm_attest::a9n::{
- AttestationContainer, AttestationData, AttestationOutput, EccPublicKeyCoords, EkCertificates,
- NitroAttestationData,
+ AttestationContainer, AttestationData, AttestationOutput, EccPublicKeyCoords,
+ GcpAttestationData, NitroAttestationData,
};
-/// How the attestation was verified
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
-pub enum VerificationMethod {
- /// Verified via AWS Nitro attestation
- Nitro,
- /// Verified via cloud provider AK certificate chain (GCP/Azure)
- /// Note: Not yet implemented - reserved for future use
- #[allow(dead_code)]
- CloudAkChain,
-}
-
/// Result of successful attestation verification
#[derive(Debug, Serialize)]
pub struct VerificationResult {
/// The nonce that was verified (hex-encoded)
pub nonce: String,
+ /// Cloud provider that issued the attestation
+ pub provider: CloudProvider,
+ /// PCR values from the attestation (index -> hex-encoded digest)
+ pub pcrs: BTreeMap,
/// SHA-256 hash of the root CA's public key
pub root_pubkey_hash: String,
- /// How the attestation was verified
- pub method: VerificationMethod,
}
/// Verify an entire AttestationOutput
///
-/// Currently supports one verification path:
+/// Supports two verification paths, both using TPM2_Quote:
///
-/// 1. **Nitro path** (AWS): If Nitro attestation is present, verify it and trust
-/// the TPM signing key via the Nitro document's `public_key` binding. The
-/// TPM2B_ATTEST structure is verified for PCR policy binding. EK certificates
-/// are not required in this path.
+/// 1. **GCP path**: If GCP attestation is present, verify the AK certificate
+/// chain to Google's root CA, then verify the TPM2_Quote signature. PCR
+/// values are verified against the Quote's PCR digest.
///
-/// Future paths (not yet implemented):
-/// - **GCP Shielded VM**: AK certificate from Google CA (NV index 0x01c10000)
-/// - **Azure Trusted Launch**: AK certificate from Microsoft CA (NV index 0x01C101D0)
+/// 2. **Nitro path** (AWS): If Nitro attestation is present, verify it and trust
+/// the TPM signing key via the Nitro document's `public_key` binding. The
+/// TPM2_Quote is verified for PCR values.
///
/// # Arguments
/// * `output` - The attestation output to verify
@@ -87,7 +166,9 @@ pub struct VerificationResult {
///
/// # Returns
/// A unified `VerificationResult` containing:
-/// - `nonce`: The verified challenge (from TPM2B_ATTEST.extraData)
+/// - `nonce`: The verified challenge (from TPM2_Quote.extraData)
+/// - `provider`: Cloud provider (AWS/GCP) if root CA is recognized
+/// - `pcrs`: PCR values from the attestation
/// - `root_pubkey_hash`: SHA-256 of the trust anchor's public key
/// - `method`: How verification was performed
///
@@ -104,186 +185,160 @@ pub fn verify_attestation_output(
));
}
+ // Try GCP verification path (certificate-based trust)
+ if let Some(ref gcp_data) = output.attestation.gcp {
+ return gcp::verify_gcp_attestation(output, gcp_data, time);
+ }
+
+ // Try Nitro verification path (Nitro NSM document-based trust)
+ if let Some(ref nitro) = output.attestation.nitro {
+ return verify_nitro_quote_attestation(output, nitro, time);
+ }
+
+ // No supported attestation method found
+ Err(VerifyError::NoValidAttestation(
+ "No GCP or Nitro attestation present. \
+ Supported verification paths: AWS Nitro, GCP Shielded VM."
+ .into(),
+ ))
+}
+
+/// Verify Nitro attestation path using TPM2_Quote
+///
+/// This verification path:
+/// 1. Parses TPM2_Quote attestation to extract PCR digest and nonce
+/// 2. Verifies Quote signature with AK public key
+/// 3. Verifies Nitro NSM document binds the AK public key
+/// 4. Verifies PCRs match signed values in Nitro document
+fn verify_nitro_quote_attestation(
+ output: &AttestationOutput,
+ nitro: &NitroAttestationData,
+ time: UnixTime,
+) -> Result {
// Get the first (and typically only) TPM attestation
let (key_type, attestation) = output.attestation.tpm.iter().next().unwrap();
// Get the corresponding signing key (AK) public key
- let ak_pk = output
- .signing_key_public_keys
- .get(key_type)
- .ok_or_else(|| {
- VerifyError::NoValidAttestation(format!("{}: missing AK public key", key_type))
- })?;
+ let ak_pk = output.ak_pubkeys.get(key_type).ok_or_else(|| {
+ VerifyError::NoValidAttestation(format!("{}: missing AK public key", key_type))
+ })?;
// Decode AK public key
let ak_x = hex::decode(&ak_pk.x)?;
let ak_y = hex::decode(&ak_pk.y)?;
- // Determine PCR algorithm based on verification path
- // Nitro uses SHA-384 (signed in Nitro document), non-Nitro uses SHA-256
- let (pcr_alg, pcrs_for_policy) = if output.attestation.nitro.is_some() {
- (
- TpmAlg::Sha384,
- output.pcrs.get("sha384").cloned().unwrap_or_default(),
- )
- } else {
- (
- TpmAlg::Sha256,
- output.pcrs.get("sha256").cloned().unwrap_or_default(),
- )
- };
-
- // Parse TPM2B_ATTEST structure (needed for both paths)
+ // Parse TPM2_Quote attestation
let attest_data = hex::decode(&attestation.attest_data)?;
- let attest_info = parse_tpm2b_attest(&attest_data)?;
+ let quote_info = parse_quote_attest(&attest_data)?;
- // Verify nonce field matches nonce in attest_data (prevents tampering)
- let nonce_from_field = hex::decode(&attestation.nonce)?;
- if nonce_from_field != attest_info.nonce {
+ // Verify top-level nonce matches nonce in Quote (prevents tampering)
+ let nonce_from_field = hex::decode(&output.nonce)?;
+ if nonce_from_field != quote_info.nonce {
return Err(VerifyError::InvalidAttest(format!(
- "Nonce field does not match nonce in attest_data. \
- Field: {}, Attest: {}",
- attestation.nonce,
- hex::encode(&attest_info.nonce)
+ "Nonce field does not match nonce in Quote. \
+ Field: {}, Quote: {}",
+ output.nonce,
+ hex::encode("e_info.nonce)
)));
}
- // Verify AK signature over TPM2B_ATTEST
+ // Verify AK signature over TPM2_Quote
let signature = hex::decode(&attestation.signature)?;
let mut ak_pubkey = vec![0x04];
ak_pubkey.extend(&ak_x);
ak_pubkey.extend(&ak_y);
verify_ecdsa_p256(&attest_data, &signature, &ak_pubkey)?;
- // Compute authPolicy from PCRs and verify certified name (proves PCR binding)
- if !pcrs_for_policy.is_empty() {
- let auth_policy_hex = calculate_pcr_policy(&pcrs_for_policy, pcr_alg)?;
- let auth_policy = hex::decode(&auth_policy_hex)?;
-
- let expected_name = compute_ecc_p256_name(&ak_x, &ak_y, &auth_policy);
- if attest_info.certified_name != expected_name {
- return Err(VerifyError::InvalidAttest(format!(
- "Certified key name does not match expected PCR policy. \
- AK's authPolicy does not match claimed PCR values. \
- Expected name: {}, Got: {}",
- hex::encode(&expected_name),
- hex::encode(&attest_info.certified_name)
- )));
- }
+ // Verify Nitro attestation (COSE signature, cert chain)
+ let nitro_result = verify_nitro_attestation(
+ &nitro.document,
+ None, // Nonce validation happens via TPM binding below
+ None, // Pubkey validation happens below
+ time,
+ )?;
+
+ // Extract signed values from Nitro document
+ let signed_pubkey = nitro_result.document.public_key.as_ref().ok_or_else(|| {
+ VerifyError::NoValidAttestation(
+ "Nitro document missing public_key field - cannot bind TPM signing key".into(),
+ )
+ })?;
+ let signed_nonce = nitro_result.document.nonce.as_ref().ok_or_else(|| {
+ VerifyError::NoValidAttestation(
+ "Nitro document missing nonce field - cannot verify freshness".into(),
+ )
+ })?;
+
+ // Verify the AK public key matches the signed public_key in NSM document
+ let ak_secg = format!("04{}{}", ak_pk.x, ak_pk.y);
+ if ak_secg != *signed_pubkey {
+ return Err(VerifyError::SignatureInvalid(format!(
+ "TPM signing key does not match Nitro public_key binding: {} != {}",
+ ak_secg, signed_pubkey
+ )));
}
- // If Nitro attestation is present, use Nitro path
- if let Some(ref nitro) = output.attestation.nitro {
- // Verify Nitro attestation (COSE signature, cert chain)
- let nitro_result = verify_nitro_attestation(
- &nitro.document,
- None, // Nonce validation happens via TPM binding
- None, // Pubkey validation happens below
- time,
- )?;
-
- // Verify the convenience fields match what's signed in the Nitro document.
- // These fields are duplicated in the JSON for easy access, but we must ensure
- // they match the cryptographically signed values to prevent tampering.
-
- // Verify public_key field matches signed document
- let signed_pubkey = nitro_result.document.public_key.as_ref().ok_or_else(|| {
- VerifyError::NoValidAttestation(
- "Nitro document missing public_key field - cannot bind TPM signing key".into(),
- )
- })?;
- if nitro.public_key != *signed_pubkey {
- return Err(VerifyError::SignatureInvalid(format!(
- "attestation.nitro.public_key does not match signed value in document: {} != {}",
- nitro.public_key, signed_pubkey
- )));
- }
-
- // Verify nonce field matches signed document
- let signed_nonce = nitro_result.document.nonce.as_ref().ok_or_else(|| {
- VerifyError::NoValidAttestation(
- "Nitro document missing nonce field - cannot verify freshness".into(),
- )
- })?;
- if nitro.nonce != *signed_nonce {
- return Err(VerifyError::SignatureInvalid(format!(
- "attestation.nitro.nonce does not match signed value in document: {} != {}",
- nitro.nonce, signed_nonce
- )));
- }
+ // Verify TPM nonce matches Nitro nonce (proves attestations generated together)
+ let tpm_nonce_hex = hex::encode("e_info.nonce);
+ if tpm_nonce_hex != *signed_nonce {
+ return Err(VerifyError::SignatureInvalid(format!(
+ "TPM nonce does not match Nitro nonce - attestations not generated together: {} != {}",
+ tpm_nonce_hex, signed_nonce
+ )));
+ }
- // Verify the AK public key matches the signed public_key
- let ak_secg = format!("04{}{}", ak_pk.x, ak_pk.y);
- if ak_secg != *signed_pubkey {
- return Err(VerifyError::SignatureInvalid(format!(
- "TPM signing key does not match Nitro public_key binding: {} != {}",
- ak_secg, signed_pubkey
- )));
- }
+ // Verify SHA-384 PCRs match signed values in Nitro document
+ // The Nitro document contains nitrotpm_pcrs which are signed by AWS hardware
+ let sha384_pcrs = output.pcrs.get("sha384").ok_or_else(|| {
+ VerifyError::InvalidAttest("Missing SHA-384 PCRs - required for Nitro attestation".into())
+ })?;
- // Verify TPM nonce matches Nitro nonce (proves attestations generated together)
- let tpm_nonce_hex = hex::encode(&attest_info.nonce);
- if tpm_nonce_hex != *signed_nonce {
- return Err(VerifyError::SignatureInvalid(format!(
- "TPM nonce does not match Nitro nonce - attestations not generated together: {} != {}",
- tpm_nonce_hex, signed_nonce
- )));
- }
+ let signed_pcrs = &nitro_result.document.pcrs;
+ if signed_pcrs.is_empty() {
+ return Err(VerifyError::InvalidAttest(
+ "Nitro document contains no signed PCRs".into(),
+ ));
+ }
- // Verify SHA-384 PCRs match signed values in Nitro document
- // The Nitro document contains nitrotpm_pcrs which are signed by AWS hardware
- if let Some(sha384_pcrs) = output.pcrs.get("sha384") {
- let signed_pcrs = &nitro_result.document.pcrs;
-
- // Check all signed PCRs are present and match
- for (idx, signed_value) in signed_pcrs.iter() {
- match sha384_pcrs.get(idx) {
- Some(claimed_value) if claimed_value == signed_value => {
- // Match - good
- }
- Some(claimed_value) => {
- return Err(VerifyError::SignatureInvalid(format!(
- "PCR {} SHA-384 value does not match signed value: {} != {}",
- idx, claimed_value, signed_value
- )));
- }
- None => {
- return Err(VerifyError::SignatureInvalid(format!(
- "PCR {} missing from output.pcrs[sha384] but present in signed Nitro document",
- idx
- )));
- }
- }
+ // All signed PCRs must be present and match
+ for (idx, signed_value) in signed_pcrs.iter() {
+ match sha384_pcrs.get(idx) {
+ Some(claimed_value) if claimed_value == signed_value => {
+ // Match - good
+ }
+ Some(claimed_value) => {
+ return Err(VerifyError::SignatureInvalid(format!(
+ "PCR {} SHA-384 mismatch: claimed {} != signed {}",
+ idx, claimed_value, signed_value
+ )));
+ }
+ None => {
+ return Err(VerifyError::SignatureInvalid(format!(
+ "PCR {} in signed Nitro document but missing from attestation",
+ idx
+ )));
}
}
-
- // Nonce is from TPM2B_ATTEST.extraData
- return Ok(VerificationResult {
- nonce: hex::encode(&attest_info.nonce),
- root_pubkey_hash: nitro_result.root_pubkey_hash,
- method: VerificationMethod::Nitro,
- });
}
- // No Nitro attestation - currently unsupported
- //
- // Future: GCP/Azure AK certificate path will be implemented here.
- // This requires:
- // 1. AK certificate from cloud provider (not just EK certificate)
- // 2. Certificate chain validation to cloud provider root CA
- // 3. AK certificate proves the signing key belongs to the cloud provider's vTPM
- //
- // EK certificates alone are NOT sufficient because:
- // - EK certificate only proves "this is a genuine TPM"
- // - It doesn't prove the AK (signing key) belongs to that TPM
- // - Without AK binding, an attacker could use their own signing key
-
- Err(VerifyError::NoValidAttestation(
- "No Nitro attestation present. \
- GCP/Azure AK certificate verification not yet implemented. \
- Currently only AWS Nitro attestation is supported."
- .into(),
- ))
+ // Verify root is the known AWS Nitro root - fail if not recognized
+ let provider = roots::provider_from_hash(&nitro_result.root_pubkey_hash).ok_or_else(|| {
+ VerifyError::ChainValidation(format!(
+ "Unknown root CA: {}. Only known cloud provider roots are trusted.",
+ nitro_result.root_pubkey_hash
+ ))
+ })?;
+
+ // Collect PCRs from the attestation (use SHA-384 for Nitro)
+ let pcrs = output.pcrs.get("sha384").cloned().unwrap_or_default();
+
+ // Nonce is from TPM2_Quote.extraData
+ Ok(VerificationResult {
+ nonce: hex::encode("e_info.nonce),
+ provider,
+ pcrs,
+ root_pubkey_hash: nitro_result.root_pubkey_hash,
+ })
}
/// Convenience function to verify attestation from JSON string
@@ -301,12 +356,19 @@ pub fn verify_attestation_json(json: &str) -> Result UnixTime {
- UnixTime::since_unix_epoch(std::time::Duration::from_secs(FIXTURE_TIMESTAMP_SECS))
+ fn nitro_fixture_time() -> UnixTime {
+ UnixTime::since_unix_epoch(std::time::Duration::from_secs(NITRO_FIXTURE_TIMESTAMP_SECS))
+ }
+
+ fn gcp_fixture_time() -> UnixTime {
+ UnixTime::since_unix_epoch(std::time::Duration::from_secs(GCP_FIXTURE_TIMESTAMP_SECS))
}
#[test]
@@ -315,11 +377,11 @@ mod tests {
let output: AttestationOutput =
serde_json::from_str(fixture).expect("Failed to parse test-nitro-fixture.json");
- let result = verify_attestation_output(&output, fixture_time())
+ let result = verify_attestation_output(&output, nitro_fixture_time())
.expect("Verification should succeed");
- // Should be verified via Nitro path (no EK certs in fixture)
- assert_eq!(result.method, VerificationMethod::Nitro);
+ // Should be AWS (Nitro)
+ assert_eq!(result.provider, CloudProvider::Aws);
// Nonce is now from TPM2B_ATTEST.extraData, not the raw attest_data field
assert!(!result.nonce.is_empty());
@@ -329,20 +391,43 @@ mod tests {
assert_eq!(result.root_pubkey_hash.len(), 64); // SHA-256 = 32 bytes = 64 hex chars
}
+ #[test]
+ fn test_verify_gcp_amd_fixture() {
+ let fixture = include_str!("../test-gcp-amd-fixture.json");
+ let output: AttestationOutput =
+ serde_json::from_str(fixture).expect("Failed to parse test-gcp-fixture.json");
+
+ let result = verify_attestation_output(&output, gcp_fixture_time())
+ .expect("Verification should succeed");
+
+ // Should be GCP
+ assert_eq!(result.provider, CloudProvider::Gcp);
+
+ // Should have the nonce from the Quote
+ assert!(!result.nonce.is_empty());
+ assert_eq!(
+ result.nonce,
+ "8a543108a653b4a1162232744cc9b945017a449dea4fbb0ca62f42d3ef145562"
+ );
+
+ // Should have PCR values
+ assert!(!result.pcrs.is_empty());
+
+ // Should have GCP root pubkey hash
+ assert!(!result.root_pubkey_hash.is_empty());
+ assert_eq!(result.root_pubkey_hash.len(), 64);
+ }
+
#[test]
fn test_reject_empty_attestation() {
let output = AttestationOutput {
- ek_certificates: EkCertificates {
- rsa_2048: None,
- ecc_p256: None,
- ecc_p384: None,
- },
+ nonce: "deadbeef".to_string(),
pcrs: std::collections::HashMap::new(),
- ek_public_keys: std::collections::HashMap::new(),
- signing_key_public_keys: std::collections::HashMap::new(),
+ ak_pubkeys: std::collections::HashMap::new(),
attestation: AttestationContainer {
tpm: std::collections::HashMap::new(),
nitro: None,
+ gcp: None,
},
};
@@ -350,19 +435,24 @@ mod tests {
assert!(matches!(result, Err(VerifyError::NoValidAttestation(_))));
}
- /// Test that tampering with the convenience public_key field is detected
+ /// Test that tampering with the AK public key field is detected
#[test]
fn test_reject_tampered_nitro_public_key() {
let fixture = include_str!("../test-nitro-fixture.json");
let mut output: AttestationOutput =
serde_json::from_str(fixture).expect("Failed to parse test-nitro-fixture.json");
- // Tamper with the convenience field (attacker tries to substitute their own key)
- if let Some(ref mut nitro) = output.attestation.nitro {
- nitro.public_key = "04aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string();
- }
+ // Tamper with the AK public key (attacker tries to substitute their own key)
+ // This should fail because it won't match the signed value in Nitro document
+ output.ak_pubkeys.insert(
+ "ecc_p256".to_string(),
+ EccPublicKeyCoords {
+ x: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(),
+ y: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(),
+ },
+ );
- let result = verify_attestation_output(&output, fixture_time());
+ let result = verify_attestation_output(&output, nitro_fixture_time());
assert!(
matches!(result, Err(VerifyError::SignatureInvalid(_))),
"Should reject tampered public_key field, got: {:?}",
@@ -370,21 +460,19 @@ mod tests {
);
}
- /// Test that tampering with the convenience nonce field is detected
+ /// Test that tampering with the top-level nonce field is detected
#[test]
fn test_reject_tampered_nitro_nonce() {
let fixture = include_str!("../test-nitro-fixture.json");
let mut output: AttestationOutput =
serde_json::from_str(fixture).expect("Failed to parse test-nitro-fixture.json");
- // Tamper with the convenience field
- if let Some(ref mut nitro) = output.attestation.nitro {
- nitro.nonce = "deadbeef".to_string();
- }
+ // Tamper with the top-level nonce
+ output.nonce = "deadbeef".to_string();
- let result = verify_attestation_output(&output, fixture_time());
+ let result = verify_attestation_output(&output, nitro_fixture_time());
assert!(
- matches!(result, Err(VerifyError::SignatureInvalid(_))),
+ matches!(result, Err(VerifyError::InvalidAttest(_))),
"Should reject tampered nonce field, got: {:?}",
result
);
@@ -405,12 +493,51 @@ mod tests {
);
}
- let result = verify_attestation_output(&output, fixture_time());
- // Tampering is detected at policy verification: AK's authPolicy doesn't match PCR values
+ let result = verify_attestation_output(&output, nitro_fixture_time());
+ // Tampering is detected: claimed PCR values don't match signed values in Nitro document
assert!(
- matches!(result, Err(VerifyError::InvalidAttest(_))),
+ matches!(result, Err(VerifyError::SignatureInvalid(_))),
"Should reject tampered PCR values, got: {:?}",
result
);
}
}
+
+#[cfg(test)]
+mod tdx_tests {
+ use super::*;
+
+ // Timestamp for GCP TDX test fixture (Feb 3, 2026 when certificates are valid)
+ const GCP_TDX_FIXTURE_TIMESTAMP_SECS: u64 = 1770091200; // Feb 3, 2026 08:00:00 UTC
+
+ fn gcp_tdx_fixture_time() -> UnixTime {
+ UnixTime::since_unix_epoch(std::time::Duration::from_secs(
+ GCP_TDX_FIXTURE_TIMESTAMP_SECS,
+ ))
+ }
+
+ #[test]
+ fn test_verify_gcp_tdx_fixture() {
+ let fixture = include_str!("../test-gcp-tdx-fixture.json");
+ let output: AttestationOutput =
+ serde_json::from_str(fixture).expect("Failed to parse test-gcp-tdx-fixture.json");
+
+ let result = verify_attestation_output(&output, gcp_tdx_fixture_time())
+ .expect("Verification should succeed");
+
+ // Should be GCP
+ assert_eq!(result.provider, CloudProvider::Gcp);
+
+ // Should have the nonce from the Quote
+ assert_eq!(
+ result.nonce,
+ "6424632e79ec068f2189adf46d121b9a10f758c45a18c52f630da14600d4317b"
+ );
+
+ // Should have PCR values
+ assert!(!result.pcrs.is_empty());
+
+ // Root pubkey hash should match the known GCP root (same as AMD)
+ assert_eq!(result.root_pubkey_hash.len(), 64);
+ }
+}
diff --git a/crates/vaportpm-verify/src/nitro.rs b/crates/vaportpm-verify/src/nitro.rs
index 47470e4..3c8de1b 100644
--- a/crates/vaportpm-verify/src/nitro.rs
+++ b/crates/vaportpm-verify/src/nitro.rs
@@ -16,7 +16,7 @@ use x509_cert::Certificate;
use pki_types::UnixTime;
use crate::error::VerifyError;
-use crate::x509::{extract_public_key, validate_cert_chain};
+use crate::x509::{extract_public_key, validate_tpm_cert_chain};
/// Result of successful Nitro attestation verification
///
@@ -118,8 +118,10 @@ pub fn verify_nitro_attestation(
let leaf_cert = Certificate::from_der(&cert_der)
.map_err(|e| VerifyError::CertificateParse(format!("Invalid leaf cert: {}", e)))?;
+ // Build chain in leaf-to-root order
+ // AWS cabundle is ordered [root, ..., issuer], so we reverse it
let mut chain = vec![leaf_cert];
- for ca_der in cabundle {
+ for ca_der in cabundle.into_iter().rev() {
let ca_cert = Certificate::from_der(&ca_der)
.map_err(|e| VerifyError::CertificateParse(format!("Invalid CA cert: {}", e)))?;
chain.push(ca_cert);
@@ -130,9 +132,9 @@ pub fn verify_nitro_attestation(
let leaf_pubkey = extract_public_key(&chain[0])?;
verify_cose_signature(&cose_sign1, &leaf_pubkey, payload)?;
- // Validate certificate chain using webpki
- // This validates signatures, dates, and returns root's public key hash
- let chain_result = validate_cert_chain(&chain, time)?;
+ // Validate certificate chain
+ // This validates signatures, dates, extensions, and returns root's public key hash
+ let chain_result = validate_tpm_cert_chain(&chain, time)?;
let root_pubkey_hash = chain_result.root_pubkey_hash;
Ok(NitroVerifyResult {
diff --git a/crates/vaportpm-verify/src/tpm.rs b/crates/vaportpm-verify/src/tpm.rs
index 55b54f0..6a4d888 100644
--- a/crates/vaportpm-verify/src/tpm.rs
+++ b/crates/vaportpm-verify/src/tpm.rs
@@ -2,35 +2,11 @@
//! TPM attestation parsing and verification
-use std::collections::BTreeMap;
-
use ecdsa::signature::hazmat::PrehashVerifier;
use p256::ecdsa::{Signature as P256Signature, VerifyingKey as P256VerifyingKey};
-use serde::Serialize;
use sha2::{Digest, Sha256};
-use pki_types::UnixTime;
-
use crate::error::VerifyError;
-use crate::x509::{extract_public_key, parse_and_validate_cert_chain, parse_cert_chain_pem};
-
-// Import from vaportpm_attest (single source of truth)
-use vaportpm_attest::{PcrOps, Tpm, TpmAlg};
-
-/// Result of successful TPM attestation verification
-///
-/// This struct is only returned when verification succeeds.
-/// Verification checks:
-/// 1. EK public key from attestation matches EK certificate's public key
-/// 2. AK signature over nonce is valid
-/// 3. Certificate chain validates to root CA
-#[derive(Debug, Serialize)]
-pub struct TpmVerifyResult {
- /// The nonce that was signed (hex-encoded)
- pub nonce: String,
- /// SHA-256 hash of the root CA's public key (hex string)
- pub root_pubkey_hash: String,
-}
/// Verify ECDSA-SHA256 signature over a message
pub fn verify_ecdsa_p256(
@@ -54,254 +30,44 @@ pub fn verify_ecdsa_p256(
.map_err(|e| VerifyError::SignatureInvalid(format!("Signature verification failed: {}", e)))
}
-/// Verify TPM attestation
-///
-/// This verification approach works with TCG standard EKs (decrypt-only, cannot sign).
-/// It verifies:
-/// 1. The EK certificate chain validates to a root CA
-/// 2. The EK public key from the attestation output matches the certificate's EK public key
-/// 3. The AK's signature over the nonce is valid
-///
-/// # Arguments
-/// * `nonce_hex` - The nonce/attest_data as hex string
-/// * `signature_hex` - DER-encoded ECDSA signature as hex string (from AK)
-/// * `ak_pubkey_x_hex` - AK public key X coordinate (hex)
-/// * `ak_pubkey_y_hex` - AK public key Y coordinate (hex)
-/// * `ek_pubkey_x_hex` - EK public key X coordinate from attestation output (hex)
-/// * `ek_pubkey_y_hex` - EK public key Y coordinate from attestation output (hex)
-/// * `ek_certs_pem` - EK certificate chain in PEM format
-///
-/// # Returns
-/// Verification result with nonce and root public key hash.
-/// Returns an error if signature, chain validation, or EK pubkey matching fails.
-pub fn verify_tpm_attestation(
- nonce_hex: &str,
- signature_hex: &str,
- ak_pubkey_x_hex: &str,
- ak_pubkey_y_hex: &str,
- ek_pubkey_x_hex: &str,
- ek_pubkey_y_hex: &str,
- ek_certs_pem: &str,
-) -> Result {
- // Decode hex inputs
- let nonce = hex::decode(nonce_hex)?;
- let signature = hex::decode(signature_hex)?;
- let ak_x = hex::decode(ak_pubkey_x_hex)?;
- let ak_y = hex::decode(ak_pubkey_y_hex)?;
- let ek_x = hex::decode(ek_pubkey_x_hex)?;
- let ek_y = hex::decode(ek_pubkey_y_hex)?;
-
- // Construct AK public key in SEC1 uncompressed format: 0x04 || X || Y
- let mut ak_pubkey = vec![0x04];
- ak_pubkey.extend(&ak_x);
- ak_pubkey.extend(&ak_y);
-
- // Construct EK public key in SEC1 uncompressed format: 0x04 || X || Y
- let mut ek_pubkey = vec![0x04];
- ek_pubkey.extend(&ek_x);
- ek_pubkey.extend(&ek_y);
-
- // Parse the certificate chain to extract leaf cert's public key
- let chain = parse_cert_chain_pem(ek_certs_pem)?;
-
- // Extract EK public key from the leaf certificate
- let cert_ek_pubkey = extract_public_key(&chain[0])?;
-
- // Compare EK public key from attestation output with certificate's EK public key
- if ek_pubkey != cert_ek_pubkey {
- return Err(VerifyError::SignatureInvalid(
- "EK public key from attestation does not match certificate's EK public key".into(),
- ));
- }
-
- // Verify the AK's signature over the nonce
- // Note: The AK signs SHA-256(nonce), not the raw nonce
- verify_ecdsa_p256(&nonce, &signature, &ak_pubkey)?;
-
- // Validate the certificate chain and get root's public key hash
- // This uses webpki for signature and date validation
- let chain_result = parse_and_validate_cert_chain(ek_certs_pem, UnixTime::now())?;
- let root_pubkey_hash = chain_result.root_pubkey_hash;
-
- Ok(TpmVerifyResult {
- nonce: nonce_hex.to_string(),
- root_pubkey_hash,
- })
-}
-
-/// Verify TPM signature only (without EK certificate chain validation)
-///
-/// This is used in the Nitro path where trust comes from the Nitro attestation
-/// binding the TPM signing key. No EK certificate validation is needed.
-///
-/// # Arguments
-/// * `nonce_hex` - The nonce/attest_data as hex string
-/// * `signature_hex` - DER-encoded ECDSA signature as hex string (from AK)
-/// * `ak_pubkey_x_hex` - AK public key X coordinate (hex)
-/// * `ak_pubkey_y_hex` - AK public key Y coordinate (hex)
-///
-/// # Returns
-/// The verified nonce (hex-encoded) if signature is valid.
-pub fn verify_tpm_signature_only(
- nonce_hex: &str,
- signature_hex: &str,
- ak_pubkey_x_hex: &str,
- ak_pubkey_y_hex: &str,
-) -> Result {
- // Decode hex inputs
- let nonce = hex::decode(nonce_hex)?;
- let signature = hex::decode(signature_hex)?;
- let ak_x = hex::decode(ak_pubkey_x_hex)?;
- let ak_y = hex::decode(ak_pubkey_y_hex)?;
-
- // Construct AK public key in SEC1 uncompressed format: 0x04 || X || Y
- let mut ak_pubkey = vec![0x04];
- ak_pubkey.extend(&ak_x);
- ak_pubkey.extend(&ak_y);
-
- // Verify the AK's signature over the nonce
- verify_ecdsa_p256(&nonce, &signature, &ak_pubkey)?;
-
- Ok(nonce_hex.to_string())
-}
-
-/// Calculate the expected PCR policy digest from PCR values
-///
-/// This calculates the TPM2 PolicyPCR digest that would be used as an
-/// authPolicy for a key bound to the given PCR values.
-///
-/// Uses the same implementation as vaportpm_attest to ensure consistency.
-///
-/// # Arguments
-/// * `pcrs` - Map of PCR index to hex-encoded PCR value
-/// * `pcr_alg` - The hash algorithm of the PCR bank (determines expected PCR size)
-///
-/// # Returns
-/// The expected policy digest as a hex-encoded string
-///
-/// # Example
-/// ```ignore
-/// let mut pcrs = BTreeMap::new();
-/// pcrs.insert(0, "0000...".to_string()); // 64 hex chars for SHA-256
-/// pcrs.insert(1, "0000...".to_string());
-/// let policy = calculate_pcr_policy(&pcrs, TpmAlg::Sha256)?;
-/// ```
-pub fn calculate_pcr_policy(
- pcrs: &BTreeMap,
- pcr_alg: TpmAlg,
-) -> Result {
- if pcrs.is_empty() {
- return Err(VerifyError::InvalidAttest("No PCR values provided".into()));
- }
-
- // Determine expected PCR size based on algorithm
- let expected_size = match pcr_alg {
- TpmAlg::Sha256 => 32,
- TpmAlg::Sha384 => 48,
- _ => {
- return Err(VerifyError::InvalidAttest(format!(
- "Unsupported PCR algorithm: {:?}",
- pcr_alg
- )))
- }
- };
-
- // Validate and convert PCR values from hex strings to bytes
- // PCRs must be in sorted order (BTreeMap guarantees this)
- let mut pcr_values: Vec<(u8, Vec)> = Vec::with_capacity(pcrs.len());
- for (&idx, value_hex) in pcrs.iter() {
- if idx > 23 {
- return Err(VerifyError::InvalidAttest(format!(
- "PCR index {} out of range (max 23)",
- idx
- )));
- }
- let value_bytes = hex::decode(value_hex)?;
- if value_bytes.len() != expected_size {
- return Err(VerifyError::InvalidAttest(format!(
- "PCR {} has invalid length for {:?}: expected {} bytes, got {}",
- idx,
- pcr_alg,
- expected_size,
- value_bytes.len()
- )));
- }
- pcr_values.push((idx, value_bytes));
- }
-
- // Use vaportpm_attest's implementation (single source of truth)
- let policy_digest = Tpm::calculate_pcr_policy_digest(&pcr_values, pcr_alg)
- .map_err(|e| VerifyError::InvalidAttest(format!("PCR policy calculation failed: {}", e)))?;
-
- Ok(hex::encode(policy_digest))
-}
-
-/// Verify that a policy digest matches the expected PCR values
-///
-/// This is useful for verifying that an AK's authPolicy (if known) matches
-/// the PCR values reported in an attestation.
-///
-/// # Arguments
-/// * `expected_policy_hex` - The expected policy digest (hex string)
-/// * `pcrs` - The PCR values to verify against
-/// * `pcr_alg` - The hash algorithm of the PCR bank
-///
-/// # Returns
-/// Ok(()) if the policy matches, error otherwise
-pub fn verify_pcr_policy(
- expected_policy_hex: &str,
- pcrs: &BTreeMap,
- pcr_alg: TpmAlg,
-) -> Result<(), VerifyError> {
- let calculated_policy = calculate_pcr_policy(pcrs, pcr_alg)?;
-
- if calculated_policy != expected_policy_hex {
- return Err(VerifyError::InvalidAttest(format!(
- "PCR policy mismatch: expected {}, calculated {}",
- expected_policy_hex, calculated_policy
- )));
- }
-
- Ok(())
-}
-
// =============================================================================
-// TPM2B_ATTEST parsing and NIZK verification
+// TPM2B_ATTEST parsing (Quote only - Certify removed)
// =============================================================================
/// TPM_GENERATED magic value (0xff544347 = "ΓΏTCG")
const TPM_GENERATED_VALUE: u32 = 0xff544347;
-/// TPM_ST_ATTEST_CERTIFY structure type
-const TPM_ST_ATTEST_CERTIFY: u16 = 0x8017;
+/// TPM_ST_ATTEST_QUOTE structure type
+const TPM_ST_ATTEST_QUOTE: u16 = 0x8018;
/// Size of TPMS_CLOCK_INFO structure: clock(8) + resetCount(4) + restartCount(4) + safe(1)
const TPMS_CLOCK_INFO_SIZE: usize = 17;
-/// Parsed TPMS_ATTEST structure (from TPM2_Certify)
+/// Parsed TPMS_ATTEST structure (from TPM2_Quote)
#[derive(Debug)]
-pub struct TpmAttestInfo {
+pub struct TpmQuoteInfo {
/// Nonce/qualifying data from extraData field (raw bytes)
pub nonce: Vec,
- /// Name of the certified object (nameAlg || H(public_area))
- pub certified_name: Vec,
/// Name of the signing key
pub signer_name: Vec,
+ /// PCR selection (algorithm, PCR indices as bitmap)
+ pub pcr_select: Vec<(u16, Vec)>,
+ /// Digest of the selected PCRs (hash of concatenated PCR values)
+ pub pcr_digest: Vec,
}
-/// Parse TPM2B_ATTEST structure (CERTIFY type)
+/// Parse TPM2B_ATTEST structure (QUOTE type)
///
/// TPM2B_ATTEST contains a TPMS_ATTEST structure which includes:
/// - magic: 0xff544347 (TPM_GENERATED_VALUE)
-/// - type: 0x8017 (TPM_ST_ATTEST_CERTIFY)
+/// - type: 0x8018 (TPM_ST_ATTEST_QUOTE)
/// - qualifiedSigner: TPM2B_NAME
/// - extraData: TPM2B_DATA (our nonce)
/// - clockInfo: TPMS_CLOCK_INFO
/// - firmwareVersion: u64
-/// - attested.certify.name: TPM2B_NAME (certified object's name)
-/// - attested.certify.qualifiedName: TPM2B_NAME
-pub fn parse_tpm2b_attest(data: &[u8]) -> Result {
- // Use a cursor to track position with overflow-safe arithmetic
+/// - attested.quote.pcrSelect: TPML_PCR_SELECTION (PCRs that were quoted)
+/// - attested.quote.pcrDigest: TPM2B_DIGEST (hash of PCR values)
+pub fn parse_quote_attest(data: &[u8]) -> Result {
let mut cursor = SafeCursor::new(data);
// magic (4 bytes)
@@ -317,10 +83,10 @@ pub fn parse_tpm2b_attest(data: &[u8]) -> Result {
// type (2 bytes)
let type_bytes = cursor.read_bytes(2, "type")?;
let attest_type = u16::from_be_bytes(type_bytes.try_into().unwrap());
- if attest_type != TPM_ST_ATTEST_CERTIFY {
+ if attest_type != TPM_ST_ATTEST_QUOTE {
return Err(VerifyError::InvalidAttest(format!(
- "Invalid attest type: expected 0x{:04x} (CERTIFY), got 0x{:04x}",
- TPM_ST_ATTEST_CERTIFY, attest_type
+ "Invalid attest type: expected 0x{:04x} (QUOTE), got 0x{:04x}",
+ TPM_ST_ATTEST_QUOTE, attest_type
)));
}
@@ -336,14 +102,18 @@ pub fn parse_tpm2b_attest(data: &[u8]) -> Result {
// firmwareVersion (8 bytes) - skip it
cursor.skip(8, "firmwareVersion")?;
- // attested (TPMS_CERTIFY_INFO)
- // - name (TPM2B_NAME)
- let certified_name = cursor.read_tpm2b("certifiedName")?;
+ // attested (TPMS_QUOTE_INFO)
+ // - pcrSelect (TPML_PCR_SELECTION)
+ let pcr_select = cursor.read_pcr_selection("pcrSelect")?;
+
+ // - pcrDigest (TPM2B_DIGEST)
+ let pcr_digest = cursor.read_tpm2b("pcrDigest")?;
- Ok(TpmAttestInfo {
+ Ok(TpmQuoteInfo {
nonce,
- certified_name,
signer_name,
+ pcr_select,
+ pcr_digest,
})
}
@@ -390,6 +160,67 @@ impl<'a> SafeCursor<'a> {
let data = self.read_bytes(size, field)?;
Ok(data.to_vec())
}
+
+ /// Read a u16 value (big-endian)
+ fn read_u16(&mut self, field: &str) -> Result {
+ let bytes = self.read_bytes(2, field)?;
+ Ok(u16::from_be_bytes(bytes.try_into().unwrap()))
+ }
+
+ /// Read a u32 value (big-endian)
+ fn read_u32(&mut self, field: &str) -> Result {
+ let bytes = self.read_bytes(4, field)?;
+ Ok(u32::from_be_bytes(bytes.try_into().unwrap()))
+ }
+
+ /// Read a u8 value
+ fn read_u8(&mut self, field: &str) -> Result {
+ let bytes = self.read_bytes(1, field)?;
+ Ok(bytes[0])
+ }
+
+ /// Read TPML_PCR_SELECTION structure
+ ///
+ /// Returns a list of (algorithm, PCR bitmap) pairs
+ fn read_pcr_selection(&mut self, field: &str) -> Result)>, VerifyError> {
+ let count = self.read_u32(&format!("{}.count", field))?;
+
+ // Sanity check: count should be reasonable (max ~16 different algorithms)
+ if count > 16 {
+ return Err(VerifyError::InvalidAttest(format!(
+ "{}: count {} exceeds reasonable maximum",
+ field, count
+ )));
+ }
+
+ let mut selections = Vec::with_capacity(count as usize);
+ for i in 0..count {
+ // TPMS_PCR_SELECTION:
+ // - hash (2 bytes) - algorithm
+ let hash_alg = self.read_u16(&format!("{}.selection[{}].hash", field, i))?;
+
+ // - sizeofSelect (1 byte) - bitmap size (typically 3 for 24 PCRs)
+ let bitmap_size = self.read_u8(&format!("{}.selection[{}].sizeofSelect", field, i))?;
+
+ // Sanity check: bitmap size should be reasonable (max 32 for 256 PCRs)
+ if bitmap_size > 32 {
+ return Err(VerifyError::InvalidAttest(format!(
+ "{}.selection[{}].sizeofSelect {} exceeds maximum",
+ field, i, bitmap_size
+ )));
+ }
+
+ // - pcrSelect (variable) - bitmap
+ let bitmap = self.read_bytes(
+ bitmap_size as usize,
+ &format!("{}.selection[{}].pcrSelect", field, i),
+ )?;
+
+ selections.push((hash_alg, bitmap.to_vec()));
+ }
+
+ Ok(selections)
+ }
}
#[cfg(test)]
@@ -398,7 +229,6 @@ mod tests {
use ecdsa::signature::hazmat::PrehashSigner;
use p256::ecdsa::SigningKey;
use sha2::Sha256;
- use vaportpm_attest::TpmAlg;
/// Generate a test P-256 key pair and sign a message
/// The signature is over SHA256(message) to match what verify_ecdsa_p256 expects
@@ -596,247 +426,4 @@ mod tests {
let result = verify_ecdsa_p256(&message, &signature, &pubkey);
assert!(result.is_ok());
}
-
- // === PCR Policy Calculation Tests ===
-
- #[test]
- fn test_calculate_pcr_policy_single_pcr() {
- // Test with a single PCR (all zeros)
- let mut pcrs = BTreeMap::new();
- let pcr0 = "0".repeat(64); // 32 bytes of zeros as hex
- pcrs.insert(0, pcr0);
-
- let result = calculate_pcr_policy(&pcrs, TpmAlg::Sha256);
- assert!(result.is_ok());
-
- let policy = result.unwrap();
- // Policy should be a 64-character hex string (32 bytes)
- assert_eq!(policy.len(), 64);
- }
-
- #[test]
- fn test_calculate_pcr_policy_multiple_pcrs() {
- // Test with PCRs 0, 1, 2
- let mut pcrs = BTreeMap::new();
- pcrs.insert(0, "0".repeat(64));
- pcrs.insert(1, "1".repeat(64)); // All 0x11...
- pcrs.insert(2, "2".repeat(64)); // All 0x22...
-
- let result = calculate_pcr_policy(&pcrs, TpmAlg::Sha256);
- assert!(result.is_ok());
- }
-
- #[test]
- fn test_calculate_pcr_policy_non_contiguous() {
- // Test with non-contiguous PCRs (0, 7, 15)
- let mut pcrs = BTreeMap::new();
- pcrs.insert(0, "0".repeat(64));
- pcrs.insert(7, "7".repeat(64));
- pcrs.insert(15, "f".repeat(64));
-
- let result = calculate_pcr_policy(&pcrs, TpmAlg::Sha256);
- assert!(result.is_ok());
- }
-
- #[test]
- fn test_calculate_pcr_policy_deterministic() {
- // Same input should produce same output
- let mut pcrs = BTreeMap::new();
- pcrs.insert(0, "0".repeat(64));
-
- let policy1 = calculate_pcr_policy(&pcrs, TpmAlg::Sha256).unwrap();
- let policy2 = calculate_pcr_policy(&pcrs, TpmAlg::Sha256).unwrap();
-
- assert_eq!(policy1, policy2);
- }
-
- #[test]
- fn test_calculate_pcr_policy_different_values() {
- // Different PCR values should produce different policies
- let mut pcrs1 = BTreeMap::new();
- pcrs1.insert(0, "0".repeat(64));
-
- let mut pcrs2 = BTreeMap::new();
- pcrs2.insert(0, "1".repeat(64));
-
- let policy1 = calculate_pcr_policy(&pcrs1, TpmAlg::Sha256).unwrap();
- let policy2 = calculate_pcr_policy(&pcrs2, TpmAlg::Sha256).unwrap();
-
- assert_ne!(policy1, policy2);
- }
-
- #[test]
- fn test_calculate_pcr_policy_empty() {
- let pcrs: BTreeMap = BTreeMap::new();
- let result = calculate_pcr_policy(&pcrs, TpmAlg::Sha256);
- assert!(matches!(result, Err(VerifyError::InvalidAttest(_))));
- }
-
- #[test]
- fn test_calculate_pcr_policy_invalid_index() {
- let mut pcrs = BTreeMap::new();
- pcrs.insert(24, "0".repeat(64)); // Index 24 is invalid (max 23)
-
- let result = calculate_pcr_policy(&pcrs, TpmAlg::Sha256);
- assert!(matches!(result, Err(VerifyError::InvalidAttest(_))));
- }
-
- #[test]
- fn test_calculate_pcr_policy_invalid_length() {
- let mut pcrs = BTreeMap::new();
- pcrs.insert(0, "0".repeat(32)); // Only 16 bytes, need 32
-
- let result = calculate_pcr_policy(&pcrs, TpmAlg::Sha256);
- assert!(matches!(result, Err(VerifyError::InvalidAttest(_))));
- }
-
- #[test]
- fn test_calculate_pcr_policy_invalid_hex() {
- let mut pcrs = BTreeMap::new();
- pcrs.insert(0, "gg".repeat(32)); // Invalid hex
-
- let result = calculate_pcr_policy(&pcrs, TpmAlg::Sha256);
- assert!(matches!(result, Err(VerifyError::HexDecode(_))));
- }
-
- #[test]
- fn test_verify_pcr_policy_match() {
- let mut pcrs = BTreeMap::new();
- pcrs.insert(0, "0".repeat(64));
-
- let expected = calculate_pcr_policy(&pcrs, TpmAlg::Sha256).unwrap();
- let result = verify_pcr_policy(&expected, &pcrs, TpmAlg::Sha256);
- assert!(result.is_ok());
- }
-
- #[test]
- fn test_verify_pcr_policy_mismatch() {
- let mut pcrs = BTreeMap::new();
- pcrs.insert(0, "0".repeat(64));
-
- // Wrong expected policy
- let wrong_expected = "f".repeat(64);
- let result = verify_pcr_policy(&wrong_expected, &pcrs, TpmAlg::Sha256);
- assert!(matches!(result, Err(VerifyError::InvalidAttest(_))));
- }
-
- // === SHA-384 PCR Policy Tests ===
-
- #[test]
- fn test_calculate_pcr_policy_sha384() {
- // Test with SHA-384 PCRs (48 bytes = 96 hex chars)
- let mut pcrs = BTreeMap::new();
- pcrs.insert(0, "0".repeat(96));
-
- let result = calculate_pcr_policy(&pcrs, TpmAlg::Sha384);
- assert!(result.is_ok());
-
- let policy = result.unwrap();
- // Policy is always SHA-256 (32 bytes = 64 hex chars)
- assert_eq!(policy.len(), 64);
- }
-
- #[test]
- fn test_calculate_pcr_policy_sha384_wrong_size() {
- // SHA-384 expects 48 bytes, not 32
- let mut pcrs = BTreeMap::new();
- pcrs.insert(0, "0".repeat(64)); // 32 bytes - wrong for SHA-384
-
- let result = calculate_pcr_policy(&pcrs, TpmAlg::Sha384);
- assert!(matches!(result, Err(VerifyError::InvalidAttest(_))));
- }
-
- #[test]
- fn test_calculate_pcr_policy_different_alg_different_policy() {
- // Same PCR value but different algorithms should produce different policies
- // because the algorithm ID is encoded in the PCR selection structure
- let mut pcrs256 = BTreeMap::new();
- pcrs256.insert(0, "0".repeat(64)); // 32 bytes for SHA-256
-
- let mut pcrs384 = BTreeMap::new();
- pcrs384.insert(0, "0".repeat(96)); // 48 bytes for SHA-384 (different zeros count)
-
- let policy256 = calculate_pcr_policy(&pcrs256, TpmAlg::Sha256).unwrap();
- let policy384 = calculate_pcr_policy(&pcrs384, TpmAlg::Sha384).unwrap();
-
- // Policies should differ due to algorithm ID in selection structure
- assert_ne!(policy256, policy384);
- }
-
- // === Malicious Input Tests for parse_tpm2b_attest ===
-
- #[test]
- fn test_attest_empty_input() {
- let result = parse_tpm2b_attest(&[]);
- assert!(matches!(result, Err(VerifyError::InvalidAttest(_))));
- }
-
- #[test]
- fn test_attest_truncated_magic() {
- // Only 2 bytes when magic needs 4
- let result = parse_tpm2b_attest(&[0xff, 0x54]);
- assert!(matches!(result, Err(VerifyError::InvalidAttest(_))));
- }
-
- #[test]
- fn test_attest_wrong_magic() {
- // Valid length but wrong magic value
- let mut data = vec![0x00, 0x00, 0x00, 0x00]; // Wrong magic
- data.extend(&[0x80, 0x17]); // Correct type
- let result = parse_tpm2b_attest(&data);
- assert!(matches!(result, Err(VerifyError::InvalidAttest(_))));
- }
-
- #[test]
- fn test_attest_huge_signer_size() {
- // Craft input with valid magic/type but huge signer size
- let mut data = vec![];
- data.extend(&0xff544347u32.to_be_bytes()); // magic
- data.extend(&0x8017u16.to_be_bytes()); // type
- data.extend(&0xffffu16.to_be_bytes()); // signer size = 65535 (way too big)
-
- let result = parse_tpm2b_attest(&data);
- assert!(
- matches!(result, Err(VerifyError::InvalidAttest(_))),
- "Should reject huge signer size, got: {:?}",
- result
- );
- }
-
- #[test]
- fn test_attest_huge_extra_size() {
- // Craft input with valid magic/type, small signer, but huge extra size
- let mut data = vec![];
- data.extend(&0xff544347u32.to_be_bytes()); // magic
- data.extend(&0x8017u16.to_be_bytes()); // type
- data.extend(&0x0000u16.to_be_bytes()); // signer size = 0
- data.extend(&0xffffu16.to_be_bytes()); // extra size = 65535 (way too big)
-
- let result = parse_tpm2b_attest(&data);
- assert!(
- matches!(result, Err(VerifyError::InvalidAttest(_))),
- "Should reject huge extra size, got: {:?}",
- result
- );
- }
-
- #[test]
- fn test_attest_truncated_after_sizes() {
- // Valid header but truncated before clockInfo
- let mut data = vec![];
- data.extend(&0xff544347u32.to_be_bytes()); // magic
- data.extend(&0x8017u16.to_be_bytes()); // type
- data.extend(&0x0002u16.to_be_bytes()); // signer size = 2
- data.extend(&[0x00, 0x0b]); // signer (2 bytes)
- data.extend(&0x0004u16.to_be_bytes()); // extra size = 4
- data.extend(&[0x01, 0x02, 0x03, 0x04]); // extra (4 bytes)
- // Missing: clockInfo, firmwareVersion, certifiedName
-
- let result = parse_tpm2b_attest(&data);
- assert!(
- matches!(result, Err(VerifyError::InvalidAttest(_))),
- "Should reject truncated input, got: {:?}",
- result
- );
- }
}
diff --git a/crates/vaportpm-verify/src/x509.rs b/crates/vaportpm-verify/src/x509.rs
index 53ece61..928b6d8 100644
--- a/crates/vaportpm-verify/src/x509.rs
+++ b/crates/vaportpm-verify/src/x509.rs
@@ -1,19 +1,139 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
-//! X.509 certificate handling using rustls-webpki for chain validation
+//! X.509 certificate chain validation
use base64::{engine::general_purpose::STANDARD, Engine as _};
+use der::oid::ObjectIdentifier;
use der::{Decode, Encode};
use ecdsa::signature::Verifier;
use p256::ecdsa::{Signature as P256Signature, VerifyingKey as P256VerifyingKey};
use p384::ecdsa::{Signature as P384Signature, VerifyingKey as P384VerifyingKey};
-use pki_types::{CertificateDer, UnixTime};
+use pki_types::UnixTime;
+use rsa::pkcs1v15::{Signature as RsaSignature, VerifyingKey as RsaVerifyingKey};
+use rsa::RsaPublicKey;
use sha2::{Digest, Sha256};
-use webpki::{anchor_from_trusted_cert, EndEntityCert, KeyUsage};
use x509_cert::Certificate;
use crate::error::VerifyError;
+// X.509 extension OIDs
+const OID_KEY_USAGE: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.29.15");
+const OID_BASIC_CONSTRAINTS: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.29.19");
+
+/// Key Usage extension flags (OID 2.5.29.15)
+/// Only includes bits used for TPM certificate chain validation.
+#[derive(Debug, Clone, Default)]
+pub struct KeyUsageFlags {
+ /// digitalSignature (bit 0) - key can be used to verify digital signatures
+ pub digital_signature: bool,
+ /// keyCertSign (bit 5) - key can be used to verify certificate signatures
+ pub key_cert_sign: bool,
+}
+
+/// Basic Constraints extension (OID 2.5.29.19)
+#[derive(Debug, Clone, Default)]
+pub struct BasicConstraints {
+ /// Whether this certificate is a CA
+ pub ca: bool,
+ /// Maximum number of intermediate certificates allowed below this CA
+ pub path_len_constraint: Option,
+}
+
+/// Extract Key Usage extension from a certificate (OID 2.5.29.15)
+///
+/// Returns None if the extension is not present.
+pub fn extract_key_usage(cert: &Certificate) -> Option {
+ let extensions = cert.tbs_certificate.extensions.as_ref()?;
+
+ for ext in extensions.iter() {
+ if ext.extn_id == OID_KEY_USAGE {
+ // Key Usage is a BIT STRING
+ // The extn_value is an OctetString containing the DER-encoded BIT STRING directly
+ // (x509_cert already unwrapped the outer OCTET STRING)
+ let bit_string = der::asn1::BitString::from_der(ext.extn_value.as_bytes()).ok()?;
+
+ // Key Usage bits are numbered from the most significant bit
+ // Bit 0 = digitalSignature (MSB of first byte)
+ // Bit 5 = keyCertSign
+ let raw_bits = bit_string.raw_bytes();
+ if raw_bits.is_empty() {
+ return Some(KeyUsageFlags::default());
+ }
+
+ let byte0 = raw_bits[0];
+
+ return Some(KeyUsageFlags {
+ digital_signature: (byte0 & 0x80) != 0, // bit 0
+ key_cert_sign: (byte0 & 0x04) != 0, // bit 5
+ });
+ }
+ }
+ None
+}
+
+/// Extract Basic Constraints extension from a certificate (OID 2.5.29.19)
+///
+/// Returns None if the extension is not present.
+pub fn extract_basic_constraints(cert: &Certificate) -> Option {
+ let extensions = cert.tbs_certificate.extensions.as_ref()?;
+
+ for ext in extensions.iter() {
+ if ext.extn_id == OID_BASIC_CONSTRAINTS {
+ // BasicConstraints ::= SEQUENCE { cA BOOLEAN DEFAULT FALSE, pathLenConstraint INTEGER (0..MAX) OPTIONAL }
+ // The extn_value is an OctetString containing the DER-encoded SEQUENCE directly
+ let bytes = ext.extn_value.as_bytes();
+
+ // Parse the SEQUENCE header manually
+ if bytes.is_empty() {
+ return Some(BasicConstraints::default());
+ }
+
+ // First byte should be SEQUENCE tag (0x30)
+ if bytes[0] != 0x30 {
+ return Some(BasicConstraints::default());
+ }
+
+ // Get length
+ if bytes.len() < 2 {
+ return Some(BasicConstraints::default());
+ }
+
+ let len = bytes[1] as usize;
+ let seq_start = 2;
+
+ // Empty sequence means cA defaults to false
+ if len == 0 {
+ return Some(BasicConstraints::default());
+ }
+
+ // Parse sequence contents
+ let seq_bytes = &bytes[seq_start..seq_start + len.min(bytes.len() - seq_start)];
+
+ let mut bc = BasicConstraints::default();
+
+ // Check if there's a BOOLEAN (tag 0x01)
+ if !seq_bytes.is_empty() && seq_bytes[0] == 0x01 {
+ // BOOLEAN: tag (0x01), length (0x01), value
+ if seq_bytes.len() >= 3 && seq_bytes[1] == 0x01 {
+ bc.ca = seq_bytes[2] != 0;
+
+ // Check for pathLenConstraint after BOOLEAN
+ if seq_bytes.len() >= 6 && seq_bytes[3] == 0x02 {
+ // INTEGER: tag (0x02), length, value
+ let int_len = seq_bytes[4] as usize;
+ if int_len == 1 && seq_bytes.len() >= 6 {
+ bc.path_len_constraint = Some(seq_bytes[5]);
+ }
+ }
+ }
+ }
+
+ return Some(bc);
+ }
+ }
+ None
+}
+
/// Maximum allowed certificate chain depth (to prevent DoS)
pub const MAX_CHAIN_DEPTH: usize = 10;
@@ -160,102 +280,33 @@ pub struct ChainValidationResult {
pub root_pubkey_hash: String,
}
-/// Validate a certificate chain using webpki
+/// Validate certificate chain with rigid X.509 validation
///
-/// Chain should be leaf-first, root-last. Time must be provided by caller.
-pub fn validate_cert_chain(
- chain: &[Certificate],
- time: UnixTime,
-) -> Result {
- if chain.is_empty() {
- return Err(VerifyError::ChainValidation(
- "Empty certificate chain".into(),
- ));
- }
- if chain.len() > MAX_CHAIN_DEPTH {
- return Err(VerifyError::ChainValidation(format!(
- "Certificate chain too deep: {} certificates (max {})",
- chain.len(),
- MAX_CHAIN_DEPTH
- )));
- }
-
- // Get signature verification algorithms from rustls-rustcrypto
- let sig_algs = rustls_rustcrypto::provider()
- .signature_verification_algorithms
- .all;
-
- // Convert to DER format for webpki
- let cert_ders: Vec = chain
- .iter()
- .map(|c| {
- let der = c.to_der().map_err(|e| {
- VerifyError::CertificateParse(format!("Failed to encode cert to DER: {}", e))
- })?;
- Ok(CertificateDer::from(der))
- })
- .collect::, VerifyError>>()?;
-
- // Root is last in chain - create trust anchor from it
- let root_der = &cert_ders[cert_ders.len() - 1];
- let trust_anchor = anchor_from_trusted_cert(root_der)
- .map_err(|e| VerifyError::ChainValidation(format!("Invalid root certificate: {:?}", e)))?;
-
- // Leaf is first
- let ee_cert = EndEntityCert::try_from(&cert_ders[0])
- .map_err(|e| VerifyError::CertificateParse(format!("Invalid leaf certificate: {:?}", e)))?;
-
- // Intermediates are everything between leaf and root
- let intermediates: Vec = if cert_ders.len() > 2 {
- cert_ders[1..cert_ders.len() - 1].to_vec()
- } else {
- Vec::new()
- };
-
- // Use webpki to verify the chain
- ee_cert
- .verify_for_usage(
- sig_algs,
- &[trust_anchor],
- &intermediates,
- time,
- KeyUsage::client_auth(),
- None, // no revocation checking
- None, // no custom path verification
- )
- .map_err(|e| VerifyError::ChainValidation(format!("Chain validation failed: {:?}", e)))?;
-
- // Extract and hash root's public key
- let root_pubkey = extract_public_key(&chain[chain.len() - 1])?;
- let root_hash = hash_public_key(&root_pubkey);
-
- Ok(ChainValidationResult {
- root_pubkey_hash: root_hash,
- })
-}
-
-/// Parse PEM and validate certificate chain
+/// This function performs comprehensive certificate chain validation suitable for
+/// TPM attestation key certificates. It validates:
///
-/// Convenience wrapper that parses PEM then validates.
-/// Chain should be leaf-first, root-last in the PEM.
-pub fn parse_and_validate_cert_chain(
- chain_pem: &str,
- time: UnixTime,
-) -> Result {
- let certs = parse_cert_chain_pem(chain_pem)?;
- validate_cert_chain(&certs, time)
-}
-
-/// Validate TPM EK certificate chain
-///
-/// Similar to validate_cert_chain but without Extended Key Usage (EKU) checking.
-/// TPM EK certificates use TPM-specific EKU OID (2.23.133.8.1) which is not
-/// recognized by webpki's standard EKU validation.
-///
-/// This function validates:
+/// **Signature chain:**
/// - Each certificate is signed by the next in chain
+/// - Root certificate is self-signed
+///
+/// **Time validity:**
/// - Certificate validity periods include the specified time
-/// - Chain is not too deep
+///
+/// **Basic Constraints (OID 2.5.29.19):**
+/// - Leaf certificate must have `CA:FALSE` (or no Basic Constraints)
+/// - Intermediate/root certificates must have `CA:TRUE`
+/// - Path length constraints are honored
+///
+/// **Key Usage (OID 2.5.29.15):**
+/// - Leaf certificate must have `digitalSignature` bit set
+/// - CA certificates must have `keyCertSign` bit set
+///
+/// **Extended Key Usage (OID 2.5.29.37):**
+/// - CA certificates should have TPM EK Certificate EKU (2.23.133.8.1)
+/// - Note: GCP AK leaf certificates don't have EKU, only Key Usage
+///
+/// **Name chaining:**
+/// - Each certificate's Issuer must match its parent's Subject
///
/// Chain should be leaf-first, root-last.
pub fn validate_tpm_cert_chain(
@@ -275,7 +326,89 @@ pub fn validate_tpm_cert_chain(
)));
}
+ // === X.509 Extension Validation ===
+ // Validate Basic Constraints, Key Usage, EKU, and name chaining
+
+ for (i, cert) in chain.iter().enumerate() {
+ let is_leaf = i == 0;
+ let is_root = i == chain.len() - 1;
+
+ // 1. Basic Constraints validation
+ if let Some(bc) = extract_basic_constraints(cert) {
+ if is_leaf && bc.ca {
+ return Err(VerifyError::ChainValidation(
+ "Leaf certificate has CA:TRUE - must be CA:FALSE".into(),
+ ));
+ }
+ if !is_leaf && !bc.ca {
+ return Err(VerifyError::ChainValidation(format!(
+ "Certificate {} (intermediate/root) must have CA:TRUE",
+ i
+ )));
+ }
+
+ // Check pathLenConstraint for CA certificates
+ // pathLenConstraint limits how many CAs can exist below this one
+ if !is_leaf {
+ if let Some(path_len) = bc.path_len_constraint {
+ // Number of CAs below this certificate in the chain
+ // i=0 is leaf, so CAs below cert at position i are at positions 0..i-1
+ // But the count should be: number of intermediate CAs between this CA and the leaf
+ // For position i, there are (i - 1) intermediate CAs between it and the leaf
+ // (position 0 is leaf, positions 1..i-1 are intermediates below)
+ let cas_below = if i > 0 { i - 1 } else { 0 };
+ if cas_below > path_len as usize {
+ return Err(VerifyError::ChainValidation(format!(
+ "Certificate {} pathLenConstraint violated: allows {} CAs below, but {} exist",
+ i, path_len, cas_below
+ )));
+ }
+ }
+ }
+ } else if !is_leaf {
+ // CA certificates SHOULD have Basic Constraints
+ // This is a SHOULD per RFC 5280, but we enforce it for security
+ return Err(VerifyError::ChainValidation(format!(
+ "Certificate {} (intermediate/root) missing Basic Constraints extension",
+ i
+ )));
+ }
+
+ // 2. Key Usage validation
+ if let Some(ku) = extract_key_usage(cert) {
+ if is_leaf && !ku.digital_signature {
+ return Err(VerifyError::ChainValidation(
+ "Leaf certificate missing digitalSignature key usage".into(),
+ ));
+ }
+ if !is_leaf && !ku.key_cert_sign {
+ return Err(VerifyError::ChainValidation(format!(
+ "Certificate {} (CA) missing keyCertSign key usage",
+ i
+ )));
+ }
+ } else if is_leaf {
+ // Leaf certificate MUST have Key Usage for signing
+ return Err(VerifyError::ChainValidation(
+ "Leaf certificate missing Key Usage extension".into(),
+ ));
+ }
+
+ // 3. Subject/Issuer name chaining
+ if !is_root {
+ let parent = &chain[i + 1];
+ if cert.tbs_certificate.issuer != parent.tbs_certificate.subject {
+ return Err(VerifyError::ChainValidation(format!(
+ "Certificate {} issuer does not match parent subject",
+ i
+ )));
+ }
+ }
+ }
+
+ // === Signature Chain Validation ===
// Validate each certificate is signed by the next one in chain
+
for i in 0..chain.len() - 1 {
let cert = &chain[i];
let issuer = &chain[i + 1];
@@ -299,9 +432,38 @@ pub fn validate_tpm_cert_chain(
// ECDSA with SHA-384 on P-384: 1.2.840.10045.4.3.3
const ECDSA_SHA256_OID: &str = "1.2.840.10045.4.3.2";
const ECDSA_SHA384_OID: &str = "1.2.840.10045.4.3.3";
+ const RSA_SHA256_OID: &str = "1.2.840.113549.1.1.11";
let alg_str = alg_oid.to_string();
match alg_str.as_str() {
+ RSA_SHA256_OID => {
+ // RSA PKCS#1 v1.5 with SHA-256 verification
+ // For RSA, we need the full SPKI structure, not just raw key bytes
+ let issuer_spki = &issuer.tbs_certificate.subject_public_key_info;
+ let issuer_spki_der = issuer_spki.to_der().map_err(|e| {
+ VerifyError::ChainValidation(format!("Failed to encode issuer SPKI: {}", e))
+ })?;
+
+ let rsa_pubkey = RsaPublicKey::try_from(
+ spki::SubjectPublicKeyInfoRef::try_from(issuer_spki_der.as_slice()).map_err(
+ |e| VerifyError::ChainValidation(format!("Invalid RSA SPKI: {}", e)),
+ )?,
+ )
+ .map_err(|e| VerifyError::ChainValidation(format!("Invalid RSA key: {}", e)))?;
+
+ let verifying_key = RsaVerifyingKey::::new(rsa_pubkey);
+
+ let signature = RsaSignature::try_from(sig_bytes).map_err(|e| {
+ VerifyError::ChainValidation(format!("Invalid RSA signature: {}", e))
+ })?;
+
+ verifying_key.verify(&tbs_der, &signature).map_err(|_| {
+ VerifyError::ChainValidation(format!(
+ "Certificate {} RSA signature verification failed",
+ i
+ ))
+ })?;
+ }
ECDSA_SHA256_OID => {
// P-256 verification
if issuer_pubkey.len() != 65 || issuer_pubkey[0] != 0x04 {
@@ -357,7 +519,9 @@ pub fn validate_tpm_cert_chain(
}
}
+ // === Time Validity ===
// Validate time for each certificate
+
let unix_secs = time.as_secs();
for (i, cert) in chain.iter().enumerate() {
@@ -559,4 +723,59 @@ mod tests {
assert!(!is_valid_base64_line("ABC DEF")); // space not allowed
assert!(is_valid_base64_line("")); // empty is valid
}
+
+ // === Extension Parsing Tests ===
+
+ #[test]
+ fn test_extract_basic_constraints_ca_true() {
+ // GCP intermediate certificate with CA:TRUE
+ let cert_b64 = "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";
+ let cert_der = STANDARD.decode(cert_b64).unwrap();
+ let cert = Certificate::from_der(&cert_der).unwrap();
+
+ let bc = extract_basic_constraints(&cert);
+ assert!(bc.is_some(), "Basic Constraints should be present");
+ let bc = bc.unwrap();
+ assert!(bc.ca, "CA flag should be true for intermediate CA cert");
+ }
+
+ #[test]
+ fn test_extract_basic_constraints_ca_false() {
+ // GCP AK leaf certificate with CA:FALSE
+ let cert_b64 = "MIIFITCCAwmgAwIBAgIUAJSyAthxJCCD6ViYtC96+AGDJCswDQYJKoZIhvcNAQELBQAwgYYxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRMwEQYDVQQKEwpHb29nbGUgTExDMRUwEwYDVQQLEwxHb29nbGUgQ2xvdWQxHjAcBgNVBAMTFUVLL0FLIENBIEludGVybWVkaWF0ZTAgFw0yNjAyMDIwNjU3NDdaGA8yMDU2MDEyNjA2NTc0NlowaTEWMBQGA1UEBxMNdXMtY2VudHJhbDEtZjEeMBwGA1UEChMVR29vZ2xlIENvbXB1dGUgRW5naW5lMREwDwYDVQQLEwhsb2NrYm9vdDEcMBoGA1UEAxMTMzQxNDI0MDY0ODIyNTQ4NTgzNjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABMp9xzVQQpEV600JB3Gj9IP+U89ypLnrhQRUBVDFnz5INT2kVd4Jhl8KHZ6qYXVOOZYhrkvO0cVY0mfclyT+tJOjggFqMIIBZjAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU2lwtWfsKczujmD2yTgCf4MptjpkwHwYDVR0jBBgwFoAUZ8O73ljj1lF2j7MaPtsHp+yTeuQwgY0GCCsGAQUFBwEBBIGAMH4wfAYIKwYBBQUHMAKGcGh0dHA6Ly9wcml2YXRlY2EtY29udGVudC02NWQ1M2IxNC0wMDAwLTIxMmEtYTYzMy04ODNkMjRmNTdiYjguc3RvcmFnZS5nb29nbGVhcGlzLmNvbS8wYzNlNzllYjA4OThkMDJlYmIwYS9jYS5jcnQwdgYKKwYBBAHWeQIBFQRoMGYMDXVzLWNlbnRyYWwxLWYCBTY7VDeMDAhsb2NrYm9vdAIIL2HRx7ct9AwMGGluc3RhbmNlLTIwMjYwMjAyLTA2NTYwOaAgMB6gAwIBAKEDAQH/ogMBAf+jAwEBAKQDAQEApQMBAQAwDQYJKoZIhvcNAQELBQADggIBAKL4yGiPbAA63EQ7bxJ+2HGDo2EC+qymJDHtWski2lRGY70u+xIdywTW5l4k7JnnwM+fk7LHtfP0md0WU4Mw30B+51sc+pXprEn1SHpP20I/mM5AZMR+cMy9SWnr0WkfTZYKvYJMhDPKuZyK8JvUtXx6NM9AGHYxtPfFnvw2F/USBYYf7/N2KHMhUB9v1zFuHwMD/LDxduIw27kYUcVHttTbHGL9Uljflz343qL2YFE8QpRqtQ/0GK4UaJ3kzPYcbMbWBgpZgKQ2UIMfldvEO8hbGqO4hkNPsv4TPQcG0mJHyUWt+jTOFesBuDgbR/8R4lnrGL3QYZo3Oj7URCJdPiJ+ztohscGydjMVvYfaVziAGJbhMbADnyh/HBshi9gc4rQ99NbOA9fmCZnl+vcMp9jwaAzWZQxc1dNsfdRuTrQSwsNXn+PXJUDgRKKNsENY73QbVBUYvlBmQPkw8zqp8Htvtlsv5AImFFJsW4XsGt4CzZLOhlNW6Ckc58fjVSmeC66kTxYefRGh1SCXRZiJovuTynnF3Z6CFnWvnN/8dCmgjD+S0JUZ8Znx2NSxyZLbEqc6TYcJKx6R8B8QKa4EHWtKMuMorykvWpXMrVl1QjVm0HyVBvgzI+xRAO2TrS6g6c6pChj9BPXPgwLbVXlXQSstoUBFSORdz0S0HrEqxCg8";
+ let cert_der = STANDARD.decode(cert_b64).unwrap();
+ let cert = Certificate::from_der(&cert_der).unwrap();
+
+ let bc = extract_basic_constraints(&cert);
+ assert!(bc.is_some(), "Basic Constraints should be present");
+ let bc = bc.unwrap();
+ assert!(!bc.ca, "CA flag should be false for leaf cert");
+ }
+
+ #[test]
+ fn test_extract_key_usage_digital_signature() {
+ // GCP AK leaf certificate with Key Usage: Digital Signature
+ let cert_b64 = "MIIFITCCAwmgAwIBAgIUAJSyAthxJCCD6ViYtC96+AGDJCswDQYJKoZIhvcNAQELBQAwgYYxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRMwEQYDVQQKEwpHb29nbGUgTExDMRUwEwYDVQQLEwxHb29nbGUgQ2xvdWQxHjAcBgNVBAMTFUVLL0FLIENBIEludGVybWVkaWF0ZTAgFw0yNjAyMDIwNjU3NDdaGA8yMDU2MDEyNjA2NTc0NlowaTEWMBQGA1UEBxMNdXMtY2VudHJhbDEtZjEeMBwGA1UEChMVR29vZ2xlIENvbXB1dGUgRW5naW5lMREwDwYDVQQLEwhsb2NrYm9vdDEcMBoGA1UEAxMTMzQxNDI0MDY0ODIyNTQ4NTgzNjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABMp9xzVQQpEV600JB3Gj9IP+U89ypLnrhQRUBVDFnz5INT2kVd4Jhl8KHZ6qYXVOOZYhrkvO0cVY0mfclyT+tJOjggFqMIIBZjAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU2lwtWfsKczujmD2yTgCf4MptjpkwHwYDVR0jBBgwFoAUZ8O73ljj1lF2j7MaPtsHp+yTeuQwgY0GCCsGAQUFBwEBBIGAMH4wfAYIKwYBBQUHMAKGcGh0dHA6Ly9wcml2YXRlY2EtY29udGVudC02NWQ1M2IxNC0wMDAwLTIxMmEtYTYzMy04ODNkMjRmNTdiYjguc3RvcmFnZS5nb29nbGVhcGlzLmNvbS8wYzNlNzllYjA4OThkMDJlYmIwYS9jYS5jcnQwdgYKKwYBBAHWeQIBFQRoMGYMDXVzLWNlbnRyYWwxLWYCBTY7VDeMDAhsb2NrYm9vdAIIL2HRx7ct9AwMGGluc3RhbmNlLTIwMjYwMjAyLTA2NTYwOaAgMB6gAwIBAKEDAQH/ogMBAf+jAwEBAKQDAQEApQMBAQAwDQYJKoZIhvcNAQELBQADggIBAKL4yGiPbAA63EQ7bxJ+2HGDo2EC+qymJDHtWski2lRGY70u+xIdywTW5l4k7JnnwM+fk7LHtfP0md0WU4Mw30B+51sc+pXprEn1SHpP20I/mM5AZMR+cMy9SWnr0WkfTZYKvYJMhDPKuZyK8JvUtXx6NM9AGHYxtPfFnvw2F/USBYYf7/N2KHMhUB9v1zFuHwMD/LDxduIw27kYUcVHttTbHGL9Uljflz343qL2YFE8QpRqtQ/0GK4UaJ3kzPYcbMbWBgpZgKQ2UIMfldvEO8hbGqO4hkNPsv4TPQcG0mJHyUWt+jTOFesBuDgbR/8R4lnrGL3QYZo3Oj7URCJdPiJ+ztohscGydjMVvYfaVziAGJbhMbADnyh/HBshi9gc4rQ99NbOA9fmCZnl+vcMp9jwaAzWZQxc1dNsfdRuTrQSwsNXn+PXJUDgRKKNsENY73QbVBUYvlBmQPkw8zqp8Htvtlsv5AImFFJsW4XsGt4CzZLOhlNW6Ckc58fjVSmeC66kTxYefRGh1SCXRZiJovuTynnF3Z6CFnWvnN/8dCmgjD+S0JUZ8Znx2NSxyZLbEqc6TYcJKx6R8B8QKa4EHWtKMuMorykvWpXMrVl1QjVm0HyVBvgzI+xRAO2TrS6g6c6pChj9BPXPgwLbVXlXQSstoUBFSORdz0S0HrEqxCg8";
+ let cert_der = STANDARD.decode(cert_b64).unwrap();
+ let cert = Certificate::from_der(&cert_der).unwrap();
+
+ let ku = extract_key_usage(&cert);
+ assert!(ku.is_some(), "Key Usage should be present");
+ let ku = ku.unwrap();
+ assert!(ku.digital_signature, "digitalSignature bit should be set");
+ assert!(!ku.key_cert_sign, "keyCertSign should not be set for leaf");
+ }
+
+ #[test]
+ fn test_extract_key_usage_ca() {
+ // GCP intermediate certificate with Key Usage: Certificate Sign, CRL Sign
+ let cert_b64 = "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";
+ let cert_der = STANDARD.decode(cert_b64).unwrap();
+ let cert = Certificate::from_der(&cert_der).unwrap();
+
+ let ku = extract_key_usage(&cert);
+ assert!(ku.is_some(), "Key Usage should be present");
+ let ku = ku.unwrap();
+ assert!(ku.key_cert_sign, "keyCertSign bit should be set for CA");
+ }
}
diff --git a/crates/vaportpm-verify/test-gcp-amd-fixture.json b/crates/vaportpm-verify/test-gcp-amd-fixture.json
new file mode 100644
index 0000000..831b312
--- /dev/null
+++ b/crates/vaportpm-verify/test-gcp-amd-fixture.json
@@ -0,0 +1,48 @@
+{
+ "nonce": "8a543108a653b4a1162232744cc9b945017a449dea4fbb0ca62f42d3ef145562",
+ "pcrs": {
+ "sha256": {
+ "0": "a0b5ff3383a1116bd7dc6df177c0c2d433b9ee1813ea958fa5d166a202cb2a85",
+ "1": "6c7b2cb52b90bffa55bf88c65ca3a047253c101f19041418abfc95be075d1bf9",
+ "2": "3d458cfe55cc03ea1f443f1562beec8df51c75e14a9fcf9a7234a13f198e7969",
+ "3": "3d458cfe55cc03ea1f443f1562beec8df51c75e14a9fcf9a7234a13f198e7969",
+ "4": "a274e8cad520128a8b8e607f7216645a288e81e09f3f130186ec4ef84754b7b7",
+ "5": "833f07fa4ee51aee2932ddcc04c3b0e6687d9806fa3f4712534ca8898ab11d18",
+ "6": "3d458cfe55cc03ea1f443f1562beec8df51c75e14a9fcf9a7234a13f198e7969",
+ "7": "a11c5239a222bb78072c2c73caa691bb9a0f118de2d95cdce1fce06711e4d3ed",
+ "8": "31e25dc9719b9506242d98f152952504b0a0a650ef73e2f178f7c6048477f541",
+ "9": "432d8127b440f9ef26835a3f91a18e19a9c360fdb286260319eae813f060d3ee",
+ "10": "12f087b4cb357d1a7d2fe480237079a853c6bf8ae5a0fe76016a3c7908853348",
+ "11": "0000000000000000000000000000000000000000000000000000000000000000",
+ "12": "0000000000000000000000000000000000000000000000000000000000000000",
+ "13": "0000000000000000000000000000000000000000000000000000000000000000",
+ "14": "306f9d8b94f17d93dc6e7cf8f5c79d652eb4c6c4d13de2dddc24af416e13ecaf",
+ "15": "0000000000000000000000000000000000000000000000000000000000000000",
+ "16": "0000000000000000000000000000000000000000000000000000000000000000",
+ "17": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "18": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "19": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "20": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "21": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "22": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "23": "0000000000000000000000000000000000000000000000000000000000000000"
+ }
+ },
+ "ak_pubkeys": {
+ "ecc_p256": {
+ "x": "ca7dc73550429115eb4d090771a3f483fe53cf72a4b9eb8504540550c59f3e48",
+ "y": "353da455de09865f0a1d9eaa61754e399621ae4bced1c558d267dc9724feb493"
+ }
+ },
+ "attestation": {
+ "tpm": {
+ "ecc_p256": {
+ "attest_data": "ff54434780180022000b764ba22a45fd6fc2720b130e119dfd62e2937cd2b05c4cc2a075142d08ea9b2e00208a543108a653b4a1162232744cc9b945017a449dea4fbb0ca62f42d3ef14556200000000001b039c0000000f0000000001201605110016280000000001000b03ffffff0020be5e5e4c5c2b3d2f55dfb017641e4240cd5b9372dcd72bdf3b559dca544fad4d",
+ "signature": "304402200134cc97ae682c7f7080ed474d714b1d8e5918785e336bdd9d7f657855cfb984022018e658d56ae85ed9d52acbdde6e46bd8f18a37af6dd40e1c977bc6dbedd360da"
+ }
+ },
+ "gcp": {
+ "ak_cert_chain": "-----BEGIN CERTIFICATE-----\nMIIFITCCAwmgAwIBAgIUAJSyAthxJCCD6ViYtC96+AGDJCswDQYJKoZIhvcNAQEL\nBQAwgYYxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQH\nEw1Nb3VudGFpbiBWaWV3MRMwEQYDVQQKEwpHb29nbGUgTExDMRUwEwYDVQQLEwxH\nb29nbGUgQ2xvdWQxHjAcBgNVBAMTFUVLL0FLIENBIEludGVybWVkaWF0ZTAgFw0y\nNjAyMDIwNjU3NDdaGA8yMDU2MDEyNjA2NTc0NlowaTEWMBQGA1UEBxMNdXMtY2Vu\ndHJhbDEtZjEeMBwGA1UEChMVR29vZ2xlIENvbXB1dGUgRW5naW5lMREwDwYDVQQL\nEwhsb2NrYm9vdDEcMBoGA1UEAxMTMzQxNDI0MDY0ODIyNTQ4NTgzNjBZMBMGByqG\nSM49AgEGCCqGSM49AwEHA0IABMp9xzVQQpEV600JB3Gj9IP+U89ypLnrhQRUBVDF\nnz5INT2kVd4Jhl8KHZ6qYXVOOZYhrkvO0cVY0mfclyT+tJOjggFqMIIBZjAOBgNV\nHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU2lwtWfsKczujmD2y\nTgCf4MptjpkwHwYDVR0jBBgwFoAUZ8O73ljj1lF2j7MaPtsHp+yTeuQwgY0GCCsG\nAQUFBwEBBIGAMH4wfAYIKwYBBQUHMAKGcGh0dHA6Ly9wcml2YXRlY2EtY29udGVu\ndC02NWQ1M2IxNC0wMDAwLTIxMmEtYTYzMy04ODNkMjRmNTdiYjguc3RvcmFnZS5n\nb29nbGVhcGlzLmNvbS8wYzNlNzllYjA4OThkMDJlYmIwYS9jYS5jcnQwdgYKKwYB\nBAHWeQIBFQRoMGYMDXVzLWNlbnRyYWwxLWYCBTY7VDeMDAhsb2NrYm9vdAIIL2HR\nx7ct9AwMGGluc3RhbmNlLTIwMjYwMjAyLTA2NTYwOaAgMB6gAwIBAKEDAQH/ogMB\nAf+jAwEBAKQDAQEApQMBAQAwDQYJKoZIhvcNAQELBQADggIBAKL4yGiPbAA63EQ7\nbxJ+2HGDo2EC+qymJDHtWski2lRGY70u+xIdywTW5l4k7JnnwM+fk7LHtfP0md0W\nU4Mw30B+51sc+pXprEn1SHpP20I/mM5AZMR+cMy9SWnr0WkfTZYKvYJMhDPKuZyK\n8JvUtXx6NM9AGHYxtPfFnvw2F/USBYYf7/N2KHMhUB9v1zFuHwMD/LDxduIw27kY\nUcVHttTbHGL9Uljflz343qL2YFE8QpRqtQ/0GK4UaJ3kzPYcbMbWBgpZgKQ2UIMf\nldvEO8hbGqO4hkNPsv4TPQcG0mJHyUWt+jTOFesBuDgbR/8R4lnrGL3QYZo3Oj7U\nRCJdPiJ+ztohscGydjMVvYfaVziAGJbhMbADnyh/HBshi9gc4rQ99NbOA9fmCZnl\n+vcMp9jwaAzWZQxc1dNsfdRuTrQSwsNXn+PXJUDgRKKNsENY73QbVBUYvlBmQPkw\n8zqp8Htvtlsv5AImFFJsW4XsGt4CzZLOhlNW6Ckc58fjVSmeC66kTxYefRGh1SCX\nRZiJovuTynnF3Z6CFnWvnN/8dCmgjD+S0JUZ8Znx2NSxyZLbEqc6TYcJKx6R8B8Q\nKa4EHWtKMuMorykvWpXMrVl1QjVm0HyVBvgzI+xRAO2TrS6g6c6pChj9BPXPgwLb\nVXlXQSstoUBFSORdz0S0HrEqxCg8\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIHIjCCBQqgAwIBAgITaI//DbE0ilSC7SjJnNGPwAKK+jANBgkqhkiG9w0BAQsF\nADB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMN\nTW91bnRhaW4gVmlldzETMBEGA1UEChMKR29vZ2xlIExMQzEVMBMGA1UECxMMR29v\nZ2xlIENsb3VkMRYwFAYDVQQDEw1FSy9BSyBDQSBSb290MCAXDTI0MDIyMjIxNDQx\nNVoYDzIxMjIwNzA4MDU1NzIzWjCBhjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNh\nbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEzARBgNVBAoTCkdvb2ds\nZSBMTEMxFTATBgNVBAsTDEdvb2dsZSBDbG91ZDEeMBwGA1UEAxMVRUsvQUsgQ0Eg\nSW50ZXJtZWRpYXRlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2FCg\nrrj91CGYC7saslxTDswjGS5oPfBnEwjrZMRUOkqA/mcxe1o1svsMllMtNjOd3MSJ\nkRwEUuwnX707XhNSBE64y2JSEotF43l/Vq+PeBlZXvJHa0JhDh8DU6c4heLd2XOV\nYs6fV2bAZv+SOuFmmiPt753TAcljNFeMZIaQ4gXEjLZodvBU/D09UUf92trSihuK\nZWGpjtRuT2ep+C4x4PL4XQlfmYY8H5V5BqBO2vFyHpctFxlrNxCMjG6TQlX07zZO\n9sQADFl1hwJkS9BoYaXCUdAewrweElkIe9P9P1MkwQhUU8dlAJrYOizZ1drCII4T\nzWf69mGe9F/cIxEfdu7ZPqeH5fJB3tahGZiP+TYK6Ey+2uTvDp7xO9GwTdMTckpa\nmU4oOnXufUKIdYohUuwy0vjb2D4VtWSVjgcL7aL93zaLyosHfSgsZoQs8FjPKzWV\nqFAq9RsTR3mkk891aC5drMR6lb1wkxfqPy9rS56Iom/cnATHxMlAysEvlUTzMd9n\nB3dOVaY1DINzuv/ZohRwkoIVFFxO+LjvhJGBkAGEWw36bNzV4slsfG9g2+o76Ilu\noDZmgAmaDKLvZvNu0aBrfBXZA3zWYFbnHnzijGN+XyLD7vJ0cd9SQ+Z6JOhQV3YS\nXgcqH5xSl0qcs/nYpghqvtIa/TLE2NgsM28vZn0CAwEAAaOCAYwwggGIMA4GA1Ud\nDwEB/wQEAwIBBjAQBgNVHSUECTAHBgVngQUIATAPBgNVHRMBAf8EBTADAQH/MB0G\nA1UdDgQWBBRnw7veWOPWUXaPsxo+2wen7JN65DAfBgNVHSMEGDAWgBRJ50pbVin1\nnXm3pjA8A7KP5xTdTDCBjQYIKwYBBQUHAQEEgYAwfjB8BggrBgEFBQcwAoZwaHR0\ncDovL3ByaXZhdGVjYS1jb250ZW50LTYyZDcxNzczLTAwMDAtMjFkYS04NTJlLWY0\nZjVlODBkNzc3OC5zdG9yYWdlLmdvb2dsZWFwaXMuY29tLzAzMmJmOWQzOWRiNGZh\nMDZhYWRlL2NhLmNydDCBggYDVR0fBHsweTB3oHWgc4ZxaHR0cDovL3ByaXZhdGVj\nYS1jb250ZW50LTYyZDcxNzczLTAwMDAtMjFkYS04NTJlLWY0ZjVlODBkNzc3OC5z\ndG9yYWdlLmdvb2dsZWFwaXMuY29tLzAzMmJmOWQzOWRiNGZhMDZhYWRlL2NybC5j\ncmwwDQYJKoZIhvcNAQELBQADggIBAG11IpWVb2PSB7jxDYYKlvYIyW4w5xI3DR/v\nblykiCHugY9lx2ukCXd9s6UV7gbpb5J1yysin0gkXa5FudKUl4DHb9O3rT5BaAaa\nwz/iuvoVDZBlOfeMg9sCCbZf0apMTVG4b03VIUL6LRZ9DljipLJN78+/s0OHWS/x\nEEqw/Z8pwg9MID3kEU7iBxVtIKCoOQW+ENtmfaPTNLFORbBxeUvKOgslTlC2Nrfa\nwPB/YP+rTB6EwZthhzeQ3oW/MvFzorr5LWBEjNY+wreYR7bu1x2qbegUAb83qnOt\nKktU0pREI/cX5Jfv9Bgt9u2Z532BneMXwMrGda6LHdTmjG1AkfB7SgSZDkg0dkvT\nEKpclGg/bRjFQRGYtKLhvMlZmj7ag54dqp01KLS33ujDSSI3QmS2MFArqxt/jQQJ\n/w3iuwcFi+BUm848fOdmSgOrufo/l0BaQuj7plVT0W2JUsaBkSw56YGOET8Dw7im\n2Z87bu3EvVMPuIcK15gQlYrrObDp8KRijqSxqQ5kBFUp1kArq0vBLqBdvyjWIQ/n\n104nxkp990d5RR9RURTMadDHCqHXGADRDXC0J8Zyqp2IarLFITqAotM8fCRaEuih\nHSVuAxYBMuMCDIf+Ps7ZHbfJOTjw5QuUF+VTPL1yAb7eJHbIUczCgt7o5Rqh2evH\n3j4IQ1VD\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\n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\nav16U4E=\n-----END CERTIFICATE-----\n"
+ }
+ }
+}
\ No newline at end of file
diff --git a/crates/vaportpm-verify/test-gcp-tdx-fixture.json b/crates/vaportpm-verify/test-gcp-tdx-fixture.json
new file mode 100644
index 0000000..84bc3e2
--- /dev/null
+++ b/crates/vaportpm-verify/test-gcp-tdx-fixture.json
@@ -0,0 +1,48 @@
+{
+ "nonce": "6424632e79ec068f2189adf46d121b9a10f758c45a18c52f630da14600d4317b",
+ "pcrs": {
+ "sha256": {
+ "0": "0cca9ec161b09288802e5a112255d21340ed5b797f5fe29cecccfd8f67b9f802",
+ "1": "8d9072270d85f934b34a274b78bae66f343cf9ff9f8578d67a2c66b9c3831993",
+ "2": "0762893a4f936faa16b3f21b7cb914668fe79bf34e9777a595f9ee09de86fb85",
+ "3": "3d458cfe55cc03ea1f443f1562beec8df51c75e14a9fcf9a7234a13f198e7969",
+ "4": "c36d9e765efa54f2981707bb7dec3c38385476cf91a413f7d85dced9f092e21f",
+ "5": "a5ceb755d043f32431d63e39f5161464620a3437280494b5850dc1b47cc074e0",
+ "6": "3d458cfe55cc03ea1f443f1562beec8df51c75e14a9fcf9a7234a13f198e7969",
+ "7": "a11c5239a222bb78072c2c73caa691bb9a0f118de2d95cdce1fce06711e4d3ed",
+ "8": "31e25dc9719b9506242d98f152952504b0a0a650ef73e2f178f7c6048477f541",
+ "9": "432d8127b440f9ef26835a3f91a18e19a9c360fdb286260319eae813f060d3ee",
+ "10": "96173f289ee41de4501c185a1a31408663f1420b7df85aafa2c83ba634ed19c8",
+ "11": "0000000000000000000000000000000000000000000000000000000000000000",
+ "12": "0000000000000000000000000000000000000000000000000000000000000000",
+ "13": "0000000000000000000000000000000000000000000000000000000000000000",
+ "14": "306f9d8b94f17d93dc6e7cf8f5c79d652eb4c6c4d13de2dddc24af416e13ecaf",
+ "15": "0000000000000000000000000000000000000000000000000000000000000000",
+ "16": "0000000000000000000000000000000000000000000000000000000000000000",
+ "17": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "18": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "19": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "20": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "21": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "22": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
+ "23": "0000000000000000000000000000000000000000000000000000000000000000"
+ }
+ },
+ "ak_pubkeys": {
+ "ecc_p256": {
+ "x": "c67c129afeaf4dab6e99f38245b280ecef81141648390b77e2ad633a93d20443",
+ "y": "a434cb13af43be4f31fc68d71a6b20c8e2be6e00519ec5decacfea917e5905fa"
+ }
+ },
+ "attestation": {
+ "tpm": {
+ "ecc_p256": {
+ "attest_data": "ff54434780180022000b118e5f465caa83d41fc47913e21ae07675957a53c8dba718b36bf346770f2fc200206424632e79ec068f2189adf46d121b9a10f758c45a18c52f630da14600d4317b0000000000022d410000000f0000000001201605110016280000000001000b03ffffff0020afbea8968e40c50829044559f63f9b530ce570597df73967883b3a03435bd59a",
+ "signature": "3045022100d83d724d801993b52f5c6db12efdd44c9066b2b27016c2d9711c15ffd1ae1bda02207febe629d9739be59fc638d673969e70c513fc9cf8a407eab484021aa34ff8d2"
+ }
+ },
+ "gcp": {
+ "ak_cert_chain": "-----BEGIN CERTIFICATE-----\nMIIFIDCCAwigAwIBAgITTLN98N9aiFYPC71j3fLoAFsRezANBgkqhkiG9w0BAQsF\nADCBhjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcT\nDU1vdW50YWluIFZpZXcxEzARBgNVBAoTCkdvb2dsZSBMTEMxFTATBgNVBAsTDEdv\nb2dsZSBDbG91ZDEeMBwGA1UEAxMVRUsvQUsgQ0EgSW50ZXJtZWRpYXRlMCAXDTI2\nMDIwMzAyMzc0OVoYDzIwNTYwMTI3MDIzNzQ4WjBpMRYwFAYDVQQHEw11cy1jZW50\ncmFsMS1hMR4wHAYDVQQKExVHb29nbGUgQ29tcHV0ZSBFbmdpbmUxETAPBgNVBAsT\nCGxvY2tib290MRwwGgYDVQQDExMyMTQxMjA5NDIxMTEzMjU0MzAzMFkwEwYHKoZI\nzj0CAQYIKoZIzj0DAQcDQgAExnwSmv6vTatumfOCRbKA7O+BFBZIOQt34q1jOpPS\nBEOkNMsTr0O+TzH8aNcaayDI4r5uAFGexd7Kz+qRflkF+qOCAWowggFmMA4GA1Ud\nDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBR9cEBcn3GqNAd3D7nc\naj9XIZ0bgDAfBgNVHSMEGDAWgBQPIZ1W4anAicVs2iPAxPEysEqJejCBjQYIKwYB\nBQUHAQEEgYAwfjB8BggrBgEFBQcwAoZwaHR0cDovL3ByaXZhdGVjYS1jb250ZW50\nLTY1ZDE2ODhlLTAwMDAtMjIwMy04NTBlLTMwZmQzODE0NTZmOC5zdG9yYWdlLmdv\nb2dsZWFwaXMuY29tLzgxMGFmMzEzNDA2YWQzZTIwNzliL2NhLmNydDB2BgorBgEE\nAdZ5AgEVBGgwZgwNdXMtY2VudHJhbDEtYQIFNjtUN4wMCGxvY2tib290Aggdtxqf\ntyzhnwwYaW5zdGFuY2UtMjAyNjAyMDMtMDIzNDQ3oCAwHqADAgEAoQMBAf+iAwEB\n/6MDAQEApAMBAQClAwEBADANBgkqhkiG9w0BAQsFAAOCAgEACyYpZgCtAhMJjQKV\nUcoH6LtuVGYg3o0ynGohIW/SJ/heKm5w5HnSRAqF4rBSW/RQlbeb+P2bG4mYlvjR\nmkPdeDD+uWJLsDkoHGzJBfDWhvLNqmrnCg9vQ64KdOCWdshWxtmWBe0PqkfiYwqZ\nDsId4xxL9Kz7vLQwEm3mKk7rwllvnKwGYsIeoEXxnZGlweTnl5RyQXQdZjRKDTKA\nV0cIu1eLC280FjIMjpUdeHGPzkYpwHW+I/Ky3ar7N06/1ahRUtKg97CO6KUWj5hi\nyUjj2TO4/e9G5URtPaR1/VTKH89W5IjyvKXNzDfJ+FM9OBbzY5Sv4g4dKNbn1uya\n+NtRNJf+JfnTwy6G6Zl0EY6olT4FXUF2neU6Ddnsz3uoSZsgBcXCBflfDHbVrHbj\nel4gH8lLhkC1RxjGRzjiJ8Aeex3C/XKPxXiv1nz76U+iFwOuhF7i7cdsxz6/kR+G\nlspmdl07RM9M5l95yAWD27IEn/mK2+uSgnZVmmCnO8PXK20rKCt2SUKecG3xKQSk\nGkO9Aqavp2XhQrEtHavMX7R6LIzJGo6WV/cZp7Ba/pzvRNHLtCMrHXLsz7Twhoj9\nSI0bBwia+GUuBj6Los3fL2N4lc8f4RFNCoRa1y5Sm1MPRLrR3fYBO62OKE50RINB\npCZgRWLMwezqat019+h3x7yo4dY=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIHIzCCBQugAwIBAgIUAPoFnAtqNET6z84unSBu/fRbsB8wDQYJKoZIhvcNAQEL\nBQAwfjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcT\nDU1vdW50YWluIFZpZXcxEzARBgNVBAoTCkdvb2dsZSBMTEMxFTATBgNVBAsTDEdv\nb2dsZSBDbG91ZDEWMBQGA1UEAxMNRUsvQUsgQ0EgUm9vdDAgFw0yNDAyMjIyMTQz\nMzdaGA8yMTIyMDcwODA1NTcyM1owgYYxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpD\nYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRMwEQYDVQQKEwpHb29n\nbGUgTExDMRUwEwYDVQQLEwxHb29nbGUgQ2xvdWQxHjAcBgNVBAMTFUVLL0FLIENB\nIEludGVybWVkaWF0ZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKEv\nsKjcY2Ey18k/6M2I/rWFLwZ9k/8GrVVQlPamEQdCJBKCjvvMrdZ83a73YShsu2bO\nuTetMn8WWy0/C9u02Q6+LIartyOnhbRHtABNTQaz+tuZ5Fm9KcXY+5I0S2/oD4O6\nU6CKV2wR+wCkbBGVqzYsBCbboAJ9Vgy7pbxMa6mK+xZM2K4QbAGnTAdu1PZiNzOA\nsgSlAnlveNJkQg91ggLkQSLVrHrpSJOB/aEPRxK5W9iMFkOb22PZvSK6OkT8U8d6\nmTCjtWi15krSOitddpz97iBfi/a5gMhXKvLwhC8zGUTnW7WBJB5nG3DmFplXCKUC\n4ptGtJ5X1Jd2ZvaOz0aC3CIm3gLfNAGS3oboVj/hgDJeEvlxNwTh7NAmCaaeF8sO\nq4LGsMcfQe5gCz7DnJ5kBBBPqjt5jvfH4WoKQxsK+dzAoIFzozapiRuoMfkcs7w/\ntdzGaqAoiRXO4eRSNg+GU12PrTPgaPXG5fYIcPL29HbuwYmwx+KiIx7lRhjGK87x\nk2Lwjr0ppvcIBcYciAnxemPafDJF7Vp5/0eckul34r22+G4Xph20foShwR1bKM8K\nLHEiLe7/mH9WSPNDi/ad0zsnvr3hyL9D4cz0ZKlAvW00cgDqzqDMA/McmHnT5Umh\n6Ib88VvVAmCDtj0XDsjO+1MMcP3dZZKxkJjnBCW3AgMBAAGjggGMMIIBiDAOBgNV\nHQ8BAf8EBAMCAQYwEAYDVR0lBAkwBwYFZ4EFCAEwDwYDVR0TAQH/BAUwAwEB/zAd\nBgNVHQ4EFgQUDyGdVuGpwInFbNojwMTxMrBKiXowHwYDVR0jBBgwFoAUSedKW1Yp\n9Z15t6YwPAOyj+cU3UwwgY0GCCsGAQUFBwEBBIGAMH4wfAYIKwYBBQUHMAKGcGh0\ndHA6Ly9wcml2YXRlY2EtY29udGVudC02MmQ3MTc3My0wMDAwLTIxZGEtODUyZS1m\nNGY1ZTgwZDc3Nzguc3RvcmFnZS5nb29nbGVhcGlzLmNvbS8wMzJiZjlkMzlkYjRm\nYTA2YWFkZS9jYS5jcnQwgYIGA1UdHwR7MHkwd6B1oHOGcWh0dHA6Ly9wcml2YXRl\nY2EtY29udGVudC02MmQ3MTc3My0wMDAwLTIxZGEtODUyZS1mNGY1ZTgwZDc3Nzgu\nc3RvcmFnZS5nb29nbGVhcGlzLmNvbS8wMzJiZjlkMzlkYjRmYTA2YWFkZS9jcmwu\nY3JsMA0GCSqGSIb3DQEBCwUAA4ICAQCCObk/o+LpoCMpjLXQe7hNPll2ZTxbhOQw\n/j4TXIk6nTu5jihPX1OO4YZvLXD7ET8OjDdW6RP3V9j9hsh8r5P6D+vIiavBvCr1\nV+iLNXIcD6WKCXc3J+FG8J4BUuih4cx5sVF39CbS9NG/qBAhFUTDl75q96kRR6gZ\nguqmk3HgAKw4tmLO1kiWkSAHyweBen3Ag1exQHFSA4lRV0ukxhkGX4H3q+BnMYUz\nItGWKP/MhZFl8gH7N32jYtZMy1yYSZRVZlTCwcDRyVqQyVD64igYTwGyyTIa1BD7\nIMPggHJAmbS8GlNepm3N9q/21LYssshC0L8URmWhd69CtBaIpqwWGs9ewmrw4FoT\nH18EwMeiWd3xLH9pZQ2dyfTXgJGReGM5PND/GpEFWs5akYTSxnYHLU1VpFIhmtvd\naq6cOMc0aXYlgbb5qX4DHqF/cCD0NFaoAlIZw4zkhwRd/LCAiIgMR+Ub1b5YBBwx\ndzMZUYKJU11hoko2kXBemgkEVUFFIQwHUXD+K+/jwd0JP89zqGOJ7YEsj7coJlPr\nCBTWMsCC1rjTvq0TIjN899okeA/MLArMT0LgBdZPcy+QctAj68YTjT9PNs+qwK7v\nDFx72m/0R2sPnSgvWS9XQH0wmOOtA7idFTxEYcnREcPfpM3TPJfRY4QfkPpH9owy\ny5/I31K8bQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\n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\nav16U4E=\n-----END CERTIFICATE-----\n"
+ }
+ }
+}
\ No newline at end of file
diff --git a/crates/vaportpm-verify/test-nitro-fixture.json b/crates/vaportpm-verify/test-nitro-fixture.json
index b4fbd49..9ec3b00 100644
--- a/crates/vaportpm-verify/test-nitro-fixture.json
+++ b/crates/vaportpm-verify/test-nitro-fixture.json
@@ -1,9 +1,9 @@
{
- "ek_certificates": {},
+ "nonce": "8ec265e6a21b13c946c70ee90b604c4aa7a742d4a334f1f2b62e6088a002cc82",
"pcrs": {
"sha384": {
"0": "6e901b16932f6e036747d7a57696e4a2cc864008ebc016826ca1d7bd42ab5ac8286ccf49cde6c0284cbc4b63d978a2ec",
- "1": "7e10323dec25050c22d2e91709373c930d194be275bde5c5e01331b9cc6ef81a9e2586991636652605bc3d322dbc7109",
+ "1": "a1f2689838a6903d15408efa219421f0513347cacd8087ad3a8cc68bd0bdc710a13c60abb3fa9d819bc2abaa7ab1dd35",
"2": "518923b0f955d08da077c96aaba522b9decede61c599cea6c41889cfbea4ae4d50529d96fe4d1afdafb65e7f95bf23c4",
"3": "518923b0f955d08da077c96aaba522b9decede61c599cea6c41889cfbea4ae4d50529d96fe4d1afdafb65e7f95bf23c4",
"4": "1a1f166e1995c1e1bfdf1c1d13b1ebc9d5d4e44397501655c911a2062d92063483ca9e17f0264efe336d4b61c5b98d1e",
@@ -12,7 +12,7 @@
"7": "98441c7f7625d10058c47683aec486ce311c633235eb555593a7ee791121e3578ae72d04ecef661f272d59058b77af35",
"8": "056a862312c282d635a1e486812191efbf58c8c5533a89c3579b1809854bfa0090854e04b82acd93a752cbd32f1f38c1",
"9": "4d5b0773776a7520553b4c08189632a76fe83024fa462035c649a0f62cd82f3951132c386488fd87d875ebf27552156d",
- "10": "c1b534d2b043b50ae0fc0d415c4881d92f1633e6b318f967f8adb201eff718d080004fa7ce98b50f1bd8a808332fa1e2",
+ "10": "1b599d35451621d3d004a509acbd60736f0ac08178d9f86b0db09c4b088cafa2a747c92e81e7f524649824229e91574f",
"11": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"12": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"13": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
@@ -25,33 +25,24 @@
"20": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"21": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"22": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
- "23": "22a3e580d7d0931f4365ebfae2618b84e3cce2c0afbf0ea1fbe40e645a43a3771ceb3e188394803642266f5512ee0eac"
+ "23": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
}
},
- "ek_public_keys": {
+ "ak_pubkeys": {
"ecc_p256": {
- "x": "499109ce4bb49e2f0c85944fc79925c2a8d15c3362db9ee823eb16588e29e47e",
- "y": "4b985e434557f4cd0e604e4a0ed013907c037010c856695569de45cd403b4791"
- }
- },
- "signing_key_public_keys": {
- "ecc_p256": {
- "x": "2417c2d23945e9e68c78c730102f951bf41f5042464d8272cd18e32d4b8ccfb5",
- "y": "d4c3d4b93e361f7d8093f1f83c2396ad932a5c6bd238575e0de758a8c3a6ddc2"
+ "x": "866469563d68b071c23eef841e1e435b90d6fb3dafd408c683889b2b5017adfe",
+ "y": "5e759fc168fca05fa196d6d02004a3dbcd0f411033db54866695f46109d18eb7"
}
},
"attestation": {
"tpm": {
"ecc_p256": {
- "nonce": "31373639393233313833",
- "attest_data": "ff54434780170022000b149bb4c9c56850e3571bed5c4b6ed5e8736678d44d4564f9e2d9d400f7bff5ac000a313736393932333138330000000000240cb3e5738876f090bf840108bffe09fb566b9f0022000bc4a66f8f2b42b7d3d214802efab53fdf840024754cff2d4448657bf84f56d0520022000b149bb4c9c56850e3571bed5c4b6ed5e8736678d44d4564f9e2d9d400f7bff5ac",
- "signature": "3045022100ba599d6291aa1eb117ecfc4ba17283096c3f3303b8b96be81c1659c595c96bcd022054b80456ccac86c6885820f294ff331d49dd68fe09143d349007ce446b728d29"
+ "attest_data": "ff54434780180022000b3532b1fa343030bb892fdd9aff716d942c11c260f737217f878f5b41afbfe49300208ec265e6a21b13c946c70ee90b604c4aa7a742d4a334f1f2b62e6088a002cc820000000000012e05e8a5f3f0326229ef015603f9c083c09e9e00000001000c03ffffff0020e7b673b4da63320c89beeb83365815581f612467029d1fd34d1b12c8955cb6a6",
+ "signature": "3045022100f194cbac633c6b79ebe4581c562bc3a64bf954e5cb12cdd1c44b69c1360ca655022008366f88d10b6ba2a45006ae9a29324473cd79b333296c881974660f211deae1"
}
},
"nitro": {
- "public_key": "042417c2d23945e9e68c78c730102f951bf41f5042464d8272cd18e32d4b8ccfb5d4c3d4b93e361f7d8093f1f83c2396ad932a5c6bd238575e0de758a8c3a6ddc2",
- "nonce": "31373639393233313833",
- "document": "8444a1013822a05912e2bf696d6f64756c655f69647827692d30333061316632333366383334383639302d74706d3030303030303030303030303030303066646967657374665348413338346974696d657374616d701b0000019c17a4830f6d6e6974726f74706d5f70637273b8180058306e901b16932f6e036747d7a57696e4a2cc864008ebc016826ca1d7bd42ab5ac8286ccf49cde6c0284cbc4b63d978a2ec0158307e10323dec25050c22d2e91709373c930d194be275bde5c5e01331b9cc6ef81a9e2586991636652605bc3d322dbc7109025830518923b0f955d08da077c96aaba522b9decede61c599cea6c41889cfbea4ae4d50529d96fe4d1afdafb65e7f95bf23c4035830518923b0f955d08da077c96aaba522b9decede61c599cea6c41889cfbea4ae4d50529d96fe4d1afdafb65e7f95bf23c40458301a1f166e1995c1e1bfdf1c1d13b1ebc9d5d4e44397501655c911a2062d92063483ca9e17f0264efe336d4b61c5b98d1e055830b3ef9dfcbc8be38f0104cea80711252de3f4f8bf19a3f27126d303fb08ec979ba90780f06a742bf66f444f62e99cc1fc065830518923b0f955d08da077c96aaba522b9decede61c599cea6c41889cfbea4ae4d50529d96fe4d1afdafb65e7f95bf23c407583098441c7f7625d10058c47683aec486ce311c633235eb555593a7ee791121e3578ae72d04ecef661f272d59058b77af35085830056a862312c282d635a1e486812191efbf58c8c5533a89c3579b1809854bfa0090854e04b82acd93a752cbd32f1f38c10958304d5b0773776a7520553b4c08189632a76fe83024fa462035c649a0f62cd82f3951132c386488fd87d875ebf27552156d0a5830c1b534d2b043b50ae0fc0d415c4881d92f1633e6b318f967f8adb201eff718d080004fa7ce98b50f1bd8a808332fa1e20b58300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c58300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d58300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e58300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f5830000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000105830000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000115830ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff125830ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff135830ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff145830ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff155830ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff165830ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff17583022a3e580d7d0931f4365ebfae2618b84e3cce2c0afbf0ea1fbe40e645a43a3771ceb3e188394803642266f5512ee0eac6b63657274696669636174655902733082026f308201f5a0030201020204697ed9f7300a06082a8648ce3d04030330818e310b30090603550406130255533113301106035504080c0a57617368696e67746f6e3110300e06035504070c0753656174746c65310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533139303706035504030c30692d30333061316632333366383334383639302e75732d656173742d312e6177732e6e6974726f2d656e636c61766573301e170d3236303230313034343333325a170d3236303230313037343333355a308193310b30090603550406130255533113301106035504080c0a57617368696e67746f6e3110300e06035504070c0753656174746c65310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753313e303c06035504030c35692d30333061316632333366383334383639302d74706d303030303030303030303030303030302e75732d656173742d312e6177733076301006072a8648ce3d020106052b8104002203620004777cac8372ab736a2043f52369f0bdcc532e5e282c3695bfbeab6db877798b22bce1f8fd961c0c6ebad4bb6f6d09fabe2c13bf9020c52a70c10c6b07a761331cf90bd77f095cba83d682b2905ab58dca582e7cc21bc01cb4f9bf3fbd3efaf1e7a31d301b300c0603551d130101ff04023000300b0603551d0f0404030206c0300a06082a8648ce3d0403030368003065023100a32fefa84e976c7728110621dddb3d933ddbc47b598db144825daa4de7ffbbd2521846bdade93383e6b4d9fc2b81d512023014ebb021eb6888d1d440a2dde4479bb5d310e685137b272e96a376f5952fd0e98843117a84ed197938b8c1f985e73a4268636162756e646c65845902153082021130820196a003020102021100f93175681b90afe11d46ccb4e4e7f856300a06082a8648ce3d0403033049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c61766573301e170d3139313032383133323830355a170d3439313032383134323830355a3049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004fc0254eba608c1f36870e29ada90be46383292736e894bfff672d989444b5051e534a4b1f6dbe3c0bc581a32b7b176070ede12d69a3fea211b66e752cf7dd1dd095f6f1370f4170843d9dc100121e4cf63012809664487c9796284304dc53ff4a3423040300f0603551d130101ff040530030101ff301d0603551d0e041604149025b50dd90547e796c396fa729dcf99a9df4b96300e0603551d0f0101ff040403020186300a06082a8648ce3d0403030369003066023100a37f2f91a1c9bd5ee7b8627c1698d255038e1f0343f95b63a9628c3d39809545a11ebcbf2e3b55d8aeee71b4c3d6adf3023100a2f39b1605b27028a5dd4ba069b5016e65b4fbde8fe0061d6a53197f9cdaf5d943bc61fc2beb03cb6fee8d2302f3dff65902c1308202bd30820244a0030201020210578aa8f46a2b9e82304ad9173550c111300a06082a8648ce3d0403033049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c61766573301e170d3236303133303131303734365a170d3236303231393132303734365a3064310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533136303406035504030c2d373333363535643664363866376264662e75732d656173742d312e6177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004325b2791b548f694a7e17d44d65a326ed1659e23da8972a0acff960f820e548b1b0fb443927dcae15e27a815355ca62ab0e590a2b353a67495b6e10a949d02dddb0e8d8e6ec82efcfaab167c3517fad31de4add54e54e7869966b5a106a85604a381d53081d230120603551d130101ff040830060101ff020102301f0603551d230418301680149025b50dd90547e796c396fa729dcf99a9df4b96301d0603551d0e04160414f309d394ba73bc082274c84cfaa28d8c12ce95e7300e0603551d0f0101ff040403020186306c0603551d1f046530633061a05fa05d865b687474703a2f2f6177732d6e6974726f2d656e636c617665732d63726c2e73332e616d617a6f6e6177732e636f6d2f63726c2f61623439363063632d376436332d343262642d396539662d3539333338636236376638342e63726c300a06082a8648ce3d0403030367003064023076291fdcc1db1eff47f6ab41074f946e8c97f6045ec959cc119a577b8f5b02fdeaaff3ccc22559d8095f4cfd75d18afa02300d2cdee4fc10d95ef64217b06a45824958482666fedf04e30a433c7155b9e0108fa43b77a8c4750430798122ed8523ea590319308203153082029aa00302010202100d3d3e6da2d9da68bf96ed11663a0f35300a06082a8648ce3d0403033064310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533136303406035504030c2d373333363535643664363866376264662e75732d656173742d312e6177732e6e6974726f2d656e636c61766573301e170d3236303230313031323235395a170d3236303230363231323235385a308189313c303a06035504030c33343863666233333166346137616362612e7a6f6e616c2e75732d656173742d312e6177732e6e6974726f2d656e636c61766573310c300a060355040b0c03415753310f300d060355040a0c06416d617a6f6e310b3009060355040613025553310b300906035504080c0257413110300e06035504070c0753656174746c653076301006072a8648ce3d020106052b810400220362000458da1984ae792b97312a98fd03ff8db0d930b5d4be21541f749e4ef96e841433cc82dbe43745e3dd46461b4ceedbb2b47932ed734bc17e6745359e54193d5f93d7a1dba4f1e3009335828fa847942f89ec22a6dd60f00c2041ef7044884f5e60a381ea3081e730120603551d130101ff040830060101ff020101301f0603551d23041830168014f309d394ba73bc082274c84cfaa28d8c12ce95e7301d0603551d0e041604146ac22d82f5ed4065cbc35a114a1e635e06ea2907300e0603551d0f0101ff0404030201863081800603551d1f047930773075a073a071866f687474703a2f2f63726c2d75732d656173742d312d6177732d6e6974726f2d656e636c617665732e73332e75732d656173742d312e616d617a6f6e6177732e636f6d2f63726c2f32613533313133362d383232332d346633632d396335632d3335333832643564663634632e63726c300a06082a8648ce3d0403030369003066023100fad27828efde63ae193f5c8e60abd7c98b0ce3c662e3ba33687bba971f4b9986596052d2f95926787243c62e8f518b790231009da96f7c7ba61dea6acf137327762efc41843e5a4d140963af8d91e852baa04819714491effee90fd081d6803c710c915902c3308202bf30820245a0030201020215008567cee73e22cb55f083d4d4f881ff3313c7e2b1300a06082a8648ce3d040303308189313c303a06035504030c33343863666233333166346137616362612e7a6f6e616c2e75732d656173742d312e6177732e6e6974726f2d656e636c61766573310c300a060355040b0c03415753310f300d060355040a0c06416d617a6f6e310b3009060355040613025553310b300906035504080c0257413110300e06035504070c0753656174746c65301e170d3236303230313034343031395a170d3236303230323034343031395a30818e310b30090603550406130255533113301106035504080c0a57617368696e67746f6e3110300e06035504070c0753656174746c65310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533139303706035504030c30692d30333061316632333366383334383639302e75732d656173742d312e6177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004562f892fd88ffc2c4bc40bdbfecc211949e1ece2007d7ebc9e535141fad0fafb7c9569de8aaf44e7a02af6687da8d5cbb971f7025a0c2aeb5d6e88835a7dec9f2b231dce3d00492bebd8337aeb89c190a684085fb7c0da808176ad972828659ca366306430120603551d130101ff040830060101ff020100300e0603551d0f0101ff040403020204301d0603551d0e04160414fdea49e532ddc19976817c8e1b5ffcec437e283f301f0603551d230418301680146ac22d82f5ed4065cbc35a114a1e635e06ea2907300a06082a8648ce3d040303036800306502301d1d14a339eadd7624b1c492300d9a6f1c365b1351718ffcbffd19be21e07b41c2762f560e9fcbe6f33c2669493395a60231009abafe346e9e177cb8bb760126d1d86455f4755af9eeaee0e9f666b2aa2980a836f67bcb75b8663593385981f3f3bd2b6a7075626c69635f6b65795841042417c2d23945e9e68c78c730102f951bf41f5042464d8272cd18e32d4b8ccfb5d4c3d4b93e361f7d8093f1f83c2396ad932a5c6bd238575e0de758a8c3a6ddc269757365725f64617461f6656e6f6e63654a31373639393233313833ff58609c3e3c2a983cd0cc8699922808812043abd958ae697df07b115a77d19c355cf5b71eb58b3a71a0047ec53f854a8d323d98270751953380ddecd150c44f41ab53ffc99898601352bf9ac71a698e9cb181fcfd7d8d6f256ba001cf91cab5b7762b"
+ "document": "8444a1013822a05912f9bf696d6f64756c655f69647827692d30303832663839623033396339376130662d74706d3030303030303030303030303030303066646967657374665348413338346974696d657374616d701b0000019c1d40bedb6d6e6974726f74706d5f70637273b8180058306e901b16932f6e036747d7a57696e4a2cc864008ebc016826ca1d7bd42ab5ac8286ccf49cde6c0284cbc4b63d978a2ec015830a1f2689838a6903d15408efa219421f0513347cacd8087ad3a8cc68bd0bdc710a13c60abb3fa9d819bc2abaa7ab1dd35025830518923b0f955d08da077c96aaba522b9decede61c599cea6c41889cfbea4ae4d50529d96fe4d1afdafb65e7f95bf23c4035830518923b0f955d08da077c96aaba522b9decede61c599cea6c41889cfbea4ae4d50529d96fe4d1afdafb65e7f95bf23c40458301a1f166e1995c1e1bfdf1c1d13b1ebc9d5d4e44397501655c911a2062d92063483ca9e17f0264efe336d4b61c5b98d1e055830b3ef9dfcbc8be38f0104cea80711252de3f4f8bf19a3f27126d303fb08ec979ba90780f06a742bf66f444f62e99cc1fc065830518923b0f955d08da077c96aaba522b9decede61c599cea6c41889cfbea4ae4d50529d96fe4d1afdafb65e7f95bf23c407583098441c7f7625d10058c47683aec486ce311c633235eb555593a7ee791121e3578ae72d04ecef661f272d59058b77af35085830056a862312c282d635a1e486812191efbf58c8c5533a89c3579b1809854bfa0090854e04b82acd93a752cbd32f1f38c10958304d5b0773776a7520553b4c08189632a76fe83024fa462035c649a0f62cd82f3951132c386488fd87d875ebf27552156d0a58301b599d35451621d3d004a509acbd60736f0ac08178d9f86b0db09c4b088cafa2a747c92e81e7f524649824229e91574f0b58300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c58300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d58300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e58300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f5830000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000105830000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000115830ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff125830ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff135830ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff145830ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff155830ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff165830ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1758300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006b636572746966696361746559027430820270308201f5a00302010202046980521c300a06082a8648ce3d04030330818e310b30090603550406130255533113301106035504080c0a57617368696e67746f6e3110300e06035504070c0753656174746c65310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533139303706035504030c30692d30303832663839623033396339376130662e75732d656173742d312e6177732e6e6974726f2d656e636c61766573301e170d3236303230323037323832355a170d3236303230323130323832385a308193310b30090603550406130255533113301106035504080c0a57617368696e67746f6e3110300e06035504070c0753656174746c65310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753313e303c06035504030c35692d30303832663839623033396339376130662d74706d303030303030303030303030303030302e75732d656173742d312e6177733076301006072a8648ce3d020106052b8104002203620004261b133ce700847876aff414ce172ed9c7c97b01b158615ac5b352c5ce231d90fb54f3b0ccd1e8b6f16cadbab82905e52294f059a0cac070aca3f5cb043686aa19bab8f859c886d86b07028329eeb1b91027afe0af6a2ff9038a763cec1f69cea31d301b300c0603551d130101ff04023000300b0603551d0f0404030206c0300a06082a8648ce3d0403030369003066023100e00025d2bfb0a292221c5c9c243d87198ea05a44eddb72f05b672167ef57025b248d90c2565f14154d8e47146acd9113023100d8f08bea8ee81928ae1961af361139904f0939b0315dc31a41669e11b9d0a3171c58ddb9024dc1b7bc2e27f595bca7ca68636162756e646c65845902153082021130820196a003020102021100f93175681b90afe11d46ccb4e4e7f856300a06082a8648ce3d0403033049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c61766573301e170d3139313032383133323830355a170d3439313032383134323830355a3049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004fc0254eba608c1f36870e29ada90be46383292736e894bfff672d989444b5051e534a4b1f6dbe3c0bc581a32b7b176070ede12d69a3fea211b66e752cf7dd1dd095f6f1370f4170843d9dc100121e4cf63012809664487c9796284304dc53ff4a3423040300f0603551d130101ff040530030101ff301d0603551d0e041604149025b50dd90547e796c396fa729dcf99a9df4b96300e0603551d0f0101ff040403020186300a06082a8648ce3d0403030369003066023100a37f2f91a1c9bd5ee7b8627c1698d255038e1f0343f95b63a9628c3d39809545a11ebcbf2e3b55d8aeee71b4c3d6adf3023100a2f39b1605b27028a5dd4ba069b5016e65b4fbde8fe0061d6a53197f9cdaf5d943bc61fc2beb03cb6fee8d2302f3dff65902c1308202bd30820244a0030201020210578aa8f46a2b9e82304ad9173550c111300a06082a8648ce3d0403033049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c61766573301e170d3236303133303131303734365a170d3236303231393132303734365a3064310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533136303406035504030c2d373333363535643664363866376264662e75732d656173742d312e6177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004325b2791b548f694a7e17d44d65a326ed1659e23da8972a0acff960f820e548b1b0fb443927dcae15e27a815355ca62ab0e590a2b353a67495b6e10a949d02dddb0e8d8e6ec82efcfaab167c3517fad31de4add54e54e7869966b5a106a85604a381d53081d230120603551d130101ff040830060101ff020102301f0603551d230418301680149025b50dd90547e796c396fa729dcf99a9df4b96301d0603551d0e04160414f309d394ba73bc082274c84cfaa28d8c12ce95e7300e0603551d0f0101ff040403020186306c0603551d1f046530633061a05fa05d865b687474703a2f2f6177732d6e6974726f2d656e636c617665732d63726c2e73332e616d617a6f6e6177732e636f6d2f63726c2f61623439363063632d376436332d343262642d396539662d3539333338636236376638342e63726c300a06082a8648ce3d0403030367003064023076291fdcc1db1eff47f6ab41074f946e8c97f6045ec959cc119a577b8f5b02fdeaaff3ccc22559d8095f4cfd75d18afa02300d2cdee4fc10d95ef64217b06a45824958482666fedf04e30a433c7155b9e0108fa43b77a8c4750430798122ed8523ea590318308203143082029aa0030201020210658893dd097e7eb1548ccc71dfb6b62c300a06082a8648ce3d0403033064310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533136303406035504030c2d373333363535643664363866376264662e75732d656173742d312e6177732e6e6974726f2d656e636c61766573301e170d3236303230313233313330375a170d3236303230373231313330375a308189313c303a06035504030c33633061633332363062373265633931332e7a6f6e616c2e75732d656173742d312e6177732e6e6974726f2d656e636c61766573310c300a060355040b0c03415753310f300d060355040a0c06416d617a6f6e310b3009060355040613025553310b300906035504080c0257413110300e06035504070c0753656174746c653076301006072a8648ce3d020106052b81040022036200043de15124bce397da8d9576d0e31c889410f3799062c0b82e5586ee588f2abc9fd7eaec88753f3335f4fb383e78e5ec7ca50f00f0b7aeb5e9c5f337d509b41027e09553c04af6d9412846f7a917019171a886c6cb459028049c4833bfeb85e1d1a381ea3081e730120603551d130101ff040830060101ff020101301f0603551d23041830168014f309d394ba73bc082274c84cfaa28d8c12ce95e7301d0603551d0e0416041415b11644aa01b9be586b880c975602c098d5afd5300e0603551d0f0101ff0404030201863081800603551d1f047930773075a073a071866f687474703a2f2f63726c2d75732d656173742d312d6177732d6e6974726f2d656e636c617665732e73332e75732d656173742d312e616d617a6f6e6177732e636f6d2f63726c2f32613533313133362d383232332d346633632d396335632d3335333832643564663634632e63726c300a06082a8648ce3d040303036800306502300ad87a6166ae1cf6df0c42ca3546d7e082040013791f111fa673de11739fafa6fcb1c3aca66b3c3663b7af93a871156e0231008cfa51ba02bbb1719d2d90c3145bbc38184f36ad1b36101c2b8e8d426356aa40053dbed7ab2b10b87d95bcbf8fb1dc605902c3308202bf30820244a00302010202146a8c4318e17ae779b7ae4f07b076a6c855a90012300a06082a8648ce3d040303308189313c303a06035504030c33633061633332363062373265633931332e7a6f6e616c2e75732d656173742d312e6177732e6e6974726f2d656e636c61766573310c300a060355040b0c03415753310f300d060355040a0c06416d617a6f6e310b3009060355040613025553310b300906035504080c0257413110300e06035504070c0753656174746c65301e170d3236303230323037323730385a170d3236303230333037323730385a30818e310b30090603550406130255533113301106035504080c0a57617368696e67746f6e3110300e06035504070c0753656174746c65310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533139303706035504030c30692d30303832663839623033396339376130662e75732d656173742d312e6177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004899e286bf42f10a4a24d829b87d1413ee847153b42c4817e1395176ac4e8f511b9ffd2c2ec7197e986f2064922e13330363180894b709dff45283ca49e0836edc132a4c6f9f15ab799a86a1836b3dc998405ae594661c63ce56b0437b1c191fda366306430120603551d130101ff040830060101ff020100300e0603551d0f0101ff040403020204301d0603551d0e04160414ab7c94dac955674d240f2ee57afeda038bafd25f301f0603551d2304183016801415b11644aa01b9be586b880c975602c098d5afd5300a06082a8648ce3d04030303690030660231008b51580cc62f5251ba209974eba75952b6adfdb7129332acf4cc42e5b1b74f78f3b9a7e2b559247462ccef567474543f023100aacbd8397b02f7233097291bb45ee07d2417f9cd9c6b398a40c21edb713b0de373a02af16c5be443f87cfe4377895e546a7075626c69635f6b6579584104866469563d68b071c23eef841e1e435b90d6fb3dafd408c683889b2b5017adfe5e759fc168fca05fa196d6d02004a3dbcd0f411033db54866695f46109d18eb769757365725f64617461f6656e6f6e636558208ec265e6a21b13c946c70ee90b604c4aa7a742d4a334f1f2b62e6088a002cc82ff5860d4189cd7f75a97ba00a0508ec3871839aeba564252176e66dfbc773db6279f0d6c04c1add3a93de3811c36a02d158f7e74f48d2013de02d4cb6a5bfa8fd4f86e3b89eee385c7d88ab416001a30cd81c7f7bc5c0507f54bf23b1c90d10424578e"
}
}
}
From f9e8c2986c00e5a9370d9941d111d673ffe80d27 Mon Sep 17 00:00:00 2001
From: user <303926+HarryR@users.noreply.github.com>
Date: Tue, 3 Feb 2026 18:34:24 +0800
Subject: [PATCH 2/2] Fixed certificate parsing (remove custom byte handling) +
restrict Nitro AK (same as GCP AK)
---
Makefile | 3 +
README.md | 13 +-
crates/vaportpm-attest/AWS-NITRO.md | 7 +-
crates/vaportpm-attest/Cargo.toml | 4 +
crates/vaportpm-attest/GCP.md | 43 ++++-
crates/vaportpm-attest/README.md | 6 +-
crates/vaportpm-attest/src/a9n.rs | 13 +-
crates/vaportpm-attest/src/bin/attest.rs | 66 +++++--
crates/vaportpm-attest/src/cert.rs | 164 +++++++-----------
crates/vaportpm-attest/src/ek.rs | 92 +++++++++-
crates/vaportpm-verify/Cargo.toml | 4 +
crates/vaportpm-verify/README.md | 4 +-
crates/vaportpm-verify/src/lib.rs | 5 +-
.../vaportpm-verify/test-nitro-fixture.json | 16 +-
14 files changed, 286 insertions(+), 154 deletions(-)
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 036db09..8f0327e 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,7 @@ You handle **policy decisions**:
| Platform | Status | Trust Anchor |
|----------|--------|--------------|
| AWS EC2 with Nitro v4+ | β
Working | Nitro Root CA |
-| GCP Confidential VM | β
Working | Google AK certificate |
+| 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)
@@ -47,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.provider);
- 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 17f8bc9..7ceb51e 100644
--- a/crates/vaportpm-attest/AWS-NITRO.md
+++ b/crates/vaportpm-attest/AWS-NITRO.md
@@ -44,10 +44,11 @@ This ensures a single, verifiable path from AWS hardware to the attested data.
let is_nitro = tpm.is_nitro_tpm()?;
let pcr_values = tpm.read_all_allocated_pcrs()?; // Reads SHA-384 bank
-// 2. Create long-term AK (no PCR binding - trust via Nitro document)
-let ak = tpm.create_primary_ecc_key(TPM_RH_OWNER)?;
+// 2. Create restricted AK in endorsement hierarchy (TCG-compliant AK profile)
+let ak = tpm.create_restricted_ak(TPM_RH_ENDORSEMENT)?;
// 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. Get Nitro attestation binding the AK public key
@@ -181,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/Cargo.toml b/crates/vaportpm-attest/Cargo.toml
index 626632f..c3e88f6 100644
--- a/crates/vaportpm-attest/Cargo.toml
+++ b/crates/vaportpm-attest/Cargo.toml
@@ -25,3 +25,7 @@ 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
index 67951db..8186a6d 100644
--- a/crates/vaportpm-attest/GCP.md
+++ b/crates/vaportpm-attest/GCP.md
@@ -56,6 +56,36 @@ EK/AK CA Root (self-signed, offline)
- **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:
@@ -89,7 +119,7 @@ TPM2B_ATTEST {
magic: 0xFF544347 ("TCG\xFF")
type: TPM_ST_ATTEST_QUOTE (0x8018)
qualifiedSigner: Hash of signing key name
- extraData: Nonce (challenge from verifier)
+ extraData: Nonce passed to attest()
clockInfo: TPM clock values
firmwareVersion: TPM firmware version
attested: TPMS_QUOTE_INFO {
@@ -99,7 +129,12 @@ TPM2B_ATTEST {
}
```
-The signature is ECDSA over the DER-encoded attest structure.
+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
@@ -133,7 +168,7 @@ The signature is ECDSA over the DER-encoded attest structure.
- 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 proves freshness (replay protection)
+- The nonce can prove freshness if verifier-provided (replay protection)
### What This Does NOT Validate
@@ -153,7 +188,7 @@ The signature is ECDSA over the DER-encoded attest structure.
|--------|---------------------|-----------|
| 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 + TPM2_Quote |
+| PCR Source | TPM2_Quote | Nitro document (nitrotpm_pcrs) |
| Algorithm | ECDSA P-256 | ECDSA P-384 |
## References
diff --git a/crates/vaportpm-attest/README.md b/crates/vaportpm-attest/README.md
index 3a19dc4..66fa97e 100644
--- a/crates/vaportpm-attest/README.md
+++ b/crates/vaportpm-attest/README.md
@@ -5,8 +5,8 @@ Produces a self-contained attestation document from a cloud vTPM. The output can
```rust
use vaportpm_attest::attest;
-let output = attest(nonce, user_data)?;
-// output.to_json() -> send to verifier
+let json = attest(nonce)?;
+// Send json to verifier
```
The library auto-detects the cloud platform (AWS Nitro, GCP Confidential VM) and produces a JSON document containing:
@@ -192,7 +192,7 @@ Supports multiple PCR banks:
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:
- **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 Shielded VM**: The AK has a certificate stored in TPM NV RAM, signed by Google's CA chain.
+- **GCP Confidential VM**: The AK has a certificate stored in TPM NV RAM, signed by Google's CA chain.
Both paths produce a TPM2_Quote (signed PCR values + nonce) that can be verified against the platform's root of trust.
diff --git a/crates/vaportpm-attest/src/a9n.rs b/crates/vaportpm-attest/src/a9n.rs
index e3a438d..c077dfb 100644
--- a/crates/vaportpm-attest/src/a9n.rs
+++ b/crates/vaportpm-attest/src/a9n.rs
@@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
use crate::cert::{der_to_pem, fetch_cert_chain, DER_SEQUENCE_LONG};
-use crate::{KeyOps, NsmOps, NvOps, PcrOps, PublicKey, Tpm, TPM_RH_ENDORSEMENT, TPM_RH_OWNER};
+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;
@@ -232,18 +232,19 @@ pub fn attest(nonce: &[u8]) -> Result {
Ok(json)
}
-/// Nitro attestation path: create long-term AK and use TPM2_Quote
+/// Nitro attestation path: create restricted AK and use TPM2_Quote
///
-/// Creates an AK without PCR binding (long-term key), then uses TPM2_Quote
-/// to sign the PCR values. The AK is bound to the Nitro NSM document instead.
+/// 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 long-term AK (no PCR binding - trust comes from Nitro NSM document)
- let signing_key = tpm.create_primary_ecc_key(TPM_RH_OWNER)?;
+ // 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(
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