diff --git a/Cargo.toml b/Cargo.toml index 9f1f160..59cb053 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,12 +24,11 @@ x509-cert = { version = "0.2", features = ["pem"] } p256 = { version = "0.13", features = ["ecdsa", "pem", "pkcs8"] } p384 = { version = "0.13", features = ["ecdsa", "pem", "pkcs8"] } ecdsa = { version = "0.16", features = ["verifying", "der"] } +rsa = { version = "0.9", features = ["sha2"] } coset = "0.3" ciborium = "0.2" -# Certificate chain validation (rustls ecosystem) -rustls-rustcrypto = { version = "=0.0.2-alpha", default-features = false, features = ["std", "zeroize"] } -webpki = { package = "rustls-webpki", version = "=0.102.8", default-features = false, features = ["alloc"] } +# Time types for certificate validation pki-types = { package = "rustls-pki-types", version = "=1.13.0", default-features = false, features = ["std"] } [profile.release] diff --git a/Makefile b/Makefile index 69bdd34..43e56ad 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,9 @@ build: release-x86: cargo build --workspace --release --target x86_64-unknown-linux-musl +release-aarch64: + cargo build --workspace --release --target aarch64-unknown-linux-musl + check: cargo check --workspace --all-targets diff --git a/README.md b/README.md index f9feb82..8f0327e 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,10 @@ # v[apor]TPM -> What does the "v" in vTPM stand for? - -Cloud vTPM attestation library for Rust. Zero C dependencies. - -## The Name +**Cloud vTPM attestation library for Rust. Zero C dependencies.** -``` -vTPM β†’ v[apor]TPM -lockboot β†’ [g]lockboot -``` - -Physical TPM trust is vapor. It evaporates under scrutiny - supply chain attacks, firmware vulnerabilities, the whole theater. The only meaningful TPM trust lives in cloud vTPMs, where the hypervisor **is** the root of trust. +> What does the "v" in vTPM stand for? -The "v" always stood for vapor. Everyone just forgot. +Physical TPM trust is vapor. It evaporates under scrutiny - supply chain attacks, firmware vulnerabilities, the whole theater. The only meaningful TPM trust lives in cloud vTPMs, where the hypervisor **is** the root of trust. The "v" always stood for vapor. Everyone just forgot. ## Crates @@ -36,10 +27,12 @@ You handle **policy decisions**: | Platform | Status | Trust Anchor | |----------|--------|--------------| -| AWS Nitro | βœ… Working | Nitro Root CA | -| GCP Shielded VM | πŸ”œ Planned | Google AK certificate | +| AWS EC2 with Nitro v4+ | βœ… Working | Nitro Root CA | +| GCP Confidential VM | βœ… Working | Google EK/AK CA Root | | Azure Trusted Launch | πŸ”œ Planned | Microsoft AK certificate | +Please note that GCP 'Shielded VM' with vTPM isn't enough, a 'Confidential VM' is necessary as Google doesn't provision AK certificates without that feature enabled (be it Intel TDX or AMD SEV) + ## Quick Start ### Generate Attestation (on cloud instance) @@ -54,15 +47,12 @@ let json = attest(b"challenge-nonce")?; ### Verify Attestation (anywhere) ```rust -use vaportpm_verify::{verify_attestation_json, VerificationResult}; +use vaportpm_verify::verify_attestation_json; let result = verify_attestation_json(&json)?; - -// Check the trust root is acceptable -if result.root_pubkey_hash == KNOWN_AWS_NITRO_ROOT_HASH { - println!("Verified via: {:?}", result.method); - println!("Nonce: {}", result.nonce); -} +// Verification succeeded - attestation is from a supported cloud provider +println!("Provider: {:?}", result.provider); +println!("PCRs: {:?}", result.pcrs); ``` ## License diff --git a/crates/vaportpm-attest/AWS-NITRO.md b/crates/vaportpm-attest/AWS-NITRO.md index 574f4dc..7ceb51e 100644 --- a/crates/vaportpm-attest/AWS-NITRO.md +++ b/crates/vaportpm-attest/AWS-NITRO.md @@ -16,9 +16,9 @@ By combining these, we get a chain of trust from AWS hardware to arbitrary appli ```mermaid flowchart TD A["AWS Nitro Root CA
Verification returns root pubkey hash"] - B["Nitro Attestation Document
COSE Sign1 structure containing:
β€’ nitrotpm_pcrs: SHA-384 PCR values
β€’ public_key: Application key binding
β€’ nonce: Freshness proof
β€’ Certificate chain to AWS root"] - C["TPM Attestation Key (AK)
β€’ ECC P-256 signing key
β€’ authPolicy = PolicyPCR(SHA-384 bank)
β€’ Can only sign when PCRs match"] - D["TPM2B_ATTEST
β€’ extraData: nonce (matches Nitro)
β€’ certifiedName: includes authPolicy
β€’ Proves: AK bound to PCR values"] + B["Nitro Attestation Document
COSE Sign1 structure containing:
β€’ nitrotpm_pcrs: SHA-384 PCR values
β€’ public_key: AK public key binding
β€’ nonce: Freshness proof
β€’ Certificate chain to AWS root"] + C["TPM Attestation Key (AK)
β€’ ECC P-256 signing key
β€’ Long-term key (no PCR binding)
β€’ Bound to Nitro document via public_key"] + D["TPM2_Quote (TPMS_ATTEST)
β€’ extraData: nonce (matches Nitro)
β€’ pcrDigest: hash of quoted PCRs
β€’ Proves: PCR values at quote time"] A -->|"Signs (ECDSA P-384)"| B B -->|"public_key field binds"| C @@ -29,9 +29,9 @@ flowchart TD The Nitro document signs `nitrotpm_pcrs` which contains **SHA-384** PCR values. For a coherent chain of trust: -- The AK's `authPolicy` references the SHA-384 PCR bank -- Verification computes policy from SHA-384 PCRs -- The signed Nitro PCRs match what the AK is bound to +- TPM2_Quote includes SHA-384 PCRs in the signed attestation +- The Nitro document contains signed SHA-384 PCR values +- Verification compares the Quote's PCRs against the signed Nitro values This ensures a single, verifiable path from AWS hardware to the attested data. @@ -40,26 +40,21 @@ This ensures a single, verifiable path from AWS hardware to the attested data. ### Generation (on Nitro instance) ```rust -// 1. Detect Nitro and choose PCR bank +// 1. Detect Nitro and read PCR values (SHA-384) let is_nitro = tpm.is_nitro_tpm()?; -let pcr_alg = if is_nitro { TpmAlg::Sha384 } else { TpmAlg::Sha256 }; +let pcr_values = tpm.read_all_allocated_pcrs()?; // Reads SHA-384 bank -// 2. Read PCR values from chosen bank -let pcr_values = tpm.read_pcrs(pcr_alg)?; +// 2. Create restricted AK in endorsement hierarchy (TCG-compliant AK profile) +let ak = tpm.create_restricted_ak(TPM_RH_ENDORSEMENT)?; -// 3. Compute PCR policy digest -let auth_policy = Tpm::calculate_pcr_policy_digest(&pcr_values, pcr_alg)?; +// 3. Quote PCRs with AK (signs PCR values) +// Note: nonce is caller-provided for freshness/replay protection +let quote_result = tpm.quote(ak.handle, &nonce, &pcr_selection)?; -// 4. Create AK bound to this policy -let ak = tpm.create_primary_ecc_key_with_policy(TPM_RH_OWNER, &auth_policy)?; - -// 5. AK self-certifies (proves it exists with this policy) -let certify_result = tpm.certify(ak.handle, ak.handle, &nonce)?; - -// 6. Get Nitro attestation binding the AK public key +// 4. Get Nitro attestation binding the AK public key let nitro_doc = tpm.nsm_attest( None, // user_data - Some(nonce.to_vec()), // nonce (same as TPM) + Some(nonce.to_vec()), // nonce (same as TPM Quote) Some(ak_public_key_secg), // public_key (binds AK) )?; ``` @@ -76,20 +71,14 @@ let nitro_result = verify_nitro_attestation(&nitro_doc)?; // 3. Verify AK public key matches Nitro's public_key binding assert!(ak_pubkey == nitro_result.document.public_key); -// 4. Verify TPM signature over TPM2B_ATTEST -verify_ecdsa_p256(&attest_data, &signature, &ak_pubkey)?; - -// 5. Compute expected AK name from PCR policy -let policy = calculate_pcr_policy(&sha384_pcrs, TpmAlg::Sha384)?; -let expected_name = compute_ecc_p256_name(&ak_x, &ak_y, &policy); - -// 6. Verify certified name matches (proves PCR binding) -assert!(attest_info.certified_name == expected_name); +// 4. Verify TPM Quote signature +verify_ecdsa_p256("e_attest_data, &signature, &ak_pubkey)?; -// 7. Verify nonces match (proves freshness and binding) -assert!(tpm_nonce == nitro_nonce); +// 5. Verify nonces match (proves freshness and binding) +let quote_info = parse_quote_attest("e_attest_data)?; +assert!(quote_info.nonce == nitro_nonce); -// 8. Verify PCR values match signed Nitro document +// 6. Verify PCR values match signed Nitro document assert!(output.pcrs["sha384"] == nitro_result.document.pcrs); ``` @@ -99,11 +88,11 @@ assert!(output.pcrs["sha384"] == nitro_result.document.pcrs); 1. **Hardware Root of Trust** - The Nitro document is signed by AWS hardware (certificate chain to AWS root CA) -2. **PCR Integrity** - The SHA-384 PCR values in the attestation match what AWS hardware measured +2. **PCR Integrity** - The SHA-384 PCR values in the attestation match what AWS hardware measured (signed in Nitro document) -3. **Key Binding** - The AK is bound to specific PCR values via `authPolicy` (it cannot sign unless PCRs match) +3. **Key Binding** - The AK is bound to the Nitro document via the `public_key` field -4. **Freshness** - The nonce in both TPM and Nitro attestations proves the attestation is fresh +4. **Freshness** - The nonce in both TPM Quote and Nitro document proves the attestation is fresh 5. **AK Authenticity** - The Nitro document's `public_key` field proves the AK belongs to this Nitro instance @@ -193,7 +182,7 @@ COSE_Sign1 = [ } ``` -Note: Only SHA-384 PCRs are included because that's the bank bound to the AK and signed in the Nitro document. +Note: Only SHA-384 PCRs are included because that's the bank signed in the Nitro document. ## References diff --git a/crates/vaportpm-attest/CLAUDE.md b/crates/vaportpm-attest/CLAUDE.md deleted file mode 100644 index c2d319e..0000000 --- a/crates/vaportpm-attest/CLAUDE.md +++ /dev/null @@ -1,30 +0,0 @@ -# vaportpm-attest - -Minimal TPM 2.0 protocol implementation in pure Rust without C dependencies. - -## Build - -Debug build: -```bash -cargo build -``` - -For deployment, we target static musl binaries: -```bash -cargo build --target x86_64-unknown-linux-musl -``` - -## Architecture - -- Direct communication with TPM via /dev/tpmrm0 (resource manager) or /dev/tpm0 (direct) -- No tpm2-tss or other C library dependencies -- Implements TPM 2.0 command/response protocol per TCG specification - -## Key Modules - -- `credential.rs` - Policy session operations and TPM object name computation -- `ek.rs` - EK and signing key operations -- `pcr.rs` - PCR read/extend operations -- `a9n.rs` - Attestation generation -- `nv.rs` - NV RAM read/write operations -- `nsm.rs` - AWS Nitro-specific vendor commands diff --git a/crates/vaportpm-attest/Cargo.toml b/crates/vaportpm-attest/Cargo.toml index 33b5183..c3e88f6 100644 --- a/crates/vaportpm-attest/Cargo.toml +++ b/crates/vaportpm-attest/Cargo.toml @@ -18,6 +18,14 @@ serde_bytes = { workspace = true } hex = { workspace = true } serde_json = { workspace = true } +# X.509 certificate parsing +der = { workspace = true } +x509-cert = { workspace = true } + [lib] name = "vaportpm_attest" path = "src/lib.rs" + +[[bin]] +name = "vaportpm-attest" +path = "src/bin/attest.rs" diff --git a/crates/vaportpm-attest/GCP.md b/crates/vaportpm-attest/GCP.md new file mode 100644 index 0000000..8186a6d --- /dev/null +++ b/crates/vaportpm-attest/GCP.md @@ -0,0 +1,198 @@ +# GCP Confidential VM Attestation + +This document describes how GCP Confidential VMs perform TPM-based attestation using +Attestation Key (AK) certificates. + +## Overview + +**Important**: Only GCP Confidential VMs receive AK certificates from Google's CA +hierarchy. Standard Shielded VMs (with Secure Boot enabled but no confidential +computing) have a vTPM but do NOT receive Google-signed AK certificates. + +GCP Confidential VMs have a virtual TPM (vTPM) with an Attestation Key (AK) that is +certified by Google's EK/AK CA hierarchy. The attestation flow is: + +1. VM requests an AK certificate from Google's metadata service +2. Google issues a certificate binding the AK public key to the VM's identity +3. The VM uses the AK to sign TPM2_Quote attestations +4. Verifiers validate the quote signature using the certificate chain + +## Certificate Chain Structure + +``` +EK/AK CA Root (self-signed, offline) + β”‚ + └── EK/AK CA Intermediate (online issuer) + β”‚ + └── AK Certificate (per-VM, short-lived) +``` + +### Root CA Certificate + +- **Subject/Issuer**: `CN=EK/AK CA Root, OU=Google Cloud, O=Google LLC, L=Mountain View, ST=California, C=US` +- **Validity**: ~100 years (July 2022 - July 2122) +- **Key Type**: RSA 4096-bit +- **Basic Constraints**: `CA:TRUE` +- **Key Usage**: `Certificate Sign, CRL Sign` +- **Extended Key Usage**: TPM EK Certificate (`2.23.133.8.1`) + +### Intermediate CA Certificate + +- **Subject**: `CN=EK/AK CA Intermediate, OU=Google Cloud, O=Google LLC, L=Mountain View, ST=California, C=US` +- **Issuer**: Root CA +- **Validity**: ~98 years +- **Key Type**: RSA 4096-bit +- **Basic Constraints**: `CA:TRUE` +- **Key Usage**: `Certificate Sign, CRL Sign` +- **Extended Key Usage**: TPM EK Certificate (`2.23.133.8.1`) + +### AK Leaf Certificate + +- **Subject**: `CN=, OU=, O=Google Compute Engine, L=` +- **Issuer**: Intermediate CA +- **Validity**: 30 years (per-instance) +- **Key Type**: ECDSA P-256 +- **Basic Constraints**: `CA:FALSE` (critical) +- **Key Usage**: `Digital Signature` (critical) +- **Extended Key Usage**: None (only Key Usage is present) + +## AK Template (NV RAM) + +Google provisions an AK template at NV index `0x01c10003` (ECC) that defines the key properties. +The template is a `TPMT_PUBLIC` structure: + +``` +type: ecc (0x23) +nameAlg: sha256 (0x0b) +attributes: fixedtpm|fixedparent|sensitivedataorigin|userwithauth|restricted|sign (0x50072) +scheme: ecdsa with sha256 +curve: NIST P-256 +unique: +``` + +Key properties: + +| Attribute | Meaning | +|-----------|---------| +| `restricted` | Can only sign TPM-generated structures (Quotes, certifications) | +| `sign` | Signing key (no decrypt capability) | +| `fixedtpm` | Key cannot be duplicated outside this TPM | +| `fixedparent` | Key cannot be moved to a different parent | + +The `unique` field contains pre-computed ECC coordinates. When `TPM2_CreatePrimary` is called +with this template in the endorsement hierarchy, the TPM deterministically derives a key +matching the certificate Google provisions at `0x01c10002`. + +This is a TCG-compliant Attestation Key profile: a restricted signing key in the endorsement +hierarchy, bound to the platform via the EK seed. + +## GCP Instance Identity Extension + +AK certificates include a custom extension that binds the certificate to the VM: + +**OID**: `1.3.6.1.4.1.11129.2.1.21` + +This extension contains a DER-encoded structure with: + +| Field | Type | Description | +|-------|------|-------------| +| `zone` | UTF8String | GCP zone (e.g., `us-central1-f`) | +| `project` | UTF8String | GCP project ID | +| `instance_id` | INTEGER | Numeric instance ID | +| `instance_name` | UTF8String | Instance name | +| Additional fields | Various | Confidential Computing flags | + +Example from a real certificate: +``` +zone: us-central1-f +project: lockboot +instance_id: 3414240648225485836 +instance_name: instance-20260202-065609 +``` + +## TPM Quote Structure + +The TPM2_Quote structure signed by the AK contains: + +``` +TPM2B_ATTEST { + magic: 0xFF544347 ("TCG\xFF") + type: TPM_ST_ATTEST_QUOTE (0x8018) + qualifiedSigner: Hash of signing key name + extraData: Nonce passed to attest() + clockInfo: TPM clock values + firmwareVersion: TPM firmware version + attested: TPMS_QUOTE_INFO { + pcrSelect: Which PCRs are included + pcrDigest: SHA-256 of concatenated PCR values + } +} +``` + +The signature is ECDSA over the TPM2B_ATTEST structure. + +The nonce in `extraData` is whatever value was passed to the `attest()` function. In a +challenge-response protocol, this would be a verifier-provided challenge. The library +itself is agnostic to how the nonce is generated or validatedβ€”that's up to the calling +application's attestation flow. + +## Verification Process + +1. **Parse certificate chain** from AK cert PEM (leaf β†’ intermediate β†’ root) + +2. **Validate X.509 extensions**: + - Leaf: `CA:FALSE`, `digitalSignature` key usage + - Intermediate/Root: `CA:TRUE`, `keyCertSign` key usage + - Intermediates: TPM EK Certificate EKU (`2.23.133.8.1`) + +3. **Verify certificate signatures**: Each cert signed by the next in chain + +4. **Check validity dates**: All certs must be valid at verification time + +5. **Verify issuer/subject chaining**: Each cert's Issuer matches parent's Subject + +6. **Extract AK public key** from leaf certificate + +7. **Verify TPM Quote signature** using AK public key + +8. **Validate nonce** in Quote.extraData matches expected challenge + +9. **Verify PCR digest**: Recompute digest from claimed PCR values, compare to Quote + +10. **Check root CA**: Hash root's public key and verify it matches known GCP root + +## Security Considerations + +### What This Validates + +- The AK was created on a GCP Confidential VM vTPM certified by Google +- The Quote was signed by that specific AK +- PCR values were selected by the Quote at signing time +- The nonce can prove freshness if verifier-provided (replay protection) + +### What This Does NOT Validate + +- The PCR values themselves are correct for the expected software state +- The VM is actually running the software you expect +- No malware modified memory after boot (vTPM measures boot, not runtime) + +### Trust Assumptions + +- Google's EK/AK CA infrastructure is not compromised +- The embedded root CA public key hash is authentic +- Time source is accurate for certificate validation + +## Differences from AWS Nitro + +| Aspect | GCP Confidential VM | AWS Nitro | +|--------|---------------------|-----------| +| Trust Root | X.509 certificate chain | COSE-signed NSM document | +| Key Binding | AK certificate includes VM identity | NSM document has public_key field | +| PCR Source | TPM2_Quote | Nitro document (nitrotpm_pcrs) | +| Algorithm | ECDSA P-256 | ECDSA P-384 | + +## References + +- [TCG TPM 2.0 Library Specification](https://trustedcomputinggroup.org/resource/tpm-library-specification/) +- [GCP Confidential VM Documentation](https://cloud.google.com/confidential-computing/confidential-vm/docs) +- [RFC 5280 - X.509 PKI Certificate Profile](https://tools.ietf.org/html/rfc5280) diff --git a/crates/vaportpm-attest/README.md b/crates/vaportpm-attest/README.md index 41cfbf7..66fa97e 100644 --- a/crates/vaportpm-attest/README.md +++ b/crates/vaportpm-attest/README.md @@ -1,62 +1,33 @@ # vaportpm-attest -Cloud vTPM attestation in pure Rust with **zero C dependencies**. +Produces a self-contained attestation document from a cloud vTPM. The output can be verified offline using [`vaportpm-verify`](../vaportpm-verify/). -> Physical TPM trust is vapor. Cloud vTPM is where the real trust lives. - ---- - -Most people wrap the crusty C-based `tss2` monolith. Instead, this library implements the bare minimum TPM 2.0 wire-protocol, yeeting bytes directly to `/dev/tpm0`: - -- No libtss2, or any C toolchain -- Direct protocol command/response serialization -- Minimal, and focused on cloud attestation -- Comprehensive `selftest` binary - -## Features - -### PCR Operations (`PcrOps` trait) - -- Read PCR values from all active banks (SHA-1, SHA-256, SHA-384, SHA-512) -- Extend PCRs with automatic multi-bank support -- Query which banks a PCR is allocated in -- Read all allocated PCRs from the TPM - -### Key Management - -- Create primary ECC P-256 signing keys -- Sign data with TPM keys (ECDSA signatures) -- Create PCR-sealed keys (policy-based authorization) - -### NV RAM Operations (`NvOps` trait) +```rust +use vaportpm_attest::attest; -- **Read Operations:** - - Read from TPM Non-Volatile storage - - Read NV index public information - - Enumerate all NV indices - - Read Endorsement Key (EK) certificates -- **Write Operations:** - - Define new NV spaces with attributes - - Write data to NV indices - - Undefine (delete) NV spaces - - Find free NV indices +let json = attest(nonce)?; +// Send json to verifier +``` -### Attestation +The library auto-detects the cloud platform (AWS Nitro, GCP Confidential VM) and produces a JSON document containing: +- TPM2_Quote (signed PCR values) +- Platform-specific trust chain (Nitro document or GCP AK certificate) +- PCR values and nonce for verification -- Certify keys using other keys -- Generate attestation structures -- PCR policy digest calculation +--- -### AWS Nitro Security Module (NSM) Support (`NsmOps` trait) +Implements the TPM 2.0 wire protocol in pure Rustβ€”no `tss2` or C dependencies. While low-level TPM operations are exposed via extension traits, the primary interface is `attest()`. -- Request attestation documents from AWS Nitro Secure Module -- Includes PCR values, optional user data, nonce, and public key -- Automatic detection of Nitro TPM via vendor string +## Low-Level TPM Operations -### TPM Vendor Detection +The following traits are available for direct TPM interaction: -- Query TPM manufacturer and vendor information -- Check if running on AWS Nitro TPM (`is_nitro_tpm()`) +| Trait | Operations | +|-------|------------| +| `PcrOps` | Read/extend PCRs across all hash banks | +| `NvOps` | Read/write NV RAM, enumerate indices | +| `KeyOps` | Create signing keys, TPM2_Quote | +| `NsmOps` | AWS Nitro Security Module attestation | ## Quick Start @@ -151,40 +122,6 @@ fn main() -> anyhow::Result<()> { } ``` -## Running Tests - -### `selftest` - Comprehensive TPM Test Suite - -The "self test" binary exercises all major functionality: - -```bash -cargo run --bin selftest -``` - -It performs: -1. Query TPM properties (manufacturer, firmware version, etc.) -2. Detect AWS Nitro TPM -3. Query active PCR banks -4. Read all non-zero PCR values -5. Extend PCR 23 and verify the change -6. Create a primary ECC signing key -7. Sign test data -8. Access the Endorsement Key -9. Read EK certificates from NV RAM -10. Enumerate all NV indices -11. Create PCR-sealed keys -12. Certify keys (attestation) - -### `nsmtest` - AWS Nitro Security Module Test - -Test NSM attestation functionality (requires AWS Nitro TPM): - -```bash -cargo run --bin nsmtest -``` - -This requests an attestation document from the Nitro Secure Module and displays the result. - ## Requirements - Linux with TPM 2.0 support @@ -221,7 +158,7 @@ The library implements the TPM 2.0 command/response protocol as specified in the - `TPM2_NV_Write` - Write to NV storage - `TPM2_NV_UndefineSpace` - Delete NV indices - `TPM2_PolicyPCR` - PCR policy operations -- `TPM2_Certify` - Key certification/attestation +- `TPM2_Quote` - PCR attestation **Vendor-Specific Commands:** - `TPM2_CC_VENDOR_AWS_NSM_REQUEST` (0x20000001) - AWS Nitro Security Module attestation @@ -233,11 +170,11 @@ The library uses extension traits to organize functionality: - **`PcrOps`** - PCR read/extend operations - **`NvOps`** - NV RAM read/write operations - **`NsmOps`** - AWS Nitro Security Module operations -- **`EkOps`** - Endorsement Key operations (create standard EK, signing keys) +- **`KeyOps`** - Key operations (create signing keys, TPM2_Quote) Import the traits you need: ```rust -use vaportpm_attest::{Tpm, PcrOps, NvOps, NsmOps, EkOps}; +use vaportpm_attest::{Tpm, PcrOps, NvOps, NsmOps, KeyOps}; ``` ### Hash Algorithms @@ -252,36 +189,11 @@ Supports multiple PCR banks: ### Attestation Model -The library implements a TPM-based attestation model where: - -1. **Endorsement Key (EK)** - A TPM's identity, certified by the manufacturer. Created using the TCG standard template for deterministic key derivation. The EK is decrypt-only (cannot sign). - -2. **Attestation Key (AK)** - A signing key created with a PCR policy. The AK can only be used when PCRs match specific values, cryptographically binding the key to the system state. - -3. **PCR Policy** - A SHA-256 digest computed from PCR values. When an AK is created with an `authPolicy`, it can only sign when `TPM2_PolicyPCR` succeeds with matching values. - -4. **TPM2_Certify** - The AK self-certifies, producing a `TPM2B_ATTEST` structure containing the AK's name (which includes its `authPolicy`). This proves the AK exists and is bound to specific PCR values. - -### Chain of Trust - -```mermaid -flowchart TD - A["Trust Anchor
Nitro Root CA / TPM Manufacturer / Cloud Provider"] - B["Platform Attestation
Nitro Document / EK Certificate / AK Certificate"] - C["Signing Key (AK)
Bound to PCR values via authPolicy"] - D["TPM2B_ATTEST + Signature
Contains: nonce, AK name (proves PCR binding)"] - - A -->|signs| B - B -->|binds| C - C -->|signs| D -``` +The library generates TPM2_Quote attestations signed by a long-lived Attestation Key (AK). The AK's authenticity is proven via platform-specific trust anchors: -### Platform-Specific Trust +- **AWS Nitro**: The AK public key is embedded in a Nitro attestation document (via `nsm_attest`), which is signed by Amazon's Nitro CA chain. +- **GCP Confidential VM**: The AK has a certificate stored in TPM NV RAM, signed by Google's CA chain. -| Platform | Trust Anchor | AK Binding Method | -|----------|-------------|-------------------| -| AWS Nitro | Nitro Root CA | Nitro document `public_key` field | -| GCP Shielded VM | Google CA | AK certificate (NV 0x01c10000) | -| Azure Trusted Launch | Microsoft CA | AK certificate (NV 0x01C101D0) | +Both paths produce a TPM2_Quote (signed PCR values + nonce) that can be verified against the platform's root of trust. See [AWS-NITRO.md](./AWS-NITRO.md) for detailed AWS Nitro attestation documentation. diff --git a/crates/vaportpm-attest/certs/aws-nitro-root.pem b/crates/vaportpm-attest/certs/aws-nitro-root.pem new file mode 100644 index 0000000..221cc0b --- /dev/null +++ b/crates/vaportpm-attest/certs/aws-nitro-root.pem @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICETCCAZagAwIBAgIRAPkxdWgbkK/hHUbMtOTn+FYwCgYIKoZIzj0EAwMwSTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoMBkFtYXpvbjEMMAoGA1UECwwDQVdTMRswGQYD +VQQDDBJhd3Mubml0cm8tZW5jbGF2ZXMwHhcNMTkxMDI4MTMyODA1WhcNNDkxMDI4 +MTQyODA1WjBJMQswCQYDVQQGEwJVUzEPMA0GA1UECgwGQW1hem9uMQwwCgYDVQQL +DANBV1MxGzAZBgNVBAMMEmF3cy5uaXRyby1lbmNsYXZlczB2MBAGByqGSM49AgEG +BSuBBAAiA2IABPwCVOumCMHzaHDimtqQvkY4MpJzbolL//Zy2YlES1BR5TSksfbb +48C8WBoyt7F2Bw7eEtaaP+ohG2bnUs990d0JX28TcPQXCEPZ3BABIeTPYwEoCWZE +h8l5YoQwTcU/9KNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUkCW1DdkF +R+eWw5b6cp3PmanfS5YwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2kAMGYC +MQCjfy+Rocm9Xue4YnwWmNJVA44fA0P5W2OpYow9OYCVRaEevL8uO1XYru5xtMPW +rfMCMQCi85sWBbJwKKXdS6BptQFuZbT73o/gBh1qUxl/nNr12UO8Yfwr6wPLb+6N +IwLz3/Y= +-----END CERTIFICATE----- diff --git a/crates/vaportpm-attest/certs/aws-nitro-root.txt b/crates/vaportpm-attest/certs/aws-nitro-root.txt new file mode 100644 index 0000000..f8075df --- /dev/null +++ b/crates/vaportpm-attest/certs/aws-nitro-root.txt @@ -0,0 +1,39 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + f9:31:75:68:1b:90:af:e1:1d:46:cc:b4:e4:e7:f8:56 + Signature Algorithm: ecdsa-with-SHA384 + Issuer: C=US, O=Amazon, OU=AWS, CN=aws.nitro-enclaves + Validity + Not Before: Oct 28 13:28:05 2019 GMT + Not After : Oct 28 14:28:05 2049 GMT + Subject: C=US, O=Amazon, OU=AWS, CN=aws.nitro-enclaves + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (384 bit) + pub: + 04:fc:02:54:eb:a6:08:c1:f3:68:70:e2:9a:da:90: + be:46:38:32:92:73:6e:89:4b:ff:f6:72:d9:89:44: + 4b:50:51:e5:34:a4:b1:f6:db:e3:c0:bc:58:1a:32: + b7:b1:76:07:0e:de:12:d6:9a:3f:ea:21:1b:66:e7: + 52:cf:7d:d1:dd:09:5f:6f:13:70:f4:17:08:43:d9: + dc:10:01:21:e4:cf:63:01:28:09:66:44:87:c9:79: + 62:84:30:4d:c5:3f:f4 + ASN1 OID: secp384r1 + NIST CURVE: P-384 + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:TRUE + X509v3 Subject Key Identifier: + 90:25:B5:0D:D9:05:47:E7:96:C3:96:FA:72:9D:CF:99:A9:DF:4B:96 + X509v3 Key Usage: critical + Digital Signature, Certificate Sign, CRL Sign + Signature Algorithm: ecdsa-with-SHA384 + Signature Value: + 30:66:02:31:00:a3:7f:2f:91:a1:c9:bd:5e:e7:b8:62:7c:16: + 98:d2:55:03:8e:1f:03:43:f9:5b:63:a9:62:8c:3d:39:80:95: + 45:a1:1e:bc:bf:2e:3b:55:d8:ae:ee:71:b4:c3:d6:ad:f3:02: + 31:00:a2:f3:9b:16:05:b2:70:28:a5:dd:4b:a0:69:b5:01:6e: + 65:b4:fb:de:8f:e0:06:1d:6a:53:19:7f:9c:da:f5:d9:43:bc: + 61:fc:2b:eb:03:cb:6f:ee:8d:23:02:f3:df:f6 diff --git a/crates/vaportpm-attest/certs/gcp-ekak-root.pem b/crates/vaportpm-attest/certs/gcp-ekak-root.pem new file mode 100644 index 0000000..080fdea --- /dev/null +++ b/crates/vaportpm-attest/certs/gcp-ekak-root.pem @@ -0,0 +1,35 @@ +-----BEGIN CERTIFICATE----- +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 +av16U4E= +-----END CERTIFICATE----- diff --git a/crates/vaportpm-attest/certs/gcp-ekak-root.txt b/crates/vaportpm-attest/certs/gcp-ekak-root.txt new file mode 100644 index 0000000..f3a2554 --- /dev/null +++ b/crates/vaportpm-attest/certs/gcp-ekak-root.txt @@ -0,0 +1,93 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + a6:5d:a4:f9:e3:28:f3:80:35:c3:a7:3d:4f:72:43:2b:df:15:dc + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=US, ST=California, L=Mountain View, O=Google LLC, OU=Google Cloud, CN=EK/AK CA Root + Validity + Not Before: Jul 8 00:40:34 2022 GMT + Not After : Jul 8 05:57:23 2122 GMT + Subject: C=US, ST=California, L=Mountain View, O=Google LLC, OU=Google Cloud, CN=EK/AK CA Root + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (4096 bit) + Modulus: + 00:9d:25:f5:50:a8:c8:96:4b:4a:89:7c:2b:28:4d: + a5:b4:bb:a4:19:ee:89:c1:3a:a6:da:dd:b7:10:16: + 21:1d:93:9c:bc:52:83:13:45:89:1e:dd:ae:da:1f: + c4:8d:2b:b9:c7:a8:08:8a:6c:6b:d3:47:20:aa:f3: + de:c3:3f:37:f1:3c:98:53:47:88:90:2e:c9:57:f7: + a4:fa:66:0e:92:c9:65:0d:6a:7b:cc:87:92:91:92: + b3:72:a3:de:ce:58:61:6b:44:79:c0:95:7f:33:6c: + e0:cb:4a:bc:67:c0:f9:62:81:e3:d0:3a:36:88:bc: + 47:d4:75:1a:89:cb:20:de:b0:49:75:f7:90:fd:62: + 6d:19:f4:34:68:1d:aa:07:e0:45:ee:47:0b:79:0a: + 02:18:d9:ea:c7:13:f0:46:f5:48:69:5a:07:21:b3: + 13:95:a8:19:39:cd:6f:28:9f:93:2c:7d:a1:46:71: + 5d:52:6c:aa:fd:aa:eb:5d:56:dd:aa:5b:3f:17:20: + 7a:ff:33:e4:95:d0:51:ba:eb:43:b1:1b:3b:44:ad: + 2b:50:ec:29:1b:0a:2e:e0:fe:60:bc:63:ac:14:50: + e1:47:88:37:95:87:a7:a1:b4:be:06:22:3a:9b:47: + 9b:a6:0a:67:14:1e:98:ef:e4:54:c2:d1:ce:c9:b5: + 01:81:fb:78:d3:85:7a:47:01:20:a4:63:4a:aa:64: + e6:6e:2e:77:4e:2a:ba:6e:9f:d2:10:63:de:21:3f: + 3d:ef:83:bc:86:d5:84:b8:7d:22:d0:2e:9c:84:86: + 40:f0:3d:75:81:20:7c:0f:ea:5a:2a:fe:a5:4e:cb: + 57:7a:61:a4:b7:86:86:e9:65:f0:6f:f3:62:64:21: + 17:78:5e:83:46:ee:21:1e:f4:69:ab:f3:4f:c8:8b: + 01:0a:f1:54:c3:51:64:91:06:33:02:0d:fe:da:bc: + 62:24:f7:fb:71:19:21:c4:aa:ab:a1:80:1a:38:ae: + 57:0d:cd:22:c5:c2:df:e0:ae:48:91:90:e4:66:c3: + a7:8f:4b:73:a5:56:3c:77:96:78:5b:f0:49:41:a0: + c4:a5:ea:e6:bd:7e:3d:1b:28:62:02:ba:4b:88:61: + 79:c8:17:11:7e:5c:ed:1b:5a:e9:37:05:94:41:5b: + d1:72:bd:7e:9c:b5:76:bf:3d:2d:e1:46:c5:40:69: + 74:3b:ca:c4:34:4b:32:6d:c5:41:e9:fc:ae:64:7f: + 3d:24:5e:11:c2:6c:6a:a6:d4:3f:fa:bb:3d:ce:e4: + d1:17:c5:f4:c6:fa:01:f3:d9:7e:7a:a0:a5:82:50: + f8:81:52:af:d1:23:45:25:b5:c0:f7:0b:d3:7c:e8: + 4c:fe:3b + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Key Usage: critical + Certificate Sign, CRL Sign + X509v3 Extended Key Usage: + Endorsement Key Certificate + X509v3 Basic Constraints: critical + CA:TRUE + X509v3 Subject Key Identifier: + 49:E7:4A:5B:56:29:F5:9D:79:B7:A6:30:3C:03:B2:8F:E7:14:DD:4C + X509v3 Authority Key Identifier: + 49:E7:4A:5B:56:29:F5:9D:79:B7:A6:30:3C:03:B2:8F:E7:14:DD:4C + Signature Algorithm: sha256WithRSAEncryption + Signature Value: + 95:f1:d1:bc:e0:77:08:9a:0b:4e:5d:58:1b:f0:2f:8c:6a:19: + 92:93:4c:ca:9e:57:e6:37:b5:20:20:90:eb:c6:f6:f7:a1:27: + f6:12:14:95:c6:18:ff:e9:ee:10:f4:8f:52:30:c0:de:2c:f0: + 27:c0:c0:7d:5e:11:12:0a:50:ce:aa:21:e9:78:51:dd:38:32: + 7c:75:e3:27:a0:c2:ee:9b:b3:12:0b:73:17:5f:b0:1d:28:54: + a4:1a:1e:dc:72:ae:2f:85:a1:07:7a:ef:c2:4d:92:12:cb:9d: + 06:ef:76:a3:d0:6e:06:37:03:27:f1:96:e4:64:6d:a4:3f:c7: + ef:51:c0:13:ca:c8:f6:32:78:d6:74:b4:16:f9:24:8d:a5:f4: + 82:5b:27:b4:c5:54:5e:2c:1c:b5:af:06:23:44:0e:13:68:a3: + b0:90:70:93:8d:6e:40:6b:74:60:e8:f6:48:69:44:c0:d0:02: + e4:30:53:07:99:19:ef:ee:97:f7:11:84:eb:f7:3e:33:74:c0: + 03:43:8d:a1:7d:7c:ac:d7:e2:7f:de:1c:0b:1f:e6:3d:83:ac: + f2:95:24:00:d4:40:43:86:93:44:1b:e9:06:ba:f2:19:3f:11: + d5:a8:53:06:43:1e:45:01:b7:b5:db:0b:1d:14:fc:7c:90:85: + 50:03:5d:e5:62:d6:b2:2f:37:d3:4e:72:fd:6b:8c:cf:cf:e0: + 14:a7:34:3c:bf:85:f8:cb:9b:0a:c2:75:38:33:d3:46:c7:07: + f7:74:25:4b:8c:c4:32:0e:61:0c:02:24:b3:e7:c3:a1:12:76: + 42:24:4e:aa:8e:c6:02:a6:d3:73:b4:93:29:f1:9c:af:df:a9: + 6b:ad:e7:6b:b7:f7:a3:2d:6c:64:f5:8f:77:10:7c:b7:1e:73: + de:58:21:88:f3:f7:92:d4:31:ff:22:bf:d4:5c:1f:d6:18:31: + 24:71:72:ff:65:1c:68:6b:8e:5d:14:51:4c:fc:84:9e:9a:59: + a7:cd:8a:8a:0b:3d:68:f1:d6:b5:a5:66:db:f2:eb:5d:7d:9f: + bd:3f:62:33:f9:34:0a:8b:cd:02:3f:2b:ce:d1:f1:b1:41:28: + 03:4c:49:9d:23:89:34:fa:5e:f4:f4:5d:8e:c4:e3:ef:fb:b7: + 5e:ee:a1:40:b3:4d:89:d0:47:80:8d:c7:72:68:bb:d3:81:cc: + 31:df:f7:c9:9e:36:e0:e3:ab:4b:7d:b2:96:97:94:d6:87:90: + 1c:5a:cb:13:a8:58:5f:8f:ed:b4:c1:13:6e:61:12:20:a7:76: + ad:38:2c:b4:fa:2d:4a:f9:2e:07:94:e5:8d:b2:98:3d:1f:a5: + ab:b8:50:6a:fd:7a:53:81 diff --git a/crates/vaportpm-attest/certs/pem2txt.sh b/crates/vaportpm-attest/certs/pem2txt.sh new file mode 100755 index 0000000..534222e --- /dev/null +++ b/crates/vaportpm-attest/certs/pem2txt.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# Convert all .pem certificates to human-readable .txt files +# Usage: ./pem2txt.sh + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +for pem in "$SCRIPT_DIR"/*.pem; do + [ -f "$pem" ] || continue + txt="${pem%.pem}.txt" + echo "Converting: $(basename "$pem") -> $(basename "$txt")" + openssl x509 -in "$pem" -text -noout > "$txt" +done + +echo "Done." diff --git a/crates/vaportpm-attest/src/a9n.rs b/crates/vaportpm-attest/src/a9n.rs index 727fbfb..c077dfb 100644 --- a/crates/vaportpm-attest/src/a9n.rs +++ b/crates/vaportpm-attest/src/a9n.rs @@ -3,45 +3,48 @@ //! TPM attestation functionality //! //! Provides high-level attestation operations including: -//! - Retrieving EK certificates from NV RAM //! - Creating and certifying attestation keys (AK) //! - Reading PCR values //! - Generating attestation documents -use anyhow::{anyhow, Context, Result}; -use base64::{engine::general_purpose::STANDARD, Engine as _}; +use anyhow::{anyhow, Result}; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, HashMap}; -use crate::credential::compute_ecc_p256_name; -use crate::{EkOps, NsmOps, NvOps, PcrOps, Tpm, TPM_RH_OWNER}; -use crate::{NV_INDEX_ECC_P256_EK_CERT, NV_INDEX_ECC_P384_EK_CERT, NV_INDEX_RSA_2048_EK_CERT}; - -/// DER SEQUENCE tag with 2-byte length (0x30 0x82) -/// Used to detect valid X.509 certificates in DER format -const DER_SEQUENCE_LONG: [u8; 2] = [0x30, 0x82]; +use crate::cert::{der_to_pem, fetch_cert_chain, DER_SEQUENCE_LONG}; +use crate::{KeyOps, NsmOps, NvOps, PcrOps, PublicKey, Tpm, TPM_RH_ENDORSEMENT}; + +/// GCP AK template NV index (RSA) - used for GCP detection +const GCP_AK_TEMPLATE_NV_INDEX_RSA: u32 = 0x01c10001; +/// GCP AK certificate NV index (ECC) +const GCP_AK_CERT_NV_INDEX_ECC: u32 = 0x01c10002; +/// GCP AK template NV index (ECC) +const GCP_AK_TEMPLATE_NV_INDEX_ECC: u32 = 0x01c10003; +/// GCP TPM manufacturer ID: "GOOG" +const GCP_MANUFACTURER_GOOG: u32 = 0x474F4F47; +/// TPM property: manufacturer +const TPM_PT_MANUFACTURER: u32 = 0x00000105; + +/// Result type for attestation helper functions +/// Contains: (ak_pubkeys, attestation_data, gcp_attestation, ak_handle) +type AttestResult = ( + HashMap, + AttestationData, + Option, + Option, +); /// Complete attestation output containing all TPM attestation data #[derive(Debug, Serialize, Deserialize)] pub struct AttestationOutput { - pub ek_certificates: EkCertificates, + /// Nonce/challenge used for this attestation (hex-encoded) + pub nonce: String, pub pcrs: HashMap>, - pub ek_public_keys: HashMap, - pub signing_key_public_keys: HashMap, + /// Attestation Key public keys (hex-encoded ECC coordinates) + pub ak_pubkeys: HashMap, pub attestation: AttestationContainer, } -/// Endorsement Key certificates in PEM format -#[derive(Debug, Serialize, Deserialize)] -pub struct EkCertificates { - #[serde(skip_serializing_if = "Option::is_none")] - pub rsa_2048: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub ecc_p256: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub ecc_p384: Option, -} - /// ECC public key coordinates #[derive(Debug, Serialize, Deserialize)] pub struct EccPublicKeyCoords { @@ -49,123 +52,105 @@ pub struct EccPublicKeyCoords { pub y: String, } -/// Container for both TPM and optional Nitro attestations +/// Container for both TPM and optional platform-specific attestations #[derive(Debug, Serialize, Deserialize)] pub struct AttestationContainer { pub tpm: HashMap, #[serde(skip_serializing_if = "Option::is_none")] pub nitro: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub gcp: Option, } -/// TPM attestation data (certify response with NIZK proof) +/// GCP Shielded VM attestation data +/// +/// Contains the AK certificate chain from NV RAM. +/// The AK is a long-term key provisioned by Google (not PCR-bound). +/// The Quote data and signature are in `attestation.tpm`. +#[derive(Debug, Serialize, Deserialize)] +pub struct GcpAttestationData { + /// AK certificate chain in PEM format (leaf first, root last) + pub ak_cert_chain: String, +} + +/// TPM attestation data (Quote response) #[derive(Debug, Serialize, Deserialize)] pub struct AttestationData { - /// The nonce/challenge provided to the attestation (hex-encoded) - /// This is duplicated from attest_data.extraData for easy access. - /// Verification MUST check this matches the nonce in attest_data. - pub nonce: String, - /// TPM2B_ATTEST structure from TPM2_Certify (hex-encoded) + /// TPM2B_ATTEST structure from TPM2_Quote (hex-encoded) pub attest_data: String, /// ECDSA signature over attest_data (DER, hex-encoded) pub signature: String, } /// Nitro Enclave attestation data +/// +/// The AK public key and nonce are inside the signed document, +/// and also available at the top level of AttestationOutput. #[derive(Debug, Serialize, Deserialize)] pub struct NitroAttestationData { - pub public_key: String, - pub nonce: String, + /// COSE Sign1 NSM document (hex-encoded) pub document: String, } -/// Convert DER-encoded data to PEM format -pub fn der_to_pem(der: &[u8], label: &str) -> String { - let base64_encoded = STANDARD.encode(der); - let mut pem = format!("-----BEGIN {}-----\n", label); - for chunk in base64_encoded.as_bytes().chunks(64) { - pem.push_str(std::str::from_utf8(chunk).unwrap()); - pem.push('\n'); +/// Detect if running on GCP Shielded VM +/// +/// Detection based on: +/// 1. TPM manufacturer ID is "GOOG" +/// 2. GCP AK template NV index exists +fn is_gcp_tpm(tpm: &mut Tpm) -> bool { + // Check manufacturer + if let Ok(manufacturer) = tpm.get_property(TPM_PT_MANUFACTURER) { + if manufacturer == GCP_MANUFACTURER_GOOG { + // Verify AK template exists + if tpm.nv_readpublic(GCP_AK_TEMPLATE_NV_INDEX_RSA).is_ok() { + return true; + } + } } - pem.push_str(&format!("-----END {}-----\n", label)); - pem + false } /// Generate a complete TPM attestation document /// -/// This function: -/// 1. Retrieves EK certificates from NV RAM -/// 2. Creates the TCG standard EK (matches certificate public key) -/// 3. Detects platform and chooses PCR bank (SHA-384 for Nitro, SHA-256 otherwise) -/// 4. Reads PCR values from the chosen bank -/// 5. Computes PCR policy and creates a signing key (AK) bound to it -/// 6. AK self-certifies via TPM2_Certify (proves AK exists with this policy) -/// 7. If on AWS Nitro, gets Nitro attestation binding the AK public key +/// This function performs platform-specific TPM2_Quote attestation: +/// +/// 1. Detects platform (Nitro or GCP) +/// 2. Reads PCR values +/// 3. Creates or retrieves AK (Attestation Key): +/// - Nitro: Creates long-term AK (bound via Nitro NSM document) +/// - GCP: Recreates AK from Google's template (bound via certificate chain) +/// 4. Signs PCRs with TPM2_Quote +/// 5. Includes platform-specific attestation: +/// - Nitro: COSE Sign1 document binding AK public key +/// - GCP: AK certificate chain from NV RAM /// /// # Arguments /// * `nonce` - User-provided nonce/challenge to include in attestation /// /// # Returns /// JSON-encoded attestation document containing all attestation data +/// +/// # Errors +/// Returns an error if the platform is not recognized (only AWS Nitro and GCP are supported) pub fn attest(nonce: &[u8]) -> Result { let mut tpm = Tpm::open_direct()?; - // Step 1: Retrieve EK certificates from NV RAM - let mut ek_certs = EkCertificates { - rsa_2048: None, - ecc_p256: None, - ecc_p384: None, - }; - - // Try to read RSA EK cert - if let Ok(cert) = tpm.nv_read(NV_INDEX_RSA_2048_EK_CERT) { - if cert.starts_with(&DER_SEQUENCE_LONG) { - ek_certs.rsa_2048 = Some(der_to_pem(&cert, "CERTIFICATE")); - } - } - - // Try to read ECC P-256 EK cert - if let Ok(cert) = tpm.nv_read(NV_INDEX_ECC_P256_EK_CERT) { - if cert.starts_with(&DER_SEQUENCE_LONG) { - ek_certs.ecc_p256 = Some(der_to_pem(&cert, "CERTIFICATE")); - } - } - - // Try to read ECC P-384 EK cert - if let Ok(cert) = tpm.nv_read(NV_INDEX_ECC_P384_EK_CERT) { - if cert.starts_with(&DER_SEQUENCE_LONG) { - ek_certs.ecc_p384 = Some(der_to_pem(&cert, "CERTIFICATE")); - } - } - - // Step 2: Create TCG standard EK (public key should match certificate) - // Note: Standard EK is decrypt-only, cannot sign. We use it only for - // identity verification (comparing public key with certificate). - let ek = tpm.create_standard_ek().context( - "Failed to create standard EK - endorsement hierarchy may require authentication", - )?; + // Step 1: Detect platform + // GCP detection is cheap - just checks for NV index existence + let is_nitro = tpm.is_nitro_tpm()?; + let is_gcp = !is_nitro && is_gcp_tpm(&mut tpm); - let mut ek_public_keys = HashMap::new(); - ek_public_keys.insert( - "ecc_p256".to_string(), - EccPublicKeyCoords { - x: hex::encode(&ek.public_key.x), - y: hex::encode(&ek.public_key.y), - }, - ); + // Step 2: Read all allocated PCRs from all banks + let all_pcrs = tpm.read_all_allocated_pcrs()?; - // Step 3: Detect platform and choose PCR bank - // Nitro TPMs use SHA-384 for signed PCRs, so we bind AK to SHA-384 bank - // Other platforms use SHA-256 - let is_nitro = tpm.is_nitro_tpm()?; + // Choose PCR bank based on platform + // Nitro uses SHA-384 for signed PCRs, others use SHA-256 let pcr_alg = if is_nitro { crate::TpmAlg::Sha384 } else { crate::TpmAlg::Sha256 }; - // Step 4: Read all allocated PCRs from all banks - let all_pcrs = tpm.read_all_allocated_pcrs()?; - // Get PCR values for the chosen bank let pcr_values: Vec<(u8, Vec)> = all_pcrs .iter() @@ -177,72 +162,47 @@ pub fn attest(nonce: &[u8]) -> Result { return Err(anyhow!("No {:?} PCRs allocated on this TPM", pcr_alg)); } - // Only include PCRs relevant to the chain of trust in output + // Build PCRs output let mut pcrs_by_alg: HashMap> = HashMap::new(); let pcr_map = pcrs_by_alg.entry(pcr_alg.name().to_string()).or_default(); for (idx, value) in &pcr_values { pcr_map.insert(*idx, hex::encode(value)); } - // Step 5: Compute policy from PCR values - let auth_policy = Tpm::calculate_pcr_policy_digest(&pcr_values, pcr_alg)?; - - // Create signing key (AK) bound to this policy - let signing_key = tpm.create_primary_ecc_key_with_policy(TPM_RH_OWNER, &auth_policy)?; - - let mut signing_key_public_keys = HashMap::new(); - signing_key_public_keys.insert( - "ecc_p256".to_string(), - EccPublicKeyCoords { - x: hex::encode(&signing_key.public_key.x), - y: hex::encode(&signing_key.public_key.y), - }, - ); - - // Compute AK name (used for PCR policy verification) - let _ak_name = compute_ecc_p256_name( - &signing_key.public_key.x, - &signing_key.public_key.y, - &auth_policy, - ); - - // Step 6: AK self-certifies via TPM2_Certify (produces TPM2B_ATTEST + signature) - // This produces TPM2B_ATTEST containing the AK's name (which includes authPolicy) - let cert_result = tpm.certify( - signing_key.handle, // object to certify (AK itself) - signing_key.handle, // signing key (AK) - nonce, // qualifying data (becomes extraData in TPM2B_ATTEST) - )?; - - let attestation_data = AttestationData { - nonce: hex::encode(nonce), - attest_data: hex::encode(&cert_result.attest_data), - signature: hex::encode(&cert_result.signature), + // Step 5: Create or retrieve AK and sign PCRs with TPM2_Quote + let (signing_key_public_keys, attestation_data, gcp_attestation, ak_handle) = if is_gcp { + // GCP path: recreate AK from Google's template + attest_gcp(&mut tpm, nonce, &pcr_values, pcr_alg)? + } else if is_nitro { + // Nitro path: create long-term AK, use TPM2_Quote + attest_nitro(&mut tpm, nonce, &pcr_values, pcr_alg)? + } else { + return Err(anyhow!( + "Unknown platform - only AWS Nitro and GCP Shielded VM are supported" + )); }; let mut tpm_attestations = HashMap::new(); tpm_attestations.insert("ecc_p256".to_string(), attestation_data); - // Get Nitro attestation if available (we already detected Nitro earlier) + // Step 6: Get Nitro attestation if on AWS let nitro_attestation = if is_nitro { - // Encode signing key public key in SECG format (0x04 || X || Y) - let mut public_key_secg = - Vec::with_capacity(1 + signing_key.public_key.x.len() + signing_key.public_key.y.len()); - public_key_secg.push(0x04); // Uncompressed point indicator - public_key_secg.extend_from_slice(&signing_key.public_key.x); - public_key_secg.extend_from_slice(&signing_key.public_key.y); - - match tpm.nsm_attest( - None, // user_data - Some(nonce.to_vec()), // nonce - Some(public_key_secg.clone()), // public_key - ) { - Ok(document) => Some(NitroAttestationData { - public_key: hex::encode(&public_key_secg), - nonce: hex::encode(nonce), - document: hex::encode(&document), - }), - Err(_e) => None, + if let Some(pk) = signing_key_public_keys.get("ecc_p256") { + let public_key_hex = format!("04{}{}", pk.x, pk.y); + let public_key_bytes = hex::decode(&public_key_hex)?; + + match tpm.nsm_attest( + None, // user_data + Some(nonce.to_vec()), // nonce + Some(public_key_bytes), // public_key + ) { + Ok(document) => Some(NitroAttestationData { + document: hex::encode(&document), + }), + Err(_e) => None, + } + } else { + None } } else { None @@ -251,18 +211,19 @@ pub fn attest(nonce: &[u8]) -> Result { let attestation = AttestationContainer { tpm: tpm_attestations, nitro: nitro_attestation, + gcp: gcp_attestation, }; // Cleanup TPM handles - tpm.flush_context(signing_key.handle)?; - tpm.flush_context(ek.handle)?; + if let Some(handle) = ak_handle { + tpm.flush_context(handle)?; + } - // Step 7: Build and output JSON + // Step 5: Build and output JSON let output = AttestationOutput { - ek_certificates: ek_certs, + nonce: hex::encode(nonce), pcrs: pcrs_by_alg, - ek_public_keys, - signing_key_public_keys, + ak_pubkeys: signing_key_public_keys, attestation, }; @@ -270,3 +231,144 @@ pub fn attest(nonce: &[u8]) -> Result { Ok(json) } + +/// Nitro attestation path: create restricted AK and use TPM2_Quote +/// +/// Creates a TCG-compliant restricted AK in the endorsement hierarchy, then uses +/// TPM2_Quote to sign the PCR values. The AK is bound to the Nitro NSM document. +fn attest_nitro( + tpm: &mut Tpm, + nonce: &[u8], + pcr_values: &[(u8, Vec)], + pcr_alg: crate::TpmAlg, +) -> Result { + // Create restricted AK in endorsement hierarchy (TCG-compliant AK profile) + // Trust comes from Nitro NSM document binding the AK public key + let signing_key = tpm.create_restricted_ak(TPM_RH_ENDORSEMENT)?; + + let mut signing_key_public_keys = HashMap::new(); + signing_key_public_keys.insert( + "ecc_p256".to_string(), + EccPublicKeyCoords { + x: hex::encode(&signing_key.public_key.x), + y: hex::encode(&signing_key.public_key.y), + }, + ); + + // Build PCR selection bitmap for Quote + let pcr_bitmap = build_pcr_bitmap(pcr_values); + let pcr_selection = vec![(pcr_alg, pcr_bitmap.as_slice())]; + + // Perform TPM2_Quote - signs PCR values with AK + let quote_result = tpm.quote(signing_key.handle, nonce, &pcr_selection)?; + + let attestation_data = AttestationData { + attest_data: hex::encode("e_result.attest_data), + signature: hex::encode("e_result.signature), + }; + + Ok(( + signing_key_public_keys, + attestation_data, + None, + Some(signing_key.handle), + )) +} + +/// GCP attestation path: recreate AK from template and use TPM2_Quote +fn attest_gcp( + tpm: &mut Tpm, + nonce: &[u8], + pcr_values: &[(u8, Vec)], + pcr_alg: crate::TpmAlg, +) -> Result { + // Read ECC AK template from NV RAM (prefer ECC over RSA for ECDSA signing) + let ak_template = tpm.nv_read(GCP_AK_TEMPLATE_NV_INDEX_ECC)?; + + // Recreate AK from template in endorsement hierarchy + let ak_result = tpm.create_primary_from_template(TPM_RH_ENDORSEMENT, &ak_template)?; + + // Extract ECC public key coordinates + let signing_key_public_keys = match &ak_result.public_key { + PublicKey::Ecc(ecc) => { + let mut pks = HashMap::new(); + pks.insert( + "ecc_p256".to_string(), + EccPublicKeyCoords { + x: hex::encode(&ecc.x), + y: hex::encode(&ecc.y), + }, + ); + pks + } + PublicKey::Rsa(_) => { + return Err(anyhow!( + "GCP ECC AK template unexpectedly created an RSA key" + )); + } + }; + + // Build PCR selection bitmap for Quote + // Include all PCRs from pcr_values + let pcr_bitmap = build_pcr_bitmap(pcr_values); + let pcr_selection = vec![(pcr_alg, pcr_bitmap.as_slice())]; + + // Perform TPM2_Quote - signs PCR values with AK + let quote_result = tpm.quote(ak_result.handle, nonce, &pcr_selection)?; + + // Read AK certificate chain from NV RAM + let ak_cert_chain = read_gcp_ak_cert_chain(tpm)?; + + let attestation_data = AttestationData { + attest_data: hex::encode("e_result.attest_data), + signature: hex::encode("e_result.signature), + }; + + let gcp_attestation = Some(GcpAttestationData { ak_cert_chain }); + + Ok(( + signing_key_public_keys, + attestation_data, + gcp_attestation, + Some(ak_result.handle), + )) +} + +/// Build PCR bitmap from list of (index, value) pairs +fn build_pcr_bitmap(pcr_values: &[(u8, Vec)]) -> Vec { + // TPM uses 3 bytes for PCR selection (24 PCRs max) + let mut bitmap = vec![0u8; 3]; + for (idx, _) in pcr_values { + if *idx < 24 { + let byte_idx = (*idx / 8) as usize; + let bit_idx = *idx % 8; + bitmap[byte_idx] |= 1 << bit_idx; + } + } + bitmap +} + +/// Read GCP ECC AK certificate chain from NV RAM and fetch issuer certs +fn read_gcp_ak_cert_chain(tpm: &mut Tpm) -> Result { + // Read ECC AK certificate (matches the ECC AK template we use) + let ak_cert = tpm.nv_read(GCP_AK_CERT_NV_INDEX_ECC)?; + + if !ak_cert.starts_with(&DER_SEQUENCE_LONG) { + return Err(anyhow!( + "GCP AK certificate is not in DER format (got {:02x?})", + &ak_cert[..ak_cert.len().min(4)] + )); + } + + // Build full chain by fetching issuer certs via AIA + let chain = fetch_cert_chain(&ak_cert)?; + + // Convert all certs to PEM and concatenate + let pem_chain: String = chain + .iter() + .map(|cert| der_to_pem(cert, "CERTIFICATE")) + .collect::>() + .join(""); + + Ok(pem_chain) +} diff --git a/crates/vaportpm-attest/src/bin/attest.rs b/crates/vaportpm-attest/src/bin/attest.rs index d7fe8d5..d644db5 100644 --- a/crates/vaportpm-attest/src/bin/attest.rs +++ b/crates/vaportpm-attest/src/bin/attest.rs @@ -3,13 +3,27 @@ //! Generate TPM attestation document //! //! Outputs a JSON attestation document to stdout. +//! +//! Usage: +//! vaportpm-attest [NONCE_HEX] +//! +//! Nonce is determined by (in order of priority): +//! 1. Command-line argument (hex-encoded) +//! 2. Stdin if not a tty (hex-encoded, whitespace trimmed) +//! 3. Random 32-byte nonce +use std::io::{self, IsTerminal, Read}; use std::process::ExitCode; use vaportpm_attest::attest; fn main() -> ExitCode { - // Generate a random nonce if none provided - let nonce: Vec = (0..32).map(|_| rand()).collect(); + let nonce = match get_nonce() { + Ok(n) => n, + Err(e) => { + eprintln!("error: {}", e); + return ExitCode::FAILURE; + } + }; match attest(&nonce) { Ok(json) => { @@ -23,20 +37,42 @@ fn main() -> ExitCode { } } -/// Simple random byte generator using /dev/urandom -fn rand() -> u8 { - use std::fs::File; - use std::io::Read; +/// Get nonce from command-line argument, stdin, or generate random +fn get_nonce() -> Result, String> { + let args: Vec = std::env::args().collect(); - thread_local! { - static URANDOM: std::cell::RefCell> = 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/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..e1d7d72 --- /dev/null +++ b/crates/vaportpm-attest/src/cert.rs @@ -0,0 +1,340 @@ +// 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::ext::pkix::name::GeneralName; +use x509_cert::ext::pkix::{AuthorityInfoAccessSyntax, AuthorityKeyIdentifier}; +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 { + let aki = AuthorityKeyIdentifier::from_der(ext.extn_value.as_bytes()).ok()?; + return aki.key_identifier.map(|ki| ki.as_bytes().to_vec()); + } + } + None +} + +/// 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 { + 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()); + } + } + } + } + } + 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) +} + +#[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/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..105578b 100644 --- a/crates/vaportpm-attest/src/ek.rs +++ b/crates/vaportpm-attest/src/ek.rs @@ -1,68 +1,66 @@ // 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 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) + /// 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 { - fn create_primary_ecc_key(&mut self, hierarchy: u32) -> Result { - let public_area = build_ecc_public_area(); +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) @@ -114,11 +112,11 @@ 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_ecc_key(&mut self, hierarchy: u32) -> Result { + let public_area = build_ecc_public_area(); let command = CommandBuffer::new() - .write_u32(TPM_RH_ENDORSEMENT) + .write_u32(hierarchy) .write_auth_empty_pw() // inSensitive (TPM2B_SENSITIVE_CREATE) .write_u16(4) @@ -143,6 +141,8 @@ impl EkOps for Tpm { // 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 @@ -165,94 +165,95 @@ 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"); - } - + fn create_primary_from_template( + &mut self, + hierarchy: u32, + template: &[u8], + ) -> Result { let command = CommandBuffer::new() - .write_u32(key_handle) + .write_u32(hierarchy) .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); + // inSensitive (TPM2B_SENSITIVE_CREATE) + .write_u16(4) + .write_u16(0) // userAuth size = 0 + .write_u16(0) // data size = 0 + // inPublic (TPM2B_PUBLIC) - the raw template from NV RAM + .write_tpm2b(template) + // 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(); - // TPMT_SIGNATURE - let sig_alg = resp.read_u16()?; - if sig_alg != TpmAlg::EcDsa as u16 { - bail!("Unexpected signature algorithm: 0x{:04X}", sig_alg); - } + // Read outPublic (TPM2B_PUBLIC) + let public_size = resp.read_u16()? as usize; + let public_data = resp.read_bytes(public_size)?; - let hash_alg = resp.read_u16()?; - if hash_alg != TpmAlg::Sha256 as u16 { - bail!("Unexpected hash algorithm: 0x{:04X}", hash_alg); - } + // Parse the public key based on algorithm type + let public_key = parse_public_key(public_data)?; - // TPMS_SIGNATURE_ECC - let r = resp.read_tpm2b()?; - let s = resp.read_tpm2b()?; + // Save the raw public bytes for later use + let public_bytes = public_data.to_vec(); - // Verify we read exactly parameter_size bytes + // Skip remaining CreatePrimary output parameters let bytes_read = resp.offset() - param_start; - if bytes_read != parameter_size as usize { + 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 in Sign: TPM said {} bytes, we read {} bytes", + "Parameter size mismatch: TPM said {} bytes, we read {} bytes", parameter_size, - bytes_read + final_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 +275,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,76 +283,49 @@ 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 -fn build_ecc_public_area() -> Vec { +/// 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() - .decrypt() + .restricted() .sign_encrypt(); CommandBuffer::new() @@ -362,17 +336,18 @@ fn build_ecc_public_area() -> Vec { .write_u16(0) // authPolicy (empty) // parameters (TPMS_ECC_PARMS) .write_u16(TpmAlg::Null as u16) // symmetric - .write_u16(TpmAlg::Null as u16) // scheme + .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 + // 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 with authPolicy -fn build_ecc_public_area_with_policy(auth_policy: &[u8]) -> 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() .fixed_parent() @@ -386,7 +361,7 @@ fn build_ecc_public_area_with_policy(auth_policy: &[u8]) -> Vec { .write_u16(TpmAlg::Ecc as u16) // type .write_u16(TpmAlg::Sha256 as u16) // nameAlg .write_u32(attrs.value()) // objectAttributes - .write_tpm2b(auth_policy) // authPolicy + .write_u16(0) // authPolicy (empty) // parameters (TPMS_ECC_PARMS) .write_u16(TpmAlg::Null as u16) // symmetric .write_u16(TpmAlg::Null as u16) // scheme @@ -398,45 +373,93 @@ fn build_ecc_public_area_with_policy(auth_policy: &[u8]) -> Vec { .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..9748104 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 } @@ -40,9 +39,6 @@ name = "vaportpm_verify" path = "src/lib.rs" [[bin]] -name = "selftest-verify" -path = "src/bin/selftest-verify.rs" +name = "vaportpm-verify" +path = "src/bin/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..ac35899 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 @@ -81,17 +91,13 @@ 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 -### 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 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. -### 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..1849e9b 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,18 @@ 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 +376,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 +390,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 +434,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 +459,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 +492,51 @@ mod tests { ); } - let result = verify_attestation_output(&output, fixture_time()); - // Tampering is detected at policy verification: AK's authPolicy doesn't match PCR values + let result = verify_attestation_output(&output, nitro_fixture_time()); + // Tampering is detected: claimed PCR values don't match signed values in Nitro document assert!( - matches!(result, Err(VerifyError::InvalidAttest(_))), + matches!(result, Err(VerifyError::SignatureInvalid(_))), "Should reject tampered PCR values, got: {:?}", result ); } } + +#[cfg(test)] +mod tdx_tests { + use super::*; + + // Timestamp for GCP TDX test fixture (Feb 3, 2026 when certificates are valid) + const GCP_TDX_FIXTURE_TIMESTAMP_SECS: u64 = 1770091200; // Feb 3, 2026 08:00:00 UTC + + fn gcp_tdx_fixture_time() -> UnixTime { + UnixTime::since_unix_epoch(std::time::Duration::from_secs( + GCP_TDX_FIXTURE_TIMESTAMP_SECS, + )) + } + + #[test] + fn test_verify_gcp_tdx_fixture() { + let fixture = include_str!("../test-gcp-tdx-fixture.json"); + let output: AttestationOutput = + serde_json::from_str(fixture).expect("Failed to parse test-gcp-tdx-fixture.json"); + + let result = verify_attestation_output(&output, gcp_tdx_fixture_time()) + .expect("Verification should succeed"); + + // Should be GCP + assert_eq!(result.provider, CloudProvider::Gcp); + + // Should have the nonce from the Quote + assert_eq!( + result.nonce, + "6424632e79ec068f2189adf46d121b9a10f758c45a18c52f630da14600d4317b" + ); + + // Should have PCR values + assert!(!result.pcrs.is_empty()); + + // Root pubkey hash should match the known GCP root (same as AMD) + assert_eq!(result.root_pubkey_hash.len(), 64); + } +} diff --git a/crates/vaportpm-verify/src/nitro.rs b/crates/vaportpm-verify/src/nitro.rs index 47470e4..3c8de1b 100644 --- a/crates/vaportpm-verify/src/nitro.rs +++ b/crates/vaportpm-verify/src/nitro.rs @@ -16,7 +16,7 @@ use x509_cert::Certificate; use pki_types::UnixTime; use crate::error::VerifyError; -use crate::x509::{extract_public_key, validate_cert_chain}; +use crate::x509::{extract_public_key, validate_tpm_cert_chain}; /// Result of successful Nitro attestation verification /// @@ -118,8 +118,10 @@ pub fn verify_nitro_attestation( let leaf_cert = Certificate::from_der(&cert_der) .map_err(|e| VerifyError::CertificateParse(format!("Invalid leaf cert: {}", e)))?; + // Build chain in leaf-to-root order + // AWS cabundle is ordered [root, ..., issuer], so we reverse it let mut chain = vec![leaf_cert]; - for ca_der in cabundle { + for ca_der in cabundle.into_iter().rev() { let ca_cert = Certificate::from_der(&ca_der) .map_err(|e| VerifyError::CertificateParse(format!("Invalid CA cert: {}", e)))?; chain.push(ca_cert); @@ -130,9 +132,9 @@ pub fn verify_nitro_attestation( let leaf_pubkey = extract_public_key(&chain[0])?; verify_cose_signature(&cose_sign1, &leaf_pubkey, payload)?; - // Validate certificate chain using webpki - // This validates signatures, dates, and returns root's public key hash - let chain_result = validate_cert_chain(&chain, time)?; + // Validate certificate chain + // This validates signatures, dates, extensions, and returns root's public key hash + let chain_result = validate_tpm_cert_chain(&chain, time)?; let root_pubkey_hash = chain_result.root_pubkey_hash; Ok(NitroVerifyResult { diff --git a/crates/vaportpm-verify/src/tpm.rs b/crates/vaportpm-verify/src/tpm.rs index 55b54f0..6a4d888 100644 --- a/crates/vaportpm-verify/src/tpm.rs +++ b/crates/vaportpm-verify/src/tpm.rs @@ -2,35 +2,11 @@ //! TPM attestation parsing and verification -use std::collections::BTreeMap; - use ecdsa::signature::hazmat::PrehashVerifier; use p256::ecdsa::{Signature as P256Signature, VerifyingKey as P256VerifyingKey}; -use serde::Serialize; use sha2::{Digest, Sha256}; -use pki_types::UnixTime; - use crate::error::VerifyError; -use crate::x509::{extract_public_key, parse_and_validate_cert_chain, parse_cert_chain_pem}; - -// Import from vaportpm_attest (single source of truth) -use vaportpm_attest::{PcrOps, Tpm, TpmAlg}; - -/// Result of successful TPM attestation verification -/// -/// This struct is only returned when verification succeeds. -/// Verification checks: -/// 1. EK public key from attestation matches EK certificate's public key -/// 2. AK signature over nonce is valid -/// 3. Certificate chain validates to root CA -#[derive(Debug, Serialize)] -pub struct TpmVerifyResult { - /// The nonce that was signed (hex-encoded) - pub nonce: String, - /// SHA-256 hash of the root CA's public key (hex string) - pub root_pubkey_hash: String, -} /// Verify ECDSA-SHA256 signature over a message pub fn verify_ecdsa_p256( @@ -54,254 +30,44 @@ pub fn verify_ecdsa_p256( .map_err(|e| VerifyError::SignatureInvalid(format!("Signature verification failed: {}", e))) } -/// Verify TPM attestation -/// -/// This verification approach works with TCG standard EKs (decrypt-only, cannot sign). -/// It verifies: -/// 1. The EK certificate chain validates to a root CA -/// 2. The EK public key from the attestation output matches the certificate's EK public key -/// 3. The AK's signature over the nonce is valid -/// -/// # Arguments -/// * `nonce_hex` - The nonce/attest_data as hex string -/// * `signature_hex` - DER-encoded ECDSA signature as hex string (from AK) -/// * `ak_pubkey_x_hex` - AK public key X coordinate (hex) -/// * `ak_pubkey_y_hex` - AK public key Y coordinate (hex) -/// * `ek_pubkey_x_hex` - EK public key X coordinate from attestation output (hex) -/// * `ek_pubkey_y_hex` - EK public key Y coordinate from attestation output (hex) -/// * `ek_certs_pem` - EK certificate chain in PEM format -/// -/// # Returns -/// Verification result with nonce and root public key hash. -/// Returns an error if signature, chain validation, or EK pubkey matching fails. -pub fn verify_tpm_attestation( - nonce_hex: &str, - signature_hex: &str, - ak_pubkey_x_hex: &str, - ak_pubkey_y_hex: &str, - ek_pubkey_x_hex: &str, - ek_pubkey_y_hex: &str, - ek_certs_pem: &str, -) -> Result { - // Decode hex inputs - let nonce = hex::decode(nonce_hex)?; - let signature = hex::decode(signature_hex)?; - let ak_x = hex::decode(ak_pubkey_x_hex)?; - let ak_y = hex::decode(ak_pubkey_y_hex)?; - let ek_x = hex::decode(ek_pubkey_x_hex)?; - let ek_y = hex::decode(ek_pubkey_y_hex)?; - - // Construct AK public key in SEC1 uncompressed format: 0x04 || X || Y - let mut ak_pubkey = vec![0x04]; - ak_pubkey.extend(&ak_x); - ak_pubkey.extend(&ak_y); - - // Construct EK public key in SEC1 uncompressed format: 0x04 || X || Y - let mut ek_pubkey = vec![0x04]; - ek_pubkey.extend(&ek_x); - ek_pubkey.extend(&ek_y); - - // Parse the certificate chain to extract leaf cert's public key - let chain = parse_cert_chain_pem(ek_certs_pem)?; - - // Extract EK public key from the leaf certificate - let cert_ek_pubkey = extract_public_key(&chain[0])?; - - // Compare EK public key from attestation output with certificate's EK public key - if ek_pubkey != cert_ek_pubkey { - return Err(VerifyError::SignatureInvalid( - "EK public key from attestation does not match certificate's EK public key".into(), - )); - } - - // Verify the AK's signature over the nonce - // Note: The AK signs SHA-256(nonce), not the raw nonce - verify_ecdsa_p256(&nonce, &signature, &ak_pubkey)?; - - // Validate the certificate chain and get root's public key hash - // This uses webpki for signature and date validation - let chain_result = parse_and_validate_cert_chain(ek_certs_pem, UnixTime::now())?; - let root_pubkey_hash = chain_result.root_pubkey_hash; - - Ok(TpmVerifyResult { - nonce: nonce_hex.to_string(), - root_pubkey_hash, - }) -} - -/// Verify TPM signature only (without EK certificate chain validation) -/// -/// This is used in the Nitro path where trust comes from the Nitro attestation -/// binding the TPM signing key. No EK certificate validation is needed. -/// -/// # Arguments -/// * `nonce_hex` - The nonce/attest_data as hex string -/// * `signature_hex` - DER-encoded ECDSA signature as hex string (from AK) -/// * `ak_pubkey_x_hex` - AK public key X coordinate (hex) -/// * `ak_pubkey_y_hex` - AK public key Y coordinate (hex) -/// -/// # Returns -/// The verified nonce (hex-encoded) if signature is valid. -pub fn verify_tpm_signature_only( - nonce_hex: &str, - signature_hex: &str, - ak_pubkey_x_hex: &str, - ak_pubkey_y_hex: &str, -) -> Result { - // Decode hex inputs - let nonce = hex::decode(nonce_hex)?; - let signature = hex::decode(signature_hex)?; - let ak_x = hex::decode(ak_pubkey_x_hex)?; - let ak_y = hex::decode(ak_pubkey_y_hex)?; - - // Construct AK public key in SEC1 uncompressed format: 0x04 || X || Y - let mut ak_pubkey = vec![0x04]; - ak_pubkey.extend(&ak_x); - ak_pubkey.extend(&ak_y); - - // Verify the AK's signature over the nonce - verify_ecdsa_p256(&nonce, &signature, &ak_pubkey)?; - - Ok(nonce_hex.to_string()) -} - -/// Calculate the expected PCR policy digest from PCR values -/// -/// This calculates the TPM2 PolicyPCR digest that would be used as an -/// authPolicy for a key bound to the given PCR values. -/// -/// Uses the same implementation as vaportpm_attest to ensure consistency. -/// -/// # Arguments -/// * `pcrs` - Map of PCR index to hex-encoded PCR value -/// * `pcr_alg` - The hash algorithm of the PCR bank (determines expected PCR size) -/// -/// # Returns -/// The expected policy digest as a hex-encoded string -/// -/// # Example -/// ```ignore -/// let mut pcrs = BTreeMap::new(); -/// pcrs.insert(0, "0000...".to_string()); // 64 hex chars for SHA-256 -/// pcrs.insert(1, "0000...".to_string()); -/// let policy = calculate_pcr_policy(&pcrs, TpmAlg::Sha256)?; -/// ``` -pub fn calculate_pcr_policy( - pcrs: &BTreeMap, - pcr_alg: TpmAlg, -) -> Result { - if pcrs.is_empty() { - return Err(VerifyError::InvalidAttest("No PCR values provided".into())); - } - - // Determine expected PCR size based on algorithm - let expected_size = match pcr_alg { - TpmAlg::Sha256 => 32, - TpmAlg::Sha384 => 48, - _ => { - return Err(VerifyError::InvalidAttest(format!( - "Unsupported PCR algorithm: {:?}", - pcr_alg - ))) - } - }; - - // Validate and convert PCR values from hex strings to bytes - // PCRs must be in sorted order (BTreeMap guarantees this) - let mut pcr_values: Vec<(u8, Vec)> = Vec::with_capacity(pcrs.len()); - for (&idx, value_hex) in pcrs.iter() { - if idx > 23 { - return Err(VerifyError::InvalidAttest(format!( - "PCR index {} out of range (max 23)", - idx - ))); - } - let value_bytes = hex::decode(value_hex)?; - if value_bytes.len() != expected_size { - return Err(VerifyError::InvalidAttest(format!( - "PCR {} has invalid length for {:?}: expected {} bytes, got {}", - idx, - pcr_alg, - expected_size, - value_bytes.len() - ))); - } - pcr_values.push((idx, value_bytes)); - } - - // Use vaportpm_attest's implementation (single source of truth) - let policy_digest = Tpm::calculate_pcr_policy_digest(&pcr_values, pcr_alg) - .map_err(|e| VerifyError::InvalidAttest(format!("PCR policy calculation failed: {}", e)))?; - - Ok(hex::encode(policy_digest)) -} - -/// Verify that a policy digest matches the expected PCR values -/// -/// This is useful for verifying that an AK's authPolicy (if known) matches -/// the PCR values reported in an attestation. -/// -/// # Arguments -/// * `expected_policy_hex` - The expected policy digest (hex string) -/// * `pcrs` - The PCR values to verify against -/// * `pcr_alg` - The hash algorithm of the PCR bank -/// -/// # Returns -/// Ok(()) if the policy matches, error otherwise -pub fn verify_pcr_policy( - expected_policy_hex: &str, - pcrs: &BTreeMap, - pcr_alg: TpmAlg, -) -> Result<(), VerifyError> { - let calculated_policy = calculate_pcr_policy(pcrs, pcr_alg)?; - - if calculated_policy != expected_policy_hex { - return Err(VerifyError::InvalidAttest(format!( - "PCR policy mismatch: expected {}, calculated {}", - expected_policy_hex, calculated_policy - ))); - } - - Ok(()) -} - // ============================================================================= -// TPM2B_ATTEST parsing and NIZK verification +// TPM2B_ATTEST parsing (Quote only - Certify removed) // ============================================================================= /// TPM_GENERATED magic value (0xff544347 = "ΓΏTCG") const TPM_GENERATED_VALUE: u32 = 0xff544347; -/// TPM_ST_ATTEST_CERTIFY structure type -const TPM_ST_ATTEST_CERTIFY: u16 = 0x8017; +/// TPM_ST_ATTEST_QUOTE structure type +const TPM_ST_ATTEST_QUOTE: u16 = 0x8018; /// Size of TPMS_CLOCK_INFO structure: clock(8) + resetCount(4) + restartCount(4) + safe(1) const TPMS_CLOCK_INFO_SIZE: usize = 17; -/// Parsed TPMS_ATTEST structure (from TPM2_Certify) +/// Parsed TPMS_ATTEST structure (from TPM2_Quote) #[derive(Debug)] -pub struct TpmAttestInfo { +pub struct TpmQuoteInfo { /// Nonce/qualifying data from extraData field (raw bytes) pub nonce: Vec, - /// Name of the certified object (nameAlg || H(public_area)) - pub certified_name: Vec, /// Name of the signing key pub signer_name: Vec, + /// PCR selection (algorithm, PCR indices as bitmap) + pub pcr_select: Vec<(u16, Vec)>, + /// Digest of the selected PCRs (hash of concatenated PCR values) + pub pcr_digest: Vec, } -/// Parse TPM2B_ATTEST structure (CERTIFY type) +/// Parse TPM2B_ATTEST structure (QUOTE type) /// /// TPM2B_ATTEST contains a TPMS_ATTEST structure which includes: /// - magic: 0xff544347 (TPM_GENERATED_VALUE) -/// - type: 0x8017 (TPM_ST_ATTEST_CERTIFY) +/// - type: 0x8018 (TPM_ST_ATTEST_QUOTE) /// - qualifiedSigner: TPM2B_NAME /// - extraData: TPM2B_DATA (our nonce) /// - clockInfo: TPMS_CLOCK_INFO /// - firmwareVersion: u64 -/// - attested.certify.name: TPM2B_NAME (certified object's name) -/// - attested.certify.qualifiedName: TPM2B_NAME -pub fn parse_tpm2b_attest(data: &[u8]) -> Result { - // Use a cursor to track position with overflow-safe arithmetic +/// - attested.quote.pcrSelect: TPML_PCR_SELECTION (PCRs that were quoted) +/// - attested.quote.pcrDigest: TPM2B_DIGEST (hash of PCR values) +pub fn parse_quote_attest(data: &[u8]) -> Result { let mut cursor = SafeCursor::new(data); // magic (4 bytes) @@ -317,10 +83,10 @@ pub fn parse_tpm2b_attest(data: &[u8]) -> Result { // type (2 bytes) let type_bytes = cursor.read_bytes(2, "type")?; let attest_type = u16::from_be_bytes(type_bytes.try_into().unwrap()); - if attest_type != TPM_ST_ATTEST_CERTIFY { + if attest_type != TPM_ST_ATTEST_QUOTE { return Err(VerifyError::InvalidAttest(format!( - "Invalid attest type: expected 0x{:04x} (CERTIFY), got 0x{:04x}", - TPM_ST_ATTEST_CERTIFY, attest_type + "Invalid attest type: expected 0x{:04x} (QUOTE), got 0x{:04x}", + TPM_ST_ATTEST_QUOTE, attest_type ))); } @@ -336,14 +102,18 @@ pub fn parse_tpm2b_attest(data: &[u8]) -> Result { // firmwareVersion (8 bytes) - skip it cursor.skip(8, "firmwareVersion")?; - // attested (TPMS_CERTIFY_INFO) - // - name (TPM2B_NAME) - let certified_name = cursor.read_tpm2b("certifiedName")?; + // attested (TPMS_QUOTE_INFO) + // - pcrSelect (TPML_PCR_SELECTION) + let pcr_select = cursor.read_pcr_selection("pcrSelect")?; + + // - pcrDigest (TPM2B_DIGEST) + let pcr_digest = cursor.read_tpm2b("pcrDigest")?; - Ok(TpmAttestInfo { + Ok(TpmQuoteInfo { nonce, - certified_name, signer_name, + pcr_select, + pcr_digest, }) } @@ -390,6 +160,67 @@ impl<'a> SafeCursor<'a> { let data = self.read_bytes(size, field)?; Ok(data.to_vec()) } + + /// Read a u16 value (big-endian) + fn read_u16(&mut self, field: &str) -> Result { + let bytes = self.read_bytes(2, field)?; + Ok(u16::from_be_bytes(bytes.try_into().unwrap())) + } + + /// Read a u32 value (big-endian) + fn read_u32(&mut self, field: &str) -> Result { + let bytes = self.read_bytes(4, field)?; + Ok(u32::from_be_bytes(bytes.try_into().unwrap())) + } + + /// Read a u8 value + fn read_u8(&mut self, field: &str) -> Result { + let bytes = self.read_bytes(1, field)?; + Ok(bytes[0]) + } + + /// Read TPML_PCR_SELECTION structure + /// + /// Returns a list of (algorithm, PCR bitmap) pairs + fn read_pcr_selection(&mut self, field: &str) -> Result)>, VerifyError> { + let count = self.read_u32(&format!("{}.count", field))?; + + // Sanity check: count should be reasonable (max ~16 different algorithms) + if count > 16 { + return Err(VerifyError::InvalidAttest(format!( + "{}: count {} exceeds reasonable maximum", + field, count + ))); + } + + let mut selections = Vec::with_capacity(count as usize); + for i in 0..count { + // TPMS_PCR_SELECTION: + // - hash (2 bytes) - algorithm + let hash_alg = self.read_u16(&format!("{}.selection[{}].hash", field, i))?; + + // - sizeofSelect (1 byte) - bitmap size (typically 3 for 24 PCRs) + let bitmap_size = self.read_u8(&format!("{}.selection[{}].sizeofSelect", field, i))?; + + // Sanity check: bitmap size should be reasonable (max 32 for 256 PCRs) + if bitmap_size > 32 { + return Err(VerifyError::InvalidAttest(format!( + "{}.selection[{}].sizeofSelect {} exceeds maximum", + field, i, bitmap_size + ))); + } + + // - pcrSelect (variable) - bitmap + let bitmap = self.read_bytes( + bitmap_size as usize, + &format!("{}.selection[{}].pcrSelect", field, i), + )?; + + selections.push((hash_alg, bitmap.to_vec())); + } + + Ok(selections) + } } #[cfg(test)] @@ -398,7 +229,6 @@ mod tests { use ecdsa::signature::hazmat::PrehashSigner; use p256::ecdsa::SigningKey; use sha2::Sha256; - use vaportpm_attest::TpmAlg; /// Generate a test P-256 key pair and sign a message /// The signature is over SHA256(message) to match what verify_ecdsa_p256 expects @@ -596,247 +426,4 @@ mod tests { let result = verify_ecdsa_p256(&message, &signature, &pubkey); assert!(result.is_ok()); } - - // === PCR Policy Calculation Tests === - - #[test] - fn test_calculate_pcr_policy_single_pcr() { - // Test with a single PCR (all zeros) - let mut pcrs = BTreeMap::new(); - let pcr0 = "0".repeat(64); // 32 bytes of zeros as hex - pcrs.insert(0, pcr0); - - let result = calculate_pcr_policy(&pcrs, TpmAlg::Sha256); - assert!(result.is_ok()); - - let policy = result.unwrap(); - // Policy should be a 64-character hex string (32 bytes) - assert_eq!(policy.len(), 64); - } - - #[test] - fn test_calculate_pcr_policy_multiple_pcrs() { - // Test with PCRs 0, 1, 2 - let mut pcrs = BTreeMap::new(); - pcrs.insert(0, "0".repeat(64)); - pcrs.insert(1, "1".repeat(64)); // All 0x11... - pcrs.insert(2, "2".repeat(64)); // All 0x22... - - let result = calculate_pcr_policy(&pcrs, TpmAlg::Sha256); - assert!(result.is_ok()); - } - - #[test] - fn test_calculate_pcr_policy_non_contiguous() { - // Test with non-contiguous PCRs (0, 7, 15) - let mut pcrs = BTreeMap::new(); - pcrs.insert(0, "0".repeat(64)); - pcrs.insert(7, "7".repeat(64)); - pcrs.insert(15, "f".repeat(64)); - - let result = calculate_pcr_policy(&pcrs, TpmAlg::Sha256); - assert!(result.is_ok()); - } - - #[test] - fn test_calculate_pcr_policy_deterministic() { - // Same input should produce same output - let mut pcrs = BTreeMap::new(); - pcrs.insert(0, "0".repeat(64)); - - let policy1 = calculate_pcr_policy(&pcrs, TpmAlg::Sha256).unwrap(); - let policy2 = calculate_pcr_policy(&pcrs, TpmAlg::Sha256).unwrap(); - - assert_eq!(policy1, policy2); - } - - #[test] - fn test_calculate_pcr_policy_different_values() { - // Different PCR values should produce different policies - let mut pcrs1 = BTreeMap::new(); - pcrs1.insert(0, "0".repeat(64)); - - let mut pcrs2 = BTreeMap::new(); - pcrs2.insert(0, "1".repeat(64)); - - let policy1 = calculate_pcr_policy(&pcrs1, TpmAlg::Sha256).unwrap(); - let policy2 = calculate_pcr_policy(&pcrs2, TpmAlg::Sha256).unwrap(); - - assert_ne!(policy1, policy2); - } - - #[test] - fn test_calculate_pcr_policy_empty() { - let pcrs: BTreeMap = BTreeMap::new(); - let result = calculate_pcr_policy(&pcrs, TpmAlg::Sha256); - assert!(matches!(result, Err(VerifyError::InvalidAttest(_)))); - } - - #[test] - fn test_calculate_pcr_policy_invalid_index() { - let mut pcrs = BTreeMap::new(); - pcrs.insert(24, "0".repeat(64)); // Index 24 is invalid (max 23) - - let result = calculate_pcr_policy(&pcrs, TpmAlg::Sha256); - assert!(matches!(result, Err(VerifyError::InvalidAttest(_)))); - } - - #[test] - fn test_calculate_pcr_policy_invalid_length() { - let mut pcrs = BTreeMap::new(); - pcrs.insert(0, "0".repeat(32)); // Only 16 bytes, need 32 - - let result = calculate_pcr_policy(&pcrs, TpmAlg::Sha256); - assert!(matches!(result, Err(VerifyError::InvalidAttest(_)))); - } - - #[test] - fn test_calculate_pcr_policy_invalid_hex() { - let mut pcrs = BTreeMap::new(); - pcrs.insert(0, "gg".repeat(32)); // Invalid hex - - let result = calculate_pcr_policy(&pcrs, TpmAlg::Sha256); - assert!(matches!(result, Err(VerifyError::HexDecode(_)))); - } - - #[test] - fn test_verify_pcr_policy_match() { - let mut pcrs = BTreeMap::new(); - pcrs.insert(0, "0".repeat(64)); - - let expected = calculate_pcr_policy(&pcrs, TpmAlg::Sha256).unwrap(); - let result = verify_pcr_policy(&expected, &pcrs, TpmAlg::Sha256); - assert!(result.is_ok()); - } - - #[test] - fn test_verify_pcr_policy_mismatch() { - let mut pcrs = BTreeMap::new(); - pcrs.insert(0, "0".repeat(64)); - - // Wrong expected policy - let wrong_expected = "f".repeat(64); - let result = verify_pcr_policy(&wrong_expected, &pcrs, TpmAlg::Sha256); - assert!(matches!(result, Err(VerifyError::InvalidAttest(_)))); - } - - // === SHA-384 PCR Policy Tests === - - #[test] - fn test_calculate_pcr_policy_sha384() { - // Test with SHA-384 PCRs (48 bytes = 96 hex chars) - let mut pcrs = BTreeMap::new(); - pcrs.insert(0, "0".repeat(96)); - - let result = calculate_pcr_policy(&pcrs, TpmAlg::Sha384); - assert!(result.is_ok()); - - let policy = result.unwrap(); - // Policy is always SHA-256 (32 bytes = 64 hex chars) - assert_eq!(policy.len(), 64); - } - - #[test] - fn test_calculate_pcr_policy_sha384_wrong_size() { - // SHA-384 expects 48 bytes, not 32 - let mut pcrs = BTreeMap::new(); - pcrs.insert(0, "0".repeat(64)); // 32 bytes - wrong for SHA-384 - - let result = calculate_pcr_policy(&pcrs, TpmAlg::Sha384); - assert!(matches!(result, Err(VerifyError::InvalidAttest(_)))); - } - - #[test] - fn test_calculate_pcr_policy_different_alg_different_policy() { - // Same PCR value but different algorithms should produce different policies - // because the algorithm ID is encoded in the PCR selection structure - let mut pcrs256 = BTreeMap::new(); - pcrs256.insert(0, "0".repeat(64)); // 32 bytes for SHA-256 - - let mut pcrs384 = BTreeMap::new(); - pcrs384.insert(0, "0".repeat(96)); // 48 bytes for SHA-384 (different zeros count) - - let policy256 = calculate_pcr_policy(&pcrs256, TpmAlg::Sha256).unwrap(); - let policy384 = calculate_pcr_policy(&pcrs384, TpmAlg::Sha384).unwrap(); - - // Policies should differ due to algorithm ID in selection structure - assert_ne!(policy256, policy384); - } - - // === Malicious Input Tests for parse_tpm2b_attest === - - #[test] - fn test_attest_empty_input() { - let result = parse_tpm2b_attest(&[]); - assert!(matches!(result, Err(VerifyError::InvalidAttest(_)))); - } - - #[test] - fn test_attest_truncated_magic() { - // Only 2 bytes when magic needs 4 - let result = parse_tpm2b_attest(&[0xff, 0x54]); - assert!(matches!(result, Err(VerifyError::InvalidAttest(_)))); - } - - #[test] - fn test_attest_wrong_magic() { - // Valid length but wrong magic value - let mut data = vec![0x00, 0x00, 0x00, 0x00]; // Wrong magic - data.extend(&[0x80, 0x17]); // Correct type - let result = parse_tpm2b_attest(&data); - assert!(matches!(result, Err(VerifyError::InvalidAttest(_)))); - } - - #[test] - fn test_attest_huge_signer_size() { - // Craft input with valid magic/type but huge signer size - let mut data = vec![]; - data.extend(&0xff544347u32.to_be_bytes()); // magic - data.extend(&0x8017u16.to_be_bytes()); // type - data.extend(&0xffffu16.to_be_bytes()); // signer size = 65535 (way too big) - - let result = parse_tpm2b_attest(&data); - assert!( - matches!(result, Err(VerifyError::InvalidAttest(_))), - "Should reject huge signer size, got: {:?}", - result - ); - } - - #[test] - fn test_attest_huge_extra_size() { - // Craft input with valid magic/type, small signer, but huge extra size - let mut data = vec![]; - data.extend(&0xff544347u32.to_be_bytes()); // magic - data.extend(&0x8017u16.to_be_bytes()); // type - data.extend(&0x0000u16.to_be_bytes()); // signer size = 0 - data.extend(&0xffffu16.to_be_bytes()); // extra size = 65535 (way too big) - - let result = parse_tpm2b_attest(&data); - assert!( - matches!(result, Err(VerifyError::InvalidAttest(_))), - "Should reject huge extra size, got: {:?}", - result - ); - } - - #[test] - fn test_attest_truncated_after_sizes() { - // Valid header but truncated before clockInfo - let mut data = vec![]; - data.extend(&0xff544347u32.to_be_bytes()); // magic - data.extend(&0x8017u16.to_be_bytes()); // type - data.extend(&0x0002u16.to_be_bytes()); // signer size = 2 - data.extend(&[0x00, 0x0b]); // signer (2 bytes) - data.extend(&0x0004u16.to_be_bytes()); // extra size = 4 - data.extend(&[0x01, 0x02, 0x03, 0x04]); // extra (4 bytes) - // Missing: clockInfo, firmwareVersion, certifiedName - - let result = parse_tpm2b_attest(&data); - assert!( - matches!(result, Err(VerifyError::InvalidAttest(_))), - "Should reject truncated input, got: {:?}", - result - ); - } } diff --git a/crates/vaportpm-verify/src/x509.rs b/crates/vaportpm-verify/src/x509.rs index 53ece61..928b6d8 100644 --- a/crates/vaportpm-verify/src/x509.rs +++ b/crates/vaportpm-verify/src/x509.rs @@ -1,19 +1,139 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -//! X.509 certificate handling using rustls-webpki for chain validation +//! X.509 certificate chain validation use base64::{engine::general_purpose::STANDARD, Engine as _}; +use der::oid::ObjectIdentifier; use der::{Decode, Encode}; use ecdsa::signature::Verifier; use p256::ecdsa::{Signature as P256Signature, VerifyingKey as P256VerifyingKey}; use p384::ecdsa::{Signature as P384Signature, VerifyingKey as P384VerifyingKey}; -use pki_types::{CertificateDer, UnixTime}; +use pki_types::UnixTime; +use rsa::pkcs1v15::{Signature as RsaSignature, VerifyingKey as RsaVerifyingKey}; +use rsa::RsaPublicKey; use sha2::{Digest, Sha256}; -use webpki::{anchor_from_trusted_cert, EndEntityCert, KeyUsage}; use x509_cert::Certificate; use crate::error::VerifyError; +// X.509 extension OIDs +const OID_KEY_USAGE: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.29.15"); +const OID_BASIC_CONSTRAINTS: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.29.19"); + +/// Key Usage extension flags (OID 2.5.29.15) +/// Only includes bits used for TPM certificate chain validation. +#[derive(Debug, Clone, Default)] +pub struct KeyUsageFlags { + /// digitalSignature (bit 0) - key can be used to verify digital signatures + pub digital_signature: bool, + /// keyCertSign (bit 5) - key can be used to verify certificate signatures + pub key_cert_sign: bool, +} + +/// Basic Constraints extension (OID 2.5.29.19) +#[derive(Debug, Clone, Default)] +pub struct BasicConstraints { + /// Whether this certificate is a CA + pub ca: bool, + /// Maximum number of intermediate certificates allowed below this CA + pub path_len_constraint: Option, +} + +/// Extract Key Usage extension from a certificate (OID 2.5.29.15) +/// +/// Returns None if the extension is not present. +pub fn extract_key_usage(cert: &Certificate) -> Option { + let extensions = cert.tbs_certificate.extensions.as_ref()?; + + for ext in extensions.iter() { + if ext.extn_id == OID_KEY_USAGE { + // Key Usage is a BIT STRING + // The extn_value is an OctetString containing the DER-encoded BIT STRING directly + // (x509_cert already unwrapped the outer OCTET STRING) + let bit_string = der::asn1::BitString::from_der(ext.extn_value.as_bytes()).ok()?; + + // Key Usage bits are numbered from the most significant bit + // Bit 0 = digitalSignature (MSB of first byte) + // Bit 5 = keyCertSign + let raw_bits = bit_string.raw_bytes(); + if raw_bits.is_empty() { + return Some(KeyUsageFlags::default()); + } + + let byte0 = raw_bits[0]; + + return Some(KeyUsageFlags { + digital_signature: (byte0 & 0x80) != 0, // bit 0 + key_cert_sign: (byte0 & 0x04) != 0, // bit 5 + }); + } + } + None +} + +/// Extract Basic Constraints extension from a certificate (OID 2.5.29.19) +/// +/// Returns None if the extension is not present. +pub fn extract_basic_constraints(cert: &Certificate) -> Option { + let extensions = cert.tbs_certificate.extensions.as_ref()?; + + for ext in extensions.iter() { + if ext.extn_id == OID_BASIC_CONSTRAINTS { + // BasicConstraints ::= SEQUENCE { cA BOOLEAN DEFAULT FALSE, pathLenConstraint INTEGER (0..MAX) OPTIONAL } + // The extn_value is an OctetString containing the DER-encoded SEQUENCE directly + let bytes = ext.extn_value.as_bytes(); + + // Parse the SEQUENCE header manually + if bytes.is_empty() { + return Some(BasicConstraints::default()); + } + + // First byte should be SEQUENCE tag (0x30) + if bytes[0] != 0x30 { + return Some(BasicConstraints::default()); + } + + // Get length + if bytes.len() < 2 { + return Some(BasicConstraints::default()); + } + + let len = bytes[1] as usize; + let seq_start = 2; + + // Empty sequence means cA defaults to false + if len == 0 { + return Some(BasicConstraints::default()); + } + + // Parse sequence contents + let seq_bytes = &bytes[seq_start..seq_start + len.min(bytes.len() - seq_start)]; + + let mut bc = BasicConstraints::default(); + + // Check if there's a BOOLEAN (tag 0x01) + if !seq_bytes.is_empty() && seq_bytes[0] == 0x01 { + // BOOLEAN: tag (0x01), length (0x01), value + if seq_bytes.len() >= 3 && seq_bytes[1] == 0x01 { + bc.ca = seq_bytes[2] != 0; + + // Check for pathLenConstraint after BOOLEAN + if seq_bytes.len() >= 6 && seq_bytes[3] == 0x02 { + // INTEGER: tag (0x02), length, value + let int_len = seq_bytes[4] as usize; + if int_len == 1 && seq_bytes.len() >= 6 { + bc.path_len_constraint = Some(seq_bytes[5]); + } + } + } + } + + return Some(bc); + } + } + None +} + /// Maximum allowed certificate chain depth (to prevent DoS) pub const MAX_CHAIN_DEPTH: usize = 10; @@ -160,102 +280,33 @@ pub struct ChainValidationResult { pub root_pubkey_hash: String, } -/// Validate a certificate chain using webpki +/// Validate certificate chain with rigid X.509 validation /// -/// Chain should be leaf-first, root-last. Time must be provided by caller. -pub fn validate_cert_chain( - chain: &[Certificate], - time: UnixTime, -) -> Result { - if chain.is_empty() { - return Err(VerifyError::ChainValidation( - "Empty certificate chain".into(), - )); - } - if chain.len() > MAX_CHAIN_DEPTH { - return Err(VerifyError::ChainValidation(format!( - "Certificate chain too deep: {} certificates (max {})", - chain.len(), - MAX_CHAIN_DEPTH - ))); - } - - // Get signature verification algorithms from rustls-rustcrypto - let sig_algs = rustls_rustcrypto::provider() - .signature_verification_algorithms - .all; - - // Convert to DER format for webpki - let cert_ders: Vec = chain - .iter() - .map(|c| { - let der = c.to_der().map_err(|e| { - VerifyError::CertificateParse(format!("Failed to encode cert to DER: {}", e)) - })?; - Ok(CertificateDer::from(der)) - }) - .collect::, VerifyError>>()?; - - // Root is last in chain - create trust anchor from it - let root_der = &cert_ders[cert_ders.len() - 1]; - let trust_anchor = anchor_from_trusted_cert(root_der) - .map_err(|e| VerifyError::ChainValidation(format!("Invalid root certificate: {:?}", e)))?; - - // Leaf is first - let ee_cert = EndEntityCert::try_from(&cert_ders[0]) - .map_err(|e| VerifyError::CertificateParse(format!("Invalid leaf certificate: {:?}", e)))?; - - // Intermediates are everything between leaf and root - let intermediates: Vec = if cert_ders.len() > 2 { - cert_ders[1..cert_ders.len() - 1].to_vec() - } else { - Vec::new() - }; - - // Use webpki to verify the chain - ee_cert - .verify_for_usage( - sig_algs, - &[trust_anchor], - &intermediates, - time, - KeyUsage::client_auth(), - None, // no revocation checking - None, // no custom path verification - ) - .map_err(|e| VerifyError::ChainValidation(format!("Chain validation failed: {:?}", e)))?; - - // Extract and hash root's public key - let root_pubkey = extract_public_key(&chain[chain.len() - 1])?; - let root_hash = hash_public_key(&root_pubkey); - - Ok(ChainValidationResult { - root_pubkey_hash: root_hash, - }) -} - -/// Parse PEM and validate certificate chain +/// This function performs comprehensive certificate chain validation suitable for +/// TPM attestation key certificates. It validates: /// -/// Convenience wrapper that parses PEM then validates. -/// Chain should be leaf-first, root-last in the PEM. -pub fn parse_and_validate_cert_chain( - chain_pem: &str, - time: UnixTime, -) -> Result { - let certs = parse_cert_chain_pem(chain_pem)?; - validate_cert_chain(&certs, time) -} - -/// Validate TPM EK certificate chain -/// -/// Similar to validate_cert_chain but without Extended Key Usage (EKU) checking. -/// TPM EK certificates use TPM-specific EKU OID (2.23.133.8.1) which is not -/// recognized by webpki's standard EKU validation. -/// -/// This function validates: +/// **Signature chain:** /// - Each certificate is signed by the next in chain +/// - Root certificate is self-signed +/// +/// **Time validity:** /// - Certificate validity periods include the specified time -/// - Chain is not too deep +/// +/// **Basic Constraints (OID 2.5.29.19):** +/// - Leaf certificate must have `CA:FALSE` (or no Basic Constraints) +/// - Intermediate/root certificates must have `CA:TRUE` +/// - Path length constraints are honored +/// +/// **Key Usage (OID 2.5.29.15):** +/// - Leaf certificate must have `digitalSignature` bit set +/// - CA certificates must have `keyCertSign` bit set +/// +/// **Extended Key Usage (OID 2.5.29.37):** +/// - CA certificates should have TPM EK Certificate EKU (2.23.133.8.1) +/// - Note: GCP AK leaf certificates don't have EKU, only Key Usage +/// +/// **Name chaining:** +/// - Each certificate's Issuer must match its parent's Subject /// /// Chain should be leaf-first, root-last. pub fn validate_tpm_cert_chain( @@ -275,7 +326,89 @@ pub fn validate_tpm_cert_chain( ))); } + // === X.509 Extension Validation === + // Validate Basic Constraints, Key Usage, EKU, and name chaining + + for (i, cert) in chain.iter().enumerate() { + let is_leaf = i == 0; + let is_root = i == chain.len() - 1; + + // 1. Basic Constraints validation + if let Some(bc) = extract_basic_constraints(cert) { + if is_leaf && bc.ca { + return Err(VerifyError::ChainValidation( + "Leaf certificate has CA:TRUE - must be CA:FALSE".into(), + )); + } + if !is_leaf && !bc.ca { + return Err(VerifyError::ChainValidation(format!( + "Certificate {} (intermediate/root) must have CA:TRUE", + i + ))); + } + + // Check pathLenConstraint for CA certificates + // pathLenConstraint limits how many CAs can exist below this one + if !is_leaf { + if let Some(path_len) = bc.path_len_constraint { + // Number of CAs below this certificate in the chain + // i=0 is leaf, so CAs below cert at position i are at positions 0..i-1 + // But the count should be: number of intermediate CAs between this CA and the leaf + // For position i, there are (i - 1) intermediate CAs between it and the leaf + // (position 0 is leaf, positions 1..i-1 are intermediates below) + let cas_below = if i > 0 { i - 1 } else { 0 }; + if cas_below > path_len as usize { + return Err(VerifyError::ChainValidation(format!( + "Certificate {} pathLenConstraint violated: allows {} CAs below, but {} exist", + i, path_len, cas_below + ))); + } + } + } + } else if !is_leaf { + // CA certificates SHOULD have Basic Constraints + // This is a SHOULD per RFC 5280, but we enforce it for security + return Err(VerifyError::ChainValidation(format!( + "Certificate {} (intermediate/root) missing Basic Constraints extension", + i + ))); + } + + // 2. Key Usage validation + if let Some(ku) = extract_key_usage(cert) { + if is_leaf && !ku.digital_signature { + return Err(VerifyError::ChainValidation( + "Leaf certificate missing digitalSignature key usage".into(), + )); + } + if !is_leaf && !ku.key_cert_sign { + return Err(VerifyError::ChainValidation(format!( + "Certificate {} (CA) missing keyCertSign key usage", + i + ))); + } + } else if is_leaf { + // Leaf certificate MUST have Key Usage for signing + return Err(VerifyError::ChainValidation( + "Leaf certificate missing Key Usage extension".into(), + )); + } + + // 3. Subject/Issuer name chaining + if !is_root { + let parent = &chain[i + 1]; + if cert.tbs_certificate.issuer != parent.tbs_certificate.subject { + return Err(VerifyError::ChainValidation(format!( + "Certificate {} issuer does not match parent subject", + i + ))); + } + } + } + + // === Signature Chain Validation === // Validate each certificate is signed by the next one in chain + for i in 0..chain.len() - 1 { let cert = &chain[i]; let issuer = &chain[i + 1]; @@ -299,9 +432,38 @@ pub fn validate_tpm_cert_chain( // ECDSA with SHA-384 on P-384: 1.2.840.10045.4.3.3 const ECDSA_SHA256_OID: &str = "1.2.840.10045.4.3.2"; const ECDSA_SHA384_OID: &str = "1.2.840.10045.4.3.3"; + const RSA_SHA256_OID: &str = "1.2.840.113549.1.1.11"; let alg_str = alg_oid.to_string(); match alg_str.as_str() { + RSA_SHA256_OID => { + // RSA PKCS#1 v1.5 with SHA-256 verification + // For RSA, we need the full SPKI structure, not just raw key bytes + let issuer_spki = &issuer.tbs_certificate.subject_public_key_info; + let issuer_spki_der = issuer_spki.to_der().map_err(|e| { + VerifyError::ChainValidation(format!("Failed to encode issuer SPKI: {}", e)) + })?; + + let rsa_pubkey = RsaPublicKey::try_from( + spki::SubjectPublicKeyInfoRef::try_from(issuer_spki_der.as_slice()).map_err( + |e| VerifyError::ChainValidation(format!("Invalid RSA SPKI: {}", e)), + )?, + ) + .map_err(|e| VerifyError::ChainValidation(format!("Invalid RSA key: {}", e)))?; + + let verifying_key = RsaVerifyingKey::::new(rsa_pubkey); + + let signature = RsaSignature::try_from(sig_bytes).map_err(|e| { + VerifyError::ChainValidation(format!("Invalid RSA signature: {}", e)) + })?; + + verifying_key.verify(&tbs_der, &signature).map_err(|_| { + VerifyError::ChainValidation(format!( + "Certificate {} RSA signature verification failed", + i + )) + })?; + } ECDSA_SHA256_OID => { // P-256 verification if issuer_pubkey.len() != 65 || issuer_pubkey[0] != 0x04 { @@ -357,7 +519,9 @@ pub fn validate_tpm_cert_chain( } } + // === Time Validity === // Validate time for each certificate + let unix_secs = time.as_secs(); for (i, cert) in chain.iter().enumerate() { @@ -559,4 +723,59 @@ mod tests { assert!(!is_valid_base64_line("ABC DEF")); // space not allowed assert!(is_valid_base64_line("")); // empty is valid } + + // === Extension Parsing Tests === + + #[test] + fn test_extract_basic_constraints_ca_true() { + // GCP intermediate certificate with CA:TRUE + let cert_b64 = "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"; + let cert_der = STANDARD.decode(cert_b64).unwrap(); + let cert = Certificate::from_der(&cert_der).unwrap(); + + let bc = extract_basic_constraints(&cert); + assert!(bc.is_some(), "Basic Constraints should be present"); + let bc = bc.unwrap(); + assert!(bc.ca, "CA flag should be true for intermediate CA cert"); + } + + #[test] + fn test_extract_basic_constraints_ca_false() { + // GCP AK leaf certificate with CA:FALSE + let cert_b64 = "MIIFITCCAwmgAwIBAgIUAJSyAthxJCCD6ViYtC96+AGDJCswDQYJKoZIhvcNAQELBQAwgYYxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRMwEQYDVQQKEwpHb29nbGUgTExDMRUwEwYDVQQLEwxHb29nbGUgQ2xvdWQxHjAcBgNVBAMTFUVLL0FLIENBIEludGVybWVkaWF0ZTAgFw0yNjAyMDIwNjU3NDdaGA8yMDU2MDEyNjA2NTc0NlowaTEWMBQGA1UEBxMNdXMtY2VudHJhbDEtZjEeMBwGA1UEChMVR29vZ2xlIENvbXB1dGUgRW5naW5lMREwDwYDVQQLEwhsb2NrYm9vdDEcMBoGA1UEAxMTMzQxNDI0MDY0ODIyNTQ4NTgzNjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABMp9xzVQQpEV600JB3Gj9IP+U89ypLnrhQRUBVDFnz5INT2kVd4Jhl8KHZ6qYXVOOZYhrkvO0cVY0mfclyT+tJOjggFqMIIBZjAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU2lwtWfsKczujmD2yTgCf4MptjpkwHwYDVR0jBBgwFoAUZ8O73ljj1lF2j7MaPtsHp+yTeuQwgY0GCCsGAQUFBwEBBIGAMH4wfAYIKwYBBQUHMAKGcGh0dHA6Ly9wcml2YXRlY2EtY29udGVudC02NWQ1M2IxNC0wMDAwLTIxMmEtYTYzMy04ODNkMjRmNTdiYjguc3RvcmFnZS5nb29nbGVhcGlzLmNvbS8wYzNlNzllYjA4OThkMDJlYmIwYS9jYS5jcnQwdgYKKwYBBAHWeQIBFQRoMGYMDXVzLWNlbnRyYWwxLWYCBTY7VDeMDAhsb2NrYm9vdAIIL2HRx7ct9AwMGGluc3RhbmNlLTIwMjYwMjAyLTA2NTYwOaAgMB6gAwIBAKEDAQH/ogMBAf+jAwEBAKQDAQEApQMBAQAwDQYJKoZIhvcNAQELBQADggIBAKL4yGiPbAA63EQ7bxJ+2HGDo2EC+qymJDHtWski2lRGY70u+xIdywTW5l4k7JnnwM+fk7LHtfP0md0WU4Mw30B+51sc+pXprEn1SHpP20I/mM5AZMR+cMy9SWnr0WkfTZYKvYJMhDPKuZyK8JvUtXx6NM9AGHYxtPfFnvw2F/USBYYf7/N2KHMhUB9v1zFuHwMD/LDxduIw27kYUcVHttTbHGL9Uljflz343qL2YFE8QpRqtQ/0GK4UaJ3kzPYcbMbWBgpZgKQ2UIMfldvEO8hbGqO4hkNPsv4TPQcG0mJHyUWt+jTOFesBuDgbR/8R4lnrGL3QYZo3Oj7URCJdPiJ+ztohscGydjMVvYfaVziAGJbhMbADnyh/HBshi9gc4rQ99NbOA9fmCZnl+vcMp9jwaAzWZQxc1dNsfdRuTrQSwsNXn+PXJUDgRKKNsENY73QbVBUYvlBmQPkw8zqp8Htvtlsv5AImFFJsW4XsGt4CzZLOhlNW6Ckc58fjVSmeC66kTxYefRGh1SCXRZiJovuTynnF3Z6CFnWvnN/8dCmgjD+S0JUZ8Znx2NSxyZLbEqc6TYcJKx6R8B8QKa4EHWtKMuMorykvWpXMrVl1QjVm0HyVBvgzI+xRAO2TrS6g6c6pChj9BPXPgwLbVXlXQSstoUBFSORdz0S0HrEqxCg8"; + let cert_der = STANDARD.decode(cert_b64).unwrap(); + let cert = Certificate::from_der(&cert_der).unwrap(); + + let bc = extract_basic_constraints(&cert); + assert!(bc.is_some(), "Basic Constraints should be present"); + let bc = bc.unwrap(); + assert!(!bc.ca, "CA flag should be false for leaf cert"); + } + + #[test] + fn test_extract_key_usage_digital_signature() { + // GCP AK leaf certificate with Key Usage: Digital Signature + let cert_b64 = "MIIFITCCAwmgAwIBAgIUAJSyAthxJCCD6ViYtC96+AGDJCswDQYJKoZIhvcNAQELBQAwgYYxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRMwEQYDVQQKEwpHb29nbGUgTExDMRUwEwYDVQQLEwxHb29nbGUgQ2xvdWQxHjAcBgNVBAMTFUVLL0FLIENBIEludGVybWVkaWF0ZTAgFw0yNjAyMDIwNjU3NDdaGA8yMDU2MDEyNjA2NTc0NlowaTEWMBQGA1UEBxMNdXMtY2VudHJhbDEtZjEeMBwGA1UEChMVR29vZ2xlIENvbXB1dGUgRW5naW5lMREwDwYDVQQLEwhsb2NrYm9vdDEcMBoGA1UEAxMTMzQxNDI0MDY0ODIyNTQ4NTgzNjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABMp9xzVQQpEV600JB3Gj9IP+U89ypLnrhQRUBVDFnz5INT2kVd4Jhl8KHZ6qYXVOOZYhrkvO0cVY0mfclyT+tJOjggFqMIIBZjAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU2lwtWfsKczujmD2yTgCf4MptjpkwHwYDVR0jBBgwFoAUZ8O73ljj1lF2j7MaPtsHp+yTeuQwgY0GCCsGAQUFBwEBBIGAMH4wfAYIKwYBBQUHMAKGcGh0dHA6Ly9wcml2YXRlY2EtY29udGVudC02NWQ1M2IxNC0wMDAwLTIxMmEtYTYzMy04ODNkMjRmNTdiYjguc3RvcmFnZS5nb29nbGVhcGlzLmNvbS8wYzNlNzllYjA4OThkMDJlYmIwYS9jYS5jcnQwdgYKKwYBBAHWeQIBFQRoMGYMDXVzLWNlbnRyYWwxLWYCBTY7VDeMDAhsb2NrYm9vdAIIL2HRx7ct9AwMGGluc3RhbmNlLTIwMjYwMjAyLTA2NTYwOaAgMB6gAwIBAKEDAQH/ogMBAf+jAwEBAKQDAQEApQMBAQAwDQYJKoZIhvcNAQELBQADggIBAKL4yGiPbAA63EQ7bxJ+2HGDo2EC+qymJDHtWski2lRGY70u+xIdywTW5l4k7JnnwM+fk7LHtfP0md0WU4Mw30B+51sc+pXprEn1SHpP20I/mM5AZMR+cMy9SWnr0WkfTZYKvYJMhDPKuZyK8JvUtXx6NM9AGHYxtPfFnvw2F/USBYYf7/N2KHMhUB9v1zFuHwMD/LDxduIw27kYUcVHttTbHGL9Uljflz343qL2YFE8QpRqtQ/0GK4UaJ3kzPYcbMbWBgpZgKQ2UIMfldvEO8hbGqO4hkNPsv4TPQcG0mJHyUWt+jTOFesBuDgbR/8R4lnrGL3QYZo3Oj7URCJdPiJ+ztohscGydjMVvYfaVziAGJbhMbADnyh/HBshi9gc4rQ99NbOA9fmCZnl+vcMp9jwaAzWZQxc1dNsfdRuTrQSwsNXn+PXJUDgRKKNsENY73QbVBUYvlBmQPkw8zqp8Htvtlsv5AImFFJsW4XsGt4CzZLOhlNW6Ckc58fjVSmeC66kTxYefRGh1SCXRZiJovuTynnF3Z6CFnWvnN/8dCmgjD+S0JUZ8Znx2NSxyZLbEqc6TYcJKx6R8B8QKa4EHWtKMuMorykvWpXMrVl1QjVm0HyVBvgzI+xRAO2TrS6g6c6pChj9BPXPgwLbVXlXQSstoUBFSORdz0S0HrEqxCg8"; + let cert_der = STANDARD.decode(cert_b64).unwrap(); + let cert = Certificate::from_der(&cert_der).unwrap(); + + let ku = extract_key_usage(&cert); + assert!(ku.is_some(), "Key Usage should be present"); + let ku = ku.unwrap(); + assert!(ku.digital_signature, "digitalSignature bit should be set"); + assert!(!ku.key_cert_sign, "keyCertSign should not be set for leaf"); + } + + #[test] + fn test_extract_key_usage_ca() { + // GCP intermediate certificate with Key Usage: Certificate Sign, CRL Sign + let cert_b64 = "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"; + let cert_der = STANDARD.decode(cert_b64).unwrap(); + let cert = Certificate::from_der(&cert_der).unwrap(); + + let ku = extract_key_usage(&cert); + assert!(ku.is_some(), "Key Usage should be present"); + let ku = ku.unwrap(); + assert!(ku.key_cert_sign, "keyCertSign bit should be set for CA"); + } } diff --git a/crates/vaportpm-verify/test-gcp-amd-fixture.json b/crates/vaportpm-verify/test-gcp-amd-fixture.json new file mode 100644 index 0000000..831b312 --- /dev/null +++ b/crates/vaportpm-verify/test-gcp-amd-fixture.json @@ -0,0 +1,48 @@ +{ + "nonce": "8a543108a653b4a1162232744cc9b945017a449dea4fbb0ca62f42d3ef145562", + "pcrs": { + "sha256": { + "0": "a0b5ff3383a1116bd7dc6df177c0c2d433b9ee1813ea958fa5d166a202cb2a85", + "1": "6c7b2cb52b90bffa55bf88c65ca3a047253c101f19041418abfc95be075d1bf9", + "2": "3d458cfe55cc03ea1f443f1562beec8df51c75e14a9fcf9a7234a13f198e7969", + "3": "3d458cfe55cc03ea1f443f1562beec8df51c75e14a9fcf9a7234a13f198e7969", + "4": "a274e8cad520128a8b8e607f7216645a288e81e09f3f130186ec4ef84754b7b7", + "5": "833f07fa4ee51aee2932ddcc04c3b0e6687d9806fa3f4712534ca8898ab11d18", + "6": "3d458cfe55cc03ea1f443f1562beec8df51c75e14a9fcf9a7234a13f198e7969", + "7": "a11c5239a222bb78072c2c73caa691bb9a0f118de2d95cdce1fce06711e4d3ed", + "8": "31e25dc9719b9506242d98f152952504b0a0a650ef73e2f178f7c6048477f541", + "9": "432d8127b440f9ef26835a3f91a18e19a9c360fdb286260319eae813f060d3ee", + "10": "12f087b4cb357d1a7d2fe480237079a853c6bf8ae5a0fe76016a3c7908853348", + "11": "0000000000000000000000000000000000000000000000000000000000000000", + "12": "0000000000000000000000000000000000000000000000000000000000000000", + "13": "0000000000000000000000000000000000000000000000000000000000000000", + "14": "306f9d8b94f17d93dc6e7cf8f5c79d652eb4c6c4d13de2dddc24af416e13ecaf", + "15": "0000000000000000000000000000000000000000000000000000000000000000", + "16": "0000000000000000000000000000000000000000000000000000000000000000", + "17": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "18": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "19": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "20": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "21": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "22": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "23": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + "ak_pubkeys": { + "ecc_p256": { + "x": "ca7dc73550429115eb4d090771a3f483fe53cf72a4b9eb8504540550c59f3e48", + "y": "353da455de09865f0a1d9eaa61754e399621ae4bced1c558d267dc9724feb493" + } + }, + "attestation": { + "tpm": { + "ecc_p256": { + "attest_data": "ff54434780180022000b764ba22a45fd6fc2720b130e119dfd62e2937cd2b05c4cc2a075142d08ea9b2e00208a543108a653b4a1162232744cc9b945017a449dea4fbb0ca62f42d3ef14556200000000001b039c0000000f0000000001201605110016280000000001000b03ffffff0020be5e5e4c5c2b3d2f55dfb017641e4240cd5b9372dcd72bdf3b559dca544fad4d", + "signature": "304402200134cc97ae682c7f7080ed474d714b1d8e5918785e336bdd9d7f657855cfb984022018e658d56ae85ed9d52acbdde6e46bd8f18a37af6dd40e1c977bc6dbedd360da" + } + }, + "gcp": { + "ak_cert_chain": "-----BEGIN CERTIFICATE-----\nMIIFITCCAwmgAwIBAgIUAJSyAthxJCCD6ViYtC96+AGDJCswDQYJKoZIhvcNAQEL\nBQAwgYYxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQH\nEw1Nb3VudGFpbiBWaWV3MRMwEQYDVQQKEwpHb29nbGUgTExDMRUwEwYDVQQLEwxH\nb29nbGUgQ2xvdWQxHjAcBgNVBAMTFUVLL0FLIENBIEludGVybWVkaWF0ZTAgFw0y\nNjAyMDIwNjU3NDdaGA8yMDU2MDEyNjA2NTc0NlowaTEWMBQGA1UEBxMNdXMtY2Vu\ndHJhbDEtZjEeMBwGA1UEChMVR29vZ2xlIENvbXB1dGUgRW5naW5lMREwDwYDVQQL\nEwhsb2NrYm9vdDEcMBoGA1UEAxMTMzQxNDI0MDY0ODIyNTQ4NTgzNjBZMBMGByqG\nSM49AgEGCCqGSM49AwEHA0IABMp9xzVQQpEV600JB3Gj9IP+U89ypLnrhQRUBVDF\nnz5INT2kVd4Jhl8KHZ6qYXVOOZYhrkvO0cVY0mfclyT+tJOjggFqMIIBZjAOBgNV\nHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU2lwtWfsKczujmD2y\nTgCf4MptjpkwHwYDVR0jBBgwFoAUZ8O73ljj1lF2j7MaPtsHp+yTeuQwgY0GCCsG\nAQUFBwEBBIGAMH4wfAYIKwYBBQUHMAKGcGh0dHA6Ly9wcml2YXRlY2EtY29udGVu\ndC02NWQ1M2IxNC0wMDAwLTIxMmEtYTYzMy04ODNkMjRmNTdiYjguc3RvcmFnZS5n\nb29nbGVhcGlzLmNvbS8wYzNlNzllYjA4OThkMDJlYmIwYS9jYS5jcnQwdgYKKwYB\nBAHWeQIBFQRoMGYMDXVzLWNlbnRyYWwxLWYCBTY7VDeMDAhsb2NrYm9vdAIIL2HR\nx7ct9AwMGGluc3RhbmNlLTIwMjYwMjAyLTA2NTYwOaAgMB6gAwIBAKEDAQH/ogMB\nAf+jAwEBAKQDAQEApQMBAQAwDQYJKoZIhvcNAQELBQADggIBAKL4yGiPbAA63EQ7\nbxJ+2HGDo2EC+qymJDHtWski2lRGY70u+xIdywTW5l4k7JnnwM+fk7LHtfP0md0W\nU4Mw30B+51sc+pXprEn1SHpP20I/mM5AZMR+cMy9SWnr0WkfTZYKvYJMhDPKuZyK\n8JvUtXx6NM9AGHYxtPfFnvw2F/USBYYf7/N2KHMhUB9v1zFuHwMD/LDxduIw27kY\nUcVHttTbHGL9Uljflz343qL2YFE8QpRqtQ/0GK4UaJ3kzPYcbMbWBgpZgKQ2UIMf\nldvEO8hbGqO4hkNPsv4TPQcG0mJHyUWt+jTOFesBuDgbR/8R4lnrGL3QYZo3Oj7U\nRCJdPiJ+ztohscGydjMVvYfaVziAGJbhMbADnyh/HBshi9gc4rQ99NbOA9fmCZnl\n+vcMp9jwaAzWZQxc1dNsfdRuTrQSwsNXn+PXJUDgRKKNsENY73QbVBUYvlBmQPkw\n8zqp8Htvtlsv5AImFFJsW4XsGt4CzZLOhlNW6Ckc58fjVSmeC66kTxYefRGh1SCX\nRZiJovuTynnF3Z6CFnWvnN/8dCmgjD+S0JUZ8Znx2NSxyZLbEqc6TYcJKx6R8B8Q\nKa4EHWtKMuMorykvWpXMrVl1QjVm0HyVBvgzI+xRAO2TrS6g6c6pChj9BPXPgwLb\nVXlXQSstoUBFSORdz0S0HrEqxCg8\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIHIjCCBQqgAwIBAgITaI//DbE0ilSC7SjJnNGPwAKK+jANBgkqhkiG9w0BAQsF\nADB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMN\nTW91bnRhaW4gVmlldzETMBEGA1UEChMKR29vZ2xlIExMQzEVMBMGA1UECxMMR29v\nZ2xlIENsb3VkMRYwFAYDVQQDEw1FSy9BSyBDQSBSb290MCAXDTI0MDIyMjIxNDQx\nNVoYDzIxMjIwNzA4MDU1NzIzWjCBhjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNh\nbGlmb3JuaWExFjAUBgNVBAcTDU1vdW50YWluIFZpZXcxEzARBgNVBAoTCkdvb2ds\nZSBMTEMxFTATBgNVBAsTDEdvb2dsZSBDbG91ZDEeMBwGA1UEAxMVRUsvQUsgQ0Eg\nSW50ZXJtZWRpYXRlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA2FCg\nrrj91CGYC7saslxTDswjGS5oPfBnEwjrZMRUOkqA/mcxe1o1svsMllMtNjOd3MSJ\nkRwEUuwnX707XhNSBE64y2JSEotF43l/Vq+PeBlZXvJHa0JhDh8DU6c4heLd2XOV\nYs6fV2bAZv+SOuFmmiPt753TAcljNFeMZIaQ4gXEjLZodvBU/D09UUf92trSihuK\nZWGpjtRuT2ep+C4x4PL4XQlfmYY8H5V5BqBO2vFyHpctFxlrNxCMjG6TQlX07zZO\n9sQADFl1hwJkS9BoYaXCUdAewrweElkIe9P9P1MkwQhUU8dlAJrYOizZ1drCII4T\nzWf69mGe9F/cIxEfdu7ZPqeH5fJB3tahGZiP+TYK6Ey+2uTvDp7xO9GwTdMTckpa\nmU4oOnXufUKIdYohUuwy0vjb2D4VtWSVjgcL7aL93zaLyosHfSgsZoQs8FjPKzWV\nqFAq9RsTR3mkk891aC5drMR6lb1wkxfqPy9rS56Iom/cnATHxMlAysEvlUTzMd9n\nB3dOVaY1DINzuv/ZohRwkoIVFFxO+LjvhJGBkAGEWw36bNzV4slsfG9g2+o76Ilu\noDZmgAmaDKLvZvNu0aBrfBXZA3zWYFbnHnzijGN+XyLD7vJ0cd9SQ+Z6JOhQV3YS\nXgcqH5xSl0qcs/nYpghqvtIa/TLE2NgsM28vZn0CAwEAAaOCAYwwggGIMA4GA1Ud\nDwEB/wQEAwIBBjAQBgNVHSUECTAHBgVngQUIATAPBgNVHRMBAf8EBTADAQH/MB0G\nA1UdDgQWBBRnw7veWOPWUXaPsxo+2wen7JN65DAfBgNVHSMEGDAWgBRJ50pbVin1\nnXm3pjA8A7KP5xTdTDCBjQYIKwYBBQUHAQEEgYAwfjB8BggrBgEFBQcwAoZwaHR0\ncDovL3ByaXZhdGVjYS1jb250ZW50LTYyZDcxNzczLTAwMDAtMjFkYS04NTJlLWY0\nZjVlODBkNzc3OC5zdG9yYWdlLmdvb2dsZWFwaXMuY29tLzAzMmJmOWQzOWRiNGZh\nMDZhYWRlL2NhLmNydDCBggYDVR0fBHsweTB3oHWgc4ZxaHR0cDovL3ByaXZhdGVj\nYS1jb250ZW50LTYyZDcxNzczLTAwMDAtMjFkYS04NTJlLWY0ZjVlODBkNzc3OC5z\ndG9yYWdlLmdvb2dsZWFwaXMuY29tLzAzMmJmOWQzOWRiNGZhMDZhYWRlL2NybC5j\ncmwwDQYJKoZIhvcNAQELBQADggIBAG11IpWVb2PSB7jxDYYKlvYIyW4w5xI3DR/v\nblykiCHugY9lx2ukCXd9s6UV7gbpb5J1yysin0gkXa5FudKUl4DHb9O3rT5BaAaa\nwz/iuvoVDZBlOfeMg9sCCbZf0apMTVG4b03VIUL6LRZ9DljipLJN78+/s0OHWS/x\nEEqw/Z8pwg9MID3kEU7iBxVtIKCoOQW+ENtmfaPTNLFORbBxeUvKOgslTlC2Nrfa\nwPB/YP+rTB6EwZthhzeQ3oW/MvFzorr5LWBEjNY+wreYR7bu1x2qbegUAb83qnOt\nKktU0pREI/cX5Jfv9Bgt9u2Z532BneMXwMrGda6LHdTmjG1AkfB7SgSZDkg0dkvT\nEKpclGg/bRjFQRGYtKLhvMlZmj7ag54dqp01KLS33ujDSSI3QmS2MFArqxt/jQQJ\n/w3iuwcFi+BUm848fOdmSgOrufo/l0BaQuj7plVT0W2JUsaBkSw56YGOET8Dw7im\n2Z87bu3EvVMPuIcK15gQlYrrObDp8KRijqSxqQ5kBFUp1kArq0vBLqBdvyjWIQ/n\n104nxkp990d5RR9RURTMadDHCqHXGADRDXC0J8Zyqp2IarLFITqAotM8fCRaEuih\nHSVuAxYBMuMCDIf+Ps7ZHbfJOTjw5QuUF+VTPL1yAb7eJHbIUczCgt7o5Rqh2evH\n3j4IQ1VD\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\n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\nav16U4E=\n-----END CERTIFICATE-----\n" + } + } +} \ No newline at end of file diff --git a/crates/vaportpm-verify/test-gcp-tdx-fixture.json b/crates/vaportpm-verify/test-gcp-tdx-fixture.json new file mode 100644 index 0000000..84bc3e2 --- /dev/null +++ b/crates/vaportpm-verify/test-gcp-tdx-fixture.json @@ -0,0 +1,48 @@ +{ + "nonce": "6424632e79ec068f2189adf46d121b9a10f758c45a18c52f630da14600d4317b", + "pcrs": { + "sha256": { + "0": "0cca9ec161b09288802e5a112255d21340ed5b797f5fe29cecccfd8f67b9f802", + "1": "8d9072270d85f934b34a274b78bae66f343cf9ff9f8578d67a2c66b9c3831993", + "2": "0762893a4f936faa16b3f21b7cb914668fe79bf34e9777a595f9ee09de86fb85", + "3": "3d458cfe55cc03ea1f443f1562beec8df51c75e14a9fcf9a7234a13f198e7969", + "4": "c36d9e765efa54f2981707bb7dec3c38385476cf91a413f7d85dced9f092e21f", + "5": "a5ceb755d043f32431d63e39f5161464620a3437280494b5850dc1b47cc074e0", + "6": "3d458cfe55cc03ea1f443f1562beec8df51c75e14a9fcf9a7234a13f198e7969", + "7": "a11c5239a222bb78072c2c73caa691bb9a0f118de2d95cdce1fce06711e4d3ed", + "8": "31e25dc9719b9506242d98f152952504b0a0a650ef73e2f178f7c6048477f541", + "9": "432d8127b440f9ef26835a3f91a18e19a9c360fdb286260319eae813f060d3ee", + "10": "96173f289ee41de4501c185a1a31408663f1420b7df85aafa2c83ba634ed19c8", + "11": "0000000000000000000000000000000000000000000000000000000000000000", + "12": "0000000000000000000000000000000000000000000000000000000000000000", + "13": "0000000000000000000000000000000000000000000000000000000000000000", + "14": "306f9d8b94f17d93dc6e7cf8f5c79d652eb4c6c4d13de2dddc24af416e13ecaf", + "15": "0000000000000000000000000000000000000000000000000000000000000000", + "16": "0000000000000000000000000000000000000000000000000000000000000000", + "17": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "18": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "19": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "20": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "21": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "22": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "23": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + "ak_pubkeys": { + "ecc_p256": { + "x": "c67c129afeaf4dab6e99f38245b280ecef81141648390b77e2ad633a93d20443", + "y": "a434cb13af43be4f31fc68d71a6b20c8e2be6e00519ec5decacfea917e5905fa" + } + }, + "attestation": { + "tpm": { + "ecc_p256": { + "attest_data": "ff54434780180022000b118e5f465caa83d41fc47913e21ae07675957a53c8dba718b36bf346770f2fc200206424632e79ec068f2189adf46d121b9a10f758c45a18c52f630da14600d4317b0000000000022d410000000f0000000001201605110016280000000001000b03ffffff0020afbea8968e40c50829044559f63f9b530ce570597df73967883b3a03435bd59a", + "signature": "3045022100d83d724d801993b52f5c6db12efdd44c9066b2b27016c2d9711c15ffd1ae1bda02207febe629d9739be59fc638d673969e70c513fc9cf8a407eab484021aa34ff8d2" + } + }, + "gcp": { + "ak_cert_chain": "-----BEGIN CERTIFICATE-----\nMIIFIDCCAwigAwIBAgITTLN98N9aiFYPC71j3fLoAFsRezANBgkqhkiG9w0BAQsF\nADCBhjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcT\nDU1vdW50YWluIFZpZXcxEzARBgNVBAoTCkdvb2dsZSBMTEMxFTATBgNVBAsTDEdv\nb2dsZSBDbG91ZDEeMBwGA1UEAxMVRUsvQUsgQ0EgSW50ZXJtZWRpYXRlMCAXDTI2\nMDIwMzAyMzc0OVoYDzIwNTYwMTI3MDIzNzQ4WjBpMRYwFAYDVQQHEw11cy1jZW50\ncmFsMS1hMR4wHAYDVQQKExVHb29nbGUgQ29tcHV0ZSBFbmdpbmUxETAPBgNVBAsT\nCGxvY2tib290MRwwGgYDVQQDExMyMTQxMjA5NDIxMTEzMjU0MzAzMFkwEwYHKoZI\nzj0CAQYIKoZIzj0DAQcDQgAExnwSmv6vTatumfOCRbKA7O+BFBZIOQt34q1jOpPS\nBEOkNMsTr0O+TzH8aNcaayDI4r5uAFGexd7Kz+qRflkF+qOCAWowggFmMA4GA1Ud\nDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBR9cEBcn3GqNAd3D7nc\naj9XIZ0bgDAfBgNVHSMEGDAWgBQPIZ1W4anAicVs2iPAxPEysEqJejCBjQYIKwYB\nBQUHAQEEgYAwfjB8BggrBgEFBQcwAoZwaHR0cDovL3ByaXZhdGVjYS1jb250ZW50\nLTY1ZDE2ODhlLTAwMDAtMjIwMy04NTBlLTMwZmQzODE0NTZmOC5zdG9yYWdlLmdv\nb2dsZWFwaXMuY29tLzgxMGFmMzEzNDA2YWQzZTIwNzliL2NhLmNydDB2BgorBgEE\nAdZ5AgEVBGgwZgwNdXMtY2VudHJhbDEtYQIFNjtUN4wMCGxvY2tib290Aggdtxqf\ntyzhnwwYaW5zdGFuY2UtMjAyNjAyMDMtMDIzNDQ3oCAwHqADAgEAoQMBAf+iAwEB\n/6MDAQEApAMBAQClAwEBADANBgkqhkiG9w0BAQsFAAOCAgEACyYpZgCtAhMJjQKV\nUcoH6LtuVGYg3o0ynGohIW/SJ/heKm5w5HnSRAqF4rBSW/RQlbeb+P2bG4mYlvjR\nmkPdeDD+uWJLsDkoHGzJBfDWhvLNqmrnCg9vQ64KdOCWdshWxtmWBe0PqkfiYwqZ\nDsId4xxL9Kz7vLQwEm3mKk7rwllvnKwGYsIeoEXxnZGlweTnl5RyQXQdZjRKDTKA\nV0cIu1eLC280FjIMjpUdeHGPzkYpwHW+I/Ky3ar7N06/1ahRUtKg97CO6KUWj5hi\nyUjj2TO4/e9G5URtPaR1/VTKH89W5IjyvKXNzDfJ+FM9OBbzY5Sv4g4dKNbn1uya\n+NtRNJf+JfnTwy6G6Zl0EY6olT4FXUF2neU6Ddnsz3uoSZsgBcXCBflfDHbVrHbj\nel4gH8lLhkC1RxjGRzjiJ8Aeex3C/XKPxXiv1nz76U+iFwOuhF7i7cdsxz6/kR+G\nlspmdl07RM9M5l95yAWD27IEn/mK2+uSgnZVmmCnO8PXK20rKCt2SUKecG3xKQSk\nGkO9Aqavp2XhQrEtHavMX7R6LIzJGo6WV/cZp7Ba/pzvRNHLtCMrHXLsz7Twhoj9\nSI0bBwia+GUuBj6Los3fL2N4lc8f4RFNCoRa1y5Sm1MPRLrR3fYBO62OKE50RINB\npCZgRWLMwezqat019+h3x7yo4dY=\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIHIzCCBQugAwIBAgIUAPoFnAtqNET6z84unSBu/fRbsB8wDQYJKoZIhvcNAQEL\nBQAwfjELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcT\nDU1vdW50YWluIFZpZXcxEzARBgNVBAoTCkdvb2dsZSBMTEMxFTATBgNVBAsTDEdv\nb2dsZSBDbG91ZDEWMBQGA1UEAxMNRUsvQUsgQ0EgUm9vdDAgFw0yNDAyMjIyMTQz\nMzdaGA8yMTIyMDcwODA1NTcyM1owgYYxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpD\nYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRMwEQYDVQQKEwpHb29n\nbGUgTExDMRUwEwYDVQQLEwxHb29nbGUgQ2xvdWQxHjAcBgNVBAMTFUVLL0FLIENB\nIEludGVybWVkaWF0ZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKEv\nsKjcY2Ey18k/6M2I/rWFLwZ9k/8GrVVQlPamEQdCJBKCjvvMrdZ83a73YShsu2bO\nuTetMn8WWy0/C9u02Q6+LIartyOnhbRHtABNTQaz+tuZ5Fm9KcXY+5I0S2/oD4O6\nU6CKV2wR+wCkbBGVqzYsBCbboAJ9Vgy7pbxMa6mK+xZM2K4QbAGnTAdu1PZiNzOA\nsgSlAnlveNJkQg91ggLkQSLVrHrpSJOB/aEPRxK5W9iMFkOb22PZvSK6OkT8U8d6\nmTCjtWi15krSOitddpz97iBfi/a5gMhXKvLwhC8zGUTnW7WBJB5nG3DmFplXCKUC\n4ptGtJ5X1Jd2ZvaOz0aC3CIm3gLfNAGS3oboVj/hgDJeEvlxNwTh7NAmCaaeF8sO\nq4LGsMcfQe5gCz7DnJ5kBBBPqjt5jvfH4WoKQxsK+dzAoIFzozapiRuoMfkcs7w/\ntdzGaqAoiRXO4eRSNg+GU12PrTPgaPXG5fYIcPL29HbuwYmwx+KiIx7lRhjGK87x\nk2Lwjr0ppvcIBcYciAnxemPafDJF7Vp5/0eckul34r22+G4Xph20foShwR1bKM8K\nLHEiLe7/mH9WSPNDi/ad0zsnvr3hyL9D4cz0ZKlAvW00cgDqzqDMA/McmHnT5Umh\n6Ib88VvVAmCDtj0XDsjO+1MMcP3dZZKxkJjnBCW3AgMBAAGjggGMMIIBiDAOBgNV\nHQ8BAf8EBAMCAQYwEAYDVR0lBAkwBwYFZ4EFCAEwDwYDVR0TAQH/BAUwAwEB/zAd\nBgNVHQ4EFgQUDyGdVuGpwInFbNojwMTxMrBKiXowHwYDVR0jBBgwFoAUSedKW1Yp\n9Z15t6YwPAOyj+cU3UwwgY0GCCsGAQUFBwEBBIGAMH4wfAYIKwYBBQUHMAKGcGh0\ndHA6Ly9wcml2YXRlY2EtY29udGVudC02MmQ3MTc3My0wMDAwLTIxZGEtODUyZS1m\nNGY1ZTgwZDc3Nzguc3RvcmFnZS5nb29nbGVhcGlzLmNvbS8wMzJiZjlkMzlkYjRm\nYTA2YWFkZS9jYS5jcnQwgYIGA1UdHwR7MHkwd6B1oHOGcWh0dHA6Ly9wcml2YXRl\nY2EtY29udGVudC02MmQ3MTc3My0wMDAwLTIxZGEtODUyZS1mNGY1ZTgwZDc3Nzgu\nc3RvcmFnZS5nb29nbGVhcGlzLmNvbS8wMzJiZjlkMzlkYjRmYTA2YWFkZS9jcmwu\nY3JsMA0GCSqGSIb3DQEBCwUAA4ICAQCCObk/o+LpoCMpjLXQe7hNPll2ZTxbhOQw\n/j4TXIk6nTu5jihPX1OO4YZvLXD7ET8OjDdW6RP3V9j9hsh8r5P6D+vIiavBvCr1\nV+iLNXIcD6WKCXc3J+FG8J4BUuih4cx5sVF39CbS9NG/qBAhFUTDl75q96kRR6gZ\nguqmk3HgAKw4tmLO1kiWkSAHyweBen3Ag1exQHFSA4lRV0ukxhkGX4H3q+BnMYUz\nItGWKP/MhZFl8gH7N32jYtZMy1yYSZRVZlTCwcDRyVqQyVD64igYTwGyyTIa1BD7\nIMPggHJAmbS8GlNepm3N9q/21LYssshC0L8URmWhd69CtBaIpqwWGs9ewmrw4FoT\nH18EwMeiWd3xLH9pZQ2dyfTXgJGReGM5PND/GpEFWs5akYTSxnYHLU1VpFIhmtvd\naq6cOMc0aXYlgbb5qX4DHqF/cCD0NFaoAlIZw4zkhwRd/LCAiIgMR+Ub1b5YBBwx\ndzMZUYKJU11hoko2kXBemgkEVUFFIQwHUXD+K+/jwd0JP89zqGOJ7YEsj7coJlPr\nCBTWMsCC1rjTvq0TIjN899okeA/MLArMT0LgBdZPcy+QctAj68YTjT9PNs+qwK7v\nDFx72m/0R2sPnSgvWS9XQH0wmOOtA7idFTxEYcnREcPfpM3TPJfRY4QfkPpH9owy\ny5/I31K8bQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\n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\nav16U4E=\n-----END CERTIFICATE-----\n" + } + } +} \ No newline at end of file diff --git a/crates/vaportpm-verify/test-nitro-fixture.json b/crates/vaportpm-verify/test-nitro-fixture.json index b4fbd49..2bad4c2 100644 --- a/crates/vaportpm-verify/test-nitro-fixture.json +++ b/crates/vaportpm-verify/test-nitro-fixture.json @@ -1,9 +1,9 @@ { - "ek_certificates": {}, + "nonce": "230af3f7c0ec43ccf99a4cab47ac61469a36ea74b1e79740fdf8ccfc8f56161a", "pcrs": { "sha384": { "0": "6e901b16932f6e036747d7a57696e4a2cc864008ebc016826ca1d7bd42ab5ac8286ccf49cde6c0284cbc4b63d978a2ec", - "1": "7e10323dec25050c22d2e91709373c930d194be275bde5c5e01331b9cc6ef81a9e2586991636652605bc3d322dbc7109", + "1": "4d59f4f7f85e95c52b278c47099315c283ae579650cf9f9336544a4d3c406f36e744cbf9431206e8482dad6a6cd88db2", "2": "518923b0f955d08da077c96aaba522b9decede61c599cea6c41889cfbea4ae4d50529d96fe4d1afdafb65e7f95bf23c4", "3": "518923b0f955d08da077c96aaba522b9decede61c599cea6c41889cfbea4ae4d50529d96fe4d1afdafb65e7f95bf23c4", "4": "1a1f166e1995c1e1bfdf1c1d13b1ebc9d5d4e44397501655c911a2062d92063483ca9e17f0264efe336d4b61c5b98d1e", @@ -12,7 +12,7 @@ "7": "98441c7f7625d10058c47683aec486ce311c633235eb555593a7ee791121e3578ae72d04ecef661f272d59058b77af35", "8": "056a862312c282d635a1e486812191efbf58c8c5533a89c3579b1809854bfa0090854e04b82acd93a752cbd32f1f38c1", "9": "4d5b0773776a7520553b4c08189632a76fe83024fa462035c649a0f62cd82f3951132c386488fd87d875ebf27552156d", - "10": "c1b534d2b043b50ae0fc0d415c4881d92f1633e6b318f967f8adb201eff718d080004fa7ce98b50f1bd8a808332fa1e2", + "10": "453fe596c51b221366998aa81bfea46a4d4643a1ede6e12f767791f9361b2df3c696c13777511bd9eaa001b2ebc141fc", "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": "7dd307a79ceec2a34771a32318f88b367de7d0dcab0bb58535aeeb7873c64356", + "y": "325b08cf7a409878432d74668453494938d26a5813971e52dc642422afea65cf" } }, "attestation": { "tpm": { "ecc_p256": { - "nonce": "31373639393233313833", - "attest_data": "ff54434780170022000b149bb4c9c56850e3571bed5c4b6ed5e8736678d44d4564f9e2d9d400f7bff5ac000a313736393932333138330000000000240cb3e5738876f090bf840108bffe09fb566b9f0022000bc4a66f8f2b42b7d3d214802efab53fdf840024754cff2d4448657bf84f56d0520022000b149bb4c9c56850e3571bed5c4b6ed5e8736678d44d4564f9e2d9d400f7bff5ac", - "signature": "3045022100ba599d6291aa1eb117ecfc4ba17283096c3f3303b8b96be81c1659c595c96bcd022054b80456ccac86c6885820f294ff331d49dd68fe09143d349007ce446b728d29" + "attest_data": "ff54434780180022000be681046803461aebd8630511a11bd7177e88b9daafb739bf1801c80c8113ea520020230af3f7c0ec43ccf99a4cab47ac61469a36ea74b1e79740fdf8ccfc8f56161a000000000000fbfa000000030000000001201910230016363600000001000c03ffffff002056b529e2b35cced0a8a4714e1ed84a28e71cab7d5855e78da5f7fe1ef96b12cb", + "signature": "30450220370815d56aab22f392c82b42971d94599e876dff888ff22d56f6f2d899670def022100cae695dcca7be10c0a8e5d10847c442a53f38cdf11d55f19f4fc1df03695a81a" } }, "nitro": { - "public_key": "042417c2d23945e9e68c78c730102f951bf41f5042464d8272cd18e32d4b8ccfb5d4c3d4b93e361f7d8093f1f83c2396ad932a5c6bd238575e0de758a8c3a6ddc2", - "nonce": "31373639393233313833", - "document": "8444a1013822a05912e2bf696d6f64756c655f69647827692d30333061316632333366383334383639302d74706d3030303030303030303030303030303066646967657374665348413338346974696d657374616d701b0000019c17a4830f6d6e6974726f74706d5f70637273b8180058306e901b16932f6e036747d7a57696e4a2cc864008ebc016826ca1d7bd42ab5ac8286ccf49cde6c0284cbc4b63d978a2ec0158307e10323dec25050c22d2e91709373c930d194be275bde5c5e01331b9cc6ef81a9e2586991636652605bc3d322dbc7109025830518923b0f955d08da077c96aaba522b9decede61c599cea6c41889cfbea4ae4d50529d96fe4d1afdafb65e7f95bf23c4035830518923b0f955d08da077c96aaba522b9decede61c599cea6c41889cfbea4ae4d50529d96fe4d1afdafb65e7f95bf23c40458301a1f166e1995c1e1bfdf1c1d13b1ebc9d5d4e44397501655c911a2062d92063483ca9e17f0264efe336d4b61c5b98d1e055830b3ef9dfcbc8be38f0104cea80711252de3f4f8bf19a3f27126d303fb08ec979ba90780f06a742bf66f444f62e99cc1fc065830518923b0f955d08da077c96aaba522b9decede61c599cea6c41889cfbea4ae4d50529d96fe4d1afdafb65e7f95bf23c407583098441c7f7625d10058c47683aec486ce311c633235eb555593a7ee791121e3578ae72d04ecef661f272d59058b77af35085830056a862312c282d635a1e486812191efbf58c8c5533a89c3579b1809854bfa0090854e04b82acd93a752cbd32f1f38c10958304d5b0773776a7520553b4c08189632a76fe83024fa462035c649a0f62cd82f3951132c386488fd87d875ebf27552156d0a5830c1b534d2b043b50ae0fc0d415c4881d92f1633e6b318f967f8adb201eff718d080004fa7ce98b50f1bd8a808332fa1e20b58300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c58300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d58300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e58300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f5830000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000105830000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000115830ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff125830ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff135830ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff145830ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff155830ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff165830ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff17583022a3e580d7d0931f4365ebfae2618b84e3cce2c0afbf0ea1fbe40e645a43a3771ceb3e188394803642266f5512ee0eac6b63657274696669636174655902733082026f308201f5a0030201020204697ed9f7300a06082a8648ce3d04030330818e310b30090603550406130255533113301106035504080c0a57617368696e67746f6e3110300e06035504070c0753656174746c65310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533139303706035504030c30692d30333061316632333366383334383639302e75732d656173742d312e6177732e6e6974726f2d656e636c61766573301e170d3236303230313034343333325a170d3236303230313037343333355a308193310b30090603550406130255533113301106035504080c0a57617368696e67746f6e3110300e06035504070c0753656174746c65310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753313e303c06035504030c35692d30333061316632333366383334383639302d74706d303030303030303030303030303030302e75732d656173742d312e6177733076301006072a8648ce3d020106052b8104002203620004777cac8372ab736a2043f52369f0bdcc532e5e282c3695bfbeab6db877798b22bce1f8fd961c0c6ebad4bb6f6d09fabe2c13bf9020c52a70c10c6b07a761331cf90bd77f095cba83d682b2905ab58dca582e7cc21bc01cb4f9bf3fbd3efaf1e7a31d301b300c0603551d130101ff04023000300b0603551d0f0404030206c0300a06082a8648ce3d0403030368003065023100a32fefa84e976c7728110621dddb3d933ddbc47b598db144825daa4de7ffbbd2521846bdade93383e6b4d9fc2b81d512023014ebb021eb6888d1d440a2dde4479bb5d310e685137b272e96a376f5952fd0e98843117a84ed197938b8c1f985e73a4268636162756e646c65845902153082021130820196a003020102021100f93175681b90afe11d46ccb4e4e7f856300a06082a8648ce3d0403033049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c61766573301e170d3139313032383133323830355a170d3439313032383134323830355a3049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004fc0254eba608c1f36870e29ada90be46383292736e894bfff672d989444b5051e534a4b1f6dbe3c0bc581a32b7b176070ede12d69a3fea211b66e752cf7dd1dd095f6f1370f4170843d9dc100121e4cf63012809664487c9796284304dc53ff4a3423040300f0603551d130101ff040530030101ff301d0603551d0e041604149025b50dd90547e796c396fa729dcf99a9df4b96300e0603551d0f0101ff040403020186300a06082a8648ce3d0403030369003066023100a37f2f91a1c9bd5ee7b8627c1698d255038e1f0343f95b63a9628c3d39809545a11ebcbf2e3b55d8aeee71b4c3d6adf3023100a2f39b1605b27028a5dd4ba069b5016e65b4fbde8fe0061d6a53197f9cdaf5d943bc61fc2beb03cb6fee8d2302f3dff65902c1308202bd30820244a0030201020210578aa8f46a2b9e82304ad9173550c111300a06082a8648ce3d0403033049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c61766573301e170d3236303133303131303734365a170d3236303231393132303734365a3064310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533136303406035504030c2d373333363535643664363866376264662e75732d656173742d312e6177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004325b2791b548f694a7e17d44d65a326ed1659e23da8972a0acff960f820e548b1b0fb443927dcae15e27a815355ca62ab0e590a2b353a67495b6e10a949d02dddb0e8d8e6ec82efcfaab167c3517fad31de4add54e54e7869966b5a106a85604a381d53081d230120603551d130101ff040830060101ff020102301f0603551d230418301680149025b50dd90547e796c396fa729dcf99a9df4b96301d0603551d0e04160414f309d394ba73bc082274c84cfaa28d8c12ce95e7300e0603551d0f0101ff040403020186306c0603551d1f046530633061a05fa05d865b687474703a2f2f6177732d6e6974726f2d656e636c617665732d63726c2e73332e616d617a6f6e6177732e636f6d2f63726c2f61623439363063632d376436332d343262642d396539662d3539333338636236376638342e63726c300a06082a8648ce3d0403030367003064023076291fdcc1db1eff47f6ab41074f946e8c97f6045ec959cc119a577b8f5b02fdeaaff3ccc22559d8095f4cfd75d18afa02300d2cdee4fc10d95ef64217b06a45824958482666fedf04e30a433c7155b9e0108fa43b77a8c4750430798122ed8523ea590319308203153082029aa00302010202100d3d3e6da2d9da68bf96ed11663a0f35300a06082a8648ce3d0403033064310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533136303406035504030c2d373333363535643664363866376264662e75732d656173742d312e6177732e6e6974726f2d656e636c61766573301e170d3236303230313031323235395a170d3236303230363231323235385a308189313c303a06035504030c33343863666233333166346137616362612e7a6f6e616c2e75732d656173742d312e6177732e6e6974726f2d656e636c61766573310c300a060355040b0c03415753310f300d060355040a0c06416d617a6f6e310b3009060355040613025553310b300906035504080c0257413110300e06035504070c0753656174746c653076301006072a8648ce3d020106052b810400220362000458da1984ae792b97312a98fd03ff8db0d930b5d4be21541f749e4ef96e841433cc82dbe43745e3dd46461b4ceedbb2b47932ed734bc17e6745359e54193d5f93d7a1dba4f1e3009335828fa847942f89ec22a6dd60f00c2041ef7044884f5e60a381ea3081e730120603551d130101ff040830060101ff020101301f0603551d23041830168014f309d394ba73bc082274c84cfaa28d8c12ce95e7301d0603551d0e041604146ac22d82f5ed4065cbc35a114a1e635e06ea2907300e0603551d0f0101ff0404030201863081800603551d1f047930773075a073a071866f687474703a2f2f63726c2d75732d656173742d312d6177732d6e6974726f2d656e636c617665732e73332e75732d656173742d312e616d617a6f6e6177732e636f6d2f63726c2f32613533313133362d383232332d346633632d396335632d3335333832643564663634632e63726c300a06082a8648ce3d0403030369003066023100fad27828efde63ae193f5c8e60abd7c98b0ce3c662e3ba33687bba971f4b9986596052d2f95926787243c62e8f518b790231009da96f7c7ba61dea6acf137327762efc41843e5a4d140963af8d91e852baa04819714491effee90fd081d6803c710c915902c3308202bf30820245a0030201020215008567cee73e22cb55f083d4d4f881ff3313c7e2b1300a06082a8648ce3d040303308189313c303a06035504030c33343863666233333166346137616362612e7a6f6e616c2e75732d656173742d312e6177732e6e6974726f2d656e636c61766573310c300a060355040b0c03415753310f300d060355040a0c06416d617a6f6e310b3009060355040613025553310b300906035504080c0257413110300e06035504070c0753656174746c65301e170d3236303230313034343031395a170d3236303230323034343031395a30818e310b30090603550406130255533113301106035504080c0a57617368696e67746f6e3110300e06035504070c0753656174746c65310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533139303706035504030c30692d30333061316632333366383334383639302e75732d656173742d312e6177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004562f892fd88ffc2c4bc40bdbfecc211949e1ece2007d7ebc9e535141fad0fafb7c9569de8aaf44e7a02af6687da8d5cbb971f7025a0c2aeb5d6e88835a7dec9f2b231dce3d00492bebd8337aeb89c190a684085fb7c0da808176ad972828659ca366306430120603551d130101ff040830060101ff020100300e0603551d0f0101ff040403020204301d0603551d0e04160414fdea49e532ddc19976817c8e1b5ffcec437e283f301f0603551d230418301680146ac22d82f5ed4065cbc35a114a1e635e06ea2907300a06082a8648ce3d040303036800306502301d1d14a339eadd7624b1c492300d9a6f1c365b1351718ffcbffd19be21e07b41c2762f560e9fcbe6f33c2669493395a60231009abafe346e9e177cb8bb760126d1d86455f4755af9eeaee0e9f666b2aa2980a836f67bcb75b8663593385981f3f3bd2b6a7075626c69635f6b65795841042417c2d23945e9e68c78c730102f951bf41f5042464d8272cd18e32d4b8ccfb5d4c3d4b93e361f7d8093f1f83c2396ad932a5c6bd238575e0de758a8c3a6ddc269757365725f64617461f6656e6f6e63654a31373639393233313833ff58609c3e3c2a983cd0cc8699922808812043abd958ae697df07b115a77d19c355cf5b71eb58b3a71a0047ec53f854a8d323d98270751953380ddecd150c44f41ab53ffc99898601352bf9ac71a698e9cb181fcfd7d8d6f256ba001cf91cab5b7762b" + "document": "8444a1013822a05912f8bf696d6f64756c655f69647827692d30316366623338333232336534323532322d74706d3030303030303030303030303030303066646967657374665348413338346974696d657374616d701b0000019c22fcdb056d6e6974726f74706d5f70637273b8180058306e901b16932f6e036747d7a57696e4a2cc864008ebc016826ca1d7bd42ab5ac8286ccf49cde6c0284cbc4b63d978a2ec0158304d59f4f7f85e95c52b278c47099315c283ae579650cf9f9336544a4d3c406f36e744cbf9431206e8482dad6a6cd88db2025830518923b0f955d08da077c96aaba522b9decede61c599cea6c41889cfbea4ae4d50529d96fe4d1afdafb65e7f95bf23c4035830518923b0f955d08da077c96aaba522b9decede61c599cea6c41889cfbea4ae4d50529d96fe4d1afdafb65e7f95bf23c40458301a1f166e1995c1e1bfdf1c1d13b1ebc9d5d4e44397501655c911a2062d92063483ca9e17f0264efe336d4b61c5b98d1e055830b3ef9dfcbc8be38f0104cea80711252de3f4f8bf19a3f27126d303fb08ec979ba90780f06a742bf66f444f62e99cc1fc065830518923b0f955d08da077c96aaba522b9decede61c599cea6c41889cfbea4ae4d50529d96fe4d1afdafb65e7f95bf23c407583098441c7f7625d10058c47683aec486ce311c633235eb555593a7ee791121e3578ae72d04ecef661f272d59058b77af35085830056a862312c282d635a1e486812191efbf58c8c5533a89c3579b1809854bfa0090854e04b82acd93a752cbd32f1f38c10958304d5b0773776a7520553b4c08189632a76fe83024fa462035c649a0f62cd82f3951132c386488fd87d875ebf27552156d0a5830453fe596c51b221366998aa81bfea46a4d4643a1ede6e12f767791f9361b2df3c696c13777511bd9eaa001b2ebc141fc0b58300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c58300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d58300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e58300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f5830000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000105830000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000115830ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff125830ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff135830ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff145830ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff155830ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff165830ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1758300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006b636572746966696361746559027430820270308201f5a00302010202046981c9f2300a06082a8648ce3d04030330818e310b30090603550406130255533113301106035504080c0a57617368696e67746f6e3110300e06035504070c0753656174746c65310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533139303706035504030c30692d30316366623338333232336534323532322e75732d656173742d312e6177732e6e6974726f2d656e636c61766573301e170d3236303230333130313135395a170d3236303230333133313230325a308193310b30090603550406130255533113301106035504080c0a57617368696e67746f6e3110300e06035504070c0753656174746c65310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753313e303c06035504030c35692d30316366623338333232336534323532322d74706d303030303030303030303030303030302e75732d656173742d312e6177733076301006072a8648ce3d020106052b81040022036200041560105d46ec49100fcf704c758cc0c80730c06cf640fca49ed7515a5828526e3d052156d301643d4caea3f380400961ff782ca6b268ec245a8fb2209cd00513d221d6d61a4c3ab37c426757fa850f8d15af5339c13ed71d842e7ab6a79b133da31d301b300c0603551d130101ff04023000300b0603551d0f0404030206c0300a06082a8648ce3d040303036900306602310084a69d40b80f26f24a858a650e807d417b142fef49c2e6b9b176f7decc94a645b4c5065451941be21166fd47139d8c07023100da2e8e8a21f569d7b60cafd633bff46fb252c69dd33c04ce229124373a076b317f178013a5b15adb5bdb782ac5bb1e9b68636162756e646c65845902153082021130820196a003020102021100f93175681b90afe11d46ccb4e4e7f856300a06082a8648ce3d0403033049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c61766573301e170d3139313032383133323830355a170d3439313032383134323830355a3049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004fc0254eba608c1f36870e29ada90be46383292736e894bfff672d989444b5051e534a4b1f6dbe3c0bc581a32b7b176070ede12d69a3fea211b66e752cf7dd1dd095f6f1370f4170843d9dc100121e4cf63012809664487c9796284304dc53ff4a3423040300f0603551d130101ff040530030101ff301d0603551d0e041604149025b50dd90547e796c396fa729dcf99a9df4b96300e0603551d0f0101ff040403020186300a06082a8648ce3d0403030369003066023100a37f2f91a1c9bd5ee7b8627c1698d255038e1f0343f95b63a9628c3d39809545a11ebcbf2e3b55d8aeee71b4c3d6adf3023100a2f39b1605b27028a5dd4ba069b5016e65b4fbde8fe0061d6a53197f9cdaf5d943bc61fc2beb03cb6fee8d2302f3dff65902c1308202bd30820244a0030201020210578aa8f46a2b9e82304ad9173550c111300a06082a8648ce3d0403033049310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c03415753311b301906035504030c126177732e6e6974726f2d656e636c61766573301e170d3236303133303131303734365a170d3236303231393132303734365a3064310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533136303406035504030c2d373333363535643664363866376264662e75732d656173742d312e6177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004325b2791b548f694a7e17d44d65a326ed1659e23da8972a0acff960f820e548b1b0fb443927dcae15e27a815355ca62ab0e590a2b353a67495b6e10a949d02dddb0e8d8e6ec82efcfaab167c3517fad31de4add54e54e7869966b5a106a85604a381d53081d230120603551d130101ff040830060101ff020102301f0603551d230418301680149025b50dd90547e796c396fa729dcf99a9df4b96301d0603551d0e04160414f309d394ba73bc082274c84cfaa28d8c12ce95e7300e0603551d0f0101ff040403020186306c0603551d1f046530633061a05fa05d865b687474703a2f2f6177732d6e6974726f2d656e636c617665732d63726c2e73332e616d617a6f6e6177732e636f6d2f63726c2f61623439363063632d376436332d343262642d396539662d3539333338636236376638342e63726c300a06082a8648ce3d0403030367003064023076291fdcc1db1eff47f6ab41074f946e8c97f6045ec959cc119a577b8f5b02fdeaaff3ccc22559d8095f4cfd75d18afa02300d2cdee4fc10d95ef64217b06a45824958482666fedf04e30a433c7155b9e0108fa43b77a8c4750430798122ed8523ea590318308203143082029aa003020102021074dd80cbf52147b2bd6c21f8c5e56966300a06082a8648ce3d0403033064310b3009060355040613025553310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533136303406035504030c2d373333363535643664363866376264662e75732d656173742d312e6177732e6e6974726f2d656e636c61766573301e170d3236303230333034343632365a170d3236303230383233343632365a308189313c303a06035504030c33373464393437666231626163343031642e7a6f6e616c2e75732d656173742d312e6177732e6e6974726f2d656e636c61766573310c300a060355040b0c03415753310f300d060355040a0c06416d617a6f6e310b3009060355040613025553310b300906035504080c0257413110300e06035504070c0753656174746c653076301006072a8648ce3d020106052b810400220362000435313cd9728afe93b86747e2e1a76923df3f6b11326bf1a6969f9b6f225a523502a1fd94c9e5a24e4a322ef0e672e36e6665b95c0693a28931a9e36ce21cff063fe54f5fac4f0f71459f23d3b8bb455f44895916e3fe82b674da7f1de8310806a381ea3081e730120603551d130101ff040830060101ff020101301f0603551d23041830168014f309d394ba73bc082274c84cfaa28d8c12ce95e7301d0603551d0e04160414f3cc62b964147a8008b49dba4d18708a8b648cfb300e0603551d0f0101ff0404030201863081800603551d1f047930773075a073a071866f687474703a2f2f63726c2d75732d656173742d312d6177732d6e6974726f2d656e636c617665732e73332e75732d656173742d312e616d617a6f6e6177732e636f6d2f63726c2f32613533313133362d383232332d346633632d396335632d3335333832643564663634632e63726c300a06082a8648ce3d0403030368003065023100ac911117cb130d9f1679a96e8d15a33de57315dd342d413b8dbe08ec36069333288b91a6fb11aac635db417e3dd4bed9023066668025c88522c2349f4c86acd807012e8b99852d8c0c8200333c3d5ac83b8634f9490f5b77de83e3dbaf42b648d6bc5902c2308202be30820244a0030201020214669127634218f02f383c2a37882b641dd297f0cd300a06082a8648ce3d040303308189313c303a06035504030c33373464393437666231626163343031642e7a6f6e616c2e75732d656173742d312e6177732e6e6974726f2d656e636c61766573310c300a060355040b0c03415753310f300d060355040a0c06416d617a6f6e310b3009060355040613025553310b300906035504080c0257413110300e06035504070c0753656174746c65301e170d3236303230333130313035355a170d3236303230343130313035355a30818e310b30090603550406130255533113301106035504080c0a57617368696e67746f6e3110300e06035504070c0753656174746c65310f300d060355040a0c06416d617a6f6e310c300a060355040b0c034157533139303706035504030c30692d30316366623338333232336534323532322e75732d656173742d312e6177732e6e6974726f2d656e636c617665733076301006072a8648ce3d020106052b8104002203620004885034c6b50a5c751e328556fc50550c6d4929be28a24d327e78456d334e6f71b702946d9c4627544bbba4391201e743632ba8ddeabbc93d41597b6142efaf515e13af12b6672d2c152ef96f1cff671cb5dfd1a9b94487d18fbd6e70fc2aad5ba366306430120603551d130101ff040830060101ff020100300e0603551d0f0101ff040403020204301d0603551d0e04160414b20b8dc7de2ea54b818b309125d88446132625fd301f0603551d23041830168014f3cc62b964147a8008b49dba4d18708a8b648cfb300a06082a8648ce3d0403030368003065023005a61c5045de8c2fdb61c69aa37f348678dfc2046e4e4ce7aa23c040429f22898f0f3515e91c661bc1a4ed07e8580106023100fb5024d455f0a855c81f6cb3bdf737a2389f72e4dcc0b1013ec6997d501cc4d203e2db3759f7eec803c694d5a34bba2d6a7075626c69635f6b65795841047dd307a79ceec2a34771a32318f88b367de7d0dcab0bb58535aeeb7873c64356325b08cf7a409878432d74668453494938d26a5813971e52dc642422afea65cf69757365725f64617461f6656e6f6e63655820230af3f7c0ec43ccf99a4cab47ac61469a36ea74b1e79740fdf8ccfc8f56161aff586028a638418327130ce73c196a64dac447c0e7463213051d33115b09b6998947cbcf2758309996ebef71b5d94d9f8d038d3219f398bc7c8cb65c9ae93d697a97416ecc0dedca4f4c0950fe3925f59070a0936a47e78e491d9ee20891097ea9f824" } } }