packages feed

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 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