This is a proof of concept for adding plausible deniability to OpenID4VP using a Zero-Knowledge proof during presentation.
This library provides a simple interface to the OpenID4VP feature addition. There is also a Swift library available.
This project has been developed as part of the SPRIND EUDI Wallet Prototypes Challenge.
When disclosing claims, there are three parties involved. The issuer, the prover and the verifier. The issuer issues the information (PID) and signs them, so that the prover can prove the authenticity of the claims to a verifier. The issuer takes the claims (of which some are disclosed) and can verify that they were indeed issued by the trusted issuer for the prover.
In the normal variant, the prover sends the signature to the verifier. While the signature proves the authenticity, it gives the verifier the ability to prove to others that the claims about the prover are authentic. In other words: The prover can not deny the made claims and thus has no plausible deniability.
We tackle this problem by using zero knowledge proofs (ZKPs) about the possession of the digital signature without revealing it. When proving the authenticity about the claims to the verifier, the prover instead sends a ZKP that proves that they have a signature issued by the issuer for those claims. This ZKP is interactive between the provider and the verifier and thus only convinces the verifier about the authenticity. The verifier is not able to prove the authenticity to others later.
We make use of a known procedure for ZKP on discrete log problems (like ECDSA signatures) by using Diffie-Hellmann key exchange as a challenge algorithm. By challenging the prover to calculate a shared secret based on part of the signature (unknown to the verifier), the verifier can verify the possession of the signature without revealing it. This protocol is based on the three-pass identification scheme used on ECDSA signatures.
The issuer
Let’s bring to mind how
The prover sends
The verifier generates an ephemeral key pair
The prover calculates
The verifier can now verify that the prover knows
By comparing the equality of the received shared secret
We propose the following mechanism in order to add plausible deniablity to OpenID4VP with as few changes as possible. The apporach outlined here basically has three aspects:
-
Signaling between Relying Party (Verifier) and Wallet app is achived by introducing a seperate format for each already supported format (e. g.
mso_mdoc+zkp
formso_mdoc
and so on). That way the presentation can be done without this feature if one of the two parties doesn't support it. If the Relying Party supports ZKP it adds the format to the supported formats in thepresentation_definition
. If the wallet app then also supports ZKP it uses the...-zkp
format for presentation. -
The Zero-Knowledge poof requires an interactive communication step before the wallet can submit the actual authorization response. Relying Party and wallet app need to exchange some challenge/response-like values. This is done by the RP exposing an endpoint for exchanging those values. OpenID4VP already supports a step like this inbetween when encrypting the responses. There in order to agree on key material the verifier exposes an endpoint for doing the key agreement. So this isn't actually a significant protocol addition at all.
-
Verification is slightly modified so that the Relying Party not validates the issuer's signature directly but rather by verifying the challenge response submitted by the wallet. A successful verification of the challenge response is the equivalent of a (plain-)signature validation.
Additional data must be added to the client_metadata
in the presentation_definition
(see OpenID4VP section 9).
-
Add claim
vp_token_zkp_key_uri
pointing to the endpoint for generating ephemeral key(s) used for interactive zero knowledge proof(s) (see section 2)- This mechanism is similar to the exchange of the
JWKS
key for response encryption (see OpenID4VP section 6.3.) in the response mode "direct_post.jwt".
- This mechanism is similar to the exchange of the
-
Add possible zero-knowledge proof formats into
vp_formats
vc+sd-jwt+zkp
for a SD-JWT-like representation (based onvc+sd-jwt
format identifier for SD-JWT)mso_mdoc+zkp
for a mdoc-like representation (based onmso_mdoc
format identifier for Mdoc)- having a subkey
proof_type
containing possible curves and hash functions (e.g. usingsecp256r1
andsha256
leads to valuesecp256r1-sha256
here)
-
Specific format requirements may be specified for each
input_descriptor
element- this follows section 5 from DIF Presentation Exchange 2.0.0
-
Example:
{ "presentation_definition": { "input_descriptors": [ { "id": "eu.europa.ec.eudiw.pid.1", "format": { "vc+sd-jwt+zkp": { "proof_type": ["secp256r1-sha256"] } }, [...] } ], "client_metadata": { "vp_token_zkp_key_uri": "https://verifier-example.dev/wallet/zkp/wDTYGoskVS9i3tijp6z0B0p/jwks.json", "vp_formats": { "vc+sd-jwt+zkp": { "proof_type": ["secp256r1-sha256"] }, "mso_mdoc+zkp": { "proof_type": ["secp256r1-sha256"] } } [...] } [...] }, [...] }
-
since the
presentation_definition
may contain multipleinput_descriptor
s, this endpoint returns a set of public keys, one for each descriptor, identified byid
-
Method:
POST
-
URI:
client_metadata.vp_token_zkp_key_uri
of Authorization Request (see section 1) -
Example request (
application/json
):[ { "id": "eu.europa.ec.eudiw.pid.1", "digest": "_4KGX6aVS8B0T6Hewpr1H9h9-gjkjOyu8A6fb85GE2w=", "r": "C7U9q9o7dkDEhcOYXO9yfckQYoRxYs8z6POaac6EBjM=", "proof_type": "secp256r1-sha256" } ]
digest
being derived from the (payload inside)vp_token
using SHA-256r
being the first half of the signature from thevp_token
-
Example response (
application/jwk-set+json
):[ { "id": "eu.europa.ec.eudiw.pid.1", "kid": "eu.europa.ec.eudiw.pid.1", "kty": "EC", "crv": "P-256", "x": "jjR25_Day4VGHGGSn0uW_dzS7dVO0at8xh8gZ8z992A", "y": "16LSQqNZzvs5BEIqsdnkWw127L0j3ThNbiMAN0BIQQY" } ]
- those keys are ephemeral public keys derived from the input data (
digest
andr
) - the EC key generation is non-standard but also on the
secp256r1
curve - each key is matched to the
input_descriptor
using theid
- those keys are ephemeral public keys derived from the input data (
- the response structure stays the same, according to OpenID4VP section 6.1.
- inside the
descriptor_map
the usedformat
is specified- possible formats:
vc+sd-jwt+zkp
andmso_mdoc+zkp
(see above at section 1)
- possible formats:
- the tokens having a
...+zkp
format include parameters to finalize the zero-knowledge proof instead of the static signature from the issuer
The sequence diagram is similar to the one without zero-knowledge proof. The green rectangle highlights the additional steps. (Base for the diagram is in the Verifier Endpoint reference implementation).
sequenceDiagram
participant UA as User Agent
participant W as Wallet
participant V as Verifier
participant VE as Verifier Endpoint
UA->>V: Trigger presentation
V->>+VE: Initiate transaction
VE-->>-V: Authorization request as request_url
V->>UA: Render request as deep link
UA->>W: Trigger wallet and pass request
W->>+VE: Get authorization request via request_uri
VE-->>-W: authorization_request
W->>W: Parse authorization request
W->>+VE: Get presentation definition
VE-->>-W: presentation_definition
rect rgb(184, 233, 148)
note right of W: Interactive Zero-Knowledge-Proof
W->>+VE: Post initial values for ZKP via vp_token_zero_knowledge_proof_key_uri
VE->>VE: Generate ephemeral key using those initial ZKP values
VE->>-W: Return ephemeral key
W->>W: Finish ZKP, remove signature from vp_token
end
W->>W: Prepare response
W->>+VE: Post vp_token response
VE->>VE: Validate response and prepare response_code
VE-->>-W: Return redirect_uri with response_code
W->>UA: Refresh user agent to follow redirect_uri
UA->>V: Follow redirect_uri passing response_code
V->>+VE: Get wallet response passing response_code
VE->>VE: Validate response_code matches wallet response
VE-->>-V: Return wallet response
V->>UA: Render wallet response
This library offers a simple interface to the necessary operations performed on the plain credential data for the wallet (proofer) and on the authorization response for the Relying Party (verifier). mdoc
and SD-JWT
are supported.
- Issuer
- The proofer initializes a
ZKPProofer
instance providing the issuer's public key.val prover = ZKPProver(issuerPublicKey)
- They then create a challenge request that is begin sent to the verifier. In this example the SD-JWT format is selected and the issued JWT is provided to the library.
val challengeRequestData = prover.createChallengeRequest(VpTokenFormat.SDJWT, JWT)
- The proofer initializes a
- Verifier
- The verifier processes the challenge request by initializing a
ZKPVerifier
instance also by providing the issuer's public key.val verifier = ZKPVerifier(issuerPublicKey)
- They calculate the challange by providing the library with the challange request received from the proofer. The challenge is being submitted to the proofer and the calculated key needs to be stored temporarily by the verifier.
val (challenge, key) = verifier.createChallenge(challengeRequestData)
- The verifier processes the challenge request by initializing a
- Issuer
- The issuer solves the challenge and replaces the signature in the original credential by the result. The modified document is then presented to the verifier using the usual OpenID4VP protocol.
val zkpJwt = prover.answerChallenge(challenge, VpTokenFormat.SDJWT, JWT)
- The issuer solves the challenge and replaces the signature in the original credential by the result. The modified document is then presented to the verifier using the usual OpenID4VP protocol.
- Verifier
- The verifier processes the OpenID4VP response presented by the proofer as usual. Only the step of verifying the issuer's signature is replaces by verifying the challange resolution. Therefore the library needs the modified document and the stored key.
val proofed = verifier.verifyChallenge(VpTokenFormat.SDJWT, zkpJwt, key) println("PROOFED? ${proofed}")
- The verifier processes the OpenID4VP response presented by the proofer as usual. Only the step of verifying the issuer's signature is replaces by verifying the challange resolution. Therefore the library needs the modified document and the stored key.
val verifier = ZKPVerifier(issuerPublicKey)
val prover = ZKPProver(issuerPublicKey)
val challengeRequestData = prover.createChallengeRequest(VpTokenFormat.SDJWT, JWT)
val (challenge, key) = verifier.createChallenge(challengeRequestData)
val zkpJwt = prover.answerChallenge(challenge, VpTokenFormat.SDJWT, JWT)
val proofed = verifier.verifyChallenge(VpTokenFormat.SDJWT, zkpJwt, key)
println("PROOFED? ${proofed}")
Copyright 2024 TICE GmbH
This project is licensed under the Apache v2.0 License.