Skip to content
Registry StackDocsDevelopment (unreleased)

Configure Evidence Gateway

View as Markdown

Configuring Evidence Gateway means assembling one deployment project: a governed bundle/ of YAML, Rhai scripts, schemas, and fixtures, plus a process-local runtime.yaml that binds it to a listener, a secret root, and audit storage. Both are trusted, startup-only artifacts. Evidence Gateway loads them once, compiles and validates them together, and refuses to reload, merge, or partially serve a later change. evidence check is the gate every edit to the project must pass before evidence serve runs it.

evidencectl new starts local authoring from an OpenAPI document or from a synthetic SQLite starter:

Terminal window
project_dir="<dir>"
openapi_source="<file-or-url>"
evidencectl new "$project_dir" --openapi "$openapi_source" --profile local
evidencectl new "$project_dir" --transport sqlite-extract --profile local
<dir>/
.gitignore
source.openapi.yaml retained OpenAPI document, for HTTP source authoring
selectors/ reusable Version 1 selector-profile objects
sources/ reusable Version 1 source objects
adapters/ source preparation and extraction scripts
schemas/ source parameter, response, and fact schemas
queries/ fixed reviewed statements, for SQLite source authoring
questions/ question-authoring documents
derivations/ derivation scripts
fixtures/ synthetic executable cases
secrets/ owner-only, unbound disposable local keys

The OpenAPI path retains the document bytes exactly and creates empty authoring directories. evidencectl source suggest --project <dir> then drafts one HTTP source and its editable artifacts. The SQLite path creates a neutral synthetic source, statement, question, derivation, schemas, and fixture that evidencectl fixtures run --project <dir> can evaluate immediately. Neither path creates Mint configuration, a production target, a real extract, or a deployable bundle. Both automatically create disposable local signing and HMAC material. After you add a question to an HTTP project, evidencectl dev compiles the authoring objects into a private local runtime and delegates validation to the real Evidence Gateway loader. Each development session also scaffolds disposable P-256 keys for Registry Mint, its caller, and the optional SD-JWT VC holder.

A compact question may set responseFormats: [signed-jws, sd-jwt-vc] to permit both signed serializations during local development. Omission means signed JWS only. Signed JWS is mandatory, and sdJwtVc on a reviewed structured answer also requires the explicit sd-jwt-vc permission. Production format authority comes from the deployment target’s bundle-wide ceiling and exact authority grants, not from this local compiler convenience.

runtime.yaml binds one governed bundle to one process: where the bundle lives, what the listener binds to, where secrets and audit storage sit, and which private CA files a source may trust. It is process-local operator configuration, not part of the reviewed bundle, and products/evidence/contracts/runtime.schema.yaml is its schema of record.

Top-level required fields:

FieldBinds
versionFixed at 1.
bundleDirectoryAbsolute path to the governed bundle/ directory.
listenerbindHost (loopback, private IPv4, or unique-local IPv6 only), port, tlsTermination (fixed at operator-controlled-upstream: TLS terminates ahead of Evidence Gateway), trustProxyIdentityHeaders (fixed false: proxy-supplied identity is never trusted), plus request-size, concurrency, timeout, and shutdown-grace bounds.
secretProviders.file.rootThe owner-only directory the file secret provider resolves every secret:file/<name> reference beneath.
auditStoragepath for the active audit segment and maximumFileBytes, a per-segment rotation threshold.
outboundTlssystemRoots (fixed true) plus named trustProfiles, each a local id bound to one CA bundle file a source can select through tlsTrustProfile.

An optional metricsListener (bindHost, port) serves GET /metrics on a second private binding; absent by default, it is documented in products/evidence/OPERATOR-CONTRACT.md under Metrics reference rather than in the public Evidence Gateway contract.

runtime.yaml cannot override anything the bundle governs. The schema’s ownership block closes the allowed set to the bundle directory, listener binding and process limits, the optional metrics listener binding, the file-secret root, audit path and rotation bound, and logical private-CA file bindings. Every other field, including service identity, authentication, authority, sources, disclosure, and signing policy, belongs to the bundle alone.

bundle/evidence.yaml is the single governed contract Evidence Gateway compiles and validates as one atomic revision (products/evidence/contracts/bundle.schema.yaml). Its top-level sections:

SectionDeclares
service, issuerThe technical provider and the legal issuing authority, both URIs.
authenticationThe one trusted OIDC access-token profile: issuer, audiences, token type, algorithms, JWKS URI, maximum token lifetime, revoked key identifiers, and the claim names for principal, requester tags, evidence audience, grant id, and grant authority.
auditKeyed JSONL audit: the hash secret reference, key version, and fixed fail-closed behavior.
subjectBindingThe secret reference and key version behind the audience-scoped entity-reference HMAC.
rateLimitsPer-principal request, burst, and failed-selector-attempt bounds.
signingFixed ES256 signing, governed active and published public JWK files, revoked service key identifiers, the fixed JWKS path, and assertion validity and clock-skew bounds.
responseFormats (optional)The bundle-wide ceiling on releasable serializations; defaults to signed JWS alone and must always include it.
selectorProfilesNamed, bounded scalar field sets a subject role may be looked up by.
sourcesNamed fixed HTTP JSON sources: transport, base URL, acquisition posture, authentication, the fixed request shape, and the response, extraction, and fact schemas bound to it.
authorityProfilesNamed requester-tag gates: which grants (requirement, purpose, audience, subject roles, selector profiles, value origins) a caller carrying those tags holds.
requirementsThe assertion definitions themselves: id, closed acquisition, purposes, subject roles, reference frameworks, evidence type, validity, derivation script and parameters, output concepts, fixtures, and disclosure-guard family.

Everything under bundle/ is mounted read-only and covered by one revision digest computed over the exact bundle bytes; there is no partial edit, hot reload, or fallback (bundle.schema.yaml, atomic_revision). Allowed bundle directories are adapters/, derivations/, schemas/, codelists/, fixtures/, and public-keys/; symlinks and any file outside them fail startup (bundle.schema.yaml, bundle_layout).

Evidence Gateway’s four coequal acceptance definitions are adult status, residence region, professional licence status, and legal-parent relationship (products/evidence/README.md, products/evidence/OPERATOR-CONTRACT.md). You shape each as an ordinary requirements entry with its own acquisition, derivation script, and fixtures. The four are presented as coequal, and none of them is a privileged domain type, a Rust built-in operation, or a special route.

Each requirement declares one closed acquisition kind. single names one fixed source. search-then-fetch names a fixed search source and a distinct fixed fetch source:

requirements:
- id: urn:example:requirement:registered-status:v1
kind: criterion
acquisition:
kind: search-then-fetch
search: subject-search
fetch: subject-record

The fetch runs only after the search returns one schema-valid match. Rust passes the validated search FactSet to the fetch as transient prior_facts; a fetch source may bind one declared scalar fact to a complete path segment. Neither a response nor a script can select a source, origin, method, credential, or additional call. The requirement’s effective acquisition posture is the weaker posture of its two sources (products/evidence/contracts/source-contract.yaml, invariant V1-I40).

A deployment project supplies three kinds of Rhai script, one per source or requirement, and Rust compiles and reviews all of them at startup (products/evidence/contracts/rhai-abi.yaml). Scripts are trusted, reviewed, immutable bundle artifacts; that trust extends to provider and requirement semantics only, and it never grants transport, credential, authorization, signing, audit, or evidence-construction authority.

ScriptSignatureOwnsNever touches
prepare (adapters)(source_required_selectors, adapter_context) -> RequestPartsRendering an ordered query and a bounded JSON body from already authorized selector values, trusted parameters, and any validated prior facts supplied to a fixed fetch.Source origin, method, path, headers, credentials, TLS, proxy, timeout, redirects, or request count; those stay Rust-owned.
extract (adapters)(projected_source_response, adapter_context) -> LookupResultReading one already-projected, schema-validated response and returning match with facts, no_match, or ambiguous.Choosing a candidate, scoring, counting, or emitting any diagnostic beyond the closed outcome.
derive (derivations)(facts, declared_authorized_selectors, evaluation_context) -> ConceptValueSetComparing facts against declared authorized selectors and producing at most 16 typed concept values.Requester, purpose, audience, authority, grant, token, credential, or a logging, audit, or signing handle.

Every stage runs on fresh invocation state. The exact adapter_context contains only parameters and prior_facts. Prior facts are empty for single and search stages; a fetch receives only the schema-validated search FactSet. Only final fetch facts reach derivation. Failures map to one closed, value-free class per stage (adapter_input_error, source_protocol_error, derivation_input_error), and raw Rhai errors never reach a public response.

Scripts get fresh local variables, bounded same-file helper functions, pure expressions, and bounded array, map, and string construction. They cannot reach the filesystem, environment, network, process, or a module system; they get no ambient clock, timezone, randomness, UUID, logging, or printing; and they cannot define anonymous functions, dispatch dynamically, or index an array with a computed negative offset (products/evidence/contracts/rhai-abi.yaml, capabilities).

Everything a script computes with runs through a fixed primitive library: date and instant parsing, exact-decimal arithmetic, calendar addition, bucket lookup, codelist lookup, list and set membership, and a handful of bounded string and array operations, each pure, deterministic, and domain-neutral (products/evidence/contracts/primitive-library.yaml). Ordinary floating-point values stay usable inside preparation and extraction, but a derived concept value can never be an ordinary Rhai float: exact numbers use the declared integer or the Rust-owned Decimal type. entity_reference_seed wraps a source-derived value in a protected type Rhai can construct but never read back, print, or serialize; only Rust projects it into an audience-scoped reference after output validation.

Generate the audit and subject-binding secrets independently and keep the private files inside the configured secret root. evidencectl keygen writes private material as owner-only files and never prints its contents (crates/registry-evidencectl/src/keygen.rs):

Terminal window
evidencectl keygen secret --out "<secrets-dir>/audit-hmac-key"
evidencectl keygen secret --out "<secrets-dir>/subject-binding-hmac-key"

For local assurance, Evidence Gateway signs with a P-256 private JSON Web Key (JWK) referenced by signer.privateKeyRef. The governed public half lives under bundle/public-keys/; its service kid is the RFC 7638 thumbprint that the runtime derives, not an operator-chosen value.

Generate a disposable local pair with:

Terminal window
evidencectl keygen signing --out-dir "<owner-only-local-directory>"

Do not use this command to provision a production signer.

Production and evidence-grade deployments use a workload-local Vault or OpenBao Transit proxy. The runtime names its Unix socket, mount, key name, pinned nonzero version, and bounded timeout. The private key stays in Transit. Startup validates the configured public P-256 JWK, pinned version, custody controls, and a sign-and-verify operation before the deployment is ready. Follow Configure Transit signing for Evidence Gateway and Registry Mint for provider key, public JWK, proxy, and policy setup.

Evidence Gateway uses this random secret to authenticate the audit chain. A changed, removed, or reordered audit entry then fails chain verification.

Evidence Gateway uses this random secret to derive audience-scoped subject references from authorized selector values. The references let an assertion identify its subject without disclosing the selector values or creating one identifier that every audience can reuse.

A source that names authentication.kind: oauth2-client-credentials authenticates to its token endpoint with either a client secret or a private key. The private-key form is RFC 7523 section 2.2 private_key_jwt: Evidence Gateway signs a short-lived assertion with the key that clientAssertionKeyRef names and sends that assertion in place of a secret, so no shared secret reaches the token endpoint. SMART on FHIR Backend Services sources authenticate this way.

Generate the pair with:

Terminal window
evidencectl keygen client-assertion --out-dir "<secrets-dir>"

The default writes client-assertion-p384-private-jwk and client-assertion-p384-public.jwk.json. Passing --algorithm rs384 writes client-assertion-rsa2048-private-jwk and client-assertion-rsa2048-public.jwk.json instead. SMART App Launch v2.2.0 requires a token endpoint to validate only one of ES384 and RS384, so the source’s authorization server decides which key you need. The two pairs use different filenames, so a deployment facing two sources that disagree keeps both in one secret root.

A deployment reaching two authorization servers needs a key for each, and both live in the same secret root, so name them:

Terminal window
evidencectl keygen client-assertion --out-dir "<secrets-dir>" --private-name records-authority-key

--private-name names the private file itself, so the name you pass is the one clientAssertionKeyRef points at, as secret:file/records-authority-key. The public half is written beside it as records-authority-key-public.jwk.json. A name follows the secret:file/ grammar: a lowercase ASCII letter, then lowercase letters, digits, ., _, or -. evidencectl refuses anything else rather than write key material no bundle could reference.

Point clientAssertionKeyRef at the private file and register the public file with the source’s authorization server. The kid is the RFC 7638 thumbprint of the public half, and Evidence Gateway sends it in the assertion header so the authorization server selects the registered key without an operator-chosen identifier. Each assertion carries a fresh jti and expires within five minutes, so an authorization server that refuses replays has an identifier to record (crates/registry-platform-authcommon/src/client_assertion.rs).

Evidence Gateway sets the assertion’s aud to tokenEndpoint exactly as the bundle spells it, which is the value SMART App Launch requires. RFC 7523 section 3 asks only that the audience identify the authorization server and says the token endpoint URL MAY be used, so a server that expects a different identifier states it in clientAssertionAudience:

authentication:
kind: oauth2-client-credentials
tokenEndpoint: https://gateway.example.gov/oauth2/token
clientIdRef: secret:file/source-client-id
clientAssertionKeyRef: secret:file/source-client-key
clientAssertionAudience: https://auth.example.gov/realms/main
maximumCacheSeconds: 300

Two kinds of server need it: one reached through a proxy or load balancer, where the URL Evidence Gateway dials is not the one the authorization server knows itself by, and one that expects its issuer identifier. RFC 8414 does not require an issuer identifier to share an origin with the token endpoint, so Evidence Gateway imposes no relationship between the two. RFC 7523 section 3 has the authorization server compare the value by Simple String Comparison, so what you write is signed byte for byte: a default port or a trailing slash survives into the claim rather than being normalized away. The key is accepted only beside clientAssertionKeyRef, since a client secret carries no assertion to hold the claim, and it changes only who may accept the assertion, never where the token request goes. Register one assertion key per authorization server: a key registered at two servers is what would let an assertion audienced for one be presented to the other.

Evidence Gateway refuses to start from a deployment input it could write to, and reports a non-immutable-input error rather than starting on it (products/evidence/OPERATOR-CONTRACT.md). Version 1 permits no reload, merge, mutation, governed-field override, or fallback bundle or runtime file: a project is either the exact bytes Evidence Gateway loaded at startup, or it is a different revision that requires a restart.

Freeze the project before validating or serving it:

Terminal window
chmod -R a-w bundle && chmod 444 runtime.yaml

The bundle directory and every file beneath it carry no write bits, runtime.yaml is mode 444, and the secret root stays owner-only at mode 0700 with mode 0600 secret files. Evidence Gateway Version 1 supports Unix targets only because these invariants rely on owner, mode, no-follow, link-count, and open-file-identity checks the platform provides (products/evidence/contracts/runtime.schema.yaml, platform). To edit the project again, restore write permission, make the change, and freeze it again before the next evidence check.

Two offline commands prove a project before it ever binds a port:

Terminal window
evidence --runtime runtime.yaml check
evidence --runtime runtime.yaml evaluate --fixture "bundle/fixtures/<cases>.yaml"

evidence check loads, compiles, and validates the complete bundle and runtime file together: every selector, role, profile, authority, and source binding resolves, every script compiles against the frozen ABI, and mounted signing material passes its provider self-test, all without opening the audit chain or contacting a source. evidence evaluate replays one fixture file’s synthetic cases through the reviewed adapter, derivation, and output gate, with no source and no network involved.

A fixture failure names the contract that broke and nothing else, so it says a case failed but not which stage it stopped at. Add --explain to print, for every case, the stages it reached and how each one ended, and --explain-format json to render the same trace as one document for a CI job (crates/registry-evidence/src/trace.rs). The trace reports member names, counts, and identifiers, never a response, fact, derived, or selector value, and each fixture’s own diagnostics_exclude canaries are checked against it before it is printed. The flag is offline only: evidence serve has no equivalent.

evidencectl fixtures run --project <dir> drives both commands across a complete deployment project: it runs check, then evaluate against every fixture path the bundle’s requirements reference, and reports PASS or FAIL per step (crates/registry-evidencectl/src/fixtures.rs). Add --explain to relay each fixture’s trace with its step, or as that fixture’s trace field under --json. The incomplete workspace created by evidencectl new has no bundle, runtime, or fixture cases, so it cannot pass this gate. After an operator supplies those deployment artifacts, both check and fixtures run must pass before evidence serve runs the revision.

An editable project is not a deployment input. Add complete named targets when the reviewed source, questions, and fixtures are ready. Keep them in the deployment-operator Git repository with public keys and nonsecret Transit settings, but never private JWKs, audit masters, provider tokens, or live identifiers. There are no overlays, environment branches, substitutions, or symlinks:

environments/
local/evidence/{governance.yaml,runtime.yaml,public-keys/}
staging/evidence/{governance.yaml,runtime.yaml,public-keys/}
production/evidence/{governance.yaml,runtime.yaml,public-keys/}

governance.yaml supplies the production-owned bundle fields, including service, issuer, authentication, audit, subject binding, rate limits, signing, response formats, and authority profiles. It cannot override selectors, sources, or requirements. Each production question adds stable concept identifiers and a governance block with its requirement, frameworks, Evidence Type, validity, observation timezone, fixture, and disclosure families.

runtime.yaml remains the ordinary runtime document. It names the final absolute bundle, private listener, secret root, audit path, and any private CA files for one target. It is copied without transformation.

Build a new candidate directory with explicit target and output paths:

Terminal window
evidencectl build \
--project "<editable-project>" \
--target "<editable-project>/environments/production/evidence" \
--output "<new-candidate-directory>"

The output is create-only and contains runtime.yaml and a closed bundle/. It contains no secret values, source responses, local request state, or audit records. The compiler validates the resulting bundle with the evidence binary and every referenced fixture before publishing. It does not contact an identity provider, Mint, or a source endpoint.

Use Build and deploy an Evidence Gateway project for the full handoff, and Evidencectl command reference for the command contract.