Heimdall opens its Idunn presence authority once, before it listens

index.ts declared the presence sequence below the startup try, so the bundle's
hoisted var was undefined at the first publish; decodePrivateIdentity refused the
Rust owner's integer-array key fields; both descriptors were re-read every pulse and
returned nothing after the first. openPresenceAuthority now reads the bundle facts and
each descriptor once (whole, from offset 0, then closed), owns the one publisher
sequence, and signs through them; a launch-contract fault throws before anything
listens, so the process exits non-zero. decodePrivateIdentity takes the shape of its
source and names the failed check, never a value. openActivationSigner and the
per-pulse descriptor reads are deleted.

tests/idunn-boot.test.ts boots the bundle under the Idunn launch contract and checks
the signed warming frames and the fail-fast exits. The descriptor, sequence and
generation tests now use the Rust-written fixtures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-23 17:48:08 +02:00
commit c044155855
8 changed files with 608 additions and 226 deletions

View file

@ -6,14 +6,16 @@ import { publishHeimdallOdinState } from "./odin-publication.js";
import { startHeimdallPrivateCommandPlane } from "./private-command-plane.js";
import type { CultNetOperationServer } from "cultnet-ts";
import { createHeimdallRuntimePulse, buildHeimdallHealthDetail, publishHeimdallVerseState } from "./verse-state.js";
import {
buildPresenceStatement,
PublisherSequence,
readRuntimeBundleFacts,
type RuntimeBundleFacts,
} from "./runtime-presence.js";
import { openPresenceAuthority } from "./runtime-presence.js";
const config = loadConfig();
// Under Idunn the launch's presence keys, bundle facts and publisher sequence
// open here, before anything listens. A launch-contract fault throws, uncaught,
// and the process exits non-zero with no listener: Idunn fails Warming in
// seconds rather than waiting on a candidate that can never sign.
const presenceAuthority = config.idunnRuntimeBundlePath
? await openPresenceAuthority(config)
: undefined;
const app = await buildApp({ config });
const versePulseIntervalMs = 60_000;
const healthPulseIntervalMs = 10_000;
@ -69,14 +71,6 @@ async function publishVerseState(): Promise<void> {
await publishHeimdallOdinState(config, pulse);
}
/**
* Read once and keep: the bundle is Idunn's immutable record of this launch and
* cannot change while the process lives, so re-reading it every pulse would be
* work that can only ever return the same answer or fail.
*/
let runtimeBundleFacts: RuntimeBundleFacts | undefined;
const presenceSequence = new PublisherSequence();
async function publishHealthState(): Promise<void> {
const pulse = createHeimdallRuntimePulse(config);
@ -94,30 +88,17 @@ async function publishHealthState(): Promise<void> {
? buildHeimdallHealthDetail(config, pulse)
: `Heimdall candidate warming; ${lease.reason}`;
if (!config.idunnRuntimeBundlePath) {
if (!presenceAuthority) {
// Outside Idunn there is no launch to attest and nothing that admits the
// statement; publishing is simply not part of that world.
return;
}
runtimeBundleFacts ??= await readRuntimeBundleFacts(config.idunnRuntimeBundlePath);
const observedAtUnixMillis = Date.parse(pulse.updatedAt);
const presence = await buildPresenceStatement({
bundle: runtimeBundleFacts,
state: lease.mayWrite ? "active" : "warming",
detail,
boundEndpoint: `http://${config.host}:${config.port}`,
// A warming candidate holds no lease and must not claim one; the contract
// refuses the pair, and Idunn needs the warming statement to promote it.
writeLeaseSha256: null,
observedAtUnixMillis,
publisherSequence: presenceSequence.next(observedAtUnixMillis),
providerHealthIdentityPath: config.providerHealthIdentityPath,
});
const state = lease.mayWrite ? "active" : "warming";
const presence = presenceAuthority.sign(state, detail, Date.parse(pulse.updatedAt));
await publishIdunnRudpHealth(config, {
daemonId: config.daemonId,
state: lease.mayWrite ? "active" : "warming",
state,
detail,
observedAt: pulse.updatedAt,
presence,

View file

@ -7,7 +7,7 @@ import {
sign,
type KeyObject,
} from "node:crypto";
import { readFileSync } from "node:fs";
import { closeSync, fstatSync, readSync } from "node:fs";
import { chmod, readFile } from "node:fs/promises";
import { SingleFileMessagePackBackingStore, type CultCacheEnvelope } from "@gamecult/cultcache-ts";
import { readPrivateStoreRecord } from "./idunn-store.js";
@ -71,7 +71,7 @@ export async function openOrEnrollProviderHealthIdentity(path: string): Promise<
if (existing.length !== 1) {
throw new Error("Provider-health identity store must contain exactly one record.");
}
return decodePrivateIdentity(existing[0]!.payload, await readMachineId());
return decodePrivateIdentity(existing[0]!.payload, await readMachineId(), "bin");
}
const PRESENCE_IDENTITY_FD_NAME = "gamecult-runtime-presence-identity";
@ -124,7 +124,8 @@ export function passedDescriptor(
*/
export async function openProviderHealthIdentity(
path: string,
env: NodeJS.ProcessEnv = process.env
env: NodeJS.ProcessEnv = process.env,
machineId: () => Promise<string> = readMachineId
): Promise<PrivateIdentity> {
const descriptor = passedDescriptor(PRESENCE_IDENTITY_FD_NAME, env);
if (descriptor === undefined) {
@ -140,11 +141,33 @@ export async function openProviderHealthIdentity(
}
const payload = readPrivateStoreRecord(
readFileSync(descriptor),
readPassedDescriptor(descriptor),
"Idunn runtime presence identity"
);
return decodePrivateIdentity(payload, await readMachineId());
// The descriptor's writer is the Rust owner, whose key fields are integer arrays.
return decodePrivateIdentity(payload, await machineId(), "int-array");
}
/**
* Read a passed descriptor whole, from offset 0, and close it. A descriptor is
* a one-shot input: reading it again starts at the offset the last read left,
* which is the end, so the owner reads once at open and keeps what it decoded.
*/
export function readPassedDescriptor(descriptor: number): Buffer {
try {
const size = fstatSync(descriptor).size;
const bytes = Buffer.alloc(size);
let read = 0;
while (read < size) {
const count = readSync(descriptor, bytes, read, size - read, read);
if (count === 0) break;
read += count;
}
return bytes.subarray(0, read);
} finally {
closeSync(descriptor);
}
}
export function signProviderHealthPayload(identity: PrivateIdentity, payload: Uint8Array): Uint8Array {
@ -176,23 +199,58 @@ export function providerHealthPublicIdentity(identity: PrivateIdentity): {
return { identityId: identity.identityId, publicKeyHex: Buffer.from(identity.publicKey).toString("hex") };
}
function decodePrivateIdentity(payload: Uint8Array, machineId: string): PrivateIdentity {
/**
* Which writer produced the record. The self-enrolling store this process
* writes keeps key fields as MessagePack bin; the Rust owner (cultnet-rs
* service_identity.rs) serialises a Vec<u8> without serde_bytes, so its key
* fields are arrays of integers. Each source is read in its own shape and the
* other is refused.
*/
type KeyShape = "bin" | "int-array";
function keyBytes(value: unknown, shape: KeyShape, field: string): Uint8Array {
const bytes =
shape === "bin"
? value instanceof Uint8Array
? value
: undefined
: Array.isArray(value) &&
value.every((byte) => typeof byte === "number" && Number.isInteger(byte) && byte >= 0 && byte <= 255)
? Uint8Array.from(value as number[])
: undefined;
if (bytes === undefined || bytes.length !== 32) {
throw new Error(
`Provider-health identity key shape check failed: ${field} is not a 32-byte ${shape === "bin" ? "binary" : "integer-array"} key field.`
);
}
return bytes;
}
function decodePrivateIdentity(payload: Uint8Array, machineId: string, shape: KeyShape): PrivateIdentity {
const value = decode(payload);
if (!Array.isArray(value) || value.length !== 10) {
throw new Error("Provider-health identity payload is malformed.");
throw new Error("Provider-health identity schema check failed: payload is not a 10-field record.");
}
const [schema, identityId, publicKey, protectedSeed, protector, binding, version] = value;
if (
schema !== PRIVATE_SCHEMA ||
typeof identityId !== "string" ||
!(publicKey instanceof Uint8Array) || publicKey.length !== 32 ||
!(protectedSeed instanceof Uint8Array) || protectedSeed.length !== 32 ||
protector !== "linux_file_mode_machine_id_binding" ||
binding !== protectorBinding(machineId) ||
version !== "v1" ||
identityId !== deriveIdentityId(publicKey)
) {
throw new Error("Provider-health identity does not belong to this host or profile.");
const [schema, identityId, publicKeyField, protectedSeedField, protector, binding, version] = value;
if (schema !== PRIVATE_SCHEMA) {
throw new Error("Provider-health identity schema check failed: unexpected schema id.");
}
if (typeof identityId !== "string") {
throw new Error("Provider-health identity id check failed: id is not a string.");
}
const publicKey = keyBytes(publicKeyField, shape, "public key");
const protectedSeed = keyBytes(protectedSeedField, shape, "protected seed");
if (protector !== "linux_file_mode_machine_id_binding") {
throw new Error("Provider-health identity protector check failed: unexpected protector.");
}
if (binding !== protectorBinding(machineId)) {
throw new Error("Provider-health identity binding check failed: not bound to this host's machine id.");
}
if (version !== "v1") {
throw new Error("Provider-health identity version check failed: unexpected version.");
}
if (identityId !== deriveIdentityId(publicKey)) {
throw new Error("Provider-health identity id check failed: id does not derive from the public key.");
}
const seed = xorSeed(protectedSeed, binding);
const privateKey = privateKeyFromSeed(seed);

View file

@ -19,7 +19,6 @@
import path from "node:path";
import { createHash, createPrivateKey, type KeyObject } from "node:crypto";
import { readFileSync } from "node:fs";
import { decode } from "@msgpack/msgpack";
import {
@ -33,11 +32,14 @@ import {
type RuntimePresenceState,
} from "cultnet-ts";
import type { HeimdallConfig } from "./config.js";
import { readIdunnStoreRecord } from "./idunn-store.js";
import {
openProviderHealthIdentity,
passedDescriptor,
readPassedDescriptor,
signWithIdentity,
type PrivateIdentity,
} from "./provider-health-identity.js";
const PKCS8_ED25519_SEED_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
@ -176,29 +178,6 @@ export async function readRuntimeBundleFacts(bundlePath: string): Promise<Runtim
};
}
/**
* Open the activation signing key Idunn passed as a descriptor.
*
* The credential is exactly 32 bytes: the raw Ed25519 seed. It is passed as a
* parent-only descriptor and not as a path, so only the process Idunn launched
* can produce this half of the proof.
*/
export function openActivationSigner(env: NodeJS.ProcessEnv = process.env): KeyObject | undefined {
const descriptor = passedDescriptor(IDUNN_RUNTIME_ACTIVATION_CREDENTIAL_NAME, env);
if (descriptor === undefined) {
return undefined;
}
const seed = readFileSync(descriptor);
if (seed.length !== 32) {
throw new Error("Idunn runtime activation credential is not a 32-byte seed");
}
return createPrivateKey({
key: Buffer.concat([PKCS8_ED25519_SEED_PREFIX, seed]),
format: "der",
type: "pkcs8",
});
}
/**
* The publisher sequence a process stamps on its presence statements.
*
@ -223,14 +202,14 @@ export class PublisherSequence {
export interface PresenceStatementInputs {
bundle: RuntimeBundleFacts;
identity: PrivateIdentity;
activationKey: KeyObject;
state: RuntimePresenceState;
detail: string;
boundEndpoint: string | null;
writeLeaseSha256: string | null;
observedAtUnixMillis: number;
publisherSequence: number;
providerHealthIdentityPath: string;
env?: NodeJS.ProcessEnv;
}
export interface PresenceStatement {
@ -239,24 +218,14 @@ export interface PresenceStatement {
}
/**
* Build and dual-sign one presence statement.
* Build and dual-sign one presence statement from keys and facts already open.
*
* Returns the target alongside the bytes because the document key must equal
* the signed target; Odin refuses the pair when they differ, and that is the
* one part of the envelope the transport cannot infer.
*/
export async function buildPresenceStatement(
inputs: PresenceStatementInputs,
): Promise<PresenceStatement> {
const env = inputs.env ?? process.env;
const activationKey = openActivationSigner(env);
if (activationKey === undefined) {
throw new Error(
"Idunn passed no runtime activation credential, so this process cannot prove it is the " +
"incarnation Idunn started",
);
}
const identity = await openProviderHealthIdentity(inputs.providerHealthIdentityPath, env);
export function buildPresenceStatement(inputs: PresenceStatementInputs): PresenceStatement {
const { identity } = inputs;
const presence: RuntimePresenceHealth = {
target: inputs.bundle.target,
@ -284,7 +253,7 @@ export async function buildPresenceStatement(
const proof = runtimePresenceProofPayload(presence);
const signature = signWithIdentity(identity, runtimePresenceProviderSigningMessage(proof));
const activationSignature = signWithIdentity(
{ ...identity, privateKey: activationKey },
{ ...identity, privateKey: inputs.activationKey },
runtimePresenceActivationSigningMessage(proof),
);
@ -293,3 +262,72 @@ export async function buildPresenceStatement(
payload: encodeRuntimePresenceHealth(presence, signature, activationSignature),
};
}
export interface PresenceAuthority {
sign(
state: RuntimePresenceState,
detail: string,
observedAtUnixMillis: number,
): PresenceStatement;
}
/**
* Open everything this launch's presence statements stand on, once, before the
* process listens: Idunn's bundle facts, the activation seed and the presence
* identity (each descriptor is read whole and closed), and the one publisher
* sequence. It owns them for the life of the process; a pulse supplies only its
* state, detail and observation time.
*
* A launch-contract fault (a missing or malformed bundle, descriptor or
* identity) throws here, so the process exits before it accepts a request and
* Idunn fails Warming at once, instead of a listener that can never sign.
*/
export async function openPresenceAuthority(
config: Pick<HeimdallConfig, "idunnRuntimeBundlePath" | "host" | "port" | "providerHealthIdentityPath">,
env: NodeJS.ProcessEnv = process.env,
machineId?: () => Promise<string>,
): Promise<PresenceAuthority> {
if (!config.idunnRuntimeBundlePath) {
throw new Error("no Idunn runtime bundle path: presence has no launch to attest");
}
const bundle = await readRuntimeBundleFacts(config.idunnRuntimeBundlePath);
const activationDescriptor = passedDescriptor(IDUNN_RUNTIME_ACTIVATION_CREDENTIAL_NAME, env);
if (activationDescriptor === undefined) {
throw new Error(
"Idunn passed no runtime activation credential, so this process cannot prove it is the " +
"incarnation Idunn started",
);
}
const seed = readPassedDescriptor(activationDescriptor);
if (seed.length !== 32) {
throw new Error("Idunn runtime activation credential is not a 32-byte seed");
}
const activationKey = createPrivateKey({
key: Buffer.concat([PKCS8_ED25519_SEED_PREFIX, seed]),
format: "der",
type: "pkcs8",
});
const identity = await openProviderHealthIdentity(config.providerHealthIdentityPath, env, machineId);
const boundEndpoint = `http://${config.host}:${config.port}`;
const sequence = new PublisherSequence();
return {
sign: (state, detail, observedAtUnixMillis) =>
buildPresenceStatement({
bundle,
identity,
activationKey,
state,
detail,
boundEndpoint,
// A warming candidate holds no lease and must not claim one; the
// contract refuses the pair, and Idunn needs the warming statement to
// promote it.
writeLeaseSha256: null,
observedAtUnixMillis,
publisherSequence: sequence.next(observedAtUnixMillis),
}),
};
}

View file

@ -1,64 +1,45 @@
/**
* What Idunn hands a generation for runtime presence: a parent-only identity
* store and an activation seed, passed as descriptors named in LISTEN_FDNAMES.
* Every key here is generated for the test or a canary byte pattern.
* What Idunn hands a generation for runtime presence: the bundle directory, a
* parent-only identity store and an activation seed, the last two passed as
* descriptors named in LISTEN_FDNAMES. The bytes are the Rust owner's, copied
* from CultLib (see idunn-runtime/README.md); every key in them is a test key.
*/
import { closeSync, openSync } from "node:fs";
import { chmod, mkdir, writeFile } from "node:fs/promises";
import { chmod, copyFile, mkdir, readFile } from "node:fs/promises";
import path from "node:path";
import { encode } from "@msgpack/msgpack";
import { SingleFileMessagePackBackingStore } from "@gamecult/cultcache-ts";
import { IDUNN_RUNTIME_ACTIVATION_CREDENTIAL_NAME } from "cultnet-ts";
import { openOrEnrollProviderHealthIdentity } from "../../src/provider-health-identity.js";
import type { RuntimeBundleFacts } from "../../src/runtime-presence.js";
export const idunnRuntimeFixtures = path.join(import.meta.dirname, "idunn-runtime");
export const bundleFacts: RuntimeBundleFacts = {
target: "heimdall",
planId: "plan-1",
incarnationId: "incarnation-1",
sealedReleaseId: "release-1",
runtimeId: "runtime-1",
runtimeInstanceId: "instance-1",
expectedProjectionSha256: "sha256-projection",
activationWitnessSha256: "sha256-witness",
activationSignerIdentityId: "activation-signer",
stateSchemaGeneration: null,
stateContractSha256: null,
capabilities: [],
healthContract: "health-contract",
};
const FIXTURE_FILES = [
"expected.cc",
"activation.cc",
"activation-key.seed",
"provider-identity.credential",
"machine-id",
] as const;
/** Write both stores into `dir`, readable by any uid (the children run as others). */
/** The machine id the fixture identity is bound to, in place of /etc/machine-id. */
export const fixtureMachineId = async (dir: string = idunnRuntimeFixtures): Promise<string> =>
(await readFile(path.join(dir, "machine-id"), "utf8")).trim();
/** Copy the fixtures into `dir`, readable by any uid (the children run as others). */
export async function writePresenceFixtures(dir: string): Promise<void> {
await mkdir(dir, { recursive: true });
await chmod(dir, 0o755);
const enrolled = path.join(dir, "enrolled.cc");
await openOrEnrollProviderHealthIdentity(enrolled);
const [record] = await new SingleFileMessagePackBackingStore(enrolled).pullAll();
// Re-wrapped the way cultcache-rs writes a store, which is how Idunn writes it.
await writeFile(
path.join(dir, "runtime-presence-identity.cc"),
encode([
[
"gamecult-provider-health-identity",
"gamecult.provider_health_identity.private.v1",
record!.payload,
"2026-09-06T00:00:00.000Z",
"gamecult.provider_health_identity.private.v1",
],
]),
{ mode: 0o644 }
);
await writeFile(path.join(dir, "activation-seed"), Buffer.alloc(32, 7), { mode: 0o644 });
for (const name of FIXTURE_FILES) {
const target = path.join(dir, name);
await copyFile(path.join(idunnRuntimeFixtures, name), target);
await chmod(target, 0o644);
}
}
/** Open both descriptors and name them the way systemd's LISTEN_FDNAMES does. */
export function openPresenceDescriptors(dir: string): { env: NodeJS.ProcessEnv; close: () => void } {
const fds = [
[openSync(path.join(dir, "runtime-presence-identity.cc"), "r"), "gamecult-runtime-presence-identity"],
[openSync(path.join(dir, "activation-seed"), "r"), IDUNN_RUNTIME_ACTIVATION_CREDENTIAL_NAME],
[openSync(path.join(dir, "provider-identity.credential"), "r"), "gamecult-runtime-presence-identity"],
[openSync(path.join(dir, "activation-key.seed"), "r"), IDUNN_RUNTIME_ACTIVATION_CREDENTIAL_NAME],
] as const;
const names: string[] = [];
for (const [fd, name] of fds) {
@ -67,6 +48,14 @@ export function openPresenceDescriptors(dir: string): { env: NodeJS.ProcessEnv;
}
return {
env: { LISTEN_FDNAMES: names.join(":") },
close: () => fds.forEach(([fd]) => closeSync(fd)),
// The authority closes each descriptor it reads; this releases only what it did not.
close: () =>
fds.forEach(([fd]) => {
try {
closeSync(fd);
} catch {
// already closed by the reader
}
}),
};
}

View file

@ -10,8 +10,8 @@ import { readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { loadConfig } from "../../src/config.js";
import { PublisherSequence, buildPresenceStatement } from "../../src/runtime-presence.js";
import { bundleFacts, openPresenceDescriptors } from "./presence-descriptors.js";
import { openPresenceAuthority } from "../../src/runtime-presence.js";
import { fixtureMachineId, openPresenceDescriptors } from "./presence-descriptors.js";
import { createHeimdallRuntimePulse, publishHeimdallVerseState } from "../../src/verse-state.js";
const [role, stateRoot, fixtureDir] = process.argv.slice(2) as [string, string, string];
@ -25,18 +25,17 @@ const config = loadConfig({
async function publishPresence(): Promise<void> {
const descriptors = openPresenceDescriptors(fixtureDir);
try {
const observedAtUnixMillis = Date.now();
await buildPresenceStatement({
bundle: bundleFacts,
state: "warming",
detail: "generation probe",
boundEndpoint: null,
writeLeaseSha256: null,
observedAtUnixMillis,
publisherSequence: new PublisherSequence().next(observedAtUnixMillis),
providerHealthIdentityPath: path.join(stateRoot, "provider-health-identity.cc"),
env: descriptors.env,
});
const authority = await openPresenceAuthority(
{
idunnRuntimeBundlePath: fixtureDir,
host: config.host,
port: config.port,
providerHealthIdentityPath: path.join(stateRoot, "provider-health-identity.cc"),
},
descriptors.env,
() => fixtureMachineId(fixtureDir),
);
authority.sign("warming", "generation probe", Date.now());
} finally {
descriptors.close();
}

302
tests/idunn-boot.test.ts Normal file
View file

@ -0,0 +1,302 @@
import { execFile, spawn, type ChildProcess } from "node:child_process";
import { createPublicKey, createPrivateKey, verify } from "node:crypto";
import dgram from "node:dgram";
import { chmod, copyFile, mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
import { closeSync, openSync } from "node:fs";
import net from "node:net";
import os from "node:os";
import path from "node:path";
import { promisify } from "node:util";
import { decode, encode } from "@msgpack/msgpack";
import {
RUNTIME_PRESENCE_SLOT,
parseCultNetMessage,
runtimePresenceActivationSigningMessage,
runtimePresenceProviderSigningMessage,
} from "cultnet-ts";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { startFakeIdunnRudp, type FakeIdunnRudp } from "./fixtures/fake-idunn-rudp.js";
import { idunnRuntimeFixtures } from "./fixtures/presence-descriptors.js";
const run = promisify(execFile);
const GENERATION_UID = 61184;
const STATE_GID = 4242;
const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
const PKCS8_ED25519_SEED_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
// The launch needs root (to pick the uid and bind a machine-id) and a mount
// namespace (so the fixture machine-id does not replace the host's). The
// stopgap's node container has both; anywhere else the test steps aside.
const canLaunch =
process.getuid?.() === 0 &&
(await run("unshare", ["--mount", "--", "true"]).then(
() => true,
() => false
));
/**
* The artifact under test. By default the esbuild bundle `pnpm bundle` makes,
* run by this process's node; a runbook proves a sealed release by naming its
* bundle and node here instead.
*/
const bundleOverride = process.env.HEIMDALL_BOOT_BUNDLE;
const nodeOverride = process.env.HEIMDALL_BOOT_NODE;
async function freePort(): Promise<number> {
return new Promise((resolve, reject) => {
const probe = net.createServer();
probe.once("error", reject);
probe.listen(0, "127.0.0.1", () => {
const { port } = probe.address() as net.AddressInfo;
probe.close(() => resolve(port));
});
});
}
interface Launch {
child: ChildProcess;
stderr: () => string;
exited: Promise<number | null>;
candidatePort: number;
}
describe.skipIf(!canLaunch)("the bundle booted as Idunn launches it", () => {
let base: string;
let bundle: string;
let node: string;
let idunn: FakeIdunnRudp;
let odin: dgram.Socket;
let odinPort: number;
const launched: ChildProcess[] = [];
beforeAll(async () => {
base = await mkdtemp(path.join(os.tmpdir(), "heimdall-boot-"));
await chmod(base, 0o755);
if (bundleOverride) {
bundle = bundleOverride;
} else {
await run("corepack", ["pnpm", "bundle"], { cwd: path.join(import.meta.dirname, "..") });
bundle = path.join(base, "heimdall.mjs");
await copyFile(path.join(import.meta.dirname, "..", "dist", "heimdall"), bundle);
await chmod(bundle, 0o644);
}
node = nodeOverride ?? process.execPath;
idunn = await startFakeIdunnRudp();
odin = dgram.createSocket("udp4");
await new Promise<void>((resolve) => odin.bind(0, "127.0.0.1", resolve));
odinPort = odin.address().port;
}, 120_000);
afterAll(() => {
for (const child of launched) child.kill("SIGKILL");
idunn?.close();
odin?.close();
});
/** Credential files as Idunn holds them: readable only by the parent. */
async function credentials(dir: string, identity: Buffer | undefined) {
await mkdir(dir, { recursive: true });
const seed = path.join(dir, "activation-key.seed");
await copyFile(path.join(idunnRuntimeFixtures, "activation-key.seed"), seed);
await chmod(seed, 0o400);
let identityFile: string | undefined;
if (identity) {
identityFile = path.join(dir, "provider-identity.credential");
await writeFile(identityFile, identity, { mode: 0o400 });
}
return { seed, identityFile };
}
/** Start the bundle as the generation unit does, with the fixture machine-id bound over the host's. */
async function launch(label: string, identity: Buffer | undefined): Promise<Launch> {
const dir = path.join(base, label);
await mkdir(dir, { recursive: true });
await chmod(dir, 0o755);
// Idunn's runtime bundle: the two records, read-only.
const bundleDir = path.join(dir, "bundle");
await mkdir(bundleDir);
for (const name of ["expected.cc", "activation.cc"]) {
await copyFile(path.join(idunnRuntimeFixtures, name), path.join(bundleDir, name));
await chmod(path.join(bundleDir, name), 0o444);
}
await chmod(bundleDir, 0o555);
const machineId = path.join(dir, "machine-id");
await copyFile(path.join(idunnRuntimeFixtures, "machine-id"), machineId);
await chmod(machineId, 0o444);
const stateRoot = path.join(dir, "state");
await mkdir(stateRoot);
await run("chown", [`0:${STATE_GID}`, stateRoot]);
await chmod(stateRoot, 0o2770);
const heimdall = path.join(dir, "heimdall.mjs");
await copyFile(bundle, heimdall);
await chmod(heimdall, 0o444);
const files = await credentials(path.join(dir, "credentials"), identity);
const candidatePort = await freePort();
const fds = [openSync(files.seed, "r")];
const names = ["gamecult-idunn-runtime-activation-key"];
if (files.identityFile) {
fds.push(openSync(files.identityFile, "r"));
names.push("gamecult-runtime-presence-identity");
}
const env = {
PATH: process.env.PATH ?? "",
LISTEN_FDS: String(fds.length),
LISTEN_FDNAMES: names.join(":"),
GAMECULT_IDUNN_CANDIDATE_BIND: `127.0.0.1:${candidatePort}`,
GAMECULT_IDUNN_RUNTIME_BUNDLE: bundleDir,
GAMECULT_IDUNN_PROCESS_WRITE_LEASE: path.join(dir, "process-write-lease.cc"),
GC_ACCESS_BASE_URL: "https://heimdall.test",
GC_ACCESS_ISSUER: "https://heimdall.test",
GC_ACCESS_STORAGE_BACKEND: "memory",
GC_ACCESS_IDUNN_RUDP_HEALTH: `127.0.0.1:${idunn.port}`,
GC_ACCESS_IDUNN_HEALTH_CONTRACT: "heimdall.cultnet-rudp-provider-health",
GC_ACCESS_ODIN_CULTMESH_URI: `rudp://127.0.0.1:${odinPort}`,
CULTMESH_URI_ODIN_RUDP: `rudp://127.0.0.1:${odinPort}`,
GC_ACCESS_SIGNING_PRIVATE_KEY_BOOTSTRAP: "1",
GC_ACCESS_SIGNING_PRIVATE_KEY_PATH: path.join(stateRoot, "signing.pem"),
GC_ACCESS_SIGNING_KEY_ID: "boot-test",
};
// uid, gid and umask as Idunn's DynamicUser unit gives them. The fixture
// machine-id is bound only inside this mount namespace.
const script =
`mount --bind ${machineId} /etc/machine-id && ` +
`exec setpriv --reuid ${GENERATION_UID} --regid ${STATE_GID} --clear-groups ` +
`sh -c 'umask 0007; cd ${dir}; exec ${node} heimdall.mjs --state-root ${stateRoot}'`;
const child = spawn("unshare", ["--mount", "--", "sh", "-c", script], {
env,
stdio: ["ignore", "ignore", "pipe", ...fds],
});
launched.push(child);
fds.forEach((fd) => closeSync(fd));
let stderr = "";
child.stderr!.on("data", (chunk: Buffer) => {
stderr += chunk.toString();
});
const exited = new Promise<number | null>((resolve) => child.once("exit", (code) => resolve(code)));
return { child, stderr: () => stderr, exited, candidatePort };
}
async function answers(port: number): Promise<number | undefined> {
return fetch(`http://127.0.0.1:${port}/.well-known/jwks.json`).then(
(response) => response.status,
() => undefined
);
}
it("signs warming presence on every pulse and answers HTTP", async () => {
const credential = await readFile(path.join(idunnRuntimeFixtures, "provider-identity.credential"));
const generation = await launch("signs", credential);
// Startup publication, then one per 10 s pulse.
await Promise.race([
idunn.frame(2),
generation.exited.then((code) => {
throw new Error(`bundle exited ${code}; stderr: ${generation.stderr()}`);
}),
]);
expect(await answers(generation.candidatePort)).toBe(200);
// Public keys of the two proofs, from the fixtures' test keys.
const store = decode(credential) as unknown[][];
const record = decode(store[0]![2] as Uint8Array) as unknown[];
const providerKey = createPublicKey({
key: Buffer.concat([ED25519_SPKI_PREFIX, Buffer.from(record[2] as number[])]),
format: "der",
type: "spki",
});
const activationKey = createPublicKey(
createPrivateKey({
key: Buffer.concat([
PKCS8_ED25519_SEED_PREFIX,
await readFile(path.join(idunnRuntimeFixtures, "activation-key.seed")),
]),
format: "der",
type: "pkcs8",
})
);
let previousSequence = 0;
for (const frame of idunn.frames.slice(0, 3)) {
const message = parseCultNetMessage(decode(frame.payload), "cultnet.schema.v0");
if (message.schemaVersion !== "cultnet.document_put_raw.v0") throw new Error("not a raw document put");
const fields = decode(message.document.payload) as unknown[];
expect(fields[RUNTIME_PRESENCE_SLOT.state]).toBe("warming");
const unsigned = [...fields];
unsigned[RUNTIME_PRESENCE_SLOT.signature] = new Uint8Array();
unsigned[RUNTIME_PRESENCE_SLOT.activationSignature] = new Uint8Array();
const proof = encode(unsigned);
expect(
verify(
null,
runtimePresenceProviderSigningMessage(proof),
providerKey,
fields[RUNTIME_PRESENCE_SLOT.signature] as Uint8Array
)
).toBe(true);
expect(
verify(
null,
runtimePresenceActivationSigningMessage(proof),
activationKey,
fields[RUNTIME_PRESENCE_SLOT.activationSignature] as Uint8Array
)
).toBe(true);
const sequence = fields[RUNTIME_PRESENCE_SLOT.publisherSequence] as number;
expect(sequence).toBeGreaterThan(previousSequence);
previousSequence = sequence;
}
expect(generation.stderr()).not.toContain("failed on startup");
expect(generation.stderr()).not.toContain("failed on interval");
generation.child.kill("SIGKILL");
}, 90_000);
it.each([
["the identity descriptor is not passed", undefined, "gamecult-runtime-presence-identity"],
["the identity is damaged", "damaged", "identity id check"],
])("exits before listening when %s", async (_name, damage, expected) => {
const credential = await readFile(path.join(idunnRuntimeFixtures, "provider-identity.credential"));
const store = decode(credential) as unknown[][];
const record = decode(store[0]![2] as Uint8Array) as unknown[];
const publicKey = [...(record[2] as number[])];
const original = { publicKeyHex: Buffer.from(publicKey).toString("hex"), id: record[1] as string, binding: record[5] as string };
let identity: Buffer | undefined;
if (damage) {
publicKey[0] = publicKey[0]! ^ 1;
record[2] = publicKey;
identity = Buffer.from(encode([[store[0]![0], store[0]![1], encode(record), store[0]![3], store[0]![4]]]));
}
const started = Date.now();
const generation = await launch(damage ? "damaged" : "no-identity", identity);
const code = await Promise.race([
generation.exited,
new Promise<"timeout">((resolve) => setTimeout(() => resolve("timeout"), 5_000)),
]);
expect(code, generation.stderr()).not.toBe("timeout");
expect(code).not.toBe(0);
expect(Date.now() - started).toBeLessThan(6_000);
expect(await answers(generation.candidatePort)).toBeUndefined();
expect(generation.stderr()).toContain(expected);
// No byte of the credential: not the key bytes, not the id, not the binding.
expect(generation.stderr()).not.toContain(original.publicKeyHex);
expect(generation.stderr()).not.toContain(original.id.slice(0, 16));
expect(generation.stderr()).not.toContain(original.binding.slice(-16));
}, 30_000);
});

View file

@ -1,18 +1,17 @@
import { access, mkdtemp, readFile, writeFile } from "node:fs/promises";
import { openSync, closeSync } from "node:fs";
import { openSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { encode } from "@msgpack/msgpack";
import { SingleFileMessagePackBackingStore } from "@gamecult/cultcache-ts";
import { decode, encode } from "@msgpack/msgpack";
import { describe, expect, it } from "vitest";
import {
openOrEnrollProviderHealthIdentity,
openProviderHealthIdentity,
passedDescriptor,
providerHealthPublicIdentity,
} from "../src/provider-health-identity.js";
import { fixtureMachineId, idunnRuntimeFixtures } from "./fixtures/presence-descriptors.js";
const machineIdAvailable = await readFile("/etc/machine-id", "utf8").then(
() => true,
@ -23,59 +22,79 @@ const machineIdAvailable = await readFile("/etc/machine-id", "utf8").then(
* The identity is machine-id bound, so enrolment only works where a machine-id
* exists. These run on Linux and are skipped elsewhere.
*/
describe.skipIf(!machineIdAvailable)("presence identity from a passed descriptor", () => {
it("reads an Idunn-written store, not a cultcache-ts one", async () => {
/** Name the presence descriptor so it lands on whatever fd open() returned. */
function presenceEnv(fd: number): NodeJS.ProcessEnv {
// The lookup maps a name index onto 3 + index, so the name list is padded
// until the presence entry lands on the descriptor open() actually returned.
// Assuming it is 3 is how this passes on one machine and reads the wrong
// descriptor on another.
const names = [
...Array.from({ length: fd - 3 }, (_, i) => `pad-${i}`),
"gamecult-runtime-presence-identity",
].join(":");
return { LISTEN_FDNAMES: names, LISTEN_PID: String(process.pid) };
}
describe("presence identity from a passed descriptor", () => {
it("decodes the Rust owner's record, whose key fields are integer arrays", async () => {
const dir = await mkdtemp(path.join(os.tmpdir(), "heimdall-presence-"));
const fd = openSync(path.join(idunnRuntimeFixtures, "provider-identity.credential"), "r");
// Enrol with the TypeScript client to get a genuine machine-bound payload,
// then re-wrap that payload the way cultcache-rs writes a store: a bare
// array of positional [key, type, payload, stored_at, schema_id] envelopes.
// Idunn writes with the Rust client, and cultcache-ts cannot read that
// layout, so wrapping it here is what makes this test resemble reality.
const tsStore = path.join(dir, "ts-enrolled.cc");
const enrolled = await openOrEnrollProviderHealthIdentity(tsStore);
const records = await new SingleFileMessagePackBackingStore(tsStore).pullAll();
expect(records).toHaveLength(1);
const idunnStore = path.join(dir, "runtime-presence-identity.cc");
await writeFile(
idunnStore,
encode([
[
"gamecult-provider-health-identity",
"gamecult.provider_health_identity.private.v1",
records[0]!.payload,
"2026-09-06T00:00:00.000Z",
"gamecult.provider_health_identity.private.v1",
],
])
const opened = await openProviderHealthIdentity(
path.join(dir, "would-be-self-enrolled.cc"),
presenceEnv(fd),
fixtureMachineId
);
const fd = openSync(idunnStore, "r");
try {
// The lookup maps a name index onto 3 + index, so the name list is padded
// until the presence entry lands on whatever descriptor open() actually
// returned. Assuming it is 3 is how this passes on one machine and reads
// the wrong descriptor on another.
const names = [
...Array.from({ length: fd - 3 }, (_, i) => `pad-${i}`),
"gamecult-runtime-presence-identity",
].join(":");
// The fixture's own public key means it came from the descriptor. A fresh
// self-enrolment would produce a different key, which is exactly the failure
// that makes Idunn refuse this daemon's health and stall promotion.
const store = decode(await readFile(path.join(idunnRuntimeFixtures, "provider-identity.credential"))) as unknown[][];
const record = decode(store[0]![2] as Uint8Array) as unknown[];
expect(providerHealthPublicIdentity(opened).publicKeyHex).toBe(
Buffer.from(record[2] as number[]).toString("hex")
);
await expect(access(path.join(dir, "would-be-self-enrolled.cc"))).rejects.toMatchObject({ code: "ENOENT" });
});
const opened = await openProviderHealthIdentity(
path.join(dir, "would-be-self-enrolled.cc"),
{ LISTEN_FDNAMES: names, LISTEN_PID: String(process.pid) }
);
it("refuses a bin-shaped record on the descriptor path, naming the key-shape check and no value", async () => {
const dir = await mkdtemp(path.join(os.tmpdir(), "heimdall-presence-bin-"));
const store = decode(await readFile(path.join(idunnRuntimeFixtures, "provider-identity.credential"))) as unknown[][];
const record = decode(store[0]![2] as Uint8Array) as unknown[];
// What this process's own enroller writes: the same record with bin key fields.
const binRecord = record.map((field, index) =>
index === 2 || index === 3 ? Uint8Array.from(field as number[]) : field
);
const binStore = path.join(dir, "bin-shaped.cc");
await writeFile(binStore, encode([[store[0]![0], store[0]![1], encode(binRecord), store[0]![3], store[0]![4]]]));
const fd = openSync(binStore, "r");
// Same key means it came from the descriptor. A fresh self-enrolment
// would produce a different key -- which is exactly the failure that
// makes Idunn refuse this daemon's health and stall promotion.
expect(providerHealthPublicIdentity(opened).publicKeyHex).toBe(
providerHealthPublicIdentity(enrolled).publicKeyHex
);
} finally {
closeSync(fd);
}
const failure = await openProviderHealthIdentity(path.join(dir, "x.cc"), presenceEnv(fd), fixtureMachineId).then(
() => undefined,
(error: Error) => error
);
expect(failure?.message).toContain("key shape check");
expect(failure?.message).not.toContain((record[1] as string).slice(0, 16));
});
it.each([
["schema", 0, "bogus", "schema check"],
["protector", 4, "bogus", "protector check"],
["binding", 5, "bogus", "binding check"],
["version", 6, "v9", "version check"],
])("a record failing only the %s check is refused by that check", async (_name, index, replacement, check) => {
const dir = await mkdtemp(path.join(os.tmpdir(), "heimdall-presence-check-"));
const store = decode(await readFile(path.join(idunnRuntimeFixtures, "provider-identity.credential"))) as unknown[][];
const record = decode(store[0]![2] as Uint8Array) as unknown[];
record[index] = replacement;
const damaged = path.join(dir, "damaged.cc");
await writeFile(damaged, encode([[store[0]![0], store[0]![1], encode(record), store[0]![3], store[0]![4]]]));
const fd = openSync(damaged, "r");
await expect(
openProviderHealthIdentity(path.join(dir, "x.cc"), presenceEnv(fd), fixtureMachineId)
).rejects.toThrow(check);
});
});

View file

@ -1,17 +1,12 @@
import { mkdtemp, readdir, readFile, writeFile } from "node:fs/promises";
import { mkdtemp, readdir, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { loadConfig } from "../src/config.js";
import { PublisherSequence, buildPresenceStatement } from "../src/runtime-presence.js";
import { bundleFacts, openPresenceDescriptors, writePresenceFixtures } from "./fixtures/presence-descriptors.js";
const machineIdAvailable = await readFile("/etc/machine-id", "utf8").then(
() => true,
() => false
);
import { PublisherSequence, openPresenceAuthority } from "../src/runtime-presence.js";
import { fixtureMachineId, openPresenceDescriptors, writePresenceFixtures } from "./fixtures/presence-descriptors.js";
describe("PublisherSequence", () => {
it("starts at the observed millis", () => {
@ -51,10 +46,11 @@ describe("PublisherSequence", () => {
});
});
describe.skipIf(!machineIdAvailable)("presence publication and the state root", () => {
it("touches no path under dataRoot", async () => {
describe("the presence authority", () => {
it("opens once, touches no path under dataRoot, and signs every pulse after its descriptors are gone", async () => {
const dir = await mkdtemp(path.join(os.tmpdir(), "heimdall-nodataroot-"));
await writePresenceFixtures(path.join(dir, "fixtures"));
const fixtures = path.join(dir, "fixtures");
await writePresenceFixtures(fixtures);
// A regular file as dataRoot: opening anything beneath it fails with
// ENOTDIR for every uid, root included.
const dataRoot = path.join(dir, "not-a-directory");
@ -64,21 +60,21 @@ describe.skipIf(!machineIdAvailable)("presence publication and the state root",
GC_ACCESS_DATA_ROOT: dataRoot,
});
const descriptors = openPresenceDescriptors(path.join(dir, "fixtures"));
const descriptors = openPresenceDescriptors(fixtures);
try {
const authority = await openPresenceAuthority(
{ ...config, idunnRuntimeBundlePath: fixtures },
descriptors.env,
fixtureMachineId,
);
// The descriptors were read whole and closed: a second read would find
// nothing, so signing again proves the authority holds what it decoded.
const observedAtUnixMillis = Date.parse("2026-10-23T12:00:00.000Z");
const statement = await buildPresenceStatement({
bundle: bundleFacts,
state: "warming",
detail: "probe",
boundEndpoint: null,
writeLeaseSha256: null,
observedAtUnixMillis,
publisherSequence: new PublisherSequence().next(observedAtUnixMillis),
providerHealthIdentityPath: config.providerHealthIdentityPath,
env: descriptors.env,
});
expect(statement.target).toBe("heimdall");
const first = authority.sign("warming", "probe", observedAtUnixMillis);
const second = authority.sign("warming", "probe", observedAtUnixMillis);
expect(first.target).toBe("heimdall");
expect(second.target).toBe("heimdall");
expect(Buffer.from(second.payload).equals(Buffer.from(first.payload))).toBe(false);
} finally {
descriptors.close();
}