Skip to content

Commit

Permalink
feat: status-list-2021-entry revocation full support (#295)
Browse files Browse the repository at this point in the history
  • Loading branch information
mikeplotean authored May 2, 2023
2 parents 3971e89 + 2347d6d commit 2584b2c
Show file tree
Hide file tree
Showing 23 changed files with 724 additions and 251 deletions.
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
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
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.*

Expand Down Expand Up @@ -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<CredentialStatusCredential>(vc.toJson())!!.credentialStatus!!.let {
when (it) {
is SimpleCredentialStatus2022 -> checkSimpleRevocation(it)
is StatusList2021EntryCredentialStatus -> checkStatusListRevocation(it)
Klaxon().parse<CredentialStatusCredential>(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<String>, val applyToVC: Boolean = true, val applyToVP: Boolean = true)
Expand Down
82 changes: 67 additions & 15 deletions src/main/kotlin/id/walt/cli/VcCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -74,6 +77,10 @@ class VcIssueCommand : CliktCommand(
val ecosystem: Ecosystem by option("--ecosystem", help = "Specify ecosystem, for specific defaults of issuing parameters")
.enum<Ecosystem>()
.default(Ecosystem.DEFAULT)
val statusType: CredentialStatus.Types? by option(
"--status-type",
help = "Specify the credentialStatus type"
).enum<CredentialStatus.Types>()

private val signatory = Signatory.getService()

Expand All @@ -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) {
Expand Down Expand Up @@ -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"
) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
10 changes: 8 additions & 2 deletions src/main/kotlin/id/walt/cli/WaltCLI.kt
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,11 @@ object WaltCLI {
VcTemplatesImportCommand(),
VcTemplatesRemoveCommand()
),
VcImportCommand()
VcImportCommand(),
VcRevocationCommand().subcommands(
VcRevocationCheckCommand(),
VcRevocationRevokeCommand(),
),
),
EssifCommand().subcommands(
EssifOnboardingCommand(),
Expand Down Expand Up @@ -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()
}
}
}
}
52 changes: 51 additions & 1 deletion src/main/kotlin/id/walt/common/CommonUtils.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
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.nio.charset.StandardCharsets
import java.util.*
import java.util.zip.*
import kotlin.reflect.full.memberProperties

fun resolveContent(fileUrlContent: String): String {
val file = File(fileUrlContent)
Expand Down Expand Up @@ -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 {
Expand All @@ -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 <reified T : Any> T.asMap() : Map<String, Any?> {
val props = T::class.memberProperties.associateBy { it.name }
return props.keys.associateWith { props[it]?.get(this) }
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,55 @@
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
import kotlinx.serialization.json.jsonObject
import java.time.Instant
import java.time.format.DateTimeFormatterBuilder

class W3CCredentialBuilderWithCredentialStatus<C : VerifiableCredential, B : AbstractW3CCredentialBuilder<C, B>>(
private val builder: AbstractW3CCredentialBuilder<C, B>,
private val proofConfig: ProofConfig,
):AbstractW3CCredentialBuilder<VerifiableCredential, W3CCredentialBuilder>(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<String> = listOf("VerifiableCredential")) :
AbstractW3CCredentialBuilder<VerifiableCredential, W3CCredentialBuilder>(type, VerifiableCredential) {
companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,24 @@ 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(
override val id: String,
val statusPurpose: String,
val statusListIndex: String,
val statusListCredential: String,
) : CredentialStatus("StatusList2021Entry")
) : CredentialStatus(Types.StatusList2021Entry.name)

class CredentialStatusTypeAdapter : TypeAdapter<CredentialStatus> {
override fun classFor(type: Any): KClass<out CredentialStatus> = when (type as String) {
Expand Down
Loading

0 comments on commit 2584b2c

Please sign in to comment.