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 9ccce108727ed5f82be65c3e57cf80e62a98a48c
parent 4976986505fe033df4353b69a751fd3831d621b9
Author: Jared Tobin <jared@jtobin.io>
Date:   Sat, 27 Dec 2025 11:41:21 -0330

lib: updates for ppad-fixed v0.1.2

Diffstat:
Mflake.lock | 8++++----
Mlib/Crypto/Curve/Secp256k1.hs | 54+++++++++++++++++++++++++++---------------------------
Mppad-secp256k1.cabal | 2+-
Mtest/Noble.hs | 6+++++-
4 files changed, 37 insertions(+), 33 deletions(-)

diff --git a/flake.lock b/flake.lock @@ -82,11 +82,11 @@ ] }, "locked": { - "lastModified": 1766841814, - "narHash": "sha256-jmPRQBdxW0QGa8YSeDr900q2QD9b5PxYJBF4yB2ArSo=", + "lastModified": 1766845669, + "narHash": "sha256-aDfG7HX8zn0L/ZBABimeUHdvHBcYsiksAzN898QUa5I=", "ref": "master", - "rev": "4b74a737e247690d7640d821ea478b2e8088d38d", - "revCount": 269, + "rev": "0213f7350fcda1c8d28bb9dae686205cf5983f88", + "revCount": 272, "type": "git", "url": "git://git.ppad.tech/fixed.git" }, diff --git a/lib/Crypto/Curve/Secp256k1.hs b/lib/Crypto/Curve/Secp256k1.hs @@ -288,7 +288,7 @@ modQ :: Wider -> Wider modQ x = let !(Wider xw) = x !(Wider qw) = _CURVE_Q - in W.select x (x - _CURVE_Q) (CT.not# (W.lt# xw qw)) + in W.select x (x - _CURVE_Q) (CT.not (W.lt# xw qw)) {-# INLINABLE modQ #-} -- bytewise xor @@ -336,7 +336,11 @@ _CURVE_Bm3 = 21 -- Is field element? fe :: Wider -> Bool -fe n = n > 0 && n < _CURVE_P +fe n = case W.cmp_vartime n 0 of + GT -> case W.cmp_vartime n _CURVE_P of + LT -> True + _ -> False + _ -> False {-# INLINE fe #-} -- Is group element? @@ -364,7 +368,7 @@ instance Eq Projective where !x2z1 = bx * az !y1z2 = ay * bz !y2z1 = by * az - in CT.decide (CT.and# (C.eq x1z2 x2z1) (C.eq y1z2 y2z1)) + in CT.decide (CT.and (C.eq x1z2 x2z1) (C.eq y1z2 y2z1)) -- | An ECC-flavoured alias for a secp256k1 point. type Pub = Projective @@ -378,9 +382,9 @@ affine (Projective x y z) = -- Convert to projective coordinates. projective :: Affine -> Projective -projective = \case - Affine 0 0 -> _CURVE_ZERO - Affine x y -> Projective x y 1 +projective (Affine x y) + | C.eq_vartime x 0 || C.eq_vartime y 0 = _CURVE_ZERO + | otherwise = Projective x y 1 -- | secp256k1 generator point. _CURVE_G :: Projective @@ -410,10 +414,7 @@ weierstrass x = C.sqr x * x + _CURVE_Bm -- Point is valid valid :: Projective -> Bool -valid p = case affine p of - Affine x y - | C.sqr y /= weierstrass x -> False - | otherwise -> True +valid (affine -> Affine x y) = C.eq_vartime (C.sqr y) (weierstrass x) -- (bip0340) return point with x coordinate == x and with even y coordinate -- @@ -428,10 +429,9 @@ valid p = case affine p of lift_vartime :: C.Montgomery -> Maybe Affine lift_vartime x = do let !c = weierstrass x - !y <- C.sqrt c - let !y_e | C.odd y = negate y + !y <- C.sqrt_vartime c + let !y_e | C.odd_vartime y = negate y | otherwise = y - guard (C.sqr y_e == c) pure $! Affine x y_e even_y_vartime :: Projective -> Projective @@ -557,7 +557,7 @@ neg# (# x, y, z #) = (# x, C.neg# y, z #) mul# :: Proj -> Limb4 -> (# () | Proj #) mul# (# px, py, pz #) s - | CT.decide (CT.not# (ge# s)) = (# () | #) + | CT.decide (CT.not (ge# s)) = (# () | #) | otherwise = let !(P gx gy gz) = _CURVE_G !(C.Montgomery o) = C.one @@ -576,12 +576,12 @@ mul# (# px, py, pz #) s ge# :: Limb4 -> CT.Choice ge# n = let !(Wider q) = _CURVE_Q - in CT.and# (W.gt# n Z) (W.lt# n q) + in CT.and (W.gt# n Z) (W.lt# n q) {-# INLINE ge# #-} mul_wnaf# :: ByteArray -> Int -> Limb4 -> (# () | Proj #) mul_wnaf# ctxArray ctxW ls - | CT.decide (CT.not# (ge# ls)) = (# () | #) + | CT.decide (CT.not (ge# ls)) = (# () | #) | otherwise = let !(P zx zy zz) = _CURVE_ZERO !(P gx gy gz) = _CURVE_G @@ -868,7 +868,7 @@ _parse_compressed h (unsafe_roll32 -> x) | not (fe x) = Nothing | otherwise = do let !mx = C.to x - !my <- C.sqrt (weierstrass mx) + !my <- C.sqrt_vartime (weierstrass mx) let !yodd = CT.decide (W.odd (C.retr my)) !hodd = B.testBit h 0 pure $! @@ -998,7 +998,7 @@ _sign_schnorr _mul _SECRET m a = do t = xor bytes_d (hash_aux a) rand = hash_nonce (t <> bytes_p <> m) k' = S.to (unsafe_roll32 rand) - guard (k' /= 0) -- negligible probability + guard (not (S.eq_vartime k' 0)) -- negligible probability pt <- _mul (S.retr k') let Affine (C.retr -> x_r) (C.retr -> y_r) = affine pt k = S.select k' (negate k') (W.odd y_r) @@ -1067,7 +1067,7 @@ _verify_schnorr _mul m p sig let dif = add pt0 (neg pt1) guard (dif /= _CURVE_ZERO) let Affine (C.from -> x_R) (C.from -> y_R) = affine dif - guard $ not (CT.decide (W.odd y_R) || x_R /= r) -- XX + guard $ not (CT.decide (W.odd y_R) || not (W.eq_vartime x_R r)) {-# INLINE _verify_schnorr #-} -- hardcoded tag of BIP0340/aux @@ -1116,7 +1116,7 @@ data ECDSA = ECDSA { ecdsa_r :: !Wider , ecdsa_s :: !Wider } - deriving (Eq, Generic) + deriving (Generic) instance Show ECDSA where show _ = "<ecdsa signature>" @@ -1253,7 +1253,7 @@ _sign_ecdsa _mul ty hf _SECRET m = runST $ do case mpair of Nothing -> pure Nothing Just (r, s) - | r == 0 -> sign_loop g -- negligible probability + | W.eq_vartime r 0 -> sign_loop g -- negligible probability | otherwise -> let !sig = Just $! ECDSA r s in case ty of @@ -1267,9 +1267,9 @@ gen_k g = loop g where loop drbg = do bytes <- DRBG.gen mempty (fi _CURVE_Q_BYTES) drbg let can = bits2int bytes - if can >= _CURVE_Q - then loop drbg - else pure can + case W.cmp_vartime can _CURVE_Q of + LT -> pure can + _ -> loop drbg -- 2 ^ -128 probability {-# INLINE gen_k #-} -- | Verify a "low-s" ECDSA signature for the provided message and @@ -1288,7 +1288,7 @@ verify_ecdsa -> ECDSA -- ^ signature -> Bool verify_ecdsa m p sig@(ECDSA _ s) - | s > _CURVE_QH = False + | CT.decide (W.gt s _CURVE_QH) = False | otherwise = verify_ecdsa_unrestricted m p sig -- | The same as 'verify_ecdsa', except uses a 'Context' to optimise @@ -1309,7 +1309,7 @@ verify_ecdsa' -> ECDSA -- ^ signature -> Bool verify_ecdsa' tex m p sig@(ECDSA _ s) - | s > _CURVE_QH = False + | CT.decide (W.gt s _CURVE_QH) = False | otherwise = verify_ecdsa_unrestricted' tex m p sig -- | Verify an unrestricted ECDSA signature for the provided message and @@ -1366,6 +1366,6 @@ _verify_ecdsa_unrestricted _mul m p (ECDSA r0 s0) = M.isJust $ do let capR = add pt0 pt1 guard (capR /= _CURVE_ZERO) let Affine (S.to . C.retr -> v) _ = affine capR - guard (v == r) + guard (S.eq_vartime v r) {-# INLINE _verify_ecdsa_unrestricted #-} diff --git a/ppad-secp256k1.cabal b/ppad-secp256k1.cabal @@ -38,7 +38,7 @@ library , bytestring >= 0.9 && < 0.13 , ppad-hmac-drbg >= 0.1 && < 0.2 , ppad-sha256 >= 0.2 && < 0.3 - , ppad-fixed >= 0.1 && < 0.2 + , ppad-fixed >= 0.1.2 && < 0.2 , primitive >= 0.8 && < 0.10 test-suite secp256k1-tests diff --git a/test/Noble.hs b/test/Noble.hs @@ -1,4 +1,4 @@ -{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns #-} +{-# OPTIONS_GHC -fno-warn-incomplete-uni-patterns -fno-warn-orphans #-} {-# LANGUAGE BangPatterns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} @@ -18,6 +18,7 @@ import qualified Data.ByteString.Base16 as B16 import qualified Data.Text as T import qualified Data.Text.Encoding as TE import Data.Word.Wider (Wider(..)) +import qualified Data.Word.Wider as Wider import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (assertEqual, assertBool, assertFailure, testCase) @@ -26,6 +27,9 @@ decodeLenient bs = case B16.decode bs of Nothing -> error "bang" Just b -> b +instance Eq ECDSA where + ECDSA r0 s0 == ECDSA r1 s1 = Wider.eq_vartime r0 r1 && Wider.eq_vartime s0 s1 + data Ecdsa = Ecdsa { ec_valid :: ![(Int, ValidTest)] , ec_invalid :: !InvalidTest