From 7d021ad416a27274299db05b5e2e2251a4d94660 Mon Sep 17 00:00:00 2001 From: greg Date: Thu, 8 Aug 2024 09:02:00 -0400 Subject: [PATCH] Enhance key pair generation: Add random key generation and print private key --- Cargo.lock | 3 + Cargo.toml | 1 + src/main.rs | 332 ++++++++++++++-------------------------------------- 3 files changed, 94 insertions(+), 242 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 33299e1..b7a3499 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "aho-corasick" version = "0.7.15" @@ -263,6 +265,7 @@ dependencies = [ "clap", "hex", "predicates", + "rand 0.8.3", "secp256k1", "serde", "serde_cbor", diff --git a/Cargo.toml b/Cargo.toml index d61bc99..9d3f5bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = {version="1.0", features = ["preserve_order"]} serde_cbor = "0.11.1" bitcoin = {version="0.26.0", features = ["secp-recovery", "base64"]} +rand = "0.8" # or the latest version [dev-dependencies] assert_cmd = "0.10" diff --git a/src/main.rs b/src/main.rs index 64c572d..0829e7c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,24 +1,20 @@ use clap::{Clap, ValueHint}; -use std::collections::HashSet; -use std::io::BufRead; -use std::path::PathBuf; -use std::str::FromStr; -extern crate hex; -extern crate secp256k1; +use rand::Rng; use secp256k1::bitcoin_hashes::sha256; +use secp256k1::{schnorrsig, Message, PublicKey, Secp256k1, SecretKey, Signature}; use serde::{Deserialize, Serialize}; use serde_json::json; - -//use secp256k1::rand::rngs::OsRng; -use secp256k1::{schnorrsig, Message, PublicKey, Secp256k1, SecretKey, Signature}; -extern crate bitcoin; +use std::collections::HashSet; +use std::io::{self, BufRead, BufReader, Read}; +use std::path::PathBuf; +use std::str::FromStr; use bitcoin::util::address::Address; -use bitcoin::util::key::PublicKey as Public_key; +use bitcoin::util::key::PublicKey as BtcPublicKey; use bitcoin::util::misc::{signed_msg_hash, MessageSignature}; +use hex; +use errors::MusignError; mod errors; -use errors::MusignError; -use std::io::{stdin, BufReader, Read}; pub trait Compact { fn to_cmpact(&self) -> String; @@ -30,8 +26,9 @@ impl Compact for Signature { let sig = self.serialize_compact().to_vec(); hex::encode(sig) } + fn from_cmpact(sig: Vec) -> Signature { - Signature::from_compact(&sig).unwrap() // TODO + Signature::from_compact(&sig).expect("Invalid compact signature") } } @@ -39,7 +36,6 @@ impl Compact for Signature { enum SigType { Ecdsa, Schnorr, - /// mainnet BtcLegacy, } @@ -49,7 +45,7 @@ struct Sig { #[serde(skip_serializing_if = "Option::is_none")] signature: Option, #[serde(skip_serializing_if = "Option::is_none")] - sig: Option>, // TODO + sig: Option>, message: String, #[serde(skip_serializing_if = "Option::is_none")] pubkey: Option, @@ -57,8 +53,8 @@ struct Sig { address: Option, } -fn signmessage(seckey: SecretKey, message: String) -> Result<(String, String), MusignError> { - let secp = secp256k1::Secp256k1::new(); +fn sign_message(seckey: SecretKey, message: String) -> Result<(String, String), MusignError> { + let secp = Secp256k1::new(); let msg_hash = signed_msg_hash(&message); let msg = secp256k1::Message::from_slice(&msg_hash)?; let secp_sig = secp.sign_recoverable(&msg, &seckey); @@ -73,12 +69,8 @@ fn signmessage(seckey: SecretKey, message: String) -> Result<(String, String), M Ok((signature.to_base64(), p2pkh.to_string())) } -fn verifymessage( - signature: String, - p2pkh_address: String, - message: String, -) -> Result { - let secp = secp256k1::Secp256k1::new(); +fn verify_message(signature: String, p2pkh_address: String, message: String) -> Result { + let secp = Secp256k1::new(); let signature = MessageSignature::from_str(&signature)?; let msg_hash = signed_msg_hash(&message); @@ -87,13 +79,9 @@ fn verifymessage( Ok(signature.is_signed_by_address(&secp, &addr, msg_hash)?) } -fn generate_schnorr_keypair( - seed: String, -) -> Result<(schnorrsig::KeyPair, schnorrsig::PublicKey), MusignError> { +fn generate_schnorr_keypair(seed: String) -> Result<(schnorrsig::KeyPair, schnorrsig::PublicKey), MusignError> { let s = Secp256k1::new(); - let keypair = schnorrsig::KeyPair::from_seckey_str(&s, &seed)?; - let pubkey = schnorrsig::PublicKey::from_keypair(&s, &keypair); Ok((keypair, pubkey)) } @@ -112,7 +100,7 @@ fn sign_schnorr(seckey: String, msg: String) -> Result Result { let s = Secp256k1::new(); let pubkey = schnorrsig::PublicKey::from_str(&pubkey)?; - let sig = schnorrsig::Signature::from_str(&signature).expect("Signature format incorrect"); + let sig = schnorrsig::Signature::from_str(&signature).expect("Invalid Schnorr signature format"); let message = Message::from_hashed_data::(msg.as_bytes()); Ok(s.schnorrsig_verify(&sig, &message, &pubkey).is_ok()) } @@ -126,13 +114,11 @@ fn generate_keypair(seed: Vec) -> Result<(SecretKey, PublicKey), MusignError fn sign(seckey: String, msg: String) -> Result { let seckey = SecretKey::from_str(&seckey)?; - let message = Message::from_hashed_data::(msg.as_bytes()); let secp = Secp256k1::new(); let sig = secp.sign(&message, &seckey); let public_key = PublicKey::from_secret_key(&secp, &seckey); assert!(secp.verify(&message, &sig, &public_key).is_ok()); - Ok(sig) } @@ -147,16 +133,14 @@ fn verify(signature: String, msg: String, pubkey: String) -> Result Result { let mut msg = obj.clone(); msg.signatures = None; - // remove signatures and whitespaces! let mut msg = serde_json::to_string(&msg)?; msg.retain(|c| !c.is_whitespace()); - let pubkeys = obj.setup.pubkeys.unwrap(); // safe - let sigs = obj.signatures.unwrap(); // safe + let pubkeys = obj.setup.pubkeys.unwrap(); + let sigs = obj.signatures.unwrap(); let pubkeys: HashSet = pubkeys.into_iter().collect(); let sigs: HashSet = sigs.into_iter().collect(); @@ -173,61 +157,36 @@ fn multisig_verify(obj: CmdMultisigConstruct) -> Result { } } - if cnt >= obj.setup.threshold.into() { - return Ok(true); - } - Ok(false) + Ok(cnt >= obj.setup.threshold.into()) } -// ecdsa multisig -fn multisig_combine( - obj: &mut Vec, -) -> Result<&CmdMultisigConstruct, MusignError> { - // Convert vector to hashset and remove signatures - let objs: HashSet = obj - .clone() - .into_iter() - .map(|mut s: CmdMultisigConstruct| { - s.signatures = None; - s - }) - .collect(); - - // All the object without signatures must be the same. Therefore only one element in hashset +fn multisig_combine(obj: &mut Vec) -> Result<&CmdMultisigConstruct, MusignError> { + let objs: HashSet = obj.clone().into_iter().map(|mut s| { + s.signatures = None; + s + }).collect(); + assert!(objs.len() == 1); - // collect all the signatures let mut v: HashSet = HashSet::new(); for o in obj.clone() { - if o.signatures.is_some() { - let p = o.signatures.ok_or_else(|| { - MusignError::new( - "conversion".to_string(), - "collecting sigantures".to_string(), - ) - })?; - v.extend(p.into_iter().clone()) + if let Some(p) = o.signatures { + v.extend(p.into_iter()) } } - let mut out = &mut obj[0]; - - let v_unique: Vec = v.into_iter().collect(); - out.signatures = Some(v_unique); + let out = &mut obj[0]; + out.signatures = Some(v.into_iter().collect()); Ok(out) } #[derive(Debug, Clap, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] #[clap()] pub struct CmdMultisigSetup { - /// Signature type. Currently only ecdsa implemented. #[clap(arg_enum, default_value = "ecdsa", short = 't')] sig_type: SigType, - /// Threshold #[clap(required = true)] threshold: u8, - /// List of public keys to participate in a multisig in hex format. - /// Alternatively, the keys can be piped via STDIN #[clap(short)] pubkeys: Option>, } @@ -235,10 +194,8 @@ pub struct CmdMultisigSetup { #[derive(Debug, Clap, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] #[clap()] pub struct CmdMultisigConstruct { - /// Message to sign. #[clap(required = true)] msg: String, - /// Multisignature setup (JSON) #[clap(required = true, parse(try_from_str = serde_json::from_str))] setup: CmdMultisigSetup, #[serde(skip_serializing_if = "Option::is_none")] @@ -249,8 +206,6 @@ pub struct CmdMultisigConstruct { #[derive(Debug, Clap, Serialize, Deserialize, Clone)] #[clap()] pub struct CmdMultisigSign { - /// Private key in hex to sign the multisig object. - /// Alternatively, the key can be provided via STDIN #[clap(short)] secret: Option, } @@ -258,156 +213,99 @@ pub struct CmdMultisigSign { #[derive(Debug, Clap)] #[clap()] pub struct CmdSign { - /// Path to private key (Not implemented) #[clap(parse(from_os_str), value_hint = ValueHint::AnyPath, short = 'f')] seckey_file: Option, - /// Private key in hex. - /// Alternatively, the key can be piped via STDIN #[clap(short)] secret: Option, - /// Message to sign. #[clap(required = true)] msg: String, - /// Signature type #[clap(arg_enum, default_value = "ecdsa", short = 't')] sig_type: SigType, - /// Output format #[clap(short='r', default_value = "json", possible_values=&["json", "cbor"])] format: String, } -// musign verify -h is correct while musign help verify is not #[derive(Debug, Clap)] #[clap()] pub struct CmdVerify { - /// Signature in hex #[clap(required = true)] signature: String, - /// Message string #[clap(required = true)] message: String, - /// Public key in hex. - /// Alternatively, the key can be provided via STDIN #[clap(short = 'p')] pubkey: Option, #[clap(arg_enum, default_value = "ecdsa", short = 't')] sig_type: SigType, - /// BTC p2pkh address #[clap(short = 'a')] address: Option, } #[derive(Clap, Debug)] #[clap(name = "musign-cli")] -/// Generate secp256k1 keys, sign and verify messages with ECDSA and Schnorr in a single- or multisignature setup. enum Opt { - /// Generate a public key from a secret (private key/seed/secret key). In case of btc-legacy type - /// p2pkh address is generated. Generate { - /// Secret (also known as seed, private key or secret key) in hex. - /// Alternatively, the key can be provided via STDIN secret: Option, - /// Type of signature. #[clap(arg_enum, default_value = "ecdsa", short = 't')] sig_type: SigType, }, - - /// Sign a message. Signature is returned. Sign(CmdSign), - - /// Verify a signature for a given message. True is returned for a valid signature otherwise false. Verify(CmdVerify), - - /// Set up a multisig: quorum and all the participants (pubkeys in hex). - #[clap(display_order = 2000)] MultisigSetup(CmdMultisigSetup), - - /// Add message to a multisig setup. Returns an unsigned multisignature object. - #[clap(display_order = 2001, name = "multisig-construct-msg")] MultisigConstruct(CmdMultisigConstruct), - - /// Sign a multisignature object passed via STDIN. - #[clap(display_order = 2002)] MultisigSign(CmdMultisigSign), - - /// Combine signatures of individually signed multisignature objects. Pass them via STDIN. - #[clap(display_order = 2003)] MultisigCombine, - - /// Verify a multisignature object passed via STDIN. Returns true or false. - #[clap(display_order = 2004)] MultisigVerify, } fn main() -> Result<(), MusignError> { let matches = Opt::parse(); - //println!("DEBUG: {:?}\n", matches); // TODO: enclose under --verbose - match matches { Opt::Generate { secret, sig_type } => { - let secret = if secret != None { - secret.unwrap() // safe - } else { - let mut privkey = String::new(); - let ret = stdin().read_to_string(&mut privkey); - assert!(ret.is_ok()); - privkey.retain(|c| !c.is_whitespace()); - privkey + let secret = match secret { + Some(s) => s, + None => { + let mut rng = rand::thread_rng(); + let mut key = [0u8; 32]; + rng.fill(&mut key); + hex::encode(key) + } }; - let seed_bytes = hex::decode(secret.clone()).map_err(|_| { + let seed_bytes = hex::decode(&secret).map_err(|_| { MusignError::new("conversion".to_string(), "cannot decode secret".to_string()) })?; match sig_type { SigType::Ecdsa => { - let (_, pubkey) = generate_keypair(seed_bytes)?; - let ret = json!({ - "pubkey": pubkey.to_string(), - }); - println!("{}", ret.to_string()); + let (secret_key, pubkey) = generate_keypair(seed_bytes)?; + println!("{}", json!({ "private_key": secret, "pubkey": pubkey.to_string() }).to_string()); } SigType::Schnorr => { let (_, pubkey) = generate_schnorr_keypair(secret)?; - let ret = json!({ - "pubkey": pubkey.to_string(), - }); - println!("{}", ret.to_string()); + println!("{}", json!({ "pubkey": pubkey.to_string() }).to_string()); } SigType::BtcLegacy => { - let (_, pubkey) = generate_keypair(seed_bytes)?; - let pubkey = Public_key { - compressed: true, - key: pubkey, - }; + let (secret_key, pubkey) = generate_keypair(seed_bytes)?; + let pubkey = BtcPublicKey { compressed: true, key: pubkey }; let p2pkh = Address::p2pkh(&pubkey, bitcoin::Network::Bitcoin); - let ret = json!({ - "address": p2pkh.to_string(), - }); - println!("{}", ret.to_string()); + println!("{}", json!({ "private_key": secret, "address": p2pkh.to_string() }).to_string()); } - }; + } } Opt::Sign(cmd) => { - let sec = if cmd.secret != None { - cmd.secret.clone().expect("error private key string") // safe - } else { + let secret = cmd.secret.unwrap_or_else(|| { let mut privkey = String::new(); - let ret = stdin().read_to_string(&mut privkey); - assert!(ret.is_ok()); + io::stdin().read_to_string(&mut privkey).expect("Error reading from stdin"); privkey.retain(|c| !c.is_whitespace()); privkey - }; + }); let out = match cmd.sig_type { SigType::Ecdsa => { - let sig = sign(sec.clone(), cmd.msg.clone())?; - - // TODO: make a method inside a struct - let seed_bytes = hex::decode(sec).expect("Decoding seed failed"); + let sig = sign(secret.clone(), cmd.msg.clone())?; + let seed_bytes = hex::decode(&secret).expect("Decoding seed failed"); let (_, pubkey) = generate_keypair(seed_bytes)?; - let mut sig = Sig { sig_type: cmd.sig_type, signature: Some(sig.to_cmpact()), @@ -416,46 +314,36 @@ fn main() -> Result<(), MusignError> { pubkey: Some(pubkey.to_string()), address: None, }; - if cmd.format == "cbor" { sig.signature = None; } else { sig.sig = None; } - sig } SigType::Schnorr => { - let sig = sign_schnorr(sec.clone(), cmd.msg.clone())?; - // TODO: make a method inside a struct - let (_, pubkey) = generate_schnorr_keypair(sec)?; - + let sig = sign_schnorr(secret.clone(), cmd.msg.clone())?; + let (_, pubkey) = generate_schnorr_keypair(secret)?; let mut sig = Sig { sig_type: cmd.sig_type, signature: Some(sig.to_string()), sig: Some(hex::decode(sig.to_string()).map_err(|_| { - MusignError::new( - "conversion".to_string(), - "cannot decode signature into hex".to_string(), - ) + MusignError::new("conversion".to_string(), "cannot decode signature into hex".to_string()) })?), message: cmd.msg, pubkey: Some(pubkey.to_string()), address: None, }; - if cmd.format == "cbor" { sig.signature = None; } else { sig.sig = None; } - sig } - SigType::BtcLegacy => { - let seckey = SecretKey::from_str(&sec)?; - let (sig, addr) = signmessage(seckey, cmd.msg.clone())?; + let seckey = SecretKey::from_str(&secret)?; + let (sig, addr) = sign_message(seckey, cmd.msg.clone())?; Sig { sig_type: cmd.sig_type, signature: Some(sig), @@ -471,23 +359,18 @@ fn main() -> Result<(), MusignError> { println!("{}", serde_json::to_string(&out)?); } else { let cbor = serde_cbor::to_vec(&out)?; - let cbor = hex::encode(cbor); - println!("{}", cbor); + println!("{}", hex::encode(cbor)); } } - Opt::Verify(cmd) => { - let pubkey = if cmd.pubkey != None { - cmd.pubkey.clone().expect("error private key string") // safe - } else if cmd.address != None { - cmd.address.unwrap() // safe - } else { - let mut pubkey = String::new(); - let ret = stdin().read_to_string(&mut pubkey); - assert!(ret.is_ok()); - pubkey.retain(|c| !c.is_whitespace()); - pubkey - }; + let pubkey = cmd.pubkey.clone().unwrap_or_else(|| { + cmd.address.clone().unwrap_or_else(|| { + let mut pubkey = String::new(); + io::stdin().read_to_string(&mut pubkey).expect("Error reading from stdin"); + pubkey.retain(|c| !c.is_whitespace()); + pubkey + }) + }); match cmd.sig_type { SigType::Ecdsa => { @@ -499,91 +382,56 @@ fn main() -> Result<(), MusignError> { println!("{}", res); } SigType::BtcLegacy => { - let ret = verifymessage(cmd.signature, pubkey, cmd.message)?; + let ret = verify_message(cmd.signature, pubkey, cmd.message)?; println!("{}", ret); } - }; + } } - Opt::MultisigSetup(mut cmd) => { - if cmd.pubkeys == None { - let mut v: Vec = Vec::new(); - let stdin = stdin(); - for line in stdin.lock().lines() { - let s = line?; - v.push(s); - } - cmd.pubkeys = Some(v); - }; + if cmd.pubkeys.is_none() { + let stdin = io::stdin(); + let pubkeys: Vec = stdin.lock().lines().collect::>()?; + cmd.pubkeys = Some(pubkeys); + } - match cmd.sig_type { - SigType::Ecdsa => { - println!("{}", serde_json::to_string(&cmd)?); - } - SigType::Schnorr => {} - SigType::BtcLegacy => {} - }; + println!("{}", serde_json::to_string(&cmd)?); } - Opt::MultisigConstruct(cmd) => { println!("{}", serde_json::to_string(&cmd)?); } - Opt::MultisigSign(mut cmd) => { - let mut v: Vec = Vec::new(); - let stdin = stdin(); - for line in stdin.lock().lines() { - let s = line?; - v.push(s); + let stdin = io::stdin(); + let lines: Vec = stdin.lock().lines().collect::>()?; + if lines.len() == 2 { + cmd.secret = Some(lines[1].clone()); } - if v.len() == 2 { - // The second stdin arg must be private key, Otherwise - // it must be provided as cli arg -s - cmd.secret = Some(v[1].clone()); - } - - let mut js: CmdMultisigConstruct = serde_json::from_str(&v[0])?; - // remove signatures before signing - let mut sigs = js.signatures; + let mut js: CmdMultisigConstruct = serde_json::from_str(&lines[0])?; js.signatures = None; - - // remove whitespaces let mut j = serde_json::to_string(&js)?; j.retain(|c| !c.is_whitespace()); - let sig = sign(cmd.secret.unwrap(), j)?; // safe - - match sigs { - Some(ref mut v) => v.push(sig.to_cmpact()), - None => { - sigs = Some(vec![sig.to_cmpact()]); - } - }; - - js.signatures = sigs; + let sig = sign(cmd.secret.unwrap(), j)?; + let mut sigs = js.signatures.unwrap_or_else(Vec::new); + sigs.push(sig.to_cmpact()); + js.signatures = Some(sigs); println!("{}", serde_json::to_string(&js)?); } - Opt::MultisigVerify => { - let multisig_reader = BufReader::new(stdin()); + let multisig_reader = BufReader::new(io::stdin()); let obj: CmdMultisigConstruct = serde_json::from_reader(multisig_reader)?; let ret = multisig_verify(obj)?; println!("{}", ret); } - Opt::MultisigCombine => { - let mut v: Vec = Vec::new(); - let stdin = stdin(); - for line in stdin.lock().lines() { - let s = line?; - let p: CmdMultisigConstruct = serde_json::from_str(&s)?; - v.push(p); - } - let ret = multisig_combine(&mut v)?; + let stdin = io::stdin(); + let mut objects: Vec = stdin.lock().lines() + .map(|line| serde_json::from_str(&line.expect("Invalid input")).expect("Failed to parse JSON")) + .collect(); + let ret = multisig_combine(&mut objects)?; println!("{}", serde_json::to_string(&ret)?); } - }; + } Ok(()) }