diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/PresentationExchange.kt b/credentials/src/main/kotlin/web5/sdk/credentials/PresentationExchange.kt index 7ec9b1465..0954adc5b 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/PresentationExchange.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/PresentationExchange.kt @@ -12,6 +12,7 @@ import web5.sdk.credentials.model.InputDescriptorV2 import web5.sdk.credentials.model.PresentationDefinitionV2 import web5.sdk.credentials.model.PresentationDefinitionV2Validator import web5.sdk.credentials.model.PresentationSubmission +import web5.sdk.credentials.model.PresentationSubmissionValidator import java.util.UUID /** @@ -136,6 +137,26 @@ public object PresentationExchange { PresentationDefinitionV2Validator.validate(presentationDefinition) } + /** + * Validates whether an object is usable as a presentation submission or not. + * + * Model as specified in https://identity.foundation/presentation-exchange/#presentation-submission. + * + * The checks are as follows: + * 1. Ensures that the presentation submission's id is not empty. + * 2. Validates that the definitionId is not empty. + * 3. Validates descriptorMap is a non-empty list. + * 4. Check for unique inputDescriptor ids at top level + * 5. Verifies the input descriptor mapping ids are the same on all levels of nesting. + * 6. Ensures that the path is valid across all levels of nesting + * + * Throws an [PexValidationException] if the provided object does not conform to the Presentation Definition + */ + @Throws(PexValidationException::class) + public fun validateSubmission(presentationSubmission: PresentationSubmission) { + PresentationSubmissionValidator.validate(presentationSubmission) + } + private fun mapInputDescriptorsToVCs( vcJwtList: Iterable, presentationDefinition: PresentationDefinitionV2 diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/model/Validator.kt b/credentials/src/main/kotlin/web5/sdk/credentials/model/Validator.kt index 14ac14c4d..25156bc25 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/model/Validator.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/model/Validator.kt @@ -149,3 +149,77 @@ public object FieldV2Validator { } } +/** + * PresentationSubmission Validator. + **/ +public object PresentationSubmissionValidator { + + @Throws(PexValidationException::class) + + /** + * Validates a PresentationSubmission. + * + * This method performs several checks to ensure the integrity of the presentation submission model object: + * 1. Ensures that the presentation submission's id is not empty. + * 2. Validates that the definitionId is not empty. + * 3. Validates descriptorMap is a non-empty list. + * 4. Check for unique inputDescriptor ids at top level + * 5. Verifies the input descriptor mapping ids are the same on all levels of nesting. + * 6. Ensures that the path is valid across all levels of nesting + * @throws PexValidationException if the PresentationDefinitionV2 is not valid. + */ + public fun validate(presentationSubmission: PresentationSubmission) { + if (presentationSubmission.id.isEmpty()) { + throw PexValidationException("PresentationSubmission id must not be empty") + } + + if (presentationSubmission.definitionId.isEmpty()) { + throw PexValidationException("PresentationSubmission definitionId must not be empty") + } + + if (presentationSubmission.descriptorMap.isEmpty()) { + throw PexValidationException("PresentationSubmission descriptorMap should be a non-empty list") + } + + // Check for unique inputDescriptor ids + val ids = presentationSubmission.descriptorMap.map { it.id } + if (ids.size != ids.toSet().size) { + throw PexValidationException("All descriptorMap top level ids must be unique") + } + + validateDescriptorMap(presentationSubmission.descriptorMap) + } + + private fun validateDescriptorMap(descriptorMap: List) { + descriptorMap.forEach { descriptor -> + validateDescriptor(descriptor, descriptor.id) + } + } + private fun validateDescriptor(descriptor: InputDescriptorMapping, id: String?) { + if (descriptor.id.isEmpty()) { + throw PexValidationException("Descriptor id should not be empty") + } + + if (descriptor.path.isEmpty()) { + throw PexValidationException("Descriptor path should not be empty") + } + + if (descriptor.format.isEmpty()) { + throw PexValidationException("Descriptor format should not be empty") + } + + if (descriptor.id != id) { + throw PexValidationException("Each descriptor should have one id in it, on all levels") + } + + if (runCatching { JsonPath(descriptor.path) }.isFailure) { + throw PexValidationException("Each descriptor should have a valid path id") + } + + descriptor.pathNested?.let { nestedDescriptor -> + validateDescriptor(nestedDescriptor, id) + } + } +} + + diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/PresentationExchangeTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/PresentationExchangeTest.kt index b1e51fdb7..1783f318e 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/PresentationExchangeTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/PresentationExchangeTest.kt @@ -11,6 +11,7 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.api.assertThrows import web5.sdk.credentials.model.PresentationDefinitionV2 +import web5.sdk.credentials.model.PresentationSubmission import web5.sdk.crypto.InMemoryKeyManager import web5.sdk.dids.methods.key.DidKey import web5.sdk.testing.TestVectors @@ -621,4 +622,27 @@ class Web5TestVectorsPresentationExchange { } } } + + data class ValidateSubmissionTestInput( + val presentationSubmission: PresentationSubmission, + val errors: Boolean + ) + @Test + fun validate_submission() { + val typeRef = object : TypeReference>() {} + val testVectors = + mapper.readValue(File("../web5-spec/test-vectors/presentation_exchange/validate_submission.json"), typeRef) + + testVectors.vectors.filterNot { it.errors ?: false }.forEach { vector -> + assertDoesNotThrow { + PresentationExchange.validateSubmission(vector.input.presentationSubmission) + } + } + + testVectors.vectors.filter { it.errors ?: false }.forEach { vector -> + assertFails { + PresentationExchange.validateSubmission(vector.input.presentationSubmission) + } + } + } } \ No newline at end of file diff --git a/web5-spec b/web5-spec index 2be5dd31c..79533b0e2 160000 --- a/web5-spec +++ b/web5-spec @@ -1 +1 @@ -Subproject commit 2be5dd31cdabec14d7417a2b9d0dc25254da643b +Subproject commit 79533b0e244eab192e7ae7e3d731b04f80c3932b