Skip to content

Commit

Permalink
Add: DCQL implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
acrusage-iaik committed Jan 17, 2025
1 parent 1181e1f commit 1581979
Show file tree
Hide file tree
Showing 31 changed files with 1,308 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package at.asitplus.openid.dcql

import at.asitplus.jsonpath.core.NodeList
import at.asitplus.jsonpath.core.NodeListEntry
import at.asitplus.jsonpath.core.NormalizedJsonPath
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import kotlin.jvm.JvmInline

/**
* 6.4. Claims Path Pointer
*
* A claims path pointer is a pointer into the JSON structure of the Verifiable Credential,
* identifying one or more claims. A claims path pointer MUST be a non-empty array of strings and
* non-negative integers. A string value indicates that the respective key is to be selected, a
* null value indicates that all elements of the currently selected array(s) are to be selected;
* and a non-negative integer indicates that the respective index in an array is to be selected.
* The path is formed as follows:
* Start with an empty array and repeat the following until the full path is formed.
* To address a particular claim within an object, append the key (claim name) to the array.
* To address an element within an array, append the index to the array (as a non-negative, 0-based
* integer).To address all elements within an array, append a null value to the array. Verifiers
* MUST NOT point to the same claim more than once in a single query. Wallets SHOULD ignore such
* duplicate claim queries.
*/
@Serializable
@JvmInline
value class DCQLClaimsPathPointer(
val segments: List<DCQLClaimsPathPointerSegment>,
) {
init {
validate()
}

constructor(startSegment: String) : this(
listOf(DCQLClaimsPathPointerSegment.DCQLClaimsPathPointerNameSegment(startSegment))
)

constructor(startSegment: UInt) : this(
listOf(DCQLClaimsPathPointerSegment.DCQLClaimsPathPointerIndexSegment(startSegment))
)

constructor(@Suppress("UNUSED_PARAMETER") nullValue: Nothing?) : this(
listOf(DCQLClaimsPathPointerSegment.DCQLClaimsPathPointerNullSegment)
)


operator fun plus(other: DCQLClaimsPathPointer) = DCQLClaimsPathPointer(
segments + other.segments
)

operator fun plus(key: String) = DCQLClaimsPathPointer(
segments + DCQLClaimsPathPointerSegment.DCQLClaimsPathPointerNameSegment(key)
)

operator fun plus(index: UInt) = DCQLClaimsPathPointer(
segments + DCQLClaimsPathPointerSegment.DCQLClaimsPathPointerIndexSegment(index)
)

operator fun plus(@Suppress("UNUSED_PARAMETER") nullValue: Nothing?) = DCQLClaimsPathPointer(
segments + DCQLClaimsPathPointerSegment.DCQLClaimsPathPointerNullSegment
)

/**
* 6.4.1. Processing
*
* In detail, the array is processed by the Wallet from left to right as follows:
* Select the root element of the Credential, i.e., the top-level JSON object.
*
* Process the query of the claims path pointer array from left to right:
* If the component is a string, select the element in the respective key in the currently
* selected element(s). If any of the currently selected element(s) is not an object, abort
* processing and return an error. If the key does not exist in any element currently selected,
* remove that element from the selection.
*
* If the component is null, select all elements of the currently selected array(s). If any of
* the currently selected element(s) is not an array, abort processing and return an error.If
* the component is a non-negative integer, select the element at the respective index in the
* currently selected array(s). If any of the currently selected element(s) is not an array,
* abort processing and return an error. If the index does not exist in a selected array,
* remove that array from the selection.If the set of elements currently selected is empty,
* abort processing and return an error.The result of the processing is the set of elements
* which is requested for presentation.
*/
fun query(jsonElement: JsonElement): NodeList {
var nodeList = listOf(NodeListEntry(NormalizedJsonPath(), jsonElement))
segments.forEach {
nodeList = it.query(nodeList)
}
return nodeList
}

private fun validate() {
if (segments.isEmpty()) {
throw IllegalArgumentException("Value must not be the empty list.")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package at.asitplus.openid.dcql

import at.asitplus.signum.indispensable.io.TransformingSerializerTemplate
import kotlinx.serialization.KSerializer
import kotlinx.serialization.json.JsonNull

object DCQLClaimsPathPointerNullSegmentSerializer : KSerializer<DCQLClaimsPathPointerSegment.DCQLClaimsPathPointerNullSegment> by TransformingSerializerTemplate<DCQLClaimsPathPointerSegment.DCQLClaimsPathPointerNullSegment, JsonNull>(
parent = JsonNull.serializer(),
encodeAs = {
JsonNull
},

decodeAs = {
DCQLClaimsPathPointerSegment.DCQLClaimsPathPointerNullSegment
}
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package at.asitplus.openid.dcql

import at.asitplus.catching
import at.asitplus.jsonpath.core.NodeList
import at.asitplus.jsonpath.core.NodeListEntry
import at.asitplus.openid.third_party.at.asitplus.jsonpath.core.plus
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlin.jvm.JvmInline

@Serializable(with = DCQLClaimsPathPointerSegmentSerializer::class)
sealed interface DCQLClaimsPathPointerSegment {
fun query(nodeList: NodeList): NodeList

@JvmInline
value class DCQLClaimsPathPointerNameSegment(val name: String) : DCQLClaimsPathPointerSegment {
override fun query(nodeList: NodeList) = nodeList.mapNotNull {
catching {
NodeListEntry(
normalizedJsonPath = it.normalizedJsonPath + name,
value = it.value.jsonObject[name]!!
)
}.getOrNull()
}
}

@JvmInline
value class DCQLClaimsPathPointerIndexSegment(val index: UInt) : DCQLClaimsPathPointerSegment {
override fun query(nodeList: NodeList) = nodeList.mapNotNull {
catching {
NodeListEntry(
normalizedJsonPath = it.normalizedJsonPath + index,
value = it.value.jsonArray[index.toInt()]
)
}.getOrNull()
}
}

@Serializable(with = DCQLClaimsPathPointerNullSegmentSerializer::class)
data object DCQLClaimsPathPointerNullSegment : DCQLClaimsPathPointerSegment {
override fun query(nodeList: NodeList) = nodeList.mapNotNull { claimQueryResult ->
catching {
claimQueryResult.value.jsonArray.mapIndexed { index, jsonElement ->
NodeListEntry(
normalizedJsonPath = claimQueryResult.normalizedJsonPath + index.toUInt(),
value = jsonElement
)
}
}.getOrNull()
}.flatten()
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package at.asitplus.openid.dcql

import at.asitplus.signum.indispensable.io.TransformingSerializerTemplate
import kotlinx.serialization.KSerializer
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.long
import kotlinx.serialization.json.longOrNull

object DCQLClaimsPathPointerSegmentSerializer : KSerializer<DCQLClaimsPathPointerSegment> by TransformingSerializerTemplate<DCQLClaimsPathPointerSegment, JsonPrimitive>(
parent = JsonPrimitive.serializer(),
encodeAs = {
when (it) {
is DCQLClaimsPathPointerSegment.DCQLClaimsPathPointerNameSegment -> {
JsonPrimitive(it.name)
}

is DCQLClaimsPathPointerSegment.DCQLClaimsPathPointerIndexSegment -> {
JsonPrimitive(it.index.toLong())
}

is DCQLClaimsPathPointerSegment.DCQLClaimsPathPointerNullSegment -> {
JsonNull
}
}
},

decodeAs = {
when {
it is JsonNull -> DCQLClaimsPathPointerSegment.DCQLClaimsPathPointerNullSegment

it.longOrNull != null -> DCQLClaimsPathPointerSegment.DCQLClaimsPathPointerIndexSegment(
it.long.toUInt()
)

else -> DCQLClaimsPathPointerSegment.DCQLClaimsPathPointerNameSegment(it.content)
}
}
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package at.asitplus.openid.dcql

import at.asitplus.KmmResult
import at.asitplus.catching
import kotlinx.serialization.Serializable

@Serializable(with = DCQLClaimsQuerySerializer::class)
interface DCQLClaimsQuery {
/**
* OID4VP draft 23: id: REQUIRED if claim_sets is present in the Credential Query; OPTIONAL
* otherwise. A string identifying the particular claim. The value MUST be a non-empty string
* consisting of alphanumeric, underscore (_) or hyphen (-) characters. Within the particular
* claims array, the same id MUST NOT be present more than once.
*/
val id: DCQLClaimsQueryIdentifier?

/**
* OID4VP draft 23: values: OPTIONAL. An array of strings, integers or boolean values that
* specifies the expected values of the claim. If the values property is present, the Wallet
* SHOULD return the claim only if the type and value of the claim both match for at least one
* of the elements in the array. Details of the processing rules are defined in Section 6.3.1.1.
*/
val values: List<DCQLExpectedClaimValue>?

object SerialNames {
const val ID = "id"
const val VALUES = "values"
}

fun <Credential : Any> executeClaimsQueryAgainstCredential(
credentialQuery: DCQLCredentialQuery,
credential: Credential,
credentialStructureExtractor: (Credential) -> DCQLCredentialClaimStructure,
): KmmResult<DCQLClaimsQueryResult> = catching {
when (this) {
is DCQLJsonClaimsQuery -> {
executeJsonClaimsQueryAgainstCredential(
credentialQuery = credentialQuery,
credential = credential,
credentialStructureExtractor = {
credentialStructureExtractor(it) as DCQLCredentialClaimStructure.JsonBasedDCQLCredentialClaimStructure
}
).getOrThrow()
}

is DCQLIsoMdocClaimsQuery -> {
executeIsoMdocClaimsQueryAgainstCredential(
credentialQuery = credentialQuery,
credential = credential,
credentialStructureExtractor = {
credentialStructureExtractor(it) as DCQLCredentialClaimStructure.IsoMdocDCQLCredentialClaimStructure
}
).getOrThrow()
}

else -> throw IllegalStateException("Unsupported claim query type")
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package at.asitplus.openid.dcql

import kotlinx.serialization.Serializable
import kotlin.jvm.JvmInline

/**
* A string identifying the particular claim. The value MUST be a non-empty string consisting of
* alphanumeric, underscore (_) or hyphen (-) characters. Within the particular claims array, the
* same id MUST NOT be present more than once.
*/
@Serializable
@JvmInline
value class DCQLClaimsQueryIdentifier(val string: String) {
init {
validate()
}

private fun validate() {
if(string.isEmpty()) {
throw IllegalArgumentException("Value must not be the empty string.")
}
string.forEach {
if(it != '_' && it != '-' && !it.isLetterOrDigit()) {
throw IllegalArgumentException("Value must only consist of alphanumeric, underscore (_) or hyphen (-) characters.")
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package at.asitplus.openid.dcql

import at.asitplus.jsonpath.core.NodeList

sealed interface DCQLClaimsQueryResult {
class JsonClaimsQueryResult(
val nodeList: NodeList
) : DCQLClaimsQueryResult

class IsoMdocClaimsQueryResult(
val namespace: String,
val claimName: String,
val claimValue: Any,
) : DCQLClaimsQueryResult
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package at.asitplus.openid.dcql

import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.jsonObject

object DCQLClaimsQuerySerializer : JsonContentPolymorphicSerializer<DCQLClaimsQuery>(DCQLClaimsQuery::class) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<DCQLClaimsQuery> {
val parameters = element.jsonObject
return when {
DCQLIsoMdocClaimsQuery.SerialNames.NAMESPACE in parameters || DCQLIsoMdocClaimsQuery.SerialNames.CLAIM_NAME in parameters -> DCQLIsoMdocClaimsQuery.serializer()
else -> DCQLJsonClaimsQuery.serializer()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package at.asitplus.openid.dcql

import kotlinx.serialization.json.JsonElement
import kotlin.jvm.JvmInline

sealed interface DCQLCredentialClaimStructure {
@JvmInline
value class JsonBasedDCQLCredentialClaimStructure(val jsonElement: JsonElement) : DCQLCredentialClaimStructure

@JvmInline
value class IsoMdocDCQLCredentialClaimStructure(val namespaceClaimValueMap: Map<String, Map<String, Any?>>) :
DCQLCredentialClaimStructure
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package at.asitplus.openid.dcql

import kotlinx.serialization.Serializable

@Serializable(with = DCQLCredentialMetadataAndValidityConstraintsSerializer::class)
interface DCQLCredentialMetadataAndValidityConstraints


Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package at.asitplus.openid.dcql

import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.jsonObject

object DCQLCredentialMetadataAndValidityConstraintsSerializer :
JsonContentPolymorphicSerializer<DCQLCredentialMetadataAndValidityConstraints>(
DCQLCredentialMetadataAndValidityConstraints::class
) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<DCQLCredentialMetadataAndValidityConstraints> {
val parameters = element.jsonObject
return when {
DCQLSdJwtCredentialMetadataAndValidityConstraints.SerialNames.VCT_VALUES in parameters -> DCQLSdJwtCredentialMetadataAndValidityConstraints.serializer()
DCQLIsoMdocCredentialMetadataAndValidityConstraints.SerialNames.DOCTYPE_VALUE in parameters -> DCQLIsoMdocCredentialMetadataAndValidityConstraints.serializer()
else -> throw IllegalArgumentException("Deserializer not found")
}
}
}
Loading

0 comments on commit 1581979

Please sign in to comment.