From a06a8d11663e0f3af38272f8a94367c59499ef40 Mon Sep 17 00:00:00 2001 From: Nick Farrow Date: Wed, 1 Feb 2023 12:31:24 +1100 Subject: [PATCH] BlindSigner wait for N sessions * Safer nonce deletion and session disconnection logic * Do not diy gen_range, use rand * fixup docs, test and proptest --- schnorr_fun/Cargo.toml | 1 + schnorr_fun/src/blind.rs | 469 +++++++++++++++++++++++---------------- 2 files changed, 279 insertions(+), 191 deletions(-) diff --git a/schnorr_fun/Cargo.toml b/schnorr_fun/Cargo.toml index e8d7b2c4..1a484776 100644 --- a/schnorr_fun/Cargo.toml +++ b/schnorr_fun/Cargo.toml @@ -15,6 +15,7 @@ keywords = ["bitcoin", "schnorr"] [dependencies] secp256kfun = { path = "../secp256kfun", version = "0.8", default-features = false } +rand = { version = "0.8" } [dev-dependencies] secp256kfun = { path = "../secp256kfun", version = "0.8", features = ["proptest"] } diff --git a/schnorr_fun/src/blind.rs b/schnorr_fun/src/blind.rs index f2a5e156..e91e053e 100644 --- a/schnorr_fun/src/blind.rs +++ b/schnorr_fun/src/blind.rs @@ -1,10 +1,10 @@ //! Blind Schnorr Signatures //! -//! Produce a schnorr signature where the signer does not know what they have signed. +//! Produce a Schnorr signature where the signer does not know what they have signed. //! -//! ⚠ When running multiple sessions in parallel a signing server must use `blind_sign_multiple` -//! which will randomly fail on 1 out of N signing requests. This is to prevent [Wagner attack]s, -//! where parallel signing sessions can allow for a forgery. +//! ⚠ When running multiple sessions in parallel a signing server must use `sign` +//! which will randomly fail on 1 out of `max_sessions` signing requests. +//! This is to prevent [Wagner attack]s, where concurrent signing sessions can allow for a forgery. //! //! # Summary //! @@ -36,102 +36,100 @@ //! use sha2::Sha256; //! //! let nonce_gen = nonce::Synthetic::>::default(); -//! let schnorr = Schnorr::::new(nonce_gen); -//! // Generate a secret & public key for the blind signing server +//! let user_schnorr = Schnorr::::new(nonce_gen.clone()); +//! let server_schnorr = Schnorr::::new(nonce_gen); +//! // Generate a secret key for the blind signing server //! let mut secret = Scalar::random(&mut rand::thread_rng()); -//! let (public_key, secret_needs_negation) = g!(secret * G).normalize().into_point_with_even_y(); -//! secret.conditional_negate(secret_needs_negation); -//! -//! // The user wants a single blind signature but must initiate two signing sessions where one will fail. +//! // The user wants a single blind signature but must initiate two signing sessions, one will fail. //! // This is to prevent Wagner attacks where many parallel signing sessions can allow forgery. -//! // Here we request two nonces corresponding to two sessions, such that we will retrieve one signature. //! let n_sessions = 2; +//! let mut blind_signer = blind::BlindSigner::new(n_sessions, secret, server_schnorr); //! -//! // The blind signing server sends out N public nonces to the user and remembers this number of sessions. -//! let mut nonces = vec![]; +//! // The blind signing server sends out two public nonces, one received for each session //! let mut pub_nonces = vec![]; -//! for _ in 0..n_sessions { -//! let mut nonce = derive_nonce!( -//! nonce_gen => schnorr.nonce_gen(), -//! secret => secret, -//! public => [public_key] -//! ); -//! let (pub_nonce, nonce_negated) = g!(nonce * G).normalize().into_point_with_even_y(); -//! nonce.conditional_negate(nonce_negated); -//! nonces.push(nonce); -//! pub_nonces.push(pub_nonce); +//! for i in 0..n_sessions { +//! pub_nonces.push(blind_signer.gen_nonce(format!("extremely-unique-session-id-{}", i).as_bytes())); //! } //! -//! // The user is going to request a signature for a message +//! // The user is wants the server to sign a message without knowing what it is //! let message = Message::::plain("test", b"sign me up"); //! -//! // The signature requester creates blinded sessions by blinding the public keys, and recieved nonces. -//! // They also create a challenge which the server will sign. +//! // The user then blinds the received nonces and creates blind challenges for the message //! let blind_sessions: Vec<_> = pub_nonces //! .iter() //! .map(|pub_nonce| { //! blind::Blinder::blind( //! *pub_nonce, -//! public_key, +//! blind_signer.public_key(), //! message, -//! schnorr.clone(), +//! user_schnorr.clone(), //! &mut rand::thread_rng(), //! ) //! }) //! .collect(); //! -//! // The user creates a signature request for each session. Comprised of the challenge, -//! // (& currently two needs negations ...) +//! // The user creates signature requests for signatures on the blinded challenges //! let mut signature_requests: Vec<_> = blind_sessions //! .iter() //! .map(|session| session.signature_request()) //! .collect(); //! -//! // The blind signer server signs under their secret key and with the corresponding nonce for each -//! // respective signature request -//! let session_signatures = blind::blind_sign_multiple( -//! &secret, -//! nonces, -//! &mut signature_requests, -//! &mut rand::thread_rng(), -//! ); +//! dbg!(&signature_requests); +//! // Sign each signature request with the blind signer +//! let session_signatures = blind_signer.sign( +//! signature_requests[0].clone(), +//! &mut rand::thread_rng(), +//! ); +//! // Nothing is signed after the first request +//! assert_eq!(session_signatures.len(), 0); +//! +//! let session_signatures = blind_signer.sign( +//! signature_requests[1].clone(), +//! &mut rand::thread_rng(), +//! ); +//! dbg!(&session_signatures); +//! // A response is given for both requests +//! assert_eq!(session_signatures.len(), 2); //! -//! // We iterate over our signing sessions, unblinding the session which completed. -//! // This reveals an uncorrelated signature for the message that is valid under the pubkey. -//! // The server has also not seen the nonce for this signature. +//! // One of the sessions will drop out, and will not receive a signature. +//! // We can take the signature we receive in the other session, and unblind it, revealing a +//! // completely uncorrelated signature for the message that is also valid under the public key. //! for (blind_session, blind_signature) in blind_sessions.iter().zip(session_signatures) { //! match blind_signature { //! Some(blind_signature) => { //! let unblinded_signature = blind_session.unblind(blind_signature); //! // Validate the unblinded signature against the public key -//! assert!(schnorr.verify(&public_key, message, &unblinded_signature)); +//! assert!(user_schnorr.verify(&blind_signer.public_key(), message, &unblinded_signature)); //! } //! None => {} //! } //! } //! ``` +use alloc::collections::BTreeMap; + use crate::{ fun::rand_core::{CryptoRng, RngCore}, Message, Schnorr, Signature, }; use alloc::vec::Vec; +use rand::Rng; use secp256kfun::{ + derive_nonce, digest::{generic_array::typenum::U32, Digest}, g, marker::*, nonce::NonceGen, - s, Point, Scalar, G, + s, Point, Scalar, Tag, G, }; -/// Use [`BlindingTweaks`] to create the blinded public key, challenge, and nonce needed for a blinded signature +/// Apply [`BlindingTweaks`] to create the blinded challenge and nonce /// /// # Returns /// /// A blinded_nonce and a blinded_challenge; -/// Also returns a needs_negation for the blinded public key and nonce -/// The [`BlindingTweaks`] values (alpha, beta, t) may be negated to ensure even y values. -pub fn create_blinded_values<'a, H: Digest + Clone, NG>( +/// Also returns a needs_negation for the blinded nonce +pub fn apply_blinding_tweaks<'a, H: Digest + Clone, NG>( nonce: Point, public_key: Point, message: Message, @@ -161,7 +159,7 @@ pub fn create_blinded_values<'a, H: Digest + Clone, NG>( ) } -/// Unblind a blinded signature +/// Unblind a blind signature /// /// # Returns /// @@ -173,8 +171,7 @@ pub fn unblind_signature( s!(blinded_signature + alpha).public() } -/// The tweaks used for blinding the nonce, public key, and challenge -/// which are later used to unblind the signature +/// The tweaks used for blinding the nonce and challenge, later used to unblind the signature #[derive(Debug)] pub struct BlindingTweaks { /// tweak value alpha @@ -184,7 +181,7 @@ pub struct BlindingTweaks { } impl BlindingTweaks { - /// Create new [`BlindingTweaks`] from an rng source + /// Create new set of [`BlindingTweaks`] from an rng source pub fn new(rng: &mut R) -> BlindingTweaks { BlindingTweaks { alpha: Scalar::random(rng), @@ -202,21 +199,25 @@ pub struct Blinder { pub challenge: Scalar, /// blinding values pub blinding_tweaks: BlindingTweaks, + /// original public nonce received from signing server + public_nonce: Point, } impl Blinder { /// Prepare a blinded challenge for the server to sign, and blind the nonce which we /// recieved from the server. /// + /// Grinds new random [`BlindingTweaks`] until the blinded nonce does not need negation. + /// /// # Returns /// /// Returns a Blinder session, which is later used to unblind the signature once signed pub fn blind< H: Digest + Clone, - NG: NonceGen + Clone, + NG: Tag + NonceGen + Clone, R: RngCore + CryptoRng, >( - pubnonce: Point, + public_nonce: Point, public_key: Point, message: Message, schnorr: Schnorr, @@ -224,8 +225,8 @@ impl Blinder { ) -> Self { loop { let mut blinding_tweaks = BlindingTweaks::new(rng); - let (blinded_nonce, blinded_challenge, nonce_needs_negation) = create_blinded_values( - pubnonce, + let (blinded_nonce, blinded_challenge, nonce_needs_negation) = apply_blinding_tweaks( + public_nonce, public_key, message, schnorr.clone(), @@ -237,6 +238,7 @@ impl Blinder { blinded_nonce, challenge: blinded_challenge, blinding_tweaks, + public_nonce, }; } } @@ -255,7 +257,7 @@ impl Blinder { } } - /// Create a signature request using this blinding session + /// Create the signature request containing the blinded challenge and nonce /// /// # Returns /// @@ -263,193 +265,278 @@ impl Blinder { pub fn signature_request(&self) -> SignatureRequest { SignatureRequest { blind_challenge: self.challenge.clone(), + public_nonce: self.public_nonce, } } } -#[derive(Clone)] +#[derive(Clone, Debug)] /// A signature request which will be sent to the signing server pub struct SignatureRequest { /// Blinded challenge request to the signing server pub blind_challenge: Scalar, + /// Public nonce to sign under + pub public_nonce: Point, } -/// Blindly sign a challenge using a secret and a nonce +/// A blind signing server /// -/// The user should send their blind challenge for signing, -/// along with whether pubkey_needs_negation and nonce_needs_negation -/// -/// # Returns +/// Generates nonces with internal schnorr, extreme care must be taken when choosing a [`NonceGen`]. +/// Keeps track of `nonces` generated and only signs under these nonces, discarding them after use. /// -/// Returns a scalar of the unblinded signature -pub fn blind_sign( - secret: &Scalar, - nonce: Scalar, - sig_request: SignatureRequest, -) -> Scalar { - let sig = s!({ nonce } + sig_request.blind_challenge * secret).public(); - sig +/// Signature requests come in one at a time with [`BlindSigner::sign`], and none of the requests +/// are signed until there are `max_sessions` of them. Then all but one of the requests are signed +/// to prevent parrallel signing attacks (unless max_sessions is one, then signing is immediate). +pub struct BlindSigner { + schnorr: Schnorr, + max_sessions: usize, + signature_requests: Vec, + nonces: BTreeMap, Scalar>, + secret: Scalar, } -/// Safely sign multiple blind schnorr signatures concurrently -/// -/// Disconnects 1 out of N times if there is N > 1 SignatureRequests supplied. -/// Does not disconnect if only supplied one SignatureRequest -pub fn blind_sign_multiple( - secret: &Scalar, - nonces: Vec, - sig_requests: &mut Vec, - skip_i: u32, -) -> Vec>> { - // let skip_i = rng.gen_range(0..sig_requests.len()); - - sig_requests - .iter() - .zip(nonces) - .enumerate() - .map(|(i, (sig_req, nonce))| { - if i == skip_i as usize { - None - } else { - Some(blind_sign(secret, nonce.clone(), sig_req.clone())) +impl BlindSigner +where + NG: Tag + NonceGen + Clone, +{ + /// Create a new blind signer + pub fn new(max_sessions: usize, mut secret: Scalar, schnorr: Schnorr) -> Self { + // We always want to sign under the secret which corresponse to our EvenY public key. + // This avoids keeping track of needs negations. + let (_, secret_needs_negation) = g!(secret * G).normalize().into_point_with_even_y(); + secret.conditional_negate(secret_needs_negation); + + Self { + max_sessions, + signature_requests: vec![], + nonces: BTreeMap::new(), + secret, + schnorr, + } + } + + /// Get the public key for the blind signing server + pub fn public_key(&self) -> Point { + let (pk, needs_negation) = g!(self.secret * G).normalize().into_point_with_even_y(); + assert!(!needs_negation); + pk + } + + /// Generate a nonce to share with users who are requesting blind signatures + /// + /// ⚠ Extreme care must be talen with the choice of [`NonceGen`] on the servers' Schnorr, + /// in order to ensure each generated nonce is unique and never reused. + /// + /// # Returns a nonce + pub fn gen_nonce(&mut self, sid: &[u8]) -> Point { + let mut nonce = derive_nonce!( + nonce_gen => self.schnorr.nonce_gen(), + secret => self.secret, + public => [ sid ] + ); + let (pub_nonce, nonce_negated) = g!(nonce * G).normalize().into_point_with_even_y(); + nonce.conditional_negate(nonce_negated); + self.nonces.insert(pub_nonce, nonce); + pub_nonce + } + + /// Fetch the secret nonce for some public nonce and forget it + fn use_secret_nonce(&mut self, public_nonce: Point) -> Option { + let secret_nonce = match self.nonces.get(&public_nonce) { + Some(secret_nonce) => Some(secret_nonce.clone()), + // skip because we do not know about this public nonce! + None => None, + }; + if secret_nonce.is_some() { + self.nonces.remove_entry(&public_nonce); + assert!(self.nonces.get(&public_nonce).is_none()); + } + secret_nonce + } + + /// Sign a blinded challenge and delete the associated secret_nonce + /// + /// ⚠ This should never be called concurrently! Use `sign` to safely sign multiple requests. + /// + /// Forgets the corresponding secret nonce to the request's public nonce after use. + /// Returns [`None`] if we are unwilling to use the public nonce in the signature request. + /// + /// # Returns + /// + /// Returns a scalar of the unblinded signature + pub fn sign_single(&mut self, sig_request: SignatureRequest) -> Option> { + let secret_nonce = self.use_secret_nonce(sig_request.public_nonce); + match secret_nonce { + Some(secret_nonce) => { + let sig = s!(secret_nonce + sig_request.blind_challenge * self.secret).public(); + Some(sig) } - }) - .collect() + // Did not expect this nonce + None => None, + } + } + + /// Sign multiple blind schnorr signatures concurrently once enough have been requested + /// + /// Does not do any signing until max_session number of [`SignatureRequest`]s have been requested. + /// Then sign them all but randomly disconnect (return None) one of the N sessions. + /// Disconnect only occurs provided N > 1. + /// + /// # Returns + /// + /// A vector of scalar signature options + pub fn sign( + &mut self, + signature_request: SignatureRequest, + rng: &mut R, + ) -> Vec>> { + // Store this signature request + self.signature_requests.push(signature_request); + + // Have we gathered all our concurrent sessions? + if self.max_sessions > 1 && self.signature_requests.len() < self.max_sessions { + vec![] + } else { + // Choose an index to skip signing request + let skip_i = rng.gen_range(0..self.signature_requests.len() as u32); + + // Sign all the signature requests but don't store one (given there is more than one) + let signatures = self + .signature_requests + .clone() + .into_iter() + .enumerate() + .map(|(i, sig_req)| { + let sig = self.sign_single(sig_req); + // Maybe don't return the signature + if self.max_sessions > 1 && i as u32 == skip_i { + None + } else { + sig + } + }) + .collect(); + + // Clear our signature requests + self.signature_requests = vec![]; + signatures + } + } } #[cfg(test)] mod test { use super::*; use crate::{Message, Schnorr}; - use rand::Rng; + use rand::rngs::ThreadRng; use secp256kfun::{ - derive_nonce, g, - nonce::Deterministic, + nonce::{Deterministic, GlobalRng, Synthetic}, proptest::{arbitrary::any, proptest}, - Scalar, G, + Scalar, }; use sha2::Sha256; #[test] fn test_blind_unblind() { - let schnorr = - Schnorr::>::new(Deterministic::::default()); - // Generate a secret & public key for the server that will blindly sign a message - let mut secret = Scalar::random(&mut rand::thread_rng()); - let (public_key, secret_needs_negation) = - g!(secret * G).normalize().into_point_with_even_y(); - secret.conditional_negate(secret_needs_negation); + let mut rng = rand::thread_rng(); + let user_schnorr = + Schnorr::::new(Synthetic::>::default()); + let server_schnorr = + Schnorr::::new(Synthetic::>::default()); - // The user wants a single blind signature but must initiate two signing sessions where one will fail. - // This is to prevent Wagner attacks where many parallel signing sessions can allow forgery. - // Here we request two nonces corresponding to two sessions, such that we will retrieve one signature. - let n_sessions = 2; - - // The blind signing server replies with N public nonces to the user and remembers this number of sessions. - let mut nonces = vec![]; - let mut pub_nonces = vec![]; - for _ in 0..n_sessions { - let mut nonce = derive_nonce!( - nonce_gen => schnorr.nonce_gen(), - secret => secret, - public => [ b"blind-signature"] - ); - // TODO: Probably want to reintroduce a singular nonce struct? And move musig/frost to "binonce" - let (pub_nonce, nonce_negated) = g!(nonce * G).normalize().into_point_with_even_y(); - nonce.conditional_negate(nonce_negated); - nonces.push(nonce); - pub_nonces.push(pub_nonce); - } + // Generate a secret & public key for the server that will blindly sign a single message + let secret = Scalar::random(&mut rand::thread_rng()); + let n_sessions = 1; - let message = Message::::plain("test", b"sign me up"); + // The blinding server + let mut blind_signer = BlindSigner::new(n_sessions, secret, server_schnorr); - // The user creates blinded sessions by blinding the public key, and recieved nonces. - // They also create a challenge which the server will sign. - let blind_sessions: Vec<_> = pub_nonces - .iter() - .map(|pub_nonce| { - let blind_session = Blinder::blind( - *pub_nonce, - public_key, - message, - schnorr.clone(), - &mut rand::thread_rng(), - ); - blind_session - }) - .collect(); + // The blind signing server replies with a public nonce to the user + let pub_nonce = blind_signer.gen_nonce(b"turbo-unique-sid"); + let message = Message::::plain("test", b"sign me up"); - // The user creates a signature request for each session. Comprised of the challenge, - // (& currently two needs negations ...) - let signature_requests: Vec<_> = blind_sessions - .iter() - .map(|session| session.signature_request()) - .collect(); + // The user creates a blinded session which blinds the recieved nonce, + // and then creating a blind challenge which the server will sign. + let blind_session = Blinder::blind( + pub_nonce, + blind_signer.public_key(), + message, + user_schnorr.clone(), + &mut rand::thread_rng(), + ); - let rng = &mut rand::thread_rng(); + // The user creates a signature request. Comprised of the challenge and public nonce + let signature_request = blind_session.signature_request(); // The blind signer server signs under their secret key and with the corresponding nonce for each // respective signature request - assert_eq!(signature_requests.len(), n_sessions); - let session_signatures = blind_sign_multiple( - &secret, - nonces, - &mut signature_requests.clone(), - rng.gen_range(0..signature_requests.len()) as _, - ); - dbg!(&session_signatures); + let session_signature = blind_signer.sign(signature_request, &mut rng); // We recieve an option of the blinded signature from the signer, and unblind it revealing // an uncorrelated signature for the message that is valid under the pubkey. // The server has also not seen the nonce for this signature. - for (blind_session, blind_signature) in blind_sessions.iter().zip(session_signatures) { - match blind_signature { - Some(blind_signature) => { - let unblinded_signature = blind_session.unblind(blind_signature); + assert_eq!(session_signature.len(), 1); + let blind_signature = + session_signature[0].expect("max sessions of 1 should sign immediately"); - // Validate the unblinded signature against the public key - assert!(schnorr.verify(&public_key, message, &unblinded_signature)); - } - None => {} - } - } + let unblinded_signature = blind_session.unblind(blind_signature); + assert!(user_schnorr.verify(&blind_signer.public_key(), message, &unblinded_signature)); } proptest! { #[test] - fn blind_sig_prop_test(mut secret in any::(), mut nonce in any::()) { - let schnorr = Schnorr::>::new(Deterministic::::default()); + fn blind_sig_prop_test(secret in any::(), n_sessions in 1usize..10) { + let mut rng = rand::thread_rng(); + let server_schnorr = Schnorr::>::new(Deterministic::::default()); - let (public_key, secret_needs_negation) = - g!(secret * G).normalize().into_point_with_even_y(); - secret.conditional_negate(secret_needs_negation); - - let (pub_nonce, nonce_negated) = g!(nonce * G).normalize().into_point_with_even_y(); - nonce.conditional_negate(nonce_negated); + let mut blind_signer = BlindSigner::new(n_sessions, secret, server_schnorr); let message = Message::::plain("test", b"sign me up"); + let user_schnorr = Schnorr::>::new(Deterministic::::default()); + let blind_sessions: Vec<_> = (0..n_sessions).map(|i| + { + Blinder::blind( + blind_signer.gen_nonce(format!("turbo-unique-sid {}",i as u16).as_bytes()), + blind_signer.public_key(), + message, + user_schnorr.clone(), + &mut rand::thread_rng(), + ) + } + ).collect(); + + let mut blind_sigs = vec![]; + for (i, blind_session) in blind_sessions.iter().enumerate() { + let signature_request = blind_session.signature_request(); + blind_sigs = blind_signer.sign( + signature_request, + &mut rng, + ); - let blind_session = Blinder::blind( - pub_nonce, - public_key, - message, - schnorr.clone(), - &mut rand::thread_rng(), - ); - - dbg!(&secret, &public_key, &nonce, &pub_nonce); - dbg!(&blind_session); + let actually_signed = blind_sigs.iter().filter_map(|v| *v).collect::>(); + if i + 1 != n_sessions { + assert_eq!(actually_signed.len(), 0) + } else { + if n_sessions != 1 { + // We signed all but one session + assert_eq!(actually_signed.len(), n_sessions - 1); + } else { + // We signed a single session when the max sessions is one + assert_eq!(actually_signed.len(), n_sessions); + } - let signature_request = blind_session.signature_request(); - let blind_signature = blind_sign( - &secret, - nonce.clone(), - signature_request, - ); + } + } - let unblinded_signature = blind_session.unblind(blind_signature); + // Unblind and verify all the signatures + let verify_schnorr = Schnorr::>::new(Deterministic::::default()); + for (blind_session, blind_signature) in blind_sessions.iter().zip(blind_sigs) { + if let Some(blind_signature) = blind_signature { + let unblinded_signature = blind_session.unblind(blind_signature); + assert!(verify_schnorr.verify(&blind_signer.public_key(), message, &unblinded_signature)); + } + } - assert!(schnorr.verify(&public_key, message, &unblinded_signature)); } } }