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

Implement createPresentationFromCredentials #151

Merged
merged 13 commits into from
Dec 11, 2023
3 changes: 3 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ subprojects {

tasks.test {
useJUnitPlatform()
reports {
junitXml
}
testLogging {
events("passed", "skipped", "failed", "standardOut", "standardError")
exceptionFormat = TestExceptionFormat.FULL
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ package web5.sdk.credentials

import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.networknt.schema.JsonSchema
import com.nfeld.jsonpathkt.JsonPath
import com.nfeld.jsonpathkt.extension.read
import com.nimbusds.jwt.JWTParser
import com.nimbusds.jwt.SignedJWT
import web5.sdk.credentials.model.DescriptorMap
import web5.sdk.credentials.model.InputDescriptorV2
import web5.sdk.credentials.model.PresentationDefinitionV2
import web5.sdk.credentials.model.PresentationSubmission
import java.util.UUID

/**
* The `PresentationExchange` object provides functions for working with Verifiable Credentials
Expand Down Expand Up @@ -65,6 +69,52 @@ public object PresentationExchange {
}
}

/**
* Creates a Presentation Submission against a list of Verifiable Credentials (VCs) against a specified
nitro-neal marked this conversation as resolved.
Show resolved Hide resolved
* Presentation Definition.
*
*
* @param vcJwts Iterable of VCs in JWT format to validate.
* @param presentationDefinition The Presentation Definition V2 object against which VCs are validated.
* @return A PresentationSubmission object.
* @throws UnsupportedOperationException if the presentation definition contains submission requirements.
* @throws IllegalStateException if no VC corresponds to an input descriptor or if a VC's index is not found.
* @throws PresentationExchangeError If the number of input descriptors matched is less than required.
*/
public fun createPresentationFromCredentials(
vcJwts: Iterable<String>,
presentationDefinition: PresentationDefinitionV2
): PresentationSubmission {

satisfiesPresentationDefinition(vcJwts, presentationDefinition)

val inputDescriptorToVcMap = mapInputDescriptorsToVCs(vcJwts, presentationDefinition)
val vcJwtToIndexMap = vcJwts.withIndex().associate { (index, vcJwt) -> vcJwt to index }

val descriptorMapList = mutableListOf<DescriptorMap>()
for ((inputDescriptor, vcList) in inputDescriptorToVcMap) {
// Even if multiple VCs satisfy the input descriptor we use the first
val vcJwt = vcList.firstOrNull()
checkNotNull(vcJwt) { "Illegal state: no vc corresponds to input descriptor" }

val vcIndex = vcJwtToIndexMap[vcJwt]
checkNotNull(vcIndex) { "Illegal state: vcJwt index not found" }

descriptorMapList.add(
DescriptorMap(
id = inputDescriptor.id,
path = "$.verifiableCredential[$vcIndex]",
format = "jwt_vc"
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it make sense to use Format as the type here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

since the input to the function is vcJwts: Iterable, we know that he descriptor map will be jwt_vc mapping

))
}

return PresentationSubmission(
id = UUID.randomUUID().toString(),
definitionId = presentationDefinition.id,
descriptorMap = descriptorMapList
)
}

private fun mapInputDescriptorsToVCs(
vcJwtList: Iterable<String>,
presentationDefinition: PresentationDefinitionV2
Expand Down Expand Up @@ -101,7 +151,7 @@ public object PresentationExchange {
?: throw PresentationExchangeError("Failed to parse VC payload as JSON.")

// If the Input Descriptor has constraints and fields defined, evaluate them.
inputDescriptor.constraints?.fields?.let { fields ->
inputDescriptor.constraints.fields?.let { fields ->
val requiredFields = fields.filter { field -> field.optional != true }

for (field in requiredFields) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package web5.sdk.credentials
package web5.sdk.credentials.model

import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonProperty
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package web5.sdk.credentials.model

import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonProperty

/**
* Represents a presentation submission object.
*
* @see [Presentation Submission](https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-submission)
*/
public data class PresentationSubmission(
nitro-neal marked this conversation as resolved.
Show resolved Hide resolved
val id: String,
@JsonProperty("definition_id")
val definitionId: String,
@JsonProperty("descriptor_map")
val descriptorMap: List<DescriptorMap>
)

/**
* Represents descriptor map for a presentation submission.
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public data class DescriptorMap(
nitro-neal marked this conversation as resolved.
Show resolved Hide resolved
val id: String,
val format: String,
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it make sense to use Format as the type here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the format is a string int he DescriptorMap where it is an object in the inputDescriptor - https://identity.foundation/presentation-exchange/#presentation-submission

val path: String,
@JsonProperty("path_nested")
val pathNested: DescriptorMap? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import org.erdtman.jcs.JsonCanonicalizer
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertDoesNotThrow
import web5.sdk.credentials.model.ConstraintsV2
import web5.sdk.credentials.model.FieldV2
import web5.sdk.credentials.model.InputDescriptorV2
import web5.sdk.credentials.model.PresentationDefinitionV2
import java.io.File

class PresentationDefinitionTest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.assertDoesNotThrow
import web5.sdk.credentials.model.PresentationDefinitionV2
import web5.sdk.crypto.InMemoryKeyManager
import web5.sdk.dids.methods.key.DidKey
import java.io.File
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue

data class DateOfBirth(val dateOfBirth: String)
data class Address(val address: String)
Expand Down Expand Up @@ -297,4 +301,102 @@ class PresentationExchangeTest {
}.messageContains("Missing input descriptors: The presentation definition requires")
}
}

@Nested
inner class CreatePresentationFromCredentials {
@Test
fun `creates valid submission when VC satisfies tbdex PD`() {
val pd = jsonMapper.readValue(
readPd("src/test/resources/pd_sanctions.json"),
PresentationDefinitionV2::class.java
)

val presentationSubmission = PresentationExchange.createPresentationFromCredentials(listOf(sanctionsVcJwt), pd)

assertNotNull(presentationSubmission.id)
assertEquals(pd.id, presentationSubmission.definitionId)

assertEquals(1, presentationSubmission.descriptorMap.size)
assertNotNull(presentationSubmission.descriptorMap[0].id)
assertEquals("jwt_vc", presentationSubmission.descriptorMap[0].format)
assertEquals("$.verifiableCredential[0]", presentationSubmission.descriptorMap[0].path)
Comment on lines +344 to +350
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you think about asserting against a presentation submission loaded from a json file? Would be nice for test-vectors, I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have a test vector PR tee'd up after this, so we can hav ea clean PR that shows how to add test vector tests

}

@Test
fun `creates valid submission when VC satisfies PD with no filter dob filed constraint and extra VC`() {
val pd = jsonMapper.readValue(
readPd("src/test/resources/pd_path_no_filter_dob.json"),
PresentationDefinitionV2::class.java
)

val vc1 = VerifiableCredential.create(
type = "DateOfBirth",
issuer = issuerDid.uri,
subject = holderDid.uri,
data = DateOfBirth(dateOfBirth = "Data1")
)
val vcJwt1 = vc1.sign(issuerDid)

val vc2 = VerifiableCredential.create(
type = "Address",
issuer = issuerDid.uri,
subject = holderDid.uri,
data = Address("abc street 123")
)
val vcJwt2 = vc2.sign(issuerDid)

val presentationSubmission = PresentationExchange.createPresentationFromCredentials(listOf(vcJwt2, vcJwt1), pd)

assertEquals(1, presentationSubmission.descriptorMap.size)
assertEquals("$.verifiableCredential[1]", presentationSubmission.descriptorMap[0].path)
}

@Test
fun `throws when VC does not satisfy sanctions requirements`() {
val pd = jsonMapper.readValue(
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the plan to read this from the hosted vectors in a follow up PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup we will be doing the same stuff we do here in this PR decentralized-identity/web5-js#344

readPd("src/test/resources/pd_sanctions.json"),
PresentationDefinitionV2::class.java
)
val vc = VerifiableCredential.create(
type = "StreetCred",
issuer = issuerDid.uri,
subject = holderDid.uri,
data = StreetCredibility(localRespect = "high", legit = true)
)
val vcJwt = vc.sign(issuerDid)

assertFailure {
PresentationExchange.createPresentationFromCredentials(listOf(vcJwt), pd)
}.messageContains("Missing input descriptors: The presentation definition requires")
}

@Test
fun `creates valid submission when VC two vcs satisfy the same input descriptor`() {
val pd = jsonMapper.readValue(
readPd("src/test/resources/pd_path_no_filter_dob.json"),
PresentationDefinitionV2::class.java
)

val vc1 = VerifiableCredential.create(
type = "DateOfBirth",
issuer = issuerDid.uri,
subject = holderDid.uri,
data = DateOfBirth(dateOfBirth = "11/11/2011")
)
val vcJwt1 = vc1.sign(issuerDid)

val vc2 = VerifiableCredential.create(
type = "DateOfBirth",
issuer = issuerDid.uri,
subject = holderDid.uri,
data = DateOfBirth(dateOfBirth = "12/12/2012")
)
val vcJwt2 = vc2.sign(issuerDid)

val presentationSubmission = PresentationExchange.createPresentationFromCredentials(listOf(vcJwt2, vcJwt1), pd)

assertEquals(1, presentationSubmission.descriptorMap.size)
assertEquals("$.verifiableCredential[0]", presentationSubmission.descriptorMap[0].path)
}
}
}