Skip to content

Commit

Permalink
fix: jwk data model
Browse files Browse the repository at this point in the history
  • Loading branch information
jcmelati committed Feb 11, 2025
1 parent 1b75776 commit 0272256
Show file tree
Hide file tree
Showing 21 changed files with 139 additions and 109 deletions.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ fun getNpmVersion(): String {

allprojects {
group = "com.sphereon.oid.fed"
version = "0.4.15-SNAPSHOT"
version = "0.4.16-SNAPSHOT"
val npmVersion by extra { getNpmVersion() }

// Common repository configuration for all projects
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.sphereon.oid.fed.server.admin.controllers

import com.sphereon.oid.fed.common.Constants
import com.sphereon.oid.fed.openapi.models.Account
import com.sphereon.oid.fed.openapi.models.BaseJwk
import com.sphereon.oid.fed.openapi.models.Jwk
import com.sphereon.oid.fed.services.JwkService
import jakarta.servlet.http.HttpServletRequest
Expand All @@ -21,7 +22,7 @@ class KeyController(
}

@GetMapping
fun getKeys(request: HttpServletRequest): Array<Jwk> {
fun getKeys(request: HttpServletRequest): Array<BaseJwk> {
val account = request.getAttribute(Constants.ACCOUNT_ATTRIBUTE) as Account
return jwkService.getKeys(account)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1752,7 +1752,7 @@ components:
keys:
type: array
items:
$ref: '#/components/schemas/Jwk'
$ref: '#/components/schemas/BaseJwk'
metadata:
additionalProperties: true
crit:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
package com.sphereon.oid.fed.client.helpers

import com.sphereon.oid.fed.openapi.models.Jwk
import com.sphereon.oid.fed.openapi.models.BaseJwk
import kotlinx.datetime.Clock
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive

fun getEntityConfigurationEndpoint(iss: String): String {
return "${if (iss.endsWith("/")) iss.dropLast(1) else iss}/.well-known/openid-federation"
Expand All @@ -15,15 +12,13 @@ fun getSubordinateStatementEndpoint(fetchEndpoint: String, sub: String): String
return "${fetchEndpoint}?sub=$sub"
}

fun findKeyInJwks(keys: JsonArray, kid: String, json: Json): Jwk? {
val key = keys.firstOrNull { it.jsonObject["kid"]?.jsonPrimitive?.content?.trim() == kid.trim() }
fun findKeyInJwks(keys: Array<BaseJwk>, kid: String, json: Json): BaseJwk? {
val key = keys.firstOrNull { it.kid?.trim() == kid.trim() }

if (key == null) return null

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

fun checkKidInJwks(keys: Array<Jwk>, kid: String): Boolean {
fun checkKidInJwks(keys: Array<BaseJwk>, kid: String): Boolean {
for (key in keys) {
if (key.kid == kid) {
return true
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
package com.sphereon.oid.fed.client.services.jwtService

import com.sphereon.oid.fed.client.context.FederationContext
import com.sphereon.oid.fed.client.helpers.findKeyInJwks
import com.sphereon.oid.fed.client.mapper.decodeJWTComponents
import com.sphereon.oid.fed.openapi.models.Jwk
import com.sphereon.oid.fed.openapi.models.BaseJwk
import kotlinx.serialization.builtins.ArraySerializer
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject

class JwtService(private val context: FederationContext) {
/**
* Fetches and verifies a JWT from a given endpoint
*/
suspend fun fetchAndVerifyJwt(endpoint: String, verifyWithKey: Jwk? = null): String {
suspend fun fetchAndVerifyJwt(endpoint: String, verifyWithKey: BaseJwk? = null): String {
context.logger.debug("Fetching JWT from endpoint: $endpoint")
val jwt = context.httpResolver.get(endpoint)

Expand All @@ -24,31 +21,25 @@ class JwtService(private val context: FederationContext) {
return jwt
}

/**
* Verifies a JWT signature with a given key
*/
suspend fun verifyJwt(jwt: String, key: Jwk) {
suspend fun verifyJwt(jwt: String, key: BaseJwk) {
context.logger.debug("Verifying JWT signature with key: ${key.kid}")
if (!context.cryptoService.verify(jwt, key)) {
throw IllegalStateException("JWT signature verification failed")
}
context.logger.debug("JWT signature verified successfully")
}

/**
* Verifies a JWT is self-signed using its own JWKS
*/
suspend fun verifySelfSignedJwt(jwt: String): Jwk {
suspend fun verifySelfSignedJwt(jwt: String) {
val decodedJwt = decodeJWTComponents(jwt)
context.logger.debug("Verifying self-signed JWT with kid: ${decodedJwt.header.kid}")

val jwks = decodedJwt.payload["jwks"]?.jsonObject?.get("keys")?.jsonArray
?: throw IllegalStateException("No JWKS found in JWT payload")
val jwks = decodedJwt.payload["jwks"]?.jsonObject?.get("keys")?.jsonArray?.let { array ->
context.json.decodeFromJsonElement(ArraySerializer(BaseJwk.serializer()), array)
} ?: throw IllegalStateException("No JWKS found in JWT payload")

val key = findKeyInJwks(jwks, decodedJwt.header.kid, context.json)
val key = jwks.find { it.kid == decodedJwt.header.kid }
?: throw IllegalStateException("No matching key found for kid: ${decodedJwt.header.kid}")

verifyJwt(jwt, key)
return key
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import com.sphereon.oid.fed.client.mapper.mapEntityStatement
import com.sphereon.oid.fed.client.services.entityConfigurationStatementService.EntityConfigurationStatementService
import com.sphereon.oid.fed.client.types.TrustChainResolveResponse
import com.sphereon.oid.fed.client.types.VerifyTrustChainResponse
import com.sphereon.oid.fed.openapi.models.BaseJwk
import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement
import com.sphereon.oid.fed.openapi.models.Jwt
import com.sphereon.oid.fed.openapi.models.SubordinateStatement
import kotlinx.serialization.builtins.ArraySerializer
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
Expand Down Expand Up @@ -241,15 +243,16 @@ class TrustChainService(
val entityConfigurationJwt = context.jwtService.fetchAndVerifyJwt(entityConfigurationEndpoint)
val decodedEntityConfiguration = decodeJWTComponents(entityConfigurationJwt)
logger.debug("Decoded entity configuration JWT header kid: ${decodedEntityConfiguration.header.kid}")

val key = findKeyInJwks(
decodedEntityConfiguration.payload["jwks"]?.jsonObject?.get("keys")?.jsonArray ?: run {
logger.debug("No JWKS found in entity configuration payload")
return null
},
decodedEntityConfiguration.header.kid,
context.json
) ?: run {
val key = decodedEntityConfiguration.payload["jwks"]?.jsonObject?.get("keys")?.jsonArray?.let { array ->
findKeyInJwks(
decodeJwksArray(array),
decodedEntityConfiguration.header.kid,
context.json
)
} ?: run {
logger.debug("No JWKS found in entity configuration payload")
return null
} ?: run {
logger.debug("Could not find key with kid: ${decodedEntityConfiguration.header.kid} in JWKS")
return null
}
Expand Down Expand Up @@ -374,11 +377,13 @@ class TrustChainService(
cache.put(authorityConfigurationEndpoint, authorityEntityConfigurationJwt)

val decodedJwt = decodeJWTComponents(authorityEntityConfigurationJwt)
val key = findKeyInJwks(
decodedJwt.payload["jwks"]?.jsonObject?.get("keys")?.jsonArray ?: return null,
decodedJwt.header.kid,
context.json
) ?: return null
val key = decodedJwt.payload["jwks"]?.jsonObject?.get("keys")?.jsonArray?.let { array ->
findKeyInJwks(
decodeJwksArray(array),
decodedJwt.header.kid,
context.json
)
} ?: return null

context.jwtService.verifyJwt(authorityEntityConfigurationJwt, key)

Expand Down Expand Up @@ -414,11 +419,14 @@ class TrustChainService(
val subordinateStatementJwt = context.httpResolver.get(subordinateStatementEndpoint)
val decodedSubordinateStatement = decodeJWTComponents(subordinateStatementJwt)

val subordinateStatementKey = findKeyInJwks(
decodedAuthorityConfiguration.payload["jwks"]?.jsonObject?.get("keys")?.jsonArray ?: return null,
decodedSubordinateStatement.header.kid,
context.json
) ?: return null
val subordinateStatementKey =
decodedAuthorityConfiguration.payload["jwks"]?.jsonObject?.get("keys")?.jsonArray?.let { array ->
findKeyInJwks(
decodeJwksArray(array),
decodedSubordinateStatement.header.kid,
context.json
)
} ?: return null

context.jwtService.verifyJwt(subordinateStatementJwt, subordinateStatementKey)

Expand All @@ -431,7 +439,6 @@ class TrustChainService(
val jwks = subordinateStatement.jwks
val keys = jwks.propertyKeys ?: return null
if (!checkKidInJwks(keys, lastStatementKid)) return null

return Pair(subordinateStatementJwt, subordinateStatement)
}

Expand Down Expand Up @@ -474,24 +481,33 @@ class TrustChainService(

private suspend fun verifySignatureWithOwnJwks(jwt: String): Boolean {
val decoded = decodeJWTComponents(jwt)
val key = findKeyInJwks(
decoded.payload["jwks"]?.jsonObject?.get("keys")?.jsonArray ?: return false,
decoded.header.kid,
context.json
) ?: return false
val key = decoded.payload["jwks"]?.jsonObject?.get("keys")?.jsonArray?.let { array ->
findKeyInJwks(
decodeJwksArray(array),
decoded.header.kid,
context.json
)
} ?: return false
return context.cryptoService.verify(jwt, key)

}

private suspend fun verifySignatureWithNextJwks(jwt: String, nextJwt: String): Boolean {
val decoded = decodeJWTComponents(jwt)
val decodedNext = decodeJWTComponents(nextJwt)
val key = findKeyInJwks(
decodedNext.payload["jwks"]?.jsonObject?.get("keys")?.jsonArray ?: return false,
decoded.header.kid,
context.json
) ?: return false
val key = decodedNext.payload["jwks"]?.jsonObject?.get("keys")?.jsonArray?.let { array ->
findKeyInJwks(
decodeJwksArray(array),
decoded.header.kid,
context.json
)
} ?: return false
return context.cryptoService.verify(jwt, key)
}

private fun decodeJwksArray(jsonArray: kotlinx.serialization.json.JsonArray): Array<BaseJwk> {
return context.json.decodeFromJsonElement(ArraySerializer(BaseJwk.serializer()), jsonArray)
}
}

class SimpleCache<K, V> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ import com.sphereon.oid.fed.client.services.entityConfigurationStatementService.
import com.sphereon.oid.fed.client.types.TrustMarkValidationResponse
import com.sphereon.oid.fed.openapi.models.EntityConfigurationStatement
import com.sphereon.oid.fed.openapi.models.Jwt
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import com.sphereon.oid.fed.openapi.models.TrustMarkOwner
import kotlinx.serialization.json.jsonPrimitive

private val logger = TrustMarkServiceConst.LOG
Expand Down Expand Up @@ -72,14 +70,14 @@ class TrustMarkService(
logger.debug("Trust Mark signature verified successfully")

// 6. Check if Trust Mark is recognized in Trust Anchor's configuration
val trustMarkOwners = trustAnchorConfig.metadata?.get("trust_mark_owners")?.jsonObject
val trustMarkOwners = trustAnchorConfig.trustMarkOwners?.filterKeys { it == trustMarkId }
if (trustMarkOwners != null) {
logger.debug("Validating Trust Mark using trust_mark_owners claim")
return validateWithTrustMarkOwners(trustMarkId, trustMarkOwners, decodedTrustMark)
}

// 7. Check if Trust Mark issuer is in Trust Anchor's trust_mark_issuers
val trustMarkIssuers = trustAnchorConfig.metadata?.get("trust_mark_issuers")?.jsonObject
val trustMarkIssuers = trustAnchorConfig.trustMarkIssuers
if (trustMarkIssuers != null) {
logger.debug("Validating Trust Mark using trust_mark_issuers claim")
return validateWithTrustMarkIssuers(
Expand All @@ -104,23 +102,25 @@ class TrustMarkService(

private suspend fun validateWithTrustMarkOwners(
trustMarkId: String,
trustMarkOwners: JsonObject,
trustMarkOwners: Map<String, TrustMarkOwner>,
decodedTrustMark: Jwt
): TrustMarkValidationResponse {
val ownerClaims = trustMarkOwners[trustMarkId]?.jsonObject
val ownerClaims = trustMarkOwners[trustMarkId]
?: return TrustMarkValidationResponse(false, "Trust Mark identifier not found in trust_mark_owners")

// Verify delegation claim exists
val delegation = decodedTrustMark.payload["delegation"]?.jsonPrimitive?.content
?: return TrustMarkValidationResponse(false, "Trust Mark missing required delegation claim")

// Verify delegation JWT signature with owner's JWKS
val ownerJwks = ownerClaims["jwks"]?.jsonObject
val ownerJwks = ownerClaims.jwks
?: return TrustMarkValidationResponse(false, "No JWKS found for Trust Mark owner")


val decodedDelegation = decodeJWTComponents(delegation)

val delegationKey = findKeyInJwks(
ownerJwks["keys"]?.jsonArray ?: return TrustMarkValidationResponse(false, "Invalid JWKS format"),
ownerJwks,
decodedDelegation.header.kid, context.json
) ?: return TrustMarkValidationResponse(false, "Delegation signing key not found in owner's JWKS")

Expand All @@ -129,7 +129,7 @@ class TrustMarkService(
}

// Verify delegation issuer matches owner's sub
val ownerSub = ownerClaims["sub"]?.jsonPrimitive?.content
val ownerSub = ownerClaims.sub
?: return TrustMarkValidationResponse(false, "Trust Mark owner missing sub claim")

if (decodedDelegation.payload["iss"]?.jsonPrimitive?.content != ownerSub) {
Expand All @@ -139,21 +139,21 @@ class TrustMarkService(
return TrustMarkValidationResponse(true)
}

private suspend fun validateWithTrustMarkIssuers(
private fun validateWithTrustMarkIssuers(
trustMarkId: String,
trustMarkIssuers: JsonObject,
trustMarkIssuers: Map<String, Array<String>>,
decodedTrustMark: Jwt
): TrustMarkValidationResponse {
val issuerClaims = trustMarkIssuers[trustMarkId]?.jsonObject
val issuerClaims = trustMarkIssuers[trustMarkId]
?: return TrustMarkValidationResponse(false, "Trust Mark identifier not found in trust_mark_issuers")

// Verify Trust Mark issuer is authorized
val trustMarkIssuer = decodedTrustMark.payload["iss"]?.jsonPrimitive?.content
?: return TrustMarkValidationResponse(false, "Trust Mark missing required issuer claim")

val isAuthorizedIssuer = issuerClaims["issuers"]?.jsonArray?.any {
it.jsonPrimitive.content == trustMarkIssuer
} ?: false
val isAuthorizedIssuer = issuerClaims.any { issuer ->
issuer == trustMarkIssuer
}

if (!isAuthorizedIssuer) {
return TrustMarkValidationResponse(false, "Trust Mark issuer not authorized")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package com.sphereon.oid.fed.client.types

import com.sphereon.oid.fed.openapi.models.Jwk
import com.sphereon.oid.fed.openapi.models.BaseJwk
import kotlin.js.JsExport

@JsExport.Ignore
interface ICryptoService {
suspend fun verify(
jwt: String,
key: Jwk
key: BaseJwk
): Boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import com.sphereon.oid.fed.client.mapper.InvalidJwtException
import com.sphereon.oid.fed.client.mockResponses.mockResponses
import com.sphereon.oid.fed.client.types.ICryptoService
import com.sphereon.oid.fed.logger.Logger
import com.sphereon.oid.fed.openapi.models.Jwk
import com.sphereon.oid.fed.openapi.models.BaseJwk
import io.ktor.client.*
import io.ktor.client.engine.mock.*
import io.ktor.client.plugins.*
Expand All @@ -16,7 +16,7 @@ import kotlin.test.*
import kotlin.time.Duration.Companion.seconds

object TestCryptoService : ICryptoService {
override suspend fun verify(jwt: String, key: Jwk): Boolean {
override suspend fun verify(jwt: String, key: BaseJwk): Boolean {
return true
}
}
Expand Down
Loading

0 comments on commit 0272256

Please sign in to comment.