Skip to content

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
masterfrom
feat/trust-levels-dark-tracer-655
Draft

feat(eudi): trust levels replace the Verified flag, with a typed gate error (#655, #656)#664
dobby-coder[bot] wants to merge 3 commits into
masterfrom
feat/trust-levels-dark-tracer-655

Conversation

@dobby-coder

@dobby-coder dobby-coder Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

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: TrustLevel replaces Verified

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.

  • eudi/trust is the leaf vocabulary: Evidence, Verdict, Listing, and the two-method View (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.TrustService is the single home for evaluation, built to the same shape as RevocationService. Each session takes one pinned trust.View from 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 nil Listing.
  • Both EUDI protocols and both IRMA display paths feed the same DTO. An X.509 verifier under the Yivi anchors ranks high, a bare DID verifier ranks low and the session proceeds, an OpenID4VCI issuer ranks low (its certificate is not surfaced until Trust levels: issuer side — x5c surfacing + per-credential levels #660), an IRMA issuer or requestor registered in a valid scheme ranks high.

#656, the typed party_validation_failed session error

eudi.PartyValidationFailed marks an error as the identity gate rejecting the party. Both protocol gates set it, and the session reports error_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

verified is gone from the wire and replaced by trust_level. The app derives whatever it showed for verified from the level being medium or high (TrustLevel.IsTrusted() on the Go side). The field is omitted when nothing was evaluated, which is deliberately distinct from low: absent means "nothing was evaluated", low means "nobody vouches". The CHANGELOG carries the breaking note.

Three decisions that need your call

NewTrustService() takes no checker yet. The design sketch in #655 has NewTrustService(checker *lote.Checker) with a nil checker meaning dark mode. Landing that now would mean creating an empty lote package 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.

Evidence omits YiviSchemeCert. In both protocol paths a validated certificate is already a Yivi-anchored one, so the flag would have no producer distinct from Certificate != nil. Evidence carries Certificate and Identifiers; 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 IssuerVerified boolean 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: TrustService runs 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 bare did:web verifier reaches it as low (the session proceeding is the fail-soft assertion); a revoked verifier certificate ends the session with party_validation_failed and 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.
  • client and irma: 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 PartyValidationFailed wrap fails the OpenID4VCI gate test.

Verification

  • go build ./..., go vet ./..., gofmt, go fix -diff ./..., ineffassign ./..., misspell and staticcheck -checks "all,-ST1000,-ST1003,-SA1019,-SA1029" ./... all clean.
  • go test ./eudi/... ./client/... ./common/... ./irma/ ./irma/irmaclient/ passes.
  • internal/sessiontest fails 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 from Verified to TrustLevel and need the CI test job 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, x5c surfacing 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 breaking clientmodels change, so it is out of the way for the rest.

Two notes for whoever picks up the next slice:


Update: gate marker was dropped one frame below where it is read

party_validation_failed did not actually reach the app in a real OpenID4VCI session. obtainCredential marks the failure, obtainCredentials wraps it per credential configuration, and perform reads 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 call obtainCredential directly, 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 %v and 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 renders high in 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: IsTrusted over all four levels, and an unevaluated level is absent from the JSON so the app can render it levelless rather than as a verdict of low.

Both session tests need the CI test job to run, like their neighbours. Locally verified: go build ./..., go vet ./..., gofmt -l . and go fix -diff ./... clean, and go test ./common/... ./eudi/... ./client/... ./irma/... passes, including TestSessionHandler/irma/disclosure and /irma/signature in internal/sessiontest, which run without the docker services and cover the IRMA requestor mapping end to end.

… 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>
@dobby-coder
dobby-coder Bot requested a review from rubenhensen July 31, 2026 14:38
dobby-coder Bot and others added 2 commits July 31, 2026 14:40
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>
@dobby-coder

dobby-coder Bot commented Jul 31, 2026

Copy link
Copy Markdown
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):

  • party_validation_failed did not actually reach the app in an OpenID4VCI session. The marker is set in obtainCredential and read in perform, and the per-configuration wrapper in between used %v, which flattens the error and drops the marker. The tests missed it because they enter at obtainCredential, one frame below the wrapping. Fixed with %w and covered at the frame the bug lives at.
  • Added the session-level coverage both slices ask for: an X.509 verifier under the Yivi anchors renders high in a real OpenID4VP session, and an OpenID4VCI issuer renders a rung on the session header and on each offered credential while still being allowed to issue.
  • common/clientmodels: IsTrusted over all four levels, and an unevaluated level is absent from the JSON.

Every check on the head SHA is green, including the docker-backed test job, so the two new session tests are confirmed rather than only written.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Trust levels: typed party_validation_failed session error Trust levels: dark tracer — TrustLevel replaces Verified, certificate channel end-to-end

0 participants