is this how you sign? maybe
This commit is contained in:
		
							parent
							
								
									e741ba9251
								
							
						
					
					
						commit
						f8a786e455
					
				
					 7 changed files with 250 additions and 118 deletions
				
			
		| 
						 | 
					@ -24,6 +24,9 @@ library
 | 
				
			||||||
    Fedi.UserDetails
 | 
					    Fedi.UserDetails
 | 
				
			||||||
    Fedi.Webfinger
 | 
					    Fedi.Webfinger
 | 
				
			||||||
    Fedi.Crypto
 | 
					    Fedi.Crypto
 | 
				
			||||||
 | 
					    Fedi.Signature.Types
 | 
				
			||||||
 | 
					    Fedi.Signature.Check
 | 
				
			||||||
 | 
					    Fedi.Signature.Sign
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Fedi.Routes
 | 
					    Fedi.Routes
 | 
				
			||||||
    Fedi.Routes.Helpers
 | 
					    Fedi.Routes.Helpers
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,6 +11,7 @@ import Fedi.Helpers
 | 
				
			||||||
import Fedi.UserDetails
 | 
					import Fedi.UserDetails
 | 
				
			||||||
import Data.ByteString.Base64 qualified as Base64
 | 
					import Data.ByteString.Base64 qualified as Base64
 | 
				
			||||||
import Data.Base64.Types qualified as Base64
 | 
					import Data.Base64.Types qualified as Base64
 | 
				
			||||||
 | 
					import Data.Text qualified as T
 | 
				
			||||||
 | 
					
 | 
				
			||||||
verifyPub :: MonadThrow m => ByteString -> ByteString -> ByteString -> m Bool
 | 
					verifyPub :: MonadThrow m => ByteString -> ByteString -> ByteString -> m Bool
 | 
				
			||||||
verifyPub pubkeypem sig message = do
 | 
					verifyPub pubkeypem sig message = do
 | 
				
			||||||
| 
						 | 
					@ -64,10 +65,20 @@ ppSigned :: Signed -> String
 | 
				
			||||||
ppSigned signed =
 | 
					ppSigned signed =
 | 
				
			||||||
  unlines
 | 
					  unlines
 | 
				
			||||||
    [ "Signature"
 | 
					    [ "Signature"
 | 
				
			||||||
    , "{ signedMessage = " <> encodeBase64 signed.signedMessage
 | 
					    , "{ signedMessage = " <> encodeBase64String signed.signedMessage
 | 
				
			||||||
    , ", signedDigest = " <> encodeBase64 signed.signedDigest
 | 
					    , ", signedDigest = " <> encodeBase64String signed.signedDigest
 | 
				
			||||||
    , "}"
 | 
					    , "}"
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
encodeBase64 :: ByteString -> String
 | 
					encodeBase64 :: ByteString -> ByteString
 | 
				
			||||||
encodeBase64 = show . Base64.extractBase64 . Base64.encodeBase64
 | 
					encodeBase64 = Base64.extractBase64 . Base64.encodeBase64'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					encodeBase64String :: ByteString -> String
 | 
				
			||||||
 | 
					encodeBase64String = T.unpack . Base64.extractBase64 . Base64.encodeBase64
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					decodeBase64 :: ByteString -> ByteString
 | 
				
			||||||
 | 
					decodeBase64 = Base64.decodeBase64Lenient
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					makeDigest :: ByteString -> String
 | 
				
			||||||
 | 
					makeDigest message =
 | 
				
			||||||
 | 
					  show (Crypto.hash message :: Crypto.Digest Crypto.SHA256)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,13 +2,15 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module Fedi.Requests where
 | 
					module Fedi.Requests where
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Data.List (intercalate)
 | 
				
			||||||
import Data.Aeson qualified as A
 | 
					import Data.Aeson qualified as A
 | 
				
			||||||
import Fedi.Helpers
 | 
					import Fedi.Helpers
 | 
				
			||||||
import Fedi.UserDetails
 | 
					import Fedi.UserDetails
 | 
				
			||||||
import Fedi.Crypto
 | 
					import Fedi.Signature.Sign
 | 
				
			||||||
import Network.HTTP.Req qualified as Req
 | 
					import Network.HTTP.Req qualified as Req
 | 
				
			||||||
import Data.ByteString.Lazy qualified as BSL
 | 
					import Data.ByteString.Lazy qualified as BSL
 | 
				
			||||||
import Text.URI qualified as URI
 | 
					import Text.URI qualified as URI
 | 
				
			||||||
 | 
					import Data.Text qualified as T
 | 
				
			||||||
 | 
					
 | 
				
			||||||
sendPost
 | 
					sendPost
 | 
				
			||||||
  :: (A.FromJSON output, A.ToJSON input)
 | 
					  :: (A.FromJSON output, A.ToJSON input)
 | 
				
			||||||
| 
						 | 
					@ -17,12 +19,10 @@ sendPost
 | 
				
			||||||
  -> input
 | 
					  -> input
 | 
				
			||||||
  -> IO output
 | 
					  -> IO output
 | 
				
			||||||
sendPost details url payload = do
 | 
					sendPost details url payload = do
 | 
				
			||||||
  let encoded = BSL.toStrict $ A.encode payload
 | 
					 | 
				
			||||||
  signed <- sign details encoded
 | 
					 | 
				
			||||||
  uri <- URI.mkURI $ fromString url
 | 
					  uri <- URI.mkURI $ fromString url
 | 
				
			||||||
 | 
					  let encoded = BSL.toStrict $ A.encode payload
 | 
				
			||||||
 | 
					  httpSignature <- makeHttpSignature details uri encoded
 | 
				
			||||||
  (url', scheme) <- maybe (throw "couldn't parse uri") pure (Req.useHttpsURI uri)
 | 
					  (url', scheme) <- maybe (throw "couldn't parse uri") pure (Req.useHttpsURI uri)
 | 
				
			||||||
  print ("url", url')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  Req.runReq Req.defaultHttpConfig do
 | 
					  Req.runReq Req.defaultHttpConfig do
 | 
				
			||||||
    r <-
 | 
					    r <-
 | 
				
			||||||
      Req.req
 | 
					      Req.req
 | 
				
			||||||
| 
						 | 
					@ -31,12 +31,39 @@ sendPost details url payload = do
 | 
				
			||||||
        (Req.ReqBodyBs encoded)
 | 
					        (Req.ReqBodyBs encoded)
 | 
				
			||||||
        Req.jsonResponse
 | 
					        Req.jsonResponse
 | 
				
			||||||
        ( scheme
 | 
					        ( scheme
 | 
				
			||||||
          <> Req.header "ContentType" "application/activity+json"
 | 
					          <> sigHeaders httpSignature
 | 
				
			||||||
          <> Req.header "Digest" signed.signedDigest
 | 
					 | 
				
			||||||
          <> Req.header "Signature" signed.signedMessage
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    pure $ Req.responseBody r
 | 
					    pure $ Req.responseBody r
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					makeHttpSignature :: UserDetails -> URI.URI -> ByteString -> IO HttpSignature
 | 
				
			||||||
 | 
					makeHttpSignature details uri encoded = do
 | 
				
			||||||
 | 
					  host <- case uri.uriAuthority of
 | 
				
			||||||
 | 
					    Right u -> pure $ T.unpack $ URI.unRText u.authHost
 | 
				
			||||||
 | 
					    _ -> throw "no host in uri"
 | 
				
			||||||
 | 
					  -- We may want to add the uri path and uri query,
 | 
				
			||||||
 | 
					  -- but maybe later.
 | 
				
			||||||
 | 
					  let
 | 
				
			||||||
 | 
					    path = case uri.uriPath of
 | 
				
			||||||
 | 
					      Just (_, list) ->
 | 
				
			||||||
 | 
					        "/" <> intercalate "/" (map (T.unpack . URI.unRText) $ toList list)
 | 
				
			||||||
 | 
					      Nothing -> "/"
 | 
				
			||||||
 | 
					  signSignature details host ("post " <> path) encoded
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					sigHeaders :: HttpSignature -> Req.Option scheme
 | 
				
			||||||
 | 
					sigHeaders =
 | 
				
			||||||
 | 
					  foldMap (uncurry Req.header) . makeSigHeaders
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					makeSigHeaders :: HttpSignature -> [(ByteString, ByteString)]
 | 
				
			||||||
 | 
					makeSigHeaders httpSignature =
 | 
				
			||||||
 | 
					  [ ("ContentType", "application/activity+json")
 | 
				
			||||||
 | 
					  , ("Accept", "application/activity+json")
 | 
				
			||||||
 | 
					  , ("Digest", httpSignature.digest)
 | 
				
			||||||
 | 
					  , ("Host", fromString httpSignature.host)
 | 
				
			||||||
 | 
					  , ("Date", fromString httpSignature.date)
 | 
				
			||||||
 | 
					  , ("Signature", toSignature httpSignature.signatureHeader)
 | 
				
			||||||
 | 
					  ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
sendGet :: (A.FromJSON a) => String -> IO a
 | 
					sendGet :: (A.FromJSON a) => String -> IO a
 | 
				
			||||||
sendGet url = do
 | 
					sendGet url = do
 | 
				
			||||||
  uri <- URI.mkURI $ fromString url
 | 
					  uri <- URI.mkURI $ fromString url
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,23 +1,13 @@
 | 
				
			||||||
{-# language RecordWildCards #-}
 | 
					 | 
				
			||||||
module Fedi.Routes.Inbox where
 | 
					module Fedi.Routes.Inbox where
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import Prelude hiding (error)
 | 
					import Prelude hiding (error)
 | 
				
			||||||
import Fedi.Requests
 | 
					 | 
				
			||||||
import Fedi.Types
 | 
					import Fedi.Types
 | 
				
			||||||
import Fedi.UserDetails
 | 
					import Fedi.UserDetails
 | 
				
			||||||
import Fedi.Routes.Helpers
 | 
					 | 
				
			||||||
import Fedi.Helpers
 | 
					 | 
				
			||||||
import Web.Twain qualified as Twain
 | 
					import Web.Twain qualified as Twain
 | 
				
			||||||
import Web.Twain.Types qualified as Twain
 | 
					import Web.Twain.Types qualified as Twain
 | 
				
			||||||
import Data.Text qualified as T
 | 
					 | 
				
			||||||
import Network.Wai qualified as Wai
 | 
					 | 
				
			||||||
import Control.Monad.IO.Class (liftIO)
 | 
					import Control.Monad.IO.Class (liftIO)
 | 
				
			||||||
import Control.Monad.Catch (catch, displayException, SomeException)
 | 
					import Control.Monad.Catch (catch, displayException, SomeException)
 | 
				
			||||||
import Text.ParserCombinators.ReadP qualified as P
 | 
					import Fedi.Signature.Check
 | 
				
			||||||
import Data.Text.Encoding qualified as T
 | 
					 | 
				
			||||||
import Data.ByteString.Base64 qualified as Base64
 | 
					 | 
				
			||||||
import Data.ByteString.Lazy qualified as BSL
 | 
					 | 
				
			||||||
import Fedi.Crypto
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
-- * Inbox
 | 
					-- * Inbox
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -37,98 +27,3 @@ handleInbox handle = do
 | 
				
			||||||
    Twain.send $
 | 
					    Twain.send $
 | 
				
			||||||
      Twain.status Twain.status500 $
 | 
					      Twain.status Twain.status500 $
 | 
				
			||||||
        Twain.text "Internal Server Error 500"
 | 
					        Twain.text "Internal Server Error 500"
 | 
				
			||||||
 | 
					 | 
				
			||||||
-- | 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
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  -- check
 | 
					 | 
				
			||||||
  liftIO $
 | 
					 | 
				
			||||||
    checkSignature personPkid personPublicKey sigheader digest (BSL.toStrict body)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  -- parse the body and return it
 | 
					 | 
				
			||||||
  parseJson body
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
checkSignature
 | 
					 | 
				
			||||||
  :: MonadThrow m
 | 
					 | 
				
			||||||
  => Url -> ByteString -> SignatureHeader -> ByteString -> ByteString -> m ()
 | 
					 | 
				
			||||||
checkSignature personPkid personPublicKey sigheader digest body = do
 | 
					 | 
				
			||||||
  -- check
 | 
					 | 
				
			||||||
  unless (personPkid == sigheader.keyId) $
 | 
					 | 
				
			||||||
    throw "public key mismatch with signature."
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  pub <- verifyPub personPublicKey sigheader.signature body
 | 
					 | 
				
			||||||
  unless pub $
 | 
					 | 
				
			||||||
    throw "signature verification failed."
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  dig <- verifyDigest personPublicKey digest body
 | 
					 | 
				
			||||||
  unless dig $
 | 
					 | 
				
			||||||
    throw "digest verification failed."
 | 
					 | 
				
			||||||
  -- todo: check date
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
data SignatureHeader
 | 
					 | 
				
			||||||
  = SignatureHeader
 | 
					 | 
				
			||||||
    { -- | Where to get the public key for this actor
 | 
					 | 
				
			||||||
      keyId :: Url
 | 
					 | 
				
			||||||
    , -- | Which headers have been sent
 | 
					 | 
				
			||||||
      headers :: [T.Text]
 | 
					 | 
				
			||||||
    , -- | Contains the signature
 | 
					 | 
				
			||||||
      signature :: ByteString
 | 
					 | 
				
			||||||
    , components :: [(Component, String)]
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  deriving Show
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
data Component
 | 
					 | 
				
			||||||
  = KeyId
 | 
					 | 
				
			||||||
  | Headers
 | 
					 | 
				
			||||||
  | Signature
 | 
					 | 
				
			||||||
  | Other String
 | 
					 | 
				
			||||||
  deriving (Eq, Show)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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 <-
 | 
					 | 
				
			||||||
        ( Base64.decodeBase64Lenient
 | 
					 | 
				
			||||||
        . fromString
 | 
					 | 
				
			||||||
        ) <$> lookup' Signature components
 | 
					 | 
				
			||||||
      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
 | 
					 | 
				
			||||||
        key <- P.munch1 (/= '=')
 | 
					 | 
				
			||||||
        _ <- P.char '='
 | 
					 | 
				
			||||||
        value <- P.between (P.char '\"') (P.char '\"') (P.munch1 (/= '\"'))
 | 
					 | 
				
			||||||
        pure (Other key, value)
 | 
					 | 
				
			||||||
      ]
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										101
									
								
								src/Fedi/Signature/Check.hs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								src/Fedi/Signature/Check.hs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,101 @@
 | 
				
			||||||
 | 
					{-# language RecordWildCards #-}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module Fedi.Signature.Check
 | 
				
			||||||
 | 
					  ( module Fedi.Signature.Types
 | 
				
			||||||
 | 
					  , module Fedi.Signature.Check
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Prelude hiding (error)
 | 
				
			||||||
 | 
					import Fedi.Types
 | 
				
			||||||
 | 
					import Fedi.UserDetails
 | 
				
			||||||
 | 
					import Fedi.Requests
 | 
				
			||||||
 | 
					import Fedi.Routes.Helpers
 | 
				
			||||||
 | 
					import Fedi.Helpers
 | 
				
			||||||
 | 
					import Web.Twain qualified as Twain
 | 
				
			||||||
 | 
					import Data.Text qualified as T
 | 
				
			||||||
 | 
					import Network.Wai qualified as Wai
 | 
				
			||||||
 | 
					import Control.Monad.IO.Class (liftIO)
 | 
				
			||||||
 | 
					import Text.ParserCombinators.ReadP qualified as P
 | 
				
			||||||
 | 
					import Data.Text.Encoding qualified as T
 | 
				
			||||||
 | 
					import Data.ByteString.Lazy qualified as BSL
 | 
				
			||||||
 | 
					import Fedi.Crypto
 | 
				
			||||||
 | 
					import Fedi.Signature.Types
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- * 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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  -- check
 | 
				
			||||||
 | 
					  liftIO $
 | 
				
			||||||
 | 
					    checkSignature personPkid personPublicKey sigheader digest (BSL.toStrict body)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  -- parse the body and return it
 | 
				
			||||||
 | 
					  parseJson body
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					checkSignature
 | 
				
			||||||
 | 
					  :: MonadThrow m
 | 
				
			||||||
 | 
					  => Url -> ByteString -> SignatureHeader -> ByteString -> ByteString -> m ()
 | 
				
			||||||
 | 
					checkSignature personPkid personPublicKey sigheader digest body = do
 | 
				
			||||||
 | 
					  -- check
 | 
				
			||||||
 | 
					  unless (personPkid == sigheader.keyId) $
 | 
				
			||||||
 | 
					    throw "public key mismatch with signature."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  pub <- verifyPub personPublicKey sigheader.signature body
 | 
				
			||||||
 | 
					  unless pub $
 | 
				
			||||||
 | 
					    throw "signature verification failed."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  dig <- verifyDigest personPublicKey digest body
 | 
				
			||||||
 | 
					  unless dig $
 | 
				
			||||||
 | 
					    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 <-
 | 
				
			||||||
 | 
					        ( decodeBase64
 | 
				
			||||||
 | 
					        . fromString
 | 
				
			||||||
 | 
					        ) <$> lookup' Signature components
 | 
				
			||||||
 | 
					      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
 | 
				
			||||||
 | 
					        key <- P.munch1 (/= '=')
 | 
				
			||||||
 | 
					        _ <- P.char '='
 | 
				
			||||||
 | 
					        value <- P.between (P.char '\"') (P.char '\"') (P.munch1 (/= '\"'))
 | 
				
			||||||
 | 
					        pure (Other key, value)
 | 
				
			||||||
 | 
					      ]
 | 
				
			||||||
							
								
								
									
										48
									
								
								src/Fedi/Signature/Sign.hs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/Fedi/Signature/Sign.hs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,48 @@
 | 
				
			||||||
 | 
					{-# language RecordWildCards #-}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module Fedi.Signature.Sign
 | 
				
			||||||
 | 
					  ( module Fedi.Signature.Types
 | 
				
			||||||
 | 
					  , module Fedi.Signature.Sign
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Prelude hiding (error)
 | 
				
			||||||
 | 
					import Fedi.UserDetails
 | 
				
			||||||
 | 
					import Fedi.Helpers
 | 
				
			||||||
 | 
					import Data.ByteString qualified as BS
 | 
				
			||||||
 | 
					import Fedi.Crypto
 | 
				
			||||||
 | 
					import Data.Time qualified as Time
 | 
				
			||||||
 | 
					import Fedi.Signature.Types
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- * Sign
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					signSignature
 | 
				
			||||||
 | 
					  :: UserDetails -> String -> String -> ByteString -> IO HttpSignature
 | 
				
			||||||
 | 
					signSignature details host requestTarget body = do
 | 
				
			||||||
 | 
					  date <- Time.getCurrentTime
 | 
				
			||||||
 | 
					    <&> Time.formatTime Time.defaultTimeLocale Time.rfc822DateFormat
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let
 | 
				
			||||||
 | 
					    digest = encodeBase64 $ fromString $ makeDigest body
 | 
				
			||||||
 | 
					    keyId = actorUrl details <> "#main-key"
 | 
				
			||||||
 | 
					    headers = ["(request-target)", "host", "date", "digest"]
 | 
				
			||||||
 | 
					    components = []
 | 
				
			||||||
 | 
					    signatureString = makeSignatureString host requestTarget date digest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  signed <- sign details signatureString
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let
 | 
				
			||||||
 | 
					    signature = encodeBase64 signed.signedMessage
 | 
				
			||||||
 | 
					    signatureHeader = SignatureHeader{..}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  pure HttpSignature{..}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					makeSignatureString
 | 
				
			||||||
 | 
					  :: String -> String -> String -> ByteString -> ByteString
 | 
				
			||||||
 | 
					makeSignatureString host requestTarget date digest =
 | 
				
			||||||
 | 
					  BS.intercalate "\n"
 | 
				
			||||||
 | 
					    [ "(request-target): " <> fromString requestTarget
 | 
				
			||||||
 | 
					    , "host: " <> fromString host
 | 
				
			||||||
 | 
					    , "date: " <> fromString date
 | 
				
			||||||
 | 
					    , "digest: SHA-256=" <> digest
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
							
								
								
									
										47
									
								
								src/Fedi/Signature/Types.hs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/Fedi/Signature/Types.hs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,47 @@
 | 
				
			||||||
 | 
					{-# language RecordWildCards #-}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module Fedi.Signature.Types where
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Prelude hiding (error)
 | 
				
			||||||
 | 
					import Fedi.UserDetails
 | 
				
			||||||
 | 
					import Data.Text qualified as T
 | 
				
			||||||
 | 
					import Data.Text.Encoding qualified as T
 | 
				
			||||||
 | 
					import Data.ByteString qualified as BS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					data HttpSignature
 | 
				
			||||||
 | 
					  = HttpSignature
 | 
				
			||||||
 | 
					    { signatureHeader :: SignatureHeader
 | 
				
			||||||
 | 
					    , date :: String
 | 
				
			||||||
 | 
					    , host :: String
 | 
				
			||||||
 | 
					    , digest :: ByteString
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  deriving Show
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					toSignature :: SignatureHeader -> ByteString
 | 
				
			||||||
 | 
					toSignature sig =
 | 
				
			||||||
 | 
					  BS.intercalate ","
 | 
				
			||||||
 | 
					    [ "keyId=\"" <> fromString sig.keyId <> "\""
 | 
				
			||||||
 | 
					    , "headers=\"" <> BS.intercalate " " (map T.encodeUtf8 sig.headers) <> "\""
 | 
				
			||||||
 | 
					    , "signature=\"" <> sig.signature <> "\""
 | 
				
			||||||
 | 
					    , "algorithm=\"" <> "rsa-sha256" <> "\""
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					data SignatureHeader
 | 
				
			||||||
 | 
					  = SignatureHeader
 | 
				
			||||||
 | 
					    { -- | Where to get the public key for this actor
 | 
				
			||||||
 | 
					      keyId :: Url
 | 
				
			||||||
 | 
					    , -- | Which headers have been sent
 | 
				
			||||||
 | 
					      headers :: [T.Text]
 | 
				
			||||||
 | 
					    , -- | Contains the signature
 | 
				
			||||||
 | 
					      signature :: ByteString
 | 
				
			||||||
 | 
					    , components :: [(Component, String)]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  deriving Show
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					data Component
 | 
				
			||||||
 | 
					  = KeyId
 | 
				
			||||||
 | 
					  | Headers
 | 
				
			||||||
 | 
					  | Signature
 | 
				
			||||||
 | 
					  | Algorithm
 | 
				
			||||||
 | 
					  | Other String
 | 
				
			||||||
 | 
					  deriving (Eq, Show)
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue