feat(eudi): trust levels replace the Verified flag, with a typed gate error (#655, #656) - #664
Draft
dobby-coder[bot] wants to merge 3 commits into
Draft
feat(eudi): trust levels replace the Verified flag, with a typed gate error (#655, #656)#664dobby-coder[bot] wants to merge 3 commits into
dobby-coder[bot] wants to merge 3 commits into
Conversation
… error Two slices of the issuer & verifier trust ladder (#654). #655 — the dark tracer. `clientmodels.TrustedParty` carries a `TrustLevel` ("low" | "medium" | "high") instead of the boolean `Verified`, and every party the wallet talks to is ranked before it reaches the permission screen. `services.TrustService` is the single home for that evaluation and hands each session one pinned `trust.View`, so a refresh landing mid-session cannot change what that session decided. Only the certificate channel is wired up: a party whose chain validated against the Yivi anchors ranks high, everything else ranks low, and no verdict carries a listing. The recognized-list channel adds medium in #657. Evaluation is fail-soft by construction — no signature in the `trust` package returns an error — so no evaluation path can fail a session. #656 — the typed gate error. A party that fails identity validation is not a low-trust party but a session failure, and the app has to tell it apart from a network hiccup to render the blocking copy. Both EUDI protocol paths now set `error_type: "party_validation_failed"` on the session error when their gate rejects the party; generic failures keep reporting no error type. Breaking for irmamobile: `verified` is gone from the wire, replaced by `trust_level`, with any checkmark derived as medium or high. The field is absent when nothing was evaluated, which is not the same as low. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The stored and obtainable credentials in an OpenID4VP disclosure plan built their issuer party without a level, so the same issuer read as low in the credential list and as unevaluated one screen over. Both carry no evidence, so both rank low. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The typed party_validation_failed error was set one call deeper than it is read: obtainCredential marks the failure, obtainCredentials wraps it per credential configuration, and perform reads the marker to type the session error. That wrapping used %v, which flattens the error to a string and drops the marker, so in a real issuance session every rejected issuer still reached the app as a generic failure. The existing tests only called obtainCredential directly, one frame below the wrapping, so they passed either way. Wrap with %w, and cover it at the level the bug lives at: a test through obtainCredentials that fails with %v and passes with %w. Also add the session-level coverage the slices ask for, and the ladder's own unit test: - an X.509 verifier under the Yivi anchors renders high in an OpenID4VP session (the EUDI reference verifier, in the irma-sdjwt disclosure suite); - an OpenID4VCI issuer renders a rung on both the session header and each offered credential, and a low issuer still gets to offer; - IsTrusted covers all four levels, and an unevaluated level is absent from the JSON so the app can render it levelless. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
Author
|
Pushed 3b788bc after the description above was written, so the "Tests" and "Verification" sections there are one commit stale. What changed (details appended at the bottom of the description):
Every check on the head SHA is green, including the docker-backed |
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.
Two slices of the trust ladder tracking issue #654: the dark tracer (#655) and the typed gate error (#656). Together they put the whole ladder on the wire and give the app a way to tell a rejected party from a broken network, without any list infrastructure yet.
Closes #655
Closes #656
Part of #654
What is in here
#655, the dark tracer:
TrustLevelreplacesVerifiedclientmodels.TrustedPartycarries aTrustLevel("low","medium","high") instead of the booleanVerified, and every party the wallet talks to is ranked before it reaches the permission screen.eudi/trustis the leaf vocabulary:Evidence,Verdict,Listing, and the two-methodView(Verifier/Issuer, so a caller cannot forget which role it is asking about). No signature in the package returns an error, so no evaluation path can fail a session.services.TrustServiceis the single home for evaluation, built to the same shape asRevocationService. Each session takes one pinnedtrust.Viewfrom it, so a list refresh landing halfway through cannot change that session's verdicts. Only the certificate channel is wired up today, so every verdict carries a nilListing.#656, the typed
party_validation_failedsession erroreudi.PartyValidationFailedmarks an error as the identity gate rejecting the party. Both protocol gates set it, and the session reportserror_type: "party_validation_failed"so the app can render the blocking copy instead of a generic error screen. Generic failures (transport, protocol) keep reporting no error type.Breaking change, irmamobile lockstep
verifiedis gone from the wire and replaced bytrust_level. The app derives whatever it showed forverifiedfrom the level being medium or high (TrustLevel.IsTrusted()on the Go side). The field is omitted when nothing was evaluated, which is deliberately distinct fromlow: absent means "nothing was evaluated",lowmeans "nobody vouches". The CHANGELOG carries the breaking note.Three decisions that need your call
NewTrustService()takes no checker yet. The design sketch in #655 hasNewTrustService(checker *lote.Checker)with a nil checker meaning dark mode. Landing that now would mean creating an emptylotepackage whose only purpose is to be nil, so the constructor grows the parameter in #657 instead, where the checker exists. One line at the call site.EvidenceomitsYiviSchemeCert. In both protocol paths a validated certificate is already a Yivi-anchored one, so the flag would have no producer distinct fromCertificate != nil.EvidencecarriesCertificateandIdentifiers; the second is what the recognized-list channel keys on. Happy to add the flag when a channel needs to tell the two apart.Log entries render levelless. The legacy
IssuerVerifiedboolean only ever recorded "Yivi vouches", so on read it maps to high or to nothing at all. Log entries get their own nullable level columns in #658; until then an EUDI issuance log carries no rung, exactly as pre-feature rows do.Tests
New, all runnable without docker:
eudi/trust: the certificate channel for both roles, including empty evidence.eudi/services:TrustServiceruns dark, and still hands out a usable view for an already-cancelled context (fail-soft).eudi/openid4vp: four whole sessions driven end to end against an httptest authorization-request endpoint. An X.509 verifier under the Yivi anchors reaches the permission screen as high; a baredid:webverifier reaches it as low (the session proceeding is the fail-soft assertion); a revoked verifier certificate ends the session withparty_validation_failedand nothing asked of the user; transport and protocol failures carry no error type.eudi/openid4vci: an issuer whose credential does not verify against its chain produces a party-validation failure, a 400 from the credential endpoint does not; the issuer's rung reaches every offered credential.clientandirma: the two IRMA display mappings, scheme-registered to high and unregistered to low.Mutation-checked the two assertions that guard the decisions: flipping the certificate channel to low fails the X.509 session test, and dropping the
PartyValidationFailedwrap fails the OpenID4VCI gate test.Verification
go build ./...,go vet ./...,gofmt,go fix -diff ./...,ineffassign ./...,misspellandstaticcheck -checks "all,-ST1000,-ST1003,-SA1019,-SA1029" ./...all clean.go test ./eudi/... ./client/... ./common/... ./irma/ ./irma/irmaclient/passes.internal/sessiontestfails locally on connect and bind errors only (the EUDI verifier on :8089 and the keyshare server on :8080), which is the documented no-services behaviour. Its assertions were updated fromVerifiedtoTrustLeveland need the CItestjob to confirm.What is left of #654
Slices #657 through #662 are untouched: the LoTE recognized-list channel and its test server, the onboarded-by-Yivi marking and display precedence,
x5csurfacing on the issuer side, activity-log level columns, transitions on stored credentials, and the coverage matrix. #655 was the one slice that had to make the breakingclientmodelschange, so it is out of the way for the rest.Two notes for whoever picks up the next slice:
staticcheckneeds to be built with a toolchain at least as new asgo.mod's, soGOTOOLCHAIN=go1.26.4 go install honnef.co/go/tools/cmd/staticcheck@2025.1.1on a container with an older base Go, otherwise it analyses nothing and reports a misleading clean run.eudi/openid4vp/client_trust_test.gohas astubQueryHandlerand a loopbackdid:webfixture that Trust levels: recognized-list channel (LoTE checker) — medium #657's session tests can reuse.Update: gate marker was dropped one frame below where it is read
party_validation_faileddid not actually reach the app in a real OpenID4VCI session.obtainCredentialmarks the failure,obtainCredentialswraps it per credential configuration, andperformreads the marker to type the session error, but that wrapping used%v. That flattens the error to a string and drops the marker, so every rejected issuer arrived as a generic failure. The tests above missed it because they callobtainCredentialdirectly, one frame below the wrapping, where the marker is still intact.Fixed by wrapping with
%w, plus a test at the level the bug lives at (Test_openid4vciSession_obtainCredentials_validationFailureSurvivesWrapping): it fails with%vand passes with%w, verified both ways.Added the session-level coverage the two slices ask for, which the unit tests above do not reach:
internal/sessiontest: an X.509 verifier under the Yivi anchors rendershighin a real OpenID4VP session against the EUDI reference verifier.internal/sessiontest: an OpenID4VCI issuer renders a rung on both the session header and each offered credential, and an unvouched-for issuer still gets to offer. This is the fail-soft assertion on the issuer side.common/clientmodels:IsTrustedover all four levels, and an unevaluated level is absent from the JSON so the app can render it levelless rather than as a verdict oflow.Both session tests need the CI
testjob to run, like their neighbours. Locally verified:go build ./...,go vet ./...,gofmt -l .andgo fix -diff ./...clean, andgo test ./common/... ./eudi/... ./client/... ./irma/...passes, includingTestSessionHandler/irma/disclosureand/irma/signatureininternal/sessiontest, which run without the docker services and cover the IRMA requestor mapping end to end.