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

Implementing satisfiesPresentationDefinition #143

Merged
merged 6 commits into from
Nov 29, 2023
Merged
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
@@ -1,10 +1,11 @@
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.jose.Payload
import com.nimbusds.jwt.JWTParser
import com.nimbusds.jwt.SignedJWT

Expand All @@ -31,74 +32,132 @@
}

/**
* Validates if a Verifiable Credential JWT satisfies a Presentation Definition.
* Validates a list of Verifiable Credentials (VCs) against a specified Presentation Definition.
*
* @param vcJwt The Verifiable Credential JWT as a string.
* @param presentationDefinition The Presentation Definition to validate against.
* @throws UnsupportedOperationException If the Presentation Definition's Submission Requirements
* feature is not implemented.
* This function ensures that the provided VCs meet the criteria defined in the Presentation Definition.
* It first checks for the presence of Submission Requirements in the definition and throws an exception if they exist,
* as this feature is not implemented. Then, it maps the input descriptors in the presentation definition to the
* corresponding VCs. If the number of mapped descriptors does not match the required count, an error is thrown.
*
* @param vcJwts Iterable of VCs in JWT format to validate.
* @param presentationDefinition The Presentation Definition V2 object against which VCs are validated.
* @throws UnsupportedOperationException If Submission Requirements are present in the definition.
* @throws PresentationExchangeError If the number of input descriptors matched is less than required.
*/
public fun satisfiesPresentationDefinition(
vcJwt: String,
vcJwts: Iterable<String>,
presentationDefinition: PresentationDefinitionV2
) {
val vc = JWTParser.parse(vcJwt) as SignedJWT

if (!presentationDefinition.submissionRequirements.isNullOrEmpty()) {
throw UnsupportedOperationException(
"Presentation Definition's Submission Requirements feature is not implemented"
)
}

presentationDefinition.inputDescriptors
.filter { !it.constraints.fields.isNullOrEmpty() }
.forEach { inputDescriptorWithFields ->
validateInputDescriptorsWithFields(inputDescriptorWithFields, vc.payload)
val inputDescriptorToVcMap = mapInputDescriptorsToVCs(vcJwts, presentationDefinition)

if (inputDescriptorToVcMap.size != presentationDefinition.inputDescriptors.size) {
throw PresentationExchangeError(
"Missing input descriptors: The presentation definition requires " +
"${presentationDefinition.inputDescriptors.size} descriptors, but only " +
"${inputDescriptorToVcMap.size} were found. Check and provide the missing descriptors."
)
Comment on lines +60 to +64
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be incredibly useful if the library would tell the user what input descriptors were missing. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

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

If you're addressing later, then disregard this comment!

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 addressing in the next PR

}
}

private fun mapInputDescriptorsToVCs(
vcJwtList: Iterable<String>,
presentationDefinition: PresentationDefinitionV2
): Map<InputDescriptorV2, List<String>> {
return presentationDefinition.inputDescriptors.associateWith { inputDescriptor ->
Copy link
Contributor

Choose a reason for hiding this comment

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

have not used associateWith before, cool!

val satisfyingVCs = vcJwtList.filter { vcJwt ->
vcSatisfiesInputDescriptor(vcJwt, inputDescriptor)
}
satisfyingVCs
}.filterValues { it.isNotEmpty() }
}

/**
* Validates the input descriptors with associated fields in a Verifiable Credential.
* Evaluates if a Verifiable Credential (VC) satisfies the criteria defined in an Input Descriptor.
*
* @param inputDescriptorWithFields The Input Descriptor with associated fields.
* @param vcPayload The payload of the Verifiable Credential.
* Parses a Verifiable Credential (VC) from JWT format and verifies if it satisfies the Input Descriptor's criteria.
* This function evaluates each required field (where 'optional' is not true) in the descriptor against the VC's JSON payload.
* It extracts data from the VC payload using JSON paths defined in each field and checks compliance with any defined schema.
* Returns false if any required field is missing or fails schema validation, indicating non-compliance with the Input Descriptor.
* Otherwise, it returns true, signifying that the VC meets all criteria.
*
* @param vcJwt The JWT string representing the Verifiable Credential.
* @param inputDescriptor An instance of InputDescriptorV2 defining the criteria to be satisfied by the VC.
* @return Boolean indicating whether the VC satisfies the criteria of the Input Descriptor.
* @throws PresentationExchangeError Any errors during processing
*/
private fun validateInputDescriptorsWithFields(
inputDescriptorWithFields: InputDescriptorV2,
vcPayload: Payload
) {
val requiredFields = inputDescriptorWithFields.constraints.fields!!.filter { it.optional != true }
private fun vcSatisfiesInputDescriptor(
vcJwt: String,
inputDescriptor: InputDescriptorV2
): Boolean {
val vc = JWTParser.parse(vcJwt) as SignedJWT

requiredFields.forEach { field ->
val vcPayloadJson = JsonPath.parse(vcPayload.toString())
?: throw PresentationExchangeError("Failed to parse VC $vcPayload as JsonNode")
val vcPayloadJson = JsonPath.parse(vc.payload.toString())
?: throw PresentationExchangeError("Failed to parse VC payload as JSON.")

Check warning on line 101 in credentials/src/main/kotlin/web5/sdk/credentials/PresentationExchange.kt

View check run for this annotation

Codecov / codecov/patch

credentials/src/main/kotlin/web5/sdk/credentials/PresentationExchange.kt#L101

Added line #L101 was not covered by tests

val matchedFields = field.path.mapNotNull { path -> vcPayloadJson.read<JsonNode>(path) }
if (matchedFields.isEmpty()) {
throw PresentationExchangeError("Could not find matching field for path: ${field.path.joinToString()}")
}
// If the Input Descriptor has constraints and fields defined, evaluate them.
inputDescriptor.constraints?.fields?.let { fields ->
val requiredFields = fields.filter { field -> field.optional != true }
nitro-neal marked this conversation as resolved.
Show resolved Hide resolved

when {
field.filterSchema != null -> {
matchedFields.any { fieldValue ->
when {
// When the field is an array, JSON schema is applied to each array item.
fieldValue.isArray -> {
if (fieldValue.none { valueSatisfiesFieldFilterSchema(it, field.filterSchema!!) })
throw PresentationExchangeError("Validating $fieldValue against ${field.filterSchema} failed")
true
}

// Otherwise, JSON schema is applied to the entire value.
else -> {
valueSatisfiesFieldFilterSchema(fieldValue, field.filterSchema!!)
}
}
for (field in requiredFields) {
val matchedFields = field.path.mapNotNull { path -> vcPayloadJson.read<JsonNode>(path) }
if (matchedFields.isEmpty()) {
// If no matching fields are found for a required field, the VC does not satisfy this Input Descriptor.
return false
}

// If there is a filter schema, process it
if (field.filterSchema != null) {
val satisfiesSchema = evaluateMatchedFields(matchedFields, field.filterSchema!!)
if (!satisfiesSchema) {
// If the field value does not satisfy the schema, the VC does not satisfy this Input Descriptor.
return false
}
}
}
}

// If the VC passes all the checks, it satisfies the criteria of the Input Descriptor.
return true
}

else -> return
/**
* Checks if any JsonNode in 'matchedFields' satisfies the 'schema'.
* Iterates through nodes: if a node or any element in a node array meets the schema, returns true; otherwise false.
*
* @param matchedFields List of JsonNodes to validate.
* @param schema JsonSchema to validate against.
* @return True if any field satisfies the schema, false if none do.
*/
private fun evaluateMatchedFields(matchedFields: List<JsonNode>, schema: JsonSchema): Boolean {
for (fieldValue in matchedFields) {
if (fieldValue.isArray() && fieldValue.any { valueSatisfiesFieldFilterSchema(it, schema) }) {
return true
}

if (fieldValue.isArray() && schema.isExpectingArray() && valueSatisfiesFieldFilterSchema(fieldValue, schema)) {
return true
}

if (!fieldValue.isArray() && valueSatisfiesFieldFilterSchema(fieldValue, schema)) {
return true
}
}
return false
}

private fun JsonSchema.isExpectingArray(): Boolean {
val schemaNode: JsonNode = this.schemaNode
return if (schemaNode is ObjectNode) {
val typeNode = schemaNode.get("type")
typeNode != null && typeNode.asText() == "array"
} else {
false

Check warning on line 160 in credentials/src/main/kotlin/web5/sdk/credentials/PresentationExchange.kt

View check run for this annotation

Codecov / codecov/patch

credentials/src/main/kotlin/web5/sdk/credentials/PresentationExchange.kt#L160

Added line #L160 was not covered by tests
}
}

Expand Down
Loading