Delegated Access β Verifiably Build Plan
Branch: feat/delegated-access (off feat/inji-authcode-dynamic-issuance). Gitignored working doc.
Source design: ADR: A Reusable Delegated-Access Adapter (2026-06-24, updated 2026-06-25 Β§0).
DPI constraint: reusability + minimalism β one DPG-agnostic evaluator; minimal new surface.
Verified against: code e619453 + live VPS endpoints, 2026-06-25 (inji-certify 0.14.0,
inji-verify 0.16.0, walt.id 0.18.2, eSignet 1.5.1, credebl/Credo-TS 0.5.x).
BUILD ORDER (user directive 2026-06-25): by data-model tier across DPGs β
(T1) JSON-LD VCDM 2.0 across all 3 DPGs β (T2) VCDM 1/1.1 where supported β (T3) SD-JWT/SD-JWT-VC where supported.
0. Executive summary β verified findings (the 5 that shape the build)
- JSON-LD (
ldp_vc) is the tier-1 lead (per directive) β with a verification-routing caveat.
ldp_vc issuance is fine; ldp_vc verification through MOSIP vc-verifier is broken on this
stack (URDNA2015 canonicalization, implicates credentialStatus + @vocab; docs/dpg-matrix.md:176-239).
β For T1 we (a) carry the capability via a defined @context term, not @vocab (avoids worsening
it), and (b) route JSON-LD verification through walt.id's verifier (W3C VCDM, after @context
alignment) and/or treat Inji-verify-of-ldp as a tracked upstream bug. The evaluator's
linkage/capability/status checks run on top of whatever signature verdict the host returns.
- Neither DPG enforces linkage. walt.id doesn't evaluate or inject
same_subject; Inji doesn't
either; credebl DCQL groups but doesn't evaluate onBehalfOf==subject. β the evaluator is the sole
linkage authority on every path. Drop "inject same_subject as defense-in-depth."
- Revocation is unchecked today.
CheckedRevocation:true is hardcoded; the cache verifies the list
JWT but never decodes the bit. D4's uniform check = ~30 new lines composing existing primitives, fail-closed.
- Trust is advisory-only.
IsTrusted labels but never blocks (I3 unmet) β the evaluator must enforce.
- In-app holder discards its key (
inji_holder.go:237) β Β§12 cnf key-binding (T3) needs an external
wallet (Credo) or the walt.id claim path; the in-app path is bearer.
Net: new code is small + concentrated β one DPG-agnostic internal/delegation evaluator, per-credential
VP normalization, a ~30-line status-bit check; everything else is reuse. The evaluator is built first
(format-agnostic), then issuance per data-model tier.
1. ADR claim-verification matrix
Verdicts: β
CONFIRMED Β· β PARTIAL Β· β REFUTED Β· ? UNVERIFIABLE-IN-CODE. Evidence = code (path:line,
under verifiably-go/) and/or live endpoint.
Trust-triangle invariants
| ADR |
Claim |
Verdict |
Evidence |
| I1 |
Issuer signs; adapter never signs |
β
|
Inji uses Certify key-manager ref CERTIFY_VC_SIGN_ED25519 (injicertify/db.go:152-157, storage/pg/subjects.go:154-155); claim returns Certify bytes verbatim inji_holder.go:254-297. Live: did:web:certify-nginx serves Ed25519 assertionMethod. walt.id signs (waltid/issuer.go:666-727). |
| I3 |
Verifier anchors trust in issuer |
β advisory |
trust/registry.go:29 IsTrusted via verifier.go:540-557 attachTrustStatus (sites verifier.go:444,:532,public_verify.go:360) β only labels, never blocks; StatusListPolicy (registry.go:60-63) unread. β enforce. |
| I4 |
No forge-enabling secret in adapter |
β
|
Only key is the ES256 trust/receipt key (trust/jwt.go:53; live JWKS ES256). Never signs credentials. |
Verification asymmetry (Β§7c) & OID4VP reality
| ADR |
Claim |
Verdict |
Evidence |
| Β§7c |
walt.id evaluates same_subject natively |
β |
0 occurrences; PD = fields+limit_disclosure only (waltid/verifier.go:206-270); VP policies hardcoded ["signature","presentation-definition"] (:390-392). |
| Β§7c |
Inji checks signatures, not PE constraints |
β
|
injiverify re-checks fields because Inji doesn't (INJIVER-1131): injiverify/adapter.go:179-184,:412-425; dpg-matrix.md:264-270. |
| Β§7b |
host-verified VP retrievable |
β |
adapters receive the full VP but collapse it: VerificationResult single-valued (backend/adapter.go:377-434); walt.id vcList[0] only (verifier.go:730-735); credebl flattens (:439-451); injiverify decodes nothing (:185-194). |
| Β§9 |
DCQL is the linkage path (credebl) |
β |
DCQL slice can carry the pair (credebl/verifier.go:30-44) but no credential_sets and no cross-claim eval. |
| β |
request a credential PAIR today |
β |
single descriptor, but wire fields already arrays β append one element (injiverify adapter.go:373-386, waltid verifier.go:170-171, credebl verifier.go:113-120). |
Status / revocation (D4)
| ADR |
Claim |
Verdict |
Evidence |
| D4 |
Verifiably hosts Bitstring + Token lists, allocates |
β
|
handlers/status_list.go:61,98; allocate issued.go:40βstatuslist/store.go:140. Live: routes wired (404 unknown id). |
| D4 |
status entry embedded in every credential |
β split |
walt.id embeds (Verifiably-hosted) waltid/issuer.go:1026-1034/:927-934; inji-ldp β Certify embeds (its own list, Verifiably proxies kid inji_proxy.go:123); inji-sd-jwt β none. |
| D4 |
uniform status check |
β today |
CheckedRevocation:true hardcoded (waltid/verifier.go:378); public path policies ["signature","expired","not-before"] (public_verify.go:316); statuslistcache.Fetch no bit decode (fetcher.go:45). Primitives exist: bitstring.go:135 Get, token.go:39. |
Formats / profile feasibility (Β§6/Β§12) / DID
| ADR |
Claim |
Verdict |
Evidence |
| Β§6 |
nested credentialSubject.onBehalfOf.id + termsOfUse (ldp) |
β as-built |
templates flat (injicertify/db.go:243-316); values string-only (claims->>, SubjectData map[string]string). T1 makes this in-scope work (template nesting + termsOfUse emission). |
| Β§12 |
bearer SD-JWT + cnf key-bound delegation |
β |
walt.id arbitrary claims + token status (issuer.go:918-936); cnf set by the claiming wallet (haip-conformance.md:66); inji in-app holder discards key (inji_holder.go:237). |
| Q6 |
stable non-pairwise anchor needed |
β
|
vc_subject PK = pairwise PSU-token base64url(SHA3-256(individualId+clientId)) (api_subjects.go:64-67); raw id only at signup (onboard.go:127,153-160). β explicit subjectRef claim. |
| Β§9 |
did:web issuers resolvable |
β
|
didresolver/web.go:45 (web only). Live: certify did.json resolves. Gap: walt.id issuer is did:jwk (waltid/issuer.go:52) β anchor trust on the DID string. |
| D6 |
consent/audit receipt |
β |
unsigned event log (verification/events.go:18); attach public_verify.go:391-397,verifier.go:463-469; ES256 signing available (trust/jwt.go:53). |
2. Per-DPG Γ per-tier issuance support (what "across all 3 DPGs" really means)
| Tier |
Format |
inji-certify |
walt.id |
credebl |
| T1 |
JSON-LD VCDM 2.0 (ldp_vc, Data Integrity) |
β
issue (verify via walt.id β ldp Inji-verify bug) |
β
issue+verify (W3C VCDM) |
β SD-JWT-only (issuer is dc+sd-jwt) β out of T1 |
| T2 |
VCDM 1/1.1 (jwt_vc_json, ldp_vc 1.1) |
β ldp_vc 1.1 |
β
jwt_vc_json (tested round-trip) |
β |
| T3 |
SD-JWT / SD-JWT VC (vc+sd-jwt,dc+sd-jwt) |
β
vc+sd-jwt |
β
vc+sd-jwt |
β
dc+sd-jwt (Β§12 cnf) |
β "across all 3 DPGs" is fully achievable only at T3. T1/T2 cover inji-certify + walt.id; credebl
joins at T3. The evaluator is identical across all tiers/DPGs (DPI reusability) β only issuance and
the host's signature verdict differ.
3. Open-question decisions (final)
Q1 policy-only crypto / sole linkage authority Β· Q2 termsOfUse(JSON-LD)/delegation(SD-JWT) via a
defined context term Β· Q3 single-hop, fail-closed on chains Β· Q4 internal receipt Β· Q5 reuse cache, ~60s,
fail-closed Β· Q6 explicit subjectRef (registry individualId), never the PSU-token.
4. Phases (reuse-first, minimal)
Phase 0 β Foundation (shared)
- Delegated-access JSON-LD
@context (delegated-access/v1) defining DelegationCapability,
onBehalfOf, delegate, allowedAction, validUntil, allowFurtherDelegation. Host via the existing
static FileServer (static/contexts/..., main.go:463) or a ~10-line application/ld+json handler
mirroring ServeJWKS (main.go:528). Needed now (JSON-LD is T1).
- Profile types in
internal/delegation (the Β§6 JSON-LD shape + the normalized internal shape).
Phase 1 β Presentation Adapter (DPG-agnostic evaluator) β FIRST
| Step |
Reuse (don't rebuild) |
Build (minimal) |
Files |
| Per-credential normalization |
parseSDJWTVC/parseVPJWT (waltid/verifier.go:658,719), credebl extractClaimsFromCompactSdJwt (:312); a JSON-LD VC parse for ldp |
emit []NormalizedCredential (stop collapsing) |
waltid/verifier.go:612, credebl/verifier.go:270, injiverify/adapter.go:185 |
| Surface to handlers |
keep single-valued fields (back-compat) |
additive Credentials []NormalizedCredential + HolderBinding |
backend/adapter.go:~432 (+ backend/normalized.go) |
| Evaluator |
statuslistcache.Fetch + statuslist decoders + trust.IsTrusted |
internal/delegation.Evaluate (linkage+invocation+caveats+status, fail-closed; enforce trust) |
internal/delegation/ (new) |
| Wire-in (uniform) |
clone attachTrustStatus (verifier.go:540); INJIVER-1131 precedent (injiverify:179) |
attachDelegationVerdict at 3 seam sites (downgrade-only) |
verifier.go:444,532, public_verify.go:362 |
| Request the pair |
wire fields already arrays |
append 2nd descriptor/credential |
the 3 PD/DCQL builders |
- Testable now with hand-crafted VCs (unit tests) before any issuance exists.
Phase 2 β T1 issuance: JSON-LD VCDM 2.0 (inji-certify + walt.id)
- walt.id (lead β issue+verify in one stack): issue a VCDM2
ldp_vc with nested credentialSubject.onBehalfOf,
termsOfUse[DelegationCapability], bitstring credentialStatus. Reuse waltid/issuer.go VCDM path
(:1026-1034 already injects credentialStatus); the credentialData is JSON we control. Verify via walt.id.
- inji-certify: teach
buildVCTemplate (injicertify/db.go:257-312) to emit nested onBehalfOf +
termsOfUse (the deferred template work, now T1) + extend FieldSpec with nesting; provision subjectRef
at signup. Verify via walt.id verifier (Inji-verify-of-ldp is the known bug). Status: Certify-hosted.
- credebl: not applicable at T1.
Phase 3 β T2 issuance: VCDM 1/1.1
- walt.id
jwt_vc_json (tested round-trip); inji-certify ldp_vc 1.1. Same nested shape, issuanceDate/expirationDate.
Phase 4 β T3 issuance: SD-JWT / SD-JWT VC (all 3 DPGs)
- Β§12 model: bearer identity +
cnf key-bound delegation (delegation JOSE claim, Token Status List).
walt.id + credebl + inji vc+sd-jwt; cnf via external wallet (Credo). credebl joins here (DCQL).
Phase 5 β Cross-cutting (applied per tier)
- Revocation/transition: reuse
RevokeIssuedCredential (issued.go:225, format-agnostic) + REST (main.go:614).
- Receipt/audit: extend
verification/events.go:18 (internal-only).
5. Recommended starting slice
- Phase 0 (@context + types) β Phase 1 (evaluator + normalization + status-bit check + enforce trust),
unit-tested with hand-crafted VCDM2 VCs.
- Phase 2 walt.id first (issue+verify VCDM2 delegation in one stack β fastest full
issueβverifyβrevoke loop), then Phase 2 inji-certify (template nesting + walt.id-verify routing).
- Then T2, then T3 (credebl joins at T3).
6. DPI scorecard
- Reusability: one evaluator for all 3 DPGs Γ all tiers via a normalized VP; reuses existing parsers,
status-list cache, trust registry, revoke UI/REST, event log, ES256 signing.
- Minimalism: new =
internal/delegation (types + Evaluate), per-credential normalization (~3 adapter
edits), ~30-line status-bit check, additive VerificationResult fields, handler hook, @context file.
The only larger item is T1 buildVCTemplate nesting (deferred otherwise; required because JSON-LD is T1).