Conversations are keyed by who is in them, and circles stay home
S8: a direct message joins a conversation only when its participants are exactly that conversation's members, found through a new DmGroup.ParticipantsKey (a hash of the sorted members). A remote context no longer decides anything: it let anyone who knew a conversation's context post into it, and joining by context while dropping a participant would have shown a reply to someone it was not addressed to. A context is kept only when it is on the author's origin. Sending a DM to the same people again reuses their conversation instead of opening a new one. S9: Group.Kind is Circle or Community. A circle is not a federated actor: its actor, collections, WebFinger and inbox answer 404, a remote Follow is refused, and posts in it are IsLocalOnly - never delivered, never in an outbox, never served. Communities keep today's behaviour until P4. Migration _003 makes every existing group a circle, marks their posts local-only and backfills the conversation keys. End-to-end inbox tests sign real deliveries from a fake peer: a context injection, a forged activity id, a note attributed to someone else, a cross-origin object, a bad signature, junk bodies and a Follow of a circle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
This commit is contained in:
1 parent
bf7c88ce71
commit
2eb2a63f1e
14 files changed
+367
-34
No files matched your search
@@ -0,0 +1,65 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
|
||||
using PrivaPub.Federation.Signing;
|
||||
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace PrivaPub.Tests.Support
|
||||
{
|
||||
public sealed class RemoteActor
|
||||
{
|
||||
readonly RSA _key = RSA.Create(2048);
|
||||
|
||||
public RemoteActor(Peer peer, string name, string origin = default)
|
||||
{
|
||||
Name = $"{name}{Guid.NewGuid():N}"[..20];
|
||||
Id = $"{origin ?? peer.A}/users/{Name}";
|
||||
peer.Serve($"/users/{Name}", Document().ToJsonString());
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public string Id { get; }
|
||||
public string KeyId => Id + "#main-key";
|
||||
|
||||
public JsonObject Document() => new()
|
||||
{
|
||||
["id"] = Id,
|
||||
["type"] = "Person",
|
||||
["preferredUsername"] = Name,
|
||||
["inbox"] = Id + "/inbox",
|
||||
["followers"] = Id + "/followers",
|
||||
["publicKey"] = new JsonObject
|
||||
{
|
||||
["id"] = KeyId,
|
||||
["owner"] = Id,
|
||||
["publicKeyPem"] = _key.ExportSubjectPublicKeyInfoPem()
|
||||
}
|
||||
};
|
||||
|
||||
public HttpRequest Post(string host, string path, JsonNode activity)
|
||||
{
|
||||
var body = Encoding.UTF8.GetBytes(activity.ToJsonString());
|
||||
var date = DateTimeOffset.UtcNow.ToString("r", CultureInfo.InvariantCulture);
|
||||
var digest = HttpSignatures.Digest(body);
|
||||
var signingString = $"(request-target): post {path}\nhost: {host}\ndate: {date}\ndigest: {digest}";
|
||||
var signature = Convert.ToBase64String(_key.SignData(Encoding.UTF8.GetBytes(signingString), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
|
||||
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.Method = "POST";
|
||||
context.Request.Host = new HostString(host);
|
||||
context.Request.Path = path;
|
||||
context.Features.Get<IHttpRequestFeature>().RawTarget = path;
|
||||
context.Request.Headers["Date"] = date;
|
||||
context.Request.Headers["Digest"] = digest;
|
||||
context.Request.Headers["Content-Type"] = "application/activity+json";
|
||||
context.Request.Headers["Signature"] = $"keyId=\"{KeyId}\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest\",signature=\"{signature}\"";
|
||||
context.Request.Body = new MemoryStream(body);
|
||||
context.Request.ContentLength = body.Length;
|
||||
return context.Request;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user