diff --git a/linera-base/src/crypto.rs b/linera-base/src/crypto.rs deleted file mode 100644 index a4ca0ce3f60a..000000000000 --- a/linera-base/src/crypto.rs +++ /dev/null @@ -1,842 +0,0 @@ -// Copyright (c) Facebook, Inc. and its affiliates. -// Copyright (c) Zefchain Labs, Inc. -// SPDX-License-Identifier: Apache-2.0 - -//! Define the cryptographic primitives used by the Linera protocol. - -use std::{borrow::Cow, fmt, io, num::ParseIntError, str::FromStr}; - -use alloy_primitives::{FixedBytes, Keccak256, B256}; -use ed25519_dalek::{self as dalek, Signer, Verifier}; -use linera_witty::{ - GuestPointer, HList, InstanceWithMemory, Layout, Memory, Runtime, RuntimeError, RuntimeMemory, - WitLoad, WitStore, WitType, -}; -use serde::{Deserialize, Serialize}; -use thiserror::Error; -#[cfg(with_testing)] -use { - proptest::{ - collection::{vec, VecStrategy}, - prelude::Arbitrary, - strategy::{self, Strategy}, - }, - std::ops::RangeInclusive, -}; - -use crate::doc_scalar; - -/// A signature key-pair. -pub struct KeyPair(dalek::SigningKey); - -/// A signature public key. -#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Hash)] -pub struct PublicKey(pub [u8; dalek::PUBLIC_KEY_LENGTH]); - -type HasherOutputSize = FixedBytes<32>; - -/// A Keccak256 value. -#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Copy, Hash)] -#[cfg_attr(with_testing, derive(Default))] -pub struct CryptoHash(B256); - -/// A vector of cryptographic hashes. -/// This is used to represent a hash of a list of hashes. -#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Hash, Serialize, Deserialize)] -#[cfg_attr(with_testing, derive(Default))] -pub struct CryptoHashVec(pub Vec); - -impl<'de> BcsHashable<'de> for CryptoHashVec {} - -/// A signature value. -#[derive(Eq, PartialEq, Copy, Clone)] -pub struct Signature(pub dalek::Signature); - -/// Error type for cryptographic errors. -#[derive(Error, Debug)] -#[allow(missing_docs)] -pub enum CryptoError { - #[error("Signature for object {type_name} is not valid: {error}")] - InvalidSignature { error: String, type_name: String }, - #[error("Signature for object {type_name} is missing")] - MissingSignature { type_name: String }, - #[error(transparent)] - NonHexDigits(#[from] hex::FromHexError), - #[error( - "Byte slice has length {0} but a `CryptoHash` requires exactly {expected} bytes", - expected = HasherOutputSize::len_bytes(), - )] - IncorrectHashSize(usize), - #[error( - "Byte slice has length {0} but a `PublicKey` requires exactly {expected} bytes", - expected = dalek::PUBLIC_KEY_LENGTH, - )] - IncorrectPublicKeySize(usize), - #[error("Could not parse integer: {0}")] - ParseIntError(#[from] ParseIntError), -} - -impl PublicKey { - /// A fake public key used for testing. - #[cfg(with_testing)] - pub fn test_key(name: u8) -> PublicKey { - let addr = [name; dalek::PUBLIC_KEY_LENGTH]; - PublicKey(addr) - } -} - -#[cfg(with_getrandom)] -/// Wrapper around [`rand::CryptoRng`] and [`rand::RngCore`]. -pub trait CryptoRng: rand::CryptoRng + rand::RngCore + Send + Sync {} - -#[cfg(with_getrandom)] -impl CryptoRng for T {} - -#[cfg(with_getrandom)] -impl From> for Box { - fn from(seed: Option) -> Self { - use rand::SeedableRng; - - match seed { - Some(seed) => Box::new(rand::rngs::StdRng::seed_from_u64(seed)), - None => Box::new(rand::rngs::OsRng), - } - } -} - -impl KeyPair { - #[cfg(all(with_getrandom, with_testing))] - /// Generates a new key-pair. - pub fn generate() -> Self { - let mut rng = rand::rngs::OsRng; - Self::generate_from(&mut rng) - } - - #[cfg(with_getrandom)] - /// Generates a new key-pair from the given RNG. Use with care. - pub fn generate_from(rng: &mut R) -> Self { - let keypair = dalek::SigningKey::generate(rng); - KeyPair(keypair) - } - - /// Obtains the public key of a key-pair. - pub fn public(&self) -> PublicKey { - PublicKey(self.0.verifying_key().to_bytes()) - } - - /// Copies the key-pair, **including the secret key**. - /// - /// The `Clone` and `Copy` traits are deliberately not implemented for `KeyPair` to prevent - /// accidental copies of secret keys. - pub fn copy(&self) -> KeyPair { - KeyPair(self.0.clone()) - } -} - -impl Serialize for PublicKey { - fn serialize(&self, serializer: S) -> Result - where - S: serde::ser::Serializer, - { - if serializer.is_human_readable() { - serializer.serialize_str(&self.to_string()) - } else { - serializer.serialize_newtype_struct("PublicKey", &self.0) - } - } -} - -impl<'de> Deserialize<'de> for PublicKey { - fn deserialize(deserializer: D) -> Result - where - D: serde::de::Deserializer<'de>, - { - if deserializer.is_human_readable() { - let s = String::deserialize(deserializer)?; - let value = Self::from_str(&s).map_err(serde::de::Error::custom)?; - Ok(value) - } else { - #[derive(Deserialize)] - #[serde(rename = "PublicKey")] - struct Foo([u8; dalek::PUBLIC_KEY_LENGTH]); - - let value = Foo::deserialize(deserializer)?; - Ok(Self(value.0)) - } - } -} - -impl Serialize for CryptoHash { - fn serialize(&self, serializer: S) -> Result - where - S: serde::ser::Serializer, - { - if serializer.is_human_readable() { - serializer.serialize_str(&self.to_string()) - } else { - serializer.serialize_newtype_struct("CryptoHash", &self.as_bytes().0) - } - } -} - -impl<'de> Deserialize<'de> for CryptoHash { - fn deserialize(deserializer: D) -> Result - where - D: serde::de::Deserializer<'de>, - { - if deserializer.is_human_readable() { - let s = String::deserialize(deserializer)?; - let value = Self::from_str(&s).map_err(serde::de::Error::custom)?; - Ok(value) - } else { - #[derive(Deserialize)] - #[serde(rename = "CryptoHash")] - struct Foo([u8; 32]); - - let value = Foo::deserialize(deserializer)?; - Ok(Self(value.0.into())) - } - } -} - -impl Serialize for KeyPair { - fn serialize(&self, serializer: S) -> Result - where - S: serde::ser::Serializer, - { - // This is only used for JSON configuration. - assert!(serializer.is_human_readable()); - serializer.serialize_str(&hex::encode(self.0.to_bytes())) - } -} - -impl<'de> Deserialize<'de> for KeyPair { - fn deserialize(deserializer: D) -> Result - where - D: serde::de::Deserializer<'de>, - { - // This is only used for JSON configuration. - assert!(deserializer.is_human_readable()); - let s = String::deserialize(deserializer)?; - let value = hex::decode(s).map_err(serde::de::Error::custom)?; - let key = - dalek::SigningKey::from_bytes(value[..].try_into().map_err(serde::de::Error::custom)?); - Ok(KeyPair(key)) - } -} - -impl Serialize for Signature { - fn serialize(&self, serializer: S) -> Result - where - S: serde::ser::Serializer, - { - if serializer.is_human_readable() { - serializer.serialize_str(&hex::encode(self.0.to_bytes())) - } else { - serializer.serialize_newtype_struct("Signature", &self.0) - } - } -} - -impl<'de> Deserialize<'de> for Signature { - fn deserialize(deserializer: D) -> Result - where - D: serde::de::Deserializer<'de>, - { - if deserializer.is_human_readable() { - let s = String::deserialize(deserializer)?; - let value = hex::decode(s).map_err(serde::de::Error::custom)?; - let sig = - dalek::Signature::try_from(value.as_slice()).map_err(serde::de::Error::custom)?; - Ok(Signature(sig)) - } else { - #[derive(Deserialize)] - #[serde(rename = "Signature")] - struct Foo(dalek::Signature); - - let value = Foo::deserialize(deserializer)?; - Ok(Self(value.0)) - } - } -} - -impl FromStr for PublicKey { - type Err = CryptoError; - - fn from_str(s: &str) -> Result { - let value = hex::decode(s)?; - (value.as_slice()).try_into() - } -} - -impl TryFrom<&[u8]> for PublicKey { - type Error = CryptoError; - - fn try_from(value: &[u8]) -> Result { - if value.len() != dalek::PUBLIC_KEY_LENGTH { - return Err(CryptoError::IncorrectPublicKeySize(value.len())); - } - let mut pubkey = [0u8; dalek::PUBLIC_KEY_LENGTH]; - pubkey.copy_from_slice(value); - Ok(PublicKey(pubkey)) - } -} - -impl From<[u64; 4]> for PublicKey { - fn from(integers: [u64; 4]) -> Self { - PublicKey(u64_array_to_le_bytes(integers)) - } -} - -impl From for [u64; 4] { - fn from(pub_key: PublicKey) -> Self { - le_bytes_to_u64_array(&pub_key.0) - } -} - -impl FromStr for CryptoHash { - type Err = CryptoError; - - fn from_str(s: &str) -> Result { - let value = hex::decode(s)?; - (value.as_slice()).try_into() - } -} - -impl TryFrom<&[u8]> for CryptoHash { - type Error = CryptoError; - - fn try_from(value: &[u8]) -> Result { - if value.len() != B256::len_bytes() { - return Err(CryptoError::IncorrectHashSize(value.len())); - } - Ok(Self(B256::from_slice(value))) - } -} - -impl From<[u64; 4]> for CryptoHash { - fn from(integers: [u64; 4]) -> Self { - CryptoHash(u64_array_to_be_bytes(integers).into()) - } -} - -impl From for [u64; 4] { - fn from(crypto_hash: CryptoHash) -> Self { - be_bytes_to_u64_array(crypto_hash.0.as_ref()) - } -} - -impl fmt::Display for Signature { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = hex::encode(self.0.to_bytes()); - write!(f, "{}", s) - } -} - -impl fmt::Display for PublicKey { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", hex::encode(&self.0[..])) - } -} - -impl fmt::Display for CryptoHash { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let prec = f.precision().unwrap_or(self.0.len() * 2); - hex::encode(&self.0[..((prec + 1) / 2)]).fmt(f) - } -} - -impl fmt::Debug for Signature { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", hex::encode(&self.0.to_bytes()[0..8])) - } -} - -impl fmt::Debug for PublicKey { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", hex::encode(&self.0[..8])) - } -} - -impl fmt::Debug for CryptoHash { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", hex::encode(&self.0[..8])) - } -} - -/// Something that we know how to hash. -pub trait Hashable { - /// Send the content of `Self` to the given hasher. - fn write(&self, hasher: &mut Hasher); -} - -/// Something that we know how to hash and sign. -pub trait HasTypeName { - /// The name of the type. - fn type_name() -> &'static str; -} - -/// Activate the blanket implementation of `Hashable` based on serde and BCS. -/// * We use `serde_name` to extract a seed from the name of structs and enums. -/// * We use `BCS` to generate canonical bytes suitable for hashing. -pub trait BcsHashable<'de>: Serialize + Deserialize<'de> {} - -/// Activate the blanket implementation of `Signable` based on serde and BCS. -/// * We use `serde_name` to extract a seed from the name of structs and enums. -/// * We use `BCS` to generate canonical bytes suitable for signing. -pub trait BcsSignable<'de>: Serialize + Deserialize<'de> {} - -impl<'de, T: BcsSignable<'de>> BcsHashable<'de> for T {} - -impl<'de, T, Hasher> Hashable for T -where - T: BcsHashable<'de>, - Hasher: io::Write, -{ - fn write(&self, hasher: &mut Hasher) { - let name = ::type_name(); - // Note: This assumes that names never contain the separator `::`. - write!(hasher, "{}::", name).expect("Hasher should not fail"); - bcs::serialize_into(hasher, &self).expect("Message serialization should not fail"); - } -} - -/// Temporary struct to extend `Keccak256` with `io::Write`. -struct Keccak256Ext(Keccak256); - -impl io::Write for Keccak256Ext { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.0.update(buf); - Ok(buf.len()) - } - - fn flush(&mut self) -> io::Result<()> { - Ok(()) - } -} - -impl Hashable for [u8] -where - Hasher: io::Write, -{ - fn write(&self, hasher: &mut Hasher) { - hasher.write_all(self).expect("Hasher should not fail"); - } -} - -impl<'de, T> HasTypeName for T -where - T: BcsHashable<'de>, -{ - fn type_name() -> &'static str { - serde_name::trace_name::().expect("Self must be a struct or an enum") - } -} - -impl CryptoHash { - /// Computes a hash. - pub fn new<'de, T: BcsHashable<'de>>(value: &T) -> Self { - let mut hasher = Keccak256Ext(Keccak256::new()); - value.write(&mut hasher); - CryptoHash(hasher.0.finalize()) - } - - /// Reads the bytes of the hash value. - pub fn as_bytes(&self) -> &B256 { - &self.0 - } - - /// Returns the hash of `TestString(s)`, for testing purposes. - #[cfg(with_testing)] - pub fn test_hash(s: impl Into) -> Self { - CryptoHash::new(&TestString::new(s)) - } -} - -impl Signature { - /// Computes a signature. - pub fn new<'de, T>(value: &T, secret: &KeyPair) -> Self - where - T: BcsSignable<'de>, - { - let mut message = Vec::new(); - value.write(&mut message); - let signature = secret.0.sign(&message); - Signature(signature) - } - - fn check_internal<'de, T>( - &self, - value: &T, - author: PublicKey, - ) -> Result<(), dalek::SignatureError> - where - T: BcsSignable<'de>, - { - let mut message = Vec::new(); - value.write(&mut message); - let public_key = dalek::VerifyingKey::from_bytes(&author.0)?; - public_key.verify(&message, &self.0) - } - - /// Checks a signature. - pub fn check<'de, T>(&self, value: &T, author: PublicKey) -> Result<(), CryptoError> - where - T: BcsSignable<'de> + fmt::Debug, - { - self.check_internal(value, author) - .map_err(|error| CryptoError::InvalidSignature { - error: error.to_string(), - type_name: T::type_name().to_string(), - }) - } - - /// Checks an optional signature. - pub fn check_optional_signature<'de, T>( - signature: Option<&Self>, - value: &T, - author: &PublicKey, - ) -> Result<(), CryptoError> - where - T: BcsSignable<'de> + fmt::Debug, - { - match signature { - Some(sig) => sig.check(value, *author), - None => Err(CryptoError::MissingSignature { - type_name: T::type_name().to_string(), - }), - } - } - - fn verify_batch_internal<'a, 'de, T, I>( - value: &'a T, - votes: I, - ) -> Result<(), dalek::SignatureError> - where - T: BcsSignable<'de>, - I: IntoIterator, - { - let mut msg = Vec::new(); - value.write(&mut msg); - let mut messages = Vec::new(); - let mut signatures = Vec::new(); - let mut public_keys = Vec::new(); - for (addr, sig) in votes.into_iter() { - messages.push(msg.as_slice()); - signatures.push(sig.0); - public_keys.push(dalek::VerifyingKey::from_bytes(&addr.0)?); - } - dalek::verify_batch(&messages[..], &signatures[..], &public_keys[..]) - } - - /// Verifies a batch of signatures. - pub fn verify_batch<'a, 'de, T, I>(value: &'a T, votes: I) -> Result<(), CryptoError> - where - T: BcsSignable<'de>, - I: IntoIterator, - { - Signature::verify_batch_internal(value, votes).map_err(|error| { - CryptoError::InvalidSignature { - error: format!("batched {}", error), - type_name: T::type_name().to_string(), - } - }) - } -} - -impl WitType for CryptoHash { - const SIZE: u32 = <(u64, u64, u64, u64) as WitType>::SIZE; - type Layout = <(u64, u64, u64, u64) as WitType>::Layout; - type Dependencies = HList![]; - - fn wit_type_name() -> Cow<'static, str> { - "crypto-hash".into() - } - - fn wit_type_declaration() -> Cow<'static, str> { - concat!( - " record crypto-hash {\n", - " part1: u64,\n", - " part2: u64,\n", - " part3: u64,\n", - " part4: u64,\n", - " }\n", - ) - .into() - } -} - -impl WitLoad for CryptoHash { - fn load( - memory: &Memory<'_, Instance>, - location: GuestPointer, - ) -> Result - where - Instance: InstanceWithMemory, - ::Memory: RuntimeMemory, - { - let (part1, part2, part3, part4) = WitLoad::load(memory, location)?; - Ok(CryptoHash::from([part1, part2, part3, part4])) - } - - fn lift_from( - flat_layout: ::Flat, - memory: &Memory<'_, Instance>, - ) -> Result - where - Instance: InstanceWithMemory, - ::Memory: RuntimeMemory, - { - let (part1, part2, part3, part4) = WitLoad::lift_from(flat_layout, memory)?; - Ok(CryptoHash::from([part1, part2, part3, part4])) - } -} - -impl WitStore for CryptoHash { - fn store( - &self, - memory: &mut Memory<'_, Instance>, - location: GuestPointer, - ) -> Result<(), RuntimeError> - where - Instance: InstanceWithMemory, - ::Memory: RuntimeMemory, - { - let [part1, part2, part3, part4] = (*self).into(); - (part1, part2, part3, part4).store(memory, location) - } - - fn lower( - &self, - memory: &mut Memory<'_, Instance>, - ) -> Result<::Flat, RuntimeError> - where - Instance: InstanceWithMemory, - ::Memory: RuntimeMemory, - { - let [part1, part2, part3, part4] = (*self).into(); - (part1, part2, part3, part4).lower(memory) - } -} - -impl WitType for PublicKey { - const SIZE: u32 = <(u64, u64, u64, u64) as WitType>::SIZE; - type Layout = <(u64, u64, u64, u64) as WitType>::Layout; - type Dependencies = HList![]; - - fn wit_type_name() -> Cow<'static, str> { - "public-key".into() - } - - fn wit_type_declaration() -> Cow<'static, str> { - concat!( - " record public-key {\n", - " part1: u64,\n", - " part2: u64,\n", - " part3: u64,\n", - " part4: u64,\n", - " }\n", - ) - .into() - } -} - -impl WitLoad for PublicKey { - fn load( - memory: &Memory<'_, Instance>, - location: GuestPointer, - ) -> Result - where - Instance: InstanceWithMemory, - ::Memory: RuntimeMemory, - { - let (part1, part2, part3, part4) = WitLoad::load(memory, location)?; - Ok(PublicKey::from([part1, part2, part3, part4])) - } - - fn lift_from( - flat_layout: ::Flat, - memory: &Memory<'_, Instance>, - ) -> Result - where - Instance: InstanceWithMemory, - ::Memory: RuntimeMemory, - { - let (part1, part2, part3, part4) = WitLoad::lift_from(flat_layout, memory)?; - Ok(PublicKey::from([part1, part2, part3, part4])) - } -} - -impl WitStore for PublicKey { - fn store( - &self, - memory: &mut Memory<'_, Instance>, - location: GuestPointer, - ) -> Result<(), RuntimeError> - where - Instance: InstanceWithMemory, - ::Memory: RuntimeMemory, - { - let [part1, part2, part3, part4] = (*self).into(); - (part1, part2, part3, part4).store(memory, location) - } - - fn lower( - &self, - memory: &mut Memory<'_, Instance>, - ) -> Result<::Flat, RuntimeError> - where - Instance: InstanceWithMemory, - ::Memory: RuntimeMemory, - { - let [part1, part2, part3, part4] = (*self).into(); - (part1, part2, part3, part4).lower(memory) - } -} - -#[cfg(with_testing)] -impl Arbitrary for CryptoHash { - type Parameters = (); - type Strategy = strategy::Map>, fn(Vec) -> CryptoHash>; - - fn arbitrary_with((): Self::Parameters) -> Self::Strategy { - vec(u8::MIN..=u8::MAX, HasherOutputSize::len_bytes()) - .prop_map(|vector| CryptoHash(B256::from_slice(&vector[..]))) - } -} - -impl<'de> BcsHashable<'de> for PublicKey {} - -doc_scalar!(CryptoHash, "A Keccak256 value"); -doc_scalar!(PublicKey, "A signature public key"); -doc_scalar!(Signature, "A signature value"); - -/// A BCS-signable struct for testing. -#[cfg(with_testing)] -#[derive(Debug, Serialize, Deserialize)] -pub struct TestString(pub String); - -#[cfg(with_testing)] -impl TestString { - /// Creates a new `TestString` with the given string. - pub fn new(s: impl Into) -> Self { - Self(s.into()) - } -} - -#[cfg(with_testing)] -impl<'de> BcsSignable<'de> for TestString {} - -#[test] -fn test_signatures() { - #[derive(Debug, Serialize, Deserialize)] - struct Foo(String); - - impl<'de> BcsSignable<'de> for Foo {} - - let key1 = KeyPair::generate(); - let addr1 = key1.public(); - let key2 = KeyPair::generate(); - let addr2 = key2.public(); - - let ts = TestString("hello".into()); - let tsx = TestString("hellox".into()); - let foo = Foo("hello".into()); - - let s = Signature::new(&ts, &key1); - assert!(s.check(&ts, addr1).is_ok()); - assert!(s.check(&ts, addr2).is_err()); - assert!(s.check(&tsx, addr1).is_err()); - assert!(s.check(&foo, addr1).is_err()); -} - -/// Reads the `bytes` as four little-endian unsigned 64-bit integers and returns them. -fn le_bytes_to_u64_array(bytes: &[u8]) -> [u64; 4] { - let mut integers = [0u64; 4]; - - integers[0] = u64::from_le_bytes(bytes[0..8].try_into().expect("incorrect indices")); - integers[1] = u64::from_le_bytes(bytes[8..16].try_into().expect("incorrect indices")); - integers[2] = u64::from_le_bytes(bytes[16..24].try_into().expect("incorrect indices")); - integers[3] = u64::from_le_bytes(bytes[24..32].try_into().expect("incorrect indices")); - - integers -} - -/// Reads the `bytes` as four big-endian unsigned 64-bit integers and returns them. -fn be_bytes_to_u64_array(bytes: &[u8]) -> [u64; 4] { - let mut integers = [0u64; 4]; - - integers[0] = u64::from_be_bytes(bytes[0..8].try_into().expect("incorrect indices")); - integers[1] = u64::from_be_bytes(bytes[8..16].try_into().expect("incorrect indices")); - integers[2] = u64::from_be_bytes(bytes[16..24].try_into().expect("incorrect indices")); - integers[3] = u64::from_be_bytes(bytes[24..32].try_into().expect("incorrect indices")); - - integers -} - -/// Returns the bytes that represent the `integers` in little-endian. -fn u64_array_to_le_bytes(integers: [u64; 4]) -> [u8; 32] { - let mut bytes = [0u8; 32]; - - bytes[0..8].copy_from_slice(&integers[0].to_le_bytes()); - bytes[8..16].copy_from_slice(&integers[1].to_le_bytes()); - bytes[16..24].copy_from_slice(&integers[2].to_le_bytes()); - bytes[24..32].copy_from_slice(&integers[3].to_le_bytes()); - - bytes -} - -/// Returns the bytes that represent the `integers` in big-endian. -fn u64_array_to_be_bytes(integers: [u64; 4]) -> [u8; 32] { - let mut bytes = [0u8; 32]; - - bytes[0..8].copy_from_slice(&integers[0].to_be_bytes()); - bytes[8..16].copy_from_slice(&integers[1].to_be_bytes()); - bytes[16..24].copy_from_slice(&integers[2].to_be_bytes()); - bytes[24..32].copy_from_slice(&integers[3].to_be_bytes()); - - bytes -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_u64_array_to_be_bytes() { - let input = [ - 0x0123456789ABCDEF, - 0xFEDCBA9876543210, - 0x0011223344556677, - 0x8899AABBCCDDEEFF, - ]; - let expected_output = [ - 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, - 0x32, 0x10, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, - 0xCC, 0xDD, 0xEE, 0xFF, - ]; - - let output = u64_array_to_be_bytes(input); - assert_eq!(output, expected_output); - assert_eq!(input, be_bytes_to_u64_array(&u64_array_to_be_bytes(input))); - } - - #[test] - fn test_u64_array_to_le_bytes() { - let input = [ - 0x0123456789ABCDEF, - 0xFEDCBA9876543210, - 0x0011223344556677, - 0x8899AABBCCDDEEFF, - ]; - let expected_output = [ - 0xEF, 0xCD, 0xAB, 0x89, 0x67, 0x45, 0x23, 0x01, 0x10, 0x32, 0x54, 0x76, 0x98, 0xBA, - 0xDC, 0xFE, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00, 0xFF, 0xEE, 0xDD, 0xCC, - 0xBB, 0xAA, 0x99, 0x88, - ]; - - let output = u64_array_to_le_bytes(input); - assert_eq!(output, expected_output); - assert_eq!(input, le_bytes_to_u64_array(&u64_array_to_le_bytes(input))); - } -} diff --git a/linera-base/src/crypto/hash.rs b/linera-base/src/crypto/hash.rs new file mode 100644 index 000000000000..479c6139b6a2 --- /dev/null +++ b/linera-base/src/crypto/hash.rs @@ -0,0 +1,244 @@ +// Copyright (c) Facebook, Inc. and its affiliates. +// Copyright (c) Zefchain Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//! Defines hashing primitives used by the Linera protocol. + +#[cfg(with_testing)] +use std::ops::RangeInclusive; +use std::{borrow::Cow, fmt, io, str::FromStr}; + +#[cfg(with_testing)] +use alloy_primitives::FixedBytes; +use alloy_primitives::{Keccak256, B256}; +use linera_witty::{ + GuestPointer, HList, InstanceWithMemory, Layout, Memory, Runtime, RuntimeError, RuntimeMemory, + WitLoad, WitStore, WitType, +}; +#[cfg(with_testing)] +use proptest::{ + collection::{vec, VecStrategy}, + prelude::{Arbitrary, Strategy}, + strategy, +}; +use serde::{Deserialize, Serialize}; + +use crate::{ + crypto::{BcsHashable, CryptoError, Hashable}, + doc_scalar, +}; + +/// A Keccak256 value. +#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Copy, Hash)] +#[cfg_attr(with_testing, derive(Default))] +pub struct CryptoHash(B256); + +impl CryptoHash { + /// Computes a hash. + pub fn new<'de, T: BcsHashable<'de>>(value: &T) -> Self { + let mut hasher = Keccak256Ext(Keccak256::new()); + value.write(&mut hasher); + CryptoHash(hasher.0.finalize()) + } + + /// Reads the bytes of the hash value. + pub fn as_bytes(&self) -> &B256 { + &self.0 + } + + /// Returns the hash of `TestString(s)`, for testing purposes. + #[cfg(with_testing)] + pub fn test_hash(s: impl Into) -> Self { + use crate::crypto::TestString; + + CryptoHash::new(&TestString::new(s)) + } +} + +/// Temporary struct to extend `Keccak256` with `io::Write`. +struct Keccak256Ext(Keccak256); + +impl io::Write for Keccak256Ext { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.0.update(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +/// A vector of cryptographic hashes. +/// This is used to represent a hash of a list of hashes. +#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Hash, Serialize, Deserialize)] +#[cfg_attr(with_testing, derive(Default))] +pub struct CryptoHashVec(pub Vec); + +impl<'de> BcsHashable<'de> for CryptoHashVec {} + +impl Serialize for CryptoHash { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + if serializer.is_human_readable() { + serializer.serialize_str(&self.to_string()) + } else { + serializer.serialize_newtype_struct("CryptoHash", &self.as_bytes().0) + } + } +} + +impl<'de> Deserialize<'de> for CryptoHash { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + if deserializer.is_human_readable() { + let s = String::deserialize(deserializer)?; + let value = Self::from_str(&s).map_err(serde::de::Error::custom)?; + Ok(value) + } else { + #[derive(Deserialize)] + #[serde(rename = "CryptoHash")] + struct Foo([u8; 32]); + + let value = Foo::deserialize(deserializer)?; + Ok(Self(value.0.into())) + } + } +} + +impl FromStr for CryptoHash { + type Err = CryptoError; + + fn from_str(s: &str) -> Result { + let value = hex::decode(s)?; + (value.as_slice()).try_into() + } +} + +impl TryFrom<&[u8]> for CryptoHash { + type Error = CryptoError; + + fn try_from(value: &[u8]) -> Result { + if value.len() != B256::len_bytes() { + return Err(CryptoError::IncorrectHashSize(value.len())); + } + Ok(Self(B256::from_slice(value))) + } +} + +impl From<[u64; 4]> for CryptoHash { + fn from(integers: [u64; 4]) -> Self { + CryptoHash(crate::crypto::u64_array_to_be_bytes(integers).into()) + } +} + +impl From for [u64; 4] { + fn from(crypto_hash: CryptoHash) -> Self { + crate::crypto::be_bytes_to_u64_array(crypto_hash.0.as_ref()) + } +} + +impl fmt::Display for CryptoHash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let prec = f.precision().unwrap_or(self.0.len() * 2); + hex::encode(&self.0[..((prec + 1) / 2)]).fmt(f) + } +} + +impl fmt::Debug for CryptoHash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", hex::encode(&self.0[..8])) + } +} + +impl WitType for CryptoHash { + const SIZE: u32 = <(u64, u64, u64, u64) as WitType>::SIZE; + type Layout = <(u64, u64, u64, u64) as WitType>::Layout; + type Dependencies = HList![]; + + fn wit_type_name() -> Cow<'static, str> { + "crypto-hash".into() + } + + fn wit_type_declaration() -> Cow<'static, str> { + concat!( + " record crypto-hash {\n", + " part1: u64,\n", + " part2: u64,\n", + " part3: u64,\n", + " part4: u64,\n", + " }\n", + ) + .into() + } +} + +impl WitLoad for CryptoHash { + fn load( + memory: &Memory<'_, Instance>, + location: GuestPointer, + ) -> Result + where + Instance: InstanceWithMemory, + ::Memory: RuntimeMemory, + { + let (part1, part2, part3, part4) = WitLoad::load(memory, location)?; + Ok(CryptoHash::from([part1, part2, part3, part4])) + } + + fn lift_from( + flat_layout: ::Flat, + memory: &Memory<'_, Instance>, + ) -> Result + where + Instance: InstanceWithMemory, + ::Memory: RuntimeMemory, + { + let (part1, part2, part3, part4) = WitLoad::lift_from(flat_layout, memory)?; + Ok(CryptoHash::from([part1, part2, part3, part4])) + } +} + +impl WitStore for CryptoHash { + fn store( + &self, + memory: &mut Memory<'_, Instance>, + location: GuestPointer, + ) -> Result<(), RuntimeError> + where + Instance: InstanceWithMemory, + ::Memory: RuntimeMemory, + { + let [part1, part2, part3, part4] = (*self).into(); + (part1, part2, part3, part4).store(memory, location) + } + + fn lower( + &self, + memory: &mut Memory<'_, Instance>, + ) -> Result<::Flat, RuntimeError> + where + Instance: InstanceWithMemory, + ::Memory: RuntimeMemory, + { + let [part1, part2, part3, part4] = (*self).into(); + (part1, part2, part3, part4).lower(memory) + } +} + +#[cfg(with_testing)] +impl Arbitrary for CryptoHash { + type Parameters = (); + type Strategy = strategy::Map>, fn(Vec) -> CryptoHash>; + + fn arbitrary_with((): Self::Parameters) -> Self::Strategy { + vec(u8::MIN..=u8::MAX, FixedBytes::<32>::len_bytes()) + .prop_map(|vector| CryptoHash(B256::from_slice(&vector[..]))) + } +} + +doc_scalar!(CryptoHash, "A Keccak256 value"); diff --git a/linera-base/src/crypto/keypair.rs b/linera-base/src/crypto/keypair.rs new file mode 100644 index 000000000000..a288355027b1 --- /dev/null +++ b/linera-base/src/crypto/keypair.rs @@ -0,0 +1,244 @@ +// Copyright (c) Facebook, Inc. and its affiliates. +// Copyright (c) Zefchain Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use std::{borrow::Cow, fmt, str::FromStr}; + +use ed25519_dalek::{self as dalek}; +use linera_witty::{ + GuestPointer, HList, InstanceWithMemory, Layout, Memory, Runtime, RuntimeError, RuntimeMemory, + WitLoad, WitStore, WitType, +}; +use serde::{Deserialize, Serialize}; + +use super::{le_bytes_to_u64_array, u64_array_to_le_bytes, BcsHashable, CryptoError}; +use crate::doc_scalar; + +/// A signature key-pair. +pub struct KeyPair(pub(crate) dalek::SigningKey); + +/// A signature public key. +#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Hash)] +pub struct PublicKey(pub [u8; dalek::PUBLIC_KEY_LENGTH]); + +impl KeyPair { + #[cfg(all(with_getrandom, with_testing))] + /// Generates a new key-pair. + pub fn generate() -> Self { + let mut rng = rand::rngs::OsRng; + Self::generate_from(&mut rng) + } + + #[cfg(with_getrandom)] + /// Generates a new key-pair from the given RNG. Use with care. + pub fn generate_from(rng: &mut R) -> Self { + let keypair = dalek::SigningKey::generate(rng); + KeyPair(keypair) + } + + /// Obtains the public key of a key-pair. + pub fn public(&self) -> PublicKey { + PublicKey(self.0.verifying_key().to_bytes()) + } + + /// Copies the key-pair, **including the secret key**. + /// + /// The `Clone` and `Copy` traits are deliberately not implemented for `KeyPair` to prevent + /// accidental copies of secret keys. + pub fn copy(&self) -> KeyPair { + KeyPair(self.0.clone()) + } +} + +impl PublicKey { + /// A fake public key used for testing. + #[cfg(with_testing)] + pub fn test_key(name: u8) -> PublicKey { + let addr = [name; dalek::PUBLIC_KEY_LENGTH]; + PublicKey(addr) + } +} + +impl Serialize for PublicKey { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + if serializer.is_human_readable() { + serializer.serialize_str(&self.to_string()) + } else { + serializer.serialize_newtype_struct("PublicKey", &self.0) + } + } +} + +impl<'de> Deserialize<'de> for PublicKey { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + if deserializer.is_human_readable() { + let s = String::deserialize(deserializer)?; + let value = Self::from_str(&s).map_err(serde::de::Error::custom)?; + Ok(value) + } else { + #[derive(Deserialize)] + #[serde(rename = "PublicKey")] + struct Foo([u8; dalek::PUBLIC_KEY_LENGTH]); + + let value = Foo::deserialize(deserializer)?; + Ok(Self(value.0)) + } + } +} + +impl Serialize for KeyPair { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + // This is only used for JSON configuration. + assert!(serializer.is_human_readable()); + serializer.serialize_str(&hex::encode(self.0.to_bytes())) + } +} + +impl<'de> Deserialize<'de> for KeyPair { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + // This is only used for JSON configuration. + assert!(deserializer.is_human_readable()); + let s = String::deserialize(deserializer)?; + let value = hex::decode(s).map_err(serde::de::Error::custom)?; + let key = + dalek::SigningKey::from_bytes(value[..].try_into().map_err(serde::de::Error::custom)?); + Ok(KeyPair(key)) + } +} + +impl FromStr for PublicKey { + type Err = CryptoError; + + fn from_str(s: &str) -> Result { + let value = hex::decode(s)?; + (value.as_slice()).try_into() + } +} + +impl TryFrom<&[u8]> for PublicKey { + type Error = CryptoError; + + fn try_from(value: &[u8]) -> Result { + if value.len() != dalek::PUBLIC_KEY_LENGTH { + return Err(CryptoError::IncorrectPublicKeySize(value.len())); + } + let mut pubkey = [0u8; dalek::PUBLIC_KEY_LENGTH]; + pubkey.copy_from_slice(value); + Ok(PublicKey(pubkey)) + } +} + +impl From<[u64; 4]> for PublicKey { + fn from(integers: [u64; 4]) -> Self { + PublicKey(u64_array_to_le_bytes(integers)) + } +} + +impl From for [u64; 4] { + fn from(pub_key: PublicKey) -> Self { + le_bytes_to_u64_array(&pub_key.0) + } +} + +impl fmt::Display for PublicKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", hex::encode(&self.0[..])) + } +} + +impl fmt::Debug for PublicKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", hex::encode(&self.0[..8])) + } +} + +impl WitType for PublicKey { + const SIZE: u32 = <(u64, u64, u64, u64) as WitType>::SIZE; + type Layout = <(u64, u64, u64, u64) as WitType>::Layout; + type Dependencies = HList![]; + + fn wit_type_name() -> Cow<'static, str> { + "public-key".into() + } + + fn wit_type_declaration() -> Cow<'static, str> { + concat!( + " record public-key {\n", + " part1: u64,\n", + " part2: u64,\n", + " part3: u64,\n", + " part4: u64,\n", + " }\n", + ) + .into() + } +} + +impl WitLoad for PublicKey { + fn load( + memory: &Memory<'_, Instance>, + location: GuestPointer, + ) -> Result + where + Instance: InstanceWithMemory, + ::Memory: RuntimeMemory, + { + let (part1, part2, part3, part4) = WitLoad::load(memory, location)?; + Ok(PublicKey::from([part1, part2, part3, part4])) + } + + fn lift_from( + flat_layout: ::Flat, + memory: &Memory<'_, Instance>, + ) -> Result + where + Instance: InstanceWithMemory, + ::Memory: RuntimeMemory, + { + let (part1, part2, part3, part4) = WitLoad::lift_from(flat_layout, memory)?; + Ok(PublicKey::from([part1, part2, part3, part4])) + } +} + +impl WitStore for PublicKey { + fn store( + &self, + memory: &mut Memory<'_, Instance>, + location: GuestPointer, + ) -> Result<(), RuntimeError> + where + Instance: InstanceWithMemory, + ::Memory: RuntimeMemory, + { + let [part1, part2, part3, part4] = (*self).into(); + (part1, part2, part3, part4).store(memory, location) + } + + fn lower( + &self, + memory: &mut Memory<'_, Instance>, + ) -> Result<::Flat, RuntimeError> + where + Instance: InstanceWithMemory, + ::Memory: RuntimeMemory, + { + let [part1, part2, part3, part4] = (*self).into(); + (part1, part2, part3, part4).lower(memory) + } +} + +impl<'de> BcsHashable<'de> for PublicKey {} + +doc_scalar!(PublicKey, "A signature public key"); diff --git a/linera-base/src/crypto/mod.rs b/linera-base/src/crypto/mod.rs new file mode 100644 index 000000000000..cf16c2b8d66e --- /dev/null +++ b/linera-base/src/crypto/mod.rs @@ -0,0 +1,207 @@ +// Copyright (c) Facebook, Inc. and its affiliates. +// Copyright (c) Zefchain Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +//! Define the cryptographic primitives used by the Linera protocol. + +mod keypair; +pub use keypair::*; +mod hash; +mod signature; +use std::{io, num::ParseIntError}; + +use alloy_primitives::FixedBytes; +use ed25519_dalek::{self as dalek}; +pub use hash::*; +use serde::{Deserialize, Serialize}; +pub use signature::*; +use thiserror::Error; + +/// Error type for cryptographic errors. +#[derive(Error, Debug)] +#[allow(missing_docs)] +pub enum CryptoError { + #[error("Signature for object {type_name} is not valid: {error}")] + InvalidSignature { error: String, type_name: String }, + #[error("Signature for object {type_name} is missing")] + MissingSignature { type_name: String }, + #[error(transparent)] + NonHexDigits(#[from] hex::FromHexError), + #[error( + "Byte slice has length {0} but a `CryptoHash` requires exactly {expected} bytes", + expected = FixedBytes::<32>::len_bytes(), + )] + IncorrectHashSize(usize), + #[error( + "Byte slice has length {0} but a `PublicKey` requires exactly {expected} bytes", + expected = dalek::PUBLIC_KEY_LENGTH, + )] + IncorrectPublicKeySize(usize), + #[error("Could not parse integer: {0}")] + ParseIntError(#[from] ParseIntError), +} + +#[cfg(with_getrandom)] +/// Wrapper around [`rand::CryptoRng`] and [`rand::RngCore`]. +pub trait CryptoRng: rand::CryptoRng + rand::RngCore + Send + Sync {} + +#[cfg(with_getrandom)] +impl CryptoRng for T {} + +#[cfg(with_getrandom)] +impl From> for Box { + fn from(seed: Option) -> Self { + use rand::SeedableRng; + + match seed { + Some(seed) => Box::new(rand::rngs::StdRng::seed_from_u64(seed)), + None => Box::new(rand::rngs::OsRng), + } + } +} + +/// Something that we know how to hash. +pub trait Hashable { + /// Send the content of `Self` to the given hasher. + fn write(&self, hasher: &mut Hasher); +} + +/// Something that we know how to hash and sign. +pub trait HasTypeName { + /// The name of the type. + fn type_name() -> &'static str; +} + +/// Activate the blanket implementation of `Hashable` based on serde and BCS. +/// * We use `serde_name` to extract a seed from the name of structs and enums. +/// * We use `BCS` to generate canonical bytes suitable for hashing. +pub trait BcsHashable<'de>: Serialize + Deserialize<'de> {} + +/// Activate the blanket implementation of `Signable` based on serde and BCS. +/// * We use `serde_name` to extract a seed from the name of structs and enums. +/// * We use `BCS` to generate canonical bytes suitable for signing. +pub trait BcsSignable<'de>: Serialize + Deserialize<'de> {} + +impl<'de, T: BcsSignable<'de>> BcsHashable<'de> for T {} + +impl<'de, T, Hasher> Hashable for T +where + T: BcsHashable<'de>, + Hasher: io::Write, +{ + fn write(&self, hasher: &mut Hasher) { + let name = ::type_name(); + // Note: This assumes that names never contain the separator `::`. + write!(hasher, "{}::", name).expect("Hasher should not fail"); + bcs::serialize_into(hasher, &self).expect("Message serialization should not fail"); + } +} + +impl Hashable for [u8] +where + Hasher: io::Write, +{ + fn write(&self, hasher: &mut Hasher) { + hasher.write_all(self).expect("Hasher should not fail"); + } +} + +impl<'de, T> HasTypeName for T +where + T: BcsHashable<'de>, +{ + fn type_name() -> &'static str { + serde_name::trace_name::().expect("Self must be a struct or an enum") + } +} + +/// Reads the `bytes` as four little-endian unsigned 64-bit integers and returns them. +pub(crate) fn le_bytes_to_u64_array(bytes: &[u8]) -> [u64; 4] { + let mut integers = [0u64; 4]; + + integers[0] = u64::from_le_bytes(bytes[0..8].try_into().expect("incorrect indices")); + integers[1] = u64::from_le_bytes(bytes[8..16].try_into().expect("incorrect indices")); + integers[2] = u64::from_le_bytes(bytes[16..24].try_into().expect("incorrect indices")); + integers[3] = u64::from_le_bytes(bytes[24..32].try_into().expect("incorrect indices")); + + integers +} + +/// Reads the `bytes` as four big-endian unsigned 64-bit integers and returns them. +pub(crate) fn be_bytes_to_u64_array(bytes: &[u8]) -> [u64; 4] { + let mut integers = [0u64; 4]; + + integers[0] = u64::from_be_bytes(bytes[0..8].try_into().expect("incorrect indices")); + integers[1] = u64::from_be_bytes(bytes[8..16].try_into().expect("incorrect indices")); + integers[2] = u64::from_be_bytes(bytes[16..24].try_into().expect("incorrect indices")); + integers[3] = u64::from_be_bytes(bytes[24..32].try_into().expect("incorrect indices")); + + integers +} + +/// Returns the bytes that represent the `integers` in little-endian. +pub(crate) fn u64_array_to_le_bytes(integers: [u64; 4]) -> [u8; 32] { + let mut bytes = [0u8; 32]; + + bytes[0..8].copy_from_slice(&integers[0].to_le_bytes()); + bytes[8..16].copy_from_slice(&integers[1].to_le_bytes()); + bytes[16..24].copy_from_slice(&integers[2].to_le_bytes()); + bytes[24..32].copy_from_slice(&integers[3].to_le_bytes()); + + bytes +} + +/// Returns the bytes that represent the `integers` in big-endian. +pub(crate) fn u64_array_to_be_bytes(integers: [u64; 4]) -> [u8; 32] { + let mut bytes = [0u8; 32]; + + bytes[0..8].copy_from_slice(&integers[0].to_be_bytes()); + bytes[8..16].copy_from_slice(&integers[1].to_be_bytes()); + bytes[16..24].copy_from_slice(&integers[2].to_be_bytes()); + bytes[24..32].copy_from_slice(&integers[3].to_be_bytes()); + + bytes +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_u64_array_to_be_bytes() { + let input = [ + 0x0123456789ABCDEF, + 0xFEDCBA9876543210, + 0x0011223344556677, + 0x8899AABBCCDDEEFF, + ]; + let expected_output = [ + 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, + 0x32, 0x10, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, + 0xCC, 0xDD, 0xEE, 0xFF, + ]; + + let output = u64_array_to_be_bytes(input); + assert_eq!(output, expected_output); + assert_eq!(input, be_bytes_to_u64_array(&u64_array_to_be_bytes(input))); + } + + #[test] + fn test_u64_array_to_le_bytes() { + let input = [ + 0x0123456789ABCDEF, + 0xFEDCBA9876543210, + 0x0011223344556677, + 0x8899AABBCCDDEEFF, + ]; + let expected_output = [ + 0xEF, 0xCD, 0xAB, 0x89, 0x67, 0x45, 0x23, 0x01, 0x10, 0x32, 0x54, 0x76, 0x98, 0xBA, + 0xDC, 0xFE, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00, 0xFF, 0xEE, 0xDD, 0xCC, + 0xBB, 0xAA, 0x99, 0x88, + ]; + + let output = u64_array_to_le_bytes(input); + assert_eq!(output, expected_output); + assert_eq!(input, le_bytes_to_u64_array(&u64_array_to_le_bytes(input))); + } +} diff --git a/linera-base/src/crypto/signature.rs b/linera-base/src/crypto/signature.rs new file mode 100644 index 000000000000..b0c53e6b2e45 --- /dev/null +++ b/linera-base/src/crypto/signature.rs @@ -0,0 +1,195 @@ +// Copyright (c) Facebook, Inc. and its affiliates. +// Copyright (c) Zefchain Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt; + +use ed25519_dalek::{self as dalek, Signer, Verifier}; +use serde::{Deserialize, Serialize}; + +use super::{BcsSignable, CryptoError, HasTypeName, Hashable, KeyPair, PublicKey}; +use crate::doc_scalar; + +/// A signature value. +#[derive(Eq, PartialEq, Copy, Clone)] +pub struct Signature(pub dalek::Signature); + +impl Signature { + /// Computes a signature. + pub fn new<'de, T>(value: &T, secret: &KeyPair) -> Self + where + T: BcsSignable<'de>, + { + let mut message = Vec::new(); + value.write(&mut message); + let signature = secret.0.sign(&message); + Signature(signature) + } + + fn check_internal<'de, T>( + &self, + value: &T, + author: PublicKey, + ) -> Result<(), dalek::SignatureError> + where + T: BcsSignable<'de>, + { + let mut message = Vec::new(); + value.write(&mut message); + let public_key = dalek::VerifyingKey::from_bytes(&author.0)?; + public_key.verify(&message, &self.0) + } + + /// Checks a signature. + pub fn check<'de, T>(&self, value: &T, author: PublicKey) -> Result<(), CryptoError> + where + T: BcsSignable<'de> + fmt::Debug, + { + self.check_internal(value, author) + .map_err(|error| CryptoError::InvalidSignature { + error: error.to_string(), + type_name: T::type_name().to_string(), + }) + } + + /// Checks an optional signature. + pub fn check_optional_signature<'de, T>( + signature: Option<&Self>, + value: &T, + author: &PublicKey, + ) -> Result<(), CryptoError> + where + T: BcsSignable<'de> + fmt::Debug, + { + match signature { + Some(sig) => sig.check(value, *author), + None => Err(CryptoError::MissingSignature { + type_name: T::type_name().to_string(), + }), + } + } + + fn verify_batch_internal<'a, 'de, T, I>( + value: &'a T, + votes: I, + ) -> Result<(), dalek::SignatureError> + where + T: BcsSignable<'de>, + I: IntoIterator, + { + let mut msg = Vec::new(); + value.write(&mut msg); + let mut messages = Vec::new(); + let mut signatures = Vec::new(); + let mut public_keys = Vec::new(); + for (addr, sig) in votes.into_iter() { + messages.push(msg.as_slice()); + signatures.push(sig.0); + public_keys.push(dalek::VerifyingKey::from_bytes(&addr.0)?); + } + dalek::verify_batch(&messages[..], &signatures[..], &public_keys[..]) + } + + /// Verifies a batch of signatures. + pub fn verify_batch<'a, 'de, T, I>(value: &'a T, votes: I) -> Result<(), CryptoError> + where + T: BcsSignable<'de>, + I: IntoIterator, + { + Signature::verify_batch_internal(value, votes).map_err(|error| { + CryptoError::InvalidSignature { + error: format!("batched {}", error), + type_name: T::type_name().to_string(), + } + }) + } +} + +impl Serialize for Signature { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + if serializer.is_human_readable() { + serializer.serialize_str(&hex::encode(self.0.to_bytes())) + } else { + serializer.serialize_newtype_struct("Signature", &self.0) + } + } +} + +impl<'de> Deserialize<'de> for Signature { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + if deserializer.is_human_readable() { + let s = String::deserialize(deserializer)?; + let value = hex::decode(s).map_err(serde::de::Error::custom)?; + let sig = + dalek::Signature::try_from(value.as_slice()).map_err(serde::de::Error::custom)?; + Ok(Signature(sig)) + } else { + #[derive(Deserialize)] + #[serde(rename = "Signature")] + struct Foo(dalek::Signature); + + let value = Foo::deserialize(deserializer)?; + Ok(Self(value.0)) + } + } +} + +impl fmt::Display for Signature { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = hex::encode(self.0.to_bytes()); + write!(f, "{}", s) + } +} + +impl fmt::Debug for Signature { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", hex::encode(&self.0.to_bytes()[0..8])) + } +} + +doc_scalar!(Signature, "A signature value"); + +/// A BCS-signable struct for testing. +#[cfg(with_testing)] +#[derive(Debug, Serialize, Deserialize)] +pub struct TestString(pub String); + +#[cfg(with_testing)] +impl TestString { + /// Creates a new `TestString` with the given string. + pub fn new(s: impl Into) -> Self { + Self(s.into()) + } +} + +#[cfg(with_testing)] +impl<'de> BcsSignable<'de> for TestString {} + +#[test] +fn test_signatures() { + #[derive(Debug, Serialize, Deserialize)] + struct Foo(String); + + impl<'de> BcsSignable<'de> for Foo {} + + let key1 = KeyPair::generate(); + let addr1 = key1.public(); + let key2 = KeyPair::generate(); + let addr2 = key2.public(); + + let ts = TestString("hello".into()); + let tsx = TestString("hellox".into()); + let foo = Foo("hello".into()); + + let s = Signature::new(&ts, &key1); + assert!(s.check(&ts, addr1).is_ok()); + assert!(s.check(&ts, addr2).is_err()); + assert!(s.check(&tsx, addr1).is_err()); + assert!(s.check(&foo, addr1).is_err()); +} diff --git a/linera-base/src/lib.rs b/linera-base/src/lib.rs index 71dec2f02823..975f5beb124d 100644 --- a/linera-base/src/lib.rs +++ b/linera-base/src/lib.rs @@ -12,7 +12,6 @@ use std::fmt; #[doc(hidden)] pub use async_trait::async_trait; - pub mod abi; #[cfg(not(target_arch = "wasm32"))] pub mod command; diff --git a/linera-service-graphql-client/gql/service_schema.graphql b/linera-service-graphql-client/gql/service_schema.graphql index ddd1e1f14946..21d331cda5f6 100644 --- a/linera-service-graphql-client/gql/service_schema.graphql +++ b/linera-service-graphql-client/gql/service_schema.graphql @@ -287,7 +287,7 @@ type ChainStateExtendedView { Hashes of all certified blocks for this sender. This ends with `block_hash` and has length `usize::from(next_block_height)`. """ - confirmedLog: LogView_CryptoHash_5f6ab77f! + confirmedLog: LogView_CryptoHash_87fbb60c! """ Sender chain and height of all certified blocks known as a receiver (local ordering). """ @@ -604,7 +604,7 @@ type LogView_ChainAndHeight_7af83576 { entries(start: Int, end: Int): [ChainAndHeight!]! } -type LogView_CryptoHash_5f6ab77f { +type LogView_CryptoHash_87fbb60c { entries(start: Int, end: Int): [CryptoHash!]! }