You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
#240 shipped a documented procedure for validating QAuth's OID4VP verifier against a
real wallet — docs/wallet-interop-manual-validation.md — and has never executed it. The
document says so itself in its second paragraph (docs/wallet-interop-manual-validation.md:9-13)
and its findings table is empty (:40-42).
Consequently every conformance claim QAuth can currently make about its wallet path is
self-consistent rather than verified: our verifier, talking to a mock we wrote, answering
requests the way we read the spec. This issue is the spike that replaces that with evidence
from a counterparty we did not author, and records the outcome — including a negative one —
in the document #240 already created.
This is a research/validation spike, not a feature. Nothing here changes shipped behaviour.
Defects the pass surfaces become their own issues; this one closes when the evidence exists.
What is true today (verified on test/240-wallet-e2e, PR #375)
The E2E suite's only counterparty is apps/auth-server/src/testing/mock-wallet.ts, built for test: [T4] E2E integration tests — reference/mock wallet (ADR-004) #240. It is deliberately constructed from @qauth-labs/core-crypto + jose with no import
of the code under test (mock-wallet.ts:29-35), which is the strongest thing an in-repo
mock can do — and still not an independent implementation. Its own docblock says as much: "Not a conformance target, and not a claim that any real wallet behaves this way"
(mock-wallet.ts:49-53).
The suite drives the server through app.inject()
(apps/auth-server/src/app/wallet-federation.integration.test.ts:96 onward). No socket, no
TLS, no reverse proxy, no real browser cookie enforcement is exercised anywhere.
The mock already signs and key-binds with ES256 end to end (mock-wallet.ts:141), so the verification side is not the gap — QAuth verifies the algorithm real wallets actually use.
The only shipped issuer-key resolver is the statically pinned one: createConfiguredIssuerKeyResolver (libs/fastify/plugins/federation/src/lib/wallet-credential-verification.ts:116-124)
→ createStaticIssuerKeyResolver (libs/server/federation/src/oid4vp/issuer-key-resolution.ts:152).
The profile table declaresissuerKeyResolution: ['x5c', 'issuer-metadata']
(libs/server/federation/src/profiles/verifier-profiles.ts:41) but neither backend exists.
This is load-bearing for the exercise: see "The issuance problem" below.
haip-1.0 still refuses to boot, and that refusal is tested
(apps/auth-server/src/app/wallet-federation-haip.integration.test.ts:51-78). So the pass this
issue asks for is a oid4vp-1.0-base pass, as the document's own checklist already scopes it.
Correction to fold into the document while you are there
docs/wallet-interop-manual-validation.md:31-34 and its HAIP heading (:166) attribute the
HAIP block to #298. That attribution is stale. #298 closed 2026-07-26 and its crypto
landed: JwsAlgorithm = 'EdDSA' | 'RS256' | 'ES256'
(libs/core/crypto/src/lib/algorithms.ts:61), plus libs/core/crypto/src/lib/jwe.ts, encryption-keys.ts and signing.es256.test.ts.
The live blocker is now provisioning and wiring, not the crypto library — apps/auth-server/src/app/crypto-capabilities.ts:79 hardcodes ES256: false and :95 responseEncryption: false, and the comments there state exactly why: no env var, schema field,
JWKS entry or code path exists to provision a P-256 signing key, and there is no direct_post.jwt intake route to decrypt at. Both are attributed there to #233/#234. wallet-federation-haip.integration.test.ts:20-27 carries the same stale reasoning.
The conclusion is unchanged (haip-1.0 cannot boot, and must not), only the reason moved.
Correcting it matters because someone reading the document today would wait for an issue that
already closed.
Tasks
Stage 0 — the cheapest independent counterparty first
Run the OpenID Foundation conformance suite's OpenID4VP verifier test plan against a
deployed QAuth on oid4vp-1.0-base. The suite acts as the wallet; it is free to self-host
from GitLab, and it is an implementation nobody on this project wrote. Configure QAuth's OID4VP_WALLET_INVOCATION_ENDPOINT to the suite's authorization_endpoint rather than openid4vp://.
Stage 1 — issuance (expect this to be the hard part)
Pick an issuance source and get one SD-JWT VC into a wallet you can drive. See "The
issuance problem" — do not start Stage 2 before this is solved, because presentation
cannot be validated without a credential the wallet will actually hold.
Extract that issuer's public JWK and pin it in OID4VP_ISSUER_JWKS, plus the issuer
identifier in OID4VP_TRUSTED_ISSUERS. Record how you obtained the key (x5c leaf?
published JWKS? out of band?) and whether it rotates — that is a finding about QAuth's
missing resolver backends, not just setup noise.
Stage 2 — the run
Deploy QAuth on a host a phone can reach over publicly-trusted HTTPS, with JWT_ISSUER
set to that exact origin — response_uri is derived from it
(apps/auth-server/src/app/helpers/wallet-login-request.ts:140).
Work docs/wallet-interop-manual-validation.md's base-profile checklist (:76-165)
top to bottom against the real wallet, recording behaviour on passes as well as failures.
Deploy from main/apps/auth-server/src/main.ts, not through the E2E harness — see
"Do not test through the harness".
Stage 3 — record
Fill the document's findings table (:40-42) with wallet, build number, date, profile
attempted and outcome. A "we tried X and it did not work" row is the deliverable too.
Add a checklist item for the rate-limit hazard: OID4VP_RESPONSE_RATE_LIMIT at its
production default can 429 a manual run, and the checklist's own rule that refusals are
indistinguishable on the wire (:144-145) makes a 429 easy to misread as a security
refusal. The E2E sidesteps this by raising every limit to 10000
(apps/auth-server/src/testing/e2e-harness.ts:138-140); a real run does not get that.
Decide and record whether the evidence justifies opening an OpenID4VP 1.0
self-certification issue (see "Relationship to certification").
Technical Details
Candidate counterparties
Named because a category ("an EUDI wallet") is not actionable. The verified/unverified split is
the point — do not let an unverified row become an assumption, which is the failure mode #240 and ADR-004 both already recorded.
Candidate
What it is
Status
OIDF conformance suite, OpenID4VP verifier plan (gitlab.com/openid/conformance-suite)
An independently-written "fake wallet" that drives your verifier. Self-hostable via Docker, free for development
Verified: OIDF opened OpenID4VP 1.0 self-certification on its platform 2026-02-26; the verifier flow is documented (point your verifier at the suite's authorization_endpoint, the test waits for your request)
EUDI Wallet reference implementation (eu-digital-identity-wallet on GitHub — eudi-app-android-wallet-ui, eudi-lib-android-wallet-core, and the iOS wallet kit)
The flagship ecosystem's own wallet, Android + iOS
Partly verified: the org, the apps and SD-JWT VC + mdoc support are real and current. Its exact OID4VP 1.0 (Final) vs. draft posture as of today is NOT verified here — confirm against the build you install before recording a result
EUDI reference issuer (issuer.eudiw.dev, dev instance dev.issuer.eudiw.dev; source eudi-srv-web-issuing-eudiw-py, eudi-srv-pid-issuer)
PID / (Q)EAA issuer, mso_mdoc + SD-JWT VC, with a form-based test authentication mode
Partly verified: exists and issues SD-JWT VC. Public references describe it as OpenID4VCI draft 13, not 1.0 Final — irrelevant to QAuth (we do not speak OID4VCI) but relevant to which wallet builds will accept it
Animo playground (playground.animo.id) + Paradym Wallet
Hosted issuer and verifier implementing OID4VP 1.0, SD-JWT VC, mdoc and most of HAIP
Partly verified: the playground and its claimed coverage are real. Best current candidate for solving Stage 1 and Stage 2 with one counterparty — verify the claim yourself before relying on it
walt.id (waltid-identity)
The reference implementation ADR-004 already names (docs/adr/004-wallet-agnostic-federation.md:27, :420)
Partly verified: their "verifier 2" is described as fully OID4VP v1 compliant with the old verifier deprecated in Q2 2026. Their wallet's OID4VP 1.0 posture is a separate question and is not verified here
wwWallet, Lissi, Sphereon (ADR-004 :19, :93)
Other named ecosystem wallets
Unverified. Named for completeness only. Do not attempt one of these without first checking its current OID4VP posture
A wallet is a phone application. localhost is not reachable from it — the document's
"Before you start" (:44-49) already covers the tunnel/deploy requirement and the JWT_ISSUER
constraint, and it is correct.
The issuance problem — call it out rather than glossing it
You cannot validate presentation without an issuer the wallet will accept. QAuth is a
Verifier and issues nothing; the document states this (:51-54) but does not solve it, and it
is the step most likely to consume the spike's budget.
Two constraints have to be satisfied simultaneously:
The wallet must accept the credential. Ecosystem wallets are frequently pinned to a
specific issuer, trust list or protocol draft. An arbitrary SD-JWT VC minted with jose will
not necessarily be storable in an EUDI reference build.
QAuth must be able to pin the issuer's key. Today the only resolver is the static OID4VP_ISSUER_JWKS map (issuer-key-resolution.ts:152). Real ecosystem issuers convey keys
via x5c and rotate them. There is no x5c backend and no issuer-metadata backend, despite
both being declared in the profile (verifier-profiles.ts:41). So the operator must extract a
leaf public key by hand, and the pass breaks the moment the issuer rotates.
Three paths, in rising cost:
A hosted playground issuer (Animo) that the same vendor's wallet already trusts — solves
(1) cheaply; (2) still needs a manual key extraction.
The EUDI dev issuer with the EUDI reference wallet — closest to the flagship ecosystem, and
the highest-value evidence; likely the most setup.
Self-hosting an issuer (walt.id, or the EUDI issuer source) — full control of the key, so
(2) is trivial; but you must then convince a real wallet to trust it, which reintroduces (1).
If constraint (2) turns out to be the wall rather than the annoyance, that is itself the
headline finding: it means an x5c issuer-key resolution backend is a prerequisite for any
real-ecosystem deployment, and it should be filed as a blocking issue against epic #231 rather
than worked around locally for the duration of the run.
What a pass settles that the mock cannot
The mock and the verifier share one author's reading of the spec. These are the claims that
reading cannot support, and that this exercise buys:
Request construction is consumable by software we did not write. A real wallet parses our
invocation URI, dcql_query, and client_metadata — or rejects it. The mock's parseOid4vpRequest (mock-wallet.ts:313) was written against our emitter.
The vp_token shape we demand is the shape wallets emit. The suite already asserts we
read only a DCQL-keyed object and refuse a Presentation Exchange envelope
(wallet-federation.integration.test.ts:821-822). Whether a shipping wallet agrees is
currently unknown, and the document flags exactly this as an interop finding rather than a
QAuth bug (:107-110).
SD-JWT VC encoding agreement — disclosure encoding, _sd_alg, cnf, and Key Binding JWT aud/nonce construction against a credential we did not mint.
The consent screen renders our client_metadata as something a human can act on.
Real-transport behaviour: HTTPS, cross-device QR handoff, __Host- cookie enforcement by
an actual browser, and a form-encoded direct_post from an unauthenticated mobile network
path. app.inject() exercises none of it.
The refusal paths hold against real inputs, not synthesised ones — in particular the
ADR-009 §1 case the document calls a full authentication bypass if it succeeds (:130-133).
Do not test through the E2E harness
apps/auth-server/src/testing/e2e-harness.ts:184 registers the app as fp(app), whereas
production registers it plain (apps/auth-server/src/main.ts:119). The harness's comment
(:179-183) is honest about it and argues the difference is encapsulation-only — db/dbPool/ redis land on the root instance so resetE2eState (:88-91) can truncate the database the
server is actually using. That reasoning is sound for the assertions the suite makes.
It is still a real-assembly difference, and it is not the only one:
main.ts:86-116 registers @fastify/swagger (always) before the app; the harness registers
neither swagger nor swagger-ui.
main.ts:37-39 sets requestIdHeader, logController and genReqId; the harness sets none.
The harness raises every rate limit to 10000 (:136-141).
Inference, not proof: none of these is known to affect wallet-flow behaviour, and I did not
find a case where they do. But #370 is the standing precedent for exactly this class — a defect
invisible to lint, typecheck and every test because they all resolve through the workspace, and
only the packaged artifact boots differently. An interop run whose purpose is to test the real
assembly should test the real assembly: deploy the image, run main.ts.
Decision: counterparty ordering
Two defensible sequences, and the answer changes the budget by a large factor:
(a) Conformance suite first, wallet second. The OIDF suite is self-hostable, scriptable,
and needs no phone, no tunnel and no issuance — it drives our verifier directly. It would
surface protocol-level defects at a fraction of the cost, leaving the phone run to test only
what a synthetic wallet cannot (UI, transport, human consent).
(b) Real wallet first. Higher-fidelity evidence, and the only thing that answers "does a
shipping wallet interoperate with us"; but gated on the issuance problem above, which may not
resolve quickly.
Recommendation: (a), then (b). Sequencing them this way means a phone run starts from a
verifier already known to be protocol-clean, so any failure it finds is attributable to
transport, UI or ecosystem policy rather than to our request construction. It also means the
spike produces some independent evidence even if Stage 1 stalls. Owner decides at
implementation time.
Decision: where the evidence lives
docs/wallet-interop-manual-validation.md is the obvious home and already has the table. But a
conformance-suite run produces per-module results that look much more like docs/oidf-op-certification-runbook.md — which is the existing precedent for "operator-facing
runbook with file:line anchors and [RE-CHECK AT RUN TIME] markers".
Options: extend the existing manual-validation doc with a suite section, or add a sibling docs/oid4vp-interop-runbook.md modelled on the OP runbook and cross-link. Recommendation:
extend the existing document for the wallet findings table (it is already the named reporting
target, :188-193) and add the suite runbook separately only if Stage 0 actually happens — do
not create an empty second document.
Correction worth stating explicitly:#286 is CLOSED, sits on the T3 milestone, and is
about OP certification (Basic OP + Config OP). It deliberately scoped OID4VP/OID4VCI/HAIP
self-certification out, noting that programme went live 2026-02-26.
So this issue does not feed #286. What it does is produce the prerequisite for a separate,
currently unowned track: there is no issue for OpenID4VP 1.0 self-certification. #286 and docs/oidf-op-certification-runbook.md are the precedent — that runbook is what a
certification-grade artifact looks like in this repo, and Stage 0 of this issue is the same
motion (deploy publicly, drive the OIDF suite, export results) against a different test plan.
Stage 3 asks for an explicit decision on whether to open that issue, rather than opening it
speculatively.
Acceptance Criteria
docs/wallet-interop-manual-validation.md's findings table has at least one row with a
named counterparty, its build/version, a date, the profile attempted, and an outcome. A
documented failure satisfies this; an empty table does not.
The document's "has not been executed" banner (:9-13) is either removed or narrowed to
the parts that genuinely remain unexecuted — and is accurate either way.
Every checklist item in the base-profile section (:76-165) is marked pass, fail, or
explicitly not-attempted-with-a-reason. No item is left silently unaddressed.
The issuer-key-pinning experience is written down: how the key was obtained, and whether
the absence of x5c / issuer-metadata resolver backends was an inconvenience or a
blocker. If a blocker, a follow-up issue exists.
Nothing in this issue authorises weakening a refusal to make a wallet work. If a real wallet
fails a security check, the finding is recorded and triaged; the check is not relaxed to
produce a green run.
References
docs/wallet-interop-manual-validation.md — the procedure this issue executes and extends
docs/oidf-op-certification-runbook.md — the precedent for a certification-grade runbook
apps/auth-server/src/testing/mock-wallet.ts, apps/auth-server/src/testing/e2e-harness.ts, apps/auth-server/src/app/wallet-federation.integration.test.ts — what exists today
Summary
#240 shipped a documented procedure for validating QAuth's OID4VP verifier against a
real wallet —
docs/wallet-interop-manual-validation.md— and has never executed it. Thedocument says so itself in its second paragraph (
docs/wallet-interop-manual-validation.md:9-13)and its findings table is empty (
:40-42).Consequently every conformance claim QAuth can currently make about its wallet path is
self-consistent rather than verified: our verifier, talking to a mock we wrote, answering
requests the way we read the spec. This issue is the spike that replaces that with evidence
from a counterparty we did not author, and records the outcome — including a negative one —
in the document #240 already created.
This is a research/validation spike, not a feature. Nothing here changes shipped behaviour.
Defects the pass surfaces become their own issues; this one closes when the evidence exists.
What is true today (verified on
test/240-wallet-e2e, PR #375)apps/auth-server/src/testing/mock-wallet.ts, built fortest: [T4] E2E integration tests — reference/mock wallet (ADR-004) #240. It is deliberately constructed from
@qauth-labs/core-crypto+josewith no importof the code under test (
mock-wallet.ts:29-35), which is the strongest thing an in-repomock can do — and still not an independent implementation. Its own docblock says as much:
"Not a conformance target, and not a claim that any real wallet behaves this way"
(
mock-wallet.ts:49-53).app.inject()(
apps/auth-server/src/app/wallet-federation.integration.test.ts:96onward). No socket, noTLS, no reverse proxy, no real browser cookie enforcement is exercised anywhere.
mock-wallet.ts:141), so theverification side is not the gap — QAuth verifies the algorithm real wallets actually use.
createConfiguredIssuerKeyResolver(libs/fastify/plugins/federation/src/lib/wallet-credential-verification.ts:116-124)→
createStaticIssuerKeyResolver(libs/server/federation/src/oid4vp/issuer-key-resolution.ts:152).The profile table declares
issuerKeyResolution: ['x5c', 'issuer-metadata'](
libs/server/federation/src/profiles/verifier-profiles.ts:41) but neither backend exists.This is load-bearing for the exercise: see "The issuance problem" below.
haip-1.0still refuses to boot, and that refusal is tested(
apps/auth-server/src/app/wallet-federation-haip.integration.test.ts:51-78). So the pass thisissue asks for is a
oid4vp-1.0-basepass, as the document's own checklist already scopes it.Correction to fold into the document while you are there
docs/wallet-interop-manual-validation.md:31-34and its HAIP heading (:166) attribute theHAIP block to #298. That attribution is stale. #298 closed 2026-07-26 and its crypto
landed:
JwsAlgorithm = 'EdDSA' | 'RS256' | 'ES256'(
libs/core/crypto/src/lib/algorithms.ts:61), pluslibs/core/crypto/src/lib/jwe.ts,encryption-keys.tsandsigning.es256.test.ts.The live blocker is now provisioning and wiring, not the crypto library —
apps/auth-server/src/app/crypto-capabilities.ts:79hardcodesES256: falseand:95responseEncryption: false, and the comments there state exactly why: no env var, schema field,JWKS entry or code path exists to provision a P-256 signing key, and there is no
direct_post.jwtintake route to decrypt at. Both are attributed there to #233/#234.wallet-federation-haip.integration.test.ts:20-27carries the same stale reasoning.The conclusion is unchanged (
haip-1.0cannot boot, and must not), only the reason moved.Correcting it matters because someone reading the document today would wait for an issue that
already closed.
Tasks
Stage 0 — the cheapest independent counterparty first
deployed QAuth on
oid4vp-1.0-base. The suite acts as the wallet; it is free to self-hostfrom GitLab, and it is an implementation nobody on this project wrote. Configure QAuth's
OID4VP_WALLET_INVOCATION_ENDPOINTto the suite'sauthorization_endpointrather thanopenid4vp://.SKIPPED/WARNING, and file each hard failure asits own issue linked to epic epic: Wallet-agnostic VC federation via OID4VP 1.0 (ADR-004) #231.
Stage 1 — issuance (expect this to be the hard part)
issuance problem" — do not start Stage 2 before this is solved, because presentation
cannot be validated without a credential the wallet will actually hold.
OID4VP_ISSUER_JWKS, plus the issueridentifier in
OID4VP_TRUSTED_ISSUERS. Record how you obtained the key (x5cleaf?published JWKS? out of band?) and whether it rotates — that is a finding about QAuth's
missing resolver backends, not just setup noise.
Stage 2 — the run
JWT_ISSUERset to that exact origin —
response_uriis derived from it(
apps/auth-server/src/app/helpers/wallet-login-request.ts:140).docs/wallet-interop-manual-validation.md's base-profile checklist (:76-165)top to bottom against the real wallet, recording behaviour on passes as well as failures.
main/apps/auth-server/src/main.ts, not through the E2E harness — see"Do not test through the harness".
Stage 3 — record
:40-42) with wallet, build number, date, profileattempted and outcome. A "we tried X and it did not work" row is the deliverable too.
:31-34,:166) per above.OID4VP_RESPONSE_RATE_LIMITat itsproduction default can 429 a manual run, and the checklist's own rule that refusals are
indistinguishable on the wire (
:144-145) makes a 429 easy to misread as a securityrefusal. The E2E sidesteps this by raising every limit to
10000(
apps/auth-server/src/testing/e2e-harness.ts:138-140); a real run does not get that.self-certification issue (see "Relationship to certification").
Technical Details
Candidate counterparties
Named because a category ("an EUDI wallet") is not actionable. The verified/unverified split is
the point — do not let an unverified row become an assumption, which is the failure mode
#240 and ADR-004 both already recorded.
gitlab.com/openid/conformance-suite)authorization_endpoint, the test waits for your request)eu-digital-identity-walleton GitHub —eudi-app-android-wallet-ui,eudi-lib-android-wallet-core, and the iOS wallet kit)issuer.eudiw.dev, dev instancedev.issuer.eudiw.dev; sourceeudi-srv-web-issuing-eudiw-py,eudi-srv-pid-issuer)mso_mdoc+ SD-JWT VC, with a form-based test authentication modeplayground.animo.id) + Paradym Walletwaltid-identity)docs/adr/004-wallet-agnostic-federation.md:27,:420):19,:93)A wallet is a phone application.
localhostis not reachable from it — the document's"Before you start" (
:44-49) already covers the tunnel/deploy requirement and theJWT_ISSUERconstraint, and it is correct.
The issuance problem — call it out rather than glossing it
You cannot validate presentation without an issuer the wallet will accept. QAuth is a
Verifier and issues nothing; the document states this (
:51-54) but does not solve it, and itis the step most likely to consume the spike's budget.
Two constraints have to be satisfied simultaneously:
specific issuer, trust list or protocol draft. An arbitrary SD-JWT VC minted with
josewillnot necessarily be storable in an EUDI reference build.
OID4VP_ISSUER_JWKSmap (issuer-key-resolution.ts:152). Real ecosystem issuers convey keysvia
x5cand rotate them. There is nox5cbackend and noissuer-metadatabackend, despiteboth being declared in the profile (
verifier-profiles.ts:41). So the operator must extract aleaf public key by hand, and the pass breaks the moment the issuer rotates.
Three paths, in rising cost:
(1) cheaply; (2) still needs a manual key extraction.
the highest-value evidence; likely the most setup.
(2) is trivial; but you must then convince a real wallet to trust it, which reintroduces (1).
If constraint (2) turns out to be the wall rather than the annoyance, that is itself the
headline finding: it means an
x5cissuer-key resolution backend is a prerequisite for anyreal-ecosystem deployment, and it should be filed as a blocking issue against epic #231 rather
than worked around locally for the duration of the run.
What a pass settles that the mock cannot
The mock and the verifier share one author's reading of the spec. These are the claims that
reading cannot support, and that this exercise buys:
invocation URI,
dcql_query, andclient_metadata— or rejects it. The mock'sparseOid4vpRequest(mock-wallet.ts:313) was written against our emitter.vp_tokenshape we demand is the shape wallets emit. The suite already asserts weread only a DCQL-keyed object and refuse a Presentation Exchange envelope
(
wallet-federation.integration.test.ts:821-822). Whether a shipping wallet agrees iscurrently unknown, and the document flags exactly this as an interop finding rather than a
QAuth bug (
:107-110)._sd_alg,cnf, and Key Binding JWTaud/nonceconstruction against a credential we did not mint.client_metadataas something a human can act on.__Host-cookie enforcement byan actual browser, and a form-encoded
direct_postfrom an unauthenticated mobile networkpath.
app.inject()exercises none of it.ADR-009 §1 case the document calls a full authentication bypass if it succeeds (
:130-133).Do not test through the E2E harness
apps/auth-server/src/testing/e2e-harness.ts:184registers the app asfp(app), whereasproduction registers it plain (
apps/auth-server/src/main.ts:119). The harness's comment(
:179-183) is honest about it and argues the difference is encapsulation-only —db/dbPool/redisland on the root instance soresetE2eState(:88-91) can truncate the database theserver is actually using. That reasoning is sound for the assertions the suite makes.
It is still a real-assembly difference, and it is not the only one:
main.ts:86-116registers@fastify/swagger(always) before the app; the harness registersneither swagger nor swagger-ui.
main.ts:37-39setsrequestIdHeader,logControllerandgenReqId; the harness sets none.10000(:136-141).Inference, not proof: none of these is known to affect wallet-flow behaviour, and I did not
find a case where they do. But #370 is the standing precedent for exactly this class — a defect
invisible to lint, typecheck and every test because they all resolve through the workspace, and
only the packaged artifact boots differently. An interop run whose purpose is to test the real
assembly should test the real assembly: deploy the image, run
main.ts.Decision: counterparty ordering
Two defensible sequences, and the answer changes the budget by a large factor:
and needs no phone, no tunnel and no issuance — it drives our verifier directly. It would
surface protocol-level defects at a fraction of the cost, leaving the phone run to test only
what a synthetic wallet cannot (UI, transport, human consent).
shipping wallet interoperate with us"; but gated on the issuance problem above, which may not
resolve quickly.
Recommendation: (a), then (b). Sequencing them this way means a phone run starts from a
verifier already known to be protocol-clean, so any failure it finds is attributable to
transport, UI or ecosystem policy rather than to our request construction. It also means the
spike produces some independent evidence even if Stage 1 stalls. Owner decides at
implementation time.
Decision: where the evidence lives
docs/wallet-interop-manual-validation.mdis the obvious home and already has the table. But aconformance-suite run produces per-module results that look much more like
docs/oidf-op-certification-runbook.md— which is the existing precedent for "operator-facingrunbook with
file:lineanchors and[RE-CHECK AT RUN TIME]markers".Options: extend the existing manual-validation doc with a suite section, or add a sibling
docs/oid4vp-interop-runbook.mdmodelled on the OP runbook and cross-link. Recommendation:extend the existing document for the wallet findings table (it is already the named reporting
target,
:188-193) and add the suite runbook separately only if Stage 0 actually happens — donot create an empty second document.
Relationship to certification (#286)
Correction worth stating explicitly: #286 is CLOSED, sits on the T3 milestone, and is
about OP certification (Basic OP + Config OP). It deliberately scoped OID4VP/OID4VCI/HAIP
self-certification out, noting that programme went live 2026-02-26.
So this issue does not feed #286. What it does is produce the prerequisite for a separate,
currently unowned track: there is no issue for OpenID4VP 1.0 self-certification. #286 and
docs/oidf-op-certification-runbook.mdare the precedent — that runbook is what acertification-grade artifact looks like in this repo, and Stage 0 of this issue is the same
motion (deploy publicly, drive the OIDF suite, export results) against a different test plan.
Stage 3 asks for an explicit decision on whether to open that issue, rather than opening it
speculatively.
Acceptance Criteria
docs/wallet-interop-manual-validation.md's findings table has at least one row with anamed counterparty, its build/version, a date, the profile attempted, and an outcome. A
documented failure satisfies this; an empty table does not.
:9-13) is either removed or narrowed tothe parts that genuinely remain unexecuted — and is accurate either way.
:31-34,:166) is corrected to name the real blocker(
crypto-capabilities.ts:79,:95, and feat: [T4] OID4VP 1.0 authorization request generation + direct_post response intake (ADR-004) #233/feat: [T4] OID4VP Verifiable Presentation validation — SD-JWT VC (ADR-004) #234).:76-165) is marked pass, fail, orexplicitly not-attempted-with-a-reason. No item is left silently unaddressed.
build number — the document's own reporting rule (
:188-193).the absence of
x5c/issuer-metadataresolver backends was an inconvenience or ablocker. If a blocker, a follow-up issue exists.
reasons — mirroring how chore: pursue OpenID Foundation OP conformance certification #286 recorded its own defer decision.
main.tsboot (image ornx serve), not theE2E harness. Stated in the record.
Additional Notes
oid4vp-1.0-baseonly. Ahaip-1.0pass stays blocked — see the correction above —and remains covered by the document's second checklist (
:166-186) plus feat: [T4] Interoperable Key Attestations validation — HAIP §9.2 (ADR-004) #308.fails a security check, the finding is recorded and triaged; the check is not relaxed to
produce a green run.
References
docs/wallet-interop-manual-validation.md— the procedure this issue executes and extendsdocs/oidf-op-certification-runbook.md— the precedent for a certification-grade runbookapps/auth-server/src/testing/mock-wallet.ts,apps/auth-server/src/testing/e2e-harness.ts,apps/auth-server/src/app/wallet-federation.integration.test.ts— what exists todayattestations), feat: [T4] Crypto prerequisites for HAIP — ES256 + JWE (ECDH-ES/A*GCM) #298 (closed — ES256 + JWE), chore: pursue OpenID Foundation OP conformance certification #286 (closed — OP certification, OID4VP explicitly
out of scope), fix(deps): auth-server image crashes at boot — @qauth-labs/core-crypto imported by production source but declared in devDependencies #370 (precedent: a defect only a real boot surfaces)
docs/eudi-regulatory-drift-log.md