Registry stack documentation: machine-readable Markdown.
Index of all pages: https://docs.registrystack.org/preview/llms.txt
Full corpus: https://docs.registrystack.org/preview/llms-full.txt

# Threat model

> 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).

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 [hardening checklist](../../security/hardening-checklist/),
the high-level posture summary to the [security overview](../../security/), 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.

## 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

Four formal components sit on or beside the trust boundaries. A separate adopter demo sits
outside the production trust boundary:

- 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.
- Solmara Lab: a separate adopter demo, running on synthetic data and demo configuration.
  Treat Solmara Lab 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

{/* SVG diagram. Every boundary label is restated in the paragraphs that follow. */}
<figure>
  <img src="../../images/registry-trust-boundaries.svg"
       alt="The Registry Stack trust boundaries in one map. The portable metadata layer
            (Registry Manifest) only describes: no production data, no authentication, no
            secrets; it authorizes nothing, and publishing grants no access. Across the
            primary trust boundary, the runtime services layer enforces authentication,
            authorization, and disclosure. A caller crosses the service edge, where each
            service runs exactly one authentication mode, static-credential fingerprint or
            OIDC, and every claim- or record-bearing route is authenticated before
            responding. Registry Relay owns registry source access, credentials, product-neutral
            HTTP and script adaptation, snapshots, and minimized consultation outputs. Registry
            Notary consumes only compiler-pinned Relay consultations for registry-backed claims,
            or configured self-attestation inputs for Notary-only claims. Federation with a peer
            Notary is static-peer only, not a dynamic trust
            mesh; it requires a verified peer-signed JWS, and peer identity, signature,
            freshness, single use, purpose, profile, and audience are verified before any
            Relay consultation or claim evaluation. Delegated self-attestation is a distinct trust context from peer
            federation: it delegates no trust to a peer Notary and stays inside the citizen
            or OIDC trust context." />
</figure>

**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 unauthenticated surfaces are the operational, discovery, and
protocol-bootstrap routes: liveness and readiness probes, issuer JWKS public verification keys,
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 Relay.** A runtime boundary sits between Registry Notary and Registry Relay for every
registry-backed claim. The compiler pins the consultation profile, contract hash, purpose, and
typed input mapping. Notary authorizes the claim before calling Relay, and Relay applies its own
workload authorization before source work. Notary has no direct registry source connector. One
Notary authority pairs with one Relay authority, and Notary owns its PostgreSQL transaction,
replay, evaluation, audit, and credential-status state.

**Relay to registry sources.** Registry Relay owns the source boundary. Its compiled integration
fixes the origin, credential interface, method and path authority, input placement, output
projection, and cumulative limits. Product and version metadata do not select the HTTP, script, or
snapshot capability.

**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; the two are distinct trust contexts. 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 issuer JWKS. 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 Audit as a control).
- Secret material: Bearer tokens, raw credentials, and key material are kept out of both
  the portable layer and client-facing surfaces.

The design treats these threat classes as security-relevant: authentication bypass, credential disclosure,
audit redaction or integrity failure, signing-key handling bugs, Relay source-adaptation 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 and deny-by-default. Relay
  refuses unauthorized callers before source work. Notary refuses unauthorized callers before a
  Relay consultation or claim evaluation. 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 a claim id and
  only the typed request inputs the claim policy admits, never the evaluated value. Relay returns
  only the compiler-declared typed outputs, and Notary rejects undeclared inputs.
- 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 defaults to `did:jwk` for
  credential profiles; where a profile keeps binding enabled, and on the OID4VCI
  issuance path, which requires EdDSA `did:jwk`, the holder is bound by a fresh audience-bound
  proof-of-possession. An operator can explicitly configure `holder_binding.mode: none` for
  a bearer-style credential profile, and `registry-notary doctor` warns on that choice.
  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 Relay consultation or claim evaluation, and
  self-asserted target fields do not satisfy trusted-principal gates.
- Source-free evaluation promoted to credential evidence: Credential-capable profiles and
  OID4VCI configurations accept only non-delegated registry-backed claims. Source-free,
  delegated, legacy, missing, stale, or modified evaluation provenance fails before signer
  access.
- Stolen issuer-initiated offer: A transaction code is required by default and is transferred
  separately from the offer. Codes are single-use, wrong PIN attempts are bounded, and invalid
  redemptions are rate limited. An explicit no-PIN compatibility profile has a maximum
  300-second lifetime, but the unredeemed offer remains bearer credential material that a thief
  can use during that window.
- 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: Registry Stack project authoring accepts exact HTTPS source
  origins. Relay's compiled plan fixes the destination, method, path authority, safe headers, and
  cumulative call budgets before the runtime resolves a credential or sends a request. Rhai can
  control ordinary request flow only inside that host-enforced authority.
- Untrusted federation peers, replay, and wrong-purpose requests: Only configured static
  peers are admitted, and before any Relay consultation or claim evaluation 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`, transaction-bound OID4VCI proof nonce, and
  holder-proof `jti`).

**Audit as a control.** Every request touching person-level data can be recorded with the
hashed principal, request id, the principal's granted scopes, and `Data-Purpose` context
when present, and a deployment can run audit fail-closed, so an unrecordable request does
not return success. Relay records ordinary scopes unchanged but replaces each exact-value
trust scope with a field-bound `registry:trust:<field>:hmac-sha256:<digest>` handle under
the deployment audit key. The handle supports investigation and same-key correlation
without disclosing the trusted value. Two caveats for the auditor. First, the recorded scope list is not a
per-route proof of which scope check authorized the request, and per-route audit coverage
is something to verify in your deployment. Second, Notary records hashed principal and
correlation identifiers, and Relay's trust-scope handles have the same operational
dependency, so operator-side investigation depends on retaining the hashing secret and
request context.

## 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 in this section are the subset a threat model must weigh.

These are the risks the design does *not* close:

- 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.
- 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.
- Project approval remains a semantic control: Schemas and validators reject malformed claims,
  duplicate identifiers, incomplete Relay pins, and unsupported evidence modes. They cannot
  decide whether an institution selected the correct source fields, lawful purpose, claim rule,
  or disclosure default. Those decisions require project review and fixture evidence.
- 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. When a status-bearing credential is used, its top-level status is
  not selectively disclosable and client verification fails closed unless the signed status is
  valid at the exact trusted HTTPS origin.
- 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. Runtime configuration changes require deploying a signed local bundle and
  restarting the service.

## 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, audit checkpoint exchange, and
  federated credential issuance are out of scope. Federation is static-peer only: not a
  dynamic trust mesh. Production replay protection uses the typed Notary-owned PostgreSQL
  state schema; explicit in-memory state is local and single-instance only.
- 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 registry-backed, issuer-initiated
  pre-authorized subset, not a full issuer or general external-wallet interoperability.
  Notary advertises `openid4vci.support: not_full_issuer`. It does not claim wallet-facing
  authorization code, a public nonce endpoint, response next nonce, source-free issuance,
  ES256 holder proof, EUDI, HAIP, PAR, DPoP, or wallet attestation.
- 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 (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. 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

- [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
- [Harden a production deployment](../../security/hardening-checklist/): hardening procedures
- [Security overview](../../security/): the trust posture summary
- [Data minimization and purpose limitation](../data-minimization-and-purpose-limitation/)