sha256

Pure Haskell SHA-256, HMAC-SHA256 (docs.ppad.tech/sha256).
git clone git://git.ppad.tech/sha256.git
Log | Files | Refs | README | LICENSE

commit ca54cc562aa75c4a237fa3fe763347ebd98780d9
parent adef763fc3f9e28fddb383e154e0c320c899ea05
Author: Jared Tobin <jared@jtobin.io>
Date:   Sat, 10 Oct 2026 15:29:34 +0400

test: compare the dispatching and pure implementations

On hosts where the ARM path is selected, the pure Haskell strict
implementation was never exercised.  The public hash, hmac, _hmac_rr
and _hmac_rsb are now checked against Crypto.Hash.SHA256.Pure on
multi-block inputs, and _hmac_rr/_hmac_rsb against hmac itself, with
exhaustive sweeps over message and key lengths to cover every padding
case.

Diffstat:
Mtest/Property.hs | 123+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 123 insertions(+), 0 deletions(-)

diff --git a/test/Property.hs b/test/Property.hs @@ -1,13 +1,26 @@ {-# OPTIONS_GHC -fno-warn-orphans #-} +{-# LANGUAGE MagicHash #-} module Property ( properties ) where +import Data.Bits ((.|.)) +import qualified Data.Bits as B import qualified Data.ByteString as BS +import qualified Data.ByteString.Builder as BSB import qualified Data.ByteString.Lazy as BL +import Data.Word (Word8, Word32) +import Foreign.Marshal.Alloc (allocaBytes) +import Foreign.Marshal.Array (peekArray) +import Foreign.Ptr (Ptr) +import qualified GHC.Exts as Exts +import qualified GHC.Word import Crypto.Hash.SHA256 +import Crypto.Hash.SHA256.Internal (Registers(..)) +import qualified Crypto.Hash.SHA256.Pure as Pure import Test.Tasty +import qualified Test.Tasty.HUnit as H import qualified Test.Tasty.QuickCheck as Q import Test.QuickCheck.Instances.ByteString () @@ -41,6 +54,102 @@ hmac_chunking k m chunks = lazy_chunked = BL.fromChunks (chunk_by (map Q.getPositive chunks) m) in hmac_lazy k lazy_single == hmac_lazy k lazy_chunked +-- dispatching vs. pure implementations ------------------------------------ +-- +-- on hosts with the ARM SHA2 extension, the public functions use it, so +-- these compare it against the pure Haskell implementation. + +-- messages spanning several blocks +newtype Msg = Msg BS.ByteString + deriving Show + +instance Q.Arbitrary Msg where + arbitrary = do + l <- Q.chooseInt (0, 8 * 64) + Msg . BS.pack <$> Q.vectorOf l Q.arbitrary + +-- register-sized strings +newtype Bytes32 = Bytes32 BS.ByteString + deriving Show + +instance Q.Arbitrary Bytes32 where + arbitrary = Bytes32 . BS.pack <$> Q.vectorOf 32 Q.arbitrary + +-- big-endian words of a 32-byte string, as registers +to_registers :: BS.ByteString -> Registers +to_registers bs = R (w 0) (w 1) (w 2) (w 3) (w 4) (w 5) (w 6) (w 7) where + w :: Int -> Exts.Word32# + w i = case word32 (BS.take 4 (BS.drop (4 * i) bs)) of + GHC.Word.W32# x -> x + word32 :: BS.ByteString -> Word32 + word32 = BS.foldl' (\acc b -> acc `B.shiftL` 8 .|. fromIntegral b) 0 + +-- run a destination-pointer HMAC, returning the big-endian digest +run_ptr :: (Ptr Word32 -> Ptr Word32 -> IO ()) -> IO BS.ByteString +run_ptr act = allocaBytes 32 $ \rp -> allocaBytes 64 $ \bp -> do + act rp bp + ws <- peekArray 8 rp + pure (BL.toStrict (BSB.toLazyByteString (foldMap BSB.word32BE ws))) + +unmac :: MAC -> BS.ByteString +unmac (MAC m) = m + +-- deterministic input of the given length +msg :: Int -> BS.ByteString +msg l = BS.pack (fmap fromIntegral (take l [(7 :: Int), 20 ..])) + +hash_matches_pure :: Msg -> Bool +hash_matches_pure (Msg m) = hash m == Pure.hash m + +hmac_matches_pure :: Msg -> Msg -> Bool +hmac_matches_pure (Msg k) (Msg m) = unmac (hmac k m) == Pure.hmac k m + +hmac_rr_matches_hmac :: Bytes32 -> Bytes32 -> Q.Property +hmac_rr_matches_hmac (Bytes32 k) (Bytes32 m) = Q.ioProperty $ do + let expected = unmac (hmac k m) + a <- run_ptr (\rp bp -> _hmac_rr rp bp (to_registers k) (to_registers m)) + b <- run_ptr (\rp bp -> + Pure._hmac_rr rp bp (to_registers k) (to_registers m)) + pure (a == expected && b == expected) + +hmac_rsb_matches_hmac :: Bytes32 -> Bytes32 -> Word8 -> Msg -> Q.Property +hmac_rsb_matches_hmac (Bytes32 k) (Bytes32 v) sep (Msg dat) = + Q.ioProperty (rsb_matches k v sep dat) + +rsb_matches + :: BS.ByteString -> BS.ByteString -> Word8 -> BS.ByteString -> IO Bool +rsb_matches k v sep dat = do + let expected = unmac (hmac k (v <> BS.singleton sep <> dat)) + a <- run_ptr (\rp bp -> + _hmac_rsb rp bp (to_registers k) (to_registers v) sep dat) + b <- run_ptr (\rp bp -> + Pure._hmac_rsb rp bp (to_registers k) (to_registers v) sep dat) + pure (a == expected && b == expected) + +-- every input length through three blocks, hitting each padding case +hash_all_lengths :: TestTree +hash_all_lengths = H.testCase "hash ~ Pure.hash (all lengths)" $ + H.assertBool mempty $ + all (\l -> hash (msg l) == Pure.hash (msg l)) [0 .. 3 * 64] + +hmac_all_lengths :: TestTree +hmac_all_lengths = H.testCase "hmac ~ Pure.hmac (all lengths)" $ + H.assertBool mempty $ + all (\l -> unmac (hmac (msg 32) (msg l)) == Pure.hmac (msg 32) (msg l)) + [0 .. 3 * 64] + +hmac_all_key_lengths :: TestTree +hmac_all_key_lengths = H.testCase "hmac ~ Pure.hmac (all key lengths)" $ + H.assertBool mempty $ + all (\l -> unmac (hmac (msg l) (msg 3)) == Pure.hmac (msg l) (msg 3)) + [0 .. 2 * 64 + 1] + +hmac_rsb_all_lengths :: TestTree +hmac_rsb_all_lengths = H.testCase "_hmac_rsb ~ hmac (all lengths)" $ do + oks <- traverse (rsb_matches (msg 32) (BS.reverse (msg 32)) 0x01 . msg) + [0 .. 3 * 64] + H.assertBool mempty (and oks) + chunk_by :: [Int] -> BS.ByteString -> [BS.ByteString] chunk_by _ bs | BS.null bs = [] chunk_by [] bs = [bs] @@ -62,4 +171,18 @@ properties = testGroup "properties" [ Q.withMaxSuccess 1000 hash_chunking , Q.testProperty "hmac_lazy chunking-invariant" $ Q.withMaxSuccess 1000 hmac_chunking + , testGroup "dispatch ~ pure" [ + Q.testProperty "hash ~ Pure.hash" $ + Q.withMaxSuccess 1000 hash_matches_pure + , Q.testProperty "hmac ~ Pure.hmac" $ + Q.withMaxSuccess 1000 hmac_matches_pure + , Q.testProperty "_hmac_rr k m ~ hmac (cat k) (cat m)" $ + Q.withMaxSuccess 1000 hmac_rr_matches_hmac + , Q.testProperty "_hmac_rsb k v s d ~ hmac (cat k) (cat v <> s <> d)" $ + Q.withMaxSuccess 1000 hmac_rsb_matches_hmac + , hash_all_lengths + , hmac_all_lengths + , hmac_all_key_lengths + , hmac_rsb_all_lengths + ] ]