diff --git a/Cargo.lock b/Cargo.lock index e2a2ea78c3..470d3d999d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3436,7 +3436,7 @@ version = "1.0.1" dependencies = [ "async-std", "async-trait", - "clap 4.5.19", + "clap 4.5.51", "futures-util", "kaspa-consensus-core", "kaspa-core", diff --git a/cli/src/modules/message.rs b/cli/src/modules/message.rs index d38624dc2b..867db4413f 100644 --- a/cli/src/modules/message.rs +++ b/cli/src/modules/message.rs @@ -1,6 +1,6 @@ use kaspa_addresses::Version; use kaspa_bip32::secp256k1::XOnlyPublicKey; -use kaspa_wallet_core::message::SignMessageOptions; +use kaspa_wallet_core::message::{SignMessageOptions, SignatureType}; use kaspa_wallet_core::{ account::{BIP32_ACCOUNT_KIND, KEYPAIR_ACCOUNT_KIND}, message::{sign_message, verify_message, PersonalMessage}, @@ -88,7 +88,7 @@ impl Message { let pm = PersonalMessage(message); let privkey = self.get_address_private_key(&ctx, kaspa_address).await?; - let sign_options = SignMessageOptions { no_aux_rand: false }; + let sign_options = SignMessageOptions { no_aux_rand: false, signature_type: SignatureType::Schnorr }; let sig_result = sign_message(&pm, &privkey, &sign_options); diff --git a/consensus/core/src/sign.rs b/consensus/core/src/sign.rs index 1a87d03f17..f3081befaa 100644 --- a/consensus/core/src/sign.rs +++ b/consensus/core/src/sign.rs @@ -1,6 +1,6 @@ use crate::{ hashing::{ - sighash::{calc_schnorr_signature_hash, SigHashReusedValuesUnsync}, + sighash::{calc_ecdsa_signature_hash, calc_schnorr_signature_hash, SigHashReusedValuesUnsync}, sighash_type::{SigHashType, SIG_HASH_ALL}, }, tx::{SignableTransaction, VerifiableTransaction}, @@ -183,6 +183,39 @@ pub fn verify(tx: &impl VerifiableTransaction) -> Result<(), Error> { Ok(()) } +/// Sign a transaction using ECDSA +#[allow(clippy::result_large_err)] +pub fn sign_with_multiple_ecdsa(mut mutable_tx: SignableTransaction, privkeys: &[[u8; 32]]) -> Signed { + let mut map = BTreeMap::new(); + for privkey in privkeys { + let secret_key = secp256k1::SecretKey::from_slice(privkey).unwrap(); + let public_key = secret_key.public_key(secp256k1::SECP256K1); + let script_pub_key_script = once(0x21).chain(public_key.serialize().into_iter()).chain(once(0xac)).collect_vec(); + map.insert(script_pub_key_script, secret_key); + } + + let reused_values = SigHashReusedValuesUnsync::new(); + let mut additional_signatures_required = false; + for i in 0..mutable_tx.tx.inputs.len() { + let script = mutable_tx.entries[i].as_ref().unwrap().script_public_key.script(); + if let Some(secret_key) = map.get(script) { + let sig_hash = calc_ecdsa_signature_hash(&mutable_tx.as_verifiable(), i, SIG_HASH_ALL, &reused_values); + let msg = secp256k1::Message::from_digest_slice(sig_hash.as_bytes().as_slice()).unwrap(); + let sig = secp256k1::SECP256K1.sign_ecdsa(&msg, secret_key); + let sig_bytes = sig.serialize_compact(); + // This represents OP_DATA_65 (since signature length is 64 bytes and SIGHASH_TYPE is one byte) + mutable_tx.tx.inputs[i].signature_script = std::iter::once(65u8).chain(sig_bytes).chain([SIG_HASH_ALL.to_u8()]).collect(); + } else { + additional_signatures_required = true; + } + } + if additional_signatures_required { + Signed::Partially(mutable_tx) + } else { + Signed::Fully(mutable_tx) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/wallet/core/src/message.rs b/wallet/core/src/message.rs index cfc8197ed8..248ac26e8e 100644 --- a/wallet/core/src/message.rs +++ b/wallet/core/src/message.rs @@ -3,7 +3,8 @@ //! use kaspa_hashes::{Hash, PersonalMessageSigningHash}; -use secp256k1::{Error, XOnlyPublicKey}; +use rand::RngCore; +use secp256k1::{Error, PublicKey, XOnlyPublicKey}; /// A personal message (text) that can be signed. #[derive(Clone)] @@ -15,6 +16,12 @@ impl AsRef<[u8]> for PersonalMessage<'_> { } } +#[derive(Clone, Debug, PartialEq)] +pub enum SignatureType { + Schnorr, + ECDSA, +} + #[derive(Clone)] pub struct SignMessageOptions { /// The auxiliary randomness exists only to mitigate specific kinds of power analysis @@ -23,25 +30,41 @@ pub struct SignMessageOptions { /// mitigations against such attacks. To read more about the relevant discussions that /// arose in adding this randomness please see: pub no_aux_rand: bool, + /// Signature type to use for signing + pub signature_type: SignatureType, } /// Sign a message with the given private key pub fn sign_message(msg: &PersonalMessage, privkey: &[u8; 32], options: &SignMessageOptions) -> Result, Error> { let hash = calc_personal_message_hash(msg); - let msg = secp256k1::Message::from_digest_slice(hash.as_bytes().as_slice())?; - let schnorr_key = secp256k1::Keypair::from_seckey_slice(secp256k1::SECP256K1, privkey)?; - - let sig: [u8; 64] = if options.no_aux_rand { - *secp256k1::SECP256K1.sign_schnorr_no_aux_rand(&msg, &schnorr_key).as_ref() - } else { - *schnorr_key.sign_schnorr(msg).as_ref() - }; - Ok(sig.to_vec()) + match options.signature_type { + SignatureType::Schnorr => { + let schnorr_key = secp256k1::Keypair::from_seckey_slice(secp256k1::SECP256K1, privkey)?; + let sig: [u8; 64] = if options.no_aux_rand { + *secp256k1::SECP256K1.sign_schnorr_no_aux_rand(&msg, &schnorr_key).as_ref() + } else { + *schnorr_key.sign_schnorr(msg).as_ref() + }; + Ok(sig.to_vec()) + } + SignatureType::ECDSA => { + let secret_key = secp256k1::SecretKey::from_slice(privkey)?; + let sig = if options.no_aux_rand { + secp256k1::SECP256K1.sign_ecdsa(&msg, &secret_key) + } else { + // TODO: Use sign_ecdsa_with_noncedata with random noncedata to add auxiliary randomness + let mut nonce_data = [0u8; 32]; + rand::thread_rng().fill_bytes(&mut nonce_data); + secp256k1::SECP256K1.sign_ecdsa_with_noncedata(&msg, &secret_key, &nonce_data) + }; + Ok(sig.serialize_compact().to_vec()) + } + } } -/// Verifies signed message. +/// Verifies Schnorr signed message. /// /// Produces `Ok(())` if the signature matches the given message and [`secp256k1::Error`] /// if any of the inputs are incorrect, or the signature is invalid. @@ -53,6 +76,18 @@ pub fn verify_message(msg: &PersonalMessage, signature: &Vec, pubkey: &XOnly sig.verify(&msg, pubkey) } +/// Verifies ECDSA signed message. +/// +/// Produces `Ok(())` if the signature matches the given message and [`secp256k1::Error`] +/// if any of the inputs are incorrect, or the signature is invalid. +/// +pub fn verify_message_ecdsa(msg: &PersonalMessage, signature: &Vec, pubkey: &PublicKey) -> Result<(), Error> { + let hash = calc_personal_message_hash(msg); + let msg = secp256k1::Message::from_digest_slice(hash.as_bytes().as_slice())?; + let sig = secp256k1::ecdsa::Signature::from_compact(signature.as_slice())?; + sig.verify(&msg, pubkey) +} + fn calc_personal_message_hash(msg: &PersonalMessage) -> Hash { let mut hasher = PersonalMessageSigningHash::new(); hasher.write(msg); @@ -89,8 +124,8 @@ mod tests { ]) .unwrap(); - let sign_with_aux_rand = SignMessageOptions { no_aux_rand: false }; - let sign_with_no_aux_rand = SignMessageOptions { no_aux_rand: true }; + let sign_with_aux_rand = SignMessageOptions { no_aux_rand: false, signature_type: SignatureType::Schnorr }; + let sign_with_no_aux_rand = SignMessageOptions { no_aux_rand: true, signature_type: SignatureType::Schnorr }; verify_message(&pm, &sign_message(&pm, &privkey, &sign_with_aux_rand).expect("sign_message failed"), &pubkey) .expect("verify_message failed"); verify_message(&pm, &sign_message(&pm, &privkey, &sign_with_no_aux_rand).expect("sign_message failed"), &pubkey) @@ -105,7 +140,7 @@ mod tests { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, ]; - let sign_with_no_aux_rand = SignMessageOptions { no_aux_rand: true }; + let sign_with_no_aux_rand = SignMessageOptions { no_aux_rand: true, signature_type: SignatureType::Schnorr }; let signature = sign_message(&pm, &privkey, &sign_with_no_aux_rand).expect("sign_message failed"); let signature_twice = sign_message(&pm, &privkey, &sign_with_no_aux_rand).expect("sign_message failed"); assert_eq!(signature, signature_twice); @@ -124,8 +159,8 @@ mod tests { ]) .unwrap(); - let sign_with_aux_rand = SignMessageOptions { no_aux_rand: false }; - let sign_with_no_aux_rand = SignMessageOptions { no_aux_rand: true }; + let sign_with_aux_rand = SignMessageOptions { no_aux_rand: false, signature_type: SignatureType::Schnorr }; + let sign_with_no_aux_rand = SignMessageOptions { no_aux_rand: true, signature_type: SignatureType::Schnorr }; verify_message(&pm, &sign_message(&pm, &privkey, &sign_with_aux_rand).expect("sign_message failed"), &pubkey) .expect("verify_message failed"); verify_message(&pm, &sign_message(&pm, &privkey, &sign_with_no_aux_rand).expect("sign_message failed"), &pubkey) @@ -149,8 +184,8 @@ Ut omnis magnam et accusamus earum rem impedit provident eum commodi repellat qu ]) .unwrap(); - let sign_with_aux_rand = SignMessageOptions { no_aux_rand: false }; - let sign_with_no_aux_rand = SignMessageOptions { no_aux_rand: true }; + let sign_with_aux_rand = SignMessageOptions { no_aux_rand: false, signature_type: SignatureType::Schnorr }; + let sign_with_no_aux_rand = SignMessageOptions { no_aux_rand: true, signature_type: SignatureType::Schnorr }; verify_message(&pm, &sign_message(&pm, &privkey, &sign_with_aux_rand).expect("sign_message failed"), &pubkey) .expect("verify_message failed"); verify_message(&pm, &sign_message(&pm, &privkey, &sign_with_no_aux_rand).expect("sign_message failed"), &pubkey) diff --git a/wallet/core/src/tx/generator/signer.rs b/wallet/core/src/tx/generator/signer.rs index e24ef653d5..05e8cf36a8 100644 --- a/wallet/core/src/tx/generator/signer.rs +++ b/wallet/core/src/tx/generator/signer.rs @@ -4,10 +4,20 @@ use crate::imports::*; use kaspa_bip32::PrivateKey; -use kaspa_consensus_core::{sign::sign_with_multiple_v2, tx::SignableTransaction}; +use kaspa_consensus_core::{ + sign::{sign_with_multiple_ecdsa, sign_with_multiple_v2}, + tx::SignableTransaction, +}; pub trait SignerT: Send + Sync + 'static { fn try_sign(&self, transaction: SignableTransaction, addresses: &[Address]) -> Result; + fn signature_type(&self) -> SignatureType; +} + +#[derive(Clone, Debug, PartialEq)] +pub enum SignatureType { + Schnorr, + ECDSA, } struct Inner { @@ -15,6 +25,7 @@ struct Inner { account: Arc, payment_secret: Option, keys: Mutex>, + signature_type: SignatureType, } pub struct Signer { @@ -23,7 +34,24 @@ pub struct Signer { impl Signer { pub fn new(account: Arc, keydata: PrvKeyData, payment_secret: Option) -> Self { - Self { inner: Arc::new(Inner { keydata, account, payment_secret, keys: Mutex::new(AHashMap::new()) }) } + Self { + inner: Arc::new(Inner { + keydata, + account, + payment_secret, + keys: Mutex::new(AHashMap::new()), + signature_type: SignatureType::Schnorr, // Default to Schnorr + }), + } + } + + pub fn new_with_signature_type( + account: Arc, + keydata: PrvKeyData, + payment_secret: Option, + signature_type: SignatureType, + ) -> Self { + Self { inner: Arc::new(Inner { keydata, account, payment_secret, keys: Mutex::new(AHashMap::new()), signature_type }) } } fn ingest(&self, addresses: &[Address]) -> Result<()> { @@ -55,16 +83,24 @@ impl SignerT for Signer { let keys = self.inner.keys.lock().unwrap(); let mut keys_for_signing = addresses.iter().map(|address| *keys.get(address).unwrap()).collect::>(); // TODO - refactor for multisig - let signable_tx = sign_with_multiple_v2(mutable_tx, &keys_for_signing).fully_signed()?; + let signable_tx = match self.inner.signature_type { + SignatureType::Schnorr => sign_with_multiple_v2(mutable_tx, &keys_for_signing).fully_signed()?, + SignatureType::ECDSA => sign_with_multiple_ecdsa(mutable_tx, &keys_for_signing).fully_signed()?, + }; keys_for_signing.zeroize(); Ok(signable_tx) } + + fn signature_type(&self) -> SignatureType { + self.inner.signature_type.clone() + } } // --- struct KeydataSignerInner { keys: HashMap, + signature_type: SignatureType, } pub struct KeydataSigner { @@ -74,16 +110,32 @@ pub struct KeydataSigner { impl KeydataSigner { pub fn new(keydata: Vec<(Address, secp256k1::SecretKey)>) -> Self { let keys = keydata.into_iter().map(|(address, key)| (address, key.to_bytes())).collect(); - Self { inner: Arc::new(KeydataSignerInner { keys }) } + Self { + inner: Arc::new(KeydataSignerInner { + keys, + signature_type: SignatureType::Schnorr, // Default to Schnorr + }), + } + } + + pub fn new_with_signature_type(keydata: Vec<(Address, secp256k1::SecretKey)>, signature_type: SignatureType) -> Self { + let keys = keydata.into_iter().map(|(address, key)| (address, key.to_bytes())).collect(); + Self { inner: Arc::new(KeydataSignerInner { keys, signature_type }) } } } impl SignerT for KeydataSigner { fn try_sign(&self, mutable_tx: SignableTransaction, addresses: &[Address]) -> Result { let mut keys_for_signing = addresses.iter().map(|address| *self.inner.keys.get(address).unwrap()).collect::>(); - // TODO - refactor for multisig - let signable_tx = sign_with_multiple_v2(mutable_tx, &keys_for_signing).fully_signed()?; + + let signable_tx = match self.inner.signature_type { + SignatureType::Schnorr => sign_with_multiple_v2(mutable_tx, &keys_for_signing).fully_signed()?, + SignatureType::ECDSA => sign_with_multiple_ecdsa(mutable_tx, &keys_for_signing).fully_signed()?, + }; keys_for_signing.zeroize(); Ok(signable_tx) } + fn signature_type(&self) -> SignatureType { + self.inner.signature_type.clone() + } } diff --git a/wallet/core/src/wasm/message.rs b/wallet/core/src/wasm/message.rs index 372129280d..6d795becb6 100644 --- a/wallet/core/src/wasm/message.rs +++ b/wallet/core/src/wasm/message.rs @@ -15,6 +15,7 @@ export interface ISignMessage { message: string; privateKey: PrivateKey | string; noAuxRand?: boolean; + signatureType?: 'schnorr' | 'ecdsa'; } "#; @@ -32,10 +33,15 @@ pub fn js_sign_message(value: ISignMessage) -> Result { let private_key = object.cast_into::("privateKey")?; let raw_msg = object.get_string("message")?; let no_aux_rand = object.get_bool("noAuxRand").unwrap_or(false); + let signature_type_str = object.get_string("signatureType").unwrap_or_else(|_| "schnorr".to_string()); + let signature_type = match signature_type_str.as_str() { + "ecdsa" => SignatureType::ECDSA, + _ => SignatureType::Schnorr, + }; let mut privkey_bytes = [0u8; 32]; privkey_bytes.copy_from_slice(&private_key.secret_bytes()); let pm = PersonalMessage(&raw_msg); - let sign_options = SignMessageOptions { no_aux_rand }; + let sign_options = SignMessageOptions { no_aux_rand, signature_type }; let sig_vec = sign_message(&pm, &privkey_bytes, &sign_options)?; privkey_bytes.zeroize(); Ok(faster_hex::hex_string(sig_vec.as_slice()).into()) @@ -55,6 +61,7 @@ export interface IVerifyMessage { message: string; signature: HexString; publicKey: PublicKey | string; + signatureType?: 'schnorr' | 'ecdsa'; } "#; @@ -72,12 +79,32 @@ pub fn js_verify_message(value: IVerifyMessage) -> Result { let public_key = object.cast_into::("publicKey")?; let raw_msg = object.get_string("message")?; let signature = object.get_string("signature")?; + let signature_type_str = object.get_string("signatureType").unwrap_or_else(|_| "schnorr".to_string()); let pm = PersonalMessage(&raw_msg); - let mut signature_bytes = [0u8; 64]; + let mut signature_bytes = vec![0u8; signature.len() / 2]; faster_hex::hex_decode(signature.as_bytes(), &mut signature_bytes)?; - Ok(verify_message(&pm, &signature_bytes.to_vec(), &public_key.xonly_public_key).is_ok()) + let result = match signature_type_str.as_str() { + "ecdsa" => { + if let Some(secp_pubkey) = public_key.public_key { + verify_message_ecdsa(&pm, &signature_bytes, &secp_pubkey).is_ok() + } else { + false + } + } + _ => { + let mut schnorr_sig_bytes = [0u8; 64]; + if signature_bytes.len() == 64 { + schnorr_sig_bytes.copy_from_slice(&signature_bytes); + verify_message(&pm, &schnorr_sig_bytes.to_vec(), &public_key.xonly_public_key).is_ok() + } else { + false + } + } + }; + + Ok(result) } else { Err(Error::custom("Failed to parse input")) }