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 bfeb72a390277c66291f45de4b7ffe57e0643f04
parent db650924ddd0401828c9a28b454d630289377869
Author: Jared Tobin <jared@jtobin.io>
Date:   Fri, 24 Jul 2026 10:03:48 -0230

lib: add barrier module

The use of 'barrier' prevents LLVM from optimizing away what are
otherwise constant-time MAC comparisons.

Diffstat:
Mlib/Crypto/Hash/SHA256/Internal.hs | 8++++++--
Alib/Data/Barrier.hs | 34++++++++++++++++++++++++++++++++++
Mppad-sha256.cabal | 2++
3 files changed, 42 insertions(+), 2 deletions(-)

diff --git a/lib/Crypto/Hash/SHA256/Internal.hs b/lib/Crypto/Hash/SHA256/Internal.hs @@ -50,6 +50,7 @@ module Crypto.Hash.SHA256.Internal ( , poke_registers ) where +import Data.Barrier (barrier) import qualified Data.Bits as B import qualified Data.ByteString as BS import qualified Data.ByteString.Internal as BI @@ -89,10 +90,13 @@ instance Eq MAC where -- fused fold: OR the bytewise XORs into an accumulator -- directly, rather than via packZipWith, so no intermediate -- ByteString holding the (secret-derived) difference bytes - -- is ever materialised on the heap. + -- is ever materialised on the heap. The accumulator is routed + -- through 'barrier' before the zero-test so the LLVM backend + -- cannot recover the array-equality idiom and short-circuit on + -- the first mismatch (see "Data.Barrier"). go :: Word8 -> Int -> Bool go !acc !i - | i == la = acc == 0 + | i == la = barrier acc == 0 | otherwise = let !x = BU.unsafeIndex a i !y = BU.unsafeIndex b i diff --git a/lib/Data/Barrier.hs b/lib/Data/Barrier.hs @@ -0,0 +1,34 @@ +{-# OPTIONS_HADDOCK hide #-} + +-- | +-- Module: Data.Barrier +-- Copyright: (c) 2026 Jared Tobin +-- License: MIT +-- Maintainer: Jared Tobin <jared@ppad.tech> +-- +-- An optimisation barrier for constant-time code. + +module Data.Barrier ( + barrier + ) where + +import Data.Word (Word8) + +-- | Identity on 'Word8', but opaque to the optimiser. A constant-time +-- accumulate-then-compare (e.g. an OR-fold of bytewise XORs, tested +-- against zero) routes its accumulator through this before the +-- zero-test, so the compiler cannot recognise it as an array-equality +-- test and lower it to a short-circuiting byte comparison (which would +-- leak the mismatch position). +-- +-- Both properties are required and must not be \"tidied\" away: +-- +-- * @NOINLINE@ -- if GHC inlines it, the LLVM backend regains the +-- accumulator's definition and short-circuits again. +-- * a /separate/ module -- a caller then compiles @barrier@ to an +-- external call it cannot see through. Inline it into the caller +-- and the barrier is gone. +-- +barrier :: Word8 -> Word8 +barrier x = x +{-# NOINLINE barrier #-} diff --git a/ppad-sha256.cabal b/ppad-sha256.cabal @@ -40,6 +40,8 @@ library Crypto.Hash.SHA256.Arm Crypto.Hash.SHA256.Internal Crypto.Hash.SHA256.Lazy + other-modules: + Data.Barrier build-depends: base >= 4.9 && < 5 , bytestring >= 0.9 && < 0.13