secp256k1

Pure Haskell cryptographic primitives on the secp256k1 elliptic curve.
git clone git://git.ppad.tech/secp256k1.git
Log | Files | Refs | README | LICENSE

commit aa6cf2bf770191fc18f1e8d1540ca0a0612e0a18
parent c44988dbfc3acec2b3fa950b9b0760507992e8c1
Author: Jared Tobin <jared@jtobin.io>
Date:   Tue, 15 Oct 2024 11:53:53 +0400

lib: basic reorg / cleanup

Diffstat:
Mlib/Crypto/Curve/Secp256k1.hs | 303++++++++++++++++++++++++++++++++++++-------------------------------------------
1 file changed, 137 insertions(+), 166 deletions(-)

diff --git a/lib/Crypto/Curve/Secp256k1.hs b/lib/Crypto/Curve/Secp256k1.hs @@ -6,35 +6,10 @@ {-# LANGUAGE UnboxedSums #-} {-# LANGUAGE ViewPatterns #-} --- XX clean up export list module Crypto.Curve.Secp256k1 ( - -- * Curve parameters and modular arithmetic utilities - _CURVE_P - , _CURVE_Q - , _CURVE_G - , modexp - , modinv - , modP - , modQ - , fe - , ge - - -- * Coordinate systems and transformations - , Affine(..) - , Projective(..) - , affine - , projective - , valid - - -- * Elliptic curve group operations - , neg - , add - , double - , mul - , mul_safe - - -- * Point parsing - , parse_point + -- * BIP0340 Schnorr signatures + sign_schnorr + , verify_schnorr -- * ECDSA , ECDSA(..) @@ -44,11 +19,23 @@ module Crypto.Curve.Secp256k1 ( , verify_ecdsa , verify_ecdsa_unrestricted - -- * Schnorr - , sign_schnorr - , verify_schnorr + -- * Point parsing + , parse_point + + -- Elliptic curve group operations + , neg + , add + , double + , mul - -- for testing + -- Coordinate systems and transformations + , Affine(..) + , Projective(..) + , affine + , projective + , valid + + -- for testing , _sign_ecdsa_no_hash ) where @@ -93,6 +80,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 +roll :: BS.ByteString -> Integer +roll = BS.foldl' alg 0 where + alg a (fi -> b) = (a `I.integerShiftL` 8) `I.integerOr` b + +-- big-endian bytestring encoding +unroll :: Integer -> BS.ByteString +unroll i = case i of + 0 -> BS.singleton 0 + _ -> BS.reverse $ BS.unfoldr step i -- XX looks slow + where + step 0 = Nothing + step m = Just (fi m, m `I.integerShiftR` 8) + +-- big-endian bytestring encoding for 256-bit ints, left-padding with +-- zeros if necessary. the size of the integer is not checked. +unroll32 :: Integer -> BS.ByteString +unroll32 (unroll -> u) + | l < 32 = BS.replicate (32 - l) 0 <> u + | otherwise = u + where + l = BS.length u + +-- (bip0340) tagged hash function +hash_tagged :: BS.ByteString -> BS.ByteString -> BS.ByteString +hash_tagged tag x = + let !h = SHA256.hash tag + in SHA256.hash (h <> h <> x) + +-- (bip0340) return point with x coordinate == x and with even y coordinate +lift :: Integer -> Maybe Affine +lift x + | not (fe x) = Nothing + | otherwise = + let c = modP (modexp x 3 _CURVE_P + 7) + y = modexp c ((_CURVE_P + 1) `div` 4) _CURVE_P + y_p + | y `rem` 2 == 0 = y + | otherwise = _CURVE_P - y + in if c /= modexp y 2 _CURVE_P + then Nothing + else Just $! (Affine x y_p) + -- coordinate systems & transformations --------------------------------------- -- curve point, affine coordinates @@ -119,7 +149,7 @@ instance Eq Projective where y2z1 = modP (by * az) in x1z2 == x2z1 && y1z2 == y2z1 --- | Convert to affine coordinates. +-- Convert to affine coordinates. affine :: Projective -> Affine affine p@(Projective x y z) | p == _ZERO = Affine 0 0 @@ -128,13 +158,13 @@ affine p@(Projective x y z) Nothing -> error "ppad-secp256k1 (affine): impossible point" Just iz -> Affine (modP (x * iz)) (modP (y * iz)) --- | Convert to projective coordinates. +-- Convert to projective coordinates. projective :: Affine -> Projective projective (Affine x y) | x == 0 && y == 0 = _ZERO | otherwise = Projective x y 1 --- | Point is valid +-- Point is valid valid :: Projective -> Bool valid p = case affine p of Affine x y @@ -151,8 +181,6 @@ valid p = case affine p of _CURVE_P :: Integer _CURVE_P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F --- XX can i make this abstract and use SPECIALIZE pragmas? - -- secp256k1 group order _CURVE_Q :: Integer _CURVE_Q = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 @@ -190,35 +218,36 @@ _CURVE_G = Projective x y 1 where _ZERO :: Projective _ZERO = Projective 0 1 0 --- | secp256k1 in prime order j-invariant 0 form (i.e. a == 0). +-- secp256k1 in prime order j-invariant 0 form (i.e. a == 0). weierstrass :: Integer -> Integer weierstrass x = modP (modP (x * x) * x + _CURVE_B) +{-# INLINE weierstrass #-} -- field, group operations ---------------------------------------------------- --- | Division modulo secp256k1 field prime. +-- Division modulo secp256k1 field prime. modP :: Integer -> Integer modP a = I.integerMod a _CURVE_P {-# INLINE modP #-} --- | Division modulo secp256k1 group order. +-- Division modulo secp256k1 group order. modQ :: Integer -> Integer modQ a = I.integerMod a _CURVE_Q {-# INLINE modQ #-} --- | Is field element? +-- Is field element? fe :: Integer -> Bool fe n = 0 < n && n < _CURVE_P {-# INLINE fe #-} --- | Is group element? +-- Is group element? ge :: Integer -> Bool ge n = 0 < n && n < _CURVE_Q {-# INLINE ge #-} --- | Square root (Shanks-Tonelli) modulo secp256k1 field prime. +-- Square root (Shanks-Tonelli) modulo secp256k1 field prime. -- --- For a, return x such that a = x x mod _CURVE_P. +-- For a, return x such that a = x x mod _CURVE_P. modsqrt :: Integer -> Maybe Integer modsqrt n = runST $ do r <- newSTRef 1 @@ -243,11 +272,11 @@ modsqrt n = runST $ do -- ec point operations -------------------------------------------------------- --- | Negate secp256k1 point. +-- Negate secp256k1 point. neg :: Projective -> Projective neg (Projective x y z) = Projective x (modP (negate y)) z --- | Elliptic curve addition on secp256k1. +-- Elliptic curve addition on secp256k1. add :: Projective -> Projective -> Projective add p q@(Projective _ _ z) | p == q = double p -- algo 9 @@ -424,7 +453,7 @@ 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. +-- Scalar multiplication of secp256k1 points. mul :: Projective -> Integer -> Projective mul p n | n == 0 = _ZERO @@ -439,25 +468,10 @@ mul p n nr = if I.integerTestBit m 0 then add r d else r in loop nr nd nm --- | Safe scalar multiplication of secp256k1 points. -mul_safe :: Projective -> Integer -> Projective -mul_safe p n - | not (ge n) = error "ppad-secp256k1 (mul_safe): scalar not in group" - | otherwise = loop _ZERO _CURVE_G p n - where - loop !r !f !d m - | m <= 0 = r - | otherwise = - let nd = double d - nm = I.integerShiftR m 1 - in if I.integerTestBit m 0 - then loop (add r d) f nd nm - else loop r (add f d) nd nm - -- parsing -------------------------------------------------------------------- --- | Parse hex-encoded compressed or uncompressed point, or BIP0340 --- public key. +-- | Parse hex-encoded compressed point (33 bytes), uncompressed point +-- (65 bytes), or BIP0340-style point (32 bytes). parse_point :: BS.ByteString -> Maybe Projective parse_point (B16.decode -> ebs) = case ebs of Left _ -> Nothing @@ -489,28 +503,67 @@ parse_point (B16.decode -> ebs) = case ebs of else Nothing else Nothing --- 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 +-- schnorr -------------------------------------------------------------------- +-- see https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki --- unroll a 256-bit integer, left-padding with zeros if necessary --- the size of the integer is not checked -unroll32 :: Integer -> BS.ByteString -unroll32 (unroll -> u) - | l < 32 = BS.replicate (32 - l) 0 <> u - | otherwise = u - where - l = BS.length u +sign_schnorr + :: Integer -- ^ secret key + -> BS.ByteString -- ^ message + -> BS.ByteString -- ^ 32 bytes of auxilliary random data + -> BS.ByteString -- ^ 64-byte schnorr signature +sign_schnorr d' m a + | not (ge d') = error "ppad-secp256k1 (sign_schnorr): invalid secret key" + | otherwise = + let p@(Affine x_p y_p) = affine (mul _CURVE_G d') + d | y_p `rem` 2 == 0 = d' -- d' group element assures p nonzero + | otherwise = _CURVE_Q - d' --- big-endian bytestring encoding -unroll :: Integer -> BS.ByteString -unroll i = case i of - 0 -> BS.singleton 0 - _ -> BS.reverse $ BS.unfoldr step i -- XX looks slow - where - step 0 = Nothing - step m = Just (fi m, m `I.integerShiftR` 8) + bytes_d = unroll32 d + h_a = hash_tagged "BIP0340/aux" a + t = xor bytes_d h_a + + bytes_p = unroll32 x_p + rand = hash_tagged "BIP0340/nonce" (t <> bytes_p <> m) + + k' = modQ (roll 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') + k | y_r `rem` 2 == 0 = k' -- k' nonzero per above + | otherwise = _CURVE_Q - k' + + bytes_r = unroll32 x_r + e = modQ . roll . hash_tagged "BIP0340/challenge" + $ bytes_r <> bytes_p <> m + + bytes_ked = unroll32 (modQ (k + e * d)) + + sig = bytes_r <> bytes_ked + + in if verify_schnorr m p sig + then sig + else error "ppad-secp256k1 (sign_schnorr): invalid signature" + +verify_schnorr + :: BS.ByteString -- ^ message + -> Affine -- ^ public key + -> BS.ByteString -- ^ 64-byte schnorr signature + -> Bool +verify_schnorr m (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 (y_R `rem` 2 /= 0 || x_R /= r) -- ecdsa ---------------------------------------------------------------------- -- see https://www.rfc-editor.org/rfc/rfc6979, https://secg.org/sec1-v2.pdf @@ -617,7 +670,6 @@ _sign_ecdsa ty hf x m Affine (modQ -> r) _ = affine kg s = case modinv k (fi _CURVE_Q) of Nothing -> error "ppad-secp256k1 (sign_ecdsa): bad k value" - -- XX check timing implications of mod division of secret by Q Just kinv -> modQ (modQ (h_modQ + modQ (x * r)) * kinv) if r == 0 -- negligible probability then sign_loop g @@ -681,84 +733,3 @@ verify_ecdsa_unrestricted (SHA256.hash -> h) p (ECDSA r s) else let Affine (modQ -> v) _ = affine capR in v == r --- schnorr -------------------------------------------------------------------- --- see https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki - -hash_tagged :: BS.ByteString -> BS.ByteString -> BS.ByteString -hash_tagged tag x = - let !h = SHA256.hash tag - in SHA256.hash (h <> h <> x) - --- return point with x coordinate == x and with even y coordinate -lift :: Integer -> Maybe Affine -lift x - | not (fe x) = Nothing - | otherwise = - let c = modP (modexp x 3 _CURVE_P + 7) - y = modexp c ((_CURVE_P + 1) `div` 4) _CURVE_P - y_p - | y `rem` 2 == 0 = y - | otherwise = _CURVE_P - y - in if c /= modexp y 2 _CURVE_P - then Nothing - else Just $! (Affine x y_p) - -sign_schnorr - :: Integer -- ^ secret key - -> BS.ByteString -- ^ message - -> BS.ByteString -- ^ 32 bytes of auxilliary random data - -> BS.ByteString -- ^ 64-byte schnorr signature -sign_schnorr d' m a - | not (ge d') = error "ppad-secp256k1 (sign_schnorr): invalid secret key" - | otherwise = - let p@(Affine x_p y_p) = affine (mul _CURVE_G d') - d | y_p `rem` 2 == 0 = d' - | otherwise = _CURVE_Q - d' - - bytes_d = unroll32 d - h_a = hash_tagged "BIP0340/aux" a - t = xor bytes_d h_a - - bytes_p = unroll32 x_p - rand = hash_tagged "BIP0340/nonce" (t <> bytes_p <> m) - - k' = modQ (roll 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') - k | y_r `rem` 2 == 0 = k' - | otherwise = _CURVE_Q - k' - - bytes_r = unroll32 x_r - e = modQ . roll . hash_tagged "BIP0340/challenge" - $ bytes_r <> bytes_p <> m - - bytes_ked = unroll32 (modQ (k + e * d)) - - sig = bytes_r <> bytes_ked - - in if verify_schnorr m p sig - then sig - else error "ppad-secp256k1 (sign_schnorr): invalid signature" - -verify_schnorr - :: BS.ByteString -- ^ message - -> Affine -- ^ public key - -> BS.ByteString -- ^ 64-byte schnorr signature - -> Bool -verify_schnorr m (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 (y_R `rem` 2 /= 0 || x_R /= r) -