From 67b8ae3eda1915a86aecb1befbbbb8676be44696 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 29 Sep 2026 03:13:28 +0200 Subject: [PATCH] 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 --- .../cultnet-ts/src/idunn-runtime-authority.ts | 41 ++++++++--- .../test/idunn-runtime-authority.test.ts | 72 +++++++++++++++++++ 2 files changed, 104 insertions(+), 9 deletions(-) diff --git a/packages/cultnet-ts/src/idunn-runtime-authority.ts b/packages/cultnet-ts/src/idunn-runtime-authority.ts index bb217ed0..cd942cec 100644 --- a/packages/cultnet-ts/src/idunn-runtime-authority.ts +++ b/packages/cultnet-ts/src/idunn-runtime-authority.ts @@ -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(); const authorityKeys = new WeakMap(); 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 extends readonly (infer U)[] + ? readonly DeepReadonly[] + : T extends object ? { readonly [K in keyof T]: DeepReadonly } : 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(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), }; } diff --git a/packages/cultnet-ts/test/idunn-runtime-authority.test.ts b/packages/cultnet-ts/test/idunn-runtime-authority.test.ts index 1df6e98f..5f34c02c 100644 --- a/packages/cultnet-ts/test/idunn-runtime-authority.test.ts +++ b/packages/cultnet-ts/test/idunn-runtime-authority.test.ts @@ -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); +});