sha-validation-0.1.0.0: src/Test/Hash/Internal.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveLift #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskellQuotes #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE ViewPatterns #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
-- |
-- Module: Test.Hash.Internal
-- Copyright: Copyright © 2022 Kadena LLC.
-- License: MIT
-- Maintainer: Lars Kuhtz <lars@kadena.io>
-- Stability: experimental
-- Description: Internal Definitions
--
module Test.Hash.Internal
(
-- * Validation Tests for SHA1, SHA2, and SHA3 Hash Algorithms
-- ** Response Files for Selected Long And Short Messages Test for Byte-Oriented Implementations
MsgFile(..)
, MsgVector(..)
, readMsgFile
, embedMsgFile
-- ** Response Files for Pseudorandomly Generated Messages (Monte Carlo) Tests
, MonteFile(..)
, MonteVector(..)
, readMonteFile
, embedMonteFile
-- * Validation Tests for SHA3-XOFs (Shake) Hash Algorithms
-- ** Response Files for Selected Long And Short Messages Test for Byte-Oriented Implementations
, ShakeMsgFile(..)
, ShakeMsgVector(..)
, readShakeMsgFile
, embedShakeMsgFile
-- ** Response Files for Pseudorandomly Generated Messages (Monte Carlo) Tests
, ShakeMonteFile(..)
, ShakeMonteVector(..)
, readShakeMonteFile
, embedShakeMonteFile
-- ** Response Files for Variable Output Tests for Byte-Oriented Implementations
, ShakeVarOutFile(..)
, ShakeVarOutVector(..)
, readShakeVarOutFile
, embedShakeVarOutFile
-- * Test Tools
, msgTest
, msgAssert
, monteTest
, monteAssert
-- * Internal: Embedding Response Files in Haskell Code
, embedIO
) where
import Control.Applicative
import Control.Monad
import Data.Attoparsec.Text.Lazy
import qualified Data.ByteString as B
import qualified Data.ByteString.Base16 as B16
import qualified Data.ByteString.Char8 as B8
import qualified Data.ByteString.Internal as B (ByteString(..))
import qualified Data.ByteString.Unsafe as B
import Data.Foldable
import Data.Functor
import qualified Data.List as L
import Data.Maybe
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.IO as TL
import qualified Data.Vector as V
import Language.Haskell.TH
import Language.Haskell.TH.Syntax
import Numeric.Natural
import System.Directory
import System.FilePath
import System.IO.Unsafe
-- -------------------------------------------------------------------------- --
-- Backward compatibility for template-haskell-2.16.0
#if MIN_VERSION_template_haskell(2,17,0)
code :: m (TExp a) -> Code m a
code = Code
#else
type Code a b = a (TExp b)
code :: a -> a
code = id
bindCode :: Q a -> (a -> Q (TExp a)) -> Code Q a
bindCode act l = act >>= l
#endif
-- -------------------------------------------------------------------------- --
-- Msg File
data MsgFile = MsgFile
{ _msgDescription :: !T.Text
, _msgL :: !Natural
, _msgVectors :: !(V.Vector MsgVector)
}
deriving (Show, Eq, Ord)
deriving (Lift)
data MsgVector = MsgVector
{ _msgLen :: !Natural
, _msgMsg :: !B.ByteString
, _msgMd :: !B.ByteString
}
deriving (Show, Eq, Ord)
deriving (Lift)
pMsgFile :: Parser MsgFile
pMsgFile = MsgFile
<$> pDescription
<*> (skipSpace *> pL)
<*> (skipSpace *> pVectors)
<* (skipSpace *> endOfInput)
<?> "RspMsgFile"
where
pVectors :: Parser (V.Vector MsgVector)
pVectors = V.fromList
<$> many1 (skipSpace *> pVector)
<?> "MsgVectors"
pVector :: Parser MsgVector
pVector = MsgVector
<$> (skipSpace *> pEquals "Len" decimal)
<*> (skipSpace *> pEquals "Msg" hexbytes)
<*> (skipSpace *> pEquals "MD" hexbytes)
<?> "MsgVector"
readMsgFile :: FilePath -> IO MsgFile
readMsgFile = parseFile "readMsgFile" pMsgFile
embedMsgFile :: FilePath -> Code Q MsgFile
embedMsgFile = embedIO . readMsgFile
-- -------------------------------------------------------------------------- --
-- SHA3 Monte File
data MonteFile = MonteFile
{ _monteDescription :: !T.Text
, _monteL :: !Natural
, _monteSeed :: !B.ByteString
, _monteVectors :: !(V.Vector MonteVector)
}
deriving (Show, Eq, Ord)
deriving (Lift)
data MonteVector = MonteVector
{ _monteCount :: !Natural
, _monteMd :: !B.ByteString
}
deriving (Show, Eq, Ord)
deriving (Lift)
pMonteFile :: Parser MonteFile
pMonteFile = MonteFile
<$> pDescription
<*> (skipSpace *> pL)
<*> (skipSpace *> pSeed)
<*> (skipSpace *> pVectors)
<* (skipSpace *> endOfInput)
<?> "RspMonteFile"
where
pSeed :: Parser B.ByteString
pSeed = pEquals "Seed" hexbytes <?> "Seed"
pVectors :: Parser (V.Vector MonteVector)
pVectors = V.fromList
<$> many1 (skipSpace *> pVector)
<?> "MonteVectors"
pVector :: Parser MonteVector
pVector = MonteVector
<$> (skipSpace *> pEquals "COUNT" decimal)
<*> (skipSpace *> pEquals "MD" hexbytes)
<?> "MonteVector"
readMonteFile :: FilePath -> IO MonteFile
readMonteFile = parseFile "readMonteFile" pMonteFile
embedMonteFile :: FilePath -> Code Q MonteFile
embedMonteFile = embedIO . readMonteFile
-- -------------------------------------------------------------------------- --
-- Shake Msg File
data ShakeMsgFile = ShakeMsgFile
{ _shakeMsgDescription :: !T.Text
, _shakeMsgOutputLen:: !Natural
, _shakeMsgVectors :: !(V.Vector MsgVector)
}
deriving (Show, Eq, Ord)
deriving (Lift)
data ShakeMsgVector = ShakeMsgVector
{ _shakeMsgLen :: !Natural
, _shakeMsgMsg :: !B.ByteString
, _shakeMsgOutput :: !B.ByteString
}
deriving (Show, Eq, Ord)
deriving (Lift)
pShakeMsgFile :: Parser ShakeMsgFile
pShakeMsgFile = ShakeMsgFile
<$> pDescription
<*> (skipSpace *> pOutputLength)
<*> (skipSpace *> pVectors)
<* (skipSpace *> endOfInput)
<?> "RspMsgFile"
where
pOutputLength :: Parser Natural
pOutputLength = pInBrackets (pEquals "Outputlen" decimal)
<?> "Outputlen"
pVectors :: Parser (V.Vector MsgVector)
pVectors = V.fromList
<$> many1 (skipSpace *> pVector)
<?> "MsgVectors"
pVector :: Parser MsgVector
pVector = MsgVector
<$> (skipSpace *> pEquals "Len" decimal)
<*> (skipSpace *> pEquals "Msg" hexbytes)
<*> (skipSpace *> pEquals "Output" hexbytes)
<?> "MsgVector"
readShakeMsgFile :: FilePath -> IO ShakeMsgFile
readShakeMsgFile = parseFile "readShakeMsgFile" pShakeMsgFile
embedShakeMsgFile :: FilePath -> Code Q ShakeMsgFile
embedShakeMsgFile = embedIO . readShakeMsgFile
-- -------------------------------------------------------------------------- --
-- SHAKE Monte File
data ShakeMonteFile = ShakeMonteFile
{ _shakeMonteDescription :: !T.Text
, _shakeMonteMinOutputBits :: !Natural
, _shakeMonteMaxOutputBits :: !Natural
, _shakeMonteMsg :: !B.ByteString
, _shakeMonteVectors :: !(V.Vector ShakeMonteVector)
}
deriving (Show, Eq, Ord)
deriving (Lift)
data ShakeMonteVector = ShakeMonteVector
{ _shakeMonteCount :: !Natural
, _shakeMonteOutputLen :: !Natural
, _shakeMonteOutput :: !B.ByteString
}
deriving (Show, Eq, Ord)
deriving (Lift)
pShakeMonteFile :: Parser ShakeMonteFile
pShakeMonteFile = ShakeMonteFile
<$> pDescription
<*> (skipSpace *> pMinOutput)
<*> (skipSpace *> pMaxOutput)
<*> (skipSpace *> pMsg)
<*> (skipSpace *> pVectors)
<* (skipSpace *> endOfInput)
<?> "RspMonteFile"
where
pMinOutput :: Parser Natural
pMinOutput = pInBrackets (pEquals "Minimum Output Length (bits)" decimal)
<?> "MinOutput"
pMaxOutput :: Parser Natural
pMaxOutput = pInBrackets (pEquals "Maximum Output Length (bits)" decimal)
<?> "MaxOutput"
pMsg :: Parser B.ByteString
pMsg = pEquals "Msg" hexbytes <?> "Msg"
pVectors :: Parser (V.Vector ShakeMonteVector)
pVectors = V.fromList
<$> many1 (skipSpace *> pVector)
<?> "ShakeMonteVectors"
pVector :: Parser ShakeMonteVector
pVector = ShakeMonteVector
<$> (skipSpace *> pEquals "COUNT" decimal)
<*> (skipSpace *> pEquals "Outputlen" decimal)
<*> (skipSpace *> pEquals "Output" hexbytes)
<?> "ShakeMonteVector"
readShakeMonteFile :: FilePath -> IO ShakeMonteFile
readShakeMonteFile = parseFile "readShakeMonteFile" pShakeMonteFile
embedShakeMonteFile :: FilePath -> Code Q ShakeMonteFile
embedShakeMonteFile = embedIO . readShakeMonteFile
-- -------------------------------------------------------------------------- --
-- SHAKE Variable Out File
data ShakeVarOutFile = ShakeVarOutFile
{ _shakeVarOutDescription :: !T.Text
, _shakeVarOutInputLength :: !Natural
, _shakeVarOutMinOutputBits :: !Natural
, _shakeVarOutMaxOutputBits :: !Natural
, _shakeVarOutVectors :: !(V.Vector ShakeVarOutVector)
}
deriving (Show, Eq, Ord)
deriving (Lift)
data ShakeVarOutVector = ShakeVarOutVector
{ _shakeVarOutCount :: !Natural
, _shakeVarOutOutputLen :: !Natural
, _shakeVarOutMsg :: !B.ByteString
, _shakeVarOutOutput :: !B.ByteString
}
deriving (Show, Eq, Ord)
deriving (Lift)
pShakeVarOutFile :: Parser ShakeVarOutFile
pShakeVarOutFile = ShakeVarOutFile
<$> pDescription
<* (skipSpace *> string "[Tested for Output of byte-oriented messages]")
<*> (skipSpace *> pInputLength)
<*> (skipSpace *> pMinOutput)
<*> (skipSpace *> pMaxOutput)
<*> (skipSpace *> pVectors)
<* (skipSpace *> endOfInput)
<?> "RspVarOutFile"
where
pInputLength :: Parser Natural
pInputLength = pInBrackets (pEquals "Input Length" decimal)
<?> "InputLength"
pMinOutput :: Parser Natural
pMinOutput = pInBrackets (pEquals "Minimum Output Length (bits)" decimal)
<?> "MinOutput"
pMaxOutput :: Parser Natural
pMaxOutput = pInBrackets (pEquals "Maximum Output Length (bits)" decimal)
<?> "MaxOutput"
pVectors :: Parser (V.Vector ShakeVarOutVector)
pVectors = V.fromList
<$> many1 (skipSpace *> pVector)
<?> "ShakeVarOutVectors"
pVector :: Parser ShakeVarOutVector
pVector = ShakeVarOutVector
<$> (skipSpace *> pEquals "COUNT" decimal)
<*> (skipSpace *> pEquals "Outputlen" decimal)
<*> (skipSpace *> pEquals "Msg" hexbytes)
<*> (skipSpace *> pEquals "Output" hexbytes)
<?> "ShakeVarOutVector"
readShakeVarOutFile :: FilePath -> IO ShakeVarOutFile
readShakeVarOutFile = parseFile "readShakeVarOutFile" pShakeVarOutFile
embedShakeVarOutFile :: FilePath -> Code Q ShakeVarOutFile
embedShakeVarOutFile = embedIO . readShakeVarOutFile
-- -------------------------------------------------------------------------- --
-- Common Parsers
pInBrackets :: Parser a -> Parser a
pInBrackets p = char '[' *> p <* char ']' <?> "pInBrackets"
pEquals :: T.Text -> Parser a -> Parser a
pEquals prompt p = string prompt
*> skipSpace
*> char '='
*> skipSpace *> p
<?> ("pEquals[" <> T.unpack prompt <> "]")
pDescription :: Parser T.Text
pDescription = T.intercalate "\n"
<$> sepBy descLine endOfLine
<?> "Description"
where
descLine = string "# " *> takeTill isEndOfLine <?> "Line"
pL :: Parser Natural
pL = pInBrackets (pEquals "L" decimal) <?> "L"
hexbytes :: Parser B.ByteString
hexbytes = go <?> "hexbytes"
where
go = do
h <- takeWhile1 (inClass "0-9a-fA-F")
-- h <- takeWhile1 (inClass "0123456789abcdefABCDEF")
-- h <- Data.Attoparsec.Text.Lazy.takeWhile (inClass "0-9a-fA-F")
case B16.decode (T.encodeUtf8 h) of
#if MIN_VERSION_base16_bytestring(1,0,0)
Left e -> fail $ "failed to decode hex-encoded bytes: " <> e
Right r -> return r
#else
(r,"") -> return r
(r,e) -> fail "failed to decode hex-encoded bytes because of invalid input characters"
#endif
parseFile :: String -> Parser a -> FilePath -> IO a
parseFile label p fp = parseOnly p <$> TL.readFile fp >>= \case
Right r -> return r
Left e -> error $ label <> ": failed to parse file " <> fp <> ": " <> e
-- -------------------------------------------------------------------------- --
-- File embedding
embedIO :: Lift a => IO a -> Code Q a
embedIO action = runIO action `bindCode` liftTyped
-- | The returned paths are relative to the given directory
--
listFiles :: String -> FilePath -> IO [FilePath]
listFiles suffix r = listDirectory r
>>= filterM (doesFileExist . (r </>))
>>= filterM (fmap readable . getPermissions . (r </>))
<&> filter (L.isSuffixOf ("." <> suffix))
{-
-- | Running this slice produces @[(FilePath, RspFile)]@.
--
-- It does not recurse into subdirectories and ignores any files that do
-- not have an the suffix @.rsp@.
--
-- The file path is the (relative) file name within the given directory.
--
embedRspFiles :: FilePath -> ExpQ
embedRspFiles fp = SigE
<$> (runIO (readRspDir fp) >>= lift)
<*> [t| [(FilePath, RspFile)] |]
readRspDir :: FilePath -> IO [(FilePath, RspFile)]
readRspDir fp = do
paths <- listRspFiles fp
forM paths $ \p -> (p,) <$> readRspFile (fp </> p)
-}
-- -------------------------------------------------------------------------- --
-- Orphan Lift instances
--
-- Requires template-haskell >=2.16
#if ! MIN_VERSION_bytestring(0,11,2)
instance Lift B.ByteString where
lift bs = return
$ AppE (VarE 'unsafePerformIO)
$ AppE
( AppE
(VarE 'B.unsafePackAddressLen)
(LitE (IntegerL $ fromIntegral $ B8.length bs))
)
(LitE (bytesPrimL (mkBytes ptr (fromIntegral off) (fromIntegral sz))))
where
B.PS ptr off sz = bs
liftTyped = code . unsafeTExpCoerce . lift
#endif
instance (Lift a) => Lift (V.Vector a) where
lift v = [| V.fromListN n' v' |]
where
n' = V.length v
v' = V.toList v
liftTyped = code . unsafeTExpCoerce . lift
-- -------------------------------------------------------------------------- --
-- Test Tools
-- | Check that all test vectors in a File are satisfied by a given hash
-- implementation.
--
msgTest :: (B.ByteString -> B.ByteString) -> MsgFile -> Bool
msgTest hash f = all (\v -> hashVector v == _msgMd v) (toList $ _msgVectors f)
where
hashVector v = hash
$ B.take (fromIntegral (_msgLen v) `quot` 8)
$ _msgMsg v
-- | For a given hash implementation, assert the correct result for each test
-- vector in a 'MsgFile'.
--
-- The function to assert equality is usually provided by some testing
-- framework.
--
msgAssert
:: Monad m
=> (String -> B.ByteString -> B.ByteString -> m ())
-- ^ Function to assertion Equality. The first argument is a test label,
-- the second argument is the actual value, and the thrid value is the
-- expected value.
-> (B.ByteString -> B.ByteString)
-- ^ Hash function
-> MsgFile
-> m ()
msgAssert assert hash f = forM_ vs $ \(i, v) ->
assert (mkTestLabel i (_msgMsg v)) (hashVector v) (_msgMd v)
where
vs = zip [1..] (toList $ _msgVectors f)
hashVector v = hash
$ B.take (fromIntegral (_msgLen v) `quot` 8)
$ _msgMsg v
-- | Check that all test vectors in a Monte Carlo File are satisfied by a given hash
-- implementation.
--
monteTest :: (B.ByteString -> B.ByteString) -> MonteFile -> Bool
monteTest hash f = go (_monteSeed f) (toList $ _monteVectors f)
where
go :: B.ByteString -> [MonteVector] -> Bool
go _ [] = True
go s ((_monteMd -> h) : t) = hashI 1000 s == h && go h t
-- Each Round consists of 1000 hash applications
hashI :: Natural -> B.ByteString -> B.ByteString
hashI 0 s = s
hashI i s = let s' = hash s in hashI (i - 1) s'
-- | For a given hash implementation, assert the correct result for each test
-- vector in a 'MonteFile'.
--
-- The function to assert equality is usually provided by some testing
-- framework.
--
monteAssert
:: Monad m
=> (String -> B.ByteString -> B.ByteString -> m ())
-- ^ Function to assertion Equality. The first argument is a test label,
-- the second argument is the actual value, and the thrid value is the
-- expected value.
-> (B.ByteString -> B.ByteString)
-- ^ Hash function
-> MonteFile
-> m ()
monteAssert assert hash f = go (_monteSeed f) (toList $ _monteVectors f)
where
go _ [] = return ()
go s (v : t) = do
let r = hashI 1000 s
let md = _monteMd v
assert (mkTestLabel (_monteCount v) md) md r
when (r == md) $ go md t
-- Each Round consists of 1000 hash applications
hashI :: Natural -> B.ByteString -> B.ByteString
hashI 0 s = s
hashI i s = let s' = hash s in hashI (i - 1) s'
mkTestLabel :: Natural -> B.ByteString -> String
mkTestLabel i input = show i <> "[" <> B8.unpack msg <> "]"
where
hex = B16.encode input
msg
| B.length hex <= 16 = B.take 16 hex
| otherwise = B.take 13 hex <> "..."