Skip to content

Commit

Permalink
Update credential issuer metadata to draft 15 (#382)
Browse files Browse the repository at this point in the history
* Update credential issuer metadata to draft 15
  • Loading branch information
vafeini authored Feb 20, 2025
1 parent 5120d4b commit 717f323
Show file tree
Hide file tree
Showing 15 changed files with 1,348 additions and 848 deletions.
191 changes: 191 additions & 0 deletions src/main/kotlin/eu/europa/ec/eudi/openid4vci/ClaimPath.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/*
* 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.openid4vci

import eu.europa.ec.eudi.openid4vci.ClaimPathElement.*
import eu.europa.ec.eudi.openid4vci.internal.ClaimPathSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.*
import kotlin.contracts.ExperimentalContracts
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 + AllArrayElements

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

/**
* Appends a named path [ClaimPathElement.Claim]
*/
fun claim(name: String): ClaimPath = this + 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(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
}
}

@OptIn(ExperimentalContracts::class)
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) {
AllArrayElements -> ifAllArrayElements()
is ArrayElement -> ifArrayElement(index)
is ClaimPathElement.Claim -> ifClaim(name)
}
}

fun JsonArray.asClaimPath(): ClaimPath {
val elements = map {
require(it is JsonPrimitive)
it.asClaimPathElement()
}
return ClaimPath(elements)
}

fun JsonPrimitive.asClaimPathElement(): ClaimPathElement = when {
this is JsonNull -> AllArrayElements
isString -> Claim(content)
intOrNull != null -> ArrayElement(int)
else -> throw IllegalArgumentException("Only string, null, int can be used")
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ enum class ProofType : Serializable {
}

sealed interface ProofTypeMeta : Serializable {
data class Jwt(val algorithms: List<JWSAlgorithm>) : ProofTypeMeta {
data class Jwt(
val algorithms: List<JWSAlgorithm>,
val keyAttestationRequirement: KeyAttestationRequirement,
) : ProofTypeMeta {
init {
require(algorithms.isNotEmpty()) { "Supported algorithms in case of JWT cannot be empty" }
}
Expand All @@ -70,6 +73,28 @@ sealed interface ProofTypeMeta : Serializable {
data class Unsupported(val type: String) : ProofTypeMeta
}

sealed interface KeyAttestationRequirement {

data object NotRequired : KeyAttestationRequirement {
private fun readResolve(): Any = NotRequired
}

data object RequiredNoConstraints : KeyAttestationRequirement {
private fun readResolve(): Any = RequiredNoConstraints
}

data class Required(
val keyStorageConstraints: List<String>,
val userAuthenticationConstraints: List<String>,
) : KeyAttestationRequirement {
init {
require(keyStorageConstraints.isNotEmpty() || userAuthenticationConstraints.isNotEmpty()) {
"Either key storage or user authentication constraints must be provided"
}
}
}
}

fun ProofTypeMeta.type(): ProofType? = when (this) {
is ProofTypeMeta.Jwt -> ProofType.JWT
is ProofTypeMeta.LdpVp -> ProofType.LDP_VP
Expand Down Expand Up @@ -103,6 +128,7 @@ data class Display(
val logo: Logo? = null,
val description: String? = null,
val backgroundColor: CssColor? = null,
val backgroundImage: URI? = null,
val textColor: CssColor? = null,
) : Serializable {

Expand All @@ -124,15 +150,16 @@ sealed interface CredentialConfiguration : Serializable {
val credentialSigningAlgorithmsSupported: List<String>
val proofTypesSupported: ProofTypesSupported
val display: List<Display>
val claims: List<Claim>?
}

/**
* The details of a Claim.
*/
@kotlinx.serialization.Serializable
data class Claim(
@SerialName("path") val path: ClaimPath,
@SerialName("mandatory") val mandatory: Boolean? = false,
@SerialName("value_type") val valueType: String? = null,
@SerialName("display") val display: List<Display> = emptyList(),
) : Serializable {

Expand All @@ -148,12 +175,6 @@ data class Claim(
}
typealias Namespace = String
typealias ClaimName = String
typealias MsoMdocClaims = Map<Namespace, Map<ClaimName, Claim>>

fun MsoMdocClaims.toClaimSet(): MsoMdocClaimSet = MsoMdocClaimSet(
mapValues { (_, claims) -> claims.keys.toList() }
.flatMap { (nameSpace, claims) -> claims.map { claimName -> nameSpace to claimName } },
)

data class MsoMdocPolicy(val oneTimeUse: Boolean, val batchSize: Int?) : Serializable

Expand All @@ -170,8 +191,7 @@ data class MsoMdocCredential(
override val proofTypesSupported: ProofTypesSupported = ProofTypesSupported.Empty,
override val display: List<Display> = emptyList(),
val docType: String,
val claims: MsoMdocClaims = emptyMap(),
val order: List<ClaimName> = emptyList(),
override val claims: List<Claim> = emptyList(),
) : CredentialConfiguration

data class SdJwtVcCredential(
Expand All @@ -181,14 +201,12 @@ data class SdJwtVcCredential(
override val proofTypesSupported: ProofTypesSupported = ProofTypesSupported.Empty,
override val display: List<Display> = emptyList(),
val type: String,
val claims: Map<ClaimName, Claim?>?,
val order: List<ClaimName> = emptyList(),
override val claims: List<Claim> = emptyList(),
) : CredentialConfiguration

data class W3CJsonLdCredentialDefinition(
val context: List<URL>,
val type: List<String>,
val credentialSubject: Map<ClaimName, Claim?>?,
)

/**
Expand All @@ -200,10 +218,8 @@ data class W3CJsonLdDataIntegrityCredential(
override val credentialSigningAlgorithmsSupported: List<String> = emptyList(),
override val proofTypesSupported: ProofTypesSupported = ProofTypesSupported.Empty,
override val display: List<Display> = emptyList(),
val context: List<String> = emptyList(),
val type: List<String> = emptyList(),
val credentialDefinition: W3CJsonLdCredentialDefinition,
val order: List<ClaimName> = emptyList(),
override val claims: List<Claim> = emptyList(),
) : CredentialConfiguration

/**
Expand All @@ -215,9 +231,8 @@ data class W3CJsonLdSignedJwtCredential(
override val credentialSigningAlgorithmsSupported: List<String> = emptyList(),
override val proofTypesSupported: ProofTypesSupported = ProofTypesSupported.Empty,
override val display: List<Display> = emptyList(),
val context: List<String> = emptyList(),
val credentialDefinition: W3CJsonLdCredentialDefinition,
val order: List<ClaimName> = emptyList(),
override val claims: List<Claim> = emptyList(),
) : CredentialConfiguration

/**
Expand All @@ -230,11 +245,10 @@ data class W3CSignedJwtCredential(
override val proofTypesSupported: ProofTypesSupported = ProofTypesSupported.Empty,
override val display: List<Display> = emptyList(),
val credentialDefinition: CredentialDefinition,
val order: List<ClaimName> = emptyList(),
override val claims: List<Claim> = emptyList(),
) : CredentialConfiguration {

data class CredentialDefinition(
val type: List<String>,
val credentialSubject: Map<ClaimName, Claim?>?,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ data class CredentialIssuerMetadata(
val credentialIssuerIdentifier: CredentialIssuerId,
val authorizationServers: List<HttpsUrl> = listOf(credentialIssuerIdentifier.value),
val credentialEndpoint: CredentialIssuerEndpoint,
val nonceEndpoint: CredentialIssuerEndpoint? = null,
val deferredCredentialEndpoint: CredentialIssuerEndpoint? = null,
val notificationEndpoint: CredentialIssuerEndpoint? = null,
val credentialResponseEncryption: CredentialResponseEncryption = CredentialResponseEncryption.NotSupported,
Expand Down Expand Up @@ -169,6 +170,11 @@ sealed class CredentialIssuerMetadataValidationError(cause: Throwable) : Credent
*/
class InvalidCredentialEndpoint(cause: Throwable) : CredentialIssuerMetadataValidationError(cause)

/**
* The URL of the Nonce Endpoint is not valid.
*/
class InvalidNonceEndpoint(cause: Throwable) : CredentialIssuerMetadataValidationError(cause)

/**
* The URL of the Deferred Credential Endpoint is not valid.
*/
Expand Down
Loading

0 comments on commit 717f323

Please sign in to comment.