spake2 0.3.0 → 0.4.0
raw patch · 8 files changed
+308/−106 lines, 8 filesdep +aesondep +process
Dependencies added: aeson, process
Files
- CHANGELOG.md +8/−0
- cmd/interop-entrypoint/Main.hs +13/−28
- spake2.cabal +9/−1
- src/Crypto/Spake2.hs +89/−19
- tests/Integration.hs +76/−0
- tests/Spake2.hs +33/−58
- tests/Tasty.hs +2/−0
- tests/python/spake2_exchange.py +78/−0
CHANGELOG.md view
@@ -1,5 +1,13 @@ # Changelog +## 0.4.0 (2017-11-22)++* Change `createSessionKey` inputs to be `inbound`, `outbound` rather than+ `side A`, `side B`. If you were passing as `side A`, `side B` before, it+ should continue to work, unless you were deliberately triggering an error+ condition.+* Add `spake2Exchange`, for much more convenient exchanges.+ ## 0.3.0 (2017-11-11) * Depend on protolude 0.2 minimum
cmd/interop-entrypoint/Main.hs view
@@ -29,14 +29,8 @@ , SideID(..) , makeSymmetricProtocol , makeAsymmetricProtocol- , createSessionKey , makePassword- , computeOutboundMessage- , generateKeyMaterial- , extractElement- , startSpake2- , elementToMessage- , formatError+ , spake2Exchange ) import Crypto.Spake2.Group (AbelianGroup, Group(..)) import Crypto.Spake2.Groups (Ed25519(..))@@ -76,31 +70,22 @@ -> Handle -> IO () runInteropTest protocol password inH outH = do- spake2 <- startSpake2 protocol password- let outElement = computeOutboundMessage spake2- output (elementToMessage protocol outElement)- line <- hGetLine inH- let inMsg = parseHex (toS line :: ByteString)- case inMsg of- Left err -> abort (toS err)- Right inMsgBytes ->- case extractElement protocol inMsgBytes of- Left err -> abort $ "Could not handle incoming message (line = " <> show line <> ", msgBytes = " <> show inMsgBytes <> "): " <> formatError err- Right inElement -> do- -- TODO: This is wrong, because it doesn't handle A/B properly.- let key = generateKeyMaterial spake2 inElement- let sessionKey = createSessionKey protocol inElement outElement key password- output sessionKey-+ sessionKey' <- spake2Exchange protocol password output input+ case sessionKey' of+ Left err -> abort $ show err+ Right sessionKey -> output sessionKey where+ output :: ByteString -> IO () output message = do hPutStrLn outH (convertToBase Base16 message :: ByteString) hFlush outH - parseHex line =- case convertFromBase Base16 line of- Left err -> Left ("Could not decode line (reason: " <> err <> "): " <> show line)- Right bytes -> Right bytes+ input :: IO (Either Text ByteString)+ input = do+ line <- hGetLine inH+ case convertFromBase Base16 (toS line :: ByteString) of+ Left err -> pure . Left . toS $ "Could not decode line (reason: " <> err <> "): " <> show line+ Right bytes -> pure (Right bytes) makeProtocolFromSide :: Side -> Protocol Ed25519 SHA256@@ -114,7 +99,7 @@ group = Ed25519 m = arbitraryElement group ("M" :: ByteString) n = arbitraryElement group ("N" :: ByteString)- s = arbitraryElement group ("S" :: ByteString)+ s = arbitraryElement group ("symmetric" :: ByteString) idA = SideID "" idB = SideID "" idSymmetric = SideID ""
spake2.cabal view
@@ -3,7 +3,7 @@ -- see: https://github.com/sol/hpack name: spake2-version: 0.3.0+version: 0.4.0 synopsis: Implementation of the SPAKE2 Password-Authenticated Key Exchange algorithm description: This library implements the SPAKE2 password-authenticated key exchange ("PAKE") algorithm. This allows two parties, who share a weak password, to@@ -21,6 +21,9 @@ extra-source-files: CHANGELOG.md +data-files:+ tests/python/spake2_exchange.py+ source-repository head type: git location: https://github.com/jml/haskell-spake2@@ -71,12 +74,17 @@ build-depends: base >= 4.9 && < 5 , protolude >= 0.2+ , aeson+ , bytestring , cryptonite+ , memory+ , process , QuickCheck , spake2 , tasty , tasty-hspec other-modules: Groups+ Integration Spake2 default-language: Haskell2010
src/Crypto/Spake2.hs view
@@ -90,6 +90,7 @@ , Protocol , makeAsymmetricProtocol , makeSymmetricProtocol+ , spake2Exchange , startSpake2 , Math.computeOutboundMessage , Math.generateKeyMaterial@@ -153,7 +154,7 @@ Asymmetric{us=SideB} -> "B" -- | An error that occurs when interpreting messages from the other side of the exchange.-data MessageError+data MessageError e = EmptyMessage -- ^ We received an empty bytestring. | UnexpectedPrefix Word8 Word8 -- ^ The bytestring had an unexpected prefix.@@ -165,13 +166,17 @@ -- ^ Message could not be decoded to an element of the group. -- This can indicate either an error in serialization logic, -- or in mathematics.+ | UnknownError e+ -- ^ An error arising from the "receive" action in 'spake2Exchange'.+ -- Since 0.4.0 deriving (Eq, Show) -- | Turn a 'MessageError' into human-readable text.-formatError :: MessageError -> Text+formatError :: Show e => MessageError e -> Text formatError EmptyMessage = "Other side sent us an empty message" formatError (UnexpectedPrefix got expected) = "Other side claims to be " <> show (chr (fromIntegral got)) <> ", expected " <> show (chr (fromIntegral expected)) formatError (BadCrypto err message) = "Could not decode message (" <> show message <> ") to element: " <> show err+formatError (UnknownError err) = "Error receiving message from other side: " <> show err -- | Extract an element on the group from an incoming message. --@@ -179,7 +184,7 @@ -- or the other side does not appear to be the expected other side. -- -- TODO: Need to protect against reflection attack at some point.-extractElement :: Group group => Protocol group hashAlgorithm -> ByteString -> Either MessageError (Element group)+extractElement :: Group group => Protocol group hashAlgorithm -> ByteString -> Either (MessageError error) (Element group) extractElement protocol message = case ByteString.uncons message of Nothing -> throwError EmptyMessage@@ -275,6 +280,59 @@ , Math.theirBlind = blind theirs } +-- | Perform an entire SPAKE2 exchange.+--+-- Given a SPAKE2 protocol that has all of the parameters for this exchange,+-- generate a one-off message from this side and receive a one off message+-- from the other.+--+-- Once we are done, return a key shared between both sides for a single+-- session.+--+-- Note: as per the SPAKE2 definition, the session key is not guaranteed+-- to actually /work/. If the other side has failed to authenticate, you will+-- still get a session key. Therefore, you must exchange some other message+-- that has been encrypted using this key in order to confirm that the session+-- key is indeed shared.+--+-- Note: the "send" and "receive" actions are performed 'concurrently'. If you+-- have ordering requirements, consider using a 'TVar' or 'MVar' to coordinate,+-- or implementing your own equivalent of 'spake2Exchange'.+--+-- If the message received from the other side cannot be parsed, return a+-- 'MessageError'.+--+-- Since 0.4.0.+spake2Exchange+ :: (AbelianGroup group, HashAlgorithm hashAlgorithm)+ => Protocol group hashAlgorithm+ -- ^ A 'Protocol' with all the parameters for the exchange. These parameters+ -- must be shared by both sides. Construct with 'makeAsymmetricProtocol' or+ -- 'makeSymmetricProtocol'.+ -> Password+ -- ^ The password shared between both sides. Construct with 'makePassword'.+ -> (ByteString -> IO ())+ -- ^ An action to send a message. The 'ByteString' parameter is this side's+ -- SPAKE2 element, encoded using the group encoding, prefixed according to+ -- the parameters in the 'Protocol'.+ -> IO (Either error ByteString)+ -- ^ An action to receive a message. The 'ByteString' generated ought to be+ -- the protocol-prefixed, group-encoded version of the other side's SPAKE2+ -- element.+ -> IO (Either (MessageError error) ByteString)+ -- ^ Either the shared session key or an error indicating we couldn't parse+ -- the other side's message.+spake2Exchange protocol password send receive = do+ exchange <- startSpake2 protocol password+ let outboundElement = Math.computeOutboundMessage exchange+ let outboundMessage = elementToMessage protocol outboundElement+ (_, inboundMessage) <- concurrently (send outboundMessage) receive+ pure $ do+ inboundMessage' <- first UnknownError inboundMessage+ inboundElement <- extractElement protocol inboundMessage'+ let keyMaterial = Math.generateKeyMaterial exchange inboundElement+ pure (createSessionKey protocol inboundElement outboundElement keyMaterial password)+ -- | Commence a SPAKE2 exchange. startSpake2 :: (MonadRandom randomly, AbelianGroup group)@@ -291,15 +349,22 @@ -- \[SK \leftarrow H(A, B, X^{\star}, Y^{\star}, K, pw)\] -- -- Including \(pw\) in the session key is what makes this SPAKE2, not SPAKE1.+--+-- __Note__: In spake2 0.3 and earlier, The \(X^{\star}\) and \(Y^{\star}\)+-- were expected to be from side A and side B respectively. Since spake2 0.4,+-- they are the outbound and inbound elements respectively. This fixes an+-- interoperability concern with the Python library, and reduces the burden on+-- the caller. Apologies for the possibly breaking change to any users of+-- older versions of spake2. createSessionKey :: (Group group, HashAlgorithm hashAlgorithm) => Protocol group hashAlgorithm -- ^ The protocol used for this exchange- -> Element group -- ^ The message from side A, \(X^{\star}\), or either side if symmetric- -> Element group -- ^ The message from side B, \(Y^{\star}\), or either side if symmetric+ -> Element group -- ^ The outbound message, generated by this, \(X^{\star}\), or either side if symmetric+ -> Element group -- ^ The inbound message, generated by the other side, \(Y^{\star}\), or either side if symmetric -> Element group -- ^ The calculated key material, \(K\) -> Password -- ^ The shared secret password -> ByteString -- ^ A session key to use for further communication-createSessionKey Protocol{group, hashAlgorithm, relation} x y k (Password password) =+createSessionKey Protocol{group, hashAlgorithm, relation} outbound inbound k (Password password) = hashDigest transcript where@@ -311,19 +376,24 @@ transcript = case relation of- Asymmetric{sideA, sideB} -> mconcat [ hashDigest password- , hashDigest (unSideID (sideID sideA))- , hashDigest (unSideID (sideID sideB))- , encodeElement group x- , encodeElement group y- , encodeElement group k- ]- Symmetric{bothSides} -> mconcat [ hashDigest password- , hashDigest (unSideID (sideID bothSides))- , symmetricElements- , encodeElement group k- ]+ Asymmetric{sideA, sideB, us} ->+ let (x, y) = case us of+ SideA -> (inbound, outbound)+ SideB -> (outbound, inbound)+ in mconcat [ hashDigest password+ , hashDigest (unSideID (sideID sideA))+ , hashDigest (unSideID (sideID sideB))+ , encodeElement group x+ , encodeElement group y+ , encodeElement group k+ ]+ Symmetric{bothSides} ->+ mconcat [ hashDigest password+ , hashDigest (unSideID (sideID bothSides))+ , symmetricElements+ , encodeElement group k+ ] symmetricElements =- let [ firstMessage, secondMessage ] = sort [ encodeElement group x, encodeElement group y ]+ let [ firstMessage, secondMessage ] = sort [ encodeElement group inbound, encodeElement group outbound ] in firstMessage <> secondMessage
+ tests/Integration.hs view
@@ -0,0 +1,76 @@+module Integration (tests) where++import Protolude hiding (stdin, stdout)++import Crypto.Hash (SHA256(..))+import Data.ByteArray.Encoding (convertFromBase, convertToBase, Base(Base16))+import qualified Data.ByteString as ByteString+import qualified Data.ByteString.Char8 as Char8+import qualified System.IO as IO+import qualified System.Process as Process+import Test.Tasty (TestTree)+import Test.Tasty.Hspec (testSpec, describe, it, shouldBe)++import qualified Crypto.Spake2 as Spake2+import Crypto.Spake2.Group (Group(arbitraryElement))+import Crypto.Spake2.Groups (Ed25519(..))++import qualified Paths_spake2++tests :: IO TestTree+tests = testSpec "Integration" $+ describe "python-spake2" $ do+ it "Generates the same SPAKE2 session key (symmetric)" $ do+ let sideID = "treebeard"+ let password = "mellon"+ let protocol = Spake2.makeSymmetricProtocol SHA256 Ed25519 blindS (Spake2.SideID sideID)+ exchangeWithPython protocol password+ [ "--side=S"+ , "--side-id=" <> toS sideID+ ]++ it "Generates the same SPAKE2 session key (asymmetric, we are side B)" $ do+ let ourSideID = "alliance"+ let theirSideID = "horde"+ let password = "mellon"+ let protocol = Spake2.makeAsymmetricProtocol SHA256 Ed25519 blindA blindB (Spake2.SideID theirSideID) (Spake2.SideID ourSideID) Spake2.SideB+ exchangeWithPython protocol password+ [ "--side=A"+ , "--side-id=" <> toS theirSideID+ , "--other-side-id=" <> toS ourSideID+ ]++ it "Generates the same SPAKE2 session key (asymmetric, we are side A)" $ do+ let ourSideID = "alliance"+ let theirSideID = "horde"+ let password = "mellon"+ let protocol = Spake2.makeAsymmetricProtocol SHA256 Ed25519 blindA blindB (Spake2.SideID ourSideID) (Spake2.SideID theirSideID) Spake2.SideA+ exchangeWithPython protocol password+ [ "--side=B"+ , "--side-id=" <> toS theirSideID+ , "--other-side-id=" <> toS ourSideID+ ]++ where+ send h x = Char8.hPutStrLn h (convertToBase Base16 x)+ receive h = convertFromBase Base16 <$> ByteString.hGetLine h+ blindA = arbitraryElement Ed25519 ("M" :: ByteString)+ blindB = arbitraryElement Ed25519 ("N" :: ByteString)+ blindS = arbitraryElement Ed25519 ("symmetric" :: ByteString)++ exchangeWithPython protocol password args = do+ scriptExe <- Paths_spake2.getDataFileName "tests/python/spake2_exchange.py"+ let testScript = (Process.proc "python" (scriptExe:("--code=" <> toS password):args))+ { Process.std_in = Process.CreatePipe+ , Process.std_out = Process.CreatePipe+ , Process.std_err = Process.Inherit -- So we get stack traces printed during test runs.+ }+ Process.withCreateProcess testScript $+ \(Just stdin) (Just stdout) _stderr ph -> do+ -- The inter-process protocol is line-based.+ IO.hSetBuffering stdin IO.LineBuffering+ IO.hSetBuffering stdout IO.LineBuffering+ IO.hSetBuffering stderr IO.LineBuffering+ (do Right sessionKey <- Spake2.spake2Exchange protocol (Spake2.makePassword password) (send stdin) (receive stdout)+ theirSpakeKey <- ByteString.hGetLine stdout+ theirSpakeKey `shouldBe` convertToBase Base16 sessionKey) `finally` Process.waitForProcess ph
tests/Spake2.hs view
@@ -14,76 +14,51 @@ describe "Asymmetric protocol" $ do it "Produces matching session keys when passwords match" $ do let password = Spake2.makePassword "abc"- let hashAlg = SHA256- let group = Ed25519- let m = Group.arbitraryElement group ("M" :: ByteString)- let n = Group.arbitraryElement group ("N" :: ByteString)- let idA = Spake2.SideID ""- let idB = Spake2.SideID ""- let protocolA = Spake2.makeAsymmetricProtocol hashAlg group m n idA idB Spake2.SideA- let protocolB = Spake2.makeAsymmetricProtocol hashAlg group m n idA idB Spake2.SideB- sideA <- Spake2.startSpake2 protocolA password- sideB <- Spake2.startSpake2 protocolB password- let aOut = Spake2.computeOutboundMessage sideA- let bOut = Spake2.computeOutboundMessage sideB- let aKey = Spake2.generateKeyMaterial sideA bOut- let bKey = Spake2.generateKeyMaterial sideB aOut- let aSessionKey = Spake2.createSessionKey protocolA aOut bOut aKey password- let bSessionKey = Spake2.createSessionKey protocolA aOut bOut bKey password+ let idA = Spake2.SideID "side-a"+ let idB = Spake2.SideID "side-b"+ let protocolA = defaultAsymmetricProtocol idA idB Spake2.SideA+ let protocolB = defaultAsymmetricProtocol idA idB Spake2.SideB+ (Right aSessionKey, Right bSessionKey) <- (protocolA, password) `versus` (protocolB, password) aSessionKey `shouldBe` bSessionKey+ it "Produces differing session keys when passwords do not match" $ do let password1 = Spake2.makePassword "abc" let password2 = Spake2.makePassword "cba"- let hashAlg = SHA256- let group = Ed25519- let m = Group.arbitraryElement group ("M" :: ByteString)- let n = Group.arbitraryElement group ("N" :: ByteString) let idA = Spake2.SideID "" let idB = Spake2.SideID ""- let protocolA = Spake2.makeAsymmetricProtocol hashAlg group m n idA idB Spake2.SideA- let protocolB = Spake2.makeAsymmetricProtocol hashAlg group m n idA idB Spake2.SideB- sideA <- Spake2.startSpake2 protocolA password1- sideB <- Spake2.startSpake2 protocolB password2- let aOut = Spake2.computeOutboundMessage sideA- let bOut = Spake2.computeOutboundMessage sideB- let aKey = Spake2.generateKeyMaterial sideA bOut- let bKey = Spake2.generateKeyMaterial sideB aOut- let aSessionKey = Spake2.createSessionKey protocolA aOut bOut aKey password1- let bSessionKey = Spake2.createSessionKey protocolA aOut bOut bKey password2+ let protocolA = defaultAsymmetricProtocol idA idB Spake2.SideA+ let protocolB = defaultAsymmetricProtocol idA idB Spake2.SideB+ (Right aSessionKey, Right bSessionKey) <- (protocolA, password1) `versus` (protocolB, password2) aSessionKey `shouldNotBe` bSessionKey+ describe "Symmetric protocol" $ do it "Produces matching session keys when passwords match" $ do let password = Spake2.makePassword "abc"- let hashAlg = SHA256- let group = Ed25519- let s = Group.arbitraryElement group ("M" :: ByteString)- let idSymmetric = Spake2.SideID ""- let protocol1 = Spake2.makeSymmetricProtocol hashAlg group s idSymmetric- let protocol2 = Spake2.makeSymmetricProtocol hashAlg group s idSymmetric- side1 <- Spake2.startSpake2 protocol1 password- side2 <- Spake2.startSpake2 protocol2 password- let out1 = Spake2.computeOutboundMessage side1- let out2 = Spake2.computeOutboundMessage side2- let key1 = Spake2.generateKeyMaterial side1 out2- let key2 = Spake2.generateKeyMaterial side2 out1- let sessionKey1 = Spake2.createSessionKey protocol1 out1 out2 key1 password- let sessionKey2 = Spake2.createSessionKey protocol2 out1 out2 key2 password+ let protocol = defaultSymmetricProtocol (Spake2.SideID "")+ (Right sessionKey1, Right sessionKey2) <- (protocol, password) `versus` (protocol, password) sessionKey1 `shouldBe` sessionKey2+ it "Produces differing session keys when passwords do not match" $ do let password1 = Spake2.makePassword "abc" let password2 = Spake2.makePassword "cba"- let hashAlg = SHA256- let group = Ed25519- let s = Group.arbitraryElement group ("M" :: ByteString)- let idSymmetric = Spake2.SideID ""- let protocol1 = Spake2.makeSymmetricProtocol hashAlg group s idSymmetric- let protocol2 = Spake2.makeSymmetricProtocol hashAlg group s idSymmetric- side1 <- Spake2.startSpake2 protocol1 password1- side2 <- Spake2.startSpake2 protocol2 password2- let out1 = Spake2.computeOutboundMessage side1- let out2 = Spake2.computeOutboundMessage side2- let key1 = Spake2.generateKeyMaterial side1 out2- let key2 = Spake2.generateKeyMaterial side2 out1- let sessionKey1 = Spake2.createSessionKey protocol1 out1 out2 key1 password1- let sessionKey2 = Spake2.createSessionKey protocol2 out1 out2 key2 password2+ let protocol = defaultSymmetricProtocol (Spake2.SideID "")+ (Right sessionKey1, Right sessionKey2) <- (protocol, password1) `versus` (protocol, password2) sessionKey1 `shouldNotBe` sessionKey2++ where+ defaultAsymmetricProtocol = Spake2.makeAsymmetricProtocol SHA256 group m n+ m = Group.arbitraryElement group ("M" :: ByteString)+ n = Group.arbitraryElement group ("N" :: ByteString)++ defaultSymmetricProtocol = Spake2.makeSymmetricProtocol SHA256 group s+ s = Group.arbitraryElement group ("symmetric" :: ByteString)++ group = Ed25519++ -- | Run protocol A with password A against protocol B with password B.+ versus (protocolA, passwordA) (protocolB, passwordB) = do+ aOutVar <- newEmptyMVar+ bOutVar <- newEmptyMVar+ concurrently+ (Spake2.spake2Exchange protocolA passwordA (putMVar aOutVar) (Right <$> readMVar bOutVar))+ (Spake2.spake2Exchange protocolB passwordB (putMVar bOutVar) (Right <$> readMVar aOutVar))
tests/Tasty.hs view
@@ -8,6 +8,7 @@ import qualified Spake2 import qualified Groups+import qualified Integration main :: IO () main = sequence tests >>= defaultMain . testGroup "Spake2"@@ -15,4 +16,5 @@ tests = [ Spake2.tests , Groups.tests+ , Integration.tests ]
+ tests/python/spake2_exchange.py view
@@ -0,0 +1,78 @@+#!/usr/bin/python+"""Exchange SPAKE2 keys and print out the session key.++Assumes symmetric exchange and uses the default SPAKE2 parameters.+"""+import argparse+from binascii import hexlify, unhexlify+import attr+import sys++from spake2 import SPAKE2_A, SPAKE2_B, SPAKE2_Symmetric+++def main():+ parser = argparse.ArgumentParser(prog='version_exchange')+ parser.add_argument(+ '--code', dest='code', type=unicode,+ help='Password to use to connect to other side')+ parser.add_argument(+ '--side-id', dest='side_id', type=unicode,+ help='Identifier for this side of the exchange')+ parser.add_argument(+ '--side', dest='side', choices=['A', 'B', 'S'],+ help=('Which side this represents. '+ 'Decides whether we use symmetric or asymmetric variant.'))+ parser.add_argument(+ '--other-side-id', dest='other_side_id', type=unicode,+ help=('Identifier for other side of the exchange. '+ 'Only necessary for asymmetric variants.'))+ params = parser.parse_args(sys.argv[1:])+ transport = Transport(input_stream=sys.stdin, output_stream=sys.stdout)+ protocol = get_protocol(+ params.code, params.side, params.side_id, params.other_side_id)+ run_exchange(transport, protocol)+++def get_protocol(code, side, side_id, other_side_id):+ code = code.encode('utf8')+ side_id = side_id.encode('utf8')+ if side == 'S':+ return SPAKE2_Symmetric(code, idSymmetric=side_id)+ other_side_id = other_side_id.encode('utf8')+ if side == 'A':+ return SPAKE2_A(code, idA=side_id, idB=other_side_id)+ elif side == 'B':+ return SPAKE2_B(code, idA=other_side_id, idB=side_id)+ else:+ raise AssertionError('Invalid side: %r' % (side,))+++def run_exchange(transport, protocol):+ # Send the SPAKE2 message+ outbound = protocol.start()+ transport.send_line(hexlify(outbound))++ # Receive SPAKE2 message+ pake_msg = transport.receive_line()+ inbound = unhexlify(pake_msg)+ spake_key = protocol.finish(inbound)+ transport.send_line(hexlify(spake_key))+++@attr.s+class Transport(object):+ input_stream = attr.ib()+ output_stream = attr.ib()++ def send_line(self, line):+ self.output_stream.write(line.rstrip().encode('utf8'))+ self.output_stream.write('\n')+ self.output_stream.flush()++ def receive_line(self):+ return self.input_stream.readline().strip().decode('utf8')+++if __name__ == '__main__':+ main()