packages feed

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 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()