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:
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"
-