diff --git a/modules/openid-federation-client/build.gradle.kts b/modules/openid-federation-client/build.gradle.kts index 0bce03c7..ad33a97e 100644 --- a/modules/openid-federation-client/build.gradle.kts +++ b/modules/openid-federation-client/build.gradle.kts @@ -31,9 +31,6 @@ kotlin { } } } - useEsModules() - generateTypeScriptDefinitions() - binaries.executable() } sourceSets { diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/ICallbackService.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/ICallbackService.kt index 4c81c66d..00feea03 100644 --- a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/ICallbackService.kt +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/ICallbackService.kt @@ -1,9 +1,11 @@ package com.sphereon.oid.fed.client import com.sphereon.oid.fed.client.httpclient.httpService +import com.sphereon.oid.fed.client.validation.trustChainValidationService object OidFederationClientService { val HTTP = httpService() + val TRUST_CHAIN_VALIDATION = trustChainValidationService() } diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/httpclient/OidFederationClient.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/httpclient/OidFederationClient.kt index ad07bc60..6f17ff32 100644 --- a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/httpclient/OidFederationClient.kt +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/httpclient/OidFederationClient.kt @@ -33,6 +33,7 @@ interface HttpClientCallbackService: ICallbackService, IHttp expect fun httpService(): HttpClientCallbackService +//FIXME Extract the implementation to a separate class object OidFederationHttpClientObject: HttpClientCallbackService { private val isRequestAuthenticated: Boolean = false diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidationConst.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidationConst.kt new file mode 100644 index 00000000..3d4df8b2 --- /dev/null +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidationConst.kt @@ -0,0 +1,9 @@ +package com.sphereon.oid.fed.client.validation + +import com.sphereon.oid.fed.common.logging.Logger + +object TrustChainValidationConst { + val LOG_NAMESPACE = "sphereon:kmp:openid-federation-client" + val LOG = Logger.Static.tag(LOG_NAMESPACE) + val TRUST_CHAIN_VALIDATION_LITERAL = "TRUST_CHAIN_VALIDATION" +} diff --git a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidation.kt b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidationObject.kt similarity index 57% rename from modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidation.kt rename to modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidationObject.kt index c06ebdc1..4c451aa0 100644 --- a/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidation.kt +++ b/modules/openid-federation-client/src/commonMain/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidationObject.kt @@ -1,30 +1,76 @@ package com.sphereon.oid.fed.client.validation -import com.sphereon.oid.fed.client.httpclient.OidFederationClient +import com.sphereon.oid.fed.client.ICallbackService +import com.sphereon.oid.fed.client.OidFederationClientService import com.sphereon.oid.fed.common.jwt.JwtService import com.sphereon.oid.fed.common.jwt.JwtVerifyInput -import com.sphereon.oid.fed.common.logging.Logger import com.sphereon.oid.fed.common.mapper.JsonMapper 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 io.ktor.client.engine.HttpClientEngine import kotlinx.datetime.Clock import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive +import kotlin.jvm.JvmStatic -class TrustChainValidationCommon(val jwtService: JwtService) { - +interface ITrustChainValidationCallback { suspend fun readAuthorityHints( partyBId: String, - engine: HttpClientEngine, + ): List> + + suspend fun fetchSubordinateStatements( + entityConfigurationStatementsList: List>, + ): List> + + suspend fun validateTrustChains( + jwts: List>, + knownTrustChainIds: List + ): List> +} + +interface TrustChainValidationCallback : ICallbackService, + ITrustChainValidationCallback + +expect fun trustChainValidationService(): TrustChainValidationCallback + +//FIXME Extract the actual implementation to a separate class +object TrustChainValidationObject : TrustChainValidationCallback { + + @JvmStatic + private lateinit var jwtService: JwtService + + @JvmStatic + private lateinit var httpService: OidFederationClientService + + @JvmStatic + private lateinit var platformCallback: ITrustChainValidationCallback + + private var disabled = false + + override suspend fun readAuthorityHints( + partyBId: String, + ): List> { + if (!isEnabled()) { + TrustChainValidationConst.LOG.info("TRUST_CHAIN_VALIDATION readAuthorityHints has been disabled") + throw IllegalStateException("TRUST_CHAIN_VALIDATION service is disabled; cannot read authority hints") + } else if (!this::platformCallback.isInitialized) { + TrustChainValidationConst.LOG.error( + "TRUST_CHAIN_VALIDATION callback (JS) is not registered" + ) + throw IllegalStateException("TRUST_CHAIN_VALIDATION have not been initialized. Please register your TrustChainValidationCallbacksServiceJS implementation, or register a default implementation") + } + return readAuthorityHintsImpl(partyBId) + } + + private suspend fun readAuthorityHintsImpl( + partyBId: String, trustChains: MutableList> = mutableListOf(), trustChain: MutableSet = mutableSetOf() ): List> { - OidFederationClient(engine).fetchEntityStatement(partyBId).run { + httpService.HTTP.fetchEntityStatement(partyBId).run { JsonMapper().mapEntityConfigurationStatement(this).let { if (it.authorityHints.isNullOrEmpty()) { trustChain.add(it) @@ -33,9 +79,8 @@ class TrustChainValidationCommon(val jwtService: JwtService) { } else { it.authorityHints?.forEach { hint -> trustChain.add(it) - readAuthorityHints( + readAuthorityHintsImpl( hint, - engine, trustChains, trustChain ) @@ -46,16 +91,24 @@ class TrustChainValidationCommon(val jwtService: JwtService) { return trustChains } - suspend fun fetchSubordinateStatements( + override suspend fun fetchSubordinateStatements( entityConfigurationStatementsList: List>, - engine: HttpClientEngine ): List> { + if (!isEnabled()) { + TrustChainValidationConst.LOG.info("TRUST_CHAIN_VALIDATION readAuthorityHints has been disabled") + throw IllegalStateException("TRUST_CHAIN_VALIDATION service is disabled; cannot read authority hints") + } else if (!this::platformCallback.isInitialized) { + TrustChainValidationConst.LOG.error( + "TRUST_CHAIN_VALIDATION callback (JS) is not registered" + ) + throw IllegalStateException("TRUST_CHAIN_VALIDATION have not been initialized. Please register your TrustChainValidationCallbacksServiceJS implementation, or register a default implementation") + } val trustChains: MutableList> = mutableListOf() val trustChain: MutableList = mutableListOf() entityConfigurationStatementsList.forEach { entityConfigurationStatements -> entityConfigurationStatements.forEach { it -> it.metadata?.jsonObject?.get("federation_entity")?.jsonObject?.get("federation_fetch_endpoint")?.jsonPrimitive?.content.let { url -> - OidFederationClient(engine).fetchEntityStatement(url.toString()).run { + httpService.HTTP.fetchEntityStatement(url.toString()).run { trustChain.add(this) } } @@ -66,22 +119,31 @@ class TrustChainValidationCommon(val jwtService: JwtService) { return trustChains } - fun validateTrustChains( + override suspend fun validateTrustChains( jwts: List>, knownTrustChainIds: List ): List> { + if (!isEnabled()) { + TrustChainValidationConst.LOG.info("TRUST_CHAIN_VALIDATION readAuthorityHints has been disabled") + throw IllegalStateException("TRUST_CHAIN_VALIDATION service is disabled; cannot read authority hints") + } else if (!this::platformCallback.isInitialized) { + TrustChainValidationConst.LOG.error( + "TRUST_CHAIN_VALIDATION callback (JS) is not registered" + ) + throw IllegalStateException("TRUST_CHAIN_VALIDATION have not been initialized. Please register your TrustChainValidationCallbacksServiceJS implementation, or register a default implementation") + } val trustChains: MutableList> = mutableListOf() - for(it in jwts) { + for (it in jwts) { try { trustChains.add(validateTrustChain(it, knownTrustChainIds)) } catch (e: Exception) { - Logger.debug("TrustChainValidation", e.message.toString()) + TrustChainValidationConst.LOG.error("Trust Chain Validation Error: ${e.message.toString()}") } } return trustChains } - private fun validateTrustChain(jwts: List, knownTrustChainIds: List): List { + private suspend fun validateTrustChain(jwts: List, knownTrustChainIds: List): List { val entityStatements = jwts.toMutableList() val firstEntityConfiguration = entityStatements.removeFirst().let { JsonMapper().mapEntityConfigurationStatement(it) } @@ -94,11 +156,13 @@ class TrustChainValidationCommon(val jwtService: JwtService) { } if (firstEntityConfiguration.jwks.jsonObject["keys"]?.jsonArray?.any { - jwtService.verify( + jwtService.JWT.verify( input = JwtVerifyInput( jwt = jwts[0], key = retrieveJwk(it) - )) } == false) { + ) + ) + } == false) { throw IllegalArgumentException("Invalid signature") } @@ -120,22 +184,27 @@ class TrustChainValidationCommon(val jwtService: JwtService) { if (current.iss != next.sub) { throw IllegalArgumentException("Entity Configuration of the Trust Chain subject requires that iss is equal to sub") } else if (next.jwks.jsonObject["keys"]?.jsonArray?.any { - jwtService.verify( + jwtService.JWT.verify( input = JwtVerifyInput( jwt = jwts[index], key = retrieveJwk(it) - )) } == false) { + ) + ) + } == false) { throw IllegalArgumentException("Invalid signature") } + is SubordinateStatement -> if (current.iss != next.sub) { throw IllegalArgumentException("Entity Configuration of the Trust Chain subject requires that iss is equal to sub") } else if (next.jwks.jsonObject["keys"]?.jsonArray?.any { - jwtService.verify( + jwtService.JWT.verify( input = JwtVerifyInput( jwt = jwts[index], key = retrieveJwk(it) - )) } == false) { + ) + ) + } == false) { throw IllegalArgumentException("Invalid signature") } } @@ -145,10 +214,13 @@ class TrustChainValidationCommon(val jwtService: JwtService) { throw IllegalArgumentException("Entity Configuration of the Trust Chain subject requires that iss is equal to the Entity Identifier of the Trust Anchor") } if (lastEntityConfiguration.jwks.jsonObject["keys"]?.jsonArray?.any { - jwtService.verify( + jwtService.JWT.verify( input = JwtVerifyInput( jwt = jwts[jwts.size - 1], - key = retrieveJwk(it))) } == false) { + key = retrieveJwk(it) + ) + ) + } == false) { throw IllegalArgumentException("Invalid signature") } @@ -169,7 +241,27 @@ class TrustChainValidationCommon(val jwtService: JwtService) { x = key["x"]?.jsonPrimitive?.content, y = key["y"]?.jsonPrimitive?.content ) + else -> throw IllegalArgumentException("Invalid key") } } + + override fun disable(): ITrustChainValidationCallback { + this.disabled = true + return this + } + + override fun enable(): ITrustChainValidationCallback { + this.disabled = false + return this + } + + override fun isEnabled(): Boolean { + return !this.disabled + } + + override fun register(platformCallback: ITrustChainValidationCallback): ITrustChainValidationCallback { + this.platformCallback = platformCallback + return this + } } diff --git a/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/ICallbackServiceJS.kt b/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/ICallbackServiceJS.kt index a55d9edf..d57c6db5 100644 --- a/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/ICallbackServiceJS.kt +++ b/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/ICallbackServiceJS.kt @@ -1,10 +1,12 @@ package com.sphereon.oid.fed.client import com.sphereon.oid.fed.client.httpclient.httpService +import com.sphereon.oid.fed.client.validation.trustChainValidationService object OidFederationClientServiceJS { val HTTP = httpService() + val TRUST_CHAIN_VALIDATION = trustChainValidationService() } /** diff --git a/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidation.js.kt b/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidation.js.kt index f5bad782..df87e3ea 100644 --- a/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidation.js.kt +++ b/modules/openid-federation-client/src/jsMain/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidation.js.kt @@ -1,52 +1,147 @@ package com.sphereon.oid.fed.client.validation -import com.sphereon.oid.fed.common.jwt.JwtService -import kotlinx.coroutines.CoroutineName -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.promise +import com.sphereon.oid.fed.client.ICallbackServiceJS +import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement +import kotlinx.coroutines.await +import kotlin.also import kotlin.js.Promise @ExperimentalJsExport @JsExport -class TrustChainValidation(val jwtService: JwtService) { +interface ITrustChainValidationCallbackJS { + fun readAuthorityHints( + partyBId: String, + ): Promise>> + + fun fetchSubordinateStatements( + entityConfigurationStatementsList: Array>, + ): Promise>> + + fun validateTrustChains( + jwts: Array>, + knownTrustChainIds: Array + ): Promise>> +} + +@ExperimentalJsExport +@JsExport +object TrustChainValidationServiceJS: ICallbackServiceJS, ITrustChainValidationCallbackJS { private val NAME = "TrustChainValidation" + private var disable = false - fun readAuthorityHints( + private lateinit var platformCallback: ITrustChainValidationCallbackJS + + override fun readAuthorityHints( partyBId: String, - engine: dynamic, - trustChains: dynamic, - trustChain: dynamic - ): Promise = CoroutineScope(context = CoroutineName(NAME)).promise { - TrustChainValidationCommon(jwtService) + ): Promise>> = + platformCallback .readAuthorityHints( - partyBId = partyBId, - engine = engine, - trustChains = trustChains, - trustChain = trustChain + partyBId = partyBId ) - } - fun fetchSubordinateStatements( - entityConfigurationStatementsList: dynamic, - engine: dynamic - ): Promise>> = CoroutineScope(context = CoroutineName(NAME)).promise { - TrustChainValidationCommon(jwtService) + override fun fetchSubordinateStatements( + entityConfigurationStatementsList: Array>, + ): Promise>> = + platformCallback .fetchSubordinateStatements( - entityConfigurationStatementsList = entityConfigurationStatementsList, - engine = engine + entityConfigurationStatementsList = entityConfigurationStatementsList ) + + override fun validateTrustChains( + jwts: Array>, + knownTrustChainIds: Array + ): Promise>> = platformCallback.validateTrustChains( + jwts = jwts, + knownTrustChainIds = knownTrustChainIds + ) + + override fun disable(): TrustChainValidationServiceJS { + this.disable = true + return this } - fun validateTrustChains( + override fun enable(): TrustChainValidationServiceJS { + this.disable = false + return this + } + + override fun isEnabled(): Boolean { + return !this.disable + } + + override fun register(platformCallback: ITrustChainValidationCallbackJS): TrustChainValidationServiceJS { + this.platformCallback = platformCallback + return this + } +} + +open class TrustChainValidationCallbackJSAdapter(val trustChainValidationServiceJS: TrustChainValidationServiceJS = TrustChainValidationServiceJS): + TrustChainValidationCallback { + + override suspend fun readAuthorityHints(partyBId: String): List> { + TrustChainValidationConst.LOG.debug("Calling TRUST CHAIN VALIDATION readAuthorityHints...") + + return try { + trustChainValidationServiceJS.readAuthorityHints(partyBId = partyBId).await().map { it.toList() }.toList() + } catch (e: Exception) { + throw e + }.also { + TrustChainValidationConst.LOG.info("Calling TRUST CHAIN VALIDATION readAuthorityHints result: $it") + } + } + + override suspend fun fetchSubordinateStatements(entityConfigurationStatementsList: List>): List> { + TrustChainValidationConst.LOG.debug("Calling TRUST CHAIN VALIDATION fetchSubordinateStatements...") + + return try { + val entities = entityConfigurationStatementsList.map { it.toTypedArray() }.toTypedArray() + trustChainValidationServiceJS.fetchSubordinateStatements( + entityConfigurationStatementsList = entities ).await().map { it.toList() }.toList() + } catch (e: Exception) { + throw e + }.also { + TrustChainValidationConst.LOG.info("Calling TRUST CHAIN VALIDATION fetchSubordinateStatements result: $it") + } + } + + override suspend fun validateTrustChains( jwts: List>, knownTrustChainIds: List - ): Promise>> = - Promise.resolve( - TrustChainValidationCommon(jwtService) - .validateTrustChains( - jwts = jwts, - knownTrustChainIds = knownTrustChainIds - ) - ) + ): List> { + TrustChainValidationConst.LOG.debug("Calling TRUST CHAIN VALIDATION validateTrustChains...") + + return try { + trustChainValidationServiceJS.validateTrustChains( + jwts = jwts.map { it.toTypedArray() }.toTypedArray(), + knownTrustChainIds = knownTrustChainIds.toTypedArray() + ).await().map { it.toList() }.toList() + } catch (e: Exception) { + throw e + }.also { + TrustChainValidationConst.LOG.info("Calling TRUST CHAIN VALIDATION validateTrustChains result: $it") + } + } + + override fun disable(): ITrustChainValidationCallback { + this.trustChainValidationServiceJS.disable() + return this + } + + override fun enable(): ITrustChainValidationCallback { + this.trustChainValidationServiceJS.disable() + return this + } + + override fun isEnabled(): Boolean { + return this.trustChainValidationServiceJS.isEnabled() + } + + override fun register(platformCallback: ITrustChainValidationCallback): ITrustChainValidationCallback { + throw Error("Register function should not be used on the adapter. It depends on the Javascript TrustChainValidationService object") + } } + +object TrustChainValidationCallbackJSObject: TrustChainValidationCallbackJSAdapter(TrustChainValidationServiceJS) + +actual fun trustChainValidationService(): TrustChainValidationCallback = TrustChainValidationCallbackJSObject diff --git a/modules/openid-federation-client/src/jsTest/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidationTest.kt b/modules/openid-federation-client/src/jsTest/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidationTest.kt index da3db5b6..10ed37ec 100644 --- a/modules/openid-federation-client/src/jsTest/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidationTest.kt +++ b/modules/openid-federation-client/src/jsTest/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidationTest.kt @@ -1,6 +1,7 @@ package com.sphereon.oid.fed.client.validation -import com.sphereon.oid.fed.common.jwt.JwtService +import com.sphereon.oid.fed.client.OidFederationClientServiceJS.TRUST_CHAIN_VALIDATION +import com.sphereon.oid.fed.common.jwt.IJwtServiceJS import com.sphereon.oid.fed.common.jwt.JwtSignInput import com.sphereon.oid.fed.common.jwt.JwtVerifyInput import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement @@ -15,7 +16,6 @@ import io.ktor.http.HttpStatusCode import io.ktor.http.Url import io.ktor.http.headersOf import io.ktor.utils.io.ByteReadChannel -import kotlinx.coroutines.await import kotlinx.coroutines.test.runTest import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.encodeToString @@ -24,7 +24,9 @@ import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.jsonObject +import kotlin.coroutines.suspendCoroutine import kotlin.js.Date +import kotlin.js.Promise import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals @@ -42,7 +44,7 @@ external object Jose { definedExternally } - fun sign(key: Any?, signOptions: Any?): String { + fun sign(key: Any?, signOptions: Any?): Promise { definedExternally } } @@ -79,16 +81,16 @@ fun convertToJwk(keyPair: dynamic): Jwk { ) } -class JwtServiceImpl: JwtService { - override fun sign(input: JwtSignInput): String { +class JwtServiceImpl: IJwtServiceJS { + override fun sign(input: JwtSignInput): Promise { return Jose.SignJWT(JSON.parse(Json.encodeToString(input.payload))) .setProtectedHeader(JSON.parse(Json.encodeToString(input.header))) .sign(key = input.key, null) } - override fun verify(input: JwtVerifyInput): Boolean { + override fun verify(input: JwtVerifyInput): Promise { val publicKey = Jose.importJWK(input.key, alg = input.key.alg ?: "RS256") - return Jose.jwtVerify(input.jwt, publicKey) + return Promise.resolve(Jose.jwtVerify(input.jwt, publicKey) !== undefined) } } @@ -171,7 +173,7 @@ class TrustChainValidationTest { federationFetchEndpoint = "https://edugain.org/federation/federation_fetch_endpoint" ) - partyBJwt = jwtServiceImpl.sign( + partyBJwt = suspendCoroutine { jwtServiceImpl.sign( JwtSignInput( payload = Json.encodeToJsonElement(serializer = EntityConfigurationStatement.serializer(), partyBConfiguration).jsonObject, header = JWTHeader( @@ -441,12 +443,9 @@ class TrustChainValidationTest { fun readAuthorityHintsTest() = runTest { assertEquals( listOfEntityConfigurationStatementList, - TrustChainValidation(jwtServiceImpl).readAuthorityHints( - partyBId = "https://edugain.org/federation", - engine = mockEngine, - trustChains = mutableListOf(mutableListOf()), - trustChain = mutableListOf(), - ).await() + TRUST_CHAIN_VALIDATION.readAuthorityHints( + partyBId = "https://edugain.org/federation" + ) ) } @@ -454,18 +453,15 @@ class TrustChainValidationTest { fun fetchSubordinateStatementsTest() = runTest { assertEquals( listOfSubordinateStatementList, - TrustChainValidation(jwtServiceImpl).fetchSubordinateStatements( - entityConfigurationStatementsList = listOfEntityConfigurationStatementList, - engine = mockEngine - ).await() + TRUST_CHAIN_VALIDATION.fetchSubordinateStatements( + entityConfigurationStatementsList = listOfEntityConfigurationStatementList + ) ) } @Test fun validateTrustChainTest() = runTest { - assertTrue( - TrustChainValidation(jwtServiceImpl).validateTrustChains(listOfSubordinateStatementList, listOf("https://openid.sunet-invalid.se", "https://openid.sunet-five.se")).await().size == 1 - ) + assertTrue(TRUST_CHAIN_VALIDATION.validateTrustChains(listOfSubordinateStatementList, listOf("https://openid.sunet-invalid.se", "https://openid.sunet-five.se")).size == 1) } } diff --git a/modules/openid-federation-client/src/jvmMain/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidation.jvm.kt b/modules/openid-federation-client/src/jvmMain/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidation.jvm.kt new file mode 100644 index 00000000..06aaeb4b --- /dev/null +++ b/modules/openid-federation-client/src/jvmMain/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidation.jvm.kt @@ -0,0 +1,3 @@ +package com.sphereon.oid.fed.client.validation + +actual fun trustChainValidationService(): TrustChainValidationCallback = TrustChainValidationObject diff --git a/modules/openid-federation-client/src/jvmTest/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidationTest.kt b/modules/openid-federation-client/src/jvmTest/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidationTest.kt index 74b23329..964a0050 100644 --- a/modules/openid-federation-client/src/jvmTest/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidationTest.kt +++ b/modules/openid-federation-client/src/jvmTest/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidationTest.kt @@ -12,7 +12,8 @@ import com.nimbusds.jose.jwk.ECKey import com.nimbusds.jose.jwk.gen.ECKeyGenerator import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT -import com.sphereon.oid.fed.common.jwt.JwtService +import com.sphereon.oid.fed.client.OidFederationClientService.TRUST_CHAIN_VALIDATION +import com.sphereon.oid.fed.common.jwt.IJwtService import com.sphereon.oid.fed.common.jwt.JwtSignInput import com.sphereon.oid.fed.common.jwt.JwtVerifyInput import com.sphereon.oid.fed.openapi.models.* @@ -20,6 +21,7 @@ import io.ktor.client.engine.mock.* import io.ktor.client.engine.mock.MockEngine.Companion.invoke import io.ktor.http.* import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -32,22 +34,22 @@ import java.time.OffsetDateTime import kotlin.test.Test import kotlin.test.assertEquals -class JwtServiceImpl : JwtService { - override fun sign(input: JwtSignInput): String { +class JwtServiceImpl : IJwtService { + override suspend fun sign(input: JwtSignInput): String { val jwkJsonString = Json.encodeToString(input.key) val ecJWK = ECKey.parse(jwkJsonString) val signer: JWSSigner = ECDSASigner(ecJWK) val jwsHeader = input.header.toJWSHeader() val signedJWT = SignedJWT( - jwsHeader, JWTClaimsSet.parse(input.payload.toString()) + jwsHeader, JWTClaimsSet.parse(JsonObject(input.payload).toString()) ) signedJWT.sign(signer) return signedJWT.serialize() } - override fun verify(input: JwtVerifyInput): Boolean { + override suspend fun verify(input: JwtVerifyInput): Boolean { try { val jwkJsonString = Json.encodeToString(input.key) val ecKey = ECKey.parse(jwkJsonString) @@ -196,7 +198,7 @@ class TrustChainValidationTest { federationFetchEndpoint = "https://edugain.org/federation/federation_fetch_endpoint" ) - partyBJwt = jwtService.sign( + partyBJwt = runBlocking { jwtService.sign( JwtSignInput( payload = Json.encodeToJsonElement( serializer = EntityConfigurationStatement.serializer(), @@ -209,7 +211,7 @@ class TrustChainValidationTest { ), key = partyBJwk ) - ) + ) } // Federation 2 intermediateEntityConfiguration = entityConfiguration( @@ -223,7 +225,7 @@ class TrustChainValidationTest { federationFetchEndpoint = "https://edugain.org/federation_two/federation_fetch_endpoint" ) - intermediateEntityConfigurationJwt = jwtService.sign( + intermediateEntityConfigurationJwt = runBlocking { jwtService.sign( JwtSignInput( payload = Json.encodeToJsonElement( serializer = EntityConfigurationStatement.serializer(), @@ -236,7 +238,7 @@ class TrustChainValidationTest { ), key = intermediateEntityConfiguration1Jwk ) - ) + ) } //signed with intermediateEntity1 Private Key intermediateEntitySubordinateStatement = intermediateEntity( @@ -245,7 +247,7 @@ class TrustChainValidationTest { sub = "https://openid.sunet.se", ) - intermediateEntitySubordinateStatementJwt = jwtService.sign( + intermediateEntitySubordinateStatementJwt = runBlocking { jwtService.sign( JwtSignInput( payload = Json.encodeToJsonElement( serializer = SubordinateStatement.serializer(), @@ -258,7 +260,7 @@ class TrustChainValidationTest { ), key = intermediateEntityConfiguration1Jwk ) - ) + ) } // Federation 4 intermediateEntityConfiguration1 = entityConfiguration( @@ -269,7 +271,7 @@ class TrustChainValidationTest { federationFetchEndpoint = "https://edugain.org/federation_four/federation_fetch_endpoint" ) - intermediateEntityConfiguration1Jwt = jwtService.sign( + intermediateEntityConfiguration1Jwt = runBlocking { jwtService.sign( JwtSignInput( payload = Json.encodeToJsonElement( serializer = EntityConfigurationStatement.serializer(), @@ -282,7 +284,7 @@ class TrustChainValidationTest { ), key = validTrustAnchorConfigurationJwk ) - ) + ) } intermediateEntity1SubordinateStatement = intermediateEntity( publicKey = intermediateEntity1KeyPair.toPublicJWK(), @@ -290,7 +292,7 @@ class TrustChainValidationTest { sub = "https://openid.sunet-one.se" ) - intermediateEntity1SubordinateStatementJwt = jwtService.sign( + intermediateEntity1SubordinateStatementJwt = runBlocking { jwtService.sign( JwtSignInput( payload = Json.encodeToJsonElement( serializer = SubordinateStatement.serializer(), @@ -303,7 +305,7 @@ class TrustChainValidationTest { ), key = validTrustAnchorConfigurationJwk ) - ) + ) } // Federation 5 validTrustAnchorConfiguration = entityConfiguration( @@ -314,7 +316,7 @@ class TrustChainValidationTest { federationFetchEndpoint = "https://edugain.org/federation_five/federation_fetch_endpoint" ) - validTrustAnchorConfigurationJwt = jwtService.sign( + validTrustAnchorConfigurationJwt = runBlocking { jwtService.sign( JwtSignInput( payload = Json.encodeToJsonElement( serializer = EntityConfigurationStatement.serializer(), @@ -327,7 +329,7 @@ class TrustChainValidationTest { ), key = validTrustAnchorConfigurationJwk ) - ) + ) } // Federation 3 unknownTrustAnchorConfiguration = entityConfiguration( @@ -338,7 +340,7 @@ class TrustChainValidationTest { federationFetchEndpoint = "https://edugain.org/federation_three/federation_fetch_endpoint" ) - unknownTrustAnchorConfigurationJwt = jwtService.sign( + unknownTrustAnchorConfigurationJwt = runBlocking { jwtService.sign( JwtSignInput( payload = Json.encodeToJsonElement( serializer = EntityConfigurationStatement.serializer(), @@ -351,7 +353,7 @@ class TrustChainValidationTest { ), key = unknownTrustAnchorConfigurationJwk ) - ) + ) } // Federation 1 invalidTrustAnchorConfiguration = entityConfiguration( @@ -362,7 +364,7 @@ class TrustChainValidationTest { federationFetchEndpoint = "https://edugain.org/federation_one/federation_fetch_endpoint" ) - invalidTrustAnchorConfigurationJwt = jwtService.sign( + invalidTrustAnchorConfigurationJwt = runBlocking { jwtService.sign( JwtSignInput( payload = Json.encodeToJsonElement( serializer = EntityConfigurationStatement.serializer(), @@ -375,7 +377,7 @@ class TrustChainValidationTest { ), key = invalidTrustAnchorConfigurationJwk ) - ) + ) } listOfEntityConfigurationStatementList = mutableListOf( mutableListOf( @@ -497,9 +499,8 @@ class TrustChainValidationTest { fun readAuthorityHintsTest() = runTest { assertEquals( listOfEntityConfigurationStatementList.toString(), - TrustChainValidationCommon(jwtService).readAuthorityHints( - partyBId = "https://edugain.org/federation", - engine = mockEngine + TRUST_CHAIN_VALIDATION.readAuthorityHints( + partyBId = "https://edugain.org/federation" ).toString() ) } @@ -508,9 +509,8 @@ class TrustChainValidationTest { fun fetchSubordinateStatementsTest() = runTest { assertEquals( listOfSubordinateStatementList, - TrustChainValidationCommon(jwtService).fetchSubordinateStatements( - entityConfigurationStatementsList = listOfEntityConfigurationStatementList, - engine = mockEngine + TRUST_CHAIN_VALIDATION.fetchSubordinateStatements( + entityConfigurationStatementsList = listOfEntityConfigurationStatementList ) ) } @@ -518,7 +518,7 @@ class TrustChainValidationTest { @Test fun validateTrustChainTest() = runTest { assertTrue( - TrustChainValidationCommon(jwtService).validateTrustChains( + TRUST_CHAIN_VALIDATION.validateTrustChains( listOfSubordinateStatementList, listOf("https://openid.sunet-invalid.se", "https://openid.sunet-five.se") ).size == 1