diff --git a/Cargo.toml b/Cargo.toml index 4390e2663..b13b9c4ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ ergo-merkle-tree = { version = "^0.15.0", path = "./ergo-merkle-tree" } ergo-rest = { version = "^0.13.0", path = "./ergo-rest" } ergo-lib = { version = "^0.28.0", path = "./ergo-lib"} k256 = { version = "0.13.1", features = ["arithmetic", "ecdsa"] } -elliptic-curve = { version = "0.12", features = ["ff"] } +elliptic-curve = { version = "0.13", features = ["ff"] } thiserror = "1" bounded-vec = { version = "^0.7.0" } bitvec = { version = "1.0.1" } diff --git a/ergo-lib/src/wallet.rs b/ergo-lib/src/wallet.rs index abe2ac038..e6305373d 100644 --- a/ergo-lib/src/wallet.rs +++ b/ergo-lib/src/wallet.rs @@ -2,6 +2,7 @@ pub mod box_selector; pub mod derivation_path; +mod deterministic; pub mod ext_pub_key; pub mod ext_secret_key; pub mod miner_fee; @@ -23,6 +24,7 @@ use signing::{sign_transaction, TxSigningError}; use thiserror::Error; use crate::chain::ergo_state_context::ErgoStateContext; +use crate::chain::transaction::reduced::reduce_tx; use crate::chain::transaction::reduced::ReducedTransaction; use crate::chain::transaction::unsigned::UnsignedTransaction; use crate::chain::transaction::Input; @@ -146,6 +148,46 @@ impl Wallet { Ok(tx_hints) } + /// Generate commitments for P2PK inputs using deterministic nonces. \ + /// See: [`Wallet::sign_transaction_deterministic`] + pub fn generate_deterministic_commitments( + &self, + reduced_tx: &ReducedTransaction, + aux_rand: &[u8], + ) -> Result { + let mut tx_hints = TransactionHintsBag::empty(); + let msg = reduced_tx.unsigned_tx.bytes_to_sign()?; + for (index, input) in reduced_tx.reduced_inputs().iter().enumerate() { + if let Some(bag) = self::deterministic::generate_commitments_for( + &*self.prover, + &input.sigma_prop, + &msg, + aux_rand, + ) { + tx_hints.add_hints_for_input(index, bag) + }; + } + Ok(tx_hints) + } + + /// Generate signatures for P2PK inputs deterministically + /// + /// Schnorr signatures need an unpredictable nonce added to the signature to avoid private key leakage. Normally this is generated using 32 bytes of entropy, but on platforms where that + /// is not available, `sign_transaction_deterministic` can be used to generate the nonce using a hash of the private key and message. \ + /// Additionally `aux_rand` can be optionally supplied with up 32 bytes of entropy. + /// # Limitations + /// Only inputs that reduce to a single public key can be signed. Thus proveDhTuple, n-of-n and t-of-n signatures can not be produced using this method + pub fn sign_transaction_deterministic( + &self, + tx_context: TransactionContext, + state_context: &ErgoStateContext, + aux_rand: &[u8], + ) -> Result { + let reduced_tx = reduce_tx(tx_context, state_context)?; + let hints = self.generate_deterministic_commitments(&reduced_tx, aux_rand)?; + self.sign_reduced_transaction(reduced_tx, Some(&hints)) + } + /// Signs a message pub fn sign_message( &self, diff --git a/ergotree-interpreter/src/sigma_protocol/dlog_protocol.rs b/ergotree-interpreter/src/sigma_protocol/dlog_protocol.rs index e26064cf6..effcf21f4 100644 --- a/ergotree-interpreter/src/sigma_protocol/dlog_protocol.rs +++ b/ergotree-interpreter/src/sigma_protocol/dlog_protocol.rs @@ -44,13 +44,18 @@ pub mod interactive_prover { use crate::sigma_protocol::crypto_utils; use crate::sigma_protocol::wscalar::Wscalar; use crate::sigma_protocol::{private_input::DlogProverInput, Challenge}; + use blake2::Blake2b; + use blake2::Digest; + use elliptic_curve::ops::MulByGenerator; use ergo_chain_types::{ ec_point::{exponentiate, generator, inverse}, EcPoint, }; + use ergotree_ir::serialization::SigmaSerializable; use ergotree_ir::sigma_protocol::dlog_group; use ergotree_ir::sigma_protocol::sigma_boolean::ProveDlog; - use k256::Scalar; + use k256::elliptic_curve::ops::Reduce; + use k256::{ProjectivePoint, Scalar}; /// Step 5 from /// For every leaf marked “simulated”, use the simulator of the sigma protocol for that leaf @@ -85,6 +90,49 @@ pub mod interactive_prover { (r.into(), FirstDlogProverMessage { a: a.into() }) } + /// Step 6 from + /// Generate first message "nonce" deterministically, optionally using auxilliary rng + /// # Safety + /// This is only intended to be used in single-signer scenarios. + /// Using this in multi-signature situations where other (untrusted) signers influence the signature can cause private key leakage by producing multiple signatures for the same message with the same nonce + pub fn first_message_deterministic( + sk: &DlogProverInput, + msg: &[u8], + aux_rand: &[u8], + ) -> (Wscalar, FirstDlogProverMessage) { + // This is based on BIP340 deterministic nonces, see: https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#default-signing + type Blake2b256 = Blake2b; + const AUX_TAG: &[u8] = b"erg/aux"; + // Perform domain seperation so alternative signature schemes don't end up producing the same nonce, for example ProveDHTuple with deterministic nonces + const NONCE_TAG: &[u8] = b"ergprovedlog/nonce"; + + let aux_rand_hash: [u8; 32] = Blake2b256::new() + .chain_update(AUX_TAG) + .chain_update(aux_rand) + .finalize() + .into(); + let mut sk_bytes = sk.w.as_scalar_ref().to_bytes(); + sk_bytes + .iter_mut() + .zip(aux_rand_hash) + .for_each(|(a, b)| *a ^= b); + #[allow(clippy::unwrap_used)] // unwrap will only fail if OOM + let hash = Blake2b256::new() + .chain_update(NONCE_TAG) + .chain_update(sk_bytes) + .chain_update(sk.public_image().h.sigma_serialize_bytes().unwrap()) + .chain_update(msg) + .finalize(); + + let r = >::reduce_bytes(&hash); + ( + r.into(), + FirstDlogProverMessage { + a: Box::new(ProjectivePoint::mul_by_generator(&r).into()), + }, + ) + } + /// Step 9 part 2 from /// compute its response "z" according to the second prover step(step 5 in whitepaper) /// of the sigma protocol given the randomness "r"(rnd) used for the commitment "a", @@ -105,7 +153,7 @@ pub mod interactive_prover { /// The function computes initial prover's commitment to randomness /// ("a" message of the sigma-protocol) based on the verifier's challenge ("e") /// and prover's response ("z") - /// + /// /// g^z = a*h^e => a = g^z/h^e pub fn compute_commitment( proposition: &ProveDlog, @@ -129,6 +177,8 @@ mod tests { use super::*; use crate::sigma_protocol::private_input::DlogProverInput; + use fiat_shamir::fiat_shamir_hash_fn; + use proptest::collection::vec; use proptest::prelude::*; proptest! { @@ -136,7 +186,6 @@ mod tests { #![proptest_config(ProptestConfig::with_cases(16))] #[test] - #[cfg(feature = "arbitrary")] fn test_compute_commitment(secret in any::(), challenge in any::()) { let pk = secret.public_image(); let (r, commitment) = interactive_prover::first_message(); @@ -144,5 +193,19 @@ mod tests { let a = interactive_prover::compute_commitment(&pk, &challenge, &second_message); prop_assert_eq!(a, *commitment.a); } + + #[test] + fn test_deterministic_commitment(secret in any::(), secret2 in any::(), message in vec(any::(), 0..100000)) { + fn sign(secret: &DlogProverInput, message: &[u8]) -> EcPoint { + let pk = secret.public_image(); + let challenge: Challenge = fiat_shamir_hash_fn(message).into(); + let (r, _) = interactive_prover::first_message_deterministic(secret, message, &[]); + let second_message = interactive_prover::second_message(secret, r, &challenge); + interactive_prover::compute_commitment(&pk, &challenge, &second_message) + } + let a = sign(&secret, &message); + let a2 = sign(&secret2, &message); + prop_assert_ne!(a, a2); + } } }