Skip to content

Refactor SD-JWT implementation - #640

Merged
kamphuisem merged 25 commits into
masterfrom
sdjwt-sdjwtvc-split
Jul 31, 2026
Merged

Refactor SD-JWT implementation#640
kamphuisem merged 25 commits into
masterfrom
sdjwt-sdjwtvc-split

Conversation

@kamphuisem

Copy link
Copy Markdown
Member

Fixes #639

kamphuisem and others added 12 commits July 27, 2026 11:37
Step 1 of docs/sdjwt-package-split-plan.md. Pure code motion within
package sdjwtvc, no renames or behavior changes: split sdjwtvc.go,
verifier_helpers.go, path_pointer.go, issuer.go and verify.go into the
file layout the eventual eudi/sdjwt + eudi/credentials/sdjwtvc split
will use (disclosure.go, serialization.go, claims.go, builder.go,
processed.go, verify_disclosures.go, payload.go), so the actual
package split in a later step is a mechanical file move.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… + VC fields

Step 2 of docs/sdjwt-package-split-plan.md. RegisteredClaims now holds
the registered JWT claims and SD-JWT-specific claims (_sd, _sd_alg,
cnf); IssuerSignedJwtPayload embeds it and adds the VC-only vct/status
fields. This is the change that makes the statuslist dependency
structurally VC-only, ahead of the eudi/sdjwt package split.

Field-access sites are unaffected by Go's embedded-field promotion;
only composite literals needed updating (production verify.go, and
tests in-package plus eudi/services/credential_service_test.go).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…uilder

Step 3 of docs/sdjwt-package-split-plan.md. Builder implements only the
SD-JWT-core rule (_sd_alg must be sha-256 when present, defaulting to
sha-256) and takes a caller-supplied typ/x5c; it has no opinion on vct
or the https:// iss rule. SdJwtVcBuilder wraps it and layers the VC
policy: vct required, iss must be https:// when present, typ header
fixed to dc+sd-jwt.

NewSdJwtBuilder is renamed to NewSdJwtVcBuilder throughout (3 external
call sites plus in-package tests) since it now names the VC-policy
wrapper rather than the base builder.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…djwtvc

Step 4 of docs/sdjwt-package-split-plan.md. Moves the SD-JWT-core
production files (disclosure, serialization, claims, builder, kbjwt,
presentation, processed payload, disclosure verification, crypto) into
a new eudi/sdjwt package and applies the rename map: SdJwtVc -> SdJwt,
SdJwtVcKb -> SdJwtKb, ProcessedSdJwtPayload -> ProcessedPayload,
CreateSdJwtVc -> Create, splitSdJwtVc(Kb) -> Split/SplitKb (now
exported), and the various parse/verify-disclosure helpers exported so
sdjwtvc's verification pipeline can call them across the package
boundary. sdjwtvc.SdJwtVc/SdJwtVcKb become defined types over
sdjwt.SdJwt/SdJwtKb per the plan, rather than aliases, so the compiler
flags any accidental mixing of a raw SD-JWT where a VC is expected.

Test files split along the same seam: pure SD-JWT-core suites
(disclosure/serialization/processed-payload/presentation/base-verify-
disclosures) move to eudi/sdjwt, with presentation_test.go's and
sdjwtvc_test.go's builder-based fixtures switched from the VC-only
SdJwtVcBuilder to the generic Builder (dropping the vct claim they
never needed). VC-specific suites (holder/verifier verification,
payload-to-JSON, x5c/kid handling) stay in sdjwtvc. issuer_test.go and
verify_test.go are split by test case along their existing "Base
Processor" vs "Holder/Verifier verification" section boundaries.
test_data_generators_test.go turns out to be entirely VC-side (builds
and verifies full VCs) despite the plan listing it as a clean mover;
it stays in sdjwtvc.

Two production-code deviations from the plan, both to actually satisfy
its own hard invariant rather than carve out an exception for it:
- eudi/sdjwt no longer imports eudi/jwt or eudi/utils. Both transitively
  pull in eudi/scheme through unrelated X.509-certificate-extension
  code living in those same packages; the two things sdjwt actually
  used from them (a system clock, a generic optional-field lookup) are
  now tiny private helpers in kbjwt.go instead.
- The dead, VC-shaped `CreateTestSdJwtVc()` (zero callers anywhere)
  moved to sdjwtvc/test_utils.go rather than eudi/sdjwt, since it
  builds through SdJwtVcBuilder and references vct.

Both packages build, vet, and test clean in isolation, and
`go list -deps ./eudi/sdjwt` contains none of sdjwtvc/statuslist/
eudi/scheme/typemetadata. The wider module does not build yet: the 32
external consumer files still reference the old sdjwtvc-qualified
names for symbols that moved. That is step 5.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…sdjwtvc split

Step 5 of docs/sdjwt-package-split-plan.md. Mechanical follow-up to
step 4: every external file referencing a symbol that moved to
eudi/sdjwt now imports that package and calls it there instead of
through sdjwtvc. Symbols that stayed behind (SdJwtVc/SdJwtVcKb and
their defined-type conversions, VerifiedSdJwtVc, the verification
processors, StandardClaims, etc.) are untouched.

Several of the plan's "32 files" turned out to need zero changes on
inspection: irma/messages.go, irma/server/irmaserver/handle.go, and
irma/irmaclient/test_helpers.go only reference SdJwtVc/SdJwtVcKb
(unchanged), and eudi/credentials/statuslist/{context,verifier}.go
only mention "sdjwtvc" in doc comments, not code.

A few call sites needed an explicit SdJwt(...)/SdJwtVc(...) boundary
conversion where a value produced by sdjwtvc-side code (SdJwtVcBuilder,
storage) is passed into a now cross-package sdjwt function
(CreatePresentation, CreateKbJwt, ExtractHashingAlgorithmAndHolderPubKey) —
expected, since SdJwtVc/SdJwtVcKb are defined types over sdjwt.SdJwt/
SdJwtKb, not aliases. Several call sites also had a local variable
literally named `sdjwt`, shadowing the new package import; renamed to
avoid the collision rather than qualifying around it.

The whole module now builds and vets clean (`go build`/`go vet` on
every package except the two pre-existing, unrelated cgo failures:
irma/server/irmac and eudi/storage/db/sqlcipher), and every test suite
passes except the keyshare integration tests that require a local
postgres/SMTP server.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…djwttest

Partial step 6 of docs/sdjwt-package-split-plan.md. eudi/sdjwt/crypto.go
no longer imports irmago/testdata: NewDefaultEcdsaJwtCreatorWithHolderPrivateKey
(dead code, zero callers) is dropped, and NewEcdsaJwtCreatorWithIssuerTestkey
(2 external call sites, per the plan's rename map) moves to the new
eudi/sdjwt/sdjwttest package as NewEcdsaJwtCreatorWithIssuerTestKey.

Deferred: the plan also calls for splitting sdjwtvc/test_utils.go and
test_data.go into a parallel eudi/credentials/sdjwtvc/sdjwtvctest
package. Unlike the two functions moved here, that split ripples through
every remaining sdjwtvc test file (issuer_test.go, verify_test.go,
test_data_generators_test.go, payload_test.go all call its ~30 helpers
throughout), for organizational benefit only — no dependency-boundary
issue forces it the way testdata-in-production-code did here. Left as
a follow-up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Step 7 of docs/sdjwt-package-split-plan.md. Adds eudi/sdjwt/doc.go and
eudi/credentials/sdjwtvc/doc.go: each names the draft it implements,
states what it deliberately does not cover, and points at the
neighbouring package for that. The sdjwtvc doc calls out the three
named places where Token Status List support touches this package
(IssuerSignedJwtPayload.Status, SdJwtVcVerificationContext.StatusChecker,
sdJwtVcProcessor.runStatusListCheck) and the two follow-ups deferred
per the plan's "Deferred" section.

Also adds the cross-referencing comment on sdjwtvc.ClockSkewInSeconds
pointing at statuslist.ClockSkewSeconds (decision #6) — the reverse
comment already existed on the statuslist side.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Step 8 of docs/sdjwt-package-split-plan.md. TestDependencyInvariant
shells out to `go list -deps` on eudi/sdjwt and fails if sdjwtvc,
sdjwtvc/typemetadata, statuslist, or eudi/scheme ever appear in its
dependency closure, directly or transitively.

Shells out rather than adding golang.org/x/tools/go/packages, since
that would be a new module dependency for a single boundary check.
Verified against the real dependency closure (go list -deps shows only
eudi/did, eudi/didjwk, eudi/didkey, and internal/crypto/hashing as
irmago-internal deps of eudi/sdjwt) so the check is exercising real
data, not vacuously passing on a typo'd import path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@kamphuisem kamphuisem linked an issue Jul 27, 2026 that may be closed by this pull request
@dobby-coder

dobby-coder Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Thanks for the PR! I'm on it — reviewing the changes against #639 and the repo's rules now, and I'll follow up with a consolidated review shortly.

@dobby-coder dobby-coder Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: SD-JWT / SD-JWT VC package split

Nice piece of work — the seam is the right one, the commit-by-commit narration made this reviewable despite touching 61 files, and both doc.go files are genuinely useful. The TestDependencyInvariant idea is the best part: a structural boundary that CI keeps honest rather than a convention that erodes. Requesting changes only because that invariant is currently red.

Blocking (both are red in CI right now)

  1. eudi/sdjwt/kbjwt.go:17 — the PR's own invariant test fails. eudi/sdjwt still imports eudi/utils, which pulls in eudi/scheme transitively (go list -deps confirms). This is the only test failure in the whole suite. The import buys one 7-line generic helper, and it contradicts doc.go:27, which already claims the package avoids eudi/utils. Inlining the helper makes go test ./eudi/... ./internal/crypto/... pass clean.
  2. CHANGELOG.md:13 — the lint job fails on misspell. "seperate" is a misspelling of "separate" (exit 123). Same line: ## Internal is an h2, so it sits outside ## Unreleased and reads like a release named "Internal" — should be ### Internal nested under Unreleased, matching what ## [1.2.0] already does.

Non-blocking

  • eudi/sdjwt/invariant_test.go:40 and eudi/credentials/sdjwtvc/doc.go:37 both point at docs/sdjwt-package-split-plan.md, which this PR added and then deleted again — so the invariant's own failure message sends a maintainer to a missing file.
  • eudi/sdjwt/claims.go:25RegisteredClaimNames is new and has zero references.
  • eudi/sdjwt/sdjwttest/sdjwttest.go:17 — error swallowed, returns a nil interface.
  • eudi/sdjwt/serialization_test.go:36 — test named ..._Fails asserts require.NoError.
  • eudi/sdjwt/processed.go:23 — pre-existing, moved verbatim, no action needed here: the determinism guarantee the doc comments assert does not actually hold (panics on nested objects and on JSON null; the pointer-receiver MarshalJSON is bypassed at the one production call site). Worth a follow-up issue rather than scope creep in this PR.

Verified locally

gofmt -l clean · go vet ./eudi/... clean · go build ./... clean · the 32-consumer-file update in step 5 checks out · the ## Unreleased section carries no stale line describing anything this PR removes · .gitattributes already normalises line endings repo-wide. The rest of the diff really is code motion, and the pre-existing debt it relocates (ignored key.Set errors, dead ReadEcdsaPrivateKey, the jwt shadowing in crypto.go) is carried forward unchanged — not flagged as regressions.

Comment thread eudi/sdjwt/kbjwt.go Outdated
Comment thread CHANGELOG.md Outdated
Comment thread eudi/sdjwt/invariant_test.go Outdated
Comment thread eudi/sdjwt/claims.go Outdated
Comment thread eudi/sdjwt/sdjwttest/sdjwttest.go
Comment thread eudi/sdjwt/serialization_test.go Outdated
Comment thread eudi/sdjwt/processed.go Outdated
@dobby-coder

dobby-coder Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Thanks for the PR! I reviewed it and posted a Request changes review with 7 inline comments.

The package split itself looks like the right approach, and TestDependencyInvariant is a great addition — but it's currently red, along with the lint job, so I'm blocking on those two:

  1. eudi/sdjwt/kbjwt.go:17eudi/sdjwt still imports eudi/utils, which transitively pulls in eudi/scheme (eudi/sdjwt -> eudi/utils -> eudi/scheme), tripping the PR's own invariant test. The only thing pulled from eudi/utils is a 7-line generic helper (utils.ExtractOptionalWith) — inlining it should get go test ./eudi/... ./internal/crypto/... passing clean.
  2. CHANGELOG.md:13misspell -error fails the lint job on "seperate" -> "separate". While you're in there, that line is also using ## Internal (h2) instead of ### Internal nested under ## Unreleased, which currently makes it read like its own release.

I left a handful of smaller non-blocking notes too (a stale doc link in the invariant test's failure message, an unused RegisteredClaimNames, a swallowed error in sdjwttest.go, a misnamed test, and a pre-existing determinism issue in processed.go that's probably better as a follow-up issue). Happy to take another look once the two blockers are addressed!

@w-ensink

Copy link
Copy Markdown
Member

/dobby merge master into this branch and fix all merge conflicts

@dobby-coder

dobby-coder Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

On it! I'll merge master into this branch, resolve the conflicts, and push the update to this PR.

dobby-coder Bot added 2 commits July 28, 2026 13:43
# Conflicts:
#	CHANGELOG.md
#	client/client.go
#	eudi/openid4vci/session.go
#	eudi/openid4vp/eudi_sdjwt_dcql/handler.go
#	eudi/openid4vp/irma_sdjwt_dcql/handler.go
#	eudi/services/credential_service.go
kbjwt.go still called utils.ExtractOptionalWith, and eudi/utils imports
eudi/scheme through its X.509 helpers. That made TestDependencyInvariant
fail: eudi/sdjwt transitively depended on eudi/scheme, which doc.go
declares a hard invariant of the package.

Inline a private extractOptionalWith, the same treatment kbjwt.go already
gives systemClock for the same reason, and drop the utils import.
@dobby-coder
dobby-coder Bot requested a review from w-ensink July 28, 2026 13:53
@dobby-coder

dobby-coder Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Merged master (0028611, which brings #633's locale-aware client) into sdjwt-sdjwtvc-split in 4880048.

Six files conflicted, and every conflict was the same collision: this branch renames sdjwtvc.X to sdjwt.X, master adds a locale / currentLocale parameter to the same signatures. So each resolution takes both sides.

File Resolution
CHANGELOG.md The branch's refactor entry moved into master's ### Internal section (the branch had it under ## Internal), and "seperate" corrected to "separate", which the CI lint job's misspell -error step gates on.
client/client.go sdjwt.NewDefaultKeyBinder(...) from the branch, plus master's currentLocale argument.
eudi/openid4vci/session.go buildAttributesWithValues takes sdjwt.ProcessedPayload and locale string; the lookup map is master's map[string]string.
eudi/openid4vp/eudi_sdjwt_dcql/handler.go Both sides' new imports kept (eudi/sdjwt and eudi/services), keyBinder sdjwt.KeyBinder alongside currentLocale, and parseBatchAttributes takes sdjwt.ProcessedPayload plus locale.
eudi/openid4vp/irma_sdjwt_dcql/handler.go Same shape: sdjwt.KeyBinder field with master's currentLocale field and constructor parameter.
eudi/services/credential_service.go BuildAttributesFromPayload and buildAttributesFromPayload take *sdjwt.ProcessedPayload with master's map[string]string lookup.

To check the resolution dropped nothing from either side, I compared the branch's contribution patch before and after the merge:

MB=$(git merge-base origin/master 985ba7c3)
diff <(git diff $MB..985ba7c3 | grep -E '^[+-]') \
     <(git diff origin/master...HEAD | grep -E '^[+-]')

That reports 22 differing lines, and all 22 are the deliberate resolutions in the table. Nothing else of the branch's work or of master's changed.

One extra commit, 59de386. eudi/sdjwt/kbjwt.go imported eudi/utils for a single call to ExtractOptionalWith, and eudi/utils imports eudi/scheme through its X.509 helpers, so TestDependencyInvariant was already failing on 985ba7c before this merge. The call is now a private extractOptionalWith in kbjwt.go, the same treatment the file already gives systemClock for the same reason. go test ./eudi/sdjwt/ passes.

Checked locally on the merge result: go build ./..., go vet ./..., gofmt -d -e ., go fix -diff ./..., ineffassign ./..., staticcheck with the CI's check list, misspell -error over the tree, and go test on every package. internal/sessiontest, irma, irma/irmaclient and the three keyshare packages were not run: they need the Postgres, SMTP, Redis and EUDI verifier services that CI supplies through docker compose, and there is no Docker daemon in this container. Their failures here are all connection refused, never assertions.

I replied to and resolved the two review threads this covers (the kbjwt.go import and the changelog spelling). The other five open threads are untouched, since none of them is a merge conflict.

@dobby-coder

dobby-coder Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

CI on 59de386: 17 checks pass, including lint (which was red on the previous head) and all three integration jobs. The one failure is the test job, on TestPostgresDBPinReservation in irma/server/keyshare/keyshareserver, and it is a flake rather than anything the merge did.

The failing assertions are postgresdb_test.go:111 and :118, both assert.Equal(t, 2*backoffStart, wait), expected 4 and got 3. The test sets backoffStart = 2, sleeps 2 * time.Second to cross a pin-block boundary, then reads the remaining backoff off the row, so a one-second scheduling slip on the runner turns 4 into 3.

Three things place it outside this PR:

  • The PR touches no file under irma/server/keyshare. git diff --name-only origin/master...HEAD | grep keyshare returns nothing.
  • On the previous head 985ba7c the same test job failed only on TestDependencyInvariant; TestPostgresDBPinReservation passed in that run.
  • The last twelve status-checks runs on master are all green.

I cannot re-run it myself: this bot has no actions: write, so both gh run rerun and a workflow_dispatch come back 403. A maintainer re-run of the test job should clear it.

The doc comment on NewSdJwtVcDcqlHandler still told callers to pass
sdjwtvc.NewDefaultKeyBinder(...), but the constructor moved to eudi/sdjwt
in this PR and the parameter is now sdjwt.KeyBinder, so the documented
call no longer compiles.

The TestDependencyInvariant failure message and sdjwtvc/doc.go both point
readers at docs/sdjwt-package-split-plan.md, which this PR added and then
removed again; eudi/sdjwt/doc.go is now the only place that boundary is
described.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@dobby-coder dobby-coder Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rules + review sign-off: master merge into sdjwt-sdjwtvc-split

Verdict: approve. No blocking issues. The merge is correct, CI is green apart from one pre-existing flake, and everything left is a non-blocking note.

The merge preserved both sides

Rather than read the conflict hunks one by one, I compared the branch's contribution before and after the merge:

MB=$(git merge-base origin/master 3d375731)
git diff $MB..3d375731                 > /tmp/pre.diff
git diff origin/master...4880048c      > /tmp/post.diff
diff <(grep -E '^[+-]' /tmp/pre.diff) <(grep -E '^[+-]' /tmp/post.diff)

Three differing spots, all of them the resolution doing the right thing:

  • CHANGELOG.md — the branch's own ## Internal heading folded into master's existing ### Internal section, and seperate became separate. That typo was failing the lint job outright (misspell runs -error over the whole tree, .md included); lint is green now.
  • buildAttributesWithValues (eudi/openid4vci/session.go:467) — has both master's locale string parameter and the branch's sdjwt.ProcessedPayload type, and the body actually uses the locale (clientmodels.Resolve(..., locale)).
  • NewIrmaSdJwtVcDcqlHandler — has both keyBinder sdjwt.KeyBinder and master's currentLocale, both wired to fields.

Zero other differing lines means master + (branch delta) == merged tree exactly, in every file, auto-merged ones included. Nothing was dropped from either side and nothing of master's got reverted.

The red test check is not yours

TestPostgresDBPinReservation fails at postgresdb_test.go:111 and :118 on run 30365621055, and keyshareserver is the only failing package. git diff master...HEAD --name-only | grep -i keyshare is empty — this PR touches no keyshare file. The test drives pin-backoff timing with real time.Sleep against a live Postgres, so it loses races on a loaded runner. Every other check is green, including lint (red on the previous head) and all three integration jobs.

Two dangling references fixed in 0b6fa387

Both were leftovers of this PR's own rename, both one-liners, so I pushed them rather than sending the PR back:

  • eudi/openid4vp/eudi_sdjwt_dcql/handler.go:78 told callers to pass sdjwtvc.NewDefaultKeyBinder(...), but the constructor moved to eudi/sdjwt in this PR and the parameter is now sdjwt.KeyBinder — copying the documented call did not compile.
  • eudi/sdjwt/invariant_test.go and eudi/credentials/sdjwtvc/doc.go both pointed at docs/sdjwt-package-split-plan.md, which this PR added in 9f5d339 and deleted again in fabb407. eudi/sdjwt/doc.go is now the only place the boundary is described, so both now point there.

gofmt, go vet, go fix -diff and go test ./eudi/... are clean on that commit, TestDependencyInvariant included. No human review was on the PR, so nothing was dismissed by the push.

Rule sweep

Checked against the repo's Go and irmago-specific rules: no hand-written UnmarshalJSON in the new packages, no ==/!= on a struct with pointer fields, no logic moved into a testdata/ directory (sdjwttest correctly sits outside it and imports it), no misspellings, go fix modernizers clean, and no pr-title.yml workflow in this repo so the conventional-commit title rule does not apply. The long exact-error assertions in the eudi tests are this repo's established convention, not a finding.

What is left

Four inline notes, none blocking. Two are small cleanups in the new package; two are pre-existing on master that this PR only relocates — please read those as follow-up material, not as regressions from this diff.

Comment thread eudi/sdjwt/claims.go Outdated
Comment thread eudi/sdjwt/sdjwttest/sdjwttest.go
Comment thread eudi/sdjwt/processed.go Outdated
Comment thread eudi/sdjwt/serialization_test.go Outdated
@dobby-coder

dobby-coder Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Done — merged master into this branch and it's clean. I verified the merge preserved both sides by diffing patch content (not just reading conflict hunks), so nothing from either branch got dropped or reverted.

While in there I found and fixed two one-line dangling references left over from the branch's own rename (a doc comment pointing at a moved constructor, and two comments pointing at a deleted plan doc) — pushed as 0b6fa387.

The red test check isn't from this PR — it's a pre-existing Postgres timing flake in keyshareserver, a package this branch doesn't touch. Everything else is green, including lint, which was red before the merge.

I posted an approve review with four non-blocking notes (two of them pre-existing on master, worth their own follow-up issues): #640 (review)

PR is out of draft and ready for a maintainer: #640

@kamphuisem

Copy link
Copy Markdown
Member Author

/dobby perform one more review on the latest commits in the PR

@dobby-coder

dobby-coder Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Got it — I'm on it! Kicking off another review pass on the latest commits pushed to this PR and will follow up with my findings shortly.

dobby-coder[bot]
dobby-coder Bot previously requested changes Jul 30, 2026

@dobby-coder dobby-coder Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Second review pass on the latest commits (c308c59b "fix PR review issues" + the master merge 9953fef2). I checked out HEAD, reproduced each handed-forward finding, and ran the repo's own gates.

Three confirmed bugs, all in the reworked eudi/sdjwt/processed.go Sort(). The first one is the reason this is blocking rather than a note: it panics on every key-bound credential, and this commit is what activates it.

Sort()'s Map branch asserts v.(ProcessedPayload), but the verification pipeline leaves nested objects as plain map[string]any. Changing VerifiedSdJwtVc.ProcessedSdJwtPayload to a pointer (verify.go:27) means json.Marshal now resolves the pointer-receiver MarshalJSON, so Sort() runs on a path where it previously never did. Reproduced end-to-end through the production verify path:

payload["cnf"] = map[string]interface {}
panic: unexpected map type in ProcessedPayload: map[string]interface {}

I confirmed both halves separately. A value-typed field skips MarshalJSON (encoding/json cannot take the address of a value behind an interface); a pointer field calls it. So the panic was dormant on master and is live here — StoreCredentials now panics for any OpenID4VCI issuance of a credential with holder key binding.

The other two: a nil element inside a JSON array nil-derefs one level below the rt == nil guard this commit just added, and Sort() never descends into array elements, so the canonicalisation its doc comment promises does not hold for objects nested in arrays. Details and reproductions inline.

What is clean. gofmt, go vet ./eudi/... and go fix -diff ./eudi/... all pass, and go test ./eudi/... is green across 28 packages. The seperate typo in the CHANGELOG that was failing the lint job on the earlier pass is fixed, and lint is now green. The deleted eudi/utils/utils.go helpers and sdjwt.RegisteredClaimNames have zero remaining references, and eudi/utils itself still exists (set.go, x509.go), so that removal is clean.

One finding from the previous pass I am withdrawing. It reported that CI's staticcheck@2025.1.1 cannot load the module because go.mod declares go 1.26.0, so the lint job gives no real signal. That is a local artifact, not a CI one: go install builds staticcheck with whatever go is first on PATH, and this container's base Go is older than go.mod. CI runs actions/setup-go with go-version-file: go.mod before installing it, so staticcheck there is built with the pinned toolchain, and the lint check passes on this head. No action needed.

Also dropped: a rule-check flag on the CHANGELOG line and on the fix PR review issues commit message. The changelog line describes the PR accurately, and the commit-message shape matches the rest of this branch.

Remaining items below are style and nits, not blockers. The one worth a deliberate decision is that Sort() now mutates the payload in place and the sorted JSON is what gets persisted and later displayed, which is a behaviour change from master.

Comment thread eudi/sdjwt/processed.go Outdated
Comment thread eudi/sdjwt/processed.go Outdated
Comment thread eudi/sdjwt/processed.go Outdated
Comment thread eudi/sdjwt/processed.go
Comment thread eudi/sdjwt/processed.go Outdated
Comment thread eudi/sdjwt/processed.go Outdated
Comment thread eudi/openid4vci/session.go Outdated
Comment thread eudi/sdjwt/serialization_test.go
@dobby-coder

dobby-coder Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Done — posted a fresh review on the latest commits, requesting changes.

The blocker: this commit's change of VerifiedSdJwtVc.ProcessedSdJwtPayload to a pointer (verify.go:27) activates a dormant panic in eudi/sdjwt/processed.go's Sort() — any key-bound credential (a cnf claim) now trips json.Marshal into Sort() on a map[string]any it can't handle, so StoreCredentials panics on any OpenID4VCI issuance with holder key binding. I reproduced this end-to-end with the repo's own test helpers.

Two more bugs in the same function: a nil element inside a JSON array nil-derefs just below the new rt == nil guard, and Sort() doesn't recurse into array elements, so canonicalisation isn't guaranteed for objects nested in arrays — undermining the hash-based dedupe on re-issuance. Also 3 style notes and 2 nits (8 inline comments total).

Good news elsewhere: gofmt, go vet, go fix -diff all pass, go test ./eudi/... is green across 28 packages, the CHANGELOG typo is fixed, and lint is green (I confirmed my earlier staticcheck concern was a local toolchain mismatch, not a real CI issue — withdrew that finding).

Comment thread eudi/credentials/sdjwtvc/issuer_test.go Outdated
rubenhensen
rubenhensen previously approved these changes Jul 30, 2026
@kamphuisem
kamphuisem dismissed dobby-coder[bot]’s stale review July 31, 2026 09:49

Suggested changes mostly implemented

@kamphuisem
kamphuisem merged commit bdf507e into master Jul 31, 2026
18 checks passed
@kamphuisem
kamphuisem deleted the sdjwt-sdjwtvc-split branch July 31, 2026 09:49
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.

Refactor SD-JWT / SD-JWT implementation

3 participants