packages feed

KpqC-0.1.0: test/Spec.hs

module Main (main) where

import Control.Monad (forM_, unless)
import Crypto.KpqC
import Data.Bits (xor)
import qualified Data.ByteArray as BA
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BSC
import System.Exit (exitFailure)

main :: IO ()
main = do
  assertEqual "signature algorithm count" 9 (length signatureAlgorithms)
  assertEqual "KEM algorithm count" 7 (length kemAlgorithms)
  forM_ signatureAlgorithms checkSignature
  forM_ kemAlgorithms checkKEM
  checkValidation
  putStrLn "All KpqC tests passed."

checkSignature :: SignatureAlgorithm -> IO ()
checkSignature algorithm = do
  keys <- expectRight (generateKeyPair algorithm)
  let sizes = signatureSizes algorithm
      message = BSC.pack "KpqC Haskell"
      context = BSC.pack "release"
  assertEqual "signature public-key size" (signaturePublicKeyBytes sizes) (BS.length (publicKey keys))
  assertEqual "signature secret-key size" (signatureSecretKeyBytes sizes) (BA.length (secretKey keys))
  assertEqual
    "key-pair display redacts the secret key"
    ("KeyPair {publicKey = <" ++ show (signaturePublicKeyBytes sizes) ++ " bytes>, secretKey = <redacted>}")
    (show keys)
  signature <- expectRight (signWithContext algorithm message (secretKey keys) context)
  assertEqual "signature size" (signatureBytes sizes) (BS.length signature)
  expectRight (verifyWithContext algorithm message signature (publicKey keys) context) >>= assertTrue "valid signature"
  expectRight (verifyWithContext algorithm message (flipFirst signature) (publicKey keys) context) >>= assertFalse "modified signature"
  expectRight (verifyWithContext algorithm (BS.snoc message 33) signature (publicKey keys) context) >>= assertFalse "modified message"
  expectRight (verifyWithContext algorithm message signature (publicKey keys) (BSC.pack "wrong")) >>= assertFalse "wrong context"
  expectRight (verify algorithm message (BS.singleton 0) (publicKey keys)) >>= assertFalse "wrong signature length"

checkKEM :: KEMAlgorithm -> IO ()
checkKEM algorithm = do
  keys <- expectRight (generateKeyPair algorithm)
  let sizes = kemSizes algorithm
  assertEqual "KEM public-key size" (kemPublicKeyBytes sizes) (BS.length (publicKey keys))
  assertEqual "KEM secret-key size" (kemSecretKeyBytes sizes) (BA.length (secretKey keys))
  outbound <- expectRight (encapsulate algorithm (publicKey keys))
  assertEqual "ciphertext size" (ciphertextBytes sizes) (BS.length (ciphertext outbound))
  assertEqual "shared-secret size" (sharedSecretBytes sizes) (BA.length (sharedSecret outbound))
  assertEqual
    "encapsulation display redacts the shared secret"
    ("EncapsulatedSecret {ciphertext = <" ++ show (ciphertextBytes sizes) ++ " bytes>, sharedSecret = <redacted>}")
    (show outbound)
  inbound <- expectRight (decapsulate algorithm (ciphertext outbound) (secretKey keys))
  assertEqual "shared secrets" (sharedSecret outbound) inbound
  rejection <- decapsulate algorithm (flipFirst (ciphertext outbound)) (secretKey keys)
  case algorithm of
    NTRUPlus768 -> assertNativeFailure rejection
    NTRUPlus864 -> assertNativeFailure rejection
    NTRUPlus1152 -> assertNativeFailure rejection
    _ -> do
      replacement <- expectEitherRight rejection
      assertEqual "replacement-secret size" (sharedSecretBytes sizes) (BA.length replacement)
      assertTrue "implicit rejection" (replacement /= sharedSecret outbound)

checkValidation :: IO ()
checkValidation = do
  keys <- expectRight (generateKeyPair AIMer128f)
  invalidSecret <- sign AIMer128f BS.empty (BS.singleton 0)
  assertEqual "invalid secret key" (Left (InvalidSize "secret key" 48 1)) invalidSecret
  longContext <- signWithContext AIMer128f BS.empty (secretKey keys) (BS.replicate 256 0)
  assertEqual "long context" (Left (ContextTooLong 256)) longContext
  invalidPublic <- verify AIMer128f BS.empty BS.empty (BS.singleton 0)
  assertEqual "invalid public key" (Left (InvalidSize "public key" 32 1)) invalidPublic
  invalidCiphertext <- decapsulate NTRUPlus768 (BS.singleton 0) (BS.replicate 2336 0)
  assertEqual "invalid ciphertext" (Left (InvalidSize "ciphertext" 1152 1)) invalidCiphertext

flipFirst :: ByteString -> ByteString
flipFirst input = case BS.uncons input of
  Nothing -> input
  Just (first, rest) -> BS.cons (first `xor` 1) rest

assertNativeFailure :: Either KpqCError SecretBytes -> IO ()
assertNativeFailure result = case result of
  Left (NativeFailure Decapsulation _) -> pure ()
  _ -> failTest ("expected native decapsulation failure, received " ++ show result)

expectRight :: IO (Either KpqCError value) -> IO value
expectRight operation = operation >>= expectEitherRight

expectEitherRight :: Either KpqCError value -> IO value
expectEitherRight result = case result of
  Right value -> pure value
  Left failure -> failTest ("unexpected failure: " ++ show failure)

assertTrue :: String -> Bool -> IO ()
assertTrue label value = unless value (failTest (label ++ ": expected True"))

assertFalse :: String -> Bool -> IO ()
assertFalse label = assertTrue label . not

assertEqual :: (Eq value, Show value) => String -> value -> value -> IO ()
assertEqual label expected actual =
  unless (expected == actual) $
    failTest (label ++ ": expected " ++ show expected ++ ", received " ++ show actual)

failTest :: String -> IO value
failTest message = do
  putStrLn ("FAIL: " ++ message)
  exitFailure