Heimdall/tests/presence-identity-descriptor.test.ts
Meta Vens 55732a901d Presence: the publisher sequence is in-process, the state root keeps no owner-only file, an Idunn generation never self-enrolls
nextPublisherSequence read and rewrote <dataRoot>/runtime-presence-sequence at
mode 0600, which the next generation (a fresh DynamicUser uid in the shared
state group) cannot open. Odin compares publisher_sequence only within one
activation and Idunn mints a new activation per start, so PublisherSequence
derives it from the observation clock, never below the previous value plus
one, as Muninn does. openProviderHealthIdentity under
GAMECULT_IDUNN_RUNTIME_BUNDLE now refuses when the presence descriptor is
absent instead of enrolling an identity Idunn does not trust.

Adds the cross-generation test (root only) and the sequence, no-dataRoot and
missing-descriptor tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 14:25:11 +02:00

140 lines
5.6 KiB
TypeScript

import { access, mkdtemp, readFile, writeFile } from "node:fs/promises";
import { openSync, closeSync } 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 { describe, expect, it } from "vitest";
import {
openOrEnrollProviderHealthIdentity,
openProviderHealthIdentity,
passedDescriptor,
providerHealthPublicIdentity,
} from "../src/provider-health-identity.js";
const machineIdAvailable = await readFile("/etc/machine-id", "utf8").then(
() => true,
() => false
);
/**
* 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 () => {
const dir = await mkdtemp(path.join(os.tmpdir(), "heimdall-presence-"));
// 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 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(":");
const opened = await openProviderHealthIdentity(
path.join(dir, "would-be-self-enrolled.cc"),
{ LISTEN_FDNAMES: names, LISTEN_PID: String(process.pid) }
);
// 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);
}
});
});
describe("presence identity descriptor selection", () => {
const dir = os.tmpdir();
it("takes the named descriptor even when LISTEN_PID is another namespace's pid", () => {
// Idunn launches candidates with PrivatePIDs=yes, so systemd sets
// LISTEN_PID to the pid it knows in the outer namespace while this process
// sees a namespace-local one. They never match. Refusing the descriptor on
// that basis sent Heimdall down the self-enrolling path, where it signed
// health with a key nothing trusts and warmed forever.
//
// This asserts the *selection*, not a read. An earlier version of this test
// called openProviderHealthIdentity, which then did readFileSync on fd 3 --
// a live handle inside the vitest worker. On Windows that blocks instead of
// throwing, which hung the worker and took the whole suite with it. The
// decision under test is which descriptor gets chosen; reading a real one
// is not part of it.
expect(
passedDescriptor("gamecult-runtime-presence-identity", {
LISTEN_FDNAMES: "gamecult-runtime-presence-identity",
LISTEN_PID: String(process.pid + 1),
})
).toBe(3);
});
it("ignores a descriptor set that does not name the presence identity", async () => {
await openProviderHealthIdentity(
path.join(dir, `heimdall-other-fd-${process.pid}.cc`),
{ LISTEN_FDNAMES: "some-other-credential", LISTEN_PID: String(process.pid) }
).catch((error) => {
expect(String(error)).not.toContain("Idunn runtime presence identity");
});
});
});
describe("an Idunn generation without the presence identity descriptor", () => {
it("refuses, naming the descriptor, and enrolls nothing", async () => {
const dir = await mkdtemp(path.join(os.tmpdir(), "heimdall-nodescriptor-"));
const identityPath = path.join(dir, "provider-health-identity.cc");
await expect(
openProviderHealthIdentity(identityPath, {
GAMECULT_IDUNN_RUNTIME_BUNDLE: "/run/idunn/bundle",
LISTEN_FDNAMES: "some-other-descriptor",
})
).rejects.toThrow("gamecult-runtime-presence-identity");
await expect(access(identityPath)).rejects.toMatchObject({ code: "ENOENT" });
});
it("still self-enrolls outside Idunn", async () => {
if (!machineIdAvailable) return;
const dir = await mkdtemp(path.join(os.tmpdir(), "heimdall-outside-idunn-"));
const identityPath = path.join(dir, "provider-health-identity.cc");
await openProviderHealthIdentity(identityPath, {});
await expect(access(identityPath)).resolves.toBeUndefined();
});
});