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 f14203f7f21f0197552249261042ef275622325a
parent 5c74f8e449dcaf9f7a8f2ef036c54e041f7c8d4f
Author: Jared Tobin <jared@jtobin.io>
Date:   Sat,  1 Aug 2026 10:14:33 -0230

lib: check digest length in no-hash ecdsa variants

The NoHash path hands the caller's bytes straight to unsafe_roll32,
which performs 32 unchecked reads. A digest shorter than 32 bytes
therefore reads past the end of the buffer, and the result depends on
whatever happens to be allocated after it: three byte-identical
one-byte digests produce three different signatures. Digests longer
than 32 bytes were silently truncated, so distinct digests shared a
signature.

Guard all four entry points on _CURVE_Q_BYTES.

Diffstat:
Mlib/Crypto/Curve/Secp256k1.hs | 15+++++++++++----
1 file changed, 11 insertions(+), 4 deletions(-)

diff --git a/lib/Crypto/Curve/Secp256k1.hs b/lib/Crypto/Curve/Secp256k1.hs @@ -1239,7 +1239,7 @@ sign_ecdsa_unrestricted' tex = _sign_ecdsa (mul_wnaf tex) Unrestricted Hash -- Produce a "low-s" ECDSA signature for the provided message, using -- the provided private key. Assumes that the message has already been --- pre-hashed. +-- pre-hashed, such that the digest is exactly 32 bytes. -- -- (Useful for testing against noble-secp256k1's suite, in which messages -- in the test vectors have already been hashed.) @@ -1247,14 +1247,18 @@ _sign_ecdsa_no_hash :: Wider -- ^ secret key -> BS.ByteString -- ^ message digest -> Maybe ECDSA -_sign_ecdsa_no_hash = _sign_ecdsa (mul _CURVE_G) LowS NoHash +_sign_ecdsa_no_hash _SECRET m + | BS.length m /= _CURVE_Q_BYTES = Nothing + | otherwise = _sign_ecdsa (mul _CURVE_G) LowS NoHash _SECRET m _sign_ecdsa_no_hash' :: Context -> Wider -> BS.ByteString -> Maybe ECDSA -_sign_ecdsa_no_hash' tex = _sign_ecdsa (mul_wnaf tex) LowS NoHash +_sign_ecdsa_no_hash' tex _SECRET m + | BS.length m /= _CURVE_Q_BYTES = Nothing + | otherwise = _sign_ecdsa (mul_wnaf tex) LowS NoHash _SECRET m _sign_ecdsa :: (Wider -> Maybe Projective) -- partially-applied multiplication function @@ -1421,7 +1425,8 @@ _verify_ecdsa_unrestricted _mul hf m p (ECDSA r0 s0) = M.isJust $ do -- and public key. -- -- Mirrors 'verify_ecdsa', but skips the internal SHA256 step, --- treating the input as the message digest itself. +-- treating the input as the message digest itself. Fails to verify +-- if the digest is not exactly 32 bytes. -- -- >>> _verify_ecdsa_no_hash dig pub valid_sig -- True @@ -1433,6 +1438,7 @@ _verify_ecdsa_no_hash -> ECDSA -- ^ signature -> Bool _verify_ecdsa_no_hash m p sig@(ECDSA _ s) + | BS.length m /= _CURVE_Q_BYTES = False | W.gt_vartime s _CURVE_QH = False | otherwise = _verify_ecdsa_unrestricted (mul_vartime _CURVE_G) NoHash m p sig @@ -1455,6 +1461,7 @@ _verify_ecdsa_no_hash' -> ECDSA -- ^ signature -> Bool _verify_ecdsa_no_hash' tex m p sig@(ECDSA _ s) + | BS.length m /= _CURVE_Q_BYTES = False | W.gt_vartime s _CURVE_QH = False | otherwise = _verify_ecdsa_unrestricted (mul_wnaf tex) NoHash m p sig