From fdd153146c7fffb9217373c793c757f543c67369 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 12:47:25 +0000 Subject: [PATCH 01/14] docs: add "Records stay home" explanation pilot page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the trust-spine pilot explanation at explanation/records-stay-home.mdx — the quality exemplar for the documentation effort. It explains how an institution proves facts from registries it already holds without the records leaving: what stays inside the boundary, what crosses it, the three disclosure modes (value / predicate / redacted), and an honest statement of what the design does and does not guarantee. Resolves the shippable frontmatter TODO by setting owner to the registry-docs area (matching sibling explanation pages) and quotes last_reviewed for house-style consistency. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01Gfv6Eurtn4CzfnxLpNL2gP Signed-off-by: Claude --- .../docs/explanation/records-stay-home.mdx | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 docs/site/src/content/docs/explanation/records-stay-home.mdx diff --git a/docs/site/src/content/docs/explanation/records-stay-home.mdx b/docs/site/src/content/docs/explanation/records-stay-home.mdx new file mode 100644 index 00000000..b9c5c05b --- /dev/null +++ b/docs/site/src/content/docs/explanation/records-stay-home.mdx @@ -0,0 +1,194 @@ +--- +title: Records stay home +description: How an institution proves facts from registries it already holds — without the records leaving. +status: draft +owner: registry-docs +source_repos: + - registry-stack +last_reviewed: "2026-06-28" +doc_type: explanation +locale: en +standards_referenced: [] +--- + +An institution that runs a civil registry, a social-protection database, or a health +registry already holds the records it needs. Registry Stack lets it **answer questions +about those records** — *is this person alive? is this household eligible?* — and return +a result another system can trust, while the records themselves are **read where they +already live, never written back, and never handed over**. + +This page explains what that means in practice: what stays inside the institution's +boundary, what crosses it, and — equally important — what the design does and does not +guarantee. + +## A question goes in, an answer comes out + +The mental model is one sentence: **a scoped question crosses into the institution, the +record is read in place, and only a computed answer crosses back out.** + +A caller never sends the value it is asking about and never receives the underlying +record. It sends a subject identifier and the id of a *claim* — a single, pre-modelled +question — and receives one of a few narrow shapes of answer: a yes/no, a single value, a +machine-readable evaluation result, or a credential the subject can carry in a wallet. The source row that the +answer was computed from stays behind. + +## The boundary + +```mermaid +flowchart LR + subgraph inst["Institution — data stays here"] + src[("Source registry\nCSV · XLSX · Parquet · PostgreSQL")] + relay["Registry Relay\nprotected read API"] + notary["Registry Notary\nevaluate · disclose · issue"] + key>"Signing key\n(private half never leaves)"] + audit[("Audit log")] + src -- read in place --> relay + relay -- governed read --> notary + notary -. records .-> audit + key -. signs .-> notary + end + caller["Caller / verifier"] + holder(["Subject / wallet"]) + caller == "request: subject id + claim id + scope" ==> notary + notary == "answer: yes/no · value · evaluation result" ==> caller + notary == "holder-bound credential" ==> holder + + classDef inside fill:#eef,stroke:#334,stroke-width:1px; + classDef outside fill:#f7f7f7,stroke:#777,stroke-dasharray:3 3; + class src,relay,notary,key,audit inside; + class caller,holder outside; +``` + +*A request and an answer cross the boundary. The source record does not.* Registry Relay +turns an existing file or database table into a read-only, access-controlled API without +replacing the source. Registry Notary evaluates one modelled question against that source +and returns a shaped result; it is the only component that evaluates claims, applies +disclosure policy, and issues credentials. + +## What stays home + +- **Source data is read in place.** Relay reads sources as batch snapshots or table scans; + there is no write-back to the source registry, and runtime services expose no + data-mutation routes. The source keeps running as it always has. +- **Storage internals stay private.** The paths, table names, and backend credentials that + point at the source live in the service's runtime configuration, decided at startup. They + are never part of the public API surface, and never part of a portable metadata file that + gets distributed. +- **The institution keeps custody.** The design premise is *distributed custody*: each + authority retains control of its own registry data, and the stack does not aggregate + records into a central system. It provides the exchange surface, not a data lake. +- **Private signing keys never leave the issuer.** The institution publishes the *public* + half of its signing key so anyone can verify a result; the private half stays inside. + +## What crosses the boundary + +Only a computed answer crosses out — never the source row. The answer takes one of a few +shapes: + +- **A yes/no** — only the true/false satisfaction of the modelled rule. +- **A single value** — the evaluated value itself, when the claim's disclosure mode is + `value` (this returns the full value). +- **A machine-readable evaluation result** — a claim-result document carrying *provenance + metadata*: which evaluation produced it, under which policy, across how many sources. This + provenance lets a receiving system trace the result; it is not a cryptographic signature. +- **A holder-bound credential** — an SD-JWT VC the subject can store in a wallet and present + later. Unlike the plain result, the credential is cryptographically verifiable against the + issuer's published keys. + +Across a federation boundary — one institution's Notary asking another's — what crosses is +a scoped, signed evaluation result, never a credential. + +## How much an answer reveals: the three disclosure modes + +Every claim carries a **disclosure mode** that fixes how much of the answer the caller +receives. There are exactly three: + +| Mode | Discloses | Withholds | +|------|-----------|-----------| +| `value` | the full evaluated value | nothing about the value | +| `predicate` | only the true/false satisfaction | the underlying value | +| `redacted` | neither — the result carries no value **and** no yes/no | the value *and* the outcome | + +The mode is policy-bound, not caller's choice: a claim defines an `allowed` set and a +`default`, the service refuses a mode outside the allowed set, and every result records +which mode was applied. A privacy-sensitive claim is expected to default to the +least-revealing mode that still answers the question. + +This is the mechanism behind "prove a fact without sharing the record". To check whether a +person has a registered record, model the question as an *existence* rule and disclose it +as a `predicate`: the caller learns `true` or `false`, and the row never crosses the +boundary. To check eligibility without exposing an income figure, derive the decision with +an expression rule and disclose the eligibility boolean as a `predicate`; the income value +stays home. + +## Why the answer is not the record + +A credential is not a copy of the record. It is an **SD-JWT VC**: the signed body carries a +SHA-256 *digest* of each selectively disclosable field rather than the field value, so a +field the holder does not present stays hidden. It is **holder-bound** — tied to the +holder's key, and not presentable without the matching private key — and the holder chooses +which fields to reveal to which verifier. Anyone can verify it against the issuer's +published public keys, served without authentication so a verifier needs no credential of +its own. The issued credential carries no full record payload. + +## How the boundary is enforced + +The "stays home" property rests on a few enforced rules, covered in depth in the Trust & +Security material: + +- **Scope-before-source, deny-by-default.** A service checks the caller's scope *before* it + reads any source or evaluates any claim, and does not widen a caller's reach at request + time beyond what its configuration grants. Only liveness probes and the public verification + keys are reachable without authentication; anything that touches a record or a claim + requires it. +- **A permit, or a closed door.** On a governed read, the policy decision point must return + a permit before data is returned; a denial fails closed with a stable reason rather than + falling back to an ungoverned read. +- **Every person-level request is audited.** An audit record captures at least the caller, + the scopes exercised, a request id, and the declared purpose where one was supplied. A + deployment can run audit fail-closed, so a request whose audit record cannot be written + does not return success. + +## What this guarantees — and what it does not + +"Records stay home" is a precise, narrow promise. Reading it as more than it is would be a +mistake, so the limits are stated plainly here. + +- **It is not "data never moves" and not "air-gapped".** The promise is *read-in-place, no + write-back, retained custody*. Authorized, minimized answers do leave the boundary by + design — that is the point of the system. +- **Minimization is modelled, not automatic.** `value` mode discloses the full value. A + claim reveals only what its author configured it to reveal; least disclosure is a design + choice the claim makes, not a property the stack imposes on every answer. +- **Correctness depends on the source.** Notary reports what the configured source says; it + does not independently vouch for whether the source is correct or current. +- **A plain result is provenance-tagged, not signed.** The everyday evaluation response + carries provenance metadata, not a cryptographic signature. Cryptographic verifiability + comes from the SD-JWT VC credential and the signed federation result — a receiving system + that must verify an answer cryptographically uses the credential, not the default response. +- **Matching is only as strict as it is configured.** Notary resolves a subject through its + configured matching policy and does not independently verify identity beyond that. By + default a matching failure collapses to a single public reason, so the lookup surface + cannot be used as an existence oracle. +- **This is not zero-knowledge.** A `predicate` answer is a policy-enforced boolean computed + inside the service; SD-JWT selective disclosure is digest omission. Neither is a + zero-knowledge proof, and the documentation should not imply one. +- **No revocation or erasure flow is defined.** This version specifies issuance, + disclosure, presentation, and verification, but not credential revocation or + data-subject erasure. A key rotated out may remain published so existing results stay + verifiable — that is not a revocation mechanism. +- **Several guarantees are the operator's to provide.** Network egress limits, key custody, + tenant isolation, audit retention, and transport security are supplied by the deployment, + not guaranteed by the stack. +- **The model is specified in draft.** The behaviour above is defined in the `RS-*` + specifications, which are still drafts and may change. Alignment with an external standard + is not a claim of conformance to it or of legal compliance. + +## Related + +- The security model and protocol contracts: [RS-SEC-G](/spec/rs-sec-g/), + [RS-PR-RELAY](/spec/rs-pr-relay/), [RS-PR-NOTARY](/spec/rs-pr-notary/), + [RS-DM-CLAIM](/spec/rs-dm-claim/) +- Evidence issuance, end to end *(explanation)* +- Disclosure and minimization in depth *(Trust & Security)* +- The threat model and security posture *(Trust & Security)* From 077dcb043d43a859293208b14bbb11ba8c40a78e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 13:14:28 +0000 Subject: [PATCH 02/14] docs(records-stay-home): fix archive links and tighten security claims Make the Related links archive-safe (../../spec/... instead of root- relative /spec/...), unblocking the check:links:built CI gate that rejects archived pages linking outside their version. Tighten two Tier-C security claims to match RS-SEC-G and the page's own honesty thesis: - the published public key verifies a signed credential or signed result, not "a result" in general (default results are provenance- tagged, not signed); - describe the unauthenticated surface accurately (liveness/readiness probes, public verification keys, credential-issuance discovery metadata) while keeping the strong rule that anything touching a record or claim requires authentication. Declare the SD-JWT VC standard in standards_referenced. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01Gfv6Eurtn4CzfnxLpNL2gP Signed-off-by: Claude --- .../docs/explanation/records-stay-home.mdx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/site/src/content/docs/explanation/records-stay-home.mdx b/docs/site/src/content/docs/explanation/records-stay-home.mdx index b9c5c05b..8696946c 100644 --- a/docs/site/src/content/docs/explanation/records-stay-home.mdx +++ b/docs/site/src/content/docs/explanation/records-stay-home.mdx @@ -8,7 +8,8 @@ source_repos: last_reviewed: "2026-06-28" doc_type: explanation locale: en -standards_referenced: [] +standards_referenced: + - sd-jwt-vc --- An institution that runs a civil registry, a social-protection database, or a health @@ -78,7 +79,8 @@ disclosure policy, and issues credentials. authority retains control of its own registry data, and the stack does not aggregate records into a central system. It provides the exchange surface, not a data lake. - **Private signing keys never leave the issuer.** The institution publishes the *public* - half of its signing key so anyone can verify a result; the private half stays inside. + half of its signing key so anyone can verify a signed credential or signed result; the + private half stays inside. ## What crosses the boundary @@ -138,9 +140,10 @@ Security material: - **Scope-before-source, deny-by-default.** A service checks the caller's scope *before* it reads any source or evaluates any claim, and does not widen a caller's reach at request - time beyond what its configuration grants. Only liveness probes and the public verification - keys are reachable without authentication; anything that touches a record or a claim - requires it. + time beyond what its configuration grants. Anything that touches a record or a claim + requires authentication; the routes reachable without it are limited to operational and + discovery surfaces: liveness and readiness probes, the public verification keys, and + credential-issuance discovery metadata. - **A permit, or a closed door.** On a governed read, the policy decision point must return a permit before data is returned; a denial fails closed with a stable reason rather than falling back to an ungoverned read. @@ -186,9 +189,9 @@ mistake, so the limits are stated plainly here. ## Related -- The security model and protocol contracts: [RS-SEC-G](/spec/rs-sec-g/), - [RS-PR-RELAY](/spec/rs-pr-relay/), [RS-PR-NOTARY](/spec/rs-pr-notary/), - [RS-DM-CLAIM](/spec/rs-dm-claim/) +- The security model and protocol contracts: [RS-SEC-G](../../spec/rs-sec-g/), + [RS-PR-RELAY](../../spec/rs-pr-relay/), [RS-PR-NOTARY](../../spec/rs-pr-notary/), + [RS-DM-CLAIM](../../spec/rs-dm-claim/) - Evidence issuance, end to end *(explanation)* - Disclosure and minimization in depth *(Trust & Security)* - The threat model and security posture *(Trust & Security)* From a8d2c4d2a10428c3965b2eb8f6354cdbc7f15b29 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 13:43:03 +0000 Subject: [PATCH 03/14] docs(records-stay-home): correct trust claims against spec and code, clear em dashes Five load-bearing claims are corrected to match the RS-* specifications (the specification oracle) and the Rust implementation, each verified against a REQ-ID or source line: - Scope the headline promise: records are "never returned as the answer" rather than "never handed over". Registry Relay is itself a scoped, authenticated read API that returns source records to authorized callers (REQ-PR-RELAY-006/008), so the stack-wide absolute was wrong; the read-in-place, no-write-back, retained-custody promise holds. - Holder binding is profile-conditional, not unconditional. The default binding mode is `none`, which issues an unbound credential, and shipped notary profiles use it; selective disclosure holds regardless. - The disclosure mode is policy-bound but caller-requestable: a caller may request a mode, which policy then bounds to the claim's `allowed` set, applying the `default` when none is requested (REQ-PR-NOTARY-009). - The existence-oracle protection is scoped to the "matching surface", the spec's term (REQ-DM-CLAIM-005); "lookup surface" over-generalized and contradicted the page's own exists-as-predicate example. - Revocation is framed accurately: the specs define no revocation, credential status, or erasure, while the implementation ships an optional, default-disabled credential-status surface (IETF Token Status List) that an operator can enable; erasure is absent everywhere. Also replace 17 em dashes with house-style punctuation so the exemplar satisfies RegistryDocs.EmDash, including the rendered diagram label and the disclosure table. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01Gfv6Eurtn4CzfnxLpNL2gP Signed-off-by: Claude --- .../docs/explanation/records-stay-home.mdx | 62 ++++++++++--------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/docs/site/src/content/docs/explanation/records-stay-home.mdx b/docs/site/src/content/docs/explanation/records-stay-home.mdx index 8696946c..e76367a9 100644 --- a/docs/site/src/content/docs/explanation/records-stay-home.mdx +++ b/docs/site/src/content/docs/explanation/records-stay-home.mdx @@ -1,6 +1,6 @@ --- title: Records stay home -description: How an institution proves facts from registries it already holds — without the records leaving. +description: How an institution proves facts from registries it already holds, without the records leaving. status: draft owner: registry-docs source_repos: @@ -14,12 +14,12 @@ standards_referenced: An institution that runs a civil registry, a social-protection database, or a health registry already holds the records it needs. Registry Stack lets it **answer questions -about those records** — *is this person alive? is this household eligible?* — and return +about those records** (*is this person alive? is this household eligible?*) and return a result another system can trust, while the records themselves are **read where they -already live, never written back, and never handed over**. +already live, never written back, and never returned as the answer**. This page explains what that means in practice: what stays inside the institution's -boundary, what crosses it, and — equally important — what the design does and does not +boundary, what crosses it, and, equally important, what the design does and does not guarantee. ## A question goes in, an answer comes out @@ -28,8 +28,8 @@ The mental model is one sentence: **a scoped question crosses into the instituti record is read in place, and only a computed answer crosses back out.** A caller never sends the value it is asking about and never receives the underlying -record. It sends a subject identifier and the id of a *claim* — a single, pre-modelled -question — and receives one of a few narrow shapes of answer: a yes/no, a single value, a +record. It sends a subject identifier and the id of a *claim* (a single, pre-modelled +question) and receives one of a few narrow shapes of answer: a yes/no, a single value, a machine-readable evaluation result, or a credential the subject can carry in a wallet. The source row that the answer was computed from stays behind. @@ -37,7 +37,7 @@ answer was computed from stays behind. ```mermaid flowchart LR - subgraph inst["Institution — data stays here"] + subgraph inst["Institution: data stays here"] src[("Source registry\nCSV · XLSX · Parquet · PostgreSQL")] relay["Registry Relay\nprotected read API"] notary["Registry Notary\nevaluate · disclose · issue"] @@ -84,20 +84,20 @@ disclosure policy, and issues credentials. ## What crosses the boundary -Only a computed answer crosses out — never the source row. The answer takes one of a few +Only a computed answer crosses out, never the source row. The answer takes one of a few shapes: -- **A yes/no** — only the true/false satisfaction of the modelled rule. -- **A single value** — the evaluated value itself, when the claim's disclosure mode is +- **A yes/no**: only the true/false satisfaction of the modelled rule. +- **A single value**: the evaluated value itself, when the claim's disclosure mode is `value` (this returns the full value). -- **A machine-readable evaluation result** — a claim-result document carrying *provenance +- **A machine-readable evaluation result**: a claim-result document carrying *provenance metadata*: which evaluation produced it, under which policy, across how many sources. This provenance lets a receiving system trace the result; it is not a cryptographic signature. -- **A holder-bound credential** — an SD-JWT VC the subject can store in a wallet and present +- **A holder-bound credential**: an SD-JWT VC the subject can store in a wallet and present later. Unlike the plain result, the credential is cryptographically verifiable against the issuer's published keys. -Across a federation boundary — one institution's Notary asking another's — what crosses is +Across a federation boundary (one institution's Notary asking another's) what crosses is a scoped, signed evaluation result, never a credential. ## How much an answer reveals: the three disclosure modes @@ -109,11 +109,12 @@ receives. There are exactly three: |------|-----------|-----------| | `value` | the full evaluated value | nothing about the value | | `predicate` | only the true/false satisfaction | the underlying value | -| `redacted` | neither — the result carries no value **and** no yes/no | the value *and* the outcome | +| `redacted` | neither: the result carries no value **and** no yes/no | the value *and* the outcome | -The mode is policy-bound, not caller's choice: a claim defines an `allowed` set and a -`default`, the service refuses a mode outside the allowed set, and every result records -which mode was applied. A privacy-sensitive claim is expected to default to the +The mode is policy-bound: a caller may request a mode, but a claim defines an `allowed` +set and a `default`, the service refuses a requested mode outside the allowed set and +applies the default when the caller requests none, and every result records which mode was +applied. A privacy-sensitive claim is expected to default to the least-revealing mode that still answers the question. This is the mechanism behind "prove a fact without sharing the record". To check whether a @@ -127,9 +128,10 @@ stays home. A credential is not a copy of the record. It is an **SD-JWT VC**: the signed body carries a SHA-256 *digest* of each selectively disclosable field rather than the field value, so a -field the holder does not present stays hidden. It is **holder-bound** — tied to the -holder's key, and not presentable without the matching private key — and the holder chooses -which fields to reveal to which verifier. Anyone can verify it against the issuer's +field the holder does not present stays hidden. Holder binding is set by the credential profile. A holder-bound profile ties the credential +to the holder's key so it is not presentable without the matching private key; the default +`none` mode issues an unbound credential. Either way, the holder chooses which fields to +reveal to which verifier. Anyone can verify it against the issuer's published public keys, served without authentication so a verifier needs no credential of its own. The issued credential carries no full record payload. @@ -152,14 +154,14 @@ Security material: deployment can run audit fail-closed, so a request whose audit record cannot be written does not return success. -## What this guarantees — and what it does not +## What this guarantees, and what it does not "Records stay home" is a precise, narrow promise. Reading it as more than it is would be a mistake, so the limits are stated plainly here. - **It is not "data never moves" and not "air-gapped".** The promise is *read-in-place, no write-back, retained custody*. Authorized, minimized answers do leave the boundary by - design — that is the point of the system. + design: that is the point of the system. - **Minimization is modelled, not automatic.** `value` mode discloses the full value. A claim reveals only what its author configured it to reveal; least disclosure is a design choice the claim makes, not a property the stack imposes on every answer. @@ -167,19 +169,23 @@ mistake, so the limits are stated plainly here. does not independently vouch for whether the source is correct or current. - **A plain result is provenance-tagged, not signed.** The everyday evaluation response carries provenance metadata, not a cryptographic signature. Cryptographic verifiability - comes from the SD-JWT VC credential and the signed federation result — a receiving system + comes from the SD-JWT VC credential and the signed federation result. A receiving system that must verify an answer cryptographically uses the credential, not the default response. - **Matching is only as strict as it is configured.** Notary resolves a subject through its configured matching policy and does not independently verify identity beyond that. By - default a matching failure collapses to a single public reason, so the lookup surface + default a matching failure collapses to a single public reason, so the matching surface cannot be used as an existence oracle. - **This is not zero-knowledge.** A `predicate` answer is a policy-enforced boolean computed inside the service; SD-JWT selective disclosure is digest omission. Neither is a zero-knowledge proof, and the documentation should not imply one. -- **No revocation or erasure flow is defined.** This version specifies issuance, - disclosure, presentation, and verification, but not credential revocation or - data-subject erasure. A key rotated out may remain published so existing results stay - verifiable — that is not a revocation mechanism. +- **No revocation or erasure flow is specified; revocation is an optional operator surface.** + The `RS-*` specifications define no credential revocation, credential status, or + data-subject erasure. The implementation ships an optional credential-status surface (an + IETF Token Status List with states `valid`, `suspended`, `revoked`, and `expired`), + disabled by default and enabled per deployment; with it enabled, an admin-scoped route can + move a credential to `revoked`. Data-subject erasure is absent from both the specifications + and the implementation. A key rotated out may remain published so existing results stay + verifiable; that is not a revocation mechanism. - **Several guarantees are the operator's to provide.** Network egress limits, key custody, tenant isolation, audit retention, and transport security are supplied by the deployment, not guaranteed by the stack. From 9b774ae28934fd426f2cc94cea0ed71dc593545c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 14:00:27 +0000 Subject: [PATCH 04/14] build(docs): extend frontmatter audience vocabulary Add `auditor` and `decision-maker` to the enforced `audience` set in check-doc-frontmatter.mjs (union, not replacement), so Decide and Trust & Security pages can declare their reader audience without failing the frontmatter gate. `audience` stays optional and is validated only when present, so existing pages are unaffected. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01Gfv6Eurtn4CzfnxLpNL2gP Signed-off-by: Claude --- docs/site/scripts/check-doc-frontmatter.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/site/scripts/check-doc-frontmatter.mjs b/docs/site/scripts/check-doc-frontmatter.mjs index fb316562..529151b0 100644 --- a/docs/site/scripts/check-doc-frontmatter.mjs +++ b/docs/site/scripts/check-doc-frontmatter.mjs @@ -25,7 +25,7 @@ const validEvidence = new Set(['aspirational', 'partial', 'verified']); // optional and only validated when present; layer enumerates the stack's real // layers, audience the reader roles. const validLayer = new Set(['metadata', 'consultation', 'evaluation', 'credential', 'federation', 'administration', 'operations']); -const validAudience = new Set(['integrator', 'operator', 'maintainer', 'specification editor', 'tooling']); +const validAudience = new Set(['integrator', 'operator', 'maintainer', 'specification editor', 'tooling', 'auditor', 'decision-maker']); const docIdPattern = /^RS-[A-Z0-9]+(-[A-Z0-9]+)*$/; const seenDocIds = new Map(); const standardsRegister = YAML.parse(await readFile('src/data/standards.yaml', 'utf8')); From 7be8ecaf1c82ca94f3c512354a28952cc53ae9cd Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 14:10:54 +0000 Subject: [PATCH 05/14] docs(records-stay-home): scope claims to runtime behavior, show Relay read surface Address a third review pass plus the approved boundary-diagram gap. Each change is verified against the RS-* specifications and the Rust code. - Request inputs: a caller sends the claim id and the inputs the claim's matching policy needs (a subject identifier, and where required, target or requester attributes, further identifiers, or relationship attributes), not only a subject id (RS-DM-CLAIM Section 3, REQ-DM-CLAIM-004). - Row shape: recast "never the source row" as a modelling discipline, since object/array claims and CEL extracts can return row-shaped data under `value` disclosure (RS-DM-CLAIM; runtime value path). - value mode: note that policy redaction removes object fields even under `value`, and that `value` is not constrained to a scalar. - Disclosure downgrade: describe the `downgrade` policy (deny refuses; default/redacted substitute an allowed fallback) instead of unconditional refusal (REQ-PR-NOTARY-009 plus the runtime downgrade path). - Audit: describe the captured field as the scope or claim the request exercised, accurate for both Relay (scope) and Notary (claim). The boundary diagram and caption now show Registry Relay's own external, scoped, audited record-consultation surface (REQ-PR-RELAY-006/008) alongside the Notary answer flow, keeping the read-in-place, no-write-back, distributed-custody thesis intact. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01Gfv6Eurtn4CzfnxLpNL2gP Signed-off-by: Claude --- .../docs/explanation/records-stay-home.mdx | 53 ++++++++++++------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/docs/site/src/content/docs/explanation/records-stay-home.mdx b/docs/site/src/content/docs/explanation/records-stay-home.mdx index e76367a9..b812091f 100644 --- a/docs/site/src/content/docs/explanation/records-stay-home.mdx +++ b/docs/site/src/content/docs/explanation/records-stay-home.mdx @@ -28,8 +28,10 @@ The mental model is one sentence: **a scoped question crosses into the instituti record is read in place, and only a computed answer crosses back out.** A caller never sends the value it is asking about and never receives the underlying -record. It sends a subject identifier and the id of a *claim* (a single, pre-modelled -question) and receives one of a few narrow shapes of answer: a yes/no, a single value, a +record. It sends the id of a *claim* (a single, pre-modelled question) and the inputs that +claim needs to resolve the subject: a subject identifier, plus, where the claim's matching +policy requires them, target or requester attributes, further identifiers, or relationship +attributes. It receives one of a few narrow shapes of answer: a yes/no, a single value, a machine-readable evaluation result, or a credential the subject can carry in a wallet. The source row that the answer was computed from stays behind. @@ -39,20 +41,23 @@ answer was computed from stays behind. flowchart LR subgraph inst["Institution: data stays here"] src[("Source registry\nCSV · XLSX · Parquet · PostgreSQL")] - relay["Registry Relay\nprotected read API"] + relay["Registry Relay\nprotected read API · scoped consultation"] notary["Registry Notary\nevaluate · disclose · issue"] key>"Signing key\n(private half never leaves)"] audit[("Audit log")] src -- read in place --> relay relay -- governed read --> notary notary -. records .-> audit + relay -. records .-> audit key -. signs .-> notary end caller["Caller / verifier"] holder(["Subject / wallet"]) - caller == "request: subject id + claim id + scope" ==> notary + caller == "request: claim id + subject inputs + scope" ==> notary notary == "answer: yes/no · value · evaluation result" ==> caller notary == "holder-bound credential" ==> holder + caller == "scoped record read · dataset:rows" ==> relay + relay == "authorized source records" ==> caller classDef inside fill:#eef,stroke:#334,stroke-width:1px; classDef outside fill:#f7f7f7,stroke:#777,stroke-dasharray:3 3; @@ -60,11 +65,14 @@ flowchart LR class caller,holder outside; ``` -*A request and an answer cross the boundary. The source record does not.* Registry Relay +*Two surfaces cross the boundary, both governed.* Registry Notary evaluates one modelled +question against a source and returns a shaped, minimized answer; it is the only component +that evaluates claims, applies disclosure policy, and issues credentials. Registry Relay turns an existing file or database table into a read-only, access-controlled API without -replacing the source. Registry Notary evaluates one modelled question against that source -and returns a shaped result; it is the only component that evaluates claims, applies -disclosure policy, and issues credentials. +replacing the source, and publishes restricted, scoped consultation routes that return +source records to authorized callers holding the `dataset:rows` permission. Notary is the +strongest minimization; Relay record reads are scoped and audited, not open data. Either +way the source is read in place, never written back, and never aggregated centrally. ## What stays home @@ -84,12 +92,15 @@ disclosure policy, and issues credentials. ## What crosses the boundary -Only a computed answer crosses out, never the source row. The answer takes one of a few -shapes: +What crosses depends on the surface. Registry Relay returns scoped records to an +authorized caller: a governed, audited read under the `dataset:rows` permission, never a +bulk export. Registry Notary returns the answer a rule computes rather than the source row; +keeping that answer narrow is a modelling discipline, since a well-modelled claim returns +one decision or one extracted value. A Notary answer takes one of a few shapes: - **A yes/no**: only the true/false satisfaction of the modelled rule. - **A single value**: the evaluated value itself, when the claim's disclosure mode is - `value` (this returns the full value). + `value`. - **A machine-readable evaluation result**: a claim-result document carrying *provenance metadata*: which evaluation produced it, under which policy, across how many sources. This provenance lets a receiving system trace the result; it is not a cryptographic signature. @@ -107,14 +118,15 @@ receives. There are exactly three: | Mode | Discloses | Withholds | |------|-----------|-----------| -| `value` | the full evaluated value | nothing about the value | +| `value` | the evaluated value, less any object fields the policy redacts | nothing beyond policy-redacted object fields | | `predicate` | only the true/false satisfaction | the underlying value | | `redacted` | neither: the result carries no value **and** no yes/no | the value *and* the outcome | The mode is policy-bound: a caller may request a mode, but a claim defines an `allowed` -set and a `default`, the service refuses a requested mode outside the allowed set and -applies the default when the caller requests none, and every result records which mode was -applied. A privacy-sensitive claim is expected to default to the +set, a `default`, and a `downgrade` policy. Under the default `deny` downgrade the service +refuses a requested mode outside the allowed set; a `default` or `redacted` downgrade +instead substitutes that fallback mode when the fallback is itself allowed. The default mode +applies when the caller requests none, and every result records which mode was applied. A privacy-sensitive claim is expected to default to the least-revealing mode that still answers the question. This is the mechanism behind "prove a fact without sharing the record". To check whether a @@ -150,7 +162,8 @@ Security material: a permit before data is returned; a denial fails closed with a stable reason rather than falling back to an ungoverned read. - **Every person-level request is audited.** An audit record captures at least the caller, - the scopes exercised, a request id, and the declared purpose where one was supplied. A + a request id, the scope or claim the request exercised, and the declared purpose where + one was supplied. A deployment can run audit fail-closed, so a request whose audit record cannot be written does not return success. @@ -162,9 +175,11 @@ mistake, so the limits are stated plainly here. - **It is not "data never moves" and not "air-gapped".** The promise is *read-in-place, no write-back, retained custody*. Authorized, minimized answers do leave the boundary by design: that is the point of the system. -- **Minimization is modelled, not automatic.** `value` mode discloses the full value. A - claim reveals only what its author configured it to reveal; least disclosure is a design - choice the claim makes, not a property the stack imposes on every answer. +- **Minimization is modelled, not automatic.** `value` mode discloses the evaluated value, + less any object fields the matching policy marks for redaction; it is not constrained to a + scalar, so a claim modelled to return an object or extracted record returns one. A claim + reveals only what its author configured it to reveal; least disclosure is a design choice + the claim makes, not a property the stack imposes on every answer. - **Correctness depends on the source.** Notary reports what the configured source says; it does not independently vouch for whether the source is correct or current. - **A plain result is provenance-tagged, not signed.** The everyday evaluation response From 1fd28a8af2d5d470d2a8baf90c76ec1c65290976 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 14:25:31 +0000 Subject: [PATCH 06/14] docs(records-stay-home): scope Relay read and SD-JWT disclosure granularity Two precision fixes from a fourth review pass, each verified against the code. - Relay `dataset:rows` reads are paginated: the entity collection route returns a cursor, so an authorized caller can iterate reachable rows. Describe the read as governed, audited, paginated, and bounded by scope, filters, and limits rather than "never a bulk export". - SD-JWT selective disclosure is per claim output: the issuer builds one disclosure per claim result (or projection output), so an object-valued output is revealed as a unit, not field by field within the object. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01Gfv6Eurtn4CzfnxLpNL2gP Signed-off-by: Claude --- .../src/content/docs/explanation/records-stay-home.mdx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/site/src/content/docs/explanation/records-stay-home.mdx b/docs/site/src/content/docs/explanation/records-stay-home.mdx index b812091f..9b5c7243 100644 --- a/docs/site/src/content/docs/explanation/records-stay-home.mdx +++ b/docs/site/src/content/docs/explanation/records-stay-home.mdx @@ -93,8 +93,9 @@ way the source is read in place, never written back, and never aggregated centra ## What crosses the boundary What crosses depends on the surface. Registry Relay returns scoped records to an -authorized caller: a governed, audited read under the `dataset:rows` permission, never a -bulk export. Registry Notary returns the answer a rule computes rather than the source row; +authorized caller: a governed, audited, paginated read bounded by the caller's `dataset:rows` +scope and the dataset's configured filters and limits. Registry Notary returns the answer a +rule computes rather than the source row; keeping that answer narrow is a modelling discipline, since a well-modelled claim returns one decision or one extracted value. A Notary answer takes one of a few shapes: @@ -143,7 +144,9 @@ SHA-256 *digest* of each selectively disclosable field rather than the field val field the holder does not present stays hidden. Holder binding is set by the credential profile. A holder-bound profile ties the credential to the holder's key so it is not presentable without the matching private key; the default `none` mode issues an unbound credential. Either way, the holder chooses which fields to -reveal to which verifier. Anyone can verify it against the issuer's +reveal to which verifier. Each selectively disclosable field is a whole claim output, so an +object-valued output is revealed as a unit, not field by field within the object. Anyone can +verify it against the issuer's published public keys, served without authentication so a verifier needs no credential of its own. The issued credential carries no full record payload. From a340c561c5e5161908ffcdb5ceec4437f2941dcf Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 14:25:31 +0000 Subject: [PATCH 07/14] Revert "build(docs): extend frontmatter audience vocabulary" This reverts the audience-vocabulary change in 9b774ae. The audience set is defined in three places that must agree: the RS-TERMS specification, the Starlight Zod schema in src/content.config.ts, and this checker. Extending only the checker lets check:content accept `auditor` and `decision-maker` while the Astro build (a five-value Zod enum) and RS-TERMS still reject them, so a page using either value passes the content check but fails the build. Restore the five-value set until the vocabulary is extended across the spec, the schema, and the checker together. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01Gfv6Eurtn4CzfnxLpNL2gP Signed-off-by: Claude --- docs/site/scripts/check-doc-frontmatter.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/site/scripts/check-doc-frontmatter.mjs b/docs/site/scripts/check-doc-frontmatter.mjs index 529151b0..fb316562 100644 --- a/docs/site/scripts/check-doc-frontmatter.mjs +++ b/docs/site/scripts/check-doc-frontmatter.mjs @@ -25,7 +25,7 @@ const validEvidence = new Set(['aspirational', 'partial', 'verified']); // optional and only validated when present; layer enumerates the stack's real // layers, audience the reader roles. const validLayer = new Set(['metadata', 'consultation', 'evaluation', 'credential', 'federation', 'administration', 'operations']); -const validAudience = new Set(['integrator', 'operator', 'maintainer', 'specification editor', 'tooling', 'auditor', 'decision-maker']); +const validAudience = new Set(['integrator', 'operator', 'maintainer', 'specification editor', 'tooling']); const docIdPattern = /^RS-[A-Z0-9]+(-[A-Z0-9]+)*$/; const seenDocIds = new Map(); const standardsRegister = YAML.parse(await readFile('src/data/standards.yaml', 'utf8')); From e64122e8a63d13f15eb9c5a126440b43c3da3d79 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 14:41:31 +0000 Subject: [PATCH 08/14] docs(records-stay-home): correct row-scope name, status-list citation, predicate scope Three accuracy fixes from a fifth review pass, each verified against the specifications and code: - The record-read scope is `:rows`, not a literal `dataset:rows` (REQ-PR-RELAY-006); a literal grant satisfies no real dataset's scope check. Corrected in the diagram, the caption, and the prose. - Describe the optional credential-status surface as a status list rather than naming a formal standard the page does not list in standards_referenced. - Predicate disclosure yields a true/false only for a claim whose rule yields a boolean; the runtime leaves the outcome absent for a non-boolean value. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01Gfv6Eurtn4CzfnxLpNL2gP Signed-off-by: Claude --- .../content/docs/explanation/records-stay-home.mdx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/site/src/content/docs/explanation/records-stay-home.mdx b/docs/site/src/content/docs/explanation/records-stay-home.mdx index 9b5c7243..2425eee3 100644 --- a/docs/site/src/content/docs/explanation/records-stay-home.mdx +++ b/docs/site/src/content/docs/explanation/records-stay-home.mdx @@ -56,7 +56,7 @@ flowchart LR caller == "request: claim id + subject inputs + scope" ==> notary notary == "answer: yes/no · value · evaluation result" ==> caller notary == "holder-bound credential" ==> holder - caller == "scoped record read · dataset:rows" ==> relay + caller == "scoped record read · per-dataset row scope" ==> relay relay == "authorized source records" ==> caller classDef inside fill:#eef,stroke:#334,stroke-width:1px; @@ -70,7 +70,7 @@ question against a source and returns a shaped, minimized answer; it is the only that evaluates claims, applies disclosure policy, and issues credentials. Registry Relay turns an existing file or database table into a read-only, access-controlled API without replacing the source, and publishes restricted, scoped consultation routes that return -source records to authorized callers holding the `dataset:rows` permission. Notary is the +source records to authorized callers holding the dataset's `:rows` permission. Notary is the strongest minimization; Relay record reads are scoped and audited, not open data. Either way the source is read in place, never written back, and never aggregated centrally. @@ -93,8 +93,8 @@ way the source is read in place, never written back, and never aggregated centra ## What crosses the boundary What crosses depends on the surface. Registry Relay returns scoped records to an -authorized caller: a governed, audited, paginated read bounded by the caller's `dataset:rows` -scope and the dataset's configured filters and limits. Registry Notary returns the answer a +authorized caller: a governed, audited, paginated read bounded by the caller's per-dataset row +scope (`:rows`) and the dataset's configured filters and limits. Registry Notary returns the answer a rule computes rather than the source row; keeping that answer narrow is a modelling discipline, since a well-modelled claim returns one decision or one extracted value. A Notary answer takes one of a few shapes: @@ -120,7 +120,7 @@ receives. There are exactly three: | Mode | Discloses | Withholds | |------|-----------|-----------| | `value` | the evaluated value, less any object fields the policy redacts | nothing beyond policy-redacted object fields | -| `predicate` | only the true/false satisfaction | the underlying value | +| `predicate` | only the true/false satisfaction, for a claim whose rule yields a boolean | the underlying value | | `redacted` | neither: the result carries no value **and** no yes/no | the value *and* the outcome | The mode is policy-bound: a caller may request a mode, but a claim defines an `allowed` @@ -198,8 +198,8 @@ mistake, so the limits are stated plainly here. zero-knowledge proof, and the documentation should not imply one. - **No revocation or erasure flow is specified; revocation is an optional operator surface.** The `RS-*` specifications define no credential revocation, credential status, or - data-subject erasure. The implementation ships an optional credential-status surface (an - IETF Token Status List with states `valid`, `suspended`, `revoked`, and `expired`), + data-subject erasure. The implementation ships an optional credential-status surface (a status + list with states `valid`, `suspended`, `revoked`, and `expired`), disabled by default and enabled per deployment; with it enabled, an admin-scoped route can move a credential to `revoked`. Data-subject erasure is absent from both the specifications and the implementation. A key rotated out may remain published so existing results stay From a8270e6026fa2db76e872a6a1d61d65bcca2e3c3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 14:56:21 +0000 Subject: [PATCH 09/14] docs(records-stay-home): complete the unauthenticated route surface The "limited to" list understated what is reachable without authentication. Per is_auth_exempt_path (crates/registry-notary-server/src/standalone.rs), the middleware-exempt routes also include the OID4VCI issuance-flow routes (offer, token, nonce, callback, credential-offer), the credential-status route, and the API and credential-type metadata routes, not only liveness and readiness, the public keys, and issuer discovery metadata. None return a record or claim result on their own, and the issuance and status routes run their own flow checks. Reword so an auditor scoping the unauthenticated surface is not misled. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01Gfv6Eurtn4CzfnxLpNL2gP Signed-off-by: Claude --- .../src/content/docs/explanation/records-stay-home.mdx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/site/src/content/docs/explanation/records-stay-home.mdx b/docs/site/src/content/docs/explanation/records-stay-home.mdx index 2425eee3..c8d8cc7b 100644 --- a/docs/site/src/content/docs/explanation/records-stay-home.mdx +++ b/docs/site/src/content/docs/explanation/records-stay-home.mdx @@ -158,9 +158,11 @@ Security material: - **Scope-before-source, deny-by-default.** A service checks the caller's scope *before* it reads any source or evaluates any claim, and does not widen a caller's reach at request time beyond what its configuration grants. Anything that touches a record or a claim - requires authentication; the routes reachable without it are limited to operational and - discovery surfaces: liveness and readiness probes, the public verification keys, and - credential-issuance discovery metadata. + requires authentication. The routes reachable without it return no record or claim result on + their own: liveness and readiness probes, the public verification keys, the public metadata + routes (API, issuer, and credential type), and, where OID4VCI issuance or credential status + is enabled, the issuance-flow and status routes that protocol defines, which run their own + flow checks. - **A permit, or a closed door.** On a governed read, the policy decision point must return a permit before data is returned; a denial fails closed with a stable reason rather than falling back to an ungoverned read. From 6b953f84799815bf0237862d9ebdea5095a318cd Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 15:03:53 +0000 Subject: [PATCH 10/14] docs(disclosure): add "Disclosure and minimization" explanation Add explanation/disclosure-and-minimization.mdx, the in-depth companion to the "Records stay home" overview. It explains the disclosure model as a mechanism and minimization as a modelling discipline, each claim traced to an RS-* requirement or a line of Rust: - the three modes in depth: value (with object-field redaction and non-scalar values), predicate (only for a boolean-yielding rule), redacted (withholds value and outcome); - how policy binds a mode (default/allowed/downgrade; caller-requestable but bounded; the recorded mode is the effective one after forcing or downgrade); - selective disclosure in the credential at claim-output granularity (an object output is revealed whole), with profile-conditional holder binding; - disclosing non-existence (matching-failure collapse) and across a federation boundary (a signed, scoped evaluation result); - an honest limits section (not zero-knowledge, modelled not automatic, provenance not signature, alignment not conformance). Wire the overview's Related section to the new page. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01Gfv6Eurtn4CzfnxLpNL2gP Signed-off-by: Claude --- .../disclosure-and-minimization.mdx | 244 ++++++++++++++++++ .../docs/explanation/records-stay-home.mdx | 2 +- 2 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 docs/site/src/content/docs/explanation/disclosure-and-minimization.mdx diff --git a/docs/site/src/content/docs/explanation/disclosure-and-minimization.mdx b/docs/site/src/content/docs/explanation/disclosure-and-minimization.mdx new file mode 100644 index 00000000..9173e23b --- /dev/null +++ b/docs/site/src/content/docs/explanation/disclosure-and-minimization.mdx @@ -0,0 +1,244 @@ +--- +title: Disclosure and minimization +description: How Registry Notary bounds what a claim answer reveals through three disclosure modes, policy-bound mode selection, and a minimization discipline the claim author models. +status: draft +owner: registry-docs +source_repos: + - registry-stack +last_reviewed: "2026-06-28" +doc_type: explanation +locale: en +standards_referenced: + - sd-jwt-vc +--- + +The overview establishes the boundary: Registry Relay returns scoped records, Registry Notary +returns a minimized answer, and a claim carries one of three disclosure modes. This page goes +underneath that summary. It explains the disclosure model as a mechanism (what each mode +computes, how a requested mode is bounded, what a downgrade does) and the minimization +discipline as a modelling stance rather than an enforced invariant. The distinction matters: +the strength of any single answer is a property of how its claim was modelled, not a guarantee +the stack imposes on every answer. + +## Disclosure controls a result, minimization shapes a claim + +Two separate ideas are often conflated. Disclosure is a runtime control: given an evaluated +value, a disclosure mode decides what the result body carries. Minimization is a design-time +discipline: a claim author decides what a claim evaluates in the first place, and therefore the +most a disclosure mode could ever reveal. + +Minimization is modelled rather than automatic. The spec states the discipline in prose: a +claim definition describes one decision or one extracted value, and a claim that tries to +return a whole record over-collects and is hard to authorize (`rs-dm-claim.mdx`). That sentence +is guidance, not a config-load invariant. `REQ-DM-CLAIM-003` makes reading only the fields a +rule needs a `SHOULD`; its single hard `MUST` is that a binding declaring an input allow-list +(`allowed_target_inputs` or `allowed_requester_inputs`) rejects any request supplying a path +outside that list, so a binding cannot over-collect by accident. Least disclosure is likewise a +per-claim `SHOULD`: `REQ-DM-CLAIM-008` says a privacy-sensitive claim should default to the +least-revealing useful mode, while the enforced behavior is narrower (apply the claim's +`default` when the caller requests none, refuse a mode outside `allowed`). The stack does not +force a narrow read or a least-revealing default; it enforces the policy a claim declares. + +A claim does declare exactly one rule. `REQ-DM-CLAIM-006` requires one rule of one implemented +kind: `exists` (the presence of exactly one source record), `extract` (a value read from a +source field), or `cel` (a value derived from source fields or earlier claim results through a +hardened expression). The rule is the single decision at the center of a claim. That single-rule +shape is what gives a claim a chance to be minimal; whether it is minimal depends on what the +rule evaluates and which mode discloses it. + +## The three modes in depth + +Registry Notary defines exactly three disclosure modes, `value`, `predicate`, and `redacted` +(Section 5 of `rs-pr-notary.mdx`; the `DisclosureProfile` enum in `registry-notary-core/src/model.rs`). Each +mode is a projection over the evaluated value, and each carries a distinct privacy contract. + +### `value`: the full value, minus policy-marked fields + +The `value` mode returns the evaluated value whole. In the runtime, with no field redaction in +play, the result is the evaluated value cloned in full (`runtime.rs`), not a coerced scalar. A +claim's declared value type may be an object or an array, and the runtime accepts those types, +so a broadly modelled `extract`, `cel`, or object claim returns row-shaped, record-like data +under `value`. This is precisely the over-collection risk the modelling discipline warns about: +`value` mode is not a safeguard against record-shaped disclosure, it permits it when the claim +is modelled that way. + +Object-field redaction narrows `value` mode, but only under tight preconditions. It applies +when the claim value type is `object`, the redaction set is non-empty, and every entry is a +non-empty top-level field name with no dots or brackets and not the literal `value` +(`runtime.rs`). When those hold, the matching policy's named fields are removed from the +returned object and the rest of the object is returned. If a field marked for redaction is +absent from the object, the result is refused (`DisclosureNotAllowed`) rather than silently +returned. Field redaction is therefore a per-field subtraction inside an object value, not a +general mechanism for trimming arbitrary structure. + +Row-level redaction on a non-object value behaves differently. When redaction fields are present +but the value does not qualify for object-field redaction, a `value` request is forced to +`redacted` before the allowed-set check runs (`runtime.rs`). The whole result collapses; there +is no partial reveal of a non-object value. + +### `predicate`: a boolean satisfaction, and only when the value is boolean + +The `predicate` mode conveys only the rule's true/false satisfaction (`rs-pr-notary.mdx` +Section 5). It is derived by interpreting the evaluated value as a boolean: the runtime projects +`result.value.as_bool()` (`runtime.rs`), which yields a value only for a JSON boolean. A +non-boolean evaluated value therefore yields an absent value under `predicate`, and the +satisfaction outcome is likewise absent. `predicate` does not threshold, coerce, or summarize a +non-boolean value; it is meaningful only for a claim whose rule yields a boolean. A claim that +needs to answer yes/no without exposing a figure must model the decision so the rule itself +produces the boolean (for example a `cel` comparison), rather than expecting `predicate` to +reduce a numeric value for it. + +`predicate` is also incompatible with a redacted-field result. When redaction fields are present +and the effective mode is `predicate`, the request is refused (`DisclosureNotAllowed`, +`runtime.rs`). Field-level redaction and predicate disclosure do not combine. + +### `redacted`: neither value nor outcome + +The `redacted` mode withholds both. `REQ-PR-NOTARY-010` requires that a redacted result not +carry the underlying source value and not convey the satisfaction outcome: the body withholds +both the value and the predicate. The runtime matches this: the disclosed value is absent, and +the `satisfied` outcome is populated only for `value` and `predicate` (by reading the value as a +boolean) and is absent for `redacted` (`runtime.rs`). A redacted answer carries no value and no +yes/no. + +## How policy binds a mode + +A claim's disclosure policy is three settings in the claim definition: a `default` mode, an +`allowed` set of modes, and a `downgrade` policy (`DisclosureConfig` in +`registry-notary-core/src/config.rs`). The defaults are the most restrictive available: the +`default` mode is `redacted`, and the sole default `allowed` entry is `redacted` +(`config.rs`). A claim that says nothing about disclosure reveals nothing. + +A caller may request a mode (the evaluation request carries an optional `disclosure` field), but +the request is bounded. When the caller requests none, the claim's `default` applies; in the +multi-claim case the runtime falls back to the `default` of the first requested claim +(`runtime.rs`). `REQ-PR-NOTARY-009` fixes the rest: Notary refuses a mode not in the claim's +`allowed` set, and applies the `default` when the caller requests none. + +The `downgrade` policy decides what happens when a requested mode is outside `allowed`. It has +exactly three variants, `deny`, `default`, and `redacted` (`DisclosureDowngrade` in +`model.rs`), and it defaults to `deny` (`config.rs`). The runtime resolves them so: + +| `downgrade` | When the requested mode is not allowed | +|------|------| +| `deny` (default) | refuse the request (`DisclosureNotAllowed`) | +| `default` | substitute the claim's `default` mode | +| `redacted` | substitute the `redacted` mode | + +A downgrade does not bypass the allowed-set rule. When the substituted fallback (`default` or +`redacted`) is itself not in the claim's `allowed` set, the request is still refused +(`runtime.rs`). The fallback is a choice among permitted modes, not an escape hatch around them. + +One consequence is worth stating directly. Every +evaluation result records the disclosure mode that was actually applied (`REQ-PR-NOTARY-008`), +and that recorded mode is the effective mode after any forcing or downgrade, not the mode the +caller requested. The view emits the effective mode (`runtime.rs`). A request that asked for +`value` and was forced to `redacted` records `redacted`. The recorded mode is the truth of what +was disclosed, not the intent of the asker. + +## Selective disclosure in the credential + +Disclosure modes govern an evaluation result. An issued credential is a distinct layer with its +own disclosure mechanism, and the two should not be conflated. The credential is an SD-JWT VC +(`typ` header `dc+sd-jwt`, signed with EdDSA over Ed25519, `REQ-PR-NOTARY-013`) that gives the +holder selective disclosure: the holder chooses which fields to reveal to which verifier. The +signed body carries a SHA-256 digest of each selectively disclosable field rather than the field +value, so an unselected field stays hidden and a holder cannot present a disclosure that was not +in the original credential (`REQ-PR-NOTARY-014`). + +The granularity of that selection is the claim output, not a field inside an output's value. The +issuer builds one Disclosure per output. In projection mode there is one Disclosure per +configured projection output, named by its `output_name`, whose value is the claim result's +value taken whole (`sd_jwt.rs`); a projection entry maps one `claim_id` to one `output_name`. In +the default, non-projection mode there is one Disclosure per claim result, named by `claim_id`, +whose value is the entire claim-result object (`sd_jwt.rs`). Either way an object-valued output +is emitted as a single Disclosure value, not split field by field. A holder who reveals an +object-valued output reveals all of it; the unit of selective disclosure is the output. + +Projection is also conditional. A projected claim whose result is unsatisfied or whose value is +null or absent is rejected (`CredentialIssuanceFailed`, `sd_jwt.rs`), so projection does not +unconditionally disclose every configured output. + +Holder binding sits alongside selective disclosure and is profile-conditional, not universal. +The default `holder_binding` mode is the literal string `none`, which issues an unbound +credential (`HolderBindingConfig` in `config.rs`). The self-attestation and OID4VCI path forces +binding: profile validation rejects any profile whose `holder_binding.mode` is not `did`, whose +`proof_of_possession` is not `required`, or whose `allowed_did_methods` is not exactly `did:jwk` +(`config.rs`). When a credential is holder-bound, `REQ-PR-NOTARY-015` names the holder's public +key as a `did:jwk` in the `cnf` claim (`did:jwk` is the only supported binding method) and +requires a fresh, audience-bound holder proof at presentation; the wallet credential endpoint +(`POST /oid4vci/credential`) must require a valid access token and a proof of possession, and +must not issue without one (`REQ-PR-NOTARY-017`). Binding is a property of the issuance path, not +of the three evaluation-disclosure modes. + +## Disclosing non-existence + +A subtle disclosure question is what a caller learns when an answer cannot be produced. A +matching failure can leak existence: a granular "subject not found" tells the caller the subject +is absent, and a granular "ambiguous" tells the caller more than one record matched. By default +that leak is closed. `REQ-DM-CLAIM-005` requires a matching failure to collapse to a single +public reason (`evidence.not_available`), with the granular reason retained only in the audit +record, so the matching surface cannot be used as an existence oracle. A deployment may disable +the collapse, but only in a controlled environment where exposing not-found, ambiguous, and +rejected outcomes to the caller is acceptable. + +In code, collapse defaults to enabled (`default_collapse_matching_errors` returns `true`, +`config.rs`) and is a per-source-binding setting, a field of the binding's matching config, not +a single deployment-wide switch (`runtime.rs` reads `binding.matching.collapse_matching_errors` +per binding). When collapse is enabled, a broad set of matching errors (source, requester, and +relationship not-found; ambiguous; insufficient-attributes; policy-rejected; and others) maps to +`MatchingEvidenceNotAvailable`, which surfaces the public code `evidence.not_available` while +retaining the granular reason as an `audit_code` (`runtime.rs`). When the binding's flag is +false, the raw error is returned unchanged. A separate helper handles the dependent-lookup +resolution stage and additionally folds `InvalidRequest` (for example a non-scalar prior source +field) into `evidence.not_available` with its specific `audit_code`, because that condition is a +function of upstream row data the caller is not authorized to observe (`runtime.rs`). + +## Disclosing across a federation boundary + +When one Registry Notary delegates an evaluation to a configured peer, what crosses is a scoped, +signed evaluation result, never a credential. A peer posts a compact signed JWT request to +`POST /federation/v1/evaluations` and receives a compact signed JWT response carrying a scoped +evaluation result; before any source read, the serving Notary verifies peer policy, replay +state, purpose, profile, and audience (`REQ-PR-NOTARY-018`). Federation is static-peer only: +peers are loaded from configuration at startup, a request from an unconfigured peer is rejected, +and dynamic discovery, shared replay storage, and federated credential issuance are out of scope +(`REQ-PR-NOTARY-019`). The federated answer is the same kind of minimized, mode-governed +evaluation result, signed for transport, rather than a portable credential. + +## Honest limits + +The disclosure model is a set of policy-enforced controls, not a cryptographic privacy proof. +The limits below are the documented honest framing, not discovered defects. + +- **This is not zero-knowledge.** A `predicate` answer is a policy-enforced boolean computed + inside the service, and SD-JWT selective disclosure is digest omission (the body carries a + SHA-256 digest of each disclosable field, `sd_jwt.rs`). Neither is a zero-knowledge proof, and + the documentation should not imply one (`records-stay-home.mdx`). +- **Minimization is modelled, not automatic.** A claim reveals only what its author configured. + `value` mode discloses the evaluated value less any object fields the policy redacts and is not + constrained to a scalar, so a claim modelled to return an object or extracted record returns + one. The single-rule shape and the least-revealing-default guidance are a modelling stance the + author adopts, not a property imposed on every answer. +- **A plain result is provenance-tagged, not signed.** The everyday evaluation response carries + provenance metadata using the `registry-notary-claim-provenance/v1` shape (`REQ-PR-NOTARY-028`; + the `ClaimProvenance` struct in `model.rs` holds `schema_version`, `generated_by`, `used`, and + `derived_from`, with no signature field). Provenance is correlation context, not a signature + over the result. Cryptographic verifiability comes from the SD-JWT VC credential (signed with + EdDSA over Ed25519, `REQ-PR-NOTARY-013`) or the signed federation result (a compact signed JWT, + `REQ-PR-NOTARY-018`), not from the default evaluation response. +- **Correctness depends on the configured source.** Notary reports what its configured source + says and does not independently vouch for whether the source is correct or current + (`REQ-PR-NOTARY-005`). +- **Alignment with a standard is not conformance.** Conformance to `RS-PR-NOTARY` does not imply + conformance to any external standard cited in its `standards_referenced` (`rs-pr-notary.mdx`), + and a CCCEV-shaped JSON-LD output is a profiled subset that must not assert conformance to CCCEV + 2.00 (`REQ-PR-NOTARY-012`). In the standards register, `aligns_with` means the project follows a + standard's model and intent without claiming formal conformance (`standards.mdx`). + +## Related + +- The protocol and data-model contracts: [RS-PR-NOTARY](../../spec/rs-pr-notary/), + [RS-DM-CLAIM](../../spec/rs-dm-claim/), [RS-SEC-G](../../spec/rs-sec-g/) +- The overview: [Records stay home](../records-stay-home/) *(explanation)* +- Evidence issuance, end to end: [Evidence issuance](../evidence-issuance/) *(explanation)* diff --git a/docs/site/src/content/docs/explanation/records-stay-home.mdx b/docs/site/src/content/docs/explanation/records-stay-home.mdx index c8d8cc7b..fb2348bf 100644 --- a/docs/site/src/content/docs/explanation/records-stay-home.mdx +++ b/docs/site/src/content/docs/explanation/records-stay-home.mdx @@ -219,5 +219,5 @@ mistake, so the limits are stated plainly here. [RS-PR-RELAY](../../spec/rs-pr-relay/), [RS-PR-NOTARY](../../spec/rs-pr-notary/), [RS-DM-CLAIM](../../spec/rs-dm-claim/) - Evidence issuance, end to end *(explanation)* -- Disclosure and minimization in depth *(Trust & Security)* +- [Disclosure and minimization](../disclosure-and-minimization/), in depth *(explanation)* - The threat model and security posture *(Trust & Security)* From 6c4dc126e5ae54479f3b70a58c1c95b13d8582f3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 15:17:09 +0000 Subject: [PATCH 11/14] docs: declare referenced standards and scope redacted CCCEV output E2 (disclosure-and-minimization): the page references OID4VCI, did:jwk (W3C DID), JSON-LD, and CCCEV beyond SD-JWT VC; declare them in standards_referenced so the standards register tracks them. Scope the redacted "no yes/no" guarantee to the claim-result JSON shape: the CCCEV JSON-LD render serializes cccev:isConformantTo from satisfied with a false fallback (runtime.rs), so a redacted CCCEV node still carries a boolean outcome field. Pilot (records-stay-home): declare oid4vci, now referenced by the unauthenticated-route sentence. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01Gfv6Eurtn4CzfnxLpNL2gP Signed-off-by: Claude --- .../docs/explanation/disclosure-and-minimization.mdx | 10 ++++++++-- .../src/content/docs/explanation/records-stay-home.mdx | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/site/src/content/docs/explanation/disclosure-and-minimization.mdx b/docs/site/src/content/docs/explanation/disclosure-and-minimization.mdx index 9173e23b..c3a71f23 100644 --- a/docs/site/src/content/docs/explanation/disclosure-and-minimization.mdx +++ b/docs/site/src/content/docs/explanation/disclosure-and-minimization.mdx @@ -10,6 +10,10 @@ doc_type: explanation locale: en standards_referenced: - sd-jwt-vc + - oid4vci + - w3c-did + - json-ld + - cccev --- The overview establishes the boundary: Registry Relay returns scoped records, Registry Notary @@ -98,8 +102,10 @@ The `redacted` mode withholds both. `REQ-PR-NOTARY-010` requires that a redacted carry the underlying source value and not convey the satisfaction outcome: the body withholds both the value and the predicate. The runtime matches this: the disclosed value is absent, and the `satisfied` outcome is populated only for `value` and `predicate` (by reading the value as a -boolean) and is absent for `redacted` (`runtime.rs`). A redacted answer carries no value and no -yes/no. +boolean) and is absent for `redacted` (`runtime.rs`). In the claim-result JSON shape a redacted answer +carries no value and no yes/no. The CCCEV JSON-LD render is the exception: it serializes +`cccev:isConformantTo` from `satisfied` with a `false` fallback, so a redacted CCCEV node still +carries a boolean outcome field (`runtime.rs`). ## How policy binds a mode diff --git a/docs/site/src/content/docs/explanation/records-stay-home.mdx b/docs/site/src/content/docs/explanation/records-stay-home.mdx index fb2348bf..81050568 100644 --- a/docs/site/src/content/docs/explanation/records-stay-home.mdx +++ b/docs/site/src/content/docs/explanation/records-stay-home.mdx @@ -10,6 +10,7 @@ doc_type: explanation locale: en standards_referenced: - sd-jwt-vc + - oid4vci --- An institution that runs a civil registry, a social-protection database, or a health From 9f9602c845954c8a7f1a7cd12071dcf29e8d56db Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 15:41:38 +0000 Subject: [PATCH 12/14] docs(trust): add "Trust and security" overview with posture table Add explanation/trust-and-security.mdx, the third trust-spine page: a single map of the security and trust posture for a reviewer or operator. Built ground, draft, adversarial-verify; every posture-table row and authorization/audit claim traces to an RS-* requirement or a line of Rust. - A posture table (authentication, authorization/scope, audit, disclosure, credential signing, holder binding, revocation, federation, key custody, network egress, transport/tenant isolation) with a stack/operator/configured "Provided by" column. - Exact scope enumerations: six data-plane suffixes (including identity_release and the parameterized evidence_verification: form) and three service-level scopes. - A Known gaps section stating the two spec/code conflicts honestly as spec-says-X, implementation-does-Y, under review: the holder-binding default none vs REQ-PR-NOTARY-015, and the missing scopes field on the Notary EvidenceAuditEvent vs REQ-SEC-G-008. Also correct credential signing to the profile's configured algorithm (EdDSA over Ed25519 by default, ES256 over P-256 supported) on the disclosure page, and wire the trust-spine Related links across the three pages. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01Gfv6Eurtn4CzfnxLpNL2gP Signed-off-by: Claude --- .../disclosure-and-minimization.mdx | 6 +- .../docs/explanation/records-stay-home.mdx | 2 +- .../docs/explanation/trust-and-security.mdx | 218 ++++++++++++++++++ 3 files changed, 223 insertions(+), 3 deletions(-) create mode 100644 docs/site/src/content/docs/explanation/trust-and-security.mdx diff --git a/docs/site/src/content/docs/explanation/disclosure-and-minimization.mdx b/docs/site/src/content/docs/explanation/disclosure-and-minimization.mdx index c3a71f23..ceba473a 100644 --- a/docs/site/src/content/docs/explanation/disclosure-and-minimization.mdx +++ b/docs/site/src/content/docs/explanation/disclosure-and-minimization.mdx @@ -146,7 +146,8 @@ was disclosed, not the intent of the asker. Disclosure modes govern an evaluation result. An issued credential is a distinct layer with its own disclosure mechanism, and the two should not be conflated. The credential is an SD-JWT VC -(`typ` header `dc+sd-jwt`, signed with EdDSA over Ed25519, `REQ-PR-NOTARY-013`) that gives the +(`typ` header `dc+sd-jwt`, signed with the profile's configured algorithm, EdDSA over Ed25519 by +default or ES256 over P-256, `REQ-PR-NOTARY-013`) that gives the holder selective disclosure: the holder chooses which fields to reveal to which verifier. The signed body carries a SHA-256 digest of each selectively disclosable field rather than the field value, so an unselected field stays hidden and a holder cannot present a disclosure that was not @@ -231,7 +232,7 @@ The limits below are the documented honest framing, not discovered defects. the `ClaimProvenance` struct in `model.rs` holds `schema_version`, `generated_by`, `used`, and `derived_from`, with no signature field). Provenance is correlation context, not a signature over the result. Cryptographic verifiability comes from the SD-JWT VC credential (signed with - EdDSA over Ed25519, `REQ-PR-NOTARY-013`) or the signed federation result (a compact signed JWT, + its configured EdDSA or ES256 key, `REQ-PR-NOTARY-013`) or the signed federation result (a compact signed JWT, `REQ-PR-NOTARY-018`), not from the default evaluation response. - **Correctness depends on the configured source.** Notary reports what its configured source says and does not independently vouch for whether the source is correct or current @@ -248,3 +249,4 @@ The limits below are the documented honest framing, not discovered defects. [RS-DM-CLAIM](../../spec/rs-dm-claim/), [RS-SEC-G](../../spec/rs-sec-g/) - The overview: [Records stay home](../records-stay-home/) *(explanation)* - Evidence issuance, end to end: [Evidence issuance](../evidence-issuance/) *(explanation)* +- The security posture: [Trust and security](../trust-and-security/) *(explanation)* diff --git a/docs/site/src/content/docs/explanation/records-stay-home.mdx b/docs/site/src/content/docs/explanation/records-stay-home.mdx index 81050568..67d85369 100644 --- a/docs/site/src/content/docs/explanation/records-stay-home.mdx +++ b/docs/site/src/content/docs/explanation/records-stay-home.mdx @@ -221,4 +221,4 @@ mistake, so the limits are stated plainly here. [RS-DM-CLAIM](../../spec/rs-dm-claim/) - Evidence issuance, end to end *(explanation)* - [Disclosure and minimization](../disclosure-and-minimization/), in depth *(explanation)* -- The threat model and security posture *(Trust & Security)* +- The security posture: [Trust and security](../trust-and-security/) *(explanation)* diff --git a/docs/site/src/content/docs/explanation/trust-and-security.mdx b/docs/site/src/content/docs/explanation/trust-and-security.mdx new file mode 100644 index 00000000..1587da67 --- /dev/null +++ b/docs/site/src/content/docs/explanation/trust-and-security.mdx @@ -0,0 +1,218 @@ +--- +title: Trust and security +description: A single map of the Registry Stack trust posture, the stack-versus-operator responsibility split, and the honest gaps a reviewer should weigh. +status: draft +owner: registry-docs +source_repos: + - registry-stack +last_reviewed: "2026-06-28" +doc_type: explanation +locale: en +standards_referenced: + - sd-jwt-vc + - oid4vci + - w3c-did +--- + +This page is the trust map for a security reviewer or operator: one place to read the posture of +the runtime services and the line where their guarantees stop and the deployment's begin. It does +not repeat the disclosure model or the boundary narrative in full. For the boundary overview see +[Records stay home](../records-stay-home/); for the disclosure model in depth see +[Disclosure and minimization](../disclosure-and-minimization/). The normative source is the draft +`RS-*` specification set, principally [RS-SEC-G](../../spec/rs-sec-g/), +[RS-PR-RELAY](../../spec/rs-pr-relay/), and [RS-PR-NOTARY](../../spec/rs-pr-notary/). + +## The trust model in one frame + +The stack draws an explicit boundary between what the runtime services guarantee and what the +deployment supplies. `RS-SEC-G` Section 9 states it plainly: the security model ends where the +deployment begins. Inside the boundary the runtime services provide cryptographic and policy +primitives. Outside it the operator provisions, configures, and operates them. + +Three properties stay inside the boundary by design. Custody is distributed: each authority keeps +control of its own registry data, and the stack does not aggregate records into a central store +(`records-stay-home.mdx`). Consultation is read-in-place with no write-back: in v1 the runtime +services must not expose source-registry data mutation routes, and consultation APIs are read-only +(`REQ-ARC-G-003`). The private signing key never leaves the issuer: an issuer signs with an +asymmetric key and publishes only the public half (`REQ-SEC-G-007`). + +Several guarantees sit outside the boundary and are the operator's to provide: +secret and key provisioning, key custody and rotation schedule, audit retention and storage, +tenant isolation, transport termination and certificate management, network rate limiting at the +edge, deployment configuration, and incident response (`RS-SEC-G` Section 9). The stack provides +the primitives; the operator provisions, configures, and operates them. + +The responsibilities below also split along a service line within the stack. Registry Notary owns +claim evaluation, disclosure policy, and credential issuance; Registry Relay must not perform claim +evaluation or issue credentials (`REQ-ARC-G-007`). That boundary does not prohibit Relay from +enforcing runtime access, freshness, and redaction policy on its own governed consultation routes. + +## Posture table + +The table maps the key properties to their current grounded posture and to who provides each. The +"Provided by" column distinguishes a stack primitive, an operator responsibility, and a property +that is configured (a stack primitive whose effect depends on deployment configuration). + +| Property | Current posture | Provided by | +|------|------|------| +| Authentication | A runtime service runs exactly one authentication mode (static credentials/API key or OIDC) and authenticates every route returning person-level records or claim results before producing a response (`REQ-SEC-G-002`, `REQ-PR-RELAY-005`). | Stack (primitive); mode configured | +| Authorization / scope | Scope-based and deny-by-default, with scope-before-source: the required scope is enforced before any source read or claim evaluation, so a caller lacking the scope is refused before source access (`REQ-SEC-G-005`, `REQ-PR-RELAY-006`). | Stack (primitive); scopes configured | +| Audit | An audit envelope is a hash-chained append record; a deployment can run audit fail-closed, so a request whose audit record cannot be written does not return a successful result (`REQ-SEC-G-009`). The Notary record body omits one spec-required field (scopes); see Known gaps. | Stack (primitive); fail-closed configured | +| Disclosure and minimization | A claim answer carries one of three policy-bound modes; minimization is modelled by the claim author, not imposed on every answer. See [Disclosure and minimization](../disclosure-and-minimization/). | Stack (primitive); claim-modelled | +| Credential signing | An issued credential is an SD-JWT VC (`typ` `dc+sd-jwt`) signed with the profile's configured algorithm (EdDSA over Ed25519 by default, or ES256 over P-256), with no W3C VC JSON-LD envelope (`REQ-PR-NOTARY-013`). | Stack; algorithm configured | +| Holder binding | Spec requires binding via `did:jwk` in `cnf` (`REQ-PR-NOTARY-015`); the implementation default is `none` (unbound), with `did` binding forced only on the self-attestation / OID4VCI path. Gap under review (below). | Stack; gap under review | +| Revocation / credential status | An optional, default-disabled status-list surface; `CredentialStatusConfig.enabled` defaults to `false`. No revocation flow is specified by the `RS-*` set. | Operator (optional surface) | +| Federation | Static-peer only: peers loaded from config at startup, an unconfigured peer rejected; the answer is a compact signed JWT carrying a scoped evaluation result, never a credential (`REQ-PR-NOTARY-018`, `REQ-PR-NOTARY-019`). | Stack; peers configured | +| Key custody | The stack never serves the private half (the `PublicJwk` type rejects private members); storage, injection, and rotation of keys are the operator's (`REQ-SEC-G-007`, `REQ-SEC-G-013`, `RS-SEC-G` Section 9). | Stack (public-only surface) + operator | +| Network egress | An outbound SSRF-guard primitive constrains destinations (scheme allowlist, deny private ranges, deny cloud-metadata), applied at `SHOULD` level; edge rate limiting and egress limits are the operator's (`REQ-SEC-G-012`, `RS-SEC-G` Section 9). | Stack (primitive) + operator | +| Transport / tenant isolation | Outside the security model; assigned to the operator (`RS-SEC-G` Section 1 and Section 9). | Operator | + +## Authorization in detail + +Authorization is scope-based and deny-by-default. A caller must hold the scope a route requires, +and the service enforces that scope before it reads any source or evaluates any claim, so a caller +that lacks the scope is refused before source access rather than after (`REQ-SEC-G-005`). On the +Relay data plane, access is scope-checked per dataset (`REQ-PR-RELAY-006`). + +The per-dataset data-plane scopes follow the pattern `:`. The bare suffixes +are `metadata`, `rows`, `aggregate`, `verify`, `evidence_verification`, and `identity_release` +(`crates/registry-relay/src/config/validate.rs`). The implementation also accepts a parameterized +`evidence_verification:` form where the suffix is non-empty, so the data-plane vocabulary +is not closed at exactly six fixed strings. + +Three service-level scopes sit outside the per-dataset pattern and are accepted directly by scope +validation: `registry_relay:admin` (reload and configuration-mutation routes), +`registry_relay:ops_read` (operational posture reads), and `registry_relay:metrics_read` (metrics) +(`validate.rs`; `REQ-PR-RELAY-018`). + +A governed read passes through a policy decision point before governed data is returned. A governed +Relay read must receive a PDP permit before returning entity-backed or aggregate data; on denial +Relay fails closed and returns a stable `pdp.*` problem code rather than falling back to an +ungoverned read (`REQ-PR-RELAY-019`). Scope authorization runs ahead of the PDP gate: an aggregate +route performs ordinary aggregate and source-read scope authorization first, and a scope denial +returns `auth.scope_denied` without reporting PDP policy provenance (`REQ-PR-RELAY-021`). Unscoped +or malformed trust metadata must not satisfy the PDP gate (`REQ-PR-RELAY-020`). + +A service must not widen a caller's reach at request time beyond what configuration grants. Only +liveness and readiness probes and public verification-key discovery (the issuer JWKS and, where +published, the `did:web` document) are served without authentication; every claim-bearing and +record-bearing route requires authentication (`REQ-SEC-G-006`). On the Notary listener the +auth-exempt surface is broader than probes alone: it covers `/healthz`, `/ready`, the evidence +JWKS, the OpenID credential-issuer metadata, the OID4VCI issuance-flow routes, the credential-status +route, the VCT routes, and the docs/OpenAPI routes (the OpenAPI document only when +`openapi_requires_auth` is disabled) (`crates/registry-notary-server/src/standalone.rs`). The +federation evaluations route is exempt from API-key/OIDC middleware only: the federation handler +still requires and verifies the peer-signed JWS, so delegated evaluation is not unauthenticated. + +A note on coverage: `REQ-SEC-G-002` and `REQ-SEC-G-008` state an invariant a conforming deployment +meets, not a per-route audit of a given build. The posture above is the specified invariant plus +the route-registration evidence, not an enumeration of every route in a running binary. + +## Audit as a control + +Audit is a security control, not best-effort logging. A deployment must be able to run audit +fail-closed, so a request whose audit record cannot be written does not return a successful result, +and a service must not silently drop an audit record on the success path (`REQ-SEC-G-009`). For +Registry Notary, every evaluated request must emit an `EvidenceAuditEvent` in an audit envelope, +and an audit write failure must surface as a request error rather than being swallowed as a silent +log entry (`REQ-PR-NOTARY-020`, `REQ-PR-NOTARY-021`). + +The audit envelope is a hash-chained append record: each `AuditEnvelope` carries an envelope id, a +timestamp, the previous envelope hash, the consumer-owned record body, and the record hash +(`crates/registry-platform-audit/src/lib.rs`). The envelope carries no retention or TTL field, +which is why audit retention and storage are the operator's (`RS-SEC-G` Section 9). `REQ-SEC-G-009` +is phrased as a capability the deployment must be able to enable, so fail-closed audit is a +configurable obligation rather than an unconditional runtime guarantee in every deployment. One +spec-required envelope field, the scopes exercised, is not captured on the Notary record body; see +Known gaps. + +## Credentials and disclosure + +Cryptographic verifiability comes from the issued credential and the signed federation result, not +from the everyday evaluation response. An issued credential is an SD-JWT VC (media type and `typ` +header `dc+sd-jwt`) signed with EdDSA over Ed25519 by default, with no W3C VC JSON-LD envelope +(`REQ-PR-NOTARY-013`). Credential-profile signing keys may be configured +as `EdDSA` or `ES256` over P-256; `RS256` is rejected for credential profiles and is reserved for +the eSignet client-assertion path, while access-token and federation signing remain EdDSA-only +(`crates/registry-notary-core/src/config.rs`). `ES256` is a configurable profile signing +algorithm, not the default issued-credential signature. + +Selective disclosure is digest-based: the signed credential body carries the SHA-256 digest of each +selectively disclosable field rather than the value, so an unselected field stays hidden and a +holder cannot present a disclosure that was not in the original credential (`REQ-PR-NOTARY-014`). +Public claim results, by contrast, are provenance-tagged (`registry-notary-claim-provenance/v1`), +not signed (`REQ-PR-NOTARY-028`). The mechanics of the three disclosure modes, mode selection, and +the minimization discipline live in [Disclosure and minimization](../disclosure-and-minimization/). + +## Federation and keys + +Federation is static-peer only. Peers are loaded from configuration at startup and a request from +an unconfigured peer is rejected: the runtime keys peers by their `iss` claim, and an issuer absent +from the configured set is rejected as an invalid token +(`crates/registry-notary-server/src/federation/`). Delegated evaluation +(`POST /federation/v1/evaluations`) accepts a compact signed JWT request and returns a compact +signed JWT response carrying a scoped evaluation result; before any source read the serving Notary +verifies peer policy, replay state, purpose, profile, and audience (`REQ-PR-NOTARY-018`). The +federated answer is never a credential, and each federation runtime holds its own replay store. +Dynamic trust-chain discovery, shared replay storage, and federated credential issuance are out of +scope for this version and must not be implied by a conformance claim (`REQ-PR-NOTARY-019`). + +A verifier needs no credential to obtain a public key. The issuer JWKS +(`GET /.well-known/evidence/jwks.json`) is served without authentication (`REQ-PR-NOTARY-002`), and +it exposes public JWKs only: the `PublicJwk` type rejects private key members, so the private half +never appears on the published surface (`crates/registry-platform-crypto/src/lib.rs`). A key +rotated out may remain published so existing results stay verifiable; that is not a revocation +mechanism (`records-stay-home.mdx`). The stack never serving the private half is a property of the +public surface, not a guarantee that the deployment's key storage is secure: how keys are stored, +injected, and rotated (environment, file, or hardware module) is an operator responsibility, and an +implementation must not embed secret material in any portable or distributable artifact +(`REQ-SEC-G-013`, `RS-SEC-G` Section 9). + +## Known gaps + +Two specification requirements diverge from the current implementation. Both are stated here as +spec-says-X, implementation-does-Y, under review, not as resolved decisions. + +- **Holder binding default.** `REQ-PR-NOTARY-015` says a credential MUST bind its holder by naming + the holder's public key as a `did:jwk` in the `cnf` claim, with `did:jwk` the only supported + method and a fresh, audience-bound holder proof required at presentation. The implementation + defaults `holder_binding` mode to `none`, which issues an unbound credential carrying no `cnf` + and requiring no holder proof; `did` binding is forced only on the self-attestation / OID4VCI + credential-profile path, where profile validation rejects any mode other than `did` and requires + `proof_of_possession` (`crates/registry-notary-core/src/config.rs`). The general default + therefore leaves credentials unbound, diverging from the MUST. Under review. +- **Audit scope capture on the Notary path.** `REQ-SEC-G-008` requires the audit envelope to + capture at least the caller principal, the scopes exercised, a request identifier, and the + `Data-Purpose` value where supplied. The Notary `EvidenceAuditEvent` struct records a principal + hash, decision, method, path, status, claim hash, purposes, and a correlation hash, but has no + field capturing the scopes exercised (`crates/registry-notary-core/src/model.rs`). The Relay + `AuditRecord` does carry `scopes_used` (`crates/registry-relay/src/audit/mod.rs`), so the + omission is specific to the Notary record, not stack-wide. Under review. + +## Honest limits + +The following are documented honest framing, not discovered defects. + +- **This is not zero-knowledge.** A `predicate` answer is a policy-enforced boolean computed inside + the service, and SD-JWT selective disclosure is digest omission. Neither is a zero-knowledge + proof, and the documentation should not imply one (`records-stay-home.mdx`). +- **Minimization is modelled, not automatic.** `value` mode is not constrained to a scalar; a claim + modelled to return an object or extracted record returns one. Least disclosure is a design choice + the claim makes, not a property the stack imposes on every answer (`records-stay-home.mdx`). +- **Correctness depends on the configured source.** Notary reports what its configured source says + and does not independently vouch for whether the source is correct or current + (`records-stay-home.mdx`). +- **Several guarantees are the operator's.** Network egress limits, key custody, tenant isolation, + audit retention, and transport security are supplied by the deployment, not guaranteed by the + stack (`records-stay-home.mdx`). +- **The model is specified in draft.** The behaviour above is defined in the `RS-*` specifications, + which are still drafts and may change. Alignment with an external standard is not a claim of + conformance to it or of legal compliance (`records-stay-home.mdx`; `RS-SEC-G`; `RS-PR-NOTARY`). + +## Related + +- The security and protocol contracts: [RS-SEC-G](../../spec/rs-sec-g/), + [RS-ARC-G](../../spec/rs-arc-g/), [RS-PR-RELAY](../../spec/rs-pr-relay/), + [RS-PR-NOTARY](../../spec/rs-pr-notary/) +- The boundary overview: [Records stay home](../records-stay-home/) *(explanation)* +- The disclosure model in depth: [Disclosure and minimization](../disclosure-and-minimization/) *(explanation)* From 380ae045f085ff45a25c0088248baac1faf82e23 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 16:04:00 +0000 Subject: [PATCH 13/14] docs(trust): complete the security surface on Trust and security Five grounded corrections from review, each verified against spec and code, so an auditor reading the security posture is not misled: - Relay issuer surface: scope the "Relay must not issue credentials" boundary to claim credentials, and note Relay's own signed response credentials (VCDM 2.0 VC-JWT) and did:web issuer surface with provenance enabled (REQ-PR-RELAY-013, REQ-PR-RELAY-014, REQ-SEC-G-007). - Self-attestation authorization: scope-before-source applies to scope-authorized callers; a self-attestation principal is gated by the trust-context policy (self_attestation.scope_policy, default Required, can be Disabled), not a per-source scope check. - Network egress: note the allow_insecure_private_network opt-in re-enables private-network HTTP destinations while still denying cloud-metadata. - Unauthenticated surface: add the Relay listener's auth-exempt routes, including the provenance verifier-support routes (/schemas, /contexts, /.well-known/did.json) and OpenAPI. - Federation replay: federation reuses the deployment replay store (Redis for multi-instance); the shared replay storage REQ-PR-NOTARY-019 places out of scope is cross-peer replay coordination, not a deployment's own Redis store. Also scope the same cross-peer-replay point on the disclosure page. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01Gfv6Eurtn4CzfnxLpNL2gP Signed-off-by: Claude --- .../disclosure-and-minimization.mdx | 4 +- .../docs/explanation/trust-and-security.mdx | 46 ++++++++++++++----- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/docs/site/src/content/docs/explanation/disclosure-and-minimization.mdx b/docs/site/src/content/docs/explanation/disclosure-and-minimization.mdx index ceba473a..5b9b4dca 100644 --- a/docs/site/src/content/docs/explanation/disclosure-and-minimization.mdx +++ b/docs/site/src/content/docs/explanation/disclosure-and-minimization.mdx @@ -209,8 +209,8 @@ signed evaluation result, never a credential. A peer posts a compact signed JWT evaluation result; before any source read, the serving Notary verifies peer policy, replay state, purpose, profile, and audience (`REQ-PR-NOTARY-018`). Federation is static-peer only: peers are loaded from configuration at startup, a request from an unconfigured peer is rejected, -and dynamic discovery, shared replay storage, and federated credential issuance are out of scope -(`REQ-PR-NOTARY-019`). The federated answer is the same kind of minimized, mode-governed +and dynamic discovery, cross-peer shared replay storage, and federated credential issuance are out +of scope (`REQ-PR-NOTARY-019`). The federated answer is the same kind of minimized, mode-governed evaluation result, signed for transport, rather than a portable credential. ## Honest limits diff --git a/docs/site/src/content/docs/explanation/trust-and-security.mdx b/docs/site/src/content/docs/explanation/trust-and-security.mdx index 1587da67..0ecf0609 100644 --- a/docs/site/src/content/docs/explanation/trust-and-security.mdx +++ b/docs/site/src/content/docs/explanation/trust-and-security.mdx @@ -43,9 +43,13 @@ edge, deployment configuration, and incident response (`RS-SEC-G` Section 9). Th the primitives; the operator provisions, configures, and operates them. The responsibilities below also split along a service line within the stack. Registry Notary owns -claim evaluation, disclosure policy, and credential issuance; Registry Relay must not perform claim -evaluation or issue credentials (`REQ-ARC-G-007`). That boundary does not prohibit Relay from -enforcing runtime access, freshness, and redaction policy on its own governed consultation routes. +claim evaluation, disclosure policy, and claim-credential issuance; Registry Relay must not perform +claim evaluation or issue claim credentials (`REQ-ARC-G-007`). That boundary does not prohibit Relay +from enforcing runtime access, freshness, and redaction policy on its own governed consultation +routes. It also does not cover signed response credentials: with `provenance` enabled, Relay attaches +its own VCDM 2.0 VC-JWT to an entity-record or aggregate response and, in gateway issuer mode, +publishes a `did:web` document, a second issuer and key surface distinct from the Notary claim +SD-JWT VC (`REQ-PR-RELAY-013`, `REQ-PR-RELAY-014`, `REQ-SEC-G-007`). ## Posture table @@ -64,15 +68,23 @@ that is configured (a stack primitive whose effect depends on deployment configu | Revocation / credential status | An optional, default-disabled status-list surface; `CredentialStatusConfig.enabled` defaults to `false`. No revocation flow is specified by the `RS-*` set. | Operator (optional surface) | | Federation | Static-peer only: peers loaded from config at startup, an unconfigured peer rejected; the answer is a compact signed JWT carrying a scoped evaluation result, never a credential (`REQ-PR-NOTARY-018`, `REQ-PR-NOTARY-019`). | Stack; peers configured | | Key custody | The stack never serves the private half (the `PublicJwk` type rejects private members); storage, injection, and rotation of keys are the operator's (`REQ-SEC-G-007`, `REQ-SEC-G-013`, `RS-SEC-G` Section 9). | Stack (public-only surface) + operator | -| Network egress | An outbound SSRF-guard primitive constrains destinations (scheme allowlist, deny private ranges, deny cloud-metadata), applied at `SHOULD` level; edge rate limiting and egress limits are the operator's (`REQ-SEC-G-012`, `RS-SEC-G` Section 9). | Stack (primitive) + operator | +| Network egress | An outbound SSRF-guard primitive constrains destinations (scheme allowlist, deny private ranges, deny cloud-metadata), applied at `SHOULD` level. The strict default denies private ranges, but a per-connection or per-peer `allow_insecure_private_network` opt-in re-enables private-network HTTP destinations while still denying cloud-metadata. Edge rate limiting and egress limits are the operator's (`REQ-SEC-G-012`, `RS-SEC-G` Section 9). | Stack (primitive) + operator | | Transport / tenant isolation | Outside the security model; assigned to the operator (`RS-SEC-G` Section 1 and Section 9). | Operator | ## Authorization in detail -Authorization is scope-based and deny-by-default. A caller must hold the scope a route requires, -and the service enforces that scope before it reads any source or evaluates any claim, so a caller -that lacks the scope is refused before source access rather than after (`REQ-SEC-G-005`). On the -Relay data plane, access is scope-checked per dataset (`REQ-PR-RELAY-006`). +Authorization is scope-based and deny-by-default. A scope-authorized caller must hold the scope a +route requires, and the service enforces that scope before it reads any source or evaluates any +claim, so a caller that lacks the scope is refused before source access rather than after +(`REQ-SEC-G-005`). On the Relay data plane, access is scope-checked per dataset +(`REQ-PR-RELAY-006`). + +Self-attestation principals are gated differently. `require_claim_access` and +`require_evaluation_access` return early for a self-attestation principal, so the per-source +required-scope check is not applied. The gate before source access is instead the self-attestation +trust-context policy (citizen client or audience match plus `self_attestation.scope_policy`). That +policy defaults to `Required` and enforces the configured scopes, but a deployment can set it to +`Disabled`, in which case no caller scope is required before source access. The per-dataset data-plane scopes follow the pattern `:`. The bare suffixes are `metadata`, `rows`, `aggregate`, `verify`, `evidence_verification`, and `identity_release` @@ -100,7 +112,12 @@ record-bearing route requires authentication (`REQ-SEC-G-006`). On the Notary li auth-exempt surface is broader than probes alone: it covers `/healthz`, `/ready`, the evidence JWKS, the OpenID credential-issuer metadata, the OID4VCI issuance-flow routes, the credential-status route, the VCT routes, and the docs/OpenAPI routes (the OpenAPI document only when -`openapi_requires_auth` is disabled) (`crates/registry-notary-server/src/standalone.rs`). The +`openapi_requires_auth` is disabled) (`crates/registry-notary-server/src/standalone.rs`). On the +Relay listener the auth-exempt surface is likewise broader than probes: it covers `/healthz`, +`/ready`, `/docs`, the `/.well-known/api-catalog` linkset, and the OpenAPI document when +`openapi_requires_auth` is disabled, and when `provenance` is enabled it adds the verifier-support +routes `/schemas/...` and `/contexts/...`, plus the `/.well-known/did.json` document in gateway +issuer mode (`REQ-PR-RELAY-013`; `crates/registry-relay/src/server.rs`). The federation evaluations route is exempt from API-key/OIDC middleware only: the federation handler still requires and verifies the peer-signed JWS, so delegated evaluation is not unauthenticated. @@ -153,9 +170,14 @@ from the configured set is rejected as an invalid token (`POST /federation/v1/evaluations`) accepts a compact signed JWT request and returns a compact signed JWT response carrying a scoped evaluation result; before any source read the serving Notary verifies peer policy, replay state, purpose, profile, and audience (`REQ-PR-NOTARY-018`). The -federated answer is never a credential, and each federation runtime holds its own replay store. -Dynamic trust-chain discovery, shared replay storage, and federated credential issuance are out of -scope for this version and must not be implied by a conformance claim (`REQ-PR-NOTARY-019`). +federated answer is never a credential, and federation reuses the deployment replay store rather +than a separate one: federation replay checks run against the same `replay.storage` store the rest +of the runtime uses, which a multi-instance deployment must back with `redis` so a replay accepted +by one process is rejected by another (`crates/registry-notary-core/src/config.rs`). The shared +replay storage that `REQ-PR-NOTARY-019` places out of scope is replay state coordinated across +federation peers, not a deployment backing its own store with Redis. Dynamic trust-chain discovery, +cross-peer shared replay storage, and federated credential issuance are out of scope for this +version and must not be implied by a conformance claim (`REQ-PR-NOTARY-019`). A verifier needs no credential to obtain a public key. The issuer JWKS (`GET /.well-known/evidence/jwks.json`) is served without authentication (`REQ-PR-NOTARY-002`), and From 26517e3eaf5ce92f8e39830a7d05ad2ba9ee8a2f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 16:18:44 +0000 Subject: [PATCH 14/14] docs(records-stay-home): correct the existence-predicate example An existence predicate does not return a clean false for a missing record: under the default matching-failure collapse, an unresolved record surfaces a single not-available reason rather than a false, the same collapse the limits section describes as the existence-oracle protection. State that a resolved record returns true and an unresolved one collapses to not-available, so the example agrees with the page's own existence-oracle limit. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01Gfv6Eurtn4CzfnxLpNL2gP Signed-off-by: Claude --- docs/site/src/content/docs/explanation/records-stay-home.mdx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/site/src/content/docs/explanation/records-stay-home.mdx b/docs/site/src/content/docs/explanation/records-stay-home.mdx index 67d85369..a1c26530 100644 --- a/docs/site/src/content/docs/explanation/records-stay-home.mdx +++ b/docs/site/src/content/docs/explanation/records-stay-home.mdx @@ -133,8 +133,9 @@ least-revealing mode that still answers the question. This is the mechanism behind "prove a fact without sharing the record". To check whether a person has a registered record, model the question as an *existence* rule and disclose it -as a `predicate`: the caller learns `true` or `false`, and the row never crosses the -boundary. To check eligibility without exposing an income figure, derive the decision with +as a `predicate`: a resolved record returns `true`, and the row never crosses the +boundary. By default a record that does not resolve collapses to a single not-available +reason rather than a `false`, so the lookup cannot be used as an existence oracle. To check eligibility without exposing an income figure, derive the decision with an expression rule and disclose the eligibility boolean as a `predicate`; the income value stays home.