Unreleased documentation. These pages follow the main branch and can change before the next release. For supported guidance, use v0.15.2.
Request a birth certificate SD-JWT VC from OpenCRVS
For the assertion provider
Extend the project created in
Issue registered-parent evidence from OpenCRVS
with a birth-record question and request the answer as a Selective Disclosure JWT Verifiable
Credential (SD-JWT VC). The credential exposes givenName, familyName, dateOfBirth, and
placeOfBirth as four independently disclosable fields.
This tutorial uses the provider-published synthetic Josh Hoeger registration, selected by national
ID 3617402568. Do not substitute a real person. The demo maintainers can reset its data,
accounts, and integrations.
Understand the credential boundary
Section titled “Understand the credential boundary”The example is informed by the European Union Once-Only Technical System birth-evidence sample, but this tutorial does not implement that exchange protocol. Evidence Gateway produces a governed extract from a registered birth:
{ "birthCertificate": { "givenName": "...", "familyName": "...", "dateOfBirth": "...", "placeOfBirth": { "locationId": "..." } }}The deployment’s law and governance determine whether that extract is accepted as a birth certificate. Evidence Gateway proves which provider signed the configured assertion. It does not grant legal status to the document.
Each direct child of birthCertificate has its own SD-JWT disclosure. placeOfBirth is one atomic
nested value, so its locationId is not disclosed separately from its container.
This response is a second serialization of one stateless assertion, not a credential lifecycle. There is no issuance session, status list, revocation, or presentation exchange.
Draft a birth-record source
Section titled “Draft a birth-record source”Continue in the registered-parent directory created by
Issue registered-parent evidence from OpenCRVS.
The retained source.openapi.yaml includes the bounded birth fields used in this tutorial.
Draft a separate source so the certificate question cannot inherit the parent-identifier projection:
evidencectl source suggest \ --project . \ --source-id opencrvs-birth-certificate \ --operation 'POST /events/search' \ --select /total \ --select '/results/*/type' \ --select '/results/*/status' \ --select '/results/*/dateOfEvent' \ --select '/results/*/placeOfEvent' \ --select '/results/*/declaration/child.nid' \ --select '/results/*/declaration/child.name/firstname' \ --select '/results/*/declaration/child.name/surname' \ --select '/results/*/declaration/child.birthLocationId'The command writes editable source artifacts. The selected operation remains a draft until you close the OpenCRVS-specific request and extraction decisions.
Configure the fixed search
Section titled “Configure the fixed search”Replace sources/opencrvs-birth-certificate.yaml with:
transport: http-jsonbaseUrl: https://events.farajaland-integration.opencrvs.devposture: record-transformedauthentication: kind: oauth2-client-credentials tokenEndpoint: https://auth.farajaland-integration.opencrvs.dev/token clientIdRef: secret:file/opencrvs-client-id clientSecretRef: secret:file/opencrvs-client-secret scope: recordsearch credentialPlacement: form-body maximumCacheSeconds: 300 assumedLifetimeSeconds: 600request: method: POST path: /events/search fixedHeaders: - name: Accept value: application/json selectorInputs: - role: child alternatives: - profile: opencrvs-national-id-v1 fields: [national_id] prepareScript: adapters/opencrvs-birth-certificate-prepare.rhai adapterParameters: childSelectorRole: child eventType: birth registeredStatus: REGISTERED childNationalIdField: child.nid childNameField: child.name childBirthLocationField: child.birthLocationId resultLimit: 2 resultOffset: 0 adapterParametersSchema: schemas/opencrvs-birth-certificate-parameters.schema.yaml preparationLimits: query: forbidden jsonBody: required maximumJsonDepth: 12 maximumCollectionItems: 32 maximumStringBytes: 512 maximumNormalizedBytes: 8192 projection: - /total - /results/*/type - /results/*/status - /results/*/dateOfEvent - /results/*/placeOfEvent - /results/*/declaration/child.nid - /results/*/declaration/child.name/firstname - /results/*/declaration/child.name/surname - /results/*/declaration/child.birthLocationId redirects: deny timeoutMilliseconds: 10000 maximumResponseBytes: 262144 concurrencyLimit: 8responseSchema: schemas/opencrvs-birth-certificate-response.schema.yamlextractScript: adapters/opencrvs-birth-certificate-extract.rhaifactSchema: schemas/opencrvs-birth-certificate-facts.schema.yamlThe source can search only for one registered birth by the authorized child selector. The caller cannot change the event type, status, response projection, or result ceiling. It reuses the owner-only Record Search credentials from the registered-parent tutorial.
Replace schemas/opencrvs-birth-certificate-parameters.schema.yaml with:
type: objectadditionalProperties: falserequired: - childSelectorRole - eventType - registeredStatus - childNationalIdField - childNameField - childBirthLocationField - resultLimit - resultOffsetproperties: childSelectorRole: {const: child} eventType: {const: birth} registeredStatus: {const: REGISTERED} childNationalIdField: {const: child.nid} childNameField: {const: child.name} childBirthLocationField: {const: child.birthLocationId} resultLimit: {const: 2} resultOffset: {const: 0}Replace adapters/opencrvs-birth-certificate-prepare.rhai with:
fn prepare(selectors, context) { let parameters = context["parameters"]; let child = selectors[parameters["childSelectorRole"]]; let data = #{}; data[parameters["childNationalIdField"]] = #{ type: "exact", term: child["values"]["national_id"] };
#{ query: [], body: #{ query: #{ type: "and", clauses: [#{ eventType: parameters["eventType"], status: #{ type: "exact", term: parameters["registeredStatus"] }, data: data }] }, limit: parameters["resultLimit"], offset: parameters["resultOffset"] } }}Extract the birth facts
Section titled “Extract the birth facts”Replace adapters/opencrvs-birth-certificate-extract.rhai with:
fn extract(source_response, context) { let parameters = context["parameters"]; let total = source_response["total"]; let results = source_response["results"]; if results.len > parameters["resultLimit"] { throw("source_protocol_error"); }
if total == 0 { if results.len != 0 { throw("source_protocol_error"); } return #{outcome: "no_match"}; } if total > 1 { if results.len < 2 { throw("source_protocol_error"); } return #{outcome: "ambiguous"}; } if results.len != 1 { throw("source_protocol_error"); }
let result = results[0]; if result["type"] != parameters["eventType"] || result["status"] != parameters["registeredStatus"] || !result.contains("declaration") { throw("source_protocol_error"); }
let declaration = result["declaration"]; let child_id_field = parameters["childNationalIdField"]; let child_name_field = parameters["childNameField"]; let birth_location_field = parameters["childBirthLocationField"]; if !declaration.contains(child_id_field) || !declaration.contains(child_name_field) || !declaration.contains(birth_location_field) || result["placeOfEvent"] != declaration[birth_location_field] { throw("source_protocol_error"); }
let child_name = declaration[child_name_field]; #{ outcome: "match", facts: #{ child_national_id: declaration[child_id_field], given_name: child_name["firstname"], family_name: child_name["surname"], date_of_birth: result["dateOfEvent"], place_of_birth_id: result["placeOfEvent"] } }}The extraction accepts exactly one registered birth. It also requires the top-level event place to match the declaration’s birth-location identifier before constructing facts.
Replace schemas/opencrvs-birth-certificate-facts.schema.yaml with:
type: objectadditionalProperties: falserequired: - child_national_id - given_name - family_name - date_of_birth - place_of_birth_idproperties: child_national_id: {type: string, minLength: 1, maxLength: 128} given_name: {type: string, minLength: 1, maxLength: 200} family_name: {type: string, minLength: 1, maxLength: 200} date_of_birth: {type: string, format: date} place_of_birth_id: {type: string, minLength: 1, maxLength: 128}Define the governed certificate shape
Section titled “Define the governed certificate shape”Create schemas/birth-certificate.yaml:
$id: urn:registrystack:evidence:local:schema:birth-certificate:v1type: objectadditionalProperties: falserequired: [givenName, familyName, dateOfBirth, placeOfBirth]properties: givenName: type: string minLength: 1 maxLength: 200 familyName: type: string minLength: 1 maxLength: 200 dateOfBirth: type: string format: date placeOfBirth: type: object additionalProperties: false required: [locationId] properties: locationId: type: string minLength: 1 maxLength: 128The schema closes every returned field. The location is an OpenCRVS identifier because the public search response does not provide a reviewed human-readable place label.
Create questions/birth-certificate.yaml:
id: birth-certificatequestion: What birth details are recorded for this person?purpose: civil-registration-extractsubject: role: child selector: national_id profile: opencrvs-national-id-v1 derivation: truesource: ref: opencrvs-birth-certificateanswers: - concept: birth_certificate type: reviewed-structured-value schema: schemas/birth-certificate.yaml maximumSerializedBytes: 2048 sdJwtVc: claim: birthCertificate disclosure: top-levelresponseFormats: [signed-jws, sd-jwt-vc]derivation: derivations/birth-certificate.rhaidisclosure: allow: [birth_certificate]birthCertificate is a configured JSON claim name. Evidence Gateway has no built-in birth-certificate
type. The same mechanism can project any reviewed structured value under another non-reserved
claim name. responseFormats explicitly permits the local bundle and this question’s local grant
to return that projection as SD-JWT VC while retaining signed JWS.
Create derivations/birth-certificate.rhai:
fn answer(facts, selectors, context) { if facts.child_national_id != selectors.child.values.national_id { throw("derivation_input_error"); }
#{ birth_certificate: #{ form: "reviewed-structured-value", schema: "urn:registrystack:evidence:local:schema:birth-certificate:v1", fields: #{ givenName: facts.given_name, familyName: facts.family_name, dateOfBirth: facts.date_of_birth, placeOfBirth: #{locationId: facts.place_of_birth_id} } } }}Evidence Gateway validates the complete value against the schema before mapping any field to an SD-JWT disclosure. A derivation cannot add undeclared certificate fields.
Start and prepare the SD-JWT request
Section titled “Start and prepare the SD-JWT request”Compile and start the updated local project:
evidencectl dev --detachPrepare the request and retain sd-jwt-vc as the expected response format before the source is
called:
evidencectl request prepare birth-certificate \ --purpose civil-registration-extract \ --subject child:national_id=3617402568 \ --format sd-jwt-vc \ --name opencrvs-birth-certificatePreparation obtains local authorization and records the expected issuer, audience, subject binding, requirement, value form, request nonce, and response format. It does not contact OpenCRVS.
Request the credential over HTTP
Section titled “Request the credential over HTTP”Send the retained request across the real Evidence Gateway boundary:
curl -fsS \ --config .evidence/requests/opencrvs-birth-certificate/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/opencrvs-birth-certificate/request.json \ --output opencrvs-birth-certificate.sd-jwtEvidence Gateway authenticates the caller, authorizes the exact question and purpose, obtains an OpenCRVS token, performs the fixed birth search, validates and derives the value, signs the SD-JWT VC, and durably records the disclosure release before returning the bytes.
Verify before reading
Section titled “Verify before reading”Verify the credential against the pre-response context:
evidencectl verify opencrvs-birth-certificate.sd-jwt \ --context .evidence/requests/opencrvs-birth-certificate/verification.json \ --output opencrvs-birth-certificate.verified.jsonVERIFIEDInspect the verified Evidence Gateway payload:
python3 -m json.tool opencrvs-birth-certificate.verified.jsonThe supported value retains the governed evidence form:
{ "providesValueFor": "urn:registrystack:evidence:local:concept:birth-certificate:birth_certificate", "value": { "form": "reviewed-structured-value", "schema": "urn:registrystack:evidence:local:schema:birth-certificate:v1", "fields": { "givenName": "Josh", "familyName": "Hoeger", "dateOfBirth": "<synthetic-date>", "placeOfBirth": { "locationId": "<synthetic-location-id>" } } }}The signed credential carries an always-visible birthCertificate container and four nested
disclosures. Count the disclosure segments in the verified credential:
awk -F '~' '{print "disclosures:", NF - 2}' opencrvs-birth-certificate.sd-jwtdisclosures: 4Evidence Gateway’s V1 verifier checks the complete stored SD-JWT VC response. Wallet presentation, omission of selected disclosures, and key-binding JWT validation remain outside this profile.
Inspect the audit and clean up
Section titled “Inspect the audit and clean up”Stop the services, verify the last operation, and remove the sealed local generation:
evidencectl dev stopevidencectl audit show --last-operationevidencectl dev cleanThe audit records the authorized requirement, purpose, requester pseudonym, decision, response format, and disclosed concept. It does not record the child selector, OpenCRVS token, source response, or certificate fields.
Delete the Record Search client in OpenCRVS when you finish. Remove its two credential files when you no longer need the project.