diff --git a/build.gradle.kts b/build.gradle.kts index 023250d77..dbc1f370d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -115,6 +115,9 @@ dependencies { testImplementation("io.kotest:kotest-assertions-core:5.5.5") testImplementation("io.kotest:kotest-assertions-json:5.5.5") + testImplementation(kotlin("test")) + testImplementation("org.junit.jupiter:junit-jupiter-params:5.9.0") + } tasks.withType { diff --git a/src/main/kotlin/id/walt/services/jwt/WaltIdJwtService.kt b/src/main/kotlin/id/walt/services/jwt/WaltIdJwtService.kt index df35734a1..98f196957 100644 --- a/src/main/kotlin/id/walt/services/jwt/WaltIdJwtService.kt +++ b/src/main/kotlin/id/walt/services/jwt/WaltIdJwtService.kt @@ -66,21 +66,6 @@ open class WaltIdJwtService : JwtService() { return jweObj.payload.toString() } - private fun createSignedJwt(jwsAlgorithm: JWSAlgorithm, keyAlias: String, claimsSet: JWTClaimsSet, includeJwk: JWK?) = - SignedJWT( - JWSHeader - .Builder(jwsAlgorithm) - .keyID(keyAlias) - .type(JOSEObjectType.JWT) - .apply { - includeJwk?.let { jwk(it) } - }.build(), - claimsSet - )/*.also { - // log.debug { "Created signable JWT object: $it." } - }*/ - - override fun sign( keyAlias: String, // verification method payload: String? @@ -110,44 +95,66 @@ open class WaltIdJwtService : JwtService() { null } - val signedJwt = when (issuerKey.algorithm) { - KeyAlgorithm.EdDSA_Ed25519 -> { - val jwt = createSignedJwt(JWSAlgorithm.EdDSA, keyAlias, claimsSet, includeJwk) + val serializedSignedJwt = createSignedJWT(issuerKey, keyAlias, claimsSet, includeJwk).serialize() + log.debug { "Signed JWT: $serializedSignedJwt" } + return serializedSignedJwt + } - //jwt.sign(Ed25519Signer(issuerKey.toOctetKeyPair())) - jwt.sign(LdSigner.JwsLtSigner(issuerKey.keyId)) - jwt - } + private fun createSignedJWT( + issuerKey: Key, keyAlias: String, claimsSet: JWTClaimsSet, includeJwk: JWK? + ): SignedJWT = when (issuerKey.algorithm) { + KeyAlgorithm.EdDSA_Ed25519 -> { + val jwt = createSignableJwt(JWSAlgorithm.EdDSA, keyAlias, claimsSet, includeJwk) - KeyAlgorithm.ECDSA_Secp256k1 -> { - val jwt = createSignedJwt(JWSAlgorithm.ES256K, keyAlias, claimsSet, includeJwk) + //jwt.sign(Ed25519Signer(issuerKey.toOctetKeyPair())) + jwt.sign(LdSigner.JwsLtSigner(issuerKey.keyId)) + jwt + } - val jwsSigner = ECDSASigner(ECPrivateKeyHandle(issuerKey.keyId), Curve.SECP256K1) - jwsSigner.jcaContext.provider = provider - jwt.sign(jwsSigner) - jwt - } + KeyAlgorithm.ECDSA_Secp256k1 -> { + val jwt = createSignableJwt(JWSAlgorithm.ES256K, keyAlias, claimsSet, includeJwk) - KeyAlgorithm.ECDSA_Secp256r1 -> { - val jwt = createSignedJwt(JWSAlgorithm.ES256, keyAlias, claimsSet, includeJwk) + val jwsSigner = ECDSASigner(ECPrivateKeyHandle(issuerKey.keyId), Curve.SECP256K1) + jwsSigner.jcaContext.provider = provider + jwt.sign(jwsSigner) + jwt + } - val jwsSigner = ECDSASigner(ECPrivateKeyHandle(issuerKey.keyId), Curve.P_256) - jwsSigner.jcaContext.provider = provider - jwt.sign(jwsSigner) - jwt - } + KeyAlgorithm.ECDSA_Secp256r1 -> { + val jwt = createSignableJwt(JWSAlgorithm.ES256, keyAlias, claimsSet, includeJwk) - else -> { - log.error { "Algorithm ${issuerKey.algorithm} not supported" } - throw UnsupportedOperationException("Algorithm ${issuerKey.algorithm} not supported") - } + val jwsSigner = ECDSASigner(ECPrivateKeyHandle(issuerKey.keyId), Curve.P_256) + jwsSigner.jcaContext.provider = provider + jwt.sign(jwsSigner) + jwt } - val serializedSignedJwt = signedJwt.serialize() - log.debug { "Signed JWT: $serializedSignedJwt" } - return serializedSignedJwt + KeyAlgorithm.RSA -> { + val jwt = createSignableJwt(JWSAlgorithm.RS256, keyAlias, claimsSet, includeJwk) + jwt.sign(LdSigner.JwsLtSigner(issuerKey.keyId)) + jwt + } + + else -> { + log.error { "Algorithm ${issuerKey.algorithm} not supported" } + throw UnsupportedOperationException("Algorithm ${issuerKey.algorithm} not supported") + } } + private fun createSignableJwt(jwsAlgorithm: JWSAlgorithm, keyAlias: String, claimsSet: JWTClaimsSet, includeJwk: JWK?) = + SignedJWT( + JWSHeader + .Builder(jwsAlgorithm) + .keyID(keyAlias) + .type(JOSEObjectType.JWT) + .apply { + includeJwk?.let { jwk(it) } + }.build(), + claimsSet + )/*.also { + // log.debug { "Created signable JWT object: $it." } + }*/ + override fun verify(token: String): JwtVerificationResult { log.debug { "Verifying token: $token" } val jwt = SignedJWT.parse(token) @@ -163,28 +170,32 @@ open class WaltIdJwtService : JwtService() { val verifierKey = keyService.load(keyAlias) - val res = when (verifierKey.algorithm) { - KeyAlgorithm.EdDSA_Ed25519 -> jwt.verify(Ed25519Verifier(keyService.toEd25519Jwk(verifierKey))) - KeyAlgorithm.ECDSA_Secp256k1 -> { - val verifier = ECDSAVerifier(PublicKeyHandle(verifierKey.keyId, verifierKey.getPublicKey() as ECPublicKey)) - verifier.jcaContext.provider = provider - jwt.verify(verifier) - } + val res = verifyJwt(verifierKey, jwt) - KeyAlgorithm.ECDSA_Secp256r1 -> { - val verifier = ECDSAVerifier(PublicKeyHandle(verifierKey.keyId, verifierKey.getPublicKey() as ECPublicKey)) - verifier.jcaContext.provider = provider - jwt.verify(verifier) - } + log.debug { "JWT verified returned: $res" } + return JwtVerificationResult(res) + } - else -> { - log.error { "Algorithm ${verifierKey.algorithm} not supported" } - throw UnsupportedOperationException("Algorithm ${verifierKey.algorithm} not supported") - } + private fun verifyJwt(verifierKey: Key, jwt: SignedJWT): Boolean = when (verifierKey.algorithm) { + KeyAlgorithm.EdDSA_Ed25519 -> jwt.verify(Ed25519Verifier(keyService.toEd25519Jwk(verifierKey))) + KeyAlgorithm.ECDSA_Secp256k1 -> { + val verifier = ECDSAVerifier(PublicKeyHandle(verifierKey.keyId, verifierKey.getPublicKey() as ECPublicKey)) + verifier.jcaContext.provider = provider + jwt.verify(verifier) } - log.debug { "JWT verified returned: $res" } - return JwtVerificationResult(res) + KeyAlgorithm.ECDSA_Secp256r1 -> { + val verifier = ECDSAVerifier(PublicKeyHandle(verifierKey.keyId, verifierKey.getPublicKey() as ECPublicKey)) + verifier.jcaContext.provider = provider + jwt.verify(verifier) + } + + KeyAlgorithm.RSA -> jwt.verify(RSASSAVerifier(keyService.toRsaJwk(verifierKey))) + + else -> { + log.error { "Algorithm ${verifierKey.algorithm} not supported" } + throw UnsupportedOperationException("Algorithm ${verifierKey.algorithm} not supported") + } } override fun parseClaims(token: String): MutableMap? { diff --git a/src/main/kotlin/id/walt/services/key/KeyService.kt b/src/main/kotlin/id/walt/services/key/KeyService.kt index df87210ea..a5e286876 100644 --- a/src/main/kotlin/id/walt/services/key/KeyService.kt +++ b/src/main/kotlin/id/walt/services/key/KeyService.kt @@ -45,7 +45,7 @@ abstract class KeyService : WaltIdService() { open fun toEd25519Jwk(key: Key, jwkKeyId: String? = null): OctetKeyPair = implementation.toEd25519Jwk(key, jwkKeyId) - open fun toRsaJwk(key: Key, jwkKeyId: String?): RSAKey = implementation.toRsaJwk(key, jwkKeyId) + open fun toRsaJwk(key: Key, jwkKeyId: String? = null): RSAKey = implementation.toRsaJwk(key, jwkKeyId) open fun getEthereumAddress(keyAlias: String): String = implementation.getEthereumAddress(keyAlias) diff --git a/src/test/kotlin/id/walt/services/jwt/JwtServiceTest.kt b/src/test/kotlin/id/walt/services/jwt/JwtServiceTest.kt index 4a1eddfc7..f278f74ad 100644 --- a/src/test/kotlin/id/walt/services/jwt/JwtServiceTest.kt +++ b/src/test/kotlin/id/walt/services/jwt/JwtServiceTest.kt @@ -1,6 +1,6 @@ package id.walt.services.jwt -import com.nimbusds.jose.jwk.ECKey +import com.nimbusds.jose.jwk.JWK import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT import id.walt.crypto.KeyAlgorithm @@ -10,14 +10,22 @@ import id.walt.services.crypto.CryptoService import id.walt.services.did.DidService import id.walt.services.key.KeyService import id.walt.test.RESOURCES_PATH -import io.kotest.core.spec.style.AnnotationSpec import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.Arguments.arguments +import org.junit.jupiter.params.provider.MethodSource import java.time.Instant import java.util.* +import java.util.stream.* -class JwtServiceTest : AnnotationSpec() { +private const val customClaim = "https://self-issued.me" +private const val defaultClaim = "https://walt.id" - @Before +internal class JwtServiceTest { + + @BeforeEach fun setup() { ServiceMatrix("$RESOURCES_PATH/service-matrix.properties") } @@ -26,61 +34,90 @@ class JwtServiceTest : AnnotationSpec() { private val keyService = KeyService.getService() private val jwtService = JwtService.getService() - @Test - fun genJwtSecp256k1() { - val keyId = cryptoService.generateKey(KeyAlgorithm.ECDSA_Secp256k1) + @ParameterizedTest + @MethodSource("generateJwtNoPayloadSource") + fun generateJwtNoPayload(input: JwtTestInput, expected: Boolean) { + val keyId = cryptoService.generateKey(input.keyAlgorithm) val jwt = jwtService.sign(keyId.id) val signedJwt = SignedJWT.parse(jwt) - "ES256K" shouldBe signedJwt.header.algorithm.name + input.algorithmName shouldBe signedJwt.header.algorithm.name keyId.id shouldBe signedJwt.header.keyID - "https://walt.id" shouldBe signedJwt.jwtClaimsSet.claims["iss"] + signedJwt.jwtClaimsSet.claims["iss"] shouldBe input.claim val res1 = jwtService.verify(jwt) - res1.verified shouldBe true + res1.verified shouldBe expected } - @Test - fun genJwtEd25519() { - val keyId = cryptoService.generateKey(KeyAlgorithm.EdDSA_Ed25519) - - val jwt = jwtService.sign(keyId.id) - - val signedJwt = SignedJWT.parse(jwt) - "EdDSA" shouldBe signedJwt.header.algorithm.name - keyId.id shouldBe signedJwt.header.keyID - "https://walt.id" shouldBe signedJwt.jwtClaimsSet.claims["iss"] - - val res1 = jwtService.verify(jwt) - res1.verified shouldBe true - } - - @Test - fun genJwtCustomPayload() { - val did = DidService.create(DidMethod.ebsi, keyService.generate(KeyAlgorithm.ECDSA_Secp256k1).id) + @ParameterizedTest + @MethodSource("generateJwtCustomPayloadSource") + fun generateJwtCustomPayload(input: JwtTestInput, expected: Boolean) { + val did = DidService.create(DidMethod.ebsi, keyService.generate(input.keyAlgorithm).id) val kid = DidService.load(did).verificationMethod!![0].id - val key = keyService.toJwk(did, jwkKeyId = kid) as ECKey + val key = keyService.toJwk(did, jwkKeyId = kid) val thumbprint = key.computeThumbprint().toString() - val payload = JWTClaimsSet.Builder() - .issuer("https://self-issued.me") - .audience("redirectUri") - .subject(thumbprint) - .issueTime(Date.from(Instant.now())) - .expirationTime(Date.from(Instant.now().plusSeconds(120))) - .claim("nonce", "nonce") - .claim("sub_jwk", key.toJSONObject()) - .build().toString() + val payload = generatePayload(key, thumbprint) val jwtStr = jwtService.sign(kid, payload) val jwt = SignedJWT.parse(jwtStr) - "ES256K" shouldBe jwt.header.algorithm.name - kid shouldBe jwt.header.keyID - "https://self-issued.me" shouldBe jwt.jwtClaimsSet.claims["iss"] - thumbprint shouldBe jwt.jwtClaimsSet.claims["sub"] + jwt.header.algorithm.name shouldBe input.algorithmName + jwt.header.keyID shouldBe kid + jwt.jwtClaimsSet.claims["iss"] shouldBe input.claim + jwt.jwtClaimsSet.claims["sub"] shouldBe thumbprint - jwtService.verify(jwtStr).verified shouldBe true + jwtService.verify(jwtStr).verified shouldBe expected } + private fun generatePayload(key: JWK, thumbprint: String) = JWTClaimsSet.Builder() + .issuer(customClaim) + .audience("redirectUri") + .subject(thumbprint) + .issueTime(Date.from(Instant.now())) + .expirationTime(Date.from(Instant.now().plusSeconds(120))) + .claim("nonce", "nonce") + .claim("sub_jwk", key.toJSONObject()) + .build().toString() + + companion object { + @JvmStatic + fun generateJwtNoPayloadSource(): Stream = Stream.of( + arguments( + JwtTestInput(KeyAlgorithm.ECDSA_Secp256k1, "ES256K", defaultClaim), + true + ), + arguments( + JwtTestInput(KeyAlgorithm.EdDSA_Ed25519, "EdDSA", defaultClaim), + true + ), + arguments( + JwtTestInput(KeyAlgorithm.RSA, "RS256", defaultClaim), + true + ), + ) + + @JvmStatic + fun generateJwtCustomPayloadSource(): Stream = Stream.of( + arguments( + JwtTestInput(KeyAlgorithm.ECDSA_Secp256k1, "ES256K", customClaim), + true + ), + arguments( + JwtTestInput(KeyAlgorithm.EdDSA_Ed25519, "EdDSA", customClaim), + true + ), + arguments( + JwtTestInput(KeyAlgorithm.RSA, "RS256", customClaim), + true + ), + ) + } + + data class JwtTestInput( + val keyAlgorithm: KeyAlgorithm, + val algorithmName: String, + val claim: String, + + ) }