From ac768f30d80ffdba1f6d027543dda39fba4ea99f Mon Sep 17 00:00:00 2001 From: feliciss <22887031+feliciss@users.noreply.github.com> Date: Sun, 8 Sep 2024 23:29:19 +0900 Subject: [PATCH] [gh-2381] revise account sign command and add account verify command (#2566) * [gh-2381] add msg prefix to the account sign command. * [gh-2381] modify the sign command output as msg and auth payload. * [gh-2381] modify sign command for signing. * [gh-2381] add verify command. * [gh-2381] revise the sign and verify process. * [gh-2381] revise account verify command and add tests. * [gh-2381] follow #2580 to add account sign command. * [gh-2381] fix some issues of the account sign command. * [gh-2381] revise account verify command. * [gh-2381] revise the integ test. * [gh-2381] fix issues arisen from comments. * [gh-2381] fix cmd.feature. * [gh-2381] fix output. * [gh-2381] fix lint issues. * [gh-2381] fix tests. * [gh-2381] add assert to test and parsed signature struct. * [gh-2381] format the code. * [gh-2381] revise the signature hex and verify command. * [gh-2381] fix the issue of verification. --------- Co-authored-by: Feliciss <10203-feliciss@users.noreply.0xacab.org> --- .../src/jsonrpc_types/account_sign_view.rs | 17 ------ crates/rooch-rpc-api/src/jsonrpc_types/mod.rs | 2 - .../rooch-types/src/framework/auth_payload.rs | 47 ++++++++++++++-- crates/rooch-types/src/lib.rs | 1 + crates/rooch-types/src/rooch_signature.rs | 27 ++++++++++ crates/rooch-types/src/transaction/rooch.rs | 7 +++ .../src/commands/account/commands/mod.rs | 1 + .../src/commands/account/commands/sign.rs | 50 ++++++++--------- .../src/commands/account/commands/verify.rs | 53 +++++++++++++++++++ crates/rooch/src/commands/account/mod.rs | 4 +- crates/testsuite/features/cmd.feature | 5 ++ 11 files changed, 167 insertions(+), 47 deletions(-) delete mode 100644 crates/rooch-rpc-api/src/jsonrpc_types/account_sign_view.rs create mode 100644 crates/rooch-types/src/rooch_signature.rs create mode 100644 crates/rooch/src/commands/account/commands/verify.rs diff --git a/crates/rooch-rpc-api/src/jsonrpc_types/account_sign_view.rs b/crates/rooch-rpc-api/src/jsonrpc_types/account_sign_view.rs deleted file mode 100644 index 2fd5c0bbaf..0000000000 --- a/crates/rooch-rpc-api/src/jsonrpc_types/account_sign_view.rs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) RoochNetwork -// SPDX-License-Identifier: Apache-2.0 - -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub struct AccountSignView { - pub msg: String, - pub signature: String, -} - -impl AccountSignView { - pub fn new(msg: String, signature: String) -> Self { - Self { msg, signature } - } -} diff --git a/crates/rooch-rpc-api/src/jsonrpc_types/mod.rs b/crates/rooch-rpc-api/src/jsonrpc_types/mod.rs index f8cd9a078b..172a62eedd 100644 --- a/crates/rooch-rpc-api/src/jsonrpc_types/mod.rs +++ b/crates/rooch-rpc-api/src/jsonrpc_types/mod.rs @@ -35,5 +35,3 @@ pub use rpc_options::*; pub use state_view::*; pub use str_view::*; pub use transaction_argument_view::*; - -pub mod account_sign_view; diff --git a/crates/rooch-types/src/framework/auth_payload.rs b/crates/rooch-types/src/framework/auth_payload.rs index 85dd07307d..3bd6ad1c0e 100644 --- a/crates/rooch-types/src/framework/auth_payload.rs +++ b/crates/rooch-types/src/framework/auth_payload.rs @@ -18,6 +18,7 @@ use moveos_types::{ h256::{sha2_256_of, H256}, state::{MoveStructState, MoveStructType}, }; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::io; @@ -25,8 +26,8 @@ pub const MODULE_NAME: &IdentStr = ident_str!("auth_payload"); /// The original message prefix of the Bitcoin wallet includes the length of the message `x18` /// We remove the length because the bitcoin consensus codec serialization format already contains the length information -const MESSAGE_INFO_PREFIX: &[u8] = b"Bitcoin Signed Message:\n"; -const MESSAGE_INFO: &[u8] = b"Rooch Transaction:\n"; +pub const MESSAGE_INFO_PREFIX: &[u8] = b"Bitcoin Signed Message:\n"; +pub const MESSAGE_INFO: &[u8] = b"Rooch Transaction:\n"; const TX_HASH_HEX_LENGTH: usize = 64; @@ -54,10 +55,24 @@ impl SignData { } } + pub fn new_without_tx_hash( + message_prefix: Vec, + message_info_without_tx_hash: Vec, + ) -> Self { + SignData { + message_prefix, + message_info: message_info_without_tx_hash, + } + } + pub fn new_with_default(tx_data: &RoochTransactionData) -> Self { Self::new(MESSAGE_INFO_PREFIX.to_vec(), MESSAGE_INFO.to_vec(), tx_data) } + pub fn new_without_tx_hash_with_default() -> Self { + Self::new_without_tx_hash(MESSAGE_INFO_PREFIX.to_vec(), MESSAGE_INFO.to_vec()) + } + pub fn encode(&self) -> Vec { let mut data = Vec::new(); self.consensus_encode(&mut data) @@ -94,7 +109,7 @@ impl Decodable for SignData { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct AuthPayload { // Message signature pub signature: Vec, @@ -149,6 +164,21 @@ impl AuthPayload { } } + pub fn new_without_tx_hash( + sign_data: SignData, + signature: Signature, + bitcoin_address: String, + ) -> Self { + debug_assert_eq!(signature.scheme(), SignatureScheme::Secp256k1); + AuthPayload { + signature: signature.signature_bytes().to_vec(), + message_prefix: sign_data.message_prefix, + message_info: sign_data.message_info, + public_key: signature.public_key_bytes().to_vec(), + from_address: bitcoin_address.into_bytes(), + } + } + pub fn verify(&self, tx_data: &RoochTransactionData) -> Result<()> { let pk = Secp256k1PublicKey::from_bytes(&self.public_key)?; let sign_data = SignData::new( @@ -163,6 +193,17 @@ impl AuthPayload { Ok(()) } + pub fn verify_without_tx_hash(&self) -> Result<()> { + let pk = Secp256k1PublicKey::from_bytes(&self.public_key)?; + let sign_data = + SignData::new_without_tx_hash(self.message_prefix.clone(), self.message_info.clone()); + let message = sign_data.encode(); + let message_hash = sha2_256_of(&message).0.to_vec(); + let signature = Secp256k1Signature::from_bytes(&self.signature)?; + pk.verify_with_hash::(&message_hash, &signature)?; + Ok(()) + } + pub fn from_address(&self) -> Result { Ok(String::from_utf8(self.from_address.to_vec())?) } diff --git a/crates/rooch-types/src/lib.rs b/crates/rooch-types/src/lib.rs index 5c89eb3073..24aba0bae8 100644 --- a/crates/rooch-types/src/lib.rs +++ b/crates/rooch-types/src/lib.rs @@ -20,6 +20,7 @@ pub mod nursery; pub mod repair; pub mod rooch_key; pub mod rooch_network; +pub mod rooch_signature; pub mod sequencer; pub mod service_status; pub mod test_utils; diff --git a/crates/rooch-types/src/rooch_signature.rs b/crates/rooch-types/src/rooch_signature.rs new file mode 100644 index 0000000000..6428d5abcc --- /dev/null +++ b/crates/rooch-types/src/rooch_signature.rs @@ -0,0 +1,27 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +use crate::crypto::Signature; +use fastcrypto::traits::ToFromBytes; +use serde::{Deserialize, Serialize}; + +// Parsed Rooch Signature, either Ed25519RoochSignature or Secp256k1RoochSignature +#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] +pub struct ParsedSignature(Signature); + +impl ParsedSignature { + pub fn into_inner(self) -> Signature { + self.0 + } + + pub fn from_signature(signature: Signature) -> Self { + Self(signature) + } + + pub fn parse(s: &str) -> anyhow::Result { + let signature_bytes = hex::decode(s)?; + Ok(Self::from_signature(Signature::from_bytes( + &signature_bytes, + )?)) + } +} diff --git a/crates/rooch-types/src/transaction/rooch.rs b/crates/rooch-types/src/transaction/rooch.rs index db6cffa089..e546d9546a 100644 --- a/crates/rooch-types/src/transaction/rooch.rs +++ b/crates/rooch-types/src/transaction/rooch.rs @@ -63,6 +63,13 @@ impl RoochTransactionData { bcs::to_bytes(self).expect("encode transaction should success") } + pub fn decode(bytes: &[u8]) -> Result + where + Self: std::marker::Sized, + { + bcs::from_bytes::(bytes).map_err(Into::into) + } + pub fn tx_hash(&self) -> H256 { moveos_types::h256::sha3_256_of(self.encode().as_slice()) } diff --git a/crates/rooch/src/commands/account/commands/mod.rs b/crates/rooch/src/commands/account/commands/mod.rs index 4fa2ac3f24..87d31ac5fd 100644 --- a/crates/rooch/src/commands/account/commands/mod.rs +++ b/crates/rooch/src/commands/account/commands/mod.rs @@ -11,3 +11,4 @@ pub mod nullify; pub mod sign; pub mod switch; pub mod transfer; +pub mod verify; diff --git a/crates/rooch/src/commands/account/commands/sign.rs b/crates/rooch/src/commands/account/commands/sign.rs index 42757bbafe..1219759df6 100644 --- a/crates/rooch/src/commands/account/commands/sign.rs +++ b/crates/rooch/src/commands/account/commands/sign.rs @@ -4,58 +4,60 @@ use crate::cli_types::{CommandAction, WalletContextOptions}; use async_trait::async_trait; use clap::Parser; -use hex::ToHex; +use moveos_types::state::MoveState; use rooch_key::keystore::account_keystore::AccountKeystore; -use rooch_rpc_api::jsonrpc_types::account_sign_view::AccountSignView; use rooch_types::{ address::ParsedAddress, - error::{RoochError, RoochResult}, + error::RoochResult, + framework::auth_payload::{SignData, MESSAGE_INFO_PREFIX}, }; -/// Sign an msg with current account private key (sign_hashed) -/// -/// This operation must be specified with -a or -/// --address to export only one address with a private key. +/// Sign a message with a parsed address #[derive(Debug, Parser)] pub struct SignCommand { + // An address to be used #[clap(short = 'a', long = "address", value_parser=ParsedAddress::parse, default_value = "")] address: ParsedAddress, + + /// A message to be signed + #[clap(short = 'm', long)] + message: String, + #[clap(flatten)] pub context_options: WalletContextOptions, /// Return command outputs in json format #[clap(long, default_value = "false")] json: bool, - - /// Msg command will sign - #[clap(long, default_value = "")] - msg: String, } #[async_trait] -impl CommandAction> for SignCommand { - async fn execute(self) -> RoochResult> { +impl CommandAction> for SignCommand { + async fn execute(self) -> RoochResult> { let context = self.context_options.build_require_password()?; let password = context.get_password(); - let mapping = context.address_mapping(); - let addrss = self.address.into_rooch_address(&mapping).map_err(|e| { - RoochError::CommandArgumentError(format!("Invalid Rooch address String: {}", e)) - })?; + let rooch_address = self.address.into_rooch_address(&mapping)?; + + let sign_data = + SignData::new_without_tx_hash(MESSAGE_INFO_PREFIX.to_vec(), self.message.to_bytes()); + let encoded_sign_data = sign_data.encode(); let signature = context .keystore - .sign_hashed(&addrss, &self.msg.clone().into_bytes(), password)?; + .sign_hashed(&rooch_address, &encoded_sign_data, password)?; + + let signature_bytes = signature.as_ref(); + let signature_hex = hex::encode(signature_bytes); if self.json { - Ok(Some(AccountSignView::new( - self.msg.clone(), - signature.encode_hex(), - ))) + Ok(Some(signature_hex)) } else { - println!("Msg you input : {}", &self.msg); - println!("Signature : {}", signature.encode_hex::()); + println!( + "Sign message succeeded with the signatue {:?}", + signature_hex + ); Ok(None) } } diff --git a/crates/rooch/src/commands/account/commands/verify.rs b/crates/rooch/src/commands/account/commands/verify.rs new file mode 100644 index 0000000000..22f1330fcf --- /dev/null +++ b/crates/rooch/src/commands/account/commands/verify.rs @@ -0,0 +1,53 @@ +// Copyright (c) RoochNetwork +// SPDX-License-Identifier: Apache-2.0 + +use crate::cli_types::{CommandAction, WalletContextOptions}; +use async_trait::async_trait; +use clap::Parser; +use moveos_types::state::MoveState; +use rooch_types::{ + crypto::RoochSignature, + error::RoochResult, + framework::auth_payload::{SignData, MESSAGE_INFO_PREFIX}, + rooch_signature::ParsedSignature, +}; + +/// Verify a signature +#[derive(Debug, Parser)] +pub struct VerifyCommand { + /// A signature for verify + #[clap(short = 's', long, value_parser=ParsedSignature::parse)] + signature: ParsedSignature, + + /// An original message to be verified + #[clap(short = 'm', long)] + message: String, + + #[clap(flatten)] + pub context_options: WalletContextOptions, + + /// Return command outputs in json format + #[clap(long, default_value = "false")] + json: bool, +} + +#[async_trait] +impl CommandAction> for VerifyCommand { + async fn execute(self) -> RoochResult> { + let sign_data = + SignData::new_without_tx_hash(MESSAGE_INFO_PREFIX.to_vec(), self.message.to_bytes()); + let encoded_sign_data = sign_data.encode(); + let verify_result = self + .signature + .into_inner() + .verify(&encoded_sign_data) + .is_ok(); + + if self.json { + Ok(Some(verify_result)) + } else { + println!("Verification result: {}", verify_result); + Ok(None) + } + } +} diff --git a/crates/rooch/src/commands/account/mod.rs b/crates/rooch/src/commands/account/mod.rs index a4be829de8..eb9338e63c 100644 --- a/crates/rooch/src/commands/account/mod.rs +++ b/crates/rooch/src/commands/account/mod.rs @@ -6,7 +6,7 @@ use async_trait::async_trait; use commands::{ balance::BalanceCommand, create::CreateCommand, create_multisign::CreateMultisignCommand, export::ExportCommand, import::ImportCommand, list::ListCommand, nullify::NullifyCommand, - sign::SignCommand, switch::SwitchCommand, transfer::TransferCommand, + sign::SignCommand, switch::SwitchCommand, transfer::TransferCommand, verify::VerifyCommand, }; use rooch_types::error::RoochResult; use std::path::PathBuf; @@ -38,6 +38,7 @@ impl CommandAction for Account { AccountCommand::Export(export) => export.execute_serialized().await, AccountCommand::Import(import) => import.execute_serialized().await, AccountCommand::Sign(sign) => sign.execute_serialized().await, + AccountCommand::Verify(verify) => verify.execute_serialized().await, } } } @@ -55,4 +56,5 @@ pub enum AccountCommand { Export(ExportCommand), Import(ImportCommand), Sign(SignCommand), + Verify(VerifyCommand), } diff --git a/crates/testsuite/features/cmd.feature b/crates/testsuite/features/cmd.feature index 271e5e406d..d112b3b24a 100644 --- a/crates/testsuite/features/cmd.feature +++ b/crates/testsuite/features/cmd.feature @@ -37,6 +37,11 @@ Feature: Rooch CLI integration tests Then cmd: "account create" Then cmd: "account list --json" + # account sign and verify + Then cmd: "account sign -a {{$.account[-1].account0.address}} -m 'empty' --json" + Then cmd: "account verify -s {{$.account[-1]}} -m 'empty' --json" + Then assert: "{{$.account[-1]}} == true" + Then cmd: "account list --json" Then cmd: "account export" Then cmd: "account export -a {{$.account[-1].account0.address}} --json" # use bitcoin_address