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 1b90018aaa78c646c1d6f99c13a0d4506494e2e9
parent 3390898f75f02eaed18d5025e8cbd6eb9b5e12f9
Author: Jared Tobin <jared@jtobin.io>
Date:   Fri, 14 Mar 2025 13:33:09 +0400

test: ecdh test parsers, runners

Diffstat:
Mtest/Main.hs | 26++++++++++++++++++--------
Mtest/WycheproofEcdh.hs | 129+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
2 files changed, 128 insertions(+), 27 deletions(-)

diff --git a/test/Main.hs b/test/Main.hs @@ -14,6 +14,7 @@ import Test.Tasty.HUnit import qualified Data.Text.IO as TIO import qualified Noble as N import qualified Wycheproof as W +import qualified WycheproofEcdh as WE import qualified BIP340 fi :: (Integral a, Num b) => a -> b @@ -25,25 +26,29 @@ 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" + wp_ecdh <- TIO.readFile + "etc/ecdh_secp256k1_test.json" noble_ecdsa <- TIO.readFile "etc/noble_ecdsa.json" bip340 <- BS.readFile "etc/bip-0340-test-vectors.csv" let !tex = precompute - quar = do + pen = do wp0 <- A.decodeStrictText wp_ecdsa_sha256 :: Maybe W.Wycheproof wp1 <- A.decodeStrictText wp_ecdsa_sha256_bitcoin :: Maybe W.Wycheproof + wp2 <- A.decodeStrictText wp_ecdh :: Maybe WE.Wycheproof nob <- A.decodeStrictText noble_ecdsa :: Maybe N.Ecdsa bip <- case AT.parseOnly BIP340.cases bip340 of Left _ -> Nothing Right b -> pure b - pure (wp0, wp1, nob, bip) - case quar of + pure (wp0, wp1, wp2, nob, bip) + case pen of Nothing -> error "couldn't parse wycheproof vectors" - Just (w0, w1, no, ip) -> defaultMain $ testGroup "ppad-secp256k1" [ + Just (w0, w1, w2, no, ip) -> defaultMain $ testGroup "ppad-secp256k1" [ units - , wycheproof_ecdsa_verify_tests tex "(ecdsa, sha256)" Unrestricted w0 - , wycheproof_ecdsa_verify_tests tex "(ecdsa, sha256, low-s)" LowS w1 - , N.execute_ecdsa tex no - , testGroup "bip0340 vectors (schnorr)" (fmap (BIP340.execute tex) ip) + -- , wycheproof_ecdsa_verify_tests tex "(ecdsa, sha256)" Unrestricted w0 + -- , wycheproof_ecdsa_verify_tests tex "(ecdsa, sha256, low-s)" LowS w1 + , wycheproof_ecdh_tests "(ecdh)" w2 + -- , N.execute_ecdsa tex no + -- , testGroup "bip0340 vectors (schnorr)" (fmap (BIP340.execute tex) ip) ] wycheproof_ecdsa_verify_tests @@ -52,6 +57,11 @@ wycheproof_ecdsa_verify_tests tex msg ty W.Wycheproof {..} = testGroup ("wycheproof vectors " <> msg) $ fmap (W.execute_group tex ty) wp_testGroups +wycheproof_ecdh_tests :: String -> WE.Wycheproof -> TestTree +wycheproof_ecdh_tests msg WE.Wycheproof {..} = + testGroup ("wycheproof vectors " <> msg) $ + fmap (WE.execute_group) wp_testGroups + units :: TestTree units = testGroup "unit tests" [ parse_point_tests diff --git a/test/WycheproofEcdh.hs b/test/WycheproofEcdh.hs @@ -19,9 +19,109 @@ 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) +import qualified Test.Tasty.HUnit as H (assertBool, assertEqual, testCase) -execute_group = undefined +fi :: (Integral a, Num b) => a -> b +fi = fromIntegral +{-# INLINE fi #-} + +execute_group :: EcdhTestGroup -> TestTree +execute_group EcdhTestGroup {..} = + testGroup msg (fmap execute etg_tests) + where + msg = "wycheproof ecdh" + +execute :: EcdhTest -> TestTree +execute EcdhTest {..} = H.testCase report $ do + let pub = case der_to_pub t_public of + Left _ -> error "der_to_pub failed" + Right p -> p + sec = to_sec t_private + sar = x_coor t_shared + + Affine x_out _ = affine (mul_unsafe pub sec) -- faster + + H.assertEqual mempty sar x_out + where + report = "wycheproof ecdh " <> show t_tcId + +-- RFC 5280 ASN.1 +-- SubjectPublicKeyInfo ::= SEQUENCE { +-- algorithm AlgorithmIdentifier, +-- subjectPublicKey BIT STRING +-- } +-- AlgorithmIdentifier ::= SEQUENCE { +-- algorithm OBJECT IDENTIFIER, +-- parameters ANY DEFINED BY algorithm OPTIONAL +-- } +parse_der_pub :: AT.Parser Projective +parse_der_pub = do + _ <- AT.word8 0x30 -- SEQUENCE + len <- fmap fi AT.anyWord8 + _ <- parse_der_algo + parse_der_subjectpubkey + +parse_der_algo :: AT.Parser () +parse_der_algo = do + _ <- AT.word8 0x30 -- SEQUENCE + _ <- fmap fi AT.anyWord8 -- XX check + _ <- parse_der_ecpubkey + _ <- parse_der_secp256k1 + pure () + +-- RFC 5480 2.1.1 +-- id-ecPublicKey OBJECT IDENTIFIER ::= { +-- iso(1) member-body(2) us(840) ansi-X9-62(10045) keyType(2) 1 } +-- +-- DER encoded -> 06 07 2A 86 48 CE 3D 02 01 +parse_der_ecpubkey :: AT.Parser () +parse_der_ecpubkey = do + _ <- AT.word8 0x06 + _ <- AT.word8 0x07 + _ <- AT.word8 0x2a + _ <- AT.word8 0x86 + _ <- AT.word8 0x48 + _ <- AT.word8 0xce + _ <- AT.word8 0x3d + _ <- AT.word8 0x02 + _ <- AT.word8 0x01 + pure () + +-- SEC1-v2 A.2 +-- certicom-arc OBJECT IDENTIFIER ::= { +-- iso(1) identified-organization(3) certicom(132) +-- } +-- +-- ellipticCurve OBJECT IDENTIFIER ::= { certicom-arc curve(0) } +-- +-- secp256k1 OBJECT IDENTIFIER ::= { ellipticCurve 10 } +-- +-- (i.e., 1.3.132.0.10) +-- +-- DER encoded -> 06 05 2B 81 04 00 0A +parse_der_secp256k1 :: AT.Parser () +parse_der_secp256k1 = do + _ <- AT.word8 0x06 + _ <- AT.word8 0x05 + _ <- AT.word8 0x2b + _ <- AT.word8 0x81 + _ <- AT.word8 0x04 + _ <- AT.word8 0x00 + _ <- AT.word8 0x0a + pure () + +parse_der_subjectpubkey :: AT.Parser Projective +parse_der_subjectpubkey = do + _ <- AT.word8 0x03 -- BIT STRING + len <- fmap fi AT.anyWord8 + _ <- AT.word8 0x00 -- extra bits (always 0x00 for DER) + content <- AT.take (len - 1) -- len counts 'extra bits' field + etc <- AT.takeByteString + if BS.length content /= len - 1 || etc /= mempty + then fail "invalid content" + else case parse_point content of + Nothing -> fail "invalid content" + Just pt -> pure pt data Wycheproof = Wycheproof { wp_testGroups :: ![EcdhTestGroup] @@ -39,24 +139,16 @@ instance A.FromJSON EcdhTestGroup where parseJSON = A.withObject "EcdhTestGroup" $ \m -> EcdhTestGroup <$> m .: "tests" -data PublicKey = PublicKey { - pk_type :: !T.Text - , pk_curve :: !T.Text - , pk_keySize :: !Int - , pk_uncompressed :: !Projective - } deriving Show +der_to_pub :: T.Text -> Either String Projective +der_to_pub (B16.decodeLenient . TE.encodeUtf8 -> bs) = + AT.parseOnly parse_der_pub bs -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 +x_coor :: T.Text -> Integer +x_coor (B16.decodeLenient . TE.encodeUtf8 -> bs) = parse_int256 bs -instance A.FromJSON PublicKey where - parseJSON = A.withObject "PublicKey" $ \m -> PublicKey - <$> m .: "type" - <*> m .: "curve" - <*> m .: "keySize" - <*> fmap toProjective (m .: "uncompressed") +to_sec :: T.Text -> Integer +to_sec (B16.decodeLenient . TE.encodeUtf8 -> bs) = + parse_int256 (BS.drop 1 bs) -- drop leading zero byte data EcdhTest = EcdhTest { t_tcId :: !Int @@ -74,4 +166,3 @@ instance A.FromJSON EcdhTest where <*> m .: "shared" <*> m .: "result" -