secp256k1

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

commit bd500270efe9a96fd477fa192b9abfc4c786647c
parent 9c44769dfa00e89cc8971bd5748c42cdbbb3edfe
Author: Jared Tobin <jared@jtobin.io>
Date:   Thu, 10 Oct 2024 16:39:58 +0400

test: noble valid suite passing

Diffstat:
Retc/ecdsa.json -> etc/noble_ecdsa.json | 0
Mlib/Crypto/Curve/Secp256k1.hs | 43+++++++++++++++++++++++++++++++++++++------
Mppad-secp256k1.cabal | 3++-
Mtest/Main.hs | 37+++++++++----------------------------
Atest/Noble.hs | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/Wycheproof.hs | 32++++++++++++++++++++++++++++++--
6 files changed, 191 insertions(+), 37 deletions(-)

diff --git a/etc/ecdsa.json b/etc/noble_ecdsa.json diff --git a/lib/Crypto/Curve/Secp256k1.hs b/lib/Crypto/Curve/Secp256k1.hs @@ -7,7 +7,14 @@ {-# LANGUAGE ViewPatterns #-} module Crypto.Curve.Secp256k1 ( - Affine(..) + _CURVE_G + , _ZERO + , _CURVE_A + , _CURVE_B + , _CURVE_P + , _CURVE_Q + + , Affine(..) , Projective(..) , affine , projective @@ -23,9 +30,12 @@ module Crypto.Curve.Secp256k1 ( , ECDSA(..) , SigType(..) , sign - , verify , sign_unrestricted + , verify , verify_unrestricted + + -- for testing + , _sign_no_hash ) where import Control.Monad (when) @@ -497,11 +507,28 @@ data ECDSA = ECDSA { } deriving (Eq, Show, Generic) +-- ECDSA signature type. data SigType = LowS | Unrestricted deriving Show +-- Hash message, or assume already hashed. +data HashFlag = + Hash + | NoHash + deriving Show + +-- Produce a "low-s" ECDSA signature for the provided message, using +-- the provided private key. +-- +-- Assumes that the message has already been pre-hashed. +-- +-- Useful for testing against noble-secp256k1's suite, in which messages +-- have already been hashed. +_sign_no_hash :: Integer -> BS.ByteString -> ECDSA +_sign_no_hash = _sign LowS NoHash + -- | Produce an ECDSA signature for the provided message, using the -- provided private key. -- @@ -512,7 +539,7 @@ sign :: Integer -> BS.ByteString -> ECDSA -sign = _sign LowS +sign = _sign LowS Hash -- | Produce an ECDSA signature for the provided message, using the -- provided private key. @@ -524,10 +551,10 @@ sign_unrestricted :: Integer -> BS.ByteString -> ECDSA -sign_unrestricted = _sign Unrestricted +sign_unrestricted = _sign Unrestricted Hash -_sign :: SigType -> Integer -> BS.ByteString -> ECDSA -_sign ty x (SHA256.hash -> h) = runST $ do +_sign :: SigType -> HashFlag -> Integer -> BS.ByteString -> ECDSA +_sign ty hf x m = runST $ do -- RFC6979 sec 3.3a let entropy = int2octets x nonce = bits2octets h @@ -552,6 +579,10 @@ _sign ty x (SHA256.hash -> h) = runST $ do Unrestricted -> pure sig LowS -> pure (low sig) + h = case hf of + Hash -> SHA256.hash m + NoHash -> m + -- RFC6979 sec 3.3b gen_k :: DRBG.DRBG s -> ST s Integer gen_k g = loop g where diff --git a/ppad-secp256k1.cabal b/ppad-secp256k1.cabal @@ -38,7 +38,8 @@ test-suite secp256k1-tests hs-source-dirs: test main-is: Main.hs other-modules: - Wycheproof + Noble + , Wycheproof ghc-options: -rtsopts -Wall diff --git a/test/Main.hs b/test/Main.hs @@ -16,6 +16,7 @@ import Test.Tasty import Test.Tasty.HUnit import qualified Data.Text.IO as TIO import qualified Data.Text.Encoding as TE +import qualified Noble as N import qualified Wycheproof as W fi :: (Integral a, Num b) => a -> b @@ -27,45 +28,25 @@ main = do wp_ecdsa_sha256 <- TIO.readFile "etc/ecdsa_secp256k1_sha256_test.json" wp_ecdsa_sha256_bitcoin <- TIO.readFile "etc/ecdsa_secp256k1_sha256_bitcoin_test.json" - let pair = do + noble_ecdsa <- TIO.readFile "etc/noble_ecdsa.json" + let trip = do wp0 <- A.decodeStrictText wp_ecdsa_sha256 :: Maybe W.Wycheproof wp1 <- A.decodeStrictText wp_ecdsa_sha256_bitcoin :: Maybe W.Wycheproof - pure (wp0, wp1) - case pair of + nob <- A.decodeStrictText noble_ecdsa :: Maybe N.Ecdsa + pure (wp0, wp1, nob) + case trip of Nothing -> error "couldn't parse wycheproof vectors" - Just (w0, w1) -> defaultMain $ testGroup "ppad-secp256k1" [ + Just (w0, w1, no) -> defaultMain $ testGroup "ppad-secp256k1" [ units , wycheproof_ecdsa_verify_tests "(ecdsa, sha256)" Unrestricted w0 , wycheproof_ecdsa_verify_tests "(ecdsa, sha256, low-s)" LowS w1 + , N.execute_ecdsa no ] wycheproof_ecdsa_verify_tests :: String -> SigType -> W.Wycheproof -> TestTree wycheproof_ecdsa_verify_tests msg ty W.Wycheproof {..} = testGroup ("wycheproof vectors " <> msg) $ - fmap (execute_group ty) wp_testGroups - -execute_group :: SigType -> W.EcdsaTestGroup -> TestTree -execute_group ty W.EcdsaTestGroup {..} = - testGroup msg (fmap (execute ty pk_uncompressed) etg_tests) - where - msg = mempty - W.PublicKey {..} = etg_publicKey - -execute :: SigType -> Projective -> W.EcdsaVerifyTest -> TestTree -execute ty pub W.EcdsaVerifyTest {..} = testCase report $ do - let msg = B16.decodeLenient (TE.encodeUtf8 t_msg) - sig = W.toEcdsa t_sig - case sig of - Left _ -> assertBool mempty (t_result == "invalid") - Right s -> do - let ver = case ty of - LowS -> verify msg pub s - Unrestricted -> verify_unrestricted msg pub s - if t_result == "invalid" - then assertBool mempty (not ver) - else assertBool mempty ver - where - report = "test " <> show t_tcId + fmap (W.execute_group ty) wp_testGroups units :: TestTree units = testGroup "unit tests" [ diff --git a/test/Noble.hs b/test/Noble.hs @@ -0,0 +1,113 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ViewPatterns #-} + +module Noble ( + Ecdsa(..) + , execute_ecdsa + , execute_valid + + , parse_compact + , roll -- uh + ) where + +import Crypto.Curve.Secp256k1 +import Data.Aeson ((.:)) +import qualified Data.Aeson as A +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 (assertEqual, testCase) + +fi :: (Integral a, Num b) => a -> b +fi = fromIntegral +{-# INLINE fi #-} + +toBS :: T.Text -> BS.ByteString +toBS = B16.decodeLenient . TE.encodeUtf8 + +toSecKey :: T.Text -> Integer +toSecKey = roll . toBS + +data Ecdsa = Ecdsa { + ec_valid :: ![(Int, ValidTest)] + , ec_invalid :: !InvalidTest + } deriving Show + +instance A.FromJSON Ecdsa where + parseJSON = A.withObject "Ecdsa" $ \m -> Ecdsa + <$> fmap (zip [0..]) (m .: "valid") + <*> m .: "invalid" + +-- XX run noble's invalid suites +execute_ecdsa :: Ecdsa -> TestTree +execute_ecdsa Ecdsa {..} = testGroup "noble_ecdsa" [ + testGroup "valid" (fmap execute_valid ec_valid) + ] + +data ValidTest = ValidTest { + vt_d :: !Integer + , vt_m :: !BS.ByteString + , vt_signature :: !BS.ByteString + } deriving Show + +instance A.FromJSON ValidTest where + parseJSON = A.withObject "ValidTest" $ \m -> ValidTest + <$> fmap toSecKey (m .: "d") + <*> fmap toBS (m .: "m") + <*> fmap toBS (m .: "signature") + +-- 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 + +parse_compact :: BS.ByteString -> ECDSA +parse_compact bs = + let (roll -> r, roll -> s) = BS.splitAt 32 bs + in ECDSA r s + +execute_valid :: (Int, ValidTest) -> TestTree +execute_valid (label, ValidTest {..}) = + testCase ("noble-secp256k1, valid (" <> show label <> ")") $ do + let msg = vt_m + x = vt_d + pec = parse_compact vt_signature + sig = _sign_no_hash x msg + assertEqual mempty pec sig + +data InvalidTest = InvalidTest { + iv_sign :: ![InvalidSignTest] + , iv_verify :: ![InvalidVerifyTest] + } deriving Show + +instance A.FromJSON InvalidTest where + parseJSON = A.withObject "InvalidTest" $ \m -> InvalidTest + <$> m .: "sign" + <*> m .: "verify" + +data InvalidSignTest = InvalidSignTest { + ivs_d :: !Integer + , ivs_m :: !BS.ByteString + } deriving Show + +instance A.FromJSON InvalidSignTest where + parseJSON = A.withObject "InvalidSignTest" $ \m -> InvalidSignTest + <$> fmap toSecKey (m .: "d") + <*> fmap toBS (m .: "m") + +data InvalidVerifyTest = InvalidVerifyTest { + ivv_Q :: !T.Text -- XX check noble pubkey encoding + , ivv_m :: !BS.ByteString + , ivv_signature :: !BS.ByteString -- XX to sig (from compact?) + } deriving Show + +instance A.FromJSON InvalidVerifyTest where + parseJSON = A.withObject "InvalidVerifyTest" $ \m -> InvalidVerifyTest + <$> m .: "Q" + <*> fmap toBS (m .: "m") + <*> fmap toBS (m .: "signature") + diff --git a/test/Wycheproof.hs b/test/Wycheproof.hs @@ -1,5 +1,6 @@ {-# LANGUAGE BangPatterns #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ViewPatterns #-} module Wycheproof ( @@ -11,6 +12,9 @@ module Wycheproof ( , parse_der_sig , toProjective , toEcdsa + + , execute + , execute_group ) where import Crypto.Curve.Secp256k1 @@ -23,6 +27,8 @@ 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 @@ -33,6 +39,29 @@ 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 = mempty + 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 msg pub s + Unrestricted -> verify_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 @@ -128,14 +157,13 @@ instance A.FromJSON PublicKey where toEcdsa :: T.Text -> Either String ECDSA toEcdsa (B16.decodeLenient . TE.encodeUtf8 -> bs) = - -- AT.parseOnly parse_der_len 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 -- XX invalid sigs prevent 'fmap toEcdsa' + , t_sig :: !T.Text , t_result :: !T.Text } deriving Show