using MongoDB.Entities; using PrivaPub.Federation.Actors; using PrivaPub.Federation.Objects; using PrivaPub.Models.Post; using System.Text.Json.Nodes; using static PrivaPub.Federation.Objects.ActivityJson; using PostEntity = PrivaPub.Models.Post.Post; namespace PrivaPub.Federation.Inbox { // Inbox forwarding (ActivityPub 7.1.2): a server passes on an activity about a thread it holds to the thread's followers, // signed with its own key. Mastodon forwards the replies to its accounts' posts, and their deletions; Friendica every // activity in its threads. The signature proves who passed it on, never who wrote it, so nothing in it is believed: a // Create or an Update is taken as its object reads at the actor's origin now, a Delete once that origin says the object // is gone, and anything else is let go (a vote or a follow cannot be checked against its origin). An LD signature // (RsaSignature2017) would prove the author, but needs JSON-LD. An integrity proof (FEP-8b32, eddsa-jcs-2022) by one of // the actor's Ed25519 keys does: such an activity is taken as forwarded, read again from nowhere. public static class Forwarded { // whether the activity carries a proof made by one of the actor's own keys (FEP-521a) public static bool Proven(JsonNode activity, Models.User.ForeignAvatar actor) => activity is JsonObject document && document["proof"] is JsonObject proof && Value(proof, "verificationMethod") is { } method && actor.AssertionKeys.FirstOrDefault(k => k.Id == method) is { } key && Signing.IntegrityProofs.Verify(document, Convert.FromBase64String(key.PublicKey)); // whether it names a key of the actor's own that we do not hold (the actor was read before it had one) public static bool NamesUnknownKey(JsonNode activity, Models.User.ForeignAvatar actor) => activity is JsonObject document && document["proof"] is JsonObject proof && Value(proof, "verificationMethod") is { } method && method.StartsWith(actor.ActorURI + "#", StringComparison.Ordinal) && actor.AssertionKeys.All(k => k.Id != method); // what may be taken from a forwarder: a post of the activity's actor, created, edited or deleted public static bool Takeable(string type, JsonNode activity, string actorUri) { var objectUri = Id(activity["object"]); return type is "Create" or "Update" or "Delete" && objectUri != default && objectUri != actorUri && Origin.Same(objectUri, actorUri); } // the activity as its origin vouches for it, or why it is dropped public static async Task<(JsonNode Activity, string Drop)> Confirm(JsonNode activity, string type, string actorUri, IRemoteActorService remoteActors, CancellationToken token) { var objectUri = Id(activity["object"]); var trusted = new JsonObject { ["id"] = Id(activity), ["type"] = type, ["actor"] = actorUri }; if (type == "Delete") { // only a copy anyone may read: the origin answers 404 for a followers-only post to our instance actor too, // and the author's own server tells its recipients of a deletion var held = await DB.Default.Find().Match(p => p.ObjectURI == objectUri && p.ActorURI == actorUri).ExecuteFirstAsync(token); if (held != default && held.Visibility is not (PostVisibility.Public or PostVisibility.Unlisted)) return (default, "forwarded-private"); if (!await remoteActors.IsGone(objectUri, token)) return (default, "forwarded-not-gone"); trusted["object"] = objectUri; return (trusted, default); } if (type == "Create") { // the Create handler reads it again from its origin, and records that it did trusted["object"] = objectUri; return (trusted, default); } using var fetched = await remoteActors.FetchObject(objectUri, token); if (fetched == default) return (default, "fetch-failed"); var node = JsonNode.Parse(fetched.Root.GetRawText()); if (!Origin.Same(Id(node), actorUri)) return (default, "cross-origin"); trusted["object"] = node; return (trusted, default); } } }