diff --git a/Cargo.lock b/Cargo.lock index 2fc5f8ec..b3e8a457 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -600,6 +600,7 @@ dependencies = [ "serde_json", "sha2 0.10.6", "thiserror", + "zeroize", ] [[package]] diff --git a/coins/bitcoin/Cargo.toml b/coins/bitcoin/Cargo.toml index bc3f4f22..6c4d318e 100644 --- a/coins/bitcoin/Cargo.toml +++ b/coins/bitcoin/Cargo.toml @@ -11,6 +11,7 @@ edition = "2021" lazy_static = "1" thiserror = "1" +zeroize = "^1.5" rand_core = "0.6" sha2 = "0.10" diff --git a/coins/bitcoin/src/algorithm.rs b/coins/bitcoin/src/algorithm.rs new file mode 100644 index 00000000..c5a668d4 --- /dev/null +++ b/coins/bitcoin/src/algorithm.rs @@ -0,0 +1,131 @@ +use core::fmt::Debug; +use std::io; + +use lazy_static::lazy_static; + +use zeroize::Zeroizing; +use rand_core::{RngCore, CryptoRng}; + +use sha2::{Digest, Sha256}; +use transcript::Transcript; + +use secp256k1::schnorr::Signature; +use k256::{elliptic_curve::ops::Reduce, U256, Scalar, ProjectivePoint}; +use frost::{ + curve::{Ciphersuite, Secp256k1}, + Participant, ThresholdKeys, ThresholdView, FrostError, + algorithm::{Hram as HramTrait, Algorithm, Schnorr as FrostSchnorr}, +}; + +use crate::crypto::{x, make_even}; + +/// A BIP-340 compatible HRAm for use with the modular-frost Schnorr Algorithm. +/// +/// If passed an odd nonce, it will have the generator added until it is even. +#[derive(Clone, Copy, Debug)] +pub struct Hram {} + +lazy_static! { + static ref TAG_HASH: [u8; 32] = Sha256::digest(b"BIP0340/challenge").into(); +} + +#[allow(non_snake_case)] +impl HramTrait<Secp256k1> for Hram { + fn hram(R: &ProjectivePoint, A: &ProjectivePoint, m: &[u8]) -> Scalar { + // Convert the nonce to be even + let (R, _) = make_even(*R); + + let mut data = Sha256::new(); + data.update(*TAG_HASH); + data.update(*TAG_HASH); + data.update(x(&R)); + data.update(x(A)); + data.update(m); + + Scalar::from_uint_reduced(U256::from_be_slice(&data.finalize())) + } +} + +/// BIP-340 Schnorr signature algorithm. +/// +/// This must be used with a ThresholdKeys whose group key is even. If it is odd, this will panic. +#[derive(Clone)] +pub struct Schnorr<T: Sync + Clone + Debug + Transcript>(FrostSchnorr<Secp256k1, T, Hram>); +impl<T: Sync + Clone + Debug + Transcript> Schnorr<T> { + /// Construct a Schnorr algorithm continuing the specified transcript. + pub fn new(transcript: T) -> Schnorr<T> { + Schnorr(FrostSchnorr::new(transcript)) + } +} + +impl<T: Sync + Clone + Debug + Transcript> Algorithm<Secp256k1> for Schnorr<T> { + type Transcript = T; + type Addendum = (); + type Signature = Signature; + + fn transcript(&mut self) -> &mut Self::Transcript { + self.0.transcript() + } + + fn nonces(&self) -> Vec<Vec<ProjectivePoint>> { + self.0.nonces() + } + + fn preprocess_addendum<R: RngCore + CryptoRng>( + &mut self, + rng: &mut R, + keys: &ThresholdKeys<Secp256k1>, + ) { + self.0.preprocess_addendum(rng, keys) + } + + fn read_addendum<R: io::Read>(&self, reader: &mut R) -> io::Result<Self::Addendum> { + self.0.read_addendum(reader) + } + + fn process_addendum( + &mut self, + view: &ThresholdView<Secp256k1>, + i: Participant, + addendum: (), + ) -> Result<(), FrostError> { + self.0.process_addendum(view, i, addendum) + } + + fn sign_share( + &mut self, + params: &ThresholdView<Secp256k1>, + nonce_sums: &[Vec<<Secp256k1 as Ciphersuite>::G>], + nonces: Vec<Zeroizing<<Secp256k1 as Ciphersuite>::F>>, + msg: &[u8], + ) -> <Secp256k1 as Ciphersuite>::F { + self.0.sign_share(params, nonce_sums, nonces, msg) + } + + #[must_use] + fn verify( + &self, + group_key: ProjectivePoint, + nonces: &[Vec<ProjectivePoint>], + sum: Scalar, + ) -> Option<Self::Signature> { + self.0.verify(group_key, nonces, sum).map(|mut sig| { + // Make the R of the final signature even + let offset; + (sig.R, offset) = make_even(sig.R); + // s = r + cx. Since we added to the r, add to s + sig.s += Scalar::from(offset); + // Convert to a secp256k1 signature + Signature::from_slice(&sig.serialize()[1 ..]).unwrap() + }) + } + + fn verify_share( + &self, + verification_share: ProjectivePoint, + nonces: &[Vec<ProjectivePoint>], + share: Scalar, + ) -> Result<Vec<(Scalar, ProjectivePoint)>, ()> { + self.0.verify_share(verification_share, nonces, share) + } +} diff --git a/coins/bitcoin/src/crypto.rs b/coins/bitcoin/src/crypto.rs index 73e2077d..9dadaa4d 100644 --- a/coins/bitcoin/src/crypto.rs +++ b/coins/bitcoin/src/crypto.rs @@ -1,23 +1,14 @@ -use lazy_static::lazy_static; - -use sha2::{Digest, Sha256}; - use k256::{ - elliptic_curve::{ - ops::Reduce, - sec1::{Tag, ToEncodedPoint}, - }, - U256, Scalar, ProjectivePoint, + elliptic_curve::sec1::{Tag, ToEncodedPoint}, + ProjectivePoint, }; use bitcoin::XOnlyPublicKey; -use frost::{algorithm::Hram, curve::Secp256k1}; - /// Get the x coordinate of a non-infinity, even point. Panics on invalid input. pub fn x(key: &ProjectivePoint) -> [u8; 32] { let encoded = key.to_encoded_point(true); - assert_eq!(encoded.tag(), Tag::CompressedEvenY); + assert_eq!(encoded.tag(), Tag::CompressedEvenY, "x coordinate of odd key"); (*encoded.x().expect("point at infinity")).into() } @@ -26,7 +17,8 @@ pub fn x_only(key: &ProjectivePoint) -> XOnlyPublicKey { XOnlyPublicKey::from_slice(&x(key)).unwrap() } -/// Make a point even, returning the even version and the offset required for it to be even. +/// Make a point even by adding the generator until it is even. Returns the even point and the +/// amount of additions required. pub fn make_even(mut key: ProjectivePoint) -> (ProjectivePoint, u64) { let mut c = 0; while key.to_encoded_point(true).tag() == Tag::CompressedOddY { @@ -35,27 +27,3 @@ pub fn make_even(mut key: ProjectivePoint) -> (ProjectivePoint, u64) { } (key, c) } - -/// A BIP-340 compatible HRAm for use with the modular-frost Schnorr Algorithm. -#[derive(Clone, Copy, Debug)] -pub struct BitcoinHram {} - -lazy_static! { - static ref TAG_HASH: [u8; 32] = Sha256::digest(b"BIP0340/challenge").into(); -} - -#[allow(non_snake_case)] -impl Hram<Secp256k1> for BitcoinHram { - fn hram(R: &ProjectivePoint, A: &ProjectivePoint, m: &[u8]) -> Scalar { - let (R, _) = make_even(*R); - - let mut data = Sha256::new(); - data.update(*TAG_HASH); - data.update(*TAG_HASH); - data.update(x(&R)); - data.update(x(A)); - data.update(m); - - Scalar::from_uint_reduced(U256::from_be_slice(&data.finalize())) - } -} diff --git a/coins/bitcoin/src/lib.rs b/coins/bitcoin/src/lib.rs index cdd28b9c..2de4a062 100644 --- a/coins/bitcoin/src/lib.rs +++ b/coins/bitcoin/src/lib.rs @@ -1,5 +1,7 @@ /// Cryptographic helpers. pub mod crypto; +/// BIP-340 Schnorr signature algorithm. +pub mod algorithm; /// Wallet functionality to create transactions. pub mod wallet; /// A minimal async RPC. diff --git a/coins/bitcoin/src/tests/mod.rs b/coins/bitcoin/src/tests/mod.rs index 7d003c6c..2b0afce4 100644 --- a/coins/bitcoin/src/tests/mod.rs +++ b/coins/bitcoin/src/tests/mod.rs @@ -2,21 +2,21 @@ use rand_core::OsRng; use sha2::{Digest, Sha256}; -use secp256k1::{SECP256K1, Message, schnorr::Signature}; +use secp256k1::{SECP256K1, Message}; use bitcoin::hashes::{Hash as HashTrait, sha256::Hash}; use k256::Scalar; +use transcript::{Transcript, RecommendedTranscript}; use frost::{ curve::Secp256k1, Participant, - algorithm::IetfSchnorr, tests::{algorithm_machines, key_gen, sign}, }; -use crate::crypto::{BitcoinHram, x_only, make_even}; +use crate::{crypto::{x_only, make_even}, algorithm::Schnorr}; #[test] -fn test_signing() { +fn test_algorithm() { let mut keys = key_gen::<_, Secp256k1>(&mut OsRng); const MESSAGE: &[u8] = b"Hello, World!"; @@ -25,22 +25,18 @@ fn test_signing() { *keys = keys.offset(Scalar::from(offset)); } - let algo = IetfSchnorr::<Secp256k1, BitcoinHram>::ietf(); - let mut sig = sign( + let algo = Schnorr::<RecommendedTranscript>::new(RecommendedTranscript::new(b"bitcoin-serai sign test")); + let sig = sign( &mut OsRng, - algo, + algo.clone(), keys.clone(), - algorithm_machines(&mut OsRng, IetfSchnorr::ietf(), &keys), + algorithm_machines(&mut OsRng, algo, &keys), &Sha256::digest(MESSAGE), ); - let offset; - (sig.R, offset) = make_even(sig.R); - sig.s += Scalar::from(offset); - SECP256K1 .verify_schnorr( - &Signature::from_slice(&sig.serialize()[1 .. 65]).unwrap(), + &sig, &Message::from(Hash::hash(MESSAGE)), &x_only(&keys[&Participant::new(1).unwrap()].group_key()), ) diff --git a/coins/bitcoin/src/wallet.rs b/coins/bitcoin/src/wallet.rs index f7de1481..d1a16648 100644 --- a/coins/bitcoin/src/wallet.rs +++ b/coins/bitcoin/src/wallet.rs @@ -11,7 +11,6 @@ use k256::{elliptic_curve::sec1::ToEncodedPoint, Scalar}; use frost::{ curve::{Ciphersuite, Secp256k1}, Participant, ThresholdKeys, FrostError, - algorithm::Schnorr, sign::*, }; @@ -22,7 +21,7 @@ use bitcoin::{ OutPoint, Script, Sequence, Witness, TxIn, TxOut, PackedLockTime, Transaction, Address, }; -use crate::crypto::{BitcoinHram, make_even}; +use crate::algorithm::Schnorr; /// A spendable output. #[derive(Clone, PartialEq, Eq, Debug)] @@ -200,7 +199,7 @@ impl SignableTransaction { /// A FROST signing machine to produce a Bitcoin transaction. pub struct TransactionMachine { tx: SignableTransaction, - sigs: Vec<AlgorithmMachine<Secp256k1, Schnorr<Secp256k1, RecommendedTranscript, BitcoinHram>>>, + sigs: Vec<AlgorithmMachine<Secp256k1, Schnorr<RecommendedTranscript>>>, } impl PreprocessMachine for TransactionMachine { @@ -229,8 +228,7 @@ impl PreprocessMachine for TransactionMachine { pub struct TransactionSignMachine { tx: SignableTransaction, - sigs: - Vec<AlgorithmSignMachine<Secp256k1, Schnorr<Secp256k1, RecommendedTranscript, BitcoinHram>>>, + sigs: Vec<AlgorithmSignMachine<Secp256k1, Schnorr<RecommendedTranscript>>>, } impl SignMachine<Transaction> for TransactionSignMachine { @@ -307,9 +305,7 @@ impl SignMachine<Transaction> for TransactionSignMachine { pub struct TransactionSignatureMachine { tx: Transaction, - sigs: Vec< - AlgorithmSignatureMachine<Secp256k1, Schnorr<Secp256k1, RecommendedTranscript, BitcoinHram>>, - >, + sigs: Vec<AlgorithmSignatureMachine<Secp256k1, Schnorr<RecommendedTranscript>>>, } impl SignatureMachine<Transaction> for TransactionSignatureMachine { @@ -324,17 +320,12 @@ impl SignatureMachine<Transaction> for TransactionSignatureMachine { mut shares: HashMap<Participant, Self::SignatureShare>, ) -> Result<Transaction, FrostError> { for (input, schnorr) in self.tx.input.iter_mut().zip(self.sigs.drain(..)) { - let mut sig = schnorr.complete( + let sig = schnorr.complete( shares.iter_mut().map(|(l, shares)| (*l, shares.remove(0))).collect::<HashMap<_, _>>(), )?; - // TODO: Implement BitcoinSchnorr Algorithm to handle this - let offset; - (sig.R, offset) = make_even(sig.R); - sig.s += Scalar::from(offset); - let mut witness: Witness = Witness::new(); - witness.push(&sig.serialize()[1 .. 65]); + witness.push(sig.as_ref()); input.witness = witness; }