snarfed / bridgy-fed

🌉 A bridge between decentralized social network protocols
https://fed.brid.gy
Creative Commons Zero v1.0 Universal
664 stars 34 forks source link

Authorization checks on incoming activities #566

Closed snarfed closed 5 months ago

snarfed commented 1 year ago

We currently do some authentication - verify HTTP sigs on incoming AP activities, require SSL and check certificates on web fetches - but we don't really do any authorization. We currently accept any activity from any actor and blindly apply it, without checking that the actor is authorized to perform the given activity. We should check any object that they're updating or deleting, that they're the follower on stop-following activities, etc.

===

TODO:

snarfed commented 1 year ago

AP mentions this very lightly for Update:

The receiving server MUST take care to be sure that the Update is authorized to modify its object. At minimum, this may be done by ensuring that the Update and its object are of same origin.

...and Delete:

The side effect of receiving this is that (assuming the object is owned by the sending actor / server) ...

snarfed commented 1 year ago

...but sadly AP doesn't specify any authorization/permission model more comprehensive than those bits.

Supposedly the most common one is "same origin," which says that the actor of any activity that modifies an object must be in the object's attributedTo. That's confusing re the much more well know browser same origin policy, which is about domains/hostnames, not full URLs like AP actor ids. Google finds some of both, and it's hard to distinguish: https://www.google.com/search?q=activitypub+"single+origin" .

(I've also seen hints of a more relaxed model that only requires that the actor is on the same instance as the object's attributedTo, and maybe only warn if it's the same instance but a different user.)

Creates have a similar question too, right? Should we require that the inbox delivery POST for a Create be signed by the object's attributedTo and/or the Create's actor?

Background:

snarfed commented 1 year ago

Another point to check: when we fetch an actor, we should check that its id is the same (final) URL we fetched. If it's not, we should override it with the fetched URL...right? Eg not doing that enabled this attack: https://hackerone.com/reports/461308

snarfed commented 1 year ago

For a second, I worried that this started to re-introduce the req't from some implementations like Mastodon that object ids are on the same domain as their author/actor's id, which made BF itself difficult back in the day, eg https://github.com/snarfed/bridgy-fed/issues/16#issuecomment-424799599 .

Fortunately I don't think that's the case here. This is all about comparing AP actor and author/attributedTo ids themselves; it doesn't care about object/activity ids or WebFinger lookups at all. So in a bridge's case, all of these already have to be on the bridge's domain (eg fed.brid.gy), so we're fine.

snarfed commented 1 year ago

TODO: make task queue handlers admin only, pass authed_as to receive task.

snarfed commented 1 year ago

TODO: update https://www.w3.org/wiki/SocialCG/ActivityPub/Authentication_Authorization with these ^ practices?

snarfed commented 1 year ago

I implemented these, log-only to start, and got some interesting results.

First up: AP inbox forwarding makes this tricky. For example, we got this Create of a reply by @hamlin81@mastodon.social to a post by @NanoRaptor@bitbang.social. It was sent to us by bitbang.social and signed by @NanoRaptor, not by mastodon.social and signed by @hamlin81. Presumably an inbox forward.

OK, so we can't require that the signing user is always the activity's actor/author. Looks like the alternatives are:

  1. If the sig user doesn't match, fetch the activity by its id and use that instead. (This only works if the activity and actor are on the same domain, right?)
  2. If the activity has an LD sig, like Mastodon generates, check that instead. (Sigh.)
{
  "id": "https://mastodon.social/users/hamlin81/statuses/111246155046597039/activity",
  "type": "Create",
  "actor": "https://mastodon.social/users/hamlin81",
  "object": {
    "id": "https://mastodon.social/users/hamlin81/statuses/111246155046597039",
    "type": "Note",
    "inReplyTo": "https://bitbang.social/users/NanoRaptor/statuses/111244176519913170",
    "url": "https://mastodon.social/@hamlin81/111246155046597039",
    "attributedTo": "https://mastodon.social/users/hamlin81",
    "..."
  },
  "signature": {
    "type": "RsaSignature2017",
    "creator": "https://mastodon.social/users/hamlin81#main-key",
    "signatureValue": "eq8DBc2FZFwttF7VgkvRa+1Xwop1q98yj/GjhWbERq8o27i0BBRMMKIJg1sYI/wWdbN2ryw5aGxKCsaeoqJrILZ7SaQ0h1cX6RcSlhexCmRuXqyW7Jbc0bCv12XATJ8s0OlN3tD8wGpG/OxU/iE++MLtF6NsrcYXcZZKhOiUKRu7h02aI3fnRdwBPZmZAZNqVRXp9kUfITv8rV5VoMaTyIrae4V0+V9qyKK+4epT8vTuW70aFD4ScWIbmM9TogMetqhEpy/m3Cv+i9j17wopfdDky2PaYpzSkfaxUvoxMhXyQ0kLllwHHxKUwnAA8e8Va/pDlWPjFlEPDUz/wp6N6g=="
  }
}
snarfed commented 1 year ago

Interesting data point, we get a substantial number of inbox forwards, roughly 2 per min over the last 45m.

snarfed commented 1 year ago

I made a first pass at writing some of this up: https://www.w3.org/wiki/SocialCG/ActivityPub/Authentication_Authorization#Authorization

snarfed commented 1 year ago

Got the ok on that writeup! Next step is to review the logs and implement these checks. After that, ideally I should abstract them across protocols, since this applies to at least some others too, eg web.

snarfed commented 9 months ago

Current status: planning to implement LD Sig verification, but first I need to know how Mastodon canonicalizes the activity JSON before it signs it.

Complete example activity from Mastodon with an LD Sig:

{
  "@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://w3id.org/security/v1",
    {
      "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
      "sensitive": "as:sensitive",
      "Hashtag": "as:Hashtag",
      "movedTo": {
        "@id": "as:movedTo",
        "@type": "@id"
      },
      "alsoKnownAs": {
        "@id": "as:alsoKnownAs",
        "@type": "@id"
      },
      "toot": "http://joinmastodon.org/ns#",
      "Emoji": "toot:Emoji",
      "featured": {
        "@id": "toot:featured",
        "@type": "@id"
      },
      "featuredTags": {
        "@id": "toot:featuredTags",
        "@type": "@id"
      },
      "schema": "http://schema.org#",
      "PropertyValue": "schema:PropertyValue",
      "value": "schema:value",
      "ostatus": "http://ostatus.org#",
      "atomUri": "ostatus:atomUri",
      "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
      "conversation": "ostatus:conversation",
      "focalPoint": {
        "@container": "@list",
        "@id": "toot:focalPoint"
      },
      "blurhash": "toot:blurhash",
      "discoverable": "toot:discoverable",
      "indexable": "toot:indexable",
      "memorial": "toot:memorial",
      "votersCount": "toot:votersCount",
      "Device": "toot:Device",
      "Ed25519Signature": "toot:Ed25519Signature",
      "Ed25519Key": "toot:Ed25519Key",
      "Curve25519Key": "toot:Curve25519Key",
      "EncryptedMessage": "toot:EncryptedMessage",
      "publicKeyBase64": "toot:publicKeyBase64",
      "deviceId": "toot:deviceId",
      "claim": {
        "@type": "@id",
        "@id": "toot:claim"
      },
      "fingerprintKey": {
        "@type": "@id",
        "@id": "toot:fingerprintKey"
      },
      "identityKey": {
        "@type": "@id",
        "@id": "toot:identityKey"
      },
      "devices": {
        "@type": "@id",
        "@id": "toot:devices"
      },
      "messageFranking": "toot:messageFranking",
      "messageType": "toot:messageType",
      "cipherText": "toot:cipherText",
      "suspended": "toot:suspended"
    }
  ],
  "id": "https://libretooth.gr/users/chartrandsaintlouis/statuses/111902659083835796/activity",
  "type": "Create",
  "actor": "https://libretooth.gr/users/chartrandsaintlouis",
  "published": "2024-02-09T17:17:50Z",
  "to": [
    "https://www.w3.org/ns/activitystreams#Public"
  ],
  "cc": [
    "https://libretooth.gr/users/chartrandsaintlouis/followers",
    "https://jasette.facil.services/users/hs0ucy"
  ],
  "object": {
    "id": "https://libretooth.gr/users/chartrandsaintlouis/statuses/111902659083835796",
    "type": "Note",
    "inReplyTo": "https://jasette.facil.services/users/hs0ucy/statuses/111902446198482548",
    "published": "2024-02-09T17:17:50Z",
    "url": "https://libretooth.gr/@chartrandsaintlouis/111902659083835796",
    "attributedTo": "https://libretooth.gr/users/chartrandsaintlouis",
    "to": [
      "https://www.w3.org/ns/activitystreams#Public"
    ],
    "cc": [
      "https://libretooth.gr/users/chartrandsaintlouis/followers",
      "https://jasette.facil.services/users/hs0ucy"
    ],
    "sensitive": false,
    "atomUri": "https://libretooth.gr/users/chartrandsaintlouis/statuses/111902659083835796",
    "inReplyToAtomUri": "https://jasette.facil.services/users/hs0ucy/statuses/111902446198482548",
    "conversation": "tag:libretooth.gr,2024-02-04:objectId=48182059:objectType=Conversation",
    "content": "<p><span class=\"h-card\" translate=\"no\"><a href=\"https://jasette.facil.services/@hs0ucy\" class=\"u-url mention\">@<span>hs0ucy</span></a></span> </p><p>Oui, c&#39;est un livre int\u00e9ressant.</p><p>Bonne lecture !</p>",
    "contentMap": {
      "fr": "<p><span class=\"h-card\" translate=\"no\"><a href=\"https://jasette.facil.services/@hs0ucy\" class=\"u-url mention\">@<span>hs0ucy</span></a></span> </p><p>Oui, c&#39;est un livre int\u00e9ressant.</p><p>Bonne lecture !</p>"
    },
    "attachment": [],
    "tag": [
      {
        "type": "Mention",
        "href": "https://jasette.facil.services/users/hs0ucy",
        "name": "@hs0ucy@jasette.facil.services"
      }
    ],
    "replies": {
      "id": "https://libretooth.gr/users/chartrandsaintlouis/statuses/111902659083835796/replies",
      "type": "Collection",
      "first": {
        "type": "CollectionPage",
        "next": "https://libretooth.gr/users/chartrandsaintlouis/statuses/111902659083835796/replies?only_other_accounts=true&page=true",
        "partOf": "https://libretooth.gr/users/chartrandsaintlouis/statuses/111902659083835796/replies",
        "items": []
      }
    }
  },
  "signature": {
    "type": "RsaSignature2017",
    "creator": "https://libretooth.gr/users/chartrandsaintlouis#main-key",
    "created": "2024-02-09T17:17:50Z",
    "signatureValue": "iz9eLOyliXRazD6++l3VEOaCYHjtWFcsdXXmxOki4DdCMZ0Z1ZYGaCymrjKcgnDJoxlwfc16Y4bIfzx0QI9MnDzumzbb1RHutsVQycFMUPrCkqO2JpZu/fJ2rigdmMNUtAUijPst4sEJOM79ejcyoD4vMrv5qHdFDQmiqTm6fA7whveyFVvHmyW59MgDiF9CfGgddmw/8zCu8k3x7fhEOJjOWg5xMO2obaD4trOrBGfIm5Ize0tHL1PuEFiTEEhf1sOxryeMPUUzouCA17KRqaqglhxwUgsSWb27M2ZW9kiq5qfKN4fZq0CPbwEXIy1IiMnASMV9abv5PxZDCk4pXQ=="
  }
}
snarfed commented 9 months ago

Aha, Claire says

this is defined in app/lib/activitypub/linked_data_signature.rb and app/helpers/jsonld_helper.rb (canonicalize)

snarfed commented 9 months ago

Code is:

graph = RDF::Graph.new << JSON::LD::API.toRdf(json, documentLoader: method(:load_jsonld_context))
graph.dump(:normalize)

The second line comes from https://github.com/ruby-rdf/rdf-normalize, docs at https://ruby-rdf.github.io/rdf-normalize/, JSON ser/de from https://github.com/ruby-rdf/json-ld . I can't find an actual description of the normalization algorithm anywhere there though, so I'm getting set up to run it myself and see.

snarfed commented 5 months ago

Finally getting back to looking at this. I'm now inclined to just skip LD Sigs for now and drop those activities instead of handling them. Need to look at a sample first though to confirm that I'm ok missing them.

snarfed commented 5 months ago

OK! Apart from inbox forwarding, one source of activities we're getting that don't pass authz is Guppe Groups. Looks like they similarly forward activities, with a new HTTP Sig from the group's actor, but there's no LD Sig from the original actor, so we can't verify the activities.

This example included HTTP Sig by the group AP actor https://a.gup.pe/u/allstartrek:

{
  "type": "Create",
  "id": "https://mindly.social/users/joewynne/statuses/112499304245297194/activity",
  "actor": "https://mindly.social/users/joewynne",
  "cc": [
    "https://mindly.social/users/joewynne/followers",
    "https://a.gup.pe/u/allstartrek",
    "https://a.gup.pe/u/allstartrek/followers"
  ],
  "object": {
    "type": "Note",
    "id": "https://mindly.social/users/joewynne/statuses/112499304245297194",
    "url": "https://mindly.social/@joewynne/112499304245297194"
    "attributedTo": "https://mindly.social/users/joewynne",
    "to": "as:Public",
    "cc": [
      "https://mindly.social/users/joewynne/followers",
      "https://a.gup.pe/u/allstartrek",
      "https://a.gup.pe/u/allstartrek/followers"
    ],
    "content": "...",
    "published": "2024-05-25T02:12:33Z",
  },
  "published": "2024-05-25T02:12:33Z",
  "to": "as:Public"
}
snarfed commented 5 months ago

^ filed https://github.com/immers-space/guppe/issues/106

snarfed commented 5 months ago

Next up! GoToSocial. Its actors' publicKey.ids are a separate URL, on a sub-path of the actor id, that serve a minimal version of the actor (without requiring a signed GET) that only include the key. Totally fine, we just need to handle this in our sig verification.

Example: actor https://social.chriswb.dev/users/chrisw_b :

{
  "type": "Person",
  "id": "https://social.chriswb.dev/users/chrisw_b",
  "url": "https://social.chriswb.dev/@chrisw_b"
  "alsoKnownAs": ["https://teal.social/users/chrisw_b"],
  "name": "chris b 💖",
  "preferredUsername": "chrisw_b",
  "publicKey": {
    "id": "https://social.chriswb.dev/users/chrisw_b/main-key",
    "owner": "https://social.chriswb.dev/users/chrisw_b",
    "publicKeyPem": "..."
  },
  "..."
}

...and publicKey.id https://social.chriswb.dev/users/chrisw_b/main-key :

{
   "type" : "Person"
   "id" : "https://social.chriswb.dev/users/chrisw_b",
   "preferredUsername" : "chrisw_b",
   "publicKey" : {
      "id" : "https://social.chriswb.dev/users/chrisw_b/main-key",
      "owner" : "https://social.chriswb.dev/users/chrisw_b",
      "publicKeyPem" : "..."
   },
}
qazmlp commented 5 months ago

^ filed https://github.com/immers-space/guppe/issues/106

IIrc this won't work in all cases though, since as far as I've heard mentioned it's possible to switch some servers into something called "secure mode" so that they don't serve forwardable JSON-LD signatures on content. But I very much haven't read up on this myself.

I think a good fallback for this case would be to re-fetch from the id to authenticate, either throwing away the forwarded data or (ideally) checking that it all matches so that there can be no disagreement about what was boosted.

(I really hope either that or at least the fresh fetch is what Guppe does in this situation.)

snarfed commented 5 months ago

Next: Bluesky app.bsky.feed.generator records. They have their own DIDs, which aren't (necessarily) unique or the same as the repo that they get published in, eg all feeds from SkyFeed get the same DID, did:web:skyfeed.me. Example from the did:plc:ffklbxnlk3kpwkyr4oxngp5q repo:

{
  "$type": "app.bsky.feed.generator",
  "did": "did:web:skyfeed.me",
  "avatar": "...",
  "createdAt": "2024-05-28T13:14:10.044Z",
  "description": "...",
  "displayName": "\u304a\u3046\u3061\u306e\u9ce5\u90e8",
  "skyfeedBuilder": "...",
}
snarfed commented 5 months ago

Getting close! I've done a ton of work on this ^ over the last few days. I've covered over all of the cases seen in the wild over the last two weeks, except for one in RSS/Atom ingest that I'll fix later in #829. Otherwise, I'll watch it log-only for a couple more days, see if there's anything new, then turn it on.

snarfed commented 5 months ago

^ Got a few more hits over the last four days, besides RSS/Atom ingest #829, but not many. Details below, interestingly they all involve momostr.pink. In any case, I think I'm ready to turn this on, as soon as I can update the tests to handle it.

Auth: https://momostr.pink/users/npub1dww6jgxykmkt7tqjqx985tg58dxlm7v83sa743578xa4j7zpe3hql6pdnf isn't https://momostr.pink/notes/note1rlsjyulj9x39s6q3q82n00suvfcjcyyrnky04v0fymu9cwkr2c2stueajd 's author or actor: ['https://momostr.pink/users/npub1r0rs5q2gk0e3dk3nlc7gnu378ec6cnlenqp8a3cjhyzu6f8k5sgs4sq9ac', 'https://momostr.pink/notes/note1rlsjyulj9x39s6q3q82n00suvfcjcyyrnky04v0fymu9cwkr2c2stueajd']
Auth: would cowardly refuse to overwrite bsky.brid.gy/followers#accept-https://momostr.pink/follow/npub1qnmamgyup683z9ehn40jrdgryjhn8qlpntwzqsrk8r80n3xspdrq4r245g/https%3A%2F%2Fbsky%2Ebrid%2Egy%2Fbsky%2Ebrid%2Egy without checking actor
Auth: would cowardly refuse to overwrite did:plc:p2cp5gopk7mgjegy6wadk3ep/followers#accept-https://momostr.pink/follow/npub1k979np6dcpwh7mkfwk7wq3msezml48fh7wksp9hakakf8pwk3y5qhdz7te/https%3A%2F%2Fbsky%2Ebrid%2Egy%2Fap%2Fdid%3Aplc%3Ap2cp5gopk7mgjegy6wadk3ep without checking actor
Auth: would cowardly refuse to overwrite did:plc:ak6xsotudhfibusxxtiqan6b/followers#accept-https://momostr.pink/follow/npub1qnmamgyup683z9ehn40jrdgryjhn8qlpntwzqsrk8r80n3xspdrq4r245g/https%3A%2F%2Fbsky%2Ebrid%2Egy%2Fap%2Fdid%3Aplc%3Aak6xsotudhfibusxxtiqan6b without checking actor
Auth: would cowardly refuse to overwrite bsky.brid.gy/followers#accept-https://momostr.pink/follow/npub1qnmamgyup683z9ehn40jrdgryjhn8qlpntwzqsrk8r80n3xspdrq4r245g/https%3A%2F%2Fbsky%2Ebrid%2Egy%2Fbsky%2Ebrid%2Egy without checking actor
Auth: would cowardly refuse to overwrite bsky.brid.gy/followers#accept-https://momostr.pink/follow/npub1uf9a0mvyvx7c449476h7e5zy5xd5yfcl7vpxcsz5g0udas2nht8qd55400/https%3A%2F%2Fbsky%2Ebrid%2Egy%2Fbsky%2Ebrid%2Egy without checking actor
snarfed commented 5 months ago

It's alive, it's alive!

snarfed commented 1 month ago

Re verifying LD Sigs and JSON canonicalization, JSON Canonicalization Scheme [RFC8785] is a useful reference. No clue if that's what Mastodon does though. (I kind of doubt it, but I don't know.)