Skip to content
Registry StackDocsv0.34.0

See Evidence Gateway refuse unsafe requests

For the assertion provider and consumer or verifier

View as Markdown

Complete Return a governed value before starting this tutorial. You will reuse that project to observe two boundaries: Evidence Gateway denies a purpose the provider did not authorize, and the verifier rejects a response changed after signing.

The commands below assume the project as the governed-value tutorial leaves it, with no access policy of its own. Control caller access comes first in the navigation and adds access/ policies to that same project. Once they exist, every preparation names its application, so add --client service-router to the request prepare command below if you completed that tutorial. Without it, preparation stops with evidencectl: the active project requires a registered client selected with --client.

Outcome
Two unsafe changes are refused without producing trusted evidence.
Time
About 10 minutes
Level
Local development with synthetic data
Prerequisites
The adult-status project after completing the governed-value tutorialThe materialized source mock from the first Evidence Gateway tutorialPython 3 and curl

In one terminal, return to the first-evidence-assertion directory and serve the checked cases:

Terminal window
evidencectl source mock serve --config adult-status/mocks/source.yaml

In another terminal, enter the existing project and start a fresh local generation:

Terminal window
cd adult-status
evidencectl dev start .
Evidence ready at http://127.0.0.1:8080
Issuer ready at http://127.0.0.1:8081

Prepare a normal age-bracket request. This records the exact verification expectations before any response exists:

Terminal window
evidencectl request prepare age-bracket \
--purpose service-path-selection \
--subject person_id=person-123 \
--name refusal-check

The provider configured service-path-selection for this question. Preparation also obtained a short-lived local token carrying the tutorial caller’s configured authority claims. The token does not authorize one exact request. Evidence Gateway applies those claims to the request it receives.

Copy the request, then change only its purpose:

Terminal window
cp .evidence/requests/refusal-check/request.json unauthorized-request.json
python3 - <<'PY'
import json
from pathlib import Path
path = Path("unauthorized-request.json")
request = json.loads(path.read_text())
request["purpose"] = "age-check"
path.write_text(json.dumps(request))
PY

The retained authorization does not grant age-check for this requirement. Send the changed request across the same HTTP boundary:

Terminal window
curl --silent --show-error \
--config .evidence/requests/refusal-check/authorization.curl \
--request POST \
--url http://127.0.0.1:8080/v1/evidence \
--header 'Content-Type: application/json' \
--header 'Accept: application/jose+json' \
--data-binary @unauthorized-request.json \
--output unauthorized-response.json \
--write-out 'HTTP %{http_code}\n'
HTTP 403

Inspect the public problem:

Terminal window
python3 -m json.tool unauthorized-response.json

The response identifies only the closed evidence.denied problem and public trace ID. It does not reveal source facts, selector values, grants, or credentials. The registry terminal shows no new GET /people/person-123 because authorization failed before source access.

Now send the unchanged prepared request:

Terminal window
curl --silent --show-error \
--config .evidence/requests/refusal-check/authorization.curl \
--request POST \
--url http://127.0.0.1:8080/v1/evidence \
--header 'Content-Type: application/json' \
--header 'Accept: application/jose+json' \
--data-binary @.evidence/requests/refusal-check/request.json \
--output authorized-response.jws.json

Verify it against the expectations retained during preparation:

Terminal window
evidencectl verify authorized-response.jws.json \
--context .evidence/requests/refusal-check/verification.json \
--output authorized-response.verified.json
VERIFIED

This is the control case. The authorized, unchanged response is trusted.

Copy the flattened JWS and change one character in its signed payload:

Terminal window
cp authorized-response.jws.json tampered-response.jws.json
python3 - <<'PY'
import json
from pathlib import Path
path = Path("tampered-response.jws.json")
document = json.loads(path.read_text())
payload = document["payload"]
document["payload"] = ("A" if payload[0] != "A" else "B") + payload[1:]
path.write_text(json.dumps(document))
PY

Try to verify the changed response:

Terminal window
evidencectl verify tampered-response.jws.json \
--context .evidence/requests/refusal-check/verification.json \
--output tampered-response.verified.json
evidencectl: Evidence response verification failed

Verification fails without publishing a trusted payload: no tampered-response.verified.json appears. Your application must make decisions only from the verifier output, never from an unverified response body.

Stop the local services, then remove the stopped generation so the next tutorial starts from a clean project:

Terminal window
evidencectl dev stop
evidencectl dev clean
Local Evidence stopped
Removed stopped local Evidence state

This preserves your editable questions, derivations, keys, and the request artifacts under .evidence/requests/, which Verify an assertion as a consumer reads. It also leaves .evidence/failed-start, where a start that never reached readiness keeps its logs, so a failure you have not read yet survives the cleanup.

Return to the registry terminal and press Ctrl+C.

  • A valid access token does not widen the provider’s authorized purpose.
  • Authorization failure happens before the protected source is called.
  • A signed response cannot be changed without invalidating its signature.
  • Only verified output is safe for an application to consume.