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 on GitHub.
This reference describes how to assemble a deployable Registry Notary configuration. It is written for teams adopting the service, not for contributors changing the implementation.
Registry Notary is config driven. The YAML file describes which claims can be evaluated, which upstream registries are contacted, how callers authenticate, how credentials are signed, and which operational stores are used. Secrets should stay in environment variables or a secret manager; config fields name the environment variable to read.
Adoption Decisions
Section titled “Adoption Decisions”Before editing YAML, decide these items:
| Decision | Use this when | Main config |
|---|---|---|
| Machine-to-machine API | A backend service calls Notary for evaluation or issuance | auth.mode: api_key |
| Citizen or wallet flows | A user-held OIDC token identifies the subject | auth.mode: oidc, self_attestation |
| SD-JWT VC issuance | Notary signs credentials from evaluated claims | evidence.signing_keys, evidence.credential_profiles |
| OID4VCI wallet facade | A wallet requests credentials directly | oid4vci, self_attestation |
| Multi-instance deployment | More than one Notary process serves traffic | replay.storage: redis, usually credential_status.storage: redis |
| Credential suspension or revocation | Verifiers need a live status URL | credential_status.enabled: true |
| Audit retention | Operators need traceability without raw personal data | audit |
| OpenFn sidecar reads | A target system needs pinned adaptor execution or normalization outside Notary | connector: openfn_sidecar, retry_on_5xx: false |
| OpenFn sidecar assurance | Notary must fail closed unless it is talking to the approved sidecar runtime | source_connections.<id>.expected_sidecar |
| OpenFn batch matching | Batch evaluation should share one OpenFn sidecar read across compatible items | bulk_mode: openfn_sidecar_batch, binding query_fields |
Start with one narrow claim, one source connection, one signing key, and one
credential profile. Add federation, wallet issuance, and batch evaluation after
the basic path passes doctor.
Top-Level Shape
Section titled “Top-Level Shape”| Block | Purpose | Required for startup |
|---|---|---|
server | Bind address and process HTTP settings | No, defaults are present |
auth | Caller authentication and scope mapping | Yes |
deployment | Operator-declared deployment profile and gate waivers | No, an undeclared profile binds no gates |
audit | Redacted audit envelope sink and HMAC secret | Recommended for every deployable environment |
config_trust | Durable local state for governed config apply | No, only for signed governed config |
evidence | Claims, sources, rules, formats, signing keys, and credential profiles | Yes |
cel | Optional CEL worker policy, limits, and regex posture | Defaults are present |
replay | One-time-use store for federation request JWTs, OID4VCI nonces, and holder proof JWTs | Defaults to in-process memory |
credential_status | Optional storage-backed lifecycle status URL for issued credentials | No |
self_attestation | OIDC-bound citizen request policy | Only for citizen or wallet flows |
oid4vci | Wallet-facing OpenID4VCI facade | Only for wallet flows |
federation | Static-peer delegated evaluation | Only for federation |
Unknown fields are rejected. That is intentional: a misspelled field should fail at config validation instead of becoming an accidental open policy.
Deployment Profile and Gates
Section titled “Deployment Profile and Gates”The deployment block lets an operator declare the assurance shape of a
deployment. The profile is always declared by the operator and is never inferred
from environment name, hostname, or network position.
deployment: profile: production # local | hosted_lab | production | evidence_grade multi_instance: true # declares this instance shares a workload with others waivers: - finding: notary.source.private_network_escape reason: "approved internal source for partner pilot, ticket OPS-123" expires: 2026-09-30| Field | Purpose |
|---|---|
profile | The declared assurance shape. Absent means undeclared. |
multi_instance | Operator declaration that this instance runs active-active with peers, which makes shared, durable replay storage mandatory. |
waivers | Per-finding suppressions, each with a mandatory reason and expiry. |
Profiles:
| Profile | Use |
|---|---|
local | Development, demos, tests, local pilots. Binds no gates. |
hosted_lab | Shared demos, partner evaluations, hosted validation. |
production | Real integrations handling sensitive or operational data. |
evidence_grade | Deployments where the evidence trail is itself part of the assurance claim. |
An undeclared profile binds no gates and keeps current behavior. The posture
report then carries a single deployment.profile_undeclared warning so the gap
is visible without breaking the deployment. An invalid profile value fails
startup, so a typo cannot silently disable enforcement.
Each gate evaluates to one of four severities under the declared profile:
| Severity | Effect |
|---|---|
startup_fail | The process refuses to start. Never waivable. |
readiness_fail | The readiness endpoint reports not-ready; the process runs. |
finding_error | A posture finding, error class. |
finding_warn | A posture finding, warn class. |
The gates bound for Registry Notary:
| Finding id | Condition | hosted_lab | production | evidence_grade |
|---|---|---|---|---|
notary.replay.in_memory_high_risk | In-memory replay while federation, OID4VCI pre-authorized code, holder proof, wallet traffic, or multi_instance is declared | error | readiness_fail | startup_fail |
notary.audit.sink_missing | No durable, retained audit sink | error | startup_fail | startup_fail |
notary.source.insecure_url | Source connection over a plain http:// URL with no localhost or private-network allowance | error | readiness_fail | startup_fail |
notary.source.private_network_escape | A source enables the private-network escape hatch | warn | error | error |
notary.sidecar.expected_sidecar_missing | An OpenFn source omits expected_sidecar | warn | error | readiness_fail |
notary.admin.shared_exposure | The admin surface shares the public listener | error | readiness_fail | startup_fail |
notary.openapi.public | OpenAPI is served without authentication | warn | error | error |
notary.config.unsigned | Local YAML config rather than signed governed config | warn | error | startup_fail |
Waivers
Section titled “Waivers”A waiver names exactly one finding id, a free-text reason, and a mandatory
expires date (YYYY-MM-DD). While active, a waiver changes a triggered
finding’s status to waived in posture instead of applying its severity effect.
startup_failgates are never waivable. A waiver for one is rejected at config load, because running at all would falsify the declared profile.- An expired waiver stops suppressing its finding and additionally raises
deployment.waiver_expiredin posture, so lapsed approvals surface rather than silently persisting. - Waiver reasons appear in the restricted-tier posture for review. Never put a secret in a reason.
Active waivers and gate findings appear in the admin posture document under the
deployment object, and the eight-field audit assurance vocabulary appears under
the top-level audit object. See docs/security-assurance.md for the assurance
vocabulary.
Secret Handling
Section titled “Secret Handling”Config files should contain names, not secret values.
| Need | Config field | Environment value |
|---|---|---|
| API key or bearer-token auth | auth.api_keys[].fingerprint, auth.bearer_tokens[].fingerprint | sha256:<hex> fingerprint |
| Static upstream source token | evidence.source_connections.<id>.token_env | Raw upstream bearer token |
| OAuth2 client credential source auth | source_auth.client_id_env, source_auth.client_secret_env | OAuth client id and secret |
| Local JWK signing key | evidence.signing_keys.<id>.private_jwk_env | Private Ed25519 JWK JSON |
| Watched local JWK signing key | evidence.signing_keys.<id>.path with provider: file_watch | Private Ed25519 JWK JSON in a host-local file |
| Publish-only JWK | evidence.signing_keys.<id>.public_jwk_env | Public JWK JSON |
| Publish-only deadline | evidence.signing_keys.<id>.publish_until_unix_seconds | Optional public metadata, not a secret |
| PKCS#11 PIN | evidence.signing_keys.<id>.pin_env | HSM token PIN |
| Audit hashing | audit.hash_secret_env | Stable high-entropy HMAC secret |
| Redis stores | replay.redis.url_env, credential_status.redis.url_env | Redis connection URL |
Use registry-notary hash-api-key --print-secret to generate a local API key
and its hash. Store only the hash in the environment variable referenced by
config; give the plaintext key only to the caller.
registry-notary doctor --config <path> validates active PKCS#11 signing keys
by loading the configured module, opening the token, checking the private-key
lookup, and running the startup self-test. Run registry-notary build-info on
the deployed artifact to confirm the pkcs11 capability is compiled in before
debugging token or vendor module configuration.
For local no-restart key material refresh, use provider: file_watch with a
host-local private JWK file path. The file is read at startup and re-read on
signing use. A valid replacement with the same configured kid, alg, and
public JWK identity is picked up without process restart; a malformed, missing,
or different-public-key replacement marks the key degraded and keeps the last
good signer serving. Use a new kid and governed config change for real key
rotation.
For local development, the binary accepts --env-file. For shared
environments, prefer the platform secret store and avoid checking dotenv files
into the repository.
Governed Config Apply
Section titled “Governed Config Apply”Most deployments can skip this section. config_trust is optional; it governs
signed, threshold-approved config changes for high-assurance deployments. Simple
local deployments omit it and keep using the local YAML loaded at startup.
This governed example is syntactically valid but illustrative. Generate the
tuf_root_sha256 and targets-role signer key IDs from your own trusted TUF
repository before using governed apply in an environment.
config_trust: antirollback_state_path: /var/lib/registry-notary/config-antirollback.json local_approval_state_path: /var/lib/registry-notary/config-local-approvals.json break_glass_rate_limit: max_accepted: 1 window_seconds: 3600 remote_tuf_repositories: - root_path: /etc/registry-notary/tuf/metadata/1.root.json metadata_base_url: https://config.example.gov/metadata targets_base_url: https://config.example.gov/targets datastore_dir: /var/lib/registry-notary/tuf allow_dev_insecure_fetch_urls: false accepted_roots: - root_id: ops-root production: false tuf_root_sha256: sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef valid_from_unix_seconds: 1770000000 valid_until_unix_seconds: 1772592000 signers: "1111111111111111111111111111111111111111111111111111111111111111": kid: "1111111111111111111111111111111111111111111111111111111111111111" enabled: true roles: - name: config-admin threshold: 1 signer_kids: ["1111111111111111111111111111111111111111111111111111111111111111"] allowed_change_classes: [public_metadata, root_transition]config_trust is optional. Simple local deployments omit it and keep using the
local YAML loaded at startup. Governed config apply requires
antirollback_state_path and local_approval_state_path, which must point to
durable local state such as a mounted volume. break_glass_rate_limit is the
trusted local rolling-window policy for break-glass apply requests; when omitted
it defaults to one accepted request per rate-limit identity per hour.
accepted_roots uses the shared Registry trust-root shape.
Standalone Registry Notary verifies local or remote signed TUF config targets
against accepted_roots when the admin request provides a tuf source.
Verified TUF targets-role signature key IDs, not target-declared custom
metadata, satisfy the role threshold. Inline YAML remains available for
verify/dry-run diagnostics. Local TUF sources use root_path, metadata_dir,
targets_dir, datastore_dir, and target_name. Remote TUF sources keep the
same root_path, datastore_dir, and target_name, and replace local
repository directories with metadata_base_url and targets_base_url. Remote
sources are recorded as signed_bundle_endpoint; local repository sources are
recorded as signed_bundle_file.
remote_tuf_repositories is an operator-controlled allowlist of remote TUF
sources that may be submitted in admin apply requests. An apply request whose
remote TUF source does not exactly match one of the listed entries (comparing
root_path, metadata_base_url, targets_base_url, and datastore_dir) is
rejected before any TUF fetch is attempted. This prevents an attacker who can
POST to the admin endpoint from directing the Notary to an arbitrary TUF server.
When omitted the list is empty and all remote TUF apply requests are rejected.
Each entry carries its own allow_dev_insecure_fetch_urls flag; the flag from
the matching allowlist entry is always used, never the value in the incoming
request. HTTP loopback remote repositories require allow_dev_insecure_fetch_urls: true and are intended only for tests and local development. Production entries
must use HTTPS URLs and must set allow_dev_insecure_fetch_urls: false.
Governed bundle metadata may set previous_config_hash as either bare lowercase
SHA-256 hex or sha256:<64 lowercase hex>. Notary normalizes both forms at the
product boundary before anti-rollback comparison. The canonical form in
verification reports, admin API responses, audit events, docs, and mismatch
errors is sha256:<64 lowercase hex>. On a true chain mismatch, the error detail
includes the expected canonical hash and the received value’s detected format.
TUF root transition
Section titled “TUF root transition”For TUF root transition, apply a signed local TUF bundle whose target metadata
includes root_transition, changes only config_trust.accepted_roots, keeps
the antirollback and local approval paths unchanged, retains existing roots
unchanged, and references a matching unexpired local approval. Add the new
final tuf_root_sha256 as another local accepted_roots entry before applying
bundles that verify through the rotated root. valid_from_unix_seconds and
valid_until_unix_seconds are optional local bounds for overlap windows;
expired or not-yet-valid roots fail authorization even when TUF verification
and signer quorum otherwise succeed.
Hot-apply and reload
Section titled “Hot-apply and reload”POST /admin/v1/config/apply can hot-apply governed signed signing-key
rotations for credential issuer, pre-authorized access-token, eSignet
(an OpenID Connect identity service) client-assertion, and federation response
signing paths after TUF verification,
trust-root authorization, and local anti-rollback acceptance. It can also
hot-apply signing_key_cleanup for expired publish-only keys that are no longer
active signing references. Inline config candidates are accepted only by verify
and dry-run; apply rejects them with
registry.admin.config.inline_apply_rejected. Other signed changes continue to
reject with rejected_restart_required, so rejected signed targets do not
advance anti-rollback state or change active posture provenance. This
restart-required apply result is distinct from unsupported live reload: it means
the signed candidate is valid but cannot be hot-applied.
Use GET /admin/v1/capabilities with registry_notary:ops_read before
automation invokes governed config or reload operations. Standalone Notary does
not support resource, table, or runtime config reload; the mounted
POST /admin/v1/reload route returns 501 registry.admin.capability.not_supported.
Break-glass apply
Section titled “Break-glass apply”Break-glass apply is
available only for signed targets whose target metadata includes the local
approval’s emergency_change_class; the approval fields come from the admin
request, the rolling-window policy comes from local
config_trust.break_glass_rate_limit, and the audit record stores no raw reason
text.
Minimal Machine Config
Section titled “Minimal Machine Config”This is the smallest useful shape for a backend caller that evaluates one claim from one DCI source and can later issue a credential from that claim.
server: bind: 127.0.0.1:8081 openapi_requires_auth: true request_timeout: 30s request_body_timeout: 10s http1_header_read_timeout: 10s max_connections: 1024
auth: mode: api_key api_keys: - id: verifier-service fingerprint: provider: env name: REGISTRY_NOTARY_API_KEY_HASH commitment: sha256:0000000000000000000000000000000000000000000000000000000000000000 scopes: - civil_registry:evidence_verification - registry_notary:credential_issue
audit: sink: stdout hash_secret_env: REGISTRY_NOTARY_AUDIT_HASH_SECRET
evidence: enabled: true service_id: civil.registry-notary api_base_url: https://notary.example.gov source_connections: civil_registry: base_url: https://registry.example.gov token_env: CIVIL_REGISTRY_TOKEN dci: search_path: /registry/sync/search query_type: idtype-value records_path: /message/search_response/0/data/reg_records field_paths: birth_date: /birth_date signing_keys: issuer-2026-05: provider: local_jwk_env private_jwk_env: REGISTRY_NOTARY_ISSUER_JWK alg: EdDSA kid: did:web:notary.example.gov#issuer-2026-05 status: active issuer-file-watch: provider: file_watch path: /run/secrets/registry-notary/issuer.jwk alg: EdDSA kid: did:web:notary.example.gov#issuer-file-watch status: active credential_profiles: birth_record_sd_jwt: format: application/dc+sd-jwt issuer: did:web:notary.example.gov signing_key: issuer-2026-05 vct: https://notary.example.gov/credentials/birth-record/v1 validity_seconds: 31536000 allowed_claims: - birth-record-exists holder_binding: mode: did proof_of_possession: required allowed_did_methods: - did:jwk disclosure: allowed: - value - redacted claims: - id: birth-record-exists title: Birth record exists version: 2026-05 subject_type: person value: type: boolean inputs: - name: target.identifiers.national_id type: string source_bindings: birth_record: connector: dci connection: civil_registry required_scope: civil_registry:evidence_verification dataset: civil_registry entity: birth_registration lookup: input: target.identifiers.national_id field: UIN op: eq cardinality: one rule: type: exists source: birth_record formats: - application/vnd.registry-notary.claim-result+json credential_profiles: - birth_record_sd_jwtserver.openapi_requires_auth defaults to true. Set it to false only for local testing or controlled tooling environments that need unauthenticated access to /openapi.json.
Authentication
Section titled “Authentication”auth.mode: api_key is for backend integrations. Configure at least one API key
or bearer token. Each entry has an id, a committed fingerprint, and scopes.
Use the smallest scope set each caller needs. Admin functions, including metrics
and credential status mutation, require registry_notary:admin.
auth.mode: oidc is for citizen and wallet flows. When OIDC is selected,
auth.api_keys and auth.bearer_tokens must be empty. Configure:
OIDC field names follow the shared Registry service runtime configuration conventions. Removed pre-convention names are rejected before deserialization with an error naming the replacement field.
issuer: expected token issuer.jwks_url: HTTPS JWKS URL, or HTTP loopback only withallow_insecure_localhost: true.audiences: accepted access-token audiences.allowed_clients: optional client allow-list.allowed_algorithms: explicit token signing algorithms accepted from the identity provider. Match the provider and do not mix unrelated algorithm families in one deployment.scope_claim,scope_separator, andscope_map: how external token scopes map to Registry Notary scopes.principal_claim: claim used for audit principal identity. The default issub.
For citizen self-attestation, the OIDC token must also carry a binding claim that Registry Notary uses to derive the requester and target context.
Source Connections
Section titled “Source Connections”Every source binding references one source_connections entry. A source
connection defines the upstream base URL, the authentication method used to
contact it, and connector-specific settings.
Use exactly one source authentication mechanism:
token_envfor a static bearer token.source_auth.type: oauth2_client_credentialsfor OAuth2 client credentials.
The OAuth2 client-credentials shape is:
source_auth: type: oauth2_client_credentials token_url: https://registry.example.gov/oauth2/client/token client_id_env: DCI_CLIENT_ID client_secret_env: DCI_CLIENT_SECRET request_format: json scope: registry.searchrequest_format is form by default and may be set to json for sources such
as the OpenCRVS DCI demo endpoint.
For DCI sources, check these fields carefully:
search_path: path appended tobase_url.sender_id,receiver_id,registry_type,registry_event_type, andrecord_type: envelope values expected by the upstream DCI implementation.query_type:idtype-valuefor one identifier lookup, orexpressionwhen the upstream supports fielded query expressions.records_path: JSON Pointer to the records array in a single response.field_paths: JSON Pointers for fields that the claim rule reads.bulk_mode: leavenoneuntil the source contract has been tested. Usedci_batched_searchorrda_in_filteronly when the upstream supports that access pattern.
For any source binding, query_fields can replace the single-field lookup
wire query when the source supports multi-field lookup. registry_data_api
sends them as query parameters, and DCI expression sends them inside the DCI
query envelope. For openfn_sidecar, Notary sends single reads through the
sidecar’s Registry Data API-shaped read endpoint, and sends batch reads through
the sidecar’s records:batchMatch endpoint. Leave query_fields empty for the
legacy single-field lookup.
For production, leave allow_insecure_localhost and
allow_insecure_private_network false unless the deployment review explicitly
accepts the private network source. Local demos may use them for loopback or
Docker Compose style setups.
OpenFn Sidecar Source Connections
Section titled “OpenFn Sidecar Source Connections”Use connector: openfn_sidecar when a target system needs OpenFn adaptor
execution, target credential handling, or output normalization outside Notary.
The source connection must use static sidecar bearer auth through token_env.
Do not configure target-service credentials in Notary; keep them in the sidecar
environment or secret store.
For high-assurance deployments, pin the sidecar runtime that Notary is allowed
to use with expected_sidecar. Notary reads the private sidecar assurance
endpoint before source reads and fails closed when the product identity,
environment, stream, config_hash, expression-hash verification, runtime
verification, or smoke-check state does not match the pin.
Single-read OpenFn sidecar example:
evidence: source_connections: openfn_crvs: base_url: http://127.0.0.1:9191 allow_insecure_localhost: true token_env: OPENFN_SIDECAR_TOKEN retry_on_5xx: false bulk_mode: none expected_sidecar: product: registry-notary-openfn-sidecar instance_id: civil-registry-sidecar environment: production stream_id: openfn-sidecar-runtime config_hash: sha256:2222222222222222222222222222222222222222222222222222222222222222 require_expression_hashes_verified: true require_runtime_verified: true require_smoke_verified: true claims: - id: date-of-birth title: Date of birth version: 2026-06 subject_type: person value: type: date inputs: - name: target.identifiers.national_id type: string source_bindings: crvs: connector: openfn_sidecar connection: openfn_crvs required_scope: civil_registry:evidence_verification dataset: civil_registry entity: civil_person lookup: input: target.identifiers.national_id field: national_id op: eq cardinality: one fields: birth_date: field: birth_date type: date required: true rule: type: extract source: crvs field: birth_dateOpenFn sidecar batch matching example with query_fields:
evidence: source_connections: openfn_crvs: base_url: http://127.0.0.1:9191 allow_insecure_localhost: true token_env: OPENFN_SIDECAR_TOKEN retry_on_5xx: false bulk_mode: openfn_sidecar_batch bulk_timeout_max_ms: 30000 expected_sidecar: product: registry-notary-openfn-sidecar instance_id: civil-registry-sidecar environment: production stream_id: openfn-sidecar-runtime config_hash: sha256:2222222222222222222222222222222222222222222222222222222222222222 require_expression_hashes_verified: true require_runtime_verified: true require_smoke_verified: true claims: - id: birth-record-exists title: Birth record exists version: 2026-06 subject_type: person value: type: boolean operations: batch_evaluate: enabled: true max_subjects: 100 inputs: - name: target.attributes.given_name type: string - name: target.attributes.family_name type: string - name: target.attributes.birthdate type: date source_bindings: crvs: connector: openfn_sidecar connection: openfn_crvs required_scope: civil_registry:evidence_verification dataset: civil_registry entity: civil_person lookup: input: target.attributes.birthdate field: birthdate op: eq cardinality: one query_fields: - input: target.attributes.given_name field: given_name op: eq - input: target.attributes.family_name field: family_name op: eq - input: target.attributes.birthdate field: birthdate op: eq matching: policy_id: civil-person-name-birthdate-v1 method: exact_name_birthdate target_type: Person allowed_purposes: - benefit_eligibility_check sufficient_target_inputs: - [target.attributes.given_name, target.attributes.family_name, target.attributes.birthdate] allowed_target_inputs: - target.attributes.given_name - target.attributes.family_name - target.attributes.birthdate collapse_matching_errors: true confidence: high fields: national_id: field: national_id type: string required: true birth_date: field: birth_date type: date required: true rule: type: exists source: crvsFor OpenFn sidecar connections:
- Set
retry_on_5xx: false. Notary does not retry OpenFn worker execution failures. - Use
bulk_mode: openfn_sidecar_batchonly after sidecar contract tests cover per-item not found, exact match, ambiguous match, missing response item, duplicate response item id, worker timeout, worker failure, and output projection. - In governed environments, set
expected_sidecaron every OpenFn sidecar connection. Local demos may omit it only when the assurance boundary is not part of the test.
See the deployment hardening runbook for network isolation requirements, responsibility boundaries between Notary and the sidecar, and deployment security expectations.
CEL Runtime
Section titled “CEL Runtime”CEL rules are evaluated out of process when the binary is built with
registry-notary-cel. The default posture is production-oriented: worker mode,
no queueing, bounded worker count, bounded frames, and regex disabled.
cel: mode: worker worker_count: 2 eval_timeout_ms: 2000 queue_max: 0 allow_regex: false max_expression_bytes: 8192 max_binding_json_bytes: 65536 max_result_json_bytes: 16384 max_string_bytes: 16384 max_list_items: 1024 max_object_depth: 16 max_object_keys: 256 worker_memory_bytes: 134217728 worker_stderr_bytes: 1024Set mode: disabled only when no configured claim uses rule.type: cel.
queue_max must stay 0; saturation fails fast so callers can retry or shed
load explicitly. Keep allow_regex: false unless the deployment has a reviewed
reason to permit regex-capable CEL helpers such as matches,
text.regex_extract, text.regex_replace, or validate.matches.
Claims
Section titled “Claims”A claim is a public capability. It should describe one thing Notary can evaluate or issue, such as “birth record exists” or “farmer under four hectares”.
Important fields:
id: stable machine id used by clients and credential profiles.title,version,subject_type, andvalue: operator and verifier metadata.inputs: request lookup paths. Supported paths includetarget.id,target.identifiers.<scheme>,target.attributes.<name>,requester.id,requester.identifiers.<scheme>,requester.attributes.<name>, andrelationship.attributes.<name>.source_bindings: upstream reads, lookup fields, required caller scope, and extracted source fields.rule:exists,extract, orcel.depends_on: prerequisite claims for CEL rules that reuse earlier results.operations: enable or capevaluateandbatch_evaluate.disclosure: default and allowed response disclosure modes.formats: response formats the claim can render.credential_profiles: profiles allowed to issue from this claim.
Avoid broad source bindings. A claim should read only the fields needed to evaluate that claim. If two credentials need different fields, prefer two claims or a small dependency graph over one over-broad claim.
Matching Policy
Section titled “Matching Policy”Each source binding has an optional matching block that gates and shapes how the
request is resolved to a source record before the read runs. The block is the
operator control behind identity and record
matching; read that page for the concepts and the
outcome model. With no matching block, a binding falls back to unrestricted,
identifier-only behavior.
source_bindings: person_record: connector: registry_data_api connection: civil_registry dataset: people entity: person lookup: input: target.attributes.birthdate field: birthdate matching: policy_id: person-name-birthdate-v1 method: exact_name_birthdate target_type: Person allowed_purposes: - benefit_eligibility_check allowed_relationships: - self - guardian relationship_purpose_scopes: guardian: - benefit_eligibility_check sufficient_target_inputs: - [target.attributes.given_name, target.attributes.family_name, target.attributes.birthdate] allowed_target_inputs: - target.attributes.given_name - target.attributes.family_name - target.attributes.birthdate confidence: highFields:
| Field | Purpose | Default |
|---|---|---|
policy_id | Stable label for this policy, returned in the response and audit trail | none |
method | Stable label for the matching method, returned in the response and audit trail | none |
target_type | If set, the request target.type must equal this value | unenforced |
requester_type | If set, the request requester.type must equal this value | unenforced |
allowed_purposes | Purposes this binding may be used for; empty means no purpose restriction here | empty |
allowed_relationships | Relationship types this binding accepts | empty |
relationship_purpose_scopes | Per-relationship purpose allow-list; a scoped relationship used for any other purpose is rejected with granular code relationship.purpose_not_allowed | empty |
sufficient_target_inputs | OR-of-AND groups of target paths; the request must satisfy at least one full group | empty |
allowed_target_inputs | Allow-list of target paths the binding may read; empty means unrestricted | empty |
allowed_requester_inputs | Allow-list of requester paths the binding may read; empty means unrestricted | empty |
collapse_matching_errors | Map every matching error to public evidence.not_available, keeping the granular reason in audit | true |
require_requester_reauthentication | Require the requester to reauthenticate before this binding reads | false |
confidence | Confidence label returned with a successful match | none |
Notes:
sufficient_target_inputsis an OR of ANDs. Each inner list is a complete set of paths that, when all present, is enough to match; the request needs to satisfy any one group. For example,[[national_id], [given_name, family_name, birthdate]]accepts either a national id alone or the full name-and-birthdate triple.allowed_target_inputsandallowed_requester_inputsare minimization controls. A request that supplies a path outside the allow-list is rejected, so a binding cannot over-collect by accident. Leave them empty only for identifier-only bindings that need no attribute minimization.relationship_purpose_scopesnarrows named relationships to specific purposes after the flatallowed_purposesandallowed_relationshipschecks. Each scoped relationship must also appear inallowed_relationships. Relationships with no entry in the map keep the unscoped behavior. Whencollapse_matching_errorsis on, callers seeevidence.not_availableand the granular code is retained for audit.collapse_matching_errorsdefaults to on. Turn it off only in a controlled environment where exposing not-found versus ambiguous versus rejected to the caller is acceptable, because those differences can be used as an existence oracle.confidenceis a fixed label for the source and method. It is returned verbatim on every successful match and does not measure how strong an individual match was.- Config validation rejects blank values:
policy_id,method,target_type, andrequester_typemust be non-empty when present, and the purpose, relationship, relationship purpose scope, and input-path lists must not contain blank entries.
Credential Profiles
Section titled “Credential Profiles”Credential profiles control SD-JWT VC issuance.
Required fields:
format: application/dc+sd-jwt.issuer: DID issuer for the credential.signing_key: key id fromevidence.signing_keys.vct: credential type URL.allowed_claims: explicit allow-list. Empty allow-lists are rejected.holder_binding: currently implemented holder binding isdid:jwk.disclosure.allowed: disclosure modes the profile may carry.
validity_seconds defaults to 600 and must be between 1 and
evidence.max_credential_validity_seconds. Keep token, proof, offer, and
evidence freshness windows short; set credential validity to the period the
issuing agency wants verifiers to treat the wallet-held VC as fresh. For
long-lived credentials, enable credential status or another revocation and
lifecycle surface.
Signing keys are covered in detail in
signing-key-provider.md.
Replay Store
Section titled “Replay Store”replay.storage: in_memory is acceptable for a single process in local
development. It is not acceptable for active-active serving because two
processes cannot see the same nonce or proof replay decisions.
When the in-memory backend is selected, /ready returns HTTP 503 with
status: degraded so operators do not miss the single-process replay posture.
Use Redis for multi-instance deployments:
replay: storage: redis redis: url_env: REGISTRY_NOTARY_REPLAY_REDIS_URL key_prefix: registry-notary connect_timeout_ms: 1000 operation_timeout_ms: 500Key fields: storage is in_memory or redis. redis.url_env names the
environment variable containing the Redis connection URL. redis.key_prefix
scopes keys for shared clusters. connect_timeout_ms and
operation_timeout_ms must both be greater than zero when Redis is
configured. Notary fails to start when the named Redis URL environment
variable is missing. /ready fails closed when the Redis replay backend is
unavailable.
See deployment-hardening-runbook.md for operational expectations, alerting guidance, and when to prefer Redis over in-memory.
Credential Status
Section titled “Credential Status”Credential status tracks the lifecycle of individual issued credentials so
verifiers can check suspension or revocation after issuance. It is disabled by
default. Enable it only when verifiers need a live status check beyond
credential expiry. base_url must be the public HTTPS issuer origin verifiers
can reach; retention_seconds should cover maximum credential validity plus
verifier tolerance. Use Redis for any deployment where more than one process
can issue credentials or where status records must survive a restart.
See credential-lifecycle-status.md for
status semantics, the full config block with all Redis fields, the status
payload shape, lifecycle state transitions, privacy boundary, and rollout
checklist.
Self-Attestation
Section titled “Self-Attestation”Self-attestation lets a citizen use their own OIDC token to evaluate or issue
only the claims that policy allows for the subject bound to that token. It
requires auth.mode: oidc. The subject binding is derived from a token claim
at request time; conflicting caller-supplied identity context is rejected
before any source read. All operations, claims, formats, disclosures, and
credential profiles are explicit allow-lists. Batch evaluation is not
supported. Credential profiles must use DID holder binding with proof of
possession and did:jwk. In-process rate limits are guardrails; public
deployments need gateway and identity-provider controls as well.
The config keys unique to this page are: subject_binding.token_claim,
subject_binding.normalize (must be exact),
subject_binding.allow_sub_as_civil_id, citizen_clients,
token_policy ceilings, allowed_operations, allowed_purposes,
allowed_claims, allowed_formats, allowed_disclosures,
credential_profiles, scope_policy, required_scopes,
allowed_wallet_origins, and rate_limits.
See the self-attestation operator guide for the full config blocks, identity-provider requirements, scope policy, wallet origin controls, rate-limit fields, and rollout checklist.
OID4VCI Wallet Facade
Section titled “OID4VCI Wallet Facade”OID4VCI depends on self-attestation. Enable it when a wallet should retrieve
Notary-issued credentials through OpenID4VCI-style metadata, offers, nonces,
and credential requests. The facade is narrow: credential format is dc+sd-jwt,
proof type is JWT with EdDSA, holder binding is did:jwk, and issuance is
backed by self-attestation policy.
oid4vci: enabled: true credential_issuer: https://notary.example.gov authorization_servers: - https://idp.example.gov accepted_token_audiences: - registry-notary-wallet credential_endpoint: https://notary.example.gov/oid4vci/credential offer_endpoint: https://notary.example.gov/oid4vci/credential-offer nonce_endpoint: https://notary.example.gov/oid4vci/nonce nonce: enabled: true ttl_seconds: 300 authorization: require_pkce_method: S256 proof: max_age_seconds: 300 max_clock_skew_seconds: 60 pre_authorized_code: enabled: true pre_authorized_code_ttl_seconds: 300 tx_code: required: true input_mode: numeric length: 6 esignet: client_id: registry-notary-rp client_signing_key_id: esignet-rp-key redirect_uri: https://notary.example.gov/oid4vci/offer/callback authorize_url: https://idp.example.gov/authorize token_url: https://idp.example.gov/oauth/v2/token issuer: https://idp.example.gov jwks_uri: https://idp.example.gov/.well-known/jwks.json scopes: - openid login_state_ttl_seconds: 300 credential_configurations: birth_record_sd_jwt: claim_id: birth-record-exists credential_profile: birth_record_sd_jwt format: dc+sd-jwt scope: birth_record vct: https://notary.example.gov/credentials/birth-record/v1 display_name: Birth record attestationPublic URLs must use HTTPS except for loopback development. Endpoint URLs must
live under credential_issuer, include a path, and have no query string.
Each vct must also be a public HTTPS URL and must match the referenced
credential profile vct.
authorization.require_pkce_method pins the PKCE challenge method wallets must
use. proof.max_age_seconds bounds how fresh a holder proof JWT must be, and
proof.max_clock_skew_seconds is the only clock difference tolerated when
checking that freshness.
pre_authorized_code.tx_code.required defaults to true. Set it to false
only for wallets that cannot send a transaction code. That compatibility mode
is reported as bearer_offer in admin posture and validates only when
pre_authorized_code_ttl_seconds is at most 300, because the offer URI is
then sufficient to redeem the code.
Each credential_configurations entry must be consistent with both the claim
and the credential profile it references:
claim_idexists inevidence.claims.claim_idis allowed byself_attestation.allowed_claims.credential_profileexists inevidence.credential_profiles.credential_profileis allowed byself_attestation.credential_profiles.- The claim references the credential profile.
- The profile allows the claim.
formatisdc+sd-jwt.vctmatches the credential profilevct.
See the OID4VCI wallet interop guide for the wallet flow sequence, authenticated pre-authorized-code flow details, nonce policy, Type Metadata serving, compatibility checklist, and troubleshooting.
Validation Workflow
Section titled “Validation Workflow”Run config checks before exposing the service:
registry-notary explain-config --config registry-notary.yaml --env-file .env.localregistry-notary doctor --config registry-notary.yaml --env-file .env.localregistry-notary doctor --config registry-notary.yaml --env-file .env.local --liveUse --live only against a test target or a controlled integration
environment. When live lookup values are supplied, the doctor output redacts
target ids and tokens, but the upstream source still receives a real lookup.
For local VC smoke tests:
registry-notary doctor \ --config registry-notary.yaml \ --env-file .env.local \ --issue-demo-vcRollout Checklist
Section titled “Rollout Checklist”- Each caller has only the scopes required for its claims and operations.
- Every source connection has exactly one auth method.
- Insecure source or JWKS allowances are absent outside local demos.
- Claims read only required upstream fields.
- Credential profiles list explicit
allowed_claims. - Signing keys are active only when they may sign; old public keys are
publish_onlyuntil their configured publication window ends or verifiers no longer need them. - Multi-instance deployments use Redis replay storage.
- Credential status, if enabled, uses the externally reachable issuer base URL and a shared store.
- Audit has a stable high-entropy
hash_secret_envvalue and off-host retention. /metricsis scraped with aregistry_notary:metrics_readcredential and normal network controls.doctorpasses without--live, then passes with a controlled live subject.