This commit is contained in:
Patryk Bęza 2024-09-03 12:12:25 +02:00
parent 8dbfe47e2f
commit 85bba1d697
No known key found for this signature in database
GPG key ID: 9AD1B44D9F6258EC
3 changed files with 219 additions and 121 deletions

View file

@ -3,11 +3,15 @@
//! Tool for SGX attestation and batch signature verification //! Tool for SGX attestation and batch signature verification
mod rpc_api;
mod verifier;
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use clap::Parser; use clap::Parser;
use reqwest::Client; use reqwest::Client;
use secp256k1::{constants::PUBLIC_KEY_SIZE, ecdsa::Signature, Message, PublicKey}; use secp256k1::{constants::PUBLIC_KEY_SIZE, ecdsa::Signature, Message, PublicKey};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::time::Duration;
use teepot::{ use teepot::{
client::TcbLevel, client::TcbLevel,
sgx::{tee_qv_get_collateral, verify_quote_with_collateral, QuoteVerificationResult}, sgx::{tee_qv_get_collateral, verify_quote_with_collateral, QuoteVerificationResult},
@ -21,6 +25,9 @@ use zksync_web3_decl::{
namespaces::ZksNamespaceClient, namespaces::ZksNamespaceClient,
}; };
use rpc_api::MainNodeClient;
use verifier::verify_proof;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(author = "Matter Labs", version, about = "SGX attestation and batch signature verifier", long_about = None)] #[command(author = "Matter Labs", version, about = "SGX attestation and batch signature verifier", long_about = None)]
struct Arguments { struct Arguments {
@ -33,6 +40,9 @@ struct Arguments {
/// Chain ID of the network to query. /// Chain ID of the network to query.
#[clap(short, long, default_value_t = L2ChainId::default().as_u64())] #[clap(short, long, default_value_t = L2ChainId::default().as_u64())]
chain_id: 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)> { 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<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] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
let args = Arguments::parse(); let args = Arguments::parse();
@ -123,93 +84,81 @@ async fn main() -> Result<()> {
.build(); .build();
let node_client = MainNodeClient(node_client); let node_client = MainNodeClient(node_client);
let http_client = Client::new(); 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 { for batch_number in start_batch_number.0..=end_batch_number.0 {
let proofs_request = GetProofsRequest { println!("Verifying batch #{}", proof.l1_batch_number);
jsonrpc: "2.0".to_string(),
id: 1, let proofs_response = get_tee_proofs(batch_number, http_client, rpc_url).await?;
method: "unstable_getTeeProofs".to_string(), let mut batch_proof_instance = 1;
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 for proof in proofs_response
.result .result
.into_iter() .into_iter()
.filter(|proof| proof.tee_type.to_lowercase() == "sgx") .filter(|proof| proof.tee_type.eq_ignore_ascii_case("Sgx"))
{ {
println!( println!(
"Verifying batch #{} proved at {}", " Verifying proof instance {} of the batch, proved at {}"
proof.l1_batch_number, proof.proved_at proof.l1_batch_number, proof.proved_at
); );
let quote_verification_result = verify_attestation_quote(&proof.attestation)?; let verification_result = verify_proof(proof, node_client).await?;
print_quote_verification_summary(&quote_verification_result); batch_proof_instance += 1;
let public_key = PublicKey::from_slice(
&quote_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!();
} }
println!();
} }
Ok(()) Ok(())
} }
fn verify_signature(signature: &[u8], public_key: PublicKey, root_hash: H256) -> Result<()> { async fn get_tee_proofs(
let signature = Signature::from_compact(signature)?; batch_number: u32,
let root_hash_msg = Message::from_digest_slice(&root_hash.0)?; http_client: &Client,
if signature.verify(&root_hash_msg, &public_key).is_ok() { rpc_url: &Url,
println!("Signature verified successfully"); ) -> Result<Vec<Proof>> {
} else { let proofs_request = GetProofsRequest {
println!("Failed to verify signature"); jsonrpc: "2.0".to_string(),
} id: 1,
Ok(()) 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::<GetProofsResponse>()
.await;
// TODO return 404 if no proofs found
fn verify_attestation_quote(attestation_quote_bytes: &[u8]) -> Result<QuoteVerificationResult> { Ok(proofs_response)
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));
} }

View file

@ -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<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>,
}

View file

@ -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(&quote_verification_result);
let public_key = PublicKey::from_slice(
&quote_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<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")
}