JJWT parse JWS token using RSA public key bytes

185 views Asked by At

Context:

My service validates JWS coming from multiple sources; each source shares a base64 key that must be used to validate their JWS. I don't have any restrictions on the algorithm they want to use to sign their JWS.

I use JJWT to parse/verify the JWS, and I noticed that the byte[] signingKey is supported only for HMAC signatures, while it fails for RSA/EC signatures:

java.lang.IllegalArgumentException: Key bytes can only be specified for HMAC signatures. Please specify a PublicKey or PrivateKey instance.
        at io.jsonwebtoken.lang.Assert.isTrue(Assert.java:35)
        at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:363)
        at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:529)
        at io.jsonwebtoken.impl.DefaultJwtParser.parseClaimsJws(DefaultJwtParser.java:589)
        at io.jsonwebtoken.impl.ImmutableJwtParser.parseClaimsJws(ImmutableJwtParser.java:173)

I found out (issue-86) that you can implement a io.jsonwebtoken.SigningKeyResolverAdapter to provide a java.security.Key after parsing the headers; doing this allows you to also know the type of algorithm, provide a Key accordingly, and to correctly verify the token signature.

Here is the code I used to retrieve an RSA public key from bytes:

final String publicKeyBase64 = "...";
final byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyBase64);
final KeyFactory keyFactory = KeyFactory.getInstance("RSA");
final X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKeyBytes);
final PublicKey publicKey = (PublicKey) keyFactory.generatePublic(x509EncodedKeySpec)

Questions:

Since this approach is not the default on the JJWT library, are there any drawbacks / problems?

Why are byte[] keys only handled for HMAC algorithms?


package com.stackoverflow.jjwt;

import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

import javax.crypto.spec.SecretKeySpec;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SigningKeyResolverAdapter;
import io.jsonwebtoken.security.Keys;

public class Test {
    public static void main(final String[] args) {
        final KeyPair pair = Keys.keyPairFor(SignatureAlgorithm.RS512);
        final PrivateKey privateKey = pair.getPrivate();
        final PublicKey publicKey = pair.getPublic();

        // this is the public key in base64 that a source may share with me
        final String publicKeyBase64 = Base64.getEncoder()
                .encodeToString(publicKey.getEncoded());

        // this is the token that my service need to verify
        final String token = Jwts.builder()
                .setSubject("Hurray!")
                .signWith(privateKey)
                .compact();

        decode(token, publicKeyBase64);
        // ↳ Exception: Key bytes can only be specified for HMAC signatures. Please specify a PublicKey or PrivateKey instance.

        decodeWithCustomResolver(token, publicKeyBase64);
        // ↳ Subject: Hurray!
    }

    public static void decode(final String token, final String publicKeyBase64) {
        try {
            final byte[] publicKeyBytes = Base64.getDecoder()
                    .decode(publicKeyBase64);
            final String subject = Jwts.parserBuilder()
                    .setSigningKey(publicKeyBytes)
                    .build()
                    .parseClaimsJws(token)
                    .getBody()
                    .getSubject();
            System.out.println("Subject: " + subject);
        } catch (final Exception e) {
            System.err.println("Exception: " + e.getMessage());
            e.printStackTrace();
            // java.lang.IllegalArgumentException: Key bytes can only be specified for HMAC signatures. Please specify a PublicKey or PrivateKey instance.
            //     at io.jsonwebtoken.lang.Assert.isTrue(Assert.java:35)
            //     at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:363)
            //     at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:529)
            //     at io.jsonwebtoken.impl.DefaultJwtParser.parseClaimsJws(DefaultJwtParser.java:589)
            //     at io.jsonwebtoken.impl.ImmutableJwtParser.parseClaimsJws(ImmutableJwtParser.java:173)
            //     at com.stackoverflow.jjwt.Test.decode(Test.java:52)
            //     at com.stackoverflow.jjwt.Test.main(Test.java:38)
        }
    }

    public static void decodeWithCustomResolver(final String token, final String publicKeyBase64) {
        final SigningKeyResolverAdapter resolver = new SigningKeyResolverAdapter() {
            @Override
            public Key resolveSigningKey(final JwsHeader header, final Claims claims) {
                final SignatureAlgorithm alg = SignatureAlgorithm.forName(header.getAlgorithm());
                if (alg.isHmac()) {
                    // cannot be used for asymmetric key algorithms (RSA, Elliptic Curve)
                    final byte[] keyBytes = this.resolveSigningKeyBytes(header, claims);
                    return new SecretKeySpec(keyBytes, alg.getJcaName());
                } else if (alg.isRsa()) {
                    try {
                        final byte[] publicKeyBytes = Base64.getDecoder()
                                .decode(publicKeyBase64);
                        final KeyFactory keyFactory = KeyFactory.getInstance("RSA");
                        final X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKeyBytes);
                        return (PublicKey) keyFactory.generatePublic(x509EncodedKeySpec);
                    } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
                        throw new IllegalArgumentException("RSA found, but with invalid key", e);
                    }
                }
                throw new IllegalArgumentException("Elliptic Curve not supported yet"); // etc.
            }
        };
        try {
            final String subject = Jwts.parserBuilder()
                    .setSigningKeyResolver(resolver)
                    .build()
                    .parseClaimsJws(token)
                    .getBody()
                    .getSubject();
            System.out.println("Subject: " + subject);
            // ↳ Subject: Hurray!
        } catch (final Exception e) {
            System.err.println("Exception: " + e.getMessage());
        }
    }
}
0

There are 0 answers