diff --git a/bindings/web5_uniffi/src/lib.rs b/bindings/web5_uniffi/src/lib.rs index 845e9f1e..b6a4e787 100644 --- a/bindings/web5_uniffi/src/lib.rs +++ b/bindings/web5_uniffi/src/lib.rs @@ -1,7 +1,11 @@ use web5_uniffi_wrapper::{ credentials::{ presentation_definition::PresentationDefinition, - verifiable_credential_1_1::{VerifiableCredential, VerifiableCredentialData}, + verifiable_credential_1_1::{ + VerifiableCredential, + VerifiableCredentialCreateOptions as VerifiableCredentialCreateOptionsData, + VerifiableCredentialData, + }, }, crypto::{ dsa::{ @@ -29,7 +33,7 @@ use web5_uniffi_wrapper::{ }; use web5::{ - credentials::verifiable_credential_1_1::VerifiableCredentialCreateOptions as VerifiableCredentialCreateOptionsData, + credentials::CredentialSchema as CredentialSchemaData, crypto::{dsa::Dsa, jwk::Jwk as JwkData}, dids::{ data_model::{ diff --git a/bindings/web5_uniffi/src/web5.udl b/bindings/web5_uniffi/src/web5.udl index 3488da6f..fca37de0 100644 --- a/bindings/web5_uniffi/src/web5.udl +++ b/bindings/web5_uniffi/src/web5.udl @@ -257,6 +257,8 @@ dictionary VerifiableCredentialCreateOptionsData { sequence? type; timestamp? issuance_date; timestamp? expiration_date; + CredentialSchemaData? credential_schema; + string? json_serialized_evidence; }; interface VerifiableCredential { @@ -266,6 +268,7 @@ interface VerifiableCredential { string json_serialized_credential_subject, VerifiableCredentialCreateOptionsData? options ); + [Throws=Web5Error] VerifiableCredentialData get_data(); [Throws=Web5Error, Name=from_vc_jwt] constructor(string vc_jwt, boolean verify); @@ -281,4 +284,11 @@ dictionary VerifiableCredentialData { string json_serialized_credential_subject; timestamp issuance_date; timestamp? expiration_date; + CredentialSchemaData? credential_schema; + string? json_serialized_evidence; +}; + +dictionary CredentialSchemaData { + string id; + string type; }; \ No newline at end of file diff --git a/bindings/web5_uniffi_wrapper/src/credentials/verifiable_credential_1_1.rs b/bindings/web5_uniffi_wrapper/src/credentials/verifiable_credential_1_1.rs index a1522a63..bf2649cd 100644 --- a/bindings/web5_uniffi_wrapper/src/credentials/verifiable_credential_1_1.rs +++ b/bindings/web5_uniffi_wrapper/src/credentials/verifiable_credential_1_1.rs @@ -3,13 +3,25 @@ use std::{sync::Arc, time::SystemTime}; use web5::{ credentials::{ verifiable_credential_1_1::{ - VerifiableCredential as InnerVerifiableCredential, VerifiableCredentialCreateOptions, + VerifiableCredential as InnerVerifiableCredential, + VerifiableCredentialCreateOptions as InnerVerifiableCredentialCreateOptions, }, - CredentialSubject, Issuer, + CredentialSchema, CredentialSubject, Issuer, }, - json::FromJson as _, + json::{FromJson as _, JsonObject}, }; +#[derive(Default)] +pub struct VerifiableCredentialCreateOptions { + pub id: Option, + pub context: Option>, + pub r#type: Option>, + pub issuance_date: Option, + pub expiration_date: Option, + pub credential_schema: Option, + pub json_serialized_evidence: Option, +} + pub struct VerifiableCredential { inner_vc: InnerVerifiableCredential, json_serialized_issuer: String, @@ -26,7 +38,25 @@ impl VerifiableCredential { let credential_subject = CredentialSubject::from_json_string(&json_serialized_credential_subject)?; - let inner_vc = InnerVerifiableCredential::create(issuer, credential_subject, options)?; + let options = options.unwrap_or_default(); + let evidence = match options.json_serialized_evidence { + Some(evidence_string) => { + Some(serde_json::from_str::>(&evidence_string)?) + } + None => None, + }; + let inner_options = InnerVerifiableCredentialCreateOptions { + id: options.id, + context: options.context, + r#type: options.r#type, + issuance_date: options.issuance_date, + expiration_date: options.expiration_date, + credential_schema: options.credential_schema, + evidence, + }; + + let inner_vc = + InnerVerifiableCredential::create(issuer, credential_subject, Some(inner_options))?; Ok(Self { inner_vc, @@ -35,8 +65,13 @@ impl VerifiableCredential { }) } - pub fn get_data(&self) -> VerifiableCredentialData { - VerifiableCredentialData { + pub fn get_data(&self) -> Result { + let json_serialized_evidence = match &self.inner_vc.evidence { + Some(e) => Some(serde_json::to_string(e)?), + None => None, + }; + + Ok(VerifiableCredentialData { context: self.inner_vc.context.clone(), id: self.inner_vc.id.clone(), r#type: self.inner_vc.r#type.clone(), @@ -44,7 +79,9 @@ impl VerifiableCredential { json_serialized_credential_subject: self.json_serialized_credential_subject.clone(), issuance_date: self.inner_vc.issuance_date, expiration_date: self.inner_vc.expiration_date, - } + credential_schema: self.inner_vc.credential_schema.clone(), + json_serialized_evidence, + }) } pub fn from_vc_jwt(vc_jwt: String, verify: bool) -> Result { @@ -79,4 +116,6 @@ pub struct VerifiableCredentialData { pub json_serialized_credential_subject: String, pub issuance_date: SystemTime, pub expiration_date: Option, + pub credential_schema: Option, + pub json_serialized_evidence: Option, } diff --git a/bindings/web5_uniffi_wrapper/src/errors.rs b/bindings/web5_uniffi_wrapper/src/errors.rs index cb9caa93..b98cc736 100644 --- a/bindings/web5_uniffi_wrapper/src/errors.rs +++ b/bindings/web5_uniffi_wrapper/src/errors.rs @@ -98,6 +98,7 @@ impl From for InnerWeb5Error { match variant.as_str() { "Json" => InnerWeb5Error::Json(msg), + "JsonSchema" => InnerWeb5Error::JsonSchema(msg), "Parameter" => InnerWeb5Error::Parameter(msg), "DataMember" => InnerWeb5Error::DataMember(msg), "NotFound" => InnerWeb5Error::NotFound(msg), diff --git a/bound/kt/src/main/kotlin/web5/sdk/rust/UniFFI.kt b/bound/kt/src/main/kotlin/web5/sdk/rust/UniFFI.kt index ee67c00f..c744941f 100644 --- a/bound/kt/src/main/kotlin/web5/sdk/rust/UniFFI.kt +++ b/bound/kt/src/main/kotlin/web5/sdk/rust/UniFFI.kt @@ -1414,7 +1414,7 @@ private fun uniffiCheckApiChecksums(lib: UniffiLib) { if (lib.uniffi_web5_uniffi_checksum_method_signer_sign() != 5738.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } - if (lib.uniffi_web5_uniffi_checksum_method_verifiablecredential_get_data() != 34047.toShort()) { + if (lib.uniffi_web5_uniffi_checksum_method_verifiablecredential_get_data() != 24514.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } if (lib.uniffi_web5_uniffi_checksum_method_verifiablecredential_sign() != 21705.toShort()) { @@ -5332,10 +5332,11 @@ open class VerifiableCredential: Disposable, AutoCloseable, VerifiableCredential } } - override fun `getData`(): VerifiableCredentialData { + + @Throws(Web5Exception::class)override fun `getData`(): VerifiableCredentialData { return FfiConverterTypeVerifiableCredentialData.lift( callWithPointer { - uniffiRustCall() { _status -> + uniffiRustCallWithError(Web5Exception) { _status -> UniffiLib.INSTANCE.uniffi_web5_uniffi_fn_method_verifiablecredential_get_data( it, _status) } @@ -5730,6 +5731,35 @@ public object FfiConverterTypeBearerDidData: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): CredentialSchemaData { + return CredentialSchemaData( + FfiConverterString.read(buf), + FfiConverterString.read(buf), + ) + } + + override fun allocationSize(value: CredentialSchemaData) = ( + FfiConverterString.allocationSize(value.`id`) + + FfiConverterString.allocationSize(value.`type`) + ) + + override fun write(value: CredentialSchemaData, buf: ByteBuffer) { + FfiConverterString.write(value.`id`, buf) + FfiConverterString.write(value.`type`, buf) + } +} + + + data class DidData ( var `uri`: kotlin.String, var `url`: kotlin.String, @@ -6231,7 +6261,9 @@ data class VerifiableCredentialCreateOptionsData ( var `context`: List?, var `type`: List?, var `issuanceDate`: java.time.Instant?, - var `expirationDate`: java.time.Instant? + var `expirationDate`: java.time.Instant?, + var `credentialSchema`: CredentialSchemaData?, + var `jsonSerializedEvidence`: kotlin.String? ) { companion object @@ -6245,6 +6277,8 @@ public object FfiConverterTypeVerifiableCredentialCreateOptionsData: FfiConverte FfiConverterOptionalSequenceString.read(buf), FfiConverterOptionalTimestamp.read(buf), FfiConverterOptionalTimestamp.read(buf), + FfiConverterOptionalTypeCredentialSchemaData.read(buf), + FfiConverterOptionalString.read(buf), ) } @@ -6253,7 +6287,9 @@ public object FfiConverterTypeVerifiableCredentialCreateOptionsData: FfiConverte FfiConverterOptionalSequenceString.allocationSize(value.`context`) + FfiConverterOptionalSequenceString.allocationSize(value.`type`) + FfiConverterOptionalTimestamp.allocationSize(value.`issuanceDate`) + - FfiConverterOptionalTimestamp.allocationSize(value.`expirationDate`) + FfiConverterOptionalTimestamp.allocationSize(value.`expirationDate`) + + FfiConverterOptionalTypeCredentialSchemaData.allocationSize(value.`credentialSchema`) + + FfiConverterOptionalString.allocationSize(value.`jsonSerializedEvidence`) ) override fun write(value: VerifiableCredentialCreateOptionsData, buf: ByteBuffer) { @@ -6262,6 +6298,8 @@ public object FfiConverterTypeVerifiableCredentialCreateOptionsData: FfiConverte FfiConverterOptionalSequenceString.write(value.`type`, buf) FfiConverterOptionalTimestamp.write(value.`issuanceDate`, buf) FfiConverterOptionalTimestamp.write(value.`expirationDate`, buf) + FfiConverterOptionalTypeCredentialSchemaData.write(value.`credentialSchema`, buf) + FfiConverterOptionalString.write(value.`jsonSerializedEvidence`, buf) } } @@ -6274,7 +6312,9 @@ data class VerifiableCredentialData ( var `jsonSerializedIssuer`: kotlin.String, var `jsonSerializedCredentialSubject`: kotlin.String, var `issuanceDate`: java.time.Instant, - var `expirationDate`: java.time.Instant? + var `expirationDate`: java.time.Instant?, + var `credentialSchema`: CredentialSchemaData?, + var `jsonSerializedEvidence`: kotlin.String? ) { companion object @@ -6290,6 +6330,8 @@ public object FfiConverterTypeVerifiableCredentialData: FfiConverterRustBuffer { + override fun read(buf: ByteBuffer): CredentialSchemaData? { + if (buf.get().toInt() == 0) { + return null + } + return FfiConverterTypeCredentialSchemaData.read(buf) + } + + override fun allocationSize(value: CredentialSchemaData?): ULong { + if (value == null) { + return 1UL + } else { + return 1UL + FfiConverterTypeCredentialSchemaData.allocationSize(value) + } + } + + override fun write(value: CredentialSchemaData?, buf: ByteBuffer) { + if (value == null) { + buf.put(0) + } else { + buf.put(1) + FfiConverterTypeCredentialSchemaData.write(value, buf) + } + } +} + + + + public object FfiConverterOptionalTypeDidDhtCreateOptions: FfiConverterRustBuffer { override fun read(buf: ByteBuffer): DidDhtCreateOptions? { if (buf.get().toInt() == 0) { diff --git a/bound/kt/src/main/kotlin/web5/sdk/vc/VerifiableCredential.kt b/bound/kt/src/main/kotlin/web5/sdk/vc/VerifiableCredential.kt index 0819807c..67af1d0b 100644 --- a/bound/kt/src/main/kotlin/web5/sdk/vc/VerifiableCredential.kt +++ b/bound/kt/src/main/kotlin/web5/sdk/vc/VerifiableCredential.kt @@ -1,26 +1,33 @@ package web5.sdk.vc +import com.fasterxml.jackson.annotation.JsonAnyGetter +import com.fasterxml.jackson.annotation.JsonAnySetter +import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonValue import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.JsonDeserializer import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.module.kotlin.readValue import web5.sdk.Json import web5.sdk.dids.BearerDid import java.util.Date import web5.sdk.rust.VerifiableCredential as RustCoreVerifiableCredential import web5.sdk.rust.VerifiableCredentialCreateOptionsData as RustCoreVerifiableCredentialCreateOptions +import web5.sdk.rust.CredentialSchemaData as RustCoreCredentialSchema data class VerifiableCredentialCreateOptions( val id: String? = null, - var context: List? = null, - var type: List? = null, - var issuanceDate: Date? = null, - var expirationDate: Date? = null + val context: List? = null, + val type: List? = null, + val issuanceDate: Date? = null, + val expirationDate: Date? = null, + val credentialSchema: CredentialSchema? = null, + val evidence: List>? = null ) -class VerifiableCredential private constructor( +data class VerifiableCredential private constructor( val context: List, val type: List, val id: String, @@ -28,6 +35,8 @@ class VerifiableCredential private constructor( val credentialSubject: CredentialSubject, val issuanceDate: Date, val expirationDate: Date? = null, + val credentialSchema: CredentialSchema? = null, + val evidence: List>? = null, internal val rustCoreVerifiableCredential: RustCoreVerifiableCredential, ) { companion object { @@ -38,6 +47,7 @@ class VerifiableCredential private constructor( val jsonSerializedIssuer = Json.stringify(issuer) val jsonSerializedCredentialSubject = Json.stringify(credentialSubject) + val jsonSerializedEvidence = options?.evidence?.let { Json.stringify(it) } val rustCoreVerifiableCredential = RustCoreVerifiableCredential.create( jsonSerializedIssuer, @@ -48,10 +58,13 @@ class VerifiableCredential private constructor( options?.type, options?.issuanceDate?.toInstant(), options?.expirationDate?.toInstant(), + options?.credentialSchema?.let { RustCoreCredentialSchema(it.id, it.type) }, + jsonSerializedEvidence ) ) val data = rustCoreVerifiableCredential.getData() + val evidence = data.jsonSerializedEvidence?.let { Json.jsonMapper.readValue>>(it) } return VerifiableCredential( data.context, @@ -61,7 +74,9 @@ class VerifiableCredential private constructor( credentialSubject, Date.from(data.issuanceDate), data.expirationDate?.let { Date.from(it) }, - rustCoreVerifiableCredential, + data.credentialSchema?.let { CredentialSchema(it.id, it.type) }, + evidence, + rustCoreVerifiableCredential ) } @@ -71,6 +86,7 @@ class VerifiableCredential private constructor( val issuer = Json.jsonMapper.readValue(data.jsonSerializedIssuer, Issuer::class.java) val credentialSubject = Json.jsonMapper.readValue(data.jsonSerializedCredentialSubject, CredentialSubject::class.java) + val evidence = data.jsonSerializedEvidence?.let { Json.jsonMapper.readValue>>(it) } return VerifiableCredential( data.context, @@ -80,7 +96,9 @@ class VerifiableCredential private constructor( credentialSubject, Date.from(data.issuanceDate), data.expirationDate?.let { Date.from(it) }, - rustCoreVerifiableCredential, + data.credentialSchema?.let { CredentialSchema(it.id, it.type) }, + evidence, + rustCoreVerifiableCredential ) } } @@ -130,12 +148,26 @@ class IssuerDeserializer : JsonDeserializer() { data class CredentialSubject( val id: String, - val additionalProperties: Map = emptyMap() + @JsonIgnore + val additionalProperties: Map = mutableMapOf() ) { + @JsonAnyGetter + internal fun getAdditionalProperties(): Map { + return additionalProperties + } + + @JsonAnySetter + internal fun setAdditionalProperty(key: String, value: Any) { + (additionalProperties as MutableMap)[key] = value + } + @JsonValue fun toJson(): Map { - return mapOf( - "id" to id, - ) + additionalProperties + return mapOf("id" to id) + additionalProperties } -} \ No newline at end of file +} + +data class CredentialSchema( + val id: String, + val type: String +) \ No newline at end of file diff --git a/bound/kt/src/test/kotlin/web5/sdk/vc/VerifiableCredentialTest.kt b/bound/kt/src/test/kotlin/web5/sdk/vc/VerifiableCredentialTest.kt index 001dd33e..1191411a 100644 --- a/bound/kt/src/test/kotlin/web5/sdk/vc/VerifiableCredentialTest.kt +++ b/bound/kt/src/test/kotlin/web5/sdk/vc/VerifiableCredentialTest.kt @@ -1,6 +1,8 @@ package web5.sdk.vc import com.nimbusds.jose.JWSObject +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer import org.junit.jupiter.api.* import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.fail @@ -277,6 +279,378 @@ class VerifiableCredentialTest { val vc = VerifiableCredential.create(ISSUER, CREDENTIAL_SUBJECT, options) assertEquals(expirationDate, vc.expirationDate) } + + @Test + fun test_evidence_must_be_set_if_supplied() { + testSuite.include() + + val evidence = listOf(mapOf("A Key" to "A Value")) + val options = VerifiableCredentialCreateOptions(evidence = evidence) + val vc = VerifiableCredential.create(ISSUER, CREDENTIAL_SUBJECT, options) + assertEquals(evidence, vc.evidence) + } + + @Test + fun test_schema_type_must_be_jsonschema() { + testSuite.include() + + val invalidSchemaType = CredentialSchema( + id = "https://example.com/schemas/invalid.json", + type = "InvalidSchemaType" + ) + + val options = VerifiableCredentialCreateOptions( + credentialSchema = invalidSchemaType + ) + + val exception = assertThrows { + VerifiableCredential.create(ISSUER, CREDENTIAL_SUBJECT, options) + } + + assertEquals("parameter error type must be JsonSchema", exception.msg) + } + + @Test + fun test_schema_resolve_network_issue() { + testSuite.include() + + val invalidUrl = "https://invalid-url.com/schemas/email.json" + val schema = CredentialSchema( + id = invalidUrl, + type = "JsonSchema" + ) + + val options = VerifiableCredentialCreateOptions( + credentialSchema = schema + ) + + val exception = assertThrows { + VerifiableCredential.create(ISSUER, CREDENTIAL_SUBJECT, options) + } + + assertTrue(exception.msg.contains("unable to resolve json schema")) + } + + @Test + fun test_schema_resolve_non_success() { + testSuite.include() + + val mockWebServer = MockWebServer() + mockWebServer.start() + + val url = mockWebServer.url("/schemas/email.json") + + mockWebServer.enqueue( + MockResponse() + .setResponseCode(500) // Simulate a server error + .addHeader("Content-Type", "application/json") + ) + + val schema = CredentialSchema( + id = url.toString(), + type = "JsonSchema" + ) + + val options = VerifiableCredentialCreateOptions( + credentialSchema = schema + ) + + val exception = assertThrows { + VerifiableCredential.create(ISSUER, CREDENTIAL_SUBJECT, options) + } + + assertTrue(exception.msg.contains("non-200 response when resolving json schema")) + + mockWebServer.shutdown() + } + + @Test + fun test_schema_resolve_invalid_response_body() { + testSuite.include() + + val mockWebServer = MockWebServer() + mockWebServer.start() + + val url = mockWebServer.url("/schemas/email.json") + + mockWebServer.enqueue( + MockResponse() + .setResponseCode(200) + .addHeader("Content-Type", "application/json") + .setBody("invalid response body") // Simulate an invalid JSON response + ) + + val schema = CredentialSchema( + id = url.toString(), + type = "JsonSchema" + ) + + val options = VerifiableCredentialCreateOptions( + credentialSchema = schema + ) + + val exception = assertThrows { + VerifiableCredential.create(ISSUER, CREDENTIAL_SUBJECT, options) + } + + assertTrue(exception.msg.contains("unable to parse json schema from response body")) + + mockWebServer.shutdown() + } + + @Test + fun test_schema_invalid_json_schema() { + testSuite.include() + + val mockWebServer = MockWebServer() + mockWebServer.start() + + val url = mockWebServer.url("/schemas/email.json") + + val invalidJsonSchema = """ + { + "${"$"}id": "${"$"}url/schemas/email.json", + "${"$"}schema": "this is not a valid ${"$"}schema", + "title": "InvalidEmailCredential", + "type": "object" + } + """ + + mockWebServer.enqueue( + MockResponse() + .setResponseCode(200) + .addHeader("Content-Type", "application/json") + .setBody(invalidJsonSchema) + ) + + val schema = CredentialSchema( + id = url.toString(), + type = "JsonSchema" + ) + + val options = VerifiableCredentialCreateOptions( + credentialSchema = schema + ) + + val exception = assertThrows { + VerifiableCredential.create(ISSUER, CREDENTIAL_SUBJECT, options) + } + + assertTrue(exception.msg.contains("unable to compile json schema")) + + mockWebServer.shutdown() + } + + @Test + fun test_schema_do_not_support_draft04() { + testSuite.include() + + val mockWebServer = MockWebServer() + mockWebServer.start() + + val url = mockWebServer.url("/schemas/email.json") + + val draft04Schema = """ + { + "${"$"}id": "$url/schemas/email.json", + "${"$"}schema": "http://json-schema.org/draft-04/schema#", + "title": "EmailCredential", + "type": "object" + } + """ + + mockWebServer.enqueue( + MockResponse() + .setResponseCode(200) + .addHeader("Content-Type", "application/json") + .setBody(draft04Schema) + ) + + val schema = CredentialSchema( + id = url.toString(), + type = "JsonSchema" + ) + + val options = VerifiableCredentialCreateOptions( + credentialSchema = schema + ) + + val exception = assertThrows { + VerifiableCredential.create(ISSUER, CREDENTIAL_SUBJECT, options) + } + + assertEquals("json schema error draft unsupported Draft4", exception.msg) + + mockWebServer.shutdown() + } + + + @Test + fun test_schema_do_not_support_draft06() { + testSuite.include() + + val mockWebServer = MockWebServer() + mockWebServer.start() + + val url = mockWebServer.url("/schemas/email.json") + + val draft06Schema = """ + { + "${"$"}id": "$url/schemas/email.json", + "${"$"}schema": "http://json-schema.org/draft-06/schema#", + "title": "EmailCredential", + "type": "object" + } + """ + + mockWebServer.enqueue( + MockResponse() + .setResponseCode(200) + .addHeader("Content-Type", "application/json") + .setBody(draft06Schema) + ) + + val schema = CredentialSchema( + id = url.toString(), + type = "JsonSchema" + ) + + val options = VerifiableCredentialCreateOptions( + credentialSchema = schema + ) + + val exception = assertThrows { + VerifiableCredential.create(ISSUER, CREDENTIAL_SUBJECT, options) + } + + assertEquals("json schema error draft unsupported Draft6", exception.msg) + + mockWebServer.shutdown() + } + + @Test + fun test_schema_fails_validation() { + testSuite.include() + + val mockWebServer = MockWebServer() + mockWebServer.start() + + val url = mockWebServer.url("/schemas/email.json") + + val validJsonSchema = """ + { + "${"$"}id": "$url/schemas/email.json", + "${"$"}schema": "https://json-schema.org/draft/2020-12/schema", + "title": "EmailCredential", + "type": "object", + "properties": { + "credentialSubject": { + "type": "object", + "properties": { + "emailAddress": { + "type": "string", + "format": "email" + } + }, + "required": ["emailAddress"] + } + } + } + """ + + mockWebServer.enqueue( + MockResponse() + .setResponseCode(200) + .addHeader("Content-Type", "application/json") + .setBody(validJsonSchema) + ) + + val schema = CredentialSchema( + id = url.toString(), + type = "JsonSchema" + ) + + // This credential subject does not match the schema as it does not have "emailAddress" + val invalidCredentialSubject = CredentialSubject( + id = SUBJECT_DID_URI, + additionalProperties = emptyMap() // Missing "emailAddress" + ) + + val options = VerifiableCredentialCreateOptions( + credentialSchema = schema + ) + + val exception = assertThrows { + VerifiableCredential.create(ISSUER, invalidCredentialSubject, options) + } + + assertTrue(exception.msg.contains("validation errors")) + + mockWebServer.shutdown() + } + + @Test + fun test_schema_example_from_spec() { + testSuite.include() + + val mockWebServer = MockWebServer() + mockWebServer.start() + + val url = mockWebServer.url("/schemas/email.json") + + val jsonSchemaFromSpec = """ + { + "${"$"}id": "$url/schemas/email.json", + "${"$"}schema": "https://json-schema.org/draft/2020-12/schema", + "title": "EmailCredential", + "type": "object", + "properties": { + "credentialSubject": { + "type": "object", + "properties": { + "emailAddress": { + "type": "string", + "format": "email" + } + }, + "required": ["emailAddress"] + } + } + } + """ + + mockWebServer.enqueue( + MockResponse() + .setResponseCode(200) + .addHeader("Content-Type", "application/json") + .setBody(jsonSchemaFromSpec) + ) + + val schema = CredentialSchema( + id = url.toString(), + type = "JsonSchema" + ) + + val additionalProperties = mapOf( + "emailAddress" to "alice@example.com" + ) + + val validCredentialSubject = CredentialSubject( + id = SUBJECT_DID_URI, + additionalProperties = additionalProperties + ) + + val options = VerifiableCredentialCreateOptions( + credentialSchema = schema + ) + + val vc = VerifiableCredential.create(ISSUER, validCredentialSubject, options) + + assertEquals(SUBJECT_DID_URI, vc.credentialSubject.id) + assertEquals(additionalProperties, vc.credentialSubject.additionalProperties) + + mockWebServer.shutdown() + } } @Nested @@ -404,7 +778,17 @@ class VerifiableCredentialTest { } @Test - fun test_issuer_string() { + fun test_can_skip_credential_schema_validation() { + testSuite.include() + + val vcJwtWithInvalidSchema = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSXlkMjFXVjFwblMySk1iM0JPVEhWTmRYUlNSV2gwTWtWMmJEbExkVkpFTFY5MlVrRnpWMlZwUm01TkluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjE1OTY4MDA4LTI1OTEtNGY0MS1hOWI1LWU2YmUxNDU5ZDcxNiIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lJeWQyMVdWMXBuUzJKTWIzQk9USFZOZFhSU1JXaDBNa1YyYkRsTGRWSkVMVjkyVWtGelYyVnBSbTVOSW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOC0zMFQxNDo1NTozNS41MTc5NjUrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifSwiY3JlZGVudGlhbFNjaGVtYSI6eyJpZCI6ImludmFsaWQgdXJsL3NjaGVtYXMvZW1haWwuanNvbiIsInR5cGUiOiJKc29uU2NoZW1hIn19LCJpc3MiOiJkaWQ6andrOmV5SmhiR2NpT2lKRlpESTFOVEU1SWl3aWEzUjVJam9pVDB0UUlpd2lZM0oySWpvaVJXUXlOVFV4T1NJc0luZ2lPaUl5ZDIxV1YxcG5TMkpNYjNCT1RIVk5kWFJTUldoME1rVjJiRGxMZFZKRUxWOTJVa0Z6VjJWcFJtNU5JbjAiLCJqdGkiOiJ1cm46dXVpZDoxNTk2ODAwOC0yNTkxLTRmNDEtYTliNS1lNmJlMTQ1OWQ3MTYiLCJzdWIiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkiLCJuYmYiOjE3MjUwMjk3MzUsImlhdCI6MTcyNTAyOTczNX0.8zNS9RWIpvTlMvAzaI9dNSjKKM1drFig7bKhQeQ6Mv9hWXwazvDhxthy3D25EmITWAPiJfGcMPDqoDobETf8DA" + + val decodedVc = VerifiableCredential.fromVcJwt(vcJwtWithInvalidSchema, false) + assertEquals("urn:uuid:15968008-2591-4f41-a9b5-e6be1459d716", decodedVc.id) + } + + @Test + fun test_decode_issuer_string() { testSuite.include() val vcJwtWithIssuerAsString = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSnlkMmhYU1VOWWNsSjNiMFphUm1SMU0wbHNOaTFCTkdVdGRqazNRbE14UmtaUmFWRTRhV05tV2t0ckluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjc0NTY5ZmIzLWMyZTktNGZiMy1hOThkLWY3NGFjNzVjYTg5NSIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lKeWQyaFhTVU5ZY2xKM2IwWmFSbVIxTTBsc05pMUJOR1V0ZGprM1FsTXhSa1pSYVZFNGFXTm1Xa3RySW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOC0yOFQxNjozNjoyOS4zNDc4ODArMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSnlkMmhYU1VOWWNsSjNiMFphUm1SMU0wbHNOaTFCTkdVdGRqazNRbE14UmtaUmFWRTRhV05tV2t0ckluMCIsImp0aSI6InVybjp1dWlkOjc0NTY5ZmIzLWMyZTktNGZiMy1hOThkLWY3NGFjNzVjYTg5NSIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsIm5iZiI6MTcyNDg2Mjk4OSwiaWF0IjoxNzI0ODYyOTg5fQ.0DSZ2XbPtjtrtxNKo3tImoByb1-jlQxZQN11lsngaFSe4lhy4mYmaxGAby4wIl-c_cLEkgBULfF3Qa_dlNSTCw" @@ -423,7 +807,7 @@ class VerifiableCredentialTest { } @Test - fun test_issuer_object() { + fun test_decode_issuer_object() { testSuite.include() val vcJwtWithIssuerObject = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSTFVazF5YVVNMVZsaHVielpTVkRoTVdWVnJibnBKWm5OamFUUXlZbXhCYVdsTFdrcENaR2huVm5WQkluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjcwNWM0MTZiLTU1ODYtNDUzMS1hMmRmLWI3YzdhNTMxMGY5NiIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjp7ImlkIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lJMVVrMXlhVU0xVmxodWJ6WlNWRGhNV1ZWcmJucEpabk5qYVRReVlteEJhV2xMV2twQ1pHaG5WblZCSW4wIiwibmFtZSI6InNvbWUgbmFtZSJ9LCJpc3N1YW5jZURhdGUiOiIyMDI0LTA4LTI4VDE2OjQwOjExLjUwNDIyMCswMDowMCIsImV4cGlyYXRpb25EYXRlIjpudWxsLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSJ9fSwiaXNzIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lJMVVrMXlhVU0xVmxodWJ6WlNWRGhNV1ZWcmJucEpabk5qYVRReVlteEJhV2xMV2twQ1pHaG5WblZCSW4wIiwianRpIjoidXJuOnV1aWQ6NzA1YzQxNmItNTU4Ni00NTMxLWEyZGYtYjdjN2E1MzEwZjk2Iiwic3ViIjoiZGlkOmRodDpxZ21tcHlqdzVod25xZmd6bjd3bXJtMzNhZHk4Z2I4ejlpZGVpYjZtOWdqNHlzNndueTh5IiwibmJmIjoxNzI0ODYzMjExLCJpYXQiOjE3MjQ4NjMyMTF9.Mv-wlUcnj0w-OWuoMBCciaQXrAogXL3qqgZnthTRI9f55S5PidYiSapWFxFqc4SzxTVSpe64H2vF7kfGU-QpBw" @@ -443,7 +827,34 @@ class VerifiableCredentialTest { } @Test - fun test_missing_vc_claim() { + fun test_decode_evidence() { + testSuite.include() + + val vcJwtWithEvidence = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSmljbll0T1VKSGRUVlhNVFV4VkVKVk9GY3hVVkozU1dwSWRXVmlVVGc1TlRCQ2VuRTFjR1ZxV25wSkluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjkxYWM1NzBmLTRjMDMtNDIxZi1iZGY4LWQ3Y2YyNzQ1YzVmNSIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lKaWNuWXRPVUpIZFRWWE1UVXhWRUpWT0ZjeFVWSjNTV3BJZFdWaVVUZzVOVEJDZW5FMWNHVnFXbnBKSW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOC0zMFQxMDowMToyNC4yNTgzNjYrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifSwiZXZpZGVuY2UiOlt7IkEgS2V5IjoiQSBWYWx1ZSJ9XX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSmljbll0T1VKSGRUVlhNVFV4VkVKVk9GY3hVVkozU1dwSWRXVmlVVGc1TlRCQ2VuRTFjR1ZxV25wSkluMCIsImp0aSI6InVybjp1dWlkOjkxYWM1NzBmLTRjMDMtNDIxZi1iZGY4LWQ3Y2YyNzQ1YzVmNSIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsIm5iZiI6MTcyNTAxMjA4NCwiaWF0IjoxNzI1MDEyMDg0fQ.M7t4Ox08v-rC-naPSfIqlE1KKhZ1nrx_QA2HbuW38AkgxnSZOYEpXEG1UTAzh6mdwKZin9jwoGrj29u24K1ABA" + val vc = VerifiableCredential.fromVcJwt(vcJwtWithEvidence, false) + + val expectedEvidence = listOf(mapOf("A Key" to "A Value")) + assertEquals(expectedEvidence, vc.evidence) + } + + @Test + fun test_decode_credential_schema() { + testSuite.include() + + val vcJwtWithCredentialSchema = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSTFUMXB2TVd4bU9VcHlOeTFZTkdGWU4yRmxka2N5WVU5MGEwWmxlSFZuYzBwcVZVbEtWVU5UYVVkUkluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjNhMDU2NjE1LWNlZDMtNGQ4Zi05ODRhLTUwMzQ2Y2FlNDQ2ZiIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lJMVQxcHZNV3htT1VweU55MVlOR0ZZTjJGbGRrY3lZVTkwYTBabGVIVm5jMHBxVlVsS1ZVTlRhVWRSSW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOC0zMFQxNDo1OToyMi4xMDMzODUrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifSwiY3JlZGVudGlhbFNjaGVtYSI6eyJpZCI6Imh0dHBzOi8vZXhhbXBsZS5jb20vc2NoZW1hcy9lbWFpbC5qc29uIiwidHlwZSI6Ikpzb25TY2hlbWEifX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSTFUMXB2TVd4bU9VcHlOeTFZTkdGWU4yRmxka2N5WVU5MGEwWmxlSFZuYzBwcVZVbEtWVU5UYVVkUkluMCIsImp0aSI6InVybjp1dWlkOjNhMDU2NjE1LWNlZDMtNGQ4Zi05ODRhLTUwMzQ2Y2FlNDQ2ZiIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsIm5iZiI6MTcyNTAyOTk2MiwiaWF0IjoxNzI1MDI5OTYyfQ.ZQkusfYLJSpfLVF9OuWrrhw8NdcBnjlalMFZbsfAxJp8i74KH47RkMsVVPadLPuKwbozgcDRCKPsokrl33TuCw" + + val decodedVc = VerifiableCredential.fromVcJwt(vcJwtWithCredentialSchema, false) + + val expectedSchema = CredentialSchema( + id = "https://example.com/schemas/email.json", + type = "JsonSchema" + ) + + assertEquals(expectedSchema, decodedVc.credentialSchema) + } + + @Test + fun test_decode_missing_vc_claim() { testSuite.include() val vcJwtWithMissingVcClaim = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSlNSbkZSVlVWS1RFOVhlbXh3T1ZaRk1rdEtSalp6UjBwT00yVnpaWHBsY0hSSE0ySTFlbTh4YjAwNEluMCMwIn0.eyJpc3MiOiJkaWQ6andrOmV5SmhiR2NpT2lKRlpESTFOVEU1SWl3aWEzUjVJam9pVDB0UUlpd2lZM0oySWpvaVJXUXlOVFV4T1NJc0luZ2lPaUpTUm5GUlZVVktURTlYZW14d09WWkZNa3RLUmpaelIwcE9NMlZ6WlhwbGNIUkhNMkkxZW04eGIwMDRJbjAiLCJqdGkiOiJ1cm46dXVpZDozNmU0ZjllNi0yYzdjLTQ0NGMtOTI4OS0zNDhmY2IxNDZlYjYiLCJzdWIiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkiLCJuYmYiOjE3MjQ4NTA1MjIsImlhdCI6MTcyNDg1MDUyMn0.SqwZC0q9RuHp9hAtFmE6sBYeJ1uHuuq1hyijF0NmW9nksSBqtDpfNroNlitK_Tl-CLWtwbTpK3b3JduTfzGEAw" @@ -456,7 +867,7 @@ class VerifiableCredentialTest { } @Test - fun test_missing_jti_claim() { + fun test_decode_missing_jti_claim() { testSuite.include() val vcJwtWithMissingJtiClaim = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSm5jMjlTZGsxUFlXMHliMlJQTlY4NWVqbExlV2xzV1VzM1Yzb3RZa1owWW5wdlVrWm1iVTlUTVRJNEluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjEwODM2MzgwLWI2MmMtNGVmZC04YmU0LTZhNzJiMDZjYWI4NyIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lKbmMyOVNkazFQWVcweWIyUlBOVjg1ZWpsTGVXbHNXVXMzVjNvdFlrWjBZbnB2VWtabWJVOVRNVEk0SW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOC0yOFQxMzoxMDo1NS4yMDYwOTIrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSm5jMjlTZGsxUFlXMHliMlJQTlY4NWVqbExlV2xzV1VzM1Yzb3RZa1owWW5wdlVrWm1iVTlUTVRJNEluMCIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsIm5iZiI6MTcyNDg1MDY1NSwiaWF0IjoxNzI0ODUwNjU1fQ.1XDmdvB1GDsCHw9Qwp0HA5r8W-JnZB4lz9Yqo0C2V_EEe-uk88bQSl8P9HV8ViNyBC_YaYatLiPTD4jBZY77DA" @@ -469,7 +880,7 @@ class VerifiableCredentialTest { } @Test - fun test_missing_issuer_claim() { + fun test_decode_missing_issuer_claim() { testSuite.include() val vcJwtWithMissingIssuerClaim = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSnpjamREVWtVek1HbzNjVVU0Y2taVVJYQXdSbFJzYnpKVVVXVmlZa1ZHTVVvelJHaHRTVWhaVTNFd0luMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjRjYzU0NWU0LWI5ZDgtNDdkNS04Zjk0LTA4MmM0ZGViNzAyZCIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lKemNqZERVa1V6TUdvM2NVVTRja1pVUlhBd1JsUnNiekpVVVdWaVlrVkdNVW96UkdodFNVaFpVM0V3SW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOC0yOFQxMzoxMTo1Mi4zMjg4MTMrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifX0sImp0aSI6InVybjp1dWlkOjRjYzU0NWU0LWI5ZDgtNDdkNS04Zjk0LTA4MmM0ZGViNzAyZCIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsIm5iZiI6MTcyNDg1MDcxMiwiaWF0IjoxNzI0ODUwNzEyfQ.hwR6edt6ItlN0HHkDcxzhE3N5hLk-5-VYDLrqkalUoTKB41vsfaPvGnt_UQK3EAuekQgrTQ0SuCq-6ut0EdlBw" @@ -482,7 +893,7 @@ class VerifiableCredentialTest { } @Test - fun test_missing_subject_claim() { + fun test_decode_missing_subject_claim() { testSuite.include() val vcJwtWithMissingSubjectClaim = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSldiRFprTjFFMWRXOTNSMVk1TWxsRlVWSkxOMnROWkdRM1lYcFJiMGxsU0hac1FXaFNSMVJmTlRJMEluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjBmYTE0MTgxLTllMWYtNDk0ZC05ZmVmLWMwYjgxZDE1ZGJiYiIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lKV2JEWmtOMUUxZFc5M1IxWTVNbGxGVVZKTE4ydE5aR1EzWVhwUmIwbGxTSFpzUVdoU1IxUmZOVEkwSW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOC0yOFQxMzoxMjo0NS40NTg4MjYrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSldiRFprTjFFMWRXOTNSMVk1TWxsRlVWSkxOMnROWkdRM1lYcFJiMGxsU0hac1FXaFNSMVJmTlRJMEluMCIsImp0aSI6InVybjp1dWlkOjBmYTE0MTgxLTllMWYtNDk0ZC05ZmVmLWMwYjgxZDE1ZGJiYiIsIm5iZiI6MTcyNDg1MDc2NSwiaWF0IjoxNzI0ODUwNzY1fQ.61IFQhdASbbcYKUzMfhO7WPmikBd8AoE468FTlqRysxXck7kNa3bAAow3jK2uhYrIWLyRu3kuBp7JyYhLavjBw" @@ -495,7 +906,7 @@ class VerifiableCredentialTest { } @Test - fun test_missing_nbf_claim() { + fun test_decode_missing_nbf_claim() { testSuite.include() val vcJwtWithMissingNbfClaim = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSXdOR1ZzZGxGdlJWbDBZbEJIT0RsWlVtaGpTR2RJT1cwMlMzSjZiRVkyUWpGUldrZGxOR2RGUjJKakluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjk3OGZhZTIxLTVmMDYtNDBmNy1iZTJmLTM4MzRmZGMwZDY0NSIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lJd05HVnNkbEZ2UlZsMFlsQkhPRGxaVW1oalNHZElPVzAyUzNKNmJFWTJRakZSV2tkbE5HZEZSMkpqSW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOC0yOFQxMzoxMzoyNi4zMzQzNjYrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSXdOR1ZzZGxGdlJWbDBZbEJIT0RsWlVtaGpTR2RJT1cwMlMzSjZiRVkyUWpGUldrZGxOR2RGUjJKakluMCIsImp0aSI6InVybjp1dWlkOjk3OGZhZTIxLTVmMDYtNDBmNy1iZTJmLTM4MzRmZGMwZDY0NSIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsImlhdCI6MTcyNDg1MDgwNn0.ZXfuZmvddH1nvmub8WDpQ2UEOhuiLaN6WL2q3XDhn0eouM_bNVa7vmCUCUZc3sfJ1YCtnAGCJOlJxSGnD3tOCw" @@ -508,7 +919,7 @@ class VerifiableCredentialTest { } @Test - fun test_claim_mismatch_id() { + fun test_decode_claim_mismatch_id() { testSuite.include() val vcJwtWithMismatchId = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSnZTWHBSUjJkTmNGTlNPSEpRWTNkd1IxZEJTRnBaV0hwUFdYRlRiMFkyTWtoM09HTlJRamRJUzIxM0luMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InNvbWV0aGluZyBpbnZhbGlkIiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOiJkaWQ6andrOmV5SmhiR2NpT2lKRlpESTFOVEU1SWl3aWEzUjVJam9pVDB0UUlpd2lZM0oySWpvaVJXUXlOVFV4T1NJc0luZ2lPaUp2U1hwUlIyZE5jRk5TT0hKUVkzZHdSMWRCU0ZwWldIcFBXWEZUYjBZMk1raDNPR05SUWpkSVMyMTNJbjAiLCJpc3N1YW5jZURhdGUiOiIyMDI0LTA4LTI4VDEzOjE2OjAwLjcyMjgxOSswMDowMCIsImV4cGlyYXRpb25EYXRlIjpudWxsLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSJ9fSwiaXNzIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lKdlNYcFJSMmROY0ZOU09ISlFZM2R3UjFkQlNGcFpXSHBQV1hGVGIwWTJNa2gzT0dOUlFqZElTMjEzSW4wIiwianRpIjoidXJuOnV1aWQ6ZGFkM2Y2MjktMzFiMS00NDcxLWFhYTMtMWE4MGZjN2I1YmU2Iiwic3ViIjoiZGlkOmRodDpxZ21tcHlqdzVod25xZmd6bjd3bXJtMzNhZHk4Z2I4ejlpZGVpYjZtOWdqNHlzNndueTh5IiwibmJmIjoxNzI0ODUwOTYwLCJpYXQiOjE3MjQ4NTA5NjB9.P8-Z3KsMxIk7-Dz9a5odVhbGJZtWsWp4mDVYLlVxuZTNJl-Km-j2S1KusTjRTDkg1DqQoiVvp2Is0kr5WoAFBA" @@ -521,7 +932,7 @@ class VerifiableCredentialTest { } @Test - fun test_claim_mismatch_issuer() { + fun test_decode_claim_mismatch_issuer() { testSuite.include() val vcJwtWithMismatchIssuer = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSXpWRVZsYWs0emIzSXpUbXR4WkZWVllYQjZaMVZ5TFcxblZFTkNkWEZRWVZkT1JWcE9lRXcwWkhRd0luMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjJiNzQzNWY0LWU0YjctNGQyZC1iN2M2LTVkOTE5ODRlNDlhOCIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoic29tZXRoaW5nIGludmFsaWQiLCJpc3N1YW5jZURhdGUiOiIyMDI0LTA4LTI4VDEzOjE3OjQ1LjI4ODk2NiswMDowMCIsImV4cGlyYXRpb25EYXRlIjpudWxsLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSJ9fSwiaXNzIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lJelZFVmxhazR6YjNJelRtdHhaRlZWWVhCNloxVnlMVzFuVkVOQ2RYRlFZVmRPUlZwT2VFdzBaSFF3SW4wIiwianRpIjoidXJuOnV1aWQ6MmI3NDM1ZjQtZTRiNy00ZDJkLWI3YzYtNWQ5MTk4NGU0OWE4Iiwic3ViIjoiZGlkOmRodDpxZ21tcHlqdzVod25xZmd6bjd3bXJtMzNhZHk4Z2I4ejlpZGVpYjZtOWdqNHlzNndueTh5IiwibmJmIjoxNzI0ODUxMDY1LCJpYXQiOjE3MjQ4NTEwNjV9.x0UY38J4lEwmrXR4qrzhnk58btjZfMf8DVhdgBoj9M0JOgJqCDFCzwcS5weVCpNAv3gN72Qo32RH9Tx0eYyoDA" @@ -534,7 +945,7 @@ class VerifiableCredentialTest { } @Test - fun test_claim_mismatch_subject() { + fun test_decode_claim_mismatch_subject() { testSuite.include() val vcJwtWithMismatchSubject = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSXdVRmh0UkVNMlNIWnVia1E0Vmw5QkxWbDVSelZ1TWtSa2IxQkdTVFkxY2tkb2MwVTVZWFZsWW5CckluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjAwNDJiYTQ4LWU0ZGYtNGVhMS04ZmJjLWJjYmI4ODY3ZjFhMCIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lJd1VGaHRSRU0yU0hadWJrUTRWbDlCTFZsNVJ6VnVNa1JrYjFCR1NUWTFja2RvYzBVNVlYVmxZbkJySW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOC0yOFQxMzoxOToxMC4xNjM0ODkrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJzb21ldGhpbmcgaW52YWxpZCJ9fSwiaXNzIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lJd1VGaHRSRU0yU0hadWJrUTRWbDlCTFZsNVJ6VnVNa1JrYjFCR1NUWTFja2RvYzBVNVlYVmxZbkJySW4wIiwianRpIjoidXJuOnV1aWQ6MDA0MmJhNDgtZTRkZi00ZWExLThmYmMtYmNiYjg4NjdmMWEwIiwic3ViIjoiZGlkOmRodDpxZ21tcHlqdzVod25xZmd6bjd3bXJtMzNhZHk4Z2I4ejlpZGVpYjZtOWdqNHlzNndueTh5IiwibmJmIjoxNzI0ODUxMTUwLCJpYXQiOjE3MjQ4NTExNTB9.bAm9kKJX2-Rcw679VS7cUPbqg9awuq5Lwu9wiZoGcE0TCSc59rQTIP4nvxlP22o3V-VVs_DbfpJU-qB4duDSCA" @@ -547,7 +958,7 @@ class VerifiableCredentialTest { } @Test - fun test_claim_misconfigured_exp() { + fun test_decode_claim_misconfigured_exp() { testSuite.include() val vcJwtWithMisconfiguredExp = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSnJkWFI2V21WM01EVTBMVlUwUVRBM2FsYzJZbkkxUlV4NU1UQlpOSGxPVTFCaVkyOTNXakJ3TjJWakluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjYxZjgwM2I4LWUxMDQtNDdhOC04YWE1LTk4YzQ1ZTFiOGUzMSIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lKcmRYUjZXbVYzTURVMExWVTBRVEEzYWxjMlluSTFSVXg1TVRCWk5IbE9VMUJpWTI5M1dqQndOMlZqSW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOC0yOFQxMzoyMzo0My45NDg4MzQrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6IjIwMjktMDgtMjJUMTM6MjM6NDMuOTQ4NzYwKzAwOjAwIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSnJkWFI2V21WM01EVTBMVlUwUVRBM2FsYzJZbkkxUlV4NU1UQlpOSGxPVTFCaVkyOTNXakJ3TjJWakluMCIsImp0aSI6InVybjp1dWlkOjYxZjgwM2I4LWUxMDQtNDdhOC04YWE1LTk4YzQ1ZTFiOGUzMSIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsIm5iZiI6MTcyNDg1MTQyMywiaWF0IjoxNzI0ODUxNDIzfQ.AWYyvLRISXwLH5gAXb5CcwBXNwaRKwacGqstXjnk-xIHx9gmm5xj8zGONvcKE2Xx0t9j3pNHicrhkp5wcOkABQ" @@ -560,7 +971,7 @@ class VerifiableCredentialTest { } @Test - fun test_claim_mismatch_exp() { + fun test_decode_claim_mismatch_exp() { testSuite.include() val vcJwtWithMismatchExp = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSk1lWEJmUjJVelVEZGtjbVZhYTJSV1VsTnJZbmROVldkcVkxUTRhMHd6VUVVMk1Hc3pZMGgzVTJ0ckluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjRhMjA2YmMzLWZmOTYtNDMwNS1iMzM4LTJiZGQ1ODRiYzkyOSIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lKTWVYQmZSMlV6VURka2NtVmFhMlJXVWxOclluZE5WV2RxWTFRNGEwd3pVRVUyTUdzelkwaDNVMnRySW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOC0yOFQxMzoyNzozMy40Mjg1NjMrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6IjIwMjktMDgtMjJUMTM6Mjc6MzMuNDI4NDgyKzAwOjAwIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSk1lWEJmUjJVelVEZGtjbVZhYTJSV1VsTnJZbmROVldkcVkxUTRhMHd6VUVVMk1Hc3pZMGgzVTJ0ckluMCIsImp0aSI6InVybjp1dWlkOjRhMjA2YmMzLWZmOTYtNDMwNS1iMzM4LTJiZGQ1ODRiYzkyOSIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsIm5iZiI6MTcyNDg1MTY1MywiaWF0IjoxNzI0ODUxNjUzLCJleHAiOjE4ODUxMjM2NTN9.lAaTG8RhL2D92iNI6psZrv1uhtHYAO0m0AacGIQrW0XIThg-Livef36_CN9t4Lz2Ta5US2Be2VP6D3lCA-z1DQ" @@ -688,6 +1099,298 @@ class VerifiableCredentialTest { assertEquals("data model validation error: credential expired", exception.msg) } + + @Test + fun test_schema_type_must_be_jsonschema() { + testSuite.include() + + val vcJwtWithWrongSchemaType = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSXhlbTlYY0VWTWN6TnhiV0p5VW5GblRFbzBjbDlCZUhCYVNFSmpjMUZJVGtSaVRGYzBOM1JmVGpkSkluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjQ1NGY1NWJmLWYzMjAtNDQyOS1iNmViLTRkMzdlNTMzNDkwYyIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lJeGVtOVhjRVZNY3pOeGJXSnlVbkZuVEVvMGNsOUJlSEJhU0VKamMxRklUa1JpVEZjME4zUmZUamRKSW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOC0zMFQxNTowMjo1NC4zNjg1NjMrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifSwiY3JlZGVudGlhbFNjaGVtYSI6eyJpZCI6Imh0dHBzOi8vZXhhbXBsZS5jb20iLCJ0eXBlIjoic29tZXRoaW5nIGludmFsaWFkIn19LCJpc3MiOiJkaWQ6andrOmV5SmhiR2NpT2lKRlpESTFOVEU1SWl3aWEzUjVJam9pVDB0UUlpd2lZM0oySWpvaVJXUXlOVFV4T1NJc0luZ2lPaUl4ZW05WGNFVk1jek54YldKeVVuRm5URW8wY2w5QmVIQmFTRUpqYzFGSVRrUmlURmMwTjNSZlRqZEpJbjAiLCJqdGkiOiJ1cm46dXVpZDo0NTRmNTViZi1mMzIwLTQ0MjktYjZlYi00ZDM3ZTUzMzQ5MGMiLCJzdWIiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkiLCJuYmYiOjE3MjUwMzAxNzQsImlhdCI6MTcyNTAzMDE3NH0.8gxVx3_Qd1Lvao-y5PZ56XS3lMQvrFtBVMgfNDIdW9eoQkBQMNv79YKIxFCig0LHanzg_vyzX7tBviW6xJUuDw" + + val exception = assertThrows { + VerifiableCredential.fromVcJwt(vcJwtWithWrongSchemaType, true) + } + + assertEquals("parameter error type must be JsonSchema", exception.msg) + } + + @Test + fun test_schema_resolve_network_issue() { + testSuite.include() + + val vcJwtWithInvalidUrl = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSmZYelYxVEU1bWNVWTRRbTB6ZVhnMmJVRndMVlJJV25sSk5WcDJWQzFmYVVKbExWZDJiMHRuTTFwakluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOmRlNDY2N2YxLTMzM2ItNDg4OC1hMDc5LTdkMGU1N2JiZmFlZiIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lKZlh6VjFURTVtY1VZNFFtMHplWGcyYlVGd0xWUklXbmxKTlZwMlZDMWZhVUpsTFZkMmIwdG5NMXBqSW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOC0zMFQxNTowNToyMC43NjQ0MDgrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifSwiY3JlZGVudGlhbFNjaGVtYSI6eyJpZCI6ImludmFsaWQgdXJsIiwidHlwZSI6Ikpzb25TY2hlbWEifX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSmZYelYxVEU1bWNVWTRRbTB6ZVhnMmJVRndMVlJJV25sSk5WcDJWQzFmYVVKbExWZDJiMHRuTTFwakluMCIsImp0aSI6InVybjp1dWlkOmRlNDY2N2YxLTMzM2ItNDg4OC1hMDc5LTdkMGU1N2JiZmFlZiIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsIm5iZiI6MTcyNTAzMDMyMCwiaWF0IjoxNzI1MDMwMzIwfQ.3sH7qzI7QrQMdkWIvqf7k8Mr2dMGjWBLrv4QB8gEz0t83RSFMtG-fWT-YVkUlo1qMvC4gNjT2Jc0eObCAA7VDQ" + + val exception = assertThrows { + VerifiableCredential.fromVcJwt(vcJwtWithInvalidUrl, true) + } + + assertTrue(exception.msg.contains("unable to resolve json schema")) + } + + @Test + fun test_schema_resolve_non_success() { + testSuite.include() + + val mockWebServer = MockWebServer() + mockWebServer.start(40001) + mockWebServer.enqueue( + MockResponse() + .setResponseCode(500) // Simulate a server error + .addHeader("Content-Type", "application/json") + ) + + val vcJwtAtPort = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSm9jMmhVZEU4M2F5MVNjVE5oVWtWR1lUVTRhUzFoWlZVdGRWaHNUVXB4UkdkallteEhhMTlpTW5OM0luMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOmE5Nzk1YTdmLTRmNzktNDU3OC1hYTkxLTcwYmYxM2YxZWVkNiIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lKb2MyaFVkRTgzYXkxU2NUTmhVa1ZHWVRVNGFTMWhaVlV0ZFZoc1RVcHhSR2RqWW14SGExOWlNbk4zSW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOS0wM1QxNTo0MDowMS4wNDg1MzIrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifSwiY3JlZGVudGlhbFNjaGVtYSI6eyJpZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDAwMDEvc2NoZW1hcy9lbWFpbC5qc29uIiwidHlwZSI6Ikpzb25TY2hlbWEifX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSm9jMmhVZEU4M2F5MVNjVE5oVWtWR1lUVTRhUzFoWlZVdGRWaHNUVXB4UkdkallteEhhMTlpTW5OM0luMCIsImp0aSI6InVybjp1dWlkOmE5Nzk1YTdmLTRmNzktNDU3OC1hYTkxLTcwYmYxM2YxZWVkNiIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsIm5iZiI6MTcyNTM3ODAwMSwiaWF0IjoxNzI1Mzc4MDAxfQ.9739UnhTfGXr2tsbsjun7FQfFuXNtqmzfxhP_okbywVDoh6nsBGk8smLUU_D0VYwtiMBTo1ujDs1QtKPbCZDDA" + + val exception = assertThrows { + VerifiableCredential.fromVcJwt(vcJwtAtPort, true) + } + + assertTrue(exception.msg.contains("non-200 response when resolving json schema")) + + mockWebServer.shutdown() + } + + @Test + fun test_schema_resolve_invalid_response_body() { + testSuite.include() + + val mockWebServer = MockWebServer() + mockWebServer.start(40002) + mockWebServer.enqueue( + MockResponse() + .setResponseCode(200) + .addHeader("Content-Type", "application/json") + .setBody("invalid response body") // Simulate invalid JSON response + ) + + val vcJwtAtPort = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSXhkek4zU25CUlZIVkNhRUZuV2tSd2JtbHZWME51ZW5kalp6bDBkMFZ4WVZGWldFUTVOblJXUTA1QkluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOmQ1NmYxMzRjLThjN2QtNDkyOC04OWYwLWQ5NWEzYjllZmU3YiIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lJeGR6TjNTbkJSVkhWQ2FFRm5Xa1J3Ym1sdlYwTnVlbmRqWnpsMGQwVnhZVkZaV0VRNU5uUldRMDVCSW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOS0wM1QxNTo0Mzo1OC40MTE0NTcrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifSwiY3JlZGVudGlhbFNjaGVtYSI6eyJpZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDAwMDIvc2NoZW1hcy9lbWFpbC5qc29uIiwidHlwZSI6Ikpzb25TY2hlbWEifX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSXhkek4zU25CUlZIVkNhRUZuV2tSd2JtbHZWME51ZW5kalp6bDBkMFZ4WVZGWldFUTVOblJXUTA1QkluMCIsImp0aSI6InVybjp1dWlkOmQ1NmYxMzRjLThjN2QtNDkyOC04OWYwLWQ5NWEzYjllZmU3YiIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsIm5iZiI6MTcyNTM3ODIzOCwiaWF0IjoxNzI1Mzc4MjM4fQ.vYZ5YeXa4ZXaomhVp2obgJwlgjwScFctNAJBqTf2hJOUr1v-jN1C5huK4JL_e16_dRCJd_ysmiOpgFOJD2MOCQ" + + val exception = assertThrows { + VerifiableCredential.fromVcJwt(vcJwtAtPort, true) + } + + assertTrue(exception.msg.contains("unable to parse json schema from response body")) + + mockWebServer.shutdown() + } + + @Test + fun test_schema_invalid_json_schema() { + testSuite.include() + + val mockWebServer = MockWebServer() + mockWebServer.start(40003) + val invalidJsonSchema = """ + { + "${"$"}id": "http://localhost:40003/schemas/email.json", + "${"$"}schema": "this is not a valid ${"$"}schema", + "title": "EmailCredential", + "type": "object", + "properties": { + "credentialSubject": { + "type": "object", + "properties": { + "emailAddress": { + "type": "string", + "format": "email" + } + }, + "required": ["emailAddress"] + } + } + } + """.trimIndent() + mockWebServer.enqueue( + MockResponse() + .setResponseCode(200) + .addHeader("Content-Type", "application/json") + .setBody(invalidJsonSchema) + ) + + val vcJwtAtPort = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSlRNemRDWVdaR01FTnRla2xmVTJ4WlEyTXlNSHBKZGt4eVprTnFjM1ptTUVWUWFtbDVkV2N5Wmt0WkluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjJjZWFmODUxLTBiYzktNDFkYy04NzNmLThhMGMyN2Y0ZDZkOCIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lKVE16ZENZV1pHTUVOdGVrbGZVMnhaUTJNeU1IcEpka3h5WmtOcWMzWm1NRVZRYW1sNWRXY3laa3RaSW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOS0wM1QxNTo0NTozMC4zMDY4MDYrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifSwiY3JlZGVudGlhbFNjaGVtYSI6eyJpZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDAwMDMvc2NoZW1hcy9lbWFpbC5qc29uIiwidHlwZSI6Ikpzb25TY2hlbWEifX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSlRNemRDWVdaR01FTnRla2xmVTJ4WlEyTXlNSHBKZGt4eVprTnFjM1ptTUVWUWFtbDVkV2N5Wmt0WkluMCIsImp0aSI6InVybjp1dWlkOjJjZWFmODUxLTBiYzktNDFkYy04NzNmLThhMGMyN2Y0ZDZkOCIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsIm5iZiI6MTcyNTM3ODMzMCwiaWF0IjoxNzI1Mzc4MzMwfQ.Drh8iEOdWWeL6l9KdmKMv9qfbBxWln-TW0KNwOJN3lZatyaSkwlBO_1o2FIWj0WIDiD_TP2EestMFazf4XFQDA" + + val exception = assertThrows { + VerifiableCredential.fromVcJwt(vcJwtAtPort, true) + } + + assertTrue(exception.msg.contains("unable to compile json schema")) + + mockWebServer.shutdown() + } + + @Test + fun test_schema_do_not_support_draft04() { + testSuite.include() + + val mockWebServer = MockWebServer() + mockWebServer.start(40004) + val draft04Schema = """ + { + "${"$"}id": "http://localhost:40004/schemas/email.json", + "${"$"}schema": "http://json-schema.org/draft-04/schema#", + "title": "EmailCredential", + "type": "object", + "properties": { + "credentialSubject": { + "type": "object", + "properties": { + "emailAddress": { + "type": "string", + "format": "email" + } + }, + "required": ["emailAddress"] + } + } + } + """.trimIndent() + mockWebServer.enqueue( + MockResponse() + .setResponseCode(200) + .addHeader("Content-Type", "application/json") + .setBody(draft04Schema) + ) + + val vcJwtAtPort = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSnBNemMwTFVkc1lVSmZiMmxQZG1aR1ZteGtiVWhhVXpNNVpEZEtlalUxUm0xU2R6WkVjbmswVkRjd0luMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOmYzNDc3ZDdkLWJiOWUtNGI5Ny1iYjhkLTk4NDMwNjgzN2RmMyIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lKcE16YzBMVWRzWVVKZmIybFBkbVpHVm14a2JVaGFVek01WkRkS2VqVTFSbTFTZHpaRWNuazBWRGN3SW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOS0wM1QxNTo0Nzo0MS4wNjgxNjIrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifSwiY3JlZGVudGlhbFNjaGVtYSI6eyJpZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDAwMDQvc2NoZW1hcy9lbWFpbC5qc29uIiwidHlwZSI6Ikpzb25TY2hlbWEifX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSnBNemMwTFVkc1lVSmZiMmxQZG1aR1ZteGtiVWhhVXpNNVpEZEtlalUxUm0xU2R6WkVjbmswVkRjd0luMCIsImp0aSI6InVybjp1dWlkOmYzNDc3ZDdkLWJiOWUtNGI5Ny1iYjhkLTk4NDMwNjgzN2RmMyIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsIm5iZiI6MTcyNTM3ODQ2MSwiaWF0IjoxNzI1Mzc4NDYxfQ.X7pZBMqPeBO0oTq1QNtMcSrYcIpDxCavPEoPDiB1A9GOqCohx7KCgOerXaJGSyklAkmNJod7ssmL4DMM-l3uDA" + + val exception = assertThrows { + VerifiableCredential.fromVcJwt(vcJwtAtPort, true) + } + + assertEquals("json schema error draft unsupported Draft4", exception.msg) + + mockWebServer.shutdown() + } + + @Test + fun test_schema_do_not_support_draft06() { + testSuite.include() + + val mockWebServer = MockWebServer() + mockWebServer.start(40005) + val draft06Schema = """ + { + "${"$"}id": "http://localhost:40005/schemas/email.json", + "${"$"}schema": "http://json-schema.org/draft-06/schema#", + "title": "EmailCredential", + "type": "object", + "properties": { + "credentialSubject": { + "type": "object", + "properties": { + "emailAddress": { + "type": "string", + "format": "email" + } + }, + "required": ["emailAddress"] + } + } + } + """.trimIndent() + mockWebServer.enqueue( + MockResponse() + .setResponseCode(200) + .addHeader("Content-Type", "application/json") + .setBody(draft06Schema) + ) + + val vcJwtAtPort = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSkhSemxDUVU1QlpXUkplR2RpYzJkVlprOWZRWFpPWVZsWmFHMWxZbmhXYWpOZlVuQnBWREp4ZG5WVkluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjQ4MGM0ZjQ5LTAyMmEtNDIwMi1hYjFiLTc1ZThjZjQ1NDEyMyIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lKSFJ6bENRVTVCWldSSmVHZGljMmRWWms5ZlFYWk9ZVmxaYUcxbFluaFdhak5mVW5CcFZESnhkblZWSW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOS0wM1QxNTo0ODo1MC4wNjM4NjIrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifSwiY3JlZGVudGlhbFNjaGVtYSI6eyJpZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDAwMDUvc2NoZW1hcy9lbWFpbC5qc29uIiwidHlwZSI6Ikpzb25TY2hlbWEifX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSkhSemxDUVU1QlpXUkplR2RpYzJkVlprOWZRWFpPWVZsWmFHMWxZbmhXYWpOZlVuQnBWREp4ZG5WVkluMCIsImp0aSI6InVybjp1dWlkOjQ4MGM0ZjQ5LTAyMmEtNDIwMi1hYjFiLTc1ZThjZjQ1NDEyMyIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsIm5iZiI6MTcyNTM3ODUzMCwiaWF0IjoxNzI1Mzc4NTMwfQ.Id6rsvMkqjocv6x5g_s8fnfR74HdXIpIdL3bMR33f1FYPFQ9CZRArMc1ZTh3xL3QfUggY8AUkRraQSAJh_onBA" + + val exception = assertThrows { + VerifiableCredential.fromVcJwt(vcJwtAtPort, true) + } + + assertEquals("json schema error draft unsupported Draft6", exception.msg) + + mockWebServer.shutdown() + } + + @Test + fun test_schema_fails_validation() { + testSuite.include() + + val mockWebServer = MockWebServer() + mockWebServer.start(40006) + val validJsonSchema = """ + { + "${"$"}id": "http://localhost:40006/schemas/email.json", + "${"$"}schema": "https://json-schema.org/draft/2020-12/schema", + "title": "EmailCredential", + "type": "object", + "properties": { + "credentialSubject": { + "type": "object", + "properties": { + "emailAddress": { + "type": "string", + "format": "email" + } + }, + "required": ["emailAddress"] + } + } + } + """.trimIndent() + mockWebServer.enqueue( + MockResponse() + .setResponseCode(200) + .addHeader("Content-Type", "application/json") + .setBody(validJsonSchema) + ) + + val vcJwtAtPort = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSnBlRkpyUVhodk5GTmtVMXAwTW1VMGFWQm5WRTV0T1dnelFYWnJYMU42Wm1WUlNVeFNkbFJHU0RSSkluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOmY4NThhZGM2LTZhMDQtNDY5ZC04MGJiLWM2MmI1N2MyNWI5NSIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lKcGVGSnJRWGh2TkZOa1UxcDBNbVUwYVZCblZFNXRPV2d6UVhaclgxTjZabVZSU1V4U2RsUkdTRFJKSW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOS0wM1QxNTo1MDozOS4yMTM3NzIrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifSwiY3JlZGVudGlhbFNjaGVtYSI6eyJpZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDAwMDYvc2NoZW1hcy9lbWFpbC5qc29uIiwidHlwZSI6Ikpzb25TY2hlbWEifX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSnBlRkpyUVhodk5GTmtVMXAwTW1VMGFWQm5WRTV0T1dnelFYWnJYMU42Wm1WUlNVeFNkbFJHU0RSSkluMCIsImp0aSI6InVybjp1dWlkOmY4NThhZGM2LTZhMDQtNDY5ZC04MGJiLWM2MmI1N2MyNWI5NSIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsIm5iZiI6MTcyNTM3ODYzOSwiaWF0IjoxNzI1Mzc4NjM5fQ.zxz0OZO1umdlxgRyrwyMJis4t4esE_7Zo6nma4Q8T4josAkw6vnQJFI_cPoZV4usQ1vve5bB5OOiMcf_tca6Cw" + + val exception = assertThrows { + VerifiableCredential.fromVcJwt(vcJwtAtPort, true) + } + + assertTrue(exception.msg.contains("validation errors")) + + mockWebServer.shutdown() + } + + @Test + fun test_schema_example_from_spec() { + testSuite.include() + + val mockWebServer = MockWebServer() + mockWebServer.start(40007) + val validJsonSchema = """ + { + "${"$"}id": "http://localhost:40007/schemas/email.json", + "${"$"}schema": "https://json-schema.org/draft/2020-12/schema", + "title": "EmailCredential", + "type": "object", + "properties": { + "credentialSubject": { + "type": "object", + "properties": { + "emailAddress": { + "type": "string", + "format": "email" + } + }, + "required": ["emailAddress"] + } + } + } + """.trimIndent() + mockWebServer.enqueue( + MockResponse() + .setResponseCode(200) + .addHeader("Content-Type", "application/json") + .setBody(validJsonSchema) + ) + + val vcJwtAtPort = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSkZjVm96YVdGVldqWnpUMlZ5VEVGMWEzcEpRbkV6Ym1sNVEzZHVVWEF0WWtkNk1EQlZMVk15YURGakluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjM4OWE2OWYyLWEwOTYtNDc0Ni1hNzU0LTRlNmE0ZThiZDEzYyIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lKRmNWb3phV0ZWV2paelQyVnlURUYxYTNwSlFuRXpibWw1UTNkdVVYQXRZa2Q2TURCVkxWTXlhREZqSW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOS0wM1QxNTo1MzoxNy43NjgzNzQrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkiLCJlbWFpbEFkZHJlc3MiOiJhbGljZUB0YmQuZW1haWwifSwiY3JlZGVudGlhbFNjaGVtYSI6eyJpZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDAwMDcvc2NoZW1hcy9lbWFpbC5qc29uIiwidHlwZSI6Ikpzb25TY2hlbWEifX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSkZjVm96YVdGVldqWnpUMlZ5VEVGMWEzcEpRbkV6Ym1sNVEzZHVVWEF0WWtkNk1EQlZMVk15YURGakluMCIsImp0aSI6InVybjp1dWlkOjM4OWE2OWYyLWEwOTYtNDc0Ni1hNzU0LTRlNmE0ZThiZDEzYyIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsIm5iZiI6MTcyNTM3ODc5NywiaWF0IjoxNzI1Mzc4Nzk3fQ.AC1OGOJ-MxfRsyNJZEQM4PW_t1eNCiSdTNtEtiPPOnIDGYnDl7JGtVIki9tHdWduQHoanrV0dWDeB5dnTTmZAw" + + val decodedVc = VerifiableCredential.fromVcJwt(vcJwtAtPort, true) + + assertEquals("alice@tbd.email", decodedVc.credentialSubject.additionalProperties["emailAddress"]) + + mockWebServer.shutdown() + } + } diff --git a/crates/web5/Cargo.toml b/crates/web5/Cargo.toml index 6e03837e..d6c5d483 100644 --- a/crates/web5/Cargo.toml +++ b/crates/web5/Cargo.toml @@ -14,7 +14,7 @@ chrono = { workspace = true } ed25519-dalek = { version = "2.1.1", features = ["rand_core"] } josekit = { version = "0.8.6", features = ["vendored"] } jsonpath-rust = "0.5.1" -jsonschema = { version = "0.18.0", default-features = false } +jsonschema = { version = "0.18.0", features = ["draft201909", "draft202012"] } k256 = { version = "0.13.3", features = ["ecdsa", "jwk"] } tokio = "1.38.0" rand = { workspace = true } diff --git a/crates/web5/src/credentials/create.rs b/crates/web5/src/credentials/create.rs index 73594a4b..30c7851d 100644 --- a/crates/web5/src/credentials/create.rs +++ b/crates/web5/src/credentials/create.rs @@ -1,18 +1,17 @@ -use std::time::SystemTime; -use uuid::Uuid; - -use crate::{ - dids::did::Did, - errors::{Result, Web5Error}, -}; - use super::{ + credential_schema::validate_credential_schema, credential_subject::CredentialSubject, issuer::Issuer, verifiable_credential_1_1::{ VerifiableCredential, VerifiableCredentialCreateOptions, BASE_CONTEXT, BASE_TYPE, }, }; +use crate::{ + dids::did::Did, + errors::{Result, Web5Error}, +}; +use std::time::SystemTime; +use uuid::Uuid; pub fn create_vc( issuer: Issuer, @@ -30,7 +29,7 @@ pub fn create_vc( .id .unwrap_or_else(|| format!("urn:uuid:{}", Uuid::new_v4())); - Ok(VerifiableCredential { + let verifiable_credential = VerifiableCredential { context, id, r#type, @@ -38,7 +37,13 @@ pub fn create_vc( issuance_date: options.issuance_date.unwrap_or_else(SystemTime::now), expiration_date: options.expiration_date, credential_subject, - }) + credential_schema: options.credential_schema, + evidence: options.evidence, + }; + + validate_credential_schema(&verifiable_credential)?; + + Ok(verifiable_credential) } fn validate_issuer(issuer: &Issuer) -> Result<()> { @@ -96,9 +101,11 @@ fn build_type(r#type: Option>) -> Vec { #[cfg(test)] mod tests { use super::*; + use crate::credentials::credential_schema::{CredentialSchema, CREDENTIAL_SCHEMA_TYPE}; use crate::json::JsonValue; use crate::{test_helpers::UnitTestSuite, test_name}; use lazy_static::lazy_static; + use mockito::Server; use regex::Regex; use std::collections::HashMap; @@ -111,6 +118,41 @@ mod tests { fn credential_subject() -> CredentialSubject { CredentialSubject::from(SUBJECT_DID_URI) } + fn mock_json_schema(url: String, schema_override: Option) -> String { + let schema = schema_override + .unwrap_or_else(|| "https://json-schema.org/draft/2020-12/schema".to_string()); + + format!( + r###" + {{ + "$id": "{url}/schemas/email.json", + "$schema": "{schema}", + "title": "EmailCredential", + "description": "EmailCredential using JsonSchema", + "type": "object", + "properties": {{ + "credentialSubject": {{ + "type": "object", + "properties": {{ + "emailAddress": {{ + "type": "string", + "format": "email" + }} + }}, + "required": [ + "emailAddress" + ] + }} + }} + }}"### + ) + } + fn mock_credential_schema(url: String) -> Option { + Some(CredentialSchema { + id: format!("{}/schemas/email.json", url), + r#type: CREDENTIAL_SCHEMA_TYPE.to_string(), + }) + } use crate::{credentials::issuer::ObjectIssuer, json::JsonObject}; @@ -529,4 +571,325 @@ mod tests { assert_eq!(vc.expiration_date, Some(expiration_date)); } + + #[test] + fn test_evidence_must_be_set_if_supplied() { + TEST_SUITE.include(test_name!()); + + let mut evidence_item = JsonObject::new(); + evidence_item.insert( + "A Key".to_string(), + JsonValue::String("A Value".to_string()), + ); + let evidence = vec![evidence_item]; + + let options = VerifiableCredentialCreateOptions { + evidence: Some(evidence.clone()), + ..Default::default() + }; + + let vc = create_vc(issuer(), credential_subject(), Some(options)).unwrap(); + + assert_eq!(Some(evidence), vc.evidence); + } + + #[test] + fn test_schema_type_must_be_jsonschema() { + TEST_SUITE.include(test_name!()); + + let result = create_vc( + issuer(), + credential_subject(), + Some(VerifiableCredentialCreateOptions { + credential_schema: Some(CredentialSchema { + id: "it doesn't matter".to_string(), + r#type: "invalid type".to_string(), // here + }), + ..Default::default() + }), + ); + + match result { + Err(Web5Error::Parameter(err_msg)) => { + assert_eq!(format!("type must be {}", CREDENTIAL_SCHEMA_TYPE), err_msg) + } + _ => panic!( + "expected Web5Error::Parameter with specific message but got {:?}", + result + ), + } + } + + #[test] + fn test_schema_resolve_network_issue() { + TEST_SUITE.include(test_name!()); + + let url = "invalid url".to_string(); // here + + let result = create_vc( + issuer(), + credential_subject(), + Some(VerifiableCredentialCreateOptions { + credential_schema: mock_credential_schema(url), + ..Default::default() + }), + ); + + match result { + Err(Web5Error::Network(err_msg)) => { + assert!(err_msg.contains("unable to resolve json schema")) + } + _ => panic!( + "expected Web5Error::Network with specific message but got {:?}", + result + ), + } + } + + #[test] + fn test_schema_resolve_non_success() { + TEST_SUITE.include(test_name!()); + + let mut mock_server = Server::new(); + let url = mock_server.url(); + + let _ = mock_server + .mock("GET", "/schemas/email.json") + .with_status(500) // here + .create(); + + let result = create_vc( + issuer(), + credential_subject(), + Some(VerifiableCredentialCreateOptions { + credential_schema: mock_credential_schema(url), + ..Default::default() + }), + ); + + match result { + Err(Web5Error::JsonSchema(err_msg)) => { + assert!(err_msg.contains("non-200 response when resolving json schema")) + } + _ => panic!( + "expected Web5Error::JsonSchema with specific message but got {:?}", + result + ), + } + } + + #[test] + fn test_schema_resolve_invalid_response_body() { + TEST_SUITE.include(test_name!()); + + let mut mock_server = Server::new(); + let url = mock_server.url(); + + let _ = mock_server + .mock("GET", "/schemas/email.json") + .with_status(200) + .with_header("content-type", "application/json") + .with_body("invalid response body") // here + .create(); + + let result = create_vc( + issuer(), + credential_subject(), + Some(VerifiableCredentialCreateOptions { + credential_schema: mock_credential_schema(url), + ..Default::default() + }), + ); + + match result { + Err(Web5Error::JsonSchema(err_msg)) => { + assert!(err_msg.contains("unable to parse json schema from response body")) + } + _ => panic!( + "expected Web5Error::JsonSchema with specific message but got {:?}", + result + ), + } + } + + #[test] + fn test_schema_invalid_json_schema() { + TEST_SUITE.include(test_name!()); + + let mut mock_server = Server::new(); + let url = mock_server.url(); + + let _ = mock_server + .mock("GET", "/schemas/email.json") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(&mock_json_schema( + url.clone(), + Some("this is not a valid $schema".to_string()), // here + )) + .create(); + + let result = create_vc( + issuer(), + credential_subject(), + Some(VerifiableCredentialCreateOptions { + credential_schema: mock_credential_schema(url), + ..Default::default() + }), + ); + + match result { + Err(Web5Error::JsonSchema(err_msg)) => { + assert!(err_msg.contains("unable to compile json schema")) + } + _ => panic!( + "expected Web5Error::JsonSchema with specific message but got {:?}", + result + ), + } + } + + #[test] + fn test_schema_do_not_support_draft04() { + TEST_SUITE.include(test_name!()); + + let mut mock_server = Server::new(); + let url = mock_server.url(); + + let _ = mock_server + .mock("GET", "/schemas/email.json") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(&mock_json_schema( + url.clone(), + Some("http://json-schema.org/draft-04/schema#".to_string()), // here + )) + .create(); + + let result = create_vc( + issuer(), + credential_subject(), + Some(VerifiableCredentialCreateOptions { + credential_schema: mock_credential_schema(url), + ..Default::default() + }), + ); + + match result { + Err(Web5Error::JsonSchema(err_msg)) => { + assert_eq!("draft unsupported Draft4", err_msg) + } + _ => panic!( + "expected Web5Error::JsonSchema with specific message but got {:?}", + result + ), + } + } + + #[test] + fn test_schema_do_not_support_draft06() { + TEST_SUITE.include(test_name!()); + + let mut mock_server = Server::new(); + let url = mock_server.url(); + + let _ = mock_server + .mock("GET", "/schemas/email.json") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(&mock_json_schema( + url.clone(), + Some("http://json-schema.org/draft-06/schema#".to_string()), // here + )) + .create(); + + let result = create_vc( + issuer(), + credential_subject(), + Some(VerifiableCredentialCreateOptions { + credential_schema: mock_credential_schema(url), + ..Default::default() + }), + ); + + match result { + Err(Web5Error::JsonSchema(err_msg)) => { + assert_eq!("draft unsupported Draft6", err_msg) + } + _ => panic!( + "expected Web5Error::JsonSchema with specific message but got {:?}", + result + ), + } + } + + #[test] + fn test_schema_fails_validation() { + TEST_SUITE.include(test_name!()); + + let mut mock_server = Server::new(); + let url = mock_server.url(); + + let _ = mock_server + .mock("GET", "/schemas/email.json") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(&mock_json_schema(url.clone(), None)) + .create(); + + let result = create_vc( + issuer(), + credential_subject(), // does not match schema + Some(VerifiableCredentialCreateOptions { + credential_schema: mock_credential_schema(url), + ..Default::default() + }), + ); + + match result { + Err(Web5Error::JsonSchema(err_msg)) => { + assert!(err_msg.contains("validation errors")) + } + _ => panic!( + "expected Web5Error::JsonSchema with specific message but got {:?}", + result + ), + } + } + + #[test] + fn test_schema_example_from_spec() { + TEST_SUITE.include(test_name!()); + + // using Example 1 & Example 2 from here https://www.w3.org/TR/vc-json-schema/#jsonschema + + let mut mock_server = Server::new(); + let url = mock_server.url(); + + let _ = mock_server + .mock("GET", "/schemas/email.json") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(&mock_json_schema(url.clone(), None)) + .create(); + + let mut additional_properties = JsonObject::new(); + additional_properties.insert( + "emailAddress".to_string(), + JsonValue::String("alice@tbd.email".to_string()), + ); + + let _ = create_vc( + issuer(), + CredentialSubject { + id: credential_subject().id, + additional_properties: Some(additional_properties), + }, + Some(VerifiableCredentialCreateOptions { + credential_schema: mock_credential_schema(url), + ..Default::default() + }), + ) + .unwrap(); + } } diff --git a/crates/web5/src/credentials/credential_schema.rs b/crates/web5/src/credentials/credential_schema.rs new file mode 100644 index 00000000..3efeb3ba --- /dev/null +++ b/crates/web5/src/credentials/credential_schema.rs @@ -0,0 +1,72 @@ +use super::verifiable_credential_1_1::VerifiableCredential; +use crate::errors::{Result, Web5Error}; +use jsonschema::{Draft, JSONSchema}; +use reqwest::blocking::get; +use serde::{Deserialize, Serialize}; + +pub const CREDENTIAL_SCHEMA_TYPE: &str = "JsonSchema"; + +#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] +pub struct CredentialSchema { + pub id: String, + pub r#type: String, +} + +pub(crate) fn validate_credential_schema( + verifiable_credential: &VerifiableCredential, +) -> Result<()> { + let credential_schema = match &verifiable_credential.credential_schema { + None => return Ok(()), + Some(c) => c, + }; + + if credential_schema.r#type != CREDENTIAL_SCHEMA_TYPE { + return Err(Web5Error::Parameter(format!( + "type must be {}", + CREDENTIAL_SCHEMA_TYPE + ))); + } + + let url = &credential_schema.id; + let response = get(url).map_err(|err| { + Web5Error::Network(format!("unable to resolve json schema {} {}", url, err)) + })?; + if !response.status().is_success() { + return Err(Web5Error::JsonSchema(format!( + "non-200 response when resolving json schema {} {}", + url, + response.status() + ))); + } + let schema_json = response.json::().map_err(|err| { + Web5Error::JsonSchema(format!( + "unable to parse json schema from response body {} {}", + url, err + )) + })?; + let compiled_schema = JSONSchema::options().compile(&schema_json).map_err(|err| { + Web5Error::JsonSchema(format!("unable to compile json schema {} {}", url, err)) + })?; + + let draft = compiled_schema.draft(); + if draft == Draft::Draft4 || draft == Draft::Draft6 { + return Err(Web5Error::JsonSchema(format!( + "draft unsupported {:?}", + draft + ))); + } + + let instance = serde_json::to_value(verifiable_credential)?; + let result = compiled_schema.validate(&instance); + if let Err(errors) = result { + let error_messages: Vec = errors + .map(|e| format!("{} at {}", e, e.instance_path)) + .collect(); + return Err(Web5Error::JsonSchema(format!( + "validation errors {}", + error_messages.join(", ") + ))); + } + + Ok(()) +} diff --git a/crates/web5/src/credentials/decode.rs b/crates/web5/src/credentials/decode.rs index 16aedac4..96b35cd4 100644 --- a/crates/web5/src/credentials/decode.rs +++ b/crates/web5/src/credentials/decode.rs @@ -149,5 +149,7 @@ pub fn decode(vc_jwt: &str, verify_signature: bool) -> Result, #[serde(skip_serializing_if = "Option::is_none", rename = "credentialSubject")] pub credential_subject: Option, + #[serde(rename = "credentialSchema", skip_serializing_if = "Option::is_none")] + pub credential_schema: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub evidence: Option>, } diff --git a/crates/web5/src/credentials/mod.rs b/crates/web5/src/credentials/mod.rs index 25838069..749978db 100644 --- a/crates/web5/src/credentials/mod.rs +++ b/crates/web5/src/credentials/mod.rs @@ -1,4 +1,5 @@ mod create; +mod credential_schema; mod credential_subject; mod data_model_validation; mod decode; @@ -9,6 +10,7 @@ pub mod presentation_definition; mod sign; pub mod verifiable_credential_1_1; +pub use credential_schema::CredentialSchema; pub use credential_subject::CredentialSubject; pub use issuer::{Issuer, ObjectIssuer}; diff --git a/crates/web5/src/credentials/sign.rs b/crates/web5/src/credentials/sign.rs index cb9b88d9..5a1de919 100644 --- a/crates/web5/src/credentials/sign.rs +++ b/crates/web5/src/credentials/sign.rs @@ -26,6 +26,8 @@ pub fn sign_with_signer( issuance_date: Some(vc.issuance_date), expiration_date: vc.expiration_date, credential_subject: Some(vc.credential_subject.clone()), + credential_schema: vc.credential_schema.clone(), + evidence: vc.evidence.clone(), }; payload .set_claim("vc", Some(serde_json::to_value(vc_claim)?)) diff --git a/crates/web5/src/credentials/verifiable_credential_1_1.rs b/crates/web5/src/credentials/verifiable_credential_1_1.rs index 9ad0e0dd..b54148c8 100644 --- a/crates/web5/src/credentials/verifiable_credential_1_1.rs +++ b/crates/web5/src/credentials/verifiable_credential_1_1.rs @@ -1,3 +1,5 @@ +use super::credential_schema::validate_credential_schema; +use super::credential_schema::CredentialSchema; use super::data_model_validation::validate_vc_data_model; use super::decode::decode; use super::CredentialSubject; @@ -5,6 +7,7 @@ use super::Issuer; use crate::dids::bearer_did::BearerDid; use crate::errors::Result; +use crate::json::JsonObject; use crate::json::{FromJson, ToJson}; use crate::rfc3339::{ deserialize_optional_system_time, deserialize_system_time, serialize_optional_system_time, @@ -39,18 +42,24 @@ pub struct VerifiableCredential { deserialize_with = "deserialize_optional_system_time" )] pub expiration_date: Option, + #[serde(rename = "credentialSchema", skip_serializing_if = "Option::is_none")] + pub credential_schema: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub evidence: Option>, } impl FromJson for VerifiableCredential {} impl ToJson for VerifiableCredential {} -#[derive(Default)] +#[derive(Default, Clone)] pub struct VerifiableCredentialCreateOptions { pub id: Option, pub context: Option>, pub r#type: Option>, pub issuance_date: Option, pub expiration_date: Option, + pub credential_schema: Option, + pub evidence: Option>, } impl VerifiableCredential { @@ -64,13 +73,14 @@ impl VerifiableCredential { // this function currently only supports Ed25519 pub fn from_vc_jwt(vc_jwt: &str, verify: bool) -> Result { - let vc = decode(vc_jwt, verify)?; + let verifiable_credential = decode(vc_jwt, verify)?; if verify { - validate_vc_data_model(&vc)?; + validate_vc_data_model(&verifiable_credential)?; + validate_credential_schema(&verifiable_credential)?; } - Ok(vc) + Ok(verifiable_credential) } pub fn sign( @@ -88,12 +98,15 @@ mod tests { mod from_vc_jwt { use super::*; + use crate::credentials::credential_schema::CREDENTIAL_SCHEMA_TYPE; + use crate::json::JsonValue; use crate::{credentials::CredentialError, errors::Web5Error}; use crate::{ dids::resolution::resolution_metadata::ResolutionMetadataError, test_helpers::UnitTestSuite, test_name, }; use lazy_static::lazy_static; + use mockito::{Server, ServerOpts}; lazy_static! { static ref TEST_SUITE: UnitTestSuite = @@ -111,6 +124,35 @@ mod tests { _ => panic!("Expected Web5Error::CredentialError, but got: {:?}", result), }; } + fn mock_json_schema(url: String, schema_override: Option) -> String { + let schema = schema_override + .unwrap_or_else(|| "https://json-schema.org/draft/2020-12/schema".to_string()); + + format!( + r###" + {{ + "$id": "{url}/schemas/email.json", + "$schema": "{schema}", + "title": "EmailCredential", + "description": "EmailCredential using JsonSchema", + "type": "object", + "properties": {{ + "credentialSubject": {{ + "type": "object", + "properties": {{ + "emailAddress": {{ + "type": "string", + "format": "email" + }} + }}, + "required": [ + "emailAddress" + ] + }} + }} + }}"### + ) + } #[test] fn z_assert_all_suite_cases_covered() { @@ -254,7 +296,19 @@ mod tests { } #[test] - fn test_issuer_string() { + fn test_can_skip_credential_schema_validation() { + TEST_SUITE.include(test_name!()); + + // expired would throw an error, but since verify=false it doesn't + let vc_jwt_with_invalid_schema = r#"eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSXlkMjFXVjFwblMySk1iM0JPVEhWTmRYUlNSV2gwTWtWMmJEbExkVkpFTFY5MlVrRnpWMlZwUm01TkluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjE1OTY4MDA4LTI1OTEtNGY0MS1hOWI1LWU2YmUxNDU5ZDcxNiIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lJeWQyMVdWMXBuUzJKTWIzQk9USFZOZFhSU1JXaDBNa1YyYkRsTGRWSkVMVjkyVWtGelYyVnBSbTVOSW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOC0zMFQxNDo1NTozNS41MTc5NjUrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifSwiY3JlZGVudGlhbFNjaGVtYSI6eyJpZCI6ImludmFsaWQgdXJsL3NjaGVtYXMvZW1haWwuanNvbiIsInR5cGUiOiJKc29uU2NoZW1hIn19LCJpc3MiOiJkaWQ6andrOmV5SmhiR2NpT2lKRlpESTFOVEU1SWl3aWEzUjVJam9pVDB0UUlpd2lZM0oySWpvaVJXUXlOVFV4T1NJc0luZ2lPaUl5ZDIxV1YxcG5TMkpNYjNCT1RIVk5kWFJTUldoME1rVjJiRGxMZFZKRUxWOTJVa0Z6VjJWcFJtNU5JbjAiLCJqdGkiOiJ1cm46dXVpZDoxNTk2ODAwOC0yNTkxLTRmNDEtYTliNS1lNmJlMTQ1OWQ3MTYiLCJzdWIiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkiLCJuYmYiOjE3MjUwMjk3MzUsImlhdCI6MTcyNTAyOTczNX0.8zNS9RWIpvTlMvAzaI9dNSjKKM1drFig7bKhQeQ6Mv9hWXwazvDhxthy3D25EmITWAPiJfGcMPDqoDobETf8DA"#; + + let vc = VerifiableCredential::from_vc_jwt(vc_jwt_with_invalid_schema, false) + .expect("vc_jwt should be valid"); + assert_eq!("urn:uuid:15968008-2591-4f41-a9b5-e6be1459d716", vc.id) + } + + #[test] + fn test_decode_issuer_string() { TEST_SUITE.include(test_name!()); let vc_jwt_with_issuer_as_string = r#"eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSnlkMmhYU1VOWWNsSjNiMFphUm1SMU0wbHNOaTFCTkdVdGRqazNRbE14UmtaUmFWRTRhV05tV2t0ckluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjc0NTY5ZmIzLWMyZTktNGZiMy1hOThkLWY3NGFjNzVjYTg5NSIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lKeWQyaFhTVU5ZY2xKM2IwWmFSbVIxTTBsc05pMUJOR1V0ZGprM1FsTXhSa1pSYVZFNGFXTm1Xa3RySW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOC0yOFQxNjozNjoyOS4zNDc4ODArMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSnlkMmhYU1VOWWNsSjNiMFphUm1SMU0wbHNOaTFCTkdVdGRqazNRbE14UmtaUmFWRTRhV05tV2t0ckluMCIsImp0aSI6InVybjp1dWlkOjc0NTY5ZmIzLWMyZTktNGZiMy1hOThkLWY3NGFjNzVjYTg5NSIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsIm5iZiI6MTcyNDg2Mjk4OSwiaWF0IjoxNzI0ODYyOTg5fQ.0DSZ2XbPtjtrtxNKo3tImoByb1-jlQxZQN11lsngaFSe4lhy4mYmaxGAby4wIl-c_cLEkgBULfF3Qa_dlNSTCw"#; @@ -273,7 +327,7 @@ mod tests { } #[test] - fn test_issuer_object() { + fn test_decode_issuer_object() { TEST_SUITE.include(test_name!()); let vc_jwt_with_issuer_object = r#"eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSTFVazF5YVVNMVZsaHVielpTVkRoTVdWVnJibnBKWm5OamFUUXlZbXhCYVdsTFdrcENaR2huVm5WQkluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjcwNWM0MTZiLTU1ODYtNDUzMS1hMmRmLWI3YzdhNTMxMGY5NiIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjp7ImlkIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lJMVVrMXlhVU0xVmxodWJ6WlNWRGhNV1ZWcmJucEpabk5qYVRReVlteEJhV2xMV2twQ1pHaG5WblZCSW4wIiwibmFtZSI6InNvbWUgbmFtZSJ9LCJpc3N1YW5jZURhdGUiOiIyMDI0LTA4LTI4VDE2OjQwOjExLjUwNDIyMCswMDowMCIsImV4cGlyYXRpb25EYXRlIjpudWxsLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSJ9fSwiaXNzIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lJMVVrMXlhVU0xVmxodWJ6WlNWRGhNV1ZWcmJucEpabk5qYVRReVlteEJhV2xMV2twQ1pHaG5WblZCSW4wIiwianRpIjoidXJuOnV1aWQ6NzA1YzQxNmItNTU4Ni00NTMxLWEyZGYtYjdjN2E1MzEwZjk2Iiwic3ViIjoiZGlkOmRodDpxZ21tcHlqdzVod25xZmd6bjd3bXJtMzNhZHk4Z2I4ejlpZGVpYjZtOWdqNHlzNndueTh5IiwibmJmIjoxNzI0ODYzMjExLCJpYXQiOjE3MjQ4NjMyMTF9.Mv-wlUcnj0w-OWuoMBCciaQXrAogXL3qqgZnthTRI9f55S5PidYiSapWFxFqc4SzxTVSpe64H2vF7kfGU-QpBw"#; @@ -293,7 +347,40 @@ mod tests { } #[test] - fn test_missing_vc_claim() { + fn test_decode_evidence() { + TEST_SUITE.include(test_name!()); + + let vc_jwt_with_evidence = r#"eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSmljbll0T1VKSGRUVlhNVFV4VkVKVk9GY3hVVkozU1dwSWRXVmlVVGc1TlRCQ2VuRTFjR1ZxV25wSkluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjkxYWM1NzBmLTRjMDMtNDIxZi1iZGY4LWQ3Y2YyNzQ1YzVmNSIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lKaWNuWXRPVUpIZFRWWE1UVXhWRUpWT0ZjeFVWSjNTV3BJZFdWaVVUZzVOVEJDZW5FMWNHVnFXbnBKSW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOC0zMFQxMDowMToyNC4yNTgzNjYrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifSwiZXZpZGVuY2UiOlt7IkEgS2V5IjoiQSBWYWx1ZSJ9XX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSmljbll0T1VKSGRUVlhNVFV4VkVKVk9GY3hVVkozU1dwSWRXVmlVVGc1TlRCQ2VuRTFjR1ZxV25wSkluMCIsImp0aSI6InVybjp1dWlkOjkxYWM1NzBmLTRjMDMtNDIxZi1iZGY4LWQ3Y2YyNzQ1YzVmNSIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsIm5iZiI6MTcyNTAxMjA4NCwiaWF0IjoxNzI1MDEyMDg0fQ.M7t4Ox08v-rC-naPSfIqlE1KKhZ1nrx_QA2HbuW38AkgxnSZOYEpXEG1UTAzh6mdwKZin9jwoGrj29u24K1ABA"#; + let vc = VerifiableCredential::from_vc_jwt(&vc_jwt_with_evidence, false) + .expect("should be valid vc jwt"); + + let mut evidence_item = JsonObject::new(); + evidence_item.insert( + "A Key".to_string(), + JsonValue::String("A Value".to_string()), + ); + let expected_evidence = vec![evidence_item]; + assert_eq!(Some(expected_evidence), vc.evidence); + } + + #[test] + fn test_decode_credential_schema() { + TEST_SUITE.include(test_name!()); + + let vc_jwt_with_credential_schema = r#"eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSTFUMXB2TVd4bU9VcHlOeTFZTkdGWU4yRmxka2N5WVU5MGEwWmxlSFZuYzBwcVZVbEtWVU5UYVVkUkluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjNhMDU2NjE1LWNlZDMtNGQ4Zi05ODRhLTUwMzQ2Y2FlNDQ2ZiIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lJMVQxcHZNV3htT1VweU55MVlOR0ZZTjJGbGRrY3lZVTkwYTBabGVIVm5jMHBxVlVsS1ZVTlRhVWRSSW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOC0zMFQxNDo1OToyMi4xMDMzODUrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifSwiY3JlZGVudGlhbFNjaGVtYSI6eyJpZCI6Imh0dHBzOi8vZXhhbXBsZS5jb20vc2NoZW1hcy9lbWFpbC5qc29uIiwidHlwZSI6Ikpzb25TY2hlbWEifX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSTFUMXB2TVd4bU9VcHlOeTFZTkdGWU4yRmxka2N5WVU5MGEwWmxlSFZuYzBwcVZVbEtWVU5UYVVkUkluMCIsImp0aSI6InVybjp1dWlkOjNhMDU2NjE1LWNlZDMtNGQ4Zi05ODRhLTUwMzQ2Y2FlNDQ2ZiIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsIm5iZiI6MTcyNTAyOTk2MiwiaWF0IjoxNzI1MDI5OTYyfQ.ZQkusfYLJSpfLVF9OuWrrhw8NdcBnjlalMFZbsfAxJp8i74KH47RkMsVVPadLPuKwbozgcDRCKPsokrl33TuCw"#; + let vc = VerifiableCredential::from_vc_jwt(&vc_jwt_with_credential_schema, false) + .expect("should be valid vc jwt"); + + let credential_schema = CredentialSchema { + id: "https://example.com/schemas/email.json".to_string(), + r#type: CREDENTIAL_SCHEMA_TYPE.to_string(), + }; + + assert_eq!(Some(credential_schema), vc.credential_schema) + } + + #[test] + fn test_decode_missing_vc_claim() { TEST_SUITE.include(test_name!()); let vc_jwt_with_missing_vc_claim = r#"eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSlNSbkZSVlVWS1RFOVhlbXh3T1ZaRk1rdEtSalp6UjBwT00yVnpaWHBsY0hSSE0ySTFlbTh4YjAwNEluMCMwIn0.eyJpc3MiOiJkaWQ6andrOmV5SmhiR2NpT2lKRlpESTFOVEU1SWl3aWEzUjVJam9pVDB0UUlpd2lZM0oySWpvaVJXUXlOVFV4T1NJc0luZ2lPaUpTUm5GUlZVVktURTlYZW14d09WWkZNa3RLUmpaelIwcE9NMlZ6WlhwbGNIUkhNMkkxZW04eGIwMDRJbjAiLCJqdGkiOiJ1cm46dXVpZDozNmU0ZjllNi0yYzdjLTQ0NGMtOTI4OS0zNDhmY2IxNDZlYjYiLCJzdWIiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkiLCJuYmYiOjE3MjQ4NTA1MjIsImlhdCI6MTcyNDg1MDUyMn0.SqwZC0q9RuHp9hAtFmE6sBYeJ1uHuuq1hyijF0NmW9nksSBqtDpfNroNlitK_Tl-CLWtwbTpK3b3JduTfzGEAw"#; @@ -302,7 +389,7 @@ mod tests { } #[test] - fn test_missing_jti_claim() { + fn test_decode_missing_jti_claim() { TEST_SUITE.include(test_name!()); let vc_jwt_with_missing_jti_claim = r#"eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSm5jMjlTZGsxUFlXMHliMlJQTlY4NWVqbExlV2xzV1VzM1Yzb3RZa1owWW5wdlVrWm1iVTlUTVRJNEluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjEwODM2MzgwLWI2MmMtNGVmZC04YmU0LTZhNzJiMDZjYWI4NyIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lKbmMyOVNkazFQWVcweWIyUlBOVjg1ZWpsTGVXbHNXVXMzVjNvdFlrWjBZbnB2VWtabWJVOVRNVEk0SW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOC0yOFQxMzoxMDo1NS4yMDYwOTIrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSm5jMjlTZGsxUFlXMHliMlJQTlY4NWVqbExlV2xzV1VzM1Yzb3RZa1owWW5wdlVrWm1iVTlUTVRJNEluMCIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsIm5iZiI6MTcyNDg1MDY1NSwiaWF0IjoxNzI0ODUwNjU1fQ.1XDmdvB1GDsCHw9Qwp0HA5r8W-JnZB4lz9Yqo0C2V_EEe-uk88bQSl8P9HV8ViNyBC_YaYatLiPTD4jBZY77DA"#; @@ -311,7 +398,7 @@ mod tests { } #[test] - fn test_missing_issuer_claim() { + fn test_decode_missing_issuer_claim() { TEST_SUITE.include(test_name!()); let vc_jwt_with_missing_iss_claim = r#"eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSnpjamREVWtVek1HbzNjVVU0Y2taVVJYQXdSbFJzYnpKVVVXVmlZa1ZHTVVvelJHaHRTVWhaVTNFd0luMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjRjYzU0NWU0LWI5ZDgtNDdkNS04Zjk0LTA4MmM0ZGViNzAyZCIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lKemNqZERVa1V6TUdvM2NVVTRja1pVUlhBd1JsUnNiekpVVVdWaVlrVkdNVW96UkdodFNVaFpVM0V3SW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOC0yOFQxMzoxMTo1Mi4zMjg4MTMrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifX0sImp0aSI6InVybjp1dWlkOjRjYzU0NWU0LWI5ZDgtNDdkNS04Zjk0LTA4MmM0ZGViNzAyZCIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsIm5iZiI6MTcyNDg1MDcxMiwiaWF0IjoxNzI0ODUwNzEyfQ.hwR6edt6ItlN0HHkDcxzhE3N5hLk-5-VYDLrqkalUoTKB41vsfaPvGnt_UQK3EAuekQgrTQ0SuCq-6ut0EdlBw"#; @@ -327,7 +414,7 @@ mod tests { } #[test] - fn test_missing_subject_claim() { + fn test_decode_missing_subject_claim() { TEST_SUITE.include(test_name!()); let vc_jwt_with_missing_sub_claim = r#"eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSldiRFprTjFFMWRXOTNSMVk1TWxsRlVWSkxOMnROWkdRM1lYcFJiMGxsU0hac1FXaFNSMVJmTlRJMEluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjBmYTE0MTgxLTllMWYtNDk0ZC05ZmVmLWMwYjgxZDE1ZGJiYiIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lKV2JEWmtOMUUxZFc5M1IxWTVNbGxGVVZKTE4ydE5aR1EzWVhwUmIwbGxTSFpzUVdoU1IxUmZOVEkwSW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOC0yOFQxMzoxMjo0NS40NTg4MjYrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSldiRFprTjFFMWRXOTNSMVk1TWxsRlVWSkxOMnROWkdRM1lYcFJiMGxsU0hac1FXaFNSMVJmTlRJMEluMCIsImp0aSI6InVybjp1dWlkOjBmYTE0MTgxLTllMWYtNDk0ZC05ZmVmLWMwYjgxZDE1ZGJiYiIsIm5iZiI6MTcyNDg1MDc2NSwiaWF0IjoxNzI0ODUwNzY1fQ.61IFQhdASbbcYKUzMfhO7WPmikBd8AoE468FTlqRysxXck7kNa3bAAow3jK2uhYrIWLyRu3kuBp7JyYhLavjBw"#; @@ -343,7 +430,7 @@ mod tests { } #[test] - fn test_missing_nbf_claim() { + fn test_decode_missing_nbf_claim() { TEST_SUITE.include(test_name!()); let vc_jwt_with_missing_nbf_claim = r#"eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSXdOR1ZzZGxGdlJWbDBZbEJIT0RsWlVtaGpTR2RJT1cwMlMzSjZiRVkyUWpGUldrZGxOR2RGUjJKakluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjk3OGZhZTIxLTVmMDYtNDBmNy1iZTJmLTM4MzRmZGMwZDY0NSIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lJd05HVnNkbEZ2UlZsMFlsQkhPRGxaVW1oalNHZElPVzAyUzNKNmJFWTJRakZSV2tkbE5HZEZSMkpqSW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOC0yOFQxMzoxMzoyNi4zMzQzNjYrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSXdOR1ZzZGxGdlJWbDBZbEJIT0RsWlVtaGpTR2RJT1cwMlMzSjZiRVkyUWpGUldrZGxOR2RGUjJKakluMCIsImp0aSI6InVybjp1dWlkOjk3OGZhZTIxLTVmMDYtNDBmNy1iZTJmLTM4MzRmZGMwZDY0NSIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsImlhdCI6MTcyNDg1MDgwNn0.ZXfuZmvddH1nvmub8WDpQ2UEOhuiLaN6WL2q3XDhn0eouM_bNVa7vmCUCUZc3sfJ1YCtnAGCJOlJxSGnD3tOCw"#; @@ -359,7 +446,7 @@ mod tests { } #[test] - fn test_claim_mismatch_id() { + fn test_decode_claim_mismatch_id() { TEST_SUITE.include(test_name!()); let vc_jwt_with_mismatch_id = r#"eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSnZTWHBSUjJkTmNGTlNPSEpRWTNkd1IxZEJTRnBaV0hwUFdYRlRiMFkyTWtoM09HTlJRamRJUzIxM0luMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InNvbWV0aGluZyBpbnZhbGlkIiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOiJkaWQ6andrOmV5SmhiR2NpT2lKRlpESTFOVEU1SWl3aWEzUjVJam9pVDB0UUlpd2lZM0oySWpvaVJXUXlOVFV4T1NJc0luZ2lPaUp2U1hwUlIyZE5jRk5TT0hKUVkzZHdSMWRCU0ZwWldIcFBXWEZUYjBZMk1raDNPR05SUWpkSVMyMTNJbjAiLCJpc3N1YW5jZURhdGUiOiIyMDI0LTA4LTI4VDEzOjE2OjAwLjcyMjgxOSswMDowMCIsImV4cGlyYXRpb25EYXRlIjpudWxsLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSJ9fSwiaXNzIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lKdlNYcFJSMmROY0ZOU09ISlFZM2R3UjFkQlNGcFpXSHBQV1hGVGIwWTJNa2gzT0dOUlFqZElTMjEzSW4wIiwianRpIjoidXJuOnV1aWQ6ZGFkM2Y2MjktMzFiMS00NDcxLWFhYTMtMWE4MGZjN2I1YmU2Iiwic3ViIjoiZGlkOmRodDpxZ21tcHlqdzVod25xZmd6bjd3bXJtMzNhZHk4Z2I4ejlpZGVpYjZtOWdqNHlzNndueTh5IiwibmJmIjoxNzI0ODUwOTYwLCJpYXQiOjE3MjQ4NTA5NjB9.P8-Z3KsMxIk7-Dz9a5odVhbGJZtWsWp4mDVYLlVxuZTNJl-Km-j2S1KusTjRTDkg1DqQoiVvp2Is0kr5WoAFBA"#; @@ -375,7 +462,7 @@ mod tests { } #[test] - fn test_claim_mismatch_issuer() { + fn test_decode_claim_mismatch_issuer() { TEST_SUITE.include(test_name!()); let vc_jwt_with_mismatch_issuer = r#"eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSXpWRVZsYWs0emIzSXpUbXR4WkZWVllYQjZaMVZ5TFcxblZFTkNkWEZRWVZkT1JWcE9lRXcwWkhRd0luMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjJiNzQzNWY0LWU0YjctNGQyZC1iN2M2LTVkOTE5ODRlNDlhOCIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoic29tZXRoaW5nIGludmFsaWQiLCJpc3N1YW5jZURhdGUiOiIyMDI0LTA4LTI4VDEzOjE3OjQ1LjI4ODk2NiswMDowMCIsImV4cGlyYXRpb25EYXRlIjpudWxsLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSJ9fSwiaXNzIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lJelZFVmxhazR6YjNJelRtdHhaRlZWWVhCNloxVnlMVzFuVkVOQ2RYRlFZVmRPUlZwT2VFdzBaSFF3SW4wIiwianRpIjoidXJuOnV1aWQ6MmI3NDM1ZjQtZTRiNy00ZDJkLWI3YzYtNWQ5MTk4NGU0OWE4Iiwic3ViIjoiZGlkOmRodDpxZ21tcHlqdzVod25xZmd6bjd3bXJtMzNhZHk4Z2I4ejlpZGVpYjZtOWdqNHlzNndueTh5IiwibmJmIjoxNzI0ODUxMDY1LCJpYXQiOjE3MjQ4NTEwNjV9.x0UY38J4lEwmrXR4qrzhnk58btjZfMf8DVhdgBoj9M0JOgJqCDFCzwcS5weVCpNAv3gN72Qo32RH9Tx0eYyoDA"#; @@ -391,7 +478,7 @@ mod tests { } #[test] - fn test_claim_mismatch_subject() { + fn test_decode_claim_mismatch_subject() { TEST_SUITE.include(test_name!()); let vc_jwt_with_mismatch_subject = r#"eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSXdVRmh0UkVNMlNIWnVia1E0Vmw5QkxWbDVSelZ1TWtSa2IxQkdTVFkxY2tkb2MwVTVZWFZsWW5CckluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjAwNDJiYTQ4LWU0ZGYtNGVhMS04ZmJjLWJjYmI4ODY3ZjFhMCIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lJd1VGaHRSRU0yU0hadWJrUTRWbDlCTFZsNVJ6VnVNa1JrYjFCR1NUWTFja2RvYzBVNVlYVmxZbkJySW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOC0yOFQxMzoxOToxMC4xNjM0ODkrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJzb21ldGhpbmcgaW52YWxpZCJ9fSwiaXNzIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lJd1VGaHRSRU0yU0hadWJrUTRWbDlCTFZsNVJ6VnVNa1JrYjFCR1NUWTFja2RvYzBVNVlYVmxZbkJySW4wIiwianRpIjoidXJuOnV1aWQ6MDA0MmJhNDgtZTRkZi00ZWExLThmYmMtYmNiYjg4NjdmMWEwIiwic3ViIjoiZGlkOmRodDpxZ21tcHlqdzVod25xZmd6bjd3bXJtMzNhZHk4Z2I4ejlpZGVpYjZtOWdqNHlzNndueTh5IiwibmJmIjoxNzI0ODUxMTUwLCJpYXQiOjE3MjQ4NTExNTB9.bAm9kKJX2-Rcw679VS7cUPbqg9awuq5Lwu9wiZoGcE0TCSc59rQTIP4nvxlP22o3V-VVs_DbfpJU-qB4duDSCA"#; @@ -407,7 +494,7 @@ mod tests { } #[test] - fn test_claim_misconfigured_exp() { + fn test_decode_claim_misconfigured_exp() { TEST_SUITE.include(test_name!()); let vc_jwt_with_misconfigured_exp = r#"eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSnJkWFI2V21WM01EVTBMVlUwUVRBM2FsYzJZbkkxUlV4NU1UQlpOSGxPVTFCaVkyOTNXakJ3TjJWakluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjYxZjgwM2I4LWUxMDQtNDdhOC04YWE1LTk4YzQ1ZTFiOGUzMSIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lKcmRYUjZXbVYzTURVMExWVTBRVEEzYWxjMlluSTFSVXg1TVRCWk5IbE9VMUJpWTI5M1dqQndOMlZqSW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOC0yOFQxMzoyMzo0My45NDg4MzQrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6IjIwMjktMDgtMjJUMTM6MjM6NDMuOTQ4NzYwKzAwOjAwIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSnJkWFI2V21WM01EVTBMVlUwUVRBM2FsYzJZbkkxUlV4NU1UQlpOSGxPVTFCaVkyOTNXakJ3TjJWakluMCIsImp0aSI6InVybjp1dWlkOjYxZjgwM2I4LWUxMDQtNDdhOC04YWE1LTk4YzQ1ZTFiOGUzMSIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsIm5iZiI6MTcyNDg1MTQyMywiaWF0IjoxNzI0ODUxNDIzfQ.AWYyvLRISXwLH5gAXb5CcwBXNwaRKwacGqstXjnk-xIHx9gmm5xj8zGONvcKE2Xx0t9j3pNHicrhkp5wcOkABQ"#; @@ -428,7 +515,7 @@ mod tests { } #[test] - fn test_claim_mismatch_exp() { + fn test_decode_claim_mismatch_exp() { TEST_SUITE.include(test_name!()); let vc_jwt_with_mismatch_exp = r#"eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSk1lWEJmUjJVelVEZGtjbVZhYTJSV1VsTnJZbmROVldkcVkxUTRhMHd6VUVVMk1Hc3pZMGgzVTJ0ckluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjRhMjA2YmMzLWZmOTYtNDMwNS1iMzM4LTJiZGQ1ODRiYzkyOSIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lKTWVYQmZSMlV6VURka2NtVmFhMlJXVWxOclluZE5WV2RxWTFRNGEwd3pVRVUyTUdzelkwaDNVMnRySW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOC0yOFQxMzoyNzozMy40Mjg1NjMrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6IjIwMjktMDgtMjJUMTM6Mjc6MzMuNDI4NDgyKzAwOjAwIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSk1lWEJmUjJVelVEZGtjbVZhYTJSV1VsTnJZbmROVldkcVkxUTRhMHd6VUVVMk1Hc3pZMGgzVTJ0ckluMCIsImp0aSI6InVybjp1dWlkOjRhMjA2YmMzLWZmOTYtNDMwNS1iMzM4LTJiZGQ1ODRiYzkyOSIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsIm5iZiI6MTcyNDg1MTY1MywiaWF0IjoxNzI0ODUxNjUzLCJleHAiOjE4ODUxMjM2NTN9.lAaTG8RhL2D92iNI6psZrv1uhtHYAO0m0AacGIQrW0XIThg-Livef36_CN9t4Lz2Ta5US2Be2VP6D3lCA-z1DQ"#; @@ -619,5 +706,258 @@ mod tests { ), }; } + + #[test] + fn test_schema_type_must_be_jsonschema() { + TEST_SUITE.include(test_name!()); + + let vc_jwt_with_wrong_schema_type = r#"eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSXhlbTlYY0VWTWN6TnhiV0p5VW5GblRFbzBjbDlCZUhCYVNFSmpjMUZJVGtSaVRGYzBOM1JmVGpkSkluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjQ1NGY1NWJmLWYzMjAtNDQyOS1iNmViLTRkMzdlNTMzNDkwYyIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lJeGVtOVhjRVZNY3pOeGJXSnlVbkZuVEVvMGNsOUJlSEJhU0VKamMxRklUa1JpVEZjME4zUmZUamRKSW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOC0zMFQxNTowMjo1NC4zNjg1NjMrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifSwiY3JlZGVudGlhbFNjaGVtYSI6eyJpZCI6Imh0dHBzOi8vZXhhbXBsZS5jb20iLCJ0eXBlIjoic29tZXRoaW5nIGludmFsaWFkIn19LCJpc3MiOiJkaWQ6andrOmV5SmhiR2NpT2lKRlpESTFOVEU1SWl3aWEzUjVJam9pVDB0UUlpd2lZM0oySWpvaVJXUXlOVFV4T1NJc0luZ2lPaUl4ZW05WGNFVk1jek54YldKeVVuRm5URW8wY2w5QmVIQmFTRUpqYzFGSVRrUmlURmMwTjNSZlRqZEpJbjAiLCJqdGkiOiJ1cm46dXVpZDo0NTRmNTViZi1mMzIwLTQ0MjktYjZlYi00ZDM3ZTUzMzQ5MGMiLCJzdWIiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkiLCJuYmYiOjE3MjUwMzAxNzQsImlhdCI6MTcyNTAzMDE3NH0.8gxVx3_Qd1Lvao-y5PZ56XS3lMQvrFtBVMgfNDIdW9eoQkBQMNv79YKIxFCig0LHanzg_vyzX7tBviW6xJUuDw"#; + + let result = VerifiableCredential::from_vc_jwt(vc_jwt_with_wrong_schema_type, true); + + match result { + Err(Web5Error::Parameter(err_msg)) => { + assert_eq!(format!("type must be {}", CREDENTIAL_SCHEMA_TYPE), err_msg) + } + _ => panic!( + "expected Web5Error::Parameter with specific message but got {:?}", + result + ), + }; + } + + #[test] + fn test_schema_resolve_network_issue() { + TEST_SUITE.include(test_name!()); + + let vc_jwt_with_invalid_url = r#"eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSmZYelYxVEU1bWNVWTRRbTB6ZVhnMmJVRndMVlJJV25sSk5WcDJWQzFmYVVKbExWZDJiMHRuTTFwakluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOmRlNDY2N2YxLTMzM2ItNDg4OC1hMDc5LTdkMGU1N2JiZmFlZiIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lKZlh6VjFURTVtY1VZNFFtMHplWGcyYlVGd0xWUklXbmxKTlZwMlZDMWZhVUpsTFZkMmIwdG5NMXBqSW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOC0zMFQxNTowNToyMC43NjQ0MDgrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifSwiY3JlZGVudGlhbFNjaGVtYSI6eyJpZCI6ImludmFsaWQgdXJsIiwidHlwZSI6Ikpzb25TY2hlbWEifX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSmZYelYxVEU1bWNVWTRRbTB6ZVhnMmJVRndMVlJJV25sSk5WcDJWQzFmYVVKbExWZDJiMHRuTTFwakluMCIsImp0aSI6InVybjp1dWlkOmRlNDY2N2YxLTMzM2ItNDg4OC1hMDc5LTdkMGU1N2JiZmFlZiIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsIm5iZiI6MTcyNTAzMDMyMCwiaWF0IjoxNzI1MDMwMzIwfQ.3sH7qzI7QrQMdkWIvqf7k8Mr2dMGjWBLrv4QB8gEz0t83RSFMtG-fWT-YVkUlo1qMvC4gNjT2Jc0eObCAA7VDQ"#; + + let result = VerifiableCredential::from_vc_jwt(vc_jwt_with_invalid_url, true); + + match result { + Err(Web5Error::Network(err_msg)) => { + assert!(err_msg.contains("unable to resolve json schema")) + } + _ => panic!( + "expected Web5Error::Network with specific message but got {:?}", + result + ), + }; + } + + #[test] + fn test_schema_resolve_non_success() { + TEST_SUITE.include(test_name!()); + + let mut mock_server = Server::new_with_opts(ServerOpts { + port: 40001, + ..Default::default() + }); + + let _ = mock_server + .mock("GET", "/schemas/email.json") + .with_status(500) // here + .create(); + + let vc_jwt_at_port = r#"eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSm9jMmhVZEU4M2F5MVNjVE5oVWtWR1lUVTRhUzFoWlZVdGRWaHNUVXB4UkdkallteEhhMTlpTW5OM0luMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOmE5Nzk1YTdmLTRmNzktNDU3OC1hYTkxLTcwYmYxM2YxZWVkNiIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lKb2MyaFVkRTgzYXkxU2NUTmhVa1ZHWVRVNGFTMWhaVlV0ZFZoc1RVcHhSR2RqWW14SGExOWlNbk4zSW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOS0wM1QxNTo0MDowMS4wNDg1MzIrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifSwiY3JlZGVudGlhbFNjaGVtYSI6eyJpZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDAwMDEvc2NoZW1hcy9lbWFpbC5qc29uIiwidHlwZSI6Ikpzb25TY2hlbWEifX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSm9jMmhVZEU4M2F5MVNjVE5oVWtWR1lUVTRhUzFoWlZVdGRWaHNUVXB4UkdkallteEhhMTlpTW5OM0luMCIsImp0aSI6InVybjp1dWlkOmE5Nzk1YTdmLTRmNzktNDU3OC1hYTkxLTcwYmYxM2YxZWVkNiIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsIm5iZiI6MTcyNTM3ODAwMSwiaWF0IjoxNzI1Mzc4MDAxfQ.9739UnhTfGXr2tsbsjun7FQfFuXNtqmzfxhP_okbywVDoh6nsBGk8smLUU_D0VYwtiMBTo1ujDs1QtKPbCZDDA"#; + + let result = VerifiableCredential::from_vc_jwt(vc_jwt_at_port, true); + match result { + Err(Web5Error::JsonSchema(err_msg)) => { + assert!(err_msg.contains("non-200 response when resolving json schema")) + } + _ => panic!( + "expected Web5Error::JsonSchema with specific message but got {:?}", + result + ), + } + } + + #[test] + fn test_schema_resolve_invalid_response_body() { + TEST_SUITE.include(test_name!()); + + let mut mock_server = Server::new_with_opts(ServerOpts { + port: 40002, + ..Default::default() + }); + + let _ = mock_server + .mock("GET", "/schemas/email.json") + .with_status(200) + .with_header("content-type", "application/json") + .with_body("invalid response body") // here + .create(); + + let vc_jwt_at_port = r#"eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSXhkek4zU25CUlZIVkNhRUZuV2tSd2JtbHZWME51ZW5kalp6bDBkMFZ4WVZGWldFUTVOblJXUTA1QkluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOmQ1NmYxMzRjLThjN2QtNDkyOC04OWYwLWQ5NWEzYjllZmU3YiIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lJeGR6TjNTbkJSVkhWQ2FFRm5Xa1J3Ym1sdlYwTnVlbmRqWnpsMGQwVnhZVkZaV0VRNU5uUldRMDVCSW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOS0wM1QxNTo0Mzo1OC40MTE0NTcrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifSwiY3JlZGVudGlhbFNjaGVtYSI6eyJpZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDAwMDIvc2NoZW1hcy9lbWFpbC5qc29uIiwidHlwZSI6Ikpzb25TY2hlbWEifX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSXhkek4zU25CUlZIVkNhRUZuV2tSd2JtbHZWME51ZW5kalp6bDBkMFZ4WVZGWldFUTVOblJXUTA1QkluMCIsImp0aSI6InVybjp1dWlkOmQ1NmYxMzRjLThjN2QtNDkyOC04OWYwLWQ5NWEzYjllZmU3YiIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsIm5iZiI6MTcyNTM3ODIzOCwiaWF0IjoxNzI1Mzc4MjM4fQ.vYZ5YeXa4ZXaomhVp2obgJwlgjwScFctNAJBqTf2hJOUr1v-jN1C5huK4JL_e16_dRCJd_ysmiOpgFOJD2MOCQ"#; + + let result = VerifiableCredential::from_vc_jwt(vc_jwt_at_port, true); + match result { + Err(Web5Error::JsonSchema(err_msg)) => { + assert!(err_msg.contains("unable to parse json schema from response body")) + } + _ => panic!( + "expected Web5Error::JsonSchema with specific message but got {:?}", + result + ), + } + } + + #[test] + fn test_schema_invalid_json_schema() { + TEST_SUITE.include(test_name!()); + + let mut mock_server = Server::new_with_opts(ServerOpts { + port: 40003, + ..Default::default() + }); + + let _ = mock_server + .mock("GET", "/schemas/email.json") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(&mock_json_schema( + "http://localhost:40003/schemas/email.json".to_string(), + Some("this is not a valid $schema".to_string()), // here + )) + .create(); + + let vc_jwt_at_port = r#"eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSlRNemRDWVdaR01FTnRla2xmVTJ4WlEyTXlNSHBKZGt4eVprTnFjM1ptTUVWUWFtbDVkV2N5Wmt0WkluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjJjZWFmODUxLTBiYzktNDFkYy04NzNmLThhMGMyN2Y0ZDZkOCIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lKVE16ZENZV1pHTUVOdGVrbGZVMnhaUTJNeU1IcEpka3h5WmtOcWMzWm1NRVZRYW1sNWRXY3laa3RaSW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOS0wM1QxNTo0NTozMC4zMDY4MDYrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifSwiY3JlZGVudGlhbFNjaGVtYSI6eyJpZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDAwMDMvc2NoZW1hcy9lbWFpbC5qc29uIiwidHlwZSI6Ikpzb25TY2hlbWEifX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSlRNemRDWVdaR01FTnRla2xmVTJ4WlEyTXlNSHBKZGt4eVprTnFjM1ptTUVWUWFtbDVkV2N5Wmt0WkluMCIsImp0aSI6InVybjp1dWlkOjJjZWFmODUxLTBiYzktNDFkYy04NzNmLThhMGMyN2Y0ZDZkOCIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsIm5iZiI6MTcyNTM3ODMzMCwiaWF0IjoxNzI1Mzc4MzMwfQ.Drh8iEOdWWeL6l9KdmKMv9qfbBxWln-TW0KNwOJN3lZatyaSkwlBO_1o2FIWj0WIDiD_TP2EestMFazf4XFQDA"#; + + let result = VerifiableCredential::from_vc_jwt(vc_jwt_at_port, true); + match result { + Err(Web5Error::JsonSchema(err_msg)) => { + assert!(err_msg.contains("unable to compile json schema")) + } + _ => panic!( + "expected Web5Error::JsonSchema with specific message but got {:?}", + result + ), + } + } + + #[test] + fn test_schema_do_not_support_draft04() { + TEST_SUITE.include(test_name!()); + + let mut mock_server = Server::new_with_opts(ServerOpts { + port: 40004, + ..Default::default() + }); + + let _ = mock_server + .mock("GET", "/schemas/email.json") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(&mock_json_schema( + "http://localhost:40004/schemas/email.json".to_string(), + Some("http://json-schema.org/draft-04/schema#".to_string()), // here + )) + .create(); + + let vc_jwt_at_port = r#"eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSnBNemMwTFVkc1lVSmZiMmxQZG1aR1ZteGtiVWhhVXpNNVpEZEtlalUxUm0xU2R6WkVjbmswVkRjd0luMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOmYzNDc3ZDdkLWJiOWUtNGI5Ny1iYjhkLTk4NDMwNjgzN2RmMyIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lKcE16YzBMVWRzWVVKZmIybFBkbVpHVm14a2JVaGFVek01WkRkS2VqVTFSbTFTZHpaRWNuazBWRGN3SW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOS0wM1QxNTo0Nzo0MS4wNjgxNjIrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifSwiY3JlZGVudGlhbFNjaGVtYSI6eyJpZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDAwMDQvc2NoZW1hcy9lbWFpbC5qc29uIiwidHlwZSI6Ikpzb25TY2hlbWEifX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSnBNemMwTFVkc1lVSmZiMmxQZG1aR1ZteGtiVWhhVXpNNVpEZEtlalUxUm0xU2R6WkVjbmswVkRjd0luMCIsImp0aSI6InVybjp1dWlkOmYzNDc3ZDdkLWJiOWUtNGI5Ny1iYjhkLTk4NDMwNjgzN2RmMyIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsIm5iZiI6MTcyNTM3ODQ2MSwiaWF0IjoxNzI1Mzc4NDYxfQ.X7pZBMqPeBO0oTq1QNtMcSrYcIpDxCavPEoPDiB1A9GOqCohx7KCgOerXaJGSyklAkmNJod7ssmL4DMM-l3uDA"#; + + let result = VerifiableCredential::from_vc_jwt(vc_jwt_at_port, true); + match result { + Err(Web5Error::JsonSchema(err_msg)) => { + assert_eq!("draft unsupported Draft4", err_msg) + } + _ => panic!( + "expected Web5Error::JsonSchema with specific message but got {:?}", + result + ), + } + } + + #[test] + fn test_schema_do_not_support_draft06() { + TEST_SUITE.include(test_name!()); + + let mut mock_server = Server::new_with_opts(ServerOpts { + port: 40005, + ..Default::default() + }); + + let _ = mock_server + .mock("GET", "/schemas/email.json") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(&mock_json_schema( + "http://localhost:40005/schemas/email.json".to_string(), + Some("http://json-schema.org/draft-06/schema#".to_string()), // here + )) + .create(); + + let vc_jwt_at_port = r#"eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSkhSemxDUVU1QlpXUkplR2RpYzJkVlprOWZRWFpPWVZsWmFHMWxZbmhXYWpOZlVuQnBWREp4ZG5WVkluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjQ4MGM0ZjQ5LTAyMmEtNDIwMi1hYjFiLTc1ZThjZjQ1NDEyMyIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lKSFJ6bENRVTVCWldSSmVHZGljMmRWWms5ZlFYWk9ZVmxaYUcxbFluaFdhak5mVW5CcFZESnhkblZWSW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOS0wM1QxNTo0ODo1MC4wNjM4NjIrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifSwiY3JlZGVudGlhbFNjaGVtYSI6eyJpZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDAwMDUvc2NoZW1hcy9lbWFpbC5qc29uIiwidHlwZSI6Ikpzb25TY2hlbWEifX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSkhSemxDUVU1QlpXUkplR2RpYzJkVlprOWZRWFpPWVZsWmFHMWxZbmhXYWpOZlVuQnBWREp4ZG5WVkluMCIsImp0aSI6InVybjp1dWlkOjQ4MGM0ZjQ5LTAyMmEtNDIwMi1hYjFiLTc1ZThjZjQ1NDEyMyIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsIm5iZiI6MTcyNTM3ODUzMCwiaWF0IjoxNzI1Mzc4NTMwfQ.Id6rsvMkqjocv6x5g_s8fnfR74HdXIpIdL3bMR33f1FYPFQ9CZRArMc1ZTh3xL3QfUggY8AUkRraQSAJh_onBA"#; + + let result = VerifiableCredential::from_vc_jwt(vc_jwt_at_port, true); + match result { + Err(Web5Error::JsonSchema(err_msg)) => { + assert_eq!("draft unsupported Draft6", err_msg) + } + _ => panic!( + "expected Web5Error::JsonSchema with specific message but got {:?}", + result + ), + } + } + + #[test] + fn test_schema_fails_validation() { + TEST_SUITE.include(test_name!()); + + let mut mock_server = Server::new_with_opts(ServerOpts { + port: 40006, + ..Default::default() + }); + + let _ = mock_server + .mock("GET", "/schemas/email.json") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(&mock_json_schema( + "http://localhost/schemas/email.json".to_string(), + None, + )) + .create(); + + let vc_jwt_at_port_with_invalid_schema = r#"eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSnBlRkpyUVhodk5GTmtVMXAwTW1VMGFWQm5WRTV0T1dnelFYWnJYMU42Wm1WUlNVeFNkbFJHU0RSSkluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOmY4NThhZGM2LTZhMDQtNDY5ZC04MGJiLWM2MmI1N2MyNWI5NSIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lKcGVGSnJRWGh2TkZOa1UxcDBNbVUwYVZCblZFNXRPV2d6UVhaclgxTjZabVZSU1V4U2RsUkdTRFJKSW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOS0wM1QxNTo1MDozOS4yMTM3NzIrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkifSwiY3JlZGVudGlhbFNjaGVtYSI6eyJpZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDAwMDYvc2NoZW1hcy9lbWFpbC5qc29uIiwidHlwZSI6Ikpzb25TY2hlbWEifX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSnBlRkpyUVhodk5GTmtVMXAwTW1VMGFWQm5WRTV0T1dnelFYWnJYMU42Wm1WUlNVeFNkbFJHU0RSSkluMCIsImp0aSI6InVybjp1dWlkOmY4NThhZGM2LTZhMDQtNDY5ZC04MGJiLWM2MmI1N2MyNWI5NSIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsIm5iZiI6MTcyNTM3ODYzOSwiaWF0IjoxNzI1Mzc4NjM5fQ.zxz0OZO1umdlxgRyrwyMJis4t4esE_7Zo6nma4Q8T4josAkw6vnQJFI_cPoZV4usQ1vve5bB5OOiMcf_tca6Cw"#; + + let result = + VerifiableCredential::from_vc_jwt(vc_jwt_at_port_with_invalid_schema, true); + match result { + Err(Web5Error::JsonSchema(err_msg)) => { + assert!(err_msg.contains("validation errors")) + } + _ => panic!( + "expected Web5Error::JsonSchema with specific message but got {:?}", + result + ), + } + } + + #[test] + fn test_schema_example_from_spec() { + TEST_SUITE.include(test_name!()); + + let mut mock_server = Server::new_with_opts(ServerOpts { + port: 40007, + ..Default::default() + }); + + let _ = mock_server + .mock("GET", "/schemas/email.json") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(&mock_json_schema( + "http://localhost:40007/schemas/email.json".to_string(), + None, + )) + .create(); + + let vc_jwt_at_port = r#"eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSkZjVm96YVdGVldqWnpUMlZ5VEVGMWEzcEpRbkV6Ym1sNVEzZHVVWEF0WWtkNk1EQlZMVk15YURGakluMCMwIn0.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJpZCI6InVybjp1dWlkOjM4OWE2OWYyLWEwOTYtNDc0Ni1hNzU0LTRlNmE0ZThiZDEzYyIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpoYkdjaU9pSkZaREkxTlRFNUlpd2lhM1I1SWpvaVQwdFFJaXdpWTNKMklqb2lSV1F5TlRVeE9TSXNJbmdpT2lKRmNWb3phV0ZWV2paelQyVnlURUYxYTNwSlFuRXpibWw1UTNkdVVYQXRZa2Q2TURCVkxWTXlhREZqSW4wIiwiaXNzdWFuY2VEYXRlIjoiMjAyNC0wOS0wM1QxNTo1MzoxNy43NjgzNzQrMDA6MDAiLCJleHBpcmF0aW9uRGF0ZSI6bnVsbCwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZGh0OnFnbW1weWp3NWh3bnFmZ3puN3dtcm0zM2FkeThnYjh6OWlkZWliNm05Z2o0eXM2d255OHkiLCJlbWFpbEFkZHJlc3MiOiJhbGljZUB0YmQuZW1haWwifSwiY3JlZGVudGlhbFNjaGVtYSI6eyJpZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NDAwMDcvc2NoZW1hcy9lbWFpbC5qc29uIiwidHlwZSI6Ikpzb25TY2hlbWEifX0sImlzcyI6ImRpZDpqd2s6ZXlKaGJHY2lPaUpGWkRJMU5URTVJaXdpYTNSNUlqb2lUMHRRSWl3aVkzSjJJam9pUldReU5UVXhPU0lzSW5naU9pSkZjVm96YVdGVldqWnpUMlZ5VEVGMWEzcEpRbkV6Ym1sNVEzZHVVWEF0WWtkNk1EQlZMVk15YURGakluMCIsImp0aSI6InVybjp1dWlkOjM4OWE2OWYyLWEwOTYtNDc0Ni1hNzU0LTRlNmE0ZThiZDEzYyIsInN1YiI6ImRpZDpkaHQ6cWdtbXB5anc1aHducWZnem43d21ybTMzYWR5OGdiOHo5aWRlaWI2bTlnajR5czZ3bnk4eSIsIm5iZiI6MTcyNTM3ODc5NywiaWF0IjoxNzI1Mzc4Nzk3fQ.AC1OGOJ-MxfRsyNJZEQM4PW_t1eNCiSdTNtEtiPPOnIDGYnDl7JGtVIki9tHdWduQHoanrV0dWDeB5dnTTmZAw"#; + + let _ = VerifiableCredential::from_vc_jwt(vc_jwt_at_port, true).unwrap(); + } } } diff --git a/crates/web5/src/errors.rs b/crates/web5/src/errors.rs index 01ffc897..eade3cda 100644 --- a/crates/web5/src/errors.rs +++ b/crates/web5/src/errors.rs @@ -11,6 +11,8 @@ pub enum Web5Error { Unknown(String), #[error("json error {0}")] Json(String), + #[error("json schema error {0}")] + JsonSchema(String), #[error("parameter error {0}")] Parameter(String), #[error("data member error {0}")] diff --git a/crates/web5/src/json.rs b/crates/web5/src/json.rs index 8e71cc76..d3594258 100644 --- a/crates/web5/src/json.rs +++ b/crates/web5/src/json.rs @@ -31,6 +31,9 @@ pub struct JsonObject { pub properties: HashMap, } +impl FromJson for JsonObject {} +impl ToJson for JsonObject {} + impl JsonObject { pub fn new() -> Self { Self { diff --git a/docs/API_DESIGN.md b/docs/API_DESIGN.md index 18e49863..84dc286c 100644 --- a/docs/API_DESIGN.md +++ b/docs/API_DESIGN.md @@ -12,8 +12,9 @@ - [`VerifiableCredential`](#verifiablecredential) - [`CredentialSubject`](#credentialsubject) - [`Issuer`](#issuer) + - [`Evidence`](#evidence) - [`CredentialStatus`](#credentialstatus) - - [`CreateOptions`](#createoptions) + - [`VerifiableCredentialCreateOptions`](#verifiablecredentialcreateoptions) - [StatusListCredential](#statuslistcredential) - [`StatusListCredential`](#statuslistcredential-1) - [Presentation Exchange (PEX)](#presentation-exchange-pex) @@ -90,8 +91,9 @@ CLASS VerifiableCredential PUBLIC DATA issuance_date: datetime PUBLIC DATA expiration_date: datetime? PUBLIC DATA credentialSubject: CredentialSubject + PUBLIC DATA evidence: []Evidence? - CONSTRUCTOR create(issuer: Issuer, credential_subject: CredentialSubject, options: CreateOptions?) + CONSTRUCTOR create(issuer: Issuer, credential_subject: CredentialSubject, options: VerifiableCredentialCreateOptions?) CONSTRUCTOR from_vc_jwt(vc_jwt: string, verify: bool) METHOD sign(bearer_did: BearerDid, verification_method_id: String?): string @@ -105,6 +107,10 @@ CLASS VerifiableCredential `Object` or `string`, and if `Object` then at least non-empty `id: string` and `name: string` data members. +##### `Evidence` + +`Object` with any data members. + ##### `CredentialStatus` ```pseudocode! @@ -116,16 +122,17 @@ CLASS CredentialStatus PUBLIC DATA status_list_credential: string ``` -##### `CreateOptions` +##### `VerifiableCredentialCreateOptions` ```psuedocode! -CLASS CreateOptions +CLASS VerifiableCredentialCreateOptions PUBLIC DATA id: string? PUBLIC DATA context: []string? PUBLIC DATA type: []string? PUBLIC DATA issuance_date: datetime? PUBLIC DATA expiration_date: datetime? PUBLIC DATA credential_status: CredentialStatus? + PUBLIC DATA evidence: []Evidence? ``` ## StatusListCredential diff --git a/tests/unit_test_cases/verifiable_credential_1_1_create.json b/tests/unit_test_cases/verifiable_credential_1_1_create.json index bd7ae02c..82b81eff 100644 --- a/tests/unit_test_cases/verifiable_credential_1_1_create.json +++ b/tests/unit_test_cases/verifiable_credential_1_1_create.json @@ -20,5 +20,15 @@ "test_credential_subject_supports_additional_properties", "test_issuance_date_must_be_set", "test_issuance_date_must_be_now_if_not_supplied", - "test_expiration_date_must_be_set_if_supplied" + "test_expiration_date_must_be_set_if_supplied", + "test_evidence_must_be_set_if_supplied", + "test_schema_type_must_be_jsonschema", + "test_schema_resolve_network_issue", + "test_schema_resolve_non_success", + "test_schema_resolve_invalid_response_body", + "test_schema_invalid_json_schema", + "test_schema_do_not_support_draft04", + "test_schema_do_not_support_draft06", + "test_schema_fails_validation", + "test_schema_example_from_spec" ] diff --git a/tests/unit_test_cases/verifiable_credential_1_1_from_vc_jwt.json b/tests/unit_test_cases/verifiable_credential_1_1_from_vc_jwt.json index 6588b3b5..e4ad3d8b 100644 --- a/tests/unit_test_cases/verifiable_credential_1_1_from_vc_jwt.json +++ b/tests/unit_test_cases/verifiable_credential_1_1_from_vc_jwt.json @@ -8,18 +8,21 @@ "test_passes_cryptographic_verification", "test_can_skip_cryptographic_verification", "test_can_skip_data_model_validation", - "test_issuer_string", - "test_issuer_object", - "test_missing_vc_claim", - "test_missing_jti_claim", - "test_missing_issuer_claim", - "test_missing_subject_claim", - "test_missing_nbf_claim", - "test_claim_mismatch_id", - "test_claim_mismatch_issuer", - "test_claim_mismatch_subject", - "test_claim_misconfigured_exp", - "test_claim_mismatch_exp", + "test_can_skip_credential_schema_validation", + "test_decode_issuer_string", + "test_decode_issuer_object", + "test_decode_evidence", + "test_decode_credential_schema", + "test_decode_missing_vc_claim", + "test_decode_missing_jti_claim", + "test_decode_missing_issuer_claim", + "test_decode_missing_subject_claim", + "test_decode_missing_nbf_claim", + "test_decode_claim_mismatch_id", + "test_decode_claim_mismatch_issuer", + "test_decode_claim_mismatch_subject", + "test_decode_claim_misconfigured_exp", + "test_decode_claim_mismatch_exp", "test_validate_dm_empty_id", "test_validate_dm_empty_context", "test_validate_dm_context_base_mismatch", @@ -28,5 +31,14 @@ "test_validate_dm_empty_issuer", "test_validate_dm_empty_subject", "test_validate_dm_issuance_in_future", - "test_validate_dm_credential_expired" + "test_validate_dm_credential_expired", + "test_schema_type_must_be_jsonschema", + "test_schema_resolve_network_issue", + "test_schema_resolve_non_success", + "test_schema_resolve_invalid_response_body", + "test_schema_invalid_json_schema", + "test_schema_do_not_support_draft04", + "test_schema_do_not_support_draft06", + "test_schema_fails_validation", + "test_schema_example_from_spec" ]