Skip to content

Commit

Permalink
Verify raw Wasm in cargo contract verify (#1551)
Browse files Browse the repository at this point in the history
* verify wasm

* add changelog

* cleanup

* load wasm directly

* correctly compare wasm binaries + tests

* Apply suggestions from code review

Co-authored-by: Andrew Jones <[email protected]>

* refactor

* fix error stderr

---------

Co-authored-by: Andrew Jones <[email protected]>
  • Loading branch information
Gherman and ascjones authored Mar 21, 2024
1 parent 04e42b9 commit 98cf70d
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 18 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Verify raw Wasm in cargo contract verify - [#1551](https://github.com/paritytech/cargo-contract/pull/1551)

## [4.0.2]

### Fixed
Expand Down
99 changes: 94 additions & 5 deletions crates/cargo-contract/src/cmd/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use anyhow::{
};
use colored::Colorize;
use contract_build::{
code_hash,
execute,
verbose_eprintln,
BuildArtifacts,
Expand All @@ -31,7 +32,10 @@ use contract_build::{
Verbosity,
VerbosityFlags,
};
use contract_metadata::ContractMetadata;
use contract_metadata::{
CodeHash,
ContractMetadata,
};

use std::{
fs::File,
Expand All @@ -49,7 +53,12 @@ pub struct VerifyCommand {
manifest_path: Option<PathBuf>,
/// The reference Wasm contract (`*.contract`) that the workspace will be checked
/// against.
contract: PathBuf,
#[clap(long)]
contract: Option<PathBuf>,
/// The reference Wasm contract binary (`*.wasm`) that the workspace will be checked
/// against.
#[clap(long, conflicts_with = "contract")]
wasm: Option<PathBuf>,
/// Denotes if output should be printed to stdout.
#[clap(flatten)]
verbosity: VerbosityFlags,
Expand All @@ -62,9 +71,90 @@ impl VerifyCommand {
pub fn run(&self) -> Result<VerificationResult> {
let manifest_path = ManifestPath::try_from(self.manifest_path.as_ref())?;
let verbosity: Verbosity = TryFrom::<&VerbosityFlags>::try_from(&self.verbosity)?;
if let Some(path) = &self.contract {
self.verify_contract(manifest_path, verbosity, path)
} else if let Some(path) = &self.wasm {
self.verify_wasm(manifest_path, verbosity, path)
} else {
anyhow::bail!("Either --wasm or --contract must be specified")
}
}

/// Verify `.wasm` binary.
fn verify_wasm(
&self,
manifest_path: ManifestPath,
verbosity: Verbosity,
path: &PathBuf,
) -> Result<VerificationResult> {
// 1. Read code hash binary from the path.
let ref_buffer = std::fs::read(path)
.context(format!("Failed to read contract binary {}", path.display()))?;

let reference_code_hash = CodeHash(code_hash(&ref_buffer));

// 2. Call `cargo contract build` in the release mode.
let args = ExecuteArgs {
manifest_path: manifest_path.clone(),
verbosity,
optimization_passes: Some(contract_build::OptimizationPasses::Z),
build_mode: BuildMode::Release,
build_artifact: BuildArtifacts::CodeOnly,
extra_lints: false,
..Default::default()
};

let build_result = execute(args)?;

// 4. Grab the code hash from the built contract and compare it with the reference
// one.
let built_wasm_path = if let Some(m) = build_result.dest_wasm {
m
} else {
// Since we're building the contract ourselves this should always be
// populated, but we'll bail out here just in case.
anyhow::bail!("\nThe workspace contract does not contain a Wasm binary,\n\
therefore we are unable to verify the contract."
.to_string()
.bright_yellow())
};

let target_buffer = std::fs::read(&built_wasm_path).context(format!(
"Failed to read contract binary {}",
built_wasm_path.display()
))?;

let output_code_hash = CodeHash(code_hash(&target_buffer));

if output_code_hash != reference_code_hash {
anyhow::bail!(format!(
"\nFailed to verify the authenticity of wasm binary at {} against the workspace \n\
found at {}.\n Expected {}, found {}",
format!("`{}`", path.display()).bright_white(),
format!("`{}`", built_wasm_path.display()).bright_white(),
format!("{}", reference_code_hash).bright_white(),
format!("{}", output_code_hash).bright_white())
);
}

Ok(VerificationResult {
is_verified: true,
image: None,
contract: built_wasm_path.display().to_string(),
reference_contract: path.display().to_string(),
output_json: self.output_json,
verbosity,
})
}

/// Verify the `.contract` bundle.
fn verify_contract(
&self,
manifest_path: ManifestPath,
verbosity: Verbosity,
path: &PathBuf,
) -> Result<VerificationResult> {
// 1. Read the given metadata, and pull out the `BuildInfo`
let path = &self.contract;
let file = File::open(path)
.context(format!("Failed to open contract bundle {}", path.display()))?;

Expand Down Expand Up @@ -166,7 +256,7 @@ impl VerifyCommand {
)
};

let target_bundle = built_contract_path.dest_bundle;
let target_bundle = &built_contract_path.dest_bundle;

let file = File::open(target_bundle.clone()).context(format!(
"Failed to open contract bundle {}",
Expand All @@ -187,7 +277,6 @@ impl VerifyCommand {
&reference_code_hash,
&target_code_hash
);

anyhow::bail!(format!(
"\nFailed to verify the authenticity of {} contract against the workspace \n\
found at {}.",
Expand Down
61 changes: 48 additions & 13 deletions crates/cargo-contract/tests/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ fn cargo_contract<P: AsRef<Path>>(path: P) -> assert_cmd::Command {
cmd
}

/// Compile the reference contract and return a byte array of its bundle.
fn compile_reference_contract() -> Vec<u8> {
/// Compile the reference contract and return a byte array of its bundle and raw wasm
/// binary.
fn compile_reference_contract() -> (Vec<u8>, Vec<u8>) {
let contract = r#"
#![cfg_attr(not(feature = "std"), no_std, no_main)]
Expand All @@ -48,7 +49,7 @@ fn compile_reference_contract() -> Vec<u8> {
#[ink(message)]
pub fn inc(&mut self, by: i32) {
self.value.saturating_add(by);
self.value = self.value.saturating_add(by);
}
#[ink(message, selector = 0xCACACACA)]
Expand Down Expand Up @@ -82,9 +83,14 @@ fn compile_reference_contract() -> Vec<u8> {
.success();

let bundle_path = project_dir.join("target/ink/incrementer.contract");
let bundle = std::fs::read(bundle_path)
.expect("Failed to read the content of the contract bundle!");

std::fs::read(bundle_path)
.expect("Failed to read the content of the contract bundle!")
let wasm_path = project_dir.join("target/ink/incrementer.wasm");
let wasm = std::fs::read(wasm_path)
.expect("Failed to read the content of the contract binary!");

(bundle, wasm)
}

#[test]
Expand Down Expand Up @@ -113,7 +119,7 @@ fn verify_equivalent_contracts() {
#[ink(message)]
pub fn inc(&mut self, by: i32) {
self.value.saturating_add(by);
self.value = self.value.saturating_add(by);
}
#[ink(message, selector = 0xCACACACA)]
Expand All @@ -139,23 +145,36 @@ fn verify_equivalent_contracts() {
let lib = project_dir.join("lib.rs");
std::fs::write(lib, contract).expect("Failed to write contract lib.rs");

// Compile reference contract and write bundle in the directory.
let reference_contents = compile_reference_contract();
// Compile reference contract and write bundle and wasm in the directory.
let (ref_bundle, ref_wasm) = compile_reference_contract();
let bundle = project_dir.join("reference.contract");
std::fs::write(bundle, reference_contents)
std::fs::write(bundle, ref_bundle)
.expect("Failed to write bundle contract to the current dir!");
let wasm = project_dir.join("reference.wasm");
std::fs::write(wasm, ref_wasm)
.expect("Failed to write wasm binary to the current dir!");

// when
let output: &str = r#""is_verified": true"#;

// then
cargo_contract(&project_dir)
.arg("verify")
.arg("--contract")
.arg("reference.contract")
.arg("--output-json")
.assert()
.success()
.stdout(predicates::str::contains(output));
// and
cargo_contract(&project_dir)
.arg("verify")
.arg("--wasm")
.arg("reference.wasm")
.arg("--output-json")
.assert()
.success()
.stdout(predicates::str::contains(output));
}

#[test]
Expand Down Expand Up @@ -184,7 +203,7 @@ fn verify_different_contracts() {
#[ink(message)]
pub fn inc(&mut self, by: i32) {
self.value.saturating_add(by);
self.value = self.value.saturating_add(by);
}
#[ink(message, selector = 0xCBCBCBCB)]
Expand Down Expand Up @@ -214,21 +233,37 @@ fn verify_different_contracts() {
tracing::debug!("Building contract in {}", project_dir.to_string_lossy());
cargo_contract(&project_dir).arg("build").assert().success();

// Compile reference contract and write bundle in the directory.
let reference_contents = compile_reference_contract();
// Compile reference contract and write bundle and wasm in the directory.
let (ref_bundle, ref_wasm) = compile_reference_contract();
let bundle = project_dir.join("reference.contract");
std::fs::write(bundle, reference_contents)
std::fs::write(bundle, ref_bundle)
.expect("Failed to write bundle contract to the current dir!");
let wasm = project_dir.join("reference.wasm");
std::fs::write(wasm, ref_wasm)
.expect("Failed to write wasm binary to the current dir!");

// when
let output: &str = r#"Failed to verify the authenticity of `incrementer`"#;

// then
cargo_contract(&project_dir)
.arg("verify")
.arg("--contract")
.arg("reference.contract")
.arg("--output-json")
.assert()
.failure()
.stderr(predicates::str::contains(output));
// and

let output: &str =
r#"Failed to verify the authenticity of wasm binary at `reference.wasm`"#;
cargo_contract(&project_dir)
.arg("verify")
.arg("--wasm")
.arg("reference.wasm")
.arg("--output-json")
.assert()
.failure()
.stderr(predicates::str::contains(output));
}

0 comments on commit 98cf70d

Please sign in to comment.