cultnet-ts: Idunn authority files are read by cultcache-ts's one store reader
readAuthorityRecord decoded the store itself and accepted only cultcache.store.v1. It now calls readSingleFileStore, so v3 reads, a zero-byte file is a StoreUnreadableError, and a refused header is never echoed. Tests derive their bundles from the Rust-written fixtures. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
0122761a74
commit
8a833730c9
2 changed files with 58 additions and 22 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { readSingleFileStore } from "@gamecult/cultcache-ts";
|
||||
import { decode, encode } from "@msgpack/msgpack";
|
||||
|
||||
import {
|
||||
|
|
@ -27,7 +28,6 @@ export const IDUNN_RUNTIME_CANDIDATE_BIND_ENVIRONMENT = "GAMECULT_IDUNN_CANDIDAT
|
|||
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");
|
||||
// Limit: more than this many warmings signed between Idunn's observation and
|
||||
// assertWriteLease would push the leased one out and refuse a valid lease.
|
||||
|
|
@ -318,25 +318,13 @@ function deepFreeze<T>(value: T): T {
|
|||
}
|
||||
|
||||
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] };
|
||||
const store = readSingleFileStore(filePath);
|
||||
if (store.absent) throw new Error(`Idunn authority file ${filePath} does not exist.`);
|
||||
if (store.envelopes.length !== 1) throw new Error(`Idunn authority file ${filePath} must contain exactly one record.`);
|
||||
const record = store.envelopes[0]!;
|
||||
if (record.schemaId !== schemaId) throw new Error(`Idunn authority file must contain exactly one ${schemaId} record.`);
|
||||
if (record.type !== type) throw new Error(`Idunn authority record has unexpected CultCache type ${record.type}.`);
|
||||
return { payload: record.payload };
|
||||
}
|
||||
|
||||
function decodeExpected(payload: Uint8Array): ExpectedIncarnation {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ import assert from "node:assert/strict";
|
|||
import { spawnSync } from "node:child_process";
|
||||
import dgram from "node:dgram";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { StoreUnreadableError } from "@gamecult/cultcache-ts";
|
||||
import { decode, encode } from "@msgpack/msgpack";
|
||||
|
||||
import {
|
||||
|
|
@ -54,7 +56,7 @@ function expectedFacts(set: FixtureSet) {
|
|||
function openAuthority(
|
||||
context: TestContext,
|
||||
set: FixtureSet,
|
||||
options: { machineId?: "etc" | "dbus"; lease?: string } = {},
|
||||
options: { machineId?: "etc" | "dbus"; lease?: string; bundle?: string } = {},
|
||||
): IdunnRuntimeAuthority {
|
||||
const facts = expectedFacts(set);
|
||||
const originalEnvironment = { ...process.env };
|
||||
|
|
@ -62,7 +64,7 @@ function openAuthority(
|
|||
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 = path.join(FIXTURES, set);
|
||||
process.env.GAMECULT_IDUNN_RUNTIME_BUNDLE = options.bundle ?? path.join(FIXTURES, set);
|
||||
process.env.GAMECULT_IDUNN_CANDIDATE_BIND = facts.candidate;
|
||||
if (options.lease) process.env.GAMECULT_IDUNN_PROCESS_WRITE_LEASE = path.join(FIXTURES, set, options.lease);
|
||||
else delete process.env.GAMECULT_IDUNN_PROCESS_WRITE_LEASE;
|
||||
|
|
@ -84,6 +86,22 @@ function openAuthority(
|
|||
return loadIdunnRuntimeAuthorityFromEnvironment(facts.target, facts.healthContract);
|
||||
}
|
||||
|
||||
/** A copy of one Rust-written bundle in a scratch directory, with `change` applied to the bytes of the named file. */
|
||||
function bundleWith(context: TestContext, set: FixtureSet, file: string, change: (bytes: Buffer) => Buffer | undefined): string {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "idunn-authority-"));
|
||||
context.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
for (const name of ["expected.cc", "activation.cc"]) {
|
||||
const bytes = fixtureFile(set, name);
|
||||
const changed = name === file ? change(bytes) : bytes;
|
||||
if (changed !== undefined) fs.writeFileSync(path.join(dir, name), changed);
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
function withHeader(header: string): (bytes: Buffer) => Buffer {
|
||||
return (bytes) => Buffer.from(encode([header, ...(decode(bytes) as unknown[]).slice(1)]));
|
||||
}
|
||||
|
||||
function signerFor(context: TestContext, set: FixtureSet, capacityDelta = 0, lease?: string) {
|
||||
const authority = openAuthority(context, set, { lease });
|
||||
const capabilities = expectedFacts(set).capabilities.map((capability) => ({ ...capability, capacity: capability.capacity + capacityDelta }));
|
||||
|
|
@ -133,6 +151,36 @@ test("rejects a credential file holding more than one envelope", (context) => {
|
|||
);
|
||||
});
|
||||
|
||||
// The authority files are read by cultcache-ts's one store reader, so a store it reads is read here and a file it
|
||||
// refuses is refused here with the same typed error and the same text.
|
||||
for (const file of ["expected.cc", "activation.cc"]) {
|
||||
test(`${file}: a store marked for element ids opens, as the one reader reads it`, (context) => {
|
||||
const bundle = bundleWith(context, "web", file, withHeader("cultcache.store.v3"));
|
||||
assert.equal(openAuthority(context, "web", { bundle }).expected.target, expectedFacts("web").target);
|
||||
});
|
||||
|
||||
test(`${file}: a zero-byte file is refused as unreadable, not read as an empty store or a missing one`, (context) => {
|
||||
const bundle = bundleWith(context, "web", file, () => Buffer.alloc(0));
|
||||
assert.throws(() => openAuthority(context, "web", { bundle }), StoreUnreadableError);
|
||||
});
|
||||
|
||||
test(`${file}: a file that is not there is refused`, (context) => {
|
||||
const bundle = bundleWith(context, "web", file, () => undefined);
|
||||
assert.throws(() => openAuthority(context, "web", { bundle }), /does not exist/);
|
||||
});
|
||||
|
||||
test(`${file}: a refused header is never echoed, whether or not it starts at the store prefix`, (context) => {
|
||||
for (const header of ["cultcache.store.SECRET", "cultcache.store.SECRETcultcache.store.v9", "SECRETcultcache.store.v9", "cultcache.store.v9"]) {
|
||||
const bundle = bundleWith(context, "web", file, withHeader(header));
|
||||
assert.throws(
|
||||
() => openAuthority(context, "web", { bundle }),
|
||||
(error: Error) => error instanceof StoreUnreadableError && (header === "cultcache.store.v9" ? error.message.includes(header) : !error.message.includes("SECRET")),
|
||||
header,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function presenceSlot(document: { payload: Uint8Array }, slot: number): unknown {
|
||||
return (decode(document.payload) as unknown[])[slot];
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue