jwtk / jjwt

Java JWT: JSON Web Token for Java and Android
Apache License 2.0
10.17k stars 1.32k forks source link

PS512 validation not working on Android #959

Closed artkoenig closed 1 month ago

artkoenig commented 1 month ago

I use the default JWTs generated by https://jwt.io/. One using the RS512 algorithm and the same token using the PS512 algorithm. I'd like to verify the token using the provided public key in my Android app. Here are my gradle includes:

jsonwebtoken-api = { group = "io.jsonwebtoken", name = "jjwt-api", version = "0.12.6" }
jsonwebtoken-impl = { group = "io.jsonwebtoken", name = "jjwt-impl", version = "0.12.6" }
jsonwebtoken-json = { group = "io.jsonwebtoken", name = "jjwt-jackson", version = "0.12.6" }
bouncycastle-prov = { group = "org.bouncycastle", name = "bcprov-jdk15to18", version = "1.78.1" }
bouncycastle-bcpkix = { group = "org.bouncycastle", name = "bcpkix-jdk15to18", version = "1.78.1" }
    implementation(libs.jsonwebtoken.api)
    runtimeOnly(libs.jsonwebtoken.impl)
    runtimeOnly(libs.jsonwebtoken.json) {
        exclude(group = "org.json", module = "json")
    }
    implementation(libs.bouncycastle.prov)
    implementation(libs.bouncycastle.bcpkix)

The MainActivity replaces the "BC" security provider by:

    companion object {
        init {
            Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
            Security.addProvider(BouncyCastleProvider())
        }
    }

Here is the code for reading the public key (for test purposes stored as local variable):

    val publicKey:String = "..."
    private fun stringToPublicKey(): PublicKey {
        val parser = PEMParser(StringReader(publicKey))
        val converter = JcaPEMKeyConverter()
        val publicKeyInfo: SubjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(parser.readObject())
        return (converter.getPublicKey(publicKeyInfo) as RSAPublicKey)
    }

Here is the code for token validation:

val jwtPS512 = "..."
try {
    val parser = Jwts.parser().verifyWith(stringToPublicKey()).build()
    val test = parser.parse(jwtPS512)
    Log.i("", "$test")
} catch (e: Throwable) {
    Log.e("", "", e)
}

Here is the link to the full project example: https://github.com/artkoenig/JWT-Test

The validation works for the RS512 algorithm, but for the PS512 algorithm I get the following error:

io.jsonwebtoken.security.SignatureException: JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.

Please let me know if I missed anything.

bdemers commented 1 month ago

Thanks for the report @artkoenig Can you include the full stack trace?

artkoenig commented 1 month ago
io.jsonwebtoken.security.SignatureException: JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.
at io.jsonwebtoken.impl.DefaultJwtParser.verifySignature(DefaultJwtParser.java:340)
at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:579)
at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:364)
at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:94)
at io.jsonwebtoken.impl.io.AbstractParser.parse(AbstractParser.java:36)
at io.jsonwebtoken.impl.io.AbstractParser.parse(AbstractParser.java:29)
at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:94)
at com.example.jwt.MainActivity.onCreate(MainActivity.kt:64)
at android.app.Activity.performCreate(Activity.java:8595)
at android.app.Activity.performCreate(Activity.java:8573)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1456)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3764)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3922)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:139)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:96)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2443)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:205)
at android.os.Looper.loop(Looper.java:294)
at android.app.ActivityThread.main(ActivityThread.java:8177)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)
bdemers commented 1 month ago

Try forcing the BC provider by doing something like:

val jwtPS512 = "..."
try {
    val parser = Jwts.parser()
+        .provider(BouncyCastleProvider())
        .verifyWith(stringToPublicKey())
        .build()
    val test = parser.parse(jwtPS512)
    Log.i("", "$test")
} catch (e: Throwable) {
    Log.e("", "", e)
}

If that doesn't help, does your reproduce case require running the tests on an Android device? Try sticking a break point at DefaultJwtParser:336 and check if there is anything suspicious with the provider that is used at that point.

I turned your example into a quick unit test and it seemed to parse fine (running on a normal JVM), any idea if this effects other versions of Android?

lhazlewood commented 1 month ago

To isolate Java vs Android environment issues, I just ran a Java test on JDK 8 (which does not support PS* algorithms, and therefore requires BouncyCastle to be loaded), and the test worked:

package test;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import org.junit.Test;

import java.security.KeyFactory;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.X509EncodedKeySpec;

public class PS512Test {

    private static final String PUB_PEM = "-----BEGIN PUBLIC KEY-----\n" +
            "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo\n" +
            "4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u\n" +
            "+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh\n" +
            "kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ\n" +
            "0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg\n" +
            "cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc\n" +
            "mwIDAQAB\n" +
            "-----END PUBLIC KEY-----";

    private static String pemToB64(String pem) {
        return pem.replaceAll("\\n", "")
                .replace("-----BEGIN PUBLIC KEY-----", "")
                .replace("-----BEGIN PRIVATE KEY-----", "")
                .replace("-----END PRIVATE KEY-----", "")
                .replace("-----END PUBLIC KEY-----", "");
    }

    private static RSAPublicKey pub(String pem) throws Exception {
        String encoded = pemToB64(pem);
        KeyFactory kf = KeyFactory.getInstance("RSA");
        X509EncodedKeySpec keySpecX509 = new X509EncodedKeySpec(Decoders.BASE64.decode(encoded));
        return (RSAPublicKey) kf.generatePublic(keySpecX509);
    }

    @Test
    public void test() throws Exception {
        String jwtPS512 = "eyJhbGciOiJQUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.GNhJz8YNyIT2e2kpO7jkH8K7z8lCP1Tsn3YO5_W_7BxB0U6VdoOK_1-l3Y8gWQV-XNrObDFsAdpvudTNkF_cQzZO3I3_6LdjU3iQ4NSTbJwaiaDzyaARC8hIWa55K7Hfz_m9btKOahJpqiiZ5RZNeCVC9VII4uxbuZozfC8r0aXsnmd97TH2vdpIcnzuADH_Cu_AhUSF2C1Bsk4RZe6wf_WmopP48WD3EUmZYvnaSuACtZrN3jRIymcvmtQWWOkFlAjHxjSyKYO33MgpPh1wI_jLfOUZY0S8gxylbd8LK3b0YE0jkpjOznsY8M03dAAS8V_pfzLOLB2yMrRR9e0mLA";
        Jwts.parser().verifyWith(pub(PUB_PEM)).build().parseSignedClaims(jwtPS512);
    }
}

This leads me to believe that BouncyCastle provider is not being loaded correctly (or in time?) in the Android environment before parsing the jws string.

I'm not sure why though. Still need more testing.

artkoenig commented 1 month ago
.provider(BouncyCastleProvider())

This change fixed my problem. Thank you very much!

lhazlewood commented 1 month ago
.provider(BouncyCastleProvider())

This change fixed my problem. Thank you very much!

Just note that will force ALL operations used by that parser to always use that provider. That's probably ok for Android environments however.

But the bigger question:

Why wasn't BouncyCastleProvider() replacing the Android default via the following?

companion object {
        init {
            Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
            Security.addProvider(BouncyCastleProvider())
        }
    }

That should 'just work'.

I could be wrong, but maybe it's because the sample project has this:

class MainActivity : ComponentActivity() {

instead of this:

class MainActivity : AppCompatActivity() {

as documented here: https://github.com/jwtk/jjwt?tab=readme-ov-file#bouncy-castle

?

lhazlewood commented 1 month ago

@artkoenig

Would you be able to try this:

class MainActivity : AppCompatActivity() { // ... etc ...
}

And see if that registers BC correctly so you don't need to specify it on the parser?

lhazlewood commented 1 month ago

Closing due to inactivity or reply from OP. Feel free to ask to re-open if necessary!