diff --git a/schnorr_fun/src/schnorr.rs b/schnorr_fun/src/schnorr.rs index ce155f59..d0f91e4b 100644 --- a/schnorr_fun/src/schnorr.rs +++ b/schnorr_fun/src/schnorr.rs @@ -30,6 +30,7 @@ pub struct Schnorr { /// /// [`NonceGen`]: crate::nonce::NonceGen nonce_challenge_bundle: NonceChallengeBundle, + application_tag: Option<[u8; 64]>, } /// Describes the kind of messages that will be signed with a [`Schnorr`] instance. @@ -48,7 +49,12 @@ pub enum MessageKind { Plain { /// You must provide a tag to separate signatures from your application /// from other applications. If two [`Schnorr`] instances are created - /// with a different `tag` then a signature valid for one will never be valid for the other. + /// with a different `tag` then a signature valid for one will never be + /// valid for the other. It will also never be valid for `Prehashed` + /// instances. The specific method of domain separation used is + /// described [here]. + /// + /// [here]: https://github.com/sipa/bips/issues/207#issuecomment-673681901 tag: &'static str, }, } @@ -107,13 +113,24 @@ where } .add_protocol_tag("BIP0340"); - if let MessageKind::Plain { tag } = msgkind { - nonce_challenge_bundle = nonce_challenge_bundle.add_application_tag(tag); - } + let application_tag = match msgkind { + MessageKind::Prehashed => None, + MessageKind::Plain { tag } => { + assert!(tag.len() <= 64); + // Only add the application tag to nonce gen since we will be + // separately adding the tag to the challenge hash in self.challenge() + nonce_challenge_bundle.nonce_gen = + nonce_challenge_bundle.nonce_gen.add_application_tag(tag); + let mut application_tag = [0u8; 64]; + application_tag[..tag.len()].copy_from_slice(tag.as_bytes()); + Some(application_tag) + } + }; Self { G: fun::G.clone(), nonce_challenge_bundle, + application_tag, } } } @@ -213,7 +230,14 @@ impl + Clone, GT> Schnorr { /// ``` pub fn challenge(&self, R: &XOnly, X: &XOnly, m: Slice<'_, S>) -> Scalar { let hash = self.nonce_challenge_bundle.challenge_hash.clone(); - let challenge = Scalar::from_hash(hash.add(R).add(X).add(&m)); + let mut hash = hash.add(R).add(X); + + if let Some(tag) = self.application_tag { + hash.update(&tag[..]); + } + + let challenge = Scalar::from_hash(hash.add(&m)); + challenge // Since the challenge pre-image is adversarially controlled we // conservatively allow for it to be zero @@ -258,6 +282,8 @@ impl + Clone, GT> Schnorr { #[cfg(test)] pub mod test { + use fun::nonce::Deterministic; + use super::*; use crate::fun::TEST_SOUNDNESS; crate::fun::test_plus_wasm! { @@ -304,5 +330,22 @@ pub mod test { assert_ne!(signature_1.R, signature_4.R); } } + + fn deterministic_nonces_for_different_message_kinds() { + use sha2::Sha256; + let schnorr_1 = Schnorr::::new(Deterministic::::default(), MessageKind::Prehashed); + let schnorr_2 = Schnorr::::new(Deterministic::::default(), MessageKind::Plain { tag: "two" }); + let schnorr_3 = Schnorr::::new(Deterministic::::default(), MessageKind::Plain { tag: "three" }); + let x = Scalar::from_str("18451f9e08af9530814243e202a4a977130e672079f5c14dcf15bd4dee723072").unwrap(); + let keypair = schnorr_1.new_keypair(x); + let message = b"foo".as_ref().mark::(); + assert_ne!(schnorr_1.sign(&keypair, message).R, schnorr_2.sign(&keypair, message).R); + assert_ne!(schnorr_1.sign(&keypair, message).R, schnorr_3.sign(&keypair, message).R); + + // make sure deterministic signatures don't change + use core::str::FromStr; + assert_eq!(schnorr_1.sign(&keypair, message), Signature::::from_str("fe9e5d0319d5d221988d6fd7fe1c4bedd2fb4465f592f1002f461503332a266977bb4a0b00c00d07072c796212cbea0957ebaaa5139143761c45d997ebe36cbe").unwrap()); + assert_eq!(schnorr_2.sign(&keypair, message), Signature::::from_str("6cad3863f4d01494ce4c40f3422e4916c616356d730bc4ffe33e386f038b328ba1dc9621e626992c2612f33cdb35f4be4badc464c1f4bf3de15517e7aedcf615").unwrap()); + } } }