spake2 0.1.0 → 0.2.0
raw patch · 13 files changed
+321/−224 lines, 13 filesdep −base16-bytestring
Dependencies removed: base16-bytestring
Files
- CHANGELOG.md +9/−0
- cmd/interop-entrypoint/Main.hs +9/−9
- spake2.cabal +5/−3
- src/Crypto/Spake2.hs +13/−26
- src/Crypto/Spake2/Group.hs +103/−44
- src/Crypto/Spake2/Groups.hs +0/−3
- src/Crypto/Spake2/Groups/Ed25519.hs +23/−16
- src/Crypto/Spake2/Groups/IntegerAddition.hs +0/−64
- src/Crypto/Spake2/Groups/IntegerGroup.hs +17/−10
- src/Crypto/Spake2/Math.hs +6/−6
- src/Crypto/Spake2/Util.hs +0/−4
- tests/Groups.hs +55/−34
- tests/Spake2.hs +81/−5
+ CHANGELOG.md view
@@ -0,0 +1,9 @@+# Changelog++## 0.2.0 (2017-06-08)++* `Group` typeclass split into `Group` and `AbelianGroup` typeclasses++## 0.1.0 (2017-05-28)++Initial release
cmd/interop-entrypoint/Main.hs view
@@ -18,7 +18,7 @@ import Protolude hiding (group) import Crypto.Hash (SHA256(..))-import qualified Data.ByteString.Base16 as Base16+import Data.ByteArray.Encoding (convertFromBase, convertToBase, Base(Base16)) import Options.Applicative import System.IO (hFlush, hGetLine, hPutStrLn) @@ -38,7 +38,7 @@ , elementToMessage , formatError )-import Crypto.Spake2.Group (Group(..))+import Crypto.Spake2.Group (AbelianGroup, Group(..)) import Crypto.Spake2.Groups (Ed25519(..)) @@ -69,7 +69,7 @@ runInteropTest- :: (HasCallStack, Group group)+ :: (HasCallStack, AbelianGroup group) => Protocol group SHA256 -> Password -> Handle@@ -80,9 +80,9 @@ let outElement = computeOutboundMessage spake2 output (elementToMessage protocol outElement) line <- hGetLine inH- let inMsg = parseHex (toS line)+ let inMsg = parseHex (toS line :: ByteString) case inMsg of- Left err -> abort err+ 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@@ -94,13 +94,13 @@ where output message = do- hPutStrLn outH (toS (Base16.encode message))+ hPutStrLn outH (toS (convertToBase Base16 message :: ByteString)) hFlush outH parseHex line =- case Base16.decode line of- (bytes, "") -> Right bytes- _ -> Left ("Could not decode line: " <> show line)+ case convertFromBase Base16 line of+ Left err -> Left ("Could not decode line (reason: " <> err <> "): " <> show line)+ Right bytes -> Right bytes makeProtocolFromSide :: Side -> Protocol Ed25519 SHA256
spake2.cabal view
@@ -3,7 +3,7 @@ -- see: https://github.com/sol/hpack name: spake2-version: 0.1.0+version: 0.2.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@@ -18,6 +18,9 @@ build-type: Simple cabal-version: >= 1.10 +extra-source-files:+ CHANGELOG.md+ source-repository head type: git location: https://github.com/jml/haskell-spake2@@ -38,7 +41,6 @@ Crypto.Spake2.Group Crypto.Spake2.Groups Crypto.Spake2.Groups.Ed25519- Crypto.Spake2.Groups.IntegerAddition Crypto.Spake2.Groups.IntegerGroup Crypto.Spake2.Math Crypto.Spake2.Util@@ -53,8 +55,8 @@ build-depends: base >= 4.9 && < 5 , protolude- , base16-bytestring , cryptonite+ , memory , optparse-applicative , spake2 default-language: Haskell2010
src/Crypto/Spake2.hs view
@@ -84,8 +84,7 @@ -} module Crypto.Spake2- ( something- , Password+ ( Password , makePassword -- * The SPAKE2 protocol , Protocol@@ -108,20 +107,15 @@ import Crypto.Error (CryptoError, CryptoFailable(..)) import Crypto.Hash (HashAlgorithm, hashWith) import Crypto.Random.Types (MonadRandom(..))-import Data.ByteArray (ByteArrayAccess, ByteArray)+import Data.ByteArray (ByteArrayAccess) import qualified Data.ByteArray as ByteArray import qualified Data.ByteString as ByteString -import Crypto.Spake2.Group (Group(..), decodeScalar, scalarSizeBytes)+import Crypto.Spake2.Group (AbelianGroup(..), Group(..), decodeScalar, scalarSizeBytes) import qualified Crypto.Spake2.Math as Math import Crypto.Spake2.Util (expandData) --- | Do-nothing function so that we have something to import in our tests.--- TODO: Actually test something genuine and then remove this.-something :: a -> a-something x = x- -- | Shared secret password used to negotiate the connection. -- -- Constructor deliberately not exported,@@ -138,17 +132,12 @@ newtype SideID = SideID { unSideID :: ByteString } deriving (Eq, Ord, Show) -- | Convert a user-supplied password into a scalar on a group.-passwordToScalar :: Group group => group -> Password -> Scalar group+passwordToScalar :: AbelianGroup group => group -> Password -> Scalar group passwordToScalar group password =- let oversized = expandPassword password (scalarSizeBytes group + 16) :: ByteString- in decodeScalar group oversized---- | Expand a password using HKDF so that it has a certain number of bytes.------ TODO: jml cannot remember why you might want to call this.-expandPassword :: ByteArray output => Password -> Int -> output-expandPassword (Password bytes) numBytes = expandData info bytes numBytes+ decodeScalar group oversized where+ oversized = expandPassword password (scalarSizeBytes group + 16) :: ByteString+ expandPassword (Password bytes) = expandData info bytes -- This needs to be exactly "SPAKE2 pw" -- See <https://github.com/bitwiseshiftleft/sjcl/pull/273/#issuecomment-185251593> info = "SPAKE2 pw"@@ -215,16 +204,14 @@ -- | Relation between two sides in SPAKE2. -- Can be either symmetric (both sides are the same), or asymmetric.------ XXX: Maybe too generic? Could reasonably replace 'a' with 'Side group'.-data Relation a+data Relation group = Asymmetric- { sideA :: a -- ^ Side A. Both sides need to agree who side A is.- , sideB :: a -- ^ Side B. Both sides need to agree who side B is.+ { sideA :: Side group -- ^ Side A. Both sides need to agree who side A is.+ , sideB :: Side group -- ^ Side B. Both sides need to agree who side B is. , us :: WhichSide -- ^ Which side we are } | Symmetric- { bothSides :: a -- ^ Description used by both sides.+ { bothSides :: Side group -- ^ Description used by both sides. } theirPrefix :: Relation a -> Word8@@ -245,7 +232,7 @@ = Protocol { group :: group -- ^ The group to use for encryption , hashAlgorithm :: hashAlgorithm -- ^ Hash algorithm used for generating the session key- , relation :: Relation (Side group) -- ^ How the two sides relate to each other+ , relation :: Relation group -- ^ How the two sides relate to each other } -- | Construct an asymmetric SPAKE2 protocol.@@ -290,7 +277,7 @@ -- | Commence a SPAKE2 exchange. startSpake2- :: (MonadRandom randomly, Group group)+ :: (MonadRandom randomly, AbelianGroup group) => Protocol group hashAlgorithm -> Password -> randomly (Math.Spake2Exchange group)
src/Crypto/Spake2/Group.hs view
@@ -1,10 +1,11 @@ {-# LANGUAGE TypeFamilies #-} {-| Module: Crypto.Spake2.Group-Description: Interface for mathematical groups+Description: Interfaces for mathematical groups -} module Crypto.Spake2.Group- ( Group(..)+ ( AbelianGroup(..)+ , Group(..) , decodeScalar , elementSizeBytes , scalarSizeBytes@@ -19,24 +20,12 @@ import Crypto.Spake2.Util (bytesToNumber) + -- | A mathematical group intended to be used with SPAKE2.------ Notes:------ * This is a much richer interface than one would expect from a group purely derived from abstract algebra--- * jml thinks this is relevant to all Diffie-Hellman cryptography,--- but too ignorant to say for sure--- * Is this group automatically abelian? cyclic?--- Must it have these properties? class Group group where -- | An element of the group. type Element group :: * - -- | A scalar for this group.- -- Mathematically equivalent to an integer,- -- but possibly stored differently for computational reasons.- type Scalar group :: *- -- | Group addition. -- -- prop> \x y z -> elementAdd group (elementAdd group x y) z == elementAdd group x (elementAdd group y z)@@ -62,10 +51,86 @@ -- prop> \x -> (elementAdd group groupIdentity x) == x groupIdentity :: group -> Element group + -- | Encode an element of the group into bytes.+ --+ -- Note [Byte encoding in Group]+ --+ -- prop> \x -> decodeElement group (encodeElement group x) == CryptoPassed x+ encodeElement :: ByteArray bytes => group -> Element group -> bytes++ -- | Decode an element into the group from some bytes.+ --+ -- Note [Byte encoding in Group]+ decodeElement :: ByteArray bytes => group -> bytes -> CryptoFailable (Element group)++ -- | Size of elements, in bits+ elementSizeBits :: group -> Int++ -- | Deterministically create an arbitrary element from a seed bytestring.+ --+ -- __XXX__: jml would much rather this take a scalar, an element, or even an integer, rather than bytes+ -- because bytes mean that the group instances have to know about hash algorithms and HKDF.+ -- If the IntegerGroup class in SPAKE2 also oversized its input,+ -- then it and the ed25519 implementation would have identical decoding.+ arbitraryElement :: ByteArrayAccess bytes => group -> bytes -> Element group+++-- | A group where 'elementAdd' is commutative.+--+-- That is, where+--+-- prop> \x y -> elementAdd group x y == elementAdd group y x+--+-- This property leads to a natural \(\mathbb{Z}\)-module,+-- where scalar multiplication is defined as repeatedly calling `elementAdd`.+--+-- === Definitions+--+-- Warning: this gets algebraic.+--+-- A /module/ is a ring \(R\) together with an abelian group \((G, +)\),+-- and a new operator \(\cdot\) (i.e. scalar multiplication)+-- such that:+--+-- 1. \(r \cdot (x + y) = r \cdot x + r \cdot y\)+-- 2. \((r + s) \cdot x = r \cdot x + s \cdot x\)+-- 3. \((rs) \cdot x = r \cdot (s \cdot x)\)+-- 4. \(1_R \cdot x = x\)+--+-- for all \(x, y\) in \(G\), and \(r, s\) in \(R\),+-- where \(1_R\) is the identity of the ring.+--+-- A /ring/ \(R, +, \cdot\) a set \(R\) with two operators such that:+--+-- 1. \(R\) is an abelian group under \(+\)+-- 2. \(R\) is a monoid under \(\cdot\)+-- 3. \(cdot\) is _distributive_ with respect to \(+\). That is,+-- 1. \(a \cdot (b + c) = (a \cdot b) + (a \cdot c) (left distributivity)+-- 2. \((b + c) \cdot a) = (b \cdot a) + (c \cdot a) (right distributivity)+--+-- Note we have to define left & right distributivity,+-- because \(\cdot\) might not be commutative.+--+-- A /monoid/ is a group without the notion of inverse. See Haskell's 'Monoid' typeclass.+--+-- A \(\mathbb{Z}\)-module is a module where the ring \(R\)+-- is the integers with normal addition and multiplication.+class Group group => AbelianGroup group where+ -- | A scalar for this group.+ -- Mathematically equivalent to an integer,+ -- but possibly stored differently for computational reasons.+ type Scalar group :: *+ -- | Multiply an element of the group with respect to a scalar. -- -- This is equivalent to adding the element to itself N times, where N is a scalar.+ -- The default implementation does exactly that. scalarMultiply :: group -> Scalar group -> Element group -> Element group+ scalarMultiply group scalar element =+ scalarMultiply' (scalarToInteger group scalar) element+ where+ scalarMultiply' 0 _ = groupIdentity group+ scalarMultiply' n x = elementAdd group x (scalarMultiply' (n - 1) x) -- | Get the scalar that corresponds to an integer. --@@ -81,39 +146,16 @@ -- prop> \x -> integerToScalar group (scalarToInteger group x) == x scalarToInteger :: group -> Scalar group -> Integer - -- | Encode an element of the group into bytes.- --- -- Note [Byte encoding in Group]- --- -- prop> \x -> decodeElement group (encodeElement group x) == CryptoPassed x- encodeElement :: ByteArray bytes => group -> Element group -> bytes-- -- | Decode an element into the group from some bytes.- --- -- Note [Byte encoding in Group]- decodeElement :: ByteArray bytes => group -> bytes -> CryptoFailable (Element group)+ -- | Size of scalars, in bits+ scalarSizeBits :: group -> Int -- | Encode a scalar into bytes. -- | Generate a new random element of the group, with corresponding scalar. generateElement :: MonadRandom randomly => group -> randomly (KeyPair group) - -- | Size of elements, in bits- elementSizeBits :: group -> Int - -- | Size of scalars, in bits- scalarSizeBits :: group -> Int-- -- | Deterministically create an arbitrary element from a seed bytestring.- --- -- __XXX__: jml would much rather this take a scalar, an element, or even an integer, rather than bytes- -- because bytes mean that the group instances have to know about hash algorithms and HKDF.- -- If the IntegerGroup class in SPAKE2 also oversized its input,- -- then it and the ed25519 implementation would have identical decoding.- arbitraryElement :: ByteArrayAccess bytes => group -> bytes -> Element group-- -- | Map some arbitrary bytes into a scalar in a group.-decodeScalar :: (ByteArrayAccess bytes, Group group) => group -> bytes -> Scalar group+decodeScalar :: (ByteArrayAccess bytes, AbelianGroup group) => group -> bytes -> Scalar group decodeScalar group bytes = integerToScalar group (bytesToNumber bytes) -- | Size of elements in a group, in bits.@@ -121,7 +163,7 @@ elementSizeBytes group = (elementSizeBits group + 7) `div` 8 -- | Size of scalars in a group, in bytes.-scalarSizeBytes :: Group group => group -> Int+scalarSizeBytes :: AbelianGroup group => group -> Int scalarSizeBytes group = (scalarSizeBits group + 7) `div` 8 -- | A group key pair composed of the private part (a scalar)@@ -133,6 +175,19 @@ } {-+Note [Algebra]+~~~~~~~~~~~~~~++* Perhaps we should call 'AbelianGroup' 'ZModule' or similar?+* A "proper" implementation would no doubt have a Ring typeclass+ and then a new Module typeclass that somehow composed a Ring and an AbelianGroup.+ This seems unnecessary for our implementation needs,+ and is perhaps best left to those who know something about designing algebraic libraries.+* Cyclic groups are necessarily abelian.++-}++{- Note [Byte encoding in Group] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -142,15 +197,19 @@ * cryptonite does it with 'EllipticCurve' * warner does it with spake2.groups+ * you just need to send different stuff over the wire for elliptic curve groups+ than integer modulo groups Reasons against: * mathematical structure of groups has no connection to serialization * might want multiple encodings for same mathematical group+ (this seems unlikely) -Including for now on the assumption that I'm ignorant.+We're keeping encode/decode in for now.+Later, we might want to split it out into a different typeclass,+perhaps one that inherits from the base 'Group' class. -TODO: Revisit decision to put byte encoding in Group after we've done a couple of implementations -} {-
src/Crypto/Spake2/Groups.hs view
@@ -9,10 +9,7 @@ , IntegerGroup.IntegerGroup(..) , IntegerGroup.makeIntegerGroup , IntegerGroup.i1024- -- * For testing only- , IntegerAddition.IntegerAddition(..) ) where import qualified Crypto.Spake2.Groups.Ed25519 as Ed25519-import qualified Crypto.Spake2.Groups.IntegerAddition as IntegerAddition import qualified Crypto.Spake2.Groups.IntegerGroup as IntegerGroup
src/Crypto/Spake2/Groups/Ed25519.hs view
@@ -28,7 +28,7 @@ import qualified Data.ByteArray as ByteArray import qualified Data.List as List -import Crypto.Spake2.Group (Group(..), KeyPair(..), scalarSizeBytes)+import Crypto.Spake2.Group (AbelianGroup(..), Group(..), KeyPair(..), scalarSizeBytes) import Crypto.Spake2.Util (bytesToNumber, expandArbitraryElementSeed) {-@@ -62,30 +62,18 @@ data Ed25519 = Ed25519 deriving (Eq, Show) instance Group Ed25519 where- type Scalar Ed25519 = Integer type Element Ed25519 = ExtendedPoint 'Member elementAdd _ x y = addExtendedPoints x y elementNegate group = scalarMultiply group (l - 1) groupIdentity _ = assertInGroup extendedZero- scalarMultiply _ n x = safeScalarMultiply n x - integerToScalar _ x = x- scalarToInteger _ x = x- encodeElement _ x = encodeAffinePoint (extendedToAffine' x) decodeElement _ bytes = toCryptoFailable $ do- affine <- decodeAffinePoint bytes- let extended = affineToExtended affine+ extended <- affineToExtended <$> decodeAffinePoint bytes ensureInGroup extended - generateElement group = do- scalar <- generateMax l- let element = scalarMultiply group scalar generator- pure (KeyPair element scalar)- elementSizeBits _ = 255- scalarSizeBits _ = 255 arbitraryElement group bytes = let seed = expandArbitraryElementSeed bytes (scalarSizeBytes group + 16) :: ByteString@@ -93,6 +81,22 @@ in List.head [ element | Right element <- map makeGroupMember [y..] ] +instance AbelianGroup Ed25519 where+ type Scalar Ed25519 = Integer++ scalarMultiply _ n x = safeScalarMultiply n x++ integerToScalar _ x = x+ scalarToInteger _ x = x++ scalarSizeBits _ = 255++ generateElement group = do+ scalar <- generateMax l+ let element = scalarMultiply group scalar generator+ pure (KeyPair element scalar)++ -- | Errors that can occur within the group. data Error = NotOnCurve Integer Integer@@ -302,7 +306,6 @@ -- | Attempt to create a member of Ed25519 from an affine @y@ coordinate. makeGroupMember :: Integer -> Either Error (Element Ed25519) makeGroupMember y = do- -- XXX: Similar to decodeElement. Can we share code? point <- affineToExtended <$> makeAffinePoint (xRecover y) y let point8 = safeScalarMultiply 8 point if isExtendedZero point8@@ -337,7 +340,11 @@ which would crash the program if incorrect. For programming convenience, I just skip these values. -jml doesn't know what is meant by the 'order' of a point.+The 'order' of a point \(x\) is the number \(n\) such that:+'scalarMultiply group (integerToScalar group n) x == groupIdentity group'++Note this is different from the order of a /group/,+which for finite groups is the number of elements in the group. -}
− src/Crypto/Spake2/Groups/IntegerAddition.hs
@@ -1,64 +0,0 @@-{-# OPTIONS_HADDOCK hide #-}-{-# LANGUAGE TypeFamilies #-}-{-|-Module: Crypto.Spake2.Groups-Description: Additive group of integers modulo \(n\)--Do __NOT__ use this for anything cryptographic.--}-module Crypto.Spake2.Groups.IntegerAddition- ( IntegerAddition(..)- ) where--import Protolude hiding (group, length)--import Crypto.Error (CryptoFailable(..))-import Crypto.Number.Basic (numBits)-import Crypto.Number.ModArithmetic (expSafe)-import Crypto.Random.Types (MonadRandom(..))--import Crypto.Spake2.Group- ( Group(..)- , KeyPair(..)- , decodeScalar- , elementSizeBytes- , scalarSizeBytes- )-import Crypto.Spake2.Util- ( expandArbitraryElementSeed- , bytesToNumber- , unsafeNumberToBytes- )---- | Simple integer addition group.------ Do __NOT__ use this for anything cryptographic.-newtype IntegerAddition = IntegerAddition { modulus :: Integer } deriving (Eq, Ord, Show)--instance Group IntegerAddition where- type Element IntegerAddition = Integer- type Scalar IntegerAddition = Integer-- elementAdd group x y = (x + y) `mod` modulus group- elementNegate group x = negate x `mod` modulus group- elementSubtract group x y = (x - y) `mod` modulus group- groupIdentity _ = 0- scalarMultiply group n x = (n * x) `mod` modulus group- integerToScalar _ x = x- scalarToInteger _ x = x- encodeElement group x = unsafeNumberToBytes (elementSizeBytes group) (x `mod` modulus group)- decodeElement _ bytes = CryptoPassed (bytesToNumber bytes)- generateElement group = do- scalarBytes <- getRandomBytes (scalarSizeBytes group)- let scalar = decodeScalar group (scalarBytes :: ByteString)- let element = scalarMultiply group scalar (groupIdentity group)- pure (KeyPair element scalar)- scalarSizeBits group = numBits (modulus group) -- XXX: Incorrect value. Not sure what it should be.- elementSizeBits group = numBits (modulus group) -- XXX: should be size of subgroup- arbitraryElement group seed =- let processedSeed = expandArbitraryElementSeed seed (elementSizeBytes group) :: ByteString- r = (modulus group - 1) `div` modulus group -- XXX: should be size of subgroup- h = bytesToNumber processedSeed `mod` modulus group- in expSafe h r (modulus group)--
src/Crypto/Spake2/Groups/IntegerGroup.hs view
@@ -19,7 +19,8 @@ import Crypto.Number.ModArithmetic (expSafe) import Crypto.Spake2.Group- ( Group(..)+ ( AbelianGroup(..)+ , Group(..) , KeyPair(..) , elementSizeBytes )@@ -52,15 +53,11 @@ instance Group IntegerGroup where type Element IntegerGroup = Integer- type Scalar IntegerGroup = Integer elementAdd group x y = (x * y) `mod` order group -- At a guess, negation is scalar multiplication where the scalar is -1 elementNegate group x = expSafe x (subgroupOrder group - 1) (order group) groupIdentity _ = 1- scalarMultiply group n x = expSafe x (n `mod` subgroupOrder group) (order group)- integerToScalar group x = x `mod` subgroupOrder group -- XXX: Should we instead fail?- scalarToInteger _ n = n encodeElement group = unsafeNumberToBytes (elementSizeBytes group) decodeElement group bytes = case bytesToNumber bytes of@@ -68,11 +65,6 @@ | x <= 0 || x >= order group -> CryptoFailed CryptoError_PointSizeInvalid | expSafe x (subgroupOrder group) (order group) /= groupIdentity group -> CryptoFailed CryptoError_PointCoordinatesInvalid | otherwise -> CryptoPassed x- generateElement group = do- scalar <- generateMax (subgroupOrder group)- let element = scalarMultiply group scalar (generator group)- pure (KeyPair element scalar)- scalarSizeBits group = numBits (subgroupOrder group) elementSizeBits group = numBits (order group) arbitraryElement group seed = let processedSeed = expandArbitraryElementSeed seed (elementSizeBytes group) :: ByteString@@ -81,6 +73,21 @@ r = (p - 1) `div` q h = bytesToNumber processedSeed `mod` p in expSafe h r p+++instance AbelianGroup IntegerGroup where+ type Scalar IntegerGroup = Integer++ scalarMultiply group n x = expSafe x (n `mod` subgroupOrder group) (order group)+ integerToScalar group x = x `mod` subgroupOrder group+ scalarToInteger _ n = n++ generateElement group = do+ scalar <- generateMax (subgroupOrder group)+ let element = scalarMultiply group scalar (generator group)+ pure (KeyPair element scalar)+ scalarSizeBits group = numBits (subgroupOrder group)+ -- | 1024 bit integer group. --
src/Crypto/Spake2/Math.hs view
@@ -91,8 +91,8 @@ -} module Crypto.Spake2.Math- ( Spake2(..) -- XXX: Not sure want to export innards but it disables "unused" warning- , Params(..) -- XXX: ditto+ ( Spake2(..)+ , Params(..) , startSpake2 , Spake2Exchange , computeOutboundMessage@@ -103,7 +103,7 @@ import Crypto.Random.Types (MonadRandom(..)) -import Crypto.Spake2.Group (Group(..), KeyPair(..))+import Crypto.Spake2.Group (AbelianGroup(..), Group(..), KeyPair(..)) -- | The parameters of the SPAKE2 protocol. The other side needs to be using -- the same values, but with swapped values for 'ourBlind' and 'theirBlind'.@@ -135,11 +135,11 @@ -- | Initiate the SPAKE2 exchange. Generates a secret (@xy@) that will be held -- by this side, and transmitted to the other side in "blinded" form.-startSpake2 :: (Group group, MonadRandom randomly) => Spake2 group -> randomly (Spake2Exchange group)+startSpake2 :: (AbelianGroup group, MonadRandom randomly) => Spake2 group -> randomly (Spake2Exchange group) startSpake2 spake2' = Started spake2' <$> generateElement (group . params $ spake2') -- | Determine the element (either \(X^{\star}\) or \(Y^{\star}\)) to send to the other side.-computeOutboundMessage :: Group group => Spake2Exchange group -> Element group+computeOutboundMessage :: AbelianGroup group => Spake2Exchange group -> Element group computeOutboundMessage Started{spake2 = Spake2{params = Params{group, ourBlind}, password}, xy} = elementAdd group (keyPairPublic xy) (scalarMultiply group password ourBlind) @@ -161,7 +161,7 @@ -- * \(K\) is the result of this function -- * \(pw\) is the password (this is what makes it SPAKE2, not SPAKE1) generateKeyMaterial- :: Group group+ :: AbelianGroup group => Spake2Exchange group -- ^ An initiated SPAKE2 exchange -> Element group -- ^ The outbound message from the other side (i.e. inbound to us) -> Element group -- ^ The final piece of key material to generate the session key.
src/Crypto/Spake2/Util.hs view
@@ -17,10 +17,6 @@ import qualified Crypto.KDF.HKDF as HKDF import Data.ByteArray (ByteArray, ByteArrayAccess(..)) --- TODO: memory package (a dependency of cryptonite) has--- Data.ByteArray.Encoding, which does base16 encoding. Perhaps we should use--- that rather than third-party base16-bytestring library.- -- | Take an arbitrary sequence of bytes and expand it to be the given number -- of bytes. Do this by extracting a pseudo-random key and expanding it using -- HKDF.
tests/Groups.hs view
@@ -5,15 +5,13 @@ import Protolude hiding (group) import Crypto.Error (CryptoFailable(..))-import GHC.Base (String) import Test.QuickCheck (Gen, (===), arbitrary, forAll, property) import Test.Tasty (TestTree) import Test.Tasty.Hspec (Spec, testSpec, describe, it, shouldBe) -import Crypto.Spake2.Group (Group(..))+import Crypto.Spake2.Group (AbelianGroup(..), Group(..)) import Crypto.Spake2.Groups- ( IntegerAddition(..)- , IntegerGroup(..)+ ( IntegerGroup(..) , Ed25519(..) , i1024) import qualified Crypto.Spake2.Groups.Ed25519 as Ed25519@@ -21,31 +19,29 @@ tests :: IO TestTree tests = testSpec "Groups" $ do- groupProperties "integer addition modulo 7" (IntegerAddition 7) 1 (makeScalar 7)- groupProperties "integer group" i1024 (IntegerGroup.generator i1024) (makeScalar (subgroupOrder i1024))- groupProperties "Ed25519" Ed25519 Ed25519.generator (makeScalar Ed25519.l)---makeScalar :: Integer -> Gen Integer-makeScalar k = do- i <- arbitrary- pure $ i `mod` k+ describe "integer group" $+ allGroupProperties i1024 (makeScalar (subgroupOrder i1024)) (IntegerGroup.generator i1024)+ describe "Ed25519" $+ allGroupProperties Ed25519 (makeScalar Ed25519.l) Ed25519.generator -makeElement :: Group group => group -> Gen (Scalar group) -> Element group -> Gen (Element group)-makeElement group scalars base = do- scalar <- scalars- pure (scalarMultiply group scalar base)+allGroupProperties+ :: (Show (Scalar group), Show (Element group), Eq (Scalar group), Eq (Element group), AbelianGroup group)+ => group+ -> Gen (Scalar group)+ -> Element group+ -> Spec+allGroupProperties group scalars base = do+ describe "is a group" $ groupProperties group (makeElement group scalars base)+ describe "is an abelian group" $ abelianGroupProperties group scalars base groupProperties- :: (Group group, Eq (Element group), Eq (Scalar group), Show (Element group), Show (Scalar group))- => String- -> group- -> Element group- -> Gen (Scalar group)+ :: (Group group, Eq (Element group), Show (Element group))+ => group+ -> Gen (Element group) -> Spec-groupProperties name group base scalars = describe name $ do+groupProperties group elements = do it "addition is associative" $ property $- forAll triples $ \(x, y, z) -> elementAdd group (elementAdd group x y) z === elementAdd group x (elementAdd group y z)+ forAll (triples elements) $ \(x, y, z) -> elementAdd group (elementAdd group x y) z === elementAdd group x (elementAdd group y z) it "addition with inverse yields identity" $ property $ forAll elements $ \x -> elementAdd group x (elementNegate group x) === groupIdentity group@@ -57,7 +53,7 @@ elementNegate group (groupIdentity group) `shouldBe` groupIdentity group it "subtraction is negated addition" $ property $- forAll pairs $ \(x, y) -> elementSubtract group x y === elementAdd group x (elementNegate group y)+ forAll (pairs elements) $ \(x, y) -> elementSubtract group x y === elementAdd group x (elementNegate group y) it "right-hand addition with identity yields original" $ property $ forAll elements $ \x -> elementAdd group x (groupIdentity group) === x@@ -69,6 +65,17 @@ forAll elements $ \x -> let bytes = encodeElement group x :: ByteString in decodeElement group bytes == CryptoPassed x ++abelianGroupProperties+ :: (AbelianGroup group, Eq (Element group), Eq (Scalar group), Show (Element group), Show (Scalar group))+ => group+ -> Gen (Scalar group)+ -> Element group+ -> Spec+abelianGroupProperties group scalars base = do+ it "addition is commutative" $ property $+ forAll (pairs elements) $ \(x, y) -> elementAdd group x y === elementAdd group y x+ it "scalar to integer roundtrips" $ property $ forAll scalars $ \n -> integerToScalar group (scalarToInteger group n) === n @@ -90,13 +97,27 @@ where elements = makeElement group scalars base - pairs = do- x <- elements- y <- elements- pure (x, y)+-- | Generate pairs of a thing.+pairs :: Gen a -> Gen (a, a)+pairs gen = do+ x <- gen+ y <- gen+ pure (x, y) - triples = do- x <- elements- y <- elements- z <- elements- pure (x, y, z)+-- | Generate triples of a thing.+triples :: Gen a -> Gen (a, a, a)+triples gen = do+ x <- gen+ y <- gen+ z <- gen+ pure (x, y, z)++makeScalar :: Integer -> Gen Integer+makeScalar k = do+ i <- arbitrary+ pure $ i `mod` k++makeElement :: AbelianGroup group => group -> Gen (Scalar group) -> Element group -> Gen (Element group)+makeElement group scalars base = do+ scalar <- scalars+ pure (scalarMultiply group scalar base)
tests/Spake2.hs view
@@ -1,13 +1,89 @@ module Spake2 (tests) where -import Protolude+import Protolude hiding (group) import Test.Tasty (TestTree)-import Test.Tasty.Hspec (testSpec, describe, it, shouldBe)+import Test.Tasty.Hspec (testSpec, describe, it, shouldBe, shouldNotBe) +import Crypto.Hash (SHA256(..)) import qualified Crypto.Spake2 as Spake2+import qualified Crypto.Spake2.Group as Group+import Crypto.Spake2.Groups (Ed25519(..)) tests :: IO TestTree tests = testSpec "Spake2" $ do- describe "something" $- it "should do things" $- Spake2.something (2 :: Int) `shouldBe` 2+ 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+ 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+ 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+ 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+ sessionKey1 `shouldNotBe` sessionKey2