diff --git a/waltid-applications/waltid-cli/README.md b/waltid-applications/waltid-cli/README.md index 2501a1910..87357a92b 100644 --- a/waltid-applications/waltid-cli/README.md +++ b/waltid-applications/waltid-cli/README.md @@ -512,7 +512,7 @@ Usage: waltid vc verify [] ╰─────────────────────────────────────────────────────────────────────────────────────────╯ Options: - -p, --policy=(signature|expired|not-before|revoked_status_list|schema|allowed-issuer|webhook) + -p, --policy=(signature|expired|not-before|revoked-status-list|schema|allowed-issuer|webhook) Specify one, or more policies to be applied during the verification process of the VC (signature policy is always applied). -a, --arg= Argument required by some policies, namely: @@ -525,7 +525,7 @@ Options: ├─────────────────────┼─────────────────────────────────────────────────────────────────┤ │ not-before │ - │ ├─────────────────────┼─────────────────────────────────────────────────────────────────┤ - │ revoked_status_list │ - │ + │ revoked-status-list │ - │ ├─────────────────────┼─────────────────────────────────────────────────────────────────┤ │ schema │ schema=/path/to/schema.json │ ├─────────────────────┼─────────────────────────────────────────────────────────────────┤ diff --git a/waltid-applications/waltid-cli/src/commonMain/kotlin/id/walt/cli/commands/VCVerifyCmd.kt b/waltid-applications/waltid-cli/src/commonMain/kotlin/id/walt/cli/commands/VCVerifyCmd.kt index 585f557b3..1b6463367 100644 --- a/waltid-applications/waltid-cli/src/commonMain/kotlin/id/walt/cli/commands/VCVerifyCmd.kt +++ b/waltid-applications/waltid-cli/src/commonMain/kotlin/id/walt/cli/commands/VCVerifyCmd.kt @@ -57,7 +57,7 @@ class VCVerifyCmd : CliktCommand( "signature", "expired", "not-before", - "revoked_status_list", + "revoked-status-list", "schema", "allowed-issuer", "webhook", @@ -75,7 +75,7 @@ class VCVerifyCmd : CliktCommand( |signature| - | |expired| - | |not-before| - | - |revoked_status_list| - | + |revoked-status-list| - | |schema|schema=/path/to/schema.json| |allowed-issuer|issuer=did:key:z6Mkp7AVwvWxnsNDuSSbf19sgKzrx223WY95AqZyAGifFVyV| |webhook|url=https://example.com| @@ -145,7 +145,7 @@ class VCVerifyCmd : CliktCommand( args.putAll(getAllowedIssuerPolicyArguments()) args.putAll(getWebhookPolicyArguments()) args.putAll(getRevocationPolicyArguments()) - for (noArgPolicyName in listOf("signature", "expired", "not-before", "revoked_status_list")) { + for (noArgPolicyName in listOf("signature", "expired", "not-before", "revoked-status-list")) { if (noArgPolicyName in policies) { args[noArgPolicyName] = "".toJsonElement() } @@ -205,7 +205,7 @@ class VCVerifyCmd : CliktCommand( private fun getRevocationPolicyArguments(): Map { val args = mutableMapOf() - if ("revoked_status_list" in policies) { + if ("revoked-status-list" in policies) { args["vc"] = vc.readText().toJsonElement() } diff --git a/waltid-applications/waltid-cli/src/jvmTest/kotlin/id/walt/cli/commands/WaltIdVCVerifyCmdTest.kt b/waltid-applications/waltid-cli/src/jvmTest/kotlin/id/walt/cli/commands/WaltIdVCVerifyCmdTest.kt index 1b7c257d6..0a8d08293 100644 --- a/waltid-applications/waltid-cli/src/jvmTest/kotlin/id/walt/cli/commands/WaltIdVCVerifyCmdTest.kt +++ b/waltid-applications/waltid-cli/src/jvmTest/kotlin/id/walt/cli/commands/WaltIdVCVerifyCmdTest.kt @@ -330,11 +330,11 @@ class WaltIdVCVerifyCmdTest { fun `should output Success when the credential does not contain a revocation status list entry`() = runTest { val result = command.test( listOf( - "--policy=revoked_status_list", + "--policy=revoked-status-list", signedVCFilePath, ) ) - assertContains(result.output, "revoked_status_list: Success!") + assertContains(result.output, "revoked-status-list: Success!") } private fun sign(vcFilePath: String): String { diff --git a/waltid-applications/waltid-web-wallet/libs/composables/issuance.ts b/waltid-applications/waltid-web-wallet/libs/composables/issuance.ts index 3de6ccacd..61c71cfc3 100644 --- a/waltid-applications/waltid-web-wallet/libs/composables/issuance.ts +++ b/waltid-applications/waltid-web-wallet/libs/composables/issuance.ts @@ -47,7 +47,7 @@ export async function useIssuance(query: any) { issuerHost = issuer; } - const credential_issuer: { credential_configurations_supported: Array<{ types: Array; }>; } = await $fetch(`${issuer}/.well-known/openid-credential-issuer`) + const credential_issuer: { credential_configurations_supported: Array<{ types: Array; }>; } = await $fetch(`/wallet-api/wallet/${currentWallet.value}/exchange/resolveIssuerOpenIDMetadata?issuer=${issuer}`) const credentialList = credentialOffer.credential_configuration_ids.map((id) => credential_issuer.credential_configurations_supported[id]); let credentialTypes: String[] = []; @@ -122,4 +122,4 @@ export async function useIssuance(query: any) { groupedCredentialTypes, issuerHost } -} \ No newline at end of file +} diff --git a/waltid-libraries/credentials/waltid-verifiable-credentials/src/commonMain/kotlin/id/walt/credentials/schemes/JwsSignatureScheme.kt b/waltid-libraries/credentials/waltid-verifiable-credentials/src/commonMain/kotlin/id/walt/credentials/schemes/JwsSignatureScheme.kt index b6254a539..988145eee 100644 --- a/waltid-libraries/credentials/waltid-verifiable-credentials/src/commonMain/kotlin/id/walt/credentials/schemes/JwsSignatureScheme.kt +++ b/waltid-libraries/credentials/waltid-verifiable-credentials/src/commonMain/kotlin/id/walt/credentials/schemes/JwsSignatureScheme.kt @@ -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 @@ -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 = emptyMap()) = mapOf( JwsOption.ISSUER to jwtOptions[JwsOption.ISSUER], @@ -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 @@ -73,32 +101,21 @@ class JwsSignatureScheme : SignatureScheme { @JsPromise @JsExport.Ignore suspend fun verify(data: String): Result = 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 = runCatching { + return SDJwt.verifyAndParse(data, jwtCryptoProvider).let { + if(it.verified) + Result.success(it.sdJwt.fullPayload) + else + Result.failure(VerificationException(it.message ?: "Verification failed")) + } } } diff --git a/waltid-libraries/credentials/waltid-verification-policies/src/commonMain/kotlin/id/walt/policies/policies/JwtSignaturePolicy.kt b/waltid-libraries/credentials/waltid-verification-policies/src/commonMain/kotlin/id/walt/policies/policies/JwtSignaturePolicy.kt index ed0f33060..9974c9e20 100644 --- a/waltid-libraries/credentials/waltid-verification-policies/src/commonMain/kotlin/id/walt/policies/policies/JwtSignaturePolicy.kt +++ b/waltid-libraries/credentials/waltid-verification-policies/src/commonMain/kotlin/id/walt/policies/policies/JwtSignaturePolicy.kt @@ -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 @@ -26,6 +26,17 @@ class JwtSignaturePolicy : JwtVerificationPolicy( @JsPromise @JsExport.Ignore override suspend fun verify(credential: String, args: Any?, context: Map): Result { - 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) + } } } diff --git a/waltid-libraries/credentials/waltid-verification-policies/src/commonMain/kotlin/id/walt/policies/policies/RevocationPolicy.kt b/waltid-libraries/credentials/waltid-verification-policies/src/commonMain/kotlin/id/walt/policies/policies/RevocationPolicy.kt index a3d9a0b1a..aa13ce271 100644 --- a/waltid-libraries/credentials/waltid-verification-policies/src/commonMain/kotlin/id/walt/policies/policies/RevocationPolicy.kt +++ b/waltid-libraries/credentials/waltid-verification-policies/src/commonMain/kotlin/id/walt/policies/policies/RevocationPolicy.kt @@ -9,7 +9,7 @@ import kotlinx.serialization.json.JsonObject abstract class RevocationPolicyMp : CredentialWrapperValidatorPolicy( ) { - override val name = "revoked_status_list" + override val name = "revoked-status-list" override val description = "Verifies Credential Status" override val supportedVCFormats = setOf(VCFormat.jwt_vc, VCFormat.jwt_vc_json, VCFormat.ldp_vc) diff --git a/waltid-libraries/credentials/waltid-verification-policies/src/commonMain/kotlin/id/walt/policies/policies/SdJwtVCSignaturePolicy.kt b/waltid-libraries/credentials/waltid-verification-policies/src/commonMain/kotlin/id/walt/policies/policies/SdJwtVCSignaturePolicy.kt index 411e08fe5..750938729 100644 --- a/waltid-libraries/credentials/waltid-verification-policies/src/commonMain/kotlin/id/walt/policies/policies/SdJwtVCSignaturePolicy.kt +++ b/waltid-libraries/credentials/waltid-verification-policies/src/commonMain/kotlin/id/walt/policies/policies/SdJwtVCSignaturePolicy.kt @@ -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 @@ -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")) + } } } diff --git a/waltid-libraries/credentials/waltid-verification-policies/src/jvmMain/kotlin/id/walt/policies/policies/RevocationPolicy.jvm.kt b/waltid-libraries/credentials/waltid-verification-policies/src/jvmMain/kotlin/id/walt/policies/policies/RevocationPolicy.jvm.kt index 5621c728d..b3b8964a7 100644 --- a/waltid-libraries/credentials/waltid-verification-policies/src/jvmMain/kotlin/id/walt/policies/policies/RevocationPolicy.jvm.kt +++ b/waltid-libraries/credentials/waltid-verification-policies/src/jvmMain/kotlin/id/walt/policies/policies/RevocationPolicy.jvm.kt @@ -47,7 +47,7 @@ actual class RevocationPolicy : RevocationPolicyMp() { val credentialSubject = payload["vc"]!!.jsonObject["credentialSubject"]?.jsonObject!! val encodedList = credentialSubject["encodedList"]?.jsonPrimitive?.content ?: "" val bitValue = get(encodedList, statusListIndex) - if (bitValue!![0].code == 0) { + if (StreamUtils.binToInt(bitValue!!.joinToString("")) == 0) { Result.success(statusListCredentialUrl!!) } else { Result.failure(Throwable("Credential has been revoked")) @@ -67,6 +67,8 @@ object Base64Utils { object StreamUtils { private const val BITS_PER_BYTE = 8u + fun binToInt(bin: String) = bin.toInt(2) + fun getBitValue(inputStream: InputStream, index: ULong, bitSize: Int): List = inputStream.use { stream -> //TODO: bitSize constraints val bitStartPosition = index * bitSize.toUInt() @@ -89,4 +91,4 @@ object StreamUtils { } fun get(bitstring: String, idx: ULong? = null, bitSize: Int = 1) = - idx?.let { StreamUtils.getBitValue(GZIPInputStream(Base64Utils.decode(bitstring).inputStream()), it, bitSize) } + idx?.let { StreamUtils.getBitValue(GZIPInputStream(Base64Utils.decode(bitstring).inputStream()), it, bitSize) } \ No newline at end of file diff --git a/waltid-libraries/crypto/waltid-crypto-android/src/androidMain/kotlin/id/walt/crypto/keys/AndroidKey.kt b/waltid-libraries/crypto/waltid-crypto-android/src/androidMain/kotlin/id/walt/crypto/keys/AndroidKey.kt index e39e693d9..1b2489bbe 100644 --- a/waltid-libraries/crypto/waltid-crypto-android/src/androidMain/kotlin/id/walt/crypto/keys/AndroidKey.kt +++ b/waltid-libraries/crypto/waltid-crypto-android/src/androidMain/kotlin/id/walt/crypto/keys/AndroidKey.kt @@ -201,6 +201,8 @@ class AndroidKey() : Key() { TODO("Not yet implemented") } + override suspend fun deleteKey(): Boolean = kotlin.runCatching { keyStore.deleteEntry(internalKeyId) }.isSuccess + private fun getSignature(): Signature { val sig = when (keyType) { KeyType.secp256k1 -> Signature.getInstance( diff --git a/waltid-libraries/crypto/waltid-crypto-ios/src/iosMain/kotlin/id/walt/crypto/IosKey.kt b/waltid-libraries/crypto/waltid-crypto-ios/src/iosMain/kotlin/id/walt/crypto/IosKey.kt index 94ac71c1c..7be201dd0 100644 --- a/waltid-libraries/crypto/waltid-crypto-ios/src/iosMain/kotlin/id/walt/crypto/IosKey.kt +++ b/waltid-libraries/crypto/waltid-crypto-ios/src/iosMain/kotlin/id/walt/crypto/IosKey.kt @@ -222,6 +222,15 @@ class IosKey private constructor( override suspend fun getMeta(): KeyMeta { error("Not yet implemented") } + + override suspend fun deleteKey(): Boolean = kotlin.runCatching { + when (options.keyType) { + KeyType.secp256r1 -> P256.PrivateKey.deleteFromKeychain(options.kid) + KeyType.Ed25519 -> Ed25519.PrivateKey.deleteFromKeychain(options.kid) + KeyType.RSA -> RSA.PrivateKey.deleteFromKeychain(options.kid) + else -> error("Not implemented") + } + }.isSuccess } // utility functions for swift diff --git a/waltid-libraries/crypto/waltid-crypto/src/iosMain/kotlin/id/walt/crypto/keys/jwk/JWKKey.ios.kt b/waltid-libraries/crypto/waltid-crypto/src/iosMain/kotlin/id/walt/crypto/keys/jwk/JWKKey.ios.kt index f92f3d1c1..0fc20f1c5 100644 --- a/waltid-libraries/crypto/waltid-crypto/src/iosMain/kotlin/id/walt/crypto/keys/jwk/JWKKey.ios.kt +++ b/waltid-libraries/crypto/waltid-crypto/src/iosMain/kotlin/id/walt/crypto/keys/jwk/JWKKey.ios.kt @@ -109,6 +109,10 @@ actual class JWKKey actual constructor(private val jwk: String?, private val _ke TODO("Not yet implemented") } + actual override suspend fun deleteKey(): Boolean { + TODO("Not yet implemented") + } + actual override val hasPrivateKey: Boolean get() = _jwkObj.toMap().any { it.key in privateParameters } diff --git a/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/OpenID4VCI.kt b/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/OpenID4VCI.kt index 06f37e0d9..984cdf93a 100644 --- a/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/OpenID4VCI.kt +++ b/waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/OpenID4VCI.kt @@ -131,7 +131,9 @@ object OpenID4VCI { appendPathSegments(it.fullPath.trim('/')) }.buildString() } - suspend fun resolveCIProviderMetadata(credOffer: CredentialOffer) = http.get(getCIProviderMetadataUrl(credOffer)).bodyAsText().let { + suspend fun resolveCIProviderMetadata(credOffer: CredentialOffer) = resolveCIProviderMetadata(credOffer.credentialIssuer) + + suspend fun resolveCIProviderMetadata(issuerBaseUrl: String) = http.get(getCIProviderMetadataUrl(issuerBaseUrl)).bodyAsText().let { OpenIDProviderMetadata.fromJSONString(it) } diff --git a/waltid-libraries/protocols/waltid-openid4vc/src/iosMain/kotlin/id/walt/oid4vc/util/COSESign1Utils.ios.kt b/waltid-libraries/protocols/waltid-openid4vc/src/iosMain/kotlin/id/walt/oid4vc/util/COSESign1Utils.ios.kt new file mode 100644 index 000000000..d37cbacb5 --- /dev/null +++ b/waltid-libraries/protocols/waltid-openid4vc/src/iosMain/kotlin/id/walt/oid4vc/util/COSESign1Utils.ios.kt @@ -0,0 +1,12 @@ +package id.walt.oid4vc.util + +import id.walt.oid4vc.providers.TokenTarget + +actual object COSESign1Utils { + actual fun verifyCOSESign1Signature( + target: TokenTarget, + token: String + ): Boolean { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/waltid-libraries/sdjwt/waltid-sdjwt/src/commonMain/kotlin/id/walt/sdjwt/SDJwt.kt b/waltid-libraries/sdjwt/waltid-sdjwt/src/commonMain/kotlin/id/walt/sdjwt/SDJwt.kt index dfc75e937..de9558d21 100644 --- a/waltid-libraries/sdjwt/waltid-sdjwt/src/commonMain/kotlin/id/walt/sdjwt/SDJwt.kt +++ b/waltid-libraries/sdjwt/waltid-sdjwt/src/commonMain/kotlin/id/walt/sdjwt/SDJwt.kt @@ -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 { - return jwtCryptoProvider.verify(jwt, keyID).let { + return jwtCryptoProvider.verify(jwt, keyID ?: this.keyID).let { VerificationResult( sdJwt = this, signatureVerified = it.verified, @@ -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("~")) } } } diff --git a/waltid-libraries/sdjwt/waltid-sdjwt/src/jvmTest/kotlin/id/walt/sdjwt/SDJwtTestJVM.kt b/waltid-libraries/sdjwt/waltid-sdjwt/src/jvmTest/kotlin/id/walt/sdjwt/SDJwtTestJVM.kt index 4cda21f82..fa0d204b4 100644 --- a/waltid-libraries/sdjwt/waltid-sdjwt/src/jvmTest/kotlin/id/walt/sdjwt/SDJwtTestJVM.kt +++ b/waltid-libraries/sdjwt/waltid-sdjwt/src/jvmTest/kotlin/id/walt/sdjwt/SDJwtTestJVM.kt @@ -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 { @@ -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("")) + }.toString().encodeToByteArray()).trimEnd('=') } @Test diff --git a/waltid-services/waltid-e2e-tests/src/test/kotlin/ExchangeExternalSignaturesApi.kt b/waltid-services/waltid-e2e-tests/src/test/kotlin/ExchangeExternalSignaturesApi.kt index eac11b37f..0b0c81a31 100644 --- a/waltid-services/waltid-e2e-tests/src/test/kotlin/ExchangeExternalSignaturesApi.kt +++ b/waltid-services/waltid-e2e-tests/src/test/kotlin/ExchangeExternalSignaturesApi.kt @@ -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 @@ -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 @@ -326,6 +330,7 @@ class ExchangeExternalSignatures { ) testOID4VP(openbadgeSdJwtPresentationRequest) testOID4VP(openbadgeSdJwtPresentationRequest, true) + testOID4VP(openbadgeSdJwtPresentationRequest, true, true) clearWalletCredentials() } @@ -338,6 +343,7 @@ class ExchangeExternalSignatures { ) testOID4VPSdJwtVc() testOID4VPSdJwtVc(true) + testOID4VPSdJwtVc(true, true) clearWalletCredentials() testPreAuthorizedOID4VCI( useOptionalParameters = false, @@ -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 @@ -522,7 +529,9 @@ 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) @@ -530,10 +539,12 @@ class ExchangeExternalSignatures { setBody(prepareRequest) }.expectSuccess() val prepareResponse = response.body() - 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( @@ -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" } @@ -565,6 +580,7 @@ class ExchangeExternalSignatures { private suspend fun testOID4VPSdJwtVc( addDisclosures: Boolean = false, + forgeDisclosures: Boolean = false, ) { lateinit var presentationRequestURL: String lateinit var resolvedPresentationRequestURL: String @@ -611,7 +627,9 @@ 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) @@ -619,10 +637,12 @@ class ExchangeExternalSignatures { setBody(prepareRequest) }.expectSuccess() val prepareResponse = response.body() - 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( @@ -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" } @@ -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("")) + }.toString().encodeToByteArray()).trimEnd('=') + }.joinToString("~") + } } diff --git a/waltid-services/waltid-e2e-tests/src/test/kotlin/IssuerApi.kt b/waltid-services/waltid-e2e-tests/src/test/kotlin/IssuerApi.kt index 533d714d7..48c298531 100644 --- a/waltid-services/waltid-e2e-tests/src/test/kotlin/IssuerApi.kt +++ b/waltid-services/waltid-e2e-tests/src/test/kotlin/IssuerApi.kt @@ -4,7 +4,7 @@ import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.request.* -class IssuerApi(private val client: HttpClient) { +class IssuerApi(private val client: HttpClient, private val cbUrl: String? = null) { suspend fun jwt(request: IssuanceRequest, output: ((String) -> Unit)? = null) = issue( name = "/openid4vc/jwt/issue - issue jwt credential", url = "/openid4vc/jwt/issue", @@ -47,6 +47,9 @@ class IssuerApi(private val client: HttpClient) { private suspend fun issue(name: String, url: String, request: IssuanceRequest, output: ((String) -> Unit)? = null) = test(name) { client.post(url) { + if(!cbUrl.isNullOrEmpty()) { + header("statusCallbackUri", cbUrl) + } setBody(request) }.expectSuccess().apply { output?.invoke(body()) diff --git a/waltid-services/waltid-e2e-tests/src/test/kotlin/WaltidServicesE2ETests.kt b/waltid-services/waltid-e2e-tests/src/test/kotlin/WaltidServicesE2ETests.kt index b3d4a839c..bfccbd452 100644 --- a/waltid-services/waltid-e2e-tests/src/test/kotlin/WaltidServicesE2ETests.kt +++ b/waltid-services/waltid-e2e-tests/src/test/kotlin/WaltidServicesE2ETests.kt @@ -225,7 +225,10 @@ class WaltidServicesE2ETests { //region -Issuer / offer url- lateinit var offerUrl: String - val issuerApi = IssuerApi(client) + val issuerApi = IssuerApi(client, + // uncomment the following line, to test status callbacks, update webhook id as required. + // "https://webhook.site/d879094b-2275-4ae7-b1c5-ebfb9f08dfdb" + ) val issuanceRequest = Json.decodeFromJsonElement(jwtCredential) println("issuance-request:") println(issuanceRequest) diff --git a/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/CIProvider.kt b/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/CIProvider.kt index 9892ffa58..48d32d83a 100644 --- a/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/CIProvider.kt +++ b/waltid-services/waltid-issuer-api/src/main/kotlin/id/walt/issuer/issuance/CIProvider.kt @@ -68,14 +68,13 @@ open class CIProvider( val baseUrl: String = let { ConfigManager.getConfig().baseUrl }, val config: CredentialIssuerConfig = CredentialIssuerConfig(credentialConfigurationsSupported = ConfigManager.getConfig().parse()) ) { - private val log = KotlinLogging.logger { } val metadata get() = OpenID4VCI.createDefaultProviderMetadata(baseUrl).copy( credentialConfigurationsSupported = config.credentialConfigurationsSupported ) companion object { - + private val log = KotlinLogging.logger { } private val http = HttpClient() { install(ContentNegotiation) { json() @@ -93,13 +92,14 @@ open class CIProvider( suspend fun sendCallback(sessionId: String, type: String, data: JsonObject, callbackUrl: String) { try { - http.post(callbackUrl.replace("\$id", sessionId)) { + val response = http.post(callbackUrl.replace("\$id", sessionId)) { setBody(buildJsonObject { put("id", sessionId) put("type", type) put("data", data) }) } + log.trace { "Sent issuance status callback: $callbackUrl, $type, $sessionId; respone: ${response.status}" } } catch (ex: Exception) { throw IllegalArgumentException("Error sending HTTP POST request to issuer callback url.", ex) } @@ -235,7 +235,10 @@ open class CIProvider( issuerKey = resolvedIssuerKey, selectiveDisclosure = request.selectiveDisclosure, dataMapping = request.mapping, - x5Chain = request.x5Chain) + x5Chain = request.x5Chain).also { + if(!issuanceSession.callbackUrl.isNullOrEmpty()) + sendCallback(issuanceSession.id, "sdjwt_issue", buildJsonObject { put("sdjwt", it) }, issuanceSession.callbackUrl) + } else -> OpenID4VCI.generateW3CJwtVC( credentialRequest = credentialRequest, credentialData = vc, @@ -245,7 +248,10 @@ open class CIProvider( selectiveDisclosure = request.selectiveDisclosure, dataMapping = request.mapping, x5Chain = request.x5Chain - ) + ).also { + if(!issuanceSession.callbackUrl.isNullOrEmpty()) + sendCallback(issuanceSession.id, "jwt_issue", buildJsonObject { put("jwt", it) }, issuanceSession.callbackUrl) + } } }.also { log.debug { "Respond VC: $it" } } })) @@ -577,6 +583,11 @@ open class CIProvider( cNonce = generateProofOfPossessionNonceFor(session).cNonce, cNonceExpiresIn = session.expirationTimestamp - Clock.System.now(), state = session.authorizationRequest?.state - ) + ).also { + if(!session.callbackUrl.isNullOrEmpty()) + sendCallback(sessionId, "requested_token", buildJsonObject { + put("request", Json.encodeToJsonElement(session.issuanceRequests.first())) + }, session.callbackUrl) + } } } diff --git a/waltid-services/waltid-verifier-api/src/main/kotlin/id/walt/verifier/VerifierApi.kt b/waltid-services/waltid-verifier-api/src/main/kotlin/id/walt/verifier/VerifierApi.kt index 5fee20060..e7c7d1b01 100644 --- a/waltid-services/waltid-verifier-api/src/main/kotlin/id/walt/verifier/VerifierApi.kt +++ b/waltid-services/waltid-verifier-api/src/main/kotlin/id/walt/verifier/VerifierApi.kt @@ -427,7 +427,7 @@ fun Application.verfierApi() { add("signature") add("expired") add("not-before") - add("revoked_status_list") + add("revoked-status-list") }, presentationDefinitionJson = when (scope.contains("openid ver_test:vp_token")) { true -> Json.parseToJsonElement(fixedPresentationDefinitionForEbsiConformanceTest).jsonObject @@ -458,7 +458,7 @@ private fun getErrorDescription(it: Throwable): String? = when (it.message) { "Verification policies did not succeed: not-before" -> "<\$presentation_submission.descriptor_map[x].id> is not yet valid" - "Verification policies did not succeed: revoked_status_list" -> + "Verification policies did not succeed: revoked-status-list" -> "<\$presentation_submission.descriptor_map[x].id> is revoked" else -> null diff --git a/waltid-services/waltid-verifier-api/src/main/kotlin/id/walt/verifier/VerifierApiExamples.kt b/waltid-services/waltid-verifier-api/src/main/kotlin/id/walt/verifier/VerifierApiExamples.kt index 972100150..3d0aa7d3c 100644 --- a/waltid-services/waltid-verifier-api/src/main/kotlin/id/walt/verifier/VerifierApiExamples.kt +++ b/waltid-services/waltid-verifier-api/src/main/kotlin/id/walt/verifier/VerifierApiExamples.kt @@ -114,7 +114,7 @@ object VerifierApiExamples { """ { "vp_policies": $vpPolicyMinMaxData, - "vc_policies": ${vcPoliciesData("\"revoked_status_list\"")}, + "vc_policies": ${vcPoliciesData("\"revoked-status-list\"")}, "request_credentials": [ { "format": "jwt_vc_json", "type": "OpenBadgeCredential" }, @@ -131,7 +131,7 @@ object VerifierApiExamples { """ { "vp_policies": $vpPolicyMinMaxData, - "vc_policies": ${vcPoliciesData("\"revoked_status_list\"")}, + "vc_policies": ${vcPoliciesData("\"revoked-status-list\"")}, "request_credentials": [ { "format": "jwt_vc_json", "type": "VerifiableId" }, @@ -157,7 +157,7 @@ object VerifierApiExamples { """ { "vp_policies": $vpPolicyMinMaxData, - "vc_policies": ${vcPoliciesData("\"revoked_status_list\"")}, + "vc_policies": ${vcPoliciesData("\"revoked-status-list\"")}, "request_credentials": [ { "format": "jwt_vc_json", "type": "VerifiableId" }, @@ -229,7 +229,7 @@ object VerifierApiExamples { val EbsiVerifiablePDA1 = jsonObjectValueExampleDescriptorDsl( """ { - "vc_policies": ${vcPoliciesData("\"revoked_status_list\"")}, + "vc_policies": ${vcPoliciesData("\"revoked-status-list\"")}, "request_credentials": [ { "input_descriptor": { diff --git a/waltid-services/waltid-wallet-api/src/main/kotlin/id/walt/webwallet/web/controllers/exchange/ExchangeController.kt b/waltid-services/waltid-wallet-api/src/main/kotlin/id/walt/webwallet/web/controllers/exchange/ExchangeController.kt index b877dc435..ff7e891cd 100644 --- a/waltid-services/waltid-wallet-api/src/main/kotlin/id/walt/webwallet/web/controllers/exchange/ExchangeController.kt +++ b/waltid-services/waltid-wallet-api/src/main/kotlin/id/walt/webwallet/web/controllers/exchange/ExchangeController.kt @@ -1,6 +1,8 @@ package id.walt.webwallet.web.controllers.exchange +import id.walt.oid4vc.OpenID4VCI import id.walt.oid4vc.data.CredentialOffer +import id.walt.oid4vc.data.OpenIDProviderMetadata import id.walt.oid4vc.data.dif.PresentationDefinition import id.walt.oid4vc.requests.CredentialOfferRequest import id.walt.sdjwt.SDJWTVCTypeMetadata @@ -18,6 +20,7 @@ import io.github.smiley4.ktorswaggerui.dsl.routing.post import io.github.smiley4.ktorswaggerui.dsl.routing.route import io.ktor.http.* import io.ktor.server.application.* +import io.ktor.server.plugins.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.util.* @@ -261,6 +264,21 @@ fun Application.exchange() = walletRoute { context.respond(HttpStatusCode.BadRequest, error.message ?: "Unknown error") } } + get("resolveIssuerOpenIDMetadata", { + summary = "Resolved Issuer OpenID Metadata" + request { + queryParameter("issuer") + } + response { + HttpStatusCode.OK to { + description = "Resolved Issuer OpenID Metadata" + body() + } + } + }) { + val issuer = call.request.queryParameters["issuer"] ?: throw BadRequestException("Issuer base url not set") + context.respond(HttpStatusCode.OK, OpenID4VCI.resolveCIProviderMetadata(issuer).toJSON()) + } } } diff --git a/waltid-services/waltid-wallet-api/src/test/kotlin/id/walt/webwallet/utils/X5CValidatorTest.kt b/waltid-services/waltid-wallet-api/src/test/kotlin/id/walt/webwallet/utils/X5CValidatorTest.kt index cc50284af..2fcdd319e 100644 --- a/waltid-services/waltid-wallet-api/src/test/kotlin/id/walt/webwallet/utils/X5CValidatorTest.kt +++ b/waltid-services/waltid-wallet-api/src/test/kotlin/id/walt/webwallet/utils/X5CValidatorTest.kt @@ -14,10 +14,10 @@ import kotlin.test.assertTrue 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) + //we don't care about the bit size of the key, it's a test case (as long as it's bigger than 1024) private val keyPairGenerator = KeyPairGenerator .getInstance("RSA").apply { - initialize(1024) + initialize(2048) } //x.509 certificate expiration dates