diff --git a/modules/openid-federation-client/build.gradle.kts b/modules/openid-federation-client/build.gradle.kts index 9e76b67a..600eb450 100644 --- a/modules/openid-federation-client/build.gradle.kts +++ b/modules/openid-federation-client/build.gradle.kts @@ -82,13 +82,13 @@ kotlin { dependencies { implementation("io.ktor:ktor-client-core-jvm:$ktorVersion") runtimeOnly("io.ktor:ktor-client-cio-jvm:$ktorVersion") - implementation("com.nimbusds:nimbus-jose-jwt:9.40") implementation(project(":modules:openid-federation-common")) } } val jvmTest by getting { dependencies { implementation(kotlin("test-junit")) + implementation("com.nimbusds:nimbus-jose-jwt:9.40") } } // TODO Should be placed back at a later point in time: https://sphereon.atlassian.net/browse/OIDF-50 @@ -141,7 +141,6 @@ kotlin { runtimeOnly("io.ktor:ktor-client-core-js:$ktorVersion") runtimeOnly("io.ktor:ktor-client-js:$ktorVersion") implementation(npm("typescript", "5.5.3")) - implementation(npm("jose", "5.6.3")) implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0-RC") implementation(project(":modules:openid-federation-common")) @@ -151,6 +150,7 @@ kotlin { val jsTest by getting { dependencies { implementation(kotlin("test-js")) + implementation(npm("jose", "5.6.3")) implementation(kotlin("test-annotations-common")) implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0-RC") } 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/TrustChainValidation.kt index 90b71213..c06ebdc1 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/TrustChainValidation.kt @@ -1,27 +1,22 @@ package com.sphereon.oid.fed.client.validation import com.sphereon.oid.fed.client.httpclient.OidFederationClient +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.js.ExperimentalJsExport -import kotlin.js.JsExport -expect class TrustChainValidation { - fun validateTrustChains( - jwts: List>, - knownTrustChainIds: List - ): List> -} - -@ExperimentalJsExport -@JsExport -class TrustChainValidationCommon { +class TrustChainValidationCommon(val jwtService: JwtService) { suspend fun readAuthorityHints( partyBId: String, @@ -71,7 +66,101 @@ class TrustChainValidationCommon { return trustChains } - fun retrieveJwk(key: JsonElement): Jwk { + fun validateTrustChains( + jwts: List>, + knownTrustChainIds: List + ): List> { + val trustChains: MutableList> = mutableListOf() + for(it in jwts) { + try { + trustChains.add(validateTrustChain(it, knownTrustChainIds)) + } catch (e: Exception) { + Logger.debug("TrustChainValidation", e.message.toString()) + } + } + return trustChains + } + + private fun validateTrustChain(jwts: List, knownTrustChainIds: List): List { + val entityStatements = jwts.toMutableList() + val firstEntityConfiguration = + entityStatements.removeFirst().let { JsonMapper().mapEntityConfigurationStatement(it) } + val lastEntityConfiguration = + entityStatements.removeLast().let { JsonMapper().mapEntityConfigurationStatement(it) } + val subordinateStatements = entityStatements.map { JsonMapper().mapSubordinateStatement(it) } + + if (firstEntityConfiguration.iss != firstEntityConfiguration.sub) { + throw IllegalArgumentException("Entity Configuration of the Trust Chain subject requires that iss is equal to sub") + } + + if (firstEntityConfiguration.jwks.jsonObject["keys"]?.jsonArray?.any { + jwtService.verify( + input = JwtVerifyInput( + jwt = jwts[0], + key = retrieveJwk(it) + )) } == false) { + throw IllegalArgumentException("Invalid signature") + } + + subordinateStatements.forEachIndexed { index, current -> + val next = + if (index < subordinateStatements.size - 1) subordinateStatements[index + 1] else lastEntityConfiguration + val now = Clock.System.now().epochSeconds.toInt() + + if (current.iat > now) { + throw IllegalArgumentException("Invalid iat") + } + + if (current.exp < now) { + throw IllegalArgumentException("Invalid exp") + } + + when (next) { + is EntityConfigurationStatement -> + 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( + input = JwtVerifyInput( + jwt = jwts[index], + key = retrieveJwk(it) + )) } == 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( + input = JwtVerifyInput( + jwt = jwts[index], + key = retrieveJwk(it) + )) } == false) { + throw IllegalArgumentException("Invalid signature") + } + } + } + + if (!knownTrustChainIds.contains(lastEntityConfiguration.iss)) { + 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( + input = JwtVerifyInput( + jwt = jwts[jwts.size - 1], + key = retrieveJwk(it))) } == false) { + throw IllegalArgumentException("Invalid signature") + } + + val validTrustChain = mutableListOf() + validTrustChain.add(firstEntityConfiguration) + validTrustChain.addAll(subordinateStatements) + validTrustChain.add(lastEntityConfiguration) + + return validTrustChain + } + + private fun retrieveJwk(key: JsonElement): Jwk { return when (key) { is JsonObject -> Jwk( kid = key["kid"]?.jsonPrimitive?.content, 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 a890240c..6fc6649d 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,22 +1,16 @@ package com.sphereon.oid.fed.client.validation -import com.sphereon.oid.fed.common.jwt.verify -import com.sphereon.oid.fed.common.logging.Logger -import com.sphereon.oid.fed.common.mapper.JsonMapper +import com.sphereon.oid.fed.common.jwt.JwtService import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement -import com.sphereon.oid.fed.openapi.models.SubordinateStatement import io.ktor.client.engine.* import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.promise -import kotlinx.datetime.Clock -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonObject import kotlin.js.Promise @ExperimentalJsExport @JsExport -actual class TrustChainValidation { +class TrustChainValidation(val jwtService: JwtService) { private val NAME = "TrustChainValidation" @@ -26,7 +20,7 @@ actual class TrustChainValidation { trustChains: MutableList> = mutableListOf(), trustChain: MutableSet = mutableSetOf() ): Promise>> = CoroutineScope(context = CoroutineName(NAME)).promise { - TrustChainValidationCommon() + TrustChainValidationCommon(jwtService) .readAuthorityHints( partyBId = partyBId, engine = engine, @@ -39,90 +33,22 @@ actual class TrustChainValidation { entityConfigurationStatementsList: List>, engine: HttpClientEngine ): Promise>> = CoroutineScope(context = CoroutineName(NAME)).promise { - TrustChainValidationCommon() + TrustChainValidationCommon(jwtService) .fetchSubordinateStatements( entityConfigurationStatementsList = entityConfigurationStatementsList, engine = engine ) } - actual fun validateTrustChains( + fun validateTrustChains( jwts: List>, knownTrustChainIds: List - ): List> { - val trustChains: MutableList> = mutableListOf() - for(it in jwts) { - try { - trustChains.add(validateTrustChain(it, knownTrustChainIds)) - } catch (e: Exception) { - Logger.debug("TrustChainValidation", e.message.toString()) - } - } - return trustChains - } - - @OptIn(ExperimentalJsExport::class) - private fun validateTrustChain(jwts: List, knownTrustChainIds: List): List { - val entityStatements = jwts.toMutableList() - val firstEntityConfiguration = - entityStatements.removeFirst().let { JsonMapper().mapEntityConfigurationStatement(it) } - val lastEntityConfiguration = - entityStatements.removeLast().let { JsonMapper().mapEntityConfigurationStatement(it) } - val subordinateStatements = entityStatements.map { JsonMapper().mapSubordinateStatement(it) } - - if (firstEntityConfiguration.iss != firstEntityConfiguration.sub) { - throw IllegalArgumentException("Entity Configuration of the Trust Chain subject requires that iss is equal to sub") - } - - if (firstEntityConfiguration.jwks.jsonObject["keys"]?.jsonArray?.any { verify(jwts[0], - TrustChainValidationCommon().retrieveJwk(it)) } == false) { - throw IllegalArgumentException("Invalid signature") - } - - subordinateStatements.forEachIndexed { index, current -> - val next = - if (index < subordinateStatements.size - 1) subordinateStatements[index + 1] else lastEntityConfiguration - val now = (Clock.System.now().toEpochMilliseconds() / 1000).toInt() - - if (current.iat > now) { - throw IllegalArgumentException("Invalid iat") - } - - if (current.exp < now) { - throw IllegalArgumentException("Invalid exp") - } - - when (next) { - is EntityConfigurationStatement -> - 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 { verify(jwts[0], - TrustChainValidationCommon().retrieveJwk(it)) } == 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 { verify(jwts[0], - TrustChainValidationCommon().retrieveJwk(it)) } == false) { - throw IllegalArgumentException("Invalid signature") - } - } - } - - if (!knownTrustChainIds.contains(lastEntityConfiguration.iss)) { - 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 { verify(jwts[jwts.size - 1], - TrustChainValidationCommon().retrieveJwk(it)) } == false) { - throw IllegalArgumentException("Invalid signature") - } - - val validTrustChain = mutableListOf() - validTrustChain.add(firstEntityConfiguration) - validTrustChain.addAll(subordinateStatements) - validTrustChain.add(lastEntityConfiguration) - - return validTrustChain - } + ): Promise>> = + Promise.resolve( + TrustChainValidationCommon(jwtService) + .validateTrustChains( + jwts = jwts, + knownTrustChainIds = knownTrustChainIds + ) + ) } 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 becc62c6..7be704be 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,8 +1,8 @@ package com.sphereon.oid.fed.client.validation -import com.sphereon.oid.fed.common.jwk.convertToJwk -import com.sphereon.oid.fed.common.jwt.Jose -import com.sphereon.oid.fed.common.jwt.sign +import com.sphereon.oid.fed.common.jwt.JwtService +import com.sphereon.oid.fed.common.jwt.JwtSignInput +import com.sphereon.oid.fed.common.jwt.JwtVerifyInput import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement import com.sphereon.oid.fed.openapi.models.JWTHeader import com.sphereon.oid.fed.openapi.models.Jwk @@ -18,6 +18,7 @@ import io.ktor.utils.io.ByteReadChannel import kotlinx.coroutines.await import kotlinx.coroutines.test.runTest import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonObject @@ -29,10 +30,74 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue -// There is an open bug for kotlin-coroutines-test: https://youtrack.jetbrains.com/issue/KT-71757/KJS-IR-AssertionError-Assertion-failed-irToJs.JsAstUtilsKt.checkOnNullabilityjsAstUtils.kt451 -// +@JsModule("jose") +@JsNonModule +external object Jose { + class SignJWT { + constructor(payload: dynamic) { + definedExternally + } + + fun setProtectedHeader(protectedHeader: dynamic): SignJWT { + definedExternally + } + + fun sign(key: Any?, signOptions: Any?): String { + definedExternally + } + } + + fun generateKeyPair(alg: String, options: dynamic = definedExternally): dynamic + fun jwtVerify(jwt: String, key: Any, options: dynamic = definedExternally): dynamic + fun exportJWK(key: dynamic): dynamic + fun importJWK(jwk: dynamic, alg: String, options: dynamic = definedExternally): dynamic +} + +fun convertToJwk(keyPair: dynamic): Jwk { + val privateJWK = Jose.exportJWK(keyPair.privateKey) + val publicJWK = Jose.exportJWK(keyPair.publicKey) + return Jwk( + crv = privateJWK.crv, + d = privateJWK.d, + kty = privateJWK.kty, + x = privateJWK.x, + y = privateJWK.y, + alg = publicJWK.alg, + kid = publicJWK.kid, + use = publicJWK.use, + x5c = publicJWK.x5c, + x5t = publicJWK.x5t, + x5tS256 = privateJWK.x5tS256, + x5u = publicJWK.x5u, + dp = privateJWK.dp, + dq = privateJWK.dq, + e = privateJWK.e, + n = privateJWK.n, + p = privateJWK.p, + q = privateJWK.q, + qi = privateJWK.qi + ) +} + +class JwtServiceImpl: JwtService { + override fun sign(input: JwtSignInput): String { + 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 { + val publicKey = Jose.importJWK(input.key, alg = input.key.alg ?: "RS256") + return Jose.jwtVerify(input.jwt, publicKey) + } + +} + + class TrustChainValidationTest { + val jwtServiceImpl = JwtServiceImpl() + // key pairs @OptIn(ExperimentalJsExport::class) val partyBKeyPair = Jose.generateKeyPair("PS256", JsonObject(mapOf("extractable" to JsonPrimitive(true)))) @@ -92,7 +157,7 @@ class TrustChainValidationTest { @OptIn(ExperimentalJsExport::class) @BeforeTest - fun setup(): Unit { + fun setup() { // Party B Entity Configuration (federation) partyBConfiguration = entityConfiguration( @@ -106,14 +171,16 @@ class TrustChainValidationTest { federationFetchEndpoint = "https://edugain.org/federation/federation_fetch_endpoint" ) - partyBJwt = sign( - payload = Json.encodeToJsonElement(serializer = EntityConfigurationStatement.serializer(), partyBConfiguration).jsonObject, - header = JWTHeader( - alg = "ES256", - typ = "entity-statement+jwt", - kid = partyBJwk.kid - ), - key = partyBJwk + partyBJwt = jwtServiceImpl.sign( + JwtSignInput( + payload = Json.encodeToJsonElement(serializer = EntityConfigurationStatement.serializer(), partyBConfiguration).jsonObject, + header = JWTHeader( + alg = "PS256", + typ = "entity-statement+jwt", + kid = partyBJwk.kid + ), + key = partyBKeyPair.privateKey + ) ) // Federation 2 @@ -128,7 +195,8 @@ class TrustChainValidationTest { federationFetchEndpoint = "https://edugain.org/federation_two/federation_fetch_endpoint" ) - intermediateEntityConfigurationJwt = sign( + intermediateEntityConfigurationJwt = jwtServiceImpl.sign( + JwtSignInput( payload = Json.encodeToJsonElement(serializer = EntityConfigurationStatement.serializer(), intermediateEntityConfiguration).jsonObject, header = JWTHeader( alg = "ES256", @@ -137,6 +205,7 @@ class TrustChainValidationTest { ), key = intermediateEntityConfiguration1Jwk ) + ) //signed with intermediateEntity1 Private Key intermediateEntitySubordinateStatement = intermediateEntity( @@ -145,7 +214,8 @@ class TrustChainValidationTest { sub = "https://openid.sunet.se", ) - intermediateEntitySubordinateStatementJwt = sign( + intermediateEntitySubordinateStatementJwt = jwtServiceImpl.sign( + JwtSignInput( payload = Json.encodeToJsonElement(serializer = SubordinateStatement.serializer(), intermediateEntitySubordinateStatement).jsonObject, header = JWTHeader( alg = "ES256", @@ -154,6 +224,7 @@ class TrustChainValidationTest { ), key = intermediateEntityConfiguration1Jwk ) + ) // Federation 4 intermediateEntityConfiguration1 = entityConfiguration( @@ -164,7 +235,8 @@ class TrustChainValidationTest { federationFetchEndpoint = "https://edugain.org/federation_four/federation_fetch_endpoint" ) - intermediateEntityConfiguration1Jwt = sign( + intermediateEntityConfiguration1Jwt = jwtServiceImpl.sign( + JwtSignInput( payload = Json.encodeToJsonElement(serializer = EntityConfigurationStatement.serializer(), intermediateEntityConfiguration1).jsonObject, header = JWTHeader( alg = "ES256", @@ -173,6 +245,7 @@ class TrustChainValidationTest { ), key = validTrustAnchorConfigurationJwk ) + ) intermediateEntity1SubordinateStatement = intermediateEntity( publicKey = intermediateEntityConfiguration1Jwk, @@ -180,7 +253,8 @@ class TrustChainValidationTest { sub = "https://openid.sunet-one.se" ) - intermediateEntity1SubordinateStatementJwt = sign( + intermediateEntity1SubordinateStatementJwt = jwtServiceImpl.sign( + JwtSignInput( payload = Json.encodeToJsonElement(serializer = SubordinateStatement.serializer(), intermediateEntity1SubordinateStatement).jsonObject, header = JWTHeader( alg = "ES256", @@ -189,6 +263,7 @@ class TrustChainValidationTest { ), key = validTrustAnchorConfigurationJwk ) + ) // Federation 5 validTrustAnchorConfiguration = entityConfiguration( @@ -199,7 +274,8 @@ class TrustChainValidationTest { federationFetchEndpoint = "https://edugain.org/federation_five/federation_fetch_endpoint" ) - validTrustAnchorConfigurationJwt = sign( + validTrustAnchorConfigurationJwt = jwtServiceImpl.sign( + JwtSignInput( payload = Json.encodeToJsonElement(serializer = EntityConfigurationStatement.serializer(), validTrustAnchorConfiguration).jsonObject, header = JWTHeader( alg = "ES256", @@ -208,6 +284,7 @@ class TrustChainValidationTest { ), key = validTrustAnchorConfigurationJwk ) + ) // Federation 3 unknownTrustAnchorConfiguration = entityConfiguration( @@ -218,7 +295,8 @@ class TrustChainValidationTest { federationFetchEndpoint = "https://edugain.org/federation_three/federation_fetch_endpoint" ) - unknownTrustAnchorConfigurationJwt = sign( + unknownTrustAnchorConfigurationJwt = jwtServiceImpl.sign( + JwtSignInput( payload = Json.encodeToJsonElement(serializer = EntityConfigurationStatement.serializer(), unknownTrustAnchorConfiguration).jsonObject, header = JWTHeader( alg = "ES256", @@ -227,6 +305,7 @@ class TrustChainValidationTest { ), key = unknownTrustAnchorConfigurationJwk ) + ) // Federation 1 invalidTrustAnchorConfiguration = entityConfiguration( @@ -237,7 +316,8 @@ class TrustChainValidationTest { federationFetchEndpoint = "https://edugain.org/federation_one/federation_fetch_endpoint" ) - invalidTrustAnchorConfigurationJwt = sign( + invalidTrustAnchorConfigurationJwt = jwtServiceImpl.sign( + JwtSignInput( payload = Json.encodeToJsonElement(serializer = EntityConfigurationStatement.serializer(), invalidTrustAnchorConfiguration).jsonObject, header = JWTHeader( alg = "ES256", @@ -246,6 +326,7 @@ class TrustChainValidationTest { ), key = invalidTrustAnchorConfigurationJwk ) + ) listOfEntityConfigurationStatementList = mutableListOf( mutableListOf( @@ -360,7 +441,7 @@ class TrustChainValidationTest { fun readAuthorityHintsTest() = runTest { assertEquals( listOfEntityConfigurationStatementList, - TrustChainValidation().readAuthorityHints( + TrustChainValidation(jwtServiceImpl).readAuthorityHints( partyBId = "https://edugain.org/federation", engine = mockEngine ).await() @@ -371,7 +452,7 @@ class TrustChainValidationTest { fun fetchSubordinateStatementsTest() = runTest { assertEquals( listOfSubordinateStatementList, - TrustChainValidation().fetchSubordinateStatements( + TrustChainValidation(jwtServiceImpl).fetchSubordinateStatements( entityConfigurationStatementsList = listOfEntityConfigurationStatementList, engine = mockEngine ).await() @@ -379,9 +460,9 @@ class TrustChainValidationTest { } @Test - fun validateTrustChainTest() { + fun validateTrustChainTest() = runTest { assertTrue( - TrustChainValidation().validateTrustChains(listOfSubordinateStatementList, listOf("https://openid.sunet-invalid.se", "https://openid.sunet-five.se")).size == 1 + TrustChainValidation(jwtServiceImpl).validateTrustChains(listOfSubordinateStatementList, listOf("https://openid.sunet-invalid.se", "https://openid.sunet-five.se")).await().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 deleted file mode 100644 index a4144227..00000000 --- a/modules/openid-federation-client/src/jvmMain/kotlin/com/sphereon/oid/fed/client/validation/TrustChainValidation.jvm.kt +++ /dev/null @@ -1,92 +0,0 @@ -package com.sphereon.oid.fed.client.validation - -import com.sphereon.oid.fed.common.jwt.verify -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.SubordinateStatement -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonObject -import java.time.OffsetDateTime - -actual class TrustChainValidation { - - actual fun validateTrustChains( - jwts: List>, - knownTrustChainIds: List - ): List> { - val trustChains: MutableList> = mutableListOf() - for(it in jwts) { - try { - trustChains.add(validateTrustChain(it, knownTrustChainIds)) - } catch (e: Exception) { - Logger.debug("TrustChainValidation", e.message.toString()) - } - } - return trustChains - } - - private fun validateTrustChain(jwts: List, knownTrustChainIds: List): List { - val entityStatements = jwts.toMutableList() - val firstEntityConfiguration = - entityStatements.removeFirst().let { JsonMapper().mapEntityConfigurationStatement(it) } - val lastEntityConfiguration = - entityStatements.removeLast().let { JsonMapper().mapEntityConfigurationStatement(it) } - val subordinateStatements = entityStatements.map { JsonMapper().mapSubordinateStatement(it) } - - if (firstEntityConfiguration.iss != firstEntityConfiguration.sub) { - throw IllegalArgumentException("Entity Configuration of the Trust Chain subject requires that iss is equal to sub") - } - - if (firstEntityConfiguration.jwks.jsonObject["keys"]?.jsonArray?.any { verify(jwts[0], - TrustChainValidationCommon().retrieveJwk(it)) } == false) { - throw IllegalArgumentException("Invalid signature") - } - - subordinateStatements.forEachIndexed { index, current -> - val next = - if (index < subordinateStatements.size - 1) subordinateStatements[index + 1] else lastEntityConfiguration - val now = OffsetDateTime.now().toEpochSecond().toInt() - - if (current.iat > now) { - throw IllegalArgumentException("Invalid iat") - } - - if (current.exp < now) { - throw IllegalArgumentException("Invalid exp") - } - - when (next) { - is EntityConfigurationStatement -> - 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 { verify(jwts[0], - TrustChainValidationCommon().retrieveJwk(it)) } == 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 { verify(jwts[0], - TrustChainValidationCommon().retrieveJwk(it)) } == false) { - throw IllegalArgumentException("Invalid signature") - } - } - } - - if (!knownTrustChainIds.contains(lastEntityConfiguration.iss)) { - 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 { verify(jwts[jwts.size - 1], - TrustChainValidationCommon().retrieveJwk(it)) } == false) { - throw IllegalArgumentException("Invalid signature") - } - - val validTrustChain = mutableListOf() - validTrustChain.add(firstEntityConfiguration) - validTrustChain.addAll(subordinateStatements) - validTrustChain.add(lastEntityConfiguration) - - return validTrustChain - } -} 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 e5167075..74b23329 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 @@ -1,15 +1,27 @@ package com.sphereon.oid.fed.client.validation +import com.nimbusds.jose.JOSEObjectType +import com.nimbusds.jose.JWSAlgorithm +import com.nimbusds.jose.JWSHeader +import com.nimbusds.jose.JWSSigner +import com.nimbusds.jose.JWSVerifier +import com.nimbusds.jose.crypto.ECDSASigner +import com.nimbusds.jose.crypto.ECDSAVerifier import com.nimbusds.jose.jwk.Curve import com.nimbusds.jose.jwk.ECKey import com.nimbusds.jose.jwk.gen.ECKeyGenerator -import com.sphereon.oid.fed.common.jwt.sign +import com.nimbusds.jwt.JWTClaimsSet +import com.nimbusds.jwt.SignedJWT +import com.sphereon.oid.fed.common.jwt.JwtService +import com.sphereon.oid.fed.common.jwt.JwtSignInput +import com.sphereon.oid.fed.common.jwt.JwtVerifyInput import com.sphereon.oid.fed.openapi.models.* 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.test.runTest +import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonObject @@ -20,10 +32,49 @@ import java.time.OffsetDateTime import kotlin.test.Test import kotlin.test.assertEquals +class JwtServiceImpl : JwtService { + override 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()) + ) + + signedJWT.sign(signer) + return signedJWT.serialize() + } + + override fun verify(input: JwtVerifyInput): Boolean { + try { + val jwkJsonString = Json.encodeToString(input.key) + val ecKey = ECKey.parse(jwkJsonString) + val verifier: JWSVerifier = ECDSAVerifier(ecKey) + val signedJWT = SignedJWT.parse(input.jwt) + val verified = signedJWT.verify(verifier) + return verified + } catch (e: Exception) { + throw Exception("Couldn't verify the JWT Signature: ${e.message}", e) + } + } + + private fun JWTHeader.toJWSHeader(): JWSHeader { + val type = typ + return JWSHeader.Builder(JWSAlgorithm.parse(alg)).apply { + type(JOSEObjectType(type)) + keyID(kid) + }.build() + } +} + class TrustChainValidationTest { companion object { + val jwtService = JwtServiceImpl() + // key pairs val partyBKeyPair = ECKeyGenerator(Curve.P_256).generate() val intermediateEntityKeyPair = ECKeyGenerator(Curve.P_256).generate() @@ -131,7 +182,7 @@ class TrustChainValidationTest { @JvmStatic @BeforeClass - fun setup(): Unit { + fun setup() { // Party B Entity Configuration (federation) partyBConfiguration = entityConfiguration( @@ -145,14 +196,19 @@ class TrustChainValidationTest { federationFetchEndpoint = "https://edugain.org/federation/federation_fetch_endpoint" ) - partyBJwt = sign( - payload = Json.encodeToJsonElement(serializer = EntityConfigurationStatement.serializer(), partyBConfiguration).jsonObject, - header = JWTHeader( - alg = "ES256", - typ = "entity-statement+jwt", - kid = partyBKeyPair.keyID - ), - key = partyBJwk + partyBJwt = jwtService.sign( + JwtSignInput( + payload = Json.encodeToJsonElement( + serializer = EntityConfigurationStatement.serializer(), + partyBConfiguration + ).jsonObject, + header = JWTHeader( + alg = "ES256", + typ = "entity-statement+jwt", + kid = partyBKeyPair.keyID + ), + key = partyBJwk + ) ) // Federation 2 @@ -167,14 +223,19 @@ class TrustChainValidationTest { federationFetchEndpoint = "https://edugain.org/federation_two/federation_fetch_endpoint" ) - intermediateEntityConfigurationJwt = sign( - payload = Json.encodeToJsonElement(serializer = EntityConfigurationStatement.serializer(), intermediateEntityConfiguration).jsonObject, - header = JWTHeader( - alg = "ES256", - typ = "entity-statement+jwt", - kid = intermediateEntityKeyPair.keyID - ), - key = intermediateEntityConfiguration1Jwk + intermediateEntityConfigurationJwt = jwtService.sign( + JwtSignInput( + payload = Json.encodeToJsonElement( + serializer = EntityConfigurationStatement.serializer(), + intermediateEntityConfiguration + ).jsonObject, + header = JWTHeader( + alg = "ES256", + typ = "entity-statement+jwt", + kid = intermediateEntityKeyPair.keyID + ), + key = intermediateEntityConfiguration1Jwk + ) ) //signed with intermediateEntity1 Private Key @@ -184,14 +245,19 @@ class TrustChainValidationTest { sub = "https://openid.sunet.se", ) - intermediateEntitySubordinateStatementJwt = sign( - payload = Json.encodeToJsonElement(serializer = SubordinateStatement.serializer(), intermediateEntitySubordinateStatement).jsonObject, - header = JWTHeader( - alg = "ES256", - typ = "entity-statement+jwt", - kid = intermediateEntityKeyPair.keyID - ), - key = intermediateEntityConfiguration1Jwk + intermediateEntitySubordinateStatementJwt = jwtService.sign( + JwtSignInput( + payload = Json.encodeToJsonElement( + serializer = SubordinateStatement.serializer(), + intermediateEntitySubordinateStatement + ).jsonObject, + header = JWTHeader( + alg = "ES256", + typ = "entity-statement+jwt", + kid = intermediateEntityKeyPair.keyID + ), + key = intermediateEntityConfiguration1Jwk + ) ) // Federation 4 @@ -203,14 +269,19 @@ class TrustChainValidationTest { federationFetchEndpoint = "https://edugain.org/federation_four/federation_fetch_endpoint" ) - intermediateEntityConfiguration1Jwt = sign( - payload = Json.encodeToJsonElement(serializer = EntityConfigurationStatement.serializer(), intermediateEntityConfiguration1).jsonObject, - header = JWTHeader( - alg = "ES256", - typ = "entity-statement+jwt", - kid = intermediateEntity1KeyPair.keyID - ), - key = validTrustAnchorConfigurationJwk + intermediateEntityConfiguration1Jwt = jwtService.sign( + JwtSignInput( + payload = Json.encodeToJsonElement( + serializer = EntityConfigurationStatement.serializer(), + intermediateEntityConfiguration1 + ).jsonObject, + header = JWTHeader( + alg = "ES256", + typ = "entity-statement+jwt", + kid = intermediateEntity1KeyPair.keyID + ), + key = validTrustAnchorConfigurationJwk + ) ) intermediateEntity1SubordinateStatement = intermediateEntity( @@ -219,14 +290,19 @@ class TrustChainValidationTest { sub = "https://openid.sunet-one.se" ) - intermediateEntity1SubordinateStatementJwt = sign( - payload = Json.encodeToJsonElement(serializer = SubordinateStatement.serializer(), intermediateEntity1SubordinateStatement).jsonObject, - header = JWTHeader( - alg = "ES256", - typ = "entity-statement+jwt", - kid = intermediateEntity1KeyPair.keyID - ), - key = validTrustAnchorConfigurationJwk + intermediateEntity1SubordinateStatementJwt = jwtService.sign( + JwtSignInput( + payload = Json.encodeToJsonElement( + serializer = SubordinateStatement.serializer(), + intermediateEntity1SubordinateStatement + ).jsonObject, + header = JWTHeader( + alg = "ES256", + typ = "entity-statement+jwt", + kid = intermediateEntity1KeyPair.keyID + ), + key = validTrustAnchorConfigurationJwk + ) ) // Federation 5 @@ -238,14 +314,19 @@ class TrustChainValidationTest { federationFetchEndpoint = "https://edugain.org/federation_five/federation_fetch_endpoint" ) - validTrustAnchorConfigurationJwt = sign( - payload = Json.encodeToJsonElement(serializer = EntityConfigurationStatement.serializer(), validTrustAnchorConfiguration).jsonObject, - header = JWTHeader( - alg = "ES256", - typ = "entity-statement+jwt", - kid = validTrustAnchorKeyPair.keyID - ), - key = validTrustAnchorConfigurationJwk + validTrustAnchorConfigurationJwt = jwtService.sign( + JwtSignInput( + payload = Json.encodeToJsonElement( + serializer = EntityConfigurationStatement.serializer(), + validTrustAnchorConfiguration + ).jsonObject, + header = JWTHeader( + alg = "ES256", + typ = "entity-statement+jwt", + kid = validTrustAnchorKeyPair.keyID + ), + key = validTrustAnchorConfigurationJwk + ) ) // Federation 3 @@ -257,14 +338,19 @@ class TrustChainValidationTest { federationFetchEndpoint = "https://edugain.org/federation_three/federation_fetch_endpoint" ) - unknownTrustAnchorConfigurationJwt = sign( - payload = Json.encodeToJsonElement(serializer = EntityConfigurationStatement.serializer(), unknownTrustAnchorConfiguration).jsonObject, - header = JWTHeader( - alg = "ES256", - typ = "entity-statement+jwt", - kid = unknownTrustAnchorKeyPair.keyID - ), - key = unknownTrustAnchorConfigurationJwk + unknownTrustAnchorConfigurationJwt = jwtService.sign( + JwtSignInput( + payload = Json.encodeToJsonElement( + serializer = EntityConfigurationStatement.serializer(), + unknownTrustAnchorConfiguration + ).jsonObject, + header = JWTHeader( + alg = "ES256", + typ = "entity-statement+jwt", + kid = unknownTrustAnchorKeyPair.keyID + ), + key = unknownTrustAnchorConfigurationJwk + ) ) // Federation 1 @@ -276,14 +362,19 @@ class TrustChainValidationTest { federationFetchEndpoint = "https://edugain.org/federation_one/federation_fetch_endpoint" ) - invalidTrustAnchorConfigurationJwt = sign( - payload = Json.encodeToJsonElement(serializer = EntityConfigurationStatement.serializer(), invalidTrustAnchorConfiguration).jsonObject, - header = JWTHeader( - alg = "ES256", - typ = "entity-statement+jwt", - kid = invalidTrustAnchorKeyPair.keyID - ), - key = invalidTrustAnchorConfigurationJwk + invalidTrustAnchorConfigurationJwt = jwtService.sign( + JwtSignInput( + payload = Json.encodeToJsonElement( + serializer = EntityConfigurationStatement.serializer(), + invalidTrustAnchorConfiguration + ).jsonObject, + header = JWTHeader( + alg = "ES256", + typ = "entity-statement+jwt", + kid = invalidTrustAnchorKeyPair.keyID + ), + key = invalidTrustAnchorConfigurationJwk + ) ) listOfEntityConfigurationStatementList = mutableListOf( @@ -294,7 +385,10 @@ class TrustChainValidationTest { partyBConfiguration, intermediateEntityConfiguration, unknownTrustAnchorConfiguration ), mutableListOf( - partyBConfiguration, intermediateEntityConfiguration, intermediateEntityConfiguration1, validTrustAnchorConfiguration + partyBConfiguration, + intermediateEntityConfiguration, + intermediateEntityConfiguration1, + validTrustAnchorConfiguration ) ) @@ -306,7 +400,10 @@ class TrustChainValidationTest { partyBJwt, intermediateEntitySubordinateStatementJwt, unknownTrustAnchorConfigurationJwt ), mutableListOf( - partyBJwt, intermediateEntitySubordinateStatementJwt, intermediateEntity1SubordinateStatementJwt, validTrustAnchorConfigurationJwt + partyBJwt, + intermediateEntitySubordinateStatementJwt, + intermediateEntity1SubordinateStatementJwt, + validTrustAnchorConfigurationJwt ) ) } @@ -394,13 +491,13 @@ class TrustChainValidationTest { else -> error("Unhandled ${request.url}") } -} + } @Test fun readAuthorityHintsTest() = runTest { assertEquals( listOfEntityConfigurationStatementList.toString(), - TrustChainValidationCommon().readAuthorityHints( + TrustChainValidationCommon(jwtService).readAuthorityHints( partyBId = "https://edugain.org/federation", engine = mockEngine ).toString() @@ -411,17 +508,20 @@ class TrustChainValidationTest { fun fetchSubordinateStatementsTest() = runTest { assertEquals( listOfSubordinateStatementList, - TrustChainValidationCommon().fetchSubordinateStatements( - entityConfigurationStatementsList = listOfEntityConfigurationStatementList, - engine = mockEngine + TrustChainValidationCommon(jwtService).fetchSubordinateStatements( + entityConfigurationStatementsList = listOfEntityConfigurationStatementList, + engine = mockEngine ) ) } @Test - fun validateTrustChainTest() { + fun validateTrustChainTest() = runTest { assertTrue( - TrustChainValidation().validateTrustChains(listOfSubordinateStatementList, listOf("https://openid.sunet-invalid.se", "https://openid.sunet-five.se")).size == 1 + TrustChainValidationCommon(jwtService).validateTrustChains( + listOfSubordinateStatementList, + listOf("https://openid.sunet-invalid.se", "https://openid.sunet-five.se") + ).size == 1 ) } } diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts index 09aab985..90a3764e 100644 --- a/modules/openid-federation-common/build.gradle.kts +++ b/modules/openid-federation-common/build.gradle.kts @@ -131,7 +131,6 @@ kotlin { runtimeOnly("io.ktor:ktor-client-core-js:$ktorVersion") runtimeOnly("io.ktor:ktor-client-js:$ktorVersion") implementation(npm("typescript", "5.5.3")) - implementation(npm("jose", "5.6.3")) implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0-RC") } @@ -139,6 +138,7 @@ kotlin { val jsTest by getting { dependencies { + implementation(npm("jose", "5.6.3")) implementation(kotlin("test-js")) implementation(kotlin("test-annotations-common")) } diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.kt deleted file mode 100644 index 70553655..00000000 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.sphereon.oid.fed.common.jwk - -import com.sphereon.oid.fed.openapi.models.Jwk - -expect fun generateKeyPair(): Jwk diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt index e4035314..05cad7f9 100644 --- a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.kt @@ -3,6 +3,27 @@ package com.sphereon.oid.fed.common.jwt import com.sphereon.oid.fed.openapi.models.JWTHeader import com.sphereon.oid.fed.openapi.models.Jwk import kotlinx.serialization.json.JsonObject +import kotlin.js.ExperimentalJsExport +import kotlin.js.JsExport -expect fun sign(payload: JsonObject, header: JWTHeader, key: Jwk): String -expect fun verify(jwt: String, key: Jwk): Boolean +@ExperimentalJsExport +@JsExport +data class JwtSignInput ( + val payload: JsonObject, + val header: JWTHeader, + val key: Jwk +) + +@ExperimentalJsExport +@JsExport +data class JwtVerifyInput ( + val jwt: String, + val key: Jwk +) + +@ExperimentalJsExport +@JsExport +interface JwtService { + fun sign(input: JwtSignInput): String + fun verify(input: JwtVerifyInput): Boolean +} diff --git a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.kt b/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.kt deleted file mode 100644 index 4819fc90..00000000 --- a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.sphereon.oid.fed.common.jwk - -import com.sphereon.oid.fed.common.jwt.Jose -import com.sphereon.oid.fed.openapi.models.Jwk - -@ExperimentalJsExport -@JsExport -actual fun generateKeyPair(): Jwk { - val key = Jose.generateKeyPair("EC") - return Jwk( - d = key.d, - alg = key.alg, - crv = key.crv, - x = key.x, - y = key.y, - kid = key.kid, - kty = key.kty, - use = key.use, - ) -} - -@ExperimentalJsExport -@JsExport -fun convertToJwk(keyPair: dynamic): Jwk { - val privateJWK = Jose.exportJWK(keyPair.privateKey) - val publicJWK = Jose.exportJWK(keyPair.publicKey) - return Jwk( - crv = privateJWK.crv, - d = privateJWK.d, - kty = privateJWK.kty, - x = privateJWK.x, - y = privateJWK.y, - alg = publicJWK.alg, - kid = publicJWK.kid, - use = publicJWK.use, - x5c = publicJWK.x5c, - x5t = publicJWK.x5t, - x5tS256 = privateJWK.x5tS256, - x5u = publicJWK.x5u, - dp = privateJWK.dp, - dq = privateJWK.dq, - e = privateJWK.e, - n = privateJWK.n, - p = privateJWK.p, - q = privateJWK.q, - qi = privateJWK.qi - ) -} diff --git a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt b/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt deleted file mode 100644 index 1d1b77a8..00000000 --- a/modules/openid-federation-common/src/jsMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.js.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.sphereon.oid.fed.common.jwt - -import com.sphereon.oid.fed.openapi.models.JWTHeader -import com.sphereon.oid.fed.openapi.models.Jwk -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject - -@JsModule("jose") -@JsNonModule -external object Jose { - class SignJWT { - constructor(payload: dynamic) { - definedExternally - } - - fun setProtectedHeader(protectedHeader: dynamic): SignJWT { - definedExternally - } - - fun sign(key: Any?, signOptions: Any?): String { - definedExternally - } - } - - fun generateKeyPair(alg: String, options: dynamic = definedExternally): dynamic - fun jwtVerify(jwt: String, key: Any, options: dynamic = definedExternally): dynamic - fun importJWK(key: Any, alg: String): dynamic - fun exportJWK(key: Any): dynamic -} - -@ExperimentalJsExport -@JsExport -actual fun sign( - payload: JsonObject, header: JWTHeader, key: Jwk -): String { - return Jose.SignJWT(JSON.parse(Json.encodeToString(payload))) - .setProtectedHeader(JSON.parse(Json.encodeToString(header))) - .sign(key = key, null) -} - -@ExperimentalJsExport -@JsExport -actual fun verify( - jwt: String, - key: Jwk -): Boolean { - val publicKey = Jose.importJWK(key, alg = key.alg ?: "RS256") - return Jose.jwtVerify(jwt, publicKey) -} diff --git a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.jvm.kt b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.jvm.kt deleted file mode 100644 index e5f97dc8..00000000 --- a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwk/Jwk.jvm.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.sphereon.oid.fed.common.jwk - -import com.nimbusds.jose.Algorithm -import com.nimbusds.jose.jwk.Curve -import com.nimbusds.jose.jwk.ECKey -import com.nimbusds.jose.jwk.gen.ECKeyGenerator -import com.sphereon.oid.fed.openapi.models.Jwk -import java.util.* - - -actual fun generateKeyPair(): Jwk { - try { - val ecKey: ECKey = ECKeyGenerator(Curve.P_256) - .keyIDFromThumbprint(true) - .algorithm(Algorithm("ES256")) - .issueTime(Date()) - .generate() - - return Jwk( - d = ecKey.d.toString(), - alg = ecKey.algorithm.name, - crv = ecKey.curve.name, - kid = ecKey.keyID, - kty = ecKey.keyType.value, - use = ecKey.keyUse?.value ?: "sig", - x = ecKey.x.toString(), - y = ecKey.y.toString() - ) - - } catch (e: Exception) { - throw Exception("Couldn't generate the EC Key Pair: ${e.message}", e) - } -} diff --git a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt b/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt deleted file mode 100644 index 15eb4c01..00000000 --- a/modules/openid-federation-common/src/jvmMain/kotlin/com/sphereon/oid/fed/common/jwt/JoseJwt.jvm.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.sphereon.oid.fed.common.jwt - -import com.nimbusds.jose.* -import com.nimbusds.jose.crypto.ECDSASigner -import com.nimbusds.jose.crypto.ECDSAVerifier -import com.nimbusds.jose.jwk.ECKey -import com.nimbusds.jwt.JWTClaimsSet -import com.nimbusds.jwt.SignedJWT -import com.sphereon.oid.fed.openapi.models.JWTHeader -import com.sphereon.oid.fed.openapi.models.Jwk -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject - -actual fun sign( - payload: JsonObject, header: JWTHeader, key: Jwk -): String { - val jwkJsonString = Json.encodeToString(key) - val ecJWK = ECKey.parse(jwkJsonString) - val signer: JWSSigner = ECDSASigner(ecJWK) - val jwsHeader = header.toJWSHeader() - - val signedJWT = SignedJWT( - jwsHeader, JWTClaimsSet.parse(payload.toString()) - ) - - signedJWT.sign(signer) - return signedJWT.serialize() -} - -actual fun verify( - jwt: String, key: Jwk -): Boolean { - try { - val jwkJsonString = Json.encodeToString(key) - val ecKey = ECKey.parse(jwkJsonString) - val verifier: JWSVerifier = ECDSAVerifier(ecKey) - val signedJWT = SignedJWT.parse(jwt) - val verified = signedJWT.verify(verifier) - return verified - } catch (e: Exception) { - throw Exception("Couldn't verify the JWT Signature: ${e.message}", e) - } -} - -fun JWTHeader.toJWSHeader(): JWSHeader { - val type = typ - return JWSHeader.Builder(JWSAlgorithm.parse(alg)).apply { - type(JOSEObjectType(type)) - keyID(kid) - }.build() -}