hmac-drbg

Pure Haskell HMAC-DRBG (docs.ppad.tech/hmac-drbg).
git clone git://git.ppad.tech/hmac-drbg.git
Log | Files | Refs | README | LICENSE

commit 41529155f1d85720234136978ab4e5313b472bcc
parent 298a9e4eb8f2149c0e04d1b97ee1f4869be65651
Author: Jared Tobin <jared@jtobin.io>
Date:   Sun,  1 Feb 2026 15:34:43 +0400

lib: major refactoring

Diffstat:
MCHANGELOG | 18++++++++++++++++++
Mbench/Main.hs | 31++++++++++++++++---------------
Mbench/Weight.hs | 29+++++++++++++++++++++--------
Dlib/Crypto/DRBG/HMAC.hs | 280-------------------------------------------------------------------------------
Alib/Crypto/DRBG/HMAC/SHA512.hs | 383+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mppad-hmac-drbg.cabal | 6+++---
Mtest/Main.hs | 85+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
7 files changed, 492 insertions(+), 340 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG @@ -1,5 +1,23 @@ # Changelog +- 0.3.0 (2026-02-01) + * This backwards-incompatible version changes the "bring your own HMAC + function" design and simply provides specialized DRBGs for + HMAC-SHA256 and HMAC-SHA512, respectively. These HMAC functions are + provided by ppad-sha256 and ppad-sha512. + + * The rationale here is to provide better security guarantees around + the DRBG state. Now, the DRBG state is restricted to a single, pinned, + heap-allocated mutable buffer; components of it are /never/ + allocated anywhere else on the heap during DRBG operation (not even + in temporary, to-be-GC'd bytestrings). A new 'wipe' function is also + exposed for explicitly zeroing out state when one is finished + generating bytes from the DRBG. + + * Aside from the increased security guarantees, DRBG performance is + dramatically improved, and other heap allocation dramatically + limited, compared to previous versions. + - 0.2.1 (2026-01-10) * Simply adds bounds to the ppad-sha{256,512} dependencies in the test and benchmark suites. diff --git a/bench/Main.hs b/bench/Main.hs @@ -5,29 +5,30 @@ module Main where import Criterion.Main -import qualified Crypto.DRBG.HMAC.SHA256 as DRBG +import qualified Crypto.DRBG.HMAC.SHA256 as DRBG256 +import qualified Crypto.DRBG.HMAC.SHA512 as DRBG512 main :: IO () main = do - !drbg256 <- DRBG.new mempty mempty mempty - -- !drbg512 <- DRBG.new mempty mempty mempty + !drbg256 <- DRBG256.new mempty mempty mempty + !drbg512 <- DRBG512.new mempty mempty mempty defaultMain [ - suite drbg256 + suite drbg256 drbg512 ] -suite drbg256 = +suite drbg256 drbg512 = bgroup "ppad-hmac-drbg" [ bgroup "HMAC-SHA256" [ - bench "new" $ whnfAppIO (DRBG.new mempty mempty) mempty - , bench "reseed" $ whnfAppIO (DRBG.reseed drbg256 mempty) mempty - , bench "gen (32B)" $ whnfAppIO (DRBG.gen drbg256 mempty) 32 - , bench "gen (256B)" $ whnfAppIO (DRBG.gen drbg256 mempty) 256 + bench "new" $ whnfAppIO (DRBG256.new mempty mempty) mempty + , bench "reseed" $ whnfAppIO (DRBG256.reseed drbg256 mempty) mempty + , bench "gen (32B)" $ whnfAppIO (DRBG256.gen drbg256 mempty) 32 + , bench "gen (256B)" $ whnfAppIO (DRBG256.gen drbg256 mempty) 256 + ] + , bgroup "HMAC-SHA512" [ + bench "new" $ whnfAppIO (DRBG512.new mempty mempty) mempty + , bench "reseed" $ whnfAppIO (DRBG512.reseed drbg512 mempty) mempty + , bench "gen (32B)" $ whnfAppIO (DRBG512.gen drbg512 mempty) 32 + , bench "gen (256B)" $ whnfAppIO (DRBG512.gen drbg512 mempty) 256 ] - -- , bgroup "HMAC-SHA512" [ - -- bench "new" $ whnfAppIO (DRBG.new mempty mempty) mempty - -- , bench "reseed" $ whnfAppIO (DRBG.reseed drbg512 mempty) mempty - -- , bench "gen (32B)" $ whnfAppIO (DRBG.gen drbg512 mempty) 32 - -- , bench "gen (256B)" $ whnfAppIO (DRBG.gen drbg512 mempty) 256 - -- ] ] diff --git a/bench/Weight.hs b/bench/Weight.hs @@ -9,21 +9,34 @@ module Main where import Control.DeepSeq -import qualified Crypto.DRBG.HMAC.SHA256 as DRBG +import qualified Crypto.DRBG.HMAC.SHA256 as DRBG256 +import qualified Crypto.DRBG.HMAC.SHA512 as DRBG512 import Weigh -instance NFData (DRBG.DRBG s) where +instance NFData (DRBG256.DRBG s) where rnf d = d `seq` () -instance NFData DRBG.Error where +instance NFData DRBG256.Error where + rnf e = e `seq` () + +instance NFData (DRBG512.DRBG s) where + rnf d = d `seq` () + +instance NFData DRBG512.Error where rnf e = e `seq` () main :: IO () main = do - !drbg <- DRBG.new mempty mempty mempty + !drbg256 <- DRBG256.new mempty mempty mempty + !drbg512 <- DRBG512.new mempty mempty mempty mainWith $ do wgroup "HMAC-SHA256" $ do - io "new" (DRBG.new mempty mempty) mempty - io "reseed" (DRBG.reseed drbg mempty) mempty - io "gen (32B)" (DRBG.gen drbg mempty) 32 - io "gen (256B)" (DRBG.gen drbg mempty) 256 + io "new" (DRBG256.new mempty mempty) mempty + io "reseed" (DRBG256.reseed drbg256 mempty) mempty + io "gen (32B)" (DRBG256.gen drbg256 mempty) 32 + io "gen (256B)" (DRBG256.gen drbg256 mempty) 256 + wgroup "HMAC-SHA512" $ do + io "new" (DRBG512.new mempty mempty) mempty + io "reseed" (DRBG512.reseed drbg512 mempty) mempty + io "gen (32B)" (DRBG512.gen drbg512 mempty) 32 + io "gen (256B)" (DRBG512.gen drbg512 mempty) 256 diff --git a/lib/Crypto/DRBG/HMAC.hs b/lib/Crypto/DRBG/HMAC.hs @@ -1,280 +0,0 @@ -{-# OPTIONS_HADDOCK prune #-} -{-# OPTIONS_GHC -funbox-small-strict-fields #-} -{-# LANGUAGE BangPatterns #-} - --- | --- Module: Crypto.DRBG.HMAC --- Copyright: (c) 2024 Jared Tobin --- License: MIT --- Maintainer: Jared Tobin <jared@ppad.tech> --- --- A pure HMAC-DRBG implementation, as specified by --- [NIST SP-800-90A](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-90Ar1.pdf). - -module Crypto.DRBG.HMAC ( - -- * DRBG and HMAC function types - DRBG - , HMAC - , Error(..) - , _read_v - , _read_k - - -- * DRBG interaction - , new - , gen - , reseed - ) where - -import Control.Monad.Primitive (PrimMonad, PrimState) -import qualified Data.ByteString as BS -import qualified Data.ByteString.Builder as BSB -import qualified Data.ByteString.Builder.Extra as BE -import qualified Data.ByteString.Internal as BI -import qualified Data.Primitive.MutVar as P -import Data.Word (Word64) - --- keystroke savers and utilities --------------------------------------------- - -fi :: (Integral a, Num b) => a -> b -fi = fromIntegral -{-# INLINE fi #-} - -to_strict :: BSB.Builder -> BS.ByteString -to_strict = BS.toStrict . BSB.toLazyByteString -{-# INLINE to_strict #-} - -to_strict_small :: BSB.Builder -> BS.ByteString -to_strict_small = BS.toStrict . BE.toLazyByteStringWith - (BE.safeStrategy 128 BE.smallChunkSize) mempty -{-# INLINE to_strict_small #-} - --- dumb strict pair -data Pair a b = Pair !a !b - deriving Show - --- types ---------------------------------------------------------------------- - --- | A DRBG error. -data Error = - MaxBytesExceeded -- ^ More than 65536 bytes have been requested. - | ReseedRequired -- ^ The DRBG must be reseeded (via 'reseed'). - deriving (Eq, Show) - --- see SP 800-90A table 2 -_RESEED_COUNTER :: Word64 -_RESEED_COUNTER = (2 :: Word64) ^ (48 :: Word64) - --- | A deterministic random bit generator (DRBG). --- --- Create a DRBG with 'new', and then use and reuse it to generate --- bytes as needed. --- --- >>> drbg <- new hmac entropy nonce personalization_string --- >>> bytes0 <- gen addl_bytes 16 drbg --- >>> bytes1 <- gen addl_bytes 16 drbg --- >>> drbg --- "<drbg>" -newtype DRBG s = DRBG (P.MutVar s DRBGState) - -instance Show (DRBG s) where - show _ = "<drbg>" - --- DRBG environment data and state -data DRBGState = DRBGState - !HMACEnv -- hmac function & outlen - {-# UNPACK #-} !Word64 -- reseed counter - {-# UNPACK #-} !BS.ByteString -- v - {-# UNPACK #-} !BS.ByteString -- key - --- NB following synonym really only exists to make haddocks more --- readable - --- | A HMAC function, taking a key as the first argument and the input --- value as the second, producing a MAC digest. --- --- >>> import qualified Crypto.Hash.SHA256 as SHA256 --- >>> let hmac k b = let SHA256.MAC m = SHA256.hmac k b in m --- >>> :t hmac --- hmac :: BS.ByteString -> BS.ByteString -> BS.ByteString -type HMAC = BS.ByteString -> BS.ByteString -> BS.ByteString - --- HMAC function and its associated outlength -data HMACEnv = HMACEnv - !HMAC - {-# UNPACK #-} !Word64 - --- the following convenience functions are useful for testing - -_read_v - :: PrimMonad m - => DRBG (PrimState m) - -> m BS.ByteString -_read_v (DRBG mut) = do - DRBGState _ _ v _ <- P.readMutVar mut - pure v - -_read_k - :: PrimMonad m - => DRBG (PrimState m) - -> m BS.ByteString -_read_k (DRBG mut) = do - DRBGState _ _ _ k <- P.readMutVar mut - pure k - --- drbg interaction ------------------------------------------------------ - --- | Create a DRBG from the supplied HMAC function, entropy, nonce, and --- personalization string. --- --- You can instantiate the DRBG using any appropriate HMAC function; --- it should merely take a key and value as input, as is standard, and --- return a MAC digest, each being a strict 'ByteString'. --- --- The DRBG is returned in any 'PrimMonad', e.g. 'ST' or 'IO'. --- --- >>> import qualified Crypto.Hash.SHA256 as SHA256 --- >>> let hmac k b = let SHA256.MAC m = SHA256.hmac k b in m --- >>> new hmac entropy nonce personalization_string --- "<drbg>" -new - :: PrimMonad m - => HMAC -- ^ HMAC function - -> BS.ByteString -- ^ entropy - -> BS.ByteString -- ^ nonce - -> BS.ByteString -- ^ personalization string - -> m (DRBG (PrimState m)) -new hmac entropy nonce ps = do - let !drbg = new_pure hmac entropy nonce ps - mut <- P.newMutVar drbg - pure (DRBG mut) - --- | Generate bytes from a DRBG, optionally injecting additional bytes --- per SP 800-90A. --- --- Per SP 800-90A, the maximum number of bytes that can be requested --- on any invocation is 65536. Larger requests will return --- 'MaxBytesExceeded'. --- --- >>> import qualified Data.ByteString.Base16 as B16 --- >>> drbg <- new hmac entropy nonce personalization_string --- >>> Right bytes0 <- gen addl_bytes 16 drbg --- >>> Right bytes1 <- gen addl_bytes 16 drbg --- >>> B16.encode bytes0 --- "938d6ca6d0b797f7b3c653349d6e3135" --- >>> B16.encode bytes1 --- "5f379d16de6f2c6f8a35c56f13f9e5a5" -gen - :: PrimMonad m - => BS.ByteString -- ^ additional bytes to inject - -> Word64 -- ^ number of bytes to generate - -> DRBG (PrimState m) - -> m (Either Error BS.ByteString) -gen addl bytes (DRBG mut) = do - drbg0 <- P.readMutVar mut - case gen_pure addl bytes drbg0 of - Left e -> pure (Left e) - Right !(Pair bs drbg1) -> do - P.writeMutVar mut drbg1 - pure (Right bs) - --- | Reseed a DRBG. --- --- Each DRBG has an internal /reseed counter/ that tracks the number --- of requests made to the generator (note /requests made/, not bytes --- generated). SP 800-90A specifies that a HMAC-DRBG should support --- 2 ^ 48 requests before requiring a reseed, so in practice you're --- unlikely to ever need to use this to actually reset the counter. --- --- Note however that 'reseed' can be used to implement "explicit" --- prediction resistance, per SP 800-90A, by injecting entropy generated --- elsewhere into the DRBG. --- --- >>> import qualified System.Entropy as E --- >>> entropy <- E.getEntropy 32 --- >>> reseed entropy addl_bytes drbg --- "<reseeded drbg>" -reseed - :: PrimMonad m - => BS.ByteString -- ^ entropy to inject - -> BS.ByteString -- ^ additional bytes to inject - -> DRBG (PrimState m) - -> m () -reseed ent add (DRBG drbg) = P.modifyMutVar' drbg (reseed_pure ent add) - --- pure drbg interaction ------------------------------------------------------ - --- SP 800-90A 10.1.2.2 -update_pure - :: BS.ByteString - -> DRBGState - -> DRBGState -update_pure provided_data (DRBGState h@(HMACEnv hmac _) r v0 k0) = - let !k1 = hmac k0 (cat v0 0x00 provided_data) - !v1 = hmac k1 v0 - in if BS.null provided_data - then DRBGState h r v1 k1 - else let !k2 = hmac k1 (cat v1 0x01 provided_data) - !v2 = hmac k2 v1 - in DRBGState h r v2 k2 - where - cat bs byte suf@(BI.PS _ _ l) = - let bil = BSB.byteString bs <> BSB.word8 byte <> BSB.byteString suf - in if l < 64 - then to_strict_small bil - else to_strict bil - {-# INLINE cat #-} - --- SP 800-90A 10.1.2.3 -new_pure - :: HMAC -- HMAC function - -> BS.ByteString -- entropy - -> BS.ByteString -- nonce - -> BS.ByteString -- personalization string - -> DRBGState -new_pure hmac entropy nonce ps = - let !drbg = DRBGState (HMACEnv hmac outlen) 1 v0 k0 - in update_pure seed_material drbg - where - seed_material = entropy <> nonce <> ps - outlen = fi (BS.length (hmac mempty mempty)) - k0 = BS.replicate (fi outlen) 0x00 - v0 = BS.replicate (fi outlen) 0x01 - --- SP 800-90A 10.1.2.4 -reseed_pure :: BS.ByteString -> BS.ByteString -> DRBGState -> DRBGState -reseed_pure entropy addl drbg = - let !(DRBGState h _ v k) = update_pure (entropy <> addl) drbg - in DRBGState h 1 v k - --- SP 800-90A 10.1.2.5 -gen_pure - :: BS.ByteString - -> Word64 - -> DRBGState - -> Either Error (Pair BS.ByteString DRBGState) -gen_pure addl bytes drbg0@(DRBGState h@(HMACEnv hmac outlen) _ _ _) - | bytes > 0x10000 = Left MaxBytesExceeded - | r > _RESEED_COUNTER = Left ReseedRequired - | otherwise = - let !(Pair temp drbg1) = loop mempty 0 v1 - returned_bits = BS.take (fi bytes) temp - drbg = update_pure addl drbg1 - in Right (Pair returned_bits drbg) - where - !(DRBGState _ r v1 k1) - | BS.null addl = drbg0 - | otherwise = update_pure addl drbg0 - - loop !acc !len !vl - | len < bytes = - let nv = hmac k1 vl - nacc = acc <> BSB.byteString nv - nlen = len + outlen - in loop nacc nlen nv - - | otherwise = - let facc | bytes < 128 = to_strict_small acc - | otherwise = to_strict acc - in Pair facc (DRBGState h (succ r) vl k1) -{-# INLINE gen_pure #-} - diff --git a/lib/Crypto/DRBG/HMAC/SHA512.hs b/lib/Crypto/DRBG/HMAC/SHA512.hs @@ -0,0 +1,383 @@ +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE MagicHash #-} +{-# LANGUAGE PatternSynonyms #-} +{-# LANGUAGE UnboxedTuples #-} + +-- | +-- Module: Crypto.DRBG.HMAC.SHA512 +-- Copyright: (c) 2024 Jared Tobin +-- License: MIT +-- Maintainer: Jared Tobin <jared@ppad.tech> +-- +-- A pure HMAC-DRBG implementation, as specified by +-- [NIST SP-800-90A](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-90Ar1.pdf). + +module Crypto.DRBG.HMAC.SHA512 ( + -- * DRBG and HMAC function types + DRBG + , Error(..) + + -- * DRBG interaction + , new + , gen + , reseed + , wipe + + -- for testing + , _read_v + , _read_k + ) where + +import qualified Crypto.Hash.SHA512 as SHA512 +import Crypto.Hash.SHA512.Internal (Registers(..)) +import qualified Crypto.Hash.SHA512.Internal as SHA512 (cat) +import Control.Monad.Primitive (PrimMonad, PrimState) +import qualified Control.Monad.Primitive as Prim (unsafeIOToPrim) +import qualified Data.ByteString as BS +import qualified Data.ByteString.Builder as BSB +import qualified Data.ByteString.Internal as BI +import qualified Data.Primitive.PrimArray as PA +import Data.Word (Word64) +import qualified GHC.Word +import qualified Foreign.Ptr as FP + +-- api ------------------------------------------------------------------------ + +-- | A DRBG error. +data Error = + MaxBytesExceeded -- ^ More than 65536 bytes have been requested. + | ReseedRequired -- ^ The DRBG must be reseeded (via 'reseed'). + deriving (Eq, Show) + +-- see SP 800-90A table 2 +_RESEED_COUNTER :: Word64 +_RESEED_COUNTER = (2 :: Word64) ^ (48 :: Word64) +{-# NOINLINE _RESEED_COUNTER #-} + +-- | A deterministic random bit generator (DRBG). +-- +-- Create a DRBG with 'new', and then use and reuse it to generate +-- bytes as needed. +-- +-- >>> drbg <- new entropy nonce personalization_string +-- >>> bytes0 <- gen drbg mempty 10 +-- >>> bytes1 <- gen drbg mempty 10 +-- >>> drbg +-- "<drbg>" + +-- layout (Word64 array): +-- index 0: counter +-- indices 1-8: k (8 Word64s = 64 bytes) +-- indices 9-16: v (8 Word64s = 64 bytes) +-- indices 17-32: scratch space (16 Word64s = 128 bytes) +newtype DRBG s = DRBG (PA.MutablePrimArray s Word64) + +instance Show (DRBG s) where + show _ = "<drbg>" + +-- | Create a HMAC-SHA512 DRBG from the supplied entropy, nonce, and +-- personalization string. +-- +-- The DRBG is returned in any 'PrimMonad', e.g. 'ST s' or 'IO'. +-- +-- >>> new entropy nonce personalization_string +-- "<drbg>" +new + :: PrimMonad m + => BS.ByteString -- ^ entropy + -> BS.ByteString -- ^ nonce + -> BS.ByteString -- ^ personalization string + -> m (DRBG (PrimState m)) +new entropy nonce ps = do + drbg <- PA.newPinnedPrimArray 33 -- 1 (ctr) + 16 (k, v) + 16 (scratch) + init_counter drbg + PA.setPrimArray drbg 01 08 (0x0000000000000000 :: Word64) -- init k + PA.setPrimArray drbg 09 08 (0x0101010101010101 :: Word64) -- init v + PA.setPrimArray drbg 17 16 (0x0000000000000000 :: Word64) -- scratch + update drbg (entropy <> nonce <> ps) + pure $! DRBG drbg +{-# INLINABLE new #-} + +-- | Reseed a DRBG. +-- +-- Each DRBG has an internal /reseed counter/ that tracks the number +-- of requests made to the generator (note /requests made/, not bytes +-- generated). SP 800-90A specifies that a HMAC-DRBG should support +-- 2 ^ 48 requests before requiring a reseed, so in practice you're +-- unlikely to ever need to use this to actually reset the counter. +-- +-- Note however that 'reseed' can be used to implement "explicit" +-- prediction resistance, per SP 800-90A, by injecting entropy generated +-- elsewhere into the DRBG. +-- +-- >>> import qualified System.Entropy as E +-- >>> entropy <- E.getEntropy 32 +-- >>> reseed entropy addl_bytes drbg +-- "<reseeded drbg>" +reseed + :: PrimMonad m + => DRBG (PrimState m) + -> BS.ByteString + -> BS.ByteString + -> m () +reseed (DRBG drbg) entr addl = do + update drbg (entr <> addl) + init_counter drbg +{-# INLINE reseed #-} + +-- | Generate bytes from a DRBG, optionally injecting additional bytes +-- per SP 800-90A. +-- +-- Per SP 800-90A, the maximum number of bytes that can be requested +-- on any invocation is 65536. Larger requests will return +-- 'MaxBytesExceeded'. +-- +-- >>> import qualified Data.ByteString.Base16 as B16 +-- >>> drbg <- new entropy nonce personalization_string +-- >>> Right bytes0 <- gen drbg addl_bytes 16 +-- >>> Right bytes1 <- gen drbg addl_bytes 16 +-- >>> B16.encode bytes0 +-- "938d6ca6d0b797f7b3c653349d6e3135" +-- >>> B16.encode bytes1 +-- "5f379d16de6f2c6f8a35c56f13f9e5a5" +gen + :: PrimMonad m + => DRBG (PrimState m) + -> BS.ByteString + -> Word64 + -> m (Either Error BS.ByteString) +gen (DRBG drbg) addl@(BI.PS _ _ l) bytes + | bytes > 0x10000 = pure $! Left MaxBytesExceeded + | otherwise = do + ctr <- read_counter drbg + if ctr > _RESEED_COUNTER + then pure $! Left ReseedRequired + else do + if l == 0 then pure () else update drbg addl + !(GHC.Word.W64# k00) <- PA.readPrimArray drbg 01 + !(GHC.Word.W64# k01) <- PA.readPrimArray drbg 02 + !(GHC.Word.W64# k02) <- PA.readPrimArray drbg 03 + !(GHC.Word.W64# k03) <- PA.readPrimArray drbg 04 + !(GHC.Word.W64# k04) <- PA.readPrimArray drbg 05 + !(GHC.Word.W64# k05) <- PA.readPrimArray drbg 06 + !(GHC.Word.W64# k06) <- PA.readPrimArray drbg 07 + !(GHC.Word.W64# k07) <- PA.readPrimArray drbg 08 + !(GHC.Word.W64# v00) <- PA.readPrimArray drbg 09 + !(GHC.Word.W64# v01) <- PA.readPrimArray drbg 10 + !(GHC.Word.W64# v02) <- PA.readPrimArray drbg 11 + !(GHC.Word.W64# v03) <- PA.readPrimArray drbg 12 + !(GHC.Word.W64# v04) <- PA.readPrimArray drbg 13 + !(GHC.Word.W64# v05) <- PA.readPrimArray drbg 14 + !(GHC.Word.W64# v06) <- PA.readPrimArray drbg 15 + !(GHC.Word.W64# v07) <- PA.readPrimArray drbg 16 + let !k0 = Registers (# k00, k01, k02, k03, k04, k05, k06, k07 #) + !v0 = Registers (# v00, v01, v02, v03, v04, v05, v06, v07 #) + !res <- gen_loop drbg k0 v0 bytes + update drbg addl + write_counter drbg (ctr + 1) + pure $! Right res +{-# INLINABLE gen #-} + +-- | Wipe the state of a DRBG. +-- +-- You should call this when you're finished with a DRBG to ensure that its +-- state is wiped from memory. +-- +-- >>> drbg <- new mempty mempty mempty +-- >>> Right bytes <- gen drbg addl_bytes 16 +-- >>> wipe drbg +-- >>> -- do something with bytes +wipe + :: PrimMonad m + => DRBG (PrimState m) + -> m () +wipe (DRBG drbg) = do + init_counter drbg + PA.setPrimArray drbg 01 08 (0x0000000000000000 :: Word64) -- init k + PA.setPrimArray drbg 09 08 (0x0101010101010101 :: Word64) -- init v + PA.setPrimArray drbg 17 16 (0x0000000000000000 :: Word64) -- init scratch +{-# INLINE wipe #-} + +-- drbg utilities ------------------------------------------------------------- + +gen_loop + :: PrimMonad m + => PA.MutablePrimArray (PrimState m) Word64 + -> Registers + -> Registers + -> Word64 + -> m BS.ByteString +gen_loop drbg k0 v0 bytes = loop mempty v0 0 where + !vp = PA.mutablePrimArrayContents drbg `FP.plusPtr` 72 -- 9 * 8 + !sp = PA.mutablePrimArrayContents drbg `FP.plusPtr` 136 -- 17 * 8 + loop !acc v l + | l >= bytes = do + write_v drbg v + pure acc + | otherwise = do + Prim.unsafeIOToPrim $ SHA512._hmac_rr vp sp k0 v + !(GHC.Word.W64# nv0) <- PA.readPrimArray drbg 09 + !(GHC.Word.W64# nv1) <- PA.readPrimArray drbg 10 + !(GHC.Word.W64# nv2) <- PA.readPrimArray drbg 11 + !(GHC.Word.W64# nv3) <- PA.readPrimArray drbg 12 + !(GHC.Word.W64# nv4) <- PA.readPrimArray drbg 13 + !(GHC.Word.W64# nv5) <- PA.readPrimArray drbg 14 + !(GHC.Word.W64# nv6) <- PA.readPrimArray drbg 15 + !(GHC.Word.W64# nv7) <- PA.readPrimArray drbg 16 + let !nv = Registers (# nv0, nv1, nv2, nv3, nv4, nv5, nv6, nv7 #) + !na = acc <> SHA512.cat nv + !nl = l + 64 + loop na nv nl +{-# INLINE gen_loop #-} + +update + :: PrimMonad m + => PA.MutablePrimArray (PrimState m) Word64 + -> BS.ByteString + -> m () +update drbg provided_data@(BI.PS _ _ l) = do + !(GHC.Word.W64# k00) <- PA.readPrimArray drbg 01 + !(GHC.Word.W64# k01) <- PA.readPrimArray drbg 02 + !(GHC.Word.W64# k02) <- PA.readPrimArray drbg 03 + !(GHC.Word.W64# k03) <- PA.readPrimArray drbg 04 + !(GHC.Word.W64# k04) <- PA.readPrimArray drbg 05 + !(GHC.Word.W64# k05) <- PA.readPrimArray drbg 06 + !(GHC.Word.W64# k06) <- PA.readPrimArray drbg 07 + !(GHC.Word.W64# k07) <- PA.readPrimArray drbg 08 + !(GHC.Word.W64# v00) <- PA.readPrimArray drbg 09 + !(GHC.Word.W64# v01) <- PA.readPrimArray drbg 10 + !(GHC.Word.W64# v02) <- PA.readPrimArray drbg 11 + !(GHC.Word.W64# v03) <- PA.readPrimArray drbg 12 + !(GHC.Word.W64# v04) <- PA.readPrimArray drbg 13 + !(GHC.Word.W64# v05) <- PA.readPrimArray drbg 14 + !(GHC.Word.W64# v06) <- PA.readPrimArray drbg 15 + !(GHC.Word.W64# v07) <- PA.readPrimArray drbg 16 + let !k0 = Registers (# k00, k01, k02, k03, k04, k05, k06, k07 #) + !v0 = Registers (# v00, v01, v02, v03, v04, v05, v06, v07 #) + !kp = PA.mutablePrimArrayContents drbg `FP.plusPtr` 08 -- 1 * 8 + !vp = PA.mutablePrimArrayContents drbg `FP.plusPtr` 72 -- 9 * 8 + !sp = PA.mutablePrimArrayContents drbg `FP.plusPtr` 136 -- 17 * 8 + Prim.unsafeIOToPrim $ SHA512._hmac_rsb kp sp k0 v0 0x00 provided_data + !(GHC.Word.W64# k10) <- PA.readPrimArray drbg 01 + !(GHC.Word.W64# k11) <- PA.readPrimArray drbg 02 + !(GHC.Word.W64# k12) <- PA.readPrimArray drbg 03 + !(GHC.Word.W64# k13) <- PA.readPrimArray drbg 04 + !(GHC.Word.W64# k14) <- PA.readPrimArray drbg 05 + !(GHC.Word.W64# k15) <- PA.readPrimArray drbg 06 + !(GHC.Word.W64# k16) <- PA.readPrimArray drbg 07 + !(GHC.Word.W64# k17) <- PA.readPrimArray drbg 08 + let !k1 = Registers (# k10, k11, k12, k13, k14, k15, k16, k17 #) + Prim.unsafeIOToPrim $ SHA512._hmac_rr vp sp k1 v0 + if l == 0 + then pure () + else do + !(GHC.Word.W64# v10) <- PA.readPrimArray drbg 09 + !(GHC.Word.W64# v11) <- PA.readPrimArray drbg 10 + !(GHC.Word.W64# v12) <- PA.readPrimArray drbg 11 + !(GHC.Word.W64# v13) <- PA.readPrimArray drbg 12 + !(GHC.Word.W64# v14) <- PA.readPrimArray drbg 13 + !(GHC.Word.W64# v15) <- PA.readPrimArray drbg 14 + !(GHC.Word.W64# v16) <- PA.readPrimArray drbg 15 + !(GHC.Word.W64# v17) <- PA.readPrimArray drbg 16 + let !v1 = Registers (# v10, v11, v12, v13, v14, v15, v16, v17 #) + Prim.unsafeIOToPrim $ SHA512._hmac_rsb kp sp k1 v1 0x01 provided_data + !(GHC.Word.W64# k20) <- PA.readPrimArray drbg 01 + !(GHC.Word.W64# k21) <- PA.readPrimArray drbg 02 + !(GHC.Word.W64# k22) <- PA.readPrimArray drbg 03 + !(GHC.Word.W64# k23) <- PA.readPrimArray drbg 04 + !(GHC.Word.W64# k24) <- PA.readPrimArray drbg 05 + !(GHC.Word.W64# k25) <- PA.readPrimArray drbg 06 + !(GHC.Word.W64# k26) <- PA.readPrimArray drbg 07 + !(GHC.Word.W64# k27) <- PA.readPrimArray drbg 08 + let !k2 = Registers (# k20, k21, k22, k23, k24, k25, k26, k27 #) + Prim.unsafeIOToPrim $ SHA512._hmac_rr vp sp k2 v1 +{-# INLINABLE update #-} + +init_counter + :: PrimMonad m + => PA.MutablePrimArray (PrimState m) Word64 + -> m () +init_counter drbg = + PA.writePrimArray drbg 0 (0x01 :: Word64) +{-# INLINE init_counter #-} + +read_counter + :: PrimMonad m + => PA.MutablePrimArray (PrimState m) Word64 + -> m Word64 +read_counter drbg = PA.readPrimArray drbg 0 +{-# INLINE read_counter #-} + +write_counter + :: PrimMonad m + => PA.MutablePrimArray (PrimState m) Word64 + -> Word64 + -> m () +write_counter drbg = PA.writePrimArray drbg 0 +{-# INLINE write_counter #-} + +write_v + :: PrimMonad m + => PA.MutablePrimArray (PrimState m) Word64 + -> Registers + -> m () +write_v drbg (R v0 v1 v2 v3 v4 v5 v6 v7) = do + PA.writePrimArray drbg 09 (GHC.Word.W64# v0) + PA.writePrimArray drbg 10 (GHC.Word.W64# v1) + PA.writePrimArray drbg 11 (GHC.Word.W64# v2) + PA.writePrimArray drbg 12 (GHC.Word.W64# v3) + PA.writePrimArray drbg 13 (GHC.Word.W64# v4) + PA.writePrimArray drbg 14 (GHC.Word.W64# v5) + PA.writePrimArray drbg 15 (GHC.Word.W64# v6) + PA.writePrimArray drbg 16 (GHC.Word.W64# v7) +{-# INLINE write_v #-} + +-- read secret drbg state (for testing) +_read_v + :: PrimMonad m + => DRBG (PrimState m) + -> m BS.ByteString +_read_v (DRBG drbg) = do + !v00 <- PA.readPrimArray drbg 09 + !v01 <- PA.readPrimArray drbg 10 + !v02 <- PA.readPrimArray drbg 11 + !v03 <- PA.readPrimArray drbg 12 + !v04 <- PA.readPrimArray drbg 13 + !v05 <- PA.readPrimArray drbg 14 + !v06 <- PA.readPrimArray drbg 15 + !v07 <- PA.readPrimArray drbg 16 + pure . BS.toStrict . BSB.toLazyByteString $ mconcat [ + BSB.word64BE v00 + , BSB.word64BE v01 + , BSB.word64BE v02 + , BSB.word64BE v03 + , BSB.word64BE v04 + , BSB.word64BE v05 + , BSB.word64BE v06 + , BSB.word64BE v07 + ] + +-- read secret drbg state (for testing) +_read_k + :: PrimMonad m + => DRBG (PrimState m) + -> m BS.ByteString +_read_k (DRBG drbg) = do + !k00 <- PA.readPrimArray drbg 01 + !k01 <- PA.readPrimArray drbg 02 + !k02 <- PA.readPrimArray drbg 03 + !k03 <- PA.readPrimArray drbg 04 + !k04 <- PA.readPrimArray drbg 05 + !k05 <- PA.readPrimArray drbg 06 + !k06 <- PA.readPrimArray drbg 07 + !k07 <- PA.readPrimArray drbg 08 + pure . BS.toStrict . BSB.toLazyByteString $ mconcat [ + BSB.word64BE k00 + , BSB.word64BE k01 + , BSB.word64BE k02 + , BSB.word64BE k03 + , BSB.word64BE k04 + , BSB.word64BE k05 + , BSB.word64BE k06 + , BSB.word64BE k07 + ] diff --git a/ppad-hmac-drbg.cabal b/ppad-hmac-drbg.cabal @@ -1,6 +1,6 @@ cabal-version: 3.0 name: ppad-hmac-drbg -version: 0.2.1 +version: 0.3.0 synopsis: HMAC-based deterministic random bit generator license: MIT license-file: LICENSE @@ -31,8 +31,8 @@ library if flag(llvm) ghc-options: -fllvm -O2 exposed-modules: - Crypto.DRBG.HMAC - , Crypto.DRBG.HMAC.SHA256 + Crypto.DRBG.HMAC.SHA256 + , Crypto.DRBG.HMAC.SHA512 build-depends: base >= 4.9 && < 5 , bytestring >= 0.9 && < 0.13 diff --git a/test/Main.hs b/test/Main.hs @@ -5,9 +5,11 @@ module Main where import Control.Applicative ((<|>)) -import qualified Crypto.DRBG.HMAC.SHA256 as DRBG +import qualified Crypto.DRBG.HMAC.SHA256 as DRBG256 +import qualified Crypto.DRBG.HMAC.SHA512 as DRBG512 import qualified Data.Attoparsec.ByteString.Char8 as A import qualified Data.ByteString as BS +import Data.Word (Word64) import qualified Data.ByteString.Char8 as B8 import qualified Data.ByteString.Base16 as B16 import Test.Tasty @@ -24,21 +26,21 @@ import Test.Tasty.HUnit main :: IO () main = do sha256_vectors <- BS.readFile "etc/HMAC_DRBG_SHA256.txt" - -- sha512_vectors <- BS.readFile "etc/HMAC_DRBG_SHA512.txt" + sha512_vectors <- BS.readFile "etc/HMAC_DRBG_SHA512.txt" let sha256_cases = case A.parseOnly parse_sha256_blocks sha256_vectors of Left _ -> error "ppad-hmac-drbg (test): parse error" Right cs -> cs - -- sha512_cases = case A.parseOnly parse_sha512_blocks sha512_vectors of - -- Left _ -> error "ppad-hmac-drbg (test): parse error" - -- Right cs -> cs + sha512_cases = case A.parseOnly parse_sha512_blocks sha512_vectors of + Left _ -> error "ppad-hmac-drbg (test): parse error" + Right cs -> cs - defaultMain (cavp_14_3 sha256_cases) + defaultMain (cavp_14_3 sha256_cases sha512_cases) -cavp_14_3 :: [CaseBlock] -> TestTree -cavp_14_3 cs = testGroup "CAVP 14.3" [ - testGroup "HMAC-SHA256" (fmap execute_caseblock cs) - -- , testGroup "HMAC-SHA512" (fmap execute_caseblock ds) +cavp_14_3 :: [CaseBlock] -> [CaseBlock] -> TestTree +cavp_14_3 cs ds = testGroup "CAVP 14.3" [ + testGroup "HMAC-SHA256" (fmap (execute_caseblock DRBG256.new DRBG256.reseed DRBG256.gen DRBG256._read_v DRBG256._read_k) cs) + , testGroup "HMAC-SHA512" (fmap (execute_caseblock DRBG512.new DRBG512.reseed DRBG512.gen DRBG512._read_v DRBG512._read_k) ds) ] data CaseBlock = CaseBlock { @@ -76,9 +78,16 @@ data Case = Case { , caseReturned :: !BS.ByteString } deriving Show -execute_caseblock :: CaseBlock -> TestTree -execute_caseblock CaseBlock {..} = - testGroup msg (fmap execute cb_cases) +execute_caseblock + :: (BS.ByteString -> BS.ByteString -> BS.ByteString -> IO drbg) + -> (drbg -> BS.ByteString -> BS.ByteString -> IO ()) + -> (drbg -> BS.ByteString -> Word64 -> IO (Either e BS.ByteString)) + -> (drbg -> IO BS.ByteString) + -> (drbg -> IO BS.ByteString) + -> CaseBlock + -> TestTree +execute_caseblock drbg_new drbg_reseed drbg_gen read_v read_k CaseBlock {..} = + testGroup msg (fmap (execute drbg_new drbg_reseed drbg_gen read_v read_k) cb_cases) where BlockHeader {..} = cb_blockHeader msg = "bitlens: " <> @@ -89,29 +98,37 @@ execute_caseblock CaseBlock {..} = "ret " <> show bh_ReturnedBitsLen -- execute test case -execute :: Case -> TestTree -execute Case {..} = testCase ("count " <> show caseCount) $ do +execute + :: (BS.ByteString -> BS.ByteString -> BS.ByteString -> IO drbg) + -> (drbg -> BS.ByteString -> BS.ByteString -> IO ()) + -> (drbg -> BS.ByteString -> Word64 -> IO (Either e BS.ByteString)) + -> (drbg -> IO BS.ByteString) + -> (drbg -> IO BS.ByteString) + -> Case + -> TestTree +execute drbg_new drbg_reseed drbg_gen read_v read_k Case {..} = + testCase ("count " <> show caseCount) $ do let bytes = fromIntegral (BS.length caseReturned) - drbg <- DRBG.new caseEntropy0 caseNonce casePs - v0 <- DRBG._read_v drbg - k0 <- DRBG._read_k drbg + drbg <- drbg_new caseEntropy0 caseNonce casePs + v0 <- read_v drbg + k0 <- read_k drbg assertEqual "v0" v0 caseV0 assertEqual "k0" k0 caseK0 - DRBG.reseed drbg caseEntropy1 caseAddl1 - Right _ <- DRBG.gen drbg mempty bytes - v1 <- DRBG._read_v drbg - k1 <- DRBG._read_k drbg + drbg_reseed drbg caseEntropy1 caseAddl1 + Right _ <- drbg_gen drbg mempty bytes + v1 <- read_v drbg + k1 <- read_k drbg assertEqual "v1" v1 caseV1 assertEqual "k1" k1 caseK1 - DRBG.reseed drbg caseEntropy2 caseAddl2 - Right returned <- DRBG.gen drbg mempty bytes - v2 <- DRBG._read_v drbg - k2 <- DRBG._read_k drbg + drbg_reseed drbg caseEntropy2 caseAddl2 + Right returned <- drbg_gen drbg mempty bytes + v2 <- read_v drbg + k2 <- read_k drbg assertEqual "returned_bytes" returned caseReturned assertEqual "v2" v2 caseV2 @@ -198,13 +215,13 @@ parse_sha256_block = do parse_sha256_blocks :: A.Parser [CaseBlock] parse_sha256_blocks = A.many1 parse_sha256_block --- parse_sha512_block :: A.Parser CaseBlock --- parse_sha512_block = do --- cb_blockHeader <- parse_header "SHA-512" --- cb_cases <- parse_cases --- A.endOfLine --- pure CaseBlock {..} +parse_sha512_block :: A.Parser CaseBlock +parse_sha512_block = do + cb_blockHeader <- parse_header "SHA-512" + cb_cases <- parse_cases + A.endOfLine + pure CaseBlock {..} --- parse_sha512_blocks :: A.Parser [CaseBlock] --- parse_sha512_blocks = A.many1 parse_sha512_block +parse_sha512_blocks :: A.Parser [CaseBlock] +parse_sha512_blocks = A.many1 parse_sha512_block