cultnet-ts: freeze the Idunn authority and allow one signer per authority

The loaded authority is deeply frozen (bind, health contract, Expected,
Activation, identities), so a caller cannot steer what the signer signs; the
typed Buffers that could not be frozen are dropped (Expected canonical bytes,
Activation public key kept as hex only for the load-time comparison). A second
createIdunnRuntimeSigner on the same authority throws, so two signers cannot
emit sequence 1 under one runtimeInstanceId.

Tests: stateful set pinned against Rust (warming, active, degraded), lease
accepted for a route-answered warming, refused for another warming and another
target, one signer per authority, frozen authority.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-29 03:13:28 +02:00
commit 67b8ae3eda
2 changed files with 104 additions and 9 deletions

View file

@ -30,6 +30,7 @@ const ACTIVATION_ID_DOMAIN = Buffer.from("idunn.runtime-activation.id.v1\0", "ut
const CULTCACHE_STORE_FORMAT = "cultcache.store.v1";
const ED25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
const MAX_RECENT_WARMING_PROOFS = 64;
const authoritiesWithSigner = new WeakSet<object>();
const authorityKeys = new WeakMap<object, { providerPrivateKey: crypto.KeyObject; activationPrivateKey: crypto.KeyObject }>();
type ExpectedIncarnation = {
@ -51,7 +52,6 @@ type ExpectedIncarnation = {
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;
};
@ -61,18 +61,27 @@ type RuntimeActivation = {
runtimeId: string;
runtimeInstanceId: string;
activationSignerIdentityId: string;
activationSignerPublicKey: Buffer;
activationSignerPublicKeyHex: string;
canonicalSha256: string;
};
export type IdunnRuntimeAuthority = {
type DeepReadonly<T> = T extends readonly (infer U)[]
? readonly DeepReadonly<U>[]
: T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T;
/**
* What Idunn admitted for this launch. Deeply frozen at load: bind, health
* contract, Expected, Activation and identities are read-only for the life of
* the process, so no caller can steer what the signer signs.
*/
export type IdunnRuntimeAuthority = DeepReadonly<{
expected: ExpectedIncarnation;
activation: RuntimeActivation;
providerSignerIdentityId: string;
activationSignerIdentityId: string;
boundEndpoint: string;
processWriteLeasePath?: string;
};
}>;
export type SignedRuntimePresence = {
document: CultNetRawDocumentRecord;
@ -152,7 +161,7 @@ export function loadIdunnRuntimeAuthorityFromEnvironment(
const activationPublicKey = rawEd25519PublicKey(crypto.createPublicKey(activationPrivateKey));
const activationSignerIdentityId = sha256Hex(Buffer.concat([ACTIVATION_ID_DOMAIN, activationPublicKey]));
if (activationSignerIdentityId !== activationValue.activationSignerIdentityId
|| !activationPublicKey.equals(activationValue.activationSignerPublicKey)) {
|| activationPublicKey.toString("hex") !== activationValue.activationSignerPublicKeyHex) {
throw new Error("Idunn activation credential does not match the root-provisioned Activation witness.");
}
@ -167,7 +176,7 @@ export function loadIdunnRuntimeAuthorityFromEnvironment(
throw new Error("Provider identity does not match Idunn Expected.");
}
const authority: IdunnRuntimeAuthority = {
const authority: IdunnRuntimeAuthority = deepFreeze({
expected: expectedValue,
activation: activationValue,
providerSignerIdentityId: expectedValue.expectedSignerIdentityId,
@ -176,7 +185,7 @@ export function loadIdunnRuntimeAuthorityFromEnvironment(
...(process.env[IDUNN_PROCESS_WRITE_LEASE_ENVIRONMENT]
? { processWriteLeasePath: process.env[IDUNN_PROCESS_WRITE_LEASE_ENVIRONMENT] }
: {}),
};
});
authorityKeys.set(authority, { providerPrivateKey, activationPrivateKey });
return authority;
}
@ -220,8 +229,15 @@ export function signIdunnRuntimePresence(
};
}
/**
* One signer per loaded authority: the publisher sequence and the warming proofs
* are the signer's, and a second signer would restart the sequence under the
* same runtimeInstanceId.
*/
export function createIdunnRuntimeSigner(options: IdunnRuntimeSignerOptions): IdunnRuntimeSigner {
const { authority } = options;
if (authoritiesWithSigner.has(authority)) throw new Error("This Idunn runtime authority already has a signer.");
authoritiesWithSigner.add(authority);
const expected = authority.expected;
let sequence = 0;
let health: RuntimePresenceHealth["state"] = "warming";
@ -291,6 +307,14 @@ export function createIdunnRuntimeSigner(options: IdunnRuntimeSignerOptions): Id
};
}
function deepFreeze<T>(value: T): T {
if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
for (const child of Object.values(value)) deepFreeze(child);
Object.freeze(value);
}
return value;
}
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])) {
@ -360,7 +384,6 @@ function decodeExpected(payload: Uint8Array): ExpectedIncarnation {
route,
capabilities: decodeExpectedCapabilities(values[16]),
dependencies: decodeExpectedDependencies(values[17]),
canonicalBytes,
canonicalSha256: prefixedSha256(canonicalBytes),
};
}
@ -388,7 +411,7 @@ function decodeActivation(payload: Uint8Array): RuntimeActivation {
runtimeId: string(values[2], "Activation runtime id"),
runtimeInstanceId: string(values[3], "Activation instance id"),
activationSignerIdentityId,
activationSignerPublicKey: Buffer.from(publicKey),
activationSignerPublicKeyHex: Buffer.from(publicKey).toString("hex"),
canonicalSha256: prefixedSha256(canonicalBytes),
};
}

View file

@ -291,3 +291,75 @@ test("the route answer and the Odin publish report the same state, because the s
[1, 3],
);
});
// The lease-bound set: Rust issued lease.cc naming presence-warming.bin's digest, and presence-active.bin
// and presence-degraded.bin carry that lease's digest.
const STATEFUL = "service-stateful" as const;
function statefulSigner(context: TestContext, lease = "lease.cc") {
return signerFor(context, STATEFUL, 0, lease);
}
test("stateful: warming, active and degraded match the Rust-verified vectors byte for byte, active and degraded bound to Rust's lease", (context) => {
freezeClock(context);
const signer = statefulSigner(context);
assert.deepEqual(Buffer.from(signer.answerRouteObservation(CHALLENGE).payload), fixtureFile(STATEFUL, "presence-warming.bin"));
signer.reportHealth("active");
const active = signer.answerRouteObservation(CHALLENGE);
assert.deepEqual(Buffer.from(active.payload), fixtureFile(STATEFUL, "presence-active.bin"));
assert.match(String(presenceSlot(active, RUNTIME_PRESENCE_SLOT.writeLeaseSha256)), /^sha256-[0-9a-f]{64}$/);
const degraded = statefulSigner(context);
degraded.answerRouteObservation(CHALLENGE);
degraded.reportHealth("degraded");
assert.deepEqual(Buffer.from(degraded.answerRouteObservation(CHALLENGE).payload), fixtureFile(STATEFUL, "presence-degraded.bin"));
});
test("a warming answered on the route becomes a lease target: Rust's lease naming it is accepted", (context) => {
freezeClock(context);
const signer = statefulSigner(context);
assert.throws(() => signer.assertWriteLease(), /does not match the current Expected incarnation/, "no warming signed yet");
signer.answerRouteObservation(CHALLENGE);
assert.equal(signer.assertWriteLease(), presenceSlot({ payload: fixtureFile(STATEFUL, "presence-active.bin") }, RUNTIME_PRESENCE_SLOT.writeLeaseSha256));
});
for (const [lease, reason] of [["lease-other-warming.cc", "names another warming digest"], ["lease-other-target.cc", "names another target"]] as const) {
test(`a lease that ${reason} is refused, and the signer cannot sign active under it`, (context) => {
freezeClock(context);
const signer = statefulSigner(context, lease);
signer.answerRouteObservation(CHALLENGE);
assert.throws(() => signer.assertWriteLease(), /does not match the current Expected incarnation/);
signer.reportHealth("active");
assert.throws(() => signer.answerRouteObservation(CHALLENGE), /does not match the current Expected incarnation/);
});
}
test("one signer per loaded authority", (context) => {
const authority = openAuthority(context, "web");
const capabilities = expectedFacts("web").capabilities;
createIdunnRuntimeSigner({ authority, capabilities });
assert.throws(() => createIdunnRuntimeSigner({ authority, capabilities }), /already has a signer/);
});
test("the loaded authority is frozen: the bind, contract, Expected, Activation and identities cannot change, and the signer signs the loaded endpoint", (context) => {
freezeClock(context);
const authority = openAuthority(context, "web");
const loaded = expectedFacts("web");
const open = authority as unknown as { [key: string]: any };
const mutations: Array<[string, () => void]> = [
["boundEndpoint", () => { open.boundEndpoint = "http://127.0.0.1:1"; }],
["providerSignerIdentityId", () => { open.providerSignerIdentityId = "forged"; }],
["activationSignerIdentityId", () => { open.activationSignerIdentityId = "forged"; }],
["expected.healthContract", () => { open.expected.healthContract = "forged"; }],
["expected.target", () => { open.expected.target = "forged"; }],
["expected.canonicalSha256", () => { open.expected.canonicalSha256 = "forged"; }],
["expected.route.candidateEndpoint", () => { open.expected.route.candidateEndpoint = "http://127.0.0.1:1"; }],
["expected.capabilities[0].minimumCapacity", () => { open.expected.capabilities[0].minimumCapacity = 0; }],
["expected.capabilities.push", () => { open.expected.capabilities.push({}); }],
["activation.runtimeInstanceId", () => { open.activation.runtimeInstanceId = "forged"; }],
["activation.canonicalSha256", () => { open.activation.canonicalSha256 = "forged"; }],
];
for (const [name, mutate] of mutations) assert.throws(mutate, TypeError, name);
const signer = createIdunnRuntimeSigner({ authority, capabilities: loaded.capabilities });
assert.equal(presenceSlot(signer.answerRouteObservation(CHALLENGE), RUNTIME_PRESENCE_SLOT.boundEndpoint), loaded.candidate);
});