Skip to content

fix(verifier): accept padded base64 vp_token and stop panicking on malformed VPs - #114

Open
vramperez wants to merge 5 commits into
mainfrom
fix/vp-token-base64-padding
Open

fix(verifier): accept padded base64 vp_token and stop panicking on malformed VPs#114
vramperez wants to merge 5 commits into
mainfrom
fix/vp-token-base64-padding

Conversation

@vramperez

Copy link
Copy Markdown
Collaborator

decodeVpString only decoded raw(unpadded) base64url, so a client emitting padded base64 - java's Base64.getUrlEncoder() or python's b64encode() - made the decode fail and the raw, still-encoded string was passed on. That string matches no DCQL shape and reaches ParseWithSdJwt, where strings.Split(token, ".")[1] panics on a string without a '.', turning a malformed request into an empty HTTP 500 instead of a 400.

Accept all four base64 variants in decodeVpString. Tokens that are not base64 at all(compact JWTs, SD-JWTs, plain json dcql responses) are unaffected, since '.', '~', '{', '"' and ':' are part of no base64 alphabet.

ParseWithSdJwt now reuses the existing extractJWTPayload helper, which checks the segment count and no longer discards the decode error. Its three remaining unchecked type assertions - the holder, the verifiableCredential array and its entries - are guarded as well, since they panic on the same unauthenticated endpoint.

…lformed VPs

decodeVpString only decoded raw(unpadded) base64url, so a client emitting
padded base64 - java's Base64.getUrlEncoder() or python's b64encode() - made
the decode fail and the raw, still-encoded string was passed on. That string
matches no DCQL shape and reaches ParseWithSdJwt, where strings.Split(token,
".")[1] panics on a string without a '.', turning a malformed request into an
empty HTTP 500 instead of a 400.

Accept all four base64 variants in decodeVpString. Tokens that are not base64
at all(compact JWTs, SD-JWTs, plain json dcql responses) are unaffected, since
'.', '~', '{', '"' and ':' are part of no base64 alphabet.

ParseWithSdJwt now reuses the existing extractJWTPayload helper, which checks
the segment count and no longer discards the decode error. Its three remaining
unchecked type assertions - the holder, the verifiableCredential array and its
entries - are guarded as well, since they panic on the same unauthenticated
endpoint.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vramperez
vramperez requested review from Mortega5 and wistefan August 17, 2026 11:35
@vramperez vramperez added the minor Should be applied for new functionality or bigger updates. label Aug 17, 2026

@Mortega5 Mortega5 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nice fix overall — the panic-to-400 conversion and the multi-variant base64 handling are well covered by tests.

One thing worth confirming before merge: holder becomes mandatory here — if the claim is missing or not a string, the whole presentation is rejected with ErrorPresentationNoHolder.

That's stricter than the rest of the codebase, where holder is treated as optional:

  • parseJSONLDPresentation just skips setting holder when it's absent, no error.
  • generateJWT in verifier.go:1351 explicitly handles an empty holder as valid: if holder != "" { jwtBuilder.Subject(holder) }.

Since the stated goal here was to stop the panic (turning an unchecked type assertion into a checked one), was making the field strictly required also intentional? If not, it might be worth mirroring the JSON-LD path and defaulting to an empty holder instead of rejecting the whole presentation — otherwise this could reject legitimate SD-JWT/DCQL presentations from wallets that don't send a holder claim (common, since holder binding for SD-JWT VC is normally done via the KB-JWT rather than this field).

holder, ok := vp[common.VPKeyHolder].(string)
if !ok {
logging.Log().Warn("VP does not contain a string holder")
return nil, ErrorPresentationNoHolder
}
presentation.Holder = holder

Comment thread verifier/presentation_parser.go Outdated
Comment on lines +552 to +557
holder, ok := vp[common.VPKeyHolder].(string)
if !ok {
logging.Log().Warn("VP does not contain a string holder")
return nil, ErrorPresentationNoHolder
}
presentation.Holder = holder

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This makes holder mandatory (missing/non-string → ErrorPresentationNoHolder, rejecting the whole presentation), which is stricter than the rest of the codebase:

  • parseJSONLDPresentation treats holder as optional and just skips setting it when absent.
  • generateJWT (verifier.go:1351) explicitly handles an empty holder as valid: if holder != "" { jwtBuilder.Subject(holder) }.

The stated goal here was to stop the panic (unchecked type assertion → checked one) — was requiring the field also intentional? If not, defaulting to an empty holder (like the JSON-LD path) instead of rejecting the presentation might be safer, since wallets that don't send holder for SD-JWT/DCQL presentations (holder binding is normally done via the KB-JWT) would otherwise be rejected.

holder, ok := vp[common.VPKeyHolder].(string)
if !ok {
logging.Log().Warn("VP does not contain a string holder")
return nil, ErrorPresentationNoHolder
}
presentation.Holder = holder

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fix in 1f1e2f0

Comment thread openapi/api_api.go Outdated
Comment thread openapi/api_api.go Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

minor Should be applied for new functionality or bigger updates.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants