diff --git a/bin/verify-era-proof-attestation/src/main.rs b/bin/verify-era-proof-attestation/src/main.rs index 8398df6..209b7d2 100644 --- a/bin/verify-era-proof-attestation/src/main.rs +++ b/bin/verify-era-proof-attestation/src/main.rs @@ -3,11 +3,15 @@ //! Tool for SGX attestation and batch signature verification +mod rpc_api; +mod verifier; + use anyhow::{anyhow, Context, Result}; use clap::Parser; use reqwest::Client; use secp256k1::{constants::PUBLIC_KEY_SIZE, ecdsa::Signature, Message, PublicKey}; use serde::{Deserialize, Serialize}; +use std::time::Duration; use teepot::{ client::TcbLevel, sgx::{tee_qv_get_collateral, verify_quote_with_collateral, QuoteVerificationResult}, @@ -21,6 +25,9 @@ use zksync_web3_decl::{ namespaces::ZksNamespaceClient, }; +use rpc_api::MainNodeClient; +use verifier::verify_proof; + #[derive(Parser, Debug)] #[command(author = "Matter Labs", version, about = "SGX attestation and batch signature verifier", long_about = None)] struct Arguments { @@ -33,6 +40,9 @@ struct Arguments { /// Chain ID of the network to query. #[clap(short, long, default_value_t = L2ChainId::default().as_u64())] chain_id: u64, + /// Run continuously, polling for new batch ranges from the RPC server. + #[clap(short, long)] + continuous: bool, } fn parse_batch_range(s: &str) -> Result<(L1BatchNumber, L1BatchNumber)> { @@ -61,55 +71,6 @@ fn parse_batch_range(s: &str) -> Result<(L1BatchNumber, L1BatchNumber)> { } } -trait JsonRpcClient { - async fn get_root_hash(&self, batch_number: L1BatchNumber) -> Result; - // TODO implement get_tee_proofs(batch_number, tee_type) once https://crates.io/crates/zksync_web3_decl crate is updated -} - -struct MainNodeClient(NodeClient); - -impl JsonRpcClient for MainNodeClient { - async fn get_root_hash(&self, batch_number: L1BatchNumber) -> Result { - self.0 - .get_l1_batch_details(batch_number) - .rpc_context("get_l1_batch_details") - .await? - .and_then(|res| res.base.root_hash) - .ok_or_else(|| anyhow!("No root hash found for batch #{}", batch_number)) - } -} - -// JSON-RPC request and response structures for fetching TEE proofs - -#[derive(Debug, Serialize, Deserialize)] -struct GetProofsRequest { - jsonrpc: String, - id: u32, - method: String, - params: (L1BatchNumber, String), -} - -#[derive(Debug, Serialize, Deserialize)] -struct GetProofsResponse { - jsonrpc: String, - result: Vec, - id: u32, -} - -#[derive(Debug, Serialize, Deserialize)] -struct Proof { - #[serde(rename = "l1BatchNumber")] - l1_batch_number: u32, - #[serde(rename = "teeType")] - tee_type: String, - pubkey: Vec, - signature: Vec, - proof: Vec, - #[serde(rename = "provedAt")] - proved_at: String, - attestation: Vec, -} - #[tokio::main] async fn main() -> Result<()> { let args = Arguments::parse(); @@ -123,93 +84,81 @@ async fn main() -> Result<()> { .build(); let node_client = MainNodeClient(node_client); let http_client = Client::new(); - let (start_batch_number, end_batch_number) = args.batch_range; + + if args.continuous { + run_continuous_mode(&http_client, &node_client, &args.rpc_url).await?; + } else { + run_once(&http_client, &node_client, &args.rpc_url, args.batch_range).await?; + } + + Ok(()) +} + +async fn run_continuous_mode( + http_client: &Client, + node_client: &MainNodeClient, + rpc_url: &Url, +) -> Result<()> { + loop { + let batch_range = poll_latest_batch_range(http_client, rpc_url).await?; + run_once(http_client, node_client, batch_range, rpc_url).await?; + tokio::time::sleep(Duration::from_secs(10)).await; + } +} + +async fn run_once( + http_client: &Client, + node_client: &MainNodeClient, + rpc_url: &Url, + batch_range: (L1BatchNumber, L1BatchNumber), +) -> Result<()> { + let (start_batch_number, end_batch_number) = batch_range; for batch_number in start_batch_number.0..=end_batch_number.0 { - let proofs_request = GetProofsRequest { - jsonrpc: "2.0".to_string(), - id: 1, - method: "unstable_getTeeProofs".to_string(), - params: (L1BatchNumber(batch_number), "Sgx".to_string()), - }; - let proofs_response = http_client - .post(args.rpc_url.clone()) - .json(&proofs_request) - .send() - .await? - .error_for_status()? - .json::() - .await?; + println!("Verifying batch #{}", proof.l1_batch_number); + + let proofs_response = get_tee_proofs(batch_number, http_client, rpc_url).await?; + let mut batch_proof_instance = 1; for proof in proofs_response .result .into_iter() - .filter(|proof| proof.tee_type.to_lowercase() == "sgx") + .filter(|proof| proof.tee_type.eq_ignore_ascii_case("Sgx")) { println!( - "Verifying batch #{} proved at {}", + " Verifying proof instance {} of the batch, proved at {}" proof.l1_batch_number, proof.proved_at ); - let quote_verification_result = verify_attestation_quote(&proof.attestation)?; - print_quote_verification_summary("e_verification_result); - let public_key = PublicKey::from_slice( - "e_verification_result.quote.report_body.reportdata[..PUBLIC_KEY_SIZE], - )?; - println!("Public key from attestation quote: {}", public_key); - let root_hash = node_client - .get_root_hash(L1BatchNumber(proof.l1_batch_number)) - .await?; - println!("Root hash: {}", root_hash); - verify_signature(&proof.signature, public_key, root_hash)?; - println!(); + let verification_result = verify_proof(proof, node_client).await?; + batch_proof_instance += 1; } + + println!(); } Ok(()) } -fn verify_signature(signature: &[u8], public_key: PublicKey, root_hash: H256) -> Result<()> { - let signature = Signature::from_compact(signature)?; - let root_hash_msg = Message::from_digest_slice(&root_hash.0)?; - if signature.verify(&root_hash_msg, &public_key).is_ok() { - println!("Signature verified successfully"); - } else { - println!("Failed to verify signature"); - } - Ok(()) -} +async fn get_tee_proofs( + batch_number: u32, + http_client: &Client, + rpc_url: &Url, +) -> Result> { + let proofs_request = GetProofsRequest { + jsonrpc: "2.0".to_string(), + id: 1, + method: "unstable_getTeeProofs".to_string(), + params: (L1BatchNumber(batch_number), "Sgx".to_string()), + }; + let proofs_response = http_client + .post(rpc_url.clone()) + .json(&proofs_request) + .send() + .await? + .error_for_status()? + .json::() + .await; + // TODO return 404 if no proofs found -fn verify_attestation_quote(attestation_quote_bytes: &[u8]) -> Result { - println!( - "Verifying quote ({} bytes)...", - attestation_quote_bytes.len() - ); - let collateral = - tee_qv_get_collateral(attestation_quote_bytes).context("Failed to get collateral")?; - let unix_time: i64 = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH)? - .as_secs() as _; - verify_quote_with_collateral(attestation_quote_bytes, Some(&collateral), unix_time) - .context("Failed to verify quote with collateral") -} - -fn print_quote_verification_summary(quote_verification_result: &QuoteVerificationResult) { - let QuoteVerificationResult { - collateral_expired, - result, - quote, - advisories, - .. - } = quote_verification_result; - if *collateral_expired { - println!("Freshly fetched collateral expired"); - } - let tcblevel = TcbLevel::from(*result); - for advisory in advisories { - println!("\tInfo: Advisory ID: {advisory}"); - } - println!("Quote verification result: {}", tcblevel); - println!("mrsigner: {}", hex::encode(quote.report_body.mrsigner)); - println!("mrenclave: {}", hex::encode(quote.report_body.mrenclave)); - println!("reportdata: {}", hex::encode(quote.report_body.reportdata)); + Ok(proofs_response) } diff --git a/bin/verify-era-proof-attestation/src/rpc_api.rs b/bin/verify-era-proof-attestation/src/rpc_api.rs new file mode 100644 index 0000000..b09e35f --- /dev/null +++ b/bin/verify-era-proof-attestation/src/rpc_api.rs @@ -0,0 +1,67 @@ +use anyhow::{anyhow, Context, Result}; +use clap::Parser; +use reqwest::Client; +use secp256k1::{constants::PUBLIC_KEY_SIZE, ecdsa::Signature, Message, PublicKey}; +use serde::{Deserialize, Serialize}; +use std::time::Duration; +use teepot::{ + client::TcbLevel, + sgx::{tee_qv_get_collateral, verify_quote_with_collateral, QuoteVerificationResult}, +}; +use url::Url; +use zksync_basic_types::{L1BatchNumber, H256}; +use zksync_types::L2ChainId; +use zksync_web3_decl::{ + client::{Client as NodeClient, L2}, + error::ClientRpcContext, + namespaces::ZksNamespaceClient, +}; + +trait JsonRpcClient { + async fn get_root_hash(&self, batch_number: L1BatchNumber) -> Result; + // TODO implement get_tee_proofs(batch_number, tee_type) once https://crates.io/crates/zksync_web3_decl crate is updated +} + +struct MainNodeClient(NodeClient); + +impl JsonRpcClient for MainNodeClient { + async fn get_root_hash(&self, batch_number: L1BatchNumber) -> Result { + self.0 + .get_l1_batch_details(batch_number) + .rpc_context("get_l1_batch_details") + .await? + .and_then(|res| res.base.root_hash) + .ok_or_else(|| anyhow!("No root hash found for batch #{}", batch_number)) + } +} + +// JSON-RPC request and response structures for fetching TEE proofs + +#[derive(Debug, Serialize, Deserialize)] +struct GetProofsRequest { + jsonrpc: String, + id: u32, + method: String, + params: (L1BatchNumber, String), +} + +#[derive(Debug, Serialize, Deserialize)] +struct GetProofsResponse { + jsonrpc: String, + result: Vec, + id: u32, +} + +#[derive(Debug, Serialize, Deserialize)] +struct Proof { + #[serde(rename = "l1BatchNumber")] + l1_batch_number: u32, + #[serde(rename = "teeType")] + tee_type: String, + pubkey: Vec, + signature: Vec, + proof: Vec, + #[serde(rename = "provedAt")] + proved_at: String, + attestation: Vec, +} diff --git a/bin/verify-era-proof-attestation/src/verifier.rs b/bin/verify-era-proof-attestation/src/verifier.rs new file mode 100644 index 0000000..b73477e --- /dev/null +++ b/bin/verify-era-proof-attestation/src/verifier.rs @@ -0,0 +1,82 @@ +use anyhow::{anyhow, Context, Result}; +use clap::Parser; +use reqwest::Client; +use secp256k1::{constants::PUBLIC_KEY_SIZE, ecdsa::Signature, Message, PublicKey}; +use serde::{Deserialize, Serialize}; +use std::time::Duration; +use teepot::{ + client::TcbLevel, + sgx::{tee_qv_get_collateral, verify_quote_with_collateral, QuoteVerificationResult}, +}; +use url::Url; +use zksync_basic_types::{L1BatchNumber, H256}; +use zksync_types::L2ChainId; +use zksync_web3_decl::{ + client::{Client as NodeClient, L2}, + error::ClientRpcContext, + namespaces::ZksNamespaceClient, +}; + +fn verify_signature(signature: &[u8], public_key: PublicKey, root_hash: H256) -> Result<()> { + let signature = Signature::from_compact(signature)?; + let root_hash_msg = Message::from_digest_slice(&root_hash.0)?; + if signature.verify(&root_hash_msg, &public_key).is_ok() { + println!(" Signature verified successfully"); + } else { + println!(" Failed to verify signature"); + } + Ok(()) +} + +async fn verify_proof(proof: Proof, node_client: &MainNodeClient) -> Result<(), anyhow::Error> { + let quote_verification_result = verify_attestation_quote(&proof.attestation)?; + print_quote_verification_summary("e_verification_result); + let public_key = PublicKey::from_slice( + "e_verification_result.quote.report_body.reportdata[..PUBLIC_KEY_SIZE], + )?; + println!(" Public key from attestation quote: {}", public_key); + let root_hash = node_client + .get_root_hash(L1BatchNumber(proof.l1_batch_number)) + .await?; + println!(" Root hash: {}", root_hash); + verify_signature(&proof.signature, public_key, root_hash)?; + Ok(()) +} + +fn print_quote_verification_summary(quote_verification_result: &QuoteVerificationResult) { + let QuoteVerificationResult { + collateral_expired, + result, + quote, + advisories, + .. + } = quote_verification_result; + if *collateral_expired { + println!(" Freshly fetched collateral expired"); + } + let tcblevel = TcbLevel::from(*result); + for advisory in advisories { + println!(" \tInfo: Advisory ID: {advisory}"); + } + println!(" Quote verification result: {}", tcblevel); + println!(" mrsigner: {}", hex::encode(quote.report_body.mrsigner)); + println!(" mrenclave: {}", hex::encode(quote.report_body.mrenclave)); + println!( + " reportdata: {}", + hex::encode(quote.report_body.reportdata) + ); +} + +fn verify_attestation_quote(attestation_quote_bytes: &[u8]) -> Result { + println!( + "Verifying quote ({} bytes)...", + attestation_quote_bytes.len() + ); + let collateral = + tee_qv_get_collateral(attestation_quote_bytes).context("Failed to get collateral")?; + let unix_time: i64 = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH)? + .as_secs() as _; + verify_quote_with_collateral(attestation_quote_bytes, Some(&collateral), unix_time) + .context("Failed to verify quote with collateral") +}