Unreleased documentation. These pages follow the main branch and can change before the next release. For supported guidance, use v0.15.2.
Get your first Evidence Gateway assertion
For the assertion provider
Evidence Gateway reads an authorized source record, answers a preconfigured question, and signs only the governed answer. In this tutorial, you will ask “Is this person an adult?” without returning the person’s name or date of birth.
Understand the flow
Section titled “Understand the flow”%%{init: {"sequence": {"mirrorActors": false}}}%%
sequenceDiagram
participant C as Tutorial caller
participant E as Evidence Gateway
participant R as Tutorial registry
C->>E: Authorized request<br/>adult-status, person_id=person-123
E->>R: GET /people/person-123
R-->>E: person_id, name, date_of_birth
E->>E: Extract date_of_birth<br/>Derive and sign is_adult: true
E-->>C: Signed assertion with is_adult: true<br/>and an opaque subject binding
Note over C,E: The assertion contains no person_id,<br/>name, or date_of_birth
Before the request, Registry Mint gives the tutorial caller short-lived local authorization. Evidence Gateway and the registry process the identifier and date of birth to answer the question, but the released assertion contains only the governed answer and an opaque subject binding.
Install Evidence Gateway
Section titled “Install Evidence Gateway”Install the latest Evidence Gateway toolset:
curl -fsSL https://github.com/registrystack/registry-stack/releases/latest/download/evidencectl-install.sh | bashevidencectl --versionThe installer provides evidencectl and the local Evidence Gateway and Registry Mint runtimes used later
in the tutorial.
Start a small registry
Section titled “Start a small registry”Create a working directory:
mkdir first-evidence-assertioncd first-evidence-assertionOpen registry.py in your editor and add this server:
import jsonfrom http.server import BaseHTTPRequestHandler, ThreadingHTTPServerfrom urllib.parse import unquote, urlsplit
PEOPLE = { "person-123": { "person_id": "person-123", "name": "Amina Example", "date_of_birth": "2000-01-01", }, "person-456": { "person_id": "person-456", "name": "Mateo Example", "date_of_birth": "2012-05-20", }, "person-789": { "person_id": "person-789", "name": "Noor Example", "date_of_birth": "1988-11-03", },}OPENAPI = { "openapi": "3.1.0", "info": {"title": "Tutorial registry", "version": "1.0.0"}, "servers": [{"url": "http://127.0.0.1:8000"}], "paths": {"/people/{person_id}": {"get": { "operationId": "getPerson", "parameters": [{"name": "person_id", "in": "path", "required": True, "schema": {"type": "string"}}], "responses": {"200": { "description": "A person record", "content": {"application/json": { "schema": { "type": "object", "required": ["person_id", "name", "date_of_birth"], "properties": { "person_id": {"type": "string", "minLength": 1, "maxLength": 64}, "name": {"type": "string", "minLength": 1, "maxLength": 100}, "date_of_birth": { "type": "string", "format": "date", "minLength": 10, "maxLength": 10, }, }, }, }}, }}, }}},}
class Registry(BaseHTTPRequestHandler): def do_GET(self): path = unquote(urlsplit(self.path).path) if path == "/openapi.json": return self.send_json(OPENAPI) people_prefix = "/people/" if path.startswith(people_prefix): person = PEOPLE.get(path[len(people_prefix):]) if person is not None: return self.send_json(person) self.send_error(404)
def send_json(self, value): body = json.dumps(value).encode() self.send_response(200) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body)
print("Registry listening on http://127.0.0.1:8000", flush=True)ThreadingHTTPServer(("127.0.0.1", 8000), Registry).serve_forever()You do not need to understand every line of the server. Three details connect it to Evidence Gateway:
/people/{person_id}returns a complete person record./openapi.jsondescribes that endpoint and its response.getPersonis the OpenAPI operation that the Evidence Gateway question will select.
In one terminal, start the registry and leave it running:
python3 registry.pyThe server publishes its OpenAPI description at /openapi.json and three synthetic records:
person-123, person-456, and person-789.
The records include both adults and a child, but this tutorial will request an assertion only for
person-123.
In another terminal, return to first-evidence-assertion and inspect the source record:
curl -s http://127.0.0.1:8000/people/person-123 | python3 -m json.tool{ "person_id": "person-123", "name": "Amina Example", "date_of_birth": "2000-01-01"}The registry returns an identifier, a name, and a date of birth. The verified assertion will contain none of those source values.
Create the Evidence Gateway project
Section titled “Create the Evidence Gateway project”Create a local project from the registry’s OpenAPI description:
evidencectl new adult-status \ --openapi http://127.0.0.1:8000/openapi.json \ --profile localcd adult-status--profile local creates a loopback-only development project, not a production deployment.
The command creates owner-only local keys for signing, subject binding, and audit integrity.
OpenAPI describes the source, but you still decide the question and what the answer may disclose.
The new project separates those decisions from the retained API description:
adult-status/├── source.openapi.yaml├── selectors/├── sources/├── adapters/├── schemas/├── questions/├── derivations/└── secrets/This first question uses the compact OpenAPI form, so you will add only a question and its derivation. Later institution integrations use the reusable source, selector, adapter, and schema directories.
questions/adult-status.yaml
Section titled “questions/adult-status.yaml”Open questions/adult-status.yaml in your editor and add the question definition:
id: adult-statusquestion: Is the person at least 18 years old?purpose: age-checksubject: role: person selector: person_idsource: operation: getPerson facts: - name: date_of_birth path: /date_of_birth combine: exactly-one collectionBounds: {}answers: - concept: is_adult type: booleanresponseFormats: [signed-jws, sd-jwt-vc]derivation: derivations/adult-status.rhaidisclosure: allow: [is_adult]The definition closes four decisions:
subjectsays thatperson_idselects the person in/people/{person_id}.sourceselectsgetPersonand allows onlydate_of_birthto reach the derivation. It does not change the complete record returned by the registry.answersdeclares one permitted result, a boolean namedis_adult.responseFormatskeeps signed JWS as the default and also permits the same answer to be returned as SD-JWT VC.disclosureis the explicit release boundary. It must name exactly the answers returned by the derivation.
combine: exactly-one rejects a missing or repeated date_of_birth. collectionBounds: {} is
empty because this fact does not traverse a collection. The request purpose must be age-check,
matching the purpose declared here.
derivations/adult-status.rhai
Section titled “derivations/adult-status.rhai”Open derivations/adult-status.rhai in your editor and add the answer logic. Rhai is the bounded
scripting language Evidence Gateway uses for requirement-specific derivations:
fn answer(facts, selectors, context) { let born = parse_date(required(facts.date_of_birth, "date_of_birth_missing")); let adult_on = add_calendar_years(born, 18); #{is_adult: compare_dates(context.legal_local_date, adult_on) >= 0}}The derivation performs four operations:
- Require and parse the selected date of birth.
- Calculate the eighteenth birthday using calendar arithmetic.
- Compare it with
context.legal_local_date, which Evidence Gateway derives from the observation instant in the configured timezone. - Return a map containing exactly the declared
is_adultanswer.
Evidence Gateway validates the returned name and boolean form before signing.
Start Evidence Gateway and Registry Mint:
evidencectl dev --detachEvidence ready at http://127.0.0.1:8080Mint ready at http://127.0.0.1:8081This command starts Evidence Gateway and Mint only. The Python registry remains the process you started in the first terminal.
Request an assertion
Section titled “Request an assertion”Prepare the request, local access, and verification expectations:
evidencectl request prepare adult-status \ --purpose age-check \ --subject person_id=person-123 \ --name first-assertionThe Evidence client prepares the request and closes its pinned verification expectations locally,
before a response exists. evidencectl separately asks the local Mint for short-lived
authorization. It sends no HTTP request to Evidence Gateway and does not contact the registry. The
command creates exactly these owner-only artifacts:
Prepared request: .evidence/requests/first-assertion/request.jsonPrepared verification context: .evidence/requests/first-assertion/verification.jsonPrepared authorization: .evidence/requests/first-assertion/authorization.curlThe artifacts have separate responsibilities:
request.jsoncontains the question, purpose, fresh request nonce, andperson-123selector.verification.jsonrecords what a valid response must satisfy before the response exists.authorization.curlcontains only the short-lived local authorization header.
Send the request yourself:
curl --silent --show-error --fail-with-body \ --config .evidence/requests/first-assertion/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/first-assertion/request.json \ --output assertion.jws.json \ --write-out 'HTTP %{http_code}\n'HTTP 200curl made the one POST /v1/evidence assertion request, Evidence Gateway called the registry, and
the HTTP response is a signed flattened JSON Web Signature (JWS). A successful HTTP response is not
yet a trusted assertion. Preparation made zero Evidence Gateway HTTP requests, and the verification
step below is offline, so one assertion requires one request to Evidence Gateway rather than two.
Verify before reading
Section titled “Verify before reading”Verify the response against the expectations recorded before the request. Verification checks the signature and exact issuer, question, purpose, audience, request nonce, subject binding, answer shape, and validity period:
evidencectl verify assertion.jws.json \ --context .evidence/requests/first-assertion/verification.json \ --output verified.jsonVERIFIEDOnly after the command prints VERIFIED, inspect the verified payload:
python3 -m json.tool verified.jsonThe verified document contains generated identifiers, timestamps, and a pseudonymous subject binding. The fields relevant to this question look like this excerpt:
{ "assuranceProfile": "local", "purpose": "age-check", "subjects": [ { "binding": "urn:evidence:subject:v1_…", "role": "person" } ], "supportedValues": [ { "providesValueFor": "urn:registrystack:evidence:local:concept:adult-status:is_adult", "value": true } ], "supportsRequirement": "urn:registrystack:evidence:local:requirement:adult-status"}supportsRequirement names the requirement that the question you authored under questions/
compiles to, which is the unit the deployed configuration and the audit record.
The verified payload does not include person_id, name, or date_of_birth. The assertion
answers the authored question without becoming another copy of the registry record.
The owner-only request.json still records that this transaction concerned person-123, while
verification.json records the expected opaque subject binding and request nonce. Retain those
files with the signed response when you need to verify Evidence Gateway later as a
consumer. The identifier cannot be recovered from the
assertion alone.
Try the SD-JWT VC serialization
Section titled “Try the SD-JWT VC serialization”Prepare a second request for the same governed question, this time recording SD-JWT VC as the expected response format:
evidencectl request prepare adult-status \ --purpose age-check \ --subject person_id=person-123 \ --format sd-jwt-vc \ --name first-vcPrepared request: .evidence/requests/first-vc/request.jsonPrepared verification context: .evidence/requests/first-vc/verification.jsonPrepared authorization: .evidence/requests/first-vc/authorization.curlSend the request with the exact SD-JWT VC media type:
curl --silent --show-error --fail-with-body \ --config .evidence/requests/first-vc/authorization.curl \ --request POST \ --url http://127.0.0.1:8080/v1/evidence \ --header 'Content-Type: application/json' \ --header 'Accept: application/dc+sd-jwt' \ --data-binary @.evidence/requests/first-vc/request.json \ --output assertion.sd-jwt \ --write-out 'HTTP %{http_code}\n'HTTP 200Treat the compact credential as opaque until verification succeeds:
evidencectl verify assertion.sd-jwt \ --context .evidence/requests/first-vc/verification.json \ --output verified-vc.jsonVERIFIEDNow inspect the verified Evidence payload:
python3 -m json.tool verified-vc.jsonIt has the same governed is_adult: true answer and minimum-disclosure boundary as the signed JWS
response. Only the serialization changed. This is a stateless credential response, not a wallet
issuance, revocation, or presentation workflow.
Continue with Explore SD-JWT VC locally to inspect disclosures, issuer metadata, tamper refusal, and structured-field projection.
Stop the local services
Section titled “Stop the local services”Stop Evidence Gateway and Mint before reading the completed audit chain:
evidencectl dev stopLocal Evidence stoppedInspect the audit entry
Section titled “Inspect the audit entry”Verify the completed audit chain and show its last operation:
evidencectl audit show --last-operationACCESS AUTHORIZED adult-status age-check requester=<pseudonym>DISCLOSURE RELEASED is_adultThe requester value changes on each fresh project. The view identifies the question, purpose, requester pseudonym, authorization decision, and disclosed concept. It does not repeat the source record or access token.
Clean up
Section titled “Clean up”Remove the stopped local generation, including the sealed bundle that Evidence Gateway used:
evidencectl dev cleanRemoved stopped local Evidence stateThis preserves your editable questions, derivations, keys, and request artifacts. It removes
.evidence/dev, whose runtime files are deliberately read-only while a generation exists. Run
dev stop and then dev clean instead of changing those permissions by hand. The command refuses
to remove a running or unrecognized generation.
Return to the first terminal and press Ctrl+C to stop the registry.
Keep the tutorial directory to continue with the governed-value tutorial. If you do not plan to continue, you can remove the complete directory with ordinary file commands.
If local ports are already in use
Section titled “If local ports are already in use”Choose two different unused ports when you start the local services:
evidencectl dev --detach --evidence-port 8180 --mint-port 8181Evidence Gateway uses the selected ports consistently in its runtime, Mint authorization, readiness
checks, and generated request artifacts. Send the assertion request to the selected Evidence Gateway port,
such as http://127.0.0.1:8180/v1/evidence.
The development profile binds both services to loopback in the environment where the command
runs. If that environment is a Docker container, run the tutorial’s curl commands in the same
container. Use the operator deployment configuration when the service must listen beyond
loopback.