summaryrefslogtreecommitdiffhomepage
path: root/HTTP/ProofOfWork.hs
diff options
context:
space:
mode:
Diffstat (limited to 'HTTP/ProofOfWork.hs')
-rw-r--r--HTTP/ProofOfWork.hs171
1 files changed, 171 insertions, 0 deletions
diff --git a/HTTP/ProofOfWork.hs b/HTTP/ProofOfWork.hs
new file mode 100644
index 0000000..a94b19b
--- /dev/null
+++ b/HTTP/ProofOfWork.hs
@@ -0,0 +1,171 @@
+{- Copyright 2016 Joey Hess <id@joeyh.name>
+ -
+ - Licensed under the GNU AGPL version 3 or higher.
+ -}
+
+{-# LANGUAGE DeriveGeneric, OverloadedStrings #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+
+module HTTP.ProofOfWork where
+
+import Types
+import Types.Cost
+import ExpensiveHash
+import Tunables
+import ByteStrings
+import GHC.Generics
+import qualified Data.Text as T
+import qualified Data.ByteString as B
+import Data.Text.Encoding (encodeUtf8)
+import Raaz.Core.Encode
+import qualified Raaz
+import Data.BloomFilter.Hash
+import Control.Monad
+import Control.DeepSeq
+import Data.Word
+import Data.Bits
+import Data.Monoid
+import Prelude
+
+-- | A value that the client has to do some work to calculate.
+data ProofOfWork = ProofOfWork B.ByteString RequestID
+ deriving (Show, Generic)
+
+instance NFData ProofOfWork
+
+data ProofOfWorkRequirement = ProofOfWorkRequirement
+ { leadingZeros :: Int
+ , addedArgon2Iterations :: Word32
+ , requestID :: RequestID
+ }
+ deriving (Generic, Show)
+
+-- | A request ID has two parts, a RandomSalt and a HMAC.
+-- The server can verify if a request ID is one it generated.
+data RequestID = RequestID
+ { randomSalt :: RandomSalt
+ , requestHMAC :: T.Text
+ }
+ deriving (Generic, Show, Eq)
+
+instance NFData RequestID
+
+instance Hashable RequestID where
+ hashIO32 = hashIO32 . hashRequestID
+ hashIO64 = hashIO64 . hashRequestID
+
+hashRequestID :: RequestID -> B.ByteString
+hashRequestID rid = encodeUtf8 (fromRandomSalt (randomSalt rid))
+ <> ":" <> encodeUtf8 (requestHMAC rid)
+
+-- | Using Text and not ByteString so that ProofOfWorkRequirement can have a
+-- JSON instance.
+newtype RandomSalt = RandomSalt { fromRandomSalt :: T.Text }
+ deriving (Generic, Show, Eq)
+
+instance NFData RandomSalt
+
+-- | Servers should never demand a proof of work that takes longer than
+-- this to generate. Note that if a server changes its mind and doubles
+-- the proof of work, a client counts that cumulatively. So, a server
+-- should avoid any single proof of work requirement taking more than half
+-- this long.
+maxProofOfWork :: Seconds
+maxProofOfWork = Seconds (16*60)
+
+-- | How long it will take to generate a proof of work meeting the
+-- requirement, maximum.
+--
+-- Of course, a client can get lucky and find a value that works
+-- on the very first try. On average, the client will need to work for half
+-- as long as the returned number of Seconds.
+generationTime :: ProofOfWorkRequirement -> Seconds
+generationTime req =
+ let UseArgon2 (CPUCost (Seconds s) _) _ = proofOfWorkHashTunable (addedArgon2Iterations req)
+ in Seconds ((2^(leadingZeros req)) * s)
+
+mkProofOfWorkRequirement :: Seconds -> Maybe (RequestID -> ProofOfWorkRequirement)
+mkProofOfWorkRequirement (Seconds n)
+ | lz < 1 || n <= 1 = Nothing
+ | otherwise = Just $ ProofOfWorkRequirement lz its
+ where
+ lz = floor (logBase 2 (fromRational (max 1 (n / s))) :: Double)
+ UseArgon2 (CPUCost (Seconds s) _) _ = proofOfWorkHashTunable its
+ its = 0
+
+newtype RequestIDSecret = RequestIDSecret (Raaz.Key (Raaz.HMAC Raaz.SHA256))
+
+newRequestIDSecret :: IO RequestIDSecret
+newRequestIDSecret = do
+ prg <- Raaz.newPRG () :: IO Raaz.SystemPRG
+ RequestIDSecret <$> Raaz.random prg
+
+mkRequestID :: RequestIDSecret -> IO RequestID
+mkRequestID secret = mkRequeestID' secret <$> mkRandomSalt
+
+mkRequeestID' :: RequestIDSecret -> RandomSalt -> RequestID
+mkRequeestID' (RequestIDSecret key) salt =
+ let hmac = Raaz.hmacSha256 key (encodeUtf8 $ fromRandomSalt salt)
+ in RequestID salt (T.pack (showBase16 hmac))
+
+validRequestID :: RequestIDSecret -> RequestID -> Bool
+validRequestID secret rid =
+ let rid' = mkRequeestID' secret (randomSalt rid)
+ in requestHMAC rid == requestHMAC rid'
+
+mkRandomSalt :: IO RandomSalt
+mkRandomSalt = do
+ prg <- Raaz.newPRG () :: IO Raaz.SystemPRG
+ rs <- replicateM 16 (Raaz.random prg :: IO Word8)
+ return $ RandomSalt $ T.pack $ concatMap show rs
+
+class POWIdent p where
+ getPOWIdent :: p -> B.ByteString
+
+instance POWIdent StorableObjectIdent where
+ getPOWIdent (StorableObjectIdent i) = i
+
+data NoPOWIdent = NoPOWIdent
+
+instance POWIdent NoPOWIdent where
+ getPOWIdent NoPOWIdent = B.empty
+
+instance POWIdent Int where
+ getPOWIdent = encodeUtf8 . T.pack . show
+
+-- Note that this does not check validRequestID.
+isValidProofOfWork :: POWIdent p => ProofOfWork -> ProofOfWorkRequirement -> p -> Bool
+isValidProofOfWork (ProofOfWork pow rid) req p = samerequestids && enoughzeros
+ where
+ samerequestids = rid == requestID req
+ enoughzeros = all (== False) (take (leadingZeros req) (setBits b))
+ tunable = proofOfWorkHashTunable (addedArgon2Iterations req)
+ salt = Salt $ POWSalt $
+ encodeUtf8 (fromRandomSalt (randomSalt (requestID req))) <> pow
+ ExpensiveHash _ hash = expensiveHash tunable salt (getPOWIdent p)
+ -- Since expensiveHash generates an ascii encoded hash that
+ -- includes the parameters, take the sha256 of it to get the
+ -- bytestring that is what's checked for the neccesary number
+ -- of leading 0 bits.
+ b = Raaz.toByteString $ Raaz.sha256 $ encodeUtf8 hash
+
+setBits :: B.ByteString -> [Bool]
+setBits = concatMap go . B.unpack
+ where
+ go byte = map (uncurry testBit) (zip (repeat byte) [0..7])
+
+newtype POWSalt = POWSalt B.ByteString
+
+instance Encodable POWSalt where
+ toByteString (POWSalt n) = n
+ fromByteString = Just . POWSalt
+
+genProofOfWork :: POWIdent p => ProofOfWorkRequirement -> p -> ProofOfWork
+genProofOfWork req p = go allByteStrings
+ where
+ go [] = error "failed to generate Proof Of Work. This should be impossible!"
+ go (b:bs)
+ | isValidProofOfWork candidate req p = candidate
+ | otherwise = go bs
+ where
+ candidate = ProofOfWork b (requestID req)