generated from TBD54566975/tbd-project-template
-
Notifications
You must be signed in to change notification settings - Fork 10
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
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
dfc5507
adding satisfy pd
nitro-neal 744dad6
new documentation
nitro-neal 5e1c646
add doc
nitro-neal 7067c25
adding to support filter array on single path
nitro-neal a5cab62
Update credentials/src/test/kotlin/web5/sdk/credentials/PresentationE…
nitro-neal 4e33934
Update credentials/src/test/kotlin/web5/sdk/credentials/PresentationE…
nitro-neal File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
||
|
@@ -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." | ||
) | ||
} | ||
} | ||
|
||
private fun mapInputDescriptorsToVCs( | ||
vcJwtList: Iterable<String>, | ||
presentationDefinition: PresentationDefinitionV2 | ||
): Map<InputDescriptorV2, List<String>> { | ||
return presentationDefinition.inputDescriptors.associateWith { inputDescriptor -> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. have not used |
||
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.") | ||
|
||
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 | ||
} | ||
} | ||
|
||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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