From 4fcaaa739813a403be0a88ee016a9f961802fcb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20B=C4=99za?= Date: Mon, 9 Sep 2024 13:12:14 +0200 Subject: [PATCH] feat(verify-era-proof-attestation): continuous mode with attestation policies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR introduces TEE Prover continuous mode with attestation policies. Attestation policies are a set of criteria that determine whether an SGX attestation should be considered valid or invalid. In practice, this means checking against a specified set of mrsigners, mrenclaves, and TCB levels. If the attestation’s mrenclave/mrsigner/TCB levels matches those in the provided --sgx-mrenclaves/--sgx-mrsigners/--sgx-allowed-tcb-levels, we treat the attestation as successfully verified. Otherwise, the attestation is considered invalid. The --continuous mode for the TEE Prover allows it to run continuously, verifying new batches exposed by the node's RPC API in real-time. To try it out, run the following commands: $ nix build -L .#container-verify-era-proof-attestation-sgx $ export IMAGE_TAG=$(docker load -i result | grep -Po 'Loaded image.*: \K.*') $ docker run -i --init --rm $IMAGE_TAG --continuous 11505 --rpc https://sepolia.era.zksync.dev --sgx-allowed-tcb-levels Ok,SwHardeningNeeded --log-level debug $ docker run -i --init --rm $IMAGE_TAG --batch 11509 --rpc https://sepolia.era.zksync.dev --sgx-allowed-tcb-levels Ok,SwHardeningNeeded --log-level debug --- Cargo.lock | 33 ++ Cargo.toml | 2 + bin/verify-era-proof-attestation/Cargo.toml | 11 +- bin/verify-era-proof-attestation/src/args.rs | 123 ++++++ .../src/client.rs | 45 ++ bin/verify-era-proof-attestation/src/main.rs | 402 +++++++++--------- bin/verify-era-proof-attestation/src/proof.rs | 159 +++++++ .../src/verification.rs | 129 ++++++ 8 files changed, 702 insertions(+), 202 deletions(-) create mode 100644 bin/verify-era-proof-attestation/src/args.rs create mode 100644 bin/verify-era-proof-attestation/src/client.rs create mode 100644 bin/verify-era-proof-attestation/src/proof.rs create mode 100644 bin/verify-era-proof-attestation/src/verification.rs diff --git a/Cargo.lock b/Cargo.lock index 1cd4f55..ab50607 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -813,6 +813,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.38" @@ -1052,6 +1058,16 @@ dependencies = [ "cipher", ] +[[package]] +name = "ctrlc" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3" +dependencies = [ + "nix", + "windows-sys 0.59.0", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -2835,6 +2851,18 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -5617,12 +5645,17 @@ version = "0.1.2-alpha.1" dependencies = [ "anyhow", "clap", + "ctrlc", "hex", + "jsonrpsee-types", "reqwest 0.12.7", "secp256k1 0.29.0", "serde", "teepot", "tokio", + "tracing", + "tracing-log 0.2.0", + "tracing-subscriber", "url", "zksync_basic_types", "zksync_types", diff --git a/Cargo.toml b/Cargo.toml index 7f5dea0..49e0e1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ actix-http = "3" actix-tls = "3" actix-web = { version = "4.5", features = ["rustls-0_22"] } anyhow = "1.0.82" +ctrlc = "3.4" awc = { version = "3.4", features = ["rustls-0_22-webpki-roots"] } base64 = "0.22.0" bitflags = "2.5" @@ -34,6 +35,7 @@ getrandom = "0.2.14" hex = { version = "0.4.3", features = ["std"], default-features = false } intel-tee-quote-verification-rs = { package = "teepot-tee-quote-verification-rs", path = "crates/teepot-tee-quote-verification-rs", version = "0.2.3-alpha.1" } intel-tee-quote-verification-sys = { version = "0.2.1" } +jsonrpsee-types = { version = "0.23", default-features = false } secp256k1 = { version = "0.29", features = ["rand-std", "global-context"] } log = "0.4" num-integer = "0.1.46" diff --git a/bin/verify-era-proof-attestation/Cargo.toml b/bin/verify-era-proof-attestation/Cargo.toml index 63e8e4e..53e0979 100644 --- a/bin/verify-era-proof-attestation/Cargo.toml +++ b/bin/verify-era-proof-attestation/Cargo.toml @@ -1,20 +1,25 @@ [package] -name = "verify-era-proof-attestation" -version.workspace = true -edition.workspace = true authors.workspace = true +edition.workspace = true license.workspace = true +name = "verify-era-proof-attestation" repository.workspace = true +version.workspace = true [dependencies] anyhow.workspace = true clap.workspace = true +ctrlc.workspace = true hex.workspace = true +jsonrpsee-types.workspace = true reqwest.workspace = true secp256k1.workspace = true serde.workspace = true teepot.workspace = true tokio.workspace = true +tracing.workspace = true +tracing-log.workspace = true +tracing-subscriber.workspace = true url.workspace = true zksync_basic_types.workspace = true zksync_types.workspace = true diff --git a/bin/verify-era-proof-attestation/src/args.rs b/bin/verify-era-proof-attestation/src/args.rs new file mode 100644 index 0000000..95555a5 --- /dev/null +++ b/bin/verify-era-proof-attestation/src/args.rs @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2023-2024 Matter Labs + +use anyhow::{anyhow, Result}; +use clap::{ArgGroup, Args, Parser}; +use std::time::Duration; +use teepot::sgx::{parse_tcb_levels, EnumSet, TcbLevel}; +use tracing_subscriber::filter::LevelFilter; +use url::Url; +use zksync_basic_types::L1BatchNumber; +use zksync_types::L2ChainId; + +#[derive(Parser, Debug, Clone)] +#[command(author = "Matter Labs", version, about = "SGX attestation and batch signature verifier", long_about = None)] +#[clap(group( + ArgGroup::new("mode") + .required(true) + .args(&["batch_range", "continuous"]), +))] +pub struct Arguments { + #[clap(long, default_value_t = LevelFilter::WARN, value_parser = LogLevelParser)] + pub log_level: LevelFilter, + /// The batch number or range of batch numbers to verify the attestation and signature (e.g., + /// "42" or "42-45"). This option is mutually exclusive with the `--continuous` mode. + #[clap(short = 'n', long = "batch", value_parser = parse_batch_range)] + pub batch_range: Option<(L1BatchNumber, L1BatchNumber)>, + /// Continuous mode: keep verifying new batches until interrupted. This option is mutually + /// exclusive with the `--batch` option. + #[clap(long, value_name = "FIRST_BATCH")] + pub continuous: Option, + /// URL of the RPC server to query for the batch attestation and signature. + #[clap(long = "rpc")] + pub rpc_url: Url, + /// Chain ID of the network to query. + #[clap(long = "chain", default_value_t = L2ChainId::default().as_u64())] + pub chain_id: u64, + /// Rate limit between requests in milliseconds. + #[clap(long, default_value = "0", value_parser = parse_duration)] + pub rate_limit: Duration, + /// Criteria for valid attestation policy. Invalid proofs will be rejected. + #[clap(flatten)] + pub attestation_policy: AttestationPolicyArgs, +} + +/// Attestation policy implemented as a set of criteria that must be met by SGX attestation. +#[derive(Args, Debug, Clone)] +pub struct AttestationPolicyArgs { + /// Comma-separated list of allowed hex-encoded SGX mrsigners. Batch attestation must consist of + /// one of these mrsigners. If the list is empty, the mrsigner check is skipped. + #[arg(long = "mrsigners")] + pub sgx_mrsigners: Option, + /// Comma-separated list of allowed hex-encoded SGX mrenclaves. Batch attestation must consist + /// of one of these mrenclaves. If the list is empty, the mrenclave check is skipped. + #[arg(long = "mrenclaves")] + pub sgx_mrenclaves: Option, + /// Comma-separated list of allowed TCB levels. If the list is empty, the TCB level check is + /// skipped. Allowed values: Ok, ConfigNeeded, ConfigAndSwHardeningNeeded, SwHardeningNeeded, + /// OutOfDate, OutOfDateConfigNeeded. + #[arg(long, value_parser = parse_tcb_levels, default_value = "Ok")] + pub sgx_allowed_tcb_levels: EnumSet, +} + +fn parse_batch_range(s: &str) -> Result<(L1BatchNumber, L1BatchNumber)> { + let parse = |s: &str| { + s.parse::() + .map(L1BatchNumber::from) + .map_err(|e| anyhow!(e)) + }; + match s.split_once('-') { + Some((start, end)) => { + let (start, end) = (parse(start)?, parse(end)?); + if start > end { + Err(anyhow!( + "Start batch number ({}) must be less than or equal to end batch number ({})", + start, + end + )) + } else { + Ok((start, end)) + } + } + None => { + let batch_number = parse(s)?; + Ok((batch_number, batch_number)) + } + } +} + +fn parse_duration(s: &str) -> Result { + let millis = s.parse()?; + Ok(Duration::from_millis(millis)) +} + +#[derive(Clone)] +struct LogLevelParser; + +impl clap::builder::TypedValueParser for LogLevelParser { + type Value = LevelFilter; + + fn parse_ref( + &self, + cmd: &clap::Command, + arg: Option<&clap::Arg>, + value: &std::ffi::OsStr, + ) -> Result { + clap::builder::TypedValueParser::parse(self, cmd, arg, value.to_owned()) + } + + fn parse( + &self, + cmd: &clap::Command, + arg: Option<&clap::Arg>, + value: std::ffi::OsString, + ) -> std::result::Result { + use std::str::FromStr; + let p = clap::builder::PossibleValuesParser::new([ + "off", "error", "warn", "info", "debug", "trace", + ]); + let v = p.parse(cmd, arg, value)?; + + Ok(LevelFilter::from_str(&v).unwrap()) + } +} diff --git a/bin/verify-era-proof-attestation/src/client.rs b/bin/verify-era-proof-attestation/src/client.rs new file mode 100644 index 0000000..6746e38 --- /dev/null +++ b/bin/verify-era-proof-attestation/src/client.rs @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2023-2024 Matter Labs + +use anyhow::{anyhow, Context, Result}; +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, +}; + +pub 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 +} + +pub struct MainNodeClient(NodeClient); + +impl MainNodeClient { + pub fn new(rpc_url: Url, chain_id: u64) -> Result { + let node_client = NodeClient::http(rpc_url.into()) + .context("failed creating JSON-RPC client for main node")? + .for_network( + L2ChainId::try_from(chain_id) + .map_err(anyhow::Error::msg)? + .into(), + ) + .build(); + + Ok(MainNodeClient(node_client)) + } +} + +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)) + } +} diff --git a/bin/verify-era-proof-attestation/src/main.rs b/bin/verify-era-proof-attestation/src/main.rs index 5924024..cbb263a 100644 --- a/bin/verify-era-proof-attestation/src/main.rs +++ b/bin/verify-era-proof-attestation/src/main.rs @@ -1,227 +1,231 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (c) 2023-2024 Matter Labs -//! Tool for SGX attestation and batch signature verification +//! Tool for SGX attestation and batch signature verification, both continuous and one-shot -use anyhow::{anyhow, Context, Result}; +mod args; +mod client; +mod proof; +mod verification; + +use anyhow::{Context, Result}; +use args::{Arguments, AttestationPolicyArgs}; use clap::Parser; +use client::MainNodeClient; +use proof::get_proofs; 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 tokio::time::sleep; +use tokio::{signal, sync::watch}; +use tracing::{debug, error, info, trace, warn}; +use tracing_log::LogTracer; +use tracing_subscriber::{filter::LevelFilter, fmt, prelude::*, EnvFilter, Registry}; 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, +use zksync_basic_types::L1BatchNumber; + +use crate::verification::{ + log_quote_verification_summary, verify_attestation_quote, verify_batch_proof, }; -#[derive(Parser, Debug)] -#[command(author = "Matter Labs", version, about = "SGX attestation and batch signature verifier", long_about = None)] -struct Arguments { - /// The batch number or range of batch numbers to verify the attestation and signature (e.g., "42" or "42-45"). - #[clap(short = 'n', long = "batch-number", value_parser = parse_batch_range)] - batch_range: (L1BatchNumber, L1BatchNumber), - /// URL of the RPC server to query for the batch attestation and signature. - #[clap(short = 'u', long)] - rpc_url: Url, - /// Chain ID of the network to query. - #[clap(short = 'c', long, default_value_t = L2ChainId::default().as_u64())] - chain_id: u64, - /// Rate limit between requests in milliseconds. - #[clap(short = 'r', long, default_value = "0", value_parser = parse_duration)] - rate_limit: Duration, -} - -fn parse_batch_range(s: &str) -> Result<(L1BatchNumber, L1BatchNumber)> { - let parse = |s: &str| { - s.parse::() - .map(L1BatchNumber::from) - .map_err(|e| anyhow!(e)) - }; - match s.split_once('-') { - Some((start, end)) => { - let (start, end) = (parse(start)?, parse(end)?); - if start > end { - Err(anyhow!( - "Start batch number ({}) must be less than or equal to end batch number ({})", - start, - end - )) - } else { - Ok((start, end)) - } - } - None => { - let batch_number = parse(s)?; - Ok((batch_number, batch_number)) - } - } -} - -fn parse_duration(s: &str) -> Result { - let millis = s.parse()?; - Ok(Duration::from_millis(millis)) -} - -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(); - let node_client = NodeClient::http(args.rpc_url.clone().into()) - .context("failed creating JSON-RPC client for main node")? - .for_network( - L2ChainId::try_from(args.chain_id) - .map_err(anyhow::Error::msg)? - .into(), - ) - .build(); - let node_client = MainNodeClient(node_client); + setup_logging(&args.log_level)?; + validate_arguments(&args)?; + let (stop_sender, stop_receiver) = watch::channel(false); + let mut process_handle = tokio::spawn(verify_batches_proofs(stop_receiver, args)); + tokio::select! { + ret = &mut process_handle => { return ret?; }, + _ = signal::ctrl_c() => { + tracing::info!("Stop signal received, shutting down"); + stop_sender.send(true).ok(); + // Wait for process_batches to complete gracefully + process_handle.await??; + } + } + + Ok(()) +} + +fn setup_logging(log_level: &LevelFilter) -> Result<()> { + LogTracer::init().context("Failed to set logger")?; + let filter = EnvFilter::builder() + .try_from_env() + .unwrap_or(match *log_level { + LevelFilter::OFF => EnvFilter::new("off"), + _ => EnvFilter::new(format!( + "warn,{crate_name}={log_level},teepot={log_level}", + crate_name = env!("CARGO_CRATE_NAME"), + log_level = log_level + )), + }); + let subscriber = Registry::default() + .with(filter) + .with(fmt::layer().with_writer(std::io::stderr)); + tracing::subscriber::set_global_default(subscriber)?; + + Ok(()) +} + +fn validate_arguments(args: &Arguments) -> Result<()> { + if args.attestation_policy.sgx_mrsigners.is_none() + && args.attestation_policy.sgx_mrenclaves.is_none() + { + error!("Neither `--sgx-mrenclaves` nor `--sgx-mrsigners` specified. Any code could have produced the proof."); + } + + Ok(()) +} + +/// Verify all TEE proofs for all batches starting from the given batch number up to the specified +/// batch number, if a range is provided. Otherwise, continue verifying batches until the stop +/// signal is received. +async fn verify_batches_proofs( + mut stop_receiver: watch::Receiver, + args: Arguments, +) -> Result<()> { + let node_client = MainNodeClient::new(args.rpc_url.clone(), args.chain_id)?; let http_client = Client::new(); - let (start_batch_number, end_batch_number) = args.batch_range; + let first_batch_number = match args.batch_range { + Some((first_batch_number, _)) => first_batch_number, + None => args + .continuous + .expect("clap::ArgGroup should guarantee batch range or continuous option is set"), + }; + let end_batch_number = args + .batch_range + .map_or(u32::MAX, |(_, end_batch_number)| end_batch_number.0); + let mut unverified_batches_count: u32 = 0; + let mut last_processed_batch_number = first_batch_number.0; - 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?; - - for proof in proofs_response - .result - .into_iter() - .filter(|proof| proof.tee_type.to_lowercase() == "sgx") - { - println!( - "Verifying 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!(); + for current_batch_number in first_batch_number.0..=end_batch_number { + if *stop_receiver.borrow() { + tracing::warn!("Stop signal received, shutting down"); + break; } - sleep(args.rate_limit).await; + trace!("Verifying TEE proofs for batch #{}", current_batch_number); + + let all_verified = verify_batch_proofs( + &mut stop_receiver, + current_batch_number.into(), + &args.rpc_url, + &http_client, + &node_client, + &args.attestation_policy, + ) + .await?; + + if !all_verified { + unverified_batches_count += 1; + } + + if current_batch_number < end_batch_number { + tokio::time::timeout(args.rate_limit, stop_receiver.changed()) + .await + .ok(); + } + + last_processed_batch_number = current_batch_number; } - Ok(()) -} + let verified_batches_count = + last_processed_batch_number + 1 - first_batch_number.0 - unverified_batches_count; -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"); + if unverified_batches_count > 0 { + if verified_batches_count == 0 { + error!( + "All {} batches failed verification!", + unverified_batches_count + ); + } else { + error!( + "Some batches failed verification! Unverified batches: {}. Verified batches: {}.", + unverified_batches_count, verified_batches_count + ); + } } else { - println!("Failed to verify signature"); + info!( + "All {} batches verified successfully!", + verified_batches_count + ); } + Ok(()) } -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") -} +/// Verify all TEE proofs for the given batch number. Note that each batch number can potentially +/// have multiple proofs of the same TEE type. +async fn verify_batch_proofs( + stop_receiver: &mut watch::Receiver, + batch_number: L1BatchNumber, + rpc_url: &Url, + http_client: &Client, + node_client: &MainNodeClient, + attestation_policy: &AttestationPolicyArgs, +) -> Result { + let proofs = get_proofs(stop_receiver, batch_number, http_client, rpc_url).await?; + let batch_no = batch_number.0; + let mut total_proofs_count: u32 = 0; + let mut unverified_proofs_count: u32 = 0; -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"); + for proof in proofs + .into_iter() + // only support SGX proofs for now + .filter(|proof| proof.tee_type.eq_ignore_ascii_case("sgx")) + { + let batch_no = proof.l1_batch_number; + + total_proofs_count += 1; + let tee_type = proof.tee_type.to_uppercase(); + + trace!(batch_no, tee_type, proof.proved_at, "Verifying proof."); + + debug!( + batch_no, + "Verifying quote ({} bytes)...", + proof.attestation.len() + ); + let quote_verification_result = verify_attestation_quote(&proof.attestation)?; + let verified_successfully = verify_batch_proof( + "e_verification_result, + attestation_policy, + node_client, + &proof.signature, + L1BatchNumber(proof.l1_batch_number), + ) + .await?; + + log_quote_verification_summary("e_verification_result); + + if verified_successfully { + info!( + batch_no, + proof.proved_at, tee_type, "Verification succeeded.", + ); + } else { + unverified_proofs_count += 1; + warn!(batch_no, proof.proved_at, tee_type, "Verification failed!",); + } } - let tcblevel = TcbLevel::from(*result); - for advisory in advisories { - println!("\tInfo: Advisory ID: {advisory}"); + + let verified_proofs_count = total_proofs_count - unverified_proofs_count; + if unverified_proofs_count > 0 { + if verified_proofs_count == 0 { + error!( + batch_no, + "All {} proofs failed verification!", unverified_proofs_count + ); + } else { + warn!( + batch_no, + "Some proofs failed verification. Unverified proofs: {}. Verified proofs: {}.", + unverified_proofs_count, + verified_proofs_count + ); + } } - 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)); + + // if at least one proof is verified, consider the batch verified + let is_batch_verified = verified_proofs_count > 0; + + Ok(is_batch_verified) } diff --git a/bin/verify-era-proof-attestation/src/proof.rs b/bin/verify-era-proof-attestation/src/proof.rs new file mode 100644 index 0000000..6f1fdb7 --- /dev/null +++ b/bin/verify-era-proof-attestation/src/proof.rs @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2023-2024 Matter Labs + +use anyhow::{bail, Result}; +use jsonrpsee_types::error::ErrorObject; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use std::time::Duration; +use tokio::sync::watch; +use tracing::{error, warn}; +use url::Url; +use zksync_basic_types::L1BatchNumber; + +#[derive(Debug, Serialize, Deserialize)] +pub struct GetProofsRequest { + pub jsonrpc: String, + pub id: u32, + pub method: String, + pub params: (L1BatchNumber, String), +} + +pub async fn get_proofs( + stop_receiver: &mut watch::Receiver, + batch_number: L1BatchNumber, + http_client: &Client, + rpc_url: &Url, +) -> Result> { + let mut proofs_request = GetProofsRequest::new(batch_number); + let mut retries = 0; + let mut backoff = Duration::from_secs(1); + let max_backoff = Duration::from_secs(128); + let retry_backoff_multiplier: f32 = 2.0; + + while !*stop_receiver.borrow() { + let proofs = proofs_request + .send(stop_receiver, http_client, rpc_url) + .await?; + + if !proofs.is_empty() { + return Ok(proofs); + } + + retries += 1; + warn!( + batch_no = batch_number.0, retries, + "No TEE proofs found for batch #{}. They may not be ready yet. Retrying in {} milliseconds.", + batch_number, backoff.as_millis(), + ); + + tokio::time::timeout(backoff, stop_receiver.changed()) + .await + .ok(); + + backoff = std::cmp::min(backoff.mul_f32(retry_backoff_multiplier), max_backoff); + } + + Ok(vec![]) +} + +impl GetProofsRequest { + pub fn new(batch_number: L1BatchNumber) -> Self { + GetProofsRequest { + jsonrpc: "2.0".to_string(), + id: 1, + method: "unstable_getTeeProofs".to_string(), + params: (batch_number, "sgx".to_string()), + } + } + + pub async fn send( + &mut self, + stop_receiver: &mut watch::Receiver, + http_client: &Client, + rpc_url: &Url, + ) -> Result> { + let mut retries = 0; + let max_retries = 5; + let mut backoff = Duration::from_secs(1); + let max_backoff = Duration::from_secs(128); + let retry_backoff_multiplier: f32 = 2.0; + let mut response = None; + + while !*stop_receiver.borrow() { + let result = http_client + .post(rpc_url.clone()) + .json(self) + .send() + .await? + .error_for_status()? + .json::() + .await; + + match result { + Ok(res) => match res.error { + None => { + response = Some(res); + break; + } + Some(error) => { + // Handle corner case, where the old RPC interface expects 'Sgx' + if let Some(data) = error.data() { + if data.get().contains("unknown variant `sgx`, expected `Sgx`") { + self.params.1 = "Sgx".to_string(); + continue; + } + } + error!(?error, "received JSONRPC error {error:?}"); + bail!("JSONRPC error {error:?}"); + } + }, + Err(err) => { + retries += 1; + if retries >= max_retries { + return Err(anyhow::anyhow!( + "Failed to send request to {} after {} retries: {}. Request details: {:?}", + rpc_url, + max_retries, + err, + self + )); + } + warn!( + %err, + "Failed to send request to {rpc_url}. {retries}/{max_retries}, retrying in {} milliseconds. Request details: {:?}", + backoff.as_millis(), + self + ); + tokio::time::timeout(backoff, stop_receiver.changed()) + .await + .ok(); + backoff = std::cmp::min(backoff.mul_f32(retry_backoff_multiplier), max_backoff); + } + }; + } + + Ok(response.map_or_else(Vec::new, |res| res.result.unwrap_or_default())) + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GetProofsResponse { + pub jsonrpc: String, + pub result: Option>, + pub id: u32, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option>, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Proof { + pub l1_batch_number: u32, + pub tee_type: String, + pub pubkey: Vec, + pub signature: Vec, + pub proof: Vec, + pub proved_at: String, + pub attestation: Vec, +} diff --git a/bin/verify-era-proof-attestation/src/verification.rs b/bin/verify-era-proof-attestation/src/verification.rs new file mode 100644 index 0000000..18d381b --- /dev/null +++ b/bin/verify-era-proof-attestation/src/verification.rs @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2023-2024 Matter Labs + +use anyhow::{Context, Result}; +use hex::encode; +use secp256k1::{constants::PUBLIC_KEY_SIZE, ecdsa::Signature, Message, PublicKey}; +use teepot::{ + client::TcbLevel, + sgx::{tee_qv_get_collateral, verify_quote_with_collateral, QuoteVerificationResult}, +}; +use tracing::{debug, info, warn}; +use zksync_basic_types::{L1BatchNumber, H256}; + +use crate::args::AttestationPolicyArgs; +use crate::client::JsonRpcClient; + +pub async fn verify_batch_proof( + quote_verification_result: &QuoteVerificationResult<'_>, + attestation_policy: &AttestationPolicyArgs, + node_client: &impl JsonRpcClient, + signature: &[u8], + batch_number: L1BatchNumber, +) -> Result { + if !is_quote_matching_policy(attestation_policy, quote_verification_result) { + return Ok(false); + } + + let batch_no = batch_number.0; + + let public_key = PublicKey::from_slice( + "e_verification_result.quote.report_body.reportdata[..PUBLIC_KEY_SIZE], + )?; + debug!(batch_no, "public key: {}", public_key); + + let root_hash = node_client.get_root_hash(batch_number).await?; + debug!(batch_no, "root hash: {}", root_hash); + + let is_verified = verify_signature(signature, public_key, root_hash)?; + if is_verified { + info!(batch_no, signature = %encode(signature), "Signature verified successfully."); + } else { + warn!(batch_no, signature = %encode(signature), "Failed to verify signature!"); + } + Ok(is_verified) +} + +pub fn verify_attestation_quote(attestation_quote_bytes: &[u8]) -> Result { + 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!") +} + +pub fn log_quote_verification_summary(quote_verification_result: &QuoteVerificationResult) { + let QuoteVerificationResult { + collateral_expired, + result, + quote, + advisories, + .. + } = quote_verification_result; + if *collateral_expired { + warn!("Freshly fetched collateral expired!"); + } + let tcblevel = TcbLevel::from(*result); + info!( + "Quote verification result: {}. mrsigner: {}, mrenclave: {}, reportdata: {}. Advisory IDs: {}.", + tcblevel, + hex::encode(quote.report_body.mrsigner), + hex::encode(quote.report_body.mrenclave), + hex::encode(quote.report_body.reportdata), + if advisories.is_empty() { + "None".to_string() + } else { + advisories.iter().map(ToString::to_string).collect::>().join(", ") + } + ); +} + +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)?; + Ok(signature.verify(&root_hash_msg, &public_key).is_ok()) +} + +fn is_quote_matching_policy( + attestation_policy: &AttestationPolicyArgs, + quote_verification_result: &QuoteVerificationResult<'_>, +) -> bool { + let quote = "e_verification_result.quote; + let tcblevel = TcbLevel::from(quote_verification_result.result); + + if !attestation_policy.sgx_allowed_tcb_levels.contains(tcblevel) { + warn!( + "Quote verification failed: TCB level mismatch (expected one of: {:?}, actual: {})", + attestation_policy.sgx_allowed_tcb_levels, tcblevel + ); + return false; + } + + check_policy( + attestation_policy.sgx_mrsigners.as_deref(), + "e.report_body.mrsigner, + "mrsigner", + ) && check_policy( + attestation_policy.sgx_mrenclaves.as_deref(), + "e.report_body.mrenclave, + "mrenclave", + ) +} + +fn check_policy(policy: Option<&str>, actual_value: &[u8], field_name: &str) -> bool { + if let Some(valid_values) = policy { + let valid_values: Vec<&str> = valid_values.split(',').collect(); + let actual_value = hex::encode(actual_value); + if !valid_values.contains(&actual_value.as_str()) { + warn!( + "Quote verification failed: {} mismatch (expected one of: {:?}, actual: {})", + field_name, valid_values, actual_value + ); + return false; + } + debug!(field_name, actual_value, "Attestation policy check passed"); + } + true +}