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:
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