From 0bedcdeb370ef1970bdaf2abe482c549e537c170 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 27 Sep 2026 20:55:49 +0200 Subject: [PATCH] Add TypeScript Idunn runtime presence publisher --- .../cultnet-ts/src/idunn-runtime-authority.ts | 717 ++++++++++++++++++ packages/cultnet-ts/src/index.ts | 14 + .../test/idunn-runtime-authority.test.ts | 140 ++++ 3 files changed, 871 insertions(+) create mode 100644 packages/cultnet-ts/src/idunn-runtime-authority.ts create mode 100644 packages/cultnet-ts/test/idunn-runtime-authority.test.ts diff --git a/packages/cultnet-ts/src/idunn-runtime-authority.ts b/packages/cultnet-ts/src/idunn-runtime-authority.ts new file mode 100644 index 00000000..28e61e97 --- /dev/null +++ b/packages/cultnet-ts/src/idunn-runtime-authority.ts @@ -0,0 +1,717 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { decode, encode } from "@msgpack/msgpack"; + +import { + GAMECULT_RUNTIME_PRESENCE_HEALTH_SCHEMA, + GAMECULT_RUNTIME_PRESENCE_IDENTITY_NAME, + IDUNN_RUNTIME_ACTIVATION_CREDENTIAL_NAME, + runtimePresenceActivationSigningMessage, + runtimePresenceProofPayload, + runtimePresenceProviderSigningMessage, + encodeRuntimePresenceHealth, + type RuntimePresenceHealth, +} from "./runtime-presence-health"; +import { encodeCultNetMessageForWire } from "./contracts"; +import { CultNetRudpSession, decodeRudpPacket, encodeRudpPacket } from "./rudp"; +import type { CultNetRudpPacket } from "./rudp"; +import dgram from "node:dgram"; + +export const IDUNN_EXPECTED_INCARNATION_SCHEMA = "idunn.expected_incarnation.v2"; +export const IDUNN_RUNTIME_ACTIVATION_SCHEMA = "idunn.runtime_activation.v2"; +export const IDUNN_PROCESS_WRITE_LEASE_SCHEMA = "idunn.process_write_lease.v1"; +export const IDUNN_PROCESS_WRITE_LEASE_ENVIRONMENT = "GAMECULT_IDUNN_PROCESS_WRITE_LEASE"; +export const IDUNN_RUNTIME_BUNDLE_ENVIRONMENT = "GAMECULT_IDUNN_RUNTIME_BUNDLE"; +export const IDUNN_RUNTIME_CANDIDATE_BIND_ENVIRONMENT = "GAMECULT_IDUNN_CANDIDATE_BIND"; + +const PROVIDER_ID_DOMAIN = Buffer.from("gamecult.provider-health.identity.v1\0", "utf8"); +const PROVIDER_PROTECTOR_CONTEXT = "gamecult-provider-health-identity-v1"; +const ACTIVATION_ID_DOMAIN = Buffer.from("idunn.runtime-activation.id.v1\0", "utf8"); +const CULTCACHE_STORE_FORMAT = "cultcache.store.v1"; +const ED25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex"); +const MAX_RECENT_WARMING_PROOFS = 64; +const authorityKeys = new WeakMap(); + +type ExpectedIncarnation = { + schemaVersion: string; + target: string; + planId: string; + incarnationId: string; + sealedReleaseId: string; + sourceRepository: string; + sourceRevision: string; + recipeSha256: string; + runtimeId: string; + expectedSignerIdentityId: string; + healthContract: string; + artifactSha256: string; + stateSchemaGeneration: string | null; + stateContractSha256: string | null; + writeLeaseRequired: boolean; + route: { routeId: string; transport: string; stableEndpoint: string; candidateEndpoint: string } | null; + capabilities: Array<{ capability: string; schema: string; compatibility: string; minimumCapacity: number }>; + dependencies: Array<{ kind: string; capability: string; schema: string; compatibility: string; providerEndpoint: string | null }>; + canonicalBytes: Buffer; + canonicalSha256: string; +}; + +type RuntimeActivation = { + schemaVersion: string; + expectedProjectionSha256: string; + runtimeId: string; + runtimeInstanceId: string; + activationSignerIdentityId: string; + activationSignerPublicKey: Buffer; + canonicalSha256: string; +}; + +export type IdunnRuntimeAuthority = { + expected: ExpectedIncarnation; + activation: RuntimeActivation; + providerSignerIdentityId: string; + activationSignerIdentityId: string; + boundEndpoint: string; + processWriteLeasePath?: string; +}; + +export type IdunnRuntimePresencePublisher = { + readonly runtimeId: string; + readonly runtimeInstanceId: string; + readonly requiresWriteLease: boolean; + publish(state: RuntimePresenceHealth["state"], detail: string): Promise; + waitForWriteLease(options?: { pollIntervalMs?: number; signal?: AbortSignal }): Promise; + assertWriteLease(): Promise; +}; + +export type IdunnRuntimePresencePublisherOptions = { + authority: IdunnRuntimeAuthority; + endpoint: string; + healthContract: string; + capabilities: RuntimePresenceHealth["capabilities"]; +}; + +/** + * Loads the root-provisioned Idunn bundle and parent-only signing descriptors. + * The expected record and activation witness are accepted only when their + * exact positional contracts and mutual incarnation binding agree. + */ +export function loadIdunnRuntimeAuthorityFromEnvironment( + target: string, + healthContract: string, + odinEndpoint: string, +): IdunnRuntimeAuthority { + const bundle = requiredEnvironment(IDUNN_RUNTIME_BUNDLE_ENVIRONMENT); + const expected = readAuthorityRecord(path.join(bundle, "expected.cc"), "idunn.expected_incarnation", IDUNN_EXPECTED_INCARNATION_SCHEMA); + const activation = readAuthorityRecord(path.join(bundle, "activation.cc"), "idunn.runtime_activation", IDUNN_RUNTIME_ACTIVATION_SCHEMA); + const expectedValue = decodeExpected(expected.payload); + const activationValue = decodeActivation(activation.payload); + if (expectedValue.target !== target || expectedValue.healthContract !== healthContract) { + throw new Error("Idunn Expected target or health contract does not match this runtime."); + } + const odinDependency = expectedValue.dependencies.find((dependency) => dependency.kind === "shared-infrastructure" + && dependency.capability === "odin.verse-rendezvous"); + if (!odinDependency || normalizeRudpEndpoint(odinDependency.providerEndpoint ?? "") !== normalizeRudpEndpoint(odinEndpoint)) { + throw new Error("Configured Odin endpoint does not match Idunn Expected dependency authority."); + } + if (activationValue.expectedProjectionSha256 !== expectedValue.canonicalSha256 + || activationValue.runtimeId !== expectedValue.runtimeId) { + throw new Error("Idunn Activation does not bind this exact Expected incarnation."); + } + + const descriptors = inheritedDescriptorMap(); + const providerCredential = descriptors.get(GAMECULT_RUNTIME_PRESENCE_IDENTITY_NAME); + const activationCredential = descriptors.get(IDUNN_RUNTIME_ACTIVATION_CREDENTIAL_NAME); + if (providerCredential === undefined || activationCredential === undefined) { + throw new Error("Idunn runtime-presence signing descriptors are missing."); + } + + const providerPrivateKey = openProviderHealthIdentity(readDescriptor(providerCredential)); + const activationSeed = readDescriptor(activationCredential); + if (activationSeed.length !== 32) throw new Error("Idunn activation credential must be exactly 32 bytes."); + const activationPrivateKey = privateKeyFromSeed(activationSeed); + activationSeed.fill(0); + const activationPublicKey = rawEd25519PublicKey(crypto.createPublicKey(activationPrivateKey)); + const activationSignerIdentityId = sha256Hex(Buffer.concat([ACTIVATION_ID_DOMAIN, activationPublicKey])); + if (activationSignerIdentityId !== activationValue.activationSignerIdentityId + || !activationPublicKey.equals(activationValue.activationSignerPublicKey)) { + throw new Error("Idunn activation credential does not match the root-provisioned Activation witness."); + } + + if (expectedValue.writeLeaseRequired !== (process.env[IDUNN_PROCESS_WRITE_LEASE_ENVIRONMENT] !== undefined)) { + throw new Error("Idunn process write-lease environment does not match Expected."); + } + const boundEndpoint = requiredEnvironment(IDUNN_RUNTIME_CANDIDATE_BIND_ENVIRONMENT); + if (!expectedValue.route || expectedValue.route.candidateEndpoint !== boundEndpoint) { + throw new Error("Idunn candidate bind differs from the Expected route."); + } + if (expectedValue.expectedSignerIdentityId !== providerSignerIdentityId(providerPrivateKey)) { + throw new Error("Provider identity does not match Idunn Expected."); + } + + const authority: IdunnRuntimeAuthority = { + expected: expectedValue, + activation: activationValue, + providerSignerIdentityId: expectedValue.expectedSignerIdentityId, + activationSignerIdentityId, + boundEndpoint, + ...(process.env[IDUNN_PROCESS_WRITE_LEASE_ENVIRONMENT] + ? { processWriteLeasePath: process.env[IDUNN_PROCESS_WRITE_LEASE_ENVIRONMENT] } + : {}), + }; + authorityKeys.set(authority, { providerPrivateKey, activationPrivateKey }); + return authority; +} + +/** + * Signs the fixed Idunn v2 positional runtime-presence contract with the + * provider identity and this launch's Idunn-issued activation key. + */ +export function signIdunnRuntimePresence( + authority: IdunnRuntimeAuthority, + fields: Omit, +): { presence: RuntimePresenceHealth; payload: Uint8Array; canonicalSha256: string } { + const presence: RuntimePresenceHealth = { + ...fields, + target: authority.expected.target, + expectedProjectionSha256: authority.expected.canonicalSha256, + planId: authority.expected.planId, + incarnationId: authority.expected.incarnationId, + sealedReleaseId: authority.expected.sealedReleaseId, + activationWitnessSha256: authority.activation.canonicalSha256, + stateSchemaGeneration: authority.expected.stateSchemaGeneration, + stateContractSha256: authority.expected.stateContractSha256, + runtimeId: authority.expected.runtimeId, + runtimeInstanceId: authority.activation.runtimeInstanceId, + boundEndpoint: authority.boundEndpoint, + healthContract: authority.expected.healthContract, + signerIdentityId: authority.providerSignerIdentityId, + activationSignerIdentityId: authority.activationSignerIdentityId, + }; + const keys = authorityKeys.get(authority); + if (!keys) throw new Error("Idunn runtime authority was not opened by the CultNet credential reader."); + const proofPayload = runtimePresenceProofPayload(presence); + const providerSignature = crypto.sign(null, runtimePresenceProviderSigningMessage(proofPayload), keys.providerPrivateKey); + const activationSignature = crypto.sign(null, runtimePresenceActivationSigningMessage(proofPayload), keys.activationPrivateKey); + const payload = encodeRuntimePresenceHealth(presence, providerSignature, activationSignature); + return { + presence, + payload, + canonicalSha256: prefixedSha256(Buffer.from(payload)), + }; +} + +export function createIdunnRuntimePresencePublisher( + options: IdunnRuntimePresencePublisherOptions, +): IdunnRuntimePresencePublisher { + const endpoint = parseRudpEndpoint(options.endpoint); + const expected = options.authority.expected; + if (options.healthContract !== expected.healthContract) throw new Error("Runtime presence contract differs from Expected."); + if (options.authority.boundEndpoint !== expected.route?.candidateEndpoint) throw new Error("Runtime presence bind differs from Expected."); + for (const required of expected.capabilities) { + if (!options.capabilities.some((actual) => actual.capability === required.capability + && actual.schema === required.schema + && actual.compatibility === required.compatibility + && actual.capacity >= required.minimumCapacity)) { + throw new Error(`Runtime does not provide Expected capability ${required.capability}/${required.schema}.`); + } + } + const connectionId = 0x0d1d0002; + let sequence = 0; + let latestLeaseSha256: string | null = null; + const recentWarmingProofs: string[] = []; + + const assertWriteLease = async () => { + if (!expected.writeLeaseRequired) return ""; + const leasePath = options.authority.processWriteLeasePath; + if (!leasePath) throw new Error("Idunn process write lease is required but no lease path is configured."); + const record = readAuthorityRecord(leasePath, "idunn.process_write_lease", IDUNN_PROCESS_WRITE_LEASE_SCHEMA); + const lease = decodeArray(record.payload, 14, "Idunn process write lease"); + assertLeaseMatches(lease, options.authority, recentWarmingProofs); + latestLeaseSha256 = prefixedSha256(Buffer.from(encode(lease))); + return latestLeaseSha256; + }; + + const publish = async (state: RuntimePresenceHealth["state"], detail: string) => { + const writeLeaseSha256 = state === "warming" || !expected.writeLeaseRequired ? null : await assertWriteLease(); + sequence += 1; + const signed = signIdunnRuntimePresence(options.authority, { + capabilities: options.capabilities, + state, + detail: detail.slice(0, 512), + writeLeaseSha256, + publisherSequence: sequence, + observedAtUnixMillis: Date.now(), + }); + await publishDocument(endpoint, connectionId, { + schemaVersion: "cultnet.document_put_raw.v0", + messageId: `runtime-presence:${expected.target}:${options.authority.activation.runtimeInstanceId}:${sequence}`, + document: { + schemaId: GAMECULT_RUNTIME_PRESENCE_HEALTH_SCHEMA, + recordKey: expected.target, + storedAt: new Date().toISOString(), + payloadEncoding: "messagepack", + payload: signed.payload, + sourceRuntimeId: expected.runtimeId, + sourceRole: "runtime-presence-health-publisher", + tags: ["cultnet.transport.rudp.v0", "runtime-presence"], + }, + }); + if (state === "warming") { + recentWarmingProofs.push(signed.canonicalSha256); + if (recentWarmingProofs.length > MAX_RECENT_WARMING_PROOFS) recentWarmingProofs.shift(); + } + return signed.canonicalSha256; + }; + + return { + runtimeId: expected.runtimeId, + runtimeInstanceId: options.authority.activation.runtimeInstanceId, + requiresWriteLease: expected.writeLeaseRequired, + publish, + async waitForWriteLease({ pollIntervalMs = 5000, signal } = {}) { + if (!expected.writeLeaseRequired) return ""; + let warmingSequence = 0; + while (!signal?.aborted) { + await publish("warming", "waiting-for-process-write-lease"); + warmingSequence += 1; + try { + return await assertWriteLease(); + } catch (error) { + if (warmingSequence > 720) throw new Error("Timed out waiting for Idunn process write lease.", { cause: error }); + } + await abortableDelay(pollIntervalMs, signal); + } + throw signal?.reason ?? new Error("Aborted while waiting for Idunn process write lease."); + }, + assertWriteLease, + }; +} + +async function publishDocument( + endpoint: { host: string; port: number }, + connectionId: number, + message: Record, +): Promise { + const socket = dgram.createSocket(endpoint.host.includes(":") ? "udp6" : "udp4"); + await new Promise((resolve, reject) => { + socket.once("error", reject); + socket.bind(0, endpoint.host.includes(":") ? "::" : "0.0.0.0", () => { + socket.off("error", reject); + resolve(); + }); + }); + const receiver = receivePackets(socket); + const session = new CultNetRudpSession({ connectionId, initialSequence: 1, resendDelayMs: 100 }); + try { + await sendPacket(socket, endpoint, session.createConnect(Date.now(), new Uint8Array())); + await receiveUntil(receiver, session, endpoint, (packet) => packet.packetType === "accept", 5000); + const wirePayload = Uint8Array.from(encodeCultNetMessageForWire(message as never, "cultnet.schema.v0") as Uint8Array); + const packets = session.sendMany("schema", wirePayload, { reliable: true, ordered: true, nowMs: Date.now() }); + const ack = receiveUntil(receiver, session, endpoint, (packet) => packet.packetType === "ack", 2000); + for (const packet of packets) await sendPacket(socket, endpoint, packet); + await ack; + } finally { + receiver.close(); + socket.close(); + } +} + +type PacketReceiver = { + socket: dgram.Socket; + next(timeoutMs: number): Promise; + close(): void; +}; + +function receivePackets(socket: dgram.Socket): PacketReceiver { + const packets: CultNetRudpPacket[] = []; + const waiters: Array<{ resolve: (packet: CultNetRudpPacket) => void; reject: (error: Error) => void; timer: NodeJS.Timeout }> = []; + const errors: Error[] = []; + const drain = () => { + while (waiters.length && (packets.length || errors.length)) { + const waiter = waiters.shift()!; + clearTimeout(waiter.timer); + if (errors.length) waiter.reject(errors.shift()!); + else waiter.resolve(packets.shift()!); + } + }; + const onMessage = (wire: Buffer) => { + try { packets.push(decodeRudpPacket(wire)); } catch (error) { errors.push(error as Error); } + drain(); + }; + const onError = (error: Error) => { errors.push(error); drain(); }; + socket.on("message", onMessage); + socket.on("error", onError); + return { + socket, + next(timeoutMs) { + if (packets.length) return Promise.resolve(packets.shift()!); + if (errors.length) return Promise.reject(errors.shift()!); + return new Promise((resolve, reject) => { + const waiter = { + resolve, + reject, + timer: setTimeout(() => { + const index = waiters.indexOf(waiter); + if (index >= 0) waiters.splice(index, 1); + const error = new Error("Timed out waiting for CultNet RUDP packet.") as NodeJS.ErrnoException; + error.code = "ETIMEDOUT"; + reject(error); + }, Math.max(1, timeoutMs)), + }; + waiters.push(waiter); + }); + }, + close() { + socket.off("message", onMessage); + socket.off("error", onError); + for (const waiter of waiters.splice(0)) { + clearTimeout(waiter.timer); + waiter.reject(new Error("CultNet RUDP publisher closed.")); + } + }, + }; +} + +async function receiveUntil( + receiver: PacketReceiver, + session: CultNetRudpSession, + endpoint: { host: string; port: number }, + predicate: (packet: CultNetRudpPacket) => boolean, + timeoutMs: number, +): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + try { + const packet = await receiver.next(Math.min(100, deadline - Date.now())); + const received = session.receive(packet, Date.now()); + if (received.reply) throw new Error("Runtime presence received an unexpected reply-required packet."); + if (predicate(packet)) return; + } catch (error) { + if ((error as NodeJS.ErrnoException)?.code !== "ETIMEDOUT") throw error; + } + for (const packet of session.dueResends(Date.now())) await sendPacket(receiver.socket, endpoint, packet); + } + throw new Error(`Timed out waiting for CultNet RUDP response after ${timeoutMs} ms.`); +} + +async function sendPacket(socket: dgram.Socket, endpoint: { host: string; port: number }, packet: CultNetRudpPacket): Promise { + const wire = encodeRudpPacket(packet); + await new Promise((resolve, reject) => { + socket.send(wire, endpoint.port, endpoint.host, (error) => error ? reject(error) : resolve()); + }); +} + +function readAuthorityRecord(filePath: string, type: string, schemaId: string): { payload: Uint8Array } { + const decoded = decode(fs.readFileSync(filePath)); + if (!Array.isArray(decoded) || decoded[0] !== CULTCACHE_STORE_FORMAT || !Array.isArray(decoded[1]) || !Array.isArray(decoded[2])) { + throw new Error(`Idunn authority file ${filePath} is not a CultCache v1 store.`); + } + if (decoded[2].length !== 1) throw new Error(`Idunn authority file ${filePath} must contain exactly one record.`); + const matchingRecords = (decoded[2] as unknown[]).filter((raw) => { + const row = array(raw, 4, "CultCache record"); + return row[1] === schemaId; + }); + if (matchingRecords.length !== 1) throw new Error(`Idunn authority file must contain exactly one ${schemaId} record.`); + const row = array(matchingRecords[0], 4, "CultCache record"); + const catalog = (decoded[1] as unknown[]).find((raw) => { + const entry = Array.isArray(raw) ? raw : []; + return entry[0] === schemaId; + }); + const catalogEntry = array(catalog, 7, "CultCache schema catalog entry"); + if (catalogEntry[1] !== type) throw new Error(`Idunn authority record has unexpected CultCache type ${String(catalogEntry[1])}.`); + if (!(row[3] instanceof Uint8Array)) throw new Error("Idunn authority payload is not MessagePack bytes."); + return { payload: row[3] }; +} + +function decodeExpected(payload: Uint8Array): ExpectedIncarnation { + const values = decodeArray(payload, 18, "Idunn Expected"); + if (values[0] !== IDUNN_EXPECTED_INCARNATION_SCHEMA) throw new Error("Idunn Expected schema is unsupported."); + const canonicalBytes = Buffer.from(encode(values)); + if (!canonicalBytes.equals(Buffer.from(payload))) throw new Error("Idunn Expected is not canonical MessagePack."); + const stateSchemaGeneration = nullableString(values[12], "Expected state generation"); + const stateContractSha256 = nullableString(values[13], "Expected state contract"); + if ((stateSchemaGeneration === null) !== (stateContractSha256 === null)) throw new Error("Idunn Expected state lineage is partial."); + if (typeof values[14] !== "boolean") throw new Error("Idunn Expected write-lease flag is not boolean."); + const route = decodeRoute(values[15]); + const writeLeaseRequired = values[14]; + if (writeLeaseRequired && stateSchemaGeneration === null) throw new Error("Stateless Idunn Expected cannot require a write lease."); + const target = string(values[1], "Expected target"); + const planId = string(values[2], "Expected plan id"); + const incarnationId = string(values[3], "Expected incarnation id"); + const sealedReleaseId = string(values[4], "Expected sealed release"); + const sourceRepository = string(values[5], "Expected source repository"); + const sourceRevision = string(values[6], "Expected source revision"); + const recipeSha256 = string(values[7], "Expected recipe hash"); + const runtimeId = string(values[8], "Expected runtime id"); + const expectedSignerIdentityId = string(values[9], "Expected signer identity"); + const healthContract = string(values[10], "Expected health contract"); + const artifactSha256 = string(values[11], "Expected artifact hash"); + for (const [name, hash] of [["plan", planId], ["release", sealedReleaseId], ["recipe", recipeSha256], ["artifact", artifactSha256]] as const) { + if (!/^sha256-[0-9a-f]{64}$/u.test(hash)) throw new Error(`Expected ${name} hash is malformed.`); + } + if (!/^[0-9a-f]{40}$/u.test(sourceRevision)) throw new Error("Expected source revision is malformed."); + if (!route || route.transport !== "http" || !route.candidateEndpoint.startsWith("http://")) { + throw new Error("Idunn Expected must include an HTTP candidate route."); + } + return { + schemaVersion: string(values[0], "Expected schema"), + target, + planId, + incarnationId, + sealedReleaseId, + sourceRepository, + sourceRevision, + recipeSha256, + runtimeId, + expectedSignerIdentityId, + healthContract, + artifactSha256, + stateSchemaGeneration, + stateContractSha256, + writeLeaseRequired, + route, + capabilities: decodeExpectedCapabilities(values[16]), + dependencies: decodeExpectedDependencies(values[17]), + canonicalBytes, + canonicalSha256: prefixedSha256(canonicalBytes), + }; +} + +function decodeActivation(payload: Uint8Array): RuntimeActivation { + const values = decodeArray(payload, 10, "Idunn Activation"); + if (values[0] !== IDUNN_RUNTIME_ACTIVATION_SCHEMA) throw new Error("Idunn Activation schema is unsupported."); + const canonicalBytes = Buffer.from(encode(values)); + if (!canonicalBytes.equals(Buffer.from(payload))) throw new Error("Idunn Activation is not canonical MessagePack."); + const publicKey = bytes(values[5], "Activation signer public key"); + if (publicKey.length !== 32) throw new Error("Activation signer public key is not Ed25519."); + const activationSignerIdentityId = string(values[4], "Activation signer identity"); + if (number(values[6], "Activation issue time") <= 0 + || !/^[0-9a-f]{64}$/u.test(string(values[7], "Idunn signer identity")) + || values[8] !== "ed25519" + || bytes(values[9], "Activation signature").length !== 64) { + throw new Error("Idunn Activation signature contract is malformed."); + } + if (sha256Hex(Buffer.concat([ACTIVATION_ID_DOMAIN, publicKey])) !== activationSignerIdentityId) { + throw new Error("Activation signer identity does not match its public key."); + } + return { + schemaVersion: string(values[0], "Activation schema"), + expectedProjectionSha256: string(values[1], "Activation Expected hash"), + runtimeId: string(values[2], "Activation runtime id"), + runtimeInstanceId: string(values[3], "Activation instance id"), + activationSignerIdentityId, + activationSignerPublicKey: Buffer.from(publicKey), + canonicalSha256: prefixedSha256(canonicalBytes), + }; +} + +function decodeExpectedCapabilities(value: unknown): ExpectedIncarnation["capabilities"] { + return arrayOf(value, "Expected capabilities").map((raw) => { + const item = array(raw, 4, "Expected capability"); + return { + capability: string(item[0], "Capability id"), + schema: string(item[1], "Capability schema"), + compatibility: string(item[2], "Capability compatibility"), + minimumCapacity: number(item[3], "Capability capacity"), + }; + }); +} + +function decodeExpectedDependencies(value: unknown): ExpectedIncarnation["dependencies"] { + return arrayOf(value, "Expected dependencies").map((raw) => { + const item = array(raw, 10, "Expected dependency"); + return { + kind: string(item[0], "Dependency kind"), + capability: string(item[1], "Dependency capability"), + schema: string(item[2], "Dependency schema"), + compatibility: string(item[3], "Dependency compatibility"), + providerEndpoint: nullableString(item[9], "Dependency provider endpoint"), + }; + }); +} + +function decodeRoute(value: unknown): ExpectedIncarnation["route"] { + if (value === null) return null; + const route = array(value, 4, "Expected route"); + return { + routeId: string(route[0], "Route id"), + transport: string(route[1], "Route transport"), + stableEndpoint: string(route[2], "Stable route endpoint"), + candidateEndpoint: string(route[3], "Candidate route endpoint"), + }; +} + +function openProviderHealthIdentity(credentialBytes: Buffer): crypto.KeyObject { + const record = readAuthorityRecordBytes(credentialBytes, "gamecult.provider_health_identity", "gamecult.provider_health_identity.private.v1"); + const fields = decodeArray(record, 10, "provider-health identity"); + const [schema, identityId, publicKeyValue, protectedSeedValue, protectorKind, binding, version, assurance, createdAt, enrollmentNonceValue] = fields; + if (schema !== "gamecult.provider_health_identity.private.v1" || version !== "v1") throw new Error("Provider-health identity schema is unsupported."); + string(createdAt, "Provider-health identity creation time"); + const enrollmentNonce = bytes(enrollmentNonceValue, "Provider-health enrollment nonce"); + if (enrollmentNonce.length !== 32) throw new Error("Provider-health enrollment nonce has an invalid length."); + const publicKey = bytesValue(publicKeyValue, "Provider-health public key"); + const protectedSeed = bytesValue(protectedSeedValue, "Provider-health protected seed"); + const machineId = fs.readFileSync("/etc/machine-id", "utf8").trim(); + if (!machineId) throw new Error("Linux machine-id is unavailable for provider-health identity."); + const expectedBinding = `${PROVIDER_PROTECTOR_CONTEXT}:machine-id-sha256:${sha256Hex(Buffer.from(machineId))}`; + if (protectorKind !== "linux_file_mode_machine_id_binding" || binding !== expectedBinding || assurance !== "os_installation_file_bound_cloneable_baseline") { + throw new Error("Provider-health identity protector does not match this Linux installation."); + } + if (publicKey.length !== 32 || protectedSeed.length !== 32) throw new Error("Provider-health identity key material has an invalid length."); + const mask = crypto.createHash("sha256").update(Buffer.concat([ + Buffer.from("gamecult-linux-service-seed-v1\0", "utf8"), + Buffer.from(PROVIDER_PROTECTOR_CONTEXT, "utf8"), + Buffer.from(expectedBinding, "utf8"), + ])).digest(); + const seed = Buffer.from(protectedSeed.map((value, index) => value ^ mask[index]!)); + const key = privateKeyFromSeed(seed); + seed.fill(0); + const derivedPublicKey = rawEd25519PublicKey(crypto.createPublicKey(key)); + const derivedId = sha256Hex(Buffer.concat([PROVIDER_ID_DOMAIN, derivedPublicKey])); + if (!derivedPublicKey.equals(publicKey) || identityId !== derivedId) throw new Error("Provider-health private key differs from its enrolled identity."); + return key; +} + +function readAuthorityRecordBytes(bytes: Buffer, type: string, schemaId: string): Uint8Array { + const decoded = decode(bytes); + if (!Array.isArray(decoded) || decoded[0] !== CULTCACHE_STORE_FORMAT || !Array.isArray(decoded[1]) || !Array.isArray(decoded[2])) { + throw new Error("Runtime credential is not a CultCache v1 store."); + } + if (decoded[2].length !== 1) throw new Error("Runtime credential must contain exactly one record."); + const rows = (decoded[2] as unknown[]).filter((raw) => Array.isArray(raw) && raw[1] === schemaId); + if (rows.length !== 1) throw new Error(`Runtime credential must contain exactly one ${schemaId} record.`); + const catalog = (decoded[1] as unknown[]).find((raw) => Array.isArray(raw) && raw[0] === schemaId); + if (!Array.isArray(catalog) || catalog[1] !== type) throw new Error("Runtime credential has an unexpected CultCache type."); + const row = array(rows[0], 4, "Runtime credential record"); + return bytesValue(row[3], "Runtime credential payload"); +} + +function inheritedDescriptorMap(): Map { + if (process.env.LISTEN_PID !== String(process.pid)) throw new Error("Inherited Idunn descriptors do not belong to this process."); + const count = Number(process.env.LISTEN_FDS); + const names = process.env.LISTEN_FDNAMES?.split(":") ?? []; + if (!Number.isInteger(count) || count < 1 || names.length !== count) throw new Error("Inherited Idunn descriptor list is malformed."); + return new Map(names.map((name, index) => [name, index + 3])); +} + +function readDescriptor(descriptor: number): Buffer { + const fdPath = `/proc/self/fd/${descriptor}`; + return fs.readFileSync(fdPath); +} + +function privateKeyFromSeed(seed: Uint8Array): crypto.KeyObject { + return crypto.createPrivateKey({ key: Buffer.concat([ED25519_PKCS8_PREFIX, Buffer.from(seed)]), format: "der", type: "pkcs8" }); +} + +function rawEd25519PublicKey(publicKey: crypto.KeyObject): Buffer { + const der = publicKey.export({ type: "spki", format: "der" }) as Buffer; + return Buffer.from(der.subarray(der.length - 32)); +} + +function providerSignerIdentityId(privateKey: crypto.KeyObject): string { + return sha256Hex(Buffer.concat([PROVIDER_ID_DOMAIN, rawEd25519PublicKey(crypto.createPublicKey(privateKey))])); +} + +function assertLeaseMatches(values: unknown[], authority: IdunnRuntimeAuthority, recentWarmingProofs: string[]): void { + const expected = authority.expected; + const activation = authority.activation; + const matches = values[0] === IDUNN_PROCESS_WRITE_LEASE_SCHEMA + && values[1] === expected.target + && values[2] === expected.canonicalSha256 + && values[3] === expected.planId + && values[4] === expected.incarnationId + && values[5] === expected.sealedReleaseId + && values[6] === activation.canonicalSha256 + && values[7] === expected.stateSchemaGeneration + && values[8] === expected.stateContractSha256 + && values[9] === expected.runtimeId + && values[10] === activation.runtimeInstanceId + && typeof values[11] === "string" + && recentWarmingProofs.includes(values[11]) + && Number.isSafeInteger(values[12]) && Number(values[12]) > 0 + && Number.isSafeInteger(values[13]) && Number(values[13]) > 0; + if (!matches) throw new Error("Idunn process write lease does not match the current Expected incarnation."); +} + +function parseRudpEndpoint(value: string): { host: string; port: number } { + const text = value.replace(/^rudp:\/\//i, ""); + const match = text.match(/^\[([^\]]+)\]:(\d+)$/) ?? text.match(/^([^:]+):(\d+)$/); + if (!match) throw new Error(`Invalid Odin RUDP endpoint: ${value}`); + const port = Number(match[2]); + if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`Invalid Odin RUDP port: ${match[2]}`); + return { host: match[1]!, port }; +} + +function normalizeRudpEndpoint(value: string): string { + const normalized = value.trim().toLowerCase().replace(/^rudp:\/\//, ""); + const parsed = parseRudpEndpoint(normalized); + return `${parsed.host.toLowerCase()}:${parsed.port}`; +} + +function requiredEnvironment(name: string): string { + const value = process.env[name]?.trim(); + if (!value) throw new Error(`${name} is required for Idunn runtime presence.`); + return value; +} + +function abortableDelay(milliseconds: number, signal?: AbortSignal): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + signal?.removeEventListener("abort", onAbort); + resolve(); + }, milliseconds); + const onAbort = () => { + clearTimeout(timer); + reject(signal?.reason ?? new Error("Aborted.")); + }; + signal?.addEventListener("abort", onAbort, { once: true }); + }); +} + +function array(value: unknown, length: number, label: string): unknown[] { + if (!Array.isArray(value) || value.length !== length) throw new Error(`${label} is not a ${length}-field positional contract.`); + return value; +} + +function decodeArray(payload: Uint8Array, length: number, label: string): unknown[] { + return array(decode(payload), length, label); +} + +function arrayOf(value: unknown, label: string): unknown[] { + if (!Array.isArray(value)) throw new Error(`${label} is not an array.`); + return value; +} + +function string(value: unknown, label: string): string { + if (typeof value !== "string" || !value.trim() || value.trim() !== value || /[\u0000-\u001f\u007f]/u.test(value)) { + throw new Error(`${label} is not a valid authority string.`); + } + return value; +} + +function nullableString(value: unknown, label: string): string | null { + return value === null ? null : string(value, label); +} + +function number(value: unknown, label: string): number { + if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new Error(`${label} is not a non-negative integer.`); + return value; +} + +function bytes(value: unknown, label: string): Uint8Array { + if (!(value instanceof Uint8Array)) throw new Error(`${label} is not bytes.`); + return value; +} + +function bytesValue(value: unknown, label: string): Buffer { + return Buffer.from(bytes(value, label)); +} + +function prefixedSha256(bytes: Uint8Array): string { + return `sha256-${sha256Hex(bytes)}`; +} + +function sha256Hex(bytes: Uint8Array): string { + return crypto.createHash("sha256").update(bytes).digest("hex"); +} diff --git a/packages/cultnet-ts/src/index.ts b/packages/cultnet-ts/src/index.ts index 84bef769..3db890ef 100644 --- a/packages/cultnet-ts/src/index.ts +++ b/packages/cultnet-ts/src/index.ts @@ -197,3 +197,17 @@ export { type RuntimePresenceHealth, type RuntimePresenceState, } from "./runtime-presence-health"; + +export { + IDUNN_EXPECTED_INCARNATION_SCHEMA, + IDUNN_PROCESS_WRITE_LEASE_ENVIRONMENT, + IDUNN_PROCESS_WRITE_LEASE_SCHEMA, + IDUNN_RUNTIME_BUNDLE_ENVIRONMENT, + IDUNN_RUNTIME_CANDIDATE_BIND_ENVIRONMENT, + IDUNN_RUNTIME_ACTIVATION_SCHEMA, + createIdunnRuntimePresencePublisher, + loadIdunnRuntimeAuthorityFromEnvironment, + type IdunnRuntimeAuthority, + type IdunnRuntimePresencePublisher, + type IdunnRuntimePresencePublisherOptions, +} from "./idunn-runtime-authority"; diff --git a/packages/cultnet-ts/test/idunn-runtime-authority.test.ts b/packages/cultnet-ts/test/idunn-runtime-authority.test.ts new file mode 100644 index 00000000..dd079c91 --- /dev/null +++ b/packages/cultnet-ts/test/idunn-runtime-authority.test.ts @@ -0,0 +1,140 @@ +import { mock, test } from "node:test"; +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { decode, encode } from "@msgpack/msgpack"; + +import { + loadIdunnRuntimeAuthorityFromEnvironment, + signIdunnRuntimePresence, +} from "../src/idunn-runtime-authority"; +import { + runtimePresenceActivationSigningMessage, + runtimePresenceProofPayload, + runtimePresenceProviderSigningMessage, +} from "../src/runtime-presence-health"; + +const providerIdentityContext = "gamecult-provider-health-identity-v1"; +const providerIdDomain = Buffer.from("gamecult.provider-health.identity.v1\0", "utf8"); +const activationIdDomain = Buffer.from("idunn.runtime-activation.id.v1\0", "utf8"); + +test("opens Idunn's machine-bound identity and signs the exact Expected incarnation twice", (context) => { + const directory = mkdtempSync(path.join(os.tmpdir(), "cultnet-runtime-authority-")); + context.after(() => rmSync(directory, { recursive: true, force: true })); + + const machineId = "1234567890abcdef1234567890abcdef"; + const providerSeed = Buffer.alloc(32, 0x31); + const activationSeed = Buffer.alloc(32, 0x72); + const providerKey = privateKeyFromSeed(providerSeed); + const activationKey = privateKeyFromSeed(activationSeed); + const providerPublic = rawPublic(crypto.createPublicKey(providerKey)); + const activationPublic = rawPublic(crypto.createPublicKey(activationKey)); + const providerIdentity = sha256Hex(Buffer.concat([providerIdDomain, providerPublic])); + const activationIdentity = sha256Hex(Buffer.concat([activationIdDomain, activationPublic])); + const target = "streampixels-service"; + const contract = "streampixels-service.cultnet-rudp-service-health"; + const candidate = "http://127.0.0.1:18831"; + const expectedValues = [ + "idunn.expected_incarnation.v2", target, `sha256-${"1".repeat(64)}`, "incarnation-1", + `sha256-${"2".repeat(64)}`, "https://github.com/GameCult/StreamPixels.git", "a".repeat(40), + `sha256-${"3".repeat(64)}`, "streampixels-service-yggdrasil", providerIdentity, contract, + `sha256-${"4".repeat(64)}`, "service-boundary-v1", `sha256-${"5".repeat(64)}`, true, + ["streampixels-service-http", "http", "http://127.0.0.1:8831", candidate], + [["streampixels.service.api", "streampixels.api.v1", "v1", 1]], + [["shared-infrastructure", "odin.verse-rendezvous", "odin.verse-topology.v1", "v1", 1, "before-promotion", null, null, null, "rudp://127.0.0.1:17871"]], + ]; + const expectedBytes = Buffer.from(encode(expectedValues)); + const expectedSha = `sha256-${sha256Hex(expectedBytes)}`; + const activationValues = [ + "idunn.runtime_activation.v2", expectedSha, "streampixels-service-yggdrasil", + `sha256-${"6".repeat(64)}`, activationIdentity, activationPublic, Date.now(), + "7".repeat(64), "ed25519", new Uint8Array(64).fill(0x17), + ]; + const bundle = path.join(directory, "bundle"); + fs.mkdirSync(bundle); + writeFileSync(path.join(bundle, "expected.cc"), cultCacheStore( + "idunn.expected_incarnation", "idunn.expected_incarnation.v2", "expected", expectedBytes, + )); + writeFileSync(path.join(bundle, "activation.cc"), cultCacheStore( + "idunn.runtime_activation", "idunn.runtime_activation.v2", "activation", Buffer.from(encode(activationValues)), + )); + + const binding = `${providerIdentityContext}:machine-id-sha256:${sha256Hex(Buffer.from(machineId))}`; + const mask = crypto.createHash("sha256").update(Buffer.concat([ + Buffer.from("gamecult-linux-service-seed-v1\0", "utf8"), + Buffer.from(providerIdentityContext, "utf8"), + Buffer.from(binding, "utf8"), + ])).digest(); + const protectedSeed = Buffer.from(providerSeed.map((value, index) => value ^ mask[index]!)); + const providerCredential = cultCacheStore( + "gamecult.provider_health_identity", "gamecult.provider_health_identity.private.v1", + "gamecult-provider-health-identity", Buffer.from(encode([ + "gamecult.provider_health_identity.private.v1", providerIdentity, providerPublic, protectedSeed, + "linux_file_mode_machine_id_binding", binding, "v1", "os_installation_file_bound_cloneable_baseline", + "2026-01-01T00:00:00Z", new Uint8Array(32).fill(0x2a), + ])), + ); + + const originalEnvironment = { ...process.env }; + context.after(() => { + for (const key of Object.keys(process.env)) if (!(key in originalEnvironment)) delete process.env[key]; + Object.assign(process.env, originalEnvironment); + }); + process.env.GAMECULT_IDUNN_RUNTIME_BUNDLE = bundle; + process.env.GAMECULT_IDUNN_CANDIDATE_BIND = candidate; + process.env.GAMECULT_IDUNN_PROCESS_WRITE_LEASE = path.join(directory, "lease.cc"); + process.env.LISTEN_PID = String(process.pid); + process.env.LISTEN_FDS = "2"; + process.env.LISTEN_FDNAMES = "gamecult-idunn-runtime-activation-key:gamecult-runtime-presence-identity"; + const realReadFileSync = fs.readFileSync.bind(fs); + mock.method(fs, "readFileSync", (filePath: fs.PathOrFileDescriptor, ...args: unknown[]) => { + if (filePath === "/proc/self/fd/3") return activationSeed; + if (filePath === "/proc/self/fd/4") return providerCredential; + if (filePath === "/etc/machine-id") return machineId; + return realReadFileSync(filePath as never, ...args as never[]); + }); + + const authority = loadIdunnRuntimeAuthorityFromEnvironment(target, contract, "127.0.0.1:17871"); + const signed = signIdunnRuntimePresence(authority, { + capabilities: [{ capability: "streampixels.service.api", schema: "streampixels.api.v1", compatibility: "v1", capacity: 1 }], + state: "warming", + detail: "starting", + writeLeaseSha256: null, + publisherSequence: 1, + observedAtUnixMillis: Date.now(), + }); + const proof = runtimePresenceProofPayload(signed.presence); + assert.equal((decode(expectedBytes) as unknown[]).length, 18); + assert.equal(signed.presence.expectedProjectionSha256, expectedSha); + const signedRecord = decode(signed.payload) as unknown[]; + assert.equal(crypto.verify(null, runtimePresenceProviderSigningMessage(proof), crypto.createPublicKey(providerKey), Buffer.from(signedRecord[21] as Uint8Array)), true); + assert.equal(crypto.verify(null, runtimePresenceActivationSigningMessage(proof), crypto.createPublicKey(activationKey), Buffer.from(signedRecord[23] as Uint8Array)), true); +}); + +function cultCacheStore(type: string, schemaId: string, key: string, payload: Uint8Array): Buffer { + return Buffer.from(encode([ + "cultcache.store.v1", + [[schemaId, type, `${schemaId}.v1`, schemaId, "{}", [schemaId], []]], + [[key, schemaId, "2026-01-01T00:00:00Z", payload]], + ])); +} + +function privateKeyFromSeed(seed: Uint8Array): crypto.KeyObject { + return crypto.createPrivateKey({ + key: Buffer.concat([Buffer.from("302e020100300506032b657004220420", "hex"), Buffer.from(seed)]), + format: "der", + type: "pkcs8", + }); +} + +function rawPublic(key: crypto.KeyObject): Buffer { + const der = key.export({ type: "spki", format: "der" }) as Buffer; + return Buffer.from(der.subarray(der.length - 32)); +} + +function sha256Hex(bytes: Uint8Array): string { + return crypto.createHash("sha256").update(bytes).digest("hex"); +}