feat(verify-attestation): RPC attestation and batch signature verification binary

This is another variant of the binary tool for verifying attestation and
the signature of a given batch. Unlike the existing tool, this variant
does not require you to provide two separate files—one for the
attestation and one for the signature. Instead, it automatically fetches
both from the RPC node.

Unfortunately, after discussing with @popzxc, we found that there is no way
to reuse the RPC client because our published crates on crates.io are
outdated and do not include the recently merged TEE-specific code
changes. To be fixed in the future.
This commit is contained in:
Patryk Bęza 2024-08-09 14:50:30 +02:00
parent 27f35a7432
commit 5e4b8901b0
No known key found for this signature in database
GPG key ID: 9AD1B44D9F6258EC
11 changed files with 2991 additions and 170 deletions

View file

@ -76,6 +76,7 @@ jobs:
- { nixpackage: 'container-self-attestation-test-sgx-dcap' }
- { nixpackage: 'container-self-attestation-test-sgx-azure' }
- { nixpackage: 'container-verify-attestation-sgx' }
- { nixpackage: 'container-verify-era-proof-attestation-sgx' }
steps:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@v27

2902
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -42,6 +42,7 @@ pgp = "0.13"
p256 = "0.13.2"
pkcs8 = { version = "0.10" }
rand = "0.8"
reqwest = { version = "0.12", features = ["json"] }
ring = { version = "0.17.8", features = ["std"], default-features = false }
rsa = { version = "0.9.6", features = ["sha2", "pem"] }
rustls = { version = "0.22" }
@ -60,7 +61,10 @@ tracing = "0.1"
tracing-actix-web = "0.7"
tracing-log = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
url = "2.5.2"
x509-cert = { version = "0.2", features = ["builder", "signature"] }
zeroize = { version = "1.7.0", features = ["serde"] }
webpki-roots = "0.26.1"
zksync_basic_types = "=0.1.0"
zksync_web3_decl = "=0.1.0"
zksync_types = "=0.1.0"

View file

@ -0,0 +1,21 @@
[package]
name = "verify-era-proof-attestation"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
anyhow.workspace = true
clap.workspace = true
hex.workspace = true
reqwest.workspace = true
secp256k1.workspace = true
serde.workspace = true
teepot.workspace = true
tokio.workspace = true
url.workspace = true
zksync_basic_types.workspace = true
zksync_types.workspace = true
zksync_web3_decl.workspace = true

View file

@ -0,0 +1,176 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023-2024 Matter Labs
//! Tool for SGX attestation and batch signature verification
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 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,
};
#[derive(Parser, Debug)]
#[command(author = "Matter Labs", version, about = "SGX attestation and batch signature verifier", long_about = None)]
struct Arguments {
/// The batch number for which we want to verify the attestation and signature.
#[clap(short = 'n', long)]
batch_number: L1BatchNumber,
/// URL of the RPC server to query for the batch attestation and signature.
#[clap(short, long)]
rpc_url: Url,
/// Chain ID of the network to query.
#[clap(short, long, default_value_t = L2ChainId::default().as_u64())]
chain_id: u64,
}
trait JsonRpcClient {
async fn get_root_hash(&self, batch_number: L1BatchNumber) -> Result<H256>;
// TODO implement get_tee_proofs(batch_number, tee_type) once 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]
async fn main() -> Result<()> {
let args = Arguments::parse();
let node_client: NodeClient<L2> = NodeClient::http(args.rpc_url.clone().into())
.context("failed creating JSON-RPC client for main node")?
.for_network(
L2ChainId::try_from(args.chain_id)
.map_err(anyhow::Error::msg)?
.into(),
)
.build();
let node_client = MainNodeClient(node_client);
let http_client = Client::new();
let request = GetProofsRequest {
jsonrpc: "2.0".to_string(),
id: 1,
method: "unstable_getTeeProofs".to_string(),
params: (args.batch_number, "Sgx".to_string()),
};
let response = http_client
.post(args.rpc_url)
.json(&request)
.send()
.await?
.error_for_status()?
.json::<GetProofsResponse>()
.await?;
for proof in response.result {
println!("Verifying batch #{}", proof.l1_batch_number);
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(args.batch_number).await?;
println!("Root hash: {}", root_hash);
verify_signature(&proof.signature, public_key, root_hash)?;
println!();
}
Ok(())
}
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(())
}
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")
}
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

@ -54,6 +54,9 @@
shells = {
default = "teepot";
};
devShells = {
default = "teepot";
};
};
outputs-builder = channels: {

View file

@ -3,6 +3,6 @@
{ teepotCrate }: teepotCrate.craneLib.cargoClippy (
teepotCrate.commonArgs // {
pname = "teepot";
inherit (teepotCrate) cargoArtifacts NIX_OUTPATH_USED_AS_RANDOM_SEED;
inherit (teepotCrate) cargoArtifacts;
}
)

View file

@ -0,0 +1,33 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2024 Matter Labs
{ dockerTools
, buildEnv
, teepot
, openssl
, curl
, nixsgx
, pkg-config
}:
dockerTools.buildLayeredImage {
name = "verify-era-proof-attestation";
config.Entrypoint = [ "${teepot.teepot.verify_era_proof_attestation}/bin/verify-era-proof-attestation" ];
config.Env = [ "LD_LIBRARY_PATH=/lib" ];
contents = buildEnv {
name = "image-root";
paths = with dockerTools; with nixsgx;[
pkg-config
openssl.out
curl.out
sgx-dcap.quote_verify
sgx-dcap.default_qpl
teepot.teepot.verify_era_proof_attestation
usrBinEnv
binSh
caCertificates
fakeNss
];
pathsToLink = [ "/bin" "/lib" "/etc" "/share" ];
};
}

View file

@ -3,9 +3,7 @@
{ teepotCrate }: teepotCrate.craneLib.buildPackage (
teepotCrate.commonArgs // {
pname = "teepot";
inherit (teepotCrate) cargoArtifacts
NIX_OUTPATH_USED_AS_RANDOM_SEED;
inherit (teepotCrate) cargoArtifacts;
passthru = {
inherit (teepotCrate) rustPlatform
@ -13,7 +11,6 @@
commonArgs
craneLib
cargoArtifacts;
NIX_OUTPATH_USED_AS_RANDOM_SEED = "aaaaaaaaaa";
};
outputs = [
@ -29,6 +26,7 @@
"vault_admin"
"vault_unseal"
"verify_attestation"
"verify_era_proof_attestation"
];
postInstall = ''
removeReferencesToVendoredSources "$out" "$cargoVendorDir"

View file

@ -9,6 +9,7 @@
}:
mkShell {
inputsFrom = [ teepot.teepot ];
packages = [
dive
taplo

View file

@ -8,7 +8,7 @@
, rust-bin
, pkgs
, src
, ...
, openssl
}:
let
rustVersion = rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
@ -24,6 +24,7 @@ let
];
buildInputs = [
openssl
nixsgx.sgx-sdk
nixsgx.sgx-dcap
nixsgx.sgx-dcap.quote_verify
@ -31,7 +32,6 @@ let
strictDeps = true;
src = with lib.fileset; toSource {
root = src;
fileset = unions [
@ -46,12 +46,15 @@ let
};
checkType = "debug";
env = {
OPENSSL_NO_VENDOR = "1";
NIX_OUTPATH_USED_AS_RANDOM_SEED = "aaaaaaaaaa";
};
};
cargoArtifacts = craneLib.buildDepsOnly (commonArgs // {
pname = "teepot-workspace";
inherit NIX_OUTPATH_USED_AS_RANDOM_SEED;
});
NIX_OUTPATH_USED_AS_RANDOM_SEED = "aaaaaaaaaa";
in
{
inherit rustPlatform
@ -59,5 +62,4 @@ in
commonArgs
craneLib
cargoArtifacts;
NIX_OUTPATH_USED_AS_RANDOM_SEED = "aaaaaaaaaa";
}