aead

Pure Haskell AEAD-ChaCha20-Poly1305 (docs.ppad.tech/aead).
git clone git://git.ppad.tech/aead.git
Log | Files | Refs | README | LICENSE

commit 29c12eb38dc6b44be45fd20663e314a187d99793
parent 9316ad643808e894163ec8cce926c6c2547872ed
Author: Jared Tobin <jared@jtobin.io>
Date:   Fri, 17 Jul 2026 15:26:58 -0230

lib: barrier ct_eq accumulator against LLVM short-circuit

The LLVM backend recognises ct_eq's OR-accumulate-then-compare-zero
as an array-equality test and lowers it to a short-circuiting
byte-by-byte compare that exits on the first differing tag byte,
re-introducing the mismatch-position timing leak the constant-time
comparison was written to avoid (research/timing ATTACK163). NCG is
unaffected.

Route the accumulator through Data.Barrier.barrier -- a NOINLINE
identity in a separate module, so it compiles to an opaque external
call the LLVM optimiser cannot see through -- before the zero-test.
This forces the full OR-fold over every byte; verified with substratum
(the early-exit branches are gone, the OR-accumulate is back) on both
aarch64 and x86_64 LLVM builds. All tests pass.

Diffstat:
M.gitignore | 1+
Mlib/Crypto/AEAD/ChaCha20Poly1305.hs | 7+++++--
Alib/Data/Barrier.hs | 34++++++++++++++++++++++++++++++++++
Mppad-aead.cabal | 2++
4 files changed, 42 insertions(+), 2 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -1,2 +1,3 @@ dist-newstyle/ result-doc +*.s diff --git a/lib/Crypto/AEAD/ChaCha20Poly1305.hs b/lib/Crypto/AEAD/ChaCha20Poly1305.hs @@ -25,6 +25,7 @@ module Crypto.AEAD.ChaCha20Poly1305 ( , _poly1305_key_gen ) where +import Data.Barrier (barrier) import qualified Crypto.Cipher.ChaCha20 as ChaCha20 import qualified Crypto.MAC.Poly1305 as Poly1305 import Data.Bits ((.>>.)) @@ -42,7 +43,9 @@ fi = fromIntegral -- 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. +-- 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"). ct_eq :: BS.ByteString -> BS.ByteString -> Bool ct_eq a@(BI.PS _ _ la) b@(BI.PS _ _ lb) | la /= lb = False @@ -50,7 +53,7 @@ ct_eq a@(BI.PS _ _ la) b@(BI.PS _ _ lb) where 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 @@ +-- | +-- 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. +-- +-- Guarded by the substratum + censor constant-time checks; weakening +-- either property re-introduces the timing leak. +barrier :: Word8 -> Word8 +barrier x = x +{-# NOINLINE barrier #-} diff --git a/ppad-aead.cabal b/ppad-aead.cabal @@ -33,6 +33,8 @@ library ghc-options: -fllvm -O2 exposed-modules: Crypto.AEAD.ChaCha20Poly1305 + other-modules: + Data.Barrier build-depends: base >= 4.9 && < 5 , bytestring >= 0.9 && < 0.13