Skip to content

Commit bdb213c

Browse files
committed
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: - matter-labs/zksync-era#3414 - #228
1 parent a9b89ef commit bdb213c

File tree

9 files changed

+155
-49
lines changed

9 files changed

+155
-49
lines changed

Cargo.lock

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bin/tee-key-preexec/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ clap.workspace = true
1515
hex.workspace = true
1616
rand.workspace = true
1717
secp256k1.workspace = true
18-
sha3.workspace = true
1918
teepot.workspace = true
2019
tracing.workspace = true
2120
tracing-log.workspace = true

bin/tee-key-preexec/src/main.rs

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88

99
use anyhow::{Context, Result};
1010
use clap::Parser;
11-
use secp256k1::{rand, PublicKey, Secp256k1, SecretKey};
12-
use sha3::{Digest, Keccak256};
11+
use secp256k1::{rand, Secp256k1};
1312
use std::{ffi::OsString, os::unix::process::CommandExt, process::Command};
13+
use teepot::ethereum::public_key_to_ethereum_address;
1414
use teepot::quote::get_quote;
1515
use tracing::error;
1616
use tracing_log::LogTracer;
@@ -29,19 +29,6 @@ struct Args {
2929
cmd_args: Vec<OsString>,
3030
}
3131

32-
/// Converts a public key into an Ethereum address by hashing the encoded public key with Keccak256.
33-
pub fn public_key_to_address(public: &PublicKey) -> [u8; 20] {
34-
let public_key_bytes = public.serialize_uncompressed();
35-
36-
// Skip the first byte (0x04) which indicates uncompressed key
37-
let hash: [u8; 32] = Keccak256::digest(&public_key_bytes[1..]).into();
38-
39-
// Take the last 20 bytes of the hash to get the Ethereum address
40-
let mut address = [0u8; 20];
41-
address.copy_from_slice(&hash[12..]);
42-
address
43-
}
44-
4532
fn main_with_error() -> Result<()> {
4633
LogTracer::init().context("Failed to set logger")?;
4734

@@ -54,7 +41,7 @@ fn main_with_error() -> Result<()> {
5441
let mut rng = rand::thread_rng();
5542
let secp = Secp256k1::new();
5643
let (signing_key, verifying_key) = secp.generate_keypair(&mut rng);
57-
let ethereum_address = public_key_to_address(&verifying_key);
44+
let ethereum_address = public_key_to_ethereum_address(&verifying_key);
5845
let tee_type = match get_quote(ethereum_address.as_ref()) {
5946
Ok((tee_type, quote)) => {
6047
// save quote to file
@@ -99,6 +86,8 @@ fn main() -> Result<()> {
9986

10087
#[cfg(test)]
10188
mod tests {
89+
use secp256k1::{PublicKey, Secp256k1, SecretKey};
90+
10291
use super::*;
10392

10493
#[test]
@@ -110,7 +99,7 @@ mod tests {
11099
let secret_key = SecretKey::from_slice(&secret_key_bytes).unwrap();
111100
let public_key = PublicKey::from_secret_key(&secp, &secret_key);
112101
let expected_address = hex::decode("627306090abaB3A6e1400e9345bC60c78a8BEf57").unwrap();
113-
let address = public_key_to_address(&public_key);
102+
let address = public_key_to_ethereum_address(&public_key);
114103

115104
assert_eq!(address, expected_address.as_slice());
116105
}

bin/verify-attestation/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ anyhow.workspace = true
1212
clap.workspace = true
1313
hex.workspace = true
1414
secp256k1.workspace = true
15+
sha3.workspace = true
1516
teepot.workspace = true
1617
zksync_basic_types.workspace = true

bin/verify-attestation/src/main.rs

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@
55
66
use anyhow::{Context, Result};
77
use clap::{Args, Parser, Subcommand};
8-
use secp256k1::{ecdsa::Signature, Message, PublicKey};
8+
use core::convert::TryInto;
9+
use hex::encode;
10+
use secp256k1::{Message, PublicKey};
911
use std::{fs, io::Read, path::PathBuf, str::FromStr, time::UNIX_EPOCH};
1012
use teepot::{
1113
client::TcbLevel,
14+
ethereum::recover_signer,
1215
quote::{error, tee_qv_get_collateral, verify_quote_with_collateral, QuoteVerificationResult},
1316
};
1417
use zksync_basic_types::H256;
@@ -87,14 +90,25 @@ fn verify_signature(
8790
let reportdata = &quote_verification_result.quote.get_report_data();
8891
let public_key = PublicKey::from_slice(reportdata)?;
8992
println!("Public key from attestation quote: {}", public_key);
90-
let signature_bytes = fs::read(&signature_args.signature_file)?;
91-
let signature = Signature::from_compact(&signature_bytes)?;
92-
let root_hash_msg = Message::from_digest_slice(&signature_args.root_hash.0)?;
93-
if signature.verify(&root_hash_msg, &public_key).is_ok() {
94-
println!("Signature verified successfully");
95-
} else {
96-
println!("Failed to verify signature");
97-
}
93+
let signature_bytes: &[u8] = &fs::read(&signature_args.signature_file)?;
94+
let ethereum_address_from_quote = &quote_verification_result.quote.get_report_data()[..20];
95+
let root_hash_bytes = signature_args.root_hash.as_bytes();
96+
let root_hash_msg = Message::from_digest_slice(root_hash_bytes)?;
97+
let ethereum_address_from_signature =
98+
recover_signer(&signature_bytes.try_into()?, &root_hash_msg)?;
99+
let verification_successful = ethereum_address_from_signature == ethereum_address_from_quote;
100+
101+
println!(
102+
"Signature '{}' {}. Ethereum address from attestation quote: {}. Ethereum address from signature: {}.",
103+
encode(signature_bytes),
104+
if verification_successful {
105+
"verified successfully"
106+
} else {
107+
"verification failed"
108+
},
109+
encode(ethereum_address_from_quote),
110+
encode(ethereum_address_from_signature)
111+
);
98112
Ok(())
99113
}
100114

bin/verify-era-proof-attestation/src/verification.rs

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,17 @@
44
use crate::{args::AttestationPolicyArgs, client::JsonRpcClient};
55
use anyhow::{Context, Result};
66
use hex::encode;
7-
use secp256k1::{constants::PUBLIC_KEY_SIZE, ecdsa::Signature, Message, PublicKey};
7+
use secp256k1::Message;
88
use teepot::{
99
client::TcbLevel,
10+
ethereum::recover_signer,
1011
quote::{
1112
error::QuoteContext, tee_qv_get_collateral, verify_quote_with_collateral,
1213
QuoteVerificationResult, Report,
1314
},
1415
};
1516
use tracing::{debug, info, warn};
16-
use zksync_basic_types::{L1BatchNumber, H256};
17+
use zksync_basic_types::L1BatchNumber;
1718

1819
pub async fn verify_batch_proof(
1920
quote_verification_result: &QuoteVerificationResult,
@@ -27,22 +28,38 @@ pub async fn verify_batch_proof(
2728
}
2829

2930
let batch_no = batch_number.0;
30-
31-
let public_key = PublicKey::from_slice(
32-
&quote_verification_result.quote.get_report_data()[..PUBLIC_KEY_SIZE],
33-
)?;
34-
debug!(batch_no, "public key: {}", public_key);
35-
3631
let root_hash = node_client.get_root_hash(batch_number).await?;
37-
debug!(batch_no, "root hash: {}", root_hash);
32+
let ethereum_address_from_quote = &quote_verification_result.quote.get_report_data()[..20];
33+
let signature_bytes: &[u8; 65] = signature.try_into()?;
34+
let root_hash_bytes = root_hash.as_bytes();
35+
let root_hash_msg = Message::from_digest_slice(root_hash_bytes)?;
36+
let ethereum_address_from_signature = recover_signer(signature_bytes, &root_hash_msg)?;
37+
let verification_successful = ethereum_address_from_signature == ethereum_address_from_quote;
38+
debug!(
39+
batch_no,
40+
"Root hash: {}. Ethereum address from the attestation quote: {}. Ethereum address from the signature: {}.",
41+
root_hash,
42+
encode(ethereum_address_from_quote),
43+
encode(ethereum_address_from_signature),
44+
);
3845

39-
let is_verified = verify_signature(signature, public_key, root_hash)?;
40-
if is_verified {
41-
info!(batch_no, signature = %encode(signature), "Signature verified successfully.");
46+
if verification_successful {
47+
info!(
48+
batch_no,
49+
signature = encode(signature),
50+
ethereum_address = encode(ethereum_address_from_quote),
51+
"Signature verified successfully."
52+
);
4253
} else {
43-
warn!(batch_no, signature = %encode(signature), "Failed to verify signature!");
54+
warn!(
55+
batch_no,
56+
signature = encode(signature),
57+
ethereum_address_from_signature = encode(ethereum_address_from_signature),
58+
ethereum_address_from_quote = encode(ethereum_address_from_quote),
59+
"Failed to verify signature!"
60+
);
4461
}
45-
Ok(is_verified)
62+
Ok(verification_successful)
4663
}
4764

4865
pub fn verify_attestation_quote(attestation_quote_bytes: &[u8]) -> Result<QuoteVerificationResult> {
@@ -85,12 +102,6 @@ pub fn log_quote_verification_summary(quote_verification_result: &QuoteVerificat
85102
);
86103
}
87104

88-
fn verify_signature(signature: &[u8], public_key: PublicKey, root_hash: H256) -> Result<bool> {
89-
let signature = Signature::from_compact(signature)?;
90-
let root_hash_msg = Message::from_digest_slice(&root_hash.0)?;
91-
Ok(signature.verify(&root_hash_msg, &public_key).is_ok())
92-
}
93-
94105
fn is_quote_matching_policy(
95106
attestation_policy: &AttestationPolicyArgs,
96107
quote_verification_result: &QuoteVerificationResult,

crates/teepot/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,12 @@ pkcs8.workspace = true
3232
rand.workspace = true
3333
rsa.workspace = true
3434
rustls.workspace = true
35+
secp256k1 = { workspace = true, features = ["recovery"] }
3536
serde.workspace = true
3637
serde_json.workspace = true
3738
serde_with.workspace = true
3839
sha2.workspace = true
40+
sha3.workspace = true
3941
signature.workspace = true
4042
tdx-attest-rs.workspace = true
4143
thiserror.workspace = true
@@ -47,8 +49,8 @@ x509-cert.workspace = true
4749
zeroize.workspace = true
4850

4951
[dev-dependencies]
50-
anyhow.workspace = true
5152
base64.workspace = true
5253
testaso.workspace = true
5354
tokio.workspace = true
5455
tracing-test.workspace = true
56+
zksync_basic_types.workspace = true

crates/teepot/src/ethereum/mod.rs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright (c) 2023-2024 Matter Labs
3+
4+
//! Ethereum-specific helper functions for on-chain verification of Intel SGX attestation.
5+
6+
use anyhow::Result;
7+
use secp256k1::{
8+
ecdsa::{RecoverableSignature, RecoveryId},
9+
Message, PublicKey, SECP256K1,
10+
};
11+
use sha3::{Digest, Keccak256};
12+
13+
/// Equivalent to the ecrecover precompile, ensuring that the signatures we produce off-chain
14+
/// can be recovered on-chain.
15+
pub fn recover_signer(sig: &[u8; 65], root_hash: &Message) -> Result<[u8; 20]> {
16+
let sig = RecoverableSignature::from_compact(
17+
&sig[0..64],
18+
RecoveryId::from_i32(sig[64] as i32 - 27)?,
19+
)?;
20+
let public = SECP256K1.recover_ecdsa(root_hash, &sig)?;
21+
Ok(public_key_to_ethereum_address(&public))
22+
}
23+
24+
/// Converts a public key into an Ethereum address by hashing the encoded public key with Keccak256.
25+
pub fn public_key_to_ethereum_address(public: &PublicKey) -> [u8; 20] {
26+
let public_key_bytes = public.serialize_uncompressed();
27+
28+
// Skip the first byte (0x04) which indicates uncompressed key
29+
let hash: [u8; 32] = Keccak256::digest(&public_key_bytes[1..]).into();
30+
31+
// Take the last 20 bytes of the hash to get the Ethereum address
32+
let mut address = [0u8; 20];
33+
address.copy_from_slice(&hash[12..]);
34+
address
35+
}
36+
37+
#[cfg(test)]
38+
mod tests {
39+
use secp256k1::{Secp256k1, SecretKey};
40+
use zksync_basic_types::H256;
41+
42+
use super::*;
43+
44+
/// Signs the message in Ethereum-compatible format for on-chain verification.
45+
fn sign_message(sec: &SecretKey, message: Message) -> Result<[u8; 65]> {
46+
let s = SECP256K1.sign_ecdsa_recoverable(&message, sec);
47+
let (rec_id, data) = s.serialize_compact();
48+
49+
let mut signature = [0u8; 65];
50+
signature[..64].copy_from_slice(&data);
51+
// as defined in the Ethereum Yellow Paper (Appendix F)
52+
// https://ethereum.github.io/yellowpaper/paper.pdf
53+
signature[64] = 27 + rec_id.to_i32() as u8;
54+
55+
Ok(signature)
56+
}
57+
58+
#[test]
59+
fn recover() {
60+
// Decode the sample secret key, generate the public key, and derive the Ethereum address
61+
// from the public key
62+
let secp = Secp256k1::new();
63+
let secret_key_bytes =
64+
hex::decode("c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3")
65+
.unwrap();
66+
let secret_key = SecretKey::from_slice(&secret_key_bytes).unwrap();
67+
let public_key = PublicKey::from_secret_key(&secp, &secret_key);
68+
let expected_address = hex::decode("627306090abaB3A6e1400e9345bC60c78a8BEf57").unwrap();
69+
let address = public_key_to_ethereum_address(&public_key);
70+
71+
assert_eq!(address, expected_address.as_slice());
72+
73+
// Generate a random root hash, create a message from the hash, and sign the message using
74+
// the secret key
75+
let root_hash = H256::random();
76+
let root_hash_bytes = root_hash.as_bytes();
77+
let msg_to_sign = Message::from_digest_slice(root_hash_bytes).unwrap();
78+
let signature = sign_message(&secret_key, msg_to_sign).unwrap();
79+
80+
// Recover the signer's Ethereum address from the signature and the message, and verify it
81+
// matches the expected address
82+
let proof_addr = recover_signer(&signature, &msg_to_sign).unwrap();
83+
84+
assert_eq!(proof_addr, expected_address.as_slice());
85+
}
86+
}

crates/teepot/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#![deny(clippy::all)]
88

99
pub mod client;
10+
pub mod ethereum;
1011
pub mod json;
1112
pub mod log;
1213
pub mod quote;

0 commit comments

Comments
 (0)