Skip to content

Commit

Permalink
Change: Move DCQL primitives to oidc module
Browse files Browse the repository at this point in the history
  • Loading branch information
acrusage-iaik committed Jan 16, 2025
1 parent 75667d4 commit 1a007d9
Show file tree
Hide file tree
Showing 52 changed files with 1,001 additions and 1,263 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package at.asitplus.openid

import at.asitplus.KmmResult.Companion.wrap
import at.asitplus.dif.PresentationDefinition
import at.asitplus.openid.dcql.DCQLQuery
import at.asitplus.signum.indispensable.josef.io.InstantLongSerializer
import kotlinx.datetime.Instant
import kotlinx.serialization.SerialName
Expand Down Expand Up @@ -146,6 +147,12 @@ data class AuthenticationRequestParameters(
@SerialName("presentation_definition_uri")
val presentationDefinitionUrl: String? = null,

/**
* OID4VP: dcql_query: A string containing a JSON-encoded DCQL query as defined in Section 6.
*/
@SerialName("dcql_query")
val dcqlQuery: DCQLQuery? = null,

/**
* RFC9396: The request parameter `authorization_details` contains, in JSON notation, an array of objects.
* Each JSON object contains the data to specify the authorization requirements for a certain type of resource.
Expand Down
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 DCQLClaimQueryIdentifier(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,123 @@
package at.asitplus.openid.dcql

import at.asitplus.catching
import at.asitplus.jsonpath.core.NodeList
import at.asitplus.jsonpath.core.NodeListEntry
import at.asitplus.jsonpath.core.NormalizedJsonPath
import at.asitplus.jsonpath.core.NormalizedJsonPathSegment
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
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.DCQLClaimsPathPointerClaimSegment(startSegment))
)

constructor(startSegment: UInt?) : this(
listOf(startSegment?.let {
DCQLClaimsPathPointerSegment.DCQLClaimsPathPointerIndexSegment(it)
})
)

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

operator fun plus(index: UInt?) = DCQLClaimsPathPointer(
segments + index?.let {
DCQLClaimsPathPointerSegment.DCQLClaimsPathPointerIndexSegment(it)
}
)

/**
* 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 claims = listOf(NodeListEntry(NormalizedJsonPath(), jsonElement))
for(segment in segments) {
claims = when(segment) {
is DCQLClaimsPathPointerSegment.DCQLClaimsPathPointerClaimSegment -> claims.mapNotNull {
catching {
NodeListEntry(
normalizedJsonPath = it.normalizedJsonPath + segment.key,
value = it.value.jsonObject[segment.key]!!
)
}.getOrNull()
}
is DCQLClaimsPathPointerSegment.DCQLClaimsPathPointerIndexSegment -> claims.mapNotNull {
catching {
NodeListEntry(
normalizedJsonPath = it.normalizedJsonPath + segment.index,
value = it.value.jsonArray[segment.index.toInt()]
)
}.getOrNull()
}
null -> claims.mapNotNull { claimQueryResult ->
catching {
claimQueryResult.value.jsonArray.mapIndexed { index, jsonElement ->
NodeListEntry(
normalizedJsonPath = claimQueryResult.normalizedJsonPath + index.toUInt(),
value = jsonElement
)
}
}.getOrNull()
}.flatten()
}
}
return claims
}

private fun validate() {
if (segments.isEmpty()) {
throw IllegalArgumentException("Value must not be the empty list.")
}
}
}

private operator fun NormalizedJsonPath.plus(name: String) = NormalizedJsonPath(segments + NormalizedJsonPathSegment.NameSegment(name))
private operator fun NormalizedJsonPath.plus(index: UInt) = NormalizedJsonPath(segments + NormalizedJsonPathSegment.IndexSegment(index))
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package at.asitplus.wallet.lib.data.oidc.oid4vp.dcql
package at.asitplus.openid.dcql

import kotlinx.serialization.Serializable
import kotlin.jvm.JvmInline
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package at.asitplus.wallet.lib.data.oidc.oid4vp.dcql
package at.asitplus.openid.dcql

import at.asitplus.signum.indispensable.io.TransformingSerializerTemplate
import kotlinx.serialization.KSerializer
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package at.asitplus.openid.dcql

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: DCQLClaimQueryIdentifier?

/**
* 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"
}
}
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 DCQLClaimsQueryMatchingResult {
class JsonClaimsMatchingResult(
val nodeList: NodeList
) : DCQLClaimsQueryMatchingResult

class IsoMdocClaimsMatchingResult(
val namespace: String,
val claimName: String,
val claimValue: Any,
) : DCQLClaimsQueryMatchingResult
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package at.asitplus.wallet.lib.data.oidc.oid4vp.dcql
package at.asitplus.openid.dcql

import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
Expand All @@ -9,7 +9,7 @@ object DCQLClaimsQuerySerializer : JsonContentPolymorphicSerializer<DCQLClaimsQu
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()
DCQLIsoMdocClaimQuery.SerialNames.NAMESPACE in parameters || DCQLIsoMdocClaimQuery.SerialNames.CLAIM_NAME in parameters -> DCQLIsoMdocClaimQuery.serializer()
else -> DCQLJsonClaimsQuery.serializer()
}
}
Expand Down
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,20 @@
package at.asitplus.openid.dcql

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

@Serializable
@JvmInline
value class DCQLCredentialFormatIdentifier(val string: String) {
companion object {
val SD_JWT = DCQLCredentialFormatIdentifier(Constants.SD_JWT)
val MSO_MDOC = DCQLCredentialFormatIdentifier(Constants.MSO_MDOC)
val ANONCREDS = DCQLCredentialFormatIdentifier(Constants.ANONCREDS)
}

object Constants {
val SD_JWT = "dc+sd-jwt"
val MSO_MDOC = "mso_mdoc"
val ANONCREDS = "ac_vc"
}
}
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
@@ -1,4 +1,4 @@
package at.asitplus.wallet.lib.data.oidc.oid4vp.dcql
package at.asitplus.openid.dcql

import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
Expand All @@ -12,6 +12,7 @@ object DCQLCredentialMetadataAndValidityConstraintsSerializer :
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")
}
Expand Down
Loading

0 comments on commit 1a007d9

Please sign in to comment.