Skip to content
This repository was archived by the owner on Sep 20, 2023. It is now read-only.
This repository was archived by the owner on Sep 20, 2023. It is now read-only.

Incorrect hashes for all hash algorithms beyond 4 GiB of input #330

Closed
@nh2

Description

@nh2

I found a problem together with @chpatrick:

Click to show imports
:set -XTypeApplications
import qualified Crypto.Hash as Hash
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BS8
> let hash = show . Hash.hash @BS.ByteString @Hash.SHA1
> let a = BS8.replicate 5000000000 'a' <> BS8.pack "hello"
> let b = BS8.replicate 5000000000 'a' <> BS8.pack "world"
> a == b
False
> hash a == hash b
True

Two different ByteStrings hash to the same SHA1.

Trying SHA256, same issue:

> let hash = show . Hash.hash @BS.ByteString @Hash.SHA256
> hash a == hash b
True

Conclusion

  • fromIntegral is bad.
    Until we deprecate fromIntegral, Haskell code will always be subtly wrong and never be secure.
    I proposed this 3 years ago (easier-to-read threaded view here).
    Concretely:
    • If we don't fix this, people will shy away from using Haskell for serious work. I found this bug in production, and you can imagine how happy I am about that. Rust and C both do this better.
    • If the authors of key crypto libraries fall for these traps (no blame on them), who can get it right? We should remove the traps.

Details

Demonstrating that the the overflow occurs at the uint32 boundary:

Good:

> show $ Hash.hash @BS.ByteString @Hash.SHA1 (BS8.replicate (2^32 - 2) 'a' <> BS8.pack "b")
"78c1411ef0ae4e0328660b7dddc08c06000eb9f0"

> show $ Hash.hash @BS.ByteString @Hash.SHA1 (BS8.replicate (2^32 - 2) 'a' <> BS8.pack "c")
"21f556aba357dd19d7bf01a2a7f05482162ed3fc"

Bad:

> show $ Hash.hash @BS.ByteString @Hash.SHA1 (BS8.replicate (2^32 - 1) 'a' <> BS8.pack "b")
"da39a3ee5e6b4b0d3255bfef95601890afd80709"

> show $ Hash.hash @BS.ByteString @Hash.SHA1 (BS8.replicate (2^32 - 1) 'a' <> BS8.pack "c")
"da39a3ee5e6b4b0d3255bfef95601890afd80709"

Location of the problem:

For SHA1 (note uint32_t len):

void cryptonite_sha1_update(struct sha1_ctx *ctx, const uint8_t *data, uint32_t len)

For all hash algorithms (note Word32 for the length parameter):

hashInternalUpdate :: Ptr (Context a) -> Ptr Word8 -> Word32 -> IO ()
-- | Finalize the context and set the digest raw memory to the right value

This is where the silent overflow occurs due to fromIntegral:

mapM_ (\b -> B.withByteArray b $ \d -> hashInternalUpdate ctx d (fromIntegral $ B.length b)) ls

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions