Skip to content

Commit

Permalink
Merge pull request #14 from jml/spake-interaction
Browse files Browse the repository at this point in the history
Spake interaction function
  • Loading branch information
jml authored Nov 28, 2017
2 parents 54f59d4 + 243228a commit 63ffe71
Show file tree
Hide file tree
Showing 11 changed files with 329 additions and 107 deletions.
9 changes: 9 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ jobs:
paths:
- /root/.stack
- .stack-work
- run:
name: apt-get update
command: apt-get update
- run:
name: Install pip
command: apt-get install -y python-pip
- run:
name: Install dependencies
command: pip install --user -r requirements.txt
- run:
name: Tests
command: stack test --skip-ghc-check --no-terminal
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
41 changes: 13 additions & 28 deletions cmd/interop-entrypoint/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,8 @@ import Crypto.Spake2
, SideID(..)
, makeSymmetricProtocol
, makeAsymmetricProtocol
, createSessionKey
, makePassword
, computeOutboundMessage
, generateKeyMaterial
, extractElement
, startSpake2
, elementToMessage
, formatError
, spake2Exchange
)
import Crypto.Spake2.Group (AbelianGroup, Group(..))
import Crypto.Spake2.Groups (Ed25519(..))
Expand Down Expand Up @@ -76,31 +70,22 @@ runInteropTest
-> 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
Expand All @@ -114,7 +99,7 @@ makeProtocolFromSide side =
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 ""
Expand Down
10 changes: 9 additions & 1 deletion package.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
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
Expand Down Expand Up @@ -45,8 +45,16 @@ tests:
main: Tasty.hs
source-dirs: tests
dependencies:
- aeson
- bytestring
- cryptonite
- memory
- process
- QuickCheck
- spake2
- tasty
- tasty-hspec

# Only used for testing.
data-files:
- tests/python/spake2_exchange.py
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# These are Python requirements for our interoperability tests.
spake2==0.7
attrs==17.3.0
10 changes: 9 additions & 1 deletion spake2.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,6 +21,9 @@ cabal-version: >= 1.10
extra-source-files:
CHANGELOG.md

data-files:
tests/python/spake2_exchange.py

source-repository head
type: git
location: https://github.com/jml/haskell-spake2
Expand Down Expand Up @@ -71,12 +74,17 @@ test-suite tasty
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
108 changes: 89 additions & 19 deletions src/Crypto/Spake2.hs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ module Crypto.Spake2
, Protocol
, makeAsymmetricProtocol
, makeSymmetricProtocol
, spake2Exchange
, startSpake2
, Math.computeOutboundMessage
, Math.generateKeyMaterial
Expand Down Expand Up @@ -153,7 +154,7 @@ elementToMessage protocol element = prefix <> encodeElement (group protocol) ele
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.
Expand All @@ -165,21 +166,25 @@ data MessageError
-- ^ 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.
--
-- Returns a 'MessageError' if we cannot decode the message,
-- 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
Expand Down Expand Up @@ -275,6 +280,59 @@ getParams Protocol{group, relation} =
, 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)
Expand All @@ -291,15 +349,22 @@ startSpake2 protocol password =
-- \[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
Expand All @@ -311,19 +376,24 @@ createSessionKey Protocol{group, hashAlgorithm, relation} x y k (Password passwo

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
Loading

0 comments on commit 63ffe71

Please sign in to comment.