diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/verification/controller/VcVerificationControllerImplSpec.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/verification/controller/VcVerificationControllerImplSpec.scala index c1e77224cb..b2e0fcf012 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/verification/controller/VcVerificationControllerImplSpec.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/verification/controller/VcVerificationControllerImplSpec.scala @@ -42,9 +42,11 @@ object VcVerificationControllerImplSpec extends ZIOSpecDefault with VcVerificati maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - CredentialSchema( - id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", - `type` = "JsonSchemaValidator2018" + Left( + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" + ) ) ), credentialSubject = Json.obj( diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala index ff8f67acc3..d83b8390ec 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala @@ -1137,8 +1137,9 @@ class CredentialServiceImpl( issuer = Right(CredentialIssuer(jwtIssuer.did.toString, `type` = "Profile")), issuanceDate = issuanceDate, maybeExpirationDate = record.validityPeriod.map(sec => issuanceDate.plusSeconds(sec.toLong)), - maybeCredentialSchema = - record.schemaUri.map(id => org.hyperledger.identus.pollux.vc.jwt.CredentialSchema(id, VC_JSON_SCHEMA_TYPE)), + maybeCredentialSchema = record.schemaUri.map(id => + Left(org.hyperledger.identus.pollux.vc.jwt.CredentialSchema(id, VC_JSON_SCHEMA_TYPE)) + ), maybeCredentialStatus = Some(credentialStatus), credentialSubject = claims.add("id", jwtPresentation.iss.asJson).asJson, maybeRefreshService = None, diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImpl.scala index 4f7f0f48a3..18b5370bb5 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImpl.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImpl.scala @@ -53,12 +53,18 @@ class VcVerificationServiceImpl(didResolver: DidResolver, uriDereferencer: URIDe ZIO .fromOption(decodedJwt.maybeCredentialSchema) .mapError(error => VcVerificationServiceError.UnexpectedError(s"Missing Credential Schema: $error")) - result <- CredentialSchema - .validSchemaValidator( - credentialSchema.id, - uriDereferencer + credentialSchemas = credentialSchema.fold(List(_), identity) + result <- + ZIO.collectAll( + credentialSchemas.map(credentialSchema => + CredentialSchema + .validSchemaValidator( + credentialSchema.id, + uriDereferencer + ) + .mapError(error => VcVerificationServiceError.UnexpectedError(s"Schema Validator Failed: $error")) + ) ) - .mapError(error => VcVerificationServiceError.UnexpectedError(s"Schema Validator Failed: $error")) } yield result result @@ -91,14 +97,20 @@ class VcVerificationServiceImpl(didResolver: DidResolver, uriDereferencer: URIDe ZIO .fromOption(decodedJwt.maybeCredentialSchema) .mapError(error => VcVerificationServiceError.UnexpectedError(s"Missing Credential Schema: $error")) - result <- CredentialSchema - .validateJWTCredentialSubject( - credentialSchema.id, - decodedJwt.credentialSubject.noSpaces, - uriDereferencer - ) - .mapError(error => - VcVerificationServiceError.UnexpectedError(s"JWT Credential Subject Validation Failed: $error") + credentialSchemas = credentialSchema.fold(List(_), identity) + result <- + ZIO.collectAll( + credentialSchemas.map(credentialSchema => + CredentialSchema + .validateJWTCredentialSubject( + credentialSchema.id, + decodedJwt.credentialSubject.noSpaces, + uriDereferencer + ) + .mapError(error => + VcVerificationServiceError.UnexpectedError(s"JWT Credential Subject Validation Failed: $error") + ) + ) ) } yield result diff --git a/pollux/core/src/test/resources/vc-schema-driver-license.json b/pollux/core/src/test/resources/vc-schema-driver-license.json new file mode 100644 index 0000000000..e5a9f20cb7 --- /dev/null +++ b/pollux/core/src/test/resources/vc-schema-driver-license.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Driving License", + "type": "object", + "properties": { + "credentialSubject": { + "type": "object", + "properties": { + "dateOfIssuance": { + "type": "string", + "format": "date-time" + }, + "drivingLicenseID": { + "type": "string" + }, + "drivingClass": { + "type": "integer" + } + }, + "required": ["dateOfIssuance", "drivingLicenseID", "drivingClass"], + "additionalProperties": false + } + }, + "required": ["credentialSubject"], + "additionalProperties": false +} diff --git a/pollux/core/src/test/resources/vc-schema-example.json b/pollux/core/src/test/resources/vc-schema-example.json index cc2b777de8..de6d6f5617 100644 --- a/pollux/core/src/test/resources/vc-schema-example.json +++ b/pollux/core/src/test/resources/vc-schema-example.json @@ -37,8 +37,6 @@ "additionalProperties": false } }, - "required": [ - "credentialSubject" - ], + "required": ["credentialSubject"], "additionalProperties": false -} \ No newline at end of file +} diff --git a/pollux/core/src/test/resources/vc-schema-personal.json b/pollux/core/src/test/resources/vc-schema-personal.json new file mode 100644 index 0000000000..3f2d0e9b15 --- /dev/null +++ b/pollux/core/src/test/resources/vc-schema-personal.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Age", + "type": "object", + "properties": { + "credentialSubject": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email" + }, + "userName": { + "type": "string" + }, + "age": { + "type": "integer" + } + }, + "required": ["email", "userName", "age"], + "additionalProperties": false + } + }, + "required": ["credentialSubject"], + "additionalProperties": false +} diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImplSpec.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImplSpec.scala index 05af3c273e..3d9f41da04 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImplSpec.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/verification/VcVerificationServiceImplSpec.scala @@ -33,9 +33,11 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - CredentialSchema( - id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", - `type` = "JsonSchemaValidator2018" + Left( + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" + ) ) ), credentialSubject = Json.obj( @@ -98,9 +100,11 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - CredentialSchema( - id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", - `type` = "JsonSchemaValidator2018" + Left( + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" + ) ) ), credentialSubject = Json.obj( @@ -163,9 +167,11 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - CredentialSchema( - id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", - `type` = "JsonSchemaValidator2018" + Left( + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" + ) ) ), credentialSubject = Json.obj( @@ -228,9 +234,11 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - CredentialSchema( - id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", - `type` = "JsonSchemaValidator2018" + Left( + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" + ) ) ), credentialSubject = Json.obj( @@ -300,9 +308,11 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - CredentialSchema( - id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", - `type` = "JsonSchemaValidator2018" + Left( + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" + ) ) ), credentialSubject = Json.obj( @@ -354,6 +364,157 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS someVcVerificationServiceLayer ++ ZLayer.succeed(WalletAccessContext(WalletId.random)) ), + test("verify schema given single schema") { + for { + svc <- ZIO.service[VcVerificationService] + verifier = "did:prism:verifier" + jwtCredentialPayload = W3cCredentialPayload( + `@context` = + Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), + maybeId = Some("http://example.edu/credentials/3732"), + `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), + issuer = Left(issuer.did.toString), + issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), + maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), + maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), + maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), + maybeCredentialSchema = Some( + Left( + CredentialSchema( + id = "resource:///vc-schema-personal.json", + `type` = "JsonSchemaValidator2018" + ) + ) + ), + credentialSubject = Json.obj( + "userName" -> Json.fromString("Alice"), + "age" -> Json.fromInt(42), + "email" -> Json.fromString("alice@wonderland.com") + ), + maybeCredentialStatus = Some( + CredentialStatus( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "StatusList2021Entry", + statusPurpose = StatusPurpose.Revocation, + statusListIndex = 0, + statusListCredential = "https://example.com/credentials/status/3" + ) + ), + maybeRefreshService = Some( + RefreshService( + id = "https://example.edu/refresh/3732", + `type` = "ManualRefreshService2018" + ) + ), + maybeEvidence = Option.empty, + maybeTermsOfUse = Option.empty, + aud = Set(verifier) + ).toJwtCredentialPayload + signedJwtCredential = issuer.signer.encode(jwtCredentialPayload.asJson) + result <- + svc.verify( + List( + VcVerificationRequest(signedJwtCredential.value, VcVerification.SchemaCheck) + ) + ) + } yield { + assertTrue( + result.contains( + VcVerificationResult( + signedJwtCredential.value, + VcVerification.SchemaCheck, + true + ) + ) + ) + } + }.provideSomeLayer( + MockDIDService.empty ++ + MockManagedDIDService.empty ++ + ResourceURIDereferencerImpl.layer >+> + someVcVerificationServiceLayer ++ + ZLayer.succeed(WalletAccessContext(WalletId.random)) + ), + test("verify schema given multiple schema") { + for { + svc <- ZIO.service[VcVerificationService] + verifier = "did:prism:verifier" + jwtCredentialPayload = W3cCredentialPayload( + `@context` = + Set("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1"), + maybeId = Some("http://example.edu/credentials/3732"), + `type` = Set("VerifiableCredential", "UniversityDegreeCredential"), + issuer = Left(issuer.did.toString), + issuanceDate = Instant.parse("2010-01-01T00:00:00Z"), + maybeExpirationDate = Some(Instant.parse("2010-01-12T00:00:00Z")), + maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), + maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), + maybeCredentialSchema = Some( + Right( + List( + CredentialSchema( + id = "resource:///vc-schema-personal.json", + `type` = "JsonSchemaValidator2018" + ), + CredentialSchema( + id = "resource:///vc-schema-driver-license.json", + `type` = "JsonSchemaValidator2018" + ) + ) + ) + ), + credentialSubject = Json.obj( + "userName" -> Json.fromString("Alice"), + "age" -> Json.fromInt(42), + "email" -> Json.fromString("alice@wonderland.com"), + "dateOfIssuance" -> Json.fromString("2000-01-01T10:00:00Z"), + "drivingLicenseID" -> Json.fromInt(12345), + "drivingClass" -> Json.fromString("5") + ), + maybeCredentialStatus = Some( + CredentialStatus( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "StatusList2021Entry", + statusPurpose = StatusPurpose.Revocation, + statusListIndex = 0, + statusListCredential = "https://example.com/credentials/status/3" + ) + ), + maybeRefreshService = Some( + RefreshService( + id = "https://example.edu/refresh/3732", + `type` = "ManualRefreshService2018" + ) + ), + maybeEvidence = Option.empty, + maybeTermsOfUse = Option.empty, + aud = Set(verifier) + ).toJwtCredentialPayload + signedJwtCredential = issuer.signer.encode(jwtCredentialPayload.asJson) + result <- + svc.verify( + List( + VcVerificationRequest(signedJwtCredential.value, VcVerification.SchemaCheck) + ) + ) + } yield { + assertTrue( + result.contains( + VcVerificationResult( + signedJwtCredential.value, + VcVerification.SchemaCheck, + true + ) + ) + ) + } + }.provideSomeLayer( + MockDIDService.empty ++ + MockManagedDIDService.empty ++ + ResourceURIDereferencerImpl.layer >+> + someVcVerificationServiceLayer ++ + ZLayer.succeed(WalletAccessContext(WalletId.random)) + ), test("verify nbf given valid") { for { svc <- ZIO.service[VcVerificationService] @@ -370,9 +531,11 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - CredentialSchema( - id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", - `type` = "JsonSchemaValidator2018" + Left( + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" + ) ) ), credentialSubject = Json.obj( @@ -436,9 +599,11 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - CredentialSchema( - id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", - `type` = "JsonSchemaValidator2018" + Left( + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" + ) ) ), credentialSubject = Json.obj( @@ -502,9 +667,11 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - CredentialSchema( - id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", - `type` = "JsonSchemaValidator2018" + Left( + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" + ) ) ), credentialSubject = Json.obj( @@ -568,9 +735,11 @@ object VcVerificationServiceImplSpec extends ZIOSpecDefault with VcVerificationS maybeValidFrom = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeValidUntil = Some(Instant.parse("2010-01-12T00:00:00Z")), maybeCredentialSchema = Some( - CredentialSchema( - id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", - `type` = "JsonSchemaValidator2018" + Left( + CredentialSchema( + id = "did:work:MDP8AsFhHzhwUvGNuYkX7T;id=06e126d1-fa44-4882-a243-1e326fbe21db;version=1.0", + `type` = "JsonSchemaValidator2018" + ) ) ), credentialSubject = Json.obj( diff --git a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala index da442b7397..cb7d84dd0b 100644 --- a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala +++ b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala @@ -87,7 +87,7 @@ sealed trait CredentialPayload { def maybeTermsOfUse: Option[Json] - def maybeCredentialSchema: Option[CredentialSchema] + def maybeCredentialSchema: Option[Either[CredentialSchema, List[CredentialSchema]]] def credentialSubject: Json @@ -137,7 +137,7 @@ sealed trait CredentialPayload { case class JwtVc( `@context`: Set[String], `type`: Set[String], - maybeCredentialSchema: Option[CredentialSchema], + maybeCredentialSchema: Option[Either[CredentialSchema, List[CredentialSchema]]], credentialSubject: Json, maybeValidFrom: Option[Instant], maybeValidUntil: Option[Instant], @@ -177,7 +177,7 @@ case class W3cCredentialPayload( issuer: Either[String, CredentialIssuer], issuanceDate: Instant, maybeExpirationDate: Option[Instant], - override val maybeCredentialSchema: Option[CredentialSchema], + override val maybeCredentialSchema: Option[Either[CredentialSchema, List[CredentialSchema]]], override val credentialSubject: Json, override val maybeCredentialStatus: Option[CredentialStatus], override val maybeRefreshService: Option[RefreshService], @@ -241,8 +241,10 @@ object CredentialPayload { case Right(issuer) => issuer.asJson } - implicit val eitherStringOrCredentialIssuerDecoder: Decoder[Either[String, CredentialIssuer]] = - Decoder[String].map(Left(_)).or(Decoder[CredentialIssuer].map(Right(_))) + implicit val eitherCredentialSchemaOrListEncoder: Encoder[Either[CredentialSchema, List[CredentialSchema]]] = { + case Left(credentialSchema) => credentialSchema.asJson + case Right(credentialSchemas) => credentialSchemas.asJson + } implicit val w3cCredentialPayloadEncoder: Encoder[W3cCredentialPayload] = (w3cCredentialPayload: W3cCredentialPayload) => @@ -368,6 +370,14 @@ object CredentialPayload { ) } + implicit val eitherStringOrCredentialIssuerDecoder: Decoder[Either[String, CredentialIssuer]] = + Decoder[String].map(Left(_)).or(Decoder[CredentialIssuer].map(Right(_))) + + implicit val eitherCredentialSchemaOrListDecoder: Decoder[Either[CredentialSchema, List[CredentialSchema]]] = + Decoder[CredentialSchema] + .map(Left(_)) + .or(Decoder[List[CredentialSchema]].map(Right(_))) + implicit val w3cCredentialPayloadDecoder: Decoder[W3cCredentialPayload] = (c: HCursor) => for { @@ -385,7 +395,9 @@ object CredentialPayload { maybeExpirationDate <- c.downField("expirationDate").as[Option[Instant]] maybeValidFrom <- c.downField("validFrom").as[Option[Instant]] maybeValidUntil <- c.downField("validUntil").as[Option[Instant]] - maybeCredentialSchema <- c.downField("credentialSchema").as[Option[CredentialSchema]] + maybeCredentialSchema <- c + .downField("credentialSchema") + .as[Option[Either[CredentialSchema, List[CredentialSchema]]]] credentialSubject <- c.downField("credentialSubject").as[Json] maybeCredentialStatus <- c.downField("credentialStatus").as[Option[CredentialStatus]] maybeRefreshService <- c.downField("refreshService").as[Option[RefreshService]] @@ -422,7 +434,9 @@ object CredentialPayload { .downField("type") .as[Set[String]] .orElse(c.downField("type").as[String].map(Set(_))) - maybeCredentialSchema <- c.downField("credentialSchema").as[Option[CredentialSchema]] + maybeCredentialSchema <- c + .downField("credentialSchema") + .as[Option[Either[CredentialSchema, List[CredentialSchema]]]] credentialSubject <- c.downField("credentialSubject").as[Json] maybeCredentialStatus <- c.downField("credentialStatus").as[Option[CredentialStatus]] maybeRefreshService <- c.downField("refreshService").as[Option[RefreshService]]