Preview release.These docs are a work in progress. Pages are still being written, links may break, and structure may shift without notice. Treat everything here as a draft and report issues onGitHub.
Self-attestation lets a citizen use their own OIDC token to evaluate, render, or issue only the claims that policy allows. In direct mode, the subject is bound to the token. In delegated mode, the authenticated requester may ask about a configured dependent target only after a configured relationship proof claim passes. This guide is for operators configuring those flows with an identity provider, source registries, and relying-party or wallet clients.
Use oid4vci-wallet-interop.md when the caller is
a wallet using the OID4VCI facade. Use this guide for the shared
self-attestation policy that sits underneath wallet and non-wallet citizen
flows.
Security goal
Section titled “Security goal”The core guarantee is that a citizen token can only be used for the exact self or dependent subject authorized by policy, and only for explicitly allowed claims, purposes, formats, disclosures, and credential profiles.
Notary validates the token, checks client and audience policy, checks subject binding, checks scopes and operation allow-lists, then reads sources. Source reads must not happen before those gates pass. Delegated requests add a relationship configuration check and a proof claim that must evaluate before the dependent claim reads its source.
flowchart TD
Tok["Citizen OIDC token"] --> V{"Validate token<br/>issuer, audience, algorithm, lifetime"}
V -- "ok" --> C{"Client and audience policy"}
C -- "ok" --> S{"Subject binding<br/>derive subject from token claim"}
S -- "ok" --> Sc{"Scopes and operation allow-lists"}
Sc -- "ok" --> M{"Access mode"}
M -- "self_attestation" --> Read["Read configured source for the bound subject"]
M -- "delegated_attestation" --> P{"Evaluate relationship proof claim"}
P -- "proven" --> ReadDep["Read dependent source for the configured target"]
Read --> Op["Evaluate, render, or issue within the allow-lists"]
ReadDep --> Op
V -- "fail" --> X["Reject before any source read"]
C -- "fail" --> X
S -- "conflicting caller identity" --> X
Sc -- "fail" --> X
P -- "not proven" --> X
Self-attestation gates a request through token validation, client and audience policy, subject binding, and scope and operation allow-lists. Any gate failure rejects the request before a source is read. Delegated mode also gates dependent source reads on a configured proof claim.
For /v1/evaluations, citizen callers do not need to send their own target
identity. Registry Notary derives requester, target, and
relationship: self from the verified subject-binding token claim. Conflicting
caller-supplied identity context is rejected before any source read.
For delegated self-attestation, the caller sends only the dependent target.
Registry Notary derives requester, relationship, and on_behalf_of from the
authenticated principal and scoped authorization details. The scoped
authorization details must also name the same dependent target by id_type and
id. Caller-supplied requester, relationship, or on_behalf_of fields are
rejected before any source read.
When to use it
Section titled “When to use it”Use self-attestation when:
- A citizen portal evaluates eligibility from the citizen’s own token.
- A wallet flow issues a credential for the token-bound subject.
- A parent, guardian, caregiver, or similar requester needs a configured dependent attestation and the source owner has approved the relationship proof source.
- The identity provider can provide a stable, reviewed subject-binding claim.
- The source owner accepts citizen-initiated reads for the configured purpose.
Do not use it when:
- The token has no trustworthy subject identifier.
- The same endpoint needs to evaluate arbitrary subjects for a case worker or service. Use machine auth for that.
- Claims require batch evaluation. Batch evaluation is not supported for self-attestation.
- The source owner has not approved citizen-token driven access.
Identity provider requirements
Section titled “Identity provider requirements”Before enabling the flow, confirm with the identity-provider owner:
- Access tokens are JWTs Notary can verify through a JWKS URL.
- Tokens have a stable issuer and audience.
- The wallet or citizen client has a stable client id or audience.
- The token carries a claim that exactly identifies the registry subject.
- The token signing algorithm is explicit and stable. Configure the algorithm
your provider actually uses, such as
EdDSAorRS256, and do not mix symmetric and asymmetric algorithms in one deployment. RS256 JWKS keys must use a 2048-8192-bit RSA modulus. - Token lifetime, auth age, assurance, and clock skew can satisfy your policy.
- External scopes can be mapped to the Notary scopes you require.
Avoid using sub as a civil identifier unless the identity-provider owner has
confirmed it is the right identifier for source lookups. If you do use sub,
set allow_sub_as_civil_id: true so the config records that decision.
OIDC auth config
Section titled “OIDC auth config”Self-attestation requires auth.mode: oidc:
auth: mode: oidc oidc: issuer: https://idp.example.gov jwks_url: https://idp.example.gov/.well-known/jwks.json audiences: - registry-notary-citizen allowed_clients: - citizen-portal allowed_algorithms: - EdDSA allowed_token_types: - JWT scope_claim: scope scope_separator: " " scope_map: citizen.attest: - registry_notary:self_attest principal_claim: sub leeway: 60sWhen OIDC mode is active, static api_keys and bearer_tokens must be empty.
Use a separate deployment or config if you need machine clients with API keys.
Subject binding
Section titled “Subject binding”Subject binding is the most important part of the config:
self_attestation: subject_binding: token_claim: civil_id claim_source: access_token request_field: subject_id id_type: UIN normalize: exactRules:
token_claimmust be present in the configured token source.claim_sourceisaccess_tokenby default. Useuserinfoonly whenauth.oidc.userinfo_endpointis configured and reviewed. The pre-authorized-code flow resolves the same binding claim from its own RP login, so whenclaim_source: userinfoit additionally requiresoid4vci.pre_authorized_code.esignet.userinfo_url(the callback fetches the userinfo JWS with the eSignet access token).request_fieldis currentlysubject_id.id_typeshould match the source lookup identifier type.normalizemust beexact.
Exact matching is deliberate. Do not rely on case folding, punctuation removal, or local identifier normalization unless that behavior is implemented and reviewed as part of the product.
Citizen client policy
Section titled “Citizen client policy”Restrict which OIDC clients can use the flow:
self_attestation: citizen_clients: allowed_client_ids: - citizen-portal allowed_audiences: - registry-notary-citizenAt least one client id or audience is required. Any allowed audience must also
appear in auth.oidc.audiences. If auth.oidc.allowed_clients is nonempty,
each self-attestation client id must also be listed there.
Token policy
Section titled “Token policy”Set explicit policy ceilings:
self_attestation: token_policy: required_acr_values: - urn:example:loa:substantial assurance_claim_source: access_token max_auth_age_seconds: 600 max_access_token_lifetime_seconds: 900 max_evaluation_age_seconds: 300 max_credential_validity_seconds: 31536000 max_clock_leeway_seconds: 60Guidance:
- Keep access-token lifetime short for public citizen flows.
- Keep evaluation age short so a credential is issued from fresh evidence.
- Set credential validity to the period the issuing agency wants verifiers to accept the wallet-held VC. Use credential status or another lifecycle surface for long-lived credentials.
- Keep clock leeway small and ensure
auth.oidc.leewaydoes not exceedmax_clock_leeway_seconds. - Use
required_acr_valueswhen the identity provider can represent assurance level reliably.
Allowed operations and claims
Section titled “Allowed operations and claims”Every self-attestation surface is allow-listed:
self_attestation: allowed_operations: evaluate: true render: false issue_credential: true batch_evaluate: false allowed_purposes: - wallet_credential_issuance allowed_claims: - birth-record-exists allowed_formats: - application/dc+sd-jwt allowed_disclosures: - value - redacted credential_profiles: - birth_record_sd_jwtRules:
- Enable only operations the citizen flow actually needs.
batch_evaluatemust remain false; batch evaluation is not supported.allowed_claimsmust reference existing claims.credential_profilesmust reference existing profiles.- Credential profiles must use DID holder binding, proof of possession, and
did:jwk. - Claims and profiles must agree that the credential profile can issue that claim.
Delegated self-attestation
Section titled “Delegated self-attestation”Delegated self-attestation is optional and disabled by default. Enable it only when the source owner has approved a relationship proof claim and the identity provider or transaction-token issuer can scope the requester to the delegated access mode.
self_attestation: delegation: enabled: true allowed_relationships: - relationship_type: guardian proof_claim: guardian-link-established target_id_type: UIN allowed_claims: - dependent-person-is-alive allowed_purposes: - dependent_attestation allowed_formats: - application/vnd.registry-notary.claim-result+json - application/dc+sd-jwt allowed_disclosures: - predicate - redacted credential_profiles: - dependent_status_sd_jwtRules:
delegation.enabled: falserequiresallowed_relationshipsto be empty.delegation.enabled: truerequires at least one relationship.- Each
relationship_typemust be unique. proof_claimmust reference an existing claim that reads a relationship source binding.- At least one source binding for
proof_claimmust bind bothrequester.*andtarget.*inputs. - Each delegated claim in
allowed_claimsmust declaredepends_onfor theproof_claim. target_id_typedefaults tosubject_binding.id_typewhen omitted.allowed_purposes,allowed_formats,allowed_disclosures, andcredential_profilesare scoped to the relationship.
Runtime behavior:
- The token-bound subject is the requester.
- The request target is the dependent subject.
- The relationship type and proof claim come from scoped authorization details, not from caller-supplied request fields.
- The scoped authorization details must include a
targetobject for the dependent subject. Notary compares that target to the request target during evaluation and re-hashes it against the stored dependent target before render or credential issuance. - Notary stores keyed hashes for the requester subject binding and dependent target, then rechecks them before delegated source reads.
- The dependent claim is not read unless the proof claim evaluates to boolean
true. - Render and credential issuance from a stored delegated evaluation recheck the current scoped authorization details against the stored metadata.
- OID4VCI credential issuance rejects delegated transaction tokens in this version. Use direct self-attestation for wallet issuance.
Scope policy
Section titled “Scope policy”Use scope policy to require citizen tokens to carry an explicit permission:
self_attestation: scope_policy: required required_scopes: - registry_notary:self_attestscope_policy values:
required: required scopes must be present.optional: scopes may be present, but policy still records what is expected.disabled: no required scopes;required_scopesmust be empty.
Prefer required for shared or public deployments. Use disabled only for
controlled demos where client and audience policy are sufficient.
Wallet origins
Section titled “Wallet origins”For browser-based wallets or portals, list exact HTTPS origins:
self_attestation: allowed_wallet_origins: - https://wallet.example.govWildcards are rejected. HTTP origins are rejected. Empty origins are acceptable for non-browser or backend-mediated flows where CORS is not part of the path.
Rate limits
Section titled “Rate limits”The implemented limiter is in-process:
self_attestation: rate_limits: mode: in_process invalid_token_per_client_address_per_minute: 20 per_principal_per_minute: 30 subject_mismatch_per_principal_per_hour: 5 per_holder_per_hour: 20 credential_issuance_per_principal_per_hour: 10All values must be greater than zero. In-process limits are useful guardrails, but public deployments should also use gateway and identity-provider controls, especially when more than one Notary process is serving traffic.
Source and purpose review
Section titled “Source and purpose review”Self-attestation still reads configured source registries. Before launch:
- Confirm each source owner accepts citizen-token driven reads.
- Confirm
required_scopeon source bindings matches the policy. - Confirm claim
purposeor request purpose values are stable and auditable. - Confirm source fields are minimized.
- Confirm missing and ambiguous source records produce acceptable user-facing behavior.
Use source-claim-modeling-guide.md to review
claim boundaries and source bindings.
Rollout checklist
Section titled “Rollout checklist”- OIDC issuer, JWKS, audience, and client id are stable.
- The subject-binding claim is reviewed and present in test tokens.
normalize: exactis acceptable for the identifier format.scope_policy: requiredis used unless there is a documented reason not to.- All allow-lists are narrow and reference existing claims and profiles.
- Batch is disabled.
- Credential profiles use DID holder binding with proof of possession.
- Wallet origins are exact HTTPS origins, or empty for non-browser flows.
- Gateway and identity-provider rate limits are in place.
- Redis replay storage is configured when multiple processes can serve holder proof or nonce traffic.
doctorpasses, then a controlled self-attestation test passes with a test subject.- Delegated claims depend on the configured proof claim, and the proof claim source binding uses both requester and target inputs.
Troubleshooting
Section titled “Troubleshooting”| Symptom | Likely cause | Check |
|---|---|---|
| Config validation fails | OIDC mode is not enabled or static auth is still present | auth.mode, auth.api_keys, auth.bearer_tokens |
| Token rejected | Issuer, audience, client id, algorithm, or scope mismatch | Token header and claims, auth.oidc, scope_map |
| Subject mismatch | Token claim is missing or caller-supplied identity context conflicts with the derived subject | subject_binding.token_claim, token claims, request body identity fields |
| Userinfo subject not found | claim_source: userinfo without a usable endpoint or issuer | auth.oidc.userinfo_endpoint, userinfo_issuers |
| Delegated relationship denied | Delegation is disabled, relationship type is not configured, authorization details do not name the configured proof claim, or caller supplied requester/relationship context | delegation.enabled, allowed_relationships, token authorization details, request body |
| Delegated relationship unproven | The proof claim failed or returned anything other than boolean true | proof_claim, relationship source data, proof claim rule |
| Credential issuance denied | Profile or claim missing from allow-lists | allowed_claims, credential_profiles, claim/profile cross references |
| Batch request denied | Batch evaluation is not supported for self-attestation | Keep batch_evaluate: false |
| Works locally but fails active-active | In-process rate limits or replay state are not shared | Add gateway limits and Redis replay storage |