From 240afb353486935db4e0b8651fae2e408738bcfa Mon Sep 17 00:00:00 2001 From: bayk Date: Mon, 30 Sep 2024 15:15:24 -0700 Subject: [PATCH] Ownership proof --- api/src/owner.rs | 50 ++++- controller/src/command.rs | 78 +++++++- controller/tests/ownership_proofs.rs | 237 +++++++++++++++++++++++ libwallet/src/api_impl/owner.rs | 279 ++++++++++++++++++++++++++- libwallet/src/api_impl/types.rs | 54 ++++++ libwallet/src/error.rs | 4 + libwallet/src/lib.rs | 6 +- libwallet/src/proof/crypto.rs | 34 ++++ libwallet/src/slate.rs | 2 +- src/bin/mwc-wallet.yml | 28 +++ src/cmd/wallet_args.rs | 19 ++ 11 files changed, 782 insertions(+), 9 deletions(-) create mode 100644 controller/tests/ownership_proofs.rs diff --git a/api/src/owner.rs b/api/src/owner.rs index 23ce91e2a..0c70ceeff 100644 --- a/api/src/owner.rs +++ b/api/src/owner.rs @@ -43,7 +43,7 @@ use crate::util::secp::key::SecretKey; use crate::util::{from_hex, Mutex, ZeroingString}; use grin_wallet_util::grin_util::secp::key::PublicKey; use grin_wallet_util::grin_util::static_secp_instance; -use libwallet::RetrieveTxQueryArgs; +use libwallet::{OwnershipProof, OwnershipProofValidation, RetrieveTxQueryArgs}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{channel, Sender}; use std::sync::Arc; @@ -2435,6 +2435,31 @@ where owner::verify_payment_proof(self.wallet_inst.clone(), keychain_mask, proof) } + pub fn retrieve_ownership_proof( + &self, + keychain_mask: Option<&SecretKey>, + message: String, + include_public_root_key: bool, + include_tor_address: bool, + include_mqs_address: bool, + ) -> Result { + owner::generate_ownership_proof( + self.wallet_inst.clone(), + keychain_mask, + message, + include_public_root_key, + include_tor_address, + include_mqs_address, + ) + } + + pub fn validate_ownership_proof( + &self, + proof: OwnershipProof, + ) -> Result { + owner::validate_ownership_proof(proof) + } + /// Start swap trade process. Return SwapID that can be used to check the status or perform further action. pub fn swap_start( &self, @@ -2796,3 +2821,26 @@ macro_rules! doctest_helper_setup_doc_env { let mut $wallet = Arc::new(Mutex::new(wallet)); }; } + +/*#[test] +#[allow(unused_mut)] +fn test_api() { + use crate as grin_wallet_api; + global::set_local_chain_type(global::ChainTypes::AutomatedTesting); + grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + let mut api_owner = Owner::new(wallet.clone(), None, None); + + let proof = api_owner.retrieve_ownership_proof(None, + "my message to sign".to_string(), + true, + true, + true).unwrap(); + + let valiation = api_owner.validate_ownership_proof(proof).unwrap(); + + assert_eq!(valiation.network, "floonet"); + assert_eq!(valiation.message, "my message to sign".to_string()); + assert!(valiation.viewing_key.is_some()); + assert!(valiation.tor_address.is_some()); + assert!(valiation.mqs_address.is_some()); +}*/ diff --git a/controller/src/command.rs b/controller/src/command.rs index 4e711aca4..946f73ff8 100644 --- a/controller/src/command.rs +++ b/controller/src/command.rs @@ -48,7 +48,7 @@ use grin_wallet_libwallet::swap::fsm::state::StateId; use grin_wallet_libwallet::swap::trades; use grin_wallet_libwallet::swap::types::Action; use grin_wallet_libwallet::swap::{message, Swap}; -use grin_wallet_libwallet::{Slate, StatusMessage, TxLogEntry, WalletInst}; +use grin_wallet_libwallet::{OwnershipProof, Slate, StatusMessage, TxLogEntry, WalletInst}; use grin_wallet_util::grin_core::consensus::MWC_BASE; use grin_wallet_util::grin_core::core::{amount_to_hr_string, Transaction}; use grin_wallet_util::grin_core::global::{FLOONET_DNS_SEEDS, MAINNET_DNS_SEEDS}; @@ -178,6 +178,82 @@ where Ok(()) } +/// Arguments for generate_ownership_proof command +pub struct GenerateOwnershipProofArgs { + /// Message to sign + pub message: String, + /// does need to include public root key + pub include_public_root_key: bool, + /// does need to include tor address + pub include_tor_address: bool, + /// does need to include MQS address + pub include_mqs_address: bool, +} + +pub fn generate_ownership_proof<'a, L, C, K>( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + args: GenerateOwnershipProofArgs, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K>, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + let proof = api.retrieve_ownership_proof( + m, + args.message, + args.include_public_root_key, + args.include_tor_address, + args.include_mqs_address, + )?; + + let proof_json = serde_json::to_string(&proof).map_err(|e| { + Error::GenericError(format!("Failed convert proof result into json, {}", e)) + })?; + + println!("Ownership Proof: {}", proof_json); + Ok(()) + })?; + Ok(()) +} + +pub fn validate_ownership_proof<'a, L, C, K>( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, + proof: &str, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K>, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, _m| { + let proof = serde_json::from_str::(proof).map_err(|e| { + Error::ArgumentError(format!("Unable to decode proof from json, {}", e)) + })?; + + let validation = api.validate_ownership_proof(proof)?; + println!("Network: {}", validation.network); + println!("Message: {}", validation.message); + println!( + "Viewing Key: {}", + validation.viewing_key.unwrap_or("Not provided".to_string()) + ); + println!( + "Tor Address: {}", + validation.tor_address.unwrap_or("Not provided".to_string()) + ); + println!( + "MWCMQS Address: {}", + validation.mqs_address.unwrap_or("Not provided".to_string()) + ); + Ok(()) + })?; + Ok(()) +} + /// Arguments for rewind hash view wallet scan command pub struct ViewWalletScanArgs { pub rewind_hash: String, diff --git a/controller/tests/ownership_proofs.rs b/controller/tests/ownership_proofs.rs new file mode 100644 index 000000000..b5faeb52e --- /dev/null +++ b/controller/tests/ownership_proofs.rs @@ -0,0 +1,237 @@ +// Copyright 2024 The Grin Developers +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test a wallet sending to self +#[macro_use] +extern crate log; +extern crate grin_wallet_controller as wallet; +extern crate grin_wallet_impls as impls; + +use grin_wallet_util::grin_core::global; + +use impls::test_framework::{self, LocalWalletClient}; +use std::sync::atomic::Ordering; +use std::thread; +use std::time::Duration; + +#[macro_use] +mod common; +use common::{clean_output_dir, create_wallet_proxy, setup}; +use grin_wallet_libwallet::PubKeySignature; +use grin_wallet_util::grin_util::ZeroingString; + +/// self send impl +fn ownership_proof_impl(test_dir: &'static str) -> Result<(), wallet::Error> { + // Create a new proxy to simulate server and wallet responses + global::set_local_chain_type(global::ChainTypes::AutomatedTesting); + let mut wallet_proxy = create_wallet_proxy(test_dir); + let chain = wallet_proxy.chain.clone(); + let stopper = wallet_proxy.running.clone(); + + // Create a new wallet test client, and set its queues to communicate with the + // proxy + create_wallet_and_add!( + client1, + wallet1, + mask1_i, + test_dir, + "wallet1", + Some(ZeroingString::from( + "room plastic there over junior comfort drip envelope hope divide cake trophy" + )), + &mut wallet_proxy, + true + ); + let mask1 = (&mask1_i).as_ref(); + + // Set the wallet proxy listener running + thread::spawn(move || { + global::set_local_chain_type(global::ChainTypes::AutomatedTesting); + if let Err(e) = wallet_proxy.run() { + error!("Wallet Proxy error: {}", e); + } + }); + + let _ = + test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 10 as usize, false); + + wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(m, true, 1)?; + assert!(wallet1_refreshed); + assert!(wallet1_info.last_confirmed_height > 0); + assert!(wallet1_info.total > 0); + + let proof = + api.retrieve_ownership_proof(m, "my message to sign".to_string(), true, true, true)?; + + assert_eq!(proof.message, "my message to sign"); + assert_eq!(proof.network, "floonet"); + assert!(proof.wallet_root.is_some()); + assert!(proof.tor_address.is_some()); + assert!(proof.mqs_address.is_some()); + + assert_eq!(format!("{:?}", proof), "OwnershipProof { network: \"floonet\", message: \"my message to sign\", wallet_root: Some(PubKeySignature { public_key: \"022e4a08245fc03ca5da9c717c1b29c589413fac96150a400500eff613d05dd34d\", signature: \"3045022100e12d078b67446cc83ab46918b5a641a61b4a02bcdfac03b8978865ca6740def302202f463ec8dea18921942e20452d15ddd2fbcf590aa36b0c414d45078a83d38b92\" }), tor_address: Some(PubKeySignature { public_key: \"fa30d0726b505d6304d93a4ed6c4a428d467b1da13a7826cfdd4131046ca27a4\", signature: \"1cb237ca781f0868752dcc0e503eb70904fd208c7b64c3d23ed6830c808663cae723ac909709f1c16495b2162ec9028a225f7074863172246930eaaee3853706\" }), mqs_address: Some(PubKeySignature { public_key: \"0214f727d0503231f7dd4797509b3e7cde3f7cfdf8c32a3b562c8bd3f650bb2509\", signature: \"30440220035e56caa572ade99ebbffdc2d166c943fe433418269cb156d21f4a0e02557520220628e9e530e662ca1632774106f37a1404873f6bc482bc603a8cfee6c3e92f2f6\" }) }"); + + let validate_res = api.validate_ownership_proof(proof.clone()); + assert!(validate_res.is_ok()); + let validate_res = validate_res.unwrap(); + + assert_eq!(proof.message, validate_res.message); + assert_eq!(proof.network, validate_res.network); + assert_eq!(format!("{:?}", validate_res), "OwnershipProofValidation { network: \"floonet\", message: \"my message to sign\", viewing_key: Some(\"60a98a5d7d1823743b9c3993a31bec49fc7114d3cdbe6bf9e81f53a7f7e02727\"), tor_address: Some(\"7iyna4tlkbowgbgzhjhnnrfefdkgpmo2cotye3h52qjrarwke6saswid\"), mqs_address: Some(\"xmfmGjJU6hLUtndfwDSQmxtYckgDQEquMwXxxCW8D2zUN1uvdrkN\") }"); + + // Now let's try to adjust something and validate that it will fail + let mut invalid_proof = proof.clone(); + invalid_proof.network = "mainnet".to_string(); + let validate_res = api.validate_ownership_proof(invalid_proof); + assert!(validate_res.is_err()); + + let mut invalid_proof = proof.clone(); + invalid_proof.message = "another message".to_string(); + let validate_res = api.validate_ownership_proof(invalid_proof); + assert!(validate_res.is_err()); + + let mut invalid_proof = proof.clone(); + invalid_proof.tor_address = None; + let validate_res = api.validate_ownership_proof(invalid_proof); + assert!(validate_res.is_err()); + + let mut invalid_proof = proof.clone(); + invalid_proof.wallet_root = None; + let validate_res = api.validate_ownership_proof(invalid_proof); + assert!(validate_res.is_err()); + + let mut invalid_proof = proof.clone(); + invalid_proof.mqs_address = None; + let validate_res = api.validate_ownership_proof(invalid_proof); + assert!(validate_res.is_err()); + + let mut invalid_proof = proof.clone(); + invalid_proof.wallet_root = Some(PubKeySignature{ + public_key: "022e4a08245fc03ca5da9c717c1b29c589413fac96150a400500eff613d15dd34d".to_string(), // PK is changed + signature: "3045022100e12d078b67446cc83ab46918b5a641a61b4a02bcdfac03b8978865ca6740def302202f463ec8dea18921942e20452d15ddd2fbcf590aa36b0c414d45078a83d38b92".to_string(), + }); + let validate_res = api.validate_ownership_proof(invalid_proof); + assert!(validate_res.is_err()); + + let mut invalid_proof = proof.clone(); + invalid_proof.wallet_root = Some(PubKeySignature{ + public_key: "022e4a08245fc03ca5da9c717c1b29c589413fac96150a400500eff613d05dd34d".to_string(), + signature: "3045022100e12d078b67446cc83ab46918b5a641a61b4a02bcdfac03b8978865ca6740def302202f453ec8dea18921942e20452d15ddd2fbcf590aa36b0c414d45078a83d38b92".to_string(), // signature is changed + }); + let validate_res = api.validate_ownership_proof(invalid_proof); + assert!(validate_res.is_err()); + + // Now let try not full proofs + let proof = + api.retrieve_ownership_proof(m, "my message to sign".to_string(), true, false, false)?; + + assert_eq!(proof.message, "my message to sign"); + assert_eq!(proof.network, "floonet"); + assert!(proof.wallet_root.is_some()); + assert!(proof.tor_address.is_none()); + assert!(proof.mqs_address.is_none()); + + let validate_res = api.validate_ownership_proof(proof).unwrap(); + assert_eq!("my message to sign", validate_res.message); + assert_eq!("floonet", validate_res.network); + assert!(validate_res.viewing_key.is_some()); + assert!(validate_res.tor_address.is_none()); + assert!(validate_res.mqs_address.is_none()); + + // Now let try not full proofs + let proof = + api.retrieve_ownership_proof(m, "my message to sign".to_string(), false, true, false)?; + + assert_eq!(proof.message, "my message to sign"); + assert_eq!(proof.network, "floonet"); + assert!(proof.wallet_root.is_none()); + assert!(proof.tor_address.is_some()); + assert!(proof.mqs_address.is_none()); + + let validate_res = api.validate_ownership_proof(proof).unwrap(); + assert_eq!("my message to sign", validate_res.message); + assert_eq!("floonet", validate_res.network); + assert!(validate_res.viewing_key.is_none()); + assert!(validate_res.tor_address.is_some()); + assert!(validate_res.mqs_address.is_none()); + + // Now let try not full proofs + let proof = + api.retrieve_ownership_proof(m, "my message to sign".to_string(), false, false, true)?; + + assert_eq!(proof.message, "my message to sign"); + assert_eq!(proof.network, "floonet"); + assert!(proof.wallet_root.is_none()); + assert!(proof.tor_address.is_none()); + assert!(proof.mqs_address.is_some()); + + let validate_res = api.validate_ownership_proof(proof).unwrap(); + assert_eq!("my message to sign", validate_res.message); + assert_eq!("floonet", validate_res.network); + assert!(validate_res.viewing_key.is_none()); + assert!(validate_res.tor_address.is_none()); + assert!(validate_res.mqs_address.is_some()); + + // Now let try not full proofs + let proof = + api.retrieve_ownership_proof(m, "my message to sign".to_string(), true, false, true)?; + + assert_eq!(proof.message, "my message to sign"); + assert_eq!(proof.network, "floonet"); + assert!(proof.wallet_root.is_some()); + assert!(proof.tor_address.is_none()); + assert!(proof.mqs_address.is_some()); + + let validate_res = api.validate_ownership_proof(proof).unwrap(); + assert_eq!("my message to sign", validate_res.message); + assert_eq!("floonet", validate_res.network); + assert!(validate_res.viewing_key.is_some()); + assert!(validate_res.tor_address.is_none()); + assert!(validate_res.mqs_address.is_some()); + + // Now let try not full proofs + let proof = + api.retrieve_ownership_proof(m, "my message to sign".to_string(), true, true, false)?; + + assert_eq!(proof.message, "my message to sign"); + assert_eq!(proof.network, "floonet"); + assert!(proof.wallet_root.is_some()); + assert!(proof.tor_address.is_some()); + assert!(proof.mqs_address.is_none()); + + let validate_res = api.validate_ownership_proof(proof).unwrap(); + assert_eq!("my message to sign", validate_res.message); + assert_eq!("floonet", validate_res.network); + assert!(validate_res.viewing_key.is_some()); + assert!(validate_res.tor_address.is_some()); + assert!(validate_res.mqs_address.is_none()); + + Ok(()) + })?; + + // let logging finish + stopper.store(false, Ordering::Relaxed); + thread::sleep(Duration::from_millis(1000)); + Ok(()) +} + +#[test] +fn wallet_ownership_proof() { + let test_dir = "test_output/ownership_proof"; + setup(test_dir); + if let Err(e) = ownership_proof_impl(test_dir) { + panic!("ownership_proof_impl Error: {}", e); + } + clean_output_dir(test_dir); +} diff --git a/libwallet/src/api_impl/owner.rs b/libwallet/src/api_impl/owner.rs index 11c97c60e..e0ae4f5a3 100644 --- a/libwallet/src/api_impl/owner.rs +++ b/libwallet/src/api_impl/owner.rs @@ -22,11 +22,14 @@ use crate::grin_core::libtx::proof; use crate::grin_keychain::ViewKey; use crate::grin_util::secp::key::SecretKey; use crate::grin_util::Mutex; -use crate::grin_util::ToHex; +use crate::proof::crypto::Hex; use crate::api_impl::owner_updater::StatusMessage; use crate::grin_keychain::{BlindingFactor, Identifier, Keychain, SwitchCommitmentType}; use crate::grin_util::secp::key::PublicKey; +use crate::grin_util::secp::Message; +use crate::grin_util::secp::Secp256k1; +use crate::grin_util::secp::Signature; use crate::internal::{keys, scan, selection, tx, updater}; use crate::slate::{PaymentInfo, Slate}; @@ -37,12 +40,20 @@ use crate::types::{ use crate::Error; use crate::{ wallet_lock, BuiltOutput, InitTxArgs, IssueInvoiceTxArgs, NodeHeightResult, - OutputCommitMapping, PaymentProof, RetrieveTxQueryArgs, ScannedBlockInfo, TxLogEntryType, - ViewWallet, WalletInst, WalletLCProvider, + OutputCommitMapping, OwnershipProof, OwnershipProofValidation, PaymentProof, PubKeySignature, + RetrieveTxQueryArgs, ScannedBlockInfo, TxLogEntryType, ViewWallet, WalletInst, + WalletLCProvider, }; use crate::proof::tx_proof::{pop_proof_for_slate, TxProof}; +use digest::Digest; +use ed25519_dalek::Keypair as DalekKeypair; use ed25519_dalek::PublicKey as DalekPublicKey; +use ed25519_dalek::SecretKey as DalekSecretKey; +use ed25519_dalek::Signature as DalekSignature; +use ed25519_dalek::Signer; +use sha2::Sha256; +use signature::Verifier; use std::cmp; use std::fs::File; use std::io::Write; @@ -52,7 +63,10 @@ use std::sync::Arc; const USER_MESSAGE_MAX_LEN: usize = 1000; // We can keep messages as long as we need unless the slate will be too large to operate. 1000 symbols should be enough to keep everybody happy use crate::proof::crypto; use crate::proof::proofaddress; +use crate::proof::proofaddress::ProvableAddress; use grin_wallet_util::grin_core::core::Committed; +use grin_wallet_util::grin_core::global; +use grin_wallet_util::grin_util::from_hex; /// List of accounts pub fn accounts<'a, T: ?Sized, C, K>(w: &mut T) -> Result, Error> @@ -98,6 +112,8 @@ where C: NodeClient + 'a, K: Keychain + 'a, { + use grin_wallet_util::grin_util::ToHex; + wallet_lock!(wallet_inst, w); let keychain = w.keychain(keychain_mask)?; let root_public_key = keychain.public_root_key(); @@ -1542,6 +1558,263 @@ where Ok((sender_mine, recipient_mine)) } +/// Generate signatures for root public keym tor address PK and MQS PK. +pub fn generate_ownership_proof<'a, L, C, K>( + wallet_inst: Arc>>>, + keychain_mask: Option<&SecretKey>, + message: String, + include_public_root_key: bool, + include_tor_address: bool, + include_mqs_address: bool, +) -> Result +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + if message.is_empty() { + return Err(Error::GenericError( + "Not defines message to sign".to_string(), + )); + } + + if !include_public_root_key && !include_tor_address && !include_mqs_address { + return Err(Error::GenericError( + "No keys are selected to include into the ownership proof".to_string(), + )); + } + + let network = if global::is_mainnet() { + "mainnet" + } else { + "floonet" + }; + let mut message2sign = String::new(); + message2sign.push_str(network); + message2sign.push('|'); + message2sign.push_str(message.as_str()); + + wallet_lock!(wallet_inst, w); + let keychain = w.keychain(keychain_mask)?; + let secp = keychain.secp(); + + if include_public_root_key { + let root_public_key = keychain.public_root_key(); + let root_public_key = root_public_key.to_hex(); + message2sign.push('|'); + message2sign.push_str(root_public_key.as_str()); + } + + if include_tor_address { + let secret = proofaddress::payment_proof_address_secret(&keychain, None)?; + let tor_pk = proofaddress::secret_2_tor_pub(&secret)?; + let tor_pk = tor_pk.to_hex(); + message2sign.push('|'); + message2sign.push_str(tor_pk.as_str()); + } + + if include_mqs_address { + let mqs_pub_key: PublicKey = proofaddress::payment_proof_address_pubkey(&keychain)?; + let mqs_pub_key = mqs_pub_key.to_hex(); + message2sign.push('|'); + message2sign.push_str(mqs_pub_key.as_str()); + } + + // message to sign is ready. Now we can go forward and generate signatures for all public keys + let mut hasher = Sha256::new(); + hasher.update(message2sign.as_bytes()); + let message_hash = hasher.finalize(); + + // generating the signatures for message + let wallet_root = if include_public_root_key { + let secret = keychain.private_root_key(); + let signature = secp + .sign( + &Message::from_slice(message_hash.as_slice()).map_err(|e| { + Error::GenericError(format!("Unable to build a message, {}", e)) + })?, + &secret, + ) + .map_err(|e| Error::from(e))?; + Some(PubKeySignature { + public_key: keychain.public_root_key().to_hex(), + signature: signature.to_hex(), + }) + } else { + None + }; + + let tor_address = if include_tor_address { + let secret = proofaddress::payment_proof_address_secret(&keychain, None)?; + let secret = DalekSecretKey::from_bytes(&secret.0) + .map_err(|e| Error::GenericError(format!("Unable build dalek public key, {}", e)))?; + let public = DalekPublicKey::from(&secret); + let keypair = DalekKeypair { secret, public }; + let signature = keypair + .try_sign(message_hash.as_slice()) + .map_err(|e| Error::GenericError(format!("Unable build dalek signature, {}", e)))?; + Some(PubKeySignature { + public_key: public.to_hex(), + signature: signature.to_hex(), + }) + } else { + None + }; + + let mqs_address = if include_mqs_address { + let secret = proofaddress::payment_proof_address_secret(&keychain, None)?; + let signature = secp + .sign( + &Message::from_slice(message_hash.as_slice()).map_err(|e| { + Error::GenericError(format!("Unable to build a message, {}", e)) + })?, + &secret, + ) + .map_err(|e| Error::from(e))?; + let mqs_pub_key = PublicKey::from_secret_key(&secp, &secret)?; + Some(PubKeySignature { + public_key: mqs_pub_key.to_hex(), + signature: signature.to_hex(), + }) + } else { + None + }; + + Ok(OwnershipProof { + network: network.to_string(), + message, + wallet_root, + tor_address, + mqs_address, + }) +} + +/// Generate signatures for root public keym tor address PK and MQS PK. +pub fn validate_ownership_proof(proof: OwnershipProof) -> Result +where +{ + if proof.message.is_empty() { + return Err(Error::InvalidOwnershipProof( + "message value is empty".to_string(), + )); + } + + let mut result = OwnershipProofValidation::empty(proof.message.clone()); + + let network = if global::is_mainnet() { + "mainnet" + } else { + "floonet" + }; + + if proof.network != network { + return Err(Error::InvalidOwnershipProof(format!( + "This proof is generated for wrong network: {}", + proof.network + ))); + } + + result.network = network.to_string(); + + let mut message2sign = String::new(); + message2sign.push_str(network); + message2sign.push('|'); + message2sign.push_str(proof.message.as_str()); + + let secp = Secp256k1::new(); + + if let Some(wallet_root) = &proof.wallet_root { + message2sign.push('|'); + message2sign.push_str(wallet_root.public_key.as_str()); + } + if let Some(tor_address) = &proof.tor_address { + message2sign.push('|'); + message2sign.push_str(tor_address.public_key.as_str()); + } + if let Some(mqs_address) = &proof.mqs_address { + message2sign.push('|'); + message2sign.push_str(mqs_address.public_key.as_str()); + } + + // message to sign is ready. Now we can go forward and generate signatures for all public keys + let mut hasher = Sha256::new(); + hasher.update(message2sign.as_bytes()); + let message_hash = hasher.finalize(); + + if let Some(wallet_root) = &proof.wallet_root { + let public_key = PublicKey::from_hex(&wallet_root.public_key).map_err(|e| { + Error::InvalidOwnershipProof(format!("Unable to decode wallet root public key, {}", e)) + })?; + let signature = Signature::from_hex(&wallet_root.signature).map_err(|e| { + Error::InvalidOwnershipProof(format!("Unable to decode wallet root signature, {}", e)) + })?; + secp.verify( + &Message::from_slice(message_hash.as_slice()) + .map_err(|e| Error::GenericError(format!("Unable to build a message, {}", e)))?, + &signature, + &public_key, + ) + .map_err(|e| { + Error::InvalidOwnershipProof(format!("wallet root signature is invalid, {}", e)) + })?; + + use grin_wallet_util::grin_util::ToHex; + // we are good so far, reporting viewing key + result.viewing_key = Some(ViewKey::rewind_hash(&secp, public_key).to_hex()); + } + + if let Some(mqs_address) = &proof.mqs_address { + let public_key = PublicKey::from_hex(&mqs_address.public_key).map_err(|e| { + Error::InvalidOwnershipProof(format!("Unable to decode mqs address public key, {}", e)) + })?; + let signature = Signature::from_hex(&mqs_address.signature).map_err(|e| { + Error::InvalidOwnershipProof(format!("Unable to decode mqs address signature, {}", e)) + })?; + secp.verify( + &Message::from_slice(message_hash.as_slice()) + .map_err(|e| Error::GenericError(format!("Unable to build a message, {}", e)))?, + &signature, + &public_key, + ) + .map_err(|e| { + Error::InvalidOwnershipProof(format!("mqs address signature is invalid, {}", e)) + })?; + + // we are good so far, reporting mwqs address + let mqs_address = ProvableAddress::from_pub_key(&public_key); + result.mqs_address = Some(mqs_address.public_key); + } + + if let Some(tor_address) = &proof.tor_address { + let public_key = from_hex(&tor_address.public_key).map_err(|e| { + Error::InvalidOwnershipProof(format!("Unable to decode tor address public key, {}", e)) + })?; + + let public_key = DalekPublicKey::from_bytes(&public_key).map_err(|e| { + Error::InvalidOwnershipProof(format!("Unable to decode tor address public key, {}", e)) + })?; + + let signature = from_hex(&tor_address.signature).map_err(|e| { + Error::InvalidOwnershipProof(format!("Unable to decode tor address signature, {}", e)) + })?; + let signature = DalekSignature::from_bytes(&signature).map_err(|e| { + Error::InvalidOwnershipProof(format!("Unable to decode tor address signature, {}", e)) + })?; + + public_key + .verify(message_hash.as_slice(), &signature) + .map_err(|e| { + Error::InvalidOwnershipProof(format!("tor address signature is invalid, {}", e)) + })?; + + // we are good so far, reporting tor address + let tor_address = ProvableAddress::from_tor_pub_key(&public_key); + result.tor_address = Some(tor_address.public_key); + } + + return Ok(result); +} + /// pub fn self_spend_particular_output<'a, L, C, K>( wallet_inst: Arc>>>, diff --git a/libwallet/src/api_impl/types.rs b/libwallet/src/api_impl/types.rs index 06440fc2c..08be98913 100644 --- a/libwallet/src/api_impl/types.rs +++ b/libwallet/src/api_impl/types.rs @@ -469,6 +469,60 @@ pub struct PaymentProof { pub sender_sig: String, } +/// Ownership proof for addresses & root public keys +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct PubKeySignature { + /// Public key + pub public_key: String, + /// Signature + pub signature: String, +} + +/// Ownership proof for addresses & root public keys +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct OwnershipProof { + /// Name of the network + pub network: String, + /// Message to sign + pub message: String, + + /// Proof for the root public key (viewing key) + pub wallet_root: Option, + /// Proof for the tor address + pub tor_address: Option, + /// Proof for MQS address + pub mqs_address: Option, +} + +/// Ownership validation results +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct OwnershipProofValidation { + /// Network name + pub network: String, + /// Message that was signed + pub message: String, + + /// Viewing key + pub viewing_key: Option, + /// Tor address + pub tor_address: Option, + /// MQS address + pub mqs_address: Option, +} + +impl OwnershipProofValidation { + /// Build empty instance + pub fn empty(message: String) -> Self { + OwnershipProofValidation { + network: String::new(), + message, + viewing_key: None, + tor_address: None, + mqs_address: None, + } + } +} + /// Init swap operation #[derive(Serialize, Deserialize, Debug, Clone)] pub struct SwapStartArgs { diff --git a/libwallet/src/error.rs b/libwallet/src/error.rs index f41a5173c..589527454 100644 --- a/libwallet/src/error.rs +++ b/libwallet/src/error.rs @@ -357,6 +357,10 @@ pub enum Error { /// Nonce creation error #[error("Nonce error: {0}")] Nonce(String), + + /// Invalid ownership proof + #[error("Invalid ownership proof: {0}")] + InvalidOwnershipProof(String), } impl From for Error { diff --git a/libwallet/src/lib.rs b/libwallet/src/lib.rs index 13031b2cd..c15bc63d2 100644 --- a/libwallet/src/lib.rs +++ b/libwallet/src/lib.rs @@ -83,9 +83,9 @@ pub use api_impl::owner_swap; pub use api_impl::owner_updater::StatusMessage; pub use api_impl::types::{ Amount, BlockFees, BuiltOutput, InitTxArgs, InitTxSendArgs, IssueInvoiceTxArgs, - NodeHeightResult, OutputCommitMapping, PaymentProof, ReplayMitigationConfig, - RetrieveTxQueryArgs, RetrieveTxQuerySortField, RetrieveTxQuerySortOrder, SendTXArgs, - SwapStartArgs, VersionInfo, + NodeHeightResult, OutputCommitMapping, OwnershipProof, OwnershipProofValidation, PaymentProof, + PubKeySignature, ReplayMitigationConfig, RetrieveTxQueryArgs, RetrieveTxQuerySortField, + RetrieveTxQuerySortOrder, SendTXArgs, SwapStartArgs, VersionInfo, }; pub use internal::scan::{scan, set_replay_config}; pub use proof::tx_proof::TxProof; diff --git a/libwallet/src/proof/crypto.rs b/libwallet/src/proof/crypto.rs index e12e35c57..35d7bccb0 100644 --- a/libwallet/src/proof/crypto.rs +++ b/libwallet/src/proof/crypto.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use ed25519_dalek::PublicKey as DalekPublicKey; +use ed25519_dalek::Signature as DalekSignature; use grin_wallet_util::grin_util::secp::key::{PublicKey, SecretKey}; use grin_wallet_util::grin_util::secp::pedersen::Commitment; use grin_wallet_util::grin_util::secp::{ContextFlag, Message, Secp256k1, Signature}; @@ -115,6 +117,38 @@ impl Hex for Signature { } } +impl Hex for DalekPublicKey { + fn from_hex(str: &str) -> Result { + let hex = util::from_hex(str).map_err(|e| { + Error::HexError(format!("Unable convert Public Key HEX {}, {}", str, e)) + })?; + DalekPublicKey::from_bytes(&hex).map_err(|e| { + Error::HexError(format!( + "Unable to build public key from HEX {}, {}", + str, e + )) + }) + } + + fn to_hex(&self) -> String { + util::to_hex(self.as_bytes()) + } +} + +impl Hex for DalekSignature { + fn from_hex(str: &str) -> Result { + let hex = util::from_hex(str) + .map_err(|e| Error::HexError(format!("Unable convert Signature HEX {}, {}", str, e)))?; + DalekSignature::from_bytes(&hex).map_err(|e| { + Error::HexError(format!("Unable to build Signature from HEX {}, {}", str, e)) + }) + } + + fn to_hex(&self) -> String { + util::to_hex(&self.clone().to_bytes()) + } +} + impl Hex for SecretKey { fn from_hex(str: &str) -> Result { let data = util::from_hex(str) diff --git a/libwallet/src/slate.rs b/libwallet/src/slate.rs index 4f003ce21..0b64de4fc 100644 --- a/libwallet/src/slate.rs +++ b/libwallet/src/slate.rs @@ -33,7 +33,6 @@ use crate::grin_util::secp::pedersen::Commitment; use crate::grin_util::secp::Signature; use crate::grin_util::ToHex; use crate::Context; -use bitcoin_lib::secp256k1::ContextFlag; use serde::ser::{Serialize, Serializer}; use serde_json; use std::convert::{TryFrom, TryInto}; @@ -57,6 +56,7 @@ use crate::types::CbData; use crate::{SlateVersion, Slatepacker, CURRENT_SLATE_VERSION}; use ed25519_dalek::SecretKey as DalekSecretKey; use grin_wallet_util::grin_core::core::FeeFields; +use grin_wallet_util::grin_util::secp::ContextFlag; use grin_wallet_util::grin_util::secp::Secp256k1; use rand::rngs::mock::StepRng; use rand::thread_rng; diff --git a/src/bin/mwc-wallet.yml b/src/bin/mwc-wallet.yml index 231eb7f29..0f4e96959 100644 --- a/src/bin/mwc-wallet.yml +++ b/src/bin/mwc-wallet.yml @@ -1021,3 +1021,31 @@ subcommands: takes_value: true - check_tor_connection: about: check this wallet tor connection (for CLI mode) + - generate_ownership_proof: + about: Gerenerate ownershup proof for the root public key, tor address and mqs addresses. + args: + - message: + help: Message to sign + short: s + long: message + takes_value: true + - include_public_root_key: + help: Include root public key and signature. Note, root public key can be user to generate rewind_hash to view the all outputs for your wallet. + short: p + long: include_public_root_key + - include_tor_address: + help: Include tor address and singature. + short: t + long: include_tor_address + - include_mqs_address: + help: Include MWCMQS address and singature. + short: m + long: include_mqs_address + - validate_ownership_proof: + about: Validate ownership proof record + args: + - proof: + help: Proof record + short: p + long: proof + takes_value: true \ No newline at end of file diff --git a/src/cmd/wallet_args.rs b/src/cmd/wallet_args.rs index 9d7a6258c..aa6ccc3db 100644 --- a/src/cmd/wallet_args.rs +++ b/src/cmd/wallet_args.rs @@ -1447,6 +1447,17 @@ pub fn parse_eth_args(args: &ArgMatches) -> Result }) } +pub fn parse_retrieve_ownership_proof( + args: &ArgMatches, +) -> Result { + Ok(command::GenerateOwnershipProofArgs { + message: parse_required(args, "message")?.to_string(), + include_public_root_key: args.is_present("include_public_root_key"), + include_tor_address: args.is_present("include_tor_address"), + include_mqs_address: args.is_present("include_mqs_address"), + }) +} + pub fn wallet_command( wallet_args: &ArgMatches, mut wallet_config: WalletConfig, @@ -1909,6 +1920,14 @@ where let a = arg_parse!(parse_eth_args(&args)); command::eth(owner_api.wallet_inst.clone(), a) } + ("generate_ownership_proof", Some(args)) => { + let a = arg_parse!(parse_retrieve_ownership_proof(&args)); + command::generate_ownership_proof(owner_api, km, a) + } + ("validate_ownership_proof", Some(args)) => { + let proof = arg_parse!(parse_required(args, "proof")); + command::validate_ownership_proof(owner_api, km, proof) + } (cmd, _) => { return Err(Error::ArgumentError(format!( "Unknown wallet command '{}', use 'mwc help wallet' for details",