Skip to content

Commit 2f67c9b

Browse files
feat: add ecdsa support
1 parent efb835a commit 2f67c9b

File tree

5 files changed

+173
-30
lines changed

5 files changed

+173
-30
lines changed

cli/src/modules/message.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use kaspa_addresses::Version;
22
use kaspa_bip32::secp256k1::XOnlyPublicKey;
3-
use kaspa_wallet_core::message::SignMessageOptions;
3+
use kaspa_wallet_core::message::{SignMessageOptions, SignatureType};
44
use kaspa_wallet_core::{
55
account::{BIP32_ACCOUNT_KIND, KEYPAIR_ACCOUNT_KIND},
66
message::{sign_message, verify_message, PersonalMessage},
@@ -88,7 +88,7 @@ impl Message {
8888

8989
let pm = PersonalMessage(message);
9090
let privkey = self.get_address_private_key(&ctx, kaspa_address).await?;
91-
let sign_options = SignMessageOptions { no_aux_rand: false };
91+
let sign_options = SignMessageOptions { no_aux_rand: false, signature_type: SignatureType::Schnorr };
9292

9393
let sig_result = sign_message(&pm, &privkey, &sign_options);
9494

consensus/core/src/sign.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::{
22
hashing::{
3-
sighash::{calc_schnorr_signature_hash, SigHashReusedValuesUnsync},
3+
sighash::{calc_ecdsa_signature_hash, calc_schnorr_signature_hash, SigHashReusedValuesUnsync},
44
sighash_type::{SigHashType, SIG_HASH_ALL},
55
},
66
tx::{SignableTransaction, VerifiableTransaction},
@@ -260,3 +260,36 @@ mod tests {
260260
assert!(verify(&signed_tx.as_verifiable()).is_ok());
261261
}
262262
}
263+
264+
/// Sign a transaction using ECDSA
265+
#[allow(clippy::result_large_err)]
266+
pub fn sign_with_multiple_ecdsa(mut mutable_tx: SignableTransaction, privkeys: &[[u8; 32]]) -> Signed {
267+
let mut map = BTreeMap::new();
268+
for privkey in privkeys {
269+
let secret_key = secp256k1::SecretKey::from_slice(privkey).unwrap();
270+
let public_key = secret_key.public_key(secp256k1::SECP256K1);
271+
let script_pub_key_script = once(0x21).chain(public_key.serialize().into_iter()).chain(once(0xac)).collect_vec();
272+
map.insert(script_pub_key_script, secret_key);
273+
}
274+
275+
let reused_values = SigHashReusedValuesUnsync::new();
276+
let mut additional_signatures_required = false;
277+
for i in 0..mutable_tx.tx.inputs.len() {
278+
let script = mutable_tx.entries[i].as_ref().unwrap().script_public_key.script();
279+
if let Some(secret_key) = map.get(script) {
280+
let sig_hash = calc_ecdsa_signature_hash(&mutable_tx.as_verifiable(), i, SIG_HASH_ALL, &reused_values);
281+
let msg = secp256k1::Message::from_digest_slice(sig_hash.as_bytes().as_slice()).unwrap();
282+
let sig = secp256k1::SECP256K1.sign_ecdsa(&msg, secret_key);
283+
let sig_bytes = sig.serialize_compact();
284+
// This represents OP_DATA_65 <SIGNATURE+SIGHASH_TYPE> (since signature length is 64 bytes and SIGHASH_TYPE is one byte)
285+
mutable_tx.tx.inputs[i].signature_script = std::iter::once(65u8).chain(sig_bytes).chain([SIG_HASH_ALL.to_u8()]).collect();
286+
} else {
287+
additional_signatures_required = true;
288+
}
289+
}
290+
if additional_signatures_required {
291+
Signed::Partially(mutable_tx)
292+
} else {
293+
Signed::Fully(mutable_tx)
294+
}
295+
}

wallet/core/src/message.rs

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//!
44
55
use kaspa_hashes::{Hash, PersonalMessageSigningHash};
6-
use secp256k1::{Error, XOnlyPublicKey};
6+
use secp256k1::{Error, PublicKey, XOnlyPublicKey};
77

88
/// A personal message (text) that can be signed.
99
#[derive(Clone)]
@@ -15,6 +15,12 @@ impl AsRef<[u8]> for PersonalMessage<'_> {
1515
}
1616
}
1717

18+
#[derive(Clone, Debug, PartialEq)]
19+
pub enum SignatureType {
20+
Schnorr,
21+
ECDSA,
22+
}
23+
1824
#[derive(Clone)]
1925
pub struct SignMessageOptions {
2026
/// The auxiliary randomness exists only to mitigate specific kinds of power analysis
@@ -23,25 +29,38 @@ pub struct SignMessageOptions {
2329
/// mitigations against such attacks. To read more about the relevant discussions that
2430
/// arose in adding this randomness please see: <https://github.com/sipa/bips/issues/195>
2531
pub no_aux_rand: bool,
32+
/// Signature type to use for signing
33+
pub signature_type: SignatureType,
2634
}
2735

2836
/// Sign a message with the given private key
2937
pub fn sign_message(msg: &PersonalMessage, privkey: &[u8; 32], options: &SignMessageOptions) -> Result<Vec<u8>, Error> {
3038
let hash = calc_personal_message_hash(msg);
31-
3239
let msg = secp256k1::Message::from_digest_slice(hash.as_bytes().as_slice())?;
33-
let schnorr_key = secp256k1::Keypair::from_seckey_slice(secp256k1::SECP256K1, privkey)?;
34-
35-
let sig: [u8; 64] = if options.no_aux_rand {
36-
*secp256k1::SECP256K1.sign_schnorr_no_aux_rand(&msg, &schnorr_key).as_ref()
37-
} else {
38-
*schnorr_key.sign_schnorr(msg).as_ref()
39-
};
4040

41-
Ok(sig.to_vec())
41+
match options.signature_type {
42+
SignatureType::Schnorr => {
43+
let schnorr_key = secp256k1::Keypair::from_seckey_slice(secp256k1::SECP256K1, privkey)?;
44+
let sig: [u8; 64] = if options.no_aux_rand {
45+
*secp256k1::SECP256K1.sign_schnorr_no_aux_rand(&msg, &schnorr_key).as_ref()
46+
} else {
47+
*schnorr_key.sign_schnorr(msg).as_ref()
48+
};
49+
Ok(sig.to_vec())
50+
}
51+
SignatureType::ECDSA => {
52+
let secret_key = secp256k1::SecretKey::from_slice(privkey)?;
53+
let sig = if options.no_aux_rand {
54+
secp256k1::SECP256K1.sign_ecdsa(&msg, &secret_key)
55+
} else {
56+
secp256k1::SECP256K1.sign_ecdsa(&msg, &secret_key)
57+
};
58+
Ok(sig.serialize_compact().to_vec())
59+
}
60+
}
4261
}
4362

44-
/// Verifies signed message.
63+
/// Verifies Schnorr signed message.
4564
///
4665
/// Produces `Ok(())` if the signature matches the given message and [`secp256k1::Error`]
4766
/// if any of the inputs are incorrect, or the signature is invalid.
@@ -53,6 +72,18 @@ pub fn verify_message(msg: &PersonalMessage, signature: &Vec<u8>, pubkey: &XOnly
5372
sig.verify(&msg, pubkey)
5473
}
5574

75+
/// Verifies ECDSA signed message.
76+
///
77+
/// Produces `Ok(())` if the signature matches the given message and [`secp256k1::Error`]
78+
/// if any of the inputs are incorrect, or the signature is invalid.
79+
///
80+
pub fn verify_message_ecdsa(msg: &PersonalMessage, signature: &Vec<u8>, pubkey: &PublicKey) -> Result<(), Error> {
81+
let hash = calc_personal_message_hash(msg);
82+
let msg = secp256k1::Message::from_digest_slice(hash.as_bytes().as_slice())?;
83+
let sig = secp256k1::ecdsa::Signature::from_compact(signature.as_slice())?;
84+
sig.verify(&msg, pubkey)
85+
}
86+
5687
fn calc_personal_message_hash(msg: &PersonalMessage) -> Hash {
5788
let mut hasher = PersonalMessageSigningHash::new();
5889
hasher.write(msg);
@@ -89,8 +120,8 @@ mod tests {
89120
])
90121
.unwrap();
91122

92-
let sign_with_aux_rand = SignMessageOptions { no_aux_rand: false };
93-
let sign_with_no_aux_rand = SignMessageOptions { no_aux_rand: true };
123+
let sign_with_aux_rand = SignMessageOptions { no_aux_rand: false, signature_type: SignatureType::Schnorr };
124+
let sign_with_no_aux_rand = SignMessageOptions { no_aux_rand: true, signature_type: SignatureType::Schnorr };
94125
verify_message(&pm, &sign_message(&pm, &privkey, &sign_with_aux_rand).expect("sign_message failed"), &pubkey)
95126
.expect("verify_message failed");
96127
verify_message(&pm, &sign_message(&pm, &privkey, &sign_with_no_aux_rand).expect("sign_message failed"), &pubkey)
@@ -105,7 +136,7 @@ mod tests {
105136
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03,
106137
];
107138

108-
let sign_with_no_aux_rand = SignMessageOptions { no_aux_rand: true };
139+
let sign_with_no_aux_rand = SignMessageOptions { no_aux_rand: true, signature_type: SignatureType::Schnorr };
109140
let signature = sign_message(&pm, &privkey, &sign_with_no_aux_rand).expect("sign_message failed");
110141
let signature_twice = sign_message(&pm, &privkey, &sign_with_no_aux_rand).expect("sign_message failed");
111142
assert_eq!(signature, signature_twice);
@@ -124,8 +155,8 @@ mod tests {
124155
])
125156
.unwrap();
126157

127-
let sign_with_aux_rand = SignMessageOptions { no_aux_rand: false };
128-
let sign_with_no_aux_rand = SignMessageOptions { no_aux_rand: true };
158+
let sign_with_aux_rand = SignMessageOptions { no_aux_rand: false, signature_type: SignatureType::Schnorr };
159+
let sign_with_no_aux_rand = SignMessageOptions { no_aux_rand: true, signature_type: SignatureType::Schnorr };
129160
verify_message(&pm, &sign_message(&pm, &privkey, &sign_with_aux_rand).expect("sign_message failed"), &pubkey)
130161
.expect("verify_message failed");
131162
verify_message(&pm, &sign_message(&pm, &privkey, &sign_with_no_aux_rand).expect("sign_message failed"), &pubkey)
@@ -149,8 +180,8 @@ Ut omnis magnam et accusamus earum rem impedit provident eum commodi repellat qu
149180
])
150181
.unwrap();
151182

152-
let sign_with_aux_rand = SignMessageOptions { no_aux_rand: false };
153-
let sign_with_no_aux_rand = SignMessageOptions { no_aux_rand: true };
183+
let sign_with_aux_rand = SignMessageOptions { no_aux_rand: false, signature_type: SignatureType::Schnorr };
184+
let sign_with_no_aux_rand = SignMessageOptions { no_aux_rand: true, signature_type: SignatureType::Schnorr };
154185
verify_message(&pm, &sign_message(&pm, &privkey, &sign_with_aux_rand).expect("sign_message failed"), &pubkey)
155186
.expect("verify_message failed");
156187
verify_message(&pm, &sign_message(&pm, &privkey, &sign_with_no_aux_rand).expect("sign_message failed"), &pubkey)

wallet/core/src/tx/generator/signer.rs

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,28 @@
44
55
use crate::imports::*;
66
use kaspa_bip32::PrivateKey;
7-
use kaspa_consensus_core::{sign::sign_with_multiple_v2, tx::SignableTransaction};
7+
use kaspa_consensus_core::{
8+
sign::{sign_with_multiple_ecdsa, sign_with_multiple_v2},
9+
tx::SignableTransaction,
10+
};
811

912
pub trait SignerT: Send + Sync + 'static {
1013
fn try_sign(&self, transaction: SignableTransaction, addresses: &[Address]) -> Result<SignableTransaction>;
14+
fn signature_type(&self) -> SignatureType;
15+
}
16+
17+
#[derive(Clone, Debug, PartialEq)]
18+
pub enum SignatureType {
19+
Schnorr,
20+
ECDSA,
1121
}
1222

1323
struct Inner {
1424
keydata: PrvKeyData,
1525
account: Arc<dyn Account>,
1626
payment_secret: Option<Secret>,
1727
keys: Mutex<AHashMap<Address, [u8; 32]>>,
28+
signature_type: SignatureType,
1829
}
1930

2031
pub struct Signer {
@@ -23,7 +34,24 @@ pub struct Signer {
2334

2435
impl Signer {
2536
pub fn new(account: Arc<dyn Account>, keydata: PrvKeyData, payment_secret: Option<Secret>) -> Self {
26-
Self { inner: Arc::new(Inner { keydata, account, payment_secret, keys: Mutex::new(AHashMap::new()) }) }
37+
Self {
38+
inner: Arc::new(Inner {
39+
keydata,
40+
account,
41+
payment_secret,
42+
keys: Mutex::new(AHashMap::new()),
43+
signature_type: SignatureType::Schnorr, // Default to Schnorr
44+
}),
45+
}
46+
}
47+
48+
pub fn new_with_signature_type(
49+
account: Arc<dyn Account>,
50+
keydata: PrvKeyData,
51+
payment_secret: Option<Secret>,
52+
signature_type: SignatureType,
53+
) -> Self {
54+
Self { inner: Arc::new(Inner { keydata, account, payment_secret, keys: Mutex::new(AHashMap::new()), signature_type }) }
2755
}
2856

2957
fn ingest(&self, addresses: &[Address]) -> Result<()> {
@@ -55,16 +83,24 @@ impl SignerT for Signer {
5583
let keys = self.inner.keys.lock().unwrap();
5684
let mut keys_for_signing = addresses.iter().map(|address| *keys.get(address).unwrap()).collect::<Vec<_>>();
5785
// TODO - refactor for multisig
58-
let signable_tx = sign_with_multiple_v2(mutable_tx, &keys_for_signing).fully_signed()?;
86+
let signable_tx = match self.inner.signature_type {
87+
SignatureType::Schnorr => sign_with_multiple_v2(mutable_tx, &keys_for_signing).fully_signed()?,
88+
SignatureType::ECDSA => sign_with_multiple_ecdsa(mutable_tx, &keys_for_signing).fully_signed()?,
89+
};
5990
keys_for_signing.zeroize();
6091
Ok(signable_tx)
6192
}
93+
94+
fn signature_type(&self) -> SignatureType {
95+
self.inner.signature_type.clone()
96+
}
6297
}
6398

6499
// ---
65100

66101
struct KeydataSignerInner {
67102
keys: HashMap<Address, [u8; 32]>,
103+
signature_type: SignatureType,
68104
}
69105

70106
pub struct KeydataSigner {
@@ -74,16 +110,32 @@ pub struct KeydataSigner {
74110
impl KeydataSigner {
75111
pub fn new(keydata: Vec<(Address, secp256k1::SecretKey)>) -> Self {
76112
let keys = keydata.into_iter().map(|(address, key)| (address, key.to_bytes())).collect();
77-
Self { inner: Arc::new(KeydataSignerInner { keys }) }
113+
Self {
114+
inner: Arc::new(KeydataSignerInner {
115+
keys,
116+
signature_type: SignatureType::Schnorr, // Default to Schnorr
117+
}),
118+
}
119+
}
120+
121+
pub fn new_with_signature_type(keydata: Vec<(Address, secp256k1::SecretKey)>, signature_type: SignatureType) -> Self {
122+
let keys = keydata.into_iter().map(|(address, key)| (address, key.to_bytes())).collect();
123+
Self { inner: Arc::new(KeydataSignerInner { keys, signature_type }) }
78124
}
79125
}
80126

81127
impl SignerT for KeydataSigner {
82128
fn try_sign(&self, mutable_tx: SignableTransaction, addresses: &[Address]) -> Result<SignableTransaction> {
83129
let mut keys_for_signing = addresses.iter().map(|address| *self.inner.keys.get(address).unwrap()).collect::<Vec<_>>();
84-
// TODO - refactor for multisig
85-
let signable_tx = sign_with_multiple_v2(mutable_tx, &keys_for_signing).fully_signed()?;
130+
131+
let signable_tx = match self.inner.signature_type {
132+
SignatureType::Schnorr => sign_with_multiple_v2(mutable_tx, &keys_for_signing).fully_signed()?,
133+
SignatureType::ECDSA => sign_with_multiple_ecdsa(mutable_tx, &keys_for_signing).fully_signed()?,
134+
};
86135
keys_for_signing.zeroize();
87136
Ok(signable_tx)
88137
}
138+
fn signature_type(&self) -> SignatureType {
139+
self.inner.signature_type.clone()
140+
}
89141
}

wallet/core/src/wasm/message.rs

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface ISignMessage {
1515
message: string;
1616
privateKey: PrivateKey | string;
1717
noAuxRand?: boolean;
18+
signatureType?: 'schnorr' | 'ecdsa';
1819
}
1920
"#;
2021

@@ -32,10 +33,15 @@ pub fn js_sign_message(value: ISignMessage) -> Result<HexString, Error> {
3233
let private_key = object.cast_into::<PrivateKey>("privateKey")?;
3334
let raw_msg = object.get_string("message")?;
3435
let no_aux_rand = object.get_bool("noAuxRand").unwrap_or(false);
36+
let signature_type_str = object.get_string("signatureType").unwrap_or_else(|_| "schnorr".to_string());
37+
let signature_type = match signature_type_str.as_str() {
38+
"ecdsa" => SignatureType::ECDSA,
39+
_ => SignatureType::Schnorr,
40+
};
3541
let mut privkey_bytes = [0u8; 32];
3642
privkey_bytes.copy_from_slice(&private_key.secret_bytes());
3743
let pm = PersonalMessage(&raw_msg);
38-
let sign_options = SignMessageOptions { no_aux_rand };
44+
let sign_options = SignMessageOptions { no_aux_rand, signature_type };
3945
let sig_vec = sign_message(&pm, &privkey_bytes, &sign_options)?;
4046
privkey_bytes.zeroize();
4147
Ok(faster_hex::hex_string(sig_vec.as_slice()).into())
@@ -55,6 +61,7 @@ export interface IVerifyMessage {
5561
message: string;
5662
signature: HexString;
5763
publicKey: PublicKey | string;
64+
signatureType?: 'schnorr' | 'ecdsa';
5865
}
5966
"#;
6067

@@ -72,12 +79,32 @@ pub fn js_verify_message(value: IVerifyMessage) -> Result<bool, Error> {
7279
let public_key = object.cast_into::<PublicKey>("publicKey")?;
7380
let raw_msg = object.get_string("message")?;
7481
let signature = object.get_string("signature")?;
82+
let signature_type_str = object.get_string("signatureType").unwrap_or_else(|_| "schnorr".to_string());
7583

7684
let pm = PersonalMessage(&raw_msg);
77-
let mut signature_bytes = [0u8; 64];
85+
let mut signature_bytes = vec![0u8; signature.len() / 2];
7886
faster_hex::hex_decode(signature.as_bytes(), &mut signature_bytes)?;
7987

80-
Ok(verify_message(&pm, &signature_bytes.to_vec(), &public_key.xonly_public_key).is_ok())
88+
let result = match signature_type_str.as_str() {
89+
"ecdsa" => {
90+
if let Some(secp_pubkey) = public_key.public_key {
91+
verify_message_ecdsa(&pm, &signature_bytes, &secp_pubkey).is_ok()
92+
} else {
93+
false
94+
}
95+
}
96+
_ => {
97+
let mut schnorr_sig_bytes = [0u8; 64];
98+
if signature_bytes.len() == 64 {
99+
schnorr_sig_bytes.copy_from_slice(&signature_bytes);
100+
verify_message(&pm, &schnorr_sig_bytes.to_vec(), &public_key.xonly_public_key).is_ok()
101+
} else {
102+
false
103+
}
104+
}
105+
};
106+
107+
Ok(result)
81108
} else {
82109
Err(Error::custom("Failed to parse input"))
83110
}

0 commit comments

Comments
 (0)