Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/target
target
Cargo.lock
.ssh
.rustup
Expand All @@ -13,3 +13,8 @@ Cargo.lock
.config
.gitconfig
.cargo
.bash_history
.risc0
.bashrc
/go
old
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[submodule "experiments/risc-zero/rustcrypto-elliptic-curves"]
path = experiments/risc-zero/rustcrypto-elliptic-curves
url = https://github.com/HarryR/RustCrypto-elliptic-curves.git
branch = risc0-p256-p384-unified
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ license = "MIT OR Apache-2.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_bytes = "0.11"
serde-big-array = "0.5"
zerocopy = { version = "0.8", features = ["derive"] }
base64 = "0.22"
sha1 = "0.10"
sha2 = "0.10"
Expand Down
13 changes: 12 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: all build release-x86 check test fmt fmt-check clippy doc clean ci
.PHONY: all build release-x86 check test fmt fmt-check clippy doc clean ci coverage setup-coverage

all: build

Expand Down Expand Up @@ -34,5 +34,16 @@ clean:

ci: fmt-check check clippy test doc

setup-coverage:
rustup component add llvm-tools-preview
cargo install cargo-llvm-cov

coverage-html:
cargo llvm-cov -p vaportpm-verify --html
@echo "Coverage report: target/llvm-cov/html/index.html"

coverage-text:
cargo llvm-cov -p vaportpm-verify

rustup:
rustup default stable
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ Physical TPM trust is vapor. It evaporates under scrutiny - supply chain attacks

The verifier handles **cryptographic verification**:
- Validates signatures and certificate chains
- Returns the SHA-256 hash of the trust anchor's public key
- Identifies the cloud provider via embedded root CA hashes

You handle **policy decisions**:
- Is this trust root acceptable?
- Do the PCR values match known-good measurements?
- Is the nonce fresh (replay protection)?

## Supported Platforms

Expand Down
2 changes: 1 addition & 1 deletion crates/vaportpm-attest/AWS-NITRO.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ COSE_Sign1 = [
...
}
},
"signing_key_public_keys": {
"ak_pubkeys": {
"ecc_p256": {
"x": "3678325466f129d8279056737fe48378...",
"y": "9384bc5fafdc7938f9a51e09490a5555..."
Expand Down
21 changes: 12 additions & 9 deletions crates/vaportpm-attest/src/a9n.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,9 @@ pub fn attest(nonce: &[u8]) -> Result<String> {
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)?
// SHA-384 is hardcoded — the Quote must attest the same PCR bank that
// the Nitro NSM document signs, so they can be cross-verified.
attest_nitro(&mut tpm, nonce, &pcr_values)?
} else {
return Err(anyhow!(
"Unknown platform - only AWS Nitro and GCP Shielded VM are supported"
Expand Down Expand Up @@ -236,12 +238,11 @@ pub fn attest(nonce: &[u8]) -> Result<String> {
///
/// 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<u8>)],
pcr_alg: crate::TpmAlg,
) -> Result<AttestResult> {
///
/// The Quote always uses SHA-384 PCR selection because the Nitro NSM document
/// signs SHA-384 PCR values. Using the same bank ensures the TPM Quote's PCR
/// digest can be cross-verified against the Nitro-signed values.
fn attest_nitro(tpm: &mut Tpm, nonce: &[u8], pcr_values: &[(u8, Vec<u8>)]) -> Result<AttestResult> {
// 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)?;
Expand All @@ -255,9 +256,11 @@ fn attest_nitro(
},
);

// Build PCR selection bitmap for Quote
// Build PCR selection bitmap for Quote — always SHA-384 for Nitro
// The TPM Quote must attest the same PCR bank that the Nitro NSM
// document signs, so the verification side can cross-check them.
let pcr_bitmap = build_pcr_bitmap(pcr_values);
let pcr_selection = vec![(pcr_alg, pcr_bitmap.as_slice())];
let pcr_selection = vec![(crate::TpmAlg::Sha384, pcr_bitmap.as_slice())];

// Perform TPM2_Quote - signs PCR values with AK
let quote_result = tpm.quote(signing_key.handle, nonce, &pcr_selection)?;
Expand Down
7 changes: 6 additions & 1 deletion crates/vaportpm-verify/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ base64 = { workspace = true }
thiserror = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
zerocopy = { workspace = true }

[dev-dependencies]
rcgen = "0.13"
p256 = { workspace = true, features = ["pkcs8"] }
p384 = { workspace = true, features = ["pkcs8"] }

[lib]
name = "vaportpm_verify"
Expand All @@ -41,4 +47,3 @@ path = "src/lib.rs"
[[bin]]
name = "vaportpm-verify"
path = "src/bin/verify.rs"

34 changes: 17 additions & 17 deletions crates/vaportpm-verify/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ This crate verifies attestation documents without requiring TPM or internet acce
- **Nitro Attestation Verification** - COSE Sign1 signature and certificate chain validation
- **TPM Signature Verification** - ECDSA P-256 signature verification
- **PCR Policy Verification** - Compute and verify PCR policy digests
- **X.509 Chain Validation** - Certificate chain validation using rustls-webpki
- **X.509 Chain Validation** - Certificate chain validation with ECDSA (P-256, P-384) and RSA signature support
- **Zero TPM Dependencies** - Pure cryptographic verification

## Usage
Expand All @@ -27,8 +27,8 @@ fn verify(json: &str) -> Result<(), Box<dyn std::error::Error>> {
let result = verify_attestation_output(&output, UnixTime::now())?;

println!("Verified via: {:?}", result.provider);
println!("Nonce: {}", result.nonce);
println!("Root CA hash: {}", result.root_pubkey_hash);
println!("Nonce: {:?}", result.nonce);
println!("PCRs: {:?}", result.pcrs);

Ok(())
}
Expand All @@ -41,7 +41,7 @@ fn verify(json: &str) -> Result<(), Box<dyn std::error::Error>> {
| Check | Description |
|-------|-------------|
| COSE Signature | ECDSA P-384 signature over Nitro document |
| Certificate Chain | Validates chain, returns root pubkey hash |
| Certificate Chain | Validates chain to AWS Nitro Root CA |
| Public Key Binding | AK public key matches signed `public_key` field |
| TPM Quote Signature | AK's ECDSA P-256 signature over TPM2_Quote |
| Nonce Binding | TPM Quote nonce matches Nitro nonce (freshness) |
Expand All @@ -51,7 +51,7 @@ fn verify(json: &str) -> Result<(), Box<dyn std::error::Error>> {

| Check | Description |
|-------|-------------|
| AK Certificate Chain | Validates chain to Google CA, returns root pubkey hash |
| AK Certificate Chain | Validates chain to Google EK/AK CA Root |
| 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 |
Expand All @@ -60,23 +60,23 @@ fn verify(json: &str) -> Result<(), Box<dyn std::error::Error>> {

```rust
pub struct VerificationResult {
/// The verified nonce (hex-encoded)
pub nonce: String,
/// Cloud provider (AWS, GCP)
/// The nonce that was verified (32 bytes)
pub nonce: [u8; 32],
/// Cloud provider that issued the attestation
pub provider: CloudProvider,
/// PCR values from the attestation
pub pcrs: BTreeMap<u8, String>,
/// SHA-256 hash of the root CA's public key
pub root_pubkey_hash: String,
/// Validated PCR bank from the attestation
pub pcrs: PcrBank,
/// Timestamp when verification was performed (seconds since Unix epoch)
pub verified_at: u64,
}
```

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.
The `provider` field identifies the trust anchor. The library embeds known root CA public key hashes and maps them to `CloudProvider::Aws` or `CloudProvider::Gcp` internally. Verification fails if the root CA is not recognized.

## API

```rust
/// Verify a complete AttestationOutput, returns the root of trust hash
/// Verify a complete AttestationOutput
pub fn verify_attestation_output(
output: &AttestationOutput,
time: UnixTime, // Use UnixTime::now() for production
Expand All @@ -89,9 +89,10 @@ pub fn verify_attestation_json(json: &str) -> Result<VerificationResult, VerifyE
The `time` parameter controls certificate validity checking. Use `UnixTime::now()` for production. For testing with fixtures that have expired certificates, pass a specific time from when the attestation was generated.

Returns `VerificationResult` containing:
- `nonce` - The verified challenge (hex)
- `root_pubkey_hash` - SHA-256 of the trust anchor's public key (hex)
- `nonce` - The verified challenge (32 bytes)
- `provider` - Cloud provider (AWS, GCP)
- `pcrs` - Validated PCR bank (SHA-256 or SHA-384, all 24 values)
- `verified_at` - Verification timestamp (Unix seconds)

## Security Considerations

Expand All @@ -104,4 +105,3 @@ However, beyond that it's up to the application to decide on the following:
2. **Nonce Freshness** - You must generate and track nonces to prevent replay attacks.

3. **Application Logic** - The attestation proves system state at a point in time. Your application must decide if that state is acceptable.

34 changes: 32 additions & 2 deletions crates/vaportpm-verify/src/bin/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,40 @@
//! Reads attestation JSON from a file or stdin, verifies it,
//! and outputs the verification result as JSON.

use std::collections::BTreeMap;
use std::fs;
use std::io::{self, Read};
use std::process::ExitCode;

use vaportpm_verify::verify_attestation_json;
use serde::Serialize;
use vaportpm_verify::{verify_attestation_json, CloudProvider, VerificationResult};

/// JSON-friendly output with hex-encoded binary fields
#[derive(Serialize)]
struct VerificationResultJson {
nonce: String,
provider: CloudProvider,
/// PCR values grouped by algorithm: {"sha256": {"0": "abc...", ...}, "sha384": {...}}
pcrs: BTreeMap<String, BTreeMap<u8, String>>,
}

impl From<VerificationResult> for VerificationResultJson {
fn from(result: VerificationResult) -> Self {
let mut pcr_map = BTreeMap::new();
for (idx, value) in result.pcrs.values().enumerate() {
pcr_map.insert(idx as u8, hex::encode(value));
}

let mut pcrs = BTreeMap::new();
pcrs.insert(result.pcrs.algorithm().to_string(), pcr_map);

VerificationResultJson {
nonce: hex::encode(result.nonce),
provider: result.provider,
pcrs,
}
}
}

fn main() -> ExitCode {
let args: Vec<String> = std::env::args().collect();
Expand All @@ -35,7 +64,8 @@ fn main() -> ExitCode {

match verify_attestation_json(&json) {
Ok(result) => {
println!("{}", serde_json::to_string_pretty(&result).unwrap());
let json_result = VerificationResultJson::from(result);
println!("{}", serde_json::to_string_pretty(&json_result).unwrap());
ExitCode::SUCCESS
}
Err(e) => {
Expand Down
Loading