commit 2b580fad35e20f77cc8c8ec4780b3679c55664d4
parent d7cfef78410784591efe93f0e3322278029c41ed
Author: Jared Tobin <jared@jtobin.io>
Date: Fri, 17 Jul 2026 17:58:10 -0230
lib: add barrier module
Diffstat:
3 files changed, 41 insertions(+), 2 deletions(-)
diff --git a/lib/Crypto/MAC/Poly1305.hs b/lib/Crypto/MAC/Poly1305.hs
@@ -27,6 +27,7 @@ module Crypto.MAC.Poly1305 (
) where
import qualified Crypto.MAC.Poly1305.Arm as Arm
+import Data.Barrier (barrier)
import qualified Data.Bits as B
import qualified Data.ByteString as BS
import qualified Data.ByteString.Internal as BI
@@ -160,10 +161,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,33 @@
+{-# OPTIONS_HADDOCK hide #-}
+
+-- |
+-- Module: Data.Barrier
+-- Copyright: (c) 2025 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-poly1305.cabal b/ppad-poly1305.cabal
@@ -38,6 +38,8 @@ library
exposed-modules:
Crypto.MAC.Poly1305
Crypto.MAC.Poly1305.Arm
+ other-modules:
+ Data.Barrier
build-depends:
base >= 4.9 && < 5
, bytestring >= 0.9 && < 0.13