Resolve #268 by adding a Zeroize to DigestTranscript which writes a full block

This is a 'better-than-nothing' attempt to invalidate its state.

Also replaces black_box features with usage of the rustversion crate.
This commit is contained in:
Luke Parker
2023-03-28 04:43:10 -04:00
parent 79aff5d4c8
commit 47be373eb0
12 changed files with 108 additions and 56 deletions

View File

@@ -2,6 +2,15 @@
#![no_std]
///! A transcript trait valid over a variety of transcript formats.
use zeroize::Zeroize;
use digest::{
typenum::{
consts::U32, marker_traits::NonZero, type_operators::IsGreaterOrEqual, operator_aliases::GrEq,
},
core_api::BlockSizeUser,
Digest, Output, HashMarker,
};
#[cfg(feature = "merlin")]
mod merlin;
@@ -12,13 +21,6 @@ pub use crate::merlin::MerlinTranscript;
#[cfg(any(test, feature = "tests"))]
pub mod tests;
use digest::{
typenum::{
consts::U32, marker_traits::NonZero, type_operators::IsGreaterOrEqual, operator_aliases::GrEq,
},
Digest, Output, HashMarker,
};
/// A transcript trait valid over a variety of transcript formats.
pub trait Transcript: Send + Clone {
type Challenge: Send + Sync + Clone + AsRef<[u8]>;
@@ -134,6 +136,61 @@ impl<D: Send + Clone + SecureDigest> Transcript for DigestTranscript<D> {
}
}
// Digest doesn't implement Zeroize
// Implement Zeroize for DigestTranscript by writing twice the block size to the digest in an
// attempt to overwrite the internal hash state/any leftover bytes
impl<D: Send + Clone + SecureDigest> Zeroize for DigestTranscript<D>
where
D: BlockSizeUser,
{
fn zeroize(&mut self) {
// Update in 4-byte chunks to reduce call quantity and enable word-level update optimizations
const WORD_SIZE: usize = 4;
// block_size returns the block_size in bytes
// Use a ceil div in case the block size isn't evenly divisible by our word size
let words = (D::block_size() + (WORD_SIZE - 1)) / WORD_SIZE;
for _ in 0 .. (2 * words) {
self.0.update([255; WORD_SIZE]);
}
// Hopefully, the hash state is now overwritten to the point no data is recoverable
// These writes may be optimized out if they're never read
// Attempt to get them marked as read
#[rustversion::since(1.66)]
fn mark_read<D: Send + Clone + SecureDigest>(transcript: &mut DigestTranscript<D>) {
// Just get a challenge from the state
let mut challenge = core::hint::black_box(transcript.0.clone().finalize());
challenge.as_mut().zeroize();
}
#[rustversion::before(1.66)]
fn mark_read<D: Send + Clone + SecureDigest>(transcript: &mut DigestTranscript<D>) {
// Get a challenge
let challenge = transcript.0.clone().finalize();
// Attempt to use subtle's, non-exposed black_box function, by creating a Choice from this
// challenge
let mut read = 0;
for byte in challenge.as_ref() {
read ^= byte;
}
challenge.as_mut().zeroize();
// Since this Choice isn't further read, its creation may be optimized out, including its
// internal black_box
// This remains our best attempt
let mut choice = bool::from(subtle::Choice::from(read >> 7));
read.zeroize();
choice.zeroize();
}
mark_read(self)
}
}
/// The recommended transcript, guaranteed to be secure against length-extension attacks.
#[cfg(feature = "recommended")]
pub type RecommendedTranscript = DigestTranscript<blake2::Blake2b512>;