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:
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 {..}) =