Repository navigation
feat(evidence): accept wallet-presented credentials as an acquisition channel - #2577
Merged
Merged
Conversation
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
jeremi
force-pushed
the
feat/evidence-wallet-m1
branch
from
November 7, 2026 04:02
bbdbe73 to
0ed97eb
Compare
… channel Evidence can answer a definition from a credential the holder presents from a wallet, beside the authoritative-source channel. A selector profile selects the channel, every assertion states it in a signed `channel` member, and relying parties accept `source` only unless they opt in through `acceptedChannels`. The wallet-facing OpenID4VP 1.0 exchange (HAIP 1.0 verifier flow) runs in the new supporting service `registry-evidence-oid4vp`; every credential check runs in Evidence through the pure `registry-evidence-presentation` crate, so a compromised verifier service can withhold or misroute a presentation but cannot forge one. A held result is released to Evidence once. Evidence gains the `wallet-presentation-v1` host protocol (SD-JWT VC, holder binding, issuer allow and deny lists, maximum credential age, `possession` or `attribute-match` subject link), the `name-match-v1` Rhai normalisation profile, `x5c` certificate chains on the issuer half, and an OpenID4VCI Draft 13 compatibility profile in `evidence-oid4vci`. Shared code moves to one implementation per concern across the products: access-token algorithms, fail-closed stop-signal handling, Prometheus exposition, compact JWS parsing, XChaCha20-Poly1305 sealing with dual-read of earlier records, and framework rejection normalisation. Platform gains `registry-platform-text`, `registry-platform-metrics` and `registry-platform-x509`. BReg blind-index lookups gain `nfc` and `case-fold` steps, a reviewed, resumable re-index for a changed normalization list, field-encryption checks that run before the apply commits, and a refusal of authored migration SQL that writes a blind-index column. Governed-action Evidence acceptance requires `channel: source`. BREAKING: `registry.assertion-evidence/v1` requires the signed `channel` member; assertions without it are refused. `evidence-oid4vci` answers framework rejections as JSON, and an undecodable token form as 400. A stop-signal handler that cannot be installed refuses startup in the adopting binaries. Each product's CHANGELOG states its changes. Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
jeremi
force-pushed
the
feat/evidence-wallet-m1
branch
from
November 7, 2026 04:45
0ed97eb to
27b0b3f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Evidence can answer a definition from a credential the holder presents from a wallet, beside the existing authoritative-source channel. A selector profile selects the channel, every assertion states it in a signed
channelmember, and relying parties acceptsourceonly unless they opt in. The wallet-facing OpenID4VP 1.0 exchange runs in a separate supporting service,registry-evidence-oid4vp; every credential check runs in Evidence through the pureregistry-evidence-presentationcrate, so a compromised verifier service can withhold or misroute a presentation but cannot forge one. The PR also lands the shared-code cleanup across products that the change made necessary.What changes
Evidence
acquisition.holderPresentedchannel and the built-inwallet-presentation-v1host protocol (SD-JWT VC, holder binding always required, issuer allow-list and deny-list, maximum credential age,possessionorattribute-matchsubject link). Behind the defaultwallet-presentationfeature.registry.assertion-evidence/v1gains the required signedchannelmember, and holder-presented assertions carrypresentedCredential. Payloads signed before this change are refused as malformed.acceptedChannels(default[source]) andmaximumCredentialAgeSeconds; the Rust client and the Node and Python bindings expose both.alternativesmember, and a caller entitled to the source channel alone sees exactly the source-only entry.normalise_name(profile, value)Rhai primitive with the single profilename-match-v1, for one exact comparison against one expected value. It folds case, Latin, Greek and Cyrillic accents and the optional Arabic and Hebrew vowel marks, and keeps the marks that are part of a spelling (Devanagari virama, nukta and anusvara, Thai tone marks), so distinct names never compare equal.evidencectl), and itsmaximumCredentialAgeSecondsmay be up to ten years, measured from the credential'siat.signing.certificateChainFile(x5cin SD-JWT VC headers and the JWKS), and an optional OpenID4VCI Draft 13 compatibility profile inevidence-oid4vci.registry-evidence-oid4vp(new supporting service)direct_post.jwt), four client identifier types, memory-only sessions with a byte budget, one canonicaltraceparent, a closed problem vocabulary underhttps://id.registrystack.org/problems/registry-evidence-oid4vp/, OpenAPI and runtime schema inproducts/evidence/generated/.Shared code (one implementation per concern)
registry-platform-oidc), fail-closed stop-signal handling, Prometheus exposition, compact JWS parsing (registry-platform-crypto), one XChaCha20-Poly1305 sealing primitive with dual-read of earlier records, framework rejection normalisation, and small helpers, adopted across Casework, Coordinator, Messaging, Document Store, Scheduling, Render, Discovery, Adapter, MCP, BReg and the Evidence family. Behaviour changes are in each product's CHANGELOG.evidence-oid4vci: shared security headers, bounded bodies,rateLimitswith429(offers budgeted per principal digest, wallet endpoints per endpoint), framework rejections as JSON, and a token form that cannot be decoded answered400 invalid_request.registry-platform-text,registry-platform-metricsandregistry-platform-x509;registry-mcpandbreg-mcprefuse startup when a stop-signal handler cannot be installed instead of waiting on a signal they cannot observe.nfcandcase-fold(NFC, full case fold, then NFC) steps, so composed and decomposed spellings of one name share a digest and auniquelookup no longer accepts both. Changing only a lookup'snormalizationlist becomes a reviewed, resumable re-index step that re-derives digests from the stored envelopes. Every field-encryption check (unique collisions, themaxTotalRowsceiling, entity and table binding, flip duplicates) runs before the apply commits anything, so a refused target is revertible; once the first chunk commits, reconcile reports the targetunresolvableand retrying the same apply resumes it. Authored migration SQL may not write a blind-index column.bregctl field-encryption preflightscans re-index steps and holds a table lock for the scan, so it belongs in a quiet window. Governed-action Evidence acceptance requireschannel: source.Breaking and behaviour changes (pre-1.0)
channelare refused; reissue them and regenerate golden fixtures.evidence-oid4vciare JSON;422on the token form became400.registry-platform-sdjwtno longer exports the Evidence holder-proof profile (moved toregistry-evidence-verifier);registry-mcp-edgere-exports the sharedAccessTokenAlgorithm, whosejsonwebtoken()is nowto_jsonwebtoken().Security review notes
This change touches authentication, assertion evaluation and signing, disclosure, audit and deployment defaults. Four independent read-only reviews ran on the branch: the wallet protocol service and its crypto, the holder-presented channel in Evidence, the shared-code cleanup, and the BReg blind-index range. None found a blocker, and each finding below was fixed and re-verified. Fixed in this PR:
evidence-oid4vcioffer limiter keyed its bucket on the raw principal, which the limiter refuses when it holds whitespace or passes 256 bytes, so an authorized client such asAdopter Portalgot429on every offer with no log (medium, a regression from the cleanup). The key is now a domain-separated digest of the principal and its kind, and a limiter fault is logged and answered500withoutRetry-After./readyfails once a client identifier certificate expires.case-foldfolded before composing, so a lookup declared[case-fold]alone gave two canonically equivalent spellings two digests and auniquelookup could hold both (integrity, unreleased). It now composes first.migration.review.blind-index-write-refused).name-match-v1dropped every nonspacing mark, so Devanagari names differing only by a virama, nukta or anusvara compared equal: a false subject match on a hard field. It now keeps those marks.attribute-matchwith no disclosed claim is refused at startup; discovery no longer reveals that a wallet channel exists to a source-only caller; a misassigned channel Source is refused at startup naming the member to repair.Known limits, documented in known-limitations and the threat model: with a cross-device flow a holder can present into a session someone else started (
possessionproves a trusted credential was presented, not who presented it); the presenting principal is stated by the caller (one relying party per Evidence deployment for wallet channels); the wallet-facing limiters of both supporting services bound each endpoint as a whole, so one source can spend a budget and per-source rate limiting belongs at the ingress.New negative tests are registered in
products/evidence/contracts/security-test-traceability.yamland bound tosecurity-invariant-matrix.yaml.Evidence (checks run)
The branch is one signed-off commit on top of current
main, squashed so the rebase merge queue can take it; the local checks below ran on the pre-squash head, whose tree differs only inCargo.lock: mergingmainhad taken main's yankedcrypto-bigint0.7.6, and the squash restores the branch's 0.7.10 (every dependent asks for^0.7.5;cargo deny checkand a check of the crypto consumers pass on it).CI (
ci:full) is green on the branch head: all 65 jobs that run on a pull request pass, including every Rust test lane, every BReg PostgreSQL lane, the Evidence PostgreSQL suites, the Evidence tutorials, the BReg to Evidence composition proof, the native client bindings, and the editor extensions. Run locally on the branch head, current withmain, unless a line says otherwise.cargo fmt --check; clippy-D warningson the whole workspace, and on the Evidence family without default features.evidence_journeyanddevice_run(simulated wallet) pass.main(which brought the deterministic Rhai base and the docs cut, and no storage change): Evidence status ledger, transitions and privacy (47), OID4VCIstore_contract(36); BRegtest-postgres.shlane by lane as CI shards it,postgres:1/3to3/3andimmediate-actions(1217, including the operator proof, the Evidence provider proof and the action-evidence trio); BReg native timer replicas against a server with 400 connections (29).main, so both were run locally on the branch head: all 13 platform and Evidence fuzz targets for 60 s each on the host target with the CI libFuzzer limits (no crashes), andcargo llvm-covoverregistry-platform-*with all features (90.54% of lines against the 80% floor).proposal_turn_filter_pages_a_bounded_scan_without_gaps_or_duplicates(its 2 s action deadline),activation::in_flight_predecessor_answer_is_terminal_after_activation(a lease settled late), andnative_database_session_loss_rolls_back_active_transaction_for_peer_recovery(run against a 100-connection server; it passes against 400); and the Adapter engine'sevidence_attempt_defaults_and_overrides_complete_after_ten_seconds, which pauses tokio time across a real socket round-trip (10 of 10 isolated reruns pass).certificate_expiry_logs_one_warning_without_certificate_contentfailed about 1 run in 40 under load. It captured logs with a thread-scoped subscriber, andtracingcaches a callsite as disabled when a parallel test with no subscriber reaches it first. It now runs in its own test binary under the process-wide subscriber (0 failures in 990 loaded runs); the service code is unchanged.registry-breg,registry-bregctl,registry-breg-mcpandregistry-breg-reviewtests (2905);products/breg/scripts/check-contracts.sh(133). Coordinator and its client (709), dependency direction and schemas. The Rhai consumersregistry-platform-script,registry-platform-hooks,registry-adapter-engineandregistry-messaging(811).check-contracts,check-source-neutralityand its self-test (now scanning the oid4vp and presentation crates and their manifests),check-verifier-portability,check-config-key-paths,check-authoring-schema,check-authoring-no-io; identifiercheck.shand--check-references; dependency-direction checks for six products; config conformance;registry-release validate-currentandvalidate-docsets;cargo deny check; Node client sync;ci_changestests; docsnpm test(948) andnpm run check;gitleaks dirwith the repository config over the committed tree (0 findings).rust/hard-coded-cryptographic-valuealerts on this PR, all triaged as not reaching production: seven are fixed keys, nonces and golden vectors inside#[cfg(test)]modules ofxchacha_seal.rs, the OID4VCIsealing.rsand the BRegcursor.rs(the golden vectors pin the stored form deployed databases already hold); the eighth (xchacha_seal.rs:57) is a zeroed nonce buffer thataws_lc_rs::rand::filloverwrites before use, and the fixed-nonce helper it feeds is private and reached only fromsealand tests. All 8 are dismissed with this triage.registry-evidencewithoutwallet-presentation, and runs the composed journey and the simulated device run.Not in this PR
response_codeflow, a typed channel picker in the Node and Python bindings, and a session-bound reader for multi-relying-party deployments.DCO
Signed-off-bytrailer.