diff --git a/bin/verify-era-proof-attestation/src/verification.rs b/bin/verify-era-proof-attestation/src/verification.rs index 4c99bf3..78b99ea 100644 --- a/bin/verify-era-proof-attestation/src/verification.rs +++ b/bin/verify-era-proof-attestation/src/verification.rs @@ -2,19 +2,67 @@ // Copyright (c) 2023-2024 Matter Labs use crate::{args::AttestationPolicyArgs, client::JsonRpcClient}; -use anyhow::{Context, Result}; +use anyhow::{anyhow, Context, Result}; use hex::encode; -use secp256k1::Message; +use secp256k1::{ecdsa::Signature, Message}; use teepot::{ client::TcbLevel, ethereum::recover_signer, + prover::reportdata::ReportData, quote::{ error::QuoteContext, tee_qv_get_collateral, verify_quote_with_collateral, QuoteVerificationResult, Report, }, }; use tracing::{debug, info, warn}; -use zksync_basic_types::L1BatchNumber; +use zksync_basic_types::{L1BatchNumber, H256}; + +struct TeeProof { + report: ReportData, + root_hash: H256, + signature: Vec, +} + +impl TeeProof { + pub fn new(report: ReportData, root_hash: H256, signature: Vec) -> Self { + Self { + report, + root_hash, + signature, + } + } + + pub fn verify(&self) -> Result { + match &self.report { + ReportData::V0(report) => { + let signature = Signature::from_compact(&self.signature)?; + let root_hash_msg = Message::from_digest_slice(&self.root_hash.0)?; + Ok(signature.verify(&root_hash_msg, &report.pubkey).is_ok()) + } + ReportData::V1(report) => { + let ethereum_address_from_report = report.ethereum_addr; + let root_hash_bytes = self.root_hash.as_bytes(); + let root_hash_msg = Message::from_digest_slice(root_hash_bytes)?; + let signature_bytes: [u8; 65] = self + .signature + .clone() + .try_into() + .map_err(|e| anyhow!("{:?}", e))?; + + let ethereum_address_from_signature = + recover_signer(&signature_bytes, &root_hash_msg)?; + debug!( + "Root hash: {}. Ethereum address from the attestation quote: {}. Ethereum address from the signature: {}.", + self.root_hash, + encode(ethereum_address_from_report), + encode(ethereum_address_from_signature), + ); + Ok(ethereum_address_from_signature == ethereum_address_from_report) + } + ReportData::Unknown(_) => Ok(false), + } + } +} pub async fn verify_batch_proof( quote_verification_result: &QuoteVerificationResult, @@ -27,38 +75,11 @@ pub async fn verify_batch_proof( return Ok(false); } - let batch_no = batch_number.0; let root_hash = node_client.get_root_hash(batch_number).await?; - 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), - ); - - 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), - ethereum_address_from_signature = encode(ethereum_address_from_signature), - ethereum_address_from_quote = encode(ethereum_address_from_quote), - "Failed to verify signature!" - ); - } + let report_data_bytes = quote_verification_result.quote.get_report_data(); + let report_data = ReportData::try_from(report_data_bytes)?; + let tee_proof = TeeProof::new(report_data, root_hash, signature.to_vec()); + let verification_successful = tee_proof.verify().is_ok(); Ok(verification_successful) } diff --git a/crates/teepot/src/lib.rs b/crates/teepot/src/lib.rs index 5cdf25b..8f73489 100644 --- a/crates/teepot/src/lib.rs +++ b/crates/teepot/src/lib.rs @@ -10,6 +10,7 @@ pub mod client; pub mod ethereum; pub mod json; pub mod log; +pub mod prover; pub mod quote; pub mod server; pub mod sgx; diff --git a/crates/teepot/src/prover/mod.rs b/crates/teepot/src/prover/mod.rs new file mode 100644 index 0000000..d7b3e63 --- /dev/null +++ b/crates/teepot/src/prover/mod.rs @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2023 Matter Labs + +//! Common functionality for TEE provers and verifiers. + +pub mod reportdata; diff --git a/crates/teepot/src/prover/reportdata.rs b/crates/teepot/src/prover/reportdata.rs new file mode 100644 index 0000000..182b869 --- /dev/null +++ b/crates/teepot/src/prover/reportdata.rs @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2024 Matter Labs + +//! Versioning of Intel SGX/TDX quote's report data for TEE prover and verifier. + +use core::convert::Into; + +use anyhow::{anyhow, Result}; +use secp256k1::{constants::PUBLIC_KEY_SIZE, PublicKey}; + +/// Report data length for Intel SGX/TDX. +const REPORT_DATA_LENGTH: usize = 64; + +/// Ethereum address length. +const ETHEREUM_ADDR_LENGTH: usize = 20; + +/// Report data version. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ReportData { + /// Legacy version of report data that was initially not intended to be versioned. + /// The report_data was on-chain incompatible and consisted of a compressed ECDSA public key. + /// + /// +-------------------------------------+--------------------------------+------------------+ + /// | compressed ECDSA pubkey (33 bytes) | zeroes (30 bytes) | version (1 byte) | + /// +-------------------------------------+--------------------------------+------------------+ + V0(ReportDataV0), + /// Latest version of report data compatible with on-chain verification. + /// + /// +--------------------------+-------------------------------------------+------------------+ + /// | Ethereum addr (20 bytes) | zeros (43 bytes) | version (1 byte) | + /// +--------------------------+-------------------------------------------+------------------+ + V1(ReportDataV1), + /// Unknown version of report data. + Unknown(Vec), +} + +impl TryFrom<&[u8]> for ReportData { + type Error = anyhow::Error; + + fn try_from(report_data_bytes: &[u8]) -> Result { + if report_data_bytes.len() != REPORT_DATA_LENGTH { + return Err(anyhow!("Invalid byte slice length")); + } + let version = report_data_bytes[REPORT_DATA_LENGTH - 1]; + match version { + 0 => Ok(Self::V0(ReportDataV0::try_from(report_data_bytes)?)), + 1 => Ok(Self::V1(ReportDataV1::try_from(report_data_bytes)?)), + _ => Ok(Self::Unknown(report_data_bytes.into())), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[allow(missing_docs)] +pub struct ReportDataV0 { + pub pubkey: PublicKey, +} + +impl TryFrom<&[u8]> for ReportDataV0 { + type Error = anyhow::Error; + + fn try_from(report_data_bytes: &[u8]) -> Result { + if report_data_bytes.len() != REPORT_DATA_LENGTH { + return Err(anyhow!("Invalid byte slice length")); + } + let pubkey = PublicKey::from_slice(&report_data_bytes[..PUBLIC_KEY_SIZE])?; + Ok(Self { pubkey }) + } +} + +impl Into<[u8; REPORT_DATA_LENGTH]> for ReportDataV0 { + fn into(self) -> [u8; REPORT_DATA_LENGTH] { + let mut bytes = [0u8; REPORT_DATA_LENGTH]; + bytes[..PUBLIC_KEY_SIZE].copy_from_slice(&self.pubkey.serialize()); + bytes + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +#[allow(missing_docs)] +pub struct ReportDataV1 { + pub ethereum_addr: [u8; ETHEREUM_ADDR_LENGTH], +} + +impl TryFrom<&[u8]> for ReportDataV1 { + type Error = anyhow::Error; + + fn try_from(report_data_bytes: &[u8]) -> Result { + if report_data_bytes.len() != REPORT_DATA_LENGTH { + return Err(anyhow!("Invalid byte slice length")); + } + let mut ethereum_addr = [0u8; ETHEREUM_ADDR_LENGTH]; + ethereum_addr.copy_from_slice(&report_data_bytes[..ETHEREUM_ADDR_LENGTH]); + Ok(Self { ethereum_addr }) + } +} + +impl Into<[u8; REPORT_DATA_LENGTH]> for ReportDataV1 { + fn into(self) -> [u8; REPORT_DATA_LENGTH] { + let mut bytes = [0u8; REPORT_DATA_LENGTH]; + bytes[..ETHEREUM_ADDR_LENGTH].copy_from_slice(&self.ethereum_addr); + bytes[REPORT_DATA_LENGTH - 1] = 1; + bytes + } +} + +#[cfg(test)] +mod tests { + use super::*; + use hex; + use secp256k1::{Secp256k1, SecretKey}; + + const ETHEREUM_ADDR: [u8; ETHEREUM_ADDR_LENGTH] = [ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, + ]; + + fn generate_test_report_data(version_byte: u8) -> [u8; REPORT_DATA_LENGTH] { + let mut data = [0u8; REPORT_DATA_LENGTH]; + data[..ETHEREUM_ADDR.len()].copy_from_slice(ÐEREUM_ADDR); + data[REPORT_DATA_LENGTH - 1] = version_byte; + data + } + + fn generate_test_pubkey() -> PublicKey { + let secp = Secp256k1::new(); + let secret_key_bytes = + hex::decode("c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3") + .unwrap(); + let secret_key = SecretKey::from_slice(&secret_key_bytes).unwrap(); + PublicKey::from_secret_key(&secp, &secret_key) + } + + fn generate_test_report_data_v0(pubkey: PublicKey) -> [u8; REPORT_DATA_LENGTH] { + let pubkey_bytes = pubkey.serialize(); + let mut report_data_bytes = [0u8; REPORT_DATA_LENGTH]; + report_data_bytes[..PUBLIC_KEY_SIZE].copy_from_slice(&pubkey_bytes); + report_data_bytes + } + + #[test] + fn test_from_bytes_v0() { + let pubkey = generate_test_pubkey(); + let report_data_bytes = generate_test_report_data_v0(pubkey); + let report_data = ReportData::try_from(report_data_bytes.as_ref()).unwrap(); + assert_eq!(report_data, ReportData::V0(ReportDataV0 { pubkey })); + } + + #[test] + fn report_data_from_bytes_v1() { + let data = generate_test_report_data(1); + let report_data = ReportData::try_from(data.as_ref()).unwrap(); + assert_eq!( + report_data, + ReportData::V1(ReportDataV1 { + ethereum_addr: ETHEREUM_ADDR + }) + ); + } + + #[test] + fn report_data_from_bytes_unknown() { + let report_data_bytes = generate_test_report_data(99); + let report_data = ReportData::try_from(report_data_bytes.as_ref()).unwrap(); + assert_eq!(report_data, ReportData::Unknown(report_data_bytes.into())); + } + + #[test] + fn report_data_to_bytes_v0() { + let pubkey = generate_test_pubkey(); + let report_data = ReportDataV0 { pubkey }; + let report_data: [u8; REPORT_DATA_LENGTH] = report_data.into(); + assert_eq!(&report_data[..PUBLIC_KEY_SIZE], pubkey.serialize().as_ref()); + assert_eq!(report_data[REPORT_DATA_LENGTH - 1], 0); + assert!(report_data[PUBLIC_KEY_SIZE..REPORT_DATA_LENGTH - 1] + .iter() + .all(|&byte| byte == 0)); + } + + #[test] + fn report_data_to_bytes_v1() { + let report_data = ReportDataV1 { + ethereum_addr: ETHEREUM_ADDR, + }; + let report_data: [u8; REPORT_DATA_LENGTH] = report_data.into(); + assert_eq!(&report_data[..ETHEREUM_ADDR_LENGTH], ÐEREUM_ADDR); + assert_eq!(report_data[REPORT_DATA_LENGTH - 1], 1); + assert!(report_data[ETHEREUM_ADDR_LENGTH..REPORT_DATA_LENGTH - 1] + .iter() + .all(|&byte| byte == 0)); + } +}