Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft22 - DCQL #292

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ kotlin {
}
compilerOptions {
apiVersion = KotlinVersion.KOTLIN_2_0
optIn = listOf(
"kotlinx.serialization.ExperimentalSerializationApi",
"kotlin.contracts.ExperimentalContracts",
)
freeCompilerArgs = listOf(
"-Xconsistent-data-class-copy-visibility",
)
}
}

Expand Down
37 changes: 37 additions & 0 deletions src/main/kotlin/eu/europa/ec/eudi/openid4vp/Format.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* 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.openid4vp

import kotlinx.serialization.Serializable

@Serializable
@JvmInline
public value class Format(public val value: String) {
init {
require(value.isNotBlank()) { "Format cannot be blank" }
}

override fun toString(): String = value

public companion object {
public val MsoMdoc: Format get() = Format(OpenId4VPSpec.FORMAT_MSO_MDOC)
public val SdJwtVcDeprecated: Format get() = Format(OpenId4VPSpec.FORMAT_SD_JWT_VC)
public val SdJwtVc: Format get() = Format(OpenId4VPSpec.FORMAT_SD_JWT_VC)
public val W3CLdpVc: Format get() = Format(OpenId4VPSpec.FORMAT_W3C_JSONLD_DATA_INTEGRITY)
public val W3CJwtVcJsonLd: Format get() = Format(OpenId4VPSpec.FORMAT_W3C_JSONLD_SIGNED_JWT)
public val W3CJwtVcJson: Format get() = Format(OpenId4VPSpec.FORMAT_W3C_SIGNED_JWT)
}
}
58 changes: 58 additions & 0 deletions src/main/kotlin/eu/europa/ec/eudi/openid4vp/OpenId4VPSpec.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* 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.openid4vp

public object OpenId4VPSpec {

public const val RM_DIRECT_POST: String = "direct_post"
public const val RM_DIRECT_POST_JWT: String = "direct_post.jwt"

public const val VP_TOKEN: String = "vp_token"

public const val WALLET_NONCE: String = "wallet_nonce"
public const val WALLET_METADATA: String = "wallet_metadata"

public const val FORMAT_MSO_MDOC: String = "mso_mdoc"

@Deprecated(
message = "Removed by spec",
)
public const val FORMAT_SD_JWT_VC_DEPRECATED: String = "vc+sd-jwt"
public const val FORMAT_SD_JWT_VC: String = "dc+sd-jwt"
public const val FORMAT_W3C_JSONLD_DATA_INTEGRITY: String = "ldp_vc"
public const val FORMAT_W3C_JSONLD_SIGNED_JWT: String = "jwt_vc_json-ld"
public const val FORMAT_W3C_SIGNED_JWT: String = "jwt_vc_json"

public const val DCQL_CREDENTIALS: String = "credentials"
public const val DCQL_CREDENTIAL_SETS: String = "credential_sets"

public const val DCQL_ID: String = "id"
public const val DCQL_FORMAT: String = "format"
public const val DCQL_META: String = "meta"
public const val DCQL_CLAIMS: String = "claims"
public const val DCQL_CLAIM_SETS: String = "claim_sets"
public const val DCQL_OPTIONS: String = "options"
public const val DCQL_REQUIRED: String = "required"
public const val DCQL_PURPOSE: String = "purpose"
public const val DCQL_PATH: String = "path"
public const val DCQL_VALUES: String = "values"
public const val DCQL_SD_JWT_VC_VCT_VALUES: String = "vct_values"
public const val DCQL_MSO_MDOC_DOCTYPE_VALUE: String = "doctype_value"
public const val DCQL_MSO_MDOC_NAMESPACE: String = "namespace"
public const val DCQL_MSO_MDOC_CLAIM_NAME: String = "claim_name"
}

public object SIOPv2
224 changes: 224 additions & 0 deletions src/main/kotlin/eu/europa/ec/eudi/openid4vp/dcql/ClaimPath.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
/*
* 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.openid4vp.dcql

import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.int
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.serializer
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

//
// That's a copy from sd-jwt-kt lib
//

/**
* The path is a non-empty [list][value] of [elements][ClaimPathElement],
* null values, or non-negative integers.
* It is used to [select][SelectPath] a particular claim in the credential or a set of claims.
*
* It is [serialized][ClaimPathSerializer] as a [JsonArray] which may contain
* string, `null`, or integer elements
*/
@Serializable(with = ClaimPathSerializer::class)
@JvmInline
value class ClaimPath(val value: List<ClaimPathElement>) {

init {
require(value.isNotEmpty())
}

override fun toString(): String = value.toString()

operator fun plus(other: ClaimPathElement): ClaimPath = ClaimPath(this.value + other)

operator fun plus(other: ClaimPath): ClaimPath = ClaimPath(this.value + other.value)

operator fun contains(that: ClaimPath): Boolean = value.foldIndexed(this.value.size <= that.value.size) { index, acc, thisElement ->
fun comp() = that.value.getOrNull(index)?.let { thatElement -> thatElement in thisElement } == true
acc and comp()
}

/**
* Appends a wild-card indicator [ClaimPathElement.AllArrayElements]
*/
fun allArrayElements(): ClaimPath = this + ClaimPathElement.AllArrayElements

/**
* Appends an indexed path [ClaimPathElement.ArrayElement]
*/
fun arrayElement(i: Int): ClaimPath = this + ClaimPathElement.ArrayElement(i)

/**
* Appends a named path [ClaimPathElement.Claim]
*/
fun claim(name: String): ClaimPath = this + ClaimPathElement.Claim(name)

/**
* Gets the ClaimPath of the parent element. Returns `null` to indicate the root element.
*/
fun parent(): ClaimPath? = value.dropLast(1)
.takeIf { it.isNotEmpty() }
?.let { ClaimPath(it) }

fun head(): ClaimPathElement = value.first()
fun tail(): ClaimPath? {
val tailElements = value.drop(1)
return if (tailElements.isEmpty()) return null
else ClaimPath(tailElements)
}

/**
* Gets the [head]
*/
operator fun component1(): ClaimPathElement = head()

/**
* Gets the [tail]
*/
operator fun component2(): ClaimPath? = tail()

companion object {
fun claim(name: String): ClaimPath = ClaimPath(listOf(ClaimPathElement.Claim(name)))
}
}

/**
* Elements of a [ClaimPath]
* - [Claim] indicates that the respective [key][Claim.name] is to be selected
* - [AllArrayElements] indicates that all elements of the currently selected array(s) are to be selected, and
* - [ArrayElement] indicates that the respective [index][ArrayElement.index] in an array is to be selected
*/
sealed interface ClaimPathElement {

/**
* Indicates that all elements of the currently selected array(s) are to be selected
* It is serialized as a [JsonNull]
*/
data object AllArrayElements : ClaimPathElement {
override fun toString(): String = "null"
}

/**
* Indicates that the respective [index][index] in an array is to be selected.
* It is serialized as an [integer][JsonPrimitive]
* @param index Non-negative index
*/
@JvmInline
value class ArrayElement(val index: Int) : ClaimPathElement {
init {
require(index >= 0) { "Index should be non-negative" }
}

override fun toString(): String = index.toString()
}

/**
* Indicates that the respective [key][name] is to be selected.
* It is serialized as a [string][JsonPrimitive]
* @param name the attribute name
*/
@JvmInline
value class Claim(val name: String) : ClaimPathElement {
override fun toString(): String = name
}

/**
* Indication of whether the current instance contains the other.
* @param that the element to compare with
* @return in case that the two elements are of the same type, and if they are equal (including attribute),
* then true is being returned. Also, an [AllArrayElements] contains [ArrayElement].
* In all other cases, a false is being returned.
*/
operator fun contains(that: ClaimPathElement): Boolean = when (this) {
AllArrayElements -> when (that) {
AllArrayElements -> true
is ArrayElement -> true
is Claim -> false
}

is ArrayElement -> this == that
is Claim -> this == that
}
}

inline fun <T> ClaimPathElement.fold(
ifAllArrayElements: () -> T,
ifArrayElement: (Int) -> T,
ifClaim: (String) -> T,
): T {
contract {
callsInPlace(ifAllArrayElements, InvocationKind.AT_MOST_ONCE)
callsInPlace(ifArrayElement, InvocationKind.AT_MOST_ONCE)
callsInPlace(ifClaim, InvocationKind.AT_MOST_ONCE)
}
return when (this) {
ClaimPathElement.AllArrayElements -> ifAllArrayElements()
is ClaimPathElement.ArrayElement -> ifArrayElement(index)
is ClaimPathElement.Claim -> ifClaim(name)
}
}

/**
* Serializer for [ClaimPath]
*/
object ClaimPathSerializer : KSerializer<ClaimPath> {

private fun claimPathElement(it: JsonPrimitive): ClaimPathElement = when {
it is JsonNull -> ClaimPathElement.AllArrayElements
it.isString -> ClaimPathElement.Claim(it.content)
it.intOrNull != null -> ClaimPathElement.ArrayElement(it.int)
else -> throw IllegalArgumentException("Only string, null, int can be used")
}

private fun claimPath(array: JsonArray): ClaimPath {
val elements = array.map {
require(it is JsonPrimitive)
claimPathElement(it)
}
return ClaimPath(elements)
}

private fun ClaimPath.toJson(): JsonArray = JsonArray(value.map { it.toJson() })

private fun ClaimPathElement.toJson(): JsonPrimitive = when (this) {
is ClaimPathElement.Claim -> JsonPrimitive(name)
is ClaimPathElement.ArrayElement -> JsonPrimitive(index)
ClaimPathElement.AllArrayElements -> JsonNull
}

private val arraySerializer = serializer<JsonArray>()

override val descriptor: SerialDescriptor = arraySerializer.descriptor

override fun serialize(encoder: Encoder, value: ClaimPath) {
val array = value.toJson()
arraySerializer.serialize(encoder, array)
}

override fun deserialize(decoder: Decoder): ClaimPath {
val array = arraySerializer.deserialize(decoder)
return claimPath(array)
}
}
Loading
Loading