/** * Seals an actor's reviewed value into the custody of a trusted declassifier: * a module policy `P` whose release rules admit only what that module's * endorsed code computes over values this host operation wrote. This module is * absent from authored pattern imports; the trusted host drives it from a * confirmation dialog, as it drives `share-snapshot.ts`. * * One instance of a custody room is `(P, D)`: `P` names the room's policy with * the room space `S` as its subject, and `D` is the digest of the room's * terms. Each seal writes one entry into the instance's box, a single document * in `S` whose every location carries `TransformedBy{builtin cfc-custody-seal}`, * so a transformation that reads the whole box earns the input witness * (`docs/specs/cfc-transformed-by-input-witnesses.md`) and a transformation * that reads anything else beside it does not. * * Entries are keyed by a blinded key, a digest of the actor's signature over * `(P, D)`, and neither the entry's value nor its label names the actor. The * key is deterministic for one actor and unpredictable to anyone without the * actor's signing key, which is what lets the seal refuse a second entry by * the same actor without telling the room who sealed. What the blinding does * not hide, and the rest of the contract, is * `docs/specs/cfc-custody-seal.md`. */ import type { JSONValue } from "@commonfabric/api"; import { CFC_ATOM_TYPE, type CfcAtom, cfcAtom, type CfcModulePolicyRefAtom, } from "@commonfabric/api/cfc"; import { sha256 } from "@commonfabric/content-hash"; import { debugStr, deepFreeze, hashStringOf } from "@commonfabric/data-model"; import { type DID, isDID, isWellFormedDID } from "@commonfabric/identity/did"; import { aclDocId, ANYONE_USER, type Capability, hasConcreteOwner, isACL, } from "@commonfabric/memory/acl"; import { toUnpaddedBase64url } from "@commonfabric/utils/base64url"; import { deepEqual } from "@commonfabric/utils/deep-equal"; import { isObjectNotArray } from "@commonfabric/utils/types"; import { utf8Compare } from "@commonfabric/utils/utf8"; import { profileNameWrittenAt } from "../builder/principal-of.ts"; import { type Cell, cellRuntime, isCell } from "../cell.ts"; import { resolveLink } from "../link-resolution.ts"; import { isPrimitiveCellLink, isWriteRedirectLink, type NormalizedFullLink, parseLink, } from "../link-utils.ts"; import type { Runtime } from "../runtime.ts"; import { normalizeCellScope } from "../scope.ts"; import type { IExtendedStorageTransaction } from "../storage/interface.ts"; import { internalVerifierRead } from "../storage/reactivity-log.ts"; import { matchAtomPattern } from "./atom-pattern.ts"; import { type CfcConfClause, clauseAlternatives, clausesEqual, } from "./clause.ts"; import { evaluateExchangeRules, modulePolicyRecordId, } from "./exchange-eval.ts"; import { canonicalJson, containsAtomPatternVariable, evidenceHolds, hasExactKeys, isActorOwnedAlternative, isTrustedGestureOn, type ReadEvidence, readEvidence, rootWrittenByBuiltin, syncResolved, } from "./host-review.ts"; import { cfcLabelViewForResolvedCellWithStatus } from "./label-view.ts"; import { cfcLabelViewFromMetadata } from "./label-view-state.ts"; import { readStoredCfcMetadata } from "./metadata.ts"; import { cfcPolicyManifestDocId, type PolicyTemplateV1 } from "./policy.ts"; import { collectConsumedLabel } from "./prepare.ts"; import { CfcReadCeilingError } from "./read-ceiling.ts"; import { mintedProfilePrincipals, principalClaimEntries, PROFILE_NAME_FIELD, representsPrincipalEntries, } from "./represents-principal.ts"; import { snapshotJsonValue } from "./share-snapshot-value.ts"; import { type CfcTrustConfig, createTrustResolver } from "./trust.ts"; import { runtimeWritePolicyAuthorization } from "./types.ts"; import { setCfcImplementationIdentity } from "../storage/extended-storage-transaction.ts"; /** Builtin implementation identity that alone may write a custody box. */ export const CUSTODY_SEAL_WRITER = "cfc-custody-seal"; /** * Trust concept a room's policy must satisfy, under the actor's own trust * closure, before the actor's value may enter its custody. */ export const TRUSTED_DECLASSIFIER_CONCEPT = "https://commonfabric.org/cfc/concepts/trusted-declassifier"; /** The `provenance.ui.pattern` mark of the host's confirmation gesture. */ export const CUSTODY_SEAL_GESTURE = "CustodySeal"; /** The room a value is sealed into. */ export interface CustodyRoom { /** * The room's write-once terms document. Its space is the room space `S`. * The terms are a JSON object naming at least `seats`, the DIDs that may * seal, and `stanceSchema`, the instruction-inert schema every sealed value * satisfies; the rest is displayed to the actor and sealed with the value. */ readonly terms: Cell; /** * The room's custody policy; its subject must be the room space. A host * whose policy reference is stored passes the cell holding it, or a cell * whose stored label declares it, as a pattern's cell declared `PolicyOf` * the room's rules does: the seal then reads the cell at prepare and again * at commit, refuses a commit whose reading differs from the one the actor * reviewed, and has the transaction that writes the entry verify that the * cell still holds it. */ readonly policy: CfcModulePolicyRefAtom | Cell; /** * The room's cell that receives the link to the instance's box: the cell * the room's projector reads its box through. The seal writes the link in * the transaction that writes the entry, attributed to the seal, so a * release rule that requires the seal's witness covers which document the * room reads as its box as well as what that document holds. It must be * in the room space. */ readonly box?: Cell; } /** Host-supplied bounds on what the actor may seal into this room. */ export interface CustodySealOptions { /** * The actor's own `Context` and `Resource` sources this room may draw on. A * value whose label names any other source is refused; an empty list admits * only values that name none: one labeled for the actor alone (`User` or a * bare DID), as a value the actor typed in is, or one made the actor's own * from data shared with other people, whom the preview names (`heldWith`). * * A host passes the actor-private settings cell that holds the list, in the * form {@link readCustodySourcePolicy} reads, rather than a list it read * itself: the seal then reads the cell at prepare and again at commit, and * the transaction that writes the entry verifies that the cell still holds * what the commit read. A fixed list is for a caller whose allowance is not * stored: nothing binds a list to the write, so a stored policy narrowed * after the host read it does not refuse the seal. */ readonly allowedSources: readonly CfcAtom[] | Cell; } /** * A principal the room space's access list lets read the room, and so read * what the room releases: a DID, or `*` for anyone. */ export interface CustodyRoomReader { /** The principal's DID, or `*` for anyone. */ readonly principal: string; /** The capability the access list gives it. */ readonly role: "owner" | "writer" | "reader"; } /** Type-only brand for host-held consent objects. */ declare const consentBrand: unique symbol; /** One-use seal authority held only by the trusted host. */ export interface CustodySealConsent { /** Nominal token whose identity is verified against a private registry. */ readonly [consentBrand]: true; } /** Frozen preview for the trusted host's confirmation dialog. */ export interface PreparedCustodySeal { /** The authenticated actor whose value is sealed. */ readonly actor: string; /** The room space `S`: the space the terms document lives in. */ readonly room: string; /** * Who can read the room, from the room space's access list, ordered by * principal. The room's readers are the audience of anything it releases. */ readonly readers: readonly CustodyRoomReader[]; /** Exact value that enters custody. */ readonly stance: JSONValue; /** Terms the value is sealed under, exactly as they are sealed. */ readonly terms: JSONValue; /** The instance `D`: the digest of the terms. */ readonly instance: string; /** The room's custody policy, with the room space as its subject. */ readonly policy: CfcModulePolicyRefAtom; /** The actor's `Context` and `Resource` sources the value draws on. */ readonly sources: readonly CfcAtom[]; /** * The people the value was drawn from data shared with, such as a * conversation: one sorted group per distinct set of people a clause of * its label names beside the actor, the groups sorted. Empty when no clause names anyone * else. The receipt records them; the box entry does not. */ readonly heldWith: readonly (readonly DID[])[]; /** * Whether every release rule of the room's policy requires that everything * confidential its releasing code read was written by this seal (an * integrity guard on `TransformedBy` with the seal's builtin identity as its * input witness), and releases what it matches to the seal alone * (`releasesOnlyToSeal`), which publishes it once per instance. When it is * `false`, a member's own code can run the room's releasing code over the * actor's entry and values it made up, or read a projection whose stamp * says whether such values yield the released answer, and learn the entry * one answer at a time; the confirmation must say so rather than state a * bound on what an answer reveals. */ readonly witnessedRelease: boolean; /** One-use authority bound to this preview and authenticated actor. */ readonly consent: CustodySealConsent; } /** Host-supplied controls on one commit. */ export interface CustodySealCommitOptions { /** * Aborted when whoever asked for the seal can no longer see it land, such * as a host client that detached. The commit checks it before each write * and until the entry's transaction is sent, and an aborted commit throws * the signal's reason without writing the entry. It cannot recall a write * already sent, and a commit aborted after its receipt is written leaves * that receipt without an entry, which is how a seal that did not commit * reads. */ readonly signal?: AbortSignal; } /** What a committed seal wrote. */ export interface CustodySealResult { /** The instance's box, in the room space. */ readonly box: Cell; /** The blinded key of the actor's entry in the box. */ readonly entryKey: string; /** The instance `D` the entry was sealed into: the digest of the terms. */ readonly instance: string; /** The actor-private receipt, in the actor's home space. */ readonly receipt: Cell; } /** Everything one inspection establishes. */ interface Inspection { readonly actor: string; readonly allowedSources: readonly CfcAtom[]; readonly room: string; readonly readers: readonly CustodyRoomReader[]; readonly draftLink: NormalizedFullLink; readonly termsLink: NormalizedFullLink; readonly stance: JSONValue; readonly terms: JSONValue; readonly instance: string; readonly policy: CfcModulePolicyRefAtom; readonly sources: readonly CfcAtom[]; readonly heldWith: readonly (readonly DID[])[]; readonly entryKey: string; readonly witnessedRelease: boolean; readonly evidence: readonly ReadEvidence[]; } /** Runtime-owned state behind an opaque consent token. */ interface ConsentState extends Inspection { readonly draft: Cell; readonly requestedRoom: CustodyRoom; readonly options: CustodySealOptions; readonly eventId: string; } const consents = new WeakMap(); const MODULE_POLICY_KEYS = [ "type", "policyRefKind", "moduleIdentity", "symbol", "policyDigest", "subject", ] as const; const STANCE_SCHEMA_MAX_DEPTH = 8; const STANCE_SCHEMA_KEYS = new Set([ "type", "const", "enum", "properties", "required", "additionalProperties", "minimum", "maximum", "items", "minItems", "maxItems", "title", "description", ]); /** The keywords that describe an array, and nothing else. */ const STANCE_ARRAY_KEYS = ["items", "minItems", "maxItems"] as const; /** * The most elements a stance array may hold. An array is a list of ratings or * choices the confirmation shows one by one, so its bound is small. */ const MAX_STANCE_ARRAY_ITEMS = 64; const ENTRY_KEY_DOMAIN = "cfc-custody-seal/entry-key/v1\n"; /** The witness a release rule names: the seal, of any instance. */ const SEALED_BY = { type: CFC_ATOM_TYPE.TransformedBy, identity: { kind: "builtin", builtinId: CUSTODY_SEAL_WRITER }, }; /** The seal's identity when it acts for `instance`. */ const sealIdentity = (instance: string) => ({ kind: "builtin" as const, builtinId: CUSTODY_SEAL_WRITER, instance, }); /** * What the seal's writes for `instance` carry. A witness over them names the * instance, so an answer computed over another instance's box is told apart * from one computed over this one's. */ const sealedFor = (instance: string) => ({ type: CFC_ATOM_TYPE.TransformedBy, identity: sealIdentity(instance), }); /** Whether `atom` is the seal's `TransformedBy`, for any instance. */ const isSealStamp = (atom: unknown): boolean => isObjectNotArray(atom) && atom.type === CFC_ATOM_TYPE.TransformedBy && isObjectNotArray(atom.identity) && atom.identity.kind === "builtin" && atom.identity.builtinId === CUSTODY_SEAL_WRITER; /** * The DID an alternative would stand for if it were an actor-owned shape, for * naming an identity mismatch, or `undefined` when it has no such shape. */ const ownerShapedSubject = (atom: unknown): string | undefined => { if (typeof atom === "string") return isDID(atom) ? atom : undefined; if (!isObjectNotArray(atom) || typeof atom.subject !== "string") { return undefined; } const exact = atom.type === CFC_ATOM_TYPE.User ? hasExactKeys(atom, ["type", "subject"]) : atom.type === CFC_ATOM_TYPE.Context ? hasExactKeys(atom, ["type", "name", "subject"]) : atom.type === CFC_ATOM_TYPE.Resource ? hasExactKeys(atom, ["type", "class", "subject"]) : false; return exact && isDID(atom.subject) ? atom.subject : undefined; }; /** * The person an alternative names when it is another principal: a * well-formed DID, bare or as `User{subject}` on its exact key set. * `undefined` for anything else. */ const personOf = (atom: unknown): DID | undefined => { if (typeof atom === "string") { return isWellFormedDID(atom) ? atom : undefined; } return isObjectNotArray(atom) && atom.type === CFC_ATOM_TYPE.User && hasExactKeys(atom, ["type", "subject"]) && isWellFormedDID(atom.subject) ? atom.subject : undefined; }; /** * One alternative as a comparable key, with the actor as a bare DID and as * `User{subject}` the same principal: a store names its owner by bare DID * (`dbOwner()`), and a value private to its reader by `User`. */ const alternativeKey = (atom: CfcAtom, actor: string): string => isActorOwnedAlternative(atom, actor) && personOf(atom) === actor ? actor : canonicalJson(atom); /** What the draft's label says about whose the value is. */ interface ActorLabel { /** The actor's own `Context` and `Resource` sources the value draws on. */ readonly sources: CfcAtom[]; /** * The other people the shared clauses name: one sorted group per distinct * set of people. */ readonly heldWith: DID[][]; } /** * Checks that the actor alone can seal the draft, and returns the actor's * sources it draws on and the people it was drawn from data shared with. * * Every clause must admit the actor through an alternative of the actor's * own. A clause that also names other people is the actor's only when it is * absorbed (§3.1.8(5)): another clause, all of whose alternatives are the * actor's own, holds only alternatives the shared clause holds. The label * then admits exactly who that clause admits (§3.1.4), and the shared clause * adds no reader, so sealing it is sealing the actor's own value. That is a * value made the actor's own from data shared with others, as a message row * labeled for its participants and the store's owner (§13.12) is when it is * joined with a clause naming the actor alone. * * Beside the actor, a shared clause may name only people, `User{subject}` or * a bare DID, so that the dialog can name them; the seal refuses a policy, a * source or a space there. * * @throws If a clause is empty, carries a caveat, has no alternative of the * actor's own, names something other than a person beside the actor, or * names others and is not absorbed. */ const actorLabel = ( confidentiality: readonly CfcConfClause[], actor: string, ): ActorLabel => { const sources: CfcAtom[] = []; const own: Set[] = []; const shared: { keys: Set; people: DID[]; clause: unknown }[] = []; for (const clause of confidentiality) { const alternatives = clauseAlternatives(clause); if (alternatives.length === 0) { throw new Error( "Custody seal refuses an unsatisfiable clause; sealing it would open it", ); } if ( alternatives.some((atom) => isObjectNotArray(atom) && atom.type === CFC_ATOM_TYPE.Caveat ) ) { throw new Error( debugStr`Custody seal refuses a value that still carries a caveat: $quote${clause}`, ); } const owned = alternatives.filter((atom) => isActorOwnedAlternative(atom, actor) ); if (owned.length === 0) { const other = alternatives.map(ownerShapedSubject) .find((subject) => subject !== undefined && subject !== actor); if (other !== undefined) { throw new Error( `Custody seal identity mismatch: the value is labeled for \`${other}\`, and this runtime acts as \`${actor}\``, ); } throw new Error( debugStr`Custody seal refuses a clause the authenticated actor does not own: $quote,long${clause}`, ); } const keys = new Set( alternatives.map((atom) => alternativeKey(atom, actor)), ); const people: DID[] = []; for (const atom of alternatives) { if (isActorOwnedAlternative(atom, actor)) continue; const person = personOf(atom); if (person === undefined) { throw new Error( debugStr`Custody seal refuses a clause that names something other than a person beside the actor: $quote,long${clause}`, ); } if (!people.includes(person)) people.push(person); } if (people.length === 0) own.push(keys); else shared.push({ keys, people: people.sort(), clause }); for (const atom of owned) { if ( isObjectNotArray(atom) && (atom.type === CFC_ATOM_TYPE.Context || atom.type === CFC_ATOM_TYPE.Resource) && !sources.some((source) => deepEqual(source, atom)) ) sources.push(atom as CfcAtom); } } const heldWith: DID[][] = []; for (const { keys, people } of shared) { const absorbed = own.some((alone) => [...alone].every((key) => keys.has(key)) ); if (!absorbed) { throw new Error( `Custody seal refuses a value \`${ people[0] }\` can also read: no clause of its label admits this runtime's actor, \`${actor}\`, alone`, ); } if (!heldWith.some((group) => deepEqual(group, people))) { heldWith.push(people); } } heldWith.sort((a, b) => canonicalJson(a) < canonicalJson(b) ? -1 : 1); return { sources, heldWith }; }; /** Stands in for a property the stance leaves out: its schema alone is checked. */ const UNUSED_PROPERTY: unique symbol = Symbol("unused property"); /** * Checks that `value` satisfies `schema`, and that `schema` admits only * instruction-inert values: booleans, bounded numbers, constants, and * enumerated primitives, composed by closed objects and by arrays whose one * `items` schema is itself inert and whose length `maxItems` bounds. An array * is a list of closed values, as a rating per option is, and never free text: * every element meets the same inert schema, and no array appears anywhere * inside an array's elements. Like an object of enumerated fields, it is a * bounded payload rather than an instruction: it carries what its elements * carry, up to `maxItems` of them, plus `log₂(maxItems − minItems + 1)` bits * in its length, and an array of bounded numbers is a payload of that many * numbers. * * @throws If the schema admits free text or any other open-ended leaf, or if * the value does not satisfy it. */ const checkInertStance = ( schema: unknown, value: unknown, path: readonly (string | number)[] = [], insideArray = false, ): void => { // A property the value leaves out still has its schema checked, so an // optional open-ended property is refused whether or not a value uses it. const checksValue = value !== UNUSED_PROPERTY; const at = `/${path.join("/")}`; const refuse = (reason: string): never => { throw new Error( `Custody seal requires an instruction-inert stance: ${reason} at \`${at}\``, ); }; if (path.length > STANCE_SCHEMA_MAX_DEPTH) refuse("the schema is too deep"); if (!isObjectNotArray(schema)) refuse("the schema is not an object"); const node = schema as Record; for (const key of Object.keys(node)) { if (!STANCE_SCHEMA_KEYS.has(key)) { refuse(`the schema keyword \`${key}\` is not allowed`); } } if (node.type !== "array") { const misplaced = STANCE_ARRAY_KEYS.find((key) => Object.hasOwn(node, key)); if (misplaced !== undefined) { refuse(`\`${misplaced}\` applies only to an array`); } } const isPrimitive = (entry: unknown) => entry === null || typeof entry === "string" || typeof entry === "boolean" || (typeof entry === "number" && Number.isFinite(entry)); if (Object.hasOwn(node, "const")) { if (!isPrimitive(node.const)) refuse("`const` is not a primitive"); if (checksValue && !deepEqual(value, node.const)) { refuse("the value is not the constant"); } return; } if (Object.hasOwn(node, "enum")) { if ( !Array.isArray(node.enum) || node.enum.length === 0 || !node.enum.every(isPrimitive) ) refuse("`enum` is not a list of primitives"); if ( checksValue && !(node.enum as unknown[]).some((entry) => deepEqual(entry, value)) ) { refuse("the value is not one of the enumerated values"); } return; } switch (node.type) { case "boolean": if (checksValue && typeof value !== "boolean") { refuse("the value is not a boolean"); } return; case "null": if (checksValue && value !== null) refuse("the value is not `null`"); return; case "number": case "integer": { const { minimum, maximum } = node; if ( typeof minimum !== "number" || typeof maximum !== "number" || !Number.isFinite(minimum) || !Number.isFinite(maximum) ) refuse("a number needs a finite `minimum` and `maximum`"); if (!checksValue) return; if ( typeof value !== "number" || !Number.isFinite(value) || value < (minimum as number) || value > (maximum as number) || (node.type === "integer" && !Number.isInteger(value)) ) refuse("the value is outside the number's bounds"); return; } case "object": { const { properties, required } = node; if (!isObjectNotArray(properties)) refuse("an object needs `properties`"); if (node.additionalProperties !== false) { refuse("an object needs `additionalProperties: false`"); } if ( required !== undefined && (!Array.isArray(required) || !required.every((key) => typeof key === "string" && Object.hasOwn(properties as object, key) )) ) refuse("`required` names a key the object does not declare"); if (!checksValue) { for (const [key, entry] of Object.entries(properties as object)) { checkInertStance( entry, UNUSED_PROPERTY, [...path, key], insideArray, ); } return; } if (!isObjectNotArray(value)) refuse("the value is not an object"); const record = value as Record; for (const key of (required as string[] | undefined) ?? []) { if (!Object.hasOwn(record, key)) refuse(`\`${key}\` is missing`); } for (const key of Object.keys(record)) { if (!Object.hasOwn(properties as object, key)) { refuse(`\`${key}\` is not declared`); } } for (const [key, entry] of Object.entries(properties as object)) { checkInertStance( entry, Object.hasOwn(record, key) ? record[key] : UNUSED_PROPERTY, [...path, key], insideArray, ); } return; } case "array": { const { items, minItems = 0, maxItems } = node; if ( typeof maxItems !== "number" || !Number.isInteger(maxItems) || maxItems < 0 || maxItems > MAX_STANCE_ARRAY_ITEMS ) { refuse( `an array needs an integer \`maxItems\` of at most ${MAX_STANCE_ARRAY_ITEMS}`, ); } if ( typeof minItems !== "number" || !Number.isInteger(minItems) || minItems < 0 || minItems > (maxItems as number) ) refuse("`minItems` must be an integer no greater than `maxItems`"); // One level of list: an array anywhere inside an array's elements, // directly or within an object, would multiply the bound by itself. if (insideArray) refuse("an array inside an array's elements"); if (!checksValue) { checkInertStance(items, UNUSED_PROPERTY, [...path, "items"], true); return; } // The element schema is checked on its own first, so an array the value // leaves empty is held to the same schema as a full one. checkInertStance(items, UNUSED_PROPERTY, [...path, "items"], true); if (!Array.isArray(value)) refuse("the value is not an array"); const list = value as unknown[]; if ( list.length < (minItems as number) || list.length > (maxItems as number) ) refuse("the array's length is outside its bounds"); for (let index = 0; index < list.length; index++) { checkInertStance(items, list[index], [...path, index], true); } return; } default: refuse(debugStr`the type $quote${node.type} admits open-ended values`); } }; /** * Checks the terms' shape and the actor's seat, returning the stance schema. * * @throws If the terms name no seats or no stance schema, or if the actor * holds no seat. */ const checkTerms = (terms: JSONValue, actor: string): unknown => { if (!isObjectNotArray(terms)) { throw new Error("Custody terms must be an object"); } const { seats, stanceSchema } = terms as Record; if ( !Array.isArray(seats) || seats.length === 0 || !seats.every(isWellFormedDID) || new Set(seats).size !== seats.length ) { throw new Error( "Custody terms must name `seats` as distinct, well-formed DIDs", ); } if (!(seats as readonly string[]).includes(actor)) { throw new Error( `Custody seal refuses \`${actor}\`: the terms give it no seat`, ); } if (stanceSchema === undefined) { throw new Error("Custody terms must name a `stanceSchema`"); } return stanceSchema; }; /** * Validates the room's policy reference and pins its subject to the room * space. * * @throws If the reference is not an exact module policy reference, or its * subject is not the room space. */ const checkPolicy = ( policy: unknown, room: string, ): CfcModulePolicyRefAtom => { if ( !isObjectNotArray(policy) || !hasExactKeys(policy, MODULE_POLICY_KEYS) || policy.type !== CFC_ATOM_TYPE.Policy || policy.policyRefKind !== "module" || typeof policy.moduleIdentity !== "string" || typeof policy.symbol !== "string" || typeof policy.policyDigest !== "string" ) { throw new Error("Custody seal requires an exact module policy reference"); } if (policy.subject !== room) { throw new Error( debugStr`Custody seal refuses a policy whose subject $quote${policy.subject} is not the room space $quote${room}`, ); } return cfcAtom.modulePolicyRef( policy.moduleIdentity, policy.symbol, policy.policyDigest, room, ); }; /** * The policy as a schema declares it: the runtime substitutes the owning * space for the subject, so a declaration in the room space names the room. */ const declaredPolicy = (policy: CfcModulePolicyRefAtom) => ({ ...policy, subject: { __ctOwningSpace: true }, }); /** * The one reader a custody room's rule releases its projection to: the seal * itself. No member's runtime holds a `Builtin` atom as a reader, so a clause * a rule widens with it is still read by no member. A room's projection is * reactive, and a member's code can point it at input of its own; were the * projection readable, an unchanged answer keeping its earlier witnessed * stamp would tell that code whether its input yields the released answer. * Released only to the seal, the projection is read by * {@link publishCustodyAnswer} alone, which declassifies it once per instance * into the instance's answer slot. */ export const CUSTODY_SEAL_READER = cfcAtom.builtin(CUSTODY_SEAL_WRITER); /** * Whether every exchange rule of `template` releases what it matches to the * seal alone (`addAlternatives: [CUSTODY_SEAL_READER]`), so that nothing its * rules release is readable by a member. A policy with no rules releases * nothing and passes, as it does {@link releaseRequiresSealWitness}. */ export const releasesOnlyToSeal = (template: PolicyTemplateV1): boolean => template.exchangeRules.every((rule) => deepEqual(rule.postCondition.confidentiality, [CUSTODY_SEAL_READER]) ); /** * Whether every exchange rule of `template` guards on the code that computed * what it releases, and every such `TransformedBy` guard names the seal's own * `TransformedBy{builtin cfc-custody-seal}` as its input witness, so that no * rule releases what code computed over anything the seal did not write. A * policy with no rules releases nothing and passes. * * The witness is necessary, not sufficient. A room's release also rests on * writer policies on the box and on the releasing code's output, and on * endorsed releasing code that takes no public selector parameters. Once * those can be checked, a seal into a policy whose rules are not all * witnessed should be refused rather than warned about. */ export const releaseRequiresSealWitness = ( template: PolicyTemplateV1, ): boolean => template.exchangeRules.every((rule) => { const transformers = rule.preCondition.integrity.filter((guard) => isObjectNotArray(guard) && guard.type === CFC_ATOM_TYPE.TransformedBy ); return transformers.length > 0 && transformers.every((guard) => { const record = guard as Record; return namesConcreteCode(record.identity) && deepEqual(record.inputWitness, SEALED_BY); }); }); /** * Whether a `TransformedBy` guard's `identity` names one piece of code * outright. Guards match by subset, so an identity left out, held in a * variable, or missing the fields that pick out the code (a verified * identity's module and symbol, or its code hash; a builtin's id) matches any * code of that kind, and a witness on such a guard vouches for nothing the * rule releases. * `THIS_POLICY.moduleIdentity` names the policy's own module, so it counts. */ const namesConcreteCode = (identity: unknown): boolean => { if (!isObjectNotArray(identity) || containsAtomPatternVariable(identity)) { return false; } switch (identity.kind) { case "verified": return (typeof identity.codeHash === "string" && identity.codeHash.length > 0) || (typeof identity.symbol === "string" && (typeof identity.moduleIdentity === "string" || deepEqual(identity.moduleIdentity, { thisPolicyField: "moduleIdentity", }))); case "builtin": return typeof identity.builtinId === "string"; default: return false; } }; /** * Whether the document at `link` is absent, or its root was written by the * seal. Anyone can address a box, so a box some other code created first is * refused rather than trusted to carry the seal's policy. */ const absentOrSealed = ( tx: IExtendedStorageTransaction, link: NormalizedFullLink, instance: string, ): boolean => { if ( tx.readValueOrThrow({ ...link, path: [] }, { meta: internalVerifierRead, }) === undefined ) return true; return rootWrittenByBuiltin(tx, link, sealIdentity(instance)); }; /** * Whether the anchor at `link` is absent, or holds exactly the value and the * label the seal gives it: one declared root clause and no integrity on any * entry. An anchor some other code created with another clause, or holding a * link whose target the seal's read would follow, would taint every entry or * leave it unattributed; one carrying integrity would hand that integrity, * and any `TransformedBy` witness in it, to every entry, since the anchor is * the entry transaction's one labeled read. */ const absentOrAnchor = ( tx: IExtendedStorageTransaction, link: NormalizedFullLink, clause: CfcConfClause, instance: string, ): boolean => { const stored = tx.readValueOrThrow({ ...link, path: [] }, { meta: internalVerifierRead, }); if (stored === undefined) return true; if (!deepEqual(stored, { instance })) return false; const entries = readStoredCfcMetadata(tx, link)?.labelMap.entries ?? []; if (entries.some((entry) => (entry.label.integrity ?? []).length > 0)) { return false; } const labeled = entries.filter((entry) => (entry.label.confidentiality ?? []).length > 0 ); return labeled.length > 0 && labeled.every((entry) => entry.path.length === 0 && entry.origin === "declared" && entry.label.confidentiality!.length === 1 && clausesEqual(entry.label.confidentiality![0] as CfcConfClause, clause) ); }; /** The clause the anchor carries: the room's policy, or the room's readers. */ const anchorClause = (policy: CfcModulePolicyRefAtom): CfcConfClause => ({ anyOf: [policy, cfcAtom.space(policy.subject as string)], }); /** Whether `prefix` is `path` or one of its ancestors. */ const isPathPrefix = ( prefix: readonly string[], path: readonly string[], ): boolean => prefix.length <= path.length && prefix.every((segment, index) => segment === path[index]); /** Whether `value` holds nothing: absent, or an empty record or list. */ const holdsNothing = (value: unknown): boolean => value === undefined || (Array.isArray(value) && value.length === 0) || (isObjectNotArray(value) && !isPrimitiveCellLink(value) && Object.keys(value).length === 0); /** * Whether the document at `link` is an instance's anchor, of any instance and * any policy its label names. The anchor carries no integrity, by design, so * its label cannot say the seal wrote it; its address can. It holds the * instance it anchors, and sits where the seal derives the anchor from that * instance and the policy its declared clause names. */ const isCustodyDocument = ( runtime: Runtime, tx: IExtendedStorageTransaction, link: NormalizedFullLink, room: string, ): boolean => { const stored = tx.readValueOrThrow({ ...link, path: [] }, { meta: internalVerifierRead, }); const instance = isObjectNotArray(stored) && typeof stored.instance === "string" ? stored.instance : undefined; if (instance === undefined) return false; const named = (readStoredCfcMetadata(tx, { space: link.space, id: link.id, scope: link.scope, })?.labelMap.entries ?? []).flatMap((entry) => (entry.label.confidentiality ?? []).flatMap((clause) => clauseAlternatives(clause as CfcConfClause) ) ); return named.some((atom) => { if ( !isObjectNotArray(atom) || atom.type !== CFC_ATOM_TYPE.Policy || atom.policyRefKind !== "module" || typeof atom.moduleIdentity !== "string" || typeof atom.symbol !== "string" || typeof atom.policyDigest !== "string" ) return false; const policy = cfcAtom.modulePolicyRef( atom.moduleIdentity, atom.symbol, atom.policyDigest, room, ); const anchor = anchorCell(runtime, policy, instance) .getAsNormalizedFullLink(); return anchor.space === link.space && anchor.id === link.id && normalizeCellScope(anchor.scope) === normalizeCellScope(link.scope); }); }; /** * Writes a link to the box into the room's box cell, in the transaction that * writes the entry. The transaction is the seal's, so the write is attributed * to it, and the link is recorded as the one reference the value stored there * holds (`CfcAssertedValueRoot.reference`), so the cell carries the seal's * stamp: a reader that follows it to the box finds it witnessed like the box. */ const linkRoomToBox = ( runtime: Runtime, tx: IExtendedStorageTransaction, binding: Cell, boxLink: NormalizedFullLink, room: string, governed: readonly NormalizedFullLink[], reviewed: NormalizedFullLink, ): void => { const destination = resolveLink( runtime, tx, binding.getAsNormalizedFullLink(), "writeRedirect", ); if (destination.space !== room) { throw new Error( "Custody seal links the box only from a cell in the room space", ); } // The seal writes under its own identity, which the box and the anchor // admit, so the cell it links from is checked to be only that: not in a // document the seal governs, for this instance or another, nor in the terms it reviewed, and holding this // box's link already, or nothing — an empty default, as a room's `Default` // leaves — in a document the seal did not write. A binding that leads // anywhere else would have the seal write there on the member's behalf. const sealWritten = (readStoredCfcMetadata(tx, { space: destination.space, id: destination.id, scope: destination.scope, })?.labelMap.entries ?? []).some((entry) => entry.path.length === 0 && entry.origin === "derived" && (entry.label.integrity ?? []).some(isSealStamp) ); const current = tx.readValueOrThrow(destination, { meta: internalVerifierRead, }); const currentLink = isPrimitiveCellLink(current) && !isWriteRedirectLink(current) ? parseLink(current, { ...destination, path: [] }) : undefined; // A cell the seal links from sits in a room document that exists. An // instance is the digest of its terms, so a member can derive the address // of an anchor, box or answer slot the seal has yet to create; an absent document could // be one, and cannot be told apart from an ordinary one by its content. const documentExists = tx.readValueOrThrow({ ...destination, path: [] }, { meta: internalVerifierRead, }) !== undefined; const holdsThisBox = currentLink !== undefined && currentLink.id === boxLink.id && currentLink.space === boxLink.space && normalizeCellScope(currentLink.scope) === normalizeCellScope(boxLink.scope) && currentLink.path.length === 0; const sameDocument = (link: NormalizedFullLink) => link.space === destination.space && link.id === destination.id && normalizeCellScope(link.scope) === normalizeCellScope(destination.scope); if ( governed.some(sameDocument) || (sameDocument(reviewed) && (isPathPrefix(reviewed.path, destination.path) || isPathPrefix(destination.path, reviewed.path))) || !documentExists || isCustodyDocument(runtime, tx, destination, room) || (!holdsThisBox && (!holdsNothing(current) || sealWritten)) ) { throw new Error( "Custody seal links the box only from a cell that holds nothing else", ); } runtime.getCellFromLink(destination, undefined, tx).set( runtime.getCellFromLink({ ...boxLink, schema: undefined }, undefined, tx), ); tx.recordCfcAssertedValueRoot( { space: destination.space, id: destination.id, scope: destination.scope, path: [...destination.path], }, runtimeWritePolicyAuthorization, { space: boxLink.space, id: boxLink.id, scope: boxLink.scope, path: [], }, ); }; /** The instance's box, in the room space. */ const boxCell = ( runtime: Runtime, policy: CfcModulePolicyRefAtom, instance: string, tx?: IExtendedStorageTransaction, ) => runtime.getCell>( policy.subject as never, { custodyBox: { policy, instance } }, { type: "object", ifc: { confidentiality: [declaredPolicy(policy)], writeAuthorizedBy: [CUSTODY_SEAL_WRITER], }, // Each entry repeats the writer claim, and names no type: a claim // whose schema names a type governs only writes of values of that type. additionalProperties: { ifc: { confidentiality: [declaredPolicy(policy)], writeAuthorizedBy: [CUSTODY_SEAL_WRITER], }, }, } as never, tx, ); /** * The instance's anchor: a seal-written constant each seal reads before it * writes. The read makes the sealing transaction's flow join nonempty, which * is what attributes its writes to the seal (`TransformedBy` is minted only * over a nonempty join), and the anchor's clause admits nothing the box's own * declared policy does not already bound. */ const anchorCell = ( runtime: Runtime, policy: CfcModulePolicyRefAtom, instance: string, tx?: IExtendedStorageTransaction, ) => runtime.getCell( policy.subject as never, { custodyAnchor: { policy, instance } }, { additionalProperties: { ifc: { writeAuthorizedBy: [CUSTODY_SEAL_WRITER] }, }, ifc: { confidentiality: [{ anyOf: [ declaredPolicy(policy), cfcAtom.space(policy.subject as string), ], }], writeAuthorizedBy: [CUSTODY_SEAL_WRITER], }, } as never, tx, ); /** * The actor's blinded entry key for an instance: a digest of the actor's * signature over `(P, D)`. Ed25519 signatures are deterministic, so the key * is stable for the actor across devices, and it is unpredictable to anyone * without the actor's signing key, including other members who know the * actor's DID. * * @throws If the runtime's storage signer is not the actor, or it signs the * same message two ways. */ const blindedEntryKey = async ( runtime: Runtime, actor: string, policy: CfcModulePolicyRefAtom, instance: string, ): Promise => { const signer = runtime.storageManager.as; if (signer.did() !== actor) { throw new Error( `Custody seal requires the storage signer \`${signer.did()}\` to be the acting principal \`${actor}\``, ); } const message = new TextEncoder().encode( ENTRY_KEY_DOMAIN + hashStringOf({ policy, instance }), ); const sign = async () => { const { ok, error } = await signer.sign(message as never); if (error || !ok) { throw new Error("Custody seal could not sign its entry key", { cause: error, }); } return toUnpaddedBase64url(sha256(new Uint8Array(ok))); }; const first = await sign(); if (first !== await sign()) { throw new Error("Custody seal requires a deterministic signer"); } return first; }; /** * Whether the actor's trust closure holds `policy` as a trusted declassifier * through a statement that names `policy`'s exact manifest digest. A * manifest's `moduleIdentity` and `symbol` are fields its author writes, so a * statement that leaves the digest open is satisfied by anyone's manifest * that copies those two fields; such statements are not consulted here. */ const trustsAsDeclassifier = ( config: CfcTrustConfig | undefined, policy: CfcModulePolicyRefAtom, actor: string, ): boolean => { if (config === undefined) return false; const pinning = config.statements.filter((statement) => isObjectNotArray(statement.concrete) && statement.concrete.policyDigest === policy.policyDigest && matchAtomPattern(statement.concrete, policy) !== null ); return createTrustResolver({ ...config, statements: pinning }) .conceptSatisfied(TRUSTED_DECLASSIFIER_CONCEPT, [policy], actor); }; const ROLE_OF = { OWNER: "owner", WRITE: "writer", READ: "reader" } as const; /** * The room's readers, ordered by principal: every principal its access list * names. The room space's own key reads only what that list grants it. * * @throws If the room space has no access list, one with no concrete owner, * or one naming a principal that is neither `*` nor a well-formed DID. * Without one, who can read the room cannot be named, and the actor would * consent to an audience nobody showed them. */ const roomReaders = (acl: unknown): CustodyRoomReader[] => { if (!isACL(acl) || !hasConcreteOwner(acl)) { throw new Error( "Custody seal requires a room space whose access list names its readers", ); } const listed = acl as Record; if ( !Object.keys(listed).every((principal) => principal === ANYONE_USER || isWellFormedDID(principal) ) ) { throw new Error( "Custody seal requires a room space whose access list names only well-formed DIDs or `*`", ); } return Object.entries(listed) .map(([principal, capability]) => ({ principal, role: ROLE_OF[capability], })) .sort((a, b) => utf8Compare(a.principal, b.principal)); }; /** * Reads the actor's allowed sources for custody rooms from a settings * document in the actor's home space: a list of the actor's own `Context` and * `Resource` atoms. This is how a seal reads the settings cell a host passes * as {@link CustodySealOptions.allowedSources}; a host calls it only to show * the allowance outside a seal. * * The document is read only from the actor's home space, so a room, or anyone * else who can write a space the actor reads, cannot widen what the actor * allows. Code running as the actor can write the actor's home space; what * holds against that code is the confirmation, which shows the sources the * value draws on. * * @throws If there is no authenticated actor, the document is not in the * actor's home space, or it holds anything other than a list of the actor's * own `Context` and `Resource` atoms. */ export async function readCustodySourcePolicy( settings: Cell, ): Promise { await settings.sync(); const tx = cellRuntime(settings).edit(); try { return sourcePolicyIn(settings, tx); } finally { tx.abort(); } } /** * Reads the actor's allowed sources from `settings` in `tx`, as * {@link readCustodySourcePolicy} describes. */ const sourcePolicyIn = ( settings: Cell, tx: IExtendedStorageTransaction, ): CfcAtom[] => { const actor = tx.getCfcState().trustSnapshot?.actingPrincipal; if (!isDID(actor)) { throw new Error("Custody seal requires an authenticated actor"); } const link = settings.withTx(tx).resolveAsCell().getAsNormalizedFullLink(); if (link.space !== actor) { throw new Error( "Custody seal reads the allowed sources only from the actor's home space", ); } const value = snapshotJsonValue(settings.withTx(tx).get()); if ( !Array.isArray(value) || !value.every((atom) => isObjectNotArray(atom) && (atom.type === CFC_ATOM_TYPE.Context || atom.type === CFC_ATOM_TYPE.Resource) && isActorOwnedAlternative(atom, actor) ) ) { throw new Error( "Custody seal requires the allowed sources to be the actor's own `Context` and `Resource` atoms", ); } return value as unknown as CfcAtom[]; }; /** * Reads the terms at `terms`, whose transaction is the caller's: every field * as a JSON value, except `seats`, whose entries may each be a DID or a * reference to a cell attesting a principal. Each reference is * returned by its position, for {@link resolveSeats}; the seat is `null` * until then. * * @throws If the terms are not an object, or a field other than `seats` * holds a value that is not JSON. */ const readTerms = (terms: Cell): { terms: Record; seatLinks: Map; } => { const raw = terms.getRawUntyped({ frozen: false }); if (!isObjectNotArray(raw)) { throw new Error("Custody terms must be an object"); } const read: Record = {}; const seatLinks = new Map(); for (const key of Object.keys(raw)) { // A field is read through the references it holds: the runtime may store // a nested list or object as a document of its own. if (key !== "seats") { read[key] = snapshotJsonValue(terms.key(key as never).get()); continue; } const seatsCell = terms.key("seats" as never).resolveAsCell(); const seats = seatsCell.getRawUntyped({ frozen: false }); if (!Array.isArray(seats)) { read.seats = snapshotJsonValue(seats); continue; } const base = seatsCell.getAsNormalizedFullLink(); read.seats = seats.map((seat, index) => { if (typeof seat === "string") return seat; const link = parseLink(seat, base); if (link === undefined) return snapshotJsonValue(seat); seatLinks.set(index, link); return null; }); } return { terms: read, seatLinks }; }; /** * The first of `clauses` that neither the room space nor the room's policy * satisfies, or `undefined`: what the terms carry is copied into every entry * and shown to every reader of the room, so it may carry only what they hold. */ const clauseWithheldFromRoom = ( clauses: readonly CfcConfClause[], room: string, policy: CfcModulePolicyRefAtom, ): CfcConfClause | undefined => clauses.find((clause) => !clauseAlternatives(clause).some((atom) => deepEqual(atom, cfcAtom.space(room)) || deepEqual(atom, policy) ) ); /** * Returns `terms` with each seat named by a reference replaced by the one * principal its cell attests, in exactly the form a runtime mints (see * `mintedProfilePrincipals()`), with a well-formed DID for its subject. A * pattern holds a member's profile, not the member's DID, so a seat can name * what the member's own runtime attested. The reads are added to `evidence`, * so the transaction that writes the entry verifies them. * * @throws If a referenced cell attests no principal or more than one, or * carries an attestation in any form but the one a runtime mints. */ const resolveSeats = async ( runtime: Runtime, terms: Record, seatLinks: ReadonlyMap, { room, policy }: { room: string; policy: CfcModulePolicyRefAtom }, evidence: ReadEvidence[], ): Promise => { if (seatLinks.size === 0) return terms; const seats = [...(terms.seats as JSONValue[])]; for (const [index, link] of seatLinks) { const seat = runtime.getCellFromLink(link); await seat.sync(); await seat.resolveAsCell().sync(); // Whether the seat's owner wrote a name decides which of its fields // attest, so the name's document is loaded too. await seat.resolveAsCell().key(PROFILE_NAME_FIELD).resolveAsCell().sync(); const tx = runtime.edit(); try { const target = seat.withTx(tx).resolveAsCell().getAsNormalizedFullLink(); const view = cfcLabelViewFromMetadata( readStoredCfcMetadata(tx, target), target.path.map(String), ); // The seal writes the DID into terms every reader of the room sees, so // the cell must already be one they hold, whether or not its label // traveled into the terms with the reference. // The attestation is read from the entries the profile rule counts, and // checked on the root and top-level fields besides, so those are the // entries the room's readers must hold, whatever their observation // class. The attestation's subject is itself public // metadata (a `represents-principal` subject is classified public for // invariant 12), so no label-metadata clause governs the DID. const nameWritten = profileNameWrittenAt(runtime, tx, target); const attesting = new Set([ ...principalClaimEntries(view), ...representsPrincipalEntries(view, nameWritten), ]); const withheld = clauseWithheldFromRoom( [...attesting] .flatMap((entry) => (entry.label.confidentiality ?? []) as CfcConfClause[] ), room, policy, ); if (withheld !== undefined) { throw new Error( debugStr`Custody terms name seat ${index} by a cell with a clause the room's readers do not hold: $quote,long${withheld}`, ); } const principals = mintedProfilePrincipals(view, nameWritten); if ( principals === undefined || !principals.every(isWellFormedDID) ) { throw new Error( `Custody terms name seat ${index} by a cell with an attestation that is not in the form a runtime mints`, ); } if (principals.length === 0) { throw new Error( `Custody terms name seat ${index} by a cell that attests no principal`, ); } if (principals.length > 1) { throw new Error( `Custody terms name seat ${index} by a cell that attests more than one principal`, ); } seats[index] = principals[0]; for (const read of readEvidence(tx, "Custody seal")) evidence.push(read); } finally { tx.abort(); } } return { ...terms, seats }; }; const STALE_REVIEW = "Custody seal review is stale; review the value again"; /** * Returns the allowed sources `options` names: a copy of its list, or what its * settings cell holds. A cell's read is added to `evidence`, so the entry's * transaction verifies it. */ const allowedSourcesOf = async ( runtime: Runtime, options: CustodySealOptions, evidence: ReadEvidence[], ): Promise => { const allowed = options?.allowedSources; if (Array.isArray(allowed)) return structuredClone(allowed); if (!isCell(allowed)) { throw new Error("Custody seal requires the room's allowed sources"); } if (cellRuntime(allowed) !== runtime) { throw new Error("Custody seal handles must belong to the same runtime"); } await allowed.sync(); const tx = runtime.edit(); try { const sources = sourcePolicyIn(allowed, tx); for (const read of readEvidence(tx, "Custody seal")) evidence.push(read); return sources; } finally { tx.abort(); } }; /** Whether `value` has the shape of a module policy reference at all. */ const isModulePolicyShaped = (value: unknown): boolean => isObjectNotArray(value) && value.type === CFC_ATOM_TYPE.Policy && value.policyRefKind === "module"; /** * The one module policy reference the stored label of the cell at `link` * declares on the cell itself, or `undefined` when it declares none. * * @throws If the label names more than one. */ const declaredPolicyOf = ( tx: IExtendedStorageTransaction, link: NormalizedFullLink, ): unknown => { const view = cfcLabelViewFromMetadata( readStoredCfcMetadata(tx, link), link.path.map(String), ); const found: unknown[] = []; for (const entry of view?.entries ?? []) { if (entry.path.length > 0) continue; if (entry.observes !== undefined && entry.observes !== "value") continue; for (const clause of entry.label.confidentiality ?? []) { for (const atom of clauseAlternatives(clause as CfcConfClause)) { if ( isModulePolicyShaped(atom) && !found.some((known) => deepEqual(known, atom)) ) found.push(atom); } } } if (found.length > 1) { throw new Error( "Custody seal refuses a policy cell whose label declares more than one module policy", ); } return found[0]; }; /** * Returns the policy reference `policy` names: the reference itself, what its * cell holds, or, for a cell holding no reference, the one module policy the * cell's stored label declares. The label is how a pattern names its own * room's policy: it cannot write the reference, which names its module's * content identity and manifest digest, but a cell it declares `PolicyOf` its * rules carries that reference, with the room space the runtime bound as its * subject. A cell's reads are added to `evidence`, so the entry's transaction * verifies them. The caller checks the reference. * * @throws If a cell's label declares more than one module policy. */ const requestedPolicyOf = async ( runtime: Runtime, policy: CustodyRoom["policy"], evidence: ReadEvidence[], ): Promise => { if (!isCell(policy)) return policy; if (cellRuntime(policy) !== runtime) { throw new Error("Custody seal handles must belong to the same runtime"); } await syncResolved(policy); const tx = runtime.edit(); try { const cell = policy.withTx(tx).resolveAsCell(); const value = cell.getRawUntyped({ frozen: false }); const requested = isModulePolicyShaped(value) ? snapshotJsonValue(value) : declaredPolicyOf(tx, cell.getAsNormalizedFullLink()); for (const read of readEvidence(tx, "Custody seal")) evidence.push(read); return requested; } finally { tx.abort(); } }; /** A room as the seal reads it: its policy, its terms, and their instance. */ interface RoomInspection { readonly policy: CfcModulePolicyRefAtom; readonly room: string; readonly termsLink: NormalizedFullLink; readonly terms: JSONValue; readonly template: PolicyTemplateV1; readonly witnessedRelease: boolean; } /** * Reads and checks a room: the policy its policy cell declares, the terms * with each seat resolved to its DID, and the policy's installed manifest. * Each read that established a check is added to `evidence`. */ const inspectRoom = async ( runtime: Runtime, requestedRoom: CustodyRoom, evidence: ReadEvidence[], reviewedPolicy?: CfcModulePolicyRefAtom, ): Promise => { const requestedPolicy = await requestedPolicyOf( runtime, requestedRoom.policy, evidence, ); if (reviewedPolicy && !deepEqual(requestedPolicy, reviewedPolicy)) { throw new Error(STALE_REVIEW); } const termsTx = runtime.edit(); let termsLink: NormalizedFullLink; let rawTerms: Record; let seatLinks: ReadonlyMap; let policy: CfcModulePolicyRefAtom; let room: string; try { termsLink = requestedRoom.terms.withTx(termsTx).resolveAsCell() .getAsNormalizedFullLink(); room = termsLink.space; if (!isDID(room)) { throw new Error("Custody terms must live in a space named by a DID"); } policy = checkPolicy(requestedPolicy, room); if (requestedRoom.box !== undefined) { if (cellRuntime(requestedRoom.box) !== runtime) { throw new Error("Custody seal handles must belong to the same runtime"); } if (requestedRoom.box.getAsNormalizedFullLink().space !== room) { throw new Error( "Custody seal links the box only from a cell in the room space", ); } } ({ terms: rawTerms, seatLinks } = readTerms( runtime.getCellFromLink(termsLink, undefined, termsTx), )); // Terms are copied into every entry, so they may carry only what the // room's readers already hold: a clause admitting the room space, or the // room's own policy. const withheld = clauseWithheldFromRoom( collectConsumedLabel(termsTx).confidentiality, room, policy, ); if (withheld !== undefined) { throw new Error( debugStr`Custody terms carry a clause the room's readers do not hold: $quote,long${withheld}`, ); } for (const read of readEvidence(termsTx, "Custody seal")) { evidence.push(read); } } finally { termsTx.abort(); } const terms = await resolveSeats( runtime, rawTerms, seatLinks, { room, policy }, evidence, ); // Read through a schema that admits the whole manifest, so the sync loads // the documents its rules are stored in as well as the manifest's own. const manifest = runtime.getCellFromEntityId( room, cfcPolicyManifestDocId(policy.policyDigest), [], { type: "object", additionalProperties: true }, ); await manifest.sync(); const manifestTx = runtime.edit(); let witnessedRelease: boolean; let template: PolicyTemplateV1; try { const artifact = runtime.resolveCfcPolicyManifest( policy, manifestTx, room, false, ); if (!artifact) { throw new Error( debugStr`Custody seal refuses a policy not installed in the room space: $quote${policy.policyDigest}`, ); } template = artifact.manifest.template; witnessedRelease = releaseRequiresSealWitness(template); } finally { manifestTx.abort(); } return { policy, room, termsLink, terms, template, witnessedRelease, }; }; /** * Reads the draft, the terms, and the room's state, and checks them all. At * commit, `reviewed` is what the prepare established, and a stored input that * no longer holds it is refused as stale before anything is checked against * it. */ const inspect = async ( draft: Cell, requestedRoom: CustodyRoom, options: CustodySealOptions, reviewed?: Inspection, ): Promise => { const runtime = cellRuntime(draft); if (cellRuntime(requestedRoom.terms) !== runtime) { throw new Error("Custody seal handles must belong to the same runtime"); } await Promise.all([ syncResolved(draft), syncResolved(requestedRoom.terms), ]); const evidence: ReadEvidence[] = []; const allowed = await allowedSourcesOf(runtime, options, evidence); if (reviewed && !deepEqual(allowed, reviewed.allowedSources)) { throw new Error(STALE_REVIEW); } const draftTx = runtime.edit(); let actor: string; let draftLink: NormalizedFullLink; let stance: JSONValue; let sources: CfcAtom[]; let heldWith: DID[][]; try { const acting = draftTx.getCfcState().trustSnapshot?.actingPrincipal; if (!isDID(acting)) { throw new Error("Custody seal requires an authenticated actor"); } actor = acting; draftLink = draft.withTx(draftTx).resolveAsCell().getAsNormalizedFullLink(); stance = snapshotJsonValue(draft.withTx(draftTx).get()); ({ sources, heldWith } = actorLabel( collectConsumedLabel(draftTx).confidentiality, actor, )); for (const read of readEvidence(draftTx, "Custody seal")) { evidence.push(read); } } finally { draftTx.abort(); } const refused = sources.find((source) => !allowed.some((entry) => deepEqual(entry, source)) ); if (refused !== undefined) { throw new Error( debugStr`Custody seal refuses a source this room does not allow: $quote,long${refused}`, ); } const inspectedRoom = await inspectRoom( runtime, requestedRoom, evidence, reviewed?.policy, ); const { policy, room, termsLink, terms } = inspectedRoom; // A bound on what an answer reveals needs the release witnessed and made // to the seal alone: a projection the room's readers can read says, by // keeping or losing its stamp, whether input of a member's choosing yields // the released answer. const witnessedRelease = inspectedRoom.witnessedRelease && releasesOnlyToSeal(inspectedRoom.template); const acl = runtime.getCellFromLink({ space: room, id: aclDocId(room), path: [], } as never); await acl.sync(); const aclTx = runtime.edit(); let readers: CustodyRoomReader[]; try { readers = roomReaders( aclTx.readValueOrThrow({ ...acl.getAsNormalizedFullLink(), path: [] }, { meta: internalVerifierRead, }), ); for (const read of readEvidence(aclTx, "Custody seal")) evidence.push(read); } finally { aclTx.abort(); } checkInertStance(checkTerms(terms, actor), stance); if (!trustsAsDeclassifier(runtime.cfcTrustConfig, policy, actor)) { throw new Error( debugStr`Custody seal refuses a policy the actor does not trust as a declassifier: $quote,long${policy}`, ); } const instance = hashStringOf(terms); const entryKey = await blindedEntryKey(runtime, actor, policy, instance); const box = boxCell(runtime, policy, instance); await box.sync(); const boxTx = runtime.edit(); try { const entry = boxTx.readValueOrThrow({ ...box.getAsNormalizedFullLink(), path: [entryKey], }, { meta: internalVerifierRead }); if (entry !== undefined) { throw new Error("Custody seal refuses a second entry for this actor"); } if (!absentOrSealed(boxTx, box.getAsNormalizedFullLink(), instance)) { throw new Error("Custody seal refuses a box the seal did not create"); } } finally { boxTx.abort(); } return { actor, allowedSources: allowed, room: policy.subject as string, readers, draftLink, termsLink, stance, terms, instance, policy, sources, heldWith, entryKey, witnessedRelease, evidence, }; }; /** * Prepares a seal of `draft` into `room` for the host's confirmation dialog. * The preview is exactly what the commit writes; the consent it returns is * good for one commit. * * @throws If anyone but the actor can read the value, it is not instruction * inert, draws on a source the room does not allow, or the room's policy is * malformed, untrusted, not the room space's, or not installed there, or the * actor holds no seat or has already sealed. */ export async function prepareCustodySeal( draft: Cell, room: CustodyRoom, options: CustodySealOptions, ): Promise { // The preview and the retained consent share these values, so they are // frozen: a caller that edits what it was shown cannot change what the // commit compares against. const inspected = await inspect(draft, room, options); deepFreeze(inspected.stance); deepFreeze(inspected.terms); deepFreeze(inspected.policy); deepFreeze(inspected.sources); deepFreeze(inspected.heldWith); deepFreeze(inspected.allowedSources); deepFreeze(inspected.readers); const consent = Object.freeze({}) as CustodySealConsent; consents.set(consent, { ...inspected, draft: draft.withTx(undefined), requestedRoom: room, options: Object.freeze({ allowedSources: isCell(options.allowedSources) ? options.allowedSources.withTx(undefined) : structuredClone(options.allowedSources), }), eventId: crypto.randomUUID(), }); return Object.freeze({ actor: inspected.actor, room: inspected.room, readers: inspected.readers, stance: inspected.stance, terms: inspected.terms, instance: inspected.instance, policy: inspected.policy, sources: inspected.sources, heldWith: inspected.heldWith, witnessedRelease: inspected.witnessedRelease, consent, }); } /** * Writes the reviewed entry after a host-trusted seal gesture: the * instance's anchor if it is absent, then the actor-private receipt in the * actor's home space, then the entry in the box. A transaction writes one * space, so each is a separate commit, and the receipt is written before the * entry so that no entry exists without one; a receipt whose entry is absent * records a seal whose commit failed, or an entry lost afterwards. * * @throws If the consent is unknown or spent, the gesture is not the host's, * anything reviewed changed, the anchor is withheld from this runtime, the * actor's entry exists, or `options.signal` aborted before the entry's * transaction was sent. */ export async function commitCustodySeal( consent: CustodySealConsent, event: unknown, options: CustodySealCommitOptions = {}, ): Promise { const { signal } = options; const state = consents.get(consent); if (!state) { throw new Error("Custody seal consent is unknown or already consumed"); } consents.delete(consent); if (!isTrustedGestureOn(event, CUSTODY_SEAL_GESTURE)) { throw new Error("Custody seal requires a trusted host seal gesture"); } signal?.throwIfAborted(); const current = await inspect( state.draft, state.requestedRoom, state.options, state, ); if ( current.actor !== state.actor || !deepEqual(current.readers, state.readers) || !deepEqual(current.stance, state.stance) || !deepEqual(current.terms, state.terms) || current.instance !== state.instance || !deepEqual(current.policy, state.policy) || !deepEqual(current.sources, state.sources) || !deepEqual(current.heldWith, state.heldWith) || !deepEqual(current.draftLink, state.draftLink) || !deepEqual(current.termsLink, state.termsLink) || current.entryKey !== state.entryKey ) { throw new Error(STALE_REVIEW); } const runtime = cellRuntime(state.draft); const { actor, policy, instance, entryKey, room } = state; signal?.throwIfAborted(); // The anchor comes first, so a seal that cannot establish it has written // nothing durable. It is written only where absent, and its value is a // constant the entry transaction verifies, so two first seals racing to // create it need no create-only mark: the loser's retry finds the winner's. const anchor = anchorCell(runtime, policy, instance); await anchor.sync(); const anchorLink = anchor.getAsNormalizedFullLink(); const anchored = await runtime.editWithRetry( (tx) => { if ( tx.readValueOrThrow(anchorLink, { meta: internalVerifierRead }) !== undefined ) { // Checked here as well as in the entry transaction, so a squatted // anchor is refused before the receipt makes anything durable. if (!absentOrAnchor(tx, anchorLink, anchorClause(policy), instance)) { throw new Error( "Custody seal refuses an anchor the seal did not create", ); } return; } setCfcImplementationIdentity(tx, sealIdentity(instance)); anchor.withTx(tx).set({ instance }); }, undefined, { signal }, ); if (anchored.error) { const reason = "reason" in anchored.error ? anchored.error.reason : undefined; if (reason instanceof Error) throw reason; throw new Error( `Custody seal could not create its anchor: ${anchored.error.message}`, ); } signal?.throwIfAborted(); const receiptTx = runtime.edit(); let receipt: Cell; try { setCfcImplementationIdentity(receiptTx, sealIdentity(instance)); receipt = runtime.getCell(actor as never, { custodySealReceipt: state.eventId, }, { type: "object", additionalProperties: { ifc: { writeAuthorizedBy: [CUSTODY_SEAL_WRITER] }, }, ifc: { confidentiality: [cfcAtom.user(actor)], writeAuthorizedBy: [CUSTODY_SEAL_WRITER], }, }, receiptTx); receipt.set({ eventId: state.eventId, policy, instance, entryKey, sources: state.sources, heldWith: state.heldWith, stanceDigest: hashStringOf(state.stance), draftId: state.draftLink.id, }); receiptTx.markCreateOnly?.(receipt.getAsNormalizedFullLink()); signal?.throwIfAborted(); const result = await receiptTx.commit().settled; if (result.error) { throw new Error(`Custody seal receipt failed: ${result.error.message}`); } } catch (error) { receiptTx.abort(); throw error; } // The room document the box link is written into, as the entry // transaction checks it. if (state.requestedRoom.box !== undefined) { await syncResolved(state.requestedRoom.box); } // Every seal of an instance writes the one box document, so seals by // different actors conflict; each retry re-runs every check against the // state that won, including whether this actor's entry now exists. The // signal is checked here and, by `editWithRetry`, at every step until the // transaction is sent. let box: Cell> | undefined; const sealed = await runtime.editWithRetry( (tx) => { signal?.throwIfAborted(); if (tx.getCfcState().trustSnapshot?.actingPrincipal !== actor) { throw new Error("Custody seal actor changed after review"); } if (!evidenceHolds(tx, current.evidence)) { throw new Error("Custody seal review changed before commit"); } setCfcImplementationIdentity(tx, sealIdentity(instance)); if (!absentOrAnchor(tx, anchorLink, anchorClause(policy), instance)) { throw new Error( "Custody seal refuses an anchor the seal did not create", ); } box = boxCell(runtime, policy, instance, tx); const boxLink = box.getAsNormalizedFullLink(); if (!absentOrSealed(tx, boxLink, instance)) { throw new Error("Custody seal refuses a box the seal did not create"); } // The one labeled read in this transaction: it attributes the entry's // writes to the seal. Every other read is a verifier read, so neither the // draft's clauses nor anyone else's reach the entry. try { anchor.withTx(tx).get(); } catch (error) { if (error instanceof CfcReadCeilingError) { throw new Error( "Custody seal requires a runtime whose read ceiling admits the room's custody", { cause: error }, ); } throw error; } const entryLink = { ...boxLink, path: [entryKey] }; if (tx.readValueOrThrow(entryLink, { meta: internalVerifierRead })) { throw new Error("Custody seal refuses a second entry for this actor"); } box.key(entryKey).set({ instance, terms: canonicalJson(state.terms), stance: state.stance, }); if (state.requestedRoom.box !== undefined) { linkRoomToBox( runtime, tx, state.requestedRoom.box, boxLink, room, [ boxLink, anchorLink, answerCell(runtime, policy, instance).getAsNormalizedFullLink(), ], state.termsLink, ); } }, undefined, { signal }, ); if (sealed.error) { // A check that threw is the refusal to report; anything else is the // commit's own failure. const reason = "reason" in sealed.error ? sealed.error.reason : undefined; if (reason instanceof Error) throw reason; throw new Error(`Custody seal failed: ${sealed.error.message}`); } return { box: box!.withTx(undefined) as Cell, entryKey, instance, receipt: receipt.withTx(undefined), }; } /** What {@link publishCustodyAnswer} published. */ export interface CustodyAnswerResult { /** The instance's answer slot, in the room space. */ readonly answer: Cell; /** The instance the answer was published for: the digest of the terms. */ readonly instance: string; /** The answer as published. */ readonly value: JSONValue; } /** The longest answer, in characters, the seal publishes. */ const MAX_ANSWER_LENGTH = 1024; /** The instance's answer slot, in the room space. */ const answerCell = ( runtime: Runtime, policy: CfcModulePolicyRefAtom, instance: string, tx?: IExtendedStorageTransaction, ) => runtime.getCell>( policy.subject as never, { custodyAnswer: { policy, instance } }, { type: "object", ifc: { confidentiality: [cfcAtom.space(policy.subject as string)], writeAuthorizedBy: [CUSTODY_SEAL_WRITER], }, } as never, tx, ); /** Whether a clause names `policy` among its alternatives. */ const namesPolicy = (clause: unknown, policy: CfcModulePolicyRefAtom) => clauseAlternatives(clause as CfcConfClause).some((atom) => isObjectNotArray(atom) && atom.type === CFC_ATOM_TYPE.Policy && atom.policyDigest === policy.policyDigest && atom.moduleIdentity === policy.moduleIdentity && atom.symbol === policy.symbol && atom.subject === policy.subject ); /** * The value `output` holds and whether the room's policy releases it to the * seal by one of its own rules, for the seal to publish to the room's * readers. The answer is a scalar, as a room's projected answer is, and its * label is what a display of it resolves: the content entries at its root, * joined. It is released when that label carries the room's policy, a rule of * that policy fires, every clause naming the policy is left admitting the * seal ({@link CUSTODY_SEAL_READER}), and every other clause admits the room * space's readers. The room's rules all require the seal's witness and * release only to the seal (checked by the caller), so a release here is one * whose computation read only what the seal wrote, and one no member could * read before the seal published it. A value that never carried the policy is * not the room's answer, whatever its label admits. */ const releasedToSeal = ( runtime: Runtime, tx: IExtendedStorageTransaction, output: Cell, policy: CfcModulePolicyRefAtom, room: string, instance: string, ): { value: JSONValue; released: boolean } => { const resolved = output.withTx(tx).resolveAsCell().getAsNormalizedFullLink(); const stored = tx.readValueOrThrow(resolved, { meta: internalVerifierRead }); // A projector that has not run yet has computed nothing: not an answer yet, // refused like one the policy does not release. if (stored === undefined) return { value: null, released: false }; const value = snapshotJsonValue(stored); if ( value === null || typeof value === "object" || (typeof value === "string" && value.length > MAX_ANSWER_LENGTH) ) { return { value, released: false }; } const { view, readFailed } = cfcLabelViewForResolvedCellWithStatus( output.withTx(tx), ); // The value's own entries: a link's entry, which labels a pointer crossed // on the way to the value, is a `followRef` entry in the view. const entries = (view?.entries ?? []).filter((entry) => entry.path.length === 0 && (entry.observes === undefined || entry.observes === "value") ); const confidentiality = entries.flatMap((entry) => entry.label.confidentiality ?? [] ); if ( readFailed || !confidentiality.some((clause) => namesPolicy(clause, policy)) ) { return { value, released: false }; } // The seal witnessed this instance's writes and no other's: every witness // naming the seal names this instance, and one does. const integrity = entries.flatMap((entry) => entry.label.integrity ?? []); const sealWitnesses = integrity.flatMap((atom) => isObjectNotArray(atom) && atom.type === CFC_ATOM_TYPE.TransformedBy && isSealStamp(atom.inputWitness) ? [atom.inputWitness] : [] ); if ( sealWitnesses.length === 0 || !sealWitnesses.every((witness) => deepEqual(witness, sealedFor(instance))) ) { return { value, released: false }; } const result = evaluateExchangeRules( { confidentiality, integrity }, undefined, { modulePolicyResolver: (reference) => runtime.resolveCfcPolicyManifest(reference, tx, room as never, false), }, ); const left = result.label.confidentiality ?? []; const roomReaders = cfcAtom.space(room); // A rule of the room's own policy fired, and released every clause naming // the policy to the seal; every other clause admits the room's readers. const roomRecord = modulePolicyRecordId(policy); const admits = (clause: unknown, reader: unknown) => clauseAlternatives(clause as CfcConfClause).some((atom) => deepEqual(atom, reader) ); return { value, released: !result.exhausted && result.resolutionFailures.length === 0 && result.firings.some((firing) => firing.recordId === roomRecord && firing.kind === "add" ) && left.every((clause) => namesPolicy(clause, policy) ? admits(clause, CUSTODY_SEAL_READER) : admits(clause, roomReaders) ), }; }; /** * The answer an instance's slot holds, when the seal wrote it: `undefined` * while nothing is published, and a refusal for a slot other code wrote. */ const sealedAnswer = ( tx: IExtendedStorageTransaction, link: NormalizedFullLink, instance: string, ): { value: JSONValue } | undefined => { const stored = tx.readValueOrThrow({ ...link, path: [] }, { meta: internalVerifierRead, }); if (stored === undefined) return undefined; if ( !absentOrSealed(tx, link, instance) || !isObjectNotArray(stored) || !hasExactKeys(stored as Record, ["answer", "instance"]) || (stored as { instance?: unknown }).instance !== instance ) { throw new Error("Custody answer refuses a slot the seal did not write"); } return { value: snapshotJsonValue((stored as { answer?: unknown }).answer) }; }; /** * Reads a custody instance's published answer: the value its answer slot * holds, verified to be the seal's own write, or `undefined` while nothing is * published. A host renders this, never a link a room holds, which the room's * members can write. The slot is that of the instance `room.terms` digest to, * under the policy `room.policy` names. Its content is create-only and the * seal's own, so the answer published for an instance never changes; which * instance this reads moves with `room.terms` and `room.policy`, which a room * member's own code can repoint at another instance's terms or at terms whose * slot is empty. A writer claim on the room's cells does not close that in * general, as write authority is keyed by code (normative CFC §8.15.8). * * @throws If the slot holds something the seal did not write. */ export async function readCustodyAnswer( room: CustodyRoom, ): Promise { const runtime = cellRuntime(room.terms); await syncResolved(room.terms); const { policy, terms } = await inspectRoom(runtime, room, []); const instance = hashStringOf(terms); const slot = answerCell(runtime, policy, instance); await slot.sync(); const tx = runtime.edit(); try { return sealedAnswer(tx, slot.getAsNormalizedFullLink(), instance)?.value; } finally { tx.abort(); } } /** * Publishes a custody instance's answer once: the value `output` holds, when * a rule of the room's policy, every one of which requires the seal's * witness and releases only to the seal, releases it to the seal, and every * seat has sealed. The seal declassifies it into the instance's answer slot, * a create-only document in the room space at * `{custodyAnswer: {policy, instance}}` labeled for the room's readers and * stamped as the seal's own like the box, and refuses a second publication. * The projection itself is read by no member. A host renders the slot through * {@link readCustodyAnswer}, so the answer published for an instance never * changes, whatever later points the projector at other input. Which instance * a room shows moves with `room.terms` and `room.policy`, as * {@link readCustodyAnswer} says. * * @throws If the room's policy releases anything without the seal's witness * or to anyone but the seal, a seat has not sealed, the policy does not * release `output` to the seal, the room changed while the answer was being * published, or the instance's answer is already published. */ export async function publishCustodyAnswer( room: CustodyRoom, output: Cell, ): Promise { const runtime = cellRuntime(output); if (cellRuntime(room.terms) !== runtime) { throw new Error("Custody seal handles must belong to the same runtime"); } await Promise.all([syncResolved(room.terms), syncResolved(output)]); const evidence: ReadEvidence[] = []; const inspected = await inspectRoom(runtime, room, evidence); if (!inspected.witnessedRelease) { throw new Error( "Custody answer requires a policy whose every rule requires the seal's witness", ); } if (!releasesOnlyToSeal(inspected.template)) { throw new Error( "Custody answer requires a policy whose every rule releases only to the seal", ); } const { policy, terms } = inspected; const space = inspected.room; const instance = hashStringOf(terms); const seats = (terms as { seats: readonly unknown[] }).seats.length; const box = boxCell(runtime, policy, instance); const anchor = anchorCell(runtime, policy, instance); const slot = answerCell(runtime, policy, instance); await Promise.all([box.sync(), anchor.sync(), slot.sync()]); let published: { value: JSONValue } | undefined; const written = await runtime.editWithRetry((tx) => { // The room read at inspection is the room published for. if (!evidenceHolds(tx, evidence)) { throw new Error("Custody answer's room changed while publishing"); } setCfcImplementationIdentity(tx, sealIdentity(instance)); const answer = answerCell(runtime, policy, instance, tx); const answerLink = answer.getAsNormalizedFullLink(); if (sealedAnswer(tx, answerLink, instance) !== undefined) { throw new Error("Custody answer is already published for this instance"); } const anchorLink = anchor.getAsNormalizedFullLink(); const boxLink = box.getAsNormalizedFullLink(); const entries = tx.readValueOrThrow({ ...boxLink, path: [] }, { meta: internalVerifierRead, }); if ( !absentOrAnchor(tx, anchorLink, anchorClause(policy), instance) || !absentOrSealed(tx, boxLink, instance) || !isObjectNotArray(entries) || Object.keys(entries).length !== seats ) { throw new Error("Custody answer requires every seat to have sealed"); } const { value, released } = releasedToSeal( runtime, tx, output, policy, space, instance, ); if (!released) { throw new Error( "Custody answer requires a value the room's policy releases to the seal", ); } // The one labeled read, as in the entry transaction: it attributes the // slot to the seal, which is what a reader verifies. anchor.withTx(tx).get(); answer.set({ instance, answer: value }); tx.markCreateOnly?.(answerLink); published = { value }; }); if (written.error) { const reason = "reason" in written.error ? written.error.reason : undefined; if (reason instanceof Error) throw reason; throw new Error(`Custody answer failed: ${written.error.message}`); } return { answer: slot, instance, value: published!.value }; }