Skip to content

Commit 8684838

Browse files
authored
Merge branch 'main' into Azure-kms-inetgration
2 parents dca1e77 + 183ff59 commit 8684838

File tree

24 files changed

+227
-72
lines changed

24 files changed

+227
-72
lines changed

waltid-applications/waltid-cli/README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,7 @@ Usage: waltid vc verify [<options>] <vc>
512512
╰─────────────────────────────────────────────────────────────────────────────────────────╯
513513
514514
Options:
515-
-p, --policy=(signature|expired|not-before|revoked_status_list|schema|allowed-issuer|webhook)
515+
-p, --policy=(signature|expired|not-before|revoked-status-list|schema|allowed-issuer|webhook)
516516
Specify one, or more policies to be applied during the verification process of the VC (signature policy is always applied).
517517
-a, --arg=<value> Argument required by some policies, namely:
518518
@@ -525,7 +525,7 @@ Options:
525525
├─────────────────────┼─────────────────────────────────────────────────────────────────┤
526526
│ not-before │ - │
527527
├─────────────────────┼─────────────────────────────────────────────────────────────────┤
528-
revoked_status_list │ - │
528+
revoked-status-list │ - │
529529
├─────────────────────┼─────────────────────────────────────────────────────────────────┤
530530
│ schema │ schema=/path/to/schema.json │
531531
├─────────────────────┼─────────────────────────────────────────────────────────────────┤

waltid-applications/waltid-cli/src/commonMain/kotlin/id/walt/cli/commands/VCVerifyCmd.kt

+4-4
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ class VCVerifyCmd : CliktCommand(
5757
"signature",
5858
"expired",
5959
"not-before",
60-
"revoked_status_list",
60+
"revoked-status-list",
6161
"schema",
6262
"allowed-issuer",
6363
"webhook",
@@ -75,7 +75,7 @@ class VCVerifyCmd : CliktCommand(
7575
|signature| - |
7676
|expired| - |
7777
|not-before| - |
78-
|revoked_status_list| - |
78+
|revoked-status-list| - |
7979
|schema|schema=/path/to/schema.json|
8080
|allowed-issuer|issuer=did:key:z6Mkp7AVwvWxnsNDuSSbf19sgKzrx223WY95AqZyAGifFVyV|
8181
|webhook|url=https://example.com|
@@ -145,7 +145,7 @@ class VCVerifyCmd : CliktCommand(
145145
args.putAll(getAllowedIssuerPolicyArguments())
146146
args.putAll(getWebhookPolicyArguments())
147147
args.putAll(getRevocationPolicyArguments())
148-
for (noArgPolicyName in listOf("signature", "expired", "not-before", "revoked_status_list")) {
148+
for (noArgPolicyName in listOf("signature", "expired", "not-before", "revoked-status-list")) {
149149
if (noArgPolicyName in policies) {
150150
args[noArgPolicyName] = "".toJsonElement()
151151
}
@@ -205,7 +205,7 @@ class VCVerifyCmd : CliktCommand(
205205

206206
private fun getRevocationPolicyArguments(): Map<out String, JsonElement> {
207207
val args = mutableMapOf<String, JsonElement>()
208-
if ("revoked_status_list" in policies) {
208+
if ("revoked-status-list" in policies) {
209209
args["vc"] = vc.readText().toJsonElement()
210210
}
211211

waltid-applications/waltid-cli/src/jvmTest/kotlin/id/walt/cli/commands/WaltIdVCVerifyCmdTest.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -330,11 +330,11 @@ class WaltIdVCVerifyCmdTest {
330330
fun `should output Success when the credential does not contain a revocation status list entry`() = runTest {
331331
val result = command.test(
332332
listOf(
333-
"--policy=revoked_status_list",
333+
"--policy=revoked-status-list",
334334
signedVCFilePath,
335335
)
336336
)
337-
assertContains(result.output, "revoked_status_list: Success!")
337+
assertContains(result.output, "revoked-status-list: Success!")
338338
}
339339

340340
private fun sign(vcFilePath: String): String {

waltid-applications/waltid-web-wallet/libs/composables/issuance.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export async function useIssuance(query: any) {
4747
issuerHost = issuer;
4848
}
4949

50-
const credential_issuer: { credential_configurations_supported: Array<{ types: Array<String>; }>; } = await $fetch(`${issuer}/.well-known/openid-credential-issuer`)
50+
const credential_issuer: { credential_configurations_supported: Array<{ types: Array<String>; }>; } = await $fetch(`/wallet-api/wallet/${currentWallet.value}/exchange/resolveIssuerOpenIDMetadata?issuer=${issuer}`)
5151
const credentialList = credentialOffer.credential_configuration_ids.map((id) => credential_issuer.credential_configurations_supported[id]);
5252

5353
let credentialTypes: String[] = [];
@@ -122,4 +122,4 @@ export async function useIssuance(query: any) {
122122
groupedCredentialTypes,
123123
issuerHost
124124
}
125-
}
125+
}

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/RevocationPolicy.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import kotlinx.serialization.json.JsonObject
99
abstract class RevocationPolicyMp : CredentialWrapperValidatorPolicy(
1010
) {
1111

12-
override val name = "revoked_status_list"
12+
override val name = "revoked-status-list"
1313
override val description = "Verifies Credential Status"
1414
override val supportedVCFormats = setOf(VCFormat.jwt_vc, VCFormat.jwt_vc_json, VCFormat.ldp_vc)
1515

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/credentials/waltid-verification-policies/src/jvmMain/kotlin/id/walt/policies/policies/RevocationPolicy.jvm.kt

+4-2
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ actual class RevocationPolicy : RevocationPolicyMp() {
4747
val credentialSubject = payload["vc"]!!.jsonObject["credentialSubject"]?.jsonObject!!
4848
val encodedList = credentialSubject["encodedList"]?.jsonPrimitive?.content ?: ""
4949
val bitValue = get(encodedList, statusListIndex)
50-
if (bitValue!![0].code == 0) {
50+
if (StreamUtils.binToInt(bitValue!!.joinToString("")) == 0) {
5151
Result.success(statusListCredentialUrl!!)
5252
} else {
5353
Result.failure(Throwable("Credential has been revoked"))
@@ -67,6 +67,8 @@ object Base64Utils {
6767
object StreamUtils {
6868
private const val BITS_PER_BYTE = 8u
6969

70+
fun binToInt(bin: String) = bin.toInt(2)
71+
7072
fun getBitValue(inputStream: InputStream, index: ULong, bitSize: Int): List<Char> = inputStream.use { stream ->
7173
//TODO: bitSize constraints
7274
val bitStartPosition = index * bitSize.toUInt()
@@ -89,4 +91,4 @@ object StreamUtils {
8991
}
9092

9193
fun get(bitstring: String, idx: ULong? = null, bitSize: Int = 1) =
92-
idx?.let { StreamUtils.getBitValue(GZIPInputStream(Base64Utils.decode(bitstring).inputStream()), it, bitSize) }
94+
idx?.let { StreamUtils.getBitValue(GZIPInputStream(Base64Utils.decode(bitstring).inputStream()), it, bitSize) }

waltid-libraries/crypto/waltid-crypto-android/src/androidMain/kotlin/id/walt/crypto/keys/AndroidKey.kt

+2
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,8 @@ class AndroidKey() : Key() {
201201
TODO("Not yet implemented")
202202
}
203203

204+
override suspend fun deleteKey(): Boolean = kotlin.runCatching { keyStore.deleteEntry(internalKeyId) }.isSuccess
205+
204206
private fun getSignature(): Signature {
205207
val sig = when (keyType) {
206208
KeyType.secp256k1 -> Signature.getInstance(

waltid-libraries/crypto/waltid-crypto-ios/src/iosMain/kotlin/id/walt/crypto/IosKey.kt

+9
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,15 @@ class IosKey private constructor(
222222
override suspend fun getMeta(): KeyMeta {
223223
error("Not yet implemented")
224224
}
225+
226+
override suspend fun deleteKey(): Boolean = kotlin.runCatching {
227+
when (options.keyType) {
228+
KeyType.secp256r1 -> P256.PrivateKey.deleteFromKeychain(options.kid)
229+
KeyType.Ed25519 -> Ed25519.PrivateKey.deleteFromKeychain(options.kid)
230+
KeyType.RSA -> RSA.PrivateKey.deleteFromKeychain(options.kid)
231+
else -> error("Not implemented")
232+
}
233+
}.isSuccess
225234
}
226235

227236
// utility functions for swift

waltid-libraries/crypto/waltid-crypto/src/iosMain/kotlin/id/walt/crypto/keys/jwk/JWKKey.ios.kt

+4
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ actual class JWKKey actual constructor(private val jwk: String?, private val _ke
109109
TODO("Not yet implemented")
110110
}
111111

112+
actual override suspend fun deleteKey(): Boolean {
113+
TODO("Not yet implemented")
114+
}
115+
112116
actual override val hasPrivateKey: Boolean
113117
get() = _jwkObj.toMap().any { it.key in privateParameters }
114118

waltid-libraries/protocols/waltid-openid4vc/src/commonMain/kotlin/id/walt/oid4vc/OpenID4VCI.kt

+3-1
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,9 @@ object OpenID4VCI {
131131
appendPathSegments(it.fullPath.trim('/'))
132132
}.buildString() }
133133

134-
suspend fun resolveCIProviderMetadata(credOffer: CredentialOffer) = http.get(getCIProviderMetadataUrl(credOffer)).bodyAsText().let {
134+
suspend fun resolveCIProviderMetadata(credOffer: CredentialOffer) = resolveCIProviderMetadata(credOffer.credentialIssuer)
135+
136+
suspend fun resolveCIProviderMetadata(issuerBaseUrl: String) = http.get(getCIProviderMetadataUrl(issuerBaseUrl)).bodyAsText().let {
135137
OpenIDProviderMetadata.fromJSONString(it)
136138
}
137139

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package id.walt.oid4vc.util
2+
3+
import id.walt.oid4vc.providers.TokenTarget
4+
5+
actual object COSESign1Utils {
6+
actual fun verifyCOSESign1Signature(
7+
target: TokenTarget,
8+
token: String
9+
): Boolean {
10+
TODO("Not yet implemented")
11+
}
12+
}

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

0 commit comments

Comments
 (0)