Generate FEP-8b32 authenticity proofs when delivering activities

Limitations:

- Verification of proofs not implemeneted yet, just generation.
  Verification probably coming in the next commit.
- Only jcs-eddsa-2022 is supported. Can add more cryptosuites once
  they're updated for the requirements of the VC Data Integrity spec.
- Bug: The proofs aren't stored in the DB versions of outgoing activities, i.e.
  HTTP GETing an activity won't include the proof. Probably not urgent
  to fix. Ideally, change the whole PersistJSONObject/Envelope/etc.
  thing to allow to serialize the activity exactly once.
This commit is contained in:
Pere Lev 2023-05-30 09:48:21 +03:00
parent ba02d62eb5
commit e8e587af26
No known key found for this signature in database
GPG key ID: 5252C5C863E5E57D
11 changed files with 128 additions and 46 deletions

View file

@ -179,7 +179,7 @@ generateActorKey = mk <$> generateSecretKey
-- renderPEM = pemWriteBS . PEM "PUBLIC KEY" [] . convert -- renderPEM = pemWriteBS . PEM "PUBLIC KEY" [] . convert
-- | A loop that runs forever and periodically generates new actor keys, -- | A loop that runs forever and periodically generates new actor keys,
-- storing them in a 'TVar'. It manages a pait of keys, and each time it toggles -- storing them in a 'TVar'. It manages a pair of keys, and each time it toggles
-- which key gets rotated. -- which key gets rotated.
actorKeyRotator :: TimeInterval -> TVar (ActorKey, ActorKey, Bool) -> IO () actorKeyRotator :: TimeInterval -> TVar (ActorKey, ActorKey, Bool) -> IO ()
actorKeyRotator interval keys = periodically interval $ do actorKeyRotator interval keys = periodically interval $ do

View file

@ -1174,6 +1174,7 @@ createPatchTrackerC (Entity pidUser personUser) senderActor maybeCap localRecips
, activitySummary = Nothing , activitySummary = Nothing
, activityAudience = AP.Audience recips [] [] [] [] [] , activityAudience = AP.Audience recips [] [] [] [] []
, activityFulfills = [] , activityFulfills = []
, activityProof = Nothing
, activitySpecific = FollowActivity AP.Follow , activitySpecific = FollowActivity AP.Follow
{ AP.followObject = encodeRouteHome $ LoomR loomHash { AP.followObject = encodeRouteHome $ LoomR loomHash
, AP.followContext = Nothing , AP.followContext = Nothing
@ -1198,6 +1199,7 @@ createPatchTrackerC (Entity pidUser personUser) senderActor maybeCap localRecips
, activitySummary = Nothing , activitySummary = Nothing
, activityAudience = Audience recips [] [] [] [] [] , activityAudience = Audience recips [] [] [] [] []
, activityFulfills = [] , activityFulfills = []
, activityProof = Nothing
, activitySpecific = AcceptActivity Accept , activitySpecific = AcceptActivity Accept
{ acceptObject = ObjURI hLocal luFollow { acceptObject = ObjURI hLocal luFollow
, acceptResult = Nothing , acceptResult = Nothing
@ -1400,6 +1402,7 @@ createRepositoryC (Entity pidUser personUser) senderActor maybeCap localRecips r
, activitySummary = Nothing , activitySummary = Nothing
, activityAudience = AP.Audience recips [] [] [] [] [] , activityAudience = AP.Audience recips [] [] [] [] []
, activityFulfills = [] , activityFulfills = []
, activityProof = Nothing
, activitySpecific = FollowActivity AP.Follow , activitySpecific = FollowActivity AP.Follow
{ AP.followObject = encodeRouteHome $ RepoR repoHash { AP.followObject = encodeRouteHome $ RepoR repoHash
, AP.followContext = Nothing , AP.followContext = Nothing
@ -1424,6 +1427,7 @@ createRepositoryC (Entity pidUser personUser) senderActor maybeCap localRecips r
, activitySummary = Nothing , activitySummary = Nothing
, activityAudience = Audience recips [] [] [] [] [] , activityAudience = Audience recips [] [] [] [] []
, activityFulfills = [] , activityFulfills = []
, activityProof = Nothing
, activitySpecific = AcceptActivity Accept , activitySpecific = AcceptActivity Accept
{ acceptObject = ObjURI hLocal luFollow { acceptObject = ObjURI hLocal luFollow
, acceptResult = Nothing , acceptResult = Nothing
@ -1652,6 +1656,7 @@ createTicketTrackerC (Entity pidUser personUser) senderActor maybeCap localRecip
, activitySummary = Nothing , activitySummary = Nothing
, activityAudience = AP.Audience recips [] [] [] [] [] , activityAudience = AP.Audience recips [] [] [] [] []
, activityFulfills = [] , activityFulfills = []
, activityProof = Nothing
, activitySpecific = FollowActivity AP.Follow , activitySpecific = FollowActivity AP.Follow
{ AP.followObject = encodeRouteHome $ DeckR deckHash { AP.followObject = encodeRouteHome $ DeckR deckHash
, AP.followContext = Nothing , AP.followContext = Nothing
@ -1676,6 +1681,7 @@ createTicketTrackerC (Entity pidUser personUser) senderActor maybeCap localRecip
, activitySummary = Nothing , activitySummary = Nothing
, activityAudience = Audience recips [] [] [] [] [] , activityAudience = Audience recips [] [] [] [] []
, activityFulfills = [] , activityFulfills = []
, activityProof = Nothing
, activitySpecific = AcceptActivity Accept , activitySpecific = AcceptActivity Accept
{ acceptObject = ObjURI hLocal luFollow { acceptObject = ObjURI hLocal luFollow
, acceptResult = Nothing , acceptResult = Nothing

View file

@ -65,43 +65,6 @@ import qualified Data.Text as T
import qualified Database.Esqueleto as E import qualified Database.Esqueleto as E
import qualified Network.Wai as W import qualified Network.Wai as W
import Data.Time.Interval
import Network.HTTP.Signature hiding (requestHeaders)
import Yesod.HttpSignature
import Crypto.PublicVerifKey
import Database.Persist.JSON
import Network.FedURI
import Network.HTTP.Digest
import Web.ActivityPub hiding (Follow, Ticket)
import Yesod.ActivityPub
import Yesod.Auth.Unverified
import Yesod.FedURI
import Yesod.Hashids
import Yesod.MonadSite
import qualified Web.ActivityPub as AP
import Control.Monad.Trans.Except.Local
import Data.Aeson.Local
import Data.Either.Local
import Data.List.Local
import Data.List.NonEmpty.Local
import Data.Maybe.Local
import Data.Tuple.Local
import Database.Persist.Local
import Yesod.Persist.Local
import Vervis.ActivityPub
import Vervis.ActorKey
import Vervis.Web.Delivery
import Vervis.Federation.Auth
import Vervis.Foundation
import Vervis.Model
import Vervis.Recipient
import Vervis.RemoteActorStore
import Vervis.Settings
{- {-
handleProjectInbox handleProjectInbox

View file

@ -75,7 +75,7 @@ import qualified Network.HTTP.Signature as S (Algorithm (..))
import Control.Concurrent.Actor hiding (Message) import Control.Concurrent.Actor hiding (Message)
import Crypto.ActorKey import Crypto.ActorKey
import Crypto.PublicVerifKey --import Crypto.PublicVerifKey
import Network.FedURI import Network.FedURI
import Web.ActivityAccess import Web.ActivityAccess
import Web.Actor.Persist import Web.Actor.Persist

View file

@ -193,7 +193,7 @@ postPersonOutboxR personHash = do
unless (federation || null remoteRecips) $ unless (federation || null remoteRecips) $
throwE "Federation disabled, but remote recipients found" throwE "Federation disabled, but remote recipients found"
handle eperson actorDB (AP.Activity _mid _actorAP muCap summary audience _fulfills specific) = do handle eperson actorDB (AP.Activity _mid _actorAP muCap summary audience _fulfills _mproof specific) = do
maybeCap <- traverse (nameExceptT "Capability" . parseActivityURI) muCap maybeCap <- traverse (nameExceptT "Capability" . parseActivityURI) muCap
ParsedAudience localRecips remoteRecips blinded fwdHosts <- do ParsedAudience localRecips remoteRecips blinded fwdHosts <- do
mrecips <- parseAudience audience mrecips <- parseAudience audience

View file

@ -1,6 +1,6 @@
{- This file is part of Vervis. {- This file is part of Vervis.
- -
- Written in 2016, 2018, 2019, 2020, 2021, 2022 - Written in 2016, 2018, 2019, 2020, 2021, 2022, 2023
- by fr33domlover <fr33domlover@riseup.net>. - by fr33domlover <fr33domlover@riseup.net>.
- -
- Copying is an act of love. Please copy, reuse and share. - Copying is an act of love. Please copy, reuse and share.
@ -581,6 +581,7 @@ changes hLocal ctx =
, activitySummary = Nothing , activitySummary = Nothing
, activityAudience = Audience [] [] [] [] [] [] , activityAudience = Audience [] [] [] [] [] []
, activityFulfills = [] , activityFulfills = []
, activityProof = Nothing
, activitySpecific = RejectActivity $ Reject fedUri , activitySpecific = RejectActivity $ Reject fedUri
} }
insertEntity $ OutboxItem20190612 pid (persistJSONObjectFromDoc doc) defaultTime insertEntity $ OutboxItem20190612 pid (persistJSONObjectFromDoc doc) defaultTime

View file

@ -82,6 +82,8 @@ module Web.ActivityPub
, Resolve (..) , Resolve (..)
, Undo (..) , Undo (..)
, Audience (..) , Audience (..)
, ProofConfig (..)
, Proof (..)
, SpecificActivity (..) , SpecificActivity (..)
, activityType , activityType
, Action (..) , Action (..)
@ -160,6 +162,9 @@ import Yesod.Core.Handler (ProvidedRep, provideRepType)
import Network.HTTP.Client.Signature import Network.HTTP.Client.Signature
import qualified Data.Attoparsec.ByteString as A import qualified Data.Attoparsec.ByteString as A
import qualified Data.ByteArray as BA
import qualified Data.ByteString as B
import qualified Data.ByteString.Base58 as B58
import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString.Char8 as BC import qualified Data.ByteString.Char8 as BC
import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Lazy as BL
@ -180,6 +185,34 @@ import Web.Text
import Data.Aeson.Local import Data.Aeson.Local
-- JSON CANONICALIZATION
--
-- In order to produce JSON-based sigs, we need the ability to produce a
-- canonical ByteString from a given ToJSON-able object. Is aeson's encoder
-- already compatible?
--
-- * Before aeson-2, clearly no, because a HashMap is used for objects
-- * After aeson-2, possibly, since ordered-map mode exists and on by default
--
-- I'm gonna list requirements here and then we can compare this with aeson.
--
-- - [ ] JSON number data MUST be expressible as IEEE 754 [IEEE754]
-- double-precision values. For applications needing higher precision or longer
-- integers than offered by IEEE 754 double precision, it is RECOMMENDED to
-- represent such numbers as JSON strings
-- - [ ] objects must be sorted by key
-- - [ ] The sorting process is applied to property name strings in their "raw" (unescaped) form. That is, a newline character is treated as U+000A
-- - [ ] Property name strings to be sorted are formatted as arrays of UTF-16 [UNICODE] code units. The sorting is based on pure value comparisons, where code units are treated as unsigned integers, independent of locale settings
--
-- Looks like the primary things to verify are key ordering and number
-- serialization.
--
-- When to encode? We need to encode the activity and then:
--
-- 1. Put it in the DB
-- 2. Send to local actors via system
-- 3. Send to remote actors via HTTP
{- {-
data Link = Link data Link = Link
{ linkHref :: URI { linkHref :: URI
@ -1857,6 +1890,57 @@ parseUndo a o = Undo <$> o .: "object"
encodeUndo :: UriMode u => Authority u -> Undo u -> Series encodeUndo :: UriMode u => Authority u -> Undo u -> Series
encodeUndo a (Undo obj) = "object" .= obj encodeUndo a (Undo obj) = "object" .= obj
data ProofConfig u = ProofConfig
{ proofKey :: LocalRefURI
, proofCreated :: UTCTime
}
instance ActivityPub ProofConfig where
jsonldContext _ = []
parseObject o = do
typ <- o .: "type"
guard $ typ == ("DataIntegrityProof" :: Text)
purpose <- o .: "proofPurpose"
guard $ purpose == ("assertionMethod" :: Text)
suite <- o .: "cryptosuite"
guard $ suite == ("jcs-eddsa-2022" :: Text)
RefURI h lruKey <- o .: "verificationMethod"
fmap (h,) $ ProofConfig
<$> pure lruKey
<*> o .: "created"
toSeries h (ProofConfig lruKey created)
= "type" .= ("DataIntegrityProof" :: Text)
<> "proofPurpose" .= ("assertionMethod" :: Text)
<> "cryptosuite" .= ("jcs-eddsa-2022" :: Text)
<> "verificationMethod" .= RefURI h lruKey
<> "created" .= created
data Proof u = Proof
{ proofConfig :: ProofConfig u
, proofValue :: ByteString
}
instance ActivityPub Proof where
jsonldContext _ = []
parseObject o = do
(h, config) <- parseObject o
value <- do
t <- o .: "proofValue"
t58 <-
case T.uncons t of
Just ('z', t') -> return t'
_ -> fail $ "No multibase 'z' prefix: " ++ T.unpack t
let b = TE.encodeUtf8 t58
case B58.decodeBase58 B58.bitcoinAlphabet b of
Nothing ->
fail $ "base58-btc decoding failed:" ++ T.unpack t
Just val -> return val
return (h, Proof config value)
toSeries h (Proof config sig)
= toSeries h config
<> "proofValue" .=
T.cons 'z' (TE.decodeUtf8 $ B58.encodeBase58 B58.bitcoinAlphabet sig)
data SpecificActivity u data SpecificActivity u
= AcceptActivity (Accept u) = AcceptActivity (Accept u)
| AddActivity (Add u) | AddActivity (Add u)
@ -1903,6 +1987,7 @@ makeActivity luId luActor Action{..} = Activity
, activitySummary = actionSummary , activitySummary = actionSummary
, activityAudience = actionAudience , activityAudience = actionAudience
, activityFulfills = actionFulfills , activityFulfills = actionFulfills
, activityProof = Nothing
, activitySpecific = actionSpecific , activitySpecific = actionSpecific
} }
@ -1913,6 +1998,7 @@ data Activity u = Activity
, activitySummary :: Maybe HTML , activitySummary :: Maybe HTML
, activityAudience :: Audience u , activityAudience :: Audience u
, activityFulfills :: [ObjURI u] , activityFulfills :: [ObjURI u]
, activityProof :: Maybe (Proof u)
, activitySpecific :: SpecificActivity u , activitySpecific :: SpecificActivity u
} }
@ -1928,6 +2014,9 @@ instance ActivityPub Activity where
<*> o .:? "summary" <*> o .:? "summary"
<*> parseAudience o <*> parseAudience o
<*> o .:? "fulfills" .!= [] <*> o .:? "fulfills" .!= []
<*> (do mp <- o .:? "proof"
for mp $ withAuthorityT a . parseObject
)
<*> do <*> do
typ <- o .: "type" typ <- o .: "type"
case typ of case typ of
@ -1947,7 +2036,7 @@ instance ActivityPub Activity where
_ -> _ ->
fail $ fail $
"Unrecognized activity type: " ++ T.unpack typ "Unrecognized activity type: " ++ T.unpack typ
toSeries authority (Activity id_ actor mcap summary audience fulfills specific) toSeries authority (Activity id_ actor mcap summary audience fulfills mproof specific)
= "type" .= activityType specific = "type" .= activityType specific
<> "id" .=? (ObjURI authority <$> id_) <> "id" .=? (ObjURI authority <$> id_)
<> "actor" .= ObjURI authority actor <> "actor" .= ObjURI authority actor
@ -1955,6 +2044,7 @@ instance ActivityPub Activity where
<> "summary" .=? summary <> "summary" .=? summary
<> encodeAudience audience <> encodeAudience audience
<> "fulfills" .=% fulfills <> "fulfills" .=% fulfills
<> "proof" .=? (Doc authority <$> mproof)
<> encodeSpecific authority actor specific <> encodeSpecific authority actor specific
where where
encodeSpecific h _ (AcceptActivity a) = encodeAccept h a encodeSpecific h _ (AcceptActivity a) = encodeAccept h a
@ -1982,6 +2072,7 @@ emptyActivity = Activity
, activitySummary = Nothing , activitySummary = Nothing
, activityAudience = emptyAudience , activityAudience = emptyAudience
, activityFulfills = [] , activityFulfills = []
, activityProof = Nothing
, activitySpecific = , activitySpecific =
RejectActivity $ Reject $ ObjURI (Authority "" Nothing) topLocalURI RejectActivity $ Reject $ ObjURI (Authority "" Nothing) topLocalURI
} }
@ -2157,17 +2248,31 @@ sending
:: UriMode u :: UriMode u
=> LocalRefURI => LocalRefURI
-> (ByteString -> S.Signature) -> (ByteString -> S.Signature)
-> Maybe (ProofConfig u, ByteString -> ByteString)
-> Bool -> Bool
-> ObjURI u -> ObjURI u
-> LocalURI -> LocalURI
-> Action u -> Action u
-> Envelope u -> Envelope u
sending lruKey sign holder uActor@(ObjURI hActor luActor) luId action = sending lruKey sign mprove holder uActor@(ObjURI hActor luActor) luId action =
Envelope Envelope
{ envelopeKey = RefURI hActor lruKey { envelopeKey = RefURI hActor lruKey
, envelopeSign = sign , envelopeSign = sign
, envelopeHolder = guard holder >> Just luActor , envelopeHolder = guard holder >> Just luActor
, envelopeBody = encode $ Doc hActor $ makeActivity luId luActor action , envelopeBody =
let act = makeActivity luId luActor action
lb = encode $ Doc hActor act
in case mprove of
Nothing -> lb
Just (config, prove) ->
let configLB = encode $ Doc hActor config
configHash = hashWith SHA256 $ BL.toStrict configLB
bodyHash = hashWith SHA256 $ BL.toStrict lb
input = BA.convert configHash `B.append` BA.convert bodyHash
proof = Proof config (prove input)
actWithProof = act { activityProof = Just proof }
in encode $ Doc hActor actWithProof
} }
retrying retrying

View file

@ -53,10 +53,12 @@ module Web.Actor
) )
where where
import Control.Monad.IO.Class
import Control.Monad.Trans.Except import Control.Monad.Trans.Except
import Data.ByteString (ByteString) import Data.ByteString (ByteString)
import Data.Proxy import Data.Proxy
import Data.Text (Text) import Data.Text (Text)
import Data.Time.Clock
import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Lazy as BL
@ -176,10 +178,13 @@ prepareToSend
prepareToSend keyR sign holder actorR idR action = do prepareToSend keyR sign holder actorR idR action = do
encodeRouteLocal <- getEncodeRouteLocal encodeRouteLocal <- getEncodeRouteLocal
encodeRouteHome <- getEncodeRouteHome encodeRouteHome <- getEncodeRouteHome
now <- liftActor $ liftIO getCurrentTime
let lruKey = LocalRefURI $ Left $ encodeRouteLocal keyR let lruKey = LocalRefURI $ Left $ encodeRouteLocal keyR
uActor = encodeRouteHome actorR uActor = encodeRouteHome actorR
luId = encodeRouteLocal idR luId = encodeRouteLocal idR
return $ AP.sending lruKey sign holder uActor luId action config = AP.ProofConfig lruKey now
signB = S.unSignature . sign
return $ AP.sending lruKey sign (Just (config, signB)) holder uActor luId action
prepareToForward prepareToForward
:: (MonadActor m, ActorEnv m ~ s, StageWebRoute s, StageURIMode s ~ u) :: (MonadActor m, ActorEnv m ~ s, StageWebRoute s, StageURIMode s ~ u)

View file

@ -104,7 +104,7 @@ prepareToSend keyR sign holder actorR idR action = do
let lruKey = LocalRefURI $ Left $ encodeRouteLocal keyR let lruKey = LocalRefURI $ Left $ encodeRouteLocal keyR
uActor = encodeRouteHome actorR uActor = encodeRouteHome actorR
luId = encodeRouteLocal idR luId = encodeRouteLocal idR
return $ AP.sending lruKey sign holder uActor luId action return $ AP.sending lruKey sign Nothing holder uActor luId action
prepareToRetry prepareToRetry
:: (MonadSite m, SiteEnv m ~ site, SiteFedURI site, SiteFedURIMode site ~ u) :: (MonadSite m, SiteEnv m ~ site, SiteFedURI site, SiteFedURIMode site ~ u)

View file

@ -56,6 +56,7 @@ extra-deps:
- url-2.1.3 - url-2.1.3
- annotated-exception-0.2.0.4 - annotated-exception-0.2.0.4
- retry-0.9.3.1 - retry-0.9.3.1
- base58-bytestring-0.1.0
# Override default flag values for local packages and extra-deps # Override default flag values for local packages and extra-deps
flags: flags:

View file

@ -303,6 +303,7 @@ library
, base , base
-- for hex display of Darcs patch hashes -- for hex display of Darcs patch hashes
, base16-bytestring , base16-bytestring
, base58-bytestring
, base64-bytestring , base64-bytestring
-- for Data.Binary.Local -- for Data.Binary.Local
, binary , binary