Skip to content

[framework] Add Nitro enclave attestation support#20103

Open
zjma wants to merge 7 commits into
mainfrom
codex/nitro-attestation-hardening
Open

[framework] Add Nitro enclave attestation support#20103
zjma wants to merge 7 commits into
mainfrom
codex/nitro-attestation-hardening

Conversation

@zjma

@zjma zjma commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Description

Supersedes #18801.

Adds AWS Nitro attestation verification support to aptos_framework::aws_nitro_utils. The Move API keeps trusted Nitro root certificates in framework-managed on-chain storage and wraps deterministic native verification that takes explicit trusted roots plus an explicit Unix timestamp.

The native implementation validates the attestation document certificate chain, verifies the COSE signature, and exposes parsed PCR, public key, user data, and nonce fields so Move policy can authorize TEE jobs before ACE-style key release flows.

Also updates the poker Move example to use Shelby/TEE-style table registration semantics: the table enclave must submit a Nitro attestation whose user_data is bound to b"APTOS_POKER_TABLE_V1" || bcs(table_address). The example clients and docs now describe the chain-managed Nitro root store and real attestation document path.

How Has This Been Tested?

  • cargo check -p aptos-framework-natives --lib
  • cargo build -p aptos --bin aptos
  • target/debug/aptos move test --package-dir aptos-move/framework/aptos-framework --skip-fetch-latest-git-deps --filter aws_nitro
  • cargo test -p aptos-move-examples test_poker -- --nocapture
  • git diff --check
  • rustfmt --check --config skip_children=true aptos-move/framework/natives/src/aws_nitro_utils.rs aptos-move/framework/natives/src/lib.rs aptos-move/move-examples/tests/move_unit_tests.rs

Key Areas to Review

  • Trusted root lifecycle in aptos_framework::aws_nitro_utils
  • Native validation path in aptos-move/framework/natives/src/aws_nitro_utils.rs
  • Dapp-facing attestation policy helper verify_attestation_user_data*
  • Poker example's table registration binding via Nitro user_data
  • Gas parameter placement and dependency wiring after the latest framework native split

Type of Change

  • New feature
  • Bug fix
  • Breaking change
  • Performance improvement
  • Refactoring
  • Dependency update
  • Tests

Which Components or Systems Does This Change Impact?

  • Validator Node
  • Full Node (API, Indexer, etc.)
  • Move/Aptos Virtual Machine
  • Aptos Framework
  • Aptos CLI/SDK
  • Developer Infrastructure
  • Move Compiler
  • Other (specify)

Note

High Risk
Introduces security-critical attestation and trusted-root lifecycle in the VM/framework; incorrect validation or root governance could allow bogus TEE claims or break dependent dapps.

Overview
Adds aptos_framework::aws_nitro_utils, enabling on-chain verification of AWS Nitro Enclave COSE attestation documents for TEE-backed policies (e.g. ACE-style key release).

The framework stores governance-managed Nitro root DER certificates on @aptos_framework, with init/rotation entry points (plus testnet-only helpers). Move wrappers use consensus time and the root store; natives (verify_attestation_with_roots, verify_and_parse_attestation_with_roots) decode the doc, validate the TLS cert chain against caller-provided roots, verify the COSE signature, and return parsed PCRs, user_data, nonce, and public_key. Higher-level Move helpers bind user_data and combined PCR/nonce/key policies. Gas entries are added for the new natives; aptos-framework-natives pulls in attestation/crypto deps (attestation-doc-validation, webpki, x509-parser, etc.).

The poker move-example is extended so register_table requires a valid attestation whose user_data is b"APTOS_POKER_TABLE_V1" || bcs(table_address), with JS clients, a Go Nitro attester, and LOCAL_AWS_E2E.md for localnet + AWS smoke testing. test_poker runs on a larger thread stack due to the heavier Nitro dependency graph.

Reviewed by Cursor Bugbot for commit 014ac24. Bugbot is set up for automated code reviews on this repo. Configure here.

@zjma zjma requested review from a team and vgao1996 as code owners June 17, 2026 19:10
Comment on lines +303 to +306
[aws_nitro_verify_attestation_base: InternalGas, "aws_nitro_utils.verify_attestation.base", 500_000],
[aws_nitro_verify_attestation_per_byte: InternalGasPerByte, "aws_nitro_utils.verify_attestation.per_byte", 50],
[aws_nitro_verify_and_parse_attestation_base: InternalGas, "aws_nitro_utils.verify_and_parse_attestation.base", 550_000],
[aws_nitro_verify_and_parse_attestation_per_byte: InternalGasPerByte, "aws_nitro_utils.verify_and_parse_attestation.per_byte", 50],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity: gas underpricing enables compute-heavy verification at a discount. These constants price Nitro verification at only 500_000/550_000 + 50 * bytes, but the new native path also performs CBOR decoding, X.509 trust-anchor parsing, certificate-chain validation, certificate parsing, hashing, and COSE/ECDSA verification over caller-controlled inputs. Because this API is public, an attacker can repeatedly buy much more validator CPU than the charged gas reflects, creating a block-execution denial-of-service vector.

Comment thread aptos-move/move-examples/poker/sources/poker.move Outdated
Comment thread aptos-move/move-examples/poker/clients/player-client.js Outdated
Comment thread aptos-move/move-examples/poker/Move.toml

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aptos Security Bugbot has reviewed your changes, there are still 2 issues that need to be addressed from previous scan.

Open findings:

Open in Web View Automation 

Sent by Cursor Automation: Security Review Bot

Comment thread aptos-move/move-examples/poker/clients/player-client.js Outdated
let payout = balance - fee;
let pay_coins = coin::extract<AptosCoin>(&mut table_info.escrow, balance);
let fee_coin = coin::extract<AptosCoin>(&mut pay_coins, fee);
coin::merge<AptosCoin>(&mut table_info.fee_pool, fee_coin);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Table fees locked in pool

Medium Severity

Leave settlement moves the 5% fee into TableInfo.fee_pool, but no entry function withdraws or transfers those coins to the table operator. Documented table fees accumulate in the resource and stay unusable for the TEE table account.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d65fb97. Configure here.

Comment on lines +194 to +207
vector::for_each_ref(&leaving_players, |addr| {
if (table::contains(&table_info.pending_leave, *addr) && table::contains(&table_info.balances, *addr)) {
let balance = *table::borrow(&table_info.balances, *addr);
if (balance > 0) {
let fee = (balance * FEE_BPS) / BPS_DENOM;
let payout = balance - fee;
let pay_coins = coin::extract<AptosCoin>(&mut table_info.escrow, balance);
let fee_coin = coin::extract<AptosCoin>(&mut pay_coins, fee);
coin::merge<AptosCoin>(&mut table_info.fee_pool, fee_coin);
coin::deposit(*addr, pay_coins);
event::emit(PlayerLeft { table: table_addr, player: *addr, payout, fee });
};
table::remove(&mut table_info.balances, *addr);
table::remove(&mut table_info.pending_leave, *addr);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity: players can be locked in escrow indefinitely after requesting leave. request_leave only records intent, and actual payout happens only if the table later calls settle_leaving_players with that address in leaving_players. A malicious or unavailable table can therefore leave exit requests unprocessed forever, trapping the player's APT in escrow with no player-initiated recovery path.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

There are 3 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 96a6dcc. Configure here.

let table = borrow_global_mut<TableInfo>(table_addr);
assert!(table::contains(&table.balances, player_addr), error::invalid_argument(EPLAYER_NOT_AT_TABLE));
table::add(&mut table.pending_leave, player_addr, true);
event::emit(LeaveRequested { table: table_addr, player: player_addr });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate leave request aborts

Medium Severity

request_leave always calls table::add on pending_leave, which aborts when the player key is already present. A second leave request (retry, double submit, or client bug) fails the transaction instead of being a no-op.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 96a6dcc. Configure here.

Comment thread aptos-move/framework/aptos-framework/sources/aws_nitro_utils.move

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aptos Security Bugbot has reviewed your changes and found 1 potential issue.

Open in Web View Automation 

Sent by Cursor Automation: Security Review Bot

Comment on lines +309 to +319
let doc_opt = verify_and_parse_attestation_with_roots(
attestation_doc,
trusted_root_certs,
unix_time_secs,
);
if (doc_opt.is_none()) {
return false
};

let doc = doc_opt.extract();
user_data_equals(&doc, expected_user_data)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium severity: this helper authorizes any Nitro enclave that can choose the expected user_data. After verify_and_parse_attestation_with_roots(...) succeeds, the only policy check here is user_data_equals(&doc, expected_user_data). user_data is caller-controlled input to GetAttestationDocument, so this does not pin PCRs, debug mode, or an attested public key. Any contract that uses verify_attestation_user_data*() as its gate, including the new poker example, can therefore be satisfied by an attacker-built Nitro enclave that simply emits the same user_data, defeating the intended "expected enclave" authorization model.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aptos Security Bugbot has reviewed your changes and found 1 potential issue.

Open in Web View Automation 

Sent by Cursor Automation: Security Review Bot

Comment on lines +146 to +147
let table_signer_addr = signer::address_of(table);
assert!(table_signer_addr == table_addr, error::permission_denied(ENOT_TABLE_OWNER));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High severity: post-registration authority is not bound to the attested enclave. register_table verifies an attestation once, but TableInfo stores no attested public_key, PCR, or other enclave identity, and the privileged paths later authorize solely by signer::address_of(table) == table_addr. In the shipped flow, that signer is just TABLE_PRIVATE_KEY supplied on the parent host, so anyone who compromises the ordinary table account key can arbitrarily call settle_hand / settle_leaving_players and redirect player escrow without ever controlling the enclave that was originally attested.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant