Add the shared CultMesh route verifier to cultnet-ts

Cut 1 of the TypeScript QUIC realtime plane: one TypeScript
implementation of Odin root lookup, the validity window, the canonical
route and session transcripts, P-256/P1363 verification, loopback
detection and the protected-scheme rule, published as
`cultnet-ts/authority`. Browser-safe by construction: WebCrypto,
TextEncoder, atob/btoa, URL, DataView; no node: import, and a test that
says so.

The protected-scheme rule is the C# rule (wss, https, any scheme
containing "quic") rather than wss-only, so the module is not wrong for
QUIC routes; the browser client still refuses non-ws(s) endpoints itself.

The transcript bytes are pinned by contracts/cultmesh/authority-route-
vectors.json, written by the C# reference under CULTMESH_WRITE_VECTORS=1
with fixed test keys and otherwise re-verified by both runtimes. The
vector carries a route certificate and a provider session proof.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-16 15:59:09 +02:00
commit e234a64ae9
6 changed files with 625 additions and 0 deletions

View file

@ -0,0 +1,44 @@
{
"nowUnixMilliseconds": 1800000000000,
"odinRoot": {
"keyId": "odin-vector",
"x": "shJLGtmRrW6D87VQKv0xYQekLIfEkeN7dVawwpmIKWM=",
"y": "iR6Gx3aVCghzSyo7TqXqr+1397L73KDNZuzN2cZ55IE="
},
"route": {
"verseId": "aetheria",
"authorityRuntimeId": "aetheria-daemon",
"endpoint": "wss://provider.example/mesh",
"protocolIds": [
"cultmesh.content.v1",
"cultmesh.documents.v1"
],
"priority": 7,
"generation": "vector-generation-1",
"certificate": {
"providerKey": {
"keyId": "provider-vector",
"x": "6XIvgx3W1q7lJFEV0yYbeKWNFgssq54BHBuh9gCwcH8=",
"y": "oi1FiKraueLic+VemzOx18MNwG6thmZMUWeqNw6vNOk="
},
"odinKeyId": "odin-vector",
"issuedAtUnixMilliseconds": 1799999940000,
"expiresAtUnixMilliseconds": 1800003600000,
"signature": "ZHuELFuCNNfbJt1T2mx1D9ZHXyxBedacDetmryMYhWKQxfROncltymkmL5KXl+1h8tDK6NytJCGy+7mYlb/DMA=="
}
},
"sessionProof": {
"request": {
"schemaVersion": "cultmesh.session_open.v2",
"messageId": "vector-message-1",
"sourceRuntimeId": "vector-consumer",
"verseId": "aetheria",
"authorityRuntimeId": "aetheria-daemon",
"protocolId": "cultmesh.documents.v1",
"routeGeneration": "vector-generation-1",
"clientNonce": "VczqjH8NdBaOb+rh8xb6P0xZjWD1el2ReNB8Vbp52ko="
},
"providerKeyId": "provider-vector",
"providerSignature": "6kGxSu/pwcR4lzh/7Av9nhblVIwIwVFzB7kY34vVvkpaFAbAxt8mWqIyZDTftE28G1g+NL4Ea41C3QsuImmkaA=="
}
}

View file

@ -14,6 +14,11 @@
"types": "./dist/contracts.d.ts",
"require": "./dist/contracts.js",
"default": "./dist/contracts.js"
},
"./authority": {
"types": "./dist/cultmesh-authority.d.ts",
"require": "./dist/cultmesh-authority.js",
"default": "./dist/cultmesh-authority.js"
}
},
"files": [

View file

@ -0,0 +1,192 @@
// The one TypeScript implementation of CultMesh route verification.
//
// Browser-safe by construction: only `crypto.subtle`, `TextEncoder`, `atob`,
// `btoa`, `URL`, `DataView` and `Uint8Array`. No `node:` import. The browser
// WebSocket client and the Node realtime consumer verify the same route bytes
// through the same functions; the C# reference is
// `src/GameCult.Mesh/CultMeshAuthorityProof.cs`.
import type { CultMeshSessionOpenMessage } from "./contracts";
export interface CultMeshAuthorityIdentity {
verseId: string;
authorityRuntimeId: string;
}
export interface CultMeshAuthorityRouteView extends CultMeshAuthorityIdentity {
endpoint: string;
protocolId?: string;
protocolIds?: readonly string[];
priority?: number;
generation: string;
certificate?: CultMeshAuthorityRouteCertificate;
}
export interface CultMeshP256PublicKey {
keyId: string;
x: string;
y: string;
}
export interface CultMeshAuthorityRouteCertificate {
providerKey: CultMeshP256PublicKey;
odinKeyId: string;
issuedAtUnixMilliseconds: number;
expiresAtUnixMilliseconds: number;
signature: string;
}
export type CultMeshAuthorityTrustMode = "authenticated-remote" | "local-development";
export interface CultMeshAuthorityTrustPolicy {
mode: CultMeshAuthorityTrustMode;
odinRoots?: readonly CultMeshP256PublicKey[];
now?: () => number;
}
export async function verifyAuthorityRoute(
route: CultMeshAuthorityRouteView,
trust: CultMeshAuthorityTrustPolicy,
): Promise<void> {
const certificate = route.certificate;
if (!certificate) {
if (trust.mode === "local-development" && isLoopbackEndpoint(route.endpoint)) return;
throw new Error("Remote CultMesh routes require an Odin-signed authority certificate.");
}
if (!isProtectedEndpoint(route.endpoint) && !(trust.mode === "local-development" && isLoopbackEndpoint(route.endpoint))) {
throw new Error("Authenticated remote CultMesh routes require TLS or QUIC channel protection.");
}
const now = trust.now?.() ?? Date.now();
if (now < certificate.issuedAtUnixMilliseconds || now >= certificate.expiresAtUnixMilliseconds) {
throw new Error("The Odin route certificate is not currently valid.");
}
const root = trust.odinRoots?.find(candidate => candidate.keyId === certificate.odinKeyId);
if (!root) throw new Error(`Odin key '${certificate.odinKeyId}' is not trusted by this consumer.`);
if (!await verifyP256(root, canonicalRoute(route), certificate.signature)) {
throw new Error("The Odin route certificate signature is invalid.");
}
}
/** Verifies the provider's session proof over the accepted handshake. */
export function verifyProviderSessionProof(
request: CultMeshSessionOpenMessage,
endpoint: string,
providerKey: CultMeshP256PublicKey,
signatureBase64: string,
): Promise<boolean> {
return verifyP256(providerKey, canonicalSession(request, endpoint), signatureBase64);
}
export function canonicalRoute(route: CultMeshAuthorityRouteView): Uint8Array {
const certificate = route.certificate!;
return canonicalFields(
"gamecult.cultmesh.route-certificate.v1",
route.verseId,
route.authorityRuntimeId,
route.endpoint,
[...(route.protocolIds ?? [route.protocolId ?? "cultmesh.documents.v1"])].sort().join(""),
String(route.priority ?? 0),
route.generation,
certificate.providerKey.keyId,
certificate.providerKey.x,
certificate.providerKey.y,
certificate.odinKeyId,
String(certificate.issuedAtUnixMilliseconds),
String(certificate.expiresAtUnixMilliseconds),
);
}
export function canonicalSession(request: CultMeshSessionOpenMessage, endpoint: string): Uint8Array {
return canonicalFields(
"gamecult.cultmesh.session-proof.v1",
request.clientNonce,
request.messageId,
request.sourceRuntimeId,
request.verseId,
request.authorityRuntimeId,
request.protocolId,
endpoint,
request.routeGeneration,
);
}
export function canonicalFields(...values: string[]): Uint8Array {
const encoder = new TextEncoder();
const encoded = values.map(value => encoder.encode(value));
const total = encoded.reduce((sum, value) => sum + 4 + value.byteLength, 0);
const result = new Uint8Array(total);
const view = new DataView(result.buffer);
let offset = 0;
for (const value of encoded) {
view.setUint32(offset, value.byteLength, false);
offset += 4;
result.set(value, offset);
offset += value.byteLength;
}
return result;
}
export async function verifyP256(
key: CultMeshP256PublicKey,
payload: Uint8Array,
signatureBase64: string,
): Promise<boolean> {
try {
const x = base64ToBytes(key.x);
const y = base64ToBytes(key.y);
const signature = base64ToBytes(signatureBase64);
if (x.byteLength !== 32 || y.byteLength !== 32 || signature.byteLength !== 64) return false;
const raw = new Uint8Array(65);
raw[0] = 4;
raw.set(x, 1);
raw.set(y, 33);
const publicKey = await crypto.subtle.importKey(
"raw",
raw.slice().buffer as ArrayBuffer,
{ name: "ECDSA", namedCurve: "P-256" },
false,
["verify"],
);
return await crypto.subtle.verify(
{ name: "ECDSA", hash: "SHA-256" },
publicKey,
signature.slice().buffer as ArrayBuffer,
payload.slice().buffer as ArrayBuffer,
);
} catch {
return false;
}
}
export function isLoopbackEndpoint(value: string): boolean {
try {
const host = new URL(value).hostname.replace(/^\[|\]$/g, "").toLowerCase();
return host === "localhost" || host === "127.0.0.1" || host === "::1";
} catch {
return false;
}
}
/** The C# rule (`CultMeshAuthorityProof.cs`, `IsProtected`): wss, https, or any scheme containing "quic". */
export function isProtectedEndpoint(value: string): boolean {
let scheme: string;
try {
scheme = new URL(value).protocol.slice(0, -1).toLowerCase();
} catch {
return false;
}
return scheme === "wss" || scheme === "https" || scheme.includes("quic");
}
export function bytesToBase64(bytes: Uint8Array): string {
let binary = "";
for (let offset = 0; offset < bytes.length; offset += 0x8000) {
binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000));
}
return btoa(binary);
}
export function base64ToBytes(value: string): Uint8Array {
const binary = atob(value);
return Uint8Array.from(binary, character => character.charCodeAt(0));
}

View file

@ -62,6 +62,24 @@ export {
type GhostlightVariableMap,
type GhostlightWorldState,
} from "./contracts";
export {
base64ToBytes,
bytesToBase64,
canonicalFields,
canonicalRoute,
canonicalSession,
isLoopbackEndpoint,
isProtectedEndpoint,
verifyAuthorityRoute,
verifyP256,
verifyProviderSessionProof,
type CultMeshAuthorityIdentity,
type CultMeshAuthorityRouteCertificate,
type CultMeshAuthorityRouteView,
type CultMeshAuthorityTrustMode,
type CultMeshAuthorityTrustPolicy,
type CultMeshP256PublicKey,
} from "./cultmesh-authority";
export { encodeFrame, LengthPrefixedMessageFramer } from "./framing";
export { CultNetPeer, createTcpFramedCultNetPeer, type CultNetPeerEvents, type CultNetPeerSchemaCatalogRequestOptions, type CultNetPeerShardCatalogRequestOptions, type TcpFramedCultNetPeerOptions } from "./peer";
export {

View file

@ -0,0 +1,238 @@
// The shared CultMesh route verifier, pinned rule by rule.
//
// Routes here are signed with `node:crypto` and verified through the shared
// module. The transcript bytes themselves are pinned by
// `contracts/cultmesh/authority-route-vectors.json`, which the C# reference
// writes (`CultMeshAuthorityProofTests.AuthorityRouteVectorsAreSharedWithTypeScript`
// under CULTMESH_WRITE_VECTORS=1); a fixture authored on this side alone would
// only agree with itself.
import test from "node:test";
import assert from "node:assert/strict";
import { generateKeyPairSync, sign, type KeyObject } from "node:crypto";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import type { CultMeshSessionOpenMessage } from "../src/contracts";
import {
base64ToBytes,
bytesToBase64,
canonicalRoute,
canonicalSession,
isLoopbackEndpoint,
isProtectedEndpoint,
verifyAuthorityRoute,
verifyP256,
verifyProviderSessionProof,
type CultMeshAuthorityRouteView,
type CultMeshP256PublicKey,
} from "../src/cultmesh-authority";
const NOW = 1_800_000_000_000;
const VECTORS_PATH = join(__dirname, "..", "..", "..", "..", "contracts", "cultmesh", "authority-route-vectors.json");
interface Vectors {
nowUnixMilliseconds: number;
odinRoot: CultMeshP256PublicKey;
route: CultMeshAuthorityRouteView;
sessionProof: {
request: CultMeshSessionOpenMessage;
providerKeyId: string;
providerSignature: string;
};
}
function keyPair(keyId: string): { privateKey: KeyObject; publicKey: CultMeshP256PublicKey } {
const pair = generateKeyPairSync("ec", { namedCurve: "P-256" });
const jwk = pair.publicKey.export({ format: "jwk" });
return {
privateKey: pair.privateKey,
publicKey: {
keyId,
x: Buffer.from(jwk.x!, "base64url").toString("base64"),
y: Buffer.from(jwk.y!, "base64url").toString("base64"),
},
};
}
function signP1363(privateKey: KeyObject, payload: Uint8Array): string {
return sign("sha256", payload, { key: privateKey, dsaEncoding: "ieee-p1363" }).toString("base64");
}
function signedRoute(endpoint = "wss://provider.example/mesh") {
const odin = keyPair("odin-root-1");
const provider = keyPair("provider-1");
const unsigned: CultMeshAuthorityRouteView = {
verseId: "sample.counter",
authorityRuntimeId: "sample.counter-daemon",
endpoint,
protocolId: "cultmesh.documents.v1",
protocolIds: ["cultmesh.documents.v1"],
priority: 0,
generation: "signed-generation-1",
certificate: {
providerKey: provider.publicKey,
odinKeyId: odin.publicKey.keyId,
issuedAtUnixMilliseconds: NOW - 60_000,
expiresAtUnixMilliseconds: NOW + 3_600_000,
signature: "",
},
};
const route: CultMeshAuthorityRouteView = {
...unsigned,
certificate: { ...unsigned.certificate!, signature: signP1363(odin.privateKey, canonicalRoute(unsigned)) },
};
return { route, odin, provider };
}
function request(clientNonce = "nonce-a"): CultMeshSessionOpenMessage {
return {
schemaVersion: "cultmesh.session_open.v2",
messageId: "message-1",
sourceRuntimeId: "consumer-1",
verseId: "sample.counter",
authorityRuntimeId: "sample.counter-daemon",
protocolId: "cultmesh.documents.v1",
routeGeneration: "signed-generation-1",
clientNonce,
};
}
const remoteTrust = (odinRoot: CultMeshP256PublicKey, now: number = NOW) =>
({ mode: "authenticated-remote" as const, odinRoots: [odinRoot], now: () => now });
test("a signed remote route verifies against its Odin root", async () => {
const { route, odin } = signedRoute();
await verifyAuthorityRoute(route, remoteTrust(odin.publicKey));
});
test("an unsigned remote route is refused in both trust modes", async () => {
const { route } = signedRoute();
const unsigned = { ...route, certificate: undefined };
await assert.rejects(verifyAuthorityRoute(unsigned, { mode: "authenticated-remote" }), /Odin-signed authority certificate/);
await assert.rejects(verifyAuthorityRoute(unsigned, { mode: "local-development" }), /Odin-signed authority certificate/);
});
test("an unsigned loopback route is accepted only under local-development", async () => {
const { route } = signedRoute("ws://127.0.0.1:4050/mesh");
const unsigned = { ...route, certificate: undefined };
await verifyAuthorityRoute(unsigned, { mode: "local-development" });
await assert.rejects(verifyAuthorityRoute(unsigned, { mode: "authenticated-remote" }), /Odin-signed authority certificate/);
});
test("a certified route on an unprotected scheme is refused unless it is loopback under local-development", async () => {
const remote = signedRoute("ws://provider.example/mesh");
await assert.rejects(verifyAuthorityRoute(remote.route, remoteTrust(remote.odin.publicKey)), /channel protection/);
const loopback = signedRoute("ws://localhost:4050/mesh");
await assert.rejects(verifyAuthorityRoute(loopback.route, remoteTrust(loopback.odin.publicKey)), /channel protection/);
await verifyAuthorityRoute(loopback.route, { ...remoteTrust(loopback.odin.publicKey), mode: "local-development" });
});
test("the validity window is half-open: issuedAt inclusive, expiresAt exclusive", async () => {
const { route, odin } = signedRoute();
const certificate = route.certificate!;
await verifyAuthorityRoute(route, remoteTrust(odin.publicKey, certificate.issuedAtUnixMilliseconds));
await verifyAuthorityRoute(route, remoteTrust(odin.publicKey, certificate.expiresAtUnixMilliseconds - 1));
await assert.rejects(
verifyAuthorityRoute(route, remoteTrust(odin.publicKey, certificate.expiresAtUnixMilliseconds)),
/not currently valid/,
);
await assert.rejects(
verifyAuthorityRoute(route, remoteTrust(odin.publicKey, certificate.issuedAtUnixMilliseconds - 1)),
/not currently valid/,
);
});
test("an Odin root the consumer does not trust is refused before any signature check", async () => {
const { route } = signedRoute();
const stranger = keyPair("odin-root-1");
await assert.rejects(verifyAuthorityRoute(route, { mode: "authenticated-remote", odinRoots: [], now: () => NOW }), /is not trusted/);
await assert.rejects(
verifyAuthorityRoute(route, { mode: "authenticated-remote", odinRoots: [{ ...stranger.publicKey, keyId: "other-root" }], now: () => NOW }),
/is not trusted/,
);
await assert.rejects(verifyAuthorityRoute(route, remoteTrust(stranger.publicKey)), /signature is invalid/);
});
test("a mutated endpoint invalidates the Odin signature", async () => {
const { route, odin } = signedRoute();
await assert.rejects(
verifyAuthorityRoute({ ...route, endpoint: "wss://evil.example/mesh" }, remoteTrust(odin.publicKey)),
/signature is invalid/,
);
});
test("a signature that is not 64 P1363 bytes is refused", async () => {
const { route, odin } = signedRoute();
const bytes = base64ToBytes(route.certificate!.signature);
for (const wrong of [bytes.subarray(0, 63), new Uint8Array([...bytes, 0])]) {
await assert.rejects(
verifyAuthorityRoute(
{ ...route, certificate: { ...route.certificate!, signature: bytesToBase64(wrong) } },
remoteTrust(odin.publicKey),
),
/signature is invalid/,
);
}
assert.equal(await verifyP256(odin.publicKey, canonicalRoute(route), "not base64!"), false);
});
test("the provider session proof binds the nonce, the endpoint and the certified key", async () => {
const { route, provider } = signedRoute();
const open = request();
const signature = signP1363(provider.privateKey, canonicalSession(open, route.endpoint));
assert.equal(await verifyProviderSessionProof(open, route.endpoint, provider.publicKey, signature), true);
assert.equal(await verifyProviderSessionProof(request("nonce-b"), route.endpoint, provider.publicKey, signature), false);
assert.equal(await verifyProviderSessionProof(open, "wss://evil.example/mesh", provider.publicKey, signature), false);
assert.equal(await verifyProviderSessionProof(open, route.endpoint, keyPair("provider-1").publicKey, signature), false);
});
test("isProtectedEndpoint is the C# rule: wss, https, or any scheme containing quic", () => {
for (const accepted of ["wss://provider.example/mesh", "https://provider.example/content", "cultmesh-state+quic://provider.example:4433"]) {
assert.equal(isProtectedEndpoint(accepted), true, accepted);
}
for (const refused of ["ws://provider.example/mesh", "cultnet+tcp://provider.example:3076", "not a url"]) {
assert.equal(isProtectedEndpoint(refused), false, refused);
}
});
test("isLoopbackEndpoint recognises localhost, 127.0.0.1 and ::1 only", () => {
for (const loopback of ["ws://localhost:4050/mesh", "ws://127.0.0.1:4050/mesh", "ws://[::1]:4050/mesh", "wss://LOCALHOST/mesh"]) {
assert.equal(isLoopbackEndpoint(loopback), true, loopback);
}
for (const remote of ["ws://192.0.2.10:4050/mesh", "wss://provider.example/mesh", "ws://127.0.0.2/mesh", "not a url"]) {
assert.equal(isLoopbackEndpoint(remote), false, remote);
}
});
test("base64 helpers round-trip across the 32 KiB chunk boundary", () => {
const bytes = new Uint8Array(70_000).map((_, index) => index % 251);
assert.deepEqual(base64ToBytes(bytesToBase64(bytes)), bytes);
});
test("the C# reference vectors verify through the shared module", async () => {
const vectors = JSON.parse(readFileSync(VECTORS_PATH, "utf8")) as Vectors;
const trust = { mode: "authenticated-remote" as const, odinRoots: [vectors.odinRoot], now: () => vectors.nowUnixMilliseconds };
await verifyAuthorityRoute(vectors.route, trust);
await assert.rejects(verifyAuthorityRoute({ ...vectors.route, priority: 8 }, trust), /signature is invalid/);
await assert.rejects(
verifyAuthorityRoute({ ...vectors.route, protocolIds: [...vectors.route.protocolIds!, "cultmesh.realtime.v1"] }, trust),
/signature is invalid/,
);
const proof = vectors.sessionProof;
assert.equal(proof.providerKeyId, vectors.route.certificate!.providerKey.keyId);
assert.equal(
await verifyProviderSessionProof(proof.request, vectors.route.endpoint, vectors.route.certificate!.providerKey, proof.providerSignature),
true,
);
assert.equal(
await verifyProviderSessionProof({ ...proof.request, clientNonce: "replayed" }, vectors.route.endpoint, vectors.route.certificate!.providerKey, proof.providerSignature),
false,
);
});
test("the shared module keeps the browser-safe surface: no node: import", () => {
const source = readFileSync(join(__dirname, "..", "..", "src", "cultmesh-authority.ts"), "utf8");
assert.doesNotMatch(source, /from "node:|require\("node:/);
});

View file

@ -1,10 +1,12 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
@ -225,6 +227,132 @@ public sealed class CultMeshAuthorityProofTests
handler.RequestUri.Query.Should().Contain("chunkHash=").And.Contain("recordKey=");
}
// Shared-vector check with `packages/cultnet-ts/src/cultmesh-authority.ts`.
// The C# reference signs a route and a session proof with fixed keys and
// writes them when CULTMESH_WRITE_VECTORS=1; otherwise it asserts the
// committed file still verifies here. The TypeScript test verifies the same
// file, so both runtimes are pinned to the same transcript bytes.
[Test]
public void AuthorityRouteVectorsAreSharedWithTypeScript()
{
var path = Path.Combine(RepoRoot(), "contracts", "cultmesh", "authority-route-vectors.json");
using var odin = ECDsa.Create();
odin.ImportFromPem(VectorOdinPrivateKeyPem);
using var provider = ECDsa.Create();
provider.ImportFromPem(VectorProviderPrivateKeyPem);
var odinPublic = CultMeshEcdsaP256PublicKey.From("odin-vector", odin);
var providerPublic = CultMeshEcdsaP256PublicKey.From("provider-vector", provider);
var request = new GameCult.Networking.CultMeshSessionOpenMessage
{
MessageId = "vector-message-1",
SourceRuntimeId = "vector-consumer",
VerseId = "aetheria",
AuthorityRuntimeId = "aetheria-daemon",
ProtocolId = CultMeshProtocols.Documents.Value,
RouteGeneration = "vector-generation-1",
ClientNonce = Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes("vector-nonce")))
};
if (Environment.GetEnvironmentVariable("CULTMESH_WRITE_VECTORS") == "1")
{
var signedRoute = CultMeshAuthorityProof.CreateSignedRoute(
"aetheria", "aetheria-daemon", "wss://provider.example/mesh",
new[] { CultMeshProtocols.Documents.Value, CultMeshProtocols.Content.Value }, 7, "vector-generation-1",
providerPublic, odinPublic.KeyId, Now.AddMinutes(-1), Now.AddHours(1), odin);
var signer = new CultMeshSessionProofSigner(signedRoute, provider);
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
File.WriteAllText(path, JsonSerializer.Serialize(new
{
nowUnixMilliseconds = Now.ToUnixTimeMilliseconds(),
odinRoot = new { keyId = odinPublic.KeyId, x = odinPublic.X, y = odinPublic.Y },
route = new
{
verseId = "aetheria",
authorityRuntimeId = signedRoute.AuthorityRuntimeId,
endpoint = signedRoute.Endpoint,
protocolIds = signedRoute.ProtocolIds,
priority = signedRoute.Priority,
generation = signedRoute.Generation,
certificate = new
{
providerKey = new { keyId = providerPublic.KeyId, x = providerPublic.X, y = providerPublic.Y },
odinKeyId = signedRoute.Certificate!.OdinKeyId,
issuedAtUnixMilliseconds = signedRoute.Certificate.IssuedAtUnixMilliseconds,
expiresAtUnixMilliseconds = signedRoute.Certificate.ExpiresAtUnixMilliseconds,
signature = signedRoute.Certificate.Signature
}
},
sessionProof = new
{
request = new
{
schemaVersion = request.SchemaVersion,
messageId = request.MessageId,
sourceRuntimeId = request.SourceRuntimeId,
verseId = request.VerseId,
authorityRuntimeId = request.AuthorityRuntimeId,
protocolId = request.ProtocolId,
routeGeneration = request.RouteGeneration,
clientNonce = request.ClientNonce
},
providerKeyId = signer.ProviderKeyId,
providerSignature = signer.Sign(request)
}
}, new JsonSerializerOptions
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
}) + "\n");
}
using var vectors = JsonDocument.Parse(File.ReadAllText(path));
var rootElement = vectors.RootElement;
var routeElement = rootElement.GetProperty("route");
var certificateElement = routeElement.GetProperty("certificate");
var route = new CultMeshAuthorityRoute(
routeElement.GetProperty("authorityRuntimeId").GetString()!,
routeElement.GetProperty("endpoint").GetString()!,
routeElement.GetProperty("protocolIds").EnumerateArray().Select(value => value.GetString()!).ToArray(),
routeElement.GetProperty("priority").GetInt32(),
routeElement.GetProperty("generation").GetString(),
new CultMeshRouteCertificate(
providerPublic,
certificateElement.GetProperty("odinKeyId").GetString()!,
certificateElement.GetProperty("issuedAtUnixMilliseconds").GetInt64(),
certificateElement.GetProperty("expiresAtUnixMilliseconds").GetInt64(),
certificateElement.GetProperty("signature").GetString()!));
var trust = new CultMeshAuthorityTrustPolicy(CultMeshAuthorityTrustMode.AuthenticatedRemote, new[] { odinPublic });
var now = DateTimeOffset.FromUnixTimeMilliseconds(rootElement.GetProperty("nowUnixMilliseconds").GetInt64());
var verseId = routeElement.GetProperty("verseId").GetString()!;
trust.Validate(verseId, route, now);
var proofElement = rootElement.GetProperty("sessionProof");
var accepted = Accepted(request, proofElement.GetProperty("providerSignature").GetString()!);
accepted.ProviderKeyId = proofElement.GetProperty("providerKeyId").GetString()!;
CultMeshAuthorityProof.VerifySessionProof(request, accepted, verseId, route, trust, now).Should().BeTrue();
}
private static string RepoRoot()
{
var directory = new DirectoryInfo(TestContext.CurrentContext.TestDirectory);
while (directory != null && !File.Exists(Path.Combine(directory.FullName, "CultLib.sln")))
directory = directory.Parent;
return directory?.FullName ?? throw new InvalidOperationException("CultLib.sln was not found above the test directory.");
}
// Fixed test keys. They sign only the committed vector and prove nothing else.
private const string VectorOdinPrivateKeyPem = @"-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgHY0//QOheSwARzXE
iX/nGqSD7XFAy530QQqKeXPNxMChRANCAASyEksa2ZGtboPztVAq/TFhB6Qsh8SR
43t1VrDCmYgpY4kehsd2lQoIc0sqO06l6q/td/ey+9ygzWbszdnGeeSB
-----END PRIVATE KEY-----";
private const string VectorProviderPrivateKeyPem = @"-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgVC+9X4t+1HAiIxP4
TEuAuBAY+3ryKK8SaitB0TqsPQqhRANCAATpci+DHdbWruUkURXTJht4pY0WCyyr
ngEcG6H2ALBwf6ItRYiq2rni4nPlXpszsdfDDcBurYZmTFFnqjcOrzTp
-----END PRIVATE KEY-----";
private static CultMeshAuthorityRoute SignedRoute(ECDsa odin, ECDsa provider) =>
CultMeshAuthorityProof.CreateSignedRoute(
"aetheria",