diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,9 @@
 # Changelog
 
+- 0.5.5 (2026-05-22)
+  * Adds no-hash ECDSA signature verification utilities that skip the
+    internal SHA256 step.
+
 - 0.5.4 (2026-02-04)
   * Nonce generation for ECDSA is now more secure due to recent
     improvements in ppad-hmac-drbg. The DRBG state is now guaranteed
diff --git a/bench/Main.hs b/bench/Main.hs
--- a/bench/Main.hs
+++ b/bench/Main.hs
@@ -177,6 +177,7 @@
 
 ecdh :: Benchmark
 ecdh = env setup $ \ ~(big, pub) ->
+    -- the order of these seems to matter particularly for some reason
     bgroup "ecdh" [
       bench "ecdh (small)" $ nf (S.ecdh pub) 2
     , bench "ecdh (large)" $ nf (S.ecdh pub) big
diff --git a/lib/Crypto/Curve/Secp256k1.hs b/lib/Crypto/Curve/Secp256k1.hs
--- a/lib/Crypto/Curve/Secp256k1.hs
+++ b/lib/Crypto/Curve/Secp256k1.hs
@@ -97,6 +97,8 @@
   , _precompute
   , _sign_ecdsa_no_hash
   , _sign_ecdsa_no_hash'
+  , _verify_ecdsa_no_hash
+  , _verify_ecdsa_no_hash'
   , roll32
   , unsafe_roll32
   , unroll32
@@ -108,6 +110,7 @@
 import qualified Crypto.DRBG.HMAC.SHA256 as DRBG
 import qualified Crypto.Hash.SHA256 as SHA256
 import qualified Data.Bits as B
+import Data.Bits ((.<<.))
 import qualified Data.ByteString as BS
 import qualified Data.ByteString.Internal as BI
 import qualified Data.ByteString.Unsafe as BU
@@ -136,9 +139,6 @@
 -- Unboxed Projective synonym.
 type Proj = (# Limb4, Limb4, Limb4 #)
 
-pattern Zero :: Wider
-pattern Zero = Wider Z
-
 pattern Z :: Limb4
 pattern Z = (# Limb 0##, Limb 0##, Limb 0##, Limb 0## #)
 
@@ -581,6 +581,28 @@
           in  loop (succ j) nacc nf nd nm
 {-# INLINE mul# #-}
 
+mul_vartime# :: Proj -> Limb4 -> (# () | Proj #)
+mul_vartime# (# px, py, pz #) s
+    | zero# s =
+        let !(P zx zy zz) = _CURVE_ZERO
+        in  (# | (# zx, zy, zz #) #)
+    | CT.decide (CT.not (ge# s)) = (# () | #)
+    | otherwise =
+        let !(P zx zy zz) = _CURVE_ZERO
+        in  (# | loop (# zx, zy, zz #) (# px, py, pz #) s #)
+  where
+    zero# (# Limb a, Limb b, Limb c, Limb d #) = Exts.isTrue#
+      ((a `Exts.or#` b `Exts.or#` c `Exts.or#` d) `Exts.eqWord#` 0##)
+
+    loop !r !d !m
+      | zero# m = r
+      | otherwise =
+          let !nd = double# d
+              !(# nm, lsb_set #) = W.shr1_c# m
+              !nr = if CT.decide lsb_set then add_proj# r d else r
+          in  loop nr nd nm
+{-# INLINE mul_vartime# #-}
+
 ge# :: Limb4 -> CT.Choice
 ge# n =
   let !(Wider q) = _CURVE_Q
@@ -597,10 +619,10 @@
   where
     !one                  = (# Limb 1##, Limb 0##, Limb 0##, Limb 0## #)
     !wins                 = fi (256 `quot` ctxW + 1)
-    !size@(GHC.Word.W# s) = 2 ^ (ctxW - 1)
-    !(GHC.Word.W# mask)   = 2 ^ ctxW - 1
+    !size@(GHC.Word.W# s) = 1 .<<. (ctxW - 1)
+    !(GHC.Word.W# mask)   = 1 .<<. ctxW - 1
     !(GHC.Word.W# texW)   = fi ctxW
-    !(GHC.Word.W# mnum)   = 2 ^ ctxW
+    !(GHC.Word.W# mnum)   = 1 .<<. ctxW
 
     loop !j@(GHC.Word.W# w) !acc !f !n@(# Limb lo, _, _, _ #)
       | j == wins = acc
@@ -720,18 +742,9 @@
 --
 -- Don't use this function if the scalar could potentially be a secret.
 mul_vartime :: Projective -> Wider -> Maybe Projective
-mul_vartime p = \case
-    Zero -> pure _CURVE_ZERO
-    n | not (ge n) -> Nothing
-      | otherwise  -> pure $! loop _CURVE_ZERO p n
-  where
-    loop !r !d = \case
-      Zero -> r
-      m ->
-        let !nd = double d
-            !(# nm, lsb_set #) = W.shr1_c m
-            !nr = if CT.decide lsb_set then add r d else r
-        in  loop nr nd nm
+mul_vartime (P x y z) (Wider s) = case mul_vartime# (# x, y, z #) s of
+  (# () | #)               -> Nothing
+  (# | (# px, py, pz #) #) -> Just $! P px py pz
 
 -- | Precomputed multiples of the secp256k1 base or generator point.
 data Context = Context {
@@ -765,7 +778,7 @@
 -- the ByteArray is (size * 96) bytes.
 _precompute :: Int -> Context
 _precompute ctxW = Context {..} where
-  capJ = (2 :: Int) ^ (ctxW - 1)
+  capJ = (1 :: Int) .<<. (ctxW - 1)
   ws = 256 `quot` ctxW + 1
   size = ws * capJ
 
@@ -1359,7 +1372,8 @@
   -> Pub           -- ^ public key
   -> ECDSA         -- ^ signature
   -> Bool
-verify_ecdsa_unrestricted = _verify_ecdsa_unrestricted (mul_vartime _CURVE_G)
+verify_ecdsa_unrestricted =
+  _verify_ecdsa_unrestricted (mul_vartime _CURVE_G) Hash
 
 -- | The same as 'verify_ecdsa_unrestricted', except uses a 'Context' to
 --   optimise internal calculations.
@@ -1378,17 +1392,21 @@
   -> Pub           -- ^ public key
   -> ECDSA         -- ^ signature
   -> Bool
-verify_ecdsa_unrestricted' tex = _verify_ecdsa_unrestricted (mul_wnaf tex)
+verify_ecdsa_unrestricted' tex =
+  _verify_ecdsa_unrestricted (mul_wnaf tex) Hash
 
 _verify_ecdsa_unrestricted
   :: (Wider -> Maybe Projective) -- partially-applied multiplication function
+  -> HashFlag
   -> BS.ByteString
   -> Pub
   -> ECDSA
   -> Bool
-_verify_ecdsa_unrestricted _mul m p (ECDSA r0 s0) = M.isJust $ do
+_verify_ecdsa_unrestricted _mul hf m p (ECDSA r0 s0) = M.isJust $ do
   -- SEC1-v2 4.1.4
-  let h = SHA256.hash m
+  let h = case hf of
+        Hash   -> SHA256.hash m
+        NoHash -> m
   guard (ge r0 && ge s0)
   let r  = S.to r0
       s  = S.to s0
@@ -1396,11 +1414,55 @@
       si = S.inv s
       u1 = S.retr (e * si)
       u2 = S.retr (r * si)
-  pt0 <- _mul u1
+      pt0 = case _mul u1 of
+        Nothing -> _CURVE_ZERO
+        Just pt -> pt
   pt1 <- mul_vartime p u2
   let capR = add pt0 pt1
   guard (capR /= _CURVE_ZERO)
   let Affine (S.to . C.retr -> v) _ = affine capR
   guard (S.eq_vartime v r)
 {-# INLINE _verify_ecdsa_unrestricted #-}
+
+-- | Verify a "low-s" ECDSA signature for the provided message digest
+--   and public key.
+--
+--   Mirrors 'verify_ecdsa', but skips the internal SHA256 step,
+--   treating the input as the message digest itself.
+--
+--   >>> _verify_ecdsa_no_hash dig pub valid_sig
+--   True
+--   >>> _verify_ecdsa_no_hash dig pub invalid_sig
+--   False
+_verify_ecdsa_no_hash
+  :: BS.ByteString -- ^ message digest
+  -> Pub           -- ^ public key
+  -> ECDSA         -- ^ signature
+  -> Bool
+_verify_ecdsa_no_hash m p sig@(ECDSA _ s)
+  | W.gt_vartime s _CURVE_QH = False
+  | otherwise =
+      _verify_ecdsa_unrestricted (mul_vartime _CURVE_G) NoHash m p sig
+
+-- | The same as '_verify_ecdsa_no_hash', except uses a 'Context' to
+--   optimise internal calculations.
+--
+--   You can expect about a 2x performance increase when using this
+--   function, compared to '_verify_ecdsa_no_hash'.
+--
+--   >>> let !tex = precompute
+--   >>> _verify_ecdsa_no_hash' tex dig pub valid_sig
+--   True
+--   >>> _verify_ecdsa_no_hash' tex dig pub invalid_sig
+--   False
+_verify_ecdsa_no_hash'
+  :: Context       -- ^ secp256k1 context
+  -> BS.ByteString -- ^ message digest
+  -> Pub           -- ^ public key
+  -> ECDSA         -- ^ signature
+  -> Bool
+_verify_ecdsa_no_hash' tex m p sig@(ECDSA _ s)
+  | W.gt_vartime s _CURVE_QH = False
+  | otherwise =
+      _verify_ecdsa_unrestricted (mul_wnaf tex) NoHash m p sig
 
diff --git a/ppad-secp256k1.cabal b/ppad-secp256k1.cabal
--- a/ppad-secp256k1.cabal
+++ b/ppad-secp256k1.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.0
 name:               ppad-secp256k1
-version:            0.5.4
+version:            0.5.5
 synopsis:           Schnorr signatures, ECDSA, and ECDH on the elliptic curve
                     secp256k1
 license:            MIT
@@ -38,7 +38,7 @@
     , bytestring >= 0.9 && < 0.13
     , ppad-hmac-drbg >= 0.3.1 && < 0.4
     , ppad-sha256 >= 0.3.2 && < 0.4
-    , ppad-fixed >= 0.1.3 && < 0.2
+    , ppad-fixed >= 0.1.4 && < 0.2
     , primitive >= 0.8 && < 0.10
 
 test-suite secp256k1-tests
diff --git a/test/Noble.hs b/test/Noble.hs
--- a/test/Noble.hs
+++ b/test/Noble.hs
@@ -52,8 +52,11 @@
         pec = parse_compact vt_signature
         Just sig = _sign_ecdsa_no_hash x msg
         Just sig' = _sign_ecdsa_no_hash' tex x msg
+        Just pub = derive_pub x
     assertEqual mempty sig sig'
     assertEqual mempty pec sig
+    assertBool mempty (_verify_ecdsa_no_hash msg pub sig)
+    assertBool mempty (_verify_ecdsa_no_hash' tex msg pub sig)
 
 execute_invalid_sign :: Context -> (Int, InvalidSignTest) -> TestTree
 execute_invalid_sign tex (label, InvalidSignTest {..}) =
