Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ fun InputDescriptor.extractConsentData(): Triple<CredentialRepresentation, Const
SD_JWT -> vctConstraint()?.filter?.referenceValues()
ISO_MDOC -> listOf(this.id)
} ?: throw Throwable("Missing Pattern")
check(credentialIdentifiers.isNotEmpty()) {
"Presentation definition input descriptor '$id' does not declare any credential identifier"
}

// TODO: How to properly handle the case with multiple applicable schemes?
val scheme = AttributeIndex.schemeSet.firstOrNull {
Expand Down Expand Up @@ -392,4 +395,4 @@ fun ConstantIndex.CredentialRepresentation.getMetadataLocalization(
}

is MdocClaimReference -> null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ class PresentationService(
val presentation =
presentationResult.getOrThrow() as PresentationResponseParameters.PresentationExchangeParameters

val deviceResponse = when (val firstResult = presentation.presentationResults[0]) {
val deviceResponse = when (val firstResult = presentation.presentationResults.firstOrNull()
?: throw PresentationException(IllegalStateException("Presentation did not return any device response"))) {
is CreatePresentationResult.DeviceResponse -> firstResult.deviceResponse
else -> throw PresentationException(IllegalStateException("Must be a device response"))
}
Expand Down Expand Up @@ -165,7 +166,8 @@ class PresentationService(
val presentation =
presentationResult.getOrThrow() as PresentationResponseParameters.PresentationExchangeParameters

val deviceResponse = when (val firstResult = presentation.presentationResults[0]) {
val deviceResponse = when (val firstResult = presentation.presentationResults.firstOrNull()
?: throw PresentationException(IllegalStateException("Presentation did not return any device response"))) {
is CreatePresentationResult.DeviceResponse -> coseCompliantSerializer.encodeToByteArray(
firstResult.deviceResponse
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ui.presentation

import at.asitplus.wallet.lib.agent.validation.CredentialFreshnessSummary
import at.asitplus.wallet.lib.openid.CredentialMatchingResult
import at.asitplus.wallet.lib.openid.PresentationExchangeMatchingResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
Expand All @@ -20,7 +21,11 @@ fun <Credential : Any> CredentialMatchingResult<Credential>.toCredentialSelectio
scope: CoroutineScope,
checkCredentialFreshness: suspend (Credential) -> CredentialFreshnessSummary,
) = CredentialSelectionProvider(
queryMatchingResult = this,
queryMatchingResult = this.also {
if (it is PresentationExchangeMatchingResult) {
validatePresentationExchangeInputDescriptorMatches(it.matchingResult.inputDescriptorMatches)
Comment on lines +25 to +26
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Route empty PE matches through the no-credential state

When an OpenID4VP/DCAPI Presentation Exchange request has at least one descriptor with no local match, this check now throws during toCredentialSelectionProvider(). In the graph-based flows (DefaultPresentationGraphViewModel/DCAPIPresentationGraphViewModel), that exception is converted to UiStateError, and PresentationGraphView immediately forwards it to onError, which WalletNavigation handles by popping back to HomeScreenRoute. The result is that a normal “no matching credential” case is now surfaced as a fatal error instead of the existing no-match UX used in AuthenticationViewModel.onConsent, so users lose the explanatory screen and are bounced out of the flow.

Useful? React with 👍 / 👎.

}
},
credentialFreshnessProviders = this.matchingResult.credentials.map {
flow {
emit(CredentialFreshnessValidationStateUiModel.Loading)
Expand All @@ -36,3 +41,16 @@ fun <Credential : Any> CredentialMatchingResult<Credential>.toCredentialSelectio
)
}
)

internal fun <Credential : Any, Match : Any> validatePresentationExchangeInputDescriptorMatches(
inputDescriptorMatches: Map<String, Map<Credential, Match>>,
) {
val missingDescriptorIds = inputDescriptorMatches
.filterValues { it.isEmpty() }
.keys
check(missingDescriptorIds.isEmpty()) {
"Presentation definition input descriptor(s) ${
missingDescriptorIds.joinToString(", ")
} did not match any stored credential"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ class LoadCredentialViewModel(
val onClickLogo: () -> Unit,
val onClickSettings: () -> Unit,
) {
init {
check(credentialIdentifiers.isNotEmpty()) {
"Issuer '$hostString' did not provide any credential configuration that can be loaded"
}
}

companion object {
suspend fun init(
Expand Down Expand Up @@ -88,4 +93,4 @@ class LoadCredentialViewModel(
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ class AuthenticationSelectionPresentationExchangeViewModel(
requests.forEach {
attributeSelection[it.key] = mutableStateMapOf()
val matchingCredentials = it.value
val defaultCredential = matchingCredentials.keys.first()
val defaultCredential = matchingCredentials.keys.firstOrNull()
?: throw IllegalStateException(
"Presentation definition input descriptor '${it.key}' did not match any stored credential"
)
credentialSelection[it.key] = mutableStateOf(defaultCredential)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package ui.presentation

import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

class CredentialSelectionProviderTest {
@Test
fun rejectsPresentationExchangeDescriptorsWithoutMatches() {
val error = assertFailsWith<IllegalStateException> {
validatePresentationExchangeInputDescriptorMatches(
mapOf(
"matched" to mapOf("credential" to Unit),
"missing" to emptyMap(),
)
)
}

assertEquals(
"Presentation definition input descriptor(s) missing did not match any stored credential",
error.message,
)
}

@Test
fun acceptsPresentationExchangeDescriptorsWithMatches() {
validatePresentationExchangeInputDescriptorMatches(
mapOf(
"matched" to mapOf("credential" to Unit),
)
)
}
}
Loading