> ## Documentation Index
> Fetch the complete documentation index at: https://docs.parmanasystems.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Content Binding & TOCTOU

> The real mechanism that closes the check-vs-use gap between authorization and execution, what it covers, and what it doesn't.

<Info>
  **\[AVAILABLE], and wired into the default server as of commit `651497a` (2026-07-11).**
  Earlier revisions of this page said the opposite, see the correction below.
</Info>

<Warning>
  **This page previously said the default local server does NOT enforce content binding.**
  That was true through commit `4740aee` (2026-07-08). As of `651497a`,
  `packages/api/src/bootstrap/createExecutionSystem.ts` unconditionally builds a real
  `ExecutionGateway`, so every `POST /execute` against the stock server does get the hash
  recompute-and-compare described below. See [The gateway](/concepts/the-gateway) for the
  full, current mechanism and the one caveat that remains (only one connector is registered).
</Warning>

## The problem: check-vs-use (TOCTOU)

A system that authorizes an action by checking a payload, then executes a *different*
payload under the same authorization, has a Time-Of-Check-To-Time-Of-Use gap. An
authorization that only names an ID (`businessTransactionId: "tx-001"`), without binding
to the actual content, cannot detect this: anything can be substituted under a
previously-approved ID.

## The real mechanism

Parmana's `SignedExecutionAuthorization` binds a **canonical content hash** into the signed
payload, not just an ID:

```typescript theme={null}
// packages/shared/src/domain/execution-authorization.ts
export interface ExecutionAuthorizationPayload {
  readonly version: 1;
  readonly authorizationId: string;
  readonly nonce: string;
  readonly decisionId: string;
  readonly businessTransactionId: string;
  readonly policyName: string;
  readonly policyVersion: string;
  readonly authorizedAt: string;
  readonly expiresAt: string;

  /**
   * Canonical content hash of the ExecutableContent (businessTransactionId,
   * action, target, parameters) approved for execution. Computed identically
   * by the signing side and the verifying side via the same
   * ExecutableContentHasher, so a receiving gateway can recompute this hash
   * from the exact content it is about to forward and reject any mismatch.
   */
  readonly businessTransactionHash: string;
}
```

`ExecutionGateway.verify()` is where the check-vs-use gap actually closes: it recomputes
the hash of the content it is about to forward, and compares it to the signed
`businessTransactionHash`:

```typescript theme={null}
// packages/execution-gateway/src/ExecutionGateway.ts:150-165
if (passed) {
  const actualHash = await this.contentHasher.hash(executableContent);
  const expectedHash = request.authorization.payload.businessTransactionHash;

  businessTransactionHashMatches = actualHash === expectedHash;

  if (!businessTransactionHashMatches) {
    hashMismatch = { expected: expectedHash, actual: actualHash };
  }
}
```

Verification order, side-effect-free checks first (`ExecutionGateway.ts:86-90`):

```
version → signature → expiry → TTL policy → businessTransactionHash recompute-and-compare → nonce
```

The nonce (single-use) check runs **last**, and only if every prior check, including the
content hash, passed. This matters: a forged or mismatched request must never burn a
nonce, or an attacker who observes a nonce in transit could poison it and get the
*legitimate* request rejected instead (`ExecutionGateway.ts:170-180`).

`ExecutableContentHasher` delegates to the same `TrustRecordHasher` used elsewhere in the
system (`packages/crypto/src/ExecutableContentHasher.ts`), the signing side and verifying
side run identical canonical serialization and hashing, never two parallel
implementations of the same computation.

## What this proves, precisely scoped

<Note>
  From CLAIMS.md 3.1 (Conditional Claim, load-bearing scope): *"For any system running the
  Parmana envelope verifier, execution requests not authorized by Parmana are
  cryptographically impossible to accept. This claim holds only for a receiving system that
  (a) runs `@parmana/envelope-verifier` and (b) gates every execution-triggering code path
  behind its verification result. Parmana enforces nothing at the network level."*
</Note>

This is real, tested protection, for a **gateway-integrated system**. It is not a
network-level guarantee, and it is not automatically true of every Parmana deployment.

## Confirmed: the default local server now enforces this

This was traced directly, not assumed. `packages/api/src/bootstrap/createExecutionSystem.ts`
unconditionally returns `createExecutionGateway()` (commit `651497a`, 2026-07-11), and
`packages/api/src/server.ts` always passes that gateway into `createApplication()`. There is
no code path where the server skips gateway verification. See [The gateway](/concepts/the-gateway)
for the current, wired mechanism and its one real caveat: only one connector is registered
by default, an action with no matching connector fails closed rather than skipping checks.

### Demonstrated, at the library level

The REST API generates and verifies an authorization within the same request today, so a
black-box HTTP client can't provoke a hash mismatch through the public API alone, both sides
of the check run inside the same process, on the same content, in the same call. The
tamper-and-verify demonstration therefore has to happen where an authorization is a
first-class object a caller can mutate before re-checking it, which is exactly what
`examples/tutorials/36-parameter-tampering/run.ts` does: build a Business Transaction, get
its real `businessTransactionHash` back from `RuntimeBuilder`, independently recompute the
hash of a **tampered** copy of the same content (payment amount changed) using the same
`ExecutableContentHasher`, and compare:

```typescript theme={null}
// examples/tutorials/36-parameter-tampering/run.ts
const originalHash = context.authorization.payload.businessTransactionHash;
const tampered = { ...content, parameters: { ...content.parameters, paymentAmount: 500000 } };
const tamperedHash = await hasher.hash(tampered);
// originalHash !== tamperedHash
```

This is the same `ExecutableContentHasher` and the same comparison
`ExecutionGateway.verify()` runs internally, exercised directly rather than through HTTP.
Tutorials 35 through 46 (`examples/tutorials/`) cover the same negative-path pattern for
replay, substitution, forgery, and expiry: mutate, re-verify, watch it fail. See [Detect
tampering](/guides/detect-tampering) for three of these run live with real output.

## Connector SDK: the same content, unmodified, all the way to the connector

`@parmana/connector-sdk`'s `SdkConnectorExecutor` sits downstream of every check above, it
only ever runs after `ExecutionGateway.verify()` has already passed and the content is
already deep-frozen (`ExecutionGateway.ts`'s `deepFreeze(executableContent)`). From there:

* `SdkConnectorExecutor.execute(content, credential)` builds `ConnectorRequest` by copying
  `businessTransactionId`, `action`, `target`, and `parameters` straight off the verified,
  frozen `content`, never re-deriving or re-interpreting any of them.
* The same `ConnectorRequest` object is what a `Connector` (`HttpConnector`, `MockConnector`,
  or a future one) receives; nothing in between constructs a second copy.
* `capability` is `content.action` itself, so "capability" cannot silently diverge from the
  authorized `action`, there is only one field, not two that could disagree.

Any mismatch between what was authorized and what a connector would execute is still
caught upstream, in the unchanged `ExecutionGateway.verify()` hash comparison, this is a
permanent architectural invariant, not something `connector-sdk` re-implements or could
weaken. `connector-sdk` adds capability-declaration and version/health checks *in front of*
a connector call (`SdkConnectorExecutor`, `CapabilityConnectorPolicy`), all of which fail
closed (throw, never a partial or guessed success) before a connector is ever invoked.

## Related, and also worth knowing

* **Nonce single-use is scoped to whichever `NonceStore` instance checks it.** Multiple
  independent gateway instances each using their own `MemoryNonceStore` can each accept the
  same authorization once. Fleet-wide single-use requires one shared store (CLAIMS.md 3.2).
* **ML-DSA-65 (post-quantum) signatures are randomized, not deterministic.** Signing the
  same message twice with the same key produces two different, both-valid signatures, only
  *verification* is deterministic. Don't build tooling that assumes identical input produces
  an identical PQ signature (CLAIMS.md §5).
* Every envelope carries a bounded TTL specifically so the exposure window from either of
  the above gaps is bounded, not unlimited.
