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 f0859a4d37e7f37df769c9a4f32b669461602754
parent 03c4cdb770f79591e5915c3ba591fd6187a7e962
Author: Jared Tobin <jared@jtobin.io>
Date:   Fri, 18 Oct 2024 12:33:23 +0400

lib: mul/mul_unsafe split, parse_int256

Diffstat:
Mbench/Main.hs | 17+++++++++++++++--
Mlib/Crypto/Curve/Secp256k1.hs | 145+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
2 files changed, 123 insertions(+), 39 deletions(-)

diff --git a/bench/Main.hs b/bench/Main.hs @@ -13,6 +13,7 @@ import qualified Crypto.Curve.Secp256k1 as S instance NFData S.Projective instance NFData S.Affine instance NFData S.ECDSA +instance NFData S.Word256 main :: IO () main = defaultMain [ @@ -30,6 +31,18 @@ parse_point = bgroup "parse_point" [ , bench "bip0340" $ nf S.parse_point (BS.drop 1 p_bs) ] +parse_integer :: Benchmark +parse_integer = env setup $ \ ~(small, big) -> + bgroup "parse_int256" [ + bench "parse_int256 (small)" $ nf S.parse_int256 small + , bench "parse_int256 (big)" $ nf S.parse_int256 big + ] + where + setup = do + let small = BS.replicate 32 0x00 + big = BS.replicate 32 0xFF + pure (small, big) + add :: Benchmark add = bgroup "add" [ bench "2 p (double, trivial projective point)" $ nf (S.add p) p @@ -56,7 +69,7 @@ schnorr = bgroup "schnorr" [ ecdsa :: Benchmark ecdsa = bgroup "ecdsa" [ bench "sign_ecdsa" $ nf (S.sign_ecdsa s_sk) s_msg - -- , bench "verify_ecdsa" $ nf (S.verify_ecdsa e_msg t) e_sig + -- , bench "verify_ecdsa" $ nf (S.verify_ecdsa e_msg t) e_sig -- XX inputs ] p_bs :: BS.ByteString @@ -104,7 +117,7 @@ t = case S.parse_point t_bs of Just !pt -> pt s_sk :: Integer -s_sk = S.parse_integer . B16.decodeLenient $ +s_sk = S.parse_int256 . B16.decodeLenient $ "B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF" s_sig :: BS.ByteString diff --git a/lib/Crypto/Curve/Secp256k1.hs b/lib/Crypto/Curve/Secp256k1.hs @@ -33,7 +33,8 @@ module Crypto.Curve.Secp256k1 ( , verify_ecdsa_unrestricted -- * Parsing - , parse_integer + , Word256(..) + , parse_int256 , parse_point -- Elliptic curve group operations @@ -41,6 +42,7 @@ module Crypto.Curve.Secp256k1 ( , add , double , mul + , mul_unsafe -- Coordinate systems and transformations , Affine(..) @@ -60,11 +62,12 @@ import Control.Monad (when) import Control.Monad.ST import qualified Crypto.DRBG.HMAC as DRBG import qualified Crypto.Hash.SHA256 as SHA256 +import Data.Bits ((.|.)) import qualified Data.Bits as B import qualified Data.ByteString as BS import qualified Data.ByteString.Unsafe as BU import Data.STRef -import Data.Word (Word8) +import Data.Word (Word8, Word64) import GHC.Generics import GHC.Natural import qualified GHC.Num.Integer as I @@ -100,10 +103,49 @@ modinv a m = case I.integerRecipMod# a m of xor :: BS.ByteString -> BS.ByteString -> BS.ByteString xor = BS.packZipWith B.xor --- big-endian bytestring decoding +-- arbitrary-size big-endian bytestring decoding roll :: BS.ByteString -> Integer roll = BS.foldl' alg 0 where - alg a (fi -> b) = (a `I.integerShiftL` 8) `I.integerOr` b + alg !a (fi -> !b) = (a `I.integerShiftL` 8) `I.integerOr` b + +data Word256 = Word256 + {-# UNPACK #-} !Word64 + {-# UNPACK #-} !Word64 + {-# UNPACK #-} !Word64 + {-# UNPACK #-} !Word64 + deriving (Eq, Show, Generic) + +word256_to_integer :: Word256 -> Integer +word256_to_integer (Word256 w0 w1 w2 w3) = + (fi w0 `B.shiftL` 192) + .|. (fi w1 `B.shiftL` 128) + .|. (fi w2 `B.shiftL` 64) + .|. fi w3 +{-# INLINE word256_to_integer #-} + +-- /Note:/ there can be substantial differences in execution time +-- when this function is called with "extreme" inputs. For example: a +-- bytestring consisting entirely of 0x00 bytes will parse more quickly +-- than one consisting of entirely 0xFF bytes. For appropriately-random +-- inputs, timings should be indistinguishable. + +-- 256-bit big-endian bytestring decoding. the input size is not checked! +roll32 :: BS.ByteString -> Integer +roll32 bs = word256_to_integer $! (go 0 0 0 0 0) where + go !acc0 !acc1 !acc2 !acc3 !j + | j == 32 = Word256 acc0 acc1 acc2 acc3 + | j < 8 = + let b = fi (BU.unsafeIndex bs j) + in go ((acc0 `B.shiftL` 8) .|. b) acc1 acc2 acc3 (j + 1) + | j < 16 = + let b = fi (BU.unsafeIndex bs j) + in go acc0 ((acc1 `B.shiftL` 8) .|. b) acc2 acc3 (j + 1) + | j < 24 = + let b = fi (BU.unsafeIndex bs j) + in go acc0 acc1 ((acc2 `B.shiftL` 8) .|. b) acc3 (j + 1) + | otherwise = + let b = fi (BU.unsafeIndex bs j) + in go acc0 acc1 acc2 ((acc3 `B.shiftL` 8) .|. b) (j + 1) -- big-endian bytestring encoding unroll :: Integer -> BS.ByteString @@ -489,28 +531,53 @@ double (Projective x y z) = runST $ do modifySTRef' x3 (\rx3 -> modP (rx3 + rx3)) Projective <$> readSTRef x3 <*> readSTRef y3 <*> readSTRef z3 --- Scalar multiplication of secp256k1 points. +-- XX must take into account integer size + +-- Timing-safe scalar multiplication of secp256k1 points. mul :: Projective -> Integer -> Projective -mul p n +mul p _MAYBE_SECRET + | not (ge _MAYBE_SECRET) = + error "ppad-secp256k1 (mul): scalar not in group" + | otherwise = loop _ZERO _CURVE_G p _MAYBE_SECRET + where + loop !r !f !d m + | m <= 0 = r + | otherwise = + let nd = double d + nm = I.integerShiftR m 1 + ev = I.integerTestBit m 0 + nr | ev = add r d + | otherwise = r + nf | not ev = add f d + | otherwise = f + in loop nr nf nd nm +{-# NOINLINE mul #-} + +-- Timing-unsafe scalar multiplication of secp256k1 points. +-- +-- Don't use this function if the scalar could potentially be a secret. +mul_unsafe :: Projective -> Integer -> Projective +mul_unsafe p n | n == 0 = _ZERO - | not (ge n) = error "ppad-secp256k1 (mul): scalar not in group" + | not (ge n) = + error "ppad-secp256k1 (mul_unsafe): scalar not in group" | otherwise = loop _ZERO p n where - loop !r !d m -- XX timing concern + loop !r !d m | m <= 0 = r | otherwise = let nd = double d nm = I.integerShiftR m 1 nr = if I.integerTestBit m 0 then add r d else r in loop nr nd nm -{-# NOINLINE mul #-} -- parsing -------------------------------------------------------------------- --- | Parse an integer. -parse_integer :: BS.ByteString -> Integer -parse_integer = roll -- XX timing concern (use constant-time roll here) -{-# NOINLINE parse_integer #-} +parse_int256 :: BS.ByteString -> Integer +parse_int256 bs + | BS.length bs /= 32 = + error "ppad-secp256k1 (parse_int256): requires exactly 32-byte input" + | otherwise = roll32 bs -- | Parse compressed point (33 bytes), uncompressed point (65 bytes), -- or BIP0340-style point (32 bytes). @@ -525,11 +592,13 @@ parse_point bs h = BU.unsafeIndex bs 0 -- lazy t = BS.drop 1 bs +-- input is guaranteed to be 32B in length _parse_bip0340 :: BS.ByteString -> Maybe Projective -_parse_bip0340 = fmap projective . lift . roll +_parse_bip0340 = fmap projective . lift . roll32 +-- bytestring input is guaranteed to be 32B in length _parse_compressed :: Word8 -> BS.ByteString -> Maybe Projective -_parse_compressed h (roll -> x) +_parse_compressed h (roll32 -> x) | h /= 0x02 && h /= 0x03 = Nothing | not (fe x) = Nothing | otherwise = do @@ -541,8 +610,9 @@ _parse_compressed h (roll -> x) then Projective x (modP (negate y)) 1 else Projective x y 1 +-- bytestring input is guaranteed to be 64B in length _parse_uncompressed :: Word8 -> BS.ByteString -> Maybe Projective -_parse_uncompressed h (BS.splitAt _CURVE_Q_BYTES -> (roll -> x, roll -> y)) +_parse_uncompressed h (BS.splitAt _CURVE_Q_BYTES -> (roll32 -> x, roll32 -> y)) | h /= 0x04 = Nothing | otherwise = let p = Projective x y 1 @@ -576,7 +646,7 @@ sign_schnorr sign_schnorr d' m a | not (ge d') = error "ppad-secp256k1 (sign_schnorr): invalid secret key" | otherwise = - let p_proj = mul _CURVE_G d' -- XX timing concern + let p_proj = mul_unsafe _CURVE_G d' -- XX timing concern Affine x_p y_p = affine p_proj d | I.integerTestBit y_p 0 = _CURVE_Q - d' -- XX timing concern | otherwise = d' @@ -588,17 +658,17 @@ sign_schnorr d' m a bytes_p = unroll32 x_p rand = hash_tagged "BIP0340/nonce" (t <> bytes_p <> m) - k' = modQ (roll rand) + k' = modQ (roll32 rand) in if k' == 0 -- negligible probability then error "ppad-secp256k1 (sign_schnorr): invalid k" else - let Affine x_r y_r = affine (mul _CURVE_G k') + let Affine x_r y_r = affine (mul_unsafe _CURVE_G k') k | I.integerTestBit y_r 0 = _CURVE_Q - k' | otherwise = k' bytes_r = unroll32 x_r - e = modQ . roll . hash_tagged "BIP0340/challenge" + e = modQ . roll32 . hash_tagged "BIP0340/challenge" $ bytes_r <> bytes_p <> m bytes_ked = unroll32 (modQ (k + e * d)) @@ -608,7 +678,6 @@ sign_schnorr d' m a in if verify_schnorr m p_proj sig then sig else error "ppad-secp256k1 (sign_schnorr): invalid signature" -{-# NOINLINE sign_schnorr #-} -- | Verify a 64-byte Schnorr signature for the provided message with -- the supplied public key. @@ -622,19 +691,22 @@ verify_schnorr -> Pub -- ^ public key -> BS.ByteString -- ^ 64-byte Schnorr signature -> Bool -verify_schnorr m (affine -> Affine x_p _) sig = case lift x_p of - Nothing -> False - Just capP@(Affine x_P _) -> - let (roll -> r, roll -> s) = BS.splitAt 32 sig - in if r >= _CURVE_P || s >= _CURVE_Q - then False - else let e = modQ . roll $ hash_tagged "BIP0340/challenge" - (unroll32 r <> unroll32 x_P <> m) - dif = add (mul _CURVE_G s) (neg (mul (projective capP) e)) - in if dif == _ZERO - then False - else let Affine x_R y_R = affine dif - in not (I.integerTestBit y_R 0 || x_R /= r) +verify_schnorr m (affine -> Affine x_p _) sig + | BS.length sig /= 64 = False + | otherwise = case lift x_p of + Nothing -> False + Just capP@(Affine x_P _) -> + let (roll32 -> r, roll32 -> s) = BS.splitAt 32 sig + in if r >= _CURVE_P || s >= _CURVE_Q + then False + else let e = modQ . roll32 $ hash_tagged "BIP0340/challenge" + (unroll32 r <> unroll32 x_P <> m) + dif = add (mul_unsafe _CURVE_G s) + (neg (mul_unsafe (projective capP) e)) + in if dif == _ZERO + then False + else let Affine x_R y_R = affine dif + in not (I.integerTestBit y_R 0 || x_R /= r) -- ecdsa ---------------------------------------------------------------------- -- see https://www.rfc-editor.org/rfc/rfc6979, https://secg.org/sec1-v2.pdf @@ -747,7 +819,7 @@ _sign_ecdsa ty hf x m sign_loop g = do k <- gen_k g - let kg = mul _CURVE_G k + let kg = mul_unsafe _CURVE_G k Affine (modQ -> r) _ = affine kg s = case modinv k (fi _CURVE_Q) of Nothing -> error "ppad-secp256k1 (sign_ecdsa): bad k value" @@ -759,7 +831,6 @@ _sign_ecdsa ty hf x m in case ty of Unrestricted -> pure sig LowS -> pure (low sig) -{-# NOINLINE _sign_ecdsa #-} -- RFC6979 sec 3.3b gen_k :: DRBG.DRBG s -> ST s Integer @@ -823,7 +894,7 @@ verify_ecdsa_unrestricted (SHA256.hash -> h) p (ECDSA r s) Just si -> si u1 = remQ (e * s_inv) u2 = remQ (r * s_inv) - capR = add (mul _CURVE_G u1) (mul p u2) + capR = add (mul_unsafe _CURVE_G u1) (mul_unsafe p u2) in if capR == _ZERO then False else let Affine (modQ -> v) _ = affine capR