KpqC-0.1.0: test/KAT.hs
{-# LANGUAGE ForeignFunctionInterface #-}
module Main (main) where
import Control.Exception (finally)
import Control.Monad (forM_)
import Crypto.KpqC
import qualified Crypto.KpqC.Internal.Native.AIMer as AIMer
import qualified Crypto.KpqC.Internal.Native.HAETAE as HAETAE
import qualified Crypto.KpqC.Internal.Native.NTRUPlus as NTRUPlus
import qualified Crypto.KpqC.Internal.Native.SMAUGT as SMAUGT
import Data.Bits (shiftR, xor, (.&.))
import qualified Data.ByteArray as BA
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Unsafe as BSU
import Data.Char (digitToInt, isHexDigit, isSpace)
import qualified Data.Map.Strict as Map
import Data.Word (Word8)
import Foreign.C.Types (CInt, CSize)
import Foreign.Marshal.Array (allocaArray)
import Foreign.Marshal.Utils (fillBytes)
import Foreign.Ptr (Ptr, castPtr)
import System.Environment (lookupEnv)
import System.Exit (exitFailure)
import System.FilePath ((</>))
import System.IO.Unsafe (unsafePerformIO)
foreign import ccall unsafe "kpqc_test_aes256_ecb" aes256EcbNative :: Ptr Word8 -> Ptr Word8 -> Ptr Word8 -> IO ()
data EntropyBackend = EntropyBackend
{ setEntropyNative :: Ptr Word8 -> CSize -> IO CInt,
entropyRemainingNative :: IO CSize,
clearEntropyNative :: IO ()
}
data DRBG = DRBG
{ drbgKey :: !ByteString,
drbgCounter :: !ByteString
}
type Vector = Map.Map String String
main :: IO ()
main = do
root <- maybe "../kpqc-test-vectors" id <$> lookupEnv "KPQC_TEST_VECTORS"
signatureCount <- sum <$> mapM (checkSignatureVectors root) signatureConfigurations
kemCount <- sum <$> mapM (checkKEMVectors root) kemConfigurations
assertEqual "known-answer vector count" 1600 (signatureCount + kemCount)
checkEntropyExhaustion
putStrLn "All 1,600 KpqC known-answer vectors passed."
signatureConfigurations :: [(SignatureAlgorithm, FilePath, Bool, Int)]
signatureConfigurations =
[ (AIMer128f, "aimer" </> "128f", False, 16),
(AIMer128s, "aimer" </> "128s", False, 16),
(AIMer192f, "aimer" </> "192f", False, 24),
(AIMer192s, "aimer" </> "192s", False, 24),
(AIMer256f, "aimer" </> "256f", False, 32),
(AIMer256s, "aimer" </> "256s", False, 32),
(HAETAE2, "haetae" </> "mode2", True, 32),
(HAETAE3, "haetae" </> "mode3", True, 32),
(HAETAE5, "haetae" </> "mode5", True, 32)
]
kemConfigurations :: [(KEMAlgorithm, FilePath, Int, Int)]
kemConfigurations =
[ (NTRUPlus768, "ntruplus" </> "768", 0, 96),
(NTRUPlus864, "ntruplus" </> "864", 0, 108),
(NTRUPlus1152, "ntruplus" </> "1152", 0, 144),
(SMAUGT128, "smaugt" </> "mode1", 2, 32),
(SMAUGT192, "smaugt" </> "mode3", 2, 32),
(SMAUGT256, "smaugt" </> "mode5", 2, 32),
(TiMER, "smaugt" </> "modet", 2, 16)
]
checkSignatureVectors :: FilePath -> (SignatureAlgorithm, FilePath, Bool, Int) -> IO Int
checkSignatureVectors root (algorithm, relativePath, isHAETAE, seedBytes) = do
vectors <- readVectors (root </> relativePath </> "kat.rsp")
assertEqual (algorithmId algorithm ++ " vector count") 100 (length vectors)
forM_ vectors $ \vector -> do
let initial = newDRBG (field vector "seed")
(keys, afterKeyPair) <-
if isHAETAE
then do
let (entropy, next) = generate seedBytes initial
result <- withExactEntropy (signatureEntropy algorithm) entropy (generateKeyPair algorithm)
value <- expectRight result
pure (value, next)
else do
(result, next) <- withRepeatedEntropy (signatureEntropy algorithm) seedBytes initial (generateKeyPair algorithm)
value <- expectRight result
pure (value, next)
assertEqual (label vector "public key") (field vector "pk") (publicKey keys)
assertEqual (label vector "secret key") (field vector "sk") (secretByteString (secretKey keys))
let message = field vector "msg"
expectedSignature =
case Map.lookup "sig" vector of
Just encoded -> decodeHex encoded
Nothing -> BS.drop (BS.length message) (field vector "sm")
(signingEntropy, afterSigningEntropy) = generate seedBytes afterKeyPair
(context, _) =
if isHAETAE
then
let (lengthByte, afterLength) = generate 1 afterSigningEntropy
contextLength = fromIntegral (BS.head lengthByte)
in generate contextLength afterLength
else (BS.empty, afterSigningEntropy)
signature <-
withExactEntropy
(signatureEntropy algorithm)
signingEntropy
(signWithContext algorithm message (secretKey keys) context)
>>= expectRight
assertEqual (label vector "signature") expectedSignature signature
verifyWithContext algorithm message expectedSignature (publicKey keys) context
>>= expectRight
>>= assertTrue (label vector "verification")
pure (length vectors)
checkKEMVectors :: FilePath -> (KEMAlgorithm, FilePath, Int, Int) -> IO Int
checkKEMVectors root (algorithm, relativePath, keyCalls, encapsulationBytes) = do
vectors <- readVectors (root </> relativePath </> "kat.rsp")
assertEqual (algorithmId algorithm ++ " vector count") 100 (length vectors)
forM_ vectors $ \vector -> do
let initial = newDRBG (field vector "seed")
(keys, afterKeyPair) <-
if keyCalls == 0
then do
(result, next) <- withRepeatedEntropy (kemEntropy algorithm) 32 initial (generateKeyPair algorithm)
value <- expectRight result
pure (value, next)
else do
let (chunks, next) = generateMany keyCalls 32 initial
result <- withExactEntropy (kemEntropy algorithm) (BS.concat chunks) (generateKeyPair algorithm)
value <- expectRight result
pure (value, next)
assertEqual (label vector "public key") (field vector "pk") (publicKey keys)
assertEqual (label vector "secret key") (field vector "sk") (secretByteString (secretKey keys))
let (entropy, _) = generate encapsulationBytes afterKeyPair
outbound <-
withExactEntropy
(kemEntropy algorithm)
entropy
(encapsulate algorithm (publicKey keys))
>>= expectRight
assertEqual (label vector "ciphertext") (field vector "ct") (ciphertext outbound)
assertEqual (label vector "shared secret") (field vector "ss") (secretByteString (sharedSecret outbound))
decapsulate algorithm (ciphertext outbound) (secretKey keys)
>>= expectRight
>>= assertEqual (label vector "decapsulation") (BA.convert (field vector "ss") :: SecretBytes)
pure (length vectors)
checkEntropyExhaustion :: IO ()
checkEntropyExhaustion = do
forM_ signatureAlgorithms $ \algorithm ->
expectEntropyExhaustion (algorithmId algorithm) (signatureEntropy algorithm) (generateKeyPair algorithm)
forM_ kemAlgorithms $ \algorithm ->
expectEntropyExhaustion (algorithmId algorithm) (kemEntropy algorithm) (generateKeyPair algorithm)
expectEntropyExhaustion :: String -> EntropyBackend -> IO (Either KpqCError value) -> IO ()
expectEntropyExhaustion name backend operation = do
setEntropy backend BS.empty
result <- operation `finally` clearEntropyNative backend
case result of
Left (NativeFailure KeyGeneration _) -> pure ()
Left failure -> failTest (name ++ " entropy exhaustion returned " ++ show failure)
Right _ -> failTest (name ++ " accepted an exhausted entropy source")
secretByteString :: SecretBytes -> ByteString
secretByteString = BA.convert
withExactEntropy :: EntropyBackend -> ByteString -> IO value -> IO value
withExactEntropy backend entropy operation = do
setEntropy backend entropy
( do
result <- operation
remaining <- (fromIntegral <$> entropyRemainingNative backend) :: IO Int
assertEqual "unused deterministic entropy" 0 remaining
pure result
)
`finally` clearEntropyNative backend
withRepeatedEntropy :: EntropyBackend -> Int -> DRBG -> IO value -> IO (value, DRBG)
withRepeatedEntropy backend callBytes state operation = do
let (chunks, _) = generateMany 128 callBytes state
entropy = BS.concat chunks
setEntropy backend entropy
( do
result <- operation
remaining <- fromIntegral <$> entropyRemainingNative backend
let consumed = BS.length entropy - remaining
assertTrue "deterministic entropy was consumed" (consumed > 0)
assertEqual "deterministic entropy chunk alignment" 0 (consumed `mod` callBytes)
let (_, next) = generateMany (consumed `div` callBytes) callBytes state
pure (result, next)
)
`finally` clearEntropyNative backend
setEntropy :: EntropyBackend -> ByteString -> IO ()
setEntropy backend entropy = do
status <-
BSU.unsafeUseAsCStringLen entropy $ \(pointer, size) ->
setEntropyNative backend (castPtr pointer) (fromIntegral size)
assertEqual "set deterministic entropy" 0 status
signatureEntropy :: SignatureAlgorithm -> EntropyBackend
signatureEntropy algorithm = case algorithm of
AIMer128f -> EntropyBackend AIMer.aimer128fSetTestEntropy AIMer.aimer128fTestEntropyRemaining AIMer.aimer128fClearTestEntropy
AIMer128s -> EntropyBackend AIMer.aimer128sSetTestEntropy AIMer.aimer128sTestEntropyRemaining AIMer.aimer128sClearTestEntropy
AIMer192f -> EntropyBackend AIMer.aimer192fSetTestEntropy AIMer.aimer192fTestEntropyRemaining AIMer.aimer192fClearTestEntropy
AIMer192s -> EntropyBackend AIMer.aimer192sSetTestEntropy AIMer.aimer192sTestEntropyRemaining AIMer.aimer192sClearTestEntropy
AIMer256f -> EntropyBackend AIMer.aimer256fSetTestEntropy AIMer.aimer256fTestEntropyRemaining AIMer.aimer256fClearTestEntropy
AIMer256s -> EntropyBackend AIMer.aimer256sSetTestEntropy AIMer.aimer256sTestEntropyRemaining AIMer.aimer256sClearTestEntropy
HAETAE2 -> EntropyBackend HAETAE.haetae2SetTestEntropy HAETAE.haetae2TestEntropyRemaining HAETAE.haetae2ClearTestEntropy
HAETAE3 -> EntropyBackend HAETAE.haetae3SetTestEntropy HAETAE.haetae3TestEntropyRemaining HAETAE.haetae3ClearTestEntropy
HAETAE5 -> EntropyBackend HAETAE.haetae5SetTestEntropy HAETAE.haetae5TestEntropyRemaining HAETAE.haetae5ClearTestEntropy
kemEntropy :: KEMAlgorithm -> EntropyBackend
kemEntropy algorithm = case algorithm of
NTRUPlus768 -> EntropyBackend NTRUPlus.ntruplus768SetTestEntropy NTRUPlus.ntruplus768TestEntropyRemaining NTRUPlus.ntruplus768ClearTestEntropy
NTRUPlus864 -> EntropyBackend NTRUPlus.ntruplus864SetTestEntropy NTRUPlus.ntruplus864TestEntropyRemaining NTRUPlus.ntruplus864ClearTestEntropy
NTRUPlus1152 -> EntropyBackend NTRUPlus.ntruplus1152SetTestEntropy NTRUPlus.ntruplus1152TestEntropyRemaining NTRUPlus.ntruplus1152ClearTestEntropy
SMAUGT128 -> EntropyBackend SMAUGT.smaugt128SetTestEntropy SMAUGT.smaugt128TestEntropyRemaining SMAUGT.smaugt128ClearTestEntropy
SMAUGT192 -> EntropyBackend SMAUGT.smaugt192SetTestEntropy SMAUGT.smaugt192TestEntropyRemaining SMAUGT.smaugt192ClearTestEntropy
SMAUGT256 -> EntropyBackend SMAUGT.smaugt256SetTestEntropy SMAUGT.smaugt256TestEntropyRemaining SMAUGT.smaugt256ClearTestEntropy
TiMER -> EntropyBackend SMAUGT.timerSetTestEntropy SMAUGT.timerTestEntropyRemaining SMAUGT.timerClearTestEntropy
newDRBG :: ByteString -> DRBG
newDRBG entropy
| BS.length entropy == 48 = updateDRBG (DRBG (BS.replicate 32 0) (BS.replicate 16 0)) entropy
| otherwise = error "CTR-DRBG seed must be 48 bytes"
generate :: Int -> DRBG -> (ByteString, DRBG)
generate size state =
let blocks = (size + 15) `div` 16
(output, advanced) = generateBlocks blocks state
in (BS.take size (BS.concat output), updateDRBG advanced BS.empty)
generateMany :: Int -> Int -> DRBG -> ([ByteString], DRBG)
generateMany count size = run count []
where
run 0 chunks state = (reverse chunks, state)
run remaining chunks state =
let (chunk, next) = generate size state
in run (remaining - 1) (chunk : chunks) next
updateDRBG :: DRBG -> ByteString -> DRBG
updateDRBG state provided =
let (blocks, _) = generateBlocks 3 state
material = xorBytes (BS.concat blocks) (provided <> BS.replicate (48 - BS.length provided) 0)
in DRBG (BS.take 32 material) (BS.drop 32 material)
generateBlocks :: Int -> DRBG -> ([ByteString], DRBG)
generateBlocks count = run count []
where
run 0 blocks state = (reverse blocks, state)
run remaining blocks state =
let counter = incrementCounter (drbgCounter state)
block = aes256 (drbgKey state) counter
next = state {drbgCounter = counter}
in run (remaining - 1) (block : blocks) next
aes256 :: ByteString -> ByteString -> ByteString
aes256 key block = unsafePerformIO $
allocaArray 16 $ \output ->
( do
fillBytes output 0 16
BSU.unsafeUseAsCStringLen key $ \(keyPointer, _) ->
BSU.unsafeUseAsCStringLen block $ \(blockPointer, _) ->
aes256EcbNative output (castPtr keyPointer) (castPtr blockPointer)
BS.packCStringLen (castPtr output, 16)
)
`finally` fillBytes output 0 16
{-# NOINLINE aes256 #-}
incrementCounter :: ByteString -> ByteString
incrementCounter counter = integerBytes 16 ((bytesInteger counter + 1) `mod` (2 ^ (128 :: Int)))
bytesInteger :: ByteString -> Integer
bytesInteger = BS.foldl' (\value byte -> value * 256 + fromIntegral byte) 0
integerBytes :: Int -> Integer -> ByteString
integerBytes size value =
BS.pack [fromIntegral ((value `shiftR` (position * 8)) .&. 255) | position <- reverse [0 .. size - 1]]
xorBytes :: ByteString -> ByteString -> ByteString
xorBytes left right = BS.pack (BS.zipWith xor left right)
readVectors :: FilePath -> IO [Vector]
readVectors path = do
contents <- readFile path
pure (finish (foldl parseLine ([], Map.empty) (lines contents)))
where
finish (vectors, current)
| Map.null current = reverse vectors
| otherwise = reverse (current : vectors)
parseLine (vectors, current) line =
case splitField line of
Just ("count", value)
| not (Map.null current) -> (current : vectors, Map.singleton "count" value)
Just (name, value) -> (vectors, Map.insert name value current)
Nothing -> (vectors, current)
splitField :: String -> Maybe (String, String)
splitField line = case break (== '=') line of
(name, '=' : value) -> Just (trim name, trim value)
_ -> Nothing
trim :: String -> String
trim = reverse . dropWhile isSpace . reverse . dropWhile isSpace
field :: Vector -> String -> ByteString
field vector name = maybe (error ("missing vector field: " ++ name)) decodeHex (Map.lookup name vector)
decodeHex :: String -> ByteString
decodeHex value
| odd (length value) || any (not . isHexDigit) value = error "invalid hexadecimal vector field"
| otherwise = BS.pack (decode value)
where
decode [] = []
decode (high : low : rest) = fromIntegral (digitToInt high * 16 + digitToInt low) : decode rest
decode _ = error "unreachable"
label :: Vector -> String -> String
label vector fieldName = Map.findWithDefault "?" "count" vector ++ " " ++ fieldName
expectRight :: Either KpqCError value -> IO value
expectRight result = case result of
Right value -> pure value
Left failure -> failTest ("unexpected failure: " ++ show failure)
assertTrue :: String -> Bool -> IO ()
assertTrue message condition = if condition then pure () else failTest message
assertEqual :: (Eq value, Show value) => String -> value -> value -> IO ()
assertEqual message expected actual =
if expected == actual
then pure ()
else failTest (message ++ ": expected " ++ show expected ++ ", received " ++ show actual)
failTest :: String -> IO value
failTest message = do
putStrLn ("FAIL: " ++ message)
exitFailure