From 2347d6d5f68a1e30ecfc9c431ada01791a6ba2e4 Mon Sep 17 00:00:00 2001 From: mikeplotean Date: Fri, 21 Apr 2023 19:50:47 +0300 Subject: [PATCH] feat: status-list-2021-entry revocation full support feat: vc-revocation cli refactor: revocation services refactor: signatory rest-api revocation --- .../CredentialPropertyVerificationPolicies.kt | 45 +++----- src/main/kotlin/id/walt/cli/VcCommand.kt | 82 +++++++++++--- src/main/kotlin/id/walt/cli/WaltCLI.kt | 10 +- src/main/kotlin/id/walt/common/CommonUtils.kt | 52 ++++++++- .../w3c/builder/W3CCredentialBuilder.kt | 42 +++++++ .../status/CredentialStatusModels.kt | 8 +- .../walt/signatory/RevocationClientService.kt | 71 ------------ .../kotlin/id/walt/signatory/Signatory.kt | 16 ++- .../id/walt/signatory/WaltIdSignatory.kt | 9 +- .../signatory/rest/SignatoryController.kt | 68 +++++++++--- .../walt/signatory/rest/SignatoryRestAPI.kt | 6 +- .../revocation/CredentialStatusFactory.kt | 58 ++++++++++ .../signatory/revocation/RevocationService.kt | 104 +++++++++++++----- .../revocation/StatusList2021EntryService.kt | 50 --------- .../SimpleCredentialClientService.kt | 53 +++++++++ ...mpleCredentialStatus2022StorageService.kt} | 19 ++-- .../StatusList2021EntryClientService.kt | 85 ++++++++++++++ .../StatusListCredentialStorageService.kt | 75 +++++++++++++ .../statuslist2021/StatusListIndexService.kt | 56 ++++++++++ .../StatusList2021Credential.json | 16 +++ ...t => SimpleCredentialClientServiceTest.kt} | 21 ++-- ...CredentialStatus2022StorageServiceTest.kt} | 20 ++-- .../revocation/StatusList2021ServiceTest.kt | 9 +- 23 files changed, 724 insertions(+), 251 deletions(-) delete mode 100644 src/main/kotlin/id/walt/signatory/RevocationClientService.kt create mode 100644 src/main/kotlin/id/walt/signatory/revocation/CredentialStatusFactory.kt delete mode 100644 src/main/kotlin/id/walt/signatory/revocation/StatusList2021EntryService.kt create mode 100644 src/main/kotlin/id/walt/signatory/revocation/simplestatus2022/SimpleCredentialClientService.kt rename src/main/kotlin/id/walt/signatory/revocation/{SimpleCredentialStatus2022Service.kt => simplestatus2022/SimpleCredentialStatus2022StorageService.kt} (61%) create mode 100644 src/main/kotlin/id/walt/signatory/revocation/statuslist2021/StatusList2021EntryClientService.kt create mode 100644 src/main/kotlin/id/walt/signatory/revocation/statuslist2021/StatusListCredentialStorageService.kt create mode 100644 src/main/kotlin/id/walt/signatory/revocation/statuslist2021/StatusListIndexService.kt create mode 100644 src/main/resources/vc-templates/StatusList2021Credential.json rename src/test/kotlin/id/walt/signatory/{RevocationClientTest.kt => SimpleCredentialClientServiceTest.kt} (53%) rename src/test/kotlin/id/walt/signatory/{RevocationServiceTest.kt => SimpleCredentialStatus2022StorageServiceTest.kt} (58%) diff --git a/src/main/kotlin/id/walt/auditor/policies/CredentialPropertyVerificationPolicies.kt b/src/main/kotlin/id/walt/auditor/policies/CredentialPropertyVerificationPolicies.kt index db53e2e39..7714ed64f 100644 --- a/src/main/kotlin/id/walt/auditor/policies/CredentialPropertyVerificationPolicies.kt +++ b/src/main/kotlin/id/walt/auditor/policies/CredentialPropertyVerificationPolicies.kt @@ -1,6 +1,5 @@ package id.walt.auditor.policies -import com.beust.klaxon.Json import com.beust.klaxon.Klaxon import id.walt.auditor.ParameterizedVerificationPolicy import id.walt.auditor.SimpleVerificationPolicy @@ -8,11 +7,10 @@ import id.walt.auditor.VerificationPolicyResult import id.walt.credentials.w3c.VerifiableCredential import id.walt.credentials.w3c.VerifiablePresentation 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.RevocationClientService -import id.walt.signatory.revocation.StatusList2021EntryService -import kotlinx.serialization.Serializable +import id.walt.signatory.revocation.CredentialStatusCredential +import id.walt.signatory.revocation.RevocationClientService +import id.walt.signatory.revocation.RevocationStatus +import id.walt.signatory.revocation.TokenRevocationStatus import java.text.SimpleDateFormat import java.util.* @@ -51,41 +49,24 @@ class ExpirationDateAfterPolicy : SimpleVerificationPolicy() { class CredentialStatusPolicy : SimpleVerificationPolicy() { - @Serializable - data class CredentialStatusCredential( - @Json(serializeNull = false) var credentialStatus: CredentialStatus? = null - ) - override val description: String = "Verify by credential status" override fun doVerify(vc: VerifiableCredential): VerificationPolicyResult = runCatching { - Klaxon().parse(vc.toJson())!!.credentialStatus!!.let { - when (it) { - is SimpleCredentialStatus2022 -> checkSimpleRevocation(it) - is StatusList2021EntryCredentialStatus -> checkStatusListRevocation(it) + Klaxon().parse(vc.toJson())!!.credentialStatus!!.let { cs -> + RevocationClientService.check(vc).let { + if (!it.isRevoked) VerificationPolicyResult.success() + else failResult(it, cs) } } }.getOrElse { VerificationPolicyResult.failure(it) } - private fun checkSimpleRevocation(cs: CredentialStatus) = let { - fun revocationVerificationPolicy(revoked: Boolean, timeOfRevocation: Long?) = - if (!revoked) VerificationPolicyResult.success() - else VerificationPolicyResult.failure( - IllegalArgumentException("CredentialStatus (type ${cs.type}) was REVOKED at timestamp $timeOfRevocation for id ${cs.id}.") - ) - - val rs = RevocationClientService.getService() - val result = rs.checkRevoked(cs.id) - revocationVerificationPolicy(result.isRevoked, result.timeOfRevocation) + private fun failResult(status: RevocationStatus, cs: CredentialStatus) = when (status) { + is TokenRevocationStatus -> "CredentialStatus (type ${cs.type}) was REVOKED at timestamp ${status.timeOfRevocation} for id ${cs.id}." + else -> "CredentialStatus ${cs.type} was REVOKED for id ${cs.id}" + }.let { + VerificationPolicyResult.failure(IllegalArgumentException(it)) } - - private fun checkStatusListRevocation(cs: StatusList2021EntryCredentialStatus) = - StatusList2021EntryService.checkRevoked(cs).let { - it.takeIf { !it }?.let { - VerificationPolicyResult.success() - } ?: VerificationPolicyResult.failure(Throwable("CredentialStatus ${cs.type} was REVOKED for id ${cs.id}")) - } } data class ChallengePolicyArg(val challenges: Set, val applyToVC: Boolean = true, val applyToVP: Boolean = true) diff --git a/src/main/kotlin/id/walt/cli/VcCommand.kt b/src/main/kotlin/id/walt/cli/VcCommand.kt index f46a3763e..cd6fdce9d 100644 --- a/src/main/kotlin/id/walt/cli/VcCommand.kt +++ b/src/main/kotlin/id/walt/cli/VcCommand.kt @@ -20,11 +20,13 @@ import id.walt.common.resolveContent import id.walt.credentials.w3c.toVerifiableCredential import id.walt.crypto.LdSignatureType import id.walt.custodian.Custodian +import id.walt.model.credential.status.CredentialStatus import id.walt.signatory.Ecosystem 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 io.ktor.util.date.* import mu.KotlinLogging import java.io.File @@ -37,6 +39,7 @@ import kotlin.io.path.readText private val log = KotlinLogging.logger {} +//region -VC Commands- class VcCommand : CliktCommand( name = "vc", help = """Verifiable Credentials (VCs) @@ -74,6 +77,10 @@ class VcIssueCommand : CliktCommand( val ecosystem: Ecosystem by option("--ecosystem", help = "Specify ecosystem, for specific defaults of issuing parameters") .enum() .default(Ecosystem.DEFAULT) + val statusType: CredentialStatus.Types? by option( + "--status-type", + help = "Specify the credentialStatus type" + ).enum() private val signatory = Signatory.getService() @@ -93,6 +100,7 @@ class VcIssueCommand : CliktCommand( proofPurpose = proofPurpose, ldSignatureType = ldSignatureType, ecosystem = ecosystem, + statusType = statusType, //creator = if (ecosystem == Ecosystem.GAIAX) null else issuerDid creator = issuerDid ), when (interactive) { @@ -228,6 +236,23 @@ class VerifyVcCommand : CliktCommand( } } +class ListVcCommand : CliktCommand( + name = "list", help = """List VCs + + """ +) { + + override fun run() { + echo("\nListing verifiable credentials...") + + echo("\nResults:\n") + + Custodian.getService().listCredentials().forEachIndexed { index, vc -> echo("- ${index + 1}: $vc") } + } +} +//endregion + +//region -Policy Commands- class VerificationPoliciesCommand : CliktCommand( name = "policies", help = "Manage verification policies" ) { @@ -339,22 +364,9 @@ class RemoveDynamicVerificationPolicyCommand : CliktCommand( } } } +//endregion -class ListVcCommand : CliktCommand( - name = "list", help = """List VCs - - """ -) { - - override fun run() { - echo("\nListing verifiable credentials...") - - echo("\nResults:\n") - - Custodian.getService().listCredentials().forEachIndexed { index, vc -> echo("- ${index + 1}: $vc") } - } -} - +//region -Templates Commands- class VcTemplatesCommand : CliktCommand( name = "templates", help = """VC templates @@ -448,3 +460,43 @@ class VcTemplatesRemoveCommand : CliktCommand( } } } +//endregion + +//region -Revocation Commands- +class VcRevocationCommand : CliktCommand( + name = "revocation", help = """VC revocations + + VC revocation related operations e.g.: check & revoke. + + """ +) { + + override fun run() { + + } +} + +class VcRevocationCheckCommand : CliktCommand( + name = "check", help = "Check VC revocation status" +) { + val vcFile: File by argument().file() + override fun run() = vcFile.takeIf { it.exists() }?.run { + println("Checking revocation status for credential stored at: ${vcFile.absolutePath}") + val status = RevocationClientService.check(this.readText().toVerifiableCredential()) + println("Revocation status:") + println(Klaxon().toJsonString(status).prettyPrint()) + } ?: Unit +} + +class VcRevocationRevokeCommand: CliktCommand( + name = "revoke", help = "Revoke VC" +) { + val vcFile: File by argument().file() + override fun run() = vcFile.takeIf { it.exists() }?.run { + println("Revoking credential stored at: ${vcFile.absolutePath}") + val result = RevocationClientService.revoke(this.readText().toVerifiableCredential()) + println("Revocation result:") + println(Klaxon().toJsonString(result).prettyPrint()) + } ?: Unit +} +//endregion diff --git a/src/main/kotlin/id/walt/cli/WaltCLI.kt b/src/main/kotlin/id/walt/cli/WaltCLI.kt index 3a909398c..f6b83e5d2 100644 --- a/src/main/kotlin/id/walt/cli/WaltCLI.kt +++ b/src/main/kotlin/id/walt/cli/WaltCLI.kt @@ -112,7 +112,11 @@ object WaltCLI { VcTemplatesImportCommand(), VcTemplatesRemoveCommand() ), - VcImportCommand() + VcImportCommand(), + VcRevocationCommand().subcommands( + VcRevocationCheckCommand(), + VcRevocationRevokeCommand(), + ), ), EssifCommand().subcommands( EssifOnboardingCommand(), @@ -164,7 +168,9 @@ object WaltCLI { if (log.isDebugEnabled) e.printStackTrace() } finally { - WaltIdServices.shutdown() + args.none { it.equals(ServeCommand().commandName, ignoreCase = true) }.takeIf { it }?.run { + WaltIdServices.shutdown() + } } } } diff --git a/src/main/kotlin/id/walt/common/CommonUtils.kt b/src/main/kotlin/id/walt/common/CommonUtils.kt index 548569016..a573a7b19 100644 --- a/src/main/kotlin/id/walt/common/CommonUtils.kt +++ b/src/main/kotlin/id/walt/common/CommonUtils.kt @@ -1,7 +1,6 @@ package id.walt.common import id.walt.services.WaltIdServices.httpNoAuth -import id.walt.signatory.revocation.SimpleCredentialStatus2022Service import io.ktor.client.request.* import io.ktor.client.statement.* import kotlinx.coroutines.runBlocking @@ -9,8 +8,10 @@ import org.apache.commons.codec.digest.DigestUtils import org.bouncycastle.util.encoders.Base32 import java.io.ByteArrayOutputStream import java.io.File +import java.nio.charset.StandardCharsets import java.util.* import java.util.zip.* +import kotlin.reflect.full.memberProperties fun resolveContent(fileUrlContent: String): String { val file = File(fileUrlContent) @@ -42,6 +43,19 @@ fun resolveContentToFile(fileUrlContent: String, tempPrefix: String = "TEMP", te return fileCheck } +fun getExternalHostname(): String? { + return System.getenv("EXTERNAL_HOSTNAME") + ?: System.getenv("HOSTNAMEE") // linux + ?: File("/etc/hostname").let { file -> // linux alternative + if (file.exists()) { + file.readText(StandardCharsets.UTF_8).trim() + } else { + null + } + } + ?: System.getenv("COMPUTERNAME") // windows +} + fun compressGzip(data: ByteArray): ByteArray { val result = ByteArrayOutputStream() GZIPOutputStream(result).use { @@ -68,5 +82,41 @@ fun uncompressGzip(data: ByteArray, idx: ULong? = null) = } ?: it.readText().toCharArray() } +fun buildRawBitString(bitSet: BitSet): ByteArray{ + var lastIndex = 0 + var currIndex = bitSet.nextSetBit(lastIndex); + val builder = StringBuilder() + while (currIndex > -1) { + val delta = 1 % (lastIndex + 1) + builder.append("0".repeat(currIndex - lastIndex - delta)).append("1") + lastIndex = currIndex + currIndex = bitSet.nextSetBit(lastIndex + 1)//TODO: handle overflow + } + builder.append("0".repeat(bitSet.size() - lastIndex - 1)) + return builder.toString().toByteArray() +} + +fun createEncodedBitString(bitSet: BitSet): ByteArray = Base64.getEncoder().encode(compressGzip(buildRawBitString(bitSet))) + fun createBaseToken() = UUID.randomUUID().toString() + UUID.randomUUID().toString() fun deriveRevocationToken(baseToken: String): String = Base32.toBase32String(DigestUtils.sha256(baseToken)).replace("=", "") + +fun String.toBitSet(initialSize: Int) = let { + val bitSet = BitSet(initialSize) + for (i in this.indices) { + if (this[i] == '1') bitSet.set(i) + } + bitSet +} + +fun CharArray.toBitSet(initialSize: Int) = String(this).toBitSet(initialSize) + +/** + * Converts a class properties into map. + * + * ___Note___: Applicable only for linear properties, nested properties will be ignored. + */ +inline fun T.asMap() : Map { + val props = T::class.memberProperties.associateBy { it.name } + return props.keys.associateWith { props[it]?.get(this) } +} 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 f7ed95f7c..851c923a0 100644 --- a/src/main/kotlin/id/walt/credentials/w3c/builder/W3CCredentialBuilder.kt +++ b/src/main/kotlin/id/walt/credentials/w3c/builder/W3CCredentialBuilder.kt @@ -1,6 +1,16 @@ package id.walt.credentials.w3c.builder +import id.walt.common.asMap +import id.walt.common.createBaseToken import id.walt.credentials.w3c.* +import id.walt.model.credential.status.CredentialStatus +import id.walt.signatory.ProofConfig +import id.walt.signatory.revocation.SimpleCredentialStatusFactory +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 kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.jsonArray @@ -8,6 +18,38 @@ import kotlinx.serialization.json.jsonObject import java.time.Instant import java.time.format.DateTimeFormatterBuilder +class W3CCredentialBuilderWithCredentialStatus>( + private val builder: AbstractW3CCredentialBuilder, + private val proofConfig: ProofConfig, +):AbstractW3CCredentialBuilder(builder.type, VerifiableCredential){ + + private val statusListEntryFactory = StatusListEntryFactory( + StatusListIndexService.getService(), + StatusListCredentialStorageService.getService(), + ) + private val simpleStatusFactory = SimpleCredentialStatusFactory() + + override fun build(): C = builder.apply { + getStatusProperty( + type = proofConfig.statusType!!, + purpose = proofConfig.statusPurpose, + credentialUrl = proofConfig.revocationUrl + )?.let { this.setProperty("credentialStatus", it) } + }.build() + + private fun getStatusProperty(type: CredentialStatus.Types, purpose: String, credentialUrl: String) = when (type) { + CredentialStatus.Types.SimpleCredentialStatus2022 -> simpleStatusFactory.create(SimpleStatusFactoryParameter( + id = credentialUrl + "token/${createBaseToken()}", + )).asMap() + CredentialStatus.Types.StatusList2021Entry -> statusListEntryFactory.create(StatusListEntryFactoryParameter( + purpose = purpose, + credentialUrl = credentialUrl + "status/$purpose", + )).asMap() + }.takeIf { + it.isNotEmpty() + } +} + class W3CCredentialBuilder(type: List = listOf("VerifiableCredential")) : AbstractW3CCredentialBuilder(type, VerifiableCredential) { companion object { diff --git a/src/main/kotlin/id/walt/model/credential/status/CredentialStatusModels.kt b/src/main/kotlin/id/walt/model/credential/status/CredentialStatusModels.kt index 2c69f4ad2..6e62c52a3 100644 --- a/src/main/kotlin/id/walt/model/credential/status/CredentialStatusModels.kt +++ b/src/main/kotlin/id/walt/model/credential/status/CredentialStatusModels.kt @@ -11,12 +11,16 @@ sealed class CredentialStatus( val type: String, ) { abstract val id: String + enum class Types{ + StatusList2021Entry, + SimpleCredentialStatus2022, + } } @Serializable class SimpleCredentialStatus2022( override val id: String, -) : CredentialStatus("SimpleCredentialStatus2022") +) : CredentialStatus(Types.SimpleCredentialStatus2022.name) @Serializable data class StatusList2021EntryCredentialStatus( @@ -24,7 +28,7 @@ data class StatusList2021EntryCredentialStatus( val statusPurpose: String, val statusListIndex: String, val statusListCredential: String, -) : CredentialStatus("StatusList2021Entry") +) : CredentialStatus(Types.StatusList2021Entry.name) class CredentialStatusTypeAdapter : TypeAdapter { override fun classFor(type: Any): KClass = when (type as String) { diff --git a/src/main/kotlin/id/walt/signatory/RevocationClientService.kt b/src/main/kotlin/id/walt/signatory/RevocationClientService.kt deleted file mode 100644 index 4a7cbb6c1..000000000 --- a/src/main/kotlin/id/walt/signatory/RevocationClientService.kt +++ /dev/null @@ -1,71 +0,0 @@ -package id.walt.signatory - -import id.walt.servicematrix.ServiceProvider -import id.walt.services.WaltIdService -import id.walt.services.WaltIdServices -import id.walt.signatory.revocation.TokenRevocationResult -import io.ktor.client.* -import io.ktor.client.call.* -import io.ktor.client.plugins.* -import io.ktor.client.plugins.contentnegotiation.* -import io.ktor.client.plugins.logging.* -import io.ktor.client.request.* -import io.ktor.http.* -import io.ktor.serialization.kotlinx.json.* -import kotlinx.coroutines.runBlocking -import mu.KotlinLogging - -open class RevocationClientService : WaltIdService() { - override val implementation get() = serviceImplementation() - - open fun checkRevoked(revocationCheckUrl: String): TokenRevocationResult = - implementation.checkRevoked(revocationCheckUrl) - - open fun revoke(baseTokenUrl: String): Unit = implementation.revoke(baseTokenUrl) - - companion object : ServiceProvider { - override fun getService() = object : RevocationClientService() {} - override fun defaultImplementation() = WaltIdRevocationClientService() - } -} - -class WaltIdRevocationClientService : RevocationClientService() { - - private val logger = KotlinLogging.logger("WaltIdRevocationClientService") - - private val http = HttpClient { - install(ContentNegotiation) { - json() - } - - if (WaltIdServices.httpLogging) { - install(Logging) { - logger = Logger.SIMPLE - level = LogLevel.HEADERS - } - } - - defaultRequest { - contentType(ContentType.Application.Json) - accept(ContentType.Application.Json) - } - } - - override fun checkRevoked(revocationCheckUrl: String): TokenRevocationResult = runBlocking { - val token = revocationCheckUrl.split("/").last() - if (token.contains("-")) throw IllegalArgumentException("Revocation token contains '-', you probably didn't supply a derived revocation token, but a base token.") - - logger.debug { "Checking revocation at $revocationCheckUrl" } - http.get(revocationCheckUrl).body() - } - - override fun revoke(baseTokenUrl: String) { - val baseToken = baseTokenUrl.split("/").last() - if (baseToken.length != 72) throw IllegalArgumentException("base token has to have 72 chars (uuiduuid)") - - logger.debug { "Revoking at $baseTokenUrl" } - runBlocking { - http.post(baseTokenUrl) - } - } -} diff --git a/src/main/kotlin/id/walt/signatory/Signatory.kt b/src/main/kotlin/id/walt/signatory/Signatory.kt index e3b9fa0f9..b940ebc28 100644 --- a/src/main/kotlin/id/walt/signatory/Signatory.kt +++ b/src/main/kotlin/id/walt/signatory/Signatory.kt @@ -1,14 +1,17 @@ package id.walt.signatory import com.beust.klaxon.Json +import id.walt.common.getExternalHostname import id.walt.credentials.w3c.VerifiableCredential import id.walt.credentials.w3c.W3CIssuer import id.walt.credentials.w3c.builder.AbstractW3CCredentialBuilder import id.walt.credentials.w3c.templates.VcTemplate import id.walt.crypto.LdSignatureType +import id.walt.model.credential.status.CredentialStatus import id.walt.servicematrix.ServiceConfiguration import id.walt.servicematrix.ServiceProvider import id.walt.services.WaltIdService +import id.walt.signatory.rest.SignatoryRestAPI import mu.KotlinLogging import java.time.Instant @@ -41,7 +44,12 @@ data class ProofConfig( @Json(serializeNull = false) val dataProviderIdentifier: String? = null, // may be used for mapping data-sets from a custom data-provider @Json(serializeNull = false) val ldSignatureType: LdSignatureType? = null, @Json(serializeNull = false) val creator: String? = issuerDid, - @Json(serializeNull = false) val ecosystem: Ecosystem = Ecosystem.DEFAULT + @Json(serializeNull = false) val ecosystem: Ecosystem = Ecosystem.DEFAULT, + @Json(serializeNull = false) val statusType: CredentialStatus.Types? = null, + @Json(serializeNull = false) val statusPurpose: String = "revocation", + @Json(serializeNull = false) val revocationUrl: String = "https://${ + getExternalHostname() ?: "${SignatoryRestAPI.BIND_ADDRESS}:${SignatoryRestAPI.SIGNATORY_API_PORT}" + }/v1/credentials/", ) data class SignatoryConfig( @@ -61,7 +69,7 @@ abstract class Signatory : WaltIdService() { config: ProofConfig, dataProvider: SignatoryDataProvider? = null, issuer: W3CIssuer? = null, - storeCredential: Boolean = false + storeCredential: Boolean = false, ): String = implementation.issue(templateIdOrFilename, config, dataProvider, issuer, storeCredential) @@ -69,8 +77,8 @@ abstract class Signatory : WaltIdService() { credentialBuilder: AbstractW3CCredentialBuilder<*, *>, config: ProofConfig, issuer: W3CIssuer? = null, - storeCredential: Boolean = false - ): String = implementation.issue(credentialBuilder, config, issuer) + storeCredential: Boolean = false, + ): String = implementation.issue(credentialBuilder, config, issuer, storeCredential) open fun listTemplates(): List = implementation.listTemplates() open fun listTemplateIds(): List = implementation.listTemplateIds() diff --git a/src/main/kotlin/id/walt/signatory/WaltIdSignatory.kt b/src/main/kotlin/id/walt/signatory/WaltIdSignatory.kt index 405d754bf..008f66cdc 100644 --- a/src/main/kotlin/id/walt/signatory/WaltIdSignatory.kt +++ b/src/main/kotlin/id/walt/signatory/WaltIdSignatory.kt @@ -4,6 +4,7 @@ import id.walt.credentials.w3c.VerifiableCredential import id.walt.credentials.w3c.W3CIssuer import id.walt.credentials.w3c.builder.AbstractW3CCredentialBuilder import id.walt.credentials.w3c.builder.W3CCredentialBuilder +import id.walt.credentials.w3c.builder.W3CCredentialBuilderWithCredentialStatus import id.walt.credentials.w3c.templates.VcTemplate import id.walt.credentials.w3c.templates.VcTemplateService import id.walt.credentials.w3c.toVerifiableCredential @@ -75,7 +76,7 @@ class WaltIdSignatory(configurationPath: String) : Signatory() { config: ProofConfig, dataProvider: SignatoryDataProvider?, issuer: W3CIssuer?, - storeCredential: Boolean + storeCredential: Boolean, ): String { val credentialBuilder = when (Files.exists(Path.of(templateIdOrFilename))) { @@ -91,7 +92,7 @@ class WaltIdSignatory(configurationPath: String) : Signatory() { credentialBuilder: AbstractW3CCredentialBuilder<*, *>, config: ProofConfig, issuer: W3CIssuer?, - storeCredential: Boolean + storeCredential: Boolean, ): String { val fullProofConfig = fillProofConfig(config) val vcRequest = credentialBuilder.apply { @@ -104,6 +105,10 @@ class WaltIdSignatory(configurationPath: String) : Signatory() { setIssuanceDate(fullProofConfig.issueDate ?: Instant.now()) setValidFrom(fullProofConfig.validDate ?: Instant.now()) fullProofConfig.expirationDate?.let { setExpirationDate(it) } + }.let { builder -> + config.statusType?.let { + W3CCredentialBuilderWithCredentialStatus(builder, config) + } ?: builder }.build() log.debug { "Signing credential with proof using ${fullProofConfig.proofType.name}..." } diff --git a/src/main/kotlin/id/walt/signatory/rest/SignatoryController.kt b/src/main/kotlin/id/walt/signatory/rest/SignatoryController.kt index 6251f3490..a7a5454ee 100644 --- a/src/main/kotlin/id/walt/signatory/rest/SignatoryController.kt +++ b/src/main/kotlin/id/walt/signatory/rest/SignatoryController.kt @@ -4,15 +4,20 @@ import id.walt.common.KlaxonWithConverters import id.walt.credentials.w3c.JsonConverter import id.walt.credentials.w3c.VerifiableCredential import id.walt.credentials.w3c.builder.W3CCredentialBuilder +import id.walt.credentials.w3c.toVerifiableCredential 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.SimpleCredentialStatus2022Service -import id.walt.signatory.revocation.TokenRevocationResult +import id.walt.signatory.revocation.RevocationClientService +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 io.javalin.http.BadRequestResponse import io.javalin.http.ContentType import io.javalin.http.Context +import io.javalin.http.HttpCode import io.javalin.plugin.openapi.dsl.document import kotlinx.serialization.json.jsonObject @@ -129,22 +134,57 @@ object SignatoryController { .queryParam("proofType") .body().json("200") + fun statusDocs() = document().operation { + it.summary("Get StatusList2021Credential").operationId("status").addTagsItem("Credentials") + .description("Fetch the StatusList2021Credential based on id") + }.json("200") + + fun status(ctx: Context) { + StatusListCredentialStorageService.getService().fetch(ctx.pathParam("id"))?.let { + ctx.result(it.toJson()).status(HttpCode.OK) + } ?: let { + val error = mapOf("error" to "StatusList2021Credential not found for id: ${ctx.pathParam("id")}") + ctx.json(error).status(HttpCode.NOT_FOUND) + } + } + + fun tokenDocs() = document().operation { + it.summary("Get the credential's specific delegated revocation-token that can be revoked on this server.") + .operationId("token").addTagsItem("Credentials") + .description("Get the revocation token based on id") + }.json("200") + + fun token(ctx: Context) { + SimpleCredentialStatus2022StorageService.checkRevoked(ctx.pathParam("id")).let { + ctx.json(it).status(HttpCode.OK) + } + } + fun checkRevokedDocs() = document().operation { it.summary("Check if credential is revoked").operationId("checkRevoked").addTagsItem("Revocations") - .description("Based on a revocation-token, this method will check if this token is still valid or has already been revoked.") - }.json("200") - - fun checkRevoked(ctx: Context) { - ctx.json(SimpleCredentialStatus2022Service.checkRevoked(ctx.pathParam("id"))) + .description("The revocation status is checked based on credential's credential-status property.") + }.body { + it.description("Verifiable credential to be checked for revocation status.") + }.json("200") + + fun checkRevoked(ctx: Context) = runCatching { + RevocationClientService.check(ctx.body().toVerifiableCredential()) + }.onSuccess { + ctx.json(it) + }.onFailure { + ctx.json(it.localizedMessage) } fun revokeDocs() = document().operation { it.summary("Revoke a credential").operationId("revoke").addTagsItem("Revocations") - .description("Based on the not-delegated revocation-token, a credential with a specific delegated revocation-token can be revoked on this server.") - }.result("201") - - fun revoke(ctx: Context) { - SimpleCredentialStatus2022Service.revokeToken(ctx.pathParam("id")) - ctx.status(201) - } + .description("The credential will be revoked based on its credential-status property on the current server.") + }.body { + it.description("Verifiable credential to be revoked.") + }.json("201") + + fun revoke(ctx: Context) = runCatching { + RevocationClientService.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/rest/SignatoryRestAPI.kt b/src/main/kotlin/id/walt/signatory/rest/SignatoryRestAPI.kt index b8d65e374..014fcc3d3 100644 --- a/src/main/kotlin/id/walt/signatory/rest/SignatoryRestAPI.kt +++ b/src/main/kotlin/id/walt/signatory/rest/SignatoryRestAPI.kt @@ -128,6 +128,8 @@ object SignatoryRestAPI { SignatoryController::issueCredentialFromJson ) ) + get("status/{id}", documented(SignatoryController.statusDocs(), SignatoryController::status)) + get("token/{id}", documented(SignatoryController.tokenDocs(), SignatoryController::token)) } path("templates") { get("", documented(SignatoryController.listTemplatesDocs(), SignatoryController::listTemplates)) @@ -137,8 +139,8 @@ object SignatoryRestAPI { } path("revocations") { - get("{id}", documented(SignatoryController.checkRevokedDocs(), SignatoryController::checkRevoked)) - post("{id}", documented(SignatoryController.revokeDocs(), SignatoryController::revoke)) + post("check", documented(SignatoryController.checkRevokedDocs(), SignatoryController::checkRevoked)) + post("revoke", documented(SignatoryController.revokeDocs(), SignatoryController::revoke)) } } }.exception(IllegalArgumentException::class.java) { e, ctx -> diff --git a/src/main/kotlin/id/walt/signatory/revocation/CredentialStatusFactory.kt b/src/main/kotlin/id/walt/signatory/revocation/CredentialStatusFactory.kt new file mode 100644 index 000000000..8873578a9 --- /dev/null +++ b/src/main/kotlin/id/walt/signatory/revocation/CredentialStatusFactory.kt @@ -0,0 +1,58 @@ +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.* + +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.credentialUrl, + statusParameter.purpose, + String(createEncodedBitString(BitSet(16 * 1024 * 8))) + ) + } + StatusList2021EntryCredentialStatus( + id = statusParameter.credentialUrl + "#${it.index}", + statusPurpose = statusParameter.purpose, + statusListIndex = it.index, + statusListCredential = statusParameter.credentialUrl, + ) + } +} + +interface CredentialStatusFactoryParameter +data class SimpleStatusFactoryParameter( + val id: String, +) : CredentialStatusFactoryParameter +data class StatusListEntryFactoryParameter( + val credentialUrl: String, + val purpose: String +) : CredentialStatusFactoryParameter diff --git a/src/main/kotlin/id/walt/signatory/revocation/RevocationService.kt b/src/main/kotlin/id/walt/signatory/revocation/RevocationService.kt index 22d183aed..c07cc0977 100644 --- a/src/main/kotlin/id/walt/signatory/revocation/RevocationService.kt +++ b/src/main/kotlin/id/walt/signatory/revocation/RevocationService.kt @@ -1,52 +1,104 @@ package id.walt.signatory.revocation import com.beust.klaxon.Json +import com.beust.klaxon.Klaxon +import id.walt.credentials.w3c.VerifiableCredential +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.simplestatus2022.SimpleCredentialClientService +import id.walt.signatory.revocation.statuslist2021.StatusList2021EntryClientService import kotlinx.serialization.Serializable -interface RevocationService { - fun checkRevocation(parameter: RevocationParameter): RevocationResult - fun getRevocation(): RevocationData - fun clearAll() - fun setRevocation(parameter: RevocationParameter) -} +interface RevocationClientService { + fun checkRevocation(parameter: RevocationCheckParameter): RevocationStatus + fun revoke(parameter: RevocationConfig) + + companion object { + fun revoke(vc: VerifiableCredential): RevocationResult = + (Klaxon().parse(vc.toJson())?.credentialStatus)?.let { + runCatching { getClient(it).revoke(getConfig(it)) }.fold(onSuccess = { + RevocationResult(succeed = true) + }, onFailure = { + RevocationResult(succeed = false, message = it.localizedMessage) + }) + } ?: RevocationResult(succeed = false, message = "Verifiable credential has no credential-status property") + + fun check(vc: VerifiableCredential): RevocationStatus = + (Klaxon().parse(vc.toJson())?.credentialStatus)?.let { + getClient(it).checkRevocation(getParameter(it)) + } ?: throw IllegalArgumentException("Verifiable credential has no credential-status property") -data class RevocationList(val revokedList: List) + private fun getConfig(credentialStatus: CredentialStatus): RevocationConfig = when (credentialStatus) { + is SimpleCredentialStatus2022 -> TokenRevocationConfig(baseTokenUrl = credentialStatus.id) + is StatusList2021EntryCredentialStatus -> StatusListRevocationConfig(credentialStatus = credentialStatus) + } + + private fun getParameter(credentialStatus: CredentialStatus): RevocationCheckParameter = when (credentialStatus) { + is SimpleCredentialStatus2022 -> TokenRevocationCheckParameter(revocationCheckUrl = credentialStatus.id) + is StatusList2021EntryCredentialStatus -> StatusListRevocationCheckParameter(credentialStatus = credentialStatus) + } + + private fun getClient(credentialStatus: CredentialStatus): RevocationClientService = when (credentialStatus) { + is SimpleCredentialStatus2022 -> SimpleCredentialClientService() + is StatusList2021EntryCredentialStatus -> StatusList2021EntryClientService() + } + } +} /* -Revocation results +Revocation status */ -@Serializable -abstract class RevocationResult { - abstract val isRevoked: Boolean +interface RevocationStatus { + val isRevoked: Boolean } - @Serializable -data class TokenRevocationResult( +data class TokenRevocationStatus( val token: String, override val isRevoked: Boolean, @Json(serializeNull = false) val timeOfRevocation: Long? = null -) : RevocationResult() - +) : RevocationStatus @Serializable -data class StatusListRevocationResult( +data class StatusListRevocationStatus( override val isRevoked: Boolean -) : RevocationResult() +) : RevocationStatus /* -Revocation parameters +Revocation check parameters */ -interface RevocationParameter -data class TokenRevocationParameter( - val token: String, -) : RevocationParameter +interface RevocationCheckParameter +data class TokenRevocationCheckParameter( + val revocationCheckUrl: String, +) : RevocationCheckParameter -data class StatusListRevocationParameter( +data class StatusListRevocationCheckParameter( val credentialStatus: StatusList2021EntryCredentialStatus, -) : RevocationParameter +) : RevocationCheckParameter /* -Revocation data +Revocation config */ -interface RevocationData +interface RevocationConfig + +data class TokenRevocationConfig( + val baseTokenUrl: String, +) : RevocationConfig + +data class StatusListRevocationConfig( + val credentialStatus: StatusList2021EntryCredentialStatus, +) : RevocationConfig + +/* +Revocation results + */ +@Serializable +data class RevocationResult( + val succeed: Boolean, + val message: String = "" +) + +@Serializable +data class CredentialStatusCredential( + @Json(serializeNull = false) var credentialStatus: CredentialStatus? = null +) diff --git a/src/main/kotlin/id/walt/signatory/revocation/StatusList2021EntryService.kt b/src/main/kotlin/id/walt/signatory/revocation/StatusList2021EntryService.kt deleted file mode 100644 index ec0e2bb5e..000000000 --- a/src/main/kotlin/id/walt/signatory/revocation/StatusList2021EntryService.kt +++ /dev/null @@ -1,50 +0,0 @@ -package id.walt.signatory.revocation - -import com.beust.klaxon.Json -import id.walt.common.resolveContent -import id.walt.common.uncompressGzip -import id.walt.credentials.w3c.VerifiableCredential -import id.walt.credentials.w3c.toVerifiableCredential -import id.walt.crypto.decBase64 -import id.walt.model.credential.status.StatusList2021EntryCredentialStatus -import kotlinx.serialization.Serializable - -object StatusList2021EntryService { - - fun checkRevoked(credentialStatus: StatusList2021EntryCredentialStatus): Boolean = let { - val credentialSubject = extractStatusListCredentialSubject(credentialStatus.statusListCredential) ?: throw IllegalArgumentException("Couldn't parse credential subject") - val credentialIndex = credentialStatus.statusListIndex.toULongOrNull()?: throw IllegalArgumentException("Couldn't parse status list index") - if(!verifyStatusPurpose(credentialStatus.statusPurpose, credentialSubject.statusPurpose)) throw IllegalArgumentException("Status purposes don't match") - - verifyBitStringStatus(credentialIndex, credentialSubject.encodedList) - } - - private fun extractStatusListCredentialSubject(statusCredential: VerifiableCredential) = - statusCredential.credentialSubject?.let { - StatusListCredentialSubject( - id = it.id, - type = it.properties["type"] as? String ?: "", - statusPurpose = it.properties["statusPurpose"] as? String ?: "", - encodedList = it.properties["encodedList"] as? String ?: "", - ) - } - - private fun extractStatusListCredentialSubject(statusCredential: String) = - extractStatusListCredentialSubject(resolveContent(statusCredential).toVerifiableCredential()) - - private fun verifyProofs() = true - - private fun verifyStatusPurpose(entryPurpose: String, credentialPurpose: String) = - entryPurpose.equals(credentialPurpose, ignoreCase = true) - - private fun verifyBitStringStatus(idx: ULong, encodedList: String) = uncompressGzip(decBase64(encodedList), idx)[0] == '1' - - @Serializable - data class StatusListCredentialSubject( - @Json(serializeNull = false) - val id: String? = null, - val type: String, - val statusPurpose: String, - val encodedList: String, - ) -} diff --git a/src/main/kotlin/id/walt/signatory/revocation/simplestatus2022/SimpleCredentialClientService.kt b/src/main/kotlin/id/walt/signatory/revocation/simplestatus2022/SimpleCredentialClientService.kt new file mode 100644 index 000000000..765265898 --- /dev/null +++ b/src/main/kotlin/id/walt/signatory/revocation/simplestatus2022/SimpleCredentialClientService.kt @@ -0,0 +1,53 @@ +package id.walt.signatory.revocation.simplestatus2022 + +import id.walt.services.WaltIdServices +import id.walt.signatory.revocation.* +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.plugins.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.plugins.logging.* +import io.ktor.client.request.* +import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* +import kotlinx.coroutines.runBlocking +import mu.KotlinLogging + +class SimpleCredentialClientService: RevocationClientService { + + private val logger = KotlinLogging.logger("WaltIdRevocationClientService") + private val credentialStorage = SimpleCredentialStatus2022StorageService + + private val http = HttpClient { + install(ContentNegotiation) { + json() + } + + if (WaltIdServices.httpLogging) { + install(Logging) { + logger = Logger.SIMPLE + level = LogLevel.HEADERS + } + } + + defaultRequest { + contentType(ContentType.Application.Json) + accept(ContentType.Application.Json) + } + } + + override fun checkRevocation(parameter: RevocationCheckParameter): RevocationStatus = runBlocking { + val tokenParameter = parameter as TokenRevocationCheckParameter + logger.debug { "Checking revocation at $parameter" } + http.get(tokenParameter.revocationCheckUrl).body() + } + + override fun revoke(parameter: RevocationConfig) { + val baseTokenUrl = (parameter as TokenRevocationConfig).baseTokenUrl + val baseToken = baseTokenUrl.split("/").last() + if (baseToken.length != 72) throw IllegalArgumentException("base token has to have 72 chars (uuiduuid)") + + logger.debug { "Revoking at $baseTokenUrl" } + credentialStorage.revokeToken(baseToken) + } +} diff --git a/src/main/kotlin/id/walt/signatory/revocation/SimpleCredentialStatus2022Service.kt b/src/main/kotlin/id/walt/signatory/revocation/simplestatus2022/SimpleCredentialStatus2022StorageService.kt similarity index 61% rename from src/main/kotlin/id/walt/signatory/revocation/SimpleCredentialStatus2022Service.kt rename to src/main/kotlin/id/walt/signatory/revocation/simplestatus2022/SimpleCredentialStatus2022StorageService.kt index a98216561..734ffeed6 100644 --- a/src/main/kotlin/id/walt/signatory/revocation/SimpleCredentialStatus2022Service.kt +++ b/src/main/kotlin/id/walt/signatory/revocation/simplestatus2022/SimpleCredentialStatus2022StorageService.kt @@ -1,14 +1,16 @@ -package id.walt.signatory.revocation +package id.walt.signatory.revocation.simplestatus2022 import com.beust.klaxon.Klaxon import id.walt.common.deriveRevocationToken +import id.walt.signatory.revocation.RevocationStatus +import id.walt.signatory.revocation.TokenRevocationStatus import java.time.Instant import kotlin.io.path.Path import kotlin.io.path.exists import kotlin.io.path.readText import kotlin.io.path.writeText -object SimpleCredentialStatus2022Service { +object SimpleCredentialStatus2022StorageService { private val klaxon = Klaxon() private val revokedPath = Path("data/revoked.json").apply { @@ -21,19 +23,20 @@ object SimpleCredentialStatus2022Service { fun clearRevocations() = setRevokedList(RevocationList(emptyList())) - fun checkRevoked(token: String): RevocationResult { - if (token.contains("-")) throw IllegalArgumentException("Revocation token contains '-', you probably didn't supply a derived revocation token, but a base token.") - - println(getRevokedList()) - return getRevokedList().firstOrNull { (it as? TokenRevocationResult)?.token == token } ?: return TokenRevocationResult(token, false) + fun checkRevoked(token: String): RevocationStatus { + val derivedToken = deriveRevocationToken(token) + if (derivedToken.contains("-")) throw IllegalArgumentException("Revocation token contains '-', you probably didn't supply a derived revocation token, but a base token.") + return getRevokedList().firstOrNull { (it as? TokenRevocationStatus)?.token == derivedToken } ?: return TokenRevocationStatus(token, false) } fun revokeToken(baseToken: String) { // UUIDUUID -> SHA256-Token (base32) if (baseToken.length != 72) throw IllegalArgumentException("base token has to have 72 chars (uuiduuid)") val token = deriveRevocationToken(baseToken) val revoked = getRevokedList().toMutableList().apply { - add(TokenRevocationResult(token, true, Instant.now().toEpochMilli())) + add(TokenRevocationStatus(token, true, Instant.now().toEpochMilli())) } setRevokedList(RevocationList(revoked)) } + + data class RevocationList(val revokedList: List) } diff --git a/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/StatusList2021EntryClientService.kt b/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/StatusList2021EntryClientService.kt new file mode 100644 index 000000000..b60c55c69 --- /dev/null +++ b/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/StatusList2021EntryClientService.kt @@ -0,0 +1,85 @@ +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.uncompressGzip +import id.walt.credentials.w3c.VerifiableCredential +import id.walt.credentials.w3c.toVerifiableCredential +import id.walt.crypto.decBase64 +import id.walt.signatory.revocation.* +import kotlinx.serialization.Serializable +import java.util.* + +class StatusList2021EntryClientService: RevocationClientService { + + private val credentialStorage = StatusListCredentialStorageService.getService() + + override fun checkRevocation(parameter: RevocationCheckParameter): RevocationStatus = let { + val credentialStatus = (parameter as StatusListRevocationCheckParameter).credentialStatus + val credentialSubject = extractStatusListCredentialSubject(credentialStatus.statusListCredential) ?: throw IllegalArgumentException("Couldn't parse credential subject") + val credentialIndex = credentialStatus.statusListIndex.toULongOrNull()?: throw IllegalArgumentException("Couldn't parse status list index") + if(!verifyStatusPurpose(credentialStatus.statusPurpose, credentialSubject.statusPurpose)) throw IllegalArgumentException("Status purposes don't match") + verifyStatusCredential() + + 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) + } + + private fun updateStatusCredentialRecord(statusCredential: VerifiableCredential, index: String){ + val credentialSubject = extractStatusListCredentialSubject(statusCredential)!! + // get 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 + credentialStorage.store(statusCredential.id!!, credentialSubject.statusPurpose, String(encodedList)) + } + + private fun extractStatusListCredentialSubject(statusCredentialUrl: String): StatusListCredentialSubject? = + extractStatusListCredentialSubject(resolveContent(statusCredentialUrl).toVerifiableCredential()) + + private fun extractStatusListCredentialSubject(statusCredential: VerifiableCredential) = + statusCredential.credentialSubject?.let { + StatusListCredentialSubject( + id = it.id, + type = it.properties["type"] as? String ?: "", + statusPurpose = it.properties["statusPurpose"] as? String ?: "", + encodedList = it.properties["encodedList"] as? String ?: "", + ) + } + + /* TODO: + - proofs + - matching issuers + */ + private fun verifyStatusCredential() = true + + private fun verifyStatusPurpose(entryPurpose: String, credentialPurpose: String) = + entryPurpose.equals(credentialPurpose, ignoreCase = true) + + private fun verifyBitStringStatus(idx: ULong, encodedList: String) = uncompressGzip(decBase64(encodedList), idx)[0] == '1' + + @Serializable + data class StatusListCredentialSubject( + @Json(serializeNull = false) + val id: String? = null, + val type: String, + val statusPurpose: String, + val encodedList: 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 new file mode 100644 index 000000000..854adb210 --- /dev/null +++ b/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/StatusListCredentialStorageService.kt @@ -0,0 +1,75 @@ +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.toVerifiableCredential +import id.walt.model.DidMethod +import id.walt.servicematrix.ServiceProvider +import id.walt.services.WaltIdService +import id.walt.services.context.ContextManager +import id.walt.services.did.DidService +import id.walt.signatory.ProofConfig +import id.walt.signatory.ProofType +import id.walt.signatory.Signatory + +open class StatusListCredentialStorageService : WaltIdService() { + override val implementation get() = serviceImplementation() + + open fun fetch(id: String): VerifiableCredential? = implementation.fetch(id) + open fun store(id: String, purpose: String, bitString: String): Unit = implementation.store(id, purpose, bitString) + + companion object : ServiceProvider { + override fun getService() = object : StatusListCredentialStorageService() {} + override fun defaultImplementation() = WaltIdStatusListCredentialStorageService() + } +} + + +class WaltIdStatusListCredentialStorageService : StatusListCredentialStorageService() { + private val templatePath = "src/main/resources/vc-templates/StatusList2021Credential.json" + private val credentialsGroup = "status-credentials" + private val signatoryService = Signatory.getService() + private val vcStoreService = ContextManager.vcStore + private val issuerDid = DidService.create(DidMethod.key)// TODO: fix it + + override fun fetch(id: String): VerifiableCredential? = + vcStoreService.getCredential(id.substringAfterLast("/"), credentialsGroup) + override fun store(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(resolveContent(templatePath)).apply { + setId(it.id ?: id) + buildSubject { + setFromJson(it.toJson()) + } + } + }.run { + val credential = signatoryService.issue( + credentialBuilder = this, config = ProofConfig( + credentialId = id, + issuerDid = issuerDid, + subjectDid = issuerDid, + proofType = ProofType.LD_PROOF, + ) + ).toVerifiableCredential() + vcStoreService.storeCredential(credential.id!!.substringAfterLast("/"), credential, credentialsGroup) + } +} diff --git a/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/StatusListIndexService.kt b/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/StatusListIndexService.kt new file mode 100644 index 000000000..3164d5057 --- /dev/null +++ b/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/StatusListIndexService.kt @@ -0,0 +1,56 @@ +package id.walt.signatory.revocation.statuslist2021 + +import com.beust.klaxon.Klaxon +import id.walt.servicematrix.ServiceProvider +import id.walt.services.WaltIdService +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 = "data/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/resources/vc-templates/StatusList2021Credential.json b/src/main/resources/vc-templates/StatusList2021Credential.json new file mode 100644 index 000000000..f89fb40be --- /dev/null +++ b/src/main/resources/vc-templates/StatusList2021Credential.json @@ -0,0 +1,16 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/vc/status-list/2021/v1" + ], + "id": "https://example.com/credentials/status/3", + "type": ["VerifiableCredential", "StatusList2021Credential"], + "issuer": "did:example:12345", + "issued": "2021-04-05T14:27:40Z", + "credentialSubject": { + "id": "https://example.com/status/3#list", + "type": "StatusList2021", + "statusPurpose": "revocation", + "encodedList": "" + } +} diff --git a/src/test/kotlin/id/walt/signatory/RevocationClientTest.kt b/src/test/kotlin/id/walt/signatory/SimpleCredentialClientServiceTest.kt similarity index 53% rename from src/test/kotlin/id/walt/signatory/RevocationClientTest.kt rename to src/test/kotlin/id/walt/signatory/SimpleCredentialClientServiceTest.kt index 17381d363..cd50cff52 100644 --- a/src/test/kotlin/id/walt/signatory/RevocationClientTest.kt +++ b/src/test/kotlin/id/walt/signatory/SimpleCredentialClientServiceTest.kt @@ -4,15 +4,18 @@ import id.walt.common.createBaseToken import id.walt.common.deriveRevocationToken import id.walt.servicematrix.ServiceMatrix import id.walt.signatory.rest.SignatoryRestAPI -import id.walt.signatory.revocation.SimpleCredentialStatus2022Service +import id.walt.signatory.revocation.TokenRevocationCheckParameter +import id.walt.signatory.revocation.TokenRevocationConfig +import id.walt.signatory.revocation.simplestatus2022.SimpleCredentialClientService +import id.walt.signatory.revocation.simplestatus2022.SimpleCredentialStatus2022StorageService import id.walt.test.RESOURCES_PATH import io.kotest.core.spec.style.AnnotationSpec -class RevocationClientTest : AnnotationSpec() { +class SimpleCredentialClientServiceTest : AnnotationSpec() { init { ServiceMatrix("$RESOURCES_PATH/service-matrix.properties") - SimpleCredentialStatus2022Service.clearRevocations() + SimpleCredentialStatus2022StorageService.clearRevocations() } private val SIGNATORY_API_HOST = "localhost" @@ -29,11 +32,11 @@ class RevocationClientTest : AnnotationSpec() { SignatoryRestAPI.stop() } -// @Test TODO: fix + @Test fun test() { - val revocationsBase = "$SIGNATORY_API_URL/v1/revocations" + val revocationsBase = "$SIGNATORY_API_URL/v1/credentials/token" - val rs = RevocationClientService.getService() + val rs = SimpleCredentialClientService() val baseToken = createBaseToken() println(baseToken) @@ -41,12 +44,12 @@ class RevocationClientTest : AnnotationSpec() { val revocationToken = deriveRevocationToken(baseToken) println(revocationToken) - var result = rs.checkRevoked("$revocationsBase/$revocationToken") + var result = rs.checkRevocation(TokenRevocationCheckParameter("$revocationsBase/$revocationToken")) println(result) - rs.revoke("$revocationsBase/$baseToken") + rs.revoke(TokenRevocationConfig("$revocationsBase/$baseToken")) - result = rs.checkRevoked("$revocationsBase/$revocationToken") + result = rs.checkRevocation(TokenRevocationCheckParameter("$revocationsBase/$revocationToken")) println(result) } diff --git a/src/test/kotlin/id/walt/signatory/RevocationServiceTest.kt b/src/test/kotlin/id/walt/signatory/SimpleCredentialStatus2022StorageServiceTest.kt similarity index 58% rename from src/test/kotlin/id/walt/signatory/RevocationServiceTest.kt rename to src/test/kotlin/id/walt/signatory/SimpleCredentialStatus2022StorageServiceTest.kt index ec63f66c9..51d33edd7 100644 --- a/src/test/kotlin/id/walt/signatory/RevocationServiceTest.kt +++ b/src/test/kotlin/id/walt/signatory/SimpleCredentialStatus2022StorageServiceTest.kt @@ -3,23 +3,24 @@ package id.walt.signatory import id.walt.common.createBaseToken import id.walt.common.deriveRevocationToken import id.walt.servicematrix.ServiceMatrix -import id.walt.signatory.revocation.SimpleCredentialStatus2022Service -import id.walt.signatory.revocation.TokenRevocationResult +import id.walt.signatory.revocation.TokenRevocationStatus +import id.walt.signatory.revocation.simplestatus2022.SimpleCredentialClientService +import id.walt.signatory.revocation.simplestatus2022.SimpleCredentialStatus2022StorageService import id.walt.test.RESOURCES_PATH import io.kotest.core.spec.style.AnnotationSpec import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe -class RevocationServiceTest : AnnotationSpec() { +class SimpleCredentialStatus2022StorageServiceTest : AnnotationSpec() { init { ServiceMatrix("$RESOURCES_PATH/service-matrix.properties") - SimpleCredentialStatus2022Service.clearRevocations() + SimpleCredentialStatus2022StorageService.clearRevocations() } - // @Test TODO: fix + @Test fun test() { - val service = RevocationClientService.getService() + val service = SimpleCredentialClientService() val baseToken = createBaseToken() println("New base token: $baseToken") @@ -28,15 +29,16 @@ class RevocationServiceTest : AnnotationSpec() { println("Revocation token derived from base token: $revocationToken") println("Check revoked with derived token: $revocationToken") - val result1 = SimpleCredentialStatus2022Service.checkRevoked(revocationToken) as TokenRevocationResult + val result1 = SimpleCredentialStatus2022StorageService.checkRevoked(baseToken) as TokenRevocationStatus result1.isRevoked shouldBe false result1.timeOfRevocation shouldBe null println("Revoke with base token: $baseToken") - SimpleCredentialStatus2022Service.revokeToken(baseToken) + SimpleCredentialStatus2022StorageService.revokeToken(baseToken) println("Check revoked with derived token: $revocationToken") - val result2 = SimpleCredentialStatus2022Service.checkRevoked(revocationToken) as TokenRevocationResult + + val result2 = SimpleCredentialStatus2022StorageService.checkRevoked(baseToken) as TokenRevocationStatus result2.isRevoked shouldBe true result2.timeOfRevocation shouldNotBe null } diff --git a/src/test/kotlin/id/walt/signatory/revocation/StatusList2021ServiceTest.kt b/src/test/kotlin/id/walt/signatory/revocation/StatusList2021ServiceTest.kt index e271ab0be..29ea13562 100644 --- a/src/test/kotlin/id/walt/signatory/revocation/StatusList2021ServiceTest.kt +++ b/src/test/kotlin/id/walt/signatory/revocation/StatusList2021ServiceTest.kt @@ -3,6 +3,7 @@ package id.walt.signatory.revocation import com.beust.klaxon.Klaxon import id.walt.common.resolveContent import id.walt.model.credential.status.StatusList2021EntryCredentialStatus +import id.walt.signatory.revocation.statuslist2021.StatusList2021EntryClientService import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.StringSpec import io.kotest.data.blocking.forAll @@ -13,7 +14,7 @@ import io.mockk.mockkStatic import io.mockk.unmockkStatic internal class StatusList2021ServiceTest : StringSpec({ - val sut = StatusList2021EntryService + val sut = StatusList2021EntryClientService() val rootPath = "src/test/resources/credential-status/" val statusLisCredential = resolveContent(rootPath + "status-list-credential.json") @@ -26,9 +27,9 @@ internal class StatusList2021ServiceTest : StringSpec({ mockkStatic(::resolveContent) every { resolveContent(any()) } returns statusLisCredential - val result = sut.checkRevoked(vcStatus) + val result = sut.checkRevocation(StatusListRevocationCheckParameter(vcStatus)) - result shouldBe isRevoked + result.isRevoked shouldBe isRevoked unmockkStatic(::resolveContent) } @@ -44,7 +45,7 @@ internal class StatusList2021ServiceTest : StringSpec({ every { resolveContent(any()) } returns statusLisCredential val exception = shouldThrow { - sut.checkRevoked(vcStatus) + sut.checkRevocation(StatusListRevocationCheckParameter(vcStatus)) } exception.message shouldBe message exception::class shouldBe throwing