csecp256k1

Haskell FFI bindings to bitcoin-core/secp256k1 (docs.ppad.tech/csecp256k1).
git clone git://git.ppad.tech/csecp256k1.git
Log | Files | Refs | README | LICENSE

commit 0d5916fe6754c86380ed81de58a40fc22c015257
parent b9f7f47b6d46c928307351c1ddef59fa92253bb7
Author: Jared Tobin <jared@jtobin.io>
Date:   Thu,  7 Nov 2024 15:39:46 +0400

test: wycheproof skeleton work

Diffstat:
Mflake.lock | 29++++++++++++++++++++++++++++-
Mflake.nix | 18++++++++++++++----
Mppad-csecp256k1.cabal | 29++++++++---------------------
Mtest/Main.hs | 23+++++++++++++++++++++--
Mtest/Wycheproof.hs | 275++++++++++++++++++++++++++++++++-----------------------------------------------
5 files changed, 181 insertions(+), 193 deletions(-)

diff --git a/flake.lock b/flake.lock @@ -34,10 +34,37 @@ "type": "github" } }, - "root": { + "ppad-sha256": { "inputs": { "flake-utils": "flake-utils", "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1728824578, + "narHash": "sha256-sMZLvmbl3fBUHpBePzGuLdUyUALba/P5DCgKVgqEH1k=", + "ref": "master", + "rev": "3cc6c146ae36f67d73c6c207486a199dbc77658e", + "revCount": 82, + "type": "git", + "url": "git://git.ppad.tech/sha256.git" + }, + "original": { + "ref": "master", + "type": "git", + "url": "git://git.ppad.tech/sha256.git" + } + }, + "root": { + "inputs": { + "flake-utils": [ + "ppad-sha256", + "flake-utils" + ], + "nixpkgs": [ + "ppad-sha256", + "nixpkgs" + ], + "ppad-sha256": "ppad-sha256" } }, "systems": { diff --git a/flake.nix b/flake.nix @@ -2,11 +2,16 @@ description = "ppad-csecp256k1"; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; - flake-utils.url = "github:numtide/flake-utils"; + ppad-sha256 = { + type = "git"; + url = "git://git.ppad.tech/sha256.git"; + ref = "master"; + }; + flake-utils.follows = "ppad-sha256/flake-utils"; + nixpkgs.follows = "ppad-sha256/nixpkgs"; }; - outputs = { self, nixpkgs, flake-utils }: + outputs = { self, nixpkgs, flake-utils, ppad-sha256 }: flake-utils.lib.eachDefaultSystem (system: let lib = "ppad-csecp256k1"; @@ -14,8 +19,13 @@ pkgs = import nixpkgs { inherit system; }; hlib = pkgs.haskell.lib; + sha256 = ppad-sha256.packages.${system}.default; + hpkgs = pkgs.haskell.packages.ghc981.extend (new: old: { - ${lib} = old.callCabal2nix lib ./. {}; + ppad-sha256 = sha256; + ${lib} = new.callCabal2nix lib ./. { + ppad-sha256 = new.ppad-sha256; + }; }); cc = pkgs.stdenv.cc; diff --git a/ppad-csecp256k1.cabal b/ppad-csecp256k1.cabal @@ -39,16 +39,23 @@ test-suite csecp256k1-tests default-language: Haskell2010 hs-source-dirs: test main-is: Main.hs + other-modules: + Wycheproof ghc-options: -rtsopts -Wall build-depends: - base + aeson + , attoparsec + , base + , base16-bytestring , bytestring , ppad-csecp256k1 + , ppad-sha256 , tasty , tasty-hunit + , text benchmark csecp256k1-bench type: exitcode-stdio-1.0 @@ -117,23 +124,3 @@ test-suite secp256k1-sys-tests , tasty , tasty-hunit -test-suite wycheproof - type: exitcode-stdio-1.0 - default-language: Haskell2010 - hs-source-dirs: test - main-is: Wycheproof.hs - - ghc-options: - -rtsopts -Wall - - build-depends: - aeson - , attoparsec - , base - , base16-bytestring - , bytestring - , ppad-csecp256k1 - , tasty - , tasty-hunit - , text - diff --git a/test/Main.hs b/test/Main.hs @@ -1,14 +1,33 @@ {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} module Main where -import qualified Data.ByteString as BS import Crypto.Curve.Secp256k1 +import qualified Data.Aeson as A +import qualified Data.ByteString as BS +import qualified Data.Text.IO as TIO import Test.Tasty import Test.Tasty.HUnit +import qualified Wycheproof as W main :: IO () -main = defaultMain units +main = do + wp_ecdsa_sha256_bitcoin <- TIO.readFile + "etc/ecdsa_secp256k1_sha256_bitcoin_test.json" + let vec = A.decodeStrictText wp_ecdsa_sha256_bitcoin :: Maybe W.Wycheproof + case vec of + Nothing -> error "couldn't parse wycheproof vectors" + Just W.Wycheproof {..} -> wcontext $ \tex -> do + tree <- traverse (W.execute_group tex) wp_testGroups + defaultMain $ testGroup "ppad-csecp256k1" [ + units + , wycheproof_ecdsa_verify_tests "(ecdsa, sha256, low-s)" tree + ] + +wycheproof_ecdsa_verify_tests :: String -> [TestTree] -> TestTree +wycheproof_ecdsa_verify_tests msg ts = + testGroup ("wycheproof vectors " <> msg) ts units :: TestTree units = testGroup "unit tests" [ diff --git a/test/Wycheproof.hs b/test/Wycheproof.hs @@ -3,170 +3,115 @@ {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ViewPatterns #-} -module Main where +module Wycheproof ( + Wycheproof(..) + , execute_group + ) where -main = pure () +import Control.Exception +import Crypto.Curve.Secp256k1 +import qualified Crypto.Hash.SHA256 as SHA256 +import Data.Aeson ((.:)) +import qualified Data.Aeson as A +import qualified Data.Attoparsec.ByteString as AT +import qualified Data.Bits as B +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) + +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 -> EcdsaTestGroup -> IO TestTree +execute_group tex EcdsaTestGroup {..} = do + let raw = B16.decodeLenient (TE.encodeUtf8 pk_uncompressed) + pub <- parse_pub tex raw + let tests = fmap (execute tex pub) etg_tests + pure (testGroup msg tests) + where + msg = "wycheproof (" <> T.unpack etg_type <> ", " <> T.unpack etg_sha <> ")" + PublicKey {..} = etg_publicKey + +execute :: Context -> Pub -> EcdsaVerifyTest -> TestTree +execute tex pub EcdsaVerifyTest {..} = testCase report $ do + let msg = B16.decodeLenient (TE.encodeUtf8 t_msg) + sig = B16.decodeLenient (TE.encodeUtf8 t_sig) + syg <- try (parse_der tex sig) :: IO (Either Secp256k1Exception Sig) + case syg of + Left _ -> assertBool mempty (t_result == "invalid") + Right s -> do + ver <- verify_ecdsa tex pub msg s + if t_result == "invalid" + then assertBool mempty (not ver) + else assertBool mempty ver + where + report = "wycheproof " <> show t_tcId + +data Wycheproof = Wycheproof { + wp_algorithm :: !T.Text + , wp_generatorVersion :: !T.Text + , wp_numberOfTests :: !Int + , wp_testGroups :: ![EcdsaTestGroup] + } deriving Show + +instance A.FromJSON Wycheproof where + parseJSON = A.withObject "Wycheproof" $ \m -> Wycheproof + <$> m .: "algorithm" + <*> m .: "generatorVersion" + <*> m .: "numberOfTests" + <*> m .: "testGroups" + +data EcdsaTestGroup = EcdsaTestGroup { + etg_type :: !T.Text + , etg_publicKey :: !PublicKey + , etg_sha :: !T.Text + , etg_tests :: ![EcdsaVerifyTest] + } deriving Show + +instance A.FromJSON EcdsaTestGroup where + parseJSON = A.withObject "EcdsaTestGroup" $ \m -> EcdsaTestGroup + <$> m .: "type" + <*> m .: "publicKey" + <*> m .: "sha" + <*> m .: "tests" + +data PublicKey = PublicKey { + pk_type :: !T.Text + , pk_curve :: !T.Text + , pk_keySize :: !Int + , pk_uncompressed :: !T.Text + } deriving Show + +instance A.FromJSON PublicKey where + parseJSON = A.withObject "PublicKey" $ \m -> PublicKey + <$> m .: "type" + <*> m .: "curve" + <*> m .: "keySize" + <*> m .: "uncompressed" + +data EcdsaVerifyTest = EcdsaVerifyTest { + t_tcId :: !Int + , t_comment :: !T.Text + , t_msg :: !T.Text + , t_sig :: !T.Text + , t_result :: !T.Text + } deriving Show + +instance A.FromJSON EcdsaVerifyTest where + parseJSON = A.withObject "EcdsaVerifyTest" $ \m -> EcdsaVerifyTest + <$> m .: "tcId" + <*> m .: "comment" + <*> m .: "msg" + <*> m .: "sig" + <*> m .: "result" --- module Wycheproof ( --- Wycheproof(..) --- , execute_group --- ) where --- --- import Crypto.Curve.Secp256k1 --- import Data.Aeson ((.:)) --- import qualified Data.Aeson as A --- import qualified Data.Attoparsec.ByteString as AT --- import qualified Data.Bits as B --- 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) --- --- 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 :: SigType -> EcdsaTestGroup -> TestTree --- execute_group ty EcdsaTestGroup {..} = --- testGroup msg (fmap (execute ty pk_uncompressed) etg_tests) --- where --- msg = "wycheproof (" <> T.unpack etg_type <> ", " <> T.unpack etg_sha <> ")" --- PublicKey {..} = etg_publicKey --- --- execute :: SigType -> Projective -> EcdsaVerifyTest -> TestTree --- execute ty pub EcdsaVerifyTest {..} = testCase report $ do --- let msg = B16.decodeLenient (TE.encodeUtf8 t_msg) --- sig = toEcdsa t_sig --- case sig of --- Left _ -> assertBool mempty (t_result == "invalid") --- Right s -> do --- let ver = case ty of --- LowS -> verify_ecdsa msg pub s --- Unrestricted -> verify_ecdsa_unrestricted msg pub s --- if t_result == "invalid" --- then assertBool mempty (not ver) --- else assertBool mempty ver --- where --- report = "wycheproof (" <> show ty <> ") " <> show t_tcId --- --- parse_der_sig :: AT.Parser ECDSA --- parse_der_sig = do --- _ <- AT.word8 0x30 --- len <- fmap fi AT.anyWord8 --- content <- AT.take len --- etc <- AT.takeByteString --- if BS.length content /= len || etc /= mempty --- then fail "invalid content" --- else case AT.parseOnly (meat len) content of --- Left _ -> fail "invalid content" --- Right v -> pure v --- where --- 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" --- --- parseAsnInt :: AT.Parser (Int, BS.ByteString) --- parseAsnInt = do --- _ <- AT.word8 0x02 --- len <- fmap fi AT.anyWord8 --- content <- AT.take len --- if BS.length content /= len --- then fail "invalid length" --- else if len == 1 --- then pure (len + 2, content) -- + tag byt + len byt --- else case BS.uncons content of --- Nothing -> fail "invalid content" --- Just (h0, t0) --- | B.testBit h0 7 -> fail "negative value" --- | otherwise -> case BS.uncons t0 of --- Nothing -> fail "invalid content" --- Just (h1, _) --- | h0 == 0x00 && not (B.testBit h1 7) -> fail "invalid padding" --- | otherwise -> case BS.unsnoc content of --- Nothing -> fail "invalid content" --- Just (_, tn) --- | tn == 0x00 -> fail "invalid padding" --- | otherwise -> pure (len + 2, content) --- --- data Wycheproof = Wycheproof { --- wp_algorithm :: !T.Text --- , wp_generatorVersion :: !T.Text --- , wp_numberOfTests :: !Int --- , wp_testGroups :: ![EcdsaTestGroup] --- } deriving Show --- --- instance A.FromJSON Wycheproof where --- parseJSON = A.withObject "Wycheproof" $ \m -> Wycheproof --- <$> m .: "algorithm" --- <*> m .: "generatorVersion" --- <*> m .: "numberOfTests" --- <*> m .: "testGroups" --- --- data EcdsaTestGroup = EcdsaTestGroup { --- etg_type :: !T.Text --- , etg_publicKey :: !PublicKey --- , etg_sha :: !T.Text --- , etg_tests :: ![EcdsaVerifyTest] --- } deriving Show --- --- instance A.FromJSON EcdsaTestGroup where --- parseJSON = A.withObject "EcdsaTestGroup" $ \m -> EcdsaTestGroup --- <$> m .: "type" --- <*> m .: "publicKey" --- <*> m .: "sha" --- <*> m .: "tests" --- --- data PublicKey = PublicKey { --- pk_type :: !T.Text --- , pk_curve :: !T.Text --- , pk_keySize :: !Int --- , pk_uncompressed :: !Projective --- } deriving Show --- --- toProjective :: T.Text -> Projective --- toProjective (B16.decodeLenient . TE.encodeUtf8 -> bs) = case parse_point bs of --- Nothing -> error "wycheproof: couldn't parse pubkey" --- Just p -> p --- --- instance A.FromJSON PublicKey where --- parseJSON = A.withObject "PublicKey" $ \m -> PublicKey --- <$> m .: "type" --- <*> m .: "curve" --- <*> m .: "keySize" --- <*> fmap toProjective (m .: "uncompressed") --- --- toEcdsa :: T.Text -> Either String ECDSA --- toEcdsa (B16.decodeLenient . TE.encodeUtf8 -> bs) = --- AT.parseOnly parse_der_sig bs --- --- data EcdsaVerifyTest = EcdsaVerifyTest { --- t_tcId :: !Int --- , t_comment :: !T.Text --- , t_msg :: !T.Text --- , t_sig :: !T.Text --- , t_result :: !T.Text --- } deriving Show --- --- instance A.FromJSON EcdsaVerifyTest where --- parseJSON = A.withObject "EcdsaVerifyTest" $ \m -> EcdsaVerifyTest --- <$> m .: "tcId" --- <*> m .: "comment" --- <*> m .: "msg" --- <*> m .: "sig" --- <*> m .: "result" ---