AP networking
Some more notes about networking between federated ActivityPub servers. A brief overview covered a fairly typical exchange to transfer a post from one server to another. Here’s a few more details, how following works, and some more notes about addressing and delivery.
(These are some rough notes, but I think it’s better to publish first and revise later instead of sitting on it forever.)
webfinger
WebFinger isn’t part of ActivityPub, but it’s more or less required for user discovery (mapping @user handles to AP actors). Some implementations won’t even talk to you without it (even if you already specify the AP actor URL).
We start with a fixed URL: /.well-known/webfinger
and make a GET request, with a query parameter of resource for an acct URN.
Example: /.well-known/webfinger?resource=acct:tedu@honk.tedunangst.com
You should also support the actor URL directly: /.well-known/webfinger?resource=acct:https://honk.tedunangst.com/u/tedu
following
To follow somebody, we send them a Follow request, and then their server replies with an Accept. (Or a Reject.) Note that it’s important to deliver the Accept, otherwise the remote server won’t know their user is actually following you. You can send them messages, but they may be ignored.
In this exchange, h2@honk2 will follow test@honktest.
{
"@context": "https://www.w3.org/ns/activitystreams",
"actor": "https://honk2/u/h2",
"id": "https://honk2/u/h2/sub/https%3A%2F%2Fhonktest%2Fu%2Ftest",
"object": "https://honktest/u/test",
"published": "2019-08-01T13:17:03Z",
"to": "https://honktest/u/test",
"type": "Follow"
}
The id
field here is required, but won’t be referenced. The important fields are the actor
and object
.
Now it’s time to Accept. We quote the Follow object back in the reply.
{
"@context": "https://www.w3.org/ns/activitystreams",
"actor": "https://honktest/u/test",
"id": "https://honktest/u/test/dub/h2",
"object": {
"@context": "https://www.w3.org/ns/activitystreams",
"actor": "https://honk2/u/h2",
"id": "https://honk2/u/h2/sub/https%3A%2F%2Fhonktest%2Fu%2Ftest",
"object": "https://honktest/u/test",
"published": "2019-08-01T13:17:03Z",
"to": "https://honktest/u/test",
"type": "Follow"
},
"published": "2019-08-01T13:17:03Z",
"to": "https://honk2/u/h2",
"type": "Accept"
}
Later, we want to unfollow somebody. That’s done with an Undo.
{
"@context": "https://www.w3.org/ns/activitystreams",
"actor": "https://honk2/u/h2",
"id": "https://honk2/u/h2/unsub/https%3A%2F%2Fhonktest%2Fu%2Ftest",
"object": {
"actor": "https://honk2/u/h2",
"id": "https://honk2/u/h2/sub/https%3A%2F%2Fhonktest%2Fu%2Ftest",
"object": "https://honktest/u/test",
"to": "https://honktest/u/test",
"type": "Follow"
},
"published": "2019-08-01T13:20:20Z",
"to": "https://honktest/u/test",
"type": "Undo"
}
Again, we quote the original Follow request.
Compatibility note: In theory, one might hope to simply Accept and Undo by specifying the id
of the Follow. That doesn’t work in practice, because not every server stores a copy of the original object in its database. Instead, all that’s saved is a relationship row, X follows Y, and so reversing that operation requires repeating the original request.
delivery
Every actor has an inbox
. Optionally, several actors on an instance may share a sharedInbox
. If a message is going to several recipients, it may be convenient to drop it in the shared box and let the remote server sort it out. But this introduces its own problems.
One may consider an analogy with email and SMTP. The misc@openbsd list has several gmail subscribers. When it delivers a message to gmail, it makes an SMTP connection, and then a series of RCPT TO commands for each recipient, and then delivers the message. ActivityPub is more like the openbsd lists server connects to gmail, says RCPT TO misc@openbsd, and then lets the gmail server figure out who’s subscribed.
This is obviously problematic if the remote server has a different idea of each user’s subscription status. You have heard that ActivityPub is distributed? Distributed is computer speak for there will be different ideas. It’s not necessarily possible to know where a message will go when it gets dropped off in the shared inbox.
dual stack
ActivityPub doesn’t work well through NAT or any other kind of asymmetric network view. Except in the most trivial exchanges, both servers are going to connect to each other.
http signatures
Every POST needs to be signed. I keep meaning to write this up, but it’s kinda mundane, if a bit tricky to get right, and frustrating to debug. The important thing for interop is to keep it simple, RSA and SHA256. You want to do better. You can’t do better.
A common mistake in deployments is losing the Host header. Most signed HTTP requests will include the Host header in the signature. Most AP implementations are located behind a reverse proxy. Not all reverse proxies forward the Host header from the front end to the back end by default. (nginx does not.)
More interesting are all the things one should check for agreement.
forgery
You receive a signed request (POST) from a remote server, delivering a Create activity of a Note. Who did this? What identify checks can or should we perform? Here’s a list of identities.
1. The HTTP signature keyId
field has a keyname, a URL.
2. That URL should contain an RSA public key, in JSON form, with an owner
field.
3. The owner should be an AP actor, such as a Person. It has an id
field.
4. The Create activity has an actor
property.
5. The Note has an attributedTo
property.
6. The Create and Note each have an id
property as well.
At a minimum, all those things should have the same domain. example.com or honk.tedunangst.com or whatever. Yes, that’s a lot of things to check.
Some of them should probably be exact matches, like the key owner and the actor should be identical, but in practice, same server means same security domain.
Anyway, this is probably better than SMTP forgery. The equivalent of DKIM is kinda built in, although the mechanism is different.
future
There are plans afoot to replace or augment HTTP signatures with a better capability model. Talk to cwebber. Nothing more about that here since at the present time, it’s neither necessary nor sufficient.