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 fae7deef..c0e3d521 100644 --- a/src/main/kotlin/id/walt/credentials/w3c/builder/W3CCredentialBuilder.kt +++ b/src/main/kotlin/id/walt/credentials/w3c/builder/W3CCredentialBuilder.kt @@ -38,7 +38,8 @@ class W3CCredentialBuilderWithCredentialStatus statusListEntryFactory.create(StatusListEntryFactoryParameter( purpose = purpose, - credentialUrl = URLBuilder().takeFrom(credentialUrl).appendPathSegments("status", purpose).buildString(), + credentialUrl = credentialUrl, issuer = issuer, )).asMap() }.takeIf { diff --git a/src/main/kotlin/id/walt/signatory/revocation/CredentialStatusFactory.kt b/src/main/kotlin/id/walt/signatory/revocation/CredentialStatusFactory.kt index c2535939..a65b9d74 100644 --- a/src/main/kotlin/id/walt/signatory/revocation/CredentialStatusFactory.kt +++ b/src/main/kotlin/id/walt/signatory/revocation/CredentialStatusFactory.kt @@ -34,7 +34,7 @@ class StatusListEntryFactory( // verify status-credential exists and create one storageService.fetch(statusParameter.credentialUrl) ?: run { storageService.store( - parameter.issuer, + statusParameter.issuer, statusParameter.credentialUrl, statusParameter.purpose, String(createEncodedBitString(BitSet(16 * 1024 * 8))) diff --git a/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/StatusListCredentialStorageService.kt b/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/StatusListCredentialStorageService.kt index 98183e57..ed8942ea 100644 --- a/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/StatusListCredentialStorageService.kt +++ b/src/main/kotlin/id/walt/signatory/revocation/statuslist2021/StatusListCredentialStorageService.kt @@ -13,13 +13,15 @@ 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(id: String): VerifiableCredential? = implementation.fetch(id) + 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) @@ -31,12 +33,12 @@ open class StatusListCredentialStorageService : WaltIdService() { class WaltIdStatusListCredentialStorageService : StatusListCredentialStorageService() { - private val templatePath = "StatusList2021Credential" + private val templateId = "StatusList2021Credential" private val signatoryService = Signatory.getService() private val templateService = VcTemplateService.getService() - override fun fetch(id: String): VerifiableCredential? = let { - val path = getCredentialPath(id.substringAfterLast("/")) + override fun fetch(url: String): VerifiableCredential? = let { + val path = getCredentialPath(url) resolveContent(path).takeIf { it != path }?.let { VerifiableCredential.fromJson(it) } @@ -61,7 +63,7 @@ class WaltIdStatusListCredentialStorageService : StatusListCredentialStorageServ ) ) }.let { - W3CCredentialBuilder.fromPartial(templateService.getTemplate(templatePath).template!!).apply { + W3CCredentialBuilder.fromPartial(templateService.getTemplate(templateId).template!!).apply { setId(it.id ?: id) buildSubject { setFromJson(it.toJson()) @@ -76,10 +78,11 @@ class WaltIdStatusListCredentialStorageService : StatusListCredentialStorageServ proofType = ProofType.LD_PROOF, ) ).toVerifiableCredential() - getCredentialPath(credential.id!!.substringAfterLast("/")).let { + getCredentialPath(credential.id!!).let { File(it).writeText(credential.encode()) } } - private fun getCredentialPath(name: String) = Path(WaltIdServices.revocationDir, "$name.cred").pathString + private fun getCredentialPath(name: String) = + Path(WaltIdServices.revocationDir, "${URLEncoder.encode(name, StandardCharsets.UTF_8)}.cred").pathString } diff --git a/src/test/kotlin/id/walt/signatory/revocation/StatusList2021ServiceTest.kt b/src/test/kotlin/id/walt/signatory/revocation/StatusList2021ServiceTest.kt index 29ea1356..5f327798 100644 --- a/src/test/kotlin/id/walt/signatory/revocation/StatusList2021ServiceTest.kt +++ b/src/test/kotlin/id/walt/signatory/revocation/StatusList2021ServiceTest.kt @@ -2,21 +2,52 @@ package id.walt.signatory.revocation import com.beust.klaxon.Klaxon import id.walt.common.resolveContent +import id.walt.credentials.w3c.VerifiableCredential +import id.walt.credentials.w3c.builder.W3CCredentialBuilder +import id.walt.credentials.w3c.templates.VcTemplateService +import id.walt.crypto.KeyAlgorithm +import id.walt.model.DidMethod +import id.walt.model.credential.status.CredentialStatus import id.walt.model.credential.status.StatusList2021EntryCredentialStatus +import id.walt.servicematrix.ServiceMatrix +import id.walt.services.WaltIdServices +import id.walt.services.did.DidService +import id.walt.services.did.DidWebCreateOptions +import id.walt.services.key.KeyService +import id.walt.signatory.ProofConfig +import id.walt.signatory.ProofType +import id.walt.signatory.Signatory 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 import io.kotest.data.row +import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.mockk.every import io.mockk.mockkStatic import io.mockk.unmockkStatic +import java.io.File +import java.net.URLEncoder +import java.nio.charset.StandardCharsets +import kotlin.io.path.Path +import kotlin.io.path.pathString +import kotlin.random.Random internal class StatusList2021ServiceTest : StringSpec({ val sut = StatusList2021EntryClientService() val rootPath = "src/test/resources/credential-status/" val statusLisCredential = resolveContent(rootPath + "status-list-credential.json") + lateinit var keyId: String + + beforeSpec { + ServiceMatrix("service-matrix.properties") + keyId = KeyService.getService().generate(KeyAlgorithm.EdDSA_Ed25519).id + } + + afterSpec { + KeyService.getService().delete(keyId) + } "test result" { forAll( @@ -53,4 +84,43 @@ internal class StatusList2021ServiceTest : StringSpec({ unmockkStatic(::resolveContent) } } + + "given issuer, when issuing a credential with status then the status-list credential has the issuer's did".config( + blockingTest = true + ) { + forAll( + row(DidService.create(DidMethod.web, keyId, DidWebCreateOptions("example.com"))), + row(DidService.create(DidMethod.key, keyId)), + row(DidService.create(DidMethod.ebsi, keyId)), + row(DidService.create(DidMethod.jwk, keyId)), + row(DidService.create(DidMethod.cheqd, keyId)), + ) { issuer -> + // given + val credentialUrl = "http://localhost:7001/credentials/status/#${Random.nextInt()}" + val path = Path(WaltIdServices.revocationDir, "${URLEncoder.encode(credentialUrl, StandardCharsets.UTF_8)}.cred").pathString + val template = VcTemplateService.getService().getTemplate("VerifiableId").template!! + // when + Signatory.getService().issue( + W3CCredentialBuilder.fromPartial(template), ProofConfig( + subjectDid = issuer, + issuerDid = issuer, + proofType = ProofType.LD_PROOF, + statusPurpose = "revocation", + statusType = CredentialStatus.Types.StatusList2021Entry, + credentialsEndpoint = credentialUrl + ) + ) + // then + val statusListVcStr = resolveContent(path) + val statusListVc = VerifiableCredential.fromString(statusListVcStr) + statusListVc.shouldNotBeNull() + statusListVc.issuerId.shouldNotBeNull() + statusListVc.issuerId shouldBe issuer + + //cleanup TODO: dids + File(path).takeIf { it.exists() }?.run { + this.delete() + } + } + } })