packages feed

mlkem 0.2.2.0 → 0.2.3.0

raw patch · 12 files changed

+137/−48 lines, 12 filesdep +filelockPVP ok

version bump matches the API change (PVP)

Dependencies added: filelock

API changes (from Hackage documentation)

Files

CHANGELOG.md view
@@ -1,5 +1,13 @@ # Changelog for `mlkem` +## 0.2.3.0 - 2026-08-16++* Improve performance of modular reduction with the NCG++* Use `finally` instead of masking asynchronous exceptions++* Refresh the test vectors+ ## 0.2.2.0 - 2026-06-28  * Clearing buffers containing secrets is more resilient and takes into account
mlkem.cabal view
@@ -1,15 +1,15 @@ cabal-version: 2.2 --- This file has been generated from package.yaml by hpack version 0.39.1.+-- This file has been generated from package.yaml by hpack version 0.39.6. -- -- see: https://github.com/sol/hpack  name:           mlkem-version:        0.2.2.0+version:        0.2.3.0 synopsis:       Module-Lattice-based Key-Encapsulation Mechanism description:    Module-Lattice-based Key-Encapsulation Mechanism (ML-KEM) implemented in                 Haskell.-category:       Crypto+category:       Cryptography homepage:       https://codeberg.org/ocheron/hs-mlkem#readme bug-reports:    https://codeberg.org/ocheron/hs-mlkem/issues author:         Olivier Chéron@@ -44,6 +44,7 @@       Builder       ByteArrayST       Crypto+      Endian       Equality       Fusion       Internal@@ -97,6 +98,7 @@     , bytestring     , deepseq     , directory+    , filelock     , mlkem     , primitive >=0.7.2     , process@@ -127,6 +129,7 @@       ByteArrayST       Crypto       Crypto.PubKey.ML_KEM+      Endian       Equality       Fusion       Internal@@ -158,6 +161,7 @@     , bytestring     , deepseq     , directory+    , filelock     , primitive >=0.7.2     , process     , tasty
src/Auxiliary.hs view
@@ -38,7 +38,9 @@ import Control.Monad.ST  import Data.Bits+#ifdef __GLASGOW_HASKELL_LLVM__ import Data.Int+#endif import Data.Proxy import Data.Word @@ -91,17 +93,40 @@     (b `unsafeShiftR` 1 .&. 1) `unsafeShiftL` 5 .|.     (b .&. 1) `unsafeShiftL` 6 -unsafeShiftIR :: Word16 -> Int -> Word16-unsafeShiftIR x s = fromIntegral ((fromIntegral x :: Int16) `unsafeShiftR` s)-{-# INLINE unsafeShiftIR #-}- -- Reduction 𝑥 mod 𝑞 for 0 ≤ 𝑥 < 2𝑞 reduceSimple :: Word16 -> Word16-reduceSimple x = (mask .&. x) .|. (complement mask .&. subtracted)+#ifdef __GLASGOW_HASKELL_LLVM__+reduceSimple = reduceSimpleLLVM+#else+reduceSimple = fromIntegral . reduceSimpleNCG . fromIntegral+#endif+{-# INLINE reduceSimple #-}++#ifdef __GLASGOW_HASKELL_LLVM__+-- LLVM operates more efficiently on Word16 directly+reduceSimpleLLVM :: Word16 -> Word16+reduceSimpleLLVM x = (mask .&. x) .|. (complement mask .&. subtracted)   where     subtracted = x - q16     mask = subtracted `unsafeShiftIR` 15-{-# INLINE reduceSimple #-}++    unsafeShiftIR :: Word16 -> Int -> Word16+    unsafeShiftIR w s = fromIntegral ((fromIntegral w :: Int16) `unsafeShiftR` s)+#else+-- NCG code performance is better with full machine words, as this removes+-- conversion instructions at several intermediate steps+reduceSimpleNCG :: Word -> Word+reduceSimpleNCG x = (mask .&. x) .|. (complement mask .&. subtracted)+  where+    subtracted = x - qW+    mask = subtracted `unsafeShiftIR` (finiteBitSize qW - 1)++    unsafeShiftIR :: Word -> Int -> Word+    unsafeShiftIR w s = fromIntegral ((fromIntegral w :: Int) `unsafeShiftR` s)++    qW :: Word+    qW = fromInteger q+#endif  -- Reduction 𝑥 mod 𝑞 for 0 ≤ 𝑥 < 2𝑞² + 𝑞 reduce :: Word32 -> Word16
src/Block.hs view
@@ -13,7 +13,7 @@     ( Block, MutableBlock, blockIndex, blockRead, blockWrite     , foldZipWith, Block.length, mutableContents, getMutableLength     , Block.new, Block.newPinned, Block.thaw, thawPinned-    , Block.unsafeCast, unsafeCastMut, Block.unsafeFreeze, Block.unsafeThaw+    , Block.unsafeCast, unsafeCastMut, Block.unsafeFreeze #ifdef ML_KEM_TESTING     , Block.toList #endif@@ -109,6 +109,3 @@  unsafeFreeze :: PrimMonad prim => MutableBlock ty (PrimState prim) -> prim (Block ty) unsafeFreeze = unsafeFreezePrimArray--unsafeThaw :: PrimMonad prim => Block ty -> prim (MutableBlock ty (PrimState prim))-unsafeThaw = unsafeThawPrimArray
src/BlockN.hs view
@@ -77,7 +77,7 @@ index :: PrimType a => BlockN marking n a -> Offset a -> a index = SecureBlock.index . unBlockN -replicate :: forall marking n a. (Classified marking, KnownNat n, PrimType a) => a -> BlockN marking n a+replicate :: (Classified marking, KnownNat n, PrimType a) => a -> BlockN marking n a replicate = create . const  #ifdef ML_KEM_TESTING@@ -96,15 +96,16 @@ toList = SecureBlock.toList . unBlockN #endif -create :: forall marking n ty. (Classified marking, KnownNat n, PrimType ty)+create :: (Classified marking, KnownNat n, PrimType ty)        => (Offset ty -> ty)        -> BlockN marking n ty-create initializer = runNew (Proxy :: Proxy marking) $ iterSet initializer+create initializer = runNew Proxy $ iterSet initializer {-# INLINE create #-}  map :: (Classified marking, KnownNat n, PrimType a, PrimType b)     => (a -> b) -> BlockN marking n a -> BlockN marking n b-map f (BlockN !a) = create $ \(Offset i) -> f (SecureBlock.index a (Offset i))+map f (BlockN a) = BlockN.seq a $+    create $ \(Offset i) -> f (SecureBlock.index a (Offset i)) {-# INLINE [2] map #-}  mapEndo :: (Classified marking, KnownNat n, PrimType a)@@ -151,8 +152,8 @@  zipWith :: (Classified mc, KnownNat n, PrimType a, PrimType b, PrimType c)         => (a -> b -> c) -> BlockN ma n a -> BlockN mb n b -> BlockN mc n c-zipWith f (BlockN !a) (BlockN !b) =-    create $ \(Offset i) ->+zipWith f (BlockN a) (BlockN b) =+    BlockN.seq a $ BlockN.seq b $ create $ \(Offset i) ->         f (SecureBlock.index a (Offset i)) (SecureBlock.index b (Offset i)) {-# INLINE [2] zipWith #-} @@ -220,7 +221,7 @@ iterMapIxContext :: (EqPrimSize a b, Classified marking, KnownNat n) => (Int -> a -> b) -> Context (BlockN marking n a) -> Context (BlockN marking n b) iterMapIxContext f = mapContext m   where m = MapF { mapUpdate = unsafeMapIx f-                 , mapInit = \x -> newF >>= \mb -> Prelude.seq x (iterSet (g x) mb) >> return mb+                 , mapInit = \x -> newF >>= \mb -> mb <$ Prelude.seq x (iterSet (g x) mb)                  }         g x (Offset i) = f i (index x (Offset i)) {-# INLINE [1] iterMapIxContext #-}
src/Crypto.hs view
@@ -20,7 +20,7 @@ import Crypto.Hash.Algorithms import Crypto.Hash.IO -import Control.Exception (assert, mask_)+import Control.Exception (assert, finally) import Control.Monad import Control.Monad.ST @@ -95,17 +95,21 @@     constEqW a b         | Block.length a /= Block.length b = falseW         | otherwise = Block.foldZipWith (\mask x y -> mask `andW` eqW x y) trueW a b+    {-# NOINLINE constEqW #-}  instance ConstEqW (ScrubbedBlock Word) where     constEqW a b         | ScrubbedBlock.length a /= ScrubbedBlock.length b = falseW         | otherwise = ScrubbedBlock.foldZipWith (\mask x y -> mask `andW` eqW x y) trueW a b+    {-# NOINLINE constEqW #-}  instance ConstEqW Bytes where     constEqW = bytesConstEqW+    {-# NOINLINE constEqW #-}  instance ConstEqW ScrubbedBytes where     constEqW = bytesConstEqW+    {-# NOINLINE constEqW #-}  bytesConstEqW :: (ByteArrayAccess bs1, ByteArrayAccess bs2) => bs1 -> bs2 -> BoolW bytesConstEqW a b@@ -149,6 +153,7 @@ constSelectBytes :: BoolW -> ScrubbedBytes -> ScrubbedBytes -> ScrubbedBytes constSelectBytes (BoolW !mask) = Crypto.zipWith f   where f yes no = (mask .&. yes) .|. (complement mask .&. no)+{-# NOINLINE constSelectBytes #-}  -- This version of snoc accepts a more general input and uses internally a call -- to copyByteArrayToPtr, so it does not need a trampoline when the input is@@ -227,8 +232,14 @@  hashWith :: forall marking a ba. (HashAlgorithm a, ByteArrayAccess ba) => a -> ba -> Builder marking hashWith a ba = Builder.unsafeCreate (hashDigestSize a) $ \dig ->-    hashMutableInit >>= \ctx -> mask_ $ do+    withScrubbedContext a $ \ctx -> do         hashMutableUpdate (ctx :: MutableContext a) ba-        B.withByteArray ctx $ \pctx -> do+        B.withByteArray ctx $ \pctx ->             hashInternalFinalize (castPtr pctx :: Ptr (Context a)) dig-            fillBytes pctx 0 (B.length ctx)++withScrubbedContext :: HashAlgorithm a => a -> (MutableContext a -> IO b) -> IO b+withScrubbedContext _ f =+    hashMutableInit >>= \ctx -> f ctx `finally` eraseBytes ctx++eraseBytes :: ByteArrayAccess ba => ba -> IO ()+eraseBytes ctx = B.withByteArray ctx $ \p -> fillBytes p 0 (B.length ctx)
+ src/Endian.hs view
@@ -0,0 +1,43 @@+-- |+-- Module      : Endian+-- License     : BSD-3-Clause+-- Copyright   : (c) 2026 Olivier Chéron+--+-- Endianness utilities+--+{-# LANGUAGE CPP #-}+module Endian+    ( B.BE, B.LE, fromLE, toLE+    ) where++#include "MachDeps.h"++-- Little-endian conversion in `memory` / `ram` is avoided at compile+-- time only for AMD/Intel, here we will short circuit on ARM too+#if (defined(arm_HOST_ARCH) || defined(aarch64_HOST_ARCH)) \+    && !defined(WORDS_BIGENDIAN)+#define MLKEM_FORCE_LITTLE_ENDIAN_ARCH 1+#endif++#ifdef MLKEM_FORCE_LITTLE_ENDIAN_ARCH+#else+import Data.Memory.Endian (ByteSwap)+#endif++import qualified Data.Memory.Endian as B++#ifdef MLKEM_FORCE_LITTLE_ENDIAN_ARCH+fromLE :: B.LE a -> a+fromLE = B.unLE  -- unwrap constructor with no byte swapping+#else+fromLE :: ByteSwap a => B.LE a -> a+fromLE = B.fromLE  -- byte swap if necessary+#endif++#ifdef MLKEM_FORCE_LITTLE_ENDIAN_ARCH+toLE :: a -> B.LE a+toLE = B.LE  -- wrap constructor with no byte swapping+#else+toLE :: ByteSwap a => a -> B.LE a+toLE = B.toLE  -- byte swap if necessary+#endif
src/Fusion.hs view
@@ -51,10 +51,10 @@  thawContext :: Fusion a => a -> Context a thawContext a = Context $ thawF a-{-# INLINE [0] thawContext #-}+{-# INLINE CONLIKE [0] thawContext #-}  modifyContext :: (forall s. Mut a s -> ST s ()) -> Context a -> Context a-modifyContext f = bindContext $ \ma -> f ma >> return ma+modifyContext f = bindContext $ \ma -> ma <$ f ma  mapContext :: MapF a b -> Context a -> Context b mapContext m = bindContext (mapUpdate m)
src/Machine.hs view
@@ -22,19 +22,12 @@     || defined(powerpc64le_HOST_ARCH) #define MLKEM_ALLOW_UNALIGNED_OP 1 --- Little-endian conversion in `memory` / `ram` is avoided at compile--- time only for AMD/Intel, here we will short circuit on ARM too-#if (defined(arm_HOST_ARCH) || defined(aarch64_HOST_ARCH)) \-    && !defined(WORDS_BIGENDIAN)-#define MLKEM_FORCE_LITTLE_ENDIAN_ARCH 1 #endif -#endif- import Control.Exception (assert)  #ifdef MLKEM_ALLOW_UNALIGNED_OP-import qualified Data.Memory.Endian as B+import qualified Endian as B #endif  import Data.Bits@@ -52,18 +45,10 @@ type WordLE = B.LE WordM  fromLE :: WordLE -> WordM-#ifdef MLKEM_FORCE_LITTLE_ENDIAN_ARCH-fromLE = B.unLE  -- unwrap constructor with no byte swapping-#else-fromLE = B.fromLE  -- byte swap if necessary-#endif+fromLE = B.fromLE  toLE :: WordM -> WordLE-#ifdef MLKEM_FORCE_LITTLE_ENDIAN_ARCH-toLE = B.LE  -- wrap constructor with no byte swapping-#else-toLE = B.toLE  -- byte swap if necessary-#endif+toLE = B.toLE  #else 
src/ScrubbedBlock.hs view
@@ -72,7 +72,7 @@ checkPinned b = assertPinned b (ScrubbedBlock b)  scrubbed :: PrimMonad prim => MutableBlock ty (PrimState prim) -> prim (MutableBlock ty (PrimState prim))-scrubbed b = unsafePrimFromIO (scheduleBlockScrubbing b >> return b)+scrubbed b = unsafePrimFromIO (b <$ scheduleBlockScrubbing b)  wakeUpAfterInception :: MutableBlock ty s -> MutableBlock ty RealWorld wakeUpAfterInception = unsafeCoerce  -- sometimes disappointing
tests/Tests.hs view
@@ -24,6 +24,7 @@ import GHC.IO.Exception (IOErrorType(..))  import System.Directory (doesFileExist)+import System.FileLock (FileLock, SharedExclusive(..), unlockFile, withFileLock) import System.IO.Error (catchIOError, mkIOError) import System.Process (readProcess) @@ -180,16 +181,30 @@   where     scriptPath = "tests/get-vectors.sh"     free _ = return ()-    alloc = do+    whenNeeded action = do         keyGenExists <- doesFileExist "tests/keyGen.json.gz"         encapDecapExists <- doesFileExist "tests/encapDecap.json.gz"-        unless (keyGenExists && encapDecapExists) $ catchIOError+        unless (keyGenExists && encapDecapExists) action+    alloc = withTestLock Shared $ \lock -> whenNeeded $ do+        unlockFile lock  -- sanity before lock upgrade+        withTestLock Exclusive $ \_ -> whenNeeded $ catchIOError             (void $ readProcess "/bin/sh" [scriptPath] "")             (\e ->                 let msg = "Could not download test vectors, you will need to run the script `" ++                             scriptPath ++ "' manually. Script failure was: " ++ show e                  in ioError (mkIOError OtherError msg Nothing Nothing)             )++withTestLock :: SharedExclusive -> (FileLock -> IO a) -> IO a+withTestLock mode what = do+    -- locking a hidden file in the directory prevents a race condition between+    -- two instances of the test suite trying both to download the test vectors+    -- (otherwise one instance may try to run with files not fully downloaded+    -- yet by the other instance)+    let path = "tests/.lock"+    exists <- doesFileExist path+    unless exists $ writeFile path "DO NOT DELETE"+    withFileLock path mode what  keyGenVectors :: (String -> IO ()) -> Assertion keyGenVectors step = do
tests/get-vectors.sh view
@@ -2,7 +2,7 @@  DESTDIR="`dirname "$0"`" -REF=commit/cbb097019deb25eb966932ee293d89417482d8bc+REF=commit/9d33b0c138bee155d78a3feae3bf3d8d79057652 CURL=curl  for KEY in keyGen encapDecap; do