As some might have inferred from other posts, I am currently writing a quite opinionated (and unopinionated) ActivityPub server:
The social graph as a graph
My assumption is that the Fediverse is an RDF graph containing statements from the ActivityStreams vocabulary, and that ActivityPub is a a graph federation/replication protocol, and that activities are contracts between nodes on how to carry out a specific transformation on the graph. More on that on the README of Vocata.
All of these assumptions are formally correct (but I know that the majority of implementations ignore most of it, partially because graphs can be hard to wrap your head around while simple JSON documents are nice and cozy, and because the AcitivityPub spec suggests that JSON-LD can be ignored even though it can’t without breakign interoperability).
Formally describing JSON-LD transferred on the wire
Hence, let’s for a moment assume what I assumed. In that case, we can make an interesting observation about the JSON-LD documents being pushed and pulled over HTTP (also laid out in Vocata’s README): The JSON-LD documents transferred on the wire represent formal subgraphs of the social graph, in the form of the Concise Bounded Description (CBD) of the transferred object.
tl;dr: The CBD of a subject in an RDF graph is the subgraph containing all statements about the subject itself, as well as all statements about all linked objects that do not have a public identifier, and nothing more. That means, in shortened examples, that:
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://vocatadev.pagekite.me/users/tester1",
"type": "Person",
"preferredUsername": "tester1",
"name": "The Vocata Tester",
"summary": "A user to test Vocata"
}
…is the CBD of this actor (it contains exactly this one object),
{
"@context": "https://www.w3.org/ns/activitystreams",
"summary": "Tester left a note",
"type": "Create",
"actor": "https://vocatadev.pagekite.me/users/tester1",
"to": [ "as:Public" ],
"object": {
"summary": "A test note",
"type": "Note",
"content": "Lorem larum, ipsum.",
"attributedTo": "https://vocatadev.pagekite.me/users/tester1",
"to": [ "as:Public" ]
}
}
…is the CBD of the Create
activity (as transferred in C2S communication), because it contains the activity itself (also not publicly dereferancable for now, but that doesn’t matter), and on top of that, the created object, which also has no public ID,
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://vocatadev.pagekite.me/testcreate-pub",
"summary": "Tester left a note",
"type": "Create",
"actor": "https://vocatadev.pagekite.me/users/tester1",
"to": [ "as:Public" ],
"object": "https://vocatadev.pagekite.me/testnote-pub"
}
…is the CBD of the Create
activity as it could be transferred over S2S, because it contains the activity as the only subject (the object is only referred to as an object identifier), but
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://vocatadev.pagekite.me/testcreate-pub",
"summary": "Tester left a note",
"type": "Create",
"actor": "https://vocatadev.pagekite.me/users/tester1",
"to": [ "as:Public" ],
"object": {
"id": "https://vocatadev.pagekite.me/testnote-pub",
"summary": "A test note",
"type": "Note",
"content": "Lorem larum, ipsum.",
"attributedTo": "https://vocatadev.pagekite.me/users/tester1",
"to": [ "as:Public" ]
}
}
…is not a CBD, because it contains the full object
as another subject in the graph, and it has a public identifier.
Why the transferred JSON-LD being a CBD matters
If we move from single-purpose platforms on the fediverse to a more open and agnostic model, as proposed by Vocata, and want to leverage the power of C2S and S2S being separated, examining the subgraphs transferred on the wire becomes harder. Ideally, we need to accept what we pull or get pushed as a real subgraph, and incorporate it into our view of the graph that we have, no matter what it contains. Doing that for the one object
might be easy, but there might be tags associated with it, and ActivityPub explicitly allows the vocabulary to be extended (which it obviously does, because RDF), so in reality, we don’t know the structure of the subgraph we get.
That means that we cannot properly do authorization on the transferred objects without major hassle.
This mainly counts for pushed objects (using POST
to inboxes), because someone might push us an object
that they are authorized to push, then go and add tags to it with a public identifier of an existing Tag
object, and trick us into overwritingthis object in our view of teh graph. Again, this does not happen in a server which is aware of the structure of tags (it will explicitly check for this case, or even just treat all tags as not being publicly dereferencable, like Mastodon does, and for all relationships it does not explicitly support, it will jsut ignore them).
Assuming, or dictating, tht the transferred subgraph JSON-LD be a CBD, it is guaranteed that the subgraph does not contain any unexpected statements: If something down in the structure of the object
refers to something else (a tag or something), it either is an anonymous object (a blank node in RDF terms), and will not clash with something we have on the graph already, or it will refer to a public identifier, but not contain any more statements about that public identifier.
The verification whether a graph is a CBD is trivial, and once we are done with that, we are done with authorization.
How to get hold of related objects
Fortunately, ActivityPub has a pull mechanism, and that relies on HTTPS. We can assume that the server reachable over a verified HTTPS connection is authoritative for the objects that are dereferencable through it, so: For all objects pointed to in our incoming JSON-LD subgraph, we can just pull them.
Now to the sad part: I fear that most implementations will not do that. The ActivityPub spec contains examples, all of which suggest that most of the times, objects are embedded in the POST
ed JSON-LD, even when it doesn’t have to. My assumptions from above require objects to never be embedded, or at least require implementations to be ready to handle activities without embedded objects (see below). But that would requires that implementations really do pull all objects referred to in the incoming JSON-LD subgraph.
Impact on current implementations
Current implementations should be good to go with embedded objects:
- if a server receives a subgraph which is not a CBD (but still a connected rooted graph, which is a superset of the CBD and current reality for inbox POSTs), it can safely remove all subjects except the root node and pull them in again, though, so implementations do not need to reject POSTs containing embedded objects
- Implementations that want to analyze the incoming object in detail and rip it apart instead of incorporating it into their view of the graph as-is can do so, either with the embedded object or after pulling
The only requirement is that they must also accept activities without embedded objects, and must pull them if they want to get hold of them. Doing so, we do not require all implementations to be fully graph-aware, but we allow implementations that are fully graph-aware to properly federate.
Proposed spec change
Currently, the ActivityPub spec under “3. Objects” says that servers SHOULD dereference the object to verify it. By the assumptions in this post, this would become a MUST, and a new SHOULD to “replace the object with the dereferenced representation before processing”.
Questions / Discussion
While that’s a pretty staightforward thing to require, there are two open questions:
- Do current implementations do that, especially for the
Create
andUpdate
activities? Can I POST aCreate
orUpdate
of an object, and not embed it? The specification clearly allows that (the spec requires JSON-LD, for fedi’s sake!), but I am afraid that as most implementors chose to ignore JSON-LD, they in fact don’t… - How can we reliably pull objects in that scenario? The object might be under access control. In that case, embedding is the easiest thing to do, but let’s assume we don’t want to embed. I see three options here:
- If the pull fails, then do not pull. Strictly speaking, we don’t need a local representation of an object, and we can still pull the object in once an actor comes around and wants to see it, at which point we know an actor and can use HTTP signatures.
- We could use the owner of the inbox being pushed to as actor and use that to sign the request
- We could use one of the actors the activity is addressed to
So: How do current implementations do that? Do they do it? Ideas ;)?
Also, if this is of interest, and there is some formal way to propose it with some impact (I stumbled over “FEPs” now and then?), I am happy to find someone to mentor me in the formal process.