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 5d1ba01983c7e005d4cb88227f6eee7932267242
parent da830adde161d1b87e1355423be5b96f207f5412
Author: Jared Tobin <jared@jtobin.io>
Date:   Fri, 19 Dec 2025 10:18:22 -0330

test: passing again

Diffstat:
Mlib/Crypto/Curve/Secp256k1.hs | 36++++++++++++++++++++----------------
Mppad-secp256k1.cabal | 1+
Mtest/BIP340.hs | 13+------------
Mtest/Noble.hs | 19+++++--------------
Mtest/Wycheproof.hs | 25++++++++++++-------------
Mtest/WycheproofEcdh.hs | 29+++++------------------------
6 files changed, 44 insertions(+), 79 deletions(-)

diff --git a/lib/Crypto/Curve/Secp256k1.hs b/lib/Crypto/Curve/Secp256k1.hs @@ -87,6 +87,9 @@ module Crypto.Curve.Secp256k1 ( -- for testing/benchmarking , _sign_ecdsa_no_hash , _sign_ecdsa_no_hash' + , roll32 + , unsafe_roll32 + , unroll32 ) where import Control.Monad (guard) @@ -218,10 +221,14 @@ unsafe_roll32 bs = {-# INLINABLE unsafe_roll32 #-} -- arbitrary-size big-endian bytestring decoding -roll :: BS.ByteString -> Wider -roll = BS.foldl' alg 0 where - alg !a (word8_to_wider -> !b) = (a `W.shl_limb` 8) `W.or` b -{-# INLINABLE roll #-} +roll32 :: BS.ByteString -> Maybe Wider +roll32 bs + | BS.length stripped > 32 = Nothing + | otherwise = Just $! BS.foldl' alg 0 stripped + where + stripped = BS.dropWhile (== 0) bs + alg !a (word8_to_wider -> !b) = (a `W.shl_limb` 8) `W.or` b +{-# INLINABLE roll32 #-} -- 256-bit big-endian bytestring encoding unroll32 :: Wider -> BS.ByteString @@ -314,16 +321,12 @@ _CURVE_Bm3 = 21 -- Is field element? fe :: Wider -> Bool -fe n = case W.cmp n _CURVE_P of - LT -> True - _ -> False +fe n = n > 0 && n < _CURVE_P {-# INLINE fe #-} -- Is group element? ge :: Wider -> Bool -ge n = case W.cmp n _CURVE_Q of - LT -> True - _ -> False +ge n = n > 0 && n < _CURVE_Q {-# INLINE ge #-} -- curve points --------------------------------------------------------------- @@ -798,7 +801,7 @@ _parse_compressed h (unsafe_roll32 -> x) _parse_uncompressed :: Word8 -> BS.ByteString -> Maybe Projective _parse_uncompressed h bs = do let (unsafe_roll32 -> x, unsafe_roll32 -> y) = BS.splitAt _CURVE_Q_BYTES bs - guard (h /= 0x04) + guard (h == 0x04) let !p = Projective (C.to x) (C.to y) 1 guard (valid p) pure $! p @@ -808,11 +811,12 @@ _parse_uncompressed h bs = do -- >>> parse_sig <64-byte compact signature> -- Just "<ecdsa signature>" parse_sig :: BS.ByteString -> Maybe ECDSA -parse_sig bs - | BS.length bs /= 64 = Nothing - | otherwise = pure $ - let (roll -> r, roll -> s) = BS.splitAt 32 bs - in ECDSA r s +parse_sig bs = do + guard (BS.length bs == 64) + let (r0, s0) = BS.splitAt 32 bs + r <- roll32 r0 + s <- roll32 s0 + pure $! ECDSA r s -- serializing ---------------------------------------------------------------- diff --git a/ppad-secp256k1.cabal b/ppad-secp256k1.cabal @@ -54,6 +54,7 @@ test-suite secp256k1-tests , base , bytestring , ppad-base16 + , ppad-fixed , ppad-secp256k1 , ppad-sha256 , tasty diff --git a/test/BIP340.hs b/test/BIP340.hs @@ -13,25 +13,14 @@ import Crypto.Curve.Secp256k1 import qualified Data.Attoparsec.ByteString.Char8 as AT import qualified Data.ByteString as BS import qualified Data.ByteString.Base16 as B16 -import qualified GHC.Num.Integer as I import Test.Tasty import Test.Tasty.HUnit --- XX make a test prelude instead of copying/pasting these things everywhere - decodeLenient :: BS.ByteString -> BS.ByteString decodeLenient bs = case B16.decode bs of Nothing -> error "bang" Just b -> b -fi :: (Integral a, Num b) => a -> b -fi = fromIntegral -{-# INLINE fi #-} - -roll :: BS.ByteString -> Integer -roll = BS.foldl' unstep 0 where - unstep a (fi -> b) = (a `I.integerShiftL` 8) `I.integerOr` b - data Case = Case { c_index :: !Int , c_sk :: !BS.ByteString @@ -61,7 +50,7 @@ execute tex Case {..} = testCase ("bip0340 " <> show c_index) $ assertBool mempty (not ver') -- XX test pubkey derivation from sk else do -- signature present; test sig too - let sk = roll c_sk + let sk = unsafe_roll32 c_sk Just sig = sign_schnorr sk c_msg c_aux Just sig' = sign_schnorr' tex sk c_msg c_aux ver = verify_schnorr c_msg pk sig diff --git a/test/Noble.hs b/test/Noble.hs @@ -17,7 +17,7 @@ import qualified Data.ByteString as BS import qualified Data.ByteString.Base16 as B16 import qualified Data.Text as T import qualified Data.Text.Encoding as TE -import qualified GHC.Num.Integer as I +import Data.Word.Wider (Wider(..)) import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (assertEqual, assertBool, assertFailure, testCase) @@ -77,22 +77,13 @@ execute_invalid_verify tex (label, InvalidVerifyTest {..}) = assertBool mempty (not ver) assertBool mempty (not ver') -fi :: (Integral a, Num b) => a -> b -fi = fromIntegral -{-# INLINE fi #-} - -- parser helper toBS :: T.Text -> BS.ByteString toBS = decodeLenient . TE.encodeUtf8 -- parser helper -toSecKey :: T.Text -> Integer -toSecKey = roll . toBS - --- big-endian bytestring decoding -roll :: BS.ByteString -> Integer -roll = BS.foldl' unstep 0 where - unstep a (fi -> b) = (a `I.integerShiftL` 8) `I.integerOr` b +toSecKey :: T.Text -> Wider +toSecKey = unsafe_roll32 . toBS instance A.FromJSON Ecdsa where parseJSON = A.withObject "Ecdsa" $ \m -> Ecdsa @@ -100,7 +91,7 @@ instance A.FromJSON Ecdsa where <*> m .: "invalid" data ValidTest = ValidTest { - vt_d :: !Integer + vt_d :: !Wider , vt_m :: !BS.ByteString , vt_signature :: !BS.ByteString } deriving Show @@ -127,7 +118,7 @@ instance A.FromJSON InvalidTest where <*> fmap (zip [0..]) (m .: "verify") data InvalidSignTest = InvalidSignTest { - ivs_d :: !Integer + ivs_d :: !Wider , ivs_m :: !BS.ByteString } deriving Show diff --git a/test/Wycheproof.hs b/test/Wycheproof.hs @@ -17,7 +17,6 @@ import qualified Data.ByteString as BS import qualified Data.ByteString.Base16 as B16 import qualified Data.Text as T import qualified Data.Text.Encoding as TE -import qualified GHC.Num.Integer as I import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (assertBool, testCase) @@ -30,11 +29,6 @@ fi :: (Integral a, Num b) => a -> b fi = fromIntegral {-# INLINE fi #-} --- big-endian bytestring decoding -roll :: BS.ByteString -> Integer -roll = BS.foldl' unstep 0 where - unstep a (fi -> b) = (a `I.integerShiftL` 8) `I.integerOr` b - execute_group :: Context -> SigType -> EcdsaTestGroup -> TestTree execute_group tex ty EcdsaTestGroup {..} = testGroup msg (fmap (execute tex ty pk_uncompressed) etg_tests) @@ -74,13 +68,18 @@ parse_der_sig = do meat len = do (lr, bs_r) <- parseAsnInt (ls, bs_s) <- parseAsnInt - let r = fi (roll bs_r) - s = fi (roll bs_s) - checks = lr + ls == len - rest <- AT.takeByteString - if rest == mempty && checks - then pure (ECDSA r s) - else fail "input remaining or length mismatch" + let rs = do + r <- roll32 bs_r + s <- roll32 bs_s + pure (r, s) + case rs of + Nothing -> fail "signature components too large" + Just (r, s) -> do + let checks = lr + ls == len + rest <- AT.takeByteString + if rest == mempty && checks + then pure (ECDSA r s) + else fail "input remaining or length mismatch" parseAsnInt :: AT.Parser (Int, BS.ByteString) parseAsnInt = do diff --git a/test/WycheproofEcdh.hs b/test/WycheproofEcdh.hs @@ -13,11 +13,11 @@ import qualified Crypto.Hash.SHA256 as SHA256 import Data.Aeson ((.:)) import qualified Data.Aeson as A import qualified Data.Attoparsec.ByteString as AT -import Data.Bits ((.<<.), (.>>.), (.|.)) import qualified Data.ByteString as BS import qualified Data.ByteString.Base16 as B16 import qualified Data.Text as T import qualified Data.Text.Encoding as TE +import Data.Word.Wider (Wider(..)) import Test.Tasty (TestTree, testGroup) import qualified Test.Tasty.HUnit as H (assertBool, assertEqual, testCase) @@ -137,29 +137,10 @@ der_to_pub :: T.Text -> Either String Projective der_to_pub (decodeLenient . TE.encodeUtf8 -> bs) = AT.parseOnly parse_der_pub bs -parse_bigint :: T.Text -> Integer -parse_bigint (decodeLenient . TE.encodeUtf8 -> bs) = roll bs where - roll :: BS.ByteString -> Integer - roll = BS.foldl' alg 0 where - alg !a (fi -> !b) = (a .<<. 8) .|. b - --- big-endian bytestring encoding -unroll :: Integer -> BS.ByteString -unroll i = case i of - 0 -> BS.singleton 0 - _ -> BS.reverse $ BS.unfoldr step i - where - step 0 = Nothing - step m = Just (fi m, m .>>. 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 +parse_bigint :: T.Text -> Wider +parse_bigint (decodeLenient . TE.encodeUtf8 -> bs) = case roll32 bs of + Nothing -> error "couldn't parse_bigint" + Just v -> v data Wycheproof = Wycheproof { wp_testGroups :: ![EcdhTestGroup]