Skip to content

Add Stylus attestation verifier contract for on-chain TEE verification#90

Merged
WillPapper merged 9 commits into
mainfrom
claude/add-stylus-verification-w1XaR
Mar 1, 2026
Merged

Add Stylus attestation verifier contract for on-chain TEE verification#90
WillPapper merged 9 commits into
mainfrom
claude/add-stylus-verification-w1XaR

Conversation

@WillPapper
Copy link
Copy Markdown
Contributor

This introduces an Arbitrum Stylus smart contract written in Rust that
provides an alternative to the RiscZeroAttestationVerifier. Instead of
verifying a RISC Zero Groth16 ZK proof, it verifies an ECDSA signature
from a trusted attestor who has validated the GCP Confidential Space
JWT off-chain.

The contract implements the same IAttestationVerifier interface, making
it a drop-in replacement via TeeKeyManager.updateAttestationVerifier().

Key features:

  • ABI-compatible with the existing Solidity verifier interface
  • Uses ecrecover precompile for signature recovery
  • Validates all attestation claims (JWK hash, timestamps, secure boot,
    debug mode, image digest, image signer)
  • Owner-managed trusted attestor list
  • Standalone crate excluded from workspace (different toolchain target)

https://claude.ai/code/session_01Go59koRavN63NtmTiyuYRv

This introduces an Arbitrum Stylus smart contract written in Rust that
provides an alternative to the RiscZeroAttestationVerifier. Instead of
verifying a RISC Zero Groth16 ZK proof, it verifies an ECDSA signature
from a trusted attestor who has validated the GCP Confidential Space
JWT off-chain.

The contract implements the same IAttestationVerifier interface, making
it a drop-in replacement via TeeKeyManager.updateAttestationVerifier().

Key features:
- ABI-compatible with the existing Solidity verifier interface
- Uses ecrecover precompile for signature recovery
- Validates all attestation claims (JWK hash, timestamps, secure boot,
  debug mode, image digest, image signer)
- Owner-managed trusted attestor list
- Standalone crate excluded from workspace (different toolchain target)

https://claude.ai/code/session_01Go59koRavN63NtmTiyuYRv
Integrate the Stylus attestation verifier as an alternative to RISC Zero
zkVM proof generation. When PROVER_MODE=stylus, the bootstrap client
calls a trusted attestor service that verifies the GCP JWT off-chain and
returns ECDSA-signed attestation claims for on-chain verification by the
Stylus contract.

- Add ProverMode::Stylus variant and ATTESTOR_SERVICE_URL config field
- Add generate_stylus_attestation() method calling /attest endpoint
- Add config validation requiring attestor URL for Stylus mode
- Update proof_client to route based on prover_mode field
- Add tests for Stylus config validation

https://claude.ai/code/session_01Go59koRavN63NtmTiyuYRv
Replace the trusted attestor model with direct JWT verification using
EVM precompiles. The contract now verifies GCP Confidential Space JWTs
entirely on-chain without any off-chain intermediary:

- RS256 signature verification via SHA-256 (0x02) and modexp (0x05) precompiles
- Base64url decoding and minimal JSON claim extraction (no serde dependency)
- PKCS#1 v1.5 padding verification for RSA-SHA256
- JWK key material hash verification (caller provides RSA key, contract
  verifies keccak256(n||e) matches stored hash to prevent key substitution)
- Validates all attestation claims: issuer, audience, timestamps, secboot,
  dbgstat, image digest against the PublicValuesStruct
- 15 unit tests covering base64, JSON parsing, PKCS#1 v1.5, ABI roundtrips

proof_bytes format changed from ECDSA signature to ABI-encoded StylusProofData
containing the raw JWT bytes and JWK RSA key material (modulus + exponent).

https://claude.ai/code/session_01Go59koRavN63NtmTiyuYRv
Replace the trusted attestor service with local proof construction for
Stylus mode. The bootstrap client now:

- Parses the JWT locally to extract claims (no external service call)
- Fetches the JWK RSA public key from Google's JWKS endpoint
- Constructs PublicValuesStruct and StylusProofData (raw JWT + JWK material)
- Sends everything to the relayer for on-chain verification by the Stylus contract

Changes:
- Remove attestor_service_url config (no longer needed)
- Add google_jwks_url config with default GCP Confidential Space endpoint
- Add generate_stylus_proof() that constructs proof data locally
- Add fetch_jwk_by_kid() for Google JWKS fetching
- Add JWT/JWKS parsing types and base64url decoder
- Add sol! type definitions for PublicValuesStruct and StylusProofData
- Add serde_json and sol-types dependencies
- Both Service (RISC Zero) and Stylus paths preserved

https://claude.ai/code/session_01Go59koRavN63NtmTiyuYRv
Add 50 new tests covering edge cases across all contract components:
- Base64: empty input, single/two byte, invalid chars, URL-safe chars, high bytes
- JSON extraction: missing keys, empty values, escaped quotes, u64 overflow/zero/max,
  bool non-bool values, key substrings, key-in-value disambiguation, deep nesting
- JWT parsing: invalid UTF-8, missing parts, empty segments, extra dots, signing input
- PKCS#1 v1.5: non-0xFF in padding, wrong DigestInfo, too short, minimum valid padding,
  128-byte key, no separator, extra data after hash
- Precompile input format: modexp EIP-198 layout, ecrecover 128-byte layout
- ABI: roundtrip with real values, invalid data decode
- Realistic GCP Confidential Space JWT with all claim types
- find_subsequence: boundary cases

Also fix clippy warnings: remove useless .into() conversions, use iterator
in PKCS#1 padding loop, extract test helper for building padded messages.

https://claude.ai/code/session_01Go59koRavN63NtmTiyuYRv
Shell script for the full deployment lifecycle of the Stylus contract:
- Check: validates WASM compiles and fits Stylus size limits
- Deploy: submits bytecode and activates on-chain via cargo-stylus
- Initialize: calls initialize() with expiration tolerance
- Saves deployment info to .synddb/stylus-deployment.json

Supports multiple modes: --check-only, --export-abi, --init-only,
--estimate-gas, --no-verify (skip Docker), --no-activate (split deploy).
Prints post-deployment configuration commands for adding trusted JWKs,
image digests, and image signers.

https://claude.ai/code/session_01Go59koRavN63NtmTiyuYRv
Ran 18 automated tests covering argument parsing, error paths, env var
fallbacks, address extraction, JSON output, and private key handling.

Bugs fixed:
- check_prerequisites ran before mode-specific validation, blocking useful
  error messages (e.g. "missing endpoint" was masked by "missing cargo-stylus")
- Prerequisite checks now mode-aware: --init-only only needs cast, not
  cargo-stylus; --check-only only needs cargo-stylus, not cast
- build_key_arg called via $() ran in a subshell where exit 1 didn't kill
  the parent script - deploy would proceed with empty key arg. Now returns
  empty string and caller checks explicitly.
- Address extraction regex (0x + 40 hex chars) would match the first 42
  chars of a 64-char tx hash. Now filters for lines containing "address"
  first via extract_address helper.
- JSON output: "chainId": unknown produced invalid JSON when cast chain-id
  failed. Now falls back to JSON null.
- JSON output: "deploymentTx": "null" was a string. Now outputs unquoted
  null for proper JSON when empty.
- Flags requiring values (--endpoint, --private-key, etc.) produced cryptic
  "unbound variable" errors under set -u. Now uses require_arg helper with
  clear "$FLAG requires a value" messages.
- Added set -euo pipefail for stricter error handling.
- Added --private-key-path file existence check.

https://claude.ai/code/session_01Go59koRavN63NtmTiyuYRv
Copilot AI review requested due to automatic review settings March 1, 2026 00:53
@WillPapper WillPapper requested a review from daniilrrr as a code owner March 1, 2026 00:53
- Fix json_get_string double-backslash escape handling: count consecutive
  backslashes before quote to correctly handle \\\" (even count = real quote)
- Forward expected_audience to generate_stylus_proof() for early validation
  instead of silently ignoring it
- Add zero-address check to transfer_ownership to prevent accidental lockout
- Cap expiration_tolerance at 86400s (1 day) matching the Solidity verifier
- Replace mock prover manual byte offsets with proper sol! type construction
- Update GeneratingProof state comment to be mode-agnostic
- Fix clippy is_multiple_of warning in proof_client base64 decoder
- Update CLAUDE.md test modification policy

https://claude.ai/code/session_01Go59koRavN63NtmTiyuYRv
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new Arbitrum Stylus (Rust) attestation verifier contract and updates the bootstrap client to support a new “Stylus” prover mode that constructs proof inputs locally for on-chain JWT verification (instead of calling the RISC Zero proof service).

Changes:

  • Introduces a Stylus verifier contract that verifies GCP Confidential Space JWTs on-chain (RS256 via SHA-256 + modexp) and validates required claims + image signer via ecrecover.
  • Extends synddb-bootstrap with a ProverMode::Stylus path that parses JWTs locally, fetches Google JWKS, and ABI-encodes the new proof payload.
  • Adds a deployment/initialization helper script for the Stylus contract and excludes the new Stylus crate from the workspace.

Reviewed changes

Copilot reviewed 11 out of 13 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
scripts/deploy-stylus.sh New script to check/deploy/init/verify the Stylus verifier and write deployment metadata JSON.
crates/synddb-bootstrap/src/proof_client.rs Adds Stylus mode to construct proof inputs locally (JWT + JWK key material) and new JWT/JWKS parsing helpers + tests.
crates/synddb-bootstrap/src/lib.rs Updates crate docs to describe Service vs Stylus vs Mock verification modes.
crates/synddb-bootstrap/src/config.rs Adds google_jwks_url config and new ProverMode::Stylus.
crates/synddb-bootstrap/Cargo.toml Moves serde_json to main deps and enables Alloy sol-types.
contracts/stylus/attestation-verifier/src/lib.rs New Stylus verifier contract implementing on-chain RS256 JWT verification and admin-managed allowlists.
contracts/stylus/attestation-verifier/rust-toolchain.toml Pins toolchain/target for building the standalone Stylus crate.
contracts/stylus/attestation-verifier/Cargo.toml Defines standalone Stylus crate dependencies/features.
contracts/stylus/attestation-verifier/Cargo.lock New lockfile for the standalone Stylus crate.
Cargo.toml Excludes the Stylus contract crate from the workspace.
Cargo.lock Minor dependency version bump (thiserror).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +142 to +150
}

#[public]
impl StylusAttestationVerifier {
/// Initializes the contract. Must be called once after deployment.
/// Sets the caller as the owner. Expiration tolerance is capped at 86400 seconds (1 day).
pub fn initialize(&mut self, expiration_tolerance: u64) -> Result<(), VerifierError> {
if self.initialized.get() {
return Err(VerifierError::AlreadyInitialized(AlreadyInitialized {}));
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

initialize() sets expiration_tolerance without any upper-bound validation. The existing Solidity verifier enforces a max tolerance (e.g., 86400s) to reduce misconfiguration risk (accepting very old attestations). Consider enforcing a similar cap here (and potentially rejecting 0 if that’s not intended).

Copilot uses AI. Check for mistakes.
Comment on lines +406 to +415

/// Removes a trusted image signer.
pub fn remove_trusted_image_signer(&mut self, signer: Address) -> Result<(), VerifierError> {
self.only_owner()?;
self.trusted_image_signers.setter(signer).set(false);
self.vm().log(TrustedImageSignerRemoved { signer });
Ok(())
}

/// Transfers ownership of the contract. Reverts if `new_owner` is the zero address.
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

transfer_ownership() allows transferring ownership to the zero address, which would permanently brick admin functions (and differs from OpenZeppelin Ownable’s non-zero requirement). Consider rejecting new_owner == Address::ZERO.

Copilot uses AI. Check for mistakes.
let claims: JwtClaims = serde_json::from_slice(&payload_bytes).map_err(|e| {
BootstrapError::ProofGenerationFailed(format!("Failed to parse JWT claims: {e}"))
})?;

Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

In Stylus prover mode, the code parses the JWT header alg but never validates it. This can produce “proof” data that is guaranteed to fail on-chain (the Stylus contract rejects non-RS256). Consider validating header.alg == "RS256" here and returning an error early.

Suggested change
// Validate that the JWT uses the expected signing algorithm.
// Stylus contracts only accept RS256, so constructing proof data for any
// other algorithm would be guaranteed to fail on-chain.
if header.alg != "RS256" {
return Err(BootstrapError::ProofGenerationFailed(format!(
"Unsupported JWT algorithm: expected 'RS256', got '{}'",
header.alg
)));
}

Copilot uses AI. Check for mistakes.
Comment thread scripts/deploy-stylus.sh
Comment on lines +186 to +195
require_private_key() {
if [[ -z "$PRIVATE_KEY" && -z "$PRIVATE_KEY_PATH" ]]; then
error "No private key provided. Use --private-key or --private-key-path"
exit 1
fi
if [[ -n "$PRIVATE_KEY_PATH" && ! -f "$PRIVATE_KEY_PATH" ]]; then
error "Private key file not found: $PRIVATE_KEY_PATH"
exit 1
fi
}
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

require_private_key() permits providing both --private-key and --private-key-path, and later silently prefers --private-key. This is easy to misconfigure (deploying from the wrong key). Consider making these options mutually exclusive and erroring if both are set.

Copilot uses AI. Check for mistakes.
Comment thread scripts/deploy-stylus.sh
Comment on lines +240 to +268
# Write deployment info as valid JSON
write_deployment_json() {
local address="$1"
local chain_id="$2"
local deploy_tx="${3:-}"

# Ensure chain_id is a number, fallback to null for JSON validity
if ! [[ "$chain_id" =~ ^[0-9]+$ ]]; then
chain_id="null"
fi

# deploymentTx: use JSON null (unquoted) when empty, quoted string otherwise
local deploy_tx_json="null"
if [[ -n "$deploy_tx" ]]; then
deploy_tx_json="\"$deploy_tx\""
fi

mkdir -p "$OUTPUT_DIR"
cat > "$OUTPUT_DIR/stylus-deployment.json" <<EOF
{
"contract": "StylusAttestationVerifier",
"address": "$address",
"chainId": $chain_id,
"endpoint": "$ENDPOINT",
"expirationTolerance": $EXPIRATION_TOLERANCE,
"deploymentTx": $deploy_tx_json,
"deployedAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
EOF
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

write_deployment_json() writes expirationTolerance as a raw value without validating it’s numeric. If the env/flag contains a non-number, this produces invalid JSON in stylus-deployment.json. Consider validating EXPIRATION_TOLERANCE is an integer (similar to chain_id) or serializing it as a JSON string.

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +21
//! # Verification Modes
//!
//! - **Service** (default): Uses RISC Zero zkVM proof service for ZK proof generation.
//! The proof service runs the RISC Zero guest program to verify the JWT and generate
//! a Groth16 proof, which is verified on-chain by `RiscZeroAttestationVerifier`.
//! - **Stylus**: Constructs proof data locally and sends the raw JWT for direct on-chain
//! verification by an Arbitrum Stylus contract. The contract verifies the RS256 signature
//! using SHA-256 and modexp EVM precompiles. No external proof service needed.
//! - **Mock**: For testing only, generates invalid proofs
Copy link

Copilot AI Mar 1, 2026

Choose a reason for hiding this comment

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

The PR description says the Stylus verifier “verifies an ECDSA signature from a trusted attestor who has validated the JWT off-chain”, but the code/docs here describe (and implement) direct on-chain RS256 JWT verification via SHA-256 + modexp precompiles. Please update the PR description (or implementation) so they match, especially around the trust model (trusted attestor list vs trusted JWKs/key-material hashes).

Copilot uses AI. Check for mistakes.
- Fix line-length formatting in proof_client.rs (cargo fmt)
- Update bytes 1.11.0 -> 1.11.1 (RUSTSEC-2026-0007: integer overflow)
- Update time 0.3.44 -> 0.3.47 (RUSTSEC-2026-0009: stack exhaustion DoS)
- Update keccak 0.1.5 -> 0.1.6 (RUSTSEC-2026-0012: unsound ARMv8 assembly)

https://claude.ai/code/session_01Go59koRavN63NtmTiyuYRv
@WillPapper WillPapper merged commit 7b7227f into main Mar 1, 2026
12 of 13 checks passed
@WillPapper WillPapper deleted the claude/add-stylus-verification-w1XaR branch March 1, 2026 01:07
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.

3 participants