Skip to content

Commit 334e367

Browse files
committed
Improve validation of VP for JWT VC
1 parent c8e9c66 commit 334e367

7 files changed

Lines changed: 100 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
Release 5.9.0 (unreleased):
4+
- Validation:
5+
- Improve validation of JWT VC
6+
37
Release 5.8.0:
48
- Refactor `AuthorizationServiceStrategy`
59
- Allow for general AuthorizationDetails

vck/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/ValidatorVcJws.kt

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,15 @@ import at.asitplus.wallet.lib.agent.Verifier.VerifyCredentialResult.*
77
import at.asitplus.wallet.lib.agent.Verifier.VerifyPresentationResult
88
import at.asitplus.wallet.lib.agent.validation.vcJws.VcJwsInputValidationResult
99
import at.asitplus.wallet.lib.agent.validation.vcJws.VcJwsInputValidator
10-
import at.asitplus.wallet.lib.data.*
1110
import at.asitplus.wallet.lib.data.VcDataModelConstants.VERIFIABLE_PRESENTATION
12-
import at.asitplus.wallet.lib.jws.*
11+
import at.asitplus.wallet.lib.data.VcJwsVerificationResultWrapper
12+
import at.asitplus.wallet.lib.data.VerifiableCredentialJws
13+
import at.asitplus.wallet.lib.data.VerifiablePresentationJws
14+
import at.asitplus.wallet.lib.data.VerifiablePresentationParsed
15+
import at.asitplus.wallet.lib.jws.VerifyJwsObject
16+
import at.asitplus.wallet.lib.jws.VerifyJwsObjectFun
17+
import at.asitplus.wallet.lib.jws.VerifyJwsSignature
18+
import at.asitplus.wallet.lib.jws.VerifyJwsSignatureFun
1319
import io.github.aakira.napier.Napier
1420
import kotlin.coroutines.cancellation.CancellationException
1521

@@ -55,7 +61,7 @@ class ValidatorVcJws(
5561
}
5662
val vpJws = input.payload.validate(challenge, clientId)
5763
val vcValidationResults = vpJws.vp.verifiableCredential
58-
.map { it to verifyVcJws(it, null) }
64+
.map { it to verifyVcJws(it, null, input) }
5965

6066
val invalidVcList = vcValidationResults.filter {
6167
it.second !is SuccessJwt
@@ -121,24 +127,28 @@ class ValidatorVcJws(
121127
*
122128
* @param input JWS in compact representation
123129
* @param publicKey Optionally, the local key, to verify VC was issued to the correct subject
130+
* @param vpJws Optionally, the VP enclosing the VC
124131
*/
125132
suspend fun verifyVcJws(
126133
input: JwsSigned<VerifiableCredentialJws>,
127134
publicKey: CryptoPublicKey?,
128-
) = verifyVcJws(input.serialize(), publicKey)
135+
vpJws: JwsSigned<VerifiablePresentationJws>? = null
136+
) = verifyVcJws(input.serialize(), publicKey, vpJws)
129137

130138
/**
131139
* Validates the content of a JWS, expected to contain a Verifiable Credential.
132140
*
133141
* @param input JWS in compact representation
134142
* @param publicKey Optionally, the local key, to verify VC was issued to the correct subject
143+
* @param vpJws Optionally, the VP enclosing the VC
135144
*/
136145
suspend fun verifyVcJws(
137146
input: String,
138147
publicKey: CryptoPublicKey?,
148+
vpJws: JwsSigned<VerifiablePresentationJws>? = null
139149
): VerifyCredentialResult {
140150
Napier.d("Validating VC-JWS $input")
141-
val validationSummary = vcJwsInputValidator(input, publicKey)
151+
val validationSummary = vcJwsInputValidator(input, publicKey, vpJws)
142152
return when {
143153
validationSummary !is VcJwsInputValidationResult.ContentValidationSummary -> InvalidStructure(
144154
input = input,

vck/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/validation/vcJws/VcJwsInputValidationResult.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ sealed interface VcJwsInputValidationResult {
2121
val isIntegrityGood: Boolean,
2222
val subjectMatchingResult: SubjectMatchingResult?,
2323
val contentSemanticsValidationSummary: VcJwsContentSemanticsValidationSummary,
24+
val vpMappingValidationSummary: VcJwsToVpJwsMappingValidationSummary?,
2425
) : VcJwsInputValidationResult {
2526
val payload: VerifiableCredentialJws
2627
get() = parsed.payload
@@ -30,6 +31,7 @@ sealed interface VcJwsInputValidationResult {
3031
isIntegrityGood,
3132
subjectMatchingResult?.isSuccess != false,
3233
contentSemanticsValidationSummary.isSuccess,
34+
vpMappingValidationSummary?.isSuccess != false,
3335
).all { it }
3436
}
3537
}

vck/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/validation/vcJws/VcJwsInputValidator.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,20 @@ import at.asitplus.signum.indispensable.josef.JwsSigned
55
import at.asitplus.wallet.lib.agent.matchesIdentifier
66
import at.asitplus.wallet.lib.agent.validation.common.SubjectMatchingResult
77
import at.asitplus.wallet.lib.data.VerifiableCredentialJws
8+
import at.asitplus.wallet.lib.data.VerifiablePresentationJws
89
import at.asitplus.wallet.lib.data.vckJsonSerializer
910
import at.asitplus.wallet.lib.jws.VerifyJwsObject
1011
import at.asitplus.wallet.lib.jws.VerifyJwsObjectFun
1112

1213
data class VcJwsInputValidator(
1314
val vcJwsContentSemanticsValidator: VcJwsContentSemanticsValidator = VcJwsContentSemanticsValidator(),
15+
val vpJwsMapsToVpJwsValidator: VcJwsToVpJwsMappingValidator = VcJwsToVpJwsMappingValidator(),
1416
val verifyJwsObject: VerifyJwsObjectFun = VerifyJwsObject(),
1517
) {
1618
suspend operator fun invoke(
1719
input: String,
1820
publicKey: CryptoPublicKey?,
21+
vpJws: JwsSigned<VerifiablePresentationJws>?,
1922
): VcJwsInputValidationResult {
2023
val jws = JwsSigned.deserialize<VerifiableCredentialJws>(
2124
VerifiableCredentialJws.serializer(),
@@ -38,6 +41,7 @@ data class VcJwsInputValidator(
3841
)
3942
},
4043
contentSemanticsValidationSummary = vcJwsContentSemanticsValidator.invoke(vcJws),
44+
vpMappingValidationSummary = vpJws?.let { vpJwsMapsToVpJwsValidator.invoke(vcJws, vpJws) }
4145
)
4246
}
4347
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package at.asitplus.wallet.lib.agent.validation.vcJws
2+
3+
import at.asitplus.signum.indispensable.CryptoPublicKey
4+
5+
data class VcJwsToVpJwsMappingValidationSummary(
6+
val inconsistentIssuerError: InconsistentIssuerError?,
7+
val inconsistentPublicKeyError: InconsistentPublicKeyError?,
8+
) {
9+
val isSuccess = listOf(
10+
inconsistentIssuerError == null,
11+
inconsistentPublicKeyError == null,
12+
).all { it }
13+
14+
data class InconsistentIssuerError(
15+
val vcSubject: String,
16+
val vpIssuer: String,
17+
)
18+
data class InconsistentPublicKeyError(
19+
val vcSubject: String,
20+
val vpPublicKey: CryptoPublicKey,
21+
)
22+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package at.asitplus.wallet.lib.agent.validation.vcJws
2+
3+
import at.asitplus.signum.indispensable.josef.JwsSigned
4+
import at.asitplus.wallet.lib.agent.matchesIdentifier
5+
import at.asitplus.wallet.lib.data.VerifiableCredentialJws
6+
import at.asitplus.wallet.lib.data.VerifiablePresentationJws
7+
import io.github.aakira.napier.Napier
8+
9+
class VcJwsToVpJwsMappingValidator {
10+
operator fun invoke(
11+
vcJws: VerifiableCredentialJws,
12+
vpJws: JwsSigned<VerifiablePresentationJws>,
13+
) = VcJwsToVpJwsMappingValidationSummary(
14+
inconsistentIssuerError = if (vpJws.payload.issuer != vcJws.subject) {
15+
Napier.w("vp.iss invalid: ${vpJws.payload.issuer}, but in VC is ${vcJws.subject}")
16+
VcJwsToVpJwsMappingValidationSummary.InconsistentIssuerError(
17+
vcSubject = vcJws.subject,
18+
vpIssuer = vpJws.payload.issuer,
19+
)
20+
} else null,
21+
inconsistentPublicKeyError = vpJws.header.publicKey?.let {
22+
if (!vpJws.header.publicKey!!.matchesIdentifier(vcJws.subject)) {
23+
Napier.w("vp.key invalid: ${vpJws.header.publicKey}, but in VC is ${vcJws.subject}")
24+
VcJwsToVpJwsMappingValidationSummary.InconsistentPublicKeyError(
25+
vcSubject = vcJws.subject,
26+
vpPublicKey = vpJws.header.publicKey!!,
27+
)
28+
} else null
29+
}
30+
).also {
31+
if (it.isSuccess) {
32+
Napier.d("VC to VP mapping is valid")
33+
}
34+
}
35+
}
36+

vck/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ValidatorVpTest.kt

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import at.asitplus.wallet.lib.jws.SignJwtFun
1818
import com.benasher44.uuid.uuid4
1919
import io.kotest.core.spec.style.FreeSpec
2020
import io.kotest.matchers.collections.shouldBeEmpty
21+
import io.kotest.matchers.collections.shouldBeSingleton
2122
import io.kotest.matchers.collections.shouldHaveSize
2223
import io.kotest.matchers.nulls.shouldNotBeNull
2324
import io.kotest.matchers.shouldBe
@@ -109,14 +110,25 @@ class ValidatorVpTest : FreeSpec({
109110
.shouldBeInstanceOf<VerifyPresentationResult.Success>()
110111
}
111112

112-
"wrong structure of VC is detected" {
113-
val holderCredentials = holder.getCredentials()
114-
holderCredentials.shouldNotBeNull()
115-
val holderVcSerialized = holderCredentials
113+
"Presentation of VC from different holder is detected" {
114+
val otherHolderKeyMaterial = EphemeralKeyWithoutCert()
115+
val otherHolder = HolderAgent(otherHolderKeyMaterial)
116+
otherHolder.storeCredential(
117+
issuer.issueCredential(
118+
DummyCredentialDataProvider.getCredential(
119+
otherHolderKeyMaterial.publicKey,
120+
ConstantIndex.AtomicAttribute2023,
121+
PLAIN_JWT,
122+
).getOrThrow()
123+
).getOrThrow().toStoreCredentialInput()
124+
).getOrThrow()
125+
val holderVc = otherHolder.getCredentials()
126+
.shouldNotBeNull()
127+
.shouldBeSingleton()
116128
.filterIsInstance<SubjectCredentialStore.StoreEntry.Vc>()
129+
val holderVcSerialized = holderVc
117130
.map { it.vcSerialized }
118131
.map { it.reversed() }
119-
120132
val vp = verifiablePresentationFactory.createVcPresentation(
121133
holderVcSerialized,
122134
PresentationRequestParameters(nonce = challenge, audience = verifierId)

0 commit comments

Comments
 (0)