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
|
|
|
|
( -- * Actor
|
|
|
|
--
|
|
|
|
-- ActivityPub actor document including a public key, with a 'FromJSON'
|
|
|
|
-- instance for fetching and a 'ToJSON' instance for publishing.
|
|
|
|
ActorType (..)
|
|
|
|
, Algorithm (..)
|
|
|
|
, PublicKey (..)
|
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
|
|
|
, PublicKeySet (..)
|
2019-01-21 16:54:57 +01:00
|
|
|
, Actor (..)
|
|
|
|
|
|
|
|
-- * Activity
|
|
|
|
--
|
|
|
|
-- Very basic activity document which is just general JSON with some
|
|
|
|
-- basic checks. 'FromJSON' instance for receiving POSTs, and 'ToJSON'
|
|
|
|
-- instance for delivering to other servers.
|
|
|
|
, Activity (..)
|
|
|
|
|
|
|
|
-- * Utilities
|
|
|
|
, provideAP
|
|
|
|
, APGetError (..)
|
|
|
|
, httpGetAP
|
|
|
|
, httpPostAP
|
2019-02-06 03:48:23 +01:00
|
|
|
, Fetched (..)
|
2019-02-04 00:39:56 +01:00
|
|
|
, fetchKey
|
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)
|
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
|
|
|
import Control.Monad ((<=<))
|
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-02-04 00:39:56 +01:00
|
|
|
import Crypto.Error (CryptoFailable (..))
|
2019-01-21 16:54:57 +01:00
|
|
|
import Data.Aeson
|
|
|
|
import Data.Aeson.Types (Parser)
|
2019-02-04 11:07:25 +01:00
|
|
|
import Data.Bifunctor (bimap, first)
|
|
|
|
import Data.Bitraversable (bitraverse)
|
2019-01-21 16:54:57 +01:00
|
|
|
import Data.ByteString (ByteString)
|
|
|
|
import Data.List.NonEmpty (NonEmpty)
|
|
|
|
import Data.PEM
|
|
|
|
import Data.Semigroup (Endo)
|
|
|
|
import Data.Text (Text)
|
|
|
|
import Data.Text.Encoding (encodeUtf8, decodeUtf8)
|
2019-02-05 05:05:44 +01:00
|
|
|
import Data.Time.Clock (UTCTime)
|
2019-01-21 16:54:57 +01:00
|
|
|
import Network.HTTP.Client
|
|
|
|
import Network.HTTP.Client.Conduit.ActivityPub (httpAPEither)
|
|
|
|
import Network.HTTP.Client.Signature (signRequest)
|
|
|
|
import Network.HTTP.Signature (KeyId, Signature)
|
|
|
|
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-02-04 00:39:56 +01:00
|
|
|
import qualified Crypto.PubKey.Ed25519 as E (PublicKey, publicKey)
|
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)
|
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
|
|
|
import qualified Data.Vector as V (fromList, toList)
|
2019-01-21 16:54:57 +01:00
|
|
|
|
2019-02-03 12:01:36 +01:00
|
|
|
import Data.Aeson.Local
|
2019-01-21 16:54:57 +01:00
|
|
|
|
|
|
|
as2context :: Text
|
|
|
|
as2context = "https://www.w3.org/ns/activitystreams"
|
|
|
|
|
|
|
|
actorContext :: Value
|
|
|
|
actorContext = Array $ V.fromList
|
|
|
|
[ String as2context
|
|
|
|
, String "https://w3id.org/security/v1"
|
|
|
|
]
|
|
|
|
|
|
|
|
data ActorType = ActorTypePerson | ActorTypeOther Text
|
|
|
|
|
|
|
|
instance FromJSON ActorType where
|
|
|
|
parseJSON = withText "ActorType" $ \ t ->
|
|
|
|
pure $ case t of
|
|
|
|
"Person" -> ActorTypePerson
|
|
|
|
_ -> ActorTypeOther t
|
|
|
|
|
|
|
|
instance ToJSON ActorType where
|
|
|
|
toJSON = error "toJSON ActorType"
|
|
|
|
toEncoding at =
|
|
|
|
toEncoding $ case at of
|
|
|
|
ActorTypePerson -> "Person"
|
|
|
|
ActorTypeOther t -> t
|
|
|
|
|
|
|
|
data Algorithm = AlgorithmEd25519 | AlgorithmOther Text
|
|
|
|
|
|
|
|
instance FromJSON Algorithm where
|
|
|
|
parseJSON = withText "Algorithm" $ \ t ->
|
|
|
|
pure $ if t == frg <> "ed25519"
|
|
|
|
then AlgorithmEd25519
|
|
|
|
else AlgorithmOther t
|
|
|
|
|
|
|
|
instance ToJSON Algorithm where
|
|
|
|
toJSON = error "toJSON Algorithm"
|
|
|
|
toEncoding algo =
|
|
|
|
toEncoding $ case algo of
|
|
|
|
AlgorithmEd25519 -> frg <> "ed25519"
|
|
|
|
AlgorithmOther t -> t
|
|
|
|
|
|
|
|
data PublicKey = PublicKey
|
2019-02-05 05:05:44 +01:00
|
|
|
{ publicKeyId :: URI
|
|
|
|
, publicKeyExpires :: Maybe UTCTime
|
|
|
|
, publicKeyOwner :: URI
|
|
|
|
, publicKeyPem :: PEM
|
|
|
|
, publicKeyAlgo :: Maybe Algorithm
|
|
|
|
, publicKeyShared :: Bool
|
2019-01-21 16:54:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
instance FromJSON PublicKey where
|
2019-02-04 00:39:56 +01:00
|
|
|
parseJSON = withObject "PublicKey" $ \ o -> do
|
|
|
|
mtyp <- optional $ o .: "@type" <|> o .: "type"
|
|
|
|
case mtyp of
|
|
|
|
Nothing -> return ()
|
|
|
|
Just t ->
|
|
|
|
if t == ("Key" :: Text)
|
|
|
|
then return ()
|
|
|
|
else fail "PublicKey @type isn't Key"
|
2019-01-21 16:54:57 +01:00
|
|
|
PublicKey
|
2019-02-03 12:01:36 +01:00
|
|
|
<$> (parseHttpsURI =<< o .: "id")
|
2019-02-05 05:05:44 +01:00
|
|
|
<*> o .:? "expires"
|
2019-02-03 12:01:36 +01:00
|
|
|
<*> (parseHttpsURI =<< o .: "owner")
|
2019-01-21 16:54:57 +01:00
|
|
|
<*> (parsePEM =<< o .: "publicKeyPem")
|
|
|
|
<*> o .:? (frg <> "algorithm")
|
2019-02-03 12:12:18 +01:00
|
|
|
<*> o .:? (frg <> "shared") .!= False
|
2019-01-21 16:54:57 +01:00
|
|
|
where
|
|
|
|
parsePEM t =
|
|
|
|
case pemParseBS $ encodeUtf8 t of
|
|
|
|
Left e -> fail $ "PEM parsing failed: " ++ e
|
|
|
|
Right xs ->
|
|
|
|
case xs of
|
|
|
|
[] -> fail "Empty PEM"
|
|
|
|
[x] -> pure x
|
|
|
|
_ -> fail "Multiple PEM sections"
|
|
|
|
|
|
|
|
instance ToJSON PublicKey where
|
|
|
|
toJSON = error "toJSON PublicKey"
|
2019-02-05 05:05:44 +01:00
|
|
|
toEncoding (PublicKey id_ mexpires owner pem malgo shared) =
|
2019-01-21 16:54:57 +01:00
|
|
|
pairs
|
2019-02-03 12:12:18 +01:00
|
|
|
$ "id" .= renderURI id_
|
2019-02-05 05:05:44 +01:00
|
|
|
<> "expires" .=? mexpires
|
2019-02-03 12:12:18 +01:00
|
|
|
<> "owner" .= renderURI owner
|
|
|
|
<> "publicKeyPem" .= decodeUtf8 (pemWriteBS pem)
|
|
|
|
<> (frg <> "algorithm") .=? malgo
|
|
|
|
<> (frg <> "shared") .= shared
|
2019-01-21 16:54:57 +01:00
|
|
|
|
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
|
|
|
data PublicKeySet = PublicKeySet
|
|
|
|
{ publicKey1 :: Either URI PublicKey
|
|
|
|
, publicKey2 :: Maybe (Either URI PublicKey)
|
|
|
|
}
|
|
|
|
|
|
|
|
instance FromJSON PublicKeySet where
|
|
|
|
parseJSON v =
|
|
|
|
case v of
|
|
|
|
Array a ->
|
|
|
|
case V.toList a of
|
|
|
|
[] -> fail "No public keys"
|
|
|
|
[k1] -> PublicKeySet <$> parseKey k1 <*> pure Nothing
|
|
|
|
[k1, k2] -> PublicKeySet <$> parseKey k1 <*> (Just <$> parseKey k2)
|
|
|
|
_ -> fail "More than 2 public keys isn't supported"
|
|
|
|
_ -> PublicKeySet <$> parseKey v <*> pure Nothing
|
|
|
|
where
|
|
|
|
parseKey = bitraverse parseHttpsURI pure . toEither <=< parseJSON
|
|
|
|
|
|
|
|
instance ToJSON PublicKeySet where
|
|
|
|
toJSON = error "toJSON PublicKeySet"
|
|
|
|
toEncoding (PublicKeySet k1 mk2) =
|
|
|
|
case mk2 of
|
|
|
|
Nothing -> toEncoding $ renderKey k1
|
|
|
|
Just k2 -> toEncodingList [renderKey k1, renderKey k2]
|
|
|
|
where
|
|
|
|
renderKey = fromEither . first renderURI
|
|
|
|
|
2019-01-21 16:54:57 +01:00
|
|
|
data Actor = Actor
|
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
|
|
|
{ actorId :: URI
|
|
|
|
, actorType :: ActorType
|
|
|
|
, actorUsername :: Text
|
|
|
|
, actorInbox :: URI
|
|
|
|
, actorPublicKeys :: PublicKeySet
|
2019-01-21 16:54:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
instance FromJSON Actor where
|
|
|
|
parseJSON = withObject "Actor" $ \ o ->
|
|
|
|
Actor
|
2019-02-03 12:01:36 +01:00
|
|
|
<$> (parseHttpsURI =<< o .: "id")
|
2019-01-21 16:54:57 +01:00
|
|
|
<*> o .: "type"
|
|
|
|
<*> o .: "preferredUsername"
|
2019-02-03 12:01:36 +01:00
|
|
|
<*> (parseHttpsURI =<< o .: "inbox")
|
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
|
|
|
<*> o .: "publicKey"
|
2019-01-21 16:54:57 +01:00
|
|
|
|
|
|
|
instance ToJSON Actor where
|
|
|
|
toJSON = error "toJSON Actor"
|
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
|
|
|
toEncoding (Actor id_ typ username inbox pkeys) =
|
2019-01-21 16:54:57 +01:00
|
|
|
pairs
|
|
|
|
$ "@context" .= actorContext
|
|
|
|
<> "id" .= renderURI id_
|
|
|
|
<> "type" .= typ
|
|
|
|
<> "preferredUsername" .= username
|
|
|
|
<> "inbox" .= renderURI inbox
|
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
|
|
|
<> "publicKey" .= pkeys
|
2019-01-21 16:54:57 +01:00
|
|
|
|
|
|
|
-- | This may seem trivial, but it exists for a good reason: In the 'FromJSON'
|
|
|
|
-- instance we perform sanity checks. We just don't need to remember the fields
|
|
|
|
-- after checking, so we don't unnecessarily add them as fields. We just keep
|
|
|
|
-- the _to_ field, which tells us who the target actor is (we currently support
|
|
|
|
-- only the _to_ field, and it has to be a single URI, and that URI has to be
|
|
|
|
-- an actor, not a collection). The 'Object' we keep is simply for encoding
|
|
|
|
-- back to JSON. I suppose that's actually silly, we could just keep the actual
|
|
|
|
-- ByteString, but I guess it's okay for now, and it happens to guarantee the
|
|
|
|
-- JSON we POST has no extra whitespace.
|
|
|
|
data Activity = Activity
|
|
|
|
{ activityTo :: URI
|
|
|
|
, activityJSON :: Object
|
|
|
|
}
|
|
|
|
|
|
|
|
instance FromJSON Activity where
|
|
|
|
parseJSON = withObject "Activity" $ \ o -> do
|
|
|
|
c <- o .: "@context"
|
|
|
|
if c == as2context
|
|
|
|
then return ()
|
|
|
|
else fail "@context isn't the AS2 context URI"
|
|
|
|
case M.lookup "id" o of
|
|
|
|
Nothing -> return ()
|
|
|
|
Just _ -> fail "id is provided; let the server set it"
|
|
|
|
case M.lookup "type" o of
|
|
|
|
Nothing -> fail "Activity type missing"
|
|
|
|
Just (String _) -> return ()
|
|
|
|
Just _ -> fail "Activity type isn't a string"
|
|
|
|
case M.lookup "actor" o of
|
|
|
|
Nothing -> return ()
|
|
|
|
Just _ -> fail "actor is provided; let the server set it"
|
|
|
|
mto <- case M.lookup "object" o of
|
|
|
|
Nothing -> return Nothing
|
|
|
|
Just v -> case v of
|
|
|
|
String _ -> return Nothing
|
|
|
|
Object obj -> do
|
|
|
|
case M.lookup "id" obj of
|
|
|
|
Nothing -> return ()
|
|
|
|
Just _ -> fail "object's id is provided; let the server set it"
|
|
|
|
case M.lookup "type" obj of
|
|
|
|
Nothing -> fail "Activity object type missing"
|
|
|
|
Just (String _) -> return ()
|
|
|
|
Just _ -> fail "Activity object type isn't a string"
|
|
|
|
case M.lookup "actor" o <|> M.lookup "attributedTo" o of
|
|
|
|
Nothing -> return ()
|
|
|
|
Just _ -> fail "attribution is provided; let the server set it"
|
|
|
|
obj .:? "to"
|
|
|
|
_ -> fail "Activity object isn't JSON string or object"
|
|
|
|
mto2 <- o .:? "to"
|
|
|
|
to <- case mto <|> mto2 of
|
|
|
|
Nothing -> fail "to not provided"
|
2019-02-03 12:01:36 +01:00
|
|
|
Just t -> parseHttpsURI t
|
2019-01-21 16:54:57 +01:00
|
|
|
return $ Activity to o
|
|
|
|
|
|
|
|
instance ToJSON Activity where
|
|
|
|
toJSON = error "toJSON Activity"
|
|
|
|
toEncoding = toEncoding . activityJSON
|
|
|
|
|
|
|
|
typeActivityStreams2 :: ContentType
|
|
|
|
typeActivityStreams2 = "application/activity+json"
|
|
|
|
|
|
|
|
typeActivityStreams2LD :: ContentType
|
|
|
|
typeActivityStreams2LD =
|
|
|
|
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
|
|
|
|
|
|
|
|
provideAP :: (Monad m, ToJSON a) => a -> Writer (Endo [ProvidedRep m]) ()
|
|
|
|
provideAP v = do
|
|
|
|
let enc = toEncoding v
|
|
|
|
-- provideRepType typeActivityStreams2 $ return enc
|
|
|
|
provideRepType typeActivityStreams2LD $ return enc
|
|
|
|
|
|
|
|
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
|
|
|
|
-> URI
|
|
|
|
-> m (Either APGetError (Response a))
|
|
|
|
httpGetAP manager uri =
|
|
|
|
if uriScheme uri /= "https:"
|
|
|
|
then return $ Left $ APGetErrorHTTP $ InvalidUrlException (show uri) "Scheme isn't https"
|
|
|
|
else liftIO $ mkResult <$> try (httpAPEither manager =<< requestFromURI uri)
|
|
|
|
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"
|
|
|
|
|
|
|
|
-- Set method to POST, Set Content-Type, make HTTP signature, set response to throw on non-2xx
|
|
|
|
-- status
|
|
|
|
|
|
|
|
-- | 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
|
|
|
|
-- * Compute HTTP signature and add _Signature_ request header
|
|
|
|
-- * Perform the POST request
|
|
|
|
-- * Verify the response status is 2xx
|
|
|
|
httpPostAP
|
|
|
|
:: (MonadIO m, ToJSON a)
|
|
|
|
=> Manager
|
|
|
|
-> URI
|
|
|
|
-> NonEmpty HeaderName
|
|
|
|
-> (ByteString -> (KeyId, Signature))
|
|
|
|
-> a
|
|
|
|
-> m (Either HttpException (Response ()))
|
|
|
|
httpPostAP manager uri headers sign value =
|
|
|
|
if uriScheme uri /= "https:"
|
|
|
|
then return $ Left $ InvalidUrlException (show uri) "Scheme isn't https"
|
|
|
|
else liftIO $ try $ do
|
|
|
|
req <- requestFromURI uri
|
|
|
|
let req' =
|
|
|
|
setRequestCheckStatus $
|
|
|
|
consHeader hContentType typeActivityStreams2LD $
|
|
|
|
req { method = "POST"
|
|
|
|
, requestBody = RequestBodyLBS $ encode value
|
|
|
|
}
|
|
|
|
sign' b =
|
|
|
|
let (k, s) = sign b
|
|
|
|
in (Nothing, k, s)
|
|
|
|
req'' <- signRequest headers sign' Nothing req'
|
2019-02-03 11:59:35 +01:00
|
|
|
httpNoBody req'' manager
|
2019-01-21 16:54:57 +01:00
|
|
|
where
|
|
|
|
consHeader n b r = r { requestHeaders = (n, b) : requestHeaders r }
|
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
|
|
|
|
{ fetchedPublicKey :: E.PublicKey
|
|
|
|
-- ^ The Ed25519 public key corresponding to the URI we requested.
|
|
|
|
, fetchedKeyExpires :: Maybe UTCTime
|
|
|
|
-- ^ Optional expiration time declared for the key we received.
|
|
|
|
, fetchedActorId :: URI
|
|
|
|
-- ^ The @id URI of the actor for whom the key's signature applies.
|
|
|
|
, fetchedHost :: Text
|
|
|
|
-- ^ The domain name of the instance from which we got the key.
|
|
|
|
, 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-02-04 00:39:56 +01:00
|
|
|
fetchKey
|
|
|
|
:: MonadIO m
|
|
|
|
=> Manager
|
|
|
|
-> Bool
|
2019-02-06 03:48:23 +01:00
|
|
|
-> Maybe URI
|
2019-02-04 00:39:56 +01:00
|
|
|
-> URI
|
2019-02-06 03:48:23 +01:00
|
|
|
-> m (Either String Fetched)
|
|
|
|
fetchKey manager sigAlgo muActor uKey = runExceptT $ do
|
2019-02-04 00:39:56 +01:00
|
|
|
let fetch :: (MonadIO m, FromJSON a) => URI -> ExceptT String m a
|
|
|
|
fetch u = ExceptT $ bimap displayException responseBody <$> httpGetAP manager u
|
2019-02-06 03:48:23 +01:00
|
|
|
obj <- fetch uKey
|
|
|
|
let inztance = uKey { uriPath = "", uriQuery = "", uriFragment = "" }
|
|
|
|
authority =
|
|
|
|
case uriAuthority uKey of
|
|
|
|
Nothing -> error "BUG! We were supposed to verify URI authority is non-empty!"
|
|
|
|
Just a -> a
|
|
|
|
(actor, pkey, shared) <-
|
2019-02-04 00:39:56 +01:00
|
|
|
case obj of
|
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
|
|
|
Left' pkey -> do
|
2019-02-06 03:48:23 +01:00
|
|
|
if publicKeyId pkey == uKey
|
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
|
|
|
then return ()
|
|
|
|
else throwE "Public key's ID doesn't match the keyid URI"
|
2019-02-05 14:02:15 +01:00
|
|
|
if uriAuthority (publicKeyOwner pkey) == Just authority
|
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
|
|
|
then return ()
|
2019-02-04 00:39:56 +01:00
|
|
|
else throwE "Actor and key on different domains, we reject"
|
2019-02-06 03:48:23 +01:00
|
|
|
uActor <-
|
|
|
|
if publicKeyShared pkey
|
|
|
|
then case muActor of
|
|
|
|
Nothing -> throwE "Key is shared but actor header not specified!"
|
|
|
|
Just u -> return u
|
|
|
|
else return $ publicKeyOwner pkey
|
|
|
|
actor <- fetch uActor
|
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
|
|
|
let PublicKeySet k1 mk2 = actorPublicKeys actor
|
2019-02-06 03:48:23 +01:00
|
|
|
match (Left uri) = uri == uKey
|
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
|
|
|
match (Right _) = False
|
|
|
|
if match k1 || maybe False match mk2
|
2019-02-06 03:48:23 +01:00
|
|
|
then return (actor, pkey, publicKeyShared pkey)
|
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
|
|
|
else throwE "Actor publicKey has no URI matching pkey @id"
|
2019-02-04 11:07:25 +01:00
|
|
|
Right' actor -> do
|
2019-02-06 03:48:23 +01:00
|
|
|
if actorId actor == uKey { uriFragment = "" }
|
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-06 03:48:23 +01:00
|
|
|
case muActor of
|
|
|
|
Nothing -> return ()
|
|
|
|
Just u ->
|
|
|
|
if actorId actor == u
|
|
|
|
then return ()
|
|
|
|
else throwE "Key's owner doesn't match actor header"
|
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
|
|
|
let PublicKeySet k1 mk2 = actorPublicKeys actor
|
|
|
|
match (Left _) = Nothing
|
|
|
|
match (Right pk) =
|
2019-02-06 03:48:23 +01:00
|
|
|
if publicKeyId pk == uKey
|
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
|
|
|
then Just pk
|
|
|
|
else Nothing
|
|
|
|
case match k1 <|> (match =<< mk2) of
|
|
|
|
Nothing -> throwE "keyId resolved to actor which doesn't have a key object with that ID"
|
2019-02-06 03:48:23 +01:00
|
|
|
Just pk ->
|
|
|
|
if publicKeyShared pk
|
|
|
|
then throwE "Actor's publicKey is shared, but embedded in actor document! We allow shared keys only if they're in a separate document"
|
|
|
|
else return (actor, pk, False)
|
2019-02-04 00:39:56 +01:00
|
|
|
ExceptT . pure $ do
|
2019-02-06 03:48:23 +01:00
|
|
|
if shared
|
|
|
|
then if publicKeyOwner pkey == inztance
|
2019-02-05 14:02:15 +01:00
|
|
|
then Right ()
|
|
|
|
else Left "Key is shared but its owner isn't the top-level instance URI"
|
|
|
|
else if publicKeyOwner pkey == actorId actor
|
|
|
|
then Right ()
|
|
|
|
else Left "Actor's publicKey's owner doesn't match the actor's ID"
|
2019-02-04 00:39:56 +01:00
|
|
|
case publicKeyAlgo pkey of
|
|
|
|
Nothing ->
|
|
|
|
Left $
|
|
|
|
if sigAlgo
|
|
|
|
then "Algo mismatch, Ed25519 in Sig but none in actor"
|
|
|
|
else "Algo not given in Sig nor actor"
|
|
|
|
Just algo ->
|
|
|
|
case algo of
|
|
|
|
AlgorithmEd25519 -> Right ()
|
|
|
|
AlgorithmOther _ ->
|
|
|
|
Left $
|
|
|
|
if sigAlgo
|
|
|
|
then "Algo mismatch, Ed25519 in Sig but unsupported algo in actor"
|
|
|
|
else "No algo in Sig, unsupported algo in actor"
|
|
|
|
case E.publicKey $ pemContent $ publicKeyPem pkey of
|
2019-02-06 03:48:23 +01:00
|
|
|
CryptoPassed k -> Right Fetched
|
|
|
|
{ fetchedPublicKey = k
|
|
|
|
, fetchedKeyExpires = publicKeyExpires pkey
|
|
|
|
, fetchedActorId = actorId actor
|
|
|
|
, fetchedHost = T.pack $ uriRegName authority
|
|
|
|
, fetchedKeyShared = shared
|
|
|
|
}
|
|
|
|
CryptoFailed _ -> Left "Parsing Ed25519 public key failed"
|