diff options
| -rw-r--r-- | .github/workflows/ci.yml | 4 | ||||
| -rw-r--r-- | Cargo.toml | 6 | ||||
| -rw-r--r-- | src/guts.rs | 163 |
3 files changed, 165 insertions, 8 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0eb486a..44852d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,8 +41,8 @@ jobs: - name: print instruction set support run: cargo run --quiet working-directory: ./tools/instruction_set_support - # Default tests plus Rayon. - - run: cargo test --features=rayon + # Default tests plus Rayon and Serde. + - run: cargo test --features=rayon,serde_derive # no_std tests. - run: cargo test --no-default-features @@ -30,6 +30,10 @@ std = ["digest/std"] # perform multi-threaded hashing. However, even if this feature is enabled, all # other APIs remain single-threaded. +# Implements `serde::{Serialize, Deserialize}` for `ExportedHasher` in the +# undocumented guts module. Most callers don't need this. +serde_derive = ["serde", "serde_json", "arrayvec/serde"] + # ---------- Features below this line are for internal testing only. ---------- # By default on x86_64, this crate uses Samuel Neves' hand-written assembly @@ -77,6 +81,8 @@ rayon = { version = "1.2.1", optional = true } cfg-if = "0.1.10" digest = "0.8.1" crypto-mac = "0.7.0" +serde = { version = "1.0", features = ["derive"], optional = true } +serde_json = { version = "1.0", optional = true } [dev-dependencies] hex = "0.4.2" diff --git a/src/guts.rs b/src/guts.rs index 88dcc86..0b54963 100644 --- a/src/guts.rs +++ b/src/guts.rs @@ -1,8 +1,31 @@ -// This module is for incremental use cases like the `bao` crate, which need to -// get their hands on internal chunk and parent chaining values. The vast -// majority of users should ignore this and use the publicly documented -// interface instead. +//! Semi-public, semi-stable APIs for "peeking under the hood." +//! +//! This module is hidden from the docs, and the vast majority of callers +//! should not use it. These APIs are experimental, and much more likely to +//! change than the rest of the crate. +//! +//! This moduls supports manipulating subtree chaining values directly, and +//! serializing `Hasher` state to external storage. All of these tools have the +//! potential to give you incorrect finalized hashes if they're misused, which +//! creates very tricky bugs and violates all sorts of security invariants. If +//! you're thinking about using these, consider [filing a GitHub +//! issue](https://github.com/BLAKE3-team/BLAKE3/issues/new) to discuss your +//! use case. We'd love to hear about it. +use crate::platform::{le_bytes_from_words_32, words_from_le_bytes_32}; +use arrayvec::ArrayVec; + +#[cfg(feature = "serde_derive")] +use serde::{Deserialize, Serialize}; + +/// An incremental state like `Hasher`, for computing possibly-non-root +/// chaining values of single chunks in the interior of the tree. +/// +/// This currently supports only the default hashing mode. If an incremental +/// user needs to expose `keyed_hash` or `derive_key`, we can add that. +/// +/// NOTE: This type is probably going to be replaced. See [this +/// discussion](https://github.com/BLAKE3-team/BLAKE3/issues/82). #[derive(Clone, Debug)] pub struct ChunkState(crate::ChunkState); @@ -39,8 +62,10 @@ impl ChunkState { } } -// As above, this currently assumes the regular hash mode. If an incremental -// user needs keyed_hash or derive_key, we can add that. +/// Similar to `ChunkState`, but for parent chaining values. +/// +/// This currently supports only the default hashing mode. If an incremental +/// user needs to expose `keyed_hash` or `derive_key`, we can add that. pub fn parent_cv( left_child: &crate::Hash, right_child: &crate::Hash, @@ -60,6 +85,73 @@ pub fn parent_cv( } } +/// A complete copy of the internal state of a `Hasher`, for serialization +/// purposes. This struct can contain buffered input bytes, and if you use a +/// secret key, a copy of that key. Is is very security sensitive. +#[derive(Clone)] +#[cfg_attr(feature = "serde_derive", derive(Serialize, Deserialize))] +pub struct ExportedHasher { + pub key: [u8; 32], + pub cv_stack: ArrayVec<[[u8; 32]; crate::MAX_DEPTH + 1]>, + pub chunk_cv: [u8; 32], + pub chunk_counter: u64, + pub chunk_buf: ArrayVec<[u8; 64]>, + pub blocks_compressed: u8, + pub flags: u8, +} + +/// Copy all the internal state of a `Hasher` to construct an `ExportedHasher`. +/// This is very security sensitive. +pub fn export_hasher(hasher: &crate::Hasher) -> ExportedHasher { + let mut chunk_buf = ArrayVec::new(); + chunk_buf + .try_extend_from_slice(&hasher.chunk_state.buf[..hasher.chunk_state.buf_len as usize]) + .unwrap(); + ExportedHasher { + key: le_bytes_from_words_32(&hasher.key), + cv_stack: hasher.cv_stack.clone(), + chunk_cv: le_bytes_from_words_32(&hasher.chunk_state.cv), + chunk_counter: hasher.chunk_state.chunk_counter, + chunk_buf, + blocks_compressed: hasher.chunk_state.blocks_compressed, + flags: hasher.chunk_state.flags, + } +} + +/// Reconstitute a `Hasher` from an `ExportedHasher`. The `ExportedHasher` must +/// come from a call to `export_hasher`, and its contents must not be modified +/// in any way. The input to this `unsafe` function is fully trusted. If a bug +/// or an attacker corrupts the input, it might trigger UB. If it doesn't +/// trigger UB today, this crate might be modified such that it triggers UB in +/// the future. +pub unsafe fn import_hasher(exported: &ExportedHasher) -> crate::Hasher { + // We don't assert everything here, but these are some essentials. + assert_eq!( + exported.chunk_counter.count_ones() as usize, + exported.cv_stack.len(), + "wrong number of entries in the CV stack", + ); + let allowed_flags = crate::KEYED_HASH | crate::DERIVE_KEY_MATERIAL; + assert_eq!(0, exported.flags & !allowed_flags, "unexpected flags"); + + let mut chunk_buf = [0; crate::BLOCK_LEN]; + chunk_buf[..exported.chunk_buf.len()].copy_from_slice(&exported.chunk_buf); + let chunk_state = crate::ChunkState { + cv: words_from_le_bytes_32(&exported.chunk_cv), + chunk_counter: exported.chunk_counter, + buf: chunk_buf, + buf_len: exported.chunk_buf.len() as u8, + blocks_compressed: exported.blocks_compressed, + flags: exported.flags, + platform: crate::platform::Platform::detect(), + }; + crate::Hasher { + key: words_from_le_bytes_32(&exported.key), + chunk_state, + cv_stack: exported.cv_stack.clone(), + } +} + #[cfg(test)] mod test { use super::*; @@ -92,4 +184,63 @@ mod test { let root = parent_cv(&parent, &chunk2_cv, true); assert_eq!(hasher.finalize(), root); } + + #[test] + fn test_export_and_import_hasher() { + let mut input = [0; 23456]; // results in a non-zero block counter + crate::test::paint_test_input(&mut input); + let mut test_key = [0; 32]; + test_key.copy_from_slice(&input[1000..1032]); + let mut hasher = crate::Hasher::new_keyed(&test_key); + hasher.update(&input); + + let exported = export_hasher(&hasher); + let mut imported = unsafe { import_hasher(&exported) }; + + hasher.update(&input); + imported.update(&input); + assert_eq!(hasher.finalize(), imported.finalize()); + } + + #[test] + #[cfg(feature = "serde_derive")] + fn test_serialize_hasher() { + let mut input = [0; 23456]; // results in a non-zero block counter + crate::test::paint_test_input(&mut input); + let test_context = "BLAKE3 2020-05-06 12:21:23 test serde"; + let mut hasher = crate::Hasher::new_derive_key(test_context); + hasher.update(&input); + + let exported = export_hasher(&hasher); + let serialized = serde_json::to_string_pretty(&exported).unwrap(); + let deserialized = serde_json::from_str(&serialized).unwrap(); + let mut imported = unsafe { import_hasher(&deserialized) }; + + hasher.update(&input); + imported.update(&input); + assert_eq!(hasher.finalize(), imported.finalize()); + } + + #[test] + #[should_panic] + fn test_bad_chunk_counter_panics() { + let mut exported = export_hasher(&crate::Hasher::new()); + // The chunk_counter needs to correspond to the number of chaining + // values in the CV stack. + exported.chunk_counter += 1; + unsafe { + import_hasher(&exported); + } + } + + #[test] + #[should_panic] + fn test_bad_flags_panic() { + let mut exported = export_hasher(&crate::Hasher::new()); + // It shouldn't be possible to set random bitflags. + exported.flags |= 1 << 7; + unsafe { + import_hasher(&exported); + } + } } |
