From fdd153146c7fffb9217373c793c757f543c67369 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 12:47:25 +0000 Subject: [PATCH 1/8] 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 2/8] 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 578cf681164c5602167b3a98932d58b841442078 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 06:30:17 +0000 Subject: [PATCH 3/8] docs: add Stage-1 Trust & Security explanation set; refresh the Relay tutorial MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add five reviewer/operator-facing explanation pages and wire them into the sidebar (a new "Trust & security" group, plus the disclosure page and the records-stay-home pilot under Explanation): - Trust posture and security guarantees — a plain-language overview with an at-a-glance posture table. - Disclosure modes and computed answers — the three modes and what each one reveals. - Data minimization and purpose limitation — the privacy / DPO view. - Threat model — boundaries, assets, threats, and residual risks. - Known limitations and non-guarantees — the canonical limits hub the other pages link to instead of restating. Refresh the Relay tutorial (publish-spreadsheet-secured-registry-api) with a clearer framing, the exact problem+json codes (auth.missing_credential / auth.scope_denied / auth.purpose_required), and a "what you built" recap, on top of its existing verified command output. The sh-command count is unchanged, so the tutorial drift check still holds. All pages follow the house style (second person, sentence-case headings, no inline spec citations — the specs are linked at the foot of each page), and every load-bearing security/privacy claim was re-verified against both the RS-* specs and the implementation. The content stages of `npm run check` pass (frontmatter, Vale, markdown, openapi lint, tutorial drift, build, llms, links, seo). Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_011U1jTj594nGXhR2r9BzWqm Signed-off-by: Claude --- docs/site/astro.config.mjs | 14 + ...ta-minimization-and-purpose-limitation.mdx | 87 +++++++ .../disclosure-modes-and-computed-answers.mdx | 87 +++++++ .../docs/explanation/known-limitations.mdx | 119 +++++++++ .../content/docs/explanation/threat-model.mdx | 246 ++++++++++++++++++ .../trust-posture-and-security-guarantees.mdx | 135 ++++++++++ ...blish-spreadsheet-secured-registry-api.mdx | 26 +- 7 files changed, 709 insertions(+), 5 deletions(-) create mode 100644 docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx create mode 100644 docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx create mode 100644 docs/site/src/content/docs/explanation/known-limitations.mdx create mode 100644 docs/site/src/content/docs/explanation/threat-model.mdx create mode 100644 docs/site/src/content/docs/explanation/trust-posture-and-security-guarantees.mdx diff --git a/docs/site/astro.config.mjs b/docs/site/astro.config.mjs index fa60de1d..704dc02b 100644 --- a/docs/site/astro.config.mjs +++ b/docs/site/astro.config.mjs @@ -237,14 +237,28 @@ export default defineConfig({ collapsed: true, items: [ { label: 'Architecture', slug: 'explanation/architecture' }, + { label: 'Records stay home', slug: 'explanation/records-stay-home' }, { label: 'Boundaries and map', slug: 'map/boundaries-and-map' }, { label: 'Consultation flow', slug: 'explanation/consultation-flow' }, { label: 'Evidence issuance', slug: 'explanation/evidence-issuance' }, + { label: 'Disclosure modes', slug: 'explanation/disclosure-modes-and-computed-answers' }, { label: 'Trusted context', slug: 'explanation/trusted-context-constraints' }, { label: 'Integration patterns', slug: 'explanation/integration-patterns' }, { label: 'DPI safeguards', slug: 'explanation/dpi-safeguards-alignment' }, ], }, + { + // Trust & Security rail (roadmap §6): the reviewer/auditor-facing + // posture, threat model, privacy story, and the canonical limits hub. + label: 'Trust & security', + collapsed: true, + items: [ + { label: 'Trust posture', slug: 'explanation/trust-posture-and-security-guarantees' }, + { label: 'Threat model', slug: 'explanation/threat-model' }, + { label: 'Data minimization', slug: 'explanation/data-minimization-and-purpose-limitation' }, + { label: 'Known limitations', slug: 'explanation/known-limitations' }, + ], + }, { label: 'Reference', collapsed: true, diff --git a/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx b/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx new file mode 100644 index 00000000..8ab9644b --- /dev/null +++ b/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx @@ -0,0 +1,87 @@ +--- +title: Data minimization and purpose limitation by design +description: How the Registry Stack architecture supports data minimization and purpose limitation, where those supports depend on operator configuration, and which data-subject-rights obligations it does not address. +status: draft +owner: registry-docs +source_repos: + - registry-stack +last_reviewed: "2026-06-28" +doc_type: explanation +locale: en +standards_referenced: + - sd-jwt-vc +--- + +As a data protection officer or privacy reviewer, you are asked whether a system supports the principles you work to — data minimization, purpose limitation, accountability — and where those supports stop. This page answers one question about Registry Stack: how does its architecture support data minimization and purpose limitation by design, where are those supports conditional on how an operator configures a deployment, and which data-protection obligations does the architecture not address at all? + +Two things this page does not do. It makes no compliance claim: conformance to the underlying specifications does not imply conformance to any external standard, nor to GDPR or any data-protection regulation. And it gives no legal advice and no jurisdiction-specific reading — that judgement remains yours. The specifications here are all draft, pre-1.0 documents and may change, so treat the design as a posture under development rather than a finished or warranted product. + +A note on terms, since you do not need Registry Stack internals to follow this. A *claim* is one pre-modelled question — one decision or one extracted value — that a system can ask about a person against a registry the institution already holds. A *source binding* is the configured rule that connects a claim to the specific source fields it reads. *Minimized evidence* means a response shaped by data minimization or selective disclosure; a *purpose-bound request* is a request that carries or is evaluated against a stated purpose. Those last two are the principles you already know, expressed in the system's own vocabulary. + +## How the claim model limits collection at the source + +The first place minimization appears is in the unit of data the system is built around. A claim definition describes one decision or one extracted value — not a whole record — so returning a full record would over-collect relative to the question actually asked. The design treats the narrow question, not the record, as the thing to be answered. + +That principle is enforced one layer down, at the binding between a claim and its source. A source binding reads only the fields its rule needs, and a request that supplies an input path outside a declared allow-list is rejected, so a binding cannot over-collect by accident. The allow-list converts "read only what you need" from an aspiration into a gate that refuses out-of-scope inputs before they reach the source. + +## Purpose limitation as an enforced gate, not a label + +Purpose limitation in this design is more than a field written into a log. It is enforced as a policy decision before any source is read. Where a source is configured to require a purpose, a request that omits the purpose header is rejected, and the supplied purpose is recorded in the audit record. Beyond recording it, the evaluation component combines the claim's permitted purposes with the source binding's permitted purposes and denies the request before source access if either set rejects the stated purpose. + +A matching policy carries the same idea further: it binds a source read to a declared purpose and relationship context and constrains which inputs may identify the subject, and a request whose purpose, relationship, or inputs the policy does not admit is refused before the source is touched. The effect a reviewer should take away is that purpose, when configured, acts as a precondition for access rather than an after-the-fact annotation. + +The phrase "when configured" is load-bearing, and the section on operator responsibilities returns to it. + +## Minimization at the output: disclosure modes and selective credentials + +Minimization also applies to what leaves the boundary. A claim's result can be shaped into one of three disclosure modes: *value* returns the full value, *predicate* returns only the true/false satisfaction, and *redacted* hides the value entirely. A privacy-sensitive claim is expected to default to the least-revealing mode that still answers the question. (How a mode is selected and policy-bound is covered in [Disclosure modes and computed answers](../disclosure-modes-and-computed-answers/) and is out of scope here.) + +The redacted mode is worth a reviewer's attention because it shows minimization without loss of accountability: a redacted result carries neither the underlying value nor the satisfaction outcome, yet the evaluation stays referenceable and verifiable through an evaluation identifier, a verification identifier, and a claim hash. You can audit that an evaluation happened and verify it later without the result itself disclosing anything about the person. + +When the system issues a credential rather than returning a direct answer, minimization continues at presentation time. Issued credentials are SD-JWT VC — a verifiable-credential format in which the signed body carries only a SHA-256 digest of each selectively disclosable field, so a field the holder does not present stays hidden, and the holder controls what is revealed. Holder binding — tying the credential to a holder key with `did:jwk` — is a per-profile setting that defaults to off, though the self-attestation (wallet) issuance path requires it; the direct claim or evaluation result is not a credential and is never holder-bound. One caveat for a reviewer: the issuance surface is a profiled, partial subset, not a full credential-issuer implementation, and should not be read as general wallet interoperability or full standards conformance. + +A further minimization detail guards against a subtler leak. A failed subject match collapses by default to a single public reason (evidence not available), with the granular reason kept only in the audit record, so the lookup surface cannot by default be used to confirm whether a person exists in a registry. + +## Data stays at source: distributed custody + +Underneath all of this is an architectural choice that matters for minimization at the system level: data stays at source. The design premise is distributed custody — the stack provides an API surface for lawful exchange between authorities and does not aggregate data into a central system. The read component must not mutate source registry data and exposes no write-back route, and runtime services are read-only in this version, with no source-registry data-mutation routes at all. + +For a reviewer this has a direct consequence. Because the architecture cannot alter or delete source records, it is also why no erasure flow exists at the source layer: the design has no mechanism to reach into and change the registry it reads from. That is described more fully in the next section as a limit, not a feature. + +One related boundary is positive rather than cautionary. The portable metadata the stack publishes must not carry person-level data or runtime secrets and bindings, which is precisely why those artifacts are safe to share — they hold no personal data. But publishing metadata only describes; it does not authorize access, enforce a policy, or assert that any record exists, so a published manifest carries no data-protection guarantee about live data. + +## Audit as an accountability primitive + +Accountability is supported through audit, which is treated as a security control rather than an optional log. Every request that returns person-level records or claim results must be recorded with at least the caller principal, a request identifier, and the purpose value where one was supplied, and a deployment can run audit fail-closed so that a request whose audit record cannot be written does not succeed. The security model expects the scopes exercised to be recorded too; in practice Relay's audit record captures them, but Notary's audit record does not yet include that field — a gap noted in the limitations inventory. + +One precise reading matters here. Audit fail-closed is a capability a deployment can turn on — not a guarantee that every route in any given build has been individually audited against it. Whether a particular deployment meets it is something a reviewer verifies in that deployment, not something the design asserts on its behalf. + +## What the architecture does not provide + +Some of the most important things for a DPO to know are the absences. Stating them plainly prevents the design from being read as more than it is. The full, canonical inventory is in [Known limitations and non-guarantees](../known-limitations/). + +- **No data-subject erasure or right-to-be-forgotten workflow.** There is no built-in erasure or deletion flow anywhere in the design, so it does not satisfy erasure obligations on its own. As noted above, the read-only design cannot mutate source records, so erasure, where it is required, remains an operation on the source registry outside this system. +- **No rectification or data-subject-rights flow.** Beyond erasure, there is no rectification or general data-subject-rights mechanism. +- **No credential revocation service.** There is no revocation flow. Key rotation exists — a rotated-out key may remain published so existing results stay verifiable — but that is not a way to revoke an already-issued credential. Don't count on revocation as a supported capability. +- **No broad cross-authority exchange beyond static peering.** Federation between authorities is static-peer only; dynamic trust-chain discovery, shared replay storage, and federated credential issuance are out of scope, so the design supports a narrower cross-authority data-exchange surface than the word "federation" might suggest. +- **No privacy-budgeted analytics.** Aggregate routes produce statistical outputs, not a longitudinal privacy budget. A data-protection impact assessment should not describe an aggregate route as privacy-budgeted unless a separate, deployed control actually provides that. +- **No compliance claim.** Conformance to these specifications does not imply conformance to any external standard or any data-protection regulation, and the specifications themselves are draft. + +## Operator responsibilities: what the design leaves to you + +The minimization and purpose-limitation posture described above is the design default, but the operator owns the configuration. Several of the protections are conditional, and a reviewer should test the deployment, not the design, for each one. + +The clearest example is the fallback when no matching policy is configured. With no matching policy in place, a binding falls back to unrestricted, identifier-only resolution with no purpose gating, no relationship gating, and no input minimization. Equivalently, purpose limitation is supported but partial: a purpose is recorded in audit only where the caller supplies one, and is enforced as a hard gate only where a claim or source binding configures a matching policy. The enforced gate described earlier exists only when someone has configured it. + +The existence-oracle protection is also defeasible. The matching surface collapses failures to a single public reason by default, but a deployment may disable that collapse and surface not-found, ambiguous, or rejected outcomes — an over-disclosure risk the operator controls. + +More broadly, the architecture defines primitives and leaves a large set of data-protection-relevant controls to the operator. Secret and key provisioning, audit retention and storage, tenant isolation, transport security, edge rate limiting, deployment configuration, and incident response are not defined by the design; they are responsibilities you provision and verify in a deployment. The clear-eyed view, then, is this: Registry Stack offers minimization, purpose limitation, and accountability as enforceable design primitives, but whether a given deployment realizes them — and whether it meets any legal obligation — depends on how the operator configures and runs it. + +## Related + +- [Disclosure modes and computed answers](../disclosure-modes-and-computed-answers/) — how disclosure modes are selected and policy-bound +- [Threat model](../threat-model/) — the boundaries, assets, and threats behind this posture +- [Trust posture and security guarantees](../trust-posture-and-security-guarantees/) — the high-level security summary +- [Known limitations and non-guarantees](../known-limitations/) — the full inventory of edges +- [Records stay home](../records-stay-home/) — what stays inside the institution's boundary and what crosses it +- The security and protocol specifications: [RS-SEC-G](../../spec/rs-sec-g/), [RS-DM-CLAIM](../../spec/rs-dm-claim/), [RS-PR-NOTARY](../../spec/rs-pr-notary/), [RS-PR-RELAY](../../spec/rs-pr-relay/) diff --git a/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx b/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx new file mode 100644 index 00000000..a638153c --- /dev/null +++ b/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx @@ -0,0 +1,87 @@ +--- +title: Disclosure modes and computed answers +description: Why Registry Notary can answer a question about a subject by returning a computed result — value, predicate, or redacted — instead of handing back the source record. +status: draft +owner: registry-docs +source_repos: + - registry-stack +last_reviewed: "2026-06-28" +doc_type: explanation +locale: en +standards_referenced: [] +--- + +You are weighing whether Registry Stack actually keeps a sensitive registry record private while still answering questions about the subjects in it. The short answer is that a caller asks a question and receives a *computed answer*, not the record. This page explains how that works, what each kind of answer does and does not reveal, and where the privacy claim has edges. + +You will see one product term used throughout: a **claim**, which is a single pre-modelled question — one decision or one extracted value, such as "is this person registered?" or "what is this person's registration date?". A claim is deliberately narrow: a claim that tried to return a whole record would over-collect and be hard to authorize, which is exactly the outcome this design avoids. + +## How Registry Notary controls what leaves the service + +Registry Notary is the component that evaluates a claim and decides what the caller receives back. It controls the answer through three **disclosure modes**: `value`, `predicate`, and `redacted`. They are the only modes — there is no fourth mode and no gradation between them. + +Per-claim disclosure control is enforced at runtime by the service. It is not left to a caller's good behaviour. + +## The evaluation pipeline: computed answers, not record handoffs + +The reason an answer can stand in for a record is that the caller never supplies the value being asked about. To evaluate a claim, the caller sends a **subject identifier** and a **claim id** over a REST call authenticated the way the rest of the service is — an API key or bearer token in static-credential mode, or an OIDC token, whichever your deployment is configured for. Registry Notary then performs the evaluation against its own configured sources. The caller does not supply the evaluated value, and cannot inject one. + +The evaluation runs as a pipeline: a source connector resolves facts about the subject, a rule computes the configured condition, a disclosure mode shapes what leaves the service, and a response format carries the result. Because Notary computes the answer itself, it can return that computed answer rather than handing back the source record it read. + +The rule that does the computing is one of three kinds. An **exists** rule checks for the presence of **exactly one** matching source record. An **extract** rule reads a value from a source field. A **cel** rule derives a value from source fields or earlier claim results through a hardened expression. Each kind produces an answer about the subject without that answer being a copy of the source row. + +## The three disclosure modes + +The disclosure mode fixes how much of the computed answer the caller receives: + +| Mode | What the caller receives | +|------|--------------------------| +| `value` | the full evaluated value | +| `predicate` | only the true/false satisfaction of the rule | +| `redacted` | no value and no satisfaction outcome | + +`value` returns the full value, so this mode does hand over the computed result in full — it is the least private of the three and is appropriate only where the use case calls for the actual value. `predicate` returns only the boolean satisfaction. `redacted` returns neither the underlying value nor the yes/no outcome. + +Every evaluation result records which disclosure mode was applied, so a downstream system can tell how much was revealed. + +## Why predicate and redacted avoid sharing the underlying record + +This is the core of the privacy story. Because Notary computes the answer from its own sources, a `predicate` result can satisfy a question while disclosing only a boolean. A question of whether someone has a registered record is modelled as an `exists` rule disclosed as `predicate`: the caller learns `true` or `false`, and the source row never leaves the service. A question about an eligibility threshold without exposing the figure behind it is modelled as a `cel` rule whose eligibility boolean is disclosed as `predicate`; the underlying value stays inside the service. + +A `redacted` result goes further: it carries neither the source value nor the satisfaction outcome. Both are withheld. + +The difference between the two matters. `predicate` is not "zero disclosure" — it still tells the caller a true-or-false fact about the subject. `redacted` goes one step further: it does not leak even a yes/no. They are not interchangeable. + +## How disclosure policy is configured per claim + +The caller does not freely pick a mode. Each claim configures a **disclosure policy**: a `default` mode plus an `allowed` set drawn from the three modes. When a caller requests a mode, Notary refuses any mode not in that claim's allowed set; when the caller requests no mode, Notary applies the claim's default. The mode is bound to the claim's policy, not the caller's choice. A privacy-sensitive claim can be configured to default to the least-revealing mode that still answers the question. + +## What a redacted result still reveals (and to whom) + +"Reveals nothing to the caller" is not the same as "leaves no trace." A `redacted` evaluation can still be pointed back at: it carries an `evaluation_id`, and the audit record carries a `verification_id` and a `claim_hash`. That lets you reference the evaluation that happened — it is not a cryptographic signature on the answer. Redaction shapes what the caller sees; it does not erase the fact that an evaluation happened. + +That trace is by design. Every evaluation — redacted or not — is audited, and a deployment can run audit fail-closed, so that a request whose audit record cannot be written does not succeed. So redaction does not mean an unlogged request; it means an unrevealed answer. + +There is a related protection on the matching surface. By default, a matching failure collapses to a single public reason, `evidence.not_available`, so the lookup cannot be used as an existence oracle — a "not found" answer does not tell the caller whether a record exists. This complements `redacted` disclosure by not leaking record existence. A deployment can turn this off, but only in a controlled environment; it is not always on. + +## Disclosure control is one safeguard among several + +Disclosure modes are one runtime safeguard within a broader posture that also includes authentication, scoped routes, no source mutation, and audit. They are not the whole privacy guarantee. A few protections are not the software's to make at all: secret and key custody, audit retention, tenant isolation, transport, and rate limiting are your deployment's job, not built-in guarantees. + +## Honest limits + +A few boundaries matter when you evaluate this design: + +- **Minimization is conditional, not absolute.** A minimized answer can be narrower than the full source record only where the configured use case supports that pattern. Notary does not always return less than the record — `value` mode returns the full value, and a claim reveals only what its policy was configured to reveal. +- **Governance depends on configured policy.** A source binding with no matching policy falls back to unrestricted, identifier-only resolution, with no purpose, relationship, or input-minimization gating. Minimization is not automatic; it follows the configuration. +- **The caller cannot inject a value, but matching strictness is configured.** The "computed answer avoids sharing the record" guarantee rests on Notary computing the answer from a subject id and claim id. Identity matching is only as strict as its configured policy. +- **A sidecar token is not a per-subject boundary.** A source adapter sidecar's internal static bearer token is not by itself an end-user, tenant, or subject authorization boundary; the sidecar hop does not enforce per-subject access. +- **Disclosure modes are not credential selective disclosure.** A holder's later choice of which credential fields to present is a separate mechanism from Notary's three evaluation modes. Do not read `value`/`predicate`/`redacted` as the same thing as a wallet holder selectively disclosing credential fields. +- **Aligning with a standard is not conforming to it.** This page describes Notary's own disclosure behaviour. It is not an endorsement of, or conformance to, any outside specification. + +## Related + +- [Trust posture and security guarantees](../trust-posture-and-security-guarantees/) — the full posture this fits into +- [Data minimization and purpose limitation](../data-minimization-and-purpose-limitation/) — the privacy view +- [Known limitations and non-guarantees](../known-limitations/) — where the edges are +- [Records stay home](../records-stay-home/) — what stays inside the institution and what crosses out +- How a claim and its disclosure policy are defined: [RS-DM-CLAIM](../../spec/rs-dm-claim/); disclosure modes, credentials, and federation: [RS-PR-NOTARY](../../spec/rs-pr-notary/) diff --git a/docs/site/src/content/docs/explanation/known-limitations.mdx b/docs/site/src/content/docs/explanation/known-limitations.mdx new file mode 100644 index 00000000..0fe96e48 --- /dev/null +++ b/docs/site/src/content/docs/explanation/known-limitations.mdx @@ -0,0 +1,119 @@ +--- +title: Known limitations and non-guarantees +description: The canonical, linked inventory of what Registry Stack does not yet do or guarantee, so you know exactly where the boundaries are before committing. +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 + - cccev + - verifiable-credentials + - odrl +--- + +You already know what Registry Stack is for: answer a question about a record without sharing the underlying record. This page answers a different question — *what does Registry Stack not yet do or guarantee?* — by giving you, in one place, the honest list of where the boundaries are. + +## What this page is and how to use it + +This is the canonical inventory of current limitations and non-guarantees across the whole stack. Each entry states the boundary plainly and links to where it is discussed in depth, so the rest of the documentation can point here whenever it says "this is not X." + +The limitations are not invented for this page. Each protocol and data-model specification carries its own limitations section; this hub collects those and links to them so you can read any one in full context. + +Three things are out of scope here, and each is a link instead: + +- **Why a given limit exists** — read the relevant specification. +- **Whether it will change** — see the project roadmap (`ROADMAP.md` in the repository root). This page describes current behavior only. +- **How to work around a limit** — mitigations are not covered here. Follow the linked specification for each boundary, and see `release/VERIFY.md` in the repository for release verification. + +Use this page as a checklist before you commit. If a limit below is a dealbreaker for your deployment, better to find that out before integrating than after. + +## Everything here is draft + +Every governing specification — architecture, security, protocol, and data-model — is currently at lifecycle status `draft`: under development or review, not `current` (in force). The contracts these limitations are stated against are themselves not yet finalized. + +Registry Stack is a pre-1.0 technical release for evaluation, integration pilots, and public review. It is not a production support commitment and carries no hosted service-level agreement. + +Two things follow, and both matter as you read the rest of the page. First, the behaviors and boundaries here may change as the specifications move toward `current`. Second, aligning with an external standard is not the same as conforming to it: speaking the shape of OIDC, OAuth 2.0, SD-JWT VC, OID4VCI, CCCEV, or the rest does not certify conformance to any of them. The [standards register](../../reference/standards/) records how each one is actually adopted. + +## Stack-wide boundaries at a glance + +These boundaries cut across components. Read them first. + +- **No source mutation, no event stream.** Registry Relay does not write back to or otherwise mutate source registry data, and it has no event-stream backend; sources are read as batch snapshots or table scans only. Registry Notary likewise has no file-based or database connector — a source connector reaches its target over HTTP, so the connector-to-target hop is HTTP-only. +- **Aggregate routes are not a privacy budget.** Relay aggregate routes return the configured statistical observations, but provide no built-in longitudinal privacy budget and do not track cumulative disclosure across repeated or overlapping queries. Do not describe an aggregate route as privacy-budgeted unless a separate deployed control provides that protection. +- **A published manifest grants nothing.** Publishing a dataset, policy, evidence offering, or federation relationship in a discovery artifact grants no access, enforces nothing, and asserts no fact about a live record. Federation discovery metadata is not a trust anchor: a published manifest does not bind a consumer to trust a JWKS endpoint, federation API host, or peer identity. Trust bootstrap stays in the consumer's local policy and Notary peer configuration. +- **Registry Lab and its hosted instance are demonstrations, not infrastructure.** The local Registry Lab is a compose-based runnable demonstration that uses fixture credentials and demo-grade configuration and ships no production deployment guidance. Any hosted instance of the lab is likewise a demonstration, not production infrastructure: it carries no uptime or data-retention commitment. + +For the rationale behind each of these, follow the linked specifications in the sections below. + +## Credential issuance and lifecycle limits (Notary) + +Registry Notary issues credentials, but the issuance surface is deliberately narrow. Read the full context in the [Registry Notary protocol](../../spec/rs-pr-notary/). + +- **One credential format, one binding method when binding is enabled.** Issued Registry Notary credentials are [SD-JWT VC](../../reference/standards/) only. Holder binding is a per-credential-profile setting that defaults to off (`mode: none`), in which case the issued credential is unbound; when a profile turns it on (`mode: did`), `did:jwk` is the single supported proof-of-possession method, and no other format or binding method is issued. The self-attestation (wallet) issuance path requires a profile with binding enabled, so a credential issued down that path is always `did:jwk`-bound with proof-of-possession. +- **A profiled issuance subset, not a full issuer.** The [OID4VCI](../../reference/standards/) surface is a scoped self-attestation issuance profile — a profiled subset of Draft 13 using the `dc+sd-jwt` format — not a full OID4VCI issuer and not a claim of general external-wallet interoperability. Discovery declares this explicitly as `openid4vci.support: not_full_issuer` — that is, it announces it is not a full issuer. +- **No delegated-attestation issuance.** Delegated-attestation transaction tokens are rejected by the credential endpoint; this profile issues only for direct self-attestation principals in this version. +- **No revocation, no issuer-metadata discovery endpoint, no erasure workflow.** At this version there is no credential revocation service, no `/.well-known/jwt-vc-issuer` endpoint, and no built-in data-subject erasure workflow. A rotated-out signing key may remain published for verification, which is not revocation. These are documented pilot limitations, recorded in `SECURITY.md`. +- **CCCEV output is a profiled shape, not conformant.** [CCCEV](../../reference/standards/)-shaped output is a profiled subset and is not conformant to CCCEV 2.00. It is consumed by parsing the `@graph` for `cccev:Evidence` nodes. +- **Two signed artifacts, not one.** Notary issues SD-JWT VC credentials (`dc+sd-jwt`, signed with EdDSA/Ed25519 by default — ES256/P-256 is also supported per credential profile — with no W3C [Verifiable Credentials Data Model](../../reference/standards/) JSON-LD envelope). Relay's optional signed response credentials are VCDM 2.0 VC-JWT. These are different artifacts — don't describe one as the other. +- **Admin reload is not implemented.** The standalone Notary admin reload route returns HTTP 501 with code `registry.admin.capability.not_supported` and performs no reload; key and configuration changes require a service restart. + +## Federation limits + +Federation across institutions is static-peer only. Read the full context in the [Registry Notary protocol](../../spec/rs-pr-notary/) and the [security model](../../spec/rs-sec-g/). + +- **Static peers only.** There is no dynamic trust-chain discovery, no shared replay storage, and no federated credential issuance. All three are out of scope for this version. +- **Delegated evaluation returns a result, never a credential.** Across a federation boundary, what crosses is a scoped evaluation result — never an issued credential. + +## Registry Relay limits + +Registry Relay exposes protected, read-only registry routes. Its boundaries are covered in depth in the [Registry Relay protocol](../../spec/rs-pr-relay/). + +- **Read-only, batch or scan.** Relay does not mutate source data in v1 and has no event-stream backend; it reads sources as batch snapshots or table scans only. +- **Feature-gated surfaces.** Several Relay surfaces — the OGC API Features, Records, and EDR adapters, the SP DCI sync adapter, and signed response credentials — are feature-gated and present only in builds configured with the matching Cargo feature. Do not infer their presence in one build from another build's route catalog. +- **Aggregates are not privacy-budgeted.** As noted stack-wide, Relay aggregate routes provide no longitudinal privacy-budget or cumulative-differencing protection. +- **Relay does not evaluate or own the manifest.** Relay does not perform claim evaluation or issue credentials — that is Notary's role — and it does not own or version the metadata manifest format, which belongs to Registry Manifest. Relay only serves and scopes compiled artifacts. +- **Governed enforcement is one PDP profile, not full interoperability.** Governed runtime policy enforcement covers only the supported Evidence Gateway PDP profile (`registry-evidence-gateway-pdp/v1` — the single PDP profile this build recognizes). It is not full Evidence Gateway interoperability, not full OID4VCI issuer behavior, not dynamic external policy discovery, and not enforcement of [ODRL](../../reference/standards/) terms outside the supported set. + +## Data-model limits (Claim and Manifest) + +These limits concern what the claim and manifest configuration loaders do and do not enforce. Read them in full in the [Claim data model](../../spec/rs-dm-claim/) and [Manifest data model](../../spec/rs-dm-manifest/). + +- **The `plugin` rule type is unimplemented.** The `plugin` rule type is declared in configuration but has no evaluation implementation; a conforming claim must not depend on it. +- **Load-time invariants are not all enforced.** The claim configuration loader does not reject duplicate claim ids, does not verify that the disclosure default is within the allowed set, and does not verify that a rule's source names a declared binding. These surface as request or evaluation errors at runtime, not as startup failures. +- **Identifier-only matching has no further gating.** A source binding with no matching policy resolves on identifiers alone, with no purpose, relationship, or input-minimization gating. +- **Manifests describe; they do not enforce.** The portable metadata layer only describes — publishing in a discovery artifact grants no access and asserts no fact about a live record. Trust bootstrap stays in the consumer's local policy and Notary peer configuration. +- **Rendered standards artifacts are well-formed, not certified.** Manifest rendering emits standards-shaped artifacts but does not validate them against external standard bodies: a rendered CPSV-AP, DCAT, or SHACL document is well-formed by construction, not certified against the standard. +- **Unknown manifest keys are ignored, not rejected.** Manifest runtime-binding exclusion is an enumerated list, not a deny-unknown-fields schema: an unrecognized key outside the runtime-only list is ignored rather than rejected. +- **Vocabulary influence is not vocabulary emission.** PROV-O is a design influence only: provenance-shaped concepts appear in audit fields and the claim provenance struct, but no PROV-O vocabulary terms are emitted as JSON-LD. No standalone SKOS artifact is published yet — only embedded SKOS-shaped nodes — and CPSV-AP has no Relay runtime metadata route. + +## Security-model operator boundary + +A healthy, reachable, internally consistent deployment is not the same as a production-secure one. Read the full security boundary in the [security model](../../spec/rs-sec-g/). + +- **Health checks do not certify key custody.** Readiness, liveness, and protocol-conformance checks do not certify production-grade private-key custody. A deployment using software keys, local JWK files, or demo-generated keys can be reachable and internally consistent yet not production-secure. Custody, rotation, and key-provider approval remain operator responsibilities. +- **Many guarantees are the operator's to provide.** Secret and key provisioning, key custody and rotation schedule, audit retention and storage, tenant isolation, transport termination, edge rate limiting, deployment configuration, and incident response are operator responsibilities — not behaviors the stack guarantees. +- **Audit fail-closed is a capability, not a guarantee about every build.** A deployment can run audit fail-closed, so that a request whose audit record cannot be written does not return success. That is a switch a deployment is able to turn on — not a promise that every route in a given build has been individually audited, nor that every build ships with it on. +- **The Notary audit record omits the scopes field.** The security model says an audit record captures at least the caller principal, the scopes exercised, a request identifier, and the purpose. Notary's audit record body records the principal, request id, and purpose, but does not include the caller's exercised scopes — so a downstream review of Notary audit alone cannot reconstruct which scopes authorized a request. (Relay's audit record does record scopes.) +- **The source-adapter sidecar is an internal connector, not a public API.** The source adapter sidecar is an internal connector surface, not a public subject-facing API. Its static bearer token is not by itself a tenant or subject authorization boundary, and it relies on deployment-network egress controls for outbound source traffic. Direct public access, broad reuse of one sidecar token across unrelated sources, or shared cache or backoff state across authorization contexts is outside the security model. +- **Demo and template code is not a production profile.** Demo helper code and generated workflow snippets are integration examples, not a production freshness or replay-protection profile, and must not be relied on for freshness, expiry, or replay protection. + +## Supply-chain and release limits + +These concern how releases are signed and what that signing currently covers. + +- **Partial release signing.** GitHub Release assets are signed with keyless cosign and, when tag-triggered, carry SLSA provenance. However, OCI image signatures are not yet published, and Git version tags are not yet cryptographically signed (GPG, SSH, or Sigstore). The `v0.8.0` prerelease was published before release-asset signing and does not currently include cosign signatures. + +For how to verify what *is* signed, see the release verification guidance in `release/VERIFY.md` in the repository. + +## Where to go next + +- **Why a given limit exists** — the rationale and trade-offs live in each specification's own limitations section: [RS-PR-NOTARY](../../spec/rs-pr-notary/), [RS-PR-RELAY](../../spec/rs-pr-relay/), [RS-DM-CLAIM](../../spec/rs-dm-claim/), and [RS-DM-MANIFEST](../../spec/rs-dm-manifest/). For the security and architecture boundaries, read [RS-SEC-G](../../spec/rs-sec-g/) and [RS-ARC-G](../../spec/rs-arc-g/). +- **How each external standard is actually adopted** — the [standards register](../../reference/standards/) records the adoption mode for every standard named across the stack. +- **What might change** — the project roadmap (`ROADMAP.md` in the repository root). This page does not track it. +- **How to verify what is signed** — release verification lives in `release/VERIFY.md` in the repository. diff --git a/docs/site/src/content/docs/explanation/threat-model.mdx b/docs/site/src/content/docs/explanation/threat-model.mdx new file mode 100644 index 00000000..748ca9bb --- /dev/null +++ b/docs/site/src/content/docs/explanation/threat-model.mdx @@ -0,0 +1,246 @@ +--- +title: Threat model +description: The trust boundaries, assets, and threats the Registry Stack design considers — what it mitigates, and the residual risks it leaves to the operator or treats as out of scope. +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 + - odrl +--- + +This page is for a reviewer or auditor who already reasons in terms of assets, trust +boundaries, and adversaries, but does not yet know where the Registry Stack's boundaries +actually sit. It answers one question: **why does this design produce the security +properties it claims — which threats does the architecture consider, what does it actually +mitigate, and where do the residual risks lie?** + +It is deliberately a map of boundaries and limits, not a runbook. Hardening procedures +belong to the Operate section, the high-level posture summary to the Trust & Security +overview, and privacy obligations to [Data minimization and purpose limitation](../data-minimization-and-purpose-limitation/). Here the goal is a +defensible threat model: the boundaries the design draws, the threats it places in and out +of scope, and the residual risks stated honestly. + +## The two-layer architecture and its primary trust boundary + +The stack is a two-layer design, and the split between the layers is the first trust +boundary the model relies on. A **portable metadata layer** only *describes*: it carries no +production data, no authentication, and no secrets. A **runtime services layer** *enforces*: +it is where authentication, authorization, and disclosure actually happen. + +The consequence for a reviewer is that the metadata layer authorizes nothing, enforces +nothing, and does not assert that any record exists or satisfies a claim. Publishing a +dataset, a policy, an offering, or a federation relationship does not grant access and is +not proof that a record exists; a published ODRL policy only describes intent until a +runtime service binds it into an enforced profile it supports. This also closes one threat +directly: because the metadata layer is meant to be distributed and inspected, secrets are +never embedded in it. The operator injects them at deployment, which keeps secret material +out of the portable artifact entirely. + +## Components and where they sit on the boundaries + +Five components sit on or beside the trust boundaries: + +- **Registry Manifest** — the offline metadata producer. No production data, no auth, no + secrets. It lives entirely on the describe side of the primary boundary. +- **Registry Relay** — a read-only consultation gateway over sources. +- **Registry Notary** — claim evaluation, credential issuance, and federation. This is the + component that evaluates a modelled question, applies disclosure policy, and issues + credentials. +- **Registry Platform** — the shared security primitives that the runtime services build on. +- **Registry Lab** — demonstration only, running on fixture credentials. Treat it as out of + scope for production trust: its demo and template integrations are integration examples, + not a production freshness or replay-protection profile, and a team copying them into + production must add request freshness, expiry, or nonce checks itself. + +Security-critical primitives are concentrated in Registry Platform so that the behavior is +identical across services and reviewable in one place. This is a deliberate attack-surface +decision: it removes the risk of divergent, per-service security code that would each need +auditing separately. + +## Trust boundaries in detail + +**The service edge (authentication).** Authentication is the trust boundary at the service +edge. Each service runs exactly one authentication mode — either static-credential +fingerprint or OIDC — and authenticates every claim- or record-bearing route before +responding. The only unauthenticated surfaces are probes and public JWKS / `did:web` +discovery. + +**Notary to its sources.** A runtime boundary sits between Registry Notary and the sources +it reads. Notary calls Relay over HTTP as a data source; it does not link Relay code. The +only connectors are HTTP (`registry_data_api`, `dci`, and the source adapter sidecar) — there +is no file or database connector. + +**The source adapter sidecar.** The sidecar is an *internal* trust boundary, not a +subject-authorization boundary, and this distinction matters for an auditor. Its bearer +token authenticates only the Notary-to-sidecar hop; it is not an end-user, tenant, or +subject authorization boundary. It is an internal, private deployment surface behind Notary, +not a public subject-facing API. Keeping the token off the public network and scoping its +credentials, cache, and backoff state is outside the security model — it is an operator +responsibility — and shared cache or backoff state across authorization contexts is not +something the model reasons about. + +**Federation between two Notary services.** Federation is a deliberate trust boundary +between two Notary services, and it is **static-peer only**: only configured static peers are +admitted. It is not a dynamic trust mesh. + +**Delegated self-attestation.** Delegated self-attestation is a distinct trust context from +peer federation — don't confuse the two. It does not delegate trust to a peer +Notary; it stays inside the citizen / OIDC trust context. + +## Assets protected by the design + +The assets the design sets out to protect are: + +- **Private signing keys.** The private signing key never leaves the issuer; only the public + half is published — through JWKS, and through `did:web` in Relay gateway mode. A verifier + never needs a credential to verify, which keeps verification off the protected side + entirely. +- **Person-level source data.** The source rows behind a claim are read in place and never + cross the boundary; only a computed, disclosure-shaped answer does. +- **Audit integrity.** Audit is treated as a security control rather than best-effort + logging (see below). +- **Secret material.** Bearer tokens, raw credentials, and key material are kept out of both + the portable layer and client-facing surfaces. + +The project itself treats a specific set of threat classes as security-relevant, which is a +useful framing for "threats considered": authentication bypass, credential disclosure, +audit redaction or integrity failure, signing-key handling bugs, source-connector data +leakage, and privacy regressions that expose raw subject identifiers. + +## Threats the design considers and mitigates + +- **Secret disclosure via stored credentials or timing.** Static credentials are stored only + as SHA-256 fingerprints — never the raw secret — and compared in constant time. +- **Token forgery / acceptance of untrusted tokens (OIDC mode).** A token is trusted only + after signature verification against the configured issuer JWKS, plus issuer, audience, and + algorithm checks. The service still owns its route scopes regardless of the token. +- **Privilege escalation / over-reach.** Authorization is scope-based, deny-by-default, and + scope-before-source: a caller is refused before any source read or claim evaluation if it + lacks the required scope, and reach is never widened at request time. +- **An existence / matching oracle.** By default a matching failure collapses to a single + public reason (`evidence.not_available`), with the granular reason kept only in audit, so + the matching surface cannot be used to probe whether a record exists. A deployment may + disable this collapse only in a controlled environment; disabling it is a residual + confidentiality risk. +- **Information leakage in error and cache surfaces.** `problem+json` error bodies carry + stable codes only. Bearer tokens, private keys, source values, filesystem paths, and + internal error chains stay in protected operator logs, never in responses; principal-scoped + responses are not public cache entries. +- **Over-collection and value injection in claim evaluation.** The caller supplies only a + subject id and a claim id and must not supply the evaluated value; bindings read only the + fields they need and reject input paths outside a declared allow-list. +- **Disclosure leakage.** A redacted result carries neither the value nor the predicate + outcome. Selective-disclosure credentials carry SHA-256 digests of unselected fields, so a + holder cannot present an undisclosed field. Holder binding is configurable per credential + profile and defaults to off; where a profile enables it (using `did:jwk`) — and on the + self-attestation issuance path, which requires it — the holder is bound by a fresh + audience-bound proof-of-possession. Binding applies only to an issued credential; the plain + evaluation result is not a credential and is never holder-bound. +- **Self-attested data crossing a trust gate.** Self-attestation and delegated + self-attestation derive subject, requester, and relationship from the authenticated + principal and scoped authorization details; caller-supplied `requester`, `relationship`, and + `on_behalf_of` fields are rejected before any source read, and self-asserted target fields do + not satisfy trusted-principal gates. +- **Spoofed trust context and ungoverned reads (Relay side).** Relay ignores trust-context + headers unless the principal is scoped to assert that exact value, and a governed read must + receive a PDP permit or fail closed with a stable `pdp.*` code rather than falling back to an + ungoverned read. +- **SSRF / uncontrolled egress.** Outbound source fetches are meant to be constrained by Registry + Platform's outbound-HTTP-policy. Where it is applied, it limits the destinations a source + connector may reach. This is recommended posture rather than a hard guarantee — confirm it + is enabled rather than assuming it. One detail an auditor should note: the guard exposes an + `allow_insecure_private_network` opt-in that, per source connection, re-enables plain-HTTP + private-network and link-local destinations the strict policy would otherwise refuse. Even + with that opt-in, cloud-metadata addresses (such as `169.254.169.254` and its IPv6 + equivalent) stay denied. Treat the opt-in as a deployment decision to review, not a default. +- **Untrusted federation peers, replay, and wrong-purpose requests.** Only configured static + peers are admitted, and before any source read the server verifies peer identity, request + signature, freshness, single use, purpose, profile, and audience. The single-use and + freshness checks are backed by a real replay-store primitive that tracks one-time JWT `jti` + and nonce values (federation request `jti`, OID4VCI `c_nonce`, and holder-proof `jti`). + +**Audit as a control.** Every request touching person-level data is recorded — principal, +request id, and `Data-Purpose` — and a deployment can run audit fail-closed, so an +unrecordable request does not return success. Two caveats for the auditor. First, this is a +capability a deployment can turn on, not a guarantee that every route in a given build has +been individually audited; treat per-route audit coverage as something to verify in your +deployment. Second, the security model expects the audit record to capture the scopes +exercised as well, but Notary's audit record does not yet include that field — Relay's does — +so Notary audit alone cannot reconstruct which scopes authorized a request. + +## Residual risks and what is left to the operator + +The canonical inventory of every current limit is [Known limitations and non-guarantees](../known-limitations/); the residual risks below are the subset a threat model must weigh. + +These are the risks the design does *not* close and states honestly: + +- **Key custody is not certified by health checks.** Readiness, liveness, and conformance + checks do not certify production-grade private-key custody. A deployment using software + keys, local JWK files, or demo-generated keys can be reachable and internally consistent yet + not production-secure. Custody, rotation, and provider approval remain operator + responsibilities. +- **The operator boundary is where coverage ends.** Secret and key provisioning, key custody + and rotation, audit retention and storage, tenant isolation, TLS termination and + certificates, edge rate limiting, deployment configuration, and incident response are + operator responsibilities, not behavior the model defines. +- **The sidecar depends on deployment-network controls.** The source adapter sidecar relies + on deployment-network egress controls; the model does not enforce them. +- **Aggregate routes are not a privacy budget.** Relay aggregate routes do not constitute a + longitudinal privacy budget; v1 does not track cumulative disclosure across repeated or + overlapping aggregate queries. Do not describe a Relay aggregate as privacy-budgeted unless + a separate deployed control provides it. This is a residual disclosure risk left to the + operator. +- **Some claim-definition invariants surface only at runtime.** Duplicate claim ids, a + disclosure default outside the allowed set, a rule source naming an undeclared binding, and + the unimplemented plugin rule are not enforced at configuration load — they surface only at + evaluation or request time. A source binding with no matching policy resolves on identifiers + alone, with no purpose, relationship, or input-minimization gating. These are residual + misconfiguration risks left to the operator. +- **Known product gaps.** There is no revocation service, no `/.well-known/jwt-vc-issuer` + endpoint, and no built-in data-subject erasure workflow in this version. +- **Admin reload is non-functional standalone.** Notary's admin reload route returns HTTP 501 + (`registry.admin.capability.not_supported`) in the standalone router and performs no + reload; key and configuration changes require a service restart. Do not treat hot reload as + a capability. + +## What is explicitly out of scope + +These are non-goals for this version. None of them should be read into a Registry Stack +conformance claim: + +- **Dynamic federation.** Dynamic trust-chain discovery, shared replay storage, audit + checkpoint exchange, and federated credential issuance are out of scope. Federation is + static-peer only — not a dynamic trust mesh. +- **Full Evidence Gateway / external-policy interoperability.** Governed Relay PDP enforcement + covers only the supported Evidence Gateway PDP profile (`registry-evidence-gateway-pdp/v1`). + It is not full Evidence Gateway interoperability, not full OID4VCI issuer behavior, not + dynamic external policy discovery, and not enforcement of ODRL terms outside the supported + profile. Unsupported ODRL terms or invalid policy identity fail closed. +- **A full credential issuer.** The OID4VCI surface is a scoped self-attestation issuance + subset of OID4VCI Draft 13, not a full issuer or general external-wallet interoperability. + Notary advertises `openid4vci.support: not_full_issuer`, and delegated-attestation + transaction tokens are rejected at the credential endpoint. +- **Certified standards compliance.** Aligning with a standard is not conforming to it: + speaking the shape of OIDC, OAuth 2.0, SD-JWT VC, OID4VCI, CCCEV, W3C DID, or the rest does + not certify conformance to any of them, and CCCEV-shaped output is not conformant to CCCEV + 2.00. +- **Feature-gated surfaces of other builds.** Several runtime surfaces — OGC API + Features/Records/EDR adapters, SP DCI sync, and signed response credentials — are + feature-gated and mount only when the build is configured with the matching Cargo feature. + Do not infer their presence from the route catalog of a different build. Admin routes sit on + a separate optional listener and are not in the public OpenAPI document. + +## Related + +- [RS-SEC-G](../../spec/rs-sec-g/) — the security model +- [RS-PR-NOTARY](../../spec/rs-pr-notary/), [RS-PR-RELAY](../../spec/rs-pr-relay/) — protocol contracts +- [RS-ARC-G](../../spec/rs-arc-g/) — the two-layer architecture +- [RS-DM-CLAIM](../../spec/rs-dm-claim/) — claim definitions and disclosure +- Hardening procedures *(Operate)* · Trust posture summary *(Trust & Security overview)* · [Data minimization and purpose limitation](../data-minimization-and-purpose-limitation/) diff --git a/docs/site/src/content/docs/explanation/trust-posture-and-security-guarantees.mdx b/docs/site/src/content/docs/explanation/trust-posture-and-security-guarantees.mdx new file mode 100644 index 00000000..89232e5a --- /dev/null +++ b/docs/site/src/content/docs/explanation/trust-posture-and-security-guarantees.mdx @@ -0,0 +1,135 @@ +--- +title: Trust posture and security guarantees +description: A plain account of what Registry Stack protects for you, what it recommends you confirm, and what it deliberately leaves to your deployment. +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 + - cccev + - w3c-did + - odrl +--- + +Before you adopt Registry Stack you want a straight answer to three questions: what does it +guarantee about security and privacy, what does it leave for you to get right, and what does +it deliberately *not* do? This page answers all three in plain terms. It is not a substitute +for the specifications — at the end you'll find links to the exact documents where each +property is defined — but it is the honest summary you can read first. + +## At a glance + +| What | Where it stands | +|------|-----------------| +| Authentication on every route that touches a record or a claim | **Built in** | +| Deny-by-default authorization, checked before any data is read | **Built in** | +| Only the public half of a signing key is ever published | **Built in** | +| Person-level access is written to an audit log | **Built in** (fail-closed is yours to switch on) | +| Three disclosure modes, with `redacted` revealing neither value nor answer | **Built in** | +| Selective-disclosure credentials, with holder binding configurable per profile (off by default) | **Built in** | +| Federation limited to peers you configure | **Built in** | +| Hardened HTTP headers and outbound-traffic limits | **Recommended** — confirm your build applies them | +| Key custody, retention, isolation, TLS, rate limiting | **Your deployment's job** | +| Compliance with any external standard it cites | **Not claimed** | + +## What's built in + +**Nothing answers without authenticating first.** Every route that returns a record or a +claim result checks the caller's credential before it does anything else. A service runs one +authentication mode at a time — either API-key fingerprints (it stores a hash, never the raw +key, and compares in constant time) or OIDC tokens (it checks the signature, issuer, audience, +and algorithm). The only routes open to an anonymous caller are health probes and the public +keys a verifier needs to check a signature. + +**Authorization is deny-by-default, and it happens before any data is touched.** A caller has +to hold the scope a route requires, and that check runs *before* Relay reads a source or +Notary evaluates a claim — so a request can never quietly widen its own reach at the last +moment. + +**Access leaves a trail.** Every request that returns person-level data is recorded with who +asked, a request id, and the declared purpose. (Relay's audit record also captures the scopes +exercised; Notary's audit record does not yet include that field — see the limitations hub.) You +can run audit fail-closed, so that if the audit record can't be written the request doesn't +succeed — that's a switch you turn on for a deployment that needs it, not something that's +always on by default. + +**An answer is shaped, not a copy of the record.** Every claim is answered in one of three +modes: `value` hands back the computed value, `predicate` returns only a yes/no, and `redacted` +returns neither the value nor the yes/no. The mode is fixed by the claim's policy, not chosen +by the caller. [Disclosure modes and computed answers](../disclosure-modes-and-computed-answers/) +covers this in depth. + +**Credentials are selective, and can be holder-bound.** When Notary issues a credential it's an +SD-JWT VC: the signed body carries hashes of each disclosable field rather than the values, so +the holder reveals only what they choose. Holder binding is configurable per credential profile +and defaults to off — when a profile enables it (using `did:jwk`), the credential is tied to the +holder's own key and can't be presented by anyone else. The self-attestation (wallet) issuance +path requires binding with proof-of-possession, so credentials issued there are always bound. +Anyone can verify a credential against the issuer's published keys without needing a credential +of their own. The plain claim or evaluation result is not a credential, so it is never +holder-bound — binding applies only to an issued SD-JWT VC. Credentials are signed with +EdDSA/Ed25519 by default, with ES256/P-256 also available per profile. + +**Federation only talks to peers you configured.** There's no automatic trust discovery. Peers +are loaded from configuration at startup, anything else is rejected, and a federated request is +checked for identity, signature, freshness, purpose, and audience before any source is read. +What crosses a federation boundary is a scoped answer — never a credential. + +## What's recommended — and worth confirming + +Two things are strong recommendations rather than hard guarantees, so they're worth a moment in +a review: + +- **Shared security primitives.** Authentication, audit, cryptography, and the credential + helpers are meant to come from one shared component (Registry Platform) so they behave the + same everywhere and can be reviewed in one place. The specs recommend this rather than force + it — so confirm a given service actually does it rather than reimplementing its own. +- **HTTP hardening.** Security headers and limits on outbound traffic are recommended posture. + Check that your build applies them rather than assuming it. + +## Where the guarantees stop + +A trustworthy posture is as honest about its edges as its strengths. The full inventory lives in +[Known limitations and non-guarantees](../known-limitations/); the three that matter most for a +trust review are: + +- **Your deployment owns the operational security.** Key custody and rotation, audit retention, + tenant isolation, TLS, rate limiting, and incident response are yours, not the stack's. A + deployment can pass every health and conformance check and still be running on demo keys — so + passing checks is not the same as production-grade key custody. +- **Aligning with a standard is not conforming to it.** Registry Stack speaks several standards' + shapes — SD-JWT VC, OID4VCI, CCCEV, W3C DID, ODRL — but conformance to a Registry Stack spec + does not imply certified conformance to any of them. The OID4VCI and CCCEV surfaces in + particular are deliberately partial subsets. +- **A published catalog grants nothing.** Listing a dataset or policy in a discovery artifact + describes intent; it doesn't grant access or prove a record exists. Only an authorized runtime + read decides that. + +## Verifying a release, and reading the source + +For checking what you're actually running, GitHub release assets are signed with keyless +Sigstore cosign, and tag-built releases also publish SLSA provenance. You can verify both with +`cosign verify-blob` and `slsa-verifier`; the steps are in `SECURITY.md` and `release/VERIFY.md` +in the repository. (Git tags themselves and container images aren't signed yet.) + +Everything above is defined normatively in the specifications, which are the place to go when +you want to check a claim rather than take it on trust: + +- [RS-SEC-G](../../spec/rs-sec-g/) — the security model (authentication, authorization, audit, + key boundary, the operator boundary) +- [RS-PR-NOTARY](../../spec/rs-pr-notary/) — disclosure modes, credentials, and federation +- [RS-PR-RELAY](../../spec/rs-pr-relay/) — the protected read API +- [RS-DM-CLAIM](../../spec/rs-dm-claim/) — how a claim and its disclosure policy are defined +- [RS-ARC-G](../../spec/rs-arc-g/) — the component boundaries these rest on + +## Related + +- [Records stay home](../records-stay-home/) — what stays inside the institution and what crosses out +- [Threat model](../threat-model/) — the boundaries, assets, and threats behind this posture +- [Data minimization and purpose limitation](../data-minimization-and-purpose-limitation/) — the privacy view +- [Known limitations and non-guarantees](../known-limitations/) — the full list of edges diff --git a/docs/site/src/content/docs/tutorials/publish-spreadsheet-secured-registry-api.mdx b/docs/site/src/content/docs/tutorials/publish-spreadsheet-secured-registry-api.mdx index c62fb655..4cca49ec 100644 --- a/docs/site/src/content/docs/tutorials/publish-spreadsheet-secured-registry-api.mdx +++ b/docs/site/src/content/docs/tutorials/publish-spreadsheet-secured-registry-api.mdx @@ -6,7 +6,7 @@ owner: registry-docs source_repos: - registry-registryctl - registry-relay -last_reviewed: "2026-06-26" +last_reviewed: "2026-06-28" doc_type: tutorial locale: en standards_referenced: @@ -15,9 +15,14 @@ standards_referenced: import QuickstartMeta from '../../../components/QuickstartMeta.astro'; +Registry Relay turns a sensitive tabular source — a spreadsheet or a database table — into a +protected, read-only consultation API, without copying the data out and without you editing a +config file by hand: the CLI writes a working configuration for you. + Use this tutorial to create a local Registry Relay project from a sample Excel workbook. -You will install `registryctl`, start a protected registry API, prove anonymous access is denied, -read spreadsheet data through a secured route, and open the local API reference. +You will install `registryctl`, start a protected registry API, prove an anonymous read is +denied, read spreadsheet data through an authorized request, and confirm that a key without the +right scope is refused. Date: Mon, 29 Jun 2026 06:46:37 +0000 Subject: [PATCH 4/8] docs: correct trust/credential claims against the implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply seven code-grounded accuracy corrections from review of this PR. Each was verified against the notary crates and the generated OpenAPI before editing: - Trust posture: broaden the anonymous-route list — it also includes credential-issuer discovery, the OID4VCI wallet-flow endpoints, the docs, public credential-status reads, and credential type metadata, not only probes and public keys. - Known limitations: qualify the absolute "no credential revocation service" — the RS-* specs define no revocation flow, but an optional, off-by-default credential-status surface can mark a credential revoked; and name the not_full_issuer flag to the /.well-known/evidence-service capability-discovery document rather than the OID4VCI credential-issuer metadata. - Threat model: admin routes ARE documented in the generated OpenAPI (the prior "not in the public OpenAPI document" claim was wrong); standalone mode still returns HTTP 501 for runtime reload. - Data minimization: field minimization is driven by the configured binding.fields (plus lookup/freshness fields), not auto-derived from the rule. - records-stay-home (pilot): the audit-record claim no longer implies Notary records exercised scopes (Relay does; Notary's record does not), and holder binding is qualified as per-profile (default unbound; the wallet path binds). check:style, check:markdown, build, and check:links all pass. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_011U1jTj594nGXhR2r9BzWqm Signed-off-by: Claude --- .../data-minimization-and-purpose-limitation.mdx | 2 +- .../content/docs/explanation/known-limitations.mdx | 4 ++-- .../content/docs/explanation/records-stay-home.mdx | 13 +++++++------ .../src/content/docs/explanation/threat-model.mdx | 5 +++-- .../trust-posture-and-security-guarantees.mdx | 7 +++++-- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx b/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx index 8ab9644b..364c063f 100644 --- a/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx +++ b/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx @@ -22,7 +22,7 @@ A note on terms, since you do not need Registry Stack internals to follow this. The first place minimization appears is in the unit of data the system is built around. A claim definition describes one decision or one extracted value — not a whole record — so returning a full record would over-collect relative to the question actually asked. The design treats the narrow question, not the record, as the thing to be answered. -That principle is enforced one layer down, at the binding between a claim and its source. A source binding reads only the fields its rule needs, and a request that supplies an input path outside a declared allow-list is rejected, so a binding cannot over-collect by accident. The allow-list converts "read only what you need" from an aspiration into a gate that refuses out-of-scope inputs before they reach the source. +That principle is enforced one layer down, at the binding between a claim and its source. A source binding reads the fields it is configured to project — the declared `binding.fields`, plus the lookup and freshness fields — so "read only what the rule needs" depends on the binding being configured that way rather than being automatically derived from the rule. A request that supplies an input path outside a declared allow-list is rejected, so a binding cannot over-collect by accident. The allow-list converts "read only what you need" from an aspiration into a gate that refuses out-of-scope inputs before they reach the source. ## Purpose limitation as an enforced gate, not a label diff --git a/docs/site/src/content/docs/explanation/known-limitations.mdx b/docs/site/src/content/docs/explanation/known-limitations.mdx index 0fe96e48..0822e49c 100644 --- a/docs/site/src/content/docs/explanation/known-limitations.mdx +++ b/docs/site/src/content/docs/explanation/known-limitations.mdx @@ -56,9 +56,9 @@ For the rationale behind each of these, follow the linked specifications in the Registry Notary issues credentials, but the issuance surface is deliberately narrow. Read the full context in the [Registry Notary protocol](../../spec/rs-pr-notary/). - **One credential format, one binding method when binding is enabled.** Issued Registry Notary credentials are [SD-JWT VC](../../reference/standards/) only. Holder binding is a per-credential-profile setting that defaults to off (`mode: none`), in which case the issued credential is unbound; when a profile turns it on (`mode: did`), `did:jwk` is the single supported proof-of-possession method, and no other format or binding method is issued. The self-attestation (wallet) issuance path requires a profile with binding enabled, so a credential issued down that path is always `did:jwk`-bound with proof-of-possession. -- **A profiled issuance subset, not a full issuer.** The [OID4VCI](../../reference/standards/) surface is a scoped self-attestation issuance profile — a profiled subset of Draft 13 using the `dc+sd-jwt` format — not a full OID4VCI issuer and not a claim of general external-wallet interoperability. Discovery declares this explicitly as `openid4vci.support: not_full_issuer` — that is, it announces it is not a full issuer. +- **A profiled issuance subset, not a full issuer.** The [OID4VCI](../../reference/standards/) surface is a scoped self-attestation issuance profile — a profiled subset of Draft 13 using the `dc+sd-jwt` format — not a full OID4VCI issuer and not a claim of general external-wallet interoperability. The capability-discovery document (`/.well-known/evidence-service`) declares `openid4vci.support: not_full_issuer` — announcing it is not a full issuer; this flag lives there rather than in the OID4VCI credential-issuer metadata. - **No delegated-attestation issuance.** Delegated-attestation transaction tokens are rejected by the credential endpoint; this profile issues only for direct self-attestation principals in this version. -- **No revocation, no issuer-metadata discovery endpoint, no erasure workflow.** At this version there is no credential revocation service, no `/.well-known/jwt-vc-issuer` endpoint, and no built-in data-subject erasure workflow. A rotated-out signing key may remain published for verification, which is not revocation. These are documented pilot limitations, recorded in `SECURITY.md`. +- **No revocation flow, no issuer-metadata discovery endpoint, no erasure workflow.** The RS-* specs define no revocation flow. The credential-status surface is optional and off by default; when an operator enables it, a public status endpoint and an admin status endpoint can mark a credential `revoked`. There is no `/.well-known/jwt-vc-issuer` endpoint and no built-in data-subject erasure workflow. A rotated-out signing key may remain published for verification, which is not revocation. These are documented pilot limitations, recorded in `SECURITY.md`. - **CCCEV output is a profiled shape, not conformant.** [CCCEV](../../reference/standards/)-shaped output is a profiled subset and is not conformant to CCCEV 2.00. It is consumed by parsing the `@graph` for `cccev:Evidence` nodes. - **Two signed artifacts, not one.** Notary issues SD-JWT VC credentials (`dc+sd-jwt`, signed with EdDSA/Ed25519 by default — ES256/P-256 is also supported per credential profile — with no W3C [Verifiable Credentials Data Model](../../reference/standards/) JSON-LD envelope). Relay's optional signed response credentials are VCDM 2.0 VC-JWT. These are different artifacts — don't describe one as the other. - **Admin reload is not implemented.** The standalone Notary admin reload route returns HTTP 501 with code `registry.admin.capability.not_supported` and performs no reload; key and configuration changes require a service restart. 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..33ef2b05 100644 --- a/docs/site/src/content/docs/explanation/records-stay-home.mdx +++ b/docs/site/src/content/docs/explanation/records-stay-home.mdx @@ -127,9 +127,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. It **can be** holder-bound — tied to the +holder's key so it is not presentable without the matching private key — when the issuing +profile enables binding (the wallet issuance path does); the default profile 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. @@ -148,9 +149,9 @@ 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 - deployment can run audit fail-closed, so a request whose audit record cannot be written - does not return success. + a request id, and the declared purpose where one was supplied; Relay additionally records + the scopes exercised (Notary's record does not). 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 diff --git a/docs/site/src/content/docs/explanation/threat-model.mdx b/docs/site/src/content/docs/explanation/threat-model.mdx index 748ca9bb..11dc17ca 100644 --- a/docs/site/src/content/docs/explanation/threat-model.mdx +++ b/docs/site/src/content/docs/explanation/threat-model.mdx @@ -234,8 +234,9 @@ conformance claim: - **Feature-gated surfaces of other builds.** Several runtime surfaces — OGC API Features/Records/EDR adapters, SP DCI sync, and signed response credentials — are feature-gated and mount only when the build is configured with the matching Cargo feature. - Do not infer their presence from the route catalog of a different build. Admin routes sit on - a separate optional listener and are not in the public OpenAPI document. + Do not infer their presence from the route catalog of a different build. Admin routes run on + a separate optional listener and are documented in the generated OpenAPI, but standalone mode + does not implement runtime config reload — it returns HTTP 501. ## Related diff --git a/docs/site/src/content/docs/explanation/trust-posture-and-security-guarantees.mdx b/docs/site/src/content/docs/explanation/trust-posture-and-security-guarantees.mdx index 89232e5a..51efa935 100644 --- a/docs/site/src/content/docs/explanation/trust-posture-and-security-guarantees.mdx +++ b/docs/site/src/content/docs/explanation/trust-posture-and-security-guarantees.mdx @@ -43,8 +43,11 @@ property is defined — but it is the honest summary you can read first. claim result checks the caller's credential before it does anything else. A service runs one authentication mode at a time — either API-key fingerprints (it stores a hash, never the raw key, and compares in constant time) or OIDC tokens (it checks the signature, issuer, audience, -and algorithm). The only routes open to an anonymous caller are health probes and the public -keys a verifier needs to check a signature. +and algorithm). The routes open to an anonymous caller are the operational and discovery +surfaces — health and readiness probes, the public verification keys, credential-issuer +discovery and the OID4VCI wallet-flow endpoints, the docs, public credential-status lookups, +and credential type metadata — while everything that returns a record or claim result still +requires a credential. **Authorization is deny-by-default, and it happens before any data is touched.** A caller has to hold the scope a route requires, and that check runs *before* Relay reads a source or From 1f6e72b24488670d8f0aa551c0ec8702c740f7c9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 07:02:13 +0000 Subject: [PATCH 5/8] docs: converge disclosure, revocation, and anonymous-surface accuracy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply nine code-verified corrections so four claim-classes read consistently and accurately across the trust/privacy pages (and the records-stay-home pilot): - Revocation: requalify the absolute "no revocation service" as no spec-defined revocation flow plus an optional, off-by-default credential-status surface (public GET + admin POST status endpoints) that can mark a credential `revoked`. (data-minimization, threat-model, records-stay-home; matches the earlier known-limitations fix.) - Anonymous surface: broaden "only probes + keys" to the full auth-exempt set (probes, public verification keys, credential-issuer discovery, OID4VCI wallet-flow endpoints, docs, public credential-status reads, type metadata), and note the federation route is exempt from the API-key/OIDC middleware but still requires a verified peer-signed JWS, so it is not anonymous. (threat-model, records-stay-home.) - value-mode gradation: keep "exactly three modes, no fourth", but note that within `value` mode an object-valued claim can have configured fields redacted (`redaction_fields`), so `value` is not always all-or-nothing. (disclosure-modes.) - Caller choice: the policy bounds which modes are allowed; within the allowed set the caller can request one (refused with claim.disclosure_not_allowed otherwise), and the default applies when none is requested — corrects the "never the caller's choice" overstatement. (disclosure-modes, trust-posture, records-stay-home.) check:style, check:markdown, build, and check:links all pass. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_011U1jTj594nGXhR2r9BzWqm Signed-off-by: Claude --- ...ta-minimization-and-purpose-limitation.mdx | 2 +- .../disclosure-modes-and-computed-answers.mdx | 4 ++-- .../docs/explanation/records-stay-home.mdx | 24 +++++++++++-------- .../content/docs/explanation/threat-model.mdx | 14 +++++++---- .../trust-posture-and-security-guarantees.mdx | 4 ++-- 5 files changed, 29 insertions(+), 19 deletions(-) diff --git a/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx b/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx index 364c063f..63224aeb 100644 --- a/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx +++ b/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx @@ -62,7 +62,7 @@ Some of the most important things for a DPO to know are the absences. Stating th - **No data-subject erasure or right-to-be-forgotten workflow.** There is no built-in erasure or deletion flow anywhere in the design, so it does not satisfy erasure obligations on its own. As noted above, the read-only design cannot mutate source records, so erasure, where it is required, remains an operation on the source registry outside this system. - **No rectification or data-subject-rights flow.** Beyond erasure, there is no rectification or general data-subject-rights mechanism. -- **No credential revocation service.** There is no revocation flow. Key rotation exists — a rotated-out key may remain published so existing results stay verifiable — but that is not a way to revoke an already-issued credential. Don't count on revocation as a supported capability. +- **No specified revocation flow; an optional status surface.** The specifications define no revocation flow, but the implementation includes an optional, off-by-default credential-status surface — a public `GET /v1/credentials/{id}/status` and an admin `POST /admin/v1/credentials/{id}/status` — that an operator can enable to mark a credential `revoked`. Key rotation exists — a rotated-out key may remain published so existing results stay verifiable — but that is not a way to revoke an already-issued credential. Treat status-based revocation as an operator-enabled capability, not an always-on one. - **No broad cross-authority exchange beyond static peering.** Federation between authorities is static-peer only; dynamic trust-chain discovery, shared replay storage, and federated credential issuance are out of scope, so the design supports a narrower cross-authority data-exchange surface than the word "federation" might suggest. - **No privacy-budgeted analytics.** Aggregate routes produce statistical outputs, not a longitudinal privacy budget. A data-protection impact assessment should not describe an aggregate route as privacy-budgeted unless a separate, deployed control actually provides that. - **No compliance claim.** Conformance to these specifications does not imply conformance to any external standard or any data-protection regulation, and the specifications themselves are draft. diff --git a/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx b/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx index a638153c..db60246c 100644 --- a/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx +++ b/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx @@ -17,7 +17,7 @@ You will see one product term used throughout: a **claim**, which is a single pr ## How Registry Notary controls what leaves the service -Registry Notary is the component that evaluates a claim and decides what the caller receives back. It controls the answer through three **disclosure modes**: `value`, `predicate`, and `redacted`. They are the only modes — there is no fourth mode and no gradation between them. +Registry Notary is the component that evaluates a claim and decides what the caller receives back. It controls the answer through three **disclosure modes**: `value`, `predicate`, and `redacted`. There are exactly three — there is no fourth mode. `value` is not always all-or-nothing, though: when a claim returns an object, `value` mode can have specific fields redacted (configured through `redaction_fields`), and the result carries the object minus those fields plus a list of what was withheld. Per-claim disclosure control is enforced at runtime by the service. It is not left to a caller's good behaviour. @@ -53,7 +53,7 @@ The difference between the two matters. `predicate` is not "zero disclosure" — ## How disclosure policy is configured per claim -The caller does not freely pick a mode. Each claim configures a **disclosure policy**: a `default` mode plus an `allowed` set drawn from the three modes. When a caller requests a mode, Notary refuses any mode not in that claim's allowed set; when the caller requests no mode, Notary applies the claim's default. The mode is bound to the claim's policy, not the caller's choice. A privacy-sensitive claim can be configured to default to the least-revealing mode that still answers the question. +The caller does not freely pick from all three modes. Each claim configures a **disclosure policy**: a `default` mode plus an `allowed` set drawn from the three modes. A caller may request a mode: Notary honours it when it is in that claim's allowed set and refuses it (`claim.disclosure_not_allowed`) otherwise; when the caller requests no mode, Notary applies the claim's default. So the policy bounds which modes are allowed, and the caller chooses among the allowed modes. A privacy-sensitive claim can be configured to default to the least-revealing mode that still answers the question. ## What a redacted result still reveals (and to whom) 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 33ef2b05..fb052b88 100644 --- a/docs/site/src/content/docs/explanation/records-stay-home.mdx +++ b/docs/site/src/content/docs/explanation/records-stay-home.mdx @@ -111,10 +111,11 @@ receives. There are exactly three: | `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. +The policy bounds which modes are allowed, and the caller chooses among them: a claim +defines an `allowed` set and a `default`, the service honours a requested mode when it is in +the allowed set and refuses it otherwise, applies the default when none is requested, 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 @@ -142,9 +143,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. 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 are the operational, discovery, + and protocol-bootstrap surfaces: liveness and readiness probes, the public verification + keys, credential-issuance discovery metadata, the OID4VCI wallet-flow endpoints, the docs, + public credential-status reads, and credential type 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. @@ -177,9 +179,11 @@ mistake, so the limits are stated plainly here. - **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 +- **No revocation flow is specified, and no erasure flow.** This version specifies issuance, + disclosure, presentation, and verification, but no credential-revocation flow and no + data-subject erasure. An optional, off-by-default credential-status surface can be enabled + to mark a credential `revoked`, but that is an operator-enabled capability rather than a + specified flow. 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, diff --git a/docs/site/src/content/docs/explanation/threat-model.mdx b/docs/site/src/content/docs/explanation/threat-model.mdx index 11dc17ca..ad1f1d76 100644 --- a/docs/site/src/content/docs/explanation/threat-model.mdx +++ b/docs/site/src/content/docs/explanation/threat-model.mdx @@ -69,8 +69,12 @@ auditing separately. **The service edge (authentication).** Authentication is the trust boundary at the service edge. Each service runs exactly one authentication mode — either static-credential fingerprint or OIDC — and authenticates every claim- or record-bearing route before -responding. The only unauthenticated surfaces are probes and public JWKS / `did:web` -discovery. +responding. The unauthenticated surfaces are the operational, discovery, and +protocol-bootstrap routes: liveness and readiness probes, the public verification keys +(JWKS / `did:web`), credential-issuer discovery, the OID4VCI wallet-flow endpoints, the +docs, public credential-status reads, and credential type metadata. The federation +evaluations route is exempt from the API-key/OIDC middleware but still requires a verified +peer-signed JWS, so it is not anonymous. **Notary to its sources.** A runtime boundary sits between Registry Notary and the sources it reads. Notary calls Relay over HTTP as a data source; it does not link Relay code. The @@ -203,8 +207,10 @@ These are the risks the design does *not* close and states honestly: evaluation or request time. A source binding with no matching policy resolves on identifiers alone, with no purpose, relationship, or input-minimization gating. These are residual misconfiguration risks left to the operator. -- **Known product gaps.** There is no revocation service, no `/.well-known/jwt-vc-issuer` - endpoint, and no built-in data-subject erasure workflow in this version. +- **Known product gaps.** No revocation flow is specified — though an optional, + off-by-default credential-status surface can be enabled to mark a credential `revoked` — + there is no `/.well-known/jwt-vc-issuer` endpoint, and no built-in data-subject erasure + workflow in this version. - **Admin reload is non-functional standalone.** Notary's admin reload route returns HTTP 501 (`registry.admin.capability.not_supported`) in the standalone router and performs no reload; key and configuration changes require a service restart. Do not treat hot reload as diff --git a/docs/site/src/content/docs/explanation/trust-posture-and-security-guarantees.mdx b/docs/site/src/content/docs/explanation/trust-posture-and-security-guarantees.mdx index 51efa935..9472fb5a 100644 --- a/docs/site/src/content/docs/explanation/trust-posture-and-security-guarantees.mdx +++ b/docs/site/src/content/docs/explanation/trust-posture-and-security-guarantees.mdx @@ -63,8 +63,8 @@ always on by default. **An answer is shaped, not a copy of the record.** Every claim is answered in one of three modes: `value` hands back the computed value, `predicate` returns only a yes/no, and `redacted` -returns neither the value nor the yes/no. The mode is fixed by the claim's policy, not chosen -by the caller. [Disclosure modes and computed answers](../disclosure-modes-and-computed-answers/) +returns neither the value nor the yes/no. The claim's policy fixes which modes are allowed; +within that set the caller can request one, and otherwise the default applies. [Disclosure modes and computed answers](../disclosure-modes-and-computed-answers/) covers this in depth. **Credentials are selective, and can be holder-bound.** When Notary issues a credential it's an From fd06aa9bcaad0d9d3f177e01d4a204c60e0ce4e6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 07:49:34 +0000 Subject: [PATCH 6/8] docs: refine credential-status, delegated-attestation, and disclosure-granularity claims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four code-verified refinements from continued review: - known limitations (revocation): only the admin status endpoint (requires the registry:notary:admin scope) marks a credential revoked; the public status endpoint reads status. Corrected the wording that implied the public endpoint could revoke. - known limitations (delegated attestation): reframed the absolute "no delegated-attestation issuance" — the OID4VCI transaction-token path is rejected, but direct issuance via /v1/credentials is supported when the stored evaluation is delegated-attestation and the relationship allow-lists the profile. - disclosure modes (redacted + CCCEV): note a current renderer limitation — the CCCEV JSON-LD renderer emits cccev:isConformantTo: false for a redacted result instead of omitting it, so the no-outcome guarantee is not fully upheld in that render. (Tracked as a product issue.) - data minimization (SD-JWT granularity): selective disclosure is at the claim (or configured-projection) level — each claim is one disclosure carrying its whole value, so an object-valued claim is revealed as a unit unless an explicit projection splits it. check:style, check:markdown, build, and check:links all pass. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_011U1jTj594nGXhR2r9BzWqm Signed-off-by: Claude --- .../explanation/data-minimization-and-purpose-limitation.mdx | 2 +- .../explanation/disclosure-modes-and-computed-answers.mdx | 2 ++ docs/site/src/content/docs/explanation/known-limitations.mdx | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx b/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx index 63224aeb..706d6fa3 100644 --- a/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx +++ b/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx @@ -38,7 +38,7 @@ Minimization also applies to what leaves the boundary. A claim's result can be s The redacted mode is worth a reviewer's attention because it shows minimization without loss of accountability: a redacted result carries neither the underlying value nor the satisfaction outcome, yet the evaluation stays referenceable and verifiable through an evaluation identifier, a verification identifier, and a claim hash. You can audit that an evaluation happened and verify it later without the result itself disclosing anything about the person. -When the system issues a credential rather than returning a direct answer, minimization continues at presentation time. Issued credentials are SD-JWT VC — a verifiable-credential format in which the signed body carries only a SHA-256 digest of each selectively disclosable field, so a field the holder does not present stays hidden, and the holder controls what is revealed. Holder binding — tying the credential to a holder key with `did:jwk` — is a per-profile setting that defaults to off, though the self-attestation (wallet) issuance path requires it; the direct claim or evaluation result is not a credential and is never holder-bound. One caveat for a reviewer: the issuance surface is a profiled, partial subset, not a full credential-issuer implementation, and should not be read as general wallet interoperability or full standards conformance. +When the system issues a credential rather than returning a direct answer, minimization continues at presentation time. Issued credentials are SD-JWT VC — a verifiable-credential format in which the signed body carries only a SHA-256 digest of each selectively disclosable field, so a field the holder does not present stays hidden, and the holder controls what is revealed. Selective disclosure here is at the claim (or configured-projection) level: by default each claim becomes one disclosure carrying its whole value, so an object-valued claim is revealed as a unit unless an explicit projection splits it into separately-disclosable fields. Holder binding — tying the credential to a holder key with `did:jwk` — is a per-profile setting that defaults to off, though the self-attestation (wallet) issuance path requires it; the direct claim or evaluation result is not a credential and is never holder-bound. One caveat for a reviewer: the issuance surface is a profiled, partial subset, not a full credential-issuer implementation, and should not be read as general wallet interoperability or full standards conformance. A further minimization detail guards against a subtler leak. A failed subject match collapses by default to a single public reason (evidence not available), with the granular reason kept only in the audit record, so the lookup surface cannot by default be used to confirm whether a person exists in a registry. diff --git a/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx b/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx index db60246c..947d2d25 100644 --- a/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx +++ b/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx @@ -49,6 +49,8 @@ This is the core of the privacy story. Because Notary computes the answer from i A `redacted` result goes further: it carries neither the source value nor the satisfaction outcome. Both are withheld. +One current renderer limitation to note: this no-outcome guarantee holds for the standard result body, but the CCCEV JSON-LD renderer currently emits `cccev:isConformantTo: false` for a redacted result instead of omitting it. A deployment that allows both redacted disclosure and CCCEV output should be aware that the outcome is not fully withheld in that render. + The difference between the two matters. `predicate` is not "zero disclosure" — it still tells the caller a true-or-false fact about the subject. `redacted` goes one step further: it does not leak even a yes/no. They are not interchangeable. ## How disclosure policy is configured per claim diff --git a/docs/site/src/content/docs/explanation/known-limitations.mdx b/docs/site/src/content/docs/explanation/known-limitations.mdx index 0822e49c..5505de4f 100644 --- a/docs/site/src/content/docs/explanation/known-limitations.mdx +++ b/docs/site/src/content/docs/explanation/known-limitations.mdx @@ -57,8 +57,8 @@ Registry Notary issues credentials, but the issuance surface is deliberately nar - **One credential format, one binding method when binding is enabled.** Issued Registry Notary credentials are [SD-JWT VC](../../reference/standards/) only. Holder binding is a per-credential-profile setting that defaults to off (`mode: none`), in which case the issued credential is unbound; when a profile turns it on (`mode: did`), `did:jwk` is the single supported proof-of-possession method, and no other format or binding method is issued. The self-attestation (wallet) issuance path requires a profile with binding enabled, so a credential issued down that path is always `did:jwk`-bound with proof-of-possession. - **A profiled issuance subset, not a full issuer.** The [OID4VCI](../../reference/standards/) surface is a scoped self-attestation issuance profile — a profiled subset of Draft 13 using the `dc+sd-jwt` format — not a full OID4VCI issuer and not a claim of general external-wallet interoperability. The capability-discovery document (`/.well-known/evidence-service`) declares `openid4vci.support: not_full_issuer` — announcing it is not a full issuer; this flag lives there rather than in the OID4VCI credential-issuer metadata. -- **No delegated-attestation issuance.** Delegated-attestation transaction tokens are rejected by the credential endpoint; this profile issues only for direct self-attestation principals in this version. -- **No revocation flow, no issuer-metadata discovery endpoint, no erasure workflow.** The RS-* specs define no revocation flow. The credential-status surface is optional and off by default; when an operator enables it, a public status endpoint and an admin status endpoint can mark a credential `revoked`. There is no `/.well-known/jwt-vc-issuer` endpoint and no built-in data-subject erasure workflow. A rotated-out signing key may remain published for verification, which is not revocation. These are documented pilot limitations, recorded in `SECURITY.md`. +- **Delegated-attestation issuance is limited to the direct path.** The OID4VCI transaction-token path for delegated attestation is rejected at the credential endpoint. Direct credential issuance via `/v1/credentials` is supported when the stored evaluation's access mode is delegated-attestation and the relationship allow-lists the credential profile. +- **No revocation flow, no issuer-metadata discovery endpoint, no erasure workflow.** The RS-* specs define no revocation flow. The credential-status surface is optional and off by default; when an operator enables it, the public status endpoint reads a credential's status, and only the admin status endpoint — which requires the `registry:notary:admin` scope — can mark a credential `revoked`. There is no `/.well-known/jwt-vc-issuer` endpoint and no built-in data-subject erasure workflow. A rotated-out signing key may remain published for verification, which is not revocation. These are documented pilot limitations, recorded in `SECURITY.md`. - **CCCEV output is a profiled shape, not conformant.** [CCCEV](../../reference/standards/)-shaped output is a profiled subset and is not conformant to CCCEV 2.00. It is consumed by parsing the `@graph` for `cccev:Evidence` nodes. - **Two signed artifacts, not one.** Notary issues SD-JWT VC credentials (`dc+sd-jwt`, signed with EdDSA/Ed25519 by default — ES256/P-256 is also supported per credential profile — with no W3C [Verifiable Credentials Data Model](../../reference/standards/) JSON-LD envelope). Relay's optional signed response credentials are VCDM 2.0 VC-JWT. These are different artifacts — don't describe one as the other. - **Admin reload is not implemented.** The standalone Notary admin reload route returns HTTP 501 with code `registry.admin.capability.not_supported` and performs no reload; key and configuration changes require a service restart. From 5e51ab03102601cf7d7adacdcb892ee9eabd995e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 08:12:59 +0000 Subject: [PATCH 7/8] docs: qualify absolute limitation claims that are conditional in the implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six code-verified refinements where an absolute "limit" is actually conditional: - data minimization: claim-level purpose constraints (the claim's permitted purposes and the deployment allowed-purposes list) are enforced before a source read even with no binding matching policy; only the binding-level gating is missing in that case. - known limitations (federation): replay protection defaults to in-memory, but a Redis-backed shared replay store is available for multi-instance deployments — "no shared replay storage" was too absolute. - known limitations (Relay): signed response credentials are config-gated via provenance, not Cargo-feature-gated; and with provenance enabled Relay can attach its own VC-JWT signed response credential (it still does not issue Notary's SD-JWT VC credentials). - known limitations (reload): standalone /admin/v1/reload returns 501 and non-swappable changes need a restart, but governed config apply and the file-watch signer hot-apply some changes (signing-key rotation, auth-policy swaps) without one. - threat model + known limitations (duplicate ids): duplicate claim ids are not rejected at load and are never reported — the runtime lookup returns the first match, so a later duplicate is silently shadowed (corrected the earlier "surface at runtime" wording on both pages). check:style, check:markdown, build, and check:links all pass. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_011U1jTj594nGXhR2r9BzWqm Signed-off-by: Claude --- .../data-minimization-and-purpose-limitation.mdx | 2 +- .../content/docs/explanation/known-limitations.mdx | 10 +++++----- .../src/content/docs/explanation/threat-model.mdx | 14 ++++++++------ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx b/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx index 706d6fa3..f9b6daa1 100644 --- a/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx +++ b/docs/site/src/content/docs/explanation/data-minimization-and-purpose-limitation.mdx @@ -71,7 +71,7 @@ Some of the most important things for a DPO to know are the absences. Stating th The minimization and purpose-limitation posture described above is the design default, but the operator owns the configuration. Several of the protections are conditional, and a reviewer should test the deployment, not the design, for each one. -The clearest example is the fallback when no matching policy is configured. With no matching policy in place, a binding falls back to unrestricted, identifier-only resolution with no purpose gating, no relationship gating, and no input minimization. Equivalently, purpose limitation is supported but partial: a purpose is recorded in audit only where the caller supplies one, and is enforced as a hard gate only where a claim or source binding configures a matching policy. The enforced gate described earlier exists only when someone has configured it. +The clearest example is the fallback when no matching policy is configured. With no matching policy in place, a binding skips the binding-level gating — no purpose gating, no relationship gating, and no input minimization — and falls back to identifier-only resolution. Claim-level purpose constraints still apply, though: the claim's own permitted purposes and the deployment's allowed-purposes list are enforced before the source is read, so a claim-purpose mismatch is refused with no source reads. Purpose limitation is therefore not entirely absent without a matching policy; what is missing is the binding-level gating. Equivalently, purpose limitation is supported but partial: a purpose is recorded in audit only where the caller supplies one, and is enforced as a binding-level hard gate only where a claim or source binding configures a matching policy. The enforced gate described earlier exists only when someone has configured it. The existence-oracle protection is also defeasible. The matching surface collapses failures to a single public reason by default, but a deployment may disable that collapse and surface not-found, ambiguous, or rejected outcomes — an over-disclosure risk the operator controls. diff --git a/docs/site/src/content/docs/explanation/known-limitations.mdx b/docs/site/src/content/docs/explanation/known-limitations.mdx index 5505de4f..e4645eb0 100644 --- a/docs/site/src/content/docs/explanation/known-limitations.mdx +++ b/docs/site/src/content/docs/explanation/known-limitations.mdx @@ -61,13 +61,13 @@ Registry Notary issues credentials, but the issuance surface is deliberately nar - **No revocation flow, no issuer-metadata discovery endpoint, no erasure workflow.** The RS-* specs define no revocation flow. The credential-status surface is optional and off by default; when an operator enables it, the public status endpoint reads a credential's status, and only the admin status endpoint — which requires the `registry:notary:admin` scope — can mark a credential `revoked`. There is no `/.well-known/jwt-vc-issuer` endpoint and no built-in data-subject erasure workflow. A rotated-out signing key may remain published for verification, which is not revocation. These are documented pilot limitations, recorded in `SECURITY.md`. - **CCCEV output is a profiled shape, not conformant.** [CCCEV](../../reference/standards/)-shaped output is a profiled subset and is not conformant to CCCEV 2.00. It is consumed by parsing the `@graph` for `cccev:Evidence` nodes. - **Two signed artifacts, not one.** Notary issues SD-JWT VC credentials (`dc+sd-jwt`, signed with EdDSA/Ed25519 by default — ES256/P-256 is also supported per credential profile — with no W3C [Verifiable Credentials Data Model](../../reference/standards/) JSON-LD envelope). Relay's optional signed response credentials are VCDM 2.0 VC-JWT. These are different artifacts — don't describe one as the other. -- **Admin reload is not implemented.** The standalone Notary admin reload route returns HTTP 501 with code `registry.admin.capability.not_supported` and performs no reload; key and configuration changes require a service restart. +- **Standalone admin reload is not implemented.** The standalone Notary `/admin/v1/reload` route returns HTTP 501 with code `registry.admin.capability.not_supported` and performs no reload, and non-swappable changes require a service restart. Governed config apply and the file-watch signer do hot-apply some changes without one — signing-key rotation reports `restart_required: false`, client and OpenAPI auth-policy changes are swapped in, and a same-public-key signer replacement reloads — so not every key or configuration change needs a restart. ## Federation limits Federation across institutions is static-peer only. Read the full context in the [Registry Notary protocol](../../spec/rs-pr-notary/) and the [security model](../../spec/rs-sec-g/). -- **Static peers only.** There is no dynamic trust-chain discovery, no shared replay storage, and no federated credential issuance. All three are out of scope for this version. +- **Static peers only.** Replay protection defaults to in-process, in-memory state, but a Redis-backed shared replay store is available for multi-instance deployments (`replay.storage = redis`, with `federation.replay.storage = redis` when the top-level store is Redis), and federation uses it for request replay and `jti` checks. There is still no dynamic trust-chain discovery and no federated credential issuance; both are out of scope for this version. - **Delegated evaluation returns a result, never a credential.** Across a federation boundary, what crosses is a scoped evaluation result — never an issued credential. ## Registry Relay limits @@ -75,9 +75,9 @@ Federation across institutions is static-peer only. Read the full context in the Registry Relay exposes protected, read-only registry routes. Its boundaries are covered in depth in the [Registry Relay protocol](../../spec/rs-pr-relay/). - **Read-only, batch or scan.** Relay does not mutate source data in v1 and has no event-stream backend; it reads sources as batch snapshots or table scans only. -- **Feature-gated surfaces.** Several Relay surfaces — the OGC API Features, Records, and EDR adapters, the SP DCI sync adapter, and signed response credentials — are feature-gated and present only in builds configured with the matching Cargo feature. Do not infer their presence in one build from another build's route catalog. +- **Feature-gated surfaces.** Several Relay surfaces — the OGC API Features, Records, and EDR adapters and the SP DCI sync adapter — are feature-gated and present only in builds configured with the matching Cargo feature. Do not infer their presence in one build from another build's route catalog. (Signed response credentials are not Cargo-gated; they are config-gated through `provenance`.) - **Aggregates are not privacy-budgeted.** As noted stack-wide, Relay aggregate routes provide no longitudinal privacy-budget or cumulative-differencing protection. -- **Relay does not evaluate or own the manifest.** Relay does not perform claim evaluation or issue credentials — that is Notary's role — and it does not own or version the metadata manifest format, which belongs to Registry Manifest. Relay only serves and scopes compiled artifacts. +- **Relay does not evaluate or own the manifest.** Relay does not evaluate claims or issue Notary's SD-JWT VC credentials — that is Notary's role — though with `provenance` enabled and a caller requesting `Accept: application/vc+jwt` it can attach its own VC-JWT signed response credential. It also does not own or version the metadata manifest format, which belongs to Registry Manifest. Relay only serves and scopes compiled artifacts. - **Governed enforcement is one PDP profile, not full interoperability.** Governed runtime policy enforcement covers only the supported Evidence Gateway PDP profile (`registry-evidence-gateway-pdp/v1` — the single PDP profile this build recognizes). It is not full Evidence Gateway interoperability, not full OID4VCI issuer behavior, not dynamic external policy discovery, and not enforcement of [ODRL](../../reference/standards/) terms outside the supported set. ## Data-model limits (Claim and Manifest) @@ -85,7 +85,7 @@ Registry Relay exposes protected, read-only registry routes. Its boundaries are These limits concern what the claim and manifest configuration loaders do and do not enforce. Read them in full in the [Claim data model](../../spec/rs-dm-claim/) and [Manifest data model](../../spec/rs-dm-manifest/). - **The `plugin` rule type is unimplemented.** The `plugin` rule type is declared in configuration but has no evaluation implementation; a conforming claim must not depend on it. -- **Load-time invariants are not all enforced.** The claim configuration loader does not reject duplicate claim ids, does not verify that the disclosure default is within the allowed set, and does not verify that a rule's source names a declared binding. These surface as request or evaluation errors at runtime, not as startup failures. +- **Load-time invariants are not all enforced.** The claim configuration loader does not reject duplicate claim ids, does not verify that the disclosure default is within the allowed set, and does not verify that a rule's source names a declared binding. A disclosure default outside the allowed set and a dangling rule source surface as request or evaluation errors at runtime, not as startup failures. Duplicate claim ids are different: they are never reported, because the runtime lookup returns the first match by id, so a later duplicate is silently shadowed. - **Identifier-only matching has no further gating.** A source binding with no matching policy resolves on identifiers alone, with no purpose, relationship, or input-minimization gating. - **Manifests describe; they do not enforce.** The portable metadata layer only describes — publishing in a discovery artifact grants no access and asserts no fact about a live record. Trust bootstrap stays in the consumer's local policy and Notary peer configuration. - **Rendered standards artifacts are well-formed, not certified.** Manifest rendering emits standards-shaped artifacts but does not validate them against external standard bodies: a rendered CPSV-AP, DCAT, or SHACL document is well-formed by construction, not certified against the standard. diff --git a/docs/site/src/content/docs/explanation/threat-model.mdx b/docs/site/src/content/docs/explanation/threat-model.mdx index ad1f1d76..1d495ca8 100644 --- a/docs/site/src/content/docs/explanation/threat-model.mdx +++ b/docs/site/src/content/docs/explanation/threat-model.mdx @@ -201,12 +201,14 @@ These are the risks the design does *not* close and states honestly: overlapping aggregate queries. Do not describe a Relay aggregate as privacy-budgeted unless a separate deployed control provides it. This is a residual disclosure risk left to the operator. -- **Some claim-definition invariants surface only at runtime.** Duplicate claim ids, a - disclosure default outside the allowed set, a rule source naming an undeclared binding, and - the unimplemented plugin rule are not enforced at configuration load — they surface only at - evaluation or request time. A source binding with no matching policy resolves on identifiers - alone, with no purpose, relationship, or input-minimization gating. These are residual - misconfiguration risks left to the operator. +- **Some claim-definition invariants surface only at runtime.** A disclosure default outside + the allowed set, a rule source naming an undeclared binding, and the unimplemented plugin + rule are not enforced at configuration load — they surface only at evaluation or request + time. Duplicate claim ids are different and worse: they are not rejected at load and are + never reported, because the runtime lookup returns the first match by id, so a later + duplicate is silently shadowed. A source binding with no matching policy resolves on + identifiers alone, with no purpose, relationship, or input-minimization gating. These are + residual misconfiguration risks left to the operator. - **Known product gaps.** No revocation flow is specified — though an optional, off-by-default credential-status surface can be enabled to mark a credential `revoked` — there is no `/.well-known/jwt-vc-issuer` endpoint, and no built-in data-subject erasure From 1ecd22f2a6741a7ab0e4875cb7a2d97136a7d31d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 29 Jun 2026 08:31:27 +0000 Subject: [PATCH 8/8] docs: snapshot-cache + exists-not_available caveats; align threat model with limitations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new code-verified corrections plus three threat-model alignments: - records-stay-home (read-in-place): in snapshot mode (the default) Relay materializes a projected copy into its local cache (cache_dir). There is still no write-back to the source and no external handoff, but a cached copy exists, with its own retention/access considerations — distinguished from "no copy". - disclosure modes (exists/predicate): an `exists` claim returns `true` when a record matches, but a subject with no matching record returns `evidence.not_available` (collapsed to a single public reason by default), not a bare `false` — absence is no-evidence, not a negative result. - threat model: align with the limitations page on three points the threat model still stated absolutely — shared replay storage is supported (Redis), Relay signed response credentials are config-gated via provenance (not Cargo-gated), and some config/key changes hot-apply without a restart. check:style, check:markdown, build, and check:links all pass. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_011U1jTj594nGXhR2r9BzWqm Signed-off-by: Claude --- .../disclosure-modes-and-computed-answers.mdx | 2 +- .../docs/explanation/records-stay-home.mdx | 5 ++++- .../content/docs/explanation/threat-model.mdx | 20 ++++++++++--------- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx b/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx index 947d2d25..fa788014 100644 --- a/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx +++ b/docs/site/src/content/docs/explanation/disclosure-modes-and-computed-answers.mdx @@ -45,7 +45,7 @@ Every evaluation result records which disclosure mode was applied, so a downstre ## Why predicate and redacted avoid sharing the underlying record -This is the core of the privacy story. Because Notary computes the answer from its own sources, a `predicate` result can satisfy a question while disclosing only a boolean. A question of whether someone has a registered record is modelled as an `exists` rule disclosed as `predicate`: the caller learns `true` or `false`, and the source row never leaves the service. A question about an eligibility threshold without exposing the figure behind it is modelled as a `cel` rule whose eligibility boolean is disclosed as `predicate`; the underlying value stays inside the service. +This is the core of the privacy story. Because Notary computes the answer from its own sources, a `predicate` result can satisfy a question while disclosing only a boolean. A question of whether someone has a registered record is modelled as an `exists` rule disclosed as `predicate`: the caller learns `true` when a matching record is found, while a subject with no matching record returns `evidence.not_available` (collapsed to a single public reason by default) rather than `false` — absence is surfaced as no-evidence, not as a negative result. Either way the source row never leaves the service. A question about an eligibility threshold without exposing the figure behind it is modelled as a `cel` rule whose eligibility boolean is disclosed as `predicate`; the underlying value stays inside the service. A `redacted` result goes further: it carries neither the source value nor the satisfaction outcome. Both are withheld. 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 fb052b88..b6db6e6f 100644 --- a/docs/site/src/content/docs/explanation/records-stay-home.mdx +++ b/docs/site/src/content/docs/explanation/records-stay-home.mdx @@ -70,7 +70,10 @@ disclosure policy, and issues credentials. - **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. + data-mutation routes. The source keeps running as it always has. "No write-back to the + source and no external handoff" is not the same as "no copy exists": in snapshot mode — + the default — Relay materializes a projected copy into its local cache (`cache_dir`), which + carries its own retention and access considerations. - **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 diff --git a/docs/site/src/content/docs/explanation/threat-model.mdx b/docs/site/src/content/docs/explanation/threat-model.mdx index 1d495ca8..3947d7f9 100644 --- a/docs/site/src/content/docs/explanation/threat-model.mdx +++ b/docs/site/src/content/docs/explanation/threat-model.mdx @@ -215,17 +215,18 @@ These are the risks the design does *not* close and states honestly: workflow in this version. - **Admin reload is non-functional standalone.** Notary's admin reload route returns HTTP 501 (`registry.admin.capability.not_supported`) in the standalone router and performs no - reload; key and configuration changes require a service restart. Do not treat hot reload as - a capability. + reload, and non-swappable changes require a service restart. Some changes do hot-apply + without a restart, though — signing-key rotation and auth-policy swaps among them. ## What is explicitly out of scope These are non-goals for this version. None of them should be read into a Registry Stack conformance claim: -- **Dynamic federation.** Dynamic trust-chain discovery, shared replay storage, audit - checkpoint exchange, and federated credential issuance are out of scope. Federation is - static-peer only — not a dynamic trust mesh. +- **Dynamic federation.** Dynamic trust-chain discovery, audit checkpoint exchange, and + federated credential issuance are out of scope. Federation is static-peer only — not a + dynamic trust mesh. Replay protection defaults to in-memory, with a Redis-backed shared + store available. - **Full Evidence Gateway / external-policy interoperability.** Governed Relay PDP enforcement covers only the supported Evidence Gateway PDP profile (`registry-evidence-gateway-pdp/v1`). It is not full Evidence Gateway interoperability, not full OID4VCI issuer behavior, not @@ -239,10 +240,11 @@ conformance claim: speaking the shape of OIDC, OAuth 2.0, SD-JWT VC, OID4VCI, CCCEV, W3C DID, or the rest does not certify conformance to any of them, and CCCEV-shaped output is not conformant to CCCEV 2.00. -- **Feature-gated surfaces of other builds.** Several runtime surfaces — OGC API - Features/Records/EDR adapters, SP DCI sync, and signed response credentials — are - feature-gated and mount only when the build is configured with the matching Cargo feature. - Do not infer their presence from the route catalog of a different build. Admin routes run on +- **Feature-gated surfaces of other builds.** Several runtime surfaces — the OGC API + Features/Records/EDR adapters and SP DCI sync — are feature-gated and mount only when the + build is configured with the matching Cargo feature. Do not infer their presence from the + route catalog of a different build. Relay signed response credentials are not Cargo-gated; + they are config-gated through `provenance`. Admin routes run on a separate optional listener and are documented in the generated OpenAPI, but standalone mode does not implement runtime config reload — it returns HTTP 501.