mirror of
https://github.com/matter-labs/teepot.git
synced 2025-07-20 22:53:57 +02:00
Merge pull request #198 from matter-labs/patrick/attestation-acceptance-params
feat(verify-era-proof-attestation): added continuous mode with attestation policies
This commit is contained in:
commit
108ef8cc07
8 changed files with 702 additions and 202 deletions
33
Cargo.lock
generated
33
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
123
bin/verify-era-proof-attestation/src/args.rs
Normal file
123
bin/verify-era-proof-attestation/src/args.rs
Normal file
|
@ -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<L1BatchNumber>,
|
||||
/// 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<String>,
|
||||
/// 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<String>,
|
||||
/// 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<TcbLevel>,
|
||||
}
|
||||
|
||||
fn parse_batch_range(s: &str) -> Result<(L1BatchNumber, L1BatchNumber)> {
|
||||
let parse = |s: &str| {
|
||||
s.parse::<u32>()
|
||||
.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<Duration> {
|
||||
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<Self::Value, clap::Error> {
|
||||
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<Self::Value, clap::Error> {
|
||||
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())
|
||||
}
|
||||
}
|
45
bin/verify-era-proof-attestation/src/client.rs
Normal file
45
bin/verify-era-proof-attestation/src/client.rs
Normal file
|
@ -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<H256>;
|
||||
// TODO implement get_tee_proofs(batch_number, tee_type) once https://crates.io/crates/zksync_web3_decl crate is updated
|
||||
}
|
||||
|
||||
pub struct MainNodeClient(NodeClient<L2>);
|
||||
|
||||
impl MainNodeClient {
|
||||
pub fn new(rpc_url: Url, chain_id: u64) -> Result<Self> {
|
||||
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<H256> {
|
||||
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))
|
||||
}
|
||||
}
|
|
@ -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::<u32>()
|
||||
.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<Duration> {
|
||||
let millis = s.parse()?;
|
||||
Ok(Duration::from_millis(millis))
|
||||
}
|
||||
|
||||
trait JsonRpcClient {
|
||||
async fn get_root_hash(&self, batch_number: L1BatchNumber) -> Result<H256>;
|
||||
// TODO implement get_tee_proofs(batch_number, tee_type) once https://crates.io/crates/zksync_web3_decl crate is updated
|
||||
}
|
||||
|
||||
struct MainNodeClient(NodeClient<L2>);
|
||||
|
||||
impl JsonRpcClient for MainNodeClient {
|
||||
async fn get_root_hash(&self, batch_number: L1BatchNumber) -> Result<H256> {
|
||||
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<Proof>,
|
||||
id: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Proof {
|
||||
#[serde(rename = "l1BatchNumber")]
|
||||
l1_batch_number: u32,
|
||||
#[serde(rename = "teeType")]
|
||||
tee_type: String,
|
||||
pubkey: Vec<u8>,
|
||||
signature: Vec<u8>,
|
||||
proof: Vec<u8>,
|
||||
#[serde(rename = "provedAt")]
|
||||
proved_at: String,
|
||||
attestation: Vec<u8>,
|
||||
}
|
||||
|
||||
#[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<bool>,
|
||||
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::<GetProofsResponse>()
|
||||
.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<QuoteVerificationResult> {
|
||||
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<bool>,
|
||||
batch_number: L1BatchNumber,
|
||||
rpc_url: &Url,
|
||||
http_client: &Client,
|
||||
node_client: &MainNodeClient,
|
||||
attestation_policy: &AttestationPolicyArgs,
|
||||
) -> Result<bool> {
|
||||
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)
|
||||
}
|
||||
|
|
159
bin/verify-era-proof-attestation/src/proof.rs
Normal file
159
bin/verify-era-proof-attestation/src/proof.rs
Normal file
|
@ -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<bool>,
|
||||
batch_number: L1BatchNumber,
|
||||
http_client: &Client,
|
||||
rpc_url: &Url,
|
||||
) -> Result<Vec<Proof>> {
|
||||
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<bool>,
|
||||
http_client: &Client,
|
||||
rpc_url: &Url,
|
||||
) -> Result<Vec<Proof>> {
|
||||
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::<GetProofsResponse>()
|
||||
.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<Vec<Proof>>,
|
||||
pub id: u32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<ErrorObject<'static>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Proof {
|
||||
pub l1_batch_number: u32,
|
||||
pub tee_type: String,
|
||||
pub pubkey: Vec<u8>,
|
||||
pub signature: Vec<u8>,
|
||||
pub proof: Vec<u8>,
|
||||
pub proved_at: String,
|
||||
pub attestation: Vec<u8>,
|
||||
}
|
129
bin/verify-era-proof-attestation/src/verification.rs
Normal file
129
bin/verify-era-proof-attestation/src/verification.rs
Normal file
|
@ -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<bool> {
|
||||
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<QuoteVerificationResult> {
|
||||
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::<Vec<_>>().join(", ")
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
fn verify_signature(signature: &[u8], public_key: PublicKey, root_hash: H256) -> Result<bool> {
|
||||
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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue