Personas prove what goes to relays; the server says what it reads
FEP-521a and FEP-8b32. Every persona has an Ed25519 key of its own (Avatar.SigningKey; migration 014 gives the earlier ones theirs), named in its actor's assertionMethod as a Multikey, the terms defined in the actor's own context. A persona's activity going to a relay carries an eddsa-jcs-2022 proof (JSON canonicalised by RFC 8785, Jcs), so what Activity-Relay forwards reaches Mastodon, which verifies it with its own code. Nothing else carries one: Mitra takes a proof over the HTTP signature and refuses one by a key it has not read, without reading the actor again. Received: an actor's own Multikeys are kept, and a forwarded activity whose proof one of them verifies is taken as it came instead of being read again from its origin. Discovery: WebFinger for the server's origin links its instance actor (FEP-d556), NodeInfo links it as the application actor (FEP-2677), and actors name RFC 9421 under implements (FEP-844e). Checked live: relay 16 (Activity-Relay's forward of alice's post reaches Mastodon), Mitra, GoToSocial and Mastodon unchanged (165 in all). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
1 parent
c47b6e5533
commit
9ab87b2779
22 files changed
+702
-30
No files matched your search
@@ -0,0 +1,138 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace PrivaPub.Federation.Signing
|
||||
{
|
||||
// RFC 8785, the JSON Canonicalization Scheme: members sorted by their names' UTF-16 code units, no whitespace, strings
|
||||
// escaped only where JSON must be, numbers as ECMAScript writes them. What an eddsa-jcs-2022 proof signs (FEP-8b32).
|
||||
public static class Jcs
|
||||
{
|
||||
public static string Serialize(JsonNode node)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
Write(builder, node);
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
static void Write(StringBuilder builder, JsonNode node)
|
||||
{
|
||||
switch (node)
|
||||
{
|
||||
case null:
|
||||
builder.Append("null");
|
||||
break;
|
||||
case JsonObject obj:
|
||||
builder.Append('{');
|
||||
var first = true;
|
||||
foreach (var (name, value) in obj.OrderBy(p => p.Key, StringComparer.Ordinal))
|
||||
{
|
||||
if (!first)
|
||||
builder.Append(',');
|
||||
first = false;
|
||||
String(builder, name);
|
||||
builder.Append(':');
|
||||
Write(builder, value);
|
||||
}
|
||||
builder.Append('}');
|
||||
break;
|
||||
case JsonArray array:
|
||||
builder.Append('[');
|
||||
for (var i = 0; i < array.Count; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
builder.Append(',');
|
||||
Write(builder, array[i]);
|
||||
}
|
||||
builder.Append(']');
|
||||
break;
|
||||
case JsonValue value:
|
||||
switch (value.GetValueKind())
|
||||
{
|
||||
case JsonValueKind.String:
|
||||
String(builder, value.GetValue<string>());
|
||||
break;
|
||||
case JsonValueKind.Number:
|
||||
builder.Append(Number(double.Parse(value.ToJsonString(), NumberStyles.Float, CultureInfo.InvariantCulture)));
|
||||
break;
|
||||
case JsonValueKind.True:
|
||||
builder.Append("true");
|
||||
break;
|
||||
case JsonValueKind.False:
|
||||
builder.Append("false");
|
||||
break;
|
||||
default:
|
||||
builder.Append("null");
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void String(StringBuilder builder, string text)
|
||||
{
|
||||
builder.Append('"');
|
||||
foreach (var c in text)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case '"':
|
||||
builder.Append("\\\"");
|
||||
break;
|
||||
case '\\':
|
||||
builder.Append("\\\\");
|
||||
break;
|
||||
case '\b':
|
||||
builder.Append("\\b");
|
||||
break;
|
||||
case '\f':
|
||||
builder.Append("\\f");
|
||||
break;
|
||||
case '\n':
|
||||
builder.Append("\\n");
|
||||
break;
|
||||
case '\r':
|
||||
builder.Append("\\r");
|
||||
break;
|
||||
case '\t':
|
||||
builder.Append("\\t");
|
||||
break;
|
||||
default:
|
||||
if (c < 0x20)
|
||||
builder.Append("\\u").Append(((int)c).ToString("x4", CultureInfo.InvariantCulture));
|
||||
else
|
||||
builder.Append(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
builder.Append('"');
|
||||
}
|
||||
|
||||
// ECMAScript's Number.prototype.toString: integers without a fraction, the shortest digits that read back the same,
|
||||
// an exponent from 1e21 up and below 1e-6
|
||||
public static string Number(double value)
|
||||
{
|
||||
if (double.IsNaN(value) || double.IsInfinity(value))
|
||||
throw new ArgumentException("JSON has no NaN or Infinity", nameof(value));
|
||||
if (value == 0)
|
||||
return "0";
|
||||
var abs = Math.Abs(value);
|
||||
if (abs >= 1e21 || abs < 1e-6)
|
||||
{
|
||||
var exponential = value.ToString("R", CultureInfo.InvariantCulture).Replace("E", "e");
|
||||
var at = exponential.IndexOf('e');
|
||||
if (at < 0)
|
||||
return exponential;
|
||||
var mantissa = exponential[..at];
|
||||
var exponent = int.Parse(exponential[(at + 1)..], CultureInfo.InvariantCulture);
|
||||
return $"{mantissa}e{(exponent < 0 ? "-" : "+")}{Math.Abs(exponent)}";
|
||||
}
|
||||
var text = value.ToString("R", CultureInfo.InvariantCulture);
|
||||
if (!text.Contains('E'))
|
||||
return text;
|
||||
// "R" chose an exponent ECMAScript would not: written out in full
|
||||
return decimal.Parse(text, NumberStyles.Float, CultureInfo.InvariantCulture).ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user