Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wal 313 fix sd property check #839

Merged
merged 5 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package id.walt.credentials.schemes

import id.walt.crypto.exceptions.VerificationException
import id.walt.crypto.keys.Key
import id.walt.crypto.utils.JsonUtils.toJsonObject
import id.walt.crypto.utils.JwsUtils.decodeJws
import id.walt.did.dids.DidService
import id.walt.did.dids.DidUtils
import id.walt.sdjwt.JWTCryptoProvider
import id.walt.sdjwt.SDJwt
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
Expand Down Expand Up @@ -36,6 +39,8 @@ class JwsSignatureScheme : SignatureScheme {
const val VC = "vc"
}

data class KeyInfo(val keyId: String, val key: Key)

fun toPayload(data: JsonObject, jwtOptions: Map<String, JsonElement> = emptyMap()) =
mapOf(
JwsOption.ISSUER to jwtOptions[JwsOption.ISSUER],
Expand All @@ -44,6 +49,29 @@ class JwsSignatureScheme : SignatureScheme {
*(jwtOptions.entries.map { it.toPair() }.toTypedArray())
).toJsonObject()

@JvmBlocking
@JvmAsync
@JsPromise
@JsExport.Ignore
suspend fun getIssuerKeyInfo(jws: String): KeyInfo {
val jwsParsed = jws.decodeJws()
val keyId = jwsParsed.header[JwsHeader.KEY_ID]!!.jsonPrimitive.content
val issuerId = (jwsParsed.payload[JwsOption.ISSUER]?.jsonPrimitive?.content ?: keyId)
val key = if (DidUtils.isDidUrl(issuerId)) {
log.trace { "Resolving key from issuer did: $issuerId" }
DidService.resolveToKey(issuerId)
.also {
if (log.isTraceEnabled()) {
val exportedJwk = it.getOrNull()?.getPublicKey()?.exportJWK()
log.trace { "Imported key: $it from did: $issuerId, public is: $exportedJwk" }
}
}
.getOrThrow()
} else
TODO("Issuer IDs other than DIDs are currently not supported for W3C credentials.")
return KeyInfo(keyId, key)
}

/**
* args:
* - kid: Key ID
Expand Down Expand Up @@ -73,32 +101,21 @@ class JwsSignatureScheme : SignatureScheme {
@JsPromise
@JsExport.Ignore
suspend fun verify(data: String): Result<JsonElement> = runCatching {
val jws = data.decodeJws()

val header = jws.header
val payload = jws.payload

val issuerDid = (payload[JwsOption.ISSUER] ?: header[JwsHeader.KEY_ID])!!.jsonPrimitive.content
if (DidUtils.isDidUrl(issuerDid)) {
verifyForIssuerDid(issuerDid, data)
} else {
TODO()
}
val keyInfo = getIssuerKeyInfo(data)
return keyInfo.key.verifyJws(data.split("~")[0])
.also { log.trace { "Verification result: $it" } }
}

private suspend fun verifyForIssuerDid(issuerDid: String, data: String): JsonElement {
log.trace { "Verifying with issuer did: $issuerDid" }

return DidService.resolveToKey(issuerDid)
.also {
if (log.isTraceEnabled()) {
val exportedJwk = it.getOrNull()?.getPublicKey()?.exportJWK()
log.trace { "Imported key: $it from did: $issuerDid, public is: $exportedJwk" }
}
}
.getOrThrow()
.verifyJws(data.split("~")[0])
.also { log.trace { "Verification result: $it" } }
.getOrThrow()
@JvmBlocking
@JvmAsync
@JsPromise
@JsExport.Ignore
suspend fun verifySDJwt(data: String, jwtCryptoProvider: JWTCryptoProvider): Result<JsonElement> = runCatching {
return SDJwt.verifyAndParse(data, jwtCryptoProvider).let {
if(it.verified)
Result.success(it.sdJwt.fullPayload)
else
Result.failure(VerificationException(it.message ?: "Verification failed"))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package id.walt.policies.policies
import id.walt.credentials.schemes.JwsSignatureScheme
import id.walt.credentials.utils.VCFormat
import id.walt.policies.JwtVerificationPolicy
import id.walt.sdjwt.SDJwtVC
import id.walt.sdjwt.SDJwt
import kotlinx.serialization.Serializable
import love.forte.plugin.suspendtrans.annotation.JsPromise
import love.forte.plugin.suspendtrans.annotation.JvmAsync
Expand All @@ -26,6 +26,17 @@ class JwtSignaturePolicy : JwtVerificationPolicy(
@JsPromise
@JsExport.Ignore
override suspend fun verify(credential: String, args: Any?, context: Map<String, Any>): Result<Any> {
return JwsSignatureScheme().verify(credential)
return JwsSignatureScheme().let {
if(SDJwt.isSDJwt(credential, sdOnly = true)) {
val keyInfo = it.getIssuerKeyInfo(credential)
it.verifySDJwt(
credential, JWTCryptoProviderManager.getDefaultJWTCryptoProvider(
mapOf(keyInfo.keyId to keyInfo.key)
)
)
}
else
it.verify(credential)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package id.walt.policies.policies
import id.walt.credentials.schemes.JwsSignatureScheme
import id.walt.credentials.utils.VCFormat
import id.walt.credentials.utils.randomUUID
import id.walt.crypto.exceptions.VerificationException
import id.walt.crypto.keys.Key
import id.walt.crypto.keys.jwk.JWKKey
import id.walt.crypto.utils.JsonUtils.toJsonElement
Expand Down Expand Up @@ -58,7 +59,12 @@ class SdJwtVCSignaturePolicy(): JwtVerificationPolicy() {
requiresHolderKeyBinding = true,
context["clientId"]?.toString(),
context["challenge"]?.toString()
).let { Result.success(sdJwtVC.undisclosedPayload) }
).let {
if(it.verified)
Result.success(sdJwtVC.undisclosedPayload)
else
Result.failure(VerificationException("SD-JWT verification failed"))
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ open class SDJwt internal constructor(
* @param jwtCryptoProvider JWT crypto provider, that implements standard JWT token verification on the target platform
*/
fun verify(jwtCryptoProvider: JWTCryptoProvider, keyID: String? = null): VerificationResult<SDJwt> {
return jwtCryptoProvider.verify(jwt, keyID).let {
return jwtCryptoProvider.verify(jwt, keyID ?: this.keyID).let {
VerificationResult(
sdJwt = this,
signatureVerified = it.verified,
Expand Down Expand Up @@ -265,8 +265,8 @@ open class SDJwt internal constructor(
/**
* Check the given string, whether it matches the pattern of an SD-JWT
*/
fun isSDJwt(value: String): Boolean {
return Regex(SD_JWT_PATTERN).matches(value)
fun isSDJwt(value: String, sdOnly: Boolean = false): Boolean {
return Regex(SD_JWT_PATTERN).matches(value) && (!sdOnly || value.contains("~"))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import korlibs.crypto.SHA256
import korlibs.crypto.encoding.ASCII
import kotlinx.datetime.Clock
import kotlinx.serialization.json.*
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.test.*

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

val disclosedJwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI0NTYiLCJfc2QiOlsiaGx6ZmpmMDRvNVpzTFIyNWhhNGMtWS05SFcyRFVseGNnaU1ZZDMyNE5nWSJdfQ.2fsLqzujWt0hS0peLS8JLHyyo3D5KCDkNnHcBYqQwVo~WyJ4RFk5VjBtOG43am82ZURIUGtNZ1J3Iiwic3ViIiwiMTIzIl0~"
val parsedDisclosedJwtVerifyResult = SDJwt.verifyAndParse(
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI0NTYiLCJfc2QiOlsiaGx6ZmpmMDRvNVpzTFIyNWhhNGMtWS05SFcyRFVseGNnaU1ZZDMyNE5nWSJdfQ.2fsLqzujWt0hS0peLS8JLHyyo3D5KCDkNnHcBYqQwVo~WyJ4RFk5VjBtOG43am82ZURIUGtNZ1J3Iiwic3ViIiwiMTIzIl0~",
disclosedJwt,
cryptoProvider
)
// print full payload with disclosed fields
println("Disclosed JWT payload:")
println(parsedDisclosedJwtVerifyResult.sdJwt.fullPayload.toString())

val forgedDisclosure = parsedDisclosedJwtVerifyResult.sdJwt.jwt + "~" + forgeDislosure(parsedDisclosedJwtVerifyResult.sdJwt.disclosureObjects.first())
val forgedDisclosureVerifyResult = SDJwt.verifyAndParse(
forgedDisclosure, cryptoProvider
)
assertFalse(forgedDisclosureVerifyResult.verified)
assertTrue(forgedDisclosureVerifyResult.signatureVerified)
assertFalse(forgedDisclosureVerifyResult.disclosuresVerified)
}

@OptIn(ExperimentalEncodingApi::class)
fun forgeDislosure(disclosure: SDisclosure): String {
return Base64.UrlSafe.encode(buildJsonArray {
add(disclosure.salt)
add(disclosure.key)
add(JsonPrimitive("<forged>"))
}.toString().encodeToByteArray()).trimEnd('=')
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import id.walt.oid4vc.data.OpenId4VPProfile
import id.walt.oid4vc.data.ProofType
import id.walt.sdjwt.SDField
import id.walt.sdjwt.SDMap
import id.walt.sdjwt.SDisclosure
import id.walt.sdjwt.utils.Base64Utils.encodeToBase64Url
import id.walt.verifier.oidc.RequestedCredential
import id.walt.webwallet.db.models.WalletCredential
Expand All @@ -49,6 +50,9 @@ import kotlinx.coroutines.runBlocking
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.json.*
import sun.font.StrikeCache
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlin.test.assertNotNull
Expand Down Expand Up @@ -326,6 +330,7 @@ class ExchangeExternalSignatures {
)
testOID4VP(openbadgeSdJwtPresentationRequest)
testOID4VP(openbadgeSdJwtPresentationRequest, true)
testOID4VP(openbadgeSdJwtPresentationRequest, true, true)
clearWalletCredentials()
}

Expand All @@ -338,6 +343,7 @@ class ExchangeExternalSignatures {
)
testOID4VPSdJwtVc()
testOID4VPSdJwtVc(true)
testOID4VPSdJwtVc(true, true)
clearWalletCredentials()
testPreAuthorizedOID4VCI(
useOptionalParameters = false,
Expand Down Expand Up @@ -490,6 +496,7 @@ class ExchangeExternalSignatures {
private suspend fun testOID4VP(
presentationRequest: String,
addDisclosures: Boolean = false,
forgeDisclosures: Boolean = false,
) {
lateinit var presentationRequestURL: String
lateinit var verificationID: String
Expand Down Expand Up @@ -522,18 +529,22 @@ class ExchangeExternalSignatures {
presentationRequest = presentationRequestURL,
selectedCredentialIdList = matchedCredentialList.map { it.id },
disclosures = if (addDisclosures) matchedCredentialList.filter { it.disclosures != null }.associate {
Pair(it.id, listOf(it.disclosures!!))
Pair(it.id, listOf(
if(forgeDisclosures) forgeSDisclosureString(it.disclosures!!) else it.disclosures!!
))
} else null,
)
println(prepareRequest)
response = client.post("/wallet-api/wallet/$walletId/exchange/external_signatures/presentation/prepare") {
setBody(prepareRequest)
}.expectSuccess()
val prepareResponse = response.body<PrepareOID4VPResponse>()
client.post("/wallet-api/wallet/$walletId/exchange/external_signatures/presentation/submit") {
val submitResponse = client.post("/wallet-api/wallet/$walletId/exchange/external_signatures/presentation/submit") {
setBody(SubmitOID4VPRequest.build(prepareResponse,
disclosures = if (addDisclosures) matchedCredentialList.filter { it.disclosures != null }.associate {
Pair(it.id, listOf(it.disclosures!!))
Pair(it.id, listOf(
if(forgeDisclosures) forgeSDisclosureString(it.disclosures!!) else it.disclosures!!
))
} else null,
w3cJwtVpProof = prepareResponse.w3CJwtVpProofParameters?.let { params ->
holderKey.signJws(
Expand All @@ -550,12 +561,16 @@ class ExchangeExternalSignatures {
)
})
)
}.expectSuccess()
}
if(!forgeDisclosures)
submitResponse.expectSuccess()
else
submitResponse.expectFailure()
verifierSessionApi.get(verificationID) { sessionInfo ->
assert(sessionInfo.tokenResponse?.vpToken?.jsonPrimitive?.contentOrNull?.expectLooksLikeJwt() != null) { "Received no valid token response!" }
assert(sessionInfo.tokenResponse?.presentationSubmission != null) { "should have a presentation submission after submission" }

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

private suspend fun testOID4VPSdJwtVc(
addDisclosures: Boolean = false,
forgeDisclosures: Boolean = false,
) {
lateinit var presentationRequestURL: String
lateinit var resolvedPresentationRequestURL: String
Expand Down Expand Up @@ -611,18 +627,22 @@ class ExchangeExternalSignatures {
presentationRequest = presentationRequestURL,
selectedCredentialIdList = matchedCredentialList.map { it.id },
disclosures = if (addDisclosures) matchedCredentialList.filter { it.disclosures != null }.associate {
Pair(it.id, listOf(it.disclosures!!))
Pair(it.id, listOf(
if(forgeDisclosures) forgeSDisclosureString(it.disclosures!!) else it.disclosures!!
))
} else null,
)
println(prepareRequest)
response = client.post("/wallet-api/wallet/$walletId/exchange/external_signatures/presentation/prepare") {
setBody(prepareRequest)
}.expectSuccess()
val prepareResponse = response.body<PrepareOID4VPResponse>()
client.post("/wallet-api/wallet/$walletId/exchange/external_signatures/presentation/submit") {
val submitResponse = client.post("/wallet-api/wallet/$walletId/exchange/external_signatures/presentation/submit") {
setBody(SubmitOID4VPRequest.build(prepareResponse,
disclosures = if (addDisclosures) matchedCredentialList.filter { it.disclosures != null }.associate {
Pair(it.id, listOf(it.disclosures!!))
Pair(it.id, listOf(
if(forgeDisclosures) forgeSDisclosureString(it.disclosures!!) else it.disclosures!!
))
} else null,
w3cJwtVpProof = prepareResponse.w3CJwtVpProofParameters?.let { params ->
holderKey.signJws(
Expand All @@ -639,12 +659,16 @@ class ExchangeExternalSignatures {
)
})
)
}.expectSuccess()
}
if(!forgeDisclosures)
submitResponse.expectSuccess()
else
submitResponse.expectFailure()
verifierSessionApi.get(verificationID) { sessionInfo ->
// assert(sessionInfo.tokenResponse?.vpToken?.jsonPrimitive?.contentOrNull?.expectLooksLikeJwt() != null) { "Received no valid token response!" }
assert(sessionInfo.tokenResponse?.presentationSubmission != null) { "should have a presentation submission after submission" }

assert(sessionInfo.verificationResult == true) { "overall verification should be valid" }
assert(sessionInfo.verificationResult == !forgeDisclosures) { "overall verification should be ${!forgeDisclosures}" }
sessionInfo.policyResults.let {
require(it != null) { "policyResults should be available after running policies" }
assert(it.size > 1) { "no policies have run" }
Expand Down Expand Up @@ -748,4 +772,15 @@ class ExchangeExternalSignatures {
}
}
}

@OptIn(ExperimentalEncodingApi::class)
fun forgeSDisclosureString(disclosures: String): String {
return disclosures.split("~").filter { it.isNotEmpty() }.map { SDisclosure.parse(it) }.map { disclosure ->
Base64.UrlSafe.encode(buildJsonArray {
add(disclosure.salt)
add(disclosure.key)
add(JsonPrimitive("<forged>"))
}.toString().encodeToByteArray()).trimEnd('=')
}.joinToString("~")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class X5CValidatorTest {
//we don't care about the bit size of the key, it's a test case (as long as it's bigger than 512)
private val keyPairGenerator = KeyPairGenerator
.getInstance("RSA").apply {
initialize(1024)
initialize(2048)
}

//x.509 certificate expiration dates
Expand Down
Loading