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----- +MIIGATCCA+mgAwIBAgIUAKZdpPnjKPOANcOnPU9yQyvfFdwwDQYJKoZIhvcNAQEL +BQAwfjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcT +DU1vdW50YWluIFZpZXcxEzARBgNVBAoTCkdvb2dsZSBMTEMxFTATBgNVBAsTDEdv +b2dsZSBDbG91ZDEWMBQGA1UEAxMNRUsvQUsgQ0EgUm9vdDAgFw0yMjA3MDgwMDQw +MzRaGA8yMTIyMDcwODA1NTcyM1owfjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNh +bGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEzARBgNVBAoTCkdvb2ds +ZSBMTEMxFTATBgNVBAsTDEdvb2dsZSBDbG91ZDEWMBQGA1UEAxMNRUsvQUsgQ0Eg +Um9vdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJ0l9VCoyJZLSol8 +KyhNpbS7pBnuicE6ptrdtxAWIR2TnLxSgxNFiR7drtofxI0ruceoCIpsa9NHIKrz +3sM/N/E8mFNHiJAuyVf3pPpmDpLJZQ1qe8yHkpGSs3Kj3s5YYWtEecCVfzNs4MtK +vGfA+WKB49A6Noi8R9R1GonLIN6wSXX3kP1ibRn0NGgdqgfgRe5HC3kKAhjZ6scT +8Eb1SGlaByGzE5WoGTnNbyifkyx9oUZxXVJsqv2q611W3apbPxcgev8z5JXQUbrr +Q7EbO0StK1DsKRsKLuD+YLxjrBRQ4UeIN5WHp6G0vgYiOptHm6YKZxQemO/kVMLR +zsm1AYH7eNOFekcBIKRjSqpk5m4ud04qum6f0hBj3iE/Pe+DvIbVhLh9ItAunISG +QPA9dYEgfA/qWir+pU7LV3phpLeGhull8G/zYmQhF3heg0buIR70aavzT8iLAQrx +VMNRZJEGMwIN/tq8YiT3+3EZIcSqq6GAGjiuVw3NIsXC3+CuSJGQ5GbDp49Lc6VW +PHeWeFvwSUGgxKXq5r1+PRsoYgK6S4hhecgXEX5c7Rta6TcFlEFb0XK9fpy1dr89 +LeFGxUBpdDvKxDRLMm3FQen8rmR/PSReEcJsaqbUP/q7Pc7k0RfF9Mb6AfPZfnqg +pYJQ+IFSr9EjRSW1wPcL03zoTP47AgMBAAGjdTBzMA4GA1UdDwEB/wQEAwIBBjAQ +BgNVHSUECTAHBgVngQUIATAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRJ50pb +Vin1nXm3pjA8A7KP5xTdTDAfBgNVHSMEGDAWgBRJ50pbVin1nXm3pjA8A7KP5xTd +TDANBgkqhkiG9w0BAQsFAAOCAgEAlfHRvOB3CJoLTl1YG/AvjGoZkpNMyp5X5je1 +ICCQ68b296En9hIUlcYY/+nuEPSPUjDA3izwJ8DAfV4REgpQzqoh6XhR3TgyfHXj +J6DC7puzEgtzF1+wHShUpBoe3HKuL4WhB3rvwk2SEsudBu92o9BuBjcDJ/GW5GRt +pD/H71HAE8rI9jJ41nS0FvkkjaX0glsntMVUXiwcta8GI0QOE2ijsJBwk41uQGt0 +YOj2SGlEwNAC5DBTB5kZ7+6X9xGE6/c+M3TAA0ONoX18rNfif94cCx/mPYOs8pUk +ANRAQ4aTRBvpBrryGT8R1ahTBkMeRQG3tdsLHRT8fJCFUANd5WLWsi83005y/WuM +z8/gFKc0PL+F+MubCsJ1ODPTRscH93QlS4zEMg5hDAIks+fDoRJ2QiROqo7GAqbT +c7STKfGcr9+pa63na7f3oy1sZPWPdxB8tx5z3lghiPP3ktQx/yK/1Fwf1hgxJHFy +/2UcaGuOXRRRTPyEnppZp82Kigs9aPHWtaVm2/LrXX2fvT9iM/k0CovNAj8rztHx +sUEoA0xJnSOJNPpe9PRdjsTj7/u3Xu6hQLNNidBHgI3Hcmi704HMMd/3yZ424OOr +S32ylpeU1oeQHFrLE6hYX4/ttMETbmESIKd2rTgstPotSvkuB5TljbKYPR+lq7hQ +av16U4E= +-----END CERTIFICATE----- diff --git a/crates/vaportpm-attest/certs/gcp-ekak-root.txt b/crates/vaportpm-attest/certs/gcp-ekak-root.txt new file mode 100644 index 0000000..f3a2554 --- /dev/null +++ b/crates/vaportpm-attest/certs/gcp-ekak-root.txt @@ -0,0 +1,93 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + a6:5d:a4:f9:e3:28:f3:80:35:c3:a7:3d:4f:72:43:2b:df:15:dc + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=US, ST=California, L=Mountain View, O=Google LLC, OU=Google Cloud, CN=EK/AK CA Root + Validity + Not Before: Jul 8 00:40:34 2022 GMT + Not After : Jul 8 05:57:23 2122 GMT + Subject: C=US, ST=California, L=Mountain View, O=Google LLC, OU=Google Cloud, CN=EK/AK CA Root + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (4096 bit) + Modulus: + 00:9d:25:f5:50:a8:c8:96:4b:4a:89:7c:2b:28:4d: + a5:b4:bb:a4:19:ee:89:c1:3a:a6:da:dd:b7:10:16: + 21:1d:93:9c:bc:52:83:13:45:89:1e:dd:ae:da:1f: + c4:8d:2b:b9:c7:a8:08:8a:6c:6b:d3:47:20:aa:f3: + de:c3:3f:37:f1:3c:98:53:47:88:90:2e:c9:57:f7: + a4:fa:66:0e:92:c9:65:0d:6a:7b:cc:87:92:91:92: + b3:72:a3:de:ce:58:61:6b:44:79:c0:95:7f:33:6c: + e0:cb:4a:bc:67:c0:f9:62:81:e3:d0:3a:36:88:bc: + 47:d4:75:1a:89:cb:20:de:b0:49:75:f7:90:fd:62: + 6d:19:f4:34:68:1d:aa:07:e0:45:ee:47:0b:79:0a: + 02:18:d9:ea:c7:13:f0:46:f5:48:69:5a:07:21:b3: + 13:95:a8:19:39:cd:6f:28:9f:93:2c:7d:a1:46:71: + 5d:52:6c:aa:fd:aa:eb:5d:56:dd:aa:5b:3f:17:20: + 7a:ff:33:e4:95:d0:51:ba:eb:43:b1:1b:3b:44:ad: + 2b:50:ec:29:1b:0a:2e:e0:fe:60:bc:63:ac:14:50: + e1:47:88:37:95:87:a7:a1:b4:be:06:22:3a:9b:47: + 9b:a6:0a:67:14:1e:98:ef:e4:54:c2:d1:ce:c9:b5: + 01:81:fb:78:d3:85:7a:47:01:20:a4:63:4a:aa:64: + e6:6e:2e:77:4e:2a:ba:6e:9f:d2:10:63:de:21:3f: + 3d:ef:83:bc:86:d5:84:b8:7d:22:d0:2e:9c:84:86: + 40:f0:3d:75:81:20:7c:0f:ea:5a:2a:fe:a5:4e:cb: + 57:7a:61:a4:b7:86:86:e9:65:f0:6f:f3:62:64:21: + 17:78:5e:83:46:ee:21:1e:f4:69:ab:f3:4f:c8:8b: + 01:0a:f1:54:c3:51:64:91:06:33:02:0d:fe:da:bc: + 62:24:f7:fb:71:19:21:c4:aa:ab:a1:80:1a:38:ae: + 57:0d:cd:22:c5:c2:df:e0:ae:48:91:90:e4:66:c3: + a7:8f:4b:73:a5:56:3c:77:96:78:5b:f0:49:41:a0: + c4:a5:ea:e6:bd:7e:3d:1b:28:62:02:ba:4b:88:61: + 79:c8:17:11:7e:5c:ed:1b:5a:e9:37:05:94:41:5b: + d1:72:bd:7e:9c:b5:76:bf:3d:2d:e1:46:c5:40:69: + 74:3b:ca:c4:34:4b:32:6d:c5:41:e9:fc:ae:64:7f: + 3d:24:5e:11:c2:6c:6a:a6:d4:3f:fa:bb:3d:ce:e4: + d1:17:c5:f4:c6:fa:01:f3:d9:7e:7a:a0:a5:82:50: + f8:81:52:af:d1:23:45:25:b5:c0:f7:0b:d3:7c:e8: + 4c:fe:3b + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Key Usage: critical + Certificate Sign, CRL Sign + X509v3 Extended Key Usage: + Endorsement Key Certificate + X509v3 Basic Constraints: critical + CA:TRUE + X509v3 Subject Key Identifier: + 49:E7:4A:5B:56:29:F5:9D:79:B7:A6:30:3C:03:B2:8F:E7:14:DD:4C + X509v3 Authority Key Identifier: + 49:E7:4A:5B:56:29:F5:9D:79:B7:A6:30:3C:03:B2:8F:E7:14:DD:4C + Signature Algorithm: sha256WithRSAEncryption + Signature Value: + 95:f1:d1:bc:e0:77:08:9a:0b:4e:5d:58:1b:f0:2f:8c:6a:19: + 92:93:4c:ca:9e:57:e6:37:b5:20:20:90:eb:c6:f6:f7:a1:27: + f6:12:14:95:c6:18:ff:e9:ee:10:f4:8f:52:30:c0:de:2c:f0: + 27:c0:c0:7d:5e:11:12:0a:50:ce:aa:21:e9:78:51:dd:38:32: + 7c:75:e3:27:a0:c2:ee:9b:b3:12:0b:73:17:5f:b0:1d:28:54: + a4:1a:1e:dc:72:ae:2f:85:a1:07:7a:ef:c2:4d:92:12:cb:9d: + 06:ef:76:a3:d0:6e:06:37:03:27:f1:96:e4:64:6d:a4:3f:c7: + ef:51:c0:13:ca:c8:f6:32:78:d6:74:b4:16:f9:24:8d:a5:f4: + 82:5b:27:b4:c5:54:5e:2c:1c:b5:af:06:23:44:0e:13:68:a3: + b0:90:70:93:8d:6e:40:6b:74:60:e8:f6:48:69:44:c0:d0:02: + e4:30:53:07:99:19:ef:ee:97:f7:11:84:eb:f7:3e:33:74:c0: + 03:43:8d:a1:7d:7c:ac:d7:e2:7f:de:1c:0b:1f:e6:3d:83:ac: + f2:95:24:00:d4:40:43:86:93:44:1b:e9:06:ba:f2:19:3f:11: + d5:a8:53:06:43:1e:45:01:b7:b5:db:0b:1d:14:fc:7c:90:85: + 50:03:5d:e5:62:d6:b2:2f:37:d3:4e:72:fd:6b:8c:cf:cf:e0: + 14:a7:34:3c:bf:85:f8:cb:9b:0a:c2:75:38:33:d3:46:c7:07: + f7:74:25:4b:8c:c4:32:0e:61:0c:02:24:b3:e7:c3:a1:12:76: + 42:24:4e:aa:8e:c6:02:a6:d3:73:b4:93:29:f1:9c:af:df:a9: + 6b:ad:e7:6b:b7:f7:a3:2d:6c:64:f5:8f:77:10:7c:b7:1e:73: + de:58:21:88:f3:f7:92:d4:31:ff:22:bf:d4:5c:1f:d6:18:31: + 24:71:72:ff:65:1c:68:6b:8e:5d:14:51:4c:fc:84:9e:9a:59: + a7:cd:8a:8a:0b:3d:68:f1:d6:b5:a5:66:db:f2:eb:5d:7d:9f: + bd:3f:62:33:f9:34:0a:8b:cd:02:3f:2b:ce:d1:f1:b1:41:28: + 03:4c:49:9d:23:89:34:fa:5e:f4:f4:5d:8e:c4:e3:ef:fb:b7: + 5e:ee:a1:40:b3:4d:89:d0:47:80:8d:c7:72:68:bb:d3:81:cc: + 31:df:f7:c9:9e:36:e0:e3:ab:4b:7d:b2:96:97:94:d6:87:90: + 1c:5a:cb:13:a8:58:5f:8f:ed:b4:c1:13:6e:61:12:20:a7:76: + ad:38:2c:b4:fa:2d:4a:f9:2e:07:94:e5:8d:b2:98:3d:1f:a5: + ab:b8:50:6a:fd:7a:53:81 diff --git a/crates/vaportpm-attest/certs/pem2txt.sh b/crates/vaportpm-attest/certs/pem2txt.sh new file mode 100755 index 0000000..534222e --- /dev/null +++ b/crates/vaportpm-attest/certs/pem2txt.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# Convert all .pem certificates to human-readable .txt files +# Usage: ./pem2txt.sh + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +for pem in "$SCRIPT_DIR"/*.pem; do + [ -f "$pem" ] || continue + txt="${pem%.pem}.txt" + echo "Converting: $(basename "$pem") -> $(basename "$txt")" + openssl x509 -in "$pem" -text -noout > "$txt" +done + +echo "Done." diff --git a/crates/vaportpm-attest/src/a9n.rs b/crates/vaportpm-attest/src/a9n.rs index 727fbfb..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 = "MIIHIjCCBQqgAwIBAgITaI//DbE0ilSC7SjJnNGPwAKK+jANBgkqhkiG9w0BAQsFADB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzETMBEGA1UEChMKR29vZ2xlIExMQzEVMBMGA1UECxMMR29vZ2xlIENsb3VkMRYwFAYDVQQDEw1FSy9BSyBDQSBSb290MCAXDTI0MDIyMjIxNDQxNVoYDzIxMjIwNzA4MDU1NzIzWjCBhjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEzARBgNVBAoTCkdvb2dsZSBMTEMxFTATBgNVBAsTDEdvb2dsZSBDbG91ZDEeMBwGA1UEAxMVRUsvQUsgQ0EgSW50ZXJtZWRpYXRlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2FCgrrj91CGYC7saslxTDswjGS5oPfBnEwjrZMRUOkqA/mcxe1o1svsMllMtNjOd3MSJkRwEUuwnX707XhNSBE64y2JSEotF43l/Vq+PeBlZXvJHa0JhDh8DU6c4heLd2XOVYs6fV2bAZv+SOuFmmiPt753TAcljNFeMZIaQ4gXEjLZodvBU/D09UUf92trSihuKZWGpjtRuT2ep+C4x4PL4XQlfmYY8H5V5BqBO2vFyHpctFxlrNxCMjG6TQlX07zZO9sQADFl1hwJkS9BoYaXCUdAewrweElkIe9P9P1MkwQhUU8dlAJrYOizZ1drCII4TzWf69mGe9F/cIxEfdu7ZPqeH5fJB3tahGZiP+TYK6Ey+2uTvDp7xO9GwTdMTckpamU4oOnXufUKIdYohUuwy0vjb2D4VtWSVjgcL7aL93zaLyosHfSgsZoQs8FjPKzWVqFAq9RsTR3mkk891aC5drMR6lb1wkxfqPy9rS56Iom/cnATHxMlAysEvlUTzMd9nB3dOVaY1DINzuv/ZohRwkoIVFFxO+LjvhJGBkAGEWw36bNzV4slsfG9g2+o76IluoDZmgAmaDKLvZvNu0aBrfBXZA3zWYFbnHnzijGN+XyLD7vJ0cd9SQ+Z6JOhQV3YSXgcqH5xSl0qcs/nYpghqvtIa/TLE2NgsM28vZn0CAwEAAaOCAYwwggGIMA4GA1UdDwEB/wQEAwIBBjAQBgNVHSUECTAHBgVngQUIATAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRnw7veWOPWUXaPsxo+2wen7JN65DAfBgNVHSMEGDAWgBRJ50pbVin1nXm3pjA8A7KP5xTdTDCBjQYIKwYBBQUHAQEEgYAwfjB8BggrBgEFBQcwAoZwaHR0cDovL3ByaXZhdGVjYS1jb250ZW50LTYyZDcxNzczLTAwMDAtMjFkYS04NTJlLWY0ZjVlODBkNzc3OC5zdG9yYWdlLmdvb2dsZWFwaXMuY29tLzAzMmJmOWQzOWRiNGZhMDZhYWRlL2NhLmNydDCBggYDVR0fBHsweTB3oHWgc4ZxaHR0cDovL3ByaXZhdGVjYS1jb250ZW50LTYyZDcxNzczLTAwMDAtMjFkYS04NTJlLWY0ZjVlODBkNzc3OC5zdG9yYWdlLmdvb2dsZWFwaXMuY29tLzAzMmJmOWQzOWRiNGZhMDZhYWRlL2NybC5jcmwwDQYJKoZIhvcNAQELBQADggIBAG11IpWVb2PSB7jxDYYKlvYIyW4w5xI3DR/vblykiCHugY9lx2ukCXd9s6UV7gbpb5J1yysin0gkXa5FudKUl4DHb9O3rT5BaAaawz/iuvoVDZBlOfeMg9sCCbZf0apMTVG4b03VIUL6LRZ9DljipLJN78+/s0OHWS/xEEqw/Z8pwg9MID3kEU7iBxVtIKCoOQW+ENtmfaPTNLFORbBxeUvKOgslTlC2NrfawPB/YP+rTB6EwZthhzeQ3oW/MvFzorr5LWBEjNY+wreYR7bu1x2qbegUAb83qnOtKktU0pREI/cX5Jfv9Bgt9u2Z532BneMXwMrGda6LHdTmjG1AkfB7SgSZDkg0dkvTEKpclGg/bRjFQRGYtKLhvMlZmj7ag54dqp01KLS33ujDSSI3QmS2MFArqxt/jQQJ/w3iuwcFi+BUm848fOdmSgOrufo/l0BaQuj7plVT0W2JUsaBkSw56YGOET8Dw7im2Z87bu3EvVMPuIcK15gQlYrrObDp8KRijqSxqQ5kBFUp1kArq0vBLqBdvyjWIQ/n104nxkp990d5RR9RURTMadDHCqHXGADRDXC0J8Zyqp2IarLFITqAotM8fCRaEuihHSVuAxYBMuMCDIf+Ps7ZHbfJOTjw5QuUF+VTPL1yAb7eJHbIUczCgt7o5Rqh2evH3j4IQ1VD"; + 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 = "MIIHIjCCBQqgAwIBAgITaI//DbE0ilSC7SjJnNGPwAKK+jANBgkqhkiG9w0BAQsFADB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzETMBEGA1UEChMKR29vZ2xlIExMQzEVMBMGA1UECxMMR29vZ2xlIENsb3VkMRYwFAYDVQQDEw1FSy9BSyBDQSBSb290MCAXDTI0MDIyMjIxNDQxNVoYDzIxMjIwNzA4MDU1NzIzWjCBhjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEzARBgNVBAoTCkdvb2dsZSBMTEMxFTATBgNVBAsTDEdvb2dsZSBDbG91ZDEeMBwGA1UEAxMVRUsvQUsgQ0EgSW50ZXJtZWRpYXRlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2FCgrrj91CGYC7saslxTDswjGS5oPfBnEwjrZMRUOkqA/mcxe1o1svsMllMtNjOd3MSJkRwEUuwnX707XhNSBE64y2JSEotF43l/Vq+PeBlZXvJHa0JhDh8DU6c4heLd2XOVYs6fV2bAZv+SOuFmmiPt753TAcljNFeMZIaQ4gXEjLZodvBU/D09UUf92trSihuKZWGpjtRuT2ep+C4x4PL4XQlfmYY8H5V5BqBO2vFyHpctFxlrNxCMjG6TQlX07zZO9sQADFl1hwJkS9BoYaXCUdAewrweElkIe9P9P1MkwQhUU8dlAJrYOizZ1drCII4TzWf69mGe9F/cIxEfdu7ZPqeH5fJB3tahGZiP+TYK6Ey+2uTvDp7xO9GwTdMTckpamU4oOnXufUKIdYohUuwy0vjb2D4VtWSVjgcL7aL93zaLyosHfSgsZoQs8FjPKzWVqFAq9RsTR3mkk891aC5drMR6lb1wkxfqPy9rS56Iom/cnATHxMlAysEvlUTzMd9nB3dOVaY1DINzuv/ZohRwkoIVFFxO+LjvhJGBkAGEWw36bNzV4slsfG9g2+o76IluoDZmgAmaDKLvZvNu0aBrfBXZA3zWYFbnHnzijGN+XyLD7vJ0cd9SQ+Z6JOhQV3YSXgcqH5xSl0qcs/nYpghqvtIa/TLE2NgsM28vZn0CAwEAAaOCAYwwggGIMA4GA1UdDwEB/wQEAwIBBjAQBgNVHSUECTAHBgVngQUIATAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRnw7veWOPWUXaPsxo+2wen7JN65DAfBgNVHSMEGDAWgBRJ50pbVin1nXm3pjA8A7KP5xTdTDCBjQYIKwYBBQUHAQEEgYAwfjB8BggrBgEFBQcwAoZwaHR0cDovL3ByaXZhdGVjYS1jb250ZW50LTYyZDcxNzczLTAwMDAtMjFkYS04NTJlLWY0ZjVlODBkNzc3OC5zdG9yYWdlLmdvb2dsZWFwaXMuY29tLzAzMmJmOWQzOWRiNGZhMDZhYWRlL2NhLmNydDCBggYDVR0fBHsweTB3oHWgc4ZxaHR0cDovL3ByaXZhdGVjYS1jb250ZW50LTYyZDcxNzczLTAwMDAtMjFkYS04NTJlLWY0ZjVlODBkNzc3OC5zdG9yYWdlLmdvb2dsZWFwaXMuY29tLzAzMmJmOWQzOWRiNGZhMDZhYWRlL2NybC5jcmwwDQYJKoZIhvcNAQELBQADggIBAG11IpWVb2PSB7jxDYYKlvYIyW4w5xI3DR/vblykiCHugY9lx2ukCXd9s6UV7gbpb5J1yysin0gkXa5FudKUl4DHb9O3rT5BaAaawz/iuvoVDZBlOfeMg9sCCbZf0apMTVG4b03VIUL6LRZ9DljipLJN78+/s0OHWS/xEEqw/Z8pwg9MID3kEU7iBxVtIKCoOQW+ENtmfaPTNLFORbBxeUvKOgslTlC2NrfawPB/YP+rTB6EwZthhzeQ3oW/MvFzorr5LWBEjNY+wreYR7bu1x2qbegUAb83qnOtKktU0pREI/cX5Jfv9Bgt9u2Z532BneMXwMrGda6LHdTmjG1AkfB7SgSZDkg0dkvTEKpclGg/bRjFQRGYtKLhvMlZmj7ag54dqp01KLS33ujDSSI3QmS2MFArqxt/jQQJ/w3iuwcFi+BUm848fOdmSgOrufo/l0BaQuj7plVT0W2JUsaBkSw56YGOET8Dw7im2Z87bu3EvVMPuIcK15gQlYrrObDp8KRijqSxqQ5kBFUp1kArq0vBLqBdvyjWIQ/n104nxkp990d5RR9RURTMadDHCqHXGADRDXC0J8Zyqp2IarLFITqAotM8fCRaEuihHSVuAxYBMuMCDIf+Ps7ZHbfJOTjw5QuUF+VTPL1yAb7eJHbIUczCgt7o5Rqh2evH3j4IQ1VD"; + 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-----\nMIIGATCCA+mgAwIBAgIUAKZdpPnjKPOANcOnPU9yQyvfFdwwDQYJKoZIhvcNAQEL\nBQAwfjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcT\nDU1vdW50YWluIFZpZXcxEzARBgNVBAoTCkdvb2dsZSBMTEMxFTATBgNVBAsTDEdv\nb2dsZSBDbG91ZDEWMBQGA1UEAxMNRUsvQUsgQ0EgUm9vdDAgFw0yMjA3MDgwMDQw\nMzRaGA8yMTIyMDcwODA1NTcyM1owfjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNh\nbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEzARBgNVBAoTCkdvb2ds\nZSBMTEMxFTATBgNVBAsTDEdvb2dsZSBDbG91ZDEWMBQGA1UEAxMNRUsvQUsgQ0Eg\nUm9vdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJ0l9VCoyJZLSol8\nKyhNpbS7pBnuicE6ptrdtxAWIR2TnLxSgxNFiR7drtofxI0ruceoCIpsa9NHIKrz\n3sM/N/E8mFNHiJAuyVf3pPpmDpLJZQ1qe8yHkpGSs3Kj3s5YYWtEecCVfzNs4MtK\nvGfA+WKB49A6Noi8R9R1GonLIN6wSXX3kP1ibRn0NGgdqgfgRe5HC3kKAhjZ6scT\n8Eb1SGlaByGzE5WoGTnNbyifkyx9oUZxXVJsqv2q611W3apbPxcgev8z5JXQUbrr\nQ7EbO0StK1DsKRsKLuD+YLxjrBRQ4UeIN5WHp6G0vgYiOptHm6YKZxQemO/kVMLR\nzsm1AYH7eNOFekcBIKRjSqpk5m4ud04qum6f0hBj3iE/Pe+DvIbVhLh9ItAunISG\nQPA9dYEgfA/qWir+pU7LV3phpLeGhull8G/zYmQhF3heg0buIR70aavzT8iLAQrx\nVMNRZJEGMwIN/tq8YiT3+3EZIcSqq6GAGjiuVw3NIsXC3+CuSJGQ5GbDp49Lc6VW\nPHeWeFvwSUGgxKXq5r1+PRsoYgK6S4hhecgXEX5c7Rta6TcFlEFb0XK9fpy1dr89\nLeFGxUBpdDvKxDRLMm3FQen8rmR/PSReEcJsaqbUP/q7Pc7k0RfF9Mb6AfPZfnqg\npYJQ+IFSr9EjRSW1wPcL03zoTP47AgMBAAGjdTBzMA4GA1UdDwEB/wQEAwIBBjAQ\nBgNVHSUECTAHBgVngQUIATAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRJ50pb\nVin1nXm3pjA8A7KP5xTdTDAfBgNVHSMEGDAWgBRJ50pbVin1nXm3pjA8A7KP5xTd\nTDANBgkqhkiG9w0BAQsFAAOCAgEAlfHRvOB3CJoLTl1YG/AvjGoZkpNMyp5X5je1\nICCQ68b296En9hIUlcYY/+nuEPSPUjDA3izwJ8DAfV4REgpQzqoh6XhR3TgyfHXj\nJ6DC7puzEgtzF1+wHShUpBoe3HKuL4WhB3rvwk2SEsudBu92o9BuBjcDJ/GW5GRt\npD/H71HAE8rI9jJ41nS0FvkkjaX0glsntMVUXiwcta8GI0QOE2ijsJBwk41uQGt0\nYOj2SGlEwNAC5DBTB5kZ7+6X9xGE6/c+M3TAA0ONoX18rNfif94cCx/mPYOs8pUk\nANRAQ4aTRBvpBrryGT8R1ahTBkMeRQG3tdsLHRT8fJCFUANd5WLWsi83005y/WuM\nz8/gFKc0PL+F+MubCsJ1ODPTRscH93QlS4zEMg5hDAIks+fDoRJ2QiROqo7GAqbT\nc7STKfGcr9+pa63na7f3oy1sZPWPdxB8tx5z3lghiPP3ktQx/yK/1Fwf1hgxJHFy\n/2UcaGuOXRRRTPyEnppZp82Kigs9aPHWtaVm2/LrXX2fvT9iM/k0CovNAj8rztHx\nsUEoA0xJnSOJNPpe9PRdjsTj7/u3Xu6hQLNNidBHgI3Hcmi704HMMd/3yZ424OOr\nS32ylpeU1oeQHFrLE6hYX4/ttMETbmESIKd2rTgstPotSvkuB5TljbKYPR+lq7hQ\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-----\nMIIGATCCA+mgAwIBAgIUAKZdpPnjKPOANcOnPU9yQyvfFdwwDQYJKoZIhvcNAQEL\nBQAwfjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcT\nDU1vdW50YWluIFZpZXcxEzARBgNVBAoTCkdvb2dsZSBMTEMxFTATBgNVBAsTDEdv\nb2dsZSBDbG91ZDEWMBQGA1UEAxMNRUsvQUsgQ0EgUm9vdDAgFw0yMjA3MDgwMDQw\nMzRaGA8yMTIyMDcwODA1NTcyM1owfjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNh\nbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEzARBgNVBAoTCkdvb2ds\nZSBMTEMxFTATBgNVBAsTDEdvb2dsZSBDbG91ZDEWMBQGA1UEAxMNRUsvQUsgQ0Eg\nUm9vdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJ0l9VCoyJZLSol8\nKyhNpbS7pBnuicE6ptrdtxAWIR2TnLxSgxNFiR7drtofxI0ruceoCIpsa9NHIKrz\n3sM/N/E8mFNHiJAuyVf3pPpmDpLJZQ1qe8yHkpGSs3Kj3s5YYWtEecCVfzNs4MtK\nvGfA+WKB49A6Noi8R9R1GonLIN6wSXX3kP1ibRn0NGgdqgfgRe5HC3kKAhjZ6scT\n8Eb1SGlaByGzE5WoGTnNbyifkyx9oUZxXVJsqv2q611W3apbPxcgev8z5JXQUbrr\nQ7EbO0StK1DsKRsKLuD+YLxjrBRQ4UeIN5WHp6G0vgYiOptHm6YKZxQemO/kVMLR\nzsm1AYH7eNOFekcBIKRjSqpk5m4ud04qum6f0hBj3iE/Pe+DvIbVhLh9ItAunISG\nQPA9dYEgfA/qWir+pU7LV3phpLeGhull8G/zYmQhF3heg0buIR70aavzT8iLAQrx\nVMNRZJEGMwIN/tq8YiT3+3EZIcSqq6GAGjiuVw3NIsXC3+CuSJGQ5GbDp49Lc6VW\nPHeWeFvwSUGgxKXq5r1+PRsoYgK6S4hhecgXEX5c7Rta6TcFlEFb0XK9fpy1dr89\nLeFGxUBpdDvKxDRLMm3FQen8rmR/PSReEcJsaqbUP/q7Pc7k0RfF9Mb6AfPZfnqg\npYJQ+IFSr9EjRSW1wPcL03zoTP47AgMBAAGjdTBzMA4GA1UdDwEB/wQEAwIBBjAQ\nBgNVHSUECTAHBgVngQUIATAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRJ50pb\nVin1nXm3pjA8A7KP5xTdTDAfBgNVHSMEGDAWgBRJ50pbVin1nXm3pjA8A7KP5xTd\nTDANBgkqhkiG9w0BAQsFAAOCAgEAlfHRvOB3CJoLTl1YG/AvjGoZkpNMyp5X5je1\nICCQ68b296En9hIUlcYY/+nuEPSPUjDA3izwJ8DAfV4REgpQzqoh6XhR3TgyfHXj\nJ6DC7puzEgtzF1+wHShUpBoe3HKuL4WhB3rvwk2SEsudBu92o9BuBjcDJ/GW5GRt\npD/H71HAE8rI9jJ41nS0FvkkjaX0glsntMVUXiwcta8GI0QOE2ijsJBwk41uQGt0\nYOj2SGlEwNAC5DBTB5kZ7+6X9xGE6/c+M3TAA0ONoX18rNfif94cCx/mPYOs8pUk\nANRAQ4aTRBvpBrryGT8R1ahTBkMeRQG3tdsLHRT8fJCFUANd5WLWsi83005y/WuM\nz8/gFKc0PL+F+MubCsJ1ODPTRscH93QlS4zEMg5hDAIks+fDoRJ2QiROqo7GAqbT\nc7STKfGcr9+pa63na7f3oy1sZPWPdxB8tx5z3lghiPP3ktQx/yK/1Fwf1hgxJHFy\n/2UcaGuOXRRRTPyEnppZp82Kigs9aPHWtaVm2/LrXX2fvT9iM/k0CovNAj8rztHx\nsUEoA0xJnSOJNPpe9PRdjsTj7/u3Xu6hQLNNidBHgI3Hcmi704HMMd/3yZ424OOr\nS32ylpeU1oeQHFrLE6hYX4/ttMETbmESIKd2rTgstPotSvkuB5TljbKYPR+lq7hQ\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> = const { std::cell::RefCell::new(None) }; + // 1. Check for command-line argument + if args.len() > 1 { + let hex_str = args[1].trim(); + return hex::decode(hex_str).map_err(|e| format!("invalid hex nonce: {}", e)); } - URANDOM.with(|cell| { - let mut borrow = cell.borrow_mut(); - let file = borrow.get_or_insert_with(|| File::open("/dev/urandom").unwrap()); - let mut buf = [0u8; 1]; - file.read_exact(&mut buf).unwrap(); - buf[0] - }) + // 2. Check if stdin has data (not a tty) + let stdin = io::stdin(); + if !stdin.is_terminal() { + let mut input = String::new(); + stdin + .lock() + .read_to_string(&mut input) + .map_err(|e| format!("failed to read nonce from stdin: {}", e))?; + let hex_str = input.trim(); + if !hex_str.is_empty() { + return hex::decode(hex_str).map_err(|e| format!("invalid hex nonce: {}", e)); + } + } + + // 3. Generate random nonce + Ok(random_nonce()) +} + +/// Generate a random 32-byte nonce using /dev/urandom +fn random_nonce() -> Vec { + use std::fs::File; + use std::io::Read; + + let mut file = File::open("/dev/urandom").expect("failed to open /dev/urandom"); + let mut buf = [0u8; 32]; + file.read_exact(&mut buf) + .expect("failed to read from /dev/urandom"); + buf.to_vec() } diff --git a/crates/vaportpm-attest/src/cert.rs b/crates/vaportpm-attest/src/cert.rs index e4c949d..e1d7d72 100644 --- a/crates/vaportpm-attest/src/cert.rs +++ b/crates/vaportpm-attest/src/cert.rs @@ -14,6 +14,8 @@ use der::Decode; use std::io::{BufRead, BufReader, Read, Write}; use std::net::TcpStream; use std::time::Duration; +use x509_cert::ext::pkix::name::GeneralName; +use x509_cert::ext::pkix::{AuthorityInfoAccessSyntax, AuthorityKeyIdentifier}; use x509_cert::Certificate; /// DER SEQUENCE tag with 2-byte length (0x30 0x82) @@ -109,67 +111,13 @@ pub fn extract_aki(cert_der: &[u8]) -> Option> { 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); + let aki = AuthorityKeyIdentifier::from_der(ext.extn_value.as_bytes()).ok()?; + return aki.key_identifier.map(|ki| ki.as_bytes().to_vec()); } } 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)?; @@ -177,54 +125,17 @@ pub fn extract_aia_url(cert_der: &[u8]) -> Option { 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(); + let aia = AuthorityInfoAccessSyntax::from_der(ext.extn_value.as_bytes()).ok()?; + for access_desc in aia.0.iter() { + if access_desc.access_method == OID_CA_ISSUERS { + if let GeneralName::UniformResourceIdentifier(uri) = + &access_desc.access_location + { + return Some(uri.to_string()); + } } } } - - // Move to next AccessDescription - pos = desc_end; } None } @@ -376,3 +287,54 @@ pub fn fetch_certificate(url: &str) -> Result> { Ok(body) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_extract_aia_url_gcp_cert() { + // GCP AK leaf certificate with AIA extension + let pem = r#"-----BEGIN CERTIFICATE----- +MIIFITCCAwmgAwIBAgIUAL1/11uaGzgty7zfCO9n8DJu4+AwDQYJKoZIhvcNAQEL +BQAwgYYxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQH +Ew1Nb3VudGFpbiBWaWV3MRMwEQYDVQQKEwpHb29nbGUgTExDMRUwEwYDVQQLEwxH +b29nbGUgQ2xvdWQxHjAcBgNVBAMTFUVLL0FLIENBIEludGVybWVkaWF0ZTAgFw0y +NjAyMDMwOTQyMzJaGA8yMDU2MDEyNzA5NDIzMVowaTEWMBQGA1UEBxMNdXMtY2Vu +dHJhbDEtZjEeMBwGA1UEChMVR29vZ2xlIENvbXB1dGUgRW5naW5lMREwDwYDVQQL +Ewhsb2NrYm9vdDEcMBoGA1UEAxMTMTUzNTM5ODk5NjkwNzY4NjkyOTBZMBMGByqG +SM49AgEGCCqGSM49AwEHA0IABIr5m4cMPky5JjeOObhO+mxaAcGpJ+hctqM9ubgu +sFyZR1agN7FCfYOW2anqx8PSpm+WXjDmzzl2GDm78mBLbn+jggFqMIIBZjAOBgNV +HQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU4sCEt4Oo4yYWVjKM +xeblb7eXu7EwHwYDVR0jBBgwFoAUZ8O73ljj1lF2j7MaPtsHp+yTeuQwgY0GCCsG +AQUFBwEBBIGAMH4wfAYIKwYBBQUHMAKGcGh0dHA6Ly9wcml2YXRlY2EtY29udGVu +dC02NWQ1M2IxNC0wMDAwLTIxMmEtYTYzMy04ODNkMjRmNTdiYjguc3RvcmFnZS5n +b29nbGVhcGlzLmNvbS8wYzNlNzllYjA4OThkMDJlYmIwYS9jYS5jcnQwdgYKKwYB +BAHWeQIBFQRoMGYMDXVzLWNlbnRyYWwxLWYCBTY7VDeMDAhsb2NrYm9vdAIIFU7V +QLcsfBEMGGluc3RhbmNlLTIwMjYwMjAzLTA5Mzk0OaAgMB6gAwIBAKEDAQH/ogMB +Af+jAwEBAKQDAQEApQMBAQAwDQYJKoZIhvcNAQELBQADggIBACCm1YXV1f22GVPl +IVL4JoNg1QCq+g5PzgPY9/afjriE8sAM/+Ebj/M96rUS+nFxYHpfzsxfW+4Y7Ko2 +O8BGQ4U5Og7Rt5rMyCe/g3qXrZhQIcXIJouXvOsI1G5njXI03kXac8I//IvyMzMr +pxy2SxVQ1djFFQoRA6MF1R3F4cZ1OUcgTPFWAuYuF6rN+F9RSTDuzFpKlWVPfPHX +K0s/eGv+zvlpzBXfX/ES7OAIomfVrmeXqdQYC+ZEJo8tG8eJlxBo8c8Y4GNQpo2I +9O/kYiOdcjzz8F3OeGH6b1dp10uur02nfz/vH0vpkVLNKllm9swZ42i1sQkl0g7u +/p6jSUwBEej54fDEOKj8yRvbuMd36w1bYFBtnkvQlKBCT1hStaAtbFilHuSqlMRm +xVcyunIlN6udQJTKCWPgFsLHgxlUBASm1k0zWsoFjIH9SFHu+GglzK2v1RoHZA5P +33xcxKVzw52TAuPJc4Za/iKmFiA647VXYbiCaKNPn/oi7rLHTUAQ2tj7SJRbJcSu +/q4xg60z8JOX7rtSxCrXFOp9ys2WzxSCqx1aXnUU+Ng+TtImheoUue+Zk3v7Olen +HysTF1gLzHRLvONeErG6mUoxbkFhVsbGfbBDoe3jojNMISreY9IsY2UgMVIdKqLH +bPF0Yysi72AJB6iorXKFwC9f61s0 +-----END CERTIFICATE-----"#; + + let der = pem_to_der(pem).expect("PEM decode should work"); + let aia_url = extract_aia_url(&der); + + assert!(aia_url.is_some(), "Should extract AIA URL from GCP cert"); + let url = aia_url.unwrap(); + assert!( + url.starts_with("http://privateca-content-"), + "URL should be GCP privateca URL" + ); + assert!(url.ends_with("/ca.crt"), "URL should end with /ca.crt"); + } +} diff --git a/crates/vaportpm-attest/src/ek.rs b/crates/vaportpm-attest/src/ek.rs index 74054fd..105578b 100644 --- a/crates/vaportpm-attest/src/ek.rs +++ b/crates/vaportpm-attest/src/ek.rs @@ -19,6 +19,12 @@ 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 restricted ECC P-256 Attestation Key (TCG-compliant AK profile) + /// + /// Creates a key with attributes: fixedtpm|fixedparent|sensitivedataorigin|userwithauth|restricted|sign + /// This matches the TCG AK profile and GCP's AK template. + fn create_restricted_ak(&mut self, hierarchy: u32) -> Result; + /// Create a primary key from a raw TPMT_PUBLIC template /// /// This takes template bytes (as stored in NV RAM by cloud providers like GCP) @@ -53,6 +59,59 @@ pub trait KeyOps { } impl KeyOps for Tpm { + fn create_restricted_ak(&mut self, hierarchy: u32) -> Result { + let public_area = build_restricted_ak_public_area(); + + 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) + .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)?; + + // Parse the public key from the public area + 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 }) + } + fn create_primary_ecc_key(&mut self, hierarchy: u32) -> Result { let public_area = build_ecc_public_area(); @@ -256,7 +315,38 @@ fn build_pcr_selection(selections: &[(TpmAlg, &[u8])]) -> Vec { buf } -/// Build a TPM2B_PUBLIC structure for an ECC P-256 signing key +/// Build a TPM2B_PUBLIC structure for a restricted ECC P-256 Attestation Key +/// +/// Attributes: fixedtpm|fixedparent|sensitivedataorigin|userwithauth|restricted|sign (0x50072) +/// This matches the TCG AK profile and GCP's AK template structure. +fn build_restricted_ak_public_area() -> Vec { + let attrs = ObjectAttributes::new() + .fixed_tpm() + .fixed_parent() + .sensitive_data_origin() + .user_with_auth() + .restricted() + .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_u16(0) // authPolicy (empty) + // parameters (TPMS_ECC_PARMS) + .write_u16(TpmAlg::Null as u16) // symmetric + .write_u16(TpmAlg::EcDsa as u16) // scheme = ECDSA (required for restricted signing key) + .write_u16(TpmAlg::Sha256 as u16) // scheme hash = SHA256 + .write_u16(TpmEccCurve::NistP256 as u16) // curveID + .write_u16(TpmAlg::Null as u16) // kdf + // unique (TPMS_ECC_POINT) - empty, TPM will generate + .write_u16(0) // x size + .write_u16(0) // y size + .into_vec() +} + +/// Build a TPM2B_PUBLIC structure for an ECC P-256 signing key (unrestricted) fn build_ecc_public_area() -> Vec { let attrs = ObjectAttributes::new() .fixed_tpm() diff --git a/crates/vaportpm-verify/Cargo.toml b/crates/vaportpm-verify/Cargo.toml index edd0481..9748104 100644 --- a/crates/vaportpm-verify/Cargo.toml +++ b/crates/vaportpm-verify/Cargo.toml @@ -38,3 +38,7 @@ serde_json = { workspace = true } name = "vaportpm_verify" path = "src/lib.rs" +[[bin]] +name = "vaportpm-verify" +path = "src/bin/verify.rs" + diff --git a/crates/vaportpm-verify/README.md b/crates/vaportpm-verify/README.md index 856c602..ac35899 100644 --- a/crates/vaportpm-verify/README.md +++ b/crates/vaportpm-verify/README.md @@ -91,11 +91,11 @@ The `time` parameter controls certificate validity checking. Use `UnixTime::now( Returns `VerificationResult` containing: - `nonce` - The verified challenge (hex) - `root_pubkey_hash` - SHA-256 of the trust anchor's public key (hex) -- `method` - How verification was performed (currently only `Nitro`) +- `provider` - Cloud provider (AWS, GCP) ## Security Considerations -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. +The library embeds the CA root certificates 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. However, beyond that it's up to the application to decide on the following: diff --git a/crates/vaportpm-verify/src/lib.rs b/crates/vaportpm-verify/src/lib.rs index c5dc8b8..1849e9b 100644 --- a/crates/vaportpm-verify/src/lib.rs +++ b/crates/vaportpm-verify/src/lib.rs @@ -356,9 +356,8 @@ pub fn verify_attestation_json(json: &str) -> Result