From 10affecadf80c09ade5a449fd6fa081d3a2a112a Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Mon, 3 Jun 2024 13:20:49 +0200 Subject: [PATCH] Remove tag, add support of all hash functions from `digest` --- Cargo.lock | 37 +++++++ Makefile | 21 ++-- cspell.yml | 10 ++ udigest/Cargo.toml | 12 ++- udigest/examples/derivation.rs | 6 +- udigest/src/encoding.rs | 36 +++++-- udigest/src/lib.rs | 154 +++++++++++++++------------- udigest/tests/derive.rs | 4 +- udigest/tests/deterministic_hash.rs | 78 ++++++++++++++ 9 files changed, 259 insertions(+), 99 deletions(-) create mode 100644 cspell.yml create mode 100644 udigest/tests/deterministic_hash.rs diff --git a/Cargo.lock b/Cargo.lock index 2524a71..f1d9362 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -44,6 +53,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -62,6 +72,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + [[package]] name = "libc" version = "0.2.149" @@ -97,6 +116,22 @@ dependencies = [ "digest", ] +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + [[package]] name = "syn" version = "2.0.38" @@ -118,9 +153,11 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" name = "udigest" version = "0.1.0" dependencies = [ + "blake2", "digest", "hex", "sha2", + "sha3", "udigest-derive 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/Makefile b/Makefile index c0cdc25..f98ed4d 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,16 @@ +.PHONY: docs docs-open + +docs: + RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --no-deps --all-features + +docs-open: + RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --no-deps --all-features --open + +docs-private: + RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --no-deps --all-features --document-private-items + readme: - cargo readme -i src/lib.rs -r udigest/ -t ../docs/readme.tpl \ - | perl -ne 's/\[(.+?)\]\((?!https).+?\)/\1/g; print;' \ - | perl -ne 's/(? README.md - cargo readme -i src/lib.rs -r udigest-derive/ -t ../docs/readme.tpl \ - > udigest-derive/README.md + (cd generic-ec; cargo rdme -r ../README.md) + (cd generic-ec-core; cargo rdme -r README.md) + (cd generic-ec-curves; cargo rdme -r README.md) + (cd generic-ec-zkp; cargo rdme -r README.md) diff --git a/cspell.yml b/cspell.yml new file mode 100644 index 0000000..2e5c24c --- /dev/null +++ b/cspell.yml @@ -0,0 +1,10 @@ +words: +- digestable +- udigest +- VecDeque +- bytestring +- bytestrings +- biglen +- sublist +- docsrs +- concated diff --git a/udigest/Cargo.toml b/udigest/Cargo.toml index 3360bb3..c3ae8f1 100644 --- a/udigest/Cargo.toml +++ b/udigest/Cargo.toml @@ -11,27 +11,33 @@ readme = "../README.md" [package.metadata.docs.rs] all-features = true +rustdoc-args = ["--cfg", "docsrs"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -digest = { version = "0.10", default-features = false } +digest = { version = "0.10", default-features = false, optional = true } udigest-derive = { version = "0.1", optional = true } [dev-dependencies] -sha2 = "0.10" hex = "0.4" +sha2 = "0.10" +sha3 = "0.10" +blake2 = "0.10" + [features] std = ["alloc"] alloc = [] derive = ["dep:udigest-derive"] +digest = ["dep:digest"] + [[test]] name = "derive" required-features = ["std", "derive"] [[example]] name = "derivation" -required-features = ["std", "derive"] \ No newline at end of file +required-features = ["std", "derive"] diff --git a/udigest/examples/derivation.rs b/udigest/examples/derivation.rs index f5b4fe2..429db9a 100644 --- a/udigest/examples/derivation.rs +++ b/udigest/examples/derivation.rs @@ -1,6 +1,3 @@ -use sha2::Sha256; -use udigest::udigest; - #[derive(udigest::Digestable)] #[udigest(tag = "udigest.example.Person.v1")] struct Person { @@ -15,7 +12,6 @@ fn main() { job_title: "cryptographer".into(), }; - let tag = udigest::Tag::::new("udigest.example"); - let hash = udigest(tag, &person); + let hash = udigest::hash::(&person); println!("{}", hex::encode(hash)); } diff --git a/udigest/src/encoding.rs b/udigest/src/encoding.rs index 9bf6445..91e8ab8 100644 --- a/udigest/src/encoding.rs +++ b/udigest/src/encoding.rs @@ -3,7 +3,7 @@ //! The core of the crate is functionality to unambiguously encode any structured data //! into bytes. It's then used to digest any data into a hash. Note that this module //! provides low-level implementation details which you normally don't need to know -//! unless you manually implemenet [Digestable](crate::Digestable) trait. +//! unless you manually implement [Digestable](crate::Digestable) trait. //! //! Any structured `value` is encoded either as a bytestring or as a list (each element //! within the list is either a bytestring or a list). The simplified grammar can be seen as @@ -18,7 +18,7 @@ //! Encoding goal is to distinguish `["12", "3"]` from `["1", "23"]`, `["1", [], "2"]` //! from `["1", "2"]` and so on. Now, we only need to map any structured data onto //! that grammar to have an unambiguous encoding. Below, we will show how Rust structures -//! can be mapped onto the lists, and then we descrive how exactly encoding works. +//! can be mapped onto the lists, and then we describe how exactly encoding works. //! //! # Mapping Rust types onto lists //! @@ -188,9 +188,25 @@ pub trait Buffer { fn write(&mut self, bytes: &[u8]); } -impl Buffer for D { +/// Wraps [`digest::Digest`] and implements [`Buffer`] +#[cfg(feature = "digest")] +pub struct BufferDigest(pub D); + +#[cfg(feature = "digest")] +impl Buffer for BufferDigest { + fn write(&mut self, bytes: &[u8]) { + self.0.update(bytes) + } +} + +/// Wraps [`digest::Update`] and implements [`Buffer`] +#[cfg(feature = "digest")] +pub struct BufferUpdate(pub D); + +#[cfg(feature = "digest")] +impl Buffer for BufferUpdate { fn write(&mut self, bytes: &[u8]) { - self.update(bytes) + self.0.update(bytes) } } @@ -312,15 +328,15 @@ impl<'b, B: Buffer> EncodeStruct<'b, B> { self } - /// Adds a fiels to the structure + /// Adds a fields to the structure /// - /// Returns an encoder that shall be used to encode the fiels value + /// Returns an encoder that shall be used to encode the fields value pub fn add_field(&mut self, field_name: impl AsRef<[u8]>) -> EncodeValue { self.list.add_leaf().chain(field_name); self.list.add_item() } - /// Finilizes the encoding, puts the necessary metadata to the buffer + /// Finalizes the encoding, puts the necessary metadata to the buffer /// /// It's an alias to dropping the encoder pub fn finish(self) {} @@ -377,7 +393,7 @@ impl<'b, B: Buffer> EncodeLeaf<'b, B> { .expect("leaf length overflows `usize`") } - /// Finilizes the encoding, puts the necessary metadata to the buffer + /// Finalizes the encoding, puts the necessary metadata to the buffer /// /// It's an alias to dropping the encoder pub fn finish(self) {} @@ -452,7 +468,7 @@ impl<'b, B: Buffer> EncodeList<'b, B> { self.add_item().encode_list() } - /// Finilizes the encoding, puts the necessary metadata to the buffer + /// Finalizes the encoding, puts the necessary metadata to the buffer /// /// It's an alias to dropping the encoder pub fn finish(self) {} @@ -475,7 +491,7 @@ impl<'b, B: Buffer> Drop for EncodeList<'b, B> { /// Encodes length of list or leaf /// -/// Altough we expose how the length is encoded, normally you should use [EncodeList] +/// Although we expose how the length is encoded, normally you should use [EncodeList] /// and [EncodeLeaf] which use this function internally pub fn encode_len(buffer: &mut impl Buffer, len: usize) { match u32::try_from(len) { diff --git a/udigest/src/lib.rs b/udigest/src/lib.rs index 38ff8dc..896b908 100644 --- a/udigest/src/lib.rs +++ b/udigest/src/lib.rs @@ -17,30 +17,26 @@ //! //! The trait is intentionally not implemented for certain types: //! -//! * `HashMap`, `HashSet` as they can not be traversed in determenistic order -//! * `usize`, `isize` as their byte size varies on differnet platforms +//! * `HashMap`, `HashSet` as they can not be traversed in deterministic order +//! * `usize`, `isize` as their byte size varies on different platforms //! //! The `Digestable` trait can be implemented for the struct using [a macro](derive@Digestable): //! ```rust -//! use udigest::{Tag, udigest}; -//! use sha2::Sha256; -//! //! #[derive(udigest::Digestable)] //! struct Person { //! name: String, //! job_title: String, //! } -//! let alice = &Person { +//! let alice = Person { //! name: "Alice".into(), //! job_title: "cryptographer".into(), //! }; //! -//! let tag = Tag::::new("udigest.example"); -//! let hash = udigest(tag, &alice); +//! let hash = udigest::hash::(&alice); //! ``` //! //! The crate intentionally does not try to follow any existing standards for unambiguous -//! encoding. The format for encoding was desingned specifically for `udigest` to provide +//! encoding. The format for encoding was designed specifically for `udigest` to provide //! a better usage experience in Rust. The details of encoding format can be found in //! [`encoding` module](encoding). //! @@ -51,6 +47,9 @@ #![no_std] #![forbid(missing_docs)] +#![cfg_attr(not(test), forbid(unused_crate_dependencies))] +#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] #[cfg(feature = "alloc")] extern crate alloc; @@ -80,12 +79,9 @@ pub use encoding::Buffer; /// attribute. /// * Fields are hashed exactly in the order in which they are defined, so changing /// the fields order will change the hashing -/// * Hashing differnet types, generally, may result into the same hash if they have +/// * Hashing different types, generally, may result into the same hash if they have /// the same byte encoding. For instance: /// ```rust -/// use udigest::{udigest, Tag}; -/// use sha2::Sha256; -/// /// #[derive(udigest::Digestable, Debug)] /// struct PersonA { name: String } /// #[derive(udigest::Digestable, Debug)] @@ -94,10 +90,9 @@ pub use encoding::Buffer; /// let person_a = PersonA{ name: "Alice".into() }; /// let person_b = PersonB{ name: b"Alice".to_vec() }; /// -/// let tag = Tag::new("udigest.example"); /// assert_eq!( -/// udigest::(tag.clone(), &person_a), -/// udigest::(tag, &person_b), +/// udigest::hash::(&person_a), +/// udigest::hash::(&person_b), /// ) /// ``` /// `person_a` and `person_b` have exactly the same hash as they have the same bytes @@ -111,7 +106,7 @@ pub use encoding::Buffer; /// tag may include a version to distinguish hashes of the same structures across different versions. /// * `#[udigest(bound = "...")]` \ /// Specifies which generic bounds to use. By default, `udigest` will generate `T: Digestable` bound per -/// each generic `T`. This behavior can be overriden via this attribute. Example: +/// each generic `T`. This behavior can be overridden via this attribute. Example: /// ```rust /// #[derive(udigest::Digestable)] /// #[udigest(bound = "")] @@ -204,77 +199,90 @@ pub use udigest_derive::Digestable; pub mod encoding; -/// Domain separation tag (DST) -/// -/// The tag is used to distinguish different applications and provide better hygiene. -/// Having different tags will result into different hashes even if the value being -/// hashed is the same. -/// -/// Tag can be constructed from a bytestring (using constructor [`new`](Self::new)), -/// or from any structured data (using constructor [`new_structured`](Self::new_structured)). -#[derive(Clone)] -pub struct Tag(D); - -impl Tag { - /// Constructs a new tag from a bytestring - /// - /// If the tag is represented by a structured data, [`Tag::new_structured`] - /// constructor can be used instead. - pub fn new(tag: impl AsRef<[u8]>) -> Self { - Self::new_structured(Bytes(tag)) - } - - /// Constructs a new tag from a structured data - pub fn new_structured(tag: impl Digestable) -> Self { - Self::with_digest_and_structured_tag(D::new(), tag) - } +/// Digests a structured `value` using fixed-output hash function (like sha2-256) +#[cfg(feature = "digest")] +pub fn hash(value: &T) -> digest::Output { + let mut hash = encoding::BufferDigest(D::new()); + value.unambiguously_encode(encoding::EncodeValue::new(&mut hash)); + hash.0.finalize() +} - /// Constructs a new tag - /// - /// Similar to [`Tag::new_structured`] but takes also a digest to use - pub fn with_digest_and_structured_tag(mut hash: D, tag: impl Digestable) -> Self { - let mut header = encoding::EncodeStruct::new(&mut hash).with_tag(b"udigest.header"); - header.add_field("udigest_version").encode_leaf().chain("1"); - let tag_encoder = header.add_field("tag"); - tag.unambiguously_encode(tag_encoder); - header.finish(); - - Self(hash) +/// Digests a list of structured data using fixed-output hash function (like sha2-256) +#[cfg(feature = "digest")] +pub fn hash_iter( + iter: impl IntoIterator, +) -> digest::Output { + let mut hash = encoding::BufferDigest(D::new()); + let mut encoder = encoding::EncodeList::new(&mut hash).with_tag(b"udigest.list"); + for value in iter { + let item_encoder = encoder.add_item(); + value.unambiguously_encode(item_encoder); } + encoder.finish(); + hash.0.finalize() +} - /// Digests a structured `value` - /// - /// Alias to [`udigest`] in root of the crate - pub fn digest(self, value: impl Digestable) -> digest::Output { - udigest(self, value) - } +/// Digests a structured `value` using extendable-output hash function (like shake-256) +#[cfg(feature = "digest")] +pub fn hash_xof(value: &T) -> D::Reader +where + T: Digestable, + D: Default + digest::Update + digest::ExtendableOutput, +{ + let mut hash = encoding::BufferUpdate(D::default()); + value.unambiguously_encode(encoding::EncodeValue::new(&mut hash)); + hash.0.finalize_xof() +} - /// Digests a list of structured data - /// - /// Alias to [`udigest_iter`] in root of the crate - pub fn digest_iter(self, iter: impl IntoIterator) -> digest::Output { - udigest_iter(self, iter) +/// Digests a list of structured data using extendable-output hash function (like shake-256) +#[cfg(feature = "digest")] +pub fn hash_xof_iter(iter: impl IntoIterator) -> D::Reader +where + D: Default + digest::Update + digest::ExtendableOutput, +{ + let mut hash = encoding::BufferUpdate(D::default()); + let mut encoder = encoding::EncodeList::new(&mut hash).with_tag(b"udigest.list"); + for value in iter { + let item_encoder = encoder.add_item(); + value.unambiguously_encode(item_encoder); } + encoder.finish(); + hash.0.finalize_xof() } -/// Digests a structured `value` -pub fn udigest(mut tag: Tag, value: impl Digestable) -> digest::Output { - value.unambiguously_encode(encoding::EncodeValue::new(&mut tag.0)); - tag.0.finalize() +/// Digests a structured `value` using variable-output hash function (like blake2b) +#[cfg(feature = "digest")] +pub fn hash_vof(value: &T, out: &mut [u8]) -> Result<(), digest::InvalidOutputSize> +where + T: Digestable, + D: digest::VariableOutput + digest::Update, +{ + let mut hash = encoding::BufferUpdate(D::new(out.len())?); + value.unambiguously_encode(encoding::EncodeValue::new(&mut hash)); + hash.0 + .finalize_variable(out) + .map_err(|_| digest::InvalidOutputSize) } -/// Digests a list of structured data -pub fn udigest_iter( - mut tag: Tag, +/// Digests a list of structured data using variable-output hash function (like blake2b) +#[cfg(feature = "digest")] +pub fn hash_vof_iter( iter: impl IntoIterator, -) -> digest::Output { - let mut encoder = encoding::EncodeList::new(&mut tag.0).with_tag(b"udigest.list"); + out: &mut [u8], +) -> Result<(), digest::InvalidOutputSize> +where + D: digest::VariableOutput + digest::Update, +{ + let mut hash = encoding::BufferUpdate(D::new(out.len())?); + let mut encoder = encoding::EncodeList::new(&mut hash).with_tag(b"udigest.list"); for value in iter { let item_encoder = encoder.add_item(); value.unambiguously_encode(item_encoder); } encoder.finish(); - tag.0.finalize() + hash.0 + .finalize_variable(out) + .map_err(|_| digest::InvalidOutputSize) } /// A value that can be unambiguously digested diff --git a/udigest/tests/derive.rs b/udigest/tests/derive.rs index bd7d776..66760e8 100644 --- a/udigest/tests/derive.rs +++ b/udigest/tests/derive.rs @@ -38,9 +38,9 @@ pub enum EnumExample { something_else: SomeValue, }, Variant2(String, #[udigest(as_bytes)] Vec, #[udigest(skip)] Empty), - Vartiant3 {}, + Variant3 {}, Variant4(), - Vartiant5, + Variant5, } #[derive(udigest::Digestable)] diff --git a/udigest/tests/deterministic_hash.rs b/udigest/tests/deterministic_hash.rs new file mode 100644 index 0000000..2d39427 --- /dev/null +++ b/udigest/tests/deterministic_hash.rs @@ -0,0 +1,78 @@ +#[derive(udigest::Digestable)] +pub struct Person { + name: &'static str, + age: u16, + job_title: &'static str, +} + +const ALICE: Person = Person { + name: "Alice", + age: 24, + job_title: "cryptographer", +}; + +const BOB: Person = Person { + name: "Bob", + age: 25, + job_title: "research engineer", +}; + +#[test] +fn sha2_256() { + let alice_hash = udigest::hash::(&ALICE); + assert_eq!( + hex::encode(alice_hash.as_slice()), + "99e258d6a6ccc430a50dcbf4e9c8cfb59ad0b94b96b83f0182a9a68eb1c5438f", + ); + + let bob_hash = udigest::hash::(&BOB); + assert_eq!( + hex::encode(bob_hash.as_slice()), + "28474b5dec79b222b74badc2d78f9f81c0fbfd1ee04a134947cd07f44237ade3", + ); +} + +#[test] +fn shake256() { + use digest::XofReader; + + let mut hash = [0u8; 123]; + let mut alice_hash_reader = udigest::hash_xof::(&ALICE); + alice_hash_reader.read(&mut hash); + assert_eq!( + hex::encode(&hash), + "54809cf7b06438f9508785fb5e46bdfd7714b39b026e86fa7cc8a8442ae10bd5\ + 49baeced19ff0642b042ae4e92636536baec5748dad99e71fc53a4361734973ae\ + 2c4f1547305a76addd5b6076509ddbf91bd5beb71ba09598e265704d1e9a1c0c3\ + 5fae7f8e4958ceb38962fc8e6fc56e32bef4e88f64bc8a88f88a" + ); + + let mut bob_hash_reader = udigest::hash_xof::(&BOB); + bob_hash_reader.read(&mut hash); + assert_eq!( + hex::encode(&hash), + "f68ca9eeb7e09657fc54a5cbbd50acdd6d9fccd29ec1a3eb460b673ea59d64a9\ + b2ec8be97c7d7858ad6724cf8c27299569bd72193c77bb339883214a4477c0762\ + f9cf31a2d698562f57dff5ede03d6928feba694975445e7dabe3d67e67b710f26\ + 11f4f14471917bd447d199c32eb93dbcaf1fdbefe05132911991" + ); +} + +#[test] +fn blake2b() { + let mut out = [0u8; 63]; + + udigest::hash_vof::(&ALICE, &mut out).unwrap(); + assert_eq!( + hex::encode(&out), + "91d1ce144fd46ed5400895c8db5f2b39c95870020c6627af034a9fa09c2f2cc3\ + f4c8c7d4e8d38ff16e4f54360b4387c0439cf30c51c21c78f904cda9205023" + ); + + udigest::hash_vof::(&BOB, &mut out).unwrap(); + assert_eq!( + hex::encode(&out), + "2f916c687c82c0f37d31df061c0453e98d0655e1877d4a55ec1507514822a2c4\ + b7cac3ca66a5e3deb678f915210e93f2fc14591b987f121083623ab024ece4" + ); +}