diff --git a/modules/openid-federation-common/build.gradle.kts b/modules/openid-federation-common/build.gradle.kts index 62762b20..33e73307 100644 --- a/modules/openid-federation-common/build.gradle.kts +++ b/modules/openid-federation-common/build.gradle.kts @@ -52,6 +52,7 @@ kotlin { implementation("io.ktor:ktor-client-core:$ktorVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.0") + implementation("com.nimbusds:nimbus-jose-jwt:9.40") } } val commonTest by getting { diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwks/JWKSGenerator.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwks/JWKSGenerator.kt new file mode 100644 index 00000000..aa2f732e --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/jwks/JWKSGenerator.kt @@ -0,0 +1,30 @@ +package com.sphereon.oid.fed.jwks + +import com.nimbusds.jose.jwk.JWK +import com.nimbusds.jose.jwk.JWKSet +import com.nimbusds.jose.jwk.KeyUse +import com.nimbusds.jose.jwk.gen.RSAKeyGenerator +import com.sphereon.oid.fed.kms.AbstractKeyStore +import java.util.* + +class JWKSGenerator ( + val kms: AbstractKeyStore +) { + fun generateJWKS(kid: String? = null): JWK { + val jwk = RSAKeyGenerator(2048) + .keyUse(KeyUse.SIGNATURE) + .keyID(kid ?: UUID.randomUUID().toString()) + .generate() + kms.importKey(jwk) + return jwk.toPublicJWK() + } + + fun getJWKSet(vararg kid: String): JWKSet { + val keys = kms.listKeys(*kid) + return JWKSet(keys.map { it.toPublicJWK() }) + } + + fun sign(kid: String, payload: String): String { + return kms.sign(kid, payload) + } +} diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/kms/AbstractKeyStore.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/kms/AbstractKeyStore.kt new file mode 100644 index 00000000..e2134dd3 --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/kms/AbstractKeyStore.kt @@ -0,0 +1,11 @@ +package com.sphereon.oid.fed.kms + +import com.nimbusds.jose.jwk.JWK + +interface AbstractKeyStore { + fun importKey(key: JWK): Boolean + fun getKey(kid: String): JWK? + fun deleteKey(kid: String): Boolean + fun listKeys(vararg kid: String): List + fun sign(kid: String, payload: String): String +} \ No newline at end of file diff --git a/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/kms/MemoryKeyStore.kt b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/kms/MemoryKeyStore.kt new file mode 100644 index 00000000..c4ebd0e3 --- /dev/null +++ b/modules/openid-federation-common/src/commonMain/kotlin/com/sphereon/oid/fed/common/kms/MemoryKeyStore.kt @@ -0,0 +1,50 @@ +package com.sphereon.oid.fed.kms + +import com.nimbusds.jose.JWSAlgorithm +import com.nimbusds.jose.JWSHeader +import com.nimbusds.jose.crypto.RSASSASigner +import com.nimbusds.jose.jwk.JWK +import com.nimbusds.jose.jwk.RSAKey +import com.nimbusds.jwt.JWTClaimsSet +import com.nimbusds.jwt.SignedJWT +import java.util.concurrent.ConcurrentHashMap + +class MemoryKeyStore : AbstractKeyStore { + + private val keyStore = ConcurrentHashMap() + + override fun importKey(key: JWK): Boolean { + if (key.keyID == null) throw IllegalArgumentException("Key ID cannot be null") + keyStore[key.keyID] = key + return keyStore.containsKey(key.keyID) + } + + override fun getKey(kid: String): JWK? { + return keyStore[kid] + } + + override fun deleteKey(kid: String): Boolean { + return keyStore.remove(kid) != null + } + + override fun listKeys(vararg kid: String): List { + if (kid.isNotEmpty()) { + return kid.mapNotNull { keyStore[it] } + } + return keyStore.values.toList() + } + + override fun sign(kid: String, payload: String): String { + val privateKey = (this.getKey(kid) as RSAKey).toRSAPrivateKey() + + val claims = JWTClaimsSet.parse(payload) + + val signer = RSASSASigner(privateKey) + val jwt = SignedJWT( + JWSHeader.Builder(JWSAlgorithm.RS256).keyID(kid).build(), + claims + ) + jwt.sign(signer) + return jwt.serialize() + } +} diff --git a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/jwks/JWKSGeneratorTest.kt b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/jwks/JWKSGeneratorTest.kt new file mode 100644 index 00000000..1071261c --- /dev/null +++ b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/jwks/JWKSGeneratorTest.kt @@ -0,0 +1,47 @@ +package com.sphereon.oid.fed.jwks + +import com.sphereon.oid.fed.kms.MemoryKeyStore +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + + +class JWKSGenerationTest { + + private lateinit var jwksGenerator: JWKSGenerator + + @BeforeEach + fun setUp() { + jwksGenerator = JWKSGenerator(MemoryKeyStore()) + } + + @Test + fun `it should generate the JWKS` () { + assertNotNull(jwksGenerator.generateJWKS()) + } + + @Test + fun `It should generate JWKS with all keys` () { + jwksGenerator.generateJWKS() + jwksGenerator.generateJWKS() + assertTrue(jwksGenerator.getJWKSet().size() == 2) + } + + @Test + fun `It should generate JWKS with selected keys` () { + val keyOne = jwksGenerator.generateJWKS() + val keyTwo = jwksGenerator.generateJWKS() + jwksGenerator.generateJWKS() + jwksGenerator.generateJWKS() + assertTrue(jwksGenerator.getJWKSet(keyOne.keyID, keyTwo.keyID).size() == 2) + } + + @Test + fun `It should sign a JWT` () { + val key = jwksGenerator.generateJWKS() + val payload = "{\"iss\":\"test\",\"sub\":\"test\"}" + assertTrue(jwksGenerator.sign(key.keyID, payload).startsWith("ey")) + } +} \ No newline at end of file diff --git a/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/kms/MemoryKeyStoreTest.kt b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/kms/MemoryKeyStoreTest.kt new file mode 100644 index 00000000..e8f1f957 --- /dev/null +++ b/modules/openid-federation-common/src/commonTest/kotlin/com/sphereon/oid/fed/common/kms/MemoryKeyStoreTest.kt @@ -0,0 +1,51 @@ +package com.sphereon.oid.fed.jwks + +import com.nimbusds.jose.jwk.KeyUse +import com.nimbusds.jose.jwk.gen.RSAKeyGenerator +import com.sphereon.oid.fed.kms.MemoryKeyStore +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.util.* + +class MemoryKeyStoreTest { + + lateinit var kms: MemoryKeyStore + lateinit var keyId: String + + @BeforeEach + fun setUp() { + kms = MemoryKeyStore() + keyId = UUID.randomUUID().toString() + val jwk = RSAKeyGenerator(2048) + .keyUse(KeyUse.SIGNATURE) + .keyID(keyId) + .generate() + kms.importKey(jwk) + } + + @Test + fun `It should import a key` () { + val jwk = RSAKeyGenerator(2048) + .keyUse(KeyUse.SIGNATURE) + .keyID(UUID.randomUUID().toString()) + .generate() + assertTrue(kms.importKey(jwk)) + } + + @Test + fun `It should retrieve a key` () { + assertNotNull(kms.getKey(keyId)) + } + + @Test + fun `It should retrieve a list of keys` () { + assertTrue(kms.listKeys().size == 1) + } + + @Test + fun `It should delete a key` () { + assertTrue(kms.deleteKey(keyId)) + } +} \ No newline at end of file