From d382217aa65d0a1208b2f103afb99c77aeb8e663 Mon Sep 17 00:00:00 2001 From: Neal Date: Fri, 16 Feb 2024 11:39:19 -0600 Subject: [PATCH 01/10] remove danubech --- .../sdk/credentials/StatusListCredential.kt | 50 +- .../sdk/credentials/VerifiableCredential.kt | 29 +- .../web5/sdk/credentials/model/VcDataModel.kt | 473 ++++++++++++++++++ .../credentials/StatusListCredentialTest.kt | 55 +- .../credentials/VerifiableCredentialTest.kt | 6 +- 5 files changed, 549 insertions(+), 64 deletions(-) create mode 100644 credentials/src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/StatusListCredential.kt b/credentials/src/main/kotlin/web5/sdk/credentials/StatusListCredential.kt index 578881d5e..ea5d7aeeb 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/StatusListCredential.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/StatusListCredential.kt @@ -1,6 +1,5 @@ package web5.sdk.credentials -import com.danubetech.verifiablecredentials.CredentialSubject import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import io.ktor.client.HttpClient import io.ktor.client.engine.okhttp.OkHttp @@ -13,6 +12,9 @@ import io.ktor.client.statement.bodyAsText import io.ktor.http.isSuccess import io.ktor.serialization.jackson.jackson import kotlinx.coroutines.runBlocking +import web5.sdk.credentials.model.BitstringStatusListEntry +import web5.sdk.credentials.model.CredentialSubject +import web5.sdk.credentials.model.VcDataModel import web5.sdk.dids.DidResolvers import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream @@ -23,12 +25,6 @@ import java.util.Date import java.util.zip.GZIPInputStream import java.util.zip.GZIPOutputStream -/** - * Type alias representing the danubetech Status List 2021 Entry data model. - * This typealias simplifies the use of the [com.danubetech.verifiablecredentials.credentialstatus.StatusList2021Entry] class. - */ -public typealias StatusList2021Entry = com.danubetech.verifiablecredentials.credentialstatus.StatusList2021Entry - /** * Status purpose of a status list credential or a credential with a credential status. */ @@ -47,6 +43,11 @@ private const val ENCODED_LIST: String = "encodedList" */ private const val STATUS_PURPOSE: String = "statusPurpose" +/** + * The JSON property key for a type. + */ +private const val TYPE: String = "type" + /** * `StatusListCredential` represents a digitally verifiable status list credential according to the * [W3C Verifiable Credentials Status List v2021](https://www.w3.org/TR/vc-status-list/). @@ -104,19 +105,22 @@ public object StatusListCredential { throw IllegalArgumentException("issuer: $issuer not resolvable", e) } - val claims = mapOf(STATUS_PURPOSE to statusPurpose.toString().lowercase(), ENCODED_LIST to bitString) - val credSubject = CredentialSubject.builder() + val claims = mapOf(TYPE to "StatusList2021", + STATUS_PURPOSE to statusPurpose.toString().lowercase(), + ENCODED_LIST to bitString) + + val credSubject = CredentialSubject.Builder() .id(URI.create(statusListCredentialId)) - .type("StatusList2021") .claims(claims) .build() - val vcDataModel = VcDataModel.builder() + val vcDataModel = VcDataModel.Builder() .id(URI.create(statusListCredentialId)) .issuer(URI.create(issuer)) .issuanceDate(Date()) - .context(URI.create("https://w3id.org/vc/status-list/2021/v1")) - .type("StatusList2021Credential") + .contexts(listOf(URI.create("https://www.w3.org/2018/credentials/v1"), + URI.create("https://w3id.org/vc/status-list/2021/v1"))) + .type(listOf("VerifiableCredential", "BitstringStatusListCredential")) .credentialSubject(credSubject) .build() @@ -141,11 +145,11 @@ public object StatusListCredential { credentialToValidate: VerifiableCredential, statusListCredential: VerifiableCredential ): Boolean { - val statusListEntryValue: StatusList2021Entry = - StatusList2021Entry.fromJsonObject(credentialToValidate.vcDataModel.credentialStatus.jsonObject) + val statusListEntryValue: BitstringStatusListEntry = + BitstringStatusListEntry.fromJsonObject(credentialToValidate.vcDataModel.credentialStatus!!.toJson()) val statusListCredStatusPurpose: String? = - statusListCredential.vcDataModel.credentialSubject.jsonObject[STATUS_PURPOSE] as? String? + statusListCredential.vcDataModel.credentialSubject.toMap()[STATUS_PURPOSE] as? String? require(statusListEntryValue.statusPurpose != null) { "Status purpose in the credential to validate is null" @@ -160,7 +164,7 @@ public object StatusListCredential { } val compressedBitstring: String? = - statusListCredential.vcDataModel.credentialSubject.jsonObject[ENCODED_LIST] as? String? + statusListCredential.vcDataModel.credentialSubject.toMap()[ENCODED_LIST] as? String? require(!compressedBitstring.isNullOrEmpty()) { "Compressed bitstring is null or empty" @@ -202,8 +206,8 @@ public object StatusListCredential { val client = httpClient ?: defaultHttpClient().also { isDefaultClient = true } try { - val statusListEntryValue: StatusList2021Entry = - StatusList2021Entry.fromJsonObject(credentialToValidate.vcDataModel.credentialStatus.jsonObject) + val statusListEntryValue: BitstringStatusListEntry = + BitstringStatusListEntry.fromJsonObject(credentialToValidate.vcDataModel.credentialStatus!!.toJson()) val statusListCredential = client.fetchStatusListCredential(statusListEntryValue.statusListCredential.toString()) @@ -260,10 +264,12 @@ public object StatusListCredential { for (vc in credentials) { requireNotNull(vc.vcDataModel.credentialStatus) { "no credential status found in credential" } - val statusListEntry: StatusList2021Entry = - StatusList2021Entry.fromJsonObject(vc.vcDataModel.credentialStatus.jsonObject) + val statusListEntry: BitstringStatusListEntry = + BitstringStatusListEntry.fromJsonObject(vc.vcDataModel.credentialStatus.toJson()) - require(statusListEntry.statusPurpose == statusPurpose.toString().lowercase()) { "status purpose mismatch" } + require(statusListEntry.statusPurpose == statusPurpose.toString().lowercase()) { + "status purpose mismatch" + } if (!duplicateSet.add(statusListEntry.statusListIndex)) { throw IllegalArgumentException("duplicate entry found with index: ${statusListEntry.statusListIndex}") diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt index 489c0d05a..9180aef78 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt @@ -1,7 +1,5 @@ package web5.sdk.credentials -import com.danubetech.verifiablecredentials.CredentialSubject -import com.danubetech.verifiablecredentials.credentialstatus.CredentialStatus import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.JsonNode @@ -13,6 +11,9 @@ import com.nfeld.jsonpathkt.extension.read import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.JWTParser import com.nimbusds.jwt.SignedJWT +import web5.sdk.credentials.model.BitstringStatusListEntry +import web5.sdk.credentials.model.CredentialSubject +import web5.sdk.credentials.model.VcDataModel import web5.sdk.credentials.util.JwtUtil import web5.sdk.dids.Did import java.net.URI @@ -20,12 +21,6 @@ import java.security.SignatureException import java.util.Date import java.util.UUID -/** - * Type alias representing the danubetech Verifiable Credential data model. - * This typealias simplifies the use of the [com.danubetech.verifiablecredentials.VerifiableCredential] class. - */ -public typealias VcDataModel = com.danubetech.verifiablecredentials.VerifiableCredential - /** * `VerifiableCredential` represents a digitally verifiable credential according to the * [W3C Verifiable Credentials Data Model](https://www.w3.org/TR/vc-data-model/). @@ -39,7 +34,7 @@ public typealias VcDataModel = com.danubetech.verifiablecredentials.VerifiableCr public class VerifiableCredential internal constructor(public val vcDataModel: VcDataModel) { public val type: String - get() = vcDataModel.types.last() + get() = vcDataModel.type.last() public val issuer: String get() = vcDataModel.issuer.toString() @@ -123,7 +118,7 @@ public class VerifiableCredential internal constructor(public val vcDataModel: V issuer: String, subject: String, data: T, - credentialStatus: CredentialStatus? = null, + credentialStatus: BitstringStatusListEntry? = null, issuanceDate: Date = Date(), expirationDate: Date? = null ): VerifiableCredential { @@ -134,13 +129,14 @@ public class VerifiableCredential internal constructor(public val vcDataModel: V false -> throw IllegalArgumentException("expected data to be parseable into a JSON object") } - val credentialSubject = CredentialSubject.builder() + val credentialSubject = CredentialSubject.Builder() .id(URI.create(subject)) .claims(mapData) .build() - val vcDataModel = VcDataModel.builder() - .type(type) + val vcDataModel = VcDataModel.Builder() + .contexts(listOf(URI.create("https://www.w3.org/2018/credentials/v1"))) + .type(listOf("VerifiableCredential", type)) .id(URI.create("urn:uuid:${UUID.randomUUID()}")) .issuer(URI.create(issuer)) .issuanceDate(issuanceDate) @@ -149,7 +145,12 @@ public class VerifiableCredential internal constructor(public val vcDataModel: V .apply { credentialStatus?.let { credentialStatus(it) - context(URI.create("https://w3id.org/vc/status-list/2021/v1")) + contexts( + listOf( + URI.create("https://www.w3.org/2018/credentials/v1"), + URI.create("https://w3id.org/vc/status-list/2021/v1") + ) + ) } } .build() diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt b/credentials/src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt new file mode 100644 index 000000000..dc21d50ba --- /dev/null +++ b/credentials/src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt @@ -0,0 +1,473 @@ +package web5.sdk.credentials.model + +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import java.net.URI +import java.text.SimpleDateFormat +import java.util.Date +import java.util.TimeZone + +/** + * Global date format used for formatting the issuance and expiration dates of credentials. + * The value of the issuanceDate property MUST be a string value of an [XMLSCHEMA11-2] combined date-time + */ +public val DATE_FORMAT: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").apply { + timeZone = TimeZone.getTimeZone("UTC") +} + +/** + * The [VcDataModel] instance representing the core data model of a verifiable credential. + * + * @see {@link https://www.w3.org/TR/vc-data-model/#credentials | VC Data Model} + */ +public data class VcDataModel( + val id: URI? = null, + val context: List, + val type: List, + val issuer: URI, + val issuanceDate: Date, + val expirationDate: Date? = null, + val credentialSubject: CredentialSubject, + val credentialSchema: CredentialSchema? = null, + val credentialStatus: BitstringStatusListEntry? = null, +) { + + /** + * Vc Data Model Builder. + */ + public class Builder { + private var id: URI? = null + private var context: List = listOf() + private var type: List = listOf() + private var issuer: URI? = null + private var issuanceDate: Date? = null + private var expirationDate: Date? = null + private var credentialSubject: CredentialSubject? = null + private var credentialSchema: CredentialSchema? = null + private var credentialStatus: BitstringStatusListEntry? = null + + /** + * Sets the identifier of the credential. + * + * @param id The unique identifier for the credential as a [URI]. + * @return The current instance of [Builder] for chaining. + */ + public fun id(id: URI): Builder = apply { this.id = id } + + /** + * Sets the context(s) of the credential. + * + * @param contexts A list of [URI]s that establish the context of the credential. + * @return The current instance of [Builder] for chaining. + */ + public fun contexts(contexts: List): Builder = apply { this.context = contexts } + + /** + * Sets the type(s) of the credential. + * + * @param type A list of strings that specify the type of credential being offered. + * @return The current instance of [Builder] for chaining. + */ + public fun type(type: List): Builder = apply { this.type = type } + + /** + * Sets the issuer of the credential. + * + * @param issuer The [URI] identifying the entity that issued the credential. + * @return The current instance of [Builder] for chaining. + */ + public fun issuer(issuer: URI): Builder = apply { this.issuer = issuer } + + /** + * Sets the issuance date of the credential. + * + * @param issuanceDate The date when the credential was issued. + * @return The current instance of [Builder] for chaining. + */ + public fun issuanceDate(issuanceDate: Date): Builder = apply { this.issuanceDate = issuanceDate } + + /** + * Sets the expiration date of the credential. + * + * @param expirationDate The date when the credential expires. + * @return The current instance of [Builder] for chaining. + */ + public fun expirationDate(expirationDate: Date): Builder = apply { this.expirationDate = expirationDate } + + /** + * Sets the subject of the credential. + * + * @param credentialSubject The [CredentialSubject] that the credential is about. + * @return The current instance of [Builder] for chaining. + */ + public fun credentialSubject(credentialSubject: CredentialSubject): Builder = + apply { this.credentialSubject = credentialSubject } + + /** + * Sets the schema of the credential. + * + * @param credentialSchema The [CredentialSchema] that defines the structure of the credential. Can be null. + * @return The current instance of [Builder] for chaining. + */ + public fun credentialSchema(credentialSchema: CredentialSchema?): Builder = + apply { this.credentialSchema = credentialSchema } + + /** + * Sets the status of the credential. + * + * @param credentialStatus The [BitstringStatusListEntry] representing the status of the credential. Can be null. + * @return The current instance of [Builder] for chaining. + */ + public fun credentialStatus(credentialStatus: BitstringStatusListEntry?): Builder = + apply { this.credentialStatus = credentialStatus } + + + /** + * Constructs a [VcDataModel] instance with the current state of the builder. + * + * @return A fully constructed [VcDataModel] instance. + * @throws IllegalArgumentException If required fields (issuer, issuanceDate, credentialSubject) are not set. + */ + public fun build(): VcDataModel { + require(issuer != null) { "Issuer must be set" } + require(issuanceDate != null) { "IssuanceDate must be set" } + require(credentialSubject != null) { "CredentialSubject must be set" } + + return VcDataModel( + id, + context, + type, + issuer!!, + issuanceDate!!, + expirationDate, + credentialSubject!!, + credentialSchema, + credentialStatus, + ) + } + } + + /** + * Converts the [VcDataModel] instance to a JSON string. + * + * @return A JSON string representation of the [VcDataModel] instance. + */ + public fun toJson(): String { + val mapper = jacksonObjectMapper() + return mapper.writeValueAsString(this) + } + + + /** + * Converts the [VcDataModel] instance into a Map representation. + * + * @return A Map containing key-value pairs representing the properties of the [VcDataModel] instance. + */ + public fun toMap(): Map = mutableMapOf().apply { + id?.also { put("id", it.toString()) } + put("@context", context.map { uri -> uri.toString() }) + put("type", type) + put("issuer", issuer.toString()) + put("issuanceDate", DATE_FORMAT.format(issuanceDate)) + expirationDate?.also { put("expirationDate", DATE_FORMAT.format(it)) } + put("credentialSubject", credentialSubject.toMap()) + credentialSchema?.also { put("credentialSchema", it.toMap()) } + credentialStatus?.also { put("credentialStatus", it.toMap()) } + } + + public companion object { + /** + * Builds a new [CredentialSubject] instance from a map of properties. + * + * @param map A map representing the [CredentialSubject]'s properties. + * @return An instance of [CredentialSubject] constructed from the provided map. + */ + public fun fromMap(map: Map): VcDataModel { + require(map.containsKey("issuer")) { "Issuer is required" } + require(map.containsKey("issuanceDate")) { "IssuanceDate is required" } + require(map.containsKey("credentialSubject")) { "CredentialSubject is required" } + + return VcDataModel( + id = (map["id"] as? String)?.let { URI.create(it) }, + context = (map["@context"] as? List<*>)?.mapNotNull { + (it as? String)?.let { str -> URI.create(str) } + } ?: listOf(), + type = map["type"] as? List ?: listOf(), + issuer = URI.create(map["issuer"] as String), + issuanceDate = DATE_FORMAT.parse(map["issuanceDate"] as String), + expirationDate = (map["expirationDate"] as? String)?.let { DATE_FORMAT.parse(it) }, + credentialSubject = CredentialSubject.fromMap(map["credentialSubject"] as Map), + credentialSchema = (map["credentialSchema"] as? Map)?.let { CredentialSchema.fromMap(it) }, + credentialStatus = (map["credentialStatus"] as? Map)?.let { BitstringStatusListEntry.fromMap(it) } + ) + } + + /** + * Parses a JSON string to create an instance of [VcDataModel]. + * + * @param jsonString The JSON string representation of a [VcDataModel]. + * @return An instance of [VcDataModel]. + */ + public fun fromJsonObject(jsonString: String): VcDataModel { + val mapper = jacksonObjectMapper() + return mapper.readValue(jsonString, object : TypeReference() {}) + } + } +} + +/** + * The [CredentialSubject] represents the value of the credentialSubject property as a set of objects containing + * properties related to the subject of the verifiable credential. + */ +public data class CredentialSubject( + val id: URI? = null, + val additionalClaims: Map = emptyMap() +) { + + /** + * Builder for [CredentialSubject]. + */ + public class Builder { + private var id: URI? = null + private var additionalClaims: Map = emptyMap() + + /** + * Sets the identifier of the credential subject. + * + * @param id The unique identifier for the credential subject as a [URI]. + * @return The current instance of [Builder] for chaining. + */ + public fun id(id: URI): Builder = apply { this.id = id } + + /** + * Sets additional claims of the credential subject. + * + * @param claims A map of additional claims with string keys and any type of values. + * @return The current instance of [Builder] for chaining. + */ + public fun claims(claims: Map): Builder = apply { this.additionalClaims = claims } + + /** + * Constructs a [CredentialSubject] instance with the current state of the builder. + * + * @return A fully constructed [CredentialSubject] instance. + */ + public fun build(): CredentialSubject = CredentialSubject(id, additionalClaims) + } + + /** + * Converts the [CredentialSubject] instance to a JSON string. + * + * @return A JSON string representation of the [CredentialSubject] instance. + */ + public fun toJson(): String { + val mapper = jacksonObjectMapper() + return mapper.writeValueAsString(this) + } + + /** + * Converts the [CredentialSubject] instance into a Map representation. + * + * @return A Map containing key-value pairs representing the properties of the [CredentialSubject] instance. + */ + public fun toMap(): Map = mapOf("id" to (id?.toString() ?: "")).plus(additionalClaims) + + public companion object { + /** + * Converts a map representation into an instance of [CredentialSubject]. + * + * @param map A map containing the properties of a [CredentialSubject]. + * @return An instance of [CredentialSubject]. + */ + public fun fromMap(map: Map): CredentialSubject { + val id = (map["id"] as? String)?.let { URI.create(it) } + val additionalClaims = map.filterKeys { it != "id" } + return CredentialSubject(id, additionalClaims) + } + } +} + +/** + * The [CredentialSchema] Represents the schema defining the structure of a credential. + */ +public data class CredentialSchema( + val id: String, + val type: String? = null +) { + /** + * Converts the [CredentialSchema] instance into a Map representation. + * + * @return A Map containing key-value pairs representing the properties of the [CredentialSchema] instance. + */ + public fun toMap(): Map = mapOf("id" to id).let { map -> + type?.let { map.plus("type" to it) } ?: map + } + + public companion object { + /** + * Converts a map representation into an instance of [CredentialSchema]. + * + * @param map A map containing the properties of a [CredentialSchema]. + * @return An instance of [CredentialSchema]. + */ + public fun fromMap(map: Map): CredentialSchema { + val id = map["id"] as? String ?: throw IllegalArgumentException("CredentialSchema id is required") + val type = map["type"] as? String + return CredentialSchema(id, type) + } + } +} + +/** + * BitstringStatusListEntry. + */ +public data class BitstringStatusListEntry( + val id: URI, + val type: String, + val statusListIndex: String, + val statusListCredential: URI, + val statusPurpose: String, +) { + + /** + * BitstringStatusListEntry Builder. + */ + public class Builder { + private var id: URI? = null + private var type: String? = null + private var statusListIndex: String? = null + private var statusListCredential: URI? = null + private var statusPurpose: String? = null + + /** + * Sets the unique identifier of the object being built. + * + * @param id The URI representing the unique identifier. This is a mandatory parameter for constructing the object. + * @return The builder instance for chaining further configuration calls. + */ + public fun id(id: URI): Builder = apply { this.id = id } + + /** + * Sets the type of the object being built. The type is a string that categorizes or specifies the object's nature within its domain. + * + * @param type A string representing the type of the object. This parameter helps in defining the object's classification. + * @return The builder instance for chaining further configuration calls. + */ + public fun type(type: String): Builder = apply{ this.type = type } + + /** + * Sets the index of the status list for the object being built. The status list index is a string that specifies the position or identifier within a status list. + * + * @param statusListIndex A string representing the index within a status list. This is used to reference or locate the object's status in a predefined list. + * @return The builder instance for chaining further configuration calls. + */ + public fun statusListIndex(statusListIndex: String): Builder = apply { this.statusListIndex = statusListIndex } + + /** + * Sets the credential of the status list for the object being built. The status list credential is a URI that points to a credential or certificate supporting the object's status. + * + * @param statusListCredential The URI pointing to the credential or certificate. This provides a verifiable link to the object's status accreditation. + * @return The builder instance for chaining further configuration calls. + */ + public fun statusListCredential(statusListCredential: URI): Builder = + apply { this.statusListCredential = statusListCredential } + + /** + * Sets the purpose of the status for the object being built. The status purpose is a string that explains why the status is assigned or its role. + * + * @param statusPurpose A string detailing the reason behind the object's status. This clarifies the context or intention of the assigned status. + * @return The builder instance for chaining further configuration calls. + */ + public fun statusPurpose(statusPurpose: String): Builder = apply { this.statusPurpose = statusPurpose } + + /** + * Constructs a [BitstringStatusListEntry] instance with the current state of the builder. + * + * @return A fully constructed [BitstringStatusListEntry] instance. + * @throws IllegalArgumentException If required fields (id, type, statusListIndex, statusListCredential, statusPurpose) are not set. + */ + public fun build(): BitstringStatusListEntry { + require(id != null) { "id must be set" } + require(statusListIndex != null) { "statusListIndex must be set" } + require(statusListCredential != null) { "statusListCredential must be set" } + require(statusPurpose != null) { "statusPurpose must be set" } + + return BitstringStatusListEntry( + id!!, + type ?: "BitstringStatusListEntry", // Assuming type can have a default value + statusListIndex!!, + statusListCredential!!, + statusPurpose!! + ) + } + } + + /** + * Converts the [BitstringStatusListEntry] instance to a JSON string. + * + * @return A JSON string representation of the [BitstringStatusListEntry] instance. + */ + public fun toJson(): String { + val mapper = jacksonObjectMapper() + return mapper.writeValueAsString(this) + } + + + /** + * Converts the [BitstringStatusListEntry] instance into a Map representation. + * + * @return A Map containing key-value pairs representing the properties of the [BitstringStatusListEntry] instance. + */ + public fun toMap(): Map { + return mapOf( + "id" to id.toString(), + "type" to type, + "statusListIndex" to statusListIndex, + "statusListCredential" to statusListCredential.toString(), + "statusPurpose" to statusPurpose + ) + } + + public companion object { + /** + * Creates an instance of [BitstringStatusListEntry] from a map of its properties. + * + * @param map A map containing the properties of a [BitstringStatusListEntry]. + * @return An instance of [BitstringStatusListEntry]. + * @throws IllegalArgumentException If required properties are missing. + */ + public fun fromMap(map: Map): BitstringStatusListEntry { + // Check for required properties and throw IllegalArgumentException if any are missing + require(map.containsKey("id") && map["id"] is String) { "id is required" } + require(map.containsKey("type") && map["type"] is String) { "type is required" } + require(map.containsKey("statusListIndex") && map["statusListIndex"] is String) { + "statusListIndex is required" + } + require(map.containsKey("statusListCredential") && map["statusListCredential"] is String) { + "statusListCredential is required" + } + require(map.containsKey("statusPurpose") && map["statusPurpose"] is String) { "statusPurpose is required" } + + + return BitstringStatusListEntry( + id = URI.create(map["id"] as String), + type = map["type"] as String, + statusListIndex = map["statusListIndex"] as String, + statusListCredential = URI.create(map["statusListCredential"] as String), + statusPurpose = map["statusPurpose"] as String + ) + } + + + /** + * Parses a JSON string to create an instance of [BitstringStatusListEntry]. + * + * @param jsonString The JSON string representation of a [BitstringStatusListEntry]. + * @return An instance of [BitstringStatusListEntry]. + */ + public fun fromJsonObject(jsonString: String): BitstringStatusListEntry { + val mapper = jacksonObjectMapper() + return mapper.readValue(jsonString, object : TypeReference() {}) + } + } +} \ No newline at end of file diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt index 923045c73..b9fbf6572 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt @@ -8,6 +8,7 @@ import io.ktor.http.fullPath import io.ktor.http.headersOf import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.assertThrows +import web5.sdk.credentials.model.BitstringStatusListEntry import web5.sdk.crypto.InMemoryKeyManager import web5.sdk.dids.methods.key.DidKey import java.io.File @@ -32,13 +33,13 @@ class StatusListCredentialTest { assertEquals(specExampleRevocableVc.issuer, "did:example:12345") assertNotNull(specExampleRevocableVc.vcDataModel.credentialStatus) assertEquals(specExampleRevocableVc.subject, "did:example:6789") - assertEquals(specExampleRevocableVc.vcDataModel.credentialSubject.type.toString(), "Person") + assertEquals(specExampleRevocableVc.vcDataModel.credentialSubject.toMap()["type"], "Person") - val credentialStatus: StatusList2021Entry = - StatusList2021Entry.fromJsonObject(specExampleRevocableVc.vcDataModel.credentialStatus.jsonObject) + val credentialStatus: BitstringStatusListEntry = + BitstringStatusListEntry.fromJsonObject(specExampleRevocableVc.vcDataModel.credentialStatus!!.toJson()) assertEquals(credentialStatus.id.toString(), "https://example.com/credentials/status/3#94567") - assertEquals(credentialStatus.type.toString(), "BitStringStatusListEntry") + assertEquals(credentialStatus.toMap()["type"].toString(), "BitStringStatusListEntry") assertEquals(credentialStatus.statusPurpose.toString(), StatusPurpose.REVOCATION.toString().lowercase()) assertEquals(credentialStatus.statusListIndex, "94567") assertEquals(credentialStatus.statusListCredential.toString(), "https://example.com/credentials/status/3") @@ -50,7 +51,7 @@ class StatusListCredentialTest { val issuerDid = DidKey.create(keyManager) val holderDid = DidKey.create(keyManager) - val credentialStatus = StatusList2021Entry.builder() + val credentialStatus = BitstringStatusListEntry.Builder() .id(URI.create("cred-with-status-id")) .statusPurpose("revocation") .statusListIndex("123") @@ -66,7 +67,7 @@ class StatusListCredentialTest { ) assertTrue( - credWithCredStatus.vcDataModel.contexts.containsAll( + credWithCredStatus.vcDataModel.context.containsAll( listOf( URI.create("https://www.w3.org/2018/credentials/v1"), URI.create("https://w3id.org/vc/status-list/2021/v1") @@ -78,13 +79,13 @@ class StatusListCredentialTest { assertEquals(credWithCredStatus.issuer, issuerDid.uri) assertNotNull(credWithCredStatus.vcDataModel.issuanceDate) assertNotNull(credWithCredStatus.vcDataModel.credentialStatus) - assertEquals(credWithCredStatus.vcDataModel.credentialSubject.claims.get("localRespect"), "high") + assertEquals(credWithCredStatus.vcDataModel.credentialSubject.toMap().get("localRespect"), "high") - val credStatus: StatusList2021Entry = - StatusList2021Entry.fromJsonObject(credWithCredStatus.vcDataModel.credentialStatus.jsonObject) + val credStatus: BitstringStatusListEntry = + BitstringStatusListEntry.fromJsonObject(credWithCredStatus.vcDataModel.credentialStatus!!.toJson()) assertEquals(credStatus.id.toString(), "cred-with-status-id") - assertEquals(credStatus.type.toString(), "StatusList2021Entry") + assertEquals(credStatus.toMap()["type"], "BitstringStatusListEntry") assertEquals(credStatus.statusPurpose.toString(), StatusPurpose.REVOCATION.toString().lowercase()) assertEquals(credStatus.statusListIndex, "123") assertEquals(credStatus.statusListCredential.toString(), "https://example.com/credentials/status/3") @@ -96,7 +97,7 @@ class StatusListCredentialTest { val issuerDid = DidKey.create(keyManager) val holderDid = DidKey.create(keyManager) - val credentialStatus1 = StatusList2021Entry.builder() + val credentialStatus1 = BitstringStatusListEntry.Builder() .id(URI.create("cred-with-status-id")) .statusPurpose("revocation") .statusListIndex("123") @@ -111,7 +112,7 @@ class StatusListCredentialTest { credentialStatus = credentialStatus1 ) - val credentialStatus2 = StatusList2021Entry.builder() + val credentialStatus2 = BitstringStatusListEntry.Builder() .id(URI.create("cred-with-status-id")) .statusPurpose("revocation") .statusListIndex("124") @@ -135,7 +136,7 @@ class StatusListCredentialTest { assertNotNull(statusListCredential) assertTrue( - statusListCredential.vcDataModel.contexts.containsAll( + statusListCredential.vcDataModel.context.containsAll( listOf( URI.create("https://www.w3.org/2018/credentials/v1"), URI.create("https://w3id.org/vc/status-list/2021/v1") @@ -143,18 +144,18 @@ class StatusListCredentialTest { ) ) assertTrue( - statusListCredential.vcDataModel.types.containsAll( + statusListCredential.vcDataModel.type.containsAll( listOf( "VerifiableCredential", - "StatusList2021Credential" + "BitstringStatusListCredential" ) ) ) assertEquals(statusListCredential.subject, "revocation-id") - assertEquals(statusListCredential.vcDataModel.credentialSubject.type, "StatusList2021") + assertEquals(statusListCredential.vcDataModel.credentialSubject.toMap()["type"], "StatusList2021") assertEquals( "revocation", - statusListCredential.vcDataModel.credentialSubject.jsonObject["statusPurpose"] as? String? + statusListCredential.vcDataModel.credentialSubject.toMap()["statusPurpose"] as? String? ) // TODO: Check encoding across other sdks and spec - https://github.com/TBD54566975/web5-kt/issues/97 @@ -171,7 +172,7 @@ class StatusListCredentialTest { val issuerDid = DidKey.create(keyManager) val holderDid = DidKey.create(keyManager) - val credentialStatus1 = StatusList2021Entry.builder() + val credentialStatus1 = BitstringStatusListEntry.Builder() .id(URI.create("cred-with-status-id")) .statusPurpose("revocation") .statusListIndex("123") @@ -186,7 +187,7 @@ class StatusListCredentialTest { credentialStatus = credentialStatus1 ) - val credentialStatus2 = StatusList2021Entry.builder() + val credentialStatus2 = BitstringStatusListEntry.Builder() .id(URI.create("cred-with-status-id")) .statusPurpose("revocation") .statusListIndex("123") @@ -222,7 +223,7 @@ class StatusListCredentialTest { val issuerDid = DidKey.create(keyManager) val holderDid = DidKey.create(keyManager) - val credentialStatus1 = StatusList2021Entry.builder() + val credentialStatus1 = BitstringStatusListEntry.Builder() .id(URI.create("cred-with-status-id")) .statusPurpose("revocation") .statusListIndex("-1") @@ -258,7 +259,7 @@ class StatusListCredentialTest { val issuerDid = DidKey.create(keyManager) val holderDid = DidKey.create(keyManager) - val credentialStatus1 = StatusList2021Entry.builder() + val credentialStatus1 = BitstringStatusListEntry.Builder() .id(URI.create("cred-with-status-id")) .statusPurpose("revocation") .statusListIndex(Int.MAX_VALUE.toString()) @@ -294,7 +295,7 @@ class StatusListCredentialTest { val issuerDid = DidKey.create(keyManager) val holderDid = DidKey.create(keyManager) - val credentialStatus1 = StatusList2021Entry.builder() + val credentialStatus1 = BitstringStatusListEntry.Builder() .id(URI.create("cred-with-status-id")) .statusPurpose("revocation") .statusListIndex("123") @@ -309,7 +310,7 @@ class StatusListCredentialTest { credentialStatus = credentialStatus1 ) - val credentialStatus2 = StatusList2021Entry.builder() + val credentialStatus2 = BitstringStatusListEntry.Builder() .id(URI.create("cred-with-status-id")) .statusPurpose("revocation") .statusListIndex("124") @@ -324,7 +325,7 @@ class StatusListCredentialTest { credentialStatus2 ) - val credentialStatus3 = StatusList2021Entry.builder() + val credentialStatus3 = BitstringStatusListEntry.Builder() .id(URI.create("cred-with-status-id")) .statusPurpose("revocation") .statusListIndex("125") @@ -363,7 +364,7 @@ class StatusListCredentialTest { val issuerDid = DidKey.create(keyManager) val holderDid = DidKey.create(keyManager) - val credentialStatus1 = StatusList2021Entry.builder() + val credentialStatus1 = BitstringStatusListEntry.Builder() .id(URI.create("cred-with-status-id")) .statusPurpose("revocation") .statusListIndex("123") @@ -378,7 +379,7 @@ class StatusListCredentialTest { credentialStatus = credentialStatus1 ) - val credentialStatus2 = StatusList2021Entry.builder() + val credentialStatus2 = BitstringStatusListEntry.Builder() .id(URI.create("cred-with-status-id")) .statusPurpose("revocation") .statusListIndex("124") @@ -433,7 +434,7 @@ class StatusListCredentialTest { val issuerDid = DidKey.create(keyManager) val holderDid = DidKey.create(keyManager) - val credentialStatus1 = StatusList2021Entry.builder() + val credentialStatus1 = BitstringStatusListEntry.Builder() .id(URI.create("cred-with-status-id")) .statusPurpose("revocation") .statusListIndex("123") diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt index a695484e9..941fdf877 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt @@ -246,6 +246,7 @@ class Web5TestVectorsCredentials { val testVectors = mapper.readValue(File("../web5-spec/test-vectors/credentials/create.json"), typeRef) testVectors.vectors.filterNot { it.errors ?: false }.forEach { vector -> + println(vector.description) val vc = VerifiableCredential.fromJson(mapper.writeValueAsString(vector.input.credential)) val keyManager = InMemoryKeyManager() @@ -253,7 +254,10 @@ class Web5TestVectorsCredentials { val issuerDid = Did.load(vector.input.signerDidUri!!, keyManager) val vcJwt = vc.sign(issuerDid) - assertEquals(vector.output, vcJwt, vector.description) + val vectorOutputParsedVc = vector.output?.let { VerifiableCredential.parseJwt(it) } + val outputParsedVc = VerifiableCredential.parseJwt(vcJwt) + + assertEquals(vectorOutputParsedVc.toString(), outputParsedVc.toString()) } testVectors.vectors.filter { it.errors ?: false }.forEach { vector -> From 2d7a842ad183bea9c6177c4c4bebcc9d7f2ec747 Mon Sep 17 00:00:00 2001 From: Neal Date: Tue, 20 Feb 2024 17:18:52 -0600 Subject: [PATCH 02/10] updates --- .../sdk/credentials/StatusListCredential.kt | 36 +- .../sdk/credentials/VerifiableCredential.kt | 45 +- .../model/BitstringStatusListEntry.kt | 70 +++ .../web5/sdk/credentials/model/VcDataModel.kt | 506 ++++-------------- .../credentials/StatusListCredentialTest.kt | 165 +++--- 5 files changed, 309 insertions(+), 513 deletions(-) create mode 100644 credentials/src/main/kotlin/web5/sdk/credentials/model/BitstringStatusListEntry.kt diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/StatusListCredential.kt b/credentials/src/main/kotlin/web5/sdk/credentials/StatusListCredential.kt index ea5d7aeeb..740a566a9 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/StatusListCredential.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/StatusListCredential.kt @@ -22,6 +22,7 @@ import java.net.URI import java.util.Base64 import java.util.BitSet import java.util.Date +import java.util.UUID import java.util.zip.GZIPInputStream import java.util.zip.GZIPOutputStream @@ -109,20 +110,20 @@ public object StatusListCredential { STATUS_PURPOSE to statusPurpose.toString().lowercase(), ENCODED_LIST to bitString) - val credSubject = CredentialSubject.Builder() - .id(URI.create(statusListCredentialId)) - .claims(claims) - .build() - - val vcDataModel = VcDataModel.Builder() - .id(URI.create(statusListCredentialId)) - .issuer(URI.create(issuer)) - .issuanceDate(Date()) - .contexts(listOf(URI.create("https://www.w3.org/2018/credentials/v1"), - URI.create("https://w3id.org/vc/status-list/2021/v1"))) - .type(listOf("VerifiableCredential", "BitstringStatusListCredential")) - .credentialSubject(credSubject) - .build() + val credSubject = CredentialSubject( + id = URI.create(statusListCredentialId), + additionalClaims = claims + ) + + val vcDataModel = VcDataModel( + id = URI.create(statusListCredentialId), + context = mutableListOf(URI.create("https://www.w3.org/2018/credentials/v1"), + URI.create("https://w3id.org/vc/status-list/2021/v1")), + type = mutableListOf("VerifiableCredential", "BitstringStatusListCredential"), + issuer = URI.create(issuer), + issuanceDate = Date(), + credentialSubject = credSubject, + ) return VerifiableCredential(vcDataModel) } @@ -148,8 +149,9 @@ public object StatusListCredential { val statusListEntryValue: BitstringStatusListEntry = BitstringStatusListEntry.fromJsonObject(credentialToValidate.vcDataModel.credentialStatus!!.toJson()) - val statusListCredStatusPurpose: String? = - statusListCredential.vcDataModel.credentialSubject.toMap()[STATUS_PURPOSE] as? String? + val credentialSubject = statusListCredential.vcDataModel.credentialSubject + + val statusListCredStatusPurpose: String? = credentialSubject.additionalClaims[STATUS_PURPOSE] as? String? require(statusListEntryValue.statusPurpose != null) { "Status purpose in the credential to validate is null" @@ -164,7 +166,7 @@ public object StatusListCredential { } val compressedBitstring: String? = - statusListCredential.vcDataModel.credentialSubject.toMap()[ENCODED_LIST] as? String? + credentialSubject.additionalClaims[ENCODED_LIST] as? String? require(!compressedBitstring.isNullOrEmpty()) { "Compressed bitstring is null or empty" diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt index 9180aef78..b90c4e4b7 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt @@ -129,31 +129,26 @@ public class VerifiableCredential internal constructor(public val vcDataModel: V false -> throw IllegalArgumentException("expected data to be parseable into a JSON object") } - val credentialSubject = CredentialSubject.Builder() - .id(URI.create(subject)) - .claims(mapData) - .build() - - val vcDataModel = VcDataModel.Builder() - .contexts(listOf(URI.create("https://www.w3.org/2018/credentials/v1"))) - .type(listOf("VerifiableCredential", type)) - .id(URI.create("urn:uuid:${UUID.randomUUID()}")) - .issuer(URI.create(issuer)) - .issuanceDate(issuanceDate) - .apply { expirationDate?.let { expirationDate(it) } } - .credentialSubject(credentialSubject) - .apply { - credentialStatus?.let { - credentialStatus(it) - contexts( - listOf( - URI.create("https://www.w3.org/2018/credentials/v1"), - URI.create("https://w3id.org/vc/status-list/2021/v1") - ) - ) - } - } - .build() + val credentialSubject = CredentialSubject( + id = URI.create(subject), + additionalClaims = mapData + ) + + val contexts = mutableListOf(URI.create("https://www.w3.org/2018/credentials/v1")) + if (credentialStatus != null) { + contexts.add(URI.create("https://w3id.org/vc/status-list/2021/v1")) + } + + val vcDataModel = VcDataModel( + id = URI.create("urn:uuid:${UUID.randomUUID()}"), + context = contexts, + type = mutableListOf("VerifiableCredential", type), + issuer = URI.create(issuer), + issuanceDate = issuanceDate, + expirationDate = expirationDate, + credentialSubject = credentialSubject, + credentialStatus = credentialStatus + ) // This should be a no-op just to make sure we've set all the correct fields. validateDataModel(vcDataModel.toMap()) diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/model/BitstringStatusListEntry.kt b/credentials/src/main/kotlin/web5/sdk/credentials/model/BitstringStatusListEntry.kt new file mode 100644 index 000000000..13e102d75 --- /dev/null +++ b/credentials/src/main/kotlin/web5/sdk/credentials/model/BitstringStatusListEntry.kt @@ -0,0 +1,70 @@ +package web5.sdk.credentials.model + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.registerKotlinModule +import java.net.URI + +private fun getObjectMapper(): ObjectMapper = jacksonObjectMapper().apply { + registerKotlinModule() + setSerializationInclusion(JsonInclude.Include.NON_NULL) +} + +/** + * BitstringStatusListEntry. + */ +public data class BitstringStatusListEntry( + val id: URI, + val type: String = "BitstringStatusListEntry", + val statusListIndex: String, + val statusListCredential: URI, + val statusPurpose: String, +) { + + init { + require( id.toString().isNotBlank()) { "Id cannot be blank" } + require( statusListIndex.isNotBlank()) { "StatusListIndex cannot be blank" } + require( statusListCredential.toString().isNotBlank()) { "StatusListCredential cannot be blank" } + require( statusPurpose.isNotBlank()) { "StatusPurpose cannot be blank" } + } + + /** + * Converts the [BitstringStatusListEntry] instance to a JSON string. + * + * @return A JSON string representation of the [BitstringStatusListEntry] instance. + */ + public fun toJson(): String = getObjectMapper().writeValueAsString(this) + + /** + * Converts the [BitstringStatusListEntry] instance into a Map representation. + * + * @return A Map containing key-value pairs representing the properties of the [BitstringStatusListEntry] instance. + */ + public fun toMap(): Map = + getObjectMapper().readValue(this.toJson(), object : TypeReference>() {}) + + public companion object { + /** + * Parses a JSON string to create an instance of [BitstringStatusListEntry]. + * + * @param jsonString The JSON string representation of a [BitstringStatusListEntry]. + * @return An instance of [BitstringStatusListEntry]. + */ + public fun fromJsonObject(jsonString: String): BitstringStatusListEntry = + getObjectMapper().readValue(jsonString, BitstringStatusListEntry::class.java) + + /** + * Creates an instance of [BitstringStatusListEntry] from a map of its properties. + * + * @param map A map containing the properties of a [BitstringStatusListEntry]. + * @return An instance of [BitstringStatusListEntry]. + * @throws IllegalArgumentException If required properties are missing. + */ + public fun fromMap(map: Map): BitstringStatusListEntry { + val json = getObjectMapper().writeValueAsString(map) + return getObjectMapper().readValue(json, BitstringStatusListEntry::class.java) + } + } +} \ No newline at end of file diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt b/credentials/src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt index dc21d50ba..9355cc17e 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt @@ -1,7 +1,19 @@ package web5.sdk.credentials.model +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.JsonSerializer +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.registerKotlinModule import java.net.URI import java.text.SimpleDateFormat import java.util.Date @@ -11,10 +23,76 @@ import java.util.TimeZone * Global date format used for formatting the issuance and expiration dates of credentials. * The value of the issuanceDate property MUST be a string value of an [XMLSCHEMA11-2] combined date-time */ -public val DATE_FORMAT: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").apply { +private val DATE_FORMAT: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").apply { timeZone = TimeZone.getTimeZone("UTC") } +/** + * DateSerializer. + */ +private class DateSerializer : JsonSerializer() { + override fun serialize(value: Date?, gen: JsonGenerator?, serializers: SerializerProvider?) { + gen?.writeString(value?.let { DATE_FORMAT.format(it) }) + } +} + +/** + * DateDeserializer. + */ +private class DateDeserializer : JsonDeserializer() { + override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): Date { + return DATE_FORMAT.parse(p?.text ?: "") + } +} + +/** + * CredentialSubjectSerializer. + */ +public class CredentialSubjectSerializer : JsonSerializer() { + override fun serialize(value: CredentialSubject, gen: JsonGenerator, serializers: SerializerProvider) { + gen.writeStartObject() + // Write the id field. If id is null, write an empty string; otherwise, write its string representation. + gen.writeStringField("id", value.id?.toString() ?: "") + // Write additional claims directly into the JSON object + value.additionalClaims.forEach { (key, claimValue) -> + gen.writeObjectField(key, claimValue) + } + gen.writeEndObject() + } +} + +/** + * CredentialSubjectDeserializer. + */ +public class CredentialSubjectDeserializer : JsonDeserializer() { + override fun deserialize(p: JsonParser, ctxt: DeserializationContext): CredentialSubject { + val node: JsonNode = p.codec.readTree(p) + val idNode = node.get("id") + val id = idNode?.asText()?.takeIf { it.isNotBlank() }?.let { URI.create(it) } + + // Remove the "id" field and treat the rest as additionalClaims + val additionalClaims = node.fields().asSequence().filterNot { it.key == "id" } + .associate { it.key to it.value.asText() } // Assuming all values are stored as text + + return CredentialSubject(id, additionalClaims) + } +} + +private fun getObjectMapper(): ObjectMapper { + return jacksonObjectMapper().apply { + registerKotlinModule() + setSerializationInclusion(JsonInclude.Include.NON_NULL) + + val dateModule = SimpleModule().apply { + addSerializer(Date::class.java, DateSerializer()) + addDeserializer(Date::class.java, DateDeserializer()) + addSerializer(CredentialSubject::class.java, CredentialSubjectSerializer()) + addDeserializer(CredentialSubject::class.java, CredentialSubjectDeserializer()) + } + registerModule(dateModule) + } +} + /** * The [VcDataModel] instance representing the core data model of a verifiable credential. * @@ -22,128 +100,30 @@ public val DATE_FORMAT: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm: */ public data class VcDataModel( val id: URI? = null, - val context: List, - val type: List, + @JsonProperty("@context") + val context: MutableList = mutableListOf(), + val type: MutableList = mutableListOf(), val issuer: URI, val issuanceDate: Date, val expirationDate: Date? = null, val credentialSubject: CredentialSubject, val credentialSchema: CredentialSchema? = null, - val credentialStatus: BitstringStatusListEntry? = null, + val credentialStatus: BitstringStatusListEntry? = null ) { + init { + if(context.isEmpty() || context[0].toString() != "https://www.w3.org/2018/credentials/v1") { + context.add(0, URI.create("https://www.w3.org/2018/credentials/v1")) + } - /** - * Vc Data Model Builder. - */ - public class Builder { - private var id: URI? = null - private var context: List = listOf() - private var type: List = listOf() - private var issuer: URI? = null - private var issuanceDate: Date? = null - private var expirationDate: Date? = null - private var credentialSubject: CredentialSubject? = null - private var credentialSchema: CredentialSchema? = null - private var credentialStatus: BitstringStatusListEntry? = null - - /** - * Sets the identifier of the credential. - * - * @param id The unique identifier for the credential as a [URI]. - * @return The current instance of [Builder] for chaining. - */ - public fun id(id: URI): Builder = apply { this.id = id } - - /** - * Sets the context(s) of the credential. - * - * @param contexts A list of [URI]s that establish the context of the credential. - * @return The current instance of [Builder] for chaining. - */ - public fun contexts(contexts: List): Builder = apply { this.context = contexts } - - /** - * Sets the type(s) of the credential. - * - * @param type A list of strings that specify the type of credential being offered. - * @return The current instance of [Builder] for chaining. - */ - public fun type(type: List): Builder = apply { this.type = type } - - /** - * Sets the issuer of the credential. - * - * @param issuer The [URI] identifying the entity that issued the credential. - * @return The current instance of [Builder] for chaining. - */ - public fun issuer(issuer: URI): Builder = apply { this.issuer = issuer } - - /** - * Sets the issuance date of the credential. - * - * @param issuanceDate The date when the credential was issued. - * @return The current instance of [Builder] for chaining. - */ - public fun issuanceDate(issuanceDate: Date): Builder = apply { this.issuanceDate = issuanceDate } - - /** - * Sets the expiration date of the credential. - * - * @param expirationDate The date when the credential expires. - * @return The current instance of [Builder] for chaining. - */ - public fun expirationDate(expirationDate: Date): Builder = apply { this.expirationDate = expirationDate } - - /** - * Sets the subject of the credential. - * - * @param credentialSubject The [CredentialSubject] that the credential is about. - * @return The current instance of [Builder] for chaining. - */ - public fun credentialSubject(credentialSubject: CredentialSubject): Builder = - apply { this.credentialSubject = credentialSubject } - - /** - * Sets the schema of the credential. - * - * @param credentialSchema The [CredentialSchema] that defines the structure of the credential. Can be null. - * @return The current instance of [Builder] for chaining. - */ - public fun credentialSchema(credentialSchema: CredentialSchema?): Builder = - apply { this.credentialSchema = credentialSchema } - - /** - * Sets the status of the credential. - * - * @param credentialStatus The [BitstringStatusListEntry] representing the status of the credential. Can be null. - * @return The current instance of [Builder] for chaining. - */ - public fun credentialStatus(credentialStatus: BitstringStatusListEntry?): Builder = - apply { this.credentialStatus = credentialStatus } - + if(type.isEmpty() || type[0] != "VerifiableCredential") { + type.add(0, "VerifiableCredential") + } - /** - * Constructs a [VcDataModel] instance with the current state of the builder. - * - * @return A fully constructed [VcDataModel] instance. - * @throws IllegalArgumentException If required fields (issuer, issuanceDate, credentialSubject) are not set. - */ - public fun build(): VcDataModel { - require(issuer != null) { "Issuer must be set" } - require(issuanceDate != null) { "IssuanceDate must be set" } - require(credentialSubject != null) { "CredentialSubject must be set" } + require(id == null || id.toString().isNotBlank()) { "ID URI cannot be blank" } + require(issuer.toString().isNotBlank()) { "Issuer URI cannot be blank" } - return VcDataModel( - id, - context, - type, - issuer!!, - issuanceDate!!, - expirationDate, - credentialSubject!!, - credentialSchema, - credentialStatus, - ) + if (expirationDate != null) { + require(issuanceDate.before(expirationDate)) { "Issuance date must be before expiration date" } } } @@ -153,54 +133,22 @@ public data class VcDataModel( * @return A JSON string representation of the [VcDataModel] instance. */ public fun toJson(): String { - val mapper = jacksonObjectMapper() + val mapper = getObjectMapper() return mapper.writeValueAsString(this) } - /** * Converts the [VcDataModel] instance into a Map representation. * * @return A Map containing key-value pairs representing the properties of the [VcDataModel] instance. */ - public fun toMap(): Map = mutableMapOf().apply { - id?.also { put("id", it.toString()) } - put("@context", context.map { uri -> uri.toString() }) - put("type", type) - put("issuer", issuer.toString()) - put("issuanceDate", DATE_FORMAT.format(issuanceDate)) - expirationDate?.also { put("expirationDate", DATE_FORMAT.format(it)) } - put("credentialSubject", credentialSubject.toMap()) - credentialSchema?.also { put("credentialSchema", it.toMap()) } - credentialStatus?.also { put("credentialStatus", it.toMap()) } + public fun toMap(): Map { + val mapper = getObjectMapper() + val jsonString = mapper.writeValueAsString(this) + val typeRef = object : TypeReference>() {} + return mapper.readValue(jsonString, typeRef) } - public companion object { - /** - * Builds a new [CredentialSubject] instance from a map of properties. - * - * @param map A map representing the [CredentialSubject]'s properties. - * @return An instance of [CredentialSubject] constructed from the provided map. - */ - public fun fromMap(map: Map): VcDataModel { - require(map.containsKey("issuer")) { "Issuer is required" } - require(map.containsKey("issuanceDate")) { "IssuanceDate is required" } - require(map.containsKey("credentialSubject")) { "CredentialSubject is required" } - - return VcDataModel( - id = (map["id"] as? String)?.let { URI.create(it) }, - context = (map["@context"] as? List<*>)?.mapNotNull { - (it as? String)?.let { str -> URI.create(str) } - } ?: listOf(), - type = map["type"] as? List ?: listOf(), - issuer = URI.create(map["issuer"] as String), - issuanceDate = DATE_FORMAT.parse(map["issuanceDate"] as String), - expirationDate = (map["expirationDate"] as? String)?.let { DATE_FORMAT.parse(it) }, - credentialSubject = CredentialSubject.fromMap(map["credentialSubject"] as Map), - credentialSchema = (map["credentialSchema"] as? Map)?.let { CredentialSchema.fromMap(it) }, - credentialStatus = (map["credentialStatus"] as? Map)?.let { BitstringStatusListEntry.fromMap(it) } - ) - } /** * Parses a JSON string to create an instance of [VcDataModel]. @@ -209,9 +157,21 @@ public data class VcDataModel( * @return An instance of [VcDataModel]. */ public fun fromJsonObject(jsonString: String): VcDataModel { - val mapper = jacksonObjectMapper() + val mapper = getObjectMapper() return mapper.readValue(jsonString, object : TypeReference() {}) } + + /** + * Builds a new [CredentialSubject] instance from a map of properties. + * + * @param map A map representing the [CredentialSubject]'s properties. + * @return An instance of [CredentialSubject] constructed from the provided map. + */ + public fun fromMap(map: Map): VcDataModel { + val mapper = getObjectMapper() + val json = mapper.writeValueAsString(map) + return mapper.readValue(json, VcDataModel::class.java) + } } } @@ -223,67 +183,8 @@ public data class CredentialSubject( val id: URI? = null, val additionalClaims: Map = emptyMap() ) { - - /** - * Builder for [CredentialSubject]. - */ - public class Builder { - private var id: URI? = null - private var additionalClaims: Map = emptyMap() - - /** - * Sets the identifier of the credential subject. - * - * @param id The unique identifier for the credential subject as a [URI]. - * @return The current instance of [Builder] for chaining. - */ - public fun id(id: URI): Builder = apply { this.id = id } - - /** - * Sets additional claims of the credential subject. - * - * @param claims A map of additional claims with string keys and any type of values. - * @return The current instance of [Builder] for chaining. - */ - public fun claims(claims: Map): Builder = apply { this.additionalClaims = claims } - - /** - * Constructs a [CredentialSubject] instance with the current state of the builder. - * - * @return A fully constructed [CredentialSubject] instance. - */ - public fun build(): CredentialSubject = CredentialSubject(id, additionalClaims) - } - - /** - * Converts the [CredentialSubject] instance to a JSON string. - * - * @return A JSON string representation of the [CredentialSubject] instance. - */ - public fun toJson(): String { - val mapper = jacksonObjectMapper() - return mapper.writeValueAsString(this) - } - - /** - * Converts the [CredentialSubject] instance into a Map representation. - * - * @return A Map containing key-value pairs representing the properties of the [CredentialSubject] instance. - */ - public fun toMap(): Map = mapOf("id" to (id?.toString() ?: "")).plus(additionalClaims) - - public companion object { - /** - * Converts a map representation into an instance of [CredentialSubject]. - * - * @param map A map containing the properties of a [CredentialSubject]. - * @return An instance of [CredentialSubject]. - */ - public fun fromMap(map: Map): CredentialSubject { - val id = (map["id"] as? String)?.let { URI.create(it) } - val additionalClaims = map.filterKeys { it != "id" } - return CredentialSubject(id, additionalClaims) - } + init { + require(id == null || id.toString().isNotBlank()) { "ID URI cannot be blank" } } } @@ -294,180 +195,7 @@ public data class CredentialSchema( val id: String, val type: String? = null ) { - /** - * Converts the [CredentialSchema] instance into a Map representation. - * - * @return A Map containing key-value pairs representing the properties of the [CredentialSchema] instance. - */ - public fun toMap(): Map = mapOf("id" to id).let { map -> - type?.let { map.plus("type" to it) } ?: map - } - - public companion object { - /** - * Converts a map representation into an instance of [CredentialSchema]. - * - * @param map A map containing the properties of a [CredentialSchema]. - * @return An instance of [CredentialSchema]. - */ - public fun fromMap(map: Map): CredentialSchema { - val id = map["id"] as? String ?: throw IllegalArgumentException("CredentialSchema id is required") - val type = map["type"] as? String - return CredentialSchema(id, type) - } - } -} - -/** - * BitstringStatusListEntry. - */ -public data class BitstringStatusListEntry( - val id: URI, - val type: String, - val statusListIndex: String, - val statusListCredential: URI, - val statusPurpose: String, -) { - - /** - * BitstringStatusListEntry Builder. - */ - public class Builder { - private var id: URI? = null - private var type: String? = null - private var statusListIndex: String? = null - private var statusListCredential: URI? = null - private var statusPurpose: String? = null - - /** - * Sets the unique identifier of the object being built. - * - * @param id The URI representing the unique identifier. This is a mandatory parameter for constructing the object. - * @return The builder instance for chaining further configuration calls. - */ - public fun id(id: URI): Builder = apply { this.id = id } - - /** - * Sets the type of the object being built. The type is a string that categorizes or specifies the object's nature within its domain. - * - * @param type A string representing the type of the object. This parameter helps in defining the object's classification. - * @return The builder instance for chaining further configuration calls. - */ - public fun type(type: String): Builder = apply{ this.type = type } - - /** - * Sets the index of the status list for the object being built. The status list index is a string that specifies the position or identifier within a status list. - * - * @param statusListIndex A string representing the index within a status list. This is used to reference or locate the object's status in a predefined list. - * @return The builder instance for chaining further configuration calls. - */ - public fun statusListIndex(statusListIndex: String): Builder = apply { this.statusListIndex = statusListIndex } - - /** - * Sets the credential of the status list for the object being built. The status list credential is a URI that points to a credential or certificate supporting the object's status. - * - * @param statusListCredential The URI pointing to the credential or certificate. This provides a verifiable link to the object's status accreditation. - * @return The builder instance for chaining further configuration calls. - */ - public fun statusListCredential(statusListCredential: URI): Builder = - apply { this.statusListCredential = statusListCredential } - - /** - * Sets the purpose of the status for the object being built. The status purpose is a string that explains why the status is assigned or its role. - * - * @param statusPurpose A string detailing the reason behind the object's status. This clarifies the context or intention of the assigned status. - * @return The builder instance for chaining further configuration calls. - */ - public fun statusPurpose(statusPurpose: String): Builder = apply { this.statusPurpose = statusPurpose } - - /** - * Constructs a [BitstringStatusListEntry] instance with the current state of the builder. - * - * @return A fully constructed [BitstringStatusListEntry] instance. - * @throws IllegalArgumentException If required fields (id, type, statusListIndex, statusListCredential, statusPurpose) are not set. - */ - public fun build(): BitstringStatusListEntry { - require(id != null) { "id must be set" } - require(statusListIndex != null) { "statusListIndex must be set" } - require(statusListCredential != null) { "statusListCredential must be set" } - require(statusPurpose != null) { "statusPurpose must be set" } - - return BitstringStatusListEntry( - id!!, - type ?: "BitstringStatusListEntry", // Assuming type can have a default value - statusListIndex!!, - statusListCredential!!, - statusPurpose!! - ) - } - } - - /** - * Converts the [BitstringStatusListEntry] instance to a JSON string. - * - * @return A JSON string representation of the [BitstringStatusListEntry] instance. - */ - public fun toJson(): String { - val mapper = jacksonObjectMapper() - return mapper.writeValueAsString(this) - } - - - /** - * Converts the [BitstringStatusListEntry] instance into a Map representation. - * - * @return A Map containing key-value pairs representing the properties of the [BitstringStatusListEntry] instance. - */ - public fun toMap(): Map { - return mapOf( - "id" to id.toString(), - "type" to type, - "statusListIndex" to statusListIndex, - "statusListCredential" to statusListCredential.toString(), - "statusPurpose" to statusPurpose - ) - } - - public companion object { - /** - * Creates an instance of [BitstringStatusListEntry] from a map of its properties. - * - * @param map A map containing the properties of a [BitstringStatusListEntry]. - * @return An instance of [BitstringStatusListEntry]. - * @throws IllegalArgumentException If required properties are missing. - */ - public fun fromMap(map: Map): BitstringStatusListEntry { - // Check for required properties and throw IllegalArgumentException if any are missing - require(map.containsKey("id") && map["id"] is String) { "id is required" } - require(map.containsKey("type") && map["type"] is String) { "type is required" } - require(map.containsKey("statusListIndex") && map["statusListIndex"] is String) { - "statusListIndex is required" - } - require(map.containsKey("statusListCredential") && map["statusListCredential"] is String) { - "statusListCredential is required" - } - require(map.containsKey("statusPurpose") && map["statusPurpose"] is String) { "statusPurpose is required" } - - - return BitstringStatusListEntry( - id = URI.create(map["id"] as String), - type = map["type"] as String, - statusListIndex = map["statusListIndex"] as String, - statusListCredential = URI.create(map["statusListCredential"] as String), - statusPurpose = map["statusPurpose"] as String - ) - } - - - /** - * Parses a JSON string to create an instance of [BitstringStatusListEntry]. - * - * @param jsonString The JSON string representation of a [BitstringStatusListEntry]. - * @return An instance of [BitstringStatusListEntry]. - */ - public fun fromJsonObject(jsonString: String): BitstringStatusListEntry { - val mapper = jacksonObjectMapper() - return mapper.readValue(jsonString, object : TypeReference() {}) - } + init { + require(type == null || type.isNotBlank()) { "Type cannot be blank" } } } \ No newline at end of file diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt index b9fbf6572..0184e572f 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt @@ -33,7 +33,7 @@ class StatusListCredentialTest { assertEquals(specExampleRevocableVc.issuer, "did:example:12345") assertNotNull(specExampleRevocableVc.vcDataModel.credentialStatus) assertEquals(specExampleRevocableVc.subject, "did:example:6789") - assertEquals(specExampleRevocableVc.vcDataModel.credentialSubject.toMap()["type"], "Person") + assertEquals(specExampleRevocableVc.vcDataModel.credentialSubject.additionalClaims["type"], "Person") val credentialStatus: BitstringStatusListEntry = BitstringStatusListEntry.fromJsonObject(specExampleRevocableVc.vcDataModel.credentialStatus!!.toJson()) @@ -51,12 +51,12 @@ class StatusListCredentialTest { val issuerDid = DidKey.create(keyManager) val holderDid = DidKey.create(keyManager) - val credentialStatus = BitstringStatusListEntry.Builder() - .id(URI.create("cred-with-status-id")) - .statusPurpose("revocation") - .statusListIndex("123") - .statusListCredential(URI.create("https://example.com/credentials/status/3")) - .build() + val credentialStatus = BitstringStatusListEntry( + id = URI.create("cred-with-status-id"), + statusPurpose = "revocation", + statusListIndex = "123", + statusListCredential = URI.create("https://example.com/credentials/status/3"), + ) val credWithCredStatus = VerifiableCredential.create( type = "StreetCred", @@ -79,7 +79,7 @@ class StatusListCredentialTest { assertEquals(credWithCredStatus.issuer, issuerDid.uri) assertNotNull(credWithCredStatus.vcDataModel.issuanceDate) assertNotNull(credWithCredStatus.vcDataModel.credentialStatus) - assertEquals(credWithCredStatus.vcDataModel.credentialSubject.toMap().get("localRespect"), "high") + assertEquals(credWithCredStatus.vcDataModel.credentialSubject.additionalClaims["localRespect"], "high") val credStatus: BitstringStatusListEntry = BitstringStatusListEntry.fromJsonObject(credWithCredStatus.vcDataModel.credentialStatus!!.toJson()) @@ -97,12 +97,12 @@ class StatusListCredentialTest { val issuerDid = DidKey.create(keyManager) val holderDid = DidKey.create(keyManager) - val credentialStatus1 = BitstringStatusListEntry.Builder() - .id(URI.create("cred-with-status-id")) - .statusPurpose("revocation") - .statusListIndex("123") - .statusListCredential(URI.create("https://example.com/credentials/status/3")) - .build() + val credentialStatus1 = BitstringStatusListEntry( + id = URI.create("cred-with-status-id"), + statusPurpose = "revocation", + statusListIndex = "123", + statusListCredential = URI.create("https://example.com/credentials/status/3"), + ) val vc1 = VerifiableCredential.create( type = "StreetCred", @@ -112,12 +112,12 @@ class StatusListCredentialTest { credentialStatus = credentialStatus1 ) - val credentialStatus2 = BitstringStatusListEntry.Builder() - .id(URI.create("cred-with-status-id")) - .statusPurpose("revocation") - .statusListIndex("124") - .statusListCredential(URI.create("https://example.com/credentials/status/3")) - .build() + val credentialStatus2 = BitstringStatusListEntry( + id = URI.create("cred-with-status-id"), + statusPurpose = "revocation", + statusListIndex = "124", + statusListCredential = URI.create("https://example.com/credentials/status/3"), + ) val vc2 = VerifiableCredential.create( type = "StreetCred", @@ -152,10 +152,10 @@ class StatusListCredentialTest { ) ) assertEquals(statusListCredential.subject, "revocation-id") - assertEquals(statusListCredential.vcDataModel.credentialSubject.toMap()["type"], "StatusList2021") + assertEquals(statusListCredential.vcDataModel.credentialSubject.additionalClaims["type"], "StatusList2021") assertEquals( "revocation", - statusListCredential.vcDataModel.credentialSubject.toMap()["statusPurpose"] as? String? + statusListCredential.vcDataModel.credentialSubject.additionalClaims["statusPurpose"] as? String? ) // TODO: Check encoding across other sdks and spec - https://github.com/TBD54566975/web5-kt/issues/97 @@ -172,12 +172,12 @@ class StatusListCredentialTest { val issuerDid = DidKey.create(keyManager) val holderDid = DidKey.create(keyManager) - val credentialStatus1 = BitstringStatusListEntry.Builder() - .id(URI.create("cred-with-status-id")) - .statusPurpose("revocation") - .statusListIndex("123") - .statusListCredential(URI.create("https://example.com/credentials/status/3")) - .build() + val credentialStatus1 = BitstringStatusListEntry( + id = URI.create("cred-with-status-id"), + statusPurpose = "revocation", + statusListIndex = "123", + statusListCredential = URI.create("https://example.com/credentials/status/3") + ) val vc1 = VerifiableCredential.create( type = "StreetCred", @@ -187,12 +187,12 @@ class StatusListCredentialTest { credentialStatus = credentialStatus1 ) - val credentialStatus2 = BitstringStatusListEntry.Builder() - .id(URI.create("cred-with-status-id")) - .statusPurpose("revocation") - .statusListIndex("123") - .statusListCredential(URI.create("https://example.com/credentials/status/3")) - .build() + val credentialStatus2 = BitstringStatusListEntry( + id = URI.create("cred-with-status-id"), + statusPurpose = "revocation", + statusListIndex = "123", + statusListCredential = URI.create("https://example.com/credentials/status/3") + ) val vc2 = VerifiableCredential.create( type = "StreetCred", @@ -223,12 +223,12 @@ class StatusListCredentialTest { val issuerDid = DidKey.create(keyManager) val holderDid = DidKey.create(keyManager) - val credentialStatus1 = BitstringStatusListEntry.Builder() - .id(URI.create("cred-with-status-id")) - .statusPurpose("revocation") - .statusListIndex("-1") - .statusListCredential(URI.create("https://example.com/credentials/status/3")) - .build() + val credentialStatus1 = BitstringStatusListEntry( + id = URI.create("cred-with-status-id"), + statusPurpose = "revocation", + statusListIndex = "-1", // Note the negative index value + statusListCredential = URI.create("https://example.com/credentials/status/3"), + ) val vc1 = VerifiableCredential.create( type = "StreetCred", @@ -259,12 +259,13 @@ class StatusListCredentialTest { val issuerDid = DidKey.create(keyManager) val holderDid = DidKey.create(keyManager) - val credentialStatus1 = BitstringStatusListEntry.Builder() - .id(URI.create("cred-with-status-id")) - .statusPurpose("revocation") - .statusListIndex(Int.MAX_VALUE.toString()) - .statusListCredential(URI.create("https://example.com/credentials/status/3")) - .build() + val credentialStatus1 = BitstringStatusListEntry( + id = URI.create("cred-with-status-id"), + statusPurpose = "revocation", + statusListIndex = Int.MAX_VALUE.toString(), + statusListCredential = URI.create("https://example.com/credentials/status/3") + ) + val vc1 = VerifiableCredential.create( type = "StreetCred", @@ -295,12 +296,12 @@ class StatusListCredentialTest { val issuerDid = DidKey.create(keyManager) val holderDid = DidKey.create(keyManager) - val credentialStatus1 = BitstringStatusListEntry.Builder() - .id(URI.create("cred-with-status-id")) - .statusPurpose("revocation") - .statusListIndex("123") - .statusListCredential(URI.create("https://example.com/credentials/status/3")) - .build() + val credentialStatus1 = BitstringStatusListEntry( + id = URI.create("cred-with-status-id"), + statusPurpose = "revocation", + statusListIndex = "123", + statusListCredential = URI.create("https://example.com/credentials/status/3"), + ) val vc1 = VerifiableCredential.create( type = "StreetCred", @@ -310,12 +311,12 @@ class StatusListCredentialTest { credentialStatus = credentialStatus1 ) - val credentialStatus2 = BitstringStatusListEntry.Builder() - .id(URI.create("cred-with-status-id")) - .statusPurpose("revocation") - .statusListIndex("124") - .statusListCredential(URI.create("https://example.com/credentials/status/3")) - .build() + val credentialStatus2 = BitstringStatusListEntry( + id = URI.create("cred-with-status-id"), + statusPurpose = "revocation", + statusListIndex = "124", // Note the different index from credentialStatus1 + statusListCredential = URI.create("https://example.com/credentials/status/3"), + ) val vc2 = VerifiableCredential.create( type = "StreetCred", @@ -325,12 +326,12 @@ class StatusListCredentialTest { credentialStatus2 ) - val credentialStatus3 = BitstringStatusListEntry.Builder() - .id(URI.create("cred-with-status-id")) - .statusPurpose("revocation") - .statusListIndex("125") - .statusListCredential(URI.create("https://example.com/credentials/status/3")) - .build() + val credentialStatus3 = BitstringStatusListEntry( + id = URI.create("cred-with-status-id"), + statusPurpose = "revocation", + statusListIndex = "125", + statusListCredential = URI.create("https://example.com/credentials/status/3"), + ) val vc3 = VerifiableCredential.create( type = "StreetCred", @@ -364,12 +365,12 @@ class StatusListCredentialTest { val issuerDid = DidKey.create(keyManager) val holderDid = DidKey.create(keyManager) - val credentialStatus1 = BitstringStatusListEntry.Builder() - .id(URI.create("cred-with-status-id")) - .statusPurpose("revocation") - .statusListIndex("123") - .statusListCredential(URI.create("https://example.com/credentials/status/3")) - .build() + val credentialStatus1 = BitstringStatusListEntry( + id = URI.create("cred-with-status-id"), + statusPurpose = "revocation", + statusListIndex = "123", + statusListCredential = URI.create("https://example.com/credentials/status/3"), + ) val vc1 = VerifiableCredential.create( type = "StreetCred", @@ -379,12 +380,12 @@ class StatusListCredentialTest { credentialStatus = credentialStatus1 ) - val credentialStatus2 = BitstringStatusListEntry.Builder() - .id(URI.create("cred-with-status-id")) - .statusPurpose("revocation") - .statusListIndex("124") - .statusListCredential(URI.create("https://example.com/credentials/status/3")) - .build() + val credentialStatus2 = BitstringStatusListEntry( + id = URI.create("cred-with-status-id"), + statusPurpose = "revocation", + statusListIndex = "124", + statusListCredential = URI.create("https://example.com/credentials/status/3"), + ) val vc2 = VerifiableCredential.create( type = "StreetCred", @@ -434,12 +435,12 @@ class StatusListCredentialTest { val issuerDid = DidKey.create(keyManager) val holderDid = DidKey.create(keyManager) - val credentialStatus1 = BitstringStatusListEntry.Builder() - .id(URI.create("cred-with-status-id")) - .statusPurpose("revocation") - .statusListIndex("123") - .statusListCredential(URI.create("https://example.com/credentials/status/3")) - .build() + val credentialStatus1 = BitstringStatusListEntry( + id = URI.create("cred-with-status-id"), + statusPurpose = "revocation", + statusListIndex = "123", + statusListCredential = URI.create("https://example.com/credentials/status/3"), + ) val credToValidate = VerifiableCredential.create( type = "StreetCred", From 91cfd675857345855e3e5ad92e6a8837eee94400 Mon Sep 17 00:00:00 2001 From: Neal Date: Mon, 26 Feb 2024 11:54:03 -0600 Subject: [PATCH 03/10] updates --- .../model/BitstringStatusListEntry.kt | 13 +- .../web5/sdk/credentials/model/VcDataModel.kt | 43 +++--- .../credentials/VerifiableCredentialTest.kt | 128 ++++++++++++++++++ 3 files changed, 158 insertions(+), 26 deletions(-) diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/model/BitstringStatusListEntry.kt b/credentials/src/main/kotlin/web5/sdk/credentials/model/BitstringStatusListEntry.kt index 13e102d75..0bae2bfb2 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/model/BitstringStatusListEntry.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/model/BitstringStatusListEntry.kt @@ -7,6 +7,7 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.registerKotlinModule import java.net.URI +public const val DEFAULT_BITSTRING_STATUS_LIST_ENTRY_TYPE: String = "BitstringStatusListEntry" private fun getObjectMapper(): ObjectMapper = jacksonObjectMapper().apply { registerKotlinModule() setSerializationInclusion(JsonInclude.Include.NON_NULL) @@ -15,12 +16,12 @@ private fun getObjectMapper(): ObjectMapper = jacksonObjectMapper().apply { /** * BitstringStatusListEntry. */ -public data class BitstringStatusListEntry( - val id: URI, - val type: String = "BitstringStatusListEntry", - val statusListIndex: String, - val statusListCredential: URI, - val statusPurpose: String, +public class BitstringStatusListEntry( + public val id: URI, + public val type: String = DEFAULT_BITSTRING_STATUS_LIST_ENTRY_TYPE, + public val statusListIndex: String, + public val statusListCredential: URI, + public val statusPurpose: String, ) { init { diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt b/credentials/src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt index 9355cc17e..0342c2886 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt @@ -19,6 +19,9 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.TimeZone +public const val DEFAULT_VC_CONTEXT: String = "https://www.w3.org/2018/credentials/v1" +public const val DEFAULT_VC_TYPE: String = "VerifiableCredential" + /** * Global date format used for formatting the issuance and expiration dates of credentials. * The value of the issuanceDate property MUST be a string value of an [XMLSCHEMA11-2] combined date-time @@ -98,25 +101,25 @@ private fun getObjectMapper(): ObjectMapper { * * @see {@link https://www.w3.org/TR/vc-data-model/#credentials | VC Data Model} */ -public data class VcDataModel( - val id: URI? = null, +public class VcDataModel( + public val id: URI? = null, @JsonProperty("@context") - val context: MutableList = mutableListOf(), - val type: MutableList = mutableListOf(), - val issuer: URI, - val issuanceDate: Date, - val expirationDate: Date? = null, - val credentialSubject: CredentialSubject, - val credentialSchema: CredentialSchema? = null, - val credentialStatus: BitstringStatusListEntry? = null + public val context: MutableList = mutableListOf(), + public val type: MutableList = mutableListOf(), + public val issuer: URI, + public val issuanceDate: Date, + public val expirationDate: Date? = null, + public val credentialSubject: CredentialSubject, + public val credentialSchema: CredentialSchema? = null, + public val credentialStatus: BitstringStatusListEntry? = null ) { init { - if(context.isEmpty() || context[0].toString() != "https://www.w3.org/2018/credentials/v1") { - context.add(0, URI.create("https://www.w3.org/2018/credentials/v1")) + if(context.isEmpty() || context[0].toString() != DEFAULT_VC_CONTEXT) { + context.add(0, URI.create(DEFAULT_VC_CONTEXT)) } - if(type.isEmpty() || type[0] != "VerifiableCredential") { - type.add(0, "VerifiableCredential") + if(type.isEmpty() || type[0] != DEFAULT_VC_TYPE) { + type.add(0, DEFAULT_VC_TYPE) } require(id == null || id.toString().isNotBlank()) { "ID URI cannot be blank" } @@ -179,9 +182,9 @@ public data class VcDataModel( * The [CredentialSubject] represents the value of the credentialSubject property as a set of objects containing * properties related to the subject of the verifiable credential. */ -public data class CredentialSubject( - val id: URI? = null, - val additionalClaims: Map = emptyMap() +public class CredentialSubject( + public val id: URI? = null, + public val additionalClaims: Map = emptyMap() ) { init { require(id == null || id.toString().isNotBlank()) { "ID URI cannot be blank" } @@ -191,9 +194,9 @@ public data class CredentialSubject( /** * The [CredentialSchema] Represents the schema defining the structure of a credential. */ -public data class CredentialSchema( - val id: String, - val type: String? = null +public class CredentialSchema( + public val id: String, + public val type: String? = null ) { init { require(type == null || type.isNotBlank()) { "Type cannot be blank" } diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt index e95a81303..a72a6b8bb 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt @@ -13,6 +13,9 @@ import com.nimbusds.jwt.SignedJWT import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow +import web5.sdk.credentials.model.CredentialSchema +import web5.sdk.credentials.model.CredentialSubject +import web5.sdk.credentials.model.VcDataModel import web5.sdk.crypto.AlgorithmId import web5.sdk.crypto.AwsKeyManager import web5.sdk.crypto.InMemoryKeyManager @@ -24,13 +27,18 @@ import web5.sdk.dids.methods.ion.JsonWebKey2020VerificationMethod import web5.sdk.dids.methods.key.DidKey import web5.sdk.testing.TestVectors import java.io.File +import java.net.URI import java.security.SignatureException import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.Date +import java.util.TimeZone import java.util.UUID import kotlin.test.Ignore import kotlin.test.assertEquals import kotlin.test.assertFails import kotlin.test.assertNotNull +import kotlin.test.assertTrue data class StreetCredibility(val localRespect: String, val legit: Boolean) class VerifiableCredentialTest { @@ -225,6 +233,126 @@ class VerifiableCredentialTest { assertEquals(vc.issuer, parsedVc.issuer) assertEquals(vc.subject, parsedVc.subject) } + + @Test + fun `vcDataModel should fail with empty id`() { + var exception = assertThrows(IllegalArgumentException::class.java) { + VcDataModel( + id = URI.create(""), + context = mutableListOf(URI.create("https://www.w3.org/2018/credentials/v1")), + type = mutableListOf("VerifiableCredential"), + issuer = URI.create("did:example:issuer"), + issuanceDate = Date(), + credentialSubject = CredentialSubject( + id = URI.create("did:example:subject"), + additionalClaims = mapOf("claimKey" to "claimValue") + ), + ) + } + + assertTrue(exception.message!!.contains("ID URI cannot be blank")) + } + + @Test + fun `vcDataModel should fail with empty issuer`() { + var exception = assertThrows(IllegalArgumentException::class.java) { + VcDataModel( + id = URI.create("123"), + context = mutableListOf(URI.create("https://www.w3.org/2018/credentials/v1")), + type = mutableListOf("VerifiableCredential"), + issuer = URI.create(""), + issuanceDate = Date(), + credentialSubject = CredentialSubject( + id = URI.create("did:example:subject"), + additionalClaims = mapOf("claimKey" to "claimValue") + ), + ) + } + + assertTrue(exception.message!!.contains("Issuer URI cannot be blank")) + } + + @Test + fun `vcDataModel should fail with issuance date before expiration date`() { + var exception = assertThrows(IllegalArgumentException::class.java) { + VcDataModel( + id = URI.create("123"), + context = mutableListOf(URI.create("https://www.w3.org/2018/credentials/v1")), + type = mutableListOf("VerifiableCredential"), + issuer = URI.create("did:example:issuer"), + issuanceDate = Date(), + expirationDate = Date(Date().time - 100), + credentialSubject = CredentialSubject( + id = URI.create("did:example:subject"), + additionalClaims = mapOf("claimKey" to "claimValue") + ), + ) + } + + assertTrue(exception.message!!.contains("Issuance date must be before expiration date")) + } + + @Test + fun `vcDataModel should add default context and type`() { + + val vcDataModel = VcDataModel( + id = URI.create("123"), + context = mutableListOf(), + type = mutableListOf(), + issuer = URI.create("http://example.com/issuer"), + issuanceDate = Date(), + credentialSubject = CredentialSubject( + id = URI.create("http://example.com/subject"), + additionalClaims = mapOf("claimKey" to "claimValue") + ), + ) + + + assertEquals("https://www.w3.org/2018/credentials/v1", vcDataModel.context[0].toString()) + assertEquals("VerifiableCredential", vcDataModel.type[0]) + } + + @Test + fun `vcDataModel fromJsonObject should correctly parse JSON into VcDataModel`() { + val dateFormat: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").apply { + timeZone = TimeZone.getTimeZone("UTC") + } + val issuanceDate = Date() + val json = """ + { + "id": "http://example.com/credential", + "@context": ["https://www.w3.org/2018/credentials/v1"], + "type": ["VerifiableCredential"], + "issuer": "http://example.com/issuer", + "issuanceDate": "${dateFormat.format(issuanceDate)}", + "credentialSubject": { + "id": "http://example.com/subject", + "additionalClaims": {} + } + } + """.trimIndent() + + val vcDataModel = VcDataModel.fromJsonObject(json) + + assertEquals(URI("http://example.com/credential"), vcDataModel.id) + assertEquals(listOf(URI("https://www.w3.org/2018/credentials/v1")), vcDataModel.context) + assertEquals(listOf("VerifiableCredential"), vcDataModel.type) + assertEquals(URI("http://example.com/issuer"), vcDataModel.issuer) + assertEquals(dateFormat.format(issuanceDate), dateFormat.format(vcDataModel.issuanceDate)) + assertEquals(URI("http://example.com/subject"), vcDataModel.credentialSubject.id) + } + + @Test + fun `vcDataModel credentialSchema should fail with empty type`() { + var exception = assertThrows(IllegalArgumentException::class.java) { + CredentialSchema( + id = "did:example:122", + type = "" + ) + } + + assertTrue(exception.message!!.contains("Type cannot be blank")) + } } class Web5TestVectorsCredentials { From bc6397d6b340d640a3f1a68cc70c4406fc125cd8 Mon Sep 17 00:00:00 2001 From: Neal Date: Mon, 26 Feb 2024 12:12:50 -0600 Subject: [PATCH 04/10] updates --- .../web5/sdk/credentials/StatusListCredential.kt | 13 +++++++++---- .../web5/sdk/credentials/VerifiableCredential.kt | 9 ++++++--- .../credentials/model/BitstringStatusListEntry.kt | 2 ++ .../sdk/credentials/StatusListCredentialTest.kt | 3 ++- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/StatusListCredential.kt b/credentials/src/main/kotlin/web5/sdk/credentials/StatusListCredential.kt index 740a566a9..35f1762c9 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/StatusListCredential.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/StatusListCredential.kt @@ -14,6 +14,11 @@ import io.ktor.serialization.jackson.jackson import kotlinx.coroutines.runBlocking import web5.sdk.credentials.model.BitstringStatusListEntry import web5.sdk.credentials.model.CredentialSubject +import web5.sdk.credentials.model.DEFAULT_BITSTRING_STATUS_LIST_ENTRY_TYPE +import web5.sdk.credentials.model.DEFAULT_BITSTRING_STATUS_LIST_VC_TYPE +import web5.sdk.credentials.model.DEFAULT_STATUS_LIST_CONTEXT +import web5.sdk.credentials.model.DEFAULT_VC_CONTEXT +import web5.sdk.credentials.model.DEFAULT_VC_TYPE import web5.sdk.credentials.model.VcDataModel import web5.sdk.dids.DidResolvers import java.io.ByteArrayInputStream @@ -106,7 +111,7 @@ public object StatusListCredential { throw IllegalArgumentException("issuer: $issuer not resolvable", e) } - val claims = mapOf(TYPE to "StatusList2021", + val claims = mapOf(TYPE to DEFAULT_BITSTRING_STATUS_LIST_ENTRY_TYPE, STATUS_PURPOSE to statusPurpose.toString().lowercase(), ENCODED_LIST to bitString) @@ -117,9 +122,9 @@ public object StatusListCredential { val vcDataModel = VcDataModel( id = URI.create(statusListCredentialId), - context = mutableListOf(URI.create("https://www.w3.org/2018/credentials/v1"), - URI.create("https://w3id.org/vc/status-list/2021/v1")), - type = mutableListOf("VerifiableCredential", "BitstringStatusListCredential"), + context = mutableListOf(URI.create(DEFAULT_VC_CONTEXT), + URI.create(DEFAULT_STATUS_LIST_CONTEXT)), + type = mutableListOf(DEFAULT_VC_TYPE, DEFAULT_BITSTRING_STATUS_LIST_VC_TYPE), issuer = URI.create(issuer), issuanceDate = Date(), credentialSubject = credSubject, diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt index b90c4e4b7..fb19c1391 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt @@ -13,6 +13,9 @@ import com.nimbusds.jwt.JWTParser import com.nimbusds.jwt.SignedJWT import web5.sdk.credentials.model.BitstringStatusListEntry import web5.sdk.credentials.model.CredentialSubject +import web5.sdk.credentials.model.DEFAULT_STATUS_LIST_CONTEXT +import web5.sdk.credentials.model.DEFAULT_VC_CONTEXT +import web5.sdk.credentials.model.DEFAULT_VC_TYPE import web5.sdk.credentials.model.VcDataModel import web5.sdk.credentials.util.JwtUtil import web5.sdk.dids.Did @@ -134,15 +137,15 @@ public class VerifiableCredential internal constructor(public val vcDataModel: V additionalClaims = mapData ) - val contexts = mutableListOf(URI.create("https://www.w3.org/2018/credentials/v1")) + val contexts = mutableListOf(URI.create(DEFAULT_VC_CONTEXT)) if (credentialStatus != null) { - contexts.add(URI.create("https://w3id.org/vc/status-list/2021/v1")) + contexts.add(URI.create(DEFAULT_STATUS_LIST_CONTEXT)) } val vcDataModel = VcDataModel( id = URI.create("urn:uuid:${UUID.randomUUID()}"), context = contexts, - type = mutableListOf("VerifiableCredential", type), + type = mutableListOf(DEFAULT_VC_TYPE, type), issuer = URI.create(issuer), issuanceDate = issuanceDate, expirationDate = expirationDate, diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/model/BitstringStatusListEntry.kt b/credentials/src/main/kotlin/web5/sdk/credentials/model/BitstringStatusListEntry.kt index 0bae2bfb2..2292706f6 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/model/BitstringStatusListEntry.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/model/BitstringStatusListEntry.kt @@ -7,7 +7,9 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.registerKotlinModule import java.net.URI +public const val DEFAULT_BITSTRING_STATUS_LIST_VC_TYPE: String = "BitstringStatusListCredential" public const val DEFAULT_BITSTRING_STATUS_LIST_ENTRY_TYPE: String = "BitstringStatusListEntry" +public const val DEFAULT_STATUS_LIST_CONTEXT: String = "https://w3id.org/vc/status-list/2021/v1" private fun getObjectMapper(): ObjectMapper = jacksonObjectMapper().apply { registerKotlinModule() setSerializationInclusion(JsonInclude.Include.NON_NULL) diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt index 0184e572f..73414fd9d 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt @@ -152,7 +152,8 @@ class StatusListCredentialTest { ) ) assertEquals(statusListCredential.subject, "revocation-id") - assertEquals(statusListCredential.vcDataModel.credentialSubject.additionalClaims["type"], "StatusList2021") + assertEquals(statusListCredential.vcDataModel.credentialSubject + .additionalClaims["type"], "BitstringStatusListEntry") assertEquals( "revocation", statusListCredential.vcDataModel.credentialSubject.additionalClaims["statusPurpose"] as? String? From d4b109a183e9ea316799d67e5ede1ce9e9400908 Mon Sep 17 00:00:00 2001 From: Neal Date: Mon, 26 Feb 2024 12:15:54 -0600 Subject: [PATCH 05/10] change to private --- .../web5/sdk/credentials/model/VcDataModel.kt | 17 +++-------------- .../sdk/credentials/VerifiableCredentialTest.kt | 1 - 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt b/credentials/src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt index 0342c2886..781798526 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt @@ -30,28 +30,20 @@ private val DATE_FORMAT: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm timeZone = TimeZone.getTimeZone("UTC") } -/** - * DateSerializer. - */ private class DateSerializer : JsonSerializer() { override fun serialize(value: Date?, gen: JsonGenerator?, serializers: SerializerProvider?) { gen?.writeString(value?.let { DATE_FORMAT.format(it) }) } } -/** - * DateDeserializer. - */ + private class DateDeserializer : JsonDeserializer() { override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): Date { return DATE_FORMAT.parse(p?.text ?: "") } } -/** - * CredentialSubjectSerializer. - */ -public class CredentialSubjectSerializer : JsonSerializer() { +private class CredentialSubjectSerializer : JsonSerializer() { override fun serialize(value: CredentialSubject, gen: JsonGenerator, serializers: SerializerProvider) { gen.writeStartObject() // Write the id field. If id is null, write an empty string; otherwise, write its string representation. @@ -64,10 +56,7 @@ public class CredentialSubjectSerializer : JsonSerializer() { } } -/** - * CredentialSubjectDeserializer. - */ -public class CredentialSubjectDeserializer : JsonDeserializer() { +private class CredentialSubjectDeserializer : JsonDeserializer() { override fun deserialize(p: JsonParser, ctxt: DeserializationContext): CredentialSubject { val node: JsonNode = p.codec.readTree(p) val idNode = node.get("id") diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt index a72a6b8bb..c3f37f318 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt @@ -375,7 +375,6 @@ class Web5TestVectorsCredentials { val testVectors = mapper.readValue(File("../web5-spec/test-vectors/credentials/create.json"), typeRef) testVectors.vectors.filterNot { it.errors ?: false }.forEach { vector -> - println(vector.description) val vc = VerifiableCredential.fromJson(mapper.writeValueAsString(vector.input.credential)) val keyManager = InMemoryKeyManager() From 3d2d9c6ba31768ce15dae74e04b80546161ce5b5 Mon Sep 17 00:00:00 2001 From: Neal Date: Mon, 26 Feb 2024 12:22:02 -0600 Subject: [PATCH 06/10] comments --- .../web5/sdk/credentials/model/BitstringStatusListEntry.kt | 4 +++- .../src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt | 2 -- .../kotlin/web5/sdk/credentials/StatusListCredentialTest.kt | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/model/BitstringStatusListEntry.kt b/credentials/src/main/kotlin/web5/sdk/credentials/model/BitstringStatusListEntry.kt index 2292706f6..4df83563b 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/model/BitstringStatusListEntry.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/model/BitstringStatusListEntry.kt @@ -16,7 +16,9 @@ private fun getObjectMapper(): ObjectMapper = jacksonObjectMapper().apply { } /** - * BitstringStatusListEntry. + * The [BitstringStatusListEntry] instance representing the core data model of a bitstring status list entry. + * + * @see {@link https://www.w3.org/TR/vc-bitstring-status-list/ | Bitstring Status List } */ public class BitstringStatusListEntry( public val id: URI, diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt b/credentials/src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt index 781798526..1407f9518 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt @@ -36,7 +36,6 @@ private class DateSerializer : JsonSerializer() { } } - private class DateDeserializer : JsonDeserializer() { override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): Date { return DATE_FORMAT.parse(p?.text ?: "") @@ -48,7 +47,6 @@ private class CredentialSubjectSerializer : JsonSerializer() gen.writeStartObject() // Write the id field. If id is null, write an empty string; otherwise, write its string representation. gen.writeStringField("id", value.id?.toString() ?: "") - // Write additional claims directly into the JSON object value.additionalClaims.forEach { (key, claimValue) -> gen.writeObjectField(key, claimValue) } diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt index 73414fd9d..23b131729 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt @@ -227,7 +227,7 @@ class StatusListCredentialTest { val credentialStatus1 = BitstringStatusListEntry( id = URI.create("cred-with-status-id"), statusPurpose = "revocation", - statusListIndex = "-1", // Note the negative index value + statusListIndex = "-1", statusListCredential = URI.create("https://example.com/credentials/status/3"), ) @@ -315,7 +315,7 @@ class StatusListCredentialTest { val credentialStatus2 = BitstringStatusListEntry( id = URI.create("cred-with-status-id"), statusPurpose = "revocation", - statusListIndex = "124", // Note the different index from credentialStatus1 + statusListIndex = "124", statusListCredential = URI.create("https://example.com/credentials/status/3"), ) From 3fa3c56901822996dd780a37186e60b511f52f74 Mon Sep 17 00:00:00 2001 From: Neal Date: Mon, 26 Feb 2024 12:22:32 -0600 Subject: [PATCH 07/10] web5 spec to main --- web5-spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web5-spec b/web5-spec index bdabc55a7..1e498f37f 160000 --- a/web5-spec +++ b/web5-spec @@ -1 +1 @@ -Subproject commit bdabc55a7cad4b469855706f0ab4167d3755756d +Subproject commit 1e498f37f07a1f29c89c10b8a59c7ed9b7d54050 From 8e9131b1ffc5497273ad44bc145b9a1a1f66011f Mon Sep 17 00:00:00 2001 From: Neal Date: Wed, 6 Mar 2024 10:43:05 -0600 Subject: [PATCH 08/10] bring back builder --- .../sdk/credentials/StatusListCredential.kt | 27 ++- .../sdk/credentials/VerifiableCredential.kt | 28 +-- .../model/BitstringStatusListEntry.kt | 73 +++++- .../web5/sdk/credentials/model/VcDataModel.kt | 207 +++++++++++++++--- .../credentials/StatusListCredentialTest.kt | 160 +++++++------- .../credentials/VerifiableCredentialTest.kt | 109 ++++----- 6 files changed, 413 insertions(+), 191 deletions(-) diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/StatusListCredential.kt b/credentials/src/main/kotlin/web5/sdk/credentials/StatusListCredential.kt index 35f1762c9..e569288fa 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/StatusListCredential.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/StatusListCredential.kt @@ -115,20 +115,19 @@ public object StatusListCredential { STATUS_PURPOSE to statusPurpose.toString().lowercase(), ENCODED_LIST to bitString) - val credSubject = CredentialSubject( - id = URI.create(statusListCredentialId), - additionalClaims = claims - ) - - val vcDataModel = VcDataModel( - id = URI.create(statusListCredentialId), - context = mutableListOf(URI.create(DEFAULT_VC_CONTEXT), - URI.create(DEFAULT_STATUS_LIST_CONTEXT)), - type = mutableListOf(DEFAULT_VC_TYPE, DEFAULT_BITSTRING_STATUS_LIST_VC_TYPE), - issuer = URI.create(issuer), - issuanceDate = Date(), - credentialSubject = credSubject, - ) + val credSubject = CredentialSubject.Builder() + .id(URI.create(statusListCredentialId)) + .additionalClaims(claims) + .build() + + val vcDataModel = VcDataModel.Builder() + .id(URI.create(statusListCredentialId)) + .context(mutableListOf(URI.create(DEFAULT_VC_CONTEXT), URI.create(DEFAULT_STATUS_LIST_CONTEXT))) + .type(mutableListOf(DEFAULT_VC_TYPE, DEFAULT_BITSTRING_STATUS_LIST_VC_TYPE)) + .issuer(URI.create(issuer)) + .issuanceDate(Date()) + .credentialSubject(credSubject) + .build() return VerifiableCredential(vcDataModel) } diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt index fb19c1391..b7a93b247 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt @@ -132,26 +132,26 @@ public class VerifiableCredential internal constructor(public val vcDataModel: V false -> throw IllegalArgumentException("expected data to be parseable into a JSON object") } - val credentialSubject = CredentialSubject( - id = URI.create(subject), - additionalClaims = mapData - ) + val credentialSubject = CredentialSubject.Builder() + .id(URI.create(subject)) + .additionalClaims(mapData) + .build() val contexts = mutableListOf(URI.create(DEFAULT_VC_CONTEXT)) if (credentialStatus != null) { contexts.add(URI.create(DEFAULT_STATUS_LIST_CONTEXT)) } - val vcDataModel = VcDataModel( - id = URI.create("urn:uuid:${UUID.randomUUID()}"), - context = contexts, - type = mutableListOf(DEFAULT_VC_TYPE, type), - issuer = URI.create(issuer), - issuanceDate = issuanceDate, - expirationDate = expirationDate, - credentialSubject = credentialSubject, - credentialStatus = credentialStatus - ) + val vcDataModel = VcDataModel.Builder() + .id(URI.create("urn:uuid:${UUID.randomUUID()}")) + .context(contexts) + .type(mutableListOf(DEFAULT_VC_TYPE, type)) + .issuer(URI.create(issuer)) + .issuanceDate(issuanceDate) + .expirationDate(expirationDate) + .credentialSubject(credentialSubject) + .credentialStatus(credentialStatus) + .build() // This should be a no-op just to make sure we've set all the correct fields. validateDataModel(vcDataModel.toMap()) diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/model/BitstringStatusListEntry.kt b/credentials/src/main/kotlin/web5/sdk/credentials/model/BitstringStatusListEntry.kt index 4df83563b..5d2d61207 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/model/BitstringStatusListEntry.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/model/BitstringStatusListEntry.kt @@ -18,21 +18,80 @@ private fun getObjectMapper(): ObjectMapper = jacksonObjectMapper().apply { /** * The [BitstringStatusListEntry] instance representing the core data model of a bitstring status list entry. * - * @see {@link https://www.w3.org/TR/vc-bitstring-status-list/ | Bitstring Status List } + * @see [Bitstring Status List](https://www.w3.org/TR/vc-bitstring-status-list/) */ public class BitstringStatusListEntry( public val id: URI, - public val type: String = DEFAULT_BITSTRING_STATUS_LIST_ENTRY_TYPE, + public val type: String, public val statusListIndex: String, public val statusListCredential: URI, public val statusPurpose: String, ) { + /** + * Builder class for creating [BitstringStatusListEntry] instances. + */ + public class Builder { + private lateinit var id: URI + private var type: String = DEFAULT_BITSTRING_STATUS_LIST_ENTRY_TYPE + private lateinit var statusListIndex: String + private lateinit var statusListCredential: URI + private lateinit var statusPurpose: String + + /** + * Sets the ID for the bitstring status list entry. + * @param id The unique identifier of the bitstring status list entry. + * @return Returns this builder to allow for chaining. + */ + public fun id(id: URI): Builder = apply { this.id = id } + + /** + * Sets the type for the bitstring status list entry. + * @param type The type of the bitstring status list entry. + * @return Returns this builder to allow for chaining. + */ + public fun type(type: String): Builder = apply { this.type = type } + + /** + * Sets the status list index for the bitstring status list entry. + * @param statusListIndex The status list index of the bitstring status list entry. + * @return Returns this builder to allow for chaining. + */ + public fun statusListIndex(statusListIndex: String): Builder = apply { this.statusListIndex = statusListIndex } - init { - require( id.toString().isNotBlank()) { "Id cannot be blank" } - require( statusListIndex.isNotBlank()) { "StatusListIndex cannot be blank" } - require( statusListCredential.toString().isNotBlank()) { "StatusListCredential cannot be blank" } - require( statusPurpose.isNotBlank()) { "StatusPurpose cannot be blank" } + /** + * Sets the status list credential for the bitstring status list entry. + * @param statusListCredential The status list credential of the bitstring status list entry. + * @return Returns this builder to allow for chaining. + */ + public fun statusListCredential(statusListCredential: URI): Builder = + apply { this.statusListCredential = statusListCredential } + + /** + * Sets the status purpose for the bitstring status list entry. + * @param statusPurpose The status purpose of the bitstring status list entry. + * @return Returns this builder to allow for chaining. + */ + public fun statusPurpose(statusPurpose: String): Builder = apply { this.statusPurpose = statusPurpose } + + /** + * Builds and returns the [BitstringStatusListEntry] object. + * @return The constructed [BitstringStatusListEntry] object. + * @throws IllegalStateException If any required fields are not set. + */ + public fun build(): BitstringStatusListEntry { + require(id.toString().isNotBlank()) { "Id cannot be blank" } + require(statusListIndex.isNotBlank()) { "StatusListIndex cannot be blank" } + require(statusListCredential.toString().isNotBlank()) { "StatusListCredential cannot be blank" } + require(statusPurpose.isNotBlank()) { "StatusPurpose cannot be blank" } + + return BitstringStatusListEntry( + id = id, + type = type, + statusListIndex = statusListIndex, + statusListCredential = statusListCredential, + statusPurpose = statusPurpose + ) + } } /** diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt b/credentials/src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt index 1407f9518..2667d0eb4 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt @@ -64,7 +64,10 @@ private class CredentialSubjectDeserializer : JsonDeserializer = mutableListOf(), - public val type: MutableList = mutableListOf(), + public val context: List, + public val type: List, public val issuer: URI, public val issuanceDate: Date, - public val expirationDate: Date? = null, + public val expirationDate: Date?, public val credentialSubject: CredentialSubject, - public val credentialSchema: CredentialSchema? = null, - public val credentialStatus: BitstringStatusListEntry? = null + public val credentialSchema: CredentialSchema?, + public val credentialStatus: BitstringStatusListEntry? ) { - init { - if(context.isEmpty() || context[0].toString() != DEFAULT_VC_CONTEXT) { - context.add(0, URI.create(DEFAULT_VC_CONTEXT)) - } + /** + * Builder class for creating [VcDataModel] instances. + */ + public class Builder { + private var id: URI? = null + private var context: MutableList = mutableListOf() + private var type: MutableList = mutableListOf() + private lateinit var issuer: URI + private lateinit var issuanceDate: Date + private var expirationDate: Date? = null + private lateinit var credentialSubject: CredentialSubject + private var credentialSchema: CredentialSchema? = null + private var credentialStatus: BitstringStatusListEntry? = null - if(type.isEmpty() || type[0] != DEFAULT_VC_TYPE) { - type.add(0, DEFAULT_VC_TYPE) - } + /** + * Sets the ID URI for the [VcDataModel]. + * @param id The unique identifier URI of the credential. + * @return Returns this builder to allow for chaining. + */ + public fun id(id: URI?): Builder = apply { this.id = id } + + /** + * Sets the context URIs for the [VcDataModel]. + * @param context A list of context URIs. + * @return Returns this builder to allow for chaining. + */ + public fun context(context: MutableList): Builder = apply { this.context = context } + + /** + * Sets the type(s) for the [VcDataModel]. + * @param type A list of types. + * @return Returns this builder to allow for chaining. + */ + public fun type(type: MutableList): Builder = apply { this.type = type } + + /** + * Sets the issuer URI for the [VcDataModel]. + * @param issuer The issuer URI of the credential. + * @return Returns this builder to allow for chaining. + */ + public fun issuer(issuer: URI): Builder = apply { this.issuer = issuer } + + /** + * Sets the issuance date for the [VcDataModel]. + * @param issuanceDate The date when the credential was issued. + * @return Returns this builder to allow for chaining. + */ + public fun issuanceDate(issuanceDate: Date): Builder = apply { this.issuanceDate = issuanceDate } + + /** + * Sets the expiration date for the [VcDataModel]. + * @param expirationDate The date when the credential expires. + * @return Returns this builder to allow for chaining. + */ + public fun expirationDate(expirationDate: Date?): Builder = apply { this.expirationDate = expirationDate } + + /** + * Sets the credential subject for the [VcDataModel]. + * @param credentialSubject The subject of the credential. + * @return Returns this builder to allow for chaining. + */ + public fun credentialSubject(credentialSubject: CredentialSubject): Builder = + apply { this.credentialSubject = credentialSubject } - require(id == null || id.toString().isNotBlank()) { "ID URI cannot be blank" } - require(issuer.toString().isNotBlank()) { "Issuer URI cannot be blank" } + /** + * Sets the credential schema for the [VcDataModel]. + * @param credentialSchema The schema of the credential. + * @return Returns this builder to allow for chaining. + */ + public fun credentialSchema(credentialSchema: CredentialSchema?): Builder = + apply { this.credentialSchema = credentialSchema } + + /** + * Sets the credential status for the [VcDataModel]. + * @param credentialStatus The status of the credential. + * @return Returns this builder to allow for chaining. + */ + public fun credentialStatus(credentialStatus: BitstringStatusListEntry?): Builder = + apply { this.credentialStatus = credentialStatus } + + /** + * Builds and returns the [VcDataModel] object. + * @return The constructed [VcDataModel] object. + * @throws IllegalStateException If the issuer or issuance date are not set, or other validation fails. + */ + public fun build(): VcDataModel { + require(issuer.toString().isNotBlank()) { "Issuer URI cannot be blank" } + require(id == null || id.toString().isNotBlank()) { "ID URI cannot be blank" } - if (expirationDate != null) { - require(issuanceDate.before(expirationDate)) { "Issuance date must be before expiration date" } + if (expirationDate != null) { + require(issuanceDate.before(expirationDate)) { "Issuance date must be before expiration date" } + } + + // Default context and type handling + if (context.isEmpty() || context[0].toString() != DEFAULT_VC_CONTEXT) { + context.add(0, URI.create(DEFAULT_VC_CONTEXT)) + } + + if (type.isEmpty() || type[0] != DEFAULT_VC_TYPE) { + type.add(0, DEFAULT_VC_TYPE) + } + + return VcDataModel(id, context.toList(), type.toList(), issuer, issuanceDate, expirationDate, + credentialSubject, credentialSchema, credentialStatus) } } @@ -170,22 +263,84 @@ public class VcDataModel( * properties related to the subject of the verifiable credential. */ public class CredentialSubject( - public val id: URI? = null, - public val additionalClaims: Map = emptyMap() + public val id: URI?, + public val additionalClaims: Map ) { - init { - require(id == null || id.toString().isNotBlank()) { "ID URI cannot be blank" } + /** + * Builder class for creating [CredentialSubject] instances. + */ + public class Builder { + private var id: URI? = null + private var additionalClaims: Map = emptyMap() + + /** + * Sets the ID URI for the credential subject. + * @param id The unique identifier URI of the credential subject. + * @return Returns this builder to allow for chaining. + */ + public fun id(id: URI?): Builder = apply { this.id = id } + + /** + * Sets the additional claims for the credential subject. + * Additional claims provide more information about the credential subject. + * @param additionalClaims A map of claim names to claim values. + * @return Returns this builder to allow for chaining. + */ + public fun additionalClaims(additionalClaims: Map): Builder = + apply { this.additionalClaims = additionalClaims } + + /** + * Builds and returns the [CredentialSubject] object. + * @return The constructed [CredentialSubject] object. + * @throws IllegalStateException If the ID URI is not valid. + */ + public fun build(): CredentialSubject { + require(id == null || id.toString().isNotBlank()) { "ID URI cannot be blank" } + + return CredentialSubject(id, additionalClaims) + } } } + /** * The [CredentialSchema] Represents the schema defining the structure of a credential. */ public class CredentialSchema( public val id: String, - public val type: String? = null + public val type: String? ) { - init { - require(type == null || type.isNotBlank()) { "Type cannot be blank" } + /** + * Builder class for creating [CredentialSchema] instances. + */ + public class Builder { + private var id: String? = null + private var type: String? = null + + /** + * Sets the ID for the credential schema. + * @param id The unique identifier of the credential schema. + * @return Returns this builder to allow for chaining. + */ + public fun id(id: String): Builder = apply { this.id = id } + + /** + * Sets the type for the credential schema. + * @param type The type of the credential schema. + * @return Returns this builder to allow for chaining. + */ + public fun type(type: String?): Builder = apply { this.type = type } + + /** + * Builds and returns the [CredentialSchema] object. + * @return The constructed [CredentialSchema] object. + * @throws IllegalStateException If the id is not set. + */ + public fun build(): CredentialSchema { + require(!id.isNullOrBlank()) { "ID cannot be blank" } + require(type == null || type!!.isNotBlank()) { "Type cannot be blank" } + + return CredentialSchema(id!!, type) + } } -} \ No newline at end of file +} diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt index 23b131729..96fdde6bf 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt @@ -51,12 +51,13 @@ class StatusListCredentialTest { val issuerDid = DidKey.create(keyManager) val holderDid = DidKey.create(keyManager) - val credentialStatus = BitstringStatusListEntry( - id = URI.create("cred-with-status-id"), - statusPurpose = "revocation", - statusListIndex = "123", - statusListCredential = URI.create("https://example.com/credentials/status/3"), - ) + val credentialStatus = BitstringStatusListEntry.Builder() + .id(URI.create("cred-with-status-id")) + .statusPurpose("revocation") + .statusListIndex("123") + .statusListCredential(URI.create("https://example.com/credentials/status/3")) + .build() + val credWithCredStatus = VerifiableCredential.create( type = "StreetCred", @@ -97,12 +98,12 @@ class StatusListCredentialTest { val issuerDid = DidKey.create(keyManager) val holderDid = DidKey.create(keyManager) - val credentialStatus1 = BitstringStatusListEntry( - id = URI.create("cred-with-status-id"), - statusPurpose = "revocation", - statusListIndex = "123", - statusListCredential = URI.create("https://example.com/credentials/status/3"), - ) + val credentialStatus1 = BitstringStatusListEntry.Builder() + .id(URI.create("cred-with-status-id")) + .statusPurpose("revocation") + .statusListIndex("123") + .statusListCredential(URI.create("https://example.com/credentials/status/3")) + .build() val vc1 = VerifiableCredential.create( type = "StreetCred", @@ -112,12 +113,12 @@ class StatusListCredentialTest { credentialStatus = credentialStatus1 ) - val credentialStatus2 = BitstringStatusListEntry( - id = URI.create("cred-with-status-id"), - statusPurpose = "revocation", - statusListIndex = "124", - statusListCredential = URI.create("https://example.com/credentials/status/3"), - ) + val credentialStatus2 = BitstringStatusListEntry.Builder() + .id(URI.create("cred-with-status-id")) + .statusPurpose("revocation") + .statusListIndex("124") + .statusListCredential(URI.create("https://example.com/credentials/status/3")) + .build() val vc2 = VerifiableCredential.create( type = "StreetCred", @@ -173,12 +174,12 @@ class StatusListCredentialTest { val issuerDid = DidKey.create(keyManager) val holderDid = DidKey.create(keyManager) - val credentialStatus1 = BitstringStatusListEntry( - id = URI.create("cred-with-status-id"), - statusPurpose = "revocation", - statusListIndex = "123", - statusListCredential = URI.create("https://example.com/credentials/status/3") - ) + val credentialStatus1 = BitstringStatusListEntry.Builder() + .id(URI.create("cred-with-status-id")) + .statusPurpose("revocation") + .statusListIndex("123") + .statusListCredential(URI.create("https://example.com/credentials/status/3")) + .build() val vc1 = VerifiableCredential.create( type = "StreetCred", @@ -188,12 +189,12 @@ class StatusListCredentialTest { credentialStatus = credentialStatus1 ) - val credentialStatus2 = BitstringStatusListEntry( - id = URI.create("cred-with-status-id"), - statusPurpose = "revocation", - statusListIndex = "123", - statusListCredential = URI.create("https://example.com/credentials/status/3") - ) + val credentialStatus2 = BitstringStatusListEntry.Builder() + .id(URI.create("cred-with-status-id")) + .statusPurpose("revocation") + .statusListIndex("123") + .statusListCredential(URI.create("https://example.com/credentials/status/3")) + .build() val vc2 = VerifiableCredential.create( type = "StreetCred", @@ -224,12 +225,13 @@ class StatusListCredentialTest { val issuerDid = DidKey.create(keyManager) val holderDid = DidKey.create(keyManager) - val credentialStatus1 = BitstringStatusListEntry( - id = URI.create("cred-with-status-id"), - statusPurpose = "revocation", - statusListIndex = "-1", - statusListCredential = URI.create("https://example.com/credentials/status/3"), - ) + val credentialStatus1 = BitstringStatusListEntry.Builder() + .id(URI.create("cred-with-status-id")) + .statusPurpose("revocation") + .statusListIndex("-1") + .statusListCredential(URI.create("https://example.com/credentials/status/3")) + .build() + val vc1 = VerifiableCredential.create( type = "StreetCred", @@ -260,13 +262,12 @@ class StatusListCredentialTest { val issuerDid = DidKey.create(keyManager) val holderDid = DidKey.create(keyManager) - val credentialStatus1 = BitstringStatusListEntry( - id = URI.create("cred-with-status-id"), - statusPurpose = "revocation", - statusListIndex = Int.MAX_VALUE.toString(), - statusListCredential = URI.create("https://example.com/credentials/status/3") - ) - + val credentialStatus1 = BitstringStatusListEntry.Builder() + .id(URI.create("cred-with-status-id")) + .statusPurpose("revocation") + .statusListIndex(Int.MAX_VALUE.toString()) + .statusListCredential(URI.create("https://example.com/credentials/status/3")) + .build() val vc1 = VerifiableCredential.create( type = "StreetCred", @@ -297,12 +298,12 @@ class StatusListCredentialTest { val issuerDid = DidKey.create(keyManager) val holderDid = DidKey.create(keyManager) - val credentialStatus1 = BitstringStatusListEntry( - id = URI.create("cred-with-status-id"), - statusPurpose = "revocation", - statusListIndex = "123", - statusListCredential = URI.create("https://example.com/credentials/status/3"), - ) + val credentialStatus1 = BitstringStatusListEntry.Builder() + .id(URI.create("cred-with-status-id")) + .statusPurpose("revocation") + .statusListIndex("123") + .statusListCredential(URI.create("https://example.com/credentials/status/3")) + .build() val vc1 = VerifiableCredential.create( type = "StreetCred", @@ -312,12 +313,13 @@ class StatusListCredentialTest { credentialStatus = credentialStatus1 ) - val credentialStatus2 = BitstringStatusListEntry( - id = URI.create("cred-with-status-id"), - statusPurpose = "revocation", - statusListIndex = "124", - statusListCredential = URI.create("https://example.com/credentials/status/3"), - ) + val credentialStatus2 = BitstringStatusListEntry.Builder() + .id(URI.create("cred-with-status-id")) + .statusPurpose("revocation") + .statusListIndex("124") + .statusListCredential(URI.create("https://example.com/credentials/status/3")) + .build() + val vc2 = VerifiableCredential.create( type = "StreetCred", @@ -327,12 +329,12 @@ class StatusListCredentialTest { credentialStatus2 ) - val credentialStatus3 = BitstringStatusListEntry( - id = URI.create("cred-with-status-id"), - statusPurpose = "revocation", - statusListIndex = "125", - statusListCredential = URI.create("https://example.com/credentials/status/3"), - ) + val credentialStatus3 = BitstringStatusListEntry.Builder() + .id(URI.create("cred-with-status-id")) + .statusPurpose("revocation") + .statusListIndex("125") + .statusListCredential(URI.create("https://example.com/credentials/status/3")) + .build() val vc3 = VerifiableCredential.create( type = "StreetCred", @@ -366,12 +368,12 @@ class StatusListCredentialTest { val issuerDid = DidKey.create(keyManager) val holderDid = DidKey.create(keyManager) - val credentialStatus1 = BitstringStatusListEntry( - id = URI.create("cred-with-status-id"), - statusPurpose = "revocation", - statusListIndex = "123", - statusListCredential = URI.create("https://example.com/credentials/status/3"), - ) + val credentialStatus1 = BitstringStatusListEntry.Builder() + .id(URI.create("cred-with-status-id")) + .statusPurpose("revocation") + .statusListIndex("123") + .statusListCredential(URI.create("https://example.com/credentials/status/3")) + .build() val vc1 = VerifiableCredential.create( type = "StreetCred", @@ -381,12 +383,12 @@ class StatusListCredentialTest { credentialStatus = credentialStatus1 ) - val credentialStatus2 = BitstringStatusListEntry( - id = URI.create("cred-with-status-id"), - statusPurpose = "revocation", - statusListIndex = "124", - statusListCredential = URI.create("https://example.com/credentials/status/3"), - ) + val credentialStatus2 = BitstringStatusListEntry.Builder() + .id(URI.create("cred-with-status-id")) + .statusPurpose("revocation") + .statusListIndex("124") + .statusListCredential(URI.create("https://example.com/credentials/status/3")) + .build() val vc2 = VerifiableCredential.create( type = "StreetCred", @@ -436,12 +438,12 @@ class StatusListCredentialTest { val issuerDid = DidKey.create(keyManager) val holderDid = DidKey.create(keyManager) - val credentialStatus1 = BitstringStatusListEntry( - id = URI.create("cred-with-status-id"), - statusPurpose = "revocation", - statusListIndex = "123", - statusListCredential = URI.create("https://example.com/credentials/status/3"), - ) + val credentialStatus1 = BitstringStatusListEntry.Builder() + .id(URI.create("cred-with-status-id")) + .statusPurpose("revocation") + .statusListIndex("123") + .statusListCredential(URI.create("https://example.com/credentials/status/3")) + .build() val credToValidate = VerifiableCredential.create( type = "StreetCred", diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt index c3f37f318..c5fcdea8e 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt @@ -237,17 +237,19 @@ class VerifiableCredentialTest { @Test fun `vcDataModel should fail with empty id`() { var exception = assertThrows(IllegalArgumentException::class.java) { - VcDataModel( - id = URI.create(""), - context = mutableListOf(URI.create("https://www.w3.org/2018/credentials/v1")), - type = mutableListOf("VerifiableCredential"), - issuer = URI.create("did:example:issuer"), - issuanceDate = Date(), - credentialSubject = CredentialSubject( - id = URI.create("did:example:subject"), - additionalClaims = mapOf("claimKey" to "claimValue") - ), - ) + VcDataModel.Builder() + .id(URI.create("")) + .context(mutableListOf(URI.create("https://www.w3.org/2018/credentials/v1"))) + .type(mutableListOf("VerifiableCredential")) + .issuer(URI.create("did:example:issuer")) + .issuanceDate(Date()) + .credentialSubject( + CredentialSubject.Builder() + .id(URI.create("did:example:subject")) + .additionalClaims(mapOf("claimKey" to "claimValue")) + .build() + ) + .build() } assertTrue(exception.message!!.contains("ID URI cannot be blank")) @@ -256,17 +258,19 @@ class VerifiableCredentialTest { @Test fun `vcDataModel should fail with empty issuer`() { var exception = assertThrows(IllegalArgumentException::class.java) { - VcDataModel( - id = URI.create("123"), - context = mutableListOf(URI.create("https://www.w3.org/2018/credentials/v1")), - type = mutableListOf("VerifiableCredential"), - issuer = URI.create(""), - issuanceDate = Date(), - credentialSubject = CredentialSubject( - id = URI.create("did:example:subject"), - additionalClaims = mapOf("claimKey" to "claimValue") - ), - ) + VcDataModel.Builder() + .id(URI.create("123")) + .context(mutableListOf(URI.create("https://www.w3.org/2018/credentials/v1"))) + .type(mutableListOf("VerifiableCredential")) + .issuer(URI.create("")) + .issuanceDate(Date()) + .credentialSubject( + CredentialSubject.Builder() + .id(URI.create("did:example:subject")) + .additionalClaims(mapOf("claimKey" to "claimValue")) + .build() + ) + .build() } assertTrue(exception.message!!.contains("Issuer URI cannot be blank")) @@ -275,18 +279,20 @@ class VerifiableCredentialTest { @Test fun `vcDataModel should fail with issuance date before expiration date`() { var exception = assertThrows(IllegalArgumentException::class.java) { - VcDataModel( - id = URI.create("123"), - context = mutableListOf(URI.create("https://www.w3.org/2018/credentials/v1")), - type = mutableListOf("VerifiableCredential"), - issuer = URI.create("did:example:issuer"), - issuanceDate = Date(), - expirationDate = Date(Date().time - 100), - credentialSubject = CredentialSubject( - id = URI.create("did:example:subject"), - additionalClaims = mapOf("claimKey" to "claimValue") - ), - ) + VcDataModel.Builder() + .id(URI.create("123")) + .context(mutableListOf(URI.create("https://www.w3.org/2018/credentials/v1"))) + .type(mutableListOf("VerifiableCredential")) + .issuer(URI.create("did:example:issuer")) + .issuanceDate(Date()) + .expirationDate(Date(Date().time - 100)) + .credentialSubject( + CredentialSubject.Builder() + .id(URI.create("did:example:subject")) + .additionalClaims(mapOf("claimKey" to "claimValue")) + .build() + ) + .build() } assertTrue(exception.message!!.contains("Issuance date must be before expiration date")) @@ -295,18 +301,19 @@ class VerifiableCredentialTest { @Test fun `vcDataModel should add default context and type`() { - val vcDataModel = VcDataModel( - id = URI.create("123"), - context = mutableListOf(), - type = mutableListOf(), - issuer = URI.create("http://example.com/issuer"), - issuanceDate = Date(), - credentialSubject = CredentialSubject( - id = URI.create("http://example.com/subject"), - additionalClaims = mapOf("claimKey" to "claimValue") - ), - ) - + val vcDataModel = VcDataModel.Builder() + .id(URI.create("123")) + .context(mutableListOf()) // Assuming default context is handled by the Builder if necessary + .type(mutableListOf()) // Assuming default type is handled by the Builder if necessary + .issuer(URI.create("http://example.com/issuer")) + .issuanceDate(Date()) + .credentialSubject( + CredentialSubject.Builder() + .id(URI.create("http://example.com/subject")) + .additionalClaims(mapOf("claimKey" to "claimValue")) + .build() + ) + .build() assertEquals("https://www.w3.org/2018/credentials/v1", vcDataModel.context[0].toString()) assertEquals("VerifiableCredential", vcDataModel.type[0]) @@ -344,11 +351,11 @@ class VerifiableCredentialTest { @Test fun `vcDataModel credentialSchema should fail with empty type`() { - var exception = assertThrows(IllegalArgumentException::class.java) { - CredentialSchema( - id = "did:example:122", - type = "" - ) + val exception = assertThrows(IllegalArgumentException::class.java) { + CredentialSchema.Builder() + .id("did:example:123") + .type("") + .build() } assertTrue(exception.message!!.contains("Type cannot be blank")) From 4793832d67b3c48ed07e28397a6dd60d0c8b1659 Mon Sep 17 00:00:00 2001 From: Neal Date: Wed, 6 Mar 2024 11:16:45 -0600 Subject: [PATCH 09/10] rename status list 2021 --- .../sdk/credentials/StatusListCredential.kt | 23 +++--- .../sdk/credentials/VerifiableCredential.kt | 4 +- ...tusListEntry.kt => StatusList2021Entry.kt} | 70 +++++++++---------- .../web5/sdk/credentials/model/VcDataModel.kt | 6 +- .../credentials/StatusListCredentialTest.kt | 45 ++++++------ .../src/test/resources/revocable_vc.json | 28 ++++---- 6 files changed, 88 insertions(+), 88 deletions(-) rename credentials/src/main/kotlin/web5/sdk/credentials/model/{BitstringStatusListEntry.kt => StatusList2021Entry.kt} (55%) diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/StatusListCredential.kt b/credentials/src/main/kotlin/web5/sdk/credentials/StatusListCredential.kt index e569288fa..ca6665eb7 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/StatusListCredential.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/StatusListCredential.kt @@ -12,13 +12,13 @@ import io.ktor.client.statement.bodyAsText import io.ktor.http.isSuccess import io.ktor.serialization.jackson.jackson import kotlinx.coroutines.runBlocking -import web5.sdk.credentials.model.BitstringStatusListEntry import web5.sdk.credentials.model.CredentialSubject -import web5.sdk.credentials.model.DEFAULT_BITSTRING_STATUS_LIST_ENTRY_TYPE -import web5.sdk.credentials.model.DEFAULT_BITSTRING_STATUS_LIST_VC_TYPE +import web5.sdk.credentials.model.DEFAULT_STATUS_LIST_2021_ENTRY_TYPE +import web5.sdk.credentials.model.DEFAULT_STATUS_LIST_2021_VC_TYPE import web5.sdk.credentials.model.DEFAULT_STATUS_LIST_CONTEXT import web5.sdk.credentials.model.DEFAULT_VC_CONTEXT import web5.sdk.credentials.model.DEFAULT_VC_TYPE +import web5.sdk.credentials.model.StatusList2021Entry import web5.sdk.credentials.model.VcDataModel import web5.sdk.dids.DidResolvers import java.io.ByteArrayInputStream @@ -27,7 +27,6 @@ import java.net.URI import java.util.Base64 import java.util.BitSet import java.util.Date -import java.util.UUID import java.util.zip.GZIPInputStream import java.util.zip.GZIPOutputStream @@ -111,7 +110,7 @@ public object StatusListCredential { throw IllegalArgumentException("issuer: $issuer not resolvable", e) } - val claims = mapOf(TYPE to DEFAULT_BITSTRING_STATUS_LIST_ENTRY_TYPE, + val claims = mapOf(TYPE to DEFAULT_STATUS_LIST_2021_ENTRY_TYPE, STATUS_PURPOSE to statusPurpose.toString().lowercase(), ENCODED_LIST to bitString) @@ -123,7 +122,7 @@ public object StatusListCredential { val vcDataModel = VcDataModel.Builder() .id(URI.create(statusListCredentialId)) .context(mutableListOf(URI.create(DEFAULT_VC_CONTEXT), URI.create(DEFAULT_STATUS_LIST_CONTEXT))) - .type(mutableListOf(DEFAULT_VC_TYPE, DEFAULT_BITSTRING_STATUS_LIST_VC_TYPE)) + .type(mutableListOf(DEFAULT_VC_TYPE, DEFAULT_STATUS_LIST_2021_VC_TYPE)) .issuer(URI.create(issuer)) .issuanceDate(Date()) .credentialSubject(credSubject) @@ -150,8 +149,8 @@ public object StatusListCredential { credentialToValidate: VerifiableCredential, statusListCredential: VerifiableCredential ): Boolean { - val statusListEntryValue: BitstringStatusListEntry = - BitstringStatusListEntry.fromJsonObject(credentialToValidate.vcDataModel.credentialStatus!!.toJson()) + val statusListEntryValue: StatusList2021Entry = + StatusList2021Entry.fromJsonObject(credentialToValidate.vcDataModel.credentialStatus!!.toJson()) val credentialSubject = statusListCredential.vcDataModel.credentialSubject @@ -212,8 +211,8 @@ public object StatusListCredential { val client = httpClient ?: defaultHttpClient().also { isDefaultClient = true } try { - val statusListEntryValue: BitstringStatusListEntry = - BitstringStatusListEntry.fromJsonObject(credentialToValidate.vcDataModel.credentialStatus!!.toJson()) + val statusListEntryValue: StatusList2021Entry = + StatusList2021Entry.fromJsonObject(credentialToValidate.vcDataModel.credentialStatus!!.toJson()) val statusListCredential = client.fetchStatusListCredential(statusListEntryValue.statusListCredential.toString()) @@ -270,8 +269,8 @@ public object StatusListCredential { for (vc in credentials) { requireNotNull(vc.vcDataModel.credentialStatus) { "no credential status found in credential" } - val statusListEntry: BitstringStatusListEntry = - BitstringStatusListEntry.fromJsonObject(vc.vcDataModel.credentialStatus.toJson()) + val statusListEntry: StatusList2021Entry = + StatusList2021Entry.fromJsonObject(vc.vcDataModel.credentialStatus.toJson()) require(statusListEntry.statusPurpose == statusPurpose.toString().lowercase()) { "status purpose mismatch" diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt index b7a93b247..552ad5eae 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/VerifiableCredential.kt @@ -11,11 +11,11 @@ import com.nfeld.jsonpathkt.extension.read import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.JWTParser import com.nimbusds.jwt.SignedJWT -import web5.sdk.credentials.model.BitstringStatusListEntry import web5.sdk.credentials.model.CredentialSubject import web5.sdk.credentials.model.DEFAULT_STATUS_LIST_CONTEXT import web5.sdk.credentials.model.DEFAULT_VC_CONTEXT import web5.sdk.credentials.model.DEFAULT_VC_TYPE +import web5.sdk.credentials.model.StatusList2021Entry import web5.sdk.credentials.model.VcDataModel import web5.sdk.credentials.util.JwtUtil import web5.sdk.dids.Did @@ -121,7 +121,7 @@ public class VerifiableCredential internal constructor(public val vcDataModel: V issuer: String, subject: String, data: T, - credentialStatus: BitstringStatusListEntry? = null, + credentialStatus: StatusList2021Entry? = null, issuanceDate: Date = Date(), expirationDate: Date? = null ): VerifiableCredential { diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/model/BitstringStatusListEntry.kt b/credentials/src/main/kotlin/web5/sdk/credentials/model/StatusList2021Entry.kt similarity index 55% rename from credentials/src/main/kotlin/web5/sdk/credentials/model/BitstringStatusListEntry.kt rename to credentials/src/main/kotlin/web5/sdk/credentials/model/StatusList2021Entry.kt index 5d2d61207..0ff3cd7fb 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/model/BitstringStatusListEntry.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/model/StatusList2021Entry.kt @@ -7,8 +7,8 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.registerKotlinModule import java.net.URI -public const val DEFAULT_BITSTRING_STATUS_LIST_VC_TYPE: String = "BitstringStatusListCredential" -public const val DEFAULT_BITSTRING_STATUS_LIST_ENTRY_TYPE: String = "BitstringStatusListEntry" +public const val DEFAULT_STATUS_LIST_2021_VC_TYPE: String = "StatusList2021" +public const val DEFAULT_STATUS_LIST_2021_ENTRY_TYPE: String = "StatusList2021Entry" public const val DEFAULT_STATUS_LIST_CONTEXT: String = "https://w3id.org/vc/status-list/2021/v1" private fun getObjectMapper(): ObjectMapper = jacksonObjectMapper().apply { registerKotlinModule() @@ -16,11 +16,11 @@ private fun getObjectMapper(): ObjectMapper = jacksonObjectMapper().apply { } /** - * The [BitstringStatusListEntry] instance representing the core data model of a bitstring status list entry. + * The [StatusList2021Entry] instance representing the core data model of a bitstring status list entry. * - * @see [Bitstring Status List](https://www.w3.org/TR/vc-bitstring-status-list/) + * @see [Credential Status List](https://www.w3.org/community/reports/credentials/CG-FINAL-vc-status-list-2021-20230102/) */ -public class BitstringStatusListEntry( +public class StatusList2021Entry( public val id: URI, public val type: String, public val statusListIndex: String, @@ -28,63 +28,63 @@ public class BitstringStatusListEntry( public val statusPurpose: String, ) { /** - * Builder class for creating [BitstringStatusListEntry] instances. + * Builder class for creating [StatusList2021Entry] instances. */ public class Builder { private lateinit var id: URI - private var type: String = DEFAULT_BITSTRING_STATUS_LIST_ENTRY_TYPE + private var type: String = DEFAULT_STATUS_LIST_2021_ENTRY_TYPE private lateinit var statusListIndex: String private lateinit var statusListCredential: URI private lateinit var statusPurpose: String /** - * Sets the ID for the bitstring status list entry. - * @param id The unique identifier of the bitstring status list entry. + * Sets the ID for the credential status list entry. + * @param id The unique identifier of the credential status list entry. * @return Returns this builder to allow for chaining. */ public fun id(id: URI): Builder = apply { this.id = id } /** - * Sets the type for the bitstring status list entry. - * @param type The type of the bitstring status list entry. + * Sets the type for the credential status list entry. + * @param type The type of the credential status list entry. * @return Returns this builder to allow for chaining. */ public fun type(type: String): Builder = apply { this.type = type } /** - * Sets the status list index for the bitstring status list entry. - * @param statusListIndex The status list index of the bitstring status list entry. + * Sets the status list index for the credential status list entry. + * @param statusListIndex The status list index of the credential status list entry. * @return Returns this builder to allow for chaining. */ public fun statusListIndex(statusListIndex: String): Builder = apply { this.statusListIndex = statusListIndex } /** - * Sets the status list credential for the bitstring status list entry. - * @param statusListCredential The status list credential of the bitstring status list entry. + * Sets the status list credential for the credential status list entry. + * @param statusListCredential The status list credential of the credential status list entry. * @return Returns this builder to allow for chaining. */ public fun statusListCredential(statusListCredential: URI): Builder = apply { this.statusListCredential = statusListCredential } /** - * Sets the status purpose for the bitstring status list entry. - * @param statusPurpose The status purpose of the bitstring status list entry. + * Sets the status purpose for the credential status list entry. + * @param statusPurpose The status purpose of the credential status list entry. * @return Returns this builder to allow for chaining. */ public fun statusPurpose(statusPurpose: String): Builder = apply { this.statusPurpose = statusPurpose } /** - * Builds and returns the [BitstringStatusListEntry] object. - * @return The constructed [BitstringStatusListEntry] object. + * Builds and returns the [StatusList2021Entry] object. + * @return The constructed [StatusList2021Entry] object. * @throws IllegalStateException If any required fields are not set. */ - public fun build(): BitstringStatusListEntry { + public fun build(): StatusList2021Entry { require(id.toString().isNotBlank()) { "Id cannot be blank" } require(statusListIndex.isNotBlank()) { "StatusListIndex cannot be blank" } require(statusListCredential.toString().isNotBlank()) { "StatusListCredential cannot be blank" } require(statusPurpose.isNotBlank()) { "StatusPurpose cannot be blank" } - return BitstringStatusListEntry( + return StatusList2021Entry( id = id, type = type, statusListIndex = statusListIndex, @@ -95,40 +95,40 @@ public class BitstringStatusListEntry( } /** - * Converts the [BitstringStatusListEntry] instance to a JSON string. + * Converts the [StatusList2021Entry] instance to a JSON string. * - * @return A JSON string representation of the [BitstringStatusListEntry] instance. + * @return A JSON string representation of the [StatusList2021Entry] instance. */ public fun toJson(): String = getObjectMapper().writeValueAsString(this) /** - * Converts the [BitstringStatusListEntry] instance into a Map representation. + * Converts the [StatusList2021Entry] instance into a Map representation. * - * @return A Map containing key-value pairs representing the properties of the [BitstringStatusListEntry] instance. + * @return A Map containing key-value pairs representing the properties of the [StatusList2021Entry] instance. */ public fun toMap(): Map = getObjectMapper().readValue(this.toJson(), object : TypeReference>() {}) public companion object { /** - * Parses a JSON string to create an instance of [BitstringStatusListEntry]. + * Parses a JSON string to create an instance of [StatusList2021Entry]. * - * @param jsonString The JSON string representation of a [BitstringStatusListEntry]. - * @return An instance of [BitstringStatusListEntry]. + * @param jsonString The JSON string representation of a [StatusList2021Entry]. + * @return An instance of [StatusList2021Entry]. */ - public fun fromJsonObject(jsonString: String): BitstringStatusListEntry = - getObjectMapper().readValue(jsonString, BitstringStatusListEntry::class.java) + public fun fromJsonObject(jsonString: String): StatusList2021Entry = + getObjectMapper().readValue(jsonString, StatusList2021Entry::class.java) /** - * Creates an instance of [BitstringStatusListEntry] from a map of its properties. + * Creates an instance of [StatusList2021Entry] from a map of its properties. * - * @param map A map containing the properties of a [BitstringStatusListEntry]. - * @return An instance of [BitstringStatusListEntry]. + * @param map A map containing the properties of a [StatusList2021Entry]. + * @return An instance of [StatusList2021Entry]. * @throws IllegalArgumentException If required properties are missing. */ - public fun fromMap(map: Map): BitstringStatusListEntry { + public fun fromMap(map: Map): StatusList2021Entry { val json = getObjectMapper().writeValueAsString(map) - return getObjectMapper().readValue(json, BitstringStatusListEntry::class.java) + return getObjectMapper().readValue(json, StatusList2021Entry::class.java) } } } \ No newline at end of file diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt b/credentials/src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt index 2667d0eb4..eb2323596 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt @@ -101,7 +101,7 @@ public class VcDataModel( public val expirationDate: Date?, public val credentialSubject: CredentialSubject, public val credentialSchema: CredentialSchema?, - public val credentialStatus: BitstringStatusListEntry? + public val credentialStatus: StatusList2021Entry? ) { /** * Builder class for creating [VcDataModel] instances. @@ -115,7 +115,7 @@ public class VcDataModel( private var expirationDate: Date? = null private lateinit var credentialSubject: CredentialSubject private var credentialSchema: CredentialSchema? = null - private var credentialStatus: BitstringStatusListEntry? = null + private var credentialStatus: StatusList2021Entry? = null /** * Sets the ID URI for the [VcDataModel]. @@ -180,7 +180,7 @@ public class VcDataModel( * @param credentialStatus The status of the credential. * @return Returns this builder to allow for chaining. */ - public fun credentialStatus(credentialStatus: BitstringStatusListEntry?): Builder = + public fun credentialStatus(credentialStatus: StatusList2021Entry?): Builder = apply { this.credentialStatus = credentialStatus } /** diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt index 96fdde6bf..927dac625 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/StatusListCredentialTest.kt @@ -8,7 +8,7 @@ import io.ktor.http.fullPath import io.ktor.http.headersOf import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.assertThrows -import web5.sdk.credentials.model.BitstringStatusListEntry +import web5.sdk.credentials.model.StatusList2021Entry import web5.sdk.crypto.InMemoryKeyManager import web5.sdk.dids.methods.key.DidKey import java.io.File @@ -35,11 +35,11 @@ class StatusListCredentialTest { assertEquals(specExampleRevocableVc.subject, "did:example:6789") assertEquals(specExampleRevocableVc.vcDataModel.credentialSubject.additionalClaims["type"], "Person") - val credentialStatus: BitstringStatusListEntry = - BitstringStatusListEntry.fromJsonObject(specExampleRevocableVc.vcDataModel.credentialStatus!!.toJson()) + val credentialStatus: StatusList2021Entry = + StatusList2021Entry.fromJsonObject(specExampleRevocableVc.vcDataModel.credentialStatus!!.toJson()) assertEquals(credentialStatus.id.toString(), "https://example.com/credentials/status/3#94567") - assertEquals(credentialStatus.toMap()["type"].toString(), "BitStringStatusListEntry") + assertEquals(credentialStatus.toMap()["type"].toString(), "StatusList2021Entry") assertEquals(credentialStatus.statusPurpose.toString(), StatusPurpose.REVOCATION.toString().lowercase()) assertEquals(credentialStatus.statusListIndex, "94567") assertEquals(credentialStatus.statusListCredential.toString(), "https://example.com/credentials/status/3") @@ -51,7 +51,7 @@ class StatusListCredentialTest { val issuerDid = DidKey.create(keyManager) val holderDid = DidKey.create(keyManager) - val credentialStatus = BitstringStatusListEntry.Builder() + val credentialStatus = StatusList2021Entry.Builder() .id(URI.create("cred-with-status-id")) .statusPurpose("revocation") .statusListIndex("123") @@ -82,11 +82,12 @@ class StatusListCredentialTest { assertNotNull(credWithCredStatus.vcDataModel.credentialStatus) assertEquals(credWithCredStatus.vcDataModel.credentialSubject.additionalClaims["localRespect"], "high") - val credStatus: BitstringStatusListEntry = - BitstringStatusListEntry.fromJsonObject(credWithCredStatus.vcDataModel.credentialStatus!!.toJson()) + val credStatus: StatusList2021Entry = + StatusList2021Entry.fromJsonObject(credWithCredStatus.vcDataModel.credentialStatus!!.toJson()) assertEquals(credStatus.id.toString(), "cred-with-status-id") - assertEquals(credStatus.toMap()["type"], "BitstringStatusListEntry") + assertEquals(credStatus.toMap()["type"], "StatusList2021Entry" + + "") assertEquals(credStatus.statusPurpose.toString(), StatusPurpose.REVOCATION.toString().lowercase()) assertEquals(credStatus.statusListIndex, "123") assertEquals(credStatus.statusListCredential.toString(), "https://example.com/credentials/status/3") @@ -98,7 +99,7 @@ class StatusListCredentialTest { val issuerDid = DidKey.create(keyManager) val holderDid = DidKey.create(keyManager) - val credentialStatus1 = BitstringStatusListEntry.Builder() + val credentialStatus1 = StatusList2021Entry.Builder() .id(URI.create("cred-with-status-id")) .statusPurpose("revocation") .statusListIndex("123") @@ -113,7 +114,7 @@ class StatusListCredentialTest { credentialStatus = credentialStatus1 ) - val credentialStatus2 = BitstringStatusListEntry.Builder() + val credentialStatus2 = StatusList2021Entry.Builder() .id(URI.create("cred-with-status-id")) .statusPurpose("revocation") .statusListIndex("124") @@ -148,13 +149,13 @@ class StatusListCredentialTest { statusListCredential.vcDataModel.type.containsAll( listOf( "VerifiableCredential", - "BitstringStatusListCredential" + "StatusList2021" ) ) ) assertEquals(statusListCredential.subject, "revocation-id") assertEquals(statusListCredential.vcDataModel.credentialSubject - .additionalClaims["type"], "BitstringStatusListEntry") + .additionalClaims["type"], "StatusList2021Entry") assertEquals( "revocation", statusListCredential.vcDataModel.credentialSubject.additionalClaims["statusPurpose"] as? String? @@ -174,7 +175,7 @@ class StatusListCredentialTest { val issuerDid = DidKey.create(keyManager) val holderDid = DidKey.create(keyManager) - val credentialStatus1 = BitstringStatusListEntry.Builder() + val credentialStatus1 = StatusList2021Entry.Builder() .id(URI.create("cred-with-status-id")) .statusPurpose("revocation") .statusListIndex("123") @@ -189,7 +190,7 @@ class StatusListCredentialTest { credentialStatus = credentialStatus1 ) - val credentialStatus2 = BitstringStatusListEntry.Builder() + val credentialStatus2 = StatusList2021Entry.Builder() .id(URI.create("cred-with-status-id")) .statusPurpose("revocation") .statusListIndex("123") @@ -225,7 +226,7 @@ class StatusListCredentialTest { val issuerDid = DidKey.create(keyManager) val holderDid = DidKey.create(keyManager) - val credentialStatus1 = BitstringStatusListEntry.Builder() + val credentialStatus1 = StatusList2021Entry.Builder() .id(URI.create("cred-with-status-id")) .statusPurpose("revocation") .statusListIndex("-1") @@ -262,7 +263,7 @@ class StatusListCredentialTest { val issuerDid = DidKey.create(keyManager) val holderDid = DidKey.create(keyManager) - val credentialStatus1 = BitstringStatusListEntry.Builder() + val credentialStatus1 = StatusList2021Entry.Builder() .id(URI.create("cred-with-status-id")) .statusPurpose("revocation") .statusListIndex(Int.MAX_VALUE.toString()) @@ -298,7 +299,7 @@ class StatusListCredentialTest { val issuerDid = DidKey.create(keyManager) val holderDid = DidKey.create(keyManager) - val credentialStatus1 = BitstringStatusListEntry.Builder() + val credentialStatus1 = StatusList2021Entry.Builder() .id(URI.create("cred-with-status-id")) .statusPurpose("revocation") .statusListIndex("123") @@ -313,7 +314,7 @@ class StatusListCredentialTest { credentialStatus = credentialStatus1 ) - val credentialStatus2 = BitstringStatusListEntry.Builder() + val credentialStatus2 = StatusList2021Entry.Builder() .id(URI.create("cred-with-status-id")) .statusPurpose("revocation") .statusListIndex("124") @@ -329,7 +330,7 @@ class StatusListCredentialTest { credentialStatus2 ) - val credentialStatus3 = BitstringStatusListEntry.Builder() + val credentialStatus3 = StatusList2021Entry.Builder() .id(URI.create("cred-with-status-id")) .statusPurpose("revocation") .statusListIndex("125") @@ -368,7 +369,7 @@ class StatusListCredentialTest { val issuerDid = DidKey.create(keyManager) val holderDid = DidKey.create(keyManager) - val credentialStatus1 = BitstringStatusListEntry.Builder() + val credentialStatus1 = StatusList2021Entry.Builder() .id(URI.create("cred-with-status-id")) .statusPurpose("revocation") .statusListIndex("123") @@ -383,7 +384,7 @@ class StatusListCredentialTest { credentialStatus = credentialStatus1 ) - val credentialStatus2 = BitstringStatusListEntry.Builder() + val credentialStatus2 = StatusList2021Entry.Builder() .id(URI.create("cred-with-status-id")) .statusPurpose("revocation") .statusListIndex("124") @@ -438,7 +439,7 @@ class StatusListCredentialTest { val issuerDid = DidKey.create(keyManager) val holderDid = DidKey.create(keyManager) - val credentialStatus1 = BitstringStatusListEntry.Builder() + val credentialStatus1 = StatusList2021Entry.Builder() .id(URI.create("cred-with-status-id")) .statusPurpose("revocation") .statusListIndex("123") diff --git a/credentials/src/test/resources/revocable_vc.json b/credentials/src/test/resources/revocable_vc.json index 4c624b8bf..e03f8b99c 100644 --- a/credentials/src/test/resources/revocable_vc.json +++ b/credentials/src/test/resources/revocable_vc.json @@ -1,23 +1,23 @@ { - "@context": [ + "@context":[ "https://www.w3.org/2018/credentials/v1", - "https://www.w3.org/2018/credentials/examples/v1" + "https://w3id.org/vc/status-list/2021/v1" ], - "id": "https://example.com/credentials/23894672394", - "type": [ + "id":"https://example.com/credentials/23894672394", + "type":[ "VerifiableCredential" ], - "issuer": "did:example:12345", + "issuer":"did:example:12345", "issuanceDate": "2021-04-05T14:27:42Z", - "credentialStatus": { - "id": "https://example.com/credentials/status/3#94567", - "type": "BitStringStatusListEntry", - "statusPurpose": "revocation", - "statusListIndex": "94567", - "statusListCredential": "https://example.com/credentials/status/3" + "credentialStatus":{ + "id":"https://example.com/credentials/status/3#94567", + "type":"StatusList2021Entry", + "statusPurpose":"revocation", + "statusListIndex":"94567", + "statusListCredential":"https://example.com/credentials/status/3" }, - "credentialSubject": { - "id": "did:example:6789", - "type": "Person" + "credentialSubject":{ + "id":"did:example:6789", + "type":"Person" } } \ No newline at end of file From 14cd2ed3be4f56be2e48adf4f1a32152b99db6a8 Mon Sep 17 00:00:00 2001 From: Neal Date: Mon, 11 Mar 2024 13:44:13 -0500 Subject: [PATCH 10/10] updates to match spec --- .../web5/sdk/credentials/model/VcDataModel.kt | 54 +++++++-------- .../credentials/VerifiableCredentialTest.kt | 65 +++++++++++++------ 2 files changed, 69 insertions(+), 50 deletions(-) diff --git a/credentials/src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt b/credentials/src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt index eb2323596..fd7ee4f69 100644 --- a/credentials/src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt +++ b/credentials/src/main/kotlin/web5/sdk/credentials/model/VcDataModel.kt @@ -60,12 +60,14 @@ private class CredentialSubjectDeserializer : JsonDeserializer = mutableListOf() - private var type: MutableList = mutableListOf() + private lateinit var id: URI + private var context: List = listOf() + private var type: List = listOf() private lateinit var issuer: URI private lateinit var issuanceDate: Date private var expirationDate: Date? = null @@ -122,21 +124,21 @@ public class VcDataModel( * @param id The unique identifier URI of the credential. * @return Returns this builder to allow for chaining. */ - public fun id(id: URI?): Builder = apply { this.id = id } + public fun id(id: URI): Builder = apply { this.id = id } /** * Sets the context URIs for the [VcDataModel]. * @param context A list of context URIs. * @return Returns this builder to allow for chaining. */ - public fun context(context: MutableList): Builder = apply { this.context = context } + public fun context(context: List): Builder = apply { this.context = context } /** * Sets the type(s) for the [VcDataModel]. * @param type A list of types. * @return Returns this builder to allow for chaining. */ - public fun type(type: MutableList): Builder = apply { this.type = type } + public fun type(type: List): Builder = apply { this.type = type } /** * Sets the issuer URI for the [VcDataModel]. @@ -189,23 +191,17 @@ public class VcDataModel( * @throws IllegalStateException If the issuer or issuance date are not set, or other validation fails. */ public fun build(): VcDataModel { + + require(context.contains(URI.create(DEFAULT_VC_CONTEXT))) { "context must include at least: $DEFAULT_VC_CONTEXT" } + require(id.toString().isNotBlank()) { "ID URI cannot be blank" } + require(type.contains(DEFAULT_VC_TYPE)) { "type must include at least: $DEFAULT_VC_TYPE" } require(issuer.toString().isNotBlank()) { "Issuer URI cannot be blank" } - require(id == null || id.toString().isNotBlank()) { "ID URI cannot be blank" } if (expirationDate != null) { require(issuanceDate.before(expirationDate)) { "Issuance date must be before expiration date" } } - // Default context and type handling - if (context.isEmpty() || context[0].toString() != DEFAULT_VC_CONTEXT) { - context.add(0, URI.create(DEFAULT_VC_CONTEXT)) - } - - if (type.isEmpty() || type[0] != DEFAULT_VC_TYPE) { - type.add(0, DEFAULT_VC_TYPE) - } - - return VcDataModel(id, context.toList(), type.toList(), issuer, issuanceDate, expirationDate, + return VcDataModel(id, context, type, issuer, issuanceDate, expirationDate, credentialSubject, credentialSchema, credentialStatus) } } @@ -263,14 +259,14 @@ public class VcDataModel( * properties related to the subject of the verifiable credential. */ public class CredentialSubject( - public val id: URI?, + public val id: URI, public val additionalClaims: Map ) { /** * Builder class for creating [CredentialSubject] instances. */ public class Builder { - private var id: URI? = null + private lateinit var id: URI private var additionalClaims: Map = emptyMap() /** @@ -278,7 +274,7 @@ public class CredentialSubject( * @param id The unique identifier URI of the credential subject. * @return Returns this builder to allow for chaining. */ - public fun id(id: URI?): Builder = apply { this.id = id } + public fun id(id: URI): Builder = apply { this.id = id } /** * Sets the additional claims for the credential subject. @@ -295,7 +291,7 @@ public class CredentialSubject( * @throws IllegalStateException If the ID URI is not valid. */ public fun build(): CredentialSubject { - require(id == null || id.toString().isNotBlank()) { "ID URI cannot be blank" } + require(id.toString().isNotBlank()) { "ID URI cannot be blank" } return CredentialSubject(id, additionalClaims) } @@ -308,14 +304,14 @@ public class CredentialSubject( */ public class CredentialSchema( public val id: String, - public val type: String? + public val type: String ) { /** * Builder class for creating [CredentialSchema] instances. */ public class Builder { - private var id: String? = null - private var type: String? = null + private lateinit var id: String + private lateinit var type: String /** * Sets the ID for the credential schema. @@ -329,7 +325,7 @@ public class CredentialSchema( * @param type The type of the credential schema. * @return Returns this builder to allow for chaining. */ - public fun type(type: String?): Builder = apply { this.type = type } + public fun type(type: String): Builder = apply { this.type = type } /** * Builds and returns the [CredentialSchema] object. @@ -337,10 +333,10 @@ public class CredentialSchema( * @throws IllegalStateException If the id is not set. */ public fun build(): CredentialSchema { - require(!id.isNullOrBlank()) { "ID cannot be blank" } - require(type == null || type!!.isNotBlank()) { "Type cannot be blank" } + require(id.toString().isNotBlank()) { "ID cannot be blank" } + require(type == "JsonSchema") { "Type must be: JsonSchema" } - return CredentialSchema(id!!, type) + return CredentialSchema(id, type) } } } diff --git a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt index 2640fe666..f91b99f1d 100644 --- a/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt +++ b/credentials/src/test/kotlin/web5/sdk/credentials/VerifiableCredentialTest.kt @@ -299,24 +299,49 @@ class VerifiableCredentialTest { } @Test - fun `vcDataModel should add default context and type`() { - - val vcDataModel = VcDataModel.Builder() - .id(URI.create("123")) - .context(mutableListOf()) // Assuming default context is handled by the Builder if necessary - .type(mutableListOf()) // Assuming default type is handled by the Builder if necessary - .issuer(URI.create("http://example.com/issuer")) - .issuanceDate(Date()) - .credentialSubject( - CredentialSubject.Builder() - .id(URI.create("http://example.com/subject")) - .additionalClaims(mapOf("claimKey" to "claimValue")) + fun `vcDataModel should add default context`() { + + val exception = + assertThrows(IllegalArgumentException::class.java) { + VcDataModel.Builder() + .id(URI.create("123")) + .context(mutableListOf()) + .type(mutableListOf()) + .issuer(URI.create("http://example.com/issuer")) + .issuanceDate(Date()) + .credentialSubject( + CredentialSubject.Builder() + .id(URI.create("http://example.com/subject")) + .additionalClaims(mapOf("claimKey" to "claimValue")) + .build() + ) .build() - ) - .build() + } + + assertTrue(exception.message!!.contains("context must include at least: https://www.w3.org/2018/credentials/v1" )) + } + + @Test + fun `vcDataModel should add default type`() { + + val exception = + assertThrows(IllegalArgumentException::class.java) { + VcDataModel.Builder() + .id(URI.create("123")) + .context(mutableListOf(URI.create("https://www.w3.org/2018/credentials/v1"))) + .type(mutableListOf()) + .issuer(URI.create("http://example.com/issuer")) + .issuanceDate(Date()) + .credentialSubject( + CredentialSubject.Builder() + .id(URI.create("http://example.com/subject")) + .additionalClaims(mapOf("claimKey" to "claimValue")) + .build() + ) + .build() + } - assertEquals("https://www.w3.org/2018/credentials/v1", vcDataModel.context[0].toString()) - assertEquals("VerifiableCredential", vcDataModel.type[0]) + assertTrue(exception.message!!.contains("type must include at least: VerifiableCredential" )) } @Test @@ -350,15 +375,15 @@ class VerifiableCredentialTest { } @Test - fun `vcDataModel credentialSchema should fail with empty type`() { + fun `vcDataModel credentialSchema should fail with wrong type`() { val exception = assertThrows(IllegalArgumentException::class.java) { CredentialSchema.Builder() .id("did:example:123") - .type("") + .type("otherType") .build() } - assertTrue(exception.message!!.contains("Type cannot be blank")) + assertTrue(exception.message!!.contains("Type must be: JsonSchema")) } } @@ -382,7 +407,6 @@ class Web5TestVectorsCredentials { val testVectors = mapper.readValue(File("../web5-spec/test-vectors/credentials/create.json"), typeRef) testVectors.vectors.filterNot { it.errors ?: false }.forEach { vector -> - println(vector.description) val vc = VerifiableCredential.fromJson(mapper.writeValueAsString(vector.input.credential)) val keyManager = InMemoryKeyManager() @@ -409,7 +433,6 @@ class Web5TestVectorsCredentials { val testVectors = mapper.readValue(File("../web5-spec/test-vectors/credentials/verify.json"), typeRef) testVectors.vectors.filterNot { it.errors ?: false }.forEach { vector -> - println(vector.description) assertDoesNotThrow { VerifiableCredential.verify(vector.input.vcJwt) }