diff --git a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationResponseParameters.kt b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationResponseParameters.kt index 4e7c1e84e..3cbe19d8b 100644 --- a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationResponseParameters.kt +++ b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/AuthenticationResponseParameters.kt @@ -5,6 +5,7 @@ import io.github.aakira.napier.Napier import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.JsonElement /** * Contents of an OIDC Authentication Response. @@ -41,7 +42,7 @@ data class AuthenticationResponseParameters( * format is already represented as a JSON object or a JSON string. */ @SerialName("vp_token") - val vpToken: String? = null, + val vpToken: JsonElement? = null, /** * OID4VP: REQUIRED. The presentation_submission element as defined in DIF.PresentationExchange. It contains diff --git a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt index a537177c7..b20bb38a6 100644 --- a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt +++ b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopVerifier.kt @@ -41,7 +41,8 @@ import at.asitplus.wallet.lib.oidvci.decodeFromUrlQuery import at.asitplus.wallet.lib.oidvci.encodeToParameters import com.benasher44.uuid.uuid4 import io.github.aakira.napier.Napier -import io.ktor.http.* +import io.ktor.http.URLBuilder +import io.ktor.http.Url import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.datetime.Clock @@ -450,23 +451,19 @@ class OidcSiopVerifier( val descriptors = presentationSubmission.descriptorMap ?: return AuthnResponseResult.ValidationError("presentation_submission", params.state) .also { Napier.w("presentation_submission contains no descriptors") } - val vp = params.vpToken // TODO: this may be an array of verifiable presentations + val verifiablePresentation = params.vpToken ?: return AuthnResponseResult.ValidationError("vp_token is null", params.state) .also { Napier.w("No VP in response") } - // descriptors use JsonPaths to describe the resulting input descriptor match locations - // -> it seems fitting to compile the vpToken to a jsonObject - // making use of assumption:50c2c2bc-df25-4e9d-9890-67bde5a0e677 - val verifiablePresentations = jsonSerializer.parseToJsonElement(vp) - val validationResults = descriptors.map { descriptor -> val cumulativeJsonPath = try { descriptor.cumulativeJsonPath } catch (exception: InvalidJsonPathException) { return AuthnResponseResult.ValidationError("presentation_submission", params.state) } + Napier.d("matching jsonPath: $cumulativeJsonPath") val relatedPresentation = - verifiablePresentations.matchJsonPath(cumulativeJsonPath).entries.first().value + verifiablePresentation.matchJsonPath(cumulativeJsonPath).entries.first().value val format = descriptor.format val result = when (format) { @@ -541,4 +538,4 @@ private val PresentationSubmissionDescriptor.cumulativeJsonPath: String descriptorIterator = descriptorIterator.nestedPath } return cummulativeJsonPath - } \ No newline at end of file + } diff --git a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopWallet.kt b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopWallet.kt index 86124b0df..62513aaf6 100644 --- a/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopWallet.kt +++ b/vclib-openid/src/commonMain/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopWallet.kt @@ -6,6 +6,7 @@ import at.asitplus.crypto.datatypes.jws.JwsSigned import at.asitplus.crypto.datatypes.jws.toJsonWebKey import at.asitplus.wallet.lib.agent.CryptoService import at.asitplus.wallet.lib.agent.Holder +import at.asitplus.wallet.lib.data.dif.ClaimFormatEnum import at.asitplus.wallet.lib.data.dif.PresentationDefinition import at.asitplus.wallet.lib.jws.DefaultJwsService import at.asitplus.wallet.lib.jws.DefaultVerifierJwsService @@ -28,12 +29,12 @@ import at.asitplus.wallet.lib.oidvci.formUrlEncode import io.github.aakira.napier.Napier import io.ktor.http.URLBuilder import io.ktor.http.Url -import io.ktor.util.encodeBase64 import io.ktor.util.flattenEntries import io.matthewnelson.encoding.base16.Base16 import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.coroutines.runBlocking import kotlinx.datetime.Clock +import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.add import kotlinx.serialization.json.buildJsonArray import kotlin.time.Duration.Companion.seconds @@ -81,7 +82,8 @@ class OidcSiopWallet( clientId: String = "https://wallet.a-sit.at/", jwkSetRetriever: (String) -> JsonWebKeySet? = { null }, requestObjectCandidateRetriever: RequestObjectCandidateRetriever = { listOf() }, - presentationDefinitionRetriever: PresentationDefinitionRetriever = object : PresentationDefinitionRetriever { + presentationDefinitionRetriever: PresentationDefinitionRetriever = object : + PresentationDefinitionRetriever { override suspend fun retrieveFromUrl(url: Url): PresentationDefinition? { return null } @@ -301,18 +303,6 @@ class OidcSiopWallet( .also { Napier.w("response_type is not specified") } if (!params.responseType.contains(VP_TOKEN) && params.presentationDefinition == null) return KmmResult.failure(OAuth2Exception(Errors.INVALID_REQUEST)) - .also { Napier.w("vp_token not requested") } - if (params.clientMetadata.vpFormats != null) { - if (params.clientMetadata.vpFormats.jwtVp?.algorithms?.contains(jwsService.algorithm.identifier) != true) - return KmmResult.failure(OAuth2Exception(Errors.REGISTRATION_VALUE_NOT_SUPPORTED)) - .also { Napier.w("Incompatible JWT algorithms") } - if (params.clientMetadata.vpFormats.jwtSd?.algorithms?.contains(jwsService.algorithm.identifier) != true) - return KmmResult.failure(OAuth2Exception(Errors.REGISTRATION_VALUE_NOT_SUPPORTED)) - .also { Napier.w("Incompatible JWT algorithms") } - if (params.clientMetadata.vpFormats.msoMdoc?.algorithms?.contains(jwsService.algorithm.identifier) != true) - return KmmResult.failure(OAuth2Exception(Errors.REGISTRATION_VALUE_NOT_SUPPORTED)) - .also { Napier.w("Incompatible JWT algorithms") } - } if (params.nonce == null) return KmmResult.failure(OAuth2Exception(Errors.INVALID_REQUEST)) .also { Napier.w("nonce is null") } @@ -346,7 +336,7 @@ class OidcSiopWallet( retriever.deriveFromScope(it) } } - } ?: throw OAuth2Exception(Errors.INVALID_REQUEST) + } ?: throw OAuth2Exception(Errors.REGISTRATION_VALUE_NOT_SUPPORTED) .also { Napier.d("No valid presentation definition has been found for request $params") } val presentationSubmissionContainer = holder.createPresentation( @@ -357,6 +347,24 @@ class OidcSiopWallet( ?: return KmmResult.failure(OAuth2Exception(Errors.USER_CANCELLED)) .also { Napier.w("Could not create presentation") } + params.clientMetadata.vpFormats?.let { supportedFormats -> + presentationSubmissionContainer.presentationSubmission.descriptorMap?.mapIndexed { index, descriptor -> + val isMissingFormatSupport = when (descriptor.format) { + ClaimFormatEnum.JWT -> supportedFormats.jwt?.algorithms?.contains(jwsService.algorithm.identifier) != true + ClaimFormatEnum.JWT_VC -> supportedFormats.jwtVc?.algorithms?.contains(jwsService.algorithm.identifier) != true + ClaimFormatEnum.JWT_VP -> supportedFormats.jwtVp?.algorithms?.contains(jwsService.algorithm.identifier) != true + ClaimFormatEnum.JWT_SD -> supportedFormats.jwtSd?.algorithms?.contains(jwsService.algorithm.identifier) != true + ClaimFormatEnum.MSO_MDOC -> supportedFormats.msoMdoc?.algorithms?.contains(jwsService.algorithm.identifier) != true + else -> true + } + + if (isMissingFormatSupport) { + return KmmResult.failure( + OAuth2Exception(Errors.REGISTRATION_VALUE_NOT_SUPPORTED) + ).also { Napier.w("Incompatible JWT algorithms for claim format ${descriptor.format}: $presentationDefinition") } + } + } + } return KmmResult.success( AuthenticationResponseParameters( @@ -372,12 +380,12 @@ class OidcSiopWallet( ) } }.let { - if (it.size == 1) it[0] + if (it.size == 1) JsonPrimitive(it[0]) else buildJsonArray { for (value in it) { add(value) } - }.toString() // making use of assumption:416dc455-ebb7-4b74-86e4-b63dc8cfe279 + } }, presentationSubmission = presentationSubmissionContainer.presentationSubmission, ) @@ -402,11 +410,4 @@ interface PresentationDefinitionRetriever { * Implementations need to match the scope to a known presentation definition. */ suspend fun deriveFromScope(scope: String): PresentationDefinition? -} - -private fun String.encodeBase64Url(): String { - return this.encodeBase64() - .replace('+', Char(0x2B)) - .replace('/', Char(0x2F)) - .replace('=', Char(0x3D)) } \ No newline at end of file diff --git a/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopProtocolTest.kt b/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopProtocolTest.kt index 85e0d4f74..bf4699f48 100644 --- a/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopProtocolTest.kt +++ b/vclib-openid/src/commonTest/kotlin/at/asitplus/wallet/lib/oidc/OidcSiopProtocolTest.kt @@ -10,14 +10,16 @@ import at.asitplus.wallet.lib.agent.Verifier import at.asitplus.wallet.lib.agent.VerifierAgent import at.asitplus.wallet.lib.data.AtomicAttribute2023 import at.asitplus.wallet.lib.data.ConstantIndex +import at.asitplus.wallet.lib.data.dif.FormatHolder import at.asitplus.wallet.lib.jws.DefaultJwsService import at.asitplus.wallet.lib.jws.DefaultVerifierJwsService +import at.asitplus.wallet.lib.oidvci.OAuth2Exception import at.asitplus.wallet.lib.oidvci.decodeFromPostBody import at.asitplus.wallet.lib.oidvci.decodeFromUrlQuery import at.asitplus.wallet.lib.oidvci.encodeToParameters import at.asitplus.wallet.lib.oidvci.formUrlEncode import com.benasher44.uuid.uuid4 -import io.github.aakira.napier.Napier +import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.booleans.shouldBeTrue import io.kotest.matchers.collections.shouldNotBeEmpty @@ -42,6 +44,7 @@ import io.ktor.http.Url import io.ktor.utils.io.ByteReadChannel import kotlinx.coroutines.runBlocking +@Suppress("unused") class OidcSiopProtocolTest : FreeSpec({ lateinit var relyingPartyUrl: String @@ -402,6 +405,367 @@ class OidcSiopProtocolTest : FreeSpec({ it.vc.credentialSubject.shouldBeInstanceOf() } } + + "test support for format holder specification" - { + "test support for mso credential request" - { + "if available despite others" { + runBlocking { + holderAgent.storeCredentials( + IssuerAgent.newDefaultInstance( + DefaultCryptoService(), + dataProvider = DummyCredentialDataProvider(), + ).issueCredential( + subjectPublicKey = holderCryptoService.publicKey, + attributeTypes = listOf(ConstantIndex.AtomicAttribute2023.vcType), + representation = ConstantIndex.CredentialRepresentation.ISO_MDOC, + ).toStoreCredentialInput() + ) + } + + verifierSiop = OidcSiopVerifier.newInstance( + verifier = verifierAgent, + cryptoService = verifierCryptoService, + relyingPartyUrl = relyingPartyUrl, + ) + + val authnRequest = verifierSiop.createAuthnRequest( + credentialScheme = ConstantIndex.AtomicAttribute2023, + representation = ConstantIndex.CredentialRepresentation.ISO_MDOC, + ).let { request -> + request.copy( + presentationDefinition = request.presentationDefinition?.let { presentationDefinition -> + presentationDefinition.copy( + formats = FormatHolder( + // only support msoMdoc here + msoMdoc = presentationDefinition.formats?.msoMdoc + ), + inputDescriptors = presentationDefinition.inputDescriptors.map { inputDescriptor -> + inputDescriptor.copy( + format = null + ) + } + ) + }, + ) + } + + val authnResponse = + holderSiop.createAuthnResponse(authnRequest).getOrThrow() + authnResponse.shouldBeInstanceOf() + .also { println(it) } + + val validationResults = verifierSiop.validateAuthnResponse(authnResponse.url) + validationResults.shouldBeInstanceOf() + val result = validationResults.validationResults.first() + result.shouldBeInstanceOf() + } + "if not available despite others" { + verifierSiop = OidcSiopVerifier.newInstance( + verifier = verifierAgent, + cryptoService = verifierCryptoService, + relyingPartyUrl = relyingPartyUrl, + ) + + val authnRequest = verifierSiop.createAuthnRequest( + credentialScheme = ConstantIndex.AtomicAttribute2023, + representation = ConstantIndex.CredentialRepresentation.ISO_MDOC, + ).let { request -> + request.copy( + presentationDefinition = request.presentationDefinition?.let { presentationDefinition -> + presentationDefinition.copy( + formats = FormatHolder( + // only support msoMdoc here + msoMdoc = presentationDefinition.formats?.msoMdoc + ), + inputDescriptors = presentationDefinition.inputDescriptors.map { inputDescriptor -> + inputDescriptor.copy( + format = null + ) + } + ) + }, + ) + } + + shouldThrow { + holderSiop.createAuthnResponse(authnRequest).getOrThrow() + } + } + } + "test support for sd jwt credential request" - { + "if available despite others" { + runBlocking { + holderAgent.storeCredentials( + IssuerAgent.newDefaultInstance( + DefaultCryptoService(), + dataProvider = DummyCredentialDataProvider(), + ).issueCredential( + subjectPublicKey = holderCryptoService.publicKey, + attributeTypes = listOf(ConstantIndex.AtomicAttribute2023.vcType), + representation = ConstantIndex.CredentialRepresentation.SD_JWT, + ).toStoreCredentialInput() + ) + holderAgent.storeCredentials( + IssuerAgent.newDefaultInstance( + DefaultCryptoService(), + dataProvider = DummyCredentialDataProvider(), + ).issueCredential( + subjectPublicKey = holderCryptoService.publicKey, + attributeTypes = listOf(ConstantIndex.AtomicAttribute2023.vcType), + representation = ConstantIndex.CredentialRepresentation.ISO_MDOC, + ).toStoreCredentialInput() + ) + } + + verifierSiop = OidcSiopVerifier.newInstance( + verifier = verifierAgent, + cryptoService = verifierCryptoService, + relyingPartyUrl = relyingPartyUrl, + ) + + val authnRequest = verifierSiop.createAuthnRequest( + credentialScheme = ConstantIndex.AtomicAttribute2023, + representation = ConstantIndex.CredentialRepresentation.SD_JWT, + ).let { request -> + request.copy( + presentationDefinition = request.presentationDefinition?.let { presentationDefinition -> + presentationDefinition.copy( + formats = FormatHolder( + // only support SD_JWT here + jwtSd = presentationDefinition.formats?.jwtSd, + ), + inputDescriptors = presentationDefinition.inputDescriptors.map { inputDescriptor -> + inputDescriptor.copy( + format = null + ) + } + ) + }, + ) + } + + val authnResponse = + holderSiop.createAuthnResponse(authnRequest).getOrThrow() + authnResponse.shouldBeInstanceOf() + .also { println(it) } + + val validationResults = verifierSiop.validateAuthnResponse(authnResponse.url) + validationResults.shouldBeInstanceOf() + val result = validationResults.validationResults.first() + result.shouldBeInstanceOf() + } + "if not available despite others" { + runBlocking { + holderAgent.storeCredentials( + IssuerAgent.newDefaultInstance( + DefaultCryptoService(), + dataProvider = DummyCredentialDataProvider(), + ).issueCredential( + subjectPublicKey = holderCryptoService.publicKey, + attributeTypes = listOf(ConstantIndex.AtomicAttribute2023.vcType), + representation = ConstantIndex.CredentialRepresentation.ISO_MDOC, + ).toStoreCredentialInput() + ) + } + + verifierSiop = OidcSiopVerifier.newInstance( + verifier = verifierAgent, + cryptoService = verifierCryptoService, + relyingPartyUrl = relyingPartyUrl, + ) + + val authnRequest = verifierSiop.createAuthnRequest( + credentialScheme = ConstantIndex.AtomicAttribute2023, + representation = ConstantIndex.CredentialRepresentation.SD_JWT, + ).let { request -> + request.copy( + presentationDefinition = request.presentationDefinition?.let { presentationDefinition -> + presentationDefinition.copy( + formats = FormatHolder( + // only support SD_JWT here + jwtSd = presentationDefinition.formats?.jwtSd, + ), + inputDescriptors = presentationDefinition.inputDescriptors.map { inputDescriptor -> + inputDescriptor.copy( + format = null + ) + } + ) + }, + ) + } + + shouldThrow { + holderSiop.createAuthnResponse(authnRequest).getOrThrow() + } + } + } + + "test support for plain jwt credential request" - { + "if available despite others" { + runBlocking { + holderAgent.storeCredentials( + IssuerAgent.newDefaultInstance( + DefaultCryptoService(), + dataProvider = DummyCredentialDataProvider(), + ).issueCredential( + subjectPublicKey = holderCryptoService.publicKey, + attributeTypes = listOf(ConstantIndex.MobileDrivingLicence2023.vcType), + representation = ConstantIndex.CredentialRepresentation.SD_JWT, + ).toStoreCredentialInput() + ) + holderAgent.storeCredentials( + IssuerAgent.newDefaultInstance( + DefaultCryptoService(), + dataProvider = DummyCredentialDataProvider(), + ).issueCredential( + subjectPublicKey = holderCryptoService.publicKey, + attributeTypes = listOf(ConstantIndex.MobileDrivingLicence2023.vcType), + representation = ConstantIndex.CredentialRepresentation.ISO_MDOC, + ).toStoreCredentialInput() + ) + } + + verifierSiop = OidcSiopVerifier.newInstance( + verifier = verifierAgent, + cryptoService = verifierCryptoService, + relyingPartyUrl = relyingPartyUrl, + ) + + val authnRequest = verifierSiop.createAuthnRequest( + credentialScheme = ConstantIndex.MobileDrivingLicence2023 + ).let { request -> + request.copy( + clientMetadata = request.clientMetadata?.let { clientMetadata -> + clientMetadata.copy( + vpFormats = FormatHolder( + // only allow plain jwt + jwtVp = clientMetadata.vpFormats?.jwtVp + ) + ) + } + ) + } + + shouldThrow { + holderSiop.createAuthnResponse(authnRequest).getOrThrow() + } + } + "if not available despite others" { + runBlocking { + holderAgent.storeCredentials( + IssuerAgent.newDefaultInstance( + DefaultCryptoService(), + dataProvider = DummyCredentialDataProvider(), + ).issueCredential( + subjectPublicKey = holderCryptoService.publicKey, + attributeTypes = listOf(ConstantIndex.AtomicAttribute2023.vcType), + representation = ConstantIndex.CredentialRepresentation.SD_JWT, + ).toStoreCredentialInput() + ) + holderAgent.storeCredentials( + IssuerAgent.newDefaultInstance( + DefaultCryptoService(), + dataProvider = DummyCredentialDataProvider(), + ).issueCredential( + subjectPublicKey = holderCryptoService.publicKey, + attributeTypes = listOf(ConstantIndex.AtomicAttribute2023.vcType), + representation = ConstantIndex.CredentialRepresentation.ISO_MDOC, + ).toStoreCredentialInput() + ) + } + verifierSiop = OidcSiopVerifier.newInstance( + verifier = verifierAgent, + cryptoService = verifierCryptoService, + relyingPartyUrl = relyingPartyUrl, + ) + + val authnRequest = verifierSiop.createAuthnRequest( + credentialScheme = ConstantIndex.AtomicAttribute2023, + ).let { request -> + request.copy( + clientMetadata = request.clientMetadata?.let { clientMetadata -> + clientMetadata.copy( + vpFormats = FormatHolder( + // only allow plain jwt + jwtVp = clientMetadata.vpFormats?.jwtVp + ) + ) + } + ) + } + + val authnResponse = + holderSiop.createAuthnResponse(authnRequest).getOrThrow() + authnResponse.shouldBeInstanceOf() + .also { println(it) } + + val validationResults = verifierSiop.validateAuthnResponse(authnResponse.url) + validationResults.shouldBeInstanceOf() + val result = validationResults.validationResults.first() + result.shouldBeInstanceOf() + result.vp.verifiableCredentials.shouldNotBeEmpty() + result.vp.verifiableCredentials.forEach { + it.vc.credentialSubject.shouldBeInstanceOf() + } + } + } + } + + + "test support presentation of multiple credentials" { + runBlocking { + holderAgent.storeCredentials( + IssuerAgent.newDefaultInstance( + DefaultCryptoService(), + dataProvider = DummyCredentialDataProvider(), + ).issueCredential( + subjectPublicKey = holderCryptoService.publicKey, + attributeTypes = listOf(ConstantIndex.MobileDrivingLicence2023.vcType), + representation = ConstantIndex.CredentialRepresentation.ISO_MDOC, + ).toStoreCredentialInput() + ) + } + + verifierSiop = OidcSiopVerifier.newInstance( + verifier = verifierAgent, + cryptoService = verifierCryptoService, + relyingPartyUrl = relyingPartyUrl, + ) + + val authnRequest1 = verifierSiop.createAuthnRequest( + credentialScheme = ConstantIndex.AtomicAttribute2023, + ) + val authnRequest2 = verifierSiop.createAuthnRequest( + credentialScheme = ConstantIndex.MobileDrivingLicence2023, + representation = ConstantIndex.CredentialRepresentation.ISO_MDOC, + ) + val inputDescriptors2 = authnRequest2.presentationDefinition?.inputDescriptors ?: listOf() + + val authnRequest = authnRequest1.copy( + presentationDefinition = authnRequest1.presentationDefinition?.let { presentationDefinition -> + presentationDefinition.copy( + inputDescriptors = presentationDefinition.inputDescriptors + inputDescriptors2, + formats = FormatHolder( + jwt = presentationDefinition.formats?.jwt ?: authnRequest2.presentationDefinition?.formats?.jwt, + jwtVc = presentationDefinition.formats?.jwtVc ?: authnRequest2.presentationDefinition?.formats?.jwtVc, + jwtVp = presentationDefinition.formats?.jwtVp ?: authnRequest2.presentationDefinition?.formats?.jwtVp, + jwtSd = presentationDefinition.formats?.jwtSd ?: authnRequest2.presentationDefinition?.formats?.jwtSd, + msoMdoc = presentationDefinition.formats?.msoMdoc ?: authnRequest2.presentationDefinition?.formats?.msoMdoc, + ) + ) + } + ) + + val authnResponse = + holderSiop.createAuthnResponse(authnRequest).getOrThrow() + authnResponse.shouldBeInstanceOf() + .also { println(it) } + + val validationResults = verifierSiop.validateAuthnResponse(authnResponse.url) + validationResults.shouldBeInstanceOf() + validationResults.validationResults.size shouldBe 2 + } }) private suspend fun verifySecondProtocolRun( diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/HolderAgent.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/HolderAgent.kt index a22ea3dba..78659900b 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/HolderAgent.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/HolderAgent.kt @@ -11,7 +11,6 @@ import at.asitplus.wallet.lib.data.SelectiveDisclosureItem import at.asitplus.wallet.lib.data.VerifiableCredentialJws import at.asitplus.wallet.lib.data.VerifiableCredentialSdJwt import at.asitplus.wallet.lib.data.VerifiablePresentation -import at.asitplus.wallet.lib.data.VerifiablePresentationConstants import at.asitplus.wallet.lib.data.dif.ClaimFormatEnum import at.asitplus.wallet.lib.data.dif.InputDescriptor import at.asitplus.wallet.lib.data.dif.InputEvaluator @@ -273,6 +272,14 @@ class HolderAgent( true // no revocation check available for iso credentials } } + }?.sortedBy { + // prefer iso credentials and sd jwt credentials over plain vc credentials + // -> they support selective disclosure! + when(it) { + is SubjectCredentialStore.StoreEntry.Vc -> 2 + is SubjectCredentialStore.StoreEntry.SdJwt -> 1 + is SubjectCredentialStore.StoreEntry.Iso -> 1 + } } ?: return null .also { Napier.w("Got no credentials from subjectCredentialStore") } @@ -291,7 +298,7 @@ class HolderAgent( is SubjectCredentialStore.StoreEntry.SdJwt -> formatHolder.jwtSd != null is SubjectCredentialStore.StoreEntry.Iso -> formatHolder.msoMdoc != null } - } ?: true // assume credential format to be supported if no format holder is specified + } ?: true // assume credential format to be supported by the verifier if no format holder is specified }.firstNotNullOfOrNull { credential -> StandardInputEvaluator().evaluateMatch( inputDescriptor = inputDescriptor,