hmac-drbg

Pure Haskell HMAC-DRBG CSPRNG per NIST-SP 800-90A.
git clone git://git.ppad.tech/hmac-drbg.git
Log | Files | Refs | LICENSE

commit d63ae899fe36f64586ea7d35884558a99653736b
parent f3f2ea5d9e1058547bdaac505420ab976f542039
Author: Jared Tobin <jared@jtobin.io>
Date:   Mon, 30 Sep 2024 16:16:13 +0400

lib: misc heavy dev

Diffstat:
A.ghci | 2++
Mlib/Crypto/DRBG/HMAC.hs | 128++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Mppad-hmac-drbg.cabal | 2+-
Mtest/Main.hs | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 138 insertions(+), 57 deletions(-)

diff --git a/.ghci b/.ghci @@ -0,0 +1,2 @@ +:set prompt "> " +:set -XOverloadedStrings diff --git a/lib/Crypto/DRBG/HMAC.hs b/lib/Crypto/DRBG/HMAC.hs @@ -2,108 +2,124 @@ {-# LANGUAGE BangPatterns #-} {-# LANGUAGE LambdaCase #-} -module Crypto.DRBG.HMAC where +module Crypto.DRBG.HMAC ( + DRBG + , read_v + , read_key + + , new + , gen + , reseed + ) where -import qualified Crypto.Hash.SHA256 as SHA256 import qualified Data.ByteString as BS import qualified Data.ByteString.Builder as BSB import Data.Word (Word64) --- keystroke saver +-- keystroke savers and utilities --------------------------------------------- + fi :: (Integral a, Num b) => a -> b fi = fromIntegral {-# INLINE fi #-} -_RESEED_INTERVAL :: Word64 -_RESEED_INTERVAL = (2 :: Word64) ^ (48 :: Word64) +toStrict :: BSB.Builder -> BS.ByteString +toStrict = BS.toStrict . BSB.toLazyByteString +{-# INLINE toStrict #-} + +-- dumb strict pair +data Pair a b = Pair !a !b + deriving Show + +-- types ---------------------------------------------------------------------- +-- HMAC function and its associated outlength data HMAC = HMAC - !(BS.ByteString -> BS.ByteString -> BS.ByteString) + !(BS.ByteString -> BS.ByteString -> BS.ByteString) {-# UNPACK #-} !Word64 +-- DRBG environment data and state data DRBG = DRBG - !HMAC -- hmac function & outlen - {-# UNPACK #-} !BS.ByteString -- v - {-# UNPACK #-} !BS.ByteString -- key - {-# UNPACK #-} !Word64 -- reseed_counter + !HMAC -- hmac function & outlen + {-# UNPACK #-} !BS.ByteString -- v + {-# UNPACK #-} !BS.ByteString -- key -instance Show DRBG where - show (DRBG _ v k r) = "DRBG " <> show v <> " " <> show k <> " " <> show r +-- | Read the 'V' value from the DRBG state. +read_v :: DRBG -> BS.ByteString +read_v (DRBG _ v _) = v --- dumb strict pair -data Pair a b = Pair !a !b - deriving Show +-- | Read the 'Key' value from the DRBG state. +read_key :: DRBG -> BS.ByteString +read_key (DRBG _ _ key) = key + +-- drbg interaction ----------------------------------------------------------- update :: BS.ByteString -> DRBG -> DRBG -update provided_data (DRBG h@(HMAC hmac _) v0 k0 r) = - let !k1 = hmac k0 (suf 0x00 v0) +update provided_data (DRBG h@(HMAC hmac _) v0 k0) = + let !k1 = hmac k0 (cat v0 0x00 provided_data) !v1 = hmac k1 v0 in if BS.null provided_data - then (DRBG h v1 k1 r) - else let !k2 = hmac k1 (suf 0x01 v1) + then (DRBG h v1 k1) + else let !k2 = hmac k1 (cat v1 0x01 provided_data) !v2 = hmac k2 v1 - in DRBG h v2 k2 r + in DRBG h v2 k2 where - suf byte bs = BS.toStrict - . BSB.toLazyByteString - $ BSB.byteString bs <> BSB.word8 byte <> BSB.byteString provided_data - -instantiate - :: (BS.ByteString -> BS.ByteString -> BS.ByteString) - -> BS.ByteString - -> BS.ByteString - -> BS.ByteString + cat bs byte suf = toStrict $ + BSB.byteString bs <> BSB.word8 byte <> BSB.byteString suf + +-- | Create a DRBG from the provided HMAC function, entropy, nonce, and +-- personalization string. +new + :: (BS.ByteString -> BS.ByteString -> BS.ByteString) -- HMAC function + -> BS.ByteString -- entropy + -> BS.ByteString -- nonce + -> BS.ByteString -- personalization string -> DRBG -instantiate hmac entropy nonce ps = - let drbg = DRBG (HMAC hmac outlen) v0 k0 1 +new hmac entropy nonce ps = + let !drbg = DRBG (HMAC hmac outlen) v0 k0 in update seed_material drbg where seed_material = entropy <> nonce <> ps - outlen = fi (BS.length (hmac mempty mempty)) -- UX hack, costs 1 hmac call + outlen = fi (BS.length (hmac mempty mempty)) k0 = BS.replicate (fi outlen) 0x00 v0 = BS.replicate (fi outlen) 0x01 -reseed :: DRBG -> BS.ByteString -> BS.ByteString -> DRBG -reseed drbg entropy addl = - let !(DRBG hmac v k _) = update seed_material drbg - in DRBG hmac v k 1 - where - seed_material = entropy <> addl +-- | Inject entropy and additional bytes into a DRBG. +-- +-- Note that we don't support "proper" reseeding (i.e., we don't track +-- a reseed counter), but this can be used for injecting entropy per +-- spec. +reseed :: BS.ByteString -> BS.ByteString -> DRBG -> DRBG +reseed entropy addl drbg = update (entropy <> addl) drbg -generate +gen :: BS.ByteString -> Word64 -> DRBG -> (BS.ByteString, DRBG) -generate addl bytes drbg0@(DRBG h@(HMAC hmac outlen) _ _ r) - | r > _RESEED_INTERVAL = error "ppad-hmac-drbg: DRBG reseed required" - | otherwise = - let !(Pair temp drbg1) = go mempty 0 v1 - - !returned_bits = BS.take (fi bytes) temp - !drbg2 = update addl drbg1 - - in (returned_bits, drbg2) +gen addl bytes drbg0@(DRBG h@(HMAC hmac outlen) _ _) = + let !(Pair temp drbg1) = loop mempty 0 v1 + !returned_bits = BS.take (fi bytes) temp + !drbg = update addl drbg1 + in (returned_bits, drbg) where - !(DRBG _ v1 k1 _) + !(DRBG _ v1 k1) | BS.null addl = drbg0 | otherwise = update addl drbg0 - go !acc !len !vl + loop !acc !len !vl | len < bytes = let nv = hmac k1 vl nacc = acc <> BSB.byteString nv nlen = len + outlen - in go nacc nlen nv + in loop nacc nlen nv - -- take opportunity to update reseed_counter here | otherwise = - let facc = BS.toStrict . BSB.toLazyByteString $ acc - in Pair facc (DRBG h vl k1 (succ r)) + let facc = toStrict acc + in Pair facc (DRBG h vl k1) + +-- XX maybe want some sort of primitive convenience here --- XX test against --- https://raw.githubusercontent.com/coruus/nist-testvectors/refs/heads/master/csrc.nist.gov/groups/STM/cavp/documents/drbg/drbgtestvectors/drbgvectors_pr_true/HMAC_DRBG.txt diff --git a/ppad-hmac-drbg.cabal b/ppad-hmac-drbg.cabal @@ -28,7 +28,6 @@ library build-depends: base >= 4.9 && < 5 , bytestring >= 0.9 && < 0.13 - , ppad-sha256 >= 0.1.0 && < 0.2.0 test-suite hmac-drbg-tests type: exitcode-stdio-1.0 @@ -44,6 +43,7 @@ test-suite hmac-drbg-tests , base16-bytestring , bytestring , ppad-hmac-drbg + , ppad-sha256 >= 0.1.0 && < 0.2.0 , tasty , tasty-hunit diff --git a/test/Main.hs b/test/Main.hs @@ -1,7 +1,70 @@ +{-# LANGUAGE OverloadedStrings #-} module Main where +import qualified Crypto.Hash.SHA256 as SHA256 import qualified Crypto.DRBG.HMAC as DRBG import qualified Data.ByteString as BS import qualified Data.ByteString.Base16 as B16 +import Data.Word (Word64) + +-- XX test against +-- https://raw.githubusercontent.com/coruus/nist-testvectors/refs/heads/master/csrc.nist.gov/groups/STM/cavp/documents/drbg/drbgtestvectors/drbgvectors_pr_true/HMAC_DRBG.txt + +fun0 = SHA256.hmac +add0 :: BS.ByteString +add0 = mempty +byts :: Word64 +byts = 128 + +ent0 :: BS.ByteString +ent0 = "9969e54b4703ff31785b879a7e5c0eae0d3e309559e9fe96b0676d49d591ea4d" +non0 :: BS.ByteString +non0 = "07d20d46d064757d3023cac2376127ab" +per0 :: BS.ByteString +per0 = mempty +ent1 :: BS.ByteString +ent1 = "c60f2999100f738c10f74792676a3fc4a262d13721798046e29a295181569f54" +ent2 :: BS.ByteString +ent2 = "c11d4524c9071bd3096015fcf7bc24a607f22fa065c937658a2a77a8699089f4" + +test func addl bytes i_ent i_non i_per g_ent0 g_ent1 = do + let d_ent = B16.decodeLenient i_ent + d_non = B16.decodeLenient i_non + d_per = B16.decodeLenient i_per + drbg0 = DRBG.new func d_ent d_non d_per + v0 = DRBG.read_v drbg0 + k0 = DRBG.read_key drbg0 + + putStrLn $ "upon instantiation:" + print $ " v: " <> B16.encode v0 + print $ " k: " <> B16.encode k0 + + let d_ent0 = B16.decodeLenient g_ent0 + drbg1 = DRBG.reseed mempty d_ent0 drbg0 + (_, drbg2) = DRBG.gen addl bytes drbg1 + v1 = DRBG.read_v drbg2 + k1 = DRBG.read_key drbg2 + + putStrLn $ "after first gen:" + print $ " v: " <> B16.encode v1 + print $ " k: " <> B16.encode k1 + + let d_ent1 = B16.decodeLenient g_ent1 + drbg3 = DRBG.reseed mempty d_ent1 drbg2 + (res, drbg4) = DRBG.gen addl bytes drbg3 + v2 = DRBG.read_v drbg4 + k2 = DRBG.read_key drbg4 + + putStrLn $ "after second gen:" + print $ " v: " <> B16.encode v2 + print $ " k: " <> B16.encode k2 + + putStrLn mempty + + putStrLn $ "returned bytes:" + print $ " " <> B16.encode res + +main :: IO () +main = test fun0 add0 byts ent0 non0 per0 ent1 ent2