WycheproofEcdh.hs (5426B)
1 {-# LANGUAGE BangPatterns #-} 2 {-# LANGUAGE OverloadedStrings #-} 3 {-# LANGUAGE RecordWildCards #-} 4 {-# LANGUAGE ViewPatterns #-} 5 6 module WycheproofEcdh ( 7 Wycheproof(..) 8 , execute_group 9 ) where 10 11 import Crypto.Curve.Secp256k1 12 import qualified Crypto.Hash.SHA256 as SHA256 13 import Data.Aeson ((.:)) 14 import qualified Data.Aeson as A 15 import qualified Data.Attoparsec.ByteString as AT 16 import Data.Bits ((.<<.), (.>>.), (.|.)) 17 import qualified Data.ByteString as BS 18 import qualified Data.ByteString.Base16 as B16 19 import qualified Data.Text as T 20 import qualified Data.Text.Encoding as TE 21 import Test.Tasty (TestTree, testGroup) 22 import qualified Test.Tasty.HUnit as H (assertBool, assertEqual, testCase) 23 24 fi :: (Integral a, Num b) => a -> b 25 fi = fromIntegral 26 {-# INLINE fi #-} 27 28 execute_group :: EcdhTestGroup -> TestTree 29 execute_group EcdhTestGroup {..} = 30 testGroup msg (fmap execute etg_tests) 31 where 32 msg = "wycheproof ecdh" 33 34 execute :: EcdhTest -> TestTree 35 execute EcdhTest {..} = H.testCase report $ do 36 case der_to_pub t_public of 37 Left _ -> 38 -- 'acceptable' in wycheproof-speak means that a public key 39 -- contains a parameter that, whilst invalid, doesn't actually 40 -- affect the ECDH computation. we work only with valid 41 -- secp256k1 points, so rule these out as invalid as well. 42 -- 43 H.assertBool "invalid" (t_result `elem` ["invalid", "acceptable"]) 44 Right pub -> do 45 let sec = parse_bigint t_private 46 sar = parse_bigint t_shared 47 h_sar = SHA256.hash (unroll32 sar) 48 out = ecdh pub sec 49 H.assertEqual mempty h_sar out 50 where 51 report = "wycheproof ecdh " <> show t_tcId 52 53 -- RFC 5280 ASN.1 54 -- SubjectPublicKeyInfo ::= SEQUENCE { 55 -- algorithm AlgorithmIdentifier, 56 -- subjectPublicKey BIT STRING 57 -- } 58 -- AlgorithmIdentifier ::= SEQUENCE { 59 -- algorithm OBJECT IDENTIFIER, 60 -- parameters ANY DEFINED BY algorithm OPTIONAL 61 -- } 62 parse_der_pub :: AT.Parser Projective 63 parse_der_pub = do 64 _ <- AT.word8 0x30 -- SEQUENCE 65 _ <- AT.anyWord8 66 _ <- parse_der_algo 67 parse_der_subjectpubkey 68 69 parse_der_algo :: AT.Parser () 70 parse_der_algo = do 71 _ <- AT.word8 0x30 -- SEQUENCE 72 _ <- AT.anyWord8 73 _ <- parse_der_ecpubkey 74 _ <- parse_der_secp256k1 75 pure () 76 77 -- RFC 5480 2.1.1 78 -- id-ecPublicKey OBJECT IDENTIFIER ::= { 79 -- iso(1) member-body(2) us(840) ansi-X9-62(10045) keyType(2) 1 } 80 -- 81 -- DER encoded -> 06 07 2A 86 48 CE 3D 02 01 82 parse_der_ecpubkey :: AT.Parser () 83 parse_der_ecpubkey = do 84 _ <- AT.word8 0x06 85 _ <- AT.word8 0x07 86 _ <- AT.word8 0x2a 87 _ <- AT.word8 0x86 88 _ <- AT.word8 0x48 89 _ <- AT.word8 0xce 90 _ <- AT.word8 0x3d 91 _ <- AT.word8 0x02 92 _ <- AT.word8 0x01 93 pure () 94 95 -- SEC1-v2 A.2 96 -- certicom-arc OBJECT IDENTIFIER ::= { 97 -- iso(1) identified-organization(3) certicom(132) 98 -- } 99 -- 100 -- ellipticCurve OBJECT IDENTIFIER ::= { certicom-arc curve(0) } 101 -- 102 -- secp256k1 OBJECT IDENTIFIER ::= { ellipticCurve 10 } 103 -- 104 -- (i.e., 1.3.132.0.10) 105 -- 106 -- DER encoded -> 06 05 2B 81 04 00 0A 107 parse_der_secp256k1 :: AT.Parser () 108 parse_der_secp256k1 = do 109 _ <- AT.word8 0x06 110 _ <- AT.word8 0x05 111 _ <- AT.word8 0x2b 112 _ <- AT.word8 0x81 113 _ <- AT.word8 0x04 114 _ <- AT.word8 0x00 115 _ <- AT.word8 0x0a 116 pure () 117 118 parse_der_subjectpubkey :: AT.Parser Projective 119 parse_der_subjectpubkey = do 120 _ <- AT.word8 0x03 -- BIT STRING 121 len <- fmap fi AT.anyWord8 122 _ <- AT.word8 0x00 -- extra bits (always 0x00 for DER) 123 content <- AT.take (len - 1) -- len counts 'extra bits' field 124 etc <- AT.takeByteString 125 if BS.length content /= len - 1 || etc /= mempty 126 then fail "invalid content" 127 else case parse_point content of 128 Nothing -> fail "invalid content" 129 Just pt -> pure pt 130 131 der_to_pub :: T.Text -> Either String Projective 132 der_to_pub (B16.decodeLenient . TE.encodeUtf8 -> bs) = 133 AT.parseOnly parse_der_pub bs 134 135 parse_bigint :: T.Text -> Integer 136 parse_bigint (B16.decodeLenient . TE.encodeUtf8 -> bs) = roll bs where 137 roll :: BS.ByteString -> Integer 138 roll = BS.foldl' alg 0 where 139 alg !a (fi -> !b) = (a .<<. 8) .|. b 140 141 -- big-endian bytestring encoding 142 unroll :: Integer -> BS.ByteString 143 unroll i = case i of 144 0 -> BS.singleton 0 145 _ -> BS.reverse $ BS.unfoldr step i 146 where 147 step 0 = Nothing 148 step m = Just (fi m, m .>>. 8) 149 150 -- big-endian bytestring encoding for 256-bit ints, left-padding with 151 -- zeros if necessary. the size of the integer is not checked. 152 unroll32 :: Integer -> BS.ByteString 153 unroll32 (unroll -> u) 154 | l < 32 = BS.replicate (32 - l) 0 <> u 155 | otherwise = u 156 where 157 l = BS.length u 158 159 data Wycheproof = Wycheproof { 160 wp_testGroups :: ![EcdhTestGroup] 161 } deriving Show 162 163 instance A.FromJSON Wycheproof where 164 parseJSON = A.withObject "Wycheproof" $ \m -> Wycheproof 165 <$> m .: "testGroups" 166 167 data EcdhTestGroup = EcdhTestGroup { 168 etg_tests :: ![EcdhTest] 169 } deriving Show 170 171 instance A.FromJSON EcdhTestGroup where 172 parseJSON = A.withObject "EcdhTestGroup" $ \m -> EcdhTestGroup 173 <$> m .: "tests" 174 175 data EcdhTest = EcdhTest { 176 t_tcId :: !Int 177 , t_public :: !T.Text 178 , t_private :: !T.Text 179 , t_shared :: !T.Text 180 , t_result :: !T.Text 181 } deriving Show 182 183 instance A.FromJSON EcdhTest where 184 parseJSON = A.withObject "EcdhTest" $ \m -> EcdhTest 185 <$> m .: "tcId" 186 <*> m .: "public" 187 <*> m .: "private" 188 <*> m .: "shared" 189 <*> m .: "result" 190