[framework] Add Nitro enclave attestation support#20103
Conversation
| [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], |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Aptos Security Bugbot has reviewed your changes, there are still 2 issues that need to be addressed from previous scan.
Open findings:
- Gas underpricing enables compute-heavy verification at a discount
- The example's TEE gate is replayable and does not bind the table signer to the attestation
Sent by Cursor Automation: Security Review Bot
| 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); |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit d65fb97. Configure here.
| 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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
There are 3 total unresolved issues (including 1 from previous review).
❌ 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 }); |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 96a6dcc. Configure here.
| 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) |
There was a problem hiding this comment.
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.
| let table_signer_addr = signer::address_of(table); | ||
| assert!(table_signer_addr == table_addr, error::permission_denied(ENOT_TABLE_OWNER)); |
There was a problem hiding this comment.
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.




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_datais bound tob"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 --libcargo build -p aptos --bin aptostarget/debug/aptos move test --package-dir aptos-move/framework/aptos-framework --skip-fetch-latest-git-deps --filter aws_nitrocargo test -p aptos-move-examples test_poker -- --nocapturegit diff --checkrustfmt --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.rsKey Areas to Review
aptos_framework::aws_nitro_utilsaptos-move/framework/natives/src/aws_nitro_utils.rsverify_attestation_user_data*user_dataType of Change
Which Components or Systems Does This Change Impact?
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_tablerequires a valid attestation whoseuser_dataisb"APTOS_POKER_TABLE_V1" || bcs(table_address), with JS clients, a Go Nitro attester, andLOCAL_AWS_E2E.mdfor localnet + AWS smoke testing.test_pokerruns 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.