From 520edf8178974dda684b02eee0ce27205c5c040d Mon Sep 17 00:00:00 2001 From: Pat Losoponkul Date: Tue, 3 Sep 2024 17:28:26 +0700 Subject: [PATCH 1/5] feat: presentation_submission verification Signed-off-by: Pat Losoponkul chore: only use claim format value defined in contract Signed-off-by: Pat Losoponkul fix: adapt verification interface Signed-off-by: Pat Losoponkul wip Signed-off-by: Pat Losoponkul feat: ps submission entry processing Signed-off-by: Pat Losoponkul test: sanity check Signed-off-by: Pat Losoponkul wip: verify input constraint test: matching descriptor and submission Signed-off-by: Pat Losoponkul test: basic matching descriptor Signed-off-by: Pat Losoponkul test: submission id not match Signed-off-by: Pat Losoponkul test: pd unique id test Signed-off-by: Pat Losoponkul test: moar test!! style: fix and fmt Signed-off-by: Pat Losoponkul wip test: cleanup testing tests: nested_path test and multiple descriptors Signed-off-by: Pat Losoponkul test: optional field constraints test Signed-off-by: Pat Losoponkul --- build.sbt | 4 +- .../pollux/prex/PresentationDefinition.scala | 23 +- .../PresentationDefinitionValidator.scala | 17 +- .../pollux/prex/PresentationSubmission.scala | 31 ++ .../PresentationSubmissionVerification.scala | 235 +++++++++ .../test/resources/ps/basic_presentation.json | 23 + .../resources/ps/nested_presentation.json | 23 + .../PresentationDefinitionValidatorSpec.scala | 30 +- .../prex/PresentationSubmissionSpec.scala | 33 ++ ...esentationSubmissionVerificationSpec.scala | 498 ++++++++++++++++++ .../identus/shared/json/JsonPath.scala | 39 +- .../identus/shared/json/JsonPathSpec.scala | 48 +- 12 files changed, 962 insertions(+), 42 deletions(-) create mode 100644 pollux/prex/src/main/scala/org/hyperledger/identus/pollux/prex/PresentationSubmission.scala create mode 100644 pollux/prex/src/main/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerification.scala create mode 100644 pollux/prex/src/test/resources/ps/basic_presentation.json create mode 100644 pollux/prex/src/test/resources/ps/nested_presentation.json create mode 100644 pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionSpec.scala create mode 100644 pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerificationSpec.scala diff --git a/build.sbt b/build.sbt index a52297b4ff..097d9d9115 100644 --- a/build.sbt +++ b/build.sbt @@ -32,7 +32,7 @@ inThisBuild( "-unchecked", ), scalacOptions += "-Wunused:all", - scalacOptions += "-Wconf:cat=deprecation:warning,any:error", // "-Wconf:help", + // scalacOptions += "-Wconf:cat=deprecation:warning,any:error", // "-Wconf:help", // TODO: revert before pr // scalacOptions += "-Yexplicit-nulls", // scalacOptions += "-Ysafe-init", // scalacOptions += "-Werror", // <=> "-Xfatal-warnings" @@ -808,7 +808,7 @@ lazy val polluxPreX = project .in(file("pollux/prex")) .settings(commonSetttings) .settings(name := "pollux-prex") - .dependsOn(shared, sharedJson) + .dependsOn(shared, sharedJson, polluxVcJWT) // ######################## // ### Pollux Anoncreds ### diff --git a/pollux/prex/src/main/scala/org/hyperledger/identus/pollux/prex/PresentationDefinition.scala b/pollux/prex/src/main/scala/org/hyperledger/identus/pollux/prex/PresentationDefinition.scala index 7a5b8ac47f..21ded1e947 100644 --- a/pollux/prex/src/main/scala/org/hyperledger/identus/pollux/prex/PresentationDefinition.scala +++ b/pollux/prex/src/main/scala/org/hyperledger/identus/pollux/prex/PresentationDefinition.scala @@ -16,7 +16,7 @@ object JsonPathValue { given Conversion[String, JsonPathValue] = identity extension (jpv: JsonPathValue) { - def toJsonPath: IO[JsonPathError, JsonPath] = JsonPath.compile(jpv) + def toJsonPath: Either[JsonPathError, JsonPath] = JsonPath.compile(jpv) def value: String = jpv } } @@ -65,7 +65,26 @@ object Ldp { given Decoder[Ldp] = deriveDecoder[Ldp] } -case class ClaimFormat(jwt: Option[Jwt] = None, ldp: Option[Ldp] = None) +enum ClaimFormatValue(val value: String) { + case jwt_vc extends ClaimFormatValue("jwt_vc") + case jwt_vp extends ClaimFormatValue("jwt_vp") +} + +object ClaimFormatValue { + given Encoder[ClaimFormatValue] = Encoder.encodeString.contramap(_.value) + given Decoder[ClaimFormatValue] = Decoder.decodeString.emap { + case "jwt_vc" => Right(ClaimFormatValue.jwt_vc) + case "jwt_vp" => Right(ClaimFormatValue.jwt_vp) + case other => Left(s"Invalid ClaimFormatValue: $other") + } +} + +case class ClaimFormat( + jwt: Option[Jwt] = None, + jwt_vc: Option[Jwt] = None, + jwt_vp: Option[Jwt] = None, + ldp: Option[Ldp] = None +) object ClaimFormat { given Encoder[ClaimFormat] = deriveEncoder[ClaimFormat] diff --git a/pollux/prex/src/main/scala/org/hyperledger/identus/pollux/prex/PresentationDefinitionValidator.scala b/pollux/prex/src/main/scala/org/hyperledger/identus/pollux/prex/PresentationDefinitionValidator.scala index 9b47a914ae..9b21a17d94 100644 --- a/pollux/prex/src/main/scala/org/hyperledger/identus/pollux/prex/PresentationDefinitionValidator.scala +++ b/pollux/prex/src/main/scala/org/hyperledger/identus/pollux/prex/PresentationDefinitionValidator.scala @@ -1,6 +1,7 @@ package org.hyperledger.identus.pollux.prex import org.hyperledger.identus.pollux.prex.PresentationDefinitionError.{ + DuplicatedDescriptorId, InvalidFilterJsonPath, InvalidFilterJsonSchema, JsonSchemaOptionNotSupported @@ -21,6 +22,12 @@ sealed trait PresentationDefinitionError extends Failure { } object PresentationDefinitionError { + final case class DuplicatedDescriptorId(ids: Seq[String]) extends PresentationDefinitionError { + override def statusCode: StatusCode = StatusCode.BadRequest + override def userFacingMessage: String = + s"PresentationDefinition input_descriptors contains duplicated id(s): ${ids.mkString(", ")}" + } + final case class InvalidFilterJsonPath(path: String, error: JsonPathError) extends PresentationDefinitionError { override def statusCode: StatusCode = StatusCode.BadRequest override def userFacingMessage: String = @@ -67,16 +74,24 @@ class PresentationDefinitionValidatorImpl(filterSchemaValidator: JsonSchemaValid val filters = fields.flatMap(_.filter) for { + _ <- validateUniqueDescriptorIds(pd.input_descriptors) _ <- validateJsonPaths(paths) _ <- validateFilters(filters) _ <- validateAllowedFilterSchemaKeys(filters) } yield () } + private def validateUniqueDescriptorIds(descriptors: Seq[InputDescriptor]): IO[PresentationDefinitionError, Unit] = { + val ids = descriptors.map(_.id) + if ids.distinct.size == ids.size + then ZIO.unit + else ZIO.fail(DuplicatedDescriptorId(ids)) + } + private def validateJsonPaths(paths: Seq[JsonPathValue]): IO[PresentationDefinitionError, Unit] = { ZIO .foreach(paths) { path => - path.toJsonPath.mapError(InvalidFilterJsonPath(path.value, _)) + ZIO.fromEither(path.toJsonPath).mapError(InvalidFilterJsonPath(path.value, _)) } .unit } diff --git a/pollux/prex/src/main/scala/org/hyperledger/identus/pollux/prex/PresentationSubmission.scala b/pollux/prex/src/main/scala/org/hyperledger/identus/pollux/prex/PresentationSubmission.scala new file mode 100644 index 0000000000..10475badc0 --- /dev/null +++ b/pollux/prex/src/main/scala/org/hyperledger/identus/pollux/prex/PresentationSubmission.scala @@ -0,0 +1,31 @@ +package org.hyperledger.identus.pollux.prex + +import io.circe.* +import io.circe.generic.semiauto.* + +case class InputDescriptorMapping( + id: String, + format: ClaimFormatValue, + path: JsonPathValue, + path_nested: Option[InputDescriptorMapping] +) + +object InputDescriptorMapping { + given Encoder[InputDescriptorMapping] = deriveEncoder[InputDescriptorMapping] + given Decoder[InputDescriptorMapping] = deriveDecoder[InputDescriptorMapping] +} + +/** Refer to Presentation + * Definition + */ +case class PresentationSubmission( + definition_id: String, + id: String = java.util.UUID.randomUUID.toString(), // UUID + descriptor_map: Seq[InputDescriptorMapping] = Seq.empty +) + +object PresentationSubmission { + given Encoder[PresentationSubmission] = deriveEncoder[PresentationSubmission] + given Decoder[PresentationSubmission] = deriveDecoder[PresentationSubmission] +} diff --git a/pollux/prex/src/main/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerification.scala b/pollux/prex/src/main/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerification.scala new file mode 100644 index 0000000000..f0e616a795 --- /dev/null +++ b/pollux/prex/src/main/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerification.scala @@ -0,0 +1,235 @@ +package org.hyperledger.identus.pollux.prex + +import io.circe.* +import io.circe.syntax.* +import org.hyperledger.identus.pollux.prex.PresentationSubmissionError.{ + ClaimDecodeFailure, + ClaimFormatVerificationFailure, + ClaimNotSatisfyInputConstraint, + InvalidDataTypeForClaimFormat, + InvalidJsonPath, + InvalidSubmissionId, + JsonPathNotFound, + SubmissionNotSatisfyInputDescriptors +} +import org.hyperledger.identus.pollux.prex.PresentationSubmissionError.InvalidNestedPathDescriptorId +import org.hyperledger.identus.pollux.vc.jwt.{JWT, JwtCredential, JwtPresentation} +import org.hyperledger.identus.pollux.vc.jwt.CredentialPayload.Implicits.* +import org.hyperledger.identus.pollux.vc.jwt.PresentationPayload.Implicits.* +import org.hyperledger.identus.shared.json.{JsonInterop, JsonPath, JsonPathError, JsonSchemaValidatorImpl} +import org.hyperledger.identus.shared.models.{Failure, StatusCode} +import zio.* +import zio.json.ast.Json as ZioJson + +sealed trait PresentationSubmissionError extends Failure { + override def namespace: String = "PresentationSubmissionError" +} + +object PresentationSubmissionError { + case class InvalidSubmissionId(expected: String, actual: String) extends PresentationSubmissionError { + override def statusCode: StatusCode = StatusCode.BadRequest + override def userFacingMessage: String = s"Expected presentation_submission id to be $expected, got $actual" + } + + case class InvalidNestedPathDescriptorId(expected: String, actual: String) extends PresentationSubmissionError { + override def statusCode: StatusCode = StatusCode.BadRequest + override def userFacingMessage: String = + s"Descriptor id for all nested_path level must be the same. Expected id $expected, got $actual" + } + + case class SubmissionNotSatisfyInputDescriptors(required: Seq[String], provided: Seq[String]) + extends PresentationSubmissionError { + override def statusCode: StatusCode = StatusCode.BadRequest + override def userFacingMessage: String = s"Submission does not satisfy all input descriptors. Required: ${required + .mkString("[", ", ", "]")}, Provided: ${provided.mkString("[", ", ", "]")}" + } + + case class InvalidDataTypeForClaimFormat(format: ClaimFormatValue, path: JsonPathValue, expectedType: String) + extends PresentationSubmissionError { + override def statusCode: StatusCode = StatusCode.BadRequest + override def userFacingMessage: String = + s"Expect json to be type $expectedType for claim format ${format.value} on path ${path.value}" + } + + case class InvalidJsonPath(path: JsonPathValue, error: JsonPathError) extends PresentationSubmissionError { + override def statusCode: StatusCode = StatusCode.BadRequest + override def userFacingMessage: String = s"Invalid json path ${path.value} in the presentation_submission" + } + + case class JsonPathNotFound(path: JsonPathValue) extends PresentationSubmissionError { + override def statusCode: StatusCode = StatusCode.BadRequest + override def userFacingMessage: String = s"Json data at path ${path.value} not found in the presentation_submission" + } + + case class ClaimDecodeFailure(format: ClaimFormatValue, path: JsonPathValue, error: String) + extends PresentationSubmissionError { + override def statusCode: StatusCode = StatusCode.BadRequest + override def userFacingMessage: String = + s"Unable to decode claim according to format ${format.value} at path ${path.value}: $error" + } + + case class ClaimFormatVerificationFailure(format: ClaimFormatValue, path: JsonPathValue, error: String) + extends PresentationSubmissionError { + override def statusCode: StatusCode = StatusCode.BadRequest + override def userFacingMessage: String = + s"Claim format ${format.value} at path ${path.value} failed verification with errors: $error" + } + + case class ClaimNotSatisfyInputConstraint(id: String) extends PresentationSubmissionError { + override def statusCode: StatusCode = StatusCode.BadRequest + override def userFacingMessage: String = + s"Claim in presentation_submission with id $id does not satisfy input constraints" + } +} + +case class ClaimFormatVerification( + jwtVp: JWT => IO[String, Unit], + jwtVc: JWT => IO[String, Unit], +) + +object PresentationSubmissionVerification { + + def verify( + pd: PresentationDefinition, + ps: PresentationSubmission, + rootTraversalObject: ZioJson, + )(formatVerification: ClaimFormatVerification): IO[PresentationSubmissionError, Unit] = { + for { + _ <- verifySubmissionId(pd, ps) + _ <- verifySubmissionRequirement(pd, ps) + entries <- ZIO + .foreach(ps.descriptor_map) { descriptor => + extractSubmissionEntry(rootTraversalObject, descriptor)(formatVerification).map(descriptor.id -> _) + } + _ <- verifyInputConstraints(pd, entries) + } yield () + } + + private def verifySubmissionId( + pd: PresentationDefinition, + ps: PresentationSubmission + ): IO[PresentationSubmissionError, Unit] = { + if pd.id == ps.definition_id + then ZIO.unit + else ZIO.fail(InvalidSubmissionId(pd.id, ps.id)) + } + + // This is not yet fully supported as in https://identity.foundation/presentation-exchange/spec/v2.1.1/#submission-requirement-feature + // It is now a simple check that submission satisties all input descriptors + private def verifySubmissionRequirement( + pd: PresentationDefinition, + ps: PresentationSubmission + ): IO[PresentationSubmissionError, Unit] = { + val pdIds = pd.input_descriptors.map(_.id) + val psIds = ps.descriptor_map.map(_.id) + if pdIds.toSet == psIds.toSet + then ZIO.unit + else ZIO.fail(SubmissionNotSatisfyInputDescriptors(pdIds.toSeq, psIds.toSeq)) + } + + private def verifyInputConstraints( + pd: PresentationDefinition, + entries: Seq[(String, ZioJson)] + ): IO[PresentationSubmissionError, Unit] = { + val descriptorLookup = pd.input_descriptors.map(d => d.id -> d).toMap + val descriptorWithEntry = entries.flatMap { case (id, entry) => descriptorLookup.get(id).map(_ -> entry) } + ZIO + .foreach(descriptorWithEntry) { case (descriptor, entry) => + verifyInputConstraint(descriptor, entry) + } + .unit + } + + private def verifyInputConstraint( + descriptor: InputDescriptor, + entry: ZioJson + ): IO[PresentationSubmissionError, Unit] = { + val fields = descriptor.constraints.fields.getOrElse(Nil) + // all fields need to be valid + ZIO + .foreach(fields) { field => + // only one of the paths need to be valid + ZIO + .validateFirst(field.path) { p => + for { + jsonPath <- ZIO.fromEither(p.toJsonPath) + jsonAtPath <- ZIO.fromEither(jsonPath.read(entry)) + maybeFilter <- ZIO.foreach(field.filter)(_.toJsonSchema) + _ <- ZIO.foreach(maybeFilter) { filter => + JsonSchemaValidatorImpl(filter).validate(jsonAtPath.toString()) + } + } yield () + } + .catchAll { errors => + // if all paths don't satisfy constraints, but optional, then the field is still valid + // https://identity.foundation/presentation-exchange/spec/v2.1.1/#input-evaluation + if field.optional.getOrElse(false) + then ZIO.unit + else ZIO.fail(errors) + } + .mapError(_ => ClaimNotSatisfyInputConstraint(descriptor.id)) + } + .unit + } + + private def extractSubmissionEntry( + traversalObject: ZioJson, + descriptor: InputDescriptorMapping + )(formatVerification: ClaimFormatVerification): IO[PresentationSubmissionError, ZioJson] = { + for { + path <- ZIO + .fromEither(descriptor.path.toJsonPath) + .mapError(InvalidJsonPath(descriptor.path, _)) + jsonAtPath <- ZIO + .fromEither(path.read(traversalObject)) + .mapError(_ => JsonPathNotFound(descriptor.path)) + currentNode <- descriptor.format match { + case ClaimFormatValue.jwt_vc => + verifyJwtVc(jsonAtPath, descriptor.path)(formatVerification.jwtVc) + case ClaimFormatValue.jwt_vp => + verifyJwtVp(jsonAtPath, descriptor.path)(formatVerification.jwtVp) + } + leafNode <- descriptor.path_nested.fold(ZIO.succeed(currentNode)) { nestedDescriptor => + if descriptor.id != nestedDescriptor.id + then ZIO.fail(InvalidNestedPathDescriptorId(descriptor.id, nestedDescriptor.id)) + else extractSubmissionEntry(currentNode, nestedDescriptor)(formatVerification) + } + } yield leafNode + } + + private def verifyJwtVc( + json: ZioJson, + path: JsonPathValue + )(formatVerification: JWT => IO[String, Unit]): IO[PresentationSubmissionError, ZioJson] = { + val format = ClaimFormatValue.jwt_vc + for { + jwt <- ZIO + .fromOption(json.asString) + .map(JWT(_)) + .mapError(_ => InvalidDataTypeForClaimFormat(format, path, "string")) + payload <- JwtCredential + .decodeJwt(jwt) + .mapError(e => ClaimDecodeFailure(format, path, e)) + _ <- formatVerification(jwt) + .mapError(errors => ClaimFormatVerificationFailure(format, path, errors.mkString)) + } yield JsonInterop.toZioJsonAst(payload.asJson) + } + + private def verifyJwtVp( + json: ZioJson, + path: JsonPathValue + )(formatVerification: JWT => IO[String, Unit]): IO[PresentationSubmissionError, ZioJson] = { + val format = ClaimFormatValue.jwt_vp + for { + jwt <- ZIO + .fromOption(json.asString) + .map(JWT(_)) + .mapError(_ => InvalidDataTypeForClaimFormat(format, path, "string")) + payload <- ZIO + .fromTry(JwtPresentation.decodeJwt(jwt)) + .mapError(e => ClaimDecodeFailure(format, path, e.getMessage())) + _ <- formatVerification(jwt) + .mapError(errors => ClaimFormatVerificationFailure(format, path, errors.mkString)) + } yield JsonInterop.toZioJsonAst(payload.asJson) + } +} diff --git a/pollux/prex/src/test/resources/ps/basic_presentation.json b/pollux/prex/src/test/resources/ps/basic_presentation.json new file mode 100644 index 0000000000..942667c045 --- /dev/null +++ b/pollux/prex/src/test/resources/ps/basic_presentation.json @@ -0,0 +1,23 @@ +{ + "presentation_submission": { + "id": "a30e3b91-fb77-4d22-95fa-871689c322e2", + "definition_id": "32f54163-7166-48f1-93d8-ff217bdb0653", + "descriptor_map": [ + { + "id": "banking_input_2", + "format": "jwt_vc", + "path": "$.verifiableCredential[0]" + }, + { + "id": "employment_input", + "format": "jwt_vc", + "path": "$.verifiableCredential[1]" + }, + { + "id": "citizenship_input_1", + "format": "jwt_vc", + "path": "$.verifiableCredential[2]" + } + ] + } +} diff --git a/pollux/prex/src/test/resources/ps/nested_presentation.json b/pollux/prex/src/test/resources/ps/nested_presentation.json new file mode 100644 index 0000000000..a5ecefb2f7 --- /dev/null +++ b/pollux/prex/src/test/resources/ps/nested_presentation.json @@ -0,0 +1,23 @@ +{ + "presentation_submission": { + "id": "a30e3b91-fb77-4d22-95fa-871689c322e2", + "definition_id": "32f54163-7166-48f1-93d8-ff217bdb0653", + "descriptor_map": [ + { + "id": "banking_input_2", + "format": "jwt_vp", + "path": "$.outerClaim[0]", + "path_nested": { + "id": "banking_input_2", + "format": "jwt_vc", + "path": "$.innerClaim[1]", + "path_nested": { + "id": "banking_input_2", + "format": "jwt_vc", + "path": "$.mostInnerClaim[2]" + } + } + } + ] + } +} diff --git a/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationDefinitionValidatorSpec.scala b/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationDefinitionValidatorSpec.scala index 108168ae62..290c72198d 100644 --- a/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationDefinitionValidatorSpec.scala +++ b/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationDefinitionValidatorSpec.scala @@ -4,6 +4,7 @@ import io.circe.* import io.circe.generic.auto.* import io.circe.parser.* import org.hyperledger.identus.pollux.prex.PresentationDefinitionError.{ + DuplicatedDescriptorId, InvalidFilterJsonPath, InvalidFilterJsonSchema, JsonSchemaOptionNotSupported @@ -134,7 +135,34 @@ object PresentationDefinitionValidatorSpec extends ZIOSpecDefault { .map(_.presentation_definition) exit <- validator.validate(pd).exit } yield assert(exit)(failsWithA[InvalidFilterJsonPath]) - } + }, + test("reject when descriptor id is not unique") { + val pdJson = + """{ + | "presentation_definition": { + | "id": "32f54163-7166-48f1-93d8-ff217bdb0653", + | "input_descriptors": [ + | { + | "id": "wa_driver_license", + | "constraints": {} + | }, + | { + | "id": "wa_driver_license", + | "constraints": {} + | } + | ] + | } + |} + """.stripMargin + + for { + validator <- ZIO.service[PresentationDefinitionValidator] + pd <- ZIO + .fromEither(decode[ExampleTransportEnvelope](pdJson)) + .map(_.presentation_definition) + exit <- validator.validate(pd).exit + } yield assert(exit)(failsWithA[DuplicatedDescriptorId]) + }, ) .provide(PresentationDefinitionValidatorImpl.layer) diff --git a/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionSpec.scala b/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionSpec.scala new file mode 100644 index 0000000000..fdc723a246 --- /dev/null +++ b/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionSpec.scala @@ -0,0 +1,33 @@ +package org.hyperledger.identus.pollux.prex + +import io.circe.* +import io.circe.generic.auto.* +import io.circe.parser.* +import zio.* +import zio.test.* + +import scala.io.Source +import scala.util.Using + +object PresentationSubmissionSpec extends ZIOSpecDefault { + + final case class ExampleTransportEnvelope(presentation_submission: PresentationSubmission) + + override def spec = suite("PresentationSubmissionSpec")( + test("parse presentation-submission exmaples from spec") { + val resourcePaths = Seq( + "ps/basic_presentation.json", + "ps/nested_presentation.json", + ) + ZIO + .foreach(resourcePaths) { path => + ZIO + .fromTry(Using(Source.fromResource(path))(_.mkString)) + .flatMap(json => ZIO.fromEither(decode[ExampleTransportEnvelope](json))) + .map(_.presentation_submission) + } + .as(assertCompletes) + } + ) + +} diff --git a/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerificationSpec.scala b/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerificationSpec.scala new file mode 100644 index 0000000000..cde4360ddb --- /dev/null +++ b/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerificationSpec.scala @@ -0,0 +1,498 @@ +package org.hyperledger.identus.pollux.prex + +import io.circe.* +import io.circe.parser.* +import org.hyperledger.identus.pollux.prex.PresentationSubmissionError.{ + InvalidNestedPathDescriptorId, + InvalidSubmissionId, + SubmissionNotSatisfyInputDescriptors +} +import org.hyperledger.identus.pollux.vc.jwt.{ + DID, + ES256KSigner, + Issuer, + JWT, + JwtCredential, + JwtCredentialPayload, + JwtVc +} +import org.hyperledger.identus.pollux.vc.jwt.JwtPresentation +import org.hyperledger.identus.pollux.vc.jwt.JwtPresentationPayload +import org.hyperledger.identus.pollux.vc.jwt.JwtVerifiableCredentialPayload +import org.hyperledger.identus.pollux.vc.jwt.JwtVp +import org.hyperledger.identus.pollux.vc.jwt.VerifiableCredentialPayload +import org.hyperledger.identus.shared.crypto.Apollo +import zio.* +import zio.json.ast.Json as ZioJson +import zio.test.* +import zio.test.Assertion.* + +import java.time.Instant + +object PresentationSubmissionVerificationSpec extends ZIOSpecDefault { + + private def decodeUnsafe[T: Decoder](json: String): T = decode[T](json).toOption.get + private def parseUnsafe(json: String): Json = parse(json).toOption.get + + private val noopFormatVerification = ClaimFormatVerification(jwtVp = _ => ZIO.unit, jwtVc = _ => ZIO.unit) + private val basePd: PresentationDefinition = + decodeUnsafe[PresentationDefinition]( + """ + |{ + | "id": "32f54163-7166-48f1-93d8-ff217bdb0653", + | "input_descriptors": [] + |} + """.stripMargin + ) + private val basePs: PresentationSubmission = + decodeUnsafe[PresentationSubmission]( + """ + |{ + | "id": "a30e3b91-fb77-4d22-95fa-871689c322e2", + | "definition_id": "32f54163-7166-48f1-93d8-ff217bdb0653", + | "descriptor_map": [] + |} + """.stripMargin + ) + + private def generateVcPayload(subject: Json): JwtCredentialPayload = { + val iss = "did:example:ebfeb1f712ebc6f1c276e12ec21" + val jwtCredentialNbf = Instant.parse("2010-01-01T00:00:00Z") + JwtCredentialPayload( + iss = iss, + maybeSub = None, + vc = JwtVc( + `@context` = Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), + `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), + maybeCredentialSchema = None, + credentialSubject = subject, + maybeCredentialStatus = None, + maybeRefreshService = None, + maybeEvidence = None, + maybeTermsOfUse = None, + maybeValidFrom = None, + maybeValidUntil = None, + maybeIssuer = Some(Left(iss)) + ), + nbf = jwtCredentialNbf, + aud = Set.empty, + maybeExp = None, + maybeJti = None + ) + } + + private def generateVpPayload(vcs: Seq[VerifiableCredentialPayload]): JwtPresentationPayload = { + val iss = "did:example:ebfeb1f712ebc6f1c276e12ec21" + JwtPresentationPayload( + iss = iss, + vp = JwtVp( + `@context` = + Vector("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), + `type` = Vector("VerifiablePresentation"), + verifiableCredential = vcs.toVector + ), + maybeNbf = None, + aud = Vector.empty, + maybeExp = None, + maybeJti = None, + maybeNonce = None, + ) + } + + private def generateJwtVc(payload: JwtCredentialPayload): JWT = { + val keyPair = Apollo.default.secp256k1.generateKeyPair + val publicKey = keyPair.publicKey + val privateKey = keyPair.privateKey + val issuer = Issuer( + DID(payload.iss), + ES256KSigner(privateKey.toJavaPrivateKey, None), + publicKey.toJavaPublicKey + ) + JwtCredential.encodeJwt(payload, issuer) + } + + private def generateJwtVp(payload: JwtPresentationPayload): JWT = { + val keyPair = Apollo.default.secp256k1.generateKeyPair + val publicKey = keyPair.publicKey + val privateKey = keyPair.privateKey + val issuer = Issuer( + DID(payload.iss), + ES256KSigner(privateKey.toJavaPrivateKey, None), + publicKey.toJavaPublicKey + ) + JwtPresentation.encodeJwt(payload, issuer) + } + + private def assertSubmissionVerification(descriptorsJson: String, descriptorMapJson: String, jwt: JWT)( + assertion: Assertion[Exit[PresentationSubmissionError, Unit]] + ) = { + val descriptors = decodeUnsafe[Seq[InputDescriptor]](descriptorsJson) + val descriptorMap = decodeUnsafe[Seq[InputDescriptorMapping]](descriptorMapJson) + val pd = basePd.copy(input_descriptors = descriptors) + val ps = basePs.copy(descriptor_map = descriptorMap) + for { + result <- PresentationSubmissionVerification + .verify(pd, ps, ZioJson.Str(jwt.value))(noopFormatVerification) + .exit + } yield assert(result)(assertion) + } + + override def spec: Spec[TestEnvironment & Scope, Any] = suite("PresentationSubmissionVerificationSpec")( + test("descriptor and submission id not match") { + val ps = basePs.copy(definition_id = "random-id") + val payload = generateVcPayload(parseUnsafe("""{"name": "alice"}""")) + val jwtVc = generateJwtVc(payload) + for { + result <- PresentationSubmissionVerification + .verify(basePd, ps, ZioJson.Str(jwtVc.value))(noopFormatVerification) + .exit + } yield assert(result)(failsWithA[InvalidSubmissionId]) + }, + test("empty descriptor and submission") { + val payload = generateVcPayload(parseUnsafe("""{"name": "alice"}""")) + val jwtVc = generateJwtVc(payload) + assertSubmissionVerification("[]", "[]", jwtVc)(succeeds(anything)) + }, + test("one descriptor and corresponding submission") { + val payload = generateVcPayload(parseUnsafe("""{"name": "alice"}""")) + val jwtVc = generateJwtVc(payload) + assertSubmissionVerification( + """[ + | {"id": "university_degree", "constraints": {}} + |] + """.stripMargin, + """[ + | {"id": "university_degree", "format": "jwt_vc", "path": "$"} + |] + """.stripMargin, + jwtVc + )(succeeds(anything)) + }, + test("descriptor with no submission") { + val payload = generateVcPayload(parseUnsafe("""{"name": "alice"}""")) + val jwtVc = generateJwtVc(payload) + assertSubmissionVerification( + """[ + | {"id": "university_degree", "constraints": {}} + |] + """.stripMargin, + "[]", + jwtVc + )(failsWithA[SubmissionNotSatisfyInputDescriptors]) + }, + test("submission with no descriptor") { + val payload = generateVcPayload(parseUnsafe("""{"name": "alice"}""")) + val jwtVc = generateJwtVc(payload) + assertSubmissionVerification( + "[]", + """[ + | {"id": "university_degree", "format": "jwt_vc", "path": "$"} + |] + """.stripMargin, + jwtVc + )(failsWithA[SubmissionNotSatisfyInputDescriptors]) + }, + test("descriptor with path verification") { + val payload = generateVcPayload(parseUnsafe("""{"name": "alice"}""")) + val jwtVc = generateJwtVc(payload) + assertSubmissionVerification( + """[ + | { + | "id": "university_degree", + | "constraints": { + | "fields": [ + | {"path": ["$.vc.credentialSubject.name"]} + | ] + | } + | } + |] + """.stripMargin, + """[ + | {"id": "university_degree", "format": "jwt_vc", "path": "$"} + |] + """.stripMargin, + jwtVc + )(succeeds(anything)) + }, + test("descriptor with multiple paths verification") { + val payload = generateVcPayload(parseUnsafe("""{"name": "alice"}""")) + val jwtVc = generateJwtVc(payload) + assertSubmissionVerification( + """[ + | { + | "id": "university_degree", + | "constraints": { + | "fields": [ + | {"path": ["$.vc.credentialSubject.fullName", "$.vc.credentialSubject.name"]} + | ] + | } + | } + |] + """.stripMargin, + """[ + | {"id": "university_degree", "format": "jwt_vc", "path": "$"} + |] + """.stripMargin, + jwtVc + )(succeeds(anything)) + }, + test("descriptor with path and filter verification") { + val payload = generateVcPayload(parseUnsafe("""{"name": "alice"}""")) + val jwtVc = generateJwtVc(payload) + assertSubmissionVerification( + """[ + | { + | "id": "university_degree", + | "constraints": { + | "fields": [ + | { + | "path": ["$.vc.credentialSubject.name"], + | "filter": { + | "type": "string", + | "const": "alice" + | } + | } + | ] + | } + | } + |] + """.stripMargin, + """[ + | {"id": "university_degree", "format": "jwt_vc", "path": "$"} + |] + """.stripMargin, + jwtVc + )(succeeds(anything)) + }, + test("descriptor with multiple paths and filter verification") { + val payload = generateVcPayload(parseUnsafe("""{"name": "alice"}""")) + val jwtVc = generateJwtVc(payload) + assertSubmissionVerification( + """[ + | { + | "id": "university_degree", + | "constraints": { + | "fields": [ + | { + | "path": ["$.vc.credentialSubject.fullName", "$.vc.credentialSubject.name"], + | "filter": { + | "type": "string", + | "const": "alice" + | } + | } + | ] + | } + | } + |] + """.stripMargin, + """[ + | {"id": "university_degree", "format": "jwt_vc", "path": "$"} + |] + """.stripMargin, + jwtVc, + )(succeeds(anything)) + }, + test("descriptor with multiple fields verification") { + val payload = generateVcPayload(parseUnsafe("""{"name": "alice", "degree": "Finance"}""")) + val jwtVc = generateJwtVc(payload) + assertSubmissionVerification( + """[ + | { + | "id": "university_degree", + | "constraints": { + | "fields": [ + | { + | "path": ["$.vc.credentialSubject.name"], + | "filter": { + | "type": "string", + | "const": "alice" + | } + | }, + | { + | "path": ["$.vc.credentialSubject.degree"], + | "filter": { + | "type": "string", + | "const": "Finance" + | } + | } + | ] + | } + | } + |] + """.stripMargin, + """[ + | {"id": "university_degree", "format": "jwt_vc", "path": "$"} + |] + """.stripMargin, + jwtVc + )(succeeds(anything)) + }, + test("submission with nested path jwt_vc inside jwt_vp") { + val vcPayload = generateVcPayload(parseUnsafe("""{"name": "alice"}""")) + val jwtVc = generateJwtVc(vcPayload) + val vpPayload = generateVpPayload(Seq(JwtVerifiableCredentialPayload(jwtVc))) + val jwtVp = generateJwtVp(vpPayload) + assertSubmissionVerification( + """[ + | { + | "id": "university_degree", + | "constraints": { + | "fields": [ + | { + | "path": ["$.vc.credentialSubject.name"], + | "filter": { + | "type": "string", + | "const": "alice" + | } + | } + | ] + | } + | } + |] + """.stripMargin, + """[ + | { + | "id": "university_degree", + | "format": "jwt_vp", + | "path": "$", + | "path_nested": { + | "id": "university_degree", + | "format": "jwt_vc", + | "path": "$.vp.verifiableCredential[0]" + | } + | } + |] + """.stripMargin, + jwtVp + )(succeeds(anything)) + }, + test("submission with nested_path having different id at each level should fail") { + val vcPayload = generateVcPayload(parseUnsafe("""{"name": "alice"}""")) + val jwtVc = generateJwtVc(vcPayload) + val vpPayload = generateVpPayload(Seq(JwtVerifiableCredentialPayload(jwtVc))) + val jwtVp = generateJwtVp(vpPayload) + assertSubmissionVerification( + """[ + | {"id": "university_degree", "constraints": {}} + |] + """.stripMargin, + """[ + | { + | "id": "university_degree", + | "format": "jwt_vp", + | "path": "$", + | "path_nested": { + | "id": "university_degree_2", + | "format": "jwt_vc", + | "path": "$.vp.verifiableCredential[0]" + | } + | } + |] + """.stripMargin, + jwtVp + )(failsWithA[InvalidNestedPathDescriptorId]) + }, + test("multiple descriptors with corresponding submission") { + val vcPayload1 = generateVcPayload(parseUnsafe("""{"name": "alice"}""")) + val jwtVc1 = generateJwtVc(vcPayload1) + val vcPayload2 = generateVcPayload(parseUnsafe("""{"vehicle_type": "car"}""")) + val jwtVc2 = generateJwtVc(vcPayload2) + val vpPayload = generateVpPayload( + Seq( + JwtVerifiableCredentialPayload(jwtVc1), + JwtVerifiableCredentialPayload(jwtVc2) + ) + ) + val jwtVp = generateJwtVp(vpPayload) + assertSubmissionVerification( + """[ + | {"id": "university_degree", "constraints": {}}, + | {"id": "driving_license", "constraints": {}} + |] + """.stripMargin, + """[ + | { + | "id": "university_degree", + | "format": "jwt_vp", + | "path": "$", + | "path_nested": { + | "id": "university_degree", + | "format": "jwt_vc", + | "path": "$.vp.verifiableCredential[0]" + | } + | }, + | { + | "id": "driving_license", + | "format": "jwt_vp", + | "path": "$", + | "path_nested": { + | "id": "driving_license", + | "format": "jwt_vc", + | "path": "$.vp.verifiableCredential[1]" + | } + | } + |] + """.stripMargin, + jwtVp + )(succeeds(anything)) + }, + test("descriptor with optional field and submission that omit optional fields") { + val payload = generateVcPayload(parseUnsafe("""{"gpa": 4.00}""")) + val jwtVc = generateJwtVc(payload) + assertSubmissionVerification( + """[ + | { + | "id": "university_degree", + | "constraints": { + | "fields": [ + | { + | "path": ["$.vc.credentialSubject.name"], + | "optional": true + | } + | ] + | } + | } + |] + """.stripMargin, + """[ + | {"id": "university_degree", "format": "jwt_vc", "path": "$"} + |] + """.stripMargin, + jwtVc + )(succeeds(anything)) + }, + test("descriptor with optional field and submission with optional fields that don't satisfy constraints") { + val payload = generateVcPayload(parseUnsafe("""{"name": "alice"}""")) + val jwtVc = generateJwtVc(payload) + assertSubmissionVerification( + """[ + | { + | "id": "university_degree", + | "constraints": { + | "fields": [ + | { + | "path": ["$.vc.credentialSubject.name"], + | "optional": true, + | "filter": { + | "type": "string", + | "const": "bob" + | } + | } + | ] + | } + | } + |] + """.stripMargin, + """[ + | {"id": "university_degree", "format": "jwt_vc", "path": "$"} + |] + """.stripMargin, + jwtVc + )(succeeds(anything)) + } + ) + /* TODO + * - test for format verification failure + */ + +} diff --git a/shared/json/src/main/scala/org/hyperledger/identus/shared/json/JsonPath.scala b/shared/json/src/main/scala/org/hyperledger/identus/shared/json/JsonPath.scala index 48770dd784..d1624331a1 100644 --- a/shared/json/src/main/scala/org/hyperledger/identus/shared/json/JsonPath.scala +++ b/shared/json/src/main/scala/org/hyperledger/identus/shared/json/JsonPath.scala @@ -7,6 +7,8 @@ import zio.* import zio.json.* import zio.json.ast.Json +import scala.util.Try + sealed trait JsonPathError extends Failure { override def namespace: String = "JsonPathError" } @@ -21,32 +23,45 @@ object JsonPathError { override def statusCode: StatusCode = StatusCode.BadRequest override def userFacingMessage: String = s"The json path '$path' cannot be found in a json" } + + final case class UnexpectedCompilePathError(path: String, e: Throwable) extends JsonPathError { + override def statusCode: StatusCode = StatusCode.InternalServerError + override def userFacingMessage: String = s"An unhandled error occurred while compiling the JsonPath for $path" + } + + final case class UnexpectedReadPathError(path: String, e: Throwable) extends JsonPathError { + override def statusCode: StatusCode = StatusCode.InternalServerError + override def userFacingMessage: String = s"An unhandled error occurred while reading the JsonPath for $path" + } } opaque type JsonPath = JaywayJsonPath object JsonPath { - def compile(path: String): IO[JsonPathError, JsonPath] = { - ZIO - .attempt(JaywayJsonPath.compile(path)) - .refineOrDie { + def compileUnsafe(path: String): JsonPath = JaywayJsonPath.compile(path) + + def compile(path: String): Either[JsonPathError, JsonPath] = + Try(compileUnsafe(path)).toEither.left + .map { case e: IllegalArgumentException => JsonPathError.InvalidPathInput(e.getMessage()) case e: InvalidPathException => JsonPathError.InvalidPathInput(e.getMessage()) + case e => JsonPathError.UnexpectedCompilePathError(path, e) } - } extension (jsonPath: JsonPath) { - def read(json: Json): IO[JsonPathError, Json] = { + def read(json: Json): Either[JsonPathError, Json] = { val jsonProvider = JacksonJsonProvider() val document = JaywayJsonPath.parse(json.toString()) for { - queriedObj <- ZIO - .attempt(document.read[java.lang.Object](jsonPath)) - .refineOrDie { case e: PathNotFoundException => - JsonPathError.PathNotFound(jsonPath.getPath()) - } + queriedObj <- Try(document.read[java.lang.Object](jsonPath)).toEither.left.map { + case e: PathNotFoundException => JsonPathError.PathNotFound(jsonPath.getPath()) + case e => JsonPathError.UnexpectedReadPathError(jsonPath.getPath(), e) + } queriedJsonStr = jsonProvider.toJson(queriedObj) - queriedJson <- ZIO.fromEither(queriedJsonStr.fromJson[Json]).orDieWith(Exception(_)) + queriedJson <- queriedJsonStr + .fromJson[Json] + .left + .map(e => JsonPathError.UnexpectedReadPathError(jsonPath.getPath(), Exception(e))) } yield queriedJson } } diff --git a/shared/json/src/test/scala/org/hyperledger/identus/shared/json/JsonPathSpec.scala b/shared/json/src/test/scala/org/hyperledger/identus/shared/json/JsonPathSpec.scala index 5f4d41c612..4158ee323a 100644 --- a/shared/json/src/test/scala/org/hyperledger/identus/shared/json/JsonPathSpec.scala +++ b/shared/json/src/test/scala/org/hyperledger/identus/shared/json/JsonPathSpec.scala @@ -28,7 +28,7 @@ object JsonPathSpec extends ZIOSpecDefault { "$['foo']['bar']" ) ZIO - .foreach(paths)(JsonPath.compile) + .foreach(paths)(p => ZIO.fromEither(JsonPath.compile(p))) .as(assertCompletes) }, test("do not accept invalid json path") { @@ -40,7 +40,7 @@ object JsonPathSpec extends ZIOSpecDefault { "hello world", ) ZIO - .foreach(paths)(p => JsonPath.compile(p).flip) + .foreach(paths)(p => ZIO.fromEither(JsonPath.compile(p)).flip) .map { errors => assert(errors)(forall(isSubtype[InvalidPathInput](anything))) } @@ -60,20 +60,20 @@ object JsonPathSpec extends ZIOSpecDefault { """.stripMargin for { json <- ZIO.fromEither(jsonStr.fromJson[Json]) - namePath <- JsonPath.compile("$.vc.name") - agePath <- JsonPath.compile("$.vc.age") - degreePath <- JsonPath.compile("$.vc.degree") - petPath <- JsonPath.compile("$.vc.pets") - firstPetPath <- JsonPath.compile("$.vc.pets[0]") - isEmployedPath <- JsonPath.compile("$.vc.isEmployed") - languagesPath <- JsonPath.compile("$.vc.languages") - name <- namePath.read(json) - age <- agePath.read(json) - degree <- degreePath.read(json) - pet <- petPath.read(json) - firstPet <- firstPetPath.read(json) - isEmployed <- isEmployedPath.read(json) - languages <- languagesPath.read(json) + namePath <- ZIO.fromEither(JsonPath.compile("$.vc.name")) + agePath <- ZIO.fromEither(JsonPath.compile("$.vc.age")) + degreePath <- ZIO.fromEither(JsonPath.compile("$.vc.degree")) + petPath <- ZIO.fromEither(JsonPath.compile("$.vc.pets")) + firstPetPath <- ZIO.fromEither(JsonPath.compile("$.vc.pets[0]")) + isEmployedPath <- ZIO.fromEither(JsonPath.compile("$.vc.isEmployed")) + languagesPath <- ZIO.fromEither(JsonPath.compile("$.vc.languages")) + name <- ZIO.fromEither(namePath.read(json)) + age <- ZIO.fromEither(agePath.read(json)) + degree <- ZIO.fromEither(degreePath.read(json)) + pet <- ZIO.fromEither(petPath.read(json)) + firstPet <- ZIO.fromEither(firstPetPath.read(json)) + isEmployed <- ZIO.fromEither(isEmployedPath.read(json)) + languages <- ZIO.fromEither(languagesPath.read(json)) } yield assert(name.asString)(isSome(equalTo("alice"))) && assert(age.asNumber)(isSome(equalTo(Json.Num(42)))) && assert(degree.asNull)(isSome(anything)) @@ -93,14 +93,14 @@ object JsonPathSpec extends ZIOSpecDefault { """.stripMargin for { json <- ZIO.fromEither(jsonStr.fromJson[Json]) - nonExistingPath <- JsonPath.compile("$.vc2.name") - invalidTypeArrayPath <- JsonPath.compile("$.vc.name[0]") - outOfBoundArrayPath <- JsonPath.compile("$.vc.name[5]") - outOfBoundSlicePath <- JsonPath.compile("$.vc.name[1:4]") - exit1 <- nonExistingPath.read(json).exit - exit2 <- invalidTypeArrayPath.read(json).exit - exit3 <- outOfBoundArrayPath.read(json).exit - exit4 <- outOfBoundSlicePath.read(json).exit + nonExistingPath <- ZIO.fromEither(JsonPath.compile("$.vc2.name")) + invalidTypeArrayPath <- ZIO.fromEither(JsonPath.compile("$.vc.name[0]")) + outOfBoundArrayPath <- ZIO.fromEither(JsonPath.compile("$.vc.name[5]")) + outOfBoundSlicePath <- ZIO.fromEither(JsonPath.compile("$.vc.name[1:4]")) + exit1 <- ZIO.fromEither(nonExistingPath.read(json)).exit + exit2 <- ZIO.fromEither(invalidTypeArrayPath.read(json)).exit + exit3 <- ZIO.fromEither(outOfBoundArrayPath.read(json)).exit + exit4 <- ZIO.fromEither(outOfBoundSlicePath.read(json)).exit } yield assert(exit1)(failsWithA[PathNotFound]) && assert(exit2)(failsWithA[PathNotFound]) && assert(exit3)(failsWithA[PathNotFound]) From 85bbb1583188b5cba0f3f127ffdeff352375e93b Mon Sep 17 00:00:00 2001 From: Pat Losoponkul Date: Tue, 10 Sep 2024 15:21:24 +0700 Subject: [PATCH 2/5] test: more unhappy path tests Signed-off-by: Pat Losoponkul --- ...esentationSubmissionVerificationSpec.scala | 61 +++++++++++++++++-- 1 file changed, 56 insertions(+), 5 deletions(-) diff --git a/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerificationSpec.scala b/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerificationSpec.scala index cde4360ddb..4f9e3a422e 100644 --- a/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerificationSpec.scala +++ b/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerificationSpec.scala @@ -7,6 +7,8 @@ import org.hyperledger.identus.pollux.prex.PresentationSubmissionError.{ InvalidSubmissionId, SubmissionNotSatisfyInputDescriptors } +import org.hyperledger.identus.pollux.prex.PresentationSubmissionError.ClaimFormatVerificationFailure +import org.hyperledger.identus.pollux.prex.PresentationSubmissionError.ClaimNotSatisfyInputConstraint import org.hyperledger.identus.pollux.vc.jwt.{ DID, ES256KSigner, @@ -123,7 +125,12 @@ object PresentationSubmissionVerificationSpec extends ZIOSpecDefault { JwtPresentation.encodeJwt(payload, issuer) } - private def assertSubmissionVerification(descriptorsJson: String, descriptorMapJson: String, jwt: JWT)( + private def assertSubmissionVerification( + descriptorsJson: String, + descriptorMapJson: String, + jwt: JWT, + formatVerification: ClaimFormatVerification = noopFormatVerification + )( assertion: Assertion[Exit[PresentationSubmissionError, Unit]] ) = { val descriptors = decodeUnsafe[Seq[InputDescriptor]](descriptorsJson) @@ -132,7 +139,7 @@ object PresentationSubmissionVerificationSpec extends ZIOSpecDefault { val ps = basePs.copy(descriptor_map = descriptorMap) for { result <- PresentationSubmissionVerification - .verify(pd, ps, ZioJson.Str(jwt.value))(noopFormatVerification) + .verify(pd, ps, ZioJson.Str(jwt.value))(formatVerification) .exit } yield assert(result)(assertion) } @@ -292,6 +299,34 @@ object PresentationSubmissionVerificationSpec extends ZIOSpecDefault { jwtVc, )(succeeds(anything)) }, + test("descriptor and submission that dosn't satisfy the filter") { + val payload = generateVcPayload(parseUnsafe("""{"name": "alice"}""")) + val jwtVc = generateJwtVc(payload) + assertSubmissionVerification( + """[ + | { + | "id": "university_degree", + | "constraints": { + | "fields": [ + | { + | "path": ["$.vc.credentialSubject.name"], + | "filter": { + | "type": "string", + | "const": "bob" + | } + | } + | ] + | } + | } + |] + """.stripMargin, + """[ + | {"id": "university_degree", "format": "jwt_vc", "path": "$"} + |] + """.stripMargin, + jwtVc + )(failsWithA[ClaimNotSatisfyInputConstraint]) + }, test("descriptor with multiple fields verification") { val payload = generateVcPayload(parseUnsafe("""{"name": "alice", "degree": "Finance"}""")) val jwtVc = generateJwtVc(payload) @@ -489,10 +524,26 @@ object PresentationSubmissionVerificationSpec extends ZIOSpecDefault { """.stripMargin, jwtVc )(succeeds(anything)) + }, + test("descriptor and submission that fail the claim format decoding") { + val formatVerification = noopFormatVerification.copy( + jwtVc = _ => ZIO.fail("jwt is missing some required properties") + ) + val payload = generateVcPayload(parseUnsafe("""{"name": "alice"}""")) + val jwtVc = generateJwtVc(payload) + assertSubmissionVerification( + """[ + | {"id": "university_degree", "constraints": {}} + |] + """.stripMargin, + """[ + | {"id": "university_degree", "format": "jwt_vc", "path": "$"} + |] + """.stripMargin, + jwtVc, + formatVerification + )(failsWithA[ClaimFormatVerificationFailure]) } ) - /* TODO - * - test for format verification failure - */ } From f9d6880bb99a94a7a6ed2afb3b92ded4e92640da Mon Sep 17 00:00:00 2001 From: Pat Losoponkul Date: Tue, 10 Sep 2024 15:27:13 +0700 Subject: [PATCH 3/5] chore: fix and fmt Signed-off-by: Pat Losoponkul --- .../PresentationSubmissionVerification.scala | 2 +- .../PresentationSubmissionVerificationSpec.scala | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pollux/prex/src/main/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerification.scala b/pollux/prex/src/main/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerification.scala index f0e616a795..b708a4ce33 100644 --- a/pollux/prex/src/main/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerification.scala +++ b/pollux/prex/src/main/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerification.scala @@ -8,11 +8,11 @@ import org.hyperledger.identus.pollux.prex.PresentationSubmissionError.{ ClaimNotSatisfyInputConstraint, InvalidDataTypeForClaimFormat, InvalidJsonPath, + InvalidNestedPathDescriptorId, InvalidSubmissionId, JsonPathNotFound, SubmissionNotSatisfyInputDescriptors } -import org.hyperledger.identus.pollux.prex.PresentationSubmissionError.InvalidNestedPathDescriptorId import org.hyperledger.identus.pollux.vc.jwt.{JWT, JwtCredential, JwtPresentation} import org.hyperledger.identus.pollux.vc.jwt.CredentialPayload.Implicits.* import org.hyperledger.identus.pollux.vc.jwt.PresentationPayload.Implicits.* diff --git a/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerificationSpec.scala b/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerificationSpec.scala index 4f9e3a422e..f813cad3d2 100644 --- a/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerificationSpec.scala +++ b/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerificationSpec.scala @@ -3,12 +3,12 @@ package org.hyperledger.identus.pollux.prex import io.circe.* import io.circe.parser.* import org.hyperledger.identus.pollux.prex.PresentationSubmissionError.{ + ClaimFormatVerificationFailure, + ClaimNotSatisfyInputConstraint, InvalidNestedPathDescriptorId, InvalidSubmissionId, SubmissionNotSatisfyInputDescriptors } -import org.hyperledger.identus.pollux.prex.PresentationSubmissionError.ClaimFormatVerificationFailure -import org.hyperledger.identus.pollux.prex.PresentationSubmissionError.ClaimNotSatisfyInputConstraint import org.hyperledger.identus.pollux.vc.jwt.{ DID, ES256KSigner, @@ -16,13 +16,13 @@ import org.hyperledger.identus.pollux.vc.jwt.{ JWT, JwtCredential, JwtCredentialPayload, - JwtVc + JwtPresentation, + JwtPresentationPayload, + JwtVc, + JwtVerifiableCredentialPayload, + JwtVp, + VerifiableCredentialPayload } -import org.hyperledger.identus.pollux.vc.jwt.JwtPresentation -import org.hyperledger.identus.pollux.vc.jwt.JwtPresentationPayload -import org.hyperledger.identus.pollux.vc.jwt.JwtVerifiableCredentialPayload -import org.hyperledger.identus.pollux.vc.jwt.JwtVp -import org.hyperledger.identus.pollux.vc.jwt.VerifiableCredentialPayload import org.hyperledger.identus.shared.crypto.Apollo import zio.* import zio.json.ast.Json as ZioJson From cfe0144f4ef50572645aa2793bbf2c6ae64e9713 Mon Sep 17 00:00:00 2001 From: Pat Losoponkul Date: Tue, 10 Sep 2024 15:33:20 +0700 Subject: [PATCH 4/5] chore: pr cleanup Signed-off-by: Pat Losoponkul --- build.sbt | 2 +- .../PresentationSubmissionVerification.scala | 30 ++++++++----------- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/build.sbt b/build.sbt index 097d9d9115..6624b043c7 100644 --- a/build.sbt +++ b/build.sbt @@ -32,7 +32,7 @@ inThisBuild( "-unchecked", ), scalacOptions += "-Wunused:all", - // scalacOptions += "-Wconf:cat=deprecation:warning,any:error", // "-Wconf:help", // TODO: revert before pr + scalacOptions += "-Wconf:cat=deprecation:warning,any:error", // "-Wconf:help", // scalacOptions += "-Yexplicit-nulls", // scalacOptions += "-Ysafe-init", // scalacOptions += "-Werror", // <=> "-Xfatal-warnings" diff --git a/pollux/prex/src/main/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerification.scala b/pollux/prex/src/main/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerification.scala index b708a4ce33..fe167a1122 100644 --- a/pollux/prex/src/main/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerification.scala +++ b/pollux/prex/src/main/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerification.scala @@ -87,6 +87,8 @@ case class ClaimFormatVerification( jwtVc: JWT => IO[String, Unit], ) +// Known issues +// 1. does not respect jwt format alg in presentation_definition object PresentationSubmissionVerification { def verify( @@ -114,8 +116,8 @@ object PresentationSubmissionVerification { else ZIO.fail(InvalidSubmissionId(pd.id, ps.id)) } - // This is not yet fully supported as in https://identity.foundation/presentation-exchange/spec/v2.1.1/#submission-requirement-feature - // It is now a simple check that submission satisties all input descriptors + // This is not yet fully supported as described in https://identity.foundation/presentation-exchange/spec/v2.1.1/#submission-requirement-feature + // It is now a simple check that submission descriptor_map satisfies all input_descriptors private def verifySubmissionRequirement( pd: PresentationDefinition, ps: PresentationSubmission @@ -144,10 +146,13 @@ object PresentationSubmissionVerification { descriptor: InputDescriptor, entry: ZioJson ): IO[PresentationSubmissionError, Unit] = { - val fields = descriptor.constraints.fields.getOrElse(Nil) + val mandatoryFields = descriptor.constraints.fields + .getOrElse(Nil) + .filterNot(_.optional.getOrElse(false)) // optional field doesn't have to pass contraints + // all fields need to be valid ZIO - .foreach(fields) { field => + .foreach(mandatoryFields) { field => // only one of the paths need to be valid ZIO .validateFirst(field.path) { p => @@ -155,18 +160,9 @@ object PresentationSubmissionVerification { jsonPath <- ZIO.fromEither(p.toJsonPath) jsonAtPath <- ZIO.fromEither(jsonPath.read(entry)) maybeFilter <- ZIO.foreach(field.filter)(_.toJsonSchema) - _ <- ZIO.foreach(maybeFilter) { filter => - JsonSchemaValidatorImpl(filter).validate(jsonAtPath.toString()) - } + _ <- ZIO.foreach(maybeFilter)(JsonSchemaValidatorImpl(_).validate(jsonAtPath.toString())) } yield () } - .catchAll { errors => - // if all paths don't satisfy constraints, but optional, then the field is still valid - // https://identity.foundation/presentation-exchange/spec/v2.1.1/#input-evaluation - if field.optional.getOrElse(false) - then ZIO.unit - else ZIO.fail(errors) - } .mapError(_ => ClaimNotSatisfyInputConstraint(descriptor.id)) } .unit @@ -184,10 +180,8 @@ object PresentationSubmissionVerification { .fromEither(path.read(traversalObject)) .mapError(_ => JsonPathNotFound(descriptor.path)) currentNode <- descriptor.format match { - case ClaimFormatValue.jwt_vc => - verifyJwtVc(jsonAtPath, descriptor.path)(formatVerification.jwtVc) - case ClaimFormatValue.jwt_vp => - verifyJwtVp(jsonAtPath, descriptor.path)(formatVerification.jwtVp) + case ClaimFormatValue.jwt_vc => verifyJwtVc(jsonAtPath, descriptor.path)(formatVerification.jwtVc) + case ClaimFormatValue.jwt_vp => verifyJwtVp(jsonAtPath, descriptor.path)(formatVerification.jwtVp) } leafNode <- descriptor.path_nested.fold(ZIO.succeed(currentNode)) { nestedDescriptor => if descriptor.id != nestedDescriptor.id From 32de11aa30e0e4e11742c6132fec6f8212df1b20 Mon Sep 17 00:00:00 2001 From: Pat Losoponkul Date: Wed, 11 Sep 2024 14:45:51 +0700 Subject: [PATCH 5/5] test: make test work again Signed-off-by: Pat Losoponkul --- .../prex/PresentationSubmissionVerificationSpec.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerificationSpec.scala b/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerificationSpec.scala index f813cad3d2..60b025826e 100644 --- a/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerificationSpec.scala +++ b/pollux/prex/src/test/scala/org/hyperledger/identus/pollux/prex/PresentationSubmissionVerificationSpec.scala @@ -2,6 +2,7 @@ package org.hyperledger.identus.pollux.prex import io.circe.* import io.circe.parser.* +import org.hyperledger.identus.castor.core.model.did.DID import org.hyperledger.identus.pollux.prex.PresentationSubmissionError.{ ClaimFormatVerificationFailure, ClaimNotSatisfyInputConstraint, @@ -10,7 +11,6 @@ import org.hyperledger.identus.pollux.prex.PresentationSubmissionError.{ SubmissionNotSatisfyInputDescriptors } import org.hyperledger.identus.pollux.vc.jwt.{ - DID, ES256KSigner, Issuer, JWT, @@ -106,7 +106,7 @@ object PresentationSubmissionVerificationSpec extends ZIOSpecDefault { val publicKey = keyPair.publicKey val privateKey = keyPair.privateKey val issuer = Issuer( - DID(payload.iss), + DID.fromString(payload.iss).toOption.get, ES256KSigner(privateKey.toJavaPrivateKey, None), publicKey.toJavaPublicKey ) @@ -118,7 +118,7 @@ object PresentationSubmissionVerificationSpec extends ZIOSpecDefault { val publicKey = keyPair.publicKey val privateKey = keyPair.privateKey val issuer = Issuer( - DID(payload.iss), + DID.fromString(payload.iss).toOption.get, ES256KSigner(privateKey.toJavaPrivateKey, None), publicKey.toJavaPublicKey )