Explicitly allowing inbox POSTs to be CBDs

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 POSTed 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 and Update activities? Can I POST a Create or Update 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.

3 Likes

A quick test showed that Mastodon, at least, accepts a Create activity without the object embedded, and dereferences it immediately using the key of the inbox’s actor. Hooray!

Always fetching activity/object ids like this was recently discussed on the FediDevs matrix chat yesterday too, for a different purpose: using SSL as an alternative to signatures to verify that an activity actually came from a given instance and wasn’t spoofed.

One big difficulty with this is scaling. AP is already a very chatty protocol, especially given the volume of Deletes Mastodon currently sends. This would increase the overall volume of network requests by a couple orders of magnitude of more.

(You’ll also find resistance to further requiring JSON-LD processing, as you mentioned. The majority of the fediverse currently doesn’t do JSON-LD and still happy interoperates. Opinions differ, but personally I think that’s a feature, not a bug.)

As it stands, at least Mastodon does already do a pull, no matter whether the object is embedded or not (as recommended by the spec).

The spec, as it stands, does not require objects of activities to be stored locally at all, so jsut noting the activity and ignoring the payload is perfectly fine (it will then have to be dereferenced once an actor tries to access it, which the spec already dictates).

Not doing JSON-LD in effect breaks the spec. I understand that people want to go the easy route and ignore it, but that does not work out as long as the spec allows to produce JSON-LD. If I am allowed to produce JSON-LD to federate, then all other serves must be expected to handle it. But, given a good serialization profile, that’s a non-issue here. This post is about dereferencing the object of an activity, nothing more.

There are many different activity and object types and shapes with bare string ids in various places. Mastodon does fetch and hydrate some of those ids, but definitely not all, and I’m not even sure it’s the majority.

I know you believe this strongly! But you should know that it’s a minority opinion in the AP development community. A number of people here may like JSON-LD, but not many agree with this “JSON-LD or broken” stance, including the spec authors and editors! If they agreed, they wouldn’t have said “JSON-LD is nice but optional” so clearly in the spec.

1 Like

When I read that section I see:

No particular mechanism for verification is authoritatively specified by this document

They do mention dereferencing as one way to verify an object, but they also mention others (e.g., signature-based verification).

It makes sense to me to deference an object of a Create Activity. Example 7 in the spec is for a Like Activity and they recommend dereferencing. It’s not so clear to me that this is necessary for a Like.

Sure. If another server sends an embedded object, do as you please.

My proposal only covers cases where another server decides to not embed the object, and I ask this case to be explicitly supported, not other cases become unsupported.

Hey there! I just heard about Vocata yesterday and was very excited. I’ve been thinking of building something like this off an on for a while, but in rust (because of course). I would love to ask about your implementation sometime. It seems like a lot of the work is done by rdflib (for which there is no rust equivalent), but I definitely agree that leaning into JSON-LD is a good idea.

I think as long as outbound activities are simple and represent a CBD, it should be fairly easy for servers that do not understand JSON-LD to handle the activities. Mastodon’s behavior (signing fetches with the inbox’s actor’s key) makes sense to me.

4 Likes

You are allowed to produce only a limited subset of JSON-LD as defined by the ActivityStreams serialization format. This subset was specifically designed to be processable by pure JSON and JSON-LD consumers.

I’m not sure I fully have my head around everything this post is saying (is it just “don’t use LD-signatures for embedding verifiable objects from a different domain?”) but in general I think it’s important to keep the requirements of as-core firmly in mind when discussing this issue.

EDIT: oh, i see this proposal would forbid subobjects from being included even from the same domain. I don’t think that restriction makes sense, the domain is responsible for handling its own access control internally and validating any objects before it sends them out over the wire.

1 Like

Yes, and this subset has no provision about embedding objects or not.

No, neither of that is the intention. The intention is to allow senders to not send embedded objects – if a sender wants to embed, then good. But a receiver must also accept and properly handle an incoming activity that does not embed the object.

By the way, is there a formal definition of this format somewhere?

Because the JSON-LD profile referenced in the ActivityPub spec alone does not produce the serialization format as expected by major implementations. Producing that involves at least compaction, while forbidding the otherwise allowed @graph top-level attribute, and excluding selected attributes from array compaction (but not all). Is that formally described somewhere?

Ah, okay, I seem to have misinterpreted your original post. This is already part of the expected practice and some apps do federate in this way, so I don’t expect any issues there. I don’t think any specs explicitly spell this out, but, yes, it’s a natural consequence of allowing for referencing objects using their URI. Apps can always choose when to embed objects vs when to reference a URI

Yes: https://www.w3.org/TR/activitystreams-core/#jsonld. ActivityPub is just the semantic layer on top of the ActivityStreams 2.0 format, which is why it doesn’t include any definition of serialization, just referencing the ActivityStreams spec normatively

The implementation I am working on NEVER embeds anything, it only works with IRI to the actual object. Much easier implementation then.

Seems like you came to a similar solution that I did.

1 Like

Yep, I know that. But I can tell for sure that at least Mastodon cannot handle quite a few documents that perfectly match this specification.

But I conclude that’s a bug.

And for the core question here: 3.1 of the spec demonstrates that not embedding an object is perfectly ok. So I think it’s reasonable to expect implementations to accept and handle it.

True! And in practice, for many objects, current AP implementations do accept and handle bare ids instead of embedded objects. If anything, sadly they more often fail on embedded objects themselves! Multiple examples in Issues · snarfed/bridgy-fed · GitHub and Help improving federation between Lemmy and other projects - #17 by nutomic

1 Like

An interop warning for developers implementing ActivityPub…

Yesterday I learned (the hard way) that Mastodon currently has a bug that causes it to require an embedded Object for a Create activity. Later, I found this…

ActivityPub Create Activity with object reference · Issue #19102 · mastodon/mastodon (github.com)

Mastodon dereferences the provided object IRI but then doesn’t use the dereferenced object to create the Status it attempts to store (it uses the dereferenced data in other places). I’m not a Ruby/Mastodon developer but it seems it would be easy to fix (just replace @json["object"] with the dereferenced object (?).

These are nice idea, unfortunately the vocata docs are currently down (sometimes references fail!)

Firstly I completely agree with a standards compliant social graph being more powerful. Facebook have this, and the open social web is behind.

Let me just say that to be standards compliant

This

  "id": "https://vocatadev.pagekite.me/users/tester1",

Should not be a document, it should be a Person denoted with a #

  "id": "https://vocatadev.pagekite.me/users/tester1#me",

e.g. #me. Not everyone has to be standards compliant, that’s a trade off. But if you want to be, you might as well take the more useful parts such as document vs thing separation.

CBD looks nice, and seems a way to modernize the processing of objects a bit. I like it. It’s up to implementers how much of this they want to take on.

Getting the social graph right and consistent I think is probably the biggest upgrade we can do, otherwise fediverse struggles to talk to itself and talking to the outside world is even harder.

1 Like

I am currently discussing with the Codeberg team why this is the case. Meanwhile, they are in the docs/ directory of the source repository.

I may have missed something from the original thread context, but why is the “#me” fragment required? And why should the first identifier not reference a document?

To be fully compatible with RDF you need to separate Things from documents. That’s what Solid does, for example.

In a nutshell it allows you to have more than one item on a page.

It is possible to cut a corner here, but you dont gain much, and exclude whole eco systems that do use fragment ids.

But then why go to the effort of following the standard without getting the full rewards. A standards based approach be very interesting, because it’s not fully been tried yet on the fediverse. The biggest benefit would be full interop with other systems outside the fediverse that follow linked data standards.