Skip to content

Commit 2d04ba0

Browse files
committed
feat(tee-key-preexec): 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.
1 parent e5cca31 commit 2d04ba0

File tree

13 files changed

+828
-43
lines changed

13 files changed

+828
-43
lines changed

Cargo.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ serde = { version = "1", features = ["derive", "rc"] }
4949
serde_json = "1"
5050
serde_with = { version = "3.8", features = ["base64", "hex"] }
5151
sha2 = "0.10.8"
52+
sha3 = "0.10.8"
5253
signature = "2.2.0"
5354
tdx-attest-rs = { version = "0.1.2", git = "https://github.com/intel/SGXDataCenterAttestationPrimitives.git", rev = "aa239d25a437a28f3f4de92c38f5b6809faac842" }
5455
teepot = { path = "crates/teepot" }

bin/tee-key-preexec/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ repository.workspace = true
1212
[dependencies]
1313
anyhow.workspace = true
1414
clap.workspace = true
15+
hex.workspace = true
1516
rand.workspace = true
1617
secp256k1.workspace = true
1718
teepot.workspace = true

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

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

99
use anyhow::{Context, Result};
1010
use clap::Parser;
11-
use secp256k1::{rand, Keypair, PublicKey, Secp256k1, SecretKey};
11+
use core::convert::AsRef;
12+
use secp256k1::{rand, Secp256k1};
1213
use std::{ffi::OsString, os::unix::process::CommandExt, process::Command};
13-
use teepot::quote::get_quote;
14+
use teepot::{
15+
ethereum::public_key_to_ethereum_address, prover::reportdata::ReportDataV1, quote::get_quote,
16+
};
1417
use tracing::error;
1518
use tracing_log::LogTracer;
1619
use tracing_subscriber::{fmt, prelude::*, EnvFilter, Registry};
@@ -37,14 +40,13 @@ fn main_with_error() -> Result<()> {
3740
tracing::subscriber::set_global_default(subscriber).context("Failed to set logger")?;
3841

3942
let args = Args::parse();
40-
4143
let mut rng = rand::thread_rng();
4244
let secp = Secp256k1::new();
43-
let keypair = Keypair::new(&secp, &mut rng);
44-
let signing_key = SecretKey::from_keypair(&keypair);
45-
let verifying_key = PublicKey::from_keypair(&keypair);
46-
let verifying_key_bytes = verifying_key.serialize();
47-
let tee_type = match get_quote(verifying_key_bytes.as_ref()) {
45+
let (signing_key, verifying_key) = secp.generate_keypair(&mut rng);
46+
let ethereum_address = public_key_to_ethereum_address(&verifying_key);
47+
let report_data = ReportDataV1 { ethereum_address };
48+
let report_data_bytes: [u8; 64] = report_data.into();
49+
let tee_type = match get_quote(report_data_bytes.as_ref()) {
4850
Ok((tee_type, quote)) => {
4951
// save quote to file
5052
std::fs::write(TEE_QUOTE_FILE, quote)?;
@@ -85,3 +87,24 @@ fn main() -> Result<()> {
8587
}
8688
ret
8789
}
90+
91+
#[cfg(test)]
92+
mod tests {
93+
use secp256k1::{PublicKey, Secp256k1, SecretKey};
94+
95+
use super::*;
96+
97+
#[test]
98+
fn test_public_key_to_address() {
99+
let secp = Secp256k1::new();
100+
let secret_key_bytes =
101+
hex::decode("c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3")
102+
.unwrap();
103+
let secret_key = SecretKey::from_slice(&secret_key_bytes).unwrap();
104+
let public_key = PublicKey::from_secret_key(&secp, &secret_key);
105+
let expected_address = hex::decode("627306090abaB3A6e1400e9345bC60c78a8BEf57").unwrap();
106+
let address = public_key_to_ethereum_address(&public_key);
107+
108+
assert_eq!(address, expected_address.as_slice());
109+
}
110+
}

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: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
// Copyright (c) 2023-2024 Matter Labs
33

44
use crate::{args::AttestationPolicyArgs, client::JsonRpcClient};
5-
use anyhow::{Context, Result};
5+
use anyhow::{anyhow, Context, Result};
66
use hex::encode;
7-
use secp256k1::{constants::PUBLIC_KEY_SIZE, ecdsa::Signature, Message, PublicKey};
7+
use secp256k1::{ecdsa::Signature, Message};
88
use teepot::{
99
client::TcbLevel,
10+
ethereum::recover_signer,
11+
prover::reportdata::ReportData,
1012
quote::{
1113
error::QuoteContext, tee_qv_get_collateral, verify_quote_with_collateral,
1214
QuoteVerificationResult, Report,
@@ -15,6 +17,51 @@ use teepot::{
1517
use tracing::{debug, info, warn};
1618
use zksync_basic_types::{L1BatchNumber, H256};
1719

20+
struct TeeProof {
21+
report: ReportData,
22+
root_hash: H256,
23+
signature: Vec<u8>,
24+
}
25+
26+
impl TeeProof {
27+
pub fn new(report: ReportData, root_hash: H256, signature: Vec<u8>) -> Self {
28+
Self {
29+
report,
30+
root_hash,
31+
signature,
32+
}
33+
}
34+
35+
pub fn verify(&self) -> Result<bool> {
36+
match &self.report {
37+
ReportData::V0(report) => {
38+
let signature = Signature::from_compact(&self.signature)?;
39+
let root_hash_msg = Message::from_digest_slice(&self.root_hash.0)?;
40+
Ok(signature.verify(&root_hash_msg, &report.pubkey).is_ok())
41+
}
42+
ReportData::V1(report) => {
43+
let ethereum_address_from_report = report.ethereum_address;
44+
let root_hash_msg = Message::from_digest_slice(self.root_hash.as_bytes())?;
45+
let signature_bytes: [u8; 65] = self
46+
.signature
47+
.clone()
48+
.try_into()
49+
.map_err(|e| anyhow!("{:?}", e))?;
50+
let ethereum_address_from_signature =
51+
recover_signer(&signature_bytes, &root_hash_msg)?;
52+
debug!(
53+
"Root hash: {}. Ethereum address from the attestation quote: {}. Ethereum address from the signature: {}.",
54+
self.root_hash,
55+
encode(ethereum_address_from_report),
56+
encode(ethereum_address_from_signature),
57+
);
58+
Ok(ethereum_address_from_signature == ethereum_address_from_report)
59+
}
60+
ReportData::Unknown(_) => Ok(false),
61+
}
62+
}
63+
}
64+
1865
pub async fn verify_batch_proof(
1966
quote_verification_result: &QuoteVerificationResult,
2067
attestation_policy: &AttestationPolicyArgs,
@@ -26,23 +73,12 @@ pub async fn verify_batch_proof(
2673
return Ok(false);
2774
}
2875

29-
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-
3676
let root_hash = node_client.get_root_hash(batch_number).await?;
37-
debug!(batch_no, "root hash: {}", root_hash);
38-
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.");
42-
} else {
43-
warn!(batch_no, signature = %encode(signature), "Failed to verify signature!");
44-
}
45-
Ok(is_verified)
77+
let report_data_bytes = quote_verification_result.quote.get_report_data();
78+
let report_data = ReportData::try_from(report_data_bytes)?;
79+
let tee_proof = TeeProof::new(report_data, root_hash, signature.to_vec());
80+
let verification_successful = tee_proof.verify().is_ok();
81+
Ok(verification_successful)
4682
}
4783

4884
pub fn verify_attestation_quote(attestation_quote_bytes: &[u8]) -> Result<QuoteVerificationResult> {
@@ -85,12 +121,6 @@ pub fn log_quote_verification_summary(quote_verification_result: &QuoteVerificat
85121
);
86122
}
87123

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-
94124
fn is_quote_matching_policy(
95125
attestation_policy: &AttestationPolicyArgs,
96126
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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
#![deny(clippy::all)]
88

99
pub mod client;
10+
pub mod ethereum;
1011
pub mod json;
1112
pub mod log;
13+
pub mod prover;
1214
pub mod quote;
1315
pub mod server;
1416
pub mod sgx;

0 commit comments

Comments
 (0)