Skip to content
Registry StackDocsv0.38.0

Evidence Gateway security model

View as Markdown

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.

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

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

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

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

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

Section titled “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).

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

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

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

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

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

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.

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

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.

Terminal window
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.

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.

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, never a public issue or pull request. See Report a vulnerability for the complete in-scope list and reporting steps.