Wycheproof.hs (5420B)
1 {-# LANGUAGE BangPatterns #-} 2 {-# LANGUAGE OverloadedStrings #-} 3 {-# LANGUAGE RecordWildCards #-} 4 {-# LANGUAGE ViewPatterns #-} 5 6 module Wycheproof ( 7 Wycheproof(..) 8 , execute_group 9 ) where 10 11 import Crypto.Curve.Secp256k1 12 import Data.Aeson ((.:)) 13 import qualified Data.Aeson as A 14 import qualified Data.Attoparsec.ByteString as AT 15 import qualified Data.Bits as B 16 import qualified Data.ByteString as BS 17 import qualified Data.ByteString.Base16 as B16 18 import qualified Data.Text as T 19 import qualified Data.Text.Encoding as TE 20 import qualified GHC.Num.Integer as I 21 import Test.Tasty (TestTree, testGroup) 22 import Test.Tasty.HUnit (assertBool, testCase) 23 24 fi :: (Integral a, Num b) => a -> b 25 fi = fromIntegral 26 {-# INLINE fi #-} 27 28 -- big-endian bytestring decoding 29 roll :: BS.ByteString -> Integer 30 roll = BS.foldl' unstep 0 where 31 unstep a (fi -> b) = (a `I.integerShiftL` 8) `I.integerOr` b 32 33 execute_group :: Context -> SigType -> EcdsaTestGroup -> TestTree 34 execute_group tex ty EcdsaTestGroup {..} = 35 testGroup msg (fmap (execute tex ty pk_uncompressed) etg_tests) 36 where 37 msg = "wycheproof (" <> T.unpack etg_type <> ", " <> T.unpack etg_sha <> ")" 38 PublicKey {..} = etg_publicKey 39 40 execute :: Context -> SigType -> Projective -> EcdsaVerifyTest -> TestTree 41 execute tex ty pub EcdsaVerifyTest {..} = testCase report $ do 42 let msg = B16.decodeLenient (TE.encodeUtf8 t_msg) 43 sig = toEcdsa t_sig 44 case sig of 45 Left _ -> assertBool mempty (t_result == "invalid") 46 Right s -> do 47 let ver = case ty of 48 LowS -> verify_ecdsa msg pub s && verify_ecdsa' tex msg pub s 49 Unrestricted -> verify_ecdsa_unrestricted msg pub s 50 && verify_ecdsa_unrestricted' tex msg pub s 51 if t_result == "invalid" 52 then assertBool mempty (not ver) 53 else assertBool mempty ver 54 where 55 report = "wycheproof (" <> show ty <> ") " <> show t_tcId 56 57 parse_der_sig :: AT.Parser ECDSA 58 parse_der_sig = do 59 _ <- AT.word8 0x30 60 len <- fmap fi AT.anyWord8 61 content <- AT.take len 62 etc <- AT.takeByteString 63 if BS.length content /= len || etc /= mempty 64 then fail "invalid content" 65 else case AT.parseOnly (meat len) content of 66 Left _ -> fail "invalid content" 67 Right v -> pure v 68 where 69 meat len = do 70 (lr, bs_r) <- parseAsnInt 71 (ls, bs_s) <- parseAsnInt 72 let r = fi (roll bs_r) 73 s = fi (roll bs_s) 74 checks = lr + ls == len 75 rest <- AT.takeByteString 76 if rest == mempty && checks 77 then pure (ECDSA r s) 78 else fail "input remaining or length mismatch" 79 80 parseAsnInt :: AT.Parser (Int, BS.ByteString) 81 parseAsnInt = do 82 _ <- AT.word8 0x02 83 len <- fmap fi AT.anyWord8 84 content <- AT.take len 85 if BS.length content /= len 86 then fail "invalid length" 87 else if len == 1 88 then pure (len + 2, content) -- + tag byt + len byt 89 else case BS.uncons content of 90 Nothing -> fail "invalid content" 91 Just (h0, t0) 92 | B.testBit h0 7 -> fail "negative value" 93 | otherwise -> case BS.uncons t0 of 94 Nothing -> fail "invalid content" 95 Just (h1, _) 96 | h0 == 0x00 && not (B.testBit h1 7) -> fail "invalid padding" 97 | otherwise -> case BS.unsnoc content of 98 Nothing -> fail "invalid content" 99 Just (_, tn) 100 | tn == 0x00 -> fail "invalid padding" 101 | otherwise -> pure (len + 2, content) 102 103 data Wycheproof = Wycheproof { 104 wp_algorithm :: !T.Text 105 , wp_generatorVersion :: !T.Text 106 , wp_numberOfTests :: !Int 107 , wp_testGroups :: ![EcdsaTestGroup] 108 } deriving Show 109 110 instance A.FromJSON Wycheproof where 111 parseJSON = A.withObject "Wycheproof" $ \m -> Wycheproof 112 <$> m .: "algorithm" 113 <*> m .: "generatorVersion" 114 <*> m .: "numberOfTests" 115 <*> m .: "testGroups" 116 117 data EcdsaTestGroup = EcdsaTestGroup { 118 etg_type :: !T.Text 119 , etg_publicKey :: !PublicKey 120 , etg_sha :: !T.Text 121 , etg_tests :: ![EcdsaVerifyTest] 122 } deriving Show 123 124 instance A.FromJSON EcdsaTestGroup where 125 parseJSON = A.withObject "EcdsaTestGroup" $ \m -> EcdsaTestGroup 126 <$> m .: "type" 127 <*> m .: "publicKey" 128 <*> m .: "sha" 129 <*> m .: "tests" 130 131 data PublicKey = PublicKey { 132 pk_type :: !T.Text 133 , pk_curve :: !T.Text 134 , pk_keySize :: !Int 135 , pk_uncompressed :: !Projective 136 } deriving Show 137 138 toProjective :: T.Text -> Projective 139 toProjective (B16.decodeLenient . TE.encodeUtf8 -> bs) = case parse_point bs of 140 Nothing -> error "wycheproof: couldn't parse pubkey" 141 Just p -> p 142 143 instance A.FromJSON PublicKey where 144 parseJSON = A.withObject "PublicKey" $ \m -> PublicKey 145 <$> m .: "type" 146 <*> m .: "curve" 147 <*> m .: "keySize" 148 <*> fmap toProjective (m .: "uncompressed") 149 150 toEcdsa :: T.Text -> Either String ECDSA 151 toEcdsa (B16.decodeLenient . TE.encodeUtf8 -> bs) = 152 AT.parseOnly parse_der_sig bs 153 154 data EcdsaVerifyTest = EcdsaVerifyTest { 155 t_tcId :: !Int 156 , t_comment :: !T.Text 157 , t_msg :: !T.Text 158 , t_sig :: !T.Text 159 , t_result :: !T.Text 160 } deriving Show 161 162 instance A.FromJSON EcdsaVerifyTest where 163 parseJSON = A.withObject "EcdsaVerifyTest" $ \m -> EcdsaVerifyTest 164 <$> m .: "tcId" 165 <*> m .: "comment" 166 <*> m .: "msg" 167 <*> m .: "sig" 168 <*> m .: "result" 169