[[ 🏗 =br6Go Vervis ]] :: [📥 Inbox] [📤 Outbox] [🐤 Followers] [🤝 Collaborators] [🐛 Tickets] [📖 Wiki] [✏ Edit]

HTTP Signatures and inbox forwarding

Created on 2019-04-23 by ~fr33domlover
[🐤 Followers] [⤴ Dependencies] [⤷ Dependants] [✋ Claim requests] [✏ Edit]

I don’t know how I missed this issue for so long. I never even saw anyone mention it. I’d like to address the problem of verifying HTTP signatures on inbox-forwarded activities.

Imagine you’re alice@A and you receive an activity whose as:actor is bob@B. There are 2 ways you can receive this activity:

  1. Bob addresses it to you, or to a collection owned by server B in which you’re listed, and server B sends you the activity
  2. Bob addresses it to a collection owned by server C, and server C then does inbox forwarding, delivering to you because you’re listed in that collection

When you get the activity in case (1), directly from B, everything works great: You verify the HTTP sig, which includes the Host and (request-target) headers, letting you know that server B is addressing this request specifically to you, and wants you to receive it. It’s not a hijacked request intended for someone else. You can tell because (request-target) contains the local path, and together with Host they make the whole request URL.

In case (2), though, there’s a problem: If you get the activity from server C, how can you determine that server B intended you to have it?

Ideas

  1. Don’t sign the Host and (request-target) headers
  2. Don’t care if they key actor and activity actor don’t match
  3. If key actor c@C and activity actor bob@B are on different hosts, check if the activity audience contains collections owned by server C, possibly checking whether we alice@A are aware of being members of those collections
  4. When server B sends to server C, it adds a special signed header specifying it grants server C permission to forward; when server C forwards, it writes the Host, (request-target) and Signature headers to differently named headers, and makes its own HTTP signature; when we receive the signature, we notice the mismatch and check that special header, verifying both signatures

The ActivityPub spec doesn’t say exactly how to do inbox forwarding, it just says “target and deliver”. However, sending the original activity as-is creates a problem: There’s no proof of ownership of the activity, because the server sending it and the server hosting it would be different. One simple solution is to send an Announce activity linking to the original, and somehow use the addressing to signal that it’s being inbox-forwarded. The recipient server then downloads the original object from its owning server.

Other than the annoyance of downloading the activity, I see one possible issue with this approach: You lose the knowledge that the activity is specifically meant to be send to you. I’m not entirely sure though, whether this information is important. I want to try an example and check.

You’re alice@A and you receive an Announce activity from bob@B. The Announce’s object is an activity hosted by server C. What you don’t know, is that Bob never intended you to receive this activity. Server C just decided to spread it around to people totally unrelated to it. And now it’s annoyingly stealing your attention. However, you aren’t even aware that you weren’t supposed to receive it: You just see it in your timeline just like many other activities.

The method with the combined signatures would prevent that. When I publish a ticket comment on a remote ticket, I deliver to direct recipient users and to the target project, but when I deliver to the project I add a special signed header, saying that I allow the project actor to forward the activity. Even if it sends an Announce, it can put my little special header as a field in the Announce activity. That way, only the server owning the project can do inbox forwarding, with the project actor specifically as the announcer.

Is it possible to bypass this little protection? If yes, providing this protection doesn’t actually have any benefit. Let’s see.

Suppose I’m server C, I have a copy of the activity that bob@B authored. If I’m not hosting the project, the protection would prevent me from doing inbox forwarding, but what if I just did a regular announce? I could spread a million regular Announces of the ticket comment. Using whichever local actors I wish. And if I’m the server hosting the project, I’m free to do fake inbox forwarding.

Assume server A does accept and happily handle regular Announce activities. I guess under this condition, the protection makes no difference. But what if server A doesn’t deal with regular Announce activities? It wants to receive only inbox forwarding. Without the protection, anyone can send Alice an inbox-forwarding Announce of Bob’s post. With protection, only server C, owning the collections, can do that, being given explicit permission by Bob. Am I right? But even then, if someone wants to spam my inbox with stuff, can’t they just send out many useless Create Note?

Decision

After a long discussion on IRC, here’s a plan.

When doing AP inbox forwarding, Vervis will:

When receiving an activity in an inbox, Vervis will:

How would the signature work?

With every POST request, we also send an HTTP signature. However, that signature is meant just for that request, and it signs HTTP headers. It’s not obvious how forward the request with that signature still attached and possible to verify. So, when we intend to allow forwarding, we’ll attach a 2nd signature, as follows. It’s still possible to ignore that signature and GET the activity instead, but using the signature, when available, allows to avoid that GET step.

When a user publishes a ticket comment on a remote ticket, we deliver it to the project under which that ticket lives, and to any direct recipients listed in the activity. And when we deliver to the project, we want it to do the inbox forwarding, because it basically conceptually owns the 2 collections we’re addressing. We want to give it a signature it can forward along with the actual activity.

When we send the activity to the project’s inbox, we include an HTTP header ActivityPub-Signature. That header’s value is the base64 encoding of a cryptographic signature made on a value formed by concatenating 5 things:

  1. The string “SHA-256” (without the quotes)
  2. ASCII equal character ‘=’
  3. base64 encoding of the SHA-256 of the request body.
  4. ASCII period character ‘.’
  5. ID URI of the actor to whose inbox we’re sending the activity

The cryptographic signature is made with the same key as the HTTP Signature.

When the target server receives the request and does inbox forwarding, it sends the activity with a new HTTP signature of its own, and it preserves the special signature from the original request by:

  1. Putting the keyId of that request in the ActivityPub-Forwarded-KeyId header
  2. Putting the signature from the ActivityPub-Signature header in the ActivityPub-Forwarded-Signature header

Note that due to inbox forwarding it’s possible that a server receives an activity it owns! We need to allow that in the YesodHttpSig instance, and to support that in handleSharerInbox by inserting an InboxItemLocal rather than InboxItemRemote. We should probably verify our own signature there to be sure the forwarding is okay, and then I suppose find the LocalMessageId in our database.

Q: What happens if, by the time the activity is forwarded, the key used for the original signature is unavailable or expires?

A: GET the activity ID URI to have a proof of authenticity. Another option would be to allow different keys, and let the forwarded signature’s key stay for a long time, weeks and months, but let’s not go there yet.

Q: If we receive an activity in which activity actor and key actor are different, but on the same server, should we check for forwarding headers and process it?

A: If a user makes a comment on a ticket on the same server, then the C2S outbox handler make sure to deliver the activity to the ticket followers. This currently happens by delivering the activity from the user as the key’s actor, not the project, and I guess it can work like this in general, so, there’s no need to handle the case of different same-host actors. No harm in it, but currently it’s never being generated, so if we see it, it may indicate a bug in the code

Q: What if instead of a custom input string, we use a HTTP signatures, except with a custom header name? Could that equally fill the requirements?

A: Let’s see. The idea is as follows: When an activity is sent to someone for forwarding, their actor URI is placed in an ActivityPub-Forwarder header, and in addition to the regular HTTP sig in Signature, another signature is sent in the ForwardedSignature header. That signature signs at least the Digest and the ActivityPub-Forwarder, and probably nothing more. The receiving server does inbox forwarding by sending the activity with a new fresh HTTP sig of its own in Signature, but it sends some headers intact: Digest, ActivityPub-Forwarder, and ForwardedSignature. It’s possible that a different header name is used here and not ForwardedSignature, so that forwarder and forwarding-target see differently named headers, but the question is, do we benefit from them being different? Let’s try to check.

When I receive an activity and the activity’s actor is identical to the sender’s actor, maybe somehow it’s possible that someone is inbox-forwarding their own activity? But even if that’s the case, makes no difference to me. I accept the activity. If ActivityPub-Forwarder header is present, and set to my actor ID, then I can do inbox forwarding.

If the activity actor and sender actor are on different hosts, I reject the activity unless it’s forwarded. How do I know? I check the HTTP sig in ForwardedSignature! If it exists and it signs at least Digest and ActivityPub-Forwarder, and ActivityPub-Forwarder is identical to the sender actor (which, as I said, isn’t the activity actor), then I take it for valid forwarding and accept the activity.

Sounds good so far! But is there any scenario in which I might get confused between receiving a forwarded activity and needing to forward an activity? In such cases what if I determine the wrong author, or I forward something I shouldn’t, or I don’t forward when I should?

So, suppose I receive an activity where sender host and activity host are the same. What if it’s some weird case where the activity is forwarded? In the case I’m meant to do forwarding, the ActivityPub-Forwarder header should be me. In the case the activity is forwarded, that header should specify the actor from whom I received the activity, the sender actor. So, here’s the rule. If I receive an activity from me to myself, I ignore it, I think the AP spec mentions that, either way I guess I’d want to ignore. But otherwise, if sender isn’t me: If ActivityPub-Forwarder is me, I should do forwarding. If it’s not me, someone did forwarding, so it should be the actor from whom I received. I can just ignore the ForwardedSignature in that case, but it would be nice to log a warning for debugging:

Now, the other case is that the hosts are different. So I require to see ActivityPub-Forwarder and ForwardedSignature headers. The usual possibility is that I got a forwarded activity: If the ActivityPub-Forwarder is the sender actor, then I got valid forwarding and I accept. What if I’m being asked to forward though? Let’s say alice@A is sending me an activity by bob@B, and I’m chris@C. If ActivityPub-Forwarder is the sender actor-

Hmmm this is feeling so complicated. It basically sounds okay, let’s go forward with this and change header names later if needed.

NOTE: When I send an activity for forwarding, I can sign ForwardedSignature with any of my keys, not necessarily the same one as Signature. When I receive an activity for forwarding, I can do forwarding without checking or verifying the ForwardedSignature at all, although I could. When I receive a forwarded activity, say, from alice@A who is the ActivityPub-Forwarder and ActivityPub-Actor, but activity author is bob@B, I need to make sure that the ForwardedSignature is made with a key owned by bob@B. Otherwise, how do I know Bob authorized the forwarding? It could be anyone else authorizing it, right? Bob has to list the key, and the key has to list Bob as owner, or be an instance-scope key for server B.

DECISION: At least for now, for safety and for forward compatibility with forward chains (you receive a forwarded message and forward it further), let’s use separate names:

Status: Closed on 2019-05-08 by ~fr33domlover

Custom fields

Discussion

new topic
[See JSON]