Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/Azure-kms-inetgration' into Azur…
Browse files Browse the repository at this point in the history
…e-kms-inetgration
  • Loading branch information
waltkb committed Dec 11, 2024
2 parents 9a55e5f + 8684838 commit 071915a
Show file tree
Hide file tree
Showing 24 changed files with 227 additions and 72 deletions.
4 changes: 2 additions & 2 deletions waltid-applications/waltid-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,7 @@ Usage: waltid vc verify [<options>] <vc>
╰─────────────────────────────────────────────────────────────────────────────────────────╯
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=<value> Argument required by some policies, namely:
Expand All @@ -525,7 +525,7 @@ Options:
├─────────────────────┼─────────────────────────────────────────────────────────────────┤
│ not-before │ - │
├─────────────────────┼─────────────────────────────────────────────────────────────────┤
revoked_status_list │ - │
revoked-status-list │ - │
├─────────────────────┼─────────────────────────────────────────────────────────────────┤
│ schema │ schema=/path/to/schema.json │
├─────────────────────┼─────────────────────────────────────────────────────────────────┤
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class VCVerifyCmd : CliktCommand(
"signature",
"expired",
"not-before",
"revoked_status_list",
"revoked-status-list",
"schema",
"allowed-issuer",
"webhook",
Expand All @@ -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|
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -205,7 +205,7 @@ class VCVerifyCmd : CliktCommand(

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export async function useIssuance(query: any) {
issuerHost = issuer;
}

const credential_issuer: { credential_configurations_supported: Array<{ types: Array<String>; }>; } = await $fetch(`${issuer}/.well-known/openid-credential-issuer`)
const credential_issuer: { credential_configurations_supported: Array<{ types: Array<String>; }>; } = 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[] = [];
Expand Down Expand Up @@ -122,4 +122,4 @@ export async function useIssuance(query: any) {
groupedCredentialTypes,
issuerHost
}
}
}
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 @@ -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)

Expand Down
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 @@ -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"))
Expand All @@ -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<Char> = inputStream.use { stream ->
//TODO: bitSize constraints
val bitStartPosition = index * bitSize.toUInt()
Expand All @@ -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) }
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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")
}
}
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
Loading

0 comments on commit 071915a

Please sign in to comment.