Skip to content

[traits] Issues with digest traits #1503

Description

@robinhundt

The digest traits were introduced recently (#1078) and haven't seen much use yet. While working on #1493 we noticed several issues of the traits. One issue, that finish didn't consume self (#1496) is fixed in #1493. However, there are more problems with the APIs.

Digest traits take const OUTPUT_LEN:usize as input generic parameter

Because the OUTPUT_LEN is a const generic parameter to the trait, there could be multiple implementations of the trait for different lengths for one hasher type. I.e., that following would be possible:

struct Sha3_224;

pub trait DigestIncremental<const OUTPUT_LEN: usize> {
    fn finish(state: &mut (), digest: &mut [u8; OUTPUT_LEN]);
}

impl DigestIncremental<14> for Sha3_224 {
    fn finish(state: &mut (), digest: &mut [u8; 28]) { todo!() }
}

impl DigestIncremental<16> for Sha3_224 {
    fn finish(state: &mut (), digest: &mut [u8; 32]) { todo!() }
}

Playground

Of course, we would not actually implement the trait in this non-sensical way. The problem is, that we could. Because the compiler can't necessarily know that there is only one implementation, there can be cases where we would need to explicitly specify which trait instantiation we want to call. I don't have a reproducer for such inference issues at the moment, but I know they can occur.

Additionally to trait inference problems, this design makes it hard to write code that is generic over hash functions with different output sizes. Everything that wants to operate on a generic hash needs to add a generic const OUTPUT_LEN:usize parameter.

Sadly, this isn't easy to resolve with current stable Rust. Ideally, we'd write something like

trait Digest {
    const OUTPUT_LEN: usize;
    
    fn finish(self) -> [u8; Self::OUTPUT_LEN];
}

Playground

But this isn't possible, because generic parameters can't be used in const contexts.

To work around this, the rust-crypto crates use hybrid-array (and previously used generic-array). If we wanted to get rid of the generic const parameter, we'd have to also solve this problem. Maybe there is a solution that solves the problem like hybrid-array, but is more easy to understand. Though my guess is, that it would look similar.

arrayref, owned, and slice variants of traits

Currently, there are 3 modules with 2 traits each for a total of 6 traits. The difference is the shape of the datatype into which the digest is written/returned as. I propose to merge the arrayref and owned traits and remove the slice variant. The result would look something like this:

trait Digest<const OUTPUT_LEN: usize> {
    fn finish(self) -> [u8; OUTPUT_LEN];
    
    fn finish_into(self, digest: &mut [u8; OUTPUT_LEN]); 
}

Playground

I'd argue there is no need to provide a fallible API that writes the digest to a slice. If a caller only has a slice available, they can perform the conversion to an array reference and error handling themselves.

Unclear interaction between digest traits and Hasher new type

Currently, the digest traits operate on a state that is an associated type of the traits and is passed into the corresponding methods. This state is then stored inside the Hasher new-type which then provides a more convenient (not trait-based) API on top of the digest traits. It is unclear to me why there is is split between the traits and the hasher new type. Using the Hasher new-type can easily hide missing trait implementations as we saw in #1493.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions