2019-01-21 16:54:57 +01:00
|
|
|
{- This file is part of Vervis.
|
|
|
|
-
|
|
|
|
- Written in 2019 by fr33domlover <fr33domlover@riseup.net>.
|
|
|
|
-
|
|
|
|
- ♡ Copying is an act of love. Please copy, reuse and share.
|
|
|
|
-
|
|
|
|
- The author(s) have dedicated all copyright and related and neighboring
|
|
|
|
- rights to this software to the public domain worldwide. This software is
|
|
|
|
- distributed without any warranty.
|
|
|
|
-
|
|
|
|
- You should have received a copy of the CC0 Public Domain Dedication along
|
|
|
|
- with this software. If not, see
|
|
|
|
- <http://creativecommons.org/publicdomain/zero/1.0/>.
|
|
|
|
-}
|
|
|
|
|
|
|
|
module Web.ActivityPub
|
2019-02-22 00:59:53 +01:00
|
|
|
( -- * Type-safe manipulation tools
|
|
|
|
--
|
|
|
|
-- Types and functions that make handling URIs and JSON-LD contexts less
|
|
|
|
-- error-prone and safer by recording safety checks in the type and
|
|
|
|
-- placing the checks in a single clear place.
|
|
|
|
ActivityPub (..)
|
|
|
|
, Doc (..)
|
|
|
|
|
|
|
|
-- * Actor
|
2019-01-21 16:54:57 +01:00
|
|
|
--
|
|
|
|
-- ActivityPub actor document including a public key, with a 'FromJSON'
|
|
|
|
-- instance for fetching and a 'ToJSON' instance for publishing.
|
2019-02-22 00:59:53 +01:00
|
|
|
, ActorType (..)
|
2019-03-11 00:15:42 +01:00
|
|
|
--, Algorithm (..)
|
2019-02-22 00:59:53 +01:00
|
|
|
, Owner (..)
|
2019-01-21 16:54:57 +01:00
|
|
|
, PublicKey (..)
|
|
|
|
, Actor (..)
|
|
|
|
|
|
|
|
-- * Activity
|
2019-02-12 12:53:24 +01:00
|
|
|
, Note (..)
|
2019-03-14 00:37:58 +01:00
|
|
|
, Accept (..)
|
2019-02-12 12:53:24 +01:00
|
|
|
, Create (..)
|
2019-03-14 00:37:58 +01:00
|
|
|
, Follow (..)
|
|
|
|
, Reject (..)
|
2019-03-14 03:30:36 +01:00
|
|
|
, Audience (..)
|
2019-03-14 00:37:58 +01:00
|
|
|
, SpecificActivity (..)
|
2019-01-21 16:54:57 +01:00
|
|
|
, Activity (..)
|
|
|
|
|
|
|
|
-- * Utilities
|
2019-03-21 20:13:36 +01:00
|
|
|
, publicURI
|
2019-03-23 03:57:34 +01:00
|
|
|
, deliverTo
|
2019-02-07 11:34:33 +01:00
|
|
|
, hActivityPubActor
|
2019-01-21 16:54:57 +01:00
|
|
|
, provideAP
|
|
|
|
, APGetError (..)
|
|
|
|
, httpGetAP
|
2019-03-05 09:26:41 +01:00
|
|
|
, APPostError (..)
|
2019-05-02 01:13:22 +02:00
|
|
|
, hActivityPubForwarder
|
|
|
|
, hForwardingSignature
|
|
|
|
, hForwardedSignature
|
2019-01-21 16:54:57 +01:00
|
|
|
, httpPostAP
|
2019-05-03 23:04:53 +02:00
|
|
|
, httpPostAPBytes
|
2019-02-06 03:48:23 +01:00
|
|
|
, Fetched (..)
|
2019-02-22 00:59:53 +01:00
|
|
|
, fetchAPID
|
2019-04-16 16:27:50 +02:00
|
|
|
, fetchAPID'
|
When verifying HTTP sig with known shared key, verify actor lists the key
Previously, when verifying an HTTP signature and we fetched the key and
discovered it's shared, we'd fetch the actor and make sure it lists the key URI
in the `publicKey` field. But if we already knew the key, had it cached in our
DB, we wouldn't check the actor at all, despite not knowing whether it lists
the key.
With this patch, we now always GET the actor when the key is shared,
determining the actor URI from the `ActivityPub-Actor` request header, and we
verify that the actor lists the key URI. We do that regardless of whether or
not we have the key in the DB, although these two cases and handled in
different parts of the code right now (for a new key, it's in Web.ActivityPub
fetchKey; for a known key, it's in Vervis.Foundation httpVerifySig).
2019-02-18 10:20:13 +01:00
|
|
|
, keyListedByActor
|
2019-02-23 18:17:52 +01:00
|
|
|
, fetchUnknownKey
|
|
|
|
, fetchKnownPersonalKey
|
|
|
|
, fetchKnownSharedKey
|
2019-01-21 16:54:57 +01:00
|
|
|
)
|
|
|
|
where
|
|
|
|
|
|
|
|
import Prelude
|
|
|
|
|
2019-02-04 00:39:56 +01:00
|
|
|
import Control.Applicative ((<|>), optional)
|
|
|
|
import Control.Exception (Exception, displayException, try)
|
2019-02-24 02:21:42 +01:00
|
|
|
import Control.Monad (when, unless, (<=<), join)
|
2019-01-21 16:54:57 +01:00
|
|
|
import Control.Monad.IO.Class
|
2019-02-04 00:39:56 +01:00
|
|
|
import Control.Monad.Trans.Except
|
2019-01-21 16:54:57 +01:00
|
|
|
import Control.Monad.Trans.Writer (Writer)
|
2019-04-25 17:49:15 +02:00
|
|
|
import Crypto.Hash hiding (Context)
|
2019-01-21 16:54:57 +01:00
|
|
|
import Data.Aeson
|
2019-02-22 00:59:53 +01:00
|
|
|
import Data.Aeson.Encoding (pair)
|
|
|
|
import Data.Aeson.Types (Parser, typeMismatch, listEncoding)
|
|
|
|
import Data.Bifunctor
|
2019-02-04 11:07:25 +01:00
|
|
|
import Data.Bitraversable (bitraverse)
|
2019-01-21 16:54:57 +01:00
|
|
|
import Data.ByteString (ByteString)
|
2019-02-17 01:14:05 +01:00
|
|
|
import Data.Foldable (for_)
|
2019-02-24 02:21:42 +01:00
|
|
|
import Data.List.NonEmpty (NonEmpty (..), nonEmpty)
|
2019-02-22 00:59:53 +01:00
|
|
|
import Data.Proxy
|
2019-01-21 16:54:57 +01:00
|
|
|
import Data.PEM
|
2019-02-24 02:21:42 +01:00
|
|
|
import Data.Semigroup (Endo, First (..))
|
2019-01-21 16:54:57 +01:00
|
|
|
import Data.Text (Text)
|
|
|
|
import Data.Text.Encoding (encodeUtf8, decodeUtf8)
|
2019-02-05 05:05:44 +01:00
|
|
|
import Data.Time.Clock (UTCTime)
|
2019-03-23 03:05:30 +01:00
|
|
|
import Data.Traversable
|
2019-03-14 03:30:36 +01:00
|
|
|
import Data.Vector (Vector)
|
2019-02-22 00:59:53 +01:00
|
|
|
import Network.HTTP.Client hiding (Proxy, proxy)
|
2019-01-21 16:54:57 +01:00
|
|
|
import Network.HTTP.Client.Conduit.ActivityPub (httpAPEither)
|
|
|
|
import Network.HTTP.Simple (JSONException)
|
|
|
|
import Network.HTTP.Types.Header (HeaderName, hContentType)
|
|
|
|
import Network.URI
|
|
|
|
import Yesod.Core.Content (ContentType)
|
|
|
|
import Yesod.Core.Handler (ProvidedRep, provideRepType)
|
|
|
|
|
2019-04-28 12:18:50 +02:00
|
|
|
import Network.HTTP.Client.Signature
|
|
|
|
|
2019-04-26 02:25:50 +02:00
|
|
|
import qualified Data.ByteString as B
|
|
|
|
import qualified Data.ByteString.Base64 as B64
|
2019-03-11 00:15:42 +01:00
|
|
|
import qualified Data.ByteString.Char8 as BC
|
2019-05-03 23:04:53 +02:00
|
|
|
import qualified Data.ByteString.Lazy as BL
|
2019-01-21 16:54:57 +01:00
|
|
|
import qualified Data.HashMap.Strict as M (lookup)
|
2019-02-06 03:48:23 +01:00
|
|
|
import qualified Data.Text as T (pack, unpack)
|
2019-03-14 03:30:36 +01:00
|
|
|
import qualified Data.Vector as V
|
2019-03-11 00:15:42 +01:00
|
|
|
import qualified Network.HTTP.Signature as S
|
2019-01-21 16:54:57 +01:00
|
|
|
|
2019-03-11 00:15:42 +01:00
|
|
|
import Crypto.PublicVerifKey
|
2019-02-08 00:08:28 +01:00
|
|
|
import Network.FedURI
|
2019-04-25 17:49:15 +02:00
|
|
|
import Network.HTTP.Digest
|
2019-02-08 00:08:28 +01:00
|
|
|
|
2019-02-03 12:01:36 +01:00
|
|
|
import Data.Aeson.Local
|
2019-01-21 16:54:57 +01:00
|
|
|
|
2019-02-22 00:59:53 +01:00
|
|
|
proxy :: a -> Proxy a
|
|
|
|
proxy _ = Proxy
|
|
|
|
|
2019-01-21 16:54:57 +01:00
|
|
|
as2context :: Text
|
|
|
|
as2context = "https://www.w3.org/ns/activitystreams"
|
|
|
|
|
2019-02-22 00:59:53 +01:00
|
|
|
secContext :: Text
|
|
|
|
secContext = "https://w3id.org/security/v1"
|
|
|
|
|
2019-03-21 20:13:36 +01:00
|
|
|
publicURI :: FedURI
|
|
|
|
publicURI = FedURI "www.w3.org" "/ns/activitystreams" "#Public"
|
|
|
|
|
|
|
|
publicT :: Text
|
|
|
|
publicT = renderFedURI publicURI
|
|
|
|
|
2019-02-22 00:59:53 +01:00
|
|
|
actorContext :: [Text]
|
|
|
|
actorContext = [as2context, secContext]
|
|
|
|
|
|
|
|
data Context = ContextAS2 | ContextPKey | ContextActor deriving Eq
|
|
|
|
|
|
|
|
instance FromJSON Context where
|
|
|
|
parseJSON (String t)
|
|
|
|
| t == as2context = return ContextAS2
|
|
|
|
| t == secContext = return ContextPKey
|
|
|
|
parseJSON (Array v)
|
|
|
|
| V.toList v == map String actorContext = return ContextActor
|
|
|
|
parseJSON _ = fail "Unrecognized @context"
|
|
|
|
|
|
|
|
instance ToJSON Context where
|
|
|
|
toJSON = error "toJSON Context"
|
|
|
|
toEncoding ContextAS2 = toEncoding as2context
|
|
|
|
toEncoding ContextPKey = toEncoding secContext
|
|
|
|
toEncoding ContextActor = toEncoding actorContext
|
|
|
|
|
|
|
|
class ActivityPub a where
|
|
|
|
jsonldContext :: Proxy a -> Context
|
|
|
|
parseObject :: Object -> Parser (Text, a)
|
|
|
|
toSeries :: Text -> a -> Series
|
|
|
|
|
|
|
|
data Doc a = Doc
|
|
|
|
{ docHost :: Text
|
|
|
|
, docValue :: a
|
|
|
|
}
|
|
|
|
|
|
|
|
instance ActivityPub a => FromJSON (Doc a) where
|
|
|
|
parseJSON = withObject "Doc" $ \ o -> do
|
|
|
|
(h, v) <- parseObject o
|
|
|
|
ctx <- o .: "@context"
|
|
|
|
if ctx == jsonldContext (proxy v)
|
|
|
|
then return $ Doc h v
|
|
|
|
else fail "@context doesn't match"
|
|
|
|
|
|
|
|
instance ActivityPub a => ToJSON (Doc a) where
|
|
|
|
toJSON = error "toJSON Doc"
|
|
|
|
toEncoding (Doc h v) =
|
|
|
|
pairs
|
|
|
|
$ "@context" .= jsonldContext (proxy v)
|
|
|
|
<> toSeries h v
|
2019-01-21 16:54:57 +01:00
|
|
|
|
2019-03-20 11:36:00 +01:00
|
|
|
data ActorType = ActorTypePerson | ActorTypeProject | ActorTypeOther Text
|
2019-01-21 16:54:57 +01:00
|
|
|
|
|
|
|
instance FromJSON ActorType where
|
2019-03-20 11:36:00 +01:00
|
|
|
parseJSON = withText "ActorType" $ pure . parse
|
|
|
|
where
|
|
|
|
parse t
|
|
|
|
| t == "Person" = ActorTypePerson
|
|
|
|
| t == frg <> "Project" = ActorTypeProject
|
|
|
|
| otherwise = ActorTypeOther t
|
2019-01-21 16:54:57 +01:00
|
|
|
|
|
|
|
instance ToJSON ActorType where
|
|
|
|
toJSON = error "toJSON ActorType"
|
|
|
|
toEncoding at =
|
|
|
|
toEncoding $ case at of
|
|
|
|
ActorTypePerson -> "Person"
|
2019-03-20 11:36:00 +01:00
|
|
|
ActorTypeProject -> frg <> "Project"
|
2019-01-21 16:54:57 +01:00
|
|
|
ActorTypeOther t -> t
|
|
|
|
|
2019-03-11 00:15:42 +01:00
|
|
|
{-
|
|
|
|
data Algorithm = AlgorithmEd25519 | AlgorithmRsaSha256 | AlgorithmOther Text
|
2019-01-21 16:54:57 +01:00
|
|
|
|
|
|
|
instance FromJSON Algorithm where
|
2019-03-11 00:15:42 +01:00
|
|
|
parseJSON = withText "Algorithm" $ \ t -> pure
|
|
|
|
| t == frg <> "ed25519" = AlgorithmEd25519
|
|
|
|
| t == frg <> "rsa-sha256" = AlgorithmRsaSha256
|
|
|
|
| otherwise = AlgorithmOther t
|
2019-01-21 16:54:57 +01:00
|
|
|
|
|
|
|
instance ToJSON Algorithm where
|
|
|
|
toJSON = error "toJSON Algorithm"
|
|
|
|
toEncoding algo =
|
|
|
|
toEncoding $ case algo of
|
2019-03-11 00:15:42 +01:00
|
|
|
AlgorithmEd25519 -> frg <> "ed25519"
|
|
|
|
AlgorithmRsaSha256 -> frg <> "rsa-sha256"
|
|
|
|
AlgorithmOther t -> t
|
|
|
|
-}
|
2019-01-21 16:54:57 +01:00
|
|
|
|
2019-02-22 00:59:53 +01:00
|
|
|
data Owner = OwnerInstance | OwnerActor LocalURI
|
|
|
|
|
|
|
|
ownerShared :: Owner -> Bool
|
|
|
|
ownerShared OwnerInstance = True
|
|
|
|
ownerShared (OwnerActor _) = False
|
|
|
|
|
2019-01-21 16:54:57 +01:00
|
|
|
data PublicKey = PublicKey
|
2019-03-11 00:15:42 +01:00
|
|
|
{ publicKeyId :: LocalURI
|
|
|
|
, publicKeyExpires :: Maybe UTCTime
|
|
|
|
, publicKeyOwner :: Owner
|
|
|
|
, publicKeyMaterial :: PublicVerifKey
|
|
|
|
--, publicKeyAlgo :: Maybe Algorithm
|
2019-01-21 16:54:57 +01:00
|
|
|
}
|
|
|
|
|
2019-02-22 00:59:53 +01:00
|
|
|
instance ActivityPub PublicKey where
|
|
|
|
jsonldContext _ = ContextPKey
|
|
|
|
parseObject o = do
|
2019-02-04 00:39:56 +01:00
|
|
|
mtyp <- optional $ o .: "@type" <|> o .: "type"
|
2019-02-22 00:59:53 +01:00
|
|
|
for_ mtyp $ \ t ->
|
|
|
|
when (t /= ("Key" :: Text)) $
|
|
|
|
fail "PublicKey @type isn't Key"
|
|
|
|
(host, id_) <- f2l <$> (o .: "@id" <|> o .: "id")
|
2019-03-15 17:33:10 +01:00
|
|
|
shared <- o .:? (frg <> "isShared") .!= False
|
2019-02-22 00:59:53 +01:00
|
|
|
fmap (host,) $
|
|
|
|
PublicKey id_
|
|
|
|
<$> o .:? "expires"
|
|
|
|
<*> (mkOwner shared =<< withHost host o "owner")
|
2019-03-11 00:15:42 +01:00
|
|
|
<*> (either fail return . decodePublicVerifKeyPEM =<<
|
|
|
|
o .: "publicKeyPem"
|
|
|
|
)
|
|
|
|
-- <*> o .:? (frg <> "algorithm")
|
2019-01-21 16:54:57 +01:00
|
|
|
where
|
2019-02-22 00:59:53 +01:00
|
|
|
withHost h o t = do
|
|
|
|
(h', lu) <- f2l <$> o .: t
|
|
|
|
if h == h'
|
|
|
|
then return lu
|
|
|
|
else fail "URI host mismatch"
|
|
|
|
mkOwner True (LocalURI "" "") = return OwnerInstance
|
|
|
|
mkOwner True _ = fail "Shared key but owner isn't instance URI"
|
|
|
|
mkOwner False lu = return $ OwnerActor lu
|
2019-03-11 00:15:42 +01:00
|
|
|
toSeries host (PublicKey id_ mexpires owner mat)
|
2019-02-22 00:59:53 +01:00
|
|
|
= "@id" .= l2f host id_
|
|
|
|
<> "expires" .=? mexpires
|
|
|
|
<> "owner" .= mkOwner host owner
|
2019-03-11 00:15:42 +01:00
|
|
|
<> "publicKeyPem" .= encodePublicVerifKeyPEM mat
|
|
|
|
-- <> (frg <> "algorithm") .=? malgo
|
2019-02-22 00:59:53 +01:00
|
|
|
<> (frg <> "isShared") .= ownerShared owner
|
|
|
|
where
|
|
|
|
mkOwner h OwnerInstance = FedURI h "" ""
|
|
|
|
mkOwner h (OwnerActor lu) = l2f h lu
|
2019-01-21 16:54:57 +01:00
|
|
|
|
2019-03-20 10:31:08 +01:00
|
|
|
parsePublicKeySet :: Value -> Parser (Text, [Either LocalURI PublicKey])
|
2019-02-22 00:59:53 +01:00
|
|
|
parsePublicKeySet v =
|
|
|
|
case v of
|
|
|
|
Array a ->
|
2019-03-20 10:31:08 +01:00
|
|
|
case V.toList a of
|
|
|
|
[] -> fail "No public keys"
|
|
|
|
k : ks -> do
|
2019-02-24 02:21:42 +01:00
|
|
|
(h, e) <- parseKey k
|
|
|
|
es <- traverse (withHost h . parseKey) ks
|
2019-03-20 10:31:08 +01:00
|
|
|
return (h, e : es)
|
|
|
|
_ -> second (: []) <$> parseKey v
|
2019-02-22 00:59:53 +01:00
|
|
|
where
|
|
|
|
parseKey (String t) = second Left . f2l <$> either fail return (parseFedURI t)
|
|
|
|
parseKey (Object o) = second Right <$> parseObject o
|
|
|
|
parseKey v = typeMismatch "PublicKeySet Item" v
|
|
|
|
withHost h a = do
|
|
|
|
(h', v) <- a
|
|
|
|
if h == h'
|
|
|
|
then return v
|
|
|
|
else fail "URI host mismatch"
|
|
|
|
|
2019-03-20 10:31:08 +01:00
|
|
|
encodePublicKeySet :: Text -> [Either LocalURI PublicKey] -> Encoding
|
|
|
|
encodePublicKeySet host es =
|
|
|
|
case es of
|
|
|
|
[e] -> renderKey e
|
|
|
|
_ -> listEncoding renderKey es
|
2019-02-22 00:59:53 +01:00
|
|
|
where
|
|
|
|
renderKey (Left lu) = toEncoding $ l2f host lu
|
|
|
|
renderKey (Right pk) = pairs $ toSeries host pk
|
Support remote actors specifying 2 keys, and DB storage of these keys
It's now possible for activities we be attributed to actors that have more than
one key. We allow up to 2 keys. We also store in the DB. Scaling to support any
number of keys is trivial, but I'm limiting to 2 to avoid potential trouble and
because 2 is the actual number we need.
By having 2 keys, and replacing only one of them in each rotation, we avoid
race conditions. With 1 key, the following can happen:
1. We send an activity to another server
2. We rotate our key
3. The server reaches the activity in its processing queue, tries to verify our
request signature, but fails because it can't fetch the key. It's the old
key and we discarded it already, replaced it with the new one
When we use 2 keys, the previous key remains available and other servers have
time to finish processing our requests signed with that key. We can safely
rotate, without worrying about whether the user sent anything right before the
rotation time.
Caveat: With this feature, we allow OTHER servers to rotate freely. It's safe
because it's optional, but it's just Vervis right now. Once Vervis itself
starts using 2 keys, it will be able to rotate freely without race condition
risk, but probably Mastodon etc. won't accept its signatures because of the use
of 2 keys and because they're server-scope keys.
Maybe I can get these features adopted by the fediverse?
2019-02-04 20:38:50 +01:00
|
|
|
|
2019-01-21 16:54:57 +01:00
|
|
|
data Actor = Actor
|
2019-02-22 00:59:53 +01:00
|
|
|
{ actorId :: LocalURI
|
Support remote actors specifying 2 keys, and DB storage of these keys
It's now possible for activities we be attributed to actors that have more than
one key. We allow up to 2 keys. We also store in the DB. Scaling to support any
number of keys is trivial, but I'm limiting to 2 to avoid potential trouble and
because 2 is the actual number we need.
By having 2 keys, and replacing only one of them in each rotation, we avoid
race conditions. With 1 key, the following can happen:
1. We send an activity to another server
2. We rotate our key
3. The server reaches the activity in its processing queue, tries to verify our
request signature, but fails because it can't fetch the key. It's the old
key and we discarded it already, replaced it with the new one
When we use 2 keys, the previous key remains available and other servers have
time to finish processing our requests signed with that key. We can safely
rotate, without worrying about whether the user sent anything right before the
rotation time.
Caveat: With this feature, we allow OTHER servers to rotate freely. It's safe
because it's optional, but it's just Vervis right now. Once Vervis itself
starts using 2 keys, it will be able to rotate freely without race condition
risk, but probably Mastodon etc. won't accept its signatures because of the use
of 2 keys and because they're server-scope keys.
Maybe I can get these features adopted by the fediverse?
2019-02-04 20:38:50 +01:00
|
|
|
, actorType :: ActorType
|
2019-03-20 13:01:10 +01:00
|
|
|
, actorUsername :: Maybe Text
|
|
|
|
, actorName :: Maybe Text
|
|
|
|
, actorSummary :: Maybe Text
|
2019-02-22 00:59:53 +01:00
|
|
|
, actorInbox :: LocalURI
|
2019-03-20 10:31:08 +01:00
|
|
|
, actorPublicKeys :: [Either LocalURI PublicKey]
|
2019-01-21 16:54:57 +01:00
|
|
|
}
|
|
|
|
|
2019-02-22 00:59:53 +01:00
|
|
|
instance ActivityPub Actor where
|
|
|
|
jsonldContext _ = ContextActor
|
|
|
|
parseObject o = do
|
|
|
|
(host, id_) <- f2l <$> o .: "id"
|
|
|
|
fmap (host,) $
|
|
|
|
Actor id_
|
|
|
|
<$> o .: "type"
|
2019-03-20 13:01:10 +01:00
|
|
|
<*> o .:? "preferredUsername"
|
|
|
|
<*> o .:? "name"
|
|
|
|
<*> o .:? "summary"
|
2019-02-22 00:59:53 +01:00
|
|
|
<*> withHost host (f2l <$> o .: "inbox")
|
|
|
|
<*> withHost host (parsePublicKeySet =<< o .: "publicKey")
|
|
|
|
where
|
|
|
|
withHost h a = do
|
|
|
|
(h', v) <- a
|
|
|
|
if h == h'
|
|
|
|
then return v
|
|
|
|
else fail "URI host mismatch"
|
2019-03-20 13:01:10 +01:00
|
|
|
toSeries host (Actor id_ typ musername mname msummary inbox pkeys)
|
2019-02-22 00:59:53 +01:00
|
|
|
= "id" .= l2f host id_
|
|
|
|
<> "type" .= typ
|
2019-03-20 13:01:10 +01:00
|
|
|
<> "preferredUsername" .=? musername
|
|
|
|
<> "name" .=? mname
|
|
|
|
<> "summary" .=? msummary
|
2019-02-22 00:59:53 +01:00
|
|
|
<> "inbox" .= l2f host inbox
|
|
|
|
<> "publicKey" `pair` encodePublicKeySet host pkeys
|
2019-03-10 07:42:03 +01:00
|
|
|
|
2019-03-23 03:57:34 +01:00
|
|
|
data Audience = Audience
|
2019-04-02 01:40:29 +02:00
|
|
|
{ audienceTo :: [FedURI]
|
|
|
|
, audienceBto :: [FedURI]
|
|
|
|
, audienceCc :: [FedURI]
|
|
|
|
, audienceBcc :: [FedURI]
|
|
|
|
, audienceGeneral :: [FedURI]
|
2019-03-23 03:57:34 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
deliverTo :: FedURI -> Audience
|
|
|
|
deliverTo to = Audience
|
2019-04-02 01:40:29 +02:00
|
|
|
{ audienceTo = [to]
|
|
|
|
, audienceBto = []
|
|
|
|
, audienceCc = []
|
|
|
|
, audienceBcc = []
|
|
|
|
, audienceGeneral = []
|
2019-03-23 03:57:34 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
newtype AdaptAudience = AdaptAudience
|
|
|
|
{ unAdapt :: FedURI
|
|
|
|
}
|
|
|
|
|
|
|
|
instance FromJSON AdaptAudience where
|
2019-05-16 23:22:45 +02:00
|
|
|
parseJSON = fmap AdaptAudience . parseJSON . adapt
|
2019-03-23 03:57:34 +01:00
|
|
|
where
|
|
|
|
adapt v =
|
|
|
|
case v of
|
|
|
|
String t
|
|
|
|
| t == "Public" -> String publicT
|
|
|
|
| t == "as:Public" -> String publicT
|
|
|
|
_ -> v
|
|
|
|
|
|
|
|
parseAudience :: Object -> Parser Audience
|
|
|
|
parseAudience o =
|
|
|
|
Audience
|
2019-04-02 01:40:29 +02:00
|
|
|
<$> o .:& "to"
|
|
|
|
<*> o .:& "bto"
|
|
|
|
<*> o .:& "cc"
|
|
|
|
<*> o .:& "bcc"
|
|
|
|
<*> o .:& "audience"
|
2019-03-23 03:57:34 +01:00
|
|
|
where
|
|
|
|
obj .:& key = do
|
2019-04-02 01:40:29 +02:00
|
|
|
l <- obj .:? key .!= []
|
|
|
|
return $ map unAdapt l
|
2019-03-23 03:57:34 +01:00
|
|
|
|
|
|
|
encodeAudience :: Audience -> Series
|
|
|
|
encodeAudience (Audience to bto cc bcc aud)
|
|
|
|
= "to" .=% to
|
|
|
|
<> "bto" .=% bto
|
|
|
|
<> "cc" .=% cc
|
|
|
|
<> "bcc" .=% bcc
|
|
|
|
<> "audience" .=% aud
|
|
|
|
where
|
|
|
|
t .=% v =
|
2019-04-02 01:40:29 +02:00
|
|
|
if null v
|
2019-03-23 03:57:34 +01:00
|
|
|
then mempty
|
|
|
|
else t .= v
|
|
|
|
|
2019-02-12 12:53:24 +01:00
|
|
|
data Note = Note
|
2019-03-23 03:05:30 +01:00
|
|
|
{ noteId :: Maybe LocalURI
|
2019-03-22 21:46:42 +01:00
|
|
|
, noteAttrib :: LocalURI
|
2019-03-23 03:57:34 +01:00
|
|
|
, noteAudience :: Audience
|
2019-03-21 23:57:15 +01:00
|
|
|
, noteReplyTo :: Maybe FedURI
|
|
|
|
, noteContext :: Maybe FedURI
|
|
|
|
, notePublished :: Maybe UTCTime
|
|
|
|
, noteContent :: Text
|
2019-01-21 16:54:57 +01:00
|
|
|
}
|
|
|
|
|
2019-03-22 21:46:42 +01:00
|
|
|
withHost h a = do
|
|
|
|
(h', v) <- a
|
|
|
|
if h == h'
|
|
|
|
then return v
|
|
|
|
else fail "URI host mismatch"
|
|
|
|
|
2019-03-23 03:05:30 +01:00
|
|
|
withHostM h a = do
|
|
|
|
mp <- a
|
|
|
|
for mp $ \ (h', v) ->
|
|
|
|
if h == h'
|
|
|
|
then return v
|
|
|
|
else fail "URI host mismatch"
|
|
|
|
|
2019-03-22 21:46:42 +01:00
|
|
|
instance ActivityPub Note where
|
|
|
|
jsonldContext _ = ContextAS2
|
|
|
|
parseObject o = do
|
|
|
|
typ <- o .: "type"
|
|
|
|
unless (typ == ("Note" :: Text)) $ fail "type isn't Note"
|
2019-03-23 03:05:30 +01:00
|
|
|
(h, attrib) <- f2l <$> o .: "attributedTo"
|
2019-03-22 21:46:42 +01:00
|
|
|
fmap (h,) $
|
2019-03-23 03:05:30 +01:00
|
|
|
Note
|
|
|
|
<$> withHostM h (fmap f2l <$> o .:? "id")
|
|
|
|
<*> pure attrib
|
2019-03-23 03:57:34 +01:00
|
|
|
<*> parseAudience o
|
2019-03-22 21:46:42 +01:00
|
|
|
<*> o .:? "inReplyTo"
|
|
|
|
<*> o .:? "context"
|
|
|
|
<*> o .:? "published"
|
|
|
|
<*> o .: "content"
|
2019-03-23 03:57:34 +01:00
|
|
|
toSeries host (Note mid attrib aud mreply mcontext mpublished content)
|
2019-03-22 21:46:42 +01:00
|
|
|
= "type" .= ("Note" :: Text)
|
2019-03-23 03:05:30 +01:00
|
|
|
<> "id" .=? (l2f host <$> mid)
|
2019-03-22 21:46:42 +01:00
|
|
|
<> "attributedTo" .= l2f host attrib
|
2019-03-23 03:57:34 +01:00
|
|
|
<> encodeAudience aud
|
2019-03-22 21:46:42 +01:00
|
|
|
<> "inReplyTo" .=? mreply
|
|
|
|
<> "context" .=? mcontext
|
|
|
|
<> "published" .=? mpublished
|
|
|
|
<> "content" .= content
|
|
|
|
|
|
|
|
{-
|
2019-03-14 03:30:36 +01:00
|
|
|
parseNote :: Value -> Parser (Text, (Note, LocalURI))
|
2019-03-10 07:42:03 +01:00
|
|
|
parseNote = withObject "Note" $ \ o -> do
|
|
|
|
typ <- o .: "type"
|
|
|
|
unless (typ == ("Note" :: Text)) $ fail "type isn't Note"
|
|
|
|
(h, id_) <- f2l <$> o .: "id"
|
|
|
|
fmap (h,) $
|
2019-03-14 03:30:36 +01:00
|
|
|
(,) <$> (Note id_
|
2019-03-10 07:42:03 +01:00
|
|
|
<$> o .:? "inReplyTo"
|
2019-03-21 23:57:15 +01:00
|
|
|
<*> o .:? "context"
|
|
|
|
<*> o .:? "published"
|
2019-03-10 07:42:03 +01:00
|
|
|
<*> o .: "content"
|
2019-03-14 03:30:36 +01:00
|
|
|
)
|
|
|
|
<*> withHost h (f2l <$> o .: "attributedTo")
|
2019-03-10 07:42:03 +01:00
|
|
|
where
|
|
|
|
withHost h a = do
|
|
|
|
(h', v) <- a
|
|
|
|
if h == h'
|
|
|
|
then return v
|
|
|
|
else fail "URI host mismatch"
|
|
|
|
|
2019-03-14 03:30:36 +01:00
|
|
|
encodeNote :: Text -> Note -> LocalURI -> Encoding
|
2019-03-21 23:57:15 +01:00
|
|
|
encodeNote host (Note id_ mreply mcontext mpublished content) attrib =
|
2019-03-10 07:42:03 +01:00
|
|
|
pairs
|
|
|
|
$ "type" .= ("Note" :: Text)
|
|
|
|
<> "id" .= l2f host id_
|
|
|
|
<> "attributedTo" .= l2f host attrib
|
|
|
|
<> "inReplyTo" .=? mreply
|
2019-03-21 23:57:15 +01:00
|
|
|
<> "context" .=? mcontext
|
|
|
|
<> "published" .=? mpublished
|
2019-03-10 07:42:03 +01:00
|
|
|
<> "content" .= content
|
2019-03-22 21:46:42 +01:00
|
|
|
-}
|
2019-02-12 12:53:24 +01:00
|
|
|
|
2019-03-14 00:37:58 +01:00
|
|
|
data Accept = Accept
|
|
|
|
{ acceptObject :: FedURI
|
|
|
|
}
|
|
|
|
|
|
|
|
parseAccept :: Object -> Parser Accept
|
|
|
|
parseAccept o = Accept <$> o .: "object"
|
|
|
|
|
|
|
|
encodeAccept :: Accept -> Series
|
|
|
|
encodeAccept (Accept obj) = "object" .= obj
|
|
|
|
|
2019-02-12 12:53:24 +01:00
|
|
|
data Create = Create
|
2019-03-14 03:30:36 +01:00
|
|
|
{ createObject :: Note
|
2019-02-12 12:53:24 +01:00
|
|
|
}
|
|
|
|
|
2019-03-14 00:37:58 +01:00
|
|
|
parseCreate :: Object -> Text -> LocalURI -> Parser Create
|
|
|
|
parseCreate o h luActor = do
|
2019-03-22 21:46:42 +01:00
|
|
|
note <- withHost h $ parseObject =<< o .: "object"
|
|
|
|
unless (luActor == noteAttrib note) $ fail "Create actor != Note attrib"
|
2019-03-14 03:30:36 +01:00
|
|
|
return $ Create note
|
2019-03-10 07:42:03 +01:00
|
|
|
where
|
|
|
|
withHost h a = do
|
|
|
|
(h', v) <- a
|
|
|
|
if h == h'
|
|
|
|
then return v
|
|
|
|
else fail "URI host mismatch"
|
|
|
|
|
2019-03-14 00:37:58 +01:00
|
|
|
encodeCreate :: Text -> LocalURI -> Create -> Series
|
2019-03-14 03:30:36 +01:00
|
|
|
encodeCreate host actor (Create obj) =
|
2019-03-22 21:46:42 +01:00
|
|
|
"object" `pair` pairs (toSeries host obj)
|
2019-02-12 12:53:24 +01:00
|
|
|
|
2019-03-14 00:37:58 +01:00
|
|
|
data Follow = Follow
|
|
|
|
{ followObject :: FedURI
|
|
|
|
, followHide :: Bool
|
|
|
|
}
|
|
|
|
|
|
|
|
parseFollow :: Object -> Parser Follow
|
|
|
|
parseFollow o =
|
|
|
|
Follow
|
|
|
|
<$> o .: "object"
|
|
|
|
<*> o .: (frg <> "hide")
|
|
|
|
|
|
|
|
encodeFollow :: Follow -> Series
|
|
|
|
encodeFollow (Follow obj hide)
|
|
|
|
= "object" .= obj
|
|
|
|
<> (frg <> "hide") .= hide
|
|
|
|
|
|
|
|
data Reject = Reject
|
|
|
|
{ rejectObject :: FedURI
|
|
|
|
}
|
|
|
|
|
|
|
|
parseReject :: Object -> Parser Reject
|
|
|
|
parseReject o = Reject <$> o .: "object"
|
|
|
|
|
|
|
|
encodeReject :: Reject -> Series
|
|
|
|
encodeReject (Reject obj) = "object" .= obj
|
|
|
|
|
|
|
|
data SpecificActivity
|
|
|
|
= AcceptActivity Accept
|
|
|
|
| CreateActivity Create
|
|
|
|
| FollowActivity Follow
|
|
|
|
| RejectActivity Reject
|
|
|
|
|
|
|
|
data Activity = Activity
|
|
|
|
{ activityId :: LocalURI
|
|
|
|
, activityActor :: LocalURI
|
2019-03-14 03:30:36 +01:00
|
|
|
, activityAudience :: Audience
|
2019-03-14 00:37:58 +01:00
|
|
|
, activitySpecific :: SpecificActivity
|
|
|
|
}
|
2019-02-12 12:53:24 +01:00
|
|
|
|
2019-03-10 07:42:03 +01:00
|
|
|
instance ActivityPub Activity where
|
|
|
|
jsonldContext _ = ContextAS2
|
|
|
|
parseObject o = do
|
2019-03-14 00:37:58 +01:00
|
|
|
(h, id_) <- f2l <$> o .: "id"
|
|
|
|
actor <- withHost h $ f2l <$> o .: "actor"
|
2019-03-14 03:30:36 +01:00
|
|
|
fmap (h,) $
|
|
|
|
Activity id_ actor
|
|
|
|
<$> parseAudience o
|
|
|
|
<*> do
|
|
|
|
typ <- o .: "type"
|
|
|
|
case typ of
|
|
|
|
"Accept" -> AcceptActivity <$> parseAccept o
|
|
|
|
"Create" -> CreateActivity <$> parseCreate o h actor
|
|
|
|
"Follow" -> FollowActivity <$> parseFollow o
|
|
|
|
"Reject" -> RejectActivity <$> parseReject o
|
|
|
|
_ ->
|
|
|
|
fail $
|
|
|
|
"Unrecognized activity type: " ++ T.unpack typ
|
2019-03-14 00:37:58 +01:00
|
|
|
where
|
|
|
|
withHost h a = do
|
|
|
|
(h', v) <- a
|
|
|
|
if h == h'
|
|
|
|
then return v
|
|
|
|
else fail "URI host mismatch"
|
2019-03-14 03:30:36 +01:00
|
|
|
toSeries host (Activity id_ actor audience specific)
|
2019-03-14 00:37:58 +01:00
|
|
|
= "type" .= activityType specific
|
|
|
|
<> "id" .= l2f host id_
|
|
|
|
<> "actor" .= l2f host actor
|
2019-03-14 03:30:36 +01:00
|
|
|
<> encodeAudience audience
|
2019-03-14 00:37:58 +01:00
|
|
|
<> encodeSpecific host actor specific
|
|
|
|
where
|
|
|
|
activityType :: SpecificActivity -> Text
|
|
|
|
activityType (AcceptActivity _) = "Accept"
|
|
|
|
activityType (CreateActivity _) = "Create"
|
|
|
|
activityType (FollowActivity _) = "Follow"
|
|
|
|
activityType (RejectActivity _) = "Reject"
|
|
|
|
encodeSpecific _ _ (AcceptActivity a) = encodeAccept a
|
|
|
|
encodeSpecific h u (CreateActivity a) = encodeCreate h u a
|
|
|
|
encodeSpecific _ _ (FollowActivity a) = encodeFollow a
|
|
|
|
encodeSpecific _ _ (RejectActivity a) = encodeReject a
|
2019-01-21 16:54:57 +01:00
|
|
|
|
|
|
|
typeActivityStreams2 :: ContentType
|
|
|
|
typeActivityStreams2 = "application/activity+json"
|
|
|
|
|
|
|
|
typeActivityStreams2LD :: ContentType
|
|
|
|
typeActivityStreams2LD =
|
|
|
|
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
|
|
|
|
|
2019-02-07 11:34:33 +01:00
|
|
|
hActivityPubActor :: HeaderName
|
|
|
|
hActivityPubActor = "ActivityPub-Actor"
|
|
|
|
|
2019-03-20 13:01:10 +01:00
|
|
|
provideAP :: (Monad m, ToJSON a) => m a -> Writer (Endo [ProvidedRep m]) ()
|
|
|
|
provideAP mk =
|
|
|
|
-- let enc = toEncoding v
|
2019-01-21 16:54:57 +01:00
|
|
|
-- provideRepType typeActivityStreams2 $ return enc
|
2019-03-20 13:01:10 +01:00
|
|
|
provideRepType typeActivityStreams2LD $ toEncoding <$> mk
|
2019-01-21 16:54:57 +01:00
|
|
|
|
|
|
|
data APGetError
|
|
|
|
= APGetErrorHTTP HttpException
|
|
|
|
| APGetErrorJSON JSONException
|
2019-01-21 23:24:09 +01:00
|
|
|
| APGetErrorContentType Text
|
2019-01-21 16:54:57 +01:00
|
|
|
deriving Show
|
|
|
|
|
|
|
|
instance Exception APGetError
|
|
|
|
|
|
|
|
-- | Perform an HTTP GET request to fetch an ActivityPub object.
|
|
|
|
--
|
|
|
|
-- * Verify the URI scheme is _https:_ and authority part is present
|
|
|
|
-- * Set _Accept_ request header
|
|
|
|
-- * Perform the GET request
|
|
|
|
-- * Verify the _Content-Type_ response header
|
|
|
|
-- * Parse the JSON response body
|
|
|
|
httpGetAP
|
|
|
|
:: (MonadIO m, FromJSON a)
|
|
|
|
=> Manager
|
2019-02-08 00:08:28 +01:00
|
|
|
-> FedURI
|
2019-01-21 16:54:57 +01:00
|
|
|
-> m (Either APGetError (Response a))
|
|
|
|
httpGetAP manager uri =
|
2019-02-08 00:08:28 +01:00
|
|
|
liftIO $
|
|
|
|
mkResult <$> try (httpAPEither manager =<< requestFromURI (toURI uri))
|
2019-01-21 16:54:57 +01:00
|
|
|
where
|
|
|
|
lookup' x = map snd . filter ((== x) . fst)
|
|
|
|
mkResult (Left e) = Left $ APGetErrorHTTP e
|
|
|
|
mkResult (Right r) =
|
|
|
|
case lookup' hContentType $ responseHeaders r of
|
|
|
|
[] -> Left $ APGetErrorContentType "No Content-Type"
|
|
|
|
[b] -> if b == typeActivityStreams2LD || b == typeActivityStreams2
|
|
|
|
then case responseBody r of
|
|
|
|
Left e -> Left $ APGetErrorJSON e
|
|
|
|
Right v -> Right $ v <$ r
|
2019-01-21 23:24:09 +01:00
|
|
|
else Left $ APGetErrorContentType $ "Non-AP Content-Type: " <> decodeUtf8 b
|
2019-01-21 16:54:57 +01:00
|
|
|
_ -> Left $ APGetErrorContentType "Multiple Content-Type"
|
|
|
|
|
2019-03-05 09:26:41 +01:00
|
|
|
data APPostError
|
2019-03-11 00:15:42 +01:00
|
|
|
= APPostErrorSig S.HttpSigGenError
|
2019-03-05 09:26:41 +01:00
|
|
|
| APPostErrorHTTP HttpException
|
|
|
|
deriving Show
|
|
|
|
|
|
|
|
instance Exception APPostError
|
|
|
|
|
2019-04-28 12:18:50 +02:00
|
|
|
hActivityPubForwarder :: HeaderName
|
|
|
|
hActivityPubForwarder = "ActivityPub-Forwarder"
|
2019-04-26 02:25:50 +02:00
|
|
|
|
2019-04-28 12:18:50 +02:00
|
|
|
hForwardingSignature :: HeaderName
|
|
|
|
hForwardingSignature = "Forwarding-Signature"
|
2019-04-26 02:25:50 +02:00
|
|
|
|
2019-04-28 12:18:50 +02:00
|
|
|
hForwardedSignature :: HeaderName
|
|
|
|
hForwardedSignature = "Forwarded-Signature"
|
2019-04-26 02:25:50 +02:00
|
|
|
|
2019-01-21 16:54:57 +01:00
|
|
|
-- | Perform an HTTP POST request to submit an ActivityPub object.
|
|
|
|
--
|
|
|
|
-- * Verify the URI scheme is _https:_ and authority part is present
|
|
|
|
-- * Set _Content-Type_ request header
|
2019-02-07 11:34:33 +01:00
|
|
|
-- * Set _ActivityPub-Actor_ request header
|
2019-04-25 17:49:15 +02:00
|
|
|
-- * Set _Digest_ request header using SHA-256 hash
|
2019-04-28 12:18:50 +02:00
|
|
|
-- * If recipient is given, set _ActivityPub-Forwarder_ header and compute
|
|
|
|
-- _Forwarding-Signature_ header
|
|
|
|
-- * If forwarded signature is given, set set _ActivityPub-Forwarder_ and
|
|
|
|
-- _Forwarded-Signature_ headers
|
2019-01-21 16:54:57 +01:00
|
|
|
-- * Compute HTTP signature and add _Signature_ request header
|
|
|
|
-- * Perform the POST request
|
|
|
|
-- * Verify the response status is 2xx
|
|
|
|
httpPostAP
|
|
|
|
:: (MonadIO m, ToJSON a)
|
|
|
|
=> Manager
|
2019-02-08 00:08:28 +01:00
|
|
|
-> FedURI
|
2019-01-21 16:54:57 +01:00
|
|
|
-> NonEmpty HeaderName
|
2019-04-26 02:25:50 +02:00
|
|
|
-> S.KeyId
|
|
|
|
-> (ByteString -> S.Signature)
|
2019-02-07 11:34:33 +01:00
|
|
|
-> Text
|
2019-04-28 12:18:50 +02:00
|
|
|
-> Maybe (Either FedURI ByteString)
|
2019-01-21 16:54:57 +01:00
|
|
|
-> a
|
2019-03-05 09:26:41 +01:00
|
|
|
-> m (Either APPostError (Response ()))
|
2019-04-28 12:18:50 +02:00
|
|
|
httpPostAP manager uri headers keyid sign uSender mfwd value =
|
2019-05-03 23:04:53 +02:00
|
|
|
httpPostAPBytes manager uri headers keyid sign uSender mfwd $ encode value
|
|
|
|
|
|
|
|
-- | Like 'httpPostAP', except it takes the object as a raw lazy
|
|
|
|
-- 'BL.ByteString'. It's your responsibility to make sure it's valid JSON.
|
|
|
|
httpPostAPBytes
|
|
|
|
:: MonadIO m
|
|
|
|
=> Manager
|
|
|
|
-> FedURI
|
|
|
|
-> NonEmpty HeaderName
|
|
|
|
-> S.KeyId
|
|
|
|
-> (ByteString -> S.Signature)
|
|
|
|
-> Text
|
|
|
|
-> Maybe (Either FedURI ByteString)
|
|
|
|
-> BL.ByteString
|
|
|
|
-> m (Either APPostError (Response ()))
|
|
|
|
httpPostAPBytes manager uri headers keyid sign uSender mfwd body =
|
2019-04-28 12:18:50 +02:00
|
|
|
liftIO $ runExceptT $ do
|
|
|
|
req <- requestFromURI $ toURI uri
|
2019-05-03 23:04:53 +02:00
|
|
|
let digest = formatHttpBodyDigest SHA256 "SHA-256" $ hashlazy body
|
2019-04-28 12:18:50 +02:00
|
|
|
req' =
|
|
|
|
setRequestCheckStatus $
|
|
|
|
consHeader hContentType typeActivityStreams2LD $
|
|
|
|
consHeader hActivityPubActor (encodeUtf8 uSender) $
|
|
|
|
consHeader hDigest digest $
|
|
|
|
req { method = "POST"
|
|
|
|
, requestBody = RequestBodyLBS body
|
|
|
|
}
|
|
|
|
req'' <- tryExceptT APPostErrorSig $ signRequest headers Nothing keyid sign Nothing req'
|
|
|
|
req''' <-
|
|
|
|
case mfwd of
|
|
|
|
Nothing -> return req''
|
|
|
|
Just (Left uRecip) ->
|
|
|
|
tryExceptT APPostErrorSig $
|
|
|
|
signRequestInto hForwardingSignature (hDigest :| [hActivityPubForwarder]) Nothing keyid sign Nothing $ consHeader hActivityPubForwarder (encodeUtf8 $ renderFedURI uRecip) req''
|
|
|
|
Just (Right sig) ->
|
|
|
|
return $
|
|
|
|
consHeader hForwardedSignature sig $
|
|
|
|
consHeader hActivityPubForwarder (encodeUtf8 uSender)
|
|
|
|
req''
|
|
|
|
tryExceptT APPostErrorHTTP $ httpNoBody req''' manager
|
2019-01-21 16:54:57 +01:00
|
|
|
where
|
|
|
|
consHeader n b r = r { requestHeaders = (n, b) : requestHeaders r }
|
2019-04-28 12:18:50 +02:00
|
|
|
tryExceptT adapt action = ExceptT $ first adapt <$> try action
|
2019-02-04 00:39:56 +01:00
|
|
|
|
2019-02-06 03:48:23 +01:00
|
|
|
-- | Result of GETing the keyId URI and processing the JSON document.
|
|
|
|
data Fetched = Fetched
|
2019-03-11 00:15:42 +01:00
|
|
|
{ fetchedPublicKey :: PublicVerifKey
|
|
|
|
-- ^ The Ed25519 or RSA public key corresponding to the URI we requested.
|
2019-02-06 03:48:23 +01:00
|
|
|
, fetchedKeyExpires :: Maybe UTCTime
|
|
|
|
-- ^ Optional expiration time declared for the key we received.
|
2019-02-22 00:59:53 +01:00
|
|
|
, fetchedActorId :: LocalURI
|
2019-02-06 03:48:23 +01:00
|
|
|
-- ^ The @id URI of the actor for whom the key's signature applies.
|
2019-02-22 00:59:53 +01:00
|
|
|
, fetchedActorInbox :: LocalURI
|
2019-02-15 00:27:40 +01:00
|
|
|
-- ^ The inbox URI of the actor for whom the key's signature applies.
|
2019-02-06 03:48:23 +01:00
|
|
|
, fetchedKeyShared :: Bool
|
|
|
|
-- ^ Whether the key we received is shared. A shared key can sign
|
|
|
|
-- requests for any actor on the same instance, while a personal key is
|
|
|
|
-- only for one actor. Knowing whether the key is shared will allow us
|
|
|
|
-- when receiving more requests, whether to accept signatures made on
|
|
|
|
-- different actors, or allow only a single permanent actor for the key
|
|
|
|
-- we received.
|
|
|
|
}
|
|
|
|
|
2019-04-16 16:27:50 +02:00
|
|
|
fetchAP' :: (MonadIO m, FromJSON a) => Manager -> FedURI -> ExceptT APGetError m a
|
|
|
|
fetchAP' m u = ExceptT $ second responseBody <$> httpGetAP m u
|
|
|
|
|
2019-02-22 00:59:53 +01:00
|
|
|
fetchAP :: (MonadIO m, FromJSON a) => Manager -> FedURI -> ExceptT String m a
|
2019-04-16 16:27:50 +02:00
|
|
|
fetchAP m u = withExceptT displayException $ fetchAP' m u
|
2019-02-22 00:59:53 +01:00
|
|
|
|
2019-02-23 18:17:52 +01:00
|
|
|
{-
|
2019-02-22 00:59:53 +01:00
|
|
|
fetchAPH :: (MonadIO m, ActivityPub a) => Manager -> Text -> LocalURI -> ExceptT String m a
|
|
|
|
fetchAPH m h lu = do
|
|
|
|
Doc h' v <- fetchAP m $ l2f h lu
|
|
|
|
if h == h'
|
|
|
|
then return v
|
|
|
|
else throwE "Object @id URI's host doesn't match the URI we fetched"
|
2019-02-23 18:17:52 +01:00
|
|
|
-}
|
2019-02-22 00:59:53 +01:00
|
|
|
|
2019-04-16 16:27:50 +02:00
|
|
|
fetchAPID' :: (MonadIO m, ActivityPub a) => Manager -> (a -> LocalURI) -> Text -> LocalURI -> m (Either (Maybe APGetError) a)
|
|
|
|
fetchAPID' m getId h lu = runExceptT $ do
|
|
|
|
Doc h' v <- withExceptT Just $ fetchAP' m $ l2f h lu
|
2019-02-22 00:59:53 +01:00
|
|
|
if h == h' && getId v == lu
|
|
|
|
then return v
|
2019-04-16 16:27:50 +02:00
|
|
|
else throwE Nothing
|
|
|
|
|
|
|
|
fetchAPID :: (MonadIO m, ActivityPub a) => Manager -> (a -> LocalURI) -> Text -> LocalURI -> m (Either String a)
|
|
|
|
fetchAPID m getId h lu = first showError <$> fetchAPID' m getId h lu
|
|
|
|
where
|
|
|
|
showError Nothing = "Object @id doesn't match the URI we fetched"
|
|
|
|
showError (Just e) = displayException e
|
2019-02-22 00:59:53 +01:00
|
|
|
|
2019-04-16 18:10:17 +02:00
|
|
|
data FetchAPError
|
|
|
|
= FetchAPErrorGet APGetError
|
|
|
|
-- Object @id doesn't match the URI we fetched
|
|
|
|
| FetchAPErrorIdMismatch
|
|
|
|
-- Object @id URI's host doesn't match the URI we fetched
|
|
|
|
| FetchAPErrorHostMismatch
|
|
|
|
deriving Show
|
|
|
|
|
|
|
|
fetchAPIDOrH'
|
2019-02-22 00:59:53 +01:00
|
|
|
:: (MonadIO m, ActivityPub a, ActivityPub b)
|
|
|
|
=> Manager
|
|
|
|
-> (a -> LocalURI)
|
|
|
|
-> Text
|
|
|
|
-> LocalURI
|
2019-04-16 18:10:17 +02:00
|
|
|
-> ExceptT FetchAPError m (Either a b)
|
|
|
|
fetchAPIDOrH' m getId h lu = do
|
|
|
|
e <- withExceptT FetchAPErrorGet $ fetchAP' m $ l2f h lu
|
2019-02-22 00:59:53 +01:00
|
|
|
case e of
|
|
|
|
Left' (Doc h' x) ->
|
|
|
|
if h == h' && getId x == lu
|
|
|
|
then return $ Left x
|
2019-04-16 18:10:17 +02:00
|
|
|
else throwE FetchAPErrorIdMismatch
|
2019-02-22 00:59:53 +01:00
|
|
|
Right' (Doc h' y) ->
|
|
|
|
if h == h'
|
|
|
|
then return $ Right y
|
2019-04-16 18:10:17 +02:00
|
|
|
else throwE FetchAPErrorHostMismatch
|
|
|
|
|
|
|
|
fetchAPIDOrH
|
|
|
|
:: (MonadIO m, ActivityPub a, ActivityPub b)
|
|
|
|
=> Manager
|
|
|
|
-> (a -> LocalURI)
|
|
|
|
-> Text
|
|
|
|
-> LocalURI
|
|
|
|
-> ExceptT String m (Either a b)
|
|
|
|
fetchAPIDOrH m getId h lu = withExceptT show $ fetchAPIDOrH' m getId h lu
|
2019-02-22 00:59:53 +01:00
|
|
|
|
When verifying HTTP sig with known shared key, verify actor lists the key
Previously, when verifying an HTTP signature and we fetched the key and
discovered it's shared, we'd fetch the actor and make sure it lists the key URI
in the `publicKey` field. But if we already knew the key, had it cached in our
DB, we wouldn't check the actor at all, despite not knowing whether it lists
the key.
With this patch, we now always GET the actor when the key is shared,
determining the actor URI from the `ActivityPub-Actor` request header, and we
verify that the actor lists the key URI. We do that regardless of whether or
not we have the key in the DB, although these two cases and handled in
different parts of the code right now (for a new key, it's in Web.ActivityPub
fetchKey; for a known key, it's in Vervis.Foundation httpVerifySig).
2019-02-18 10:20:13 +01:00
|
|
|
-- | Fetches the given actor and checks whether it lists the given key (as a
|
|
|
|
-- URI, not as an embedded object). If it does, returns 'Right' the fetched
|
|
|
|
-- actor. Otherwise, or if an error occurs during fetching, returns 'Left' an
|
|
|
|
-- error message.
|
2019-02-22 00:59:53 +01:00
|
|
|
keyListedByActor :: MonadIO m => Manager -> Text -> LocalURI -> LocalURI -> m (Either String Actor)
|
|
|
|
keyListedByActor manager host luKey luActor = runExceptT $ do
|
|
|
|
actor <- ExceptT $ fetchAPID manager actorId host luActor
|
|
|
|
if keyUriListed luKey actor
|
When verifying HTTP sig with known shared key, verify actor lists the key
Previously, when verifying an HTTP signature and we fetched the key and
discovered it's shared, we'd fetch the actor and make sure it lists the key URI
in the `publicKey` field. But if we already knew the key, had it cached in our
DB, we wouldn't check the actor at all, despite not knowing whether it lists
the key.
With this patch, we now always GET the actor when the key is shared,
determining the actor URI from the `ActivityPub-Actor` request header, and we
verify that the actor lists the key URI. We do that regardless of whether or
not we have the key in the DB, although these two cases and handled in
different parts of the code right now (for a new key, it's in Web.ActivityPub
fetchKey; for a known key, it's in Vervis.Foundation httpVerifySig).
2019-02-18 10:20:13 +01:00
|
|
|
then return actor
|
|
|
|
else throwE "Actor publicKey has no URI matching pkey @id"
|
|
|
|
where
|
|
|
|
keyUriListed uk a =
|
2019-02-24 02:21:42 +01:00
|
|
|
let match (Left uri) = uri == uk
|
When verifying HTTP sig with known shared key, verify actor lists the key
Previously, when verifying an HTTP signature and we fetched the key and
discovered it's shared, we'd fetch the actor and make sure it lists the key URI
in the `publicKey` field. But if we already knew the key, had it cached in our
DB, we wouldn't check the actor at all, despite not knowing whether it lists
the key.
With this patch, we now always GET the actor when the key is shared,
determining the actor URI from the `ActivityPub-Actor` request header, and we
verify that the actor lists the key URI. We do that regardless of whether or
not we have the key in the DB, although these two cases and handled in
different parts of the code right now (for a new key, it's in Web.ActivityPub
fetchKey; for a known key, it's in Vervis.Foundation httpVerifySig).
2019-02-18 10:20:13 +01:00
|
|
|
match (Right _) = False
|
2019-02-24 02:21:42 +01:00
|
|
|
in any match $ actorPublicKeys a
|
When verifying HTTP sig with known shared key, verify actor lists the key
Previously, when verifying an HTTP signature and we fetched the key and
discovered it's shared, we'd fetch the actor and make sure it lists the key URI
in the `publicKey` field. But if we already knew the key, had it cached in our
DB, we wouldn't check the actor at all, despite not knowing whether it lists
the key.
With this patch, we now always GET the actor when the key is shared,
determining the actor URI from the `ActivityPub-Actor` request header, and we
verify that the actor lists the key URI. We do that regardless of whether or
not we have the key in the DB, although these two cases and handled in
different parts of the code right now (for a new key, it's in Web.ActivityPub
fetchKey; for a known key, it's in Vervis.Foundation httpVerifySig).
2019-02-18 10:20:13 +01:00
|
|
|
|
2019-02-24 02:21:42 +01:00
|
|
|
matchKeyObj :: (Foldable f, Monad m) => LocalURI -> f (Either LocalURI PublicKey) -> ExceptT String m PublicKey
|
|
|
|
matchKeyObj luKey es =
|
|
|
|
case find' (match luKey) es of
|
|
|
|
Nothing -> throwE "keyId resolved to actor which doesn't have a key object with that ID"
|
|
|
|
Just pk -> return pk
|
2019-02-23 18:17:52 +01:00
|
|
|
where
|
2019-02-24 02:21:42 +01:00
|
|
|
find' :: Foldable f => (a -> Maybe b) -> f a -> Maybe b
|
|
|
|
find' p = join . fmap getFirst . foldMap (Just . First . p)
|
2019-02-23 18:17:52 +01:00
|
|
|
match _ (Left _) = Nothing
|
|
|
|
match luk (Right pk) =
|
|
|
|
if publicKeyId pk == luk
|
|
|
|
then Just pk
|
|
|
|
else Nothing
|
|
|
|
|
2019-03-11 00:15:42 +01:00
|
|
|
verifyAlgo :: Maybe S.Algorithm -> PublicVerifKey -> Either String ()
|
|
|
|
verifyAlgo Nothing _ = Right ()
|
|
|
|
verifyAlgo (Just a) k =
|
|
|
|
case a of
|
|
|
|
S.AlgorithmEd25519 ->
|
|
|
|
case k of
|
|
|
|
PublicVerifKeyEd25519 _ -> Right ()
|
|
|
|
PublicVerifKeyRSA _ ->
|
|
|
|
Left "Algo mismatch, algo is Ed25519 but actual key is RSA"
|
|
|
|
S.AlgorithmRsaSha256 ->
|
|
|
|
case k of
|
|
|
|
PublicVerifKeyEd25519 _ ->
|
|
|
|
Left
|
|
|
|
"Algo mismatch, algo is RSA-SHA256 but actual key is \
|
|
|
|
\Ed25519"
|
|
|
|
PublicVerifKeyRSA _ -> Right ()
|
|
|
|
S.AlgorithmOther b -> Left $ concat
|
|
|
|
[ "Unrecognized algo "
|
|
|
|
, BC.unpack b
|
|
|
|
, ", actual key is "
|
|
|
|
, case k of
|
|
|
|
PublicVerifKeyEd25519 _ -> "Ed25519"
|
|
|
|
PublicVerifKeyRSA _ -> "RSA"
|
|
|
|
]
|
2019-02-23 18:17:52 +01:00
|
|
|
|
|
|
|
-- | Fetch a key we don't have cached locally.
|
|
|
|
fetchUnknownKey
|
2019-02-04 00:39:56 +01:00
|
|
|
:: MonadIO m
|
|
|
|
=> Manager
|
2019-02-23 18:17:52 +01:00
|
|
|
-- ^ Manager for making HTTP requests
|
2019-03-11 00:15:42 +01:00
|
|
|
-> Maybe S.Algorithm
|
|
|
|
-- ^ Signature algorithm possibly specified in the HTTP signature header
|
2019-02-22 00:59:53 +01:00
|
|
|
-> Text
|
2019-02-23 18:17:52 +01:00
|
|
|
-- ^ Instance host
|
2019-02-22 00:59:53 +01:00
|
|
|
-> Maybe LocalURI
|
2019-02-23 18:17:52 +01:00
|
|
|
-- ^ Actor URI possibly provided in the HTTP request's actor header
|
2019-02-22 00:59:53 +01:00
|
|
|
-> LocalURI
|
2019-02-23 18:17:52 +01:00
|
|
|
-- ^ Key URI provided in HTTP signature header
|
|
|
|
-> ExceptT String m Fetched
|
2019-03-11 00:15:42 +01:00
|
|
|
fetchUnknownKey manager malgo host mluActor luKey = do
|
2019-02-22 00:59:53 +01:00
|
|
|
obj <- fetchAPIDOrH manager publicKeyId host luKey
|
2019-03-11 00:15:42 +01:00
|
|
|
fetched <-
|
2019-02-04 00:39:56 +01:00
|
|
|
case obj of
|
2019-02-22 00:59:53 +01:00
|
|
|
Left pkey -> do
|
2019-02-22 08:20:19 +01:00
|
|
|
(oi, luActor) <-
|
2019-02-22 00:59:53 +01:00
|
|
|
case publicKeyOwner pkey of
|
|
|
|
OwnerInstance ->
|
|
|
|
case mluActor of
|
|
|
|
Nothing -> throwE "Key is shared but actor header not specified!"
|
2019-02-22 08:20:19 +01:00
|
|
|
Just u -> return (True, u)
|
2019-02-22 00:59:53 +01:00
|
|
|
OwnerActor owner -> do
|
|
|
|
for_ mluActor $ \ lu ->
|
|
|
|
if owner == lu
|
2019-02-17 01:14:05 +01:00
|
|
|
then return ()
|
|
|
|
else throwE "Key's owner doesn't match actor header"
|
2019-02-22 08:20:19 +01:00
|
|
|
return (False, owner)
|
|
|
|
inbox <- actorInbox <$> ExceptT (keyListedByActor manager host luKey luActor)
|
2019-03-11 00:15:42 +01:00
|
|
|
return Fetched
|
|
|
|
{ fetchedPublicKey = publicKeyMaterial pkey
|
|
|
|
, fetchedKeyExpires = publicKeyExpires pkey
|
|
|
|
, fetchedActorId = luActor
|
|
|
|
, fetchedActorInbox = inbox
|
|
|
|
, fetchedKeyShared = oi
|
|
|
|
}
|
2019-02-22 00:59:53 +01:00
|
|
|
Right actor -> do
|
|
|
|
if actorId actor == luKey { luriFragment = "" }
|
2019-02-04 11:07:25 +01:00
|
|
|
then return ()
|
2019-02-04 00:39:56 +01:00
|
|
|
else throwE "Actor ID doesn't match the keyid URI we fetched"
|
2019-02-22 00:59:53 +01:00
|
|
|
for_ mluActor $ \ lu ->
|
|
|
|
if actorId actor == lu
|
2019-02-17 01:14:05 +01:00
|
|
|
then return ()
|
|
|
|
else throwE "Key's owner doesn't match actor header"
|
2019-02-22 09:30:43 +01:00
|
|
|
pk <- matchKeyObj luKey $ actorPublicKeys actor
|
|
|
|
owner <- case publicKeyOwner pk of
|
|
|
|
OwnerInstance -> throwE "Actor's publicKey is shared, but embedded in actor document! We allow shared keys only if they're in a separate document"
|
2019-02-24 02:21:42 +01:00
|
|
|
OwnerActor owner ->
|
2019-02-22 09:30:43 +01:00
|
|
|
if owner == actorId actor
|
|
|
|
then return owner
|
|
|
|
else throwE "Actor's publicKey's owner doesn't match the actor's ID"
|
2019-03-11 00:15:42 +01:00
|
|
|
return Fetched
|
|
|
|
{ fetchedPublicKey = publicKeyMaterial pk
|
|
|
|
, fetchedKeyExpires = publicKeyExpires pk
|
|
|
|
, fetchedActorId = owner
|
|
|
|
, fetchedActorInbox = actorInbox actor
|
|
|
|
, fetchedKeyShared = False
|
|
|
|
}
|
|
|
|
ExceptT . pure $ verifyAlgo malgo $ fetchedPublicKey fetched
|
|
|
|
return fetched
|
|
|
|
|
|
|
|
keyDetail pk = (publicKeyMaterial pk, publicKeyExpires pk)
|
2019-02-23 18:17:52 +01:00
|
|
|
|
|
|
|
-- | Fetch a personal key we already have cached locally, but we'd like to
|
|
|
|
-- refresh the local copy by fetching the key again from the server.
|
|
|
|
fetchKnownPersonalKey
|
|
|
|
:: MonadIO m
|
|
|
|
=> Manager
|
|
|
|
-- ^ Manager for making HTTP requests
|
2019-03-11 00:15:42 +01:00
|
|
|
-> Maybe S.Algorithm
|
|
|
|
-- ^ Signature algorithm possibly specified in the HTTP signature header
|
2019-02-23 18:17:52 +01:00
|
|
|
-> Text
|
|
|
|
-- ^ Instance host
|
|
|
|
-> LocalURI
|
|
|
|
-- ^ Key owner actor ID URI
|
|
|
|
-> LocalURI
|
|
|
|
-- ^ Key URI
|
2019-03-11 00:15:42 +01:00
|
|
|
-> ExceptT String m (PublicVerifKey, Maybe UTCTime)
|
|
|
|
fetchKnownPersonalKey manager malgo host luOwner luKey = do
|
2019-02-23 18:17:52 +01:00
|
|
|
obj <- fetchAPIDOrH manager publicKeyId host luKey
|
2019-03-11 00:15:42 +01:00
|
|
|
(material, mexpires) <-
|
2019-02-23 18:17:52 +01:00
|
|
|
case obj of
|
|
|
|
Left pkey -> do
|
|
|
|
case publicKeyOwner pkey of
|
|
|
|
OwnerInstance -> throwE "Personal key became shared"
|
|
|
|
OwnerActor owner ->
|
|
|
|
when (luOwner /= owner) $ throwE "Key owner changed"
|
|
|
|
return $ keyDetail pkey
|
|
|
|
Right actor -> do
|
|
|
|
when (actorId actor /= luKey { luriFragment = "" }) $
|
|
|
|
throwE "Actor ID doesn't match the keyid URI we fetched"
|
|
|
|
when (actorId actor /= luOwner) $
|
|
|
|
throwE "Key owner changed"
|
|
|
|
pk <- matchKeyObj luKey $ actorPublicKeys actor
|
|
|
|
case publicKeyOwner pk of
|
|
|
|
OwnerInstance -> throwE "Personal key became shared"
|
|
|
|
OwnerActor owner ->
|
|
|
|
when (owner /= luOwner) $
|
|
|
|
throwE "Actor's publicKey's owner doesn't match the actor's ID"
|
|
|
|
return $ keyDetail pk
|
2019-03-11 00:15:42 +01:00
|
|
|
ExceptT . pure $ verifyAlgo malgo material
|
|
|
|
return (material, mexpires)
|
2019-02-23 18:17:52 +01:00
|
|
|
|
|
|
|
-- | Fetch a shared key we already have cached locally, but we'd like to
|
|
|
|
-- refresh the local copy by fetching the key again from the server.
|
|
|
|
fetchKnownSharedKey
|
|
|
|
:: MonadIO m
|
|
|
|
=> Manager
|
|
|
|
-- ^ Manager for making HTTP requests
|
2019-03-11 00:15:42 +01:00
|
|
|
-> Maybe S.Algorithm
|
|
|
|
-- ^ Signature algorithm possibly specified in the HTTP signature header
|
2019-02-23 18:17:52 +01:00
|
|
|
-> Text
|
|
|
|
-- ^ Instance host
|
|
|
|
-> LocalURI
|
|
|
|
-- ^ Actor ID from HTTP actor header
|
|
|
|
-> LocalURI
|
|
|
|
-- ^ Key URI
|
2019-03-11 00:15:42 +01:00
|
|
|
-> ExceptT String m (PublicVerifKey, Maybe UTCTime)
|
2019-03-21 22:38:59 +01:00
|
|
|
fetchKnownSharedKey manager malgo host luActor luKey = do
|
2019-02-23 18:17:52 +01:00
|
|
|
obj <- fetchAPIDOrH manager publicKeyId host luKey
|
|
|
|
pkey <-
|
|
|
|
case obj :: Either PublicKey Actor of
|
|
|
|
Left pk -> return pk
|
|
|
|
Right _actor -> throwE "Expected stand-alone key, got embedded key"
|
|
|
|
case publicKeyOwner pkey of
|
|
|
|
OwnerInstance -> return ()
|
|
|
|
OwnerActor _owner -> throwE "Shared key became personal"
|
2019-03-11 00:15:42 +01:00
|
|
|
let (material, mexpires) = keyDetail pkey
|
|
|
|
ExceptT . pure $ verifyAlgo malgo material
|
|
|
|
return (material, mexpires)
|