From b6959029be1dc632c2143939f254418655ee3b68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Z=20Sink=C3=B3?= Date: Sun, 8 Nov 2026 14:07:23 +0100 Subject: [PATCH 1/2] docs: Relay requirements for approval, authority and execution state (M4b, R6 to R10) The module now proves a request is internally consistent and echoes its execution_digest (M4a). This records, as requirements toward the Relay and as a concept-level spec, what has to be added so an execution is also authorized, happens at most once and leaves evidence: an authenticated run-bound execution context (R6), approval verification naming the execution_digest before the first provider call (R7), an atomic durable reservation with a write-ahead record before each provider request and an UNKNOWN state that is never resent automatically and leaves only through recorded reconciliation (R8), a ProofTrace that keeps requested, authorized, sent, confirmed and uncertain apart and names the decision context (R9), and destroy and invoke under the same model (R10). Each item has the current behaviour read from the Relay at 55a899f, the required behaviour, a negative test and an acceptance condition; what was not read is marked NOT VERIFIED. The seven acceptance cases and the crash and module-refusal cases are written as tests for later. Documentation only: no code, no Relay change. --- [signing-metadata] key = cic-my-sign-key signature = vault:v1:MEUCIF4XuTubkyAMdUO0odGU1S3GaCjDiKMlkhemaYaBPXteAiEAv/jXy04OQ1OgLLaoYKceeaxIC/xD9GWNs+M/WvAL958= hash-algorithm = sha256 digest = 0cPd/Geh8e9QRlNKY7G0bO0EdurpuFeWxpAwGicECLk= [certificate] -----BEGIN CERTIFICATE----- MIICBjCCAaygAwIBAgIUSnRMR6RPnEbg296XWPOqq/u5PCwwCgYIKoZIzj0EAwIw QzELMAkGA1UEBhMCSFUxGTAXBgNVBAoMEENlbnRyYWxJbmZyYUNvcmUxGTAXBgNV BAMMEENJQyBEZXZlbG9wZXIgQ0EwHhcNMjYwMzIwMTMyMjU5WhcNMjYxMjMxMTMy MjU5WjBFMQswCQYDVQQGEwJIVTEZMBcGA1UECgwQQ2VudHJhbEluZnJhQ29yZTEb MBkGA1UEAwwSR2Fib3IgWm9sdGFuIFNpbmtvMFkwEwYHKoZIzj0CAQYIKoZIzj0D AQcDQgAEIG2CVmTfmLB9pLLclj7YmP2eedAjklpy4LGrU2ijoiy6Xqpuybv7OgJe i+ez31s65NEV8+X/ByeX1cstR988z6N8MHowCQYDVR0TBAIwADAdBgNVHQ4EFgQU yZN6AIX/TNnIJ9GwAa/NRN3ujHAwHwYDVR0jBBgwFoAUXn6CHYzPUqU4JVP8g+OS WeDYjhcwDgYDVR0PAQH/BAQDAgeAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEF BQcDBDAKBggqhkjOPQQDAgNIADBFAiEA+bFzXRoJ4PCQbhAAtpkcMjt0vNj5rEW0 lOMBGDNyaWkCIB1vmM7PcZzv/c9bIrxF5kqv6QXomouhByUfeNUTbpKW -----END CERTIFICATE----- --- MANIFEST.sha256 | 5 +- docs/design/README.md | 3 + docs/design/relay-requirements.md | 108 ++++++++++++ .../specs/approval-and-execution-state.md | 156 ++++++++++++++++++ 4 files changed, 270 insertions(+), 2 deletions(-) create mode 100644 docs/design/specs/approval-and-execution-state.md diff --git a/MANIFEST.sha256 b/MANIFEST.sha256 index 795aaa9..ff447be 100644 --- a/MANIFEST.sha256 +++ b/MANIFEST.sha256 @@ -2,6 +2,7 @@ 01956821ece300a66ff61849618cb03b0b3fd5aefce4dfd9bff7a4a33c867e1e docs/design/specs/capability-manifest.md 052458dca6bed674aec47f25561534ce72cda99a62faf33e2e947474400c7374 tools/vault-rootCA-sign-agent.sh 05fca2112ed6319a91d0f373f9dcab23df35687daea510498110e9f1ce80e498 module/testdata/vcn-v1/cases/field-absent/expected/verdict.fields.json +076a0144b5843b04b96ae8982544ebe3350434732dcbaf34ee8ccb9d88e0f2b9 docs/design/relay-requirements.md 0991a93d56b1e24dc9285369a6f636b50fcc5fa6dec48305f76647828c6fbd19 mk/wasm.mk 0d08c6a7982760ef2c1bac82bf587e8d2c0e30fadc6638056e63aaa5e259a1cd tools/finalize_release.py 0e290d5b03efe23941d250236579dbed1c3cd6afdee70234f757b44aa7f6cc77 md.meta.schema.yaml @@ -98,6 +99,7 @@ 7080b3423beded8b64891afdcf2e6f6d8bec6ebeaa88aecbb1b06e149b54ef86 module/module.wasm 7136d8366bbd7c5ccc19a3d810a7f55c31541acec9b0f05bd84e45a39107ded7 tests/test_capability_manifest.py 7426752feb1140b2023192728df35248e4f802617c150bb0523ab97dc1638c0b tools/init-hooks.sh +7499755898c9fe815f8cb5b62314630258bc68fe5d461097ff7f10644508bed3 docs/design/README.md 74d93981f1935d21001ed0475ebb84132ab67c71840ca82ca0cbfac5aca29ea2 docs/contracts/en/wasm-abi.md 763f2814427dc8faaa7fb1ccf3e8afe2f6460273a2dfe852287b3ef66bc43b96 tools/oci-extract/policy_test.go 768d54fe8ec5fde38c41ec075ce02b51d93cf3bf5f47e8d81e33932f3e3ece27 module/ocid_boundary_test.go @@ -157,7 +159,6 @@ b3585cfa2c622b28548424d092f4ee05a679d7ced46a762f7c1f5bc4931e5c27 tools/oci-extr b3d0d0dd71c294b5fa8bad4fcdd9784feae35141aeb71e1755abd8bbd0cfca58 pyproject.toml b4243f2ae7001af0ff5fe747ce968866becba32ec759c0adcf3b7eba008f6ed3 README.hu.md b579f80d62ce67a1be2da3093c3d26e63c4cde31000ddb4fd4787eeda57f423a .gitignore -b5b105b792741f2103911ab66377ca99cc3d06b249a14e97ce94b39849e457c8 docs/design/README.md b5fdf8f066fa2295376d100b62429c7a20edbb04dd0f31f1b7a83507a3374b2e module/testdata/vcn-v1/cases/transport-error/oci/error.json b73da39348121a3ae5bf42915a4ab3c35653b7c9fb5a4c575a9ce0d89da66c7a docs/en/wasm-module-authoring.md b8107f8b819135d4ba3d56804ecca545c24bde54a381f5ddde7ac19815253734 docs/design/forms-integration-direction.md @@ -180,7 +181,6 @@ cb2ab94de33591fc8421f3a9e60a50a7086fc0a24aca6be83c8e383b3753af75 docs/design/ro cd509e106eb7e827109cca550f46afbcd0ec049c47a719de9debabbbc9c32c57 module/testdata/vcn-v1/cases/transport-error/expected/observed.oci.json cdc268dd275143f3cbad6b84078f397367c40951812143d227b5490cd007ce82 tools/infra.py d08117cca9ab07e9035ddd59f5e7cb0a610517f95b85542988a3222f08a015da module/testdata/instance-fixture.json -d1beaffed059ebbdbee38bc20b465a0e82bb0a4e11b0dc4ee801887cde0f358b docs/design/relay-requirements.md d2c71865652ef4c4f9bed86eaeac6e5a63570e8677987b90a97cb756258ab234 feature-list.md d2dfadbd6532e7de6ae35af273fbdc446ffbb3e7f5b24d7092b3dea70ec2001b module/poll_contract_test.go d3bce87b8745997eb0fb912dcd61c4ee8fd6d6596be6fec41a51649edcf32684 docs/design/specs/canonical-json.md @@ -201,6 +201,7 @@ dbfb640d40533eeb73ececd170189f951f16ad07c549a8321dbb12251e78d067 features/featu dc0c98d1e3ca6fac0ef40227c6f8c026397180059c95188d9adb4567cef2e11d tests/test_tools/test_infra.py dcd74edeae0b4e8ffc610d2dfc01c302221eda1b1aac6df12f8334b2c96fbf20 module/testdata/vcn-v1/cases/malformed-cidr/expected/verdict.fields.json de838914aa32b597e9575a29096df381802c369f867f2655e7ca114106ddab92 module/testdata/vcn-v1/cases/not-found/oci/error.json +def74b3e76207bb03d86e722d877ca5b56f3dcc6221ef6ac6519878dce1e829b docs/design/specs/approval-and-execution-state.md df1a1d134a0917f492882af78775b072430c47dd43008625140ad35e206457e9 tests/test_tools/test_releaselib/test_vault_service.py df4af3ee8dfbb9616d0409f9975d7caf232a84517813debddce5a1aa06920f69 oci-sdk.lock.yaml e08953038f05a27cb31a632c32821b549741bade6bb8a8a1377d22f95ba62081 module/schemas/core/vcn.json diff --git a/docs/design/README.md b/docs/design/README.md index 68b44b2..f1eb056 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -31,6 +31,9 @@ namespace it opens. Written 2026-07-18 from the design session recorded in computed over. - [specs/plan-digest.md](specs/plan-digest.md) — `plan_digest`, `target_digest`, `execution_digest` and the gate `execute` applies before it signs anything. +- [specs/approval-and-execution-state.md](specs/approval-and-execution-state.md) — M4b (concept): + what an approval names, the execution state of one approval (atomic reservation, UNKNOWN), + the ProofTrace evidence, and the acceptance tests the Relay side will be held to. ## Direction diff --git a/docs/design/relay-requirements.md b/docs/design/relay-requirements.md index 2f89ae0..d504532 100644 --- a/docs/design/relay-requirements.md +++ b/docs/design/relay-requirements.md @@ -177,6 +177,114 @@ third-party module author has no way to hit this problem today either. --- +## M4b - approval, authority and execution state (R6 to R10) + +The module can now prove that an `execute` request is internally consistent and echo its +`execution_digest` (M4a, [specs/plan-digest.md](specs/plan-digest.md)). It cannot prove that +anyone allowed it, that it has not run before, or what happened to it. Those belong to the +relay. The behaviour we need, and the tests that will show it, are in +[specs/approval-and-execution-state.md](specs/approval-and-execution-state.md); the items +below are the asks. All are `needed` and **not yet raised** in `CIC-Relay`. Relay facts are +from commit `55a899f`, read, not executed; what was not read is marked NOT VERIFIED. As +above, these state needs, not an implementation: the relay team owns the design. + +Each item: current behaviour, required behaviour, negative test, acceptance condition. + +## R6 - An authenticated, run-bound execution context for the guest, and for its host calls · needed + +**Current behaviour (read).** The cabinet gives a guest an `authContextJson` that is just +the component id (`core/cabinet/service.go:228`) and dispatches only `init`, `process`, +`get` and `notify` (`:234-240`, `core/cabinet/cicwasm.go`). The `cic-flow` host functions +get `cicFlowDeps{sign, doer, egress}` (`cmd/relay/cic_flow_host_funcs.go:51-55`): no run, no +principal, no plan. `sign` and `actuate` each accept a `trace_id` in the request and never +use it (`:112-116`, `:141-148`); it is chosen by the guest, so it cannot be an authority. + +**Required.** Each `sign`/`actuate` call is attributable, by the relay's own authenticated +context and not by anything the guest supplies, to one authenticated principal, one run and +one execution (an `execution_digest`). A guest cannot name another execution's context. + +**Negative test.** A guest that sends a forged `trace_id`, or none, is attributed to the +real run; a call made outside any run is refused. + +**Acceptance.** Every provider request in the trace names the authenticated run and the +execution it belongs to. + +## R7 - Approval verification before the first provider call · needed + +**Current behaviour (read).** No approval concept sits on the module's execution path. The +relay has an intent-authorization seam, `core/nexus/ois` (`Intent{Actor, Action, TargetRef, +Reason}`, `Policy.Authorize`), but its only non-test importers are `core/nexus/recorder` +(rollback) and `tools/statechain-demo`; neither `cmd/relay` nor `core/cabinet` uses it. The +model exists; the link to the module's execution breaks there. + +**Required.** Before the first `sign` of an execution the relay verifies an approval that +names the execution's `execution_digest`: authentic, not expired, not revoked, in scope for +the kind and operation, issued by a policy decision that is identified. The module's digest +is compared; the module is not asked whether the approval is genuine. + +**Negative test.** No approval; an approval for another `execution_digest`; an expired or +revoked one: no provider request in each case. + +**Acceptance.** The only way a `sign` or `actuate` reaches the provider is through a verified +approval for exactly that execution. + +## R8 - Atomic, durable single use, and an UNKNOWN state · needed + +**Current behaviour (read).** Nothing in the cic-flow path records that an approval, or a +request, was used; `actuate` turns any transport error into one error text +(`cmd/relay/cic_flow_host_funcs.go:141-190`), so a request that never left and one whose +answer was lost look the same to the guest. NOT VERIFIED: whether the relay has a durable +store it could use, and its guarantees. + +**Required.** The state machine of the spec: an atomic, durable reservation before the +first provider request; a write-ahead record before each provider request; a release only +when no request was attempted; UNKNOWN when a request was attempted and no answer was +recorded, never resent automatically and left only through recorded reconciliation by +evidence (for an asynchronous operation including the correlation of the Work Request with +the execution). Concurrent requests for one approval: at most one executes. A restart +finds the state as left. + +**Negative tests.** Two or more concurrent uses; a lost answer; a restart after the +reservation; a restart after the write-ahead; a module that refuses with zero sends. + +**Acceptance.** For one approval the provider sees the requests of at most one execution, +whatever the concurrency, restarts or lost answers; an execution of unknown outcome is +visible as such. + +## R9 - Decision context and the five kinds of evidence in the ProofTrace · needed + +**Current behaviour (read).** The cic-flow host functions at `55a899f` use no audit or +ProofTrace sink (no use of one in `cmd/relay/cic_flow_*.go`, outside tests). R4 above +covers the chained audit entry on the FFI path. + +**Required.** An execution's trace records, as separate kinds of entry, what was requested, +what was authorized, what was sent to the provider, what the provider confirmed, and what +remains uncertain; and carries: the authenticated run id, the policy decision id, the digest +of the verified module artifact, the reservation transition, and the three digests. A signed +observation is its own kind and is never recorded as the proven physical state. + +**Negative test.** An UNKNOWN execution shows an `uncertain` entry and no `confirmed` one. + +**Acceptance.** From the trace alone one can tell the five apart for any execution. + +## R10 - `destroy` and `invoke` under the same model · needed + +**Current behaviour (read).** The module has `destroy` and `invoke` as entry points of its +own, with no plan and no digest. NOT VERIFIED: whether the relay sees the operation name +before the guest runs. + +**Required.** An approval is required for them too, naming a digest of the request; the +relay must not route them around R7 to R9. (The module still has to produce that digest: +see "Entry points" in the spec.) + +**Negative test.** A direct `destroy` or `invoke` with no approval: refused, no provider +request. + +**Acceptance.** No operation that can change the provider's state is reachable without an +approval. + +--- + ## How to raise these 1. Keep this file current as needs are discovered or resolved. diff --git a/docs/design/specs/approval-and-execution-state.md b/docs/design/specs/approval-and-execution-state.md new file mode 100644 index 0000000..8e97b98 --- /dev/null +++ b/docs/design/specs/approval-and-execution-state.md @@ -0,0 +1,156 @@ +# Approval, authority and execution state (M4b) + +Status: **concept**. Nothing in this document is implemented in the module or the Relay. +The module side (M4a, `plan-digest.md`) proves that a request is internally consistent and +echoes an `execution_digest`; this document says what the Relay has to add so that an +execution is also *authorized*, happens *at most once*, and leaves *evidence*. The Relay is +read-only from this repository: what follows is the behaviour we need from it +(`relay-requirements.md`, R6 to R10), not a design of it. Relay facts are from CIC-Relay at +commit `55a899f`, read, not executed; anything not read is marked NOT VERIFIED. + +## The three questions, kept apart + +| Question | Answered by | Where | +|---|---|---| +| Is this exactly the plan and target we mean? | the module, by deriving it again | M4a, implemented | +| Did an authenticated principal allow this execution, under which policy, within which limits? | the Relay | M4b, concept | +| Was this approval used before, by whom, and what happened? | the Relay, durably | M4b, concept | + +A request that is consistent is not approved; an approval is not proof that the request +matches it (the Relay compares its digest); and a used approval says nothing about whether +the provider acted. They are checked independently and tied together by one value, the +`execution_digest`. + +## What an approval names + +An approval is a record the Relay can authenticate. It names, at least: + +- `execution_digest` (the module's: it binds `plan_digest` and `target_digest`); an approval + for any other digest is not an approval for this execution; +- the approver, and the policy decision that allowed it (an identifier of the decision, not + a bare "yes"); +- its scope: the kind, the operation class, and optionally a specific authenticated run; +- its validity window (not before, not after) and its revocation state; +- the digest of the module artifact that was verified when the approval was bound, so an + approval cannot be spent on a different module build (see the negative tests). + +The module never decides whether an approval is genuine. It only produces the digest. + +## The execution state of one approval + +The Relay keeps one durable record per approval and moves it through these states. Every +transition is recorded before the action it permits, and acknowledged by the store. + +``` +ISSUED --reserve--> RESERVED --about to send step i--> SEND_ATTEMPTED(i) --answer--> ANSWERED(i) + | | | + | +--run ends, zero sends--> RELEASED +--no answer recorded--> UNKNOWN + +--expired / revoked--> VOID +``` + +- **Reservation is atomic and durable.** `reserve` is a compare-and-set from ISSUED to + RESERVED keyed by the approval, recording the authenticated run, the `execution_digest` + and the verified module digest. Of any number of concurrent requests for one approval, + at most one wins; the others are refused. The reservation is committed to durable state + and acknowledged **before the first `sign` or `actuate` of that execution**. A reservation + that only lives in memory is not a reservation. +- **Write-ahead before every provider request.** Before each request that can cause a + side effect leaves the Relay, the Relay records SEND_ATTEMPTED for that step; when the + answer arrives it records ANSWERED with what the provider said. This is what makes a + crash or a lost answer visible afterwards. +- **Zero sends may be released.** The Relay brokers every `actuate`. If a run ends with no + SEND_ATTEMPTED at all (the module refused the request in its own gates, or the run failed + before sending), nothing can have reached the provider, and the reservation may return to + ISSUED (RELEASED) so the approval can be used again, within its validity. Once any + SEND_ATTEMPTED exists the reservation is never released automatically. +- **A component restart** must find the state as it left it: a RESERVED record with no + SEND_ATTEMPTED older than its lease may be released; a SEND_ATTEMPTED with no recorded + answer becomes UNKNOWN. + +### UNKNOWN is not failure and is not retryable + +The case that decides the design: the Relay sent a provider request and lost the answer +(timeout, restart, a transport error after the request left). It does not know whether the +operation happened. + +- The execution is **not** recorded as failed and the approval is **not** released. +- Nothing is resent automatically: not by the Relay, not by a retry in the module, not by + re-running the same request. A second create can create a second resource. +- The state leaves UNKNOWN only through **reconciliation by evidence**, which is itself + recorded: for an update or an action, an observation compared with the desired state and + the revision; for a create, evidence that the resource exists or provably does not; for an + asynchronous operation, the Work Request correlated with this execution (its id from the + provider's answer; if the answer with the id was the lost one, a correlation by other + evidence, which has to be defined and measured before it is relied on). +- A provider answer that proves the request had no effect (a validation or authorization + refusal) may be ANSWERED as rejected; an ambiguous answer (a 5xx, a gateway timeout) is + not such a proof and stays UNKNOWN until reconciled. +- NOT MEASURED: whether the OCI operations used here accept an idempotency token that the + `execution_digest` could be derived into; nothing here assumes one. + +Reading the Relay at `55a899f` (R8): the guest-facing `actuate` returns every transport +error as one error text, so a request that was never sent and one whose answer was lost look +alike to the guest. That is why the UNKNOWN rule has to be the Relay's. + +## Evidence (ProofTrace) + +The trace of an execution keeps five things apart, as separate kinds of entry, never +merged into one "result": + +| Kind | What it says | Minimum content | +|---|---|---| +| **requested** | what was asked | authenticated requester, authenticated run id, `execution_digest`, `plan_digest`, `target_digest` | +| **authorized** | what was allowed | `approval_id`, approver, policy decision id, scope and validity, the reservation transition (ISSUED to RESERVED) | +| **sent** | what went to the provider | per step: index, operation, method, URL without secrets, a digest of the request, time, the digest of the verified module artifact | +| **confirmed** | what the provider said back | status, `opc-request-id`, etag, Work Request id, a digest of the answer | +| **uncertain** | what is not known | the UNKNOWN state, why, and what evidence is missing | + +A signed **observation** is its own kind: it says what a read returned, from which source, +at which time. It is never recorded as the proven physical state of the resource. + +The entries are hash-chained as the existing audit mechanism does (R4), and the trace names +the same `execution_digest` the module echoes in its result. + +## Entry points + +`execute` is the one with a plan and a digest. `destroy` and `invoke` are separate entry +points of the module with no plan: a delete and an action on a named resource. They must +not remain a way around the approval model, so an approval has to be required for them too: + +- Relay side: R10. NOT VERIFIED whether the Relay sees the operation name (`execute`, + `destroy`, `invoke`) before the guest runs; the cabinet at `55a899f` dispatches only + `init`, `process`, `get` and `notify` to a guest (`core/cabinet/service.go:228-240`). +- Module side (**concept**, not built): a digest for `destroy` and `invoke` over the same + framing (operation, kind, the target, and for `invoke` the canonical body), so an approval + can name it. Until it exists the module cannot hand the Relay a value to approve for them. + +`observe` and `poll` only read; whether they need an approval is a policy choice of the +Relay, not decided here. + +## Acceptance tests (to be written when the Relay side exists) + +Each is a test the Relay's owner can run against the mocked provider; "no provider request" +means zero `sign` and zero `actuate` calls at the provider seam. + +| Case | Given | Expected | +|---|---|---| +| No approval | an `execute` with no approval, or an unauthenticated one | no provider request | +| Wrong digest | an approval for another `execution_digest` | refused, no provider request | +| Expired or revoked | an approval outside its window or revoked | refused, no provider request | +| Two concurrent uses | N parallel requests for one approval (N >= 2, repeated) | exactly one reserves; the rest are refused; the provider sees the requests of one execution only | +| Lost answer | the provider seam drops the answer to a sent request | the state is UNKNOWN; after any number of retries and a restart the provider has still seen that request once; no automatic resend | +| Reconciliation needed | an UNKNOWN execution | it leaves UNKNOWN only through a recorded evidence entry | +| Crash after reservation | the component stops after RESERVED, before any send | after restart the approval is releasable (zero sends), never executed twice | +| Crash after write-ahead | the component stops after SEND_ATTEMPTED | after restart the state is UNKNOWN, not RELEASED | +| Module refused | the module refuses the request in its own gates (zero sends) | RESERVED returns to ISSUED; the approval is still usable within its window | +| Changed module artifact | the module digest differs from the one verified when the approval was bound | the approval is not usable; no provider request | +| Destroy or invoke without approval | `destroy` or `invoke` called directly | refused, no provider request | +| Trace shape | any execution, including an UNKNOWN one | the five kinds are present where they apply and distinguishable; an observation is not labelled as proven state | + +## What this does not claim + +Not that the Relay does any of this today (it does not: see R6 to R10), not that the +store, the policy engine or the identity source exist, not that an approval, once +authenticated, makes the plan safe, and not that a reconciled UNKNOWN proves what the +physical state is: it records the evidence that was found. `module.wasm` is not rebuilt, +and a production integration stays on hold until the Relay side exists and is measured. From 5b0a6ed96d0e68a7d374ca046b245b111868ff55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Z=20Sink=C3=B3?= Date: Sun, 8 Nov 2026 14:24:26 +0100 Subject: [PATCH 2/2] docs: bind the approval to the host call, and make the safety record fail-closed (M4b, review F1 and F2) F1: the cic-flow host functions are independent of each other and of any plan: sign signs the bytes the guest hands it, actuate forwards the guest's own method, URL, headers and body. A valid execution_digest and approval therefore prove nothing about which HTTP request leaves. R7 now states as a required security property that, before every actuate that can cause a side effect, the relay checks from its own authenticated context that the actual method, target, body and step belong to the reserved, approved execution, that a sign serving no such step is refused, and that a digest or trace id supplied by the guest is not authority. Two new negative tests: a different request under a valid approval, and a direct call of the host function outside an approved context. The spec gets a section on the host boundary. F2: the reservation and the write-ahead record are safety state, not telemetry: if they cannot be made durable no provider request is sent, and they are kept apart from optional trace processing. The cabinet's recorder at 55a899f is optional, runs once after all steps and continues on a failed write, so it is a post-hoc best-effort record (read: core/cabinet/service.go:99, :105-106, :299-307). A test for a failing store is added. Documentation only. --- [signing-metadata] key = cic-my-sign-key signature = vault:v1:MEUCIHBcFbHkfWq+LzDdJk57zY3PfvrbbJpozZ9KRfJQBraqAiEA7aV/FmgWI4XDnGfiqQSUEJvuBS/s8hrbFh5HtrCscJc= hash-algorithm = sha256 digest = YG1uxcICA5NtGtUbOksRFXtrjvAqM/OnyocQX8e+bNw= [certificate] -----BEGIN CERTIFICATE----- MIICBjCCAaygAwIBAgIUSnRMR6RPnEbg296XWPOqq/u5PCwwCgYIKoZIzj0EAwIw QzELMAkGA1UEBhMCSFUxGTAXBgNVBAoMEENlbnRyYWxJbmZyYUNvcmUxGTAXBgNV BAMMEENJQyBEZXZlbG9wZXIgQ0EwHhcNMjYwMzIwMTMyMjU5WhcNMjYxMjMxMTMy MjU5WjBFMQswCQYDVQQGEwJIVTEZMBcGA1UECgwQQ2VudHJhbEluZnJhQ29yZTEb MBkGA1UEAwwSR2Fib3IgWm9sdGFuIFNpbmtvMFkwEwYHKoZIzj0CAQYIKoZIzj0D AQcDQgAEIG2CVmTfmLB9pLLclj7YmP2eedAjklpy4LGrU2ijoiy6Xqpuybv7OgJe i+ez31s65NEV8+X/ByeX1cstR988z6N8MHowCQYDVR0TBAIwADAdBgNVHQ4EFgQU yZN6AIX/TNnIJ9GwAa/NRN3ujHAwHwYDVR0jBBgwFoAUXn6CHYzPUqU4JVP8g+OS WeDYjhcwDgYDVR0PAQH/BAQDAgeAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEF BQcDBDAKBggqhkjOPQQDAgNIADBFAiEA+bFzXRoJ4PCQbhAAtpkcMjt0vNj5rEW0 lOMBGDNyaWkCIB1vmM7PcZzv/c9bIrxF5kqv6QXomouhByUfeNUTbpKW -----END CERTIFICATE----- --- MANIFEST.sha256 | 4 +- docs/design/relay-requirements.md | 63 ++++++++++++++++--- .../specs/approval-and-execution-state.md | 35 +++++++++++ 3 files changed, 91 insertions(+), 11 deletions(-) diff --git a/MANIFEST.sha256 b/MANIFEST.sha256 index ff447be..8371d3a 100644 --- a/MANIFEST.sha256 +++ b/MANIFEST.sha256 @@ -2,7 +2,6 @@ 01956821ece300a66ff61849618cb03b0b3fd5aefce4dfd9bff7a4a33c867e1e docs/design/specs/capability-manifest.md 052458dca6bed674aec47f25561534ce72cda99a62faf33e2e947474400c7374 tools/vault-rootCA-sign-agent.sh 05fca2112ed6319a91d0f373f9dcab23df35687daea510498110e9f1ce80e498 module/testdata/vcn-v1/cases/field-absent/expected/verdict.fields.json -076a0144b5843b04b96ae8982544ebe3350434732dcbaf34ee8ccb9d88e0f2b9 docs/design/relay-requirements.md 0991a93d56b1e24dc9285369a6f636b50fcc5fa6dec48305f76647828c6fbd19 mk/wasm.mk 0d08c6a7982760ef2c1bac82bf587e8d2c0e30fadc6638056e63aaa5e259a1cd tools/finalize_release.py 0e290d5b03efe23941d250236579dbed1c3cd6afdee70234f757b44aa7f6cc77 md.meta.schema.yaml @@ -166,6 +165,7 @@ b8e7c23cfe46a0daf7d3441b15657e9d4b35c4bf50f9421c2841dfbf19cacc82 docs/en/archit b9325f58a95216b240f3715a773e34fe8cdfa4d5fe48a5e7b32002a40be5cc2e tests/test_tools/test_finalize_release.py bb8a1743ab9f77a309bd851b4a1660f6f2420900455a5a0dfb83f4e6905968ff module/abi_manifest_test.go bbe93c718d90a03de5aab2999031c7097bd63a8e40ec14589c8dbe4364a4ec52 renovate.json +bdea227cb29b579b2daaa7fa398102adc096ff9073c78bd9ec1f0dde44b89824 docs/design/relay-requirements.md bef22a55d1559676a57b4a05b48efc7cd226d3d733a1c932e183666679046b53 module/testdata/vcn-v1/cases/drift/expected/evidence.claims.json bfccbd3cd778ac6f0458b7d45d7b25e2cbdc0a789f40e81c1de476f8aab44363 tools/schemalib/loader.py c31a1313ace9ae3da9483d7ac7bbc0938d5e2f6c7c0460d785d94db5bb931e85 docs/contracts/en/release-artifact.md @@ -176,6 +176,7 @@ c6919cec77cbf13916932f91cdc9b9d32d9290282c5d87244af7adc1a30d441a module/go.sum c6f4ed120394d5d71b37e9b44293561cc76be2c7a8087d3facb12458c1b08805 module/m4a_gate_test.go c83bb77196b26e0f656d38b38736a488ff2aa316d995908bf9abade63fccdd67 tools/wasm_fresh_guest_test.sh ca7e4a9412ff28a5ca3e3fd9c759a62126564d82e8892b12b6efa1cbf11d5810 tools/oci-extract/client.go +cb1301d5e401e05e2c14042cb1af909dc2bc819a636b5b4e1533f4c87ed48831 docs/design/specs/approval-and-execution-state.md cb147348de6aadbcef95a82efb66d8e5252d1c82f34aba14c716747dea57d10f module/testdata/vcn-v1/cases/malformed-cidr/expected/evidence.claims.json cb2ab94de33591fc8421f3a9e60a50a7086fc0a24aca6be83c8e383b3753af75 docs/design/roadmap.md cd509e106eb7e827109cca550f46afbcd0ec049c47a719de9debabbbc9c32c57 module/testdata/vcn-v1/cases/transport-error/expected/observed.oci.json @@ -201,7 +202,6 @@ dbfb640d40533eeb73ececd170189f951f16ad07c549a8321dbb12251e78d067 features/featu dc0c98d1e3ca6fac0ef40227c6f8c026397180059c95188d9adb4567cef2e11d tests/test_tools/test_infra.py dcd74edeae0b4e8ffc610d2dfc01c302221eda1b1aac6df12f8334b2c96fbf20 module/testdata/vcn-v1/cases/malformed-cidr/expected/verdict.fields.json de838914aa32b597e9575a29096df381802c369f867f2655e7ca114106ddab92 module/testdata/vcn-v1/cases/not-found/oci/error.json -def74b3e76207bb03d86e722d877ca5b56f3dcc6221ef6ac6519878dce1e829b docs/design/specs/approval-and-execution-state.md df1a1d134a0917f492882af78775b072430c47dd43008625140ad35e206457e9 tests/test_tools/test_releaselib/test_vault_service.py df4af3ee8dfbb9616d0409f9975d7caf232a84517813debddce5a1aa06920f69 oci-sdk.lock.yaml e08953038f05a27cb31a632c32821b549741bade6bb8a8a1377d22f95ba62081 module/schemas/core/vcn.json diff --git a/docs/design/relay-requirements.md b/docs/design/relay-requirements.md index d504532..e94fa44 100644 --- a/docs/design/relay-requirements.md +++ b/docs/design/relay-requirements.md @@ -197,7 +197,11 @@ the component id (`core/cabinet/service.go:228`) and dispatches only `init`, `pr `get` and `notify` (`:234-240`, `core/cabinet/cicwasm.go`). The `cic-flow` host functions get `cicFlowDeps{sign, doer, egress}` (`cmd/relay/cic_flow_host_funcs.go:51-55`): no run, no principal, no plan. `sign` and `actuate` each accept a `trace_id` in the request and never -use it (`:112-116`, `:141-148`); it is chosen by the guest, so it cannot be an authority. +use it (`:112-116`, `:141-148`); it is chosen by the guest, so it cannot be an authority. Beyond that, the two host +functions are independent of each other and of any plan: `sign` signs whatever bytes the +guest hands it (`:112-140`), and `actuate` forwards the guest's own method, URL, headers +(its `Authorization` verbatim) and body to any destination the egress policy allows +(`:141-190`). Neither knows what execution, step or approval a call belongs to. **Required.** Each `sign`/`actuate` call is attributable, by the relay's own authenticated context and not by anything the guest supplies, to one authenticated principal, one run and @@ -222,11 +226,31 @@ names the execution's `execution_digest`: authentic, not expired, not revoked, i the kind and operation, issued by a policy decision that is identified. The module's digest is compared; the module is not asked whether the approval is genuine. -**Negative test.** No approval; an approval for another `execution_digest`; an expired or -revoked one: no provider request in each case. +**The approval must bind the actual host call, not only the module's entry point (a +required security property).** Verifying a valid `execution_digest` and approval proves +nothing about which HTTP request leaves the relay, because `sign` and `actuate` are +separate host functions that act on what the guest hands them. Before every `actuate` that +can cause a side effect, the relay checks, from its own authenticated context, that the +actual method, target, request body and step belong to the reserved, approved execution; +the set of external requests an execution may make is determined from the approved +execution (for instance through the plan its digest covers), never from what the guest asks +for. The same holds for `sign`: a signature over bytes the relay cannot tie to a step of a +reserved execution is itself a capability, so a `sign` that serves no such step is refused. +A digest or a trace id the guest supplies is not proof of authority. How the relay does +this is its own design; that it does is not optional. The module's own gate (M4a) covers +only the module's `execute` path and cannot protect the host boundary. + +**Negative tests.** +1. No approval; an approval for another `execution_digest`; an expired or revoked one: no + provider request in each case. +2. A valid approval, and the guest sends a different HTTP request (another method, target, + body or step than the approved execution's): no provider request. +3. The guest calls `cic-flow.actuate` (or `cic-flow.sign`) directly, outside any reserved, + approved execution context: refused, no provider request. **Acceptance.** The only way a `sign` or `actuate` reaches the provider is through a verified -approval for exactly that execution. +approval for exactly that execution, and only for the requests that execution is approved to +make. ## R8 - Atomic, durable single use, and an UNKNOWN state · needed @@ -244,18 +268,30 @@ evidence (for an asynchronous operation including the correlation of the Work Re the execution). Concurrent requests for one approval: at most one executes. A restart finds the state as left. +The reservation and the write-ahead record are **safety state, not telemetry**: their +durable recording is fail-closed. If a reservation or a write-ahead record cannot be made +durable, no provider request is sent. They cannot be best-effort. (This is different from the +optional trace recording the cabinet does today: see R9.) + **Negative tests.** Two or more concurrent uses; a lost answer; a restart after the -reservation; a restart after the write-ahead; a module that refuses with zero sends. +reservation; a restart after the write-ahead; a module that refuses with zero sends; the +durable store unavailable or failing at the reservation and at the write-ahead (no provider +request in either case). **Acceptance.** For one approval the provider sees the requests of at most one execution, whatever the concurrency, restarts or lost answers; an execution of unknown outcome is -visible as such. +visible as such; and no provider request leaves while the state that makes it safe cannot be +recorded. ## R9 - Decision context and the five kinds of evidence in the ProofTrace · needed **Current behaviour (read).** The cic-flow host functions at `55a899f` use no audit or ProofTrace sink (no use of one in `cmd/relay/cic_flow_*.go`, outside tests). R4 above -covers the chained audit entry on the FFI path. +covers the chained audit entry on the FFI path. The cabinet's ProofTrace recorder is +optional (`nil` skips recording, `core/cabinet/service.go:99`, `:105-106`) and runs once, +after every step has already run, with a recording failure logged and the workflow +continuing (`:299-307`). That is a post-hoc, best-effort record: it is not a write-ahead +record and cannot gate a provider request. **Required.** An execution's trace records, as separate kinds of entry, what was requested, what was authorized, what was sent to the provider, what the provider confirmed, and what @@ -263,7 +299,15 @@ remains uncertain; and carries: the authenticated run id, the policy decision id of the verified module artifact, the reservation transition, and the three digests. A signed observation is its own kind and is never recorded as the proven physical state. -**Negative test.** An UNKNOWN execution shows an `uncertain` entry and no `confirmed` one. +The evidence that makes an execution safe (the reservation, the write-ahead record, the +transitions of R8) is **durable and fail-closed**; richer or analytic processing of the +trace (display, aggregation, export) may stay optional and best-effort. The two are kept +separate so that losing a display does not lose a safety record, and a missing safety record +never goes unnoticed. + +**Negative test.** An UNKNOWN execution shows an `uncertain` entry and no `confirmed` one; +a durable evidence record that cannot be written stops the execution before the provider +request (R8). **Acceptance.** From the trace alone one can tell the five apart for any execution. @@ -278,7 +322,8 @@ relay must not route them around R7 to R9. (The module still has to produce that see "Entry points" in the spec.) **Negative test.** A direct `destroy` or `invoke` with no approval: refused, no provider -request. +request; and the same through the host boundary (a guest calling `cic-flow.actuate` +directly with a delete or an action request, R7 test 3). **Acceptance.** No operation that can change the provider's state is reachable without an approval. diff --git a/docs/design/specs/approval-and-execution-state.md b/docs/design/specs/approval-and-execution-state.md index 8e97b98..b4dc0b5 100644 --- a/docs/design/specs/approval-and-execution-state.md +++ b/docs/design/specs/approval-and-execution-state.md @@ -67,6 +67,11 @@ ISSUED --reserve--> RESERVED --about to send step i--> SEND_ATTEMPTED(i) --answe SEND_ATTEMPTED older than its lease may be released; a SEND_ATTEMPTED with no recorded answer becomes UNKNOWN. +The reservation and the write-ahead record are **safety state, not telemetry**. Recording +them durably is fail-closed: if a reservation or a write-ahead record cannot be made durable, +no provider request is sent. They cannot be best-effort, and they are kept apart from any +optional trace processing (see "Evidence"). + ### UNKNOWN is not failure and is not retryable The case that decides the design: the Relay sent a provider request and lost the answer @@ -92,6 +97,25 @@ Reading the Relay at `55a899f` (R8): the guest-facing `actuate` returns every tr error as one error text, so a request that was never sent and one whose answer was lost look alike to the guest. That is why the UNKNOWN rule has to be the Relay's. +## The host boundary + +The module's own gate (M4a) protects only the module's `execute` path. The thing that causes +an external side effect is not that path but the host boundary: `cic-flow.sign` signs the +bytes the guest hands it, and `cic-flow.actuate` forwards the guest's own method, URL, +headers (its `Authorization` verbatim) and body (read at `55a899f`). A valid +`execution_digest` and a verified approval therefore prove nothing about which HTTP request +actually leaves, unless the relay ties the host calls to the approved execution. + +**Required property (not an implementation).** Before every `actuate` that can cause a side +effect the relay checks, from its own authenticated context, that the actual method, target, +request body and step belong to the reserved, approved execution; the requests an execution +may make are determined from the approved execution (for instance through the plan its +digest covers), never from what the guest asks for. A `sign` that serves no step of a +reserved execution is refused too, because a signature over bytes the relay cannot tie to a +step is a capability on its own. A digest or a trace id supplied by the guest is not proof of +authority. Another guest path, another call, or a modified request must not be able to go +around the approval. + ## Evidence (ProofTrace) The trace of an execution keeps five things apart, as separate kinds of entry, never @@ -111,6 +135,14 @@ at which time. It is never recorded as the proven physical state of the resource The entries are hash-chained as the existing audit mechanism does (R4), and the trace names the same `execution_digest` the module echoes in its result. +Two kinds of recording are kept apart. The evidence that makes an execution safe (the +reservation, the write-ahead record, the transitions above) is durable and fail-closed. Richer +or analytic processing of the trace (display, aggregation, export) may stay optional and +best-effort. In the Relay at `55a899f` the cabinet's ProofTrace recorder is optional (`nil` +skips it, `core/cabinet/service.go:99`, `:105-106`) and runs once, after all steps have run, +with a failure logged and the workflow continuing (`:299-307`): a post-hoc, best-effort +record, which is not what the reservation and the write-ahead record need to be. + ## Entry points `execute` is the one with a plan and a digest. `destroy` and `invoke` are separate entry @@ -145,6 +177,9 @@ means zero `sign` and zero `actuate` calls at the provider seam. | Module refused | the module refuses the request in its own gates (zero sends) | RESERVED returns to ISSUED; the approval is still usable within its window | | Changed module artifact | the module digest differs from the one verified when the approval was bound | the approval is not usable; no provider request | | Destroy or invoke without approval | `destroy` or `invoke` called directly | refused, no provider request | +| A different request under a valid approval | a reserved, approved execution; the guest sends another method, target, body or step than the approved one | refused, no provider request | +| The host function called directly | the guest calls `cic-flow.actuate` or `cic-flow.sign` outside any reserved, approved execution context | refused, no provider request | +| Durable record cannot be written | the store fails at the reservation, and again at a write-ahead record | no provider request in either case | | Trace shape | any execution, including an UNKNOWN one | the five kinds are present where they apply and distinguishable; an observation is not labelled as proven state | ## What this does not claim