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

# Evidence Gateway security model

> The security invariants Evidence Gateway enforces, how each traces to a named test, and the duties that remain with the operator.

Evidence Gateway's security model is contract-driven. The invariant matrix in
`products/evidence/contracts/security-invariant-matrix.yaml` is the source of
truth for what the runtime guarantees, `products/evidence/contracts/security-test-traceability.yaml`
binds every invariant to the named tests that prove it, and continuous
integration regenerates and byte-diffs both contracts through the
`evidence-contracts` job, which runs
`products/evidence/scripts/check-contracts.sh`. A change that narrows a
guarantee or drops its test fails that job rather than merging unnoticed.

## What the invariant matrix guarantees

The matrix pins numbered invariants, `V1-I01` onward, each with a rule, the
threat it closes, its enforcement, and one `negative_test` id. The set grows as
the contract does, so this page states no count. This section summarizes the
invariants by theme and cites each id in backticks; the
[matrix itself](https://github.com/registrystack/registry-stack/blob/main/products/evidence/contracts/security-invariant-matrix.yaml)
is the complete, normative text, and an invariant it adds is binding from the
change that adds it, whether or not this summary has caught up.

### Closed request and requirement scope

Evidence Gateway evaluates only predefined, versioned requirement revisions
(`V1-I01`). The request schema is closed, so callers cannot supply
thresholds, expressions, scripts, paths, headers, source fields, relationship
types, adapter parameters, or response projections (`V1-I02`). Because a
bundle enabling several requirements at once can reconstruct a protected
value across them, the complete enabled bundle is reviewed as one disclosure
surface (`V1-I03`).

### Authentication and authorization

Principals and attributes derive only from configured, validated
authentication sources, and missing data denies rather than falling back to
another claim (`V1-I04`). One authorization decision binds requester,
optional actor, requirement revision, purpose, every role, profile, and
origin tuple, authority, and audience together, so partial matches across
grants cannot be unioned into access (`V1-I05`). Selector profiles and values
are provider-lookup inputs only, never proof of authority (`V1-I06`), and a
caller-supplied consent, approval, or grant reference cannot create authority
on its own (`V1-I07`). Callers cannot choose selector fields, operators,
weights, thresholds, normalization, or query plans; named profiles close the
exact field set (`V1-I08`).

When authentication states `allowedClients` or `requiredScopes`, admission is
that explicit client list and the verified token's required scope set; an
audience plus static issuer-governed attributes never establishes resource
permission on its own, and a missing scope is never inferred from tags,
principal, roles, `sub`, or request fields (`V1-I54`). When authentication
states `assertionIssuers`, a token carrying the platform verifier's
`registry_assertion_issuer` claim is admitted only when the client it names is
listed and the claim value is one of that client's listed authorities
(`V1-I55`). Every authorization refusal after successful authentication is
durably recorded as one minimal native audit event before the generic 403 is
returned; if that audit fails, the caller receives the generic 503 instead
(`V1-I39`).

### Source access and the Rhai boundary

Source calls are fixed by trusted configuration and executed only by the
core, closing off SSRF, credential forwarding, and script-directed
networking (`V1-I09`). Provider lookup exposes only `match`, `no_match`, or
`ambiguous`; Evidence Gateway never surfaces or chooses candidates (`V1-I10`). Rhai
scripts return one closed lookup result and declared typed concept values
only (`V1-I11`), and the core rejects undeclared concepts, extra fields, and
any value that violates its type, codelist, cardinality, precision, or size
before evidence construction (`V1-I12`). Public evidence is assembled only by
the core after that validation, so a script cannot inject envelope fields,
subject identifiers, or unsupported claims (`V1-I25`).

### Fail-closed behavior and non-disclosure

Missing facts, undefined decisions, script failures, audit failures, and
evaluation failures all fail closed rather than releasing partial or
unaudited evidence (`V1-I13`). Raw source responses are never persisted or
logged (`V1-I14`), and selector, source, and disclosed values never appear in
logs or native audit (`V1-I15`). No-match, ambiguous, missing-fact, and
inconsistent-derivation outcomes collapse to one public problem shape by
default, so a caller cannot use response shape, message, or timing as an
existence oracle for registry membership (`V1-I16`). Subject bindings are
scoped to their audience, purpose, role, and selector bundle, so the same
subject is not linkable across purposes or relying parties (`V1-I17`).

### Configuration and process tenancy

Configuration is immutable for the life of the serving process: there is no
runtime override, reload, merge, or fallback path (`V1-I18`). One process
serves one operator-controlled trust domain, with one service trust domain,
issuer governance boundary, bundle lifecycle, signer, and audit boundary
(`V1-I19`). Rate controls are defense in depth rather than a substitute for
safe bundle design; combination validation runs independently of configured
rate limits (`V1-I20`).

### Signing and response integrity

A signed flattened JWS over the exact evidence payload is mandatory,
available to every authorized grant, and the default response; unsigned
output exists only through its own exact media type when both the bundle and
the matched grant permit it (`V1-I21`). Missing or failed signing never falls
back to unsigned evidence (`V1-I22`). Private signing material is owned by
the core alone and stays absent from bundle values, Rhai, logs, audit, and
errors (`V1-I23`). A signature authenticates the provider and payload
integrity; it does not assert legal-signature status or source truth
(`V1-I24`).

### Discovery, nonce handling, and response-format authorization

Evidence Gateway definition discovery is authenticated and requester-scoped; it never
creates authority or exposes deployment internals such as selectors, source
plans, or credentials (`V1-I26`). Provider discovery is a deterministic,
closed projection of governed public service identity, endpoint, roles,
conformance, compatible capabilities, jurisdictions, and Evidence Type
identifiers, and reaches no Evidence data path (`V1-I53`). Every request carries one canonical
32-byte random nonce that is echoed into the evidence payload but never
stored, uniqueness-checked, or exposed to authorization, rate limits, Rhai,
source requests, logs, metrics, traces, or native audit (`V1-I27`). The
unsigned response format is authorized only when the immutable bundle
enables it and the one complete matched grant also permits it; API
selection, runtime configuration, or other grants create no permission on
their own (`V1-I28`). The final immutable response bytes exist before the
disclosure-release audit is durably accepted, and those are the exact bytes
released afterward (`V1-I29`). Every native audit event records the closed
response-protection mode, and a signing key identity is present exactly for
signed release (`V1-I30`).

### Verification and token binding

Strict signed verification compares the response against independently
retained expectations, the expected nonce, the expected unordered set of
unique role-bound subject bindings, and the expected concept identifiers,
forms, and cardinalities, and returns one generic policy-mismatch error
rather than revealing which comparison failed (`V1-I31`). An access token
carrying a proof-of-possession confirmation claim is denied rather than
accepted as an ordinary bearer token, because Evidence Gateway validates no sender
proof and accepting one would discard the constraint the token was issued
under (`V1-I32`).

### Assurance and fixed acquisition

The local assurance profile alone may omit requirement fixtures, while production and evidence-grade
retain complete fixture validation (`V1-I34`). Assertions, definition responses, and audit events carry
the governed assurance profile, and verification compares it with an independent expectation
(`V1-I35`). Credential-free source access is limited to local authoring against an exact numeric
loopback HTTP origin and sends no authentication header (`V1-I36`). Configured authority claims use
distinct token members and cannot reuse registered JWT claims, except that the principal may use `sub`
(`V1-I37`). Every authority selector set must fill each selector-bound path template it can activate
(`V1-I38`). A search-then-fetch acquisition is one fixed search and, only after a unique schema-valid
match, one fixed fetch; only validated prior facts cross the stage boundary and no response or script
can create another call (`V1-I40`). A search-then-fetch-set acquisition extends that shape to the two
to four fetch stages the bundle declares, run in declared order: each stage receives only its declared
projection of the validated search facts, and one declared ceiling bounds the whole acquisition
(`V1-I41`). A gated acquisition kind serves only where the bundle declares it and the operator
separately enabled it in the runtime configuration (`V1-I42`). A `sqlite-extract` source executes
exactly one bundle-fixed, reviewed statement against one read-only extract file the runtime bound,
under declared row, cell, statement-step, time, and concurrency bounds that no caller, response, or
script can change (`V1-I43`). A selector value that a source would bind into a request path segment
the value cannot occupy is refused as `request.selector_invalid` during selector resolution, before
any access audit event, credential acquisition, or source contact (`V1-I56`).

### Holder-bound assertions

Under the holder-bound subject binding, every subject binding derives from the presented holder key's
RFC 7638 thumbprint, under a domain separate from the audience-scoped one, and from nothing about the
requester: one holder receives one binding however many relying parties collect on its behalf, and two
holders never receive the same binding for the same subject (`V1-I44`). The mode is reached only when
the requirement declares it and the one complete matched grant permits it, through the serializations
the mode allows, and every refusal across those layers is one indistinguishable denial (`V1-I45`). A
holder batch is bounded before anything is acquired, its presented keys are pairwise distinct, one
acquisition serves the whole batch, and a failure on any member releases nothing (`V1-I46`). A
holder-bound credential is accepted only on presentation with a valid key-binding JWT made by the key
the signed confirmation names, over the pinned audience and nonce; an authentic issuer signature with
a failed proof is rejected, never downgraded to issuer-only acceptance (`V1-I47`). Audience-scoped and
presentation verification are two closed policy documents and two entry points, and each refuses the
other's input (`V1-I48`).

### Request batches

A multi-subject request batch authenticates once, uses one evaluation instant, charges its complete
item count against the request rate atomically, and validates and authorizes every item before any
source credential or I/O (`V1-I49`). It returns one ordered result per item and releases the complete
envelope or nothing: a condition a single request would collapse to `evidence_not_available` becomes
that item's outcome, and every other failure aborts through the ordinary problem contract
(`V1-I50`). Optional source batching is one fixed-path HTTP call, selected before I/O only when the
bundle, the runtime, the source, and the requirement all allow it, and it never retries or fans out
(`V1-I51`). The batch emits one durably accepted access event per physical source call and exactly
one terminal event, which carries no nonce, selector, fact, body, or signed material (`V1-I52`).

### Telemetry

Operational telemetry is off by default, served only on a separate
operator-private listener, and every series label is drawn from a closed set
(route template, method, status category, problem code) rather than from
request content (`V1-I33`).

### Public trace correlation

Evidence Gateway returns a bounded W3C trace identifier for public correlation,
including in a problem's `traceId`, but keeps the server-minted audit operation
identifier internal. It does not echo `tracestate`, so caller tracing metadata
cannot become a response or audit channel.

### Cross-cutting controls

The matrix also pins cross-cutting controls, keyed by name rather than a
`V1-I` id, each with its own negative test. They cover bundle and secret
trust (`config_trust`, `secret_parsing`), durable audit ordering
(`audit_order`), listener exposure and outbound transport
(`transport_identity`, `outbound_tls_and_proxy`, `transport_pinning`), secret
file identity (`secret_file_identity`), the closed runtime file
(`runtime_ownership_split`), request- and script-boundary details
(`request_preparation`, `subject_role_order`, `source_response_shape`,
`script_resource_exhaustion`, `reserved_header_aliases`,
`jwks_route_parity`, `unsigned_envelope_distinct`, `exact_decimal`,
`entity_reference_projection`), signing and audit key governance
(`signing_key_governance`, `audit_key_epoch`), and the SD-JWT VC serialization
(`sd_jwt_vc_projection_integrity`, `sd_jwt_vc_format_authorization`,
`sd_jwt_vc_holder_key_closed`, `sd_jwt_vc_claims_closed`), holder-bound
issuance (`holder_bound_entity_reference_prohibited`,
`holder_bound_issuance_purpose`, `holder_key_confinement`), trace correlation
(`trace_context`), and the OID4VCI delivery front end
(`oid4vci_offer_authorization`, `oid4vci_single_use_code`,
`oid4vci_transaction_code_bounded`, `oid4vci_proof_closed`,
`oid4vci_holder_bound_only`, `oid4vci_no_signing_key`,
`oid4vci_telemetry_private`).

## How test traceability works

Each invariant's `negative_test` id in the matrix is a key into
`products/evidence/contracts/security-test-traceability.yaml`, which lists
the file and function name of every test that proves it. A negative test may
be split across several functions, but the matrix states one binding review
rule: a test may be split, never weakened or deleted.

The mapping is not narrative. `crates/registry-evidence/tests/security_contract_traceability.rs`
reads both contracts and checks three things: every matrix id has a
traceability entry and every traceability entry maps to a matrix id, so
neither can drift from the other; each referenced test file exists and
contains a matching `fn <name>(` signature; and that signature sits under a
`#[test]` or `#[tokio::test]` attribute rather than a plain function that
happens to share a name. A traceability entry that names a deleted or
renamed test fails this check before it fails anything else.

```bash
products/evidence/scripts/check-contracts.sh
```

Three concrete examples:

- `V1-I04` (principals and attributes derive only from configured
  authentication sources) traces to `sec-missing-principal-no-fallback`,
  proven by `missing_principal_never_falls_back_to_client_id_or_azp` in
  `crates/registry-evidence/src/runtime_tests.rs`.
- `V1-I23` (private signing material stays core-owned and absent from bundle
  values, Rhai, logs, audit, and errors) traces to
  `sec-private-key-canary-unreachable`, proven by
  `yaml_names_and_secret_references_are_strict` in
  `crates/registry-evidence/src/config.rs` and
  `jwks_contains_public_material_only` in
  `crates/registry-evidence/src/signing.rs`.
- `V1-I29` (final immutable response bytes exist before the disclosure-release
  audit is durably accepted) traces to `sec-release-bytes-pre-audited`,
  proven by four tests in `crates/registry-evidence/src/runtime_tests.rs`:
  `disclosure_audit_failure_prevents_signed_response_release`,
  `disclosure_audit_failure_prevents_unsigned_response_release`,
  `signing_failure_returns_a_problem_and_never_an_unsigned_body`, and
  `sd_jwt_signing_failure_no_fallback_format`.

## What the operator must uphold

The invariant matrix describes what the `evidence` binary enforces in code.
`products/evidence/OPERATOR-CONTRACT.md` states duties that remain with the
operator because no service-side control can hold them for a deployment it
does not otherwise touch.

Owner-only key files. Each secret file below the configured
`secretProviders.file.root` must be a regular, non-symlink file owned by the
service identity with mode `0400` or `0600`; the file provider rejects anything else.
Audit and subject-binding secret files must contain independently generated
raw key bytes and be at least 32 bytes. The runtime expands the audit master into the key that
computes identifier pseudonyms, and Evidence Gateway also requires the two secret references and
resolved master bytes to be distinct.

Immutable bundle and runtime file. The operator mounts one reviewed governed
bundle and one closed `runtime.yaml` read-only at startup; the bundle
directory, the runtime file, and every captured artifact must be
non-writable to the service process, with directories and files carrying no
write bits. There is no runtime upload, editor, approval API, hot reload,
merge, mutation, governed-field override, or fallback bundle or runtime
file: a new revision is a new deployment, not a live change.

Secret handling. Source credentials and local-development private signing material reach
Evidence Gateway only through the secret-reference mechanism and must not appear in
YAML values, Rhai, command arguments, environment dumps, logs, audit,
errors, snapshots, or generated contracts. Production and evidence-grade deployment uses a
workload-local Vault or OpenBao Transit proxy over a Unix socket, leaving the service private key
non-exportable. The operator configures one active public ES256 P-256 JWK, whose derived RFC 7638
thumbprint is its `kid`, and retains published public keys for at least the maximum assertion
validity plus allowed clock skew. A revoked key can be neither active nor published.

## Report a vulnerability

Suspected Evidence Gateway vulnerabilities, including credential disclosure,
authentication bypass, audit redaction failure, source connector data
leakage, and signing-key handling bugs, go through the private disclosure
process in [SECURITY.md](https://github.com/registrystack/registry-stack/blob/main/SECURITY.md),
never a public issue or pull request. See
[Report a vulnerability](../report-a-vulnerability/) for the complete in-scope
list and reporting steps.

## Next

- [Security overview](../)
- [Report a vulnerability](../report-a-vulnerability/)