From ca9f0d53f39bed7e17a87eee9fb0046a89503489 Mon Sep 17 00:00:00 2001 From: mikeplotean Date: Fri, 10 Nov 2023 19:44:14 +0200 Subject: [PATCH] refactor: credential status --- service-matrix.properties | 3 + .../CredentialPropertyVerificationPolicies.kt | 4 +- src/main/kotlin/id/walt/cli/VcCommand.kt | 6 +- src/main/kotlin/id/walt/common/CommonUtils.kt | 5 +- .../w3c/builder/W3CCredentialBuilder.kt | 39 +++---- .../services/vc/JsonLdCredentialService.kt | 2 +- .../vc/WaltIdJsonLdCredentialService.kt | 2 +- .../signatory/rest/SignatoryController.kt | 8 +- ...ce.kt => CredentialStatusClientService.kt} | 5 +- .../revocation/CredentialStatusFactory.kt | 53 ++------- .../SimpleCredentialClientService.kt | 8 +- .../StatusList2021EntryClientService.kt | 110 +++++++++++++----- .../StatusListCredentialStorageService.kt | 88 -------------- .../statuslist2021/StatusListIndexService.kt | 57 --------- .../index/IncrementalIndexingStrategy.kt | 10 ++ .../statuslist2021/index/IndexingStrategy.kt | 16 +++ .../index/RandomIndexingStrategy.kt | 18 +++ .../index/StatusListIndexService.kt | 16 +++ .../index/WaltIdStatusListIndexService.kt | 40 +++++++ .../StatusListCredentialStorageService.kt | 17 +++ ...altIdStatusListCredentialStorageService.kt | 30 +++++ 21 files changed, 283 insertions(+), 254 deletions(-) rename src/main/kotlin/id/walt/signatory/revocation/{RevocationService.kt => CredentialStatusClientService.kt} (95%) delete mode 100644 src/main/kotlin/id/walt/signatory/revocation/statuslist2021/StatusListCredentialStorageService.kt delete mode 100644 src/main/kotlin/id/walt/signatory/revocation/statuslist2021/StatusListIndexService.kt create mode 100644 src/main/kotlin/id/walt/signatory/revocation/statuslist2021/index/IncrementalIndexingStrategy.kt create mode 100644 src/main/kotlin/id/walt/signatory/revocation/statuslist2021/index/IndexingStrategy.kt create mode 100644 src/main/kotlin/id/walt/signatory/revocation/statuslist2021/index/RandomIndexingStrategy.kt create mode 100644 src/main/kotlin/id/walt/signatory/revocation/statuslist2021/index/StatusListIndexService.kt create mode 100644 src/main/kotlin/id/walt/signatory/revocation/statuslist2021/index/WaltIdStatusListIndexService.kt create mode 100644 src/main/kotlin/id/walt/signatory/revocation/statuslist2021/storage/StatusListCredentialStorageService.kt create mode 100644 src/main/kotlin/id/walt/signatory/revocation/statuslist2021/storage/WaltIdStatusListCredentialStorageService.kt diff --git a/service-matrix.properties b/service-matrix.properties index 5af5d726..8818e540 100644 --- a/service-matrix.properties +++ b/service-matrix.properties @@ -14,3 +14,6 @@ id.walt.signatory.Signatory=id.walt.signatory.WaltIdSignatory:config/signatory.c id.walt.custodian.Custodian=id.walt.custodian.WaltIdCustodian id.walt.auditor.Auditor=id.walt.auditor.WaltIdAuditor id.walt.services.ecosystems.gaiax.GaiaxService=id.walt.services.ecosystems.gaiax.WaltIdGaiaxService +id.walt.signatory.revocation.statuslist2021.storage.StatusListCredentialStorageService=id.walt.signatory.revocation.statuslist2021.storage.WaltIdStatusListCredentialStorageService +id.walt.signatory.revocation.statuslist2021.index.IndexingStrategy=id.walt.signatory.revocation.statuslist2021.index.IncrementalIndexingStrategy +id.walt.signatory.revocation.statuslist2021.index.StatusListIndexService=id.walt.signatory.revocation.statuslist2021.index.WaltIdStatusListIndexService diff --git a/src/main/kotlin/id/walt/auditor/policies/CredentialPropertyVerificationPolicies.kt b/src/main/kotlin/id/walt/auditor/policies/CredentialPropertyVerificationPolicies.kt index ed1d7bb9..f66a5564 100644 --- a/src/main/kotlin/id/walt/auditor/policies/CredentialPropertyVerificationPolicies.kt +++ b/src/main/kotlin/id/walt/auditor/policies/CredentialPropertyVerificationPolicies.kt @@ -8,7 +8,7 @@ import id.walt.credentials.w3c.VerifiableCredential import id.walt.credentials.w3c.VerifiablePresentation import id.walt.model.credential.status.CredentialStatus import id.walt.signatory.revocation.CredentialStatusCredential -import id.walt.signatory.revocation.RevocationClientService +import id.walt.signatory.revocation.CredentialStatusClientService import id.walt.signatory.revocation.RevocationStatus import id.walt.signatory.revocation.TokenRevocationStatus import java.text.SimpleDateFormat @@ -53,7 +53,7 @@ class CredentialStatusPolicy : SimpleVerificationPolicy() { override fun doVerify(vc: VerifiableCredential): VerificationPolicyResult { val maybeCredentialStatus = Klaxon().parse(vc.toJson())!!.credentialStatus return maybeCredentialStatus?.let { cs -> runCatching { - RevocationClientService.check(vc).let { + CredentialStatusClientService.check(vc).let { if (!it.isRevoked) VerificationPolicyResult.success() else failResult(it, cs) } diff --git a/src/main/kotlin/id/walt/cli/VcCommand.kt b/src/main/kotlin/id/walt/cli/VcCommand.kt index 41a0d819..006f5237 100644 --- a/src/main/kotlin/id/walt/cli/VcCommand.kt +++ b/src/main/kotlin/id/walt/cli/VcCommand.kt @@ -32,7 +32,7 @@ import id.walt.signatory.ProofConfig import id.walt.signatory.ProofType import id.walt.signatory.Signatory import id.walt.signatory.dataproviders.CLIDataProvider -import id.walt.signatory.revocation.RevocationClientService +import id.walt.signatory.revocation.CredentialStatusClientService import io.ktor.util.date.* import io.ktor.util.reflect.* import mu.KotlinLogging @@ -561,7 +561,7 @@ class VcRevocationCheckCommand : CliktCommand( override fun run() = vcFile.takeIf { it.exists() }?.run { println("Checking revocation status for credential stored at: ${vcFile.absolutePath}") runWithErrorHandling( - runner = { RevocationClientService.check(this.readText().toVerifiableCredential()) }, + runner = { CredentialStatusClientService.check(this.readText().toVerifiableCredential()) }, onSuccess = { println("Revocation status:") println(Klaxon().toJsonString(it).prettyPrint()) @@ -577,7 +577,7 @@ class VcRevocationRevokeCommand: CliktCommand( override fun run() = vcFile.takeIf { it.exists() }?.run { println("Revoking credential stored at: ${vcFile.absolutePath}") runWithErrorHandling( - runner = { RevocationClientService.revoke(this.readText().toVerifiableCredential()) }, + runner = { CredentialStatusClientService.revoke(this.readText().toVerifiableCredential()) }, onSuccess = { println("Revocation result:") println(Klaxon().toJsonString(it).prettyPrint()) diff --git a/src/main/kotlin/id/walt/common/CommonUtils.kt b/src/main/kotlin/id/walt/common/CommonUtils.kt index 8dc096b4..2416e4ed 100644 --- a/src/main/kotlin/id/walt/common/CommonUtils.kt +++ b/src/main/kotlin/id/walt/common/CommonUtils.kt @@ -88,7 +88,10 @@ fun buildRawBitString(bitSet: BitSet): ByteArray{ return builder.toString().toByteArray() } -fun createEncodedBitString(bitSet: BitSet): ByteArray = Base64.getEncoder().encode(compressGzip(buildRawBitString(bitSet))) +fun createEncodedBitString(bitSet: BitSet = BitSet(16 * 1024 * 8)): ByteArray = + Base64.getEncoder().encode(compressGzip(buildRawBitString(bitSet))) + +fun decodeBitSet(bitString: String): BitSet = uncompressGzip(Base64.getDecoder().decode(bitString)).toBitSet(16 * 1024 * 8) fun createBaseToken() = UUID.randomUUID().toString() + UUID.randomUUID().toString() fun deriveRevocationToken(baseToken: String): String = Base32.toBase32String(DigestUtils.sha256(baseToken)).replace("=", "") diff --git a/src/main/kotlin/id/walt/credentials/w3c/builder/W3CCredentialBuilder.kt b/src/main/kotlin/id/walt/credentials/w3c/builder/W3CCredentialBuilder.kt index c0e3d521..e61acb71 100644 --- a/src/main/kotlin/id/walt/credentials/w3c/builder/W3CCredentialBuilder.kt +++ b/src/main/kotlin/id/walt/credentials/w3c/builder/W3CCredentialBuilder.kt @@ -7,12 +7,9 @@ import id.walt.model.credential.status.CredentialStatus import id.walt.signatory.ProofConfig import id.walt.signatory.Signatory import id.walt.signatory.SignatoryConfig -import id.walt.signatory.revocation.SimpleCredentialStatusFactory +import id.walt.signatory.revocation.CredentialStatusFactory import id.walt.signatory.revocation.SimpleStatusFactoryParameter -import id.walt.signatory.revocation.StatusListEntryFactory import id.walt.signatory.revocation.StatusListEntryFactoryParameter -import id.walt.signatory.revocation.statuslist2021.StatusListCredentialStorageService -import id.walt.signatory.revocation.statuslist2021.StatusListIndexService import io.ktor.http.* import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject @@ -24,13 +21,8 @@ import java.time.format.DateTimeFormatterBuilder class W3CCredentialBuilderWithCredentialStatus>( private val builder: AbstractW3CCredentialBuilder, private val proofConfig: ProofConfig, -):AbstractW3CCredentialBuilder(builder.type, VerifiableCredential){ +) : AbstractW3CCredentialBuilder(builder.type, VerifiableCredential) { - private val statusListEntryFactory = StatusListEntryFactory( - StatusListIndexService.getService(), - StatusListCredentialStorageService.getService(), - ) - private val simpleStatusFactory = SimpleCredentialStatusFactory() private val signatoryConfig = Signatory.getService().configuration as? SignatoryConfig override fun build(): C = builder.apply { @@ -43,18 +35,21 @@ class W3CCredentialBuilderWithCredentialStatus simpleStatusFactory.create(SimpleStatusFactoryParameter( - id = URLBuilder().takeFrom(credentialUrl).appendPathSegments("token", createBaseToken()).buildString(), - )).asMap() - CredentialStatus.Types.StatusList2021Entry -> statusListEntryFactory.create(StatusListEntryFactoryParameter( - purpose = purpose, - credentialUrl = credentialUrl, - issuer = issuer, - )).asMap() - }.takeIf { - it.isNotEmpty() - } + private fun getStatusProperty(issuer: String, type: CredentialStatus.Types, purpose: String, credentialUrl: String) = + when (type) { + CredentialStatus.Types.SimpleCredentialStatus2022 -> SimpleStatusFactoryParameter( + id = URLBuilder().takeFrom(credentialUrl).appendPathSegments("token", createBaseToken()).buildString(), + ) + CredentialStatus.Types.StatusList2021Entry -> StatusListEntryFactoryParameter( + purpose = purpose, + credentialUrl = credentialUrl, + issuer = issuer, + ) + }.let { + CredentialStatusFactory.create(it).asMap() + }.takeIf { + it.isNotEmpty() + } } class W3CCredentialBuilder(type: List = listOf("VerifiableCredential")) : diff --git a/src/main/kotlin/id/walt/services/vc/JsonLdCredentialService.kt b/src/main/kotlin/id/walt/services/vc/JsonLdCredentialService.kt index 01ddafa9..b0665b42 100644 --- a/src/main/kotlin/id/walt/services/vc/JsonLdCredentialService.kt +++ b/src/main/kotlin/id/walt/services/vc/JsonLdCredentialService.kt @@ -19,7 +19,7 @@ enum class VerificationType { @Serializable data class VerificationResult(val verified: Boolean, val verificationType: VerificationType) -abstract class JsonLdCredentialService() : WaltIdService() { +abstract class JsonLdCredentialService : WaltIdService() { override val implementation get() = serviceImplementation() open fun sign(jsonCred: String, config: ProofConfig): String = implementation.sign(jsonCred, config) diff --git a/src/main/kotlin/id/walt/services/vc/WaltIdJsonLdCredentialService.kt b/src/main/kotlin/id/walt/services/vc/WaltIdJsonLdCredentialService.kt index 51315e4a..fd9b7205 100644 --- a/src/main/kotlin/id/walt/services/vc/WaltIdJsonLdCredentialService.kt +++ b/src/main/kotlin/id/walt/services/vc/WaltIdJsonLdCredentialService.kt @@ -31,7 +31,7 @@ import java.util.* private val log = KotlinLogging.logger {} -open class WaltIdJsonLdCredentialService() : JsonLdCredentialService() { +open class WaltIdJsonLdCredentialService : JsonLdCredentialService() { private val keyStore: KeyStoreService get() = ContextManager.keyStore private val documentLoaderService: JsonLdDocumentLoaderService get() = JsonLdDocumentLoaderService.getService() diff --git a/src/main/kotlin/id/walt/signatory/rest/SignatoryController.kt b/src/main/kotlin/id/walt/signatory/rest/SignatoryController.kt index 7ace6901..7a3a8e7b 100644 --- a/src/main/kotlin/id/walt/signatory/rest/SignatoryController.kt +++ b/src/main/kotlin/id/walt/signatory/rest/SignatoryController.kt @@ -12,11 +12,11 @@ import id.walt.signatory.ProofConfig import id.walt.signatory.ProofType import id.walt.signatory.Signatory import id.walt.signatory.dataproviders.MergingDataProvider -import id.walt.signatory.revocation.RevocationClientService +import id.walt.signatory.revocation.CredentialStatusClientService import id.walt.signatory.revocation.RevocationStatus import id.walt.signatory.revocation.TokenRevocationStatus import id.walt.signatory.revocation.simplestatus2022.SimpleCredentialStatus2022StorageService -import id.walt.signatory.revocation.statuslist2021.StatusListCredentialStorageService +import id.walt.signatory.revocation.statuslist2021.storage.StatusListCredentialStorageService import io.javalin.http.BadRequestResponse import io.javalin.http.ContentType import io.javalin.http.Context @@ -178,7 +178,7 @@ object SignatoryController { }.json("200") fun checkRevoked(ctx: Context) = runCatching { - RevocationClientService.check(ctx.body().toVerifiableCredential()) + CredentialStatusClientService.check(ctx.body().toVerifiableCredential()) }.onSuccess { ctx.json(it) }.onFailure { @@ -193,7 +193,7 @@ object SignatoryController { }.json("201") fun revoke(ctx: Context) = runCatching { - RevocationClientService.revoke(ctx.body().toVerifiableCredential()) + CredentialStatusClientService.revoke(ctx.body().toVerifiableCredential()) }.onSuccess { ctx.status(if (it.succeed) HttpCode.OK else HttpCode.NOT_FOUND).json(it.message) }.onFailure { ctx.status(HttpCode.NOT_FOUND).json(it.localizedMessage) } diff --git a/src/main/kotlin/id/walt/signatory/revocation/RevocationService.kt b/src/main/kotlin/id/walt/signatory/revocation/CredentialStatusClientService.kt similarity index 95% rename from src/main/kotlin/id/walt/signatory/revocation/RevocationService.kt rename to src/main/kotlin/id/walt/signatory/revocation/CredentialStatusClientService.kt index c07cc097..37837290 100644 --- a/src/main/kotlin/id/walt/signatory/revocation/RevocationService.kt +++ b/src/main/kotlin/id/walt/signatory/revocation/CredentialStatusClientService.kt @@ -10,9 +10,10 @@ import id.walt.signatory.revocation.simplestatus2022.SimpleCredentialClientServi import id.walt.signatory.revocation.statuslist2021.StatusList2021EntryClientService import kotlinx.serialization.Serializable -interface RevocationClientService { +interface CredentialStatusClientService { fun checkRevocation(parameter: RevocationCheckParameter): RevocationStatus fun revoke(parameter: RevocationConfig) + fun create(parameter: CredentialStatusFactoryParameter): CredentialStatus companion object { fun revoke(vc: VerifiableCredential): RevocationResult = @@ -39,7 +40,7 @@ interface RevocationClientService { is StatusList2021EntryCredentialStatus -> StatusListRevocationCheckParameter(credentialStatus = credentialStatus) } - private fun getClient(credentialStatus: CredentialStatus): RevocationClientService = when (credentialStatus) { + private fun getClient(credentialStatus: CredentialStatus): CredentialStatusClientService = when (credentialStatus) { is SimpleCredentialStatus2022 -> SimpleCredentialClientService() is StatusList2021EntryCredentialStatus -> StatusList2021EntryClientService() } diff --git a/src/main/kotlin/id/walt/signatory/revocation/CredentialStatusFactory.kt b/src/main/kotlin/id/walt/signatory/revocation/CredentialStatusFactory.kt index a65b9d74..1e52f674 100644 --- a/src/main/kotlin/id/walt/signatory/revocation/CredentialStatusFactory.kt +++ b/src/main/kotlin/id/walt/signatory/revocation/CredentialStatusFactory.kt @@ -1,51 +1,16 @@ package id.walt.signatory.revocation -import id.walt.common.createEncodedBitString import id.walt.model.credential.status.CredentialStatus -import id.walt.model.credential.status.SimpleCredentialStatus2022 -import id.walt.model.credential.status.StatusList2021EntryCredentialStatus -import id.walt.signatory.revocation.statuslist2021.StatusListCredentialStorageService -import id.walt.signatory.revocation.statuslist2021.StatusListIndex -import id.walt.signatory.revocation.statuslist2021.StatusListIndexService -import java.util.* +import id.walt.signatory.revocation.simplestatus2022.SimpleCredentialClientService +import id.walt.signatory.revocation.statuslist2021.StatusList2021EntryClientService -interface CredentialStatusFactory { - fun create(parameter: CredentialStatusFactoryParameter): CredentialStatus -} - -class SimpleCredentialStatusFactory : CredentialStatusFactory { - override fun create(parameter: CredentialStatusFactoryParameter) = SimpleCredentialStatus2022( - id = (parameter as? SimpleStatusFactoryParameter)?.id ?: "" - ) -} - -class StatusListEntryFactory( - private val indexService: StatusListIndexService, - private val storageService: StatusListCredentialStorageService, -) : CredentialStatusFactory { - override fun create(parameter: CredentialStatusFactoryParameter) = let { - indexService.read() ?: indexService.create() - }.let { - val statusParameter = parameter as StatusListEntryFactoryParameter - // update index - indexService.update(StatusListIndex( - index = ((it.index.toIntOrNull() ?: 0) + 1).toString() - )) - // verify status-credential exists and create one - storageService.fetch(statusParameter.credentialUrl) ?: run { - storageService.store( - statusParameter.issuer, - statusParameter.credentialUrl, - statusParameter.purpose, - String(createEncodedBitString(BitSet(16 * 1024 * 8))) - ) - } - StatusList2021EntryCredentialStatus( - id = statusParameter.credentialUrl + "#${it.index}", - statusPurpose = statusParameter.purpose, - statusListIndex = it.index, - statusListCredential = statusParameter.credentialUrl, - ) +object CredentialStatusFactory { + private val simpleStatus = SimpleCredentialClientService() + private val statusList2021 = StatusList2021EntryClientService() + fun create(parameter: CredentialStatusFactoryParameter): CredentialStatus = when (parameter) { + is SimpleStatusFactoryParameter -> simpleStatus.create(parameter) + is StatusListEntryFactoryParameter -> statusList2021.create(parameter) + else -> throw IllegalArgumentException("Status type not supported: ${parameter.javaClass.simpleName}") } } diff --git a/src/main/kotlin/id/walt/signatory/revocation/simplestatus2022/SimpleCredentialClientService.kt b/src/main/kotlin/id/walt/signatory/revocation/simplestatus2022/SimpleCredentialClientService.kt index 76526589..14158edd 100644 --- a/src/main/kotlin/id/walt/signatory/revocation/simplestatus2022/SimpleCredentialClientService.kt +++ b/src/main/kotlin/id/walt/signatory/revocation/simplestatus2022/SimpleCredentialClientService.kt @@ -1,5 +1,7 @@ package id.walt.signatory.revocation.simplestatus2022 +import id.walt.model.credential.status.CredentialStatus +import id.walt.model.credential.status.SimpleCredentialStatus2022 import id.walt.services.WaltIdServices import id.walt.signatory.revocation.* import io.ktor.client.* @@ -13,7 +15,7 @@ import io.ktor.serialization.kotlinx.json.* import kotlinx.coroutines.runBlocking import mu.KotlinLogging -class SimpleCredentialClientService: RevocationClientService { +class SimpleCredentialClientService: CredentialStatusClientService { private val logger = KotlinLogging.logger("WaltIdRevocationClientService") private val credentialStorage = SimpleCredentialStatus2022StorageService @@ -50,4 +52,8 @@ class SimpleCredentialClientService: RevocationClientService { logger.debug { "Revoking at $baseTokenUrl" } credentialStorage.revokeToken(baseToken) } + + override fun create(parameter: CredentialStatusFactoryParameter): CredentialStatus = SimpleCredentialStatus2022( + id = (parameter as? SimpleStatusFactoryParameter)?.id ?: "" + ) } diff --git a/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/StatusList2021EntryClientService.kt b/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/StatusList2021EntryClientService.kt index 971e719a..5f1785de 100644 --- a/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/StatusList2021EntryClientService.kt +++ b/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/StatusList2021EntryClientService.kt @@ -2,19 +2,30 @@ package id.walt.signatory.revocation.statuslist2021 import com.beust.klaxon.Json import id.walt.common.createEncodedBitString -import id.walt.common.resolveContent -import id.walt.common.toBitSet +import id.walt.common.decodeBitSet import id.walt.common.uncompressGzip import id.walt.credentials.w3c.VerifiableCredential +import id.walt.credentials.w3c.W3CCredentialSubject +import id.walt.credentials.w3c.builder.W3CCredentialBuilder +import id.walt.credentials.w3c.templates.VcTemplateService import id.walt.credentials.w3c.toVerifiableCredential import id.walt.crypto.decBase64 +import id.walt.model.credential.status.StatusList2021EntryCredentialStatus +import id.walt.signatory.ProofConfig +import id.walt.signatory.ProofType +import id.walt.signatory.Signatory import id.walt.signatory.revocation.* +import id.walt.signatory.revocation.statuslist2021.index.StatusListIndexService +import id.walt.signatory.revocation.statuslist2021.storage.StatusListCredentialStorageService import kotlinx.serialization.Serializable -import java.util.* -class StatusList2021EntryClientService: RevocationClientService { +class StatusList2021EntryClientService: CredentialStatusClientService { - private val credentialStorage = StatusListCredentialStorageService.getService() + private val storageService = StatusListCredentialStorageService.getService() + private val indexingService = StatusListIndexService.getService() + private val templateId = "StatusList2021Credential" + private val signatoryService = Signatory.getService() + private val templateService = VcTemplateService.getService() override fun checkRevocation(parameter: RevocationCheckParameter): RevocationStatus = let { val credentialStatus = (parameter as StatusListRevocationCheckParameter).credentialStatus @@ -26,36 +37,74 @@ class StatusList2021EntryClientService: RevocationClientService { StatusListRevocationStatus(verifyBitStringStatus(credentialIndex, credentialSubject.encodedList)) } - override fun revoke(parameter: RevocationConfig) { - val configParam = parameter as StatusListRevocationConfig - // credential - val credential = resolveContent(configParam.credentialStatus.statusListCredential).toVerifiableCredential() - // check if credential with id exists - val statusCredential = credentialStorage.fetch(credential.id ?: "") - ?: throw IllegalArgumentException("No status credential found for the provided id: ${credential.id}") - updateStatusCredentialRecord(statusCredential, configParam.credentialStatus.statusListIndex) + override fun revoke(parameter: RevocationConfig): Unit = (parameter as StatusListRevocationConfig).run { + storageService.fetch(this.credentialStatus.statusListCredential)?.let { + extractStatusListCredentialSubject(it) + }?.let { + updateBitString(it.encodedList, this.credentialStatus.statusListIndex, 1) + } } - private fun updateStatusCredentialRecord(statusCredential: VerifiableCredential, index: String){ - val credentialSubject = extractStatusListCredentialSubject(statusCredential)!! + override fun create(parameter: CredentialStatusFactoryParameter): StatusList2021EntryCredentialStatus = + (parameter as StatusListEntryFactoryParameter).let { + val idx = indexingService.index(parameter.credentialUrl) + StatusList2021EntryCredentialStatus( + id = "${parameter.credentialUrl}#$idx", + statusPurpose = parameter.purpose, + statusListIndex = idx, + statusListCredential = parameter.credentialUrl + ) + }.let { + val bitString = storageService.fetch(parameter.credentialUrl)?.let { + extractStatusListCredentialSubject(it) + }?.encodedList ?: String(createEncodedBitString()) + val credential = issue( + it.id, + it.statusPurpose, + it.statusListCredential, + parameter.issuer, + updateBitString(bitString, it.statusListIndex, 0) + ) + // create / update the status list credential + storageService.store(credential, parameter.credentialUrl) + it + } + + + private fun issue(id: String, purpose: String, url: String, issuer: String, bitString: String) = W3CCredentialSubject( + id, mapOf("type" to "StatusList2021Credential", "statusPurpose" to purpose, "encodedList" to bitString) + ).let { + W3CCredentialBuilder.fromPartial(templateService.getTemplate(templateId).template!!).apply { + setId(it.id ?: url) + buildSubject { + setFromJson(it.toJson()) + } + } + }.let { + signatoryService.issue( + credentialBuilder = it, config = ProofConfig( + credentialId = url, + issuerDid = issuer, + subjectDid = issuer, + proofType = ProofType.LD_PROOF, + ) + ).toVerifiableCredential() + } + + private fun updateBitString(encodedBitString: String, index: String, value: Int) = let { // get credential index - val idx = index.toIntOrNull()?: throw IllegalArgumentException("Couldn't parse credential index") + val idx = index.toIntOrNull() ?: throw IllegalArgumentException("Couldn't parse credential index") // get bitString - val bitString = uncompressGzip(Base64.getDecoder().decode(credentialSubject.encodedList)) - val bitSet = bitString.toBitSet(16 * 1024 * 8) - // set the respective bit - bitSet.set(idx) - val encodedList = createEncodedBitString(bitSet) - // create / update the status list credential - statusCredential.issuerId?.let { - credentialStorage.store( - it, statusCredential.id!!, credentialSubject.statusPurpose, String(encodedList) - ) - } ?: throw IllegalArgumentException("Missing issuer for statusList credential.") + val bitSet = decodeBitSet(encodedBitString) + // update the respective bit + value.takeIf { it == 0 }?.let { + bitSet.clear(idx) + } ?: bitSet.set(idx) + String(createEncodedBitString(bitSet)) } private fun extractStatusListCredentialSubject(statusCredentialUrl: String): StatusListCredentialSubject? = - extractStatusListCredentialSubject(resolveContent(statusCredentialUrl).toVerifiableCredential()) + storageService.fetch(statusCredentialUrl)?.let { extractStatusListCredentialSubject(it) } private fun extractStatusListCredentialSubject(statusCredential: VerifiableCredential) = statusCredential.credentialSubject?.let { @@ -86,4 +135,9 @@ class StatusList2021EntryClientService: RevocationClientService { val statusPurpose: String, val encodedList: String, ) + + data class StatusListCreateData( + val statusEntry: StatusList2021EntryCredentialStatus, + val bitString: String, + ) } diff --git a/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/StatusListCredentialStorageService.kt b/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/StatusListCredentialStorageService.kt deleted file mode 100644 index ed8942ea..00000000 --- a/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/StatusListCredentialStorageService.kt +++ /dev/null @@ -1,88 +0,0 @@ -package id.walt.signatory.revocation.statuslist2021 - -import id.walt.common.resolveContent -import id.walt.credentials.w3c.VerifiableCredential -import id.walt.credentials.w3c.W3CCredentialSubject -import id.walt.credentials.w3c.builder.W3CCredentialBuilder -import id.walt.credentials.w3c.templates.VcTemplateService -import id.walt.credentials.w3c.toVerifiableCredential -import id.walt.servicematrix.ServiceProvider -import id.walt.services.WaltIdService -import id.walt.services.WaltIdServices -import id.walt.signatory.ProofConfig -import id.walt.signatory.ProofType -import id.walt.signatory.Signatory -import java.io.File -import java.net.URLEncoder -import java.nio.charset.StandardCharsets -import kotlin.io.path.Path -import kotlin.io.path.pathString - -open class StatusListCredentialStorageService : WaltIdService() { - override val implementation get() = serviceImplementation() - - open fun fetch(url: String): VerifiableCredential? = implementation.fetch(url) - open fun store(issuer: String, id: String, purpose: String, bitString: String): Unit = - implementation.store(issuer, id, purpose, bitString) - - companion object : ServiceProvider { - override fun getService() = object : StatusListCredentialStorageService() {} - override fun defaultImplementation() = WaltIdStatusListCredentialStorageService() - } -} - - -class WaltIdStatusListCredentialStorageService : StatusListCredentialStorageService() { - private val templateId = "StatusList2021Credential" - private val signatoryService = Signatory.getService() - private val templateService = VcTemplateService.getService() - - override fun fetch(url: String): VerifiableCredential? = let { - val path = getCredentialPath(url) - resolveContent(path).takeIf { it != path }?.let { - VerifiableCredential.fromJson(it) - } - } - - override fun store(issuer: String, id: String, purpose: String, bitString: String): Unit = let { - fetch(id)?.let { vc -> - // update vc - W3CCredentialSubject( - vc.id, mapOf( - "type" to vc.credentialSubject!!.properties["type"] as String, - "statusPurpose" to vc.credentialSubject!!.properties["statusPurpose"] as String, - "encodedList" to bitString - ) - ) - // new vc - } ?: W3CCredentialSubject( - id, mapOf( - "type" to "StatusList2021Credential", - "statusPurpose" to purpose, - "encodedList" to bitString - ) - ) - }.let { - W3CCredentialBuilder.fromPartial(templateService.getTemplate(templateId).template!!).apply { - setId(it.id ?: id) - buildSubject { - setFromJson(it.toJson()) - } - } - }.run { - val credential = signatoryService.issue( - credentialBuilder = this, config = ProofConfig( - credentialId = id, - issuerDid = issuer, - subjectDid = issuer, - proofType = ProofType.LD_PROOF, - ) - ).toVerifiableCredential() - getCredentialPath(credential.id!!).let { - File(it).writeText(credential.encode()) - } - } - - private fun getCredentialPath(name: String) = - Path(WaltIdServices.revocationDir, "${URLEncoder.encode(name, StandardCharsets.UTF_8)}.cred").pathString -} diff --git a/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/StatusListIndexService.kt b/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/StatusListIndexService.kt deleted file mode 100644 index cf79c625..00000000 --- a/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/StatusListIndexService.kt +++ /dev/null @@ -1,57 +0,0 @@ -package id.walt.signatory.revocation.statuslist2021 - -import com.beust.klaxon.Klaxon -import id.walt.servicematrix.ServiceProvider -import id.walt.services.WaltIdService -import id.walt.services.WaltIdServices -import kotlinx.serialization.Serializable -import java.io.File - - -open class StatusListIndexService : WaltIdService() { - override val implementation get() = serviceImplementation() - - open fun create(): StatusListIndex = implementation.create() - open fun read(): StatusListIndex? = implementation.read() - open fun update(index: StatusListIndex): Unit = implementation.update(index) - open fun delete(): Unit = implementation.delete() - - companion object : ServiceProvider { - override fun getService() = object : StatusListIndexService() {} - override fun defaultImplementation() = WaltIdStatusListIndexService() - } - -} - -@Serializable -data class StatusListIndex( - val index: String -) - -class WaltIdStatusListIndexService : StatusListIndexService() { - private val indexPath = "${WaltIdServices.revocationDir}/status-list-index.json" - - override fun create(): StatusListIndex = StatusListIndex(index = "0").let { - createAndUpdateIndex(it) - it - } - - override fun read(): StatusListIndex? = checkIndex()?.let { - Klaxon().parse(it.readText()) - } - - override fun update(index: StatusListIndex): Unit = createAndUpdateIndex(index) - - override fun delete(): Unit = run { checkIndex()?.delete() } - - private fun checkIndex() = File(indexPath).takeIf { it.exists() } - - private fun createAndUpdateIndex(index: StatusListIndex): Unit = run { - File(indexPath).takeIf { it.exists() } ?: let { - File(indexPath).createNewFile() - File(indexPath) - } - }.run { - this.writeText(Klaxon().toJsonString(index)) - } -} diff --git a/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/index/IncrementalIndexingStrategy.kt b/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/index/IncrementalIndexingStrategy.kt new file mode 100644 index 00000000..403a605f --- /dev/null +++ b/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/index/IncrementalIndexingStrategy.kt @@ -0,0 +1,10 @@ +package id.walt.signatory.revocation.statuslist2021.index + +class IncrementalIndexingStrategy : IndexingStrategy() { + + override fun next(bitset: Array): String = let { + bitset.maxOrNull()?.toLongOrNull()?.let { + it + 1 + } ?: 0 + }.toString() +} diff --git a/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/index/IndexingStrategy.kt b/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/index/IndexingStrategy.kt new file mode 100644 index 00000000..bbddfb5d --- /dev/null +++ b/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/index/IndexingStrategy.kt @@ -0,0 +1,16 @@ +package id.walt.signatory.revocation.statuslist2021.index + +import id.walt.servicematrix.ServiceProvider +import id.walt.servicematrix.ServiceRegistry +import id.walt.services.WaltIdService + +abstract class IndexingStrategy : WaltIdService() { + override val implementation: IndexingStrategy get() = serviceImplementation() + open fun next(bitset: Array): String = implementation.next(bitset) + + companion object : ServiceProvider { + override fun getService() = ServiceRegistry.getService(IndexingStrategy::class) + override fun defaultImplementation() = IncrementalIndexingStrategy() + + } +} diff --git a/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/index/RandomIndexingStrategy.kt b/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/index/RandomIndexingStrategy.kt new file mode 100644 index 00000000..52b159a7 --- /dev/null +++ b/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/index/RandomIndexingStrategy.kt @@ -0,0 +1,18 @@ +package id.walt.signatory.revocation.statuslist2021.index + +import kotlin.random.Random + +class RandomIndexingStrategy : IndexingStrategy() { + private val trialAttempts = 100 + override fun next(bitset: Array): String = + generate(bitset) ?: throw Exception("Couldn't find an empty bit, exhausted the given attempts limit: $trialAttempts") + + private fun generate(bitset: Array) = let { + var idx: String? = null + var attempts = 1 + do { + idx = Random.nextLong(from = 0, until = Long.MAX_VALUE).toString() + } while (bitset.contains(idx) && attempts++ <= trialAttempts) + idx + } +} diff --git a/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/index/StatusListIndexService.kt b/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/index/StatusListIndexService.kt new file mode 100644 index 00000000..99e6d431 --- /dev/null +++ b/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/index/StatusListIndexService.kt @@ -0,0 +1,16 @@ +package id.walt.signatory.revocation.statuslist2021.index + +import id.walt.servicematrix.ServiceProvider +import id.walt.servicematrix.ServiceRegistry +import id.walt.services.WaltIdService + +abstract class StatusListIndexService : WaltIdService() { + override val implementation: StatusListIndexService get() = serviceImplementation() + abstract fun index(url: String): String + + companion object : ServiceProvider { + override fun getService() = ServiceRegistry.getService(StatusListIndexService::class) + override fun defaultImplementation() = WaltIdStatusListIndexService() + + } +} diff --git a/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/index/WaltIdStatusListIndexService.kt b/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/index/WaltIdStatusListIndexService.kt new file mode 100644 index 00000000..5665031a --- /dev/null +++ b/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/index/WaltIdStatusListIndexService.kt @@ -0,0 +1,40 @@ +package id.walt.signatory.revocation.statuslist2021.index + +import id.walt.common.resolveContent +import id.walt.services.WaltIdServices +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import java.io.File +import java.net.URLEncoder +import java.nio.charset.StandardCharsets + +class WaltIdStatusListIndexService: StatusListIndexService() { + private val indexingStrategy = IndexingStrategy.getService() + private val indexRootPath = WaltIdServices.revocationDir + + override fun index(url: String): String = let { + checkIndex(url).takeIf { it } ?: createIndex(url) + }.let { + resolveContent(getIndexPath(url)) + }.let{ + val bitset: Array = Json.decodeFromString(it) + val index = generate(bitset) + updateIndex(url, bitset.plus(index)) + index + } + + private fun generate(occupied: Array): String = indexingStrategy.next(occupied) + + private fun getIndexPath(url: String) = "$indexRootPath/${URLEncoder.encode(url, StandardCharsets.UTF_8)}.index" + + private fun createIndex(url: String) = File(getIndexPath(url)).run { + this.createNewFile() + this.writeText(Json.encodeToString(emptyArray())) + } + + private fun checkIndex(url: String) = File(getIndexPath(url)).exists() + + private fun updateIndex(url: String, bitset: Array){ + File(getIndexPath(url)).writeText(Json.encodeToString(bitset)) + } +} diff --git a/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/storage/StatusListCredentialStorageService.kt b/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/storage/StatusListCredentialStorageService.kt new file mode 100644 index 00000000..5009027f --- /dev/null +++ b/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/storage/StatusListCredentialStorageService.kt @@ -0,0 +1,17 @@ +package id.walt.signatory.revocation.statuslist2021.storage + +import id.walt.credentials.w3c.VerifiableCredential +import id.walt.servicematrix.ServiceProvider +import id.walt.services.WaltIdService + +open class StatusListCredentialStorageService : WaltIdService() { + override val implementation get() = serviceImplementation() + + open fun fetch(url: String): VerifiableCredential? = implementation.fetch(url) + open fun store(credential: VerifiableCredential, url: String): Unit = implementation.store(credential, url) + + companion object : ServiceProvider { + override fun getService() = object : StatusListCredentialStorageService() {} + override fun defaultImplementation() = WaltIdStatusListCredentialStorageService() + } +} diff --git a/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/storage/WaltIdStatusListCredentialStorageService.kt b/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/storage/WaltIdStatusListCredentialStorageService.kt new file mode 100644 index 00000000..22e75b8f --- /dev/null +++ b/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/storage/WaltIdStatusListCredentialStorageService.kt @@ -0,0 +1,30 @@ +package id.walt.signatory.revocation.statuslist2021.storage + +import id.walt.common.resolveContent +import id.walt.credentials.w3c.VerifiableCredential +import id.walt.services.WaltIdServices +import java.io.File +import java.net.URLEncoder +import java.nio.charset.StandardCharsets +import kotlin.io.path.Path +import kotlin.io.path.pathString + +class WaltIdStatusListCredentialStorageService : StatusListCredentialStorageService() { + + override fun fetch(url: String): VerifiableCredential? = let { + val path = getCredentialPath(url) + resolveContent(path).takeIf { it != path }?.let { + VerifiableCredential.fromJson(it) + } + } + + override fun store(credential: VerifiableCredential, url: String): Unit = File(getCredentialPath(url)).run { + this.exists().takeIf { !it }?.let { + this.createNewFile() + } + this.writeText(credential.encode()) + } + + private fun getCredentialPath(name: String) = + Path(WaltIdServices.revocationDir, "${URLEncoder.encode(name, StandardCharsets.UTF_8)}.cred").pathString +}