/** * Runtime secrets: values the runtime mints for its own use, one document per * space and name, which executed code can neither choose, read nor use. The * salt `sqliteQuery` hashes into each result row document's id is one: * without it a reader could recompute a row's id from a guess at the row's * content, and so confirm the guess without reading the row. * * The id namespace is reserved. The transaction write chokepoint refuses every * unprivileged write to it, and the read chokepoint every read of a secret's * value but this module's own, whose marker is private to it, so no executed * code holds a secret. * `IExtendedStorageTransaction.ensureRuntimeSecret()` is the one writer: it * mints a random value when no trusted one is stored, hands none back, and * takes the runtime's in-package authorization, so executed code cannot mint * a secret in its own transaction and read it there before its label is * stored. * * A stored value is trusted only when its stored schema carries the writer * claim `writeAuthorizedBy: [RUNTIME_SECRET_WRITER]`, or when the transaction * reading it minted it. The runtime committing a write refuses a claim whose * writer is not the builtin it names, and no runtime lets executed code act as * a builtin, so a value that code planted in the namespace, before the * chokepoint existed or through a runtime without it, carries no such claim. * `ensureRuntimeSecret()` replaces an untrusted value. A value whose stored * schema is named but resolves neither in the replica nor in the schema * registry is neither trusted nor untrusted, and reading it throws: a replica * can hold a document before the schema document its metadata names, and the * mint writing over it then would re-roll a trusted secret with nothing in * its commit to catch the overwrite. * * A secret is labeled with {@link RUNTIME_SECRET_LABEL}, an atom no reader * holds and no ceiling admits. The runtime reads its own secrets with * {@link readRuntimeSecret}, as a verifier-internal read that joins nothing * to the transaction's flow label. */ import type { MemorySpace } from "@commonfabric/memory/interface"; import { isObjectOrArray } from "@commonfabric/utils/types"; import type { JSONSchema } from "./builder/types.ts"; import { ContextualFlowControl } from "./cfc.ts"; import { readStoredCfcMetadata } from "./cfc/metadata.ts"; import { CFC_LABEL_READ_FAILED_ATOM } from "./cfc/observation.ts"; import { loadSchemaDocument } from "./cfc/prepare.ts"; import type { NormalizedFullLink } from "./link-utils.ts"; import { isRuntimeSecretId, RUNTIME_SECRET_ID_PREFIX, } from "./runtime-secret-id.ts"; import type { URI } from "./sigil-types.ts"; import type { IExtendedStorageTransaction, IMemorySpaceAddress, Metadata, } from "./storage/interface.ts"; import { internalVerifierRead } from "./storage/reactivity-log.ts"; /** * The confidentiality a runtime secret is stored under: the read-failed * marker, which `cfcObservationFitsCeiling` treats as ungrantable, so a value * derived from a secret fits no ceiling, even one that names the marker. */ export const RUNTIME_SECRET_LABEL = { confidentiality: [CFC_LABEL_READ_FAILED_ATOM], } as const; /** The builtin identity a runtime secret is minted under. */ export const RUNTIME_SECRET_WRITER = "runtime-secret"; /** The schema a runtime secret is written under. */ export const RUNTIME_SECRET_SCHEMA = { type: "string", ifc: { ...RUNTIME_SECRET_LABEL, writeAuthorizedBy: [RUNTIME_SECRET_WRITER] }, } as const; /** Returns the id of the runtime secret called `name`. */ export const runtimeSecretId = (name: string): URI => `${RUNTIME_SECRET_ID_PREFIX}${name}` as URI; /** Returns the address of the runtime secret called `name` in `space`. */ export const runtimeSecretLink = ( space: MemorySpace, name: string, ): NormalizedFullLink => ({ space, id: runtimeSecretId(name), scope: "space", path: [], }); /** * Returns whether a read of `address` reads a runtime secret's value: the * whole document, or a path inside its value. A read of another field, such * as the label envelope, holds nothing secret. */ export const readsRuntimeSecretValue = ( address: Pick, ): boolean => isRuntimeSecretId(address.id) && (address.path.length === 0 || address.path[0] === "value"); const ownReadMarker: unique symbol = Symbol("runtimeSecretOwnReadMarker"); /** * What this module's reads carry under their marker. The read chokepoint * compares it by identity, so metadata that answers every lookup, such as a * proxy a caller hands a cell read, cannot pass for one of these reads; a * copy of the metadata, such as the merge with a transaction's ambient read * metadata, keeps it. */ const ownReadToken: unique symbol = Symbol("runtimeSecretOwnReadToken"); /** This module's read of a secret's value: verifier-internal, and marked. */ const ownRead: Metadata = { ...internalVerifierRead, [ownReadMarker]: ownReadToken, }; /** * Returns whether `meta` marks this module's read of a runtime secret's * value, the only read of it the read chokepoint admits from a transaction * outside a privileged write. The marker and its token are private to this * module. */ export const isRuntimeSecretOwnRead = (meta?: Metadata): boolean => meta?.[ownReadMarker] === ownReadToken; /** * Thrown by a read of a runtime secret whose stored metadata names a schema * that resolves neither in the replica nor in the schema registry, so that * whether the runtime wrote it is unknown. */ export class RuntimeSecretUnresolvedError extends Error { /** Constructs an instance for the secret at `id`. */ constructor(id: string, options: { cause: unknown }) { super(`${id}: the schema stored with it cannot be resolved`, options); this.name = "RuntimeSecretUnresolvedError"; } } /** * Returns the runtime secret called `name` in `space`, or `undefined` when no * trusted one is stored: none at all, or a value whose stored schema does not * carry the runtime's writer claim and which this transaction did not mint. * The reads are verifier-internal: they stay in the transaction's conflict * set, so a secret minted concurrently elsewhere fails this transaction's * commit rather than going unseen, and they join nothing to the flow label. * * @throws RuntimeSecretUnresolvedError when whether the runtime wrote the * stored value is unknown. */ export const readRuntimeSecret = ( tx: IExtendedStorageTransaction, space: MemorySpace, name: string, ): string | undefined => { const link = runtimeSecretLink(space, name); const value = tx.readValueOrThrow(link, { meta: ownRead }); // The writer is settled before the value's type: a stored value whose // writer is unknown makes the read throw whatever the value is, so the // mint never replaces it. const trusted = mintedIn(tx, link) || carriesWriterClaim(tx, link); return trusted && typeof value === "string" ? value : undefined; }; /** Returns whether `tx` wrote the document `link` names. */ const mintedIn = ( tx: IExtendedStorageTransaction, link: NormalizedFullLink, ): boolean => [...(tx.getWriteDetails?.(link.space) ?? [])].some((detail) => detail.address.id === link.id ); /** * Returns whether the schema stored with the document `link` names claims it * for {@link RUNTIME_SECRET_WRITER} at its root. A document with no stored * metadata carries no claim. * * @throws RuntimeSecretUnresolvedError when the stored metadata names a schema * that cannot be resolved. */ const carriesWriterClaim = ( tx: IExtendedStorageTransaction, link: NormalizedFullLink, ): boolean => { const metadata = readStoredCfcMetadata(tx, link); if (metadata === undefined) return false; let stored: JSONSchema; try { stored = loadSchemaDocument(tx, link.space, metadata.schemaHash); } catch (error) { throw new RuntimeSecretUnresolvedError(link.id, { cause: error }); } const schema = ContextualFlowControl.getSchemaAtPath(stored, []); const claim = isObjectOrArray(schema) && isObjectOrArray(schema.ifc) ? schema.ifc.writeAuthorizedBy : undefined; return Array.isArray(claim) && claim.includes(RUNTIME_SECRET_WRITER); };