Skip to content

Commit

Permalink
feat: status list 2021 entry - check revocation (#294)
Browse files Browse the repository at this point in the history
  • Loading branch information
mikeplotean authored Apr 21, 2023
2 parents a2cc57c + 6ef76f9 commit 31a84c0
Show file tree
Hide file tree
Showing 20 changed files with 337 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import id.walt.auditor.SimpleVerificationPolicy
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 java.text.SimpleDateFormat
import java.util.*
Expand Down Expand Up @@ -47,35 +51,41 @@ class ExpirationDateAfterPolicy : SimpleVerificationPolicy() {

class CredentialStatusPolicy : SimpleVerificationPolicy() {

@Serializable
data class CredentialStatus(
val id: String,
var type: String
)

@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 {
val cs = Klaxon().parse<CredentialStatusCredential>(vc.toJson())!!.credentialStatus!!
override fun doVerify(vc: VerifiableCredential): VerificationPolicyResult = runCatching {
Klaxon().parse<CredentialStatusCredential>(vc.toJson())!!.credentialStatus!!.let {
when (it) {
is SimpleCredentialStatus2022 -> checkSimpleRevocation(it)
is StatusList2021EntryCredentialStatus -> checkStatusListRevocation(it)
}
}
}.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}."))
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)
}

return when (cs.type) {
"SimpleCredentialStatus2022" -> {
val rs = RevocationClientService.getService()
val result = rs.checkRevoked(cs.id)
revocationVerificationPolicy(result.isRevoked, result.timeOfRevocation)
}
"StatusList2021Credential" -> VerificationPolicyResult.success()//TODO: implement
"CredentialStatusList2020" -> VerificationPolicyResult.success()//TODO: implement
else -> VerificationPolicyResult.failure(UnsupportedOperationException("CredentialStatus type \"${cs.type}\" is not yet supported."))
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<String>, val applyToVC: Boolean = true, val applyToVP: Boolean = true)
Expand Down
35 changes: 35 additions & 0 deletions src/main/kotlin/id/walt/common/CommonUtils.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
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
import org.apache.commons.codec.digest.DigestUtils
import org.bouncycastle.util.encoders.Base32
import java.io.ByteArrayOutputStream
import java.io.File
import java.util.*
import java.util.zip.*

fun resolveContent(fileUrlContent: String): String {
val file = File(fileUrlContent)
Expand Down Expand Up @@ -35,3 +41,32 @@ fun resolveContentToFile(fileUrlContent: String, tempPrefix: String = "TEMP", te
}
return fileCheck
}

fun compressGzip(data: ByteArray): ByteArray {
val result = ByteArrayOutputStream()
GZIPOutputStream(result).use {
it.write(data)
}
return result.toByteArray()
}

fun uncompressGzip(data: ByteArray, idx: ULong? = null) =
GZIPInputStream(data.inputStream()).bufferedReader().use {
idx?.let { index ->
var int = it.read()
var count = 0U
var char = int.toChar()
while (int != -1 && count++ <= index) {
char = int.toChar()
int = it.read()
}
char
}?.let {
val array = CharArray(1)
array[0] = it
array
} ?: it.readText().toCharArray()
}

fun createBaseToken() = UUID.randomUUID().toString() + UUID.randomUUID().toString()
fun deriveRevocationToken(baseToken: String): String = Base32.toBase32String(DigestUtils.sha256(baseToken)).replace("=", "")
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package id.walt.model.credential.status

import com.beust.klaxon.TypeAdapter
import com.beust.klaxon.TypeFor
import kotlinx.serialization.Serializable
import kotlin.reflect.KClass

@Serializable
@TypeFor(field = "type", adapter = CredentialStatusTypeAdapter::class)
sealed class CredentialStatus(
val type: String,
) {
abstract val id: String
}

@Serializable
class SimpleCredentialStatus2022(
override val id: String,
) : CredentialStatus("SimpleCredentialStatus2022")

@Serializable
data class StatusList2021EntryCredentialStatus(
override val id: String,
val statusPurpose: String,
val statusListIndex: String,
val statusListCredential: String,
) : CredentialStatus("StatusList2021Entry")

class CredentialStatusTypeAdapter : TypeAdapter<CredentialStatus> {
override fun classFor(type: Any): KClass<out CredentialStatus> = when (type as String) {
"SimpleCredentialStatus2022" -> SimpleCredentialStatus2022::class
"StatusList2021Entry" -> StatusList2021EntryCredentialStatus::class
else -> throw IllegalArgumentException("CredentialStatus type is not supported: $type")
}
}
2 changes: 1 addition & 1 deletion src/main/kotlin/id/walt/services/did/DidService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,7 @@ object DidService {

fun isDidEbsiV2(did: String): Boolean = checkIsDidEbsiAndVersion(did, 2)

fun checkIsDidEbsiAndVersion(did: String, version: Int): Boolean {
private fun checkIsDidEbsiAndVersion(did: String, version: Int): Boolean {
return DidUrl.isDidUrl(did) &&
DidUrl.from(did).let { didUrl ->
didUrl.method == DidMethod.ebsi.name && Multibase.decode(didUrl.identifier).first().toInt() == version
Expand Down
13 changes: 3 additions & 10 deletions src/main/kotlin/id/walt/signatory/RevocationClientService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package id.walt.signatory
import id.walt.servicematrix.ServiceProvider
import id.walt.services.WaltIdService
import id.walt.services.WaltIdServices
import id.walt.signatory.RevocationService.RevocationResult
import id.walt.signatory.revocation.TokenRevocationResult
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.*
Expand All @@ -14,19 +14,15 @@ import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.runBlocking
import mu.KotlinLogging
import java.util.*

open class RevocationClientService : WaltIdService() {
override val implementation get() = serviceImplementation<RevocationClientService>()

open fun checkRevoked(revocationCheckUrl: String): RevocationResult =
open fun checkRevoked(revocationCheckUrl: String): TokenRevocationResult =
implementation.checkRevoked(revocationCheckUrl)

open fun revoke(baseTokenUrl: String): Unit = implementation.revoke(baseTokenUrl)

open fun createBaseToken(): String = implementation.createBaseToken()
open fun deriveRevocationToken(baseToken: String): String = implementation.deriveRevocationToken(baseToken)

companion object : ServiceProvider {
override fun getService() = object : RevocationClientService() {}
override fun defaultImplementation() = WaltIdRevocationClientService()
Expand Down Expand Up @@ -55,7 +51,7 @@ class WaltIdRevocationClientService : RevocationClientService() {
}
}

override fun checkRevoked(revocationCheckUrl: String): RevocationResult = runBlocking {
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.")

Expand All @@ -72,7 +68,4 @@ class WaltIdRevocationClientService : RevocationClientService() {
http.post(baseTokenUrl)
}
}

override fun createBaseToken() = UUID.randomUUID().toString() + UUID.randomUUID().toString()
override fun deriveRevocationToken(baseToken: String): String = RevocationService.getRevocationToken(baseToken)
}
9 changes: 5 additions & 4 deletions src/main/kotlin/id/walt/signatory/rest/SignatoryController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import id.walt.credentials.w3c.VerifiableCredential
import id.walt.credentials.w3c.builder.W3CCredentialBuilder
import id.walt.signatory.ProofConfig
import id.walt.signatory.ProofType
import id.walt.signatory.RevocationService
import id.walt.signatory.Signatory
import id.walt.signatory.dataproviders.MergingDataProvider
import id.walt.signatory.revocation.SimpleCredentialStatus2022Service
import id.walt.signatory.revocation.TokenRevocationResult
import io.javalin.http.BadRequestResponse
import io.javalin.http.ContentType
import io.javalin.http.Context
Expand Down Expand Up @@ -131,10 +132,10 @@ object SignatoryController {
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<RevocationService.RevocationResult>("200")
}.json<TokenRevocationResult>("200")

fun checkRevoked(ctx: Context) {
ctx.json(RevocationService.checkRevoked(ctx.pathParam("id")))
ctx.json(SimpleCredentialStatus2022Service.checkRevoked(ctx.pathParam("id")))
}

fun revokeDocs() = document().operation {
Expand All @@ -143,7 +144,7 @@ object SignatoryController {
}.result<String>("201")

fun revoke(ctx: Context) {
RevocationService.revokeToken(ctx.pathParam("id"))
SimpleCredentialStatus2022Service.revokeToken(ctx.pathParam("id"))
ctx.status(201)
}
}
52 changes: 52 additions & 0 deletions src/main/kotlin/id/walt/signatory/revocation/RevocationService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package id.walt.signatory.revocation

import com.beust.klaxon.Json
import id.walt.model.credential.status.StatusList2021EntryCredentialStatus
import kotlinx.serialization.Serializable

interface RevocationService {
fun checkRevocation(parameter: RevocationParameter): RevocationResult
fun getRevocation(): RevocationData
fun clearAll()
fun setRevocation(parameter: RevocationParameter)
}

data class RevocationList(val revokedList: List<RevocationResult>)

/*
Revocation results
*/
@Serializable
abstract class RevocationResult {
abstract val isRevoked: Boolean
}

@Serializable
data class TokenRevocationResult(
val token: String,
override val isRevoked: Boolean,
@Json(serializeNull = false)
val timeOfRevocation: Long? = null
) : RevocationResult()

@Serializable
data class StatusListRevocationResult(
override val isRevoked: Boolean
) : RevocationResult()

/*
Revocation parameters
*/
interface RevocationParameter
data class TokenRevocationParameter(
val token: String,
) : RevocationParameter

data class StatusListRevocationParameter(
val credentialStatus: StatusList2021EntryCredentialStatus,
) : RevocationParameter

/*
Revocation data
*/
interface RevocationData
Original file line number Diff line number Diff line change
@@ -1,33 +1,21 @@
package id.walt.signatory
package id.walt.signatory.revocation

import com.beust.klaxon.Json
import com.beust.klaxon.Klaxon
import kotlinx.serialization.Serializable
import org.apache.commons.codec.digest.DigestUtils
import org.bouncycastle.util.encoders.Base32.toBase32String
import id.walt.common.deriveRevocationToken
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 RevocationService {
object SimpleCredentialStatus2022Service {

private val klaxon = Klaxon()
private val revokedPath = Path("data/revoked.json").apply {
if (!exists())
writeText(klaxon.toJsonString(RevocationList(emptyList())))
}

data class RevocationList(val revokedList: List<RevocationResult>)

@Serializable
data class RevocationResult(
val token: String,
val isRevoked: Boolean,
@Json(serializeNull = false) val timeOfRevocation: Long? = null
)

private fun getRevokedList() = klaxon.parse<RevocationList>(revokedPath.readText())!!.revokedList
private fun setRevokedList(revoked: RevocationList) = revokedPath.writeText(klaxon.toJsonString(revoked))

Expand All @@ -37,17 +25,15 @@ object RevocationService {
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.token == token } ?: return RevocationResult(token, false)
return getRevokedList().firstOrNull { (it as? TokenRevocationResult)?.token == token } ?: return TokenRevocationResult(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 = getRevocationToken(baseToken)
val token = deriveRevocationToken(baseToken)
val revoked = getRevokedList().toMutableList().apply {
add(RevocationResult(token, true, Instant.now().toEpochMilli()))
add(TokenRevocationResult(token, true, Instant.now().toEpochMilli()))
}
setRevokedList(RevocationList(revoked))
}

fun getRevocationToken(baseToken: String) = toBase32String(DigestUtils.sha256(baseToken)).replace("=", "")
}
Loading

0 comments on commit 31a84c0

Please sign in to comment.