Skip to content

Commit

Permalink
Add support for DCQL queries/responses (#232)
Browse files Browse the repository at this point in the history
dzarras authored Jan 23, 2025
1 parent ea1b54a commit 62432a5
Showing 21 changed files with 1,306 additions and 108 deletions.
80 changes: 70 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -154,8 +154,10 @@ sequenceDiagram
W->>W: Parse authorization request
W->>+VE: Get presentation definition
VE-->>-W: presentation_definition
opt
W->>+VE: Get presentation definition
VE-->>-W: presentation_definition
end
W->>W: Prepare response
@@ -201,8 +203,10 @@ sequenceDiagram
W->>W: Parse authorization request
W->>+VE: Get presentation definition
VE-->>-W: presentation_definition
opt
W->>+VE: Get presentation definition
VE-->>-W: presentation_definition
end
W->>W: Prepare response
@@ -245,14 +249,18 @@ This identifier is used in the [WalletApi](src/main/kotlin/eu/europa/ec/eudi/ver
An endpoint to control the content of the authorization request that will be prepared from the verifier backend service. Payload of this request is a json object with the following acceptable attributes:
- `type`: The type of the response to the authorization request. Allowed values are one of: `id_token`, `vp_token` or `vp_token id_token`.
- `id_token_type`: In case type is `id_token` controls the type of id_token that will be requested from wallet. Allowed values are one of `subject_signed_id_token` or `attester_signed_id_token`.
- `presentation_definition`: A json object that depicting the presentation definition to be included in the OpenId4VP authorization request in case `type` is 'vp_token'.
- `presentation_definition`: A json object depicting the presentation definition to be included in the OpenId4VP authorization request in case `type` is 'vp_token', or 'vp_token id_token'.
- `dcql_query`: A json object depicting the query, expressed using DCQL, to be included in the OpenId4VP authorization request in case `type` is 'vp_token', or 'vp_token id_token'.
- `nonce`: Nonce value to be included in the OpenId4VP authorization request.
- `response_mode`: Controls the `response_mode` attribute of the OpenId4VP authorization request. Allowed values are one of `direct_post` or `direct_post.jwt`.
- `jar_mode`: Controls the way the generated authorization request will be passed. If 'by_value' the request will be passed inline to the wallet upon request, if `by_reference` a `request_uri` url will be returned.
- `presentation_definition_mode`: Controls how the presentation definition will be embedded in the request. If 'by_value' it will be embedded inline, if `by_reference` a `presentation_definition_uri` url will be embedded in the request.
- `wallet_response_redirect_uri_template`: If provided will be used to construct the response to wallet, when it posts its response to the authorization request.

**Usage:**

Using Presentation Exchange:

```bash
curl -X POST -H "Content-type: application/json" -d '{
"type": "vp_token",
@@ -265,12 +273,12 @@ curl -X POST -H "Content-type: application/json" -d '{
{
"intent_to_retain": false,
"path": [
"$['eu.europa.ec.eudiw.pid.1']['family_name']"
"$['eu.europa.ec.eudi.pid.1']['family_name']"
]
}
]
},
"id": "eu.europa.ec.eudiw.pid.1",
"id": "eu.europa.ec.eudi.pid.1",
"format": {
"mso_mdoc": {
"alg": [
@@ -285,7 +293,44 @@ curl -X POST -H "Content-type: application/json" -d '{
"purpose": "We need to verify your identity"
}
]
},
},
"dcql_query": null,
"nonce": "nonce"
}' 'http://localhost:8080/ui/presentations'
```

Using DCQL:

```bash
curl -X POST -H "Content-type: application/json" -d '{
"type": "vp_token",
"dcql_query": {
"credentials": [
{
"id": "32f54163-7166-48f1-93d8-ff217bdb0653",
"format": "mso_mdoc",
"meta": {
"doctype_value": "eu.europa.ec.eudi.pid.1"
},
"claims": [
{
"namespace": "eu.europa.ec.eudi.pid.1",
"claim_name": "family_name"
}
]
}
],
"credential_sets": [
{
"options": [
[
"32f54163-7166-48f1-93d8-ff217bdb0653"
]
],
"purpose": "We need to verify your identity"
}
]
},
"nonce": "nonce"
}' 'http://localhost:8080/ui/presentations'
```
@@ -294,7 +339,7 @@ curl -X POST -H "Content-type: application/json" -d '{
```json
{
"transaction_id": "STMMbidoCQTtyk9id5IcoL8CqdC8rxgks5FF8cqqUrHvw0IL3AaIHGnwxvrvcEyUJ6uUPNdoBQDa7yCqpjtKaw",
"client_id": "dev.verifier-backend.eudiw.dev",
"client_id": "x509_san_dns:localhost",
"request_uri": "https://localhost:8080/wallet/request.jwt/5N6E7VZsmwXOGLz1Xlfi96MoyZVC3FZxwdAuJ26DnGcan-vYs-VAKErioQ58BWEsKlVw2_X49jpZHyp0Mk9nKw"
}
```
@@ -350,7 +395,7 @@ A form post (application/x-www-form-urlencoded encoding) with the following form
- `state`: The state claim included in the authorization request JWT. Its value matches the authorization request identifier.
- `id_token`: The requested id_token if authorization request 'response_type' attribute contains `id_token`.
- `vp_token`: The requested vp_token if authorization request 'response_type' attribute contains `vp_token`.
- `presentation_submission`: The presentation submission accompanying the vp_token in case 'response_type' attribute of authorization request contains `vp_token`.
- `presentation_submission`: The presentation submission accompanying the vp_token in case 'response_type' attribute of authorization request contains `vp_token` (applicable only when using Presentation Exchange).

_**response_mode = direct_post.jwt**_

@@ -359,6 +404,9 @@ A form post (application/x-www-form-urlencoded encoding) with the following form
- `response`: A string representing an encrypted JWT (JWE) that contains as claims the form parameters mentioned in the case above

**Usage:**

Using Presentation Exchange:

```bash
STATE=IsoY9VwZXJ8GS7zg4CEHsCNu-5LpAiPGjbwYssZ2nh3tnkhytNw2mNZLSFsKOwdG2Ww33hX6PUp6P9xImdS-qA
curl -v -X POST 'http://localhost:8080/wallet/direct_post' \
@@ -380,6 +428,18 @@ curl -v -X POST 'http://localhost:8080/wallet/direct_post' \
}
EOF
```

Using DCQL:

```bash
STATE=IsoY9VwZXJ8GS7zg4CEHsCNu-5LpAiPGjbwYssZ2nh3tnkhytNw2mNZLSFsKOwdG2Ww33hX6PUp6P9xImdS-qA
curl -v -X POST 'http://localhost:8080/wallet/direct_post' \
-H "Content-type: application/x-www-form-urlencoded" \
-H "Accept: application/json" \
--data-urlencode "state=$STATE" \
--data-urlencode 'vp_token={"32f54163-7166-48f1-93d8-ff217bdb0653": {"id": "123456"}}'
```

**Returns:**

* Same device case
Original file line number Diff line number Diff line change
@@ -26,6 +26,7 @@ internal data class RequestObject(
val responseType: List<String>,
val presentationDefinitionUri: URL?,
val presentationDefinition: PresentationDefinition? = null,
val dcqlQuery: DCQL? = null,
val scope: List<String>,
val idTokenType: List<String>,
val nonce: String,
@@ -87,6 +88,7 @@ internal fun requestObjectFromDomain(
idTokenType = idTokenType,
presentationDefinitionUri = presentationDefinitionUri,
presentationDefinition = presentationDefinition,
dcqlQuery = type.dcqlQueryOrNull,
responseType = responseType,
aud = aud,
nonce = presentation.nonce.value,
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@ import com.nimbusds.oauth2.sdk.Scope
import com.nimbusds.oauth2.sdk.id.ClientID
import com.nimbusds.oauth2.sdk.id.State
import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata
import eu.europa.ec.eudi.verifier.endpoint.adapter.out.json.toJackson
import eu.europa.ec.eudi.verifier.endpoint.domain.*
import eu.europa.ec.eudi.verifier.endpoint.domain.EmbedOption.ByReference
import eu.europa.ec.eudi.verifier.endpoint.domain.EmbedOption.ByValue
@@ -109,6 +110,7 @@ class SignRequestObjectNimbus : SignRequestObject {
optionalClaim("client_metadata", clientMetaData?.toJSONObject())
optionalClaim("response_uri", r.responseUri?.toExternalForm())
optionalClaim("presentation_definition_uri", r.presentationDefinitionUri?.toExternalForm())
optionalClaim("dcql_query", r.dcqlQuery?.toJackson())
build()
}
}
Original file line number Diff line number Diff line change
@@ -20,14 +20,18 @@ import com.nimbusds.jose.jwk.source.ImmutableJWKSet
import com.nimbusds.jose.proc.JWEDecryptionKeySelector
import com.nimbusds.jose.proc.SecurityContext
import com.nimbusds.jose.shaded.gson.Gson
import com.nimbusds.jose.util.Base64URL
import com.nimbusds.jose.util.JSONObjectUtils
import com.nimbusds.jwt.EncryptedJWT
import com.nimbusds.jwt.JWTClaimsSet
import com.nimbusds.jwt.JWTParser
import com.nimbusds.jwt.proc.DefaultJWTProcessor
import com.nimbusds.jwt.proc.JWTProcessor
import eu.europa.ec.eudi.prex.PresentationExchange
import eu.europa.ec.eudi.verifier.endpoint.domain.EphemeralEncryptionKeyPairJWK
import eu.europa.ec.eudi.verifier.endpoint.domain.JarmOption
import eu.europa.ec.eudi.verifier.endpoint.domain.Jwt
import eu.europa.ec.eudi.verifier.endpoint.domain.Nonce
import eu.europa.ec.eudi.verifier.endpoint.port.input.AuthorisationResponseTO
import eu.europa.ec.eudi.verifier.endpoint.port.out.jose.VerifyJarmJwtSignature
import kotlinx.serialization.json.Json
@@ -48,10 +52,17 @@ object VerifyJarmEncryptedJwtNimbus : VerifyJarmJwtSignature {
jarmOption: JarmOption,
ephemeralEcPrivateKey: EphemeralEncryptionKeyPairJWK?,
jarmJwt: Jwt,
apv: Nonce,
): Result<AuthorisationResponseTO> = runCatching {
processor(jarmOption, ephemeralEcPrivateKey)
.process(jarmJwt, null)
.mapToDomain()
val processor = processor(jarmOption, ephemeralEcPrivateKey)
val jwt = JWTParser.parse(jarmJwt)
if (jwt is EncryptedJWT) {
require(jwt.header.agreementPartyVInfo == Base64URL.encode(apv.value)) {
"'apv' header claim must be set to the 'nonce' value provided by the verifier"
}
}
val claimSet = processor.process(jwt, null)
claimSet.mapToDomain()
}

private fun processor(
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2023 European Commission
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.europa.ec.eudi.verifier.endpoint.adapter.out.json

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import kotlinx.serialization.KSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.serializer

private val objectMapper: ObjectMapper by lazy { jacksonObjectMapper() }

/**
* Converts a KotlinX Serialization serializable object to a Jackson JSON representation.
*/
internal inline fun <reified T> T.toJackson(serializer: KSerializer<T> = serializer()): Any {
val serialized = Json.encodeToString(serializer, this)
return objectMapper.readValue<Any>(serialized)
}
Loading

0 comments on commit 62432a5

Please sign in to comment.