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());
}
}
}