mirror of
https://github.com/logos-storage/transport-over-mix.git
synced 2026-05-19 19:39:35 +00:00
372 lines
12 KiB
Haskell
372 lines
12 KiB
Haskell
|
|
|
||
|
|
-- | The Sphinx packet format header.
|
||
|
|
--
|
||
|
|
-- See:
|
||
|
|
--
|
||
|
|
-- * George Danezis, Ian Goldberg: "Sphinx: A Compact and Provably Secure Mix Format"
|
||
|
|
--
|
||
|
|
-- <https://cypherpunks.ca/~iang/pubs/Sphinx_Oakland09.pdf>
|
||
|
|
--
|
||
|
|
|
||
|
|
{-# OPTIONS_GHC -Wno-x-partial #-}
|
||
|
|
{-# LANGUAGE DeriveGeneric, TypeApplications #-}
|
||
|
|
module Sphinx.Header where
|
||
|
|
|
||
|
|
--------------------------------------------------------------------------------
|
||
|
|
|
||
|
|
import Data.Bits
|
||
|
|
import Data.Word
|
||
|
|
import Data.Semigroup
|
||
|
|
import Data.Monoid
|
||
|
|
|
||
|
|
import Control.Monad
|
||
|
|
import System.Random
|
||
|
|
|
||
|
|
import qualified Data.ByteString as B
|
||
|
|
import qualified Data.ByteString.Lazy as L
|
||
|
|
import GHC.Generics
|
||
|
|
import Data.Binary
|
||
|
|
|
||
|
|
import Crypto.X25519.DH
|
||
|
|
import Crypto.X25519.Elliptic
|
||
|
|
import Crypto.X25519.ScalarField ( Fq , toFq )
|
||
|
|
|
||
|
|
import Crypto.Symmetric
|
||
|
|
import Crypto.Types
|
||
|
|
|
||
|
|
import Octet
|
||
|
|
|
||
|
|
--------------------------------------------------------------------------------
|
||
|
|
-- * global constants
|
||
|
|
|
||
|
|
-- | Maximum number of hops
|
||
|
|
maxNumberOfHops :: Int
|
||
|
|
maxNumberOfHops = 5
|
||
|
|
|
||
|
|
-- | Targeted security in bits = size of symmetric keys = size of MACs = half the size of private keys
|
||
|
|
lambda :: Int
|
||
|
|
lambda = 128
|
||
|
|
|
||
|
|
lambdaBytes :: Int
|
||
|
|
lambdaBytes = div lambda 8
|
||
|
|
|
||
|
|
--------------------------------------------------------------------------------
|
||
|
|
-- * crypto primitives
|
||
|
|
|
||
|
|
mixKDF :: Domain -> [Word8] -> Word128
|
||
|
|
mixKDF = kdf128 KDF_SHA256
|
||
|
|
|
||
|
|
mixMAC :: Key -> [Word8] -> MAC
|
||
|
|
mixMAC = hmac (HMAC128 SHA256)
|
||
|
|
|
||
|
|
mixPRG :: Key -> IV -> [Word8]
|
||
|
|
mixPRG = streamCipherPRGBytes AES128_CTR
|
||
|
|
|
||
|
|
mixRouteEnc :: Key -> IV -> [Word8] -> [Word8]
|
||
|
|
mixRouteEnc key iv input = zipWith xor input (mixPRG key iv)
|
||
|
|
|
||
|
|
mixRouteDec :: Key -> IV -> [Word8] -> [Word8]
|
||
|
|
mixRouteDec = mixRouteEnc
|
||
|
|
|
||
|
|
--------------------------------------------------------------------------------
|
||
|
|
-- * types
|
||
|
|
|
||
|
|
-- | Size of something measured in bytes
|
||
|
|
type SizeInBytes = Int
|
||
|
|
|
||
|
|
-- | The actual number of hops. Can be less or equal to @maxNumberOfHops@.
|
||
|
|
type NHops = Int
|
||
|
|
|
||
|
|
-- | A destination address is usually outside the mix network. We represent it by
|
||
|
|
-- a string (padded to a fixed length)
|
||
|
|
type DestinationAddr = String
|
||
|
|
|
||
|
|
-- | A mix node address should be represented as a fixed length bytestring
|
||
|
|
newtype MixAddr
|
||
|
|
= MkMixAddr [Word8]
|
||
|
|
deriving (Eq,Show)
|
||
|
|
|
||
|
|
-- | A (random) message identifier, to identify replies.
|
||
|
|
type MessageId = Word128
|
||
|
|
|
||
|
|
-- | A mix node (extenal view) has a public key and an address
|
||
|
|
data MixNodeExt = MkMixNodeExt
|
||
|
|
{ nodePubKey :: PubKey
|
||
|
|
, nodeAddress :: MixAddr
|
||
|
|
}
|
||
|
|
deriving (Eq,Show)
|
||
|
|
|
||
|
|
-- | Internally, a mix node also has a private key (required for processing)
|
||
|
|
data MixNodeInt = MkMixNodeInt
|
||
|
|
{ nodePrivKey :: SecretKey
|
||
|
|
, nodeExt :: MixNodeExt
|
||
|
|
}
|
||
|
|
deriving (Eq,Show)
|
||
|
|
|
||
|
|
data Address
|
||
|
|
= ForwardDestination !DestinationAddr
|
||
|
|
| ReplyDestination !DestinationAddr
|
||
|
|
| MixNode !MixAddr
|
||
|
|
deriving (Eq,Show)
|
||
|
|
|
||
|
|
-- | A mix path is a route consisting of several mix nodes
|
||
|
|
type MixPath = [MixNodeInt]
|
||
|
|
|
||
|
|
-- | A Sphinx header consists of three part, denoted (after the Sphinx paper) by alpha, beta, and gamma.
|
||
|
|
data SphinxHeader = MkHeader
|
||
|
|
{ sphinxAlpha :: PubKey -- ^ blinded per-hop public key
|
||
|
|
, sphinxBeta :: [Word8] -- ^ encrypted routing info
|
||
|
|
, sphinxGamma :: MAC -- ^ MAC of beta
|
||
|
|
}
|
||
|
|
deriving (Eq) -- ,Show)
|
||
|
|
|
||
|
|
instance Show SphinxHeader where
|
||
|
|
show (MkHeader alpha beta gamma) = unlines
|
||
|
|
[ "alpha = " ++ show alpha
|
||
|
|
, "beta = " ++ showHexBytes beta
|
||
|
|
, "gamma = " ++ show gamma
|
||
|
|
]
|
||
|
|
|
||
|
|
--------------------------------------------------------------------------------
|
||
|
|
-- * per-hop secrets
|
||
|
|
|
||
|
|
data PerHopSecrets = MkPerHopSecrets
|
||
|
|
{ hopSecretKey :: SecretKey -- ^ per-node secret key @x@ (an element of the scalar field)
|
||
|
|
, hopPublicKey :: PubKey -- ^ per-node public key @alpha@ (an element of the elliptic curve group)
|
||
|
|
, hopSharedSecret :: Word256 -- ^ per-node shared secret @s@ (a group element interpreted as 32 bytes)
|
||
|
|
, hopBlindingFactor :: Fq -- ^ the blinding factor @b@ is also an element of the scalar field
|
||
|
|
}
|
||
|
|
deriving (Eq,Show)
|
||
|
|
|
||
|
|
-- TODO: really the blinder should be computed by a "proper" KDF, but everybody out there just uses plain SHA256...
|
||
|
|
computeBlinder :: PubKey -> Word256 -> Integer
|
||
|
|
computeBlinder alpha sharedSecret = toIntegerLE blinder where
|
||
|
|
blindInput = pubKeyBytes alpha ++ fromWord256 sharedSecret :: [Word8]
|
||
|
|
blinder = hash SHA256 blindInput :: Word256
|
||
|
|
|
||
|
|
computePerHopSecrets :: SecretKey -> [MixNodeExt] -> [PerHopSecrets]
|
||
|
|
computePerHopSecrets initialSecret path = go initialSecret path where
|
||
|
|
go :: SecretKey -> [MixNodeExt] -> [PerHopSecrets]
|
||
|
|
go _ [] = []
|
||
|
|
go x (mixnode:rest) = this : go x' rest where
|
||
|
|
alpha = secretKeyToPubKey x :: PubKey
|
||
|
|
shared = diffieHellmanSharedSecret x (nodePubKey mixnode) :: Word256
|
||
|
|
blindInput = pubKeyBytes alpha ++ fromWord256 shared :: [Word8]
|
||
|
|
blinder = computeBlinder alpha shared :: Integer
|
||
|
|
x' = blindSecretKey blinder x
|
||
|
|
this = MkPerHopSecrets
|
||
|
|
{ hopSecretKey = x
|
||
|
|
, hopPublicKey = alpha
|
||
|
|
, hopSharedSecret = shared
|
||
|
|
, hopBlindingFactor = toFq blinder
|
||
|
|
}
|
||
|
|
|
||
|
|
--------------------------------------------------------------------------------
|
||
|
|
-- * filler strings
|
||
|
|
|
||
|
|
newtype Filler
|
||
|
|
= Filler [Word8]
|
||
|
|
deriving (Eq)
|
||
|
|
|
||
|
|
instance Show Filler where
|
||
|
|
show (Filler bs) = "<" ++ showHexBytes bs ++ ">"
|
||
|
|
|
||
|
|
fromFiller :: Filler -> [Word8]
|
||
|
|
fromFiller (Filler ws) = ws
|
||
|
|
|
||
|
|
emptyFiller :: Filler
|
||
|
|
emptyFiller = Filler []
|
||
|
|
|
||
|
|
zeroFiller :: SizeInBytes -> Filler
|
||
|
|
zeroFiller len = Filler $ replicate len 0
|
||
|
|
|
||
|
|
fillerSize :: Filler -> SizeInBytes
|
||
|
|
fillerSize (Filler bs) = length bs
|
||
|
|
|
||
|
|
instance Semigroup Filler where
|
||
|
|
(<>) (Filler ws1) (Filler ws2) = Filler (ws1 ++ ws2)
|
||
|
|
|
||
|
|
instance Monoid Filler where
|
||
|
|
mempty = emptyFiller
|
||
|
|
|
||
|
|
-- | compute the both the unencrypted (plain) and encrypted fillers
|
||
|
|
computeAllFillers :: SizeInBytes -> [(PerHopSecrets,SizeInBytes)] -> [Filler]
|
||
|
|
computeAllFillers headerBetaSize hops
|
||
|
|
| padding < 0 = error "computeFillers: cannot fit in the given header beta size"
|
||
|
|
| otherwise = go emptyFiller hops
|
||
|
|
where
|
||
|
|
used = sum (map snd hops)
|
||
|
|
padding = headerBetaSize - used
|
||
|
|
|
||
|
|
go :: Filler -> [(PerHopSecrets,SizeInBytes)] -> [Filler]
|
||
|
|
go prev [] = []
|
||
|
|
go prev ((perhop,size):rest) = prev : go this rest where
|
||
|
|
ss = fromWord256 $ hopSharedSecret perhop
|
||
|
|
key = Key (mixKDF SphinxRouteEncKey ss)
|
||
|
|
iv = IV (mixKDF SphinxRouteEncIV ss)
|
||
|
|
thisPlain = fromFiller (prev <> zeroFiller size)
|
||
|
|
this = Filler $ zipWith xor thisPlain (drop padlen $ mixPRG key iv)
|
||
|
|
padlen = headerBetaSize - fillerSize prev
|
||
|
|
|
||
|
|
computeFinalFiller :: SizeInBytes -> [(PerHopSecrets,SizeInBytes)] -> Filler
|
||
|
|
computeFinalFiller headerBetaSize perHops = last $ computeAllFillers headerBetaSize perHops
|
||
|
|
|
||
|
|
--------------------------------------------------------------------------------
|
||
|
|
-- * constructing mix headers
|
||
|
|
|
||
|
|
encodeIntoBytes :: Binary a => a -> [Word8]
|
||
|
|
encodeIntoBytes = L.unpack . encode
|
||
|
|
|
||
|
|
decodeFromBytes :: Binary a => [Word8] -> Maybe (a, SizeInBytes, [Word8])
|
||
|
|
decodeFromBytes what = case decodeOrFail (L.pack what) of
|
||
|
|
Left _ -> Nothing
|
||
|
|
Right (rest,size,y) -> Just (y, fromIntegral size, L.unpack rest)
|
||
|
|
|
||
|
|
computeHeaderGeneric :: Binary route => SizeInBytes -> [(PerHopSecrets,route)] -> SphinxHeader
|
||
|
|
computeHeaderGeneric betaSize hops = head $ computeAllHeadersGeneric betaSize hops
|
||
|
|
|
||
|
|
data NextBeta
|
||
|
|
= NextBeta { _nextBeta :: [Word8] , _nextMac :: [Word8] } -- ^ @beta, gamma@
|
||
|
|
| FinalBeta { _nextBeta :: [Word8] , _nextFiller :: Filler } -- ^ @beta, filler@
|
||
|
|
deriving Show
|
||
|
|
|
||
|
|
computeAllHeadersGeneric :: Binary route => SizeInBytes -> [(PerHopSecrets,route)] -> [SphinxHeader]
|
||
|
|
computeAllHeadersGeneric headerBetaSize hops
|
||
|
|
| sum sizes > headerBetaSize = error "computeHeadersGeneric: total route size is larger than the header beta size"
|
||
|
|
| otherwise = reverse $ worker finalBeta $ reverse (zip perhops routes)
|
||
|
|
where
|
||
|
|
nhops = length hops
|
||
|
|
perhops = map fst hops :: [PerHopSecrets]
|
||
|
|
routes = map (encodeIntoBytes . snd) hops :: [[Word8]]
|
||
|
|
sizes = map length routes :: [SizeInBytes]
|
||
|
|
|
||
|
|
-- add the size of the mac to the size of the route, except for the final destination
|
||
|
|
addMacSize :: [SizeInBytes] -> [SizeInBytes]
|
||
|
|
addMacSize [] = []
|
||
|
|
addMacSize [final] = [final]
|
||
|
|
addMacSize (n:ns) = (n+16) : addMacSize ns
|
||
|
|
|
||
|
|
finalFiller = computeFinalFiller headerBetaSize (zip perhops $ addMacSize sizes)
|
||
|
|
finalRoute = last routes
|
||
|
|
finalPad = headerBetaSize - length finalRoute - fillerSize finalFiller
|
||
|
|
finalBeta = FinalBeta (replicate finalPad 0) finalFiller
|
||
|
|
|
||
|
|
worker :: NextBeta -> [(PerHopSecrets, [Word8])] -> [SphinxHeader]
|
||
|
|
worker next [] = []
|
||
|
|
worker next ((perhop,route):rest) = header : worker next' rest where
|
||
|
|
|
||
|
|
ss = fromWord256 $ hopSharedSecret perhop
|
||
|
|
encKey = Key (mixKDF SphinxRouteEncKey ss)
|
||
|
|
encIV = IV (mixKDF SphinxRouteEncIV ss)
|
||
|
|
macKey = Key (mixKDF SphinxMacKey ss)
|
||
|
|
|
||
|
|
encrypt :: [Word8] -> [Word8]
|
||
|
|
encrypt = mixRouteEnc encKey encIV
|
||
|
|
|
||
|
|
beta = case next of
|
||
|
|
NextBeta nextBeta nextGamma -> encrypt $ take headerBetaSize $ (route ++ nextGamma ++ nextBeta)
|
||
|
|
FinalBeta nextBeta filler -> encrypt (route ++ nextBeta) ++ fromFiller filler
|
||
|
|
|
||
|
|
alpha = hopPublicKey perhop
|
||
|
|
gamma = mixMAC macKey beta
|
||
|
|
header = MkHeader alpha beta gamma
|
||
|
|
next' = NextBeta beta (macBytes gamma)
|
||
|
|
|
||
|
|
--------------------------------------------------------------------------------
|
||
|
|
-- * processing mix headers
|
||
|
|
|
||
|
|
processMixHeaderGeneric :: Binary route => MixNodeInt -> SphinxHeader -> Either String (route, SphinxHeader)
|
||
|
|
processMixHeaderGeneric mixNode (MkHeader alpha beta gamma) =
|
||
|
|
if gamma /= macBeta
|
||
|
|
then Left $ "MAC of beta doesn't match\n - header gamma = " ++ show gamma ++ "\n - MAC(beta) = " ++ show macBeta
|
||
|
|
else case decodeFromBytes betaTilde of
|
||
|
|
Nothing -> Left "cannot parse beginning of beta"
|
||
|
|
Just (route, _size, rest) -> Right (route, MkHeader alpha' beta' gamma') where
|
||
|
|
(gamma1, rest1) = splitAt 16 rest
|
||
|
|
gamma' = MAC (W128 gamma1)
|
||
|
|
beta' = take betaSize rest1
|
||
|
|
|
||
|
|
where
|
||
|
|
shared = diffieHellmanSharedSecret (nodePrivKey mixNode) alpha
|
||
|
|
ss = fromWord256 shared
|
||
|
|
encKey = Key (mixKDF SphinxRouteEncKey ss)
|
||
|
|
encIV = IV (mixKDF SphinxRouteEncIV ss)
|
||
|
|
macKey = Key (mixKDF SphinxMacKey ss)
|
||
|
|
|
||
|
|
betaSize = length beta
|
||
|
|
macBeta = mixMAC macKey beta
|
||
|
|
betaTilde = mixRouteDec encKey encIV (beta ++ replicate betaSize 0) -- we don't know yet how long "route" is, so just add enough zeros lol
|
||
|
|
|
||
|
|
blinder = computeBlinder alpha shared
|
||
|
|
alpha' = blindPublicKey blinder alpha
|
||
|
|
|
||
|
|
--------------------------------------------------------------------------------
|
||
|
|
-- * generate random mix nodes
|
||
|
|
|
||
|
|
randomMixAddr :: Int -> IO MixAddr
|
||
|
|
randomMixAddr len = MkMixAddr <$> replicateM len randomIO
|
||
|
|
|
||
|
|
randomMixNode :: IO MixNodeInt
|
||
|
|
randomMixNode = do
|
||
|
|
(sk,pk) <- randomKeyPair
|
||
|
|
addr <- randomMixAddr 16
|
||
|
|
let nodeExt = MkMixNodeExt
|
||
|
|
{ nodePubKey = pk
|
||
|
|
, nodeAddress = addr
|
||
|
|
}
|
||
|
|
return $ MkMixNodeInt
|
||
|
|
{ nodePrivKey = sk
|
||
|
|
, nodeExt = nodeExt
|
||
|
|
}
|
||
|
|
|
||
|
|
randomMixPath :: NHops -> IO [MixNodeInt]
|
||
|
|
randomMixPath nhops = replicateM nhops randomMixNode
|
||
|
|
|
||
|
|
--------------------------------------------------------------------------------
|
||
|
|
-- * testing
|
||
|
|
|
||
|
|
data TestRouting
|
||
|
|
= A String
|
||
|
|
| B Int
|
||
|
|
| C Bool [Int]
|
||
|
|
| F String
|
||
|
|
deriving (Eq,Show,Generic)
|
||
|
|
|
||
|
|
instance Binary TestRouting
|
||
|
|
|
||
|
|
testRoute :: [TestRouting]
|
||
|
|
testRoute =
|
||
|
|
[ A "foobar"
|
||
|
|
, B 1137
|
||
|
|
, C True [3,4,5]
|
||
|
|
, A "whatever"
|
||
|
|
, A "almafa"
|
||
|
|
, B 777
|
||
|
|
, F "final destination"
|
||
|
|
]
|
||
|
|
|
||
|
|
testProcess :: [MixNodeInt] -> SphinxHeader -> [TestRouting]
|
||
|
|
testProcess = go 0 where
|
||
|
|
go idx [] _ = []
|
||
|
|
go idx (node:nodes) header = case processMixHeaderGeneric node header of
|
||
|
|
Left errmsg -> error $ "error at hop #" ++ show idx ++ ": " ++ errmsg
|
||
|
|
Right (route,header') -> route : go (idx+1) nodes header'
|
||
|
|
|
||
|
|
testMain :: IO ()
|
||
|
|
testMain = do
|
||
|
|
let headerBetaSize = 250
|
||
|
|
sk <- randomSecretKey
|
||
|
|
let route = testRoute
|
||
|
|
let nhops = length route
|
||
|
|
mixpath <- randomMixPath nhops
|
||
|
|
let perhopsecrets = computePerHopSecrets sk (map nodeExt mixpath)
|
||
|
|
|
||
|
|
let headers = computeAllHeadersGeneric headerBetaSize (zip perhopsecrets route)
|
||
|
|
-- mapM_ print headers
|
||
|
|
let header = head headers
|
||
|
|
print $ map (length . sphinxBeta) headers
|
||
|
|
print $ testProcess mixpath header
|
||
|
|
|
||
|
|
--------------------------------------------------------------------------------
|
||
|
|
|