Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experimental contract verify command #1778

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions FULL_HELP_DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ Tools for smart contract developers
* `extend` — Extend the time to live ledger of a contract-data ledger entry
* `deploy` — Deploy a wasm contract
* `fetch` — Fetch a contract's Wasm binary
* `verify` — Verify the source that build the Wasm binary
* `id` — Generate the contract id for a given contract or asset
* `info` — Access info about contracts
* `init` — Initialize a Soroban contract project
Expand Down Expand Up @@ -450,6 +451,26 @@ Fetch a contract's Wasm binary



## `stellar contract verify`

Verify the source that build the Wasm binary

**Usage:** `stellar contract verify [OPTIONS] <--wasm <WASM>|--wasm-hash <WASM_HASH>|--id <CONTRACT_ID>>`

###### **Options:**

* `--wasm <WASM>` — Wasm file to extract the data from
* `--wasm-hash <WASM_HASH>` — Wasm hash to get the data for
* `--id <CONTRACT_ID>` — Contract id or contract alias to get the data for
* `--rpc-url <RPC_URL>` — RPC server endpoint
* `--rpc-header <RPC_HEADERS>` — RPC Header(s) to include in requests to the RPC provider
* `--network-passphrase <NETWORK_PASSPHRASE>` — Network passphrase to sign the transaction sent to the rpc server
* `--network <NETWORK>` — Name of network to use from config
* `--global` — Use global config
* `--config-dir <CONFIG_DIR>` — Location of config directory, default is "."



## `stellar contract id`

Generate the contract id for a given contract or asset
Expand Down
2 changes: 1 addition & 1 deletion cmd/soroban-cli/src/commands/contract/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::fmt::Debug;
pub mod env_meta;
pub mod interface;
pub mod meta;
mod shared;
pub mod shared;

#[derive(Debug, clap::Subcommand)]
pub enum Cmd {
Expand Down
8 changes: 8 additions & 0 deletions cmd/soroban-cli/src/commands/contract/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub mod invoke;
pub mod optimize;
pub mod read;
pub mod restore;
pub mod verify;

use crate::commands::global;

Expand Down Expand Up @@ -45,6 +46,9 @@ pub enum Cmd {
/// Fetch a contract's Wasm binary
Fetch(fetch::Cmd),

/// Verify the source that build the Wasm binary
Verify(verify::Cmd),

/// Generate the contract id for a given contract or asset
#[command(subcommand)]
Id(id::Cmd),
Expand Down Expand Up @@ -113,6 +117,9 @@ pub enum Error {
#[error(transparent)]
Fetch(#[from] fetch::Error),

#[error(transparent)]
Verify(#[from] verify::Error),

#[error(transparent)]
Init(#[from] init::Error),

Expand Down Expand Up @@ -158,6 +165,7 @@ impl Cmd {
Cmd::Invoke(invoke) => invoke.run(global_args).await?,
Cmd::Optimize(optimize) => optimize.run()?,
Cmd::Fetch(fetch) => fetch.run().await?,
Cmd::Verify(verify) => verify.run(global_args).await?,
Cmd::Read(read) => read.run().await?,
Cmd::Restore(restore) => restore.run().await?,
}
Expand Down
250 changes: 250 additions & 0 deletions cmd/soroban-cli/src/commands/contract/verify.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
use super::info::shared;
use crate::{
commands::{contract::info::shared::fetch_wasm, global},
print::Print,
utils::http,
};
use base64::Engine as _;
use clap::{command, Parser};
use sha2::{Digest, Sha256};
use soroban_spec_tools::contract;
use soroban_spec_tools::contract::Spec;
use std::fmt::Debug;
use stellar_xdr::curr::{ScMetaEntry, ScMetaV0};

#[derive(Parser, Debug, Clone)]
#[group(skip)]
pub struct Cmd {
#[command(flatten)]
pub common: shared::Args,
}

#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error(transparent)]
Wasm(#[from] shared::Error),
#[error(transparent)]
Spec(#[from] contract::Error),
#[error("'source_repo' meta entry is not stored in the contract")]
SourceRepoNotSpecified,
#[error("'source_repo' meta entry '{0}' has prefix unsupported, only 'github:' supported")]
SourceRepoUnsupported(String),
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error(transparent)]
Reqwest(#[from] reqwest::Error),
#[error("GitHub attestation not found")]
AttestationNotFound,
#[error("GitHub attestation invalid")]
AttestationInvalid,
}

impl Cmd {
pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
let print = Print::new(global_args.quiet);

print.infoln("Loading wasm...");
let Some(bytes) = fetch_wasm(&self.common).await? else {
return Err(Error::SourceRepoNotSpecified);
};
let wasm_hash = Sha256::digest(&bytes);
let wasm_hash_hex = hex::encode(wasm_hash);
print.infoln(format!("Wasm Hash: {wasm_hash_hex}"));

let spec = Spec::new(&bytes)?;
let Some(source_repo) = spec.meta.iter().find_map(|meta_entry| {
let ScMetaEntry::ScMetaV0(ScMetaV0 { key, val }) = meta_entry;
if key.to_string() == "source_repo" {
Some(val.to_string())
} else {
None
}
}) else {
return Err(Error::SourceRepoNotSpecified);
};
print.infoln(format!("Source Repo: {source_repo}"));
let Some(github_source_repo) = source_repo.strip_prefix("github:") else {
return Err(Error::SourceRepoUnsupported(source_repo));
};

let url = format!(
"https://api.github.com/repos/{github_source_repo}/attestations/sha256:{wasm_hash_hex}"
);
print.infoln(format!("Collecting GitHub attestation from {url}..."));
let resp = http::client().get(url).send().await?;
let resp: gh_attest_resp::Root = resp.json().await?;
let Some(attestation) = resp.attestations.first() else {
return Err(Error::AttestationNotFound);
};
let Ok(payload) = base64::engine::general_purpose::STANDARD
.decode(&attestation.bundle.dsse_envelope.payload)
else {
return Err(Error::AttestationInvalid);
};
let payload: gh_payload::Root = serde_json::from_slice(&payload)?;
print.checkln("Attestation found linked to GitHub Actions Workflow Run:");
let workflow_repo = payload
.predicate
.build_definition
.external_parameters
.workflow
.repository;
let workflow_ref = payload
.predicate
.build_definition
.external_parameters
.workflow
.ref_field;
let workflow_path = payload
.predicate
.build_definition
.external_parameters
.workflow
.path;
let git_commit = &payload
.predicate
.build_definition
.resolved_dependencies
.first()
.unwrap()
.digest
.git_commit;
let runner_environment = payload
.predicate
.build_definition
.internal_parameters
.github
.runner_environment
.as_str();
print.checkln(format!(" • Repository: {workflow_repo}"));
print.checkln(format!(" • Ref: {workflow_ref}"));
print.checkln(format!(" • Path: {workflow_path}"));
print.checkln(format!(" • Git Commit: {git_commit}"));
match runner_environment
{
runner @ "github-hosted" => print.checkln(format!(" • Runner: {runner}")),
runner => print.warnln(format!(" • Runner: {runner} (runners not hosted by GitHub could have any configuration or environmental changes)")),
}
print.checkln(format!(
" • Run: {}",
payload.predicate.run_details.metadata.invocation_id
));
print.globeln(format!(
"View the workflow at {workflow_repo}/blob/{git_commit}/{workflow_path}"
));
print.globeln(format!(
"View the repo at {workflow_repo}/tree/{git_commit}"
));

Ok(())
}
}

mod gh_attest_resp {
use serde::Deserialize;
use serde::Serialize;

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Root {
pub attestations: Vec<Attestation>,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Attestation {
pub bundle: Bundle,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Bundle {
pub dsse_envelope: DsseEnvelope,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DsseEnvelope {
pub payload: String,
}
}

mod gh_payload {
use serde::Deserialize;
use serde::Serialize;

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Root {
pub predicate_type: String,
pub predicate: Predicate,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Predicate {
pub build_definition: BuildDefinition,
pub run_details: RunDetails,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BuildDefinition {
pub external_parameters: ExternalParameters,
pub internal_parameters: InternalParameters,
pub resolved_dependencies: Vec<ResolvedDependency>,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExternalParameters {
pub workflow: Workflow,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Workflow {
#[serde(rename = "ref")]
pub ref_field: String,
pub repository: String,
pub path: String,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InternalParameters {
pub github: Github,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Github {
#[serde(rename = "runner_environment")]
pub runner_environment: String,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResolvedDependency {
pub uri: String,
pub digest: Digest,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Digest {
pub git_commit: String,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RunDetails {
pub metadata: Metadata,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Metadata {
pub invocation_id: String,
}
}
Loading