Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Must you not also verify the key ID in addition to the other verification steps? #58

Open
SorteKanin opened this issue Aug 21, 2024 · 9 comments

Comments

@SorteKanin
Copy link

So the current suggested procedure for verifying a HTTP Signature looks like this:

  1. The HTTP Signature string is the value of the HTTP request's Signature header. If that header is not present, the request has no signature.
  2. Extract the keyId parameter from the HTTP Signature.
  3. Follow the instructions in [[[#how-to-obtain-a-signature-s-public-key]]] to obtain the public key for that keyId.
  4. Extract the encryption algorithm from the signature. If it's hs2019, assume that means rsa-sha256, as described in [[[#survey-of-standards-compliance]]].
  5. Extract the signed headers from the signature. They should ideally include Date, Host, and Content-Type. They may also include Digest and the (request-target) pseudo-header with the target URL.
  6. Generate the expected request body digest:
    1. Generate the SHA-256 hash of the request body.
    2. Base64-encode that hash.
    3. Prefix the base64-encoded hash with the string SHA-256=.
  7. Optionally compare this to the request's Digest header. If they don't match, the signature is invalid, and you can optionally return an informative message in the error response.
  8. Compare the request's Date header to the current time. If they differ significantly, the verification fails. (The standards don't give a concrete time window to use for this comparison. In practice, an hour plus a few minutes buffer in either direction may be a good value, to account for both clock skew and differences in time zone/daylight savings time configuration across systems.)
  9. Follow cavage-12 section 2.5 to check the signature in the Signature header.
  10. If that verification fails, and you're using a locally cached public key, the actor may have rotated their key (see [[[#key-rotation]]]). Go back to step 3, re-fetch the actor and key from their source, and try again.

It's possible that I am missing something, but is it not also necessary to verify that the keyId URL has the same hostname (domain) as the actor performing the activity represented in the body of the request? Or at least somehow verify that the actor sending the activity does in fact control that key?

To explain, imagine you receive this activity:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "summary": "A malicious impersonating note",
  "type": "Create",
  "actor": "https://example.org/users/123",
  "object": {
    "type": "Note",
    "content": "I am a big idiot"
  }
}

The request you got this activity from is signed with a HTTP Signature, but the key ID is just https://shady.website/maliciouskey. When you fetch the key from this URL, you get some actor object with the correct public key and using that key you verify the HTTP Signature on the request. But surely this alone does not verify that this note does in fact come from the user with ID 123 from example.org? I mean anyone could set up a domain, host a public key and send the activity above signed with that key as the key ID.

Am I correct that you need to check this as well or is this somehow verified by the other steps?

@snarfed
Copy link
Collaborator

snarfed commented Aug 21, 2024

Yes! You're absolutely right, you also want to check that the activity's actor owns the keyId that signed the request, and also (for some activity types) that it owns the activity's object, etc.

This all falls under authorization and access control, which are obviously important! The AP authn/authz primer https://www.w3.org/wiki/ActivityPub/Primer/Authentication_Authorization#Actor_ownership discusses them in more detail, but we didn't try to comprehensively cover them in this report. Just covering authentication, ie signing and verifying signatures based on keyIds, felt like more than enough scope. We listed authn/authz as explicit non-goals: https://swicg.github.io/activitypub-http-signature/#non-goals .

@nightpool
Copy link
Collaborator

To be a little more precise, Steps 5-6 in the "How to obtain a siganture's public key" section are designed to handle the case of making sure the actor ID is correct:

  1. If it's a raw Key object, use its controller or owner property as the new key id, jump back to step 2, and repeat. (This is necessary to confirm that the owner actually owns and uses this key.)
  2. Otherwise, the key can't be fetched at this time, and the signature verification fails.
  3. The public key object will be in the actor's publicKey property. If there are multiple values, find the one whose id matches the original keyId.

So, first you have to resolve to an actor. At this point, you need to write down which actor ID you've resolved the key to (e.g. which URL you requested) and then make sure to use this Actor object in any future authorization decisions.

Deciding e.g. whether the actor https://example.com/foo or https://shady.website/maliciousactor or whatever is allowed to sign a request is an authorization decision that's out of scope of this specification (since it may rely on what sort of content it is), but I think we could do a better job of indicating in the spec WHERE the authz decision should go, since the "validated actor ID" output that this algorithm provides can be a little tricky to implement. (it's been the cause of a few CVEs in mastodon, for example, where we don't pass through the right URLs for the right requests in the right places)

@SorteKanin
Copy link
Author

SorteKanin commented Aug 22, 2024

the activity's actor owns the keyId that signed the request

What does this mean in practice? How do I decide whether an actor owns the key? For instance, let's say you get this activity:

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "summary": "Just a note",
  "type": "Create",
  "actor": "https://example.org/users/123",
  "object": {
    "type": "Note",
    "content": "Just a boring note"
  }
}

The HTTP signature on the request has the key ID https://example.org/public_key, which gives this object:

{
  "id": "https://example.org/public_key",
  "owner": "https://example.org/instance_actor",
  "publicKeyPem": "-----BEGIN PUBLIC KEY----- ..."
}

... so then you fetch https://example.org/instance_actor which gives you...

{
  "@context": "https://www.w3.org/ns/activitystreams",
  "type": "Service",
  "id": "https://example.org/instance_actor",
  "publicKey": {
    "id": "https://example.org/public_key",
    "owner": "https://example.org/instance_actor",
    "publicKeyPem": "-----BEGIN PUBLIC KEY----- ..."
  },
  ...
}

This key isn't owned by https://example.org/users/123, the actor who sent the Note. So should I trust it? It is after all using the same hostname for the key ID, so in that sense, the same domain controls the actor as controls the key, I would presume. Is that enough to trust this activity? Or do you need the actor to literally be the key owner? Is it enough that the hostname of the sending actor is the same as the hostname of the owning actor of the key?

@silverpill
Copy link

silverpill commented Aug 22, 2024

@SorteKanin Usually there's a property that indicates an owner. For activities it's actor, for objects it's attributedTo, and for Key it is owner.

So should I trust it?

In general:

  • You can trust if origin is the same, and you shouldn't trust if origin is different.
  • In some cases you may want owner to be exactly the same.

(more info in FEP-c7d3: Ownership)

I think in this particular case it is ok if key owner and activity actor (owner) are different (as long as origin is the same). I shall clarify this in FEP-c7d3 (edit: https://codeberg.org/fediverse/fep/pulls/387).

@nightpool
Copy link
Collaborator

nightpool commented Aug 22, 2024

What does this mean in practice? How do I decide whether an actor owns the key? For instance, let's say you get this activity:

This is authentication, and it's straightforward as long as you follow the steps from the spec very carefully. In your example, the actor who owns the key is https://example.org/instance_actor. Note that this is not the same as the question you go on to ask, which is "can this actor be trusted?" Trust decisions always have two parts—first you have to authenticate the user, and then you have to authorize them. Here are some docs to help understand the difference:

https://auth0.com/docs/get-started/identity-fundamentals/authentication-and-authorization
https://www.onelogin.com/learn/authentication-vs-authorization

This key isn't owned by https://example.org/users/123, the actor who sent the Note. So should I trust it? It is after all using the same hostname for the key ID, so in that sense, the same domain controls the actor as controls the key, I would presume. Is that enough to trust this activity? Or do you need the actor to literally be the key owner? Is it enough that the hostname of the sending actor is the same as the hostname of the owning actor of the key?

You're talking about an authorization decision. You've determined that https://example.org/instance_actor owns the key, and that https://example.org/users/123 created the note. Now you have to determine whether you think https://example.org/instance_actor is authorized to create a note for https://example.org/users/123.

That's something your server has to determine for itself, the HTTP Signatures spec cannot help you, it can only give you the inputs you need to authenticate the request before you make the authorization decision. Here is a doc from the ActivityPub primer representing the current state of the fediverse / best practices:

https://www.w3.org/wiki/ActivityPub/Primer/Authentication_Authorization

I hope that helps, LMK if that answers your question

@SorteKanin
Copy link
Author

SorteKanin commented Aug 22, 2024

Thanks for indulging me so far, this has all been very enlightening.

In some cases you may want owner to be exactly the same.

@silverpill In what cases? Why might I require that?

Now you have to determine whether you think https://example.org/instance_actor is authorized to create a note for https://example.org/users/123.

@nightpool I guess my question is, as long as the actor with the key has the same host as the authoring actor of the note, why would I not authorize the note? Clearly the same instance controls both the actor with the key and the authoring actor, so only that instance could have produced the request (HTTP Signature + Activity content). So I feel like there is no authentication doubt - the user with ID https://example.org/users/123 on that server must have produced this note legitimately. Who am I to say what exact key the instance then uses to sign the delivery request - why should I care? As long as I can see that it comes from that instance, it should be fine.

And as long as there's no authentication doubt, why would there be any authorization doubt? Unless of course I have banned the external user from my instance or something along those lines, but that's a separate issue I would say.

Btw the primer page seems to suggest that the authoring actor and the owner of the key really ought to be the exact same actor, so following that this wouldn't work I think.

@snarfed
Copy link
Collaborator

snarfed commented Aug 22, 2024

as long as the actor with the key has the same host as the authoring actor of the note, why would I not authorize the note?

Many reasons. Off the top of my head:

  • The domain may have changed hands since the note was originally created. If the new owners didn't legitimately take ownership of the original server and its actors' private keys, which they'd demonstrate with a signature from the original actor's key, new activities from that domain should no longer be authorized to modify objects created under the previous owners.
  • In practice in the fediverse right now, keys are per actor, not per host. Yes, in practice, most server admins do have access to actors' private keys, but people are actively working to change that, eg with client signing, portable objects, and other FEPs we've mentioned. @nightpool described a real security problem, but problems can be solved, worked around, etc. We won't necessarily always live in a world of admin-custodial private keys.
  • Admin-custodial keys are a de facto reality that we happen to live in, but it's not specified by any applicable standard that I know of. You can definitely plan for and optimize for this reality, but you don't want to assume it's guaranteed. I try hard not to assume it in my AP code.

@silverpill
Copy link

In what cases? Why might I require that?

@SorteKanin There are two ways to think about ActivityPub: actor-centric and server-centric.
In actor-centric paradigm all interactions happen between actors, and servers only facilitate connections. There, "same owner" policy feels more natural. I believe many existing ActivityPub implementations are built in this paradigm and wouldn't accept a signature created with a key that doesn't belong to activity's actor. This view was also favored by some of ActivityPub specification authors (see OcapPub, for example).
In server-centric paradigm, all interactions happen between servers, and actors are just entities managed by them. There, "same origin" policy is more natural.
Which view is correct? I think today the real (not theoretical) ActivityPub network is mostly server-centric with artificial limitations added here and there due by actor-centric thinking. In the past I worried that nomadic identity would require an actor-centric approach to security, but it turned out to be server-centric too (authority-centric to be precise).

@nightpool
Copy link
Collaborator

The domain may have changed hands since the note was originally created. If the new owners didn't legitimately take ownership of the original server and its actors' private keys, which they'd demonstrate with a signature from the original actor's key, new activities from that domain should no longer be authorized to modify objects created under the previous owners.

@snarfed This doesn't make any sense to me. The actor ID is rooted at a https://example.com url. Whoever controls example.com controls what keys are provided for that actor. I don't think we should recommend that servers try and "keep track" of when / how keys are rotated in this way to try and "second guess" HTTPS.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants