Skip to content

Commit

Permalink
fix: verify function
Browse files Browse the repository at this point in the history
  • Loading branch information
jcmelati committed Oct 19, 2024
1 parent 4c413c9 commit 2fa41b4
Show file tree
Hide file tree
Showing 8 changed files with 156 additions and 101 deletions.
Original file line number Diff line number Diff line change
@@ -1,33 +1,39 @@
package com.sphereon.oid.fed.client.crypto

import com.sphereon.oid.fed.client.types.ICallbackService

import com.sphereon.oid.fed.openapi.models.Jwk

interface ICryptoService {
suspend fun verify(
jwt: String,
key: Jwk,
): Boolean
}

interface ICryptoCallbackService : ICallbackService<ICryptoService>, ICryptoService

expect fun cryptoService(): ICryptoCallbackService

class DefaultPlatformCallback : ICryptoService {
override suspend fun verify(jwt: String): Boolean {
return verify(jwt)
override suspend fun verify(jwt: String, key: Jwk): Boolean {
return verifyImpl(jwt, key)
}
}

object CryptoServiceObject : ICryptoCallbackService {
private lateinit var platformCallback: ICryptoService

override suspend fun verify(jwt: String): Boolean {
return this.platformCallback.verify(jwt)
override suspend fun verify(jwt: String, key: Jwk): Boolean {
if (!::platformCallback.isInitialized) {
throw IllegalStateException("CryptoServiceObject not initialized")
}
return platformCallback.verify(jwt, key)
}

override fun register(platformCallback: ICryptoService?): ICryptoCallbackService {
this.platformCallback = platformCallback ?: DefaultPlatformCallback()
return this
}
}

expect fun cryptoService(): ICryptoCallbackService

expect suspend fun verifyImpl(jwt: String, key: Jwk): Boolean
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ object FetchServiceObject : IFetchCallbackService {
private lateinit var httpClient: HttpClient

override suspend fun fetchStatement(endpoint: String): String {
return httpClient.get(endpoint) {
headers {
append(HttpHeaders.Accept, "application/entity-statement+jwt")
}
}.body()
return httpClient
.get(endpoint) {
headers {
append(HttpHeaders.Accept, "application/entity-statement+jwt")
}
}.body()
}

override fun getHttpClient(): HttpClient {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import com.sphereon.oid.fed.client.mapper.mapEntityStatement
import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement
import com.sphereon.oid.fed.openapi.models.Jwk
import com.sphereon.oid.fed.openapi.models.SubordinateStatement
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlin.collections.set

class SimpleCache<K, V> {
Expand All @@ -28,7 +33,20 @@ class TrustChain(private val fetchService: IFetchCallbackService, private val cr
): MutableList<String>? {
val cache = SimpleCache<String, String>()
val chain: MutableList<String> = arrayListOf()
return buildTrustChainRecursive(entityIdentifier, trustAnchors, chain, cache)
return try {
buildTrustChainRecursive(entityIdentifier, trustAnchors, chain, cache)
} catch (_: Exception) {
// Log error
null
}
}

private fun findKeyInJwks(keys: JsonArray, kid: String): Jwk? {
val key = keys.firstOrNull { it.jsonObject["kid"]?.jsonPrimitive?.content?.trim() == kid.trim() }

if (key == null) return null

return Json.decodeFromJsonElement(Jwk.serializer(), key)
}

private suspend fun buildTrustChainRecursive(
Expand All @@ -37,12 +55,17 @@ class TrustChain(private val fetchService: IFetchCallbackService, private val cr
chain: MutableList<String>,
cache: SimpleCache<String, String>
): MutableList<String>? {

val entityConfigurationJwt = this.fetchService.fetchStatement(getEntityConfigurationEndpoint(entityIdentifier))

val decodedEntityConfiguration = decodeJWTComponents(entityConfigurationJwt)

if (!cryptoService.verify(entityConfigurationJwt)) {
val key = findKeyInJwks(
decodedEntityConfiguration.payload["jwks"]?.jsonObject?.get("keys")?.jsonArray ?: return null,
decodedEntityConfiguration.header.kid
)

if (key == null) return null

if (!cryptoService.verify(entityConfigurationJwt, key)) {
return null
}

Expand All @@ -64,6 +87,7 @@ class TrustChain(private val fetchService: IFetchCallbackService, private val cr
decodedEntityConfiguration.header.kid,
cache
)

if (result != null) {
return result
}
Expand All @@ -90,7 +114,21 @@ class TrustChain(private val fetchService: IFetchCallbackService, private val cr
val authorityEntityConfigurationJwt = fetchService.fetchStatement(authorityConfigurationEndpoint)
cache.put(authorityConfigurationEndpoint, authorityEntityConfigurationJwt)

if (!cryptoService.verify(authorityEntityConfigurationJwt)) {
val decodedJwt = decodeJWTComponents(authorityEntityConfigurationJwt)
val kid = decodedJwt.header.kid

val key = findKeyInJwks(
decodedJwt.payload["jwks"]?.jsonObject?.get("keys")?.jsonArray ?: return null,
kid
)

if (key == null) return null

if (!cryptoService.verify(
authorityEntityConfigurationJwt,
key
)
) {
return null
}

Expand All @@ -109,7 +147,17 @@ class TrustChain(private val fetchService: IFetchCallbackService, private val cr

val subordinateStatementJwt = fetchService.fetchStatement(subordinateStatementEndpoint)

if (!cryptoService.verify(subordinateStatementJwt)) {
val decodedSubordinateStatement = decodeJWTComponents(subordinateStatementJwt)

val subordinateStatementKey = findKeyInJwks(
decodedJwt.payload["jwks"]?.jsonObject?.get("keys")?.jsonArray
?: return null,
decodedSubordinateStatement.header.kid.trim()
)

if (subordinateStatementKey == null) return null

if (!cryptoService.verify(subordinateStatementJwt, subordinateStatementKey)) {
return null
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.sphereon.oid.fed.client.trustchain
import com.sphereon.oid.fed.client.FederationClient
import com.sphereon.oid.fed.client.crypto.ICryptoService
import com.sphereon.oid.fed.client.fetch.IFetchService
import com.sphereon.oid.fed.openapi.models.Jwk
import io.ktor.client.*
import io.ktor.client.engine.mock.*
import io.ktor.http.*
Expand All @@ -27,7 +28,7 @@ class PlatformCallback : IFetchService {
}

class CryptoServiceCallback : ICryptoService {
override suspend fun verify(jwt: String): Boolean {
override suspend fun verify(jwt: String, jwk: Jwk): Boolean {
return true
}
}
Expand All @@ -44,7 +45,8 @@ class TrustChainTest() {
)

assertNotNull(trustChain)
assertEquals(trustChain.size, 4)

assertEquals(4, trustChain.size)

assertEquals(
trustChain[0],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,21 @@ package com.sphereon.oid.fed.client.crypto
import com.sphereon.oid.fed.client.mapper.decodeJWTComponents
import com.sphereon.oid.fed.client.types.ICallbackService
import com.sphereon.oid.fed.openapi.models.Jwk
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.await
import kotlinx.coroutines.promise
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlin.js.Promise

@JsModule("jose")
@JsNonModule
external object Jose {
fun importJWK(jwk: Jwk, alg: String, options: dynamic = definedExternally): Promise<dynamic>
fun jwtVerify(jwt: String, key: Any, options: dynamic = definedExternally): Promise<dynamic>
}

@JsExport
@JsName("ICallbackCryptoService")
external interface ICallbackCryptoServiceJS<PlatformCallbackType> {
Expand All @@ -23,46 +30,22 @@ external interface ICallbackCryptoServiceJS<PlatformCallbackType> {
fun register(platformCallback: PlatformCallbackType?): ICallbackCryptoServiceJS<PlatformCallbackType>
}

@JsExport
external interface ICryptoServiceCallbackJS {
@JsName("verify")
fun verify(
jwt: String,
key: Jwk
): Promise<Boolean>
}

class DefaultPlatformCallbackJS : ICryptoServiceCallbackJS {
override fun verify(jwt: String): Promise<Boolean> {
return try {
val decodedJwt = decodeJWTComponents(jwt)
val kid = decodedJwt.header.kid

val jwk = decodedJwt.payload["jwks"]?.jsonObject?.get("keys")?.jsonArray
?.firstOrNull { it.jsonObject["kid"]?.jsonPrimitive?.content == kid }
?: throw Exception("JWK not found")

Jose.importJWK(
JSON.parse<dynamic>(Json.encodeToString(jwk)), alg = decodedJwt.header.alg ?: "RS256"
).then { publicKey: dynamic ->
Jose.jwtVerify(jwt, publicKey).then { verification: dynamic ->
println("Verification result: $verification")
verification != undefined
}.catch { error ->
println("Error during JWT verification: $error")
false
}
}.catch { error ->
println("Error importing JWK: $error")
false
}
} catch (e: Throwable) {
println("Error: $e")
Promise.resolve(false)
}
@OptIn(DelicateCoroutinesApi::class)
override fun verify(jwt: String, key: Jwk): Promise<Boolean> {
return GlobalScope.promise { verifyImpl(jwt, key) }
}
}

@JsExport
@JsName("CryptoService")
object CryptoServiceJS : ICallbackCryptoServiceJS<ICryptoServiceCallbackJS>, ICryptoServiceCallbackJS {
private lateinit var platformCallback: ICryptoServiceCallbackJS

Expand All @@ -71,8 +54,12 @@ object CryptoServiceJS : ICallbackCryptoServiceJS<ICryptoServiceCallbackJS>, ICr
return this
}

override fun verify(jwt: String): Promise<Boolean> {
return platformCallback.verify(jwt)
override fun verify(jwt: String, key: Jwk): Promise<Boolean> {
if (!::platformCallback.isInitialized) {
throw IllegalStateException("CryptoServiceJS not initialized")
}

return platformCallback.verify(jwt, key)
}
}

Expand All @@ -82,18 +69,26 @@ open class CryptoServiceJSAdapter(private val cryptoServiceCallbackJS: CryptoSer
throw Error("Register function should not be used on the adapter. It depends on the Javascript CryptoService object")
}

override suspend fun verify(jwt: String): Boolean {
return cryptoServiceCallbackJS.verify(jwt).await()
override suspend fun verify(jwt: String, key: Jwk): Boolean {
return cryptoServiceCallbackJS.verify(jwt, key).await()
}
}

object CryptoServiceJSAdapterObject : CryptoServiceJSAdapter(CryptoServiceJS)

actual fun cryptoService(): ICryptoCallbackService = CryptoServiceJSAdapterObject

@JsModule("jose")
@JsNonModule
external object Jose {
fun importJWK(jwk: Jwk, alg: String, options: dynamic = definedExternally): Promise<dynamic>
fun jwtVerify(jwt: String, key: Any, options: dynamic = definedExternally): Promise<Boolean>
actual suspend fun verifyImpl(jwt: String, key: Jwk): Boolean {
try {
val decodedJwt = decodeJWTComponents(jwt)

val publicKey = Jose.importJWK(
JSON.parse<dynamic>(Json.encodeToString(key)), alg = decodedJwt.header.alg ?: "RS256"
).await()

val verification = Jose.jwtVerify(jwt, publicKey).await()
return verification != undefined
} catch (e: Throwable) {
return false
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
package com.sphereon.oid.fed.client.crypto

import com.sphereon.oid.fed.client.mapper.decodeJWTComponents
import com.sphereon.oid.fed.openapi.models.Jwk
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlin.js.Promise

class CryptoPlatformCallback : ICryptoServiceCallbackJS {
override fun verify(jwt: String): Promise<Boolean> {
override fun verify(jwt: String, key: Jwk): Promise<Boolean> {
return try {
val decodedJwt = decodeJWTComponents(jwt)
val kid = decodedJwt.header.kid

val jwk = decodedJwt.payload["jwks"]?.jsonObject?.get("keys")?.jsonArray
?.firstOrNull { it.jsonObject["kid"]?.jsonPrimitive?.content == kid }
?: throw Exception("JWK not found")

Jose.importJWK(
JSON.parse<dynamic>(Json.encodeToString(jwk)), alg = decodedJwt.header.alg ?: "RS256"
JSON.parse<dynamic>(Json.encodeToString(key)), alg = decodedJwt.header.alg ?: "RS256"
).then { publicKey: dynamic ->
val options: dynamic = js("({})")
options["currentDate"] = js("new Date(Date.parse(\"Oct 14, 2024 01:00:00\"))")
Expand Down
Loading

0 comments on commit 2fa41b4

Please sign in to comment.