OpenID4VP
OpenID4VP copied to clipboard
Add a way for a wallet to actively mitigate relay without requiring the verifier to be compliant
Anti-phishing for lazy RPs
Problem
When considering the same-device flow steps outlined below, the following attack is possible:
- An attacker acquires a genuine OID4VP Authz Request from a legitimate RP.
- The OID4VP Authz Request becomes associated with the attacker's session with the RP, typically through Cookies.
- The attacker then sends the OID4VP Authz Request to the user via a link, such as through email, QR Code (although not applicable in the same-device flow), or text message/SMS.
- The user opens the link, triggering the user's native Wallet app.
- The Wallet successfully verifies the OID4VP Authz Request, which may involve validating the
client_idaccording to theclient_id_schemeAuthz Request parameter. - The user authenticates and provides consent to share their data with the legitimate RP.
- The Wallet generates the OID4VP Authz Response.
- The Wallet sends the OID4VP Authz Response to the Response URI of the RP.
- The Response URI returns a Redirect URI containing a
response_codeparameter tied to the session, usually viastate. - The Wallet redirects the user through the mobile browser, including the
response_codeparameter in the Redirect URI. - The RP receives the
response_codevia the frontend (if theresponse_codewas provided in the URL fragment) or backend (if theresponse_codeis part of the URL query string). - The RP checks if the
response_codeis valid (exists, not invalidated, etc.) without validating that theresponse_codebelongs to the session and returns the success page. - The attacker gains access to the success page (protected resource or the digital credential itself).
This attack can be mitigated if the RP implements response_code/session binding validation appropriately, requiring a diligent implementation.
It is worth noting that the wallet and the user are unable to detect whether the RP's implementation is diligent. This issue was highlighted by the ISO/IEC SC17/JTC1 WG10.
However, many RPs, while genuine, often opt for the least implementation effort without disrupting the flow (similar to nonce handling in existing RPs).
To safeguard users from lazy RPs, it would be beneficial to define a flow that does not necessitate the RP to be diligent and still allows the flow to proceed smoothly.
Proposed Solution
At IETF 118 a side conversation concluded that the only effective mitigation for this would involve encrypting the Authz Response and providing the last input of the key derivation function via the Redirect URI to the RP. In this manner, the JWE has some detached inputs for the key derivation function so to say.
The following proposed solution involves encrypting the Authz Response Object using JARM/ECDH-ES. In that case, the RP has to provide its public key via client_metadata or client_metadata_uri in the Authz Request. Alternatively, the RP is pre-registered. The process is outlined below:
- ... (see flow above)
- The Wallet selects a secure random value for the
apuvalue. - The Wallet generates the Authz Response Object, encoding it as a JARM/JWE using the RP's public key and a new
epkof the wallet. - The Wallet removes the
apuvalue from the JWE. Important input to the key derivation function are now detached from the JWE. This input is not known to the RP yet. - The Wallet sends the Authz Response Object to the Response URI.
- The Wallet receives the Redirect URI from the Response URI.
- The Wallet appends the
apuparameter to the Redirect URI as a new parameter. Note that the wallet does not have to intercept the actual redirect, as the Redirect URI is provided as a JSON payload in the HTTP response body of the Response URI request. - The Wallet parses the
redirect_uriparameter from the JSON response and redirects the user through the mobile browser to the Redirect URI with the additionalapuparameter. - The RP receives the Redirect URI, including the
apuparameter, looks up the Authz Response received earlier in the flow, and can now decrypt it since it received the producer info. - The RP returns the success page.
Alternatively, this approach works without requiring client_metadata/client_metadata_uri or jwks/jwks_uri, and it can be implemented with a symmetric key for direct encryption, simplifying the implementation process.
In the example above, the producer info (apu) is used by ECDH-ES as input to ConcatKDF which changes the derived symmetric key.
relates to #27
To prevent extremely lazy RPs from including the JWE in the path/query/fragment of the redirect_uri, we could add further requirements on the redirect_uri which the wallet must be able to check. The simplest although not best solution since it might not work in certain environments would be to say redirect_uri eq response_uri.
Update:
- The approach below is better since it does not limit the value of the
redirect_uri.
To prevent extremely lazy RPs from including the JWE in the path/query/fragment of the
redirect_uri, we could add further requirements on theredirect_uriwhich the wallet must be able to check. The simplest although not best solution since it might not work in certain environments would be to sayredirect_urieqresponse_uri.
Alternatively, we could provide the redirect_uri before the Authz Response object is sent to the Response URI, and make the wallet to append the state parameter to the Redirect URI in case they received the state parameter in the Authz Request Object. This way, there has to be no limitation on the actual length/value of the redirect_uri since this way it won't be able to encode the Authz Response Object (vp_token) in any form.
After some some internal discussions, the apu approach would only work if the entire protected header is provided in the last step since the RP needs the same binary representation of the protected header to verify the authentication tag of the JWE.
Another approach would be to use JWE with direct encryption where a wallet-generated secret is provided to the RP in the last redirect. The secret is used to derive the symmetric decryption key to decrypt the JWE.
The following is the current flow using direct_post and an additional response_code:
sequenceDiagram
actor u as User
participant vf as Verifier
participant vb as Verifier Response Endpoint
participant w as Wallet
autonumber
u->>vf: Interacts
vf->>vf: Create nonce
vf->>vb: Initiate transaction
vb->>vb: Generate transaction_id, request_id
vb->>vb: Cache [transaction_id] -> request_id
vb-->>vf: Return transaction_id, request_id as state
vf->>w: Authorization Request (response_uri, nonce, state)
u->w: User authentication and consent
w->>w: Generate Authorization Response
w->>vb: Authorization Response (vp_token, state)
vb->>vb: Check if request_id from state is valid
vb->>vb: Generate response_code
vb->>vb: Cache<br/>[transaction_id] -> Authorization Response, response_code
vb-->>w: Return redirect_uri with response_code
w->>vf: Redirect to redirect_uri
vf->>vb: Fetch response data (transaction_id, response_code)
vb->>vb: Lookup cached data for transaction_id
vb->>vb: Validate received response_code
vb-->>vf: Response data (vp_token, presentation_submission)
vf->>vf: Validate nonce
vf->>vf: Validate response data
The idea is to force the verifier to maintain the session by encrypting response and providing the key to the response endpoint and redirect URI using secret sharing (HKDF(IKM, salt) = derived key). The red rectangles show what needs to be changed.
sequenceDiagram
actor u as User
participant vf as Verifier
participant vb as Verifier Response Endpoint
participant w as Wallet
autonumber
u->>vf: Interacts
vf->>vf: Create nonce
vf->>vb: Initiate transaction
vb->>vb: Generate transaction_id, request_id
vb->>vb: Cache [transaction_id] -> request_id
vb-->>vf: Return transaction_id, request_id as state
vf->>w: Authorization Request (response_uri, nonce, state)
u->w: User authentication and consent
w->>w: Generate Authorization Response
rect RGB(255,0,0,.1)
w->>w: Generate new random salt and key (IKM) and derive encryption key using HKDF
w->>w: Encrypt Authorization Response with derived key
w->>vb: Authorization Response (JWE(vp_token, state), IKM)
end
vb->>vb: Check if request_id from state is valid
vb->>vb: Generate response_code
rect RGB(255,0,0,0.1)
vb->>vb: Cache<br/>[transaction_id] -> Authorization Response, response_code, IKM
vb-->>w: Return redirect_uri with response_code
w->>vf: Redirect to redirect_uri and HKDF salt
end
vf->>vb: Fetch response data (transaction_id, response_code)
vb->>vb: Lookup cached data for transaction_id
vb->>vb: Validate received response_code
rect RGB(255,0,0,0.1)
vb-->>vf: Response data (JWE, IKM)
vf->>vf: Derive decryption key using HKDF from IKM and salt redirect_uri parameter
vf->>vf: Decrypt JWE
end
vf->>vf: Validate nonce
vf->>vf: Validate response data
We seem to be missing the step where the wallet checks if the verifier is trusted before sending the authz response.
It seems you are proposing to encrypt the whole authorization request and then provide the Verifier Response Endpoint with the IKM. That would allow the Verifier Response Endpoint to decrypt the message and provide its frontend with encrypted data. What threat do we want to solve here?
I also re-read the intial attack description and have to admit, I fail to see the problem. What is described in step (12)
"The RP checks if the response_code is valid (exists, not invalidated, etc.) without validating that the response_code belongs to the session and returns the success page."
should be prevented by the Verifier Response Endpoint requiring the Verifier Frontend to query the response using the transaction_id, which shall be gathered from the session (see https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-response-mode-direct_post-2, step (8)).
It seems you are proposing to encrypt the whole authorization request and then provide the Verifier Response Endpoint with the IKM. That would allow the Verifier Response Endpoint to decrypt the message and provide its frontend with encrypted data. What threat do we want to solve here?
Additionally, the salt is provided via the browser redirect, the IKM is provided to the Response Endpoint. Only with salt and IKM, the verifier can derive the key to decrypt the data. The verifier is forced to associate the response in the Response Endpoint with the session in the frontend, otherwise the verifier cannot combine salt and IKM. That is the idea.
should be prevented by the Verifier Response Endpoint requiring the Verifier Frontend to query the response using the transaction_id, which shall be gathered from the session (see https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-response-mode-direct_post-2, step (8)).
Yes, if the verifier is compliant and diligent, then there is no problem. As stated, this proposal aims to enable the wallet to actively force the verifier to implement session validation correctly. Currently, if the verifier is negligent, the wallet has no way to determine whether the verifier is lazy nor has a mechanism to enforce diligence.
this has not made progress for 9 months. confirmed with @awoie this can be closed.