From 70f46b6c1447e0eca7ba71e54e14129028d468f7 Mon Sep 17 00:00:00 2001 From: Victor Lopes Date: Wed, 9 Feb 2022 22:47:13 +0100 Subject: [PATCH] Add verifiable signature (#2) Signature verification is required by the consensus protocol to validate peer messages --- .github/workflows/ci.yml | 10 +- src/lib.rs | 10 +- src/message.rs | 129 +++++++++++++++++++++++++ src/public.rs | 112 +++++++--------------- src/secret.rs | 67 +------------ src/signature.rs | 198 +++++++++++++++++++++++++++++++++++++++ tests/signature.rs | 100 ++++++++++---------- 7 files changed, 432 insertions(+), 194 deletions(-) create mode 100644 src/message.rs create mode 100644 src/signature.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b915eb397f..bd601f8a61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,17 +58,23 @@ jobs: command: build args: --verbose --target thumbv6m-none-eabi --no-default-features --features serde-types-minimal + - name: Build no-std random + uses: actions-rs/cargo@v1 + with: + command: build + args: --verbose --target thumbv6m-none-eabi --no-default-features --features random + - name: Run tests all features uses: actions-rs/cargo@v1 with: command: test - args: --verbose --all-features + args: --verbose --all-features --release - name: Run tests serde uses: actions-rs/cargo@v1 with: command: test - args: --verbose --features serde-types + args: --verbose --features serde-types --release - name: Run tests no-std uses: actions-rs/cargo@v1 diff --git a/src/lib.rs b/src/lib.rs index 81fe830f71..d9fc2be648 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,16 +2,20 @@ #![cfg_attr(not(feature = "std"), no_std)] #![warn(missing_docs)] +// Wrong clippy convention; check +// https://rust-lang.github.io/api-guidelines/naming.html +#![allow(clippy::wrong_self_convention)] mod error; mod hasher; +mod message; mod public; mod secret; +mod signature; pub use error::Error; pub use hasher::Hasher; +pub use message::Message; pub use public::PublicKey; pub use secret::SecretKey; - -/// Signature of a message -pub type Signature = fuel_types::Bytes64; +pub use signature::Signature; diff --git a/src/message.rs b/src/message.rs new file mode 100644 index 0000000000..0e0b7bb7e1 --- /dev/null +++ b/src/message.rs @@ -0,0 +1,129 @@ +use crate::Hasher; + +pub use fuel_types::Bytes32; + +use core::fmt; +use core::ops::Deref; + +/// Normalized signature message +#[derive(Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr( + feature = "serde-types-minimal", + derive(serde::Serialize, serde::Deserialize) +)] +#[repr(transparent)] +pub struct Message(Bytes32); + +impl Message { + /// Memory length of the type + pub const LEN: usize = Bytes32::LEN; + + /// Normalize a message for signature + pub fn new(message: M) -> Self + where + M: AsRef<[u8]>, + { + Self(Hasher::hash(message)) + } + + /// Add a conversion from arbitrary slices into owned + /// + /// # Safety + /// + /// There is no guarantee the provided bytes will be the product of a cryptographically secure + /// hash. Using insecure messages might compromise the security of the signature. + pub unsafe fn from_bytes_unchecked(bytes: [u8; Self::LEN]) -> Self { + Self(bytes.into()) + } + + /// Add a conversion from arbitrary slices into owned + /// + /// # Safety + /// + /// This function will not panic if the length of the slice is smaller than + /// `Self::LEN`. Instead, it will cause undefined behavior and read random + /// disowned bytes. + /// + /// This function extends the unsafety of [`Self::from_bytes_unchecked`]. + pub unsafe fn from_slice_unchecked(bytes: &[u8]) -> Self { + Self(Bytes32::from_slice_unchecked(bytes)) + } + + /// Copy-free reference cast + /// + /// # Safety + /// + /// Inputs smaller than `Self::LEN` will cause undefined behavior. + /// + /// This function extends the unsafety of [`Self::from_bytes_unchecked`]. + pub unsafe fn as_ref_unchecked(bytes: &[u8]) -> &Self { + // The interpreter will frequently make references to keys and values using + // logically checked slices. + // + // This function will avoid unnecessary copy to owned slices for the interpreter + // access + &*(bytes.as_ptr() as *const Self) + } +} + +impl Deref for Message { + type Target = [u8; Message::LEN]; + + fn deref(&self) -> &[u8; Message::LEN] { + self.0.deref() + } +} + +impl AsRef<[u8]> for Message { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl From for [u8; Message::LEN] { + fn from(message: Message) -> [u8; Message::LEN] { + message.0.into() + } +} + +impl fmt::LowerHex for Message { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl fmt::UpperHex for Message { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl fmt::Debug for Message { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +impl fmt::Display for Message { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +#[cfg(feature = "std")] +mod use_std { + use crate::Message; + + use secp256k1::Message as Secp256k1Message; + + impl Message { + pub(crate) fn to_secp(&self) -> Secp256k1Message { + // The only validation performed by `Message::from_slice` is to check if it is + // 32 bytes. This validation exists to prevent users from signing + // non-hashed messages, which is a severe violation of the protocol + // security. + debug_assert_eq!(Self::LEN, secp256k1::constants::MESSAGE_SIZE); + Secp256k1Message::from_slice(self.as_ref()).expect("Unreachable error") + } + } +} diff --git a/src/public.rs b/src/public.rs index b200b1647a..62010e959b 100644 --- a/src/public.rs +++ b/src/public.rs @@ -5,10 +5,7 @@ use fuel_types::{Bytes32, Bytes64}; use core::fmt; use core::ops::Deref; -/// Signature public key -/// -/// The compression scheme is described in -/// +/// Asymmetric public key #[derive(Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[repr(transparent)] // TODO serde implementation blocked by https://github.com/FuelLabs/fuel-types/issues/13 @@ -79,6 +76,12 @@ impl AsRef<[u8]> for PublicKey { } } +impl AsMut<[u8]> for PublicKey { + fn as_mut(&mut self) -> &mut [u8] { + self.0.as_mut() + } +} + impl From for [u8; PublicKey::LEN] { fn from(salt: PublicKey) -> [u8; PublicKey::LEN] { salt.0.into() @@ -112,39 +115,21 @@ impl fmt::Display for PublicKey { #[cfg(feature = "std")] mod use_std { use super::*; - use crate::{Error, SecretKey, Signature}; + use crate::{Error, SecretKey}; - use secp256k1::{ - recovery::{RecoverableSignature, RecoveryId}, - {Error as Secp256k1Error, Message, PublicKey as Secp256k1PublicKey, Secp256k1}, - }; + use secp256k1::{Error as Secp256k1Error, PublicKey as Secp256k1PublicKey, Secp256k1}; use core::borrow::Borrow; use core::str; const UNCOMPRESSED_PUBLIC_KEY_SIZE: usize = 65; - impl PublicKey { - /// Convert an uncompressed public key representation into self. - /// - /// # Safety - /// - /// Will not check elliptic-curve correctness. - pub unsafe fn from_uncompressed_unchecked( - pk: [u8; UNCOMPRESSED_PUBLIC_KEY_SIZE], - ) -> PublicKey { - debug_assert_eq!( - UNCOMPRESSED_PUBLIC_KEY_SIZE, - secp256k1::constants::UNCOMPRESSED_PUBLIC_KEY_SIZE - ); - - // Ignore the first byte of the compressed flag - let pk = &pk[1..]; - - // Safety: compile-time assertion of size correctness - Self::from_slice_unchecked(pk) - } + // Internal secp256k1 identifier for uncompressed point + // + // https://github.com/rust-bitcoin/rust-secp256k1/blob/ecb62612b57bf3aa8d8017d611d571f86bfdb5dd/secp256k1-sys/depend/secp256k1/include/secp256k1.h#L196 + const SECP_UNCOMPRESSED_FLAG: u8 = 4; + impl PublicKey { /// Check if the provided slice represents a public key that is in the /// curve. /// @@ -178,58 +163,33 @@ mod use_std { unsafe { Self::is_slice_in_curve_unchecked(self.as_ref()) } } - /// Recover the public key from a signature performed with - /// [`SecretKey::sign`] - pub fn recover(signature: Signature, message: M) -> Result - where - M: AsRef<[u8]>, - { - let message = SecretKey::normalize_message(message); + pub(crate) fn from_secp(pk: &Secp256k1PublicKey) -> PublicKey { + debug_assert_eq!( + UNCOMPRESSED_PUBLIC_KEY_SIZE, + secp256k1::constants::UNCOMPRESSED_PUBLIC_KEY_SIZE + ); - Self::_recover(signature, &message) - } + let pk = pk.serialize_uncompressed(); - /// Recover the public key from a signature performed with - /// [`SecretKey::sign`] - /// - /// # Safety - /// - /// The protocol expects the message to be the result of a hash - - /// otherwise, its verification is malleable. The output of the - /// hash must be 32 bytes. - /// - /// The unsafe directive of this function is related only to the message - /// input. It might fail if the signature is inconsistent. - pub unsafe fn recover_unchecked( - signature: Signature, - message: M, - ) -> Result - where - M: AsRef<[u8]>, - { - let message = SecretKey::cast_message(message.as_ref()); - - Self::_recover(signature, message) - } + debug_assert_eq!(SECP_UNCOMPRESSED_FLAG, pk[0]); + + // Ignore the first byte of the compression flag + let pk = &pk[1..]; - fn _recover(mut signature: Signature, message: &Message) -> Result { - let v = ((signature.as_mut()[32] & 0x90) >> 7) as i32; - signature.as_mut()[32] &= 0x7f; + // Safety: compile-time assertion of size correctness + unsafe { Self::from_slice_unchecked(pk) } + } - let v = RecoveryId::from_i32(v)?; - let signature = RecoverableSignature::from_compact(signature.as_ref(), v)?; + pub(crate) fn _to_secp(&self) -> Result { + let mut pk = [SECP_UNCOMPRESSED_FLAG; UNCOMPRESSED_PUBLIC_KEY_SIZE]; - let pk = Secp256k1::new() - .recover(message, &signature)? - .serialize_uncompressed(); + debug_assert_eq!(SECP_UNCOMPRESSED_FLAG, pk[0]); - // Ignore the first byte of the compressed flag - let pk = &pk[1..]; + (&mut pk[1..]).copy_from_slice(self.as_ref()); - // Safety: secp256k1 protocol specifies 65 bytes output - let pk = unsafe { Bytes64::from_slice_unchecked(pk) }; + let pk = Secp256k1PublicKey::from_slice(&pk)?; - Ok(Self(pk)) + Ok(pk) } } @@ -264,11 +224,9 @@ mod use_std { // Copy here is unavoidable since there is no API in secp256k1 to create // uncompressed keys directly - let public = - Secp256k1PublicKey::from_secret_key(&secp, secret).serialize_uncompressed(); + let public = Secp256k1PublicKey::from_secret_key(&secp, secret); - // Safety: FFI is guaranteed to return valid public key. - unsafe { PublicKey::from_uncompressed_unchecked(public) } + Self::from_secp(&public) } } diff --git a/src/secret.rs b/src/secret.rs index 575851e42c..c74d4d1770 100644 --- a/src/secret.rs +++ b/src/secret.rs @@ -3,7 +3,7 @@ use fuel_types::Bytes32; use core::fmt; use core::ops::Deref; -/// Signature secret key +/// Asymmetric secret key #[derive(Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr( feature = "serde-types-minimal", @@ -103,9 +103,9 @@ impl fmt::Display for SecretKey { #[cfg(feature = "std")] mod use_std { use super::*; - use crate::{Error, Hasher, PublicKey, Signature}; + use crate::{Error, PublicKey}; - use secp256k1::{Error as Secp256k1Error, Message, Secp256k1, SecretKey as Secp256k1SecretKey}; + use secp256k1::{Error as Secp256k1Error, SecretKey as Secp256k1SecretKey}; use core::borrow::Borrow; use core::str; @@ -117,49 +117,6 @@ mod use_std { }; impl SecretKey { - /// Sign a given message and compress the `v` to the signature - /// - /// The compression scheme is described in - /// - pub fn sign(&self, message: M) -> Signature - where - M: AsRef<[u8]>, - { - let message = Self::normalize_message(message); - - self._sign(&message) - } - - /// Sign a given message and compress the `v` to the signature - /// - /// The compression scheme is described in - /// - /// # Safety - /// - /// The protocol expects the message to be the result of a hash - - /// otherwise, its verification is malleable. The output of the - /// hash must be 32 bytes. - pub unsafe fn sign_unchecked(&self, message: M) -> Signature - where - M: AsRef<[u8]>, - { - let message = SecretKey::cast_message(message.as_ref()); - - self._sign(message) - } - - fn _sign(&self, message: &Message) -> Signature { - let secret = self.borrow(); - - let signature = Secp256k1::new().sign_recoverable(message, secret); - let (v, mut signature) = signature.serialize_compact(); - - let v = v.to_i32(); - signature[32] |= (v << 7) as u8; - - signature.into() - } - /// Create a new random secret #[cfg(feature = "random")] pub fn random(rng: &mut R) -> Self @@ -198,24 +155,6 @@ mod use_std { Self(secret) } - pub(crate) fn normalize_message(message: M) -> Message - where - M: AsRef<[u8]>, - { - let message = Hasher::hash(message); - - // The only validation performed by `Message::from_slice` is to check if it is - // 32 bytes. This validation exists to prevent users from signing - // non-hashed messages, which is a severe violation of the protocol - // security. - debug_assert_eq!(Hasher::OUTPUT_LEN, secp256k1::constants::MESSAGE_SIZE); - Message::from_slice(message.as_ref()).expect("Unreachable error") - } - - pub(crate) unsafe fn cast_message(message: &[u8]) -> &Message { - &*(message.as_ptr() as *const Message) - } - /// Check if the provided slice represents a scalar that fits the field. /// /// # Safety diff --git a/src/signature.rs b/src/signature.rs new file mode 100644 index 0000000000..ad10744515 --- /dev/null +++ b/src/signature.rs @@ -0,0 +1,198 @@ +use fuel_types::Bytes64; + +use core::fmt; +use core::ops::Deref; + +#[derive(Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +/// Secp256k1 signature implementation +pub struct Signature(Bytes64); + +impl Signature { + /// Memory length of the type + pub const LEN: usize = Bytes64::LEN; + + /// Add a conversion from arbitrary slices into owned + /// + /// # Safety + /// + /// There is no guarantee the provided bytes will be a valid signature. Internally, some FFI + /// calls to `secp256k1` are performed and we might have undefined behavior in case the bytes + /// are not canonically encoded to a valid `secp256k1` signature. + pub unsafe fn from_bytes_unchecked(bytes: [u8; Self::LEN]) -> Self { + Self(bytes.into()) + } + + /// Add a conversion from arbitrary slices into owned + /// + /// # Safety + /// + /// This function will not panic if the length of the slice is smaller than + /// `Self::LEN`. Instead, it will cause undefined behavior and read random + /// disowned bytes. + /// + /// There is no guarantee the provided bytes will be a valid signature. Internally, some FFI + /// calls to `secp256k1` are performed and we might have undefined behavior in case the bytes + /// are not canonically encoded to a valid `secp256k1` signature. + pub unsafe fn from_slice_unchecked(bytes: &[u8]) -> Self { + Self(Bytes64::from_slice_unchecked(bytes)) + } + + /// Copy-free reference cast + /// + /// There is no guarantee the provided bytes will fit the field. + /// + /// # Safety + /// + /// Inputs smaller than `Self::LEN` will cause undefined behavior. + pub unsafe fn as_ref_unchecked(bytes: &[u8]) -> &Self { + // The interpreter will frequently make references to keys and values using + // logically checked slices. + // + // This function will avoid unnecessary copy to owned slices for the interpreter + // access + &*(bytes.as_ptr() as *const Self) + } +} + +impl Deref for Signature { + type Target = [u8; Signature::LEN]; + + fn deref(&self) -> &[u8; Signature::LEN] { + self.0.deref() + } +} + +impl AsRef<[u8]> for Signature { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl AsMut<[u8]> for Signature { + fn as_mut(&mut self) -> &mut [u8] { + self.0.as_mut() + } +} + +impl From for [u8; Signature::LEN] { + fn from(salt: Signature) -> [u8; Signature::LEN] { + salt.0.into() + } +} + +impl fmt::LowerHex for Signature { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl fmt::UpperHex for Signature { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl fmt::Debug for Signature { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +impl fmt::Display for Signature { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +#[cfg(feature = "std")] +mod use_std { + use crate::{Error, Message, PublicKey, SecretKey, Signature}; + + use secp256k1::recovery::{RecoverableSignature as SecpRecoverableSignature, RecoveryId}; + use secp256k1::Secp256k1; + + use std::borrow::Borrow; + + impl Signature { + // Internal API - this isn't meant to be made public because some assumptions and pre-checks + // are performed prior to this call + pub(crate) fn to_secp(&mut self) -> SecpRecoverableSignature { + let v = (self.as_mut()[32] >> 7) as i32; + + self.truncate_recovery_id(); + + let v = RecoveryId::from_i32(v) + .unwrap_or_else(|_| RecoveryId::from_i32(0).expect("0 is infallible recovery ID")); + + let signature = SecpRecoverableSignature::from_compact(self.as_ref(), v) + .unwrap_or_else(|_| { + SecpRecoverableSignature::from_compact(&[0u8; 64], v) + .expect("Zeroed signature is infallible") + }); + + signature + } + + pub(crate) fn from_secp(signature: SecpRecoverableSignature) -> Self { + let (v, mut signature) = signature.serialize_compact(); + + let v = v.to_i32(); + + signature[32] |= (v << 7) as u8; + + // Safety: the security of this call reflects the security of secp256k1 FFI + unsafe { Signature::from_bytes_unchecked(signature) } + } + + /// Truncate the recovery id from the signature, producing a valid `secp256k1` + /// representation. + pub(crate) fn truncate_recovery_id(&mut self) { + self.as_mut()[32] &= 0x7f; + } + + /// Sign a given message and compress the `v` to the signature + /// + /// The compression scheme is described in + /// + pub fn sign(secret: &SecretKey, message: &Message) -> Self { + let secret = secret.borrow(); + let message = message.to_secp(); + + let signature = Secp256k1::signing_only().sign_recoverable(&message, secret); + + Signature::from_secp(signature) + } + + /// Recover the public key from a signature performed with + /// [`Signature::sign`] + /// + /// It takes the signature as owned because this operation is not idempotent. The taken + /// signature will not be recoverable. Signatures are meant to be single use, so this + /// avoids unnecessary copy. + pub fn recover(mut self, message: &Message) -> Result { + let signature = self.to_secp(); + let message = message.to_secp(); + + let pk = Secp256k1::new() + .recover(&message, &signature) + .map(|pk| PublicKey::from_secp(&pk))?; + + Ok(pk) + } + + /// Verify a signature produced by [`Signature::sign`] + /// + /// It takes the signature as owned because this operation is not idempotent. The taken + /// signature will not be recoverable. Signatures are meant to be single use, so this + /// avoids unnecessary copy. + pub fn verify(self, pk: &PublicKey, message: &Message) -> Result<(), Error> { + // TODO evaluate if its worthy to use native verify + // + // https://github.com/FuelLabs/fuel-crypto/issues/4 + + self.recover(message) + .and_then(|pk_p| (pk == &pk_p).then(|| ()).ok_or(Error::InvalidSignature)) + } + } +} diff --git a/tests/signature.rs b/tests/signature.rs index 993bc820e4..cab3059494 100644 --- a/tests/signature.rs +++ b/tests/signature.rs @@ -1,41 +1,70 @@ -use fuel_crypto::{Error, Hasher, PublicKey, SecretKey, Signature}; +use fuel_crypto::{Error, Message, PublicKey, SecretKey, Signature}; use rand::rngs::StdRng; use rand::SeedableRng; #[test] -fn ecrecover() { +fn recover() { let rng = &mut StdRng::seed_from_u64(8586); let message = b"A beast can never be as cruel as a human being, so artistically, so picturesquely cruel."; - let secret = SecretKey::random(rng); - let public = secret.public_key(); + for _ in 0..100 { + let message = Message::new(message); + + let secret = SecretKey::random(rng); + let public = secret.public_key(); + + let signature = Signature::sign(&secret, &message); + let recover = signature.recover(&message).expect("Failed to recover PK"); + + assert_eq!(public, recover); + } +} + +#[test] +fn verify() { + let rng = &mut StdRng::seed_from_u64(8586); + + let message = + b"Music expresses that which cannot be put into words and that which cannot remain silent."; + + for _ in 0..100 { + let message = Message::new(message); + + let secret = SecretKey::random(rng); + let public = secret.public_key(); - let signature = secret.sign(message); - let recover = PublicKey::recover(signature, message).expect("Failed to recover PK"); + let signature = Signature::sign(&secret, &message); - assert_eq!(public, recover); + signature + .verify(&public, &message) + .expect("Failed to verify signature"); + } } #[test] -fn ecrecover_corrupted_signature() { +fn corrupted_signature() { let rng = &mut StdRng::seed_from_u64(8586); let message = b"When life itself seems lunatic, who knows where madness lies?"; + let message = Message::new(message); let secret = SecretKey::random(rng); let public = secret.public_key(); - let signature = secret.sign(message); + let signature = Signature::sign(&secret, &message); + // Tamper, bit by bit, the signature and public key. + // + // The recover and verify operations should fail in all cases. (0..Signature::LEN).for_each(|i| { (0..7).fold(1u8, |m, _| { let mut s = signature; - s[i] ^= m; + s.as_mut()[i] ^= m; - match PublicKey::recover(s, message) { + match s.recover(&message) { Ok(pk) => assert_ne!(public, pk), Err(Error::InvalidSignature) => (), Err(e) => panic!("Unexpected error: {}", e), @@ -44,51 +73,26 @@ fn ecrecover_corrupted_signature() { m << 1 }); }); -} - -#[test] -fn ecrecover_unchecked() { - let rng = &mut StdRng::seed_from_u64(8586); - - let message = - b"Music expresses that which cannot be put into words and that which cannot remain silent."; - let message = Hasher::hash(message); - - let secret = SecretKey::random(rng); - let public = secret.public_key(); - - let signature = unsafe { secret.sign_unchecked(message) }; - let recover = - unsafe { PublicKey::recover_unchecked(signature, message).expect("Failed to recover PK") }; - assert_eq!(public, recover); -} - -#[test] -fn ecrecover_unchecked_corrupted_signature() { - let rng = &mut StdRng::seed_from_u64(8586); + (0..Signature::LEN).for_each(|i| { + (0..7).fold(1u8, |m, _| { + let mut s = signature; - let message = b"All things excellent are as difficult as they are rare."; - let message = Hasher::hash(message); + s.as_mut()[i] ^= m; - let secret = SecretKey::random(rng); - let public = secret.public_key(); + assert!(s.verify(&public, &message).is_err()); - let signature = unsafe { secret.sign_unchecked(message) }; + m << 1 + }); + }); - (0..Signature::LEN).for_each(|i| { + (0..PublicKey::LEN).for_each(|i| { (0..7).fold(1u8, |m, _| { - let mut s = signature; + let mut p = public; - s[i] ^= m; + p.as_mut()[i] ^= m; - let recover = unsafe { PublicKey::recover_unchecked(s, message) }; - - match recover { - Ok(pk) => assert_ne!(public, pk), - Err(Error::InvalidSignature) => (), - Err(e) => panic!("Unexpected error: {}", e), - } + assert!(signature.verify(&p, &message).is_err()); m << 1 });