musig2-0.3.0: test/NonceSafety.hs
{-# LANGUAGE OverloadedStrings #-}
{- | One-shot secret nonces, session binding, the "Crypto.Curve.Secp256k1.MuSig2.Unsafe"
functions and 'deterministicSign' beyond the official vectors.
-}
module NonceSafety (testNonceSafety) where
import Control.Concurrent (forkIO, getNumCapabilities)
import Control.Concurrent.MVar (newEmptyMVar, putMVar, readMVar, takeMVar)
import Control.Monad (forM_, replicateM, void)
import Crypto.Curve.Secp256k1.MuSig2
import Crypto.Curve.Secp256k1.MuSig2.Internal.Nonce (peekSecNonce, secNonceIsUsed, secNoncePubNonce)
import Crypto.Curve.Secp256k1.MuSig2.Unsafe
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import Data.Either (lefts, rights)
import Data.IORef (newIORef, readIORef)
import Data.List.NonEmpty (NonEmpty (..))
import Data.Maybe (isJust)
import Data.Word (Word64)
import Test.Tasty
import Test.Tasty.HUnit
import Util (failsWith, pubNonceFromInts, secKeyFromInt, unsafeRight)
testNonceSafety :: TestTree
testNonceSafety =
testGroup
"nonce safety"
[ testGroup
"one-shot secret nonces"
[ testCase "a second sign with the same nonce returns SecNonceAlreadyUsed" secondSign
, testCase "copies of the handle share the used state" copiesShareState
, testCase "sign zero-fills the nonce's buffer" signWipes
, testCase "concurrent signing with one nonce succeeds exactly once" concurrentSign
, testCase "a failed sign (wrong key, signer not in session) consumes the nonce" failuresConsume
]
, testGroup
"session binding"
[ testCase "a nonce bound to an aggregate key refuses other sessions" bindAggPubKey
, testCase "a nonce bound to a message refuses other messages" bindMessage
, testCase "a nonce bound to the session's key and message signs" bindMatches
]
, testGroup
"Unsafe"
[ testCase "secNonceToBytes consumes the nonce" exportConsumes
, testCase "secNonceFromBytes . secNonceToBytes signs like the original" exportImport
, testCase "secNonceFromBytes validates its input" importValidates
, testCase "nonceGenWithRand rejects random bytes that are not 32 long" randLength
, testCase "nonceGenWithRand is deterministic and each result is a fresh nonce" withRandFresh
, testCase "nonceGenCounter is NonceGen with bytes(8, counter) || 0^24 and the secret key" counterIsNonceGen
, testCase "signWithoutVerification gives the same partial signature as sign" withoutVerification
]
, testGroup
"deterministicSign"
[ testCase "the last signer signs deterministically in a 3-signer session" detSignSession
, testCase "same inputs give the same output; rand, message and aggothernonce change it" detSignDeterministic
, testCase "rejects auxiliary randomness that is not 32 bytes" detSignRandLength
, testCase "rejects an aggothernonce with a half at infinity" detSignInfinity
, testCase "rejects a signer outside the key list" detSignNotInSession
]
]
-- Fixtures ---------------------------------------------------------------------
sk1, sk2, sk3 :: SecKey
sk1 = secKeyFromInt 0x1111
sk2 = secKeyFromInt 0x2222
sk3 = secKeyFromInt 0x3333
pk1, pk2, pk3 :: PubKey
pk1 = secKeyPubKey sk1
pk2 = secKeyPubKey sk2
pk3 = secKeyPubKey sk3
ctx12 :: KeyAggContext
ctx12 = unsafeRight (keyAgg (pk1 :| [pk2]))
msgA, msgB :: ByteString
msgA = "message A"
msgB = "message B"
-- | A fresh nonce for a key, from a counter (deterministic test setup).
nonceFor :: SecKey -> NonceInputs -> Word64 -> IO (SecNonce, PubNonce)
nonceFor sk inputs n = unsafeRight <$> nonceGenCounter sk n inputs
-- | Another signer's public nonce (any valid points will do).
pubNonceFor :: Integer -> PubNonce
pubNonceFor n = pubNonceFromInts (1000 + n) (2000 + n)
-- | A session for signers 1 and 2 with the given nonces.
sessionWith :: KeyAggContext -> ByteString -> PubNonce -> PubNonce -> Session
sessionWith ctx m a b = newSession ctx (nonceAgg (a :| [b])) m
failure :: Either e a -> Maybe e
failure = either Just (const Nothing)
-- | Signer 1's nonce (with the given inputs), signer 2's nonce, and their session.
setup :: NonceInputs -> KeyAggContext -> ByteString -> IO (SecNonce, Session)
setup inputs ctx m = do
(sn, pn) <- nonceFor sk1 inputs 1
pure (sn, sessionWith ctx m pn (pubNonceFor 1))
-- One-shot ----------------------------------------------------------------------
secondSign :: Assertion
secondSign = do
(sn, ses) <- setup noNonceInputs ctx12 msgA
result <- sign sn sk1 ses
assertBool "first sign succeeds" (isJust (either (const Nothing) Just result))
failsWith SecNonceAlreadyUsed (sign sn sk1 ses)
-- also with another session
(_, sesB) <- setup noNonceInputs ctx12 msgB
failsWith SecNonceAlreadyUsed (sign sn sk1 sesB)
copiesShareState :: Assertion
copiesShareState = do
(sn, ses) <- setup noNonceInputs ctx12 msgA
-- A copy kept elsewhere (in a mutable reference, in a list).
ref <- newIORef sn
let copies = [sn, sn]
copy <- readIORef ref
void (sign copy sk1 ses)
forM_ copies $ \c -> failsWith SecNonceAlreadyUsed (sign c sk1 ses)
secNonceIsUsed sn >>= assertBool "the original is marked used"
signWipes :: Assertion
signWipes = do
(sn, ses) <- setup noNonceInputs ctx12 msgA
fresh <- peekSecNonce sn
assertBool "the buffer holds the scalars before use" (BS.any (/= 0) fresh)
secNonceIsUsed sn >>= assertBool "fresh" . not
void (sign sn sk1 ses)
wiped <- peekSecNonce sn
wiped @?= BS.replicate 64 0
concurrentSign :: Assertion
concurrentSign = do
caps <- getNumCapabilities
let threads = max 16 (4 * caps)
-- Many rounds, each racing all threads on one fresh nonce.
forM_ [1 .. 50 :: Word64] $ \round' -> do
(sn, pn) <- nonceFor sk1 noNonceInputs round'
let ses = sessionWith ctx12 msgA pn (pubNonceFor (toInteger round'))
start <- newEmptyMVar
dones <- replicateM threads newEmptyMVar
forM_ dones $ \done -> forkIO $ do
readMVar start
sign sn sk1 ses >>= putMVar done
putMVar start ()
results <- traverse takeMVar dones
let ok = rights results
errs = lefts results
length ok @?= 1
errs @?= replicate (threads - 1) SecNonceAlreadyUsed
assertBool "the signature verifies" (all (partialSigVerify ses pk1 pn) ok)
failuresConsume :: Assertion
failuresConsume = do
-- SecNonceKeyMismatch
(sn, ses) <- setup noNonceInputs ctx12 msgA
failsWith SecNonceKeyMismatch (sign sn sk2 ses)
failsWith SecNonceAlreadyUsed (sign sn sk1 ses)
-- SignerNotInSession
(sn3, pn3) <- nonceFor sk3 noNonceInputs 1
let ses3 = sessionWith ctx12 msgA pn3 (pubNonceFor 1)
ctx13 = unsafeRight (keyAgg (pk1 :| [pk3]))
failsWith SignerNotInSession (sign sn3 sk3 ses3)
failsWith SecNonceAlreadyUsed (sign sn3 sk3 (sessionWith ctx13 msgA pn3 pn3))
peekSecNonce sn3 >>= (@?= BS.replicate 64 0)
-- Session binding ---------------------------------------------------------------
bindAggPubKey :: Assertion
bindAggPubKey = do
let ctx13 = unsafeRight (keyAgg (pk1 :| [pk3]))
(sn, ses) <- setup noNonceInputs{nonceAggPubKey = Just (aggXOnlyPubKey ctx13)} ctx12 msgA
failsWith SecNonceSessionMismatch (sign sn sk1 ses)
-- The failed attempt consumed it.
(_, ses13) <- setup noNonceInputs ctx13 msgA
failsWith SecNonceAlreadyUsed (sign sn sk1 ses13)
-- The untweaked key does not match a tweaked session either.
let tweaked = unsafeRight (applyTaprootTweak Nothing ctx12)
(sn', ses') <- setup noNonceInputs{nonceAggPubKey = Just (aggXOnlyPubKey ctx12)} tweaked msgA
failsWith SecNonceSessionMismatch (sign sn' sk1 ses')
bindMessage :: Assertion
bindMessage = do
(sn, ses) <- setup noNonceInputs{nonceMessage = Just msgA} ctx12 msgB
failsWith SecNonceSessionMismatch (sign sn sk1 ses)
-- An empty message is a message too.
(sn', ses') <- setup noNonceInputs{nonceMessage = Just ""} ctx12 msgA
failsWith SecNonceSessionMismatch (sign sn' sk1 ses')
bindMatches :: Assertion
bindMatches = do
let tweaked = unsafeRight (applyTaprootTweak Nothing ctx12)
inputs = noNonceInputs{nonceAggPubKey = Just (aggXOnlyPubKey tweaked), nonceMessage = Just msgA}
(sn, pn) <- nonceFor sk1 inputs 7
let ses = sessionWith tweaked msgA pn (pubNonceFor 7)
psig <- sign sn sk1 ses >>= either (assertFailure . show) pure
assertBool "partialSigVerify" (partialSigVerify ses pk1 pn psig)
-- Unsafe --------------------------------------------------------------------------
exportConsumes :: Assertion
exportConsumes = do
(sn, ses) <- setup noNonceInputs ctx12 msgA
exported <- secNonceToBytes sn
fmap BS.length exported @?= Just 97
secNonceToBytes sn >>= (@?= Nothing)
failsWith SecNonceAlreadyUsed (sign sn sk1 ses)
peekSecNonce sn >>= (@?= BS.replicate 64 0)
-- and a used nonce cannot be exported
(sn', ses') <- setup noNonceInputs ctx12 msgB
void (sign sn' sk1 ses')
secNonceToBytes sn' >>= (@?= Nothing)
exportImport :: Assertion
exportImport = do
(snA, ses) <- setup noNonceInputs ctx12 msgA
(snB, _) <- setup noNonceInputs ctx12 msgA -- same scalars, separate handle
bytes <- secNonceToBytes snA >>= maybe (assertFailure "export failed") pure
BS.drop 64 bytes @?= encode pk1
imported <- secNonceFromBytes bytes >>= either (assertFailure . show) pure
secNoncePubNonce imported @?= secNoncePubNonce snB
a <- sign imported sk1 ses
b <- sign snB sk1 ses
assertBool "both sign" (isJust (either (const Nothing) Just a))
a @?= b
importValidates :: Assertion
importValidates = do
(sn, _) <- setup noNonceInputs ctx12 msgA
bytes <- secNonceToBytes sn >>= maybe (assertFailure "export failed") pure
let err b = failure <$> secNonceFromBytes b
err (BS.take 96 bytes) >>= (@?= Just (InvalidLength 97 96))
err (BS.replicate 32 0 <> BS.drop 32 bytes) >>= (@?= Just ScalarZero)
err (BS.take 32 bytes <> BS.replicate 32 0xFF <> BS.drop 64 bytes) >>= (@?= Just ScalarOutOfRange)
err (BS.take 64 bytes <> BS.replicate 33 0) >>= (@?= Just PointAtInfinity)
err (BS.take 64 bytes <> BS.cons 0x04 (BS.drop 65 bytes)) >>= (@?= Just InvalidPoint)
randLength :: Assertion
randLength = do
failsWith (InvalidRandLength 31) (nonceGenWithRand (BS.replicate 31 1) pk1 noNonceInputs)
failsWith (InvalidRandLength 33) (nonceGenWithRand (BS.replicate 33 1) pk1 noNonceInputs)
withRandFresh :: Assertion
withRandFresh = do
let gen r inputs = unsafeRight <$> nonceGenWithRand (BS.replicate 32 r) pk1 inputs
(sn, pn) <- gen 9 noNonceInputs
(sn', pn') <- gen 9 noNonceInputs
(_, other) <- gen 8 noNonceInputs
(_, extra) <- gen 9 noNonceInputs{nonceExtraInput = Just ""}
pn' @?= pn
assertBool "other bytes give another nonce" (other /= pn)
-- BIP-327: an absent extra input is the empty one.
extra @?= pn
-- Each call is its own handle: using one does not use the other.
let ses = sessionWith ctx12 msgA pn (pubNonceFor 1)
a <- sign sn sk1 ses
failsWith SecNonceAlreadyUsed (sign sn sk1 ses)
b <- sign sn' sk1 ses
assertBool "signs" (isJust (either (const Nothing) Just a))
a @?= b
counterIsNonceGen :: Assertion
counterIsNonceGen = do
let counter = 0x0102030405060708 :: Word64
rand = BS.pack [1 .. 8] <> BS.replicate 24 0
inputs = noNonceInputs{nonceMessage = Just msgA, nonceAggPubKey = Just (aggXOnlyPubKey ctx12)}
(snC, pnC) <- unsafeRight <$> nonceGenCounter sk1 counter inputs
(snR, pnR) <- unsafeRight <$> nonceGenWithRand rand pk1 inputs{nonceSecKey = Just sk1}
pnC @?= pnR
bC <- secNonceToBytes snC
bR <- secNonceToBytes snR
bC @?= bR
-- The nonceSecKey field is ignored; the counter matters.
ignored <- nonceGenCounter sk1 counter inputs{nonceSecKey = Just sk2}
fmap snd (either (const Nothing) Just ignored) @?= Just pnC
next <- nonceGenCounter sk1 (counter + 1) inputs
assertBool "another counter gives another nonce" (fmap snd (either (const Nothing) Just next) /= Just pnC)
withoutVerification :: Assertion
withoutVerification = do
(snA, ses) <- setup noNonceInputs ctx12 msgA
(snB, _) <- setup noNonceInputs ctx12 msgA
a <- sign snA sk1 ses
b <- signWithoutVerification snB sk1 ses
assertBool "signs" (isJust (either (const Nothing) Just a))
a @?= b
failsWith SecNonceAlreadyUsed (signWithoutVerification snB sk1 ses)
-- deterministicSign ----------------------------------------------------------------
ctx123 :: KeyAggContext
ctx123 = unsafeRight (keyAgg (pk1 :| [pk2, pk3]) >>= applyTaprootTweak (Just (BS.replicate 32 0xAB)))
detSignSession :: Assertion
detSignSession = do
let m = BS.replicate 32 0x42
-- Signers 1 and 2 use nonceGen and send their public nonces first.
let gen sk = nonceGen (secKeyPubKey sk) noNonceInputs{nonceSecKey = Just sk} >>= either (assertFailure . show) pure
(sn1, pn1) <- gen sk1
(sn2, pn2) <- gen sk2
let aggOther = nonceAgg (pn1 :| [pn2])
-- Signer 3 signs deterministically, last.
(pn3, psig3) <- either (assertFailure . show) pure (deterministicSign sk3 aggOther ctx123 m Nothing)
let ses = newSession ctx123 (nonceAgg (pn1 :| [pn2, pn3])) m
psig1 <- sign sn1 sk1 ses >>= either (assertFailure . show) pure
psig2 <- sign sn2 sk2 ses >>= either (assertFailure . show) pure
assertBool "partialSigVerify of the deterministic signer" (partialSigVerify ses pk3 pn3 psig3)
assertBool "the signature verifies" (verify (aggXOnlyPubKey ctx123) m (partialSigAgg ses (psig1 :| [psig2, psig3])))
detSignDeterministic :: Assertion
detSignDeterministic = do
let aggOther = nonceAgg (pubNonceFor 3 :| [pubNonceFor 4])
aggOther' = nonceAgg (pubNonceFor 3 :| [pubNonceFor 5])
run a m r = unsafeRight (deterministicSign sk3 a ctx123 m r)
base = run aggOther msgA Nothing
run aggOther msgA Nothing @?= base
run aggOther msgA (Just (BS.replicate 32 1)) @?= run aggOther msgA (Just (BS.replicate 32 1))
let differs what x = assertBool what (fst x /= fst base && snd x /= snd base)
differs "rand" (run aggOther msgA (Just (BS.replicate 32 0)))
differs "message" (run aggOther msgB Nothing)
differs "aggothernonce" (run aggOther' msgA Nothing)
detSignRandLength :: Assertion
detSignRandLength = do
let aggOther = nonceAgg (pubNonceFor 3 :| [])
failure (deterministicSign sk3 aggOther ctx123 msgA (Just (BS.replicate 31 0))) @?= Just (InvalidAuxRandLength 31)
failure (deterministicSign sk3 aggOther ctx123 msgA (Just BS.empty)) @?= Just (InvalidAuxRandLength 0)
detSignInfinity :: Assertion
detSignInfinity = do
let pn = pubNonceFor 3
half = BS.take 33 (encode pn)
agg b = unsafeRight (decode b) :: AggNonce
failure (deterministicSign sk3 (agg (BS.replicate 33 0 <> half)) ctx123 msgA Nothing) @?= Just AggOtherNonceAtInfinity
failure (deterministicSign sk3 (agg (half <> BS.replicate 33 0)) ctx123 msgA Nothing) @?= Just AggOtherNonceAtInfinity
detSignNotInSession :: Assertion
detSignNotInSession = do
let aggOther = nonceAgg (pubNonceFor 3 :| [])
failure (deterministicSign sk3 aggOther ctx12 msgA Nothing) @?= Just SignerNotInSession