Skip to content
This repository has been archived by the owner on Jan 9, 2024. It is now read-only.

Commit

Permalink
Make VCs for presentation selectable
Browse files Browse the repository at this point in the history
  • Loading branch information
waltkb committed Nov 22, 2023
1 parent ad63fdf commit 7213a6f
Show file tree
Hide file tree
Showing 11 changed files with 309 additions and 185 deletions.
45 changes: 45 additions & 0 deletions src/main/kotlin/id/walt/db/models/WalletCredentials.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package id.walt.db.models

import id.walt.crypto.utils.JwsUtils.decodeJws
import kotlinx.datetime.Instant
import kotlinx.datetime.toKotlinInstant
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.jsonObject
import kotlinx.uuid.UUID
import kotlinx.uuid.toKotlinUUID
import org.jetbrains.exposed.sql.ResultRow
Expand All @@ -28,7 +33,47 @@ data class WalletCredential(
val document: String,
val disclosures: String?,
val addedOn: Instant,

val parsedDocument: JsonObject? = parseDocument(document, id)
) {

companion object {
fun parseDocument(document: String, id: String) =
runCatching {
when {
document.startsWith("{") -> Json.parseToJsonElement(document).jsonObject
document.startsWith("ey") -> document.decodeJws().payload
.run { jsonObject["vc"]?.jsonObject ?: jsonObject }

else -> throw IllegalArgumentException("Unknown credential format")
}.toMutableMap().also {
it.putIfAbsent("id", JsonPrimitive(id))
}.let {
JsonObject(it)
}
}.onFailure { it.printStackTrace() }
.getOrNull()
}

/*
val parsedDocument: JsonObject?
get() =
runCatching {
when {
document.startsWith("{") -> Json.parseToJsonElement(document).jsonObject
document.startsWith("ey") -> document.decodeJws().payload
.run { jsonObject["vc"]?.jsonObject ?: jsonObject }
else -> throw IllegalArgumentException("Unknown credential format")
}.toMutableMap().also {
it.putIfAbsent("id", JsonPrimitive(id))
}.let {
JsonObject(it)
}
}.onFailure { it.printStackTrace() }
.getOrNull()*/


constructor(result: ResultRow) : this(
wallet = result[WalletCredentials.wallet].value.toKotlinUUID(),
id = result[WalletCredentials.id],
Expand Down
81 changes: 57 additions & 24 deletions src/main/kotlin/id/walt/service/SSIKit2WalletService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import id.walt.did.helpers.WaltidServices
import id.walt.did.utils.EnumUtils.enumValueIgnoreCase
import id.walt.oid4vc.data.GrantType
import id.walt.oid4vc.data.OpenIDProviderMetadata
import id.walt.oid4vc.data.dif.PresentationDefinition
import id.walt.oid4vc.providers.OpenIDClientConfig
import id.walt.oid4vc.providers.SIOPProviderConfig
import id.walt.oid4vc.requests.*
Expand Down Expand Up @@ -68,24 +69,7 @@ class SSIKit2WalletService(accountId: UUID, walletId: UUID) : WalletService(acco
}
}

override fun listCredentials(): List<Credential> = CredentialsService.list(walletId).mapNotNull {
val credentialId = it.id
runCatching {
val cred = it.document
val parsedCred = when {
cred.startsWith("{") -> Json.parseToJsonElement(cred).jsonObject
cred.startsWith("ey") -> cred.decodeJws().payload
.run { jsonObject["vc"]?.jsonObject ?: jsonObject }

else -> throw IllegalArgumentException("Unknown credential format")
}
Credential(parsedCred, cred)
}.onFailure { it.printStackTrace() }.getOrNull()?.let { cred ->
Credential(JsonObject(cred.parsedCredential.toMutableMap().also {
it.putIfAbsent("id", JsonPrimitive(credentialId))
}), cred.rawCredential)
}
}
override fun listCredentials(): List<WalletCredential> = CredentialsService.list(walletId)

override suspend fun listRawCredentials(): List<String> = CredentialsService.list(walletId).map {
it.document
Expand All @@ -97,6 +81,44 @@ class SSIKit2WalletService(accountId: UUID, walletId: UUID) : WalletService(acco
CredentialsService.get(walletId, credentialId)
?: throw IllegalArgumentException("WalletCredential not found for credentialId: $credentialId")

override fun matchCredentialsByPresentationDefinition(presentationDefinition: PresentationDefinition): List<WalletCredential> {
val credentialList = listCredentials()

println("WalletCredential list is: ${credentialList.map { it.parsedDocument?.get("type")!!.jsonArray }}")

data class TypeFilter(val path: String, val type: String? = null, val pattern: String)

val filters = presentationDefinition.inputDescriptors.mapNotNull {
it.constraints?.fields?.map {
val path = it.path.first().removePrefix("$.")
val filterType = it.filter?.get("type")?.jsonPrimitive?.content
val filterPattern = it.filter?.get("pattern")?.jsonPrimitive?.content
?: throw IllegalArgumentException("No filter pattern in presentation definition constraint")

TypeFilter(path, filterType, filterPattern)
}
}
println("Using filters: $filters")

val matchedCredentials = credentialList.filter { credential ->
filters.any { fields ->
fields.all { typeFilter ->
val credField = credential.parsedDocument!![typeFilter.path] ?: return@all false

when (credField) {
is JsonPrimitive -> credField.jsonPrimitive.content == typeFilter.pattern
is JsonArray -> credField.jsonArray.last().jsonPrimitive.content == typeFilter.pattern
else -> false
}
}
}
}
println("Matched credentials: $matchedCredentials")


return matchedCredentials
}

private fun getQueryParams(url: String): Map<String, MutableList<String>> {
val params: MutableMap<String, MutableList<String>> = HashMap()
val urlParts = url.split("\\?".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
Expand Down Expand Up @@ -155,26 +177,32 @@ class SSIKit2WalletService(accountId: UUID, walletId: UUID) : WalletService(acco
val redirectUri: String?
) : IllegalArgumentException(message)


/**
* @return redirect uri
*/
override suspend fun usePresentationRequest(request: String, did: String): Result<String?> {
override suspend fun usePresentationRequest(request: String, did: String, selectedCredentialIds: List<String>): Result<String?> {
val credentialWallet = getCredentialWallet(did)

val authReq = AuthorizationRequest.fromHttpQueryString(Url(request).encodedQuery)
println("Auth req: $authReq")
val walletSession = credentialWallet.initializeAuthorization(authReq, 60.seconds)
println("Resolved presentation definition: ${walletSession.authorizationRequest!!.presentationDefinition!!.toJSONString()}")
val tokenResponse = credentialWallet.processImplicitFlowAuthorization(walletSession.authorizationRequest!!)
val resp = ktorClient.submitForm(walletSession.authorizationRequest!!.responseUri!!,

println("USING PRESENTATION REQUEST, SELECTED CREDENTIALS: $selectedCredentialIds")

val presentationSession = credentialWallet.initializeAuthorization(authReq, 60.seconds)
.copy(selectedCredentialIds = selectedCredentialIds.toSet())
println("Resolved presentation definition: ${presentationSession.authorizationRequest!!.presentationDefinition!!.toJSONString()}")

val tokenResponse = credentialWallet.processImplicitFlowAuthorization(presentationSession.authorizationRequest!!)
val resp = ktorClient.submitForm(presentationSession.authorizationRequest!!.responseUri!!,
parameters {
tokenResponse.toHttpParameters().forEach { entry ->
entry.value.forEach { append(entry.key, it) }
}
})
val httpResponseBody = runCatching { resp.bodyAsText() }.getOrNull()
val isResponseRedirectUrl =
httpResponseBody != null && httpResponseBody.take(8).lowercase().let {
httpResponseBody != null && httpResponseBody.take(10).lowercase().let {
@Suppress("HttpUrlsUsage")
it.startsWith("http://") || it.startsWith("https://")
}
Expand Down Expand Up @@ -545,6 +573,11 @@ class SSIKit2WalletService(accountId: UUID, walletId: UUID) : WalletService(acco
override suspend fun getIssuer(name: String): IssuerDataTransferObject =
IssuersService.get(walletId, name) ?: throw IllegalArgumentException("Issuer: $name not found for: $walletId")

override fun getCredentialsByIds(credentialIds: List<String>): List<WalletCredential> {
// todo: select by SQL
return listCredentials().filter { it.id in credentialIds }
}

private fun getDidOptions(method: String, args: Map<String, JsonPrimitive>) = when (method.lowercase()) {
"key" -> DidKeyCreateOptions(
args["key"]?.let { enumValueIgnoreCase<KeyType>(it.content) } ?: KeyType.Ed25519,
Expand Down
15 changes: 12 additions & 3 deletions src/main/kotlin/id/walt/service/WalletKitWalletService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package id.walt.service
import id.walt.config.ConfigManager
import id.walt.config.RemoteWalletConfig
import id.walt.db.models.*
import id.walt.oid4vc.data.dif.PresentationDefinition
import id.walt.service.dids.DidsService
import id.walt.service.dto.LinkedWalletDataTransferObject
import id.walt.service.dto.WalletDataTransferObject
Expand Down Expand Up @@ -124,7 +125,7 @@ class WalletKitWalletService(accountId: UUID, walletId: UUID) : WalletService(ac

override fun listCredentials() = runBlocking {
authenticatedJsonGet("/api/wallet/credentials/list")
.body<JsonObject>()["list"]!!.jsonArray.toList().map { Credential(it.jsonObject, it.toString()) }
.body<JsonObject>()["list"]!!.jsonArray.toList().map { WalletCredential(walletId, it.jsonObject["id"]!!.jsonPrimitive.content, it.toString(), null, Instant.DISTANT_PAST) }
}

override suspend fun listRawCredentials(): List<String> {
Expand All @@ -139,10 +140,14 @@ class WalletKitWalletService(accountId: UUID, walletId: UUID) : WalletService(ac
override suspend fun getCredential(credentialId: String) = WalletCredential(
wallet = walletId,
id = credentialId,
document = listCredentials().first { it.parsedCredential["id"]?.jsonPrimitive?.content == credentialId }.toString(),
document = listCredentials().first { it.parsedDocument?.get("id")?.jsonPrimitive?.content == credentialId }.toString(),
disclosures = null,
addedOn = Instant.DISTANT_PAST
)

override fun matchCredentialsByPresentationDefinition(presentationDefinition: PresentationDefinition): List<WalletCredential> {
TODO("Not yet implemented")
}
/*prettyJson.encodeToString(*/
//)
/* override suspend fun getCredential(credentialId: String): String =
Expand Down Expand Up @@ -195,7 +200,7 @@ class WalletKitWalletService(accountId: UUID, walletId: UUID) : WalletService(ac
val state: String?
)

override suspend fun usePresentationRequest(request: String, did: String): Result<String?> {
override suspend fun usePresentationRequest(request: String, did: String, selectedCredentialIds: List<String>): Result<String?> {
val decoded = URLDecoder.decode(request, Charset.defaultCharset())
val queryParams = getQueryParams(decoded)
val redirectUri = queryParams["redirect_uri"]?.first()
Expand Down Expand Up @@ -371,5 +376,9 @@ class WalletKitWalletService(accountId: UUID, walletId: UUID) : WalletService(ac
override suspend fun getIssuer(name: String): IssuerDataTransferObject {
TODO("Not yet implemented")
}

override fun getCredentialsByIds(credentialIds: List<String>): List<WalletCredential> {
TODO("Not yet implemented")
}
}

9 changes: 6 additions & 3 deletions src/main/kotlin/id/walt/service/WalletService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package id.walt.service
import id.walt.db.models.WalletCredential
import id.walt.db.models.WalletDid
import id.walt.db.models.WalletOperationHistory
import id.walt.oid4vc.data.dif.PresentationDefinition
import id.walt.service.dto.LinkedWalletDataTransferObject
import id.walt.service.dto.WalletDataTransferObject
import id.walt.service.issuers.IssuerDataTransferObject
Expand All @@ -13,13 +14,15 @@ import kotlinx.uuid.UUID
abstract class WalletService(val accountId: UUID, val walletId: UUID) {

// WalletCredentials
abstract fun listCredentials(): List<Credential>
abstract fun listCredentials(): List<WalletCredential>
abstract suspend fun listRawCredentials(): List<String>
abstract suspend fun deleteCredential(id: String): Boolean
abstract suspend fun getCredential(credentialId: String): WalletCredential

abstract fun matchCredentialsByPresentationDefinition(presentationDefinition: PresentationDefinition): List<WalletCredential>

// SIOP
abstract suspend fun usePresentationRequest(request: String, did: String): Result<String?>
abstract suspend fun usePresentationRequest(request: String, did: String, selectedCredentialIds: List<String>): Result<String?>
abstract suspend fun resolvePresentationRequest(request: String): String
abstract suspend fun useOfferRequest(offer: String, did: String)

Expand Down Expand Up @@ -52,9 +55,9 @@ abstract class WalletService(val accountId: UUID, val walletId: UUID) {
// Issuers TODO: move each such component to use-case
abstract suspend fun listIssuers(): List<IssuerDataTransferObject>
abstract suspend fun getIssuer(name: String): IssuerDataTransferObject
abstract fun getCredentialsByIds(credentialIds: List<String>): List<WalletCredential>


// TODO: Push
// TODO: SIOP mid steps

}
Loading

0 comments on commit 7213a6f

Please sign in to comment.