crypton-2.1.8: tests/PubKey/MLDSASpec.hs
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- | ML-DSA against NIST's ACVP vectors, and against itself.
--
-- The vectors are the point. A signature this module makes and then verifies
-- says only that its two halves agree; the vectors say the signature is the
-- one FIPS 204 asks for, which is what a peer will check it against.
module PubKey.MLDSASpec (spec) where
import qualified Data.ByteArray as B
import Data.ByteArray.Encoding (Base (Base16), convertFromBase)
import qualified Data.ByteString as BS
import Data.Proxy (Proxy (..))
import Control.Monad (forM_, when)
import Data.List (nub)
import Test.Hspec hiding (context)
import Crypto.Error
import Crypto.PubKey.MLDSA
import Imports ()
import PubKey.MLDSAVectors
hex :: String -> BS.ByteString
hex s = case convertFromBase Base16 (BS.pack (map (fromIntegral . fromEnum) s)) of
Left e -> error ("bad hex in a test vector: " ++ e)
Right b -> b
withSet :: String -> (forall p. MLDSA p => Proxy p -> r) -> r
withSet "ML-DSA-44" k = k (Proxy :: Proxy MLDSA44)
withSet "ML-DSA-65" k = k (Proxy :: Proxy MLDSA65)
withSet "ML-DSA-87" k = k (Proxy :: Proxy MLDSA87)
withSet s _ = error ("unknown parameter set in a test vector: " ++ s)
ctxOf :: String -> Context
ctxOf "" = emptyContext
ctxOf s = case context (hex s) of
CryptoPassed c -> c
CryptoFailed e -> error (show e)
spec :: Spec
spec = do
describe "ACVP keyGen" $
mapM_ keyGenCase keyGenVectors
describe "ACVP sigGen" $
mapM_ sigGenCase sigGenVectors
describe "what a signature is bound to" $
mapM_ bindingCase sigGenVectors
describe "ACVP sigGen, the external-mu interface" $
mapM_ extMuCase extMuVectors
describe "the message representative" $
mapM_ muCase sigGenVectors
describe "the message representative, a piece at a time" $
mapM_ muStreamCase sigGenVectors
describe "the seed a key pair came from" $ do
seedKeeps "ML-DSA-44" (Proxy :: Proxy MLDSA44)
seedKeeps "ML-DSA-65" (Proxy :: Proxy MLDSA65)
seedKeeps "ML-DSA-87" (Proxy :: Proxy MLDSA87)
describe "round trip" $ do
roundTrip "ML-DSA-44" (Proxy :: Proxy MLDSA44)
roundTrip "ML-DSA-65" (Proxy :: Proxy MLDSA65)
roundTrip "ML-DSA-87" (Proxy :: Proxy MLDSA87)
keyGenCase :: KeyGenVector -> Spec
keyGenCase v =
it (kgSet v ++ " tcId " ++ show (kgId v)) $
withSet (kgSet v) $ \p ->
case keyPairFromSeed p (hex (kgSeed v)) of
CryptoFailed e -> expectationFailure (show e)
CryptoPassed (vk, sk) -> do
B.convert vk `shouldBe` hex (kgPk v)
B.convert sk `shouldBe` hex (kgSk v)
sigGenCase :: SigGenVector -> Spec
sigGenCase v =
it (label v) $
withSet (sgSet v) $ \(_ :: Proxy p) ->
case signingKey (hex (sgSk v)) :: CryptoFailable (SigningKey p) of
CryptoFailed e -> expectationFailure (show e)
CryptoPassed sk ->
let ctx = ctxOf (sgContext v)
msg = hex (sgMessage v)
got
| sgDeterministic v =
CryptoPassed (signDeterministic sk ctx msg)
| otherwise = signWith sk ctx msg (hex (sgRnd v))
in case got of
CryptoFailed e -> expectationFailure (show e)
CryptoPassed s ->
B.convert s `shouldBe` hex (sgSignature v)
-- | The vector's own signature, verified -- and then the three ways it should
-- stop verifying. Signing and verifying with this module alone could agree
-- on a wrong domain prefix; this pins the prefix to the vector's signature
-- and then shows the context is really part of it.
bindingCase :: SigGenVector -> Spec
bindingCase v =
it (label v) $
withSet (sgSet v) $ \(_ :: Proxy p) ->
case ( signingKey (hex (sgSk v)) :: CryptoFailable (SigningKey p)
, signature (hex (sgSignature v)) :: CryptoFailable (Signature p)
) of
(CryptoPassed sk, CryptoPassed sig) -> do
let vk = toPublic sk
ctx = ctxOf (sgContext v)
msg = hex (sgMessage v)
verify vk ctx msg sig `shouldBe` True
verify vk ctx (flipFirst msg) sig `shouldBe` False
verify vk (otherContext (sgContext v)) msg sig `shouldBe` False
case signature (flipFirst (hex (sgSignature v))) of
CryptoPassed bad -> verify vk ctx msg bad `shouldBe` False
CryptoFailed e -> expectationFailure (show e)
(CryptoFailed e, _) -> expectationFailure (show e)
(_, CryptoFailed e) -> expectationFailure (show e)
where
flipFirst b
| BS.null b = BS.singleton 1
| otherwise = BS.cons (BS.head b `seq` BS.head b + 1) (BS.tail b)
-- any context other than the one it was signed under
otherContext "" = ctxOf "00"
otherContext _ = emptyContext
-- Signing a representative the vector supplies.
extMuCase :: ExtMuVector -> Spec
extMuCase v =
it (xmSet v ++ " tcId " ++ show (xmId v) ++ det) $
withSet (xmSet v) $ \(_ :: Proxy p) ->
case ( signingKey (hex (xmSk v)) :: CryptoFailable (SigningKey p)
, mu (hex (xmMu v))
) of
(CryptoPassed sk, CryptoPassed m) -> do
let got
| xmDeterministic v =
CryptoPassed (signExternalMuDeterministic sk m)
| otherwise = signExternalMuWith sk m (hex (xmRnd v))
case got of
CryptoFailed e -> expectationFailure (show e)
CryptoPassed sig -> do
B.convert sig `shouldBe` hex (xmSignature v)
verifyExternalMu (toPublic sk) m sig `shouldBe` True
(CryptoFailed e, _) -> expectationFailure (show e)
(_, CryptoFailed e) -> expectationFailure (show e)
where
det = if xmDeterministic v then ", deterministic" else ", hedged"
-- messageRepresentative, against a vector that never mentions mu.
--
-- The vectors for the external-mu interface supply the representative, so
-- using them would only say that signing it works, not that this computes
-- the right one. Taking a vector from the ordinary interface and computing
-- the representative from its key, context and message does say that: the
-- signature has to come out the same as the one the vector gives for
-- signing that message directly.
muCase :: SigGenVector -> Spec
muCase v =
it (label v) $
withSet (sgSet v) $ \(_ :: Proxy p) ->
case signingKey (hex (sgSk v)) :: CryptoFailable (SigningKey p) of
CryptoFailed e -> expectationFailure (show e)
CryptoPassed sk -> do
let vk = toPublic sk
ctx = ctxOf (sgContext v)
msg = hex (sgMessage v)
m = messageRepresentative vk ctx msg
B.length m `shouldBe` muSize
let viaMu = B.convert (signExternalMuDeterministic sk m)
direct = B.convert (signDeterministic sk ctx msg)
(viaMu :: BS.ByteString) `shouldBe` direct
-- and for the deterministic vectors it is the
-- signature the vector itself gives
when (sgDeterministic v) $
viaMu `shouldBe` hex (sgSignature v)
-- The streaming form, against the same vectors.
--
-- Where the message is cut must not matter, so every cut is tried: none,
-- at the front, at the back, at thirds, and one byte at a time. All of
-- them have to give what 'messageRepresentative' gives for the whole
-- message, and -- for the deterministic vectors -- signing that has to
-- give the signature the vector itself holds. Without that last step the
-- test would only say two of this module's paths agree with each other.
muStreamCase :: SigGenVector -> Spec
muStreamCase v =
it (label v) $
withSet (sgSet v) $ \(_ :: Proxy p) ->
case signingKey (hex (sgSk v)) :: CryptoFailable (SigningKey p) of
CryptoFailed e -> expectationFailure (show e)
CryptoPassed sk -> do
let vk = toPublic sk
ctx = ctxOf (sgContext v)
msg = hex (sgMessage v)
whole = B.convert (messageRepresentative vk ctx msg)
n = BS.length msg
cuts = nub [0, 1, n `div` 3, n `div` 2, n - 1, n]
chunked :: [BS.ByteString] -> BS.ByteString
chunked cs =
B.convert (muFinalize (muUpdates (muInit vk ctx) cs))
forM_ (filter (\i -> i >= 0 && i <= n) cuts) $ \i ->
let (a, b) = BS.splitAt i msg
in chunked [a, b] `shouldBe` (whole :: BS.ByteString)
chunked (map BS.singleton (BS.unpack msg))
`shouldBe` (whole :: BS.ByteString)
-- an empty piece is not a piece
chunked [BS.empty, msg, BS.empty]
`shouldBe` (whole :: BS.ByteString)
let streamed =
muFinalize $
muUpdate (muUpdate (muInit vk ctx) (BS.take 1 msg)) $
BS.drop 1 msg
when (sgDeterministic v) $
(B.convert (signExternalMuDeterministic sk streamed) :: BS.ByteString)
`shouldBe` hex (sgSignature v)
-- The seed generateKeyPairAndSeed hands back has to be the one the pair
-- was derived from: expanding it again has to give that very pair, not
-- merely some pair. Two generated pairs also have to differ.
seedKeeps :: MLDSA p => String -> Proxy p -> Spec
seedKeeps name p =
it (name ++ ": the seed comes back and rebuilds the pair") $ do
(vk, sk, seed) <- generateKeyPairAndSeed p
B.length seed `shouldBe` seedSize
case keyPairFromSeed p seed of
CryptoPassed (vk', sk') -> do
(B.convert vk' :: BS.ByteString) `shouldBe` B.convert vk
(B.convert sk' :: BS.ByteString) `shouldBe` B.convert sk
CryptoFailed e -> expectationFailure (show e)
(vk2, _, _) <- generateKeyPairAndSeed p
(B.convert vk2 :: BS.ByteString) `shouldNotBe` B.convert vk
roundTrip :: MLDSA p => String -> Proxy p -> Spec
roundTrip name p =
it (name ++ ": a signature this module makes, it verifies") $
case keyPairFromSeed p (BS.replicate 32 5) of
CryptoFailed e -> expectationFailure (show e)
CryptoPassed (vk, sk) -> do
let msg = BS.pack [1 .. 40]
ctx = ctxOf "aabb"
sig = signDeterministic sk ctx msg
verify vk ctx msg sig `shouldBe` True
toPublic sk `shouldBe` vk
-- the same key and message twice give the same signature
signDeterministic sk ctx msg `shouldBe` sig
label :: SigGenVector -> String
label v =
sgSet v
++ " tcId "
++ show (sgId v)
++ (if sgDeterministic v then ", deterministic" else ", hedged")
++ (if null (sgContext v) then ", no context" else ", with a context")