Skip to content

Commit 4de8aaf

Browse files
authored
Merge pull request #839 from walt-id/wal-313-fix-sd-property-check
Wal 313 fix sd property check
2 parents c58dfba + 2d506c5 commit 4de8aaf

File tree

7 files changed

+132
-43
lines changed

7 files changed

+132
-43
lines changed

waltid-libraries/credentials/waltid-verifiable-credentials/src/commonMain/kotlin/id/walt/credentials/schemes/JwsSignatureScheme.kt

+42-25
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package id.walt.credentials.schemes
22

3+
import id.walt.crypto.exceptions.VerificationException
34
import id.walt.crypto.keys.Key
45
import id.walt.crypto.utils.JsonUtils.toJsonObject
56
import id.walt.crypto.utils.JwsUtils.decodeJws
67
import id.walt.did.dids.DidService
78
import id.walt.did.dids.DidUtils
9+
import id.walt.sdjwt.JWTCryptoProvider
10+
import id.walt.sdjwt.SDJwt
811
import io.github.oshai.kotlinlogging.KotlinLogging
912
import kotlinx.serialization.encodeToString
1013
import kotlinx.serialization.json.Json
@@ -36,6 +39,8 @@ class JwsSignatureScheme : SignatureScheme {
3639
const val VC = "vc"
3740
}
3841

42+
data class KeyInfo(val keyId: String, val key: Key)
43+
3944
fun toPayload(data: JsonObject, jwtOptions: Map<String, JsonElement> = emptyMap()) =
4045
mapOf(
4146
JwsOption.ISSUER to jwtOptions[JwsOption.ISSUER],
@@ -44,6 +49,29 @@ class JwsSignatureScheme : SignatureScheme {
4449
*(jwtOptions.entries.map { it.toPair() }.toTypedArray())
4550
).toJsonObject()
4651

52+
@JvmBlocking
53+
@JvmAsync
54+
@JsPromise
55+
@JsExport.Ignore
56+
suspend fun getIssuerKeyInfo(jws: String): KeyInfo {
57+
val jwsParsed = jws.decodeJws()
58+
val keyId = jwsParsed.header[JwsHeader.KEY_ID]!!.jsonPrimitive.content
59+
val issuerId = (jwsParsed.payload[JwsOption.ISSUER]?.jsonPrimitive?.content ?: keyId)
60+
val key = if (DidUtils.isDidUrl(issuerId)) {
61+
log.trace { "Resolving key from issuer did: $issuerId" }
62+
DidService.resolveToKey(issuerId)
63+
.also {
64+
if (log.isTraceEnabled()) {
65+
val exportedJwk = it.getOrNull()?.getPublicKey()?.exportJWK()
66+
log.trace { "Imported key: $it from did: $issuerId, public is: $exportedJwk" }
67+
}
68+
}
69+
.getOrThrow()
70+
} else
71+
TODO("Issuer IDs other than DIDs are currently not supported for W3C credentials.")
72+
return KeyInfo(keyId, key)
73+
}
74+
4775
/**
4876
* args:
4977
* - kid: Key ID
@@ -73,32 +101,21 @@ class JwsSignatureScheme : SignatureScheme {
73101
@JsPromise
74102
@JsExport.Ignore
75103
suspend fun verify(data: String): Result<JsonElement> = runCatching {
76-
val jws = data.decodeJws()
77-
78-
val header = jws.header
79-
val payload = jws.payload
80-
81-
val issuerDid = (payload[JwsOption.ISSUER] ?: header[JwsHeader.KEY_ID])!!.jsonPrimitive.content
82-
if (DidUtils.isDidUrl(issuerDid)) {
83-
verifyForIssuerDid(issuerDid, data)
84-
} else {
85-
TODO()
86-
}
104+
val keyInfo = getIssuerKeyInfo(data)
105+
return keyInfo.key.verifyJws(data.split("~")[0])
106+
.also { log.trace { "Verification result: $it" } }
87107
}
88108

89-
private suspend fun verifyForIssuerDid(issuerDid: String, data: String): JsonElement {
90-
log.trace { "Verifying with issuer did: $issuerDid" }
91-
92-
return DidService.resolveToKey(issuerDid)
93-
.also {
94-
if (log.isTraceEnabled()) {
95-
val exportedJwk = it.getOrNull()?.getPublicKey()?.exportJWK()
96-
log.trace { "Imported key: $it from did: $issuerDid, public is: $exportedJwk" }
97-
}
98-
}
99-
.getOrThrow()
100-
.verifyJws(data.split("~")[0])
101-
.also { log.trace { "Verification result: $it" } }
102-
.getOrThrow()
109+
@JvmBlocking
110+
@JvmAsync
111+
@JsPromise
112+
@JsExport.Ignore
113+
suspend fun verifySDJwt(data: String, jwtCryptoProvider: JWTCryptoProvider): Result<JsonElement> = runCatching {
114+
return SDJwt.verifyAndParse(data, jwtCryptoProvider).let {
115+
if(it.verified)
116+
Result.success(it.sdJwt.fullPayload)
117+
else
118+
Result.failure(VerificationException(it.message ?: "Verification failed"))
119+
}
103120
}
104121
}

waltid-libraries/credentials/waltid-verification-policies/src/commonMain/kotlin/id/walt/policies/policies/JwtSignaturePolicy.kt

+13-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package id.walt.policies.policies
33
import id.walt.credentials.schemes.JwsSignatureScheme
44
import id.walt.credentials.utils.VCFormat
55
import id.walt.policies.JwtVerificationPolicy
6-
import id.walt.sdjwt.SDJwtVC
6+
import id.walt.sdjwt.SDJwt
77
import kotlinx.serialization.Serializable
88
import love.forte.plugin.suspendtrans.annotation.JsPromise
99
import love.forte.plugin.suspendtrans.annotation.JvmAsync
@@ -26,6 +26,17 @@ class JwtSignaturePolicy : JwtVerificationPolicy(
2626
@JsPromise
2727
@JsExport.Ignore
2828
override suspend fun verify(credential: String, args: Any?, context: Map<String, Any>): Result<Any> {
29-
return JwsSignatureScheme().verify(credential)
29+
return JwsSignatureScheme().let {
30+
if(SDJwt.isSDJwt(credential, sdOnly = true)) {
31+
val keyInfo = it.getIssuerKeyInfo(credential)
32+
it.verifySDJwt(
33+
credential, JWTCryptoProviderManager.getDefaultJWTCryptoProvider(
34+
mapOf(keyInfo.keyId to keyInfo.key)
35+
)
36+
)
37+
}
38+
else
39+
it.verify(credential)
40+
}
3041
}
3142
}

waltid-libraries/credentials/waltid-verification-policies/src/commonMain/kotlin/id/walt/policies/policies/SdJwtVCSignaturePolicy.kt

+7-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package id.walt.policies.policies
33
import id.walt.credentials.schemes.JwsSignatureScheme
44
import id.walt.credentials.utils.VCFormat
55
import id.walt.credentials.utils.randomUUID
6+
import id.walt.crypto.exceptions.VerificationException
67
import id.walt.crypto.keys.Key
78
import id.walt.crypto.keys.jwk.JWKKey
89
import id.walt.crypto.utils.JsonUtils.toJsonElement
@@ -58,7 +59,12 @@ class SdJwtVCSignaturePolicy(): JwtVerificationPolicy() {
5859
requiresHolderKeyBinding = true,
5960
context["clientId"]?.toString(),
6061
context["challenge"]?.toString()
61-
).let { Result.success(sdJwtVC.undisclosedPayload) }
62+
).let {
63+
if(it.verified)
64+
Result.success(sdJwtVC.undisclosedPayload)
65+
else
66+
Result.failure(VerificationException("SD-JWT verification failed"))
67+
}
6268
}
6369
}
6470

waltid-libraries/sdjwt/waltid-sdjwt/src/commonMain/kotlin/id/walt/sdjwt/SDJwt.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ open class SDJwt internal constructor(
145145
* @param jwtCryptoProvider JWT crypto provider, that implements standard JWT token verification on the target platform
146146
*/
147147
fun verify(jwtCryptoProvider: JWTCryptoProvider, keyID: String? = null): VerificationResult<SDJwt> {
148-
return jwtCryptoProvider.verify(jwt, keyID).let {
148+
return jwtCryptoProvider.verify(jwt, keyID ?: this.keyID).let {
149149
VerificationResult(
150150
sdJwt = this,
151151
signatureVerified = it.verified,
@@ -265,8 +265,8 @@ open class SDJwt internal constructor(
265265
/**
266266
* Check the given string, whether it matches the pattern of an SD-JWT
267267
*/
268-
fun isSDJwt(value: String): Boolean {
269-
return Regex(SD_JWT_PATTERN).matches(value)
268+
fun isSDJwt(value: String, sdOnly: Boolean = false): Boolean {
269+
return Regex(SD_JWT_PATTERN).matches(value) && (!sdOnly || value.contains("~"))
270270
}
271271
}
272272
}

waltid-libraries/sdjwt/waltid-sdjwt/src/jvmTest/kotlin/id/walt/sdjwt/SDJwtTestJVM.kt

+21-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import korlibs.crypto.SHA256
88
import korlibs.crypto.encoding.ASCII
99
import kotlinx.datetime.Clock
1010
import kotlinx.serialization.json.*
11+
import kotlin.io.encoding.Base64
12+
import kotlin.io.encoding.ExperimentalEncodingApi
1113
import kotlin.test.*
1214

1315
class SDJwtTestJVM {
@@ -99,13 +101,31 @@ class SDJwtTestJVM {
99101
val isValid = parsedUndisclosedJwt.verify(cryptoProvider).verified
100102
println("Undisclosed SD-JWT verified: $isValid")
101103

104+
val disclosedJwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI0NTYiLCJfc2QiOlsiaGx6ZmpmMDRvNVpzTFIyNWhhNGMtWS05SFcyRFVseGNnaU1ZZDMyNE5nWSJdfQ.2fsLqzujWt0hS0peLS8JLHyyo3D5KCDkNnHcBYqQwVo~WyJ4RFk5VjBtOG43am82ZURIUGtNZ1J3Iiwic3ViIiwiMTIzIl0~"
102105
val parsedDisclosedJwtVerifyResult = SDJwt.verifyAndParse(
103-
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI0NTYiLCJfc2QiOlsiaGx6ZmpmMDRvNVpzTFIyNWhhNGMtWS05SFcyRFVseGNnaU1ZZDMyNE5nWSJdfQ.2fsLqzujWt0hS0peLS8JLHyyo3D5KCDkNnHcBYqQwVo~WyJ4RFk5VjBtOG43am82ZURIUGtNZ1J3Iiwic3ViIiwiMTIzIl0~",
106+
disclosedJwt,
104107
cryptoProvider
105108
)
106109
// print full payload with disclosed fields
107110
println("Disclosed JWT payload:")
108111
println(parsedDisclosedJwtVerifyResult.sdJwt.fullPayload.toString())
112+
113+
val forgedDisclosure = parsedDisclosedJwtVerifyResult.sdJwt.jwt + "~" + forgeDislosure(parsedDisclosedJwtVerifyResult.sdJwt.disclosureObjects.first())
114+
val forgedDisclosureVerifyResult = SDJwt.verifyAndParse(
115+
forgedDisclosure, cryptoProvider
116+
)
117+
assertFalse(forgedDisclosureVerifyResult.verified)
118+
assertTrue(forgedDisclosureVerifyResult.signatureVerified)
119+
assertFalse(forgedDisclosureVerifyResult.disclosuresVerified)
120+
}
121+
122+
@OptIn(ExperimentalEncodingApi::class)
123+
fun forgeDislosure(disclosure: SDisclosure): String {
124+
return Base64.UrlSafe.encode(buildJsonArray {
125+
add(disclosure.salt)
126+
add(disclosure.key)
127+
add(JsonPrimitive("<forged>"))
128+
}.toString().encodeToByteArray()).trimEnd('=')
109129
}
110130

111131
@Test

waltid-services/waltid-e2e-tests/src/test/kotlin/ExchangeExternalSignaturesApi.kt

+45-10
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import id.walt.oid4vc.data.OpenId4VPProfile
2525
import id.walt.oid4vc.data.ProofType
2626
import id.walt.sdjwt.SDField
2727
import id.walt.sdjwt.SDMap
28+
import id.walt.sdjwt.SDisclosure
2829
import id.walt.sdjwt.utils.Base64Utils.encodeToBase64Url
2930
import id.walt.verifier.oidc.RequestedCredential
3031
import id.walt.webwallet.db.models.WalletCredential
@@ -49,6 +50,9 @@ import kotlinx.coroutines.runBlocking
4950
import kotlinx.serialization.ExperimentalSerializationApi
5051
import kotlinx.serialization.decodeFromByteArray
5152
import kotlinx.serialization.json.*
53+
import sun.font.StrikeCache
54+
import kotlin.io.encoding.Base64
55+
import kotlin.io.encoding.ExperimentalEncodingApi
5256
import kotlin.test.assertEquals
5357
import kotlin.test.assertNotEquals
5458
import kotlin.test.assertNotNull
@@ -326,6 +330,7 @@ class ExchangeExternalSignatures {
326330
)
327331
testOID4VP(openbadgeSdJwtPresentationRequest)
328332
testOID4VP(openbadgeSdJwtPresentationRequest, true)
333+
testOID4VP(openbadgeSdJwtPresentationRequest, true, true)
329334
clearWalletCredentials()
330335
}
331336

@@ -338,6 +343,7 @@ class ExchangeExternalSignatures {
338343
)
339344
testOID4VPSdJwtVc()
340345
testOID4VPSdJwtVc(true)
346+
testOID4VPSdJwtVc(true, true)
341347
clearWalletCredentials()
342348
testPreAuthorizedOID4VCI(
343349
useOptionalParameters = false,
@@ -490,6 +496,7 @@ class ExchangeExternalSignatures {
490496
private suspend fun testOID4VP(
491497
presentationRequest: String,
492498
addDisclosures: Boolean = false,
499+
forgeDisclosures: Boolean = false,
493500
) {
494501
lateinit var presentationRequestURL: String
495502
lateinit var verificationID: String
@@ -522,18 +529,22 @@ class ExchangeExternalSignatures {
522529
presentationRequest = presentationRequestURL,
523530
selectedCredentialIdList = matchedCredentialList.map { it.id },
524531
disclosures = if (addDisclosures) matchedCredentialList.filter { it.disclosures != null }.associate {
525-
Pair(it.id, listOf(it.disclosures!!))
532+
Pair(it.id, listOf(
533+
if(forgeDisclosures) forgeSDisclosureString(it.disclosures!!) else it.disclosures!!
534+
))
526535
} else null,
527536
)
528537
println(prepareRequest)
529538
response = client.post("/wallet-api/wallet/$walletId/exchange/external_signatures/presentation/prepare") {
530539
setBody(prepareRequest)
531540
}.expectSuccess()
532541
val prepareResponse = response.body<PrepareOID4VPResponse>()
533-
client.post("/wallet-api/wallet/$walletId/exchange/external_signatures/presentation/submit") {
542+
val submitResponse = client.post("/wallet-api/wallet/$walletId/exchange/external_signatures/presentation/submit") {
534543
setBody(SubmitOID4VPRequest.build(prepareResponse,
535544
disclosures = if (addDisclosures) matchedCredentialList.filter { it.disclosures != null }.associate {
536-
Pair(it.id, listOf(it.disclosures!!))
545+
Pair(it.id, listOf(
546+
if(forgeDisclosures) forgeSDisclosureString(it.disclosures!!) else it.disclosures!!
547+
))
537548
} else null,
538549
w3cJwtVpProof = prepareResponse.w3CJwtVpProofParameters?.let { params ->
539550
holderKey.signJws(
@@ -550,12 +561,16 @@ class ExchangeExternalSignatures {
550561
)
551562
})
552563
)
553-
}.expectSuccess()
564+
}
565+
if(!forgeDisclosures)
566+
submitResponse.expectSuccess()
567+
else
568+
submitResponse.expectFailure()
554569
verifierSessionApi.get(verificationID) { sessionInfo ->
555570
assert(sessionInfo.tokenResponse?.vpToken?.jsonPrimitive?.contentOrNull?.expectLooksLikeJwt() != null) { "Received no valid token response!" }
556571
assert(sessionInfo.tokenResponse?.presentationSubmission != null) { "should have a presentation submission after submission" }
557572

558-
assert(sessionInfo.verificationResult == true) { "overall verification should be valid" }
573+
assert(sessionInfo.verificationResult == !forgeDisclosures) { "overall verification should be ${!forgeDisclosures}" }
559574
sessionInfo.policyResults.let {
560575
require(it != null) { "policyResults should be available after running policies" }
561576
assert(it.size > 1) { "no policies have run" }
@@ -565,6 +580,7 @@ class ExchangeExternalSignatures {
565580

566581
private suspend fun testOID4VPSdJwtVc(
567582
addDisclosures: Boolean = false,
583+
forgeDisclosures: Boolean = false,
568584
) {
569585
lateinit var presentationRequestURL: String
570586
lateinit var resolvedPresentationRequestURL: String
@@ -611,18 +627,22 @@ class ExchangeExternalSignatures {
611627
presentationRequest = presentationRequestURL,
612628
selectedCredentialIdList = matchedCredentialList.map { it.id },
613629
disclosures = if (addDisclosures) matchedCredentialList.filter { it.disclosures != null }.associate {
614-
Pair(it.id, listOf(it.disclosures!!))
630+
Pair(it.id, listOf(
631+
if(forgeDisclosures) forgeSDisclosureString(it.disclosures!!) else it.disclosures!!
632+
))
615633
} else null,
616634
)
617635
println(prepareRequest)
618636
response = client.post("/wallet-api/wallet/$walletId/exchange/external_signatures/presentation/prepare") {
619637
setBody(prepareRequest)
620638
}.expectSuccess()
621639
val prepareResponse = response.body<PrepareOID4VPResponse>()
622-
client.post("/wallet-api/wallet/$walletId/exchange/external_signatures/presentation/submit") {
640+
val submitResponse = client.post("/wallet-api/wallet/$walletId/exchange/external_signatures/presentation/submit") {
623641
setBody(SubmitOID4VPRequest.build(prepareResponse,
624642
disclosures = if (addDisclosures) matchedCredentialList.filter { it.disclosures != null }.associate {
625-
Pair(it.id, listOf(it.disclosures!!))
643+
Pair(it.id, listOf(
644+
if(forgeDisclosures) forgeSDisclosureString(it.disclosures!!) else it.disclosures!!
645+
))
626646
} else null,
627647
w3cJwtVpProof = prepareResponse.w3CJwtVpProofParameters?.let { params ->
628648
holderKey.signJws(
@@ -639,12 +659,16 @@ class ExchangeExternalSignatures {
639659
)
640660
})
641661
)
642-
}.expectSuccess()
662+
}
663+
if(!forgeDisclosures)
664+
submitResponse.expectSuccess()
665+
else
666+
submitResponse.expectFailure()
643667
verifierSessionApi.get(verificationID) { sessionInfo ->
644668
// assert(sessionInfo.tokenResponse?.vpToken?.jsonPrimitive?.contentOrNull?.expectLooksLikeJwt() != null) { "Received no valid token response!" }
645669
assert(sessionInfo.tokenResponse?.presentationSubmission != null) { "should have a presentation submission after submission" }
646670

647-
assert(sessionInfo.verificationResult == true) { "overall verification should be valid" }
671+
assert(sessionInfo.verificationResult == !forgeDisclosures) { "overall verification should be ${!forgeDisclosures}" }
648672
sessionInfo.policyResults.let {
649673
require(it != null) { "policyResults should be available after running policies" }
650674
assert(it.size > 1) { "no policies have run" }
@@ -748,4 +772,15 @@ class ExchangeExternalSignatures {
748772
}
749773
}
750774
}
775+
776+
@OptIn(ExperimentalEncodingApi::class)
777+
fun forgeSDisclosureString(disclosures: String): String {
778+
return disclosures.split("~").filter { it.isNotEmpty() }.map { SDisclosure.parse(it) }.map { disclosure ->
779+
Base64.UrlSafe.encode(buildJsonArray {
780+
add(disclosure.salt)
781+
add(disclosure.key)
782+
add(JsonPrimitive("<forged>"))
783+
}.toString().encodeToByteArray()).trimEnd('=')
784+
}.joinToString("~")
785+
}
751786
}

waltid-services/waltid-wallet-api/src/test/kotlin/id/walt/webwallet/utils/X5CValidatorTest.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class X5CValidatorTest {
1717
//we don't care about the bit size of the key, it's a test case (as long as it's bigger than 512)
1818
private val keyPairGenerator = KeyPairGenerator
1919
.getInstance("RSA").apply {
20-
initialize(1024)
20+
initialize(2048)
2121
}
2222

2323
//x.509 certificate expiration dates

0 commit comments

Comments
 (0)