Skip to content

Commit

Permalink
Merge pull request #244 from tdiesler/ghi243
Browse files Browse the repository at this point in the history
[#243] Policy verification silently swallows verification errors
  • Loading branch information
severinstampler authored Mar 22, 2023
2 parents d1b92ca + c8b39ca commit 6677a9a
Show file tree
Hide file tree
Showing 27 changed files with 202 additions and 184 deletions.
29 changes: 17 additions & 12 deletions src/main/kotlin/id/walt/auditor/Auditor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import id.walt.credentials.w3c.toVerifiableCredential
import id.walt.servicematrix.ServiceProvider
import id.walt.services.WaltIdService
import mu.KotlinLogging
import java.util.concurrent.atomic.*


private val log = KotlinLogging.logger {}
Expand All @@ -14,7 +15,7 @@ private val log = KotlinLogging.logger {}
abstract class Auditor : WaltIdService() {
override val implementation: Auditor get() = serviceImplementation()

protected fun allAccepted(policyResults: Map<String, Boolean>) = policyResults.values.all { it }
protected fun allAccepted(policyResults: Map<String, VerificationPolicyResult>) = policyResults.values.all { it.isSuccess }

open fun verify(vc: VerifiableCredential, policies: List<VerificationPolicy>): VerificationResult =
implementation.verify(vc, policies)
Expand All @@ -36,19 +37,23 @@ class WaltIdAuditor : Auditor() {

override fun verify(vc: VerifiableCredential, policies: List<VerificationPolicy>): VerificationResult {

val policyResults = policies
.associateBy(keySelector = VerificationPolicy::id) { policy ->
log.debug { "Verifying vc with ${policy.id} ..." }

policy.verify(vc) && when (vc) {
is VerifiablePresentation -> vc.verifiableCredential?.all { cred ->
log.debug { "Verifying ${cred.type.last()} in VP with ${policy.id}..." }
policy.verify(cred)
} ?: true

else -> true
val policyResults = policies.associateBy(keySelector = VerificationPolicy::id) { policy ->
log.debug { "Verifying vc with ${policy.id} ..." }

val vcResult = policy.verify(vc)
val success = AtomicBoolean(vcResult.outcome)
val allErrors = vcResult.errors.toMutableList()
if (allErrors.isEmpty() && vc is VerifiablePresentation) {
vc.verifiableCredential?.forEach { cred ->
log.debug { "Verifying ${cred.type.last()} in VP with ${policy.id}..." }
val vpResult = policy.verify(cred)
allErrors.addAll(vpResult.errors)
success.compareAndSet(true, vpResult.outcome)
}
}
allErrors.forEach { log.error { "${policy.id}: $it" } }
VerificationPolicyResult(success.get(), allErrors)
}

return VerificationResult(allAccepted(policyResults), policyResults)
}
Expand Down
131 changes: 74 additions & 57 deletions src/main/kotlin/id/walt/auditor/VerificationPolicy.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import id.walt.signatory.RevocationClientService
import io.ktor.client.plugins.*
import kotlinx.serialization.Serializable
import mu.KotlinLogging
import java.net.URI
import java.text.SimpleDateFormat
import java.util.*

Expand All @@ -37,20 +36,39 @@ data class VerificationPolicyMetadata(
val isMutable: Boolean
)

class VerificationPolicyResult(val outcome: Boolean, val errors: List<Any> = listOf()) {
companion object {
fun success() = VerificationPolicyResult(true)
fun failure(error: Any) = VerificationPolicyResult(false, listOf(error))
fun failure(errors: List<Any> = listOf()) = VerificationPolicyResult(false, errors.toList())
}
val isSuccess = outcome
val isFailure = !outcome

override fun toString(): String {
return when(outcome) {
true -> "true"
false -> "false $errors"
}
}
}

abstract class VerificationPolicy {
open val id: String
get() = this.javaClass.simpleName
abstract val description: String
protected abstract fun doVerify(vc: VerifiableCredential): Boolean
open val applyToVC: Boolean = true
open val applyToVP: Boolean = true

fun verify(vc: VerifiableCredential) = when {
vc is VerifiablePresentation && applyToVP
|| vc !is VerifiablePresentation && applyToVC -> doVerify(vc)

else -> true
}.also { log.debug { "VC ${vc.type} passes policy $id: $it" } }
protected abstract fun doVerify(vc: VerifiableCredential): VerificationPolicyResult
open val applyToVC = true
open val applyToVP = true

fun verify(vc: VerifiableCredential): VerificationPolicyResult {
val verifyPresentation = vc is VerifiablePresentation && applyToVP
val verifyCredential = vc !is VerifiablePresentation && applyToVC
return when {
verifyPresentation || verifyCredential -> doVerify(vc)
else -> VerificationPolicyResult.success()
}.also { log.debug { "VC ${vc.type} passes policy $id: $it" } }
}
}

abstract class SimpleVerificationPolicy : VerificationPolicy()
Expand All @@ -63,13 +81,13 @@ class SignaturePolicy : SimpleVerificationPolicy() {
override val description: String = "Verify by signature"
override fun doVerify(vc: VerifiableCredential) = runCatching {
log.debug { "is jwt: ${vc.jwt != null}" }
when (vc.jwt) {
VerificationPolicyResult(when (vc.jwt) {
null -> jsonLdCredentialService.verify(vc.encode()).verified
else -> jwtCredentialService.verify(vc.encode()).verified
}
})
}.onFailure {
log.error(it.localizedMessage)
}.getOrDefault(false)
}.getOrDefault(VerificationPolicyResult.failure())
}

/**
Expand All @@ -81,10 +99,10 @@ class JsonSchemaPolicy(schemaPolicyArg: JsonSchemaPolicyArg?) : OptionalParamete
constructor() : this(null)

override val description: String = "Verify by JSON schema"
override fun doVerify(vc: VerifiableCredential): Boolean {
override fun doVerify(vc: VerifiableCredential): VerificationPolicyResult {
return (argument?.schema ?: vc.credentialSchema?.id)?.let {
SchemaValidatorFactory.get(it).validate(vc.toJson())
} ?: false
} ?: VerificationPolicyResult.failure()
}

override val applyToVP: Boolean
Expand All @@ -101,12 +119,12 @@ class TrustedSchemaRegistryPolicy : SimpleVerificationPolicy() {

class TrustedIssuerDidPolicy : SimpleVerificationPolicy() {
override val description: String = "Verify by trusted issuer did"
override fun doVerify(vc: VerifiableCredential): Boolean {
override fun doVerify(vc: VerifiableCredential): VerificationPolicyResult {
return try {
DidService.loadOrResolveAnyDid(vc.issuerId!!) != null
VerificationPolicyResult(DidService.loadOrResolveAnyDid(vc.issuerId!!) != null)
} catch (e: ClientRequestException) {
if (!e.message.contains("did must be a valid DID") && !e.message.contains("Identifier Not Found")) throw e
false
VerificationPolicyResult.failure()
}
}
}
Expand All @@ -126,11 +144,11 @@ class TrustedIssuerRegistryPolicy(registryArg: TrustedIssuerRegistryPolicyArg) :
)

override val description: String = "Verify by an EBSI Trusted Issuers Registry compliant api."
override fun doVerify(vc: VerifiableCredential): Boolean {
override fun doVerify(vc: VerifiableCredential): VerificationPolicyResult {

// VPs are not considered
if (vc is VerifiablePresentation)
return true
return VerificationPolicyResult.success()

val issuerDid = vc.issuerId!!

Expand All @@ -139,19 +157,19 @@ class TrustedIssuerRegistryPolicy(registryArg: TrustedIssuerRegistryPolicyArg) :

if (resolvedIssuerDid.id != issuerDid) {
log.debug { "Resolved DID ${resolvedIssuerDid.id} does not match the issuer DID $issuerDid" }
return false
return VerificationPolicyResult.failure()
}
var tirRecord: TrustedIssuer


runCatching {
return VerificationPolicyResult(runCatching {
tirRecord = TrustedIssuerClient.getIssuer(issuerDid, argument.registryAddress)
return isValidTrustedIssuerRecord(tirRecord)
isValidTrustedIssuerRecord(tirRecord)
}.getOrElse {
log.debug { it }
log.warn { "Could not resolve issuer TIR record of $issuerDid" }
return false
}
false
})
}

private fun isValidTrustedIssuerRecord(tirRecord: TrustedIssuer): Boolean {
Expand All @@ -171,46 +189,46 @@ class TrustedIssuerRegistryPolicy(registryArg: TrustedIssuerRegistryPolicyArg) :

class TrustedSubjectDidPolicy : SimpleVerificationPolicy() {
override val description: String = "Verify by trusted subject did"
override fun doVerify(vc: VerifiableCredential): Boolean {
return vc.subjectId?.let {
override fun doVerify(vc: VerifiableCredential): VerificationPolicyResult {
return VerificationPolicyResult(vc.subjectId?.let {
if (it.isEmpty()) true
else try {
DidService.loadOrResolveAnyDid(it) != null
} catch (e: ClientRequestException) {
if (!e.message.contains("did must be a valid DID") && !e.message.contains("Identifier Not Found")) throw e
false
}
} ?: false
} ?: false)
}
}

class IssuedDateBeforePolicy : SimpleVerificationPolicy() {
override val description: String = "Verify by issuance date"
override fun doVerify(vc: VerifiableCredential): Boolean {
return when (vc) {
override fun doVerify(vc: VerifiableCredential): VerificationPolicyResult {
return VerificationPolicyResult(when (vc) {
is VerifiablePresentation -> true
else -> parseDate(vc.issued).let { it != null && it.before(Date()) }
}
})
}
}

class ValidFromBeforePolicy : SimpleVerificationPolicy() {
override val description: String = "Verify by valid from"
override fun doVerify(vc: VerifiableCredential): Boolean {
return when (vc) {
override fun doVerify(vc: VerifiableCredential): VerificationPolicyResult {
return VerificationPolicyResult(when (vc) {
is VerifiablePresentation -> true
else -> parseDate(vc.validFrom).let { it != null && it.before(Date()) }
}
})
}
}

class ExpirationDateAfterPolicy : SimpleVerificationPolicy() {
override val description: String = "Verify by expiration date"
override fun doVerify(vc: VerifiableCredential): Boolean {
return when (vc) {
override fun doVerify(vc: VerifiableCredential): VerificationPolicyResult {
return VerificationPolicyResult(when (vc) {
is VerifiablePresentation -> true
else -> parseDate(vc.expirationDate).let { it == null || it.after(Date()) }
}
})
}
}

Expand All @@ -228,22 +246,20 @@ class CredentialStatusPolicy : SimpleVerificationPolicy() {
)

override val description: String = "Verify by credential status"
override fun doVerify(vc: VerifiableCredential): Boolean {
override fun doVerify(vc: VerifiableCredential): VerificationPolicyResult {
val cs = Klaxon().parse<CredentialStatusCredential>(vc.toJson())!!.credentialStatus!!

when (cs.type) {
return VerificationPolicyResult(when (cs.type) {
"SimpleCredentialStatus2022" -> {
val rs = RevocationClientService.getService()

val result = rs.checkRevoked(cs.id)

return !result.isRevoked
!result.isRevoked
}

else -> {
throw IllegalArgumentException("CredentialStatus type \"\"")
}
}
})
}
}

Expand All @@ -260,8 +276,8 @@ class ChallengePolicy(challengeArg: ChallengePolicyArg) :
)

override val description: String = "Verify challenge"
override fun doVerify(vc: VerifiableCredential): Boolean {
return vc.challenge?.let { argument.challenges.contains(it) } ?: false
override fun doVerify(vc: VerifiableCredential): VerificationPolicyResult {
return VerificationPolicyResult(vc.challenge?.let { argument.challenges.contains(it) } ?: false)
}

override val applyToVC: Boolean
Expand All @@ -274,14 +290,12 @@ class ChallengePolicy(challengeArg: ChallengePolicyArg) :
class PresentationDefinitionPolicy(presentationDefinition: PresentationDefinition) :
ParameterizedVerificationPolicy<PresentationDefinition>(presentationDefinition) {
override val description: String = "Verify that verifiable presentation complies with presentation definition"
override fun doVerify(vc: VerifiableCredential): Boolean {
if (vc is VerifiablePresentation) {
return argument.input_descriptors.all { desc ->
override fun doVerify(vc: VerifiableCredential): VerificationPolicyResult {
return VerificationPolicyResult(if (vc is VerifiablePresentation) {
argument.input_descriptors.all { desc ->
vc.verifiableCredential?.any { cred -> OIDCUtils.matchesInputDescriptor(cred, desc) } ?: false
}
}
// else: nothing to check
return false
} else false)
}

override var applyToVC: Boolean = false
Expand Down Expand Up @@ -316,8 +330,8 @@ class PresentationDefinitionPolicy(presentationDefinition: PresentationDefinitio

class GaiaxSDPolicy : SimpleVerificationPolicy() {
override val description: String = "Verify Gaiax SD fields"
override fun doVerify(vc: VerifiableCredential): Boolean {
return true
override fun doVerify(vc: VerifiableCredential): VerificationPolicyResult {
return VerificationPolicyResult.success()
}
}

Expand All @@ -331,9 +345,12 @@ data class VerificationResult(
/***
* Validation status over all policy results.
*/
val valid: Boolean = false,
val policyResults: Map<String, Boolean>
val outcome: Boolean = false,
val policyResults: Map<String, VerificationPolicyResult>
) {
@Deprecated("Deprecated in favour of: outcome")
val valid: Boolean = outcome

override fun toString() =
"VerificationResult(valid=$valid, policyResults={${policyResults.entries.joinToString { it.key + "=" + it.value }}})"
"VerificationResult(outcome=$outcome, policyResults={${policyResults.entries.joinToString { it.key + "=" + it.value }}})"
}
5 changes: 3 additions & 2 deletions src/main/kotlin/id/walt/auditor/dynamic/DynamicPolicy.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package id.walt.auditor.dynamic

import com.jayway.jsonpath.JsonPath
import id.walt.auditor.ParameterizedVerificationPolicy
import id.walt.auditor.VerificationPolicyResult
import id.walt.credentials.w3c.VerifiableCredential
import mu.KotlinLogging

Expand All @@ -11,7 +12,7 @@ open class DynamicPolicy(dynPolArg: DynamicPolicyArg) : ParameterizedVerificatio
override val id: String
get() = argument.name
override val description = "Verify credential by rego policy"
override fun doVerify(vc: VerifiableCredential): Boolean {
override fun doVerify(vc: VerifiableCredential): VerificationPolicyResult {
val json = vc.toJson()
// params: rego (string, URL, file, credential property), input (json string), data jsonpath (default: $.credentialSubject)
val rego = if (argument.policy.startsWith("$")) {
Expand All @@ -25,7 +26,7 @@ open class DynamicPolicy(dynPolArg: DynamicPolicyArg) : ParameterizedVerificatio
policy = rego,
query = argument.policyQuery
).also {
log.debug { "DYNAMIC POLICY CHECK: VC ${vc.type} passed $it: $it" }
log.debug { "DYNAMIC POLICY CHECK: VC ${vc.type} result: $it" }
log.debug { "Policy: ${argument.policy}" }
}
}
Expand Down
Loading

0 comments on commit 6677a9a

Please sign in to comment.