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 079a416155060950576ee931bec17cc9ea49d615
parent f14203f7f21f0197552249261042ef275622325a
Author: Jared Tobin <jared@jtobin.io>
Date:   Sat,  1 Aug 2026 10:15:27 -0230

lib: validate the secret key when signing with ecdsa

_sign_ecdsa never passed the secret through mul#, so its ge# range
check only ever guarded the per-signature nonce. The key reached the
computation solely via S.to, which reduces mod q without complaint, so
sign_ecdsa 0 produced a signature where derive_pub 0 returns Nothing,
and a key >= q signed as its reduction while seeding the DRBG with the
unreduced bytes. sign_schnorr avoids this only incidentally, since it
needs d * G and so multiplies by the secret.

Reject a secret outside (0, q) via ge, as _verify_ecdsa_unrestricted
already does for r and s.

The noble suite carries the four vectors that cover this, but
execute_invalid_sign wrapped each call in pure (...) >> pure False.
pure never forces the thunk, so no ErrorCall could escape and the
group passed unconditionally. Assert Nothing from both signing
variants instead.

Diffstat:
Mlib/Crypto/Curve/Secp256k1.hs | 16+++++++++-------
Mtest/Noble.hs | 16++++++----------
2 files changed, 15 insertions(+), 17 deletions(-)

diff --git a/lib/Crypto/Curve/Secp256k1.hs b/lib/Crypto/Curve/Secp256k1.hs @@ -1267,13 +1267,15 @@ _sign_ecdsa -> Wider -> BS.ByteString -> 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 entropy nonce mempty - -- RFC6979 sec 2.4 - sign_loop drbg +_sign_ecdsa _mul ty hf _SECRET m + | not (ge _SECRET) = Nothing + | otherwise = runST $ do + -- RFC6979 sec 3.3a + let entropy = int2octets _SECRET + nonce = bits2octets h + drbg <- DRBG.new entropy nonce mempty + -- RFC6979 sec 2.4 + sign_loop drbg where d = S.to _SECRET hm = S.to (bits2int h) diff --git a/test/Noble.hs b/test/Noble.hs @@ -9,7 +9,6 @@ module Noble ( , execute_ecdsa ) where -import Control.Exception import Crypto.Curve.Secp256k1 import Data.Aeson ((.:)) import qualified Data.Aeson as A @@ -61,16 +60,13 @@ execute_valid tex (label, ValidTest {..}) = execute_invalid_sign :: Context -> (Int, InvalidSignTest) -> TestTree execute_invalid_sign tex (label, InvalidSignTest {..}) = testCase ("noble-secp256k1, invalid sign (" <> show label <> ")") $ do - let x = ivs_d - m = ivs_m - err <- catch (pure (_sign_ecdsa_no_hash x m) >> pure False) handler - err' <- catch (pure (_sign_ecdsa_no_hash' tex x m) >> pure False) handler - if err || err' - then assertFailure "expected error not caught" - else pure () + expect_nothing (_sign_ecdsa_no_hash ivs_d ivs_m) + expect_nothing (_sign_ecdsa_no_hash' tex ivs_d ivs_m) where - handler :: ErrorCall -> IO Bool - handler _ = pure True + expect_nothing :: Maybe ECDSA -> IO () + expect_nothing Nothing = pure () + expect_nothing (Just _) = + assertFailure "signed with an out-of-range secret key" execute_invalid_verify :: Context -> (Int, InvalidVerifyTest) -> TestTree execute_invalid_verify tex (label, InvalidVerifyTest {..}) =