musig2 0.1.4 → 0.2.0
raw patch · 18 files changed
+707/−627 lines, 18 filesdep ~base16-bytestringdep ~ppad-hmac-drbgdep ~ppad-secp256k1PVP ok
version bump matches the API change (PVP)
Dependency ranges changed: base16-bytestring, ppad-hmac-drbg, ppad-secp256k1, ppad-sha256
API changes (from Hackage documentation)
- Crypto.Curve.Secp256k1.MuSig2: SecNonce :: !Integer -> !Integer -> SecNonce
- Crypto.Curve.Secp256k1.MuSig2: [k1] :: SecNonce -> !Integer
- Crypto.Curve.Secp256k1.MuSig2: [k2] :: SecNonce -> !Integer
- Crypto.Curve.Secp256k1.MuSig2: instance GHC.Classes.Eq Crypto.Curve.Secp256k1.MuSig2.SecNonce
- Crypto.Curve.Secp256k1.MuSig2: instance GHC.Classes.Ord Crypto.Curve.Secp256k1.MuSig2.SecNonce
- Crypto.Curve.Secp256k1.MuSig2: instance GHC.Generics.Generic Crypto.Curve.Secp256k1.MuSig2.SecNonce
- Crypto.Curve.Secp256k1.MuSig2: instance GHC.Read.Read Crypto.Curve.Secp256k1.MuSig2.SecNonce
+ Crypto.Curve.Secp256k1.MuSig2: AggregatedPublicKeyAtInfinity :: MuSig2Error
+ Crypto.Curve.Secp256k1.MuSig2: EmptyNonceCollection :: MuSig2Error
+ Crypto.Curve.Secp256k1.MuSig2: EmptyPartialSignatureCollection :: MuSig2Error
+ Crypto.Curve.Secp256k1.MuSig2: EmptyPublicKeyCollection :: MuSig2Error
+ Crypto.Curve.Secp256k1.MuSig2: InvalidPointFormat :: MuSig2Error
+ Crypto.Curve.Secp256k1.MuSig2: InvalidRandomBytesLength :: Int -> MuSig2Error
+ Crypto.Curve.Secp256k1.MuSig2: InvalidSignerIndex :: Int -> MuSig2Error
+ Crypto.Curve.Secp256k1.MuSig2: KeyDerivationFailed :: MuSig2Error
+ Crypto.Curve.Secp256k1.MuSig2: NegativeTweak :: Integer -> MuSig2Error
+ Crypto.Curve.Secp256k1.MuSig2: PartialSignatureOutOfRange :: MuSig2Error
+ Crypto.Curve.Secp256k1.MuSig2: PublicKeyAtInfinity :: MuSig2Error
+ Crypto.Curve.Secp256k1.MuSig2: PublicNonceGenerationFailed :: String -> MuSig2Error
+ Crypto.Curve.Secp256k1.MuSig2: ScalarMultiplicationFailed :: String -> MuSig2Error
+ Crypto.Curve.Secp256k1.MuSig2: SecretKeyPublicKeyMismatch :: MuSig2Error
+ Crypto.Curve.Secp256k1.MuSig2: SecretScalarOutOfRange :: String -> MuSig2Error
+ Crypto.Curve.Secp256k1.MuSig2: SecretScalarZero :: String -> MuSig2Error
+ Crypto.Curve.Secp256k1.MuSig2: SignerCountMismatch :: Int -> Int -> MuSig2Error
+ Crypto.Curve.Secp256k1.MuSig2: TooManyPublicKeys :: MuSig2Error
+ Crypto.Curve.Secp256k1.MuSig2: TooManyTweaks :: MuSig2Error
+ Crypto.Curve.Secp256k1.MuSig2: TweakOutOfRange :: Integer -> MuSig2Error
+ Crypto.Curve.Secp256k1.MuSig2: TweakResultAtInfinity :: MuSig2Error
+ Crypto.Curve.Secp256k1.MuSig2: ZeroNonceGenerated :: MuSig2Error
+ Crypto.Curve.Secp256k1.MuSig2: data MuSig2Error
+ Crypto.Curve.Secp256k1.MuSig2: instance GHC.Classes.Eq Crypto.Curve.Secp256k1.MuSig2.MuSig2Error
+ Crypto.Curve.Secp256k1.MuSig2: instance GHC.Show.Show Crypto.Curve.Secp256k1.MuSig2.MuSig2Error
- Crypto.Curve.Secp256k1.MuSig2: aggNonces :: Traversable t => t PubNonce -> Maybe PubNonce
+ Crypto.Curve.Secp256k1.MuSig2: aggNonces :: Traversable t => t PubNonce -> Either MuSig2Error PubNonce
- Crypto.Curve.Secp256k1.MuSig2: aggPartials :: Traversable t => t PartialSignature -> SessionContext -> ByteString
+ Crypto.Curve.Secp256k1.MuSig2: aggPartials :: Traversable t => t PartialSignature -> SessionContext -> Either MuSig2Error ByteString
- Crypto.Curve.Secp256k1.MuSig2: applyTweak :: KeyAggContext -> Tweak -> KeyAggContext
+ Crypto.Curve.Secp256k1.MuSig2: applyTweak :: KeyAggContext -> Tweak -> Either MuSig2Error KeyAggContext
- Crypto.Curve.Secp256k1.MuSig2: mkKeyAggContext :: Traversable t => t Pub -> Maybe Tweak -> KeyAggContext
+ Crypto.Curve.Secp256k1.MuSig2: mkKeyAggContext :: Traversable t => t Pub -> Maybe Tweak -> Either MuSig2Error KeyAggContext
- Crypto.Curve.Secp256k1.MuSig2: mkSecNonce :: IO SecNonce
+ Crypto.Curve.Secp256k1.MuSig2: mkSecNonce :: Pub -> Integer -> Integer -> Either MuSig2Error SecNonce
- Crypto.Curve.Secp256k1.MuSig2: mkSessionContext :: Traversable t => PubNonce -> t Pub -> t Tweak -> ByteString -> SessionContext
+ Crypto.Curve.Secp256k1.MuSig2: mkSessionContext :: Traversable t => PubNonce -> t Pub -> t Tweak -> ByteString -> Either MuSig2Error SessionContext
- Crypto.Curve.Secp256k1.MuSig2: partialSigVerify :: Traversable t => PartialSignature -> t PubNonce -> t Pub -> t Tweak -> ByteString -> Int -> Bool
+ Crypto.Curve.Secp256k1.MuSig2: partialSigVerify :: Traversable t => PartialSignature -> t PubNonce -> t Pub -> t Tweak -> ByteString -> Int -> Either MuSig2Error Bool
- Crypto.Curve.Secp256k1.MuSig2: publicNonce :: SecNonce -> PubNonce
+ Crypto.Curve.Secp256k1.MuSig2: publicNonce :: SecNonce -> Either MuSig2Error PubNonce
- Crypto.Curve.Secp256k1.MuSig2: secNonceGen :: SecNonceGenParams -> IO SecNonce
+ Crypto.Curve.Secp256k1.MuSig2: secNonceGen :: SecNonceGenParams -> IO (Either MuSig2Error SecNonce)
- Crypto.Curve.Secp256k1.MuSig2: secNonceGenWithRand :: ByteString -> SecNonceGenParams -> SecNonce
+ Crypto.Curve.Secp256k1.MuSig2: secNonceGenWithRand :: ByteString -> SecNonceGenParams -> Either MuSig2Error SecNonce
- Crypto.Curve.Secp256k1.MuSig2: sign :: SecNonce -> SecKey -> SessionContext -> PartialSignature
+ Crypto.Curve.Secp256k1.MuSig2: sign :: SecNonce -> SecKey -> SessionContext -> Either MuSig2Error PartialSignature
Files
- CHANGELOG +15/−0
- bench/Main.hs +26/−29
- lib/Crypto/Curve/Secp256k1/MuSig2.hs +310/−226
- lib/Crypto/Curve/Secp256k1/MuSig2/Internal.hs +25/−5
- musig2.cabal +10/−10
- test/AggNonces.hs +2/−3
- test/AggPartials.hs +9/−7
- test/AggPartialsProperty.hs +41/−49
- test/AggPubkeys.hs +13/−19
- test/ApplyTweaks.hs +8/−8
- test/NonceGen.hs +8/−6
- test/NonceGenProperty.hs +18/−13
- test/ParityPub.hs +3/−3
- test/SignVerify.hs +42/−59
- test/SignVerifyProperty.hs +36/−43
- test/SignVerifyTweakProperty.hs +52/−77
- test/Tweak.hs +34/−59
- test/Util.hs +55/−11
CHANGELOG view
@@ -1,5 +1,20 @@ # Changelog +## 0.2.0 (2026-04-10)++* Security hardening and API changes:+ - Bind `SecNonce` values to the originating signer public key and reject nonce/key mismatches during signing+ - Replace panic-based validation in signing, verification, context creation, tweaking, and nonce generation with structured `MuSig2Error` results+ - Eliminate all remaining `error` calls from library code (`publicNonce`, `getSigningNonce`, `isEvenPub`)+ - Make `SecNonce` opaque and require checked construction via `mkSecNonce` or the nonce generation APIs+ - Remove `Eq` and `Ord` instances from `SecNonce` to discourage storing nonces in collections+ - Move `secNonceScalars` out of public API into `Internal` module to prevent leaking secret nonce scalars+ - Strip secret scalar values from `MuSig2Error` constructors to prevent leaking secrets through error logging+ - Reject out-of-range partial signatures during `aggPartials` instead of reducing them modulo the curve order+ - Update nonce generation and session construction APIs to return checked `Either` results+ - Add security considerations documentation (nonce reuse, non-constant-time arithmetic, no zeroization)+ - Refresh tests, benchmarks, and documentation to cover the new checked API and regression cases+ ## 0.1.3 (2026-01-04) * Performance improvements:
bench/Main.hs view
@@ -13,7 +13,7 @@ KeyAggContext, PubNonce (..), SecKey (..),- SecNonce (..),+ SecNonce, SecNonceGenParams (..), SessionContext (..), Tweak (..),@@ -22,6 +22,10 @@ import qualified Data.ByteString as BS import qualified Data.ByteString.Base16 as B16 +-- | Unwrap a Right value, crashing on Left (bench setup only).+unsafeRight :: (Show e) => Either e a -> a+unsafeRight = either (error . show) id+ -- NFData instances for benchmarking instance NFData S.Projective instance NFData S.Affine@@ -32,7 +36,7 @@ rnf (SecKey i) = rnf i instance NFData SecNonce where- rnf (SecNonce k1 k2) = rnf k1 `seq` rnf k2+ rnf sn = sn `seq` () instance NFData PubNonce where rnf (PubNonce r1 r2) = rnf r1 `seq` rnf r2@@ -96,8 +100,7 @@ nonceGen = env setupNonce $ \ ~(params, rand) -> bgroup "nonce_generation"- [ bench "mkSecNonce" $ nfIO M.mkSecNonce- , bench "secNonceGen (minimal params)" $ nfIO (M.secNonceGen params)+ [ bench "secNonceGen (minimal params)" $ nfIO (M.secNonceGen params) , bench "secNonceGenWithRand" $ nf (M.secNonceGenWithRand rand) params , bench "publicNonce" $ nf M.publicNonce secNonce1 ]@@ -127,9 +130,7 @@ ] where setupSession = do- let !aggNonce = case M.aggNonces [pubNonce1, pubNonce2] of- Nothing -> error "failed to aggregate nonces"- Just n -> n+ let !aggNonce = unsafeRight $ M.aggNonces [pubNonce1, pubNonce2] !pks = [p, q] !tweaks = [plainTweak, xonlyTweak] pure (aggNonce, pks, tweaks)@@ -143,11 +144,9 @@ ] where setupSigning = do- let !aggNonce = case M.aggNonces [pubNonce1, pubNonce2] of- Nothing -> error "failed to aggregate nonces"- Just n -> n- !ctx = M.mkSessionContext aggNonce [p, q] [] sMsg- !secNonce = SecNonce sk1 sk2+ let !aggNonce = unsafeRight $ M.aggNonces [pubNonce1, pubNonce2]+ !ctx = unsafeRight $ M.mkSessionContext aggNonce [p, q] [] sMsg+ !secNonce = secNonce1 !sk = SecKey ssk pure (ctx, secNonce, sk) @@ -161,13 +160,11 @@ ] where setupVerification = do- let !aggNonce = case M.aggNonces [pubNonce1, pubNonce2] of- Nothing -> error "failed to aggregate nonces"- Just n -> n- !ctx = M.mkSessionContext aggNonce [p, q] [] sMsg- !secNonce = SecNonce sk1 sk2+ let !aggNonce = unsafeRight $ M.aggNonces [pubNonce1, pubNonce2]+ !ctx = unsafeRight $ M.mkSessionContext aggNonce [p, q] [] sMsg+ !secNonce = secNonce1 !sk = SecKey ssk- !partial = M.sign secNonce sk ctx+ !partial = unsafeRight $ M.sign secNonce sk ctx !nonces = [pubNonce1, pubNonce2] !pks = [p, q] pure (partial, nonces, pks, ctx)@@ -238,7 +235,7 @@ -- Contexts and nonces for testing keyCtx2 :: KeyAggContext-keyCtx2 = M.mkKeyAggContext [p, q] Nothing+keyCtx2 = unsafeRight $ M.mkKeyAggContext [p, q] Nothing plainTweak :: Tweak plainTweak = PlainTweak 0x1234567890ABCDEF@@ -247,31 +244,31 @@ xonlyTweak = XOnlyTweak 0xFEDCBA0987654321 secNonce1 :: SecNonce-secNonce1 = SecNonce sk1 sk2+secNonce1 = unsafeRight $ M.mkSecNonce p sk1 sk2 pubNonce1 :: PubNonce-pubNonce1 = M.publicNonce secNonce1+pubNonce1 = unsafeRight $ M.publicNonce secNonce1 secNonce2 :: SecNonce-secNonce2 = SecNonce (sk1 + 1) (sk2 + 1)+secNonce2 = unsafeRight $ M.mkSecNonce p (sk1 + 1) (sk2 + 1) pubNonce2 :: PubNonce-pubNonce2 = M.publicNonce secNonce2+pubNonce2 = unsafeRight $ M.publicNonce secNonce2 secNonce3 :: SecNonce-secNonce3 = SecNonce (sk1 + 2) (sk2 + 2)+secNonce3 = unsafeRight $ M.mkSecNonce p (sk1 + 2) (sk2 + 2) pubNonce3 :: PubNonce-pubNonce3 = M.publicNonce secNonce3+pubNonce3 = unsafeRight $ M.publicNonce secNonce3 secNonce4 :: SecNonce-secNonce4 = SecNonce (sk1 + 3) (sk2 + 3)+secNonce4 = unsafeRight $ M.mkSecNonce p (sk1 + 3) (sk2 + 3) pubNonce4 :: PubNonce-pubNonce4 = M.publicNonce secNonce4+pubNonce4 = unsafeRight $ M.publicNonce secNonce4 secNonce5 :: SecNonce-secNonce5 = SecNonce (sk1 + 4) (sk2 + 4)+secNonce5 = unsafeRight $ M.mkSecNonce p (sk1 + 4) (sk2 + 4) pubNonce5 :: PubNonce-pubNonce5 = M.publicNonce secNonce5+pubNonce5 = unsafeRight $ M.publicNonce secNonce5
lib/Crypto/Curve/Secp256k1/MuSig2.hs view
@@ -19,6 +19,24 @@ [MuSig2](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki) (partial)signatures with tweak support on the elliptic curve secp256k1. +== Security Considerations++* __Nonce single-use:__ Each 'SecNonce' /must/ be used to 'sign' at most one+ message. Reusing a nonce across two different messages enables algebraic+ recovery of the signer's private key. This library does not enforce+ single-use semantics at the type level; callers must ensure fresh nonces+ per signing session.++* __Non-constant-time arithmetic:__ This library uses Haskell's arbitrary-precision+ 'Integer' type for all scalar operations. Branching and comparison on secret+ values (e.g., key negation during signing) is not constant-time. For+ applications requiring side-channel resistance, consider a C FFI binding+ such as @libsecp256k1-musig@.++* __No secret zeroization:__ Secret key material ('SecKey', 'SecNonce') is not+ zeroized after use due to Haskell's garbage-collected runtime. Secrets may+ persist in memory until collected.+ == Usage A sample GHCi session:@@ -43,7 +61,7 @@ > let pubkeys = [pub1, pub2] > > -- create key aggregation context-> let keyagg_ctx = MuSig2.mkKeyAggContext pubkeys Nothing+> let Right keyagg_ctx = MuSig2.mkKeyAggContext pubkeys Nothing > let agg_pk = MuSig2.aggregatedPubkey keyagg_ctx > > -- message to sign@@ -52,23 +70,23 @@ > -- generate nonces for each signer > let params1 = MuSig2.defaultSecNonceGenParams pub1 > let params2 = MuSig2.defaultSecNonceGenParams pub2-> secnonce1 <- MuSig2.secNonceGen params1-> secnonce2 <- MuSig2.secNonceGen params2-> let pubnonce1 = MuSig2.publicNonce secnonce1-> let pubnonce2 = MuSig2.publicNonce secnonce2+> Right secnonce1 <- MuSig2.secNonceGen params1+> Right secnonce2 <- MuSig2.secNonceGen params2+> let Right pubnonce1 = MuSig2.publicNonce secnonce1+> let Right pubnonce2 = MuSig2.publicNonce secnonce2 > let pubnonces = [pubnonce1, pubnonce2] > > -- aggregate nonces and create session context-> let Just aggnonce = MuSig2.aggNonces pubnonces-> let session_ctx = MuSig2.mkSessionContext aggnonce pubkeys [] msg+> let Right aggnonce = MuSig2.aggNonces pubnonces+> let Right session_ctx = MuSig2.mkSessionContext aggnonce pubkeys [] msg > > -- each signer creates a partial signature-> let psig1 = MuSig2.sign secnonce1 sec1 session_ctx-> let psig2 = MuSig2.sign secnonce2 sec2 session_ctx+> let Right psig1 = MuSig2.sign secnonce1 sec1 session_ctx+> let Right psig2 = MuSig2.sign secnonce2 sec2 session_ctx > let psigs = [psig1, psig2] > > -- aggregate partial signatures into final signature-> let final_sig = MuSig2.aggPartials psigs session_ctx+> let Right final_sig = MuSig2.aggPartials psigs session_ctx > > -- verify the aggregated signature > Secp256k1.verify_schnorr msg agg_pk final_sig@@ -77,6 +95,7 @@ -} module Crypto.Curve.Secp256k1.MuSig2 ( -- Main types and functions+ MuSig2Error (..), sign, SecKey (..), PartialSignature,@@ -94,7 +113,7 @@ Tweak (..), sortPublicKeys, -- nonces- SecNonce (..),+ SecNonce, mkSecNonce, SecNonceGenParams (..), defaultSecNonceGenParams,@@ -105,7 +124,7 @@ aggNonces, ) where -import Control.Exception (ErrorCall (..), evaluate, throwIO, try)+import Control.Monad (foldM) import Crypto.Curve.Secp256k1 (Projective, Pub, add, derive_pub, mul, neg, serialize_point, _CURVE_G, _CURVE_ZERO) import Crypto.Curve.Secp256k1.MuSig2.Internal import Data.Binary.Put (@@ -121,9 +140,8 @@ #if !MIN_VERSION_base(4,20,0) import Data.Foldable (foldl') #endif-import Data.Foldable (toList)-import Data.List (isPrefixOf)-import Data.Maybe (fromJust, fromMaybe)+import Data.Foldable (toList, traverse_)+import Data.Maybe (fromMaybe) import Data.Sequence (Seq) import qualified Data.Sequence as Seq import Data.Traversable ()@@ -131,6 +149,51 @@ import GHC.Generics (Generic) import System.Entropy (getEntropy) +data MuSig2Error+ = EmptyPartialSignatureCollection+ | EmptyNonceCollection+ | EmptyPublicKeyCollection+ | TooManyPublicKeys+ | TooManyTweaks+ | PublicKeyAtInfinity+ | SignerCountMismatch Int Int+ | InvalidSignerIndex Int+ | NegativeTweak Integer+ | TweakOutOfRange Integer+ | AggregatedPublicKeyAtInfinity+ | TweakResultAtInfinity+ | SecretScalarZero String+ | SecretScalarOutOfRange String+ | SecretKeyPublicKeyMismatch+ | PartialSignatureOutOfRange+ | InvalidRandomBytesLength Int+ | PublicNonceGenerationFailed String+ | KeyDerivationFailed+ | ScalarMultiplicationFailed String+ | InvalidPointFormat+ | ZeroNonceGenerated+ deriving (Eq, Show)++liftMaybe :: e -> Maybe a -> Either e a+liftMaybe err = maybe (Left err) Right++validateSecretScalar :: String -> Integer -> Either MuSig2Error Integer+validateSecretScalar label scalar+ | scalar == 0 = Left (SecretScalarZero label)+ | scalar < 0 || scalar >= curveOrder = Left (SecretScalarOutOfRange label)+ | otherwise = Right scalar++validateTweakValue :: Integer -> Either MuSig2Error Integer+validateTweakValue tweak+ | tweak < 0 = Left (NegativeTweak tweak)+ | tweak >= curveOrder = Left (TweakOutOfRange tweak)+ | otherwise = Right tweak++validatePublicKey :: Pub -> Either MuSig2Error Pub+validatePublicKey pub+ | pub == _CURVE_ZERO = Left PublicKeyAtInfinity+ | otherwise = Right pub+ -- | Aggregates 'PartialSignature's into a 64-byte Schnorr signature. aggPartials :: (Traversable t) =>@@ -139,26 +202,38 @@ -- | Session context. SessionContext -> -- | 64-byte Schnorr signature.- ByteString+ Either MuSig2Error ByteString aggPartials partials ctx =- let- nonce = getSigningNonce ctx- e = bytesToInteger $ getSigningHash ctx- keyCtx = cachedKeyAggCtx ctx- aggPk = q keyCtx- taccVal = maybe 0 getTweak $ tacc keyCtx- gaccVal = gacc keyCtx- -- BIP 327: Let g = 1 if has_even_y(Q), otherwise let g = -1 mod n- g = if isEvenPub aggPk then 1 else curveOrder - 1- -- Apply accumulated parity factor- g' = modQ (g * gaccVal)- sSum = modQ $ sum partials- -- BIP 327: Let s = s₁ + ... + sᵤ + e⋅g'⋅tacc mod n- s = modQ (sSum + e * g' * taccVal)- left = xBytes nonce- right = integerToBytes32 s- in- left <> right+ if Seq.null partialsSeq+ then Left EmptyPartialSignatureCollection+ else case traverse validatePartialSignature partialsSeq of+ Left err -> Left err+ Right validPartials -> do+ nonce <- getSigningNonce ctx+ signingHash <- getSigningHash ctx+ let+ e = bytesToInteger signingHash+ keyCtx = cachedKeyAggCtx ctx+ aggPk = q keyCtx+ taccVal = maybe 0 getTweak $ tacc keyCtx+ gaccVal = gacc keyCtx+ -- BIP 327: Let g = 1 if has_even_y(Q), otherwise let g = -1 mod n+ evenAggPk <- liftMaybe InvalidPointFormat (isEvenPub aggPk)+ let+ g = if evenAggPk then 1 else curveOrder - 1+ -- Apply accumulated parity factor+ g' = modQ (g * gaccVal)+ sSum = modQ $ sum validPartials+ -- BIP 327: Let s = s₁ + ... + sᵤ + e⋅g'⋅tacc mod n+ s = modQ (sSum + e * g' * taccVal)+ left = xBytes nonce+ right = integerToBytes32 s+ Right (left <> right)+ where+ partialsSeq = Seq.fromList (toList partials)+ validatePartialSignature partial+ | partial < 0 || partial >= curveOrder = Left PartialSignatureOutOfRange+ | otherwise = Right partial {- | Compute a partial signature on a message. @@ -174,38 +249,46 @@ -- | Session context. SessionContext -> -- | Partial signature.- PartialSignature+ Either MuSig2Error PartialSignature sign secnonce sk ctx =- let- publicKeys = pks ctx- nonce = getSigningNonce ctx- e = bytesToInteger $ getSigningHash ctx- keyCtx = cachedKeyAggCtx ctx- aggPk = q keyCtx- oddAggPk = not $ isEvenPub aggPk- gaccVal = gacc keyCtx- k1 = if secnonce.k1 == 0 then error "musig2 (sign): first secret scalar k1 is zero" else secnonce.k1- k2 = if secnonce.k2 == 0 then error "musig2 (sign): first secret scalar k2 is zero" else secnonce.k2- d' = if unSecKey sk == 0 then error "musig2 (sign): secret key is zero" else unSecKey sk- -- `d` is negated if exactly one of the parity accumulator OR the aggregated pubkey has odd parity.- -- gaccVal == 1 means no negation, gaccVal == n-1 means negation- parityFromGacc = gaccVal /= 1- d = if parityFromGacc /= oddAggPk then curveOrder - d' else d'- p = fromMaybe (error "musig2 (sign): failed to derive public key") $ derive_pub (fromInteger d') -- Use original secret key for public key derivation- a = computeKeyAggCoef p publicKeys- -- if has_even_Y(R):- -- k = k1 + b*k2- -- else:- -- k = (n-k1) + b(n-k2)- -- = n - (k1 + b*k2)- b = getSigningNonceCoeff ctx- k = if isEvenPub nonce then k1 + b * k2 else curveOrder - (k1 + b * k2)- s = modQ (k + e * a * d)- r1' = fromMaybe (error "musig2 (sign): failed to compute r1") $ mul _CURVE_G (fromInteger secnonce.k1)- r2' = fromMaybe (error "musig2 (sign): failed to compute r2") $ mul _CURVE_G (fromInteger secnonce.k2)- pubNonce' = PubNonce r1' r2'- in- if partialSigVerifyInternal s pubNonce' p ctx then s else error "musig2 (sign): could not verify partial signature against public nonce, public key and session context"+ do+ nonce <- getSigningNonce ctx+ signingHash <- getSigningHash ctx+ let publicKeys = pks ctx+ e = bytesToInteger signingHash+ keyCtx = cachedKeyAggCtx ctx+ aggPk = q keyCtx+ gaccVal = gacc keyCtx+ SecNonce k1 k2 boundPk = secnonce+ evenAggPk <- liftMaybe InvalidPointFormat (isEvenPub aggPk)+ evenNonce <- liftMaybe InvalidPointFormat (isEvenPub nonce)+ _ <- validateSecretScalar "k1" k1+ _ <- validateSecretScalar "k2" k2+ d' <- validateSecretScalar "secret key" (unSecKey sk)+ p <- liftMaybe KeyDerivationFailed $ derive_pub (fromInteger d')+ if p /= boundPk+ then Left SecretKeyPublicKeyMismatch+ else do+ let+ -- `d` is negated if exactly one of the parity accumulator OR the aggregated pubkey has odd parity.+ -- gaccVal == 1 means no negation, gaccVal == n-1 means negation+ oddAggPk = not evenAggPk+ parityFromGacc = gaccVal /= 1+ d = if parityFromGacc /= oddAggPk then curveOrder - d' else d'+ a = computeKeyAggCoef p publicKeys+ -- if has_even_Y(R):+ -- k = k1 + b*k2+ -- else:+ -- k = (n-k1) + b(n-k2)+ -- = n - (k1 + b*k2)+ b = getSigningNonceCoeff ctx+ k = if evenNonce then k1 + b * k2 else curveOrder - (k1 + b * k2)+ s = modQ (k + e * a * d)+ pubNonce' <- publicNonce secnonce+ verified <- partialSigVerifyInternal s pubNonce' p ctx+ if verified+ then Right s+ else Left (PublicNonceGenerationFailed "partial signature self-verification failed") {- | A partial signature which is a scalar in the range \(0 \leq x < n\) where \(n\) is the curve order.@@ -228,19 +311,19 @@ -- | Index of the signer. Int -> -- | If the partial signature is valid.- Bool+ Either MuSig2Error Bool partialSigVerify partial nonces pks tweaks msg idx =- let aggNonce = fromJust $ aggNonces nonces- ctx = mkSessionContext aggNonce pks tweaks msg- noncesSeq = Seq.fromList (toList nonces)- pksSeq = Seq.fromList (toList pks)- pk = case Seq.lookup idx pksSeq of- Just p -> p- Nothing -> error "musig2 (partialSigVerify): signer index out of range of the list of public keys"- pubnonce = case Seq.lookup idx noncesSeq of- Just n -> n- Nothing -> error "musig2 (partialSigVerify): signer index out of range of the list of public nonces"- in partialSigVerifyInternal partial pubnonce pk ctx+ do+ aggNonce <- aggNonces nonces+ ctx <- mkSessionContext aggNonce pks tweaks msg+ let noncesSeq = Seq.fromList (toList nonces)+ pksSeq = Seq.fromList (toList pks)+ if Seq.length noncesSeq /= Seq.length pksSeq+ then Left (SignerCountMismatch (Seq.length pksSeq) (Seq.length noncesSeq))+ else do+ pk <- maybe (Left (InvalidSignerIndex idx)) Right (Seq.lookup idx pksSeq)+ pubnonce <- maybe (Left (InvalidSignerIndex idx)) Right (Seq.lookup idx noncesSeq)+ partialSigVerifyInternal partial pubnonce pk ctx {- | Verifies a 'PartialSignature'. @@ -258,35 +341,43 @@ -- | MuSig2 session context. SessionContext -> -- | If the partial signature is valid.- Bool+ Either MuSig2Error Bool partialSigVerifyInternal partial pubnonce pk ctx =- let- publicKeys = pks ctx- keyCtx = cachedKeyAggCtx ctx- aggPk = q keyCtx- oddAggPk = not $ isEvenPub aggPk- gaccVal = gacc keyCtx- e = bytesToInteger $ getSigningHash ctx- r1' = pubnonce.r1- r2' = pubnonce.r2- b = getSigningNonceCoeff ctx- finalNonce = getSigningNonce ctx -- This is the final aggregate nonce used for evenness check- s = if partial < 0 || partial >= curveOrder then error "musig2 (partialSigVerifyInternal): partial signature must be within curve order." else partial- -- Reconstruct the individual's effective nonce: R_s1 + b * R_s2- r2b = fromMaybe (error "musig2 (partialSigVerifyInternal): failed to compute r2 * b") $ mul r2' (fromInteger b)- re' = add r1' r2b- -- Negate individual nonce if final aggregate nonce has odd Y- re = if isEvenPub finalNonce then re' else neg re'- a = computeKeyAggCoef pk publicKeys- -- Calculate g factor: 1 if aggregate pubkey has even Y, n-1 if odd- g = if oddAggPk then curveOrder - 1 else 1- -- Apply parity accumulator: gacc is accumulated parity factor- g' = modQ (g * gaccVal)- sG = fromMaybe (error "musig2 (partialSigVerifyInternal): failed to compute s * G") $ mul _CURVE_G (fromInteger s)- pkMul = fromMaybe (error "musig2 (partialSigVerifyInternal): failed to compute pk multiplication") $ mul pk (fromInteger (modQ (e * a * g')))- sG' = re `add` pkMul- in- sG == sG'+ do+ s <- validatePartialSignature partial+ signingHash <- getSigningHash ctx+ finalNonce <- getSigningNonce ctx+ let+ publicKeys = pks ctx+ keyCtx = cachedKeyAggCtx ctx+ aggPk = q keyCtx+ gaccVal = gacc keyCtx+ e = bytesToInteger signingHash+ r1' = pubnonce.r1+ r2' = pubnonce.r2+ b = getSigningNonceCoeff ctx+ a = computeKeyAggCoef pk publicKeys+ evenAggPk <- liftMaybe InvalidPointFormat (isEvenPub aggPk)+ evenFinalNonce <- liftMaybe InvalidPointFormat (isEvenPub finalNonce)+ let+ oddAggPk = not evenAggPk+ -- Calculate g factor: 1 if aggregate pubkey has even Y, n-1 if odd+ g = if oddAggPk then curveOrder - 1 else 1+ -- Apply parity accumulator: gacc is accumulated parity factor+ g' = modQ (g * gaccVal)+ r2b <- liftMaybe (ScalarMultiplicationFailed "r2 * b") $ mul r2' (fromInteger b)+ sG <- liftMaybe (ScalarMultiplicationFailed "s * G") $ mul _CURVE_G (fromInteger s)+ pkMul <- liftMaybe (ScalarMultiplicationFailed "pk multiplication") $ mul pk (fromInteger (modQ (e * a * g')))+ let+ re' = add r1' r2b+ -- Negate individual nonce if final aggregate nonce has odd Y+ re = if evenFinalNonce then re' else neg re'+ sG' = re `add` pkMul+ Right (sG == sG')+ where+ validatePartialSignature s+ | s < 0 || s >= curveOrder = Left PartialSignatureOutOfRange+ | otherwise = Right s -- | Secret key. newtype SecKey = SecKey Integer@@ -328,22 +419,20 @@ -- | Optional 'Tweak' value. Maybe Tweak -> -- | Resulting 'KeyAggContext'.- KeyAggContext+ Either MuSig2Error KeyAggContext mkKeyAggContext pks mTweak- | Seq.null pks' = error "musig2 (mkKeyAggContext): empty public key collection"- | Seq.length pks' > fromIntegral (maxBound :: Word32) = error "musig2 (mkKeyAggContext): too many public keys (max 2^32 - 1)"- | _CURVE_ZERO `elem` pks' = error "musig2 (mkKeyAggContext): public key at point of infinity"- | maybe False ((< 0) . getTweak) mTweak = error "musig2 (mkKeyAggContext): tweak must be non-negative"- | maybe False ((>= curveOrder) . getTweak) mTweak = error "musig2 (mkKeyAggContext): tweak must be less than curve order"- | otherwise = case aggPublicKeys pks' of- Nothing -> error "musig2 (mkKeyAggContext): failed to aggregate public keys"- Just aggPk- | aggPk == _CURVE_ZERO -> error "musig2 (mkKeyAggContext): aggregated public key is point at infinity"- | otherwise ->- let baseCtx = KeyAggContext aggPk Nothing 1- in case mTweak of- Nothing -> baseCtx- Just tweak -> applyTweak baseCtx tweak+ | Seq.null pks' = Left EmptyPublicKeyCollection+ | Seq.length pks' > fromIntegral (maxBound :: Word32) = Left TooManyPublicKeys+ | otherwise = do+ traverse_ validatePublicKey pks'+ aggPk <- liftMaybe AggregatedPublicKeyAtInfinity $ aggPublicKeys pks'+ if aggPk == _CURVE_ZERO+ then Left AggregatedPublicKeyAtInfinity+ else do+ let baseCtx = KeyAggContext aggPk Nothing 1+ case mTweak of+ Nothing -> Right baseCtx+ Just tweak -> applyTweak baseCtx tweak where pks' = Seq.fromList (toList pks) @@ -387,42 +476,32 @@ -- | Message to be signed. ByteString -> -- | Resulting 'SessionContext'.- SessionContext+ Either MuSig2Error SessionContext mkSessionContext aggNonce pks tweaks msg- | Seq.null pks' = error "musig2 (mkSessionContext): empty public key collection"- | Seq.length pks' > fromIntegral (maxBound :: Word32) = error "musig2 (mkSessionContext): too many public keys (max 2^32 - 1)"- | _CURVE_ZERO `elem` pks' = error "musig2 (mkSessionContext): public key at point of infinity"- | Seq.length tweaks' > fromIntegral (maxBound :: Word32) = error "musig2 (mkSessionContext): too many tweaks (max 2^32 - 1)"- | any checkNeg tweaks' = error "musig2 (mkSessionContext): tweaks must be non-negative"- | any checkOrder tweaks' = error "musig2 (mkSessionContext): tweaks must be less than curve order"- | otherwise = case aggPublicKeys pks' of- Nothing -> error "musig2 (mkSessionContext): failed to aggregate public keys"- Just aggPk- | aggPk == _CURVE_ZERO -> error "musig2 (mkSessionContext): aggregated public key is point at infinity"- | otherwise -> SessionContext aggNonce pks' tweaks' msg keyCtx+ | Seq.null pks' = Left EmptyPublicKeyCollection+ | Seq.length pks' > fromIntegral (maxBound :: Word32) = Left TooManyPublicKeys+ | Seq.length tweaks' > fromIntegral (maxBound :: Word32) = Left TooManyTweaks+ | otherwise = do+ traverse_ validatePublicKey pks'+ traverse_ (validateTweakValue . getTweak) tweaks'+ keyCtx <- if Seq.null tweaks' then mkKeyAggContext pks' Nothing else mkKeyAggContext pks' Nothing >>= \baseCtx -> foldM applyTweak baseCtx tweaks'+ Right (SessionContext aggNonce pks' tweaks' msg keyCtx) where pks' = Seq.fromList (toList pks) tweaks' = Seq.fromList (toList tweaks)- checkNeg = (< 0) . getTweak- checkOrder = (>= curveOrder) . getTweak- -- Compute and cache the KeyAggContext once- keyCtx =- let baseCtx = mkKeyAggContext pks' Nothing- in if Seq.null tweaks' then baseCtx else foldl' applyTweak baseCtx tweaks' {- | Gets the signing nonce as a 'Projective' following [BIP-0327 algorithm and recommendations](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki#dealing-with-infinity-in-nonce-aggregation). -}-getSigningNonce :: SessionContext -> Projective-getSigningNonce ctx =+getSigningNonce :: SessionContext -> Either MuSig2Error Projective+getSigningNonce ctx = do let b = getSigningNonceCoeff ctx aggNonce = ctx.aggNonce aggNonce' = if aggNonce.r1 == _CURVE_ZERO then PubNonce _CURVE_G aggNonce.r2 else aggNonce- r2b = fromMaybe (error "musig2 (getSigningNonce): failed to compute r2 * b") $ mul aggNonce'.r2 (fromInteger b)- finalNonce = add aggNonce'.r1 r2b- in- if finalNonce == _CURVE_ZERO then _CURVE_G else finalNonce+ r2b <- liftMaybe (ScalarMultiplicationFailed "r2 * b") $ mul aggNonce'.r2 (fromInteger b)+ let finalNonce = add aggNonce'.r1 r2b+ Right (if finalNonce == _CURVE_ZERO then _CURVE_G else finalNonce) {- | Gets the signing nonce coefficient following [BIP-0327 algorithm and recommendations](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki#dealing-with-infinity-in-nonce-aggregation).@@ -447,17 +526,16 @@ [MuSig2 paper, page 6](https://eprint.iacr.org/2020/1261). In the BIP-0327 it is referred as @e@. -}-getSigningHash :: SessionContext -> ByteString-getSigningHash ctx =+getSigningHash :: SessionContext -> Either MuSig2Error ByteString+getSigningHash ctx = do+ nonce <- getSigningNonce ctx let aggPubKey = q (cachedKeyAggCtx ctx) qBytes = xBytes aggPubKey msg = ctx.msg- nonce = getSigningNonce ctx r = xBytes nonce preimage = r <> qBytes <> msg- in- hashTagModQ "BIP0340/challenge" preimage+ Right (hashTagModQ "BIP0340/challenge" preimage) -- | Tweak that can be added to an aggregated 'Pub'key. data Tweak@@ -476,36 +554,35 @@ getTweak (XOnlyTweak int) = int getTweak (PlainTweak int) = int --- | Applies a tweak to a KeyAggContext and returns a new KeyAggContext following [BIP-0327](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki).-applyTweak :: KeyAggContext -> Tweak -> KeyAggContext-applyTweak ctx newTweak =+applyTweak :: KeyAggContext -> Tweak -> Either MuSig2Error KeyAggContext+applyTweak ctx newTweak = do let pubkey = q ctx mAccTweak = tacc ctx gaccIn = gacc ctx accTweakVal = maybe 0 getTweak mAccTweak- in case newTweak of- PlainTweak t ->- -- Plain tweak: g = 1, Q' = g*Q + t*G, tacc' = t + g*tacc, gacc' = g*gacc- let g = 1- pubkeyMul = fromMaybe (error "musig2 (applyTweak): failed to compute pubkey * g") $ mul pubkey (fromInteger g)- tG = fromMaybe (error "musig2 (applyTweak): failed to compute t * G") $ mul _CURVE_G (fromInteger t)- tweakedPk = add pubkeyMul tG- newAccTweak = modQ (t + (g * accTweakVal))- newGacc = modQ (g * gaccIn)- in if tweakedPk == _CURVE_ZERO- then error "musig2 (applyTweak): result of tweaking cannot be infinity"- else ctx{q = tweakedPk, tacc = Just (PlainTweak newAccTweak), gacc = newGacc}- XOnlyTweak t ->- -- X-only tweak: g = 1 if even Y, g = n-1 if odd Y, tacc' = t + g*tacc- let g = if isEvenPub pubkey then 1 else curveOrder - 1- pubkeyMul = fromMaybe (error "musig2 (applyTweak): failed to compute pubkey * g") $ mul pubkey (fromInteger g)- tG = fromMaybe (error "musig2 (applyTweak): failed to compute t * G") $ mul _CURVE_G (fromInteger t)- tweakedPk = add pubkeyMul tG- newAccTweak = modQ (t + (g * accTweakVal))- newGacc = modQ (g * gaccIn)- in if tweakedPk == _CURVE_ZERO- then error "musig2 (applyTweak): result of tweaking cannot be infinity"- else ctx{q = tweakedPk, tacc = Just (XOnlyTweak newAccTweak), gacc = newGacc}+ t <- validateTweakValue (getTweak newTweak)+ case newTweak of+ PlainTweak _ -> do+ let g = 1+ pubkeyMul <- liftMaybe (ScalarMultiplicationFailed "pubkey * g") $ mul pubkey (fromInteger g)+ tG <- liftMaybe (ScalarMultiplicationFailed "t * G") $ mul _CURVE_G (fromInteger t)+ let tweakedPk = add pubkeyMul tG+ newAccTweak = modQ (t + (g * accTweakVal))+ newGacc = modQ (g * gaccIn)+ if tweakedPk == _CURVE_ZERO+ then Left TweakResultAtInfinity+ else Right ctx{q = tweakedPk, tacc = Just (PlainTweak newAccTweak), gacc = newGacc}+ XOnlyTweak _ -> do+ evenPubkey <- liftMaybe InvalidPointFormat (isEvenPub pubkey)+ let g = if evenPubkey then 1 else curveOrder - 1+ pubkeyMul <- liftMaybe (ScalarMultiplicationFailed "pubkey * g") $ mul pubkey (fromInteger g)+ tG <- liftMaybe (ScalarMultiplicationFailed "t * G") $ mul _CURVE_G (fromInteger t)+ let tweakedPk = add pubkeyMul tG+ newAccTweak = modQ (t + (g * accTweakVal))+ newGacc = modQ (g * gaccIn)+ if tweakedPk == _CURVE_ZERO+ then Left TweakResultAtInfinity+ else Right ctx{q = tweakedPk, tacc = Just (XOnlyTweak newAccTweak), gacc = newGacc} -- | Manual 'Ord' implementation of 'Projective' for lexicography sorting. instance Ord Projective where@@ -532,41 +609,38 @@ {- | Secret nonce. The secret nonce provides randomness, blinding a signer's private key when-signing. It is imperative that the same 'SecNonce' is not used to sign more-than one message with the same key, as this would allow an observer to-compute the private key used to create both signatures.--If you want to follow-[BIP-0327](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki)-suggestions, then use 'secNonceGen' otherwise use 'mkSecNonce'.--}-data SecNonce = SecNonce- { k1 :: !Integer- -- ^ First secret scalar.- , k2 :: !Integer- -- ^ Second secret scalar.- }- deriving (Read, Eq, Ord, Generic)+signing. -{- | Generates a 'SecNonce' using only the system's underlying Cryptographic Secure-Pseudorandom Number Generator (CSPRNG) using the-[@entropy@](https://hackage.haskell.org/package/entropy) package.+== SECURITY: Single-Use Requirement -== WARNING+A 'SecNonce' /must/ be used to 'sign' at most one message. Reusing the same+'SecNonce' across two different messages or session contexts allows an+adversary to algebraically recover the signer's private key. -Make sure that you have access to a good CSPRNG in your system before calling-this function.+This library does not enforce single-use semantics at the type level. It is+the caller's responsibility to ensure that each 'SecNonce' is consumed+exactly once and then discarded. Generate a fresh nonce (via 'secNonceGen')+for every signing session. -Note that this does not follow the+If you want to follow [BIP-0327](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki)-algorithm.+suggestions, then use 'secNonceGen' otherwise use 'mkSecNonce'.++@since 0.2.0 -}-mkSecNonce :: IO SecNonce-mkSecNonce = do- bytes <- getEntropy 64 -- 64 bytes = 512 bits for two 256-bit scalars- let k1' = bytesToInteger (BS.take 32 bytes)- k2' = bytesToInteger (BS.drop 32 bytes)- pure SecNonce{k1 = k1', k2 = k2'}+mkSecNonce ::+ -- | Public key this nonce is bound to.+ Pub ->+ -- | First secret scalar.+ Integer ->+ -- | Second secret scalar.+ Integer ->+ Either MuSig2Error SecNonce+mkSecNonce pub k1 k2 = do+ _ <- validatePublicKey pub+ k1' <- validateSecretScalar "k1" k1+ k2' <- validateSecretScalar "k2" k2+ Right SecNonce{secNonceK1Internal = k1', secNonceK2Internal = k2', secNoncePubKeyInternal = pub} -- | Required and Optional data to generate a 'SecNonce'. data SecNonceGenParams = SecNonceGenParams@@ -606,16 +680,14 @@ Make sure that you have access to a good CSPRNG in your system before calling this function. -}-secNonceGen :: SecNonceGenParams -> IO SecNonce+secNonceGen :: SecNonceGenParams -> IO (Either MuSig2Error SecNonce) secNonceGen params = loop where loop = do rand <- getEntropy 32- eres <- try (evaluate (secNonceGenWithRand rand params))- case eres of- Right sn -> pure sn- Left (ErrorCall msg) | "musig2 (nonceGen): zero nonce generated" `isPrefixOf` msg -> loop- Left e -> throwIO e+ case secNonceGenWithRand rand params of+ Left ZeroNonceGenerated -> loop+ other -> pure other {- | Generates a 'SecNonce' using a given random 'ByteString' and the inputs and algorithms from@@ -626,8 +698,21 @@ You should probably use 'secNonceGen'. Use this function if you really have a randomly-generated 'ByteString'. -}-secNonceGenWithRand :: ByteString -> SecNonceGenParams -> SecNonce-secNonceGenWithRand rand _params@(SecNonceGenParams{_pk = pkPoint, ..}) =+secNonceGenWithRand :: ByteString -> SecNonceGenParams -> Either MuSig2Error SecNonce+secNonceGenWithRand rand _params@(SecNonceGenParams{_pk = pkPoint, ..}) = do+ if BS.length rand /= 32+ then Left (InvalidRandomBytesLength (BS.length rand))+ else Right ()+ _ <- validatePublicKey pkPoint+ traverse_ validatePublicKey _aggpk+ case _sk of+ Nothing -> Right ()+ Just (SecKey skScalar) -> do+ skScalar' <- validateSecretScalar "secret key" skScalar+ expectedPk <- liftMaybe KeyDerivationFailed $ derive_pub (fromInteger skScalar')+ if expectedPk == pkPoint+ then Right ()+ else Left SecretKeyPublicKeyMismatch let -- Step 2: Optional sk XOR (with tagged hash for safety) rand' = case _sk of@@ -663,11 +748,9 @@ k1' = modQ . bytesToInteger $ hashTag "MuSig/nonce" (mkInput 0) k2' = modQ . bytesToInteger $ hashTag "MuSig/nonce" (mkInput 1)- in- -- Step 9: check for zero nonce and retry if so- if k1' == 0 || k2' == 0- then error "musig2 (nonceGen): zero nonce generated (retry)"- else SecNonce{k1 = k1', k2 = k2'}+ if k1' == 0 || k2' == 0+ then Left ZeroNonceGenerated+ else mkSecNonce pkPoint k1' k2' {- | Public nonce. @@ -686,11 +769,12 @@ deriving (Eq, Ord, Show) -- | Generates a 'PubNonce' from a 'SecNonce'.-publicNonce :: SecNonce -> PubNonce-publicNonce secNonce =- let r1' = fromMaybe (error "musig2 (publicNonce): failed to compute r1") $ mul _CURVE_G (fromInteger (k1 secNonce))- r2' = fromMaybe (error "musig2 (publicNonce): failed to compute r2") $ mul _CURVE_G (fromInteger (k2 secNonce))- in PubNonce r1' r2'+publicNonce :: SecNonce -> Either MuSig2Error PubNonce+publicNonce secNonce = do+ let (k1, k2) = secNonceScalars secNonce+ r1' <- liftMaybe (ScalarMultiplicationFailed "k1 * G") $ mul _CURVE_G (fromInteger k1)+ r2' <- liftMaybe (ScalarMultiplicationFailed "k2 * G") $ mul _CURVE_G (fromInteger k2)+ Right (PubNonce r1' r2') -- | 'Data.Semigroup' implementation of 'PubNonce' for algebraic sound combination of public nonces. instance Semigroup PubNonce where@@ -708,11 +792,11 @@ {- | Aggregates a 'Traversable' of 'PubNonce's using the [Nonce Aggregation algorithm in BIP-0327](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki). -}-aggNonces :: (Traversable t) => t PubNonce -> Maybe PubNonce+aggNonces :: (Traversable t) => t PubNonce -> Either MuSig2Error PubNonce aggNonces nonces- | Seq.null noncesSeq = Nothing+ | Seq.null noncesSeq = Left EmptyNonceCollection | otherwise = case Seq.viewl noncesSeq of- Seq.EmptyL -> Nothing- x Seq.:< xs -> Just $! foldl' (<>) x xs+ Seq.EmptyL -> Left EmptyNonceCollection+ x Seq.:< xs -> Right $! foldl' (<>) x xs where noncesSeq = Seq.fromList (toList nonces)
lib/Crypto/Curve/Secp256k1/MuSig2/Internal.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE OverloadedStrings #-} {-# OPTIONS_GHC -Wno-orphans #-}@@ -17,6 +18,9 @@ -- Key aggregation computeKeyAggCoef, getSecondKey,+ -- SecNonce internals (not re-exported from public API)+ SecNonce (..),+ secNonceScalars, -- utils/misc isEvenPub, xBytes,@@ -44,7 +48,23 @@ import Data.Sequence (Seq) import qualified Data.Sequence as Seq import Data.Traversable ()+import GHC.Generics (Generic) +-- | Secret nonce (internal representation). See public API for documentation.+data SecNonce = SecNonce+ { secNonceK1Internal :: !Integer+ -- ^ First secret scalar.+ , secNonceK2Internal :: !Integer+ -- ^ Second secret scalar.+ , secNoncePubKeyInternal :: !Pub+ -- ^ Public key this nonce is bound to.+ }+ deriving (Generic)++-- | Returns the two secret scalars contained in a 'SecNonce'.+secNonceScalars :: SecNonce -> (Integer, Integer)+secNonceScalars secNonce = (secNonceK1Internal secNonce, secNonceK2Internal secNonce)+ {- | Aggregates a 'Traversable' of 'Pub'keys using the [Key Aggregation algorithm in BIP-0327](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki). @@ -144,12 +164,12 @@ encodeLen :: ByteString -> ByteString encodeLen bs = BSL.toStrict . toLazyByteString . word64BE . fromIntegral $ BS.length bs --- | Checks if a 'Pub'key is even.-isEvenPub :: Pub -> Bool+-- | Checks if a 'Pub'key is even. Returns 'Nothing' for invalid compressed point formats.+isEvenPub :: Pub -> Maybe Bool isEvenPub pub = case BS.unpack (serialize_point pub) of- (0x02 : _) -> True -- even y-coordinate- (0x03 : _) -> False -- odd y-coordinate- _ -> error "musig2 (isEvenPub): invalid compressed point format"+ (0x02 : _) -> Just True -- even y-coordinate+ (0x03 : _) -> Just False -- odd y-coordinate+ _ -> Nothing -- | Gets the X-coordinate from a 'Pub'lic key as 'ByteString' xBytes :: Pub -> ByteString
musig2.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: musig2-version: 0.1.4+version: 0.2.0 synopsis: MuSig2 library license: MIT license-file: LICENSE@@ -34,15 +34,15 @@ exposed-modules: Crypto.Curve.Secp256k1.MuSig2 other-modules: Crypto.Curve.Secp256k1.MuSig2.Internal build-depends:- , base >=4.19 && <5- , base16-bytestring >=1.0 && <1.1.0- , binary >=0.8 && <0.9- , bytestring >=0.9 && <0.13- , containers >=0.6 && <0.9- , entropy >=0.4 && <0.5- , ppad-hmac-drbg >=0.2 && <0.3- , ppad-secp256k1 >=0.5 && <0.6- , ppad-sha256 >=0.3 && <0.4+ , base >=4.19 && <5+ , base16-bytestring >=1.0 && <1.1+ , binary >=0.8 && <0.9+ , bytestring >=0.9 && <0.13+ , containers >=0.6 && <0.9+ , entropy >=0.4 && <0.5+ , ppad-hmac-drbg >=0.3.1 && <0.4+ , ppad-secp256k1 >=0.5.4 && <0.6+ , ppad-sha256 >=0.3.2 && <0.4 test-suite musig2-tests type: exitcode-stdio-1.0
test/AggNonces.hs view
@@ -4,10 +4,9 @@ import Crypto.Curve.Secp256k1 (_CURVE_ZERO) import Crypto.Curve.Secp256k1.MuSig2 (PubNonce (..), aggNonces)-import Data.Maybe (fromJust) import Test.Tasty import Test.Tasty.HUnit-import Util (parsePoint, parsePubNonce)+import Util (parsePoint, parsePubNonce, unsafeRight) -- | Input 'PubNonce's from BIP-0327 test vectors inputPubNonces :: [PubNonce]@@ -32,7 +31,7 @@ makeTestCase :: Int -> ([Int], PubNonce) -> TestTree makeTestCase i (indices, expected) = testCase ("BIP-0327 test vector " <> show (i + 1)) $- fromJust aggNonce @=? expected+ unsafeRight aggNonce @=? expected where selectedNonces = map (inputPubNonces !!) indices aggNonce = aggNonces selectedNonces
test/AggPartials.hs view
@@ -5,11 +5,11 @@ module AggPartials (testAggPartials) where import Crypto.Curve.Secp256k1 (Pub)-import Crypto.Curve.Secp256k1.MuSig2 (PartialSignature, PubNonce (..), Tweak (..), aggPartials, mkSessionContext)+import Crypto.Curve.Secp256k1.MuSig2 (MuSig2Error (..), PartialSignature, PubNonce (..), Tweak (..), aggPartials, mkSessionContext) import Data.ByteString (ByteString) import Test.Tasty import Test.Tasty.HUnit-import Util (decodeHex, parsePoint, parsePubNonce, parseScalar)+import Util (decodeHex, parsePoint, parsePubNonce, parseScalar, unsafeRight) -- | Test vector structure for signature aggregation. data SigAggTestVector = SigAggTestVector@@ -142,8 +142,8 @@ let selectedKeys = map (pubkeys !!) keyIndices let selectedTweaks = buildTweaks tweakIndices isXOnly let selectedPsigs = map (psigs !!) psigIndices- let ctx = mkSessionContext aggNonce selectedKeys selectedTweaks msg- let result = aggPartials selectedPsigs ctx+ let ctx = unsafeRight $ mkSessionContext aggNonce selectedKeys selectedTweaks msg+ let result = unsafeRight $ aggPartials selectedPsigs ctx case expected of Just expectedSig -> assertEqual "signature mismatch" expectedSig result Nothing -> assertFailure "Expected signature but got nothing"@@ -152,10 +152,12 @@ makeErrorTestCase :: Int -> SigAggTestVector -> TestTree makeErrorTestCase i SigAggTestVector{..} = testCase ("BIP-0327 SigAgg Error Vector " ++ show (i + 1)) $ do- -- For the error case, we expect aggPartials to fail due to invalid partial signature- -- The test vector has psig[8] which is FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141- -- This exceeds the curve order and should cause validation to fail+ let selectedKeys = map (pubkeys !!) keyIndices+ selectedTweaks = buildTweaks tweakIndices isXOnly+ selectedPsigs = map (psigs !!) psigIndices+ ctx = unsafeRight $ mkSessionContext aggNonce selectedKeys selectedTweaks msg assertBool "Expected error case" errorCase+ aggPartials selectedPsigs ctx @?= Left PartialSignatureOutOfRange -- | Main test group for signature aggregation. testAggPartials :: TestTree
test/AggPartialsProperty.hs view
@@ -1,15 +1,15 @@+{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-} module AggPartialsProperty (propertyAggPartials) where -import Crypto.Curve.Secp256k1 (derive_pub)-import Crypto.Curve.Secp256k1.MuSig2 (SecKey (..), SecNonce (..), aggNonces, aggPartials, mkSessionContext, partialSigVerify, publicNonce, sign)+import Crypto.Curve.Secp256k1 (Pub)+import Crypto.Curve.Secp256k1.MuSig2 (PubNonce, SessionContext, aggNonces, aggPartials, mkSessionContext, partialSigVerify, publicNonce, sign) import Data.ByteString (ByteString) import qualified Data.ByteString as BS-import Data.Maybe (fromJust, fromMaybe) import Test.Tasty import Test.Tasty.QuickCheck as QC-import Util ()+import Util (SignerMaterial (..), unsafeRight) propertyAggPartials :: TestTree propertyAggPartials =@@ -21,61 +21,53 @@ , testProperty "Aggregation with single signer equals partial sig" prop_singleSignerAgg ] +mkTwoSignerContext :: SignerMaterial -> SignerMaterial -> ByteString -> ([PubNonce], [Pub], SessionContext)+mkTwoSignerContext signer1 signer2 msg =+ let pubNonces = [unsafeRight $ publicNonce signer1.signerSecNonce, unsafeRight $ publicNonce signer2.signerSecNonce]+ pubkeys = [signer1.signerPubKey, signer2.signerPubKey]+ aggNonce = unsafeRight $ aggNonces pubNonces+ ctx = unsafeRight $ mkSessionContext aggNonce pubkeys [] msg+ in (pubNonces, pubkeys, ctx)+ -- | Property: Aggregating valid partial signatures produces a consistent result-prop_aggValidPartials :: SecNonce -> SecKey -> SecNonce -> SecKey -> ByteString -> Property-prop_aggValidPartials secNonce1 secKey1 secNonce2 secKey2 msg =- let pubkey1 = fromMaybe (error "Failed to derive pubkey1") $ derive_pub (fromInteger (case secKey1 of SecKey sk -> sk))- pubkey2 = fromMaybe (error "Failed to derive pubkey2") $ derive_pub (fromInteger (case secKey2 of SecKey sk -> sk))- pubkeys = [pubkey1, pubkey2]- pubNonces = [publicNonce secNonce1, publicNonce secNonce2]- aggNonce = fromJust $ aggNonces pubNonces- ctx = mkSessionContext aggNonce pubkeys [] msg- partialSig1 = sign secNonce1 secKey1 ctx- partialSig2 = sign secNonce2 secKey2 ctx- aggregated = aggPartials [partialSig1, partialSig2] ctx- in BS.length aggregated === 64 -- Schnorr signature is 64 bytes+prop_aggValidPartials :: SignerMaterial -> SignerMaterial -> ByteString -> Property+prop_aggValidPartials signer1 signer2 msg =+ let (_, _, ctx) = mkTwoSignerContext signer1 signer2 msg+ partialSig1 = unsafeRight $ sign signer1.signerSecNonce signer1.signerSecKey ctx+ partialSig2 = unsafeRight $ sign signer2.signerSecNonce signer2.signerSecKey ctx+ aggregated = unsafeRight $ aggPartials [partialSig1, partialSig2] ctx+ in BS.length aggregated === 64 -- | Property: Aggregation is deterministic - same inputs produce same output-prop_aggDeterministic :: SecNonce -> SecKey -> SecNonce -> SecKey -> ByteString -> Property-prop_aggDeterministic secNonce1 secKey1 secNonce2 secKey2 msg =- let pubkey1 = fromMaybe (error "Failed to derive pubkey1") $ derive_pub (fromInteger (case secKey1 of SecKey sk -> sk))- pubkey2 = fromMaybe (error "Failed to derive pubkey2") $ derive_pub (fromInteger (case secKey2 of SecKey sk -> sk))- pubkeys = [pubkey1, pubkey2]- pubNonces = [publicNonce secNonce1, publicNonce secNonce2]- aggNonce = fromJust $ aggNonces pubNonces- ctx = mkSessionContext aggNonce pubkeys [] msg- partialSig1 = sign secNonce1 secKey1 ctx- partialSig2 = sign secNonce2 secKey2 ctx+prop_aggDeterministic :: SignerMaterial -> SignerMaterial -> ByteString -> Property+prop_aggDeterministic signer1 signer2 msg =+ let (_, _, ctx) = mkTwoSignerContext signer1 signer2 msg+ partialSig1 = unsafeRight $ sign signer1.signerSecNonce signer1.signerSecKey ctx+ partialSig2 = unsafeRight $ sign signer2.signerSecNonce signer2.signerSecKey ctx partialSigs = [partialSig1, partialSig2]- aggregated1 = aggPartials partialSigs ctx- aggregated2 = aggPartials partialSigs ctx+ aggregated1 = unsafeRight $ aggPartials partialSigs ctx+ aggregated2 = unsafeRight $ aggPartials partialSigs ctx in aggregated1 === aggregated2 -- | Property: All partial signatures should verify individually before aggregation-prop_partialsVerifyBeforeAgg :: SecNonce -> SecKey -> SecNonce -> SecKey -> ByteString -> Property-prop_partialsVerifyBeforeAgg secNonce1 secKey1 secNonce2 secKey2 msg =- let pubkey1 = fromMaybe (error "Failed to derive pubkey1") $ derive_pub (fromInteger (case secKey1 of SecKey sk -> sk))- pubkey2 = fromMaybe (error "Failed to derive pubkey2") $ derive_pub (fromInteger (case secKey2 of SecKey sk -> sk))- pubkeys = [pubkey1, pubkey2]- pubNonces = [publicNonce secNonce1, publicNonce secNonce2]- aggNonce = fromJust $ aggNonces pubNonces- ctx = mkSessionContext aggNonce pubkeys [] msg- partialSig1 = sign secNonce1 secKey1 ctx- partialSig2 = sign secNonce2 secKey2 ctx+prop_partialsVerifyBeforeAgg :: SignerMaterial -> SignerMaterial -> ByteString -> Property+prop_partialsVerifyBeforeAgg signer1 signer2 msg =+ let (pubNonces, pubkeys, ctx) = mkTwoSignerContext signer1 signer2 msg+ partialSig1 = unsafeRight $ sign signer1.signerSecNonce signer1.signerSecKey ctx+ partialSig2 = unsafeRight $ sign signer2.signerSecNonce signer2.signerSecKey ctx verify1 = partialSigVerify partialSig1 pubNonces pubkeys [] msg 0 verify2 = partialSigVerify partialSig2 pubNonces pubkeys [] msg 1- in verify1 === True .&&. verify2 === True+ in verify1 === Right True .&&. verify2 === Right True -- | Property: For a single signer, aggregation should work correctly-prop_singleSignerAgg :: SecNonce -> SecKey -> ByteString -> Property-prop_singleSignerAgg secNonce secKey@(SecKey sk) msg =- let pubkey = fromMaybe (error "Failed to derive pubkey") $ derive_pub (fromInteger sk)- pubNonce = publicNonce secNonce+prop_singleSignerAgg :: SignerMaterial -> ByteString -> Property+prop_singleSignerAgg signer msg =+ let pubNonce = unsafeRight $ publicNonce signer.signerSecNonce pubNonces = [pubNonce]- pubkeys = [pubkey]- aggNonce = fromJust $ aggNonces pubNonces- ctx = mkSessionContext aggNonce pubkeys [] msg- partialSig = sign secNonce secKey ctx- aggregated = aggPartials [partialSig] ctx+ pubkeys = [signer.signerPubKey]+ aggNonce = unsafeRight $ aggNonces pubNonces+ ctx = unsafeRight $ mkSessionContext aggNonce pubkeys [] msg+ partialSig = unsafeRight $ sign signer.signerSecNonce signer.signerSecKey ctx+ aggregated = unsafeRight $ aggPartials [partialSig] ctx verifyResult = partialSigVerify partialSig pubNonces pubkeys [] msg 0- in verifyResult === True .&&. BS.length aggregated === 64+ in verifyResult === Right True .&&. BS.length aggregated === 64
test/AggPubkeys.hs view
@@ -2,13 +2,12 @@ module AggPubkeys (testAggPubkeys) where -import Control.Exception (ErrorCall (..), evaluate, try) import Crypto.Curve.Secp256k1 (Pub)-import Crypto.Curve.Secp256k1.MuSig2 (Tweak (..), aggregatedPubkey, mkKeyAggContext)+import Crypto.Curve.Secp256k1.MuSig2 (MuSig2Error (..), Tweak (..), aggregatedPubkey, mkKeyAggContext) import Data.ByteString (ByteString) import Test.Tasty import Test.Tasty.HUnit-import Util (decodeHex, extractXOnly, parsePoint)+import Util (decodeHex, extractXOnly, parsePoint, unsafeRight) -- | Input public keys from BIP-0327 test vectors inputPubkeys :: [Pub]@@ -40,11 +39,14 @@ , 0x252E4BD67410A76CDF933D30EAA1608214037F1B105A013ECCD3C5C184A6110B -- tweak that causes infinity ] --- | Error test cases: (key indices, tweak index, is_xonly, expected error message).-errorTestVectors :: [([Int], Int, Bool, String)]+invalidTweakOutOfRange :: Integer+invalidTweakOutOfRange = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141++-- | Error test cases: (key indices, tweak index, is_xonly, expected error).+errorTestVectors :: [([Int], Int, Bool, MuSig2Error)] errorTestVectors =- [ ([0, 1], 0, True, "tweak must be less than curve order") -- Tweak is out of range- , ([6], 1, False, "result of tweaking cannot be infinity") -- Intermediate tweaking result is point at infinity+ [ ([0, 1], 0, True, TweakOutOfRange invalidTweakOutOfRange)+ , ([6], 1, False, TweakResultAtInfinity) ] -- | Creates test case from vector data.@@ -54,28 +56,20 @@ extractXOnly aggPk @=? expected where selectedKeys = map (inputPubkeys !!) indices- keyAggCtx = mkKeyAggContext selectedKeys Nothing+ keyAggCtx = unsafeRight $ mkKeyAggContext selectedKeys Nothing aggPk = aggregatedPubkey keyAggCtx -- | Creates error test case from vector data.-makeErrorTestCase :: Int -> ([Int], Int, Bool, String) -> TestTree+makeErrorTestCase :: Int -> ([Int], Int, Bool, MuSig2Error) -> TestTree makeErrorTestCase i (keyIndices, tweakIndex, isXOnly, expectedMsg) = testCase ("BIP-0327 error test vector " <> show (i + 1)) $ do- result <- try $ evaluate $ mkKeyAggContext selectedKeys (Just tweak)- case result of- Left (ErrorCall msg) -> assertBool ("Expected '" <> expectedMsg <> "' in error message, got: " <> msg) (expectedMsg `isSubsequenceOf` msg)+ case mkKeyAggContext selectedKeys (Just tweak) of+ Left err -> err @?= expectedMsg Right _ -> assertFailure "Expected error but got success" where selectedKeys = map (inputPubkeys !!) keyIndices tweakVal = tweaks !! tweakIndex tweak = if isXOnly then XOnlyTweak tweakVal else PlainTweak tweakVal-- isSubsequenceOf :: String -> String -> Bool- isSubsequenceOf [] _ = True- isSubsequenceOf _ [] = False- isSubsequenceOf (x : xs) (y : ys)- | x == y = isSubsequenceOf xs ys- | otherwise = isSubsequenceOf (x : xs) ys -- | Test vectors from [BIP-0327 `key_agg_vectors.json`](https://github.com/bitcoin/bips/blob/master/bip-0327/vectors/key_agg_vectors.json). testAggPubkeys :: TestTree
test/ApplyTweaks.hs view
@@ -9,7 +9,7 @@ import Data.Maybe (fromJust) import Test.Tasty import Test.Tasty.HUnit-import Util (decodeHex, extractXOnly, parsePoint)+import Util (decodeHex, extractXOnly, parsePoint, unsafeRight) -- | Computes taproot tweak from pubkey and merkle root. computeTaprootTweak :: Pub -> ByteString -> Integer@@ -32,23 +32,23 @@ testTweakSequence :: TestTree testTweakSequence = testCase "Tweak sequence" $ do- let keyAggCtx1 = mkKeyAggContext testPubkeys Nothing+ let keyAggCtx1 = unsafeRight $ mkKeyAggContext testPubkeys Nothing -- Apply first X-only tweak let tweak1 = XOnlyTweak 0xE8F791FF9225A2AF0102AFFF4A9A723D9612A682A25EBE79802B263CDFCD83BB- keyAggCtx2 = applyTweak keyAggCtx1 tweak1+ keyAggCtx2 = unsafeRight $ applyTweak keyAggCtx1 tweak1 -- Apply second X-only tweak let tweak2 = XOnlyTweak 0xAE2EA797CC0FE72AC5B97B97F3C6957D7E4199A167A58EB08BCAFFDA70AC0455- keyAggCtx3 = applyTweak keyAggCtx2 tweak2+ keyAggCtx3 = unsafeRight $ applyTweak keyAggCtx2 tweak2 -- Apply third plain tweak let tweak3 = PlainTweak 0xF52ECBC565B3D8BEA2DFD5B75A4F457E54369809322E4120831626F290FA87E0- keyAggCtx4 = applyTweak keyAggCtx3 tweak3+ keyAggCtx4 = unsafeRight $ applyTweak keyAggCtx3 tweak3 -- Apply fourth plain tweak let tweak4 = PlainTweak 0x1969AD73CC177FA0B4FCED6DF1F7BF9907E665FDE9BA196A74FED0A3CF5AEF9D- finalKeyAggCtx = applyTweak keyAggCtx4 tweak4+ finalKeyAggCtx = unsafeRight $ applyTweak keyAggCtx4 tweak4 finalAggPk = aggregatedPubkey finalKeyAggCtx expected = fromJust $ parse_point $ decodeHex "0269434B39A026A4AAC9E6C1AEBDD3993FFA581C8F7F21B6FAAE15608057F5CE85" @@ -58,13 +58,13 @@ testTaprootTweak :: TestTree testTaprootTweak = testCase "Taproot tweak with merkle root" $ do- let keyAggCtx = mkKeyAggContext testPubkeys Nothing+ let keyAggCtx = unsafeRight $ mkKeyAggContext testPubkeys Nothing originalPk = aggregatedPubkey keyAggCtx merkleRootBytes = decodeHex "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" -- Compute proper taproot tweak hash taprootTweakValue = computeTaprootTweak originalPk merkleRootBytes taprootTweak = XOnlyTweak taprootTweakValue- tweakedCtx = applyTweak keyAggCtx taprootTweak+ tweakedCtx = unsafeRight $ applyTweak keyAggCtx taprootTweak tweakedPk = aggregatedPubkey tweakedCtx expected = fromJust $ parse_point $ decodeHex "024650cca5e389f62e960f66ca0400927a7727fc6e84b9c38a1fd9a80271377ceb"
test/NonceGen.hs view
@@ -5,12 +5,13 @@ module NonceGen (testNonceGen) where import Crypto.Curve.Secp256k1 (Pub)-import Crypto.Curve.Secp256k1.MuSig2 (PubNonce (..), SecKey (..), SecNonce (..), SecNonceGenParams (..), publicNonce, secNonceGenWithRand)+import Crypto.Curve.Secp256k1.MuSig2 (PubNonce (..), SecKey (..), SecNonceGenParams (..), publicNonce, secNonceGenWithRand)+import Crypto.Curve.Secp256k1.MuSig2.Internal (secNonceScalars) import Data.ByteString (ByteString) import qualified Data.ByteString as BS import Test.Tasty import Test.Tasty.HUnit-import Util (decodeHex, parsePoint, parseScalar)+import Util (decodeHex, parsePoint, parseScalar, unsafeRight) -- | Test vector structure. data NonceGenTestVector = NonceGenTestVector@@ -91,10 +92,11 @@ , _msg = msg , _extraIn = extra_in }- let sec = secNonceGenWithRand rand_ params- let pub = publicNonce sec- assertEqual "k1 mismatch" expected_k1 sec.k1- assertEqual "k2 mismatch" expected_k2 sec.k2+ let sec = unsafeRight $ secNonceGenWithRand rand_ params+ let pub = unsafeRight $ publicNonce sec+ let (k1, k2) = secNonceScalars sec+ assertEqual "k1 mismatch" expected_k1 k1+ assertEqual "k2 mismatch" expected_k2 k2 assertEqual "r1 mismatch" expected_r1 pub.r1 assertEqual "r2 mismatch" expected_r2 pub.r2
test/NonceGenProperty.hs view
@@ -1,16 +1,15 @@ {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-} module NonceGenProperty (propertyNonceGen) where -import Crypto.Curve.Secp256k1 (mul, _CURVE_G)-import Crypto.Curve.Secp256k1.MuSig2 (PubNonce (..), SecKey (..), SecNonce (..), SecNonceGenParams (..), publicNonce, secNonceGenWithRand)-import Crypto.Curve.Secp256k1.MuSig2.Internal (curveOrder, hashTag, integerToBytes32, xorByteStrings)+import Crypto.Curve.Secp256k1 (derive_pub, mul, _CURVE_G)+import Crypto.Curve.Secp256k1.MuSig2 (PubNonce (..), SecKey (..), SecNonceGenParams (..), publicNonce, secNonceGenWithRand)+import Crypto.Curve.Secp256k1.MuSig2.Internal (curveOrder, hashTag, integerToBytes32, secNonceScalars, xorByteStrings) import Data.ByteString () import Data.Maybe (fromMaybe) import Test.Tasty import Test.Tasty.QuickCheck as QC-import Util (Rand32 (..), Scalar (..))+import Util (Rand32 (..), Scalar (..), unsafeRight) propertyNonceGen :: TestTree propertyNonceGen =@@ -24,24 +23,30 @@ -- | Property: Generated k1 and k2 are in the valid range [1, Q-1] prop_validRange :: Rand32 -> SecNonceGenParams -> Property prop_validRange (Rand32 rand) params =- let SecNonce{..} = secNonceGenWithRand rand params+ let (k1, k2) = secNonceScalars (unsafeRight $ secNonceGenWithRand rand params) in (1 <= k1 && k1 < curveOrder) .&&. (1 <= k2 && k2 < curveOrder) -- | Property: The public nonce points correspond to G multiplied by k1 and k2 prop_correctPubNonce :: Rand32 -> SecNonceGenParams -> Property prop_correctPubNonce (Rand32 rand) params =- let sn@SecNonce{..} = secNonceGenWithRand rand params- PubNonce r1 r2 = publicNonce sn- in r1 === fromMaybe (error "Failed to multiply scalar by generator") (mul _CURVE_G (fromInteger k1)) .&&. r2 === fromMaybe (error "Failed to multiply scalar by generator") (mul _CURVE_G (fromInteger k2))+ let sn = unsafeRight $ secNonceGenWithRand rand params+ (k1, k2) = secNonceScalars sn+ PubNonce nonceR1 nonceR2 = unsafeRight $ publicNonce sn+ in nonceR1 === fromMaybe (error "Failed to multiply scalar by generator") (mul _CURVE_G (fromInteger k1))+ .&&. nonceR2+ === fromMaybe (error "Failed to multiply scalar by generator") (mul _CURVE_G (fromInteger k2)) -- | Property: Generating with sk provided is equivalent to XORing rand with the aux hash and generating without sk prop_skConsistency :: Rand32 -> SecNonceGenParams -> Scalar -> Property prop_skConsistency (Rand32 rand) params (Scalar sk) =- let paramsNoSk = params{_sk = Nothing}- paramsWithSk = params{_sk = Just (SecKey sk)}+ let pkForSk = case derive_pub (fromInteger sk) of+ Just pub -> pub+ Nothing -> error "Failed to derive pubkey for property"+ paramsNoSk = params{_pk = pkForSk, _sk = Nothing}+ paramsWithSk = params{_pk = pkForSk, _sk = Just (SecKey sk)} auxHash = hashTag "MuSig/aux" rand skBytes = integerToBytes32 sk rand' = xorByteStrings skBytes auxHash- SecNonce k1l k2l = secNonceGenWithRand rand paramsWithSk- SecNonce k1r k2r = secNonceGenWithRand rand' paramsNoSk+ (k1l, k2l) = secNonceScalars (unsafeRight $ secNonceGenWithRand rand paramsWithSk)+ (k1r, k2r) = secNonceScalars (unsafeRight $ secNonceGenWithRand rand' paramsNoSk) in k1l === k1r .&&. k2l === k2r
test/ParityPub.hs view
@@ -12,7 +12,7 @@ testParityPub = testGroup "isEvenPub"- [ testCase "Even compressed key" $ assertBool "Pubkey is even" (isEvenPub $ parsePoint "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8")- , testCase "Odd compressed key" $ assertBool "Pubkey is not even" (not $ isEvenPub $ parsePoint "03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659")- , testCase "X-only key" $ assertBool "X-only pubkeys are always even" (isEvenPub $ parsePoint "6204DE8B083426DC6EAF9502D27024D53FC826BF7D2012148A0575435DF54B2B")+ [ testCase "Even compressed key" $ isEvenPub (parsePoint "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8") @?= Just True+ , testCase "Odd compressed key" $ isEvenPub (parsePoint "03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659") @?= Just False+ , testCase "X-only key" $ isEvenPub (parsePoint "6204DE8B083426DC6EAF9502D27024D53FC826BF7D2012148A0575435DF54B2B") @?= Just True ]
test/SignVerify.hs view
@@ -1,18 +1,15 @@ {-# LANGUAGE OverloadedStrings #-}-{-# OPTIONS_GHC -Wno-x-partial #-} module SignVerify (testSignVerify) where -import Control.Exception (ErrorCall (..), evaluate, try)-import Crypto.Curve.Secp256k1 (Pub)-import Crypto.Curve.Secp256k1.MuSig2 (PubNonce (..), SecKey (..), SecNonce (..), aggNonces, mkSessionContext, partialSigVerify, sign)+import Crypto.Curve.Secp256k1 (Pub, derive_pub, _CURVE_ZERO)+import Crypto.Curve.Secp256k1.MuSig2 (MuSig2Error (..), PubNonce, SecKey (..), aggNonces, mkSecNonce, mkSessionContext, partialSigVerify, publicNonce, sign) import Crypto.Curve.Secp256k1.MuSig2.Internal (bytesToInteger) import Data.ByteString (ByteString)-import Data.List (isInfixOf)-import Data.Maybe (fromJust)+import Data.Maybe (fromMaybe) import Test.Tasty import Test.Tasty.HUnit-import Util (decodeHex, parsePoint, parsePubNonce)+import Util (decodeHex, parsePoint, parsePubNonce, unsafeMkSecNonce, unsafeRight) -- | Input public keys from BIP-0327 test vectors. inputPubkeys :: [Pub]@@ -45,40 +42,29 @@ , decodeHex "2626262626262626262626262626262626262626262626262626262626262626262626262626" ] --- | Valid test vector data: (key indices, nonce indices, msg index, signer index, expected signature) validTestVectors :: [([Int], [Int], Int, Int, ByteString)] validTestVectors = [ ([0, 1, 2], [0, 1, 2], 0, 0, decodeHex "012ABBCB52B3016AC03AD82395A1A415C48B93DEF78718E62A7A90052FE224FB") , ([1, 0, 2], [1, 0, 2], 0, 1, decodeHex "9FF2F7AAA856150CC8819254218D3ADEEB0535269051897724F9DB3789513A52") , ([1, 2, 0], [1, 2, 0], 0, 2, decodeHex "FA23C359F6FAC4E7796BB93BC9F0532A95468C539BA20FF86D7C76ED92227900")- , ([0, 1, 2], [0, 1, 2], 1, 0, decodeHex "D7D63FFD644CCDA4E62BC2BC0B1D02DD32A1DC3030E155195810231D1037D82D") -- Empty message- , ([0, 1, 2], [0, 1, 2], 2, 0, decodeHex "E184351828DA5094A97C79CABDAAA0BFB87608C32E8829A4DF5340A6F243B78C") -- 38-byte message+ , ([0, 1, 2], [0, 1, 2], 1, 0, decodeHex "D7D63FFD644CCDA4E62BC2BC0B1D02DD32A1DC3030E155195810231D1037D82D")+ , ([0, 1, 2], [0, 1, 2], 2, 0, decodeHex "E184351828DA5094A97C79CABDAAA0BFB87608C32E8829A4DF5340A6F243B78C") ] --- | Invalid test vector data: (signature, key indices, nonce indices, msg index, signer index, expected to fail) invalidTestVectors :: [(ByteString, [Int], [Int], Int, Int, String)] invalidTestVectors = [ (decodeHex "FED54434AD4CFE953FC527DC6A5E5BE8F6234907B7C187559557CE87A0541C46", [0, 1, 2], [0, 1, 2], 0, 0, "Wrong signature (negation of valid signature)") , (decodeHex "012ABBCB52B3016AC03AD82395A1A415C48B93DEF78718E62A7A90052FE224FB", [0, 1, 2], [0, 1, 2], 0, 1, "Wrong signer") ] --- | Error test vector data: (signature, key indices, nonce indices, msg index, signer index, expected error message)-errorTestVectors :: [(ByteString, [Int], [Int], Int, Int, String)]+errorTestVectors :: [(ByteString, [Int], [Int], Int, Int, MuSig2Error)] errorTestVectors =- [ (decodeHex "012ABBCB52B3016AC03AD82395A1A415C48B93DEF78718E62A7A90052FE224FB", [0, 1, 2], [4, 1, 2], 0, 0, "Invalid pubnonce")- , (decodeHex "012ABBCB52B3016AC03AD82395A1A415C48B93DEF78718E62A7A90052FE224FB", [3, 1, 2], [0, 1, 2], 0, 0, "Invalid pubkey")- , (decodeHex "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", [0, 1, 2], [0, 1, 2], 0, 0, "Signature exceeds group size")+ [ (decodeHex "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", [0, 1, 2], [0, 1, 2], 0, 0, PartialSignatureOutOfRange) ] --- | Valid secret key from BIP-0327 test vectors.-testSecKey :: SecKey-testSecKey = SecKey $ bytesToInteger $ decodeHex "7FB9E0E687ADA1EEBF7ECFE2F21E73EBDB51A7D450948DFE8D76D7F2D1007671"---- | Invalid secret nonce with k1 = 0 (from BIP-0327 test vectors secnonce index 1)-invalidSecNonce :: SecNonce-invalidSecNonce = SecNonce{k1 = 0, k2 = 0}+invalidSecNoncePub :: Pub+invalidSecNoncePub = fromMaybe (error "Failed to derive pubkey") $ derive_pub 1 --- | Creates test case from valid test vector data. makeValidTestCase :: Int -> ([Int], [Int], Int, Int, ByteString) -> TestTree makeValidTestCase i (keyIndices, nonceIndices, msgIndex, signerIndex, expectedSig) = testCase ("BIP-0327 valid test vector " <> show (i + 1)) $ do@@ -86,12 +72,8 @@ selectedNonces = map (inputPubNonces !!) nonceIndices msg = testMessages !! msgIndex signature = bytesToInteger expectedSig-- -- Test that the valid signature verifies.- let result = partialSigVerify signature selectedNonces selectedKeys [] msg signerIndex- assertBool "Valid signature should verify" result+ partialSigVerify signature selectedNonces selectedKeys [] msg signerIndex @?= Right True --- | Creates test case from invalid test vector data. makeInvalidTestCase :: Int -> (ByteString, [Int], [Int], Int, Int, String) -> TestTree makeInvalidTestCase i (sigBytes, keyIndices, nonceIndices, msgIndex, signerIndex, comment) = testCase ("BIP-0327 invalid test vector " <> show (i + 1) <> ": " <> comment) $ do@@ -99,49 +81,50 @@ selectedNonces = map (inputPubNonces !!) nonceIndices msg = testMessages !! msgIndex signature = bytesToInteger sigBytes-- -- Test that the invalid signature fails verification- let result = partialSigVerify signature selectedNonces selectedKeys [] msg signerIndex- assertBool ("Invalid signature should not verify: " <> comment) (not result)+ partialSigVerify signature selectedNonces selectedKeys [] msg signerIndex @?= Right False --- | Creates test case from error test vector data-makeErrorTestCase :: Int -> (ByteString, [Int], [Int], Int, Int, String) -> TestTree-makeErrorTestCase i (sigBytes, keyIndices, nonceIndices, msgIndex, signerIndex, comment) =- testCase ("BIP-0327 error test vector " <> show (i + 1) <> ": " <> comment) $ do+makeErrorTestCase :: Int -> (ByteString, [Int], [Int], Int, Int, MuSig2Error) -> TestTree+makeErrorTestCase i (sigBytes, keyIndices, nonceIndices, msgIndex, signerIndex, expectedError) =+ testCase ("BIP-0327 error test vector " <> show (i + 1)) $ do let selectedKeys = map (inputPubkeys !!) keyIndices selectedNonces = map (inputPubNonces !!) nonceIndices msg = testMessages !! msgIndex signature = bytesToInteger sigBytes-- -- Test that this causes an error during verification- result <- try $ evaluate $ partialSigVerify signature selectedNonces selectedKeys [] msg signerIndex- case result of- Left (ErrorCall _) -> return () -- Expected error- Right _ -> assertFailure ("Expected error but verification succeeded: " <> comment)+ partialSigVerify signature selectedNonces selectedKeys [] msg signerIndex @?= Left expectedError --- | Test case for invalid secret nonce (k1 = 0) from BIP-0327 sign_error_test_cases testInvalidSecNonce :: TestTree testInvalidSecNonce =- testCase "BIP-0327 sign error: first secnonce value is out of range" $ do- let selectedKeys = take 3 inputPubkeys -- [0, 1, 2]- selectedNonces = take 3 inputPubNonces -- [0, 1, 2]- msg = head testMessages -- msg_index 0- aggNonce = fromJust $ aggNonces selectedNonces- ctx = mkSessionContext aggNonce selectedKeys [] msg+ testCase "mkSecNonce rejects zero scalars" $+ case mkSecNonce invalidSecNoncePub 1 0 of+ Left (SecretScalarZero "k2") -> pure ()+ Left err -> assertFailure $ "Expected SecretScalarZero \"k2\", got: Left " ++ show err+ Right _ -> assertFailure "Expected Left, got Right" - result <- try $ evaluate $ sign invalidSecNonce testSecKey ctx- case result of- Left (ErrorCall errMsg) ->- assertBool- ("Expected error message containing 'first secret scalar k1 is zero', got: " ++ errMsg)- ("first secret scalar k1 is zero" `isInfixOf` errMsg)- Right _ -> assertFailure "Expected error but signing succeeded"+testContextRejectsInfinityPubkey :: TestTree+testContextRejectsInfinityPubkey =+ testCase "mkSessionContext rejects infinity public keys" $ do+ let aggNonce = unsafeRight $ aggNonces [firstPubNonce]+ case mkSessionContext aggNonce [_CURVE_ZERO] [] "msg" of+ Left PublicKeyAtInfinity -> pure ()+ _ -> assertFailure "Expected Left PublicKeyAtInfinity"+ where+ firstPubNonce = case inputPubNonces of+ nonce : _ -> nonce+ [] -> error "Expected at least one input public nonce" --- | Test vectors from [BIP-0327 `sign_verify_vectors.json`](https://github.com/bitcoin/bips/blob/master/bip-0327/vectors/sign_verify_vectors.json)+testSignerKeyMismatch :: TestTree+testSignerKeyMismatch =+ testCase "sign rejects nonce/key mismatches" $ do+ let signerPub = fromMaybe (error "Failed to derive signer pubkey") $ derive_pub 2+ secNonce = unsafeMkSecNonce signerPub 5 7+ aggNonce = unsafeRight $ aggNonces [unsafeRight $ publicNonce secNonce]+ ctx = unsafeRight $ mkSessionContext aggNonce [signerPub] [] "msg"+ sign secNonce (SecKey 3) ctx @?= Left SecretKeyPublicKeyMismatch+ testSignVerify :: TestTree testSignVerify = testGroup "sign and verify" $ zipWith makeValidTestCase [0 ..] validTestVectors <> zipWith makeInvalidTestCase [0 ..] invalidTestVectors <> zipWith makeErrorTestCase [0 ..] errorTestVectors- <> [testInvalidSecNonce]+ <> [testInvalidSecNonce, testContextRejectsInfinityPubkey, testSignerKeyMismatch]
test/SignVerifyProperty.hs view
@@ -1,15 +1,14 @@+{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-} module SignVerifyProperty (propertySignVerify) where -import Crypto.Curve.Secp256k1 (derive_pub)-import Crypto.Curve.Secp256k1.MuSig2 (SecKey (..), SecNonce (..), aggNonces, mkSessionContext, partialSigVerify, publicNonce, sign)+import Crypto.Curve.Secp256k1.MuSig2 (MuSig2Error (..), aggNonces, mkSessionContext, partialSigVerify, publicNonce, sign) import Crypto.Curve.Secp256k1.MuSig2.Internal (curveOrder) import Data.ByteString (ByteString)-import Data.Maybe (fromJust, fromMaybe) import Test.Tasty import Test.Tasty.QuickCheck as QC-import Util ()+import Util (SignerMaterial (..), unsafeRight) propertySignVerify :: TestTree propertySignVerify =@@ -22,54 +21,48 @@ ] -- | Property: Generated signatures are in the valid range \([0, Q-1]\).-prop_validSignatureRange :: SecNonce -> SecKey -> ByteString -> Property-prop_validSignatureRange secNonce secKey@(SecKey sk) msg =- let pubkey = fromMaybe (error "Failed to derive pubkey") $ derive_pub (fromInteger sk)- pubNonce = publicNonce secNonce+prop_validSignatureRange :: SignerMaterial -> ByteString -> Property+prop_validSignatureRange signer msg =+ let pubNonce = unsafeRight $ publicNonce signer.signerSecNonce pubNonces = [pubNonce]- pubkeys = [pubkey]- aggNonce = fromJust $ aggNonces pubNonces- ctx = mkSessionContext aggNonce pubkeys [] msg- sig = sign secNonce secKey ctx+ pubkeys = [signer.signerPubKey]+ aggNonce = unsafeRight $ aggNonces pubNonces+ ctx = unsafeRight $ mkSessionContext aggNonce pubkeys [] msg+ sig = unsafeRight $ sign signer.signerSecNonce signer.signerSecKey ctx in (sig >= 0) .&&. (sig < curveOrder) -- | Property: A signature created with sign verifies with 'partialSigVerify'.-prop_signVerifyRoundtrip :: SecNonce -> SecKey -> ByteString -> Property-prop_signVerifyRoundtrip secNonce secKey@(SecKey sk) msg =- let pubkey = fromMaybe (error "Failed to derive pubkey") $ derive_pub (fromInteger sk)- pubNonce = publicNonce secNonce+prop_signVerifyRoundtrip :: SignerMaterial -> ByteString -> Property+prop_signVerifyRoundtrip signer msg =+ let pubNonce = unsafeRight $ publicNonce signer.signerSecNonce pubNonces = [pubNonce]- pubkeys = [pubkey]- aggNonce = fromJust $ aggNonces pubNonces- ctx = mkSessionContext aggNonce pubkeys [] msg- sig = sign secNonce secKey ctx- signerIndex = 0 -- We're always the first signer in this test- result = partialSigVerify sig pubNonces pubkeys [] msg signerIndex- in result === True+ pubkeys = [signer.signerPubKey]+ aggNonce = unsafeRight $ aggNonces pubNonces+ ctx = unsafeRight $ mkSessionContext aggNonce pubkeys [] msg+ sig = unsafeRight $ sign signer.signerSecNonce signer.signerSecKey ctx+ result = partialSigVerify sig pubNonces pubkeys [] msg 0+ in result === Right True -- | Property: Signing the same message with the same parameters produces the same signature.-prop_signatureDeterminism :: SecNonce -> SecKey -> ByteString -> Property-prop_signatureDeterminism secNonce secKey@(SecKey sk) msg =- let pubkey = fromMaybe (error "Failed to derive pubkey") $ derive_pub (fromInteger sk)- pubNonce = publicNonce secNonce+prop_signatureDeterminism :: SignerMaterial -> ByteString -> Property+prop_signatureDeterminism signer msg =+ let pubNonce = unsafeRight $ publicNonce signer.signerSecNonce pubNonces = [pubNonce]- pubkeys = [pubkey]- aggNonce = fromJust $ aggNonces pubNonces- ctx = mkSessionContext aggNonce pubkeys [] msg- sig1 = sign secNonce secKey ctx- sig2 = sign secNonce secKey ctx+ pubkeys = [signer.signerPubKey]+ aggNonce = unsafeRight $ aggNonces pubNonces+ ctx = unsafeRight $ mkSessionContext aggNonce pubkeys [] msg+ sig1 = unsafeRight $ sign signer.signerSecNonce signer.signerSecKey ctx+ sig2 = unsafeRight $ sign signer.signerSecNonce signer.signerSecKey ctx in sig1 === sig2 -- | Property: Using an invalid signer index should fail verification.-prop_invalidSignerIndex :: SecNonce -> SecKey -> ByteString -> Property-prop_invalidSignerIndex secNonce secKey@(SecKey sk) msg =- let pubkey = fromMaybe (error "Failed to derive pubkey") $ derive_pub (fromInteger sk)- pubNonce = publicNonce secNonce+prop_invalidSignerIndex :: SignerMaterial -> ByteString -> Property+prop_invalidSignerIndex signer msg =+ let pubNonce = unsafeRight $ publicNonce signer.signerSecNonce pubNonces = [pubNonce]- pubkeys = [pubkey]- aggNonce = fromJust $ aggNonces pubNonces- ctx = mkSessionContext aggNonce pubkeys [] msg- sig = sign secNonce secKey ctx- invalidIndex = 1 -- Out of bounds index (only 1 signer at index 0)- result = partialSigVerify sig pubNonces pubkeys [] msg invalidIndex- in expectFailure (result === True)+ pubkeys = [signer.signerPubKey]+ aggNonce = unsafeRight $ aggNonces pubNonces+ ctx = unsafeRight $ mkSessionContext aggNonce pubkeys [] msg+ sig = unsafeRight $ sign signer.signerSecNonce signer.signerSecKey ctx+ result = partialSigVerify sig pubNonces pubkeys [] msg 1+ in result === Left (InvalidSignerIndex 1)
test/SignVerifyTweakProperty.hs view
@@ -1,15 +1,15 @@+{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-} module SignVerifyTweakProperty (propertySignVerifyTweak) where -import Crypto.Curve.Secp256k1 (derive_pub)-import Crypto.Curve.Secp256k1.MuSig2 (SecKey (..), SecNonce (..), Tweak (..), aggNonces, mkSessionContext, partialSigVerify, publicNonce, sign)+import Crypto.Curve.Secp256k1 (Pub)+import Crypto.Curve.Secp256k1.MuSig2 (PubNonce, SessionContext, Tweak (..), aggNonces, mkSessionContext, partialSigVerify, publicNonce, sign) import Crypto.Curve.Secp256k1.MuSig2.Internal (curveOrder) import Data.ByteString (ByteString)-import Data.Maybe (fromJust, fromMaybe) import Test.Tasty import Test.Tasty.QuickCheck-import Util ()+import Util (SignerMaterial (..), unsafeRight) propertySignVerifyTweak :: TestTree propertySignVerifyTweak =@@ -22,92 +22,67 @@ , testProperty "Plain Tweaks Are Commutative" prop_plainTweaksCommutative ] +mkTweakedContext :: SignerMaterial -> [Tweak] -> ByteString -> ([PubNonce], [Pub], SessionContext)+mkTweakedContext signer tweaks msg =+ let pubNonce = unsafeRight $ publicNonce signer.signerSecNonce+ pubNonces = [pubNonce]+ pubkeys = [signer.signerPubKey]+ aggNonce = unsafeRight $ aggNonces pubNonces+ ctx = unsafeRight $ mkSessionContext aggNonce pubkeys tweaks msg+ in (pubNonces, pubkeys, ctx)+ -- | Property: Generated signatures with tweaks are in the valid range \([0, Q-1]\).-prop_validSignatureRangeWithTweaks :: SecNonce -> SecKey -> Property-prop_validSignatureRangeWithTweaks secNonce secKey@(SecKey sk) =- forAll (resize 5 $ listOf arbitrary) $ \tweaks ->- forAll arbitrary $ \msg ->- let pubkey = fromMaybe (error "Failed to derive pubkey") $ derive_pub (fromInteger sk)- pubNonce = publicNonce secNonce- pubNonces = [pubNonce]- pubkeys = [pubkey]- aggNonce = fromJust $ aggNonces pubNonces- ctx = mkSessionContext aggNonce pubkeys tweaks msg- sig = sign secNonce secKey ctx+prop_validSignatureRangeWithTweaks :: SignerMaterial -> Property+prop_validSignatureRangeWithTweaks signer =+ forAll (resize 5 $ listOf arbitrary :: Gen [Tweak]) $ \tweaks ->+ forAll (arbitrary :: Gen ByteString) $ \msg ->+ let (_, _, ctx) = mkTweakedContext signer tweaks msg+ sig = unsafeRight $ sign signer.signerSecNonce signer.signerSecKey ctx in (sig >= 0) .&&. (sig < curveOrder) -- | Property: A signature created with tweaks verifies with 'partialSigVerify'.-prop_signVerifyRoundtripWithTweaks :: SecNonce -> SecKey -> Property-prop_signVerifyRoundtripWithTweaks secNonce secKey@(SecKey sk) =- forAll (resize 5 $ listOf arbitrary) $ \tweaks ->- forAll arbitrary $ \msg ->- let pubkey = fromMaybe (error "Failed to derive pubkey") $ derive_pub (fromInteger sk)- pubNonce = publicNonce secNonce- pubNonces = [pubNonce]- pubkeys = [pubkey]- aggNonce = fromJust $ aggNonces pubNonces- ctx = mkSessionContext aggNonce pubkeys tweaks msg- sig = sign secNonce secKey ctx- signerIndex = 0 -- We're always the first signer in this test- result = partialSigVerify sig pubNonces pubkeys tweaks msg signerIndex- in result === True+prop_signVerifyRoundtripWithTweaks :: SignerMaterial -> Property+prop_signVerifyRoundtripWithTweaks signer =+ forAll (resize 5 $ listOf arbitrary :: Gen [Tweak]) $ \tweaks ->+ forAll (arbitrary :: Gen ByteString) $ \msg ->+ let (pubNonces, pubkeys, ctx) = mkTweakedContext signer tweaks msg+ sig = unsafeRight $ sign signer.signerSecNonce signer.signerSecKey ctx+ result = partialSigVerify sig pubNonces pubkeys tweaks msg 0+ in result === Right True -- | Property: Signing the same message with the same tweaks produces the same signature.-prop_signatureDeterminismWithTweaks :: SecNonce -> SecKey -> Property-prop_signatureDeterminismWithTweaks secNonce secKey@(SecKey sk) =- forAll (resize 5 $ listOf arbitrary) $ \tweaks ->- forAll arbitrary $ \msg ->- let pubkey = fromMaybe (error "Failed to derive pubkey") $ derive_pub (fromInteger sk)- pubNonce = publicNonce secNonce- pubNonces = [pubNonce]- pubkeys = [pubkey]- aggNonce = fromJust $ aggNonces pubNonces- ctx = mkSessionContext aggNonce pubkeys tweaks msg- sig1 = sign secNonce secKey ctx- sig2 = sign secNonce secKey ctx+prop_signatureDeterminismWithTweaks :: SignerMaterial -> Property+prop_signatureDeterminismWithTweaks signer =+ forAll (resize 5 $ listOf arbitrary :: Gen [Tweak]) $ \tweaks ->+ forAll (arbitrary :: Gen ByteString) $ \msg ->+ let (_, _, ctx) = mkTweakedContext signer tweaks msg+ sig1 = unsafeRight $ sign signer.signerSecNonce signer.signerSecKey ctx+ sig2 = unsafeRight $ sign signer.signerSecNonce signer.signerSecKey ctx in sig1 === sig2 -- | Property: Empty tweaks should produce the same result as no tweaks.-prop_emptyTweaksEqualsNoTweaks :: SecNonce -> SecKey -> ByteString -> Property-prop_emptyTweaksEqualsNoTweaks secNonce secKey@(SecKey sk) msg =- let pubkey = fromMaybe (error "Failed to derive pubkey") $ derive_pub (fromInteger sk)- pubNonce = publicNonce secNonce- pubNonces = [pubNonce]- pubkeys = [pubkey]- aggNonce = fromJust $ aggNonces pubNonces-- -- Context with empty tweaks- ctxWithEmptyTweaks = mkSessionContext aggNonce pubkeys [] msg- sigWithEmptyTweaks = sign secNonce secKey ctxWithEmptyTweaks-- -- Context with no tweaks parameter (though we still pass empty list)- ctxWithNoTweaks = mkSessionContext aggNonce pubkeys [] msg- sigWithNoTweaks = sign secNonce secKey ctxWithNoTweaks+prop_emptyTweaksEqualsNoTweaks :: SignerMaterial -> ByteString -> Property+prop_emptyTweaksEqualsNoTweaks signer msg =+ let (_, _, ctxWithEmptyTweaks) = mkTweakedContext signer [] msg+ sigWithEmptyTweaks = unsafeRight $ sign signer.signerSecNonce signer.signerSecKey ctxWithEmptyTweaks+ (_, _, ctxWithNoTweaks) = mkTweakedContext signer [] msg+ sigWithNoTweaks = unsafeRight $ sign signer.signerSecNonce signer.signerSecKey ctxWithNoTweaks in sigWithEmptyTweaks === sigWithNoTweaks -- | Property: Plain tweaks are commutative.-prop_plainTweaksCommutative :: SecNonce -> SecKey -> Integer -> Integer -> ByteString -> Property-prop_plainTweaksCommutative secNonce secKey@(SecKey sk) t1 t2 msg =+prop_plainTweaksCommutative :: SignerMaterial -> Integer -> Integer -> ByteString -> Property+prop_plainTweaksCommutative signer t1 t2 msg = t1 > 0 && t1 < curveOrder && t2 > 0 && t2 < curveOrder- && t1- /= t2- ==> let pubkey = fromMaybe (error "Failed to derive pubkey") $ derive_pub (fromInteger sk)- pubNonce = publicNonce secNonce- pubNonces = [pubNonce]- pubkeys = [pubkey]- aggNonce = fromJust $ aggNonces pubNonces-- -- First order: [PlainTweak t1, PlainTweak t2]- tweak1 = PlainTweak t1- tweak2 = PlainTweak t2- ctx1 = mkSessionContext aggNonce pubkeys [tweak1, tweak2] msg- sig1 = sign secNonce secKey ctx1-- -- Second order: [PlainTweak t2, PlainTweak t1]- ctx2 = mkSessionContext aggNonce pubkeys [tweak2, tweak1] msg- sig2 = sign secNonce secKey ctx2- in -- Plain tweaks are commutative, order shouldn't matter- sig1 === sig2+ && t1 /= t2+ ==> let+ tweak1 = PlainTweak t1+ tweak2 = PlainTweak t2+ (_, _, ctx1) = mkTweakedContext signer [tweak1, tweak2] msg+ sig1 = unsafeRight $ sign signer.signerSecNonce signer.signerSecKey ctx1+ (_, _, ctx2) = mkTweakedContext signer [tweak2, tweak1] msg+ sig2 = unsafeRight $ sign signer.signerSecNonce signer.signerSecKey ctx2+ in+ sig1 === sig2
test/Tweak.hs view
@@ -1,31 +1,19 @@ {-# LANGUAGE OverloadedStrings #-}-{-# OPTIONS_GHC -Wno-x-partial #-} module Tweak (testTweak) where -import Control.Exception (ErrorCall (..), evaluate, try)-import Crypto.Curve.Secp256k1 (Pub)-import Crypto.Curve.Secp256k1.MuSig2 (PubNonce (..), SecKey (..), SecNonce (..), Tweak (..), aggNonces, mkSessionContext, partialSigVerify, sign)+import Crypto.Curve.Secp256k1 (Pub, derive_pub)+import Crypto.Curve.Secp256k1.MuSig2 (MuSig2Error (..), PubNonce (..), SecKey (..), Tweak (..), aggNonces, mkSessionContext, partialSigVerify, sign) import Crypto.Curve.Secp256k1.MuSig2.Internal (bytesToInteger) import Data.ByteString (ByteString)-import Data.List (isInfixOf)-import Data.Maybe (fromJust) import Test.Tasty import Test.Tasty.HUnit-import Util (decodeHex, parsePoint, parsePubNonce, parseScalar)+import Util (decodeHex, parsePoint, parsePubNonce, parseScalar, unsafeMkSecNonce, unsafeRight) -- | Secret key from BIP-0327 test vectors. testSecKey :: SecKey testSecKey = SecKey $ parseScalar "7FB9E0E687ADA1EEBF7ECFE2F21E73EBDB51A7D450948DFE8D76D7F2D1007671" --- | Secret nonce from BIP-0327 test vectors.-testSecNonce :: SecNonce-testSecNonce =- SecNonce- { k1 = parseScalar "508B81A611F100A6B2B6B29656590898AF488BCF2E1F55CF22E5CFB84421FE61"- , k2 = parseScalar "FA27FD49B1D50085B481285E1CA205D55C82CC1B31FF5CD54A489829355901F7"- }- -- | Input public keys from BIP-0327 test vectors. inputPubkeys :: [Pub] inputPubkeys =@@ -90,60 +78,47 @@ makeValidTestCase :: Int -> ([Int], [Int], [Int], [Bool], Int, ByteString, String) -> TestTree makeValidTestCase i (keyIndices, nonceIndices, tweakIndices, isXOnly, signerIndex, expectedSig, comment) = testCase ("BIP-0327 valid test vector " <> show (i + 1) <> ": " <> comment) $ do- let selectedKeys = map (inputPubkeys !!) keyIndices+ let signerPub = case derive_pub (fromInteger (parseScalar "7FB9E0E687ADA1EEBF7ECFE2F21E73EBDB51A7D450948DFE8D76D7F2D1007671")) of+ Just pub -> pub+ Nothing -> error "Failed to derive test signer pubkey"+ selectedKeys = map (inputPubkeys !!) keyIndices selectedNonces = map (inputPubNonces !!) nonceIndices tweaks = createTweaks tweakIndices isXOnly expectedSignature = bytesToInteger expectedSig- aggNonce = fromJust $ aggNonces selectedNonces- ctx = mkSessionContext aggNonce selectedKeys tweaks testMessage-- -- Test 1: Generate signature using the sign function (with error handling).- signResult <- try $ evaluate $ sign testSecNonce testSecKey ctx- case signResult of- Left (ErrorCall errMsg) -> do- -- If sign fails, this indicates a potential bug in the implementation.- assertFailure ("POTENTIAL BUG: Sign function failed: " <> errMsg <> " for test: " <> comment)- Right generatedSig -> do- -- Test 2: Verify that the generated signature is valid.- let generatedSigVerifies = partialSigVerify generatedSig selectedNonces selectedKeys tweaks testMessage signerIndex- assertBool ("Generated signature should verify: " <> comment) generatedSigVerifies-- -- Test 3: Check if the expected signature from BIP-0327 test vectors verifies.- let expectedSigVerifies = partialSigVerify expectedSignature selectedNonces selectedKeys tweaks testMessage signerIndex+ aggNonce = unsafeRight $ aggNonces selectedNonces+ ctx = unsafeRight $ mkSessionContext aggNonce selectedKeys tweaks testMessage+ testSecNonce = unsafeMkSecNonce signerPub (parseScalar "508B81A611F100A6B2B6B29656590898AF488BCF2E1F55CF22E5CFB84421FE61") (parseScalar "FA27FD49B1D50085B481285E1CA205D55C82CC1B31FF5CD54A489829355901F7")+ generatedSig = unsafeRight $ sign testSecNonce testSecKey ctx+ generatedSigVerifies = partialSigVerify generatedSig selectedNonces selectedKeys tweaks testMessage signerIndex+ expectedSigVerifies = partialSigVerify expectedSignature selectedNonces selectedKeys tweaks testMessage signerIndex - -- Test 4: Compare generated signature with expected signature.- -- Note: If they don't match, it could indicate a difference in implementation or test vector interpretation.- if expectedSigVerifies- then do- -- If the expected signature verifies, we can compare it with our generated one.- if generatedSig == expectedSignature- then assertBool ("Generated signature matches expected: " <> comment) True- else do- -- They don't match - this could be due to different nonce generation or other implementation details.- -- For now, we accept this as long as both signatures verify correctly- assertBool ("Different but valid signatures (implementation variation): " <> comment) True- else do- -- Expected signature doesn't verify - this suggests our interpretation might be wrong.- -- But if our generated signature verifies, the core functionality works- assertBool ("Expected signature from test vector doesn't verify, but generated signature does: " <> comment) generatedSigVerifies+ signerPub @?= (selectedKeys !! signerIndex)+ generatedSigVerifies @?= Right True+ if expectedSigVerifies == Right True+ then assertBool ("Generated signature matches expected or remains valid: " <> comment) (generatedSig == expectedSignature || generatedSigVerifies == Right True)+ else assertBool ("Expected signature from test vector doesn't verify, but generated signature does: " <> comment) (generatedSigVerifies == Right True) -- | Creates test case from error test vector data. makeErrorTestCase :: Int -> ([Int], [Bool], String, String) -> TestTree makeErrorTestCase i (tweakIndices, isXOnly, expectedError, comment) = testCase ("BIP-0327 error test vector " <> show (i + 1) <> ": " <> comment) $ do- let selectedKeys = [inputPubkeys !! 1, inputPubkeys !! 2, head inputPubkeys] -- [1, 2, 0]- selectedNonces = [inputPubNonces !! 1, inputPubNonces !! 2, head inputPubNonces] -- [1, 2, 0]+ let selectedKeys = [inputPubkeys !! 1, inputPubkeys !! 2, firstInputPubKey] -- [1, 2, 0]+ selectedNonces = [inputPubNonces !! 1, inputPubNonces !! 2, firstInputPubNonce] -- [1, 2, 0] tweaks = createTweaks tweakIndices isXOnly- aggNonce = fromJust $ aggNonces selectedNonces-- -- Test that creating a session context with invalid tweaks causes an error.- result <- try $ evaluate $ mkSessionContext aggNonce selectedKeys tweaks testMessage- case result of- Left (ErrorCall errMsg) ->- assertBool- ("Expected error message containing '" <> expectedError <> "', got: " <> errMsg)- (expectedError `isInfixOf` errMsg)- Right _ -> assertFailure ("Expected error but session context creation succeeded: " <> comment)+ aggNonce = unsafeRight $ aggNonces selectedNonces+ assertBool+ (expectedError <> ": " <> comment)+ ( case mkSessionContext aggNonce selectedKeys tweaks testMessage of+ Left (TweakOutOfRange _) -> True+ _ -> False+ )+ where+ firstInputPubKey = case inputPubkeys of+ pub : _ -> pub+ [] -> error "Expected at least one input public key"+ firstInputPubNonce = case inputPubNonces of+ nonce : _ -> nonce+ [] -> error "Expected at least one input public nonce" -- | Test vectors from [BIP-0327 `tweak_vectors.json`](https://github.com/bitcoin/bips/blob/master/bip-0327/vectors/tweak_vectors.json). testTweak :: TestTree
test/Util.hs view
@@ -3,11 +3,11 @@ {-# LANGUAGE RecordWildCards #-} {-# OPTIONS_GHC -Wno-orphans #-} -module Util (parsePoint, parseScalar, parsePubNonce, extractXOnly, decodeHex, Rand32 (..), Scalar (..)) where+module Util (parsePoint, parseScalar, parsePubNonce, extractXOnly, decodeHex, Rand32 (..), Scalar (..), SignerMaterial (..), unsafeRight, unsafeMkSecNonce) where -import Crypto.Curve.Secp256k1 (Projective, Pub, mul, parse_point, serialize_point, _CURVE_G, _CURVE_ZERO)-import Crypto.Curve.Secp256k1.MuSig2 (PubNonce (..), SecKey (..), SecNonce (..), SecNonceGenParams (..), Tweak (..))-import Crypto.Curve.Secp256k1.MuSig2.Internal (curveOrder)+import Crypto.Curve.Secp256k1 (Projective, Pub, derive_pub, mul, parse_point, serialize_point, _CURVE_G, _CURVE_ZERO)+import Crypto.Curve.Secp256k1.MuSig2 (PubNonce (..), SecKey (..), SecNonce, SecNonceGenParams (..), Tweak (..), mkSecNonce)+import Crypto.Curve.Secp256k1.MuSig2.Internal (curveOrder, secNonceScalars) import Data.ByteString (ByteString) import qualified Data.ByteString as BS import qualified Data.ByteString.Base16 as B16@@ -62,6 +62,11 @@ len <- choose (0, 1024) :: Gen Int -- Limit size to avoid excessive memory use BS.pack <$> vectorOf len arbitrary +genNonZeroPub :: Gen Pub+genNonZeroPub = do+ scalar <- choose (1, curveOrder - 1)+ pure $ fromMaybe (error "Failed to derive non-zero public key") $ derive_pub (fromInteger scalar)+ -- | 'Arbitrary' instance for 'ByteString'. instance Arbitrary ByteString where arbitrary = arbitraryBS@@ -80,15 +85,29 @@ instance Arbitrary Rand32 where arbitrary = Rand32 . BS.pack <$> vectorOf 32 arbitrary +unsafeRight :: (Show e) => Either e a -> a+unsafeRight = either (error . show) id++unsafeMkSecNonce :: Pub -> Integer -> Integer -> SecNonce+unsafeMkSecNonce pub k1 k2 = unsafeRight (mkSecNonce pub k1 k2)++data SignerMaterial = SignerMaterial+ { signerSecKey :: SecKey+ , signerPubKey :: Pub+ , signerSecNonce :: SecNonce+ }+ {- | 'Arbitrary' instance for 'SecNonceGenParams'. Slightly biased towards 'Just' than 'Nothing'. -} instance Arbitrary SecNonceGenParams where arbitrary = do- _pk <- arbitrary _sk <- frequency [(2, return Nothing), (3, genMaybeSecKey)]- _aggpk <- frequency [(2, return Nothing), (3, Just <$> arbitrary)]+ _pk <- case _sk of+ Nothing -> genNonZeroPub+ Just (SecKey sk) -> pure $ fromMaybe (error "Failed to derive pubkey from generated secret key") $ derive_pub (fromInteger sk)+ _aggpk <- frequency [(2, return Nothing), (3, Just <$> genNonZeroPub)] _msg <- frequency [(2, return Nothing), (3, Just <$> arbitraryBS)] _extraIn <- frequency [(2, return Nothing), (3, Just <$> arbitraryBS)] return SecNonceGenParams{..}@@ -133,17 +152,20 @@ -- | 'Arbitrary' instace of 'SecNonce'. instance Arbitrary SecNonce where arbitrary = do- k1 <- choose (1, curveOrder - 1) -- Ensure non-zero- k2 <- choose (1, curveOrder - 1) -- Ensure non-zero- return SecNonce{k1 = k1, k2 = k2}+ scalar <- choose (1, curveOrder - 1)+ let pub = fromMaybe (error "Failed to derive pubkey for SecNonce") $ derive_pub (fromInteger scalar)+ k1 <- choose (1, curveOrder - 1)+ k2 <- choose (1, curveOrder - 1)+ pure (unsafeMkSecNonce pub k1 k2) {- | 'Show' instance for 'SecNonce'. Should only used for testing purposes, hence why it is only defined in this test module. -} instance Show SecNonce where- show (SecNonce k1 k2) =- "SecNonce { k1=" ++ show k1 ++ ", k2=" ++ show k2 ++ "}"+ show secNonce =+ let (k1, k2) = secNonceScalars secNonce+ in "SecNonce { k1=" ++ show k1 ++ ", k2=" ++ show k2 ++ "}" -- | 'Arbitrary' instace of 'SecKey'. instance Arbitrary SecKey where@@ -157,6 +179,28 @@ -} instance Show SecKey where show (SecKey int) = "SecKey " ++ show int++instance Arbitrary SignerMaterial where+ arbitrary = do+ sk <- choose (1, curveOrder - 1)+ let secKey = SecKey sk+ pub = fromMaybe (error "Failed to derive pubkey for signer material") $ derive_pub (fromInteger sk)+ k1 <- choose (1, curveOrder - 1)+ k2 <- choose (1, curveOrder - 1)+ pure+ SignerMaterial+ { signerSecKey = secKey+ , signerPubKey = pub+ , signerSecNonce = unsafeMkSecNonce pub k1 k2+ }++instance Show SignerMaterial where+ show signer =+ "SignerMaterial { signerSecKey = "+ ++ show (signerSecKey signer)+ ++ ", signerSecNonce = "+ ++ show (signerSecNonce signer)+ ++ "}" -- | 'Arbitrary' instance for 'Tweak'. instance Arbitrary Tweak where