Description:
In the current implementation of the Verifier API (POST /openid4vc/verify/{state}), a session remains active for its entire TTL (default 5 minutes) even after a Verifiable Presentation (VP) has been successfully verified.
Because the session is not locked, consumed, or invalidated upon the first successful verification, an attacker can capture the VP Token and resubmit it multiple times within the TTL window. Each submission yields a verificationResult = true and triggers the webhook (VerifierService.notifySubscribers).
This violates the fundamental cryptographic principle of a "nonce" (Number used ONCE) in OpenID4VP and RFC 6819 (OAuth 2.0 Threat Model - Mitigate Replay of Authorization Response), leaving downstream applications vulnerable to Replay Attacks if they do not implement their own distributed locking mechanisms.
Steps to Reproduce:
- Initialize a verification session via
POST /openid4vc/verify and retrieve the session_id and nonce.
- Submit a valid VP Token (containing the correct signature and nonce) to
POST /openid4vc/verify/{session_id}.
- Observe that the verification is successful and the webhook is triggered.
- Immediately submit the exact same VP Token again to the same endpoint within the 5-minute TTL.
- Observe that the API accepts the payload, returns a successful validation, and triggers the webhook again.
Expected Behavior:
According to OID4VP standards, once a VP Token is successfully verified against a specific session/nonce, that session MUST be immediately marked as consumed/resolved. Any subsequent attempts to submit to the same session should be rejected (e.g., HTTP 400 or 403) to prevent Replay Attacks.
Actual Behavior:
Based on the source code in OpenIDCredentialVerifier.kt (inside the verify function), the session state is updated via putSession but there is no mechanism to prevent re-verification if the verificationResult is already true.
// In OpenIDCredentialVerifier.kt
return session.copy(
tokenResponse = tokenResponse,
verificationResult = doVerify(...)
).also {
putSession(id = it.id, session = it, ttl = remainingTtl) // Session is updated, but not locked/consumed
}
Suggested Fix:
Implement a state lock or a status flag (e.g., PENDING, RESOLVED, FAILED). If doVerify() evaluates to true, the session status should transition to RESOLVED and the nonce should be considered burned. Any incoming request for a RESOLVED session should be immediately rejected.
Environment:
- Repository:
waltid-identity
- Branch:
main
- Module:
VerifierService, OpenIDCredentialVerifier
Description:
In the current implementation of the Verifier API (
POST /openid4vc/verify/{state}), a session remains active for its entire TTL (default 5 minutes) even after a Verifiable Presentation (VP) has been successfully verified.Because the session is not locked, consumed, or invalidated upon the first successful verification, an attacker can capture the VP Token and resubmit it multiple times within the TTL window. Each submission yields a
verificationResult = trueand triggers the webhook (VerifierService.notifySubscribers).This violates the fundamental cryptographic principle of a "nonce" (Number used ONCE) in OpenID4VP and RFC 6819 (OAuth 2.0 Threat Model - Mitigate Replay of Authorization Response), leaving downstream applications vulnerable to Replay Attacks if they do not implement their own distributed locking mechanisms.
Steps to Reproduce:
POST /openid4vc/verifyand retrieve thesession_idandnonce.POST /openid4vc/verify/{session_id}.Expected Behavior:
According to OID4VP standards, once a VP Token is successfully verified against a specific session/nonce, that session MUST be immediately marked as consumed/resolved. Any subsequent attempts to submit to the same session should be rejected (e.g., HTTP 400 or 403) to prevent Replay Attacks.
Actual Behavior:
Based on the source code in
OpenIDCredentialVerifier.kt(inside theverifyfunction), the session state is updated viaputSessionbut there is no mechanism to prevent re-verification if theverificationResultis alreadytrue.Suggested Fix:
Implement a state lock or a
statusflag (e.g.,PENDING,RESOLVED,FAILED). IfdoVerify()evaluates totrue, the session status should transition toRESOLVEDand thenonceshould be considered burned. Any incoming request for aRESOLVEDsession should be immediately rejected.Environment:
waltid-identitymainVerifierService,OpenIDCredentialVerifier