hsoz-0.0.0.3: src/Network/Hawk/Client.hs
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE RecordWildCards #-}
-- | Functions for making Hawk-authenticated request headers and
-- verifying responses from the server.
module Network.Hawk.Client
( header
, headerOz
, getBewit
, authenticate
, ServerAuthorizationCheck(..)
, Credentials(..)
, Header(..)
, Authorization
, HeaderArtifacts
, module Network.Hawk.Types
) where
import Control.Monad.IO.Class (MonadIO, liftIO)
import Crypto.Hash
import Crypto.Random
import qualified Data.ByteArray as BA (unpack)
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString.Char8 as S8
import qualified Data.ByteString.Lazy as BL
import Data.Byteable (constEqBytes)
import Data.CaseInsensitive (CI (..))
import qualified Data.Map as M
import Data.Maybe (catMaybes, fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8)
import Data.Time.Clock (NominalDiffTime)
import Data.Time.Clock.POSIX
import Network.HTTP.Types.Header (hContentType, hWWWAuthenticate, HeaderName)
import Network.HTTP.Types.Method (Method)
import Network.HTTP.Types.URI (extractPath)
import Network.HTTP.Client (Response, responseHeaders)
import Network.Socket (PortNumber, SockAddr (..))
import URI.ByteString (authorityHost, authorityPort,
hostBS, laxURIParserOptions,
parseURI, portNumber, uriAuthority)
import Network.Hawk.Common
import Network.Hawk.Types
import Network.Hawk.Util
import Network.Iron.Util
import Network.Hawk.Client.Types
import Network.Hawk.Client.HeaderParser
-- | Generates the Hawk authentication header for a request.
header :: Text -- ^ The request URL
-> Method -- ^ The request method
-> Credentials -- ^ Credentials used to generate the header
-> Maybe PayloadInfo -- ^ Optional request payload
-> Maybe Text -- ^ @ext@ data
-> IO Header
header url method creds payload ext = headerBase url method creds payload ext Nothing Nothing
-- | Generates the Hawk authentication header for an Oz request. Oz
-- requires another attribute -- the application id. It also has an
-- optional delegated-by attribute, which is the application id of the
-- application the credentials were directly issued to.
headerOz :: Text -> Method -> Credentials -> Maybe PayloadInfo -> Maybe Text
-> Text -> Maybe Text -> IO Header
headerOz url method creds payload ext app dlg = headerBase url method creds payload ext (Just app) dlg
headerBase :: Text -> Method -> Credentials -> Maybe PayloadInfo -> Maybe Text
-> Maybe Text -> Maybe Text -> IO Header
headerBase url method creds payload ext app dlg = do
now <- getPOSIXTime
nonce <- genNonce
let hash = calculatePayloadHash (ccAlgorithm creds) <$> payload
let art = clientHeaderArtifacts now nonce method (encodeUtf8 url) hash (encodeUtf8 <$> ext) app dlg
let auth = clientHawkAuth creds art
return $ Header auth art
clientHeaderArtifacts :: POSIXTime -> ByteString -> Method -> ByteString
-> Maybe ByteString -> Maybe ByteString
-> Maybe Text -> Maybe Text
-> HeaderArtifacts
clientHeaderArtifacts now nonce method url hash ext app dlg = case splitUrl url of
Just (SplitURL host port resource) ->
HeaderArtifacts now nonce method host port resource hash ext app dlg
Nothing ->
HeaderArtifacts now nonce method "" Nothing url hash ext app dlg
clientHawkAuth :: Credentials -> HeaderArtifacts -> ByteString
clientHawkAuth creds arts@HeaderArtifacts{..} = hawkHeaderString (hawkHeaderItems items)
where
items = [ ("id", (Just . encodeUtf8 . ccId) creds)
, ("ts", (Just . S8.pack . show . round) chaTimestamp)
, ("nonce", Just chaNonce)
, ("hash", chaHash)
, ("ext", chaExt)
, ("mac", Just $ clientMac HawkHeader creds arts)
, ("app", encodeUtf8 <$> chaApp)
, ("dlg", encodeUtf8 <$> chaDlg)
]
clientMac :: HawkType -> Credentials -> HeaderArtifacts -> ByteString
clientMac h Credentials{..} HeaderArtifacts{..} =
calculateMac ccAlgorithm ccKey
chaTimestamp chaNonce chaMethod chaResource chaHost chaPort h
hawkHeaderItems :: [(ByteString, Maybe ByteString)] -> [(ByteString, ByteString)]
hawkHeaderItems = catMaybes . map pull
where
pull (k, Just v) = Just (k, v)
pull (k, Nothing) = Nothing
splitUrl :: ByteString -> Maybe SplitURL
splitUrl url = SplitURL <$> host <*> pure port <*> path
where
p = either (const Nothing) uriAuthority (parseURI laxURIParserOptions url)
host = fmap (hostBS . authorityHost) p
port :: Maybe Int
port = fmap portNumber $ p >>= authorityPort
path = fmap (const (extractPath url)) p
genNonce :: IO ByteString
genNonce = do
g <- getSystemDRG
return $ fst $ withRandomBytes g 10 B64.encode
-- | Whether the client wants to check the received
-- @Server-Authorization@ header depends on the application.
data ServerAuthorizationCheck = ServerAuthorizationNotRequired
| ServerAuthorizationRequired
deriving Show
-- | Validates the server response.
authenticate :: Response body -> Credentials -> HeaderArtifacts
-> Maybe BL.ByteString -> ServerAuthorizationCheck
-> IO (Either String ())
authenticate r creds artifacts payload saCheck = do
now <- getPOSIXTime
return $ clientAuthenticate' r creds artifacts payload saCheck now
clientAuthenticate' :: Response body -> Credentials -> HeaderArtifacts
-> Maybe BL.ByteString -> ServerAuthorizationCheck
-> POSIXTime -> Either String ()
clientAuthenticate' r creds artifacts payload saCheck now = do
let w = responseHeader hWWWAuthenticate r
ts <- mapM (checkWwwAuthenticateHeader creds) w
let sa = responseHeader hServerAuthorization r
sarh <- checkServerAuthorizationHeader creds artifacts saCheck now sa
let ct = fromMaybe "" $ responseHeader hContentType r
let payload' = PayloadInfo ct <$> payload
case sarh of
Just sarh' -> checkPayloadHash (ccAlgorithm creds) (sarhHash sarh') payload'
Nothing -> Right ()
-- fixme: lens version from wreq is better
responseHeader :: HeaderName -> Response body -> Maybe ByteString
responseHeader h = lookup h . responseHeaders
-- | The protocol relies on a clock sync between the client and
-- server. To accomplish this, the server informs the client of its
-- current time when an invalid timestamp is received.
--
-- If an attacker is able to manipulate this information and cause the
-- client to use an incorrect time, it would be able to cause the
-- client to generate authenticated requests using time in the
-- future. Such requests will fail when sent by the client, and will
-- not likely leave a trace on the server (given the common
-- implementation of nonce, if at all enforced). The attacker will
-- then be able to replay the request at the correct time without
-- detection.
--
-- The client must only use the time information provided by the
-- server if:
--
-- * it was delivered over a TLS connection and the server identity
-- has been verified, or
-- * the `tsm` MAC digest calculated using the same client credentials
-- over the timestamp has been verified.
--
-- fixme: implement checks for both of the above conditions
checkWwwAuthenticateHeader :: Credentials -> ByteString -> Either String POSIXTime
checkWwwAuthenticateHeader creds w = do
WwwAuthenticateHeader{..} <- parseWwwAuthenticateHeader w
let tsm = calculateTsMac (ccAlgorithm creds) wahTs
if wahTsm `constEqBytes` tsm
then Right wahTs
else Left "Invalid server timestamp hash"
checkServerAuthorizationHeader :: Credentials -> HeaderArtifacts
-> ServerAuthorizationCheck -> POSIXTime
-> Maybe ByteString
-> Either String (Maybe ServerAuthorizationReplyHeader)
checkServerAuthorizationHeader _ _ ServerAuthorizationNotRequired _ Nothing = Right Nothing
checkServerAuthorizationHeader _ _ ServerAuthorizationRequired _ Nothing = Left "Missing Server-Authorization header"
checkServerAuthorizationHeader creds arts _ now (Just sa) = do
sarh <- parseServerAuthorizationReplyHeader sa
let mac = clientMac HawkResponse creds arts
if sarhMac sarh `constEqBytes` mac
then Right (Just sarh)
else Left "Bad response mac"
----------------------------------------------------------------------------
-- | Generate a bewit value for a given URI.
getBewit :: Credentials -> NominalDiffTime -> Maybe ByteString -> NominalDiffTime
-> ByteString -> IO (Maybe ByteString)
-- fixme: ext is a json value i think
-- fixme: javascript version supports deconstructed parsed uri objects
-- fixme: not much point having two time interval arguments?
getBewit creds ttl ext offset uri = do
exp <- fmap (+ (ttl + offset)) getPOSIXTime
return $ bewit exp <$> splitUrl uri
where
bewit exp = encode . clientMac HawkBewit creds . make
where
make (SplitURL host port resource) =
HeaderArtifacts exp "" "GET" host port resource Nothing ext Nothing Nothing
encode = b64url . S8.intercalate "\\" . parts
parts mac = [ encodeUtf8 . ccId $ creds
, S8.pack . show . round $ exp
, mac, fromMaybe "" ext ]