From 6dd08045bd4ff751504bb0d3c34bd4dde68847d0 Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Mon, 5 Feb 2024 17:35:05 +0100 Subject: [PATCH] Add wallet commands to sign and verify arbitrary messages --- .../signature/inputsig/arbitrary_message.rs | 4 + .../test_framework/wallet_cli_controller.py | 12 ++ test/functional/test_runner.py | 1 + test/functional/wallet_tx_compose.py | 2 +- wallet/src/account/mod.rs | 22 ++++ wallet/src/wallet/mod.rs | 18 +++ wallet/wallet-cli-lib/src/commands/mod.rs | 111 ++++++++++++++++++ .../src/synced_controller.rs | 11 ++ wallet/wallet-rpc-lib/src/rpc/mod.rs | 44 +++++++ wallet/wallet-rpc-lib/src/rpc/types.rs | 4 + 10 files changed, 228 insertions(+), 1 deletion(-) diff --git a/common/src/chain/transaction/signature/inputsig/arbitrary_message.rs b/common/src/chain/transaction/signature/inputsig/arbitrary_message.rs index 476cb2aed1..ec5597da0c 100644 --- a/common/src/chain/transaction/signature/inputsig/arbitrary_message.rs +++ b/common/src/chain/transaction/signature/inputsig/arbitrary_message.rs @@ -70,6 +70,10 @@ impl SignedArbitraryMessage { Self { raw_signature } } + pub fn to_hex(self) -> String { + hex::encode(self.raw_signature) + } + pub fn verify_signature( &self, chain_config: &ChainConfig, diff --git a/test/functional/test_framework/wallet_cli_controller.py b/test/functional/test_framework/wallet_cli_controller.py index 2b02038dc7..f4d1dde516 100644 --- a/test/functional/test_framework/wallet_cli_controller.py +++ b/test/functional/test_framework/wallet_cli_controller.py @@ -294,6 +294,18 @@ async def decommission_stake_pool_request(self, pool_id: str) -> str: async def sign_raw_transaction(self, transaction: str) -> str: return await self._write_command(f"account-sign-raw-transaction {transaction}\n") + async def sign_challenge_plain(self, message: str, address: str) -> str: + return await self._write_command(f'account-sign-challenge-plain "{message}" {address}\n') + + async def sign_challenge_hex(self, message: str, address: str) -> str: + return await self._write_command(f'account-sign-challenge-hex "{message}" {address}\n') + + async def verify_challenge_plain(self, message: str, signature: str, address: str) -> str: + return await self._write_command(f'verify-challenge-plain "{message}" {signature} {address}\n') + + async def verify_challenge_hex(self, message: str, signature: str, address: str) -> str: + return await self._write_command(f'verify-challenge-hex "{message}" {signature} {address}\n') + async def submit_transaction(self, transaction: str) -> str: return await self._write_command(f"node-submit-transaction {transaction}\n") diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 3b142dc4a3..19e10e815b 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -130,6 +130,7 @@ class UnicodeOnWindowsError(ValueError): 'feature_db_reinit.py', 'feature_lmdb_backend_test.py', 'wallet_conflict.py', + 'wallet_sign_message.py', 'wallet_tx_compose.py', 'wallet_data_deposit.py', 'wallet_submit_tx.py', diff --git a/test/functional/wallet_tx_compose.py b/test/functional/wallet_tx_compose.py index f6f0fd0d80..8d6fc29926 100644 --- a/test/functional/wallet_tx_compose.py +++ b/test/functional/wallet_tx_compose.py @@ -154,7 +154,7 @@ def make_output(pub_key_bytes): assert len(encoded_tx) < len(encoded_ptx) output = await wallet.sign_raw_transaction(encoded_tx) - assert_in("The transaction has been fully signed signed", output) + assert_in("The transaction has been fully signed and is ready to be broadcast to network.", output) signed_tx = output.split('\n')[2] assert_in("The transaction was submitted successfully", await wallet.submit_transaction(signed_tx)) diff --git a/wallet/src/account/mod.rs b/wallet/src/account/mod.rs index 66822bcd55..a0f9d2896b 100644 --- a/wallet/src/account/mod.rs +++ b/wallet/src/account/mod.rs @@ -20,6 +20,7 @@ mod utxo_selector; use common::address::pubkeyhash::PublicKeyHash; use common::chain::block::timestamp::BlockTimestamp; +use common::chain::signature::inputsig::arbitrary_message::SignedArbitraryMessage; use common::chain::{AccountCommand, AccountOutPoint, AccountSpending, TransactionCreationError}; use common::primitives::id::WithId; use common::primitives::{Idable, H256}; @@ -1234,6 +1235,27 @@ impl Account { )) } + pub fn sign_challenge( + &self, + message: Vec, + destination: Destination, + db_tx: &impl WalletStorageReadUnlocked, + ) -> WalletResult { + let private_key = self + .key_chain + .get_private_key_for_destination(&destination, db_tx)? + .ok_or(WalletError::DestinationNotFromThisWallet)? + .private_key(); + + let sig = SignedArbitraryMessage::produce_uniparty_signature( + &private_key, + &destination, + &message, + )?; + + Ok(sig) + } + pub fn sign_raw_transaction( &self, tx: TransactionToSign, diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index a951ddac83..c2af75530d 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -32,6 +32,9 @@ use crate::{Account, SendRequest}; pub use bip39::{Language, Mnemonic}; use common::address::{Address, AddressError}; use common::chain::block::timestamp::BlockTimestamp; +use common::chain::signature::inputsig::arbitrary_message::{ + SignArbitraryMessageError, SignedArbitraryMessage, +}; use common::chain::signature::DestinationSigError; use common::chain::tokens::{ make_token_id, IsTokenUnfreezable, Metadata, RPCFungibleTokenInfo, TokenId, TokenIssuance, @@ -199,6 +202,10 @@ pub enum WalletError { FullySignedTransactionInDecommissionReq, #[error("Input cannot be signed")] InputCannotBeSigned, + #[error("Destination does not belong to this wallet")] + DestinationNotFromThisWallet, + #[error("Sign message error: {0}")] + SignMessageError(#[from] SignArbitraryMessageError), #[error("Input cannot be spent {0:?}")] InputCannotBeSpent(TxOutput), #[error("Failed to convert partially signed tx to signed")] @@ -1373,6 +1380,17 @@ impl Wallet { }) } + pub fn sign_challenge( + &mut self, + account_index: U31, + challenge: Vec, + destination: Destination, + ) -> WalletResult { + self.for_account_rw_unlocked(account_index, |account, db_tx, _| { + account.sign_challenge(challenge, destination, db_tx) + }) + } + pub fn get_pos_gen_block_data( &self, account_index: U31, diff --git a/wallet/wallet-cli-lib/src/commands/mod.rs b/wallet/wallet-cli-lib/src/commands/mod.rs index 70596af0a0..f7992844f9 100644 --- a/wallet/wallet-cli-lib/src/commands/mod.rs +++ b/wallet/wallet-cli-lib/src/commands/mod.rs @@ -198,6 +198,46 @@ pub enum ColdWalletCommand { transaction: String, }, + /// Signs a challenge with a private key corresponding to the provided address destination. + #[clap(name = "account-sign-challenge-hex")] + SignChallegeHex { + /// Hex encoded message to be signed + message: String, + /// Address with whose private key to sign the challenge + address: String, + }, + + /// Signs a challenge with a private key corresponding to the provided address destination. + #[clap(name = "account-sign-challenge-plain")] + SignChallege { + /// The message to be signed + message: String, + /// Address with whose private key to sign the challenge + address: String, + }, + + /// Verifies a signed challenge against an address destination + #[clap(name = "verify-challenge-hex")] + VerifyChallengeHex { + /// The hex encoded message that was signed + message: String, + /// Hex encoded signed challenge + signed_challenge: String, + /// Address with whose private key the challenge was signed with + address: String, + }, + + /// Verifies a signed challenge against an address destination + #[clap(name = "verify-challenge-plain")] + VerifyChallenge { + /// The message that was signed + message: String, + /// Hex encoded signed challenge + signed_challenge: String, + /// Address with whose private key the challenge was signed with + address: String, + }, + /// Print command history in the wallet for this execution #[clap(name = "history-print")] PrintHistory, @@ -1094,6 +1134,77 @@ where Ok(ConsoleCommand::Print(output_str)) } + ColdWalletCommand::SignChallegeHex { + message: challenge, + address, + } => { + let selected_account = self.get_selected_acc()?; + let challenge = hex::decode(challenge) + .map_err(|err| WalletCliError::InvalidInput(err.to_string()))?; + let result = + self.wallet_rpc.sign_challenge(selected_account, challenge, address).await?; + + Ok(ConsoleCommand::Print(format!( + "The generated hex encoded signature is\n{}", + result.to_hex() + ))) + } + + ColdWalletCommand::SignChallege { + message: challenge, + address, + } => { + let selected_account = self.get_selected_acc()?; + let result = self + .wallet_rpc + .sign_challenge(selected_account, challenge.into_bytes(), address) + .await?; + + Ok(ConsoleCommand::Print(format!( + "The generated hex encoded signature is\n{}", + result.to_hex() + ))) + } + + ColdWalletCommand::VerifyChallengeHex { + message, + signed_challenge, + address, + } => { + let message = hex::decode(message).map_err(|e| { + WalletCliError::InvalidInput(format!("invalid hex data: {}", e)) + })?; + let signed_challenge = hex::decode(signed_challenge).map_err(|e| { + WalletCliError::InvalidInput(format!("invalid hex data: {}", e)) + })?; + + self.wallet_rpc.verify_challenge(message, signed_challenge, address)?; + + Ok(ConsoleCommand::Print( + "The provided signature is correct".to_string(), + )) + } + + ColdWalletCommand::VerifyChallenge { + message, + signed_challenge, + address, + } => { + let signed_challenge = hex::decode(signed_challenge).map_err(|e| { + WalletCliError::InvalidInput(format!("invalid hex data: {}", e)) + })?; + + self.wallet_rpc.verify_challenge( + message.into_bytes(), + signed_challenge, + address, + )?; + + Ok(ConsoleCommand::Print( + "The provided signature is correct".to_string(), + )) + } + ColdWalletCommand::Version => Ok(ConsoleCommand::Print(get_version())), ColdWalletCommand::Exit => { if let Some(rpc) = self.server_rpc.take() { diff --git a/wallet/wallet-controller/src/synced_controller.rs b/wallet/wallet-controller/src/synced_controller.rs index 5ee4014bb0..09d62979f0 100644 --- a/wallet/wallet-controller/src/synced_controller.rs +++ b/wallet/wallet-controller/src/synced_controller.rs @@ -18,6 +18,7 @@ use std::collections::BTreeSet; use common::{ address::Address, chain::{ + signature::inputsig::arbitrary_message::SignedArbitraryMessage, tokens::{ IsTokenFreezable, IsTokenUnfreezable, Metadata, RPCTokenInfo, TokenId, TokenIssuance, TokenIssuanceV1, TokenTotalSupply, @@ -640,6 +641,16 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { .map_err(ControllerError::WalletError) } + pub fn sign_challenge( + &mut self, + challenge: Vec, + destination: Destination, + ) -> Result> { + self.wallet + .sign_challenge(self.account_index, challenge, destination) + .map_err(ControllerError::WalletError) + } + async fn get_current_and_consolidation_fee_rate( &mut self, ) -> Result<(mempool::FeeRate, mempool::FeeRate), ControllerError> { diff --git a/wallet/wallet-rpc-lib/src/rpc/mod.rs b/wallet/wallet-rpc-lib/src/rpc/mod.rs index 94ea1f169c..0e98926773 100644 --- a/wallet/wallet-rpc-lib/src/rpc/mod.rs +++ b/wallet/wallet-rpc-lib/src/rpc/mod.rs @@ -36,6 +36,9 @@ use wallet::{ use common::{ address::Address, chain::{ + signature::inputsig::arbitrary_message::{ + produce_message_challenge, SignedArbitraryMessage, + }, tokens::{IsTokenFreezable, IsTokenUnfreezable, Metadata, TokenTotalSupply}, Block, ChainConfig, DelegationId, Destination, GenBlock, PoolId, SignedTransaction, Transaction, TxOutput, UtxoOutPoint, @@ -401,6 +404,47 @@ impl WalletRpc { .await? } + pub async fn sign_challenge( + &self, + account_index: U31, + challenge: Vec, + address: String, + ) -> WRpcResult { + let config = ControllerConfig { in_top_x_mb: 5 }; // irrelevant + let destination = Address::from_str(&self.chain_config, &address) + .and_then(|addr| addr.decode_object(&self.chain_config)) + .map_err(|_| RpcError::InvalidAddress)?; + + self.wallet + .call_async(move |controller| { + Box::pin(async move { + controller + .synced_controller(account_index, config) + .await? + .sign_challenge(challenge, destination) + .map_err(RpcError::Controller) + }) + }) + .await? + } + + pub fn verify_challenge( + &self, + message: Vec, + signed_challenge: Vec, + address: String, + ) -> WRpcResult<(), N> { + let destination = Address::from_str(&self.chain_config, &address) + .and_then(|addr| addr.decode_object(&self.chain_config)) + .map_err(|_| RpcError::InvalidAddress)?; + + let message_challenge = produce_message_challenge(&message); + let sig = SignedArbitraryMessage::from_data(signed_challenge); + sig.verify_signature(&self.chain_config, &destination, &message_challenge)?; + + Ok(()) + } + pub async fn send_coins( &self, account_index: U31, diff --git a/wallet/wallet-rpc-lib/src/rpc/types.rs b/wallet/wallet-rpc-lib/src/rpc/types.rs index dbf8d9831f..0a7e920adb 100644 --- a/wallet/wallet-rpc-lib/src/rpc/types.rs +++ b/wallet/wallet-rpc-lib/src/rpc/types.rs @@ -19,6 +19,7 @@ use common::{ address::Address, chain::{ block::timestamp::BlockTimestamp, + signature::DestinationSigError, tokens::{self, IsTokenFreezable, Metadata, TokenCreator}, ChainConfig, DelegationId, Destination, PoolId, Transaction, TxOutput, UtxoOutPoint, }, @@ -88,6 +89,9 @@ pub enum RpcError { #[error("Invalid hex encoded partially signed transaction")] InvalidPartialTransaction, + + #[error("{0}")] + DestinationSigError(#[from] DestinationSigError), } impl From> for rpc::Error {