Skip to content

Commit

Permalink
fix: added rsa implementation for jwt sign and verify
Browse files Browse the repository at this point in the history
  • Loading branch information
mikeplotean committed Jun 28, 2023
1 parent f74aa88 commit 51c9fa6
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 105 deletions.
3 changes: 3 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Test> {
Expand Down
133 changes: 72 additions & 61 deletions src/main/kotlin/id/walt/services/jwt/WaltIdJwtService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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)
Expand All @@ -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<String, Any>? {
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/id/walt/services/key/KeyService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
123 changes: 80 additions & 43 deletions src/test/kotlin/id/walt/services/jwt/JwtServiceTest.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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")
}
Expand All @@ -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<Arguments> = 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<Arguments> = 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,

)
}

0 comments on commit 51c9fa6

Please sign in to comment.