From a74e5b2ebcb7aa904892d4d60182d140c5d0729d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20B=C4=99za?= Date: Tue, 31 Dec 2024 15:47:40 +0100 Subject: [PATCH] feat(tee-proof-verifier): add support for Solidity-compatible pubkey in report_data This PR is part of the effort to implement on-chain TEE proof verification. This PR goes hand in hand with: - https://github.com/matter-labs/zksync-era/pull/3414 - https://github.com/matter-labs/teepot/pull/228 --- Cargo.lock | 5 +- bin/tee-key-preexec/Cargo.toml | 1 - bin/tee-key-preexec/src/main.rs | 23 ++--- bin/verify-attestation/Cargo.toml | 1 + bin/verify-attestation/src/main.rs | 32 +++++-- .../src/verification.rs | 51 ++++++----- crates/teepot/Cargo.toml | 4 +- crates/teepot/src/ethereum/mod.rs | 86 +++++++++++++++++++ crates/teepot/src/lib.rs | 1 + 9 files changed, 155 insertions(+), 49 deletions(-) create mode 100644 crates/teepot/src/ethereum/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 4ceefab..b770552 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5217,7 +5217,6 @@ dependencies = [ "hex", "rand", "secp256k1 0.29.1", - "sha3", "teepot", "tracing", "tracing-log 0.2.0", @@ -5326,10 +5325,12 @@ dependencies = [ "rand", "rsa", "rustls", + "secp256k1 0.29.1", "serde", "serde_json", "serde_with 3.11.0", "sha2", + "sha3", "signature 2.2.0", "tdx-attest-rs", "teepot-tee-quote-verification-rs", @@ -5343,6 +5344,7 @@ dependencies = [ "webpki-roots", "x509-cert", "zeroize", + "zksync_basic_types", ] [[package]] @@ -6033,6 +6035,7 @@ dependencies = [ "clap 4.5.23", "hex", "secp256k1 0.29.1", + "sha3", "teepot", "zksync_basic_types", ] diff --git a/bin/tee-key-preexec/Cargo.toml b/bin/tee-key-preexec/Cargo.toml index 7732d68..20d3e37 100644 --- a/bin/tee-key-preexec/Cargo.toml +++ b/bin/tee-key-preexec/Cargo.toml @@ -15,7 +15,6 @@ clap.workspace = true hex.workspace = true rand.workspace = true secp256k1.workspace = true -sha3.workspace = true teepot.workspace = true tracing.workspace = true tracing-log.workspace = true diff --git a/bin/tee-key-preexec/src/main.rs b/bin/tee-key-preexec/src/main.rs index 2d61478..ac38432 100644 --- a/bin/tee-key-preexec/src/main.rs +++ b/bin/tee-key-preexec/src/main.rs @@ -8,9 +8,9 @@ use anyhow::{Context, Result}; use clap::Parser; -use secp256k1::{rand, PublicKey, Secp256k1, SecretKey}; -use sha3::{Digest, Keccak256}; +use secp256k1::{rand, Secp256k1}; use std::{ffi::OsString, os::unix::process::CommandExt, process::Command}; +use teepot::ethereum::public_key_to_ethereum_address; use teepot::quote::get_quote; use tracing::error; use tracing_log::LogTracer; @@ -29,19 +29,6 @@ struct Args { cmd_args: Vec, } -/// Converts a public key into an Ethereum address by hashing the encoded public key with Keccak256. -pub fn public_key_to_address(public: &PublicKey) -> [u8; 20] { - let public_key_bytes = public.serialize_uncompressed(); - - // Skip the first byte (0x04) which indicates uncompressed key - let hash: [u8; 32] = Keccak256::digest(&public_key_bytes[1..]).into(); - - // Take the last 20 bytes of the hash to get the Ethereum address - let mut address = [0u8; 20]; - address.copy_from_slice(&hash[12..]); - address -} - fn main_with_error() -> Result<()> { LogTracer::init().context("Failed to set logger")?; @@ -54,7 +41,7 @@ fn main_with_error() -> Result<()> { let mut rng = rand::thread_rng(); let secp = Secp256k1::new(); let (signing_key, verifying_key) = secp.generate_keypair(&mut rng); - let ethereum_address = public_key_to_address(&verifying_key); + let ethereum_address = public_key_to_ethereum_address(&verifying_key); let tee_type = match get_quote(ethereum_address.as_ref()) { Ok((tee_type, quote)) => { // save quote to file @@ -99,6 +86,8 @@ fn main() -> Result<()> { #[cfg(test)] mod tests { + use secp256k1::{PublicKey, Secp256k1, SecretKey}; + use super::*; #[test] @@ -110,7 +99,7 @@ mod tests { let secret_key = SecretKey::from_slice(&secret_key_bytes).unwrap(); let public_key = PublicKey::from_secret_key(&secp, &secret_key); let expected_address = hex::decode("627306090abaB3A6e1400e9345bC60c78a8BEf57").unwrap(); - let address = public_key_to_address(&public_key); + let address = public_key_to_ethereum_address(&public_key); assert_eq!(address, expected_address.as_slice()); } diff --git a/bin/verify-attestation/Cargo.toml b/bin/verify-attestation/Cargo.toml index ff473d9..5a7c276 100644 --- a/bin/verify-attestation/Cargo.toml +++ b/bin/verify-attestation/Cargo.toml @@ -12,5 +12,6 @@ anyhow.workspace = true clap.workspace = true hex.workspace = true secp256k1.workspace = true +sha3.workspace = true teepot.workspace = true zksync_basic_types.workspace = true diff --git a/bin/verify-attestation/src/main.rs b/bin/verify-attestation/src/main.rs index d799462..8ba0c2b 100644 --- a/bin/verify-attestation/src/main.rs +++ b/bin/verify-attestation/src/main.rs @@ -5,10 +5,13 @@ use anyhow::{Context, Result}; use clap::{Args, Parser, Subcommand}; -use secp256k1::{ecdsa::Signature, Message, PublicKey}; +use core::convert::TryInto; +use hex::encode; +use secp256k1::{Message, PublicKey}; use std::{fs, io::Read, path::PathBuf, str::FromStr, time::UNIX_EPOCH}; use teepot::{ client::TcbLevel, + ethereum::recover_signer, quote::{error, tee_qv_get_collateral, verify_quote_with_collateral, QuoteVerificationResult}, }; use zksync_basic_types::H256; @@ -87,14 +90,25 @@ fn verify_signature( let reportdata = "e_verification_result.quote.get_report_data(); let public_key = PublicKey::from_slice(reportdata)?; println!("Public key from attestation quote: {}", public_key); - let signature_bytes = fs::read(&signature_args.signature_file)?; - let signature = Signature::from_compact(&signature_bytes)?; - let root_hash_msg = Message::from_digest_slice(&signature_args.root_hash.0)?; - if signature.verify(&root_hash_msg, &public_key).is_ok() { - println!("Signature verified successfully"); - } else { - println!("Failed to verify signature"); - } + let signature_bytes: &[u8] = &fs::read(&signature_args.signature_file)?; + let ethereum_address_from_quote = "e_verification_result.quote.get_report_data()[..20]; + let root_hash_bytes = signature_args.root_hash.as_bytes(); + let root_hash_msg = Message::from_digest_slice(root_hash_bytes)?; + let ethereum_address_from_signature = + recover_signer(&signature_bytes.try_into()?, &root_hash_msg)?; + let verification_successful = ethereum_address_from_signature == ethereum_address_from_quote; + + println!( + "Signature '{}' {}. Ethereum address from attestation quote: {}. Ethereum address from signature: {}.", + encode(signature_bytes), + if verification_successful { + "verified successfully" + } else { + "verification failed" + }, + encode(ethereum_address_from_quote), + encode(ethereum_address_from_signature) + ); Ok(()) } diff --git a/bin/verify-era-proof-attestation/src/verification.rs b/bin/verify-era-proof-attestation/src/verification.rs index b389500..4c99bf3 100644 --- a/bin/verify-era-proof-attestation/src/verification.rs +++ b/bin/verify-era-proof-attestation/src/verification.rs @@ -4,16 +4,17 @@ use crate::{args::AttestationPolicyArgs, client::JsonRpcClient}; use anyhow::{Context, Result}; use hex::encode; -use secp256k1::{constants::PUBLIC_KEY_SIZE, ecdsa::Signature, Message, PublicKey}; +use secp256k1::Message; use teepot::{ client::TcbLevel, + ethereum::recover_signer, quote::{ error::QuoteContext, tee_qv_get_collateral, verify_quote_with_collateral, QuoteVerificationResult, Report, }, }; use tracing::{debug, info, warn}; -use zksync_basic_types::{L1BatchNumber, H256}; +use zksync_basic_types::L1BatchNumber; pub async fn verify_batch_proof( quote_verification_result: &QuoteVerificationResult, @@ -27,22 +28,38 @@ pub async fn verify_batch_proof( } let batch_no = batch_number.0; - - let public_key = PublicKey::from_slice( - "e_verification_result.quote.get_report_data()[..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 ethereum_address_from_quote = "e_verification_result.quote.get_report_data()[..20]; + let signature_bytes: &[u8; 65] = signature.try_into()?; + let root_hash_bytes = root_hash.as_bytes(); + let root_hash_msg = Message::from_digest_slice(root_hash_bytes)?; + let ethereum_address_from_signature = recover_signer(signature_bytes, &root_hash_msg)?; + let verification_successful = ethereum_address_from_signature == ethereum_address_from_quote; + debug!( + batch_no, + "Root hash: {}. Ethereum address from the attestation quote: {}. Ethereum address from the signature: {}.", + root_hash, + encode(ethereum_address_from_quote), + encode(ethereum_address_from_signature), + ); - let is_verified = verify_signature(signature, public_key, root_hash)?; - if is_verified { - info!(batch_no, signature = %encode(signature), "Signature verified successfully."); + if verification_successful { + info!( + batch_no, + signature = encode(signature), + ethereum_address = encode(ethereum_address_from_quote), + "Signature verified successfully." + ); } else { - warn!(batch_no, signature = %encode(signature), "Failed to verify signature!"); + warn!( + batch_no, + signature = encode(signature), + ethereum_address_from_signature = encode(ethereum_address_from_signature), + ethereum_address_from_quote = encode(ethereum_address_from_quote), + "Failed to verify signature!" + ); } - Ok(is_verified) + Ok(verification_successful) } pub fn verify_attestation_quote(attestation_quote_bytes: &[u8]) -> Result { @@ -85,12 +102,6 @@ pub fn log_quote_verification_summary(quote_verification_result: &QuoteVerificat ); } -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, diff --git a/crates/teepot/Cargo.toml b/crates/teepot/Cargo.toml index df20004..3b3b6b8 100644 --- a/crates/teepot/Cargo.toml +++ b/crates/teepot/Cargo.toml @@ -32,10 +32,12 @@ pkcs8.workspace = true rand.workspace = true rsa.workspace = true rustls.workspace = true +secp256k1 = { workspace = true, features = ["recovery"] } serde.workspace = true serde_json.workspace = true serde_with.workspace = true sha2.workspace = true +sha3.workspace = true signature.workspace = true tdx-attest-rs.workspace = true thiserror.workspace = true @@ -47,8 +49,8 @@ x509-cert.workspace = true zeroize.workspace = true [dev-dependencies] -anyhow.workspace = true base64.workspace = true testaso.workspace = true tokio.workspace = true tracing-test.workspace = true +zksync_basic_types.workspace = true diff --git a/crates/teepot/src/ethereum/mod.rs b/crates/teepot/src/ethereum/mod.rs new file mode 100644 index 0000000..a8ab878 --- /dev/null +++ b/crates/teepot/src/ethereum/mod.rs @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2023-2024 Matter Labs + +//! Ethereum-specific helper functions for on-chain verification of Intel SGX attestation. + +use anyhow::Result; +use secp256k1::{ + ecdsa::{RecoverableSignature, RecoveryId}, + Message, PublicKey, SECP256K1, +}; +use sha3::{Digest, Keccak256}; + +/// Equivalent to the ecrecover precompile, ensuring that the signatures we produce off-chain +/// can be recovered on-chain. +pub fn recover_signer(sig: &[u8; 65], root_hash: &Message) -> Result<[u8; 20]> { + let sig = RecoverableSignature::from_compact( + &sig[0..64], + RecoveryId::from_i32(sig[64] as i32 - 27)?, + )?; + let public = SECP256K1.recover_ecdsa(root_hash, &sig)?; + Ok(public_key_to_ethereum_address(&public)) +} + +/// Converts a public key into an Ethereum address by hashing the encoded public key with Keccak256. +pub fn public_key_to_ethereum_address(public: &PublicKey) -> [u8; 20] { + let public_key_bytes = public.serialize_uncompressed(); + + // Skip the first byte (0x04) which indicates uncompressed key + let hash: [u8; 32] = Keccak256::digest(&public_key_bytes[1..]).into(); + + // Take the last 20 bytes of the hash to get the Ethereum address + let mut address = [0u8; 20]; + address.copy_from_slice(&hash[12..]); + address +} + +#[cfg(test)] +mod tests { + use secp256k1::{Secp256k1, SecretKey}; + use zksync_basic_types::H256; + + use super::*; + + /// Signs the message in Ethereum-compatible format for on-chain verification. + fn sign_message(sec: &SecretKey, message: Message) -> Result<[u8; 65]> { + let s = SECP256K1.sign_ecdsa_recoverable(&message, sec); + let (rec_id, data) = s.serialize_compact(); + + let mut signature = [0u8; 65]; + signature[..64].copy_from_slice(&data); + // as defined in the Ethereum Yellow Paper (Appendix F) + // https://ethereum.github.io/yellowpaper/paper.pdf + signature[64] = 27 + rec_id.to_i32() as u8; + + Ok(signature) + } + + #[test] + fn recover() { + // Decode the sample secret key, generate the public key, and derive the Ethereum address + // from the public key + let secp = Secp256k1::new(); + let secret_key_bytes = + hex::decode("c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3") + .unwrap(); + let secret_key = SecretKey::from_slice(&secret_key_bytes).unwrap(); + let public_key = PublicKey::from_secret_key(&secp, &secret_key); + let expected_address = hex::decode("627306090abaB3A6e1400e9345bC60c78a8BEf57").unwrap(); + let address = public_key_to_ethereum_address(&public_key); + + assert_eq!(address, expected_address.as_slice()); + + // Generate a random root hash, create a message from the hash, and sign the message using + // the secret key + let root_hash = H256::random(); + let root_hash_bytes = root_hash.as_bytes(); + let msg_to_sign = Message::from_digest_slice(root_hash_bytes).unwrap(); + let signature = sign_message(&secret_key, msg_to_sign).unwrap(); + + // Recover the signer's Ethereum address from the signature and the message, and verify it + // matches the expected address + let proof_addr = recover_signer(&signature, &msg_to_sign).unwrap(); + + assert_eq!(proof_addr, expected_address.as_slice()); + } +} diff --git a/crates/teepot/src/lib.rs b/crates/teepot/src/lib.rs index 2d9f7cc..5cdf25b 100644 --- a/crates/teepot/src/lib.rs +++ b/crates/teepot/src/lib.rs @@ -7,6 +7,7 @@ #![deny(clippy::all)] pub mod client; +pub mod ethereum; pub mod json; pub mod log; pub mod quote;