164 lines
5.5 KiB
Haskell
164 lines
5.5 KiB
Haskell
{-# LANGUAGE RecordWildCards #-}
|
|
{-# LANGUAGE ViewPatterns #-}
|
|
|
|
module Fedi.Signature.Check (
|
|
module Fedi.Signature.Types,
|
|
module Fedi.Signature.Check,
|
|
) where
|
|
|
|
import Control.Monad.IO.Class
|
|
import Data.ByteString qualified as BS
|
|
import Data.ByteString.Lazy qualified as BSL
|
|
import Data.CaseInsensitive qualified as CI
|
|
import Data.Text qualified as T
|
|
import Data.Text.Encoding qualified as T
|
|
import Fedi.Crypto
|
|
import Fedi.Helpers
|
|
import Fedi.Requests
|
|
import Fedi.Routes.Helpers
|
|
import Fedi.Signature.Types
|
|
import Fedi.Types
|
|
import Fedi.UserDetails
|
|
import Network.HTTP.Types.Header qualified as HTTP
|
|
import Network.HTTP.Types.URI qualified as HTTP
|
|
import Network.Wai qualified as Wai
|
|
import Text.ParserCombinators.ReadP qualified as P
|
|
import Web.Twain qualified as Twain
|
|
import Prelude hiding (error)
|
|
|
|
-- * Check
|
|
|
|
-- | Check the signature of the sender and parse the body of the request.
|
|
checkSignatureAndParseBody :: Twain.ResponderM AnyActivity
|
|
checkSignatureAndParseBody = do
|
|
-- get info
|
|
request <- Twain.request
|
|
-- liftIO $ print ("headers", Twain.requestHeaders request)
|
|
body <- liftIO (Wai.strictRequestBody request)
|
|
sigheader <- parseSignature =<< Twain.header "Signature"
|
|
digest <-
|
|
Twain.header "Digest"
|
|
>>= maybe (throw "missing header Digest") (pure . T.encodeUtf8)
|
|
(person :: Person) <- liftIO $ sendGet sigheader.keyId
|
|
let
|
|
personPkid = person.otype.publicKey.pkid
|
|
let
|
|
personPublicKey = pemToBS person.otype.publicKey.publicKeyPem
|
|
|
|
signatureString <-
|
|
signatureStringFromRequest request sigheader.headers
|
|
|
|
-- check
|
|
liftIO $
|
|
checkSignature personPkid personPublicKey sigheader signatureString digest (BSL.toStrict body)
|
|
|
|
-- parse the body and return it
|
|
parseJson body
|
|
|
|
signatureStringFromRequest
|
|
:: forall m
|
|
. (MonadThrow m)
|
|
=> Wai.Request -> [T.Text] -> m ByteString
|
|
signatureStringFromRequest request (map (T.encodeUtf8 . T.toLower) -> headers) = do
|
|
let
|
|
requestHeaders = Wai.requestHeaders request
|
|
method = T.encodeUtf8 $ T.toLower $ T.decodeUtf8 $ Wai.requestMethod request
|
|
path =
|
|
"/"
|
|
<> T.encodeUtf8 (T.intercalate "/" $ Wai.pathInfo request)
|
|
<> HTTP.renderQuery True (Wai.queryString request)
|
|
requestTarget = method <> " " <> path
|
|
signatureStringFromSpecifics requestHeaders requestTarget headers
|
|
|
|
signatureStringFromSpecifics
|
|
:: forall m
|
|
. (MonadThrow m)
|
|
=> HTTP.RequestHeaders -> ByteString -> [ByteString] -> m ByteString
|
|
signatureStringFromSpecifics requestHeaders requestTarget headers = do
|
|
let
|
|
mylookup :: ByteString -> m ByteString
|
|
mylookup header
|
|
| header == "(request-target)" =
|
|
pure $ header <> ": " <> requestTarget
|
|
| header == "host" = do
|
|
let
|
|
result = lookup (CI.mk header) requestHeaders
|
|
case result of
|
|
Nothing -> throw $ "Missing header '" <> show header <> "'."
|
|
Just value ->
|
|
pure $
|
|
header
|
|
<> ": "
|
|
<> if ":443" `BS.isSuffixOf` value
|
|
then BS.dropEnd (BS.length ":443") value
|
|
else value
|
|
| otherwise = do
|
|
let
|
|
result = lookup (CI.mk header) requestHeaders
|
|
case result of
|
|
Nothing -> throw $ "Missing header '" <> show header <> "'."
|
|
Just value -> pure $ header <> ": " <> value
|
|
BS.intercalate "\n" <$> traverse mylookup headers
|
|
|
|
checkSignature
|
|
:: (MonadIO m)
|
|
=> (MonadThrow m)
|
|
=> Url -> ByteString -> SignatureHeader -> ByteString -> ByteString -> ByteString -> m ()
|
|
checkSignature personPkid personPublicKey sigheader signatureString digest body = do
|
|
-- check
|
|
unless (personPkid == sigheader.keyId) $
|
|
throw "public key mismatch with signature."
|
|
|
|
pub <- verifyPub personPublicKey sigheader.signature signatureString
|
|
unless pub $
|
|
throw "signature verification failed."
|
|
|
|
let
|
|
mydigest = "SHA-256=" <> encodeBase64 (makeDigest body)
|
|
unless (mydigest == digest) $
|
|
throw "digest verification failed."
|
|
|
|
-- todo: check date
|
|
|
|
parseSignature :: (MonadThrow m) => Maybe T.Text -> m SignatureHeader
|
|
parseSignature minput = do
|
|
input <- maybe (throw "no signature.") (pure . T.unpack) minput
|
|
case P.readP_to_S parser input of
|
|
[(sig, "")] -> pure sig
|
|
xs -> throw $ "error parsing signature: " <> show xs
|
|
where
|
|
lookup' a b =
|
|
maybe (fail "error parsing signature") pure $ lookup a b
|
|
parser = do
|
|
components <- component `P.sepBy` P.char ','
|
|
keyId <- lookup' KeyId components
|
|
headers <- T.split (== ' ') . T.pack <$> lookup' Headers components
|
|
signature <-
|
|
(fromString)
|
|
<$> lookup' Signature components
|
|
P.eof
|
|
pure SignatureHeader {..}
|
|
component =
|
|
P.choice
|
|
[ do
|
|
_ <- P.string "keyId="
|
|
url <- P.between (P.char '\"') (P.char '\"') (P.munch1 (/= '\"'))
|
|
pure (KeyId, url)
|
|
, do
|
|
_ <- P.string "headers="
|
|
url <- P.between (P.char '\"') (P.char '\"') (P.munch1 (/= '\"'))
|
|
pure (Headers, url)
|
|
, do
|
|
_ <- P.string "signature="
|
|
url <- P.between (P.char '\"') (P.char '\"') (P.munch1 (/= '\"'))
|
|
pure (Signature, url)
|
|
, do
|
|
_ <- P.string "algorithm="
|
|
alg <- P.between (P.char '\"') (P.char '\"') (P.string "rsa-sha256")
|
|
pure (Algorithm, alg)
|
|
-- , do
|
|
-- key <- P.munch1 (/= '=')
|
|
-- _ <- P.char '='
|
|
-- value <- P.between (P.char '\"') (P.char '\"') (P.munch1 (/= '\"'))
|
|
-- pure (Other key, value)
|
|
]
|