mlkem-0.1.1.0: tests/Tests.hs
-- |
-- Module : Main
-- License : BSD-3-Clause
-- Copyright : (c) 2025 Olivier Chéron
--
-- The ML-KEM test suite. Can be instanciated twice, with and without the
-- @ML_KEM_TESTING@ macro to run property testing with assertions enabled in
-- the internal modules.
--
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE KindSignatures #-}
module Main (main) where
import Data.ByteArray (Bytes)
import qualified Data.ByteArray as B
import Control.Monad
import Data.Maybe (isJust, fromJust)
import Data.Proxy
import GHC.IO.Exception (IOErrorType(..))
import System.Directory (doesFileExist)
import System.IO.Error (catchIOError, mkIOError)
import System.Process (readProcess)
#ifdef ML_KEM_TESTING
import Data.Bits
import Data.Word
import GHC.TypeNats
import Foreign.Ptr (plusPtr)
import Auxiliary
import BlockN (BlockN)
import Builder (Builder)
import Marking (Leak(..), SecurityMarking(..))
import Math
import Matrix
import Vector
import qualified BlockN
import qualified Builder
#endif
import Crypto.PubKey.ML_KEM as Lib
import Test.Tasty
import Test.Tasty.HUnit
import Test.Tasty.QuickCheck
import qualified EncapDecap
import qualified KeyGen
import qualified Vectors
#ifdef ML_KEM_TESTING
newtype Bit7 = Bit7 Word8 deriving Show
instance Arbitrary Bit7 where
#if (MIN_VERSION_tasty_quickcheck(0,10,2))
arbitrary = Bit7 <$> chooseBoundedIntegral (0, 127)
#else
arbitrary = Bit7 <$> choose (0, 127)
#endif
newtype FE = FE { unFE :: Zq} deriving Show
instance Arbitrary FE where
#if (MIN_VERSION_tasty_quickcheck(0,10,2))
arbitrary = FE . toZq <$> chooseBoundedIntegral (0, 3328)
#else
arbitrary = FE . toZq <$> choose (0, 3328)
#endif
newtype Poly = Poly (Rq Sec) deriving Show
instance Arbitrary Poly where
arbitrary = do
coeffs <- map unFE <$> vectorOf 256 arbitrary
let a = fromJust (fromCoeffs coeffs)
return (Poly a)
newtype PolyNTT = PolyNTT (Tq Sec) deriving Show
instance Arbitrary PolyNTT where
arbitrary = (\(Poly f) -> PolyNTT (ntt f)) <$> arbitrary
newtype D = D Int deriving Show
instance Arbitrary D where
arbitrary = D <$> choose (0, 12)
data Dim = forall (n :: Nat). KnownNat n => Dim (Proxy n)
instance Show Dim where
show (Dim n) = show n
instance Arbitrary Dim where
arbitrary = toDim <$> choose (1, 9)
toDim :: Int -> Dim
toDim n = case someNatVal (fromIntegral n) of SomeNat p -> Dim p
type VElem = Zq -- test with any ring but Tq would also work here
arbitraryVector :: KnownNat n => proxy n -> Gen (Vector n VElem)
arbitraryVector _ = Vector.replicateM (unFE <$> arbitrary)
arbitraryMatrix :: (KnownNat m, KnownNat n) => proxy n -> proxy m -> Gen (Vector n (Vector m VElem))
arbitraryMatrix _ m = Vector.replicateM (arbitraryVector m)
arbitraryBytes :: Int -> Gen Bytes
arbitraryBytes n = B.pack <$> vectorOf n arbitrary
byteDecodeBytes :: Int -> Bytes -> BlockN Sec 256 Word16
byteDecodeBytes = byteDecode
byteEncodeBytes :: Int -> BlockN Sec 256 Word16 -> Bytes
byteEncodeBytes d = runBytes . byteEncode d
runBytes :: Builder Sec -> Bytes
runBytes = Builder.run . leak
runUnaligned :: Builder Sec -> UnalignedBytes
runUnaligned = Builder.runRelaxed . leak
newtype UnalignedBytes = UnalignedBytes (B.View Bytes)
deriving (Eq,Ord,Show)
instance Semigroup UnalignedBytes where
UnalignedBytes a <> UnalignedBytes b =
B.allocAndFreeze (na + nb) $ \p -> do
B.copyByteArrayToPtr a p
B.copyByteArrayToPtr b (p `plusPtr` na)
where
na = B.length a
nb = B.length b
instance Monoid UnalignedBytes where
mempty = B.convert (B.empty :: Bytes)
instance B.ByteArrayAccess UnalignedBytes where
length (UnalignedBytes v) = B.length v
withByteArray (UnalignedBytes v) = B.withByteArray v
instance B.ByteArray UnalignedBytes where
allocRet n f = do
let build ba = UnalignedBytes (B.dropView ba offset)
(a, ba) <- B.allocRet (n + offset) $ \p -> f (p `plusPtr` offset)
return (a, build ba)
where offset = 1
#endif
data P = forall a. (ParamSet a, Show a) => P (Proxy a)
instance Show P where
show (P p) = show p
instance Arbitrary P where
arbitrary = elements
[ P (Proxy :: Proxy ML_KEM_512)
, P (Proxy :: Proxy ML_KEM_768)
, P (Proxy :: Proxy ML_KEM_1024)
]
toP :: String -> P
toP "ML-KEM-512" = P (Proxy :: Proxy ML_KEM_512)
toP "ML-KEM-768" = P (Proxy :: Proxy ML_KEM_768)
toP "ML-KEM-1024" = P (Proxy :: Proxy ML_KEM_1024)
toP paramSet = error ("unknown parameter set " ++ paramSet)
withVectors :: (IO () -> TestTree) -> TestTree
withVectors = withResource alloc free
where
scriptPath = "tests/get-vectors.sh"
free _ = return ()
alloc = do
keyGenExists <- doesFileExist "tests/keyGen.json.gz"
encapDecapExists <- doesFileExist "tests/encapDecap.json.gz"
unless (keyGenExists && encapDecapExists) $ 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)
)
keyGenVectors :: (String -> IO ()) -> Assertion
keyGenVectors step = do
step "Reading test vectors ..."
file <- Vectors.readJson "tests/keyGen.json.gz"
forM_ (Vectors.testGroups file) $ \group -> do
let paramSet = KeyGen.parameterSet group
step paramSet
case toP paramSet of
P p -> forM_ (KeyGen.tests group) $ \t -> do
let tcId = KeyGen.tcId t
eks = Lib.encode ek
dks = Lib.encode dk
(ek, dk) = fromJust $ Lib.generateWith p (KeyGen.d t) (KeyGen.z t)
assertEqual ("ek mismatch for tcId=" ++ show tcId) (KeyGen.ek t) eks
assertEqual ("dk mismatch for tcId=" ++ show tcId) (KeyGen.dk t) dks
encapDecapVectors :: (String -> IO ()) -> Assertion
encapDecapVectors step = do
step "Reading test vectors ..."
file <- Vectors.readJson "tests/encapDecap.json.gz"
forM_ (Vectors.testGroups file) $ \group -> do
let paramSet = EncapDecap.parameterSet group
step (paramSet ++ " (" ++ EncapDecap.function group ++ ")")
case toP paramSet of
P p -> case EncapDecap.payload group of
EncapDecap.FunctionEncapsulation tests ->
forM_ tests (testEncapsulation p)
EncapDecap.FunctionDecapsulation tests ->
forM_ tests (testDecapsulation p)
EncapDecap.FunctionEncapsulationKeyCheck tests ->
forM_ tests (testEncapsulationKeyCheck p)
EncapDecap.FunctionDecapsulationKeyCheck tests ->
forM_ tests (testDecapsulationKeyCheck p)
where
ensureEk = id :: f (EncapsulationKey a) -> f (EncapsulationKey a)
ensureDk = id :: f (DecapsulationKey a) -> f (DecapsulationKey a)
testEncapsulation p test = do
let tcId = EncapDecap.tcId test
ext = EncapDecap.tcExt test
ek = fromJust $ Lib.decode p (EncapDecap.ekEnc ext)
k' = fromJust $ Lib.decode p (EncapDecap.kEnc ext)
c' = fromJust $ Lib.decode p (EncapDecap.cEnc ext)
(k, c) = fromJust $ Lib.encapsulateWith ek (EncapDecap.mEnc ext)
assertEqual ("k mismatch for tcId=" ++ show tcId) k' k
assertEqual ("c mismatch for tcId=" ++ show tcId) c' c
testDecapsulation p test = do
let tcId = EncapDecap.tcId test
ext = EncapDecap.tcExt test
dk = fromJust $ Lib.decode p (EncapDecap.dkDec ext)
c = fromJust $ Lib.decode p (EncapDecap.cDec ext)
k' = fromJust $ Lib.decode p (EncapDecap.kDec ext)
k = Lib.decapsulate dk c
assertEqual ("k mismatch for tcId=" ++ show tcId) k' k
testEncapsulationKeyCheck p test = do
let tcId = EncapDecap.tcId test
ext = EncapDecap.tcExt test
mek = ensureEk $ Lib.decode p (EncapDecap.ekEkc ext)
assertBool ("opposite outcome for tcId=" ++ show tcId)
(EncapDecap.passedEkc ext == isJust mek)
testDecapsulationKeyCheck p test = do
let tcId = EncapDecap.tcId test
ext = EncapDecap.tcExt test
mdk = ensureDk $ Lib.decode p (EncapDecap.dkDkc ext)
assertBool ("opposite outcome for tcId=" ++ show tcId)
(EncapDecap.passedDkc ext == isJust mdk)
main :: IO ()
main = defaultMain $ testGroup "mlkem"
[ withVectors $ \_ -> testGroup "vectors"
[ testCaseSteps "keyGen" keyGenVectors
, testCaseSteps "encapDecap" encapDecapVectors
]
, testGroup "properties"
[ testGroup "ML-KEM"
[ testProperty "encapsulate/decapsulate" $ \(P p) -> ioProperty $ do
(ek, dk) <- Lib.generate p
(kk, c) <- Lib.encapsulate ek
let kk' = Lib.decapsulate dk c
return (kk === kk')
, testProperty "encode/decode keys" $ \(P p) -> ioProperty $ do
(ek, dk) <- Lib.generate p
return $ conjoin
[ Just ek === Lib.decode p (Lib.encode ek :: Bytes)
, Just dk === Lib.decode p (Lib.encode dk :: Bytes)
]
, testProperty "convert/decode ciphertext and shared secret" $ \(P p) -> ioProperty $ do
(ek, _) <- Lib.generate p
(kk, c) <- Lib.encapsulate ek
return $ conjoin
[ Just c === Lib.decode p (B.convert c :: Bytes)
, Just kk === Lib.decode p (B.convert kk :: Bytes)
]
, testProperty "toPublic" $ \(P p) -> ioProperty $ do
(ek, dk) <- Lib.generate p
return (ek === toPublic dk)
, testProperty "checkKeyPair" $ \(P p) -> ioProperty $
Lib.generate p >>= checkKeyPair
]
#ifdef ML_KEM_TESTING
, testGroup "bitRev7"
[ testCase "powers of two" $
let powers = [1, 2, 4, 8, 16, 32, 64]
in reverse powers @=? map bitRev7 powers
, testProperty "or" $ \(Bit7 a) (Bit7 b) ->
bitRev7 (a .|. b) === bitRev7 a .|. bitRev7 b
, testProperty "not" $ \(Bit7 a) ->
let comp = xor 127
in bitRev7 (comp a) === comp (bitRev7 a)
, testProperty "involutive" $ \(Bit7 a) ->
a === bitRev7 (bitRev7 a)
, testProperty "preserves bit count" $ \(Bit7 a) ->
popCount a === popCount (bitRev7 a)
]
, testGroup "compression"
[ testProperty "compress . decompress == id" $ \(D d) -> do
y <- choose (0, 2^d - 1)
return (d < 12 ==> y === compress d (decompress d y))
]
, testGroup "encoding"
[ testProperty "byteEncode . byteDecode == id" $ \(D d) -> do
b <- arbitraryBytes (32 * d)
return (b === runBytes (byteEncode d (byteDecode d b)))
, testProperty "byteEncode . byteDecode == id (unaligned)" $ \(D d) -> do
b <- arbitraryBytes (32 * d)
return (B.convert b === runUnaligned (byteEncode d (byteDecode d b)))
, testProperty "byteEncode1 . byteDecode1 == id" $ do
b <- arbitraryBytes 32
return (b === runBytes (byteEncode1 (byteDecode1 b)))
, testProperty "byteEncode1 . byteDecode1 == id (unaligned)" $ do
b <- arbitraryBytes 32
return (B.convert b === runUnaligned (byteEncode1 (byteDecode1 b)))
, testProperty "byteDecode12 . byteEncode12 == id" $ \(PolyNTT p) ->
p === byteDecode12 (runBytes $ byteEncode12 p)
, testProperty "byteDecode12 . byteEncode12 == id (unaligned)" $ \(PolyNTT p) ->
p === byteDecode12 (runUnaligned $ byteEncode12 p)
, testProperty "byteEncode 8" $ \x ->
B.replicate 256 x === byteEncodeBytes 8 (BlockN.replicate $ fromIntegral x)
, testCase "byteEncode 1 (zeros)" $
B.replicate 32 0 @=? byteEncodeBytes 1 (BlockN.replicate 0)
, testCase "byteEncode 1 (ones)" $
B.replicate 32 255 @=? byteEncodeBytes 1 (BlockN.replicate 1)
, testProperty "byteDecode1 == byteDecode 1" $ do
b <- arbitraryBytes 32
return (byteDecodeBytes 1 b === byteDecode1 b)
]
, testGroup "Zq"
[ testProperty "toZq . fromZq == id " $ \(FE a) ->
a === toZq (fromZq a)
, testProperty "fromZq . toZq == id " $ \a ->
mod a 3329 === fromZq (toZq a)
, testCase "field order" $ zero @=? toZq 3329
, testProperty "addition with zero" $ \(FE a) ->
conjoin [ a === zero .+ a
, a === a .+ zero
]
, testProperty "addition associative" $ \(FE a) (FE b) (FE c) ->
a .+ (b .+ c) === (a .+ b) .+ c
, testProperty "addition commutative" $ \(FE a) (FE b) ->
a .+ b === b .+ a
, testProperty "substraction with zero" $ \(FE a) ->
a === a .- zero
, testProperty "substraction non-associative" $ \(FE a) (FE b) (FE c) ->
a .- (b .- c) === (a .- b) .+ c
, testProperty "substraction anti-commutative" $ \(FE a) (FE b) ->
a .- b === neg (b .- a)
, testProperty "negation" $ \(FE a) ->
neg a === zero .- a
, testProperty "double negation" $ \(FE a) ->
a === neg (neg a)
, testProperty "multiplication with zero" $ \(FE a) ->
conjoin [ zero === zero .* a
, zero === a .* zero
]
, testProperty "multiplication with one" $ \(FE a) ->
conjoin [ a === one .* a
, a === a .* one
]
, testProperty "multiplication associative" $ \(FE a) (FE b) (FE c) ->
a .* (b .* c) === (a .* b) .* c
, testProperty "multiplication commutative" $ \(FE a) (FE b) ->
a .* b === b .* a
, testProperty "multiplication distributive" $ \(FE a) (FE b) (FE c) ->
conjoin [ (a .* b) .+ (a .* c) === a .* (b .+ c)
, (b .* a) .+ (c .* a) === (b .+ c) .* a
]
, testProperty "mulAdd" $ \(FE a) (FE b) (FE c) ->
a .* b .+ c === mulAdd a b c
]
, testGroup "Rq"
[ testProperty "fromCoeffs . toCoeffs == id " $ \(Poly a) ->
Just a === fromCoeffs (toCoeffs a)
, testProperty "addition with zero" $ \(Poly a) ->
conjoin [ a === zero .+ a
, a === a .+ zero
]
, testProperty "addition associative" $ \(Poly a) (Poly b) (Poly c) ->
a .+ (b .+ c) === (a .+ b) .+ c
, testProperty "addition commutative" $ \(Poly a) (Poly b) ->
a .+ b === b .+ a
, testProperty "substraction with zero" $ \(Poly a) ->
a === a .- zero
, testProperty "substraction non-associative" $ \(Poly a) (Poly b) (Poly c) ->
a .- (b .- c) === (a .- b) .+ c
, testProperty "substraction anti-commutative" $ \(Poly a) (Poly b) ->
a .- b === neg (b .- a)
, testProperty "negation" $ \(Poly a) ->
neg a === zero .- a
, testProperty "double negation" $ \(Poly a) ->
a === neg (neg a)
]
, testGroup "Tq"
[ testProperty "nttInv . ntt == id" $ \(Poly a) ->
a === nttInv (ntt a)
, testProperty "addition with zero" $ \(PolyNTT a) ->
conjoin [ a === zero .+ a
, a === a .+ zero
]
, testProperty "addition associative" $ \(PolyNTT a) (PolyNTT b) (PolyNTT c) ->
a .+ (b .+ c) === (a .+ b) .+ c
, testProperty "addition commutative" $ \(PolyNTT a) (PolyNTT b) ->
a .+ b === b .+ a
, testProperty "substraction with zero" $ \(PolyNTT a) ->
a === a .- zero
, testProperty "substraction non-associative" $ \(PolyNTT a) (PolyNTT b) (PolyNTT c) ->
a .- (b .- c) === (a .- b) .+ c
, testProperty "substraction anti-commutative" $ \(PolyNTT a) (PolyNTT b) ->
a .- b === neg (b .- a)
, testProperty "negation" $ \(PolyNTT a) ->
neg a === zero .- a
, testProperty "double negation" $ \(PolyNTT a) ->
a === neg (neg a)
, testProperty "multiplication with zero" $ \(PolyNTT a) ->
conjoin [ zero === zero .* a
, zero === a .* zero
]
, testProperty "multiplication with one" $ \(PolyNTT a) ->
conjoin [ a === one .* a
, a === a .* one
]
, testProperty "multiplication associative" $ \(PolyNTT a) (PolyNTT b) (PolyNTT c) ->
a .* (b .* c) === (a .* b) .* c
, testProperty "multiplication commutative" $ \(PolyNTT a) (PolyNTT b) ->
a .* b === b .* a
, testProperty "multiplication distributive" $ \(PolyNTT a) (PolyNTT b) (PolyNTT c) ->
conjoin [ (a .* b) .+ (a .* c) === a .* (b .+ c)
, (b .* a) .+ (c .* a) === (b .+ c) .* a
]
, testProperty "mulAdd" $ \(PolyNTT a) (PolyNTT b) (PolyNTT c) ->
a .* b .+ c === mulAdd a b c
]
, testGroup "Vector"
[ testProperty "addition with zero" $ \(Dim n) -> do
a <- arbitraryVector n
return $ conjoin
[ a === zero .+ a
, a === a .+ zero
]
, testProperty "addition associative" $ \(Dim n) -> do
(a, b, c) <- (,,) <$> arbitraryVector n <*> arbitraryVector n <*> arbitraryVector n
return (a .+ (b .+ c) === (a .+ b) .+ c)
, testProperty "addition commutative" $ \(Dim n) -> do
(a, b) <- (,) <$> arbitraryVector n <*> arbitraryVector n
return (a .+ b === b .+ a)
, testProperty "substraction with zero" $ \(Dim n) -> do
a <- arbitraryVector n
return (a === a .- zero)
, testProperty "substraction non-associative" $ \(Dim n) -> do
(a, b, c) <- (,,) <$> arbitraryVector n <*> arbitraryVector n <*> arbitraryVector n
return (a .- (b .- c) === (a .- b) .+ c)
, testProperty "substraction anti-commutative" $ \(Dim n) -> do
(a, b) <- (,) <$> arbitraryVector n <*> arbitraryVector n
return (a .- b === neg (b .- a))
, testProperty "negation" $ \(Dim n) -> do
a <- arbitraryVector n
return (neg a === zero .- a)
, testProperty "double negation" $ \(Dim n) -> do
a <- arbitraryVector n
return (a === neg (neg a))
]
, testGroup "Matrix"
[ testProperty "mulw distributive left" $ \(Dim n) (Dim m) -> do
(a, b, u, v) <- (,,,) <$> arbitraryMatrix m n <*> arbitraryMatrix m n <*> arbitraryVector m <*> arbitraryVector n
return (mulw (a .+ b) u v === mulw a u (mulw b u zero) .+ v)
, testProperty "mulw distributive right" $ \(Dim n) (Dim m) -> do
(a, u, v, w) <- (,,,) <$> arbitraryMatrix m n <*> arbitraryVector m <*> arbitraryVector m <*> arbitraryVector n
return (mulw a (u .+ v) w === mulw a u (mulw a v w))
, testProperty "muly definition" $ \(Dim n) (Dim m) -> do
(a, u, v) <- (,,) <$> arbitraryMatrix n m <*> arbitraryVector m <*> arbitraryVector n
return ((a `muly` u) .+ v == mulw (transpose a) u v)
, testProperty "muly distributive left" $ \(Dim n) (Dim m) -> do
(a, b, u) <- (,,) <$> arbitraryMatrix n m <*> arbitraryMatrix n m <*> arbitraryVector m
return ((a .+ b) `muly` u === (a `muly` u) .+ (b `muly` u))
, testProperty "muly distributive right" $ \(Dim n) (Dim m) -> do
(a, u, v) <- (,,) <$> arbitraryMatrix n m <*> arbitraryVector m <*> arbitraryVector m
return (a `muly` (u .+ v) === (a `muly` u) .+ (a `muly` v))
, testProperty "mulz commutative" $ \(Dim n) -> do
(u, v) <- (,) <$> arbitraryVector n <*> arbitraryVector n
return (u `mulz` v === v `mulz` u)
, testProperty "mulz distributive" $ \(Dim n) -> do
(u, v, w) <- (,,) <$> arbitraryVector n <*> arbitraryVector n <*> arbitraryVector n
return $ conjoin
[ u `mulz` (v .+ w) === (u `mulz` v) .+ (u `mulz` w)
, (u .+ v) `mulz` w === (u `mulz` w) .+ (v `mulz` w)
]
]
#endif
]
]