{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NoFieldSelectors #-}
{-# LANGUAGE StrictData #-}
{-# OPTIONS_GHC -Wno-type-defaults #-}
module Puppy.Protocol.HTTP.Signature (
KeySource (..),
makeSignature,
makeDigestHeader,
fmtDateHeader,
fmtSignature,
Server (..),
Signer (..),
Signature (..),
Algorithm (..),
Request (..),
) where
import Crypto.Hash.Algorithms (SHA256(..))
import Data.Bifunctor (Bifunctor(..))
import Data.ByteString.Base64 (encodeBase64)
import Data.Foldable (Foldable(..))
import Data.Function ((&))
import Data.Functor ((<&>))
import Data.List (intersperse)
import Data.String (IsString (..))
import Data.Time.Format
import Data.Time (UTCTime)
import Puppy.Config (ServerConfig (..))
import Puppy.Crypto
import Puppy.Protocol.ActivityStreams (Id (..))
import Puppy.Types (Channel (..))
import qualified Crypto.PubKey.RSA as RSA
import qualified Crypto.PubKey.RSA.PKCS15 as PKCS15
import qualified Data.ByteString as BS
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
data Request
= POST {
body :: BS.ByteString,
host :: BS.ByteString,
path :: BS.ByteString,
date :: UTCTime
}
| GET {
host :: BS.ByteString,
path :: BS.ByteString,
date :: UTCTime
}
data Signature
= Signature {
keyId :: Id,
algorithm :: Algorithm,
components :: [BS.ByteString],
signature :: T.Text
}
data Algorithm
= RSA_SHA256
| HS2019
instance Show Algorithm where
show RSA_SHA256 = "rsa-sha256"
show HS2019 = "hs2019"
makeSignature :: Signer -> Request -> Either String Signature
makeSignature (Signer { privateKey, keyId }) req =
let common = [ ("(request-target)", fmtTarget req),
("date", fmtDateHeader req.date),
("host", req.host) ]
components
| POST { body } <- req = common <> [("digest", makeDigestHeader body)]
| otherwise = common
in generateSignature components privateKey keyId
& first show
where
fmtTarget (POST { path }) = "post " <> path
fmtTarget (GET { path }) = "get " <> path
generateSignature
:: [(BS.ByteString, BS.ByteString)]
-> RSA.PrivateKey
-> Id
-> Either RSA.Error Signature
generateSignature components key keyId =
let signatureString :: BS.ByteString
signatureString = [ fold [k, ": ", v] | (k,v) <- components ]
& intersperse "\n"
& fold
sign = PKCS15.sign
Nothing (Just SHA256) key
signatureString
structure signature = Signature { signature, algorithm = RSA_SHA256, components = map fst components, keyId }
in sign <&> encodeBase64
<&> structure
makeDigestHeader :: BS.ByteString -> BS.ByteString
makeDigestHeader = ("sha-256=" <>) . base64 . sha256
fmtDateHeader :: UTCTime -> BS.ByteString
fmtDateHeader now = formatTime defaultTimeLocale "%a, %d %b %Y %H:%M:%S GMT" now
& T.pack
& T.encodeUtf8
fmtSignature :: Signature -> BS.ByteString
fmtSignature sig =
fromString $ fold $ intersperse ", " [
"keyId=" <> show sig.keyId.url,
"algorithm=" <> show (show sig.algorithm),
"headers=" <> show (fold $ intersperse " " sig.components),
"signature=" <> show sig.signature
]
class KeySource s where
toSigner :: s -> Signer
data Signer
= Signer {
keyId :: Id,
privateKey :: RSA.PrivateKey
}
newtype Server = Server ServerConfig
instance KeySource Server where
toSigner (Server c) = Signer {
keyId = Id ("https://" <> c.name <> "/ap/a/server#key"),
privateKey = c.serverKey
}
instance KeySource Channel where
toSigner c = Signer {
keyId = Id (c.linkedActorId <> "#key"),
privateKey = c.privateKeyPem
}
instance KeySource Signer where
toSigner = id