Skip to content

Commit

Permalink
Add verifiable signature (#2)
Browse files Browse the repository at this point in the history
Signature verification is required by the consensus protocol to validate peer messages
  • Loading branch information
vlopes11 authored and xgreenx committed Dec 20, 2022
1 parent 0803741 commit 70f46b6
Show file tree
Hide file tree
Showing 7 changed files with 432 additions and 194 deletions.
10 changes: 8 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
129 changes: 129 additions & 0 deletions src/message.rs
Original file line number Diff line number Diff line change
@@ -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<M>(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<Message> 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")
}
}
}
112 changes: 35 additions & 77 deletions src/public.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
/// <https://github.com/lazyledger/lazyledger-specs/blob/master/specs/data_structures.md#public-key-cryptography>
/// 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
Expand Down Expand Up @@ -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<PublicKey> for [u8; PublicKey::LEN] {
fn from(salt: PublicKey) -> [u8; PublicKey::LEN] {
salt.0.into()
Expand Down Expand Up @@ -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.
///
Expand Down Expand Up @@ -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<M>(signature: Signature, message: M) -> Result<PublicKey, Error>
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<M>(
signature: Signature,
message: M,
) -> Result<PublicKey, Error>
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<PublicKey, Error> {
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<Secp256k1PublicKey, Error> {
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)
}
}

Expand Down Expand Up @@ -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)
}
}

Expand Down
Loading

0 comments on commit 70f46b6

Please sign in to comment.