openid / OpenID4VP

52 stars 18 forks source link

Add a way for a wallet to actively mitigate relay without requiring the verifier to be compliant #65

Open awoie opened 10 months ago

awoie commented 10 months ago

Anti-phishing for lazy RPs

Problem

When considering the same-device flow steps outlined below, the following attack is possible:

  1. An attacker acquires a genuine OID4VP Authz Request from a legitimate RP.
  2. The OID4VP Authz Request becomes associated with the attacker's session with the RP, typically through Cookies.
  3. 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.
  4. The user opens the link, triggering the user's native Wallet app.
  5. The Wallet successfully verifies the OID4VP Authz Request, which may involve validating the client_id according to the client_id_scheme Authz Request parameter.
  6. The user authenticates and provides consent to share their data with the legitimate RP.
  7. The Wallet generates the OID4VP Authz Response.
  8. The Wallet sends the OID4VP Authz Response to the Response URI of the RP.
  9. The Response URI returns a Redirect URI containing a response_code parameter tied to the session, usually via state.
  10. The Wallet redirects the user through the mobile browser, including the response_code parameter in the Redirect URI.
  11. The RP receives the response_code via the frontend (if the response_code was provided in the URL fragment) or backend (if the response_code is part of the URL query string).
  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.
  13. 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:

  1. ... (see flow above)
  2. The Wallet selects a secure random value for the apu value.
  3. The Wallet generates the Authz Response Object, encoding it as a JARM/JWE using the RP's public key and a new epk of the wallet.
  4. The Wallet removes the apu value 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.
  5. The Wallet sends the Authz Response Object to the Response URI.
  6. The Wallet receives the Redirect URI from the Response URI.
  7. The Wallet appends the apu parameter 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.
  8. The Wallet parses the redirect_uri parameter from the JSON response and redirects the user through the mobile browser to the Redirect URI with the additional apu parameter.
  9. The RP receives the Redirect URI, including the apu parameter, looks up the Authz Response received earlier in the flow, and can now decrypt it since it received the producer info.
  10. 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.

awoie commented 10 months ago

In the example above, the producer info (apu) is used by ECDH-ES as input to ConcatKDF which changes the derived symmetric key.

awoie commented 10 months ago

relates to #27

awoie commented 10 months ago

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:

awoie commented 9 months ago

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.

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.

awoie commented 9 months ago

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.

awoie commented 7 months ago

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
David-Chadwick commented 7 months ago

We seem to be missing the step where the wallet checks if the verifier is trusted before sending the authz response.

tlodderstedt commented 7 months ago

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)).

awoie commented 7 months ago

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.

awoie commented 7 months ago

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.