secp256k1

Pure Haskell Schnorr, ECDSA on the elliptic curve secp256k1 (docs.ppad.tech/secp256k1).
git clone git://git.ppad.tech/secp256k1.git
Log | Files | Refs | README | LICENSE

commit 4a1df2ca61233e4d19777b64bf5ead0ab3f1192e
parent 697bc7a07c3d0ec26b49be175fed2af4210e9751
Author: Jared Tobin <jared@jtobin.io>
Date:   Tue, 17 Jun 2025 10:43:46 +0400

lib: make sig/verify, ecdh functions total

Diffstat:
Mlib/Crypto/Curve/Secp256k1.hs | 386++++++++++++++++++++++++++++++++++++++-----------------------------------------
1 file changed, 186 insertions(+), 200 deletions(-)

diff --git a/lib/Crypto/Curve/Secp256k1.hs b/lib/Crypto/Curve/Secp256k1.hs @@ -96,6 +96,7 @@ import Data.Bits ((.|.)) import qualified Data.Bits as B import qualified Data.ByteString as BS import qualified Data.ByteString.Unsafe as BU +import qualified Data.Maybe as M (isJust) import qualified Data.Primitive.Array as A import Data.STRef import Data.Word (Word8, Word64) @@ -554,8 +555,8 @@ double (Projective x y z) = runST $ do Projective <$> readSTRef x3 <*> readSTRef y3 <*> readSTRef z3 -- Timing-safe scalar multiplication of secp256k1 points. -mul' :: Projective -> Integer -> Maybe Projective -mul' p _SECRET = do +mul :: Projective -> Integer -> Maybe Projective +mul p _SECRET = do guard (ge _SECRET) pure $! loop (0 :: Int) _CURVE_ZERO _CURVE_G p _SECRET where @@ -567,13 +568,13 @@ mul' p _SECRET = do in if I.integerTestBit m 0 then loop (succ j) (add acc d) f nd nm else loop (succ j) acc (add f d) nd nm -{-# INLINE mul' #-} +{-# INLINE mul #-} -- Timing-unsafe scalar multiplication of secp256k1 points. -- -- Don't use this function if the scalar could potentially be a secret. -mul_unsafe' :: Projective -> Integer -> Maybe Projective -mul_unsafe' p n +mul_unsafe :: Projective -> Integer -> Maybe Projective +mul_unsafe p n | n == 0 = pure $! _CURVE_ZERO | not (ge n) = Nothing | otherwise = pure $! loop _CURVE_ZERO p n @@ -586,39 +587,39 @@ mul_unsafe' p n nr = if I.integerTestBit m 0 then add r d else r in loop nr nd nm --- Timing-safe scalar multiplication of secp256k1 points. -mul :: Projective -> Integer -> Projective -mul p _SECRET - | not (ge _SECRET) = error "ppad-secp256k1 (mul): scalar not in group" - | otherwise = loop (0 :: Int) _CURVE_ZERO _CURVE_G p _SECRET - where - loop !j !acc !f !d !m - | j == _CURVE_Q_BITS = acc - | otherwise = - let nd = double d - nm = I.integerShiftR m 1 - in if I.integerTestBit m 0 - then loop (succ j) (add acc d) f nd nm - else loop (succ j) acc (add f d) nd nm -{-# INLINE mul #-} - --- Timing-unsafe scalar multiplication of secp256k1 points. --- --- Don't use this function if the scalar could potentially be a secret. -mul_unsafe :: Projective -> Integer -> Projective -mul_unsafe p n - | n == 0 = _CURVE_ZERO - | not (ge n) = - error "ppad-secp256k1 (mul_unsafe): scalar not in group" - | otherwise = loop _CURVE_ZERO p n - where - loop !r !d m - | m <= 0 = r - | otherwise = - let nd = double d - nm = I.integerShiftR m 1 - nr = if I.integerTestBit m 0 then add r d else r - in loop nr nd nm +-- -- Timing-safe scalar multiplication of secp256k1 points. +-- mul :: Projective -> Integer -> Projective +-- mul p _SECRET +-- | not (ge _SECRET) = error "ppad-secp256k1 (mul): scalar not in group" +-- | otherwise = loop (0 :: Int) _CURVE_ZERO _CURVE_G p _SECRET +-- where +-- loop !j !acc !f !d !m +-- | j == _CURVE_Q_BITS = acc +-- | otherwise = +-- let nd = double d +-- nm = I.integerShiftR m 1 +-- in if I.integerTestBit m 0 +-- then loop (succ j) (add acc d) f nd nm +-- else loop (succ j) acc (add f d) nd nm +-- {-# INLINE mul #-} + +-- -- Timing-unsafe scalar multiplication of secp256k1 points. +-- -- +-- -- Don't use this function if the scalar could potentially be a secret. +-- mul_unsafe :: Projective -> Integer -> Projective +-- mul_unsafe p n +-- | n == 0 = _CURVE_ZERO +-- | not (ge n) = +-- error "ppad-secp256k1 (mul_unsafe): scalar not in group" +-- | otherwise = loop _CURVE_ZERO p n +-- where +-- loop !r !d m +-- | m <= 0 = r +-- | otherwise = +-- let nd = double d +-- nm = I.integerShiftR m 1 +-- nr = if I.integerTestBit m 0 then add r d else r +-- in loop nr nd nm @@ -670,49 +671,49 @@ _precompute ctxW = Context {..} where let nb = add b p in loop_j p (nb : acc) nb (succ j) --- Timing-safe wNAF (w-ary non-adjacent form) scalar multiplication of --- secp256k1 points. -mul_wnaf :: Context -> Integer -> Projective -mul_wnaf Context {..} _SECRET - | not (ge _SECRET) = error "ppad-secp256k1 (mul_wnaf): invalid scalar" - | otherwise = loop 0 _CURVE_ZERO _CURVE_G _SECRET - where - wins = 256 `quot` ctxW + 1 - wsize = 2 ^ (ctxW - 1) - mask = 2 ^ ctxW - 1 - mnum = 2 ^ ctxW - - loop !w !acc !f !n - | w == wins = acc - | otherwise = - let !off0 = w * fi wsize - - !b0 = n `I.integerAnd` mask - !n0 = n `I.integerShiftR` fi ctxW - - !(Pair b1 n1) | b0 > wsize = Pair (b0 - mnum) (n0 + 1) - | otherwise = Pair b0 n0 - - !c0 = B.testBit w 0 - !c1 = b1 < 0 - - !off1 = off0 + fi (abs b1) - 1 - - in if b1 == 0 - then let !pr = A.indexArray ctxArray off0 - !pt | c0 = neg pr - | otherwise = pr - in loop (w + 1) acc (add f pt) n1 - else let !pr = A.indexArray ctxArray off1 - !pt | c1 = neg pr - | otherwise = pr - in loop (w + 1) (add acc pt) f n1 -{-# INLINE mul_wnaf #-} +-- -- Timing-safe wNAF (w-ary non-adjacent form) scalar multiplication of +-- -- secp256k1 points. +-- mul_wnaf :: Context -> Integer -> Projective +-- mul_wnaf Context {..} _SECRET +-- | not (ge _SECRET) = error "ppad-secp256k1 (mul_wnaf): invalid scalar" +-- | otherwise = loop 0 _CURVE_ZERO _CURVE_G _SECRET +-- where +-- wins = 256 `quot` ctxW + 1 +-- wsize = 2 ^ (ctxW - 1) +-- mask = 2 ^ ctxW - 1 +-- mnum = 2 ^ ctxW +-- +-- loop !w !acc !f !n +-- | w == wins = acc +-- | otherwise = +-- let !off0 = w * fi wsize +-- +-- !b0 = n `I.integerAnd` mask +-- !n0 = n `I.integerShiftR` fi ctxW +-- +-- !(Pair b1 n1) | b0 > wsize = Pair (b0 - mnum) (n0 + 1) +-- | otherwise = Pair b0 n0 +-- +-- !c0 = B.testBit w 0 +-- !c1 = b1 < 0 +-- +-- !off1 = off0 + fi (abs b1) - 1 +-- +-- in if b1 == 0 +-- then let !pr = A.indexArray ctxArray off0 +-- !pt | c0 = neg pr +-- | otherwise = pr +-- in loop (w + 1) acc (add f pt) n1 +-- else let !pr = A.indexArray ctxArray off1 +-- !pt | c1 = neg pr +-- | otherwise = pr +-- in loop (w + 1) (add acc pt) f n1 +-- {-# INLINE mul_wnaf #-} -- Timing-safe wNAF (w-ary non-adjacent form) scalar multiplication of -- secp256k1 points. -mul_wnaf' :: Context -> Integer -> Maybe Projective -mul_wnaf' Context {..} _SECRET = do +mul_wnaf :: Context -> Integer -> Maybe Projective +mul_wnaf Context {..} _SECRET = do guard (ge _SECRET) pure $! loop 0 _CURVE_ZERO _CURVE_G _SECRET where @@ -746,7 +747,7 @@ mul_wnaf' Context {..} _SECRET = do !pt | c1 = neg pr | otherwise = pr in loop (w + 1) (add acc pt) f n1 -{-# INLINE mul_wnaf' #-} +{-# INLINE mul_wnaf #-} -- | Derive a public key (i.e., a secp256k1 point) from the provided -- secret. @@ -755,12 +756,8 @@ mul_wnaf' Context {..} _SECRET = do -- >>> sk <- fmap parse_int256 (E.getEntropy 32) -- >>> derive_pub sk -- "<secp256k1 point>" -derive_pub :: Integer -> Pub -derive_pub _SECRET - | not (ge _SECRET) = - error "ppad-secp256k1 (derive_pub): invalid secret key" - | otherwise = - mul _CURVE_G _SECRET +derive_pub :: Integer -> Maybe Pub +derive_pub = mul _CURVE_G {-# NOINLINE derive_pub #-} -- | The same as 'derive_pub', except uses a 'Context' to optimise @@ -771,12 +768,8 @@ derive_pub _SECRET -- >>> let !tex = precompute -- >>> derive_pub' tex sk -- "<secp256k1 point>" -derive_pub' :: Context -> Integer -> Pub -derive_pub' tex _SECRET - | not (ge _SECRET) = - error "ppad-secp256k1 (derive_pub): invalid secret key" - | otherwise = - mul_wnaf tex _SECRET +derive_pub' :: Context -> Integer -> Maybe Pub +derive_pub' = mul_wnaf {-# NOINLINE derive_pub' #-} -- parsing -------------------------------------------------------------------- @@ -887,7 +880,7 @@ sign_schnorr :: Integer -- ^ secret key -> BS.ByteString -- ^ message -> BS.ByteString -- ^ 32 bytes of auxilliary random data - -> BS.ByteString -- ^ 64-byte Schnorr signature + -> Maybe BS.ByteString -- ^ 64-byte Schnorr signature sign_schnorr = _sign_schnorr (mul _CURVE_G) -- | The same as 'sign_schnorr', except uses a 'Context' to optimise @@ -906,50 +899,48 @@ sign_schnorr' -> Integer -- ^ secret key -> BS.ByteString -- ^ message -> BS.ByteString -- ^ 32 bytes of auxilliary random data - -> BS.ByteString -- ^ 64-byte Schnorr signature + -> Maybe BS.ByteString -- ^ 64-byte Schnorr signature sign_schnorr' tex = _sign_schnorr (mul_wnaf tex) _sign_schnorr - :: (Integer -> Projective) -- partially-applied multiplication function + :: (Integer -> Maybe Projective) -- partially-applied multiplication function -> Integer -- secret key -> BS.ByteString -- message -> BS.ByteString -- 32 bytes of auxilliary random data - -> BS.ByteString -_sign_schnorr _mul _SECRET m a - | not (ge _SECRET) = error "ppad-secp256k1 (sign_schnorr): invalid secret key" - | otherwise = - let p_proj = _mul _SECRET - Affine x_p y_p = affine p_proj - d | I.integerTestBit y_p 0 = _CURVE_Q - _SECRET - | otherwise = _SECRET + -> Maybe BS.ByteString +_sign_schnorr _mul _SECRET m a = do + p_proj <- _mul _SECRET + let Affine x_p y_p = affine p_proj + d | I.integerTestBit y_p 0 = _CURVE_Q - _SECRET + | otherwise = _SECRET - bytes_d = unroll32 d - h_a = hash_aux a - t = xor bytes_d h_a + bytes_d = unroll32 d + h_a = hash_aux a + t = xor bytes_d h_a - bytes_p = unroll32 x_p - rand = hash_nonce (t <> bytes_p <> m) + bytes_p = unroll32 x_p + rand = hash_nonce (t <> bytes_p <> m) - k' = modQ (roll32 rand) + k' = modQ (roll32 rand) - in if k' == 0 -- negligible probability - then error "ppad-secp256k1 (sign_schnorr): invalid k" - else - let Affine x_r y_r = affine (_mul k') - k | I.integerTestBit y_r 0 = _CURVE_Q - k' - | otherwise = k' + if k' == 0 -- negligible probability + then Nothing -- XX handle me + else do + pt <- _mul k' + let Affine x_r y_r = affine pt + k | I.integerTestBit y_r 0 = _CURVE_Q - k' + | otherwise = k' - bytes_r = unroll32 x_r - e = modQ . roll32 . hash_challenge - $ bytes_r <> bytes_p <> m + bytes_r = unroll32 x_r + e = modQ . roll32 . hash_challenge + $ bytes_r <> bytes_p <> m - bytes_ked = unroll32 (modQ (k + e * d)) + bytes_ked = unroll32 (modQ (k + e * d)) - sig = bytes_r <> bytes_ked + sig = bytes_r <> bytes_ked - in if verify_schnorr m p_proj sig - then sig - else error "ppad-secp256k1 (sign_schnorr): invalid signature" + guard (verify_schnorr m p_proj sig) + pure $! sig {-# INLINE _sign_schnorr #-} -- | Verify a 64-byte Schnorr signature for the provided message with @@ -986,27 +977,26 @@ verify_schnorr' verify_schnorr' tex = _verify_schnorr (mul_wnaf tex) _verify_schnorr - :: (Integer -> Projective) -- partially-applied multiplication function + :: (Integer -> Maybe Projective) -- partially-applied multiplication function -> BS.ByteString -> Pub -> BS.ByteString -> Bool _verify_schnorr _mul m (affine -> Affine x_p _) sig | BS.length sig /= 64 = False - | otherwise = case lift x_p of - Nothing -> False - Just capP@(Affine x_P _) -> - let (roll32 -> r, roll32 -> s) = BS.splitAt 32 sig - in if r >= _CURVE_P || s >= _CURVE_Q - then False - else let e = modQ . roll32 $ hash_challenge - (unroll32 r <> unroll32 x_P <> m) - dif = add (_mul s) - (neg (mul_unsafe (projective capP) e)) - in if dif == _CURVE_ZERO - then False - else let Affine x_R y_R = affine dif - in not (I.integerTestBit y_R 0 || x_R /= r) + | otherwise = M.isJust $ do + capP@(Affine x_P _) <- lift x_p + let (roll32 -> r, roll32 -> s) = BS.splitAt 32 sig + guard (not (r >= _CURVE_P || s >= _CURVE_Q)) + let e = modQ . roll32 $ hash_challenge + (unroll32 r <> unroll32 x_P <> m) + pt0 <- _mul s + pt1 <- mul_unsafe (projective capP) e + let dif = add pt0 (neg pt1) + guard (dif /= _CURVE_ZERO) + let Affine x_R y_R = affine dif + guard $ not (I.integerTestBit y_R 0 || x_R /= r) + pure () {-# INLINE _verify_schnorr #-} -- hardcoded tag of BIP0340/aux @@ -1092,7 +1082,7 @@ data HashFlag = sign_ecdsa :: Integer -- ^ secret key -> BS.ByteString -- ^ message - -> ECDSA + -> Maybe ECDSA sign_ecdsa = _sign_ecdsa (mul _CURVE_G) LowS Hash -- | The same as 'sign_ecdsa', except uses a 'Context' to optimise internal @@ -1108,7 +1098,7 @@ sign_ecdsa' :: Context -- ^ secp256k1 context -> Integer -- ^ secret key -> BS.ByteString -- ^ message - -> ECDSA + -> Maybe ECDSA sign_ecdsa' tex = _sign_ecdsa (mul_wnaf tex) LowS Hash -- | Produce an ECDSA signature for the provided message, using the @@ -1124,7 +1114,7 @@ sign_ecdsa' tex = _sign_ecdsa (mul_wnaf tex) LowS Hash sign_ecdsa_unrestricted :: Integer -- ^ secret key -> BS.ByteString -- ^ message - -> ECDSA + -> Maybe ECDSA sign_ecdsa_unrestricted = _sign_ecdsa (mul _CURVE_G) Unrestricted Hash -- | The same as 'sign_ecdsa_unrestricted', except uses a 'Context' to @@ -1140,7 +1130,7 @@ sign_ecdsa_unrestricted' :: Context -- ^ secp256k1 context -> Integer -- ^ secret key -> BS.ByteString -- ^ message - -> ECDSA + -> Maybe ECDSA sign_ecdsa_unrestricted' tex = _sign_ecdsa (mul_wnaf tex) Unrestricted Hash -- Produce a "low-s" ECDSA signature for the provided message, using @@ -1152,52 +1142,54 @@ sign_ecdsa_unrestricted' tex = _sign_ecdsa (mul_wnaf tex) Unrestricted Hash _sign_ecdsa_no_hash :: Integer -- ^ secret key -> BS.ByteString -- ^ message digest - -> ECDSA + -> Maybe ECDSA _sign_ecdsa_no_hash = _sign_ecdsa (mul _CURVE_G) LowS NoHash _sign_ecdsa_no_hash' :: Context -> Integer -> BS.ByteString - -> ECDSA + -> Maybe ECDSA _sign_ecdsa_no_hash' tex = _sign_ecdsa (mul_wnaf tex) LowS NoHash _sign_ecdsa - :: (Integer -> Projective) -- partially-applied multiplication function + :: (Integer -> Maybe Projective) -- partially-applied multiplication function -> SigType -> HashFlag -> Integer -> BS.ByteString - -> ECDSA -_sign_ecdsa _mul ty hf _SECRET m - | not (ge _SECRET) = error "ppad-secp256k1 (sign_ecdsa): invalid secret key" - | otherwise = runST $ do - -- RFC6979 sec 3.3a - let entropy = int2octets _SECRET - nonce = bits2octets h - drbg <- DRBG.new SHA256.hmac entropy nonce mempty - -- RFC6979 sec 2.4 - sign_loop drbg - where - h = case hf of - Hash -> SHA256.hash m - NoHash -> m - - h_modQ = remQ (bits2int h) -- bits2int yields nonnegative - - sign_loop g = do - k <- gen_k g - let kg = _mul k - Affine (modQ -> r) _ = affine kg - s = case modinv k (fi _CURVE_Q) of - Nothing -> error "ppad-secp256k1 (sign_ecdsa): bad k value" - Just kinv -> remQ (remQ (h_modQ + remQ (_SECRET * r)) * kinv) - if r == 0 -- negligible probability - then sign_loop g - else let !sig = ECDSA r s - in case ty of - Unrestricted -> pure sig - LowS -> pure (low sig) + -> Maybe ECDSA +_sign_ecdsa _mul ty hf _SECRET m = runST $ do + -- RFC6979 sec 3.3a + let entropy = int2octets _SECRET + nonce = bits2octets h + drbg <- DRBG.new SHA256.hmac entropy nonce mempty + -- RFC6979 sec 2.4 + sign_loop drbg + where + h = case hf of + Hash -> SHA256.hash m + NoHash -> m + + h_modQ = remQ (bits2int h) -- bits2int yields nonnegative + + sign_loop g = do + k <- gen_k g + let mpair = do + kg <- _mul k + let Affine (modQ -> r) _ = affine kg + kinv <- modinv k (fi _CURVE_Q) + let s = remQ (remQ (h_modQ + remQ (_SECRET * r)) * kinv) + pure $! (r, s) + case mpair of + Nothing -> pure Nothing + Just (r, s) + | r == 0 -> sign_loop g -- negligible probability + | otherwise -> + let !sig = Just $! ECDSA r s + in case ty of + Unrestricted -> pure sig + LowS -> pure (fmap low sig) {-# INLINE _sign_ecdsa #-} -- RFC6979 sec 3.3b @@ -1293,28 +1285,25 @@ verify_ecdsa_unrestricted' verify_ecdsa_unrestricted' tex = _verify_ecdsa_unrestricted (mul_wnaf tex) _verify_ecdsa_unrestricted - :: (Integer -> Projective) -- partially-applied multiplication function + :: (Integer -> Maybe Projective) -- partially-applied multiplication function -> BS.ByteString -> Pub -> ECDSA -> Bool -_verify_ecdsa_unrestricted _mul (SHA256.hash -> h) p (ECDSA r s) +_verify_ecdsa_unrestricted _mul (SHA256.hash -> h) p (ECDSA r s) = M.isJust $ do -- SEC1-v2 4.1.4 - | not (ge r) || not (ge s) = False - | otherwise = - let e = remQ (bits2int h) - s_inv = case modinv s (fi _CURVE_Q) of - -- 'ge s' assures existence of inverse - Nothing -> - error "ppad-secp256k1 (verify_ecdsa_unrestricted): no inverse" - Just si -> si - u1 = remQ (e * s_inv) - u2 = remQ (r * s_inv) - capR = add (_mul u1) (mul_unsafe p u2) - in if capR == _CURVE_ZERO - then False - else let Affine (modQ -> v) _ = affine capR - in v == r + guard (ge r && ge s) + let e = remQ (bits2int h) + s_inv <- modinv s (fi _CURVE_Q) + let u1 = remQ (e * s_inv) + u2 = remQ (r * s_inv) + pt0 <- _mul u1 + pt1 <- mul_unsafe p u2 + let capR = add pt0 pt1 + guard (capR /= _CURVE_ZERO) + let Affine (modQ -> v) _ = affine capR + guard (v == r) + pure () {-# INLINE _verify_ecdsa_unrestricted #-} -- ecdh ----------------------------------------------------------------------- @@ -1338,13 +1327,10 @@ _verify_ecdsa_unrestricted _mul (SHA256.hash -> h) p (ECDSA r s) ecdh :: Projective -- ^ public key -> Integer -- ^ secret key - -> BS.ByteString -- ^ shared secret -ecdh pub _SECRET - | not (ge _SECRET) = error "ppad-secp256k1 (ecdh): invalid secret key" - | otherwise = - let pt = mul pub _SECRET - in if pt == _CURVE_ZERO - then error "ppad-secp256k1 (ecdh): invalid public key" - else let Affine x _ = affine pt - in SHA256.hash (unroll32 x) + -> Maybe BS.ByteString -- ^ shared secret +ecdh pub _SECRET = do + pt <- mul pub _SECRET + guard (pt /= _CURVE_ZERO) + let Affine x _ = affine pt + pure $! SHA256.hash (unroll32 x)