aead

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

commit 00acf5b0ee9dd34fd7029deab376e28e3b9ca93f
parent 6d4e867bcb0d4182b147f03e3985f8b1e815c686
Author: Jared Tobin <jared@jtobin.io>
Date:   Fri,  3 Jul 2026 16:34:40 -0230

lib: fuse the constant-time tag comparison

Fold the bytewise XORs of the two buffers into the accumulator
directly, rather than materialising their XOR as an intermediate
ByteString via packZipWith: no per-comparison allocation, and no
secret-derived difference bytes ever hit the heap. Semantics and
the data-independent full walk are unchanged.

The implementation was checked with ppad-censor against
ppad-sha256's identical instance: mismatch-position independence
(byte 0 vs byte 31) passes at full budget under wall clock, with
the mean position effect bounded within ~1.4 ns per call.

Diffstat:
MCHANGELOG | 6++++++
Mlib/Crypto/AEAD/ChaCha20Poly1305.hs | 21+++++++++++++++++----
2 files changed, 23 insertions(+), 4 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG @@ -1,5 +1,11 @@ # Changelog +- Unreleased + * The internal constant-time tag comparison now folds over the + two buffers directly, rather than materialising an intermediate + ByteString holding their XOR on the heap. Semantics are + unchanged. + - 0.3.2 (2026-05-16) * Bumps the poly1305 dependency to a version that returns its MAC in a custom type having a constant-time Eq instance. diff --git a/lib/Crypto/AEAD/ChaCha20Poly1305.hs b/lib/Crypto/AEAD/ChaCha20Poly1305.hs @@ -31,17 +31,30 @@ import Data.Bits ((.>>.)) import qualified Data.Bits as B import qualified Data.ByteString as BS import qualified Data.ByteString.Internal as BI -import Data.Word (Word64) +import qualified Data.ByteString.Unsafe as BU +import Data.Word (Word8, Word64) fi :: (Integral a, Num b) => a -> b fi = fromIntegral {-# INLINE fi #-} --- constant-time equality comparison on bytestrings +-- constant-time equality comparison on bytestrings. 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. ct_eq :: BS.ByteString -> BS.ByteString -> Bool ct_eq a@(BI.PS _ _ la) b@(BI.PS _ _ lb) - | la /= lb = False - | otherwise = BS.foldl' (B..|.) 0 (BS.packZipWith B.xor a b) == 0 + | la /= lb = False + | otherwise = go 0 0 + where + go :: Word8 -> Int -> Bool + go !acc !i + | i == la = acc == 0 + | otherwise = + let !x = BU.unsafeIndex a i + !y = BU.unsafeIndex b i + in go (acc B..|. B.xor x y) (i + 1) {-# INLINE ct_eq #-} -- little-endian bytestring encoding